@agentprojectcontext/apx 1.11.0 → 1.13.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: apc-context
3
+ 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."
4
+ homepage: https://github.com/agentprojectcontext/apx
5
+ ---
6
+
7
+ # Agent Project Context
8
+
9
+ This project uses APC. APC stores portable project context in `.apc/` and `AGENTS.md`.
10
+
11
+ APC does not store raw runtime sessions. Sessions, conversations, messages, caches, provider
12
+ threads, and private runtime memory stay in the IDE, CLI, daemon, or user-level store that created
13
+ them.
14
+
15
+ ## FIRST: check for pending migration
16
+
17
+ Before doing anything else, check if `.apc/migrate.md` exists:
18
+
19
+ ```bash
20
+ cat .apc/migrate.md 2>/dev/null
21
+ ```
22
+
23
+ If it exists, offer to migrate before answering anything else. Read detected files, separate durable
24
+ project context from runtime/private state, and migrate only what belongs in APC.
25
+
26
+ If the user says no or later, delete `.apc/migrate.md` so the offer is not repeated.
27
+
28
+ ## Migration rule: think, do not copy
29
+
30
+ Classify content:
31
+
32
+ | Content | Action |
33
+ |---|---|
34
+ | Agent definitions: role, model, skills, description | Put in `.apc/agents/<slug>.md` and/or `AGENTS.md` |
35
+ | Shared project rules, stack notes, commands, testing policy | Keep in `AGENTS.md` |
36
+ | Reusable instruction blocks | Move to `.apc/skills/<name>.md` |
37
+ | Durable safe facts useful to all contributors | Add to `.apc/agents/<slug>/memory.md` only after curation |
38
+ | MCP expectations without secrets | Add to `.apc/mcps.json` |
39
+ | Raw sessions, transcripts, conversations, messages, tool logs | Do not move into `.apc/`; leave with source runtime |
40
+ | Secrets, tokens, credentials, private headers | Do not store in repository |
41
+ | IDE UI settings or personal aliases | Leave in IDE/user config |
42
+ | Instructions to store sessions under `.apc/` | Drop as obsolete |
43
+
44
+ ## APC structure
45
+
46
+ ```text
47
+ AGENTS.md ← root project contract
48
+ .apc/
49
+ project.json ← project metadata
50
+ .gitignore ← safety guard
51
+ agents/<slug>.md ← agent definition
52
+ agents/<slug>/memory.md ← optional curated project memory
53
+ skills/<name>.md ← reusable project instructions
54
+ mcps.json ← MCP hints without secrets
55
+ ```
56
+
57
+ Do not store:
58
+
59
+ ```text
60
+ .apc/agents/<slug>/sessions/
61
+ .apc/sessions/
62
+ .apc/conversations/
63
+ .apc/messages/
64
+ .apc/project.db
65
+ .apc/cache/
66
+ .apc/tmp/
67
+ .apc/private/
68
+ .apc/secrets/
69
+ ```
70
+
71
+ ## Operating rules
72
+
73
+ 1. Read `AGENTS.md` and relevant `.apc/` files before assuming project context.
74
+ 2. Read agent definitions from `.apc/agents/<slug>.md` when present.
75
+ 3. Read curated project memory from `.apc/agents/<slug>/memory.md` when present.
76
+ 4. Write only durable, safe, curated facts to APC memory.
77
+ 5. Never write raw sessions, transcripts, messages, conversations, or tool logs into `.apc/`.
78
+ 6. Keep secrets out of APC and out of git.
79
+ 7. Treat `.apc/mcps.json` as MCP configuration hints, not as an MCP implementation.
80
+
81
+ ## Sessions
82
+
83
+ Sessions belong to the runtime that created them.
84
+
85
+ Examples:
86
+
87
+ ```text
88
+ Codex runtime storage
89
+ Claude Code runtime storage
90
+ OpenCode runtime storage
91
+ ~/.apx/projects/<project-id>/agents/<slug>/sessions/
92
+ ```
93
+
94
+ At task end, provide the user a concise result. If project memory should be updated, write a short
95
+ sanitized fact to `.apc/agents/<slug>/memory.md` only when useful and safe.
96
+
97
+ ## APX
98
+
99
+ APX can provide a local daemon, MCP management, Telegram bridge, routines, and runtime dispatch
100
+ across Codex, Claude Code, OpenCode, Aider, Cursor Agent, Gemini CLI, Qwen Code, or direct LLM
101
+ engines. Those are APX runtime features, not APC portable-core requirements.
102
+
103
+ The APX super-agent uses `~/.apx/projects/default` for system-level work when no project is named.
104
+ APX routines can run heartbeat, shell, Telegram, project agent, or super-agent tasks on a schedule.
105
+
106
+ APX runtime state belongs outside the repository:
107
+
108
+ ```text
109
+ ~/.apx/projects/<project-id>/
110
+ ```
@@ -359,9 +359,14 @@ async function runPrompt(pid, state, previousMessages, renderScreen, text, userI
359
359
  const startTime = Date.now();
360
360
 
361
361
  try {
362
+ const cwd = process.cwd();
362
363
  const body = {
363
364
  prompt: `[Mode: ${MODES[state.currentModeIdx]}]\n${text}`,
364
- contextNote: "Channel: terminal. Format freely using markdown, but keep it readable. Use code diffs when editing.",
365
+ contextNote: [
366
+ "Channel: terminal. Format freely using markdown, but keep it readable. Use code diffs when editing.",
367
+ `CWD: ${cwd}`,
368
+ "When the user says \"este directorio\", \"este proyecto\", \"acá\", \"aquí\", \"this directory\", \"current dir\" or any equivalent reference without naming a path, they mean exactly the CWD above. Use it as the path argument directly — don't ask the user to provide it.",
369
+ ].join("\n"),
365
370
  previousMessages,
366
371
  model: state.activeModel,
367
372
  };
@@ -0,0 +1,228 @@
1
+ // daemon/skills-loader.js
2
+ // Discover and load APX skills on-demand for the super-agent.
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.
10
+ //
11
+ // 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
20
+ //
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.
24
+
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import os from "node:os";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = path.dirname(__filename);
32
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
33
+
34
+ const RUNTIME_SKILLS_DIR = path.join(PACKAGE_ROOT, "src", "core", "runtime-skills");
35
+ 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
+
68
+ // ---------------------------------------------------------------------------
69
+ // Frontmatter parsing (minimal — handles the YAML we ship)
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function parseFrontmatter(raw) {
73
+ if (!raw.startsWith("---")) return { fm: {}, body: raw };
74
+ const end = raw.indexOf("\n---", 3);
75
+ if (end < 0) return { fm: {}, body: raw };
76
+
77
+ const fmBlock = raw.slice(3, end).trim();
78
+ const body = raw.slice(end + 4).replace(/^\n/, "");
79
+
80
+ const fm = {};
81
+ for (const line of fmBlock.split("\n")) {
82
+ const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/);
83
+ if (!m) continue;
84
+ let val = m[2].trim();
85
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
86
+ val = val.slice(1, -1);
87
+ }
88
+ fm[m[1]] = val;
89
+ }
90
+ return { fm, body };
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Directory scanners
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /** Returns [{slug, source, file}] from a directory using <slug>/SKILL.md layout. */
98
+ function scanDirStyle(baseDir, source) {
99
+ if (!baseDir || !fs.existsSync(baseDir)) return [];
100
+ const out = [];
101
+ let entries;
102
+ try { entries = fs.readdirSync(baseDir, { withFileTypes: true }); }
103
+ catch { return []; }
104
+ for (const e of entries) {
105
+ if (!e.isDirectory()) continue;
106
+ const file = path.join(baseDir, e.name, "SKILL.md");
107
+ if (fs.existsSync(file)) out.push({ slug: e.name, source, file });
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** Returns [{slug, source, file}] from a directory using <slug>.md layout. */
113
+ function scanFlatStyle(baseDir, source) {
114
+ if (!baseDir || !fs.existsSync(baseDir)) return [];
115
+ const out = [];
116
+ let entries;
117
+ try { entries = fs.readdirSync(baseDir, { withFileTypes: true }); }
118
+ catch { return []; }
119
+ for (const e of entries) {
120
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
121
+ if (e.name === "README.md") continue;
122
+ out.push({
123
+ slug: e.name.replace(/\.md$/, ""),
124
+ source,
125
+ file: path.join(baseDir, e.name),
126
+ });
127
+ }
128
+ return out;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Public API
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Discover available skills across all locations.
137
+ * Returns lightweight metadata only — body is NOT read.
138
+ *
139
+ * @param {object} opts
140
+ * @param {string=} opts.projectPath optional project root to also scan
141
+ * @returns {Array<{slug, source, description, file}>}
142
+ */
143
+ export function listSkills({ projectPath } = {}) {
144
+ const found = [];
145
+
146
+ // priority 1: project-scoped
147
+ if (projectPath) {
148
+ const apcSkills = path.join(projectPath, ".apc", "skills");
149
+ found.push(...scanDirStyle(apcSkills, "project"));
150
+ found.push(...scanFlatStyle(apcSkills, "project"));
151
+ }
152
+
153
+ // priority 2: user-installed global
154
+ found.push(...scanDirStyle(GLOBAL_DIR, "global"));
155
+
156
+ // priority 3: built-in runtime docs (have frontmatter)
157
+ found.push(...scanFlatStyle(RUNTIME_SKILLS_DIR, "builtin"));
158
+
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
+ }
165
+
166
+ // dedupe by slug (first-wins = higher priority shadows lower)
167
+ const seen = new Set();
168
+ const result = [];
169
+ for (const entry of found) {
170
+ if (seen.has(entry.slug)) continue;
171
+ seen.add(entry.slug);
172
+
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
+ }
182
+
183
+ result.push({
184
+ slug: entry.slug,
185
+ source: entry.source,
186
+ description,
187
+ file: entry.file,
188
+ });
189
+ }
190
+ return result;
191
+ }
192
+
193
+ /**
194
+ * Load the full body of a skill (frontmatter stripped if present). Resolves
195
+ * via the same priority chain as listSkills().
196
+ *
197
+ * @param {string} slug
198
+ * @param {object} opts
199
+ * @param {string=} opts.projectPath
200
+ * @returns {{slug, source, file, description, body, frontmatter}}
201
+ */
202
+ export function loadSkill(slug, { projectPath } = {}) {
203
+ if (!slug) throw new Error("loadSkill: slug required");
204
+
205
+ const list = listSkills({ projectPath });
206
+ const entry = list.find(s => s.slug === slug);
207
+ if (!entry) {
208
+ throw new Error(`skill "${slug}" not found. Available: ${list.map(s => s.slug).join(", ") || "(none)"}`);
209
+ }
210
+
211
+ const raw = fs.readFileSync(entry.file, "utf8");
212
+ const { fm, body } = parseFrontmatter(raw);
213
+ return {
214
+ slug: entry.slug,
215
+ source: entry.source,
216
+ file: entry.file,
217
+ description: entry.description || fm.description || "",
218
+ frontmatter: fm,
219
+ body: body.trim(),
220
+ };
221
+ }
222
+
223
+ // Useful for diagnostics
224
+ export const SKILL_LOCATIONS = {
225
+ runtime_skills: RUNTIME_SKILLS_DIR,
226
+ intrinsic: CORE_DIR,
227
+ global: GLOBAL_DIR,
228
+ };
@@ -19,9 +19,12 @@ import sendTelegram from "./tools/send-telegram.js";
19
19
  import setIdentity from "./tools/set-identity.js";
20
20
  import setPermissionMode from "./tools/set-permission-mode.js";
21
21
  import searchFiles from "./tools/search-files.js";
22
+ import listSkills from "./tools/list-skills.js";
23
+ import loadSkill from "./tools/load-skill.js";
22
24
  import { createPermissionGuard } from "./helpers.js";
25
+ import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
23
26
 
24
- const TOOLS = [
27
+ const NATIVE_TOOLS = [
25
28
  listProjects,
26
29
  listAgents,
27
30
  listVaultAgents,
@@ -43,8 +46,22 @@ const TOOLS = [
43
46
  setIdentity,
44
47
  setPermissionMode,
45
48
  searchFiles,
49
+ listSkills,
50
+ loadSkill,
46
51
  ];
47
52
 
53
+ // Registry-backed bridges. Categories can be overridden per-process via env
54
+ // APX_BRIDGE_CATEGORIES (comma-separated), e.g. "browser,fetch,search".
55
+ // Default: browser, fetch, search, glob, grep (see registry-bridge.js).
56
+ function resolveBridgeCategories() {
57
+ const env = (process.env.APX_BRIDGE_CATEGORIES || "").trim();
58
+ if (!env) return DEFAULT_CATEGORIES;
59
+ return new Set(env.split(",").map(s => s.trim()).filter(Boolean));
60
+ }
61
+
62
+ const BRIDGED_TOOLS = buildBridgedTools({ categories: resolveBridgeCategories() });
63
+ const TOOLS = [...NATIVE_TOOLS, ...BRIDGED_TOOLS];
64
+
48
65
  export const TOOL_SCHEMAS = TOOLS.map((tool) => tool.schema);
49
66
 
50
67
  export function makeToolHandlers(ctx) {
@@ -56,3 +73,8 @@ export function makeToolHandlers(ctx) {
56
73
  };
57
74
  return Object.fromEntries(TOOLS.map((tool) => [tool.name, tool.makeHandler(toolCtx)]));
58
75
  }
76
+
77
+ // Diagnostic helper — useful for `apx daemon status` or debug logging.
78
+ export function listBridgedToolNames() {
79
+ return BRIDGED_TOOLS.map(t => t.name);
80
+ }
@@ -0,0 +1,122 @@
1
+ // daemon/super-agent-tools/registry-bridge.js
2
+ //
3
+ // Generic bridge that exposes registry-backed HTTP tools (browser, fetch,
4
+ // search, glob, grep, etc.) to the super-agent — no per-tool import boilerplate.
5
+ //
6
+ // How it works:
7
+ // 1. Read TOOL_DEFINITIONS from daemon/tools/registry.js
8
+ // 2. Drop entries whose names collide with native super-agent tools (those
9
+ // win — they touch in-process state directly).
10
+ // 3. For each remaining entry, produce { name, schema, makeHandler } in the
11
+ // exact shape index.js expects, so they slot into TOOL_SCHEMAS alongside
12
+ // the native ones.
13
+ // 4. The generated handler POSTs/GETs to the daemon's own HTTP server on
14
+ // 127.0.0.1:<port>. Yes, the super-agent talks to its own daemon — that
15
+ // keeps the bridge dead-simple, lets the engine adapter format tool
16
+ // schemas uniformly, and reuses the exact code path external callers hit.
17
+ //
18
+ // Net result: adding a tool = adding one entry to registry.js. No file in
19
+ // super-agent-tools/tools/, no import in index.js.
20
+
21
+ import { TOOL_DEFINITIONS } from "../tools/registry.js";
22
+
23
+ // Native handlers in super-agent-tools/tools/ that own these names. The bridge
24
+ // MUST skip them or the registry version (HTTP roundtrip) would shadow the
25
+ // native one with possibly different semantics.
26
+ const NATIVE_NAMES = new Set([
27
+ "list_projects", "list_agents", "list_vault_agents", "import_agent",
28
+ "add_project", "list_mcps", "read_agent_memory",
29
+ "list_files", "read_file", "write_file", "edit_file", "search_files",
30
+ "run_shell", "tail_messages", "search_messages",
31
+ "call_agent", "call_mcp", "call_runtime",
32
+ "send_telegram", "set_identity", "set_permission_mode",
33
+ ]);
34
+
35
+ // Default allow-list of categories the bridge will expose. The NATIVE_NAMES
36
+ // filter handles duplicates inside these categories (e.g. "file" contains
37
+ // both read_file [native] and glob [bridged]). Anything outside is ignored
38
+ // — "shell"/"mcp"/"memory"/"session" have different semantics handled
39
+ // natively. Override with env APX_BRIDGE_CATEGORIES.
40
+ const DEFAULT_CATEGORIES = new Set(["browser", "fetch", "search", "file"]);
41
+
42
+ function buildSchema(entry) {
43
+ return {
44
+ type: "function",
45
+ function: {
46
+ name: entry.name,
47
+ description: entry.description,
48
+ parameters: entry.parameters || { type: "object", properties: {} },
49
+ },
50
+ };
51
+ }
52
+
53
+ function buildHandler(entry) {
54
+ return ({ globalConfig }) => async (args = {}) => {
55
+ const port = globalConfig?.port || process.env.APX_PORT || 7430;
56
+ const method = String(entry.endpoint?.method || "POST").toUpperCase();
57
+ let url = `http://127.0.0.1:${port}${entry.endpoint?.path || ""}`;
58
+
59
+ const opts = {
60
+ method,
61
+ headers: { "content-type": "application/json" },
62
+ };
63
+
64
+ if (method === "GET" || method === "HEAD") {
65
+ const qs = new URLSearchParams();
66
+ for (const [k, v] of Object.entries(args)) {
67
+ if (v === undefined || v === null) continue;
68
+ qs.set(k, typeof v === "object" ? JSON.stringify(v) : String(v));
69
+ }
70
+ const q = qs.toString();
71
+ if (q) url += (url.includes("?") ? "&" : "?") + q;
72
+ } else {
73
+ opts.body = JSON.stringify(args);
74
+ }
75
+
76
+ let res, text;
77
+ try {
78
+ res = await fetch(url, opts);
79
+ text = await res.text();
80
+ } catch (e) {
81
+ return { error: `bridge fetch failed: ${e.message}`, url };
82
+ }
83
+
84
+ let parsed;
85
+ try { parsed = JSON.parse(text); }
86
+ catch { parsed = { raw: text }; }
87
+
88
+ if (!res.ok) {
89
+ return {
90
+ error: parsed?.error || `HTTP ${res.status}`,
91
+ status: res.status,
92
+ ...(typeof parsed === "object" ? parsed : {}),
93
+ };
94
+ }
95
+ return parsed;
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Returns an array of tool objects in the shape super-agent-tools/index.js
101
+ * expects: { name, schema, makeHandler }.
102
+ *
103
+ * @param {object} opts
104
+ * @param {Set<string>=} opts.categories override DEFAULT_CATEGORIES
105
+ * @param {Set<string>=} opts.skipNames extra names to skip in addition to NATIVE_NAMES
106
+ */
107
+ export function buildBridgedTools(opts = {}) {
108
+ const categories = opts.categories instanceof Set ? opts.categories : DEFAULT_CATEGORIES;
109
+ const skipNames = opts.skipNames instanceof Set ? opts.skipNames : new Set();
110
+
111
+ return TOOL_DEFINITIONS
112
+ .filter(e => categories.has(e.category))
113
+ .filter(e => !NATIVE_NAMES.has(e.name) && !skipNames.has(e.name))
114
+ .filter(e => e.endpoint?.path)
115
+ .map(entry => ({
116
+ name: entry.name,
117
+ schema: buildSchema(entry),
118
+ makeHandler: buildHandler(entry),
119
+ }));
120
+ }
121
+
122
+ export { NATIVE_NAMES, DEFAULT_CATEGORIES };
@@ -1,36 +1,66 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { readConfig, addProject as addProjectInConfig } from "../../../core/config.js";
4
+ import { initApf } from "../../../core/scaffold.js";
3
5
  import { confirmedProperty, projectMeta } from "../helpers.js";
4
6
 
7
+ function isApcProject(absPath) {
8
+ return (
9
+ fs.existsSync(path.join(absPath, "AGENTS.md")) &&
10
+ fs.existsSync(path.join(absPath, ".apc", "project.json"))
11
+ );
12
+ }
13
+
5
14
  export default {
6
15
  name: "add_project",
7
16
  schema: {
8
17
  type: "function",
9
18
  function: {
10
19
  name: "add_project",
11
- description: "Register an existing APC project path with the APX daemon. The path must contain AGENTS.md and .apc/project.json.",
20
+ description:
21
+ "Register a project path with the APX daemon. If the path is not yet an APC project (missing AGENTS.md or .apc/project.json), the tool runs the init scaffold first and then registers it — one call covers both cases. Pass init=false to require the path to already be an APC project (strict mode).",
12
22
  parameters: {
13
23
  type: "object",
14
24
  properties: {
15
- path: { type: "string", description: "absolute or relative filesystem path to an APC project" },
25
+ path: { type: "string", description: "absolute or relative filesystem path to add" },
26
+ name: { type: "string", description: "optional project name (used only when initializing a new APC project)" },
27
+ init: { type: "boolean", description: "auto-create AGENTS.md and .apc/project.json if missing (default true)" },
16
28
  confirmed: confirmedProperty("true only after explicit user confirmation for this exact project registration"),
17
29
  },
18
30
  required: ["path"],
19
31
  },
20
32
  },
21
33
  },
22
- makeHandler: ({ projects, requirePermission }) => ({ path: projectPath, confirmed = false }) => {
34
+ makeHandler: ({ projects, requirePermission }) => ({ path: projectPath, name, init = true, confirmed = false }) => {
23
35
  requirePermission("add_project", { dangerous: true, confirmed });
24
36
  if (!projectPath) throw new Error("add_project: path required");
25
37
 
38
+ const abs = path.resolve(projectPath);
39
+ if (!fs.existsSync(abs)) {
40
+ throw new Error(`add_project: path does not exist: ${abs}`);
41
+ }
42
+
43
+ let initialized = false;
44
+ if (!isApcProject(abs)) {
45
+ if (!init) {
46
+ throw new Error(
47
+ `not an APC project: ${abs} (no AGENTS.md / .apc/project.json). ` +
48
+ `Pass init=true to scaffold it before registering.`
49
+ );
50
+ }
51
+ initApf(abs, { name });
52
+ initialized = true;
53
+ }
54
+
26
55
  const cfg = readConfig();
27
- const result = addProjectInConfig(cfg, projectPath);
56
+ const result = addProjectInConfig(cfg, abs);
28
57
  const p = projects.register(result.project.path);
29
58
  return {
30
59
  ok: true,
31
60
  added: result.added,
61
+ initialized,
32
62
  project: projectMeta(projects, p),
33
- normalized_path: path.resolve(projectPath),
63
+ normalized_path: abs,
34
64
  };
35
65
  },
36
66
  };
@@ -0,0 +1,32 @@
1
+ import { listSkills, SKILL_LOCATIONS } from "../../skills-loader.js";
2
+
3
+ export default {
4
+ name: "list_skills",
5
+ schema: {
6
+ type: "function",
7
+ function: {
8
+ name: "list_skills",
9
+ description:
10
+ "List available skills (documentation modules) the super-agent can load on demand. Returns slug + 1-line description for each — NO body content (cheap). Call load_skill(slug) to actually fetch the doc when needed. Scans built-in skills shipped with apx, user-installed globals in ~/.apx/skills/, and project-scoped skills in <project>/.apc/skills/.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ project_path: {
15
+ type: "string",
16
+ description: "optional project root to also scan for project-scoped skills (use the CWD when the user is working in a project)",
17
+ },
18
+ },
19
+ },
20
+ },
21
+ },
22
+ makeHandler: () => ({ project_path } = {}) => {
23
+ const skills = listSkills({ projectPath: project_path });
24
+ return {
25
+ ok: true,
26
+ count: skills.length,
27
+ locations: SKILL_LOCATIONS,
28
+ project_path: project_path || null,
29
+ skills: skills.map(({ slug, source, description }) => ({ slug, source, description })),
30
+ };
31
+ },
32
+ };
@@ -0,0 +1,31 @@
1
+ import { loadSkill } from "../../skills-loader.js";
2
+
3
+ export default {
4
+ name: "load_skill",
5
+ schema: {
6
+ type: "function",
7
+ function: {
8
+ name: "load_skill",
9
+ description:
10
+ "Load the full body of a named skill (markdown documentation, frontmatter stripped). Use after list_skills found a relevant slug. Resolves the slug via priority: project > global (~/.apx/skills/) > built-in. The body is loaded into the conversation only on the turn you call this — keeps baseline tokens at zero.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ slug: {
15
+ type: "string",
16
+ description: "skill slug as listed by list_skills (e.g. \"apx\", \"apc-context\")",
17
+ },
18
+ project_path: {
19
+ type: "string",
20
+ description: "optional project root for resolving project-scoped skills",
21
+ },
22
+ },
23
+ required: ["slug"],
24
+ },
25
+ },
26
+ },
27
+ makeHandler: () => ({ slug, project_path } = {}) => {
28
+ if (!slug) throw new Error("load_skill: slug required");
29
+ return loadSkill(slug, { projectPath: project_path });
30
+ },
31
+ };
@@ -13,6 +13,7 @@
13
13
  // }
14
14
  import { callEngine } from "./engines/index.js";
15
15
  import { TOOL_SCHEMAS, makeToolHandlers } from "./super-agent-tools.js";
16
+ import { listSkills } from "./skills-loader.js";
16
17
  import {
17
18
  extractPseudoToolCalls,
18
19
  cleanTextOfPseudoToolCalls,
@@ -61,7 +62,10 @@ HARD RULES (do not deviate):
61
62
  15. NO-PENDING RULE: never say "give me a second", "I will do it", or "I will try later" as a final answer. Either call the tool in this same turn or say what blocks you.
62
63
  16. IDENTITY RULE: when the user asks you to change your name, call yourself something, or update your personality/language, call set_identity and persist the change. Then confirm with your new name.
63
64
  17. ROUTINES RULE: NEVER create a routine in the default project (id=0). Routines MUST be tied to a specific registered project. Before adding a routine, call list_projects to find the correct project id or name. Then pass --project <id|name> to apx routine add. If no project fits, ask the user which project to use. Creating routines in project 0/default mixes unrelated projects' schedules and corrupts state.
64
- 18. **NO EMPTY RESPONSES**: Never respond with only text when you have tools available and the user is asking you to DO something. Call the tool FIRST, then explain. Never say "I'll do X" without immediately calling the tool. Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking", "stand by") without a tool call are invalid responses — they will be re-prompted and waste a turn.`;
65
+ 18. **NO EMPTY RESPONSES**: Never respond with only text when you have tools available and the user is asking you to DO something. Call the tool FIRST, then explain. Never say "I'll do X" without immediately calling the tool. Empty acknowledgments ("ok", "entendido", "dame un minuto", "voy", "checking", "stand by") without a tool call are invalid responses — they will be re-prompted and waste a turn.
66
+ 19. **CWD RULE**: When the channel context includes a "CWD: <path>" line, that is the user's current working directory. References to "este directorio", "este proyecto", "esta carpeta", "acá", "aquí", "this directory", "this project", "current dir/folder" all mean that exact CWD path. Use it as the path argument directly — DO NOT ask the user "what's the path?" when CWD is already given. Example: if user says "agregá este proyecto a la lista", call add_project({path: <CWD>}) immediately.
67
+ 20. **NO MANUAL SCAFFOLDING**: To register or scaffold a project, ALWAYS use add_project — it auto-creates AGENTS.md and .apc/project.json when missing (one call, atomic). NEVER write AGENTS.md, .apc/project.json, or any APC scaffold file by hand via run_shell / write_file / shell pipes. The schema must come from the official initApf scaffold, not improvised. If add_project errors, report the error to the user — don't try to work around it with shell hacks. Same for any other APC-managed file (.apc/agents/*, .apc/skills/*, etc.) — use the dedicated tool, never raw filesystem writes.
68
+ 21. **SKILLS — ON DEMAND**: The "# Available skills" section below lists every skill available to you (slug + description, NO body). When the user asks about specific APX/APC commands, project structure, agent runtimes, or anything where exact syntax or detailed behavior matches a skill description (in ANY language — match semantically, not by keyword), call load_skill({slug}) to fetch the full markdown body. If a CWD is in the contextNote, pass it as project_path so project-scoped skills resolve. If the user explicitly asks "what skills do you have?", you can either read the catalog below directly OR call list_skills to get a fresh enumeration. Do NOT load skills for trivial / unrelated questions — that wastes tokens. Don't guess CLI syntax when a skill can tell you; load it.`;
65
69
 
66
70
  function isShortConfirmation(text) {
67
71
  return /^(yes|y|si|si dale|dale|ok|okay|confirm|confirmed|go|proceed|do it)\b/i
@@ -135,12 +139,32 @@ export async function runSuperAgent({
135
139
  "When a tool schema has confirmed, set confirmed=true only after explicit user confirmation for that exact action.",
136
140
  ].join("\n");
137
141
 
142
+ // Build a lightweight catalog of available skills (slug + 1-line description).
143
+ // Skill BODIES are NOT included — only the catalog. The model decides which
144
+ // (if any) to load on demand via load_skill(slug). Cross-lingual matching is
145
+ // handled by the LLM itself (no router needed). Empty if no skills found.
146
+ const skillsCatalog = (() => {
147
+ let list = [];
148
+ try { list = listSkills(); } catch { /* loader failure → empty catalog */ }
149
+ if (!list.length) return "";
150
+ return [
151
+ "# Available skills (load on demand)",
152
+ "Below is the catalog of skills (slug + description). Bodies are NOT loaded yet.",
153
+ "If the user asks how something works, requests syntax/docs, or otherwise needs",
154
+ "knowledge that matches a skill description (in any language — match semantically),",
155
+ "call load_skill({slug}) to load the full markdown into your context.",
156
+ "",
157
+ ...list.map(s => `- **${s.slug}** [${s.source}]: ${s.description || "(no description)"}`),
158
+ ].join("\n");
159
+ })();
160
+
138
161
  const system = [
139
162
  sa.system || DEFAULT_SYSTEM,
140
163
  permissionNote,
141
164
  contextNote,
142
165
  "# Registered projects (just the index — call tools for details)",
143
166
  projectIndex || "(no projects registered)",
167
+ skillsCatalog,
144
168
  ]
145
169
  .filter(Boolean)
146
170
  .join("\n\n");