@11agents/cli 0.1.3 → 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 CHANGED
@@ -60,18 +60,20 @@ 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
- Before a claimed task is handed to the built-in Codex worker, the daemon prepares a local read-only project cache:
67
+ On startup, and every 30 minutes after that, the daemon syncs project metadata and prepares local project headquarters:
68
68
 
69
- - Cloud database snapshot: `~/.11agents/projects/<project>/database/snapshot.json`
70
- - Project knowledge base: `~/.11agents/projects/<project>/knowledge_base/`
71
- - Skill sync state: `~/.11agents/projects/<project>/skills-state.json`
72
- - Task scratch directory: `~/.11agents/projects/<project>/tmp/<taskId>/`
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>/`
73
75
 
74
- Codex runs from `~/.11agents/projects/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
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.
75
77
 
76
78
  The built-in task runner currently supports Codex tasks. A custom handler may export:
77
79
 
@@ -85,6 +87,18 @@ export async function handleRuntimeTask(task) {
85
87
  }
86
88
  ```
87
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
+
88
102
  ## Telemetry Compatibility
89
103
 
90
104
  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>
@@ -38,9 +40,11 @@ Environment:
38
40
  GTM_SWARM_TOKEN project swarm token for push/node commands
39
41
 
40
42
  Runtime task workspace:
41
- Built-in Codex tasks run from ~/.11agents/projects/<project>/ by default.
42
- That directory is read-only project context. Temporary writes belong under
43
- ./tmp/<taskId>/ and are cleaned by the daemon after the task finishes.`)
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.`)
44
48
  }
45
49
 
46
50
  async function main() {
@@ -105,6 +109,11 @@ async function main() {
105
109
  return
106
110
  }
107
111
 
112
+ if (command === 'mcp' && (!subcommand || subcommand === 'start')) {
113
+ await startMcpServer()
114
+ return
115
+ }
116
+
108
117
  if (command === 'validate') {
109
118
  const file = subcommand
110
119
  if (!file) throw new Error('validate requires a file')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@11agents/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "11agents local runtime and telemetry CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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', 'projects', slugify(project), 'knowledge_base')
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 nodes = await Promise.all(snapshot.nodes.map(async node => {
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 = {}) {
@@ -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'
@@ -11,6 +11,7 @@ import { flag } from '../args.js'
11
11
  import { getControlConfig, requestJson } from '../client.js'
12
12
  import { syncKnowledge } from './knowledge.js'
13
13
  import { buildRuntimeScan } from '../runtime-scan.js'
14
+ import { handleMcpRequest } from '../mcp.js'
14
15
 
15
16
  const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
16
17
 
@@ -66,6 +67,7 @@ function runtimeDeps(overrides = {}) {
66
67
  requestJson: overrides.requestJson || requestJson,
67
68
  sleep: overrides.sleep || sleep,
68
69
  syncKnowledge: overrides.syncKnowledge || syncKnowledge,
70
+ mcpKnowledgeSync: overrides.mcpKnowledgeSync || mcpKnowledgeSync,
69
71
  }
70
72
  }
71
73
 
@@ -147,6 +149,37 @@ export async function heartbeatRuntime(flags = {}, deps = {}) {
147
149
  return result
148
150
  }
149
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
+
150
183
  function normalizeTaskCompletion(task, completion) {
151
184
  const result = completion && typeof completion === 'object' ? completion : {}
152
185
  return {
@@ -172,6 +205,22 @@ function compactJson(value) {
172
205
  return JSON.stringify(value ?? null, null, 2)
173
206
  }
174
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
+
175
224
  const nonAlphaNum = /[^a-z0-9]+/g
176
225
 
177
226
  function slugify(value, fallback = 'project') {
@@ -186,12 +235,92 @@ function sanitizeTaskId(value) {
186
235
  return slugify(value, 'task')
187
236
  }
188
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
+
189
254
  function projectSlugForTask(task, flags = {}) {
190
255
  return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
191
256
  }
192
257
 
193
258
  function projectDirForTask(task, flags = {}, deps) {
194
- return path.join(deps.homeDir, '.11agents', 'projects', projectSlugForTask(task, flags))
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
+ }
195
324
  }
196
325
 
197
326
  async function readJsonFile(filePath, fallback = {}) {
@@ -220,13 +349,8 @@ function decodeFileContent(file) {
220
349
  return Buffer.from(String(file?.content || ''), 'utf8')
221
350
  }
222
351
 
223
- function skillDirForProvider({ workdir, provider, codexHome }) {
224
- if (provider === 'codex') return path.join(codexHome || path.join(os.homedir(), '.codex'), 'skills')
225
- if (provider === 'claude') return path.join(workdir, '.claude', 'skills')
226
- if (provider === 'copilot') return path.join(workdir, '.github', 'skills')
227
- if (provider === 'opencode') return path.join(workdir, '.opencode', 'skills')
228
- if (provider === 'cursor') return path.join(workdir, '.cursor', 'skills')
229
- return path.join(workdir, '.agent_context', 'skills')
352
+ function agentNameForTask(task) {
353
+ return task.agent?.name || task.agent?.id || task.agent_id || task.assignee_id || 'agent'
230
354
  }
231
355
 
232
356
  function normalizeSkillBundle(skill = {}) {
@@ -248,17 +372,12 @@ async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
248
372
  const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
249
373
  if (!skills.length) return { changed: false, count: 0 }
250
374
 
251
- const statePath = path.join(workdir, 'skills-state.json')
375
+ const skillsDir = path.join(workdir, 'agents', slugify(agentNameForTask(task), 'agent'), 'skills')
376
+ const statePath = path.join(skillsDir, 'skills-state.json')
252
377
  const nextHash = stableHash(skills)
253
378
  const current = await readJsonFile(statePath, {})
254
379
  if (current.hash === nextHash) return { changed: false, count: skills.length }
255
380
 
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
381
  await mkdir(skillsDir, { recursive: true })
263
382
  for (const skill of skills) {
264
383
  const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
@@ -280,6 +399,23 @@ async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
280
399
  return { changed: true, count: skills.length, skills_dir: skillsDir }
281
400
  }
282
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
+
283
419
  function databaseSyncSpec(task) {
284
420
  const spec = task.database || task.cloud_database || task.workspace?.database || null
285
421
  if (!spec || typeof spec !== 'object') return null
@@ -364,6 +500,7 @@ async function prepareRuntimeTask(task, flags, deps, config) {
364
500
  }
365
501
 
366
502
  function buildCodexPrompt(task) {
503
+ const deepOrganize = knowledgeDeepOrganizeSpec(task)
367
504
  return [
368
505
  'You are executing an 11agents task as the assigned agent.',
369
506
  '',
@@ -401,6 +538,33 @@ function buildCodexPrompt(task) {
401
538
  '',
402
539
  'Thread comments:',
403
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') : '',
404
568
  '',
405
569
  'Work in this repository and make the needed changes. When finished, respond with a concise summary for the task thread.',
406
570
  ].join('\n')
@@ -524,10 +688,12 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
524
688
  } finally {
525
689
  await rm(executionContext.tmp_dir, { recursive: true, force: true })
526
690
  }
691
+ await appendTaskMemoryDelta({ task: runtimeTask, completion, workdir: executionContext.workdir })
527
692
 
528
693
  if (runtimeTask.workspace?.slug) {
694
+ const syncBack = knowledgeDeepOrganizeSpec(runtimeTask) ? deps.mcpKnowledgeSync : deps.syncKnowledge
529
695
  await runWithDaemonRetry('sync knowledge base back to cloud', () => (
530
- deps.syncKnowledge({
696
+ syncBack({
531
697
  project: runtimeTask.workspace.slug,
532
698
  mode: 'push',
533
699
  server: flags.server,
@@ -559,6 +725,7 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
559
725
  const heartbeatIntervalMs = Number(flag(flags, 'heartbeat-interval', '15')) * 1000
560
726
  const scanIntervalMs = Number(flag(flags, 'scan-interval', '60')) * 1000
561
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
562
729
  const once = Boolean(flags.once)
563
730
  const handlerPath = flag(flags, 'handler')
564
731
 
@@ -571,18 +738,23 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
571
738
  if (!Number.isFinite(taskIntervalMs) || taskIntervalMs <= 0) {
572
739
  throw new Error('--task-interval must be a positive number of seconds')
573
740
  }
741
+ if (!Number.isFinite(projectRefreshIntervalMs) || projectRefreshIntervalMs <= 0) {
742
+ throw new Error('--project-refresh-interval must be a positive number of seconds')
743
+ }
574
744
 
575
745
  const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
576
746
  const retryState = createRetryState()
577
747
  let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
748
+ await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
578
749
  await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
579
750
  if (once) return
580
751
 
581
752
  let lastScan = Date.now()
582
753
  let lastHeartbeat = Date.now()
583
754
  let lastTaskPoll = Date.now()
755
+ let lastProjectRefresh = Date.now()
584
756
  while (true) {
585
- await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs))
757
+ await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
586
758
  const now = Date.now()
587
759
  if (now - lastScan >= scanIntervalMs) {
588
760
  registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
@@ -596,5 +768,9 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
596
768
  await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState)
597
769
  lastTaskPoll = now
598
770
  }
771
+ if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
772
+ await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
773
+ lastProjectRefresh = now
774
+ }
599
775
  }
600
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
+ }