@unpolarize/code-sessions 0.3.0 → 0.4.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.
@@ -141,6 +141,7 @@ Commands:
141
141
  index (Re)build the internal SQLite index from the git store
142
142
  query List recent sessions from the index [--limit N] [--agent X]
143
143
  usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
144
+ graph Sessions \xD7 topics graph (nodes + edges) [--json]
144
145
  search Full-text search session turns <text> [--limit N]
145
146
  fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
146
147
  analytics Compute MVP-2 rollups + digest into analytics/
@@ -2157,6 +2158,41 @@ var SessionIndex = class {
2157
2158
  ).all(like, like, limit);
2158
2159
  return rows.map((r) => this.rowToIndex(r));
2159
2160
  }
2161
+ /** Graph of sessions clustered by topic (+ fork lineage where known). */
2162
+ graphData() {
2163
+ const rows = this.db.prepare("SELECT session_id, agent, topic, intent, cost_usd, title FROM session ORDER BY started_at DESC").all();
2164
+ const nodes = [];
2165
+ const edges = [];
2166
+ const topicNodes = /* @__PURE__ */ new Map();
2167
+ const topicId = (t) => {
2168
+ let id = topicNodes.get(t);
2169
+ if (!id) {
2170
+ id = `topic:${topicNodes.size}`;
2171
+ topicNodes.set(t, id);
2172
+ nodes.push({ id, kind: "topic", label: t, sessions: 0, cost_usd: 0 });
2173
+ }
2174
+ return id;
2175
+ };
2176
+ for (const r of rows) {
2177
+ const sid = `s:${r.session_id}`;
2178
+ nodes.push({
2179
+ id: sid,
2180
+ kind: "session",
2181
+ label: (r.topic || r.title || r.session_id).slice(0, 48),
2182
+ agent: r.agent,
2183
+ intent: r.intent ?? null,
2184
+ cost_usd: r.cost_usd,
2185
+ sessions: 1
2186
+ });
2187
+ const topic = (r.topic || r.intent || "misc").toString();
2188
+ const tid = topicId(topic);
2189
+ edges.push({ from: sid, to: tid, kind: "has-topic" });
2190
+ const tn = nodes.find((n) => n.id === tid);
2191
+ tn.sessions += 1;
2192
+ tn.cost_usd += r.cost_usd;
2193
+ }
2194
+ return { nodes, edges };
2195
+ }
2160
2196
  /** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
2161
2197
  usageSummary(opts = {}) {
2162
2198
  const rows = this.db.prepare(
@@ -2605,6 +2641,19 @@ function cmdFork(cfg, opts) {
2605
2641
  return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
2606
2642
  }
2607
2643
  }
2644
+ function cmdGraph(cfg, opts = {}) {
2645
+ syncIndex(cfg);
2646
+ const index = new SessionIndex(cfg.indexPath);
2647
+ try {
2648
+ const g = index.graphData();
2649
+ if (opts.json) return { code: 0, output: JSON.stringify(g) };
2650
+ const topics = g.nodes.filter((n) => n.kind === "topic").length;
2651
+ const sessions = g.nodes.filter((n) => n.kind === "session").length;
2652
+ return { code: 0, output: `graph: ${sessions} sessions across ${topics} topics, ${g.edges.length} edges` };
2653
+ } finally {
2654
+ index.close();
2655
+ }
2656
+ }
2608
2657
  function cmdUsage(cfg, opts = {}) {
2609
2658
  syncIndex(cfg);
2610
2659
  const index = new SessionIndex(cfg.indexPath);
@@ -2794,6 +2843,7 @@ export {
2794
2843
  cmdDoctor,
2795
2844
  cmdExport,
2796
2845
  cmdFork,
2846
+ cmdGraph,
2797
2847
  cmdUsage,
2798
2848
  cmdInstallSkills,
2799
2849
  cmdIndex,
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  cmdDoctor,
6
6
  cmdExport,
7
7
  cmdFork,
8
+ cmdGraph,
8
9
  cmdIndex,
9
10
  cmdInit,
10
11
  cmdInstallHooks,
@@ -23,7 +24,7 @@ import {
23
24
  parseFlags,
24
25
  readStdin,
25
26
  startDaemon
26
- } from "./chunk-ON3CPW4C.js";
27
+ } from "./chunk-3VPXOUIE.js";
27
28
 
28
29
  // src/analytics/command.ts
29
30
  import { mkdirSync, writeFileSync } from "fs";
@@ -260,6 +261,9 @@ async function main(argv) {
260
261
  case "usage":
261
262
  emit(cmdUsage(cfg, { json: flags.json === true }));
262
263
  break;
264
+ case "graph":
265
+ emit(cmdGraph(cfg, { json: flags.json === true }));
266
+ break;
263
267
  case "query":
264
268
  emit(
265
269
  cmdQuery(cfg, {
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  cmdDoctor,
21
21
  cmdExport,
22
22
  cmdFork,
23
+ cmdGraph,
23
24
  cmdIndex,
24
25
  cmdInit,
25
26
  cmdInstallHooks,
@@ -92,7 +93,7 @@ import {
92
93
  writeBlobFile,
93
94
  writeImportedSession,
94
95
  writeTurnFile
95
- } from "./chunk-ON3CPW4C.js";
96
+ } from "./chunk-3VPXOUIE.js";
96
97
  export {
97
98
  CaptureEngine,
98
99
  DEFAULT_HOOK_EVENTS,
@@ -115,6 +116,7 @@ export {
115
116
  cmdDoctor,
116
117
  cmdExport,
117
118
  cmdFork,
119
+ cmdGraph,
118
120
  cmdIndex,
119
121
  cmdInit,
120
122
  cmdInstallHooks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unpolarize/code-sessions",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Headless, event-driven cross-agent session capture agent (daemon + CLI)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "build": "tsup src/index.ts src/cli.ts --format esm --clean --out-dir dist"
26
26
  },
27
27
  "dependencies": {
28
- "@unpolarize/code-sessions-schema": "^0.3.0",
28
+ "@unpolarize/code-sessions-schema": "^0.4.0",
29
29
  "zod": "^3.23.8"
30
30
  }
31
31
  }
package/src/cli.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  cmdDoctor,
6
6
  cmdExport,
7
7
  cmdFork,
8
+ cmdGraph,
8
9
  cmdIndex,
9
10
  cmdInit,
10
11
  cmdInstallHooks,
@@ -74,6 +75,9 @@ export async function main(argv: string[]): Promise<void> {
74
75
  case 'usage':
75
76
  emit(cmdUsage(cfg, { json: flags.json === true }));
76
77
  break;
78
+ case 'graph':
79
+ emit(cmdGraph(cfg, { json: flags.json === true }));
80
+ break;
77
81
  case 'query':
78
82
  emit(
79
83
  cmdQuery(cfg, {
package/src/cliargs.ts CHANGED
@@ -59,6 +59,7 @@ Commands:
59
59
  index (Re)build the internal SQLite index from the git store
60
60
  query List recent sessions from the index [--limit N] [--agent X]
61
61
  usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
62
+ graph Sessions × topics graph (nodes + edges) [--json]
62
63
  search Full-text search session turns <text> [--limit N]
63
64
  fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
64
65
  analytics Compute MVP-2 rollups + digest into analytics/
package/src/commands.ts CHANGED
@@ -234,6 +234,21 @@ export function cmdFork(
234
234
  }
235
235
  }
236
236
 
237
+ /** Sessions × topics graph (nodes + edges) from the CS index. JSON for the graph view. */
238
+ export function cmdGraph(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
239
+ syncIndex(cfg);
240
+ const index = new SessionIndex(cfg.indexPath);
241
+ try {
242
+ const g = index.graphData();
243
+ if (opts.json) return { code: 0, output: JSON.stringify(g) };
244
+ const topics = g.nodes.filter((n) => n.kind === 'topic').length;
245
+ const sessions = g.nodes.filter((n) => n.kind === 'session').length;
246
+ return { code: 0, output: `graph: ${sessions} sessions across ${topics} topics, ${g.edges.length} edges` };
247
+ } finally {
248
+ index.close();
249
+ }
250
+ }
251
+
237
252
  /** Aggregated usage from the CS index (totals/byAgent/byDay/byProject/topByCost). */
238
253
  export function cmdUsage(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
239
254
  syncIndex(cfg); // ensure the index reflects the current store
@@ -116,6 +116,25 @@ describe('SessionIndex', () => {
116
116
  }
117
117
  });
118
118
 
119
+ it('builds a sessions×topics graph', () => {
120
+ const idx = new SessionIndex(':memory:');
121
+ try {
122
+ idx.upsertSession(env('a', 'claude-code'), { ...src, topic: 'fix parser' });
123
+ idx.upsertSession(env('b', 'grok'), { ...src, source_path: '/s/b.json', topic: 'fix parser' });
124
+ idx.upsertSession(env('c', 'codex'), { ...src, source_path: '/s/c.json', topic: 'add feature' });
125
+ const g = idx.graphData();
126
+ const topics = g.nodes.filter((n) => n.kind === 'topic');
127
+ const sessions = g.nodes.filter((n) => n.kind === 'session');
128
+ expect(sessions).toHaveLength(3);
129
+ expect(topics).toHaveLength(2); // "fix parser" + "add feature"
130
+ expect(g.edges).toHaveLength(3); // each session -> its topic
131
+ const fixParser = topics.find((t) => t.label === 'fix parser')!;
132
+ expect(fixParser.sessions).toBe(2);
133
+ } finally {
134
+ idx.close();
135
+ }
136
+ });
137
+
119
138
  it('filters by agent and deletes sessions (cascade turns)', () => {
120
139
  const idx = new SessionIndex(':memory:');
121
140
  try {
@@ -51,6 +51,25 @@ export interface UsageBucket {
51
51
  cost_usd: number;
52
52
  }
53
53
 
54
+ export interface GraphNode {
55
+ id: string;
56
+ kind: 'session' | 'topic';
57
+ label: string;
58
+ agent?: string;
59
+ intent?: string | null;
60
+ cost_usd: number;
61
+ sessions: number;
62
+ }
63
+ export interface GraphEdge {
64
+ from: string;
65
+ to: string;
66
+ kind: 'has-topic' | 'forked-from';
67
+ }
68
+ export interface GraphData {
69
+ nodes: GraphNode[];
70
+ edges: GraphEdge[];
71
+ }
72
+
54
73
  export interface UsageSummary {
55
74
  totals: { sessions: number; input_tokens: number; output_tokens: number; cost_usd: number };
56
75
  byAgent: Record<string, UsageBucket>;
@@ -308,6 +327,44 @@ export class SessionIndex {
308
327
  return (rows as any[]).map((r) => this.rowToIndex(r));
309
328
  }
310
329
 
330
+ /** Graph of sessions clustered by topic (+ fork lineage where known). */
331
+ graphData(): GraphData {
332
+ const rows = this.db
333
+ .prepare('SELECT session_id, agent, topic, intent, cost_usd, title FROM session ORDER BY started_at DESC')
334
+ .all() as any[];
335
+ const nodes: GraphNode[] = [];
336
+ const edges: GraphEdge[] = [];
337
+ const topicNodes = new Map<string, string>(); // topic -> node id
338
+ const topicId = (t: string) => {
339
+ let id = topicNodes.get(t);
340
+ if (!id) {
341
+ id = `topic:${topicNodes.size}`;
342
+ topicNodes.set(t, id);
343
+ nodes.push({ id, kind: 'topic', label: t, sessions: 0, cost_usd: 0 });
344
+ }
345
+ return id;
346
+ };
347
+ for (const r of rows) {
348
+ const sid = `s:${r.session_id}`;
349
+ nodes.push({
350
+ id: sid,
351
+ kind: 'session',
352
+ label: (r.topic || r.title || r.session_id).slice(0, 48),
353
+ agent: r.agent,
354
+ intent: r.intent ?? null,
355
+ cost_usd: r.cost_usd,
356
+ sessions: 1,
357
+ });
358
+ const topic = (r.topic || r.intent || 'misc').toString();
359
+ const tid = topicId(topic);
360
+ edges.push({ from: sid, to: tid, kind: 'has-topic' });
361
+ const tn = nodes.find((n) => n.id === tid)!;
362
+ tn.sessions += 1;
363
+ tn.cost_usd += r.cost_usd;
364
+ }
365
+ return { nodes, edges };
366
+ }
367
+
311
368
  /** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
312
369
  usageSummary(opts: { days?: number; topN?: number } = {}): UsageSummary {
313
370
  const rows = this.db