@unpolarize/code-sessions 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,13 +4,16 @@ 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,
13
15
  cmdStatus,
16
+ cmdUsage,
14
17
  envelopeFile,
15
18
  handleHookInput,
16
19
  insightsFile,
@@ -20,7 +23,7 @@ import {
20
23
  parseFlags,
21
24
  readStdin,
22
25
  startDaemon
23
- } from "./chunk-ZJG2DWAK.js";
26
+ } from "./chunk-ON3CPW4C.js";
24
27
 
25
28
  // src/analytics/command.ts
26
29
  import { mkdirSync, writeFileSync } from "fs";
@@ -231,6 +234,9 @@ async function main(argv) {
231
234
  })
232
235
  );
233
236
  break;
237
+ case "install-skills":
238
+ emit(cmdInstallSkills(typeof flags.agent === "string" ? { agent: flags.agent } : {}));
239
+ break;
234
240
  case "backfill":
235
241
  emit(
236
242
  await cmdBackfill(cfg, {
@@ -251,6 +257,9 @@ async function main(argv) {
251
257
  case "index":
252
258
  emit(cmdIndex(cfg));
253
259
  break;
260
+ case "usage":
261
+ emit(cmdUsage(cfg, { json: flags.json === true }));
262
+ break;
254
263
  case "query":
255
264
  emit(
256
265
  cmdQuery(cfg, {
@@ -264,6 +273,17 @@ async function main(argv) {
264
273
  emit(cmdSearch(cfg, { query: q, ...typeof flags.limit === "string" ? { limit: Number(flags.limit) } : {} }));
265
274
  break;
266
275
  }
276
+ case "fork": {
277
+ const sid = argv.slice(1).find((a) => !a.startsWith("--")) ?? "";
278
+ emit(
279
+ cmdFork(cfg, {
280
+ sessionId: sid,
281
+ atTurn: typeof flags.at === "string" ? Number(flags.at) : NaN,
282
+ ...typeof flags.id === "string" ? { newId: flags.id } : {}
283
+ })
284
+ );
285
+ break;
286
+ }
267
287
  case "hook": {
268
288
  try {
269
289
  const input = await readStdin();
package/dist/index.js CHANGED
@@ -9,25 +9,35 @@ import {
9
9
  StateStore,
10
10
  THRESHOLDS,
11
11
  applyHygiene,
12
+ buildClaudeSkill,
13
+ buildLabelSkillBody,
12
14
  buildMetricPayload,
13
15
  buildPrompt,
16
+ buildPromptFile,
14
17
  buildTracePayload,
15
18
  claudeRunner,
16
19
  cmdBackfill,
17
20
  cmdDoctor,
18
21
  cmdExport,
22
+ cmdFork,
19
23
  cmdIndex,
20
24
  cmdInit,
21
25
  cmdInstallHooks,
26
+ cmdInstallSkills,
22
27
  cmdQuery,
23
28
  cmdReindex,
24
29
  cmdSearch,
25
30
  cmdStatus,
31
+ cmdUsage,
32
+ codebuildSessionsRoot,
26
33
  codexSessionsRoot,
27
34
  computeEnvelope,
28
35
  defaultConfig,
36
+ deriveIntent,
37
+ deriveProjects,
29
38
  deriveSignals,
30
39
  deriveTags,
40
+ discoverCodebuildSessions,
31
41
  discoverCodexSessions,
32
42
  discoverGrokSessions,
33
43
  envelopeFile,
@@ -35,12 +45,14 @@ import {
35
45
  exportSession,
36
46
  exportStore,
37
47
  findTranscript,
48
+ forkSession,
38
49
  grokRunner,
39
50
  grokSessionsRoot,
40
51
  guessTopic,
41
52
  handleHookInput,
42
53
  insightsFile,
43
54
  installHooks,
55
+ installSkills,
44
56
  isSessionEndEvent,
45
57
  isoNano,
46
58
  labelSession,
@@ -52,6 +64,7 @@ import {
52
64
  monthOf,
53
65
  ollamaRunner,
54
66
  overridesFromFlags,
67
+ parseCodebuildSession,
55
68
  parseCodexSession,
56
69
  parseFlags,
57
70
  parseGrokSession,
@@ -59,6 +72,7 @@ import {
59
72
  parseLabelJson,
60
73
  postOtlp,
61
74
  priceFor,
75
+ projectIdFromPath,
62
76
  rawBlobFile,
63
77
  readEntries,
64
78
  readNewLines,
@@ -78,7 +92,7 @@ import {
78
92
  writeBlobFile,
79
93
  writeImportedSession,
80
94
  writeTurnFile
81
- } from "./chunk-ZJG2DWAK.js";
95
+ } from "./chunk-ON3CPW4C.js";
82
96
  export {
83
97
  CaptureEngine,
84
98
  DEFAULT_HOOK_EVENTS,
@@ -90,25 +104,35 @@ export {
90
104
  StateStore,
91
105
  THRESHOLDS,
92
106
  applyHygiene,
107
+ buildClaudeSkill,
108
+ buildLabelSkillBody,
93
109
  buildMetricPayload,
94
110
  buildPrompt,
111
+ buildPromptFile,
95
112
  buildTracePayload,
96
113
  claudeRunner,
97
114
  cmdBackfill,
98
115
  cmdDoctor,
99
116
  cmdExport,
117
+ cmdFork,
100
118
  cmdIndex,
101
119
  cmdInit,
102
120
  cmdInstallHooks,
121
+ cmdInstallSkills,
103
122
  cmdQuery,
104
123
  cmdReindex,
105
124
  cmdSearch,
106
125
  cmdStatus,
126
+ cmdUsage,
127
+ codebuildSessionsRoot,
107
128
  codexSessionsRoot,
108
129
  computeEnvelope,
109
130
  defaultConfig,
131
+ deriveIntent,
132
+ deriveProjects,
110
133
  deriveSignals,
111
134
  deriveTags,
135
+ discoverCodebuildSessions,
112
136
  discoverCodexSessions,
113
137
  discoverGrokSessions,
114
138
  envelopeFile,
@@ -116,12 +140,14 @@ export {
116
140
  exportSession,
117
141
  exportStore,
118
142
  findTranscript,
143
+ forkSession,
119
144
  grokRunner,
120
145
  grokSessionsRoot,
121
146
  guessTopic,
122
147
  handleHookInput,
123
148
  insightsFile,
124
149
  installHooks,
150
+ installSkills,
125
151
  isSessionEndEvent,
126
152
  isoNano,
127
153
  labelSession,
@@ -133,6 +159,7 @@ export {
133
159
  monthOf,
134
160
  ollamaRunner,
135
161
  overridesFromFlags,
162
+ parseCodebuildSession,
136
163
  parseCodexSession,
137
164
  parseFlags,
138
165
  parseGrokSession,
@@ -140,6 +167,7 @@ export {
140
167
  parseLabelJson,
141
168
  postOtlp,
142
169
  priceFor,
170
+ projectIdFromPath,
143
171
  rawBlobFile,
144
172
  readEntries,
145
173
  readNewLines,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unpolarize/code-sessions",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless, event-driven cross-agent session capture agent (daemon + CLI)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,14 +8,24 @@
8
8
  "code-sessions": "./bin/code-sessions.mjs"
9
9
  },
10
10
  "main": "./dist/index.js",
11
- "files": ["dist", "src", "bin"],
12
- "publishConfig": { "access": "public" },
13
- "repository": { "type": "git", "url": "git+https://github.com/unpolarize/code-sessions.git", "directory": "packages/agent" },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "bin"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/unpolarize/code-sessions.git",
22
+ "directory": "packages/agent"
23
+ },
14
24
  "scripts": {
15
25
  "build": "tsup src/index.ts src/cli.ts --format esm --clean --out-dir dist"
16
26
  },
17
27
  "dependencies": {
18
- "@unpolarize/code-sessions-schema": "^0.1.0",
28
+ "@unpolarize/code-sessions-schema": "^0.3.0",
19
29
  "zod": "^3.23.8"
20
30
  }
21
31
  }
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
4
4
  import { sessionDir, turnFile, envelopeFile } from '../store/paths';
5
5
  import { makeConfig, withTempDir } from '../test/tmp';
6
6
  import { discoverCodexSessions, parseCodexSession } from './codex';
7
+ import { discoverCodebuildSessions, parseCodebuildSession } from './codebuild';
7
8
  import { discoverGrokSessions, parseGrokSession } from './grok';
8
9
  import { writeImportedSession } from './import';
9
10
 
@@ -98,6 +99,41 @@ describe('codex adapter', () => {
98
99
  });
99
100
  });
100
101
 
102
+ function seedCodebuild(root: string): void {
103
+ mkdirSync(root, { recursive: true });
104
+ writeFileSync(
105
+ join(root, 'cb-1.jsonl'),
106
+ [
107
+ '{"type":"meta","meta":{"id":"cb-1","backend":"claude","title":"Plan MVP","cwd":"/Users/x/projects/foo"}}',
108
+ '{"type":"update","update":{"kind":"system_init","backendSessionId":"native-1"}}',
109
+ '{"type":"user","text":"add a feature"}',
110
+ '{"type":"update","update":{"kind":"agent_message_chunk","content":{"type":"text","text":"sure, "}}}',
111
+ '{"type":"update","update":{"kind":"agent_message_chunk","content":{"type":"text","text":"editing"}}}',
112
+ '{"type":"update","update":{"kind":"tool_call","toolCall":{"toolCallId":"t1","title":"Edit","rawInput":{"file_path":"a.ts"}}}}',
113
+ '{"type":"update","update":{"kind":"result","stopReason":"success","usage":{"inputTokens":1000,"outputTokens":40,"costUsd":0.25}}}',
114
+ ].join('\n'),
115
+ );
116
+ }
117
+
118
+ describe('codebuild adapter', () => {
119
+ it('folds CB stream into user/assistant turns with usage', () => {
120
+ withTempDir((root) => {
121
+ seedCodebuild(root);
122
+ const found = discoverCodebuildSessions(root);
123
+ expect(found).toHaveLength(1);
124
+ const imported = parseCodebuildSession(found[0]!, 'test-host')!;
125
+ expect(imported.agent).toBe('claude-code'); // backend=claude -> claude-code
126
+ expect(imported.sessionId).toBe('cb-1');
127
+ expect(imported.meta.title).toBe('Plan MVP');
128
+ expect(imported.turns.map((t) => t.role)).toEqual(['user', 'assistant']);
129
+ expect(imported.turns[1]!.text).toBe('sure, editing'); // chunks concatenated
130
+ expect(imported.turns[1]!.tool_calls[0]).toMatchObject({ name: 'Edit' });
131
+ expect(imported.turns[1]!.usage.input_tokens).toBe(1000);
132
+ expect(imported.turns[1]!.telemetry?.cost_usd).toBe(0.25);
133
+ });
134
+ });
135
+ });
136
+
101
137
  describe('writeImportedSession', () => {
102
138
  it('writes per-turn files + envelope for an imported session', () => {
103
139
  withTempDir((store) => {
@@ -0,0 +1,188 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ SCHEMA_VERSIONS,
6
+ type AgentKind,
7
+ type ClaudeSessionMeta,
8
+ type ToolCall,
9
+ type Turn,
10
+ } from '@unpolarize/code-sessions-schema';
11
+ import type { ImportedSession } from './import';
12
+
13
+ /**
14
+ * Code Build VSCode (CB) adapter. CB stores each chat at
15
+ * ~/.codebuild/sessions/<uuid>.jsonl as a stream of:
16
+ * {type:"meta", meta:{id, backend, title, cwd}}
17
+ * {type:"user", text}
18
+ * {type:"update", update:{kind:"agent_message_chunk", content:{type:"text",text}}}
19
+ * {type:"update", update:{kind:"tool_call", toolCall:{title, rawInput}}}
20
+ * {type:"update", update:{kind:"usage"|"result", usage:{...}}}
21
+ * Events carry no per-line timestamp; synthesized from file mtime + ordinal.
22
+ *
23
+ * Importing CB sessions into the CS store is how "CB context management uses CS":
24
+ * every CB turn becomes discoverable in the shared sessions store + index, and a
25
+ * CB switch/fork can be expressed as a CS forkSession on the persisted session.
26
+ */
27
+
28
+ export function codebuildSessionsRoot(): string {
29
+ return join(homedir(), '.codebuild', 'sessions');
30
+ }
31
+
32
+ export interface CodebuildSessionInfo {
33
+ sessionId: string;
34
+ path: string;
35
+ }
36
+
37
+ export function discoverCodebuildSessions(root = codebuildSessionsRoot()): CodebuildSessionInfo[] {
38
+ if (!existsSync(root)) return [];
39
+ let files: string[];
40
+ try {
41
+ files = readdirSync(root).filter((f) => f.endsWith('.jsonl'));
42
+ } catch {
43
+ return [];
44
+ }
45
+ return files.map((f) => ({ sessionId: f.replace(/\.jsonl$/, ''), path: join(root, f) }));
46
+ }
47
+
48
+ function backendToAgent(backend: unknown): AgentKind {
49
+ if (backend === 'claude') return 'claude-code';
50
+ if (backend === 'grok') return 'grok';
51
+ if (backend === 'codex') return 'codex';
52
+ return 'unknown';
53
+ }
54
+
55
+ interface Pending {
56
+ text: string;
57
+ tools: ToolCall[];
58
+ input_tokens: number;
59
+ output_tokens: number;
60
+ cache_read_tokens: number;
61
+ cost_usd: number;
62
+ }
63
+
64
+ export function parseCodebuildSession(info: CodebuildSessionInfo, host: string): ImportedSession | null {
65
+ let raw = '';
66
+ try {
67
+ raw = readFileSync(info.path, 'utf8');
68
+ } catch {
69
+ return null;
70
+ }
71
+ const lines = raw.split('\n').filter((l) => l.trim().length > 0);
72
+ const baseMs = statMtime(info.path);
73
+
74
+ let agent: AgentKind = 'unknown';
75
+ let sessionId = info.sessionId;
76
+ let title: string | undefined;
77
+ let cwd: string | undefined;
78
+
79
+ const turns: Turn[] = [];
80
+ let pending: Pending | null = null;
81
+ let idx = 0;
82
+
83
+ const flush = (): void => {
84
+ if (!pending) return;
85
+ const p = pending;
86
+ pending = null;
87
+ const turn = mkTurn(sessionId, host, agent, idx++, baseMs, 'assistant', p.text, p.tools);
88
+ turn.usage = {
89
+ input_tokens: p.input_tokens,
90
+ output_tokens: p.output_tokens,
91
+ cache_read_tokens: p.cache_read_tokens,
92
+ cache_write_tokens: 0,
93
+ };
94
+ if (p.cost_usd > 0) turn.telemetry = { cost_usd: p.cost_usd };
95
+ turns.push(turn);
96
+ };
97
+ const ensurePending = (): Pending => {
98
+ if (!pending) pending = { text: '', tools: [], input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cost_usd: 0 };
99
+ return pending;
100
+ };
101
+
102
+ for (const line of lines) {
103
+ let ev: any;
104
+ try {
105
+ ev = JSON.parse(line);
106
+ } catch {
107
+ continue;
108
+ }
109
+ if (ev.type === 'meta' && ev.meta) {
110
+ agent = backendToAgent(ev.meta.backend);
111
+ if (typeof ev.meta.id === 'string') sessionId = ev.meta.id;
112
+ if (typeof ev.meta.title === 'string') title = ev.meta.title;
113
+ if (typeof ev.meta.cwd === 'string') cwd = ev.meta.cwd;
114
+ continue;
115
+ }
116
+ if (ev.type === 'user' && typeof ev.text === 'string') {
117
+ flush();
118
+ turns.push(mkTurn(sessionId, host, agent, idx++, baseMs, 'user', ev.text, []));
119
+ continue;
120
+ }
121
+ if (ev.type === 'update' && ev.update) {
122
+ const u = ev.update;
123
+ if (u.kind === 'agent_message_chunk') {
124
+ const t = u.content?.text;
125
+ if (typeof t === 'string') ensurePending().text += t;
126
+ } else if (u.kind === 'tool_call' && u.toolCall) {
127
+ const p = ensurePending();
128
+ p.tools.push({ name: String(u.toolCall.title ?? u.toolCall.kind ?? 'tool'), input: u.toolCall.rawInput });
129
+ } else if (u.kind === 'usage' && u.usage) {
130
+ const p = ensurePending();
131
+ p.input_tokens += Number(u.usage.inputTokens) || 0;
132
+ p.output_tokens += Number(u.usage.outputTokens) || 0;
133
+ p.cache_read_tokens += Number(u.usage.cacheReadTokens) || 0;
134
+ } else if (u.kind === 'result' && u.usage) {
135
+ const p = ensurePending();
136
+ p.input_tokens += Number(u.usage.inputTokens) || 0;
137
+ p.output_tokens += Number(u.usage.outputTokens) || 0;
138
+ p.cost_usd += Number(u.usage.costUsd) || 0;
139
+ flush();
140
+ }
141
+ }
142
+ }
143
+ flush();
144
+
145
+ if (turns.length === 0) return null;
146
+ const meta: ClaudeSessionMeta = {
147
+ session_id: sessionId,
148
+ started_at: turns[0]!.ts,
149
+ ended_at: turns[turns.length - 1]!.ts,
150
+ };
151
+ if (cwd) meta.project_path = cwd;
152
+ if (title) meta.title = title;
153
+ return { host, sessionId, agent, turns, meta };
154
+ }
155
+
156
+ function statMtime(p: string): number {
157
+ try {
158
+ return statSync(p).mtimeMs;
159
+ } catch {
160
+ return Date.parse('2020-01-01T00:00:00Z');
161
+ }
162
+ }
163
+
164
+ function mkTurn(
165
+ sessionId: string,
166
+ host: string,
167
+ agent: AgentKind,
168
+ index: number,
169
+ baseMs: number,
170
+ role: Turn['role'],
171
+ text: string,
172
+ tool_calls: ToolCall[],
173
+ ): Turn {
174
+ return {
175
+ schema: SCHEMA_VERSIONS.turn,
176
+ session_id: sessionId,
177
+ host,
178
+ agent,
179
+ turn_index: index,
180
+ ts: new Date(baseMs + index * 1000).toISOString(),
181
+ role,
182
+ text,
183
+ tool_calls,
184
+ usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
185
+ scrubbed: false,
186
+ raw_ref: null,
187
+ };
188
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './import';
2
2
  export * from './grok';
3
3
  export * from './codex';
4
+ export * from './codebuild';
package/src/cli.ts CHANGED
@@ -4,13 +4,16 @@ 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,
13
15
  cmdStatus,
16
+ cmdUsage,
14
17
  startDaemon,
15
18
  type CommandResult,
16
19
  } from './commands';
@@ -45,11 +48,14 @@ export async function main(argv: string[]): Promise<void> {
45
48
  }),
46
49
  );
47
50
  break;
51
+ case 'install-skills':
52
+ emit(cmdInstallSkills(typeof flags.agent === 'string' ? { agent: flags.agent as 'claude' | 'codex' | 'grok' | 'all' } : {}));
53
+ break;
48
54
  case 'backfill':
49
55
  emit(
50
56
  await cmdBackfill(cfg, {
51
57
  ...(typeof flags.projects === 'string' ? { projectsDir: flags.projects } : {}),
52
- ...(typeof flags.agent === 'string' ? { agent: flags.agent as 'claude' | 'grok' | 'codex' | 'all' } : {}),
58
+ ...(typeof flags.agent === 'string' ? { agent: flags.agent as 'claude' | 'grok' | 'codex' | 'codebuild' | 'all' } : {}),
53
59
  }),
54
60
  );
55
61
  break;
@@ -65,6 +71,9 @@ export async function main(argv: string[]): Promise<void> {
65
71
  case 'index':
66
72
  emit(cmdIndex(cfg));
67
73
  break;
74
+ case 'usage':
75
+ emit(cmdUsage(cfg, { json: flags.json === true }));
76
+ break;
68
77
  case 'query':
69
78
  emit(
70
79
  cmdQuery(cfg, {
@@ -78,6 +87,17 @@ export async function main(argv: string[]): Promise<void> {
78
87
  emit(cmdSearch(cfg, { query: q, ...(typeof flags.limit === 'string' ? { limit: Number(flags.limit) } : {}) }));
79
88
  break;
80
89
  }
90
+ case 'fork': {
91
+ const sid = argv.slice(1).find((a) => !a.startsWith('--')) ?? '';
92
+ emit(
93
+ cmdFork(cfg, {
94
+ sessionId: sid,
95
+ atTurn: typeof flags.at === 'string' ? Number(flags.at) : NaN,
96
+ ...(typeof flags.id === 'string' ? { newId: flags.id } : {}),
97
+ }),
98
+ );
99
+ break;
100
+ }
81
101
  case 'hook': {
82
102
  // Never fail the agent: swallow everything, always exit 0.
83
103
  try {
package/src/cliargs.ts CHANGED
@@ -51,13 +51,16 @@ Commands:
51
51
  init Initialize the git-backed store (~/.sessions)
52
52
  start Run the capture daemon (foreground)
53
53
  install-hooks Install Claude Code hooks that feed the daemon
54
+ install-skills Install the cs-label-session skill into agents [--agent claude|codex|grok|all]
54
55
  hook (internal) forward a hook payload from stdin to the daemon
55
- backfill Import existing transcripts into the store [--agent claude|grok|codex|all]
56
+ backfill Import existing transcripts into the store [--agent claude|grok|codex|codebuild|all]
56
57
  reindex (Re)derive insights for stored sessions [--since YYYY-MM]
57
58
  export Export stored sessions as OTLP to a collector [--since YYYY-MM]
58
59
  index (Re)build the internal SQLite index from the git store
59
60
  query List recent sessions from the index [--limit N] [--agent X]
61
+ usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
60
62
  search Full-text search session turns <text> [--limit N]
63
+ fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
61
64
  analytics Compute MVP-2 rollups + digest into analytics/
62
65
  status Show daemon/store status
63
66
  doctor Environment checks
package/src/commands.ts CHANGED
@@ -12,9 +12,12 @@ import { installHooks } from './hooks/install';
12
12
  import { exportSession, exportStore } from './telemetry/exporter';
13
13
  import { discoverGrokSessions, parseGrokSession } from './adapters/grok';
14
14
  import { discoverCodexSessions, parseCodexSession } from './adapters/codex';
15
+ import { discoverCodebuildSessions, parseCodebuildSession } from './adapters/codebuild';
15
16
  import { writeImportedSession } from './adapters/import';
16
17
  import { SessionIndex, type SessionIndexRow } from './index_store/db';
17
18
  import { syncIndex } from './index_store/sync';
19
+ import { installSkills, type SkillAgent } from './skills/install';
20
+ import { forkSession } from './fork';
18
21
 
19
22
  export interface CommandResult {
20
23
  code: number;
@@ -80,7 +83,7 @@ export function cmdStatus(cfg: CodeSessionsConfig): CommandResult {
80
83
  return { code: 0, output: lines.join('\n') };
81
84
  }
82
85
 
83
- export type BackfillAgent = 'claude' | 'grok' | 'codex' | 'all';
86
+ export type BackfillAgent = 'claude' | 'grok' | 'codex' | 'codebuild' | 'all';
84
87
 
85
88
  export async function cmdBackfill(
86
89
  cfg: CodeSessionsConfig,
@@ -132,6 +135,21 @@ export async function cmdBackfill(
132
135
  parts.push(`codex: ${n} sessions / ${t} turns`);
133
136
  }
134
137
 
138
+ if (agent === 'codebuild' || agent === 'all') {
139
+ const found = discoverCodebuildSessions();
140
+ let n = 0;
141
+ let t = 0;
142
+ for (const info of found) {
143
+ const imported = parseCodebuildSession(info, cfg.host);
144
+ if (!imported) continue;
145
+ t += writeImportedSession(cfg, imported).turns;
146
+ n++;
147
+ }
148
+ sessions += n;
149
+ turns += t;
150
+ parts.push(`codebuild: ${n} sessions / ${t} turns`);
151
+ }
152
+
135
153
  const git = gitStoreFor(cfg);
136
154
  git.init();
137
155
  git.commit(`backfill (${agent}): ${sessions} sessions`);
@@ -192,6 +210,55 @@ export async function cmdExport(
192
210
  };
193
211
  }
194
212
 
213
+ export function cmdFork(
214
+ cfg: CodeSessionsConfig,
215
+ opts: { sessionId: string; atTurn: number; newId?: string },
216
+ ): CommandResult {
217
+ if (!opts.sessionId || Number.isNaN(opts.atTurn)) {
218
+ return { code: 1, output: 'usage: code-sessions fork <session-id> --at <turn> [--id <new-id>]' };
219
+ }
220
+ try {
221
+ const res = forkSession(cfg, {
222
+ sessionId: opts.sessionId,
223
+ atTurn: opts.atTurn,
224
+ ...(opts.newId ? { newSessionId: opts.newId } : {}),
225
+ });
226
+ const git = gitStoreFor(cfg);
227
+ if (git.isRepo()) git.commit(`fork ${opts.sessionId}@${opts.atTurn} -> ${res.newSessionId}`);
228
+ return {
229
+ code: 0,
230
+ output: `Forked ${opts.sessionId} at turn ${opts.atTurn} → ${res.newSessionId} (${res.turns} turns) at ${res.sessionDir}`,
231
+ };
232
+ } catch (e) {
233
+ return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
234
+ }
235
+ }
236
+
237
+ /** Aggregated usage from the CS index (totals/byAgent/byDay/byProject/topByCost). */
238
+ export function cmdUsage(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
239
+ syncIndex(cfg); // ensure the index reflects the current store
240
+ const index = new SessionIndex(cfg.indexPath);
241
+ try {
242
+ const u = index.usageSummary();
243
+ if (opts.json) return { code: 0, output: JSON.stringify(u) };
244
+ const lines = [
245
+ `# usage — ${u.totals.sessions} sessions · ${u.totals.input_tokens.toLocaleString()} in / ${u.totals.output_tokens.toLocaleString()} out · $${u.totals.cost_usd.toFixed(2)}`,
246
+ 'by agent:',
247
+ ...Object.entries(u.byAgent).map(([a, b]) => ` ${a.padEnd(12)} ${b.sessions} sess $${b.cost_usd.toFixed(2)}`),
248
+ 'top sessions by cost:',
249
+ ...u.topByCost.slice(0, 5).map((t) => ` $${t.cost_usd.toFixed(2).padStart(8)} ${t.agent.padEnd(12)} ${t.label.slice(0, 50)}`),
250
+ ];
251
+ return { code: 0, output: lines.join('\n') };
252
+ } finally {
253
+ index.close();
254
+ }
255
+ }
256
+
257
+ export function cmdInstallSkills(opts: { agent?: SkillAgent } = {}): CommandResult {
258
+ const res = installSkills(opts.agent ? { agent: opts.agent } : {});
259
+ return { code: 0, output: `Installed cs-label-session skill:\n ${res.installed.join('\n ')}` };
260
+ }
261
+
195
262
  export function cmdIndex(cfg: CodeSessionsConfig): CommandResult {
196
263
  const stats = syncIndex(cfg);
197
264
  return {
@@ -205,8 +272,9 @@ function fmtRow(r: SessionIndexRow): string {
205
272
  const agent = (r.agent || '?').padEnd(11).slice(0, 11);
206
273
  const tok = String(r.input_tokens + r.output_tokens).padStart(8);
207
274
  const cost = `$${r.cost_usd.toFixed(2)}`.padStart(8);
208
- const title = (r.topic || r.title || r.session_id).slice(0, 48);
209
- return `${date} ${agent} ${tok} ${cost} ${title}`;
275
+ const intent = (r.intent || '·').padEnd(8).slice(0, 8);
276
+ const title = (r.topic || r.title || r.session_id).slice(0, 44);
277
+ return `${date} ${agent} ${intent} ${tok} ${cost} ${title}`;
210
278
  }
211
279
 
212
280
  export function cmdQuery(