Skip to content

Latest commit

 

History

History
365 lines (288 loc) · 23.1 KB

README.md

File metadata and controls

365 lines (288 loc) · 23.1 KB

@minimax-ai/code-sdk

A TypeScript SDK for driving the local MiniMax Code (mcode) agent programmatically from Node.js, modeled after the Codex TypeScript SDK. Use it to run MiniMax Code in CI/CD (fixes, reviews, code generation), embed it in internal tools or your own agents, or orchestrate multi-turn conversations from code.

Note

The SDK is in Phase 1 (0.x): the API may change between minor versions until 1.0. It is versioned independently of the CLI; see CHANGELOG.md.

How it works

  • Each run() / runStreamed() starts one mcode exec --output-format stream-json --input - --input-format json process, writes the prompt to its stdin as {"prompt": "..."}, and closes stdin. One run = one CLI process = one Turn.
  • A Thread only holds the Session id inside the SDK; later Turns continue the Session with --session <id>. The CLI's embedded runtime persists Sessions in its data directory, so a crashed host can resume a Thread from its id with resumeThread.
  • The SDK consumes only the public mcode exec contract (stdout JSONL plus exit code) and never reads runtime-internal data.
  • No runtime dependencies. The package is ESM; its exports have import and default conditions and no separate CommonJS build. CommonJS callers can still require() it, because every Node.js version in the supported range loads ES modules through require(esm).

Installation and CLI resolution

The SDK does not hard-depend on the CLI, which is large and ships native dependencies. @minimax-ai/code is an optional peer dependency:

npm install @minimax-ai/code-sdk
npm install @minimax-ai/code   # optional; a globally installed mcode works too

@minimax-ai/code runs install scripts for its native dependencies. npm 11 rejects --allow-scripts for project-scoped installs (EALLOWSCRIPTS), so approve them in the project's package.json before installing the CLI into the project:

{
  "allowScripts": {
    "@minimax-ai/code": true,
    "better-sqlite3": true
  }
}

Other package managers that gate install scripts need the equivalent approval for the same two packages.

The SDK looks for the CLI in this order:

  1. The bin constructor option (an executable, a JS entry, or a command name on PATH).
  2. An installed @minimax-ai/code resolved from the SDK's own location (where the optional peer dependency is installed). The SDK reads bin.mcode from that package.json and runs the JS entry with process.execPath.
  3. mcode on PATH.

Set resolveFromCwd: true to also search the host's working directory, after the SDK-local package and before PATH:

  • @minimax-ai/code resolved from process.cwd()/package.json.
  • The nearest node_modules/.bin/mcode (mcode.cmd / mcode.ps1 on Windows) found walking up from process.cwd(). The SDK prefers the JS entry behind the symlink or npm/pnpm shim and runs it with process.execPath, so the CLI package name does not matter.

Leave resolveFromCwd off (the default) when the working directory may be untrusted, for example a CI job checking out a pull request. A checkout can contain node_modules/.bin/mcode, and the SDK would execute it with the host environment. Pass bin, or install the CLI next to the SDK or on PATH, instead.

The SDK never starts the CLI through a shell. On Windows, mcode.cmd is followed to the JS entry it runs, which the SDK then starts with process.execPath. Supported Windows installations:

  • the official installer (install.ps1): mcode.cmd reads the current release pointer and calls releases\<release>\.mcode-launcher.cmd. The pointer is read again before every run, so a CLI self-update between Turns is picked up;
  • npm and pnpm installs, global or project-local, through their generated .cmd / .ps1 shims.

If a .cmd launcher is found but its JS entry cannot be determined, resolution fails with MCodeNotFoundError; pass the CLI's JS entry as bin.

Before the first run, each MCode instance runs mcode --version once and caches the result. A CLI older than MIN_CLI_VERSION (currently 0.6.3) fails with MCodeVersionError. Only major.minor.patch is compared, so 0.6.3-preview.N satisfies 0.6.3. 0.6.3 is the first CLI release that includes every mcode exec flag the SDK passes (--system-prompt-file, --append-system-prompt-file and --mode landed there; stream-json was already present). A source build of this repository reports 0.6.5 and meets the minimum.

Compatibility

@minimax-ai/code-sdk MiniMax Code CLI (mcode) Node.js Event schemaVersion
0.1.x >=0.6.3 (MIN_CLI_VERSION) >=22.19 <23 || >=24.2 <27 1

The SDK and the CLI are released separately. Upgrading the SDK does not require upgrading the CLI as long as the CLI meets MIN_CLI_VERSION. If the CLI ever emits a new event schemaVersion, upgrade to an SDK release that supports it. The Node.js range matches the CLI's.

Quick start

import { MCode } from "@minimax-ai/code-sdk";

const mcode = new MCode({ dataDir: "/srv/mcode-data" });
const thread = mcode.startThread({
  cwd: "/path/to/repo",
  model: "minimax/MiniMax-M3",
  permission: "smart",
});

const turn = await thread.run("Diagnose and fix the failing CI job", {
  files: ["./ci.log"],
  timeoutMs: 10 * 60_000,
});
console.log(thread.id, turn.finalResponse, turn.usage);

// Continue the same Thread
await thread.run("Add unit tests for the fix");

// Resume an earlier Session
const resumed = mcode.resumeThread("mvs_xxx", { cwd: "/path/to/repo" });
await resumed.run("Continue where you left off");

Streaming:

const { events } = await thread.runStreamed("Implement the plan");
for await (const event of events) {
  switch (event.type) {
    case "item.completed":
      console.log(event.item);
      break;
    case "turn.failed":
      console.error(event.status, event.error);
      break;
    default:
      // Newer CLIs may add event types; the SDK passes them through unchanged.
      break;
  }
}

API

new MCode(options?)

Option Description
bin Explicit mcode path or command name; skips package and PATH lookup
resolveFromCwd Also search process.cwd() for @minimax-ai/code and for node_modules/.bin/mcode in parent directories. Default false. See CLI resolution
dataDir Data directory, passed to the CLI as the MINIMAX_DATA_DIR environment variable
env Child process environment. Replaces process.env when set (no merge); defaults to process.env
configPath Runtime config file, passed as --config

env replaces the environment entirely: passing only custom variables drops PATH, HOME and so on, so the CLI may fail to find Node.js or tools, or to read its credentials. To add variables, spread process.env:

const mcode = new MCode({ env: { ...process.env, FOO: "bar" } });

startThread(options?) / resumeThread(id, options?)

Thread options are passed to the CLI on every Turn of the Thread:

Option CLI flag
cwd --cwd (defaults to the host process working directory)
model / effort --model / --effort
permission --permission: smart / full / off
mode --mode: standard / lightweight, only on the Turn that creates the Session
promptMode --prompt-mode: tui / coding / work
systemPrompt --system-prompt-file (the SDK writes a temporary file)
appendSystemPrompt --append-system-prompt-file (the SDK writes a temporary file)
  • mode: 'lightweight' is only valid for a new Thread; passing it to resumeThread throws MCodeInvocationError immediately.
  • There is no ask permission: headless runs cannot ask for approval. MiniMax Code's smart / full / off policies are not equivalent to Codex's OS-level sandbox modes, and the SDK does not map between them.
  • thread.id becomes available once the first Turn emits its first event, even if that Turn fails.

thread.run(input, options?) / thread.runStreamed(input, options?)

Option Description
files Attachments, --file (repeatable); relative paths resolve against cwd
signal AbortSignal; see Cancellation
timeoutMs Passed to the CLI as --timeout <n>ms; the SDK force-stops the CLI at timeoutMs + 90s (60s CLI shutdown plus 30s startup slack). At most MAX_TIMEOUT_MS (about 24.8 days, a Node.js timer limit); larger values throw MCodeInvocationError
maxSteps --max-steps
  • run() resolves only when the status is succeeded, with a Turn (finalResponse, normalized items, usage, model, sessionId, turnId, durationMs and the full result). Every other outcome rejects; see Errors.
  • runStreamed() yields the CLI events unchanged. turn.failed is an ordinary event; the iterator throws only for process or protocol failures. Breaking out of the loop cancels the Turn.
  • A Thread runs one Turn at a time. A concurrent call throws ThreadBusyError; Turns are not queued.
  • After breaking out of the iterator, or after a protocol error, the SDK sends the CLI a cancel signal. Until the process exits (about one second for a cooperating CLI, at most the CLI's 60 second shutdown budget: SIGTERM, then SIGKILL), the Thread stays busy and further run() / runStreamed() calls throw ThreadBusyError. The for await loop you broke out of also waits for the process to exit.
  • The prompt only travels over stdin, and system prompts through temporary files (mode 0600). The files are removed before run() settles and before a runStreamed() iterator ends. If the host calls process.exit() during a Turn, the SDK's exit hook deletes those directories with rmSync. argv contains only SDK-generated flags, identifiers and paths.

Events and items

Event types match the CLI: exec.started, session.started / session.resumed, turn.started, item.started / item.updated / item.completed, turn.completed, turn.failed and exec.completed. Every stdout line must be JSON with schemaVersion === 1; anything else is a protocol error.

Turn.items are SDK-normalized items (agent_message, reasoning, tool_call):

  • Items are merged by item.id without assuming any order (events of different items interleave); the list is in order of first appearance.
  • While streaming, an item.updated identical to the previous snapshot of the same item is dropped. This applies to snapshots only, never to text deltas (contentDelta).
  • Items without an item.completed when the Turn ends are marked incomplete: true (for example, a tool that was running when the Turn was cancelled).
  • tool_call.toolCall is an @experimental RawToolCall: status is string | number (often an internal numeric enum) and input / output are tool-specific structures. Neither is covered by the compatibility promise.

usage.totalTokens = inputTokens + outputTokens and excludes cacheReadTokens / cacheWriteTokens; reasoningTokens may overlap outputTokens.

Errors

Every error extends MCodeError and carries exitCode, signal and the last 64 KiB of the CLI stderr (stderr, for diagnostics only).

When the CLI emits exec.completed, its result.status decides the error:

result.status Error
failed MCodeRunError with category from the result (config / runtime / internal)
timeout MCodeRunError with status === 'timeout'
limit_exceeded MCodeRunError with status === 'limit_exceeded'
cancelled MCodeAbortError with name === 'AbortError'

When the CLI exits without exec.completed, the exit code decides:

Exit code Error
2 (invalid invocation) MCodeInvocationError
3 MCodeRunError, status: 'failed', category: 'config'
4 MCodeRunError, status: 'failed', category: 'runtime'
6 MCodeRunError, status: 'timeout'
7 MCodeRunError, status: 'limit_exceeded'
70 MCodeRunError, status: 'failed', category: 'internal'
130 MCodeAbortError
141 (broken pipe) MCodeProtocolError
0 (success without a result) MCodeProtocolError
Any other code (e.g. 1), or a signal MCodeRunError, status: 'failed', category: 'internal'

If the SDK itself cancelled the run (signal, or breaking out of runStreamed()), a missing result is reported as MCodeAbortError unless the CLI exited with 2. If the SDK force-stopped the CLI after timeoutMs + 90s, it is MCodeRunError with status === 'timeout'.

Errors raised by the SDK itself:

Source Error
An option the SDK rejects before starting the CLI MCodeInvocationError
Non-JSON stdout or an event missing required fields MCodeProtocolError
No CLI found (includes the install command), or a found launcher whose JS entry cannot be determined (asks for bin) MCodeNotFoundError
CLI older than MIN_CLI_VERSION, or an unsupported event schemaVersion MCodeVersionError
Concurrent Turn on the same Thread ThreadBusyError

MCodeRunError carries result (the full ExecResult when exec.completed was received), items, retryable (from result.error.retryable) and sessionId; MCodeAbortError also carries sessionId. sessionId is set whenever the CLI emitted at least one event (or the Thread came from resumeThread), even without exec.completed. The SDK never retries automatically, to avoid replaying writes with unknown outcomes; call resumeThread(error.sessionId) to continue.

Cancellation, timeouts and process cleanup

  • When signal aborts, the SDK sends SIGTERM. The CLI treats SIGTERM the same way as SIGINT: it cancels the Turn, shuts the runtime down (budget 60 seconds), then writes exec.completed with status cancelled. If the process is still running after that budget, the SDK sends SIGKILL. It does not send SIGINT.

  • Ctrl+C. The CLI stays in the host's process group, so Ctrl+C delivers SIGINT to the host and the CLI together. That SIGINT already starts the CLI's cancel path and consumes its one-shot SIGINT handler. If the host also aborts the AbortSignal from its own SIGINT handler, the SDK follows with SIGTERM, which the CLI still handles as a cancel (SIGTERM is a separate handler). A second SIGINT would take the default action and kill the CLI before it writes exec.completed. The README example below aborts on SIGTERM, which does not collide with Ctrl+C.

  • timeoutMs is enforced by the CLI. The CLI starts that timer after its access check, session resolution and model selection, then ends the run with status timeout and may spend the same 60 second shutdown budget before writing the result. The SDK's timer starts at spawn and force-stops the process at timeoutMs + 90s (60s shutdown plus 30s of startup slack).

  • Only normal host exits are covered: when the host emits exit (the event loop drains or process.exit() is called), the SDK sends SIGTERM to any running CLI process, which the CLI treats as a cancel, and deletes temporary prompt directories with rmSync. A host killed by a signal such as SIGTERM or SIGINT does not emit exit, and its CLI processes keep running as orphans. The SDK installs no signal handlers and Phase 1 has no close() / dispose(). Hosts that need reliable cleanup should abort in-flight runs from their own signal handlers and wait for them, for example:

    const controller = new AbortController();
    process.once("SIGTERM", () => controller.abort());
    await thread
      .run(prompt, { signal: controller.signal })
      .catch(() => undefined);
  • After the CLI exits, the SDK keeps reading stdout / stderr for at most about two seconds. If a grandchild process inherited and holds the pipes, the SDK closes them and settles the Turn from the events already received instead of waiting for the grandchild.

  • An unconsumed runStreamed() stream still runs to completion and frees the Thread.

Warning

Windows: Node.js cannot deliver a catchable signal to a child process, so SIGTERM is a hard kill and the runtime may leave the Turn unfinished. Phase 1 only supports the hard kill; graceful cancellation needs a signal-independent cancel channel in the CLI.

Concurrency and data directories

Concurrent exec processes sharing one data directory have had limited testing. Prefer a separate dataDir per SDK instance (or per concurrent task), with authentication configured in that directory beforehand.

Not available in Phase 1

  • outputSchema (MiniMax-hosted models do not reliably follow the schema yet).
  • Approval callbacks, steering a running Turn, continuing after an interrupt, and forking.
  • A Python SDK.

中文

SDK 在 Node.js 里驱动本机 MiniMax Code(mcode)。下面只覆盖本次行为;完整说明见上文英文。

CLI 查找

默认顺序:

  1. 构造参数 bin(可执行文件、JS 入口,或 PATH 上的命令名)。
  2. 安装在 SDK 旁边的 @minimax-ai/code(可选 peer dependency)。SDK 读取其 bin.mcode,并用 process.execPath 运行该 JS 入口。
  3. PATH 上的 mcode。

resolveFromCwd 默认为 false。设为 true 后,还会在 SDK 本地安装之后、PATH 之前搜索宿主的工作目录:先从 process.cwd()/package.json 解析 @minimax-ai/code,再从 process.cwd() 向上查找最近的 node_modules/.bin/mcode(Windows 上为 mcode.cmd / mcode.ps1)。工作目录不可信时(例如 CI 检出的拉取请求)请保持关闭:检出内容可以自带 node_modules/.bin/mcode,SDK 会带着宿主环境执行它。此时应传入 bin,或把 CLI 装在 SDK 旁边 / PATH 上。

SDK 从不通过 shell 启动 CLI。要求的最低 CLI 版本是 0.6.3(MIN_CLI_VERSION):该版本起具备 SDK 传入的全部 mcode exec 参数。本仓库源码构建的 CLI 是 0.6.5,满足该下限。只比较 major.minor.patch,因此 0.6.3-preview.N 视为满足 0.6.3。

取消、超时和退出

  • AbortSignal 中止时,SDK 发送 SIGTERM。CLI 把 SIGTERM 与 SIGINT 同样视为取消:结束本轮、在 60 秒运行时关闭预算内收尾,然后写出 exec.completed(cancelled)。超过该预算进程仍在,SDK 才发送 SIGKILL。SDK 不发送 SIGINT。
  • 终端里的 Ctrl+C 会把 SIGINT 发给整个进程组,CLI 自己的一次性 SIGINT 处理函数会被用掉。若宿主也因此中止 AbortSignal,SDK 随后发送的 SIGTERM 仍走 CLI 的取消路径。再发一次 SIGINT 会直接杀死 CLI,来不及写出 exec.completed。
  • timeoutMs 由 CLI 在访问检查、会话解析和模型选择之后开始计时。SDK 从进程启动起计时,在 timeoutMs + 90s(60 秒关闭预算加 30 秒启动余量)时强制结束。
  • 宿主正常退出(事件循环排空或调用 process.exit())时,SDK 向仍在运行的 CLI 发送 SIGTERM,并用 rmSync 删除临时 system prompt 目录。宿主被 SIGTERM / SIGINT 杀死时不会发出 exit,子进程会留下。Windows 上 SIGTERM 是硬终止,无法优雅取消。

发布

@minimax-ai/code-sdk 是本仓库里第一个非 private 的工作区包。不要在包目录里直接 npm publish:prepublishOnly 会拒绝。在最新的 main 上运行 pnpm --filter @minimax-ai/code-sdk run release:check,再发布它打印出的、已经检查过的 tarball。见 RELEASING.md。