@agentprojectcontext/apx 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/config.js +23 -0
  4. package/src/cli/commands/messages.js +45 -0
  5. package/src/cli/commands/routine.js +27 -2
  6. package/src/cli/commands/setup.js +2 -2
  7. package/src/cli/index.js +969 -3
  8. package/src/core/apc-context-skill.md +3 -0
  9. package/src/core/apx-skill.md +30 -0
  10. package/src/core/config.js +2 -0
  11. package/src/core/mascot.js +5 -7
  12. package/src/core/messages-store.js +94 -20
  13. package/src/core/routines-store.js +3 -1
  14. package/src/daemon/api.js +3 -3
  15. package/src/daemon/index.js +38 -2
  16. package/src/daemon/plugins/telegram.js +32 -2
  17. package/src/daemon/routines.js +64 -2
  18. package/src/daemon/super-agent-tools/helpers.js +120 -0
  19. package/src/daemon/super-agent-tools/index.js +56 -0
  20. package/src/daemon/super-agent-tools/tools/add-project.js +36 -0
  21. package/src/daemon/super-agent-tools/tools/call-agent.js +45 -0
  22. package/src/daemon/super-agent-tools/tools/call-mcp.js +30 -0
  23. package/src/daemon/super-agent-tools/tools/call-runtime.js +107 -0
  24. package/src/daemon/super-agent-tools/tools/edit-file.js +44 -0
  25. package/src/daemon/super-agent-tools/tools/import-agent.js +48 -0
  26. package/src/daemon/super-agent-tools/tools/list-agents.js +36 -0
  27. package/src/daemon/super-agent-tools/tools/list-files.js +38 -0
  28. package/src/daemon/super-agent-tools/tools/list-mcps.js +48 -0
  29. package/src/daemon/super-agent-tools/tools/list-projects.js +20 -0
  30. package/src/daemon/super-agent-tools/tools/list-vault-agents.js +18 -0
  31. package/src/daemon/super-agent-tools/tools/read-agent-memory.js +28 -0
  32. package/src/daemon/super-agent-tools/tools/read-file.js +33 -0
  33. package/src/daemon/super-agent-tools/tools/run-shell.js +86 -0
  34. package/src/daemon/super-agent-tools/tools/search-messages.js +34 -0
  35. package/src/daemon/super-agent-tools/tools/send-telegram.js +30 -0
  36. package/src/daemon/super-agent-tools/tools/set-identity.js +35 -0
  37. package/src/daemon/super-agent-tools/tools/set-permission-mode.js +32 -0
  38. package/src/daemon/super-agent-tools/tools/tail-messages.js +39 -0
  39. package/src/daemon/super-agent-tools/tools/write-file.js +33 -0
  40. package/src/daemon/super-agent-tools.js +1 -539
  41. package/src/daemon/super-agent.js +56 -7
@@ -1,539 +1 @@
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
- }
1
+ export { TOOL_SCHEMAS, makeToolHandlers } from "./super-agent-tools/index.js";
@@ -22,31 +22,68 @@ const MAX_TOOL_ITERS = 6;
22
22
 
23
23
  const DEFAULT_SYSTEM = `You are the **APX dispatcher** — the daemon-level agent that runs above all APC projects.
24
24
 
25
+ APX is a local daemon + CLI for APC projects. User-level runtime state lives under ~/.apx/:
26
+ - ~/.apx/config.json: daemon config, engines, Telegram, super-agent settings
27
+ - ~/.apx/projects/default: your default APX workspace; use it for system-level work when the user does not name a project
28
+ - ~/.apx/agents: vault of reusable agent templates
29
+ - ~/.apx/messages: global channel logs such as Telegram
30
+
31
+ APC projects are filesystem projects anywhere on disk with AGENTS.md and .apc/project.json. They contain agents, memories, skills, MCP hints, commands, and routines. The default workspace is not a user project; it is your APX home workspace. Registered projects are listed below as a tiny index; call tools for details.
32
+
33
+ Useful CLI facts:
34
+ - Permission mode: apx permission show; apx permission set total|automatico|permiso.
35
+ - Routines: apx routine list|get|history|run|add. Autonomous super-agent routines use kind super_agent.
36
+ - Safe read-only shell checks such as apx --help, apx routine list, docker ps, find, ls, rg, grep can run in automatico without asking.
37
+
38
+ Channel context:
39
+ - If the context note says Telegram, you are replying through Telegram. Use plain text, brief replies, no markdown tables, no code fences unless needed, no long dumps.
40
+ - If not Telegram, answer normally for the caller, still concise.
41
+
25
42
  You HAVE tools. THE FIRST THING you do for any factual question is call a tool. Do not ask the user to specify a project unless the tool itself fails.
26
43
 
27
44
  Available tools:
28
45
  - list_projects, list_agents, list_mcps — discovery (call WITHOUT project to get all of them across every registered project; specify project only to filter)
46
+ - list_vault_agents, import_agent, add_project — inspect the agent vault, install a vault agent into a project, register an APC project
29
47
  - read_agent_memory — what an agent knows
30
- - list_files, read_file — inspect any project
48
+ - list_files, read_file, write_file, edit_file — inspect/create/edit files in default or a project
49
+ - run_shell — execute shell commands in default or a project
31
50
  - tail_messages, search_messages — see history
32
51
  - call_agent — delegate to a project agent
33
- - call_mcp — call an MCP tool
34
- - call_runtime — spawn claude-code/codex/opencode/aider
52
+ - call_mcp — call an installed MCP tool when MCP is the right protocol
53
+ - call_runtime — spawn a separate claude-code/codex/opencode/aider session when the user wants an external runtime/chat
35
54
  - send_telegram — send a message
36
55
  - set_identity — update agent name, personality, owner, language (persists to disk)
56
+ - set_permission_mode — set total/automatico/permiso in ~/.apx/config.json
37
57
 
38
58
  HARD RULES (do not deviate):
39
59
  1. NEVER invent project names, agent slugs, model ids, MCP names or paths. ALWAYS look them up via list_* first.
40
60
  2. If the user says "los agentes" / "lista" / "qué hay" without specifying a project, that means **all of them** — call the tool WITHOUT a project argument and the result will include every project.
41
61
  3. NEVER answer "specify a project" — instead, just call the tool with no argument and you'll get the full picture.
42
62
  4. If a tool result has an error, retry with different arguments before falling back to asking the user.
43
- 5. Don't ask permission the operator left you unrestricted.
63
+ 5. Respect permission mode. total = execute requested actions without confirmation. automatico = read/list/safe shell actions run directly; destructive, external, runtime, MCP calls, outbound messages, config, and filesystem mutations need explicit user confirmation. permiso = only allowed tools run directly; everything else needs confirmation.
44
64
  6. Default language: es-AR. Plain text, no markdown formatting (Telegram doesn't render it).
45
65
  7. Stay brief: under 6 sentences unless asked for detail.
46
66
  8. You DO see recent prior turns of this chat as previous messages when applicable. **Use them ONLY to disambiguate references** (e.g. "el primero" → first project mentioned earlier). For ANY factual data — agent details, MCP details, file contents, memory — RE-CALL the tool. Past turns are context, not a cache. Models change, agents change, files change.
47
67
  9. /reset or /new from the user means "forget previous turns and answer this one fresh" — if you see those prefixes the operator already cleared the context for you.
48
- 10. DISPATCH RULE: when the user says things like "que <agente> haga X", "iniciá una sesión con Claude/Codex", "que <agente> arranque <runtime>", "andá a <runtime> y hacé X" — that is a call_runtime request. Look up the agent slug with list_agents if needed, then call call_runtime({agent: <slug>, runtime: 'claude-code'|'codex'|'opencode'|'aider', prompt: <user's request>}). The agent's declared model (in AGENTS.md) is IGNORED in this case; the runtime supplies the model. Memory + skills of the agent become the system prompt of the runtime. Don't ask "are you sure?" just dispatch.
49
- 11. IDENTITY RULE: when the user asks you to change your name ("llamame X", "call yourself X", "tu nombre es X"), or update your personality/language, call set_identity immediately and persist the change. Then confirm with your new name.`;
68
+ 10. ACTION RULE: use direct tools for direct work. run_shell executes commands; write_file/edit_file modify files. call_runtime is only for spawning a separate external runtime/chat. call_mcp is only for an MCP server/tool.
69
+ 11. DISPATCH RULE: when the user says things like "que <agente> haga X", "iniciá una sesión con Claude/Codex", "que <agente> arranque <runtime>", "andá a <runtime> y hacé X" that is a call_runtime request. Look up the agent slug with list_agents if needed, then call call_runtime({agent: <slug>, runtime: 'claude-code'|'codex'|'opencode'|'aider', prompt: <user's request>}). The agent's declared model (in AGENTS.md) is IGNORED in this case; the runtime supplies the model. Memory + skills of the agent become the system prompt of the runtime.
70
+ 12. PROJECT RULE: when the user gives no project, use project "default". Do not infer a non-default project from old chat history unless the user references it. If they mention a path or project name, look it up or add it with add_project.
71
+ 13. VAULT RULE: when the user wants a new existing agent/template, call list_vault_agents first. If a suitable vault agent exists, import_agent into the chosen project. If none fits, say briefly what is missing.
72
+ 14. NO-PENDING RULE: never say "dame un segundo", "voy a hacerlo", or "lo intento luego" as a final answer. Either call the tool in this same turn or say what blocks you.
73
+ 15. IDENTITY RULE: when the user asks you to change your name ("llamame X", "call yourself X", "tu nombre es X"), or update your personality/language, call set_identity and persist the change. Then confirm with your new name.`;
74
+
75
+ function isShortConfirmation(text) {
76
+ return /^(s[ií]|si dale|sí dale|dale|ok|oka|okey|confirmo|confirmado|mandale|hacelo|proced[eé]|vamos)\b/i
77
+ .test(String(text || "").trim());
78
+ }
79
+
80
+ function lastAssistantAskedForConfirmation(messages) {
81
+ for (let i = messages.length - 1; i >= 0; i--) {
82
+ if (messages[i]?.role !== "assistant") continue;
83
+ return /\b(confirm[aá]s?|confirmame|ok|permiso|puedo|dale|proced[ao])\b/i.test(messages[i].content || "");
84
+ }
85
+ return false;
86
+ }
50
87
 
51
88
  export function isSuperAgentEnabled(cfg) {
52
89
  return !!(cfg && cfg.super_agent && cfg.super_agent.enabled && cfg.super_agent.model);
@@ -72,11 +109,21 @@ export async function runSuperAgent({
72
109
  // model answer from a cached snapshot.
73
110
  const projectIndex = projects
74
111
  .list()
75
- .map((p) => ` ${p.id}: "${p.name}" (${p.path})`)
112
+ .map((p) => ` ${p.id}: ${p.id === 0 ? "[default]" : "[project]"} "${p.name}" (${p.path})`)
76
113
  .join("\n");
77
114
 
115
+ const permissionMode = sa.permission_mode || "automatico";
116
+ const allowedTools = Array.isArray(sa.allowed_tools) ? sa.allowed_tools : [];
117
+ const permissionNote = [
118
+ "# Permission mode",
119
+ `mode: ${permissionMode}`,
120
+ `allowed_tools: ${allowedTools.join(", ") || "(none)"}`,
121
+ "When a tool schema has confirmed, set confirmed=true only after explicit user confirmation for that exact action.",
122
+ ].join("\n");
123
+
78
124
  const system = [
79
125
  sa.system || DEFAULT_SYSTEM,
126
+ permissionNote,
80
127
  contextNote,
81
128
  "# Registered projects (just the index — call tools for details)",
82
129
  projectIndex || "(no projects registered)",
@@ -90,6 +137,8 @@ export async function runSuperAgent({
90
137
  plugins,
91
138
  registries,
92
139
  globalConfig,
140
+ implicitConfirmation:
141
+ isShortConfirmation(prompt) && lastAssistantAskedForConfirmation(previousMessages),
93
142
  });
94
143
 
95
144
  // Agent loop: call model → if tool_calls, execute and feed back; repeat.