autowonder 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +50 -0
  2. package/bin/cli.js +426 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # autowonder
2
+
3
+ Local runtime daemon for executing AI agent dispatch packages.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Connect to server (like harness)
9
+ npx -y autowonder connect --server-url https://autowonder.example.com --token <your-token>
10
+
11
+ # Or run locally
12
+ npx -y autowonder start
13
+ npx -y autowonder dispatch ./assignment.json
14
+ ```
15
+
16
+ ## Commands
17
+
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 |
26
+
27
+ ## Options
28
+
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
36
+
37
+ - One of: Claude Code CLI, Codex CLI, or Qoder CLI
38
+ - For building from source: Go 1.22+ and Git
39
+
40
+ ## How It Works
41
+
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
47
+
48
+ ## License
49
+
50
+ Apache-2.0
package/bin/cli.js ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execFileSync, spawn, spawnSync } = require("child_process");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const https = require("https");
9
+ const http = require("http");
10
+
11
+ const VERSION = require("../package.json").version;
12
+ const AUTOWONDER_HOME = path.join(os.homedir(), ".autowonder");
13
+ const BIN_DIR = path.join(AUTOWONDER_HOME, "bin");
14
+ const DAEMON_BIN = path.join(BIN_DIR, "autowonder-daemon");
15
+ const CONFIG_PATH = path.join(AUTOWONDER_HOME, "config.json");
16
+ const PID_PATH = path.join(AUTOWONDER_HOME, "daemon.pid");
17
+ const LOG_PATH = path.join(AUTOWONDER_HOME, "daemon.log");
18
+
19
+ const BINARY_BASE_URL = process.env.AUTOWONDER_BINARY_URL || "https://github.com/ZichaoJin/auto-wonder-client-runtime/releases/download";
20
+ const SOURCE_REPO = "https://github.com/ZichaoJin/auto-wonder-client-runtime.git";
21
+
22
+ const DEFAULT_API_ADDR = "127.0.0.1:34989";
23
+
24
+ // ─── Helpers ─────────────────────────────────────────────────────────
25
+
26
+ function ensureDir(dir) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+
30
+ function log(msg) {
31
+ console.log(`\x1b[36m[autowonder]\x1b[0m ${msg}`);
32
+ }
33
+
34
+ function error(msg) {
35
+ console.error(`\x1b[31m[autowonder]\x1b[0m ${msg}`);
36
+ }
37
+
38
+ function detectProvider() {
39
+ const providers = [];
40
+ try { execFileSync("which", ["claude"], { stdio: "ignore" }); providers.push("claude"); } catch {}
41
+ try { execFileSync("which", ["codex"], { stdio: "ignore" }); providers.push("codex"); } catch {}
42
+ try { execFileSync("which", ["qoder"], { stdio: "ignore" }); providers.push("qoder"); } catch {}
43
+ return providers;
44
+ }
45
+
46
+ function daemonRunning() {
47
+ if (!fs.existsSync(PID_PATH)) return false;
48
+ const pid = parseInt(fs.readFileSync(PID_PATH, "utf8").trim(), 10);
49
+ if (!pid) return false;
50
+ try { process.kill(pid, 0); return true; } catch { return false; }
51
+ }
52
+
53
+ function readPid() {
54
+ if (!fs.existsSync(PID_PATH)) return null;
55
+ const pid = parseInt(fs.readFileSync(PID_PATH, "utf8").trim(), 10);
56
+ return pid || null;
57
+ }
58
+
59
+ function loadConfig() {
60
+ if (!fs.existsSync(CONFIG_PATH)) return {};
61
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch { return {}; }
62
+ }
63
+
64
+ function saveConfig(config) {
65
+ ensureDir(AUTOWONDER_HOME);
66
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
67
+ }
68
+
69
+ function binaryPlatformKey() {
70
+ const platform = process.platform;
71
+ const arch = process.arch === "arm64" ? "arm64" : "amd64";
72
+ return `${platform}-${arch}`;
73
+ }
74
+
75
+ function binaryExists() {
76
+ try {
77
+ fs.accessSync(DAEMON_BIN, fs.constants.X_OK);
78
+ return true;
79
+ } catch { return false; }
80
+ }
81
+
82
+ // ─── Binary Installation ─────────────────────────────────────────────
83
+
84
+ async function installBinary(options = {}) {
85
+ if (binaryExists() && !options.force) {
86
+ log("Daemon binary already installed.");
87
+ return true;
88
+ }
89
+
90
+ const platform = binaryPlatformKey();
91
+ log(`Installing autowonder-daemon for ${platform}...`);
92
+
93
+ // Try download pre-built binary
94
+ const url = `${BINARY_BASE_URL}/v${VERSION}/autowonder-daemon-${platform}`;
95
+ const downloaded = await tryDownload(url, DAEMON_BIN);
96
+ if (downloaded) {
97
+ fs.chmodSync(DAEMON_BIN, 0o755);
98
+ log(`Installed daemon binary: ${DAEMON_BIN}`);
99
+ return true;
100
+ }
101
+
102
+ // Fallback: build from source
103
+ log("Pre-built binary not available. Building from source...");
104
+ return buildFromSource();
105
+ }
106
+
107
+ function tryDownload(url, dest) {
108
+ return new Promise((resolve) => {
109
+ ensureDir(path.dirname(dest));
110
+ const client = url.startsWith("https") ? https : http;
111
+
112
+ function doRequest(requestUrl, redirects) {
113
+ if (redirects > 5) { resolve(false); return; }
114
+ const parsedUrl = new URL(requestUrl);
115
+ const options = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, headers: { "User-Agent": "autowonder-npm" } };
116
+ client.get(options, (res) => {
117
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
118
+ doRequest(res.headers.location, redirects + 1);
119
+ return;
120
+ }
121
+ if (res.statusCode !== 200) { res.resume(); resolve(false); return; }
122
+ const file = fs.createWriteStream(dest);
123
+ res.pipe(file);
124
+ file.on("finish", () => { file.close(); resolve(true); });
125
+ file.on("error", () => { try { fs.unlinkSync(dest); } catch {} resolve(false); });
126
+ }).on("error", () => resolve(false));
127
+ }
128
+ doRequest(url, 0);
129
+ });
130
+ }
131
+
132
+ function buildFromSource() {
133
+ try { execFileSync("which", ["go"], { stdio: "ignore" }); } catch {
134
+ error("Go is not installed. Install Go 1.22+ or provide a pre-built binary.");
135
+ error(" macOS: brew install go");
136
+ error(" Linux: https://go.dev/dl/");
137
+ return false;
138
+ }
139
+
140
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "autowonder-build-"));
141
+ try {
142
+ log("Cloning source repository...");
143
+ execFileSync("git", ["clone", "--depth", "1", SOURCE_REPO, tmpDir], { stdio: "inherit" });
144
+ log("Building daemon...");
145
+ ensureDir(BIN_DIR);
146
+ execFileSync("go", ["build", "-o", DAEMON_BIN, "./cmd/autowonder-daemon"], { cwd: tmpDir, stdio: "inherit" });
147
+ fs.chmodSync(DAEMON_BIN, 0o755);
148
+ log(`Built daemon binary: ${DAEMON_BIN}`);
149
+ return true;
150
+ } catch (e) {
151
+ error(`Build failed: ${e.message}`);
152
+ return false;
153
+ } finally {
154
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
155
+ }
156
+ }
157
+
158
+ // ─── Commands ────────────────────────────────────────────────────────
159
+
160
+ async function cmdConnect(args) {
161
+ const flags = parseFlags(args, ["token", "server-url", "provider", "name", "max-tasks"]);
162
+
163
+ if (!flags.token && !flags["server-url"]) {
164
+ error("Usage: autowonder connect --server-url <url> --token <token> [--provider claude] [--name my-machine]");
165
+ process.exit(1);
166
+ }
167
+
168
+ const installed = await installBinary();
169
+ if (!installed) process.exit(1);
170
+
171
+ const providers = detectProvider();
172
+ const provider = flags.provider || providers[0] || "claude";
173
+ if (providers.length === 0) {
174
+ error(`No agent CLI detected. Install one of: claude, codex, qoder`);
175
+ process.exit(1);
176
+ }
177
+
178
+ const config = {
179
+ serverUrl: flags["server-url"] || "",
180
+ token: flags.token || "",
181
+ provider,
182
+ name: flags.name || os.hostname(),
183
+ maxTasks: parseInt(flags["max-tasks"] || "2", 10),
184
+ };
185
+ saveConfig(config);
186
+
187
+ log(`Connecting to ${config.serverUrl || "local mode"}...`);
188
+ log(`Provider: ${provider}`);
189
+ log(`Name: ${config.name}`);
190
+ log(`Max concurrent: ${config.maxTasks}`);
191
+ log("");
192
+
193
+ // Start daemon in foreground (like harness connect)
194
+ const env = {
195
+ ...process.env,
196
+ AUTOWONDER_PROVIDER: config.provider,
197
+ AUTOWONDER_MAX_CONCURRENT_DISPATCHES: String(config.maxTasks),
198
+ AUTOWONDER_DAEMON_ID: config.name,
199
+ };
200
+ if (config.serverUrl) env.AUTOWONDER_SERVER_URL = config.serverUrl;
201
+ if (config.token) env.AUTOWONDER_SERVER_TOKEN = config.token;
202
+
203
+ const child = spawn(DAEMON_BIN, [], { env, stdio: "inherit" });
204
+ child.on("exit", (code) => process.exit(code || 0));
205
+ process.on("SIGINT", () => { child.kill("SIGINT"); });
206
+ process.on("SIGTERM", () => { child.kill("SIGTERM"); });
207
+ }
208
+
209
+ async function cmdStart(args) {
210
+ const flags = parseFlags(args, ["provider", "max-tasks", "addr"]);
211
+
212
+ if (daemonRunning()) {
213
+ log("Daemon is already running.");
214
+ return;
215
+ }
216
+
217
+ const installed = await installBinary();
218
+ if (!installed) process.exit(1);
219
+
220
+ const providers = detectProvider();
221
+ const provider = flags.provider || providers[0] || "claude";
222
+ const addr = flags.addr || DEFAULT_API_ADDR;
223
+ const maxTasks = flags["max-tasks"] || "2";
224
+
225
+ log(`Starting daemon (provider=${provider}, addr=${addr})...`);
226
+
227
+ ensureDir(AUTOWONDER_HOME);
228
+ const logFd = fs.openSync(LOG_PATH, "a");
229
+ const child = spawn(DAEMON_BIN, [], {
230
+ detached: true,
231
+ stdio: ["ignore", logFd, logFd],
232
+ env: {
233
+ ...process.env,
234
+ AUTOWONDER_PROVIDER: provider,
235
+ AUTOWONDER_LOCAL_API_ADDR: addr,
236
+ AUTOWONDER_MAX_CONCURRENT_DISPATCHES: maxTasks,
237
+ },
238
+ });
239
+ fs.writeFileSync(PID_PATH, String(child.pid) + "\n");
240
+ child.unref();
241
+ fs.closeSync(logFd);
242
+
243
+ log(`Daemon started (pid=${child.pid})`);
244
+ log(` API: http://${addr}`);
245
+ log(` Log: ${LOG_PATH}`);
246
+ log(` PID: ${PID_PATH}`);
247
+ }
248
+
249
+ function cmdStop() {
250
+ const pid = readPid();
251
+ if (!pid || !daemonRunning()) {
252
+ log("Daemon is not running.");
253
+ try { fs.unlinkSync(PID_PATH); } catch {}
254
+ return;
255
+ }
256
+ process.kill(pid, "SIGTERM");
257
+ log(`Stopped daemon (pid=${pid})`);
258
+ try { fs.unlinkSync(PID_PATH); } catch {}
259
+ }
260
+
261
+ async function cmdStatus() {
262
+ const running = daemonRunning();
263
+ const pid = readPid();
264
+ const config = loadConfig();
265
+
266
+ console.log("");
267
+ console.log(` Status: ${running ? "\x1b[32mrunning\x1b[0m" : "\x1b[31mstopped\x1b[0m"}`);
268
+ if (pid && running) console.log(` PID: ${pid}`);
269
+ console.log(` Provider: ${config.provider || "not configured"}`);
270
+ console.log(` Server: ${config.serverUrl || "local only"}`);
271
+ console.log(` Binary: ${binaryExists() ? DAEMON_BIN : "not installed"}`);
272
+ console.log(` Home: ${AUTOWONDER_HOME}`);
273
+ console.log("");
274
+
275
+ if (running) {
276
+ try {
277
+ const res = spawnSync("curl", ["-s", `http://${DEFAULT_API_ADDR}/health`], { encoding: "utf8", timeout: 3000 });
278
+ if (res.stdout) {
279
+ const health = JSON.parse(res.stdout);
280
+ console.log(` API: http://${DEFAULT_API_ADDR} (${health.status})`);
281
+ }
282
+ } catch {}
283
+
284
+ try {
285
+ const res = spawnSync("curl", ["-s", `http://${DEFAULT_API_ADDR}/runtimes`], { encoding: "utf8", timeout: 3000 });
286
+ if (res.stdout) {
287
+ const runtimes = JSON.parse(res.stdout);
288
+ for (const rt of (runtimes.runtimes || runtimes)) {
289
+ console.log(` Runtime: ${rt.id} (${rt.provider}) ${rt.available ? "available" : "unavailable"}${rt.version ? " v" + rt.version : ""}`);
290
+ }
291
+ }
292
+ } catch {}
293
+ console.log("");
294
+ }
295
+ }
296
+
297
+ async function cmdDispatch(args) {
298
+ if (args.length === 0) {
299
+ error("Usage: autowonder dispatch <assignment.json | package-url>");
300
+ process.exit(1);
301
+ }
302
+
303
+ if (!daemonRunning()) {
304
+ error("Daemon is not running. Start it first: autowonder start");
305
+ process.exit(1);
306
+ }
307
+
308
+ const input = args[0];
309
+ let body;
310
+
311
+ if (fs.existsSync(input)) {
312
+ body = fs.readFileSync(input, "utf8");
313
+ } else {
314
+ error(`File not found: ${input}`);
315
+ process.exit(1);
316
+ }
317
+
318
+ log(`Submitting dispatch to http://${DEFAULT_API_ADDR}...`);
319
+ const res = spawnSync("curl", [
320
+ "-s", "-w", "\n%{http_code}",
321
+ "-X", "POST",
322
+ "-H", "Content-Type: application/json",
323
+ "-d", body,
324
+ `http://${DEFAULT_API_ADDR}/dispatches`,
325
+ ], { encoding: "utf8", timeout: 10000 });
326
+
327
+ const lines = (res.stdout || "").trim().split("\n");
328
+ const statusCode = lines.pop();
329
+ const responseBody = lines.join("\n");
330
+
331
+ if (statusCode === "200" || statusCode === "202") {
332
+ log("Dispatch submitted successfully.");
333
+ if (responseBody) console.log(responseBody);
334
+ } else {
335
+ error(`Dispatch failed (HTTP ${statusCode})`);
336
+ if (responseBody) console.error(responseBody);
337
+ process.exit(1);
338
+ }
339
+ }
340
+
341
+ async function cmdInstall(args) {
342
+ const flags = parseFlags(args, ["force"]);
343
+ const installed = await installBinary({ force: flags.force === "true" || args.includes("--force") });
344
+ if (!installed) process.exit(1);
345
+ }
346
+
347
+ function cmdHelp() {
348
+ console.log(`
349
+ autowonder — local runtime for AI agent dispatch execution
350
+
351
+ Usage:
352
+ autowonder connect --server-url <url> --token <token> Connect to server and run
353
+ autowonder start [--provider claude] Start daemon in background
354
+ autowonder stop Stop background daemon
355
+ autowonder status Show daemon status
356
+ autowonder dispatch <assignment.json> Submit a dispatch
357
+ autowonder install [--force] Install/update daemon binary
358
+
359
+ Options:
360
+ --provider <name> Agent provider: claude, codex, qoder (default: auto-detect)
361
+ --max-tasks <n> Max concurrent dispatches (default: 2)
362
+ --addr <host:port> Local API address (default: 127.0.0.1:34989)
363
+ --server-url <url> Server URL for remote dispatch
364
+ --token <token> Authentication token
365
+ --name <name> Runtime name (default: hostname)
366
+ -h, --help Show this help
367
+
368
+ Examples:
369
+ # Quick start — connect to server
370
+ npx -y autowonder connect --server-url https://autowonder.example.com --token xxx
371
+
372
+ # Local mode — start daemon, submit tasks manually
373
+ npx -y autowonder start
374
+ npx -y autowonder dispatch ./assignment.json
375
+
376
+ # Check what's happening
377
+ npx -y autowonder status
378
+ `);
379
+ }
380
+
381
+ // ─── Flag Parser ─────────────────────────────────────────────────────
382
+
383
+ function parseFlags(args, knownFlags) {
384
+ const flags = {};
385
+ for (let i = 0; i < args.length; i++) {
386
+ const arg = args[i];
387
+ if (arg.startsWith("--")) {
388
+ const key = arg.slice(2);
389
+ if (knownFlags.includes(key) && i + 1 < args.length && !args[i + 1].startsWith("--")) {
390
+ flags[key] = args[++i];
391
+ } else {
392
+ flags[key] = "true";
393
+ }
394
+ }
395
+ }
396
+ return flags;
397
+ }
398
+
399
+ // ─── Main ────────────────────────────────────────────────────────────
400
+
401
+ async function main() {
402
+ const args = process.argv.slice(2);
403
+ const command = args[0] || "help";
404
+ const commandArgs = args.slice(1);
405
+
406
+ switch (command) {
407
+ case "connect": return cmdConnect(commandArgs);
408
+ case "start": return cmdStart(commandArgs);
409
+ case "stop": return cmdStop();
410
+ case "status": return cmdStatus();
411
+ case "dispatch": return cmdDispatch(commandArgs);
412
+ case "install": return cmdInstall(commandArgs);
413
+ case "help":
414
+ case "--help":
415
+ case "-h": return cmdHelp();
416
+ default:
417
+ error(`Unknown command: ${command}`);
418
+ cmdHelp();
419
+ process.exit(1);
420
+ }
421
+ }
422
+
423
+ main().catch((e) => {
424
+ error(e.message || String(e));
425
+ process.exit(1);
426
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "autowonder",
3
+ "version": "0.1.0",
4
+ "description": "AutoWonder local runtime — execute AI agent dispatch packages on your machine",
5
+ "bin": {
6
+ "autowonder": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "scripts/"
11
+ ],
12
+ "keywords": [
13
+ "autowonder",
14
+ "runtime",
15
+ "ai-agent",
16
+ "daemon",
17
+ "dispatch",
18
+ "claude",
19
+ "codex",
20
+ "qoder"
21
+ ],
22
+ "author": "Alibaba Cloud",
23
+ "license": "Apache-2.0",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/ZichaoJin/auto-wonder-client-runtime.git"
27
+ },
28
+ "engines": {
29
+ "node": ">=16"
30
+ }
31
+ }