autowonder 0.2.131 → 0.2.133

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.131 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.133 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.131 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
12
+ npx -y autowonder@0.2.133 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.131 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider claude
15
+ npx -y autowonder@0.2.133 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.131 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.133 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,14 +62,32 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
62
62
  也可以直接提交到本地 API:
63
63
 
64
64
  ```bash
65
- npx -y autowonder@0.2.131 dispatch ./assignment.json
65
+ npx -y autowonder@0.2.133 dispatch ./assignment.json
66
66
  ```
67
67
 
68
+ ## 上传工单附件
69
+
70
+ 把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
71
+
72
+ ```bash
73
+ npx -y autowonder@0.2.133 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
74
+ ```
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
+
68
78
  ## 管理 daemon
69
79
 
70
80
  ```bash
71
- npx -y autowonder@0.2.131 status
72
- npx -y autowonder@0.2.131 stop
81
+ npx -y autowonder@0.2.133 status
82
+ npx -y autowonder@0.2.133 stop
73
83
  ```
74
84
 
75
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.133 connect ... --debug
91
+ ```
92
+
93
+ `--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
package/bin/cli.js CHANGED
@@ -617,6 +617,7 @@ async function cmdConnect(args) {
617
617
  if (flags["reasoning-effort"]) env.AUTOWONDER_REASONING_EFFORT = flags["reasoning-effort"];
618
618
  if (flags["context-window"]) env.AUTOWONDER_CONTEXT_WINDOW = flags["context-window"];
619
619
  if (config.claudeSettingsPath) env.AUTOWONDER_CLAUDE_SETTINGS = config.claudeSettingsPath;
620
+ if (flags.debug) env.AUTOWONDER_DEBUG = "1";
620
621
 
621
622
  const child = spawn(DAEMON_BIN, [], { env, stdio: "inherit" });
622
623
  child.on("exit", (code) => process.exit(code || 0));
@@ -807,6 +808,94 @@ async function cmdDispatch(args) {
807
808
  }
808
809
  }
809
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
+
810
899
  async function cmdInstall(args) {
811
900
  const flags = parseFlags(args, ["force", "provider", "max-tasks", "addr"]);
812
901
  const force = flags.force === "true" || args.includes("--force");
@@ -829,6 +918,8 @@ function cmdHelp() {
829
918
  autowonder status Show daemon status
830
919
  autowonder dispatch <assignment.json> Submit a dispatch
831
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
832
923
 
833
924
  Options:
834
925
  --provider <name> Agent provider: claude, codex, qoder, qodercn (default: auto-detect; qodercn must be explicit)
@@ -843,6 +934,11 @@ function cmdHelp() {
843
934
  --settings <file> Claude settings JSON file
844
935
  --memory-mode <mode> Memory: platform, provider-local, or none (default: platform)
845
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)
846
942
  -h, --help Show this help
847
943
 
848
944
  Examples:
@@ -853,6 +949,9 @@ function cmdHelp() {
853
949
  npx -y autowonder start
854
950
  npx -y autowonder dispatch ./assignment.json
855
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
+
856
955
  # Check what's happening
857
956
  npx -y autowonder status
858
957
  `);
@@ -890,6 +989,7 @@ async function main() {
890
989
  case "status": return cmdStatus();
891
990
  case "dispatch": return cmdDispatch(commandArgs);
892
991
  case "install": return cmdInstall(commandArgs);
992
+ case "workitem": return cmdWorkitem(commandArgs);
893
993
  case "help":
894
994
  case "--help":
895
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.131",
3
+ "version": "0.2.133",
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
  ],