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 CHANGED
@@ -1,50 +1,41 @@
1
1
  # autowonder
2
2
 
3
- Local runtime daemon for executing AI agent dispatch packages.
3
+ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 assignment 队列并执行任务包。
4
4
 
5
- ## Quick Start
5
+ ## 安装并启动
6
6
 
7
7
  ```bash
8
- # Connect to server (like harness)
9
- npx -y autowonder connect --server-url https://autowonder.example.com --token <your-token>
8
+ # Codex
9
+ npx -y autowonder@0.2.1 install --force --provider codex
10
10
 
11
- # Or run locally
12
- npx -y autowonder start
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
- ## Commands
15
+ `install` 会安装或更新 binary 并立即启动 daemon;`--force` 会先停止旧 daemon,再切换到新版本。目标机器必须提前完成对应 CLI 的登录认证。
17
16
 
18
- | Command | Description |
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
- ## Options
19
+ daemon 默认轮询 `~/autowonder_workspaces/assignments/*.json`。队列里放 assignment JSON,不放 package zip;JSON 的 `taskPackageRef.downloadUrl` 应指向可下载的 OSS、HTTP 或本地 package 地址。
28
20
 
29
- - `--provider <name>` — Agent CLI: claude, codex, qoder (auto-detected)
30
- - `--max-tasks <n>` — Max concurrent dispatches (default: 2)
31
- - `--server-url <url>` — Server URL for remote task polling
32
- - `--token <token>` — Authentication token for server
33
- - `--name <name>` — Runtime instance name (default: hostname)
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
- - One of: Claude Code CLI, Codex CLI, or Qoder CLI
38
- - For building from source: Go 1.22+ and Git
28
+ 也可以直接提交到本地 API:
39
29
 
40
- ## How It Works
30
+ ```bash
31
+ npx -y autowonder@0.2.1 dispatch ./assignment.json
32
+ ```
41
33
 
42
- 1. Downloads or builds the `autowonder-daemon` binary
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
- ## License
36
+ ```bash
37
+ npx -y autowonder@0.2.1 status
38
+ npx -y autowonder@0.2.1 stop
39
+ ```
49
40
 
50
- Apache-2.0
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
- if (!fs.existsSync(PID_PATH)) return false;
49
- const pid = parseInt(fs.readFileSync(PID_PATH, "utf8").trim(), 10);
50
- if (!pid) return false;
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 = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, headers: { "User-Agent": "autowonder-npm" } };
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 || !daemonRunning()) {
271
- log("Daemon is not running.");
272
- try { fs.unlinkSync(PID_PATH); } catch {}
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
- log(`Stopped daemon (pid=${pid})`);
277
- try { fs.unlinkSync(PID_PATH); } catch {}
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 installed = await installBinary({ force: flags.force === "true" || args.includes("--force") });
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 binary
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.0",
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
- SOURCE_DIR=$(mktemp -d)
34
- trap "rm -rf $SOURCE_DIR" EXIT
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
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
80
- npm version patch --no-git-tag-version
81
- npm publish --access public
82
- rm -f .npmrc
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")"