@11agents/cli 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +9 -0
- package/bin/11agents.js +23 -2
- package/examples/voc-mcp-tool-call-batch.json +1 -0
- package/examples/x-agent-batch.json +1 -0
- package/examples/x-observation-job-result.json +1 -0
- package/package.json +2 -2
- package/src/commands/knowledge.js +157 -0
- package/src/commands/push.js +1 -0
- package/src/commands/runtime.js +250 -3
- package/src/daemon-process.js +29 -3
- package/src/schema.js +5 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 11agents.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -64,6 +64,15 @@ Useful daemon options:
|
|
|
64
64
|
11agents daemon start --handler ./worker.js
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Before a claimed task is handed to the built-in Codex worker, the daemon prepares a local read-only project cache:
|
|
68
|
+
|
|
69
|
+
- Cloud database snapshot: `~/.11agents/projects/<project>/database/snapshot.json`
|
|
70
|
+
- Project knowledge base: `~/.11agents/projects/<project>/knowledge_base/`
|
|
71
|
+
- Skill sync state: `~/.11agents/projects/<project>/skills-state.json`
|
|
72
|
+
- Task scratch directory: `~/.11agents/projects/<project>/tmp/<taskId>/`
|
|
73
|
+
|
|
74
|
+
Codex runs from `~/.11agents/projects/<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.
|
|
75
|
+
|
|
67
76
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
68
77
|
|
|
69
78
|
```js
|
package/bin/11agents.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { parseArgs } from '../src/args.js'
|
|
5
|
+
import { knowledgeStatus, syncKnowledge } from '../src/commands/knowledge.js'
|
|
5
6
|
import { runNode } from '../src/commands/node.js'
|
|
6
7
|
import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
|
|
7
8
|
import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
|
|
8
9
|
import { startBackgroundDaemon, statusBackgroundDaemon, stopBackgroundDaemon } from '../src/daemon-process.js'
|
|
9
10
|
import { getControlConfig } from '../src/client.js'
|
|
10
|
-
import { printStartupInfo } from '../src/info.js'
|
|
11
|
+
import { CLI_VERSION, printStartupInfo } from '../src/info.js'
|
|
11
12
|
import { validateTelemetryBatch } from '../src/schema.js'
|
|
12
13
|
|
|
13
14
|
function usage() {
|
|
@@ -25,6 +26,8 @@ Usage:
|
|
|
25
26
|
11agents push batch <file>
|
|
26
27
|
11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
|
|
27
28
|
11agents push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123
|
|
29
|
+
11agents knowledge sync --project <slug> [--mode pull|push]
|
|
30
|
+
11agents knowledge status --project <slug>
|
|
28
31
|
11agents node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
|
|
29
32
|
|
|
30
33
|
Environment:
|
|
@@ -32,7 +35,12 @@ Environment:
|
|
|
32
35
|
GTM_SWARM_SERVER compatibility fallback for server URL
|
|
33
36
|
GTM_WRITES_TOKEN control-plane token for runtime registration
|
|
34
37
|
ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
|
|
35
|
-
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
38
|
+
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
39
|
+
|
|
40
|
+
Runtime task workspace:
|
|
41
|
+
Built-in Codex tasks run from ~/.11agents/projects/<project>/ by default.
|
|
42
|
+
That directory is read-only project context. Temporary writes belong under
|
|
43
|
+
./tmp/<taskId>/ and are cleaned by the daemon after the task finishes.`)
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
async function main() {
|
|
@@ -65,9 +73,12 @@ async function main() {
|
|
|
65
73
|
const result = await startBackgroundDaemon({
|
|
66
74
|
argv,
|
|
67
75
|
scriptPath: fileURLToPath(import.meta.url),
|
|
76
|
+
version: CLI_VERSION,
|
|
68
77
|
})
|
|
69
78
|
if (result.alreadyRunning) {
|
|
70
79
|
console.log(`11agents daemon already running with pid ${result.pid}`)
|
|
80
|
+
} else if (result.restarted) {
|
|
81
|
+
console.log(`11agents daemon restarted pid ${result.previousPid} -> ${result.pid}`)
|
|
71
82
|
} else {
|
|
72
83
|
console.log(`11agents daemon started with pid ${result.pid}`)
|
|
73
84
|
}
|
|
@@ -120,6 +131,16 @@ async function main() {
|
|
|
120
131
|
return
|
|
121
132
|
}
|
|
122
133
|
|
|
134
|
+
if (command === 'knowledge' && subcommand === 'sync') {
|
|
135
|
+
await syncKnowledge(flags)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === 'knowledge' && subcommand === 'status') {
|
|
140
|
+
await knowledgeStatus(flags)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
if (command === 'node' && subcommand === 'run') {
|
|
124
145
|
await runNode(flags)
|
|
125
146
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@11agents/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "11agents local runtime and telemetry CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"bugs": {
|
|
31
31
|
"url": "https://github.com/11Agents/11agents-ai/issues"
|
|
32
32
|
},
|
|
33
|
-
"license": "
|
|
33
|
+
"license": "MIT",
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
36
36
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { flag } from '../args.js'
|
|
5
|
+
import { requestJson } from '../client.js'
|
|
6
|
+
|
|
7
|
+
function slugify(value) {
|
|
8
|
+
return String(value || 'project')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '') || 'project'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeLocalPath(value, id) {
|
|
16
|
+
const raw = String(value || '').trim()
|
|
17
|
+
if (raw && !raw.startsWith('/') && !raw.split('/').includes('..')) return raw
|
|
18
|
+
return `nodes/${slugify(id)}.md`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeSnapshot(snapshot = {}) {
|
|
22
|
+
return {
|
|
23
|
+
cloud_revision: Number(snapshot.cloud_revision || snapshot.knowledge_base?.cloud_revision || 0),
|
|
24
|
+
local_revision: String(snapshot.local_revision || ''),
|
|
25
|
+
nodes: Array.isArray(snapshot.nodes) ? snapshot.nodes.map((node, index) => {
|
|
26
|
+
const id = slugify(node.id || node.title || `node-${index + 1}`)
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
kind: String(node.kind || 'document'),
|
|
30
|
+
title: String(node.title || id),
|
|
31
|
+
body: String(node.body || ''),
|
|
32
|
+
local_path: safeLocalPath(node.local_path || node.localPath, id),
|
|
33
|
+
metadata: node.metadata && typeof node.metadata === 'object' ? node.metadata : {},
|
|
34
|
+
}
|
|
35
|
+
}).sort((a, b) => a.id.localeCompare(b.id)) : [],
|
|
36
|
+
edges: Array.isArray(snapshot.edges) ? snapshot.edges.map(edge => ({
|
|
37
|
+
from: slugify(edge.from || edge.from_node_id),
|
|
38
|
+
to: slugify(edge.to || edge.to_node_id),
|
|
39
|
+
relation: slugify(edge.relation || 'related'),
|
|
40
|
+
metadata: edge.metadata && typeof edge.metadata === 'object' ? edge.metadata : {},
|
|
41
|
+
})).filter(edge => edge.from && edge.to).sort((a, b) => (
|
|
42
|
+
`${a.from}:${a.to}:${a.relation}`.localeCompare(`${b.from}:${b.to}:${b.relation}`)
|
|
43
|
+
)) : [],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function markdownForNode(node) {
|
|
48
|
+
return [
|
|
49
|
+
`# ${node.title}`,
|
|
50
|
+
'',
|
|
51
|
+
`kind: ${node.kind}`,
|
|
52
|
+
`id: ${node.id}`,
|
|
53
|
+
'',
|
|
54
|
+
node.body || '',
|
|
55
|
+
'',
|
|
56
|
+
].join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function bodyFromMarkdownNode(markdown, fallback = '') {
|
|
60
|
+
const lines = String(markdown || '').split('\n')
|
|
61
|
+
let index = 0
|
|
62
|
+
if (lines[index]?.startsWith('# ')) index += 1
|
|
63
|
+
if (lines[index] === '') index += 1
|
|
64
|
+
if (/^kind:\s*/.test(lines[index] || '')) index += 1
|
|
65
|
+
if (/^id:\s*/.test(lines[index] || '')) index += 1
|
|
66
|
+
if (lines[index] === '') index += 1
|
|
67
|
+
const body = lines.slice(index).join('\n').replace(/\n+$/, '')
|
|
68
|
+
return body || fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildKnowledgeBaseDir(project, { homeDir = os.homedir() } = {}) {
|
|
72
|
+
return path.join(homeDir, '.11agents', 'projects', slugify(project), 'knowledge_base')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function writeKnowledgeSnapshot(project, snapshot, { homeDir = os.homedir() } = {}) {
|
|
76
|
+
const normalized = normalizeSnapshot(snapshot)
|
|
77
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
78
|
+
const localRevision = normalized.local_revision || `${Date.now()}`
|
|
79
|
+
const index = {
|
|
80
|
+
cloud_revision: normalized.cloud_revision,
|
|
81
|
+
local_revision: localRevision,
|
|
82
|
+
nodes: normalized.nodes,
|
|
83
|
+
edges: normalized.edges,
|
|
84
|
+
}
|
|
85
|
+
await mkdir(path.join(baseDir, 'nodes'), { recursive: true })
|
|
86
|
+
await writeFile(path.join(baseDir, 'index.json'), JSON.stringify(index, null, 2))
|
|
87
|
+
await writeFile(path.join(baseDir, 'sync-state.json'), JSON.stringify({
|
|
88
|
+
cloud_revision: normalized.cloud_revision,
|
|
89
|
+
local_revision: localRevision,
|
|
90
|
+
synced_at: new Date().toISOString(),
|
|
91
|
+
}, null, 2))
|
|
92
|
+
for (const node of normalized.nodes) {
|
|
93
|
+
const target = path.join(baseDir, node.local_path)
|
|
94
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
95
|
+
await writeFile(target, markdownForNode(node))
|
|
96
|
+
}
|
|
97
|
+
return index
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function readKnowledgeSnapshot(project, { homeDir = os.homedir() } = {}) {
|
|
101
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
102
|
+
const raw = await readFile(path.join(baseDir, 'index.json'), 'utf-8').catch(() => '{}')
|
|
103
|
+
const snapshot = normalizeSnapshot(JSON.parse(raw))
|
|
104
|
+
const nodes = await Promise.all(snapshot.nodes.map(async node => {
|
|
105
|
+
const markdown = await readFile(path.join(baseDir, node.local_path), 'utf-8').catch(() => null)
|
|
106
|
+
if (markdown === null) return node
|
|
107
|
+
return { ...node, body: bodyFromMarkdownNode(markdown, node.body) }
|
|
108
|
+
}))
|
|
109
|
+
return { ...snapshot, nodes }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function syncKnowledge(flags = {}, deps = {}) {
|
|
113
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
114
|
+
if (!project) throw new Error('knowledge sync requires --project <slug>')
|
|
115
|
+
const mode = flag(flags, 'mode', flags.push ? 'push' : 'pull')
|
|
116
|
+
const request = deps.requestJson || requestJson
|
|
117
|
+
const log = deps.log || (value => console.log(value))
|
|
118
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
119
|
+
const localSnapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
120
|
+
const body = mode === 'push'
|
|
121
|
+
? { ...localSnapshot, mode: 'push', actor: 'cli' }
|
|
122
|
+
: { mode: 'pull', local_revision: localSnapshot.local_revision, cloud_revision: localSnapshot.cloud_revision, actor: 'cli' }
|
|
123
|
+
const result = await request(`/api/projects/${project}/knowledge/sync`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body,
|
|
126
|
+
})
|
|
127
|
+
const written = await writeKnowledgeSnapshot(project, result, { homeDir })
|
|
128
|
+
const payload = {
|
|
129
|
+
ok: true,
|
|
130
|
+
project,
|
|
131
|
+
mode,
|
|
132
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
133
|
+
cloud_revision: written.cloud_revision,
|
|
134
|
+
local_revision: written.local_revision,
|
|
135
|
+
nodes: written.nodes.length,
|
|
136
|
+
edges: written.edges.length,
|
|
137
|
+
}
|
|
138
|
+
log(JSON.stringify(payload, null, 2))
|
|
139
|
+
return { ...written, path: payload.path }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function knowledgeStatus(flags = {}, deps = {}) {
|
|
143
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
144
|
+
if (!project) throw new Error('knowledge status requires --project <slug>')
|
|
145
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
146
|
+
const snapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
147
|
+
const payload = {
|
|
148
|
+
project,
|
|
149
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
150
|
+
cloud_revision: snapshot.cloud_revision,
|
|
151
|
+
local_revision: snapshot.local_revision,
|
|
152
|
+
nodes: snapshot.nodes.length,
|
|
153
|
+
edges: snapshot.edges.length,
|
|
154
|
+
}
|
|
155
|
+
;(deps.log || (value => console.log(value)))(JSON.stringify(payload, null, 2))
|
|
156
|
+
return payload
|
|
157
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildTelemetryBatch, validateTelemetryBatch } from '../schema.js'
|
|
|
6
6
|
function defaults(flags) {
|
|
7
7
|
return {
|
|
8
8
|
workspace: flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''),
|
|
9
|
+
agent_id: flag(flags, 'agent-id', process.env.GTM_SWARM_AGENT_ID || ''),
|
|
9
10
|
agent_key: flag(flags, 'agent', process.env.GTM_SWARM_AGENT || ''),
|
|
10
11
|
node_id: flag(flags, 'node', process.env.GTM_SWARM_NODE || 'local'),
|
|
11
12
|
}
|
package/src/commands/runtime.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import os from 'node:os'
|
|
3
6
|
import { dirname, resolve } from 'node:path'
|
|
7
|
+
import path from 'node:path'
|
|
4
8
|
import { pathToFileURL } from 'node:url'
|
|
5
9
|
import { fileURLToPath } from 'node:url'
|
|
6
10
|
import { flag } from '../args.js'
|
|
7
11
|
import { getControlConfig, requestJson } from '../client.js'
|
|
12
|
+
import { syncKnowledge } from './knowledge.js'
|
|
8
13
|
import { buildRuntimeScan } from '../runtime-scan.js'
|
|
9
14
|
|
|
10
15
|
const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
|
|
@@ -55,10 +60,12 @@ function runtimeDeps(overrides = {}) {
|
|
|
55
60
|
buildRuntimeScan: overrides.buildRuntimeScan || buildRuntimeScan,
|
|
56
61
|
importHandler: overrides.importHandler || (async handlerPath => import(pathToFileURL(resolve(handlerPath)).href)),
|
|
57
62
|
log: overrides.log || (value => console.log(value)),
|
|
63
|
+
homeDir: overrides.homeDir || os.homedir(),
|
|
58
64
|
runCodex: overrides.runCodex || runCodex,
|
|
59
65
|
runProcess: overrides.runProcess || runProcess,
|
|
60
66
|
requestJson: overrides.requestJson || requestJson,
|
|
61
67
|
sleep: overrides.sleep || sleep,
|
|
68
|
+
syncKnowledge: overrides.syncKnowledge || syncKnowledge,
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -165,10 +172,211 @@ function compactJson(value) {
|
|
|
165
172
|
return JSON.stringify(value ?? null, null, 2)
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
const nonAlphaNum = /[^a-z0-9]+/g
|
|
176
|
+
|
|
177
|
+
function slugify(value, fallback = 'project') {
|
|
178
|
+
return String(value || fallback)
|
|
179
|
+
.trim()
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
.replace(nonAlphaNum, '-')
|
|
182
|
+
.replace(/^-|-$/g, '') || fallback
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sanitizeTaskId(value) {
|
|
186
|
+
return slugify(value, 'task')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function projectSlugForTask(task, flags = {}) {
|
|
190
|
+
return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function projectDirForTask(task, flags = {}, deps) {
|
|
194
|
+
return path.join(deps.homeDir, '.11agents', 'projects', projectSlugForTask(task, flags))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readJsonFile(filePath, fallback = {}) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error?.code === 'ENOENT') return fallback
|
|
202
|
+
return fallback
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableHash(value) {
|
|
207
|
+
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertSafeRelativePath(filePath) {
|
|
211
|
+
const clean = path.normalize(String(filePath || ''))
|
|
212
|
+
if (!clean || path.isAbsolute(clean) || clean === '..' || clean.startsWith(`..${path.sep}`)) {
|
|
213
|
+
throw new Error(`invalid skill file path: ${filePath}`)
|
|
214
|
+
}
|
|
215
|
+
return clean
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function decodeFileContent(file) {
|
|
219
|
+
if (file?.encoding === 'base64') return Buffer.from(String(file.content || ''), 'base64')
|
|
220
|
+
return Buffer.from(String(file?.content || ''), 'utf8')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function skillDirForProvider({ workdir, provider, codexHome }) {
|
|
224
|
+
if (provider === 'codex') return path.join(codexHome || path.join(os.homedir(), '.codex'), 'skills')
|
|
225
|
+
if (provider === 'claude') return path.join(workdir, '.claude', 'skills')
|
|
226
|
+
if (provider === 'copilot') return path.join(workdir, '.github', 'skills')
|
|
227
|
+
if (provider === 'opencode') return path.join(workdir, '.opencode', 'skills')
|
|
228
|
+
if (provider === 'cursor') return path.join(workdir, '.cursor', 'skills')
|
|
229
|
+
return path.join(workdir, '.agent_context', 'skills')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeSkillBundle(skill = {}) {
|
|
233
|
+
return {
|
|
234
|
+
id: String(skill.id || ''),
|
|
235
|
+
name: String(skill.name || 'skill'),
|
|
236
|
+
content: String(skill.content || ''),
|
|
237
|
+
files: Array.isArray(skill.files)
|
|
238
|
+
? skill.files.map(file => ({
|
|
239
|
+
path: String(file.path || ''),
|
|
240
|
+
encoding: file.encoding === 'base64' ? 'base64' : 'utf8',
|
|
241
|
+
content: String(file.content || ''),
|
|
242
|
+
})).sort((a, b) => a.path.localeCompare(b.path))
|
|
243
|
+
: [],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
248
|
+
const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
|
|
249
|
+
if (!skills.length) return { changed: false, count: 0 }
|
|
250
|
+
|
|
251
|
+
const statePath = path.join(workdir, 'skills-state.json')
|
|
252
|
+
const nextHash = stableHash(skills)
|
|
253
|
+
const current = await readJsonFile(statePath, {})
|
|
254
|
+
if (current.hash === nextHash) return { changed: false, count: skills.length }
|
|
255
|
+
|
|
256
|
+
const provider = task.runtime?.provider || ''
|
|
257
|
+
const skillsDir = skillDirForProvider({
|
|
258
|
+
workdir,
|
|
259
|
+
provider,
|
|
260
|
+
codexHome: flag(flags, 'codex-home', process.env.CODEX_HOME || path.join(deps.homeDir, '.codex')),
|
|
261
|
+
})
|
|
262
|
+
await mkdir(skillsDir, { recursive: true })
|
|
263
|
+
for (const skill of skills) {
|
|
264
|
+
const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
|
|
265
|
+
await mkdir(skillDir, { recursive: true })
|
|
266
|
+
await writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf8')
|
|
267
|
+
for (const file of skill.files) {
|
|
268
|
+
const relativePath = assertSafeRelativePath(file.path)
|
|
269
|
+
const target = path.join(skillDir, relativePath)
|
|
270
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
271
|
+
await writeFile(target, decodeFileContent(file))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
await writeFile(statePath, JSON.stringify({
|
|
275
|
+
hash: nextHash,
|
|
276
|
+
count: skills.length,
|
|
277
|
+
skills_dir: skillsDir,
|
|
278
|
+
synced_at: new Date().toISOString(),
|
|
279
|
+
}, null, 2))
|
|
280
|
+
return { changed: true, count: skills.length, skills_dir: skillsDir }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function databaseSyncSpec(task) {
|
|
284
|
+
const spec = task.database || task.cloud_database || task.workspace?.database || null
|
|
285
|
+
if (!spec || typeof spec !== 'object') return null
|
|
286
|
+
const cloudRevision = Number(spec.cloud_revision ?? spec.cloudRevision ?? spec.revision ?? 0)
|
|
287
|
+
const syncRequired = spec.sync_required === true || spec.syncRequired === true
|
|
288
|
+
if (!syncRequired && !cloudRevision) return null
|
|
289
|
+
return { cloudRevision, syncRequired }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function syncDatabaseIfNeeded({ task, workdir, config, deps }) {
|
|
293
|
+
const spec = databaseSyncSpec(task)
|
|
294
|
+
if (!spec) return { synced: false }
|
|
295
|
+
const statePath = path.join(workdir, 'database', 'sync-state.json')
|
|
296
|
+
const current = await readJsonFile(statePath, {})
|
|
297
|
+
const localRevision = Number(current.cloud_revision || 0)
|
|
298
|
+
if (!spec.syncRequired && localRevision === spec.cloudRevision) return { synced: false, cloud_revision: localRevision }
|
|
299
|
+
|
|
300
|
+
const project = projectSlugForTask(task)
|
|
301
|
+
const result = await deps.requestJson(`/api/projects/${project}/database/sync`, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
body: {
|
|
304
|
+
task_id: task.id,
|
|
305
|
+
local_revision: localRevision,
|
|
306
|
+
cloud_revision: spec.cloudRevision,
|
|
307
|
+
},
|
|
308
|
+
config,
|
|
309
|
+
})
|
|
310
|
+
await mkdir(path.dirname(statePath), { recursive: true })
|
|
311
|
+
if (result?.snapshot !== undefined) {
|
|
312
|
+
await writeFile(path.join(path.dirname(statePath), 'snapshot.json'), JSON.stringify(result.snapshot, null, 2))
|
|
313
|
+
}
|
|
314
|
+
await writeFile(statePath, JSON.stringify({
|
|
315
|
+
cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision),
|
|
316
|
+
synced_at: result?.synced_at || new Date().toISOString(),
|
|
317
|
+
}, null, 2))
|
|
318
|
+
return { synced: true, cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision) }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function agentEnvironment(task) {
|
|
322
|
+
const values = task.agent?.environment_variables || task.agent?.environment || task.agent?.custom_env || {}
|
|
323
|
+
const env = {}
|
|
324
|
+
if (Array.isArray(values)) {
|
|
325
|
+
for (const item of values) {
|
|
326
|
+
const key = String(item?.key || '').trim()
|
|
327
|
+
if (key) env[key] = String(item?.value || '')
|
|
328
|
+
}
|
|
329
|
+
return env
|
|
330
|
+
}
|
|
331
|
+
if (values && typeof values === 'object') {
|
|
332
|
+
for (const [key, value] of Object.entries(values)) {
|
|
333
|
+
const cleanKey = String(key || '').trim()
|
|
334
|
+
if (cleanKey) env[cleanKey] = String(value ?? '')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return env
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function prepareRuntimeTask(task, flags, deps, config) {
|
|
341
|
+
const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
|
|
342
|
+
const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
|
|
343
|
+
await mkdir(tmpDir, { recursive: true })
|
|
344
|
+
|
|
345
|
+
const database = await syncDatabaseIfNeeded({ task, workdir, config, deps })
|
|
346
|
+
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
347
|
+
const env = {
|
|
348
|
+
...process.env,
|
|
349
|
+
...agentEnvironment(task),
|
|
350
|
+
ELEVENAGENTS_PROJECT_DIR: workdir,
|
|
351
|
+
ELEVENAGENTS_TASK_TMP: tmpDir,
|
|
352
|
+
ELEVENAGENTS_TASK_ID: String(task.id || ''),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
workdir,
|
|
357
|
+
tmp_dir: tmpDir,
|
|
358
|
+
project_slug: projectSlugForTask(task, flags),
|
|
359
|
+
readonly: true,
|
|
360
|
+
env,
|
|
361
|
+
database,
|
|
362
|
+
skills,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
168
366
|
function buildCodexPrompt(task) {
|
|
169
367
|
return [
|
|
170
368
|
'You are executing an 11agents task as the assigned agent.',
|
|
171
369
|
'',
|
|
370
|
+
'Execution workspace:',
|
|
371
|
+
compactJson({
|
|
372
|
+
workdir: task.execution_context?.workdir,
|
|
373
|
+
tmp_dir: task.execution_context?.tmp_dir,
|
|
374
|
+
project_slug: task.execution_context?.project_slug,
|
|
375
|
+
knowledge_base: './knowledge_base/',
|
|
376
|
+
rule: 'Treat the project directory as read-only project context except ./knowledge_base/ for durable project knowledge updates and ./tmp/<taskId>/ for temporary scratch files.',
|
|
377
|
+
cleanup: 'Temporary files under ./tmp/<taskId>/ are removed by the CLI after the task finishes.',
|
|
378
|
+
}),
|
|
379
|
+
'',
|
|
172
380
|
'Task context:',
|
|
173
381
|
compactJson({
|
|
174
382
|
queue_event: task.queue_event,
|
|
@@ -176,6 +384,7 @@ function buildCodexPrompt(task) {
|
|
|
176
384
|
issue: task.issue,
|
|
177
385
|
trigger_summary: task.trigger_summary,
|
|
178
386
|
thread_memory: task.thread_memory,
|
|
387
|
+
project_knowledge: task.project_knowledge,
|
|
179
388
|
}),
|
|
180
389
|
'',
|
|
181
390
|
'Assigned agent context:',
|
|
@@ -199,11 +408,12 @@ function buildCodexPrompt(task) {
|
|
|
199
408
|
|
|
200
409
|
async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
201
410
|
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
202
|
-
const workdir = flag(flags, 'codex-workdir', process.cwd())
|
|
411
|
+
const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
|
|
203
412
|
const args = [
|
|
204
413
|
'--ask-for-approval',
|
|
205
414
|
'never',
|
|
206
415
|
'exec',
|
|
416
|
+
'--skip-git-repo-check',
|
|
207
417
|
'--sandbox',
|
|
208
418
|
flag(flags, 'codex-sandbox', 'workspace-write'),
|
|
209
419
|
'-C',
|
|
@@ -216,12 +426,18 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
216
426
|
if (model) args.splice(execIndex + 1, 0, '--model', model)
|
|
217
427
|
if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
|
|
218
428
|
|
|
219
|
-
const
|
|
429
|
+
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
430
|
+
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
431
|
+
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir, env: task.execution_context?.env || process.env })
|
|
220
432
|
const output = String(result.stdout || '').trim()
|
|
221
433
|
const error = String(result.stderr || '').trim()
|
|
222
434
|
if (result.code !== 0) {
|
|
435
|
+
const body = error || output || `codex exited with status ${result.code}`
|
|
436
|
+
const trustHint = body.includes('--skip-git-repo-check')
|
|
437
|
+
? '\n\nCodex was invoked with --skip-git-repo-check. If this message persists, the background daemon may still be running an older CLI; run `11agents daemon start --background` again or restart it.'
|
|
438
|
+
: ''
|
|
223
439
|
return {
|
|
224
|
-
comment:
|
|
440
|
+
comment: `${body}\n\nCodex command: ${commandLine}${trustHint}`,
|
|
225
441
|
status: 'failed',
|
|
226
442
|
}
|
|
227
443
|
}
|
|
@@ -282,6 +498,21 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
282
498
|
}
|
|
283
499
|
|
|
284
500
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
501
|
+
if (runtimeTask.workspace?.slug) {
|
|
502
|
+
await runWithDaemonRetry('sync knowledge base', () => (
|
|
503
|
+
deps.syncKnowledge({
|
|
504
|
+
project: runtimeTask.workspace.slug,
|
|
505
|
+
mode: 'pull',
|
|
506
|
+
server: flags.server,
|
|
507
|
+
token: flags.token,
|
|
508
|
+
}, {
|
|
509
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
510
|
+
log: () => {},
|
|
511
|
+
})
|
|
512
|
+
), deps, retryState)
|
|
513
|
+
}
|
|
514
|
+
const executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
515
|
+
runtimeTask.execution_context = executionContext
|
|
285
516
|
let completion
|
|
286
517
|
try {
|
|
287
518
|
completion = await handlerModule.handleRuntimeTask(runtimeTask)
|
|
@@ -290,6 +521,22 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
290
521
|
comment: error instanceof Error ? error.message : String(error),
|
|
291
522
|
status: 'failed',
|
|
292
523
|
}
|
|
524
|
+
} finally {
|
|
525
|
+
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (runtimeTask.workspace?.slug) {
|
|
529
|
+
await runWithDaemonRetry('sync knowledge base back to cloud', () => (
|
|
530
|
+
deps.syncKnowledge({
|
|
531
|
+
project: runtimeTask.workspace.slug,
|
|
532
|
+
mode: 'push',
|
|
533
|
+
server: flags.server,
|
|
534
|
+
token: flags.token,
|
|
535
|
+
}, {
|
|
536
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
537
|
+
log: () => {},
|
|
538
|
+
})
|
|
539
|
+
), deps, retryState)
|
|
293
540
|
}
|
|
294
541
|
|
|
295
542
|
const body = normalizeTaskCompletion(runtimeTask, completion)
|
package/src/daemon-process.js
CHANGED
|
@@ -9,6 +9,7 @@ export function backgroundPaths(homeDir = homedir()) {
|
|
|
9
9
|
return {
|
|
10
10
|
dir,
|
|
11
11
|
pidPath: join(dir, 'daemon.pid'),
|
|
12
|
+
metaPath: join(dir, 'daemon.json'),
|
|
12
13
|
logPath: join(dir, 'daemon.log'),
|
|
13
14
|
}
|
|
14
15
|
}
|
|
@@ -31,6 +32,15 @@ async function readPid(pidPath) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
async function readMeta(metaPath) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(await readFile(metaPath, 'utf-8'))
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code === 'ENOENT') return null
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
function isRunning(pid, killFn = process.kill) {
|
|
35
45
|
if (!pid) return false
|
|
36
46
|
try {
|
|
@@ -46,13 +56,24 @@ export async function startBackgroundDaemon({
|
|
|
46
56
|
homeDir = homedir(),
|
|
47
57
|
nodePath = process.execPath,
|
|
48
58
|
scriptPath,
|
|
59
|
+
version = '',
|
|
49
60
|
spawnFn = spawn,
|
|
61
|
+
killFn = process.kill,
|
|
50
62
|
} = {}) {
|
|
51
63
|
const paths = backgroundPaths(homeDir)
|
|
52
64
|
await mkdir(paths.dir, { recursive: true })
|
|
53
65
|
const existingPid = await readPid(paths.pidPath)
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
let restarted = false
|
|
67
|
+
let previousPid = null
|
|
68
|
+
if (isRunning(existingPid, killFn)) {
|
|
69
|
+
const meta = await readMeta(paths.metaPath)
|
|
70
|
+
if (meta?.version === version && meta?.scriptPath === scriptPath) {
|
|
71
|
+
return { alreadyRunning: true, pid: existingPid, logPath: paths.logPath }
|
|
72
|
+
}
|
|
73
|
+
killFn(existingPid, 'SIGTERM')
|
|
74
|
+
previousPid = existingPid
|
|
75
|
+
restarted = true
|
|
76
|
+
await rm(paths.pidPath, { force: true })
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
const log = await open(paths.logPath, constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY, 0o600)
|
|
@@ -67,8 +88,13 @@ export async function startBackgroundDaemon({
|
|
|
67
88
|
})
|
|
68
89
|
if (!child?.pid) throw new Error('failed to start daemon')
|
|
69
90
|
await writeFile(paths.pidPath, String(child.pid))
|
|
91
|
+
await writeFile(paths.metaPath, JSON.stringify({
|
|
92
|
+
version,
|
|
93
|
+
scriptPath,
|
|
94
|
+
startedAt: new Date().toISOString(),
|
|
95
|
+
}))
|
|
70
96
|
child.unref?.()
|
|
71
|
-
return { pid: child.pid, logPath: paths.logPath }
|
|
97
|
+
return { pid: child.pid, logPath: paths.logPath, restarted, previousPid }
|
|
72
98
|
} finally {
|
|
73
99
|
await log.close()
|
|
74
100
|
}
|
package/src/schema.js
CHANGED
|
@@ -19,6 +19,8 @@ function fail(error) {
|
|
|
19
19
|
export function validateTelemetryBatch(input) {
|
|
20
20
|
if (!isObject(input)) return fail('batch must be an object')
|
|
21
21
|
if (input.schema_version !== TELEMETRY_SCHEMA_VERSION) return fail(`schema_version must be ${TELEMETRY_SCHEMA_VERSION}`)
|
|
22
|
+
const agentId = input.agent_id ?? input.agentId
|
|
23
|
+
if (!isNonEmptyString(agentId)) return fail('agent_id is required')
|
|
22
24
|
for (const field of ['workspace', 'agent_key', 'node_id']) {
|
|
23
25
|
if (!isNonEmptyString(input[field])) return fail(`${field} is required`)
|
|
24
26
|
}
|
|
@@ -43,13 +45,14 @@ export function validateTelemetryBatch(input) {
|
|
|
43
45
|
if (typeof value !== 'number' || !Number.isFinite(value)) return fail(`observations[${i}].metrics.${key} must be a finite number`)
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
return { ok: true, batch: input }
|
|
48
|
+
return { ok: true, batch: { ...input, agent_id: agentId.trim() } }
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export function buildTelemetryBatch({ workspace, agent_key, node_id, artifacts = [], observations = [] }) {
|
|
51
|
+
export function buildTelemetryBatch({ workspace, agent_id, agent_key, node_id, artifacts = [], observations = [] }) {
|
|
50
52
|
return {
|
|
51
53
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
52
54
|
workspace,
|
|
55
|
+
agent_id,
|
|
53
56
|
agent_key,
|
|
54
57
|
node_id,
|
|
55
58
|
sent_at: new Date().toISOString(),
|