@11agents/cli 0.1.2 → 0.1.4
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 +24 -1
- package/bin/11agents.js +29 -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 +1 -1
- package/src/commands/data-collection.js +95 -0
- package/src/commands/database.js +60 -0
- package/src/commands/knowledge.js +213 -0
- package/src/commands/push.js +1 -0
- package/src/commands/runtime.js +419 -3
- package/src/mcp.js +100 -0
- package/src/schema.js +5 -2
package/README.md
CHANGED
|
@@ -60,10 +60,21 @@ Background mode writes its pid to `~/.11agents/daemon.pid` and logs to `~/.11age
|
|
|
60
60
|
Useful daemon options:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
11agents daemon start --heartbeat-interval 15 --scan-interval 60 --task-interval 15
|
|
63
|
+
11agents daemon start --heartbeat-interval 15 --scan-interval 60 --task-interval 15 --project-refresh-interval 1800
|
|
64
64
|
11agents daemon start --handler ./worker.js
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
On startup, and every 30 minutes after that, the daemon syncs project metadata and prepares local project headquarters:
|
|
68
|
+
|
|
69
|
+
- Project headquarters: `~/.11agents/<project>/`
|
|
70
|
+
- Project knowledge base: `~/.11agents/<project>/knowledge_base/`
|
|
71
|
+
- Agent QMD memory: `~/.11agents/<project>/agents/<agent>/memory/`
|
|
72
|
+
- Agent-local skills: `~/.11agents/<project>/agents/<agent>/skills/`
|
|
73
|
+
- Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
|
|
74
|
+
- Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
67
78
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
68
79
|
|
|
69
80
|
```js
|
|
@@ -76,6 +87,18 @@ export async function handleRuntimeTask(task) {
|
|
|
76
87
|
}
|
|
77
88
|
```
|
|
78
89
|
|
|
90
|
+
## MCP Project Sync Server
|
|
91
|
+
|
|
92
|
+
Run the local MCP server over stdio:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
11agents mcp start
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Configure your MCP client to run that command and pass the project token when calling tools. The server exposes:
|
|
99
|
+
|
|
100
|
+
- `knowledge_sync` — pull/push the project knowledge base between cloud and `~/.11agents/<project>/knowledge_base/`.
|
|
101
|
+
|
|
79
102
|
## Telemetry Compatibility
|
|
80
103
|
|
|
81
104
|
The package still includes the original `gtm-swarm` binary for swarm telemetry compatibility.
|
package/bin/11agents.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
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
11
|
import { CLI_VERSION, printStartupInfo } from '../src/info.js'
|
|
12
|
+
import { startMcpServer } from '../src/mcp.js'
|
|
11
13
|
import { validateTelemetryBatch } from '../src/schema.js'
|
|
12
14
|
|
|
13
15
|
function usage() {
|
|
@@ -17,14 +19,17 @@ Usage:
|
|
|
17
19
|
11agents help
|
|
18
20
|
11agents runtime scan
|
|
19
21
|
11agents runtime register [--server <url>] [--token <token>] [--machine <key>]
|
|
20
|
-
11agents daemon start [--server <url>] [--token <token>] [--machine <key>] [--task-interval <seconds>] [--background]
|
|
22
|
+
11agents daemon start [--server <url>] [--token <token>] [--machine <key>] [--task-interval <seconds>] [--project-refresh-interval <seconds>] [--background]
|
|
21
23
|
11agents daemon status
|
|
22
24
|
11agents daemon stop
|
|
23
25
|
11agents daemon start --handler ./worker.js # optional custom worker override
|
|
26
|
+
11agents mcp start
|
|
24
27
|
11agents validate <file>
|
|
25
28
|
11agents push batch <file>
|
|
26
29
|
11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
|
|
27
30
|
11agents push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123
|
|
31
|
+
11agents knowledge sync --project <slug> [--mode pull|push]
|
|
32
|
+
11agents knowledge status --project <slug>
|
|
28
33
|
11agents node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
|
|
29
34
|
|
|
30
35
|
Environment:
|
|
@@ -32,7 +37,14 @@ Environment:
|
|
|
32
37
|
GTM_SWARM_SERVER compatibility fallback for server URL
|
|
33
38
|
GTM_WRITES_TOKEN control-plane token for runtime registration
|
|
34
39
|
ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
|
|
35
|
-
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
40
|
+
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
41
|
+
|
|
42
|
+
Runtime task workspace:
|
|
43
|
+
Daemon project headquarters live under ~/.11agents/<project>/.
|
|
44
|
+
Knowledge lives under knowledge_base/. Agent QMD memory lives under
|
|
45
|
+
agents/<agent>/memory/. Agent-local skills live under agents/<agent>/skills/.
|
|
46
|
+
Built-in Codex tasks run from ~/.11agents/<project>/ by default. Temporary
|
|
47
|
+
writes belong under ./tmp/<taskId>/ and are cleaned after the task finishes.`)
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
async function main() {
|
|
@@ -97,6 +109,11 @@ async function main() {
|
|
|
97
109
|
return
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
if (command === 'mcp' && (!subcommand || subcommand === 'start')) {
|
|
113
|
+
await startMcpServer()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
if (command === 'validate') {
|
|
101
118
|
const file = subcommand
|
|
102
119
|
if (!file) throw new Error('validate requires a file')
|
|
@@ -123,6 +140,16 @@ async function main() {
|
|
|
123
140
|
return
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
if (command === 'knowledge' && subcommand === 'sync') {
|
|
144
|
+
await syncKnowledge(flags)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (command === 'knowledge' && subcommand === 'status') {
|
|
149
|
+
await knowledgeStatus(flags)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
if (command === 'node' && subcommand === 'run') {
|
|
127
154
|
await runNode(flags)
|
|
128
155
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { requestJson } from '../client.js'
|
|
5
|
+
|
|
6
|
+
function slugify(value) {
|
|
7
|
+
return String(value || '')
|
|
8
|
+
.trim()
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-|-$/g, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function projectDir(project, { homeDir = os.homedir() } = {}) {
|
|
15
|
+
return path.join(homeDir, '.11agents', slugify(project))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatsDir(project, options) {
|
|
19
|
+
return path.join(projectDir(project, options), 'data-formats')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function schemaPath(project, dataset, options) {
|
|
23
|
+
return path.join(formatsDir(project, options), `${slugify(dataset)}.json`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateValue(value, schema, label) {
|
|
27
|
+
const type = schema?.type
|
|
28
|
+
if (!type) return
|
|
29
|
+
if (type === 'number' && typeof value !== 'number') throw new Error(`${label} must be number`)
|
|
30
|
+
if (type === 'integer' && (!Number.isInteger(value))) throw new Error(`${label} must be integer`)
|
|
31
|
+
if (type === 'string' && typeof value !== 'string') throw new Error(`${label} must be string`)
|
|
32
|
+
if (type === 'boolean' && typeof value !== 'boolean') throw new Error(`${label} must be boolean`)
|
|
33
|
+
if (type === 'array' && !Array.isArray(value)) throw new Error(`${label} must be array`)
|
|
34
|
+
if (type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) throw new Error(`${label} must be object`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateRowsAgainstSchema(rows, schema) {
|
|
38
|
+
if (!Array.isArray(rows)) throw new Error('rows must be an array')
|
|
39
|
+
const required = Array.isArray(schema?.required) ? schema.required : []
|
|
40
|
+
const properties = schema?.properties && typeof schema.properties === 'object' ? schema.properties : {}
|
|
41
|
+
rows.forEach((row, index) => {
|
|
42
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error(`rows[${index}] must be object`)
|
|
43
|
+
for (const key of required) {
|
|
44
|
+
if (row[key] === undefined || row[key] === null || row[key] === '') throw new Error(`rows[${index}].${key} is required`)
|
|
45
|
+
}
|
|
46
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
47
|
+
if (row[key] !== undefined && row[key] !== null) validateValue(row[key], propertySchema, `rows[${index}].${key}`)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function defineDataFormat({ project, dataset, schema, token = '', server = '' } = {}, deps = {}) {
|
|
53
|
+
if (!project) throw new Error('project is required')
|
|
54
|
+
if (!dataset) throw new Error('dataset is required')
|
|
55
|
+
if (!schema || typeof schema !== 'object' || schema.type !== 'object') throw new Error('schema must be a JSON object schema')
|
|
56
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
57
|
+
const request = deps.requestJson || requestJson
|
|
58
|
+
const result = await request(`/api/projects/${slugify(project)}/data/formats`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: { dataset: slugify(dataset), schema },
|
|
61
|
+
config: { token, server: server || 'https://app.11agents.ai' },
|
|
62
|
+
})
|
|
63
|
+
await mkdir(formatsDir(project, { homeDir }), { recursive: true })
|
|
64
|
+
await writeFile(schemaPath(project, dataset, { homeDir }), JSON.stringify({
|
|
65
|
+
dataset: slugify(dataset),
|
|
66
|
+
schema,
|
|
67
|
+
schema_revision: Number(result?.schema_revision || 1),
|
|
68
|
+
defined_at: new Date().toISOString(),
|
|
69
|
+
}, null, 2))
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readLockedFormat(project, dataset, { homeDir = os.homedir() } = {}) {
|
|
74
|
+
return JSON.parse(await readFile(schemaPath(project, dataset, { homeDir }), 'utf8'))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function pushDataRows({ project, dataset, date, rows, token = '', server = '' } = {}, deps = {}) {
|
|
78
|
+
if (!project) throw new Error('project is required')
|
|
79
|
+
if (!dataset) throw new Error('dataset is required')
|
|
80
|
+
if (!date) throw new Error('date is required')
|
|
81
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
82
|
+
const locked = await readLockedFormat(project, dataset, { homeDir })
|
|
83
|
+
validateRowsAgainstSchema(rows, locked.schema)
|
|
84
|
+
const request = deps.requestJson || requestJson
|
|
85
|
+
return request(`/api/projects/${slugify(project)}/data/push`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: {
|
|
88
|
+
dataset: slugify(dataset),
|
|
89
|
+
date,
|
|
90
|
+
schema_revision: locked.schema_revision,
|
|
91
|
+
rows,
|
|
92
|
+
},
|
|
93
|
+
config: { token, server: server || 'https://app.11agents.ai' },
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { requestJson } from '../client.js'
|
|
5
|
+
|
|
6
|
+
function slugify(value) {
|
|
7
|
+
return String(value || 'project')
|
|
8
|
+
.trim()
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-|-$/g, '') || 'project'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildDatabaseDir(project, { homeDir = os.homedir() } = {}) {
|
|
15
|
+
return path.join(homeDir, '.11agents', slugify(project), 'database')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readState(project, { homeDir = os.homedir() } = {}) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(path.join(buildDatabaseDir(project, { homeDir }), 'sync-state.json'), 'utf8'))
|
|
21
|
+
} catch {
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function syncDatabase({ project, mode = 'pull', token = '', server = '' } = {}, deps = {}) {
|
|
27
|
+
if (!project) throw new Error('database sync requires project')
|
|
28
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
29
|
+
const request = deps.requestJson || requestJson
|
|
30
|
+
const dir = buildDatabaseDir(project, { homeDir })
|
|
31
|
+
const state = await readState(project, { homeDir })
|
|
32
|
+
const body = {
|
|
33
|
+
mode,
|
|
34
|
+
local_revision: Number(state.cloud_revision || 0),
|
|
35
|
+
}
|
|
36
|
+
if (mode === 'push') {
|
|
37
|
+
body.snapshot = JSON.parse(await readFile(path.join(dir, 'snapshot.json'), 'utf8'))
|
|
38
|
+
}
|
|
39
|
+
const result = await request(`/api/projects/${slugify(project)}/database/sync`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body,
|
|
42
|
+
config: { token, server: server || 'https://app.11agents.ai' },
|
|
43
|
+
})
|
|
44
|
+
await mkdir(dir, { recursive: true })
|
|
45
|
+
if (result?.snapshot !== undefined) {
|
|
46
|
+
await writeFile(path.join(dir, 'snapshot.json'), JSON.stringify(result.snapshot, null, 2))
|
|
47
|
+
}
|
|
48
|
+
await writeFile(path.join(dir, 'sync-state.json'), JSON.stringify({
|
|
49
|
+
cloud_revision: Number(result?.cloud_revision || 0),
|
|
50
|
+
synced_at: result?.synced_at || new Date().toISOString(),
|
|
51
|
+
}, null, 2))
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
project: slugify(project),
|
|
55
|
+
mode,
|
|
56
|
+
path: dir,
|
|
57
|
+
cloud_revision: Number(result?.cloud_revision || 0),
|
|
58
|
+
synced_at: result?.synced_at || '',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, 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
|
+
const MARKDOWN_NODE_KINDS = new Set(['document', 'fact', 'decision', 'insight', 'resource', 'task-context'])
|
|
22
|
+
|
|
23
|
+
function normalizeSnapshot(snapshot = {}) {
|
|
24
|
+
return {
|
|
25
|
+
cloud_revision: Number(snapshot.cloud_revision || snapshot.knowledge_base?.cloud_revision || 0),
|
|
26
|
+
local_revision: String(snapshot.local_revision || ''),
|
|
27
|
+
nodes: Array.isArray(snapshot.nodes) ? snapshot.nodes.map((node, index) => {
|
|
28
|
+
const id = slugify(node.id || node.title || `node-${index + 1}`)
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
kind: String(node.kind || 'document'),
|
|
32
|
+
title: String(node.title || id),
|
|
33
|
+
body: String(node.body || ''),
|
|
34
|
+
local_path: safeLocalPath(node.local_path || node.localPath, id),
|
|
35
|
+
metadata: node.metadata && typeof node.metadata === 'object' ? node.metadata : {},
|
|
36
|
+
}
|
|
37
|
+
}).sort((a, b) => a.id.localeCompare(b.id)) : [],
|
|
38
|
+
edges: Array.isArray(snapshot.edges) ? snapshot.edges.map(edge => ({
|
|
39
|
+
from: slugify(edge.from || edge.from_node_id),
|
|
40
|
+
to: slugify(edge.to || edge.to_node_id),
|
|
41
|
+
relation: slugify(edge.relation || 'related'),
|
|
42
|
+
metadata: edge.metadata && typeof edge.metadata === 'object' ? edge.metadata : {},
|
|
43
|
+
})).filter(edge => edge.from && edge.to).sort((a, b) => (
|
|
44
|
+
`${a.from}:${a.to}:${a.relation}`.localeCompare(`${b.from}:${b.to}:${b.relation}`)
|
|
45
|
+
)) : [],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function markdownForNode(node) {
|
|
50
|
+
return [
|
|
51
|
+
`# ${node.title}`,
|
|
52
|
+
'',
|
|
53
|
+
`kind: ${node.kind}`,
|
|
54
|
+
`id: ${node.id}`,
|
|
55
|
+
'',
|
|
56
|
+
node.body || '',
|
|
57
|
+
'',
|
|
58
|
+
].join('\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bodyFromMarkdownNode(markdown, fallback = '') {
|
|
62
|
+
const lines = String(markdown || '').split('\n')
|
|
63
|
+
let index = 0
|
|
64
|
+
if (lines[index]?.startsWith('# ')) index += 1
|
|
65
|
+
if (lines[index] === '') index += 1
|
|
66
|
+
if (/^kind:\s*/.test(lines[index] || '')) index += 1
|
|
67
|
+
if (/^id:\s*/.test(lines[index] || '')) index += 1
|
|
68
|
+
if (lines[index] === '') index += 1
|
|
69
|
+
const body = lines.slice(index).join('\n').replace(/\n+$/, '')
|
|
70
|
+
return body || fallback
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isKnowledgeTextFile(name) {
|
|
74
|
+
return name.endsWith('.md') || name === 'llms.txt' || name === 'llms-full.txt'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function listKnowledgeTextFiles(baseDir, currentDir = baseDir) {
|
|
78
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => [])
|
|
79
|
+
const files = []
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.name.startsWith('.')) continue
|
|
82
|
+
const fullPath = path.join(currentDir, entry.name)
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
files.push(...await listKnowledgeTextFiles(baseDir, fullPath))
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (!entry.isFile() || !isKnowledgeTextFile(entry.name)) continue
|
|
88
|
+
files.push(path.relative(baseDir, fullPath).split(path.sep).join('/'))
|
|
89
|
+
}
|
|
90
|
+
return files.sort((a, b) => a.localeCompare(b))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function titleFromLocalPath(localPath) {
|
|
94
|
+
const basename = path.basename(localPath).replace(/\.(md|txt)$/i, '')
|
|
95
|
+
if (basename.toLowerCase() === 'readme') return 'README'
|
|
96
|
+
if (basename.toLowerCase() === 'claude') return 'Agent Instructions'
|
|
97
|
+
if (basename.toLowerCase() === 'index') return 'Knowledge Map'
|
|
98
|
+
return basename
|
|
99
|
+
.split(/[-_]+/)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map(part => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
102
|
+
.join(' ') || localPath
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function kindFromLocalPath(localPath) {
|
|
106
|
+
const folder = localPath.split('/').filter(Boolean)[0]
|
|
107
|
+
if (MARKDOWN_NODE_KINDS.has(folder)) return folder
|
|
108
|
+
if (folder === 'resources') return 'resource'
|
|
109
|
+
if (folder === 'decisions') return 'decision'
|
|
110
|
+
if (folder === 'insights') return 'insight'
|
|
111
|
+
if (folder === 'tasks') return 'task-context'
|
|
112
|
+
return 'document'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildKnowledgeBaseDir(project, { homeDir = os.homedir() } = {}) {
|
|
116
|
+
return path.join(homeDir, '.11agents', slugify(project), 'knowledge_base')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function writeKnowledgeSnapshot(project, snapshot, { homeDir = os.homedir() } = {}) {
|
|
120
|
+
const normalized = normalizeSnapshot(snapshot)
|
|
121
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
122
|
+
const localRevision = normalized.local_revision || `${Date.now()}`
|
|
123
|
+
const index = {
|
|
124
|
+
cloud_revision: normalized.cloud_revision,
|
|
125
|
+
local_revision: localRevision,
|
|
126
|
+
nodes: normalized.nodes,
|
|
127
|
+
edges: normalized.edges,
|
|
128
|
+
}
|
|
129
|
+
await mkdir(path.join(baseDir, 'nodes'), { recursive: true })
|
|
130
|
+
await writeFile(path.join(baseDir, 'index.json'), JSON.stringify(index, null, 2))
|
|
131
|
+
await writeFile(path.join(baseDir, 'sync-state.json'), JSON.stringify({
|
|
132
|
+
cloud_revision: normalized.cloud_revision,
|
|
133
|
+
local_revision: localRevision,
|
|
134
|
+
synced_at: new Date().toISOString(),
|
|
135
|
+
}, null, 2))
|
|
136
|
+
for (const node of normalized.nodes) {
|
|
137
|
+
const target = path.join(baseDir, node.local_path)
|
|
138
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
139
|
+
await writeFile(target, markdownForNode(node))
|
|
140
|
+
}
|
|
141
|
+
return index
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function readKnowledgeSnapshot(project, { homeDir = os.homedir() } = {}) {
|
|
145
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
146
|
+
const raw = await readFile(path.join(baseDir, 'index.json'), 'utf-8').catch(() => '{}')
|
|
147
|
+
const snapshot = normalizeSnapshot(JSON.parse(raw))
|
|
148
|
+
const nodesByPath = new Map(snapshot.nodes.map(node => [node.local_path, node]))
|
|
149
|
+
for (const localPath of await listKnowledgeTextFiles(baseDir)) {
|
|
150
|
+
if (nodesByPath.has(localPath)) continue
|
|
151
|
+
nodesByPath.set(localPath, {
|
|
152
|
+
id: slugify(localPath.replace(/\.(md|txt)$/i, '')),
|
|
153
|
+
kind: kindFromLocalPath(localPath),
|
|
154
|
+
title: titleFromLocalPath(localPath),
|
|
155
|
+
body: '',
|
|
156
|
+
local_path: localPath,
|
|
157
|
+
metadata: { source: 'local-markdown' },
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
const nodes = await Promise.all([...nodesByPath.values()].map(async node => {
|
|
161
|
+
const markdown = await readFile(path.join(baseDir, node.local_path), 'utf-8').catch(() => null)
|
|
162
|
+
if (markdown === null) return node
|
|
163
|
+
return { ...node, body: bodyFromMarkdownNode(markdown, node.body) }
|
|
164
|
+
}))
|
|
165
|
+
return { ...snapshot, nodes: nodes.sort((a, b) => a.id.localeCompare(b.id)) }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function syncKnowledge(flags = {}, deps = {}) {
|
|
169
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
170
|
+
if (!project) throw new Error('knowledge sync requires --project <slug>')
|
|
171
|
+
const mode = flag(flags, 'mode', flags.push ? 'push' : 'pull')
|
|
172
|
+
const request = deps.requestJson || requestJson
|
|
173
|
+
const log = deps.log || (value => console.log(value))
|
|
174
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
175
|
+
const localSnapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
176
|
+
const body = mode === 'push'
|
|
177
|
+
? { ...localSnapshot, mode: 'push', actor: 'cli' }
|
|
178
|
+
: { mode: 'pull', local_revision: localSnapshot.local_revision, cloud_revision: localSnapshot.cloud_revision, actor: 'cli' }
|
|
179
|
+
const result = await request(`/api/projects/${project}/knowledge/sync`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
body,
|
|
182
|
+
})
|
|
183
|
+
const written = await writeKnowledgeSnapshot(project, result, { homeDir })
|
|
184
|
+
const payload = {
|
|
185
|
+
ok: true,
|
|
186
|
+
project,
|
|
187
|
+
mode,
|
|
188
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
189
|
+
cloud_revision: written.cloud_revision,
|
|
190
|
+
local_revision: written.local_revision,
|
|
191
|
+
nodes: written.nodes.length,
|
|
192
|
+
edges: written.edges.length,
|
|
193
|
+
}
|
|
194
|
+
log(JSON.stringify(payload, null, 2))
|
|
195
|
+
return { ...written, path: payload.path }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function knowledgeStatus(flags = {}, deps = {}) {
|
|
199
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
200
|
+
if (!project) throw new Error('knowledge status requires --project <slug>')
|
|
201
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
202
|
+
const snapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
203
|
+
const payload = {
|
|
204
|
+
project,
|
|
205
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
206
|
+
cloud_revision: snapshot.cloud_revision,
|
|
207
|
+
local_revision: snapshot.local_revision,
|
|
208
|
+
nodes: snapshot.nodes.length,
|
|
209
|
+
edges: snapshot.edges.length,
|
|
210
|
+
}
|
|
211
|
+
;(deps.log || (value => console.log(value)))(JSON.stringify(payload, null, 2))
|
|
212
|
+
return payload
|
|
213
|
+
}
|
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,11 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { appendFile, 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'
|
|
14
|
+
import { handleMcpRequest } from '../mcp.js'
|
|
9
15
|
|
|
10
16
|
const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
|
|
11
17
|
|
|
@@ -55,10 +61,13 @@ function runtimeDeps(overrides = {}) {
|
|
|
55
61
|
buildRuntimeScan: overrides.buildRuntimeScan || buildRuntimeScan,
|
|
56
62
|
importHandler: overrides.importHandler || (async handlerPath => import(pathToFileURL(resolve(handlerPath)).href)),
|
|
57
63
|
log: overrides.log || (value => console.log(value)),
|
|
64
|
+
homeDir: overrides.homeDir || os.homedir(),
|
|
58
65
|
runCodex: overrides.runCodex || runCodex,
|
|
59
66
|
runProcess: overrides.runProcess || runProcess,
|
|
60
67
|
requestJson: overrides.requestJson || requestJson,
|
|
61
68
|
sleep: overrides.sleep || sleep,
|
|
69
|
+
syncKnowledge: overrides.syncKnowledge || syncKnowledge,
|
|
70
|
+
mcpKnowledgeSync: overrides.mcpKnowledgeSync || mcpKnowledgeSync,
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
|
@@ -140,6 +149,37 @@ export async function heartbeatRuntime(flags = {}, deps = {}) {
|
|
|
140
149
|
return result
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
export async function syncRuntimeProjectMetadata(flags = {}, deps = {}) {
|
|
153
|
+
const { log, requestJson: request, homeDir } = runtimeDeps(deps)
|
|
154
|
+
const config = configFromFlags(flags)
|
|
155
|
+
if (!config.token) throw new Error('GTM_WRITES_TOKEN or --token is required')
|
|
156
|
+
|
|
157
|
+
const result = await request('/api/runtime/projects', {
|
|
158
|
+
method: 'GET',
|
|
159
|
+
config,
|
|
160
|
+
})
|
|
161
|
+
const materialized = await materializeRuntimeProjectDirectories(result?.projects || [], { homeDir })
|
|
162
|
+
log(JSON.stringify({
|
|
163
|
+
ok: true,
|
|
164
|
+
synced: 'runtime projects',
|
|
165
|
+
base_dir: materialized.base_dir,
|
|
166
|
+
projects: materialized.projects,
|
|
167
|
+
}, null, 2))
|
|
168
|
+
return { ...result, ...materialized }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function syncRuntimeProjectMetadataBestEffort(flags, deps) {
|
|
172
|
+
try {
|
|
173
|
+
return await syncRuntimeProjectMetadata(flags, deps)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
deps.log(JSON.stringify({
|
|
176
|
+
warning: 'runtime project metadata sync failed',
|
|
177
|
+
error: errorMessage(error),
|
|
178
|
+
}, null, 2))
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
function normalizeTaskCompletion(task, completion) {
|
|
144
184
|
const result = completion && typeof completion === 'object' ? completion : {}
|
|
145
185
|
return {
|
|
@@ -165,10 +205,315 @@ function compactJson(value) {
|
|
|
165
205
|
return JSON.stringify(value ?? null, null, 2)
|
|
166
206
|
}
|
|
167
207
|
|
|
208
|
+
async function mcpKnowledgeSync(flags = {}, deps = {}) {
|
|
209
|
+
const result = await handleMcpRequest({
|
|
210
|
+
method: 'tools/call',
|
|
211
|
+
params: {
|
|
212
|
+
name: 'knowledge_sync',
|
|
213
|
+
arguments: {
|
|
214
|
+
project: flags.project,
|
|
215
|
+
mode: flags.mode,
|
|
216
|
+
server: flags.server,
|
|
217
|
+
token: flags.token,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}, deps)
|
|
221
|
+
return JSON.parse(result.content?.[0]?.text || '{}')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nonAlphaNum = /[^a-z0-9]+/g
|
|
225
|
+
|
|
226
|
+
function slugify(value, fallback = 'project') {
|
|
227
|
+
return String(value || fallback)
|
|
228
|
+
.trim()
|
|
229
|
+
.toLowerCase()
|
|
230
|
+
.replace(nonAlphaNum, '-')
|
|
231
|
+
.replace(/^-|-$/g, '') || fallback
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sanitizeTaskId(value) {
|
|
235
|
+
return slugify(value, 'task')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function buildRuntimeProjectDir(project, { homeDir = os.homedir() } = {}) {
|
|
239
|
+
return path.join(homeDir, '.11agents', slugify(project))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildRuntimeKnowledgeDir(project, { homeDir = os.homedir() } = {}) {
|
|
243
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'knowledge_base')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function buildRuntimeAgentMemoryDir(project, agent, { homeDir = os.homedir() } = {}) {
|
|
247
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'agents', slugify(agent, 'agent'), 'memory')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function buildRuntimeAgentSkillsDir(project, agent, { homeDir = os.homedir() } = {}) {
|
|
251
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'agents', slugify(agent, 'agent'), 'skills')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function projectSlugForTask(task, flags = {}) {
|
|
255
|
+
return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function projectDirForTask(task, flags = {}, deps) {
|
|
259
|
+
return buildRuntimeProjectDir(projectSlugForTask(task, flags), { homeDir: deps.homeDir })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function writeFileIfMissing(filePath, content) {
|
|
263
|
+
try {
|
|
264
|
+
await writeFile(filePath, content, { flag: 'wx' })
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error?.code !== 'EEXIST') throw error
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function defaultMemoryQmd(project, agent) {
|
|
271
|
+
return [
|
|
272
|
+
`# ${agent.name || agent.id || 'Agent'} Memory`,
|
|
273
|
+
'',
|
|
274
|
+
`project: ${project.slug || project.name || 'project'}`,
|
|
275
|
+
`agent: ${agent.name || agent.id || 'agent'}`,
|
|
276
|
+
'',
|
|
277
|
+
].join('\n')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function safeQmdName(value, fallback) {
|
|
281
|
+
const name = slugify(String(value || '').replace(/\.qmd$/i, ''), fallback)
|
|
282
|
+
return `${name}.qmd`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function materializeRuntimeProjectDirectories(projects = [], { homeDir = os.homedir() } = {}) {
|
|
286
|
+
const baseDir = path.join(homeDir, '.11agents')
|
|
287
|
+
await mkdir(baseDir, { recursive: true })
|
|
288
|
+
const written = []
|
|
289
|
+
for (const project of Array.isArray(projects) ? projects : []) {
|
|
290
|
+
const projectSlug = slugify(project?.slug || project?.name, 'project')
|
|
291
|
+
const projectDir = buildRuntimeProjectDir(projectSlug, { homeDir })
|
|
292
|
+
await mkdir(buildRuntimeKnowledgeDir(projectSlug, { homeDir }), { recursive: true })
|
|
293
|
+
written.push(projectDir)
|
|
294
|
+
for (const agent of Array.isArray(project?.agents) ? project.agents : []) {
|
|
295
|
+
const agentName = agent?.name || agent?.id || 'agent'
|
|
296
|
+
const memoryDir = buildRuntimeAgentMemoryDir(projectSlug, agentName, { homeDir })
|
|
297
|
+
const skillsDir = buildRuntimeAgentSkillsDir(projectSlug, agentName, { homeDir })
|
|
298
|
+
await mkdir(memoryDir, { recursive: true })
|
|
299
|
+
await mkdir(skillsDir, { recursive: true })
|
|
300
|
+
await writeFileIfMissing(path.join(memoryDir, 'index.qmd'), defaultMemoryQmd({ ...project, slug: projectSlug }, agent))
|
|
301
|
+
for (const memory of Array.isArray(agent?.memory) ? agent.memory : []) {
|
|
302
|
+
await writeFileIfMissing(path.join(memoryDir, safeQmdName(memory, 'memory')), '')
|
|
303
|
+
}
|
|
304
|
+
for (const skill of Array.isArray(agent?.skills) ? agent.skills : []) {
|
|
305
|
+
await mkdir(path.join(skillsDir, slugify(skill?.name || skill, 'skill')), { recursive: true })
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { base_dir: baseDir, projects: written.length }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function knowledgeDeepOrganizeSpec(task) {
|
|
313
|
+
if (task.queue_event?.trigger_type !== 'knowledge_deep_organize') return null
|
|
314
|
+
const raw = String(task.queue_event?.trigger_summary || '')
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(raw)
|
|
317
|
+
return {
|
|
318
|
+
source_url: String(parsed.source_url || parsed.url || ''),
|
|
319
|
+
source_type: String(parsed.source_type || '').toLowerCase(),
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
return { source_url: raw, source_type: raw.includes('github.com') ? 'github' : 'url' }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function readJsonFile(filePath, fallback = {}) {
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error?.code === 'ENOENT') return fallback
|
|
331
|
+
return fallback
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function stableHash(value) {
|
|
336
|
+
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function assertSafeRelativePath(filePath) {
|
|
340
|
+
const clean = path.normalize(String(filePath || ''))
|
|
341
|
+
if (!clean || path.isAbsolute(clean) || clean === '..' || clean.startsWith(`..${path.sep}`)) {
|
|
342
|
+
throw new Error(`invalid skill file path: ${filePath}`)
|
|
343
|
+
}
|
|
344
|
+
return clean
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function decodeFileContent(file) {
|
|
348
|
+
if (file?.encoding === 'base64') return Buffer.from(String(file.content || ''), 'base64')
|
|
349
|
+
return Buffer.from(String(file?.content || ''), 'utf8')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function agentNameForTask(task) {
|
|
353
|
+
return task.agent?.name || task.agent?.id || task.agent_id || task.assignee_id || 'agent'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function normalizeSkillBundle(skill = {}) {
|
|
357
|
+
return {
|
|
358
|
+
id: String(skill.id || ''),
|
|
359
|
+
name: String(skill.name || 'skill'),
|
|
360
|
+
content: String(skill.content || ''),
|
|
361
|
+
files: Array.isArray(skill.files)
|
|
362
|
+
? skill.files.map(file => ({
|
|
363
|
+
path: String(file.path || ''),
|
|
364
|
+
encoding: file.encoding === 'base64' ? 'base64' : 'utf8',
|
|
365
|
+
content: String(file.content || ''),
|
|
366
|
+
})).sort((a, b) => a.path.localeCompare(b.path))
|
|
367
|
+
: [],
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
372
|
+
const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
|
|
373
|
+
if (!skills.length) return { changed: false, count: 0 }
|
|
374
|
+
|
|
375
|
+
const skillsDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'skills')
|
|
376
|
+
const statePath = path.join(skillsDir, 'skills-state.json')
|
|
377
|
+
const nextHash = stableHash(skills)
|
|
378
|
+
const current = await readJsonFile(statePath, {})
|
|
379
|
+
if (current.hash === nextHash) return { changed: false, count: skills.length }
|
|
380
|
+
|
|
381
|
+
await mkdir(skillsDir, { recursive: true })
|
|
382
|
+
for (const skill of skills) {
|
|
383
|
+
const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
|
|
384
|
+
await mkdir(skillDir, { recursive: true })
|
|
385
|
+
await writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf8')
|
|
386
|
+
for (const file of skill.files) {
|
|
387
|
+
const relativePath = assertSafeRelativePath(file.path)
|
|
388
|
+
const target = path.join(skillDir, relativePath)
|
|
389
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
390
|
+
await writeFile(target, decodeFileContent(file))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await writeFile(statePath, JSON.stringify({
|
|
394
|
+
hash: nextHash,
|
|
395
|
+
count: skills.length,
|
|
396
|
+
skills_dir: skillsDir,
|
|
397
|
+
synced_at: new Date().toISOString(),
|
|
398
|
+
}, null, 2))
|
|
399
|
+
return { changed: true, count: skills.length, skills_dir: skillsDir }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function appendTaskMemoryDelta({ task, completion, workdir }) {
|
|
403
|
+
const memoryDelta = String(completion?.memory_delta || completion?.memoryDelta || '').trim()
|
|
404
|
+
if (!memoryDelta) return { written: false }
|
|
405
|
+
const memoryDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'memory')
|
|
406
|
+
await mkdir(memoryDir, { recursive: true })
|
|
407
|
+
const indexPath = path.join(memoryDir, 'index.qmd')
|
|
408
|
+
await writeFileIfMissing(indexPath, defaultMemoryQmd({ slug: projectSlugForTask(task), name: task.workspace?.name }, task.agent || {}))
|
|
409
|
+
await appendFile(indexPath, [
|
|
410
|
+
'',
|
|
411
|
+
`## ${new Date().toISOString()} ${task.id || ''}`.trim(),
|
|
412
|
+
'',
|
|
413
|
+
memoryDelta,
|
|
414
|
+
'',
|
|
415
|
+
].join('\n'))
|
|
416
|
+
return { written: true, path: indexPath }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function databaseSyncSpec(task) {
|
|
420
|
+
const spec = task.database || task.cloud_database || task.workspace?.database || null
|
|
421
|
+
if (!spec || typeof spec !== 'object') return null
|
|
422
|
+
const cloudRevision = Number(spec.cloud_revision ?? spec.cloudRevision ?? spec.revision ?? 0)
|
|
423
|
+
const syncRequired = spec.sync_required === true || spec.syncRequired === true
|
|
424
|
+
if (!syncRequired && !cloudRevision) return null
|
|
425
|
+
return { cloudRevision, syncRequired }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function syncDatabaseIfNeeded({ task, workdir, config, deps }) {
|
|
429
|
+
const spec = databaseSyncSpec(task)
|
|
430
|
+
if (!spec) return { synced: false }
|
|
431
|
+
const statePath = path.join(workdir, 'database', 'sync-state.json')
|
|
432
|
+
const current = await readJsonFile(statePath, {})
|
|
433
|
+
const localRevision = Number(current.cloud_revision || 0)
|
|
434
|
+
if (!spec.syncRequired && localRevision === spec.cloudRevision) return { synced: false, cloud_revision: localRevision }
|
|
435
|
+
|
|
436
|
+
const project = projectSlugForTask(task)
|
|
437
|
+
const result = await deps.requestJson(`/api/projects/${project}/database/sync`, {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
body: {
|
|
440
|
+
task_id: task.id,
|
|
441
|
+
local_revision: localRevision,
|
|
442
|
+
cloud_revision: spec.cloudRevision,
|
|
443
|
+
},
|
|
444
|
+
config,
|
|
445
|
+
})
|
|
446
|
+
await mkdir(path.dirname(statePath), { recursive: true })
|
|
447
|
+
if (result?.snapshot !== undefined) {
|
|
448
|
+
await writeFile(path.join(path.dirname(statePath), 'snapshot.json'), JSON.stringify(result.snapshot, null, 2))
|
|
449
|
+
}
|
|
450
|
+
await writeFile(statePath, JSON.stringify({
|
|
451
|
+
cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision),
|
|
452
|
+
synced_at: result?.synced_at || new Date().toISOString(),
|
|
453
|
+
}, null, 2))
|
|
454
|
+
return { synced: true, cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision) }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function agentEnvironment(task) {
|
|
458
|
+
const values = task.agent?.environment_variables || task.agent?.environment || task.agent?.custom_env || {}
|
|
459
|
+
const env = {}
|
|
460
|
+
if (Array.isArray(values)) {
|
|
461
|
+
for (const item of values) {
|
|
462
|
+
const key = String(item?.key || '').trim()
|
|
463
|
+
if (key) env[key] = String(item?.value || '')
|
|
464
|
+
}
|
|
465
|
+
return env
|
|
466
|
+
}
|
|
467
|
+
if (values && typeof values === 'object') {
|
|
468
|
+
for (const [key, value] of Object.entries(values)) {
|
|
469
|
+
const cleanKey = String(key || '').trim()
|
|
470
|
+
if (cleanKey) env[cleanKey] = String(value ?? '')
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return env
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function prepareRuntimeTask(task, flags, deps, config) {
|
|
477
|
+
const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
|
|
478
|
+
const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
|
|
479
|
+
await mkdir(tmpDir, { recursive: true })
|
|
480
|
+
|
|
481
|
+
const database = await syncDatabaseIfNeeded({ task, workdir, config, deps })
|
|
482
|
+
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
483
|
+
const env = {
|
|
484
|
+
...process.env,
|
|
485
|
+
...agentEnvironment(task),
|
|
486
|
+
ELEVENAGENTS_PROJECT_DIR: workdir,
|
|
487
|
+
ELEVENAGENTS_TASK_TMP: tmpDir,
|
|
488
|
+
ELEVENAGENTS_TASK_ID: String(task.id || ''),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
workdir,
|
|
493
|
+
tmp_dir: tmpDir,
|
|
494
|
+
project_slug: projectSlugForTask(task, flags),
|
|
495
|
+
readonly: true,
|
|
496
|
+
env,
|
|
497
|
+
database,
|
|
498
|
+
skills,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
168
502
|
function buildCodexPrompt(task) {
|
|
503
|
+
const deepOrganize = knowledgeDeepOrganizeSpec(task)
|
|
169
504
|
return [
|
|
170
505
|
'You are executing an 11agents task as the assigned agent.',
|
|
171
506
|
'',
|
|
507
|
+
'Execution workspace:',
|
|
508
|
+
compactJson({
|
|
509
|
+
workdir: task.execution_context?.workdir,
|
|
510
|
+
tmp_dir: task.execution_context?.tmp_dir,
|
|
511
|
+
project_slug: task.execution_context?.project_slug,
|
|
512
|
+
knowledge_base: './knowledge_base/',
|
|
513
|
+
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.',
|
|
514
|
+
cleanup: 'Temporary files under ./tmp/<taskId>/ are removed by the CLI after the task finishes.',
|
|
515
|
+
}),
|
|
516
|
+
'',
|
|
172
517
|
'Task context:',
|
|
173
518
|
compactJson({
|
|
174
519
|
queue_event: task.queue_event,
|
|
@@ -176,6 +521,7 @@ function buildCodexPrompt(task) {
|
|
|
176
521
|
issue: task.issue,
|
|
177
522
|
trigger_summary: task.trigger_summary,
|
|
178
523
|
thread_memory: task.thread_memory,
|
|
524
|
+
project_knowledge: task.project_knowledge,
|
|
179
525
|
}),
|
|
180
526
|
'',
|
|
181
527
|
'Assigned agent context:',
|
|
@@ -192,6 +538,33 @@ function buildCodexPrompt(task) {
|
|
|
192
538
|
'',
|
|
193
539
|
'Thread comments:',
|
|
194
540
|
compactJson(task.comments || []),
|
|
541
|
+
deepOrganize ? [
|
|
542
|
+
'',
|
|
543
|
+
'Knowledge deep organization task:',
|
|
544
|
+
compactJson({
|
|
545
|
+
trigger_type: 'knowledge_deep_organize',
|
|
546
|
+
source_url: deepOrganize.source_url,
|
|
547
|
+
source_type: deepOrganize.source_type || (deepOrganize.source_url.includes('github.com') ? 'github' : 'url'),
|
|
548
|
+
output_dir: task.execution_context?.workdir ? `${task.execution_context.workdir}/knowledge_base` : './knowledge_base',
|
|
549
|
+
callback: 'After organizing files, call the 11agents MCP knowledge_sync tool with mode=push so cloud receives the finished wiki.',
|
|
550
|
+
}),
|
|
551
|
+
'',
|
|
552
|
+
'Build the knowledge base using the standard llms.txt / LLM Wiki pattern popularized for LLM-readable documentation:',
|
|
553
|
+
'- Create or update ./knowledge_base/llms.txt with one H1 project title, a blockquote summary, concise context, and H2 sections containing Markdown links to the most important files.',
|
|
554
|
+
'- Create or update ./knowledge_base/llms-full.txt as a fuller single-file context pack when useful.',
|
|
555
|
+
'- Create or update ./knowledge_base/README.md, ./knowledge_base/index.md, and ./knowledge_base/CLAUDE.md as the human entry point, node map, and agent operating guide.',
|
|
556
|
+
'- Treat the finished result as an agent skill graph like a strong hand-built knowledge base: every durable file should tell the next agent exactly when to read it and how to act on it.',
|
|
557
|
+
'- Do not stop at project-profile.md / knowledge-map.md placeholders. Create concrete markdown files with actionable content.',
|
|
558
|
+
'- For product, website, or business sources, use semantic folders such as information/, voice/, audience/, marketing/, product-design/, resources/, and decisions/. Create files like information/what-we-do/product-overview.md, voice/brand-voice.md, audience/README.md, and marketing/README.md when the source supports them.',
|
|
559
|
+
'- For GitHub sources, use semantic folders such as codebase/, architecture/, api/, operations/, examples/, resources/, and agent-playbook/. Create files like codebase/README.md, architecture/overview.md, operations/setup.md, api/README.md, and examples/README.md when the repo supports them.',
|
|
560
|
+
'- README.md should include a "Where to Start" table and a directory overview.',
|
|
561
|
+
'- index.md should include Identity, Node Map, and Execution Instructions sections with wiki-style references to important files.',
|
|
562
|
+
'- CLAUDE.md should include the exact read order and output/update rules for runtime agents.',
|
|
563
|
+
'- If source_type is github, inspect README, docs, package manifests, examples, and source tree. Prefer concise durable explanations over raw dumps.',
|
|
564
|
+
'- If source_type is url, read the page and prefer /llms.txt or /llms-full.txt from that origin when available.',
|
|
565
|
+
'- Keep file paths stable and descriptive. Do not write temporary research into the durable wiki.',
|
|
566
|
+
'- When done, use MCP to push the local knowledge_base back to cloud.',
|
|
567
|
+
].join('\n') : '',
|
|
195
568
|
'',
|
|
196
569
|
'Work in this repository and make the needed changes. When finished, respond with a concise summary for the task thread.',
|
|
197
570
|
].join('\n')
|
|
@@ -199,7 +572,7 @@ function buildCodexPrompt(task) {
|
|
|
199
572
|
|
|
200
573
|
async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
201
574
|
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
202
|
-
const workdir = flag(flags, 'codex-workdir', process.cwd())
|
|
575
|
+
const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
|
|
203
576
|
const args = [
|
|
204
577
|
'--ask-for-approval',
|
|
205
578
|
'never',
|
|
@@ -219,7 +592,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
219
592
|
|
|
220
593
|
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
221
594
|
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
222
|
-
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir })
|
|
595
|
+
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir, env: task.execution_context?.env || process.env })
|
|
223
596
|
const output = String(result.stdout || '').trim()
|
|
224
597
|
const error = String(result.stderr || '').trim()
|
|
225
598
|
if (result.code !== 0) {
|
|
@@ -289,6 +662,21 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
289
662
|
}
|
|
290
663
|
|
|
291
664
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
665
|
+
if (runtimeTask.workspace?.slug) {
|
|
666
|
+
await runWithDaemonRetry('sync knowledge base', () => (
|
|
667
|
+
deps.syncKnowledge({
|
|
668
|
+
project: runtimeTask.workspace.slug,
|
|
669
|
+
mode: 'pull',
|
|
670
|
+
server: flags.server,
|
|
671
|
+
token: flags.token,
|
|
672
|
+
}, {
|
|
673
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
674
|
+
log: () => {},
|
|
675
|
+
})
|
|
676
|
+
), deps, retryState)
|
|
677
|
+
}
|
|
678
|
+
const executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
679
|
+
runtimeTask.execution_context = executionContext
|
|
292
680
|
let completion
|
|
293
681
|
try {
|
|
294
682
|
completion = await handlerModule.handleRuntimeTask(runtimeTask)
|
|
@@ -297,6 +685,24 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
297
685
|
comment: error instanceof Error ? error.message : String(error),
|
|
298
686
|
status: 'failed',
|
|
299
687
|
}
|
|
688
|
+
} finally {
|
|
689
|
+
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
690
|
+
}
|
|
691
|
+
await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
|
|
692
|
+
|
|
693
|
+
if (runtimeTask.workspace?.slug) {
|
|
694
|
+
const syncBack = knowledgeDeepOrganizeSpec(runtimeTask) ? deps.mcpKnowledgeSync : deps.syncKnowledge
|
|
695
|
+
await runWithDaemonRetry('sync knowledge base back to cloud', () => (
|
|
696
|
+
syncBack({
|
|
697
|
+
project: runtimeTask.workspace.slug,
|
|
698
|
+
mode: 'push',
|
|
699
|
+
server: flags.server,
|
|
700
|
+
token: flags.token,
|
|
701
|
+
}, {
|
|
702
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
703
|
+
log: () => {},
|
|
704
|
+
})
|
|
705
|
+
), deps, retryState)
|
|
300
706
|
}
|
|
301
707
|
|
|
302
708
|
const body = normalizeTaskCompletion(runtimeTask, completion)
|
|
@@ -319,6 +725,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
319
725
|
const heartbeatIntervalMs = Number(flag(flags, 'heartbeat-interval', '15')) * 1000
|
|
320
726
|
const scanIntervalMs = Number(flag(flags, 'scan-interval', '60')) * 1000
|
|
321
727
|
const taskIntervalMs = Number(flag(flags, 'task-interval', flag(flags, 'heartbeat-interval', '15'))) * 1000
|
|
728
|
+
const projectRefreshIntervalMs = Number(flag(flags, 'project-refresh-interval', '1800')) * 1000
|
|
322
729
|
const once = Boolean(flags.once)
|
|
323
730
|
const handlerPath = flag(flags, 'handler')
|
|
324
731
|
|
|
@@ -331,18 +738,23 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
331
738
|
if (!Number.isFinite(taskIntervalMs) || taskIntervalMs <= 0) {
|
|
332
739
|
throw new Error('--task-interval must be a positive number of seconds')
|
|
333
740
|
}
|
|
741
|
+
if (!Number.isFinite(projectRefreshIntervalMs) || projectRefreshIntervalMs <= 0) {
|
|
742
|
+
throw new Error('--project-refresh-interval must be a positive number of seconds')
|
|
743
|
+
}
|
|
334
744
|
|
|
335
745
|
const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
|
|
336
746
|
const retryState = createRetryState()
|
|
337
747
|
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
748
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
338
749
|
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
339
750
|
if (once) return
|
|
340
751
|
|
|
341
752
|
let lastScan = Date.now()
|
|
342
753
|
let lastHeartbeat = Date.now()
|
|
343
754
|
let lastTaskPoll = Date.now()
|
|
755
|
+
let lastProjectRefresh = Date.now()
|
|
344
756
|
while (true) {
|
|
345
|
-
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs))
|
|
757
|
+
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
|
|
346
758
|
const now = Date.now()
|
|
347
759
|
if (now - lastScan >= scanIntervalMs) {
|
|
348
760
|
registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
@@ -356,5 +768,9 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
356
768
|
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
357
769
|
lastTaskPoll = now
|
|
358
770
|
}
|
|
771
|
+
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|
|
772
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
773
|
+
lastProjectRefresh = now
|
|
774
|
+
}
|
|
359
775
|
}
|
|
360
776
|
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { syncKnowledge } from './commands/knowledge.js'
|
|
2
|
+
import { requestJson } from './client.js'
|
|
3
|
+
|
|
4
|
+
const TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
name: 'knowledge_sync',
|
|
7
|
+
description: 'Pull cloud project knowledge to local disk or push local project knowledge back to cloud with a project token.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
required: ['project', 'mode'],
|
|
11
|
+
properties: {
|
|
12
|
+
project: { type: 'string' },
|
|
13
|
+
mode: { type: 'string', enum: ['pull', 'push'] },
|
|
14
|
+
token: { type: 'string' },
|
|
15
|
+
server: { type: 'string' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
function textResult(value) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify(value, null, 2),
|
|
26
|
+
}],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolArgs(request) {
|
|
31
|
+
return request?.params?.arguments || {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleMcpRequest(request, deps = {}) {
|
|
35
|
+
if (request.method === 'initialize') {
|
|
36
|
+
return {
|
|
37
|
+
protocolVersion: '2024-11-05',
|
|
38
|
+
capabilities: { tools: {} },
|
|
39
|
+
serverInfo: { name: '11agents-project-sync', version: '0.1.0' },
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (request.method === 'tools/list') return { tools: TOOLS }
|
|
43
|
+
if (request.method !== 'tools/call') throw new Error(`unsupported MCP method: ${request.method}`)
|
|
44
|
+
|
|
45
|
+
const name = request?.params?.name
|
|
46
|
+
const args = toolArgs(request)
|
|
47
|
+
if (name === 'knowledge_sync') {
|
|
48
|
+
const result = await syncKnowledge({
|
|
49
|
+
project: args.project,
|
|
50
|
+
mode: args.mode,
|
|
51
|
+
server: args.server,
|
|
52
|
+
token: args.token,
|
|
53
|
+
}, {
|
|
54
|
+
...deps,
|
|
55
|
+
requestJson: (apiPath, options = {}) => (deps.requestJson || requestJson)(apiPath, {
|
|
56
|
+
...options,
|
|
57
|
+
config: { token: args.token || '', server: args.server || 'https://app.11agents.ai' },
|
|
58
|
+
}),
|
|
59
|
+
})
|
|
60
|
+
return textResult(result)
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`unknown MCP tool: ${name}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function jsonRpcError(id, error) {
|
|
66
|
+
return {
|
|
67
|
+
jsonrpc: '2.0',
|
|
68
|
+
id,
|
|
69
|
+
error: {
|
|
70
|
+
code: -32000,
|
|
71
|
+
message: error instanceof Error ? error.message : String(error),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function startMcpServer({ input = process.stdin, output = process.stdout, deps = {} } = {}) {
|
|
77
|
+
input.setEncoding('utf8')
|
|
78
|
+
let buffer = ''
|
|
79
|
+
input.on('data', chunk => {
|
|
80
|
+
buffer += chunk
|
|
81
|
+
let index = buffer.indexOf('\n')
|
|
82
|
+
while (index >= 0) {
|
|
83
|
+
const line = buffer.slice(0, index).trim()
|
|
84
|
+
buffer = buffer.slice(index + 1)
|
|
85
|
+
index = buffer.indexOf('\n')
|
|
86
|
+
if (!line) continue
|
|
87
|
+
;(async () => {
|
|
88
|
+
let request
|
|
89
|
+
try {
|
|
90
|
+
request = JSON.parse(line)
|
|
91
|
+
if (!request.id && request.method === 'notifications/initialized') return
|
|
92
|
+
const result = await handleMcpRequest(request, deps)
|
|
93
|
+
output.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\n`)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
output.write(`${JSON.stringify(jsonRpcError(request?.id || null, error))}\n`)
|
|
96
|
+
}
|
|
97
|
+
})()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
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(),
|