@unpolarize/code-sessions 0.1.0 → 0.2.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-HV6FQJPS.js} +301 -35
- package/dist/cli.js +17 -1
- package/dist/index.js +21 -1
- package/package.json +15 -5
- package/src/cli.ts +16 -0
- package/src/cliargs.ts +2 -0
- package/src/commands.ts +34 -2
- package/src/fork.test.ts +80 -0
- package/src/fork.ts +91 -0
- package/src/index.ts +2 -0
- package/src/index_store/db.ts +39 -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,6 +133,7 @@ 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
138
|
backfill Import existing transcripts into the store [--agent claude|grok|codex|all]
|
|
138
139
|
reindex (Re)derive insights for stored sessions [--since YYYY-MM]
|
|
@@ -140,6 +141,7 @@ Commands:
|
|
|
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]
|
|
142
143
|
search Full-text search session turns <text> [--limit N]
|
|
144
|
+
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
143
145
|
analytics Compute MVP-2 rollups + digest into analytics/
|
|
144
146
|
status Show daemon/store status
|
|
145
147
|
doctor Environment checks
|
|
@@ -973,6 +975,45 @@ function deriveTags(turns) {
|
|
|
973
975
|
for (const t of turns) for (const c of t.tool_calls) tags.add(c.name);
|
|
974
976
|
return [...tags].slice(0, 12);
|
|
975
977
|
}
|
|
978
|
+
function projectIdFromPath(p) {
|
|
979
|
+
const segs = p.split("/").filter(Boolean);
|
|
980
|
+
const i = segs.indexOf("projects");
|
|
981
|
+
if (i >= 0 && segs[i + 1] === "ai" && segs[i + 2]) return `ai/${segs[i + 2]}`;
|
|
982
|
+
if (i >= 0 && segs[i + 1]) return segs[i + 1];
|
|
983
|
+
if (segs.includes("docs")) return "docs";
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
function deriveProjects(turns) {
|
|
987
|
+
const set = /* @__PURE__ */ new Set();
|
|
988
|
+
for (const t of turns) {
|
|
989
|
+
for (const c of t.tool_calls) {
|
|
990
|
+
const fp = c.input?.file_path ?? c.input?.path;
|
|
991
|
+
if (typeof fp === "string") {
|
|
992
|
+
const id = projectIdFromPath(fp);
|
|
993
|
+
if (id) set.add(id);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return [...set].sort().slice(0, 12);
|
|
998
|
+
}
|
|
999
|
+
var INTENT_PATTERNS = [
|
|
1000
|
+
["bugfix", /\b(fix|bug|broken|error|crash|regression|failing|stack ?trace)\b/i],
|
|
1001
|
+
["feature", /\b(add|implement|build|create|feature|support|introduce|new )\b/i],
|
|
1002
|
+
["refactor", /\b(refactor|clean ?up|simplify|rename|restructure|extract|dedupe)\b/i],
|
|
1003
|
+
["research", /\b(research|investigate|explore|compare|evaluate|find out|how (do|does|to)|why)\b/i],
|
|
1004
|
+
["docs", /\b(document|docs|readme|write[ -]?up|notes|comment)\b/i],
|
|
1005
|
+
["review", /\b(review|audit|critique|check|inspect)\b/i],
|
|
1006
|
+
["ops", /\b(deploy|release|publish|install|configure|ci\/?cd|pipeline|infra)\b/i]
|
|
1007
|
+
];
|
|
1008
|
+
function deriveIntent(turns) {
|
|
1009
|
+
const firstUser = turns.find((t) => t.role === "user" && t.text.trim().length > 0);
|
|
1010
|
+
if (!firstUser) return void 0;
|
|
1011
|
+
const text = firstUser.text;
|
|
1012
|
+
for (const [intent, re] of INTENT_PATTERNS) {
|
|
1013
|
+
if (re.test(text)) return intent;
|
|
1014
|
+
}
|
|
1015
|
+
return "other";
|
|
1016
|
+
}
|
|
976
1017
|
|
|
977
1018
|
// src/insights/provider.ts
|
|
978
1019
|
var FakeProvider = class {
|
|
@@ -980,10 +1021,13 @@ var FakeProvider = class {
|
|
|
980
1021
|
async label(req) {
|
|
981
1022
|
const result = {
|
|
982
1023
|
tags: deriveTags(req.turns),
|
|
1024
|
+
projects: deriveProjects(req.turns),
|
|
983
1025
|
signals: []
|
|
984
1026
|
};
|
|
985
1027
|
const topic = guessTopic(req.turns);
|
|
986
1028
|
if (topic) result.topic = topic;
|
|
1029
|
+
const intent = deriveIntent(req.turns);
|
|
1030
|
+
if (intent) result.intent = intent;
|
|
987
1031
|
const assistantText = req.turns.find((t) => t.role === "assistant")?.text;
|
|
988
1032
|
if (assistantText) result.summary = assistantText.slice(0, 160);
|
|
989
1033
|
return result;
|
|
@@ -992,7 +1036,7 @@ var FakeProvider = class {
|
|
|
992
1036
|
|
|
993
1037
|
// src/insights/llm.ts
|
|
994
1038
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
995
|
-
import { SIGNAL_KINDS } from "@unpolarize/code-sessions-schema";
|
|
1039
|
+
import { INTENTS, SIGNAL_KINDS } from "@unpolarize/code-sessions-schema";
|
|
996
1040
|
var MAX_HEAD = 40;
|
|
997
1041
|
var MAX_TAIL = 10;
|
|
998
1042
|
var MAX_TURN_CHARS = 400;
|
|
@@ -1006,8 +1050,8 @@ function buildPrompt(req) {
|
|
|
1006
1050
|
}).join("\n");
|
|
1007
1051
|
return [
|
|
1008
1052
|
"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.",
|
|
1053
|
+
'{"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}]}',
|
|
1054
|
+
"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
1055
|
"",
|
|
1012
1056
|
"Transcript:",
|
|
1013
1057
|
transcript
|
|
@@ -1017,19 +1061,23 @@ var KIND_SET = new Set(SIGNAL_KINDS);
|
|
|
1017
1061
|
function parseLabelJson(out) {
|
|
1018
1062
|
const start = out.indexOf("{");
|
|
1019
1063
|
const end = out.lastIndexOf("}");
|
|
1020
|
-
if (start < 0 || end <= start) return { tags: [], signals: [] };
|
|
1064
|
+
if (start < 0 || end <= start) return { tags: [], projects: [], signals: [] };
|
|
1021
1065
|
let obj;
|
|
1022
1066
|
try {
|
|
1023
1067
|
obj = JSON.parse(out.slice(start, end + 1));
|
|
1024
1068
|
} catch {
|
|
1025
|
-
return { tags: [], signals: [] };
|
|
1069
|
+
return { tags: [], projects: [], signals: [] };
|
|
1026
1070
|
}
|
|
1027
1071
|
const result = {
|
|
1028
1072
|
tags: Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === "string") : [],
|
|
1073
|
+
projects: Array.isArray(obj.projects) ? obj.projects.filter((t) => typeof t === "string") : [],
|
|
1029
1074
|
signals: coerceSignals(obj.signals)
|
|
1030
1075
|
};
|
|
1031
1076
|
if (typeof obj.topic === "string") result.topic = obj.topic;
|
|
1032
1077
|
if (typeof obj.summary === "string") result.summary = obj.summary;
|
|
1078
|
+
if (typeof obj.intent === "string" && INTENTS.includes(obj.intent)) {
|
|
1079
|
+
result.intent = obj.intent;
|
|
1080
|
+
}
|
|
1033
1081
|
return result;
|
|
1034
1082
|
}
|
|
1035
1083
|
function coerceSignals(raw) {
|
|
@@ -1130,13 +1178,15 @@ async function labelSession(sessionDir2, identity, provider, opts = {}) {
|
|
|
1130
1178
|
const turns = readTurns(sessionDir2);
|
|
1131
1179
|
if (turns.length === 0) return void 0;
|
|
1132
1180
|
const heuristicSignals = deriveSignals(turns);
|
|
1133
|
-
let provided = { tags: [], signals: [] };
|
|
1181
|
+
let provided = { tags: [], projects: [], signals: [] };
|
|
1134
1182
|
try {
|
|
1135
1183
|
provided = await provider.label({ sessionId: identity.sessionId, host: identity.host, turns });
|
|
1136
1184
|
} catch {
|
|
1137
1185
|
}
|
|
1138
1186
|
const topic = provided.topic ?? guessTopic(turns);
|
|
1187
|
+
const intent = provided.intent ?? deriveIntent(turns);
|
|
1139
1188
|
const tags = [.../* @__PURE__ */ new Set([...provided.tags, ...deriveTags(turns)])].slice(0, 16);
|
|
1189
|
+
const projects = [.../* @__PURE__ */ new Set([...provided.projects, ...deriveProjects(turns)])].slice(0, 16);
|
|
1140
1190
|
const signals = dedupeSignals([...heuristicSignals, ...provided.signals]);
|
|
1141
1191
|
const insights = {
|
|
1142
1192
|
schema: SCHEMA_VERSIONS2.insights,
|
|
@@ -1145,15 +1195,17 @@ async function labelSession(sessionDir2, identity, provider, opts = {}) {
|
|
|
1145
1195
|
generated_at: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1146
1196
|
provider: provider.name,
|
|
1147
1197
|
tags,
|
|
1198
|
+
projects,
|
|
1148
1199
|
signals
|
|
1149
1200
|
};
|
|
1150
1201
|
if (topic) insights.topic = topic;
|
|
1202
|
+
if (intent) insights.intent = intent;
|
|
1151
1203
|
if (provided.summary) insights.summary = provided.summary;
|
|
1152
1204
|
writeJsonAtomic2(insightsFile(sessionDir2), insights);
|
|
1153
|
-
updateEnvelopeLabels(sessionDir2, topic,
|
|
1205
|
+
updateEnvelopeLabels(sessionDir2, { topic, intent, projects });
|
|
1154
1206
|
return insights;
|
|
1155
1207
|
}
|
|
1156
|
-
function updateEnvelopeLabels(sessionDir2,
|
|
1208
|
+
function updateEnvelopeLabels(sessionDir2, l) {
|
|
1157
1209
|
const path = envelopeFile(sessionDir2);
|
|
1158
1210
|
if (!existsSync7(path)) return;
|
|
1159
1211
|
let env;
|
|
@@ -1162,7 +1214,13 @@ function updateEnvelopeLabels(sessionDir2, topic, tags) {
|
|
|
1162
1214
|
} catch {
|
|
1163
1215
|
return;
|
|
1164
1216
|
}
|
|
1165
|
-
env.labels = [
|
|
1217
|
+
env.labels = [
|
|
1218
|
+
.../* @__PURE__ */ new Set([
|
|
1219
|
+
...l.intent ? [`intent:${l.intent}`] : [],
|
|
1220
|
+
...l.topic ? [l.topic] : [],
|
|
1221
|
+
...l.projects.map((p) => `project:${p}`)
|
|
1222
|
+
])
|
|
1223
|
+
].slice(0, 16);
|
|
1166
1224
|
writeJsonAtomic2(path, env);
|
|
1167
1225
|
}
|
|
1168
1226
|
async function reindexStore(cfg, provider, opts = {}) {
|
|
@@ -1752,7 +1810,7 @@ import { createRequire } from "module";
|
|
|
1752
1810
|
import { dirname as dirname6 } from "path";
|
|
1753
1811
|
var nodeRequire = createRequire(import.meta.url);
|
|
1754
1812
|
var { DatabaseSync } = nodeRequire("node:sqlite");
|
|
1755
|
-
var SCHEMA_VERSION =
|
|
1813
|
+
var SCHEMA_VERSION = 2;
|
|
1756
1814
|
function toMs(iso) {
|
|
1757
1815
|
if (!iso) return null;
|
|
1758
1816
|
const v = Date.parse(iso);
|
|
@@ -1768,7 +1826,8 @@ var SessionIndex = class {
|
|
|
1768
1826
|
}
|
|
1769
1827
|
migrate() {
|
|
1770
1828
|
const row = this.db.prepare("PRAGMA user_version").get();
|
|
1771
|
-
|
|
1829
|
+
const cur = row?.user_version ?? 0;
|
|
1830
|
+
if (cur < 1) {
|
|
1772
1831
|
this.db.exec(`
|
|
1773
1832
|
CREATE TABLE IF NOT EXISTS session (
|
|
1774
1833
|
session_id TEXT PRIMARY KEY,
|
|
@@ -1786,6 +1845,8 @@ var SessionIndex = class {
|
|
|
1786
1845
|
title TEXT,
|
|
1787
1846
|
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
1788
1847
|
topic TEXT,
|
|
1848
|
+
intent TEXT,
|
|
1849
|
+
projects_json TEXT NOT NULL DEFAULT '[]',
|
|
1789
1850
|
source_path TEXT NOT NULL,
|
|
1790
1851
|
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
1791
1852
|
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
@@ -1809,13 +1870,23 @@ var SessionIndex = class {
|
|
|
1809
1870
|
CREATE TABLE IF NOT EXISTS insight (
|
|
1810
1871
|
session_id TEXT PRIMARY KEY REFERENCES session(session_id) ON DELETE CASCADE,
|
|
1811
1872
|
topic TEXT,
|
|
1873
|
+
intent TEXT,
|
|
1812
1874
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
1875
|
+
projects_json TEXT NOT NULL DEFAULT '[]',
|
|
1813
1876
|
signals_json TEXT NOT NULL DEFAULT '[]',
|
|
1814
1877
|
provider TEXT,
|
|
1815
1878
|
generated_at TEXT
|
|
1816
1879
|
);
|
|
1817
1880
|
`);
|
|
1818
1881
|
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1882
|
+
} else if (cur < 2) {
|
|
1883
|
+
this.db.exec(`
|
|
1884
|
+
ALTER TABLE session ADD COLUMN intent TEXT;
|
|
1885
|
+
ALTER TABLE session ADD COLUMN projects_json TEXT NOT NULL DEFAULT '[]';
|
|
1886
|
+
ALTER TABLE insight ADD COLUMN intent TEXT;
|
|
1887
|
+
ALTER TABLE insight ADD COLUMN projects_json TEXT NOT NULL DEFAULT '[]';
|
|
1888
|
+
PRAGMA user_version = ${SCHEMA_VERSION};
|
|
1889
|
+
`);
|
|
1819
1890
|
}
|
|
1820
1891
|
}
|
|
1821
1892
|
/** session_id -> {mtime_ms, size_bytes} for incremental sync invalidation. */
|
|
@@ -1829,15 +1900,16 @@ var SessionIndex = class {
|
|
|
1829
1900
|
this.db.prepare(
|
|
1830
1901
|
`INSERT INTO session (session_id, host, agent, project_path, model, started_at, ended_at,
|
|
1831
1902
|
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 (
|
|
1903
|
+
topic, intent, projects_json, source_path, mtime_ms, size_bytes, indexed_at)
|
|
1904
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1834
1905
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
1835
1906
|
host=excluded.host, agent=excluded.agent, project_path=excluded.project_path,
|
|
1836
1907
|
model=excluded.model, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
1837
1908
|
turn_count=excluded.turn_count, tool_call_count=excluded.tool_call_count,
|
|
1838
1909
|
input_tokens=excluded.input_tokens, output_tokens=excluded.output_tokens,
|
|
1839
1910
|
cost_usd=excluded.cost_usd, title=excluded.title, labels_json=excluded.labels_json,
|
|
1840
|
-
topic=excluded.topic,
|
|
1911
|
+
topic=excluded.topic, intent=excluded.intent, projects_json=excluded.projects_json,
|
|
1912
|
+
source_path=excluded.source_path, mtime_ms=excluded.mtime_ms,
|
|
1841
1913
|
size_bytes=excluded.size_bytes, indexed_at=excluded.indexed_at`
|
|
1842
1914
|
).run(
|
|
1843
1915
|
env.session_id,
|
|
@@ -1855,6 +1927,8 @@ var SessionIndex = class {
|
|
|
1855
1927
|
env.title ?? null,
|
|
1856
1928
|
JSON.stringify(env.labels ?? []),
|
|
1857
1929
|
src.topic ?? null,
|
|
1930
|
+
src.intent ?? null,
|
|
1931
|
+
JSON.stringify(src.projects ?? []),
|
|
1858
1932
|
src.source_path,
|
|
1859
1933
|
src.mtime_ms,
|
|
1860
1934
|
src.size_bytes,
|
|
@@ -1884,12 +1958,14 @@ var SessionIndex = class {
|
|
|
1884
1958
|
}
|
|
1885
1959
|
upsertInsight(ins) {
|
|
1886
1960
|
this.db.prepare(
|
|
1887
|
-
`INSERT OR REPLACE INTO insight (session_id, topic, tags_json, signals_json, provider, generated_at)
|
|
1888
|
-
VALUES (
|
|
1961
|
+
`INSERT OR REPLACE INTO insight (session_id, topic, intent, tags_json, projects_json, signals_json, provider, generated_at)
|
|
1962
|
+
VALUES (?,?,?,?,?,?,?,?)`
|
|
1889
1963
|
).run(
|
|
1890
1964
|
ins.session_id,
|
|
1891
1965
|
ins.topic ?? null,
|
|
1966
|
+
ins.intent ?? null,
|
|
1892
1967
|
JSON.stringify(ins.tags ?? []),
|
|
1968
|
+
JSON.stringify(ins.projects ?? []),
|
|
1893
1969
|
JSON.stringify(ins.signals ?? []),
|
|
1894
1970
|
ins.provider,
|
|
1895
1971
|
ins.generated_at
|
|
@@ -1917,6 +1993,8 @@ var SessionIndex = class {
|
|
|
1917
1993
|
title: r.title ?? null,
|
|
1918
1994
|
labels: safeJson(r.labels_json),
|
|
1919
1995
|
topic: r.topic ?? null,
|
|
1996
|
+
intent: r.intent ?? null,
|
|
1997
|
+
projects: safeJson(r.projects_json),
|
|
1920
1998
|
source_path: r.source_path
|
|
1921
1999
|
};
|
|
1922
2000
|
}
|
|
@@ -1990,6 +2068,8 @@ function syncIndex(cfg, opts = {}) {
|
|
|
1990
2068
|
if (!parsed.success) continue;
|
|
1991
2069
|
const env = parsed.data;
|
|
1992
2070
|
let topic;
|
|
2071
|
+
let intent;
|
|
2072
|
+
let projects = [];
|
|
1993
2073
|
const insPath = insightsFile(ref.dir);
|
|
1994
2074
|
let insights = void 0;
|
|
1995
2075
|
if (existsSync13(insPath)) {
|
|
@@ -1997,6 +2077,8 @@ function syncIndex(cfg, opts = {}) {
|
|
|
1997
2077
|
if (pi.success) {
|
|
1998
2078
|
insights = pi.data;
|
|
1999
2079
|
topic = pi.data.topic;
|
|
2080
|
+
intent = pi.data.intent;
|
|
2081
|
+
projects = pi.data.projects ?? [];
|
|
2000
2082
|
}
|
|
2001
2083
|
}
|
|
2002
2084
|
index.upsertSession(env, {
|
|
@@ -2004,7 +2086,9 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2004
2086
|
mtime_ms,
|
|
2005
2087
|
size_bytes,
|
|
2006
2088
|
indexed_at: now,
|
|
2007
|
-
|
|
2089
|
+
projects,
|
|
2090
|
+
...topic ? { topic } : {},
|
|
2091
|
+
...intent ? { intent } : {}
|
|
2008
2092
|
});
|
|
2009
2093
|
index.replaceTurns(env.session_id, readTurns(ref.dir));
|
|
2010
2094
|
if (insights) index.upsertInsight(insights);
|
|
@@ -2018,9 +2102,155 @@ function syncIndex(cfg, opts = {}) {
|
|
|
2018
2102
|
}
|
|
2019
2103
|
}
|
|
2020
2104
|
|
|
2105
|
+
// src/skills/templates.ts
|
|
2106
|
+
import { INTENTS as INTENTS2, SIGNAL_KINDS as SIGNAL_KINDS2 } from "@unpolarize/code-sessions-schema";
|
|
2107
|
+
function buildLabelSkillBody() {
|
|
2108
|
+
return `You are labeling a coding-agent session for the code-sessions (CS) store.
|
|
2109
|
+
|
|
2110
|
+
Read the provided session transcript and emit ONLY a single JSON object \u2014 no prose, no code fence:
|
|
2111
|
+
|
|
2112
|
+
\`\`\`json
|
|
2113
|
+
{
|
|
2114
|
+
"topic": "3-6 word summary of what the session was about",
|
|
2115
|
+
"intent": "one of: ${INTENTS2.join(" | ")}",
|
|
2116
|
+
"tags": ["short", "themes", "or", "tool", "names"],
|
|
2117
|
+
"projects": ["repo-or-dir-names-touched"],
|
|
2118
|
+
"signals": [
|
|
2119
|
+
{ "kind": "one of: ${SIGNAL_KINDS2.join(" | ")}", "severity": "info|warn|critical", "note": "why" }
|
|
2120
|
+
],
|
|
2121
|
+
"summary": "one sentence"
|
|
2122
|
+
}
|
|
2123
|
+
\`\`\`
|
|
2124
|
+
|
|
2125
|
+
Guidance:
|
|
2126
|
+
- **intent** = what the user wanted (a feature, a bug fixed, a refactor, research, docs, ops, a review, a chore).
|
|
2127
|
+
- **projects** = the projects/repos the session actually edited (from file paths it wrote to), not everything mentioned.
|
|
2128
|
+
- **signals** = only notable ones: stuck loops, error-recovery, unusually high cost, very long sessions, tool-heavy stretches, strong negative/positive affect.
|
|
2129
|
+
- Keep it terse and machine-parseable. Output the JSON and nothing else.`;
|
|
2130
|
+
}
|
|
2131
|
+
function buildClaudeSkill() {
|
|
2132
|
+
return `---
|
|
2133
|
+
name: cs-label-session
|
|
2134
|
+
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.
|
|
2135
|
+
---
|
|
2136
|
+
|
|
2137
|
+
${buildLabelSkillBody()}
|
|
2138
|
+
`;
|
|
2139
|
+
}
|
|
2140
|
+
function buildPromptFile() {
|
|
2141
|
+
return `# cs-label-session
|
|
2142
|
+
|
|
2143
|
+
${buildLabelSkillBody()}
|
|
2144
|
+
`;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// src/skills/install.ts
|
|
2148
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
2149
|
+
import { homedir as homedir4 } from "os";
|
|
2150
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
2151
|
+
function targetsFor(agent, home) {
|
|
2152
|
+
const out = [];
|
|
2153
|
+
const want = (a) => agent === "all" || agent === a;
|
|
2154
|
+
if (want("claude")) {
|
|
2155
|
+
out.push({
|
|
2156
|
+
agent: "claude",
|
|
2157
|
+
path: join8(home, ".claude", "skills", "cs-label-session", "SKILL.md"),
|
|
2158
|
+
content: buildClaudeSkill()
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
if (want("codex")) {
|
|
2162
|
+
out.push({
|
|
2163
|
+
agent: "codex",
|
|
2164
|
+
path: join8(home, ".codex", "prompts", "cs-label-session.md"),
|
|
2165
|
+
content: buildPromptFile()
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
if (want("grok")) {
|
|
2169
|
+
out.push({
|
|
2170
|
+
agent: "grok",
|
|
2171
|
+
path: join8(home, ".grok", "prompts", "cs-label-session.md"),
|
|
2172
|
+
content: buildPromptFile()
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
return out;
|
|
2176
|
+
}
|
|
2177
|
+
function installSkills(opts = {}) {
|
|
2178
|
+
const home = opts.home ?? homedir4();
|
|
2179
|
+
const installed = [];
|
|
2180
|
+
for (const t of targetsFor(opts.agent ?? "all", home)) {
|
|
2181
|
+
mkdirSync8(dirname7(t.path), { recursive: true });
|
|
2182
|
+
writeFileSync7(t.path, t.content);
|
|
2183
|
+
installed.push(t.path);
|
|
2184
|
+
}
|
|
2185
|
+
return { installed };
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/fork.ts
|
|
2189
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
|
|
2190
|
+
import { randomUUID } from "crypto";
|
|
2191
|
+
import { mkdirSync as mkdirSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
2192
|
+
import { dirname as dirname8 } from "path";
|
|
2193
|
+
import {
|
|
2194
|
+
safeParseSession as safeParseSession3
|
|
2195
|
+
} from "@unpolarize/code-sessions-schema";
|
|
2196
|
+
function locateSession(storeDir, sessionId) {
|
|
2197
|
+
const ref = listSessionDirs(storeDir).find((r) => r.sessionId === sessionId);
|
|
2198
|
+
return ref ? { dir: ref.dir } : void 0;
|
|
2199
|
+
}
|
|
2200
|
+
function loadEnvelope2(dir) {
|
|
2201
|
+
const p = envelopeFile(dir);
|
|
2202
|
+
if (!existsSync14(p)) return void 0;
|
|
2203
|
+
const parsed = safeParseSession3(JSON.parse(readFileSync12(p, "utf8")));
|
|
2204
|
+
return parsed.success ? parsed.data : void 0;
|
|
2205
|
+
}
|
|
2206
|
+
function writeJsonAtomic4(path, value) {
|
|
2207
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
2208
|
+
const tmp = `${path}.tmp`;
|
|
2209
|
+
writeFileSync8(tmp, `${JSON.stringify(value, null, 2)}
|
|
2210
|
+
`);
|
|
2211
|
+
renameSync5(tmp, path);
|
|
2212
|
+
}
|
|
2213
|
+
function forkSession(cfg, opts) {
|
|
2214
|
+
const located = locateSession(cfg.storeDir, opts.sessionId);
|
|
2215
|
+
if (!located) throw new Error(`session not found in store: ${opts.sessionId}`);
|
|
2216
|
+
const srcEnv = loadEnvelope2(located.dir);
|
|
2217
|
+
const allTurns = readTurns(located.dir);
|
|
2218
|
+
const prefix = allTurns.filter((t) => t.turn_index <= opts.atTurn);
|
|
2219
|
+
if (prefix.length === 0) throw new Error(`no turns at or before index ${opts.atTurn}`);
|
|
2220
|
+
const newId = opts.newSessionId ?? randomUUID();
|
|
2221
|
+
const agent = opts.agent ?? srcEnv?.agent ?? "claude-code";
|
|
2222
|
+
const month = monthOf(srcEnv?.started_at ?? prefix[0]?.ts);
|
|
2223
|
+
const dir = sessionDir(cfg.storeDir, cfg.host, month, newId);
|
|
2224
|
+
const newTurns = prefix.map((t) => ({
|
|
2225
|
+
...t,
|
|
2226
|
+
session_id: newId,
|
|
2227
|
+
host: cfg.host,
|
|
2228
|
+
agent
|
|
2229
|
+
}));
|
|
2230
|
+
for (const t of newTurns) writeTurnFile(dir, t);
|
|
2231
|
+
const env = computeEnvelope(
|
|
2232
|
+
newTurns,
|
|
2233
|
+
{
|
|
2234
|
+
...srcEnv?.model ? { model: srcEnv.model } : {},
|
|
2235
|
+
...srcEnv?.project_path ? { project_path: srcEnv.project_path } : {},
|
|
2236
|
+
...srcEnv?.title ? { title: `fork: ${srcEnv.title}` } : {}
|
|
2237
|
+
},
|
|
2238
|
+
{ session_id: newId, host: cfg.host, agent, native_uuid: newId }
|
|
2239
|
+
);
|
|
2240
|
+
env.forked_from = { session_id: opts.sessionId, turn_index: opts.atTurn };
|
|
2241
|
+
env.native_ref.format = "fork";
|
|
2242
|
+
writeJsonAtomic4(envelopeFile(dir), env);
|
|
2243
|
+
return {
|
|
2244
|
+
newSessionId: newId,
|
|
2245
|
+
sessionDir: dir,
|
|
2246
|
+
turns: newTurns.length,
|
|
2247
|
+
forkedFrom: env.forked_from
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2021
2251
|
// src/commands.ts
|
|
2022
|
-
import { existsSync as
|
|
2023
|
-
import { join as
|
|
2252
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
2253
|
+
import { join as join9 } from "path";
|
|
2024
2254
|
function gitStoreFor(cfg) {
|
|
2025
2255
|
return new GitStore(cfg.storeDir, {
|
|
2026
2256
|
...cfg.git.remote ? { remote: cfg.git.remote } : {},
|
|
@@ -2030,13 +2260,13 @@ function gitStoreFor(cfg) {
|
|
|
2030
2260
|
function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
2031
2261
|
const out = [];
|
|
2032
2262
|
const walk = (dir, depth) => {
|
|
2033
|
-
if (depth > maxDepth || !
|
|
2263
|
+
if (depth > maxDepth || !existsSync15(dir)) return;
|
|
2034
2264
|
for (const e of readEntries(dir)) {
|
|
2035
2265
|
const name = String(e.name);
|
|
2036
2266
|
if (e.isFile() && name.endsWith(".jsonl")) {
|
|
2037
|
-
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path:
|
|
2267
|
+
out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join9(dir, name) });
|
|
2038
2268
|
} else if (e.isDirectory()) {
|
|
2039
|
-
walk(
|
|
2269
|
+
walk(join9(dir, name), depth + 1);
|
|
2040
2270
|
}
|
|
2041
2271
|
}
|
|
2042
2272
|
};
|
|
@@ -2046,9 +2276,9 @@ function listClaudeTranscripts(projectsDir, maxDepth = 3) {
|
|
|
2046
2276
|
function cmdInit(cfg) {
|
|
2047
2277
|
const git = gitStoreFor(cfg);
|
|
2048
2278
|
git.init();
|
|
2049
|
-
const configPath =
|
|
2050
|
-
if (!
|
|
2051
|
-
|
|
2279
|
+
const configPath = join9(cfg.storeDir, "config.json");
|
|
2280
|
+
if (!existsSync15(configPath)) {
|
|
2281
|
+
writeFileSync9(
|
|
2052
2282
|
configPath,
|
|
2053
2283
|
`${JSON.stringify({ insights: cfg.insights, batch: cfg.batch, hygiene: cfg.hygiene }, null, 2)}
|
|
2054
2284
|
`
|
|
@@ -2061,7 +2291,7 @@ function cmdStatus(cfg) {
|
|
|
2061
2291
|
const state = new StateStore(cfg.statePath);
|
|
2062
2292
|
const sessions = Object.keys(state.all());
|
|
2063
2293
|
const stored = listSessionDirs(cfg.storeDir);
|
|
2064
|
-
const socketUp =
|
|
2294
|
+
const socketUp = existsSync15(cfg.socketPath);
|
|
2065
2295
|
const lines = [
|
|
2066
2296
|
`store: ${cfg.storeDir}`,
|
|
2067
2297
|
`host: ${cfg.host}`,
|
|
@@ -2130,7 +2360,7 @@ async function cmdReindex(cfg, opts = {}) {
|
|
|
2130
2360
|
}
|
|
2131
2361
|
function cmdInstallHooks(cfg, opts = {}) {
|
|
2132
2362
|
const home = cfg.claudeProjectsDir.replace(/\/projects\/?$/, "");
|
|
2133
|
-
const settingsPath = opts.settingsPath ??
|
|
2363
|
+
const settingsPath = opts.settingsPath ?? join9(home, "settings.json");
|
|
2134
2364
|
const command = opts.command ?? "code-sessions hook";
|
|
2135
2365
|
const res = installHooks(settingsPath, command);
|
|
2136
2366
|
return {
|
|
@@ -2140,10 +2370,10 @@ function cmdInstallHooks(cfg, opts = {}) {
|
|
|
2140
2370
|
}
|
|
2141
2371
|
function cmdDoctor(cfg) {
|
|
2142
2372
|
const checks = [
|
|
2143
|
-
["store dir exists",
|
|
2144
|
-
["store is git repo",
|
|
2145
|
-
["daemon socket present",
|
|
2146
|
-
["claude projects dir",
|
|
2373
|
+
["store dir exists", existsSync15(cfg.storeDir)],
|
|
2374
|
+
["store is git repo", existsSync15(join9(cfg.storeDir, ".git"))],
|
|
2375
|
+
["daemon socket present", existsSync15(cfg.socketPath)],
|
|
2376
|
+
["claude projects dir", existsSync15(cfg.claudeProjectsDir)]
|
|
2147
2377
|
];
|
|
2148
2378
|
const lines = checks.map(([name, ok]) => `${ok ? "\u2713" : "\u2717"} ${name}`);
|
|
2149
2379
|
const code = checks.every(([, ok]) => ok) ? 0 : 1;
|
|
@@ -2159,6 +2389,31 @@ async function cmdExport(cfg, opts = {}) {
|
|
|
2159
2389
|
output: `Exported ${res.exported}/${res.total} session(s) to ${cfg.telemetry.endpoint} (${res.failed} failed)`
|
|
2160
2390
|
};
|
|
2161
2391
|
}
|
|
2392
|
+
function cmdFork(cfg, opts) {
|
|
2393
|
+
if (!opts.sessionId || Number.isNaN(opts.atTurn)) {
|
|
2394
|
+
return { code: 1, output: "usage: code-sessions fork <session-id> --at <turn> [--id <new-id>]" };
|
|
2395
|
+
}
|
|
2396
|
+
try {
|
|
2397
|
+
const res = forkSession(cfg, {
|
|
2398
|
+
sessionId: opts.sessionId,
|
|
2399
|
+
atTurn: opts.atTurn,
|
|
2400
|
+
...opts.newId ? { newSessionId: opts.newId } : {}
|
|
2401
|
+
});
|
|
2402
|
+
const git = gitStoreFor(cfg);
|
|
2403
|
+
if (git.isRepo()) git.commit(`fork ${opts.sessionId}@${opts.atTurn} -> ${res.newSessionId}`);
|
|
2404
|
+
return {
|
|
2405
|
+
code: 0,
|
|
2406
|
+
output: `Forked ${opts.sessionId} at turn ${opts.atTurn} \u2192 ${res.newSessionId} (${res.turns} turns) at ${res.sessionDir}`
|
|
2407
|
+
};
|
|
2408
|
+
} catch (e) {
|
|
2409
|
+
return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
function cmdInstallSkills(opts = {}) {
|
|
2413
|
+
const res = installSkills(opts.agent ? { agent: opts.agent } : {});
|
|
2414
|
+
return { code: 0, output: `Installed cs-label-session skill:
|
|
2415
|
+
${res.installed.join("\n ")}` };
|
|
2416
|
+
}
|
|
2162
2417
|
function cmdIndex(cfg) {
|
|
2163
2418
|
const stats = syncIndex(cfg);
|
|
2164
2419
|
return {
|
|
@@ -2171,8 +2426,9 @@ function fmtRow(r) {
|
|
|
2171
2426
|
const agent = (r.agent || "?").padEnd(11).slice(0, 11);
|
|
2172
2427
|
const tok = String(r.input_tokens + r.output_tokens).padStart(8);
|
|
2173
2428
|
const cost = `$${r.cost_usd.toFixed(2)}`.padStart(8);
|
|
2174
|
-
const
|
|
2175
|
-
|
|
2429
|
+
const intent = (r.intent || "\xB7").padEnd(8).slice(0, 8);
|
|
2430
|
+
const title = (r.topic || r.title || r.session_id).slice(0, 44);
|
|
2431
|
+
return `${date} ${agent} ${intent} ${tok} ${cost} ${title}`;
|
|
2176
2432
|
}
|
|
2177
2433
|
function cmdQuery(cfg, opts = {}) {
|
|
2178
2434
|
const index = new SessionIndex(cfg.indexPath);
|
|
@@ -2218,9 +2474,9 @@ async function startDaemon(cfg) {
|
|
|
2218
2474
|
}
|
|
2219
2475
|
|
|
2220
2476
|
// src/hooks/shim.ts
|
|
2221
|
-
import { existsSync as
|
|
2477
|
+
import { existsSync as existsSync16 } from "fs";
|
|
2222
2478
|
async function handleHookInput(socketPath, rawInput) {
|
|
2223
|
-
if (!
|
|
2479
|
+
if (!existsSync16(socketPath)) return { ok: false, error: "daemon not running" };
|
|
2224
2480
|
let parsed;
|
|
2225
2481
|
try {
|
|
2226
2482
|
parsed = JSON.parse(rawInput);
|
|
@@ -2276,6 +2532,9 @@ export {
|
|
|
2276
2532
|
deriveSignals,
|
|
2277
2533
|
guessTopic,
|
|
2278
2534
|
deriveTags,
|
|
2535
|
+
projectIdFromPath,
|
|
2536
|
+
deriveProjects,
|
|
2537
|
+
deriveIntent,
|
|
2279
2538
|
FakeProvider,
|
|
2280
2539
|
buildPrompt,
|
|
2281
2540
|
parseLabelJson,
|
|
@@ -2304,6 +2563,11 @@ export {
|
|
|
2304
2563
|
writeImportedSession,
|
|
2305
2564
|
SessionIndex,
|
|
2306
2565
|
syncIndex,
|
|
2566
|
+
buildLabelSkillBody,
|
|
2567
|
+
buildClaudeSkill,
|
|
2568
|
+
buildPromptFile,
|
|
2569
|
+
installSkills,
|
|
2570
|
+
forkSession,
|
|
2307
2571
|
listClaudeTranscripts,
|
|
2308
2572
|
cmdInit,
|
|
2309
2573
|
cmdStatus,
|
|
@@ -2312,6 +2576,8 @@ export {
|
|
|
2312
2576
|
cmdInstallHooks,
|
|
2313
2577
|
cmdDoctor,
|
|
2314
2578
|
cmdExport,
|
|
2579
|
+
cmdFork,
|
|
2580
|
+
cmdInstallSkills,
|
|
2315
2581
|
cmdIndex,
|
|
2316
2582
|
cmdQuery,
|
|
2317
2583
|
cmdSearch,
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
cmdBackfill,
|
|
5
5
|
cmdDoctor,
|
|
6
6
|
cmdExport,
|
|
7
|
+
cmdFork,
|
|
7
8
|
cmdIndex,
|
|
8
9
|
cmdInit,
|
|
9
10
|
cmdInstallHooks,
|
|
11
|
+
cmdInstallSkills,
|
|
10
12
|
cmdQuery,
|
|
11
13
|
cmdReindex,
|
|
12
14
|
cmdSearch,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
parseFlags,
|
|
21
23
|
readStdin,
|
|
22
24
|
startDaemon
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-HV6FQJPS.js";
|
|
24
26
|
|
|
25
27
|
// src/analytics/command.ts
|
|
26
28
|
import { mkdirSync, writeFileSync } from "fs";
|
|
@@ -231,6 +233,9 @@ async function main(argv) {
|
|
|
231
233
|
})
|
|
232
234
|
);
|
|
233
235
|
break;
|
|
236
|
+
case "install-skills":
|
|
237
|
+
emit(cmdInstallSkills(typeof flags.agent === "string" ? { agent: flags.agent } : {}));
|
|
238
|
+
break;
|
|
234
239
|
case "backfill":
|
|
235
240
|
emit(
|
|
236
241
|
await cmdBackfill(cfg, {
|
|
@@ -264,6 +269,17 @@ async function main(argv) {
|
|
|
264
269
|
emit(cmdSearch(cfg, { query: q, ...typeof flags.limit === "string" ? { limit: Number(flags.limit) } : {} }));
|
|
265
270
|
break;
|
|
266
271
|
}
|
|
272
|
+
case "fork": {
|
|
273
|
+
const sid = argv.slice(1).find((a) => !a.startsWith("--")) ?? "";
|
|
274
|
+
emit(
|
|
275
|
+
cmdFork(cfg, {
|
|
276
|
+
sessionId: sid,
|
|
277
|
+
atTurn: typeof flags.at === "string" ? Number(flags.at) : NaN,
|
|
278
|
+
...typeof flags.id === "string" ? { newId: flags.id } : {}
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
267
283
|
case "hook": {
|
|
268
284
|
try {
|
|
269
285
|
const input = await readStdin();
|