@addai/node 0.26.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,9 @@ export interface KimiInput {
11
11
  model?: string;
12
12
  /** Resume an existing session (mapped to `--session <id>`). */
13
13
  resumeSessionId?: string;
14
- /** Skip approval prompts. Maps to --yolo. */
14
+ /** Accepted for interface parity with the other adapters and deliberately
15
+ * ignored: prompt mode always runs in "auto" permission mode, and passing
16
+ * --yolo alongside --prompt is a hard error in the CLI. */
15
17
  bypassPermissions?: boolean;
16
18
  /** When set, kimi runs against a sandboxed mcp config that contains
17
19
  * ONLY these MCP servers (host's ~/.kimi/mcp.json suppressed). */
@@ -20,12 +22,12 @@ export interface KimiInput {
20
22
  * --system-prompt flag, so we prepend it to the user prompt with a
21
23
  * separator the model can recognise. */
22
24
  appendSystemPrompt?: string;
23
- /** Per-run reasoning effort. The kimi CLI has no verified reasoning-effort
24
- * flag (unlike claude `--effort` / codex `reasoning.effort`), so by
25
- * default this is NOT forwarded emitting an unsupported flag would make
26
- * every effort-tagged kimi run fail to spawn. The level IS exported as an
27
- * env var (KIMI_REASONING_EFFORT) so a kimi wrapper/config that opts in
28
- * can read it without us guessing at a CLI flag that might not exist. */
25
+ /** Per-run reasoning effort. kimi has no effort FLAG, but it does read
26
+ * KIMI_MODEL_THINKING_EFFORT from the environment, which bypasses the
27
+ * model's declared support_efforts list. (The old code set
28
+ * KIMI_REASONING_EFFORT, a name that appears nowhere in the CLI, so effort
29
+ * was simply ignored.) 'max' is migrated to 'high' by kimi itself; we send
30
+ * 'high' rather than rely on that. */
29
31
  effortLevel?: RuntimeEffortLevel;
30
32
  }
31
33
  export interface KimiHandle {
@@ -39,16 +41,58 @@ export interface KimiHandle {
39
41
  onActivity(cb: () => void): void;
40
42
  done: Promise<number>;
41
43
  }
42
- /** Write a sandbox $HOME with an empty .kimi dir so kimi's default
43
- * `~/.kimi/mcp.json` resolves to nothing. Caller is responsible for
44
- * cleanup (we drop it under os.tmpdir() so the OS reaps it). */
45
- export declare function writeKimiSandboxHome(servers: KimiMcpServer[]): {
46
- homeDir: string;
47
- mcpConfigPath: string;
48
- };
49
- /** Parse one stream-json line into 0+ RuntimeEvents. Kimi's shapes are
50
- * similar in spirit to claude's: assistant text deltas, tool calls,
51
- * turn completion, and session metadata. Anything we don't recognise
52
- * is forwarded as `kimi:<type>` opaque events. */
44
+ /**
45
+ * Write the entity's MCP servers where kimi will actually read them:
46
+ * `<workDir>/.kimi-code/mcp.json`, the last and highest-precedence of the
47
+ * three files it merges (user `~/.kimi-code/mcp.json`, project-root
48
+ * `.mcp.json`, then this one).
49
+ *
50
+ * The two alternatives are both worse. `--mcp-config-file` — what this used
51
+ * to pass is not a flag kimi has, so it took the whole run down at argument
52
+ * parsing. Pointing KIMI_CODE_HOME at a throwaway dir WOULD isolate the user
53
+ * file, but that dir also holds kimi's credentials, config.toml and session
54
+ * index: it would log the daemon out on every run. Same trap as the HOME
55
+ * override this file already backed away from.
56
+ *
57
+ * The cost is that a server configured on the host machine still merges in.
58
+ * Ours always win on key collision, and the file is rewritten on every spawn
59
+ * so a detached MCP disappears on the next run rather than lingering.
60
+ */
61
+ export declare function writeKimiProjectMcpConfig(workingDirectory: string, servers: KimiMcpServer[]): string;
62
+ /**
63
+ * Parse one stream-json line into 0+ RuntimeEvents.
64
+ *
65
+ * kimi's prompt-mode stream is OpenAI-CHAT shaped, not claude shaped: every
66
+ * content line is keyed by `role`, and only the three metadata lines carry a
67
+ * `type` at all (`system.version`, `session.resume_hint`,
68
+ * `turn.step.retrying`). The role branches therefore come first. Without them
69
+ * every real line fell through to the opaque passthrough at the bottom, which
70
+ * meant a kimi run that worked perfectly would still have delivered an empty
71
+ * reply — a second, independent bug hiding behind the argument-parsing one.
72
+ *
73
+ * Shapes, from PromptJsonWriter in the shipped bundle:
74
+ * {role:"assistant", content?:string, tool_calls?:[{id,function:{name,arguments}}]}
75
+ * {role:"tool", tool_call_id:string, content:string}
76
+ * {role:"meta", type:"...", ...}
77
+ *
78
+ * Note there is no terminal line and no usage line: the runner keys
79
+ * completion off process exit, and kimi runs report no token counts.
80
+ *
81
+ * The `type`-keyed branches below are kept for older builds, and anything
82
+ * unrecognised is still forwarded as `kimi:<type>` rather than dropped.
83
+ */
53
84
  export declare function lineToEvents(line: Record<string, unknown>): RuntimeEvent[];
85
+ /**
86
+ * The kimi command line for one prompt-mode run.
87
+ *
88
+ * Extracted so the flag set is testable without spawning anything — this is
89
+ * the surface where four invented flags shipped undetected, each one killing
90
+ * the run before a single token was generated. Every flag below is present in
91
+ * the CLI's own option table.
92
+ *
93
+ * Not here on purpose: --work-dir (kimi reads process.cwd(), which the spawn
94
+ * sets), --mcp-config-file (config comes from <cwd>/.kimi-code/mcp.json), and
95
+ * --yolo (prompt mode forces "auto" permissions and rejects the flag).
96
+ */
97
+ export declare function buildKimiArgs(input: KimiInput): string[];
54
98
  export declare function spawnKimi(input: KimiInput): KimiHandle;
@@ -1,11 +1,29 @@
1
1
  "use strict";
2
- // kimi --print --output-format stream-json adapter.
2
+ // kimi --prompt --output-format stream-json adapter.
3
3
  //
4
- // Mirrors claude-print.ts: spawns `kimi` non-interactively with a
5
- // JSONL output stream, parses each line into our normalised RuntimeEvent
6
- // shape, and resolves on exit. Kimi's stream-json shape is documented
7
- // upstream but we passthrough unknown event types as `kimi:<type>` for
8
- // safe forward-compat.
4
+ // Mirrors claude-print.ts: spawns `kimi` non-interactively with a JSONL
5
+ // output stream, parses each line into our normalised RuntimeEvent shape,
6
+ // and resolves on exit.
7
+ //
8
+ // Everything here is read out of the shipped bundle (@moonshot-ai/kimi-code
9
+ // 0.36.0, dist/main.mjs), not the docs, because the previous version of this
10
+ // file was written from the docs and got FOUR things wrong at once — every
11
+ // one of them fatal:
12
+ //
13
+ // --print does not exist. `-p, --prompt <prompt>` IS prompt
14
+ // mode and takes the prompt as its value. Every kimi
15
+ // run died at argument parsing, silently laddering to
16
+ // claude.
17
+ // --work-dir <dir> does not exist. kimi takes its work dir from
18
+ // process.cwd(), which spawn() already sets.
19
+ // --mcp-config-file does not exist. kimi merges three files; the
20
+ // highest-precedence one is <cwd>/.kimi-code/mcp.json.
21
+ // --yolo is REJECTED alongside --prompt ("Cannot combine
22
+ // --prompt with --yolo"). It is also unnecessary:
23
+ // prompt mode calls forceAuto() and runs the whole
24
+ // turn in "auto" permission mode by itself.
25
+ //
26
+ // The output format was wrong too — see lineToEvents.
9
27
  //
10
28
  // Reference: https://moonshotai.github.io/kimi-cli/en/reference/kimi-command.html
11
29
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -42,24 +60,66 @@ var __importStar = (this && this.__importStar) || (function () {
42
60
  };
43
61
  })();
44
62
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.writeKimiSandboxHome = writeKimiSandboxHome;
63
+ exports.writeKimiProjectMcpConfig = writeKimiProjectMcpConfig;
46
64
  exports.lineToEvents = lineToEvents;
65
+ exports.buildKimiArgs = buildKimiArgs;
47
66
  exports.spawnKimi = spawnKimi;
48
67
  const child_process_1 = require("child_process");
49
68
  const fs = __importStar(require("fs"));
50
- const os = __importStar(require("os"));
51
69
  const path = __importStar(require("path"));
52
70
  const kimi_binary_1 = require("./kimi-binary");
71
+ const harness_registry_1 = require("./harness-registry");
53
72
  const win_1 = require("./win");
54
73
  const events_1 = require("./events");
55
74
  const think_split_1 = require("./think-split");
56
75
  const mcp_headers_1 = require("./mcp-headers");
57
- /** Write a sandbox $HOME with an empty .kimi dir so kimi's default
58
- * `~/.kimi/mcp.json` resolves to nothing. Caller is responsible for
59
- * cleanup (we drop it under os.tmpdir() so the OS reaps it). */
60
- function writeKimiSandboxHome(servers) {
61
- const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-kimi-'));
62
- fs.mkdirSync(path.join(homeDir, '.kimi'), { mode: 0o700 });
76
+ /**
77
+ * The effort level to send kimi, or null to send nothing.
78
+ *
79
+ * KIMI_MODEL_THINKING_EFFORT is a real kimi env var, but it exists precisely to
80
+ * BYPASS the model's declared support_efforts — kimi forwards the string to the
81
+ * provider verbatim and lets the API judge it. Setting it unconditionally is
82
+ * therefore not a safe default: an entity on medium effort got
83
+ *
84
+ * provider.api_error: 400 Invalid request Error
85
+ *
86
+ * four seconds into the run, every run, because the model behind kimi takes no
87
+ * thinking effort at all.
88
+ *
89
+ * The harness registry already records which levels a harness supports, and
90
+ * kimi's list is empty — so the registry decides, and an empty list means the
91
+ * variable is never set. If kimi gains effort support, updating the registry is
92
+ * enough; nothing here needs to change.
93
+ */
94
+ function kimiEffort(level) {
95
+ const supported = (0, harness_registry_1.harness)('kimi')?.efforts ?? [];
96
+ if (supported.length === 0)
97
+ return null;
98
+ // 'max' was retired by kimi (it migrates a stored 'max' to 'high') and it
99
+ // never had our 'xhigh', so both collapse to the highest level it declares.
100
+ const wanted = level === 'max' || level === 'xhigh' ? 'high' : level;
101
+ return supported.includes(wanted) ? wanted : null;
102
+ }
103
+ /**
104
+ * Write the entity's MCP servers where kimi will actually read them:
105
+ * `<workDir>/.kimi-code/mcp.json`, the last and highest-precedence of the
106
+ * three files it merges (user `~/.kimi-code/mcp.json`, project-root
107
+ * `.mcp.json`, then this one).
108
+ *
109
+ * The two alternatives are both worse. `--mcp-config-file` — what this used
110
+ * to pass — is not a flag kimi has, so it took the whole run down at argument
111
+ * parsing. Pointing KIMI_CODE_HOME at a throwaway dir WOULD isolate the user
112
+ * file, but that dir also holds kimi's credentials, config.toml and session
113
+ * index: it would log the daemon out on every run. Same trap as the HOME
114
+ * override this file already backed away from.
115
+ *
116
+ * The cost is that a server configured on the host machine still merges in.
117
+ * Ours always win on key collision, and the file is rewritten on every spawn
118
+ * so a detached MCP disappears on the next run rather than lingering.
119
+ */
120
+ function writeKimiProjectMcpConfig(workingDirectory, servers) {
121
+ const configDir = path.join(workingDirectory, '.kimi-code');
122
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
63
123
  const mcpConfig = { mcpServers: {} };
64
124
  for (const s of servers) {
65
125
  if (!s.slug || !s.command)
@@ -90,15 +150,76 @@ function writeKimiSandboxHome(servers) {
90
150
  ...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
91
151
  };
92
152
  }
93
- const mcpConfigPath = path.join(homeDir, '.kimi', 'mcp.json');
153
+ // 0600: stdio servers carry their credentials in `env` and remote ones in
154
+ // `headers`, and this file lives inside the entity's working directory.
155
+ const mcpConfigPath = path.join(configDir, 'mcp.json');
94
156
  fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
95
- return { homeDir, mcpConfigPath };
157
+ return mcpConfigPath;
96
158
  }
97
- /** Parse one stream-json line into 0+ RuntimeEvents. Kimi's shapes are
98
- * similar in spirit to claude's: assistant text deltas, tool calls,
99
- * turn completion, and session metadata. Anything we don't recognise
100
- * is forwarded as `kimi:<type>` opaque events. */
159
+ /**
160
+ * Parse one stream-json line into 0+ RuntimeEvents.
161
+ *
162
+ * kimi's prompt-mode stream is OpenAI-CHAT shaped, not claude shaped: every
163
+ * content line is keyed by `role`, and only the three metadata lines carry a
164
+ * `type` at all (`system.version`, `session.resume_hint`,
165
+ * `turn.step.retrying`). The role branches therefore come first. Without them
166
+ * every real line fell through to the opaque passthrough at the bottom, which
167
+ * meant a kimi run that worked perfectly would still have delivered an empty
168
+ * reply — a second, independent bug hiding behind the argument-parsing one.
169
+ *
170
+ * Shapes, from PromptJsonWriter in the shipped bundle:
171
+ * {role:"assistant", content?:string, tool_calls?:[{id,function:{name,arguments}}]}
172
+ * {role:"tool", tool_call_id:string, content:string}
173
+ * {role:"meta", type:"...", ...}
174
+ *
175
+ * Note there is no terminal line and no usage line: the runner keys
176
+ * completion off process exit, and kimi runs report no token counts.
177
+ *
178
+ * The `type`-keyed branches below are kept for older builds, and anything
179
+ * unrecognised is still forwarded as `kimi:<type>` rather than dropped.
180
+ */
101
181
  function lineToEvents(line) {
182
+ const role = typeof line.role === 'string' ? line.role : '';
183
+ if (role === 'assistant') {
184
+ const out = [];
185
+ if (typeof line.content === 'string' && line.content) {
186
+ out.push({ type: 'assistant_text', delta: line.content });
187
+ }
188
+ const calls = Array.isArray(line.tool_calls) ? line.tool_calls : [];
189
+ for (const c of calls) {
190
+ // Arguments arrive as a JSON string (streamed in parts, joined by the
191
+ // writer). Keep the raw string if it doesn't parse — a malformed
192
+ // argument list is worth showing, not worth losing the tool call over.
193
+ const rawArgs = c?.function?.arguments;
194
+ let input = rawArgs;
195
+ if (typeof rawArgs === 'string' && rawArgs.length > 0) {
196
+ try {
197
+ input = JSON.parse(rawArgs);
198
+ }
199
+ catch {
200
+ input = rawArgs;
201
+ }
202
+ }
203
+ out.push({
204
+ type: 'tool_use',
205
+ id: String(c?.id ?? ''),
206
+ name: String(c?.function?.name ?? ''),
207
+ input,
208
+ });
209
+ }
210
+ // An assistant line with neither content nor calls shouldn't happen (the
211
+ // writer skips empty flushes) — pass it through rather than swallow it.
212
+ return out.length > 0 ? out : [{ type: 'kimi:assistant', raw: line }];
213
+ }
214
+ if (role === 'tool') {
215
+ // kimi has no error channel on tool results; failures come back as text.
216
+ return [{
217
+ type: 'tool_result',
218
+ id: String(line.tool_call_id ?? ''),
219
+ content: line.content,
220
+ isError: false,
221
+ }];
222
+ }
102
223
  const type = typeof line.type === 'string' ? line.type : '';
103
224
  // Assistant text — kimi emits 'assistant_message' or 'assistant.delta'
104
225
  // depending on version.
@@ -179,6 +300,36 @@ function lineToEvents(line) {
179
300
  // Anything else — opaque passthrough
180
301
  return [{ type: `kimi:${type || 'unknown'}`, raw: line }];
181
302
  }
303
+ /**
304
+ * The kimi command line for one prompt-mode run.
305
+ *
306
+ * Extracted so the flag set is testable without spawning anything — this is
307
+ * the surface where four invented flags shipped undetected, each one killing
308
+ * the run before a single token was generated. Every flag below is present in
309
+ * the CLI's own option table.
310
+ *
311
+ * Not here on purpose: --work-dir (kimi reads process.cwd(), which the spawn
312
+ * sets), --mcp-config-file (config comes from <cwd>/.kimi-code/mcp.json), and
313
+ * --yolo (prompt mode forces "auto" permissions and rejects the flag).
314
+ */
315
+ function buildKimiArgs(input) {
316
+ // --output-format is only accepted in prompt mode, which --prompt turns on.
317
+ const args = ['--output-format', 'stream-json'];
318
+ // kimi refuses a bare --session in prompt mode, so only pass it with an id.
319
+ if (input.resumeSessionId)
320
+ args.push('--session', input.resumeSessionId);
321
+ if (input.model)
322
+ args.push('--model', input.model);
323
+ // kimi has no --append-system-prompt; prepend with a separator the model can
324
+ // recognise as a system-like instruction block.
325
+ let promptText = input.prompt;
326
+ if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
327
+ promptText = `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`;
328
+ }
329
+ // Last, and always: --prompt takes the prompt as its VALUE.
330
+ args.push('--prompt', promptText);
331
+ return args;
332
+ }
182
333
  function spawnKimi(input) {
183
334
  const bin = (0, kimi_binary_1.findKimiBinary)();
184
335
  if (!bin) {
@@ -201,49 +352,19 @@ function spawnKimi(input) {
201
352
  done: Promise.resolve(127),
202
353
  };
203
354
  }
204
- // Sandbox the kimi config dir. HOME override means kimi's default
205
- // We pass --mcp-config-file explicitly, so kimi reads OUR servers,
206
- // not the host's ~/.kimi/mcp.json. The mcp config path can live in a
207
- // throwaway temp dir; we no longer need to override HOME.
208
- //
209
- // The OLD approach (HOME=<tempdir>) was destructive: kimi shells out
210
- // to git, pip, etc., and those tools read ~/.gitconfig, ~/.npmrc,
211
- // ~/.netrc from HOME. Pointing HOME at an empty dir blew away every
212
- // user-level tool config, causing silent tool-call failures
213
- // (git push without creds, pip install without index URL).
214
- const { homeDir: kimiSandboxDir, mcpConfigPath } = writeKimiSandboxHome(input.mcpServers ?? []);
215
- const args = [
216
- '--print',
217
- '--output-format', 'stream-json',
218
- '--work-dir', input.workingDirectory,
219
- '--mcp-config-file', mcpConfigPath,
220
- ];
221
- if (input.resumeSessionId) {
222
- args.push('--session', input.resumeSessionId);
223
- }
224
- if (input.model) {
225
- args.push('--model', input.model);
226
- }
227
- if (input.bypassPermissions) {
228
- args.push('--yolo');
229
- }
230
- // Kimi doesn't have --append-system-prompt; prepend with a separator
231
- // the model can recognise as a system-like instruction block.
232
- let promptText = input.prompt;
233
- if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
234
- promptText = `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`;
235
- }
236
- args.push('--prompt', promptText);
355
+ // MCP servers reach kimi through a file in the working directory, not a
356
+ // flag. Written before spawn and rewritten every run — see above.
357
+ writeKimiProjectMcpConfig(input.workingDirectory, input.mcpServers ?? []);
358
+ const args = buildKimiArgs(input);
237
359
  // No HOME override: the real $HOME flows through so kimi's own auth +
238
360
  // shelled-out tools (git/pip/node) can find their config files. The
239
361
  // explicit --mcp-config-file already prevents host mcp.json leakage.
362
+ const effortValue = input.effortLevel ? kimiEffort(input.effortLevel) : null;
240
363
  const childEnv = {
241
364
  ...process.env,
242
365
  TERM: 'dumb',
243
- // Surface the requested reasoning effort without betting on a CLI flag
244
- // that may not exist. A kimi config / wrapper can pick this up; if kimi
245
- // ignores it, behaviour is unchanged (no spawn failure).
246
- ...(input.effortLevel ? { KIMI_REASONING_EFFORT: input.effortLevel } : {}),
366
+ // Effort, only when the registry says kimi can take one see kimiEffort.
367
+ ...(effortValue ? { KIMI_MODEL_THINKING_EFFORT: effortValue } : {}),
247
368
  };
248
369
  const listeners = [];
249
370
  const emit = (e) => { for (const l of listeners)
@@ -273,10 +394,6 @@ function spawnKimi(input) {
273
394
  }
274
395
  catch (err) {
275
396
  const msg = err.message || String(err);
276
- try {
277
- fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
278
- }
279
- catch { }
280
397
  const events = (0, events_1.bufferedEvents)([
281
398
  { type: 'error', code: 'kimi_spawn_failed', message: msg },
282
399
  { type: 'turn_complete', stopReason: 'failed' },
@@ -328,11 +445,6 @@ function spawnKimi(input) {
328
445
  // Release anything the splitter is still holding — a run that ends
329
446
  // mid-tag must not lose the tail of its answer.
330
447
  emitSplit.flush();
331
- // Best-effort cleanup of sandbox tmp dir
332
- try {
333
- fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
334
- }
335
- catch { }
336
448
  resolve(code ?? -1);
337
449
  });
338
450
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [