autowonder 0.3.8 → 0.3.9

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 CHANGED
@@ -6,10 +6,10 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
6
6
 
7
7
  ```bash
8
8
  # Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
9
- npx -y autowonder@0.3.8 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
9
+ npx -y autowonder@0.3.9 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
10
10
 
11
11
  # Qoder CLI CN
12
- npx -y autowonder@0.3.8 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
12
+ npx -y autowonder@0.3.9 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
13
13
  ```
14
14
 
15
15
  `connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。客户端只支持通过 Qoder 系列 CLI 启动(`qoder` 对应 `qodercli`,`qodercn` 对应 `qodercn`),其他 `--provider` 取值会在启动 daemon 前直接报错拦截并提示 `Unsupported provider`。选择 Qoder CLI 或 Qoder CLI CN 时需要 Node.js 20+;runtime 会在缺少对应 CLI 时自动通过 npm 安装,并在尚未登录时打开浏览器登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
@@ -27,7 +27,7 @@ runtime 启动时始终只拉取一次 Platform 环境变量快照,即使已
27
27
 
28
28
  出于安全考虑,命令行 token 可能出现在 shell 历史和操作系统进程列表中,建议优先使用环境变量或 Platform 绑定。
29
29
 
30
- `connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.50`。检测到旧版或无法解析的版本时,runtime 会对当前优先选中的用户 CLI 执行内置 `update`,原位升级到最新版并再次强校验;升级失败或升级后仍不满足最低版本时拒绝启动 daemon。所有 Qoder 系列子进程(普通任务、conversation、warm 会话、恢复回退、命令与模型探测)都会强制注入固定的 `QODER_INTEGRATION_ID`,调用方环境不能覆盖该流量归因标识。
30
+ `connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.50`。版本探测保留退出码、信号与有界输出作为证据;单次探测没有拿到版本(超时、启动失败或输出无法解析)时先按有界节奏重试,确认确实不满足后才对当前优先选中的用户 CLI 执行内置 `update`,原位升级到最新版并再次强校验;升级失败或升级后仍不满足最低版本时拒绝启动 daemon,并在日志中打印探测证据。所有 Qoder 系列子进程(普通任务、conversation、warm 会话、恢复回退、命令与模型探测)都会强制注入固定的 `QODER_INTEGRATION_ID`,调用方环境不能覆盖该流量归因标识。
31
31
 
32
32
  `qodercn` 是国内版 Qoder CLI。runtime 会在缺少 `qodercn` 时自动把 `@qodercn-ai/qoderclicn` 安装到 `~/.autowonder/qoder-cn-cli`,并在尚未登录时打开浏览器登录。它使用自己的配置根(`QODERCN_CONFIG_DIR`,默认 `~/.qoder-cn`),与国际版 `qoder` 不共享可执行文件、凭证和会话。可用 `AUTOWONDER_QODERCN_CLI` 指定自定义可执行文件路径。
33
33
 
@@ -158,7 +158,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
158
158
  也可以直接提交到本地 API:
159
159
 
160
160
  ```bash
161
- npx -y autowonder@0.3.8 dispatch ./assignment.json
161
+ npx -y autowonder@0.3.9 dispatch ./assignment.json
162
162
  ```
163
163
 
164
164
  ## 上传工单附件
@@ -166,7 +166,7 @@ npx -y autowonder@0.3.8 dispatch ./assignment.json
166
166
  把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
167
167
 
168
168
  ```bash
169
- npx -y autowonder@0.3.8 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
169
+ npx -y autowonder@0.3.9 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
170
170
  ```
171
171
 
172
172
  上传令牌通过 MCP 工具 `autowonder.workitem_cli_upload_token` 签发,经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 环境变量传入。`--file` 可重复指定多个文件;`--json` 输出机器可读结果。失败时按错误类型返回不同退出码(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
@@ -176,7 +176,7 @@ npx -y autowonder@0.3.8 workitem upload --server-url <autowonder-server-url> --w
176
176
  把工单上已有的需求/设计文档下载到本地目录,再用本地文件工具阅读,避免把大文件或二进制附件搬进会话上下文:
177
177
 
178
178
  ```bash
179
- npx -y autowonder@0.3.8 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
179
+ npx -y autowonder@0.3.9 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
180
180
  ```
181
181
 
182
182
  下载令牌通过 MCP 工具 `autowonder.workitem_cli_download_token` 签发(与上传令牌相互独立),经 `--token` 或 `AUTOWONDER_DOWNLOAD_TOKEN` 环境变量传入。`--file` 可重复指定文档名或 artifact id,省略则下载该工单的全部附件;`--output-dir` 默认为当前目录;`--json` 输出机器可读结果,含每个文件的 `id`、`name`、`path`、`size`。退出码约定与 `workitem upload` 相同(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
@@ -186,7 +186,7 @@ npx -y autowonder@0.3.8 workitem download --server-url <autowonder-server-url> -
186
186
  把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
187
187
 
188
188
  ```bash
189
- npx -y autowonder@0.3.8 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
189
+ npx -y autowonder@0.3.9 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
190
190
  ```
191
191
 
192
192
  上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
@@ -194,8 +194,8 @@ npx -y autowonder@0.3.8 scheduled-task upload --server-url <autowonder-server-ur
194
194
  ## 管理 daemon
195
195
 
196
196
  ```bash
197
- npx -y autowonder@0.3.8 status
198
- npx -y autowonder@0.3.8 stop
197
+ npx -y autowonder@0.3.9 status
198
+ npx -y autowonder@0.3.9 stop
199
199
  ```
200
200
 
201
201
  默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
@@ -203,7 +203,7 @@ npx -y autowonder@0.3.8 stop
203
203
  ## 调试
204
204
 
205
205
  ```bash
206
- npx -y autowonder@0.3.8 connect ... --debug
206
+ npx -y autowonder@0.3.9 connect ... --debug
207
207
  ```
208
208
 
209
209
  `--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
package/bin/cli.js CHANGED
@@ -1051,10 +1051,14 @@ function parseFlags(args, knownFlags) {
1051
1051
 
1052
1052
  function ensureWindowsUTF8() {
1053
1053
  if (process.platform !== "win32") return;
1054
- const result = spawnSync("chcp", [], { shell: true, encoding: "utf8", timeout: 2000 });
1054
+ // chcp is a .com console tool and CreateProcess resolves no PATHEXT, so it is
1055
+ // addressed by full path. shell:true would join the code page into a cmd.exe
1056
+ // command line instead, which is what emits Node's DEP0190 warning.
1057
+ const chcp = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "chcp.com");
1058
+ const result = spawnSync(chcp, [], { encoding: "utf8", timeout: 2000 });
1055
1059
  const currentCP = (result.stdout || "").match(/:\s*(\d+)/)?.[1];
1056
1060
  if (currentCP !== "65001") {
1057
- spawnSync("chcp", ["65001"], { shell: true, stdio: "ignore" });
1061
+ spawnSync(chcp, ["65001"], { stdio: "ignore" });
1058
1062
  }
1059
1063
  process.env.PYTHONIOENCODING = process.env.PYTHONIOENCODING || "utf-8";
1060
1064
  process.env.PYTHONUTF8 = process.env.PYTHONUTF8 || "1";
@@ -340,23 +340,136 @@ function installQoderCLIUnlocked(provider) {
340
340
  const MACHINE_ID_REL_PATH = path.join(".auth", "machine_id");
341
341
  const CREDENTIAL_REL_PATH = path.join(".auth", "user");
342
342
 
343
- function qoderBinaryVersion(executable, options = {}) {
344
- if ((path.isAbsolute(executable) || executable.includes(path.sep)) && !fs.existsSync(executable)) return "missing";
343
+ // Both tree runners report a timeout as exit code 124, so a bound probe stays
344
+ // distinguishable from a CLI that really failed to start.
345
+ const QODER_PROBE_TIMEOUT_STATUS = 124;
346
+ const QODER_PROBE_OUTPUT_LIMIT = 1000;
347
+
348
+ function classifyQoderVersionProbe(result) {
349
+ if (result.error) {
350
+ if (result.error.code === "ENOENT") return "missing";
351
+ if (result.error.code === "ETIMEDOUT") return "timeout";
352
+ return "spawn_failed";
353
+ }
354
+ if (result.signal) return "failed";
355
+ if (result.status === null || result.status === undefined) return "timeout";
356
+ if (result.status === QODER_PROBE_TIMEOUT_STATUS) return "timeout";
357
+ if (result.status !== 0) return "failed";
358
+ return "unparseable";
359
+ }
360
+
361
+ // The probe keeps the raw evidence of the invocation so a permanent failure can
362
+ // be told apart from a transient one, and so the operator sees exit code,
363
+ // signal, spawn error and output instead of a single opaque "unknown".
364
+ function probeQoderBinaryVersion(executable, options = {}) {
365
+ const absent = (path.isAbsolute(executable) || executable.includes(path.sep)) && !fs.existsSync(executable);
366
+ const probe = {
367
+ executable,
368
+ version: absent ? "missing" : "unknown",
369
+ outcome: absent ? "missing" : "failed",
370
+ status: null,
371
+ signal: null,
372
+ spawnError: "",
373
+ stdout: "",
374
+ stderr: "",
375
+ };
376
+ if (absent) return probe;
345
377
  const result = spawnQoderSync(executable, ["--version"], {
346
378
  encoding: "utf8",
347
379
  stdio: ["ignore", "pipe", "pipe"],
348
380
  timeout: options.timeout || 20000,
349
381
  treeTimeout: true,
350
382
  });
383
+ probe.status = result.status === undefined ? null : result.status;
384
+ probe.signal = result.signal || null;
385
+ probe.spawnError = result.error
386
+ ? `${result.error.code ? `${result.error.code}: ` : ""}${result.error.message}`
387
+ : "";
388
+ probe.stdout = result.stdout || "";
389
+ probe.stderr = result.stderr || "";
390
+ probe.outcome = classifyQoderVersionProbe(result);
351
391
  // "missing" means the file itself is absent; "unknown" means the file is
352
392
  // there but produced no version, so the two failure classes never collapse
353
393
  // into one misleading answer.
354
- if (result.error && result.error.code === "ENOENT") return "missing";
355
- if (result.status !== 0) return "unknown";
356
- // Accept only an unambiguous Qoder version line. Runtime warnings and update
357
- // banners often contain unrelated/newer semvers and must never satisfy the
358
- // minimum gate for an old executable.
359
- return qoderVersionLine(`${result.stdout || ""}\n${result.stderr || ""}`) || "unknown";
394
+ if (probe.outcome === "missing") probe.version = "missing";
395
+ // Accept only an unambiguous Qoder version line, and only from a probe that
396
+ // exited zero. Runtime warnings and update banners often contain
397
+ // unrelated/newer semvers and must never satisfy the minimum gate for an old
398
+ // executable.
399
+ if (probe.outcome === "unparseable") {
400
+ const line = qoderVersionLine(`${probe.stdout}\n${probe.stderr}`);
401
+ if (line) {
402
+ probe.version = line;
403
+ probe.outcome = "ok";
404
+ }
405
+ }
406
+ return probe;
407
+ }
408
+
409
+ function qoderBinaryVersion(executable, options = {}) {
410
+ return probeQoderBinaryVersion(executable, options).version;
411
+ }
412
+
413
+ // "missing" is a definitive filesystem answer. Every other non-ok outcome can
414
+ // be a one-off glitch (first run, a short-lived file lock, a slow antivirus),
415
+ // which is exactly the field failure this retry exists for.
416
+ function isTransientQoderVersionProbe(probe) {
417
+ return probe.outcome !== "ok" && probe.outcome !== "missing";
418
+ }
419
+
420
+ // A single probe must not be trusted on its own: a CLI that is already up to
421
+ // date can answer nothing while its installation settles, and a just-updated
422
+ // CLI can need a moment before it reports the version it just installed.
423
+ // Re-probe on this bounded schedule instead of acting on one transient answer.
424
+ const QODER_VERSION_RETRY_DELAYS_MS = [300, 1000, 2000];
425
+
426
+ function qoderVersionRetryDelays() {
427
+ if (process.env.NODE_ENV === "test" && process.env.AUTOWONDER_TEST_QODER_VERSION_RETRY_DELAY_MS) {
428
+ const override = Number.parseInt(process.env.AUTOWONDER_TEST_QODER_VERSION_RETRY_DELAY_MS, 10);
429
+ if (Number.isFinite(override) && override >= 0) return QODER_VERSION_RETRY_DELAYS_MS.map(() => override);
430
+ }
431
+ return QODER_VERSION_RETRY_DELAYS_MS;
432
+ }
433
+
434
+ // The bootstrap runs before any event-loop work, so the wait blocks exactly
435
+ // like the update lock's own queue does.
436
+ function sleepMs(milliseconds) {
437
+ if (milliseconds > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
438
+ }
439
+
440
+ function describeQoderVersionProbe(probe) {
441
+ const status = probe.status === null ? "none" : probe.status;
442
+ return `version ${probe.version}, outcome ${probe.outcome}, exit code ${status}, signal ${probe.signal || "none"}, spawn error ${probe.spawnError || "none"}`;
443
+ }
444
+
445
+ // A chatty or hostile CLI must not be able to flood the operator's log.
446
+ function boundedQoderProbeOutput(text) {
447
+ const value = String(text || "").replace(/\s+/g, " ").trim();
448
+ if (!value) return "<empty>";
449
+ if (value.length <= QODER_PROBE_OUTPUT_LIMIT) return value;
450
+ return `${value.slice(0, QODER_PROBE_OUTPUT_LIMIT)}... (${value.length} characters total)`;
451
+ }
452
+
453
+ function reportQoderVersionProbe(probe) {
454
+ error(`probe outcome : ${probe.outcome}`);
455
+ error(`probe exit code : ${probe.status === null ? "none" : probe.status}`);
456
+ error(`probe signal : ${probe.signal || "none"}`);
457
+ error(`probe spawn err : ${probe.spawnError || "none"}`);
458
+ error(`probe stdout : ${boundedQoderProbeOutput(probe.stdout)}`);
459
+ error(`probe stderr : ${boundedQoderProbeOutput(probe.stderr)}`);
460
+ }
461
+
462
+ // Re-probes until `accept` is satisfied or the bounded schedule is spent,
463
+ // reusing `initialProbe` when the caller has already probed once.
464
+ function probeQoderVersionUntil(profile, executable, accept, initialProbe) {
465
+ let probe = initialProbe || probeQoderBinaryVersion(executable);
466
+ const delays = qoderVersionRetryDelays();
467
+ for (let step = 0; step < delays.length && !accept(probe); step += 1) {
468
+ log(`${profile.displayName} version probe did not settle (${describeQoderVersionProbe(probe)}); re-probing in ${delays[step]}ms...`);
469
+ sleepMs(delays[step]);
470
+ probe = probeQoderBinaryVersion(executable);
471
+ }
472
+ return probe;
360
473
  }
361
474
 
362
475
  function qoderVersionLine(output) {
@@ -568,10 +681,16 @@ function ensureMinimumQoderVersion(profile, provider, executable) {
568
681
  }
569
682
 
570
683
  function ensureMinimumQoderVersionLocked(profile, provider, executable) {
571
- let version = qoderBinaryVersion(executable);
572
- if (qoderVersionMeetsMinimum(version)) return { ok: true, version };
684
+ let probe = probeQoderBinaryVersion(executable);
685
+ if (isTransientQoderVersionProbe(probe)) {
686
+ // An up-to-date CLI answers nothing far more often than it is really too
687
+ // old, so confirm the failure before paying for a download.
688
+ log(`${profile.displayName} did not report a version on the first probe (${describeQoderVersionProbe(probe)}); re-probing before updating...`);
689
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => candidate.outcome === "ok", probe);
690
+ }
691
+ if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version };
573
692
 
574
- log(`${profile.displayName} ${version} is older than required version 1.1.50 (or could not be parsed). Updating the current installation to latest...`);
693
+ log(`${profile.displayName} ${probe.version} is older than required version 1.1.50 (or could not be parsed). Updating the current installation to latest...`);
575
694
  const update = spawnQoderSync(executable, ["update"], { stdio: "inherit", timeout: 5 * 60 * 1000, treeTimeout: true });
576
695
  if (update.status !== 0) {
577
696
  const managedDir = qoderPaths(profile).managedDir;
@@ -579,22 +698,26 @@ function ensureMinimumQoderVersionLocked(profile, provider, executable) {
579
698
  const reinstalled = installQoderCLI(provider, { lockHeld: true });
580
699
  if (reinstalled) {
581
700
  executable = reinstalled;
582
- version = qoderBinaryVersion(executable);
583
- if (qoderVersionMeetsMinimum(version)) return { ok: true, version, executable };
701
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => qoderVersionMeetsMinimum(candidate.version));
702
+ if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version, executable };
584
703
  }
585
704
  }
586
705
  const detail = update.error?.message || (update.signal ? `signal ${update.signal}` : `exit status ${update.status}`);
587
706
  error(`${profile.displayName} requires version 1.1.50 or newer, but updating ${executable} failed: ${detail}`);
588
- return { ok: false, version, executable };
707
+ reportQoderVersionProbe(probe);
708
+ return { ok: false, version: probe.version, executable };
589
709
  }
590
710
 
591
- version = qoderBinaryVersion(executable);
592
- if (!qoderVersionMeetsMinimum(version)) {
593
- error(`${profile.displayName} requires version 1.1.50 or newer, but ${executable} reports ${version} after updating to latest.`);
594
- return { ok: false, version, executable };
711
+ // The update reported success; give the installation the bounded schedule to
712
+ // start answering with the version it just installed.
713
+ probe = probeQoderVersionUntil(profile, executable, (candidate) => qoderVersionMeetsMinimum(candidate.version));
714
+ if (!qoderVersionMeetsMinimum(probe.version)) {
715
+ error(`${profile.displayName} requires version 1.1.50 or newer, but ${executable} reports ${probe.version} after updating to latest.`);
716
+ reportQoderVersionProbe(probe);
717
+ return { ok: false, version: probe.version, executable };
595
718
  }
596
- log(`${profile.displayName} updated in place to ${version}.`);
597
- return { ok: true, version, executable };
719
+ log(`${profile.displayName} updated in place to ${probe.version}.`);
720
+ return { ok: true, version: probe.version, executable };
598
721
  }
599
722
 
600
723
  // A missing or unreadable record means "no history", never a bootstrap failure.
@@ -1088,6 +1211,8 @@ async function ensureQoderReady(provider, options = {}) {
1088
1211
 
1089
1212
  module.exports = {
1090
1213
  QODER_FAMILY_PROFILES,
1214
+ QODER_VERSION_RETRY_DELAYS_MS,
1215
+ classifyQoderVersionProbe,
1091
1216
  ensureMinimumQoderVersion,
1092
1217
  ensureQoderReady,
1093
1218
  findInInstallDirs,
@@ -1096,6 +1221,7 @@ module.exports = {
1096
1221
  inspectQoderHealth,
1097
1222
  installQoderCLIUnlocked,
1098
1223
  isQoderFamilyProvider,
1224
+ probeQoderBinaryVersion,
1099
1225
  qoderBinaryVersion,
1100
1226
  qoderChildEnv,
1101
1227
  qoderUpdateLockPaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autowonder",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
5
5
  "bin": {
6
6
  "autowonder": "bin/cli.js"