@vibelet/cli 0.1.37 → 1.0.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 (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 +1062 -0
  7. package/dist/index.cjs +126 -0
  8. package/package.json +25 -24
  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
@@ -1,1045 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
4
- import { join } from 'path';
5
- import { tmpdir } from 'os';
6
- import {
7
- extractSessionIdFromSessionFile,
8
- readSessionFileMeta,
9
- readSessionHistory,
10
- readSessionRuntimeHints,
11
- searchSessionContent,
12
- } from './scanner.js';
13
- import { homedir } from 'os';
14
-
15
- async function withTempJsonl(
16
- lines: unknown[],
17
- run: (filePath: string) => Promise<void>,
18
- ): Promise<void> {
19
- const dir = await mkdtemp(join(tmpdir(), 'vibelet-scanner-'));
20
- const filePath = join(dir, 'session.jsonl');
21
- await writeFile(filePath, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`, 'utf-8');
22
- try {
23
- await run(filePath);
24
- } finally {
25
- await rm(dir, { recursive: true, force: true });
26
- }
27
- }
28
-
29
- test('derives a Claude title from the first user sentence when the file starts with progress events', async () => {
30
- await withTempJsonl([
31
- {
32
- type: 'progress',
33
- cwd: '/repo',
34
- sessionId: 'claude-session-1',
35
- data: { type: 'hook_progress' },
36
- },
37
- {
38
- type: 'file-history-snapshot',
39
- messageId: 'msg-1',
40
- snapshot: { timestamp: '2026-03-20T00:00:00.000Z' },
41
- isSnapshotUpdate: false,
42
- },
43
- {
44
- type: 'user',
45
- cwd: '/repo',
46
- sessionId: 'claude-session-1',
47
- message: {
48
- role: 'user',
49
- content: 'Investigate this regression. It started after the inventory merge.',
50
- },
51
- },
52
- ], async (filePath) => {
53
- const meta = await readSessionFileMeta(filePath, 'claude');
54
- assert.equal(meta?.cwd, '/repo');
55
- assert.equal(meta?.sessionId, 'claude-session-1');
56
- assert.equal(meta?.title, 'Investigate this regression');
57
- });
58
- });
59
-
60
- test('reads the latest Claude approval mode from session entries', async () => {
61
- await withTempJsonl([
62
- {
63
- type: 'permission-mode',
64
- permissionMode: 'bypassPermissions',
65
- sessionId: 'claude-session-mode',
66
- },
67
- {
68
- type: 'user',
69
- cwd: '/repo',
70
- sessionId: 'claude-session-mode',
71
- permissionMode: 'default',
72
- message: {
73
- role: 'user',
74
- content: 'First prompt',
75
- },
76
- },
77
- {
78
- type: 'user',
79
- cwd: '/repo',
80
- sessionId: 'claude-session-mode',
81
- permissionMode: 'acceptEdits',
82
- message: {
83
- role: 'user',
84
- content: 'Second prompt',
85
- },
86
- },
87
- ], async (filePath) => {
88
- const meta = await readSessionFileMeta(filePath, 'claude');
89
- assert.equal(meta?.approvalMode, 'acceptEdits');
90
- });
91
- });
92
-
93
- test('skips Claude local-command meta messages when deriving a title', async () => {
94
- await withTempJsonl([
95
- {
96
- type: 'file-history-snapshot',
97
- messageId: 'msg-1',
98
- snapshot: { timestamp: '2026-03-20T00:00:00.000Z' },
99
- isSnapshotUpdate: false,
100
- },
101
- {
102
- type: 'user',
103
- cwd: '/repo',
104
- sessionId: 'claude-session-2',
105
- isMeta: true,
106
- message: {
107
- role: 'user',
108
- content: '<local-command-caveat>Caveat: generated by local commands</local-command-caveat>',
109
- },
110
- },
111
- {
112
- type: 'user',
113
- cwd: '/repo',
114
- sessionId: 'claude-session-2',
115
- message: {
116
- role: 'user',
117
- content: '<command-name>/model</command-name>\n<command-message>model</command-message>',
118
- },
119
- },
120
- {
121
- type: 'user',
122
- cwd: '/repo',
123
- sessionId: 'claude-session-2',
124
- message: {
125
- role: 'user',
126
- content: '为什么我的 statusline 全没了?',
127
- },
128
- },
129
- ], async (filePath) => {
130
- const meta = await readSessionFileMeta(filePath, 'claude');
131
- assert.equal(meta?.title, '为什么我的 statusline 全没了');
132
- });
133
- });
134
-
135
- test('skips Claude task notifications when deriving a title', async () => {
136
- await withTempJsonl([
137
- {
138
- type: 'user',
139
- cwd: '/repo',
140
- sessionId: 'claude-session-3',
141
- message: {
142
- role: 'user',
143
- content: '<task-notification>\n<task-id>abc123</task-id>\n<status>failed</status>\n</task-notification>',
144
- },
145
- },
146
- {
147
- type: 'user',
148
- cwd: '/repo',
149
- sessionId: 'claude-session-3',
150
- message: {
151
- role: 'user',
152
- content: '打开浏览器查看今天的杭州天气。',
153
- },
154
- },
155
- ], async (filePath) => {
156
- const meta = await readSessionFileMeta(filePath, 'claude');
157
- assert.equal(meta?.title, '打开浏览器查看今天的杭州天气');
158
- });
159
- });
160
-
161
- test('derives a Codex title from the first meaningful user message instead of session metadata', async () => {
162
- await withTempJsonl([
163
- {
164
- type: 'session_meta',
165
- payload: {
166
- id: '0199a7f0-83df-7440-a5df-815f4771e93c',
167
- cwd: '/repo',
168
- },
169
- },
170
- {
171
- type: 'response_item',
172
- payload: {
173
- type: 'message',
174
- role: 'user',
175
- content: [
176
- {
177
- type: 'input_text',
178
- text: '<environment_context>\n <cwd>/repo</cwd>\n</environment_context>',
179
- },
180
- ],
181
- },
182
- },
183
- {
184
- type: 'response_item',
185
- payload: {
186
- type: 'message',
187
- role: 'user',
188
- content: [
189
- {
190
- type: 'input_text',
191
- text: "'/repo/src/file.ts'\n这个指示器的状态不对了, 即使远程任务存在, 也不会显示 CloudOutlined",
192
- },
193
- ],
194
- },
195
- },
196
- ], async (filePath) => {
197
- const meta = await readSessionFileMeta(filePath, 'codex');
198
- assert.equal(meta?.cwd, '/repo');
199
- assert.equal(meta?.sessionId, '0199a7f0-83df-7440-a5df-815f4771e93c');
200
- assert.equal(meta?.title, '这个指示器的状态不对了, 即使远程任务存在, 也不会显示 CloudOutlined');
201
- });
202
- });
203
-
204
- test('reads the latest Codex approval mode from turn_context entries', async () => {
205
- await withTempJsonl([
206
- {
207
- type: 'session_meta',
208
- payload: {
209
- id: '0199a7f0-83df-7440-a5df-815f4771e93c',
210
- cwd: '/repo',
211
- },
212
- },
213
- {
214
- type: 'turn_context',
215
- payload: {
216
- approval_policy: 'never',
217
- sandbox_policy: { type: 'danger-full-access' },
218
- },
219
- },
220
- {
221
- type: 'turn_context',
222
- payload: {
223
- approval_policy: 'on-request',
224
- sandbox_policy: { type: 'workspace-write', writable_roots: [] },
225
- },
226
- },
227
- ], async (filePath) => {
228
- const meta = await readSessionFileMeta(filePath, 'codex');
229
- assert.equal(meta?.approvalMode, 'plan');
230
- });
231
- });
232
-
233
- test('ignores Claude jsonl files that do not contain a real session cwd', async () => {
234
- await withTempJsonl([
235
- {
236
- type: 'queue-operation',
237
- operation: 'enqueue',
238
- timestamp: '2026-03-20T00:00:00.000Z',
239
- sessionId: 'queue-only',
240
- content: 'not a transcript',
241
- },
242
- ], async (filePath) => {
243
- const meta = await readSessionFileMeta(filePath, 'claude');
244
- assert.equal(meta, null);
245
- });
246
- });
247
-
248
- // ===== extractSessionIdFromSessionFile =====
249
-
250
- test('extracts claude session id from filename (basename without .jsonl)', () => {
251
- assert.equal(
252
- extractSessionIdFromSessionFile('claude', '/Users/test/.claude/projects/-Users-test-repo/my-session-id.jsonl'),
253
- 'my-session-id',
254
- );
255
- });
256
-
257
- test('extracts claude session id even for dot-prefixed filenames', () => {
258
- // basename('/some/path/.jsonl', '.jsonl') returns '.jsonl' which is truthy
259
- assert.equal(
260
- extractSessionIdFromSessionFile('claude', '/some/path/.jsonl'),
261
- '.jsonl',
262
- );
263
- });
264
-
265
- test('extracts codex session id (UUID) from rollout filename', () => {
266
- assert.equal(
267
- extractSessionIdFromSessionFile('codex', '/Users/test/.codex/sessions/2026/03/20/rollout-2026-03-20T08-37-12-019d08ac-c265-7270-9497-d14125228a50.jsonl'),
268
- '019d08ac-c265-7270-9497-d14125228a50',
269
- );
270
- });
271
-
272
- test('returns null for codex when filename has no UUID', () => {
273
- assert.equal(
274
- extractSessionIdFromSessionFile('codex', '/Users/test/.codex/sessions/2026/03/20/no-uuid-here.jsonl'),
275
- null,
276
- );
277
- });
278
-
279
- test('extracts codex UUID from simple filename', () => {
280
- assert.equal(
281
- extractSessionIdFromSessionFile('codex', '/path/019d03e3-2672-7011-9c6d-5ba083b71111.jsonl'),
282
- '019d03e3-2672-7011-9c6d-5ba083b71111',
283
- );
284
- });
285
-
286
- // ===== searchSessionContent (with temp files) =====
287
-
288
- async function withTempClaudeProject(
289
- sessions: Array<{ id: string; lines: unknown[] }>,
290
- run: (projectsDir: string) => Promise<void>,
291
- ): Promise<void> {
292
- const dir = await mkdtemp(join(tmpdir(), 'vibelet-search-'));
293
- const projectDir = join(dir, '-test-project');
294
- const { mkdir } = await import('fs/promises');
295
- await mkdir(projectDir, { recursive: true });
296
- for (const session of sessions) {
297
- const filePath = join(projectDir, `${session.id}.jsonl`);
298
- await writeFile(filePath, `${session.lines.map((l) => JSON.stringify(l)).join('\n')}\n`, 'utf-8');
299
- }
300
- try {
301
- await run(dir);
302
- } finally {
303
- await rm(dir, { recursive: true, force: true });
304
- }
305
- }
306
-
307
- test('searchSessionContent finds Claude user message text', async () => {
308
- await withTempClaudeProject([
309
- {
310
- id: 'session-match',
311
- lines: [
312
- { type: 'progress', cwd: '/repo', sessionId: 'session-match' },
313
- { type: 'user', message: { role: 'user', content: '请帮我修复登录问题' }, cwd: '/repo', sessionId: 'session-match' },
314
- ],
315
- },
316
- {
317
- id: 'session-nomatch',
318
- lines: [
319
- { type: 'progress', cwd: '/repo', sessionId: 'session-nomatch' },
320
- { type: 'user', message: { role: 'user', content: 'Add a new feature' }, cwd: '/repo', sessionId: 'session-nomatch' },
321
- ],
322
- },
323
- ], async (projectsDir) => {
324
- // Monkey-patch homedir for this test
325
- const originalHome = process.env.HOME;
326
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
327
- const { mkdir, symlink } = await import('fs/promises');
328
- await mkdir(join(tempHome, '.claude'), { recursive: true });
329
- await symlink(projectsDir, join(tempHome, '.claude', 'projects'));
330
- process.env.HOME = tempHome;
331
- try {
332
- const results = await searchSessionContent('登录', 'claude');
333
- assert.equal(results.size, 1);
334
- assert.ok(results.has('session-match'));
335
- } finally {
336
- process.env.HOME = originalHome;
337
- await rm(tempHome, { recursive: true, force: true });
338
- }
339
- });
340
- });
341
-
342
- test('searchSessionContent finds Claude assistant message text', async () => {
343
- await withTempClaudeProject([
344
- {
345
- id: 'asst-session',
346
- lines: [
347
- { type: 'progress', cwd: '/repo', sessionId: 'asst-session' },
348
- { type: 'user', message: { role: 'user', content: 'help me' }, cwd: '/repo', sessionId: 'asst-session' },
349
- { type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: '我来帮你解决这个问题' }] }, sessionId: 'asst-session' },
350
- ],
351
- },
352
- ], async (projectsDir) => {
353
- const originalHome = process.env.HOME;
354
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
355
- const { mkdir, symlink } = await import('fs/promises');
356
- await mkdir(join(tempHome, '.claude'), { recursive: true });
357
- await symlink(projectsDir, join(tempHome, '.claude', 'projects'));
358
- process.env.HOME = tempHome;
359
- try {
360
- const results = await searchSessionContent('解决', 'claude');
361
- assert.equal(results.size, 1);
362
- assert.ok(results.has('asst-session'));
363
- } finally {
364
- process.env.HOME = originalHome;
365
- await rm(tempHome, { recursive: true, force: true });
366
- }
367
- });
368
- });
369
-
370
- test('searchSessionContent uses sessionId from JSONL content, not filename', async () => {
371
- await withTempClaudeProject([
372
- {
373
- id: 'filename-id',
374
- lines: [
375
- { type: 'progress', cwd: '/repo', sessionId: 'internal-id' },
376
- { type: 'user', message: { role: 'user', content: 'unique-search-term' }, cwd: '/repo', sessionId: 'internal-id' },
377
- ],
378
- },
379
- ], async (projectsDir) => {
380
- const originalHome = process.env.HOME;
381
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
382
- const { mkdir, symlink } = await import('fs/promises');
383
- await mkdir(join(tempHome, '.claude'), { recursive: true });
384
- await symlink(projectsDir, join(tempHome, '.claude', 'projects'));
385
- process.env.HOME = tempHome;
386
- try {
387
- const results = await searchSessionContent('unique-search-term', 'claude');
388
- assert.equal(results.size, 1);
389
- assert.ok(results.has('internal-id'), 'should use sessionId from JSONL content, not filename');
390
- assert.ok(!results.has('filename-id'), 'should NOT use filename as sessionId');
391
- } finally {
392
- process.env.HOME = originalHome;
393
- await rm(tempHome, { recursive: true, force: true });
394
- }
395
- });
396
- });
397
-
398
- // ===== readSessionHistory =====
399
-
400
- test('readSessionHistory returns empty array for non-existent claude session', async () => {
401
- const result = await readSessionHistory('nonexistent-session-id-12345', 'claude', '/tmp/nonexistent-cwd-xyz');
402
- assert.deepEqual(result, []);
403
- });
404
-
405
- test('readSessionHistory returns empty array for non-existent codex session', async () => {
406
- const result = await readSessionHistory('nonexistent-session-id-12345', 'codex');
407
- assert.deepEqual(result, []);
408
- });
409
-
410
- test('readSessionHistory returns empty array when cwd is not provided for claude', async () => {
411
- const result = await readSessionHistory('nonexistent-no-cwd', 'claude');
412
- assert.deepEqual(result, []);
413
- });
414
-
415
- // ===== readSessionFileMeta edge cases =====
416
-
417
- test('readSessionFileMeta returns null for non-existent file path', async () => {
418
- const result = await readSessionFileMeta('/tmp/this-file-does-not-exist-at-all.jsonl', 'claude');
419
- assert.equal(result, null);
420
- });
421
-
422
- test('readSessionFileMeta returns null for non-existent codex file path', async () => {
423
- const result = await readSessionFileMeta('/tmp/this-file-does-not-exist-at-all.jsonl', 'codex');
424
- assert.equal(result, null);
425
- });
426
-
427
- test('readSessionFileMeta handles file with only empty lines', async () => {
428
- await withTempJsonl([
429
- ], async (filePath) => {
430
- // Overwrite with just whitespace lines
431
- await writeFile(filePath, '\n\n \n', 'utf-8');
432
- const meta = await readSessionFileMeta(filePath, 'claude');
433
- assert.equal(meta, null);
434
- });
435
- });
436
-
437
- test('readSessionFileMeta handles file with invalid JSON lines', async () => {
438
- const dir = await mkdtemp(join(tmpdir(), 'vibelet-scanner-'));
439
- const filePath = join(dir, 'broken.jsonl');
440
- await writeFile(filePath, 'not json at all\n{broken json\n', 'utf-8');
441
- try {
442
- const meta = await readSessionFileMeta(filePath, 'claude');
443
- assert.equal(meta, null);
444
- } finally {
445
- await rm(dir, { recursive: true, force: true });
446
- }
447
- });
448
-
449
- test('readSessionFileMeta for codex returns null when no cwd in entries', async () => {
450
- await withTempJsonl([
451
- { type: 'session_meta', payload: { id: 'codex-no-cwd-123' } },
452
- ], async (filePath) => {
453
- const meta = await readSessionFileMeta(filePath, 'codex');
454
- assert.equal(meta, null);
455
- });
456
- });
457
-
458
- test('readSessionFileMeta for codex with session_meta but no user message derives no title', async () => {
459
- await withTempJsonl([
460
- {
461
- type: 'session_meta',
462
- payload: {
463
- id: '0199a7f0-0000-0000-0000-000000000000',
464
- cwd: '/some/project',
465
- },
466
- },
467
- ], async (filePath) => {
468
- const meta = await readSessionFileMeta(filePath, 'codex');
469
- assert.equal(meta?.cwd, '/some/project');
470
- assert.equal(meta?.sessionId, '0199a7f0-0000-0000-0000-000000000000');
471
- assert.equal(meta?.title, undefined);
472
- });
473
- });
474
-
475
- test('readSessionFileMeta for claude extracts sessionId from filename when not in content', async () => {
476
- await withTempJsonl([
477
- { type: 'user', cwd: '/repo', message: { role: 'user', content: 'Hello world' } },
478
- ], async (filePath) => {
479
- const meta = await readSessionFileMeta(filePath, 'claude');
480
- assert.equal(meta?.cwd, '/repo');
481
- // sessionId should fall back to filename (basename without .jsonl)
482
- assert.equal(meta?.sessionId, 'session');
483
- });
484
- });
485
-
486
- test('readSessionFileMeta for claude uses customTitle when available', async () => {
487
- await withTempJsonl([
488
- {
489
- type: 'user',
490
- cwd: '/repo',
491
- sessionId: 'titled-session',
492
- customTitle: 'My Custom Title',
493
- message: { role: 'user', content: 'Some long message that would otherwise be the title' },
494
- },
495
- ], async (filePath) => {
496
- const meta = await readSessionFileMeta(filePath, 'claude');
497
- assert.equal(meta?.title, 'My Custom Title');
498
- });
499
- });
500
-
501
- test('readSessionFileMeta for codex skips environment_context setup messages for title', async () => {
502
- await withTempJsonl([
503
- {
504
- type: 'session_meta',
505
- payload: { id: 'codex-setup-skip', cwd: '/repo' },
506
- },
507
- {
508
- type: 'response_item',
509
- payload: {
510
- type: 'message',
511
- role: 'user',
512
- content: [{ type: 'input_text', text: '<environment_context>\n <cwd>/repo</cwd>\n</environment_context>' }],
513
- },
514
- },
515
- {
516
- type: 'response_item',
517
- payload: {
518
- type: 'message',
519
- role: 'user',
520
- content: [{ type: 'input_text', text: 'Fix the broken tests in scanner module' }],
521
- },
522
- },
523
- ], async (filePath) => {
524
- const meta = await readSessionFileMeta(filePath, 'codex');
525
- assert.equal(meta?.sessionId, 'codex-setup-skip');
526
- assert.ok(meta?.title);
527
- assert.ok(!meta?.title?.includes('environment_context'));
528
- });
529
- });
530
-
531
- test('readSessionFileMeta for codex with event_msg user_message type', async () => {
532
- await withTempJsonl([
533
- {
534
- type: 'session_meta',
535
- payload: { id: 'codex-event-msg', cwd: '/project' },
536
- },
537
- {
538
- type: 'event_msg',
539
- payload: {
540
- type: 'user_message',
541
- message: 'Refactor the database layer for better performance',
542
- },
543
- },
544
- ], async (filePath) => {
545
- const meta = await readSessionFileMeta(filePath, 'codex');
546
- assert.equal(meta?.cwd, '/project');
547
- assert.equal(meta?.sessionId, 'codex-event-msg');
548
- assert.ok(meta?.title);
549
- assert.ok(meta?.title?.includes('Refactor'));
550
- });
551
- });
552
-
553
- test('readSessionFileMeta includes createdAt and lastActivityAt from file stats', async () => {
554
- await withTempJsonl([
555
- { type: 'user', cwd: '/repo', sessionId: 'stat-test', message: { role: 'user', content: 'test message' } },
556
- ], async (filePath) => {
557
- const meta = await readSessionFileMeta(filePath, 'claude');
558
- assert.ok(meta);
559
- assert.ok(meta.createdAt);
560
- assert.ok(meta.lastActivityAt);
561
- // Should be valid ISO date strings
562
- assert.ok(!isNaN(new Date(meta.createdAt).getTime()));
563
- assert.ok(!isNaN(new Date(meta.lastActivityAt).getTime()));
564
- assert.equal(meta.filePath, filePath);
565
- assert.equal(meta.agent, 'claude');
566
- });
567
- });
568
-
569
- // ===== listClaudeSessions / listCodexSessions / listSessions =====
570
-
571
- test('listClaudeSessions returns sessions from temp claude projects dir', async () => {
572
- const { listClaudeSessions } = await import('./scanner.js');
573
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
574
- const projectDir = join(tempHome, '.claude', 'projects', '-test-repo');
575
- await mkdir(projectDir, { recursive: true });
576
-
577
- // Create a session file
578
- await writeFile(join(projectDir, 'session-abc.jsonl'), JSON.stringify({
579
- type: 'user',
580
- cwd: '/test-repo',
581
- sessionId: 'session-abc',
582
- message: { role: 'user', content: 'Hello world' },
583
- }) + '\n', 'utf-8');
584
-
585
- const originalHome = process.env.HOME;
586
- process.env.HOME = tempHome;
587
- try {
588
- const sessions = await listClaudeSessions();
589
- assert.ok(sessions.length >= 1);
590
- const found = sessions.find(s => s.sessionId === 'session-abc');
591
- assert.ok(found, 'Should find session-abc');
592
- assert.equal(found!.agent, 'claude');
593
- assert.equal(found!.cwd, '/test-repo');
594
- } finally {
595
- process.env.HOME = originalHome;
596
- await rm(tempHome, { recursive: true, force: true });
597
- }
598
- });
599
-
600
- test('listClaudeSessions filters by cwd when provided', async () => {
601
- const { listClaudeSessions } = await import('./scanner.js');
602
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
603
-
604
- // encodeCwd replaces non-alphanumeric with -
605
- const projectDir = join(tempHome, '.claude', 'projects', '-test-repo');
606
- await mkdir(projectDir, { recursive: true });
607
- await writeFile(join(projectDir, 'sess1.jsonl'), JSON.stringify({
608
- type: 'user', cwd: '/test-repo', sessionId: 'sess1',
609
- message: { role: 'user', content: 'Hello' },
610
- }) + '\n', 'utf-8');
611
-
612
- const originalHome = process.env.HOME;
613
- process.env.HOME = tempHome;
614
- try {
615
- // Search with matching cwd - encodeCwd('/test-repo') = '-test-repo'
616
- const sessions = await listClaudeSessions('/test-repo');
617
- assert.ok(sessions.length >= 1);
618
-
619
- // Search with non-matching cwd
620
- const empty = await listClaudeSessions('/other-repo');
621
- assert.equal(empty.length, 0);
622
- } finally {
623
- process.env.HOME = originalHome;
624
- await rm(tempHome, { recursive: true, force: true });
625
- }
626
- });
627
-
628
- test('listCodexSessions returns sessions from temp codex sessions dir', async () => {
629
- const { listCodexSessions } = await import('./scanner.js');
630
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
631
- const dayDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
632
- await mkdir(dayDir, { recursive: true });
633
-
634
- const sessionId = '019d08ac-c265-7270-9497-d14125228a50';
635
- await writeFile(join(dayDir, `rollout-2026-03-20T08-37-12-${sessionId}.jsonl`), [
636
- JSON.stringify({ type: 'session_meta', payload: { id: sessionId, cwd: '/my-project' } }),
637
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Fix the bug' }] } }),
638
- ].join('\n') + '\n', 'utf-8');
639
-
640
- const originalHome = process.env.HOME;
641
- process.env.HOME = tempHome;
642
- try {
643
- const sessions = await listCodexSessions();
644
- assert.ok(sessions.length >= 1);
645
- const found = sessions.find(s => s.sessionId === sessionId);
646
- assert.ok(found, `Should find session ${sessionId}`);
647
- assert.equal(found!.agent, 'codex');
648
- assert.equal(found!.cwd, '/my-project');
649
- } finally {
650
- process.env.HOME = originalHome;
651
- await rm(tempHome, { recursive: true, force: true });
652
- }
653
- });
654
-
655
- test('listCodexSessions filters by cwd when provided', async () => {
656
- const { listCodexSessions } = await import('./scanner.js');
657
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
658
- const dayDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
659
- await mkdir(dayDir, { recursive: true });
660
-
661
- const sessionId = '019d08ac-c265-7270-9497-aaaaaaaaaaaa';
662
- await writeFile(join(dayDir, `rollout-${sessionId}.jsonl`), [
663
- JSON.stringify({ type: 'session_meta', payload: { id: sessionId, cwd: '/specific-project' } }),
664
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Do something' }] } }),
665
- ].join('\n') + '\n', 'utf-8');
666
-
667
- const originalHome = process.env.HOME;
668
- process.env.HOME = tempHome;
669
- try {
670
- const matching = await listCodexSessions('/specific-project');
671
- assert.ok(matching.length >= 1);
672
-
673
- const nonMatching = await listCodexSessions('/other-project');
674
- assert.equal(nonMatching.length, 0);
675
- } finally {
676
- process.env.HOME = originalHome;
677
- await rm(tempHome, { recursive: true, force: true });
678
- }
679
- });
680
-
681
- test('listSessions returns both claude and codex sessions when no agent filter', async () => {
682
- const { listSessions: listAllSessions } = await import('./scanner.js');
683
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
684
-
685
- // Create claude session
686
- const claudeDir = join(tempHome, '.claude', 'projects', '-repo');
687
- await mkdir(claudeDir, { recursive: true });
688
- await writeFile(join(claudeDir, 'claude-sess.jsonl'), JSON.stringify({
689
- type: 'user', cwd: '/repo', sessionId: 'claude-sess',
690
- message: { role: 'user', content: 'Hello' },
691
- }) + '\n', 'utf-8');
692
-
693
- // Create codex session
694
- const codexDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
695
- await mkdir(codexDir, { recursive: true });
696
- await writeFile(join(codexDir, 'rollout-019d0000-0000-0000-0000-000000000000.jsonl'), [
697
- JSON.stringify({ type: 'session_meta', payload: { id: '019d0000-0000-0000-0000-000000000000', cwd: '/repo' } }),
698
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Hi' }] } }),
699
- ].join('\n') + '\n', 'utf-8');
700
-
701
- const originalHome = process.env.HOME;
702
- process.env.HOME = tempHome;
703
- try {
704
- const all = await listAllSessions();
705
- const agents = new Set(all.map(s => s.agent));
706
- assert.ok(agents.has('claude'));
707
- assert.ok(agents.has('codex'));
708
- assert.ok(all.length >= 2);
709
- } finally {
710
- process.env.HOME = originalHome;
711
- await rm(tempHome, { recursive: true, force: true });
712
- }
713
- });
714
-
715
- test('listClaudeSessions returns empty when .claude/projects does not exist', async () => {
716
- const { listClaudeSessions } = await import('./scanner.js');
717
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
718
- // Don't create .claude/projects
719
-
720
- const originalHome = process.env.HOME;
721
- process.env.HOME = tempHome;
722
- try {
723
- const sessions = await listClaudeSessions();
724
- assert.deepEqual(sessions, []);
725
- } finally {
726
- process.env.HOME = originalHome;
727
- await rm(tempHome, { recursive: true, force: true });
728
- }
729
- });
730
-
731
- test('listCodexSessions returns empty when .codex/sessions does not exist', async () => {
732
- const { listCodexSessions } = await import('./scanner.js');
733
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-home-'));
734
-
735
- const originalHome = process.env.HOME;
736
- process.env.HOME = tempHome;
737
- try {
738
- const sessions = await listCodexSessions();
739
- assert.deepEqual(sessions, []);
740
- } finally {
741
- process.env.HOME = originalHome;
742
- await rm(tempHome, { recursive: true, force: true });
743
- }
744
- });
745
-
746
- // ===== readSessionHistory with temp files =====
747
-
748
- test('readSessionHistory reads claude history from temp project dir', async () => {
749
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-hist-'));
750
- // encodeCwd('/test') = '-test'
751
- const projectDir = join(tempHome, '.claude', 'projects', '-test');
752
- await mkdir(projectDir, { recursive: true });
753
-
754
- await writeFile(join(projectDir, 'sess-hist-1.jsonl'), [
755
- JSON.stringify({ type: 'user', cwd: '/test', sessionId: 'sess-hist-1', message: { role: 'user', content: 'Hello' } }),
756
- JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'Hi there' }] } }),
757
- ].join('\n') + '\n', 'utf-8');
758
-
759
- const originalHome = process.env.HOME;
760
- process.env.HOME = tempHome;
761
- try {
762
- const history = await readSessionHistory('sess-hist-1', 'claude', '/test');
763
- assert.equal(history.length, 2);
764
- assert.equal(history[0].role, 'user');
765
- assert.equal(history[0].content, 'Hello');
766
- assert.equal(history[1].role, 'assistant');
767
- assert.equal(history[1].content, 'Hi there');
768
- } finally {
769
- process.env.HOME = originalHome;
770
- await rm(tempHome, { recursive: true, force: true });
771
- }
772
- });
773
-
774
- test('readSessionHistory filters Claude internal task notifications from history', async () => {
775
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-hist-'));
776
- const projectDir = join(tempHome, '.claude', 'projects', '-test');
777
- await mkdir(projectDir, { recursive: true });
778
-
779
- await writeFile(join(projectDir, 'sess-hist-2.jsonl'), [
780
- JSON.stringify({
781
- type: 'user',
782
- cwd: '/test',
783
- sessionId: 'sess-hist-2',
784
- message: {
785
- role: 'user',
786
- content: '<task-notification>\n<task-id>abc123</task-id>\n<status>failed</status>\n</task-notification>',
787
- },
788
- }),
789
- JSON.stringify({ type: 'user', cwd: '/test', sessionId: 'sess-hist-2', message: { role: 'user', content: '继续' } }),
790
- JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: '好的,我继续。' }] } }),
791
- ].join('\n') + '\n', 'utf-8');
792
-
793
- const originalHome = process.env.HOME;
794
- process.env.HOME = tempHome;
795
- try {
796
- const history = await readSessionHistory('sess-hist-2', 'claude', '/test');
797
- assert.deepEqual(history, [
798
- { role: 'user', content: '继续' },
799
- { role: 'assistant', content: '好的,我继续。' },
800
- ]);
801
- } finally {
802
- process.env.HOME = originalHome;
803
- await rm(tempHome, { recursive: true, force: true });
804
- }
805
- });
806
-
807
- test('readSessionHistory reads codex history from temp sessions dir', async () => {
808
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-hist-'));
809
- const dayDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
810
- await mkdir(dayDir, { recursive: true });
811
-
812
- const sessionId = '019d0000-1111-2222-3333-444444444444';
813
- await writeFile(join(dayDir, `rollout-${sessionId}.jsonl`), [
814
- JSON.stringify({ type: 'session_meta', payload: { id: sessionId, cwd: '/repo' } }),
815
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: '# AGENTS.md instructions for /repo' }] } }),
816
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Fix bug' }] } }),
817
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Inspecting the repo' }] } }),
818
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Done' }] } }),
819
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Ship it' }] } }),
820
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Running tests' }] } }),
821
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Shipped' }] } }),
822
- ].join('\n') + '\n', 'utf-8');
823
-
824
- const originalHome = process.env.HOME;
825
- process.env.HOME = tempHome;
826
- try {
827
- const history = await readSessionHistory(sessionId, 'codex');
828
- assert.deepEqual(history, [
829
- { role: 'user', content: 'Fix bug' },
830
- { role: 'assistant', content: 'Done' },
831
- { role: 'user', content: 'Ship it' },
832
- { role: 'assistant', content: 'Shipped' },
833
- ]);
834
- } finally {
835
- process.env.HOME = originalHome;
836
- await rm(tempHome, { recursive: true, force: true });
837
- }
838
- });
839
-
840
- test('readSessionHistory falls back to other dirs when cwd dir has no file', async () => {
841
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-hist-'));
842
- // Session file exists under a different project dir, not the one matching cwd
843
- const otherDir = join(tempHome, '.claude', 'projects', '-other-project');
844
- await mkdir(otherDir, { recursive: true });
845
-
846
- await writeFile(join(otherDir, 'sess-fallback.jsonl'), [
847
- JSON.stringify({ type: 'user', cwd: '/other-project', sessionId: 'sess-fallback', message: { role: 'user', content: 'Found me' } }),
848
- JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'Yes' }] } }),
849
- ].join('\n') + '\n', 'utf-8');
850
-
851
- const originalHome = process.env.HOME;
852
- process.env.HOME = tempHome;
853
- try {
854
- // Pass cwd='/wrong' which encodes to '-wrong' — no file there, should fallback
855
- const history = await readSessionHistory('sess-fallback', 'claude', '/wrong');
856
- assert.equal(history.length, 2);
857
- assert.equal(history[0].role, 'user');
858
- assert.equal(history[0].content, 'Found me');
859
- assert.equal(history[1].role, 'assistant');
860
- assert.equal(history[1].content, 'Yes');
861
- } finally {
862
- process.env.HOME = originalHome;
863
- await rm(tempHome, { recursive: true, force: true });
864
- }
865
- });
866
-
867
- test('readSessionHistory prefers cwd dir over fallback', async () => {
868
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-hist-'));
869
- // Create files in both the cwd dir and another dir
870
- const cwdDir = join(tempHome, '.claude', 'projects', '-correct');
871
- const otherDir = join(tempHome, '.claude', 'projects', '-other');
872
- await mkdir(cwdDir, { recursive: true });
873
- await mkdir(otherDir, { recursive: true });
874
-
875
- await writeFile(join(cwdDir, 'sess-prefer.jsonl'), [
876
- JSON.stringify({ type: 'user', cwd: '/correct', sessionId: 'sess-prefer', message: { role: 'user', content: 'From correct dir' } }),
877
- ].join('\n') + '\n', 'utf-8');
878
-
879
- await writeFile(join(otherDir, 'sess-prefer.jsonl'), [
880
- JSON.stringify({ type: 'user', cwd: '/other', sessionId: 'sess-prefer', message: { role: 'user', content: 'From other dir' } }),
881
- ].join('\n') + '\n', 'utf-8');
882
-
883
- const originalHome = process.env.HOME;
884
- process.env.HOME = tempHome;
885
- try {
886
- const history = await readSessionHistory('sess-prefer', 'claude', '/correct');
887
- assert.equal(history.length, 1);
888
- assert.equal(history[0].content, 'From correct dir');
889
- } finally {
890
- process.env.HOME = originalHome;
891
- await rm(tempHome, { recursive: true, force: true });
892
- }
893
- });
894
-
895
- test('readSessionRuntimeHints marks Claude sessions as responding while assistant text is still in-flight', async () => {
896
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-runtime-'));
897
- const projectDir = join(tempHome, '.claude', 'projects', '-test');
898
- await mkdir(projectDir, { recursive: true });
899
-
900
- await writeFile(join(projectDir, 'sess-runtime-active.jsonl'), [
901
- JSON.stringify({ type: 'user', cwd: '/test', sessionId: 'sess-runtime-active', message: { role: 'user', content: 'Check this file' } }),
902
- JSON.stringify({
903
- type: 'assistant',
904
- message: {
905
- role: 'assistant',
906
- stop_reason: null,
907
- content: [{ type: 'text', text: 'Still checking the file' }],
908
- },
909
- }),
910
- ].join('\n') + '\n', 'utf-8');
911
-
912
- const originalHome = process.env.HOME;
913
- process.env.HOME = tempHome;
914
- try {
915
- const hints = await readSessionRuntimeHints('sess-runtime-active', 'claude', '/test');
916
- assert.deepEqual(hints, {
917
- isResponding: true,
918
- partialReplyText: 'Still checking the file',
919
- });
920
- } finally {
921
- process.env.HOME = originalHome;
922
- await rm(tempHome, { recursive: true, force: true });
923
- }
924
- });
925
-
926
- test('readSessionRuntimeHints keeps Claude sessions responding after a tool result', async () => {
927
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-runtime-'));
928
- const projectDir = join(tempHome, '.claude', 'projects', '-test');
929
- await mkdir(projectDir, { recursive: true });
930
-
931
- await writeFile(join(projectDir, 'sess-runtime-tool.jsonl'), [
932
- JSON.stringify({ type: 'user', cwd: '/test', sessionId: 'sess-runtime-tool', message: { role: 'user', content: 'Run the checks' } }),
933
- JSON.stringify({
934
- type: 'assistant',
935
- message: {
936
- role: 'assistant',
937
- stop_reason: 'tool_use',
938
- content: [{ type: 'text', text: 'Running checks now' }, { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { cmd: 'npm test' } }],
939
- },
940
- }),
941
- JSON.stringify({
942
- type: 'user',
943
- cwd: '/test',
944
- sessionId: 'sess-runtime-tool',
945
- message: {
946
- role: 'user',
947
- content: [{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok' }],
948
- },
949
- }),
950
- ].join('\n') + '\n', 'utf-8');
951
-
952
- const originalHome = process.env.HOME;
953
- process.env.HOME = tempHome;
954
- try {
955
- const hints = await readSessionRuntimeHints('sess-runtime-tool', 'claude', '/test');
956
- assert.deepEqual(hints, { isResponding: true });
957
- } finally {
958
- process.env.HOME = originalHome;
959
- await rm(tempHome, { recursive: true, force: true });
960
- }
961
- });
962
-
963
- test('readSessionRuntimeHints ignores trailing system events after a completed Claude turn', async () => {
964
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-runtime-'));
965
- const projectDir = join(tempHome, '.claude', 'projects', '-test');
966
- await mkdir(projectDir, { recursive: true });
967
-
968
- await writeFile(join(projectDir, 'sess-runtime-done.jsonl'), [
969
- JSON.stringify({ type: 'user', cwd: '/test', sessionId: 'sess-runtime-done', message: { role: 'user', content: 'Summarize the diff' } }),
970
- JSON.stringify({
971
- type: 'assistant',
972
- message: {
973
- role: 'assistant',
974
- stop_reason: 'end_turn',
975
- content: [{ type: 'text', text: 'Summary complete' }],
976
- },
977
- }),
978
- JSON.stringify({ type: 'system', subtype: 'permission', message: 'post-turn metadata' }),
979
- ].join('\n') + '\n', 'utf-8');
980
-
981
- const originalHome = process.env.HOME;
982
- process.env.HOME = tempHome;
983
- try {
984
- const hints = await readSessionRuntimeHints('sess-runtime-done', 'claude', '/test');
985
- assert.deepEqual(hints, {});
986
- } finally {
987
- process.env.HOME = originalHome;
988
- await rm(tempHome, { recursive: true, force: true });
989
- }
990
- });
991
-
992
- // ===== searchSessionContent with codex =====
993
-
994
- test('searchSessionContent searches codex session files', async () => {
995
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-search-'));
996
- const dayDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
997
- await mkdir(dayDir, { recursive: true });
998
-
999
- const sessionId = '019d0000-aaaa-bbbb-cccc-dddddddddddd';
1000
- await writeFile(join(dayDir, `rollout-${sessionId}.jsonl`), [
1001
- JSON.stringify({ type: 'session_meta', payload: { id: sessionId, cwd: '/repo' } }),
1002
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'unique_codex_search_term_xyz' }] } }),
1003
- ].join('\n') + '\n', 'utf-8');
1004
-
1005
- const originalHome = process.env.HOME;
1006
- process.env.HOME = tempHome;
1007
- try {
1008
- const results = await searchSessionContent('unique_codex_search_term_xyz', 'codex');
1009
- assert.ok(results.has(sessionId));
1010
- } finally {
1011
- process.env.HOME = originalHome;
1012
- await rm(tempHome, { recursive: true, force: true });
1013
- }
1014
- });
1015
-
1016
- test('searchSessionContent searches both agents when no agent filter', async () => {
1017
- const tempHome = await mkdtemp(join(tmpdir(), 'vibe-search-'));
1018
-
1019
- // Claude session
1020
- const claudeDir = join(tempHome, '.claude', 'projects', '-repo');
1021
- await mkdir(claudeDir, { recursive: true });
1022
- await writeFile(join(claudeDir, 'claude-sess.jsonl'), [
1023
- JSON.stringify({ type: 'user', cwd: '/repo', sessionId: 'claude-sess', message: { role: 'user', content: 'shared_search_keyword_abc' } }),
1024
- ].join('\n') + '\n', 'utf-8');
1025
-
1026
- // Codex session
1027
- const codexDir = join(tempHome, '.codex', 'sessions', '2026', '03', '20');
1028
- await mkdir(codexDir, { recursive: true });
1029
- const codexId = '019d0000-0000-0000-0000-eeeeeeeeeeee';
1030
- await writeFile(join(codexDir, `rollout-${codexId}.jsonl`), [
1031
- JSON.stringify({ type: 'session_meta', payload: { id: codexId, cwd: '/repo' } }),
1032
- JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'shared_search_keyword_abc' }] } }),
1033
- ].join('\n') + '\n', 'utf-8');
1034
-
1035
- const originalHome = process.env.HOME;
1036
- process.env.HOME = tempHome;
1037
- try {
1038
- const results = await searchSessionContent('shared_search_keyword_abc');
1039
- assert.ok(results.has('claude-sess'));
1040
- assert.ok(results.has(codexId));
1041
- } finally {
1042
- process.env.HOME = originalHome;
1043
- await rm(tempHome, { recursive: true, force: true });
1044
- }
1045
- });