@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
@@ -0,0 +1,2321 @@
1
+ // src/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { homedir, hostname } from "os";
4
+ import { join } from "path";
5
+ function defaultConfig(home = homedir(), host = hostname()) {
6
+ const storeDir = join(home, ".sessions");
7
+ const runtimeDir = join(storeDir, ".daemon");
8
+ return {
9
+ host,
10
+ agent: "claude-code",
11
+ storeDir,
12
+ runtimeDir,
13
+ socketPath: join(runtimeDir, "daemon.sock"),
14
+ statePath: join(runtimeDir, "state.json"),
15
+ indexPath: join(runtimeDir, "index.db"),
16
+ claudeProjectsDir: join(home, ".claude", "projects"),
17
+ batch: { maxTurns: 8, maxIntervalMs: 5e3 },
18
+ hygiene: { maxTurnBytes: 64 * 1024, scrubSecrets: true },
19
+ git: { autoCommit: true, autoPush: false },
20
+ insights: { provider: "none", mode: "off" },
21
+ telemetry: {
22
+ enabled: true,
23
+ endpoint: "http://localhost:4318",
24
+ serviceName: "code-sessions",
25
+ timeoutMs: 2e3
26
+ }
27
+ };
28
+ }
29
+ function resolveConfig(base, override = {}) {
30
+ const merged = {
31
+ ...base,
32
+ ...stripUndefined(override),
33
+ batch: { ...base.batch, ...stripUndefined(override.batch) },
34
+ hygiene: { ...base.hygiene, ...stripUndefined(override.hygiene) },
35
+ git: { ...base.git, ...stripUndefined(override.git) },
36
+ insights: { ...base.insights, ...stripUndefined(override.insights) },
37
+ telemetry: { ...base.telemetry, ...stripUndefined(override.telemetry) }
38
+ };
39
+ if (override.storeDir && override.runtimeDir === void 0) {
40
+ merged.runtimeDir = join(merged.storeDir, ".daemon");
41
+ }
42
+ if (merged.socketPath === base.socketPath && override.socketPath === void 0) {
43
+ merged.socketPath = join(merged.runtimeDir, "daemon.sock");
44
+ }
45
+ if (merged.statePath === base.statePath && override.statePath === void 0) {
46
+ merged.statePath = join(merged.runtimeDir, "state.json");
47
+ }
48
+ if (merged.indexPath === base.indexPath && override.indexPath === void 0) {
49
+ merged.indexPath = join(merged.runtimeDir, "index.db");
50
+ }
51
+ return merged;
52
+ }
53
+ function loadConfig(override = {}) {
54
+ let cfg = defaultConfig();
55
+ const configPath = join(cfg.storeDir, "config.json");
56
+ if (existsSync(configPath)) {
57
+ try {
58
+ const fileCfg = JSON.parse(readFileSync(configPath, "utf8"));
59
+ cfg = resolveConfig(cfg, fileCfg);
60
+ } catch {
61
+ }
62
+ }
63
+ cfg = resolveConfig(cfg, envOverrides());
64
+ cfg = resolveConfig(cfg, override);
65
+ return cfg;
66
+ }
67
+ function envOverrides() {
68
+ const o = {};
69
+ const env = process.env;
70
+ if (env.CODE_SESSIONS_STORE) o.storeDir = env.CODE_SESSIONS_STORE;
71
+ if (env.CODE_SESSIONS_HOST) o.host = env.CODE_SESSIONS_HOST;
72
+ if (env.CODE_SESSIONS_REMOTE) o.git = { remote: env.CODE_SESSIONS_REMOTE };
73
+ if (env.CODE_SESSIONS_INSIGHTS_PROVIDER)
74
+ o.insights = { provider: env.CODE_SESSIONS_INSIGHTS_PROVIDER };
75
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) o.telemetry = { endpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT };
76
+ if (env.CODE_SESSIONS_TELEMETRY === "0" || env.CODE_SESSIONS_TELEMETRY === "false")
77
+ o.telemetry = { ...o.telemetry ?? {}, enabled: false };
78
+ return o;
79
+ }
80
+ function stripUndefined(obj) {
81
+ if (!obj) return {};
82
+ const out = {};
83
+ for (const [k, v] of Object.entries(obj)) {
84
+ if (v !== void 0) out[k] = v;
85
+ }
86
+ return out;
87
+ }
88
+
89
+ // src/cliargs.ts
90
+ function parseFlags(args) {
91
+ const flags = {};
92
+ for (let i = 0; i < args.length; i++) {
93
+ const a = args[i];
94
+ if (!a.startsWith("--")) continue;
95
+ const body = a.slice(2);
96
+ const eq = body.indexOf("=");
97
+ if (eq >= 0) {
98
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
99
+ } else {
100
+ const next = args[i + 1];
101
+ if (next !== void 0 && !next.startsWith("--")) {
102
+ flags[body] = next;
103
+ i++;
104
+ } else {
105
+ flags[body] = true;
106
+ }
107
+ }
108
+ }
109
+ return flags;
110
+ }
111
+ function overridesFromFlags(flags) {
112
+ const o = {};
113
+ if (typeof flags.store === "string") o.storeDir = flags.store;
114
+ if (typeof flags.host === "string") o.host = flags.host;
115
+ if (typeof flags.remote === "string") o.git = { remote: flags.remote };
116
+ if (flags.push === true) o.git = { ...o.git ?? {}, autoPush: true };
117
+ const insights = {};
118
+ if (typeof flags.provider === "string") insights.provider = flags.provider;
119
+ if (typeof flags.mode === "string") insights.mode = flags.mode;
120
+ if (typeof flags.model === "string") insights.model = flags.model;
121
+ if (Object.keys(insights).length > 0) o.insights = insights;
122
+ const telemetry = {};
123
+ if (typeof flags.endpoint === "string") telemetry.endpoint = flags.endpoint;
124
+ if (flags["no-telemetry"] === true || flags["telemetry"] === false) telemetry.enabled = false;
125
+ if (Object.keys(telemetry).length > 0) o.telemetry = telemetry;
126
+ return o;
127
+ }
128
+ var HELP = `code-sessions \u2014 headless cross-agent session capture
129
+
130
+ Usage: code-sessions <command> [flags]
131
+
132
+ Commands:
133
+ init Initialize the git-backed store (~/.sessions)
134
+ start Run the capture daemon (foreground)
135
+ install-hooks Install Claude Code hooks that feed the daemon
136
+ hook (internal) forward a hook payload from stdin to the daemon
137
+ backfill Import existing transcripts into the store [--agent claude|grok|codex|all]
138
+ reindex (Re)derive insights for stored sessions [--since YYYY-MM]
139
+ export Export stored sessions as OTLP to a collector [--since YYYY-MM]
140
+ index (Re)build the internal SQLite index from the git store
141
+ query List recent sessions from the index [--limit N] [--agent X]
142
+ search Full-text search session turns <text> [--limit N]
143
+ analytics Compute MVP-2 rollups + digest into analytics/
144
+ status Show daemon/store status
145
+ doctor Environment checks
146
+
147
+ Flags:
148
+ --store <dir> store dir (default ~/.sessions)
149
+ --host <name> logical host id
150
+ --remote <url> git remote for the store
151
+ --push push after commit
152
+ --provider <p> insights provider: none|fake|claude|grok|ollama
153
+ --mode <m> insights mode: off|on-stop|per-turn
154
+ --model <m> provider model
155
+ --since <YYYY-MM> reindex/export: only sessions since month
156
+ --endpoint <url> OTLP/HTTP collector base (default http://localhost:4318)
157
+ --no-telemetry disable OTLP export
158
+ --settings <path> install-hooks: target settings.json
159
+ `;
160
+
161
+ // src/hygiene.ts
162
+ import { createHash } from "crypto";
163
+ var PATTERNS = [
164
+ { kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
165
+ { kind: "github-token", re: /\bgh[posru]_[A-Za-z0-9]{36,255}\b/g },
166
+ { kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
167
+ { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
168
+ { kind: "slack-token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g },
169
+ { kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
170
+ { kind: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
171
+ { kind: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/g },
172
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g }
173
+ ];
174
+ function scrubSecrets(text) {
175
+ let out = text;
176
+ const matches = [];
177
+ for (const { kind, re } of PATTERNS) {
178
+ let count = 0;
179
+ out = out.replace(re, () => {
180
+ count++;
181
+ return `[REDACTED:${kind}]`;
182
+ });
183
+ if (count > 0) matches.push({ kind, count });
184
+ }
185
+ return { text: out, matches };
186
+ }
187
+ function sha256(content) {
188
+ return createHash("sha256").update(content, "utf8").digest("hex");
189
+ }
190
+ var PREVIEW_CHARS = 280;
191
+ function applyHygiene(turn, opts) {
192
+ let text = turn.text;
193
+ let scrubbed = turn.scrubbed;
194
+ let raw = turn.raw;
195
+ const redactions = [];
196
+ if (opts.scrubSecrets) {
197
+ const res = scrubSecrets(text);
198
+ if (res.matches.length > 0) {
199
+ text = res.text;
200
+ scrubbed = true;
201
+ raw = void 0;
202
+ redactions.push(...res.matches);
203
+ }
204
+ }
205
+ let blob;
206
+ let rawRef = turn.raw_ref;
207
+ const bytes = Buffer.byteLength(text, "utf8");
208
+ if (bytes > opts.maxTurnBytes) {
209
+ const sha = sha256(text);
210
+ blob = { sha, content: text };
211
+ rawRef = sha;
212
+ raw = void 0;
213
+ const preview = text.slice(0, PREVIEW_CHARS);
214
+ text = `${preview}
215
+ \u2026[externalized ${bytes}B \u2192 raw/${sha}]`;
216
+ }
217
+ const next = {
218
+ ...turn,
219
+ text,
220
+ scrubbed,
221
+ raw_ref: rawRef,
222
+ ...raw === void 0 ? { raw: void 0 } : { raw }
223
+ };
224
+ return { turn: next, ...blob ? { blob } : {}, redactions };
225
+ }
226
+
227
+ // src/pricing.ts
228
+ var PRICES = {
229
+ "claude-opus-4-8": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
230
+ "claude-opus": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
231
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
232
+ "claude-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
233
+ "claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
234
+ "claude-haiku": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 }
235
+ };
236
+ var DEFAULT_PRICE = PRICES["claude-sonnet"];
237
+ function priceFor(model) {
238
+ if (!model) return DEFAULT_PRICE;
239
+ const lower = model.toLowerCase();
240
+ for (const key of Object.keys(PRICES)) {
241
+ if (lower.includes(key)) return PRICES[key];
242
+ }
243
+ if (lower.includes("opus")) return PRICES["claude-opus"];
244
+ if (lower.includes("sonnet")) return PRICES["claude-sonnet"];
245
+ if (lower.includes("haiku")) return PRICES["claude-haiku"];
246
+ return DEFAULT_PRICE;
247
+ }
248
+ function estimateCostUsd(usage, model) {
249
+ const p = priceFor(model);
250
+ const dollars = (usage.input_tokens * p.input + usage.output_tokens * p.output + usage.cache_read_tokens * p.cacheRead + usage.cache_write_tokens * p.cacheWrite) / 1e6;
251
+ return Math.round(dollars * 1e6) / 1e6;
252
+ }
253
+
254
+ // src/store/paths.ts
255
+ import { join as join2 } from "path";
256
+ function monthOf(ts) {
257
+ if (ts) {
258
+ const m = /^(\d{4})-(\d{2})/.exec(ts);
259
+ if (m) return `${m[1]}-${m[2]}`;
260
+ }
261
+ return "unknown";
262
+ }
263
+ function sessionDir(storeDir, host, month, sessionId) {
264
+ return join2(storeDir, "hosts", host, month, sessionId);
265
+ }
266
+ function turnFile(dir, index) {
267
+ return join2(dir, "turns", `${String(index).padStart(6, "0")}.json`);
268
+ }
269
+ function envelopeFile(dir) {
270
+ return join2(dir, "session.json");
271
+ }
272
+ function insightsFile(dir) {
273
+ return join2(dir, "insights", "labels.json");
274
+ }
275
+ function telemetryFile(dir) {
276
+ return join2(dir, "telemetry", "otel.jsonl");
277
+ }
278
+ function rawBlobFile(dir, sha) {
279
+ return join2(dir, "raw", sha);
280
+ }
281
+
282
+ // src/store/writer.ts
283
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
284
+ import { dirname } from "path";
285
+ import {
286
+ SCHEMA_VERSIONS
287
+ } from "@unpolarize/code-sessions-schema";
288
+ function ensureDir(filePath) {
289
+ mkdirSync(dirname(filePath), { recursive: true });
290
+ }
291
+ function writeJsonAtomic(filePath, value) {
292
+ ensureDir(filePath);
293
+ const tmp = `${filePath}.tmp`;
294
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
295
+ `);
296
+ renameSync(tmp, filePath);
297
+ }
298
+ function writeTurnFile(dir, turn) {
299
+ const path = turnFile(dir, turn.turn_index);
300
+ if (existsSync2(path)) return { path, written: false };
301
+ ensureDir(path);
302
+ writeFileSync(path, `${JSON.stringify(turn, null, 2)}
303
+ `);
304
+ return { path, written: true };
305
+ }
306
+ function writeBlobFile(dir, sha, content) {
307
+ const path = rawBlobFile(dir, sha);
308
+ if (!existsSync2(path)) {
309
+ ensureDir(path);
310
+ writeFileSync(path, content);
311
+ }
312
+ return path;
313
+ }
314
+ function readTurns(dir) {
315
+ const turnsDir = `${dir}/turns`;
316
+ if (!existsSync2(turnsDir)) return [];
317
+ return readdirSync(turnsDir).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).sort().map((f) => JSON.parse(readFileSync2(`${turnsDir}/${f}`, "utf8")));
318
+ }
319
+ function computeEnvelope(turns, meta, identity, existing) {
320
+ let inputTokens = 0;
321
+ let outputTokens = 0;
322
+ let cost = 0;
323
+ let toolCalls = 0;
324
+ for (const t of turns) {
325
+ inputTokens += t.usage.input_tokens;
326
+ outputTokens += t.usage.output_tokens;
327
+ toolCalls += t.tool_calls.length;
328
+ cost += t.telemetry?.cost_usd ?? 0;
329
+ }
330
+ const first = turns[0];
331
+ const last = turns[turns.length - 1];
332
+ const env = {
333
+ schema: SCHEMA_VERSIONS.session,
334
+ session_id: identity.session_id,
335
+ host: identity.host,
336
+ agent: identity.agent,
337
+ project_path: meta.project_path ?? existing?.project_path ?? "",
338
+ turn_count: turns.length,
339
+ tool_call_count: toolCalls,
340
+ totals: {
341
+ input_tokens: inputTokens,
342
+ output_tokens: outputTokens,
343
+ cost_usd: Math.round(cost * 1e6) / 1e6
344
+ },
345
+ labels: existing?.labels ?? [],
346
+ native_ref: { format: "claude-jsonl", uuid: identity.native_uuid }
347
+ };
348
+ const branch = meta.git_branch ?? existing?.git_branch;
349
+ if (branch) env.git_branch = branch;
350
+ const model = meta.model ?? existing?.model;
351
+ if (model) env.model = model;
352
+ const startedAt = meta.started_at ?? first?.ts ?? existing?.started_at;
353
+ if (startedAt) env.started_at = startedAt;
354
+ const endedAt = meta.ended_at ?? last?.ts ?? existing?.ended_at;
355
+ if (endedAt) env.ended_at = endedAt;
356
+ const title = meta.title ?? existing?.title;
357
+ if (title) env.title = title;
358
+ return env;
359
+ }
360
+ function rebuildEnvelope(storeDir, host, month, sessionId, meta, identity) {
361
+ const dir = sessionDir(storeDir, host, month, sessionId);
362
+ const turns = readTurns(dir);
363
+ const envPath = envelopeFile(dir);
364
+ let existing;
365
+ if (existsSync2(envPath)) {
366
+ try {
367
+ existing = JSON.parse(readFileSync2(envPath, "utf8"));
368
+ } catch {
369
+ }
370
+ }
371
+ const env = computeEnvelope(turns, meta, identity, existing);
372
+ writeJsonAtomic(envPath, env);
373
+ return env;
374
+ }
375
+
376
+ // src/tail.ts
377
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
378
+ function readNewLines(filePath, fromOffset) {
379
+ const empty = { records: [], rawLines: [], newOffset: fromOffset, parseErrors: 0 };
380
+ if (!existsSync3(filePath)) return empty;
381
+ const buf = readFileSync3(filePath);
382
+ if (fromOffset >= buf.length) return { ...empty, newOffset: buf.length };
383
+ const slice = buf.subarray(fromOffset);
384
+ const lastNewline = slice.lastIndexOf(10);
385
+ if (lastNewline < 0) return empty;
386
+ const complete = slice.subarray(0, lastNewline + 1).toString("utf8");
387
+ const consumedBytes = Buffer.byteLength(complete, "utf8");
388
+ const records = [];
389
+ const rawLines = [];
390
+ let parseErrors = 0;
391
+ for (const line of complete.split("\n")) {
392
+ if (line.trim().length === 0) continue;
393
+ rawLines.push(line);
394
+ try {
395
+ records.push(JSON.parse(line));
396
+ } catch {
397
+ parseErrors++;
398
+ }
399
+ }
400
+ return { records, rawLines, newOffset: fromOffset + consumedBytes, parseErrors };
401
+ }
402
+
403
+ // src/capture.ts
404
+ import {
405
+ buildTurn,
406
+ extractClaudeSessionMeta,
407
+ normalizeClaudeEvent
408
+ } from "@unpolarize/code-sessions-schema";
409
+ var CaptureEngine = class {
410
+ constructor(config, state) {
411
+ this.config = config;
412
+ this.state = state;
413
+ }
414
+ config;
415
+ state;
416
+ captureSession(sessionId, transcriptPath) {
417
+ const st = this.state.ensure(sessionId, transcriptPath);
418
+ const tail = readNewLines(transcriptPath, st.offset);
419
+ const meta = extractClaudeSessionMeta(tail.records);
420
+ const month = st.month ?? monthOf(meta.started_at ?? firstTs(tail.records));
421
+ const dir = sessionDir(this.config.storeDir, this.config.host, month, sessionId);
422
+ const writtenPaths = [];
423
+ let nextIndex = st.nextTurnIndex;
424
+ let redactions = 0;
425
+ for (const rec of tail.records) {
426
+ const norm = normalizeClaudeEvent(rec);
427
+ if (!norm) continue;
428
+ let turn = buildTurn(norm, {
429
+ session_id: sessionId,
430
+ host: this.config.host,
431
+ agent: this.config.agent,
432
+ turn_index: nextIndex
433
+ });
434
+ const cost = estimateCostUsd(turn.usage, meta.model);
435
+ if (cost > 0) turn = { ...turn, telemetry: { cost_usd: cost } };
436
+ const hy = applyHygiene(turn, this.config.hygiene);
437
+ redactions += hy.redactions.reduce((a, m) => a + m.count, 0);
438
+ if (hy.blob) writtenPaths.push(writeBlobFile(dir, hy.blob.sha, hy.blob.content));
439
+ const res = writeTurnFile(dir, hy.turn);
440
+ if (res.written) {
441
+ writtenPaths.push(res.path);
442
+ nextIndex++;
443
+ }
444
+ }
445
+ this.state.update(sessionId, {
446
+ transcriptPath,
447
+ offset: tail.newOffset,
448
+ nextTurnIndex: nextIndex,
449
+ month,
450
+ startedAt: st.startedAt ?? meta.started_at,
451
+ lastTs: meta.ended_at ?? st.lastTs
452
+ });
453
+ const newTurns = nextIndex - st.nextTurnIndex;
454
+ const result = {
455
+ sessionId,
456
+ sessionDir: dir,
457
+ month,
458
+ newTurns,
459
+ writtenPaths,
460
+ redactions
461
+ };
462
+ if (newTurns > 0 || writtenPaths.length > 0) {
463
+ result.envelope = rebuildEnvelope(
464
+ this.config.storeDir,
465
+ this.config.host,
466
+ month,
467
+ sessionId,
468
+ meta,
469
+ {
470
+ session_id: sessionId,
471
+ host: this.config.host,
472
+ agent: this.config.agent,
473
+ native_uuid: sessionId
474
+ }
475
+ );
476
+ }
477
+ return result;
478
+ }
479
+ };
480
+ function firstTs(records) {
481
+ for (const r of records) {
482
+ if (r && typeof r === "object" && typeof r.timestamp === "string") {
483
+ return r.timestamp;
484
+ }
485
+ }
486
+ return void 0;
487
+ }
488
+
489
+ // src/ipc.ts
490
+ import { connect } from "net";
491
+ var LIFECYCLE_END = /* @__PURE__ */ new Set(["Stop", "SubagentStop", "SessionEnd"]);
492
+ function isSessionEndEvent(event) {
493
+ return LIFECYCLE_END.has(event);
494
+ }
495
+ function parseHookEvent(raw) {
496
+ if (!raw || typeof raw !== "object") return null;
497
+ const r = raw;
498
+ const event = r.event ?? r.hook_event_name ?? r.hookEventName;
499
+ const session_id = r.session_id ?? r.sessionId;
500
+ if (!event || !session_id) return null;
501
+ const out = { event, session_id };
502
+ const tp = r.transcript_path ?? r.transcriptPath;
503
+ if (typeof tp === "string") out.transcript_path = tp;
504
+ if (typeof r.cwd === "string") out.cwd = r.cwd;
505
+ return out;
506
+ }
507
+ function sendEvent(socketPath, event, timeoutMs = 4e3) {
508
+ return new Promise((resolve) => {
509
+ const sock = connect(socketPath);
510
+ let buf = "";
511
+ let settled = false;
512
+ const done = (ack) => {
513
+ if (settled) return;
514
+ settled = true;
515
+ sock.destroy();
516
+ resolve(ack);
517
+ };
518
+ sock.setTimeout(timeoutMs);
519
+ sock.on("connect", () => sock.write(`${JSON.stringify(event)}
520
+ `));
521
+ sock.on("data", (d) => {
522
+ buf += d.toString("utf8");
523
+ const nl = buf.indexOf("\n");
524
+ if (nl >= 0) {
525
+ try {
526
+ done(JSON.parse(buf.slice(0, nl)));
527
+ } catch {
528
+ done({ ok: false, error: "bad ack" });
529
+ }
530
+ }
531
+ });
532
+ sock.on("timeout", () => done({ ok: false, error: "timeout" }));
533
+ sock.on("error", (e) => done({ ok: false, error: e.message }));
534
+ });
535
+ }
536
+
537
+ // src/state.ts
538
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
539
+ import { dirname as dirname2 } from "path";
540
+ var StateStore = class {
541
+ constructor(path) {
542
+ this.path = path;
543
+ this.data = this.read();
544
+ }
545
+ path;
546
+ data;
547
+ read() {
548
+ if (existsSync4(this.path)) {
549
+ try {
550
+ const parsed = JSON.parse(readFileSync4(this.path, "utf8"));
551
+ if (parsed && parsed.version === 1 && parsed.sessions) return parsed;
552
+ } catch {
553
+ }
554
+ }
555
+ return { version: 1, sessions: {} };
556
+ }
557
+ /** Atomic write: tmp file + rename. */
558
+ flush() {
559
+ mkdirSync2(dirname2(this.path), { recursive: true });
560
+ const tmp = `${this.path}.tmp`;
561
+ writeFileSync2(tmp, JSON.stringify(this.data, null, 2));
562
+ renameSync2(tmp, this.path);
563
+ }
564
+ get(sessionId) {
565
+ return this.data.sessions[sessionId];
566
+ }
567
+ /** Get existing state or initialize a fresh one for this transcript. */
568
+ ensure(sessionId, transcriptPath) {
569
+ let s = this.data.sessions[sessionId];
570
+ if (!s) {
571
+ s = { transcriptPath, offset: 0, nextTurnIndex: 0 };
572
+ this.data.sessions[sessionId] = s;
573
+ this.flush();
574
+ } else if (transcriptPath && s.transcriptPath !== transcriptPath) {
575
+ s.transcriptPath = transcriptPath;
576
+ this.flush();
577
+ }
578
+ return s;
579
+ }
580
+ update(sessionId, patch) {
581
+ const s = this.data.sessions[sessionId] ?? {
582
+ transcriptPath: "",
583
+ offset: 0,
584
+ nextTurnIndex: 0
585
+ };
586
+ const next = { ...s, ...patch };
587
+ this.data.sessions[sessionId] = next;
588
+ this.flush();
589
+ return next;
590
+ }
591
+ all() {
592
+ return this.data.sessions;
593
+ }
594
+ };
595
+
596
+ // src/store/git.ts
597
+ import { spawnSync } from "child_process";
598
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
599
+ import { join as join3 } from "path";
600
+ var FALLBACK_IDENTITY = ["-c", "user.name=code-sessions", "-c", "user.email=agent@code-sessions"];
601
+ var GITIGNORE = `# code-sessions store \u2014 runtime + local-only artifacts
602
+ .daemon/
603
+ *.tmp
604
+ # large externalized blobs stay local in MVP-1 (LFS/object-store in MVP-2)
605
+ raw/
606
+ `;
607
+ var GITATTRIBUTES = `# append-only manifests merge by union so concurrent appends both survive
608
+ *.jsonl merge=union
609
+ telemetry/*.jsonl merge=union
610
+ `;
611
+ var GitStore = class {
612
+ constructor(dir, opts = {}) {
613
+ this.dir = dir;
614
+ this.opts = opts;
615
+ }
616
+ dir;
617
+ opts;
618
+ run(args, extraEnv) {
619
+ const res = spawnSync("git", ["-C", this.dir, ...args], {
620
+ encoding: "utf8",
621
+ env: { ...process.env, ...extraEnv }
622
+ });
623
+ return {
624
+ ok: res.status === 0,
625
+ stdout: (res.stdout ?? "").trim(),
626
+ stderr: (res.stderr ?? "").trim(),
627
+ code: res.status ?? -1
628
+ };
629
+ }
630
+ isRepo() {
631
+ return existsSync5(join3(this.dir, ".git"));
632
+ }
633
+ /** Initialize the store repo (idempotent): git init, scaffolding files, remote. */
634
+ init() {
635
+ if (!this.isRepo()) {
636
+ const r = spawnSync("git", ["init", "-b", "main", this.dir], { encoding: "utf8" });
637
+ if (r.status !== 0) throw new Error(`git init failed: ${r.stderr}`);
638
+ }
639
+ const giPath = join3(this.dir, ".gitignore");
640
+ if (!existsSync5(giPath)) writeFileSync3(giPath, GITIGNORE);
641
+ const gaPath = join3(this.dir, ".gitattributes");
642
+ if (!existsSync5(gaPath)) writeFileSync3(gaPath, GITATTRIBUTES);
643
+ if (this.opts.remote) this.ensureRemote(this.opts.remote);
644
+ }
645
+ ensureRemote(remote) {
646
+ const existing = this.run(["remote", "get-url", "origin"]);
647
+ if (existing.ok) {
648
+ if (existing.stdout !== remote) this.run(["remote", "set-url", "origin", remote]);
649
+ } else {
650
+ this.run(["remote", "add", "origin", remote]);
651
+ }
652
+ }
653
+ hasChanges() {
654
+ const r = this.run(["status", "--porcelain"]);
655
+ return r.stdout.length > 0;
656
+ }
657
+ /** Stage everything and commit. Returns committed:false when the tree is clean. */
658
+ commit(message) {
659
+ this.run(["add", "-A"]);
660
+ if (!this.hasChanges() && !this.hasStaged()) {
661
+ return { committed: false, reason: "nothing to commit" };
662
+ }
663
+ let r = this.run(["commit", "-m", message]);
664
+ if (!r.ok && /Please tell me who you are|user\.email|empty ident/i.test(r.stderr)) {
665
+ r = this.run([...FALLBACK_IDENTITY, "commit", "-m", message]);
666
+ }
667
+ if (!r.ok) {
668
+ if (/nothing to commit/i.test(r.stdout + r.stderr)) {
669
+ return { committed: false, reason: "nothing to commit" };
670
+ }
671
+ return { committed: false, reason: r.stderr || r.stdout };
672
+ }
673
+ const sha = this.run(["rev-parse", "HEAD"]).stdout;
674
+ return { committed: true, sha };
675
+ }
676
+ hasStaged() {
677
+ const r = this.run(["diff", "--cached", "--quiet"]);
678
+ return r.code === 1;
679
+ }
680
+ currentBranch() {
681
+ return this.run(["rev-parse", "--abbrev-ref", "HEAD"]).stdout || "main";
682
+ }
683
+ /** Rebase local commits on top of the remote (clean by construction for host-keyed paths). */
684
+ pull() {
685
+ return this.run(["pull", "--rebase", "--autostash", "origin", this.currentBranch()]);
686
+ }
687
+ push() {
688
+ return this.run(["push", "-u", "origin", this.currentBranch()]);
689
+ }
690
+ /** Convenience: commit, and when a remote+autoPush are configured, pull --rebase then push. */
691
+ sync(message) {
692
+ const commit = this.commit(message);
693
+ if (!commit.committed) return { commit, pushed: false };
694
+ if (!this.opts.remote || !this.opts.autoPush) return { commit, pushed: false };
695
+ this.pull();
696
+ const p = this.push();
697
+ return { commit, pushed: p.ok, ...p.ok ? {} : { pushError: p.stderr } };
698
+ }
699
+ };
700
+
701
+ // src/store/scan.ts
702
+ import { readdirSync as readdirSync2 } from "fs";
703
+ import { join as join4 } from "path";
704
+ function readEntries(dir) {
705
+ try {
706
+ return readdirSync2(dir, { withFileTypes: true });
707
+ } catch {
708
+ return [];
709
+ }
710
+ }
711
+ function subdirs(dir) {
712
+ return readEntries(dir).filter((e) => e.isDirectory()).map((e) => String(e.name));
713
+ }
714
+ function listSessionDirs(storeDir, opts = {}) {
715
+ const hostsRoot = join4(storeDir, "hosts");
716
+ const out = [];
717
+ for (const host of subdirs(hostsRoot)) {
718
+ for (const month of subdirs(join4(hostsRoot, host))) {
719
+ if (opts.sinceMonth && month < opts.sinceMonth) continue;
720
+ for (const sessionId of subdirs(join4(hostsRoot, host, month))) {
721
+ out.push({ host, month, sessionId, dir: join4(hostsRoot, host, month, sessionId) });
722
+ }
723
+ }
724
+ }
725
+ return out;
726
+ }
727
+
728
+ // src/daemon.ts
729
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync } from "fs";
730
+ import { createServer } from "net";
731
+ import { join as join5 } from "path";
732
+ function findTranscript(projectsDir, sessionId, maxDepth = 3) {
733
+ const target = `${sessionId}.jsonl`;
734
+ const walk = (dir, depth) => {
735
+ if (depth > maxDepth || !existsSync6(dir)) return void 0;
736
+ const entries = readEntries(dir);
737
+ for (const e of entries) {
738
+ if (e.isFile() && String(e.name) === target) return join5(dir, String(e.name));
739
+ }
740
+ for (const e of entries) {
741
+ if (e.isDirectory()) {
742
+ const found = walk(join5(dir, String(e.name)), depth + 1);
743
+ if (found) return found;
744
+ }
745
+ }
746
+ return void 0;
747
+ };
748
+ return walk(projectsDir, 0);
749
+ }
750
+ var Daemon = class {
751
+ constructor(cfg, deps = {}) {
752
+ this.cfg = cfg;
753
+ this.state = deps.state ?? new StateStore(cfg.statePath);
754
+ this.capture = deps.capture ?? new CaptureEngine(cfg, this.state);
755
+ if (cfg.git.autoCommit) {
756
+ this.git = deps.git ?? new GitStore(cfg.storeDir, {
757
+ ...cfg.git.remote ? { remote: cfg.git.remote } : {},
758
+ autoPush: cfg.git.autoPush
759
+ });
760
+ }
761
+ if (deps.onSessionEnd) this.onSessionEnd = deps.onSessionEnd;
762
+ }
763
+ cfg;
764
+ server;
765
+ capture;
766
+ state;
767
+ git;
768
+ onSessionEnd;
769
+ dirty = false;
770
+ pendingTurns = 0;
771
+ commitTimer;
772
+ running = false;
773
+ stats = { events: 0, turns: 0, commits: 0 };
774
+ sessions = /* @__PURE__ */ new Set();
775
+ async start() {
776
+ mkdirSync3(this.cfg.storeDir, { recursive: true });
777
+ mkdirSync3(this.cfg.runtimeDir, { recursive: true });
778
+ this.git?.init();
779
+ if (existsSync6(this.cfg.socketPath)) rmSync(this.cfg.socketPath);
780
+ await new Promise((resolve, reject) => {
781
+ const server = createServer((sock) => this.onConnection(sock));
782
+ server.on("error", reject);
783
+ server.listen(this.cfg.socketPath, () => {
784
+ this.running = true;
785
+ resolve();
786
+ });
787
+ this.server = server;
788
+ });
789
+ }
790
+ onConnection(sock) {
791
+ let buf = "";
792
+ sock.on("data", async (chunk) => {
793
+ buf += chunk.toString("utf8");
794
+ let nl;
795
+ while ((nl = buf.indexOf("\n")) >= 0) {
796
+ const line = buf.slice(0, nl);
797
+ buf = buf.slice(nl + 1);
798
+ if (!line.trim()) continue;
799
+ let ack;
800
+ try {
801
+ const evt = parseHookEvent(JSON.parse(line));
802
+ ack = evt ? await this.handleEvent(evt) : { ok: false, error: "invalid event" };
803
+ } catch {
804
+ ack = { ok: false, error: "parse error" };
805
+ }
806
+ if (!sock.destroyed) sock.write(`${JSON.stringify(ack)}
807
+ `);
808
+ }
809
+ });
810
+ sock.on("error", () => {
811
+ });
812
+ }
813
+ /** Process a single hook event: capture, then decide whether to flush a commit. */
814
+ async handleEvent(evt) {
815
+ this.stats.events++;
816
+ const transcript = evt.transcript_path && existsSync6(evt.transcript_path) ? evt.transcript_path : findTranscript(this.cfg.claudeProjectsDir, evt.session_id);
817
+ if (!transcript) return { ok: false, error: "transcript not found" };
818
+ const res = this.capture.captureSession(evt.session_id, transcript);
819
+ this.sessions.add(evt.session_id);
820
+ if (res.newTurns > 0 || res.writtenPaths.length > 0) {
821
+ this.dirty = true;
822
+ this.pendingTurns += res.newTurns;
823
+ this.stats.turns += res.newTurns;
824
+ }
825
+ const end = isSessionEndEvent(evt.event);
826
+ let flushed = false;
827
+ if (end && this.onSessionEnd) {
828
+ await this.onSessionEnd(evt.session_id, res.sessionDir);
829
+ this.dirty = true;
830
+ }
831
+ if (end || this.pendingTurns >= this.cfg.batch.maxTurns) {
832
+ flushed = this.flush(`capture ${evt.session_id}`);
833
+ } else {
834
+ this.scheduleFlush();
835
+ }
836
+ return { ok: true, newTurns: res.newTurns, flushed };
837
+ }
838
+ scheduleFlush() {
839
+ if (this.commitTimer) return;
840
+ this.commitTimer = setTimeout(() => {
841
+ this.commitTimer = void 0;
842
+ this.flush("batch interval");
843
+ }, this.cfg.batch.maxIntervalMs);
844
+ this.commitTimer.unref?.();
845
+ }
846
+ /** Commit (and push when configured) all buffered store changes. Returns whether a commit landed. */
847
+ flush(message) {
848
+ if (this.commitTimer) {
849
+ clearTimeout(this.commitTimer);
850
+ this.commitTimer = void 0;
851
+ }
852
+ if (!this.dirty || !this.git) {
853
+ this.dirty = false;
854
+ this.pendingTurns = 0;
855
+ return false;
856
+ }
857
+ const r = this.git.sync(message);
858
+ this.dirty = false;
859
+ this.pendingTurns = 0;
860
+ if (r.commit.committed) this.stats.commits++;
861
+ return r.commit.committed;
862
+ }
863
+ status() {
864
+ return {
865
+ running: this.running,
866
+ socketPath: this.cfg.socketPath,
867
+ storeDir: this.cfg.storeDir,
868
+ events: this.stats.events,
869
+ turns: this.stats.turns,
870
+ commits: this.stats.commits,
871
+ sessions: [...this.sessions]
872
+ };
873
+ }
874
+ async stop() {
875
+ if (this.commitTimer) {
876
+ clearTimeout(this.commitTimer);
877
+ this.commitTimer = void 0;
878
+ }
879
+ this.flush("daemon shutdown");
880
+ if (this.server) {
881
+ await new Promise((resolve) => this.server.close(() => resolve()));
882
+ this.server = void 0;
883
+ }
884
+ if (existsSync6(this.cfg.socketPath)) {
885
+ try {
886
+ rmSync(this.cfg.socketPath);
887
+ } catch {
888
+ }
889
+ }
890
+ this.running = false;
891
+ }
892
+ };
893
+
894
+ // src/insights/heuristics.ts
895
+ var THRESHOLDS = {
896
+ stuckRepeat: 3,
897
+ // N consecutive identical assistant/tool actions
898
+ highCostUsd: 0.5,
899
+ longSessionTurns: 80,
900
+ toolHeavyRatio: 1.5
901
+ };
902
+ var ERROR_RE = /(error|exception|traceback|failed|fatal|panic)/i;
903
+ function actionKey(t) {
904
+ if (t.tool_calls.length > 0) {
905
+ return `tool:${t.tool_calls.map((c) => `${c.name}(${JSON.stringify(c.input ?? null)})`).join(",")}`;
906
+ }
907
+ return `${t.role}:${t.text.slice(0, 120)}`;
908
+ }
909
+ function deriveSignals(turns) {
910
+ const signals = [];
911
+ let runKey = "";
912
+ let runLen = 0;
913
+ let runStart = 0;
914
+ let flaggedStuck = false;
915
+ for (let i = 0; i < turns.length; i++) {
916
+ const key = actionKey(turns[i]);
917
+ if (key === runKey) {
918
+ runLen++;
919
+ } else {
920
+ runKey = key;
921
+ runLen = 1;
922
+ runStart = i;
923
+ }
924
+ if (runLen >= THRESHOLDS.stuckRepeat && !flaggedStuck) {
925
+ signals.push({
926
+ kind: "stuck-loop",
927
+ severity: "warn",
928
+ turn_index: turns[runStart].turn_index,
929
+ note: `repeated action \xD7${runLen}`
930
+ });
931
+ flaggedStuck = true;
932
+ }
933
+ }
934
+ for (const t of turns) {
935
+ if (ERROR_RE.test(t.text)) {
936
+ signals.push({ kind: "error-recovery", severity: "info", turn_index: t.turn_index });
937
+ break;
938
+ }
939
+ }
940
+ for (const t of turns) {
941
+ const cost = t.telemetry?.cost_usd ?? 0;
942
+ if (cost >= THRESHOLDS.highCostUsd) {
943
+ signals.push({
944
+ kind: "high-cost-turn",
945
+ severity: "warn",
946
+ turn_index: t.turn_index,
947
+ note: `$${cost.toFixed(2)}`
948
+ });
949
+ break;
950
+ }
951
+ }
952
+ if (turns.length >= THRESHOLDS.longSessionTurns) {
953
+ signals.push({ kind: "long-session", severity: "info", note: `${turns.length} turns` });
954
+ }
955
+ const toolCalls = turns.reduce((a, t) => a + t.tool_calls.length, 0);
956
+ if (turns.length > 0 && toolCalls / turns.length >= THRESHOLDS.toolHeavyRatio) {
957
+ signals.push({
958
+ kind: "tool-heavy",
959
+ severity: "info",
960
+ note: `${toolCalls} tool calls / ${turns.length} turns`
961
+ });
962
+ }
963
+ return signals;
964
+ }
965
+ function guessTopic(turns) {
966
+ const firstUser = turns.find((t) => t.role === "user" && t.text.trim().length > 0);
967
+ if (!firstUser) return void 0;
968
+ const words = firstUser.text.trim().split(/\s+/).slice(0, 8).join(" ");
969
+ return words.length > 0 ? words : void 0;
970
+ }
971
+ function deriveTags(turns) {
972
+ const tags = /* @__PURE__ */ new Set();
973
+ for (const t of turns) for (const c of t.tool_calls) tags.add(c.name);
974
+ return [...tags].slice(0, 12);
975
+ }
976
+
977
+ // src/insights/provider.ts
978
+ var FakeProvider = class {
979
+ name = "fake";
980
+ async label(req) {
981
+ const result = {
982
+ tags: deriveTags(req.turns),
983
+ signals: []
984
+ };
985
+ const topic = guessTopic(req.turns);
986
+ if (topic) result.topic = topic;
987
+ const assistantText = req.turns.find((t) => t.role === "assistant")?.text;
988
+ if (assistantText) result.summary = assistantText.slice(0, 160);
989
+ return result;
990
+ }
991
+ };
992
+
993
+ // src/insights/llm.ts
994
+ import { spawnSync as spawnSync2 } from "child_process";
995
+ import { SIGNAL_KINDS } from "@unpolarize/code-sessions-schema";
996
+ var MAX_HEAD = 40;
997
+ var MAX_TAIL = 10;
998
+ var MAX_TURN_CHARS = 400;
999
+ function buildPrompt(req) {
1000
+ const turns = req.turns;
1001
+ const picked = turns.length > MAX_HEAD + MAX_TAIL ? [...turns.slice(0, MAX_HEAD), { role: "\u2026", text: `(${turns.length - MAX_HEAD - MAX_TAIL} turns elided)`, tool_calls: [] }, ...turns.slice(-MAX_TAIL)] : turns;
1002
+ const transcript = picked.map((t) => {
1003
+ const tools = t.tool_calls?.map((c) => c.name).join(",");
1004
+ const head = tools ? `[${t.role} tools:${tools}]` : `[${t.role}]`;
1005
+ return `${head} ${String(t.text).slice(0, MAX_TURN_CHARS)}`;
1006
+ }).join("\n");
1007
+ return [
1008
+ "You label a coding-agent session. Respond with ONLY a JSON object, no prose:",
1009
+ '{"topic": string, "tags": string[], "summary": string, "signals": [{"kind": one of ' + SIGNAL_KINDS.join("|") + ', "severity": "info"|"warn"|"critical", "note": string}]}',
1010
+ "topic: 3-6 words. tags: tools/themes. summary: <=1 sentence. signals: only notable ones.",
1011
+ "",
1012
+ "Transcript:",
1013
+ transcript
1014
+ ].join("\n");
1015
+ }
1016
+ var KIND_SET = new Set(SIGNAL_KINDS);
1017
+ function parseLabelJson(out) {
1018
+ const start = out.indexOf("{");
1019
+ const end = out.lastIndexOf("}");
1020
+ if (start < 0 || end <= start) return { tags: [], signals: [] };
1021
+ let obj;
1022
+ try {
1023
+ obj = JSON.parse(out.slice(start, end + 1));
1024
+ } catch {
1025
+ return { tags: [], signals: [] };
1026
+ }
1027
+ const result = {
1028
+ tags: Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === "string") : [],
1029
+ signals: coerceSignals(obj.signals)
1030
+ };
1031
+ if (typeof obj.topic === "string") result.topic = obj.topic;
1032
+ if (typeof obj.summary === "string") result.summary = obj.summary;
1033
+ return result;
1034
+ }
1035
+ function coerceSignals(raw) {
1036
+ if (!Array.isArray(raw)) return [];
1037
+ const out = [];
1038
+ for (const s of raw) {
1039
+ if (!s || typeof s !== "object") continue;
1040
+ const o = s;
1041
+ if (typeof o.kind !== "string" || !KIND_SET.has(o.kind)) continue;
1042
+ const sig = {
1043
+ kind: o.kind,
1044
+ severity: o.severity === "warn" || o.severity === "critical" ? o.severity : "info"
1045
+ };
1046
+ if (typeof o.note === "string") sig.note = o.note;
1047
+ if (typeof o.turn_index === "number") sig.turn_index = o.turn_index;
1048
+ out.push(sig);
1049
+ }
1050
+ return out;
1051
+ }
1052
+ var LlmProvider = class {
1053
+ constructor(name, runner) {
1054
+ this.name = name;
1055
+ this.runner = runner;
1056
+ }
1057
+ name;
1058
+ runner;
1059
+ async label(req) {
1060
+ const out = await this.runner(buildPrompt(req));
1061
+ return parseLabelJson(out);
1062
+ }
1063
+ };
1064
+ function spawnText(cmd, args, input) {
1065
+ const res = spawnSync2(cmd, args, {
1066
+ encoding: "utf8",
1067
+ input,
1068
+ maxBuffer: 16 * 1024 * 1024
1069
+ });
1070
+ if (res.error) throw new Error(`${cmd} failed: ${res.error.message}`);
1071
+ if (res.status !== 0) throw new Error(`${cmd} exited ${res.status}: ${(res.stderr ?? "").slice(0, 200)}`);
1072
+ return res.stdout ?? "";
1073
+ }
1074
+ var claudeRunner = (model) => async (prompt) => spawnText("claude", model ? ["-p", "--model", model, prompt] : ["-p", prompt]);
1075
+ var grokRunner = (model) => async (prompt) => spawnText("grok", model ? ["--model", model, "-p", prompt] : ["-p", prompt]);
1076
+ var ollamaRunner = (model = "llama3.1", host = "http://localhost:11434") => async (prompt) => {
1077
+ const res = await fetch(`${host}/api/generate`, {
1078
+ method: "POST",
1079
+ headers: { "content-type": "application/json" },
1080
+ body: JSON.stringify({ model, prompt, stream: false })
1081
+ });
1082
+ if (!res.ok) throw new Error(`ollama ${res.status}`);
1083
+ const data = await res.json();
1084
+ return data.response ?? "";
1085
+ };
1086
+
1087
+ // src/insights/labeler.ts
1088
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
1089
+ import { dirname as dirname3 } from "path";
1090
+ import {
1091
+ SCHEMA_VERSIONS as SCHEMA_VERSIONS2,
1092
+ parseSession
1093
+ } from "@unpolarize/code-sessions-schema";
1094
+ function writeJsonAtomic2(path, value) {
1095
+ mkdirSync4(dirname3(path), { recursive: true });
1096
+ const tmp = `${path}.tmp`;
1097
+ writeFileSync4(tmp, `${JSON.stringify(value, null, 2)}
1098
+ `);
1099
+ renameSync3(tmp, path);
1100
+ }
1101
+ function dedupeSignals(signals) {
1102
+ const seen = /* @__PURE__ */ new Set();
1103
+ const out = [];
1104
+ for (const s of signals) {
1105
+ const key = `${s.kind}:${s.turn_index ?? ""}`;
1106
+ if (seen.has(key)) continue;
1107
+ seen.add(key);
1108
+ out.push(s);
1109
+ }
1110
+ return out;
1111
+ }
1112
+ function makeProvider(cfg) {
1113
+ const { provider, model } = cfg.insights;
1114
+ switch (provider) {
1115
+ case "none":
1116
+ return null;
1117
+ case "fake":
1118
+ return new FakeProvider();
1119
+ case "claude":
1120
+ return new LlmProvider("claude", claudeRunner(model));
1121
+ case "grok":
1122
+ return new LlmProvider("grok", grokRunner(model));
1123
+ case "ollama":
1124
+ return new LlmProvider("ollama", ollamaRunner(model));
1125
+ default:
1126
+ return null;
1127
+ }
1128
+ }
1129
+ async function labelSession(sessionDir2, identity, provider, opts = {}) {
1130
+ const turns = readTurns(sessionDir2);
1131
+ if (turns.length === 0) return void 0;
1132
+ const heuristicSignals = deriveSignals(turns);
1133
+ let provided = { tags: [], signals: [] };
1134
+ try {
1135
+ provided = await provider.label({ sessionId: identity.sessionId, host: identity.host, turns });
1136
+ } catch {
1137
+ }
1138
+ const topic = provided.topic ?? guessTopic(turns);
1139
+ const tags = [.../* @__PURE__ */ new Set([...provided.tags, ...deriveTags(turns)])].slice(0, 16);
1140
+ const signals = dedupeSignals([...heuristicSignals, ...provided.signals]);
1141
+ const insights = {
1142
+ schema: SCHEMA_VERSIONS2.insights,
1143
+ session_id: identity.sessionId,
1144
+ host: identity.host,
1145
+ generated_at: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(),
1146
+ provider: provider.name,
1147
+ tags,
1148
+ signals
1149
+ };
1150
+ if (topic) insights.topic = topic;
1151
+ if (provided.summary) insights.summary = provided.summary;
1152
+ writeJsonAtomic2(insightsFile(sessionDir2), insights);
1153
+ updateEnvelopeLabels(sessionDir2, topic, tags);
1154
+ return insights;
1155
+ }
1156
+ function updateEnvelopeLabels(sessionDir2, topic, tags) {
1157
+ const path = envelopeFile(sessionDir2);
1158
+ if (!existsSync7(path)) return;
1159
+ let env;
1160
+ try {
1161
+ env = parseSession(JSON.parse(readFileSync5(path, "utf8")));
1162
+ } catch {
1163
+ return;
1164
+ }
1165
+ env.labels = [.../* @__PURE__ */ new Set([...topic ? [topic] : [], ...tags])].slice(0, 16);
1166
+ writeJsonAtomic2(path, env);
1167
+ }
1168
+ async function reindexStore(cfg, provider, opts = {}) {
1169
+ const refs = listSessionDirs(cfg.storeDir, opts.sinceMonth ? { sinceMonth: opts.sinceMonth } : {});
1170
+ const labeled = [];
1171
+ for (const ref of refs) {
1172
+ const labelOpts = opts.now ? { now: opts.now } : {};
1173
+ const res = await labelSession(ref.dir, { sessionId: ref.sessionId, host: ref.host }, provider, labelOpts);
1174
+ if (res) labeled.push(ref.sessionId);
1175
+ }
1176
+ return { count: labeled.length, sessions: labeled };
1177
+ }
1178
+
1179
+ // src/hooks/install.ts
1180
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1181
+ import { dirname as dirname4 } from "path";
1182
+ var DEFAULT_HOOK_EVENTS = [
1183
+ "SessionStart",
1184
+ "UserPromptSubmit",
1185
+ "PostToolUse",
1186
+ "Stop",
1187
+ "SubagentStop"
1188
+ ];
1189
+ function groupHasCommand(groups, command) {
1190
+ return groups.some((g) => g.hooks?.some((h) => h.command === command));
1191
+ }
1192
+ function mergeHooks(settings, command, events = DEFAULT_HOOK_EVENTS) {
1193
+ const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
1194
+ const hooks = next.hooks;
1195
+ const added = [];
1196
+ for (const event of events) {
1197
+ const groups = hooks[event] ? [...hooks[event]] : [];
1198
+ if (groupHasCommand(groups, command)) {
1199
+ hooks[event] = groups;
1200
+ continue;
1201
+ }
1202
+ groups.push({ matcher: "", hooks: [{ type: "command", command }] });
1203
+ hooks[event] = groups;
1204
+ added.push(event);
1205
+ }
1206
+ return { settings: next, added };
1207
+ }
1208
+ function installHooks(settingsPath, command, events = DEFAULT_HOOK_EVENTS) {
1209
+ let settings = {};
1210
+ if (existsSync8(settingsPath)) {
1211
+ try {
1212
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1213
+ } catch {
1214
+ settings = {};
1215
+ }
1216
+ }
1217
+ const { settings: merged, added } = mergeHooks(settings, command, events);
1218
+ mkdirSync5(dirname4(settingsPath), { recursive: true });
1219
+ writeFileSync5(settingsPath, `${JSON.stringify(merged, null, 2)}
1220
+ `);
1221
+ return { settingsPath, command, added };
1222
+ }
1223
+
1224
+ // src/telemetry/otlp.ts
1225
+ import { createHash as createHash2 } from "crypto";
1226
+ function attr(key, value) {
1227
+ if (typeof value === "boolean") return { key, value: { boolValue: value } };
1228
+ if (typeof value === "string") return { key, value: { stringValue: value } };
1229
+ return Number.isInteger(value) ? { key, value: { intValue: value } } : { key, value: { doubleValue: value } };
1230
+ }
1231
+ function hexId(input, bytes) {
1232
+ return createHash2("sha256").update(input).digest("hex").slice(0, bytes * 2);
1233
+ }
1234
+ function isoNano(ts) {
1235
+ if (!ts) return "0";
1236
+ const ms = Date.parse(ts);
1237
+ return Number.isNaN(ms) ? "0" : `${ms}000000`;
1238
+ }
1239
+ var SCOPE = { name: "code-sessions", version: "0.1.0" };
1240
+ function resource(serviceName, host) {
1241
+ return {
1242
+ attributes: [
1243
+ attr("service.name", serviceName),
1244
+ attr("host.name", host),
1245
+ attr("telemetry.sdk.name", "code-sessions"),
1246
+ attr("telemetry.sdk.language", "nodejs")
1247
+ ]
1248
+ };
1249
+ }
1250
+ function sumUsage(turns) {
1251
+ const u = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
1252
+ for (const t of turns) {
1253
+ u.input += t.usage.input_tokens;
1254
+ u.output += t.usage.output_tokens;
1255
+ u.cacheRead += t.usage.cache_read_tokens;
1256
+ u.cacheWrite += t.usage.cache_write_tokens;
1257
+ u.cost += t.telemetry?.cost_usd ?? 0;
1258
+ }
1259
+ return u;
1260
+ }
1261
+ function buildTracePayload(session, turns, serviceName) {
1262
+ const traceId = hexId(session.session_id, 16);
1263
+ const rootId = hexId(`${session.session_id}:root`, 8);
1264
+ const totals = sumUsage(turns);
1265
+ const rootSpan = {
1266
+ traceId,
1267
+ spanId: rootId,
1268
+ name: `session ${session.title ?? session.session_id}`,
1269
+ kind: 1,
1270
+ startTimeUnixNano: isoNano(session.started_at),
1271
+ endTimeUnixNano: isoNano(session.ended_at ?? session.started_at),
1272
+ attributes: [
1273
+ attr("session.id", session.session_id),
1274
+ attr("gen_ai.system", session.agent),
1275
+ ...session.model ? [attr("gen_ai.request.model", session.model)] : [],
1276
+ attr("session.turn_count", session.turn_count),
1277
+ attr("gen_ai.usage.input_tokens", totals.input),
1278
+ attr("gen_ai.usage.output_tokens", totals.output),
1279
+ attr("code_sessions.cost_usd", Math.round(totals.cost * 1e6) / 1e6),
1280
+ ...session.project_path ? [attr("project.path", session.project_path)] : []
1281
+ ],
1282
+ status: {}
1283
+ };
1284
+ const turnSpans = turns.map((t) => ({
1285
+ traceId,
1286
+ spanId: hexId(`${session.session_id}:${t.turn_index}`, 8),
1287
+ parentSpanId: rootId,
1288
+ name: `turn ${t.turn_index} ${t.role}`,
1289
+ kind: 1,
1290
+ startTimeUnixNano: isoNano(t.ts),
1291
+ endTimeUnixNano: isoNano(t.ts),
1292
+ attributes: [
1293
+ attr("turn.index", t.turn_index),
1294
+ attr("gen_ai.role", t.role),
1295
+ attr("gen_ai.usage.input_tokens", t.usage.input_tokens),
1296
+ attr("gen_ai.usage.output_tokens", t.usage.output_tokens),
1297
+ attr("code_sessions.tool_count", t.tool_calls.length),
1298
+ attr("code_sessions.cost_usd", t.telemetry?.cost_usd ?? 0)
1299
+ ],
1300
+ status: {}
1301
+ }));
1302
+ return {
1303
+ resourceSpans: [
1304
+ {
1305
+ resource: resource(serviceName, session.host),
1306
+ scopeSpans: [{ scope: SCOPE, spans: [rootSpan, ...turnSpans] }]
1307
+ }
1308
+ ]
1309
+ };
1310
+ }
1311
+ function buildMetricPayload(session, turns, serviceName) {
1312
+ const totals = sumUsage(turns);
1313
+ const time = isoNano(session.ended_at ?? session.started_at);
1314
+ const base = [attr("session.id", session.session_id), attr("gen_ai.system", session.agent)];
1315
+ const tokenPoint = (type, value) => ({
1316
+ asInt: value,
1317
+ timeUnixNano: time,
1318
+ attributes: [...base, attr("gen_ai.token.type", type)]
1319
+ });
1320
+ return {
1321
+ resourceMetrics: [
1322
+ {
1323
+ resource: resource(serviceName, session.host),
1324
+ scopeMetrics: [
1325
+ {
1326
+ scope: SCOPE,
1327
+ metrics: [
1328
+ {
1329
+ name: "code_sessions.tokens",
1330
+ unit: "{token}",
1331
+ sum: {
1332
+ aggregationTemporality: 2,
1333
+ isMonotonic: true,
1334
+ dataPoints: [
1335
+ tokenPoint("input", totals.input),
1336
+ tokenPoint("output", totals.output),
1337
+ tokenPoint("cache_read", totals.cacheRead),
1338
+ tokenPoint("cache_write", totals.cacheWrite)
1339
+ ]
1340
+ }
1341
+ },
1342
+ {
1343
+ name: "code_sessions.cost_usd",
1344
+ unit: "USD",
1345
+ gauge: {
1346
+ dataPoints: [
1347
+ { asDouble: Math.round(totals.cost * 1e6) / 1e6, timeUnixNano: time, attributes: base }
1348
+ ]
1349
+ }
1350
+ },
1351
+ {
1352
+ name: "code_sessions.turns",
1353
+ unit: "{turn}",
1354
+ sum: {
1355
+ aggregationTemporality: 2,
1356
+ isMonotonic: true,
1357
+ dataPoints: [{ asInt: turns.length, timeUnixNano: time, attributes: base }]
1358
+ }
1359
+ }
1360
+ ]
1361
+ }
1362
+ ]
1363
+ }
1364
+ ]
1365
+ };
1366
+ }
1367
+ async function postOtlp(endpoint, signalPath, payload, timeoutMs) {
1368
+ const url = `${endpoint.replace(/\/$/, "")}${signalPath}`;
1369
+ const controller = new AbortController();
1370
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1371
+ try {
1372
+ const res = await fetch(url, {
1373
+ method: "POST",
1374
+ headers: { "content-type": "application/json" },
1375
+ body: JSON.stringify(payload),
1376
+ signal: controller.signal
1377
+ });
1378
+ return { ok: res.ok, status: res.status };
1379
+ } catch (e) {
1380
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1381
+ } finally {
1382
+ clearTimeout(timer);
1383
+ }
1384
+ }
1385
+
1386
+ // src/telemetry/exporter.ts
1387
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1388
+ import { safeParseSession } from "@unpolarize/code-sessions-schema";
1389
+ function loadEnvelope(sessionDir2) {
1390
+ const path = envelopeFile(sessionDir2);
1391
+ if (!existsSync9(path)) return void 0;
1392
+ try {
1393
+ const parsed = safeParseSession(JSON.parse(readFileSync7(path, "utf8")));
1394
+ return parsed.success ? parsed.data : void 0;
1395
+ } catch {
1396
+ return void 0;
1397
+ }
1398
+ }
1399
+ async function exportSession(cfg, sessionDir2) {
1400
+ if (!cfg.telemetry.enabled) return { ok: false, skipped: true, reason: "telemetry disabled" };
1401
+ const envelope = loadEnvelope(sessionDir2);
1402
+ if (!envelope) return { ok: false, reason: "no envelope" };
1403
+ const turns = readTurns(sessionDir2);
1404
+ const { endpoint, serviceName, timeoutMs } = cfg.telemetry;
1405
+ const traces = await postOtlp(endpoint, "/v1/traces", buildTracePayload(envelope, turns, serviceName), timeoutMs);
1406
+ const metrics = await postOtlp(endpoint, "/v1/metrics", buildMetricPayload(envelope, turns, serviceName), timeoutMs);
1407
+ return { ok: traces.ok && metrics.ok, traces, metrics };
1408
+ }
1409
+ async function exportStore(cfg, opts = {}) {
1410
+ const refs = listSessionDirs(cfg.storeDir, opts.sinceMonth ? { sinceMonth: opts.sinceMonth } : {});
1411
+ let exported = 0;
1412
+ let failed = 0;
1413
+ for (const ref of refs) {
1414
+ const res = await exportSession(cfg, ref.dir);
1415
+ if (res.ok) exported++;
1416
+ else if (!res.skipped) failed++;
1417
+ }
1418
+ return { total: refs.length, exported, failed };
1419
+ }
1420
+
1421
+ // src/adapters/grok.ts
1422
+ import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync } from "fs";
1423
+ import { homedir as homedir2 } from "os";
1424
+ import { join as join6 } from "path";
1425
+ import {
1426
+ SCHEMA_VERSIONS as SCHEMA_VERSIONS3
1427
+ } from "@unpolarize/code-sessions-schema";
1428
+ function grokSessionsRoot() {
1429
+ return join6(homedir2(), ".grok", "sessions");
1430
+ }
1431
+ function safeDirs(dir) {
1432
+ try {
1433
+ return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1434
+ } catch {
1435
+ return [];
1436
+ }
1437
+ }
1438
+ function discoverGrokSessions(root = grokSessionsRoot()) {
1439
+ if (!existsSync10(root)) return [];
1440
+ const out = [];
1441
+ for (const enc of safeDirs(root)) {
1442
+ let cwd = enc;
1443
+ try {
1444
+ cwd = decodeURIComponent(enc);
1445
+ } catch {
1446
+ }
1447
+ for (const uuid of safeDirs(join6(root, enc))) {
1448
+ const dir = join6(root, enc, uuid);
1449
+ const chatPath = join6(dir, "chat_history.jsonl");
1450
+ const summaryPath = join6(dir, "summary.json");
1451
+ if (existsSync10(chatPath)) out.push({ sessionId: uuid, chatPath, summaryPath, cwd });
1452
+ }
1453
+ }
1454
+ return out;
1455
+ }
1456
+ function extractText(content) {
1457
+ if (typeof content === "string") return content;
1458
+ if (Array.isArray(content)) {
1459
+ return content.map((b) => typeof b === "string" ? b : b && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "").filter(Boolean).join("\n\n");
1460
+ }
1461
+ return "";
1462
+ }
1463
+ function parseArgs(s) {
1464
+ if (typeof s !== "string") return s;
1465
+ try {
1466
+ return JSON.parse(s);
1467
+ } catch {
1468
+ return s;
1469
+ }
1470
+ }
1471
+ function parseGrokSession(info, host) {
1472
+ let summary = {};
1473
+ if (existsSync10(info.summaryPath)) {
1474
+ try {
1475
+ summary = JSON.parse(readFileSync8(info.summaryPath, "utf8"));
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ if (summary.session_kind === "claude_import") return null;
1480
+ const baseMs = Date.parse(summary.created_at ?? "") || statMtime(info.chatPath);
1481
+ const lines = readFileSync8(info.chatPath, "utf8").split("\n").filter((l) => l.trim().length > 0);
1482
+ const turns = [];
1483
+ let idx = 0;
1484
+ let model = summary.current_model_id;
1485
+ for (const line of lines) {
1486
+ let ev;
1487
+ try {
1488
+ ev = JSON.parse(line);
1489
+ } catch {
1490
+ continue;
1491
+ }
1492
+ const ts = new Date(baseMs + idx * 1e3).toISOString();
1493
+ if (ev.type === "user") {
1494
+ turns.push(mkTurn(info.sessionId, host, idx++, ts, "user", extractText(ev.content), []));
1495
+ } else if (ev.type === "assistant") {
1496
+ if (typeof ev.model_id === "string") model = ev.model_id;
1497
+ const tools = Array.isArray(ev.tool_calls) ? ev.tool_calls.filter((t) => t && typeof t.name === "string").map((t) => ({ name: t.name, input: parseArgs(t.arguments), ...t.id ? { id: t.id } : {} })) : [];
1498
+ turns.push(mkTurn(info.sessionId, host, idx++, ts, "assistant", extractText(ev.content), tools));
1499
+ } else if (ev.type === "tool_result") {
1500
+ turns.push(mkTurn(info.sessionId, host, idx++, ts, "tool", extractText(ev.content), []));
1501
+ }
1502
+ }
1503
+ if (turns.length === 0) return null;
1504
+ const startedAt = new Date(baseMs).toISOString();
1505
+ const endedAt = turns[turns.length - 1].ts;
1506
+ const meta = {
1507
+ session_id: info.sessionId,
1508
+ project_path: summary.info?.cwd ?? info.cwd,
1509
+ started_at: startedAt,
1510
+ ended_at: endedAt
1511
+ };
1512
+ if (model) meta.model = model;
1513
+ const title = summary.generated_title?.trim() || summary.session_summary?.trim();
1514
+ if (title) meta.title = title;
1515
+ return { host, sessionId: info.sessionId, agent: "grok", turns, meta };
1516
+ }
1517
+ function statMtime(p) {
1518
+ try {
1519
+ return statSync(p).mtimeMs;
1520
+ } catch {
1521
+ return Date.parse("2020-01-01T00:00:00Z");
1522
+ }
1523
+ }
1524
+ function mkTurn(sessionId, host, index, ts, role, text, tool_calls) {
1525
+ return {
1526
+ schema: SCHEMA_VERSIONS3.turn,
1527
+ session_id: sessionId,
1528
+ host,
1529
+ agent: "grok",
1530
+ turn_index: index,
1531
+ ts,
1532
+ role,
1533
+ text,
1534
+ tool_calls,
1535
+ usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
1536
+ scrubbed: false,
1537
+ raw_ref: null
1538
+ };
1539
+ }
1540
+
1541
+ // src/adapters/codex.ts
1542
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
1543
+ import { homedir as homedir3 } from "os";
1544
+ import { join as join7 } from "path";
1545
+ import {
1546
+ SCHEMA_VERSIONS as SCHEMA_VERSIONS4
1547
+ } from "@unpolarize/code-sessions-schema";
1548
+ var UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
1549
+ function codexSessionsRoot() {
1550
+ return join7(homedir3(), ".codex", "sessions");
1551
+ }
1552
+ function discoverCodexSessions(root = codexSessionsRoot()) {
1553
+ if (!existsSync11(root)) return [];
1554
+ const out = [];
1555
+ const walk = (dir, depth) => {
1556
+ if (depth > 6) return;
1557
+ for (const e of readEntries(dir)) {
1558
+ const name = String(e.name);
1559
+ const full = join7(dir, name);
1560
+ if (e.isDirectory()) walk(full, depth + 1);
1561
+ else if (e.isFile() && name.endsWith(".jsonl")) {
1562
+ const m = UUID_RE.exec(name);
1563
+ out.push({ sessionId: m ? m[1] : name.replace(/\.jsonl$/, ""), path: full });
1564
+ }
1565
+ }
1566
+ };
1567
+ walk(root, 0);
1568
+ return out;
1569
+ }
1570
+ function textFromContent(content) {
1571
+ if (typeof content === "string") return content;
1572
+ if (Array.isArray(content)) {
1573
+ return content.map((b) => {
1574
+ if (typeof b === "string") return b;
1575
+ if (b && typeof b === "object") {
1576
+ const o = b;
1577
+ if (typeof o.text === "string") return o.text;
1578
+ }
1579
+ return "";
1580
+ }).filter(Boolean).join("\n");
1581
+ }
1582
+ if (content && typeof content === "object" && typeof content.text === "string") {
1583
+ return content.text;
1584
+ }
1585
+ return "";
1586
+ }
1587
+ function normalizeCodexLine(ev) {
1588
+ const p = ev?.payload && typeof ev.payload === "object" ? ev.payload : ev;
1589
+ const ptype = p?.type;
1590
+ if (ev?.type === "event_msg") {
1591
+ if (ptype === "user_message" && typeof p.message === "string") {
1592
+ return { role: "user", text: p.message, tool_calls: [] };
1593
+ }
1594
+ if (ptype === "agent_message" && typeof p.message === "string") {
1595
+ return { role: "assistant", text: p.message, tool_calls: [] };
1596
+ }
1597
+ return null;
1598
+ }
1599
+ if (ev?.type === "response_item") {
1600
+ if (ptype === "function_call" || ptype === "local_shell_call" || ptype === "tool_call") {
1601
+ const name = typeof p.name === "string" ? p.name : ptype === "local_shell_call" ? "shell" : "tool";
1602
+ let input = p.arguments ?? p.input ?? p.action;
1603
+ if (typeof input === "string") {
1604
+ try {
1605
+ input = JSON.parse(input);
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ return { role: "assistant", text: "", tool_calls: [{ name, input }] };
1610
+ }
1611
+ if (ptype === "function_call_output" || ptype === "tool_result") {
1612
+ return { role: "tool", text: textFromContent(p.output ?? p.content), tool_calls: [] };
1613
+ }
1614
+ return null;
1615
+ }
1616
+ return null;
1617
+ }
1618
+ function readTokenCount(ev) {
1619
+ if (ev?.type !== "event_msg" || ev?.payload?.type !== "token_count") return null;
1620
+ const u = ev.payload.info?.total_token_usage ?? ev.payload.info;
1621
+ if (!u || typeof u !== "object") return null;
1622
+ return {
1623
+ input_tokens: Number(u.input_tokens) || 0,
1624
+ output_tokens: Number(u.output_tokens) || 0,
1625
+ cache_read_tokens: Number(u.cached_input_tokens ?? u.cache_read_tokens) || 0
1626
+ };
1627
+ }
1628
+ function lineTs(ev, fallback) {
1629
+ const t = ev?.timestamp ?? ev?.ts;
1630
+ if (typeof t === "string" && !Number.isNaN(Date.parse(t))) return t;
1631
+ return fallback;
1632
+ }
1633
+ function parseCodexSession(info, host) {
1634
+ const lines = readFileSync9(info.path, "utf8").split("\n").filter((l) => l.trim().length > 0);
1635
+ if (lines.length === 0) return null;
1636
+ let model;
1637
+ let cwd;
1638
+ let sessionId = info.sessionId;
1639
+ let baseTs = "2020-01-01T00:00:00Z";
1640
+ let latestUsage = null;
1641
+ let lastAssistantIdx = -1;
1642
+ const turns = [];
1643
+ let idx = 0;
1644
+ for (const line of lines) {
1645
+ let ev;
1646
+ try {
1647
+ ev = JSON.parse(line);
1648
+ } catch {
1649
+ continue;
1650
+ }
1651
+ if (ev?.type === "session_meta") {
1652
+ const src = ev.payload && typeof ev.payload === "object" ? ev.payload : ev;
1653
+ if (typeof src.model === "string") model = src.model;
1654
+ if (typeof src.cwd === "string") cwd = src.cwd;
1655
+ if (typeof src.id === "string") sessionId = UUID_RE.exec(src.id)?.[1] ?? sessionId;
1656
+ const t = src.timestamp ?? ev.timestamp;
1657
+ if (typeof t === "string" && !Number.isNaN(Date.parse(t))) baseTs = t;
1658
+ continue;
1659
+ }
1660
+ if (ev?.type === "turn_context" && typeof ev.payload?.cwd === "string" && !cwd) {
1661
+ cwd = ev.payload.cwd;
1662
+ }
1663
+ const usage = readTokenCount(ev);
1664
+ if (usage) {
1665
+ latestUsage = usage;
1666
+ continue;
1667
+ }
1668
+ const norm = normalizeCodexLine(ev);
1669
+ if (!norm) continue;
1670
+ const ts = lineTs(ev, new Date(Date.parse(baseTs) + idx * 1e3).toISOString());
1671
+ if (norm.role === "assistant") lastAssistantIdx = turns.length;
1672
+ turns.push({
1673
+ schema: SCHEMA_VERSIONS4.turn,
1674
+ session_id: sessionId,
1675
+ host,
1676
+ agent: "codex",
1677
+ turn_index: idx++,
1678
+ ts,
1679
+ role: norm.role,
1680
+ text: norm.text,
1681
+ tool_calls: norm.tool_calls,
1682
+ usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
1683
+ scrubbed: false,
1684
+ raw_ref: null
1685
+ });
1686
+ }
1687
+ if (turns.length === 0) return null;
1688
+ if (latestUsage && lastAssistantIdx >= 0) {
1689
+ turns[lastAssistantIdx].usage = {
1690
+ input_tokens: latestUsage.input_tokens,
1691
+ output_tokens: latestUsage.output_tokens,
1692
+ cache_read_tokens: latestUsage.cache_read_tokens,
1693
+ cache_write_tokens: 0
1694
+ };
1695
+ }
1696
+ const meta = {
1697
+ session_id: sessionId,
1698
+ started_at: turns[0].ts,
1699
+ ended_at: turns[turns.length - 1].ts
1700
+ };
1701
+ if (model) meta.model = model;
1702
+ if (cwd) meta.project_path = cwd;
1703
+ return { host, sessionId, agent: "codex", turns, meta };
1704
+ }
1705
+
1706
+ // src/adapters/import.ts
1707
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
1708
+ import { dirname as dirname5 } from "path";
1709
+ var NATIVE_FORMAT = {
1710
+ "claude-code": "claude-jsonl",
1711
+ grok: "grok-jsonl",
1712
+ codex: "codex-rollout",
1713
+ unknown: "unknown"
1714
+ };
1715
+ function writeJsonAtomic3(path, value) {
1716
+ mkdirSync6(dirname5(path), { recursive: true });
1717
+ const tmp = `${path}.tmp`;
1718
+ writeFileSync6(tmp, `${JSON.stringify(value, null, 2)}
1719
+ `);
1720
+ renameSync4(tmp, path);
1721
+ }
1722
+ function writeImportedSession(cfg, s) {
1723
+ const month = monthOf(s.meta.started_at ?? s.turns[0]?.ts);
1724
+ const dir = sessionDir(cfg.storeDir, s.host, month, s.sessionId);
1725
+ for (const turn of s.turns) {
1726
+ const hy = applyHygiene(turn, cfg.hygiene);
1727
+ if (hy.blob) writeBlobFile(dir, hy.blob.sha, hy.blob.content);
1728
+ writeTurnFile(dir, hy.turn);
1729
+ }
1730
+ const env = computeEnvelope(s.turns, s.meta, {
1731
+ session_id: s.sessionId,
1732
+ host: s.host,
1733
+ agent: s.agent,
1734
+ native_uuid: s.sessionId
1735
+ });
1736
+ env.native_ref.format = NATIVE_FORMAT[s.agent] ?? "unknown";
1737
+ const envPath = envelopeFile(dir);
1738
+ if (existsSync12(envPath)) {
1739
+ try {
1740
+ const prev = JSON.parse(readFileSync10(envPath, "utf8"));
1741
+ if (prev.labels?.length) env.labels = prev.labels;
1742
+ } catch {
1743
+ }
1744
+ }
1745
+ writeJsonAtomic3(envPath, env);
1746
+ return { sessionId: s.sessionId, sessionDir: dir, turns: s.turns.length, envelope: env };
1747
+ }
1748
+
1749
+ // src/index_store/db.ts
1750
+ import { mkdirSync as mkdirSync7 } from "fs";
1751
+ import { createRequire } from "module";
1752
+ import { dirname as dirname6 } from "path";
1753
+ var nodeRequire = createRequire(import.meta.url);
1754
+ var { DatabaseSync } = nodeRequire("node:sqlite");
1755
+ var SCHEMA_VERSION = 1;
1756
+ function toMs(iso) {
1757
+ if (!iso) return null;
1758
+ const v = Date.parse(iso);
1759
+ return Number.isNaN(v) ? null : v;
1760
+ }
1761
+ var SessionIndex = class {
1762
+ db;
1763
+ constructor(path) {
1764
+ if (path !== ":memory:") mkdirSync7(dirname6(path), { recursive: true });
1765
+ this.db = new DatabaseSync(path);
1766
+ this.db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
1767
+ this.migrate();
1768
+ }
1769
+ migrate() {
1770
+ const row = this.db.prepare("PRAGMA user_version").get();
1771
+ if ((row?.user_version ?? 0) < 1) {
1772
+ this.db.exec(`
1773
+ CREATE TABLE IF NOT EXISTS session (
1774
+ session_id TEXT PRIMARY KEY,
1775
+ host TEXT NOT NULL,
1776
+ agent TEXT NOT NULL,
1777
+ project_path TEXT NOT NULL DEFAULT '',
1778
+ model TEXT,
1779
+ started_at INTEGER,
1780
+ ended_at INTEGER,
1781
+ turn_count INTEGER NOT NULL DEFAULT 0,
1782
+ tool_call_count INTEGER NOT NULL DEFAULT 0,
1783
+ input_tokens INTEGER NOT NULL DEFAULT 0,
1784
+ output_tokens INTEGER NOT NULL DEFAULT 0,
1785
+ cost_usd REAL NOT NULL DEFAULT 0,
1786
+ title TEXT,
1787
+ labels_json TEXT NOT NULL DEFAULT '[]',
1788
+ topic TEXT,
1789
+ source_path TEXT NOT NULL,
1790
+ mtime_ms INTEGER NOT NULL DEFAULT 0,
1791
+ size_bytes INTEGER NOT NULL DEFAULT 0,
1792
+ indexed_at INTEGER NOT NULL DEFAULT 0
1793
+ );
1794
+ CREATE INDEX IF NOT EXISTS idx_session_started ON session(started_at DESC);
1795
+ CREATE INDEX IF NOT EXISTS idx_session_agent ON session(agent);
1796
+ CREATE TABLE IF NOT EXISTS turn (
1797
+ turn_uuid TEXT PRIMARY KEY,
1798
+ session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE,
1799
+ turn_index INTEGER NOT NULL,
1800
+ ts INTEGER,
1801
+ role TEXT NOT NULL,
1802
+ text TEXT NOT NULL DEFAULT '',
1803
+ tool_names_csv TEXT NOT NULL DEFAULT '',
1804
+ input_tokens INTEGER NOT NULL DEFAULT 0,
1805
+ output_tokens INTEGER NOT NULL DEFAULT 0,
1806
+ cost_usd REAL NOT NULL DEFAULT 0
1807
+ );
1808
+ CREATE INDEX IF NOT EXISTS idx_turn_session ON turn(session_id, turn_index);
1809
+ CREATE TABLE IF NOT EXISTS insight (
1810
+ session_id TEXT PRIMARY KEY REFERENCES session(session_id) ON DELETE CASCADE,
1811
+ topic TEXT,
1812
+ tags_json TEXT NOT NULL DEFAULT '[]',
1813
+ signals_json TEXT NOT NULL DEFAULT '[]',
1814
+ provider TEXT,
1815
+ generated_at TEXT
1816
+ );
1817
+ `);
1818
+ this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
1819
+ }
1820
+ }
1821
+ /** session_id -> {mtime_ms, size_bytes} for incremental sync invalidation. */
1822
+ knownSources() {
1823
+ const rows = this.db.prepare("SELECT session_id, mtime_ms, size_bytes FROM session").all();
1824
+ const m = /* @__PURE__ */ new Map();
1825
+ for (const r of rows) m.set(r.session_id, { mtime_ms: r.mtime_ms, size_bytes: r.size_bytes });
1826
+ return m;
1827
+ }
1828
+ upsertSession(env, src) {
1829
+ this.db.prepare(
1830
+ `INSERT INTO session (session_id, host, agent, project_path, model, started_at, ended_at,
1831
+ turn_count, tool_call_count, input_tokens, output_tokens, cost_usd, title, labels_json,
1832
+ topic, source_path, mtime_ms, size_bytes, indexed_at)
1833
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
1834
+ ON CONFLICT(session_id) DO UPDATE SET
1835
+ host=excluded.host, agent=excluded.agent, project_path=excluded.project_path,
1836
+ model=excluded.model, started_at=excluded.started_at, ended_at=excluded.ended_at,
1837
+ turn_count=excluded.turn_count, tool_call_count=excluded.tool_call_count,
1838
+ input_tokens=excluded.input_tokens, output_tokens=excluded.output_tokens,
1839
+ cost_usd=excluded.cost_usd, title=excluded.title, labels_json=excluded.labels_json,
1840
+ topic=excluded.topic, source_path=excluded.source_path, mtime_ms=excluded.mtime_ms,
1841
+ size_bytes=excluded.size_bytes, indexed_at=excluded.indexed_at`
1842
+ ).run(
1843
+ env.session_id,
1844
+ env.host,
1845
+ env.agent,
1846
+ env.project_path,
1847
+ env.model ?? null,
1848
+ toMs(env.started_at),
1849
+ toMs(env.ended_at),
1850
+ env.turn_count,
1851
+ env.tool_call_count,
1852
+ env.totals.input_tokens,
1853
+ env.totals.output_tokens,
1854
+ env.totals.cost_usd,
1855
+ env.title ?? null,
1856
+ JSON.stringify(env.labels ?? []),
1857
+ src.topic ?? null,
1858
+ src.source_path,
1859
+ src.mtime_ms,
1860
+ src.size_bytes,
1861
+ src.indexed_at
1862
+ );
1863
+ }
1864
+ replaceTurns(sessionId, turns) {
1865
+ this.db.prepare("DELETE FROM turn WHERE session_id = ?").run(sessionId);
1866
+ const stmt = this.db.prepare(
1867
+ `INSERT OR REPLACE INTO turn (turn_uuid, session_id, turn_index, ts, role, text,
1868
+ tool_names_csv, input_tokens, output_tokens, cost_usd) VALUES (?,?,?,?,?,?,?,?,?,?)`
1869
+ );
1870
+ for (const t of turns) {
1871
+ stmt.run(
1872
+ `${sessionId}#${t.turn_index}`,
1873
+ sessionId,
1874
+ t.turn_index,
1875
+ toMs(t.ts),
1876
+ t.role,
1877
+ t.text.slice(0, 8192),
1878
+ t.tool_calls.map((c) => c.name).join(","),
1879
+ t.usage.input_tokens,
1880
+ t.usage.output_tokens,
1881
+ t.telemetry?.cost_usd ?? 0
1882
+ );
1883
+ }
1884
+ }
1885
+ upsertInsight(ins) {
1886
+ this.db.prepare(
1887
+ `INSERT OR REPLACE INTO insight (session_id, topic, tags_json, signals_json, provider, generated_at)
1888
+ VALUES (?,?,?,?,?,?)`
1889
+ ).run(
1890
+ ins.session_id,
1891
+ ins.topic ?? null,
1892
+ JSON.stringify(ins.tags ?? []),
1893
+ JSON.stringify(ins.signals ?? []),
1894
+ ins.provider,
1895
+ ins.generated_at
1896
+ );
1897
+ }
1898
+ deleteSessions(ids) {
1899
+ if (ids.length === 0) return;
1900
+ const stmt = this.db.prepare("DELETE FROM session WHERE session_id = ?");
1901
+ for (const id of ids) stmt.run(id);
1902
+ }
1903
+ rowToIndex(r) {
1904
+ return {
1905
+ session_id: r.session_id,
1906
+ host: r.host,
1907
+ agent: r.agent,
1908
+ project_path: r.project_path,
1909
+ model: r.model ?? null,
1910
+ started_at: r.started_at ?? null,
1911
+ ended_at: r.ended_at ?? null,
1912
+ turn_count: r.turn_count,
1913
+ tool_call_count: r.tool_call_count,
1914
+ input_tokens: r.input_tokens,
1915
+ output_tokens: r.output_tokens,
1916
+ cost_usd: r.cost_usd,
1917
+ title: r.title ?? null,
1918
+ labels: safeJson(r.labels_json),
1919
+ topic: r.topic ?? null,
1920
+ source_path: r.source_path
1921
+ };
1922
+ }
1923
+ listRecent(limit = 50, agent) {
1924
+ const rows = agent ? this.db.prepare("SELECT * FROM session WHERE agent = ? ORDER BY started_at DESC LIMIT ?").all(agent, limit) : this.db.prepare("SELECT * FROM session ORDER BY started_at DESC LIMIT ?").all(limit);
1925
+ return rows.map((r) => this.rowToIndex(r));
1926
+ }
1927
+ getSession(id) {
1928
+ const r = this.db.prepare("SELECT * FROM session WHERE session_id = ?").get(id);
1929
+ return r ? this.rowToIndex(r) : void 0;
1930
+ }
1931
+ /** Full-text-ish search over turn text + session titles. */
1932
+ searchTurns(query, limit = 50) {
1933
+ const like = `%${query}%`;
1934
+ const rows = this.db.prepare(
1935
+ `SELECT DISTINCT s.* FROM session s
1936
+ LEFT JOIN turn t ON t.session_id = s.session_id
1937
+ WHERE t.text LIKE ? OR s.title LIKE ?
1938
+ ORDER BY s.started_at DESC LIMIT ?`
1939
+ ).all(like, like, limit);
1940
+ return rows.map((r) => this.rowToIndex(r));
1941
+ }
1942
+ stats() {
1943
+ const s = this.db.prepare("SELECT COUNT(*) c, COALESCE(SUM(cost_usd),0) cost FROM session").get();
1944
+ const t = this.db.prepare("SELECT COUNT(*) c FROM turn").get();
1945
+ const agents = this.db.prepare("SELECT agent, COUNT(*) c FROM session GROUP BY agent").all();
1946
+ const byAgent = {};
1947
+ for (const a of agents) byAgent[a.agent] = a.c;
1948
+ return { sessions: s.c, turns: t.c, cost_usd: Math.round(s.cost * 1e6) / 1e6, byAgent };
1949
+ }
1950
+ close() {
1951
+ this.db.close();
1952
+ }
1953
+ };
1954
+ function safeJson(s) {
1955
+ if (typeof s !== "string") return [];
1956
+ try {
1957
+ const v = JSON.parse(s);
1958
+ return Array.isArray(v) ? v : [];
1959
+ } catch {
1960
+ return [];
1961
+ }
1962
+ }
1963
+
1964
+ // src/index_store/sync.ts
1965
+ import { existsSync as existsSync13, readFileSync as readFileSync11, statSync as statSync2 } from "fs";
1966
+ import { safeParseInsights, safeParseSession as safeParseSession2 } from "@unpolarize/code-sessions-schema";
1967
+ function syncIndex(cfg, opts = {}) {
1968
+ const index = opts.index ?? new SessionIndex(cfg.indexPath);
1969
+ const ownsIndex = !opts.index;
1970
+ const now = opts.now ?? Date.now();
1971
+ try {
1972
+ const refs = listSessionDirs(cfg.storeDir);
1973
+ const known = index.knownSources();
1974
+ const seen = /* @__PURE__ */ new Set();
1975
+ let indexed = 0;
1976
+ let unchanged = 0;
1977
+ for (const ref of refs) {
1978
+ const envPath = envelopeFile(ref.dir);
1979
+ if (!existsSync13(envPath)) continue;
1980
+ const st = statSync2(envPath);
1981
+ const mtime_ms = Math.floor(st.mtimeMs);
1982
+ const size_bytes = st.size;
1983
+ seen.add(ref.sessionId);
1984
+ const cached = known.get(ref.sessionId);
1985
+ if (cached && cached.mtime_ms === mtime_ms && cached.size_bytes === size_bytes) {
1986
+ unchanged++;
1987
+ continue;
1988
+ }
1989
+ const parsed = safeParseSession2(JSON.parse(readFileSync11(envPath, "utf8")));
1990
+ if (!parsed.success) continue;
1991
+ const env = parsed.data;
1992
+ let topic;
1993
+ const insPath = insightsFile(ref.dir);
1994
+ let insights = void 0;
1995
+ if (existsSync13(insPath)) {
1996
+ const pi = safeParseInsights(JSON.parse(readFileSync11(insPath, "utf8")));
1997
+ if (pi.success) {
1998
+ insights = pi.data;
1999
+ topic = pi.data.topic;
2000
+ }
2001
+ }
2002
+ index.upsertSession(env, {
2003
+ source_path: envPath,
2004
+ mtime_ms,
2005
+ size_bytes,
2006
+ indexed_at: now,
2007
+ ...topic ? { topic } : {}
2008
+ });
2009
+ index.replaceTurns(env.session_id, readTurns(ref.dir));
2010
+ if (insights) index.upsertInsight(insights);
2011
+ indexed++;
2012
+ }
2013
+ const removed = [...known.keys()].filter((id) => !seen.has(id));
2014
+ index.deleteSessions(removed);
2015
+ return { total: refs.length, indexed, unchanged, removed: removed.length };
2016
+ } finally {
2017
+ if (ownsIndex) index.close();
2018
+ }
2019
+ }
2020
+
2021
+ // src/commands.ts
2022
+ import { existsSync as existsSync14, writeFileSync as writeFileSync7 } from "fs";
2023
+ import { join as join8 } from "path";
2024
+ function gitStoreFor(cfg) {
2025
+ return new GitStore(cfg.storeDir, {
2026
+ ...cfg.git.remote ? { remote: cfg.git.remote } : {},
2027
+ autoPush: cfg.git.autoPush
2028
+ });
2029
+ }
2030
+ function listClaudeTranscripts(projectsDir, maxDepth = 3) {
2031
+ const out = [];
2032
+ const walk = (dir, depth) => {
2033
+ if (depth > maxDepth || !existsSync14(dir)) return;
2034
+ for (const e of readEntries(dir)) {
2035
+ const name = String(e.name);
2036
+ if (e.isFile() && name.endsWith(".jsonl")) {
2037
+ out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join8(dir, name) });
2038
+ } else if (e.isDirectory()) {
2039
+ walk(join8(dir, name), depth + 1);
2040
+ }
2041
+ }
2042
+ };
2043
+ walk(projectsDir, 0);
2044
+ return out;
2045
+ }
2046
+ function cmdInit(cfg) {
2047
+ const git = gitStoreFor(cfg);
2048
+ git.init();
2049
+ const configPath = join8(cfg.storeDir, "config.json");
2050
+ if (!existsSync14(configPath)) {
2051
+ writeFileSync7(
2052
+ configPath,
2053
+ `${JSON.stringify({ insights: cfg.insights, batch: cfg.batch, hygiene: cfg.hygiene }, null, 2)}
2054
+ `
2055
+ );
2056
+ }
2057
+ git.commit("init store");
2058
+ return { code: 0, output: `Initialized store at ${cfg.storeDir}` };
2059
+ }
2060
+ function cmdStatus(cfg) {
2061
+ const state = new StateStore(cfg.statePath);
2062
+ const sessions = Object.keys(state.all());
2063
+ const stored = listSessionDirs(cfg.storeDir);
2064
+ const socketUp = existsSync14(cfg.socketPath);
2065
+ const lines = [
2066
+ `store: ${cfg.storeDir}`,
2067
+ `host: ${cfg.host}`,
2068
+ `daemon: ${socketUp ? "running (socket present)" : "not running"}`,
2069
+ `tracked: ${sessions.length} session(s) in state`,
2070
+ `stored: ${stored.length} session(s) in store`,
2071
+ `insights: ${cfg.insights.provider} / ${cfg.insights.mode}`,
2072
+ `remote: ${cfg.git.remote ?? "(none)"} autoPush=${cfg.git.autoPush}`
2073
+ ];
2074
+ return { code: 0, output: lines.join("\n") };
2075
+ }
2076
+ async function cmdBackfill(cfg, opts = {}) {
2077
+ const agent = opts.agent ?? "claude";
2078
+ const parts = [];
2079
+ let sessions = 0;
2080
+ let turns = 0;
2081
+ if (agent === "claude" || agent === "all") {
2082
+ const projectsDir = opts.projectsDir ?? cfg.claudeProjectsDir;
2083
+ const transcripts = listClaudeTranscripts(projectsDir);
2084
+ const engine = new CaptureEngine(cfg, new StateStore(cfg.statePath));
2085
+ let t = 0;
2086
+ for (const tr of transcripts) t += engine.captureSession(tr.sessionId, tr.path).newTurns;
2087
+ sessions += transcripts.length;
2088
+ turns += t;
2089
+ parts.push(`claude: ${transcripts.length} sessions / ${t} turns`);
2090
+ }
2091
+ if (agent === "grok" || agent === "all") {
2092
+ const found = discoverGrokSessions();
2093
+ let n = 0;
2094
+ let t = 0;
2095
+ for (const info of found) {
2096
+ const imported = parseGrokSession(info, cfg.host);
2097
+ if (!imported) continue;
2098
+ t += writeImportedSession(cfg, imported).turns;
2099
+ n++;
2100
+ }
2101
+ sessions += n;
2102
+ turns += t;
2103
+ parts.push(`grok: ${n} sessions / ${t} turns`);
2104
+ }
2105
+ if (agent === "codex" || agent === "all") {
2106
+ const found = discoverCodexSessions();
2107
+ let n = 0;
2108
+ let t = 0;
2109
+ for (const info of found) {
2110
+ const imported = parseCodexSession(info, cfg.host);
2111
+ if (!imported) continue;
2112
+ t += writeImportedSession(cfg, imported).turns;
2113
+ n++;
2114
+ }
2115
+ sessions += n;
2116
+ turns += t;
2117
+ parts.push(`codex: ${n} sessions / ${t} turns`);
2118
+ }
2119
+ const git = gitStoreFor(cfg);
2120
+ git.init();
2121
+ git.commit(`backfill (${agent}): ${sessions} sessions`);
2122
+ return { code: 0, output: `Backfilled ${sessions} session(s), ${turns} turn(s) \u2014 ${parts.join(", ")}` };
2123
+ }
2124
+ async function cmdReindex(cfg, opts = {}) {
2125
+ const provider = makeProvider(cfg) ?? new FakeProvider();
2126
+ const res = await reindexStore(cfg, provider, opts.since ? { sinceMonth: opts.since } : {});
2127
+ const git = gitStoreFor(cfg);
2128
+ if (git.isRepo()) git.sync(`insights reindex (${res.count})`);
2129
+ return { code: 0, output: `Reindexed ${res.count} session(s) with provider=${provider.name}` };
2130
+ }
2131
+ function cmdInstallHooks(cfg, opts = {}) {
2132
+ const home = cfg.claudeProjectsDir.replace(/\/projects\/?$/, "");
2133
+ const settingsPath = opts.settingsPath ?? join8(home, "settings.json");
2134
+ const command = opts.command ?? "code-sessions hook";
2135
+ const res = installHooks(settingsPath, command);
2136
+ return {
2137
+ code: 0,
2138
+ output: res.added.length > 0 ? `Installed hooks (${res.added.join(", ")}) \u2192 ${settingsPath}` : `Hooks already present \u2192 ${settingsPath}`
2139
+ };
2140
+ }
2141
+ function cmdDoctor(cfg) {
2142
+ const checks = [
2143
+ ["store dir exists", existsSync14(cfg.storeDir)],
2144
+ ["store is git repo", existsSync14(join8(cfg.storeDir, ".git"))],
2145
+ ["daemon socket present", existsSync14(cfg.socketPath)],
2146
+ ["claude projects dir", existsSync14(cfg.claudeProjectsDir)]
2147
+ ];
2148
+ const lines = checks.map(([name, ok]) => `${ok ? "\u2713" : "\u2717"} ${name}`);
2149
+ const code = checks.every(([, ok]) => ok) ? 0 : 1;
2150
+ return { code, output: lines.join("\n") };
2151
+ }
2152
+ async function cmdExport(cfg, opts = {}) {
2153
+ if (!cfg.telemetry.enabled) {
2154
+ return { code: 0, output: "Telemetry export disabled (telemetry.enabled=false)" };
2155
+ }
2156
+ const res = await exportStore(cfg, opts.since ? { sinceMonth: opts.since } : {});
2157
+ return {
2158
+ code: 0,
2159
+ output: `Exported ${res.exported}/${res.total} session(s) to ${cfg.telemetry.endpoint} (${res.failed} failed)`
2160
+ };
2161
+ }
2162
+ function cmdIndex(cfg) {
2163
+ const stats = syncIndex(cfg);
2164
+ return {
2165
+ code: 0,
2166
+ output: `Indexed ${stats.indexed} new/changed, ${stats.unchanged} unchanged, ${stats.removed} removed \u2192 ${cfg.indexPath}`
2167
+ };
2168
+ }
2169
+ function fmtRow(r) {
2170
+ const date = r.started_at ? new Date(r.started_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014".padEnd(16);
2171
+ const agent = (r.agent || "?").padEnd(11).slice(0, 11);
2172
+ const tok = String(r.input_tokens + r.output_tokens).padStart(8);
2173
+ const cost = `$${r.cost_usd.toFixed(2)}`.padStart(8);
2174
+ const title = (r.topic || r.title || r.session_id).slice(0, 48);
2175
+ return `${date} ${agent} ${tok} ${cost} ${title}`;
2176
+ }
2177
+ function cmdQuery(cfg, opts = {}) {
2178
+ const index = new SessionIndex(cfg.indexPath);
2179
+ try {
2180
+ const rows = index.listRecent(opts.limit ?? 25, opts.agent);
2181
+ const s = index.stats();
2182
+ const header = `# ${s.sessions} sessions, ${s.turns} turns, $${s.cost_usd.toFixed(2)} \u2014 ${Object.entries(s.byAgent).map(([a, n]) => `${a}:${n}`).join(" ")}`;
2183
+ return { code: 0, output: [header, ...rows.map(fmtRow)].join("\n") };
2184
+ } finally {
2185
+ index.close();
2186
+ }
2187
+ }
2188
+ function cmdSearch(cfg, opts) {
2189
+ if (!opts.query) return { code: 1, output: "usage: code-sessions search <text> [--limit N]" };
2190
+ const index = new SessionIndex(cfg.indexPath);
2191
+ try {
2192
+ const rows = index.searchTurns(opts.query, opts.limit ?? 25);
2193
+ return {
2194
+ code: 0,
2195
+ output: rows.length ? [`# ${rows.length} match(es) for "${opts.query}"`, ...rows.map(fmtRow)].join("\n") : `No matches for "${opts.query}"`
2196
+ };
2197
+ } finally {
2198
+ index.close();
2199
+ }
2200
+ }
2201
+ async function startDaemon(cfg) {
2202
+ const provider = makeProvider(cfg);
2203
+ const wantInsights = provider && cfg.insights.mode !== "off";
2204
+ const wantTelemetry = cfg.telemetry.enabled;
2205
+ const deps = wantInsights || wantTelemetry ? {
2206
+ onSessionEnd: async (sessionId, sessionDir2) => {
2207
+ if (wantInsights && provider) {
2208
+ await labelSession(sessionDir2, { sessionId, host: cfg.host }, provider);
2209
+ }
2210
+ if (wantTelemetry) {
2211
+ await exportSession(cfg, sessionDir2);
2212
+ }
2213
+ }
2214
+ } : {};
2215
+ const daemon = new Daemon(cfg, deps);
2216
+ await daemon.start();
2217
+ return daemon;
2218
+ }
2219
+
2220
+ // src/hooks/shim.ts
2221
+ import { existsSync as existsSync15 } from "fs";
2222
+ async function handleHookInput(socketPath, rawInput) {
2223
+ if (!existsSync15(socketPath)) return { ok: false, error: "daemon not running" };
2224
+ let parsed;
2225
+ try {
2226
+ parsed = JSON.parse(rawInput);
2227
+ } catch {
2228
+ return { ok: false, error: "invalid hook json" };
2229
+ }
2230
+ const evt = parseHookEvent(parsed);
2231
+ if (!evt) return { ok: false, error: "unrecognized hook payload" };
2232
+ return sendEvent(socketPath, evt);
2233
+ }
2234
+ async function readStdin() {
2235
+ const chunks = [];
2236
+ for await (const chunk of process.stdin) chunks.push(chunk);
2237
+ return Buffer.concat(chunks).toString("utf8");
2238
+ }
2239
+
2240
+ export {
2241
+ defaultConfig,
2242
+ resolveConfig,
2243
+ loadConfig,
2244
+ parseFlags,
2245
+ overridesFromFlags,
2246
+ HELP,
2247
+ scrubSecrets,
2248
+ sha256,
2249
+ applyHygiene,
2250
+ priceFor,
2251
+ estimateCostUsd,
2252
+ monthOf,
2253
+ sessionDir,
2254
+ turnFile,
2255
+ envelopeFile,
2256
+ insightsFile,
2257
+ telemetryFile,
2258
+ rawBlobFile,
2259
+ writeTurnFile,
2260
+ writeBlobFile,
2261
+ readTurns,
2262
+ computeEnvelope,
2263
+ rebuildEnvelope,
2264
+ readNewLines,
2265
+ CaptureEngine,
2266
+ isSessionEndEvent,
2267
+ parseHookEvent,
2268
+ sendEvent,
2269
+ StateStore,
2270
+ GitStore,
2271
+ readEntries,
2272
+ listSessionDirs,
2273
+ findTranscript,
2274
+ Daemon,
2275
+ THRESHOLDS,
2276
+ deriveSignals,
2277
+ guessTopic,
2278
+ deriveTags,
2279
+ FakeProvider,
2280
+ buildPrompt,
2281
+ parseLabelJson,
2282
+ LlmProvider,
2283
+ claudeRunner,
2284
+ grokRunner,
2285
+ ollamaRunner,
2286
+ makeProvider,
2287
+ labelSession,
2288
+ reindexStore,
2289
+ DEFAULT_HOOK_EVENTS,
2290
+ mergeHooks,
2291
+ installHooks,
2292
+ isoNano,
2293
+ buildTracePayload,
2294
+ buildMetricPayload,
2295
+ postOtlp,
2296
+ exportSession,
2297
+ exportStore,
2298
+ grokSessionsRoot,
2299
+ discoverGrokSessions,
2300
+ parseGrokSession,
2301
+ codexSessionsRoot,
2302
+ discoverCodexSessions,
2303
+ parseCodexSession,
2304
+ writeImportedSession,
2305
+ SessionIndex,
2306
+ syncIndex,
2307
+ listClaudeTranscripts,
2308
+ cmdInit,
2309
+ cmdStatus,
2310
+ cmdBackfill,
2311
+ cmdReindex,
2312
+ cmdInstallHooks,
2313
+ cmdDoctor,
2314
+ cmdExport,
2315
+ cmdIndex,
2316
+ cmdQuery,
2317
+ cmdSearch,
2318
+ startDaemon,
2319
+ handleHookInput,
2320
+ readStdin
2321
+ };