@11agents/cli 0.1.12 → 0.1.14
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 +9 -1
- package/bin/11agents.js +14 -0
- package/package.json +1 -1
- package/src/commands/logs.js +96 -0
- package/src/commands/runtime.js +86 -4
package/README.md
CHANGED
|
@@ -84,6 +84,13 @@ Run the daemon in the background:
|
|
|
84
84
|
|
|
85
85
|
Background mode writes its pid to `~/.11agents/daemon.pid` and logs to `~/.11agents/daemon.log`.
|
|
86
86
|
|
|
87
|
+
Inspect daemon and task execution logs:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
11agents logs daemon --tail 200
|
|
91
|
+
11agents logs task <taskId> --project <slug>
|
|
92
|
+
```
|
|
93
|
+
|
|
87
94
|
Useful daemon options:
|
|
88
95
|
|
|
89
96
|
```bash
|
|
@@ -99,10 +106,11 @@ On startup, and every 30 minutes after that, the daemon syncs project metadata a
|
|
|
99
106
|
- Agent-local skills: `~/.11agents/<project>/agents/<agent>/skills/`
|
|
100
107
|
- Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
|
|
101
108
|
- Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
|
|
109
|
+
- Task execution logs: `~/.11agents/<project>/runs/<taskId>/`
|
|
102
110
|
|
|
103
111
|
Codex runs from `~/.11agents/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
|
|
104
112
|
|
|
105
|
-
The built-in Codex worker starts task executions with `codex --
|
|
113
|
+
The built-in Codex worker starts task executions with `codex --yolo exec` and prefixes the task prompt with `/goal ` by default so remote runtime tasks can run without approval prompts or sandbox restrictions. To opt a daemon back into a Codex sandbox, start it with `--codex-sandbox read-only`, `--codex-sandbox workspace-write`, or `--codex-sandbox danger-full-access`.
|
|
106
114
|
|
|
107
115
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
108
116
|
|
package/bin/11agents.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { parseArgs } from '../src/args.js'
|
|
5
5
|
import { knowledgeStatus, syncKnowledge } from '../src/commands/knowledge.js'
|
|
6
|
+
import { showDaemonLog, showTaskLog } from '../src/commands/logs.js'
|
|
6
7
|
import { runNode } from '../src/commands/node.js'
|
|
7
8
|
import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
|
|
8
9
|
import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
|
|
@@ -24,6 +25,8 @@ Usage:
|
|
|
24
25
|
11agents daemon status
|
|
25
26
|
11agents daemon stop
|
|
26
27
|
11agents daemon start --handler ./worker.js # optional custom worker override
|
|
28
|
+
11agents logs daemon [--tail 200]
|
|
29
|
+
11agents logs task <task-id> [--project <slug>] [--tail 120]
|
|
27
30
|
11agents mcp start
|
|
28
31
|
11agents validate <file>
|
|
29
32
|
11agents push batch <file>
|
|
@@ -116,6 +119,17 @@ async function main() {
|
|
|
116
119
|
return
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
if (command === 'logs' && subcommand === 'daemon') {
|
|
123
|
+
await showDaemonLog(flags)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command === 'logs' && subcommand === 'task') {
|
|
128
|
+
if (!target) throw new Error('logs task requires a task id')
|
|
129
|
+
await showTaskLog(target, flags)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
if (command === 'mcp' && (!subcommand || subcommand === 'start')) {
|
|
120
134
|
await startMcpServer()
|
|
121
135
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { backgroundPaths } from '../daemon-process.js'
|
|
5
|
+
import { flag } from '../args.js'
|
|
6
|
+
|
|
7
|
+
function tailText(text, lines) {
|
|
8
|
+
const count = Math.max(1, Number(lines) || 200)
|
|
9
|
+
const parts = String(text || '').replace(/\n$/, '').split('\n')
|
|
10
|
+
return parts.slice(-count).join('\n')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readText(filePath, fallback = '') {
|
|
14
|
+
try {
|
|
15
|
+
return await readFile(filePath, 'utf8')
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.code === 'ENOENT') return fallback
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readJson(filePath, fallback = {}) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error?.code === 'ENOENT') return fallback
|
|
27
|
+
return fallback
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readDaemonLog({ homeDir = homedir(), tail = 200 } = {}) {
|
|
32
|
+
const text = await readText(backgroundPaths(homeDir).logPath, '')
|
|
33
|
+
return tailText(text, tail)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function findTaskRunDir({ taskId, project = '', homeDir = homedir() }) {
|
|
37
|
+
if (!taskId) throw new Error('task id is required')
|
|
38
|
+
const baseDir = path.join(homeDir, '.11agents')
|
|
39
|
+
if (project) return path.join(baseDir, project, 'runs', taskId)
|
|
40
|
+
|
|
41
|
+
const entries = await readdir(baseDir, { withFileTypes: true }).catch(error => {
|
|
42
|
+
if (error?.code === 'ENOENT') return []
|
|
43
|
+
throw error
|
|
44
|
+
})
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory()) continue
|
|
47
|
+
const runDir = path.join(baseDir, entry.name, 'runs', taskId)
|
|
48
|
+
const meta = await readJson(path.join(runDir, 'meta.json'), null)
|
|
49
|
+
if (meta) return runDir
|
|
50
|
+
}
|
|
51
|
+
return path.join(baseDir, 'project', 'runs', taskId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function formatTaskLog({ taskId, project = '', homeDir = homedir(), tail = 120 } = {}) {
|
|
55
|
+
const runDir = await findTaskRunDir({ taskId, project, homeDir })
|
|
56
|
+
const meta = await readJson(path.join(runDir, 'meta.json'), null)
|
|
57
|
+
if (!meta) throw new Error(`task log not found: ${taskId}`)
|
|
58
|
+
|
|
59
|
+
const stdout = tailText(await readText(path.join(runDir, 'stdout.log'), ''), tail)
|
|
60
|
+
const stderr = tailText(await readText(path.join(runDir, 'stderr.log'), ''), tail)
|
|
61
|
+
const completion = await readText(path.join(runDir, 'completion.json'), '')
|
|
62
|
+
return [
|
|
63
|
+
`task: ${meta.task_id || taskId}`,
|
|
64
|
+
`provider: ${meta.provider || ''}`,
|
|
65
|
+
`workdir: ${meta.workdir || ''}`,
|
|
66
|
+
`command: ${meta.command_line || ''}`,
|
|
67
|
+
`exit_code: ${meta.exit_code ?? ''}`,
|
|
68
|
+
`run_dir: ${runDir}`,
|
|
69
|
+
'',
|
|
70
|
+
'stdout:',
|
|
71
|
+
stdout,
|
|
72
|
+
'',
|
|
73
|
+
'stderr:',
|
|
74
|
+
stderr,
|
|
75
|
+
'',
|
|
76
|
+
'completion:',
|
|
77
|
+
completion.trim(),
|
|
78
|
+
].join('\n').trimEnd()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function showDaemonLog(flags = {}, deps = {}) {
|
|
82
|
+
const log = await readDaemonLog({ homeDir: deps.homeDir, tail: flag(flags, 'tail', '200') })
|
|
83
|
+
;(deps.log || console.log)(log)
|
|
84
|
+
return log
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function showTaskLog(taskId, flags = {}, deps = {}) {
|
|
88
|
+
const text = await formatTaskLog({
|
|
89
|
+
taskId,
|
|
90
|
+
project: flag(flags, 'project'),
|
|
91
|
+
homeDir: deps.homeDir,
|
|
92
|
+
tail: flag(flags, 'tail', '120'),
|
|
93
|
+
})
|
|
94
|
+
;(deps.log || console.log)(text)
|
|
95
|
+
return text
|
|
96
|
+
}
|
package/src/commands/runtime.js
CHANGED
|
@@ -96,6 +96,41 @@ function createRetryState() {
|
|
|
96
96
|
return { failures: 0 }
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
async function heartbeatRegisteredRuntime(registration, flags, deps) {
|
|
100
|
+
const config = configFromFlags(flags)
|
|
101
|
+
const machineKey = registration?.machine?.machine_key || machineOverride(flags) || ''
|
|
102
|
+
if (!machineKey) return null
|
|
103
|
+
return deps.requestJson('/api/runtime/machines/heartbeat', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
body: {
|
|
106
|
+
machine_key: machineKey,
|
|
107
|
+
runtime_providers: (registration?.runtimes || []).map(runtime => runtime.provider).filter(Boolean),
|
|
108
|
+
health: {
|
|
109
|
+
heartbeat_at: new Date().toISOString(),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
config,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runWithRuntimeHeartbeat(operation, registration, flags, deps, heartbeatIntervalMs) {
|
|
117
|
+
const intervalMs = Math.max(10, Math.min(Number(heartbeatIntervalMs) || 15000, 60000))
|
|
118
|
+
const timer = setInterval(() => {
|
|
119
|
+
heartbeatRegisteredRuntime(registration, flags, deps).catch(error => {
|
|
120
|
+
deps.log(JSON.stringify({
|
|
121
|
+
warning: 'runtime heartbeat during task failed',
|
|
122
|
+
error: errorMessage(error),
|
|
123
|
+
}, null, 2))
|
|
124
|
+
})
|
|
125
|
+
}, intervalMs)
|
|
126
|
+
timer.unref?.()
|
|
127
|
+
try {
|
|
128
|
+
return await operation()
|
|
129
|
+
} finally {
|
|
130
|
+
clearInterval(timer)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
async function runWithDaemonRetry(label, operation, deps, retryState) {
|
|
100
135
|
while (true) {
|
|
101
136
|
try {
|
|
@@ -658,6 +693,19 @@ async function appendTaskMemoryDelta({ task, completion, workdir }) {
|
|
|
658
693
|
return { written: true, path: indexPath }
|
|
659
694
|
}
|
|
660
695
|
|
|
696
|
+
async function writeRunFile(runDir, fileName, content) {
|
|
697
|
+
if (!runDir) return
|
|
698
|
+
await mkdir(runDir, { recursive: true })
|
|
699
|
+
await writeFile(path.join(runDir, fileName), content)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function updateRunMeta(runDir, patch) {
|
|
703
|
+
if (!runDir) return
|
|
704
|
+
const metaPath = path.join(runDir, 'meta.json')
|
|
705
|
+
const current = await readJsonFile(metaPath, {})
|
|
706
|
+
await writeRunFile(runDir, 'meta.json', JSON.stringify({ ...current, ...patch }, null, 2))
|
|
707
|
+
}
|
|
708
|
+
|
|
661
709
|
function databaseSyncSpec(task) {
|
|
662
710
|
const spec = task.database || task.cloud_database || task.workspace?.database || null
|
|
663
711
|
if (!spec || typeof spec !== 'object') return null
|
|
@@ -720,7 +768,9 @@ function agentEnvironment(task) {
|
|
|
720
768
|
async function prepareRuntimeTask(task, flags, deps, config) {
|
|
721
769
|
const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
|
|
722
770
|
const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
|
|
771
|
+
const runDir = path.join(workdir, 'runs', sanitizeTaskId(task.id))
|
|
723
772
|
await mkdir(tmpDir, { recursive: true })
|
|
773
|
+
await mkdir(runDir, { recursive: true })
|
|
724
774
|
|
|
725
775
|
const database = await syncDatabaseIfNeeded({ task, workdir, config, flags, deps })
|
|
726
776
|
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
@@ -735,6 +785,7 @@ async function prepareRuntimeTask(task, flags, deps, config) {
|
|
|
735
785
|
return {
|
|
736
786
|
workdir,
|
|
737
787
|
tmp_dir: tmpDir,
|
|
788
|
+
run_dir: runDir,
|
|
738
789
|
project_slug: projectSlugForTask(task, flags),
|
|
739
790
|
readonly: true,
|
|
740
791
|
env,
|
|
@@ -839,10 +890,22 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
839
890
|
if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
|
|
840
891
|
|
|
841
892
|
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
893
|
+
const runDir = task.execution_context?.run_dir
|
|
894
|
+
await writeRunFile(runDir, 'prompt.md', `/goal ${prompt}`)
|
|
895
|
+
await updateRunMeta(runDir, {
|
|
896
|
+
provider: 'codex',
|
|
897
|
+
command: codexBin,
|
|
898
|
+
args,
|
|
899
|
+
command_line: commandLine,
|
|
900
|
+
workdir,
|
|
901
|
+
})
|
|
842
902
|
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
843
903
|
const result = await deps.runProcess(codexBin, args, { input: `/goal ${prompt}`, cwd: workdir, env: task.execution_context?.env || process.env })
|
|
844
904
|
const output = String(result.stdout || '').trim()
|
|
845
905
|
const error = String(result.stderr || '').trim()
|
|
906
|
+
await writeRunFile(runDir, 'stdout.log', String(result.stdout || ''))
|
|
907
|
+
await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
|
|
908
|
+
await updateRunMeta(runDir, { exit_code: result.code })
|
|
846
909
|
if (result.code !== 0) {
|
|
847
910
|
const body = error || output || `codex exited with status ${result.code}`
|
|
848
911
|
const trustHint = body.includes('--skip-git-repo-check')
|
|
@@ -906,7 +969,7 @@ function defaultTaskHandler(flags, deps) {
|
|
|
906
969
|
}
|
|
907
970
|
}
|
|
908
971
|
|
|
909
|
-
async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule, retryState = createRetryState()) {
|
|
972
|
+
async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule, retryState = createRetryState(), heartbeatIntervalMs = 15000) {
|
|
910
973
|
if (!handlerModule) return 0
|
|
911
974
|
const config = configFromFlags(flags)
|
|
912
975
|
const machineKey = registration?.machine?.machine_key || machineOverride(flags) || ''
|
|
@@ -967,8 +1030,23 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
967
1030
|
if (!completion) {
|
|
968
1031
|
executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
969
1032
|
runtimeTask.execution_context = executionContext
|
|
1033
|
+
await updateRunMeta(executionContext.run_dir, {
|
|
1034
|
+
task_id: String(runtimeTask.id || ''),
|
|
1035
|
+
runtime_id: String(runtimeTask.runtime_id || ''),
|
|
1036
|
+
provider: runtimeTask.runtime?.provider || runtime.provider || '',
|
|
1037
|
+
project_slug: executionContext.project_slug,
|
|
1038
|
+
agent: agentNameForTask(runtimeTask),
|
|
1039
|
+
issue_title: runtimeTask.issue?.title || '',
|
|
1040
|
+
started_at: new Date().toISOString(),
|
|
1041
|
+
})
|
|
970
1042
|
try {
|
|
971
|
-
completion = await
|
|
1043
|
+
completion = await runWithRuntimeHeartbeat(
|
|
1044
|
+
() => handlerModule.handleRuntimeTask(runtimeTask),
|
|
1045
|
+
registration,
|
|
1046
|
+
flags,
|
|
1047
|
+
deps,
|
|
1048
|
+
heartbeatIntervalMs
|
|
1049
|
+
)
|
|
972
1050
|
} catch (error) {
|
|
973
1051
|
completion = {
|
|
974
1052
|
comment: error instanceof Error ? error.message : String(error),
|
|
@@ -978,6 +1056,10 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
978
1056
|
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
979
1057
|
}
|
|
980
1058
|
}
|
|
1059
|
+
if (executionContext) {
|
|
1060
|
+
await writeRunFile(executionContext.run_dir, 'completion.json', JSON.stringify(normalizeTaskCompletion(runtimeTask, completion), null, 2))
|
|
1061
|
+
await updateRunMeta(executionContext.run_dir, { ended_at: new Date().toISOString() })
|
|
1062
|
+
}
|
|
981
1063
|
if (executionContext) {
|
|
982
1064
|
await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
|
|
983
1065
|
}
|
|
@@ -1063,7 +1145,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1063
1145
|
const retryState = createRetryState()
|
|
1064
1146
|
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1065
1147
|
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1066
|
-
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
1148
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1067
1149
|
if (once) return
|
|
1068
1150
|
|
|
1069
1151
|
let lastScan = Date.now()
|
|
@@ -1082,7 +1164,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1082
1164
|
lastHeartbeat = now
|
|
1083
1165
|
}
|
|
1084
1166
|
if (now - lastTaskPoll >= taskIntervalMs) {
|
|
1085
|
-
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
1167
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1086
1168
|
lastTaskPoll = now
|
|
1087
1169
|
}
|
|
1088
1170
|
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|