@agentprojectcontext/apx 1.77.3 → 1.78.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 (293) hide show
  1. package/README.md +23 -9
  2. package/package.json +12 -12
  3. package/src/core/agent/build-agent-system.js +3 -2
  4. package/src/core/agent/constants.js +3 -2
  5. package/src/core/agent/loop/greeting-guard.js +55 -0
  6. package/src/core/agent/loop/side-effects.js +52 -0
  7. package/src/core/agent/prompt-builder.js +49 -8
  8. package/src/core/agent/run-agent.js +13 -48
  9. package/src/core/agent/security.js +1 -1
  10. package/src/core/agent/self-memory.js +4 -2
  11. package/src/core/agent/skills/index-store.js +2 -2
  12. package/src/core/agent/skills/loader.js +3 -22
  13. package/src/core/agent/super-agent.js +0 -2
  14. package/src/core/agent/tools/handlers/call-runtime.js +4 -4
  15. package/src/core/agent/tools/handlers/search-sessions.js +1 -1
  16. package/src/core/agent/tools/handlers/set-permission-mode.js +11 -4
  17. package/src/core/agent/tools/names.js +58 -0
  18. package/src/core/apc/context-copy.js +1 -1
  19. package/src/core/apc/frontmatter.js +85 -0
  20. package/src/core/apc/parser.js +9 -21
  21. package/src/core/apc/paths.js +8 -8
  22. package/src/core/apc/scaffold.js +5 -4
  23. package/src/core/channels/telegram/config.js +201 -0
  24. package/src/core/config/index.js +25 -183
  25. package/src/core/config/paths.js +54 -1
  26. package/src/core/confirmation/adapters/web.js +1 -1
  27. package/src/core/deck/manifest.js +12 -12
  28. package/src/core/desktop/autostart.js +3 -2
  29. package/src/core/desktop/process.js +4 -4
  30. package/src/core/engines/presets.js +2 -2
  31. package/src/core/http-tools/browser.js +1 -1
  32. package/src/core/http-tools/catalog.js +551 -0
  33. package/src/core/http-tools/fetch.js +1 -1
  34. package/src/core/http-tools/glob.js +3 -3
  35. package/src/core/http-tools/grep.js +2 -9
  36. package/src/core/http-tools/inline-handlers.js +59 -0
  37. package/src/core/http-tools/registry.js +31 -615
  38. package/src/core/http-tools/search.js +2 -2
  39. package/src/core/identity/self.js +6 -7
  40. package/src/core/identity/telegram.js +3 -3
  41. package/src/core/logging.js +6 -5
  42. package/src/core/mcp/sources.js +12 -16
  43. package/src/core/memory/indexer.js +2 -5
  44. package/src/core/profiles/lifecycle.js +0 -2
  45. package/src/core/profiles/store.js +2 -7
  46. package/src/core/routines/runner.js +1 -1
  47. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
  48. package/src/core/runtime-skills/apx-profile/SKILL.md +3 -3
  49. package/src/core/runtime-skills/apx-project/SKILL.md +1 -1
  50. package/src/core/runtime-skills/apx-task/SKILL.md +8 -8
  51. package/src/core/runtime-skills/apx-telegram/SKILL.md +4 -4
  52. package/src/core/runtime-skills/apx-voice/SKILL.md +4 -4
  53. package/src/core/runtimes/detect.js +2 -2
  54. package/src/core/sessions/index.js +800 -0
  55. package/src/core/stores/code-sessions.js +3 -14
  56. package/src/core/stores/engine-sessions.js +0 -4
  57. package/src/core/stores/messages.js +0 -5
  58. package/src/core/stores/organization.js +9 -11
  59. package/src/core/stores/routines.js +6 -11
  60. package/src/core/stores/runtime-callbacks.js +2 -1
  61. package/src/core/util/json-file.js +128 -0
  62. package/src/core/vars/sources.js +14 -21
  63. package/src/core/voice/stt-models.js +1 -1
  64. package/src/core/voice/tts.js +5 -4
  65. package/src/host/daemon/api/admin-config.js +5 -5
  66. package/src/host/daemon/api/admin.js +8 -7
  67. package/src/host/daemon/api/agents.js +16 -16
  68. package/src/host/daemon/api/artifact-preview.js +8 -8
  69. package/src/host/daemon/api/artifacts.js +7 -7
  70. package/src/host/daemon/api/code.js +8 -8
  71. package/src/host/daemon/api/config.js +6 -6
  72. package/src/host/daemon/api/confirm.js +2 -2
  73. package/src/host/daemon/api/connections.js +2 -2
  74. package/src/host/daemon/api/conversations.js +12 -12
  75. package/src/host/daemon/api/deck.js +6 -6
  76. package/src/host/daemon/api/desktop.js +8 -8
  77. package/src/host/daemon/api/embeddings.js +4 -4
  78. package/src/host/daemon/api/engines.js +8 -7
  79. package/src/host/daemon/api/exec.js +5 -4
  80. package/src/host/daemon/api/files-project.js +6 -6
  81. package/src/host/daemon/api/health.js +2 -2
  82. package/src/host/daemon/api/identity.js +3 -3
  83. package/src/host/daemon/api/inbox.js +2 -2
  84. package/src/host/daemon/api/integrations.js +23 -19
  85. package/src/host/daemon/api/mcps.js +16 -12
  86. package/src/host/daemon/api/messages.js +5 -5
  87. package/src/host/daemon/api/organization.js +8 -8
  88. package/src/host/daemon/api/pairing.js +6 -6
  89. package/src/host/daemon/api/plugins.js +3 -3
  90. package/src/host/daemon/api/prefix.js +20 -0
  91. package/src/host/daemon/api/profiles.js +9 -9
  92. package/src/host/daemon/api/projects.js +5 -5
  93. package/src/host/daemon/api/routines.js +8 -8
  94. package/src/host/daemon/api/run.js +2 -2
  95. package/src/host/daemon/api/runtimes.js +6 -6
  96. package/src/host/daemon/api/sessions-search.js +18 -11
  97. package/src/host/daemon/api/sessions.js +6 -6
  98. package/src/host/daemon/api/shared.js +59 -13
  99. package/src/host/daemon/api/skills.js +12 -12
  100. package/src/host/daemon/api/super-agent.js +4 -4
  101. package/src/host/daemon/api/tasks.js +11 -11
  102. package/src/host/daemon/api/telegram.js +19 -20
  103. package/src/host/daemon/api/tools.js +7 -7
  104. package/src/host/daemon/api/top-level.js +7 -7
  105. package/src/host/daemon/api/transcribe.js +6 -6
  106. package/src/host/daemon/api/tts.js +3 -3
  107. package/src/host/daemon/api/vars.js +11 -8
  108. package/src/host/daemon/api/voice.js +6 -6
  109. package/src/host/daemon/api/web.js +28 -32
  110. package/src/host/daemon/api.js +77 -48
  111. package/src/host/daemon/db.js +1 -1
  112. package/src/host/daemon/desktop-ws.js +7 -3
  113. package/src/host/daemon/index.js +11 -5
  114. package/src/host/daemon/plugins/desktop/index.js +1 -1
  115. package/src/host/daemon/plugins/telegram/index.js +1 -1
  116. package/src/host/daemon/stt-venv.js +20 -81
  117. package/src/host/daemon/wakeup.js +1 -1
  118. package/src/interfaces/acp/index.js +3 -3
  119. package/src/interfaces/acp/session.js +2 -2
  120. package/src/interfaces/cli/commands/a2a.js +2 -2
  121. package/src/interfaces/cli/commands/agent.js +3 -3
  122. package/src/interfaces/cli/commands/artifact.js +13 -13
  123. package/src/interfaces/cli/commands/chat.js +3 -3
  124. package/src/interfaces/cli/commands/command.js +2 -2
  125. package/src/interfaces/cli/commands/config.js +4 -4
  126. package/src/interfaces/cli/commands/daemon.js +17 -18
  127. package/src/interfaces/cli/commands/desktop.js +7 -14
  128. package/src/interfaces/cli/commands/exec.js +2 -2
  129. package/src/interfaces/cli/commands/log.js +3 -4
  130. package/src/interfaces/cli/commands/mcp.js +9 -9
  131. package/src/interfaces/cli/commands/memory.js +2 -2
  132. package/src/interfaces/cli/commands/messages.js +5 -5
  133. package/src/interfaces/cli/commands/model.js +0 -18
  134. package/src/interfaces/cli/commands/obsidian.js +5 -5
  135. package/src/interfaces/cli/commands/org.js +5 -5
  136. package/src/interfaces/cli/commands/pair.js +6 -6
  137. package/src/interfaces/cli/commands/plugins.js +2 -2
  138. package/src/interfaces/cli/commands/profile.js +10 -10
  139. package/src/interfaces/cli/commands/project-config.js +6 -6
  140. package/src/interfaces/cli/commands/project.js +10 -10
  141. package/src/interfaces/cli/commands/routine.js +8 -9
  142. package/src/interfaces/cli/commands/runtime.js +2 -2
  143. package/src/interfaces/cli/commands/search.js +0 -1
  144. package/src/interfaces/cli/commands/session.js +12 -40
  145. package/src/interfaces/cli/commands/sessions.js +21 -798
  146. package/src/interfaces/cli/commands/setup.js +16 -6
  147. package/src/interfaces/cli/commands/skills.js +1 -2
  148. package/src/interfaces/cli/commands/status.js +3 -3
  149. package/src/interfaces/cli/commands/task.js +8 -8
  150. package/src/interfaces/cli/commands/telegram.js +21 -21
  151. package/src/interfaces/cli/commands/voice.js +6 -6
  152. package/src/interfaces/cli/help/index.js +2245 -0
  153. package/src/interfaces/cli/http.js +9 -4
  154. package/src/interfaces/cli/index.js +14 -2857
  155. package/src/interfaces/cli/routes/acp.js +13 -0
  156. package/src/interfaces/cli/routes/agent.js +27 -0
  157. package/src/interfaces/cli/routes/artifact.js +28 -0
  158. package/src/interfaces/cli/routes/chat.js +11 -0
  159. package/src/interfaces/cli/routes/code.js +11 -0
  160. package/src/interfaces/cli/routes/command.js +21 -0
  161. package/src/interfaces/cli/routes/config.js +16 -0
  162. package/src/interfaces/cli/routes/connections.js +11 -0
  163. package/src/interfaces/cli/routes/conversations.js +21 -0
  164. package/src/interfaces/cli/routes/daemon.js +19 -0
  165. package/src/interfaces/cli/routes/desktop.js +20 -0
  166. package/src/interfaces/cli/routes/env.js +13 -0
  167. package/src/interfaces/cli/routes/exec.js +11 -0
  168. package/src/interfaces/cli/routes/identity.js +11 -0
  169. package/src/interfaces/cli/routes/index.js +71 -0
  170. package/src/interfaces/cli/routes/init.js +11 -0
  171. package/src/interfaces/cli/routes/log.js +20 -0
  172. package/src/interfaces/cli/routes/mcp.js +22 -0
  173. package/src/interfaces/cli/routes/memory.js +11 -0
  174. package/src/interfaces/cli/routes/messages.js +16 -0
  175. package/src/interfaces/cli/routes/model.js +11 -0
  176. package/src/interfaces/cli/routes/obsidian.js +17 -0
  177. package/src/interfaces/cli/routes/org.js +34 -0
  178. package/src/interfaces/cli/routes/overlay.js +10 -0
  179. package/src/interfaces/cli/routes/pair.js +17 -0
  180. package/src/interfaces/cli/routes/panel.js +16 -0
  181. package/src/interfaces/cli/routes/permission.js +11 -0
  182. package/src/interfaces/cli/routes/plugins.js +21 -0
  183. package/src/interfaces/cli/routes/profile.js +27 -0
  184. package/src/interfaces/cli/routes/project.js +41 -0
  185. package/src/interfaces/cli/routes/restart.js +15 -0
  186. package/src/interfaces/cli/routes/routine.js +28 -0
  187. package/src/interfaces/cli/routes/run.js +11 -0
  188. package/src/interfaces/cli/routes/search.js +11 -0
  189. package/src/interfaces/cli/routes/send.js +11 -0
  190. package/src/interfaces/cli/routes/session.js +26 -0
  191. package/src/interfaces/cli/routes/sessions.js +15 -0
  192. package/src/interfaces/cli/routes/setup.js +18 -0
  193. package/src/interfaces/cli/routes/skills.js +20 -0
  194. package/src/interfaces/cli/routes/status.js +12 -0
  195. package/src/interfaces/cli/routes/task.js +26 -0
  196. package/src/interfaces/cli/routes/telegram.js +39 -0
  197. package/src/interfaces/cli/routes/update.js +24 -0
  198. package/src/interfaces/cli/routes/voice.js +17 -0
  199. package/src/interfaces/desktop/main.js +6 -7
  200. package/src/interfaces/desktop/renderer.js +0 -5
  201. package/src/interfaces/mcp-server/index.js +15 -14
  202. package/src/interfaces/tui/_shims/globals.d.ts +8 -0
  203. package/src/interfaces/tui/tsconfig.json +4 -1
  204. package/src/interfaces/web/README.md +6 -6
  205. package/src/interfaces/web/dist/assets/{index-BptlbKjU.js → index-CBR_-QyA.js} +52 -52
  206. package/src/interfaces/web/dist/assets/index-CBR_-QyA.js.map +1 -0
  207. package/src/interfaces/web/dist/index.html +1 -1
  208. package/src/interfaces/web/src/components/AddProjectDialog.tsx +1 -1
  209. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +1 -1
  210. package/src/interfaces/web/src/components/chat/ChatList.tsx +2 -2
  211. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +1 -1
  212. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +1 -1
  213. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +1 -1
  214. package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +1 -1
  215. package/src/interfaces/web/src/components/files/FileBrowser.tsx +2 -2
  216. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +2 -2
  217. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +1 -1
  218. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +2 -2
  219. package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +1 -1
  220. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
  221. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
  222. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +2 -2
  223. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +1 -1
  224. package/src/interfaces/web/src/hooks/useDaemonStatus.ts +1 -1
  225. package/src/interfaces/web/src/hooks/useDevices.ts +1 -1
  226. package/src/interfaces/web/src/hooks/useEngines.ts +1 -1
  227. package/src/interfaces/web/src/hooks/useGlobalConfig.ts +2 -2
  228. package/src/interfaces/web/src/hooks/useIdentity.ts +1 -1
  229. package/src/interfaces/web/src/hooks/useInbox.ts +1 -1
  230. package/src/interfaces/web/src/hooks/useProfiles.ts +3 -3
  231. package/src/interfaces/web/src/hooks/useProjects.ts +1 -1
  232. package/src/interfaces/web/src/hooks/useTelegram.ts +3 -3
  233. package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +3 -3
  234. package/src/interfaces/web/src/lib/api/admin.ts +11 -11
  235. package/src/interfaces/web/src/lib/api/agents.ts +14 -14
  236. package/src/interfaces/web/src/lib/api/artifacts.ts +11 -11
  237. package/src/interfaces/web/src/lib/api/code.ts +1 -1
  238. package/src/interfaces/web/src/lib/api/conversations.ts +8 -8
  239. package/src/interfaces/web/src/lib/api/deck.ts +3 -3
  240. package/src/interfaces/web/src/lib/api/desktop.ts +8 -8
  241. package/src/interfaces/web/src/lib/api/embeddings.ts +3 -3
  242. package/src/interfaces/web/src/lib/api/engines.ts +3 -3
  243. package/src/interfaces/web/src/lib/api/filesystem.ts +2 -2
  244. package/src/interfaces/web/src/lib/api/health.ts +1 -1
  245. package/src/interfaces/web/src/lib/api/identity.ts +2 -2
  246. package/src/interfaces/web/src/lib/api/inbox.ts +1 -1
  247. package/src/interfaces/web/src/lib/api/integrations.ts +8 -8
  248. package/src/interfaces/web/src/lib/api/mcps.ts +6 -6
  249. package/src/interfaces/web/src/lib/api/messages.ts +3 -3
  250. package/src/interfaces/web/src/lib/api/organization.ts +7 -7
  251. package/src/interfaces/web/src/lib/api/profiles.ts +8 -8
  252. package/src/interfaces/web/src/lib/api/projectFiles.ts +5 -5
  253. package/src/interfaces/web/src/lib/api/projects.ts +12 -12
  254. package/src/interfaces/web/src/lib/api/routines.ts +7 -7
  255. package/src/interfaces/web/src/lib/api/sessions.ts +2 -2
  256. package/src/interfaces/web/src/lib/api/skills.ts +11 -11
  257. package/src/interfaces/web/src/lib/api/super_agent.ts +3 -3
  258. package/src/interfaces/web/src/lib/api/tasks.ts +12 -12
  259. package/src/interfaces/web/src/lib/api/telegram.ts +14 -14
  260. package/src/interfaces/web/src/lib/api/tools.ts +1 -1
  261. package/src/interfaces/web/src/lib/api/vars.ts +4 -4
  262. package/src/interfaces/web/src/lib/api/voice.ts +6 -6
  263. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +1 -1
  264. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +1 -1
  265. package/src/interfaces/web/src/screens/base/LogsTab.tsx +3 -3
  266. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +1 -1
  267. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  268. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +1 -1
  269. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -1
  270. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  271. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -8
  272. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +2 -2
  273. package/src/interfaces/web/src/screens/project/ChatTab.tsx +3 -3
  274. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +1 -1
  275. package/src/interfaces/web/src/screens/project/McpsTab.tsx +3 -3
  276. package/src/interfaces/web/src/screens/project/Overview.tsx +6 -6
  277. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +2 -2
  278. package/src/interfaces/web/src/screens/project/StructureTab.tsx +1 -1
  279. package/src/interfaces/web/src/screens/project/TasksTab.tsx +1 -1
  280. package/src/interfaces/web/src/screens/project/VarsTab.tsx +1 -1
  281. package/src/interfaces/web/vite.config.ts +13 -18
  282. package/src/interfaces/web/dist/assets/index-BptlbKjU.js.map +0 -1
  283. package/src/skills/apc-context/SKILL.md +0 -159
  284. /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
  285. /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
  286. /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
  287. /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
  288. /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
  289. /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
  290. /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
  291. /package/src/{host/daemon → core}/runtimes/index.js +0 -0
  292. /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
  293. /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
@@ -2,23 +2,30 @@
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 path from "node:path";
6
5
  import { readAgents } from "#core/apc/parser.js";
7
6
  import { compactConversation } from "#core/stores/conversations-compactor.js";
8
7
  import { searchSessions, findSessionFile } from "#core/stores/sessions-search.js";
8
+ import { asyncRoute } from "./shared.js";
9
+ import { resolveProject } from "#core/apc/projects-helpers.js";
9
10
 
11
+ // Plural on purpose: no ref means "search every project". This is a different
12
+ // operation from core's resolveProject (which resolves exactly one and throws),
13
+ // but the *matching* must agree with it — this used to accept only an id or an
14
+ // exact path, so `?project=savia` matched nothing here while working on every
15
+ // other route.
10
16
  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)] : [];
17
+ if (projectRef == null || projectRef === "") {
18
+ return projects.list().map((p) => projects.get(p.id)).filter(Boolean);
19
+ }
20
+ try {
21
+ return [resolveProject(projects, projectRef)];
22
+ } catch {
23
+ return []; // unknown or ambiguous ref → no results, not a 500
16
24
  }
17
- return all.map((p) => projects.get(p.id)).filter(Boolean);
18
25
  }
19
26
 
20
- export function register(app, { projects, config }) {
21
- app.get("/sessions/search", (req, res) => {
27
+ export function register(api, { projects, config }) {
28
+ api.get("/sessions/search", (req, res) => {
22
29
  const { q, project: projectRef, limit = "20" } = req.query;
23
30
  if (!q) return res.status(400).json({ error: "q required" });
24
31
  const lim = Math.min(parseInt(limit, 10) || 20, 200);
@@ -27,7 +34,7 @@ export function register(app, { projects, config }) {
27
34
  res.json({ q, count: results.length, results });
28
35
  });
29
36
 
30
- app.post("/sessions/:id/compact", async (req, res) => {
37
+ api.post("/sessions/:id/compact", asyncRoute(async (req, res) => {
31
38
  const { id } = req.params;
32
39
  const { model: modelOverride, project: projectRef } = req.body || {};
33
40
  const candidates = resolveProjects(projects, projectRef);
@@ -57,5 +64,5 @@ export function register(app, { projects, config }) {
57
64
  } catch (e) {
58
65
  res.status(500).json({ error: e.message });
59
66
  }
60
- });
67
+ }));
61
68
  }
@@ -12,16 +12,16 @@ import {
12
12
  agentSessionsDir,
13
13
  createAgentSessionFile,
14
14
  } from "#core/stores/sessions.js";
15
- import { collectAllSessions, filterSessionsByQuery } from "#interfaces/cli/commands/sessions.js";
15
+ import { collectAllSessions, filterSessionsByQuery } from "#core/sessions/index.js";
16
16
  import { pageEnvelope } from "./shared.js";
17
17
 
18
- export function register(app, { projects, project }) {
18
+ export function register(api, { projects, project }) {
19
19
  // Cross-engine sessions (apx · claude · codex), newest first. Returns a
20
20
  // { meta, data } envelope (meta = pagination info, data = rows). Paginated
21
21
  // via ?limit & ?offset; with no limit, data is the full set as one page.
22
22
  // Optional ?q= filters via the same core as `apx session find` (title match,
23
23
  // + transcript content when ?deep=1) so terminal and web search are identical.
24
- app.get("/sessions", (req, res) => {
24
+ api.get("/sessions", (req, res) => {
25
25
  const engineId = req.query.engine ? String(req.query.engine) : null;
26
26
  const q = req.query.q ? String(req.query.q) : "";
27
27
  const deep = req.query.deep === "1" || req.query.deep === "true";
@@ -49,7 +49,7 @@ export function register(app, { projects, project }) {
49
49
  res.json(pageEnvelope(rows, req.query));
50
50
  });
51
51
 
52
- app.get("/projects/:pid/agents/:slug/sessions", (req, res) => {
52
+ api.get("/projects/:pid/agents/:slug/sessions", (req, res) => {
53
53
  const p = project(req, res);
54
54
  if (!p) return;
55
55
  const agents = readAgents(p.path);
@@ -78,7 +78,7 @@ export function register(app, { projects, project }) {
78
78
  res.json(sessions);
79
79
  });
80
80
 
81
- app.post("/projects/:pid/agents/:slug/sessions", (req, res) => {
81
+ api.post("/projects/:pid/agents/:slug/sessions", (req, res) => {
82
82
  const p = project(req, res);
83
83
  if (!p) return;
84
84
  const { title, body = "" } = req.body || {};
@@ -93,7 +93,7 @@ export function register(app, { projects, project }) {
93
93
  });
94
94
 
95
95
  // GET session by filename (sid may include or omit the .md extension)
96
- app.get("/projects/:pid/sessions/:sid", (req, res) => {
96
+ api.get("/projects/:pid/sessions/:sid", (req, res) => {
97
97
  const p = project(req, res);
98
98
  if (!p) return;
99
99
  const sid = req.params.sid;
@@ -2,7 +2,6 @@
2
2
  //
3
3
  // These were inlined in api.js (monolith). They are kept dependency-free so
4
4
  // any route module can `import` them without pulling in the world.
5
- import fs from "node:fs";
6
5
  import path from "node:path";
7
6
  import { randomUUID } from "node:crypto";
8
7
  import { appendErrorTrace, previewText } from "#core/logging.js";
@@ -11,6 +10,7 @@ import { agentMemoryPath } from "#core/agent/memory.js";
11
10
  import { apcMemoryFile } from "#core/apc/paths.js";
12
11
  import { CHANNELS } from "#core/constants/channels.js";
13
12
  import { isKnownSpaRoute } from "./web.js";
13
+ import { apiPath, isApiPath } from "./prefix.js";
14
14
 
15
15
  export const nowIso = () =>
16
16
  new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -20,6 +20,44 @@ export const nowIso = () =>
20
20
  // full set as a single page (data = 100% of rows) so non-paginated callers get
21
21
  // the same shape — meta just reports one page covering everything.
22
22
  // meta: { total, offset, limit, pageSize, page, pageCount }
23
+ /**
24
+ * Wrap an async route handler so a rejected promise becomes a 500 instead of
25
+ * an unhandled rejection.
26
+ *
27
+ * Express 4 does not await handlers: if one throws asynchronously, the error
28
+ * never reaches the error middleware, the request hangs, and on Node >= 15 the
29
+ * unhandled rejection takes the whole daemon down. Several routes awaited work
30
+ * with no try/catch, so a single failing preview or engine probe could kill a
31
+ * process serving the SPA, Telegram polling and voice at the same time.
32
+ *
33
+ * api.get("/things", asyncRoute(async (req, res) => { ... }))
34
+ */
35
+ export function asyncRoute(handler) {
36
+ return (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next);
37
+ }
38
+
39
+ /**
40
+ * Terminal error middleware. Mounted after every route; turns anything that
41
+ * reached `next(err)` into a JSON `{ error }` with a real status code.
42
+ */
43
+ export function errorMiddleware(log = () => {}) {
44
+ // eslint-disable-next-line no-unused-vars -- Express detects the 4-arg shape.
45
+ return (err, req, res, _next) => {
46
+ const status = Number(err?.status || err?.statusCode) || 500;
47
+ const message = err?.message || "internal error";
48
+ log(`error: ${req.method} ${req.originalUrl} → ${status}: ${err?.stack || message}`);
49
+ try {
50
+ appendErrorTrace({
51
+ where: `${req.method} ${req.originalUrl}`,
52
+ error: message,
53
+ trace_id: req.apxTraceId,
54
+ });
55
+ } catch { /* tracing must never mask the original error */ }
56
+ if (res.headersSent) return;
57
+ res.status(status).json({ error: message });
58
+ };
59
+ }
60
+
23
61
  export function pageEnvelope(rows, query = {}) {
24
62
  const total = rows.length;
25
63
  const hasLimit = query.limit != null && query.limit !== "";
@@ -46,12 +84,19 @@ export function traceIdMiddleware(req, res, next) {
46
84
  next();
47
85
  }
48
86
 
49
- // Paths that bypass auth: /health for liveness probes, /pair/* so a fresh
50
- // client can bootstrap a token without already having one, and
51
- // /admin/web-token so the local same-origin admin panel can self-bootstrap.
52
- // /pair/init and /admin/web-token both enforce localhost-only checks of
87
+ // Paths that bypass auth: /api/health for liveness probes, /api/pair/* so a
88
+ // fresh client can bootstrap a token without already having one, and
89
+ // /api/admin/web-token so the local same-origin admin panel can self-bootstrap.
90
+ // /api/pair/init and /api/admin/web-token both enforce localhost-only checks of
53
91
  // their own — the auth middleware just gets out of their way.
54
- const UNAUTHENTICATED_PREFIXES = ["/health", "/pair/", "/admin/web-token"];
92
+ //
93
+ // These are full paths: the middleware runs at app level (before the /api
94
+ // router), so req.path is the complete URL path.
95
+ const UNAUTHENTICATED_PREFIXES = [
96
+ apiPath("/health"),
97
+ apiPath("/pair/"),
98
+ apiPath("/admin/web-token"),
99
+ ];
55
100
 
56
101
  // Does this path look like a static asset (has a file extension)? Vite emits
57
102
  // hashed, extension-bearing filenames (index-abc123.js, logo.svg, font.woff2),
@@ -62,18 +107,19 @@ function isStaticAssetPath(p) {
62
107
  }
63
108
 
64
109
  function isUnauthenticatedPath(p, method = "GET") {
65
- if (p === "/health") return true;
66
- if (p === "/admin/web-token") return true;
67
110
  for (const prefix of UNAUTHENTICATED_PREFIXES) {
68
111
  if (p === prefix.replace(/\/$/, "") || p.startsWith(prefix)) return true;
69
112
  }
113
+ // Everything else under /api needs a token, full stop. Checked BEFORE the
114
+ // SPA-bootstrap exemption below, which would otherwise let a data route pass
115
+ // simply because its last segment happens to carry a file extension —
116
+ // GET /api/projects/0/artifacts/report.html is a data route, not an asset.
117
+ if (isApiPath(p)) return false;
70
118
  // SPA bootstrap: the admin bundle loads before it holds a bearer, so a GET
71
119
  // for a static asset or a known client-router route is served without auth —
72
- // the bundle then fetches /admin/web-token. Everything else, including every
73
- // data GET (/skills, /plugins, /embeddings, …), REQUIRES a token. This is an
74
- // allowlist by construction: a new data route can never silently become
75
- // public just because someone forgot to register its prefix (the old
76
- // denylist failure mode).
120
+ // the bundle then fetches /api/admin/web-token. Since every data route now
121
+ // lives under /api and returned false above, this exemption can only ever
122
+ // reach panel assets and client-router paths.
77
123
  if (method === "GET" && (isStaticAssetPath(p) || isKnownSpaRoute(p))) return true;
78
124
  return false;
79
125
  }
@@ -113,8 +113,8 @@ function indexStatus() {
113
113
  };
114
114
  }
115
115
 
116
- export function register(app /*, ctx */) {
117
- app.get("/skills", (req, res) => {
116
+ export function register(api /*, ctx */) {
117
+ api.get("/skills", (req, res) => {
118
118
  const projectPath = typeof req.query?.project_path === "string" && req.query.project_path
119
119
  ? req.query.project_path
120
120
  : undefined;
@@ -149,7 +149,7 @@ export function register(app /*, ctx */) {
149
149
 
150
150
  // ---- Full detail (viewer) ----------------------------------------------
151
151
 
152
- app.get("/skills/:slug/detail", (req, res) => {
152
+ api.get("/skills/:slug/detail", (req, res) => {
153
153
  try {
154
154
  const projectPath = typeof req.query?.project_path === "string" && req.query.project_path
155
155
  ? req.query.project_path
@@ -175,7 +175,7 @@ export function register(app /*, ctx */) {
175
175
 
176
176
  // ---- Enable / disable per scope ----------------------------------------
177
177
 
178
- app.put("/skills/enabled", (req, res) => {
178
+ api.put("/skills/enabled", (req, res) => {
179
179
  try {
180
180
  const { slug, enabled, scope, project_path } = req.body || {};
181
181
  if (!slug || typeof slug !== "string") {
@@ -202,7 +202,7 @@ export function register(app /*, ctx */) {
202
202
  // ---- Create / import user skills ---------------------------------------
203
203
 
204
204
  // Online editor: write a SKILL.md from slug + description + body.
205
- app.post("/skills", (req, res) => {
205
+ api.post("/skills", (req, res) => {
206
206
  try {
207
207
  const { slug, description, body, project_path } = req.body || {};
208
208
  if (!slug || typeof slug !== "string" || !SLUG_RE.test(slug)) {
@@ -220,7 +220,7 @@ export function register(app /*, ctx */) {
220
220
  });
221
221
 
222
222
  // Import from an uploaded .zip (sent as base64 in JSON — skills are tiny).
223
- app.post("/skills/import/zip", (req, res) => {
223
+ api.post("/skills/import/zip", (req, res) => {
224
224
  let tmp;
225
225
  try {
226
226
  const { data, project_path } = req.body || {};
@@ -257,7 +257,7 @@ export function register(app /*, ctx */) {
257
257
 
258
258
  // Import by cloning a git repo. The repo root (or its single skill subdir)
259
259
  // must contain a SKILL.md.
260
- app.post("/skills/import/repo", (req, res) => {
260
+ api.post("/skills/import/repo", (req, res) => {
261
261
  let tmp;
262
262
  try {
263
263
  const { url, project_path } = req.body || {};
@@ -293,7 +293,7 @@ export function register(app /*, ctx */) {
293
293
  }
294
294
  });
295
295
 
296
- app.delete("/skills/:slug", (req, res) => {
296
+ api.delete("/skills/:slug", (req, res) => {
297
297
  try {
298
298
  const slug = req.params.slug;
299
299
  if (!slug || !SLUG_RE.test(slug)) {
@@ -344,7 +344,7 @@ export function register(app /*, ctx */) {
344
344
 
345
345
  // ---- Inspector config + status -----------------------------------------
346
346
 
347
- app.get("/skills/inspector", (_req, res) => {
347
+ api.get("/skills/inspector", (_req, res) => {
348
348
  try {
349
349
  const cfg = readConfig();
350
350
  res.json({
@@ -358,7 +358,7 @@ export function register(app /*, ctx */) {
358
358
  }
359
359
  });
360
360
 
361
- app.put("/skills/inspector", (req, res) => {
361
+ api.put("/skills/inspector", (req, res) => {
362
362
  try {
363
363
  const patch = req.body || {};
364
364
  const cfg = readConfig();
@@ -388,7 +388,7 @@ export function register(app /*, ctx */) {
388
388
 
389
389
  // ---- Index build --------------------------------------------------------
390
390
 
391
- app.post("/skills/index", async (req, res) => {
391
+ api.post("/skills/index", async (req, res) => {
392
392
  try {
393
393
  const { project_path, force } = req.body || {};
394
394
  const cfg = readConfig();
@@ -423,7 +423,7 @@ export function register(app /*, ctx */) {
423
423
 
424
424
  // ---- Dry-run ------------------------------------------------------------
425
425
 
426
- app.post("/skills/inspect", async (req, res) => {
426
+ api.post("/skills/inspect", async (req, res) => {
427
427
  try {
428
428
  const { prompt, project_path } = req.body || {};
429
429
  if (!prompt || typeof prompt !== "string") {
@@ -91,8 +91,8 @@ function wrapOnEventForLog(send, { trace_id, channel }) {
91
91
  };
92
92
  }
93
93
 
94
- export function register(app, { projects, registries, plugins, project, config }) {
95
- app.post("/projects/:pid/super-agent/chat/stream", async (req, res) => {
94
+ export function register(api, { projects, registries, plugins, project, config }) {
95
+ api.post("/projects/:pid/super-agent/chat/stream", async (req, res) => {
96
96
  const p = project(req, res);
97
97
  if (!p) return;
98
98
  // Optional coding-surface knobs: the terminal Code TUI (apx code, Build
@@ -223,7 +223,7 @@ export function register(app, { projects, registries, plugins, project, config }
223
223
  // <id>` when the session lives outside any registered APX project (e.g. a
224
224
  // raw Claude/Codex session). Returns { text } so callers can format the
225
225
  // summary however they want.
226
- app.post("/super-agent/summarize", async (req, res) => {
226
+ api.post("/super-agent/summarize", async (req, res) => {
227
227
  const { prompt, context_note: contextNote = "", model, max_tokens } = req.body || {};
228
228
  if (!prompt) return res.status(400).json({ error: "prompt required" });
229
229
  try {
@@ -254,7 +254,7 @@ export function register(app, { projects, registries, plugins, project, config }
254
254
  }
255
255
  });
256
256
 
257
- app.post("/projects/:pid/super-agent/chat", async (req, res) => {
257
+ api.post("/projects/:pid/super-agent/chat", async (req, res) => {
258
258
  const p = project(req, res);
259
259
  if (!p) return;
260
260
  const { prompt, previousMessages, model, maxIters, maxTokens, completionContract } =
@@ -24,11 +24,11 @@ import {
24
24
  } from "#core/stores/tasks.js";
25
25
  import { pageEnvelope } from "./shared.js";
26
26
 
27
- export function register(app, { project, projects }) {
27
+ export function register(api, { project, projects }) {
28
28
  // Global tasks across every project, newest first. Returns a { meta, data }
29
29
  // envelope. Paginated via ?limit & ?offset; with no limit, data is the full
30
30
  // set as one page.
31
- app.get("/tasks", (req, res) => {
31
+ api.get("/tasks", (req, res) => {
32
32
  const { state, tag, agent, due_before, due_after, status, updated_since } = req.query;
33
33
 
34
34
  // Resolve the registered projects to what core needs, dropping any the
@@ -63,7 +63,7 @@ export function register(app, { project, projects }) {
63
63
 
64
64
  // Per-project tasks. Returns a { meta, data } envelope; with no ?limit the
65
65
  // data array is the full filtered set (one page).
66
- app.get("/projects/:pid/tasks", (req, res) => {
66
+ api.get("/projects/:pid/tasks", (req, res) => {
67
67
  const p = project(req, res);
68
68
  if (!p) return;
69
69
  const { state, tag, agent, due_before, due_after, status, updated_since } = req.query;
@@ -79,7 +79,7 @@ export function register(app, { project, projects }) {
79
79
  res.json(pageEnvelope(all, req.query));
80
80
  });
81
81
 
82
- app.post("/projects/:pid/tasks", (req, res) => {
82
+ api.post("/projects/:pid/tasks", (req, res) => {
83
83
  const p = project(req, res);
84
84
  if (!p) return;
85
85
  try {
@@ -90,7 +90,7 @@ export function register(app, { project, projects }) {
90
90
  }
91
91
  });
92
92
 
93
- app.get("/projects/:pid/tasks/:id", (req, res) => {
93
+ api.get("/projects/:pid/tasks/:id", (req, res) => {
94
94
  const p = project(req, res);
95
95
  if (!p) return;
96
96
  const task = getTask(p.storagePath, req.params.id);
@@ -98,7 +98,7 @@ export function register(app, { project, projects }) {
98
98
  res.json(task);
99
99
  });
100
100
 
101
- app.patch("/projects/:pid/tasks/:id", (req, res) => {
101
+ api.patch("/projects/:pid/tasks/:id", (req, res) => {
102
102
  const p = project(req, res);
103
103
  if (!p) return;
104
104
  const { patch } = req.body || {};
@@ -110,7 +110,7 @@ export function register(app, { project, projects }) {
110
110
  res.json(updated);
111
111
  });
112
112
 
113
- app.post("/projects/:pid/tasks/:id/done", (req, res) => {
113
+ api.post("/projects/:pid/tasks/:id/done", (req, res) => {
114
114
  const p = project(req, res);
115
115
  if (!p) return;
116
116
  const { by = null } = req.body || {};
@@ -119,7 +119,7 @@ export function register(app, { project, projects }) {
119
119
  res.json(updated);
120
120
  });
121
121
 
122
- app.post("/projects/:pid/tasks/:id/drop", (req, res) => {
122
+ api.post("/projects/:pid/tasks/:id/drop", (req, res) => {
123
123
  const p = project(req, res);
124
124
  if (!p) return;
125
125
  const { by = null } = req.body || {};
@@ -128,7 +128,7 @@ export function register(app, { project, projects }) {
128
128
  res.json(updated);
129
129
  });
130
130
 
131
- app.post("/projects/:pid/tasks/:id/reopen", (req, res) => {
131
+ api.post("/projects/:pid/tasks/:id/reopen", (req, res) => {
132
132
  const p = project(req, res);
133
133
  if (!p) return;
134
134
  const updated = reopenTask(p.storagePath, req.params.id);
@@ -137,7 +137,7 @@ export function register(app, { project, projects }) {
137
137
  });
138
138
 
139
139
  // Move an open task through its workflow (pending → running → in_review …).
140
- app.post("/projects/:pid/tasks/:id/status", (req, res) => {
140
+ api.post("/projects/:pid/tasks/:id/status", (req, res) => {
141
141
  const p = project(req, res);
142
142
  if (!p) return;
143
143
  const { status } = req.body || {};
@@ -149,7 +149,7 @@ export function register(app, { project, projects }) {
149
149
  });
150
150
 
151
151
  // Lightweight summary endpoint for status displays.
152
- app.get("/projects/:pid/tasks-summary", (req, res) => {
152
+ api.get("/projects/:pid/tasks-summary", (req, res) => {
153
153
  const p = project(req, res);
154
154
  if (!p) return;
155
155
  res.json(countTasks(p.storagePath));
@@ -31,7 +31,6 @@ import {
31
31
  removeTelegramChannel,
32
32
  unsetTelegramChannelFields,
33
33
  listContacts,
34
- findContact,
35
34
  upsertContact,
36
35
  removeContact,
37
36
  listRoles,
@@ -41,13 +40,13 @@ import {
41
40
 
42
41
  import { redactChannel, isSecretMarker } from "#core/config/redact.js";
43
42
 
44
- export function register(app, { telegram }) {
45
- app.get("/telegram/status", (_req, res) => {
43
+ export function register(api, { telegram }) {
44
+ api.get("/telegram/status", (_req, res) => {
46
45
  if (!telegram) return res.json({ enabled: false, channels: [] });
47
46
  res.json(telegram.status());
48
47
  });
49
48
 
50
- app.post("/telegram/start", (_req, res) => {
49
+ api.post("/telegram/start", (_req, res) => {
51
50
  if (!telegram)
52
51
  return res.status(503).json({ error: "telegram plugin not loaded" });
53
52
  try {
@@ -58,7 +57,7 @@ export function register(app, { telegram }) {
58
57
  }
59
58
  });
60
59
 
61
- app.post("/telegram/stop", (_req, res) => {
60
+ api.post("/telegram/stop", (_req, res) => {
62
61
  if (!telegram)
63
62
  return res.status(503).json({ error: "telegram plugin not loaded" });
64
63
  try {
@@ -69,7 +68,7 @@ export function register(app, { telegram }) {
69
68
  }
70
69
  });
71
70
 
72
- app.post("/telegram/send", async (req, res) => {
71
+ api.post("/telegram/send", async (req, res) => {
73
72
  const { chat_id, text, channel } = req.body || {};
74
73
  if (!text) return res.status(400).json({ error: "text required" });
75
74
  if (!telegram)
@@ -82,7 +81,7 @@ export function register(app, { telegram }) {
82
81
  }
83
82
  });
84
83
 
85
- app.post("/telegram/send_photo", async (req, res) => {
84
+ api.post("/telegram/send_photo", async (req, res) => {
86
85
  const { chat_id, photo, caption, parse_mode, channel } = req.body || {};
87
86
  if (!photo)
88
87
  return res.status(400).json({ error: "photo required (path or url)" });
@@ -102,7 +101,7 @@ export function register(app, { telegram }) {
102
101
  }
103
102
  });
104
103
 
105
- app.post("/telegram/send_voice", async (req, res) => {
104
+ api.post("/telegram/send_voice", async (req, res) => {
106
105
  const { chat_id, audio, caption, duration, channel } = req.body || {};
107
106
  if (!audio) return res.status(400).json({ error: "audio required (path)" });
108
107
  if (!telegram)
@@ -121,7 +120,7 @@ export function register(app, { telegram }) {
121
120
  }
122
121
  });
123
122
 
124
- app.post("/telegram/send_audio", async (req, res) => {
123
+ api.post("/telegram/send_audio", async (req, res) => {
125
124
  const { chat_id, audio, caption, title, performer, channel } =
126
125
  req.body || {};
127
126
  if (!audio) return res.status(400).json({ error: "audio required (path)" });
@@ -145,7 +144,7 @@ export function register(app, { telegram }) {
145
144
  // ── Channel CRUD (config-only; caller must POST /admin/reload to apply) ──
146
145
  // We read fresh config from disk on each call so concurrent writes from the
147
146
  // CLI and the daemon don't clobber one another via stale closures.
148
- app.get("/telegram/channels", (_req, res) => {
147
+ api.get("/telegram/channels", (_req, res) => {
149
148
  try {
150
149
  const cfg = readConfig();
151
150
  res.json({ channels: listTelegramChannels(cfg).map(redactChannel) });
@@ -154,7 +153,7 @@ export function register(app, { telegram }) {
154
153
  }
155
154
  });
156
155
 
157
- app.post("/telegram/channels", (req, res) => {
156
+ api.post("/telegram/channels", (req, res) => {
158
157
  const body = req.body || {};
159
158
  if (!body.name || typeof body.name !== "string") {
160
159
  return res.status(400).json({ error: "name required" });
@@ -173,7 +172,7 @@ export function register(app, { telegram }) {
173
172
  }
174
173
  });
175
174
 
176
- app.patch("/telegram/channels/:name", (req, res) => {
175
+ api.patch("/telegram/channels/:name", (req, res) => {
177
176
  const { name } = req.params;
178
177
  const body = req.body || {};
179
178
  try {
@@ -202,7 +201,7 @@ export function register(app, { telegram }) {
202
201
  }
203
202
  });
204
203
 
205
- app.delete("/telegram/channels/:name", (req, res) => {
204
+ api.delete("/telegram/channels/:name", (req, res) => {
206
205
  const { name } = req.params;
207
206
  try {
208
207
  const cfg = readConfig();
@@ -219,7 +218,7 @@ export function register(app, { telegram }) {
219
218
  // channel. These edit ~/.apx/config.json directly; the running poller picks
220
219
  // changes up on its next inbound message (it re-reads config per message), so
221
220
  // no /admin/reload is required for contact/role edits.
222
- app.get("/telegram/contacts", (_req, res) => {
221
+ api.get("/telegram/contacts", (_req, res) => {
223
222
  try {
224
223
  const cfg = readConfig();
225
224
  res.json({
@@ -235,7 +234,7 @@ export function register(app, { telegram }) {
235
234
  }
236
235
  });
237
236
 
238
- app.patch("/telegram/contacts/:user_id", (req, res) => {
237
+ api.patch("/telegram/contacts/:user_id", (req, res) => {
239
238
  const { user_id } = req.params;
240
239
  const body = req.body || {};
241
240
  try {
@@ -254,7 +253,7 @@ export function register(app, { telegram }) {
254
253
  }
255
254
  });
256
255
 
257
- app.delete("/telegram/contacts/:user_id", (req, res) => {
256
+ api.delete("/telegram/contacts/:user_id", (req, res) => {
258
257
  const { user_id } = req.params;
259
258
  try {
260
259
  const cfg = readConfig();
@@ -267,7 +266,7 @@ export function register(app, { telegram }) {
267
266
  });
268
267
 
269
268
  // ── Roles (role → { tools }) ────────────────────────────────────────────
270
- app.get("/telegram/roles", (_req, res) => {
269
+ api.get("/telegram/roles", (_req, res) => {
271
270
  try {
272
271
  res.json({ roles: listRoles(readConfig()) });
273
272
  } catch (e) {
@@ -275,7 +274,7 @@ export function register(app, { telegram }) {
275
274
  }
276
275
  });
277
276
 
278
- app.put("/telegram/roles/:name", (req, res) => {
277
+ api.put("/telegram/roles/:name", (req, res) => {
279
278
  const { name } = req.params;
280
279
  const body = req.body || {};
281
280
  try {
@@ -288,7 +287,7 @@ export function register(app, { telegram }) {
288
287
  }
289
288
  });
290
289
 
291
- app.delete("/telegram/roles/:name", (req, res) => {
290
+ api.delete("/telegram/roles/:name", (req, res) => {
292
291
  const { name } = req.params;
293
292
  try {
294
293
  const { removed } = removeRole(readConfig(), name);
@@ -300,7 +299,7 @@ export function register(app, { telegram }) {
300
299
  });
301
300
 
302
301
  // Alias for proactive daemon-initiated pushes (routines, error handlers, …).
303
- app.post("/telegram/notify", async (req, res) => {
302
+ api.post("/telegram/notify", async (req, res) => {
304
303
  const { chat_id, text, channel } = req.body || {};
305
304
  if (!text) return res.status(400).json({ error: "text required" });
306
305
  if (!telegram)
@@ -11,11 +11,11 @@ import { buildRegistryRouter } from "#core/http-tools/registry.js";
11
11
  import { buildGlobRouter } from "#core/http-tools/glob.js";
12
12
  import { buildGrepRouter } from "#core/http-tools/grep.js";
13
13
 
14
- export function register(app, { express, projects, registries }) {
15
- app.use("/tools/fetch", buildFetchRouter(express));
16
- app.use("/tools/browser", buildBrowserRouter(express));
17
- app.use("/tools/search", buildSearchRouter(express));
18
- app.use("/tools/glob", buildGlobRouter(express));
19
- app.use("/tools/grep", buildGrepRouter(express));
20
- app.use("/tools", buildRegistryRouter(express, { projects, registries }));
14
+ export function register(api, { express, projects, registries }) {
15
+ api.use("/tools/fetch", buildFetchRouter(express));
16
+ api.use("/tools/browser", buildBrowserRouter(express));
17
+ api.use("/tools/search", buildSearchRouter(express));
18
+ api.use("/tools/glob", buildGlobRouter(express));
19
+ api.use("/tools/grep", buildGrepRouter(express));
20
+ api.use("/tools", buildRegistryRouter(express, { projects, registries }));
21
21
  }