@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.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/package.json +52 -0
- package/skills/apx/SKILL.md +77 -0
- package/src/cli/commands/a2a.js +66 -0
- package/src/cli/commands/agent.js +181 -0
- package/src/cli/commands/chat.js +84 -0
- package/src/cli/commands/command.js +42 -0
- package/src/cli/commands/config.js +56 -0
- package/src/cli/commands/daemon.js +148 -0
- package/src/cli/commands/exec.js +56 -0
- package/src/cli/commands/identity.js +146 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/mcp.js +147 -0
- package/src/cli/commands/memory.js +69 -0
- package/src/cli/commands/messages.js +61 -0
- package/src/cli/commands/plugins.js +23 -0
- package/src/cli/commands/project.js +124 -0
- package/src/cli/commands/routine.js +99 -0
- package/src/cli/commands/runtime.js +64 -0
- package/src/cli/commands/session.js +387 -0
- package/src/cli/commands/skills.js +153 -0
- package/src/cli/commands/telegram.js +48 -0
- package/src/cli/http.js +102 -0
- package/src/cli/index.js +481 -0
- package/src/cli/postinstall.js +25 -0
- package/src/core/apc-context-skill.md +150 -0
- package/src/core/apx-skill.md +78 -0
- package/src/core/config.js +129 -0
- package/src/core/identity.js +23 -0
- package/src/core/messages-store.js +421 -0
- package/src/core/parser.js +217 -0
- package/src/core/routines-store.js +144 -0
- package/src/core/scaffold.js +417 -0
- package/src/core/session-store.js +36 -0
- package/src/daemon/apc-runtime-context.js +123 -0
- package/src/daemon/api.js +946 -0
- package/src/daemon/compact.js +140 -0
- package/src/daemon/conversations.js +108 -0
- package/src/daemon/db.js +81 -0
- package/src/daemon/engines/anthropic.js +58 -0
- package/src/daemon/engines/gemini.js +55 -0
- package/src/daemon/engines/index.js +65 -0
- package/src/daemon/engines/mock.js +18 -0
- package/src/daemon/engines/ollama.js +66 -0
- package/src/daemon/engines/openai.js +58 -0
- package/src/daemon/env-detect.js +69 -0
- package/src/daemon/index.js +156 -0
- package/src/daemon/mcp-runner.js +218 -0
- package/src/daemon/mcp-sources.js +114 -0
- package/src/daemon/plugins/index.js +91 -0
- package/src/daemon/plugins/telegram.js +549 -0
- package/src/daemon/project-config.js +98 -0
- package/src/daemon/routines.js +211 -0
- package/src/daemon/runtimes/_spawn.js +44 -0
- package/src/daemon/runtimes/aider.js +32 -0
- package/src/daemon/runtimes/claude-code.js +60 -0
- package/src/daemon/runtimes/codex.js +30 -0
- package/src/daemon/runtimes/index.js +39 -0
- package/src/daemon/runtimes/opencode.js +28 -0
- package/src/daemon/smoke.js +54 -0
- package/src/daemon/super-agent-tools.js +539 -0
- package/src/daemon/super-agent.js +188 -0
- package/src/daemon/thinking.js +45 -0
- package/src/daemon/tool-call-parser.js +116 -0
- package/src/daemon/wakeup.js +92 -0
- 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
|
+
}
|