@agentprojectcontext/apx 1.14.1 → 1.15.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.
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { APX_HOME } from "./config.js";
4
+
5
+ export const LOG_DIR = path.join(APX_HOME, "logs");
6
+ export const ERROR_TRACE_PATH = path.join(LOG_DIR, "errors.jsonl");
7
+
8
+ const SECRET_KEY_RE = /(token|secret|password|api[_-]?key|authorization|bot[_-]?token)/i;
9
+
10
+ function redact(value, seen = new WeakSet()) {
11
+ if (value === null || value === undefined) return value;
12
+ if (typeof value !== "object") return value;
13
+ if (seen.has(value)) return "[circular]";
14
+ seen.add(value);
15
+
16
+ if (Array.isArray(value)) return value.map((item) => redact(item, seen));
17
+
18
+ const out = {};
19
+ for (const [key, val] of Object.entries(value)) {
20
+ out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : redact(val, seen);
21
+ }
22
+ return out;
23
+ }
24
+
25
+ export function appendErrorTrace(record) {
26
+ fs.mkdirSync(LOG_DIR, { recursive: true });
27
+ const entry = {
28
+ ts: new Date().toISOString(),
29
+ ...redact(record),
30
+ };
31
+ fs.appendFileSync(ERROR_TRACE_PATH, JSON.stringify(entry) + "\n", "utf8");
32
+ }
33
+
34
+ export function previewText(text, max = 500) {
35
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
36
+ return clean.length > max ? clean.slice(0, max - 1) + "…" : clean;
37
+ }
@@ -6,9 +6,43 @@ import { fileURLToPath } from "node:url";
6
6
  import { readAgents, readAgentsFromDir, VAULT_DIR } from "./parser.js";
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
10
+ const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
11
+ const RUNTIME_SKILLS_DIR = path.join(__dirname, "runtime-skills");
9
12
 
10
13
  export const SPEC_VERSION = "0.1.0";
11
14
 
15
+ // ---------------------------------------------------------------------------
16
+ // Bundled skills — single source of truth lives at <packageRoot>/skills/<slug>/SKILL.md
17
+ // with proper frontmatter. The `apc-context` copy is refreshed on every
18
+ // install/update from the canonical APC repo (see src/cli/postinstall.js).
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function readBundledSkill(slug) {
22
+ const file = path.join(BUNDLED_SKILLS_DIR, slug, "SKILL.md");
23
+ if (!fs.existsSync(file)) return null;
24
+ return fs.readFileSync(file, "utf8");
25
+ }
26
+
27
+ // Split frontmatter and body from a SKILL.md. Used by IDE targets that need
28
+ // to re-wrap the body in their own rule-file frontmatter.
29
+ function splitFrontmatter(raw) {
30
+ if (!raw.startsWith("---")) return { fm: "", body: raw };
31
+ const end = raw.indexOf("\n---", 3);
32
+ if (end < 0) return { fm: "", body: raw };
33
+ const fm = raw.slice(0, end + 4);
34
+ const body = raw.slice(end + 4).replace(/^\n/, "");
35
+ return { fm, body };
36
+ }
37
+
38
+ // Pull description from frontmatter so cursor/.mdc rule files can advertise
39
+ // the same activation trigger.
40
+ function readDescription(raw) {
41
+ const { fm } = splitFrontmatter(raw);
42
+ const m = fm.match(/^description:\s*"?(.*?)"?\s*$/m);
43
+ return m ? m[1] : "";
44
+ }
45
+
12
46
  // ---------------------------------------------------------------------------
13
47
  // IDE skill targets — written during `apx init` and `apx skills add`
14
48
  // ---------------------------------------------------------------------------
@@ -21,7 +55,8 @@ export const IDE_TARGETS = [
21
55
  label: "Claude Code",
22
56
  ideDir: ".claude",
23
57
  file: ".claude/skills/apx/SKILL.md",
24
- render: (c) => buildSkillMd(c),
58
+ // Claude Code consumes SKILL.md with its native frontmatter as-is.
59
+ render: (raw) => raw,
25
60
  append: false,
26
61
  },
27
62
  {
@@ -29,8 +64,11 @@ export const IDE_TARGETS = [
29
64
  label: "Cursor",
30
65
  ideDir: ".cursor",
31
66
  file: ".cursor/rules/apx.mdc",
32
- render: (c) =>
33
- `---\ndescription: APX CLI skill. Activate when the user asks about running agents, coordinating between agents, or uses apx commands (apx run, apx exec, apx memory, apx mcp, apx session, apx messages).\n---\n\n${c}`,
67
+ render: (raw) => {
68
+ const { body } = splitFrontmatter(raw);
69
+ const desc = readDescription(raw);
70
+ return `---\ndescription: ${desc}\n---\n\n${body}`;
71
+ },
34
72
  append: false,
35
73
  },
36
74
  {
@@ -38,8 +76,11 @@ export const IDE_TARGETS = [
38
76
  label: "Windsurf",
39
77
  ideDir: ".windsurf",
40
78
  file: ".windsurf/rules/apx.md",
41
- render: (c) =>
42
- `---\ntrigger: model_decision\ndescription: APX CLI skill. Activate when the user asks about running agents, coordinating between agents, or uses apx commands (apx run, apx exec, apx memory, apx mcp, apx session, apx messages).\n---\n\n${c}`,
79
+ render: (raw) => {
80
+ const { body } = splitFrontmatter(raw);
81
+ const desc = readDescription(raw);
82
+ return `---\ntrigger: model_decision\ndescription: ${desc}\n---\n\n${body}`;
83
+ },
43
84
  append: false,
44
85
  },
45
86
  {
@@ -47,7 +88,10 @@ export const IDE_TARGETS = [
47
88
  label: "GitHub Copilot",
48
89
  ideDir: ".github",
49
90
  file: ".github/copilot-instructions.md",
50
- render: (c) => `\n<!-- apx-skill -->\n${c}\n<!-- /apx-skill -->\n`,
91
+ render: (raw) => {
92
+ const { body } = splitFrontmatter(raw);
93
+ return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
94
+ },
51
95
  append: true,
52
96
  guard: "<!-- apx-skill -->",
53
97
  },
@@ -56,13 +100,16 @@ export const IDE_TARGETS = [
56
100
  label: "Trae",
57
101
  ideDir: ".trae",
58
102
  file: ".trae/rules/project_rules.md",
59
- render: (c) => `\n<!-- apx-skill -->\n${c}\n<!-- /apx-skill -->\n`,
103
+ render: (raw) => {
104
+ const { body } = splitFrontmatter(raw);
105
+ return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
106
+ },
60
107
  append: true,
61
108
  guard: "<!-- apx-skill -->",
62
109
  },
63
110
  ];
64
111
 
65
- // Global targets (absolute paths, use ~/<dir>/skills/apx/SKILL.md format).
112
+ // Global targets (absolute paths, use ~/<dir>/skills/<slug>/SKILL.md format).
66
113
  // These dirs are read by Claude Code, Cursor (compat), and tools adopting the skills.sh spec.
67
114
  const GLOBAL_SKILL_DIRS = [
68
115
  path.join(os.homedir(), ".claude", "skills"), // Claude Code + Cursor legacy compat
@@ -71,52 +118,23 @@ const GLOBAL_SKILL_DIRS = [
71
118
  path.join(os.homedir(), ".agents", "skills"), // Antigravity/other skills.sh adopters
72
119
  ];
73
120
 
74
- function buildApcContextSkillMd(content) {
75
- const frontmatter = [
76
- "---",
77
- "name: apc-context",
78
- "description: \"ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again.\"",
79
- "homepage: https://github.com/agentprojectcontext/agentprojectcontext",
80
- "---",
81
- "",
82
- ].join("\n");
83
- return frontmatter + content;
84
- }
85
-
86
- function buildSkillMd(content) {
87
- const frontmatter = [
88
- "---",
89
- "name: apx",
90
- "description: \"APX CLI skill. Activate when: user asks to run or coordinate agents, use MCP tools from .apc/mcps.json, install agents from a team workspace, or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', 'run an agent', 'coordinate agents', 'MCP not working', 'install agent', 'team agents', 'apx memory', 'daemon'.\"",
91
- "homepage: https://github.com/agentprojectcontext/apx",
92
- "---",
93
- "",
94
- ].join("\n");
95
- return frontmatter + content;
96
- }
97
-
98
121
  function readRuntimeSkillFiles() {
99
- const skillsDir = path.join(__dirname, "runtime-skills");
100
- if (!fs.existsSync(skillsDir)) return [];
101
-
102
- return fs.readdirSync(skillsDir)
122
+ if (!fs.existsSync(RUNTIME_SKILLS_DIR)) return [];
123
+ return fs.readdirSync(RUNTIME_SKILLS_DIR)
103
124
  .filter((name) => name.endsWith(".md"))
104
125
  .sort()
105
126
  .map((name) => ({
106
127
  slug: path.basename(name, ".md"),
107
- md: fs.readFileSync(path.join(skillsDir, name), "utf8").trim(),
128
+ md: fs.readFileSync(path.join(RUNTIME_SKILLS_DIR, name), "utf8").trim(),
108
129
  }));
109
130
  }
110
131
 
111
132
  // Install APX + APC context skills into IDE rule files. Returns an array of result objects.
112
133
  // targetIds: array of target ids to install; null = all project targets.
113
134
  export function installIdeSkills(root, targetIds = null) {
114
- const apxSrc = path.join(__dirname, "apx-skill.md");
115
- const apcSrc = path.join(__dirname, "apc-context-skill.md");
116
- if (!fs.existsSync(apxSrc)) return [];
117
-
118
- const apxContent = fs.readFileSync(apxSrc, "utf8").trim();
119
- const apcContent = fs.existsSync(apcSrc) ? fs.readFileSync(apcSrc, "utf8").trim() : null;
135
+ const apxRaw = readBundledSkill("apx");
136
+ const apcRaw = readBundledSkill("apc-context");
137
+ if (!apxRaw) return [];
120
138
 
121
139
  const targets = targetIds
122
140
  ? IDE_TARGETS.filter((t) => targetIds.includes(t.id))
@@ -129,10 +147,9 @@ export function installIdeSkills(root, targetIds = null) {
129
147
  continue;
130
148
  }
131
149
 
132
- // Install APX skill
133
150
  const dest = path.join(root, t.file);
134
151
  fs.mkdirSync(path.dirname(dest), { recursive: true });
135
- const rendered = t.render(apxContent);
152
+ const rendered = t.render(apxRaw);
136
153
  if (t.append) {
137
154
  const existing = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
138
155
  if (t.guard && existing.includes(t.guard)) {
@@ -147,31 +164,28 @@ export function installIdeSkills(root, targetIds = null) {
147
164
  results.push({ ...t, status: existed ? "updated" : "created" });
148
165
  }
149
166
 
150
- // Install APC context skill alongside (only for non-append targets with a skills dir)
151
- if (apcContent && t.id === "claude-code") {
167
+ // Install APC context skill alongside Claude Code (dir-style skills dir).
168
+ if (apcRaw && t.id === "claude-code") {
152
169
  const apcDest = path.join(root, ".claude", "skills", "apc-context", "SKILL.md");
153
170
  fs.mkdirSync(path.dirname(apcDest), { recursive: true });
154
171
  const existed = fs.existsSync(apcDest);
155
- fs.writeFileSync(apcDest, buildApcContextSkillMd(apcContent), "utf8");
172
+ fs.writeFileSync(apcDest, apcRaw, "utf8");
156
173
  results.push({ ...t, id: "claude-code/apc-context", label: "Claude Code (apc-context)", file: apcDest, status: existed ? "updated" : "created" });
157
174
  }
158
175
  }
159
176
  return results;
160
177
  }
161
178
 
162
- // Install both APX and APC context skills to global ~/.../skills/ dirs.
179
+ // Install bundled APX/APC skills + runtime docs to global ~/.../skills/ dirs.
163
180
  // Returns an array of result objects with { dir, skill, status }.
164
181
  export function installGlobalSkills() {
165
182
  const results = [];
166
183
 
167
- const apxSrc = path.join(__dirname, "apx-skill.md");
168
- const apcSrc = path.join(__dirname, "apc-context-skill.md");
169
-
170
184
  const skills = [];
171
- if (fs.existsSync(apxSrc))
172
- skills.push({ slug: "apx", md: buildSkillMd(fs.readFileSync(apxSrc, "utf8").trim()) });
173
- if (fs.existsSync(apcSrc))
174
- skills.push({ slug: "apc-context", md: buildApcContextSkillMd(fs.readFileSync(apcSrc, "utf8").trim()) });
185
+ const apxRaw = readBundledSkill("apx");
186
+ const apcRaw = readBundledSkill("apc-context");
187
+ if (apxRaw) skills.push({ slug: "apx", md: apxRaw });
188
+ if (apcRaw) skills.push({ slug: "apc-context", md: apcRaw });
175
189
  skills.push(...readRuntimeSkillFiles());
176
190
 
177
191
  for (const base of GLOBAL_SKILL_DIRS) {
package/src/daemon/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Express REST API for APX. See APC docs reference/apx-daemon.
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
4
5
  import { execFile } from "node:child_process";
5
6
  import express from "express";
6
7
  import { buildBrowserRouter } from "./tools/browser.js";
@@ -49,6 +50,7 @@ import { readAgents } from "../core/parser.js";
49
50
  import { parseSessionFrontmatter } from "../core/parser.js";
50
51
  import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
51
52
  import { buildAgentSystem } from "../core/agent-system.js";
53
+ import { appendErrorTrace, previewText } from "../core/logging.js";
52
54
  import {
53
55
  createArtifact,
54
56
  listArtifacts,
@@ -58,11 +60,34 @@ import {
58
60
 
59
61
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
60
62
 
63
+ function appendSuperAgentErrorTrace(req, error, details = {}) {
64
+ appendErrorTrace({
65
+ trace_id: req.apxTraceId,
66
+ surface: "daemon_api",
67
+ route: `${req.method} ${req.route?.path || req.path}`,
68
+ project_id: req.params?.pid || null,
69
+ channel: /Channel:\s*([^\n]+)/i.exec(details.contextNote || "")?.[1]?.trim() || null,
70
+ model: details.model || null,
71
+ stream: !!details.stream,
72
+ prompt_preview: previewText(details.prompt),
73
+ previous_messages: Array.isArray(details.previousMessages) ? details.previousMessages.length : 0,
74
+ error: {
75
+ message: error?.message || String(error),
76
+ stack: error?.stack || null,
77
+ },
78
+ });
79
+ }
80
+
61
81
  export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
62
82
  const telegram = plugins?.get("telegram");
63
83
 
64
84
  const app = express();
65
85
  app.use(express.json({ limit: "2mb" }));
86
+ app.use((req, res, next) => {
87
+ req.apxTraceId = req.get("x-apx-trace-id") || randomUUID();
88
+ res.setHeader("x-apx-trace-id", req.apxTraceId);
89
+ next();
90
+ });
66
91
 
67
92
  // ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
68
93
  // fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
@@ -636,7 +661,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
636
661
  });
637
662
  res.end();
638
663
  } catch (e) {
639
- send({ type: "error", error: e.message });
664
+ appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: true });
665
+ send({ type: "error", trace_id: req.apxTraceId, error: `${e.message} (trace: ${req.apxTraceId})` });
640
666
  res.end();
641
667
  }
642
668
  });
@@ -665,7 +691,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
665
691
  trace: saResult.trace,
666
692
  });
667
693
  } catch (e) {
668
- res.status(500).json({ error: e.message });
694
+ appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: false });
695
+ res.status(500).json({ error: e.message, trace_id: req.apxTraceId });
669
696
  }
670
697
  });
671
698
 
@@ -11,7 +11,7 @@ function getKey(config) {
11
11
  export default {
12
12
  id: "anthropic",
13
13
 
14
- async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
14
+ async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
15
15
  const key = getKey(config);
16
16
  if (!key) throw new Error("anthropic: no api_key (set ANTHROPIC_API_KEY or engines.anthropic.api_key)");
17
17
  if (!model) throw new Error("anthropic: model required");
@@ -47,6 +47,7 @@ export default {
47
47
  "anthropic-version": API_VERSION,
48
48
  },
49
49
  body: JSON.stringify(body),
50
+ signal,
50
51
  });
51
52
  const json = await res.json();
52
53
  if (!res.ok) {
@@ -10,7 +10,7 @@ function getKey(config) {
10
10
  export default {
11
11
  id: "gemini",
12
12
 
13
- async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {} }) {
13
+ async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {}, signal }) {
14
14
  const key = getKey(config);
15
15
  if (!key) throw new Error("gemini: no api_key (set GEMINI_API_KEY or engines.gemini.api_key)");
16
16
  if (!model) throw new Error("gemini: model required");
@@ -33,6 +33,7 @@ export default {
33
33
  method: "POST",
34
34
  headers: { "content-type": "application/json" },
35
35
  body: JSON.stringify(body),
36
+ signal,
36
37
  });
37
38
  const json = await res.json();
38
39
  if (!res.ok) {
@@ -46,11 +46,10 @@ export function getAdapter(provider) {
46
46
  return a;
47
47
  }
48
48
 
49
- export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice }) {
49
+ export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice, signal }) {
50
50
  const { provider, model } = resolveProvider(modelId);
51
51
  const adapter = getAdapter(provider);
52
- const providerCfg =
53
- (config && config.engines && config.engines[provider]) || {};
52
+ const providerCfg = (config && config.engines && config.engines[provider]) || {};
54
53
  return adapter.chat({
55
54
  system,
56
55
  messages,
@@ -60,6 +59,7 @@ export async function callEngine({ modelId, system, messages, config, temperatur
60
59
  tools,
61
60
  toolChoice,
62
61
  config: providerCfg,
62
+ signal,
63
63
  });
64
64
  }
65
65
 
@@ -8,7 +8,7 @@ function baseUrl(config) {
8
8
  export default {
9
9
  id: "ollama",
10
10
 
11
- async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {} }) {
11
+ async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {}, signal }) {
12
12
  if (!model) throw new Error("ollama: model required");
13
13
 
14
14
  // The caller can pass `messages` as either:
@@ -45,6 +45,7 @@ export default {
45
45
  method: "POST",
46
46
  headers: { "content-type": "application/json" },
47
47
  body: JSON.stringify(body),
48
+ signal,
48
49
  });
49
50
  if (!res.ok) {
50
51
  const text = await res.text();
@@ -10,7 +10,7 @@ function getKey(config) {
10
10
  export default {
11
11
  id: "openai",
12
12
 
13
- async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
13
+ async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
14
14
  const key = getKey(config);
15
15
  if (!key) throw new Error("openai: no api_key (set OPENAI_API_KEY or engines.openai.api_key)");
16
16
  if (!model) throw new Error("openai: model required");
@@ -52,6 +52,7 @@ export default {
52
52
  authorization: `Bearer ${key}`,
53
53
  },
54
54
  body: JSON.stringify(body),
55
+ signal,
55
56
  });
56
57
  const json = await res.json();
57
58
  if (!res.ok) {
@@ -290,6 +290,7 @@ class ChannelPoller {
290
290
  this.polling = false;
291
291
  this.lastError = null;
292
292
  this.lastUpdateAt = null;
293
+ this.activeRequests = new Map(); // chat_id -> AbortController
293
294
  }
294
295
 
295
296
  resolveProject() {
@@ -390,7 +391,19 @@ class ChannelPoller {
390
391
  ? "@" + msg.from.username
391
392
  : `${msg.from?.first_name || ""} ${msg.from?.last_name || ""}`.trim() || "unknown";
392
393
  const chat_id = msg.chat?.id;
393
- const text = msg.text || msg.caption || "";
394
+
395
+ // Default Interrupt: abort any running request for this chat_id
396
+ if (chat_id) {
397
+ const prev = this.activeRequests.get(chat_id);
398
+ if (prev) {
399
+ this.log(`telegram[${this.channel.name}] interrupting previous request for chat ${chat_id}`);
400
+ prev.abort();
401
+ }
402
+ }
403
+ const abortCtrl = new AbortController();
404
+ if (chat_id) this.activeRequests.set(chat_id, abortCtrl);
405
+
406
+ let text = msg.text || msg.caption || "";
394
407
 
395
408
  // ── Incoming photo handling ───────────────────────────────────────────
396
409
  if (msg.photo && msg.photo.length > 0) {
@@ -617,16 +630,22 @@ class ChannelPoller {
617
630
  prompt: text,
618
631
  previousMessages,
619
632
  contextNote: `You are replying inside Telegram right now. Telegram channel="${this.channel.name}", author=${author}, chat_id=${chat_id}. Keep the reply plain-text and concise. Previous turns of this chat are included only for local conversational context; re-call tools for facts.`,
633
+ signal: abortCtrl.signal,
620
634
  });
621
635
  replyText = sa.text;
622
636
  replyAuthor = sa.name;
623
637
  saTrace = sa.trace;
624
638
  saUsage = sa.usage;
625
639
  } catch (e) {
640
+ if (abortCtrl.signal.aborted) {
641
+ this.log(`telegram[${this.channel.name}] request aborted for chat ${chat_id}`);
642
+ return; // don't send reply if aborted
643
+ }
626
644
  this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
627
645
  }
628
646
  }
629
647
 
648
+ if (chat_id) this.activeRequests.delete(chat_id);
630
649
  if (!replyText) {
631
650
  stopTyping();
632
651
  return;
@@ -1,26 +1,28 @@
1
1
  // daemon/skills-loader.js
2
2
  // Discover and load APX skills on-demand for the super-agent.
3
3
  //
4
- // The super-agent reads skills from immutable INTERNAL sources under
5
- // src/core/ — they ship with apx and can never be deleted by the user. This
6
- // guarantees apx/apc/runtime knowledge is always available regardless of
7
- // what the user does to ~/.apx/skills/. Distribution copies under
8
- // <package>/skills/ are a separate concern (scaffold.js handles them) and
9
- // the loader does NOT read from there.
4
+ // The super-agent reads skills from immutable INTERNAL sources under the
5
+ // package root — they ship with apx and can never be deleted by the user.
6
+ // This guarantees apx/apc/runtime knowledge is always available regardless
7
+ // of what the user does to ~/.apx/skills/ or per-project overrides.
10
8
  //
11
9
  // Discovery order (priority high → low):
12
- // 1. <projectPath>/.apc/skills/<slug>.md ← project-scoped
13
- // 1b.<projectPath>/.apc/skills/<slug>/SKILL.md same, dir-style
14
- // 2. ~/.apx/skills/<slug>/SKILL.md ← user-installed global
15
- // 3. <packageRoot>/src/core/runtime-skills/<slug>.md built-in runtime docs
16
- // (claude-code, codex-cli,
17
- // opencode-cli, openrouter)
18
- // 4. <packageRoot>/src/core/apx-skill.md ← built-in intrinsic apx
19
- // 4b.<packageRoot>/src/core/apc-context-skill.md ← built-in intrinsic apc-context
10
+ // 1. <projectPath>/.apc/skills/<slug>.md ← project-scoped (flat)
11
+ // 1b.<projectPath>/.apc/skills/<slug>/SKILL.md project-scoped (dir)
12
+ // 2. ~/.apx/skills/<slug>/SKILL.md ← user-installed global
13
+ // 3. <packageRoot>/skills/<slug>/SKILL.md bundled core skills
14
+ // (apx, apc-context)
15
+ // 4. <packageRoot>/src/core/runtime-skills/<slug>.md
16
+ // (claude-code, codex-cli,
17
+ // opencode-cli, openrouter)
20
18
  //
21
- // A slug found in a higher-priority location SHADOWS lower ones so a user
22
- // who drops `~/.apx/skills/apx/SKILL.md` overrides the intrinsic one, but the
23
- // intrinsic stays in the package as a safety net.
19
+ // A slug found in a higher-priority location SHADOWS lower ones. A user can
20
+ // override the bundled apc-context by dropping `~/.apx/skills/apc-context/SKILL.md`,
21
+ // but the bundled copy stays in the package as a safety net.
22
+ //
23
+ // Note: the bundled `apc-context` skill is REFRESHED from the canonical apc
24
+ // repo on every npm install / update (see src/cli/postinstall.js). APC is a
25
+ // living standard, so its skill content is not pinned to an apx version.
24
26
 
25
27
  import fs from "node:fs";
26
28
  import path from "node:path";
@@ -32,38 +34,8 @@ const __dirname = path.dirname(__filename);
32
34
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
33
35
 
34
36
  const RUNTIME_SKILLS_DIR = path.join(PACKAGE_ROOT, "src", "core", "runtime-skills");
37
+ const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
35
38
  const GLOBAL_DIR = path.join(os.homedir(), ".apx", "skills");
36
- const CORE_DIR = path.join(PACKAGE_ROOT, "src", "core");
37
-
38
- // Intrinsic built-in skills whose source files (src/core/*-skill.md) do NOT
39
- // carry frontmatter — the scaffold.js wrapper adds frontmatter when copying
40
- // these out to external IDE skill dirs. For the super-agent's catalog we
41
- // supply slug + description inline. Keep in sync with scaffold.js.
42
- const INTRINSIC = [
43
- {
44
- slug: "apx",
45
- file: path.join(CORE_DIR, "apx-skill.md"),
46
- description:
47
- "APX CLI skill. Activate when: user asks to run or coordinate agents, " +
48
- "use MCP tools from .apc/mcps.json, install agents from a team workspace, " +
49
- "or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — " +
50
- "that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', " +
51
- "'run an agent', 'coordinate agents', 'MCP not working', 'install agent', " +
52
- "'team agents', 'apx memory', 'daemon'.",
53
- },
54
- {
55
- slug: "apc-context",
56
- file: path.join(CORE_DIR, "apc-context-skill.md"),
57
- description:
58
- "ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. " +
59
- "Do not wait to be asked. Read .apc/ before making any assumption about agents, " +
60
- "memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', " +
61
- "'list agents', 'agent context', 'who are the agents', any question about agents " +
62
- "or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the " +
63
- "conversation with a migration offer before answering anything else. If the user " +
64
- "declines, delete .apc/migrate.md immediately so it is not shown again.",
65
- },
66
- ];
67
39
 
68
40
  // ---------------------------------------------------------------------------
69
41
  // Frontmatter parsing (minimal — handles the YAML we ship)
@@ -153,15 +125,11 @@ export function listSkills({ projectPath } = {}) {
153
125
  // priority 2: user-installed global
154
126
  found.push(...scanDirStyle(GLOBAL_DIR, "global"));
155
127
 
156
- // priority 3: built-in runtime docs (have frontmatter)
157
- found.push(...scanFlatStyle(RUNTIME_SKILLS_DIR, "builtin"));
128
+ // priority 3: bundled core skills (apx, apc-context)
129
+ found.push(...scanDirStyle(BUNDLED_SKILLS_DIR, "builtin"));
158
130
 
159
- // priority 4: intrinsic built-ins (no frontmatter descriptions hardcoded)
160
- for (const it of INTRINSIC) {
161
- if (fs.existsSync(it.file)) {
162
- found.push({ slug: it.slug, source: "builtin", file: it.file, _description: it.description });
163
- }
164
- }
131
+ // priority 4: runtime docs (claude-code, codex-cli, opencode-cli, openrouter)
132
+ found.push(...scanFlatStyle(RUNTIME_SKILLS_DIR, "builtin"));
165
133
 
166
134
  // dedupe by slug (first-wins = higher priority shadows lower)
167
135
  const seen = new Set();
@@ -170,15 +138,12 @@ export function listSkills({ projectPath } = {}) {
170
138
  if (seen.has(entry.slug)) continue;
171
139
  seen.add(entry.slug);
172
140
 
173
- // Description: prefer inline (intrinsic) → frontmatter → empty
174
- let description = entry._description || "";
175
- if (!description) {
176
- try {
177
- const raw = fs.readFileSync(entry.file, "utf8");
178
- const { fm } = parseFrontmatter(raw);
179
- description = fm.description || "";
180
- } catch { /* unreadable — skip description */ }
181
- }
141
+ let description = "";
142
+ try {
143
+ const raw = fs.readFileSync(entry.file, "utf8");
144
+ const { fm } = parseFrontmatter(raw);
145
+ description = fm.description || "";
146
+ } catch { /* unreadable skip description */ }
182
147
 
183
148
  result.push({
184
149
  slug: entry.slug,
@@ -223,6 +188,6 @@ export function loadSkill(slug, { projectPath } = {}) {
223
188
  // Useful for diagnostics
224
189
  export const SKILL_LOCATIONS = {
225
190
  runtime_skills: RUNTIME_SKILLS_DIR,
226
- intrinsic: CORE_DIR,
191
+ bundled: BUNDLED_SKILLS_DIR,
227
192
  global: GLOBAL_DIR,
228
193
  };
@@ -14,7 +14,14 @@ import { readAgents } from "../core/parser.js";
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
16
16
 
17
- const EXAMPLE = path.resolve(__dirname, "..", "..", "examples", "my-first-project");
17
+ const EXAMPLE_CANDIDATES = [
18
+ path.resolve(__dirname, "..", "..", "examples", "my-first-project"),
19
+ path.resolve(__dirname, "..", "..", "..", "apc", "examples", "my-first-project"),
20
+ ];
21
+ const EXAMPLE = EXAMPLE_CANDIDATES.find((p) =>
22
+ fs.existsSync(path.join(p, "AGENTS.md")) &&
23
+ fs.existsSync(path.join(p, ".apc", "project.json"))
24
+ );
18
25
 
19
26
  function assert(cond, msg) {
20
27
  if (!cond) {
@@ -24,6 +31,7 @@ function assert(cond, msg) {
24
31
  }
25
32
 
26
33
  const projects = new ProjectManager();
34
+ assert(EXAMPLE, `example project missing; checked ${EXAMPLE_CANDIDATES.join(", ")}`);
27
35
  const entry = projects.register(EXAMPLE);
28
36
  console.log("registered project", entry.id, entry.path);
29
37
 
@@ -22,6 +22,7 @@ import searchFiles from "./tools/search-files.js";
22
22
  import listSkills from "./tools/list-skills.js";
23
23
  import loadSkill from "./tools/load-skill.js";
24
24
  import transcribeAudio from "./tools/transcribe-audio.js";
25
+ import askQuestions from "./tools/ask-questions.js";
25
26
  import { createPermissionGuard } from "./helpers.js";
26
27
  import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
27
28
 
@@ -50,6 +51,7 @@ const NATIVE_TOOLS = [
50
51
  listSkills,
51
52
  loadSkill,
52
53
  transcribeAudio,
54
+ askQuestions,
53
55
  ];
54
56
 
55
57
  // Registry-backed bridges. Categories can be overridden per-process via env