@nowcrew/daemon 0.5.28 → 0.5.29

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 (67) hide show
  1. package/dist/attachments.js +196 -0
  2. package/dist/bound-im-decision.js +22 -0
  3. package/dist/completion-retransmitter.js +77 -0
  4. package/dist/computer-cli.js +274 -0
  5. package/dist/computer-profile-lock.js +395 -0
  6. package/dist/computer-profile.js +364 -0
  7. package/dist/computer-service.js +358 -0
  8. package/dist/config.js +82 -0
  9. package/dist/console-collapse.js +13 -0
  10. package/dist/console-formatter.js +77 -0
  11. package/dist/console-payload.js +73 -0
  12. package/dist/console.js +329 -0
  13. package/dist/daemon-startup-error.js +30 -0
  14. package/dist/execution-backend.js +44 -0
  15. package/dist/execution-event-limit.js +64 -0
  16. package/dist/execution-journal-lock.js +421 -0
  17. package/dist/execution-journal.js +716 -0
  18. package/dist/execution-protocol.js +342 -0
  19. package/dist/execution-recovery.js +95 -0
  20. package/dist/execution-runner.js +659 -0
  21. package/dist/execution-supervisor-child.js +236 -0
  22. package/dist/execution-supervisor.js +316 -0
  23. package/dist/execution-telemetry-journal.js +71 -0
  24. package/dist/external-output.js +114 -0
  25. package/dist/i18n.js +64 -0
  26. package/dist/json-result.js +27 -0
  27. package/dist/list-models.js +92 -0
  28. package/dist/local-executor.js +439 -0
  29. package/dist/log-format.js +10 -0
  30. package/dist/machine-info.js +124 -0
  31. package/dist/main.js +118 -0
  32. package/dist/normalize.js +170 -0
  33. package/dist/origin-decision.js +44 -0
  34. package/dist/platform.js +8 -0
  35. package/dist/prompt.js +307 -0
  36. package/dist/provider-env.js +90 -0
  37. package/dist/runner.js +234 -0
  38. package/dist/runtime-cancellation.js +74 -0
  39. package/dist/runtime-capabilities.js +43 -0
  40. package/dist/runtime-path.js +60 -0
  41. package/dist/runtimes/claude.js +51 -0
  42. package/dist/runtimes/codex-app-server-runner.js +344 -0
  43. package/dist/runtimes/codex-deepseek-catalog.js +7 -0
  44. package/dist/runtimes/codex-deepseek-config.js +50 -0
  45. package/dist/runtimes/codex.js +53 -0
  46. package/dist/runtimes/kimi-acp-runner.js +364 -0
  47. package/dist/runtimes/kimi.js +45 -0
  48. package/dist/runtimes/progress-watchdog.js +26 -0
  49. package/dist/scheduled-report.js +51 -0
  50. package/dist/scheduled-run-report.js +57 -0
  51. package/dist/serve-lifecycle.js +82 -0
  52. package/dist/serve.js +868 -0
  53. package/dist/session.js +82 -0
  54. package/dist/shared-execution-slots.js +68 -0
  55. package/dist/shutdown-deadline.js +32 -0
  56. package/dist/skill-preview.js +21 -0
  57. package/dist/skills.js +56 -0
  58. package/dist/slog.js +228 -0
  59. package/dist/supervised-runtime.js +104 -0
  60. package/dist/token.js +24 -0
  61. package/dist/unified-diff.js +84 -0
  62. package/dist/websocket-shutdown.js +53 -0
  63. package/dist/win32-job-object.js +193 -0
  64. package/dist/workspace-fs.js +80 -0
  65. package/dist/workspace-import.js +127 -0
  66. package/dist/workspace.js +148 -0
  67. package/package.json +1 -1
package/dist/main.js ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * crew-daemon 入口 (M3b 当前形态:本地手动唤醒一个 agent 跑一轮)。
4
+ * crew-daemon run --agent <handle> --channel <id> [--wake "<text>"] [--display "<name>"]
5
+ *
6
+ * 后续 (M3c):常驻 + 连 server 控制面 WS,由 agent:start 自动唤醒。
7
+ */
8
+ import { parseArgs } from "node:util";
9
+ import { homedir } from "node:os";
10
+ import { loadConfig, ConfigError } from "./config.js";
11
+ import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
12
+ import { cliVersion, daemonVersion } from "./machine-info.js";
13
+ import { runAgent } from "./runner.js";
14
+ import { serve } from "./serve.js";
15
+ import { initSlog, flushSlog } from "./slog.js";
16
+ import { formatDaemonLogLine } from "./log-format.js";
17
+ import { applyProfileToEnv, assertProfileAgentsRootUnique, daemonHome, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, } from "./computer-profile.js";
18
+ import { runComputerCommand } from "./computer-cli.js";
19
+ import { runServeLifecycle } from "./serve-lifecycle.js";
20
+ import { formatDaemonStartupError } from "./daemon-startup-error.js";
21
+ async function main() {
22
+ const computerResult = await runComputerCommand(process.argv.slice(2));
23
+ if (computerResult !== null) {
24
+ process.exitCode = computerResult;
25
+ return;
26
+ }
27
+ const lang = detectDaemonLang();
28
+ const td = (message) => translateDaemon(lang, message);
29
+ const { values, positionals } = parseArgs({
30
+ args: process.argv.slice(2),
31
+ allowPositionals: true,
32
+ options: {
33
+ agent: { type: "string" },
34
+ channel: { type: "string" },
35
+ wake: { type: "string" },
36
+ display: { type: "string" },
37
+ // 连接参数(也可用 CREW_SERVER_URL / CREW_MACHINE_TOKEN 环境变量)
38
+ "server-url": { type: "string" },
39
+ "api-key": { type: "string" },
40
+ token: { type: "string" }, // --api-key 的别名
41
+ profile: { type: "string" },
42
+ "daemon-home": { type: "string" },
43
+ },
44
+ });
45
+ // 无子命令时默认 serve(对齐 `npx @nowcrew/daemon@latest --server-url ... --api-key ...`)
46
+ const cmd = positionals[0] ?? "serve";
47
+ if (cmd !== "run" && cmd !== "serve") {
48
+ process.stderr.write(td("Usage:") + "\n" +
49
+ " npx @nowcrew/daemon@latest --server-url <url> --api-key <sk_machine_*> # " + td("connect and stay resident") + "\n" +
50
+ " crew-daemon run --agent <h> --channel <id> [--wake ...] # " + td("run once manually") + "\n");
51
+ process.exit(2);
52
+ }
53
+ // profile 先加载,显式命令行参数仍具有最高优先级。服务描述只保留 profile 名,
54
+ // 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
55
+ if (values["daemon-home"])
56
+ process.env.CREW_DAEMON_HOME = values["daemon-home"];
57
+ if (values.profile) {
58
+ const home = daemonHome();
59
+ const profile = await loadProfile(values.profile, home);
60
+ if (cmd === "serve")
61
+ await assertProfileAgentsRootUnique(profile, home, homedir());
62
+ applyProfileToEnv(profile, process.env);
63
+ }
64
+ // 命令行参数优先于环境变量,填回 env 供 loadConfig 读取
65
+ if (values["server-url"])
66
+ process.env.CREW_SERVER_URL = values["server-url"];
67
+ const apiKey = values["api-key"] ?? values.token;
68
+ if (apiKey)
69
+ process.env.CREW_MACHINE_TOKEN = apiKey;
70
+ let config;
71
+ try {
72
+ config = loadConfig();
73
+ }
74
+ catch (e) {
75
+ if (e instanceof ConfigError) {
76
+ process.stderr.write(e.message + "\n");
77
+ process.exit(3);
78
+ }
79
+ throw e;
80
+ }
81
+ if (cmd === "serve") {
82
+ process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
83
+ const service = serve(config, {
84
+ ...(values.profile === undefined ? {} : { profileName: values.profile }),
85
+ });
86
+ await runServeLifecycle(service);
87
+ return;
88
+ }
89
+ if (!values.agent || !values.channel) {
90
+ process.stderr.write(`${td("Usage:")} crew-daemon run --agent <handle> --channel <id> [--wake ...]\n`);
91
+ process.exit(2);
92
+ }
93
+ process.stdout.write(formatDaemonLogLine(`🚀 ${td("Waking agent")} "${values.agent}" ${td("for channel")} ${values.channel}`) + "\n");
94
+ initSlog(config.serverUrl, config.machineToken); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
95
+ const result = await runAgent(config, {
96
+ handle: values.agent,
97
+ channelId: values.channel,
98
+ ...(values.wake ? { wake: values.wake } : {}),
99
+ ...(values.display ? { displayName: values.display } : {}),
100
+ });
101
+ process.stdout.write(formatDaemonLogLine(`— ${td("agent exited")} (code ${result.exitCode}), ${td("activities")}: ${result.activities.length} —`) + "\n");
102
+ await flushSlog();
103
+ process.exit(result.exitCode);
104
+ }
105
+ main().catch((e) => {
106
+ const lang = detectDaemonLang();
107
+ const message = formatDaemonStartupError(e, lang)
108
+ ?? (e instanceof ProfileAgentsRootConflictError
109
+ ? formatDaemonText(lang, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
110
+ profile: e.profile,
111
+ conflict: e.conflict,
112
+ agentsRoot: e.agentsRoot,
113
+ command: e.command,
114
+ })
115
+ : e.message);
116
+ process.stderr.write(`crew-daemon: ${message}\n`);
117
+ process.exitCode = 1;
118
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * 把 coding-agent runtime 的 stream-json 事件归一化为**语义活动**。
3
+ *
4
+ * 这是 daemon 的关键职责:前端/server 看到的是"在读历史/领任务/发消息",而非裸 Bash。
5
+ * 纯函数,无 IO,完整单测。
6
+ */
7
+ /**
8
+ * Strip /bin/zsh -lc, /bin/sh -c, or /bin/bash -lc shell wrappers (single OR double quotes),
9
+ * including multi-line heredoc forms. [\s\S]+ matches across newlines. Backreference \1 ensures
10
+ * opening and closing quotes match. classifyCommand requires the result to START with the crew
11
+ * prefix, keeping false-positive risk low (e.g. echo "crew message send" stays as tool).
12
+ * Codex runtime uses zsh/bash -lc and switches between single/double quotes depending on context
13
+ * (e.g. heredoc <<'CREWMSG' triggers double-quote wrapper: /bin/zsh -lc "cmd").
14
+ */
15
+ function unwrapShellCommand(command) {
16
+ const m = command.match(/^\/bin\/(?:zsh\s+-lc|sh\s+-c|bash\s+-lc)\s+(['"])([\s\S]+)\1$/);
17
+ return m?.[2] ?? command;
18
+ }
19
+ /** 把一条 Bash 命令归类为语义活动 (仅看 crew 子命令)。 */
20
+ export function classifyCommand(command) {
21
+ const c = command.trim();
22
+ if (/^crew\s+message\s+read\b/.test(c))
23
+ return { kind: "reading", label: "读历史", detail: c };
24
+ if (/^crew\s+message\s+send\b/.test(c))
25
+ return { kind: "sending", label: "发消息", detail: c };
26
+ if (/^crew\s+message\s+check\b/.test(c))
27
+ return { kind: "checking", label: "查未读", detail: c };
28
+ if (/^crew\s+task\s+claim\b/.test(c))
29
+ return { kind: "claiming", label: "领任务", detail: c };
30
+ if (/^crew\b/.test(c))
31
+ return { kind: "crew", label: "crew 命令", detail: c };
32
+ return { kind: "tool", label: "执行命令", detail: c };
33
+ }
34
+ /** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
35
+ export function extractFinalText(event) {
36
+ const e = (event ?? {});
37
+ if (e.type === "kimi.acp.text_delta" && e.text)
38
+ return e.text;
39
+ if (e.type === "result" && !e.is_error && e.result?.trim())
40
+ return e.result.trim();
41
+ if (e.type === "item.completed" && e.item?.type === "agent_message" && e.item.text?.trim()) {
42
+ return e.item.text.trim();
43
+ }
44
+ if (e.role === "assistant" && !e.type && typeof e.content === "string" && e.content.trim()) {
45
+ return e.content.trim();
46
+ }
47
+ return null;
48
+ }
49
+ /** kimi 的 Bash 工具 arguments 是 JSON 字符串({"command": "..."}),容错解析出 command。 */
50
+ function parseKimiBashCommand(args) {
51
+ if (!args)
52
+ return null;
53
+ try {
54
+ const parsed = JSON.parse(args);
55
+ return typeof parsed.command === "string" && parsed.command.trim() ? parsed.command.trim() : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /** 把一个 stream-json 事件归一化为 0..N 个活动。 */
62
+ export function normalizeEvent(event) {
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
+ }
73
+ if (e.type === "system" && e.subtype === "init") {
74
+ return [{ kind: "init", label: "agent 启动" }];
75
+ }
76
+ if (e.type === "thread.started") {
77
+ return [{ kind: "init", label: "agent 启动" }];
78
+ }
79
+ if (e.type === "item.completed" && e.item?.type === "agent_message" && e.item.text?.trim()) {
80
+ return [{ kind: "text", label: "思考/说明", detail: e.item.text.trim() }];
81
+ }
82
+ if (e.type === "item.completed" && e.item?.type === "command_execution" && e.item.command?.trim()) {
83
+ const command = e.item.command.trim();
84
+ return [classifyCommand(unwrapShellCommand(command))];
85
+ }
86
+ if (e.type === "turn.completed") {
87
+ return [{ kind: "done", label: "本轮结束" }];
88
+ }
89
+ if (e.type === "result") {
90
+ return e.is_error
91
+ ? [{ kind: "error", label: "运行出错", ...(e.result ? { detail: e.result } : {}) }]
92
+ : [{ kind: "done", label: "本轮结束", ...(e.result ? { detail: e.result } : {}) }];
93
+ }
94
+ if (e.type === "assistant" && Array.isArray(e.message?.content)) {
95
+ const out = [];
96
+ for (const block of e.message.content) {
97
+ if (block.type === "text" && block.text?.trim()) {
98
+ out.push({ kind: "text", label: "思考/说明", detail: block.text.trim() });
99
+ }
100
+ else if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
101
+ out.push(classifyCommand(block.input.command));
102
+ }
103
+ else if (block.type === "tool_use" && block.name) {
104
+ out.push({ kind: "tool", label: `工具:${block.name}` });
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+ if (e.type === "user" && Array.isArray(e.message?.content)) {
110
+ const hasResult = e.message.content.some((b) => b.type === "tool_result");
111
+ if (hasResult)
112
+ return [{ kind: "tool_result", label: "工具返回" }];
113
+ }
114
+ // kimi stream-json:{role:"assistant", content?, tool_calls?} / {role:"tool", ...}。
115
+ // kimi 行没有顶层 type(meta 行 role="meta",不产活动),与 claude/codex 分支互斥。
116
+ if (e.role === "assistant" && !e.type) {
117
+ const out = [];
118
+ if (typeof e.content === "string" && e.content.trim()) {
119
+ out.push({ kind: "text", label: "思考/说明", detail: e.content.trim() });
120
+ }
121
+ for (const call of Array.isArray(e.tool_calls) ? e.tool_calls : []) {
122
+ const name = call.function?.name ?? "";
123
+ const command = name === "Bash" ? parseKimiBashCommand(call.function?.arguments) : null;
124
+ if (command)
125
+ out.push(classifyCommand(command));
126
+ else if (name)
127
+ out.push({ kind: "tool", label: `工具:${name}` });
128
+ }
129
+ return out;
130
+ }
131
+ if (e.role === "tool" && !e.type) {
132
+ return [{ kind: "tool_result", label: "工具返回" }];
133
+ }
134
+ return [];
135
+ }
136
+ export function extractRunMeta(event) {
137
+ const e = (event ?? {});
138
+ const meta = {};
139
+ // 任何带 session_id 的事件(system/init、result …)都用来确认/更新 session id
140
+ if (typeof e.session_id === "string" && e.session_id)
141
+ meta.sessionId = e.session_id;
142
+ if (typeof e.thread_id === "string" && e.thread_id)
143
+ meta.sessionId = e.thread_id;
144
+ // claude 的 system/init 事件自报实际模型(配置可能为空/别名,以 runtime 自报为准)
145
+ if (typeof e.model === "string" && e.model)
146
+ meta.model = e.model;
147
+ // 仅 result 事件携带本轮 usage 汇总
148
+ if ((e.type === "result" || e.type === "turn.completed") && e.usage) {
149
+ meta.usage = {
150
+ inputTokens: e.usage.input_tokens ?? 0,
151
+ outputTokens: e.usage.output_tokens ?? 0,
152
+ cacheReadTokens: e.usage.cache_read_input_tokens ?? e.usage.cached_input_tokens ?? 0,
153
+ cacheCreationTokens: e.usage.cache_creation_input_tokens ?? 0,
154
+ ...(typeof e.total_cost_usd === "number" ? { costUsd: e.total_cost_usd } : {}),
155
+ };
156
+ }
157
+ return meta;
158
+ }
159
+ /** 解析一行 ndjson;非法行返回 null。 */
160
+ export function parseLine(line) {
161
+ const t = line.trim();
162
+ if (!t)
163
+ return null;
164
+ try {
165
+ return JSON.parse(t);
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
@@ -0,0 +1,44 @@
1
+ import { readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export function decisionFilePath(runDir) {
4
+ return join(runDir, ".origin-decision.json");
5
+ }
6
+ export async function resetOriginDecisionFile(path) {
7
+ await unlink(path).catch((error) => {
8
+ if (error.code !== "ENOENT")
9
+ throw error;
10
+ });
11
+ }
12
+ export async function readOriginDecisionFile(path) {
13
+ try {
14
+ const parsed = JSON.parse(await readFile(path, "utf8"));
15
+ if (!parsed || typeof parsed !== "object")
16
+ return null;
17
+ const value = parsed;
18
+ if (value.decision === "reply")
19
+ return { decision: "reply" };
20
+ if (value.decision !== "silent")
21
+ return null;
22
+ const reason = typeof value.reason === "string" ? value.reason.trim().slice(0, 500) : "";
23
+ return { decision: "silent", ...(reason ? { reason } : {}) };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function shouldRetryOriginDecision(wakeOrigin, decision, attempt) {
30
+ if (attempt !== 0 || wakeOrigin === undefined)
31
+ return false;
32
+ return wakeOrigin === "wecom" ? decision?.decision !== "reply" : decision === null;
33
+ }
34
+ export async function runWithOriginDecisionGuard(wakeOrigin, runAttempt) {
35
+ const first = await runAttempt(0);
36
+ const firstDecision = first.originDecision === "reply"
37
+ ? { decision: "reply" }
38
+ : first.originDecision === "silent" ? { decision: "silent" } : null;
39
+ if (!shouldRetryOriginDecision(wakeOrigin, firstDecision, 0)) {
40
+ return { results: [first], final: first };
41
+ }
42
+ const retry = await runAttempt(1);
43
+ return { results: [first, retry], final: retry };
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 平台差异集中点(win32 vs unix)。
3
+ * Windows 三个坑:没有 `which`(用 `where`);npm 全局 CLI 是 .cmd shim,
4
+ * spawn/execFile 不带 shell 无法执行(Node 18.20+ 直接拒绝);shebang 脚本不可执行。
5
+ */
6
+ export const isWin = (p = process.platform) => p === "win32";
7
+ /** 探测可执行文件用的命令:win32 = where,其余 = which。 */
8
+ export const lookupCmd = (p = process.platform) => (isWin(p) ? "where" : "which");