@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,946 @@
|
|
|
1
|
+
// Express REST API for APX. See docs/APX-DAEMON.md §4.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { readApfMcps, writeApfMcps, SOURCES } from "./mcp-sources.js";
|
|
6
|
+
import { callEngine, ENGINE_IDS } from "./engines/index.js";
|
|
7
|
+
import { getRuntime, RUNTIME_IDS } from "./runtimes/index.js";
|
|
8
|
+
import { detectAll } from "./env-detect.js";
|
|
9
|
+
import {
|
|
10
|
+
startConversation,
|
|
11
|
+
appendTurn,
|
|
12
|
+
readConversation,
|
|
13
|
+
listConversations,
|
|
14
|
+
conversationPath,
|
|
15
|
+
setStatus,
|
|
16
|
+
} from "./conversations.js";
|
|
17
|
+
import { compactConversation } from "./compact.js";
|
|
18
|
+
import {
|
|
19
|
+
readProjectConfig,
|
|
20
|
+
writeProjectConfig,
|
|
21
|
+
setDottedKey,
|
|
22
|
+
unsetDottedKey,
|
|
23
|
+
} from "./project-config.js";
|
|
24
|
+
import {
|
|
25
|
+
listRoutines,
|
|
26
|
+
getRoutine,
|
|
27
|
+
upsertRoutine,
|
|
28
|
+
deleteRoutine,
|
|
29
|
+
setEnabled as setRoutineEnabled,
|
|
30
|
+
runRoutineNow,
|
|
31
|
+
} from "./routines.js";
|
|
32
|
+
import {
|
|
33
|
+
buildApfHint,
|
|
34
|
+
createRuntimeSession,
|
|
35
|
+
closeRuntimeSession,
|
|
36
|
+
extractApfResult,
|
|
37
|
+
} from "./apc-runtime-context.js";
|
|
38
|
+
import { readSessionFrontmatter } from "../core/session-store.js";
|
|
39
|
+
import { runSuperAgent, isSuperAgentEnabled } from "./super-agent.js";
|
|
40
|
+
import { readGlobalMessages, readProjectMessages, searchProjectMessages } from "../core/messages-store.js";
|
|
41
|
+
import { readAgents } from "../core/parser.js";
|
|
42
|
+
import { parseSessionFrontmatter } from "../core/parser.js";
|
|
43
|
+
import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
|
|
44
|
+
|
|
45
|
+
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
46
|
+
|
|
47
|
+
export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
|
|
48
|
+
const telegram = plugins?.get("telegram");
|
|
49
|
+
|
|
50
|
+
const app = express();
|
|
51
|
+
app.use(express.json({ limit: "2mb" }));
|
|
52
|
+
|
|
53
|
+
// ---- Health -------------------------------------------------------
|
|
54
|
+
app.get("/health", (_req, res) => {
|
|
55
|
+
res.json({
|
|
56
|
+
status: "ok",
|
|
57
|
+
version,
|
|
58
|
+
uptime_s: Math.round((Date.now() - startedAt) / 1000),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---- Projects -----------------------------------------------------
|
|
63
|
+
app.get("/projects", (_req, res) => res.json(projects.list()));
|
|
64
|
+
|
|
65
|
+
app.post("/projects", (req, res) => {
|
|
66
|
+
const { path: p } = req.body || {};
|
|
67
|
+
if (!p) return res.status(400).json({ error: "path required" });
|
|
68
|
+
try {
|
|
69
|
+
const entry = projects.register(p);
|
|
70
|
+
addProjectGlobally(entry.path);
|
|
71
|
+
registries.ensure(entry);
|
|
72
|
+
res.status(201).json({ id: entry.id, path: entry.path });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
res.status(400).json({ error: e.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.delete("/projects/:id", (req, res) => {
|
|
79
|
+
const ok = projects.unregister(req.params.id);
|
|
80
|
+
res.status(ok ? 204 : 404).end();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.post("/projects/:id/rebuild", (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = projects.rebuild(req.params.id);
|
|
86
|
+
res.json({ ok: true, ...result });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
res.status(400).json({ error: e.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---- Helper -------------------------------------------------------
|
|
93
|
+
function project(req, res) {
|
|
94
|
+
const p = projects.get(req.params.pid);
|
|
95
|
+
if (!p) {
|
|
96
|
+
res.status(404).json({ error: "project not found" });
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return p;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- Agents -------------------------------------------------------
|
|
103
|
+
app.get("/projects/:pid/agents", (req, res) => {
|
|
104
|
+
const p = project(req, res);
|
|
105
|
+
if (!p) return;
|
|
106
|
+
res.json(readAgents(p.path).map(agentToResponse));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
app.get("/projects/:pid/agents/:slug", (req, res) => {
|
|
110
|
+
const p = project(req, res);
|
|
111
|
+
if (!p) return;
|
|
112
|
+
const agents = readAgents(p.path);
|
|
113
|
+
const a = agents.find((x) => x.slug === req.params.slug);
|
|
114
|
+
if (!a) return res.status(404).json({ error: "agent not found" });
|
|
115
|
+
const memPath = path.join(p.path, ".apc", "agents", a.slug, "memory.md");
|
|
116
|
+
const memory = fs.existsSync(memPath) ? fs.readFileSync(memPath, "utf8") : "";
|
|
117
|
+
res.json({ ...agentToResponse(a), memory });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.post("/projects/:pid/agents", (req, res) => {
|
|
121
|
+
const p = project(req, res);
|
|
122
|
+
if (!p) return;
|
|
123
|
+
const { slug, role, model, skills, language, description, tools } = req.body || {};
|
|
124
|
+
if (!slug) return res.status(400).json({ error: "slug required" });
|
|
125
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(slug))
|
|
126
|
+
return res.status(400).json({ error: "invalid slug" });
|
|
127
|
+
const existing = readAgents(p.path).find((a) => a.slug === slug);
|
|
128
|
+
if (existing) return res.status(400).json({ error: `agent ${slug} already exists` });
|
|
129
|
+
try {
|
|
130
|
+
writeAgentFile(p.path, slug, {
|
|
131
|
+
Role: role || null,
|
|
132
|
+
Model: model || null,
|
|
133
|
+
Language: language || null,
|
|
134
|
+
Description: description || null,
|
|
135
|
+
Skills: skills || [],
|
|
136
|
+
Tools: tools || [],
|
|
137
|
+
});
|
|
138
|
+
ensureAgentDir(p.path, slug);
|
|
139
|
+
regenerateAgentsMd(p.path);
|
|
140
|
+
projects.rebuild(p.id);
|
|
141
|
+
const created = readAgents(p.path).find((a) => a.slug === slug);
|
|
142
|
+
res.status(201).json(agentToResponse(created));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
res.status(400).json({ error: e.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---- Memory -------------------------------------------------------
|
|
149
|
+
app.get("/projects/:pid/agents/:slug/memory", (req, res) => {
|
|
150
|
+
const p = project(req, res);
|
|
151
|
+
if (!p) return;
|
|
152
|
+
const memPath = path.join(p.path, ".apc", "agents", req.params.slug, "memory.md");
|
|
153
|
+
if (!fs.existsSync(memPath)) return res.json({ body: "" });
|
|
154
|
+
res.json({ body: fs.readFileSync(memPath, "utf8") });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.put("/projects/:pid/agents/:slug/memory", (req, res) => {
|
|
158
|
+
const p = project(req, res);
|
|
159
|
+
if (!p) return;
|
|
160
|
+
const { body } = req.body || {};
|
|
161
|
+
if (typeof body !== "string")
|
|
162
|
+
return res.status(400).json({ error: "body must be string" });
|
|
163
|
+
const dir = path.join(p.path, ".apc", "agents", req.params.slug);
|
|
164
|
+
fs.mkdirSync(path.join(dir, "sessions"), { recursive: true });
|
|
165
|
+
const memPath = path.join(dir, "memory.md");
|
|
166
|
+
fs.writeFileSync(memPath, body);
|
|
167
|
+
projects.rebuild(p.id);
|
|
168
|
+
res.json({ ok: true, bytes: Buffer.byteLength(body, "utf8") });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ---- Sessions -----------------------------------------------------
|
|
172
|
+
app.get("/projects/:pid/agents/:slug/sessions", (req, res) => {
|
|
173
|
+
const p = project(req, res);
|
|
174
|
+
if (!p) return;
|
|
175
|
+
const agents = readAgents(p.path);
|
|
176
|
+
if (!agents.find((a) => a.slug === req.params.slug))
|
|
177
|
+
return res.status(404).json({ error: "agent not found" });
|
|
178
|
+
const sessionsDir = path.join(p.path, ".apc", "agents", req.params.slug, "sessions");
|
|
179
|
+
if (!fs.existsSync(sessionsDir)) return res.json([]);
|
|
180
|
+
const sessions = fs
|
|
181
|
+
.readdirSync(sessionsDir)
|
|
182
|
+
.filter((f) => f.endsWith(".md"))
|
|
183
|
+
.sort()
|
|
184
|
+
.reverse()
|
|
185
|
+
.map((f) => {
|
|
186
|
+
const text = fs.readFileSync(path.join(sessionsDir, f), "utf8");
|
|
187
|
+
const fm = parseSessionFrontmatter(text);
|
|
188
|
+
const titleFromFile = f.replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(/\.md$/, "");
|
|
189
|
+
return {
|
|
190
|
+
filename: f,
|
|
191
|
+
title: fm.title || titleFromFile,
|
|
192
|
+
started_at: fm.started || null,
|
|
193
|
+
ended_at: fm.ended || null,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
res.json(sessions);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
app.post("/projects/:pid/agents/:slug/sessions", (req, res) => {
|
|
200
|
+
const p = project(req, res);
|
|
201
|
+
if (!p) return;
|
|
202
|
+
const { title, body = "" } = req.body || {};
|
|
203
|
+
if (!title) return res.status(400).json({ error: "title required" });
|
|
204
|
+
const sessionsDir = path.join(p.path, ".apc", "agents", req.params.slug, "sessions");
|
|
205
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
206
|
+
const titleSlug =
|
|
207
|
+
title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "session";
|
|
208
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
209
|
+
let candidate = path.join(sessionsDir, `${today}-${titleSlug}.md`);
|
|
210
|
+
let n = 2;
|
|
211
|
+
while (fs.existsSync(candidate)) {
|
|
212
|
+
candidate = path.join(sessionsDir, `${today}-${titleSlug}-${n}.md`);
|
|
213
|
+
n++;
|
|
214
|
+
}
|
|
215
|
+
const started = nowIso();
|
|
216
|
+
const content = `---\ntitle: ${title}\nstarted: ${started}\n---\n\n# ${title}\n\n${body}\n`;
|
|
217
|
+
fs.writeFileSync(candidate, content);
|
|
218
|
+
projects.rebuild(p.id);
|
|
219
|
+
res.status(201).json({ filename: path.basename(candidate), path: candidate });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// GET session by filename (sid may include or omit the .md extension)
|
|
223
|
+
app.get("/projects/:pid/sessions/:sid", (req, res) => {
|
|
224
|
+
const p = project(req, res);
|
|
225
|
+
if (!p) return;
|
|
226
|
+
const sid = req.params.sid;
|
|
227
|
+
const filename = sid.endsWith(".md") ? sid : `${sid}.md`;
|
|
228
|
+
const agentsDir = path.join(p.path, ".apc", "agents");
|
|
229
|
+
let found = null;
|
|
230
|
+
if (fs.existsSync(agentsDir)) {
|
|
231
|
+
for (const slug of fs.readdirSync(agentsDir)) {
|
|
232
|
+
const f = path.join(agentsDir, slug, "sessions", filename);
|
|
233
|
+
if (fs.existsSync(f)) {
|
|
234
|
+
const text = fs.readFileSync(f, "utf8");
|
|
235
|
+
const fm = parseSessionFrontmatter(text);
|
|
236
|
+
found = { filename, agent: slug, ...fm, body_md: text };
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!found) return res.status(404).json({ error: "session not found" });
|
|
242
|
+
res.json(found);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ---- MCPs ---------------------------------------------------------
|
|
246
|
+
app.get("/projects/:pid/mcps", (req, res) => {
|
|
247
|
+
const p = project(req, res);
|
|
248
|
+
if (!p) return;
|
|
249
|
+
res.json(registries.for(p).list());
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.post("/projects/:pid/mcps", (req, res) => {
|
|
253
|
+
const p = project(req, res);
|
|
254
|
+
if (!p) return;
|
|
255
|
+
const { name, command, args, env, url, headers, enabled } = req.body || {};
|
|
256
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
257
|
+
if (!command && !url)
|
|
258
|
+
return res.status(400).json({ error: "either command or url required" });
|
|
259
|
+
|
|
260
|
+
const json = readApfMcps(p.path);
|
|
261
|
+
json.mcpServers = json.mcpServers || {};
|
|
262
|
+
const existing = json.mcpServers[name] || {};
|
|
263
|
+
json.mcpServers[name] = {
|
|
264
|
+
...existing,
|
|
265
|
+
...(command !== undefined ? { command } : {}),
|
|
266
|
+
...(args !== undefined ? { args } : {}),
|
|
267
|
+
...(env !== undefined ? { env } : {}),
|
|
268
|
+
...(url !== undefined ? { url } : {}),
|
|
269
|
+
...(headers !== undefined ? { headers } : {}),
|
|
270
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
271
|
+
};
|
|
272
|
+
writeApfMcps(p.path, json);
|
|
273
|
+
registries.for(p).evict(name);
|
|
274
|
+
projects.rebuild(p.id);
|
|
275
|
+
const entry = registries.for(p).getByName(name);
|
|
276
|
+
res.status(201).json(entry);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
app.delete("/projects/:pid/mcps/:name", (req, res) => {
|
|
280
|
+
const p = project(req, res);
|
|
281
|
+
if (!p) return;
|
|
282
|
+
const json = readApfMcps(p.path);
|
|
283
|
+
if (!json.mcpServers || !(req.params.name in (json.mcpServers || {}))) {
|
|
284
|
+
const all = registries.for(p).list();
|
|
285
|
+
const m = all.find((x) => x.name === req.params.name);
|
|
286
|
+
if (m && m.source !== "apc") {
|
|
287
|
+
return res.status(409).json({
|
|
288
|
+
error: `MCP "${req.params.name}" comes from "${m.source}" config — not APC-owned, cannot be removed by apx. Edit ${SOURCES.find((s) => s.id === m.source)?.file} directly.`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return res.status(404).end();
|
|
292
|
+
}
|
|
293
|
+
delete json.mcpServers[req.params.name];
|
|
294
|
+
writeApfMcps(p.path, json);
|
|
295
|
+
registries.for(p).evict(req.params.name);
|
|
296
|
+
projects.rebuild(p.id);
|
|
297
|
+
res.status(204).end();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
app.get("/projects/:pid/mcps/check", (req, res) => {
|
|
301
|
+
const p = project(req, res);
|
|
302
|
+
if (!p) return;
|
|
303
|
+
const reg = registries.for(p);
|
|
304
|
+
res.json({
|
|
305
|
+
sources: SOURCES.map((s) => ({
|
|
306
|
+
id: s.id,
|
|
307
|
+
file: s.file,
|
|
308
|
+
present: fs.existsSync(path.join(p.path, s.file)),
|
|
309
|
+
})),
|
|
310
|
+
entries: reg.list().map((m) => ({
|
|
311
|
+
name: m.name,
|
|
312
|
+
source: m.source,
|
|
313
|
+
transport: m.transport,
|
|
314
|
+
enabled: m.enabled,
|
|
315
|
+
})),
|
|
316
|
+
conflicts: reg.conflicts(),
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
app.post("/projects/:pid/mcps/:name/call", async (req, res) => {
|
|
321
|
+
const p = project(req, res);
|
|
322
|
+
if (!p) return;
|
|
323
|
+
const { tool, params } = req.body || {};
|
|
324
|
+
if (!tool) return res.status(400).json({ error: "tool required" });
|
|
325
|
+
try {
|
|
326
|
+
const result = await registries.for(p).call(req.params.name, tool, params);
|
|
327
|
+
res.json({ result });
|
|
328
|
+
} catch (e) {
|
|
329
|
+
res.status(500).json({ error: e.message });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ---- Messages -----------------------------------------------------
|
|
334
|
+
app.get("/projects/:pid/messages", (req, res) => {
|
|
335
|
+
const p = project(req, res);
|
|
336
|
+
if (!p) return;
|
|
337
|
+
const { agent, channel, since, limit = "100" } = req.query;
|
|
338
|
+
const rows = readProjectMessages(p.path, {
|
|
339
|
+
channel: channel || undefined,
|
|
340
|
+
agent_slug: agent || undefined,
|
|
341
|
+
since: since || undefined,
|
|
342
|
+
limit: Math.min(parseInt(limit, 10) || 100, 1000),
|
|
343
|
+
});
|
|
344
|
+
res.json(rows);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
app.post("/projects/:pid/messages", (req, res) => {
|
|
348
|
+
const p = project(req, res);
|
|
349
|
+
if (!p) return;
|
|
350
|
+
const { channel, direction, agent_slug, body, meta = {}, author = null } =
|
|
351
|
+
req.body || {};
|
|
352
|
+
if (!channel || !direction || !body)
|
|
353
|
+
return res.status(400).json({ error: "channel, direction, body required" });
|
|
354
|
+
if (!["in", "out"].includes(direction))
|
|
355
|
+
return res.status(400).json({ error: "direction must be in|out" });
|
|
356
|
+
const r = p.logMessage({ agent_slug: agent_slug || null, channel, direction, author, body, meta });
|
|
357
|
+
res.status(201).json({ ok: true, ts: r.ts });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
app.get("/projects/:pid/messages/search", (req, res) => {
|
|
361
|
+
const p = project(req, res);
|
|
362
|
+
if (!p) return;
|
|
363
|
+
const { q, limit = "50" } = req.query;
|
|
364
|
+
if (!q) return res.status(400).json({ error: "q required" });
|
|
365
|
+
res.json(searchProjectMessages(p.path, q, Math.min(parseInt(limit, 10) || 50, 500)));
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ---- Global messages (cross-project channels: telegram, direct, …) ----
|
|
369
|
+
app.get("/messages/global", (req, res) => {
|
|
370
|
+
const { channel, limit = "100", since } = req.query;
|
|
371
|
+
const lim = Math.min(parseInt(limit, 10) || 100, 1000);
|
|
372
|
+
const rows = readGlobalMessages({ channel: channel || undefined, limit: lim, since });
|
|
373
|
+
res.json(rows);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ---- Telegram -----------------------------------------------------
|
|
377
|
+
app.get("/telegram/status", (_req, res) => {
|
|
378
|
+
if (!telegram) return res.json({ enabled: false, channels: [] });
|
|
379
|
+
res.json(telegram.status());
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
app.post("/telegram/send", async (req, res) => {
|
|
383
|
+
const { chat_id, text, channel } = req.body || {};
|
|
384
|
+
if (!text) return res.status(400).json({ error: "text required" });
|
|
385
|
+
if (!telegram) return res.status(503).json({ error: "telegram plugin not loaded" });
|
|
386
|
+
try {
|
|
387
|
+
const r = await telegram.send({ chat_id, text, channel });
|
|
388
|
+
res.status(202).json({ ok: true, message_id: r.message_id });
|
|
389
|
+
} catch (e) {
|
|
390
|
+
res.status(502).json({ error: e.message });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ---- Plugins -----------------------------------------------------
|
|
395
|
+
app.get("/plugins", (_req, res) => {
|
|
396
|
+
if (!plugins) return res.json({});
|
|
397
|
+
res.json(plugins.status());
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
app.get("/plugins/:id/status", (req, res) => {
|
|
401
|
+
if (!plugins) return res.status(404).end();
|
|
402
|
+
const inst = plugins.get(req.params.id);
|
|
403
|
+
if (!inst) return res.status(404).json({ error: `plugin ${req.params.id} not loaded` });
|
|
404
|
+
res.json(inst.status?.() || {});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ---- Engines & Conversations -------------------------------------
|
|
408
|
+
app.get("/engines", (_req, res) => res.json({ engines: ENGINE_IDS }));
|
|
409
|
+
|
|
410
|
+
// POST /projects/:pid/agents/:slug/exec
|
|
411
|
+
app.post("/projects/:pid/agents/:slug/exec", async (req, res) => {
|
|
412
|
+
const p = project(req, res);
|
|
413
|
+
if (!p) return;
|
|
414
|
+
const { prompt, model: modelOverride, temperature, maxTokens } = req.body || {};
|
|
415
|
+
if (!prompt) return res.status(400).json({ error: "prompt required" });
|
|
416
|
+
const agents = readAgents(p.path);
|
|
417
|
+
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
418
|
+
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
419
|
+
const modelId = modelOverride || agent.fields.Model;
|
|
420
|
+
if (!modelId) return res.status(400).json({ error: "agent has no model and none provided" });
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const system = buildAgentSystem(p, agent);
|
|
424
|
+
const conv = startConversation({ projectRoot: p.path, agentSlug: agent.slug, engine: modelId, system });
|
|
425
|
+
appendTurn({ filePath: conv.path, role: "user", content: prompt });
|
|
426
|
+
|
|
427
|
+
const result = await callEngine({
|
|
428
|
+
modelId,
|
|
429
|
+
system,
|
|
430
|
+
messages: [{ role: "user", content: prompt }],
|
|
431
|
+
config: p.config || config,
|
|
432
|
+
temperature,
|
|
433
|
+
maxTokens,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
appendTurn({ filePath: conv.path, role: "assistant", content: result.text });
|
|
437
|
+
setStatus(conv.path, "closed");
|
|
438
|
+
|
|
439
|
+
p.logMessage({ agent_slug: agent.slug, channel: "engine", direction: "in", author: "user", body: prompt, meta: { conversation: conv.id } });
|
|
440
|
+
p.logMessage({ agent_slug: agent.slug, channel: "engine", direction: "out", author: agent.slug, body: result.text, meta: { conversation: conv.id, usage: result.usage } });
|
|
441
|
+
|
|
442
|
+
projects.rebuild(p.id);
|
|
443
|
+
res.json({
|
|
444
|
+
conversation: { id: conv.id, filename: conv.filename, path: conv.path },
|
|
445
|
+
text: result.text,
|
|
446
|
+
usage: result.usage,
|
|
447
|
+
engine: modelId,
|
|
448
|
+
});
|
|
449
|
+
} catch (e) {
|
|
450
|
+
res.status(500).json({ error: e.message });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// POST /projects/:pid/agents/:slug/chat
|
|
455
|
+
app.post("/projects/:pid/agents/:slug/chat", async (req, res) => {
|
|
456
|
+
const p = project(req, res);
|
|
457
|
+
if (!p) return;
|
|
458
|
+
const { prompt, conversation_id, model: modelOverride, temperature, maxTokens } = req.body || {};
|
|
459
|
+
if (!prompt) return res.status(400).json({ error: "prompt required" });
|
|
460
|
+
const agents = readAgents(p.path);
|
|
461
|
+
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
462
|
+
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
463
|
+
const modelId = modelOverride || agent.fields.Model;
|
|
464
|
+
if (!modelId) return res.status(400).json({ error: "agent has no model and none provided" });
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
let convPath;
|
|
468
|
+
let convId;
|
|
469
|
+
let history = [];
|
|
470
|
+
let compactSummary = null;
|
|
471
|
+
|
|
472
|
+
if (conversation_id) {
|
|
473
|
+
const existing = readConversation(p.path, agent.slug, conversation_id);
|
|
474
|
+
if (!existing) return res.status(404).json({ error: `conversation ${conversation_id} not found` });
|
|
475
|
+
convPath = existing.path;
|
|
476
|
+
convId = conversation_id;
|
|
477
|
+
// Extract compact summary if present — inject into system instead of messages.
|
|
478
|
+
const compactTurn = existing.turns.find((t) => t.role === "compact");
|
|
479
|
+
if (compactTurn) {
|
|
480
|
+
// Strip the "[Compacted N turns on ...]" header line from the summary body
|
|
481
|
+
compactSummary = compactTurn.content.replace(/^\[Compacted \d+ turns.*?\]\n\n?/, "").trim();
|
|
482
|
+
}
|
|
483
|
+
history = existing.turns
|
|
484
|
+
.filter((t) => t.role === "user" || t.role === "assistant")
|
|
485
|
+
.map((t) => ({ role: t.role, content: t.content }));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Build system prompt — inject compact summary if this conversation was compacted.
|
|
489
|
+
const extraParts = compactSummary
|
|
490
|
+
? [`## Contexto de conversación anterior (compactado)\n${compactSummary}`]
|
|
491
|
+
: [];
|
|
492
|
+
const system = buildAgentSystem(p, agent, { extraParts });
|
|
493
|
+
|
|
494
|
+
if (!conversation_id) {
|
|
495
|
+
const conv = startConversation({ projectRoot: p.path, agentSlug: agent.slug, engine: modelId, system });
|
|
496
|
+
convPath = conv.path;
|
|
497
|
+
convId = conv.id;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
appendTurn({ filePath: convPath, role: "user", content: prompt });
|
|
501
|
+
history.push({ role: "user", content: prompt });
|
|
502
|
+
|
|
503
|
+
const result = await callEngine({ modelId, system, messages: history, config: p.config || config, temperature, maxTokens });
|
|
504
|
+
appendTurn({ filePath: convPath, role: "assistant", content: result.text });
|
|
505
|
+
projects.rebuild(p.id);
|
|
506
|
+
|
|
507
|
+
res.json({
|
|
508
|
+
conversation_id: convId,
|
|
509
|
+
text: result.text,
|
|
510
|
+
usage: result.usage,
|
|
511
|
+
engine: modelId,
|
|
512
|
+
compacted: !!compactSummary,
|
|
513
|
+
});
|
|
514
|
+
} catch (e) {
|
|
515
|
+
res.status(500).json({ error: e.message });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// GET /projects/:pid/agents/:slug/conversations
|
|
520
|
+
app.get("/projects/:pid/agents/:slug/conversations", (req, res) => {
|
|
521
|
+
const p = project(req, res);
|
|
522
|
+
if (!p) return;
|
|
523
|
+
const agents = readAgents(p.path);
|
|
524
|
+
if (!agents.find((a) => a.slug === req.params.slug))
|
|
525
|
+
return res.status(404).json({ error: "agent not found" });
|
|
526
|
+
res.json(listConversations(p.path, req.params.slug));
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// GET /projects/:pid/agents/:slug/conversations/:id
|
|
530
|
+
app.get("/projects/:pid/agents/:slug/conversations/:id", (req, res) => {
|
|
531
|
+
const p = project(req, res);
|
|
532
|
+
if (!p) return;
|
|
533
|
+
const conv = readConversation(p.path, req.params.slug, req.params.id);
|
|
534
|
+
if (!conv) return res.status(404).json({ error: "conversation not found" });
|
|
535
|
+
res.json(conv);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// POST /projects/:pid/agents/:slug/compact ← compacts the latest conversation
|
|
539
|
+
// POST /projects/:pid/agents/:slug/conversations/:id/compact ← compacts a specific one
|
|
540
|
+
async function handleCompact(req, res, filename) {
|
|
541
|
+
const p = project(req, res);
|
|
542
|
+
if (!p) return;
|
|
543
|
+
const agents = readAgents(p.path);
|
|
544
|
+
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
545
|
+
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
546
|
+
const modelId = (req.body || {}).model || agent.fields.Model;
|
|
547
|
+
if (!modelId) return res.status(400).json({ error: "agent has no model" });
|
|
548
|
+
try {
|
|
549
|
+
const result = await compactConversation({
|
|
550
|
+
projectRoot: p.path,
|
|
551
|
+
agentSlug: agent.slug,
|
|
552
|
+
filename: filename || null,
|
|
553
|
+
modelId,
|
|
554
|
+
config: p.config || config,
|
|
555
|
+
});
|
|
556
|
+
res.json(result);
|
|
557
|
+
} catch (e) {
|
|
558
|
+
res.status(500).json({ error: e.message });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
app.post("/projects/:pid/agents/:slug/compact", (req, res) =>
|
|
563
|
+
handleCompact(req, res, null)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
app.post("/projects/:pid/agents/:slug/conversations/:id/compact", (req, res) =>
|
|
567
|
+
handleCompact(req, res, req.params.id)
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// ---- Agent-to-agent routing --------------------------------------
|
|
571
|
+
app.post("/projects/:pid/send", async (req, res) => {
|
|
572
|
+
const p = project(req, res);
|
|
573
|
+
if (!p) return;
|
|
574
|
+
const { from, to, body, deliver = false, _depth = 0 } = req.body || {};
|
|
575
|
+
if (!from || !to || !body)
|
|
576
|
+
return res.status(400).json({ error: "from, to, body required" });
|
|
577
|
+
if (_depth > 3)
|
|
578
|
+
return res.status(429).json({ error: "a2a depth limit (3) exceeded" });
|
|
579
|
+
|
|
580
|
+
const agents = readAgents(p.path);
|
|
581
|
+
const fromAgent = agents.find((a) => a.slug === from);
|
|
582
|
+
const toAgent = agents.find((a) => a.slug === to);
|
|
583
|
+
if (!fromAgent) return res.status(404).json({ error: `from agent "${from}" not found` });
|
|
584
|
+
if (!toAgent) return res.status(404).json({ error: `to agent "${to}" not found` });
|
|
585
|
+
|
|
586
|
+
const ts = nowIso();
|
|
587
|
+
p.logMessage({ agent_slug: from, channel: "a2a", direction: "out", author: from, body, meta: { to, depth: _depth }, ts });
|
|
588
|
+
p.logMessage({ agent_slug: to, channel: "a2a", direction: "in", author: from, body, meta: { from, depth: _depth }, ts });
|
|
589
|
+
|
|
590
|
+
let reply = null;
|
|
591
|
+
if (deliver && toAgent.fields.Model) {
|
|
592
|
+
try {
|
|
593
|
+
const tf = toAgent.fields;
|
|
594
|
+
const parts = [];
|
|
595
|
+
if (tf.Description) parts.push(tf.Description);
|
|
596
|
+
if (tf.Role) parts.push(`Role: ${tf.Role}`);
|
|
597
|
+
if (tf.Language) parts.push(`Default language: ${tf.Language}`);
|
|
598
|
+
parts.push(`You are ${toAgent.slug}. You just received a message from ${fromAgent.slug}. Reply concisely.`);
|
|
599
|
+
const memPath = path.join(p.path, ".apc", "agents", toAgent.slug, "memory.md");
|
|
600
|
+
if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
601
|
+
|
|
602
|
+
const result = await callEngine({
|
|
603
|
+
modelId: toAgent.fields.Model,
|
|
604
|
+
system: parts.join("\n\n"),
|
|
605
|
+
messages: [{ role: "user", content: `From ${fromAgent.slug}:\n\n${body}` }],
|
|
606
|
+
config: p.config || config,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
p.logMessage({ agent_slug: to, channel: "a2a", direction: "out", author: to, body: result.text, meta: { to: from, depth: _depth + 1, reply_to: fromAgent.slug, usage: result.usage } });
|
|
610
|
+
p.logMessage({ agent_slug: from, channel: "a2a", direction: "in", author: to, body: result.text, meta: { from: to, depth: _depth + 1 } });
|
|
611
|
+
reply = { text: result.text, usage: result.usage };
|
|
612
|
+
} catch (e) {
|
|
613
|
+
reply = { error: e.message };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
res.json({ from, to, body, ts, reply });
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// GET /projects/:pid/agents/:slug/connections
|
|
621
|
+
app.get("/projects/:pid/agents/:slug/connections", (req, res) => {
|
|
622
|
+
const p = project(req, res);
|
|
623
|
+
if (!p) return;
|
|
624
|
+
const agents = readAgents(p.path);
|
|
625
|
+
if (!agents.find((a) => a.slug === req.params.slug))
|
|
626
|
+
return res.status(404).json({ error: "agent not found" });
|
|
627
|
+
|
|
628
|
+
const messages = readProjectMessages(p.path, { agent_slug: req.params.slug });
|
|
629
|
+
const peers = new Map();
|
|
630
|
+
for (const m of messages) {
|
|
631
|
+
const peer = m.meta?.from || m.meta?.to || null;
|
|
632
|
+
if (!peer) continue;
|
|
633
|
+
const key = `${peer}|${m.channel}|${m.direction}`;
|
|
634
|
+
const existing = peers.get(key);
|
|
635
|
+
if (!existing) {
|
|
636
|
+
peers.set(key, { peer, channel: m.channel, direction: m.direction, n: 1, last_ts: m.ts });
|
|
637
|
+
} else {
|
|
638
|
+
existing.n++;
|
|
639
|
+
if (m.ts > existing.last_ts) existing.last_ts = m.ts;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
res.json(
|
|
643
|
+
Array.from(peers.values()).sort((a, b) => (b.last_ts || "").localeCompare(a.last_ts || ""))
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// ---- Runtimes (external CLI agents) -------------------------------
|
|
648
|
+
app.get("/runtimes", (_req, res) => res.json({ runtimes: RUNTIME_IDS }));
|
|
649
|
+
|
|
650
|
+
app.get("/env/detect", async (_req, res) => {
|
|
651
|
+
const detected = await detectAll();
|
|
652
|
+
res.json(detected);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// POST /projects/:pid/agents/:slug/runtime
|
|
656
|
+
app.post("/projects/:pid/agents/:slug/runtime", async (req, res) => {
|
|
657
|
+
const p = project(req, res);
|
|
658
|
+
if (!p) return;
|
|
659
|
+
const { runtime, prompt, timeoutMs } = req.body || {};
|
|
660
|
+
if (!runtime || !prompt)
|
|
661
|
+
return res.status(400).json({ error: "runtime and prompt required" });
|
|
662
|
+
|
|
663
|
+
const agents = readAgents(p.path);
|
|
664
|
+
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
665
|
+
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
666
|
+
|
|
667
|
+
let rt;
|
|
668
|
+
try {
|
|
669
|
+
rt = getRuntime(runtime);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
return res.status(400).json({ error: e.message });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let projectName = path.basename(p.path);
|
|
675
|
+
try {
|
|
676
|
+
const meta = JSON.parse(fs.readFileSync(path.join(p.path, ".apc", "project.json"), "utf8"));
|
|
677
|
+
if (meta.name) projectName = meta.name;
|
|
678
|
+
} catch {}
|
|
679
|
+
|
|
680
|
+
const session = createRuntimeSession({
|
|
681
|
+
projectRoot: p.path,
|
|
682
|
+
agentSlug: agent.slug,
|
|
683
|
+
runtime,
|
|
684
|
+
title: req.body?.title,
|
|
685
|
+
taskRef: req.body?.task_ref || "",
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const system = buildAgentSystem(p, agent, {
|
|
689
|
+
extraParts: [
|
|
690
|
+
buildApfHint({
|
|
691
|
+
projectName,
|
|
692
|
+
projectPath: p.path,
|
|
693
|
+
agentSlug: agent.slug,
|
|
694
|
+
sessionId: session.id,
|
|
695
|
+
}),
|
|
696
|
+
],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const r = await rt.run({
|
|
701
|
+
system,
|
|
702
|
+
prompt,
|
|
703
|
+
cwd: p.path,
|
|
704
|
+
timeoutMs: timeoutMs || 5 * 60 * 1000,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const apfResult = extractApfResult(r.output) || (r.output || "").slice(0, 200);
|
|
708
|
+
closeRuntimeSession({ filePath: session.path, externalSessionPath: r.externalSessionPath || null, exitCode: r.exitCode, result: apfResult });
|
|
709
|
+
|
|
710
|
+
p.logMessage({ agent_slug: agent.slug, channel: "runtime", direction: "in", author: "user", body: prompt, meta: { runtime, apc_session: session.id } });
|
|
711
|
+
p.logMessage({ agent_slug: agent.slug, channel: "runtime", direction: "out", author: agent.slug, body: r.output || "", meta: { runtime, exit_code: r.exitCode, external_session_path: r.externalSessionPath || null, session_id: r.sessionId || null, apc_session: session.id } });
|
|
712
|
+
projects.rebuild(p.id);
|
|
713
|
+
|
|
714
|
+
res.json({
|
|
715
|
+
runtime,
|
|
716
|
+
exit_code: r.exitCode,
|
|
717
|
+
output: r.output,
|
|
718
|
+
stderr: r.stderr,
|
|
719
|
+
external_session_path: r.externalSessionPath || null,
|
|
720
|
+
session_id: r.sessionId || null,
|
|
721
|
+
apc_session: session.id,
|
|
722
|
+
});
|
|
723
|
+
} catch (e) {
|
|
724
|
+
try {
|
|
725
|
+
closeRuntimeSession({ filePath: session.path, exitCode: -1, result: `error: ${e.message.slice(0, 200)}` });
|
|
726
|
+
} catch {}
|
|
727
|
+
res.status(500).json({ error: e.message, apc_session: session.id });
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// ---- Session resume -----------------------------------------------
|
|
732
|
+
app.get("/projects/:pid/sessions/:id/resume", async (req, res) => {
|
|
733
|
+
const p = project(req, res);
|
|
734
|
+
if (!p) return;
|
|
735
|
+
const { id } = req.params;
|
|
736
|
+
|
|
737
|
+
const agentsDir = path.join(p.path, ".apc", "agents");
|
|
738
|
+
let sessionFile = null;
|
|
739
|
+
let agentSlug = null;
|
|
740
|
+
if (fs.existsSync(agentsDir)) {
|
|
741
|
+
for (const slug of fs.readdirSync(agentsDir)) {
|
|
742
|
+
const f = path.join(agentsDir, slug, "sessions", `${id}.md`);
|
|
743
|
+
if (fs.existsSync(f)) {
|
|
744
|
+
sessionFile = f;
|
|
745
|
+
agentSlug = slug;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (!sessionFile) return res.status(404).json({ error: `session ${id} not found` });
|
|
751
|
+
|
|
752
|
+
const session = readSessionFrontmatter(sessionFile);
|
|
753
|
+
const out = {
|
|
754
|
+
id,
|
|
755
|
+
agent: agentSlug,
|
|
756
|
+
session_path: sessionFile,
|
|
757
|
+
frontmatter: session?.fm || {},
|
|
758
|
+
external_transcript: null,
|
|
759
|
+
summary: null,
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const externalPath = session?.fm?.external_session_path;
|
|
763
|
+
if (externalPath && fs.existsSync(externalPath)) {
|
|
764
|
+
const stat = fs.statSync(externalPath);
|
|
765
|
+
const raw = fs.readFileSync(externalPath, "utf8");
|
|
766
|
+
out.external_transcript = {
|
|
767
|
+
path: externalPath,
|
|
768
|
+
size: stat.size,
|
|
769
|
+
tail: raw.length > 32 * 1024 ? raw.slice(-32 * 1024) : raw,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (req.query.summarize === "true" && isSuperAgentEnabled(config)) {
|
|
774
|
+
try {
|
|
775
|
+
const prompt =
|
|
776
|
+
`Resumí qué pasó en esta sesión APC en 4 bullets concretos.\n\n` +
|
|
777
|
+
`Frontmatter:\n${JSON.stringify(out.frontmatter, null, 2)}\n\n` +
|
|
778
|
+
(out.external_transcript
|
|
779
|
+
? `Transcript externo (últimos ${out.external_transcript.tail.length} chars):\n${out.external_transcript.tail}`
|
|
780
|
+
: `(sin transcript externo)`);
|
|
781
|
+
const sa = await runSuperAgent({ globalConfig: config, projects, plugins, registries, prompt, contextNote: `Resume request for session ${id}.` });
|
|
782
|
+
out.summary = sa.text;
|
|
783
|
+
} catch (e) {
|
|
784
|
+
out.summary = `(super-agent failed: ${e.message})`;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
res.json(out);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ---- Routines (per-project scheduled tasks) ----------------------
|
|
792
|
+
app.get("/projects/:pid/routines", (req, res) => {
|
|
793
|
+
const p = project(req, res);
|
|
794
|
+
if (!p) return;
|
|
795
|
+
res.json(listRoutines(p.path));
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
app.get("/projects/:pid/routines/:name", (req, res) => {
|
|
799
|
+
const p = project(req, res);
|
|
800
|
+
if (!p) return;
|
|
801
|
+
const r = getRoutine(p.path, req.params.name);
|
|
802
|
+
if (!r) return res.status(404).json({ error: "routine not found" });
|
|
803
|
+
res.json(r);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
app.post("/projects/:pid/routines", (req, res) => {
|
|
807
|
+
const p = project(req, res);
|
|
808
|
+
if (!p) return;
|
|
809
|
+
try {
|
|
810
|
+
const r = upsertRoutine(p.path, req.body || {});
|
|
811
|
+
res.status(201).json(r);
|
|
812
|
+
} catch (e) {
|
|
813
|
+
res.status(400).json({ error: e.message });
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
app.delete("/projects/:pid/routines/:name", (req, res) => {
|
|
818
|
+
const p = project(req, res);
|
|
819
|
+
if (!p) return;
|
|
820
|
+
const ok = deleteRoutine(p.path, req.params.name);
|
|
821
|
+
res.status(ok ? 204 : 404).end();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
app.post("/projects/:pid/routines/:name/enable", (req, res) => {
|
|
825
|
+
const p = project(req, res);
|
|
826
|
+
if (!p) return;
|
|
827
|
+
setRoutineEnabled(p.path, req.params.name, true);
|
|
828
|
+
res.json({ ok: true });
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
app.post("/projects/:pid/routines/:name/disable", (req, res) => {
|
|
832
|
+
const p = project(req, res);
|
|
833
|
+
if (!p) return;
|
|
834
|
+
setRoutineEnabled(p.path, req.params.name, false);
|
|
835
|
+
res.json({ ok: true });
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
app.post("/projects/:pid/routines/:name/run", async (req, res) => {
|
|
839
|
+
const p = project(req, res);
|
|
840
|
+
if (!p) return;
|
|
841
|
+
const r = getRoutine(p.path, req.params.name);
|
|
842
|
+
if (!r) return res.status(404).json({ error: "routine not found" });
|
|
843
|
+
try {
|
|
844
|
+
const result = await runRoutineNow({ project: p, plugins, globalConfig: config }, r);
|
|
845
|
+
res.json(result);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
res.status(500).json({ error: e.message });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// ---- Per-project config (.apc/config.json) -----------------------
|
|
852
|
+
app.get("/projects/:pid/config", (req, res) => {
|
|
853
|
+
const p = project(req, res);
|
|
854
|
+
if (!p) return;
|
|
855
|
+
res.json({
|
|
856
|
+
effective: p.config || {},
|
|
857
|
+
project_only: readProjectConfig(p.path),
|
|
858
|
+
project_config_path: path.join(p.path, ".apc", "config.json"),
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
app.put("/projects/:pid/config", (req, res) => {
|
|
863
|
+
const p = project(req, res);
|
|
864
|
+
if (!p) return;
|
|
865
|
+
const body = req.body || {};
|
|
866
|
+
if (typeof body !== "object" || Array.isArray(body))
|
|
867
|
+
return res.status(400).json({ error: "body must be a JSON object" });
|
|
868
|
+
writeProjectConfig(p.path, body);
|
|
869
|
+
projects.rebuild(p.id);
|
|
870
|
+
res.json({ ok: true });
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
app.patch("/projects/:pid/config", (req, res) => {
|
|
874
|
+
const p = project(req, res);
|
|
875
|
+
if (!p) return;
|
|
876
|
+
const { set, unset } = req.body || {};
|
|
877
|
+
const cfg = readProjectConfig(p.path);
|
|
878
|
+
if (set && typeof set === "object") {
|
|
879
|
+
for (const [k, v] of Object.entries(set)) setDottedKey(cfg, k, v);
|
|
880
|
+
}
|
|
881
|
+
if (Array.isArray(unset)) {
|
|
882
|
+
for (const k of unset) unsetDottedKey(cfg, k);
|
|
883
|
+
}
|
|
884
|
+
writeProjectConfig(p.path, cfg);
|
|
885
|
+
projects.rebuild(p.id);
|
|
886
|
+
res.json({ ok: true, project_only: cfg });
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// ---- Admin --------------------------------------------------------
|
|
890
|
+
app.post("/admin/shutdown", (_req, res) => {
|
|
891
|
+
res.json({ ok: true });
|
|
892
|
+
setTimeout(() => process.exit(0), 50);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// ---- 404 catchall -------------------------------------------------
|
|
896
|
+
app.use((req, res) => res.status(404).json({ error: `no route ${req.method} ${req.path}` }));
|
|
897
|
+
|
|
898
|
+
return app;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ---------------------------------------------------------------------
|
|
902
|
+
// Helpers
|
|
903
|
+
// ---------------------------------------------------------------------
|
|
904
|
+
|
|
905
|
+
function agentToResponse(a) {
|
|
906
|
+
if (!a) return null;
|
|
907
|
+
const f = a.fields || {};
|
|
908
|
+
const reserved = new Set(["Role", "Model", "Language", "Description", "Skills", "Tools"]);
|
|
909
|
+
const extra = {};
|
|
910
|
+
for (const [k, v] of Object.entries(f)) {
|
|
911
|
+
if (!reserved.has(k)) extra[k] = v;
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
slug: a.slug,
|
|
915
|
+
role: f.Role || null,
|
|
916
|
+
model: f.Model || null,
|
|
917
|
+
language: f.Language || null,
|
|
918
|
+
description: f.Description || null,
|
|
919
|
+
skills: Array.isArray(f.Skills) ? f.Skills : [],
|
|
920
|
+
tools: Array.isArray(f.Tools) ? f.Tools : [],
|
|
921
|
+
extra,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Build system prompt from an agent's fields + memory + skills.
|
|
926
|
+
// Optional `extraParts` are appended at the end.
|
|
927
|
+
function buildAgentSystem(p, agent, { extraParts = [] } = {}) {
|
|
928
|
+
const f = agent.fields || {};
|
|
929
|
+
const parts = [];
|
|
930
|
+
if (f.Description) parts.push(f.Description);
|
|
931
|
+
if (f.Role) parts.push(`Role: ${f.Role}`);
|
|
932
|
+
if (f.Language) parts.push(`Default language: ${f.Language}`);
|
|
933
|
+
const memPath = path.join(p.path, ".apc", "agents", agent.slug, "memory.md");
|
|
934
|
+
if (fs.existsSync(memPath)) {
|
|
935
|
+
parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
936
|
+
}
|
|
937
|
+
const apxSkill = path.join(p.path, ".apc", "skills", "apx.md");
|
|
938
|
+
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
|
939
|
+
const skills = Array.isArray(f.Skills) ? f.Skills : [];
|
|
940
|
+
for (const skill of skills) {
|
|
941
|
+
const sp = path.join(p.path, ".apc", "skills", `${skill}.md`);
|
|
942
|
+
if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
|
|
943
|
+
}
|
|
944
|
+
for (const ep of extraParts) parts.push(ep);
|
|
945
|
+
return parts.join("\n\n");
|
|
946
|
+
}
|