@agentprojectcontext/apx 1.33.1 → 1.35.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 (208) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +49 -61
  3. package/src/core/agent/a2a/reply.js +48 -0
  4. package/src/core/agent/build-agent-system.js +136 -59
  5. package/src/core/agent/channels/voice-context.js +98 -0
  6. package/src/core/agent/memory.js +2 -1
  7. package/src/core/agent/prompt-builder.js +178 -124
  8. package/src/core/agent/prompts/channels/code.md +12 -10
  9. package/src/core/agent/prompts/channels/desktop.md +5 -32
  10. package/src/core/agent/prompts/channels/telegram.md +4 -15
  11. package/src/core/agent/prompts/channels/web_code.md +11 -11
  12. package/src/core/agent/prompts/core/agent-base.md +24 -0
  13. package/src/core/agent/prompts/core/project-agent.md +11 -0
  14. package/src/core/agent/prompts/core/super-agent.md +21 -0
  15. package/src/core/agent/prompts/discipline/action.md +10 -0
  16. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  17. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  18. package/src/core/agent/prompts/modes/code-build.md +1 -0
  19. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  20. package/src/core/agent/prompts/modes/index.js +28 -0
  21. package/src/core/agent/self-memory.js +43 -1
  22. package/src/core/agent/skills/index-store.js +307 -0
  23. package/src/core/agent/skills/index.js +15 -1
  24. package/src/core/agent/skills/inspector.js +317 -0
  25. package/src/core/agent/skills/loader.js +22 -18
  26. package/src/core/agent/stream/turn-accumulator.js +73 -0
  27. package/src/core/agent/suggestions.js +37 -0
  28. package/src/core/agent/super-agent.js +7 -1
  29. package/src/core/agent/tools/handlers/_git.js +50 -0
  30. package/src/core/agent/tools/handlers/add-project.js +5 -2
  31. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  32. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  33. package/src/core/agent/tools/handlers/git-log.js +38 -0
  34. package/src/core/agent/tools/handlers/git-show.js +34 -0
  35. package/src/core/agent/tools/handlers/git-status.js +61 -0
  36. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  37. package/src/core/agent/tools/helpers.js +2 -2
  38. package/src/core/agent/tools/names.js +169 -0
  39. package/src/core/agent/tools/registry-bridge.js +6 -14
  40. package/src/core/agent/tools/registry.js +103 -69
  41. package/src/core/apc/context-copy.js +27 -0
  42. package/src/core/apc/notes.js +19 -0
  43. package/src/core/apc/parser.js +12 -5
  44. package/src/core/apc/paths.js +87 -0
  45. package/src/core/apc/scaffold.js +82 -76
  46. package/src/core/apc/skill-sync.js +10 -0
  47. package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
  48. package/src/core/config/index.js +24 -2
  49. package/src/core/config/redact.js +95 -0
  50. package/src/core/constants/channels.js +2 -0
  51. package/src/core/constants/code-modes.js +10 -0
  52. package/src/core/constants/index.js +1 -0
  53. package/src/core/deck/manifest.js +186 -0
  54. package/src/core/engines/catalog.js +83 -0
  55. package/src/core/{tools → http-tools}/browser.js +0 -1
  56. package/src/core/{tools → http-tools}/fetch.js +0 -1
  57. package/src/core/{tools → http-tools}/glob.js +0 -1
  58. package/src/core/{tools → http-tools}/grep.js +0 -1
  59. package/src/core/{tools → http-tools}/registry.js +0 -1
  60. package/src/core/{tools → http-tools}/search.js +0 -1
  61. package/src/core/i18n/en.js +9 -0
  62. package/src/core/i18n/es.js +12 -0
  63. package/src/core/i18n/index.js +54 -0
  64. package/src/core/i18n/pt.js +9 -0
  65. package/src/core/identity/telegram.js +2 -1
  66. package/src/core/mcp/runner.js +272 -14
  67. package/src/core/mcp/sources.js +3 -2
  68. package/src/core/routines/index.js +16 -0
  69. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  70. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  71. package/src/core/runtime-skills/apx/SKILL.md +83 -0
  72. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +125 -0
  73. package/src/core/runtime-skills/apx-agent/SKILL.md +97 -0
  74. package/src/core/runtime-skills/apx-mcp/SKILL.md +111 -0
  75. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +169 -0
  76. package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +20 -29
  77. package/src/core/runtime-skills/apx-routine/SKILL.md +127 -0
  78. package/src/core/runtime-skills/apx-runtime/SKILL.md +99 -0
  79. package/src/core/runtime-skills/apx-sessions/SKILL.md +232 -0
  80. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +129 -0
  81. package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +18 -21
  82. package/src/core/runtime-skills/apx-telegram/SKILL.md +120 -0
  83. package/src/core/runtime-skills/apx-voice/SKILL.md +117 -0
  84. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  85. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  86. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  87. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  88. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  89. package/src/core/stores/code-sessions.js +50 -2
  90. package/src/core/stores/routine-memory.js +1 -1
  91. package/src/core/stores/sessions-search.js +121 -0
  92. package/src/core/stores/sessions.js +38 -0
  93. package/src/core/vars/index.js +14 -0
  94. package/src/core/vars/interpolate.js +86 -0
  95. package/src/core/vars/sources.js +151 -0
  96. package/src/core/voice/audio-decode.js +38 -0
  97. package/src/core/voice/transcription.js +225 -0
  98. package/src/host/daemon/api/admin-config.js +5 -82
  99. package/src/host/daemon/api/agents.js +5 -5
  100. package/src/host/daemon/api/code.js +17 -169
  101. package/src/host/daemon/api/config.js +3 -4
  102. package/src/host/daemon/api/conversations.js +8 -29
  103. package/src/host/daemon/api/deck.js +37 -404
  104. package/src/host/daemon/api/engines.js +1 -80
  105. package/src/host/daemon/api/exec.js +1 -1
  106. package/src/host/daemon/api/mcps.js +32 -0
  107. package/src/host/daemon/api/routines.js +1 -1
  108. package/src/host/daemon/api/runtimes.js +4 -3
  109. package/src/host/daemon/api/sessions-search.js +24 -140
  110. package/src/host/daemon/api/sessions.js +12 -30
  111. package/src/host/daemon/api/shared.js +2 -1
  112. package/src/host/daemon/api/skills.js +140 -6
  113. package/src/host/daemon/api/super-agent.js +56 -1
  114. package/src/host/daemon/api/telegram.js +1 -11
  115. package/src/host/daemon/api/tools.js +6 -6
  116. package/src/host/daemon/api/transcribe.js +2 -2
  117. package/src/host/daemon/api/vars.js +137 -0
  118. package/src/host/daemon/api/voice.js +13 -290
  119. package/src/host/daemon/api.js +2 -0
  120. package/src/host/daemon/db.js +6 -6
  121. package/src/host/daemon/deck-exec.js +148 -0
  122. package/src/host/daemon/index.js +20 -3
  123. package/src/host/daemon/plugins/telegram/index.js +9 -9
  124. package/src/host/daemon/routines-scheduler.js +64 -0
  125. package/src/host/daemon/smoke.js +3 -2
  126. package/src/host/daemon/whisper-server.js +225 -0
  127. package/src/interfaces/cli/branding.js +53 -0
  128. package/src/interfaces/cli/commands/agent.js +3 -2
  129. package/src/interfaces/cli/commands/command.js +2 -3
  130. package/src/interfaces/cli/commands/messages.js +6 -2
  131. package/src/interfaces/cli/commands/pair.js +5 -4
  132. package/src/interfaces/cli/commands/search.js +1 -1
  133. package/src/interfaces/cli/commands/sessions.js +3 -2
  134. package/src/interfaces/cli/commands/skills.js +290 -55
  135. package/src/interfaces/cli/index.js +84 -2
  136. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
  137. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
  138. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +1 -0
  139. package/src/interfaces/web/dist/index.html +2 -2
  140. package/src/interfaces/web/package-lock.json +182 -182
  141. package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
  142. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  143. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  144. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +37 -4
  145. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  146. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  147. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  148. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  149. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  150. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  151. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  152. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  153. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  154. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  155. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  156. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  157. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  158. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +73 -4
  159. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  160. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  161. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  162. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  163. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  164. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  165. package/src/interfaces/web/src/constants/index.ts +1 -1
  166. package/src/interfaces/web/src/hooks/useChat.ts +19 -0
  167. package/src/interfaces/web/src/i18n/en.ts +175 -7
  168. package/src/interfaces/web/src/i18n/es.ts +180 -15
  169. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  170. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  171. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  172. package/src/interfaces/web/src/lib/api.ts +1 -0
  173. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  174. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  175. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  176. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  177. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  178. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  179. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  180. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  181. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  182. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  183. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  184. package/src/interfaces/web/src/types/daemon.ts +15 -0
  185. package/skills/apx-agency-agents/SKILL.md +0 -141
  186. package/skills/apx-agent/SKILL.md +0 -100
  187. package/skills/apx-mcp-builder/SKILL.md +0 -183
  188. package/skills/apx-routine/SKILL.md +0 -140
  189. package/skills/apx-runtime/SKILL.md +0 -117
  190. package/skills/apx-sessions/SKILL.md +0 -281
  191. package/skills/apx-skill-builder/SKILL.md +0 -153
  192. package/skills/apx-telegram/SKILL.md +0 -131
  193. package/skills/apx-voice/SKILL.md +0 -137
  194. package/src/core/agent/prompts/action-discipline.md +0 -24
  195. package/src/core/agent/prompts/super-agent-base.md +0 -42
  196. package/src/host/daemon/transcription.js +0 -538
  197. package/src/host/daemon/whisper-transcribe.py +0 -73
  198. package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
  199. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
  200. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
  201. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  202. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  203. /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
  204. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  205. /package/src/core/{tools → http-tools}/index.js +0 -0
  206. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  207. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  208. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -15,7 +15,7 @@ import {
15
15
  deleteRoutine,
16
16
  setEnabled as setRoutineEnabled,
17
17
  runRoutineNow,
18
- } from "../routines.js";
18
+ } from "#core/routines/index.js";
19
19
 
20
20
  export function register(app, { projects, registries, plugins, project, config }) {
21
21
  app.get("/projects/:pid/routines", (req, res) => {
@@ -8,11 +8,12 @@
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { readAgents } from "#core/apc/parser.js";
11
+ import { apcProjectFile, apcAgentsDir } from "#core/apc/paths.js";
11
12
  import { readSessionFrontmatter } from "#core/stores/sessions.js";
12
13
  import { buildAgentSystem } from "#core/agent/build-agent-system.js";
13
14
  import { CHANNELS } from "#core/constants/channels.js";
14
15
  import { getRuntime, RUNTIME_IDS } from "../runtimes/index.js";
15
- import { detectAll } from "../env-detect.js";
16
+ import { detectAll } from "#core/runtimes/detect.js";
16
17
  import { buildRuntimeBridgeHint as buildApfHint } from "#core/agent/runtime-bridge.js";
17
18
  import {
18
19
  createRuntimeSession,
@@ -52,7 +53,7 @@ export function register(app, { projects, registries, plugins, project, config }
52
53
  let projectName = path.basename(p.path);
53
54
  try {
54
55
  const meta = JSON.parse(
55
- fs.readFileSync(path.join(p.path, ".apc", "project.json"), "utf8")
56
+ fs.readFileSync(apcProjectFile(p.path), "utf8")
56
57
  );
57
58
  if (meta.name) projectName = meta.name;
58
59
  } catch {}
@@ -152,7 +153,7 @@ export function register(app, { projects, registries, plugins, project, config }
152
153
 
153
154
  const sessionRoots = [
154
155
  path.join(p.storagePath || p.path, "agents"),
155
- path.join(p.path, ".apc", "agents"),
156
+ apcAgentsDir(p.path),
156
157
  ];
157
158
  let sessionFile = null;
158
159
  let agentSlug = null;
@@ -2,169 +2,53 @@
2
2
  // GET /sessions/search?q=…&project=…&limit=20
3
3
  // POST /sessions/:id/compact resolves which project/agent owns the file
4
4
  // then delegates to compactConversation.
5
- import fs from "node:fs";
6
5
  import path from "node:path";
7
6
  import { readAgents } from "#core/apc/parser.js";
8
- import { compactConversation } from "../compact.js";
7
+ import { compactConversation } from "#core/stores/conversations-compactor.js";
8
+ import { searchSessions, findSessionFile } from "#core/stores/sessions-search.js";
9
+
10
+ function resolveProjects(projects, projectRef) {
11
+ const all = projects.list();
12
+ if (projectRef != null) {
13
+ const ref = String(projectRef);
14
+ const found = all.find((p) => String(p.id) === ref || p.path === path.resolve(ref));
15
+ return found ? [projects.get(found.id)] : [];
16
+ }
17
+ return all.map((p) => projects.get(p.id)).filter(Boolean);
18
+ }
9
19
 
10
20
  export function register(app, { projects, config }) {
11
21
  app.get("/sessions/search", (req, res) => {
12
22
  const { q, project: projectRef, limit = "20" } = req.query;
13
23
  if (!q) return res.status(400).json({ error: "q required" });
14
24
  const lim = Math.min(parseInt(limit, 10) || 20, 200);
15
- const needle = q.toLowerCase();
16
-
17
- const allProjects = projects.list();
18
- const targetProjects = (() => {
19
- if (projectRef != null) {
20
- const ref = String(projectRef);
21
- const found = allProjects.find(
22
- (p) => String(p.id) === ref || p.path === path.resolve(ref)
23
- );
24
- return found ? [projects.get(found.id)] : [];
25
- }
26
- return allProjects.map((p) => projects.get(p.id)).filter(Boolean);
27
- })();
28
-
29
- const matches = [];
30
-
31
- for (const p of targetProjects) {
32
- if (!p) continue;
33
-
34
- // 1) Legacy session files in the repo (.apc/agents/<slug>/sessions/)
35
- const sessionAgentsDir = path.join(p.path, ".apc", "agents");
36
- if (fs.existsSync(sessionAgentsDir)) {
37
- for (const slug of fs.readdirSync(sessionAgentsDir)) {
38
- const sessionsDir = path.join(sessionAgentsDir, slug, "sessions");
39
- if (!fs.existsSync(sessionsDir)) continue;
40
- for (const f of fs
41
- .readdirSync(sessionsDir)
42
- .filter((x) => x.endsWith(".md"))) {
43
- const filePath = path.join(sessionsDir, f);
44
- try {
45
- const text = fs.readFileSync(filePath, "utf8");
46
- if (text.toLowerCase().includes(needle)) {
47
- const lines = text.split("\n");
48
- const matchLine = lines.findIndex((l) =>
49
- l.toLowerCase().includes(needle)
50
- );
51
- const excerpt = lines
52
- .slice(Math.max(0, matchLine - 1), matchLine + 3)
53
- .join("\n");
54
- matches.push({
55
- type: "session",
56
- project: p.id,
57
- agent: slug,
58
- filename: f,
59
- path: filePath,
60
- excerpt: excerpt.slice(0, 300),
61
- });
62
- if (matches.length >= lim) break;
63
- }
64
- } catch {}
65
- }
66
- if (matches.length >= lim) break;
67
- }
68
- }
69
-
70
- if (matches.length >= lim) break;
71
-
72
- // 2) Conversation files in daemon storage (~/.apx/…/conversations/)
73
- const convAgentsDir = path.join(p.storagePath, "agents");
74
- if (fs.existsSync(convAgentsDir)) {
75
- for (const slug of fs.readdirSync(convAgentsDir)) {
76
- const convDir = path.join(convAgentsDir, slug, "conversations");
77
- if (!fs.existsSync(convDir)) continue;
78
- for (const f of fs
79
- .readdirSync(convDir)
80
- .filter((x) => x.endsWith(".md"))) {
81
- const filePath = path.join(convDir, f);
82
- try {
83
- const text = fs.readFileSync(filePath, "utf8");
84
- if (text.toLowerCase().includes(needle)) {
85
- const lines = text.split("\n");
86
- const matchLine = lines.findIndex((l) =>
87
- l.toLowerCase().includes(needle)
88
- );
89
- const excerpt = lines
90
- .slice(Math.max(0, matchLine - 1), matchLine + 3)
91
- .join("\n");
92
- matches.push({
93
- type: "conversation",
94
- project: p.id,
95
- agent: slug,
96
- filename: f,
97
- path: filePath,
98
- excerpt: excerpt.slice(0, 300),
99
- });
100
- if (matches.length >= lim) break;
101
- }
102
- } catch {}
103
- }
104
- if (matches.length >= lim) break;
105
- }
106
- }
107
-
108
- if (matches.length >= lim) break;
109
- }
110
-
111
- res.json({ q, count: matches.length, results: matches });
25
+ const targets = resolveProjects(projects, projectRef);
26
+ const results = searchSessions(targets, q, lim);
27
+ res.json({ q, count: results.length, results });
112
28
  });
113
29
 
114
30
  app.post("/sessions/:id/compact", async (req, res) => {
115
31
  const { id } = req.params;
116
32
  const { model: modelOverride, project: projectRef } = req.body || {};
117
-
118
- const candidates =
119
- projectRef != null
120
- ? (() => {
121
- const ref = String(projectRef);
122
- const found = projects
123
- .list()
124
- .find(
125
- (p) => String(p.id) === ref || p.path === path.resolve(ref)
126
- );
127
- return found ? [projects.get(found.id)] : [];
128
- })()
129
- : projects.list().map((p) => projects.get(p.id)).filter(Boolean);
130
-
131
- let found = null;
132
- const filename = id.endsWith(".md") ? id : `${id}.md`;
133
-
134
- for (const p of candidates) {
135
- if (!p) continue;
136
- const agentsDir = path.join(p.storagePath, "agents");
137
- if (fs.existsSync(agentsDir)) {
138
- for (const slug of fs.readdirSync(agentsDir)) {
139
- const f = path.join(agentsDir, slug, "conversations", filename);
140
- if (fs.existsSync(f)) {
141
- found = { p, slug };
142
- break;
143
- }
144
- }
145
- }
146
- if (found) break;
147
- }
33
+ const candidates = resolveProjects(projects, projectRef);
34
+ const found = findSessionFile(candidates, id);
148
35
 
149
36
  if (!found) {
150
- return res
151
- .status(404)
152
- .json({ error: `session/conversation "${id}" not found` });
37
+ return res.status(404).json({ error: `session/conversation "${id}" not found` });
153
38
  }
154
39
 
155
- const { p, slug } = found;
40
+ const { project: p, agentSlug, filename } = found;
156
41
  const agents = readAgents(p.path);
157
- const agent = agents.find((a) => a.slug === slug);
42
+ const agent = agents.find((a) => a.slug === agentSlug);
158
43
  const modelId = modelOverride || agent?.fields?.Model;
159
- if (!modelId)
160
- return res
161
- .status(400)
162
- .json({ error: "agent has no model; pass model in body" });
44
+ if (!modelId) {
45
+ return res.status(400).json({ error: "agent has no model; pass model in body" });
46
+ }
163
47
 
164
48
  try {
165
49
  const result = await compactConversation({
166
50
  storagePath: p.storagePath,
167
- agentSlug: slug,
51
+ agentSlug,
168
52
  filename,
169
53
  modelId,
170
54
  config: p.config || config,
@@ -4,9 +4,15 @@
4
4
  // GET /projects/:pid/sessions/:sid by filename (cross-agent lookup)
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
- import { parseSessionFrontmatter, readAgents } from "#core/apc/parser.js";
7
+ import { readAgents } from "#core/apc/parser.js";
8
+ import {
9
+ parseSessionFrontmatter,
10
+ } from "#core/apc/parser.js";
11
+ import {
12
+ agentSessionsDir,
13
+ createAgentSessionFile,
14
+ } from "#core/stores/sessions.js";
8
15
  import { collectAllSessions } from "#interfaces/cli/commands/sessions.js";
9
- import { nowIso } from "./shared.js";
10
16
 
11
17
  export function register(app, { projects, project }) {
12
18
  // Cross-engine sessions (apx · claude · codex), newest first.
@@ -29,12 +35,7 @@ export function register(app, { projects, project }) {
29
35
  const agents = readAgents(p.path);
30
36
  if (!agents.find((a) => a.slug === req.params.slug))
31
37
  return res.status(404).json({ error: "agent not found" });
32
- const sessionsDir = path.join(
33
- p.storagePath,
34
- "agents",
35
- req.params.slug,
36
- "sessions"
37
- );
38
+ const sessionsDir = agentSessionsDir(p.storagePath, req.params.slug);
38
39
  if (!fs.existsSync(sessionsDir)) return res.json([]);
39
40
  const sessions = fs
40
41
  .readdirSync(sessionsDir)
@@ -62,32 +63,13 @@ export function register(app, { projects, project }) {
62
63
  if (!p) return;
63
64
  const { title, body = "" } = req.body || {};
64
65
  if (!title) return res.status(400).json({ error: "title required" });
65
- const sessionsDir = path.join(
66
+ const { filename, path: filePath } = createAgentSessionFile(
66
67
  p.storagePath,
67
- "agents",
68
68
  req.params.slug,
69
- "sessions"
69
+ { title, body }
70
70
  );
71
- fs.mkdirSync(sessionsDir, { recursive: true });
72
- const titleSlug =
73
- title
74
- .toLowerCase()
75
- .replace(/[^a-z0-9]+/g, "-")
76
- .replace(/^-|-$/g, "") || "session";
77
- const today = new Date().toISOString().slice(0, 10);
78
- let candidate = path.join(sessionsDir, `${today}-${titleSlug}.md`);
79
- let n = 2;
80
- while (fs.existsSync(candidate)) {
81
- candidate = path.join(sessionsDir, `${today}-${titleSlug}-${n}.md`);
82
- n++;
83
- }
84
- const started = nowIso();
85
- const content = `---\ntitle: ${title}\nstarted: ${started}\n---\n\n# ${title}\n\n${body}\n`;
86
- fs.writeFileSync(candidate, content);
87
71
  projects.rebuild(p.id);
88
- res
89
- .status(201)
90
- .json({ filename: path.basename(candidate), path: candidate });
72
+ res.status(201).json({ filename, path: filePath });
91
73
  });
92
74
 
93
75
  // GET session by filename (sid may include or omit the .md extension)
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  import { appendErrorTrace, previewText } from "#core/logging.js";
9
9
  import { readAgents } from "#core/apc/parser.js";
10
10
  import { agentMemoryPath } from "#core/agent/memory.js";
11
+ import { apcMemoryFile } from "#core/apc/paths.js";
11
12
  import { CHANNELS } from "#core/constants/channels.js";
12
13
 
13
14
  export const nowIso = () =>
@@ -117,7 +118,7 @@ export function makeTopProjectResolver(projects) {
117
118
  export function resolveMemoryPath(p) {
118
119
  const firstAgent = readAgents(p.path)[0];
119
120
  if (firstAgent) return agentMemoryPath(p, firstAgent.slug);
120
- return path.join(p.path, ".apc", "memory.md");
121
+ return apcMemoryFile(p.path);
121
122
  }
122
123
 
123
124
  // Channel context passed to the super-agent loop. `api` is the default when
@@ -1,12 +1,42 @@
1
- // Lightweight `/skills` listing for UI surfaces (web composer picker,
2
- // future palettes). Same data backing `list_skills` tool, but here without
3
- // auth-binding to a project — anyone with a valid daemon token can ask
4
- // "which skills are around right now?".
1
+ // `/skills` listing + Skill Inspector control surface for UI clients.
5
2
  //
6
- // Returns the catalog already condensed (slug + first-sentence description)
7
- // so the picker doesn't have to repeat the cleanup work.
3
+ // GET /skills catalog (slug + condensed description)
4
+ // GET /skills/inspector inspector config + index status
5
+ // PUT /skills/inspector toggle / tune inspector config
6
+ // POST /skills/index (re)build the inspector vector index
7
+ // POST /skills/inspect dry-run the inspector for a prompt
8
+ //
9
+ // The listing is the same data backing `list_skills` (no auth-binding to a
10
+ // project). The inspector routes mirror /embeddings/* so the web admin can
11
+ // configure the skill RAG exactly like it configures the memory RAG.
8
12
  import { listSkills } from "#core/agent/skills/loader.js";
9
13
  import { condenseSkillDescription } from "#core/agent/skills/catalog.js";
14
+ import {
15
+ inspectPromptForSkills,
16
+ INSPECTOR_DEFAULTS,
17
+ } from "#core/agent/skills/inspector.js";
18
+ import {
19
+ ensureIndex,
20
+ planIndex,
21
+ readIndex,
22
+ } from "#core/agent/skills/index-store.js";
23
+ import { readConfig, writeConfig } from "#core/config/index.js";
24
+
25
+ const KNOWN_KEYS = Object.keys(INSPECTOR_DEFAULTS);
26
+
27
+ function mergedInspectorConfig(cfg) {
28
+ return { ...INSPECTOR_DEFAULTS, ...(cfg?.skills?.inspector || {}) };
29
+ }
30
+
31
+ function indexStatus() {
32
+ const idx = readIndex();
33
+ return {
34
+ count: Object.keys(idx.items || {}).length,
35
+ embedder: idx.embedder || null,
36
+ dim: idx.dim || null,
37
+ updated_at: idx.updated_at || null,
38
+ };
39
+ }
10
40
 
11
41
  export function register(app /*, ctx */) {
12
42
  app.get("/skills", (req, res) => {
@@ -27,4 +57,108 @@ export function register(app /*, ctx */) {
27
57
  res.status(500).json({ error: e.message });
28
58
  }
29
59
  });
60
+
61
+ // ---- Inspector config + status -----------------------------------------
62
+
63
+ app.get("/skills/inspector", (_req, res) => {
64
+ try {
65
+ const cfg = readConfig();
66
+ res.json({
67
+ config: mergedInspectorConfig(cfg),
68
+ defaults: INSPECTOR_DEFAULTS,
69
+ keys: KNOWN_KEYS,
70
+ index: indexStatus(),
71
+ });
72
+ } catch (e) {
73
+ res.status(500).json({ error: e.message });
74
+ }
75
+ });
76
+
77
+ app.put("/skills/inspector", (req, res) => {
78
+ try {
79
+ const patch = req.body || {};
80
+ const cfg = readConfig();
81
+ cfg.skills = cfg.skills || {};
82
+ const current = mergedInspectorConfig(cfg);
83
+ const next = { ...current };
84
+
85
+ for (const [k, v] of Object.entries(patch)) {
86
+ if (!KNOWN_KEYS.includes(k)) continue;
87
+ const def = INSPECTOR_DEFAULTS[k];
88
+ if (typeof def === "boolean") next[k] = !!v;
89
+ else if (typeof def === "number") {
90
+ const n = Number(v);
91
+ if (Number.isFinite(n)) next[k] = n;
92
+ } else {
93
+ next[k] = v;
94
+ }
95
+ }
96
+
97
+ cfg.skills.inspector = next;
98
+ writeConfig(cfg);
99
+ res.json({ ok: true, config: next, index: indexStatus() });
100
+ } catch (e) {
101
+ res.status(500).json({ error: e.message });
102
+ }
103
+ });
104
+
105
+ // ---- Index build --------------------------------------------------------
106
+
107
+ app.post("/skills/index", async (req, res) => {
108
+ try {
109
+ const { project_path, force } = req.body || {};
110
+ const cfg = readConfig();
111
+ const plan = planIndex({ projectPath: project_path });
112
+ const out = await ensureIndex({
113
+ projectPath: project_path,
114
+ embedOpts: { globalConfig: cfg },
115
+ force: !!force,
116
+ });
117
+ res.json({
118
+ ok: true,
119
+ embedder: out.embedder,
120
+ dim: out.dim,
121
+ planned: {
122
+ missing: plan.missing.length,
123
+ stale: plan.stale.length,
124
+ gone: plan.gone.length,
125
+ total: plan.total,
126
+ },
127
+ changed: {
128
+ added: out.changed.added.length,
129
+ refreshed: out.changed.refreshed.length,
130
+ removed: out.changed.removed.length,
131
+ kept: out.changed.kept.length,
132
+ },
133
+ index: indexStatus(),
134
+ });
135
+ } catch (e) {
136
+ res.status(500).json({ error: e.message });
137
+ }
138
+ });
139
+
140
+ // ---- Dry-run ------------------------------------------------------------
141
+
142
+ app.post("/skills/inspect", async (req, res) => {
143
+ try {
144
+ const { prompt, project_path } = req.body || {};
145
+ if (!prompt || typeof prompt !== "string") {
146
+ return res.status(400).json({ error: "prompt required" });
147
+ }
148
+ const cfg = readConfig();
149
+ // Force enabled for the dry-run so the operator sees what it WOULD do
150
+ // even when the feature is currently off.
151
+ const probed = structuredClone(cfg);
152
+ probed.skills = probed.skills || {};
153
+ probed.skills.inspector = { ...mergedInspectorConfig(cfg), enabled: true };
154
+ const out = await inspectPromptForSkills({
155
+ prompt,
156
+ projectPath: project_path,
157
+ globalConfig: probed,
158
+ });
159
+ res.json({ trace: out.trace, contextNote: out.contextNote });
160
+ } catch (e) {
161
+ res.status(500).json({ error: e.message });
162
+ }
163
+ });
30
164
  }
@@ -15,10 +15,30 @@ import { appendGlobalMessage } from "#core/stores/messages.js";
15
15
  import { createWebConfirmAdapter } from "#core/confirmation/adapters/web.js";
16
16
  import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
17
17
  import { suggestSkillForPrompt } from "#core/agent/skills/rag.js";
18
+ import { inspectPromptForSkills, isInspectorEnabled, summarizeTrace } from "#core/agent/skills/inspector.js";
18
19
  import { CHANNELS } from "#core/constants/channels.js";
19
20
 
20
21
  const log = loggerFor("super-agent");
21
22
 
23
+ // Emit a single, readable line so `apx log -f` shows exactly what the skill
24
+ // inspector decided this turn (which skills it loaded/hinted, the embedder, and
25
+ // the top similarity). Best-effort: logging must never break a reply.
26
+ function logInspectorDecision(trace, { trace_id, channel } = {}) {
27
+ if (!trace) return;
28
+ try {
29
+ const top = trace.scored?.[0];
30
+ const topStr = top ? ` top=${top.slug}@${top.sim}` : "";
31
+ log.info(`skill inspector: ${summarizeTrace(trace)} [${trace.embedder || "?"}]${topStr}`, {
32
+ trace_id,
33
+ channel,
34
+ loaded: trace.loaded || [],
35
+ hinted: trace.hinted || [],
36
+ });
37
+ } catch {
38
+ /* logging is best-effort */
39
+ }
40
+ }
41
+
22
42
  // Persist human web turns to the cross-channel message store so they feed the
23
43
  // RAG index, search_messages, and the "active threads" awareness block. Only
24
44
  // the human surfaces (web big chat + sidebar) — not generic "api"/automation
@@ -79,10 +99,25 @@ export function register(app, { projects, registries, plugins, project, config }
79
99
  // slug is unknown.
80
100
  const slashed = tryResolveSkillCommand(rawPrompt, { projectPath: p.path });
81
101
  const prompt = slashed.handled ? slashed.prompt : rawPrompt;
102
+ const inspectorOn = isInspectorEnabled(config);
103
+ let inspectorTrace = null;
82
104
  if (slashed.handled) {
83
105
  ctx.contextNote = [ctx.contextNote, slashed.contextNote].filter(Boolean).join("\n\n");
106
+ } else if (inspectorOn) {
107
+ // Inspector middleware: per-turn semantic RAG. Replaces both the passive
108
+ // suggestSkillForPrompt nudge AND the static slug-dump in the system
109
+ // prompt — see runSuperAgent({ skipSkillsHint }).
110
+ const out = await inspectPromptForSkills({
111
+ prompt,
112
+ projectPath: p.path,
113
+ globalConfig: config,
114
+ });
115
+ inspectorTrace = out.trace;
116
+ if (out.contextNote) {
117
+ ctx.contextNote = [ctx.contextNote, out.contextNote].filter(Boolean).join("\n\n");
118
+ }
84
119
  } else {
85
- // Semantic skill nudge only when there was no explicit /slug.
120
+ // Legacy path — passive nudge, still works when inspector is off.
86
121
  const hint = await suggestSkillForPrompt(prompt, { projectPath: p.path });
87
122
  if (hint) ctx.contextNote = [ctx.contextNote, hint].filter(Boolean).join("\n\n");
88
123
  }
@@ -101,6 +136,14 @@ export function register(app, { projects, registries, plugins, project, config }
101
136
  channel: ctx.channel,
102
137
  });
103
138
 
139
+ // Surface the inspector decision to clients before model_start so the web
140
+ // debug panel / TUI can render "loaded: X" the moment the turn begins.
141
+ if (inspectorTrace) {
142
+ try { onEvent({ type: "skill_inspector", inspector: inspectorTrace }); }
143
+ catch { /* trace is best-effort */ }
144
+ logInspectorDecision(inspectorTrace, { trace_id: req.apxTraceId, channel: ctx.channel });
145
+ }
146
+
104
147
  // Web/TUI channels receive a "confirmation_required" SSE event and respond
105
148
  // via POST /super-agent/confirm/:correlationId (see api/confirm.js).
106
149
  const requestConfirmation = createWebConfirmAdapter({ onEvent });
@@ -122,6 +165,7 @@ export function register(app, { projects, registries, plugins, project, config }
122
165
  ...(completionContract ? { completionContract: true } : {}),
123
166
  onEvent,
124
167
  requestConfirmation,
168
+ skipSkillsHint: inspectorOn,
125
169
  });
126
170
  projects.rebuild(p.id);
127
171
  logWebTurn(ctx.channel, { prompt, replyText: saResult.text });
@@ -194,6 +238,16 @@ export function register(app, { projects, registries, plugins, project, config }
194
238
  req.body || {};
195
239
  if (!prompt) return res.status(400).json({ error: "prompt required" });
196
240
  const ctx = resolveSuperAgentContext(req, p);
241
+ const inspectorOn = isInspectorEnabled(config);
242
+ if (inspectorOn) {
243
+ try {
244
+ const out = await inspectPromptForSkills({ prompt, projectPath: p.path, globalConfig: config });
245
+ if (out.contextNote) {
246
+ ctx.contextNote = [ctx.contextNote, out.contextNote].filter(Boolean).join("\n\n");
247
+ }
248
+ logInspectorDecision(out.trace, { trace_id: req.apxTraceId, channel: ctx.channel });
249
+ } catch { /* inspector failure must not block the turn */ }
250
+ }
197
251
  try {
198
252
  const saResult = await runSuperAgent({
199
253
  globalConfig: config,
@@ -213,6 +267,7 @@ export function register(app, { projects, registries, plugins, project, config }
213
267
  trace_id: req.apxTraceId,
214
268
  channel: ctx.channel,
215
269
  }),
270
+ skipSkillsHint: inspectorOn,
216
271
  });
217
272
  projects.rebuild(p.id);
218
273
  logWebTurn(ctx.channel, { prompt, replyText: saResult.text });
@@ -39,17 +39,7 @@ import {
39
39
  removeRole,
40
40
  } from "#core/config/index.js";
41
41
 
42
- function redactChannel(channel) {
43
- if (!channel?.bot_token) return channel;
44
- return {
45
- ...channel,
46
- bot_token: `*** set *** (...${String(channel.bot_token).slice(-5)})`,
47
- };
48
- }
49
-
50
- function isSecretMarker(value) {
51
- return typeof value === "string" && value.startsWith("*** set ***");
52
- }
42
+ import { redactChannel, isSecretMarker } from "#core/config/redact.js";
53
43
 
54
44
  export function register(app, { telegram }) {
55
45
  app.get("/telegram/status", (_req, res) => {
@@ -4,12 +4,12 @@
4
4
  // search / glob / grep → filesystem-bounded
5
5
  // registry → /:name wildcard, MOUNT LAST so it
6
6
  // doesn't shadow the specific paths
7
- import { buildBrowserRouter } from "#core/tools/browser.js";
8
- import { buildFetchRouter } from "#core/tools/fetch.js";
9
- import { buildSearchRouter } from "#core/tools/search.js";
10
- import { buildRegistryRouter } from "#core/tools/registry.js";
11
- import { buildGlobRouter } from "#core/tools/glob.js";
12
- import { buildGrepRouter } from "#core/tools/grep.js";
7
+ import { buildBrowserRouter } from "#core/http-tools/browser.js";
8
+ import { buildFetchRouter } from "#core/http-tools/fetch.js";
9
+ import { buildSearchRouter } from "#core/http-tools/search.js";
10
+ import { buildRegistryRouter } from "#core/http-tools/registry.js";
11
+ import { buildGlobRouter } from "#core/http-tools/glob.js";
12
+ import { buildGrepRouter } from "#core/http-tools/grep.js";
13
13
 
14
14
  export function register(app, { express, projects, registries }) {
15
15
  app.use("/tools/fetch", buildFetchRouter(express));
@@ -11,7 +11,7 @@ export function register(app) {
11
11
  // the first real utterance doesn't pay the cold-load cost.
12
12
  app.get("/transcribe/warmup", async (_req, res) => {
13
13
  try {
14
- const { warmupWhisper } = await import("../transcription.js");
14
+ const { warmupWhisper } = await import("../whisper-server.js");
15
15
  res.json(await warmupWhisper());
16
16
  } catch (e) {
17
17
  res.status(500).json({ ok: false, error: e.message });
@@ -29,7 +29,7 @@ export function register(app) {
29
29
  const language = req.headers["x-language"] || "auto";
30
30
  const provider = req.headers["x-provider"];
31
31
  try {
32
- const { transcribeBuffer } = await import("../transcription.js");
32
+ const { transcribeBuffer } = await import("#core/voice/transcription.js");
33
33
  const result = await transcribeBuffer(buf, format, {
34
34
  language: language === "auto" ? undefined : language,
35
35
  beam_size: 3,