@unpolarize/code-sessions 0.1.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 (67) hide show
  1. package/bin/code-sessions.mjs +20 -0
  2. package/dist/chunk-ZJG2DWAK.js +2321 -0
  3. package/dist/cli.js +308 -0
  4. package/dist/index.js +162 -0
  5. package/package.json +21 -0
  6. package/src/adapters/adapters.test.ts +121 -0
  7. package/src/adapters/codex.ts +228 -0
  8. package/src/adapters/grok.ts +179 -0
  9. package/src/adapters/import.ts +79 -0
  10. package/src/adapters/index.ts +3 -0
  11. package/src/analytics/analytics.test.ts +94 -0
  12. package/src/analytics/command.ts +38 -0
  13. package/src/analytics/digest.ts +48 -0
  14. package/src/analytics/rollup.ts +114 -0
  15. package/src/analytics/site.ts +41 -0
  16. package/src/capture.test.ts +103 -0
  17. package/src/capture.ts +121 -0
  18. package/src/cli.ts +118 -0
  19. package/src/cliargs.test.ts +31 -0
  20. package/src/cliargs.ts +77 -0
  21. package/src/commands.test.ts +99 -0
  22. package/src/commands.ts +266 -0
  23. package/src/config.test.ts +36 -0
  24. package/src/config.ts +158 -0
  25. package/src/daemon.test.ts +130 -0
  26. package/src/daemon.ts +216 -0
  27. package/src/hooks/install.test.ts +47 -0
  28. package/src/hooks/install.ts +81 -0
  29. package/src/hooks/shim.test.ts +57 -0
  30. package/src/hooks/shim.ts +26 -0
  31. package/src/hygiene.test.ts +78 -0
  32. package/src/hygiene.ts +107 -0
  33. package/src/index.ts +21 -0
  34. package/src/index_store/db.test.ts +108 -0
  35. package/src/index_store/db.ts +289 -0
  36. package/src/index_store/index.ts +2 -0
  37. package/src/index_store/sync.test.ts +88 -0
  38. package/src/index_store/sync.ts +83 -0
  39. package/src/insights/heuristics.test.ts +71 -0
  40. package/src/insights/heuristics.ts +106 -0
  41. package/src/insights/index.ts +4 -0
  42. package/src/insights/labeler.test.ts +105 -0
  43. package/src/insights/labeler.ts +136 -0
  44. package/src/insights/llm.test.ts +77 -0
  45. package/src/insights/llm.ts +130 -0
  46. package/src/insights/provider.ts +37 -0
  47. package/src/ipc.test.ts +35 -0
  48. package/src/ipc.ts +70 -0
  49. package/src/pricing.test.ts +28 -0
  50. package/src/pricing.ts +45 -0
  51. package/src/state.test.ts +46 -0
  52. package/src/state.ts +89 -0
  53. package/src/store/git.test.ts +99 -0
  54. package/src/store/git.ts +138 -0
  55. package/src/store/paths.ts +45 -0
  56. package/src/store/scan.ts +39 -0
  57. package/src/store/writer.test.ts +93 -0
  58. package/src/store/writer.ts +135 -0
  59. package/src/tail.test.ts +50 -0
  60. package/src/tail.ts +47 -0
  61. package/src/telemetry/exporter.test.ts +104 -0
  62. package/src/telemetry/exporter.ts +64 -0
  63. package/src/telemetry/index.ts +2 -0
  64. package/src/telemetry/otlp.test.ts +123 -0
  65. package/src/telemetry/otlp.ts +215 -0
  66. package/src/test/e2e.test.ts +112 -0
  67. package/src/test/tmp.ts +36 -0
package/dist/cli.js ADDED
@@ -0,0 +1,308 @@
1
+ import {
2
+ GitStore,
3
+ HELP,
4
+ cmdBackfill,
5
+ cmdDoctor,
6
+ cmdExport,
7
+ cmdIndex,
8
+ cmdInit,
9
+ cmdInstallHooks,
10
+ cmdQuery,
11
+ cmdReindex,
12
+ cmdSearch,
13
+ cmdStatus,
14
+ envelopeFile,
15
+ handleHookInput,
16
+ insightsFile,
17
+ listSessionDirs,
18
+ loadConfig,
19
+ overridesFromFlags,
20
+ parseFlags,
21
+ readStdin,
22
+ startDaemon
23
+ } from "./chunk-ZJG2DWAK.js";
24
+
25
+ // src/analytics/command.ts
26
+ import { mkdirSync, writeFileSync } from "fs";
27
+ import { join } from "path";
28
+
29
+ // src/analytics/digest.ts
30
+ function renderDigest(report) {
31
+ const lines = [];
32
+ lines.push("# Session digest");
33
+ lines.push("");
34
+ lines.push(`_Generated ${report.generated_at}_`);
35
+ lines.push("");
36
+ lines.push(`- **Sessions:** ${report.sessions}`);
37
+ lines.push(
38
+ `- **Tokens:** ${report.totals.input_tokens.toLocaleString()} in / ${report.totals.output_tokens.toLocaleString()} out`
39
+ );
40
+ lines.push(`- **Estimated cost:** $${report.totals.cost_usd.toFixed(2)}`);
41
+ lines.push(`- **Hosts:** ${Object.entries(report.hosts).map(([h, n]) => `${h} (${n})`).join(", ") || "\u2014"}`);
42
+ lines.push("");
43
+ if (report.topTopics.length) {
44
+ lines.push("## Top topics");
45
+ for (const t of report.topTopics) lines.push(`- ${t.topic} \u2014 ${t.count}`);
46
+ lines.push("");
47
+ }
48
+ if (report.topTags.length) {
49
+ lines.push("## Top tags");
50
+ lines.push(report.topTags.map((t) => `\`${t.tag}\` (${t.count})`).join(" \xB7 "));
51
+ lines.push("");
52
+ }
53
+ if (Object.keys(report.signalCounts).length) {
54
+ lines.push("## Signals");
55
+ for (const [kind, count] of Object.entries(report.signalCounts).sort((a, b) => b[1] - a[1])) {
56
+ lines.push(`- ${kind}: ${count}`);
57
+ }
58
+ lines.push("");
59
+ }
60
+ if (report.similar.length) {
61
+ lines.push("## Related sessions (shared tags)");
62
+ for (const s of report.similar) lines.push(`- \`${s.tag}\`: ${s.sessions.length} sessions`);
63
+ lines.push("");
64
+ }
65
+ if (Object.keys(report.byMonth).length) {
66
+ lines.push("## By month");
67
+ for (const [month, m] of Object.entries(report.byMonth).sort()) {
68
+ lines.push(`- ${month}: ${m.sessions} sessions, $${m.cost_usd.toFixed(2)}`);
69
+ }
70
+ lines.push("");
71
+ }
72
+ return lines.join("\n");
73
+ }
74
+
75
+ // src/analytics/rollup.ts
76
+ import { existsSync, readFileSync } from "fs";
77
+ import {
78
+ safeParseInsights,
79
+ safeParseSession
80
+ } from "@unpolarize/code-sessions-schema";
81
+ function loadSession(ref) {
82
+ const out = { ref };
83
+ const envPath = envelopeFile(ref.dir);
84
+ if (existsSync(envPath)) {
85
+ try {
86
+ const parsed = safeParseSession(JSON.parse(readFileSync(envPath, "utf8")));
87
+ if (parsed.success) out.envelope = parsed.data;
88
+ } catch {
89
+ }
90
+ }
91
+ const insPath = insightsFile(ref.dir);
92
+ if (existsSync(insPath)) {
93
+ try {
94
+ const parsed = safeParseInsights(JSON.parse(readFileSync(insPath, "utf8")));
95
+ if (parsed.success) out.insights = parsed.data;
96
+ } catch {
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ function topN(counts, n) {
102
+ return [...counts.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)).slice(0, n);
103
+ }
104
+ function computeReport(storeDir, now) {
105
+ const refs = listSessionDirs(storeDir);
106
+ const loaded = refs.map(loadSession);
107
+ const hosts = {};
108
+ const byMonth = {};
109
+ const totals = { input_tokens: 0, output_tokens: 0, cost_usd: 0 };
110
+ const topicCounts = /* @__PURE__ */ new Map();
111
+ const tagCounts = /* @__PURE__ */ new Map();
112
+ const tagToSessions = /* @__PURE__ */ new Map();
113
+ const signalCounts = {};
114
+ for (const { ref, envelope, insights } of loaded) {
115
+ hosts[ref.host] = (hosts[ref.host] ?? 0) + 1;
116
+ const month = byMonth[ref.month] ??= { sessions: 0, cost_usd: 0 };
117
+ month.sessions++;
118
+ if (envelope) {
119
+ totals.input_tokens += envelope.totals.input_tokens;
120
+ totals.output_tokens += envelope.totals.output_tokens;
121
+ totals.cost_usd += envelope.totals.cost_usd;
122
+ month.cost_usd += envelope.totals.cost_usd;
123
+ }
124
+ if (insights) {
125
+ if (insights.topic) topicCounts.set(insights.topic, (topicCounts.get(insights.topic) ?? 0) + 1);
126
+ for (const tag of insights.tags) {
127
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
128
+ (tagToSessions.get(tag) ?? tagToSessions.set(tag, []).get(tag)).push(ref.sessionId);
129
+ }
130
+ for (const sig of insights.signals) {
131
+ signalCounts[sig.kind] = (signalCounts[sig.kind] ?? 0) + 1;
132
+ }
133
+ }
134
+ }
135
+ totals.cost_usd = Math.round(totals.cost_usd * 1e6) / 1e6;
136
+ for (const m of Object.values(byMonth)) m.cost_usd = Math.round(m.cost_usd * 1e6) / 1e6;
137
+ const similar = [...tagToSessions.entries()].filter(([, s]) => s.length >= 2).map(([tag, sessions]) => ({ tag, sessions: [...new Set(sessions)] })).sort((a, b) => b.sessions.length - a.sessions.length).slice(0, 10);
138
+ return {
139
+ generated_at: now,
140
+ sessions: loaded.length,
141
+ hosts,
142
+ totals,
143
+ byMonth,
144
+ topTopics: topN(topicCounts, 10).map(({ key, count }) => ({ topic: key, count })),
145
+ topTags: topN(tagCounts, 15).map(({ key, count }) => ({ tag: key, count })),
146
+ signalCounts,
147
+ similar
148
+ };
149
+ }
150
+
151
+ // src/analytics/site.ts
152
+ function esc(s) {
153
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
154
+ }
155
+ function renderSite(report) {
156
+ const rows = (pairs) => pairs.map(([k, v]) => `<tr><td>${esc(k)}</td><td>${esc(String(v))}</td></tr>`).join("");
157
+ const topics = report.topTopics.map((t) => `<li>${esc(t.topic)} \u2014 ${t.count}</li>`).join("");
158
+ const tags = report.topTags.map((t) => `<span class="tag">${esc(t.tag)} (${t.count})</span>`).join(" ");
159
+ const signals = Object.entries(report.signalCounts).map(([k, v]) => `<li>${esc(k)}: ${v}</li>`).join("");
160
+ return `<!doctype html>
161
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
162
+ <title>code-sessions \u2014 analytics</title>
163
+ <style>
164
+ body{font:14px/1.5 system-ui,sans-serif;margin:2rem auto;max-width:720px;color:#111}
165
+ h1{margin-bottom:.2rem} .muted{color:#666}
166
+ table{border-collapse:collapse;margin:1rem 0} td{padding:.2rem .8rem;border-bottom:1px solid #eee}
167
+ .tag{display:inline-block;background:#eef;border-radius:4px;padding:.1rem .4rem;margin:.1rem}
168
+ ul{margin:.3rem 0}
169
+ </style></head><body>
170
+ <h1>code-sessions</h1>
171
+ <div class="muted">analytics \xB7 generated ${esc(report.generated_at)}</div>
172
+ <table>${rows([
173
+ ["Sessions", report.sessions],
174
+ ["Input tokens", report.totals.input_tokens],
175
+ ["Output tokens", report.totals.output_tokens],
176
+ ["Estimated cost (USD)", report.totals.cost_usd.toFixed(2)]
177
+ ])}</table>
178
+ <h2>Top topics</h2><ul>${topics || '<li class="muted">none</li>'}</ul>
179
+ <h2>Top tags</h2><div>${tags || '<span class="muted">none</span>'}</div>
180
+ <h2>Signals</h2><ul>${signals || '<li class="muted">none</li>'}</ul>
181
+ </body></html>
182
+ `;
183
+ }
184
+
185
+ // src/analytics/command.ts
186
+ async function cmdAnalytics(cfg, opts = {}) {
187
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
188
+ const report = computeReport(cfg.storeDir, now);
189
+ const dir = join(cfg.storeDir, "analytics");
190
+ mkdirSync(dir, { recursive: true });
191
+ writeFileSync(join(dir, "report.json"), `${JSON.stringify(report, null, 2)}
192
+ `);
193
+ writeFileSync(join(dir, "digest.md"), renderDigest(report));
194
+ writeFileSync(join(dir, "index.html"), renderSite(report));
195
+ const git = new GitStore(cfg.storeDir, {
196
+ ...cfg.git.remote ? { remote: cfg.git.remote } : {},
197
+ autoPush: cfg.git.autoPush
198
+ });
199
+ if (git.isRepo()) git.sync(`analytics rollup (${report.sessions} sessions)`);
200
+ return {
201
+ code: 0,
202
+ output: `Analytics written for ${report.sessions} session(s) \u2192 ${dir}`
203
+ };
204
+ }
205
+
206
+ // src/cli.ts
207
+ function emit(res) {
208
+ if (res.output) process.stdout.write(`${res.output}
209
+ `);
210
+ process.exit(res.code);
211
+ }
212
+ async function main(argv) {
213
+ const command = argv[0];
214
+ const flags = parseFlags(argv.slice(1));
215
+ const cfg = loadConfig(overridesFromFlags(flags));
216
+ switch (command) {
217
+ case "init":
218
+ emit(cmdInit(cfg));
219
+ break;
220
+ case "status":
221
+ emit(cmdStatus(cfg));
222
+ break;
223
+ case "doctor":
224
+ emit(cmdDoctor(cfg));
225
+ break;
226
+ case "install-hooks":
227
+ emit(
228
+ cmdInstallHooks(cfg, {
229
+ ...typeof flags.settings === "string" ? { settingsPath: flags.settings } : {},
230
+ ...typeof flags.command === "string" ? { command: flags.command } : {}
231
+ })
232
+ );
233
+ break;
234
+ case "backfill":
235
+ emit(
236
+ await cmdBackfill(cfg, {
237
+ ...typeof flags.projects === "string" ? { projectsDir: flags.projects } : {},
238
+ ...typeof flags.agent === "string" ? { agent: flags.agent } : {}
239
+ })
240
+ );
241
+ break;
242
+ case "reindex":
243
+ emit(await cmdReindex(cfg, typeof flags.since === "string" ? { since: flags.since } : {}));
244
+ break;
245
+ case "analytics":
246
+ emit(await cmdAnalytics(cfg));
247
+ break;
248
+ case "export":
249
+ emit(await cmdExport(cfg, typeof flags.since === "string" ? { since: flags.since } : {}));
250
+ break;
251
+ case "index":
252
+ emit(cmdIndex(cfg));
253
+ break;
254
+ case "query":
255
+ emit(
256
+ cmdQuery(cfg, {
257
+ ...typeof flags.limit === "string" ? { limit: Number(flags.limit) } : {},
258
+ ...typeof flags.agent === "string" ? { agent: flags.agent } : {}
259
+ })
260
+ );
261
+ break;
262
+ case "search": {
263
+ const q = argv.slice(1).find((a) => !a.startsWith("--")) ?? "";
264
+ emit(cmdSearch(cfg, { query: q, ...typeof flags.limit === "string" ? { limit: Number(flags.limit) } : {} }));
265
+ break;
266
+ }
267
+ case "hook": {
268
+ try {
269
+ const input = await readStdin();
270
+ await handleHookInput(cfg.socketPath, input);
271
+ } catch {
272
+ }
273
+ process.exit(0);
274
+ break;
275
+ }
276
+ case "start": {
277
+ const daemon = await startDaemon(cfg);
278
+ process.stdout.write(`code-sessions daemon listening on ${cfg.socketPath}
279
+ `);
280
+ const stop = async () => {
281
+ await daemon.stop();
282
+ process.exit(0);
283
+ };
284
+ process.on("SIGINT", stop);
285
+ process.on("SIGTERM", stop);
286
+ break;
287
+ }
288
+ case "help":
289
+ case "--help":
290
+ case void 0:
291
+ process.stdout.write(HELP);
292
+ process.exit(command ? 0 : 1);
293
+ break;
294
+ default:
295
+ process.stderr.write(`Unknown command: ${command}
296
+
297
+ ${HELP}`);
298
+ process.exit(1);
299
+ }
300
+ }
301
+ main(process.argv.slice(2)).catch((err) => {
302
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
303
+ `);
304
+ process.exit(1);
305
+ });
306
+ export {
307
+ main
308
+ };
package/dist/index.js ADDED
@@ -0,0 +1,162 @@
1
+ import {
2
+ CaptureEngine,
3
+ DEFAULT_HOOK_EVENTS,
4
+ Daemon,
5
+ FakeProvider,
6
+ GitStore,
7
+ LlmProvider,
8
+ SessionIndex,
9
+ StateStore,
10
+ THRESHOLDS,
11
+ applyHygiene,
12
+ buildMetricPayload,
13
+ buildPrompt,
14
+ buildTracePayload,
15
+ claudeRunner,
16
+ cmdBackfill,
17
+ cmdDoctor,
18
+ cmdExport,
19
+ cmdIndex,
20
+ cmdInit,
21
+ cmdInstallHooks,
22
+ cmdQuery,
23
+ cmdReindex,
24
+ cmdSearch,
25
+ cmdStatus,
26
+ codexSessionsRoot,
27
+ computeEnvelope,
28
+ defaultConfig,
29
+ deriveSignals,
30
+ deriveTags,
31
+ discoverCodexSessions,
32
+ discoverGrokSessions,
33
+ envelopeFile,
34
+ estimateCostUsd,
35
+ exportSession,
36
+ exportStore,
37
+ findTranscript,
38
+ grokRunner,
39
+ grokSessionsRoot,
40
+ guessTopic,
41
+ handleHookInput,
42
+ insightsFile,
43
+ installHooks,
44
+ isSessionEndEvent,
45
+ isoNano,
46
+ labelSession,
47
+ listClaudeTranscripts,
48
+ listSessionDirs,
49
+ loadConfig,
50
+ makeProvider,
51
+ mergeHooks,
52
+ monthOf,
53
+ ollamaRunner,
54
+ overridesFromFlags,
55
+ parseCodexSession,
56
+ parseFlags,
57
+ parseGrokSession,
58
+ parseHookEvent,
59
+ parseLabelJson,
60
+ postOtlp,
61
+ priceFor,
62
+ rawBlobFile,
63
+ readEntries,
64
+ readNewLines,
65
+ readStdin,
66
+ readTurns,
67
+ rebuildEnvelope,
68
+ reindexStore,
69
+ resolveConfig,
70
+ scrubSecrets,
71
+ sendEvent,
72
+ sessionDir,
73
+ sha256,
74
+ startDaemon,
75
+ syncIndex,
76
+ telemetryFile,
77
+ turnFile,
78
+ writeBlobFile,
79
+ writeImportedSession,
80
+ writeTurnFile
81
+ } from "./chunk-ZJG2DWAK.js";
82
+ export {
83
+ CaptureEngine,
84
+ DEFAULT_HOOK_EVENTS,
85
+ Daemon,
86
+ FakeProvider,
87
+ GitStore,
88
+ LlmProvider,
89
+ SessionIndex,
90
+ StateStore,
91
+ THRESHOLDS,
92
+ applyHygiene,
93
+ buildMetricPayload,
94
+ buildPrompt,
95
+ buildTracePayload,
96
+ claudeRunner,
97
+ cmdBackfill,
98
+ cmdDoctor,
99
+ cmdExport,
100
+ cmdIndex,
101
+ cmdInit,
102
+ cmdInstallHooks,
103
+ cmdQuery,
104
+ cmdReindex,
105
+ cmdSearch,
106
+ cmdStatus,
107
+ codexSessionsRoot,
108
+ computeEnvelope,
109
+ defaultConfig,
110
+ deriveSignals,
111
+ deriveTags,
112
+ discoverCodexSessions,
113
+ discoverGrokSessions,
114
+ envelopeFile,
115
+ estimateCostUsd,
116
+ exportSession,
117
+ exportStore,
118
+ findTranscript,
119
+ grokRunner,
120
+ grokSessionsRoot,
121
+ guessTopic,
122
+ handleHookInput,
123
+ insightsFile,
124
+ installHooks,
125
+ isSessionEndEvent,
126
+ isoNano,
127
+ labelSession,
128
+ listClaudeTranscripts,
129
+ listSessionDirs,
130
+ loadConfig,
131
+ makeProvider,
132
+ mergeHooks,
133
+ monthOf,
134
+ ollamaRunner,
135
+ overridesFromFlags,
136
+ parseCodexSession,
137
+ parseFlags,
138
+ parseGrokSession,
139
+ parseHookEvent,
140
+ parseLabelJson,
141
+ postOtlp,
142
+ priceFor,
143
+ rawBlobFile,
144
+ readEntries,
145
+ readNewLines,
146
+ readStdin,
147
+ readTurns,
148
+ rebuildEnvelope,
149
+ reindexStore,
150
+ resolveConfig,
151
+ scrubSecrets,
152
+ sendEvent,
153
+ sessionDir,
154
+ sha256,
155
+ startDaemon,
156
+ syncIndex,
157
+ telemetryFile,
158
+ turnFile,
159
+ writeBlobFile,
160
+ writeImportedSession,
161
+ writeTurnFile
162
+ };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@unpolarize/code-sessions",
3
+ "version": "0.1.0",
4
+ "description": "Headless, event-driven cross-agent session capture agent (daemon + CLI)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "code-sessions": "./bin/code-sessions.mjs"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": ["dist", "src", "bin"],
12
+ "publishConfig": { "access": "public" },
13
+ "repository": { "type": "git", "url": "git+https://github.com/unpolarize/code-sessions.git", "directory": "packages/agent" },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts src/cli.ts --format esm --clean --out-dir dist"
16
+ },
17
+ "dependencies": {
18
+ "@unpolarize/code-sessions-schema": "^0.1.0",
19
+ "zod": "^3.23.8"
20
+ }
21
+ }
@@ -0,0 +1,121 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { sessionDir, turnFile, envelopeFile } from '../store/paths';
5
+ import { makeConfig, withTempDir } from '../test/tmp';
6
+ import { discoverCodexSessions, parseCodexSession } from './codex';
7
+ import { discoverGrokSessions, parseGrokSession } from './grok';
8
+ import { writeImportedSession } from './import';
9
+
10
+ function seedGrok(root: string): void {
11
+ const dir = join(root, '%2FUsers%2Fx%2Fprojects%2Ffoo', 'gg-uuid-1');
12
+ mkdirSync(dir, { recursive: true });
13
+ writeFileSync(
14
+ join(dir, 'summary.json'),
15
+ JSON.stringify({
16
+ created_at: '2026-06-20T08:00:00Z',
17
+ generated_title: 'Fix foo bug',
18
+ current_model_id: 'grok-build',
19
+ info: { cwd: '/Users/x/projects/foo' },
20
+ }),
21
+ );
22
+ writeFileSync(
23
+ join(dir, 'chat_history.jsonl'),
24
+ [
25
+ '{"type":"system","content":"sys"}',
26
+ '{"type":"user","content":"Fix the bug in foo.ts"}',
27
+ '{"type":"reasoning","summary":"thinking"}',
28
+ '{"type":"assistant","content":"I\'ll edit it","model_id":"grok-build","tool_calls":[{"id":"c1","name":"Read","arguments":"{\\"path\\":\\"/Users/x/projects/foo/a.ts\\"}"}]}',
29
+ '{"type":"tool_result","tool_call_id":"c1","content":"file contents"}',
30
+ ].join('\n'),
31
+ );
32
+ }
33
+
34
+ describe('grok adapter', () => {
35
+ it('discovers and parses a grok session into canonical turns', () => {
36
+ withTempDir((root) => {
37
+ seedGrok(root);
38
+ const found = discoverGrokSessions(root);
39
+ expect(found).toHaveLength(1);
40
+ const imported = parseGrokSession(found[0]!, 'test-host')!;
41
+ expect(imported.agent).toBe('grok');
42
+ expect(imported.turns.map((t) => t.role)).toEqual(['user', 'assistant', 'tool']);
43
+ expect(imported.turns[1]!.tool_calls[0]).toMatchObject({ name: 'Read' });
44
+ expect(imported.meta.model).toBe('grok-build');
45
+ expect(imported.meta.title).toBe('Fix foo bug');
46
+ expect(imported.meta.started_at).toBe('2026-06-20T08:00:00.000Z');
47
+ });
48
+ });
49
+
50
+ it('skips claude_import grok sessions', () => {
51
+ withTempDir((root) => {
52
+ const dir = join(root, '%2Fx', 'ci');
53
+ mkdirSync(dir, { recursive: true });
54
+ writeFileSync(join(dir, 'summary.json'), JSON.stringify({ session_kind: 'claude_import' }));
55
+ writeFileSync(join(dir, 'chat_history.jsonl'), '{"type":"user","content":"hi"}');
56
+ expect(parseGrokSession(discoverGrokSessions(root)[0]!, 'h')).toBeNull();
57
+ });
58
+ });
59
+ });
60
+
61
+ function seedCodex(root: string): void {
62
+ const dir = join(root, '2026', '06', '20');
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(
65
+ join(dir, 'rollout-2026-06-20T09-00-00-11111111-2222-3333-4444-555555555555.jsonl'),
66
+ [
67
+ '{"timestamp":"2026-06-20T09:00:00Z","type":"session_meta","payload":{"id":"11111111-2222-3333-4444-555555555555","timestamp":"2026-06-20T09:00:00Z","model":"gpt-5-codex","cwd":"/Users/x/proj"}}',
68
+ '{"timestamp":"2026-06-20T09:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"t1"}}',
69
+ '{"timestamp":"2026-06-20T09:00:02Z","type":"response_item","payload":{"type":"message","role":"developer","content":[{"type":"input_text","text":"<permission scaffolding>"}]}}',
70
+ '{"timestamp":"2026-06-20T09:00:03Z","type":"event_msg","payload":{"type":"user_message","message":"print 42","images":[]}}',
71
+ '{"timestamp":"2026-06-20T09:00:04Z","type":"response_item","payload":{"type":"function_call","name":"shell","arguments":"{\\"command\\":\\"echo 42\\"}"}}',
72
+ '{"timestamp":"2026-06-20T09:00:05Z","type":"event_msg","payload":{"type":"agent_message","message":"42","phase":"final_answer"}}',
73
+ '{"timestamp":"2026-06-20T09:00:06Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":100,"cached_input_tokens":10,"output_tokens":5}}}}',
74
+ '{"timestamp":"2026-06-20T09:00:07Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"t1"}}',
75
+ ].join('\n'),
76
+ );
77
+ }
78
+
79
+ describe('codex adapter', () => {
80
+ it('discovers and parses a codex rollout into canonical turns', () => {
81
+ withTempDir((root) => {
82
+ seedCodex(root);
83
+ const found = discoverCodexSessions(root);
84
+ expect(found).toHaveLength(1);
85
+ expect(found[0]!.sessionId).toBe('11111111-2222-3333-4444-555555555555');
86
+ const imported = parseCodexSession(found[0]!, 'test-host')!;
87
+ expect(imported.agent).toBe('codex');
88
+ expect(imported.meta.model).toBe('gpt-5-codex');
89
+ expect(imported.meta.project_path).toBe('/Users/x/proj');
90
+ expect(imported.turns.map((t) => t.role)).toEqual(['user', 'assistant', 'assistant']);
91
+ expect(imported.turns[0]!.text).toBe('print 42'); // from event_msg/user_message
92
+ expect(imported.turns[1]!.tool_calls[0]).toMatchObject({ name: 'shell' });
93
+ expect(imported.turns[2]!.text).toBe('42'); // from event_msg/agent_message
94
+ // cumulative token_count attributed to the final assistant turn
95
+ expect(imported.turns[2]!.usage.input_tokens).toBe(100);
96
+ expect(imported.turns[2]!.usage.cache_read_tokens).toBe(10);
97
+ });
98
+ });
99
+ });
100
+
101
+ describe('writeImportedSession', () => {
102
+ it('writes per-turn files + envelope for an imported session', () => {
103
+ withTempDir((store) => {
104
+ seedGrokInStore(store);
105
+ });
106
+ });
107
+ });
108
+
109
+ function seedGrokInStore(store: string): void {
110
+ const grokRoot = join(store, 'grok');
111
+ seedGrok(grokRoot);
112
+ const imported = parseGrokSession(discoverGrokSessions(grokRoot)[0]!, 'other-host')!;
113
+ const cfg = makeConfig(store);
114
+ const res = writeImportedSession(cfg, imported);
115
+ const dir = sessionDir(store, 'other-host', '2026-06', 'gg-uuid-1');
116
+ expect(res.sessionDir).toBe(dir);
117
+ expect(existsSync(turnFile(dir, 0))).toBe(true);
118
+ expect(existsSync(envelopeFile(dir))).toBe(true);
119
+ expect(res.envelope.agent).toBe('grok');
120
+ expect(res.envelope.native_ref.format).toBe('grok-jsonl');
121
+ }