@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.
- package/README.md +96 -0
- package/dist/agent-auth.d.ts +9 -0
- package/dist/agent-auth.js +96 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.js +164 -0
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +406 -0
- package/dist/claude-binary.d.ts +16 -0
- package/dist/claude-binary.js +145 -0
- package/dist/claude-config.d.ts +11 -0
- package/dist/claude-config.js +144 -0
- package/dist/claude-print.d.ts +35 -0
- package/dist/claude-print.js +312 -0
- package/dist/claude-spawn.d.ts +40 -0
- package/dist/claude-spawn.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +137 -0
- package/dist/codex-binary.d.ts +4 -0
- package/dist/codex-binary.js +70 -0
- package/dist/codex-spawn.d.ts +47 -0
- package/dist/codex-spawn.js +320 -0
- package/dist/command-runner.d.ts +9 -0
- package/dist/command-runner.js +592 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +292 -0
- package/dist/diskguard.d.ts +96 -0
- package/dist/diskguard.js +435 -0
- package/dist/error-classify.d.ts +20 -0
- package/dist/error-classify.js +159 -0
- package/dist/events.d.ts +26 -0
- package/dist/events.js +97 -0
- package/dist/gemini-binary.d.ts +4 -0
- package/dist/gemini-binary.js +64 -0
- package/dist/gemini-spawn.d.ts +52 -0
- package/dist/gemini-spawn.js +369 -0
- package/dist/git-identity.d.ts +18 -0
- package/dist/git-identity.js +104 -0
- package/dist/grok-binary.d.ts +4 -0
- package/dist/grok-binary.js +70 -0
- package/dist/grok-spawn.d.ts +41 -0
- package/dist/grok-spawn.js +495 -0
- package/dist/guardrails.d.ts +32 -0
- package/dist/guardrails.js +125 -0
- package/dist/hang-watchdog.d.ts +17 -0
- package/dist/hang-watchdog.js +48 -0
- package/dist/harness-registry.d.ts +55 -0
- package/dist/harness-registry.js +180 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +440 -0
- package/dist/install.d.ts +63 -0
- package/dist/install.js +467 -0
- package/dist/jsonl-tail.d.ts +20 -0
- package/dist/jsonl-tail.js +192 -0
- package/dist/kimi-binary.d.ts +4 -0
- package/dist/kimi-binary.js +68 -0
- package/dist/kimi-spawn.d.ts +47 -0
- package/dist/kimi-spawn.js +319 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +169 -0
- package/dist/mcp-config.d.ts +23 -0
- package/dist/mcp-config.js +133 -0
- package/dist/memory-capture.d.ts +20 -0
- package/dist/memory-capture.js +45 -0
- package/dist/memory-pack.d.ts +34 -0
- package/dist/memory-pack.js +171 -0
- package/dist/model-filter.d.ts +4 -0
- package/dist/model-filter.js +34 -0
- package/dist/node-health.d.ts +18 -0
- package/dist/node-health.js +74 -0
- package/dist/pairing.d.ts +20 -0
- package/dist/pairing.js +129 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +48 -0
- package/dist/precompact-hook.d.ts +27 -0
- package/dist/precompact-hook.js +136 -0
- package/dist/projects.d.ts +25 -0
- package/dist/projects.js +255 -0
- package/dist/prompt-keystrokes.d.ts +3 -0
- package/dist/prompt-keystrokes.js +56 -0
- package/dist/pty-helper.d.ts +26 -0
- package/dist/pty-helper.js +156 -0
- package/dist/request-pump.d.ts +12 -0
- package/dist/request-pump.js +249 -0
- package/dist/run-retry.d.ts +28 -0
- package/dist/run-retry.js +81 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +192 -0
- package/dist/session-runner.d.ts +130 -0
- package/dist/session-runner.js +2271 -0
- package/dist/sleep-detector.d.ts +11 -0
- package/dist/sleep-detector.js +32 -0
- package/dist/state-migrate.d.ts +8 -0
- package/dist/state-migrate.js +91 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.js +82 -0
- package/dist/supabase-client.d.ts +15 -0
- package/dist/supabase-client.js +119 -0
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +71 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.js +33 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.js +82 -0
- package/dist/tui/console-capture.d.ts +13 -0
- package/dist/tui/console-capture.js +62 -0
- package/dist/tui/dashboard.d.ts +42 -0
- package/dist/tui/dashboard.js +205 -0
- package/dist/tui/data.d.ts +57 -0
- package/dist/tui/data.js +62 -0
- package/dist/tui/harnesses.d.ts +16 -0
- package/dist/tui/harnesses.js +354 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.js +116 -0
- package/dist/tui/requests.d.ts +18 -0
- package/dist/tui/requests.js +127 -0
- package/dist/tui/run.d.ts +13 -0
- package/dist/tui/run.js +178 -0
- package/dist/tui/transcript.d.ts +39 -0
- package/dist/tui/transcript.js +234 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/win.d.ts +67 -0
- package/dist/win.js +317 -0
- package/package.json +60 -0
- package/scripts/fix-pty-helper.js +28 -0
- package/scripts/precompact-capture.js +292 -0
- 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;
|
package/dist/lockfile.js
ADDED
|
@@ -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 };
|