@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 +103 -0
- package/bin/11agents.js +135 -0
- package/bin/gtm-swarm.js +74 -0
- package/examples/voc-mcp-tool-call-batch.json +45 -0
- package/examples/x-agent-batch.json +54 -0
- package/examples/x-observation-job-result.json +24 -0
- package/package.json +37 -0
- package/specs/agent-json-contract.md +77 -0
- package/src/args.js +43 -0
- package/src/client.js +42 -0
- package/src/commands/node.js +54 -0
- package/src/commands/push.js +56 -0
- package/src/commands/runtime.js +353 -0
- package/src/daemon-process.js +92 -0
- package/src/info.js +62 -0
- package/src/runtime-scan.js +114 -0
- package/src/schema.js +59 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { flag, listFlag, parseMetricFlags } from '../args.js'
|
|
3
|
+
import { requestJson } from '../client.js'
|
|
4
|
+
import { buildTelemetryBatch, validateTelemetryBatch } from '../schema.js'
|
|
5
|
+
|
|
6
|
+
function defaults(flags) {
|
|
7
|
+
return {
|
|
8
|
+
workspace: flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''),
|
|
9
|
+
agent_key: flag(flags, 'agent', process.env.GTM_SWARM_AGENT || ''),
|
|
10
|
+
node_id: flag(flags, 'node', process.env.GTM_SWARM_NODE || 'local'),
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function sendBatch(batch) {
|
|
15
|
+
const validation = validateTelemetryBatch(batch)
|
|
16
|
+
if (!validation.ok) throw new Error(validation.error)
|
|
17
|
+
return requestJson('/api/swarm/ingest', { method: 'POST', body: batch })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function pushBatch(file) {
|
|
21
|
+
const body = JSON.parse(await readFile(file, 'utf-8'))
|
|
22
|
+
const result = await sendBatch(body)
|
|
23
|
+
console.log(JSON.stringify(result, null, 2))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function pushArtifact(flags) {
|
|
27
|
+
const base = defaults(flags)
|
|
28
|
+
const artifact = {
|
|
29
|
+
platform: flag(flags, 'platform', 'x'),
|
|
30
|
+
artifact_type: flag(flags, 'type'),
|
|
31
|
+
external_id: flag(flags, 'external-id'),
|
|
32
|
+
url: flag(flags, 'url') || null,
|
|
33
|
+
title: flag(flags, 'title') || null,
|
|
34
|
+
body: flag(flags, 'body') || null,
|
|
35
|
+
created_at: flag(flags, 'created-at', new Date().toISOString()),
|
|
36
|
+
payload: {},
|
|
37
|
+
}
|
|
38
|
+
const batch = buildTelemetryBatch({ ...base, artifacts: [artifact], observations: [] })
|
|
39
|
+
const result = await sendBatch(batch)
|
|
40
|
+
console.log(JSON.stringify(result, null, 2))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function pushObservation(flags) {
|
|
44
|
+
const base = defaults(flags)
|
|
45
|
+
const observation = {
|
|
46
|
+
platform: flag(flags, 'platform', 'x'),
|
|
47
|
+
artifact_type: flag(flags, 'type'),
|
|
48
|
+
external_id: flag(flags, 'external-id'),
|
|
49
|
+
observed_at: flag(flags, 'observed-at', new Date().toISOString()),
|
|
50
|
+
metrics: parseMetricFlags(listFlag(flags, 'metric')),
|
|
51
|
+
payload: {},
|
|
52
|
+
}
|
|
53
|
+
const batch = buildTelemetryBatch({ ...base, artifacts: [], observations: [observation] })
|
|
54
|
+
const result = await sendBatch(batch)
|
|
55
|
+
console.log(JSON.stringify(result, null, 2))
|
|
56
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { flag } from '../args.js'
|
|
7
|
+
import { getControlConfig, requestJson } from '../client.js'
|
|
8
|
+
import { buildRuntimeScan } from '../runtime-scan.js'
|
|
9
|
+
|
|
10
|
+
const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
|
|
11
|
+
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runProcess(command, args, { input = '', cwd = process.cwd(), env = process.env } = {}) {
|
|
17
|
+
return new Promise(resolve => {
|
|
18
|
+
const child = spawn(command, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
19
|
+
let stdout = ''
|
|
20
|
+
let stderr = ''
|
|
21
|
+
child.stdout.on('data', chunk => { stdout += chunk })
|
|
22
|
+
child.stderr.on('data', chunk => { stderr += chunk })
|
|
23
|
+
child.on('error', error => {
|
|
24
|
+
resolve({ code: 127, stdout, stderr: error.message })
|
|
25
|
+
})
|
|
26
|
+
child.on('close', code => {
|
|
27
|
+
resolve({ code: code ?? 1, stdout, stderr })
|
|
28
|
+
})
|
|
29
|
+
child.stdin.end(input)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configFromFlags(flags) {
|
|
34
|
+
return getControlConfig({
|
|
35
|
+
server: flag(flags, 'server'),
|
|
36
|
+
token: flag(flags, 'token'),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function machineOverride(flags) {
|
|
41
|
+
return flag(flags, 'machine')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scanEnvWithOverrides(flags) {
|
|
45
|
+
const machine = machineOverride(flags)
|
|
46
|
+
if (!machine) return process.env
|
|
47
|
+
return {
|
|
48
|
+
...process.env,
|
|
49
|
+
ELEVENAGENTS_MACHINE: machine,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runtimeDeps(overrides = {}) {
|
|
54
|
+
return {
|
|
55
|
+
buildRuntimeScan: overrides.buildRuntimeScan || buildRuntimeScan,
|
|
56
|
+
importHandler: overrides.importHandler || (async handlerPath => import(pathToFileURL(resolve(handlerPath)).href)),
|
|
57
|
+
log: overrides.log || (value => console.log(value)),
|
|
58
|
+
runCodex: overrides.runCodex || runCodex,
|
|
59
|
+
runProcess: overrides.runProcess || runProcess,
|
|
60
|
+
requestJson: overrides.requestJson || requestJson,
|
|
61
|
+
sleep: overrides.sleep || sleep,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function errorMessage(error) {
|
|
66
|
+
return error instanceof Error ? error.message : String(error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function retryDelayMs(failures) {
|
|
70
|
+
const initialMs = 1000
|
|
71
|
+
const maxMs = 15 * 60 * 1000
|
|
72
|
+
return Math.min(maxMs, initialMs * (2 ** Math.max(0, failures - 1)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createRetryState() {
|
|
76
|
+
return { failures: 0 }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runWithDaemonRetry(label, operation, deps, retryState) {
|
|
80
|
+
while (true) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await operation()
|
|
83
|
+
retryState.failures = 0
|
|
84
|
+
return result
|
|
85
|
+
} catch (error) {
|
|
86
|
+
retryState.failures += 1
|
|
87
|
+
const delay = retryDelayMs(retryState.failures)
|
|
88
|
+
deps.log(JSON.stringify({
|
|
89
|
+
retrying: label,
|
|
90
|
+
error: errorMessage(error),
|
|
91
|
+
consecutive_failures: retryState.failures,
|
|
92
|
+
next_retry_ms: delay,
|
|
93
|
+
}, null, 2))
|
|
94
|
+
await deps.sleep(delay)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function scanRuntime(flags = {}) {
|
|
100
|
+
const scan = await buildRuntimeScan({ env: scanEnvWithOverrides(flags), cliVersion: CLI_VERSION })
|
|
101
|
+
console.log(JSON.stringify(scan, null, 2))
|
|
102
|
+
return scan
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function registerRuntime(flags = {}, deps = {}) {
|
|
106
|
+
const { buildRuntimeScan: scanBuilder, log, requestJson: request } = runtimeDeps(deps)
|
|
107
|
+
const config = configFromFlags(flags)
|
|
108
|
+
if (!config.token) throw new Error('GTM_WRITES_TOKEN or --token is required')
|
|
109
|
+
|
|
110
|
+
const scan = await scanBuilder({ env: scanEnvWithOverrides(flags), cliVersion: CLI_VERSION })
|
|
111
|
+
if (!scan.runtimes.length) throw new Error('no local AI runtimes detected on PATH')
|
|
112
|
+
|
|
113
|
+
const result = await request('/api/runtime/machines/register', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: scan,
|
|
116
|
+
config,
|
|
117
|
+
})
|
|
118
|
+
log(JSON.stringify(result, null, 2))
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function heartbeatRuntime(flags = {}, deps = {}) {
|
|
123
|
+
const { buildRuntimeScan: scanBuilder, log, requestJson: request } = runtimeDeps(deps)
|
|
124
|
+
const config = configFromFlags(flags)
|
|
125
|
+
if (!config.token) throw new Error('GTM_WRITES_TOKEN or --token is required')
|
|
126
|
+
|
|
127
|
+
const scan = await scanBuilder({ env: scanEnvWithOverrides(flags), cliVersion: CLI_VERSION })
|
|
128
|
+
const result = await request('/api/runtime/machines/heartbeat', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: {
|
|
131
|
+
machine_key: scan.machine.machine_key,
|
|
132
|
+
runtime_providers: scan.runtimes.map(runtime => runtime.provider),
|
|
133
|
+
health: {
|
|
134
|
+
heartbeat_at: new Date().toISOString(),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
config,
|
|
138
|
+
})
|
|
139
|
+
log(JSON.stringify(result, null, 2))
|
|
140
|
+
return result
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeTaskCompletion(task, completion) {
|
|
144
|
+
const result = completion && typeof completion === 'object' ? completion : {}
|
|
145
|
+
return {
|
|
146
|
+
task_id: task.id,
|
|
147
|
+
runtime_id: task.runtime_id,
|
|
148
|
+
machine_key: task.runtime?.machine_key || '',
|
|
149
|
+
comment: String(result.comment || result.summary || ''),
|
|
150
|
+
memory_delta: String(result.memory_delta || result.memoryDelta || ''),
|
|
151
|
+
status: result.status ? String(result.status) : null,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function loadTaskHandler(handlerPath, deps) {
|
|
156
|
+
if (!handlerPath) return null
|
|
157
|
+
const handlerModule = await deps.importHandler(handlerPath)
|
|
158
|
+
if (typeof handlerModule.handleRuntimeTask !== 'function') {
|
|
159
|
+
throw new Error('handler must export async function handleRuntimeTask(task)')
|
|
160
|
+
}
|
|
161
|
+
return handlerModule
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function compactJson(value) {
|
|
165
|
+
return JSON.stringify(value ?? null, null, 2)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildCodexPrompt(task) {
|
|
169
|
+
return [
|
|
170
|
+
'You are executing an 11agents task as the assigned agent.',
|
|
171
|
+
'',
|
|
172
|
+
'Task context:',
|
|
173
|
+
compactJson({
|
|
174
|
+
queue_event: task.queue_event,
|
|
175
|
+
workspace: task.workspace,
|
|
176
|
+
issue: task.issue,
|
|
177
|
+
trigger_summary: task.trigger_summary,
|
|
178
|
+
thread_memory: task.thread_memory,
|
|
179
|
+
}),
|
|
180
|
+
'',
|
|
181
|
+
'Assigned agent context:',
|
|
182
|
+
compactJson({
|
|
183
|
+
id: task.agent?.id,
|
|
184
|
+
name: task.agent?.name,
|
|
185
|
+
instructions: task.agent?.instructions,
|
|
186
|
+
goal: task.agent?.goal,
|
|
187
|
+
memory: task.agent?.memory,
|
|
188
|
+
permissions: task.agent?.permissions,
|
|
189
|
+
triggers: task.agent?.triggers,
|
|
190
|
+
skills: task.agent?.skills,
|
|
191
|
+
}),
|
|
192
|
+
'',
|
|
193
|
+
'Thread comments:',
|
|
194
|
+
compactJson(task.comments || []),
|
|
195
|
+
'',
|
|
196
|
+
'Work in this repository and make the needed changes. When finished, respond with a concise summary for the task thread.',
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
201
|
+
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
202
|
+
const workdir = flag(flags, 'codex-workdir', process.cwd())
|
|
203
|
+
const args = [
|
|
204
|
+
'--ask-for-approval',
|
|
205
|
+
'never',
|
|
206
|
+
'exec',
|
|
207
|
+
'--sandbox',
|
|
208
|
+
flag(flags, 'codex-sandbox', 'workspace-write'),
|
|
209
|
+
'-C',
|
|
210
|
+
workdir,
|
|
211
|
+
'-',
|
|
212
|
+
]
|
|
213
|
+
const model = flag(flags, 'codex-model')
|
|
214
|
+
const profile = flag(flags, 'codex-profile')
|
|
215
|
+
const execIndex = args.indexOf('exec')
|
|
216
|
+
if (model) args.splice(execIndex + 1, 0, '--model', model)
|
|
217
|
+
if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
|
|
218
|
+
|
|
219
|
+
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir })
|
|
220
|
+
const output = String(result.stdout || '').trim()
|
|
221
|
+
const error = String(result.stderr || '').trim()
|
|
222
|
+
if (result.code !== 0) {
|
|
223
|
+
return {
|
|
224
|
+
comment: error || output || `codex exited with status ${result.code}`,
|
|
225
|
+
status: 'failed',
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
comment: output || `Codex completed task ${task.id}.`,
|
|
230
|
+
memory_delta: `Codex completed task ${task.id}.`,
|
|
231
|
+
status: 'in_review',
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function defaultTaskHandler(flags, deps) {
|
|
236
|
+
return {
|
|
237
|
+
async handleRuntimeTask(task) {
|
|
238
|
+
const provider = task.runtime?.provider || ''
|
|
239
|
+
if (provider !== 'codex') {
|
|
240
|
+
return {
|
|
241
|
+
comment: `unsupported runtime provider: ${provider || 'unknown'}`,
|
|
242
|
+
status: 'failed',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const prompt = buildCodexPrompt(task)
|
|
246
|
+
return deps.runCodex({ task, prompt, flags, deps })
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule, retryState = createRetryState()) {
|
|
252
|
+
if (!handlerModule) return 0
|
|
253
|
+
const config = configFromFlags(flags)
|
|
254
|
+
const machineKey = registration?.machine?.machine_key || machineOverride(flags) || ''
|
|
255
|
+
let handled = 0
|
|
256
|
+
|
|
257
|
+
for (const runtime of registration?.runtimes || []) {
|
|
258
|
+
if (!runtime?.id) continue
|
|
259
|
+
const claim = await runWithDaemonRetry('claim runtime task', () => (
|
|
260
|
+
deps.requestJson('/api/runtime/tasks/claim', {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
body: {
|
|
263
|
+
runtime_id: runtime.id,
|
|
264
|
+
machine_key: machineKey,
|
|
265
|
+
},
|
|
266
|
+
config,
|
|
267
|
+
})
|
|
268
|
+
), deps, retryState)
|
|
269
|
+
const task = claim?.task
|
|
270
|
+
if (!task) continue
|
|
271
|
+
|
|
272
|
+
const runtimeTask = {
|
|
273
|
+
...task,
|
|
274
|
+
runtime_id: task.runtime_id || runtime.id,
|
|
275
|
+
runtime: {
|
|
276
|
+
provider: runtime.provider,
|
|
277
|
+
model: runtime.model || '',
|
|
278
|
+
...(task.runtime || {}),
|
|
279
|
+
id: task.runtime?.id || runtime.id,
|
|
280
|
+
machine_key: task.runtime?.machine_key || machineKey,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
285
|
+
let completion
|
|
286
|
+
try {
|
|
287
|
+
completion = await handlerModule.handleRuntimeTask(runtimeTask)
|
|
288
|
+
} catch (error) {
|
|
289
|
+
completion = {
|
|
290
|
+
comment: error instanceof Error ? error.message : String(error),
|
|
291
|
+
status: 'failed',
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const body = normalizeTaskCompletion(runtimeTask, completion)
|
|
296
|
+
const result = await runWithDaemonRetry('complete runtime task', () => (
|
|
297
|
+
deps.requestJson('/api/runtime/tasks/complete', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
body,
|
|
300
|
+
config,
|
|
301
|
+
})
|
|
302
|
+
), deps, retryState)
|
|
303
|
+
deps.log(JSON.stringify(result, null, 2))
|
|
304
|
+
handled += 1
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return handled
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
311
|
+
const resolvedDeps = runtimeDeps(deps)
|
|
312
|
+
const heartbeatIntervalMs = Number(flag(flags, 'heartbeat-interval', '15')) * 1000
|
|
313
|
+
const scanIntervalMs = Number(flag(flags, 'scan-interval', '60')) * 1000
|
|
314
|
+
const taskIntervalMs = Number(flag(flags, 'task-interval', flag(flags, 'heartbeat-interval', '15'))) * 1000
|
|
315
|
+
const once = Boolean(flags.once)
|
|
316
|
+
const handlerPath = flag(flags, 'handler')
|
|
317
|
+
|
|
318
|
+
if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs <= 0) {
|
|
319
|
+
throw new Error('--heartbeat-interval must be a positive number of seconds')
|
|
320
|
+
}
|
|
321
|
+
if (!Number.isFinite(scanIntervalMs) || scanIntervalMs <= 0) {
|
|
322
|
+
throw new Error('--scan-interval must be a positive number of seconds')
|
|
323
|
+
}
|
|
324
|
+
if (!Number.isFinite(taskIntervalMs) || taskIntervalMs <= 0) {
|
|
325
|
+
throw new Error('--task-interval must be a positive number of seconds')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
|
|
329
|
+
const retryState = createRetryState()
|
|
330
|
+
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
331
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
332
|
+
if (once) return
|
|
333
|
+
|
|
334
|
+
let lastScan = Date.now()
|
|
335
|
+
let lastHeartbeat = Date.now()
|
|
336
|
+
let lastTaskPoll = Date.now()
|
|
337
|
+
while (true) {
|
|
338
|
+
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs))
|
|
339
|
+
const now = Date.now()
|
|
340
|
+
if (now - lastScan >= scanIntervalMs) {
|
|
341
|
+
registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
342
|
+
lastScan = now
|
|
343
|
+
lastHeartbeat = now
|
|
344
|
+
} else if (now - lastHeartbeat >= heartbeatIntervalMs) {
|
|
345
|
+
await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
346
|
+
lastHeartbeat = now
|
|
347
|
+
}
|
|
348
|
+
if (now - lastTaskPoll >= taskIntervalMs) {
|
|
349
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
350
|
+
lastTaskPoll = now
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { constants } from 'node:fs'
|
|
2
|
+
import { mkdir, open, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
export function backgroundPaths(homeDir = homedir()) {
|
|
8
|
+
const dir = join(homeDir, '.11agents')
|
|
9
|
+
return {
|
|
10
|
+
dir,
|
|
11
|
+
pidPath: join(dir, 'daemon.pid'),
|
|
12
|
+
logPath: join(dir, 'daemon.log'),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function withoutBackgroundFlag(argv) {
|
|
17
|
+
return argv.filter(arg => arg !== '--background')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parsePid(raw) {
|
|
21
|
+
const pid = Number.parseInt(String(raw).trim(), 10)
|
|
22
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readPid(pidPath) {
|
|
26
|
+
try {
|
|
27
|
+
return parsePid(await readFile(pidPath, 'utf-8'))
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error?.code === 'ENOENT') return null
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRunning(pid, killFn = process.kill) {
|
|
35
|
+
if (!pid) return false
|
|
36
|
+
try {
|
|
37
|
+
killFn(pid, 0)
|
|
38
|
+
return true
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function startBackgroundDaemon({
|
|
45
|
+
argv,
|
|
46
|
+
homeDir = homedir(),
|
|
47
|
+
nodePath = process.execPath,
|
|
48
|
+
scriptPath,
|
|
49
|
+
spawnFn = spawn,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const paths = backgroundPaths(homeDir)
|
|
52
|
+
await mkdir(paths.dir, { recursive: true })
|
|
53
|
+
const existingPid = await readPid(paths.pidPath)
|
|
54
|
+
if (isRunning(existingPid)) {
|
|
55
|
+
return { alreadyRunning: true, pid: existingPid, logPath: paths.logPath }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const log = await open(paths.logPath, constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY, 0o600)
|
|
59
|
+
try {
|
|
60
|
+
const child = spawnFn(nodePath, [scriptPath, ...withoutBackgroundFlag(argv)], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: ['ignore', log.fd, log.fd],
|
|
63
|
+
env: {
|
|
64
|
+
...process.env,
|
|
65
|
+
ELEVENAGENTS_DAEMON_BACKGROUND_CHILD: '1',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
if (!child?.pid) throw new Error('failed to start daemon')
|
|
69
|
+
await writeFile(paths.pidPath, String(child.pid))
|
|
70
|
+
child.unref?.()
|
|
71
|
+
return { pid: child.pid, logPath: paths.logPath }
|
|
72
|
+
} finally {
|
|
73
|
+
await log.close()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function statusBackgroundDaemon({ homeDir = homedir(), killFn = process.kill } = {}) {
|
|
78
|
+
const paths = backgroundPaths(homeDir)
|
|
79
|
+
const pid = await readPid(paths.pidPath)
|
|
80
|
+
if (!pid) return { running: false, stale: false, pid: null, logPath: paths.logPath }
|
|
81
|
+
const running = isRunning(pid, killFn)
|
|
82
|
+
return { running, stale: !running, pid, logPath: paths.logPath }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function stopBackgroundDaemon({ homeDir = homedir(), killFn = process.kill } = {}) {
|
|
86
|
+
const paths = backgroundPaths(homeDir)
|
|
87
|
+
const pid = await readPid(paths.pidPath)
|
|
88
|
+
if (!pid) return { stopped: false, pid: null }
|
|
89
|
+
if (isRunning(pid, killFn)) killFn(pid, 'SIGTERM')
|
|
90
|
+
await rm(paths.pidPath, { force: true })
|
|
91
|
+
return { stopped: true, pid }
|
|
92
|
+
}
|
package/src/info.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const PACKAGE_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json')
|
|
6
|
+
export const CLI_VERSION = JSON.parse(readFileSync(PACKAGE_PATH, 'utf-8')).version
|
|
7
|
+
|
|
8
|
+
export function compareVersions(current, latest) {
|
|
9
|
+
const left = String(current).split(/[.-]/).map(part => Number.parseInt(part, 10) || 0)
|
|
10
|
+
const right = String(latest).split(/[.-]/).map(part => Number.parseInt(part, 10) || 0)
|
|
11
|
+
const length = Math.max(left.length, right.length)
|
|
12
|
+
for (let i = 0; i < length; i += 1) {
|
|
13
|
+
if ((left[i] || 0) < (right[i] || 0)) return -1
|
|
14
|
+
if ((left[i] || 0) > (right[i] || 0)) return 1
|
|
15
|
+
}
|
|
16
|
+
return 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchLatestVersion({ fetchFn = globalThis.fetch, timeoutMs = 1500 } = {}) {
|
|
20
|
+
if (typeof fetchFn !== 'function') return null
|
|
21
|
+
const controller = new AbortController()
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchFn('https://registry.npmjs.org/@11agents%2fcli/latest', {
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
headers: { accept: 'application/json' },
|
|
27
|
+
})
|
|
28
|
+
if (!response.ok) return null
|
|
29
|
+
const data = await response.json()
|
|
30
|
+
return data?.version ? String(data.version) : null
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function buildStartupInfo({
|
|
37
|
+
currentVersion = CLI_VERSION,
|
|
38
|
+
server,
|
|
39
|
+
fetchLatestVersion: latestVersionFetcher = fetchLatestVersion,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const lines = [
|
|
42
|
+
`11agents CLI ${currentVersion}`,
|
|
43
|
+
`server: ${server}`,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const latest = await latestVersionFetcher()
|
|
48
|
+
if (latest && compareVersions(currentVersion, latest) < 0) {
|
|
49
|
+
lines.push(`New version available: ${currentVersion} -> ${latest}. Upgrade: npm install -g @11agents/cli@latest`)
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Startup info must never block CLI work.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function printStartupInfo(options = {}) {
|
|
59
|
+
if (options.quiet) return
|
|
60
|
+
const lines = await buildStartupInfo(options)
|
|
61
|
+
for (const line of lines) console.error(line)
|
|
62
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { access } from 'node:fs/promises'
|
|
4
|
+
import { constants } from 'node:fs'
|
|
5
|
+
import { execFile } from 'node:child_process'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile)
|
|
9
|
+
|
|
10
|
+
export const RUNTIME_EXECUTABLES = [
|
|
11
|
+
{ provider: 'codex', executable: 'codex' },
|
|
12
|
+
{ provider: 'claude', executable: 'claude' },
|
|
13
|
+
{ provider: 'gemini', executable: 'gemini' },
|
|
14
|
+
{ provider: 'cursor', executable: 'cursor-agent' },
|
|
15
|
+
{ provider: 'copilot', executable: 'copilot' },
|
|
16
|
+
{ provider: 'opencode', executable: 'opencode' },
|
|
17
|
+
{ provider: 'openclaw', executable: 'openclaw' },
|
|
18
|
+
{ provider: 'hermes', executable: 'hermes' },
|
|
19
|
+
{ provider: 'pi', executable: 'pi' },
|
|
20
|
+
{ provider: 'kimi', executable: 'kimi' },
|
|
21
|
+
{ provider: 'kiro', executable: 'kiro-cli' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function machineKeyFromEnv(env, hostname) {
|
|
25
|
+
return env.ELEVENAGENTS_MACHINE || env['11AGENTS_MACHINE'] || env.GTM_SWARM_NODE || hostname
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function lookupExecutable(name, env = process.env) {
|
|
29
|
+
const pathValue = env.PATH || ''
|
|
30
|
+
const extensions = process.platform === 'win32'
|
|
31
|
+
? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
32
|
+
: ['']
|
|
33
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
34
|
+
for (const ext of extensions) {
|
|
35
|
+
const candidate = path.join(dir, `${name}${ext}`)
|
|
36
|
+
try {
|
|
37
|
+
await access(candidate, constants.X_OK)
|
|
38
|
+
return candidate
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readRuntimeVersion(_provider, executablePath) {
|
|
46
|
+
const attempts = [
|
|
47
|
+
['--version'],
|
|
48
|
+
['version'],
|
|
49
|
+
['-v'],
|
|
50
|
+
]
|
|
51
|
+
let lastError = null
|
|
52
|
+
for (const args of attempts) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout, stderr } = await execFileAsync(executablePath, args, { timeout: 5000 })
|
|
55
|
+
const text = `${stdout || ''}${stderr || ''}`.trim()
|
|
56
|
+
if (text) return text.split('\n')[0]
|
|
57
|
+
} catch (error) {
|
|
58
|
+
lastError = error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastError || new Error('version unavailable')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function buildRuntimeScan({
|
|
65
|
+
env = process.env,
|
|
66
|
+
hostname = os.hostname,
|
|
67
|
+
platform = process.platform,
|
|
68
|
+
arch = process.arch,
|
|
69
|
+
cliVersion = 'dev',
|
|
70
|
+
lookupExecutable: lookup = lookupExecutable,
|
|
71
|
+
readVersion = readRuntimeVersion,
|
|
72
|
+
} = {}) {
|
|
73
|
+
const host = hostname()
|
|
74
|
+
const runtimes = []
|
|
75
|
+
|
|
76
|
+
for (const item of RUNTIME_EXECUTABLES) {
|
|
77
|
+
const executablePath = await lookup(item.executable, env)
|
|
78
|
+
if (!executablePath) continue
|
|
79
|
+
|
|
80
|
+
let version = null
|
|
81
|
+
const health = {}
|
|
82
|
+
try {
|
|
83
|
+
version = await readVersion(item.provider, executablePath)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
health.version_error = error instanceof Error ? error.message : String(error)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
runtimes.push({
|
|
89
|
+
provider: item.provider,
|
|
90
|
+
executable: item.executable,
|
|
91
|
+
executable_path: executablePath,
|
|
92
|
+
version,
|
|
93
|
+
model: env[`ELEVENAGENTS_${item.provider.toUpperCase()}_MODEL`] || '',
|
|
94
|
+
status: 'online',
|
|
95
|
+
capabilities: ['code', 'terminal'],
|
|
96
|
+
health,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
machine: {
|
|
102
|
+
machine_key: machineKeyFromEnv(env, host),
|
|
103
|
+
hostname: host,
|
|
104
|
+
platform,
|
|
105
|
+
arch,
|
|
106
|
+
cli_version: cliVersion,
|
|
107
|
+
capabilities: ['local_runtime'],
|
|
108
|
+
},
|
|
109
|
+
runtimes,
|
|
110
|
+
health: {
|
|
111
|
+
scan_at: new Date().toISOString(),
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|