autowonder 0.2.0 → 0.2.1
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 +25 -34
- package/bin/cli.js +140 -17
- package/package.json +5 -1
- package/scripts/build-and-publish.sh +27 -7
- 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/README.md
CHANGED
|
@@ -1,50 +1,41 @@
|
|
|
1
1
|
# autowonder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 assignment 队列并执行任务包。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 安装并启动
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
#
|
|
9
|
-
npx -y autowonder
|
|
8
|
+
# Codex
|
|
9
|
+
npx -y autowonder@0.2.1 install --force --provider codex
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
npx -y autowonder
|
|
13
|
-
npx -y autowonder dispatch ./assignment.json
|
|
11
|
+
# 或 Claude
|
|
12
|
+
npx -y autowonder@0.2.1 install --force --provider claude
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
`install` 会安装或更新 binary 并立即启动 daemon;`--force` 会先停止旧 daemon,再切换到新版本。目标机器必须提前完成对应 CLI 的登录认证。
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|---------|-------------|
|
|
20
|
-
| `autowonder connect` | Connect to server and run in foreground |
|
|
21
|
-
| `autowonder start` | Start daemon in background |
|
|
22
|
-
| `autowonder stop` | Stop background daemon |
|
|
23
|
-
| `autowonder status` | Show daemon status and runtime info |
|
|
24
|
-
| `autowonder dispatch <file>` | Submit a dispatch assignment |
|
|
25
|
-
| `autowonder install` | Install/update daemon binary |
|
|
17
|
+
## 提交 assignment
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
daemon 默认轮询 `~/autowonder_workspaces/assignments/*.json`。队列里放 assignment JSON,不放 package zip;JSON 的 `taskPackageRef.downloadUrl` 应指向可下载的 OSS、HTTP 或本地 package 地址。
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## Requirements
|
|
21
|
+
```bash
|
|
22
|
+
queue="$HOME/autowonder_workspaces/assignments"
|
|
23
|
+
mkdir -p "$queue"
|
|
24
|
+
cp ./assignment.json "$queue/.assignment.tmp"
|
|
25
|
+
mv "$queue/.assignment.tmp" "$queue/assignment.json"
|
|
26
|
+
```
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
- For building from source: Go 1.22+ and Git
|
|
28
|
+
也可以直接提交到本地 API:
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npx -y autowonder@0.2.1 dispatch ./assignment.json
|
|
32
|
+
```
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
2. Starts a local HTTP API on `127.0.0.1:34989`
|
|
44
|
-
3. Accepts dispatch assignments via file queue, API, or server polling
|
|
45
|
-
4. Executes packages using the configured agent provider
|
|
46
|
-
5. Produces structured artifacts, results, and observability
|
|
34
|
+
## 管理 daemon
|
|
47
35
|
|
|
48
|
-
|
|
36
|
+
```bash
|
|
37
|
+
npx -y autowonder@0.2.1 status
|
|
38
|
+
npx -y autowonder@0.2.1 stop
|
|
39
|
+
```
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
|
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { execFileSync, spawn, spawnSync } = require("child_process");
|
|
5
|
+
const crypto = require("crypto");
|
|
5
6
|
const path = require("path");
|
|
6
7
|
const fs = require("fs");
|
|
7
8
|
const os = require("os");
|
|
@@ -14,6 +15,7 @@ const BIN_DIR = path.join(AUTOWONDER_HOME, "bin");
|
|
|
14
15
|
const DAEMON_BIN = path.join(BIN_DIR, "autowonder-daemon");
|
|
15
16
|
const CONFIG_PATH = path.join(AUTOWONDER_HOME, "config.json");
|
|
16
17
|
const PID_PATH = path.join(AUTOWONDER_HOME, "daemon.pid");
|
|
18
|
+
const STATE_PATH = path.join(AUTOWONDER_HOME, "daemon-state.json");
|
|
17
19
|
const LOG_PATH = path.join(AUTOWONDER_HOME, "daemon.log");
|
|
18
20
|
|
|
19
21
|
const BINARY_BASE_URL = process.env.AUTOWONDER_BINARY_URL || "";
|
|
@@ -45,12 +47,98 @@ function detectProvider() {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function daemonRunning() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const pid = readPid();
|
|
51
|
+
return Boolean(pid && processRunning(pid) && ownsDaemonProcess(pid));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function processRunning(pid) {
|
|
51
55
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
function processFingerprint(pid) {
|
|
59
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
timeout: 2000,
|
|
62
|
+
});
|
|
63
|
+
return result.status === 0 ? (result.stdout || "").trim() : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function legacyDaemonProcess(pid) {
|
|
67
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 2000,
|
|
70
|
+
});
|
|
71
|
+
if (result.status !== 0) return false;
|
|
72
|
+
const command = (result.stdout || "").trim();
|
|
73
|
+
return command === DAEMON_BIN || command.startsWith(`${DAEMON_BIN} `);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readDaemonState() {
|
|
77
|
+
if (!fs.existsSync(STATE_PATH)) return null;
|
|
78
|
+
try { return JSON.parse(fs.readFileSync(STATE_PATH, "utf8")); } catch { return null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ownsDaemonProcess(pid) {
|
|
82
|
+
const state = readDaemonState();
|
|
83
|
+
if (state && state.pid === pid) {
|
|
84
|
+
const fingerprint = processFingerprint(pid);
|
|
85
|
+
return Boolean(fingerprint && fingerprint === state.fingerprint);
|
|
86
|
+
}
|
|
87
|
+
return legacyDaemonProcess(pid);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function clearDaemonState() {
|
|
91
|
+
try { fs.unlinkSync(PID_PATH); } catch {}
|
|
92
|
+
try { fs.unlinkSync(STATE_PATH); } catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function saveDaemonState(pid, instanceId) {
|
|
96
|
+
const fingerprint = processFingerprint(pid);
|
|
97
|
+
if (!fingerprint) return false;
|
|
98
|
+
fs.writeFileSync(PID_PATH, String(pid) + "\n");
|
|
99
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify({ pid, fingerprint, instanceId }) + "\n", { mode: 0o600 });
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function waitForProcessExit(pid, timeoutMs = 5000) {
|
|
104
|
+
const deadline = Date.now() + timeoutMs;
|
|
105
|
+
while (Date.now() < deadline) {
|
|
106
|
+
if (!processRunning(pid)) return true;
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
108
|
+
}
|
|
109
|
+
return !processRunning(pid);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkDaemonHealth(addr, instanceId) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const request = http.get(`http://${addr}/health`, { timeout: 500 }, (response) => {
|
|
115
|
+
let body = "";
|
|
116
|
+
response.setEncoding("utf8");
|
|
117
|
+
response.on("data", (chunk) => { body += chunk; });
|
|
118
|
+
response.on("end", () => {
|
|
119
|
+
try {
|
|
120
|
+
const health = JSON.parse(body);
|
|
121
|
+
resolve(response.statusCode === 200 && health.status === "ok" && health.instanceId === instanceId);
|
|
122
|
+
} catch {
|
|
123
|
+
resolve(false);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
request.on("timeout", () => request.destroy());
|
|
128
|
+
request.on("error", () => resolve(false));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function waitForDaemonHealth(addr, pid, instanceId, timeoutMs = 5000) {
|
|
133
|
+
const deadline = Date.now() + timeoutMs;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
if (!processRunning(pid)) return false;
|
|
136
|
+
if (await checkDaemonHealth(addr, instanceId)) return true;
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
54
142
|
function readPid() {
|
|
55
143
|
if (!fs.existsSync(PID_PATH)) return null;
|
|
56
144
|
const pid = parseInt(fs.readFileSync(PID_PATH, "utf8").trim(), 10);
|
|
@@ -125,7 +213,12 @@ function tryDownload(url, dest) {
|
|
|
125
213
|
function doRequest(requestUrl, redirects) {
|
|
126
214
|
if (redirects > 5) { resolve(false); return; }
|
|
127
215
|
const parsedUrl = new URL(requestUrl);
|
|
128
|
-
const options = {
|
|
216
|
+
const options = {
|
|
217
|
+
hostname: parsedUrl.hostname,
|
|
218
|
+
port: parsedUrl.port || undefined,
|
|
219
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
220
|
+
headers: { "User-Agent": "autowonder-npm" },
|
|
221
|
+
};
|
|
129
222
|
client.get(options, (res) => {
|
|
130
223
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
131
224
|
doRequest(res.headers.location, redirects + 1);
|
|
@@ -230,7 +323,7 @@ async function cmdStart(args) {
|
|
|
230
323
|
|
|
231
324
|
if (daemonRunning()) {
|
|
232
325
|
log("Daemon is already running.");
|
|
233
|
-
return;
|
|
326
|
+
return true;
|
|
234
327
|
}
|
|
235
328
|
|
|
236
329
|
const installed = await installBinary();
|
|
@@ -245,6 +338,7 @@ async function cmdStart(args) {
|
|
|
245
338
|
|
|
246
339
|
ensureDir(AUTOWONDER_HOME);
|
|
247
340
|
const logFd = fs.openSync(LOG_PATH, "a");
|
|
341
|
+
const instanceId = crypto.randomUUID();
|
|
248
342
|
const child = spawn(DAEMON_BIN, [], {
|
|
249
343
|
detached: true,
|
|
250
344
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -253,28 +347,52 @@ async function cmdStart(args) {
|
|
|
253
347
|
AUTOWONDER_PROVIDER: provider,
|
|
254
348
|
AUTOWONDER_LOCAL_API_ADDR: addr,
|
|
255
349
|
AUTOWONDER_MAX_CONCURRENT_DISPATCHES: maxTasks,
|
|
350
|
+
AUTOWONDER_DAEMON_INSTANCE_ID: instanceId,
|
|
256
351
|
},
|
|
257
352
|
});
|
|
353
|
+
fs.closeSync(logFd);
|
|
258
354
|
fs.writeFileSync(PID_PATH, String(child.pid) + "\n");
|
|
355
|
+
|
|
356
|
+
if (!await waitForDaemonHealth(addr, child.pid, instanceId) || !saveDaemonState(child.pid, instanceId)) {
|
|
357
|
+
if (processRunning(child.pid)) {
|
|
358
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
359
|
+
await waitForProcessExit(child.pid, 2000);
|
|
360
|
+
}
|
|
361
|
+
clearDaemonState();
|
|
362
|
+
error(`Daemon failed to become healthy at http://${addr}/health.`);
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
259
367
|
child.unref();
|
|
260
|
-
fs.closeSync(logFd);
|
|
261
368
|
|
|
262
369
|
log(`Daemon started (pid=${child.pid})`);
|
|
263
370
|
log(` API: http://${addr}`);
|
|
264
371
|
log(` Log: ${LOG_PATH}`);
|
|
265
372
|
log(` PID: ${PID_PATH}`);
|
|
373
|
+
return true;
|
|
266
374
|
}
|
|
267
375
|
|
|
268
|
-
function cmdStop() {
|
|
376
|
+
async function cmdStop(options = {}) {
|
|
269
377
|
const pid = readPid();
|
|
270
|
-
if (!pid || !
|
|
271
|
-
log("Daemon is not running.");
|
|
272
|
-
|
|
273
|
-
return;
|
|
378
|
+
if (!pid || !processRunning(pid)) {
|
|
379
|
+
if (!options.quiet) log("Daemon is not running.");
|
|
380
|
+
clearDaemonState();
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
if (!ownsDaemonProcess(pid)) {
|
|
384
|
+
if (!options.quiet) log(`Ignoring stale daemon PID ${pid}; it belongs to another process.`);
|
|
385
|
+
clearDaemonState();
|
|
386
|
+
return true;
|
|
274
387
|
}
|
|
275
388
|
process.kill(pid, "SIGTERM");
|
|
276
|
-
|
|
277
|
-
|
|
389
|
+
if (!await waitForProcessExit(pid)) {
|
|
390
|
+
error(`Daemon did not stop within 5 seconds (pid=${pid}).`);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (!options.quiet) log(`Stopped daemon (pid=${pid})`);
|
|
394
|
+
clearDaemonState();
|
|
395
|
+
return true;
|
|
278
396
|
}
|
|
279
397
|
|
|
280
398
|
async function cmdStatus() {
|
|
@@ -340,7 +458,7 @@ async function cmdDispatch(args) {
|
|
|
340
458
|
"-X", "POST",
|
|
341
459
|
"-H", "Content-Type: application/json",
|
|
342
460
|
"-d", body,
|
|
343
|
-
`http://${DEFAULT_API_ADDR}/dispatches`,
|
|
461
|
+
`http://${DEFAULT_API_ADDR}/dispatches/local`,
|
|
344
462
|
], { encoding: "utf8", timeout: 10000 });
|
|
345
463
|
|
|
346
464
|
const lines = (res.stdout || "").trim().split("\n");
|
|
@@ -358,9 +476,14 @@ async function cmdDispatch(args) {
|
|
|
358
476
|
}
|
|
359
477
|
|
|
360
478
|
async function cmdInstall(args) {
|
|
361
|
-
const flags = parseFlags(args, ["force"]);
|
|
362
|
-
const
|
|
479
|
+
const flags = parseFlags(args, ["force", "provider", "max-tasks", "addr"]);
|
|
480
|
+
const force = flags.force === "true" || args.includes("--force");
|
|
481
|
+
if (force && daemonRunning() && !await cmdStop({ quiet: true })) {
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
const installed = await installBinary({ force });
|
|
363
485
|
if (!installed) process.exit(1);
|
|
486
|
+
if (!await cmdStart(args)) process.exit(1);
|
|
364
487
|
}
|
|
365
488
|
|
|
366
489
|
function cmdHelp() {
|
|
@@ -373,7 +496,7 @@ function cmdHelp() {
|
|
|
373
496
|
autowonder stop Stop background daemon
|
|
374
497
|
autowonder status Show daemon status
|
|
375
498
|
autowonder dispatch <assignment.json> Submit a dispatch
|
|
376
|
-
autowonder install [--force] Install/update daemon
|
|
499
|
+
autowonder install [--force] Install/update and start daemon
|
|
377
500
|
|
|
378
501
|
Options:
|
|
379
502
|
--provider <name> Agent provider: claude, codex, qoder (default: auto-detect)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autowonder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
|
|
5
5
|
"bin": {
|
|
6
6
|
"autowonder": "bin/cli.js"
|
|
@@ -28,5 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=16"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "./scripts/build-and-publish.sh --build",
|
|
34
|
+
"test": "node --test test/*.test.js"
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -18,6 +18,18 @@ PKG_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
18
18
|
VENDOR_DIR="$PKG_DIR/vendor"
|
|
19
19
|
SOURCE_DIR="${AUTOWONDER_SOURCE_DIR:-}"
|
|
20
20
|
SOURCE_REPO="${AUTOWONDER_SOURCE_REPO:-}"
|
|
21
|
+
TEMP_SOURCE_DIR=""
|
|
22
|
+
NPM_CONFIG_FILE=""
|
|
23
|
+
|
|
24
|
+
cleanup() {
|
|
25
|
+
if [[ -n "$TEMP_SOURCE_DIR" ]]; then
|
|
26
|
+
rm -rf "$TEMP_SOURCE_DIR"
|
|
27
|
+
fi
|
|
28
|
+
if [[ -n "$NPM_CONFIG_FILE" ]]; then
|
|
29
|
+
rm -f "$NPM_CONFIG_FILE"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
trap cleanup EXIT
|
|
21
33
|
|
|
22
34
|
BUILD_ONLY=false
|
|
23
35
|
if [[ "${1:-}" == "--build" ]]; then
|
|
@@ -27,11 +39,19 @@ fi
|
|
|
27
39
|
echo "━━━ autowonder npm build ━━━"
|
|
28
40
|
|
|
29
41
|
# Get source code
|
|
42
|
+
if [[ -z "$SOURCE_DIR" && -f "$PKG_DIR/../go.mod" ]]; then
|
|
43
|
+
SOURCE_DIR="$(cd "$PKG_DIR/.." && pwd)"
|
|
44
|
+
fi
|
|
45
|
+
|
|
30
46
|
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
|
|
31
47
|
echo "Using local source: $SOURCE_DIR"
|
|
32
48
|
else
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
if [[ -z "$SOURCE_REPO" ]]; then
|
|
50
|
+
echo "Error: set AUTOWONDER_SOURCE_DIR or AUTOWONDER_SOURCE_REPO"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
TEMP_SOURCE_DIR="$(mktemp -d)"
|
|
54
|
+
SOURCE_DIR="$TEMP_SOURCE_DIR"
|
|
35
55
|
echo "Cloning from $SOURCE_REPO..."
|
|
36
56
|
git clone --depth 1 "$SOURCE_REPO" "$SOURCE_DIR"
|
|
37
57
|
fi
|
|
@@ -51,7 +71,7 @@ for i in "${!platforms[@]}"; do
|
|
|
51
71
|
output="$VENDOR_DIR/autowonder-daemon-${goos}-${goarch}"
|
|
52
72
|
|
|
53
73
|
echo "Building $platform..."
|
|
54
|
-
(cd "$SOURCE_DIR" && GOOS="$goos" GOARCH="$goarch" go build -o "$output" -ldflags="-s -w" ./cmd/autowonder-daemon)
|
|
74
|
+
(cd "$SOURCE_DIR" && CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -o "$output" -ldflags="-s -w" ./cmd/autowonder-daemon)
|
|
55
75
|
chmod +x "$output"
|
|
56
76
|
echo " -> $output ($(du -h "$output" | cut -f1))"
|
|
57
77
|
done
|
|
@@ -76,10 +96,10 @@ if [[ -z "${NPM_TOKEN:-}" ]]; then
|
|
|
76
96
|
exit 1
|
|
77
97
|
fi
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
umask 077
|
|
100
|
+
NPM_CONFIG_FILE="$(mktemp)"
|
|
101
|
+
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" > "$NPM_CONFIG_FILE"
|
|
102
|
+
npm publish --access public --userconfig "$NPM_CONFIG_FILE"
|
|
83
103
|
|
|
84
104
|
echo ""
|
|
85
105
|
echo "Published $(node -p "require('./package.json').name + '@' + require('./package.json').version")"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|