@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,539 @@
|
|
|
1
|
+
// Tools the super-agent can call directly (function calling). Two halves:
|
|
2
|
+
// - SCHEMAS: the JSON-schema definitions sent to the model.
|
|
3
|
+
// - HANDLERS: server-side implementations that operate on projects/messages.
|
|
4
|
+
//
|
|
5
|
+
// Every handler returns a JSON-serializable result; errors throw and the loop
|
|
6
|
+
// catches them so the tool message comes back as `{error: "..."}`.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { callEngine } from "./engines/index.js";
|
|
11
|
+
import { getRuntime, RUNTIME_IDS } from "./runtimes/index.js";
|
|
12
|
+
import { readAgents } from "../core/parser.js";
|
|
13
|
+
import { readProjectMessages, searchProjectMessages } from "../core/messages-store.js";
|
|
14
|
+
import { readIdentity, writeIdentity } from "../core/identity.js";
|
|
15
|
+
|
|
16
|
+
// ---------- helpers ---------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function resolveProject(projects, target, { allowMulti = false } = {}) {
|
|
19
|
+
if (target === undefined || target === null || target === "") {
|
|
20
|
+
const all = projects.list();
|
|
21
|
+
if (all.length === 1) return projects.get(all[0].id);
|
|
22
|
+
if (allowMulti) return null; // signal "list all"
|
|
23
|
+
throw new Error(
|
|
24
|
+
`multiple projects registered (${all.length}); specify project=<id|name|path>`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
// numeric id
|
|
28
|
+
if (typeof target === "number" || /^\d+$/.test(String(target))) {
|
|
29
|
+
const e = projects.get(parseInt(target, 10));
|
|
30
|
+
if (!e) throw new Error(`project id ${target} not found`);
|
|
31
|
+
return e;
|
|
32
|
+
}
|
|
33
|
+
const tgt = String(target);
|
|
34
|
+
const all = projects.list();
|
|
35
|
+
// exact path or name
|
|
36
|
+
const byPath = all.find((p) => p.path === path.resolve(tgt));
|
|
37
|
+
if (byPath) return projects.get(byPath.id);
|
|
38
|
+
const byName = all.find((p) => p.name === tgt);
|
|
39
|
+
if (byName) return projects.get(byName.id);
|
|
40
|
+
// substring on name or path
|
|
41
|
+
const tgtLow = tgt.toLowerCase();
|
|
42
|
+
const fuzzy = all.filter(
|
|
43
|
+
(p) =>
|
|
44
|
+
p.name.toLowerCase().includes(tgtLow) ||
|
|
45
|
+
p.path.toLowerCase().includes(tgtLow)
|
|
46
|
+
);
|
|
47
|
+
if (fuzzy.length === 1) return projects.get(fuzzy[0].id);
|
|
48
|
+
if (fuzzy.length > 1) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`project "${tgt}" is ambiguous; matches: ${fuzzy.map((p) => p.name).join(", ")}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`project "${tgt}" not found`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safePathJoin(root, sub) {
|
|
57
|
+
// Refuse anything that escapes the project root.
|
|
58
|
+
const target = path.resolve(root, sub || ".");
|
|
59
|
+
const rootResolved = path.resolve(root);
|
|
60
|
+
if (target !== rootResolved && !target.startsWith(rootResolved + path.sep)) {
|
|
61
|
+
throw new Error(`path "${sub}" escapes the project root`);
|
|
62
|
+
}
|
|
63
|
+
return target;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------- SCHEMAS ---------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export const TOOL_SCHEMAS = [
|
|
69
|
+
{
|
|
70
|
+
type: "function",
|
|
71
|
+
function: {
|
|
72
|
+
name: "list_projects",
|
|
73
|
+
description: "List all projects registered with the APX daemon. Returns id, name, path, agent count.",
|
|
74
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "function",
|
|
79
|
+
function: {
|
|
80
|
+
name: "list_agents",
|
|
81
|
+
description: "List agents. If `project` is given, returns the agents of that project (slug, role, model, language, skills). If `project` is omitted AND there are multiple projects, returns ALL agents grouped by project — use this form when the user asks generically about 'los agentes' or 'lista de agentes' without specifying a project.",
|
|
82
|
+
parameters: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
project: { type: "string", description: "project id, name, path, or substring. OMIT to list every project's agents." },
|
|
86
|
+
},
|
|
87
|
+
required: [],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "function",
|
|
93
|
+
function: {
|
|
94
|
+
name: "list_mcps",
|
|
95
|
+
description: "List MCPs (multi-source merged: apf/cursor/claude/etc). If `project` is omitted AND there are multiple projects, returns ALL MCPs grouped by project.",
|
|
96
|
+
parameters: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: { project: { type: "string", description: "OMIT to list every project's MCPs." } },
|
|
99
|
+
required: [],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "function",
|
|
105
|
+
function: {
|
|
106
|
+
name: "read_agent_memory",
|
|
107
|
+
description: "Read an agent's memory.md file (its persistent long-term knowledge).",
|
|
108
|
+
parameters: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
project: { type: "string" },
|
|
112
|
+
agent: { type: "string", description: "agent slug" },
|
|
113
|
+
},
|
|
114
|
+
required: ["agent"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "function",
|
|
120
|
+
function: {
|
|
121
|
+
name: "list_files",
|
|
122
|
+
description: "List files and subdirectories of a path inside the project root.",
|
|
123
|
+
parameters: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
project: { type: "string" },
|
|
127
|
+
path: { type: "string", description: "relative path inside the project; default '.'" },
|
|
128
|
+
},
|
|
129
|
+
required: [],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "function",
|
|
135
|
+
function: {
|
|
136
|
+
name: "read_file",
|
|
137
|
+
description: "Read a text file inside the project root. Returns first 64KB.",
|
|
138
|
+
parameters: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
project: { type: "string" },
|
|
142
|
+
path: { type: "string", description: "relative path inside the project" },
|
|
143
|
+
},
|
|
144
|
+
required: ["path"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "function",
|
|
150
|
+
function: {
|
|
151
|
+
name: "tail_messages",
|
|
152
|
+
description: "Tail the project's messages log. Optional filter by channel and/or agent slug.",
|
|
153
|
+
parameters: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
project: { type: "string" },
|
|
157
|
+
channel: { type: "string", description: "e.g. telegram, engine, a2a, runtime, heartbeat" },
|
|
158
|
+
agent: { type: "string", description: "agent slug" },
|
|
159
|
+
limit: { type: "integer", description: "max rows (default 20)" },
|
|
160
|
+
},
|
|
161
|
+
required: [],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: "function",
|
|
167
|
+
function: {
|
|
168
|
+
name: "search_messages",
|
|
169
|
+
description: "Full-text search inside a project's messages.",
|
|
170
|
+
parameters: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
project: { type: "string" },
|
|
174
|
+
query: { type: "string" },
|
|
175
|
+
},
|
|
176
|
+
required: ["query"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "function",
|
|
182
|
+
function: {
|
|
183
|
+
name: "call_agent",
|
|
184
|
+
description: "Run a one-shot prompt through a project agent's engine. Returns the agent's reply text.",
|
|
185
|
+
parameters: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
project: { type: "string" },
|
|
189
|
+
agent: { type: "string", description: "agent slug" },
|
|
190
|
+
prompt: { type: "string" },
|
|
191
|
+
},
|
|
192
|
+
required: ["agent", "prompt"],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: "function",
|
|
198
|
+
function: {
|
|
199
|
+
name: "call_mcp",
|
|
200
|
+
description: "Call a tool on an MCP server registered in a project. Args is a JSON object.",
|
|
201
|
+
parameters: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
project: { type: "string" },
|
|
205
|
+
mcp: { type: "string", description: "MCP server name" },
|
|
206
|
+
tool: { type: "string", description: "tool name on that MCP" },
|
|
207
|
+
args: { type: "object", description: "arguments object" },
|
|
208
|
+
},
|
|
209
|
+
required: ["mcp", "tool"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
type: "function",
|
|
215
|
+
function: {
|
|
216
|
+
name: "call_runtime",
|
|
217
|
+
description: "Spawn an external CLI agent (Claude Code, Codex, OpenCode, Aider) impersonating one of the project's APC agents. APX creates an APC session, builds the system prompt from the agent's memory+skills, runs the runtime, captures its transcript path. IMPORTANT: `agent` is the slug declared in AGENTS.md (e.g. 'sofia', 'martin', 'sandbox') — NOT the name of the LLM/runtime. The LLM/runtime goes in the `runtime` parameter ('claude-code', 'codex', 'opencode', 'aider'). If unsure which agents exist, call list_agents first.",
|
|
218
|
+
parameters: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
project: { type: "string" },
|
|
222
|
+
agent: { type: "string", description: "APC agent slug from AGENTS.md (sofia/martin/etc) — NOT the runtime name" },
|
|
223
|
+
runtime: {
|
|
224
|
+
type: "string",
|
|
225
|
+
enum: ["claude-code", "codex", "opencode", "aider"],
|
|
226
|
+
description: "which external CLI to spawn",
|
|
227
|
+
},
|
|
228
|
+
prompt: { type: "string" },
|
|
229
|
+
timeout_s: { type: "integer", description: "seconds before SIGTERM (default 300)" },
|
|
230
|
+
},
|
|
231
|
+
required: ["agent", "runtime", "prompt"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: "function",
|
|
237
|
+
function: {
|
|
238
|
+
name: "send_telegram",
|
|
239
|
+
description: "Send a Telegram message via the daemon's Telegram plugin. Optional channel and chat_id.",
|
|
240
|
+
parameters: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
channel: { type: "string", description: "telegram channel name; omit for default" },
|
|
244
|
+
chat_id: { type: "string", description: "destination chat id; omit to use the channel default" },
|
|
245
|
+
text: { type: "string" },
|
|
246
|
+
},
|
|
247
|
+
required: ["text"],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
type: "function",
|
|
253
|
+
function: {
|
|
254
|
+
name: "set_identity",
|
|
255
|
+
description: "Update the daemon's own identity fields (agent_name, owner_name, personality, language). Use when the user asks to rename the agent, change its personality, or update owner info. The change persists across restarts.",
|
|
256
|
+
parameters: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
agent_name: { type: "string", description: "New name for the agent (e.g. 'Roby')" },
|
|
260
|
+
owner_name: { type: "string", description: "Owner's name" },
|
|
261
|
+
personality: { type: "string", description: "Comma-separated personality traits" },
|
|
262
|
+
language: { type: "string", description: "Preferred language for agent messages (e.g. 'es', 'en', 'Spanish', 'Español')" },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// ---------- HANDLERS --------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export function makeToolHandlers({ projects, plugins, registries, globalConfig }) {
|
|
272
|
+
return {
|
|
273
|
+
list_projects: () => {
|
|
274
|
+
return projects.list().map((p) => ({
|
|
275
|
+
id: p.id,
|
|
276
|
+
name: p.name,
|
|
277
|
+
path: p.path,
|
|
278
|
+
agents: p.agents,
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
list_agents: ({ project } = {}) => {
|
|
283
|
+
const agentRow = (a) => ({
|
|
284
|
+
slug: a.slug,
|
|
285
|
+
role: a.fields.Role || null,
|
|
286
|
+
model: a.fields.Model || null,
|
|
287
|
+
language: a.fields.Language || null,
|
|
288
|
+
description: a.fields.Description || null,
|
|
289
|
+
skills: Array.isArray(a.fields.Skills) ? a.fields.Skills : (a.fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
290
|
+
});
|
|
291
|
+
const p = resolveProject(projects, project, { allowMulti: true });
|
|
292
|
+
if (p) {
|
|
293
|
+
return readAgents(p.path).map(agentRow);
|
|
294
|
+
}
|
|
295
|
+
// No project specified and >1 registered → return everything grouped.
|
|
296
|
+
return projects.list().map((entry) => {
|
|
297
|
+
const e = projects.get(entry.id);
|
|
298
|
+
return {
|
|
299
|
+
project: { id: entry.id, name: entry.name, path: entry.path },
|
|
300
|
+
agents: readAgents(e.path).map(agentRow),
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
list_mcps: ({ project } = {}) => {
|
|
306
|
+
const mcpRow = (m) => ({
|
|
307
|
+
name: m.name,
|
|
308
|
+
source: m.source,
|
|
309
|
+
transport: m.transport,
|
|
310
|
+
enabled: !!m.enabled,
|
|
311
|
+
command: m.command,
|
|
312
|
+
url: m.url,
|
|
313
|
+
});
|
|
314
|
+
const p = resolveProject(projects, project, { allowMulti: true });
|
|
315
|
+
if (p) {
|
|
316
|
+
if (!registries) throw new Error("MCP registry unavailable");
|
|
317
|
+
return registries.for(p).list().map(mcpRow);
|
|
318
|
+
}
|
|
319
|
+
return projects.list().map((entry) => {
|
|
320
|
+
const e = projects.get(entry.id);
|
|
321
|
+
return {
|
|
322
|
+
project: { id: entry.id, name: entry.name, path: entry.path },
|
|
323
|
+
mcps: registries ? registries.for(e).list().map(mcpRow) : [],
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
read_agent_memory: ({ project, agent }) => {
|
|
329
|
+
const p = resolveProject(projects, project);
|
|
330
|
+
const f = path.join(p.path, ".apc", "agents", agent, "memory.md");
|
|
331
|
+
if (!fs.existsSync(f)) return { error: `no memory.md for agent ${agent}` };
|
|
332
|
+
return { body: fs.readFileSync(f, "utf8") };
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
list_files: ({ project, path: sub = "." }) => {
|
|
336
|
+
const p = resolveProject(projects, project);
|
|
337
|
+
const target = safePathJoin(p.path, sub);
|
|
338
|
+
if (!fs.existsSync(target)) return { error: `path not found: ${sub}` };
|
|
339
|
+
const stat = fs.statSync(target);
|
|
340
|
+
if (!stat.isDirectory()) return { error: `${sub} is not a directory` };
|
|
341
|
+
return fs.readdirSync(target).map((name) => {
|
|
342
|
+
const full = path.join(target, name);
|
|
343
|
+
const s = fs.statSync(full);
|
|
344
|
+
return {
|
|
345
|
+
name,
|
|
346
|
+
type: s.isDirectory() ? "dir" : "file",
|
|
347
|
+
size: s.size,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
read_file: ({ project, path: sub }) => {
|
|
353
|
+
if (!sub) throw new Error("read_file: path required");
|
|
354
|
+
const p = resolveProject(projects, project);
|
|
355
|
+
const target = safePathJoin(p.path, sub);
|
|
356
|
+
if (!fs.existsSync(target)) return { error: `file not found: ${sub}` };
|
|
357
|
+
const buf = fs.readFileSync(target, "utf8").slice(0, 64 * 1024);
|
|
358
|
+
return { content: buf, truncated: fs.statSync(target).size > 64 * 1024 };
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
tail_messages: ({ project, channel, agent, limit = 20 }) => {
|
|
362
|
+
const p = resolveProject(projects, project);
|
|
363
|
+
return readProjectMessages(p.path, {
|
|
364
|
+
channel,
|
|
365
|
+
agent_slug: agent,
|
|
366
|
+
limit: Math.min(limit, 100),
|
|
367
|
+
}).map((m) => ({ ts: m.ts, channel: m.channel, direction: m.direction, author: m.author, body: m.body }));
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
search_messages: ({ project, query }) => {
|
|
371
|
+
if (!query) throw new Error("search_messages: query required");
|
|
372
|
+
const p = resolveProject(projects, project);
|
|
373
|
+
return searchProjectMessages(p.path, query, 25)
|
|
374
|
+
.map((m) => ({ ts: m.ts, channel: m.channel, direction: m.direction, author: m.author, body: m.body }));
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
call_agent: async ({ project, agent: slug, prompt }) => {
|
|
378
|
+
const p = resolveProject(projects, project);
|
|
379
|
+
const agent = readAgents(p.path).find((a) => a.slug === slug);
|
|
380
|
+
if (!agent) throw new Error(`agent ${slug} not found`);
|
|
381
|
+
const model = agent.fields.Model;
|
|
382
|
+
if (!model) throw new Error(`agent ${slug} has no model`);
|
|
383
|
+
const parts = [];
|
|
384
|
+
if (agent.fields.Description) parts.push(agent.fields.Description);
|
|
385
|
+
if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
|
|
386
|
+
const memPath = path.join(p.path, ".apc", "agents", slug, "memory.md");
|
|
387
|
+
if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
388
|
+
const apxSkill = path.join(p.path, ".apc", "skills", "apx.md");
|
|
389
|
+
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
|
390
|
+
const skills = (agent.fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
391
|
+
for (const skill of skills) {
|
|
392
|
+
const sp = path.join(p.path, ".apc", "skills", `${skill}.md`);
|
|
393
|
+
if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
|
|
394
|
+
}
|
|
395
|
+
const result = await callEngine({
|
|
396
|
+
modelId: model,
|
|
397
|
+
system: parts.join("\n\n"),
|
|
398
|
+
messages: [{ role: "user", content: prompt }],
|
|
399
|
+
config: p.config || globalConfig,
|
|
400
|
+
});
|
|
401
|
+
p.logMessage({
|
|
402
|
+
agent_slug: slug,
|
|
403
|
+
channel: "engine",
|
|
404
|
+
direction: "out",
|
|
405
|
+
author: slug,
|
|
406
|
+
body: result.text,
|
|
407
|
+
meta: { invoked_by: "super_agent_tool", usage: result.usage },
|
|
408
|
+
});
|
|
409
|
+
return { text: result.text, usage: result.usage };
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
call_mcp: async ({ project, mcp, tool, args = {} }) => {
|
|
413
|
+
const p = resolveProject(projects, project);
|
|
414
|
+
if (!registries) throw new Error("MCP registry unavailable");
|
|
415
|
+
const reg = registries.for ? registries.for(p) : registries.ensure(p);
|
|
416
|
+
const result = await reg.call(mcp, tool, args);
|
|
417
|
+
return result;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
call_runtime: async ({ project, agent: slug, runtime, prompt, timeout_s = 300 }) => {
|
|
421
|
+
// If `project` was not provided AND multiple projects exist, try to
|
|
422
|
+
// find the agent across all of them. Only one match → use it. Zero or
|
|
423
|
+
// multiple matches → return an actionable error instead of throwing.
|
|
424
|
+
let p;
|
|
425
|
+
if (project) {
|
|
426
|
+
p = resolveProject(projects, project);
|
|
427
|
+
} else {
|
|
428
|
+
const all = projects.list();
|
|
429
|
+
if (all.length === 1) {
|
|
430
|
+
p = projects.get(all[0].id);
|
|
431
|
+
} else {
|
|
432
|
+
const matches = [];
|
|
433
|
+
for (const entry of all) {
|
|
434
|
+
const e = projects.get(entry.id);
|
|
435
|
+
if (readAgents(e.path).find((a) => a.slug === slug)) matches.push(e);
|
|
436
|
+
}
|
|
437
|
+
if (matches.length === 1) {
|
|
438
|
+
p = matches[0];
|
|
439
|
+
} else if (matches.length > 1) {
|
|
440
|
+
return {
|
|
441
|
+
error: `agent "${slug}" exists in multiple projects: ${matches.map((m) => m.path).join(", ")}. Specify project explicitly.`,
|
|
442
|
+
candidates: matches.map((m) => m.path),
|
|
443
|
+
};
|
|
444
|
+
} else {
|
|
445
|
+
// Not found anywhere — give the model the global directory
|
|
446
|
+
const directory = all.map((entry) => {
|
|
447
|
+
const e = projects.get(entry.id);
|
|
448
|
+
return { project: entry.name, path: entry.path, agents: readAgents(e.path).map((a) => a.slug) };
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
error: `agent "${slug}" not found in any registered project.`,
|
|
452
|
+
directory,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const agent = readAgents(p.path).find((a) => a.slug === slug);
|
|
458
|
+
if (!agent) {
|
|
459
|
+
const available = readAgents(p.path).map((a) => a.slug).sort();
|
|
460
|
+
return {
|
|
461
|
+
error: `agent "${slug}" not found in project "${p.path}". Available agents: ${available.join(", ")}. Note: 'agent' is the APC agent slug (e.g. sofia, martin); 'runtime' is the external CLI (claude-code, codex, opencode, aider).`,
|
|
462
|
+
available_agents: available,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
let rt;
|
|
466
|
+
try {
|
|
467
|
+
rt = getRuntime(runtime);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
return { error: `${e.message}. Available runtimes: ${RUNTIME_IDS.join(", ")}` };
|
|
470
|
+
}
|
|
471
|
+
const parts = [];
|
|
472
|
+
if (agent.fields.Description) parts.push(agent.fields.Description);
|
|
473
|
+
if (agent.fields.Role) parts.push(`Role: ${agent.fields.Role}`);
|
|
474
|
+
const memPath = path.join(p.path, ".apc", "agents", slug, "memory.md");
|
|
475
|
+
if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
|
|
476
|
+
const apxSkill = path.join(p.path, ".apc", "skills", "apx.md");
|
|
477
|
+
if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
|
|
478
|
+
const skills = (agent.fields.Skills || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
479
|
+
for (const skill of skills) {
|
|
480
|
+
const sp = path.join(p.path, ".apc", "skills", `${skill}.md`);
|
|
481
|
+
if (fs.existsSync(sp)) parts.push(`## Skill: ${skill}\n` + fs.readFileSync(sp, "utf8"));
|
|
482
|
+
}
|
|
483
|
+
const r = await rt.run({
|
|
484
|
+
system: parts.join("\n\n"),
|
|
485
|
+
prompt,
|
|
486
|
+
cwd: p.path,
|
|
487
|
+
timeoutMs: timeout_s * 1000,
|
|
488
|
+
});
|
|
489
|
+
// Log on channel='runtime' so it shows up in messages tail
|
|
490
|
+
p.logMessage({
|
|
491
|
+
agent_slug: slug,
|
|
492
|
+
channel: "runtime",
|
|
493
|
+
direction: "in",
|
|
494
|
+
author: "user",
|
|
495
|
+
body: prompt,
|
|
496
|
+
meta: { runtime, invoked_by: "super_agent_tool" },
|
|
497
|
+
});
|
|
498
|
+
p.logMessage({
|
|
499
|
+
agent_slug: slug,
|
|
500
|
+
channel: "runtime",
|
|
501
|
+
direction: "out",
|
|
502
|
+
author: slug,
|
|
503
|
+
body: r.output || "",
|
|
504
|
+
meta: {
|
|
505
|
+
runtime,
|
|
506
|
+
exit_code: r.exitCode,
|
|
507
|
+
external_session_path: r.externalSessionPath || null,
|
|
508
|
+
invoked_by: "super_agent_tool",
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
runtime,
|
|
513
|
+
exit_code: r.exitCode,
|
|
514
|
+
output: (r.output || "").slice(0, 4000),
|
|
515
|
+
truncated: (r.output || "").length > 4000,
|
|
516
|
+
external_session_path: r.externalSessionPath || null,
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
send_telegram: async ({ channel, chat_id, text }) => {
|
|
521
|
+
if (!plugins) throw new Error("plugins unavailable");
|
|
522
|
+
const tg = plugins.get("telegram");
|
|
523
|
+
if (!tg) throw new Error("telegram plugin not loaded");
|
|
524
|
+
const r = await tg.send({ channel, chat_id, text, author: "apx" });
|
|
525
|
+
return { ok: true, message_id: r.message_id };
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
set_identity: ({ agent_name, owner_name, personality, language } = {}) => {
|
|
529
|
+
const fields = {};
|
|
530
|
+
if (agent_name) fields.agent_name = agent_name;
|
|
531
|
+
if (owner_name) fields.owner_name = owner_name;
|
|
532
|
+
if (personality) fields.personality = personality;
|
|
533
|
+
if (language) fields.language = language;
|
|
534
|
+
if (Object.keys(fields).length === 0) throw new Error("no fields provided");
|
|
535
|
+
const updated = writeIdentity(fields);
|
|
536
|
+
return { ok: true, identity: updated };
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|