autowonder 0.2.130 → 0.2.132
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 +29 -6
- package/bin/cli.js +176 -33
- package/lib/workitem-upload.js +285 -0
- package/package.json +2 -1
- package/vendor/autowonder-daemon-darwin-amd64 +0 -0
- package/vendor/autowonder-daemon-darwin-arm64 +0 -0
- package/vendor/autowonder-daemon-linux-amd64 +0 -0
- package/vendor/autowonder-daemon-linux-arm64 +0 -0
- package/vendor/autowonder-daemon-win32-amd64.exe +0 -0
- package/vendor/autowonder-daemon-win32-arm64.exe +0 -0
package/README.md
CHANGED
|
@@ -6,17 +6,22 @@ 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.
|
|
9
|
+
npx -y autowonder@0.2.132 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
|
|
10
|
+
|
|
11
|
+
# Qoder CLI CN
|
|
12
|
+
npx -y autowonder@0.2.132 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
|
|
10
13
|
|
|
11
14
|
# Claude Code
|
|
12
|
-
npx -y autowonder@0.2.
|
|
15
|
+
npx -y autowonder@0.2.132 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider claude
|
|
13
16
|
|
|
14
17
|
# Codex CLI
|
|
15
|
-
npx -y autowonder@0.2.
|
|
18
|
+
npx -y autowonder@0.2.132 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider codex --model gpt-5.5 --reasoning-effort medium
|
|
16
19
|
```
|
|
17
20
|
|
|
18
21
|
`connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。选择 Qoder 时需要 Node.js 20+;runtime 会在缺少 `qodercli` 时自动通过 npm 安装,并在尚未登录时打开 Qoder 浏览器登录。Claude Code 和 Codex CLI 仍需提前安装并登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
|
|
19
22
|
|
|
23
|
+
`qodercn` 是国内版 Qoder CLI。runtime 会在缺少 `qodercn` 时自动把 `@qodercn-ai/qoderclicn` 安装到 `~/.autowonder/qoder-cn-cli`,并在尚未登录时打开浏览器登录。它使用自己的配置根(`QODERCN_CONFIG_DIR`,默认 `~/.qoder-cn`),与国际版 `qoder` 不共享可执行文件、凭证和会话。可用 `AUTOWONDER_QODERCN_CLI` 指定自定义可执行文件路径。
|
|
24
|
+
|
|
20
25
|
Qoder 的 Context Window 只能使用 `1000000`、`400000` 或 `260000`,Reasoning Effort 只能使用 `none`、`low`、`medium`、`high`、`xhigh` 或 `max`。建议直接复制 AutoWonder 执行器页面生成的命令,避免把模型展示名称误当成 provider model ID。
|
|
21
26
|
|
|
22
27
|
## 自动更新
|
|
@@ -57,14 +62,32 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
|
|
|
57
62
|
也可以直接提交到本地 API:
|
|
58
63
|
|
|
59
64
|
```bash
|
|
60
|
-
npx -y autowonder@0.2.
|
|
65
|
+
npx -y autowonder@0.2.132 dispatch ./assignment.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 上传工单附件
|
|
69
|
+
|
|
70
|
+
把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx -y autowonder@0.2.132 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
61
74
|
```
|
|
62
75
|
|
|
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)。
|
|
77
|
+
|
|
63
78
|
## 管理 daemon
|
|
64
79
|
|
|
65
80
|
```bash
|
|
66
|
-
npx -y autowonder@0.2.
|
|
67
|
-
npx -y autowonder@0.2.
|
|
81
|
+
npx -y autowonder@0.2.132 status
|
|
82
|
+
npx -y autowonder@0.2.132 stop
|
|
68
83
|
```
|
|
69
84
|
|
|
70
85
|
默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
|
|
86
|
+
|
|
87
|
+
## 调试
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx -y autowonder@0.2.132 connect ... --debug
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
|
package/bin/cli.js
CHANGED
|
@@ -24,6 +24,46 @@ const PID_PATH = path.join(AUTOWONDER_HOME, "daemon.pid");
|
|
|
24
24
|
const STATE_PATH = path.join(AUTOWONDER_HOME, "daemon-state.json");
|
|
25
25
|
const LOG_PATH = path.join(AUTOWONDER_HOME, "daemon.log");
|
|
26
26
|
const MANAGED_QODER_DIR = path.join(AUTOWONDER_HOME, "qoder-cli");
|
|
27
|
+
const MANAGED_QODERCN_DIR = path.join(AUTOWONDER_HOME, "qoder-cn-cli");
|
|
28
|
+
|
|
29
|
+
// qoder and qodercn are two distinct provider identities that share the same
|
|
30
|
+
// bootstrap flow. Each identity keeps its own executable, npm package, managed
|
|
31
|
+
// install dir and env override so the two CLIs never shadow each other.
|
|
32
|
+
const QODER_FAMILY_PROFILES = {
|
|
33
|
+
qoder: {
|
|
34
|
+
displayName: "Qoder CLI",
|
|
35
|
+
executable: "qodercli",
|
|
36
|
+
envVar: "AUTOWONDER_QODER_CLI",
|
|
37
|
+
npmPackage: "@qoder-ai/qodercli@latest",
|
|
38
|
+
managedDir: MANAGED_QODER_DIR,
|
|
39
|
+
managedBinName: "qodercli",
|
|
40
|
+
windowsShimName: "qodercli.cmd",
|
|
41
|
+
unixCandidates: [
|
|
42
|
+
path.join(os.homedir(), ".local", "bin", "qodercli"),
|
|
43
|
+
path.join(os.homedir(), ".qoder", "bin", "qodercli", "qodercli"),
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
qodercn: {
|
|
47
|
+
displayName: "Qoder CLI CN",
|
|
48
|
+
executable: "qodercn",
|
|
49
|
+
envVar: "AUTOWONDER_QODERCN_CLI",
|
|
50
|
+
npmPackage: "@qodercn-ai/qoderclicn@latest",
|
|
51
|
+
managedDir: MANAGED_QODERCN_DIR,
|
|
52
|
+
managedBinName: "qodercn",
|
|
53
|
+
windowsShimName: "qodercn.cmd",
|
|
54
|
+
unixCandidates: [
|
|
55
|
+
path.join(os.homedir(), ".local", "bin", "qodercn"),
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function profileForExecutable(name) {
|
|
61
|
+
return Object.values(QODER_FAMILY_PROFILES).find((profile) => profile.executable === name) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isQoderFamilyProvider(provider) {
|
|
65
|
+
return provider === "qoder" || provider === "qodercn";
|
|
66
|
+
}
|
|
27
67
|
|
|
28
68
|
const BINARY_BASE_URL = process.env.AUTOWONDER_BINARY_URL || "";
|
|
29
69
|
const SOURCE_REPO = process.env.AUTOWONDER_SOURCE_REPO || "";
|
|
@@ -59,8 +99,9 @@ function detectProvider() {
|
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
function findExecutable(name) {
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
const profile = profileForExecutable(name);
|
|
103
|
+
if (profile && process.env[profile.envVar]) {
|
|
104
|
+
const configured = resolveUserPath(process.env[profile.envVar]);
|
|
64
105
|
try { fs.accessSync(configured, fs.constants.X_OK); return configured; } catch {}
|
|
65
106
|
}
|
|
66
107
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -69,17 +110,17 @@ function findExecutable(name) {
|
|
|
69
110
|
const first = (found.stdout || "").split(/\r?\n/).map((value) => value.trim()).find(Boolean);
|
|
70
111
|
if (first) return first;
|
|
71
112
|
}
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
path.join(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
if (profile && process.platform !== "win32") {
|
|
114
|
+
const candidates = [
|
|
115
|
+
path.join(profile.managedDir, "node_modules", ".bin", profile.managedBinName),
|
|
116
|
+
...profile.unixCandidates,
|
|
117
|
+
];
|
|
118
|
+
for (const candidate of candidates) {
|
|
78
119
|
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch {}
|
|
79
120
|
}
|
|
80
121
|
}
|
|
81
|
-
if (
|
|
82
|
-
const candidate = path.join(
|
|
122
|
+
if (profile && process.platform === "win32") {
|
|
123
|
+
const candidate = path.join(profile.managedDir, "node_modules", ".bin", profile.windowsShimName);
|
|
83
124
|
try { fs.accessSync(candidate, fs.constants.F_OK); return candidate; } catch {}
|
|
84
125
|
}
|
|
85
126
|
return "";
|
|
@@ -100,10 +141,11 @@ function qoderAuthenticated(executable) {
|
|
|
100
141
|
return result.status === 0 && Boolean((result.stdout || "").trim());
|
|
101
142
|
}
|
|
102
143
|
|
|
103
|
-
function installQoderCLI() {
|
|
104
|
-
|
|
144
|
+
function installQoderCLI(provider) {
|
|
145
|
+
const profile = QODER_FAMILY_PROFILES[provider];
|
|
146
|
+
log(`${profile.displayName} is not installed. Installing ${profile.npmPackage}...`);
|
|
105
147
|
const args = [
|
|
106
|
-
"install", "--prefix",
|
|
148
|
+
"install", "--prefix", profile.managedDir, profile.npmPackage, "--no-audit", "--no-fund",
|
|
107
149
|
];
|
|
108
150
|
// npm is a .cmd shim on Windows. When launched by npx, npm_execpath points
|
|
109
151
|
// at npm-cli.js, which Node can execute directly without cmd.exe quoting.
|
|
@@ -112,16 +154,16 @@ function installQoderCLI() {
|
|
|
112
154
|
: spawnSync("npm", npmArgs, { stdio: "inherit" });
|
|
113
155
|
let result = runNpm(args);
|
|
114
156
|
if (result.status !== 0 && IS_WIN32) {
|
|
115
|
-
log(
|
|
157
|
+
log(`Default npm registry failed; retrying ${profile.displayName} installation with npmmirror...`);
|
|
116
158
|
result = runNpm([...args, "--registry", "https://registry.npmmirror.com"]);
|
|
117
159
|
}
|
|
118
160
|
if (result.status !== 0) {
|
|
119
161
|
const detail = result.error?.message || (result.signal ? `signal ${result.signal}` : `exit status ${result.status}`);
|
|
120
|
-
error(`Failed to install
|
|
162
|
+
error(`Failed to install ${profile.displayName} into ${profile.managedDir}: ${detail}`);
|
|
121
163
|
return "";
|
|
122
164
|
}
|
|
123
|
-
const executable = findExecutable(
|
|
124
|
-
if (!executable) error(
|
|
165
|
+
const executable = findExecutable(profile.executable);
|
|
166
|
+
if (!executable) error(`${profile.displayName} was installed but ${profile.executable} is not available on PATH.`);
|
|
125
167
|
return executable;
|
|
126
168
|
}
|
|
127
169
|
|
|
@@ -166,19 +208,20 @@ function qoderLoginCommand(executable) {
|
|
|
166
208
|
return IS_WIN32 ? `& ${quoted} login` : `${quoted} login`;
|
|
167
209
|
}
|
|
168
210
|
|
|
169
|
-
async function ensureQoderReady() {
|
|
211
|
+
async function ensureQoderReady(provider) {
|
|
170
212
|
if (process.env.AUTOWONDER_SKIP_QODER_BOOTSTRAP === "1") return true;
|
|
213
|
+
const profile = QODER_FAMILY_PROFILES[provider];
|
|
171
214
|
const nodeVersion = qoderNodeVersion();
|
|
172
215
|
const nodeMajor = Number.parseInt(nodeVersion.split(".")[0], 10);
|
|
173
216
|
if (!Number.isInteger(nodeMajor) || nodeMajor < 20) {
|
|
174
|
-
error(
|
|
217
|
+
error(`${profile.displayName} requires Node.js 20 or newer; current version is ${nodeVersion}. Upgrade Node.js and retry.`);
|
|
175
218
|
return false;
|
|
176
219
|
}
|
|
177
|
-
let executable = findExecutable(
|
|
178
|
-
if (!executable) executable = installQoderCLI();
|
|
220
|
+
let executable = findExecutable(profile.executable);
|
|
221
|
+
if (!executable) executable = installQoderCLI(provider);
|
|
179
222
|
if (!executable) return false;
|
|
180
223
|
if (!qoderAuthenticated(executable)) {
|
|
181
|
-
log(
|
|
224
|
+
log(`${profile.displayName} login is required. Opening browser login...`);
|
|
182
225
|
const login = spawnQoderSync(executable, ["login"], { stdio: "inherit" });
|
|
183
226
|
if (login.status !== 0) {
|
|
184
227
|
const detail = login.error?.message || `exit status ${login.status}`;
|
|
@@ -191,13 +234,13 @@ async function ensureQoderReady() {
|
|
|
191
234
|
return false;
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
|
-
process.env.
|
|
237
|
+
process.env[profile.envVar] = executable;
|
|
195
238
|
const executableDir = path.dirname(executable);
|
|
196
239
|
const pathEntries = (process.env.PATH || "").split(path.delimiter);
|
|
197
240
|
if (!pathEntries.includes(executableDir)) {
|
|
198
241
|
process.env.PATH = `${executableDir}${path.delimiter}${process.env.PATH || ""}`;
|
|
199
242
|
}
|
|
200
|
-
log(
|
|
243
|
+
log(`${profile.displayName} is installed and authenticated.`);
|
|
201
244
|
return true;
|
|
202
245
|
}
|
|
203
246
|
|
|
@@ -483,12 +526,12 @@ async function cmdConnect(args) {
|
|
|
483
526
|
const flags = parseFlags(args, ["ws-url", "token", "executor-id", "provider", "name", "max-tasks", "model", "reasoning-effort", "context-window", "memory-mode", "settings"]);
|
|
484
527
|
|
|
485
528
|
if (!flags["ws-url"] || !flags.token || !flags["executor-id"]) {
|
|
486
|
-
error("Usage: autowonder connect --ws-url <url> --token <token> --executor-id <id> [--provider qoder]");
|
|
529
|
+
error("Usage: autowonder connect --ws-url <url> --token <token> --executor-id <id> [--provider qoder|qodercn]");
|
|
487
530
|
process.exit(1);
|
|
488
531
|
}
|
|
489
532
|
|
|
490
533
|
const requestedProvider = flags.provider;
|
|
491
|
-
if (requestedProvider
|
|
534
|
+
if (isQoderFamilyProvider(requestedProvider) && !validateQoderOptions(flags)) process.exit(1);
|
|
492
535
|
|
|
493
536
|
const providers = detectProvider();
|
|
494
537
|
const provider = requestedProvider || providers[0];
|
|
@@ -496,7 +539,7 @@ async function cmdConnect(args) {
|
|
|
496
539
|
error(`No agent CLI detected. Install one of: claude, codex, qodercli`);
|
|
497
540
|
process.exit(1);
|
|
498
541
|
}
|
|
499
|
-
if (provider
|
|
542
|
+
if (isQoderFamilyProvider(provider) && !(await ensureQoderReady(provider))) process.exit(1);
|
|
500
543
|
|
|
501
544
|
// A version-pinned npx command must always run the binary bundled with that package.
|
|
502
545
|
// Windows locks a running .exe. Its versioned path is immutable, so another
|
|
@@ -505,15 +548,15 @@ async function cmdConnect(args) {
|
|
|
505
548
|
if (!installed) process.exit(1);
|
|
506
549
|
|
|
507
550
|
const hasProviderSettings = Boolean(flags.model || flags["reasoning-effort"]);
|
|
508
|
-
if (hasProviderSettings && provider !== "codex" && provider
|
|
509
|
-
error("--model and --reasoning-effort are supported only for codex and
|
|
551
|
+
if (hasProviderSettings && provider !== "codex" && !isQoderFamilyProvider(provider)) {
|
|
552
|
+
error("--model and --reasoning-effort are supported only for codex, qoder and qodercn");
|
|
510
553
|
process.exit(1);
|
|
511
554
|
}
|
|
512
|
-
if (flags["context-window"] && provider
|
|
513
|
-
error("--context-window is supported only for qoder");
|
|
555
|
+
if (flags["context-window"] && !isQoderFamilyProvider(provider)) {
|
|
556
|
+
error("--context-window is supported only for qoder and qodercn");
|
|
514
557
|
process.exit(1);
|
|
515
558
|
}
|
|
516
|
-
if (provider
|
|
559
|
+
if (isQoderFamilyProvider(provider) && !validateQoderOptions(flags)) process.exit(1);
|
|
517
560
|
let claudeSettingsPath = "";
|
|
518
561
|
if (flags.settings) {
|
|
519
562
|
if (provider !== "claude") {
|
|
@@ -574,6 +617,7 @@ async function cmdConnect(args) {
|
|
|
574
617
|
if (flags["reasoning-effort"]) env.AUTOWONDER_REASONING_EFFORT = flags["reasoning-effort"];
|
|
575
618
|
if (flags["context-window"]) env.AUTOWONDER_CONTEXT_WINDOW = flags["context-window"];
|
|
576
619
|
if (config.claudeSettingsPath) env.AUTOWONDER_CLAUDE_SETTINGS = config.claudeSettingsPath;
|
|
620
|
+
if (flags.debug) env.AUTOWONDER_DEBUG = "1";
|
|
577
621
|
|
|
578
622
|
const child = spawn(DAEMON_BIN, [], { env, stdio: "inherit" });
|
|
579
623
|
child.on("exit", (code) => process.exit(code || 0));
|
|
@@ -764,6 +808,94 @@ async function cmdDispatch(args) {
|
|
|
764
808
|
}
|
|
765
809
|
}
|
|
766
810
|
|
|
811
|
+
async function cmdWorkitem(args) {
|
|
812
|
+
const sub = args[0];
|
|
813
|
+
if (sub === "upload") {
|
|
814
|
+
return cmdWorkitemUpload(args.slice(1));
|
|
815
|
+
}
|
|
816
|
+
error(`Unknown workitem command: ${sub || "(none)"}`);
|
|
817
|
+
cmdHelp();
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function uploadExitCode(err) {
|
|
822
|
+
if (err && err.statusCode) {
|
|
823
|
+
switch (err.statusCode) {
|
|
824
|
+
case 401: return 3;
|
|
825
|
+
case 403: return 4;
|
|
826
|
+
case 404: return 5;
|
|
827
|
+
case 409: return 6;
|
|
828
|
+
case 413: return 7;
|
|
829
|
+
}
|
|
830
|
+
if (err.statusCode >= 400) return 8;
|
|
831
|
+
return 9;
|
|
832
|
+
}
|
|
833
|
+
if (err && (err.kind === "network" || err.kind === "redirect")) return 9;
|
|
834
|
+
return 2;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function reportUploadFailure(err, json) {
|
|
838
|
+
if (json) {
|
|
839
|
+
const payload = { success: false, error: err.message };
|
|
840
|
+
if (err.statusCode) payload.status = err.statusCode;
|
|
841
|
+
if (err.kind) payload.kind = err.kind;
|
|
842
|
+
if (err.serverError) payload.server = err.serverError;
|
|
843
|
+
console.log(JSON.stringify(payload));
|
|
844
|
+
} else {
|
|
845
|
+
error(err.message);
|
|
846
|
+
if (err.serverError && (err.serverError.message || err.serverError.code)) {
|
|
847
|
+
error(`server: ${err.serverError.message || "rejected"} (code ${err.serverError.code})`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function cmdWorkitemUpload(args) {
|
|
853
|
+
const upload = require("../lib/workitem-upload");
|
|
854
|
+
const json = args.includes("--json");
|
|
855
|
+
try {
|
|
856
|
+
const parsed = upload.parseUploadFlags(args);
|
|
857
|
+
const serverUrl = upload.validateServerUrl(parsed.serverUrl);
|
|
858
|
+
const workitemId = upload.validateWorkitemId(parsed.workitemId);
|
|
859
|
+
const token = parsed.token || process.env.AUTOWONDER_UPLOAD_TOKEN;
|
|
860
|
+
if (!token) {
|
|
861
|
+
throw new upload.UploadError(
|
|
862
|
+
"no upload token provided. Invoke the MCP tool autowonder.workitem_cli_upload_token to mint one, " +
|
|
863
|
+
"then pass it via --token or the AUTOWONDER_UPLOAD_TOKEN environment variable.",
|
|
864
|
+
{ kind: "token" },
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const files = upload.preflightFiles(parsed.files);
|
|
868
|
+
|
|
869
|
+
const { statusCode, body } = await upload.uploadWorkitemDocuments({
|
|
870
|
+
serverUrl, workitemId, files, token,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
if (statusCode === 200) {
|
|
874
|
+
let parsedBody = null;
|
|
875
|
+
try { parsedBody = JSON.parse(body); } catch { /* non-JSON success bodies are ignored */ }
|
|
876
|
+
if (parsed.json) {
|
|
877
|
+
console.log(JSON.stringify({
|
|
878
|
+
success: true,
|
|
879
|
+
workitemId: Number(workitemId),
|
|
880
|
+
uploaded: files.map((f) => f.filename),
|
|
881
|
+
response: parsedBody,
|
|
882
|
+
}));
|
|
883
|
+
} else {
|
|
884
|
+
log(`Uploaded ${files.length} file(s) to workitem ${workitemId}: ${files.map((f) => f.filename).join(", ")}`);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
throw new upload.UploadError(
|
|
890
|
+
`server rejected the upload (HTTP ${statusCode})`,
|
|
891
|
+
{ kind: "http", statusCode, serverError: upload.parseServerError(body) },
|
|
892
|
+
);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
reportUploadFailure(err, json);
|
|
895
|
+
process.exit(uploadExitCode(err));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
767
899
|
async function cmdInstall(args) {
|
|
768
900
|
const flags = parseFlags(args, ["force", "provider", "max-tasks", "addr"]);
|
|
769
901
|
const force = flags.force === "true" || args.includes("--force");
|
|
@@ -786,9 +918,11 @@ function cmdHelp() {
|
|
|
786
918
|
autowonder status Show daemon status
|
|
787
919
|
autowonder dispatch <assignment.json> Submit a dispatch
|
|
788
920
|
autowonder install [--force] Install/update and start daemon
|
|
921
|
+
autowonder workitem upload --server-url <url> --workitem-id <id> --file <path> [--file <path>...]
|
|
922
|
+
Upload local requirement/design files to a workitem
|
|
789
923
|
|
|
790
924
|
Options:
|
|
791
|
-
--provider <name> Agent provider: claude, codex, qoder (default: auto-detect)
|
|
925
|
+
--provider <name> Agent provider: claude, codex, qoder, qodercn (default: auto-detect; qodercn must be explicit)
|
|
792
926
|
--max-tasks <n> Max concurrent dispatches (default: 10)
|
|
793
927
|
--addr <host:port> Local API address (default: 127.0.0.1:34989)
|
|
794
928
|
--ws-url <url> Executor WebSocket endpoint
|
|
@@ -800,6 +934,11 @@ function cmdHelp() {
|
|
|
800
934
|
--settings <file> Claude settings JSON file
|
|
801
935
|
--memory-mode <mode> Memory: platform, provider-local, or none (default: platform)
|
|
802
936
|
--name <name> Runtime name (default: provider + executor ID)
|
|
937
|
+
--debug Stream full daemon and agent activity to console + log file
|
|
938
|
+
--server-url <url> AutoWonder server base URL (workitem upload)
|
|
939
|
+
--workitem-id <id> Target workitem ID (workitem upload)
|
|
940
|
+
--file <path> Local file to upload, repeatable (workitem upload)
|
|
941
|
+
--json Machine-readable output (workitem upload)
|
|
803
942
|
-h, --help Show this help
|
|
804
943
|
|
|
805
944
|
Examples:
|
|
@@ -810,6 +949,9 @@ function cmdHelp() {
|
|
|
810
949
|
npx -y autowonder start
|
|
811
950
|
npx -y autowonder dispatch ./assignment.json
|
|
812
951
|
|
|
952
|
+
# Upload requirement/design documents from local files
|
|
953
|
+
npx -y autowonder@0.2.130 workitem upload --server-url https://private-autowonder.example.com --workitem-id 50063 --file <filepath-1> --file <filepath-2> --file <images-1> --json
|
|
954
|
+
|
|
813
955
|
# Check what's happening
|
|
814
956
|
npx -y autowonder status
|
|
815
957
|
`);
|
|
@@ -847,6 +989,7 @@ async function main() {
|
|
|
847
989
|
case "status": return cmdStatus();
|
|
848
990
|
case "dispatch": return cmdDispatch(commandArgs);
|
|
849
991
|
case "install": return cmdInstall(commandArgs);
|
|
992
|
+
case "workitem": return cmdWorkitem(commandArgs);
|
|
850
993
|
case "help":
|
|
851
994
|
case "--help":
|
|
852
995
|
case "-h": return cmdHelp();
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Workitem requirement/design document upload: streaming multipart over
|
|
4
|
+
// Node >= 16 built-ins only. Never Base64 encodes and never buffers whole
|
|
5
|
+
// file bodies; the server is authoritative for validation.
|
|
6
|
+
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const http = require("node:http");
|
|
10
|
+
const https = require("node:https");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
|
|
13
|
+
const ALLOWED_EXTENSIONS = [".md", ".markdown", ".png", ".jpg", ".jpeg", ".webp"];
|
|
14
|
+
const MAX_FILES = 10;
|
|
15
|
+
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
const MAX_TOTAL_SIZE_BYTES = 20 * 1024 * 1024;
|
|
17
|
+
const UPLOAD_PATH_PREFIX = "/api/cli/workitems/";
|
|
18
|
+
const UPLOAD_PATH_SUFFIX = "/requirement-documents";
|
|
19
|
+
|
|
20
|
+
const CONTENT_TYPES = {
|
|
21
|
+
".md": "text/markdown",
|
|
22
|
+
".markdown": "text/markdown",
|
|
23
|
+
".png": "image/png",
|
|
24
|
+
".jpg": "image/jpeg",
|
|
25
|
+
".jpeg": "image/jpeg",
|
|
26
|
+
".webp": "image/webp",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class UploadError extends Error {
|
|
30
|
+
constructor(message, details = {}) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "UploadError";
|
|
33
|
+
Object.assign(this, details);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseUploadFlags(args) {
|
|
38
|
+
const FLAG_KEYS = {
|
|
39
|
+
"server-url": "serverUrl",
|
|
40
|
+
"workitem-id": "workitemId",
|
|
41
|
+
token: "token",
|
|
42
|
+
file: "file",
|
|
43
|
+
};
|
|
44
|
+
const options = { files: [], json: false };
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
if (arg === "--json") {
|
|
48
|
+
options.json = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!arg.startsWith("--")) {
|
|
52
|
+
throw new UploadError(`unexpected argument: ${arg}`, { kind: "usage" });
|
|
53
|
+
}
|
|
54
|
+
const key = arg.slice(2);
|
|
55
|
+
if (!FLAG_KEYS[key]) {
|
|
56
|
+
throw new UploadError(`unknown option: ${arg}`, { kind: "usage" });
|
|
57
|
+
}
|
|
58
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
|
|
59
|
+
throw new UploadError(`--${key} requires a value`, { kind: "usage" });
|
|
60
|
+
}
|
|
61
|
+
const value = args[++i];
|
|
62
|
+
if (key === "file") {
|
|
63
|
+
options.files.push(value);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const optionKey = FLAG_KEYS[key];
|
|
67
|
+
if (options[optionKey] !== undefined) {
|
|
68
|
+
throw new UploadError(`--${key} may only be specified once`, { kind: "usage" });
|
|
69
|
+
}
|
|
70
|
+
options[optionKey] = value;
|
|
71
|
+
}
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateServerUrl(value) {
|
|
76
|
+
if (!value) {
|
|
77
|
+
throw new UploadError("--server-url is required", { kind: "usage" });
|
|
78
|
+
}
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(value);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new UploadError(`--server-url must be an absolute http(s) URL: ${value}`, { kind: "usage" });
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
86
|
+
throw new UploadError(`--server-url must use http or https: ${value}`, { kind: "usage" });
|
|
87
|
+
}
|
|
88
|
+
if (url.username || url.password) {
|
|
89
|
+
throw new UploadError("--server-url must not embed credentials", { kind: "usage" });
|
|
90
|
+
}
|
|
91
|
+
if (url.search) {
|
|
92
|
+
throw new UploadError("--server-url must not contain a query string", { kind: "usage" });
|
|
93
|
+
}
|
|
94
|
+
if (url.hash) {
|
|
95
|
+
throw new UploadError("--server-url must not contain a fragment", { kind: "usage" });
|
|
96
|
+
}
|
|
97
|
+
return `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateWorkitemId(value) {
|
|
101
|
+
if (value === undefined || value === null || !/^[1-9][0-9]*$/.test(String(value))) {
|
|
102
|
+
throw new UploadError(`--workitem-id must be a positive integer: ${value}`, { kind: "usage" });
|
|
103
|
+
}
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function preflightFiles(filePaths) {
|
|
108
|
+
if (!filePaths.length) {
|
|
109
|
+
throw new UploadError("at least one --file is required", { kind: "usage" });
|
|
110
|
+
}
|
|
111
|
+
if (filePaths.length > MAX_FILES) {
|
|
112
|
+
throw new UploadError(`too many files: at most ${MAX_FILES} per upload`, { kind: "usage" });
|
|
113
|
+
}
|
|
114
|
+
let total = 0;
|
|
115
|
+
const prepared = [];
|
|
116
|
+
for (const filePath of filePaths) {
|
|
117
|
+
let stat;
|
|
118
|
+
try {
|
|
119
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
120
|
+
stat = fs.statSync(filePath);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new UploadError(`file not found or not readable: ${filePath}`, { kind: "usage" });
|
|
123
|
+
}
|
|
124
|
+
if (!stat.isFile()) {
|
|
125
|
+
throw new UploadError(`not a regular file: ${filePath}`, { kind: "usage" });
|
|
126
|
+
}
|
|
127
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
128
|
+
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
|
129
|
+
throw new UploadError(
|
|
130
|
+
`unsupported file type: ${filePath} (allowed: ${ALLOWED_EXTENSIONS.join(", ")})`,
|
|
131
|
+
{ kind: "usage" },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
135
|
+
throw new UploadError(
|
|
136
|
+
`file too large: ${filePath} (${stat.size} bytes; limit is ${MAX_FILE_SIZE_BYTES})`,
|
|
137
|
+
{ kind: "usage" },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
total += stat.size;
|
|
141
|
+
prepared.push({ filePath, filename: path.basename(filePath), size: stat.size });
|
|
142
|
+
}
|
|
143
|
+
if (total > MAX_TOTAL_SIZE_BYTES) {
|
|
144
|
+
throw new UploadError(
|
|
145
|
+
`total upload size too large: ${total} bytes (limit is ${MAX_TOTAL_SIZE_BYTES})`,
|
|
146
|
+
{ kind: "usage" },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return prepared;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeChunk(request, chunk) {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
if (request.write(chunk)) {
|
|
155
|
+
resolve();
|
|
156
|
+
} else {
|
|
157
|
+
request.once("drain", resolve);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writePart(request, boundary, file) {
|
|
163
|
+
const safeName = file.filename.replace(/["\r\n]/g, "_");
|
|
164
|
+
const contentType = CONTENT_TYPES[path.extname(file.filename).toLowerCase()] || "application/octet-stream";
|
|
165
|
+
await writeChunk(
|
|
166
|
+
request,
|
|
167
|
+
`--${boundary}\r\n` +
|
|
168
|
+
`Content-Disposition: form-data; name="files"; filename="${safeName}"\r\n` +
|
|
169
|
+
`Content-Type: ${contentType}\r\n\r\n`,
|
|
170
|
+
);
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
const stream = fs.createReadStream(file.filePath);
|
|
173
|
+
stream.on("error", (err) => {
|
|
174
|
+
err.filePath = file.filePath;
|
|
175
|
+
reject(err);
|
|
176
|
+
});
|
|
177
|
+
stream.on("data", (chunk) => {
|
|
178
|
+
if (!request.write(chunk)) {
|
|
179
|
+
stream.pause();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
request.on("drain", () => stream.resume());
|
|
183
|
+
stream.on("end", resolve);
|
|
184
|
+
});
|
|
185
|
+
await writeChunk(request, "\r\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseServerError(body) {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(body);
|
|
191
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
192
|
+
} catch {
|
|
193
|
+
// non-JSON bodies are kept as-is by callers
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function uploadWorkitemDocuments({ serverUrl, workitemId, files, token }) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const boundary = `----autowonder${crypto.randomBytes(16).toString("hex")}`;
|
|
201
|
+
const url = new URL(`${serverUrl}${UPLOAD_PATH_PREFIX}${workitemId}${UPLOAD_PATH_SUFFIX}`);
|
|
202
|
+
const client = url.protocol === "https:" ? https : http;
|
|
203
|
+
let settled = false;
|
|
204
|
+
let sentAny = false;
|
|
205
|
+
|
|
206
|
+
const fail = (err) => {
|
|
207
|
+
if (!settled) {
|
|
208
|
+
settled = true;
|
|
209
|
+
reject(err);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const finish = (value) => {
|
|
213
|
+
if (!settled) {
|
|
214
|
+
settled = true;
|
|
215
|
+
resolve(value);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const request = client.request(
|
|
220
|
+
url,
|
|
221
|
+
{
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
225
|
+
Authorization: `Bearer ${token}`,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
(response) => {
|
|
229
|
+
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
230
|
+
request.destroy();
|
|
231
|
+
fail(new UploadError(
|
|
232
|
+
`server responded with a redirect (HTTP ${response.statusCode}); refusing to follow redirects`,
|
|
233
|
+
{ kind: "redirect", statusCode: response.statusCode },
|
|
234
|
+
));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let body = "";
|
|
238
|
+
response.on("data", (chunk) => { body += chunk; });
|
|
239
|
+
response.on("end", () => finish({ statusCode: response.statusCode, body }));
|
|
240
|
+
response.on("error", (err) => fail(new UploadError(`response error: ${err.message}`, { kind: "network" })));
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
request.on("error", (err) => {
|
|
245
|
+
fail(new UploadError(
|
|
246
|
+
sentAny
|
|
247
|
+
? "connection lost after part of the upload was sent; the outcome is ambiguous. " +
|
|
248
|
+
"List the workitem's documents to see what arrived before retrying."
|
|
249
|
+
: `connection failed: ${err.message}`,
|
|
250
|
+
{ kind: "network" },
|
|
251
|
+
));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
(async () => {
|
|
255
|
+
try {
|
|
256
|
+
for (const file of files) {
|
|
257
|
+
await writePart(request, boundary, file);
|
|
258
|
+
sentAny = true;
|
|
259
|
+
}
|
|
260
|
+
await writeChunk(request, `--${boundary}--\r\n`);
|
|
261
|
+
request.end();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
request.destroy();
|
|
264
|
+
fail(new UploadError(
|
|
265
|
+
`failed reading ${err.filePath || "file"}: ${err.message}`,
|
|
266
|
+
{ kind: "read" },
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
ALLOWED_EXTENSIONS,
|
|
275
|
+
MAX_FILES,
|
|
276
|
+
MAX_FILE_SIZE_BYTES,
|
|
277
|
+
MAX_TOTAL_SIZE_BYTES,
|
|
278
|
+
UploadError,
|
|
279
|
+
parseUploadFlags,
|
|
280
|
+
validateServerUrl,
|
|
281
|
+
validateWorkitemId,
|
|
282
|
+
preflightFiles,
|
|
283
|
+
parseServerError,
|
|
284
|
+
uploadWorkitemDocuments,
|
|
285
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autowonder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.132",
|
|
4
4
|
"description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
|
|
5
5
|
"bin": {
|
|
6
6
|
"autowonder": "bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
+
"lib/",
|
|
10
11
|
"scripts/",
|
|
11
12
|
"vendor/"
|
|
12
13
|
],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|