@unpolarize/code-sessions 0.1.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-ZJG2DWAK.js → chunk-ON3CPW4C.js} +529 -45
- package/dist/cli.js +21 -1
- package/dist/index.js +29 -1
- package/package.json +15 -5
- 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 +21 -1
- package/src/cliargs.ts +4 -1
- package/src/commands.ts +71 -3
- package/src/fork.test.ts +80 -0
- package/src/fork.ts +91 -0
- package/src/index.ts +2 -0
- package/src/index_store/db.test.ts +25 -0
- package/src/index_store/db.ts +111 -8
- package/src/index_store/sync.ts +6 -0
- package/src/insights/heuristics.test.ts +23 -1
- package/src/insights/heuristics.ts +48 -1
- package/src/insights/labeler.test.ts +4 -1
- package/src/insights/labeler.ts +18 -5
- package/src/insights/llm.test.ts +1 -1
- package/src/insights/llm.ts +13 -5
- package/src/insights/provider.ts +7 -2
- package/src/skills/index.ts +2 -0
- package/src/skills/install.ts +52 -0
- package/src/skills/skills.test.ts +42 -0
- package/src/skills/templates.ts +48 -0
|
@@ -133,13 +133,16 @@ Commands:
|
|
|
133
133
|
init Initialize the git-backed store (~/.sessions)
|
|
134
134
|
start Run the capture daemon (foreground)
|
|
135
135
|
install-hooks Install Claude Code hooks that feed the daemon
|
|
136
|
+
install-skills Install the cs-label-session skill into agents [--agent claude|codex|grok|all]
|
|
136
137
|
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
|
+
backfill Import existing transcripts into the store [--agent claude|grok|codex|codebuild|all]
|
|
138
139
|
reindex (Re)derive insights for stored sessions [--since YYYY-MM]
|
|
139
140
|
export Export stored sessions as OTLP to a collector [--since YYYY-MM]
|
|
140
141
|
index (Re)build the internal SQLite index from the git store
|
|
141
142
|
query List recent sessions from the index [--limit N] [--agent X]
|
|
143
|
+
usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
|
|
142
144
|
search Full-text search session turns <text> [--limit N]
|
|
145
|
+
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
143
146
|
analytics Compute MVP-2 rollups + digest into analytics/
|
|
144
147
|
status Show daemon/store status
|
|
145
148
|
doctor Environment checks
|
|
@@ -973,6 +976,45 @@ function deriveTags(turns) {
|
|
|
973
976
|
for (const t of turns) for (const c of t.tool_calls) tags.add(c.name);
|
|
974
977
|
return [...tags].slice(0, 12);
|
|
975
978
|
}
|
|
979
|
+
function projectIdFromPath(p) {
|
|
980
|
+
const segs = p.split("/").filter(Boolean);
|
|
981
|
+
const i = segs.indexOf("projects");
|
|
982
|
+
if (i >= 0 && segs[i + 1] === "ai" && segs[i + 2]) return `ai/${segs[i + 2]}`;
|
|
983
|
+
if (i >= 0 && segs[i + 1]) return segs[i + 1];
|
|
984
|
+
if (segs.includes("docs")) return "docs";
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function deriveProjects(turns) {
|
|
988
|
+
const set = /* @__PURE__ */ new Set();
|
|
989
|
+
for (const t of turns) {
|
|
990
|
+
for (const c of t.tool_calls) {
|
|
991
|
+
const fp = c.input?.file_path ?? c.input?.path;
|
|
992
|
+
if (typeof fp === "string") {
|
|
993
|
+
const id = projectIdFromPath(fp);
|
|
994
|
+
if (id) set.add(id);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return [...set].sort().slice(0, 12);
|
|
999
|
+
}
|
|
1000
|
+
var INTENT_PATTERNS = [
|
|
1001
|
+
["bugfix", /\b(fix|bug|broken|error|crash|regression|failing|stack ?trace)\b/i],
|
|
1002
|
+
["feature", /\b(add|implement|build|create|feature|support|introduce|new )\b/i],
|
|
1003
|
+
["refactor", /\b(refactor|clean ?up|simplify|rename|restructure|extract|dedupe)\b/i],
|
|
1004
|
+
["research", /\b(research|investigate|explore|compare|evaluate|find out|how (do|does|to)|why)\b/i],
|
|
1005
|
+
["docs", /\b(document|docs|readme|write[ -]?up|notes|comment)\b/i],
|
|
1006
|
+
["review", /\b(review|audit|critique|check|inspect)\b/i],
|
|
1007
|
+
["ops", /\b(deploy|release|publish|install|configure|ci\/?cd|pipeline|infra)\b/i]
|
|
1008
|
+
];
|
|
1009
|
+
function deriveIntent(turns) {
|
|
1010
|
+
const firstUser = turns.find((t) => t.role === "user" && t.text.trim().length > 0);
|
|
1011
|
+
if (!firstUser) return void 0;
|
|
1012
|
+
const text = firstUser.text;
|
|
1013
|
+
for (const [intent, re] of INTENT_PATTERNS) {
|
|
1014
|
+
if (re.test(text)) return intent;
|
|
1015
|
+
}
|
|
1016
|
+
return "other";
|
|
1017
|
+
}
|
|
976
1018
|
|
|
977
1019
|
// src/insights/provider.ts
|
|
978
1020
|
var FakeProvider = class {
|
|
@@ -980,10 +1022,13 @@ var FakeProvider = class {
|
|
|
980
1022
|
async label(req) {
|
|
981
1023
|
const result = {
|
|
982
1024
|
tags: deriveTags(req.turns),
|
|
1025
|
+
projects: deriveProjects(req.turns),
|
|
983
1026
|
signals: []
|
|
984
1027
|
};
|
|
985
1028
|
const topic = guessTopic(req.turns);
|
|
986
1029
|
if (topic) result.topic = topic;
|
|
1030
|
+
const intent = deriveIntent(req.turns);
|
|
1031
|
+
if (intent) result.intent = intent;
|
|
987
1032
|
const assistantText = req.turns.find((t) => t.role === "assistant")?.text;
|
|
988
1033
|
if (assistantText) result.summary = assistantText.slice(0, 160);
|
|
989
1034
|
return result;
|
|
@@ -992,7 +1037,7 @@ var FakeProvider = class {
|
|
|
992
1037
|
|
|
993
1038
|
// src/insights/llm.ts
|
|
994
1039
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
995
|
-
import { SIGNAL_KINDS } from "@unpolarize/code-sessions-schema";
|
|
1040
|
+
import { INTENTS, SIGNAL_KINDS } from "@unpolarize/code-sessions-schema";
|
|
996
1041
|
var MAX_HEAD = 40;
|
|
997
1042
|
var MAX_TAIL = 10;
|
|
998
1043
|
var MAX_TURN_CHARS = 400;
|
|
@@ -1006,8 +1051,8 @@ function buildPrompt(req) {
|
|
|
1006
1051
|
}).join("\n");
|
|
1007
1052
|
return [
|
|
1008
1053
|
"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.",
|
|
1054
|
+
'{"topic": string, "intent": one of ' + INTENTS.join("|") + ', "tags": string[], "projects": string[], "summary": string, "signals": [{"kind": one of ' + SIGNAL_KINDS.join("|") + ', "severity": "info"|"warn"|"critical", "note": string}]}',
|
|
1055
|
+
"topic: 3-6 words. intent: what the user wanted. tags: tools/themes. projects: repo/dir names touched. summary: <=1 sentence. signals: only notable ones.",
|
|
1011
1056
|
"",
|
|
1012
1057
|
"Transcript:",
|
|
1013
1058
|
transcript
|
|
@@ -1017,19 +1062,23 @@ var KIND_SET = new Set(SIGNAL_KINDS);
|
|
|
1017
1062
|
function parseLabelJson(out) {
|
|
1018
1063
|
const start = out.indexOf("{");
|
|
1019
1064
|
const end = out.lastIndexOf("}");
|
|
1020
|
-
if (start < 0 || end <= start) return { tags: [], signals: [] };
|
|
1065
|
+
if (start < 0 || end <= start) return { tags: [], projects: [], signals: [] };
|
|
1021
1066
|
let obj;
|
|
1022
1067
|
try {
|
|
1023
1068
|
obj = JSON.parse(out.slice(start, end + 1));
|
|
1024
1069
|
} catch {
|
|
1025
|
-
return { tags: [], signals: [] };
|
|
1070
|
+
return { tags: [], projects: [], signals: [] };
|
|
1026
1071
|
}
|
|
1027
1072
|
const result = {
|
|
1028
1073
|
tags: Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === "string") : [],
|
|
1074
|
+
projects: Array.isArray(obj.projects) ? obj.projects.filter((t) => typeof t === "string") : [],
|
|
1029
1075
|
signals: coerceSignals(obj.signals)
|
|
1030
1076
|
};
|
|
1031
1077
|
if (typeof obj.topic === "string") result.topic = obj.topic;
|
|
1032
1078
|
if (typeof obj.summary === "string") result.summary = obj.summary;
|
|
1079
|
+
if (typeof obj.intent === "string" && INTENTS.includes(obj.intent)) {
|
|
1080
|
+
result.intent = obj.intent;
|
|
1081
|
+
}
|
|
1033
1082
|
return result;
|
|
1034
1083
|
}
|
|
1035
1084
|
function coerceSignals(raw) {
|
|
@@ -1130,13 +1179,15 @@ async function labelSession(sessionDir2, identity, provider, opts = {}) {
|
|
|
1130
1179
|
const turns = readTurns(sessionDir2);
|
|
1131
1180
|
if (turns.length === 0) return void 0;
|
|
1132
1181
|
const heuristicSignals = deriveSignals(turns);
|
|
1133
|
-
let provided = { tags: [], signals: [] };
|
|
1182
|
+
let provided = { tags: [], projects: [], signals: [] };
|
|
1134
1183
|
try {
|
|
1135
1184
|
provided = await provider.label({ sessionId: identity.sessionId, host: identity.host, turns });
|
|
1136
1185
|
} catch {
|
|
1137
1186
|
}
|
|
1138
1187
|
const topic = provided.topic ?? guessTopic(turns);
|
|
1188
|
+
const intent = provided.intent ?? deriveIntent(turns);
|
|
1139
1189
|
const tags = [.../* @__PURE__ */ new Set([...provided.tags, ...deriveTags(turns)])].slice(0, 16);
|
|
1190
|
+
const projects = [.../* @__PURE__ */ new Set([...provided.projects, ...deriveProjects(turns)])].slice(0, 16);
|
|
1140
1191
|
const signals = dedupeSignals([...heuristicSignals, ...provided.signals]);
|
|
1141
1192
|
const insights = {
|
|
1142
1193
|
schema: SCHEMA_VERSIONS2.insights,
|
|
@@ -1145,15 +1196,17 @@ async function labelSession(sessionDir2, identity, provider, opts = {}) {
|
|
|
1145
1196
|
generated_at: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1146
1197
|
provider: provider.name,
|
|
1147
1198
|
tags,
|
|
1199
|
+
projects,
|
|
1148
1200
|
signals
|
|
1149
1201
|
};
|
|
1150
1202
|
if (topic) insights.topic = topic;
|
|
1203
|
+
if (intent) insights.intent = intent;
|
|
1151
1204
|
if (provided.summary) insights.summary = provided.summary;
|
|
1152
1205
|
writeJsonAtomic2(insightsFile(sessionDir2), insights);
|
|
1153
|
-
updateEnvelopeLabels(sessionDir2, topic,
|
|
1206
|
+
updateEnvelopeLabels(sessionDir2, { topic, intent, projects });
|
|
1154
1207
|
return insights;
|
|
1155
1208
|
}
|
|
1156
|
-
function updateEnvelopeLabels(sessionDir2,
|
|
1209
|
+
function updateEnvelopeLabels(sessionDir2, l) {
|
|
1157
1210
|
const path = envelopeFile(sessionDir2);
|
|
1158
1211
|
if (!existsSync7(path)) return;
|
|
1159
1212
|
let env;
|
|
@@ -1162,7 +1215,13 @@ function updateEnvelopeLabels(sessionDir2, topic, tags) {
|
|
|
1162
1215
|
} catch {
|
|
1163
1216
|
return;
|
|
1164
1217
|
}
|
|
1165
|
-
env.labels = [
|
|
1218
|
+
env.labels = [
|
|
1219
|
+
.../* @__PURE__ */ new Set([
|
|
1220
|
+
...l.intent ? [`intent:${l.intent}`] : [],
|
|
1221
|
+
...l.topic ? [l.topic] : [],
|
|
1222
|
+
...l.projects.map((p) => `project:${p}`)
|
|
1223
|
+
])
|
|
1224
|
+
].slice(0, 16);
|
|
1166
1225
|
writeJsonAtomic2(path, env);
|
|
1167
1226
|
}
|
|
1168
1227
|
async function reindexStore(cfg, provider, opts = {}) {
|
|
@@ -1703,8 +1762,144 @@ function parseCodexSession(info, host) {
|
|
|
1703
1762
|
return { host, sessionId, agent: "codex", turns, meta };
|
|
1704
1763
|
}
|
|
1705
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
|
+
|
|
1706
1901
|
// src/adapters/import.ts
|
|
1707
|
-
import { existsSync as
|
|
1902
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
1708
1903
|
import { dirname as dirname5 } from "path";
|
|
1709
1904
|
var NATIVE_FORMAT = {
|
|
1710
1905
|
"claude-code": "claude-jsonl",
|
|
@@ -1735,9 +1930,9 @@ function writeImportedSession(cfg, s) {
|
|
|
1735
1930
|
});
|
|
1736
1931
|
env.native_ref.format = NATIVE_FORMAT[s.agent] ?? "unknown";
|
|
1737
1932
|
const envPath = envelopeFile(dir);
|
|
1738
|
-
if (
|
|
1933
|
+
if (existsSync13(envPath)) {
|
|
1739
1934
|
try {
|
|
1740
|
-
const prev = JSON.parse(
|
|
1935
|
+
const prev = JSON.parse(readFileSync11(envPath, "utf8"));
|
|
1741
1936
|
if (prev.labels?.length) env.labels = prev.labels;
|
|
1742
1937
|
} catch {
|
|
1743
1938
|
}
|
|
@@ -1752,12 +1947,15 @@ import { createRequire } from "module";
|
|
|
1752
1947
|
import { dirname as dirname6 } from "path";
|
|
1753
1948
|
var nodeRequire = createRequire(import.meta.url);
|
|
1754
1949
|
var { DatabaseSync } = nodeRequire("node:sqlite");
|
|
1755
|
-
var SCHEMA_VERSION =
|
|
1950
|
+
var SCHEMA_VERSION = 2;
|
|
1756
1951
|
function toMs(iso) {
|
|
1757
1952
|
if (!iso) return null;
|
|
1758
1953
|
const v = Date.parse(iso);
|
|
1759
1954
|
return Number.isNaN(v) ? null : v;
|
|
1760
1955
|
}
|
|
1956
|
+
function round6(n) {
|
|
1957
|
+
return Math.round(n * 1e6) / 1e6;
|
|
1958
|
+
}
|
|
1761
1959
|
var SessionIndex = class {
|
|
1762
1960
|
db;
|
|
1763
1961
|
constructor(path) {
|
|
@@ -1768,7 +1966,8 @@ var SessionIndex = class {
|
|
|
1768
1966
|
}
|
|
1769
1967
|
migrate() {
|
|
1770
1968
|
const row = this.db.prepare("PRAGMA user_version").get();
|
|
1771
|
-
|
|
1969
|
+
const cur = row?.user_version ?? 0;
|
|
1970
|
+
if (cur < 1) {
|
|
1772
1971
|
this.db.exec(`
|
|
1773
1972
|
CREATE TABLE IF NOT EXISTS session (
|
|
1774
1973
|
session_id TEXT PRIMARY KEY,
|
|
@@ -1786,6 +1985,8 @@ var SessionIndex = class {
|
|
|
1786
1985
|
title TEXT,
|
|
1787
1986
|
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
1788
1987
|
topic TEXT,
|
|
1988
|
+
intent TEXT,
|
|
1989
|
+
projects_json TEXT NOT NULL DEFAULT '[]',
|
|
1789
1990
|
source_path TEXT NOT NULL,
|
|
1790
1991
|
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
1791
1992
|
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
@@ -1809,13 +2010,23 @@ var SessionIndex = class {
|
|
|
1809
2010
|
CREATE TABLE IF NOT EXISTS insight (
|
|
1810
2011
|
session_id TEXT PRIMARY KEY REFERENCES session(session_id) ON DELETE CASCADE,
|
|
1811
2012
|
topic TEXT,
|
|
2013
|
+
intent TEXT,
|
|
1812
2014
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
2015
|
+
projects_json TEXT NOT NULL DEFAULT '[]',
|
|
1813
2016
|
signals_json TEXT NOT NULL DEFAULT '[]',
|
|
1814
2017
|
provider TEXT,
|
|
1815
2018
|
generated_at TEXT
|
|
1816
2019
|
);
|
|
1817
2020
|
`);
|
|
1818
2021
|
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
2022
|
+
} else if (cur < 2) {
|
|
2023
|
+
this.db.exec(`
|
|
2024
|
+
ALTER TABLE session ADD COLUMN intent TEXT;
|
|
2025
|
+
ALTER TABLE session ADD COLUMN projects_json TEXT NOT NULL DEFAULT '[]';
|
|
2026
|
+
ALTER TABLE insight ADD COLUMN intent TEXT;
|
|
2027
|
+
ALTER TABLE insight ADD COLUMN projects_json TEXT NOT NULL DEFAULT '[]';
|
|
2028
|
+
PRAGMA user_version = ${SCHEMA_VERSION};
|
|
2029
|
+
`);
|
|
1819
2030
|
}
|
|
1820
2031
|
}
|
|
1821
2032
|
/** session_id -> {mtime_ms, size_bytes} for incremental sync invalidation. */
|
|
@@ -1829,15 +2040,16 @@ var SessionIndex = class {
|
|
|
1829
2040
|
this.db.prepare(
|
|
1830
2041
|
`INSERT INTO session (session_id, host, agent, project_path, model, started_at, ended_at,
|
|
1831
2042
|
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 (
|
|
2043
|
+
topic, intent, projects_json, source_path, mtime_ms, size_bytes, indexed_at)
|
|
2044
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1834
2045
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
1835
2046
|
host=excluded.host, agent=excluded.agent, project_path=excluded.project_path,
|
|
1836
2047
|
model=excluded.model, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
1837
2048
|
turn_count=excluded.turn_count, tool_call_count=excluded.tool_call_count,
|
|
1838
2049
|
input_tokens=excluded.input_tokens, output_tokens=excluded.output_tokens,
|
|
1839
2050
|
cost_usd=excluded.cost_usd, title=excluded.title, labels_json=excluded.labels_json,
|
|
1840
|
-
topic=excluded.topic,
|
|
2051
|
+
topic=excluded.topic, intent=excluded.intent, projects_json=excluded.projects_json,
|
|
2052
|
+
source_path=excluded.source_path, mtime_ms=excluded.mtime_ms,
|
|
1841
2053
|
size_bytes=excluded.size_bytes, indexed_at=excluded.indexed_at`
|
|
1842
2054
|
).run(
|
|
1843
2055
|
env.session_id,
|
|
@@ -1855,6 +2067,8 @@ var SessionIndex = class {
|
|
|
1855
2067
|
env.title ?? null,
|
|
1856
2068
|
JSON.stringify(env.labels ?? []),
|
|
1857
2069
|
src.topic ?? null,
|
|
2070
|
+
src.intent ?? null,
|
|
2071
|
+
JSON.stringify(src.projects ?? []),
|
|
1858
2072
|
src.source_path,
|
|
1859
2073
|
src.mtime_ms,
|
|
1860
2074
|
src.size_bytes,
|
|
@@ -1884,12 +2098,14 @@ var SessionIndex = class {
|
|
|
1884
2098
|
}
|
|
1885
2099
|
upsertInsight(ins) {
|
|
1886
2100
|
this.db.prepare(
|
|
1887
|
-
`INSERT OR REPLACE INTO insight (session_id, topic, tags_json, signals_json, provider, generated_at)
|
|
1888
|
-
VALUES (
|
|
2101
|
+
`INSERT OR REPLACE INTO insight (session_id, topic, intent, tags_json, projects_json, signals_json, provider, generated_at)
|
|
2102
|
+
VALUES (?,?,?,?,?,?,?,?)`
|
|
1889
2103
|
).run(
|
|
1890
2104
|
ins.session_id,
|
|
1891
2105
|
ins.topic ?? null,
|
|
2106
|
+
ins.intent ?? null,
|
|
1892
2107
|
JSON.stringify(ins.tags ?? []),
|
|
2108
|
+
JSON.stringify(ins.projects ?? []),
|
|
1893
2109
|
JSON.stringify(ins.signals ?? []),
|
|
1894
2110
|
ins.provider,
|
|
1895
2111
|
ins.generated_at
|
|
@@ -1917,6 +2133,8 @@ var SessionIndex = class {
|
|
|
1917
2133
|
title: r.title ?? null,
|
|
1918
2134
|
labels: safeJson(r.labels_json),
|
|
1919
2135
|
topic: r.topic ?? null,
|
|
2136
|
+
intent: r.intent ?? null,
|
|
2137
|
+
projects: safeJson(r.projects_json),
|
|
1920
2138
|
source_path: r.source_path
|
|
1921
2139
|
};
|
|
1922
2140
|
}
|
|
@@ -1939,6 +2157,48 @@ var SessionIndex = class {
|
|
|
1939
2157
|
).all(like, like, limit);
|
|
1940
2158
|
return rows.map((r) => this.rowToIndex(r));
|
|
1941
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
|
+
}
|
|
1942
2202
|
stats() {
|
|
1943
2203
|
const s = this.db.prepare("SELECT COUNT(*) c, COALESCE(SUM(cost_usd),0) cost FROM session").get();
|
|
1944
2204
|
const t = this.db.prepare("SELECT COUNT(*) c FROM turn").get();
|
|
@@ -1962,7 +2222,7 @@ function safeJson(s) {
|
|
|
1962
2222
|
}
|
|
1963
2223
|
|
|
1964
2224
|
// src/index_store/sync.ts
|
|
1965
|
-
import { existsSync as
|
|
2225
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
1966
2226
|
import { safeParseInsights, safeParseSession as safeParseSession2 } from "@unpolarize/code-sessions-schema";
|
|
1967
2227
|
function syncIndex(cfg, opts = {}) {
|
|
1968
2228
|
const index = opts.index ?? new SessionIndex(cfg.indexPath);
|
|
@@ -1976,8 +2236,8 @@ function syncIndex(cfg, opts = {}) {
|
|
|
1976
2236
|
let unchanged = 0;
|
|
1977
2237
|
for (const ref of refs) {
|
|
1978
2238
|
const envPath = envelopeFile(ref.dir);
|
|
1979
|
-
if (!
|
|
1980
|
-
const st =
|
|
2239
|
+
if (!existsSync14(envPath)) continue;
|
|
2240
|
+
const st = statSync3(envPath);
|
|
1981
2241
|
const mtime_ms = Math.floor(st.mtimeMs);
|
|
1982
2242
|
const size_bytes = st.size;
|
|
1983
2243
|
seen.add(ref.sessionId);
|
|
@@ -1986,17 +2246,21 @@ function syncIndex(cfg, opts = {}) {
|
|
|
1986
2246
|
unchanged++;
|
|
1987
2247
|
continue;
|
|
1988
2248
|
}
|
|
1989
|
-
const parsed = safeParseSession2(JSON.parse(
|
|
2249
|
+
const parsed = safeParseSession2(JSON.parse(readFileSync12(envPath, "utf8")));
|
|
1990
2250
|
if (!parsed.success) continue;
|
|
1991
2251
|
const env = parsed.data;
|
|
1992
2252
|
let topic;
|
|
2253
|
+
let intent;
|
|
2254
|
+
let projects = [];
|
|
1993
2255
|
const insPath = insightsFile(ref.dir);
|
|
1994
2256
|
let insights = void 0;
|
|
1995
|
-
if (
|
|
1996
|
-
const pi = safeParseInsights(JSON.parse(
|
|
2257
|
+
if (existsSync14(insPath)) {
|
|
2258
|
+
const pi = safeParseInsights(JSON.parse(readFileSync12(insPath, "utf8")));
|
|
1997
2259
|
if (pi.success) {
|
|
1998
2260
|
insights = pi.data;
|
|
1999
2261
|
topic = pi.data.topic;
|
|
2262
|
+
intent = pi.data.intent;
|
|
2263
|
+
projects = pi.data.projects ?? [];
|
|
2000
2264
|
}
|
|
2001
2265
|
}
|
|
2002
2266
|
index.upsertSession(env, {
|
|
@@ -2004,7 +2268,9 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2004
2268
|
mtime_ms,
|
|
2005
2269
|
size_bytes,
|
|
2006
2270
|
indexed_at: now,
|
|
2007
|
-
|
|
2271
|
+
projects,
|
|
2272
|
+
...topic ? { topic } : {},
|
|
2273
|
+
...intent ? { intent } : {}
|
|
2008
2274
|
});
|
|
2009
2275
|
index.replaceTurns(env.session_id, readTurns(ref.dir));
|
|
2010
2276
|
if (insights) index.upsertInsight(insights);
|
|
@@ -2018,9 +2284,155 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2018
2284
|
}
|
|
2019
2285
|
}
|
|
2020
2286
|
|
|
2287
|
+
// src/skills/templates.ts
|
|
2288
|
+
import { INTENTS as INTENTS2, SIGNAL_KINDS as SIGNAL_KINDS2 } from "@unpolarize/code-sessions-schema";
|
|
2289
|
+
function buildLabelSkillBody() {
|
|
2290
|
+
return `You are labeling a coding-agent session for the code-sessions (CS) store.
|
|
2291
|
+
|
|
2292
|
+
Read the provided session transcript and emit ONLY a single JSON object \u2014 no prose, no code fence:
|
|
2293
|
+
|
|
2294
|
+
\`\`\`json
|
|
2295
|
+
{
|
|
2296
|
+
"topic": "3-6 word summary of what the session was about",
|
|
2297
|
+
"intent": "one of: ${INTENTS2.join(" | ")}",
|
|
2298
|
+
"tags": ["short", "themes", "or", "tool", "names"],
|
|
2299
|
+
"projects": ["repo-or-dir-names-touched"],
|
|
2300
|
+
"signals": [
|
|
2301
|
+
{ "kind": "one of: ${SIGNAL_KINDS2.join(" | ")}", "severity": "info|warn|critical", "note": "why" }
|
|
2302
|
+
],
|
|
2303
|
+
"summary": "one sentence"
|
|
2304
|
+
}
|
|
2305
|
+
\`\`\`
|
|
2306
|
+
|
|
2307
|
+
Guidance:
|
|
2308
|
+
- **intent** = what the user wanted (a feature, a bug fixed, a refactor, research, docs, ops, a review, a chore).
|
|
2309
|
+
- **projects** = the projects/repos the session actually edited (from file paths it wrote to), not everything mentioned.
|
|
2310
|
+
- **signals** = only notable ones: stuck loops, error-recovery, unusually high cost, very long sessions, tool-heavy stretches, strong negative/positive affect.
|
|
2311
|
+
- Keep it terse and machine-parseable. Output the JSON and nothing else.`;
|
|
2312
|
+
}
|
|
2313
|
+
function buildClaudeSkill() {
|
|
2314
|
+
return `---
|
|
2315
|
+
name: cs-label-session
|
|
2316
|
+
description: Label a coding session (topic, intent, tags, projects touched, signals) as JSON for the code-sessions store. Use when asked to classify or summarize a session.
|
|
2317
|
+
---
|
|
2318
|
+
|
|
2319
|
+
${buildLabelSkillBody()}
|
|
2320
|
+
`;
|
|
2321
|
+
}
|
|
2322
|
+
function buildPromptFile() {
|
|
2323
|
+
return `# cs-label-session
|
|
2324
|
+
|
|
2325
|
+
${buildLabelSkillBody()}
|
|
2326
|
+
`;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/skills/install.ts
|
|
2330
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
2331
|
+
import { homedir as homedir5 } from "os";
|
|
2332
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
2333
|
+
function targetsFor(agent, home) {
|
|
2334
|
+
const out = [];
|
|
2335
|
+
const want = (a) => agent === "all" || agent === a;
|
|
2336
|
+
if (want("claude")) {
|
|
2337
|
+
out.push({
|
|
2338
|
+
agent: "claude",
|
|
2339
|
+
path: join9(home, ".claude", "skills", "cs-label-session", "SKILL.md"),
|
|
2340
|
+
content: buildClaudeSkill()
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
if (want("codex")) {
|
|
2344
|
+
out.push({
|
|
2345
|
+
agent: "codex",
|
|
2346
|
+
path: join9(home, ".codex", "prompts", "cs-label-session.md"),
|
|
2347
|
+
content: buildPromptFile()
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
if (want("grok")) {
|
|
2351
|
+
out.push({
|
|
2352
|
+
agent: "grok",
|
|
2353
|
+
path: join9(home, ".grok", "prompts", "cs-label-session.md"),
|
|
2354
|
+
content: buildPromptFile()
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
return out;
|
|
2358
|
+
}
|
|
2359
|
+
function installSkills(opts = {}) {
|
|
2360
|
+
const home = opts.home ?? homedir5();
|
|
2361
|
+
const installed = [];
|
|
2362
|
+
for (const t of targetsFor(opts.agent ?? "all", home)) {
|
|
2363
|
+
mkdirSync8(dirname7(t.path), { recursive: true });
|
|
2364
|
+
writeFileSync7(t.path, t.content);
|
|
2365
|
+
installed.push(t.path);
|
|
2366
|
+
}
|
|
2367
|
+
return { installed };
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// src/fork.ts
|
|
2371
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
2372
|
+
import { randomUUID } from "crypto";
|
|
2373
|
+
import { mkdirSync as mkdirSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
2374
|
+
import { dirname as dirname8 } from "path";
|
|
2375
|
+
import {
|
|
2376
|
+
safeParseSession as safeParseSession3
|
|
2377
|
+
} from "@unpolarize/code-sessions-schema";
|
|
2378
|
+
function locateSession(storeDir, sessionId) {
|
|
2379
|
+
const ref = listSessionDirs(storeDir).find((r) => r.sessionId === sessionId);
|
|
2380
|
+
return ref ? { dir: ref.dir } : void 0;
|
|
2381
|
+
}
|
|
2382
|
+
function loadEnvelope2(dir) {
|
|
2383
|
+
const p = envelopeFile(dir);
|
|
2384
|
+
if (!existsSync15(p)) return void 0;
|
|
2385
|
+
const parsed = safeParseSession3(JSON.parse(readFileSync13(p, "utf8")));
|
|
2386
|
+
return parsed.success ? parsed.data : void 0;
|
|
2387
|
+
}
|
|
2388
|
+
function writeJsonAtomic4(path, value) {
|
|
2389
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
2390
|
+
const tmp = `${path}.tmp`;
|
|
2391
|
+
writeFileSync8(tmp, `${JSON.stringify(value, null, 2)}
|
|
2392
|
+
`);
|
|
2393
|
+
renameSync5(tmp, path);
|
|
2394
|
+
}
|
|
2395
|
+
function forkSession(cfg, opts) {
|
|
2396
|
+
const located = locateSession(cfg.storeDir, opts.sessionId);
|
|
2397
|
+
if (!located) throw new Error(`session not found in store: ${opts.sessionId}`);
|
|
2398
|
+
const srcEnv = loadEnvelope2(located.dir);
|
|
2399
|
+
const allTurns = readTurns(located.dir);
|
|
2400
|
+
const prefix = allTurns.filter((t) => t.turn_index <= opts.atTurn);
|
|
2401
|
+
if (prefix.length === 0) throw new Error(`no turns at or before index ${opts.atTurn}`);
|
|
2402
|
+
const newId = opts.newSessionId ?? randomUUID();
|
|
2403
|
+
const agent = opts.agent ?? srcEnv?.agent ?? "claude-code";
|
|
2404
|
+
const month = monthOf(srcEnv?.started_at ?? prefix[0]?.ts);
|
|
2405
|
+
const dir = sessionDir(cfg.storeDir, cfg.host, month, newId);
|
|
2406
|
+
const newTurns = prefix.map((t) => ({
|
|
2407
|
+
...t,
|
|
2408
|
+
session_id: newId,
|
|
2409
|
+
host: cfg.host,
|
|
2410
|
+
agent
|
|
2411
|
+
}));
|
|
2412
|
+
for (const t of newTurns) writeTurnFile(dir, t);
|
|
2413
|
+
const env = computeEnvelope(
|
|
2414
|
+
newTurns,
|
|
2415
|
+
{
|
|
2416
|
+
...srcEnv?.model ? { model: srcEnv.model } : {},
|
|
2417
|
+
...srcEnv?.project_path ? { project_path: srcEnv.project_path } : {},
|
|
2418
|
+
...srcEnv?.title ? { title: `fork: ${srcEnv.title}` } : {}
|
|
2419
|
+
},
|
|
2420
|
+
{ session_id: newId, host: cfg.host, agent, native_uuid: newId }
|
|
2421
|
+
);
|
|
2422
|
+
env.forked_from = { session_id: opts.sessionId, turn_index: opts.atTurn };
|
|
2423
|
+
env.native_ref.format = "fork";
|
|
2424
|
+
writeJsonAtomic4(envelopeFile(dir), env);
|
|
2425
|
+
return {
|
|
2426
|
+
newSessionId: newId,
|
|
2427
|
+
sessionDir: dir,
|
|
2428
|
+
turns: newTurns.length,
|
|
2429
|
+
forkedFrom: env.forked_from
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2021
2433
|
// src/commands.ts
|
|
2022
|
-
import { existsSync as
|
|
2023
|
-
import { join as
|
|
2434
|
+
import { existsSync as existsSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
2435
|
+
import { join as join10 } from "path";
|
|
2024
2436
|
function gitStoreFor(cfg) {
|
|
2025
2437
|
return new GitStore(cfg.storeDir, {
|
|
2026
2438
|
...cfg.git.remote ? { remote: cfg.git.remote } : {},
|
|
@@ -2030,13 +2442,13 @@ function gitStoreFor(cfg) {
|
|
|
2030
2442
|
function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
2031
2443
|
const out = [];
|
|
2032
2444
|
const walk = (dir, depth) => {
|
|
2033
|
-
if (depth > maxDepth || !
|
|
2445
|
+
if (depth > maxDepth || !existsSync16(dir)) return;
|
|
2034
2446
|
for (const e of readEntries(dir)) {
|
|
2035
2447
|
const name = String(e.name);
|
|
2036
2448
|
if (e.isFile() && name.endsWith(".jsonl")) {
|
|
2037
|
-
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path:
|
|
2449
|
+
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join10(dir, name) });
|
|
2038
2450
|
} else if (e.isDirectory()) {
|
|
2039
|
-
walk(
|
|
2451
|
+
walk(join10(dir, name), depth + 1);
|
|
2040
2452
|
}
|
|
2041
2453
|
}
|
|
2042
2454
|
};
|
|
@@ -2046,9 +2458,9 @@ function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
|
2046
2458
|
function cmdInit(cfg) {
|
|
2047
2459
|
const git = gitStoreFor(cfg);
|
|
2048
2460
|
git.init();
|
|
2049
|
-
const configPath =
|
|
2050
|
-
if (!
|
|
2051
|
-
|
|
2461
|
+
const configPath = join10(cfg.storeDir, "config.json");
|
|
2462
|
+
if (!existsSync16(configPath)) {
|
|
2463
|
+
writeFileSync9(
|
|
2052
2464
|
configPath,
|
|
2053
2465
|
`${JSON.stringify({ insights: cfg.insights, batch: cfg.batch, hygiene: cfg.hygiene }, null, 2)}
|
|
2054
2466
|
`
|
|
@@ -2061,7 +2473,7 @@ function cmdStatus(cfg) {
|
|
|
2061
2473
|
const state = new StateStore(cfg.statePath);
|
|
2062
2474
|
const sessions = Object.keys(state.all());
|
|
2063
2475
|
const stored = listSessionDirs(cfg.storeDir);
|
|
2064
|
-
const socketUp =
|
|
2476
|
+
const socketUp = existsSync16(cfg.socketPath);
|
|
2065
2477
|
const lines = [
|
|
2066
2478
|
`store: ${cfg.storeDir}`,
|
|
2067
2479
|
`host: ${cfg.host}`,
|
|
@@ -2116,6 +2528,20 @@ async function cmdBackfill(cfg, opts = {}) {
|
|
|
2116
2528
|
turns += t;
|
|
2117
2529
|
parts.push(`codex: ${n} sessions / ${t} turns`);
|
|
2118
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
|
+
}
|
|
2119
2545
|
const git = gitStoreFor(cfg);
|
|
2120
2546
|
git.init();
|
|
2121
2547
|
git.commit(`backfill (${agent}): ${sessions} sessions`);
|
|
@@ -2130,7 +2556,7 @@ async function cmdReindex(cfg, opts = {}) {
|
|
|
2130
2556
|
}
|
|
2131
2557
|
function cmdInstallHooks(cfg, opts = {}) {
|
|
2132
2558
|
const home = cfg.claudeProjectsDir.replace(/\/projects\/?$/, "");
|
|
2133
|
-
const settingsPath = opts.settingsPath ??
|
|
2559
|
+
const settingsPath = opts.settingsPath ?? join10(home, "settings.json");
|
|
2134
2560
|
const command = opts.command ?? "code-sessions hook";
|
|
2135
2561
|
const res = installHooks(settingsPath, command);
|
|
2136
2562
|
return {
|
|
@@ -2140,10 +2566,10 @@ function cmdInstallHooks(cfg, opts = {}) {
|
|
|
2140
2566
|
}
|
|
2141
2567
|
function cmdDoctor(cfg) {
|
|
2142
2568
|
const checks = [
|
|
2143
|
-
["store dir exists",
|
|
2144
|
-
["store is git repo",
|
|
2145
|
-
["daemon socket present",
|
|
2146
|
-
["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)]
|
|
2147
2573
|
];
|
|
2148
2574
|
const lines = checks.map(([name, ok]) => `${ok ? "\u2713" : "\u2717"} ${name}`);
|
|
2149
2575
|
const code = checks.every(([, ok]) => ok) ? 0 : 1;
|
|
@@ -2159,6 +2585,49 @@ async function cmdExport(cfg, opts = {}) {
|
|
|
2159
2585
|
output: `Exported ${res.exported}/${res.total} session(s) to ${cfg.telemetry.endpoint} (${res.failed} failed)`
|
|
2160
2586
|
};
|
|
2161
2587
|
}
|
|
2588
|
+
function cmdFork(cfg, opts) {
|
|
2589
|
+
if (!opts.sessionId || Number.isNaN(opts.atTurn)) {
|
|
2590
|
+
return { code: 1, output: "usage: code-sessions fork <session-id> --at <turn> [--id <new-id>]" };
|
|
2591
|
+
}
|
|
2592
|
+
try {
|
|
2593
|
+
const res = forkSession(cfg, {
|
|
2594
|
+
sessionId: opts.sessionId,
|
|
2595
|
+
atTurn: opts.atTurn,
|
|
2596
|
+
...opts.newId ? { newSessionId: opts.newId } : {}
|
|
2597
|
+
});
|
|
2598
|
+
const git = gitStoreFor(cfg);
|
|
2599
|
+
if (git.isRepo()) git.commit(`fork ${opts.sessionId}@${opts.atTurn} -> ${res.newSessionId}`);
|
|
2600
|
+
return {
|
|
2601
|
+
code: 0,
|
|
2602
|
+
output: `Forked ${opts.sessionId} at turn ${opts.atTurn} \u2192 ${res.newSessionId} (${res.turns} turns) at ${res.sessionDir}`
|
|
2603
|
+
};
|
|
2604
|
+
} catch (e) {
|
|
2605
|
+
return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
2606
|
+
}
|
|
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
|
+
}
|
|
2626
|
+
function cmdInstallSkills(opts = {}) {
|
|
2627
|
+
const res = installSkills(opts.agent ? { agent: opts.agent } : {});
|
|
2628
|
+
return { code: 0, output: `Installed cs-label-session skill:
|
|
2629
|
+
${res.installed.join("\n ")}` };
|
|
2630
|
+
}
|
|
2162
2631
|
function cmdIndex(cfg) {
|
|
2163
2632
|
const stats = syncIndex(cfg);
|
|
2164
2633
|
return {
|
|
@@ -2171,8 +2640,9 @@ function fmtRow(r) {
|
|
|
2171
2640
|
const agent = (r.agent || "?").padEnd(11).slice(0, 11);
|
|
2172
2641
|
const tok = String(r.input_tokens + r.output_tokens).padStart(8);
|
|
2173
2642
|
const cost = `$${r.cost_usd.toFixed(2)}`.padStart(8);
|
|
2174
|
-
const
|
|
2175
|
-
|
|
2643
|
+
const intent = (r.intent || "\xB7").padEnd(8).slice(0, 8);
|
|
2644
|
+
const title = (r.topic || r.title || r.session_id).slice(0, 44);
|
|
2645
|
+
return `${date} ${agent} ${intent} ${tok} ${cost} ${title}`;
|
|
2176
2646
|
}
|
|
2177
2647
|
function cmdQuery(cfg, opts = {}) {
|
|
2178
2648
|
const index = new SessionIndex(cfg.indexPath);
|
|
@@ -2218,9 +2688,9 @@ async function startDaemon(cfg) {
|
|
|
2218
2688
|
}
|
|
2219
2689
|
|
|
2220
2690
|
// src/hooks/shim.ts
|
|
2221
|
-
import { existsSync as
|
|
2691
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2222
2692
|
async function handleHookInput(socketPath, rawInput) {
|
|
2223
|
-
if (!
|
|
2693
|
+
if (!existsSync17(socketPath)) return { ok: false, error: "daemon not running" };
|
|
2224
2694
|
let parsed;
|
|
2225
2695
|
try {
|
|
2226
2696
|
parsed = JSON.parse(rawInput);
|
|
@@ -2276,6 +2746,9 @@ export {
|
|
|
2276
2746
|
deriveSignals,
|
|
2277
2747
|
guessTopic,
|
|
2278
2748
|
deriveTags,
|
|
2749
|
+
projectIdFromPath,
|
|
2750
|
+
deriveProjects,
|
|
2751
|
+
deriveIntent,
|
|
2279
2752
|
FakeProvider,
|
|
2280
2753
|
buildPrompt,
|
|
2281
2754
|
parseLabelJson,
|
|
@@ -2301,9 +2774,17 @@ export {
|
|
|
2301
2774
|
codexSessionsRoot,
|
|
2302
2775
|
discoverCodexSessions,
|
|
2303
2776
|
parseCodexSession,
|
|
2777
|
+
codebuildSessionsRoot,
|
|
2778
|
+
discoverCodebuildSessions,
|
|
2779
|
+
parseCodebuildSession,
|
|
2304
2780
|
writeImportedSession,
|
|
2305
2781
|
SessionIndex,
|
|
2306
2782
|
syncIndex,
|
|
2783
|
+
buildLabelSkillBody,
|
|
2784
|
+
buildClaudeSkill,
|
|
2785
|
+
buildPromptFile,
|
|
2786
|
+
installSkills,
|
|
2787
|
+
forkSession,
|
|
2307
2788
|
listClaudeTranscripts,
|
|
2308
2789
|
cmdInit,
|
|
2309
2790
|
cmdStatus,
|
|
@@ -2312,6 +2793,9 @@ export {
|
|
|
2312
2793
|
cmdInstallHooks,
|
|
2313
2794
|
cmdDoctor,
|
|
2314
2795
|
cmdExport,
|
|
2796
|
+
cmdFork,
|
|
2797
|
+
cmdUsage,
|
|
2798
|
+
cmdInstallSkills,
|
|
2315
2799
|
cmdIndex,
|
|
2316
2800
|
cmdQuery,
|
|
2317
2801
|
cmdSearch,
|