autowonder 0.2.138 → 0.2.139

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,16 +6,16 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
6
6
 
7
7
  ```bash
8
8
  # Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
9
- npx -y autowonder@0.2.138 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.2.139 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.2.138 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
12
+ npx -y autowonder@0.2.139 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
13
13
 
14
14
  # Claude Code
15
- npx -y autowonder@0.2.138 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider claude
15
+ npx -y autowonder@0.2.139 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider claude
16
16
 
17
17
  # Codex CLI
18
- npx -y autowonder@0.2.138 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider codex --model gpt-5.5 --reasoning-effort medium
18
+ npx -y autowonder@0.2.139 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider codex --model gpt-5.5 --reasoning-effort medium
19
19
  ```
20
20
 
21
21
  `connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。选择 Qoder 时需要 Node.js 20+;runtime 会在缺少 `qodercli` 时自动通过 npm 安装,并在尚未登录时打开 Qoder 浏览器登录。Claude Code 和 Codex CLI 仍需提前安装并登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
@@ -62,7 +62,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
62
62
  也可以直接提交到本地 API:
63
63
 
64
64
  ```bash
65
- npx -y autowonder@0.2.138 dispatch ./assignment.json
65
+ npx -y autowonder@0.2.139 dispatch ./assignment.json
66
66
  ```
67
67
 
68
68
  ## 上传工单附件
@@ -70,7 +70,7 @@ npx -y autowonder@0.2.138 dispatch ./assignment.json
70
70
  把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
71
71
 
72
72
  ```bash
73
- npx -y autowonder@0.2.138 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
73
+ npx -y autowonder@0.2.139 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
74
74
  ```
75
75
 
76
76
  上传令牌通过 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)。
@@ -78,8 +78,8 @@ npx -y autowonder@0.2.138 workitem upload --server-url <autowonder-server-url> -
78
78
  ## 管理 daemon
79
79
 
80
80
  ```bash
81
- npx -y autowonder@0.2.138 status
82
- npx -y autowonder@0.2.138 stop
81
+ npx -y autowonder@0.2.139 status
82
+ npx -y autowonder@0.2.139 stop
83
83
  ```
84
84
 
85
85
  默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
@@ -87,7 +87,7 @@ npx -y autowonder@0.2.138 stop
87
87
  ## 调试
88
88
 
89
89
  ```bash
90
- npx -y autowonder@0.2.138 connect ... --debug
90
+ npx -y autowonder@0.2.139 connect ... --debug
91
91
  ```
92
92
 
93
93
  `--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
@@ -25,6 +25,7 @@ const QODER_FAMILY_PROFILES = {
25
25
  defaultConfigRootName: ".qoder",
26
26
  managedBinName: "qodercli",
27
27
  windowsShimName: "qodercli.cmd",
28
+ windowsInstallDirNames: ["Qoder CLI", "QoderCLI"],
28
29
  unixCandidateRelPaths: [
29
30
  [".local", "bin", "qodercli"],
30
31
  [".qoder", "bin", "qodercli", "qodercli"],
@@ -41,6 +42,7 @@ const QODER_FAMILY_PROFILES = {
41
42
  defaultConfigRootName: ".qoder-cn",
42
43
  managedBinName: "qodercn",
43
44
  windowsShimName: "qodercn.cmd",
45
+ windowsInstallDirNames: ["Qoder CN CLI", "QoderCNCLI"],
44
46
  unixCandidateRelPaths: [[".local", "bin", "qodercn"]],
45
47
  },
46
48
  };
@@ -63,10 +65,127 @@ function isQoderFamilyProvider(provider) {
63
65
  return provider === "qoder" || provider === "qodercn";
64
66
  }
65
67
 
68
+ // Quotes one argument for cmd.exe. Paths containing spaces must stay a single
69
+ // token when a .cmd shim is launched through ComSpec.
70
+ function quoteWindowsArg(arg) {
71
+ if (arg && !/[\s"]/.test(arg)) return arg;
72
+ return `"${String(arg).replace(/"/g, '\\"')}"`;
73
+ }
74
+
66
75
  function spawnQoderSync(executable, args, options = {}) {
67
76
  // npm exposes package binaries as .cmd shims on Windows. CreateProcess
68
- // cannot execute those shims directly, so let ComSpec resolve them.
69
- return spawnSync(executable, args, { ...options, shell: IS_WIN32 });
77
+ // cannot execute those shims directly, but shell:true would join the
78
+ // executable and its arguments with bare spaces and shred any path that
79
+ // contains a space (e.g. "...\Qoder CN CLI\..."). Launch ComSpec ourselves
80
+ // with an explicitly quoted command line instead; everything else runs as a
81
+ // plain argument array with no shell involved.
82
+ if (IS_WIN32 && /\.(cmd|bat)$/i.test(executable)) {
83
+ const comspec = process.env.ComSpec || "cmd.exe";
84
+ const command = [quoteWindowsArg(executable), ...args.map(quoteWindowsArg)].join(" ");
85
+ return spawnSync(comspec, ["/d", "/s", "/c", command], options);
86
+ }
87
+ return spawnSync(executable, args, { ...options, shell: false });
88
+ }
89
+
90
+ // Roots that hold user-level installers on Windows. Kept env-driven so the
91
+ // probe logic stays testable off Windows.
92
+ function windowsInstallRoots(env) {
93
+ const roots = [];
94
+ if (env.LOCALAPPDATA) roots.push(path.join(env.LOCALAPPDATA, "Programs"));
95
+ if (env.PROGRAMFILES) roots.push(env.PROGRAMFILES);
96
+ if (env["PROGRAMFILES(X86)"]) roots.push(env["PROGRAMFILES(X86)"]);
97
+ if (env.USERPROFILE) roots.push(env.USERPROFILE);
98
+ return roots;
99
+ }
100
+
101
+ // Matches a directory inside `parent` against the wanted names
102
+ // case-insensitively, also accepting the space-free spelling, so casing or
103
+ // spacing drift cannot hide an existing install.
104
+ function matchInstallDir(parent, wantedNames, probed) {
105
+ let entries;
106
+ try { entries = fs.readdirSync(parent); } catch { return ""; }
107
+ const wanted = new Set(wantedNames.map((name) => name.toLowerCase()));
108
+ for (const entry of entries) {
109
+ const lower = entry.toLowerCase();
110
+ if (!wanted.has(lower) && !wanted.has(lower.replace(/\s+/g, ""))) continue;
111
+ const candidate = path.join(parent, entry);
112
+ probed.push(candidate);
113
+ try {
114
+ if (fs.statSync(candidate).isDirectory()) return candidate;
115
+ } catch {}
116
+ }
117
+ return "";
118
+ }
119
+
120
+ // Relative locations of the CLI executable inside an install directory.
121
+ function windowsExeRelPaths(profile) {
122
+ const exe = profile.executable;
123
+ return [
124
+ `${exe}.exe`,
125
+ exe,
126
+ `${exe}.cmd`,
127
+ path.join("bin", `${exe}.exe`),
128
+ path.join("bin", exe),
129
+ path.join("bin", `${exe}.cmd`),
130
+ ];
131
+ }
132
+
133
+ // Probes the known install directories with the filesystem only; PATH entries
134
+ // and install paths are always treated as whole strings, never split on
135
+ // spaces. Returns the executable plus every location that was probed so a
136
+ // miss can be explained to the operator.
137
+ function findInInstallDirs(provider, env = process.env) {
138
+ const profile = QODER_FAMILY_PROFILES[provider];
139
+ const probed = [];
140
+ const roots = windowsInstallRoots(env);
141
+ roots.push(path.join(os.homedir(), profile.defaultConfigRootName));
142
+ for (const root of roots) {
143
+ const dir = matchInstallDir(root, profile.windowsInstallDirNames, probed);
144
+ if (!dir) {
145
+ for (const name of profile.windowsInstallDirNames) probed.push(path.join(root, name));
146
+ continue;
147
+ }
148
+ for (const rel of windowsExeRelPaths(profile)) {
149
+ const candidate = path.join(dir, rel);
150
+ probed.push(candidate);
151
+ try {
152
+ fs.accessSync(candidate, fs.constants.F_OK);
153
+ if (!fs.statSync(candidate).isDirectory()) return { found: candidate, probed };
154
+ } catch {}
155
+ }
156
+ }
157
+ return { found: "", probed };
158
+ }
159
+
160
+ // Everything a "not installed" report must show: the install candidates, the
161
+ // managed shim and any PATH entry that mentions the CLI family.
162
+ function qoderSearchSummary(provider) {
163
+ const profile = QODER_FAMILY_PROFILES[provider];
164
+ const paths = qoderPaths(profile);
165
+ const probed = [];
166
+ for (const root of windowsInstallRoots(process.env)) {
167
+ for (const name of profile.windowsInstallDirNames) probed.push(path.join(root, name));
168
+ }
169
+ probed.push(path.join(os.homedir(), profile.defaultConfigRootName));
170
+ probed.push(path.join(paths.managedDir, "node_modules", ".bin", profile.windowsShimName));
171
+ for (const rel of profile.unixCandidateRelPaths) probed.push(path.join(os.homedir(), ...rel));
172
+ const needle = "qoder";
173
+ const pathHints = (process.env.PATH || "").split(path.delimiter)
174
+ .filter((entry) => entry && entry.toLowerCase().includes(needle));
175
+ return { probed, pathHints };
176
+ }
177
+
178
+ function reportQoderNotFound(provider) {
179
+ const profile = QODER_FAMILY_PROFILES[provider];
180
+ const { probed, pathHints } = qoderSearchSummary(provider);
181
+ error(`${profile.displayName} was not found. Probed locations (PATH entries were scanned as whole strings):`);
182
+ probed.forEach((location) => error(` - ${location}`));
183
+ if (pathHints.length) {
184
+ error("PATH entries mentioning qoder:");
185
+ pathHints.forEach((entry) => error(` - ${entry}`));
186
+ } else {
187
+ error(`no PATH entry mentions "${profile.executable}".`);
188
+ }
70
189
  }
71
190
 
72
191
  function findQoderExecutable(provider) {
@@ -85,6 +204,8 @@ function findQoderExecutable(provider) {
85
204
  const onPath = findOnPath(profile.executable);
86
205
  if (onPath) return onPath;
87
206
  if (IS_WIN32) {
207
+ const { found } = findInInstallDirs(provider);
208
+ if (found) return found;
88
209
  const shim = path.join(paths.managedDir, "node_modules", ".bin", profile.windowsShimName);
89
210
  try { fs.accessSync(shim, fs.constants.F_OK); return shim; } catch {}
90
211
  return "";
@@ -137,6 +258,10 @@ function qoderBinaryVersion(executable) {
137
258
  stdio: ["ignore", "pipe", "pipe"],
138
259
  timeout: 20000,
139
260
  });
261
+ // "missing" means the file itself is absent; "unknown" means the file is
262
+ // there but produced no version, so the two failure classes never collapse
263
+ // into one misleading answer.
264
+ if (result.error && result.error.code === "ENOENT") return "missing";
140
265
  // stdout wins so a warning on stderr cannot masquerade as the version and
141
266
  // make the identity comparison see a change that did not happen.
142
267
  const firstLine = (stream) => (stream || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean);
@@ -385,6 +510,9 @@ function reportQoderDiagnostics(profile, info) {
385
510
  const { executable, version, configRoot, rootSource, verdict, identityChanged, previous } = info;
386
511
  error(`--- ${profile.displayName} bootstrap diagnostics ---`);
387
512
  error(`executable : ${executable}`);
513
+ if (/\s/.test(executable)) {
514
+ error("note : the executable path contains spaces; it is launched as an argument array / quoted command line");
515
+ }
388
516
  error(`version : ${version}`);
389
517
  error(`config root : ${configRoot} (${rootSource})`);
390
518
  error(`verdict : ${verdict.state} - ${verdict.reason || "no further detail"}`);
@@ -456,6 +584,7 @@ async function ensureQoderReady(provider, options = {}) {
456
584
  // reinstall the package that just landed.
457
585
  let refreshed = false;
458
586
  if (!executable) {
587
+ reportQoderNotFound(provider);
459
588
  executable = installQoderCLI(provider);
460
589
  refreshed = true;
461
590
  }
@@ -564,13 +693,17 @@ async function ensureQoderReady(provider, options = {}) {
564
693
  module.exports = {
565
694
  QODER_FAMILY_PROFILES,
566
695
  ensureQoderReady,
696
+ findInInstallDirs,
567
697
  findQoderExecutable,
568
698
  inspectQoderHealth,
569
699
  isQoderFamilyProvider,
570
700
  qoderBinaryVersion,
571
701
  qoderPaths,
702
+ qoderSearchSummary,
703
+ quoteWindowsArg,
572
704
  readQoderState,
573
705
  reportQoderDiagnostics,
706
+ reportQoderNotFound,
574
707
  selectQoderConfigRoot,
575
708
  spawnQoderSync,
576
709
  waitForQoderLogin,
package/bin/shared.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
 
3
- const { spawnSync } = require("child_process");
4
3
  const fs = require("fs");
5
4
  const os = require("os");
6
5
  const path = require("path");
@@ -25,11 +24,24 @@ function resolveUserPath(value) {
25
24
  return path.resolve(value);
26
25
  }
27
26
 
27
+ // Scans PATH entry by entry with the filesystem instead of shelling out to
28
+ // where/which: each entry stays a single string, so directories containing
29
+ // spaces can never be split or misquoted by a shell.
28
30
  function findOnPath(name) {
29
- const finder = process.platform === "win32" ? "where" : "which";
30
- const found = spawnSync(finder, [name], { encoding: "utf8", timeout: 3000 });
31
- if (found.status !== 0) return "";
32
- return (found.stdout || "").split(/\r?\n/).map((value) => value.trim()).find(Boolean) || "";
31
+ const isWin32 = process.platform === "win32";
32
+ const suffixes = isWin32 ? ["", ".exe", ".cmd", ".bat", ".com"] : [""];
33
+ const entries = (process.env.PATH || process.env.Path || "").split(path.delimiter);
34
+ for (const entry of entries) {
35
+ if (!entry) continue;
36
+ for (const suffix of suffixes) {
37
+ const candidate = path.join(entry, name + suffix);
38
+ try {
39
+ fs.accessSync(candidate, isWin32 ? fs.constants.F_OK : fs.constants.X_OK);
40
+ if (!fs.statSync(candidate).isDirectory()) return candidate;
41
+ } catch {}
42
+ }
43
+ }
44
+ return "";
33
45
  }
34
46
 
35
47
  module.exports = { ensureDir, error, findOnPath, log, resolveUserPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autowonder",
3
- "version": "0.2.138",
3
+ "version": "0.2.139",
4
4
  "description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
5
5
  "bin": {
6
6
  "autowonder": "bin/cli.js"