@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
@@ -14,6 +14,7 @@ import path from "node:path";
14
14
  import { nowIso } from "../util/time.js";
15
15
  import { shortId as makeShortId } from "../util/ids.js";
16
16
  import { CODE_MODES, DEFAULT_CODE_MODE } from "../constants/code-modes.js";
17
+ import { readJson, writeJson } from "#core/util/json-file.js";
17
18
 
18
19
  function sessionsDir(storagePath) {
19
20
  return path.join(storagePath, "code-sessions");
@@ -28,20 +29,8 @@ function shortId() {
28
29
  }
29
30
 
30
31
  // Atomic write: tmp file + rename so a crash mid-write can't corrupt a session.
31
- function writeJson(file, obj) {
32
- fs.mkdirSync(path.dirname(file), { recursive: true });
33
- const tmp = `${file}.${process.pid}.tmp`;
34
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
35
- fs.renameSync(tmp, file);
36
- }
37
-
38
- function readJson(file) {
39
- try {
40
- return JSON.parse(fs.readFileSync(file, "utf8"));
41
- } catch {
42
- return null;
43
- }
44
- }
32
+ // readJson/writeJson come from the shared kernel (this module's atomic write
33
+ // was the one the others were missing).
45
34
 
46
35
  /** Lightweight row for the session list (no messages). */
47
36
  function toRow(s) {
@@ -13,10 +13,6 @@ import path from "node:path";
13
13
 
14
14
  const homeDir = (opts) => (opts && opts.home) || os.homedir();
15
15
 
16
- function encodeClaudeProjectPath(cwd) {
17
- return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
18
- }
19
-
20
16
  function safeStatMtime(p) {
21
17
  try {
22
18
  return fs.statSync(p).mtimeMs;
@@ -29,11 +29,6 @@ function dayPathJsonl(projectRoot, ts) {
29
29
  return path.join(projectRoot, "messages", `${day}.jsonl`);
30
30
  }
31
31
 
32
- function dayPathMd(projectRoot, ts) {
33
- const day = (ts || nowIso()).slice(0, 10);
34
- return path.join(projectRoot, "messages", `${day}.md`);
35
- }
36
-
37
32
  // `compact` is a progressive-compaction summary record (Pieza 3): a dense
38
33
  // recap of older turns, stored inline in the channel JSONL so the reader can
39
34
  // prepend it as a [RESUMEN COMPACTADO] system turn instead of replaying the
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import { apcOrganizationFile } from "../apc/paths.js";
22
+ import { readJson } from "#core/util/json-file.js";
22
23
 
23
24
  export const ORG_SLUG_RE = /^[a-z][a-z0-9_-]*$/;
24
25
 
@@ -39,17 +40,14 @@ function emptyOrg() {
39
40
 
40
41
  export function readOrganization(root) {
41
42
  const file = apcOrganizationFile(root);
42
- if (!fs.existsSync(file)) return emptyOrg();
43
- try {
44
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
45
- return {
46
- areas: Array.isArray(raw.areas) ? raw.areas : [],
47
- roles: Array.isArray(raw.roles) ? raw.roles : [],
48
- };
49
- } catch {
50
- // A corrupt file shouldn't take down the whole project view.
51
- return emptyOrg();
52
- }
43
+ // A missing or corrupt file shouldn't take down the whole project view —
44
+ // readJson returns the fallback for both.
45
+ const raw = readJson(file, null);
46
+ if (!raw) return emptyOrg();
47
+ return {
48
+ areas: Array.isArray(raw.areas) ? raw.areas : [],
49
+ roles: Array.isArray(raw.roles) ? raw.roles : [],
50
+ };
53
51
  }
54
52
 
55
53
  function writeOrganization(root, org) {
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { CronExpressionParser } from "cron-parser";
6
6
  import { nowIso, isoToMs } from "../util/time.js";
7
7
  import { shortId } from "../util/ids.js";
8
+ import { readJson, writeJson } from "#core/util/json-file.js";
8
9
 
9
10
  function routinesPath(storagePath) {
10
11
  // storagePath is always ~/.apx/projects/{apxId}/ — flat, no .apc subdir needed.
@@ -12,20 +13,14 @@ function routinesPath(storagePath) {
12
13
  }
13
14
 
14
15
  function readFile(projectPath) {
15
- const p = routinesPath(projectPath);
16
- if (!fs.existsSync(p)) return [];
17
- try {
18
- const raw = JSON.parse(fs.readFileSync(p, "utf8"));
19
- return Array.isArray(raw.routines) ? raw.routines : [];
20
- } catch {
21
- return [];
22
- }
16
+ const raw = readJson(routinesPath(projectPath), {});
17
+ return Array.isArray(raw?.routines) ? raw.routines : [];
23
18
  }
24
19
 
20
+ // Atomic: a crash mid-write used to leave a truncated routines.json, and since
21
+ // the reader swallows parse errors the next read reported "no routines".
25
22
  function writeFile(projectPath, routines) {
26
- const p = routinesPath(projectPath);
27
- fs.mkdirSync(path.dirname(p), { recursive: true });
28
- fs.writeFileSync(p, JSON.stringify({ routines }, null, 2) + "\n");
23
+ writeJson(routinesPath(projectPath), { routines });
29
24
  }
30
25
 
31
26
  // --------------------- schedule parsing -------------------------------------
@@ -18,6 +18,7 @@ import fs from "node:fs";
18
18
  import path from "node:path";
19
19
  import { APX_HOME } from "#core/config/paths.js";
20
20
  import { nowIso } from "#core/util/time.js";
21
+ import { readJson } from "#core/util/json-file.js";
21
22
 
22
23
  export const PENDING_CALLBACKS_DIR = path.join(APX_HOME, "pending-callbacks");
23
24
 
@@ -63,7 +64,7 @@ export function listPendingCallbacks() {
63
64
  const out = [];
64
65
  for (const f of files) {
65
66
  try {
66
- const entry = JSON.parse(fs.readFileSync(path.join(PENDING_CALLBACKS_DIR, f), "utf8"));
67
+ const entry = readJson(path.join(PENDING_CALLBACKS_DIR, f), null);
67
68
  if (entry?.session_id) out.push(entry);
68
69
  } catch {
69
70
  /* skip corrupt */
@@ -0,0 +1,128 @@
1
+ // JSON files on disk — one implementation.
2
+ //
3
+ // Six modules had grown their own read/write pair and 35 more sites inlined
4
+ // `JSON.parse(fs.readFileSync(...))`. They did not agree on the things that
5
+ // matter, so the guarantees your data got depended on which copy happened to
6
+ // touch it:
7
+ //
8
+ // - Atomicity. Only core/stores/code-sessions.js wrote through a temp file
9
+ // and renamed. Everywhere else, a crash or a full disk mid-write left a
10
+ // truncated file — and since the readers swallow parse errors, the next
11
+ // read silently returned "empty" instead of "corrupt". routines.json and
12
+ // the MCP stores were exposed to that.
13
+ // - Permissions. vars and mcp chmod 0600 because they hold tokens; the
14
+ // others did not, so the same kind of data could land world-readable
15
+ // depending on the writer.
16
+ // - Missing-file semantics. Some returned null, some {}, some threw.
17
+ //
18
+ // Reads are forgiving by design (a missing or corrupt file yields the caller's
19
+ // fallback), because every caller here treats "no data" as a valid state.
20
+ // Writes are strict: they create the directory, write atomically, and throw if
21
+ // they cannot.
22
+ import fs from "node:fs";
23
+ import fsp from "node:fs/promises";
24
+ import path from "node:path";
25
+
26
+ /** Mode for files that may contain tokens or credentials. */
27
+ export const SECRET_MODE = 0o600;
28
+
29
+ // Twelve call sites still parse inline, on purpose. They are the ones whose
30
+ // failure mode is not "fall back to empty":
31
+ //
32
+ // config/index.js a corrupt ~/.apx/config.json must THROW, loudly. Its
33
+ // silent fallback would be an empty config, i.e. every
34
+ // key reset — the worst possible recovery.
35
+ // mcp/sources.js, these report *which* file failed and why, so the user
36
+ // integrations/sources can fix it; a bare fallback would hide the problem.
37
+ // profiles/lifecycle, these read package.json / manifests where a parse
38
+ // project-config, … error is a real error, not a missing-file state.
39
+ //
40
+ // If you are adding a new read whose answer on failure is "treat it as empty",
41
+ // use readJson. If it is "tell the user", keep it explicit.
42
+
43
+ /**
44
+ * Read and parse a JSON file.
45
+ *
46
+ * @param {string} file
47
+ * @param {unknown} [fallback=null] returned when the file is missing, empty or
48
+ * unparseable. Pass `{}` or `[]` when the caller wants a shape it can spread.
49
+ */
50
+ export function readJson(file, fallback = null) {
51
+ let raw;
52
+ try {
53
+ raw = fs.readFileSync(file, "utf8");
54
+ } catch {
55
+ return fallback; // missing file is a normal state, not an error
56
+ }
57
+ if (!raw.trim()) return fallback;
58
+ try {
59
+ return JSON.parse(raw);
60
+ } catch {
61
+ return fallback;
62
+ }
63
+ }
64
+
65
+ /** Async variant, for request paths that must not block the event loop. */
66
+ export async function readJsonAsync(file, fallback = null) {
67
+ let raw;
68
+ try {
69
+ raw = await fsp.readFile(file, "utf8");
70
+ } catch {
71
+ return fallback;
72
+ }
73
+ if (!raw.trim()) return fallback;
74
+ try {
75
+ return JSON.parse(raw);
76
+ } catch {
77
+ return fallback;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Write a value as pretty JSON, atomically.
83
+ *
84
+ * Writes to `<file>.<pid>.tmp` and renames, so a reader never observes a
85
+ * half-written file and a crash cannot destroy the previous contents.
86
+ *
87
+ * @param {string} file
88
+ * @param {unknown} value
89
+ * @param {{ mode?: number }} [opts] `mode: SECRET_MODE` for files holding
90
+ * credentials.
91
+ */
92
+ export function writeJson(file, value, { mode } = {}) {
93
+ fs.mkdirSync(path.dirname(file), { recursive: true });
94
+ const tmp = `${file}.${process.pid}.tmp`;
95
+ try {
96
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
97
+ if (mode !== undefined) fs.chmodSync(tmp, mode);
98
+ fs.renameSync(tmp, file);
99
+ } catch (err) {
100
+ // Never leave the temp file behind on a failed write.
101
+ try { fs.unlinkSync(tmp); } catch { /* already gone */ }
102
+ throw err;
103
+ }
104
+ }
105
+
106
+ /** Async variant. Same atomic rename. */
107
+ export async function writeJsonAsync(file, value, { mode } = {}) {
108
+ await fsp.mkdir(path.dirname(file), { recursive: true });
109
+ const tmp = `${file}.${process.pid}.tmp`;
110
+ try {
111
+ await fsp.writeFile(tmp, JSON.stringify(value, null, 2) + "\n");
112
+ if (mode !== undefined) await fsp.chmod(tmp, mode);
113
+ await fsp.rename(tmp, file);
114
+ } catch (err) {
115
+ try { await fsp.unlink(tmp); } catch { /* already gone */ }
116
+ throw err;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Read, transform, write back — atomically, with the same fallback rules.
122
+ * Returns the value that was written.
123
+ */
124
+ export function updateJson(file, updater, { fallback = null, mode } = {}) {
125
+ const next = updater(readJson(file, fallback));
126
+ writeJson(file, next, { mode });
127
+ return next;
128
+ }
@@ -13,16 +13,16 @@
13
13
  //
14
14
  // project wins over global when the same name exists in both.
15
15
 
16
- import fs from "node:fs";
17
- import os from "node:os";
18
16
  import path from "node:path";
17
+ import { apxHome } from "#core/config/paths.js";
18
+ import { readJson, writeJson, SECRET_MODE } from "#core/util/json-file.js";
19
19
 
20
- const APX_HOME = path.join(os.homedir(), ".apx");
21
- const GLOBAL_VARS_FILE = path.join(APX_HOME, "vars.json");
20
+
21
+ const globalVarsFile = () => path.join(apxHome(), "vars.json");
22
22
  const PROJECT_VARS_FILENAME = "vars.json";
23
23
 
24
24
  export function globalVarsPath() {
25
- return GLOBAL_VARS_FILE;
25
+ return globalVarsFile();
26
26
  }
27
27
 
28
28
  export function projectVarsPath(storagePath) {
@@ -30,32 +30,25 @@ export function projectVarsPath(storagePath) {
30
30
  return path.join(storagePath, PROJECT_VARS_FILENAME);
31
31
  }
32
32
 
33
+ // A vars file must be a plain object; anything else is treated as empty.
33
34
  function readJsonSafe(absPath) {
34
- if (!absPath || !fs.existsSync(absPath)) return {};
35
- try {
36
- const json = JSON.parse(fs.readFileSync(absPath, "utf8"));
37
- return json && typeof json === "object" && !Array.isArray(json) ? json : {};
38
- } catch {
39
- return {};
40
- }
35
+ if (!absPath) return {};
36
+ const json = readJson(absPath, {});
37
+ return json && typeof json === "object" && !Array.isArray(json) ? json : {};
41
38
  }
42
39
 
40
+ // SECRET_MODE: these files hold tokens. The write is atomic, so a crash can no
41
+ // longer truncate the store.
43
42
  function writeJsonSecure(absPath, obj) {
44
- fs.mkdirSync(path.dirname(absPath), { recursive: true });
45
- fs.writeFileSync(absPath, JSON.stringify(obj, null, 2) + "\n");
46
- try {
47
- fs.chmodSync(absPath, 0o600);
48
- } catch {
49
- // Best effort on non-POSIX filesystems.
50
- }
43
+ writeJson(absPath, obj, { mode: SECRET_MODE });
51
44
  }
52
45
 
53
46
  export function readGlobalVars() {
54
- return readJsonSafe(GLOBAL_VARS_FILE);
47
+ return readJsonSafe(globalVarsFile());
55
48
  }
56
49
 
57
50
  export function writeGlobalVars(obj) {
58
- writeJsonSecure(GLOBAL_VARS_FILE, obj);
51
+ writeJsonSecure(globalVarsFile(), obj);
59
52
  }
60
53
 
61
54
  export function readProjectVars(storagePath) {
@@ -23,7 +23,7 @@ function repoCacheName(repoId) {
23
23
 
24
24
  function dirSizeBytes(dir) {
25
25
  let total = 0;
26
- let stack = [dir];
26
+ const stack = [dir];
27
27
  while (stack.length) {
28
28
  const d = stack.pop();
29
29
  let entries;
@@ -5,11 +5,12 @@
5
5
  //
6
6
  // All audio files land under ~/.apx/tmp/tts/<uuid>.<ext>. Callers are
7
7
  // responsible for cleaning up (Telegram plugin already uses similar temp
8
- // files via /telegram/send_voice).
8
+ // files via /api/telegram/send_voice).
9
9
 
10
10
  import fs from "node:fs";
11
- import path from "node:path";
12
- import os from "node:os";
11
+ import { TTS_TMP_DIR } from "#core/config/paths.js";
12
+
13
+ export { TTS_TMP_DIR };
13
14
  import { readConfig } from "../config/index.js";
14
15
  import {
15
16
  selectTtsEngine,
@@ -19,7 +20,7 @@ import {
19
20
  } from "./engines/index.js";
20
21
  import { emotionConfigFor, stripEmotionTags } from "./emotions.js";
21
22
 
22
- export const TTS_TMP_DIR = path.join(os.homedir(), ".apx", "tmp", "tts");
23
+
23
24
 
24
25
  export function ensureTtsTmpDir() {
25
26
  fs.mkdirSync(TTS_TMP_DIR, { recursive: true });
@@ -8,7 +8,7 @@
8
8
  import { readConfig, writeConfig } from "#core/config/index.js";
9
9
  import { resolveAgentName } from "#core/identity/index.js";
10
10
  import { setDottedKey, unsetDottedKey } from "../project-config.js";
11
- import { PERMISSION_MODES, DEFAULT_PERMISSION_MODE } from "#core/constants/permissions.js";
11
+ import { PERMISSION_MODES } from "#core/constants/permissions.js";
12
12
  import {
13
13
  redactConfig as redact,
14
14
  isSecretMarker,
@@ -16,8 +16,8 @@ import {
16
16
  } from "#core/config/redact.js";
17
17
  import { collectSecretValues, registerSecretValues } from "#core/config/secret-values.js";
18
18
 
19
- export function register(app, { config, scheduler, plugins }) {
20
- app.get("/admin/config", (_req, res) => {
19
+ export function register(api, { config, scheduler, plugins }) {
20
+ api.get("/admin/config", (_req, res) => {
21
21
  try {
22
22
  const fresh = readConfig();
23
23
  res.json({ config: redact(fresh) });
@@ -26,7 +26,7 @@ export function register(app, { config, scheduler, plugins }) {
26
26
  }
27
27
  });
28
28
 
29
- app.patch("/admin/config", (req, res) => {
29
+ api.patch("/admin/config", (req, res) => {
30
30
  const { set, unset } = req.body || {};
31
31
  let cfg;
32
32
  try {
@@ -68,7 +68,7 @@ export function register(app, { config, scheduler, plugins }) {
68
68
  });
69
69
 
70
70
  // Convenience: GET the resolved super_agent prompt (used by SettingsScreen)
71
- app.get("/admin/super-agent", (_req, res) => {
71
+ api.get("/admin/super-agent", (_req, res) => {
72
72
  try {
73
73
  const fresh = readConfig();
74
74
  const sa = fresh.super_agent || {};
@@ -8,11 +8,12 @@ import { execFile } from "node:child_process";
8
8
  import fs from "node:fs";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
+ import { LOG_DIR } from "#core/config/paths.js";
11
12
 
12
- export function register(app, { scheduler, plugins, config, registries }) {
13
+ export function register(api, { scheduler, plugins, config, registries }) {
13
14
  // Daemon logs: errors.jsonl (structured) or apx.log (plain), newest first.
14
- app.get("/admin/logs", (req, res) => {
15
- const dir = path.join(os.homedir(), ".apx", "logs");
15
+ api.get("/admin/logs", (req, res) => {
16
+ const dir = LOG_DIR;
16
17
  const which = req.query.file === "apx" ? "apx.log" : "errors.jsonl";
17
18
  const file = path.join(dir, which);
18
19
  const limit = Math.min(parseInt(req.query.limit, 10) || 200, 2000);
@@ -26,7 +27,7 @@ export function register(app, { scheduler, plugins, config, registries }) {
26
27
  res.json({ file: which, lines: tail });
27
28
  });
28
29
 
29
- app.post("/admin/reload", (_req, res) => {
30
+ api.post("/admin/reload", (_req, res) => {
30
31
  try {
31
32
  const fresh = readConfig();
32
33
  // Mutate in place so every closure that captured `config` sees the new
@@ -46,7 +47,7 @@ export function register(app, { scheduler, plugins, config, registries }) {
46
47
  }
47
48
  });
48
49
 
49
- app.post("/admin/shutdown", (_req, res) => {
50
+ api.post("/admin/shutdown", (_req, res) => {
50
51
  res.json({ ok: true });
51
52
  setTimeout(() => process.exit(0), 50);
52
53
  });
@@ -56,7 +57,7 @@ export function register(app, { scheduler, plugins, config, registries }) {
56
57
  // zenity (if present); Windows uses PowerShell's Shell.Application. If the
57
58
  // platform lacks a usable picker — or none is installed — the endpoint
58
59
  // returns 501 so the frontend can fall back to the inline directory list.
59
- app.get("/admin/fs/pick-dir", (req, res) => {
60
+ api.get("/admin/fs/pick-dir", (req, res) => {
60
61
  // The prompt is attacker-controllable (query string). NEVER interpolate it
61
62
  // into a shell string — use execFile (no shell) with an argv array, and
62
63
  // pass the prompt out-of-band via an env var so it can't break out of any
@@ -109,7 +110,7 @@ export function register(app, { scheduler, plugins, config, registries }) {
109
110
  });
110
111
  });
111
112
 
112
- app.get("/admin/fs/dirs", (req, res) => {
113
+ api.get("/admin/fs/dirs", (req, res) => {
113
114
  const requested = String(req.query.path || os.homedir());
114
115
  const base = path.resolve(requested.replace(/^~(?=$|\/)/, os.homedir()));
115
116
  try {
@@ -70,12 +70,12 @@ function normalizeAutonomy(v) {
70
70
  return AUTONOMY_VALUES.has(v) ? v : undefined;
71
71
  }
72
72
 
73
- export function register(app, { projects, project }) {
73
+ export function register(api, { projects, project }) {
74
74
  // Vault = global agent templates. Two-layer: bundled defaults shipped with
75
75
  // APX (assets/agent-vault-defaults/) + user overrides/new ones in
76
76
  // ~/.apx/agents/. The user layer wins per slug; tombstones in .removed.json
77
77
  // hide bundled entries. GET merges both with `source` set per item.
78
- app.get("/agents/vault", (req, res) => {
78
+ api.get("/agents/vault", (req, res) => {
79
79
  const includeRemoved = req.query?.include_removed === "1";
80
80
  res.json(readVaultAgents({ includeRemoved }).map((a) => ({
81
81
  ...agentToResponse(a),
@@ -84,7 +84,7 @@ export function register(app, { projects, project }) {
84
84
  });
85
85
 
86
86
  // Create or replace a vault template (user layer / copy-on-write).
87
- app.post("/agents/vault", (req, res) => {
87
+ api.post("/agents/vault", (req, res) => {
88
88
  const { slug, fields, body = "" } = req.body || {};
89
89
  if (!slug || !/^[a-z][a-z0-9_-]*$/.test(slug)) {
90
90
  return res.status(400).json({ error: "valid slug required" });
@@ -100,7 +100,7 @@ export function register(app, { projects, project }) {
100
100
 
101
101
  // Patch a vault template. If the slug is bundled-only, copy it to the user
102
102
  // layer first (the writer already does this), then apply the merged fields.
103
- app.patch("/agents/vault/:slug", (req, res) => {
103
+ api.patch("/agents/vault/:slug", (req, res) => {
104
104
  const { slug } = req.params;
105
105
  const current = readVaultAgent(slug);
106
106
  if (!current) return res.status(404).json({ error: `vault agent ${slug} not found` });
@@ -118,14 +118,14 @@ export function register(app, { projects, project }) {
118
118
 
119
119
  // Delete a vault template. Tombstones bundled slugs so they stay hidden;
120
120
  // deletes the user-layer file otherwise. POST .../restore lifts a tombstone.
121
- app.delete("/agents/vault/:slug", (req, res) => {
121
+ api.delete("/agents/vault/:slug", (req, res) => {
122
122
  const { slug } = req.params;
123
123
  const out = removeVaultAgent(slug);
124
124
  if (!out.removed) return res.status(404).json({ error: `vault agent ${slug} not found` });
125
125
  res.json({ ok: true, ...out });
126
126
  });
127
127
 
128
- app.post("/agents/vault/:slug/restore", (req, res) => {
128
+ api.post("/agents/vault/:slug/restore", (req, res) => {
129
129
  const { slug } = req.params;
130
130
  const out = restoreVaultAgent(slug);
131
131
  if (!out.restored) return res.status(404).json({ error: `slug ${slug} was not tombstoned` });
@@ -134,7 +134,7 @@ export function register(app, { projects, project }) {
134
134
  });
135
135
 
136
136
  // Import a vault template into a project (copies it to .apc/agents/<slug>.md).
137
- app.post("/projects/:pid/agents/import", (req, res) => {
137
+ api.post("/projects/:pid/agents/import", (req, res) => {
138
138
  const p = project(req, res);
139
139
  if (!p) return;
140
140
  const { slug } = req.body || {};
@@ -154,7 +154,7 @@ export function register(app, { projects, project }) {
154
154
  }
155
155
  });
156
156
 
157
- app.get("/projects/:pid/agents", (req, res) => {
157
+ api.get("/projects/:pid/agents", (req, res) => {
158
158
  const p = project(req, res);
159
159
  if (!p) return;
160
160
  const agents = readAgents(p.path).map(agentToResponse);
@@ -162,7 +162,7 @@ export function register(app, { projects, project }) {
162
162
  res.json(agents);
163
163
  });
164
164
 
165
- app.get("/projects/:pid/agents/:slug", (req, res) => {
165
+ api.get("/projects/:pid/agents/:slug", (req, res) => {
166
166
  const p = project(req, res);
167
167
  if (!p) return;
168
168
  const agents = readAgents(p.path);
@@ -172,7 +172,7 @@ export function register(app, { projects, project }) {
172
172
  res.json({ ...agentToResponse(a), memory, system: a.body || "" });
173
173
  });
174
174
 
175
- app.post("/projects/:pid/agents", (req, res) => {
175
+ api.post("/projects/:pid/agents", (req, res) => {
176
176
  const p = project(req, res);
177
177
  if (!p) return;
178
178
  const {
@@ -213,7 +213,7 @@ export function register(app, { projects, project }) {
213
213
 
214
214
  // Edit an existing agent. Merges provided fields into the AGENT.md
215
215
  // frontmatter; `system` rewrites the body (the agent's system prompt).
216
- app.patch("/projects/:pid/agents/:slug", (req, res) => {
216
+ api.patch("/projects/:pid/agents/:slug", (req, res) => {
217
217
  const p = project(req, res);
218
218
  if (!p) return;
219
219
  const slug = req.params.slug;
@@ -255,7 +255,7 @@ export function register(app, { projects, project }) {
255
255
  });
256
256
 
257
257
  // Delete an agent: removes .apc/agents/<slug>.md and runtime data dir.
258
- app.delete("/projects/:pid/agents/:slug", (req, res) => {
258
+ api.delete("/projects/:pid/agents/:slug", (req, res) => {
259
259
  const p = project(req, res);
260
260
  if (!p) return;
261
261
  const slug = req.params.slug;
@@ -276,7 +276,7 @@ export function register(app, { projects, project }) {
276
276
  });
277
277
 
278
278
  // ---- Project-level memory (.apc/memory.md) ----
279
- app.get("/projects/:pid/memory", (req, res) => {
279
+ api.get("/projects/:pid/memory", (req, res) => {
280
280
  const p = project(req, res);
281
281
  if (!p) return;
282
282
  const memPath = apcMemoryFile(p.path);
@@ -284,7 +284,7 @@ export function register(app, { projects, project }) {
284
284
  res.json({ body, path: memPath });
285
285
  });
286
286
 
287
- app.put("/projects/:pid/memory", (req, res) => {
287
+ api.put("/projects/:pid/memory", (req, res) => {
288
288
  const p = project(req, res);
289
289
  if (!p) return;
290
290
  const { body } = req.body || {};
@@ -298,7 +298,7 @@ export function register(app, { projects, project }) {
298
298
  });
299
299
 
300
300
  // ---- Per-agent memory ----
301
- app.get("/projects/:pid/agents/:slug/memory", (req, res) => {
301
+ api.get("/projects/:pid/agents/:slug/memory", (req, res) => {
302
302
  const p = project(req, res);
303
303
  if (!p) return;
304
304
  // Validate the agent exists — otherwise an unknown slug returned 200 with an
@@ -308,7 +308,7 @@ export function register(app, { projects, project }) {
308
308
  res.json({ body: readAgentMemory(p, req.params.slug) });
309
309
  });
310
310
 
311
- app.put("/projects/:pid/agents/:slug/memory", (req, res) => {
311
+ api.put("/projects/:pid/agents/:slug/memory", (req, res) => {
312
312
  const p = project(req, res);
313
313
  if (!p) return;
314
314
  if (!readAgents(p.path).some((a) => a.slug === req.params.slug))
@@ -12,9 +12,9 @@
12
12
  import { previews } from "#core/artifacts/preview.js";
13
13
  import { tunnels, detectProviders } from "#core/artifacts/tunnel.js";
14
14
 
15
- export function register(app, { project }) {
15
+ export function register(api, { project }) {
16
16
  // Start (or reuse) an ephemeral preview server for an artifact.
17
- app.post("/projects/:pid/artifacts/:name/preview", async (req, res) => {
17
+ api.post("/projects/:pid/artifacts/:name/preview", async (req, res) => {
18
18
  const p = project(req, res);
19
19
  if (!p) return;
20
20
  const name = decodeURIComponent(req.params.name);
@@ -33,24 +33,24 @@ export function register(app, { project }) {
33
33
  });
34
34
 
35
35
  // List preview servers scoped to a project.
36
- app.get("/projects/:pid/previews", (req, res) => {
36
+ api.get("/projects/:pid/previews", (req, res) => {
37
37
  const p = project(req, res);
38
38
  if (!p) return;
39
39
  res.json(previews.list(p.id));
40
40
  });
41
41
 
42
42
  // List every live preview (all projects).
43
- app.get("/previews", (_req, res) => {
43
+ api.get("/previews", (_req, res) => {
44
44
  res.json(previews.list());
45
45
  });
46
46
 
47
47
  // Which tunnel providers this host can use, best first.
48
- app.get("/previews/tunnel-providers", (_req, res) => {
48
+ api.get("/previews/tunnel-providers", (_req, res) => {
49
49
  res.json({ providers: detectProviders() });
50
50
  });
51
51
 
52
52
  // Stop a preview server (also closes its tunnel).
53
- app.delete("/previews/:id", async (req, res) => {
53
+ api.delete("/previews/:id", async (req, res) => {
54
54
  const rec = previews.get(req.params.id);
55
55
  if (rec?.tunnel) tunnels.close(rec.tunnel.id);
56
56
  const ok = await previews.stop(req.params.id);
@@ -58,7 +58,7 @@ export function register(app, { project }) {
58
58
  });
59
59
 
60
60
  // Open a public tunnel to a preview's local port.
61
- app.post("/previews/:id/tunnel", async (req, res) => {
61
+ api.post("/previews/:id/tunnel", async (req, res) => {
62
62
  const rec = previews.get(req.params.id);
63
63
  if (!rec) return res.status(404).json({ error: "preview not found" });
64
64
  if (rec.tunnel) return res.json(rec.tunnel); // already shared
@@ -72,7 +72,7 @@ export function register(app, { project }) {
72
72
  });
73
73
 
74
74
  // Close a preview's tunnel but keep the local server running.
75
- app.delete("/previews/:id/tunnel", (req, res) => {
75
+ api.delete("/previews/:id/tunnel", (req, res) => {
76
76
  const rec = previews.get(req.params.id);
77
77
  if (!rec || !rec.tunnel) return res.status(404).end();
78
78
  tunnels.close(rec.tunnel.id);