@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.
@@ -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
+ }