@agentprojectcontext/apx 1.77.3 → 1.79.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 (356) 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 +43 -53
  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/tool-summary.js +65 -0
  15. package/src/core/agent/tools/handlers/call-runtime.js +4 -4
  16. package/src/core/agent/tools/handlers/list-commitments.js +80 -0
  17. package/src/core/agent/tools/handlers/list-tasks.js +66 -27
  18. package/src/core/agent/tools/handlers/record-commitment.js +68 -0
  19. package/src/core/agent/tools/handlers/search-sessions.js +1 -1
  20. package/src/core/agent/tools/handlers/send-telegram.js +68 -2
  21. package/src/core/agent/tools/handlers/set-permission-mode.js +11 -4
  22. package/src/core/agent/tools/names.js +64 -0
  23. package/src/core/agent/tools/registry.js +9 -0
  24. package/src/core/agent/tools/tool-call-parser.js +70 -1
  25. package/src/core/apc/context-copy.js +1 -1
  26. package/src/core/apc/frontmatter.js +85 -0
  27. package/src/core/apc/parser.js +9 -21
  28. package/src/core/apc/paths.js +8 -8
  29. package/src/core/apc/scaffold.js +5 -4
  30. package/src/core/channels/telegram/ask-callbacks.js +35 -0
  31. package/src/core/channels/telegram/config.js +201 -0
  32. package/src/core/channels/telegram/dispatch.js +3 -0
  33. package/src/core/channels/telegram/reply.js +30 -5
  34. package/src/core/config/index.js +25 -183
  35. package/src/core/config/paths.js +57 -1
  36. package/src/core/config/redact.js +22 -0
  37. package/src/core/confirmation/adapters/web.js +1 -1
  38. package/src/core/daemon/service.js +238 -0
  39. package/src/core/deck/manifest.js +12 -12
  40. package/src/core/desktop/autostart.js +3 -2
  41. package/src/core/desktop/process.js +4 -4
  42. package/src/core/engines/gemini.js +322 -60
  43. package/src/core/engines/openai-compatible.js +21 -2
  44. package/src/core/engines/presets.js +2 -2
  45. package/src/core/http-tools/browser.js +1 -1
  46. package/src/core/http-tools/catalog.js +551 -0
  47. package/src/core/http-tools/fetch.js +1 -1
  48. package/src/core/http-tools/glob.js +3 -3
  49. package/src/core/http-tools/grep.js +2 -9
  50. package/src/core/http-tools/inline-handlers.js +59 -0
  51. package/src/core/http-tools/registry.js +31 -615
  52. package/src/core/http-tools/search.js +2 -2
  53. package/src/core/identity/self.js +6 -7
  54. package/src/core/identity/telegram.js +3 -3
  55. package/src/core/logging.js +6 -5
  56. package/src/core/mcp/sources.js +12 -16
  57. package/src/core/memory/consolidate.js +225 -0
  58. package/src/core/memory/indexer.js +2 -5
  59. package/src/core/nudge/index.js +192 -0
  60. package/src/core/nudge/policy.js +143 -0
  61. package/src/core/nudge/store.js +141 -0
  62. package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
  63. package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
  64. package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
  65. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
  66. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
  67. package/src/core/profiles/lifecycle.js +0 -2
  68. package/src/core/profiles/store.js +2 -7
  69. package/src/core/routines/runner.js +103 -4
  70. package/src/core/routines/signals.js +270 -0
  71. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
  72. package/src/core/runtime-skills/apx-profile/SKILL.md +3 -3
  73. package/src/core/runtime-skills/apx-project/SKILL.md +1 -1
  74. package/src/core/runtime-skills/apx-task/SKILL.md +8 -8
  75. package/src/core/runtime-skills/apx-telegram/SKILL.md +4 -4
  76. package/src/core/runtime-skills/apx-voice/SKILL.md +4 -4
  77. package/src/core/runtimes/detect.js +2 -2
  78. package/src/core/sessions/index.js +800 -0
  79. package/src/core/stores/code-sessions.js +3 -14
  80. package/src/core/stores/commitments.js +331 -0
  81. package/src/core/stores/engine-sessions.js +0 -4
  82. package/src/core/stores/messages.js +4 -5
  83. package/src/core/stores/organization.js +9 -11
  84. package/src/core/stores/routines.js +23 -14
  85. package/src/core/stores/runtime-callbacks.js +2 -1
  86. package/src/core/util/json-file.js +128 -0
  87. package/src/core/util/thinking.js +51 -0
  88. package/src/core/vars/sources.js +14 -21
  89. package/src/core/voice/stt-models.js +1 -1
  90. package/src/core/voice/tts.js +5 -4
  91. package/src/host/daemon/api/admin-config.js +5 -5
  92. package/src/host/daemon/api/admin.js +8 -7
  93. package/src/host/daemon/api/agents.js +16 -16
  94. package/src/host/daemon/api/artifact-preview.js +8 -8
  95. package/src/host/daemon/api/artifacts.js +7 -7
  96. package/src/host/daemon/api/code.js +8 -8
  97. package/src/host/daemon/api/commitments.js +135 -0
  98. package/src/host/daemon/api/config.js +6 -6
  99. package/src/host/daemon/api/confirm.js +2 -2
  100. package/src/host/daemon/api/connections.js +2 -2
  101. package/src/host/daemon/api/conversations.js +12 -12
  102. package/src/host/daemon/api/deck.js +6 -6
  103. package/src/host/daemon/api/desktop.js +8 -8
  104. package/src/host/daemon/api/embeddings.js +4 -4
  105. package/src/host/daemon/api/engines.js +8 -7
  106. package/src/host/daemon/api/exec.js +5 -4
  107. package/src/host/daemon/api/files-project.js +6 -6
  108. package/src/host/daemon/api/health.js +2 -2
  109. package/src/host/daemon/api/identity.js +3 -3
  110. package/src/host/daemon/api/inbox.js +2 -2
  111. package/src/host/daemon/api/integrations.js +23 -19
  112. package/src/host/daemon/api/mcps.js +16 -12
  113. package/src/host/daemon/api/messages.js +5 -5
  114. package/src/host/daemon/api/nudges.js +112 -0
  115. package/src/host/daemon/api/organization.js +8 -8
  116. package/src/host/daemon/api/pairing.js +6 -6
  117. package/src/host/daemon/api/plugins.js +3 -3
  118. package/src/host/daemon/api/prefix.js +20 -0
  119. package/src/host/daemon/api/profiles.js +9 -9
  120. package/src/host/daemon/api/projects.js +5 -5
  121. package/src/host/daemon/api/routines.js +32 -8
  122. package/src/host/daemon/api/run.js +2 -2
  123. package/src/host/daemon/api/runtimes.js +6 -6
  124. package/src/host/daemon/api/self-memory.js +50 -0
  125. package/src/host/daemon/api/sessions-search.js +18 -11
  126. package/src/host/daemon/api/sessions.js +6 -6
  127. package/src/host/daemon/api/shared.js +59 -13
  128. package/src/host/daemon/api/skills.js +12 -12
  129. package/src/host/daemon/api/super-agent.js +4 -4
  130. package/src/host/daemon/api/tasks.js +11 -11
  131. package/src/host/daemon/api/telegram.js +61 -24
  132. package/src/host/daemon/api/tools.js +7 -7
  133. package/src/host/daemon/api/top-level.js +7 -7
  134. package/src/host/daemon/api/transcribe.js +6 -6
  135. package/src/host/daemon/api/tts.js +3 -3
  136. package/src/host/daemon/api/vars.js +11 -8
  137. package/src/host/daemon/api/voice.js +9 -7
  138. package/src/host/daemon/api/web.js +28 -32
  139. package/src/host/daemon/api.js +83 -48
  140. package/src/host/daemon/callback-reconciler.js +16 -0
  141. package/src/host/daemon/db.js +1 -1
  142. package/src/host/daemon/desktop-ws.js +7 -3
  143. package/src/host/daemon/index.js +11 -5
  144. package/src/host/daemon/plugins/desktop/index.js +8 -2
  145. package/src/host/daemon/plugins/telegram/index.js +7 -2
  146. package/src/host/daemon/stt-venv.js +20 -81
  147. package/src/host/daemon/wakeup.js +18 -4
  148. package/src/interfaces/acp/index.js +3 -3
  149. package/src/interfaces/acp/session.js +2 -2
  150. package/src/interfaces/cli/commands/a2a.js +2 -2
  151. package/src/interfaces/cli/commands/agent.js +3 -3
  152. package/src/interfaces/cli/commands/artifact.js +13 -13
  153. package/src/interfaces/cli/commands/chat.js +3 -3
  154. package/src/interfaces/cli/commands/command.js +2 -2
  155. package/src/interfaces/cli/commands/commitment.js +154 -0
  156. package/src/interfaces/cli/commands/config.js +4 -4
  157. package/src/interfaces/cli/commands/daemon.js +74 -18
  158. package/src/interfaces/cli/commands/desktop.js +7 -14
  159. package/src/interfaces/cli/commands/exec.js +2 -2
  160. package/src/interfaces/cli/commands/log.js +3 -4
  161. package/src/interfaces/cli/commands/mcp.js +9 -9
  162. package/src/interfaces/cli/commands/memory.js +75 -2
  163. package/src/interfaces/cli/commands/messages.js +5 -5
  164. package/src/interfaces/cli/commands/model.js +0 -18
  165. package/src/interfaces/cli/commands/nudge.js +130 -0
  166. package/src/interfaces/cli/commands/obsidian.js +5 -5
  167. package/src/interfaces/cli/commands/org.js +5 -5
  168. package/src/interfaces/cli/commands/pair.js +6 -6
  169. package/src/interfaces/cli/commands/plugins.js +2 -2
  170. package/src/interfaces/cli/commands/profile.js +10 -10
  171. package/src/interfaces/cli/commands/project-config.js +6 -6
  172. package/src/interfaces/cli/commands/project.js +10 -10
  173. package/src/interfaces/cli/commands/routine.js +8 -9
  174. package/src/interfaces/cli/commands/runtime.js +2 -2
  175. package/src/interfaces/cli/commands/search.js +0 -1
  176. package/src/interfaces/cli/commands/session.js +12 -40
  177. package/src/interfaces/cli/commands/sessions.js +21 -798
  178. package/src/interfaces/cli/commands/setup.js +16 -6
  179. package/src/interfaces/cli/commands/skills.js +1 -2
  180. package/src/interfaces/cli/commands/status.js +3 -3
  181. package/src/interfaces/cli/commands/task.js +8 -8
  182. package/src/interfaces/cli/commands/telegram.js +21 -21
  183. package/src/interfaces/cli/commands/voice.js +6 -6
  184. package/src/interfaces/cli/help/index.js +2245 -0
  185. package/src/interfaces/cli/http.js +9 -4
  186. package/src/interfaces/cli/index.js +14 -2857
  187. package/src/interfaces/cli/routes/acp.js +13 -0
  188. package/src/interfaces/cli/routes/agent.js +27 -0
  189. package/src/interfaces/cli/routes/artifact.js +28 -0
  190. package/src/interfaces/cli/routes/chat.js +11 -0
  191. package/src/interfaces/cli/routes/code.js +11 -0
  192. package/src/interfaces/cli/routes/command.js +21 -0
  193. package/src/interfaces/cli/routes/commitment.js +19 -0
  194. package/src/interfaces/cli/routes/config.js +16 -0
  195. package/src/interfaces/cli/routes/connections.js +11 -0
  196. package/src/interfaces/cli/routes/conversations.js +21 -0
  197. package/src/interfaces/cli/routes/daemon.js +25 -0
  198. package/src/interfaces/cli/routes/desktop.js +20 -0
  199. package/src/interfaces/cli/routes/env.js +13 -0
  200. package/src/interfaces/cli/routes/exec.js +11 -0
  201. package/src/interfaces/cli/routes/identity.js +11 -0
  202. package/src/interfaces/cli/routes/index.js +75 -0
  203. package/src/interfaces/cli/routes/init.js +11 -0
  204. package/src/interfaces/cli/routes/log.js +20 -0
  205. package/src/interfaces/cli/routes/mcp.js +22 -0
  206. package/src/interfaces/cli/routes/memory.js +19 -0
  207. package/src/interfaces/cli/routes/messages.js +16 -0
  208. package/src/interfaces/cli/routes/model.js +11 -0
  209. package/src/interfaces/cli/routes/nudge.js +17 -0
  210. package/src/interfaces/cli/routes/obsidian.js +17 -0
  211. package/src/interfaces/cli/routes/org.js +34 -0
  212. package/src/interfaces/cli/routes/overlay.js +10 -0
  213. package/src/interfaces/cli/routes/pair.js +17 -0
  214. package/src/interfaces/cli/routes/panel.js +16 -0
  215. package/src/interfaces/cli/routes/permission.js +11 -0
  216. package/src/interfaces/cli/routes/plugins.js +21 -0
  217. package/src/interfaces/cli/routes/profile.js +27 -0
  218. package/src/interfaces/cli/routes/project.js +41 -0
  219. package/src/interfaces/cli/routes/restart.js +15 -0
  220. package/src/interfaces/cli/routes/routine.js +28 -0
  221. package/src/interfaces/cli/routes/run.js +11 -0
  222. package/src/interfaces/cli/routes/search.js +11 -0
  223. package/src/interfaces/cli/routes/send.js +11 -0
  224. package/src/interfaces/cli/routes/session.js +26 -0
  225. package/src/interfaces/cli/routes/sessions.js +15 -0
  226. package/src/interfaces/cli/routes/setup.js +18 -0
  227. package/src/interfaces/cli/routes/skills.js +20 -0
  228. package/src/interfaces/cli/routes/status.js +12 -0
  229. package/src/interfaces/cli/routes/task.js +26 -0
  230. package/src/interfaces/cli/routes/telegram.js +39 -0
  231. package/src/interfaces/cli/routes/update.js +24 -0
  232. package/src/interfaces/cli/routes/voice.js +17 -0
  233. package/src/interfaces/desktop/main.js +6 -7
  234. package/src/interfaces/desktop/renderer.js +0 -5
  235. package/src/interfaces/mcp-server/index.js +15 -14
  236. package/src/interfaces/tui/_shims/globals.d.ts +8 -0
  237. package/src/interfaces/tui/tsconfig.json +4 -1
  238. package/src/interfaces/web/README.md +6 -6
  239. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
  240. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
  241. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
  242. package/src/interfaces/web/dist/index.html +2 -2
  243. package/src/interfaces/web/package-lock.json +11 -10
  244. package/src/interfaces/web/src/components/AddProjectDialog.tsx +1 -1
  245. package/src/interfaces/web/src/components/Section.tsx +18 -3
  246. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +1 -1
  247. package/src/interfaces/web/src/components/chat/ChatList.tsx +2 -2
  248. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
  249. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +1 -1
  250. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +1 -1
  251. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +1 -1
  252. package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
  253. package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +1 -1
  254. package/src/interfaces/web/src/components/files/FileBrowser.tsx +2 -2
  255. package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
  256. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +36 -6
  257. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +1 -1
  258. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
  259. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +14 -4
  260. package/src/interfaces/web/src/components/routines/shared.ts +14 -5
  261. package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +1 -1
  262. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
  263. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
  264. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
  265. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
  266. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +2 -2
  267. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +1 -1
  268. package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
  269. package/src/interfaces/web/src/components/ui.tsx +1 -0
  270. package/src/interfaces/web/src/constants/index.ts +1 -0
  271. package/src/interfaces/web/src/hooks/useChat.ts +5 -1
  272. package/src/interfaces/web/src/hooks/useDaemonStatus.ts +1 -1
  273. package/src/interfaces/web/src/hooks/useDevices.ts +1 -1
  274. package/src/interfaces/web/src/hooks/useEngines.ts +1 -1
  275. package/src/interfaces/web/src/hooks/useGlobalConfig.ts +2 -2
  276. package/src/interfaces/web/src/hooks/useIdentity.ts +1 -1
  277. package/src/interfaces/web/src/hooks/useInbox.ts +1 -1
  278. package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
  279. package/src/interfaces/web/src/hooks/useProfiles.ts +3 -3
  280. package/src/interfaces/web/src/hooks/useProjects.ts +1 -1
  281. package/src/interfaces/web/src/hooks/useTelegram.ts +3 -3
  282. package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +3 -3
  283. package/src/interfaces/web/src/i18n/en.ts +127 -0
  284. package/src/interfaces/web/src/i18n/es.ts +127 -0
  285. package/src/interfaces/web/src/lib/api/admin.ts +11 -11
  286. package/src/interfaces/web/src/lib/api/agents.ts +14 -14
  287. package/src/interfaces/web/src/lib/api/artifacts.ts +11 -11
  288. package/src/interfaces/web/src/lib/api/code.ts +1 -1
  289. package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
  290. package/src/interfaces/web/src/lib/api/conversations.ts +8 -8
  291. package/src/interfaces/web/src/lib/api/deck.ts +3 -3
  292. package/src/interfaces/web/src/lib/api/desktop.ts +8 -8
  293. package/src/interfaces/web/src/lib/api/embeddings.ts +3 -3
  294. package/src/interfaces/web/src/lib/api/engines.ts +3 -3
  295. package/src/interfaces/web/src/lib/api/filesystem.ts +2 -2
  296. package/src/interfaces/web/src/lib/api/health.ts +1 -1
  297. package/src/interfaces/web/src/lib/api/identity.ts +2 -2
  298. package/src/interfaces/web/src/lib/api/inbox.ts +1 -1
  299. package/src/interfaces/web/src/lib/api/integrations.ts +8 -8
  300. package/src/interfaces/web/src/lib/api/mcps.ts +6 -6
  301. package/src/interfaces/web/src/lib/api/messages.ts +3 -3
  302. package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
  303. package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
  304. package/src/interfaces/web/src/lib/api/organization.ts +7 -7
  305. package/src/interfaces/web/src/lib/api/profiles.ts +8 -8
  306. package/src/interfaces/web/src/lib/api/projectFiles.ts +5 -5
  307. package/src/interfaces/web/src/lib/api/projects.ts +12 -12
  308. package/src/interfaces/web/src/lib/api/routines.ts +7 -7
  309. package/src/interfaces/web/src/lib/api/sessions.ts +2 -2
  310. package/src/interfaces/web/src/lib/api/skills.ts +11 -11
  311. package/src/interfaces/web/src/lib/api/super_agent.ts +3 -3
  312. package/src/interfaces/web/src/lib/api/tasks.ts +12 -12
  313. package/src/interfaces/web/src/lib/api/telegram.ts +14 -14
  314. package/src/interfaces/web/src/lib/api/tools.ts +1 -1
  315. package/src/interfaces/web/src/lib/api/vars.ts +4 -4
  316. package/src/interfaces/web/src/lib/api/voice.ts +6 -6
  317. package/src/interfaces/web/src/lib/cron.ts +196 -0
  318. package/src/interfaces/web/src/lib/when.ts +32 -0
  319. package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
  320. package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
  321. package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
  322. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +1 -1
  323. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
  324. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +103 -20
  325. package/src/interfaces/web/src/screens/base/LogsTab.tsx +18 -3
  326. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +1 -1
  327. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  328. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +1 -1
  329. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -1
  330. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  331. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -8
  332. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +2 -2
  333. package/src/interfaces/web/src/screens/project/ChatTab.tsx +24 -6
  334. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +1 -1
  335. package/src/interfaces/web/src/screens/project/McpsTab.tsx +3 -3
  336. package/src/interfaces/web/src/screens/project/Overview.tsx +6 -6
  337. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +15 -13
  338. package/src/interfaces/web/src/screens/project/StructureTab.tsx +1 -1
  339. package/src/interfaces/web/src/screens/project/TasksTab.tsx +1 -1
  340. package/src/interfaces/web/src/screens/project/VarsTab.tsx +1 -1
  341. package/src/interfaces/web/src/types/daemon.ts +10 -1
  342. package/src/interfaces/web/vite.config.ts +13 -18
  343. package/src/interfaces/web/dist/assets/index-BptlbKjU.js +0 -824
  344. package/src/interfaces/web/dist/assets/index-BptlbKjU.js.map +0 -1
  345. package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
  346. package/src/skills/apc-context/SKILL.md +0 -159
  347. /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
  348. /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
  349. /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
  350. /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
  351. /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
  352. /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
  353. /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
  354. /package/src/{host/daemon → core}/runtimes/index.js +0 -0
  355. /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
  356. /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
@@ -3,7 +3,7 @@
3
3
  // 2. Brave Search API (requires BRAVE_API_KEY env)
4
4
  // 3. Puppeteer Google fallback (requires puppeteer installed)
5
5
  //
6
- // Endpoint: POST /tools/search
6
+ // Endpoint: POST /api/tools/search
7
7
  // Body: { query, mode: "auto"|"ddg"|"brave"|"browser", limit? }
8
8
 
9
9
  import { browser_navigate, browser_get_text, browser_evaluate } from "./browser.js";
@@ -302,7 +302,7 @@ export function buildSearchRouter(express) {
302
302
  }
303
303
  });
304
304
 
305
- // GET convenience: /tools/search?q=...&mode=auto&limit=5
305
+ // GET convenience: /api/tools/search?q=...&mode=auto&limit=5
306
306
  router.get("/", async (req, res) => {
307
307
  const { q, query, mode, limit } = req.query;
308
308
  try {
@@ -1,23 +1,22 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import os from "node:os";
3
+ import { IDENTITY_PATH } from "../config/paths.js";
4
+
5
+ export { IDENTITY_PATH };
4
6
  import { SUPERAGENT_ACTOR_ID } from "../constants/actors.js";
7
+ import { readJson } from "#core/util/json-file.js";
5
8
 
6
9
  // Re-export so callers that already imported the actor id from here keep
7
10
  // working. The single source of truth lives in core/constants/actors.js.
8
11
  export { SUPERAGENT_ACTOR_ID };
9
12
 
10
- export const IDENTITY_PATH = path.join(os.homedir(), ".apx", "identity.json");
13
+
11
14
 
12
15
  // Shown when no persona is configured yet. Brand of the app, not a persona.
13
16
  export const SUPERAGENT_DISPLAY_FALLBACK = "APX";
14
17
 
15
18
  export function readIdentity() {
16
- try {
17
- return JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf8"));
18
- } catch {
19
- return null;
20
- }
19
+ return readJson(IDENTITY_PATH, null);
21
20
  }
22
21
 
23
22
  // Resolve the super-agent's DISPLAY name (the persona shown to users). Order:
@@ -37,7 +37,7 @@ export function resolveSender({ cfg, channelName, from, chatType }) {
37
37
  const isOwner =
38
38
  channel?.owner_user_id != null &&
39
39
  String(channel.owner_user_id) === String(userId);
40
- const role = isOwner ? "owner" : contact?.role || "guest";
40
+ const role = isOwner ? SENDER_ROLES.OWNER : contact?.role || SENDER_ROLES.GUEST;
41
41
  return {
42
42
  userId,
43
43
  username,
@@ -115,11 +115,11 @@ export function registerSender({ cfg, channelName, from, chatType }) {
115
115
  if (kind === "claim") {
116
116
  upsertTelegramChannel(disk, channelName, { owner_user_id: userId });
117
117
  upsertContact(disk, userId, {
118
- name, username, role: "owner", first_seen: now, last_seen: now,
118
+ name, username, role: SENDER_ROLES.OWNER, first_seen: now, last_seen: now,
119
119
  });
120
120
  } else if (kind === "guest") {
121
121
  upsertContact(disk, userId, {
122
- name, username, role: "guest", first_seen: now, last_seen: now,
122
+ name, username, role: SENDER_ROLES.GUEST, first_seen: now, last_seen: now,
123
123
  });
124
124
  } else {
125
125
  upsertContact(disk, userId, { last_seen: now });
@@ -1,14 +1,15 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import { APX_HOME } from "./config/index.js";
4
2
  import { maskSecretValues } from "./config/secret-values.js";
3
+ import { LOG_DIR, APX_LOG_PATH, ERROR_TRACE_PATH } from "./config/paths.js";
4
+
5
+ // Canonical definitions live in config/paths.js; re-exported so the existing
6
+ // `import { APX_LOG_PATH } from "#core/logging.js"` call sites keep working.
7
+ export { LOG_DIR, APX_LOG_PATH, ERROR_TRACE_PATH };
5
8
 
6
- export const LOG_DIR = path.join(APX_HOME, "logs");
7
- export const ERROR_TRACE_PATH = path.join(LOG_DIR, "errors.jsonl");
8
9
  // Unified daemon log. Every module (daemon, telegram, whisper, super-agent,
9
10
  // tools, desktop) writes here with one consistent format so the user can
10
11
  // follow the whole system from a single tail.
11
- export const APX_LOG_PATH = path.join(LOG_DIR, "apx.log");
12
+
12
13
 
13
14
  const SECRET_KEY_RE = /(token|secret|password|api[_-]?key|authorization|bot[_-]?token)/i;
14
15
 
@@ -34,12 +34,14 @@
34
34
  // instead, then to true.
35
35
 
36
36
  import fs from "node:fs";
37
- import os from "node:os";
38
37
  import path from "node:path";
38
+ import { apxHome } from "#core/config/paths.js";
39
39
  import { apcMcpsFile } from "#core/apc/paths.js";
40
+ import { readJson } from "#core/util/json-file.js";
40
41
 
41
- const APX_HOME = path.join(os.homedir(), ".apx");
42
- const GLOBAL_MCPS_FILE = path.join(APX_HOME, "mcps.json");
42
+
43
+ // Resolved per call: the global store must follow APX_HOME if it moves.
44
+ const globalMcpsFile = () => path.join(apxHome(), "mcps.json");
43
45
  const RUNTIME_MCPS_FILENAME = "mcps.json";
44
46
 
45
47
  // Project-relative sources (file path resolved against projectRoot).
@@ -63,15 +65,9 @@ export const SOURCES = [
63
65
  { id: "global", file: "~/.apx/mcps.json", key: "mcpServers", scope: "global" },
64
66
  ];
65
67
 
66
- function readJsonSafe(absPath) {
67
- if (!absPath) return null;
68
- if (!fs.existsSync(absPath)) return null;
69
- try {
70
- return JSON.parse(fs.readFileSync(absPath, "utf8"));
71
- } catch {
72
- return null;
73
- }
74
- }
68
+ // readJsonSafe is the shared readJson(); secrets are written with SECRET_MODE.
69
+ const readJsonSafe = (absPath) => readJson(absPath, null);
70
+
75
71
 
76
72
  // Resolve the runtime mcps.json path for a given project. `storagePath` is the
77
73
  // project's ~/.apx/projects/<apxId>/ directory.
@@ -81,7 +77,7 @@ export function runtimeMcpsPath(storagePath) {
81
77
  }
82
78
 
83
79
  export function globalMcpsPath() {
84
- return GLOBAL_MCPS_FILE;
80
+ return globalMcpsFile();
85
81
  }
86
82
 
87
83
  // Aggregate all MCP sources for a project, in priority order.
@@ -119,7 +115,7 @@ export function loadAll(projectRoot, opts = {}) {
119
115
  // last. global
120
116
  pipeline.push({
121
117
  id: "global",
122
- abs: GLOBAL_MCPS_FILE,
118
+ abs: globalMcpsFile(),
123
119
  key: "mcpServers",
124
120
  });
125
121
 
@@ -229,7 +225,7 @@ export function writeRuntimeMcps(storagePath, json) {
229
225
  // ---------------------------------------------------------------------------
230
226
 
231
227
  export function readGlobalMcps() {
232
- const p = GLOBAL_MCPS_FILE;
228
+ const p = globalMcpsFile();
233
229
  if (!fs.existsSync(p)) return { mcpServers: {} };
234
230
  try {
235
231
  const json = JSON.parse(fs.readFileSync(p, "utf8"));
@@ -241,7 +237,7 @@ export function readGlobalMcps() {
241
237
  }
242
238
 
243
239
  export function writeGlobalMcps(json) {
244
- const p = GLOBAL_MCPS_FILE;
240
+ const p = globalMcpsFile();
245
241
  fs.mkdirSync(path.dirname(p), { recursive: true });
246
242
  fs.writeFileSync(p, JSON.stringify(json, null, 2) + "\n");
247
243
  }
@@ -0,0 +1,225 @@
1
+ // Post-session memory consolidation.
2
+ //
3
+ // THE GAP THIS CLOSES: self-memory.js has always claimed the notebook is
4
+ // "refreshed by skimming its own recent sessions". That function did not
5
+ // exist. Today a durable fact only reaches ~/.apx/memory.md if the model
6
+ // happens to call `remember` in the moment. The RAG index and the compactor
7
+ // are automatic, but those are RETRIEVAL, not learning.
8
+ //
9
+ // THE DANGER, which is bigger than the gap: memory.md is injected into every
10
+ // prompt on every channel. A notebook that grows without judgement is a
11
+ // permanent tax paid on every turn for facts nobody needed. So the whole
12
+ // design here leans one way — when in doubt, do not save.
13
+ //
14
+ // Three deliberate constraints:
15
+ //
16
+ // 1. CANDIDATES, NOT WRITES, by default. `apx memory consolidate` proposes;
17
+ // writing is a separate, explicit step. A background job that silently
18
+ // edits the file the agent believes about itself is not something to
19
+ // switch on by default.
20
+ // 2. DEDUP AGAINST WHAT IS ALREADY THERE. The same fact learned three times
21
+ // is one fact.
22
+ // 3. EVERY WRITE IS TAGGED AND REVERSIBLE. Consolidated bullets carry a
23
+ // marker so `revert` can remove exactly what a run added and nothing the
24
+ // user or the model wrote by hand.
25
+ import { readSelfMemory, appendSelfMemory, parseSelfMemoryEntries, SELF_MEMORY_PATH } from "#core/agent/self-memory.js";
26
+ import fs from "node:fs";
27
+
28
+ /** Marks a bullet as machine-distilled, so a revert can find its own writes. */
29
+ export const CONSOLIDATED_CHANNEL = "consolidated";
30
+
31
+ /** Conservative by design — see the header. */
32
+ export const DEFAULT_LIMITS = Object.freeze({
33
+ /** Never propose more than this from one run. A day is not a biography. */
34
+ max_candidates: 5,
35
+ /** Below this, a line is chatter, not a fact. */
36
+ min_chars: 20,
37
+ /** Above this it is a paragraph; the notebook holds one-liners. */
38
+ max_chars: 240,
39
+ /**
40
+ * Jaccard overlap at which two facts are "the same fact".
41
+ *
42
+ * Set low on purpose. The two errors here are not symmetric: calling a new
43
+ * fact a duplicate costs one thing not saved, which the model can say again
44
+ * tomorrow. Calling a duplicate new costs a permanent second copy in a file
45
+ * that ships on every turn of every channel. So it errs toward dedup — real
46
+ * paraphrases ("instead of npm" / "rather than npm") land around 0.55.
47
+ */
48
+ dedup_threshold: 0.5,
49
+ });
50
+
51
+ /**
52
+ * Openers that mark a durable fact about how someone works or what they
53
+ * decided, as opposed to a transient exchange. Matching on shape rather than
54
+ * meaning keeps this deterministic and testable — the model does the
55
+ * distilling upstream; this decides what is worth keeping.
56
+ */
57
+ const DURABLE_MARKERS = [
58
+ /\b(prefer|prefers|prefiere|prefiero)\b/i,
59
+ /\b(decided|decidimos|decidí|decision)\b/i,
60
+ /\b(always|never|siempre|nunca)\b/i,
61
+ /\b(uses?|usa|usamos)\b.*\b(instead of|en vez de)\b/i,
62
+ /\b(works? (?:on|at)|trabaja en)\b/i,
63
+ /\b(deadline|vence|due)\b/i,
64
+ /\b(rule|regla|convention|convención)\b/i,
65
+ ];
66
+
67
+ /** Things that look like facts but age out within a day. */
68
+ const TRANSIENT_MARKERS = [
69
+ /\b(today|hoy|right now|ahora mismo|esta mañana|this morning)\b/i,
70
+ /\b(will (?:check|look)|voy a (?:ver|revisar))\b/i,
71
+ /^\s*(ok|okay|dale|listo|thanks|gracias|perfecto)\b/i,
72
+ ];
73
+
74
+ /** Words that carry meaning, lowercased, for overlap comparison. */
75
+ function tokenise(text) {
76
+ return new Set(
77
+ String(text || "")
78
+ .toLowerCase()
79
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
80
+ .split(/\s+/)
81
+ .filter((w) => w.length > 3),
82
+ );
83
+ }
84
+
85
+ /** Jaccard overlap. 1 = identical vocabulary, 0 = nothing in common. */
86
+ export function similarity(a, b) {
87
+ const A = tokenise(a);
88
+ const B = tokenise(b);
89
+ if (!A.size || !B.size) return 0;
90
+ let shared = 0;
91
+ for (const w of A) if (B.has(w)) shared += 1;
92
+ return shared / (A.size + B.size - shared);
93
+ }
94
+
95
+ /** Is this line worth keeping past today? */
96
+ export function looksDurable(text, limits = DEFAULT_LIMITS) {
97
+ const s = String(text || "").trim();
98
+ if (s.length < limits.min_chars || s.length > limits.max_chars) return false;
99
+ if (TRANSIENT_MARKERS.some((re) => re.test(s))) return false;
100
+ return DURABLE_MARKERS.some((re) => re.test(s));
101
+ }
102
+
103
+ /**
104
+ * Propose durable facts from a batch of candidate lines.
105
+ *
106
+ * The caller supplies the lines — from a model's distillation of a session, or
107
+ * from anywhere else. This module owns the JUDGEMENT about what survives, so
108
+ * the same rules apply no matter who is proposing.
109
+ *
110
+ * @param {string[]} candidates
111
+ * @param {object} opts { existing?: string, limits?, now? }
112
+ * @returns {{ kept: string[], rejected: {text: string, reason: string}[] }}
113
+ */
114
+ export function proposeConsolidation(candidates, opts = {}) {
115
+ const limits = { ...DEFAULT_LIMITS, ...(opts.limits || {}) };
116
+ const existingText = opts.existing !== undefined ? opts.existing : readSelfMemory();
117
+ const known = parseSelfMemoryEntries(existingText).map((e) => e.text);
118
+
119
+ const kept = [];
120
+ const rejected = [];
121
+
122
+ for (const raw of candidates || []) {
123
+ const text = String(raw || "").replace(/\s+/g, " ").trim();
124
+ if (!text) continue;
125
+
126
+ if (kept.length >= limits.max_candidates) {
127
+ rejected.push({ text, reason: "over the per-run limit" });
128
+ continue;
129
+ }
130
+ if (!looksDurable(text, limits)) {
131
+ rejected.push({ text, reason: "not durable" });
132
+ continue;
133
+ }
134
+
135
+ // Against what is already saved AND against what this run already kept —
136
+ // a session that says the same thing twice must not save it twice.
137
+ const clash = [...known, ...kept].find((k) => similarity(k, text) >= limits.dedup_threshold);
138
+ if (clash) {
139
+ rejected.push({ text, reason: `already known: "${clash.slice(0, 60)}"` });
140
+ continue;
141
+ }
142
+
143
+ kept.push(text);
144
+ }
145
+
146
+ return { kept, rejected };
147
+ }
148
+
149
+ /**
150
+ * Write accepted facts to the notebook, tagged so they can be reverted.
151
+ * Separate from proposing on purpose: the decision to write is the user's.
152
+ */
153
+ export function applyConsolidation(facts, opts = {}) {
154
+ const written = [];
155
+ for (const text of facts || []) {
156
+ appendSelfMemory(text, { channel: CONSOLIDATED_CHANNEL, ...(opts.time ? { time: opts.time } : {}) });
157
+ written.push(text);
158
+ }
159
+ return { written, path: SELF_MEMORY_PATH };
160
+ }
161
+
162
+ /**
163
+ * Undo consolidation: drop bullets this module wrote, leaving everything the
164
+ * user or the model wrote by hand exactly where it is.
165
+ *
166
+ * @param {object} opts { since?: "YYYY-MM-DD" } — only that day and after.
167
+ * @returns {{ removed: number, path: string }}
168
+ */
169
+ export function revertConsolidation(opts = {}) {
170
+ const text = readSelfMemory();
171
+ if (!text.trim()) return { removed: 0, path: SELF_MEMORY_PATH };
172
+
173
+ const since = opts.since || "";
174
+ const lines = text.split("\n");
175
+ const out = [];
176
+ let day = "";
177
+ let removed = 0;
178
+
179
+ for (const line of lines) {
180
+ const h = line.trim().match(/^##\s+(\d{4}-\d{2}-\d{2})/);
181
+ if (h) { day = h[1]; out.push(line); continue; }
182
+
183
+ // Only ever removes a bullet carrying OUR channel tag. A hand-written note
184
+ // that happens to sit next to one is untouched.
185
+ const isOurs = /^[-*]\s+(?:\[[^\]]+\]\s*)?\[consolidated\]\s/i.test(line.trim());
186
+ if (isOurs && (!since || day >= since)) {
187
+ removed += 1;
188
+ continue;
189
+ }
190
+ out.push(line);
191
+ }
192
+
193
+ if (removed) {
194
+ fs.writeFileSync(SELF_MEMORY_PATH, stripEmptyDays(out).join("\n"));
195
+ }
196
+ return { removed, path: SELF_MEMORY_PATH };
197
+ }
198
+
199
+ /** Drop day headings left with no bullets under them. */
200
+ function stripEmptyDays(lines) {
201
+ const out = [];
202
+ for (let i = 0; i < lines.length; i++) {
203
+ const isHeading = /^##\s+\d{4}-\d{2}-\d{2}/.test(lines[i].trim());
204
+ if (!isHeading) { out.push(lines[i]); continue; }
205
+ let hasBullet = false;
206
+ for (let j = i + 1; j < lines.length; j++) {
207
+ if (/^##\s+/.test(lines[j].trim())) break;
208
+ if (/^[-*]\s+\S/.test(lines[j].trim())) { hasBullet = true; break; }
209
+ }
210
+ if (hasBullet) out.push(lines[i]);
211
+ }
212
+ return out;
213
+ }
214
+
215
+ /** How much of the prompt budget the notebook is currently costing. */
216
+ export function notebookSize() {
217
+ const text = readSelfMemory();
218
+ const entries = parseSelfMemoryEntries(text);
219
+ return {
220
+ chars: text.length,
221
+ approx_tokens: Math.ceil(text.length / 4),
222
+ entries: entries.length,
223
+ consolidated: entries.filter((e) => e.channel === CONSOLIDATED_CHANNEL).length,
224
+ };
225
+ }
@@ -23,6 +23,7 @@ import { GLOBAL_MESSAGES_DIR, APX_HOME } from "../config/index.js";
23
23
  import { SELF_MEMORY_PATH, parseSelfMemoryEntries } from "../agent/self-memory.js";
24
24
  import { apcMemoryFile } from "../apc/paths.js";
25
25
  import { embedBatch, embedOne } from "./embeddings.js";
26
+ import { readJson } from "#core/util/json-file.js";
26
27
 
27
28
  export const CURSOR_PATH = path.join(APX_HOME, "memory-cursor.json");
28
29
 
@@ -48,11 +49,7 @@ function readIfExists(p) {
48
49
  }
49
50
 
50
51
  function readCursor(cursorPath) {
51
- try {
52
- return JSON.parse(fs.readFileSync(cursorPath, "utf8"));
53
- } catch {
54
- return { channels: {} };
55
- }
52
+ return readJson(cursorPath, { channels: {} });
56
53
  }
57
54
 
58
55
  function writeCursor(cursorPath, cursor) {
@@ -0,0 +1,192 @@
1
+ // The interruption budget — one gate every unrequested message passes through.
2
+ //
3
+ // THE RULE THAT MATTERS: the gate lives at the CALL SITES that decide to speak
4
+ // unprompted, never inside the channel's `_send`. `_send` also carries the
5
+ // user's own replies, and a budget that can swallow an answer someone is
6
+ // waiting for reads as a hung bot, not as restraint. Each caller states
7
+ // `unsolicited` explicitly, so the intent is visible in the diff rather than
8
+ // inferred from the call stack.
9
+ //
10
+ // The budget is a feature, not a limitation: it is what makes the message get
11
+ // opened when the agent does speak.
12
+ //
13
+ // Shape of a call site:
14
+ //
15
+ // const gate = canNudge({ kind: "day_open", project_id, severity }, config);
16
+ // if (!gate.allowed) return; // log gate.reason
17
+ // await send({ ..., reply_markup: nudgeFeedbackKeyboard(gate.nudge_id) });
18
+ // recordNudge(gate, { chat_id, preview: text });
19
+ import { shortId } from "#core/util/ids.js";
20
+ import { resolveNudgePolicy, isQuietAt, quietEndsAt } from "./policy.js";
21
+ import {
22
+ readNudgeLedger, appendNudge, setNudgeFeedback, nudgesOnDay, lastNudge,
23
+ } from "./store.js";
24
+
25
+ export { resolveNudgePolicy, isQuietAt, DEFAULT_POLICY } from "./policy.js";
26
+ export { listNudges, nudgeStats, readNudgeLedger } from "./store.js";
27
+
28
+ /** Severity that may cross a closed gate, when the policy allows it. */
29
+ const CRITICAL = "critical";
30
+
31
+ /**
32
+ * May APX speak right now?
33
+ *
34
+ * @param {object} req
35
+ * @param {string} req.kind what sort of interruption ("day_open", "signal", "session_result", …)
36
+ * @param {string?} req.project_id which project it concerns, when it concerns one
37
+ * @param {string} req.severity "low" | "normal" | "high" | "critical"
38
+ * @param {boolean} req.unsolicited false for a delivery the user asked for. Be honest here.
39
+ * @param {boolean} req.scheduled true when the user themselves put this on the clock (an
40
+ * anchor routine). Exempt from the ceiling, still recorded.
41
+ * @param {string} req.channel
42
+ * @param {object} config parsed ~/.apx/config.json
43
+ * @param {Date} now injectable for tests
44
+ * @returns {{allowed: boolean, reason: string, retry_after_ms: number|null, nudge_id: string,
45
+ * kind: string, project_id: string|null, severity: string, channel: string,
46
+ * unsolicited: boolean, bypassed_budget: boolean, policy: object}}
47
+ */
48
+ export function canNudge(req = {}, config = {}, now = new Date()) {
49
+ const kind = req.kind || "unknown";
50
+ const projectId = req.project_id ?? null;
51
+ const severity = req.severity || "normal";
52
+ const channel = req.channel || "telegram";
53
+ const unsolicited = req.unsolicited !== false;
54
+ const scheduled = req.scheduled === true;
55
+ const policy = resolveNudgePolicy(config);
56
+
57
+ const decision = (allowed, reason, retry_after_ms = null, bypassed_budget = false) => ({
58
+ allowed, reason, retry_after_ms,
59
+ nudge_id: shortId("ndg"),
60
+ kind, project_id: projectId, severity, channel, unsolicited, scheduled, bypassed_budget, policy,
61
+ });
62
+
63
+ // A reply, or a result the user launched and is waiting for. It passes
64
+ // through so the audit holds and so it lands in the ledger, but it never
65
+ // spends a budget it did not ask to spend.
66
+ if (!unsolicited) return decision(true, "solicited");
67
+
68
+ if (!policy.enabled) return decision(true, "budget-disabled");
69
+
70
+ // An ANCHOR — the morning and evening messages the user themselves put on
71
+ // the clock. The profile schema calls the daily number "the ceiling OUTSIDE
72
+ // the anchors", and it means it: a budget of three that two anchors spend
73
+ // leaves one, which is not what anybody chose. Charging someone's own
74
+ // schedule against their interruption allowance is the same category of
75
+ // wrong as gating a reply.
76
+ //
77
+ // Quiet hours are skipped too, for the same reason: an anchor scheduled
78
+ // inside them is a contradiction the USER wrote, and their explicit cron
79
+ // beats our default window.
80
+ if (scheduled) return decision(true, "scheduled-by-user");
81
+
82
+ const isCritical = severity === CRITICAL;
83
+ if (isCritical && policy.critical_bypasses_budget) {
84
+ // Audited, per the spec: it goes in the ledger flagged, so an integration
85
+ // that discovers "critical" as a way to shout cannot do it quietly.
86
+ return decision(true, "critical-bypass", null, true);
87
+ }
88
+
89
+ if (isQuietAt(policy.quiet_hours, now)) {
90
+ const ends = quietEndsAt(policy.quiet_hours, now);
91
+ return decision(false, `quiet-hours (${policy.quiet_hours})`, ends ? ends - now : null);
92
+ }
93
+
94
+ const ledger = readNudgeLedger();
95
+
96
+ if (policy.daily_max > 0) {
97
+ // Anchors and audited bypasses are recorded but do not consume the
98
+ // allowance — otherwise the number the user configured is not the number
99
+ // they get.
100
+ const today = nudgesOnDay(now, ledger).filter((e) => !e.bypassed_budget && !e.scheduled);
101
+ if (today.length >= policy.daily_max) {
102
+ return decision(
103
+ false,
104
+ `daily budget spent (${today.length}/${policy.daily_max})`,
105
+ msUntilTomorrow(now),
106
+ );
107
+ }
108
+ }
109
+
110
+ const cooldowns = [
111
+ [policy.cooldown_minutes, () => true, "global cooldown"],
112
+ [policy.project_cooldown_minutes, (e) => projectId != null && String(e.project_id) === String(projectId), `cooldown for this project`],
113
+ [policy.kind_cooldown_minutes, (e) => e.kind === kind, `cooldown for "${kind}"`],
114
+ ];
115
+
116
+ for (const [minutes, match, label] of cooldowns) {
117
+ if (!minutes) continue;
118
+ const last = lastNudge(match, ledger);
119
+ if (!last) continue;
120
+ const elapsed = now - new Date(last.at);
121
+ const window = minutes * 60_000;
122
+ if (Number.isFinite(elapsed) && elapsed < window) {
123
+ return decision(false, `${label} (${minutes}m)`, window - elapsed);
124
+ }
125
+ }
126
+
127
+ return decision(true, "within budget");
128
+ }
129
+
130
+ /**
131
+ * Record a nudge that was actually delivered. Takes the decision object
132
+ * `canNudge` returned, so the id in the feedback button and the id in the
133
+ * ledger are the same one.
134
+ */
135
+ export function recordNudge(decision, { chat_id = null, preview = "" } = {}) {
136
+ if (!decision?.allowed) return null;
137
+ // Solicited traffic is not an interruption and must not fill the ledger the
138
+ // user reads to answer "how often does this thing bother me".
139
+ if (decision.unsolicited === false) return null;
140
+ return appendNudge({
141
+ id: decision.nudge_id,
142
+ kind: decision.kind,
143
+ project_id: decision.project_id,
144
+ severity: decision.severity,
145
+ channel: decision.channel,
146
+ chat_id,
147
+ preview,
148
+ bypassed_budget: decision.bypassed_budget,
149
+ scheduled: decision.scheduled,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * "That wasn't useful." The loop is not optional: initiative that never learns
155
+ * gets switched off, and the switch is the user muting the bot.
156
+ */
157
+ export function recordFeedback(nudgeId, useful, note = "") {
158
+ return setNudgeFeedback(nudgeId, { useful, note });
159
+ }
160
+
161
+ /** Telegram inline keyboard for a proactive push. Two taps, no typing. */
162
+ export function nudgeFeedbackKeyboard(nudgeId) {
163
+ if (!nudgeId) return undefined;
164
+ return {
165
+ inline_keyboard: [[
166
+ { text: "👍 Útil", callback_data: `apx:nudge:${nudgeId}:useful` },
167
+ { text: "👎 No me servía", callback_data: `apx:nudge:${nudgeId}:noise` },
168
+ ]],
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Handle a press on that keyboard. Returns the acknowledgement text, or null
174
+ * when the callback belongs to someone else.
175
+ */
176
+ export function applyNudgeCallback(data) {
177
+ if (typeof data !== "string" || !data.startsWith("apx:nudge:")) return null;
178
+ const [nudgeId, verb] = data.slice("apx:nudge:".length).split(":");
179
+ if (!nudgeId || (verb !== "useful" && verb !== "noise")) return null;
180
+ const entry = recordFeedback(nudgeId, verb === "useful");
181
+ if (!entry) return { ack: "Ese mensaje ya no está en el registro.", entry: null };
182
+ return {
183
+ ack: verb === "useful" ? "Anotado: te sirvió." : "Anotado: no te servía.",
184
+ entry,
185
+ };
186
+ }
187
+
188
+ function msUntilTomorrow(now) {
189
+ const t = new Date(now);
190
+ t.setHours(24, 0, 0, 0);
191
+ return t - now;
192
+ }