@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.
@@ -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, tags);
1206
+ updateEnvelopeLabels(sessionDir2, { topic, intent, projects });
1154
1207
  return insights;
1155
1208
  }
1156
- function updateEnvelopeLabels(sessionDir2, topic, tags) {
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 = [.../* @__PURE__ */ new Set([...topic ? [topic] : [], ...tags])].slice(0, 16);
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 existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
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 (existsSync12(envPath)) {
1933
+ if (existsSync13(envPath)) {
1739
1934
  try {
1740
- const prev = JSON.parse(readFileSync10(envPath, "utf8"));
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 = 1;
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
- if ((row?.user_version ?? 0) < 1) {
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, source_path=excluded.source_path, mtime_ms=excluded.mtime_ms,
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 existsSync13, readFileSync as readFileSync11, statSync as statSync2 } from "fs";
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 (!existsSync13(envPath)) continue;
1980
- const st = statSync2(envPath);
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(readFileSync11(envPath, "utf8")));
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 (existsSync13(insPath)) {
1996
- const pi = safeParseInsights(JSON.parse(readFileSync11(insPath, "utf8")));
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
- ...topic ? { topic } : {}
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 existsSync14, writeFileSync as writeFileSync7 } from "fs";
2023
- import { join as join8 } from "path";
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 || !existsSync14(dir)) return;
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: join8(dir, name) });
2449
+ out.push({ sessionId: name.replace(/\.jsonl$/, ""), path: join10(dir, name) });
2038
2450
  } else if (e.isDirectory()) {
2039
- walk(join8(dir, name), depth + 1);
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 = join8(cfg.storeDir, "config.json");
2050
- if (!existsSync14(configPath)) {
2051
- writeFileSync7(
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 = existsSync14(cfg.socketPath);
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 ?? join8(home, "settings.json");
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", 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)]
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 title = (r.topic || r.title || r.session_id).slice(0, 48);
2175
- return `${date} ${agent} ${tok} ${cost} ${title}`;
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 existsSync15 } from "fs";
2691
+ import { existsSync as existsSync17 } from "fs";
2222
2692
  async function handleHookInput(socketPath, rawInput) {
2223
- if (!existsSync15(socketPath)) return { ok: false, error: "daemon not running" };
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,