@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.
- package/bin/code-sessions.mjs +20 -0
- package/dist/chunk-ZJG2DWAK.js +2321 -0
- package/dist/cli.js +308 -0
- package/dist/index.js +162 -0
- package/package.json +21 -0
- package/src/adapters/adapters.test.ts +121 -0
- package/src/adapters/codex.ts +228 -0
- package/src/adapters/grok.ts +179 -0
- package/src/adapters/import.ts +79 -0
- package/src/adapters/index.ts +3 -0
- package/src/analytics/analytics.test.ts +94 -0
- package/src/analytics/command.ts +38 -0
- package/src/analytics/digest.ts +48 -0
- package/src/analytics/rollup.ts +114 -0
- package/src/analytics/site.ts +41 -0
- package/src/capture.test.ts +103 -0
- package/src/capture.ts +121 -0
- package/src/cli.ts +118 -0
- package/src/cliargs.test.ts +31 -0
- package/src/cliargs.ts +77 -0
- package/src/commands.test.ts +99 -0
- package/src/commands.ts +266 -0
- package/src/config.test.ts +36 -0
- package/src/config.ts +158 -0
- package/src/daemon.test.ts +130 -0
- package/src/daemon.ts +216 -0
- package/src/hooks/install.test.ts +47 -0
- package/src/hooks/install.ts +81 -0
- package/src/hooks/shim.test.ts +57 -0
- package/src/hooks/shim.ts +26 -0
- package/src/hygiene.test.ts +78 -0
- package/src/hygiene.ts +107 -0
- package/src/index.ts +21 -0
- package/src/index_store/db.test.ts +108 -0
- package/src/index_store/db.ts +289 -0
- package/src/index_store/index.ts +2 -0
- package/src/index_store/sync.test.ts +88 -0
- package/src/index_store/sync.ts +83 -0
- package/src/insights/heuristics.test.ts +71 -0
- package/src/insights/heuristics.ts +106 -0
- package/src/insights/index.ts +4 -0
- package/src/insights/labeler.test.ts +105 -0
- package/src/insights/labeler.ts +136 -0
- package/src/insights/llm.test.ts +77 -0
- package/src/insights/llm.ts +130 -0
- package/src/insights/provider.ts +37 -0
- package/src/ipc.test.ts +35 -0
- package/src/ipc.ts +70 -0
- package/src/pricing.test.ts +28 -0
- package/src/pricing.ts +45 -0
- package/src/state.test.ts +46 -0
- package/src/state.ts +89 -0
- package/src/store/git.test.ts +99 -0
- package/src/store/git.ts +138 -0
- package/src/store/paths.ts +45 -0
- package/src/store/scan.ts +39 -0
- package/src/store/writer.test.ts +93 -0
- package/src/store/writer.ts +135 -0
- package/src/tail.test.ts +50 -0
- package/src/tail.ts +47 -0
- package/src/telemetry/exporter.test.ts +104 -0
- package/src/telemetry/exporter.ts +64 -0
- package/src/telemetry/index.ts +2 -0
- package/src/telemetry/otlp.test.ts +123 -0
- package/src/telemetry/otlp.ts +215 -0
- package/src/test/e2e.test.ts +112 -0
- 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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
+
}
|