@vibelet/cli 0.1.34 → 0.1.36

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/app.json +5 -0
  2. package/dist/advertised-hosts.d.ts +34 -0
  3. package/dist/advertised-hosts.d.ts.map +1 -0
  4. package/dist/advertised-hosts.js +176 -0
  5. package/dist/advertised-hosts.js.map +1 -0
  6. package/dist/advertised-hosts.test.d.ts +2 -0
  7. package/dist/advertised-hosts.test.d.ts.map +1 -0
  8. package/dist/advertised-hosts.test.js +96 -0
  9. package/dist/advertised-hosts.test.js.map +1 -0
  10. package/dist/audit.d.ts +30 -0
  11. package/dist/audit.d.ts.map +1 -0
  12. package/dist/audit.js +73 -0
  13. package/dist/audit.js.map +1 -0
  14. package/dist/audit.test.d.ts +2 -0
  15. package/dist/audit.test.d.ts.map +1 -0
  16. package/dist/audit.test.js +33 -0
  17. package/dist/audit.test.js.map +1 -0
  18. package/dist/auth.d.ts +6 -0
  19. package/dist/auth.d.ts.map +1 -0
  20. package/dist/auth.js +27 -0
  21. package/dist/auth.js.map +1 -0
  22. package/dist/claude-hooks.d.ts +58 -0
  23. package/dist/claude-hooks.d.ts.map +1 -0
  24. package/dist/claude-hooks.js +129 -0
  25. package/dist/claude-hooks.js.map +1 -0
  26. package/dist/cli-version.d.ts +3 -0
  27. package/dist/cli-version.d.ts.map +1 -0
  28. package/dist/cli-version.js +35 -0
  29. package/dist/cli-version.js.map +1 -0
  30. package/dist/cli-version.test.d.ts +2 -0
  31. package/dist/cli-version.test.d.ts.map +1 -0
  32. package/dist/cli-version.test.js +38 -0
  33. package/dist/cli-version.test.js.map +1 -0
  34. package/dist/config.d.ts +30 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +327 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/config.test.d.ts +2 -0
  39. package/dist/config.test.d.ts.map +1 -0
  40. package/dist/config.test.js +184 -0
  41. package/dist/config.test.js.map +1 -0
  42. package/dist/dev-auth.test.d.ts +2 -0
  43. package/dist/dev-auth.test.d.ts.map +1 -0
  44. package/dist/dev-auth.test.js +154 -0
  45. package/dist/dev-auth.test.js.map +1 -0
  46. package/dist/dev-script.test.d.ts +2 -0
  47. package/dist/dev-script.test.d.ts.map +1 -0
  48. package/dist/dev-script.test.js +412 -0
  49. package/dist/dev-script.test.js.map +1 -0
  50. package/dist/drivers/claude.d.ts +34 -0
  51. package/dist/drivers/claude.d.ts.map +1 -0
  52. package/dist/drivers/claude.js +413 -0
  53. package/dist/drivers/claude.js.map +1 -0
  54. package/dist/drivers/claude.test.d.ts +2 -0
  55. package/dist/drivers/claude.test.d.ts.map +1 -0
  56. package/dist/drivers/claude.test.js +951 -0
  57. package/dist/drivers/claude.test.js.map +1 -0
  58. package/dist/drivers/codex.d.ts +38 -0
  59. package/dist/drivers/codex.d.ts.map +1 -0
  60. package/dist/drivers/codex.js +771 -0
  61. package/dist/drivers/codex.js.map +1 -0
  62. package/dist/drivers/codex.test.d.ts +2 -0
  63. package/dist/drivers/codex.test.d.ts.map +1 -0
  64. package/dist/drivers/codex.test.js +939 -0
  65. package/dist/drivers/codex.test.js.map +1 -0
  66. package/dist/drivers/types.d.ts +14 -0
  67. package/dist/drivers/types.d.ts.map +1 -0
  68. package/dist/drivers/types.js +2 -0
  69. package/dist/drivers/types.js.map +1 -0
  70. package/dist/e2e.test.d.ts +2 -0
  71. package/dist/e2e.test.d.ts.map +1 -0
  72. package/dist/e2e.test.js +111 -0
  73. package/dist/e2e.test.js.map +1 -0
  74. package/dist/identity.d.ts +10 -0
  75. package/dist/identity.d.ts.map +1 -0
  76. package/dist/identity.js +66 -0
  77. package/dist/identity.js.map +1 -0
  78. package/dist/identity.test.d.ts +2 -0
  79. package/dist/identity.test.d.ts.map +1 -0
  80. package/dist/identity.test.js +25 -0
  81. package/dist/identity.test.js.map +1 -0
  82. package/dist/index-entry.test.d.ts +2 -0
  83. package/dist/index-entry.test.d.ts.map +1 -0
  84. package/dist/index-entry.test.js +272 -0
  85. package/dist/index-entry.test.js.map +1 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +707 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logger.d.ts +31 -0
  91. package/dist/logger.d.ts.map +1 -0
  92. package/dist/logger.js +75 -0
  93. package/dist/logger.js.map +1 -0
  94. package/dist/metrics.d.ts +52 -0
  95. package/dist/metrics.d.ts.map +1 -0
  96. package/dist/metrics.js +89 -0
  97. package/dist/metrics.js.map +1 -0
  98. package/dist/pairing-store.d.ts +29 -0
  99. package/dist/pairing-store.d.ts.map +1 -0
  100. package/dist/pairing-store.js +131 -0
  101. package/dist/pairing-store.js.map +1 -0
  102. package/dist/pairing-store.test.d.ts +2 -0
  103. package/dist/pairing-store.test.d.ts.map +1 -0
  104. package/dist/pairing-store.test.js +47 -0
  105. package/dist/pairing-store.test.js.map +1 -0
  106. package/dist/paths.d.ts +16 -0
  107. package/dist/paths.d.ts.map +1 -0
  108. package/dist/paths.js +18 -0
  109. package/dist/paths.js.map +1 -0
  110. package/dist/perf-compare.d.ts +13 -0
  111. package/dist/perf-compare.d.ts.map +1 -0
  112. package/dist/perf-compare.js +125 -0
  113. package/dist/perf-compare.js.map +1 -0
  114. package/dist/port-conflict.d.ts +9 -0
  115. package/dist/port-conflict.d.ts.map +1 -0
  116. package/dist/port-conflict.js +33 -0
  117. package/dist/port-conflict.js.map +1 -0
  118. package/dist/port-conflict.test.d.ts +2 -0
  119. package/dist/port-conflict.test.d.ts.map +1 -0
  120. package/dist/port-conflict.test.js +38 -0
  121. package/dist/port-conflict.test.js.map +1 -0
  122. package/dist/process-scanner.d.ts +43 -0
  123. package/dist/process-scanner.d.ts.map +1 -0
  124. package/dist/process-scanner.js +453 -0
  125. package/dist/process-scanner.js.map +1 -0
  126. package/dist/process-scanner.perf.test.d.ts +2 -0
  127. package/dist/process-scanner.perf.test.d.ts.map +1 -0
  128. package/dist/process-scanner.perf.test.js +186 -0
  129. package/dist/process-scanner.perf.test.js.map +1 -0
  130. package/dist/process-scanner.test.d.ts +2 -0
  131. package/dist/process-scanner.test.d.ts.map +1 -0
  132. package/dist/process-scanner.test.js +399 -0
  133. package/dist/process-scanner.test.js.map +1 -0
  134. package/dist/push-protocol.d.ts +15 -0
  135. package/dist/push-protocol.d.ts.map +1 -0
  136. package/dist/push-protocol.js +23 -0
  137. package/dist/push-protocol.js.map +1 -0
  138. package/dist/push-protocol.test.d.ts +2 -0
  139. package/dist/push-protocol.test.d.ts.map +1 -0
  140. package/dist/push-protocol.test.js +57 -0
  141. package/dist/push-protocol.test.js.map +1 -0
  142. package/dist/push-store.d.ts +22 -0
  143. package/dist/push-store.d.ts.map +1 -0
  144. package/dist/push-store.js +103 -0
  145. package/dist/push-store.js.map +1 -0
  146. package/dist/push-store.test.d.ts +2 -0
  147. package/dist/push-store.test.d.ts.map +1 -0
  148. package/dist/push-store.test.js +79 -0
  149. package/dist/push-store.test.js.map +1 -0
  150. package/dist/push.d.ts +65 -0
  151. package/dist/push.d.ts.map +1 -0
  152. package/dist/push.js +202 -0
  153. package/dist/push.js.map +1 -0
  154. package/dist/push.test.d.ts +2 -0
  155. package/dist/push.test.d.ts.map +1 -0
  156. package/dist/push.test.js +199 -0
  157. package/dist/push.test.js.map +1 -0
  158. package/dist/safe-stdio.d.ts +3 -0
  159. package/dist/safe-stdio.d.ts.map +1 -0
  160. package/dist/safe-stdio.js +46 -0
  161. package/dist/safe-stdio.js.map +1 -0
  162. package/dist/scanner.d.ts +30 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +859 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scanner.perf.test.d.ts +2 -0
  167. package/dist/scanner.perf.test.d.ts.map +1 -0
  168. package/dist/scanner.perf.test.js +320 -0
  169. package/dist/scanner.perf.test.js.map +1 -0
  170. package/dist/scanner.test.d.ts +2 -0
  171. package/dist/scanner.test.d.ts.map +1 -0
  172. package/dist/scanner.test.js +948 -0
  173. package/dist/scanner.test.js.map +1 -0
  174. package/dist/session-inventory.d.ts +63 -0
  175. package/dist/session-inventory.d.ts.map +1 -0
  176. package/dist/session-inventory.js +525 -0
  177. package/dist/session-inventory.js.map +1 -0
  178. package/dist/session-inventory.perf.test.d.ts +2 -0
  179. package/dist/session-inventory.perf.test.d.ts.map +1 -0
  180. package/dist/session-inventory.perf.test.js +220 -0
  181. package/dist/session-inventory.perf.test.js.map +1 -0
  182. package/dist/session-inventory.test.d.ts +2 -0
  183. package/dist/session-inventory.test.d.ts.map +1 -0
  184. package/dist/session-inventory.test.js +712 -0
  185. package/dist/session-inventory.test.js.map +1 -0
  186. package/dist/session-manager.d.ts +75 -0
  187. package/dist/session-manager.d.ts.map +1 -0
  188. package/dist/session-manager.js +1515 -0
  189. package/dist/session-manager.js.map +1 -0
  190. package/dist/session-manager.test.d.ts +2 -0
  191. package/dist/session-manager.test.d.ts.map +1 -0
  192. package/dist/session-manager.test.js +2861 -0
  193. package/dist/session-manager.test.js.map +1 -0
  194. package/dist/session-store.d.ts +42 -0
  195. package/dist/session-store.d.ts.map +1 -0
  196. package/dist/session-store.js +163 -0
  197. package/dist/session-store.js.map +1 -0
  198. package/dist/session-store.test.d.ts +2 -0
  199. package/dist/session-store.test.d.ts.map +1 -0
  200. package/dist/session-store.test.js +236 -0
  201. package/dist/session-store.test.js.map +1 -0
  202. package/dist/session-title.d.ts +6 -0
  203. package/dist/session-title.d.ts.map +1 -0
  204. package/dist/session-title.js +105 -0
  205. package/dist/session-title.js.map +1 -0
  206. package/dist/session-title.perf.test.d.ts +2 -0
  207. package/dist/session-title.perf.test.d.ts.map +1 -0
  208. package/dist/session-title.perf.test.js +99 -0
  209. package/dist/session-title.perf.test.js.map +1 -0
  210. package/dist/session-title.test.d.ts +2 -0
  211. package/dist/session-title.test.d.ts.map +1 -0
  212. package/dist/session-title.test.js +199 -0
  213. package/dist/session-title.test.js.map +1 -0
  214. package/dist/shutdown-endpoint.test.d.ts +2 -0
  215. package/dist/shutdown-endpoint.test.d.ts.map +1 -0
  216. package/dist/shutdown-endpoint.test.js +93 -0
  217. package/dist/shutdown-endpoint.test.js.map +1 -0
  218. package/dist/storage-housekeeping.d.ts +28 -0
  219. package/dist/storage-housekeeping.d.ts.map +1 -0
  220. package/dist/storage-housekeeping.js +76 -0
  221. package/dist/storage-housekeeping.js.map +1 -0
  222. package/dist/storage-housekeeping.test.d.ts +2 -0
  223. package/dist/storage-housekeeping.test.d.ts.map +1 -0
  224. package/dist/storage-housekeeping.test.js +65 -0
  225. package/dist/storage-housekeeping.test.js.map +1 -0
  226. package/dist/test-daemon-harness.d.ts +31 -0
  227. package/dist/test-daemon-harness.d.ts.map +1 -0
  228. package/dist/test-daemon-harness.js +337 -0
  229. package/dist/test-daemon-harness.js.map +1 -0
  230. package/dist/token-auth.test.d.ts +2 -0
  231. package/dist/token-auth.test.d.ts.map +1 -0
  232. package/dist/token-auth.test.js +52 -0
  233. package/dist/token-auth.test.js.map +1 -0
  234. package/dist/utils.d.ts +4 -0
  235. package/dist/utils.d.ts.map +1 -0
  236. package/dist/utils.js +40 -0
  237. package/dist/utils.js.map +1 -0
  238. package/dist/utils.test.d.ts +2 -0
  239. package/dist/utils.test.d.ts.map +1 -0
  240. package/dist/utils.test.js +54 -0
  241. package/dist/utils.test.js.map +1 -0
  242. package/dist/ws-data.d.ts +4 -0
  243. package/dist/ws-data.d.ts.map +1 -0
  244. package/dist/ws-data.js +20 -0
  245. package/dist/ws-data.js.map +1 -0
  246. package/dist/ws-data.test.d.ts +2 -0
  247. package/dist/ws-data.test.d.ts.map +1 -0
  248. package/dist/ws-data.test.js +17 -0
  249. package/dist/ws-data.test.js.map +1 -0
  250. package/package.json +24 -27
  251. package/perf-reporter.mjs +138 -0
  252. package/scripts/build-release.mjs +41 -0
  253. package/scripts/dev.mjs +537 -0
  254. package/src/advertised-hosts.test.ts +125 -0
  255. package/src/advertised-hosts.ts +225 -0
  256. package/src/audit.test.ts +38 -0
  257. package/src/audit.ts +117 -0
  258. package/src/auth.ts +31 -0
  259. package/src/claude-hooks.ts +195 -0
  260. package/src/cli-version.test.ts +36 -0
  261. package/src/cli-version.ts +46 -0
  262. package/src/config.test.ts +254 -0
  263. package/src/config.ts +324 -0
  264. package/src/dev-auth.test.ts +183 -0
  265. package/src/dev-script.test.ts +511 -0
  266. package/src/drivers/claude.test.ts +1186 -0
  267. package/src/drivers/claude.ts +443 -0
  268. package/src/drivers/codex.test.ts +1096 -0
  269. package/src/drivers/codex.ts +879 -0
  270. package/src/drivers/types.ts +15 -0
  271. package/src/e2e.test.ts +139 -0
  272. package/src/identity.test.ts +26 -0
  273. package/src/identity.ts +82 -0
  274. package/src/index-entry.test.ts +336 -0
  275. package/src/index.ts +781 -0
  276. package/src/logger.ts +112 -0
  277. package/src/metrics.ts +117 -0
  278. package/src/pairing-store.test.ts +53 -0
  279. package/src/pairing-store.ts +154 -0
  280. package/src/paths.ts +19 -0
  281. package/src/perf-compare.ts +164 -0
  282. package/src/port-conflict.test.ts +45 -0
  283. package/src/port-conflict.ts +44 -0
  284. package/src/process-scanner.perf.test.ts +222 -0
  285. package/src/process-scanner.test.ts +575 -0
  286. package/src/process-scanner.ts +514 -0
  287. package/src/push-protocol.test.ts +74 -0
  288. package/src/push-protocol.ts +36 -0
  289. package/src/push-store.test.ts +89 -0
  290. package/src/push-store.ts +126 -0
  291. package/src/push.test.ts +234 -0
  292. package/src/push.ts +318 -0
  293. package/src/safe-stdio.ts +51 -0
  294. package/src/scanner.perf.test.ts +359 -0
  295. package/src/scanner.test.ts +1045 -0
  296. package/src/scanner.ts +924 -0
  297. package/src/session-inventory.perf.test.ts +250 -0
  298. package/src/session-inventory.test.ts +1002 -0
  299. package/src/session-inventory.ts +721 -0
  300. package/src/session-manager.test.ts +3430 -0
  301. package/src/session-manager.ts +1775 -0
  302. package/src/session-store.test.ts +276 -0
  303. package/src/session-store.ts +202 -0
  304. package/src/session-title.perf.test.ts +118 -0
  305. package/src/session-title.test.ts +286 -0
  306. package/src/session-title.ts +108 -0
  307. package/src/shutdown-endpoint.test.ts +95 -0
  308. package/src/storage-housekeeping.test.ts +78 -0
  309. package/src/storage-housekeeping.ts +111 -0
  310. package/src/test-daemon-harness.ts +410 -0
  311. package/src/token-auth.test.ts +67 -0
  312. package/src/utils.test.ts +65 -0
  313. package/src/utils.ts +47 -0
  314. package/src/ws-data.test.ts +20 -0
  315. package/src/ws-data.ts +26 -0
  316. package/tsconfig.json +12 -0
  317. package/README.md +0 -80
  318. package/bin/cloudflared-quick-tunnel.mjs +0 -11
  319. package/bin/cloudflared-resolver.mjs +0 -68
  320. package/bin/vibelet-runtime-policy.mjs +0 -36
  321. package/bin/vibelet.cjs +0 -12
  322. package/bin/vibelet.mjs +0 -1019
  323. package/dist/index.cjs +0 -123
@@ -0,0 +1,276 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, rm } from 'fs/promises';
4
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+ import { tmpdir } from 'os';
7
+ import type { SessionRecord } from './session-store.js';
8
+
9
+ // We need to override SESSION_STORE_PATH before importing SessionStore.
10
+ // Use a dynamic approach: create the store, then replace its internals.
11
+
12
+ async function withTempStore(fn: (storePath: string) => Promise<void>): Promise<void> {
13
+ const dir = await mkdtemp(join(tmpdir(), 'ss-test-'));
14
+ const storePath = join(dir, 'sessions.json');
15
+ try {
16
+ await fn(storePath);
17
+ } finally {
18
+ await rm(dir, { recursive: true, force: true });
19
+ }
20
+ }
21
+
22
+ function makeRecord(overrides: Partial<SessionRecord> & { sessionId: string }): SessionRecord {
23
+ return {
24
+ agent: 'claude',
25
+ cwd: '/test',
26
+ title: 'Test',
27
+ createdAt: '2026-01-01T00:00:00.000Z',
28
+ lastActivityAt: '2026-01-01T00:00:00.000Z',
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ // Import SessionStore and override its disk path for testing
34
+ async function createTestStore(storePath: string, initialRecords?: SessionRecord[]) {
35
+ if (initialRecords) {
36
+ mkdirSync(dirname(storePath), { recursive: true });
37
+ writeFileSync(storePath, JSON.stringify({
38
+ records: initialRecords,
39
+ deletedSessionIds: [],
40
+ }));
41
+ }
42
+ const { SessionStore } = await import('./session-store.js');
43
+ const store = new SessionStore();
44
+ // Override path and records for isolated testing
45
+ (store as any).records = initialRecords ?? [];
46
+ (store as any).deletedSessionIds = new Set<string>();
47
+ // Make save synchronous and write to our temp path
48
+ (store as any).saveToDisk = () => {
49
+ mkdirSync(dirname(storePath), { recursive: true });
50
+ writeFileSync(storePath, JSON.stringify({
51
+ records: (store as any).records,
52
+ deletedSessionIds: [...(store as any).deletedSessionIds],
53
+ }, null, 2));
54
+ };
55
+ (store as any).scheduleSave = () => (store as any).saveToDisk();
56
+ return store;
57
+ }
58
+
59
+ test('getAll: returns empty array initially', async () => {
60
+ await withTempStore(async (storePath) => {
61
+ const store = await createTestStore(storePath);
62
+ assert.deepEqual(store.getAll(), []);
63
+ });
64
+ });
65
+
66
+ test('upsert: inserts new record at beginning', async () => {
67
+ await withTempStore(async (storePath) => {
68
+ const store = await createTestStore(storePath);
69
+ const r1 = makeRecord({ sessionId: 's1' });
70
+ const r2 = makeRecord({ sessionId: 's2' });
71
+
72
+ store.upsert(r1);
73
+ store.upsert(r2);
74
+
75
+ const all = store.getAll();
76
+ assert.equal(all.length, 2);
77
+ assert.equal(all[0].sessionId, 's2');
78
+ assert.equal(all[1].sessionId, 's1');
79
+ });
80
+ });
81
+
82
+ test('upsert: updates existing record in place', async () => {
83
+ await withTempStore(async (storePath) => {
84
+ const store = await createTestStore(storePath);
85
+ store.upsert(makeRecord({ sessionId: 's1', title: 'Original' }));
86
+ store.upsert(makeRecord({ sessionId: 's1', title: 'Updated' }));
87
+
88
+ const all = store.getAll();
89
+ assert.equal(all.length, 1);
90
+ assert.equal(all[0].title, 'Updated');
91
+ });
92
+ });
93
+
94
+ test('find: returns matching record', async () => {
95
+ await withTempStore(async (storePath) => {
96
+ const store = await createTestStore(storePath);
97
+ store.upsert(makeRecord({ sessionId: 's1', title: 'Found' }));
98
+ store.upsert(makeRecord({ sessionId: 's2', title: 'Other' }));
99
+
100
+ const found = store.find('s1');
101
+ assert.ok(found);
102
+ assert.equal(found.title, 'Found');
103
+ });
104
+ });
105
+
106
+ test('find: returns undefined for missing record', async () => {
107
+ await withTempStore(async (storePath) => {
108
+ const store = await createTestStore(storePath);
109
+ assert.equal(store.find('nonexistent'), undefined);
110
+ });
111
+ });
112
+
113
+ test('remove: removes matching record', async () => {
114
+ await withTempStore(async (storePath) => {
115
+ const store = await createTestStore(storePath);
116
+ store.upsert(makeRecord({ sessionId: 's1' }));
117
+ store.upsert(makeRecord({ sessionId: 's2' }));
118
+
119
+ store.remove('s1');
120
+
121
+ assert.equal(store.getAll().length, 1);
122
+ assert.equal(store.find('s1'), undefined);
123
+ assert.equal(store.isDeleted('s1'), true);
124
+ assert.ok(store.find('s2'));
125
+ });
126
+ });
127
+
128
+ test('remove: no-op for missing record', async () => {
129
+ await withTempStore(async (storePath) => {
130
+ const store = await createTestStore(storePath);
131
+ store.upsert(makeRecord({ sessionId: 's1' }));
132
+
133
+ store.remove('nonexistent');
134
+
135
+ assert.equal(store.getAll().length, 1);
136
+ });
137
+ });
138
+
139
+ test('flushSync: persists to disk', async () => {
140
+ await withTempStore(async (storePath) => {
141
+ const store = await createTestStore(storePath);
142
+ store.upsert(makeRecord({ sessionId: 's1', title: 'Persisted' }));
143
+ store.flushSync();
144
+
145
+ const data = JSON.parse(readFileSync(storePath, 'utf-8'));
146
+ assert.equal(data.records.length, 1);
147
+ assert.equal(data.records[0].sessionId, 's1');
148
+ assert.equal(data.records[0].title, 'Persisted');
149
+ assert.deepEqual(data.deletedSessionIds, []);
150
+ });
151
+ });
152
+
153
+ test('upsert: triggers save to disk', async () => {
154
+ await withTempStore(async (storePath) => {
155
+ const store = await createTestStore(storePath);
156
+ store.upsert(makeRecord({ sessionId: 's1' }));
157
+
158
+ // scheduleSave is synchronous in our test setup
159
+ const data = JSON.parse(readFileSync(storePath, 'utf-8'));
160
+ assert.equal(data.records.length, 1);
161
+ assert.deepEqual(data.deletedSessionIds, []);
162
+ });
163
+ });
164
+
165
+ test('remove: triggers save to disk', async () => {
166
+ await withTempStore(async (storePath) => {
167
+ const store = await createTestStore(storePath);
168
+ store.upsert(makeRecord({ sessionId: 's1' }));
169
+ store.upsert(makeRecord({ sessionId: 's2' }));
170
+ store.remove('s1');
171
+
172
+ const data = JSON.parse(readFileSync(storePath, 'utf-8'));
173
+ assert.equal(data.records.length, 1);
174
+ assert.equal(data.records[0].sessionId, 's2');
175
+ assert.deepEqual(data.deletedSessionIds, ['s1']);
176
+ });
177
+ });
178
+
179
+ test('upsert: clears tombstone for a recreated session', async () => {
180
+ await withTempStore(async (storePath) => {
181
+ const store = await createTestStore(storePath);
182
+ store.upsert(makeRecord({ sessionId: 's1' }));
183
+ store.remove('s1');
184
+
185
+ assert.equal(store.isDeleted('s1'), true);
186
+
187
+ store.upsert(makeRecord({ sessionId: 's1', title: 'Recreated' }));
188
+
189
+ assert.equal(store.isDeleted('s1'), false);
190
+ assert.equal(store.find('s1')?.title, 'Recreated');
191
+ });
192
+ });
193
+
194
+ test('loadFromDisk: preserves accepted client message ids and trims oversized arrays', async () => {
195
+ await withTempStore(async (storePath) => {
196
+ const initial = makeRecord({
197
+ sessionId: 's1',
198
+ acceptedClientMessageIds: Array.from({ length: 205 }, (_, index) => `cm_${index}`),
199
+ });
200
+ const { normalizeSessionRecord } = await import('./session-store.js');
201
+ const record = normalizeSessionRecord(initial);
202
+
203
+ assert.ok(record);
204
+ assert.equal(record.acceptedClientMessageIds?.length, 200);
205
+ assert.equal(record.acceptedClientMessageIds?.[0], 'cm_5');
206
+ assert.equal(record.acceptedClientMessageIds?.[199], 'cm_204');
207
+ });
208
+ });
209
+
210
+ test('loadFromDisk: backfills missing accepted client message ids for legacy records', async () => {
211
+ await withTempStore(async (storePath) => {
212
+ const legacyRecord = makeRecord({ sessionId: 'legacy' });
213
+ delete (legacyRecord as SessionRecord & { acceptedClientMessageIds?: string[] }).acceptedClientMessageIds;
214
+ const { normalizeSessionRecord } = await import('./session-store.js');
215
+ const record = normalizeSessionRecord(legacyRecord);
216
+
217
+ assert.ok(record);
218
+ assert.deepEqual(record.acceptedClientMessageIds, []);
219
+ });
220
+ });
221
+
222
+ test('normalizeSessionRecord preserves a valid pending approval payload', async () => {
223
+ await withTempStore(async () => {
224
+ const { normalizeSessionRecord } = await import('./session-store.js');
225
+ const record = normalizeSessionRecord(makeRecord({
226
+ sessionId: 'pending-approval',
227
+ pendingApproval: {
228
+ requestId: 'req-1',
229
+ toolName: 'Bash',
230
+ input: { command: 'pwd' },
231
+ description: 'Run pwd',
232
+ approvalContext: {
233
+ provider: 'codex',
234
+ kind: 'command-execution',
235
+ rpcId: 42,
236
+ },
237
+ },
238
+ }));
239
+
240
+ assert.deepEqual(record.pendingApproval, {
241
+ requestId: 'req-1',
242
+ toolName: 'Bash',
243
+ input: { command: 'pwd' },
244
+ description: 'Run pwd',
245
+ approvalContext: {
246
+ provider: 'codex',
247
+ kind: 'command-execution',
248
+ rpcId: 42,
249
+ },
250
+ });
251
+ });
252
+ });
253
+
254
+ test('normalizeSessionStoreDiskState: supports legacy array-only store files', async () => {
255
+ const { normalizeSessionStoreDiskState } = await import('./session-store.js');
256
+ const legacyRecord = makeRecord({ sessionId: 'legacy-array' });
257
+ delete (legacyRecord as SessionRecord & { acceptedClientMessageIds?: string[] }).acceptedClientMessageIds;
258
+
259
+ const state = normalizeSessionStoreDiskState([legacyRecord]);
260
+
261
+ assert.equal(state.records.length, 1);
262
+ assert.equal(state.records[0].sessionId, 'legacy-array');
263
+ assert.deepEqual(state.records[0].acceptedClientMessageIds, []);
264
+ assert.deepEqual(state.deletedSessionIds, []);
265
+ });
266
+
267
+ test('normalizeSessionStoreDiskState: sanitizes deletedSessionIds entries', async () => {
268
+ const { normalizeSessionStoreDiskState } = await import('./session-store.js');
269
+
270
+ const state = normalizeSessionStoreDiskState({
271
+ records: [makeRecord({ sessionId: 'kept' })],
272
+ deletedSessionIds: ['dup', '', 'dup', null, 123, 'ok'],
273
+ });
274
+
275
+ assert.deepEqual(state.deletedSessionIds, ['dup', 'ok']);
276
+ });
@@ -0,0 +1,202 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import type { AgentType, ApprovalMode, ApprovalRequestPayload } from '@vibelet/shared';
4
+ import { SESSION_STORE_PATH } from './paths.js';
5
+
6
+ export interface SessionRecord {
7
+ sessionId: string;
8
+ agent: AgentType;
9
+ cwd: string;
10
+ approvalMode?: ApprovalMode;
11
+ acceptedClientMessageIds?: string[];
12
+ pendingApproval?: ApprovalRequestPayload;
13
+ title: string;
14
+ createdAt: string;
15
+ lastActivityAt: string;
16
+ /** True for sessions created from the Vibelet app. */
17
+ managed?: boolean;
18
+ /** True if the session was responding when last persisted. */
19
+ isResponding?: boolean;
20
+ }
21
+
22
+ interface SessionStoreDiskState {
23
+ records: SessionRecord[];
24
+ deletedSessionIds: string[];
25
+ }
26
+
27
+ const DEBOUNCE_MS = 500;
28
+ const MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 200;
29
+
30
+ function sanitizeAcceptedClientMessageIds(ids: unknown): string[] {
31
+ if (!Array.isArray(ids)) return [];
32
+ const sanitized = ids.filter((value): value is string => typeof value === 'string' && value.length > 0);
33
+ if (sanitized.length <= MAX_ACCEPTED_CLIENT_MESSAGE_IDS) {
34
+ return sanitized;
35
+ }
36
+ return sanitized.slice(-MAX_ACCEPTED_CLIENT_MESSAGE_IDS);
37
+ }
38
+
39
+ function sanitizeDeletedSessionIds(ids: unknown): string[] {
40
+ if (!Array.isArray(ids)) return [];
41
+ const result: string[] = [];
42
+ for (const value of ids) {
43
+ if (typeof value !== 'string' || value.length === 0 || result.includes(value)) continue;
44
+ result.push(value);
45
+ }
46
+ return result;
47
+ }
48
+
49
+ function sanitizeApprovalRequest(value: unknown): ApprovalRequestPayload | undefined {
50
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
51
+ return undefined;
52
+ }
53
+
54
+ const candidate = value as Partial<ApprovalRequestPayload>;
55
+ const requestId = typeof candidate.requestId === 'string' ? candidate.requestId : '';
56
+ const toolName = typeof candidate.toolName === 'string' ? candidate.toolName : '';
57
+ const description = typeof candidate.description === 'string' ? candidate.description : '';
58
+ const input = candidate.input && typeof candidate.input === 'object' && !Array.isArray(candidate.input)
59
+ ? candidate.input as Record<string, unknown>
60
+ : {};
61
+
62
+ if (!requestId || !toolName || !description) {
63
+ return undefined;
64
+ }
65
+
66
+ const approvalContext = candidate.approvalContext;
67
+ if (
68
+ approvalContext
69
+ && typeof approvalContext === 'object'
70
+ && !Array.isArray(approvalContext)
71
+ && approvalContext.provider === 'codex'
72
+ && typeof approvalContext.kind === 'string'
73
+ && typeof approvalContext.rpcId === 'number'
74
+ ) {
75
+ return {
76
+ requestId,
77
+ toolName,
78
+ input,
79
+ description,
80
+ approvalContext: {
81
+ provider: 'codex',
82
+ kind: approvalContext.kind,
83
+ rpcId: approvalContext.rpcId,
84
+ ...(typeof approvalContext.questionId === 'string' ? { questionId: approvalContext.questionId } : {}),
85
+ ...(typeof approvalContext.approveLabel === 'string' ? { approveLabel: approvalContext.approveLabel } : {}),
86
+ ...(typeof approvalContext.denyLabel === 'string' ? { denyLabel: approvalContext.denyLabel } : {}),
87
+ },
88
+ };
89
+ }
90
+
91
+ return {
92
+ requestId,
93
+ toolName,
94
+ input,
95
+ description,
96
+ };
97
+ }
98
+
99
+ export function normalizeSessionRecord(record: SessionRecord & { lastActivityAt?: string }): SessionRecord {
100
+ return {
101
+ ...record,
102
+ lastActivityAt: record.lastActivityAt ?? record.createdAt,
103
+ acceptedClientMessageIds: sanitizeAcceptedClientMessageIds(record.acceptedClientMessageIds),
104
+ pendingApproval: sanitizeApprovalRequest(record.pendingApproval),
105
+ };
106
+ }
107
+
108
+ export function normalizeSessionStoreDiskState(parsed: unknown): SessionStoreDiskState {
109
+ if (Array.isArray(parsed)) {
110
+ return {
111
+ records: parsed.map(normalizeSessionRecord),
112
+ deletedSessionIds: [],
113
+ };
114
+ }
115
+ if (parsed && typeof parsed === 'object') {
116
+ const state = parsed as Partial<SessionStoreDiskState>;
117
+ return {
118
+ records: Array.isArray(state.records) ? state.records.map(normalizeSessionRecord) : [],
119
+ deletedSessionIds: sanitizeDeletedSessionIds(state.deletedSessionIds),
120
+ };
121
+ }
122
+ return { records: [], deletedSessionIds: [] };
123
+ }
124
+
125
+ export class SessionStore {
126
+ private records: SessionRecord[];
127
+ private deletedSessionIds: Set<string>;
128
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
129
+
130
+ constructor() {
131
+ const state = this.loadFromDisk();
132
+ this.records = state.records;
133
+ this.deletedSessionIds = new Set(state.deletedSessionIds);
134
+ }
135
+
136
+ getAll(): SessionRecord[] {
137
+ return this.records;
138
+ }
139
+
140
+ getDeletedSessionIds(): string[] {
141
+ return [...this.deletedSessionIds];
142
+ }
143
+
144
+ find(sessionId: string): SessionRecord | undefined {
145
+ return this.records.find(r => r.sessionId === sessionId);
146
+ }
147
+
148
+ isDeleted(sessionId: string): boolean {
149
+ return this.deletedSessionIds.has(sessionId);
150
+ }
151
+
152
+ upsert(record: SessionRecord): void {
153
+ this.deletedSessionIds.delete(record.sessionId);
154
+ const index = this.records.findIndex(r => r.sessionId === record.sessionId);
155
+ if (index >= 0) {
156
+ this.records[index] = record;
157
+ } else {
158
+ this.records.unshift(record);
159
+ }
160
+ this.scheduleSave();
161
+ }
162
+
163
+ remove(sessionId: string): void {
164
+ this.records = this.records.filter(r => r.sessionId !== sessionId);
165
+ this.deletedSessionIds.add(sessionId);
166
+ this.scheduleSave();
167
+ }
168
+
169
+ flushSync(): void {
170
+ if (this.debounceTimer) {
171
+ clearTimeout(this.debounceTimer);
172
+ this.debounceTimer = null;
173
+ }
174
+ this.saveToDisk();
175
+ }
176
+
177
+ private scheduleSave(): void {
178
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
179
+ this.debounceTimer = setTimeout(() => {
180
+ this.debounceTimer = null;
181
+ this.saveToDisk();
182
+ }, DEBOUNCE_MS);
183
+ }
184
+
185
+ private loadFromDisk(): SessionStoreDiskState {
186
+ try {
187
+ const data = readFileSync(SESSION_STORE_PATH, 'utf-8');
188
+ return normalizeSessionStoreDiskState(JSON.parse(data) as unknown);
189
+ } catch {
190
+ // Ignore invalid or missing store files and start from an empty state.
191
+ }
192
+ return { records: [], deletedSessionIds: [] };
193
+ }
194
+
195
+ private saveToDisk(): void {
196
+ mkdirSync(dirname(SESSION_STORE_PATH), { recursive: true });
197
+ writeFileSync(SESSION_STORE_PATH, JSON.stringify({
198
+ records: this.records,
199
+ deletedSessionIds: [...this.deletedSessionIds],
200
+ }, null, 2));
201
+ }
202
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ sanitizeSessionTitle,
5
+ isIdLikeSessionTitle,
6
+ isFallbackSessionTitle,
7
+ titleFromFirstSentence,
8
+ preferSessionTitle,
9
+ } from './session-title.js';
10
+
11
+ function measure(fn: () => void, iterations: number): { totalMs: number; avgMs: number } {
12
+ const start = performance.now();
13
+ for (let i = 0; i < iterations; i++) fn();
14
+ const totalMs = performance.now() - start;
15
+ return { totalMs, avgMs: totalMs / iterations };
16
+ }
17
+
18
+ const ITERATIONS = 10_000;
19
+
20
+ describe('session-title performance', () => {
21
+ const longText = 'a'.repeat(500);
22
+ const multiLine = Array.from({ length: 50 }, (_, i) => `Line ${i}: some content here`).join('\n');
23
+ const uuid = '019d03e3-2672-7011-9c6d-5ba083b71111';
24
+ const hexId = 'abcdef1234567890abcdef';
25
+
26
+ it(`sanitizeSessionTitle × ${ITERATIONS} short strings < 50ms`, () => {
27
+ const { totalMs } = measure(() => sanitizeSessionTitle(' hello world '), ITERATIONS);
28
+ console.log(` sanitizeSessionTitle (short): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
29
+ assert.ok(totalMs < 50, `took ${totalMs.toFixed(2)}ms, expected < 50ms`);
30
+ });
31
+
32
+ it(`sanitizeSessionTitle × ${ITERATIONS} long strings < 100ms`, () => {
33
+ const { totalMs } = measure(() => sanitizeSessionTitle(longText), ITERATIONS);
34
+ console.log(` sanitizeSessionTitle (long): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
35
+ assert.ok(totalMs < 100, `took ${totalMs.toFixed(2)}ms, expected < 100ms`);
36
+ });
37
+
38
+ it(`isIdLikeSessionTitle × ${ITERATIONS} UUID checks < 50ms`, () => {
39
+ const { totalMs } = measure(() => isIdLikeSessionTitle(uuid), ITERATIONS);
40
+ console.log(` isIdLikeSessionTitle (uuid): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
41
+ assert.ok(totalMs < 50, `took ${totalMs.toFixed(2)}ms, expected < 50ms`);
42
+ });
43
+
44
+ it(`isIdLikeSessionTitle × ${ITERATIONS} hex checks < 50ms`, () => {
45
+ const { totalMs } = measure(() => isIdLikeSessionTitle(hexId), ITERATIONS);
46
+ console.log(` isIdLikeSessionTitle (hex): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
47
+ assert.ok(totalMs < 50, `took ${totalMs.toFixed(2)}ms, expected < 50ms`);
48
+ });
49
+
50
+ it(`isIdLikeSessionTitle × ${ITERATIONS} normal title < 50ms`, () => {
51
+ const { totalMs } = measure(() => isIdLikeSessionTitle('Fix the login bug'), ITERATIONS);
52
+ console.log(` isIdLikeSessionTitle (normal): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
53
+ assert.ok(totalMs < 50, `took ${totalMs.toFixed(2)}ms, expected < 50ms`);
54
+ });
55
+
56
+ it(`isFallbackSessionTitle × ${ITERATIONS} mixed checks < 100ms`, () => {
57
+ const titles = ['New session', 'Resumed session', uuid, 'Fix bug', hexId, 'Untitled session'];
58
+ const { totalMs } = measure(() => {
59
+ for (const title of titles) isFallbackSessionTitle(title);
60
+ }, ITERATIONS);
61
+ console.log(` isFallbackSessionTitle (mixed×6): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
62
+ assert.ok(totalMs < 100, `took ${totalMs.toFixed(2)}ms, expected < 100ms`);
63
+ });
64
+
65
+ it(`titleFromFirstSentence × ${ITERATIONS} single line < 50ms`, () => {
66
+ const { totalMs } = measure(() => titleFromFirstSentence('Fix the login bug. Then deploy.'), ITERATIONS);
67
+ console.log(` titleFromFirstSentence (single): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
68
+ assert.ok(totalMs < 50, `took ${totalMs.toFixed(2)}ms, expected < 50ms`);
69
+ });
70
+
71
+ it(`titleFromFirstSentence × ${ITERATIONS} multi-line < 200ms`, () => {
72
+ const { totalMs } = measure(() => titleFromFirstSentence(multiLine), ITERATIONS);
73
+ console.log(` titleFromFirstSentence (multi): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
74
+ assert.ok(totalMs < 200, `took ${totalMs.toFixed(2)}ms, expected < 200ms`);
75
+ });
76
+
77
+ it(`titleFromFirstSentence × ${ITERATIONS} with meta prefix noise < 200ms`, () => {
78
+ const text = '<environment_context>setup\n# AGENTS.md instructions for project\nActual title here. More text.';
79
+ const { totalMs } = measure(() => titleFromFirstSentence(text), ITERATIONS);
80
+ console.log(` titleFromFirstSentence (meta noise): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
81
+ assert.ok(totalMs < 200, `took ${totalMs.toFixed(2)}ms, expected < 200ms`);
82
+ });
83
+
84
+ it(`titleFromFirstSentence × ${ITERATIONS} long truncation < 100ms`, () => {
85
+ const sentence = 'A'.repeat(200) + '. Done.';
86
+ const { totalMs } = measure(() => titleFromFirstSentence(sentence, 80), ITERATIONS);
87
+ console.log(` titleFromFirstSentence (truncation): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
88
+ assert.ok(totalMs < 100, `took ${totalMs.toFixed(2)}ms, expected < 100ms`);
89
+ });
90
+
91
+ it(`preferSessionTitle × ${ITERATIONS} candidate chain < 100ms`, () => {
92
+ const candidates: Array<string | null | undefined> = [
93
+ null,
94
+ undefined,
95
+ 'New session',
96
+ uuid,
97
+ 'Actual good title',
98
+ ];
99
+ const { totalMs } = measure(() => preferSessionTitle(candidates, 'fallback text here'), ITERATIONS);
100
+ console.log(` preferSessionTitle (chain): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
101
+ assert.ok(totalMs < 100, `took ${totalMs.toFixed(2)}ms, expected < 100ms`);
102
+ });
103
+
104
+ it(`preferSessionTitle × ${ITERATIONS} fallback to text < 200ms`, () => {
105
+ const candidates: Array<string | null | undefined> = [null, 'New session', uuid];
106
+ const { totalMs } = measure(() => preferSessionTitle(candidates, multiLine), ITERATIONS);
107
+ console.log(` preferSessionTitle (fallback): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
108
+ assert.ok(totalMs < 200, `took ${totalMs.toFixed(2)}ms, expected < 200ms`);
109
+ });
110
+
111
+ // CJK sentence boundary test
112
+ it(`titleFromFirstSentence × ${ITERATIONS} CJK text < 100ms`, () => {
113
+ const text = '修复登录页面的样式问题。然后部署到生产环境。';
114
+ const { totalMs } = measure(() => titleFromFirstSentence(text), ITERATIONS);
115
+ console.log(` titleFromFirstSentence (CJK): ${totalMs.toFixed(2)}ms for ${ITERATIONS} calls`);
116
+ assert.ok(totalMs < 100, `took ${totalMs.toFixed(2)}ms, expected < 100ms`);
117
+ });
118
+ });