@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
@@ -1,6 +1,13 @@
1
+ // Changes the super-agent's own autonomy level. This is the gate that decides
2
+ // whether destructive tools run unattended, so the mode vocabulary must come
3
+ // from one place: it used to be spelled out three times in this file (a Set,
4
+ // the schema enum, and the error message), which meant adding or renaming a
5
+ // mode silently left two of them stale.
1
6
  import { readConfig, writeConfig } from "#core/config/index.js";
7
+ import { PERMISSION_MODES } from "#core/constants/permissions.js";
2
8
 
3
- const MODES = new Set(["total", "automatico", "permiso"]);
9
+ const MODES = Object.values(PERMISSION_MODES);
10
+ const MODE_LIST = MODES.join(", ");
4
11
 
5
12
  export default {
6
13
  name: "set_permission_mode",
@@ -8,11 +15,11 @@ export default {
8
15
  type: "function",
9
16
  function: {
10
17
  name: "set_permission_mode",
11
- description: "Set APX tool permission mode in ~/.apx/config.json. Modes: total, automatico, permiso.",
18
+ description: `Set APX tool permission mode in ~/.apx/config.json. Modes: ${MODE_LIST}.`,
12
19
  parameters: {
13
20
  type: "object",
14
21
  properties: {
15
- mode: { type: "string", enum: ["total", "automatico", "permiso"] },
22
+ mode: { type: "string", enum: MODES },
16
23
  },
17
24
  required: ["mode"],
18
25
  },
@@ -20,7 +27,7 @@ export default {
20
27
  },
21
28
  makeHandler: ({ requirePermission }) => async ({ mode, confirmed = false }) => {
22
29
  await requirePermission("set_permission_mode", { dangerous: true, confirmed, args: { mode } });
23
- if (!MODES.has(mode)) throw new Error("mode must be total, automatico, or permiso");
30
+ if (!MODES.includes(mode)) throw new Error(`mode must be one of: ${MODE_LIST}`);
24
31
  const cfg = readConfig();
25
32
  cfg.super_agent = cfg.super_agent || {};
26
33
  cfg.super_agent.permission_mode = mode;
@@ -44,6 +44,10 @@ export const TOOLS = Object.freeze({
44
44
 
45
45
  // Interaction
46
46
  ASK_QUESTIONS: "ask_questions",
47
+ // Synthesised by run-agent.js rather than living in handlers/ — it is how the
48
+ // model declares the turn is over. It was missing from this catalog, so
49
+ // security.js referenced it as a bare "finish" literal.
50
+ FINISH: "finish",
47
51
 
48
52
  // Delegation / external
49
53
  CALL_AGENT: "call_agent",
@@ -195,3 +199,57 @@ export const CODE_PLAN_TOOLS = Object.freeze([
195
199
  * "*" as "expose every tool the channel is otherwise allowed to see".
196
200
  */
197
201
  export const CODE_BUILD_TOOLS = "*";
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Behavioural tool sets
205
+ //
206
+ // These drive real user-facing protections, so they belong next to the names
207
+ // they reference rather than as string literals scattered across the loop. The
208
+ // side-effect set in particular was an inline `new Set([...])` inside
209
+ // runAgent(): rename a tool there and the de-duplication below silently stops
210
+ // working, with the only symptom being a user receiving the same Telegram
211
+ // message three times. tests/tool-name-sets.test.js fails if any member here
212
+ // stops being a real tool.
213
+ // ---------------------------------------------------------------------------
214
+
215
+ /**
216
+ * Tools that mutate the world. Weak models (Gemini especially) re-emit the
217
+ * same call across iterations; for these we remember the (name + args)
218
+ * signature and answer duplicates with a synthetic "already done" instead of
219
+ * running them again. Read-only tools are exempt — they are idempotent and are
220
+ * legitimately repeated (list_tasks before and after a change).
221
+ */
222
+ export const SIDE_EFFECT_TOOLS = new Set([
223
+ TOOLS.SEND_TELEGRAM,
224
+ TOOLS.CREATE_TASK,
225
+ TOOLS.WRITE_FILE,
226
+ TOOLS.EDIT_FILE,
227
+ TOOLS.RUN_SHELL,
228
+ TOOLS.CALL_RUNTIME,
229
+ TOOLS.ADD_PROJECT,
230
+ TOOLS.SET_IDENTITY,
231
+ ]);
232
+
233
+ /**
234
+ * Tools that only acknowledge — the turn has produced no new information for
235
+ * the user, so a run of them is capped (MAX_CONSECUTIVE_ACKS).
236
+ */
237
+ export const ACK_ONLY_TOOLS = new Set([TOOLS.SEND_TELEGRAM]);
238
+
239
+ /**
240
+ * Tools whose semantics REQUIRE handing control back to a human. The loop
241
+ * breaks after these even under a completion contract, because the task
242
+ * cannot advance without a reply. Without it, models under forced toolChoice
243
+ * ask the same question every iteration.
244
+ */
245
+ export const TURN_ENDING_TOOLS = new Set([TOOLS.ASK_QUESTIONS]);
246
+
247
+ /**
248
+ * Tools exempt from the injected `security_risk` parameter: they cannot touch
249
+ * anything, so grading them is noise the model has to pay for on every call.
250
+ */
251
+ export const RISK_EXEMPT_TOOLS = new Set([
252
+ TOOLS.FINISH,
253
+ TOOLS.ASK_QUESTIONS,
254
+ TOOLS.DISCOVER_TOOLS,
255
+ ]);
@@ -1,5 +1,5 @@
1
1
  // Snapshot a project's read-aloud context — AGENTS.md + .apc/memory.md —
2
- // into a single labelled markdown string. Used by /deck/context/copy and
2
+ // into a single labelled markdown string. Used by /api/deck/context/copy and
3
3
  // (potentially) any other "give me the context" surface.
4
4
  import fs from "node:fs/promises";
5
5
  import { AGENTS_MD, apcMemoryFile } from "./paths.js";
@@ -0,0 +1,85 @@
1
+ // YAML-ish frontmatter — one parser.
2
+ //
3
+ // Four copies existed, and they did not agree on which keys are legal:
4
+ //
5
+ // apc/parser.js /^([a-zA-Z_-]+):/ dashes, no digits
6
+ // agent/skills/loader /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:/ digits and dashes, strips quotes
7
+ // core/sessions /^([a-zA-Z_]+):/ neither
8
+ // cli/commands/session /^([a-zA-Z_]+):/ neither
9
+ //
10
+ // That is not cosmetic duplication: a key like `route_to_agent2` or
11
+ // `agent-slug` parsed in some code paths and vanished in others, so the same
12
+ // file read by the CLI and by the daemon yielded different data — with no
13
+ // error anywhere, because a non-matching line is simply skipped.
14
+ //
15
+ // This takes the most permissive correct behaviour (identifiers may contain
16
+ // digits and dashes, quoted values are unquoted) and returns everything the
17
+ // callers between them needed: the fields, the body, and where the body starts.
18
+ //
19
+ // Deliberately not a YAML library: the frontmatter APX writes is flat
20
+ // `key: value` and staying strict about that keeps the format predictable for
21
+ // the models that also read these files.
22
+
23
+ const KEY_VALUE = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/;
24
+
25
+ function unquote(value) {
26
+ const v = value.trim();
27
+ if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
28
+ return v.slice(1, -1);
29
+ }
30
+ return v;
31
+ }
32
+
33
+ /**
34
+ * Parse leading `---` frontmatter.
35
+ *
36
+ * @param {string} text
37
+ * @returns {{ fm: Record<string,string>, body: string, bodyStart: number }}
38
+ * `fm` is `{}` when there is no frontmatter; `body` is then the whole text
39
+ * and `bodyStart` is 0.
40
+ */
41
+ export function parseFrontmatter(text) {
42
+ const raw = String(text ?? "");
43
+ // Accept both "---\n" and a bare leading "---" (the skills loader allowed
44
+ // the latter; session files always write the former).
45
+ if (!raw.startsWith("---")) return { fm: {}, body: raw, bodyStart: 0 };
46
+
47
+ const openLen = raw.startsWith("---\n") ? 4 : 3;
48
+ const end = raw.indexOf("\n---", openLen - 1);
49
+ if (end === -1) return { fm: {}, body: raw, bodyStart: 0 };
50
+
51
+ const fm = {};
52
+ for (const line of raw.slice(openLen, end).split("\n")) {
53
+ const m = line.match(KEY_VALUE);
54
+ if (m) fm[m[1]] = unquote(m[2]);
55
+ }
56
+
57
+ // The closing fence is followed by a newline, and authors conventionally
58
+ // leave a blank line after it — consume both so `body` starts at content.
59
+ const bodyStart = end + 4;
60
+ return { fm, body: raw.slice(bodyStart).replace(/^\n+/, ""), bodyStart };
61
+ }
62
+
63
+ /** Just the fields, for callers that don't need the body. */
64
+ export function parseFrontmatterFields(text) {
65
+ return parseFrontmatter(text).fm;
66
+ }
67
+
68
+ /**
69
+ * Set or replace one field in place, preserving the rest of the file byte for
70
+ * byte. Returns the text unchanged when there is no frontmatter to edit.
71
+ */
72
+ export function setFrontmatterField(text, field, value) {
73
+ const raw = String(text ?? "");
74
+ if (!raw.startsWith("---\n")) return raw;
75
+ const end = raw.indexOf("\n---", 4);
76
+ if (end === -1) return raw;
77
+
78
+ const lines = raw.slice(4, end).split("\n");
79
+ const idx = lines.findIndex((l) => l.match(KEY_VALUE)?.[1] === field);
80
+ const entry = `${field}: ${value}`;
81
+ if (idx >= 0) lines[idx] = entry;
82
+ else lines.push(entry);
83
+
84
+ return `---\n${lines.join("\n")}${raw.slice(end)}`;
85
+ }
@@ -1,6 +1,7 @@
1
1
  // Core parsers for APC — pure ESM, no deps.
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { AGENT_VAULT_DIR } from "../config/paths.js";
4
5
  import {
5
6
  apcAgentsDir,
6
7
  apcAgentFile,
@@ -124,12 +125,13 @@ export function readAgentsFromDir(root) {
124
125
  // if it's a bundled slug, deletes the user file otherwise.
125
126
  // ---------------------------------------------------------------------------
126
127
 
127
- import os from "node:os";
128
128
  import { fileURLToPath } from "node:url";
129
+ import { parseFrontmatterFields } from "./frontmatter.js";
130
+ import { readJson } from "#core/util/json-file.js";
129
131
 
130
132
  const __parserDir = path.dirname(fileURLToPath(import.meta.url));
131
133
 
132
- export const VAULT_DIR = path.join(os.homedir(), ".apx", "agents");
134
+ export const VAULT_DIR = AGENT_VAULT_DIR;
133
135
  export const BUNDLED_VAULT_DIR = path.resolve(__parserDir, "../../../assets/agent-vault-defaults");
134
136
  export const VAULT_TOMBSTONE_PATH = path.join(VAULT_DIR, ".removed.json");
135
137
 
@@ -144,10 +146,8 @@ function readVaultDirRaw(dir) {
144
146
 
145
147
  export function readVaultTombstones() {
146
148
  if (!fs.existsSync(VAULT_TOMBSTONE_PATH)) return new Set();
147
- try {
148
- const raw = JSON.parse(fs.readFileSync(VAULT_TOMBSTONE_PATH, "utf8"));
149
- return new Set(Array.isArray(raw.slugs) ? raw.slugs : []);
150
- } catch { return new Set(); }
149
+ const raw = readJson(VAULT_TOMBSTONE_PATH, {});
150
+ return new Set(Array.isArray(raw?.slugs) ? raw.slugs : []);
151
151
  }
152
152
 
153
153
  export function writeVaultTombstones(slugs) {
@@ -212,10 +212,7 @@ export { readVaultAgent };
212
212
  export function importedVaultSlugs(root) {
213
213
  const p = apcProjectFile(root);
214
214
  if (!fs.existsSync(p)) return [];
215
- try {
216
- const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
217
- return cfg.agents?.imported ?? [];
218
- } catch { return []; }
215
+ return readJson(p, {})?.agents?.imported ?? [];
219
216
  }
220
217
 
221
218
  // Primary entry point.
@@ -273,14 +270,5 @@ export function findApfRoot(start = process.cwd()) {
273
270
  // Session / conversation frontmatter
274
271
  // ---------------------------------------------------------------------------
275
272
 
276
- export function parseSessionFrontmatter(text) {
277
- if (!text.startsWith("---\n")) return {};
278
- const end = text.indexOf("\n---", 4);
279
- if (end === -1) return {};
280
- const out = {};
281
- for (const line of text.slice(4, end).split("\n")) {
282
- const m = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
283
- if (m) out[m[1]] = m[2].trim();
284
- }
285
- return out;
286
- }
273
+ // Kept as the public name; the implementation is the shared parser.
274
+ export const parseSessionFrontmatter = parseFrontmatterFields;
@@ -9,16 +9,16 @@ import fs from "node:fs";
9
9
  import path from "node:path";
10
10
 
11
11
  // Raw names — exported for the rare caller that needs to glob/match by name.
12
- export const APC_DIR = ".apc";
13
- export const APC_PROJECT_FILE = "project.json";
14
- export const APC_PROJECT_CONFIG_FILE = "config.json";
15
- export const APC_PROJECT_MEMORY_FILE = "memory.md";
16
- export const APC_AGENTS_DIR = "agents";
17
- export const APC_SKILLS_DIR = "skills";
12
+ const APC_DIR = ".apc";
13
+ const APC_PROJECT_FILE = "project.json";
14
+ const APC_PROJECT_CONFIG_FILE = "config.json";
15
+ const APC_PROJECT_MEMORY_FILE = "memory.md";
16
+ const APC_AGENTS_DIR = "agents";
17
+ const APC_SKILLS_DIR = "skills";
18
18
  export const APC_COMMANDS_DIR = "commands";
19
19
  export const APC_NOTES_DIR = "notes";
20
- export const APC_MCPS_FILE = "mcps.json";
21
- export const APC_ORGANIZATION_FILE = "organization.json";
20
+ const APC_MCPS_FILE = "mcps.json";
21
+ const APC_ORGANIZATION_FILE = "organization.json";
22
22
  export const APC_REMOVED_FILE = ".removed.json";
23
23
  export const AGENTS_MD = "AGENTS.md";
24
24
 
@@ -18,6 +18,7 @@ import {
18
18
  apcAgentFile,
19
19
  agentsMdFile,
20
20
  } from "./paths.js";
21
+ import { readJson } from "#core/util/json-file.js";
21
22
 
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
24
  // Now under src/core/apc/ — one more "../" to escape than before.
@@ -431,8 +432,8 @@ function writeMigrateMd(apfDir, found) {
431
432
  export function getOrCreateApxId(root) {
432
433
  const p = apcProjectFile(root);
433
434
  if (!fs.existsSync(p)) return null;
434
- let cfg;
435
- try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
435
+ const cfg = readJson(p, null);
436
+ if (!cfg) return null;
436
437
  if (cfg.apx_id) return cfg.apx_id;
437
438
  const apxId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
438
439
  console.log(`[apx] Generating new stable ID ${apxId} for project at ${root}`);
@@ -490,7 +491,7 @@ export function initApf(directory, { name } = {}) {
490
491
  return { root, agentsMd, projectJson, pendingMigration: scattered };
491
492
  }
492
493
 
493
- export function ensureAgentDir(root, slug) {
494
+ export function ensureAgentDir(root, _slug) {
494
495
  fs.mkdirSync(apcAgentsDir(root), { recursive: true });
495
496
  return apcAgentsDir(root);
496
497
  }
@@ -587,7 +588,7 @@ export function restoreVaultAgent(slug) {
587
588
  export function addImportedAgent(root, slug) {
588
589
  const p = apcProjectFile(root);
589
590
  let cfg = {};
590
- try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
591
+ cfg = readJson(p, {});
591
592
  if (!cfg.agents) cfg.agents = {};
592
593
  if (!cfg.agents.imported) cfg.agents.imported = [];
593
594
  if (!cfg.agents.imported.includes(slug)) cfg.agents.imported.push(slug);
@@ -0,0 +1,201 @@
1
+ // Telegram identity: channels, contacts and roles.
2
+ //
3
+ // This is channel domain, not configuration plumbing — it happened to live in
4
+ // core/config/index.js because the data is stored in the global config file.
5
+ // That made the config module 711 lines, half of them Telegram, and forced
6
+ // host/daemon/api/telegram.js to import 13 symbols from "the config module".
7
+ //
8
+ // The shapes:
9
+ // cfg.telegram.channels[] — { name, bot_token, chat_id, route_to_agent,
10
+ // project, respond_with_engine, poll_interval_ms, owner_user_id }
11
+ // cfg.telegram.contacts[] — the global roster, keyed by Telegram user_id.
12
+ // Identity is per-person and the same across every chat; a channel's
13
+ // owner_user_id only marks who owns *that* channel.
14
+ // cfg.telegram.roles — role -> capabilities. "owner" is implicit (full),
15
+ // "guest" is the default for unknown senders (no tools).
16
+ //
17
+ // Re-exported from core/config/index.js so existing import sites keep working.
18
+ import { writeConfig } from "../../config/index.js";
19
+
20
+ // ── Telegram channels (multi-channel mode) ──────────────────────────────────
21
+ // Each entry in cfg.telegram.channels[] is { name, bot_token, chat_id,
22
+ // route_to_agent, project, respond_with_engine, poll_interval_ms }.
23
+ // These helpers keep the array shape stable for the CLI and the daemon API.
24
+
25
+ const CHANNEL_FIELDS = [
26
+ "name",
27
+ "bot_token",
28
+ "chat_id",
29
+ "route_to_agent",
30
+ "project",
31
+ "respond_with_engine",
32
+ "poll_interval_ms",
33
+ "owner_user_id",
34
+ ];
35
+
36
+ function ensureChannelsArray(cfg) {
37
+ cfg.telegram = cfg.telegram || {};
38
+ if (!Array.isArray(cfg.telegram.channels)) cfg.telegram.channels = [];
39
+ return cfg.telegram.channels;
40
+ }
41
+
42
+ export function listTelegramChannels(cfg) {
43
+ return ensureChannelsArray(cfg).slice();
44
+ }
45
+
46
+ export function findTelegramChannel(cfg, name) {
47
+ return ensureChannelsArray(cfg).find((c) => c.name === name) || null;
48
+ }
49
+
50
+ // Create-or-patch a channel by name. `patch` is a partial channel object;
51
+ // unknown keys are dropped. Returns { created, channel }.
52
+ export function upsertTelegramChannel(cfg, name, patch = {}) {
53
+ if (!name || typeof name !== "string")
54
+ throw new Error("upsertTelegramChannel: name required");
55
+ const channels = ensureChannelsArray(cfg);
56
+ let entry = channels.find((c) => c.name === name);
57
+ const created = !entry;
58
+ if (!entry) {
59
+ entry = { name };
60
+ channels.push(entry);
61
+ }
62
+ for (const k of CHANNEL_FIELDS) {
63
+ if (k === "name") continue;
64
+ if (patch[k] !== undefined) entry[k] = patch[k];
65
+ }
66
+ // Default respond_with_engine to true on create.
67
+ if (created && entry.respond_with_engine === undefined) {
68
+ entry.respond_with_engine = true;
69
+ }
70
+ writeConfig(cfg);
71
+ return { created, channel: entry };
72
+ }
73
+
74
+ export function removeTelegramChannel(cfg, name) {
75
+ const channels = ensureChannelsArray(cfg);
76
+ const before = channels.length;
77
+ cfg.telegram.channels = channels.filter((c) => c.name !== name);
78
+ const removed = before - cfg.telegram.channels.length;
79
+ if (removed > 0) {
80
+ // Explicit user-initiated removal — bypass the credential-loss guard
81
+ // so the write goes through even when this empties the array.
82
+ cfg._allowClear = true;
83
+ writeConfig(cfg);
84
+ }
85
+ return { removed };
86
+ }
87
+
88
+ // Clear specific optional fields on a channel (project, route_to_agent, …).
89
+ // Returns { channel } or null when no such channel.
90
+ export function unsetTelegramChannelFields(cfg, name, fields = []) {
91
+ const ch = findTelegramChannel(cfg, name);
92
+ if (!ch) return null;
93
+ let mutated = false;
94
+ for (const f of fields) {
95
+ if (!CHANNEL_FIELDS.includes(f) || f === "name") continue;
96
+ if (f in ch) {
97
+ delete ch[f];
98
+ mutated = true;
99
+ }
100
+ }
101
+ if (mutated) writeConfig(cfg);
102
+ return { channel: ch };
103
+ }
104
+
105
+ // ── Telegram contacts (global roster, keyed by user_id) ─────────────────────
106
+ // Identity of a person is global; the per-channel owner_user_id only marks who
107
+ // owns a given channel. Role lives on the contact (global), per the chosen
108
+ // design — owner_user_id overrides it to "owner" for that channel only.
109
+
110
+ const CONTACT_FIELDS = [
111
+ "user_id",
112
+ "name",
113
+ "username",
114
+ "role",
115
+ "note",
116
+ "first_seen",
117
+ "last_seen",
118
+ ];
119
+
120
+ function ensureContactsArray(cfg) {
121
+ cfg.telegram = cfg.telegram || {};
122
+ if (!Array.isArray(cfg.telegram.contacts)) cfg.telegram.contacts = [];
123
+ return cfg.telegram.contacts;
124
+ }
125
+
126
+ export function listContacts(cfg) {
127
+ return ensureContactsArray(cfg).slice();
128
+ }
129
+
130
+ export function findContact(cfg, userId) {
131
+ if (userId == null) return null;
132
+ return (
133
+ ensureContactsArray(cfg).find((c) => String(c.user_id) === String(userId)) ||
134
+ null
135
+ );
136
+ }
137
+
138
+ // Create-or-patch a contact by user_id. Unknown keys are dropped.
139
+ export function upsertContact(cfg, userId, patch = {}, { persist = true } = {}) {
140
+ if (userId == null) throw new Error("upsertContact: user_id required");
141
+ const contacts = ensureContactsArray(cfg);
142
+ let entry = contacts.find((c) => String(c.user_id) === String(userId));
143
+ const created = !entry;
144
+ if (!entry) {
145
+ entry = { user_id: userId };
146
+ contacts.push(entry);
147
+ }
148
+ for (const k of CONTACT_FIELDS) {
149
+ if (k === "user_id") continue;
150
+ if (patch[k] !== undefined) entry[k] = patch[k];
151
+ }
152
+ if (persist) writeConfig(cfg);
153
+ return { created, contact: entry };
154
+ }
155
+
156
+ export function setContactRole(cfg, userId, role) {
157
+ const { contact } = upsertContact(cfg, userId, { role });
158
+ return contact;
159
+ }
160
+
161
+ export function removeContact(cfg, userId) {
162
+ const contacts = ensureContactsArray(cfg);
163
+ const before = contacts.length;
164
+ cfg.telegram.contacts = contacts.filter(
165
+ (c) => String(c.user_id) !== String(userId)
166
+ );
167
+ const removed = before - cfg.telegram.contacts.length;
168
+ if (removed > 0) writeConfig(cfg);
169
+ return { removed };
170
+ }
171
+
172
+ export function setChannelOwner(cfg, channelName, userId) {
173
+ return upsertTelegramChannel(cfg, channelName, { owner_user_id: userId });
174
+ }
175
+
176
+ // ── Telegram roles (role → capability map) ──────────────────────────────────
177
+
178
+ export function listRoles(cfg) {
179
+ cfg.telegram = cfg.telegram || {};
180
+ return { ...(cfg.telegram.roles || {}) };
181
+ }
182
+
183
+ export function setRole(cfg, name, def) {
184
+ if (!name || typeof name !== "string") throw new Error("setRole: name required");
185
+ cfg.telegram = cfg.telegram || {};
186
+ cfg.telegram.roles = cfg.telegram.roles || {};
187
+ cfg.telegram.roles[name] = def;
188
+ writeConfig(cfg);
189
+ return cfg.telegram.roles[name];
190
+ }
191
+
192
+ export function removeRole(cfg, name) {
193
+ cfg.telegram = cfg.telegram || {};
194
+ if (!cfg.telegram.roles || !(name in cfg.telegram.roles)) return { removed: 0 };
195
+ if (name === "owner" || name === "guest") {
196
+ throw new Error(`role "${name}" is built-in and cannot be removed`);
197
+ }
198
+ delete cfg.telegram.roles[name];
199
+ writeConfig(cfg);
200
+ return { removed: 1 };
201
+ }