@11agents/cli 0.1.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 ADDED
@@ -0,0 +1,103 @@
1
+ # 11agents CLI
2
+
3
+ Local CLI for connecting AI coding runtimes to the 11agents control plane.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @11agents/cli@latest
9
+ ```
10
+
11
+ The CLI requires Node.js 22 or newer.
12
+
13
+ ## Configure
14
+
15
+ ```bash
16
+ export GTM_WRITES_TOKEN="<control-plane-token>"
17
+ export ELEVENAGENTS_MACHINE="mac-mini-01"
18
+ ```
19
+
20
+ The default control plane is `https://app.11agents.ai`. Set `ELEVENAGENTS_SERVER` only when targeting a local or custom deployment:
21
+
22
+ ```bash
23
+ export ELEVENAGENTS_SERVER="http://localhost:8082"
24
+ ```
25
+
26
+ `ELEVENAGENTS_MACHINE` is optional. If omitted, the CLI uses the local hostname.
27
+
28
+ On startup, the CLI prints its current version and target server to stderr. It also checks npm for a newer `@11agents/cli` package and prints an upgrade command when one is available.
29
+
30
+ ## Runtime Pool
31
+
32
+ Scan local AI runtimes:
33
+
34
+ ```bash
35
+ 11agents runtime scan
36
+ ```
37
+
38
+ Register this machine and its detected runtimes:
39
+
40
+ ```bash
41
+ 11agents runtime register
42
+ ```
43
+
44
+ Run the foreground daemon:
45
+
46
+ ```bash
47
+ 11agents daemon start
48
+ ```
49
+
50
+ Run the daemon in the background:
51
+
52
+ ```bash
53
+ 11agents daemon start --background
54
+ 11agents daemon status
55
+ 11agents daemon stop
56
+ ```
57
+
58
+ Background mode writes its pid to `~/.11agents/daemon.pid` and logs to `~/.11agents/daemon.log`.
59
+
60
+ Useful daemon options:
61
+
62
+ ```bash
63
+ 11agents daemon start --heartbeat-interval 15 --scan-interval 60 --task-interval 15
64
+ 11agents daemon start --handler ./worker.js
65
+ ```
66
+
67
+ The built-in task runner currently supports Codex tasks. A custom handler may export:
68
+
69
+ ```js
70
+ export async function handleRuntimeTask(task) {
71
+ return {
72
+ status: 'in_review',
73
+ comment: `Handled task ${task.id}`,
74
+ memory_delta: ''
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Telemetry Compatibility
80
+
81
+ The package still includes the original `gtm-swarm` binary for swarm telemetry compatibility.
82
+
83
+ ```bash
84
+ export GTM_SWARM_SERVER="https://<your-11agents-platform>"
85
+ export GTM_SWARM_TOKEN="<workspace-swarm-token>"
86
+ export GTM_SWARM_WORKSPACE="flatkey"
87
+ export GTM_SWARM_AGENT="x-growth-agent"
88
+ export GTM_SWARM_NODE="mac-mini-01"
89
+
90
+ gtm-swarm validate examples/x-agent-batch.json
91
+ gtm-swarm push batch examples/x-agent-batch.json
92
+ ```
93
+
94
+ ## Publish
95
+
96
+ First public release should go out on the beta dist-tag:
97
+
98
+ ```bash
99
+ npm login
100
+ npm test
101
+ npm pack --dry-run --json
102
+ npm publish --tag beta
103
+ ```
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { parseArgs } from '../src/args.js'
5
+ import { runNode } from '../src/commands/node.js'
6
+ import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
7
+ import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
8
+ import { startBackgroundDaemon, statusBackgroundDaemon, stopBackgroundDaemon } from '../src/daemon-process.js'
9
+ import { getControlConfig } from '../src/client.js'
10
+ import { printStartupInfo } from '../src/info.js'
11
+ import { validateTelemetryBatch } from '../src/schema.js'
12
+
13
+ function usage() {
14
+ console.log(`11agents CLI
15
+
16
+ Usage:
17
+ 11agents help
18
+ 11agents runtime scan
19
+ 11agents runtime register [--server <url>] [--token <token>] [--machine <key>]
20
+ 11agents daemon start [--server <url>] [--token <token>] [--machine <key>] [--task-interval <seconds>] [--background]
21
+ 11agents daemon status
22
+ 11agents daemon stop
23
+ 11agents daemon start --handler ./worker.js # optional custom worker override
24
+ 11agents validate <file>
25
+ 11agents push batch <file>
26
+ 11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
27
+ 11agents push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123
28
+ 11agents node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
29
+
30
+ Environment:
31
+ ELEVENAGENTS_SERVER default https://app.11agents.ai
32
+ GTM_SWARM_SERVER compatibility fallback for server URL
33
+ GTM_WRITES_TOKEN control-plane token for runtime registration
34
+ ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
35
+ GTM_SWARM_TOKEN project swarm token for push/node commands`)
36
+ }
37
+
38
+ async function main() {
39
+ const argv = process.argv.slice(2)
40
+ const { positional, flags } = parseArgs(argv)
41
+ const [command, subcommand, target] = positional
42
+
43
+ if (!command || command === 'help') {
44
+ usage()
45
+ return
46
+ }
47
+
48
+ await printStartupInfo({
49
+ server: getControlConfig({ server: flags.server }).server,
50
+ quiet: Boolean(flags.quiet),
51
+ })
52
+
53
+ if (command === 'runtime' && subcommand === 'scan') {
54
+ await scanRuntime(flags)
55
+ return
56
+ }
57
+
58
+ if (command === 'runtime' && subcommand === 'register') {
59
+ await registerRuntime(flags)
60
+ return
61
+ }
62
+
63
+ if (command === 'daemon' && subcommand === 'start') {
64
+ if (flags.background) {
65
+ const result = await startBackgroundDaemon({
66
+ argv,
67
+ scriptPath: fileURLToPath(import.meta.url),
68
+ })
69
+ if (result.alreadyRunning) {
70
+ console.log(`11agents daemon already running with pid ${result.pid}`)
71
+ } else {
72
+ console.log(`11agents daemon started with pid ${result.pid}`)
73
+ }
74
+ console.log(`log: ${result.logPath}`)
75
+ return
76
+ }
77
+ await startRuntimeDaemon(flags)
78
+ return
79
+ }
80
+
81
+ if (command === 'daemon' && subcommand === 'status') {
82
+ const status = await statusBackgroundDaemon()
83
+ if (status.running) console.log(`11agents daemon running with pid ${status.pid}`)
84
+ else if (status.stale) console.log(`11agents daemon not running; stale pid ${status.pid}`)
85
+ else console.log('11agents daemon not running')
86
+ console.log(`log: ${status.logPath}`)
87
+ return
88
+ }
89
+
90
+ if (command === 'daemon' && subcommand === 'stop') {
91
+ const result = await stopBackgroundDaemon()
92
+ if (result.stopped) console.log(`11agents daemon stopped pid ${result.pid}`)
93
+ else console.log('11agents daemon not running')
94
+ return
95
+ }
96
+
97
+ if (command === 'validate') {
98
+ const file = subcommand
99
+ if (!file) throw new Error('validate requires a file')
100
+ const json = JSON.parse(await readFile(file, 'utf-8'))
101
+ const result = validateTelemetryBatch(json)
102
+ if (!result.ok) throw new Error(result.error)
103
+ console.log('valid')
104
+ return
105
+ }
106
+
107
+ if (command === 'push' && subcommand === 'batch') {
108
+ if (!target) throw new Error('push batch requires a file')
109
+ await pushBatch(target)
110
+ return
111
+ }
112
+
113
+ if (command === 'push' && subcommand === 'artifact') {
114
+ await pushArtifact(flags)
115
+ return
116
+ }
117
+
118
+ if (command === 'push' && subcommand === 'observation') {
119
+ await pushObservation(flags)
120
+ return
121
+ }
122
+
123
+ if (command === 'node' && subcommand === 'run') {
124
+ await runNode(flags)
125
+ return
126
+ }
127
+
128
+ usage()
129
+ process.exitCode = 1
130
+ }
131
+
132
+ main().catch(error => {
133
+ console.error(error instanceof Error ? error.message : String(error))
134
+ process.exitCode = 1
135
+ })
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises'
3
+ import { parseArgs } from '../src/args.js'
4
+ import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
5
+ import { runNode } from '../src/commands/node.js'
6
+ import { validateTelemetryBatch } from '../src/schema.js'
7
+
8
+ function usage() {
9
+ console.log(`GTM Swarm CLI
10
+
11
+ Usage:
12
+ gtm-swarm help
13
+ gtm-swarm validate <file>
14
+ gtm-swarm push batch <file>
15
+ gtm-swarm push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id> [--url <url>] [--body <text>]
16
+ gtm-swarm push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123 --metric replies=4
17
+ gtm-swarm node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
18
+
19
+ Environment:
20
+ GTM_SWARM_SERVER default https://app.11agents.ai
21
+ GTM_SWARM_TOKEN bearer token
22
+ GTM_SWARM_WORKSPACE default workspace
23
+ GTM_SWARM_AGENT default agent key
24
+ GTM_SWARM_NODE default node id`)
25
+ }
26
+
27
+ async function main() {
28
+ const { positional, flags } = parseArgs(process.argv.slice(2))
29
+ const [command, subcommand, target] = positional
30
+
31
+ if (!command || command === 'help') {
32
+ usage()
33
+ return
34
+ }
35
+
36
+ if (command === 'validate') {
37
+ const file = subcommand
38
+ if (!file) throw new Error('validate requires a file')
39
+ const json = JSON.parse(await readFile(file, 'utf-8'))
40
+ const result = validateTelemetryBatch(json)
41
+ if (!result.ok) throw new Error(result.error)
42
+ console.log('valid')
43
+ return
44
+ }
45
+
46
+ if (command === 'push' && subcommand === 'batch') {
47
+ if (!target) throw new Error('push batch requires a file')
48
+ await pushBatch(target)
49
+ return
50
+ }
51
+
52
+ if (command === 'push' && subcommand === 'artifact') {
53
+ await pushArtifact(flags)
54
+ return
55
+ }
56
+
57
+ if (command === 'push' && subcommand === 'observation') {
58
+ await pushObservation(flags)
59
+ return
60
+ }
61
+
62
+ if (command === 'node' && subcommand === 'run') {
63
+ await runNode(flags)
64
+ return
65
+ }
66
+
67
+ usage()
68
+ process.exitCode = 1
69
+ }
70
+
71
+ main().catch(error => {
72
+ console.error(error instanceof Error ? error.message : String(error))
73
+ process.exitCode = 1
74
+ })
@@ -0,0 +1,45 @@
1
+ {
2
+ "schema_version": "swarm.telemetry.v1",
3
+ "workspace": "voc-ai",
4
+ "agent_key": "voc-amazon-reviews-mcp",
5
+ "node_id": "vercel-prod",
6
+ "sent_at": "2026-05-25T10:00:02Z",
7
+ "artifacts": [
8
+ {
9
+ "platform": "mcp",
10
+ "artifact_type": "mcp_tool_call",
11
+ "external_id": "1766656800000-client_abc123-fetch_reviews-a8f21c",
12
+ "title": "fetch_reviews ok",
13
+ "created_at": "2026-05-25T10:00:00Z",
14
+ "payload": {
15
+ "service_name": "voc-amazon-reviews-mcp",
16
+ "metric_name": "mcp_tool_calls_total",
17
+ "tool": "fetch_reviews",
18
+ "status": "ok",
19
+ "client": "claude-code",
20
+ "error_type": "",
21
+ "source_catalog": "amazon-us",
22
+ "client_instance_id": "client_abc123",
23
+ "business_success": true,
24
+ "route": "POST /mcp",
25
+ "http_status": 200
26
+ }
27
+ }
28
+ ],
29
+ "observations": [
30
+ {
31
+ "platform": "mcp",
32
+ "artifact_type": "mcp_tool_call",
33
+ "external_id": "1766656800000-client_abc123-fetch_reviews-a8f21c",
34
+ "observed_at": "2026-05-25T10:00:02Z",
35
+ "metrics": {
36
+ "calls": 1,
37
+ "latency_ms": 842,
38
+ "business_success": 1,
39
+ "http_2xx": 1,
40
+ "http_4xx": 0,
41
+ "http_5xx": 0
42
+ }
43
+ }
44
+ ]
45
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "schema_version": "swarm.telemetry.v1",
3
+ "workspace": "flatkey",
4
+ "agent_key": "x-growth-agent",
5
+ "node_id": "mac-mini-01",
6
+ "sent_at": "2026-05-25T09:30:00Z",
7
+ "artifacts": [
8
+ {
9
+ "platform": "x",
10
+ "artifact_type": "post",
11
+ "external_id": "1794312345678900000",
12
+ "url": "https://x.com/acme/status/1794312345678900000",
13
+ "body": "We shipped a new API key flow today.",
14
+ "created_at": "2026-05-25T08:10:00Z",
15
+ "payload": {
16
+ "account": "@acme"
17
+ }
18
+ },
19
+ {
20
+ "platform": "x",
21
+ "artifact_type": "reply",
22
+ "external_id": "1794312345678900001",
23
+ "url": "https://x.com/acme/status/1794312345678900001",
24
+ "body": "Thanks for the feedback. We added this to the roadmap.",
25
+ "created_at": "2026-05-25T08:12:00Z",
26
+ "payload": {
27
+ "account": "@acme",
28
+ "reply_to_external_id": "1794300000000000000"
29
+ }
30
+ }
31
+ ],
32
+ "observations": [
33
+ {
34
+ "platform": "x",
35
+ "artifact_type": "post",
36
+ "external_id": "1794312345678900000",
37
+ "observed_at": "2026-05-25T09:25:00Z",
38
+ "metrics": {
39
+ "views": 1834,
40
+ "replies": 12
41
+ }
42
+ },
43
+ {
44
+ "platform": "x",
45
+ "artifact_type": "reply",
46
+ "external_id": "1794312345678900001",
47
+ "observed_at": "2026-05-25T09:25:00Z",
48
+ "metrics": {
49
+ "views": 321,
50
+ "replies": 1
51
+ }
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "status": "completed",
3
+ "summary": "Collected 1 X post observation.",
4
+ "batch": {
5
+ "schema_version": "swarm.telemetry.v1",
6
+ "workspace": "flatkey",
7
+ "agent_key": "x-growth-agent",
8
+ "node_id": "mac-mini-01",
9
+ "sent_at": "2026-05-25T09:34:00Z",
10
+ "artifacts": [],
11
+ "observations": [
12
+ {
13
+ "platform": "x",
14
+ "artifact_type": "post",
15
+ "external_id": "1794312345678900000",
16
+ "observed_at": "2026-05-25T09:33:30Z",
17
+ "metrics": {
18
+ "views": 1901,
19
+ "replies": 13
20
+ }
21
+ }
22
+ ]
23
+ }
24
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@11agents/cli",
3
+ "version": "0.1.1",
4
+ "description": "11agents local runtime and telemetry CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "11agents": "bin/11agents.js",
8
+ "gtm-swarm": "bin/gtm-swarm.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "!src/**/*.test.js",
14
+ "examples/",
15
+ "specs/",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test src/*.test.js src/commands/*.test.js"
20
+ },
21
+ "engines": {
22
+ "node": "22.x"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/11Agents/11agents-ai.git",
27
+ "directory": "gtm-swarm-cli"
28
+ },
29
+ "homepage": "https://11agents.ai",
30
+ "bugs": {
31
+ "url": "https://github.com/11Agents/11agents-ai/issues"
32
+ },
33
+ "license": "UNLICENSED",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,77 @@
1
+ # Agent JSON Contract
2
+
3
+ This is the short document an AI agent should read before pushing data to GTM Swarm.
4
+
5
+ ## Rules
6
+
7
+ - Use `schema_version: "swarm.telemetry.v1"`.
8
+ - Use ISO 8601 timestamps with timezone, preferably UTC `Z`.
9
+ - `workspace` is the GTM workspace slug.
10
+ - `agent_key` is a stable machine-readable agent name.
11
+ - `node_id` is a stable machine name.
12
+ - `platform` and `artifact_type` are lowercase.
13
+ - `external_id` is required and should match the source platform ID.
14
+ - Metric values must be numbers. Put raw strings and nested metadata in `payload`.
15
+
16
+ ## Batch
17
+
18
+ ```json
19
+ {
20
+ "schema_version": "swarm.telemetry.v1",
21
+ "workspace": "flatkey",
22
+ "agent_key": "x-growth-agent",
23
+ "node_id": "mac-mini-01",
24
+ "sent_at": "2026-05-25T09:30:00Z",
25
+ "artifacts": [],
26
+ "observations": []
27
+ }
28
+ ```
29
+
30
+ ## Artifact
31
+
32
+ Use an artifact when the agent created or discovered a durable object.
33
+
34
+ ```json
35
+ {
36
+ "platform": "x",
37
+ "artifact_type": "post",
38
+ "external_id": "1794312345678900000",
39
+ "url": "https://x.com/acme/status/1794312345678900000",
40
+ "body": "We shipped a new API key flow today.",
41
+ "created_at": "2026-05-25T08:10:00Z",
42
+ "payload": {
43
+ "account": "@acme"
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Observation
49
+
50
+ Use an observation when the agent collects current metrics for an artifact.
51
+
52
+ ```json
53
+ {
54
+ "platform": "x",
55
+ "artifact_type": "post",
56
+ "external_id": "1794312345678900000",
57
+ "observed_at": "2026-05-25T09:25:00Z",
58
+ "metrics": {
59
+ "views": 1834,
60
+ "replies": 12
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Push
66
+
67
+ ```bash
68
+ export GTM_SWARM_SERVER="https://gtm.shulex.com"
69
+ export GTM_SWARM_TOKEN="..."
70
+
71
+ gtm-swarm push batch ./result.json
72
+ ```
73
+
74
+ For examples, see:
75
+
76
+ - `examples/x-agent-batch.json`
77
+ - `examples/x-observation-job-result.json`
package/src/args.js ADDED
@@ -0,0 +1,43 @@
1
+ export function parseArgs(argv) {
2
+ const positional = []
3
+ const flags = {}
4
+ for (let i = 0; i < argv.length; i += 1) {
5
+ const arg = argv[i]
6
+ if (!arg.startsWith('--')) {
7
+ positional.push(arg)
8
+ continue
9
+ }
10
+ const key = arg.slice(2)
11
+ const next = argv[i + 1]
12
+ if (!next || next.startsWith('--')) {
13
+ flags[key] = true
14
+ continue
15
+ }
16
+ if (flags[key] === undefined) flags[key] = next
17
+ else if (Array.isArray(flags[key])) flags[key].push(next)
18
+ else flags[key] = [flags[key], next]
19
+ i += 1
20
+ }
21
+ return { positional, flags }
22
+ }
23
+
24
+ export function flag(flags, name, fallback = '') {
25
+ return flags[name] ?? fallback
26
+ }
27
+
28
+ export function listFlag(flags, name) {
29
+ const value = flags[name]
30
+ if (value === undefined) return []
31
+ return Array.isArray(value) ? value : [value]
32
+ }
33
+
34
+ export function parseMetricFlags(values) {
35
+ const metrics = {}
36
+ for (const item of values) {
37
+ const [key, raw] = String(item).split('=')
38
+ const value = Number(raw)
39
+ if (!key || !Number.isFinite(value)) throw new Error(`invalid metric: ${item}`)
40
+ metrics[key] = value
41
+ }
42
+ return metrics
43
+ }
package/src/client.js ADDED
@@ -0,0 +1,42 @@
1
+ export const DEFAULT_SERVER = 'https://app.11agents.ai'
2
+
3
+ export function getConfig(overrides = {}) {
4
+ const env = overrides.env || process.env
5
+ return {
6
+ server: overrides.server || env.ELEVENAGENTS_SERVER || env['11AGENTS_SERVER'] || env.GTM_SWARM_SERVER || DEFAULT_SERVER,
7
+ token: overrides.token || env.GTM_SWARM_TOKEN || '',
8
+ }
9
+ }
10
+
11
+ export function getControlConfig(overrides = {}) {
12
+ const env = overrides.env || process.env
13
+ return {
14
+ server: overrides.server || env.ELEVENAGENTS_SERVER || env['11AGENTS_SERVER'] || env.GTM_SWARM_SERVER || DEFAULT_SERVER,
15
+ token: overrides.token || env.GTM_WRITES_TOKEN || '',
16
+ }
17
+ }
18
+
19
+ export async function requestJson(path, { method = 'GET', body = null, config = getConfig() } = {}) {
20
+ const headers = { 'content-type': 'application/json' }
21
+ if (config.token) headers.authorization = `Bearer ${config.token}`
22
+ const response = await fetch(`${config.server.replace(/\/$/, '')}${path}`, {
23
+ method,
24
+ headers,
25
+ body: body ? JSON.stringify(body) : undefined,
26
+ })
27
+ const data = await response.json().catch(() => ({}))
28
+ if (!response.ok) {
29
+ const error = new Error(data.error || `HTTP ${response.status}`)
30
+ error.status = response.status
31
+ throw error
32
+ }
33
+ return data
34
+ }
35
+
36
+ export function encodeQuery(params) {
37
+ const q = new URLSearchParams()
38
+ for (const [key, value] of Object.entries(params)) {
39
+ if (value !== undefined && value !== null && value !== '') q.set(key, String(value))
40
+ }
41
+ return q.toString()
42
+ }
@@ -0,0 +1,54 @@
1
+ import { pathToFileURL } from 'node:url'
2
+ import { flag } from '../args.js'
3
+ import { encodeQuery, requestJson } from '../client.js'
4
+
5
+ function sleep(ms) {
6
+ return new Promise(resolve => setTimeout(resolve, ms))
7
+ }
8
+
9
+ export async function runNode(flags) {
10
+ const workspace = flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || '')
11
+ const agent_key = flag(flags, 'agent', process.env.GTM_SWARM_AGENT || '')
12
+ const node_id = flag(flags, 'node', process.env.GTM_SWARM_NODE || 'local')
13
+ const handlerPath = flag(flags, 'handler')
14
+ const once = Boolean(flags.once)
15
+ const intervalMs = Number(flag(flags, 'interval-ms', '5000'))
16
+
17
+ if (!workspace) throw new Error('--workspace or GTM_SWARM_WORKSPACE required')
18
+ if (!agent_key) throw new Error('--agent or GTM_SWARM_AGENT required')
19
+ if (!handlerPath) throw new Error('--handler required')
20
+
21
+ const handlerModule = await import(pathToFileURL(handlerPath).href)
22
+ if (typeof handlerModule.handleJob !== 'function') throw new Error('handler must export async function handleJob(job)')
23
+
24
+ while (true) {
25
+ const qs = encodeQuery({ workspace, agent_key, node_id })
26
+ const lease = await requestJson(`/api/swarm/jobs/lease?${qs}`)
27
+ if (!lease.job) {
28
+ if (once) {
29
+ console.log(JSON.stringify({ job: null }, null, 2))
30
+ return
31
+ }
32
+ await sleep(intervalMs)
33
+ continue
34
+ }
35
+
36
+ console.log(JSON.stringify({ leased: lease.job.id, kind: lease.job.kind }, null, 2))
37
+ let completion
38
+ try {
39
+ completion = await handlerModule.handleJob(lease.job)
40
+ } catch (error) {
41
+ completion = {
42
+ status: 'failed',
43
+ summary: error instanceof Error ? error.message : String(error),
44
+ error: 'handler_error',
45
+ }
46
+ }
47
+ const result = await requestJson(`/api/swarm/jobs/${lease.job.id}/complete`, {
48
+ method: 'POST',
49
+ body: completion,
50
+ })
51
+ console.log(JSON.stringify(result, null, 2))
52
+ if (once) return
53
+ }
54
+ }