@addai/node 0.4.0

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.
Files changed (133) hide show
  1. package/README.md +96 -0
  2. package/dist/agent-auth.d.ts +9 -0
  3. package/dist/agent-auth.js +96 -0
  4. package/dist/attachments.d.ts +20 -0
  5. package/dist/attachments.js +164 -0
  6. package/dist/capabilities.d.ts +64 -0
  7. package/dist/capabilities.js +406 -0
  8. package/dist/claude-binary.d.ts +16 -0
  9. package/dist/claude-binary.js +145 -0
  10. package/dist/claude-config.d.ts +11 -0
  11. package/dist/claude-config.js +144 -0
  12. package/dist/claude-print.d.ts +35 -0
  13. package/dist/claude-print.js +312 -0
  14. package/dist/claude-spawn.d.ts +40 -0
  15. package/dist/claude-spawn.js +220 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +137 -0
  18. package/dist/codex-binary.d.ts +4 -0
  19. package/dist/codex-binary.js +70 -0
  20. package/dist/codex-spawn.d.ts +47 -0
  21. package/dist/codex-spawn.js +320 -0
  22. package/dist/command-runner.d.ts +9 -0
  23. package/dist/command-runner.js +592 -0
  24. package/dist/config.d.ts +7 -0
  25. package/dist/config.js +38 -0
  26. package/dist/control-server.d.ts +5 -0
  27. package/dist/control-server.js +292 -0
  28. package/dist/diskguard.d.ts +96 -0
  29. package/dist/diskguard.js +435 -0
  30. package/dist/error-classify.d.ts +20 -0
  31. package/dist/error-classify.js +159 -0
  32. package/dist/events.d.ts +26 -0
  33. package/dist/events.js +97 -0
  34. package/dist/gemini-binary.d.ts +4 -0
  35. package/dist/gemini-binary.js +64 -0
  36. package/dist/gemini-spawn.d.ts +52 -0
  37. package/dist/gemini-spawn.js +369 -0
  38. package/dist/git-identity.d.ts +18 -0
  39. package/dist/git-identity.js +104 -0
  40. package/dist/grok-binary.d.ts +4 -0
  41. package/dist/grok-binary.js +70 -0
  42. package/dist/grok-spawn.d.ts +41 -0
  43. package/dist/grok-spawn.js +495 -0
  44. package/dist/guardrails.d.ts +32 -0
  45. package/dist/guardrails.js +125 -0
  46. package/dist/hang-watchdog.d.ts +17 -0
  47. package/dist/hang-watchdog.js +48 -0
  48. package/dist/harness-registry.d.ts +55 -0
  49. package/dist/harness-registry.js +180 -0
  50. package/dist/heartbeat.d.ts +13 -0
  51. package/dist/heartbeat.js +95 -0
  52. package/dist/index.d.ts +12 -0
  53. package/dist/index.js +440 -0
  54. package/dist/install.d.ts +63 -0
  55. package/dist/install.js +467 -0
  56. package/dist/jsonl-tail.d.ts +20 -0
  57. package/dist/jsonl-tail.js +192 -0
  58. package/dist/kimi-binary.d.ts +4 -0
  59. package/dist/kimi-binary.js +68 -0
  60. package/dist/kimi-spawn.d.ts +47 -0
  61. package/dist/kimi-spawn.js +319 -0
  62. package/dist/lockfile.d.ts +30 -0
  63. package/dist/lockfile.js +169 -0
  64. package/dist/mcp-config.d.ts +23 -0
  65. package/dist/mcp-config.js +133 -0
  66. package/dist/memory-capture.d.ts +20 -0
  67. package/dist/memory-capture.js +45 -0
  68. package/dist/memory-pack.d.ts +34 -0
  69. package/dist/memory-pack.js +171 -0
  70. package/dist/model-filter.d.ts +4 -0
  71. package/dist/model-filter.js +34 -0
  72. package/dist/node-health.d.ts +18 -0
  73. package/dist/node-health.js +74 -0
  74. package/dist/pairing.d.ts +20 -0
  75. package/dist/pairing.js +129 -0
  76. package/dist/paths.d.ts +9 -0
  77. package/dist/paths.js +48 -0
  78. package/dist/precompact-hook.d.ts +27 -0
  79. package/dist/precompact-hook.js +136 -0
  80. package/dist/projects.d.ts +25 -0
  81. package/dist/projects.js +255 -0
  82. package/dist/prompt-keystrokes.d.ts +3 -0
  83. package/dist/prompt-keystrokes.js +56 -0
  84. package/dist/pty-helper.d.ts +26 -0
  85. package/dist/pty-helper.js +156 -0
  86. package/dist/request-pump.d.ts +12 -0
  87. package/dist/request-pump.js +249 -0
  88. package/dist/run-retry.d.ts +28 -0
  89. package/dist/run-retry.js +81 -0
  90. package/dist/self-update.d.ts +75 -0
  91. package/dist/self-update.js +192 -0
  92. package/dist/session-runner.d.ts +130 -0
  93. package/dist/session-runner.js +2271 -0
  94. package/dist/sleep-detector.d.ts +11 -0
  95. package/dist/sleep-detector.js +32 -0
  96. package/dist/state-migrate.d.ts +8 -0
  97. package/dist/state-migrate.js +91 -0
  98. package/dist/store.d.ts +5 -0
  99. package/dist/store.js +82 -0
  100. package/dist/supabase-client.d.ts +15 -0
  101. package/dist/supabase-client.js +119 -0
  102. package/dist/time-context.d.ts +10 -0
  103. package/dist/time-context.js +71 -0
  104. package/dist/tokens.d.ts +4 -0
  105. package/dist/tokens.js +33 -0
  106. package/dist/tui/app.d.ts +35 -0
  107. package/dist/tui/app.js +82 -0
  108. package/dist/tui/console-capture.d.ts +13 -0
  109. package/dist/tui/console-capture.js +62 -0
  110. package/dist/tui/dashboard.d.ts +42 -0
  111. package/dist/tui/dashboard.js +205 -0
  112. package/dist/tui/data.d.ts +57 -0
  113. package/dist/tui/data.js +62 -0
  114. package/dist/tui/harnesses.d.ts +16 -0
  115. package/dist/tui/harnesses.js +354 -0
  116. package/dist/tui/render.d.ts +21 -0
  117. package/dist/tui/render.js +116 -0
  118. package/dist/tui/requests.d.ts +18 -0
  119. package/dist/tui/requests.js +127 -0
  120. package/dist/tui/run.d.ts +13 -0
  121. package/dist/tui/run.js +178 -0
  122. package/dist/tui/transcript.d.ts +39 -0
  123. package/dist/tui/transcript.js +234 -0
  124. package/dist/tui.d.ts +1 -0
  125. package/dist/tui.js +314 -0
  126. package/dist/types.d.ts +65 -0
  127. package/dist/types.js +3 -0
  128. package/dist/win.d.ts +67 -0
  129. package/dist/win.js +317 -0
  130. package/package.json +60 -0
  131. package/scripts/fix-pty-helper.js +28 -0
  132. package/scripts/precompact-capture.js +292 -0
  133. package/scripts/smoke-test.sh +74 -0
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ // Locate the `kimi` binary on the host. The Moonshot installer drops it
3
+ // at ~/.kimi/bin/kimi by default, but it can also live on PATH or under
4
+ // /usr/local/bin / /opt/homebrew/bin depending on how the user installed.
5
+ // On Windows: kimi.exe under %USERPROFILE%\.kimi\bin, or an npm .cmd shim.
6
+ //
7
+ // Returns null if not present so callers can surface an install hint.
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.resetCache = resetCache;
43
+ exports.findKimiBinary = findKimiBinary;
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const win_1 = require("./win");
47
+ const POSIX_CANDIDATES = [
48
+ path.join(os.homedir(), '.kimi', 'bin', 'kimi'), // Moonshot installer default
49
+ '/usr/local/bin/kimi',
50
+ '/opt/homebrew/bin/kimi',
51
+ ];
52
+ const WIN32_CANDIDATES = [
53
+ path.join(os.homedir(), '.kimi', 'bin', 'kimi.exe'),
54
+ path.join(os.homedir(), '.kimi', 'bin', 'kimi.cmd'),
55
+ ];
56
+ /** Forget the cached lookup — call after an install so a fresh probe
57
+ * can see a binary that was missing at daemon start. */
58
+ function resetCache() { cached = undefined; }
59
+ let cached;
60
+ function findKimiBinary() {
61
+ if (cached !== undefined)
62
+ return cached;
63
+ cached = (0, win_1.findCliBinary)('kimi', {
64
+ posixCandidates: POSIX_CANDIDATES,
65
+ win32Candidates: WIN32_CANDIDATES,
66
+ });
67
+ return cached;
68
+ }
@@ -0,0 +1,47 @@
1
+ import type { RuntimeEvent, RuntimeEffortLevel } from './types';
2
+ export interface KimiMcpServer {
3
+ slug: string;
4
+ command: string;
5
+ args?: string[] | null;
6
+ env?: Record<string, string> | null;
7
+ }
8
+ export interface KimiInput {
9
+ workingDirectory: string;
10
+ prompt: string;
11
+ model?: string;
12
+ /** Resume an existing session (mapped to `--session <id>`). */
13
+ resumeSessionId?: string;
14
+ /** Skip approval prompts. Maps to --yolo. */
15
+ bypassPermissions?: boolean;
16
+ /** When set, kimi runs against a sandboxed mcp config that contains
17
+ * ONLY these MCP servers (host's ~/.kimi/mcp.json suppressed). */
18
+ mcpServers?: KimiMcpServer[];
19
+ /** Studio-entity system prompt — kimi doesn't have an explicit
20
+ * --system-prompt flag, so we prepend it to the user prompt with a
21
+ * separator the model can recognise. */
22
+ 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. */
29
+ effortLevel?: RuntimeEffortLevel;
30
+ }
31
+ export interface KimiHandle {
32
+ pid: number | undefined;
33
+ sessionId: string | null;
34
+ kill(): void;
35
+ onEvent(cb: (e: RuntimeEvent) => void): void;
36
+ /** Fires on every raw stdout/stderr chunk. kimi emits no per-tool progress
37
+ * events, so raw bytes are the only liveness signal during silent tool
38
+ * work — the hang-watchdog consumes this to avoid killing a busy agent. */
39
+ onActivity(cb: () => void): void;
40
+ done: Promise<number>;
41
+ }
42
+ /** Parse one stream-json line into 0+ RuntimeEvents. Kimi's shapes are
43
+ * similar in spirit to claude's: assistant text deltas, tool calls,
44
+ * turn completion, and session metadata. Anything we don't recognise
45
+ * is forwarded as `kimi:<type>` opaque events. */
46
+ export declare function lineToEvents(line: Record<string, unknown>): RuntimeEvent[];
47
+ export declare function spawnKimi(input: KimiInput): KimiHandle;
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ // kimi --print --output-format stream-json adapter.
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.
9
+ //
10
+ // Reference: https://moonshotai.github.io/kimi-cli/en/reference/kimi-command.html
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.lineToEvents = lineToEvents;
46
+ exports.spawnKimi = spawnKimi;
47
+ const child_process_1 = require("child_process");
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const kimi_binary_1 = require("./kimi-binary");
52
+ const win_1 = require("./win");
53
+ const events_1 = require("./events");
54
+ /** Write a sandbox $HOME with an empty .kimi dir so kimi's default
55
+ * `~/.kimi/mcp.json` resolves to nothing. Caller is responsible for
56
+ * cleanup (we drop it under os.tmpdir() so the OS reaps it). */
57
+ function writeKimiSandboxHome(servers) {
58
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-kimi-'));
59
+ fs.mkdirSync(path.join(homeDir, '.kimi'), { mode: 0o700 });
60
+ const mcpConfig = { mcpServers: {} };
61
+ for (const s of servers) {
62
+ if (!s.slug || !s.command)
63
+ continue;
64
+ // `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
65
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args : []);
66
+ mcpConfig.mcpServers[s.slug] = {
67
+ command: wrapped.command,
68
+ ...(wrapped.args.length > 0 ? { args: wrapped.args } : {}),
69
+ ...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
70
+ };
71
+ }
72
+ const mcpConfigPath = path.join(homeDir, '.kimi', 'mcp.json');
73
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
74
+ return { homeDir, mcpConfigPath };
75
+ }
76
+ /** Parse one stream-json line into 0+ RuntimeEvents. Kimi's shapes are
77
+ * similar in spirit to claude's: assistant text deltas, tool calls,
78
+ * turn completion, and session metadata. Anything we don't recognise
79
+ * is forwarded as `kimi:<type>` opaque events. */
80
+ function lineToEvents(line) {
81
+ const type = typeof line.type === 'string' ? line.type : '';
82
+ // Assistant text — kimi emits 'assistant_message' or 'assistant.delta'
83
+ // depending on version.
84
+ if (type === 'assistant_message' || type === 'assistant.delta' || type === 'message') {
85
+ const text = (typeof line.text === 'string' && line.text) ||
86
+ (typeof line.content === 'string' && line.content) ||
87
+ (line.delta?.text) ||
88
+ undefined;
89
+ if (text)
90
+ return [{ type: 'assistant_text', delta: text }];
91
+ }
92
+ // 'assistant' (claude-shaped block-array) fallback
93
+ if (type === 'assistant') {
94
+ const msg = line.message;
95
+ const content = msg?.content;
96
+ const out = [];
97
+ if (Array.isArray(content)) {
98
+ for (const b of content) {
99
+ if (b.type === 'text' && typeof b.text === 'string') {
100
+ out.push({ type: 'assistant_text', delta: b.text });
101
+ }
102
+ else if (b.type === 'tool_use') {
103
+ out.push({
104
+ type: 'tool_use',
105
+ id: String(b.id ?? ''),
106
+ name: String(b.name ?? ''),
107
+ input: b.input,
108
+ });
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+ // content is a plain STRING (kimi emits this for text-only turns) — the
114
+ // old code returned [] here, silently dropping the ENTIRE reply and all
115
+ // liveness for the turn. Synthesise the assistant_text so the answer
116
+ // (and heartbeat) survives.
117
+ if (typeof content === 'string' && content) {
118
+ return [{ type: 'assistant_text', delta: content }];
119
+ }
120
+ // Any other shape → fall through to the opaque passthrough below instead
121
+ // of being dropped, so we never silently swallow a populated message.
122
+ return [{ type: `kimi:${type}`, raw: line }];
123
+ }
124
+ // Tool result. Kimi's exact tool_result shape isn't verified upstream, so
125
+ // map the fields we can recognise (claude/gemini use id+content/output) and
126
+ // otherwise fall through to an opaque passthrough rather than dropping it.
127
+ if (type === 'tool_result') {
128
+ const id = String(line.tool_id ?? line.tool_use_id ?? line.id ?? '');
129
+ const content = line.output ?? line.content ?? line.result;
130
+ if (id || content !== undefined) {
131
+ const status = typeof line.status === 'string' ? line.status.toLowerCase() : '';
132
+ return [{
133
+ type: 'tool_result',
134
+ id,
135
+ content,
136
+ isError: line.is_error === true || line.error != null || (status !== '' && status !== 'success'),
137
+ }];
138
+ }
139
+ return [{ type: `kimi:${type}`, raw: line }];
140
+ }
141
+ // Turn complete / final message
142
+ if (type === 'turn_complete' || type === 'final_message' || type === 'result') {
143
+ return [{
144
+ type: 'turn_complete',
145
+ usage: line.usage ?? undefined,
146
+ stopReason: typeof line.stop_reason === 'string' ? line.stop_reason : undefined,
147
+ }];
148
+ }
149
+ // Errors
150
+ if (type === 'error' || type === 'turn_failed') {
151
+ const message = line.error?.message
152
+ ?? (typeof line.message === 'string' ? line.message : 'kimi error');
153
+ return [
154
+ { type: 'error', code: 'kimi_error', message },
155
+ { type: 'turn_complete', stopReason: 'failed' },
156
+ ];
157
+ }
158
+ // Anything else — opaque passthrough
159
+ return [{ type: `kimi:${type || 'unknown'}`, raw: line }];
160
+ }
161
+ function spawnKimi(input) {
162
+ const bin = (0, kimi_binary_1.findKimiBinary)();
163
+ if (!bin) {
164
+ // Surface a "kimi not installed" error via the event stream so the
165
+ // chat fails cleanly instead of hanging. bufferedEvents delivers even
166
+ // though the runner attaches its listener after an await.
167
+ const hint = win_1.IS_WINDOWS
168
+ ? 'kimi CLI is not installed on this runtime. See code.kimi.com for the Windows install.'
169
+ : 'kimi CLI is not installed on this runtime. Install: curl -L code.kimi.com/install.sh | bash';
170
+ const events = (0, events_1.bufferedEvents)([
171
+ { type: 'error', code: 'kimi_not_installed', message: hint },
172
+ { type: 'turn_complete', stopReason: 'failed' },
173
+ ]);
174
+ return {
175
+ pid: undefined,
176
+ sessionId: null,
177
+ kill: () => { },
178
+ onEvent: events.onEvent,
179
+ onActivity: () => { },
180
+ done: Promise.resolve(127),
181
+ };
182
+ }
183
+ // Sandbox the kimi config dir. HOME override means kimi's default
184
+ // We pass --mcp-config-file explicitly, so kimi reads OUR servers,
185
+ // not the host's ~/.kimi/mcp.json. The mcp config path can live in a
186
+ // throwaway temp dir; we no longer need to override HOME.
187
+ //
188
+ // The OLD approach (HOME=<tempdir>) was destructive: kimi shells out
189
+ // to git, pip, etc., and those tools read ~/.gitconfig, ~/.npmrc,
190
+ // ~/.netrc from HOME. Pointing HOME at an empty dir blew away every
191
+ // user-level tool config, causing silent tool-call failures
192
+ // (git push without creds, pip install without index URL).
193
+ const { homeDir: kimiSandboxDir, mcpConfigPath } = writeKimiSandboxHome(input.mcpServers ?? []);
194
+ const args = [
195
+ '--print',
196
+ '--output-format', 'stream-json',
197
+ '--work-dir', input.workingDirectory,
198
+ '--mcp-config-file', mcpConfigPath,
199
+ ];
200
+ if (input.resumeSessionId) {
201
+ args.push('--session', input.resumeSessionId);
202
+ }
203
+ if (input.model) {
204
+ args.push('--model', input.model);
205
+ }
206
+ if (input.bypassPermissions) {
207
+ args.push('--yolo');
208
+ }
209
+ // Kimi doesn't have --append-system-prompt; prepend with a separator
210
+ // the model can recognise as a system-like instruction block.
211
+ let promptText = input.prompt;
212
+ if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
213
+ promptText = `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`;
214
+ }
215
+ args.push('--prompt', promptText);
216
+ // No HOME override: the real $HOME flows through so kimi's own auth +
217
+ // shelled-out tools (git/pip/node) can find their config files. The
218
+ // explicit --mcp-config-file already prevents host mcp.json leakage.
219
+ const childEnv = {
220
+ ...process.env,
221
+ TERM: 'dumb',
222
+ // Surface the requested reasoning effort without betting on a CLI flag
223
+ // that may not exist. A kimi config / wrapper can pick this up; if kimi
224
+ // ignores it, behaviour is unchanged (no spawn failure).
225
+ ...(input.effortLevel ? { KIMI_REASONING_EFFORT: input.effortLevel } : {}),
226
+ };
227
+ const listeners = [];
228
+ const emit = (e) => { for (const l of listeners)
229
+ try {
230
+ l(e);
231
+ }
232
+ catch { } };
233
+ const activityListeners = [];
234
+ const touchActivity = () => { for (const l of activityListeners)
235
+ try {
236
+ l();
237
+ }
238
+ catch { } };
239
+ let sessionId = null;
240
+ let buffer = '';
241
+ let proc;
242
+ try {
243
+ // npm .cmd shim → node script (win32); identity on POSIX. Both this
244
+ // resolution and spawn() itself can throw synchronously on Windows —
245
+ // fail the request cleanly instead of crashing the daemon.
246
+ const inv = (0, win_1.resolveCliInvocation)(bin, args);
247
+ proc = (0, child_process_1.spawn)(inv.file, inv.args, {
248
+ cwd: input.workingDirectory,
249
+ env: childEnv,
250
+ stdio: ['ignore', 'pipe', 'pipe'],
251
+ });
252
+ }
253
+ catch (err) {
254
+ const msg = err.message || String(err);
255
+ try {
256
+ fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
257
+ }
258
+ catch { }
259
+ const events = (0, events_1.bufferedEvents)([
260
+ { type: 'error', code: 'kimi_spawn_failed', message: msg },
261
+ { type: 'turn_complete', stopReason: 'failed' },
262
+ ]);
263
+ return {
264
+ pid: undefined,
265
+ sessionId: null,
266
+ kill: () => { },
267
+ onEvent: events.onEvent,
268
+ onActivity: () => { },
269
+ done: Promise.resolve(-1),
270
+ };
271
+ }
272
+ proc.stdout?.on('data', (chunk) => {
273
+ touchActivity();
274
+ buffer += chunk.toString('utf8');
275
+ let nl;
276
+ while ((nl = buffer.indexOf('\n')) !== -1) {
277
+ const raw = buffer.slice(0, nl).trim();
278
+ buffer = buffer.slice(nl + 1);
279
+ if (!raw)
280
+ continue;
281
+ let parsed;
282
+ try {
283
+ parsed = JSON.parse(raw);
284
+ }
285
+ catch {
286
+ continue;
287
+ }
288
+ if (typeof parsed.session_id === 'string' && !sessionId)
289
+ sessionId = parsed.session_id;
290
+ for (const ev of lineToEvents(parsed))
291
+ emit(ev);
292
+ }
293
+ });
294
+ proc.stderr?.on('data', (chunk) => {
295
+ touchActivity();
296
+ const txt = chunk.toString('utf8').trim();
297
+ if (txt)
298
+ emit({ type: 'error', code: 'kimi_stderr', message: txt.slice(0, 4096) });
299
+ });
300
+ const done = new Promise((resolve, reject) => {
301
+ proc.once('error', reject);
302
+ proc.once('exit', (code) => {
303
+ // Best-effort cleanup of sandbox tmp dir
304
+ try {
305
+ fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
306
+ }
307
+ catch { }
308
+ resolve(code ?? -1);
309
+ });
310
+ });
311
+ return {
312
+ pid: proc.pid,
313
+ get sessionId() { return sessionId; },
314
+ kill: () => { (0, win_1.killProcessTree)(proc); },
315
+ onEvent: (cb) => { listeners.push(cb); },
316
+ onActivity: (cb) => { activityListeners.push(cb); },
317
+ done,
318
+ };
319
+ }
@@ -0,0 +1,30 @@
1
+ export interface LockfileBody {
2
+ pid: number;
3
+ port: number;
4
+ version: string;
5
+ startedAt: number;
6
+ }
7
+ /**
8
+ * Does this command line belong to one of our daemons?
9
+ *
10
+ * Kept deliberately broad across the +Ai Node rename: a box mid-roll may
11
+ * still be running a pre-rename daemon, and treating it as an unrelated
12
+ * process would let a second daemon start alongside it.
13
+ */
14
+ export declare function isOwnDaemonCmdline(cmdline: string): boolean;
15
+ /**
16
+ * Is the pid in the lockfile a live daemon of ours?
17
+ *
18
+ * Exported because the CLI's read-only "viewer" path needs the same answer
19
+ * acquireLockfile() needs: a lockfile left behind by a crashed daemon must
20
+ * not be mistaken for a running one.
21
+ */
22
+ export declare function processAlive(pid: number): boolean;
23
+ export declare function readLockfile(): LockfileBody | null;
24
+ /**
25
+ * Throw if another runtime is already running. Returns the lockfile body
26
+ * we wrote on success. The caller is responsible for calling
27
+ * `releaseLockfile()` on clean shutdown.
28
+ */
29
+ export declare function acquireLockfile(body: Omit<LockfileBody, 'pid'>): LockfileBody;
30
+ export declare function releaseLockfile(): void;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ // Tiny lockfile at `~/.ainode/runtime.json`. Written on start
3
+ // (with pid + port + version), polled by the CodeFlows desktop to decide
4
+ // whether the runtime is alive, and unlinked on a clean exit.
5
+ //
6
+ // We trust pid checks via `process.kill(pid, 0)` to detect stale lockfiles
7
+ // from previous crashes — Node throws if the pid doesn't belong to a
8
+ // running process we can signal.
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.isOwnDaemonCmdline = isOwnDaemonCmdline;
44
+ exports.processAlive = processAlive;
45
+ exports.readLockfile = readLockfile;
46
+ exports.acquireLockfile = acquireLockfile;
47
+ exports.releaseLockfile = releaseLockfile;
48
+ const fs = __importStar(require("fs"));
49
+ const paths_1 = require("./paths");
50
+ function ensureHome() {
51
+ if (!fs.existsSync(paths_1.RUNTIME_HOME)) {
52
+ fs.mkdirSync(paths_1.RUNTIME_HOME, { recursive: true, mode: 0o700 });
53
+ }
54
+ }
55
+ /**
56
+ * Does this command line belong to one of our daemons?
57
+ *
58
+ * Kept deliberately broad across the +Ai Node rename: a box mid-roll may
59
+ * still be running a pre-rename daemon, and treating it as an unrelated
60
+ * process would let a second daemon start alongside it.
61
+ */
62
+ function isOwnDaemonCmdline(cmdline) {
63
+ const s = cmdline.toLowerCase();
64
+ if (!s.trim())
65
+ return false;
66
+ // Every name this daemon has shipped under. '@addai/node' must be matched
67
+ // explicitly: its install path reads '.../@addai/node/dist/cli.js', which
68
+ // contains 'addai/node' but NOT 'ainode', so the bin-name check alone
69
+ // would call a live daemon stale and let a second one start beside it.
70
+ // Bare 'node' is deliberately never matched — it is every Node process.
71
+ return s.includes('@addai/node')
72
+ || s.includes('addai/node')
73
+ || s.includes('ainode')
74
+ || s.includes('entity-runtime')
75
+ || s.includes('entities-runtime');
76
+ }
77
+ /**
78
+ * Is the pid in the lockfile a live daemon of ours?
79
+ *
80
+ * Exported because the CLI's read-only "viewer" path needs the same answer
81
+ * acquireLockfile() needs: a lockfile left behind by a crashed daemon must
82
+ * not be mistaken for a running one.
83
+ */
84
+ function processAlive(pid) {
85
+ let signalSucceeded;
86
+ try {
87
+ process.kill(pid, 0);
88
+ signalSucceeded = true;
89
+ }
90
+ catch (err) {
91
+ // ESRCH = no such process. EPERM = exists but not ours.
92
+ if (err.code !== 'EPERM')
93
+ return false;
94
+ signalSucceeded = false; // EPERM: process exists, but it might be a recycled PID owned by root etc.
95
+ }
96
+ // Defense against PID recycling: macOS recycles PIDs aggressively. After
97
+ // a `sudo pkill -9` + restart, the old daemon's PID can be reassigned to
98
+ // a system process owned by another user. `process.kill(pid, 0)` then
99
+ // returns EPERM (legitimately — that process IS alive, just not ours)
100
+ // and the naive check would refuse to start the new daemon forever.
101
+ // Verify the PID actually points at OUR daemon by checking its command
102
+ // line for the expected binary name. If the cmdline doesn't include
103
+ // 'entity-runtime' or 'entities-runtime', the lockfile is stale.
104
+ try {
105
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
106
+ const { execFileSync } = require('child_process');
107
+ let out;
108
+ if (process.platform === 'win32') {
109
+ // No `ps` on Windows; the daemon runs as node.exe so tasklist's image
110
+ // name can't identify it — read the full command line via CIM.
111
+ out = execFileSync('powershell.exe', [
112
+ '-NoProfile', '-NonInteractive', '-Command',
113
+ `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`,
114
+ ], { encoding: 'utf8', timeout: 5000, windowsHide: true }).toString().toLowerCase();
115
+ }
116
+ else {
117
+ out = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
118
+ encoding: 'utf8',
119
+ timeout: 2000,
120
+ }).toString().toLowerCase();
121
+ }
122
+ // Both probes exit 0 with empty output if pid is dead — handle that too.
123
+ if (!out.trim())
124
+ return false;
125
+ if (!isOwnDaemonCmdline(out)) {
126
+ return false; // pid was recycled to an unrelated process
127
+ }
128
+ return true;
129
+ }
130
+ catch {
131
+ // ps/powershell failed (timeout, weird platform). Fall back to the
132
+ // original signal result — better to refuse start than to clobber a
133
+ // real daemon.
134
+ return signalSucceeded;
135
+ }
136
+ }
137
+ function readLockfile() {
138
+ try {
139
+ const raw = fs.readFileSync(paths_1.RUNTIME_LOCK_FILE, 'utf-8');
140
+ const parsed = JSON.parse(raw);
141
+ if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number')
142
+ return null;
143
+ return parsed;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Throw if another runtime is already running. Returns the lockfile body
151
+ * we wrote on success. The caller is responsible for calling
152
+ * `releaseLockfile()` on clean shutdown.
153
+ */
154
+ function acquireLockfile(body) {
155
+ ensureHome();
156
+ const existing = readLockfile();
157
+ if (existing && processAlive(existing.pid)) {
158
+ throw new Error(`ainode is already running (pid ${existing.pid}, port ${existing.port}).`);
159
+ }
160
+ const final = { pid: process.pid, ...body };
161
+ fs.writeFileSync(paths_1.RUNTIME_LOCK_FILE, JSON.stringify(final, null, 2), { mode: 0o600 });
162
+ return final;
163
+ }
164
+ function releaseLockfile() {
165
+ try {
166
+ fs.unlinkSync(paths_1.RUNTIME_LOCK_FILE);
167
+ }
168
+ catch { }
169
+ }
@@ -0,0 +1,23 @@
1
+ import { CLAUDE_HOME } from './paths';
2
+ interface RawMcpServer {
3
+ command: string;
4
+ args?: string[];
5
+ env?: Record<string, string>;
6
+ scope?: 'global' | 'project';
7
+ }
8
+ interface McpEntry extends RawMcpServer {
9
+ name: string;
10
+ scope: 'global' | 'project';
11
+ }
12
+ /** Best-effort MCP catalog. Used to decide what to whitelist when the
13
+ * caller asks for a subset. */
14
+ export declare function listMcps(workingDirectory: string): McpEntry[];
15
+ /**
16
+ * Write a temp `.mcp.json` containing ONLY the caller-allowed servers,
17
+ * suitable for `claude --mcp-config <path> --strict-mcp-config`. Returns
18
+ * null when no filtering is needed (caller wants all visible MCPs).
19
+ */
20
+ export declare function writeSessionMcpConfig(ptyId: string, workingDirectory: string, enabled: string[] | null): string | null;
21
+ export declare function cleanupSessionMcpConfig(filePath: string | undefined): void;
22
+ export declare function cleanupAllSessionMcpConfigs(): void;
23
+ export { CLAUDE_HOME };