@agentprojectcontext/apx 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/package.json +52 -0
  4. package/skills/apx/SKILL.md +77 -0
  5. package/src/cli/commands/a2a.js +66 -0
  6. package/src/cli/commands/agent.js +181 -0
  7. package/src/cli/commands/chat.js +84 -0
  8. package/src/cli/commands/command.js +42 -0
  9. package/src/cli/commands/config.js +56 -0
  10. package/src/cli/commands/daemon.js +148 -0
  11. package/src/cli/commands/exec.js +56 -0
  12. package/src/cli/commands/identity.js +146 -0
  13. package/src/cli/commands/init.js +23 -0
  14. package/src/cli/commands/mcp.js +147 -0
  15. package/src/cli/commands/memory.js +69 -0
  16. package/src/cli/commands/messages.js +61 -0
  17. package/src/cli/commands/plugins.js +23 -0
  18. package/src/cli/commands/project.js +124 -0
  19. package/src/cli/commands/routine.js +99 -0
  20. package/src/cli/commands/runtime.js +64 -0
  21. package/src/cli/commands/session.js +387 -0
  22. package/src/cli/commands/skills.js +153 -0
  23. package/src/cli/commands/telegram.js +48 -0
  24. package/src/cli/http.js +102 -0
  25. package/src/cli/index.js +481 -0
  26. package/src/cli/postinstall.js +25 -0
  27. package/src/core/apc-context-skill.md +150 -0
  28. package/src/core/apx-skill.md +78 -0
  29. package/src/core/config.js +129 -0
  30. package/src/core/identity.js +23 -0
  31. package/src/core/messages-store.js +421 -0
  32. package/src/core/parser.js +217 -0
  33. package/src/core/routines-store.js +144 -0
  34. package/src/core/scaffold.js +417 -0
  35. package/src/core/session-store.js +36 -0
  36. package/src/daemon/apc-runtime-context.js +123 -0
  37. package/src/daemon/api.js +946 -0
  38. package/src/daemon/compact.js +140 -0
  39. package/src/daemon/conversations.js +108 -0
  40. package/src/daemon/db.js +81 -0
  41. package/src/daemon/engines/anthropic.js +58 -0
  42. package/src/daemon/engines/gemini.js +55 -0
  43. package/src/daemon/engines/index.js +65 -0
  44. package/src/daemon/engines/mock.js +18 -0
  45. package/src/daemon/engines/ollama.js +66 -0
  46. package/src/daemon/engines/openai.js +58 -0
  47. package/src/daemon/env-detect.js +69 -0
  48. package/src/daemon/index.js +156 -0
  49. package/src/daemon/mcp-runner.js +218 -0
  50. package/src/daemon/mcp-sources.js +114 -0
  51. package/src/daemon/plugins/index.js +91 -0
  52. package/src/daemon/plugins/telegram.js +549 -0
  53. package/src/daemon/project-config.js +98 -0
  54. package/src/daemon/routines.js +211 -0
  55. package/src/daemon/runtimes/_spawn.js +44 -0
  56. package/src/daemon/runtimes/aider.js +32 -0
  57. package/src/daemon/runtimes/claude-code.js +60 -0
  58. package/src/daemon/runtimes/codex.js +30 -0
  59. package/src/daemon/runtimes/index.js +39 -0
  60. package/src/daemon/runtimes/opencode.js +28 -0
  61. package/src/daemon/smoke.js +54 -0
  62. package/src/daemon/super-agent-tools.js +539 -0
  63. package/src/daemon/super-agent.js +188 -0
  64. package/src/daemon/thinking.js +45 -0
  65. package/src/daemon/tool-call-parser.js +116 -0
  66. package/src/daemon/wakeup.js +92 -0
  67. package/src/mcp/index.js +220 -0
@@ -0,0 +1,417 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import { readAgents, readAgentsFromDir, VAULT_DIR } from "./parser.js";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ export const SPEC_VERSION = "0.1.0";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // IDE skill targets — written during `apx init` and `apx skills add`
13
+ // ---------------------------------------------------------------------------
14
+
15
+ // Project-scoped targets (relative to project root).
16
+ // `ideDir` = the IDE's root config dir that must already exist for the target to apply.
17
+ export const IDE_TARGETS = [
18
+ {
19
+ id: "claude-code",
20
+ label: "Claude Code",
21
+ ideDir: ".claude",
22
+ file: ".claude/skills/apx/SKILL.md",
23
+ render: (c) => buildSkillMd(c),
24
+ append: false,
25
+ },
26
+ {
27
+ id: "cursor",
28
+ label: "Cursor",
29
+ ideDir: ".cursor",
30
+ file: ".cursor/rules/apx.mdc",
31
+ render: (c) =>
32
+ `---\ndescription: APX CLI skill. Activate when the user asks about running agents, coordinating between agents, or uses apx commands (apx run, apx exec, apx memory, apx mcp, apx session, apx messages).\n---\n\n${c}`,
33
+ append: false,
34
+ },
35
+ {
36
+ id: "windsurf",
37
+ label: "Windsurf",
38
+ ideDir: ".windsurf",
39
+ file: ".windsurf/rules/apx.md",
40
+ render: (c) =>
41
+ `---\ntrigger: model_decision\ndescription: APX CLI skill. Activate when the user asks about running agents, coordinating between agents, or uses apx commands (apx run, apx exec, apx memory, apx mcp, apx session, apx messages).\n---\n\n${c}`,
42
+ append: false,
43
+ },
44
+ {
45
+ id: "copilot",
46
+ label: "GitHub Copilot",
47
+ ideDir: ".github",
48
+ file: ".github/copilot-instructions.md",
49
+ render: (c) => `\n<!-- apx-skill -->\n${c}\n<!-- /apx-skill -->\n`,
50
+ append: true,
51
+ guard: "<!-- apx-skill -->",
52
+ },
53
+ {
54
+ id: "trae",
55
+ label: "Trae",
56
+ ideDir: ".trae",
57
+ file: ".trae/rules/project_rules.md",
58
+ render: (c) => `\n<!-- apx-skill -->\n${c}\n<!-- /apx-skill -->\n`,
59
+ append: true,
60
+ guard: "<!-- apx-skill -->",
61
+ },
62
+ ];
63
+
64
+ // Global targets (absolute paths, use ~/<dir>/skills/apx/SKILL.md format).
65
+ // These dirs are read by Claude Code, Cursor (compat), and tools adopting the skills.sh spec.
66
+ const GLOBAL_SKILL_DIRS = [
67
+ path.join(os.homedir(), ".claude", "skills"), // Claude Code + Cursor legacy compat
68
+ path.join(os.homedir(), ".cursor", "skills"), // Cursor primary global path
69
+ path.join(os.homedir(), ".codex", "skills"), // Codex (OpenAI)
70
+ path.join(os.homedir(), ".agents", "skills"), // Antigravity/other skills.sh adopters
71
+ ];
72
+
73
+ function buildApcContextSkillMd(content) {
74
+ const frontmatter = [
75
+ "---",
76
+ "name: apc-context",
77
+ "description: \"ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again.\"",
78
+ "homepage: https://github.com/agentprojectcontext/agentprojectcontext",
79
+ "---",
80
+ "",
81
+ ].join("\n");
82
+ return frontmatter + content;
83
+ }
84
+
85
+ function buildSkillMd(content) {
86
+ const frontmatter = [
87
+ "---",
88
+ "name: apx",
89
+ "description: APX CLI skill. Activate when: user asks to run or coordinate agents, use MCP tools from .apc/mcps.json, install agents from a team workspace, or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', 'run an agent', 'coordinate agents', 'MCP not working', 'install agent', 'team agents', 'apx memory', 'daemon'.",
90
+ "homepage: https://github.com/agentprojectcontext/apx",
91
+ "---",
92
+ "",
93
+ ].join("\n");
94
+ return frontmatter + content;
95
+ }
96
+
97
+ // Install APX + APC context skills into IDE rule files. Returns an array of result objects.
98
+ // targetIds: array of target ids to install; null = all project targets.
99
+ export function installIdeSkills(root, targetIds = null) {
100
+ const apxSrc = path.join(__dirname, "apx-skill.md");
101
+ const apcSrc = path.join(__dirname, "apc-context-skill.md");
102
+ if (!fs.existsSync(apxSrc)) return [];
103
+
104
+ const apxContent = fs.readFileSync(apxSrc, "utf8").trim();
105
+ const apcContent = fs.existsSync(apcSrc) ? fs.readFileSync(apcSrc, "utf8").trim() : null;
106
+
107
+ const targets = targetIds
108
+ ? IDE_TARGETS.filter((t) => targetIds.includes(t.id))
109
+ : IDE_TARGETS;
110
+
111
+ const results = [];
112
+ for (const t of targets) {
113
+ if (t.ideDir && !fs.existsSync(path.join(root, t.ideDir))) {
114
+ results.push({ ...t, status: "skipped (IDE not present)" });
115
+ continue;
116
+ }
117
+
118
+ // Install APX skill
119
+ const dest = path.join(root, t.file);
120
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
121
+ const rendered = t.render(apxContent);
122
+ if (t.append) {
123
+ const existing = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
124
+ if (t.guard && existing.includes(t.guard)) {
125
+ results.push({ ...t, status: "already installed" });
126
+ } else {
127
+ fs.appendFileSync(dest, rendered, "utf8");
128
+ results.push({ ...t, status: "appended" });
129
+ }
130
+ } else {
131
+ const existed = fs.existsSync(dest);
132
+ fs.writeFileSync(dest, rendered, "utf8");
133
+ results.push({ ...t, status: existed ? "updated" : "created" });
134
+ }
135
+
136
+ // Install APC context skill alongside (only for non-append targets with a skills dir)
137
+ if (apcContent && t.id === "claude-code") {
138
+ const apcDest = path.join(root, ".claude", "skills", "apc-context", "SKILL.md");
139
+ fs.mkdirSync(path.dirname(apcDest), { recursive: true });
140
+ const existed = fs.existsSync(apcDest);
141
+ fs.writeFileSync(apcDest, buildApcContextSkillMd(apcContent), "utf8");
142
+ results.push({ ...t, id: "claude-code/apc-context", label: "Claude Code (apc-context)", file: apcDest, status: existed ? "updated" : "created" });
143
+ }
144
+ }
145
+ return results;
146
+ }
147
+
148
+ // Install both APX and APC context skills to global ~/.../skills/ dirs.
149
+ // Returns an array of result objects with { dir, skill, status }.
150
+ export function installGlobalSkills() {
151
+ const results = [];
152
+
153
+ const apxSrc = path.join(__dirname, "apx-skill.md");
154
+ const apcSrc = path.join(__dirname, "apc-context-skill.md");
155
+
156
+ const skills = [];
157
+ if (fs.existsSync(apxSrc))
158
+ skills.push({ slug: "apx", md: buildSkillMd(fs.readFileSync(apxSrc, "utf8").trim()) });
159
+ if (fs.existsSync(apcSrc))
160
+ skills.push({ slug: "apc-context", md: buildApcContextSkillMd(fs.readFileSync(apcSrc, "utf8").trim()) });
161
+
162
+ for (const base of GLOBAL_SKILL_DIRS) {
163
+ for (const { slug, md } of skills) {
164
+ const dest = path.join(base, slug, "SKILL.md");
165
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
166
+ const existed = fs.existsSync(dest);
167
+ fs.writeFileSync(dest, md, "utf8");
168
+ results.push({ dir: base, skill: slug, file: dest, status: existed ? "updated" : "created" });
169
+ }
170
+ }
171
+ return results;
172
+ }
173
+
174
+
175
+ const AGENTS_MD_TEMPLATE = `# Agents
176
+
177
+ > This file is the contract for agents in this project.
178
+ > It follows the APC spec (https://github.com/agentprojectcontext/agentprojectcontext).
179
+
180
+ <!-- Add an agent like this:
181
+
182
+ ## sofia
183
+ - **Role**: Support
184
+ - **Model**: claude-haiku-4-5
185
+ - **Skills**: customer-support
186
+ - **Language**: es-AR
187
+
188
+ -->
189
+ `;
190
+
191
+ const APC_GITIGNORE = `# APC runtime data — local-first by default
192
+ agents/*/sessions/
193
+ sessions/
194
+ chats/
195
+ cache/
196
+ tmp/
197
+ private/
198
+ secrets/
199
+ *.local.json
200
+ *.secret.json
201
+ *.env
202
+ *.env.*
203
+ migrate.md
204
+ `;
205
+
206
+ function nowIso() {
207
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
208
+ }
209
+
210
+ // Files that carry project context but are IDE-specific — candidates for APC migration.
211
+ const SCATTERED_CONTEXT_FILES = [
212
+ { file: "CLAUDE.md", label: "Claude Code instructions" },
213
+ { file: ".cursorrules", label: "Cursor rules" },
214
+ { file: ".windsurfrules", label: "Windsurf rules" },
215
+ { file: ".clinerules", label: "Cline rules" },
216
+ { file: ".github/copilot-instructions.md", label: "GitHub Copilot instructions" },
217
+ { file: ".trae/rules/project_rules.md", label: "Trae rules" },
218
+ ];
219
+
220
+ // Returns files found in `root` that look like scattered context.
221
+ export function detectScatteredContext(root) {
222
+ return SCATTERED_CONTEXT_FILES.filter(({ file }) =>
223
+ fs.existsSync(path.join(root, file))
224
+ );
225
+ }
226
+
227
+ // Writes .apc/migrate.md so the next agent session opens with a migration offer.
228
+ function writeMigrateMd(apfDir, found) {
229
+ const lines = [
230
+ "# APC Migration Pending",
231
+ "",
232
+ "This file was created by `apx init`. It signals to agents that this project",
233
+ "has existing context files that have not yet been migrated to `.apc/`.",
234
+ "",
235
+ "**Delete this file** once migration is complete.",
236
+ "",
237
+ "## Detected files",
238
+ "",
239
+ ...found.map(({ file, label }) => `- \`${file}\` — ${label}`),
240
+ ];
241
+ fs.writeFileSync(path.join(apfDir, "migrate.md"), lines.join("\n") + "\n");
242
+ }
243
+
244
+ export function initApf(directory, { name } = {}) {
245
+ const root = path.resolve(directory);
246
+ fs.mkdirSync(root, { recursive: true });
247
+
248
+ const apfDir = path.join(root, ".apc");
249
+ fs.mkdirSync(path.join(apfDir, "agents"), { recursive: true });
250
+ fs.mkdirSync(path.join(apfDir, "skills"), { recursive: true });
251
+ fs.mkdirSync(path.join(apfDir, "commands"), { recursive: true });
252
+
253
+ const projectJson = path.join(apfDir, "project.json");
254
+ if (!fs.existsSync(projectJson)) {
255
+ fs.writeFileSync(
256
+ projectJson,
257
+ JSON.stringify(
258
+ {
259
+ name: name || path.basename(root),
260
+ version: "0.1.0",
261
+ apf: SPEC_VERSION,
262
+ created: nowIso(),
263
+ apx: null,
264
+ sessions: { defaultVisibility: "local" },
265
+ },
266
+ null,
267
+ 2
268
+ ) + "\n"
269
+ );
270
+ }
271
+
272
+ const gitignore = path.join(apfDir, ".gitignore");
273
+ if (!fs.existsSync(gitignore)) {
274
+ fs.writeFileSync(gitignore, APC_GITIGNORE);
275
+ }
276
+
277
+ const agentsMd = path.join(root, "AGENTS.md");
278
+ if (!fs.existsSync(agentsMd)) {
279
+ fs.writeFileSync(agentsMd, AGENTS_MD_TEMPLATE);
280
+ }
281
+
282
+ // Detect scattered context files and flag for agent-driven migration.
283
+ const scattered = detectScatteredContext(root);
284
+ const migrateMd = path.join(apfDir, "migrate.md");
285
+ if (scattered.length > 0 && !fs.existsSync(migrateMd)) {
286
+ writeMigrateMd(apfDir, scattered);
287
+ }
288
+
289
+ return { root, agentsMd, projectJson, pendingMigration: scattered };
290
+ }
291
+
292
+ export function ensureAgentDir(root, slug) {
293
+ const dir = path.join(root, ".apc", "agents", slug);
294
+ fs.mkdirSync(path.join(dir, "sessions"), { recursive: true });
295
+ const memory = path.join(dir, "memory.md");
296
+ if (!fs.existsSync(memory)) {
297
+ fs.writeFileSync(
298
+ memory,
299
+ `# Memory — ${slug}\n\n` +
300
+ `## Identity\n- \n\n` +
301
+ `## Long-term facts\n- \n\n` +
302
+ `## Recent context\n- \n`
303
+ );
304
+ }
305
+ return dir;
306
+ }
307
+
308
+ // Write .apc/agents/<slug>.md — the canonical agent definition file.
309
+ export function writeAgentFile(root, slug, fields, body = "") {
310
+ const dest = path.join(root, ".apc", "agents", `${slug}.md`);
311
+ const lines = ["---"];
312
+ const order = ["role", "model", "language", "description", "skills", "tools"];
313
+ const written = new Set();
314
+ for (const key of order) {
315
+ const titleKey = key.charAt(0).toUpperCase() + key.slice(1);
316
+ const v = fields[titleKey] ?? fields[key];
317
+ if (v === undefined || v === null || v === "") continue;
318
+ const value = Array.isArray(v) ? v.join(", ") : v;
319
+ lines.push(`${key}: ${value}`);
320
+ written.add(titleKey);
321
+ }
322
+ // Any extra fields not in the ordered list
323
+ for (const [k, v] of Object.entries(fields)) {
324
+ const titleKey = k.charAt(0).toUpperCase() + k.slice(1);
325
+ if (written.has(titleKey) || v === undefined || v === null || v === "") continue;
326
+ const value = Array.isArray(v) ? v.join(", ") : v;
327
+ lines.push(`${k.toLowerCase()}: ${value}`);
328
+ }
329
+ lines.push("---");
330
+ if (body) lines.push("", body);
331
+ fs.writeFileSync(dest, lines.join("\n") + "\n");
332
+ }
333
+
334
+ // Write a vault agent template to ~/.apx/agents/<slug>.md
335
+ export function writeVaultAgentFile(slug, fields, body = "") {
336
+ fs.mkdirSync(VAULT_DIR, { recursive: true });
337
+ const dest = path.join(VAULT_DIR, `${slug}.md`);
338
+ const lines = ["---"];
339
+ const order = ["role", "model", "language", "description", "skills", "tools"];
340
+ const written = new Set();
341
+ for (const key of order) {
342
+ const titleKey = key.charAt(0).toUpperCase() + key.slice(1);
343
+ const v = fields[titleKey] ?? fields[key];
344
+ if (v === undefined || v === null || v === "") continue;
345
+ lines.push(`${key}: ${Array.isArray(v) ? v.join(", ") : v}`);
346
+ written.add(titleKey);
347
+ }
348
+ for (const [k, v] of Object.entries(fields)) {
349
+ const titleKey = k.charAt(0).toUpperCase() + k.slice(1);
350
+ if (written.has(titleKey) || v === undefined || v === null || v === "") continue;
351
+ lines.push(`${k.toLowerCase()}: ${Array.isArray(v) ? v.join(", ") : v}`);
352
+ }
353
+ lines.push("---");
354
+ if (body) lines.push("", body);
355
+ fs.writeFileSync(dest, lines.join("\n") + "\n");
356
+ }
357
+
358
+ // Add a slug to the project's agents.imported list in project.json
359
+ export function addImportedAgent(root, slug) {
360
+ const p = path.join(root, ".apc", "project.json");
361
+ let cfg = {};
362
+ try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
363
+ if (!cfg.agents) cfg.agents = {};
364
+ if (!cfg.agents.imported) cfg.agents.imported = [];
365
+ if (!cfg.agents.imported.includes(slug)) cfg.agents.imported.push(slug);
366
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n");
367
+ }
368
+
369
+ // Regenerate AGENTS.md from .apc/agents/*.md for Codex/Antigravity compat.
370
+ export function regenerateAgentsMd(root) {
371
+ const agents = readAgents(root);
372
+ const header = [
373
+ "# Agents",
374
+ "",
375
+ "> Auto-generated from .apc/agents/*.md — edit individual agent files, not this file.",
376
+ "> Read by Codex, Antigravity, and other tools that follow the AGENTS.md convention.",
377
+ "",
378
+ ].join("\n");
379
+
380
+ if (agents.length === 0) {
381
+ fs.writeFileSync(path.join(root, "AGENTS.md"), header);
382
+ return;
383
+ }
384
+
385
+ const blocks = agents.map((a) => {
386
+ const tag = a.source === "vault" ? " <!-- vault -->" : "";
387
+ return renderAgentBlock(a.slug, a.fields) + tag;
388
+ });
389
+ fs.writeFileSync(path.join(root, "AGENTS.md"), header + blocks.join("\n\n") + "\n");
390
+ }
391
+
392
+ export function appendAgentToAgentsMd(root, slug, fields) {
393
+ const agentsMdPath = path.join(root, "AGENTS.md");
394
+ let text = fs.existsSync(agentsMdPath)
395
+ ? fs.readFileSync(agentsMdPath, "utf8")
396
+ : AGENTS_MD_TEMPLATE;
397
+
398
+ if (!/^#\s+Agents\s*$/im.test(text)) {
399
+ text = `# Agents\n\n${text}`;
400
+ }
401
+
402
+ const block = renderAgentBlock(slug, fields);
403
+
404
+ if (!text.endsWith("\n")) text += "\n";
405
+ text += `\n${block}\n`;
406
+ fs.writeFileSync(agentsMdPath, text);
407
+ }
408
+
409
+ export function renderAgentBlock(slug, fields) {
410
+ const lines = [`## ${slug}`];
411
+ for (const [k, v] of Object.entries(fields)) {
412
+ if (v === undefined || v === null || v === "") continue;
413
+ const value = Array.isArray(v) ? v.join(", ") : v;
414
+ lines.push(`- **${k}**: ${value}`);
415
+ }
416
+ return lines.join("\n");
417
+ }
@@ -0,0 +1,36 @@
1
+ // Session id generator: YYYY-MM-DD-NN, auto-incremented per agent per UTC day.
2
+ // Mirrors cli/src/id.js — kept duplicated so the daemon doesn't import from
3
+ // the CLI module tree.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ export function generateSessionId(projectRoot, agentSlug) {
9
+ const today = new Date().toISOString().slice(0, 10);
10
+ const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "sessions");
11
+ let next = 1;
12
+ if (fs.existsSync(dir)) {
13
+ for (const f of fs.readdirSync(dir)) {
14
+ const m = f.match(new RegExp(`^${today}-(\\d{2,})\\.md$`));
15
+ if (m) {
16
+ const n = parseInt(m[1], 10);
17
+ if (n + 1 > next) next = n + 1;
18
+ }
19
+ }
20
+ }
21
+ return `${today}-${String(next).padStart(2, "0")}`;
22
+ }
23
+
24
+ export function readSessionFrontmatter(filePath) {
25
+ if (!fs.existsSync(filePath)) return null;
26
+ const text = fs.readFileSync(filePath, "utf8");
27
+ if (!text.startsWith("---\n")) return null;
28
+ const end = text.indexOf("\n---", 4);
29
+ if (end === -1) return null;
30
+ const fm = {};
31
+ for (const line of text.slice(4, end).split("\n")) {
32
+ const m = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
33
+ if (m) fm[m[1]] = m[2].trim();
34
+ }
35
+ return { fm, body: text.slice(end + 4).replace(/^\n+/, "") };
36
+ }
@@ -0,0 +1,123 @@
1
+ // Helpers that wrap external runtimes (Claude Code, Codex, OpenCode, Aider)
2
+ // with APC awareness:
3
+ //
4
+ // 1. Create an APC session BEFORE the runtime starts.
5
+ // 2. Inject an "APC Runtime Context" block into the system prompt so the
6
+ // runtime knows the session id, the cwd of the project, and the apx
7
+ // commands it can use to update memory / append session notes.
8
+ // 3. After the runtime returns, capture the external transcript path
9
+ // (Claude Code gives one, Codex/OpenCode/Aider don't yet) and write it
10
+ // into the APC session frontmatter.
11
+ // 4. Close the session with a synthesised result (truncated stdout).
12
+ //
13
+ // Used by both POST /projects/:pid/agents/:slug/runtime (CLI) and the
14
+ // super-agent's call_runtime tool.
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { generateSessionId } from "../core/session-store.js";
19
+
20
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
21
+
22
+ const APC_RUNTIME_HINT = `
23
+ # APC Runtime Context
24
+
25
+ You are running inside an APC (Agent Project Framework) project. APC gives you durable session state across runs.
26
+
27
+ - **Project**: {{name}} ({{path}})
28
+ - **Agent**: {{agent}}
29
+ - **APC session id**: {{session_id}}
30
+ (filename: .apc/agents/{{agent}}/sessions/{{session_id}}.md)
31
+
32
+ ## Commands you can use during this run
33
+
34
+ - \`apx memory {{agent}} --append "<note>"\` save a long-term fact for this agent
35
+ - \`apx session update {{session_id}} --status "..."\` update the session status
36
+ - \`apx session update {{session_id}} --task-ref TASK-...\` link to an external task
37
+
38
+ ## When you finish
39
+
40
+ Close the session with a one-line result so a future operator (or apx session resume) can summarize:
41
+
42
+ apx session close {{session_id}} --result "<one-line summary of what you did>"
43
+
44
+ If you cannot run apx (sandboxed shell), just print the result on the last line of your output prefixed with "APC_RESULT:" and APX will capture it automatically.
45
+ `.trim();
46
+
47
+ export function buildApfHint({ projectName, projectPath, agentSlug, sessionId }) {
48
+ return APC_RUNTIME_HINT
49
+ .replace(/\{\{name\}\}/g, projectName)
50
+ .replace(/\{\{path\}\}/g, projectPath)
51
+ .replace(/\{\{agent\}\}/g, agentSlug)
52
+ .replace(/\{\{session_id\}\}/g, sessionId);
53
+ }
54
+
55
+ // Create the APC session file on disk. Returns { id, filename, path }.
56
+ export function createRuntimeSession({ projectRoot, agentSlug, runtime, taskRef = "", title }) {
57
+ const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "sessions");
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ const id = generateSessionId(projectRoot, agentSlug);
60
+ const file = path.join(dir, `${id}.md`);
61
+ const started = nowIso();
62
+ const sessionTitle = title || `Runtime: ${runtime}`;
63
+ const body =
64
+ `---\n` +
65
+ `id: ${id}\n` +
66
+ `agent: ${agentSlug}\n` +
67
+ `title: ${sessionTitle}\n` +
68
+ `task_ref: ${taskRef}\n` +
69
+ `status: 🔄 En progreso\n` +
70
+ `started: ${started}\n` +
71
+ `completed: \n` +
72
+ `result: \n` +
73
+ `runtime: ${runtime}\n` +
74
+ `external_session_path: \n` +
75
+ `---\n\n` +
76
+ `# ${sessionTitle}\n\n`;
77
+ fs.writeFileSync(file, body);
78
+ return { id, filename: `${id}.md`, path: file };
79
+ }
80
+
81
+ // Update session frontmatter with the external transcript path captured from
82
+ // the runtime adapter. Closes the session with a result string.
83
+ export function closeRuntimeSession({ filePath, externalSessionPath, exitCode, result }) {
84
+ let text = fs.readFileSync(filePath, "utf8");
85
+ text = setField(text, "completed", nowIso());
86
+ if (externalSessionPath) {
87
+ text = setField(text, "external_session_path", externalSessionPath);
88
+ }
89
+ if (typeof exitCode === "number") {
90
+ text = setField(text, "result", `${exitCode === 0 ? "✅" : "⚠️"} exit ${exitCode}: ${(result || "").slice(0, 200)}`);
91
+ } else if (result) {
92
+ text = setField(text, "result", result.slice(0, 300));
93
+ }
94
+ text = setField(text, "status", exitCode === 0 ? "✅ Completada" : "⚠️ Cerrada con error");
95
+ fs.writeFileSync(filePath, text);
96
+ }
97
+
98
+ function setField(text, field, value) {
99
+ if (!text.startsWith("---\n")) return text;
100
+ const end = text.indexOf("\n---", 4);
101
+ if (end === -1) return text;
102
+ const fmText = text.slice(4, end);
103
+ const lines = fmText.split("\n");
104
+ let found = false;
105
+ const out = lines.map((line) => {
106
+ if (line.startsWith(`${field}:`)) {
107
+ found = true;
108
+ return `${field}: ${value}`;
109
+ }
110
+ return line;
111
+ });
112
+ if (!found) out.push(`${field}: ${value}`);
113
+ return `---\n${out.join("\n")}\n---${text.slice(end + 4)}`;
114
+ }
115
+
116
+ // Look at the runtime's stdout for a self-reported "APC_RESULT: ..." line
117
+ // (the convention printed in the hint above). Returns the captured string or
118
+ // null. This is the fallback for runtimes that can't shell out.
119
+ export function extractApfResult(stdout) {
120
+ if (!stdout || typeof stdout !== "string") return null;
121
+ const m = stdout.match(/^APC_RESULT:\s*(.+?)\s*$/m);
122
+ return m ? m[1].trim() : null;
123
+ }