@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
@@ -0,0 +1,800 @@
1
+ // Cross-engine session discovery.
2
+ //
3
+ // Every AI coding engine keeps its transcripts somewhere different and in a
4
+ // different shape: Claude Code writes JSONL under a folder named after the
5
+ // encoded project path, Codex writes dated rollout files, APX keeps markdown
6
+ // with frontmatter, Antigravity has its own tree. This module hides that: each
7
+ // engine is a small adapter, and callers ask for "sessions for this directory"
8
+ // without knowing whose format it is.
9
+ //
10
+ // It used to live in src/interfaces/cli/commands/sessions.js — 1008 lines of
11
+ // domain engine inside a *surface*. Because that was its only home, both
12
+ // core/agent/tools/handlers/search-sessions.js and host/daemon/api/sessions.js
13
+ // had to import upward into the CLI, inverting the layering. Two narrower
14
+ // re-implementations had also grown in core/stores/ to avoid the dependency.
15
+ // One home now: the CLI, the daemon and the agent tool all call this.
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { apcProjectFile } from "#core/apc/paths.js";
20
+ import { parseFrontmatterFields as parseFrontmatter } from "#core/apc/frontmatter.js";
21
+
22
+ // ── shared helpers ───────────────────────────────────────────────────────────
23
+
24
+ const homeDir = (opts) => (opts && opts.home) || os.homedir();
25
+
26
+ // Claude Code encodes a project cwd into a folder name by replacing every
27
+ // non-alphanumeric character with "-". Mirrors encodeClaudeProjectPath in the
28
+ // claude-code runtime adapter.
29
+ function encodeClaudeProjectPath(cwd) {
30
+ return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
31
+ }
32
+
33
+ function safeStatMtime(p) {
34
+ try {
35
+ return fs.statSync(p).mtimeMs;
36
+ } catch {
37
+ return 0;
38
+ }
39
+ }
40
+
41
+ // File-modification timestamps get clobbered by Time Machine and other backup
42
+ // tools. Most engines write a `"timestamp": "<iso>"` field on every JSONL line
43
+ // — when present, that's a far more reliable order key than mtime. We read a
44
+ // short tail of the file and scan back for the latest valid timestamp,
45
+ // falling back to the head, then mtime, when nothing parses.
46
+ function readInternalTimestamp(file, { tailBytes = 8192, headBytes = 8192 } = {}) {
47
+ let stat;
48
+ try {
49
+ stat = fs.statSync(file);
50
+ } catch {
51
+ return 0;
52
+ }
53
+ const tail = readTimestampFromRange(file, Math.max(0, stat.size - tailBytes), stat.size, { fromEnd: true });
54
+ if (tail) return tail;
55
+ if (stat.size > tailBytes) {
56
+ const head = readTimestampFromRange(file, 0, Math.min(stat.size, headBytes), { fromEnd: false });
57
+ if (head) return head;
58
+ }
59
+ return stat.mtimeMs || 0;
60
+ }
61
+
62
+ function readTimestampFromRange(file, start, end, { fromEnd }) {
63
+ if (end <= start) return 0;
64
+ let buf;
65
+ try {
66
+ const fd = fs.openSync(file, "r");
67
+ try {
68
+ buf = Buffer.alloc(end - start);
69
+ fs.readSync(fd, buf, 0, buf.length, start);
70
+ } finally {
71
+ try { fs.closeSync(fd); } catch {}
72
+ }
73
+ } catch {
74
+ return 0;
75
+ }
76
+ const lines = buf.toString("utf8").split("\n");
77
+ const order = fromEnd ? [...lines.keys()].reverse() : [...lines.keys()];
78
+ for (const i of order) {
79
+ const line = lines[i];
80
+ if (!line || !line.includes('"timestamp"')) continue;
81
+ let d;
82
+ try {
83
+ d = JSON.parse(line);
84
+ } catch {
85
+ continue;
86
+ }
87
+ const ts = d?.timestamp || d?.payload?.timestamp;
88
+ if (!ts) continue;
89
+ const t = Date.parse(ts);
90
+ if (!Number.isNaN(t)) return t;
91
+ }
92
+ return 0;
93
+ }
94
+
95
+ function sessionTimestamp(file) {
96
+ return readInternalTimestamp(file) || safeStatMtime(file);
97
+ }
98
+
99
+ export function fmtDate(ms) {
100
+ if (!ms) return " ";
101
+ const d = new Date(ms);
102
+ const p = (n) => String(n).padStart(2, "0");
103
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
104
+ }
105
+
106
+ // Read the first bytes of a file (used for one-line JSONL headers).
107
+ function readHead(file, bytes = 8192) {
108
+ let fd;
109
+ try {
110
+ fd = fs.openSync(file, "r");
111
+ const buf = Buffer.alloc(bytes);
112
+ const n = fs.readSync(fd, buf, 0, bytes, 0);
113
+ return buf.subarray(0, n).toString("utf8");
114
+ } catch {
115
+ return "";
116
+ } finally {
117
+ if (fd !== undefined) {
118
+ try {
119
+ fs.closeSync(fd);
120
+ } catch {}
121
+ }
122
+ }
123
+ }
124
+
125
+ // Registered APX projects come from ~/.apx/config.json — APX does not know
126
+ // every project on disk, so an unregistered project must be passed via --dir.
127
+ function readApxProjects(opts) {
128
+ const cfgPath = path.join(homeDir(opts), ".apx", "config.json");
129
+ let entries = [];
130
+ try {
131
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
132
+ entries = Array.isArray(cfg.projects) ? cfg.projects : [];
133
+ } catch {}
134
+ return entries
135
+ .filter((e) => e && e.path)
136
+ .map((e) => {
137
+ const proj = { path: path.resolve(e.path), name: null, apxId: null };
138
+ try {
139
+ const pj = JSON.parse(
140
+ fs.readFileSync(apcProjectFile(proj.path), "utf8")
141
+ );
142
+ if (pj.name) proj.name = pj.name;
143
+ if (pj.apx_id) proj.apxId = pj.apx_id;
144
+ } catch {}
145
+ if (!proj.name) proj.name = path.basename(proj.path);
146
+ return proj;
147
+ });
148
+ }
149
+
150
+ // Resolve the working directory the user wants sessions for.
151
+ // --dir <path> → explicit path
152
+ // --project <name> → look up a registered APX project
153
+ // neither → null (caller enters discovery mode)
154
+ export function resolveTargetDir(args, opts) {
155
+ const dirFlag = args.flags.dir;
156
+ if (dirFlag && dirFlag !== true) return path.resolve(String(dirFlag));
157
+
158
+ const projFlag = args.flags.project;
159
+ if (projFlag && projFlag !== true) {
160
+ const q = String(projFlag).toLowerCase();
161
+ const projects = readApxProjects(opts);
162
+ const exact =
163
+ projects.find((p) => p.name.toLowerCase() === q) ||
164
+ projects.find((p) => path.basename(p.path).toLowerCase() === q);
165
+ if (exact) return exact.path;
166
+
167
+ const fuzzy = projects.filter(
168
+ (p) =>
169
+ p.name.toLowerCase().includes(q) || p.path.toLowerCase().includes(q)
170
+ );
171
+ if (fuzzy.length === 1) return fuzzy[0].path;
172
+ if (fuzzy.length > 1) {
173
+ throw new Error(
174
+ `--project "${projFlag}" is ambiguous; matches: ${fuzzy
175
+ .map((p) => p.name)
176
+ .join(", ")}`
177
+ );
178
+ }
179
+ const known = projects.length
180
+ ? projects.map((p) => p.name).join(", ")
181
+ : "(none registered)";
182
+ throw new Error(
183
+ `--project "${projFlag}" not found in registered APX projects (${known}). ` +
184
+ `Use --dir <path> for an unregistered project.`
185
+ );
186
+ }
187
+ return null;
188
+ }
189
+
190
+ // ── claude code engine ───────────────────────────────────────────────────────
191
+
192
+ function claudeProjectsDir(opts) {
193
+ return path.join(homeDir(opts), ".claude", "projects");
194
+ }
195
+
196
+ function claudeReadTitle(file) {
197
+ let title = null;
198
+ let lastPrompt = null;
199
+ let text;
200
+ try {
201
+ text = fs.readFileSync(file, "utf8");
202
+ } catch {
203
+ return null;
204
+ }
205
+ for (const line of text.split("\n")) {
206
+ if (!line.includes('"aiTitle"') && !line.includes('"lastPrompt"')) continue;
207
+ let d;
208
+ try {
209
+ d = JSON.parse(line);
210
+ } catch {
211
+ continue;
212
+ }
213
+ if (d.type === "ai-title" && d.aiTitle) title = d.aiTitle;
214
+ else if (d.type === "last-prompt" && d.lastPrompt) lastPrompt = d.lastPrompt;
215
+ }
216
+ return title || lastPrompt || null;
217
+ }
218
+
219
+ // Decode a Claude project folder name back to a cwd-like path. The encoding is
220
+ // lossy (every non-alphanumeric becomes "-"), so this returns the encoded name
221
+ // when no registered APX project maps back to a real cwd.
222
+ function claudeDecodeProject(encoded, opts) {
223
+ const known = readApxProjects(opts);
224
+ const hit = known.find((p) => encodeClaudeProjectPath(p.path) === encoded);
225
+ return hit ? hit.path : null;
226
+ }
227
+
228
+ const claudeEngine = {
229
+ id: "claude",
230
+ label: "Claude Code",
231
+ implemented: true,
232
+ detect(opts) {
233
+ const dir = claudeProjectsDir(opts);
234
+ return fs.existsSync(dir)
235
+ ? { available: true }
236
+ : { available: false, reason: `${dir} not found` };
237
+ },
238
+ // Locate <id>.jsonl across every Claude project folder. Used by
239
+ // `apx session resume <id>` so the user doesn't need to know the cwd.
240
+ findSessionById(id, opts) {
241
+ const root = claudeProjectsDir(opts);
242
+ if (!fs.existsSync(root)) return null;
243
+ for (const name of fs.readdirSync(root)) {
244
+ const file = path.join(root, name, `${id}.jsonl`);
245
+ if (fs.existsSync(file)) {
246
+ return {
247
+ engine: "claude",
248
+ id,
249
+ path: file,
250
+ cwd: claudeDecodeProject(name, opts) || name,
251
+ mtime: sessionTimestamp(file),
252
+ title: claudeReadTitle(file) || "(sin título)",
253
+ };
254
+ }
255
+ }
256
+ return null;
257
+ },
258
+ // Read the full JSONL transcript. Returns raw text plus a tail trimmed to
259
+ // tailBytes (default 64 KB) so callers can show "last N bytes" without
260
+ // loading huge logs into memory.
261
+ readSession(meta, { tailBytes = 64 * 1024 } = {}) {
262
+ if (!meta?.path || !fs.existsSync(meta.path)) {
263
+ return { found: false };
264
+ }
265
+ const raw = fs.readFileSync(meta.path, "utf8");
266
+ return {
267
+ found: true,
268
+ raw,
269
+ tail: raw.length > tailBytes ? raw.slice(-tailBytes) : raw,
270
+ size: raw.length,
271
+ format: "jsonl",
272
+ };
273
+ },
274
+ listProjects(opts) {
275
+ const root = claudeProjectsDir(opts);
276
+ const known = new Map(
277
+ readApxProjects(opts).map((p) => [encodeClaudeProjectPath(p.path), p])
278
+ );
279
+ const out = [];
280
+ for (const name of fs.readdirSync(root)) {
281
+ const dir = path.join(root, name);
282
+ let files;
283
+ try {
284
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
285
+ } catch {
286
+ continue;
287
+ }
288
+ if (files.length === 0) continue;
289
+ const matched = known.get(name);
290
+ out.push({
291
+ key: matched ? matched.name : name,
292
+ dir: matched ? matched.path : null,
293
+ label: matched ? matched.path : name,
294
+ count: files.length,
295
+ mtime: Math.max(...files.map((f) => sessionTimestamp(path.join(dir, f)))),
296
+ });
297
+ }
298
+ return out.sort((a, b) => b.mtime - a.mtime);
299
+ },
300
+ listSessions(dir, opts) {
301
+ const folder = path.join(
302
+ claudeProjectsDir(opts),
303
+ encodeClaudeProjectPath(dir)
304
+ );
305
+ if (!fs.existsSync(folder)) return { found: false, location: folder };
306
+ const sessions = [];
307
+ for (const f of fs.readdirSync(folder)) {
308
+ if (!f.endsWith(".jsonl")) continue;
309
+ const file = path.join(folder, f);
310
+ sessions.push({
311
+ id: f.slice(0, -6),
312
+ mtime: sessionTimestamp(file),
313
+ title: claudeReadTitle(file) || "(sin título)",
314
+ path: file,
315
+ });
316
+ }
317
+ sessions.sort((a, b) => b.mtime - a.mtime);
318
+ return { found: true, location: folder, sessions };
319
+ },
320
+ continueHint() {
321
+ return `claude --continue (run from the project directory)`;
322
+ },
323
+ resumeHint(id) {
324
+ return `claude -p --resume ${id} "your prompt"`;
325
+ },
326
+ };
327
+
328
+ // ── codex engine ─────────────────────────────────────────────────────────────
329
+
330
+ function codexSessionsDir(opts) {
331
+ return path.join(homeDir(opts), ".codex", "sessions");
332
+ }
333
+
334
+ // Codex stores titles in ~/.codex/session_index.jsonl keyed by session id.
335
+ function codexTitleIndex(opts) {
336
+ const file = path.join(homeDir(opts), ".codex", "session_index.jsonl");
337
+ const map = new Map();
338
+ let text;
339
+ try {
340
+ text = fs.readFileSync(file, "utf8");
341
+ } catch {
342
+ return map;
343
+ }
344
+ for (const line of text.split("\n")) {
345
+ if (!line.trim()) continue;
346
+ try {
347
+ const d = JSON.parse(line);
348
+ if (d.id && d.thread_name) map.set(d.id, d.thread_name);
349
+ } catch {}
350
+ }
351
+ return map;
352
+ }
353
+
354
+ // Walk ~/.codex/sessions/YYYY/MM/DD/ collecting rollout-*.jsonl files and
355
+ // reading their session_meta header (first line) for id + cwd. Prefers the
356
+ // internal timestamp over mtime so backups don't scramble the order.
357
+ function codexScanRollouts(opts) {
358
+ const root = codexSessionsDir(opts);
359
+ const found = [];
360
+ const walk = (dir) => {
361
+ let entries;
362
+ try {
363
+ entries = fs.readdirSync(dir, { withFileTypes: true });
364
+ } catch {
365
+ return;
366
+ }
367
+ for (const e of entries) {
368
+ const full = path.join(dir, e.name);
369
+ if (e.isDirectory()) walk(full);
370
+ else if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) {
371
+ const head = readHead(full);
372
+ const id = (head.match(/"id":"([^"]+)"/) || [])[1];
373
+ const cwd = (head.match(/"cwd":"([^"]+)"/) || [])[1];
374
+ if (id) found.push({ id, cwd: cwd || null, mtime: sessionTimestamp(full) });
375
+ }
376
+ }
377
+ };
378
+ walk(root);
379
+ return found;
380
+ }
381
+
382
+ // Scan rollouts but also keep the absolute path so resume-by-id can read the
383
+ // file straight off disk. Old codexScanRollouts dropped the path; this helper
384
+ // keeps it.
385
+ function codexScanRolloutsWithPath(opts) {
386
+ const root = codexSessionsDir(opts);
387
+ const found = [];
388
+ const walk = (dir) => {
389
+ let entries;
390
+ try {
391
+ entries = fs.readdirSync(dir, { withFileTypes: true });
392
+ } catch {
393
+ return;
394
+ }
395
+ for (const e of entries) {
396
+ const full = path.join(dir, e.name);
397
+ if (e.isDirectory()) walk(full);
398
+ else if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) {
399
+ const head = readHead(full);
400
+ const id = (head.match(/"id":"([^"]+)"/) || [])[1];
401
+ const cwd = (head.match(/"cwd":"([^"]+)"/) || [])[1];
402
+ if (id)
403
+ found.push({
404
+ id,
405
+ cwd: cwd || null,
406
+ mtime: sessionTimestamp(full),
407
+ path: full,
408
+ });
409
+ }
410
+ }
411
+ };
412
+ walk(root);
413
+ return found;
414
+ }
415
+
416
+ const codexEngine = {
417
+ id: "codex",
418
+ label: "Codex",
419
+ implemented: true,
420
+ detect(opts) {
421
+ const dir = codexSessionsDir(opts);
422
+ return fs.existsSync(dir)
423
+ ? { available: true }
424
+ : { available: false, reason: `${dir} not found` };
425
+ },
426
+ findSessionById(id, opts) {
427
+ const hit = codexScanRolloutsWithPath(opts).find((r) => r.id === id);
428
+ if (!hit) return null;
429
+ const titles = codexTitleIndex(opts);
430
+ return {
431
+ engine: "codex",
432
+ id,
433
+ path: hit.path,
434
+ cwd: hit.cwd,
435
+ mtime: hit.mtime,
436
+ title: titles.get(id) || "(sin título)",
437
+ };
438
+ },
439
+ readSession(meta, { tailBytes = 64 * 1024 } = {}) {
440
+ if (!meta?.path || !fs.existsSync(meta.path)) {
441
+ return { found: false };
442
+ }
443
+ const raw = fs.readFileSync(meta.path, "utf8");
444
+ return {
445
+ found: true,
446
+ raw,
447
+ tail: raw.length > tailBytes ? raw.slice(-tailBytes) : raw,
448
+ size: raw.length,
449
+ format: "jsonl",
450
+ };
451
+ },
452
+ listProjects(opts) {
453
+ const byCwd = new Map();
454
+ for (const r of codexScanRollouts(opts)) {
455
+ if (!r.cwd) continue;
456
+ const cur = byCwd.get(r.cwd) || { count: 0, mtime: 0 };
457
+ cur.count++;
458
+ if (r.mtime > cur.mtime) cur.mtime = r.mtime;
459
+ byCwd.set(r.cwd, cur);
460
+ }
461
+ return [...byCwd.entries()]
462
+ .map(([cwd, v]) => ({
463
+ key: cwd,
464
+ dir: cwd,
465
+ label: cwd,
466
+ count: v.count,
467
+ mtime: v.mtime,
468
+ }))
469
+ .sort((a, b) => b.mtime - a.mtime);
470
+ },
471
+ listSessions(dir, opts) {
472
+ const titles = codexTitleIndex(opts);
473
+ const sessions = codexScanRolloutsWithPath(opts)
474
+ .filter((r) => r.cwd === dir)
475
+ .map((r) => ({
476
+ id: r.id,
477
+ mtime: r.mtime,
478
+ title: titles.get(r.id) || "(sin título)",
479
+ path: r.path,
480
+ }))
481
+ .sort((a, b) => b.mtime - a.mtime);
482
+ return { found: sessions.length > 0, location: dir, sessions };
483
+ },
484
+ continueHint() {
485
+ return `codex resume --last`;
486
+ },
487
+ resumeHint(id) {
488
+ return `codex exec resume ${id} "your prompt" (interactive: codex resume ${id})`;
489
+ },
490
+ };
491
+
492
+ // ── apx engine (default) ─────────────────────────────────────────────────────
493
+
494
+
495
+ // APX sessions are .md with ISO timestamps in frontmatter — prefer those over
496
+ // mtime for the same backup-safety reason the JSONL engines do.
497
+ function apxSessionTimestamp(fm, file) {
498
+ for (const field of ["completed", "started"]) {
499
+ const v = fm?.[field];
500
+ if (!v || typeof v !== "string") continue;
501
+ const t = Date.parse(v);
502
+ if (!Number.isNaN(t)) return t;
503
+ }
504
+ return safeStatMtime(file);
505
+ }
506
+
507
+ function apxStorageRoot(opts) {
508
+ return path.join(homeDir(opts), ".apx", "projects");
509
+ }
510
+
511
+ const apxEngine = {
512
+ id: "apx",
513
+ label: "APX",
514
+ implemented: true,
515
+ detect() {
516
+ return { available: true };
517
+ },
518
+ // Walk every registered APX project looking for a session file whose
519
+ // frontmatter `id:` or filename (without .md) equals the given id.
520
+ findSessionById(id, opts) {
521
+ const projects = readApxProjects(opts);
522
+ for (const proj of projects) {
523
+ if (!proj.apxId) continue;
524
+ const agentsDir = path.join(apxStorageRoot(opts), proj.apxId, "agents");
525
+ if (!fs.existsSync(agentsDir)) continue;
526
+ for (const slug of fs.readdirSync(agentsDir)) {
527
+ const sdir = path.join(agentsDir, slug, "sessions");
528
+ let files;
529
+ try {
530
+ files = fs.readdirSync(sdir);
531
+ } catch {
532
+ continue;
533
+ }
534
+ for (const f of files) {
535
+ if (!f.endsWith(".md")) continue;
536
+ const file = path.join(sdir, f);
537
+ const baseId = f.slice(0, -3);
538
+ const fm = parseFrontmatter(fs.readFileSync(file, "utf8"));
539
+ if (fm.id === id || baseId === id) {
540
+ return {
541
+ engine: "apx",
542
+ id: fm.id || baseId,
543
+ path: file,
544
+ cwd: proj.path,
545
+ mtime: apxSessionTimestamp(fm, file),
546
+ title: fm.title || baseId,
547
+ agentSlug: slug,
548
+ apxId: proj.apxId,
549
+ projectName: proj.name,
550
+ externalSessionPath: fm.external_session_path || null,
551
+ };
552
+ }
553
+ }
554
+ }
555
+ }
556
+ return null;
557
+ },
558
+ // Read the APX session markdown. If the frontmatter points to an external
559
+ // transcript (the JSONL the underlying engine left behind), include its tail
560
+ // too — same data shape callers see for claude/codex direct reads.
561
+ readSession(meta, { tailBytes = 64 * 1024 } = {}) {
562
+ if (!meta?.path || !fs.existsSync(meta.path)) {
563
+ return { found: false };
564
+ }
565
+ const raw = fs.readFileSync(meta.path, "utf8");
566
+ const result = {
567
+ found: true,
568
+ raw,
569
+ tail: raw,
570
+ size: raw.length,
571
+ format: "markdown",
572
+ };
573
+ if (meta.externalSessionPath && fs.existsSync(meta.externalSessionPath)) {
574
+ const ext = fs.readFileSync(meta.externalSessionPath, "utf8");
575
+ result.external = {
576
+ path: meta.externalSessionPath,
577
+ raw: ext,
578
+ tail: ext.length > tailBytes ? ext.slice(-tailBytes) : ext,
579
+ size: ext.length,
580
+ format: "jsonl",
581
+ };
582
+ }
583
+ return result;
584
+ },
585
+ listProjects(opts) {
586
+ return readApxProjects(opts).map((p) => {
587
+ let count = 0;
588
+ let mtime = 0;
589
+ if (p.apxId) {
590
+ const agentsDir = path.join(apxStorageRoot(opts), p.apxId, "agents");
591
+ try {
592
+ for (const slug of fs.readdirSync(agentsDir)) {
593
+ const sdir = path.join(agentsDir, slug, "sessions");
594
+ try {
595
+ for (const f of fs.readdirSync(sdir)) {
596
+ if (!f.endsWith(".md")) continue;
597
+ count++;
598
+ const file = path.join(sdir, f);
599
+ const fm = parseFrontmatter(fs.readFileSync(file, "utf8"));
600
+ mtime = Math.max(mtime, apxSessionTimestamp(fm, file));
601
+ }
602
+ } catch {}
603
+ }
604
+ } catch {}
605
+ }
606
+ return { key: p.name, dir: p.path, label: p.path, count, mtime };
607
+ });
608
+ },
609
+ listSessions(dir, opts) {
610
+ let apxId = null;
611
+ try {
612
+ const pj = JSON.parse(
613
+ fs.readFileSync(apcProjectFile(dir), "utf8")
614
+ );
615
+ apxId = pj.apx_id || null;
616
+ } catch {}
617
+ if (!apxId) return { found: false, location: dir };
618
+ const agentsDir = path.join(apxStorageRoot(opts), apxId, "agents");
619
+ if (!fs.existsSync(agentsDir)) return { found: false, location: agentsDir };
620
+ const sessions = [];
621
+ for (const slug of fs.readdirSync(agentsDir)) {
622
+ const sdir = path.join(agentsDir, slug, "sessions");
623
+ let files;
624
+ try {
625
+ files = fs.readdirSync(sdir);
626
+ } catch {
627
+ continue;
628
+ }
629
+ for (const f of files) {
630
+ if (!f.endsWith(".md")) continue;
631
+ const file = path.join(sdir, f);
632
+ const fm = parseFrontmatter(fs.readFileSync(file, "utf8"));
633
+ sessions.push({
634
+ id: fm.id || f.slice(0, -3),
635
+ mtime: apxSessionTimestamp(fm, file),
636
+ title: `[${slug}] ${fm.title || "(sin título)"}`,
637
+ path: file,
638
+ });
639
+ }
640
+ }
641
+ sessions.sort((a, b) => b.mtime - a.mtime);
642
+ return { found: sessions.length > 0, location: agentsDir, sessions };
643
+ },
644
+ continueHint() {
645
+ return `apx session list (run from the project directory)`;
646
+ },
647
+ resumeHint(id) {
648
+ return `apx session resume ${id}`;
649
+ },
650
+ };
651
+
652
+ // ── antigravity engine (detected, listing not implemented) ───────────────────
653
+
654
+ const antigravityEngine = {
655
+ id: "antigravity",
656
+ label: "Antigravity",
657
+ implemented: false,
658
+ detect(opts) {
659
+ const candidates = [
660
+ path.join(homeDir(opts), ".antigravity"),
661
+ path.join(
662
+ homeDir(opts),
663
+ "Library",
664
+ "Application Support",
665
+ "Antigravity"
666
+ ),
667
+ ];
668
+ const hit = candidates.find((c) => fs.existsSync(c));
669
+ return hit
670
+ ? { available: true }
671
+ : { available: false, reason: "Antigravity not installed" };
672
+ },
673
+ };
674
+
675
+ export const ENGINES = {
676
+ apx: apxEngine,
677
+ claude: claudeEngine,
678
+ codex: codexEngine,
679
+ antigravity: antigravityEngine,
680
+ };
681
+
682
+ // ── cross-engine helpers (used by `apx session resume <id>` etc.) ────────────
683
+
684
+ /**
685
+ * Search every implemented + detected engine for a session whose id matches.
686
+ * Returns an array of hits (0, 1, or more if the same id exists in multiple
687
+ * engines — callers should disambiguate via --engine).
688
+ */
689
+ export function findSessionAcrossEngines(id, opts = {}) {
690
+ const hits = [];
691
+ for (const engine of Object.values(ENGINES)) {
692
+ if (!engine.implemented || typeof engine.findSessionById !== "function") continue;
693
+ const detected = engine.detect(opts);
694
+ if (!detected.available) continue;
695
+ try {
696
+ const hit = engine.findSessionById(id, opts);
697
+ if (hit) hits.push(hit);
698
+ } catch {}
699
+ }
700
+ return hits;
701
+ }
702
+
703
+ /** Same as findSessionAcrossEngines but limited to one engine id. */
704
+ export function findSessionInEngine(engineId, id, opts = {}) {
705
+ const engine = ENGINES[engineId];
706
+ if (!engine) return null;
707
+ if (!engine.implemented || typeof engine.findSessionById !== "function") return null;
708
+ const detected = engine.detect(opts);
709
+ if (!detected.available) return null;
710
+ return engine.findSessionById(id, opts);
711
+ }
712
+
713
+ /**
714
+ * Enumerate every session across every detected+implemented engine, returning
715
+ * flat rows of { engine, id, title, mtime, cwd, path }. Used by `apx session
716
+ * find`. When `dir` is given, only that working directory is scanned (cheap);
717
+ * otherwise every project the engine knows about is walked.
718
+ *
719
+ * Caveat: an engine can only enumerate a project when it can resolve its cwd.
720
+ * Codex always records cwd; APX uses registered projects; Claude can only list
721
+ * folders that map back to a registered APX project (its folder names are a
722
+ * lossy encoding of the original path). Scope with --dir to reach the rest.
723
+ */
724
+ export function collectAllSessions(opts = {}, { dir = null, engineId = null } = {}) {
725
+ const out = [];
726
+ for (const engine of Object.values(ENGINES)) {
727
+ if (engineId && engine.id !== engineId) continue;
728
+ if (!engine.implemented || typeof engine.listSessions !== "function") continue;
729
+ if (!engine.detect(opts).available) continue;
730
+
731
+ const dirs = [];
732
+ if (dir) {
733
+ dirs.push(dir);
734
+ } else {
735
+ let projects = [];
736
+ try {
737
+ projects = engine.listProjects(opts) || [];
738
+ } catch {}
739
+ for (const p of projects) if (p.dir) dirs.push(p.dir);
740
+ }
741
+
742
+ for (const d of dirs) {
743
+ let res;
744
+ try {
745
+ res = engine.listSessions(d, opts);
746
+ } catch {
747
+ continue;
748
+ }
749
+ if (!res || !res.found) continue;
750
+ for (const s of res.sessions || []) {
751
+ out.push({
752
+ engine: engine.id,
753
+ id: s.id,
754
+ title: s.title || "",
755
+ mtime: s.mtime || 0,
756
+ cwd: d,
757
+ path: s.path || null,
758
+ });
759
+ }
760
+ }
761
+ }
762
+ return out;
763
+ }
764
+
765
+ // Read a session file off disk for content (deep) search. Cheap: we already
766
+ // have the absolute path from collectAllSessions, so no per-candidate rescan.
767
+ function sessionContainsText(row, needle) {
768
+ if (!row.path) return false;
769
+ let text;
770
+ try {
771
+ text = fs.readFileSync(row.path, "utf8");
772
+ } catch {
773
+ return false;
774
+ }
775
+ return text.toLowerCase().includes(needle);
776
+ }
777
+
778
+ // Filter collected session rows by a free-text query. Always matches the
779
+ // title; with deep=true also scans transcript content (slower). De-dupes by
780
+ // engine:id and returns full rows + a `match` field ("title"|"content"),
781
+ // newest first. Shared core for the CLI (apx session find) and the daemon
782
+ // (GET /sessions?q=…) so terminal and web search behave identically.
783
+ export function filterSessionsByQuery(rows, { query, deep = false, limit = 0 } = {}) {
784
+ const needle = String(query || "").trim().toLowerCase();
785
+ if (!needle) return [];
786
+ const seen = new Set();
787
+ const matches = [];
788
+ for (const row of rows) {
789
+ const titleHit = String(row.title).toLowerCase().includes(needle);
790
+ let where = titleHit ? "title" : null;
791
+ if (!titleHit && deep && sessionContainsText(row, needle)) where = "content";
792
+ if (!where) continue;
793
+ const key = `${row.engine}:${row.id}`;
794
+ if (seen.has(key)) continue;
795
+ seen.add(key);
796
+ matches.push({ ...row, match: where });
797
+ }
798
+ matches.sort((a, b) => b.mtime - a.mtime);
799
+ return limit > 0 ? matches.slice(0, limit) : matches;
800
+ }