@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 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 --ask-for-approval never --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`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -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 handlerModule.handleRuntimeTask(runtimeTask)
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) {