@11agents/cli 0.1.3 → 0.1.5
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 +31 -7
- package/bin/11agents.js +14 -4
- 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 +60 -4
- package/src/commands/runtime.js +215 -22
- package/src/credentials.js +70 -0
- package/src/mcp.js +105 -0
package/README.md
CHANGED
|
@@ -27,6 +27,16 @@ export ELEVENAGENTS_SERVER="http://localhost:8082"
|
|
|
27
27
|
|
|
28
28
|
On startup, the CLI prints its current version and target server to stderr. It also checks npm for a newer `@11agents/cli` package and prints an upgrade command when one is available.
|
|
29
29
|
|
|
30
|
+
Project-scoped MCP and knowledge-base sync tokens can be stored in `~/.11agents/credentials`:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# tokens:
|
|
34
|
+
flatkey: gtm_xxxxxxxxxxxxxxxxxxxx
|
|
35
|
+
voc-ai: gtm_yyyyyyyyyyyyyyyyyyyy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
When syncing a project knowledge base, the CLI chooses tokens in this order: explicit tool/command token, matching project token from `~/.11agents/credentials`, `GTM_SWARM_TOKEN`, then the daemon control token as a compatibility fallback.
|
|
39
|
+
|
|
30
40
|
## Runtime Pool
|
|
31
41
|
|
|
32
42
|
Scan local AI runtimes:
|
|
@@ -60,18 +70,20 @@ Background mode writes its pid to `~/.11agents/daemon.pid` and logs to `~/.11age
|
|
|
60
70
|
Useful daemon options:
|
|
61
71
|
|
|
62
72
|
```bash
|
|
63
|
-
11agents daemon start --heartbeat-interval 15 --scan-interval 60 --task-interval 15
|
|
73
|
+
11agents daemon start --heartbeat-interval 15 --scan-interval 60 --task-interval 15 --project-refresh-interval 1800
|
|
64
74
|
11agents daemon start --handler ./worker.js
|
|
65
75
|
```
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
On startup, and every 30 minutes after that, the daemon syncs project metadata and prepares local project headquarters:
|
|
68
78
|
|
|
69
|
-
-
|
|
70
|
-
- Project knowledge base: `~/.11agents
|
|
71
|
-
-
|
|
72
|
-
-
|
|
79
|
+
- Project headquarters: `~/.11agents/<project>/`
|
|
80
|
+
- Project knowledge base: `~/.11agents/<project>/knowledge_base/`
|
|
81
|
+
- Agent QMD memory: `~/.11agents/<project>/agents/<agent>/memory/`
|
|
82
|
+
- Agent-local skills: `~/.11agents/<project>/agents/<agent>/skills/`
|
|
83
|
+
- Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
|
|
84
|
+
- Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
|
|
73
85
|
|
|
74
|
-
Codex runs from `~/.11agents
|
|
86
|
+
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.
|
|
75
87
|
|
|
76
88
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
77
89
|
|
|
@@ -85,6 +97,18 @@ export async function handleRuntimeTask(task) {
|
|
|
85
97
|
}
|
|
86
98
|
```
|
|
87
99
|
|
|
100
|
+
## MCP Project Sync Server
|
|
101
|
+
|
|
102
|
+
Run the local MCP server over stdio:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
11agents mcp start
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Configure your MCP client to run that command. The server automatically reads matching project tokens from `~/.11agents/credentials`; a tool-call token can still be passed explicitly when needed. The server exposes:
|
|
109
|
+
|
|
110
|
+
- `knowledge_sync` — pull/push the project knowledge base between cloud and `~/.11agents/<project>/knowledge_base/`.
|
|
111
|
+
|
|
88
112
|
## Telemetry Compatibility
|
|
89
113
|
|
|
90
114
|
The package still includes the original `gtm-swarm` binary for swarm telemetry compatibility.
|
package/bin/11agents.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/command
|
|
|
9
9
|
import { startBackgroundDaemon, statusBackgroundDaemon, stopBackgroundDaemon } from '../src/daemon-process.js'
|
|
10
10
|
import { getControlConfig } from '../src/client.js'
|
|
11
11
|
import { CLI_VERSION, printStartupInfo } from '../src/info.js'
|
|
12
|
+
import { startMcpServer } from '../src/mcp.js'
|
|
12
13
|
import { validateTelemetryBatch } from '../src/schema.js'
|
|
13
14
|
|
|
14
15
|
function usage() {
|
|
@@ -18,10 +19,11 @@ Usage:
|
|
|
18
19
|
11agents help
|
|
19
20
|
11agents runtime scan
|
|
20
21
|
11agents runtime register [--server <url>] [--token <token>] [--machine <key>]
|
|
21
|
-
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]
|
|
22
23
|
11agents daemon status
|
|
23
24
|
11agents daemon stop
|
|
24
25
|
11agents daemon start --handler ./worker.js # optional custom worker override
|
|
26
|
+
11agents mcp start
|
|
25
27
|
11agents validate <file>
|
|
26
28
|
11agents push batch <file>
|
|
27
29
|
11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
|
|
@@ -36,11 +38,14 @@ Environment:
|
|
|
36
38
|
GTM_WRITES_TOKEN control-plane token for runtime registration
|
|
37
39
|
ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
|
|
38
40
|
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
41
|
+
~/.11agents/credentials project token map for MCP knowledge sync
|
|
39
42
|
|
|
40
43
|
Runtime task workspace:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
Daemon project headquarters live under ~/.11agents/<project>/.
|
|
45
|
+
Knowledge lives under knowledge_base/. Agent QMD memory lives under
|
|
46
|
+
agents/<agent>/memory/. Agent-local skills live under agents/<agent>/skills/.
|
|
47
|
+
Built-in Codex tasks run from ~/.11agents/<project>/ by default. Temporary
|
|
48
|
+
writes belong under ./tmp/<taskId>/ and are cleaned after the task finishes.`)
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
async function main() {
|
|
@@ -105,6 +110,11 @@ async function main() {
|
|
|
105
110
|
return
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
if (command === 'mcp' && (!subcommand || subcommand === 'start')) {
|
|
114
|
+
await startMcpServer()
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
if (command === 'validate') {
|
|
109
119
|
const file = subcommand
|
|
110
120
|
if (!file) throw new Error('validate requires a file')
|
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { flag } from '../args.js'
|
|
@@ -18,6 +18,8 @@ function safeLocalPath(value, id) {
|
|
|
18
18
|
return `nodes/${slugify(id)}.md`
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const MARKDOWN_NODE_KINDS = new Set(['document', 'fact', 'decision', 'insight', 'resource', 'task-context'])
|
|
22
|
+
|
|
21
23
|
function normalizeSnapshot(snapshot = {}) {
|
|
22
24
|
return {
|
|
23
25
|
cloud_revision: Number(snapshot.cloud_revision || snapshot.knowledge_base?.cloud_revision || 0),
|
|
@@ -68,8 +70,50 @@ function bodyFromMarkdownNode(markdown, fallback = '') {
|
|
|
68
70
|
return body || fallback
|
|
69
71
|
}
|
|
70
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
|
+
|
|
71
115
|
export function buildKnowledgeBaseDir(project, { homeDir = os.homedir() } = {}) {
|
|
72
|
-
return path.join(homeDir, '.11agents',
|
|
116
|
+
return path.join(homeDir, '.11agents', slugify(project), 'knowledge_base')
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
export async function writeKnowledgeSnapshot(project, snapshot, { homeDir = os.homedir() } = {}) {
|
|
@@ -101,12 +145,24 @@ export async function readKnowledgeSnapshot(project, { homeDir = os.homedir() }
|
|
|
101
145
|
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
102
146
|
const raw = await readFile(path.join(baseDir, 'index.json'), 'utf-8').catch(() => '{}')
|
|
103
147
|
const snapshot = normalizeSnapshot(JSON.parse(raw))
|
|
104
|
-
const
|
|
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 => {
|
|
105
161
|
const markdown = await readFile(path.join(baseDir, node.local_path), 'utf-8').catch(() => null)
|
|
106
162
|
if (markdown === null) return node
|
|
107
163
|
return { ...node, body: bodyFromMarkdownNode(markdown, node.body) }
|
|
108
164
|
}))
|
|
109
|
-
return { ...snapshot, nodes }
|
|
165
|
+
return { ...snapshot, nodes: nodes.sort((a, b) => a.id.localeCompare(b.id)) }
|
|
110
166
|
}
|
|
111
167
|
|
|
112
168
|
export async function syncKnowledge(flags = {}, deps = {}) {
|
package/src/commands/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
5
|
import os from 'node:os'
|
|
6
6
|
import { dirname, resolve } from 'node:path'
|
|
7
7
|
import path from 'node:path'
|
|
@@ -9,8 +9,10 @@ import { pathToFileURL } from 'node:url'
|
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { flag } from '../args.js'
|
|
11
11
|
import { getControlConfig, requestJson } from '../client.js'
|
|
12
|
+
import { resolveProjectToken } from '../credentials.js'
|
|
12
13
|
import { syncKnowledge } from './knowledge.js'
|
|
13
14
|
import { buildRuntimeScan } from '../runtime-scan.js'
|
|
15
|
+
import { handleMcpRequest } from '../mcp.js'
|
|
14
16
|
|
|
15
17
|
const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
|
|
16
18
|
|
|
@@ -66,6 +68,7 @@ function runtimeDeps(overrides = {}) {
|
|
|
66
68
|
requestJson: overrides.requestJson || requestJson,
|
|
67
69
|
sleep: overrides.sleep || sleep,
|
|
68
70
|
syncKnowledge: overrides.syncKnowledge || syncKnowledge,
|
|
71
|
+
mcpKnowledgeSync: overrides.mcpKnowledgeSync || mcpKnowledgeSync,
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -147,6 +150,37 @@ export async function heartbeatRuntime(flags = {}, deps = {}) {
|
|
|
147
150
|
return result
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
export async function syncRuntimeProjectMetadata(flags = {}, deps = {}) {
|
|
154
|
+
const { log, requestJson: request, homeDir } = runtimeDeps(deps)
|
|
155
|
+
const config = configFromFlags(flags)
|
|
156
|
+
if (!config.token) throw new Error('GTM_WRITES_TOKEN or --token is required')
|
|
157
|
+
|
|
158
|
+
const result = await request('/api/runtime/projects', {
|
|
159
|
+
method: 'GET',
|
|
160
|
+
config,
|
|
161
|
+
})
|
|
162
|
+
const materialized = await materializeRuntimeProjectDirectories(result?.projects || [], { homeDir })
|
|
163
|
+
log(JSON.stringify({
|
|
164
|
+
ok: true,
|
|
165
|
+
synced: 'runtime projects',
|
|
166
|
+
base_dir: materialized.base_dir,
|
|
167
|
+
projects: materialized.projects,
|
|
168
|
+
}, null, 2))
|
|
169
|
+
return { ...result, ...materialized }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function syncRuntimeProjectMetadataBestEffort(flags, deps) {
|
|
173
|
+
try {
|
|
174
|
+
return await syncRuntimeProjectMetadata(flags, deps)
|
|
175
|
+
} catch (error) {
|
|
176
|
+
deps.log(JSON.stringify({
|
|
177
|
+
warning: 'runtime project metadata sync failed',
|
|
178
|
+
error: errorMessage(error),
|
|
179
|
+
}, null, 2))
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
150
184
|
function normalizeTaskCompletion(task, completion) {
|
|
151
185
|
const result = completion && typeof completion === 'object' ? completion : {}
|
|
152
186
|
return {
|
|
@@ -172,6 +206,27 @@ function compactJson(value) {
|
|
|
172
206
|
return JSON.stringify(value ?? null, null, 2)
|
|
173
207
|
}
|
|
174
208
|
|
|
209
|
+
async function mcpKnowledgeSync(flags = {}, deps = {}) {
|
|
210
|
+
const token = await resolveProjectToken(flags.project, {
|
|
211
|
+
homeDir: deps.homeDir,
|
|
212
|
+
token: flags.projectToken,
|
|
213
|
+
fallbackToken: flags.token,
|
|
214
|
+
})
|
|
215
|
+
const result = await handleMcpRequest({
|
|
216
|
+
method: 'tools/call',
|
|
217
|
+
params: {
|
|
218
|
+
name: 'knowledge_sync',
|
|
219
|
+
arguments: {
|
|
220
|
+
project: flags.project,
|
|
221
|
+
mode: flags.mode,
|
|
222
|
+
server: flags.server,
|
|
223
|
+
token,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
}, deps)
|
|
227
|
+
return JSON.parse(result.content?.[0]?.text || '{}')
|
|
228
|
+
}
|
|
229
|
+
|
|
175
230
|
const nonAlphaNum = /[^a-z0-9]+/g
|
|
176
231
|
|
|
177
232
|
function slugify(value, fallback = 'project') {
|
|
@@ -186,12 +241,99 @@ function sanitizeTaskId(value) {
|
|
|
186
241
|
return slugify(value, 'task')
|
|
187
242
|
}
|
|
188
243
|
|
|
244
|
+
export function buildRuntimeProjectDir(project, { homeDir = os.homedir() } = {}) {
|
|
245
|
+
return path.join(homeDir, '.11agents', slugify(project))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function buildRuntimeKnowledgeDir(project, { homeDir = os.homedir() } = {}) {
|
|
249
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'knowledge_base')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function buildRuntimeAgentMemoryDir(project, agent, { homeDir = os.homedir() } = {}) {
|
|
253
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'agents', slugify(agent, 'agent'), 'memory')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildRuntimeAgentSkillsDir(project, agent, { homeDir = os.homedir() } = {}) {
|
|
257
|
+
return path.join(buildRuntimeProjectDir(project, { homeDir }), 'agents', slugify(agent, 'agent'), 'skills')
|
|
258
|
+
}
|
|
259
|
+
|
|
189
260
|
function projectSlugForTask(task, flags = {}) {
|
|
190
261
|
return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
|
|
191
262
|
}
|
|
192
263
|
|
|
193
264
|
function projectDirForTask(task, flags = {}, deps) {
|
|
194
|
-
return
|
|
265
|
+
return buildRuntimeProjectDir(projectSlugForTask(task, flags), { homeDir: deps.homeDir })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function projectSyncToken(project, flags = {}, deps = {}) {
|
|
269
|
+
return resolveProjectToken(project, {
|
|
270
|
+
homeDir: deps.homeDir,
|
|
271
|
+
fallbackToken: flags.token,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function writeFileIfMissing(filePath, content) {
|
|
276
|
+
try {
|
|
277
|
+
await writeFile(filePath, content, { flag: 'wx' })
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error?.code !== 'EEXIST') throw error
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function defaultMemoryQmd(project, agent) {
|
|
284
|
+
return [
|
|
285
|
+
`# ${agent.name || agent.id || 'Agent'} Memory`,
|
|
286
|
+
'',
|
|
287
|
+
`project: ${project.slug || project.name || 'project'}`,
|
|
288
|
+
`agent: ${agent.name || agent.id || 'agent'}`,
|
|
289
|
+
'',
|
|
290
|
+
].join('\n')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function safeQmdName(value, fallback) {
|
|
294
|
+
const name = slugify(String(value || '').replace(/\.qmd$/i, ''), fallback)
|
|
295
|
+
return `${name}.qmd`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function materializeRuntimeProjectDirectories(projects = [], { homeDir = os.homedir() } = {}) {
|
|
299
|
+
const baseDir = path.join(homeDir, '.11agents')
|
|
300
|
+
await mkdir(baseDir, { recursive: true })
|
|
301
|
+
const written = []
|
|
302
|
+
for (const project of Array.isArray(projects) ? projects : []) {
|
|
303
|
+
const projectSlug = slugify(project?.slug || project?.name, 'project')
|
|
304
|
+
const projectDir = buildRuntimeProjectDir(projectSlug, { homeDir })
|
|
305
|
+
await mkdir(buildRuntimeKnowledgeDir(projectSlug, { homeDir }), { recursive: true })
|
|
306
|
+
written.push(projectDir)
|
|
307
|
+
for (const agent of Array.isArray(project?.agents) ? project.agents : []) {
|
|
308
|
+
const agentName = agent?.name || agent?.id || 'agent'
|
|
309
|
+
const memoryDir = buildRuntimeAgentMemoryDir(projectSlug, agentName, { homeDir })
|
|
310
|
+
const skillsDir = buildRuntimeAgentSkillsDir(projectSlug, agentName, { homeDir })
|
|
311
|
+
await mkdir(memoryDir, { recursive: true })
|
|
312
|
+
await mkdir(skillsDir, { recursive: true })
|
|
313
|
+
await writeFileIfMissing(path.join(memoryDir, 'index.qmd'), defaultMemoryQmd({ ...project, slug: projectSlug }, agent))
|
|
314
|
+
for (const memory of Array.isArray(agent?.memory) ? agent.memory : []) {
|
|
315
|
+
await writeFileIfMissing(path.join(memoryDir, safeQmdName(memory, 'memory')), '')
|
|
316
|
+
}
|
|
317
|
+
for (const skill of Array.isArray(agent?.skills) ? agent.skills : []) {
|
|
318
|
+
await mkdir(path.join(skillsDir, slugify(skill?.name || skill, 'skill')), { recursive: true })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { base_dir: baseDir, projects: written.length }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function knowledgeDeepOrganizeSpec(task) {
|
|
326
|
+
if (task.queue_event?.trigger_type !== 'knowledge_deep_organize') return null
|
|
327
|
+
const raw = String(task.queue_event?.trigger_summary || '')
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(raw)
|
|
330
|
+
return {
|
|
331
|
+
source_url: String(parsed.source_url || parsed.url || ''),
|
|
332
|
+
source_type: String(parsed.source_type || '').toLowerCase(),
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
return { source_url: raw, source_type: raw.includes('github.com') ? 'github' : 'url' }
|
|
336
|
+
}
|
|
195
337
|
}
|
|
196
338
|
|
|
197
339
|
async function readJsonFile(filePath, fallback = {}) {
|
|
@@ -220,13 +362,8 @@ function decodeFileContent(file) {
|
|
|
220
362
|
return Buffer.from(String(file?.content || ''), 'utf8')
|
|
221
363
|
}
|
|
222
364
|
|
|
223
|
-
function
|
|
224
|
-
|
|
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')
|
|
365
|
+
function agentNameForTask(task) {
|
|
366
|
+
return task.agent?.name || task.agent?.id || task.agent_id || task.assignee_id || 'agent'
|
|
230
367
|
}
|
|
231
368
|
|
|
232
369
|
function normalizeSkillBundle(skill = {}) {
|
|
@@ -248,17 +385,12 @@ async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
|
248
385
|
const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
|
|
249
386
|
if (!skills.length) return { changed: false, count: 0 }
|
|
250
387
|
|
|
251
|
-
const
|
|
388
|
+
const skillsDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'skills')
|
|
389
|
+
const statePath = path.join(skillsDir, 'skills-state.json')
|
|
252
390
|
const nextHash = stableHash(skills)
|
|
253
391
|
const current = await readJsonFile(statePath, {})
|
|
254
392
|
if (current.hash === nextHash) return { changed: false, count: skills.length }
|
|
255
393
|
|
|
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
394
|
await mkdir(skillsDir, { recursive: true })
|
|
263
395
|
for (const skill of skills) {
|
|
264
396
|
const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
|
|
@@ -280,6 +412,23 @@ async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
|
280
412
|
return { changed: true, count: skills.length, skills_dir: skillsDir }
|
|
281
413
|
}
|
|
282
414
|
|
|
415
|
+
async function appendTaskMemoryDelta({ task, completion, workdir }) {
|
|
416
|
+
const memoryDelta = String(completion?.memory_delta || completion?.memoryDelta || '').trim()
|
|
417
|
+
if (!memoryDelta) return { written: false }
|
|
418
|
+
const memoryDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'memory')
|
|
419
|
+
await mkdir(memoryDir, { recursive: true })
|
|
420
|
+
const indexPath = path.join(memoryDir, 'index.qmd')
|
|
421
|
+
await writeFileIfMissing(indexPath, defaultMemoryQmd({ slug: projectSlugForTask(task), name: task.workspace?.name }, task.agent || {}))
|
|
422
|
+
await appendFile(indexPath, [
|
|
423
|
+
'',
|
|
424
|
+
`## ${new Date().toISOString()} ${task.id || ''}`.trim(),
|
|
425
|
+
'',
|
|
426
|
+
memoryDelta,
|
|
427
|
+
'',
|
|
428
|
+
].join('\n'))
|
|
429
|
+
return { written: true, path: indexPath }
|
|
430
|
+
}
|
|
431
|
+
|
|
283
432
|
function databaseSyncSpec(task) {
|
|
284
433
|
const spec = task.database || task.cloud_database || task.workspace?.database || null
|
|
285
434
|
if (!spec || typeof spec !== 'object') return null
|
|
@@ -364,6 +513,7 @@ async function prepareRuntimeTask(task, flags, deps, config) {
|
|
|
364
513
|
}
|
|
365
514
|
|
|
366
515
|
function buildCodexPrompt(task) {
|
|
516
|
+
const deepOrganize = knowledgeDeepOrganizeSpec(task)
|
|
367
517
|
return [
|
|
368
518
|
'You are executing an 11agents task as the assigned agent.',
|
|
369
519
|
'',
|
|
@@ -401,6 +551,33 @@ function buildCodexPrompt(task) {
|
|
|
401
551
|
'',
|
|
402
552
|
'Thread comments:',
|
|
403
553
|
compactJson(task.comments || []),
|
|
554
|
+
deepOrganize ? [
|
|
555
|
+
'',
|
|
556
|
+
'Knowledge deep organization task:',
|
|
557
|
+
compactJson({
|
|
558
|
+
trigger_type: 'knowledge_deep_organize',
|
|
559
|
+
source_url: deepOrganize.source_url,
|
|
560
|
+
source_type: deepOrganize.source_type || (deepOrganize.source_url.includes('github.com') ? 'github' : 'url'),
|
|
561
|
+
output_dir: task.execution_context?.workdir ? `${task.execution_context.workdir}/knowledge_base` : './knowledge_base',
|
|
562
|
+
callback: 'After organizing files, call the 11agents MCP knowledge_sync tool with mode=push so cloud receives the finished wiki.',
|
|
563
|
+
}),
|
|
564
|
+
'',
|
|
565
|
+
'Build the knowledge base using the standard llms.txt / LLM Wiki pattern popularized for LLM-readable documentation:',
|
|
566
|
+
'- 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.',
|
|
567
|
+
'- Create or update ./knowledge_base/llms-full.txt as a fuller single-file context pack when useful.',
|
|
568
|
+
'- 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.',
|
|
569
|
+
'- 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.',
|
|
570
|
+
'- Do not stop at project-profile.md / knowledge-map.md placeholders. Create concrete markdown files with actionable content.',
|
|
571
|
+
'- 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.',
|
|
572
|
+
'- 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.',
|
|
573
|
+
'- README.md should include a "Where to Start" table and a directory overview.',
|
|
574
|
+
'- index.md should include Identity, Node Map, and Execution Instructions sections with wiki-style references to important files.',
|
|
575
|
+
'- CLAUDE.md should include the exact read order and output/update rules for runtime agents.',
|
|
576
|
+
'- If source_type is github, inspect README, docs, package manifests, examples, and source tree. Prefer concise durable explanations over raw dumps.',
|
|
577
|
+
'- If source_type is url, read the page and prefer /llms.txt or /llms-full.txt from that origin when available.',
|
|
578
|
+
'- Keep file paths stable and descriptive. Do not write temporary research into the durable wiki.',
|
|
579
|
+
'- When done, use MCP to push the local knowledge_base back to cloud.',
|
|
580
|
+
].join('\n') : '',
|
|
404
581
|
'',
|
|
405
582
|
'Work in this repository and make the needed changes. When finished, respond with a concise summary for the task thread.',
|
|
406
583
|
].join('\n')
|
|
@@ -499,14 +676,16 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
499
676
|
|
|
500
677
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
501
678
|
if (runtimeTask.workspace?.slug) {
|
|
679
|
+
const token = await projectSyncToken(runtimeTask.workspace.slug, flags, deps)
|
|
680
|
+
const syncConfig = { ...config, token }
|
|
502
681
|
await runWithDaemonRetry('sync knowledge base', () => (
|
|
503
682
|
deps.syncKnowledge({
|
|
504
683
|
project: runtimeTask.workspace.slug,
|
|
505
684
|
mode: 'pull',
|
|
506
685
|
server: flags.server,
|
|
507
|
-
token
|
|
686
|
+
token,
|
|
508
687
|
}, {
|
|
509
|
-
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
688
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config: syncConfig }),
|
|
510
689
|
log: () => {},
|
|
511
690
|
})
|
|
512
691
|
), deps, retryState)
|
|
@@ -524,16 +703,20 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
524
703
|
} finally {
|
|
525
704
|
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
526
705
|
}
|
|
706
|
+
await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
|
|
527
707
|
|
|
528
708
|
if (runtimeTask.workspace?.slug) {
|
|
709
|
+
const syncBack = knowledgeDeepOrganizeSpec(runtimeTask) ? deps.mcpKnowledgeSync : deps.syncKnowledge
|
|
710
|
+
const token = await projectSyncToken(runtimeTask.workspace.slug, flags, deps)
|
|
711
|
+
const syncConfig = { ...config, token }
|
|
529
712
|
await runWithDaemonRetry('sync knowledge base back to cloud', () => (
|
|
530
|
-
|
|
713
|
+
syncBack({
|
|
531
714
|
project: runtimeTask.workspace.slug,
|
|
532
715
|
mode: 'push',
|
|
533
716
|
server: flags.server,
|
|
534
|
-
token
|
|
717
|
+
token,
|
|
535
718
|
}, {
|
|
536
|
-
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
719
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config: syncConfig }),
|
|
537
720
|
log: () => {},
|
|
538
721
|
})
|
|
539
722
|
), deps, retryState)
|
|
@@ -559,6 +742,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
559
742
|
const heartbeatIntervalMs = Number(flag(flags, 'heartbeat-interval', '15')) * 1000
|
|
560
743
|
const scanIntervalMs = Number(flag(flags, 'scan-interval', '60')) * 1000
|
|
561
744
|
const taskIntervalMs = Number(flag(flags, 'task-interval', flag(flags, 'heartbeat-interval', '15'))) * 1000
|
|
745
|
+
const projectRefreshIntervalMs = Number(flag(flags, 'project-refresh-interval', '1800')) * 1000
|
|
562
746
|
const once = Boolean(flags.once)
|
|
563
747
|
const handlerPath = flag(flags, 'handler')
|
|
564
748
|
|
|
@@ -571,18 +755,23 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
571
755
|
if (!Number.isFinite(taskIntervalMs) || taskIntervalMs <= 0) {
|
|
572
756
|
throw new Error('--task-interval must be a positive number of seconds')
|
|
573
757
|
}
|
|
758
|
+
if (!Number.isFinite(projectRefreshIntervalMs) || projectRefreshIntervalMs <= 0) {
|
|
759
|
+
throw new Error('--project-refresh-interval must be a positive number of seconds')
|
|
760
|
+
}
|
|
574
761
|
|
|
575
762
|
const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
|
|
576
763
|
const retryState = createRetryState()
|
|
577
764
|
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
765
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
578
766
|
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
579
767
|
if (once) return
|
|
580
768
|
|
|
581
769
|
let lastScan = Date.now()
|
|
582
770
|
let lastHeartbeat = Date.now()
|
|
583
771
|
let lastTaskPoll = Date.now()
|
|
772
|
+
let lastProjectRefresh = Date.now()
|
|
584
773
|
while (true) {
|
|
585
|
-
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs))
|
|
774
|
+
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
|
|
586
775
|
const now = Date.now()
|
|
587
776
|
if (now - lastScan >= scanIntervalMs) {
|
|
588
777
|
registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
@@ -596,5 +785,9 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
596
785
|
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
|
|
597
786
|
lastTaskPoll = now
|
|
598
787
|
}
|
|
788
|
+
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|
|
789
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
790
|
+
lastProjectRefresh = now
|
|
791
|
+
}
|
|
599
792
|
}
|
|
600
793
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
function slugify(value) {
|
|
6
|
+
return String(value || 'project')
|
|
7
|
+
.trim()
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
10
|
+
.replace(/^-|-$/g, '') || 'project'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanHeader(value) {
|
|
14
|
+
return String(value || '').trim().replace(/^#\s*/, '').trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseCredentials(content = '') {
|
|
18
|
+
const tokens = {}
|
|
19
|
+
let inTokens = false
|
|
20
|
+
|
|
21
|
+
for (const rawLine of String(content).split(/\r?\n/)) {
|
|
22
|
+
const line = rawLine.trim()
|
|
23
|
+
if (!line) continue
|
|
24
|
+
|
|
25
|
+
const header = cleanHeader(line)
|
|
26
|
+
if (/^tokens\s*:\s*$/.test(header)) {
|
|
27
|
+
inTokens = true
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith('#')) continue
|
|
31
|
+
|
|
32
|
+
const match = line.match(/^([^:#]+):\s*(.+)$/)
|
|
33
|
+
if (!match) {
|
|
34
|
+
inTokens = false
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (!inTokens && Object.keys(tokens).length === 0) inTokens = true
|
|
38
|
+
if (!inTokens) continue
|
|
39
|
+
|
|
40
|
+
const project = slugify(match[1])
|
|
41
|
+
const token = match[2].trim()
|
|
42
|
+
if (project && token) tokens[project] = token
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { tokens }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readCredentials({ homeDir = os.homedir() } = {}) {
|
|
49
|
+
try {
|
|
50
|
+
return parseCredentials(await readFile(path.join(homeDir, '.11agents', 'credentials'), 'utf8'))
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code === 'ENOENT') return { tokens: {} }
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function resolveProjectToken(project, {
|
|
58
|
+
homeDir = os.homedir(),
|
|
59
|
+
token = '',
|
|
60
|
+
fallbackToken = '',
|
|
61
|
+
env = process.env,
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (token) return token
|
|
64
|
+
|
|
65
|
+
const credentials = await readCredentials({ homeDir })
|
|
66
|
+
const projectToken = credentials.tokens[slugify(project)]
|
|
67
|
+
if (projectToken) return projectToken
|
|
68
|
+
|
|
69
|
+
return env.GTM_SWARM_TOKEN || fallbackToken || ''
|
|
70
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { syncKnowledge } from './commands/knowledge.js'
|
|
2
|
+
import { requestJson } from './client.js'
|
|
3
|
+
import { resolveProjectToken } from './credentials.js'
|
|
4
|
+
|
|
5
|
+
const TOOLS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'knowledge_sync',
|
|
8
|
+
description: 'Pull cloud project knowledge to local disk or push local project knowledge back to cloud with a project token.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
required: ['project', 'mode'],
|
|
12
|
+
properties: {
|
|
13
|
+
project: { type: 'string' },
|
|
14
|
+
mode: { type: 'string', enum: ['pull', 'push'] },
|
|
15
|
+
token: { type: 'string' },
|
|
16
|
+
server: { type: 'string' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function textResult(value) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: JSON.stringify(value, null, 2),
|
|
27
|
+
}],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toolArgs(request) {
|
|
32
|
+
return request?.params?.arguments || {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function handleMcpRequest(request, deps = {}) {
|
|
36
|
+
if (request.method === 'initialize') {
|
|
37
|
+
return {
|
|
38
|
+
protocolVersion: '2024-11-05',
|
|
39
|
+
capabilities: { tools: {} },
|
|
40
|
+
serverInfo: { name: '11agents-project-sync', version: '0.1.0' },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (request.method === 'tools/list') return { tools: TOOLS }
|
|
44
|
+
if (request.method !== 'tools/call') throw new Error(`unsupported MCP method: ${request.method}`)
|
|
45
|
+
|
|
46
|
+
const name = request?.params?.name
|
|
47
|
+
const args = toolArgs(request)
|
|
48
|
+
if (name === 'knowledge_sync') {
|
|
49
|
+
const token = await resolveProjectToken(args.project, {
|
|
50
|
+
homeDir: deps.homeDir,
|
|
51
|
+
token: args.token,
|
|
52
|
+
})
|
|
53
|
+
const result = await syncKnowledge({
|
|
54
|
+
project: args.project,
|
|
55
|
+
mode: args.mode,
|
|
56
|
+
server: args.server,
|
|
57
|
+
token,
|
|
58
|
+
}, {
|
|
59
|
+
...deps,
|
|
60
|
+
requestJson: (apiPath, options = {}) => (deps.requestJson || requestJson)(apiPath, {
|
|
61
|
+
...options,
|
|
62
|
+
config: { token, server: args.server || 'https://app.11agents.ai' },
|
|
63
|
+
}),
|
|
64
|
+
})
|
|
65
|
+
return textResult(result)
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`unknown MCP tool: ${name}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function jsonRpcError(id, error) {
|
|
71
|
+
return {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
id,
|
|
74
|
+
error: {
|
|
75
|
+
code: -32000,
|
|
76
|
+
message: error instanceof Error ? error.message : String(error),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function startMcpServer({ input = process.stdin, output = process.stdout, deps = {} } = {}) {
|
|
82
|
+
input.setEncoding('utf8')
|
|
83
|
+
let buffer = ''
|
|
84
|
+
input.on('data', chunk => {
|
|
85
|
+
buffer += chunk
|
|
86
|
+
let index = buffer.indexOf('\n')
|
|
87
|
+
while (index >= 0) {
|
|
88
|
+
const line = buffer.slice(0, index).trim()
|
|
89
|
+
buffer = buffer.slice(index + 1)
|
|
90
|
+
index = buffer.indexOf('\n')
|
|
91
|
+
if (!line) continue
|
|
92
|
+
;(async () => {
|
|
93
|
+
let request
|
|
94
|
+
try {
|
|
95
|
+
request = JSON.parse(line)
|
|
96
|
+
if (!request.id && request.method === 'notifications/initialized') return
|
|
97
|
+
const result = await handleMcpRequest(request, deps)
|
|
98
|
+
output.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\n`)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
output.write(`${JSON.stringify(jsonRpcError(request?.id || null, error))}\n`)
|
|
101
|
+
}
|
|
102
|
+
})()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|