@nowcrew/daemon 0.3.0 → 0.4.3

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
@@ -23,7 +23,7 @@ const zh = {
23
23
  "Usage:": "用法:",
24
24
  "connect and stay resident": "连接并常驻",
25
25
  "run once manually": "手动运行一次",
26
- "crew-daemon resident, connecting to": "crew-daemon 常驻,连接到",
26
+ "resident, connecting to": "常驻,连接到",
27
27
  "control plane": "控制面",
28
28
  "Waking agent": "唤醒 agent",
29
29
  "for channel": "处理频道",
@@ -8,6 +8,7 @@ import { promisify } from "node:util";
8
8
  import { readFileSync } from "node:fs";
9
9
  import { readdir } from "node:fs/promises";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { createRequire } from "node:module";
11
12
  import { dirname, resolve } from "node:path";
12
13
  const execFileP = promisify(execFile);
13
14
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
@@ -35,7 +36,7 @@ export async function detectRuntimes() {
35
36
  const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
36
37
  return checks.filter((x) => x !== null);
37
38
  }
38
- function daemonVersion() {
39
+ export function daemonVersion() {
39
40
  try {
40
41
  const here = dirname(fileURLToPath(import.meta.url));
41
42
  const pkg = JSON.parse(readFileSync(resolve(here, "../package.json"), "utf8"));
@@ -45,6 +46,17 @@ function daemonVersion() {
45
46
  return "0.0.0";
46
47
  }
47
48
  }
49
+ /** 读取同装的 @nowcrew/cli 版本(daemon 的依赖,npx 场景与 daemon 一起安装);解析失败返回 "0.0.0"。 */
50
+ export function cliVersion() {
51
+ try {
52
+ const req = createRequire(import.meta.url);
53
+ const pkg = JSON.parse(readFileSync(req.resolve("@nowcrew/cli/package.json"), "utf8"));
54
+ return pkg.version ?? "0.0.0";
55
+ }
56
+ catch {
57
+ return "0.0.0";
58
+ }
59
+ }
48
60
  /** 列出 agentsRoot 下的 agent handle(每个子目录 = 一个 agent),跳过隐藏目录;读不到则返回 []。 */
49
61
  async function listAgentHandles(agentsRoot) {
50
62
  try {
package/dist/main.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { parseArgs } from "node:util";
9
9
  import { loadConfig, ConfigError } from "./config.js";
10
10
  import { detectDaemonLang, translateDaemon } from "./i18n.js";
11
+ import { cliVersion, daemonVersion } from "./machine-info.js";
11
12
  import { runAgent } from "./runner.js";
12
13
  import { serve } from "./serve.js";
13
14
  async function main() {
@@ -53,7 +54,7 @@ async function main() {
53
54
  throw e;
54
55
  }
55
56
  if (cmd === "serve") {
56
- process.stdout.write(`\n🛰️ ${td("crew-daemon resident, connecting to")} ${config.serverUrl} ${td("control plane")}...\n`);
57
+ process.stdout.write(`\n🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...\n`);
57
58
  serve(config);
58
59
  await new Promise(() => { }); // 常驻,直到被 kill
59
60
  return;
package/dist/normalize.js CHANGED
@@ -106,6 +106,9 @@ export function extractRunMeta(event) {
106
106
  meta.sessionId = e.session_id;
107
107
  if (typeof e.thread_id === "string" && e.thread_id)
108
108
  meta.sessionId = e.thread_id;
109
+ // claude 的 system/init 事件自报实际模型(配置可能为空/别名,以 runtime 自报为准)
110
+ if (typeof e.model === "string" && e.model)
111
+ meta.model = e.model;
109
112
  // 仅 result 事件携带本轮 usage 汇总
110
113
  if ((e.type === "result" || e.type === "turn.completed") && e.usage) {
111
114
  meta.usage = {
package/dist/runner.js CHANGED
@@ -72,6 +72,9 @@ onConsole = () => { }) {
72
72
  : baseWake;
73
73
  const childEnv = {
74
74
  ...process.env,
75
+ // 用户自定义 env(表单 ENVIRONMENT VARIABLES):先合入,可覆盖继承的 shell 环境;
76
+ // 但 PATH/CREW_*/XDG_* 及下方由表单生成的值(ANTHROPIC_*/思考预算)在其后合入,始终以系统为准。
77
+ ...sanitizeEnvVars(cfg.envVars),
75
78
  PATH: `${ws.crewDir}${delimiter}${process.env.PATH ?? ""}`,
76
79
  CREW_SERVER_URL: config.serverUrl,
77
80
  CREW_TOKEN: cred.token,
@@ -139,6 +142,7 @@ onConsole = () => { }) {
139
142
  // 身份是本轮下发的 launchSessionId;仍兜底采 claude 自报的 session_id(理应一致)。
140
143
  let sessionId = launchSessionId;
141
144
  let usage;
145
+ let observedModel = currentModel;
142
146
  let finalText = null;
143
147
  let sentViaCrew = false;
144
148
  const rl = createInterface({ input: child.stdout });
@@ -151,6 +155,8 @@ onConsole = () => { }) {
151
155
  sessionId = meta.sessionId;
152
156
  if (meta.usage)
153
157
  usage = meta.usage;
158
+ if (meta.model)
159
+ observedModel = meta.model;
154
160
  for (const a of normalizeEvent(evt)) {
155
161
  if (a.kind === "sending")
156
162
  sentViaCrew = true;
@@ -183,15 +189,21 @@ onConsole = () => { }) {
183
189
  onActivity(a);
184
190
  }
185
191
  if ((runtime === "codex" || runtime === "kimi") && exitCode === 0 && !sentViaCrew && finalText) {
192
+ // force:兜底回帖锚定本轮触发消息的线程,语义上必须送达;不 force 时 agent(-p 单发不跑
193
+ // crew read)游标落后,回帖会被 freshness hold 成 draft(202)而永远不可见。
186
194
  const sent = await sendAgentMessage(config.serverUrl, cred.token, input.channelId, {
187
195
  content: finalText,
196
+ force: true,
188
197
  ...(input.wakeMessageId ? { thread: input.wakeMessageId } : {}),
189
198
  });
190
- if (sent.ok) {
199
+ if (sent.delivered) {
191
200
  const a = { kind: "sending", label: "发消息", detail: `${runtime} final answer fallback` };
192
201
  activities.push(a);
193
202
  onActivity(a);
194
203
  }
204
+ else if (sent.status === 202) {
205
+ process.stderr.write(`${runtime} fallback reply was held as draft (HTTP 202) — not visible in channel\n`);
206
+ }
195
207
  else {
196
208
  process.stderr.write(`${runtime} fallback send failed (HTTP ${sent.status})\n`);
197
209
  }
@@ -210,14 +222,30 @@ onConsole = () => { }) {
210
222
  process.stdout.write(`📊 tokens: in=${u.inputTokens} out=${u.outputTokens} cache_read=${u.cacheReadTokens} cache_create=${u.cacheCreationTokens}` +
211
223
  `${u.costUsd != null ? ` cost=$${u.costUsd.toFixed(4)}` : ""} ${resuming ? "(resumed)" : "(fresh)"}\n`);
212
224
  }
213
- return { exitCode, activities, ...(usage ? { usage } : {}) };
225
+ return { exitCode, activities, model: observedModel, runtime, resumed: resuming, ...(usage ? { usage } : {}) };
214
226
  }
215
227
  function defaultPrint(a) {
216
228
  const icon = ICON[a.kind] ?? "·";
217
229
  const detail = a.detail ? ` ${a.detail.replace(/\s+/g, " ").slice(0, 120)}` : "";
218
230
  process.stdout.write(`${icon} ${a.label}${detail}\n`);
219
231
  }
220
- async function sendAgentMessage(serverUrl, token, channelId, body) {
232
+ // daemon 侧兜底(server 已校验,这里防旧数据/绕过):只接受合法 key string 值,CREW_ 前缀保留给系统。
233
+ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
234
+ export function sanitizeEnvVars(raw) {
235
+ if (!raw)
236
+ return {};
237
+ const out = {};
238
+ for (const [k, v] of Object.entries(raw)) {
239
+ if (typeof v !== "string")
240
+ continue;
241
+ if (!ENV_KEY_RE.test(k) || k.toUpperCase().startsWith("CREW_"))
242
+ continue;
243
+ out[k] = v;
244
+ }
245
+ return out;
246
+ }
247
+ /** exported for tests */
248
+ export async function sendAgentMessage(serverUrl, token, channelId, body) {
221
249
  const res = await fetch(`${serverUrl}/agent/channels/${encodeURIComponent(channelId)}/messages`, {
222
250
  method: "POST",
223
251
  headers: {
@@ -226,5 +254,7 @@ async function sendAgentMessage(serverUrl, token, channelId, body) {
226
254
  },
227
255
  body: JSON.stringify(body),
228
256
  });
229
- return { ok: res.ok, status: res.status };
257
+ // 只有 201(sent)算送达;202 表示被 freshness hold 成 draft——消息没有出现在频道里,
258
+ // 不能当成功(把 202 当成功正是 kimi/codex fallback 回帖丢失的根因)。
259
+ return { delivered: res.status === 201, status: res.status };
230
260
  }
package/dist/serve.js CHANGED
@@ -222,7 +222,7 @@ export function serve(config, opts = {}) {
222
222
  const readCmd = threadId
223
223
  ? `crew thread read`
224
224
  : `crew message read --channel ${msg.channelId}`;
225
- await runAgent(config, {
225
+ const result = await runAgent(config, {
226
226
  handle: msg.agentHandle,
227
227
  channelId: msg.channelId,
228
228
  taskKey, // 每任务隔离 cwd + work-log(并行不冲突)
@@ -230,6 +230,28 @@ export function serve(config, opts = {}) {
230
230
  ...(threadId ? { wakeMessageId: threadId } : {}),
231
231
  ...(msg.wake?.content ? { wake: `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}` } : {}),
232
232
  }, reportActivity, reportConsole);
233
+ // 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
234
+ // 连同模型/runtime 一起上送控制面落库 → 支撑每 agent / 每任务(线程)的用量监控与成本核算。
235
+ if (result.usage) {
236
+ const u = result.usage;
237
+ try {
238
+ ws?.send(JSON.stringify({
239
+ type: "agent:usage",
240
+ agentHandle: msg.agentHandle,
241
+ channelId: msg.channelId,
242
+ threadId: threadId ?? null, // 线程根消息 id(= 任务锚点);null = 频道级唤醒
243
+ runtime: result.runtime,
244
+ model: result.model,
245
+ resumed: result.resumed,
246
+ inputTokens: u.inputTokens,
247
+ outputTokens: u.outputTokens,
248
+ cacheReadTokens: u.cacheReadTokens,
249
+ cacheCreationTokens: u.cacheCreationTokens,
250
+ ...(u.costUsd != null ? { costUsd: u.costUsd } : {}),
251
+ }));
252
+ }
253
+ catch { /* ws 非 OPEN,忽略(用量非关键路径,丢一轮不阻塞) */ }
254
+ }
233
255
  reportActivity({ kind: "done", label: "本轮结束" });
234
256
  reportConsole({ stream: "result", text: "● 本轮结束" });
235
257
  log(`✅ agent=${msg.agentHandle} 本轮完成`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.3.0",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",