@unpolarize/code-sessions 0.2.0 → 0.3.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/dist/{chunk-HV6FQJPS.js → chunk-ON3CPW4C.js} +252 -34
- package/dist/cli.js +5 -1
- package/dist/index.js +9 -1
- package/package.json +2 -2
- package/src/adapters/adapters.test.ts +36 -0
- package/src/adapters/codebuild.ts +188 -0
- package/src/adapters/index.ts +1 -0
- package/src/cli.ts +5 -1
- package/src/cliargs.ts +2 -1
- package/src/commands.ts +37 -1
- package/src/index_store/db.test.ts +25 -0
- package/src/index_store/db.ts +72 -0
|
@@ -135,11 +135,12 @@ Commands:
|
|
|
135
135
|
install-hooks Install Claude Code hooks that feed the daemon
|
|
136
136
|
install-skills Install the cs-label-session skill into agents [--agent claude|codex|grok|all]
|
|
137
137
|
hook (internal) forward a hook payload from stdin to the daemon
|
|
138
|
-
backfill Import existing transcripts into the store [--agent claude|grok|codex|all]
|
|
138
|
+
backfill Import existing transcripts into the store [--agent claude|grok|codex|codebuild|all]
|
|
139
139
|
reindex (Re)derive insights for stored sessions [--since YYYY-MM]
|
|
140
140
|
export Export stored sessions as OTLP to a collector [--since YYYY-MM]
|
|
141
141
|
index (Re)build the internal SQLite index from the git store
|
|
142
142
|
query List recent sessions from the index [--limit N] [--agent X]
|
|
143
|
+
usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
|
|
143
144
|
search Full-text search session turns <text> [--limit N]
|
|
144
145
|
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
145
146
|
analytics Compute MVP-2 rollups + digest into analytics/
|
|
@@ -1761,8 +1762,144 @@ function parseCodexSession(info, host) {
|
|
|
1761
1762
|
return { host, sessionId, agent: "codex", turns, meta };
|
|
1762
1763
|
}
|
|
1763
1764
|
|
|
1765
|
+
// src/adapters/codebuild.ts
|
|
1766
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
1767
|
+
import { homedir as homedir4 } from "os";
|
|
1768
|
+
import { join as join8 } from "path";
|
|
1769
|
+
import {
|
|
1770
|
+
SCHEMA_VERSIONS as SCHEMA_VERSIONS5
|
|
1771
|
+
} from "@unpolarize/code-sessions-schema";
|
|
1772
|
+
function codebuildSessionsRoot() {
|
|
1773
|
+
return join8(homedir4(), ".codebuild", "sessions");
|
|
1774
|
+
}
|
|
1775
|
+
function discoverCodebuildSessions(root = codebuildSessionsRoot()) {
|
|
1776
|
+
if (!existsSync12(root)) return [];
|
|
1777
|
+
let files;
|
|
1778
|
+
try {
|
|
1779
|
+
files = readdirSync4(root).filter((f) => f.endsWith(".jsonl"));
|
|
1780
|
+
} catch {
|
|
1781
|
+
return [];
|
|
1782
|
+
}
|
|
1783
|
+
return files.map((f) => ({ sessionId: f.replace(/\.jsonl$/, ""), path: join8(root, f) }));
|
|
1784
|
+
}
|
|
1785
|
+
function backendToAgent(backend) {
|
|
1786
|
+
if (backend === "claude") return "claude-code";
|
|
1787
|
+
if (backend === "grok") return "grok";
|
|
1788
|
+
if (backend === "codex") return "codex";
|
|
1789
|
+
return "unknown";
|
|
1790
|
+
}
|
|
1791
|
+
function parseCodebuildSession(info, host) {
|
|
1792
|
+
let raw = "";
|
|
1793
|
+
try {
|
|
1794
|
+
raw = readFileSync10(info.path, "utf8");
|
|
1795
|
+
} catch {
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1799
|
+
const baseMs = statMtime2(info.path);
|
|
1800
|
+
let agent = "unknown";
|
|
1801
|
+
let sessionId = info.sessionId;
|
|
1802
|
+
let title;
|
|
1803
|
+
let cwd;
|
|
1804
|
+
const turns = [];
|
|
1805
|
+
let pending = null;
|
|
1806
|
+
let idx = 0;
|
|
1807
|
+
const flush = () => {
|
|
1808
|
+
if (!pending) return;
|
|
1809
|
+
const p = pending;
|
|
1810
|
+
pending = null;
|
|
1811
|
+
const turn = mkTurn2(sessionId, host, agent, idx++, baseMs, "assistant", p.text, p.tools);
|
|
1812
|
+
turn.usage = {
|
|
1813
|
+
input_tokens: p.input_tokens,
|
|
1814
|
+
output_tokens: p.output_tokens,
|
|
1815
|
+
cache_read_tokens: p.cache_read_tokens,
|
|
1816
|
+
cache_write_tokens: 0
|
|
1817
|
+
};
|
|
1818
|
+
if (p.cost_usd > 0) turn.telemetry = { cost_usd: p.cost_usd };
|
|
1819
|
+
turns.push(turn);
|
|
1820
|
+
};
|
|
1821
|
+
const ensurePending = () => {
|
|
1822
|
+
if (!pending) pending = { text: "", tools: [], input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cost_usd: 0 };
|
|
1823
|
+
return pending;
|
|
1824
|
+
};
|
|
1825
|
+
for (const line of lines) {
|
|
1826
|
+
let ev;
|
|
1827
|
+
try {
|
|
1828
|
+
ev = JSON.parse(line);
|
|
1829
|
+
} catch {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (ev.type === "meta" && ev.meta) {
|
|
1833
|
+
agent = backendToAgent(ev.meta.backend);
|
|
1834
|
+
if (typeof ev.meta.id === "string") sessionId = ev.meta.id;
|
|
1835
|
+
if (typeof ev.meta.title === "string") title = ev.meta.title;
|
|
1836
|
+
if (typeof ev.meta.cwd === "string") cwd = ev.meta.cwd;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (ev.type === "user" && typeof ev.text === "string") {
|
|
1840
|
+
flush();
|
|
1841
|
+
turns.push(mkTurn2(sessionId, host, agent, idx++, baseMs, "user", ev.text, []));
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (ev.type === "update" && ev.update) {
|
|
1845
|
+
const u = ev.update;
|
|
1846
|
+
if (u.kind === "agent_message_chunk") {
|
|
1847
|
+
const t = u.content?.text;
|
|
1848
|
+
if (typeof t === "string") ensurePending().text += t;
|
|
1849
|
+
} else if (u.kind === "tool_call" && u.toolCall) {
|
|
1850
|
+
const p = ensurePending();
|
|
1851
|
+
p.tools.push({ name: String(u.toolCall.title ?? u.toolCall.kind ?? "tool"), input: u.toolCall.rawInput });
|
|
1852
|
+
} else if (u.kind === "usage" && u.usage) {
|
|
1853
|
+
const p = ensurePending();
|
|
1854
|
+
p.input_tokens += Number(u.usage.inputTokens) || 0;
|
|
1855
|
+
p.output_tokens += Number(u.usage.outputTokens) || 0;
|
|
1856
|
+
p.cache_read_tokens += Number(u.usage.cacheReadTokens) || 0;
|
|
1857
|
+
} else if (u.kind === "result" && u.usage) {
|
|
1858
|
+
const p = ensurePending();
|
|
1859
|
+
p.input_tokens += Number(u.usage.inputTokens) || 0;
|
|
1860
|
+
p.output_tokens += Number(u.usage.outputTokens) || 0;
|
|
1861
|
+
p.cost_usd += Number(u.usage.costUsd) || 0;
|
|
1862
|
+
flush();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
flush();
|
|
1867
|
+
if (turns.length === 0) return null;
|
|
1868
|
+
const meta = {
|
|
1869
|
+
session_id: sessionId,
|
|
1870
|
+
started_at: turns[0].ts,
|
|
1871
|
+
ended_at: turns[turns.length - 1].ts
|
|
1872
|
+
};
|
|
1873
|
+
if (cwd) meta.project_path = cwd;
|
|
1874
|
+
if (title) meta.title = title;
|
|
1875
|
+
return { host, sessionId, agent, turns, meta };
|
|
1876
|
+
}
|
|
1877
|
+
function statMtime2(p) {
|
|
1878
|
+
try {
|
|
1879
|
+
return statSync2(p).mtimeMs;
|
|
1880
|
+
} catch {
|
|
1881
|
+
return Date.parse("2020-01-01T00:00:00Z");
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
function mkTurn2(sessionId, host, agent, index, baseMs, role, text, tool_calls) {
|
|
1885
|
+
return {
|
|
1886
|
+
schema: SCHEMA_VERSIONS5.turn,
|
|
1887
|
+
session_id: sessionId,
|
|
1888
|
+
host,
|
|
1889
|
+
agent,
|
|
1890
|
+
turn_index: index,
|
|
1891
|
+
ts: new Date(baseMs + index * 1e3).toISOString(),
|
|
1892
|
+
role,
|
|
1893
|
+
text,
|
|
1894
|
+
tool_calls,
|
|
1895
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
|
|
1896
|
+
scrubbed: false,
|
|
1897
|
+
raw_ref: null
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1764
1901
|
// src/adapters/import.ts
|
|
1765
|
-
import { existsSync as
|
|
1902
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
1766
1903
|
import { dirname as dirname5 } from "path";
|
|
1767
1904
|
var NATIVE_FORMAT = {
|
|
1768
1905
|
"claude-code": "claude-jsonl",
|
|
@@ -1793,9 +1930,9 @@ function writeImportedSession(cfg, s) {
|
|
|
1793
1930
|
});
|
|
1794
1931
|
env.native_ref.format = NATIVE_FORMAT[s.agent] ?? "unknown";
|
|
1795
1932
|
const envPath = envelopeFile(dir);
|
|
1796
|
-
if (
|
|
1933
|
+
if (existsSync13(envPath)) {
|
|
1797
1934
|
try {
|
|
1798
|
-
const prev = JSON.parse(
|
|
1935
|
+
const prev = JSON.parse(readFileSync11(envPath, "utf8"));
|
|
1799
1936
|
if (prev.labels?.length) env.labels = prev.labels;
|
|
1800
1937
|
} catch {
|
|
1801
1938
|
}
|
|
@@ -1816,6 +1953,9 @@ function toMs(iso) {
|
|
|
1816
1953
|
const v = Date.parse(iso);
|
|
1817
1954
|
return Number.isNaN(v) ? null : v;
|
|
1818
1955
|
}
|
|
1956
|
+
function round6(n) {
|
|
1957
|
+
return Math.round(n * 1e6) / 1e6;
|
|
1958
|
+
}
|
|
1819
1959
|
var SessionIndex = class {
|
|
1820
1960
|
db;
|
|
1821
1961
|
constructor(path) {
|
|
@@ -2017,6 +2157,48 @@ var SessionIndex = class {
|
|
|
2017
2157
|
).all(like, like, limit);
|
|
2018
2158
|
return rows.map((r) => this.rowToIndex(r));
|
|
2019
2159
|
}
|
|
2160
|
+
/** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
|
|
2161
|
+
usageSummary(opts = {}) {
|
|
2162
|
+
const rows = this.db.prepare(
|
|
2163
|
+
"SELECT session_id, agent, started_at, input_tokens, output_tokens, cost_usd, projects_json, topic, title FROM session"
|
|
2164
|
+
).all();
|
|
2165
|
+
const totals = { sessions: rows.length, input_tokens: 0, output_tokens: 0, cost_usd: 0 };
|
|
2166
|
+
const byAgent = {};
|
|
2167
|
+
const byDay = {};
|
|
2168
|
+
const byProject = {};
|
|
2169
|
+
const add = (m, key, r) => {
|
|
2170
|
+
const b = m[key] ??= { sessions: 0, input_tokens: 0, output_tokens: 0, cost_usd: 0 };
|
|
2171
|
+
b.sessions++;
|
|
2172
|
+
b.input_tokens += r.input_tokens;
|
|
2173
|
+
b.output_tokens += r.output_tokens;
|
|
2174
|
+
b.cost_usd += r.cost_usd;
|
|
2175
|
+
};
|
|
2176
|
+
for (const r of rows) {
|
|
2177
|
+
totals.input_tokens += r.input_tokens;
|
|
2178
|
+
totals.output_tokens += r.output_tokens;
|
|
2179
|
+
totals.cost_usd += r.cost_usd;
|
|
2180
|
+
add(byAgent, r.agent || "unknown", r);
|
|
2181
|
+
if (r.started_at) add(byDay, new Date(r.started_at).toISOString().slice(0, 10), r);
|
|
2182
|
+
for (const p of safeJson(r.projects_json)) add(byProject, p, r);
|
|
2183
|
+
}
|
|
2184
|
+
totals.cost_usd = round6(totals.cost_usd);
|
|
2185
|
+
for (const m of [byAgent, byDay, byProject]) for (const b of Object.values(m)) b.cost_usd = round6(b.cost_usd);
|
|
2186
|
+
const days = opts.days ?? 30;
|
|
2187
|
+
const recentDays = Object.entries(byDay).sort((a, b) => a[0] < b[0] ? 1 : -1).slice(0, days).map(([day, b]) => ({ day, ...b }));
|
|
2188
|
+
const topByCost = rows.map((r) => ({
|
|
2189
|
+
session_id: r.session_id,
|
|
2190
|
+
agent: r.agent,
|
|
2191
|
+
cost_usd: round6(r.cost_usd),
|
|
2192
|
+
label: r.topic || r.title || r.session_id
|
|
2193
|
+
})).sort((a, b) => b.cost_usd - a.cost_usd).slice(0, opts.topN ?? 10);
|
|
2194
|
+
return {
|
|
2195
|
+
totals,
|
|
2196
|
+
byAgent,
|
|
2197
|
+
byDay: recentDays,
|
|
2198
|
+
byProject,
|
|
2199
|
+
topByCost
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2020
2202
|
stats() {
|
|
2021
2203
|
const s = this.db.prepare("SELECT COUNT(*) c, COALESCE(SUM(cost_usd),0) cost FROM session").get();
|
|
2022
2204
|
const t = this.db.prepare("SELECT COUNT(*) c FROM turn").get();
|
|
@@ -2040,7 +2222,7 @@ function safeJson(s) {
|
|
|
2040
2222
|
}
|
|
2041
2223
|
|
|
2042
2224
|
// src/index_store/sync.ts
|
|
2043
|
-
import { existsSync as
|
|
2225
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
2044
2226
|
import { safeParseInsights, safeParseSession as safeParseSession2 } from "@unpolarize/code-sessions-schema";
|
|
2045
2227
|
function syncIndex(cfg, opts = {}) {
|
|
2046
2228
|
const index = opts.index ?? new SessionIndex(cfg.indexPath);
|
|
@@ -2054,8 +2236,8 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2054
2236
|
let unchanged = 0;
|
|
2055
2237
|
for (const ref of refs) {
|
|
2056
2238
|
const envPath = envelopeFile(ref.dir);
|
|
2057
|
-
if (!
|
|
2058
|
-
const st =
|
|
2239
|
+
if (!existsSync14(envPath)) continue;
|
|
2240
|
+
const st = statSync3(envPath);
|
|
2059
2241
|
const mtime_ms = Math.floor(st.mtimeMs);
|
|
2060
2242
|
const size_bytes = st.size;
|
|
2061
2243
|
seen.add(ref.sessionId);
|
|
@@ -2064,7 +2246,7 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2064
2246
|
unchanged++;
|
|
2065
2247
|
continue;
|
|
2066
2248
|
}
|
|
2067
|
-
const parsed = safeParseSession2(JSON.parse(
|
|
2249
|
+
const parsed = safeParseSession2(JSON.parse(readFileSync12(envPath, "utf8")));
|
|
2068
2250
|
if (!parsed.success) continue;
|
|
2069
2251
|
const env = parsed.data;
|
|
2070
2252
|
let topic;
|
|
@@ -2072,8 +2254,8 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2072
2254
|
let projects = [];
|
|
2073
2255
|
const insPath = insightsFile(ref.dir);
|
|
2074
2256
|
let insights = void 0;
|
|
2075
|
-
if (
|
|
2076
|
-
const pi = safeParseInsights(JSON.parse(
|
|
2257
|
+
if (existsSync14(insPath)) {
|
|
2258
|
+
const pi = safeParseInsights(JSON.parse(readFileSync12(insPath, "utf8")));
|
|
2077
2259
|
if (pi.success) {
|
|
2078
2260
|
insights = pi.data;
|
|
2079
2261
|
topic = pi.data.topic;
|
|
@@ -2146,36 +2328,36 @@ ${buildLabelSkillBody()}
|
|
|
2146
2328
|
|
|
2147
2329
|
// src/skills/install.ts
|
|
2148
2330
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
2149
|
-
import { homedir as
|
|
2150
|
-
import { dirname as dirname7, join as
|
|
2331
|
+
import { homedir as homedir5 } from "os";
|
|
2332
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
2151
2333
|
function targetsFor(agent, home) {
|
|
2152
2334
|
const out = [];
|
|
2153
2335
|
const want = (a) => agent === "all" || agent === a;
|
|
2154
2336
|
if (want("claude")) {
|
|
2155
2337
|
out.push({
|
|
2156
2338
|
agent: "claude",
|
|
2157
|
-
path:
|
|
2339
|
+
path: join9(home, ".claude", "skills", "cs-label-session", "SKILL.md"),
|
|
2158
2340
|
content: buildClaudeSkill()
|
|
2159
2341
|
});
|
|
2160
2342
|
}
|
|
2161
2343
|
if (want("codex")) {
|
|
2162
2344
|
out.push({
|
|
2163
2345
|
agent: "codex",
|
|
2164
|
-
path:
|
|
2346
|
+
path: join9(home, ".codex", "prompts", "cs-label-session.md"),
|
|
2165
2347
|
content: buildPromptFile()
|
|
2166
2348
|
});
|
|
2167
2349
|
}
|
|
2168
2350
|
if (want("grok")) {
|
|
2169
2351
|
out.push({
|
|
2170
2352
|
agent: "grok",
|
|
2171
|
-
path:
|
|
2353
|
+
path: join9(home, ".grok", "prompts", "cs-label-session.md"),
|
|
2172
2354
|
content: buildPromptFile()
|
|
2173
2355
|
});
|
|
2174
2356
|
}
|
|
2175
2357
|
return out;
|
|
2176
2358
|
}
|
|
2177
2359
|
function installSkills(opts = {}) {
|
|
2178
|
-
const home = opts.home ??
|
|
2360
|
+
const home = opts.home ?? homedir5();
|
|
2179
2361
|
const installed = [];
|
|
2180
2362
|
for (const t of targetsFor(opts.agent ?? "all", home)) {
|
|
2181
2363
|
mkdirSync8(dirname7(t.path), { recursive: true });
|
|
@@ -2186,7 +2368,7 @@ function installSkills(opts = {}) {
|
|
|
2186
2368
|
}
|
|
2187
2369
|
|
|
2188
2370
|
// src/fork.ts
|
|
2189
|
-
import { existsSync as
|
|
2371
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
2190
2372
|
import { randomUUID } from "crypto";
|
|
2191
2373
|
import { mkdirSync as mkdirSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
2192
2374
|
import { dirname as dirname8 } from "path";
|
|
@@ -2199,8 +2381,8 @@ function locateSession(storeDir, sessionId) {
|
|
|
2199
2381
|
}
|
|
2200
2382
|
function loadEnvelope2(dir) {
|
|
2201
2383
|
const p = envelopeFile(dir);
|
|
2202
|
-
if (!
|
|
2203
|
-
const parsed = safeParseSession3(JSON.parse(
|
|
2384
|
+
if (!existsSync15(p)) return void 0;
|
|
2385
|
+
const parsed = safeParseSession3(JSON.parse(readFileSync13(p, "utf8")));
|
|
2204
2386
|
return parsed.success ? parsed.data : void 0;
|
|
2205
2387
|
}
|
|
2206
2388
|
function writeJsonAtomic4(path, value) {
|
|
@@ -2249,8 +2431,8 @@ function forkSession(cfg, opts) {
|
|
|
2249
2431
|
}
|
|
2250
2432
|
|
|
2251
2433
|
// src/commands.ts
|
|
2252
|
-
import { existsSync as
|
|
2253
|
-
import { join as
|
|
2434
|
+
import { existsSync as existsSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
2435
|
+
import { join as join10 } from "path";
|
|
2254
2436
|
function gitStoreFor(cfg) {
|
|
2255
2437
|
return new GitStore(cfg.storeDir, {
|
|
2256
2438
|
...cfg.git.remote ? { remote: cfg.git.remote } : {},
|
|
@@ -2260,13 +2442,13 @@ function gitStoreFor(cfg) {
|
|
|
2260
2442
|
function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
2261
2443
|
const out = [];
|
|
2262
2444
|
const walk = (dir, depth) => {
|
|
2263
|
-
if (depth > maxDepth || !
|
|
2445
|
+
if (depth > maxDepth || !existsSync16(dir)) return;
|
|
2264
2446
|
for (const e of readEntries(dir)) {
|
|
2265
2447
|
const name = String(e.name);
|
|
2266
2448
|
if (e.isFile() && name.endsWith(".jsonl")) {
|
|
2267
|
-
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path:
|
|
2449
|
+
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join10(dir, name) });
|
|
2268
2450
|
} else if (e.isDirectory()) {
|
|
2269
|
-
walk(
|
|
2451
|
+
walk(join10(dir, name), depth + 1);
|
|
2270
2452
|
}
|
|
2271
2453
|
}
|
|
2272
2454
|
};
|
|
@@ -2276,8 +2458,8 @@ function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
|
2276
2458
|
function cmdInit(cfg) {
|
|
2277
2459
|
const git = gitStoreFor(cfg);
|
|
2278
2460
|
git.init();
|
|
2279
|
-
const configPath =
|
|
2280
|
-
if (!
|
|
2461
|
+
const configPath = join10(cfg.storeDir, "config.json");
|
|
2462
|
+
if (!existsSync16(configPath)) {
|
|
2281
2463
|
writeFileSync9(
|
|
2282
2464
|
configPath,
|
|
2283
2465
|
`${JSON.stringify({ insights: cfg.insights, batch: cfg.batch, hygiene: cfg.hygiene }, null, 2)}
|
|
@@ -2291,7 +2473,7 @@ function cmdStatus(cfg) {
|
|
|
2291
2473
|
const state = new StateStore(cfg.statePath);
|
|
2292
2474
|
const sessions = Object.keys(state.all());
|
|
2293
2475
|
const stored = listSessionDirs(cfg.storeDir);
|
|
2294
|
-
const socketUp =
|
|
2476
|
+
const socketUp = existsSync16(cfg.socketPath);
|
|
2295
2477
|
const lines = [
|
|
2296
2478
|
`store: ${cfg.storeDir}`,
|
|
2297
2479
|
`host: ${cfg.host}`,
|
|
@@ -2346,6 +2528,20 @@ async function cmdBackfill(cfg, opts = {}) {
|
|
|
2346
2528
|
turns += t;
|
|
2347
2529
|
parts.push(`codex: ${n} sessions / ${t} turns`);
|
|
2348
2530
|
}
|
|
2531
|
+
if (agent === "codebuild" || agent === "all") {
|
|
2532
|
+
const found = discoverCodebuildSessions();
|
|
2533
|
+
let n = 0;
|
|
2534
|
+
let t = 0;
|
|
2535
|
+
for (const info of found) {
|
|
2536
|
+
const imported = parseCodebuildSession(info, cfg.host);
|
|
2537
|
+
if (!imported) continue;
|
|
2538
|
+
t += writeImportedSession(cfg, imported).turns;
|
|
2539
|
+
n++;
|
|
2540
|
+
}
|
|
2541
|
+
sessions += n;
|
|
2542
|
+
turns += t;
|
|
2543
|
+
parts.push(`codebuild: ${n} sessions / ${t} turns`);
|
|
2544
|
+
}
|
|
2349
2545
|
const git = gitStoreFor(cfg);
|
|
2350
2546
|
git.init();
|
|
2351
2547
|
git.commit(`backfill (${agent}): ${sessions} sessions`);
|
|
@@ -2360,7 +2556,7 @@ async function cmdReindex(cfg, opts = {}) {
|
|
|
2360
2556
|
}
|
|
2361
2557
|
function cmdInstallHooks(cfg, opts = {}) {
|
|
2362
2558
|
const home = cfg.claudeProjectsDir.replace(/\/projects\/?$/, "");
|
|
2363
|
-
const settingsPath = opts.settingsPath ??
|
|
2559
|
+
const settingsPath = opts.settingsPath ?? join10(home, "settings.json");
|
|
2364
2560
|
const command = opts.command ?? "code-sessions hook";
|
|
2365
2561
|
const res = installHooks(settingsPath, command);
|
|
2366
2562
|
return {
|
|
@@ -2370,10 +2566,10 @@ function cmdInstallHooks(cfg, opts = {}) {
|
|
|
2370
2566
|
}
|
|
2371
2567
|
function cmdDoctor(cfg) {
|
|
2372
2568
|
const checks = [
|
|
2373
|
-
["store dir exists",
|
|
2374
|
-
["store is git repo",
|
|
2375
|
-
["daemon socket present",
|
|
2376
|
-
["claude projects dir",
|
|
2569
|
+
["store dir exists", existsSync16(cfg.storeDir)],
|
|
2570
|
+
["store is git repo", existsSync16(join10(cfg.storeDir, ".git"))],
|
|
2571
|
+
["daemon socket present", existsSync16(cfg.socketPath)],
|
|
2572
|
+
["claude projects dir", existsSync16(cfg.claudeProjectsDir)]
|
|
2377
2573
|
];
|
|
2378
2574
|
const lines = checks.map(([name, ok]) => `${ok ? "\u2713" : "\u2717"} ${name}`);
|
|
2379
2575
|
const code = checks.every(([, ok]) => ok) ? 0 : 1;
|
|
@@ -2409,6 +2605,24 @@ function cmdFork(cfg, opts) {
|
|
|
2409
2605
|
return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
2410
2606
|
}
|
|
2411
2607
|
}
|
|
2608
|
+
function cmdUsage(cfg, opts = {}) {
|
|
2609
|
+
syncIndex(cfg);
|
|
2610
|
+
const index = new SessionIndex(cfg.indexPath);
|
|
2611
|
+
try {
|
|
2612
|
+
const u = index.usageSummary();
|
|
2613
|
+
if (opts.json) return { code: 0, output: JSON.stringify(u) };
|
|
2614
|
+
const lines = [
|
|
2615
|
+
`# usage \u2014 ${u.totals.sessions} sessions \xB7 ${u.totals.input_tokens.toLocaleString()} in / ${u.totals.output_tokens.toLocaleString()} out \xB7 $${u.totals.cost_usd.toFixed(2)}`,
|
|
2616
|
+
"by agent:",
|
|
2617
|
+
...Object.entries(u.byAgent).map(([a, b]) => ` ${a.padEnd(12)} ${b.sessions} sess $${b.cost_usd.toFixed(2)}`),
|
|
2618
|
+
"top sessions by cost:",
|
|
2619
|
+
...u.topByCost.slice(0, 5).map((t) => ` $${t.cost_usd.toFixed(2).padStart(8)} ${t.agent.padEnd(12)} ${t.label.slice(0, 50)}`)
|
|
2620
|
+
];
|
|
2621
|
+
return { code: 0, output: lines.join("\n") };
|
|
2622
|
+
} finally {
|
|
2623
|
+
index.close();
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2412
2626
|
function cmdInstallSkills(opts = {}) {
|
|
2413
2627
|
const res = installSkills(opts.agent ? { agent: opts.agent } : {});
|
|
2414
2628
|
return { code: 0, output: `Installed cs-label-session skill:
|
|
@@ -2474,9 +2688,9 @@ async function startDaemon(cfg) {
|
|
|
2474
2688
|
}
|
|
2475
2689
|
|
|
2476
2690
|
// src/hooks/shim.ts
|
|
2477
|
-
import { existsSync as
|
|
2691
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2478
2692
|
async function handleHookInput(socketPath, rawInput) {
|
|
2479
|
-
if (!
|
|
2693
|
+
if (!existsSync17(socketPath)) return { ok: false, error: "daemon not running" };
|
|
2480
2694
|
let parsed;
|
|
2481
2695
|
try {
|
|
2482
2696
|
parsed = JSON.parse(rawInput);
|
|
@@ -2560,6 +2774,9 @@ export {
|
|
|
2560
2774
|
codexSessionsRoot,
|
|
2561
2775
|
discoverCodexSessions,
|
|
2562
2776
|
parseCodexSession,
|
|
2777
|
+
codebuildSessionsRoot,
|
|
2778
|
+
discoverCodebuildSessions,
|
|
2779
|
+
parseCodebuildSession,
|
|
2563
2780
|
writeImportedSession,
|
|
2564
2781
|
SessionIndex,
|
|
2565
2782
|
syncIndex,
|
|
@@ -2577,6 +2794,7 @@ export {
|
|
|
2577
2794
|
cmdDoctor,
|
|
2578
2795
|
cmdExport,
|
|
2579
2796
|
cmdFork,
|
|
2797
|
+
cmdUsage,
|
|
2580
2798
|
cmdInstallSkills,
|
|
2581
2799
|
cmdIndex,
|
|
2582
2800
|
cmdQuery,
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
cmdReindex,
|
|
14
14
|
cmdSearch,
|
|
15
15
|
cmdStatus,
|
|
16
|
+
cmdUsage,
|
|
16
17
|
envelopeFile,
|
|
17
18
|
handleHookInput,
|
|
18
19
|
insightsFile,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
parseFlags,
|
|
23
24
|
readStdin,
|
|
24
25
|
startDaemon
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-ON3CPW4C.js";
|
|
26
27
|
|
|
27
28
|
// src/analytics/command.ts
|
|
28
29
|
import { mkdirSync, writeFileSync } from "fs";
|
|
@@ -256,6 +257,9 @@ async function main(argv) {
|
|
|
256
257
|
case "index":
|
|
257
258
|
emit(cmdIndex(cfg));
|
|
258
259
|
break;
|
|
260
|
+
case "usage":
|
|
261
|
+
emit(cmdUsage(cfg, { json: flags.json === true }));
|
|
262
|
+
break;
|
|
259
263
|
case "query":
|
|
260
264
|
emit(
|
|
261
265
|
cmdQuery(cfg, {
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
cmdReindex,
|
|
29
29
|
cmdSearch,
|
|
30
30
|
cmdStatus,
|
|
31
|
+
cmdUsage,
|
|
32
|
+
codebuildSessionsRoot,
|
|
31
33
|
codexSessionsRoot,
|
|
32
34
|
computeEnvelope,
|
|
33
35
|
defaultConfig,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
deriveProjects,
|
|
36
38
|
deriveSignals,
|
|
37
39
|
deriveTags,
|
|
40
|
+
discoverCodebuildSessions,
|
|
38
41
|
discoverCodexSessions,
|
|
39
42
|
discoverGrokSessions,
|
|
40
43
|
envelopeFile,
|
|
@@ -61,6 +64,7 @@ import {
|
|
|
61
64
|
monthOf,
|
|
62
65
|
ollamaRunner,
|
|
63
66
|
overridesFromFlags,
|
|
67
|
+
parseCodebuildSession,
|
|
64
68
|
parseCodexSession,
|
|
65
69
|
parseFlags,
|
|
66
70
|
parseGrokSession,
|
|
@@ -88,7 +92,7 @@ import {
|
|
|
88
92
|
writeBlobFile,
|
|
89
93
|
writeImportedSession,
|
|
90
94
|
writeTurnFile
|
|
91
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-ON3CPW4C.js";
|
|
92
96
|
export {
|
|
93
97
|
CaptureEngine,
|
|
94
98
|
DEFAULT_HOOK_EVENTS,
|
|
@@ -119,6 +123,8 @@ export {
|
|
|
119
123
|
cmdReindex,
|
|
120
124
|
cmdSearch,
|
|
121
125
|
cmdStatus,
|
|
126
|
+
cmdUsage,
|
|
127
|
+
codebuildSessionsRoot,
|
|
122
128
|
codexSessionsRoot,
|
|
123
129
|
computeEnvelope,
|
|
124
130
|
defaultConfig,
|
|
@@ -126,6 +132,7 @@ export {
|
|
|
126
132
|
deriveProjects,
|
|
127
133
|
deriveSignals,
|
|
128
134
|
deriveTags,
|
|
135
|
+
discoverCodebuildSessions,
|
|
129
136
|
discoverCodexSessions,
|
|
130
137
|
discoverGrokSessions,
|
|
131
138
|
envelopeFile,
|
|
@@ -152,6 +159,7 @@ export {
|
|
|
152
159
|
monthOf,
|
|
153
160
|
ollamaRunner,
|
|
154
161
|
overridesFromFlags,
|
|
162
|
+
parseCodebuildSession,
|
|
155
163
|
parseCodexSession,
|
|
156
164
|
parseFlags,
|
|
157
165
|
parseGrokSession,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unpolarize/code-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Headless, event-driven cross-agent session capture agent (daemon + CLI)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"build": "tsup src/index.ts src/cli.ts --format esm --clean --out-dir dist"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@unpolarize/code-sessions-schema": "^0.
|
|
28
|
+
"@unpolarize/code-sessions-schema": "^0.3.0",
|
|
29
29
|
"zod": "^3.23.8"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
4
4
|
import { sessionDir, turnFile, envelopeFile } from '../store/paths';
|
|
5
5
|
import { makeConfig, withTempDir } from '../test/tmp';
|
|
6
6
|
import { discoverCodexSessions, parseCodexSession } from './codex';
|
|
7
|
+
import { discoverCodebuildSessions, parseCodebuildSession } from './codebuild';
|
|
7
8
|
import { discoverGrokSessions, parseGrokSession } from './grok';
|
|
8
9
|
import { writeImportedSession } from './import';
|
|
9
10
|
|
|
@@ -98,6 +99,41 @@ describe('codex adapter', () => {
|
|
|
98
99
|
});
|
|
99
100
|
});
|
|
100
101
|
|
|
102
|
+
function seedCodebuild(root: string): void {
|
|
103
|
+
mkdirSync(root, { recursive: true });
|
|
104
|
+
writeFileSync(
|
|
105
|
+
join(root, 'cb-1.jsonl'),
|
|
106
|
+
[
|
|
107
|
+
'{"type":"meta","meta":{"id":"cb-1","backend":"claude","title":"Plan MVP","cwd":"/Users/x/projects/foo"}}',
|
|
108
|
+
'{"type":"update","update":{"kind":"system_init","backendSessionId":"native-1"}}',
|
|
109
|
+
'{"type":"user","text":"add a feature"}',
|
|
110
|
+
'{"type":"update","update":{"kind":"agent_message_chunk","content":{"type":"text","text":"sure, "}}}',
|
|
111
|
+
'{"type":"update","update":{"kind":"agent_message_chunk","content":{"type":"text","text":"editing"}}}',
|
|
112
|
+
'{"type":"update","update":{"kind":"tool_call","toolCall":{"toolCallId":"t1","title":"Edit","rawInput":{"file_path":"a.ts"}}}}',
|
|
113
|
+
'{"type":"update","update":{"kind":"result","stopReason":"success","usage":{"inputTokens":1000,"outputTokens":40,"costUsd":0.25}}}',
|
|
114
|
+
].join('\n'),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('codebuild adapter', () => {
|
|
119
|
+
it('folds CB stream into user/assistant turns with usage', () => {
|
|
120
|
+
withTempDir((root) => {
|
|
121
|
+
seedCodebuild(root);
|
|
122
|
+
const found = discoverCodebuildSessions(root);
|
|
123
|
+
expect(found).toHaveLength(1);
|
|
124
|
+
const imported = parseCodebuildSession(found[0]!, 'test-host')!;
|
|
125
|
+
expect(imported.agent).toBe('claude-code'); // backend=claude -> claude-code
|
|
126
|
+
expect(imported.sessionId).toBe('cb-1');
|
|
127
|
+
expect(imported.meta.title).toBe('Plan MVP');
|
|
128
|
+
expect(imported.turns.map((t) => t.role)).toEqual(['user', 'assistant']);
|
|
129
|
+
expect(imported.turns[1]!.text).toBe('sure, editing'); // chunks concatenated
|
|
130
|
+
expect(imported.turns[1]!.tool_calls[0]).toMatchObject({ name: 'Edit' });
|
|
131
|
+
expect(imported.turns[1]!.usage.input_tokens).toBe(1000);
|
|
132
|
+
expect(imported.turns[1]!.telemetry?.cost_usd).toBe(0.25);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
101
137
|
describe('writeImportedSession', () => {
|
|
102
138
|
it('writes per-turn files + envelope for an imported session', () => {
|
|
103
139
|
withTempDir((store) => {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
SCHEMA_VERSIONS,
|
|
6
|
+
type AgentKind,
|
|
7
|
+
type ClaudeSessionMeta,
|
|
8
|
+
type ToolCall,
|
|
9
|
+
type Turn,
|
|
10
|
+
} from '@unpolarize/code-sessions-schema';
|
|
11
|
+
import type { ImportedSession } from './import';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Code Build VSCode (CB) adapter. CB stores each chat at
|
|
15
|
+
* ~/.codebuild/sessions/<uuid>.jsonl as a stream of:
|
|
16
|
+
* {type:"meta", meta:{id, backend, title, cwd}}
|
|
17
|
+
* {type:"user", text}
|
|
18
|
+
* {type:"update", update:{kind:"agent_message_chunk", content:{type:"text",text}}}
|
|
19
|
+
* {type:"update", update:{kind:"tool_call", toolCall:{title, rawInput}}}
|
|
20
|
+
* {type:"update", update:{kind:"usage"|"result", usage:{...}}}
|
|
21
|
+
* Events carry no per-line timestamp; synthesized from file mtime + ordinal.
|
|
22
|
+
*
|
|
23
|
+
* Importing CB sessions into the CS store is how "CB context management uses CS":
|
|
24
|
+
* every CB turn becomes discoverable in the shared sessions store + index, and a
|
|
25
|
+
* CB switch/fork can be expressed as a CS forkSession on the persisted session.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export function codebuildSessionsRoot(): string {
|
|
29
|
+
return join(homedir(), '.codebuild', 'sessions');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CodebuildSessionInfo {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
path: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function discoverCodebuildSessions(root = codebuildSessionsRoot()): CodebuildSessionInfo[] {
|
|
38
|
+
if (!existsSync(root)) return [];
|
|
39
|
+
let files: string[];
|
|
40
|
+
try {
|
|
41
|
+
files = readdirSync(root).filter((f) => f.endsWith('.jsonl'));
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return files.map((f) => ({ sessionId: f.replace(/\.jsonl$/, ''), path: join(root, f) }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function backendToAgent(backend: unknown): AgentKind {
|
|
49
|
+
if (backend === 'claude') return 'claude-code';
|
|
50
|
+
if (backend === 'grok') return 'grok';
|
|
51
|
+
if (backend === 'codex') return 'codex';
|
|
52
|
+
return 'unknown';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Pending {
|
|
56
|
+
text: string;
|
|
57
|
+
tools: ToolCall[];
|
|
58
|
+
input_tokens: number;
|
|
59
|
+
output_tokens: number;
|
|
60
|
+
cache_read_tokens: number;
|
|
61
|
+
cost_usd: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseCodebuildSession(info: CodebuildSessionInfo, host: string): ImportedSession | null {
|
|
65
|
+
let raw = '';
|
|
66
|
+
try {
|
|
67
|
+
raw = readFileSync(info.path, 'utf8');
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
72
|
+
const baseMs = statMtime(info.path);
|
|
73
|
+
|
|
74
|
+
let agent: AgentKind = 'unknown';
|
|
75
|
+
let sessionId = info.sessionId;
|
|
76
|
+
let title: string | undefined;
|
|
77
|
+
let cwd: string | undefined;
|
|
78
|
+
|
|
79
|
+
const turns: Turn[] = [];
|
|
80
|
+
let pending: Pending | null = null;
|
|
81
|
+
let idx = 0;
|
|
82
|
+
|
|
83
|
+
const flush = (): void => {
|
|
84
|
+
if (!pending) return;
|
|
85
|
+
const p = pending;
|
|
86
|
+
pending = null;
|
|
87
|
+
const turn = mkTurn(sessionId, host, agent, idx++, baseMs, 'assistant', p.text, p.tools);
|
|
88
|
+
turn.usage = {
|
|
89
|
+
input_tokens: p.input_tokens,
|
|
90
|
+
output_tokens: p.output_tokens,
|
|
91
|
+
cache_read_tokens: p.cache_read_tokens,
|
|
92
|
+
cache_write_tokens: 0,
|
|
93
|
+
};
|
|
94
|
+
if (p.cost_usd > 0) turn.telemetry = { cost_usd: p.cost_usd };
|
|
95
|
+
turns.push(turn);
|
|
96
|
+
};
|
|
97
|
+
const ensurePending = (): Pending => {
|
|
98
|
+
if (!pending) pending = { text: '', tools: [], input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cost_usd: 0 };
|
|
99
|
+
return pending;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
let ev: any;
|
|
104
|
+
try {
|
|
105
|
+
ev = JSON.parse(line);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ev.type === 'meta' && ev.meta) {
|
|
110
|
+
agent = backendToAgent(ev.meta.backend);
|
|
111
|
+
if (typeof ev.meta.id === 'string') sessionId = ev.meta.id;
|
|
112
|
+
if (typeof ev.meta.title === 'string') title = ev.meta.title;
|
|
113
|
+
if (typeof ev.meta.cwd === 'string') cwd = ev.meta.cwd;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ev.type === 'user' && typeof ev.text === 'string') {
|
|
117
|
+
flush();
|
|
118
|
+
turns.push(mkTurn(sessionId, host, agent, idx++, baseMs, 'user', ev.text, []));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ev.type === 'update' && ev.update) {
|
|
122
|
+
const u = ev.update;
|
|
123
|
+
if (u.kind === 'agent_message_chunk') {
|
|
124
|
+
const t = u.content?.text;
|
|
125
|
+
if (typeof t === 'string') ensurePending().text += t;
|
|
126
|
+
} else if (u.kind === 'tool_call' && u.toolCall) {
|
|
127
|
+
const p = ensurePending();
|
|
128
|
+
p.tools.push({ name: String(u.toolCall.title ?? u.toolCall.kind ?? 'tool'), input: u.toolCall.rawInput });
|
|
129
|
+
} else if (u.kind === 'usage' && u.usage) {
|
|
130
|
+
const p = ensurePending();
|
|
131
|
+
p.input_tokens += Number(u.usage.inputTokens) || 0;
|
|
132
|
+
p.output_tokens += Number(u.usage.outputTokens) || 0;
|
|
133
|
+
p.cache_read_tokens += Number(u.usage.cacheReadTokens) || 0;
|
|
134
|
+
} else if (u.kind === 'result' && u.usage) {
|
|
135
|
+
const p = ensurePending();
|
|
136
|
+
p.input_tokens += Number(u.usage.inputTokens) || 0;
|
|
137
|
+
p.output_tokens += Number(u.usage.outputTokens) || 0;
|
|
138
|
+
p.cost_usd += Number(u.usage.costUsd) || 0;
|
|
139
|
+
flush();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
flush();
|
|
144
|
+
|
|
145
|
+
if (turns.length === 0) return null;
|
|
146
|
+
const meta: ClaudeSessionMeta = {
|
|
147
|
+
session_id: sessionId,
|
|
148
|
+
started_at: turns[0]!.ts,
|
|
149
|
+
ended_at: turns[turns.length - 1]!.ts,
|
|
150
|
+
};
|
|
151
|
+
if (cwd) meta.project_path = cwd;
|
|
152
|
+
if (title) meta.title = title;
|
|
153
|
+
return { host, sessionId, agent, turns, meta };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function statMtime(p: string): number {
|
|
157
|
+
try {
|
|
158
|
+
return statSync(p).mtimeMs;
|
|
159
|
+
} catch {
|
|
160
|
+
return Date.parse('2020-01-01T00:00:00Z');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mkTurn(
|
|
165
|
+
sessionId: string,
|
|
166
|
+
host: string,
|
|
167
|
+
agent: AgentKind,
|
|
168
|
+
index: number,
|
|
169
|
+
baseMs: number,
|
|
170
|
+
role: Turn['role'],
|
|
171
|
+
text: string,
|
|
172
|
+
tool_calls: ToolCall[],
|
|
173
|
+
): Turn {
|
|
174
|
+
return {
|
|
175
|
+
schema: SCHEMA_VERSIONS.turn,
|
|
176
|
+
session_id: sessionId,
|
|
177
|
+
host,
|
|
178
|
+
agent,
|
|
179
|
+
turn_index: index,
|
|
180
|
+
ts: new Date(baseMs + index * 1000).toISOString(),
|
|
181
|
+
role,
|
|
182
|
+
text,
|
|
183
|
+
tool_calls,
|
|
184
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
|
|
185
|
+
scrubbed: false,
|
|
186
|
+
raw_ref: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/adapters/index.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
cmdReindex,
|
|
14
14
|
cmdSearch,
|
|
15
15
|
cmdStatus,
|
|
16
|
+
cmdUsage,
|
|
16
17
|
startDaemon,
|
|
17
18
|
type CommandResult,
|
|
18
19
|
} from './commands';
|
|
@@ -54,7 +55,7 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
54
55
|
emit(
|
|
55
56
|
await cmdBackfill(cfg, {
|
|
56
57
|
...(typeof flags.projects === 'string' ? { projectsDir: flags.projects } : {}),
|
|
57
|
-
...(typeof flags.agent === 'string' ? { agent: flags.agent as 'claude' | 'grok' | 'codex' | 'all' } : {}),
|
|
58
|
+
...(typeof flags.agent === 'string' ? { agent: flags.agent as 'claude' | 'grok' | 'codex' | 'codebuild' | 'all' } : {}),
|
|
58
59
|
}),
|
|
59
60
|
);
|
|
60
61
|
break;
|
|
@@ -70,6 +71,9 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
70
71
|
case 'index':
|
|
71
72
|
emit(cmdIndex(cfg));
|
|
72
73
|
break;
|
|
74
|
+
case 'usage':
|
|
75
|
+
emit(cmdUsage(cfg, { json: flags.json === true }));
|
|
76
|
+
break;
|
|
73
77
|
case 'query':
|
|
74
78
|
emit(
|
|
75
79
|
cmdQuery(cfg, {
|
package/src/cliargs.ts
CHANGED
|
@@ -53,11 +53,12 @@ Commands:
|
|
|
53
53
|
install-hooks Install Claude Code hooks that feed the daemon
|
|
54
54
|
install-skills Install the cs-label-session skill into agents [--agent claude|codex|grok|all]
|
|
55
55
|
hook (internal) forward a hook payload from stdin to the daemon
|
|
56
|
-
backfill Import existing transcripts into the store [--agent claude|grok|codex|all]
|
|
56
|
+
backfill Import existing transcripts into the store [--agent claude|grok|codex|codebuild|all]
|
|
57
57
|
reindex (Re)derive insights for stored sessions [--since YYYY-MM]
|
|
58
58
|
export Export stored sessions as OTLP to a collector [--since YYYY-MM]
|
|
59
59
|
index (Re)build the internal SQLite index from the git store
|
|
60
60
|
query List recent sessions from the index [--limit N] [--agent X]
|
|
61
|
+
usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
|
|
61
62
|
search Full-text search session turns <text> [--limit N]
|
|
62
63
|
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
63
64
|
analytics Compute MVP-2 rollups + digest into analytics/
|
package/src/commands.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { installHooks } from './hooks/install';
|
|
|
12
12
|
import { exportSession, exportStore } from './telemetry/exporter';
|
|
13
13
|
import { discoverGrokSessions, parseGrokSession } from './adapters/grok';
|
|
14
14
|
import { discoverCodexSessions, parseCodexSession } from './adapters/codex';
|
|
15
|
+
import { discoverCodebuildSessions, parseCodebuildSession } from './adapters/codebuild';
|
|
15
16
|
import { writeImportedSession } from './adapters/import';
|
|
16
17
|
import { SessionIndex, type SessionIndexRow } from './index_store/db';
|
|
17
18
|
import { syncIndex } from './index_store/sync';
|
|
@@ -82,7 +83,7 @@ export function cmdStatus(cfg: CodeSessionsConfig): CommandResult {
|
|
|
82
83
|
return { code: 0, output: lines.join('\n') };
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
export type BackfillAgent = 'claude' | 'grok' | 'codex' | 'all';
|
|
86
|
+
export type BackfillAgent = 'claude' | 'grok' | 'codex' | 'codebuild' | 'all';
|
|
86
87
|
|
|
87
88
|
export async function cmdBackfill(
|
|
88
89
|
cfg: CodeSessionsConfig,
|
|
@@ -134,6 +135,21 @@ export async function cmdBackfill(
|
|
|
134
135
|
parts.push(`codex: ${n} sessions / ${t} turns`);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
if (agent === 'codebuild' || agent === 'all') {
|
|
139
|
+
const found = discoverCodebuildSessions();
|
|
140
|
+
let n = 0;
|
|
141
|
+
let t = 0;
|
|
142
|
+
for (const info of found) {
|
|
143
|
+
const imported = parseCodebuildSession(info, cfg.host);
|
|
144
|
+
if (!imported) continue;
|
|
145
|
+
t += writeImportedSession(cfg, imported).turns;
|
|
146
|
+
n++;
|
|
147
|
+
}
|
|
148
|
+
sessions += n;
|
|
149
|
+
turns += t;
|
|
150
|
+
parts.push(`codebuild: ${n} sessions / ${t} turns`);
|
|
151
|
+
}
|
|
152
|
+
|
|
137
153
|
const git = gitStoreFor(cfg);
|
|
138
154
|
git.init();
|
|
139
155
|
git.commit(`backfill (${agent}): ${sessions} sessions`);
|
|
@@ -218,6 +234,26 @@ export function cmdFork(
|
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
/** Aggregated usage from the CS index (totals/byAgent/byDay/byProject/topByCost). */
|
|
238
|
+
export function cmdUsage(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
|
|
239
|
+
syncIndex(cfg); // ensure the index reflects the current store
|
|
240
|
+
const index = new SessionIndex(cfg.indexPath);
|
|
241
|
+
try {
|
|
242
|
+
const u = index.usageSummary();
|
|
243
|
+
if (opts.json) return { code: 0, output: JSON.stringify(u) };
|
|
244
|
+
const lines = [
|
|
245
|
+
`# usage — ${u.totals.sessions} sessions · ${u.totals.input_tokens.toLocaleString()} in / ${u.totals.output_tokens.toLocaleString()} out · $${u.totals.cost_usd.toFixed(2)}`,
|
|
246
|
+
'by agent:',
|
|
247
|
+
...Object.entries(u.byAgent).map(([a, b]) => ` ${a.padEnd(12)} ${b.sessions} sess $${b.cost_usd.toFixed(2)}`),
|
|
248
|
+
'top sessions by cost:',
|
|
249
|
+
...u.topByCost.slice(0, 5).map((t) => ` $${t.cost_usd.toFixed(2).padStart(8)} ${t.agent.padEnd(12)} ${t.label.slice(0, 50)}`),
|
|
250
|
+
];
|
|
251
|
+
return { code: 0, output: lines.join('\n') };
|
|
252
|
+
} finally {
|
|
253
|
+
index.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
221
257
|
export function cmdInstallSkills(opts: { agent?: SkillAgent } = {}): CommandResult {
|
|
222
258
|
const res = installSkills(opts.agent ? { agent: opts.agent } : {});
|
|
223
259
|
return { code: 0, output: `Installed cs-label-session skill:\n ${res.installed.join('\n ')}` };
|
|
@@ -91,6 +91,31 @@ describe('SessionIndex', () => {
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('aggregates usage by agent, day, project, and top-cost', () => {
|
|
95
|
+
const idx = new SessionIndex(':memory:');
|
|
96
|
+
try {
|
|
97
|
+
idx.upsertSession(env('c1', 'claude-code', { totals: { input_tokens: 100, output_tokens: 20, cost_usd: 2 } }), {
|
|
98
|
+
...src,
|
|
99
|
+
projects: ['foo'],
|
|
100
|
+
});
|
|
101
|
+
idx.upsertSession(env('g1', 'grok', { totals: { input_tokens: 50, output_tokens: 5, cost_usd: 0.5 } }), {
|
|
102
|
+
...src,
|
|
103
|
+
source_path: '/s/g.json',
|
|
104
|
+
projects: ['foo', 'bar'],
|
|
105
|
+
});
|
|
106
|
+
const u = idx.usageSummary();
|
|
107
|
+
expect(u.totals.sessions).toBe(2);
|
|
108
|
+
expect(u.totals.cost_usd).toBe(2.5);
|
|
109
|
+
expect(u.byAgent['claude-code']!.cost_usd).toBe(2);
|
|
110
|
+
expect(u.byProject['foo']!.sessions).toBe(2);
|
|
111
|
+
expect(u.byProject['bar']!.sessions).toBe(1);
|
|
112
|
+
expect(u.byDay[0]!.day).toBe('2026-06-20');
|
|
113
|
+
expect(u.topByCost[0]!.session_id).toBe('c1'); // highest cost first
|
|
114
|
+
} finally {
|
|
115
|
+
idx.close();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
94
119
|
it('filters by agent and deletes sessions (cascade turns)', () => {
|
|
95
120
|
const idx = new SessionIndex(':memory:');
|
|
96
121
|
try {
|
package/src/index_store/db.ts
CHANGED
|
@@ -44,12 +44,31 @@ export interface SessionIndexRow {
|
|
|
44
44
|
source_path: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface UsageBucket {
|
|
48
|
+
sessions: number;
|
|
49
|
+
input_tokens: number;
|
|
50
|
+
output_tokens: number;
|
|
51
|
+
cost_usd: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UsageSummary {
|
|
55
|
+
totals: { sessions: number; input_tokens: number; output_tokens: number; cost_usd: number };
|
|
56
|
+
byAgent: Record<string, UsageBucket>;
|
|
57
|
+
byDay: Array<{ day: string } & UsageBucket>;
|
|
58
|
+
byProject: Record<string, UsageBucket>;
|
|
59
|
+
topByCost: Array<{ session_id: string; agent: string; cost_usd: number; label: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function toMs(iso: string | undefined): number | null {
|
|
48
63
|
if (!iso) return null;
|
|
49
64
|
const v = Date.parse(iso);
|
|
50
65
|
return Number.isNaN(v) ? null : v;
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
function round6(n: number): number {
|
|
69
|
+
return Math.round(n * 1e6) / 1e6;
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
export class SessionIndex {
|
|
54
73
|
readonly db: DatabaseSyncT;
|
|
55
74
|
|
|
@@ -289,6 +308,59 @@ export class SessionIndex {
|
|
|
289
308
|
return (rows as any[]).map((r) => this.rowToIndex(r));
|
|
290
309
|
}
|
|
291
310
|
|
|
311
|
+
/** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
|
|
312
|
+
usageSummary(opts: { days?: number; topN?: number } = {}): UsageSummary {
|
|
313
|
+
const rows = this.db
|
|
314
|
+
.prepare(
|
|
315
|
+
'SELECT session_id, agent, started_at, input_tokens, output_tokens, cost_usd, projects_json, topic, title FROM session',
|
|
316
|
+
)
|
|
317
|
+
.all() as any[];
|
|
318
|
+
const totals = { sessions: rows.length, input_tokens: 0, output_tokens: 0, cost_usd: 0 };
|
|
319
|
+
const byAgent: Record<string, UsageBucket> = {};
|
|
320
|
+
const byDay: Record<string, UsageBucket> = {};
|
|
321
|
+
const byProject: Record<string, UsageBucket> = {};
|
|
322
|
+
const add = (m: Record<string, UsageBucket>, key: string, r: any) => {
|
|
323
|
+
const b = (m[key] ??= { sessions: 0, input_tokens: 0, output_tokens: 0, cost_usd: 0 });
|
|
324
|
+
b.sessions++;
|
|
325
|
+
b.input_tokens += r.input_tokens;
|
|
326
|
+
b.output_tokens += r.output_tokens;
|
|
327
|
+
b.cost_usd += r.cost_usd;
|
|
328
|
+
};
|
|
329
|
+
for (const r of rows) {
|
|
330
|
+
totals.input_tokens += r.input_tokens;
|
|
331
|
+
totals.output_tokens += r.output_tokens;
|
|
332
|
+
totals.cost_usd += r.cost_usd;
|
|
333
|
+
add(byAgent, r.agent || 'unknown', r);
|
|
334
|
+
if (r.started_at) add(byDay, new Date(r.started_at).toISOString().slice(0, 10), r);
|
|
335
|
+
for (const p of safeJson(r.projects_json)) add(byProject, p, r);
|
|
336
|
+
}
|
|
337
|
+
totals.cost_usd = round6(totals.cost_usd);
|
|
338
|
+
for (const m of [byAgent, byDay, byProject]) for (const b of Object.values(m)) b.cost_usd = round6(b.cost_usd);
|
|
339
|
+
|
|
340
|
+
const days = opts.days ?? 30;
|
|
341
|
+
const recentDays = Object.entries(byDay)
|
|
342
|
+
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
|
|
343
|
+
.slice(0, days)
|
|
344
|
+
.map(([day, b]) => ({ day, ...b }));
|
|
345
|
+
const topByCost = rows
|
|
346
|
+
.map((r) => ({
|
|
347
|
+
session_id: r.session_id,
|
|
348
|
+
agent: r.agent,
|
|
349
|
+
cost_usd: round6(r.cost_usd),
|
|
350
|
+
label: r.topic || r.title || r.session_id,
|
|
351
|
+
}))
|
|
352
|
+
.sort((a, b) => b.cost_usd - a.cost_usd)
|
|
353
|
+
.slice(0, opts.topN ?? 10);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
totals,
|
|
357
|
+
byAgent,
|
|
358
|
+
byDay: recentDays,
|
|
359
|
+
byProject,
|
|
360
|
+
topByCost,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
292
364
|
stats(): { sessions: number; turns: number; cost_usd: number; byAgent: Record<string, number> } {
|
|
293
365
|
const s = this.db.prepare('SELECT COUNT(*) c, COALESCE(SUM(cost_usd),0) cost FROM session').get() as {
|
|
294
366
|
c: number;
|