@nowcrew/daemon 0.5.13 → 0.5.15

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/dist/i18n.js CHANGED
@@ -29,9 +29,34 @@ const zh = {
29
29
  "for channel": "处理频道",
30
30
  "agent exited": "agent 退出",
31
31
  "activities": "活动数",
32
+ "Computer lifecycle:": "电脑生命周期:",
33
+ "profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin.": "profile save 从 CREW_MACHINE_TOKEN 或 --token-stdin 的标准输入读取机器令牌。",
34
+ "Missing {{name}}": "缺少 {{name}}",
35
+ "Service '{{id}}' is still installed; uninstall it before removing the profile": "服务 '{{id}}' 仍已安装;请先卸载服务再删除配置",
36
+ "Removed profile '{{name}}'.": "已删除配置 '{{name}}'。",
37
+ "Expected profile save, list, show, or remove": "profile 子命令应为 save、list、show 或 remove",
38
+ "Choose one token source: CREW_MACHINE_TOKEN or --token-stdin": "CREW_MACHINE_TOKEN 与 --token-stdin 只能选择一种令牌来源",
39
+ "Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin": "缺少机器令牌;请使用 CREW_MACHINE_TOKEN 或 --token-stdin",
40
+ "Saved profile '{{name}}' with private credentials.": "已保存配置 '{{name}}',凭证仅私有可读。",
41
+ "Service '{{id}}' is not installed": "服务 '{{id}}' 尚未安装",
42
+ "Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
43
+ "Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
44
+ "Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
45
+ "Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
46
+ "Uninstalled '{{id}}'.": "已卸载 '{{id}}'。",
47
+ "{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
48
+ "--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
49
+ "Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@latest",
32
50
  };
33
51
  export function translateDaemon(lang, message) {
34
52
  if (lang === "zh")
35
53
  return zh[message] ?? message;
36
54
  return message;
37
55
  }
56
+ export function formatDaemonText(lang, message, values = {}) {
57
+ let rendered = translateDaemon(lang, message);
58
+ for (const [key, value] of Object.entries(values)) {
59
+ rendered = rendered.replaceAll(`{{${key}}}`, String(value));
60
+ }
61
+ return rendered;
62
+ }
@@ -261,8 +261,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
261
261
  callbacks.onActivity?.(activity);
262
262
  }
263
263
  const extracted = extractFinalText(event);
264
- if (extracted)
265
- finalText = extracted;
264
+ if (extracted) {
265
+ const incremental = typeof event === "object" && event !== null
266
+ && "type" in event && event.type === "kimi.acp.text_delta";
267
+ finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
268
+ }
266
269
  for (const chunk of toConsoleLines(event))
267
270
  callbacks.onConsole?.(chunk);
268
271
  });
@@ -11,11 +11,16 @@ import { readdir } from "node:fs/promises";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { createRequire } from "node:module";
13
13
  import { dirname, resolve } from "node:path";
14
+ import { executableRuntimes } from "./runtime-capabilities.js";
15
+ import { executionBackendCapability } from "./execution-backend.js";
16
+ import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
17
+ export { executableRuntimes } from "./runtime-capabilities.js";
14
18
  const execFileP = promisify(execFile);
15
19
  export const DAEMON_CAPABILITIES = [
16
20
  "scheduled_job_v1",
17
21
  "reply_origin_v1",
18
22
  "origin_decision_v1",
23
+ "execution_telemetry_ack_v1",
19
24
  ];
20
25
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
21
26
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
@@ -43,6 +48,17 @@ export async function detectRuntimes() {
43
48
  const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
44
49
  return checks.filter((x) => x !== null);
45
50
  }
51
+ async function supportsKimiAcp() {
52
+ return probeKimiAcp({ bin: "kimi" });
53
+ }
54
+ /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
55
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp) {
56
+ const present = await installed;
57
+ const supported = executableRuntimes(present).filter((runtime) => runtime !== "kimi");
58
+ if (present.includes("kimi") && await kimiAcpProbe())
59
+ supported.push("kimi");
60
+ return supported;
61
+ }
46
62
  export function daemonVersion() {
47
63
  try {
48
64
  const here = dirname(fileURLToPath(import.meta.url));
@@ -77,22 +93,27 @@ async function listAgentHandles(agentsRoot) {
77
93
  return [];
78
94
  }
79
95
  }
80
- export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform) {
96
+ export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform, dependencies = {}) {
81
97
  const [runtimes, agentHandles] = await Promise.all([
82
- detectRuntimes(),
98
+ (dependencies.detectInstalled ?? detectRuntimes)(),
83
99
  listAgentHandles(agentsRoot),
84
100
  ]);
101
+ const backend = executionBackendCapability(runtimePlatform);
102
+ const executionRuntimes = backend.supported
103
+ ? await (dependencies.detectExecutable ?? detectExecutionRuntimes)(runtimes)
104
+ : [];
85
105
  return {
86
106
  type: "machine:hello",
87
107
  hostname: hostname(),
88
108
  os: `${osPlatform()} ${arch()}`,
89
109
  daemonVersion: daemonVersion(),
90
110
  runtimes,
111
+ executionRuntimes,
91
112
  capabilities: DAEMON_CAPABILITIES,
92
- ...(runtimePlatform === "win32" ? {} : {
113
+ ...(backend.supported ? {
93
114
  executionProtocol: EXECUTION_PROTOCOL,
94
115
  executionLimits: Object.freeze({ ...executionLimits }),
95
- }),
116
+ } : {}),
96
117
  agentHandles,
97
118
  };
98
119
  }
package/dist/main.js CHANGED
@@ -13,7 +13,14 @@ import { runAgent } from "./runner.js";
13
13
  import { serve } from "./serve.js";
14
14
  import { initSlog, flushSlog } from "./slog.js";
15
15
  import { formatDaemonLogLine } from "./log-format.js";
16
+ import { loadProfile, applyProfileToEnv } from "./computer-profile.js";
17
+ import { runComputerCommand } from "./computer-cli.js";
16
18
  async function main() {
19
+ const computerResult = await runComputerCommand(process.argv.slice(2));
20
+ if (computerResult !== null) {
21
+ process.exitCode = computerResult;
22
+ return;
23
+ }
17
24
  const lang = detectDaemonLang();
18
25
  const td = (message) => translateDaemon(lang, message);
19
26
  const { values, positionals } = parseArgs({
@@ -28,6 +35,8 @@ async function main() {
28
35
  "server-url": { type: "string" },
29
36
  "api-key": { type: "string" },
30
37
  token: { type: "string" }, // --api-key 的别名
38
+ profile: { type: "string" },
39
+ "daemon-home": { type: "string" },
31
40
  },
32
41
  });
33
42
  // 无子命令时默认 serve(对齐 `npx @nowcrew/daemon@latest --server-url ... --api-key ...`)
@@ -38,6 +47,12 @@ async function main() {
38
47
  " crew-daemon run --agent <h> --channel <id> [--wake ...] # " + td("run once manually") + "\n");
39
48
  process.exit(2);
40
49
  }
50
+ // profile 先加载,显式命令行参数仍具有最高优先级。服务描述只保留 profile 名,
51
+ // 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
52
+ if (values["daemon-home"])
53
+ process.env.CREW_DAEMON_HOME = values["daemon-home"];
54
+ if (values.profile)
55
+ applyProfileToEnv(await loadProfile(values.profile), process.env);
41
56
  // 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
42
57
  if (values["server-url"])
43
58
  process.env.CREW_SERVER_URL = values["server-url"];
package/dist/normalize.js CHANGED
@@ -34,6 +34,8 @@ export function classifyCommand(command) {
34
34
  /** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
35
35
  export function extractFinalText(event) {
36
36
  const e = (event ?? {});
37
+ if (e.type === "kimi.acp.text_delta" && e.text)
38
+ return e.text;
37
39
  if (e.type === "result" && !e.is_error && e.result?.trim())
38
40
  return e.result.trim();
39
41
  if (e.type === "item.completed" && e.item?.type === "agent_message" && e.item.text?.trim()) {
@@ -59,6 +61,15 @@ function parseKimiBashCommand(args) {
59
61
  /** 把一个 stream-json 事件归一化为 0..N 个活动。 */
60
62
  export function normalizeEvent(event) {
61
63
  const e = (event ?? {});
64
+ if (e.type === "kimi.acp.text_delta" && e.text?.trim()) {
65
+ return [{ kind: "text", label: "思考/说明", detail: e.text }];
66
+ }
67
+ if (e.type === "kimi.acp.tool_call") {
68
+ return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
69
+ }
70
+ if (e.type === "kimi.acp.tool_result") {
71
+ return [{ kind: "tool_result", label: "工具返回" }];
72
+ }
62
73
  if (e.type === "system" && e.subtype === "init") {
63
74
  return [{ kind: "init", label: "agent 启动" }];
64
75
  }
package/dist/prompt.js CHANGED
@@ -29,8 +29,25 @@ export function buildSystemPrompt(ctx) {
29
29
  ?? (ctx.scheduled ? "silent_unless_report" : null);
30
30
  const scheduled = scheduledPolicy !== null;
31
31
  const alwaysReport = scheduledPolicy === "always_report";
32
- // 启动序列:非 scheduled 保持原文(含"先确认接手"/"收到消息就处理并回复"——这两条是
33
- // 协作场景的核心礼仪);scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
32
+ const messageBodyInput = ctx.platform === "win32"
33
+ ? `PowerShell 5.1 向 native 进程传管道时默认可能使用 US-ASCII。发送中文等非 ASCII 正文时,**必须在同一条 shell 命令内**先设置无 BOM UTF-8:
34
+ \`\`\`powershell
35
+ $utf8 = New-Object System.Text.UTF8Encoding($false)
36
+ $OutputEncoding = $utf8
37
+ [Console]::OutputEncoding = $utf8
38
+ @'
39
+ 你的消息正文
40
+ '@ | crew message send --channel <id> --thread <完整线程根消息 id>
41
+ \`\`\`
42
+ 不得在未设置 \`$OutputEncoding\` 时把非 ASCII 正文通过 PowerShell 管道传给 \`crew\`。`
43
+ : `正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
44
+ \`\`\`bash
45
+ crew message send --channel <id> <<'CREWMSG'
46
+ 你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
47
+ CREWMSG
48
+ \`\`\``;
49
+ // 启动序列:普通交互对可直答来信只发一条完整回复,多步工作仍可发有信息量的进度;
50
+ // scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
34
51
  const startupSequence = alwaysReport
35
52
  ? `## 启动序列(每轮汇报定时任务)
36
53
  1. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
@@ -47,7 +64,7 @@ export function buildSystemPrompt(ctx) {
47
64
  4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
48
65
  5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
49
66
  : `## 启动序列
50
- 1. 若本轮已带具体来信,先判断是否需要立即确认/提问/声明接手;需要就先用 \`crew message send\` 发出,再去深挖上下文。确认消息的内容标准见下面「沟通风格」。
67
+ 1. 若本轮来信可以直接回答,不要先发确认消息;处理完后只发一条完整回复。只有需要澄清、调用工具、协调他人或较长时间处理时,才先发有信息量的确认/进度消息。
51
68
  2. 读 cwd 下的 MEMORY.md,以及处理本轮所需的其它笔记。
52
69
  3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
53
70
  4. 收到消息就处理,并用 \`crew message send\` 回复。
@@ -114,12 +131,7 @@ ${taskAndScheduleCommands}`
114
131
  1. **\`crew whoami\`** —— 查看你自己的身份。
115
132
  2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
116
133
  3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
117
- 4. **\`crew message send --channel <id>\`** —— 发消息。正文从 stdin 读,用 heredoc 避免 shell 解释引号/反引号/代码块:
118
- \`\`\`bash
119
- crew message send --channel <id> <<'CREWMSG'
120
- 你的消息正文,可含 "引号"、\\\`反引号\\\`、代码块。
121
- CREWMSG
122
- \`\`\`
134
+ 4. **\`crew message send --channel <id>\`** —— 发消息。${messageBodyInput}
123
135
  也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
124
136
  ${taskAndScheduleCommands}`;
125
137
  const freshnessRule = alwaysReport
package/dist/runner.js CHANGED
@@ -43,6 +43,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
43
43
  agentId: credential.agentId,
44
44
  homeDir: workspace.dir,
45
45
  productName: config.productName,
46
+ platform: process.platform,
46
47
  ...(input.scheduled ? { scheduledOutputPolicy: input.scheduled.outputPolicy } : {}),
47
48
  ...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
48
49
  memory: capMemoryForInject(workspace.memory),
@@ -0,0 +1,5 @@
1
+ export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi"];
2
+ const LOCAL_EXECUTION_RUNTIME_SET = new Set(LOCAL_EXECUTION_RUNTIMES);
3
+ export function executableRuntimes(installed) {
4
+ return [...new Set(installed)].filter((runtime) => LOCAL_EXECUTION_RUNTIME_SET.has(runtime));
5
+ }
@@ -0,0 +1,264 @@
1
+ import { once } from "node:events";
2
+ import { parseArgs } from "node:util";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { pathToFileURL } from "node:url";
5
+ import spawn from "cross-spawn";
6
+ import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
7
+ const ERROR_MESSAGE_CAP = 2_000;
8
+ const PROBE_TIMEOUT_MS = 5_000;
9
+ function jsonLine(event) {
10
+ if (process.stdout.write(`${JSON.stringify(event)}\n`))
11
+ return Promise.resolve();
12
+ return once(process.stdout, "drain").then(() => undefined);
13
+ }
14
+ function textContent(content) {
15
+ if (typeof content === "string")
16
+ return content;
17
+ if (!Array.isArray(content))
18
+ return "";
19
+ return content.flatMap((part) => {
20
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
21
+ return [part.text];
22
+ }
23
+ if (part && typeof part === "object" && "content" in part
24
+ && part.content && typeof part.content === "object"
25
+ && "type" in part.content && part.content.type === "text"
26
+ && "text" in part.content && typeof part.content.text === "string") {
27
+ return [part.content.text];
28
+ }
29
+ return [];
30
+ }).join("\n");
31
+ }
32
+ /** Translate stable ACP updates into daemon-owned NDJSON, without exposing thought chunks. */
33
+ export function mapKimiAcpUpdate(update) {
34
+ if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
35
+ return [{ type: "kimi.acp.text_delta", text: update.content.text }];
36
+ }
37
+ if (update.sessionUpdate === "tool_call") {
38
+ return [{
39
+ type: "kimi.acp.tool_call",
40
+ id: update.toolCallId,
41
+ title: update.title,
42
+ ...(update.kind === undefined ? {} : { kind: update.kind }),
43
+ ...(update.status === undefined ? {} : { status: update.status }),
44
+ ...(update.rawInput === undefined ? {} : { input: update.rawInput }),
45
+ }];
46
+ }
47
+ if (update.sessionUpdate === "tool_call_update") {
48
+ const output = textContent(update.content);
49
+ return [{
50
+ type: "kimi.acp.tool_result",
51
+ id: update.toolCallId,
52
+ ...(update.status === undefined ? {} : { status: update.status }),
53
+ ...(output ? { content: output } : {}),
54
+ }];
55
+ }
56
+ return [];
57
+ }
58
+ function safeErrorMessage(error, prompt) {
59
+ const raw = error instanceof Error ? error.message : String(error);
60
+ const redacted = prompt && raw.includes(prompt) ? raw.replaceAll(prompt, "[prompt redacted]") : raw;
61
+ return redacted.slice(0, ERROR_MESSAGE_CAP);
62
+ }
63
+ /** Full access may approve an operation, but it must never fabricate an answer to an agent question. */
64
+ export function selectKimiPermission(params) {
65
+ const allowOnce = params.options.filter((option) => option.kind === "allow_once");
66
+ if (params.toolCall.title === "AskUserQuestion" || allowOnce.length > 1) {
67
+ return { outcome: { outcome: "cancelled" } };
68
+ }
69
+ const allowed = allowOnce[0]
70
+ ?? (params.options.filter((option) => option.kind === "allow_always").length === 1
71
+ ? params.options.find((option) => option.kind === "allow_always")
72
+ : undefined);
73
+ return allowed === undefined
74
+ ? { outcome: { outcome: "cancelled" } }
75
+ : { outcome: { outcome: "selected", optionId: allowed.optionId } };
76
+ }
77
+ async function readPrompt() {
78
+ process.stdin.setEncoding("utf8");
79
+ let prompt = "";
80
+ for await (const chunk of process.stdin)
81
+ prompt += String(chunk);
82
+ if (!prompt)
83
+ throw new Error("Kimi ACP prompt is empty");
84
+ return prompt;
85
+ }
86
+ async function stopChild(child) {
87
+ if (child.exitCode !== null || child.signalCode !== null)
88
+ return;
89
+ const closed = once(child, "close").then(() => undefined);
90
+ child.kill("SIGTERM");
91
+ let timer;
92
+ const graceful = await Promise.race([
93
+ closed.then(() => true),
94
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
95
+ ]);
96
+ if (timer !== undefined)
97
+ clearTimeout(timer);
98
+ if (!graceful && child.exitCode === null && child.signalCode === null) {
99
+ child.kill("SIGKILL");
100
+ await closed;
101
+ }
102
+ }
103
+ /** A help banner is insufficient: Kimi's authenticate RPC re-checks its token without creating a session. */
104
+ export async function probeKimiAcp(options, spawnProcess = spawn) {
105
+ const child = spawnProcess(options.bin, ["acp"], {
106
+ cwd: process.cwd(),
107
+ env: process.env,
108
+ stdio: ["pipe", "pipe", "pipe"],
109
+ });
110
+ if (child.stdin === null || child.stdout === null || child.stderr === null)
111
+ return false;
112
+ child.stderr.resume();
113
+ const app = client({ name: "nowcrew-daemon-kimi-probe" })
114
+ .onRequest(methods.client.session.requestPermission, () => ({
115
+ outcome: { outcome: "cancelled" },
116
+ }))
117
+ .onNotification(methods.client.session.update, () => undefined);
118
+ let timeout;
119
+ try {
120
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
121
+ const connected = app.connectWith(stream, async (context) => {
122
+ const initialized = await context.request(methods.agent.initialize, {
123
+ protocolVersion: PROTOCOL_VERSION,
124
+ clientCapabilities: {},
125
+ clientInfo: { name: "nowcrew-daemon", version: "1" },
126
+ });
127
+ const authMethod = initialized.authMethods?.[0];
128
+ if (authMethod !== undefined) {
129
+ await context.request(methods.agent.authenticate, { methodId: authMethod.id });
130
+ }
131
+ return true;
132
+ });
133
+ const result = await Promise.race([
134
+ connected,
135
+ new Promise((resolve) => {
136
+ timeout = setTimeout(() => {
137
+ void stopChild(child);
138
+ resolve(false);
139
+ }, PROBE_TIMEOUT_MS);
140
+ }),
141
+ ]);
142
+ return result;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ finally {
148
+ if (timeout !== undefined)
149
+ clearTimeout(timeout);
150
+ await stopChild(child);
151
+ }
152
+ }
153
+ export async function runKimiAcp(options) {
154
+ const prompt = await readPrompt();
155
+ const child = spawn(options.bin, ["acp"], {
156
+ cwd: process.cwd(),
157
+ env: process.env,
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ });
160
+ if (child.stdin === null || child.stdout === null || child.stderr === null) {
161
+ throw new Error("Kimi ACP process did not expose stdio");
162
+ }
163
+ child.stderr.pipe(process.stderr, { end: false });
164
+ let context = null;
165
+ let sessionId = null;
166
+ let cancelling = false;
167
+ const cancel = async () => {
168
+ if (cancelling)
169
+ return;
170
+ cancelling = true;
171
+ if (context !== null && sessionId !== null) {
172
+ await context.notify(methods.agent.session.cancel, { sessionId }).catch(() => undefined);
173
+ }
174
+ await stopChild(child);
175
+ };
176
+ const onSignal = () => {
177
+ void cancel().finally(() => process.exit(130));
178
+ };
179
+ process.once("SIGTERM", onSignal);
180
+ process.once("SIGINT", onSignal);
181
+ const app = client({ name: "nowcrew-daemon-kimi" })
182
+ .onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
183
+ .onNotification(methods.client.session.update, async ({ params }) => {
184
+ for (const event of mapKimiAcpUpdate(params.update))
185
+ await jsonLine(event);
186
+ });
187
+ try {
188
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
189
+ const result = await app.connectWith(stream, async (nextContext) => {
190
+ context = nextContext;
191
+ const initialized = await nextContext.request(methods.agent.initialize, {
192
+ protocolVersion: PROTOCOL_VERSION,
193
+ clientCapabilities: {},
194
+ clientInfo: { name: "nowcrew-daemon", version: "1" },
195
+ });
196
+ if (process.env.CREW_KIMI_ACP_DEBUG === "1") {
197
+ process.stderr.write(`Kimi ACP auth methods: ${JSON.stringify(initialized.authMethods ?? [])}\n`);
198
+ }
199
+ const authMethod = initialized.authMethods?.[0];
200
+ if (authMethod !== undefined) {
201
+ await nextContext.request(methods.agent.authenticate, { methodId: authMethod.id });
202
+ }
203
+ const session = await nextContext.request(methods.agent.session.new, {
204
+ cwd: process.cwd(),
205
+ mcpServers: [],
206
+ });
207
+ sessionId = session.sessionId;
208
+ if (options.model) {
209
+ await nextContext.request(methods.agent.session.setConfigOption, {
210
+ sessionId,
211
+ configId: "model",
212
+ value: options.model,
213
+ });
214
+ }
215
+ return nextContext.request(methods.agent.session.prompt, {
216
+ sessionId,
217
+ prompt: [{ type: "text", text: prompt }],
218
+ });
219
+ });
220
+ const usage = result.usage;
221
+ await jsonLine({
222
+ type: "turn.completed",
223
+ stop_reason: result.stopReason,
224
+ ...(usage === undefined || usage === null ? {} : {
225
+ usage: {
226
+ input_tokens: usage.inputTokens,
227
+ output_tokens: usage.outputTokens,
228
+ cache_read_input_tokens: usage.cachedReadTokens ?? 0,
229
+ cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
230
+ },
231
+ }),
232
+ });
233
+ return result.stopReason === "end_turn" ? 0 : result.stopReason === "cancelled" ? 130 : 1;
234
+ }
235
+ catch (error) {
236
+ process.stderr.write(`Kimi ACP execution failed: ${safeErrorMessage(error, prompt)}\n`);
237
+ return 1;
238
+ }
239
+ finally {
240
+ process.off("SIGTERM", onSignal);
241
+ process.off("SIGINT", onSignal);
242
+ await stopChild(child);
243
+ }
244
+ }
245
+ function optionsFromArgv(argv) {
246
+ const { values } = parseArgs({
247
+ args: [...argv],
248
+ options: {
249
+ bin: { type: "string" },
250
+ model: { type: "string" },
251
+ },
252
+ });
253
+ if (!values.bin)
254
+ throw new Error("--bin is required");
255
+ return { bin: values.bin, ...(values.model ? { model: values.model } : {}) };
256
+ }
257
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
258
+ runKimiAcp(optionsFromArgv(process.argv.slice(2)))
259
+ .then((code) => { process.exitCode = code; })
260
+ .catch((error) => {
261
+ process.stderr.write(`Kimi ACP runner failed: ${safeErrorMessage(error, "")}\n`);
262
+ process.exitCode = 1;
263
+ });
264
+ }
@@ -10,6 +10,15 @@
10
10
  */
11
11
  // cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
12
12
  import spawn from "cross-spawn";
13
+ // Windows CreateProcess receives one UTF-16 command line. Reserve room for the executable, flags,
14
+ // model and cmd shim quoting instead of relying on the theoretical 32767-character ceiling.
15
+ export const KIMI_LEGACY_PROMPT_MAX_UTF16 = 28_000;
16
+ export function assertKimiLegacyPromptFits(prompt, platform = process.platform) {
17
+ if (platform !== "win32" || prompt.length <= KIMI_LEGACY_PROMPT_MAX_UTF16)
18
+ return;
19
+ throw new Error(`Kimi legacy prompt exceeds the Windows argv limit (${prompt.length}/${KIMI_LEGACY_PROMPT_MAX_UTF16}); `
20
+ + "shorten the prompt (protocol-v1 remains disabled on Windows until Job Object ownership is available)");
21
+ }
13
22
  // Kimi Code 思考强度档位(kimi-code 0.23.0 实测+源码):无 CLI 参数,
14
23
  // 由 runner 经 KIMI_MODEL_THINKING_EFFORT env 注入;白名单外的值不注。
15
24
  export const KIMI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
@@ -24,6 +33,7 @@ export function buildKimiArgs(input) {
24
33
  return args;
25
34
  }
26
35
  export function spawnKimi(input) {
36
+ assertKimiLegacyPromptFits(input.wakePrompt);
27
37
  // stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
28
38
  return spawn(input.bin, buildKimiArgs(input), {
29
39
  cwd: input.cwd,