@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.
@@ -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, tags);
1205
+ updateEnvelopeLabels(sessionDir2, { topic, intent, projects });
1154
1206
  return insights;
1155
1207
  }
1156
- function updateEnvelopeLabels(sessionDir2, topic, tags) {
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 = [.../* @__PURE__ */ new Set([...topic ? [topic] : [], ...tags])].slice(0, 16);
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 = 1;
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
- if ((row?.user_version ?? 0) < 1) {
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, source_path=excluded.source_path, mtime_ms=excluded.mtime_ms,
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
- ...topic ? { topic } : {}
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 existsSync14, writeFileSync as writeFileSync7 } from "fs";
2023
- import { join as join8 } from "path";
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 || !existsSync14(dir)) return;
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: join8(dir, name) });
2267
+ out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join9(dir, name) });
2038
2268
  } else if (e.isDirectory()) {
2039
- walk(join8(dir, name), depth + 1);
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 = join8(cfg.storeDir, "config.json");
2050
- if (!existsSync14(configPath)) {
2051
- writeFileSync7(
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 = existsSync14(cfg.socketPath);
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 ?? join8(home, "settings.json");
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", existsSync14(cfg.storeDir)],
2144
- ["store is git repo", existsSync14(join8(cfg.storeDir, ".git"))],
2145
- ["daemon socket present", existsSync14(cfg.socketPath)],
2146
- ["claude projects dir", existsSync14(cfg.claudeProjectsDir)]
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 title = (r.topic || r.title || r.session_id).slice(0, 48);
2175
- return `${date} ${agent} ${tok} ${cost} ${title}`;
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 existsSync15 } from "fs";
2477
+ import { existsSync as existsSync16 } from "fs";
2222
2478
  async function handleHookInput(socketPath, rawInput) {
2223
- if (!existsSync15(socketPath)) return { ok: false, error: "daemon not running" };
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-ZJG2DWAK.js";
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();