@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/{chunk-ZJG2DWAK.js → chunk-ON3CPW4C.js} +529 -45
- package/dist/cli.js +21 -1
- package/dist/index.js +29 -1
- package/package.json +15 -5
- package/src/adapters/adapters.test.ts +36 -0
- package/src/adapters/codebuild.ts +188 -0
- package/src/adapters/index.ts +1 -0
- package/src/cli.ts +21 -1
- package/src/cliargs.ts +4 -1
- package/src/commands.ts +71 -3
- package/src/fork.test.ts +80 -0
- package/src/fork.ts +91 -0
- package/src/index.ts +2 -0
- package/src/index_store/db.test.ts +25 -0
- package/src/index_store/db.ts +111 -8
- package/src/index_store/sync.ts +6 -0
- package/src/insights/heuristics.test.ts +23 -1
- package/src/insights/heuristics.ts +48 -1
- package/src/insights/labeler.test.ts +4 -1
- package/src/insights/labeler.ts +18 -5
- package/src/insights/llm.test.ts +1 -1
- package/src/insights/llm.ts +13 -5
- package/src/insights/provider.ts +7 -2
- package/src/skills/index.ts +2 -0
- package/src/skills/install.ts +52 -0
- package/src/skills/skills.test.ts +42 -0
- package/src/skills/templates.ts +48 -0
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-
|
|
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-
|
|
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.
|
|
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": [
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/adapters/index.ts
CHANGED
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
|
|
209
|
-
|
|
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(
|