@agentprojectcontext/apx 1.77.2 → 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 (294) 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 +10 -7
  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-CBR_-QyA.js +824 -0
  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 +30 -7
  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-BWWpTTSd.js +0 -824
  283. package/src/interfaces/web/dist/assets/index-BWWpTTSd.js.map +0 -1
  284. package/src/skills/apc-context/SKILL.md +0 -159
  285. /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
  286. /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
  287. /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
  288. /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
  289. /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
  290. /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
  291. /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
  292. /package/src/{host/daemon → core}/runtimes/index.js +0 -0
  293. /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
  294. /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
@@ -12,9 +12,9 @@ import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { resolveMemoryPath } from "./shared.js";
14
14
 
15
- export function register(app, { projects, registries, resolveTopProject }) {
15
+ export function register(api, { projects, registries, resolveTopProject }) {
16
16
  // ---- /memory ----
17
- app.get("/memory", (req, res) => {
17
+ api.get("/memory", (req, res) => {
18
18
  const p = resolveTopProject(req.query);
19
19
  if (!p) return res.status(404).json({ error: "no project registered" });
20
20
  const memPath = resolveMemoryPath(p);
@@ -22,7 +22,7 @@ export function register(app, { projects, registries, resolveTopProject }) {
22
22
  res.json({ project_id: p.id, path: memPath, body });
23
23
  });
24
24
 
25
- app.post("/memory", (req, res) => {
25
+ api.post("/memory", (req, res) => {
26
26
  const p = resolveTopProject(req.query);
27
27
  if (!p) return res.status(404).json({ error: "no project registered" });
28
28
  const { body } = req.body || {};
@@ -42,7 +42,7 @@ export function register(app, { projects, registries, resolveTopProject }) {
42
42
  });
43
43
 
44
44
  // ---- /files ----
45
- app.get("/files", (req, res) => {
45
+ api.get("/files", (req, res) => {
46
46
  const p = resolveTopProject(req.query);
47
47
  if (!p) return res.status(404).json({ error: "no project registered" });
48
48
  const rel = req.query.path;
@@ -90,7 +90,7 @@ export function register(app, { projects, registries, resolveTopProject }) {
90
90
  });
91
91
  });
92
92
 
93
- app.post("/files", (req, res) => {
93
+ api.post("/files", (req, res) => {
94
94
  const p = resolveTopProject(req.query);
95
95
  if (!p) return res.status(404).json({ error: "no project registered" });
96
96
  const { path: rel, content } = req.body || {};
@@ -111,13 +111,13 @@ export function register(app, { projects, registries, resolveTopProject }) {
111
111
  });
112
112
 
113
113
  // ---- /mcp ----
114
- app.get("/mcp", (req, res) => {
114
+ api.get("/mcp", (req, res) => {
115
115
  const p = resolveTopProject(req.query);
116
116
  if (!p) return res.status(404).json({ error: "no project registered" });
117
117
  res.json(registries.for(p).list());
118
118
  });
119
119
 
120
- app.post("/mcp/run", async (req, res) => {
120
+ api.post("/mcp/run", async (req, res) => {
121
121
  const { project: projectRef, name, tool, params } = req.body || {};
122
122
  if (!name) return res.status(400).json({ error: "name required" });
123
123
  if (!tool) return res.status(400).json({ error: "tool required" });
@@ -5,12 +5,12 @@
5
5
  // X-Provider auto | local | openai | custom (overrides config)
6
6
  //
7
7
  // Shared by overlay, telegram voice messages, and any external caller.
8
- export function register(app) {
8
+ export function register(api) {
9
9
  // GET /transcribe/providers — STT engine list + availability for the web
10
10
  // admin (mirror of /tts/providers). local = embedded faster-whisper;
11
11
  // openai = cloud Whisper; custom = any OpenAI-compatible server (mlx-audio
12
12
  // on Metal, a Radeon/NVIDIA box on the LAN, a remote endpoint).
13
- app.get("/transcribe/providers", async (_req, res) => {
13
+ api.get("/transcribe/providers", async (_req, res) => {
14
14
  try {
15
15
  const { readConfig } = await import("#core/config/index.js");
16
16
  const { listSttProviders } = await import("#core/voice/transcription.js");
@@ -23,7 +23,7 @@ export function register(app) {
23
23
  // GET /transcribe/hardware — detected machine + the recommended local backend
24
24
  // (mlx on Apple Silicon, faster-whisper cuda on NVIDIA, else CPU). Drives the
25
25
  // "engine adapts itself" UX in the web admin.
26
- app.get("/transcribe/hardware", async (_req, res) => {
26
+ api.get("/transcribe/hardware", async (_req, res) => {
27
27
  try {
28
28
  const { detectHardware, recommendStt } = await import("#core/voice/stt-hardware.js");
29
29
  const hw = detectHardware();
@@ -35,7 +35,7 @@ export function register(app) {
35
35
 
36
36
  // GET /transcribe/models?backend=faster|mlx — model catalog with on-disk
37
37
  // status (downloaded? size) for the model-manager UI.
38
- app.get("/transcribe/models", async (req, res) => {
38
+ api.get("/transcribe/models", async (req, res) => {
39
39
  try {
40
40
  const backend = String(req.query.backend || "faster");
41
41
  const { listSttModels } = await import("#core/voice/stt-models.js");
@@ -48,7 +48,7 @@ export function register(app) {
48
48
  // GET /transcribe/warmup — load the local whisper model (if needed) and reset
49
49
  // its idle watchdog. Callers (e.g. the desktop window) ping this while open so
50
50
  // the first real utterance doesn't pay the cold-load cost.
51
- app.get("/transcribe/warmup", async (_req, res) => {
51
+ api.get("/transcribe/warmup", async (_req, res) => {
52
52
  try {
53
53
  const { warmupWhisper } = await import("../whisper-server.js");
54
54
  res.json(await warmupWhisper());
@@ -57,7 +57,7 @@ export function register(app) {
57
57
  }
58
58
  });
59
59
 
60
- app.post("/transcribe/chunk", async (req, res) => {
60
+ api.post("/transcribe/chunk", async (req, res) => {
61
61
  const chunks = [];
62
62
  req.on("data", (c) => chunks.push(c));
63
63
  req.on("end", async () => {
@@ -11,8 +11,8 @@
11
11
  import { synthesize, listProviders } from "#core/voice/tts.js";
12
12
  import { readConfig } from "#core/config/index.js";
13
13
 
14
- export function register(app) {
15
- app.post("/tts/say", async (req, res) => {
14
+ export function register(api) {
15
+ api.post("/tts/say", async (req, res) => {
16
16
  try {
17
17
  const { text, voice, language, provider, format, style } = req.body || {};
18
18
  if (typeof text !== "string" || !text.trim()) {
@@ -33,7 +33,7 @@ export function register(app) {
33
33
  }
34
34
  });
35
35
 
36
- app.get("/tts/providers", async (_req, res) => {
36
+ api.get("/tts/providers", async (_req, res) => {
37
37
  try {
38
38
  const info = await listProviders(readConfig());
39
39
  res.json(info);
@@ -24,7 +24,10 @@ import {
24
24
  maskValue,
25
25
  } from "#core/vars/index.js";
26
26
 
27
- function normalizeScope(raw, { isBase }) {
27
+ // Var scope vocabulary: project | global. The default depends on whether the
28
+ // request targets the base store. See the note in api/mcps.js: these scope
29
+ // helpers share a shape but not a vocabulary.
30
+ function normalizeVarScope(raw, { isBase }) {
28
31
  if (!raw) return isBase ? "global" : "project";
29
32
  const s = String(raw).toLowerCase();
30
33
  if (s === "project" || s === "global") return s;
@@ -37,8 +40,8 @@ function maskAll(obj) {
37
40
  return out;
38
41
  }
39
42
 
40
- export function register(app, { project, registries }) {
41
- app.get("/projects/:pid/vars", (req, res) => {
43
+ export function register(api, { project, registries }) {
44
+ api.get("/projects/:pid/vars", (req, res) => {
42
45
  const p = project(req, res);
43
46
  if (!p) return;
44
47
  const reveal = req.query?.reveal === "1" || req.query?.reveal === "true";
@@ -54,7 +57,7 @@ export function register(app, { project, registries }) {
54
57
  });
55
58
  });
56
59
 
57
- app.get("/projects/:pid/vars/:name", (req, res) => {
60
+ api.get("/projects/:pid/vars/:name", (req, res) => {
58
61
  const p = project(req, res);
59
62
  if (!p) return;
60
63
  const name = req.params.name;
@@ -80,7 +83,7 @@ export function register(app, { project, registries }) {
80
83
  });
81
84
  });
82
85
 
83
- app.post("/projects/:pid/vars", (req, res) => {
86
+ api.post("/projects/:pid/vars", (req, res) => {
84
87
  const p = project(req, res);
85
88
  if (!p) return;
86
89
  const { name, value } = req.body || {};
@@ -91,7 +94,7 @@ export function register(app, { project, registries }) {
91
94
  return res.status(400).json({ error: "value required" });
92
95
  }
93
96
  const isBase = String(p.id) === "0";
94
- const scope = normalizeScope(req.body?.scope, { isBase });
97
+ const scope = normalizeVarScope(req.body?.scope, { isBase });
95
98
  if (scope === null) {
96
99
  return res
97
100
  .status(400)
@@ -111,11 +114,11 @@ export function register(app, { project, registries }) {
111
114
  res.status(201).json({ ok: true, name, scope });
112
115
  });
113
116
 
114
- app.delete("/projects/:pid/vars/:name", (req, res) => {
117
+ api.delete("/projects/:pid/vars/:name", (req, res) => {
115
118
  const p = project(req, res);
116
119
  if (!p) return;
117
120
  const isBase = String(p.id) === "0";
118
- const scope = normalizeScope(req.query?.scope, { isBase });
121
+ const scope = normalizeVarScope(req.query?.scope, { isBase });
119
122
  if (scope === null) {
120
123
  return res
121
124
  .status(400)
@@ -20,6 +20,7 @@
20
20
  // core/. This file is just glue: parse request → call core → format response.
21
21
  import fs from "node:fs";
22
22
  import path from "node:path";
23
+ import { TTS_TMP_DIR } from "#core/config/paths.js";
23
24
  import { readConfig } from "#core/config/index.js";
24
25
  import { synthesize } from "#core/voice/tts.js";
25
26
  import { stripEmotionTags } from "#core/voice/emotions.js";
@@ -31,18 +32,17 @@ import { extractSuggestions } from "#core/agent/suggestions.js";
31
32
  import { appendGlobalMessage } from "#core/stores/messages.js";
32
33
  import { appendErrorTrace, previewText } from "#core/logging.js";
33
34
 
34
- export function register(app, { projects, plugins, registries }) {
35
+ export function register(api, { projects, plugins, registries }) {
35
36
  // GET /voice/tts?path=<abs>
36
37
  //
37
38
  // Streams a TTS audio file back to the caller. Sandboxed to the
38
39
  // ~/.apx/tmp/tts directory so a client can't request arbitrary
39
40
  // filesystem paths through a manifest-leaked reply_audio_path.
40
- app.get("/voice/tts", async (req, res) => {
41
+ api.get("/voice/tts", async (req, res) => {
41
42
  const rawPath = String(req.query.path || "");
42
43
  if (!rawPath) return res.status(400).json({ error: "path required" });
43
44
  try {
44
- const os = await import("node:os");
45
- const ttsRoot = path.resolve(os.homedir(), ".apx", "tmp", "tts");
45
+ const ttsRoot = path.resolve(TTS_TMP_DIR);
46
46
  const resolved = path.resolve(rawPath);
47
47
  if (!resolved.startsWith(ttsRoot + path.sep)) {
48
48
  return res.status(403).json({ error: "path outside tts dir" });
@@ -65,7 +65,7 @@ export function register(app, { projects, plugins, registries }) {
65
65
  }
66
66
  });
67
67
 
68
- app.post("/voice/turn", async (req, res) => {
68
+ api.post("/voice/turn", async (req, res) => {
69
69
  const body = req.body || {};
70
70
  const cfg = readConfig();
71
71
  let userText = (body.text || "").trim();
@@ -103,7 +103,7 @@ export function register(app, { projects, plugins, registries }) {
103
103
  : [];
104
104
  const channel = body.channel || "voice";
105
105
  let suggestions = [];
106
- let toolsUsed = [];
106
+ const toolsUsed = [];
107
107
  // Attribution for the persisted turn (see the appendGlobalMessage below).
108
108
  let replyModel = null;
109
109
  let replyUsage = null;
@@ -2,9 +2,10 @@
2
2
  //
3
3
  // The web bundle is built into src/interfaces/web/dist. When that folder
4
4
  // exists, this module mounts it at `/` so users can open
5
- // http://127.0.0.1:7430 and get the UI. SPA fallback: anything that isn't an
6
- // /api/* / /projects/* / etc. route falls through to index.html so React
7
- // Router can resolve client-side paths like /p/1/tasks.
5
+ // http://127.0.0.1:7430 and get the UI. The root namespace belongs to the
6
+ // panel alone every data route lives under /api (see ./prefix.js) so the
7
+ // SPA fallback only has to step aside for that single prefix instead of a
8
+ // hand-maintained list that drifts away from the route registry.
8
9
  //
9
10
  // During development the user runs `vite dev` on :7431 with proxy → :7430,
10
11
  // so this mount is a no-op (dist/ doesn't exist yet).
@@ -12,29 +13,17 @@ import fs from "node:fs";
12
13
  import path from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
15
 
16
+ import { isApiPath } from "./prefix.js";
17
+
18
+ // Re-exported so the SPA/API seam keeps a single import site for callers that
19
+ // reason about "which side of the seam is this path on?" (tests, auth).
20
+ export { isApiPath };
21
+
15
22
  const __filename = fileURLToPath(import.meta.url);
16
23
  const __dirname = path.dirname(__filename);
17
24
  // host/daemon/api/web.js → ../../.. = src/ then interfaces/web/dist
18
25
  const WEB_DIST = path.resolve(__dirname, "..", "..", "..", "interfaces", "web", "dist");
19
26
 
20
- // Paths the panel is NOT allowed to swallow with its SPA fallback. These are
21
- // the real API surfaces; we keep the list flat so it doesn't drift away from
22
- // the actual route registry.
23
- const API_PREFIXES = [
24
- "/health", "/admin", "/projects", "/telegram", "/engines", "/runtimes",
25
- "/messages", "/sessions", "/tools", "/mcp", "/voice", "/tts", "/desktop", "/overlay",
26
- "/transcribe", "/run", "/files", "/memory", "/env", "/pair", "/deck",
27
- "/super-agent", "/identity", "/skills", "/profiles", "/inbox",
28
- "/tasks", "/agents", "/plugins", "/previews", "/embeddings",
29
- ];
30
-
31
- export function isApiPath(p) {
32
- for (const prefix of API_PREFIXES) {
33
- if (p === prefix || p.startsWith(prefix + "/")) return true;
34
- }
35
- return false;
36
- }
37
-
38
27
  // Client-side routes the SPA actually knows how to render. Must mirror the
39
28
  // <Routes> registry in src/interfaces/web/src/App.tsx. Anything else still
40
29
  // gets the SPA shell (so React Router shows the styled 404 page) but with an
@@ -51,15 +40,18 @@ export function isKnownSpaRoute(p) {
51
40
  return SPA_ROUTES.some((re) => re.test(p));
52
41
  }
53
42
 
54
- export function register(app, { express, token }) {
55
- // /admin/web-token: localhost-only endpoint that returns the daemon token
56
- // so the same-origin admin panel can authenticate every subsequent call.
57
- // Refuses if the request didn't come from loopback. Also refuses if the
58
- // request was tunneled in via Cloudflare/ngrok/etc. those connect from
59
- // a local agent so the socket IP IS loopback, but tunnel-specific headers
60
- // give them away. When tunneled, the SPA must instead receive the token
61
- // via URL fragment (#token=…) that the operator shares out-of-band.
62
- app.get("/admin/web-token", (req, res) => {
43
+ // /api/admin/web-token: localhost-only endpoint that returns the daemon token
44
+ // so the same-origin admin panel can authenticate every subsequent call.
45
+ // Refuses if the request didn't come from loopback. Also refuses if the
46
+ // request was tunneled in via Cloudflare/ngrok/etc. those connect from
47
+ // a local agent so the socket IP IS loopback, but tunnel-specific headers
48
+ // give them away. When tunneled, the SPA must instead receive the token
49
+ // via URL fragment (#token=…) that the operator shares out-of-band.
50
+ //
51
+ // Registered on the API router (not the static mount below) so the panel's
52
+ // bootstrap call is a normal /api/* request like every other.
53
+ export function registerWebToken(api, { token }) {
54
+ api.get("/admin/web-token", (req, res) => {
63
55
  const ra = req.ip || req.socket?.remoteAddress || "";
64
56
  const isLocal =
65
57
  ra === "127.0.0.1" ||
@@ -86,7 +78,11 @@ export function register(app, { express, token }) {
86
78
  }
87
79
  res.json({ token });
88
80
  });
81
+ }
89
82
 
83
+ // Static admin panel at the root. Mounted after `app.use("/api", api)` in
84
+ // api.js, so a stray /api/* request can never reach the SPA fallback.
85
+ export function register(app, { express }) {
90
86
  if (!fs.existsSync(WEB_DIST)) {
91
87
  // No build present. Expose a hint endpoint so users hitting the daemon
92
88
  // root know what to do.
@@ -112,8 +108,8 @@ export function register(app, { express, token }) {
112
108
  return;
113
109
  }
114
110
 
115
- // Serve static assets. NB: we mount AFTER the API routes are registered
116
- // in api.js so /projects, /admin/*, etc. always win over the catch-all.
111
+ // Serve static assets. NB: we mount AFTER the /api router is mounted in
112
+ // api.js so data routes always win over this catch-all.
117
113
  app.use(
118
114
  express.static(WEB_DIST, {
119
115
  index: false,
@@ -1,15 +1,26 @@
1
1
  // Express REST API for APX. See APC docs reference/apx-daemon.
2
2
  //
3
3
  // Routes are split by domain under ./api/*.js — each module exports
4
- // `register(app, ctx)`. This file is purely orchestration: middlewares,
4
+ // `register(router, ctx)`. This file is purely orchestration: middlewares,
5
5
  // context construction, mount order, 404 catch-all.
6
+ //
7
+ // EVERY data route lives under /api. Domain modules still declare their paths
8
+ // root-relative (`/projects/:pid/tasks`); they register on an express.Router()
9
+ // that is mounted at API_PREFIX, so the effective URL is /api/projects/:pid/
10
+ // tasks. The root namespace belongs exclusively to the admin panel (static
11
+ // bundle + client-side routes), which is why the "is this an API path?" test
12
+ // is now a single startsWith instead of a hand-maintained prefix list.
6
13
  import express from "express";
7
14
 
15
+ import { API_PREFIX } from "./api/prefix.js";
16
+ import { logError } from "#core/logging.js";
17
+
8
18
  import {
9
19
  traceIdMiddleware,
10
20
  buildAuthMiddleware,
11
21
  makeProjectResolver,
12
22
  makeTopProjectResolver,
23
+ errorMiddleware,
13
24
  } from "./api/shared.js";
14
25
 
15
26
  import { register as registerTools } from "./api/tools.js";
@@ -53,7 +64,7 @@ import { register as registerAdminConfig } from "./api/admin-config.js";
53
64
  import { register as registerIdentity } from "./api/identity.js";
54
65
  import { register as registerProfiles } from "./api/profiles.js";
55
66
  import { register as registerInbox } from "./api/inbox.js";
56
- import { register as registerWeb } from "./api/web.js";
67
+ import { register as registerWeb, registerWebToken } from "./api/web.js";
57
68
  import { register as registerConfirm } from "./api/confirm.js";
58
69
 
59
70
  export function buildApi({
@@ -70,6 +81,9 @@ export function buildApi({
70
81
  }) {
71
82
  const telegram = plugins?.get("telegram");
72
83
  const app = express();
84
+ // Every domain module registers on this router; it is mounted at /api at the
85
+ // bottom of this function. Modules keep declaring root-relative paths.
86
+ const api = express.Router();
73
87
 
74
88
  // ---- Global middleware -------------------------------------------
75
89
  app.use(express.json({ limit: "2mb" }));
@@ -103,62 +117,71 @@ export function buildApi({
103
117
  };
104
118
 
105
119
  // ---- Tool routers — must mount BEFORE wildcard registry below ----
106
- registerTools(app, ctx);
120
+ registerTools(api, ctx);
107
121
 
108
122
  // ---- Health (unauthenticated) ------------------------------------
109
- registerHealth(app, ctx);
123
+ registerHealth(api, ctx);
110
124
 
111
125
  // ---- Projects + per-project surfaces -----------------------------
112
- registerProjects(app, ctx);
113
- registerAgents(app, ctx);
114
- registerSessions(app, ctx);
115
- registerMcps(app, ctx);
116
- registerIntegrations(app, ctx);
117
- registerVars(app, ctx);
118
- registerMessages(app, ctx);
119
- registerEngines(app, ctx);
120
- registerSkills(app, ctx);
121
- registerExec(app, ctx);
122
- registerSuperAgent(app, ctx);
123
- registerConfirm(app, ctx);
124
- registerCode(app, ctx);
125
- registerConversations(app, ctx);
126
- registerConnections(app, ctx);
127
- registerRuntimes(app, ctx);
128
- registerRoutines(app, ctx);
129
- registerArtifacts(app, ctx);
130
- registerArtifactPreview(app, ctx);
131
- registerTasks(app, ctx);
132
- registerOrganization(app, ctx);
133
- registerProjectFiles(app, ctx);
134
- registerConfig(app, ctx);
126
+ registerProjects(api, ctx);
127
+ registerAgents(api, ctx);
128
+ registerSessions(api, ctx);
129
+ registerMcps(api, ctx);
130
+ registerIntegrations(api, ctx);
131
+ registerVars(api, ctx);
132
+ registerMessages(api, ctx);
133
+ registerEngines(api, ctx);
134
+ registerSkills(api, ctx);
135
+ registerExec(api, ctx);
136
+ registerSuperAgent(api, ctx);
137
+ registerConfirm(api, ctx);
138
+ registerCode(api, ctx);
139
+ registerConversations(api, ctx);
140
+ registerConnections(api, ctx);
141
+ registerRuntimes(api, ctx);
142
+ registerRoutines(api, ctx);
143
+ registerArtifacts(api, ctx);
144
+ registerArtifactPreview(api, ctx);
145
+ registerTasks(api, ctx);
146
+ registerOrganization(api, ctx);
147
+ registerProjectFiles(api, ctx);
148
+ registerConfig(api, ctx);
135
149
 
136
150
  // ---- Top-level shortcuts (MCP server clients) --------------------
137
- registerRun(app, ctx);
138
- registerTopLevel(app, ctx);
139
- registerSessionsSearch(app, ctx);
151
+ registerRun(api, ctx);
152
+ registerTopLevel(api, ctx);
153
+ registerSessionsSearch(api, ctx);
140
154
 
141
155
  // ---- Channels & plugin surfaces ----------------------------------
142
- registerTelegram(app, ctx);
143
- registerPlugins(app, ctx);
144
- registerTranscribe(app, ctx);
145
- registerTts(app, ctx);
146
- registerEmbeddings(app, ctx);
147
- registerVoice(app, ctx);
148
- registerDesktop(app, ctx);
149
- registerDeck(app, ctx);
150
- registerPairing(app, ctx);
156
+ registerTelegram(api, ctx);
157
+ registerPlugins(api, ctx);
158
+ registerTranscribe(api, ctx);
159
+ registerTts(api, ctx);
160
+ registerEmbeddings(api, ctx);
161
+ registerVoice(api, ctx);
162
+ registerDesktop(api, ctx);
163
+ registerDeck(api, ctx);
164
+ registerPairing(api, ctx);
151
165
 
152
166
  // ---- Admin -------------------------------------------------------
153
- registerAdmin(app, ctx);
154
- registerAdminConfig(app, ctx);
155
- registerIdentity(app, ctx);
156
- registerProfiles(app, ctx);
157
- registerInbox(app, ctx);
158
-
159
- // ---- Web admin panel (static SPA, must mount before 404) ---------
160
- // Serves src/interfaces/web/dist when present + the /admin/web-token
161
- // localhost-only token endpoint. No-op until the panel is built.
167
+ registerAdmin(api, ctx);
168
+ registerAdminConfig(api, ctx);
169
+ registerIdentity(api, ctx);
170
+ registerProfiles(api, ctx);
171
+ registerInbox(api, ctx);
172
+ registerWebToken(api, ctx);
173
+
174
+ // ---- API 404 (MUST be last on the router) ------------------------
175
+ // Terminal for the /api namespace: an unknown /api/* path answers JSON here
176
+ // instead of falling through to the SPA shell below.
177
+ api.use((req, res) =>
178
+ res.status(404).json({ error: `no route ${req.method} ${req.baseUrl}${req.path}` })
179
+ );
180
+
181
+ app.use(API_PREFIX, api);
182
+
183
+ // ---- Web admin panel (static SPA, must mount after /api) ---------
184
+ // Serves src/interfaces/web/dist when present. No-op until the panel is built.
162
185
  registerWeb(app, ctx);
163
186
 
164
187
  // ---- 404 catch-all (MUST be last) --------------------------------
@@ -166,5 +189,11 @@ export function buildApi({
166
189
  res.status(404).json({ error: `no route ${req.method} ${req.path}` })
167
190
  );
168
191
 
192
+ // ---- Error handler (MUST be after everything) --------------------
193
+ // Express only recognises the 4-argument shape, and only when it is mounted
194
+ // last. Anything an asyncRoute() rejects lands here as JSON instead of an
195
+ // unhandled rejection that kills the daemon.
196
+ app.use(errorMiddleware(logError));
197
+
169
198
  return app;
170
199
  }
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { appendMessageToFs } from "#core/stores/messages.js";
6
6
  import { effectiveConfig } from "./project-config.js";
7
7
  import { readAgents } from "#core/apc/parser.js";
8
- import { apcDir, apcProjectFile, apcAgentsDir, apcCommandsDir } from "#core/apc/paths.js";
8
+ import { apcProjectFile, apcAgentsDir, apcCommandsDir } from "#core/apc/paths.js";
9
9
  import { getOrCreateApxId } from "#core/apc/scaffold.js";
10
10
  import {
11
11
  ensureProjectStorage,
@@ -1,5 +1,6 @@
1
1
  // Singleton WebSocket hub for the desktop channel.
2
2
  // Imported by api.js (to register connections) and by plugins/desktop/index.js (to broadcast).
3
+ import { apiPath } from "./api/prefix.js";
3
4
 
4
5
  const _clients = new Set(); // Set<WebSocket>
5
6
  let _messageHandler = null; // (ws, data) => void — set by desktop plugin
@@ -12,7 +13,7 @@ export function setDesktopMessageHandler(fn) {
12
13
 
13
14
  // --- WS upgrade auth helpers (shared by the daemon upgrade handler + tests) ---
14
15
  //
15
- // The desktop WS channel must authenticate the same way the HTTP /desktop/*
16
+ // The desktop WS channel must authenticate the same way the HTTP /api/desktop/*
16
17
  // routes do: a bearer token (master or paired client) carried on the upgrade
17
18
  // request. The legitimate desktop window sends `Authorization: Bearer <token>`
18
19
  // (src/interfaces/desktop/main.js); browser clients can pass `?token=`. Without
@@ -20,11 +21,14 @@ export function setDesktopMessageHandler(fn) {
20
21
  // when host is set to 0.0.0.0) could open the channel and drive the
21
22
  // super-agent. See QA BUG-WS-AUTH.
22
23
 
23
- /** Path-gate: is this upgrade for the desktop (or legacy overlay) WS channel? */
24
+ /** The desktop channel's upgrade path. Lives under /api like every other route. */
25
+ export const DESKTOP_WS_PATH = apiPath("/desktop/ws");
26
+
27
+ /** Path-gate: is this upgrade for the desktop WS channel? */
24
28
  export function isDesktopUpgradePath(url) {
25
29
  let pathname = url || "";
26
30
  try { pathname = new URL(url, "http://localhost").pathname; } catch { /* keep raw */ }
27
- return pathname === "/desktop/ws" || pathname === "/overlay/ws";
31
+ return pathname === DESKTOP_WS_PATH;
28
32
  }
29
33
 
30
34
  /** Extract the bearer token from the upgrade request (header first, ?token= fallback). */
@@ -12,12 +12,10 @@ import { randomBytes } from "node:crypto";
12
12
  const LOOPBACK_HOST = "127.0.0.1";
13
13
  import {
14
14
  readConfig,
15
- writeConfig,
16
15
  effectiveHost,
17
16
  effectivePort,
18
17
  addProject as addProjectInConfig,
19
18
  PID_PATH,
20
- LOG_PATH,
21
19
  APX_HOME,
22
20
  TOKEN_PATH,
23
21
  } from "#core/config/index.js";
@@ -297,12 +295,11 @@ async function main() {
297
295
  }).catch(() => {});
298
296
  });
299
297
 
300
- // Attach WebSocket upgrade for the desktop channel on /desktop/ws
301
- // (legacy /overlay/ws still accepted for one release).
298
+ // Attach WebSocket upgrade for the desktop channel on /api/desktop/ws.
302
299
  server.on("upgrade", async (req, socket, head) => {
303
300
  if (!isDesktopUpgradePath(req.url)) { socket.destroy(); return; }
304
301
  // Auth: the WS upgrade must carry a valid token (master or paired client),
305
- // matching the HTTP /desktop/* routes. Without this, any client that can
302
+ // matching the HTTP /api/desktop/* routes. Without this, any client that can
306
303
  // reach the daemon (host binds 0.0.0.0 → the LAN) could open the desktop
307
304
  // channel and drive the super-agent (permission_mode "total"). The
308
305
  // legitimate desktop window already sends the bearer token. See QA BUG-WS-AUTH.
@@ -358,6 +355,15 @@ async function main() {
358
355
  process.on("uncaughtException", (e) => {
359
356
  log(`uncaughtException: ${e.stack || e.message}`);
360
357
  });
358
+ // Node >= 15 terminates the process on an unhandled rejection. The CLI has
359
+ // always registered this; the daemon had not, so one un-awaited failure in a
360
+ // route or a plugin took down the process serving the SPA, Telegram polling,
361
+ // voice and the deck at once. Log and keep running — the request-level
362
+ // errorMiddleware already answers the caller.
363
+ process.on("unhandledRejection", (reason) => {
364
+ const e = reason instanceof Error ? reason : new Error(String(reason));
365
+ log(`unhandledRejection: ${e.stack || e.message}`);
366
+ });
361
367
  }
362
368
 
363
369
  main().catch((e) => {
@@ -103,7 +103,7 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
103
103
  await appendGlobalMessage({ channel: CHANNEL, direction: "in", type: "user", author: "user", body: text });
104
104
  } catch {}
105
105
 
106
- let toolsExecuted = [];
106
+ const toolsExecuted = [];
107
107
 
108
108
  // Per-segment streaming: instead of merging the whole turn into one blob, we
109
109
  // emit each assistant text piece as its own `segment` (an intro before a tool,
@@ -311,7 +311,7 @@ export default {
311
311
  // that bot; otherwise first available bot-tokened channel. Always logs
312
312
  // the outbound on `messages` of the channel's target project so audit
313
313
  // trails are complete.
314
- async send({ channel: channelName, chat_id, text, author = resolveAgentName(config), project }) {
314
+ async send({ channel: channelName, chat_id, text, author = resolveAgentName(config) }) {
315
315
  const p = pickPoller(pollers, channelName);
316
316
  const result = await p._send({ chat_id, text });
317
317
  appendGlobalMessage({