@vibelet/cli 0.1.38 → 1.0.1

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 (323) hide show
  1. package/README.md +80 -0
  2. package/bin/cloudflared-quick-tunnel.mjs +11 -0
  3. package/bin/cloudflared-resolver.mjs +171 -0
  4. package/bin/vibelet-runtime-policy.mjs +36 -0
  5. package/bin/vibelet.cjs +12 -0
  6. package/bin/vibelet.mjs +1235 -0
  7. package/dist/index.cjs +126 -0
  8. package/package.json +24 -22
  9. package/app.json +0 -5
  10. package/dist/advertised-hosts.d.ts +0 -34
  11. package/dist/advertised-hosts.d.ts.map +0 -1
  12. package/dist/advertised-hosts.js +0 -176
  13. package/dist/advertised-hosts.js.map +0 -1
  14. package/dist/advertised-hosts.test.d.ts +0 -2
  15. package/dist/advertised-hosts.test.d.ts.map +0 -1
  16. package/dist/advertised-hosts.test.js +0 -96
  17. package/dist/advertised-hosts.test.js.map +0 -1
  18. package/dist/audit.d.ts +0 -30
  19. package/dist/audit.d.ts.map +0 -1
  20. package/dist/audit.js +0 -73
  21. package/dist/audit.js.map +0 -1
  22. package/dist/audit.test.d.ts +0 -2
  23. package/dist/audit.test.d.ts.map +0 -1
  24. package/dist/audit.test.js +0 -33
  25. package/dist/audit.test.js.map +0 -1
  26. package/dist/auth.d.ts +0 -6
  27. package/dist/auth.d.ts.map +0 -1
  28. package/dist/auth.js +0 -27
  29. package/dist/auth.js.map +0 -1
  30. package/dist/claude-hooks.d.ts +0 -58
  31. package/dist/claude-hooks.d.ts.map +0 -1
  32. package/dist/claude-hooks.js +0 -129
  33. package/dist/claude-hooks.js.map +0 -1
  34. package/dist/cli-version.d.ts +0 -3
  35. package/dist/cli-version.d.ts.map +0 -1
  36. package/dist/cli-version.js +0 -35
  37. package/dist/cli-version.js.map +0 -1
  38. package/dist/cli-version.test.d.ts +0 -2
  39. package/dist/cli-version.test.d.ts.map +0 -1
  40. package/dist/cli-version.test.js +0 -38
  41. package/dist/cli-version.test.js.map +0 -1
  42. package/dist/config.d.ts +0 -30
  43. package/dist/config.d.ts.map +0 -1
  44. package/dist/config.js +0 -327
  45. package/dist/config.js.map +0 -1
  46. package/dist/config.test.d.ts +0 -2
  47. package/dist/config.test.d.ts.map +0 -1
  48. package/dist/config.test.js +0 -184
  49. package/dist/config.test.js.map +0 -1
  50. package/dist/dev-auth.test.d.ts +0 -2
  51. package/dist/dev-auth.test.d.ts.map +0 -1
  52. package/dist/dev-auth.test.js +0 -154
  53. package/dist/dev-auth.test.js.map +0 -1
  54. package/dist/dev-script.test.d.ts +0 -2
  55. package/dist/dev-script.test.d.ts.map +0 -1
  56. package/dist/dev-script.test.js +0 -412
  57. package/dist/dev-script.test.js.map +0 -1
  58. package/dist/drivers/claude.d.ts +0 -34
  59. package/dist/drivers/claude.d.ts.map +0 -1
  60. package/dist/drivers/claude.js +0 -413
  61. package/dist/drivers/claude.js.map +0 -1
  62. package/dist/drivers/claude.test.d.ts +0 -2
  63. package/dist/drivers/claude.test.d.ts.map +0 -1
  64. package/dist/drivers/claude.test.js +0 -951
  65. package/dist/drivers/claude.test.js.map +0 -1
  66. package/dist/drivers/codex.d.ts +0 -38
  67. package/dist/drivers/codex.d.ts.map +0 -1
  68. package/dist/drivers/codex.js +0 -771
  69. package/dist/drivers/codex.js.map +0 -1
  70. package/dist/drivers/codex.test.d.ts +0 -2
  71. package/dist/drivers/codex.test.d.ts.map +0 -1
  72. package/dist/drivers/codex.test.js +0 -939
  73. package/dist/drivers/codex.test.js.map +0 -1
  74. package/dist/drivers/types.d.ts +0 -14
  75. package/dist/drivers/types.d.ts.map +0 -1
  76. package/dist/drivers/types.js +0 -2
  77. package/dist/drivers/types.js.map +0 -1
  78. package/dist/e2e.test.d.ts +0 -2
  79. package/dist/e2e.test.d.ts.map +0 -1
  80. package/dist/e2e.test.js +0 -111
  81. package/dist/e2e.test.js.map +0 -1
  82. package/dist/identity.d.ts +0 -10
  83. package/dist/identity.d.ts.map +0 -1
  84. package/dist/identity.js +0 -66
  85. package/dist/identity.js.map +0 -1
  86. package/dist/identity.test.d.ts +0 -2
  87. package/dist/identity.test.d.ts.map +0 -1
  88. package/dist/identity.test.js +0 -25
  89. package/dist/identity.test.js.map +0 -1
  90. package/dist/index-entry.test.d.ts +0 -2
  91. package/dist/index-entry.test.d.ts.map +0 -1
  92. package/dist/index-entry.test.js +0 -272
  93. package/dist/index-entry.test.js.map +0 -1
  94. package/dist/index.d.ts +0 -2
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/index.js +0 -707
  97. package/dist/index.js.map +0 -1
  98. package/dist/logger.d.ts +0 -31
  99. package/dist/logger.d.ts.map +0 -1
  100. package/dist/logger.js +0 -75
  101. package/dist/logger.js.map +0 -1
  102. package/dist/metrics.d.ts +0 -52
  103. package/dist/metrics.d.ts.map +0 -1
  104. package/dist/metrics.js +0 -89
  105. package/dist/metrics.js.map +0 -1
  106. package/dist/pairing-store.d.ts +0 -29
  107. package/dist/pairing-store.d.ts.map +0 -1
  108. package/dist/pairing-store.js +0 -131
  109. package/dist/pairing-store.js.map +0 -1
  110. package/dist/pairing-store.test.d.ts +0 -2
  111. package/dist/pairing-store.test.d.ts.map +0 -1
  112. package/dist/pairing-store.test.js +0 -47
  113. package/dist/pairing-store.test.js.map +0 -1
  114. package/dist/paths.d.ts +0 -16
  115. package/dist/paths.d.ts.map +0 -1
  116. package/dist/paths.js +0 -18
  117. package/dist/paths.js.map +0 -1
  118. package/dist/perf-compare.d.ts +0 -13
  119. package/dist/perf-compare.d.ts.map +0 -1
  120. package/dist/perf-compare.js +0 -125
  121. package/dist/perf-compare.js.map +0 -1
  122. package/dist/port-conflict.d.ts +0 -9
  123. package/dist/port-conflict.d.ts.map +0 -1
  124. package/dist/port-conflict.js +0 -33
  125. package/dist/port-conflict.js.map +0 -1
  126. package/dist/port-conflict.test.d.ts +0 -2
  127. package/dist/port-conflict.test.d.ts.map +0 -1
  128. package/dist/port-conflict.test.js +0 -38
  129. package/dist/port-conflict.test.js.map +0 -1
  130. package/dist/process-scanner.d.ts +0 -43
  131. package/dist/process-scanner.d.ts.map +0 -1
  132. package/dist/process-scanner.js +0 -453
  133. package/dist/process-scanner.js.map +0 -1
  134. package/dist/process-scanner.perf.test.d.ts +0 -2
  135. package/dist/process-scanner.perf.test.d.ts.map +0 -1
  136. package/dist/process-scanner.perf.test.js +0 -186
  137. package/dist/process-scanner.perf.test.js.map +0 -1
  138. package/dist/process-scanner.test.d.ts +0 -2
  139. package/dist/process-scanner.test.d.ts.map +0 -1
  140. package/dist/process-scanner.test.js +0 -399
  141. package/dist/process-scanner.test.js.map +0 -1
  142. package/dist/push-protocol.d.ts +0 -15
  143. package/dist/push-protocol.d.ts.map +0 -1
  144. package/dist/push-protocol.js +0 -23
  145. package/dist/push-protocol.js.map +0 -1
  146. package/dist/push-protocol.test.d.ts +0 -2
  147. package/dist/push-protocol.test.d.ts.map +0 -1
  148. package/dist/push-protocol.test.js +0 -57
  149. package/dist/push-protocol.test.js.map +0 -1
  150. package/dist/push-store.d.ts +0 -22
  151. package/dist/push-store.d.ts.map +0 -1
  152. package/dist/push-store.js +0 -103
  153. package/dist/push-store.js.map +0 -1
  154. package/dist/push-store.test.d.ts +0 -2
  155. package/dist/push-store.test.d.ts.map +0 -1
  156. package/dist/push-store.test.js +0 -79
  157. package/dist/push-store.test.js.map +0 -1
  158. package/dist/push.d.ts +0 -65
  159. package/dist/push.d.ts.map +0 -1
  160. package/dist/push.js +0 -202
  161. package/dist/push.js.map +0 -1
  162. package/dist/push.test.d.ts +0 -2
  163. package/dist/push.test.d.ts.map +0 -1
  164. package/dist/push.test.js +0 -199
  165. package/dist/push.test.js.map +0 -1
  166. package/dist/safe-stdio.d.ts +0 -3
  167. package/dist/safe-stdio.d.ts.map +0 -1
  168. package/dist/safe-stdio.js +0 -46
  169. package/dist/safe-stdio.js.map +0 -1
  170. package/dist/scanner.d.ts +0 -30
  171. package/dist/scanner.d.ts.map +0 -1
  172. package/dist/scanner.js +0 -859
  173. package/dist/scanner.js.map +0 -1
  174. package/dist/scanner.perf.test.d.ts +0 -2
  175. package/dist/scanner.perf.test.d.ts.map +0 -1
  176. package/dist/scanner.perf.test.js +0 -320
  177. package/dist/scanner.perf.test.js.map +0 -1
  178. package/dist/scanner.test.d.ts +0 -2
  179. package/dist/scanner.test.d.ts.map +0 -1
  180. package/dist/scanner.test.js +0 -948
  181. package/dist/scanner.test.js.map +0 -1
  182. package/dist/session-inventory.d.ts +0 -63
  183. package/dist/session-inventory.d.ts.map +0 -1
  184. package/dist/session-inventory.js +0 -525
  185. package/dist/session-inventory.js.map +0 -1
  186. package/dist/session-inventory.perf.test.d.ts +0 -2
  187. package/dist/session-inventory.perf.test.d.ts.map +0 -1
  188. package/dist/session-inventory.perf.test.js +0 -220
  189. package/dist/session-inventory.perf.test.js.map +0 -1
  190. package/dist/session-inventory.test.d.ts +0 -2
  191. package/dist/session-inventory.test.d.ts.map +0 -1
  192. package/dist/session-inventory.test.js +0 -712
  193. package/dist/session-inventory.test.js.map +0 -1
  194. package/dist/session-manager.d.ts +0 -75
  195. package/dist/session-manager.d.ts.map +0 -1
  196. package/dist/session-manager.js +0 -1515
  197. package/dist/session-manager.js.map +0 -1
  198. package/dist/session-manager.test.d.ts +0 -2
  199. package/dist/session-manager.test.d.ts.map +0 -1
  200. package/dist/session-manager.test.js +0 -2861
  201. package/dist/session-manager.test.js.map +0 -1
  202. package/dist/session-store.d.ts +0 -42
  203. package/dist/session-store.d.ts.map +0 -1
  204. package/dist/session-store.js +0 -163
  205. package/dist/session-store.js.map +0 -1
  206. package/dist/session-store.test.d.ts +0 -2
  207. package/dist/session-store.test.d.ts.map +0 -1
  208. package/dist/session-store.test.js +0 -236
  209. package/dist/session-store.test.js.map +0 -1
  210. package/dist/session-title.d.ts +0 -6
  211. package/dist/session-title.d.ts.map +0 -1
  212. package/dist/session-title.js +0 -105
  213. package/dist/session-title.js.map +0 -1
  214. package/dist/session-title.perf.test.d.ts +0 -2
  215. package/dist/session-title.perf.test.d.ts.map +0 -1
  216. package/dist/session-title.perf.test.js +0 -99
  217. package/dist/session-title.perf.test.js.map +0 -1
  218. package/dist/session-title.test.d.ts +0 -2
  219. package/dist/session-title.test.d.ts.map +0 -1
  220. package/dist/session-title.test.js +0 -199
  221. package/dist/session-title.test.js.map +0 -1
  222. package/dist/shutdown-endpoint.test.d.ts +0 -2
  223. package/dist/shutdown-endpoint.test.d.ts.map +0 -1
  224. package/dist/shutdown-endpoint.test.js +0 -93
  225. package/dist/shutdown-endpoint.test.js.map +0 -1
  226. package/dist/storage-housekeeping.d.ts +0 -28
  227. package/dist/storage-housekeeping.d.ts.map +0 -1
  228. package/dist/storage-housekeeping.js +0 -76
  229. package/dist/storage-housekeeping.js.map +0 -1
  230. package/dist/storage-housekeeping.test.d.ts +0 -2
  231. package/dist/storage-housekeeping.test.d.ts.map +0 -1
  232. package/dist/storage-housekeeping.test.js +0 -65
  233. package/dist/storage-housekeeping.test.js.map +0 -1
  234. package/dist/test-daemon-harness.d.ts +0 -31
  235. package/dist/test-daemon-harness.d.ts.map +0 -1
  236. package/dist/test-daemon-harness.js +0 -337
  237. package/dist/test-daemon-harness.js.map +0 -1
  238. package/dist/token-auth.test.d.ts +0 -2
  239. package/dist/token-auth.test.d.ts.map +0 -1
  240. package/dist/token-auth.test.js +0 -52
  241. package/dist/token-auth.test.js.map +0 -1
  242. package/dist/utils.d.ts +0 -4
  243. package/dist/utils.d.ts.map +0 -1
  244. package/dist/utils.js +0 -40
  245. package/dist/utils.js.map +0 -1
  246. package/dist/utils.test.d.ts +0 -2
  247. package/dist/utils.test.d.ts.map +0 -1
  248. package/dist/utils.test.js +0 -54
  249. package/dist/utils.test.js.map +0 -1
  250. package/dist/ws-data.d.ts +0 -4
  251. package/dist/ws-data.d.ts.map +0 -1
  252. package/dist/ws-data.js +0 -20
  253. package/dist/ws-data.js.map +0 -1
  254. package/dist/ws-data.test.d.ts +0 -2
  255. package/dist/ws-data.test.d.ts.map +0 -1
  256. package/dist/ws-data.test.js +0 -17
  257. package/dist/ws-data.test.js.map +0 -1
  258. package/perf-reporter.mjs +0 -138
  259. package/scripts/build-release.mjs +0 -41
  260. package/scripts/dev.mjs +0 -537
  261. package/src/advertised-hosts.test.ts +0 -125
  262. package/src/advertised-hosts.ts +0 -225
  263. package/src/audit.test.ts +0 -38
  264. package/src/audit.ts +0 -117
  265. package/src/auth.ts +0 -31
  266. package/src/claude-hooks.ts +0 -195
  267. package/src/cli-version.test.ts +0 -36
  268. package/src/cli-version.ts +0 -46
  269. package/src/config.test.ts +0 -254
  270. package/src/config.ts +0 -324
  271. package/src/dev-auth.test.ts +0 -183
  272. package/src/dev-script.test.ts +0 -511
  273. package/src/drivers/claude.test.ts +0 -1186
  274. package/src/drivers/claude.ts +0 -443
  275. package/src/drivers/codex.test.ts +0 -1096
  276. package/src/drivers/codex.ts +0 -879
  277. package/src/drivers/types.ts +0 -15
  278. package/src/e2e.test.ts +0 -139
  279. package/src/identity.test.ts +0 -26
  280. package/src/identity.ts +0 -82
  281. package/src/index-entry.test.ts +0 -336
  282. package/src/index.ts +0 -781
  283. package/src/logger.ts +0 -112
  284. package/src/metrics.ts +0 -117
  285. package/src/pairing-store.test.ts +0 -53
  286. package/src/pairing-store.ts +0 -154
  287. package/src/paths.ts +0 -19
  288. package/src/perf-compare.ts +0 -164
  289. package/src/port-conflict.test.ts +0 -45
  290. package/src/port-conflict.ts +0 -44
  291. package/src/process-scanner.perf.test.ts +0 -222
  292. package/src/process-scanner.test.ts +0 -575
  293. package/src/process-scanner.ts +0 -514
  294. package/src/push-protocol.test.ts +0 -74
  295. package/src/push-protocol.ts +0 -36
  296. package/src/push-store.test.ts +0 -89
  297. package/src/push-store.ts +0 -126
  298. package/src/push.test.ts +0 -234
  299. package/src/push.ts +0 -318
  300. package/src/safe-stdio.ts +0 -51
  301. package/src/scanner.perf.test.ts +0 -359
  302. package/src/scanner.test.ts +0 -1045
  303. package/src/scanner.ts +0 -924
  304. package/src/session-inventory.perf.test.ts +0 -250
  305. package/src/session-inventory.test.ts +0 -1002
  306. package/src/session-inventory.ts +0 -721
  307. package/src/session-manager.test.ts +0 -3430
  308. package/src/session-manager.ts +0 -1775
  309. package/src/session-store.test.ts +0 -276
  310. package/src/session-store.ts +0 -202
  311. package/src/session-title.perf.test.ts +0 -118
  312. package/src/session-title.test.ts +0 -286
  313. package/src/session-title.ts +0 -108
  314. package/src/shutdown-endpoint.test.ts +0 -95
  315. package/src/storage-housekeeping.test.ts +0 -78
  316. package/src/storage-housekeeping.ts +0 -111
  317. package/src/test-daemon-harness.ts +0 -410
  318. package/src/token-auth.test.ts +0 -67
  319. package/src/utils.test.ts +0 -65
  320. package/src/utils.ts +0 -47
  321. package/src/ws-data.test.ts +0 -20
  322. package/src/ws-data.ts +0 -26
  323. package/tsconfig.json +0 -12
package/src/scanner.ts DELETED
@@ -1,924 +0,0 @@
1
- import { readdir, stat, readFile, open } from 'fs/promises';
2
- import { createReadStream } from 'fs';
3
- import { createInterface } from 'readline';
4
- import { join, basename } from 'path';
5
- import { homedir } from 'os';
6
- import type {
7
- AgentType,
8
- ApprovalMode,
9
- SessionInfo,
10
- SessionSource,
11
- } from '@vibelet/shared';
12
- import { preferSessionTitle } from './session-title.js';
13
- import { createIdleRuntime } from './session-inventory.js';
14
- import { logger as rootLogger } from './logger.js';
15
-
16
- const log = rootLogger.child({ module: 'scanner' });
17
-
18
- // ---------------------------------------------------------------------------
19
- // TTL cache helper
20
- // ---------------------------------------------------------------------------
21
- interface CacheEntry<T> { value: T; expiresAt: number }
22
-
23
- function createTtlCache<T>(ttlMs: number) {
24
- const store = new Map<string, CacheEntry<T>>();
25
- return {
26
- get(key: string): T | undefined {
27
- const entry = store.get(key);
28
- if (!entry) return undefined;
29
- if (Date.now() > entry.expiresAt) { store.delete(key); return undefined; }
30
- return entry.value;
31
- },
32
- set(key: string, value: T): void {
33
- store.set(key, { value, expiresAt: Date.now() + ttlMs });
34
- },
35
- clear(): void { store.clear(); },
36
- };
37
- }
38
-
39
- const SCAN_CACHE_TTL = 30_000; // 30s — explicit invalidation on mutations keeps it fresh
40
- const scannerCache = createTtlCache<SessionInfo[]>(SCAN_CACHE_TTL);
41
- const searchCache = createTtlCache<Set<string>>(SCAN_CACHE_TTL);
42
- const scannerInflight = new Map<string, Promise<SessionInfo[]>>();
43
- const searchInflight = new Map<string, Promise<Set<string>>>();
44
- const FILE_READ_CONCURRENCY = 24;
45
-
46
- /** Invalidate all scanner caches. Call after session creation/deletion. */
47
- export function invalidateScannerCache(): void {
48
- scannerCache.clear();
49
- searchCache.clear();
50
- }
51
-
52
- export interface SessionFileMeta {
53
- sessionId: string;
54
- agent: AgentType;
55
- cwd: string;
56
- title?: string;
57
- approvalMode?: ApprovalMode;
58
- createdAt: string;
59
- lastActivityAt: string;
60
- filePath: string;
61
- }
62
-
63
- export interface SessionRuntimeHints {
64
- isResponding?: boolean;
65
- partialReplyText?: string;
66
- }
67
-
68
- function encodeCwd(cwd: string): string {
69
- return cwd.replace(/[^a-zA-Z0-9]/g, '-');
70
- }
71
-
72
- async function mapWithConcurrency<T, R>(
73
- items: T[],
74
- concurrency: number,
75
- worker: (item: T, index: number) => Promise<R>,
76
- ): Promise<R[]> {
77
- if (items.length === 0) return [];
78
-
79
- const limit = Math.max(1, Math.min(concurrency, items.length));
80
- const results = new Array<R>(items.length);
81
- let nextIndex = 0;
82
-
83
- await Promise.all(Array.from({ length: limit }, async () => {
84
- while (true) {
85
- const index = nextIndex;
86
- nextIndex += 1;
87
- if (index >= items.length) return;
88
- results[index] = await worker(items[index], index);
89
- }
90
- }));
91
-
92
- return results;
93
- }
94
-
95
- /** Yield up to `maxLines` non-empty lines from text without splitting the entire string. */
96
- function* iterateFirstLines(text: string, maxLines: number): Generator<string> {
97
- let start = 0;
98
- let yielded = 0;
99
- while (start < text.length && yielded < maxLines) {
100
- let end = text.indexOf('\n', start);
101
- if (end === -1) end = text.length;
102
- const line = text.substring(start, end);
103
- start = end + 1;
104
- if (line.length === 0 || line.trim().length === 0) continue;
105
- yield line;
106
- yielded += 1;
107
- }
108
- }
109
-
110
- /**
111
- * Read the head of a file (up to `maxBytes`) instead of the entire file.
112
- * For files smaller than `maxBytes`, this is equivalent to readFile.
113
- * For large files (100MB+), this avoids loading the entire content into memory.
114
- */
115
- const HEAD_READ_MAX_BYTES = 512 * 1024; // 512 KB — enough for 64-96 JSONL lines
116
- const TAIL_READ_MAX_BYTES = 256 * 1024; // 256 KB — enough to capture recent mode changes
117
-
118
- async function readFileHead(filePath: string, fileSize: number): Promise<string> {
119
- if (fileSize <= HEAD_READ_MAX_BYTES) {
120
- return readFile(filePath, 'utf-8');
121
- }
122
- const fd = await open(filePath, 'r');
123
- try {
124
- const buf = Buffer.alloc(HEAD_READ_MAX_BYTES);
125
- const { bytesRead } = await fd.read(buf, 0, HEAD_READ_MAX_BYTES, 0);
126
- return buf.toString('utf-8', 0, bytesRead);
127
- } finally {
128
- await fd.close();
129
- }
130
- }
131
-
132
- async function readFileTail(filePath: string, fileSize: number): Promise<string> {
133
- if (fileSize <= TAIL_READ_MAX_BYTES) {
134
- return readFile(filePath, 'utf-8');
135
- }
136
- const fd = await open(filePath, 'r');
137
- try {
138
- const start = Math.max(0, fileSize - TAIL_READ_MAX_BYTES);
139
- const buf = Buffer.alloc(fileSize - start);
140
- const { bytesRead } = await fd.read(buf, 0, buf.length, start);
141
- return buf.toString('utf-8', 0, bytesRead);
142
- } finally {
143
- await fd.close();
144
- }
145
- }
146
-
147
- function mapClaudePermissionMode(permissionMode: unknown): ApprovalMode | undefined {
148
- switch (permissionMode) {
149
- case 'plan':
150
- return 'plan';
151
- case 'acceptEdits':
152
- return 'acceptEdits';
153
- case 'bypassPermissions':
154
- return 'autoApprove';
155
- case 'default':
156
- return 'normal';
157
- default:
158
- return undefined;
159
- }
160
- }
161
-
162
- function mapCodexApprovalModeFromTurnContext(payload: any): ApprovalMode | undefined {
163
- const approvalPolicy = typeof payload?.approval_policy === 'string' ? payload.approval_policy : '';
164
- const sandboxPolicy = payload?.sandbox_policy;
165
- const sandboxType = typeof sandboxPolicy?.type === 'string' ? sandboxPolicy.type : '';
166
- const writableRoots = Array.isArray(sandboxPolicy?.writable_roots) ? sandboxPolicy.writable_roots : null;
167
-
168
- if (approvalPolicy === 'never' && sandboxType === 'danger-full-access') {
169
- return 'autoApprove';
170
- }
171
- if (approvalPolicy === 'on-request' && sandboxType === 'workspace-write') {
172
- return writableRoots?.length ? 'normal' : 'plan';
173
- }
174
- return undefined;
175
- }
176
-
177
- function readClaudeApprovalModeFromEntry(entry: any): ApprovalMode | undefined {
178
- if (entry?.type === 'permission-mode') {
179
- return mapClaudePermissionMode(entry.permissionMode);
180
- }
181
- return mapClaudePermissionMode(entry?.permissionMode);
182
- }
183
-
184
- function readCodexApprovalModeFromEntry(entry: any): ApprovalMode | undefined {
185
- if (entry?.type !== 'turn_context') {
186
- return undefined;
187
- }
188
- return mapCodexApprovalModeFromTurnContext(entry.payload);
189
- }
190
-
191
- async function readSessionApprovalModeFromFile(
192
- filePath: string,
193
- agent: AgentType,
194
- fileSize: number,
195
- ): Promise<ApprovalMode | undefined> {
196
- let latestApprovalMode: ApprovalMode | undefined;
197
- const content = await readFileTail(filePath, fileSize);
198
- for (const line of content.split('\n')) {
199
- if (!line.trim()) continue;
200
- let entry: any;
201
- try {
202
- entry = JSON.parse(line);
203
- } catch {
204
- continue;
205
- }
206
- const approvalMode = agent === 'claude'
207
- ? readClaudeApprovalModeFromEntry(entry)
208
- : readCodexApprovalModeFromEntry(entry);
209
- if (approvalMode) {
210
- latestApprovalMode = approvalMode;
211
- }
212
- }
213
- return latestApprovalMode;
214
- }
215
-
216
- function extractCodexText(content: unknown): string {
217
- if (typeof content === 'string') return content;
218
- if (!Array.isArray(content)) return '';
219
- return content
220
- .map((item) => {
221
- if (!item || typeof item !== 'object') return '';
222
- if (typeof (item as any).text === 'string') return (item as any).text;
223
- if (typeof (item as any).content === 'string') return (item as any).content;
224
- return '';
225
- })
226
- .join('');
227
- }
228
-
229
- function extractCodexUserMessageText(entry: any): string {
230
- if (entry?.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'user') {
231
- return extractCodexText(entry.payload.content);
232
- }
233
- if (entry?.type === 'event_msg' && entry.payload?.type === 'user_message' && typeof entry.payload.message === 'string') {
234
- return entry.payload.message;
235
- }
236
- return '';
237
- }
238
-
239
- function extractClaudeText(content: unknown): string {
240
- if (typeof content === 'string') return content;
241
- if (!Array.isArray(content)) return '';
242
- return content
243
- .map((item) => {
244
- if (!item || typeof item !== 'object') return '';
245
- if ((item as any).type !== 'text') return '';
246
- return typeof (item as any).text === 'string' ? (item as any).text : '';
247
- })
248
- .join('');
249
- }
250
-
251
- const CLAUDE_INTERNAL_USER_MESSAGE_PREFIXES = [
252
- '<task-notification>',
253
- '<local-command-caveat>',
254
- '<local-command-stdout>',
255
- '<local-command-stderr>',
256
- '<command-name>',
257
- '<command-message>',
258
- '<command-args>',
259
- '<system-reminder>',
260
- '<environment_context>',
261
- '# AGENTS.md instructions for ',
262
- ];
263
-
264
- function isClaudeInternalUserMessage(entry: any, text: string): boolean {
265
- if (entry?.isMeta === true) return true;
266
- const trimmed = text.trim();
267
- if (!trimmed) return false;
268
- return CLAUDE_INTERNAL_USER_MESSAGE_PREFIXES.some(prefix => trimmed.startsWith(prefix));
269
- }
270
-
271
- function extractClaudeUserMessageText(entry: any): string {
272
- if (entry?.type !== 'user' || entry.message?.role !== 'user') return '';
273
- const text = extractClaudeText(entry.message.content);
274
- return isClaudeInternalUserMessage(entry, text) ? '' : text;
275
- }
276
-
277
- function hasClaudeToolResult(content: unknown): boolean {
278
- return Array.isArray(content) && content.some((item) => item && typeof item === 'object' && (item as any).type === 'tool_result');
279
- }
280
-
281
- function isCodexSetupMessage(text: string): boolean {
282
- const trimmed = text.trim();
283
- return trimmed.startsWith('# AGENTS.md instructions for ') || trimmed.startsWith('<environment_context>');
284
- }
285
-
286
- function toIdleSessionInfo(meta: SessionFileMeta, source: SessionSource = 'scanner'): SessionInfo {
287
- return {
288
- sessionId: meta.sessionId,
289
- agent: meta.agent,
290
- cwd: meta.cwd,
291
- title: meta.title,
292
- observedApprovalMode: meta.approvalMode,
293
- createdAt: meta.createdAt,
294
- lastActivityAt: meta.lastActivityAt,
295
- sources: [source],
296
- runtime: createIdleRuntime(),
297
- };
298
- }
299
-
300
- export function extractSessionIdFromSessionFile(agent: AgentType, filePath: string): string | null {
301
- const name = basename(filePath, '.jsonl');
302
- if (agent === 'claude') return name || null;
303
- const match = name.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
304
- return match?.[1] ?? null;
305
- }
306
-
307
- async function readClaudeSessionFileMeta(filePath: string, fileSize: number): Promise<Pick<SessionFileMeta, 'sessionId' | 'cwd' | 'title' | 'approvalMode'> | null> {
308
- const content = await readFileHead(filePath, fileSize);
309
- const lines = iterateFirstLines(content, 64);
310
-
311
- let cwd = '';
312
- let sessionId = '';
313
- let title: string | undefined;
314
- let firstUserText: string | undefined;
315
- let derivedTitle: string | undefined;
316
-
317
- for (const line of lines) {
318
- let entry: any;
319
- try {
320
- entry = JSON.parse(line);
321
- } catch {
322
- continue;
323
- }
324
-
325
- if (!cwd && typeof entry.cwd === 'string') cwd = entry.cwd;
326
- if (!sessionId && typeof entry.sessionId === 'string') sessionId = entry.sessionId;
327
- if (!title) {
328
- title = preferSessionTitle([entry.customTitle, entry.summary]);
329
- }
330
- if (!derivedTitle) {
331
- const userText = extractClaudeUserMessageText(entry);
332
- const nextDerivedTitle = preferSessionTitle([], userText);
333
- if (nextDerivedTitle) {
334
- firstUserText = userText;
335
- derivedTitle = nextDerivedTitle;
336
- }
337
- }
338
-
339
- if (cwd && (title || derivedTitle)) break;
340
- }
341
-
342
- if (!cwd) return null;
343
-
344
- return {
345
- sessionId: sessionId || extractSessionIdFromSessionFile('claude', filePath) || basename(filePath, '.jsonl'),
346
- cwd,
347
- title: title ?? derivedTitle ?? preferSessionTitle([], firstUserText),
348
- approvalMode: await readSessionApprovalModeFromFile(filePath, 'claude', fileSize),
349
- };
350
- }
351
-
352
- async function readCodexSessionFileMeta(filePath: string, fileSize: number): Promise<Pick<SessionFileMeta, 'sessionId' | 'cwd' | 'title' | 'approvalMode'> | null> {
353
- const content = await readFileHead(filePath, fileSize);
354
- const lines = iterateFirstLines(content, 96);
355
-
356
- let cwd = '';
357
- let sessionId = '';
358
- let title: string | undefined;
359
- let firstUserText: string | undefined;
360
- let derivedTitle: string | undefined;
361
-
362
- for (const line of lines) {
363
- let entry: any;
364
- try {
365
- entry = JSON.parse(line);
366
- } catch {
367
- continue;
368
- }
369
-
370
- if (!cwd) {
371
- cwd = entry.payload?.cwd ?? entry.cwd ?? '';
372
- }
373
- if (!sessionId) {
374
- sessionId = entry.payload?.id ?? entry.id ?? '';
375
- }
376
- if (!title) {
377
- title = preferSessionTitle([entry.payload?.title, entry.title]);
378
- }
379
- if (!derivedTitle) {
380
- const userText = extractCodexUserMessageText(entry);
381
- const nextDerivedTitle = userText && !isCodexSetupMessage(userText)
382
- ? preferSessionTitle([], userText)
383
- : undefined;
384
- if (nextDerivedTitle) {
385
- firstUserText = userText;
386
- derivedTitle = nextDerivedTitle;
387
- }
388
- }
389
-
390
- if (cwd && (title || derivedTitle)) break;
391
- }
392
-
393
- if (!cwd) return null;
394
-
395
- return {
396
- sessionId: sessionId || extractSessionIdFromSessionFile('codex', filePath) || basename(filePath, '.jsonl'),
397
- cwd,
398
- title: title ?? derivedTitle ?? preferSessionTitle([], firstUserText),
399
- approvalMode: await readSessionApprovalModeFromFile(filePath, 'codex', fileSize),
400
- };
401
- }
402
-
403
- export async function readSessionFileMeta(filePath: string, agent: AgentType): Promise<SessionFileMeta | null> {
404
- const fileStat = await stat(filePath).catch(() => null);
405
- if (!fileStat) return null;
406
-
407
- try {
408
- const fileSize = fileStat.size;
409
- if (agent === 'claude') {
410
- const meta = await readClaudeSessionFileMeta(filePath, fileSize);
411
- if (!meta) return null;
412
- return {
413
- sessionId: meta.sessionId,
414
- agent,
415
- cwd: meta.cwd,
416
- title: meta.title,
417
- approvalMode: meta.approvalMode,
418
- createdAt: new Date(fileStat.birthtimeMs).toISOString(),
419
- lastActivityAt: new Date(fileStat.mtimeMs).toISOString(),
420
- filePath,
421
- };
422
- }
423
-
424
- const meta = await readCodexSessionFileMeta(filePath, fileSize);
425
- if (!meta) return null;
426
-
427
- return {
428
- sessionId: meta.sessionId,
429
- agent,
430
- cwd: meta.cwd,
431
- title: meta.title,
432
- approvalMode: meta.approvalMode,
433
- createdAt: new Date(fileStat.birthtimeMs).toISOString(),
434
- lastActivityAt: new Date(fileStat.mtimeMs).toISOString(),
435
- filePath,
436
- };
437
- } catch {
438
- return null;
439
- }
440
- }
441
-
442
- export async function listClaudeSessions(cwd?: string, partial?: SessionInfo[]): Promise<SessionInfo[]> {
443
- const projectsDir = join(homedir(), '.claude', 'projects');
444
-
445
- try {
446
- let dirNames: string[];
447
- if (cwd) {
448
- dirNames = [encodeCwd(cwd)];
449
- } else {
450
- const entries = await readdir(projectsDir, { withFileTypes: true });
451
- dirNames = entries.filter(e => e.isDirectory()).map(e => e.name);
452
- }
453
-
454
- // Collect all file paths, then read in parallel
455
- const filePaths: string[] = [];
456
- for (const dir of dirNames) {
457
- const dirPath = join(projectsDir, dir);
458
- const files = await readdir(dirPath).catch(() => [] as string[]);
459
- for (const file of files) {
460
- if (file.endsWith('.jsonl')) filePaths.push(join(dirPath, file));
461
- }
462
- }
463
-
464
- // Stat files to get mtime, then sort by recency so active sessions are scanned first.
465
- // Even if the inventory timeout fires mid-scan, recently active sessions are already in `partial`.
466
- const fileStats = await mapWithConcurrency(filePaths, FILE_READ_CONCURRENCY, async (fp) => {
467
- const s = await stat(fp).catch(() => null);
468
- return { path: fp, mtimeMs: s?.mtimeMs ?? 0 };
469
- });
470
- fileStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
471
-
472
- const metas = await mapWithConcurrency(fileStats, FILE_READ_CONCURRENCY, async (entry) => {
473
- const meta = await readSessionFileMeta(entry.path, 'claude');
474
- if (meta && partial) partial.push(toIdleSessionInfo(meta));
475
- return meta;
476
- });
477
- const sessions = metas
478
- .filter((meta): meta is SessionFileMeta => meta !== null)
479
- .map(meta => toIdleSessionInfo(meta));
480
-
481
- return sessions.sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
482
- } catch (e) {
483
- // .claude/projects might not exist
484
- log.warn({ error: String(e) }, 'failed to list claude sessions');
485
- return [];
486
- }
487
- }
488
-
489
- export async function listCodexSessions(cwd?: string, partial?: SessionInfo[]): Promise<SessionInfo[]> {
490
- const sessionsDir = join(homedir(), '.codex', 'sessions');
491
-
492
- try {
493
- const years = await readdir(sessionsDir).catch(() => []);
494
-
495
- // Collect all file paths first via directory traversal
496
- const filePaths: string[] = [];
497
- for (const year of years) {
498
- const yearPath = join(sessionsDir, year);
499
- const months = await readdir(yearPath).catch(() => []);
500
-
501
- for (const month of months) {
502
- const monthPath = join(yearPath, month);
503
- const days = await readdir(monthPath).catch(() => []);
504
-
505
- for (const day of days) {
506
- const dayPath = join(monthPath, day);
507
- const files = await readdir(dayPath).catch(() => []);
508
-
509
- for (const file of files) {
510
- if (file.endsWith('.jsonl')) filePaths.push(join(dayPath, file));
511
- }
512
- }
513
- }
514
- }
515
-
516
- // Stat files to get mtime, then sort by recency so active sessions are scanned first
517
- const fileStats = await mapWithConcurrency(filePaths, FILE_READ_CONCURRENCY, async (fp) => {
518
- const s = await stat(fp).catch(() => null);
519
- return { path: fp, mtimeMs: s?.mtimeMs ?? 0 };
520
- });
521
- fileStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
522
-
523
- const metas = await mapWithConcurrency(fileStats, FILE_READ_CONCURRENCY, async (entry) => {
524
- const meta = await readSessionFileMeta(entry.path, 'codex');
525
- if (meta && partial && (!cwd || meta.cwd === cwd)) partial.push(toIdleSessionInfo(meta));
526
- return meta;
527
- });
528
- const sessions = metas
529
- .filter((meta): meta is SessionFileMeta => meta !== null)
530
- .filter(meta => !cwd || meta.cwd === cwd)
531
- .map(meta => toIdleSessionInfo(meta));
532
-
533
- return sessions.sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
534
- } catch (e) {
535
- // .codex/sessions might not exist
536
- log.warn({ error: String(e) }, 'failed to list codex sessions');
537
- return [];
538
- }
539
- }
540
-
541
- export async function listSessions(agent?: AgentType, cwd?: string, partial?: SessionInfo[]): Promise<SessionInfo[]> {
542
- const home = homedir();
543
- const cacheKey = `list:${home}:${agent ?? 'all'}:${cwd ?? ''}`;
544
- const cached = scannerCache.get(cacheKey);
545
- if (cached) return cached;
546
-
547
- // Coalesce concurrent callers into a single in-flight scan
548
- const inflight = scannerInflight.get(cacheKey);
549
- if (inflight) return inflight;
550
-
551
- const promise = (async () => {
552
- let result: SessionInfo[];
553
- if (agent === 'claude') {
554
- result = await listClaudeSessions(cwd, partial);
555
- } else if (agent === 'codex') {
556
- result = await listCodexSessions(cwd, partial);
557
- } else {
558
- const [claude, codex] = await Promise.all([
559
- listClaudeSessions(cwd, partial),
560
- listCodexSessions(cwd, partial),
561
- ]);
562
- result = [...claude, ...codex].sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
563
- }
564
-
565
- scannerCache.set(cacheKey, result);
566
- return result;
567
- })();
568
-
569
- scannerInflight.set(cacheKey, promise);
570
- promise.finally(() => scannerInflight.delete(cacheKey));
571
- return promise;
572
- }
573
-
574
- export async function searchSessionContent(query: string, agent?: AgentType): Promise<Set<string>> {
575
- const home = homedir();
576
- const cacheKey = `search:${home}:${query}:${agent ?? 'all'}`;
577
- const cached = searchCache.get(cacheKey);
578
- if (cached) return cached;
579
-
580
- // Coalesce concurrent callers into a single in-flight search
581
- const inflight = searchInflight.get(cacheKey);
582
- if (inflight) return inflight;
583
-
584
- const promise = (async () => {
585
- const matched = new Set<string>();
586
- const q = query.toLowerCase();
587
-
588
- async function searchClaudeFiles(): Promise<void> {
589
- const projectsDir = join(homedir(), '.claude', 'projects');
590
- try {
591
- const entries = await readdir(projectsDir, { withFileTypes: true });
592
- const filePaths: string[] = [];
593
- for (const entry of entries) {
594
- if (!entry.isDirectory()) continue;
595
- const dirPath = join(projectsDir, entry.name);
596
- const files = await readdir(dirPath).catch(() => [] as string[]);
597
- for (const file of files) {
598
- if (file.endsWith('.jsonl')) filePaths.push(join(dirPath, file));
599
- }
600
- }
601
- const results = await mapWithConcurrency(filePaths, FILE_READ_CONCURRENCY, (filePath) => searchFileContent(filePath, q, 'claude'));
602
- for (const result of results) {
603
- if (result) matched.add(result);
604
- }
605
- } catch (e) {
606
- log.warn({ error: String(e) }, 'failed to search claude files');
607
- }
608
- }
609
-
610
- async function searchCodexFiles(): Promise<void> {
611
- const sessionsDir = join(homedir(), '.codex', 'sessions');
612
- try {
613
- const filePaths: string[] = [];
614
- async function collectFiles(dir: string): Promise<void> {
615
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
616
- for (const entry of entries) {
617
- const fullPath = join(dir, entry.name);
618
- if (entry.isDirectory()) {
619
- await collectFiles(fullPath);
620
- } else if (entry.name.endsWith('.jsonl')) {
621
- filePaths.push(fullPath);
622
- }
623
- }
624
- }
625
- await collectFiles(sessionsDir);
626
- const results = await mapWithConcurrency(filePaths, FILE_READ_CONCURRENCY, (filePath) => searchFileContent(filePath, q, 'codex'));
627
- for (const result of results) {
628
- if (result) matched.add(result);
629
- }
630
- } catch (e) {
631
- log.warn({ error: String(e) }, 'failed to search codex files');
632
- }
633
- }
634
-
635
- if (agent === 'claude') {
636
- await searchClaudeFiles();
637
- } else if (agent === 'codex') {
638
- await searchCodexFiles();
639
- } else {
640
- await Promise.all([searchClaudeFiles(), searchCodexFiles()]);
641
- }
642
-
643
- searchCache.set(cacheKey, matched);
644
- return matched;
645
- })();
646
-
647
- searchInflight.set(cacheKey, promise);
648
- promise.finally(() => searchInflight.delete(cacheKey));
649
- return promise;
650
- }
651
-
652
- /** Returns the sessionId if the file contains matching text, null otherwise. */
653
- async function searchFileContent(filePath: string, query: string, agent: AgentType): Promise<string | null> {
654
- const input = createReadStream(filePath, { encoding: 'utf-8' });
655
- const reader = createInterface({ input, crlfDelay: Infinity });
656
- const extractText = agent === 'claude' ? extractClaudeMessageText : extractCodexMessageText;
657
- let sessionId = '';
658
- let found = false;
659
-
660
- try {
661
- for await (const line of reader) {
662
- if (!line.trim()) continue;
663
- let entry: any;
664
- try {
665
- entry = JSON.parse(line);
666
- } catch {
667
- continue;
668
- }
669
- if (!sessionId) {
670
- if (agent === 'claude') {
671
- if (typeof entry.sessionId === 'string') sessionId = entry.sessionId;
672
- } else {
673
- sessionId = entry.payload?.id ?? entry.id ?? '';
674
- }
675
- }
676
- if (!found) {
677
- const text = extractText(entry);
678
- if (text && text.toLowerCase().includes(query)) {
679
- found = true;
680
- }
681
- }
682
- if (sessionId && found) break;
683
- }
684
- } finally {
685
- reader.close();
686
- input.destroy();
687
- }
688
-
689
- if (!found) return null;
690
- return sessionId || extractSessionIdFromSessionFile(agent, filePath);
691
- }
692
-
693
- function extractClaudeMessageText(entry: any): string {
694
- if (entry?.type === 'user' && entry.message?.role === 'user') {
695
- return extractClaudeUserMessageText(entry);
696
- }
697
- if (entry?.type === 'assistant' && entry.message?.role === 'assistant') {
698
- return extractClaudeText(entry.message.content);
699
- }
700
- return '';
701
- }
702
-
703
- function extractCodexMessageText(entry: any): string {
704
- if (entry?.type === 'response_item' && entry.payload?.type === 'message') {
705
- const role = entry.payload.role;
706
- if (role === 'user' || role === 'assistant') {
707
- return extractCodexText(entry.payload.content);
708
- }
709
- }
710
- const userText = extractCodexUserMessageText(entry);
711
- if (userText) return userText;
712
- return '';
713
- }
714
-
715
- async function readJsonlFile(filePath: string, onEntry: (entry: any) => boolean | void): Promise<void> {
716
- const input = createReadStream(filePath, { encoding: 'utf-8' });
717
- const reader = createInterface({ input, crlfDelay: Infinity });
718
-
719
- try {
720
- for await (const line of reader) {
721
- if (!line.trim()) continue;
722
- let entry: any;
723
- try {
724
- entry = JSON.parse(line);
725
- } catch {
726
- continue;
727
- }
728
- if (onEntry(entry)) break;
729
- }
730
- } finally {
731
- reader.close();
732
- input.destroy();
733
- }
734
- }
735
-
736
- async function resolveClaudeSessionFilePath(sessionId: string, cwd?: string): Promise<string | null> {
737
- const projectsDir = join(homedir(), '.claude', 'projects');
738
- const primaryDir = cwd ? encodeCwd(cwd) : null;
739
- const allDirs = await readdir(projectsDir).catch(() => [] as string[]);
740
- const dirs = primaryDir
741
- ? [primaryDir, ...allDirs.filter((dir) => dir !== primaryDir)]
742
- : allDirs;
743
-
744
- for (const dir of dirs) {
745
- const filePath = join(projectsDir, dir, `${sessionId}.jsonl`);
746
- const fileStat = await stat(filePath).catch(() => null);
747
- if (fileStat?.isFile()) {
748
- return filePath;
749
- }
750
- }
751
-
752
- return null;
753
- }
754
-
755
- async function resolveSessionFilePath(sessionId: string, agent: AgentType, cwd?: string): Promise<string | null> {
756
- if (agent === 'claude') {
757
- return resolveClaudeSessionFilePath(sessionId, cwd);
758
- }
759
-
760
- const sessionsDir = join(homedir(), '.codex', 'sessions');
761
- return findCodexSessionFile(sessionsDir, sessionId);
762
- }
763
-
764
- type ClaudeRuntimeRelevantEntry =
765
- | { kind: 'userPrompt' }
766
- | { kind: 'toolResult' }
767
- | { kind: 'assistant'; stopReason?: string | null; text?: string }
768
- | { kind: 'stopHook' };
769
-
770
- function classifyClaudeRuntimeEntry(entry: any): ClaudeRuntimeRelevantEntry | null {
771
- if (entry?.type === 'user' && entry.message?.role === 'user') {
772
- if (hasClaudeToolResult(entry.message.content)) {
773
- return { kind: 'toolResult' };
774
- }
775
-
776
- const text = extractClaudeUserMessageText(entry);
777
- if (text) {
778
- return { kind: 'userPrompt' };
779
- }
780
- return null;
781
- }
782
-
783
- if (entry?.type === 'assistant' && entry.message?.role === 'assistant') {
784
- const text = extractClaudeText(entry.message.content);
785
- const stopReason = entry.message?.stop_reason;
786
- return {
787
- kind: 'assistant',
788
- stopReason: typeof stopReason === 'string' ? stopReason : stopReason ?? null,
789
- ...(text ? { text } : {}),
790
- };
791
- }
792
-
793
- if (entry?.type === 'system' && entry.subtype === 'stop_hook_summary') {
794
- return { kind: 'stopHook' };
795
- }
796
-
797
- return null;
798
- }
799
-
800
- function runtimeHintsFromClaudeEntry(entry: ClaudeRuntimeRelevantEntry | null): SessionRuntimeHints {
801
- if (!entry) return {};
802
-
803
- if (entry.kind === 'stopHook') {
804
- return {};
805
- }
806
-
807
- if (entry.kind === 'assistant') {
808
- const isResponding = entry.stopReason == null || entry.stopReason === 'tool_use';
809
- return {
810
- ...(isResponding ? { isResponding: true } : {}),
811
- ...(isResponding && entry.text ? { partialReplyText: entry.text } : {}),
812
- };
813
- }
814
-
815
- return { isResponding: true };
816
- }
817
-
818
- export async function readSessionRuntimeHintsFromFile(filePath: string, agent: AgentType): Promise<SessionRuntimeHints> {
819
- if (agent !== 'claude') {
820
- return {};
821
- }
822
-
823
- let lastRelevant: ClaudeRuntimeRelevantEntry | null = null;
824
- try {
825
- await readJsonlFile(filePath, (entry) => {
826
- const relevant = classifyClaudeRuntimeEntry(entry);
827
- if (relevant) {
828
- lastRelevant = relevant;
829
- }
830
- });
831
- } catch (error) {
832
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
833
- log.warn({ filePath, error: String(error) }, 'failed to read session runtime hints');
834
- }
835
- return {};
836
- }
837
-
838
- return runtimeHintsFromClaudeEntry(lastRelevant);
839
- }
840
-
841
- export async function readSessionRuntimeHints(sessionId: string, agent: AgentType, cwd?: string): Promise<SessionRuntimeHints> {
842
- const filePath = await resolveSessionFilePath(sessionId, agent, cwd);
843
- if (!filePath) {
844
- return {};
845
- }
846
-
847
- return readSessionRuntimeHintsFromFile(filePath, agent);
848
- }
849
-
850
- export async function readSessionHistory(sessionId: string, agent: AgentType, cwd?: string): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {
851
- const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
852
- const appendHistoryMessage = (
853
- message: { role: 'user' | 'assistant'; content: string },
854
- options?: { coalesceConsecutiveAssistant?: boolean },
855
- ) => {
856
- if (options?.coalesceConsecutiveAssistant && message.role === 'assistant') {
857
- const lastMessage = messages[messages.length - 1];
858
- if (lastMessage?.role === 'assistant') {
859
- messages[messages.length - 1] = message;
860
- return;
861
- }
862
- }
863
- messages.push(message);
864
- };
865
-
866
- if (agent === 'claude') {
867
- try {
868
- const filePath = await resolveClaudeSessionFilePath(sessionId, cwd);
869
- if (filePath) {
870
- await readJsonlFile(filePath, (msg) => {
871
- if (msg.type === 'user' && msg.message?.role === 'user') {
872
- const text = extractClaudeUserMessageText(msg);
873
- if (text) appendHistoryMessage({ role: 'user', content: text });
874
- } else if (msg.type === 'assistant' && msg.message?.role === 'assistant') {
875
- const text = extractClaudeText(msg.message.content);
876
- if (text) appendHistoryMessage({ role: 'assistant', content: text });
877
- }
878
- });
879
- }
880
- } catch (e) {
881
- log.warn({ error: String(e) }, 'failed to read claude session history');
882
- }
883
- } else if (agent === 'codex') {
884
- try {
885
- const filePath = await resolveSessionFilePath(sessionId, agent, cwd);
886
- if (filePath) {
887
- await readJsonlFile(filePath, (msg) => {
888
- if (msg.type !== 'response_item' || msg.payload?.type !== 'message') return;
889
- const role = msg.payload.role;
890
- if (role !== 'user' && role !== 'assistant') return;
891
- const text = extractCodexText(msg.payload.content);
892
- if (!text) return;
893
- if (role === 'user' && isCodexSetupMessage(text)) return;
894
- appendHistoryMessage(
895
- { role, content: text },
896
- { coalesceConsecutiveAssistant: true },
897
- );
898
- });
899
- }
900
- } catch (e) {
901
- log.warn({ error: String(e) }, 'failed to read codex session history');
902
- }
903
- }
904
-
905
- return messages;
906
- }
907
-
908
- async function findCodexSessionFile(dir: string, sessionId: string): Promise<string | null> {
909
- try {
910
- const entries = await readdir(dir, { withFileTypes: true });
911
- for (const entry of entries) {
912
- const fullPath = join(dir, entry.name);
913
- if (entry.isDirectory()) {
914
- const found = await findCodexSessionFile(fullPath, sessionId);
915
- if (found) return found;
916
- } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
917
- return fullPath;
918
- }
919
- }
920
- } catch (e) {
921
- log.warn({ error: String(e) }, 'failed to walk codex sessions dir');
922
- }
923
- return null;
924
- }