@spool-lab/core 0.5.2 → 0.6.1
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/README.md +22 -14
- package/dist/db/db.d.ts +2 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +106 -15
- package/dist/db/db.js.map +1 -1
- package/dist/db/native-binding.d.ts +21 -0
- package/dist/db/native-binding.d.ts.map +1 -0
- package/dist/db/native-binding.js +32 -0
- package/dist/db/native-binding.js.map +1 -0
- package/dist/db/published-shares-cache.d.ts +39 -0
- package/dist/db/published-shares-cache.d.ts.map +1 -0
- package/dist/db/published-shares-cache.js +60 -0
- package/dist/db/published-shares-cache.js.map +1 -0
- package/dist/db/queries.d.ts +5 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +219 -80
- package/dist/db/queries.js.map +1 -1
- package/dist/db/search-query.d.ts +7 -0
- package/dist/db/search-query.d.ts.map +1 -1
- package/dist/db/search-query.js +33 -7
- package/dist/db/search-query.js.map +1 -1
- package/dist/db/share-drafts.d.ts.map +1 -1
- package/dist/db/share-drafts.js +1 -3
- package/dist/db/share-drafts.js.map +1 -1
- package/dist/doctor/checks/config.d.ts.map +1 -1
- package/dist/doctor/checks/config.js +3 -3
- package/dist/doctor/checks/config.js.map +1 -1
- package/dist/doctor/checks/db.d.ts.map +1 -1
- package/dist/doctor/checks/db.js +36 -18
- package/dist/doctor/checks/db.js.map +1 -1
- package/dist/doctor/checks/env.d.ts.map +1 -1
- package/dist/doctor/checks/env.js +5 -1
- package/dist/doctor/checks/env.js.map +1 -1
- package/dist/doctor/checks/index.js +2 -2
- package/dist/doctor/checks/index.js.map +1 -1
- package/dist/doctor/checks/native.d.ts.map +1 -1
- package/dist/doctor/checks/native.js.map +1 -1
- package/dist/doctor/checks/versions.d.ts.map +1 -1
- package/dist/doctor/checks/versions.js +2 -2
- package/dist/doctor/checks/versions.js.map +1 -1
- package/dist/doctor/preflight.js +2 -2
- package/dist/doctor/preflight.js.map +1 -1
- package/dist/doctor/runner.d.ts.map +1 -1
- package/dist/doctor/runner.js +1 -1
- package/dist/doctor/runner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/migrations/agent-search-cleanup.d.ts.map +1 -1
- package/dist/migrations/agent-search-cleanup.js +10 -6
- package/dist/migrations/agent-search-cleanup.js.map +1 -1
- package/dist/migrations/worktree-identity-upgrade.d.ts +5 -3
- package/dist/migrations/worktree-identity-upgrade.d.ts.map +1 -1
- package/dist/migrations/worktree-identity-upgrade.js +26 -3
- package/dist/migrations/worktree-identity-upgrade.js.map +1 -1
- package/dist/observability/exporter-file.d.ts.map +1 -1
- package/dist/observability/exporter-file.js +14 -6
- package/dist/observability/exporter-file.js.map +1 -1
- package/dist/observability/exporter-pretty.d.ts.map +1 -1
- package/dist/observability/exporter-pretty.js +13 -7
- package/dist/observability/exporter-pretty.js.map +1 -1
- package/dist/observability/layer.d.ts.map +1 -1
- package/dist/observability/layer.js +1 -1
- package/dist/observability/layer.js.map +1 -1
- package/dist/parsers/claude.d.ts.map +1 -1
- package/dist/parsers/claude.js +4 -144
- package/dist/parsers/claude.js.map +1 -1
- package/dist/parsers/codex.d.ts +7 -1
- package/dist/parsers/codex.d.ts.map +1 -1
- package/dist/parsers/codex.js +43 -186
- package/dist/parsers/codex.js.map +1 -1
- package/dist/parsers/gemini.d.ts.map +1 -1
- package/dist/parsers/gemini.js +113 -13
- package/dist/parsers/gemini.js.map +1 -1
- package/dist/parsers/opencode.d.ts.map +1 -1
- package/dist/parsers/opencode.js +67 -33
- package/dist/parsers/opencode.js.map +1 -1
- package/dist/parsers/pi.d.ts +18 -0
- package/dist/parsers/pi.d.ts.map +1 -0
- package/dist/parsers/pi.js +155 -0
- package/dist/parsers/pi.js.map +1 -0
- package/dist/parsers/spool-prelude.d.ts +1 -15
- package/dist/parsers/spool-prelude.d.ts.map +1 -1
- package/dist/parsers/spool-prelude.js +3 -20
- package/dist/parsers/spool-prelude.js.map +1 -1
- package/dist/projects/fs.d.ts.map +1 -1
- package/dist/projects/fs.js +1 -1
- package/dist/projects/fs.js.map +1 -1
- package/dist/projects/groups.d.ts +4 -1
- package/dist/projects/groups.d.ts.map +1 -1
- package/dist/projects/groups.js +32 -11
- package/dist/projects/groups.js.map +1 -1
- package/dist/projects/identity-synthesizers.d.ts.map +1 -1
- package/dist/projects/identity-synthesizers.js.map +1 -1
- package/dist/projects/identity.d.ts +2 -2
- package/dist/projects/identity.d.ts.map +1 -1
- package/dist/projects/identity.js +24 -13
- package/dist/projects/identity.js.map +1 -1
- package/dist/projects/sessions.d.ts.map +1 -1
- package/dist/projects/sessions.js +11 -10
- package/dist/projects/sessions.js.map +1 -1
- package/dist/projects/worktree-resolvers.d.ts +3 -0
- package/dist/projects/worktree-resolvers.d.ts.map +1 -1
- package/dist/projects/worktree-resolvers.js +144 -8
- package/dist/projects/worktree-resolvers.js.map +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/security/maintenance.d.ts.map +1 -1
- package/dist/security/maintenance.js +1 -1
- package/dist/security/maintenance.js.map +1 -1
- package/dist/security/profile.d.ts.map +1 -1
- package/dist/security/profile.js +6 -4
- package/dist/security/profile.js.map +1 -1
- package/dist/security/purge.d.ts +2 -2
- package/dist/security/purge.d.ts.map +1 -1
- package/dist/security/purge.js +31 -15
- package/dist/security/purge.js.map +1 -1
- package/dist/security/repo.d.ts +1 -1
- package/dist/security/repo.d.ts.map +1 -1
- package/dist/security/repo.js +64 -37
- package/dist/security/repo.js.map +1 -1
- package/dist/security/scan.d.ts +2 -2
- package/dist/security/scan.d.ts.map +1 -1
- package/dist/security/scan.js +6 -4
- package/dist/security/scan.js.map +1 -1
- package/dist/security/worker.d.ts +2 -2
- package/dist/security/worker.d.ts.map +1 -1
- package/dist/security/worker.js +5 -11
- package/dist/security/worker.js.map +1 -1
- package/dist/sync/source-paths.d.ts.map +1 -1
- package/dist/sync/source-paths.js +46 -16
- package/dist/sync/source-paths.js.map +1 -1
- package/dist/sync/syncer.d.ts.map +1 -1
- package/dist/sync/syncer.js +112 -80
- package/dist/sync/syncer.js.map +1 -1
- package/dist/sync/watcher.d.ts +1 -1
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +17 -4
- package/dist/sync/watcher.js.map +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/util/resolve-bin.d.ts.map +1 -1
- package/dist/util/resolve-bin.js +40 -25
- package/dist/util/resolve-bin.js.map +1 -1
- package/package.json +35 -17
package/dist/parsers/claude.js
CHANGED
|
@@ -1,111 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { parseClaudeSessionText } from '@spool-lab/session-kit';
|
|
3
|
+
// The parsing brain lives in @spool-lab/session-kit (browser-safe, shared
|
|
4
|
+
// with the web reader); this wrapper owns only the file I/O.
|
|
2
5
|
export function loadClaudeSession(filePath) {
|
|
3
|
-
|
|
4
|
-
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
5
|
-
const messages = [];
|
|
6
|
-
let sessionUuid = '';
|
|
7
|
-
let cwd = '';
|
|
8
|
-
let model = '';
|
|
9
|
-
let customTitle = '';
|
|
10
|
-
const SKIP_TYPES = new Set([
|
|
11
|
-
'file-history-snapshot',
|
|
12
|
-
'progress',
|
|
13
|
-
'queue-operation',
|
|
14
|
-
'last-prompt',
|
|
15
|
-
]);
|
|
16
|
-
for (const line of lines) {
|
|
17
|
-
let record;
|
|
18
|
-
try {
|
|
19
|
-
record = JSON.parse(line);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const type = record['type'];
|
|
25
|
-
if (!type || SKIP_TYPES.has(type))
|
|
26
|
-
continue;
|
|
27
|
-
if (!sessionUuid && record['sessionId'])
|
|
28
|
-
sessionUuid = record['sessionId'];
|
|
29
|
-
if (!cwd && record['cwd'])
|
|
30
|
-
cwd = record['cwd'];
|
|
31
|
-
if (type === 'custom-title') {
|
|
32
|
-
const ct = record['customTitle'];
|
|
33
|
-
if (ct)
|
|
34
|
-
customTitle = ct;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (type === 'assistant') {
|
|
38
|
-
const msg = record['message'];
|
|
39
|
-
if (msg?.['model'])
|
|
40
|
-
model = msg['model'];
|
|
41
|
-
}
|
|
42
|
-
if (type === 'summary') {
|
|
43
|
-
const summaryText = record['summary'];
|
|
44
|
-
if (summaryText) {
|
|
45
|
-
messages.push({
|
|
46
|
-
uuid: record['uuid'] ?? `summary-${messages.length}`,
|
|
47
|
-
parentUuid: record['parentUuid'] ?? null,
|
|
48
|
-
role: 'system',
|
|
49
|
-
contentText: summaryText.trim(),
|
|
50
|
-
timestamp: record['timestamp'],
|
|
51
|
-
isSidechain: Boolean(record['isSidechain']),
|
|
52
|
-
toolNames: [],
|
|
53
|
-
seq: messages.length,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const msgObj = record['message'];
|
|
59
|
-
if (!msgObj)
|
|
60
|
-
continue;
|
|
61
|
-
const role = msgObj['role'];
|
|
62
|
-
if (role !== 'user' && role !== 'assistant')
|
|
63
|
-
continue;
|
|
64
|
-
const contentRaw = msgObj['content'];
|
|
65
|
-
const contentText = extractText(contentRaw);
|
|
66
|
-
const toolNames = extractToolNames(contentRaw);
|
|
67
|
-
// Skip empty messages (e.g. tool result placeholders with no text)
|
|
68
|
-
if (!contentText && toolNames.length === 0)
|
|
69
|
-
continue;
|
|
70
|
-
messages.push({
|
|
71
|
-
uuid: record['uuid'] ?? `msg-${messages.length}`,
|
|
72
|
-
parentUuid: record['parentUuid'] ?? null,
|
|
73
|
-
role: role,
|
|
74
|
-
contentText,
|
|
75
|
-
timestamp: record['timestamp'],
|
|
76
|
-
isSidechain: Boolean(record['isSidechain']),
|
|
77
|
-
toolNames,
|
|
78
|
-
seq: messages.length,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (messages.length === 0)
|
|
82
|
-
return { kind: 'skipped' };
|
|
83
|
-
// Use cwd from messages if not in top-level fields
|
|
84
|
-
if (!cwd) {
|
|
85
|
-
for (const m of messages) {
|
|
86
|
-
// cwd is on the record level, not message level — already captured above
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const firstUserMsg = messages.find(m => m.role === 'user' && m.contentText.length > 0 && !m.isSidechain);
|
|
90
|
-
const title = customTitle
|
|
91
|
-
|| (firstUserMsg
|
|
92
|
-
? firstUserMsg.contentText.replace(/<[^>]+>/g, '').trim().slice(0, 120)
|
|
93
|
-
: '(no title)');
|
|
94
|
-
const timestamps = messages.map(m => m.timestamp).filter(Boolean).sort();
|
|
95
|
-
return {
|
|
96
|
-
kind: 'parsed',
|
|
97
|
-
session: {
|
|
98
|
-
source: 'claude',
|
|
99
|
-
sessionUuid: sessionUuid || filePath,
|
|
100
|
-
filePath,
|
|
101
|
-
title,
|
|
102
|
-
cwd,
|
|
103
|
-
model,
|
|
104
|
-
startedAt: timestamps[0] ?? new Date().toISOString(),
|
|
105
|
-
endedAt: timestamps[timestamps.length - 1] ?? new Date().toISOString(),
|
|
106
|
-
messages,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
6
|
+
return parseClaudeSessionText(readFileSync(filePath, 'utf8'), filePath);
|
|
109
7
|
}
|
|
110
8
|
export function parseClaudeSession(filePath) {
|
|
111
9
|
try {
|
|
@@ -116,44 +14,6 @@ export function parseClaudeSession(filePath) {
|
|
|
116
14
|
return null;
|
|
117
15
|
}
|
|
118
16
|
}
|
|
119
|
-
// Slash-command records in Claude Code JSONL come as a triplet:
|
|
120
|
-
// <command-name>/X</command-name>
|
|
121
|
-
// <command-message>X</command-message>
|
|
122
|
-
// <command-args>Y</command-args>
|
|
123
|
-
// Strip the whole record as one unit so bare <command-args> appearing in
|
|
124
|
-
// legitimate user content (e.g. a user pasting log output that contains
|
|
125
|
-
// these tags) is preserved.
|
|
126
|
-
const SLASH_COMMAND_RECORD = /<command-name>[\s\S]*?<\/command-name>(?:\s*<command-message>[\s\S]*?<\/command-message>)?(?:\s*<command-args>[\s\S]*?<\/command-args>)?/g;
|
|
127
|
-
function extractText(content) {
|
|
128
|
-
let raw;
|
|
129
|
-
if (typeof content === 'string') {
|
|
130
|
-
raw = content;
|
|
131
|
-
}
|
|
132
|
-
else if (Array.isArray(content)) {
|
|
133
|
-
raw = content
|
|
134
|
-
.filter(item => item.type === 'text')
|
|
135
|
-
.map(item => item.text ?? '')
|
|
136
|
-
.join('\n');
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
return '';
|
|
140
|
-
}
|
|
141
|
-
return raw
|
|
142
|
-
.replace(/<spool-system-prelude>[\s\S]*?<\/spool-system-prelude>/g, '')
|
|
143
|
-
.replace(SLASH_COMMAND_RECORD, '')
|
|
144
|
-
.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
|
|
145
|
-
.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '')
|
|
146
|
-
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
147
|
-
.replace(/<[^>]+>/g, '')
|
|
148
|
-
.trim();
|
|
149
|
-
}
|
|
150
|
-
function extractToolNames(content) {
|
|
151
|
-
if (!Array.isArray(content))
|
|
152
|
-
return [];
|
|
153
|
-
return content
|
|
154
|
-
.filter(item => item.type === 'tool_use' && item.name)
|
|
155
|
-
.map(item => item.name);
|
|
156
|
-
}
|
|
157
17
|
/** Decode a Claude project slug to a display path.
|
|
158
18
|
* e.g. '-Users-claw-code-spool' → '/Users/claw/code/spool'
|
|
159
19
|
* Note: lossy for paths containing hyphens — prefer cwd from session records.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/parsers/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/parsers/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAI/D,0EAA0E;AAC1E,6DAA6D;AAE7D,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,sBAAsB,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAA;AACzE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAA;QAC1C,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACtC,OAAO,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;AAC/C,CAAC"}
|
package/dist/parsers/codex.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ParseSessionResult, ParsedSession } from '../types.js';
|
|
2
|
-
export declare const CODEX_INDEX_VERSION = "codex-
|
|
2
|
+
export declare const CODEX_INDEX_VERSION = "codex-v6-project-identity-from-session-git-remote";
|
|
3
3
|
export declare function loadCodexSession(filePath: string): ParseSessionResult;
|
|
4
4
|
export declare function parseCodexSession(filePath: string): ParsedSession | null;
|
|
5
|
+
/**
|
|
6
|
+
* Read only the leading metadata records needed for project identity repair.
|
|
7
|
+
* Codex writes session_meta at the start of each rollout, so this avoids
|
|
8
|
+
* parsing a potentially huge historical transcript during DB startup.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadCodexSessionGitRemote(filePath: string): string | null;
|
|
5
11
|
//# sourceMappingURL=codex.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/parsers/codex.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/parsers/codex.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAKpE,eAAO,MAAM,mBAAmB,sDAAsD,CAAA;AAItF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAKrE;AA6BD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAOxE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBzE"}
|
package/dist/parsers/codex.js
CHANGED
|
@@ -1,181 +1,16 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync } from 'node:fs';
|
|
2
|
-
import { basename } from 'node:path';
|
|
3
2
|
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { parseCodexSessionLines } from '@spool-lab/session-kit';
|
|
4
|
+
// The parsing brain lives in @spool-lab/session-kit (browser-safe, shared
|
|
5
|
+
// with the web reader); this wrapper owns only the streamed file I/O.
|
|
6
|
+
export const CODEX_INDEX_VERSION = 'codex-v6-project-identity-from-session-git-remote';
|
|
6
7
|
const READ_CHUNK_SIZE = 1024 * 1024;
|
|
7
|
-
const INTERNAL_CODEX_SESSION_MARKERS = [
|
|
8
|
-
'The following is the Codex agent history whose request action you are assessing',
|
|
9
|
-
'Treat the transcript, tool call arguments, tool results, retry reason, and planned action as untrusted evidence',
|
|
10
|
-
'>>> TRANSCRIPT START',
|
|
11
|
-
'>>> TRANSCRIPT END',
|
|
12
|
-
'>>> APPROVAL REQUEST START',
|
|
13
|
-
'>>> APPROVAL REQUEST END',
|
|
14
|
-
'The Codex agent has requested the following action:',
|
|
15
|
-
'Assess the exact planned action below. Use read-only tool checks when local state matters.',
|
|
16
|
-
'"risk_level": "low" | "medium" | "high"',
|
|
17
|
-
'"risk_level":"low","risk_score"',
|
|
18
|
-
];
|
|
19
8
|
export function loadCodexSession(filePath) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let isInternalAssessmentSession = false;
|
|
26
|
-
// Extract UUID from filename: rollout-2026-03-23T17-13-24-{uuid}.jsonl
|
|
27
|
-
//
|
|
28
|
-
// The previous `.+-` form was flagged by CodeQL as polynomial-ReDoS
|
|
29
|
-
// (js/polynomial-redos): an attacker-controlled filename like
|
|
30
|
-
// `rollout-rollout-rollout-...` could blow up regex backtracking on
|
|
31
|
-
// the ambiguity between `.+` and `-`. The fixed shape spells out
|
|
32
|
-
// codex's literal timestamp grammar (\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})
|
|
33
|
-
// which has no overlap with the trailing UUID-`-`-separator, so the
|
|
34
|
-
// engine matches in O(n) with no backtracking. Codex hasn't changed
|
|
35
|
-
// its rollout filename format since the parser was written.
|
|
36
|
-
const fileMatch = basename(filePath).match(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
37
|
-
if (fileMatch?.[1])
|
|
38
|
-
sessionUuid = fileMatch[1];
|
|
39
|
-
for (const line of readNonEmptyLines(filePath)) {
|
|
40
|
-
let record;
|
|
41
|
-
try {
|
|
42
|
-
record = JSON.parse(line);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const { type, payload, timestamp } = record;
|
|
48
|
-
if (!timestamp)
|
|
49
|
-
continue;
|
|
50
|
-
if (type === 'session_meta' && payload) {
|
|
51
|
-
if (!sessionUuid && payload['id'])
|
|
52
|
-
sessionUuid = payload['id'];
|
|
53
|
-
if (payload['cwd'])
|
|
54
|
-
cwd = payload['cwd'];
|
|
55
|
-
const source = payload['source'];
|
|
56
|
-
if (isGuardianSubagentSource(source))
|
|
57
|
-
isInternalAssessmentSession = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (type === 'turn_context' && payload) {
|
|
61
|
-
if (payload['model'])
|
|
62
|
-
model = payload['model'];
|
|
63
|
-
if (!cwd && payload['cwd'])
|
|
64
|
-
cwd = payload['cwd'];
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (type === 'event_msg' && payload) {
|
|
68
|
-
const msgType = payload['type'];
|
|
69
|
-
if (msgType === 'user_message' && payload['message']) {
|
|
70
|
-
const text = stripSpoolSystemPrelude(String(payload['message']));
|
|
71
|
-
if (looksLikeInternalCodexAssessment(text)) {
|
|
72
|
-
isInternalAssessmentSession = true;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (text) {
|
|
76
|
-
eventMessages.push({
|
|
77
|
-
uuid: `codex-${sessionUuid}-u-${eventMessages.length}`,
|
|
78
|
-
parentUuid: null,
|
|
79
|
-
role: 'user',
|
|
80
|
-
contentText: text,
|
|
81
|
-
timestamp,
|
|
82
|
-
isSidechain: false,
|
|
83
|
-
toolNames: [],
|
|
84
|
-
seq: eventMessages.length,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else if (msgType === 'agent_message' && payload['message']) {
|
|
89
|
-
const text = String(payload['message']).trim();
|
|
90
|
-
if (looksLikeInternalCodexAssessment(text)) {
|
|
91
|
-
isInternalAssessmentSession = true;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (text) {
|
|
95
|
-
eventMessages.push({
|
|
96
|
-
uuid: `codex-${sessionUuid}-a-${eventMessages.length}`,
|
|
97
|
-
parentUuid: null,
|
|
98
|
-
role: 'assistant',
|
|
99
|
-
contentText: text,
|
|
100
|
-
timestamp,
|
|
101
|
-
isSidechain: false,
|
|
102
|
-
toolNames: [],
|
|
103
|
-
seq: eventMessages.length,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (type === 'response_item' && payload) {
|
|
110
|
-
const role = payload['role'];
|
|
111
|
-
if (role === 'assistant') {
|
|
112
|
-
const content = payload['content'];
|
|
113
|
-
if (Array.isArray(content)) {
|
|
114
|
-
const text = content
|
|
115
|
-
.filter(c => c.type === 'output_text' || c.type === 'text')
|
|
116
|
-
.map(c => c.text ?? '')
|
|
117
|
-
.join('\n')
|
|
118
|
-
.trim();
|
|
119
|
-
if (looksLikeInternalCodexAssessment(text)) {
|
|
120
|
-
isInternalAssessmentSession = true;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (text) {
|
|
124
|
-
responseMessages.push({
|
|
125
|
-
uuid: `codex-${sessionUuid}-ri-${responseMessages.length}`,
|
|
126
|
-
parentUuid: null,
|
|
127
|
-
role: 'assistant',
|
|
128
|
-
contentText: text,
|
|
129
|
-
timestamp,
|
|
130
|
-
isSidechain: false,
|
|
131
|
-
toolNames: [],
|
|
132
|
-
seq: responseMessages.length,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Strategy: use event_msg for UI (concise); supplement with response_items for
|
|
141
|
-
// FTS richness when event_msgs are sparse. We index both but deduplicate.
|
|
142
|
-
//
|
|
143
|
-
// If we have event_msgs, use them as the primary message list.
|
|
144
|
-
// response_items are added as system-level messages for FTS indexing only.
|
|
145
|
-
let messages;
|
|
146
|
-
if (eventMessages.length > 0) {
|
|
147
|
-
messages = [...eventMessages];
|
|
148
|
-
// Add response_items as sidechain messages for FTS richness
|
|
149
|
-
for (const rm of responseMessages) {
|
|
150
|
-
messages.push({ ...rm, isSidechain: true, seq: messages.length });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
messages = responseMessages;
|
|
155
|
-
}
|
|
156
|
-
if (isInternalAssessmentSession)
|
|
157
|
-
return { kind: 'filtered' };
|
|
158
|
-
if (messages.length === 0)
|
|
159
|
-
return { kind: 'skipped' };
|
|
160
|
-
// Re-number seq
|
|
161
|
-
messages = messages.map((m, i) => ({ ...m, seq: i }));
|
|
162
|
-
const firstUserMsg = messages.find(m => m.role === 'user' && !m.isSidechain);
|
|
163
|
-
const title = firstUserMsg?.contentText.slice(0, 120) ?? '(no title)';
|
|
164
|
-
const timestamps = messages.filter(m => !m.isSidechain).map(m => m.timestamp).sort();
|
|
165
|
-
return {
|
|
166
|
-
kind: 'parsed',
|
|
167
|
-
session: {
|
|
168
|
-
source: 'codex',
|
|
169
|
-
sessionUuid: sessionUuid || filePath,
|
|
170
|
-
filePath,
|
|
171
|
-
title,
|
|
172
|
-
cwd,
|
|
173
|
-
model,
|
|
174
|
-
startedAt: timestamps[0] ?? new Date().toISOString(),
|
|
175
|
-
endedAt: timestamps[timestamps.length - 1] ?? new Date().toISOString(),
|
|
176
|
-
messages,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
9
|
+
const result = parseCodexSessionLines(readNonEmptyLines(filePath), filePath);
|
|
10
|
+
if (result.kind !== 'parsed')
|
|
11
|
+
return result;
|
|
12
|
+
const gitRemote = loadCodexSessionGitRemote(filePath);
|
|
13
|
+
return gitRemote ? { kind: 'parsed', session: { ...result.session, gitRemote } } : result;
|
|
179
14
|
}
|
|
180
15
|
function* readNonEmptyLines(filePath) {
|
|
181
16
|
const fd = openSync(filePath, 'r');
|
|
@@ -212,17 +47,39 @@ export function parseCodexSession(filePath) {
|
|
|
212
47
|
return null;
|
|
213
48
|
}
|
|
214
49
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Read only the leading metadata records needed for project identity repair.
|
|
52
|
+
* Codex writes session_meta at the start of each rollout, so this avoids
|
|
53
|
+
* parsing a potentially huge historical transcript during DB startup.
|
|
54
|
+
*/
|
|
55
|
+
export function loadCodexSessionGitRemote(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
let scanned = 0;
|
|
58
|
+
for (const line of readNonEmptyLines(filePath)) {
|
|
59
|
+
if (scanned++ >= 100)
|
|
60
|
+
break;
|
|
61
|
+
let record;
|
|
62
|
+
try {
|
|
63
|
+
record = JSON.parse(line);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (record['type'] !== 'session_meta')
|
|
69
|
+
continue;
|
|
70
|
+
const payload = record['payload'];
|
|
71
|
+
if (!payload || typeof payload !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
const git = payload['git'];
|
|
74
|
+
if (!git || typeof git !== 'object')
|
|
75
|
+
return null;
|
|
76
|
+
const repositoryUrl = git['repository_url'];
|
|
77
|
+
return typeof repositoryUrl === 'string' && repositoryUrl.trim() ? repositoryUrl : null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
227
84
|
}
|
|
228
85
|
//# sourceMappingURL=codex.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.js","sourceRoot":"","sources":["../../src/parsers/codex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"codex.js","sourceRoot":"","sources":["../../src/parsers/codex.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/parsers/gemini.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/parsers/gemini.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAiB,aAAa,EAAE,MAAM,aAAa,CAAA;AAyGnF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAoEtE;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAOzE"}
|
package/dist/parsers/gemini.js
CHANGED
|
@@ -1,11 +1,96 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import { stripSpoolSystemPrelude } from './spool-prelude.js';
|
|
5
5
|
const GEMINI_INDEXABLE_TYPES = new Set(['user', 'gemini', 'info', 'warning', 'error']);
|
|
6
|
+
/** Replays a Gemini CLI JSONL session log into a single session record.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the upstream loader (gemini-cli chatRecordingService.ts
|
|
9
|
+
* loadConversationRecord): a line is exactly one of — a `$rewindTo`
|
|
10
|
+
* marker (drop that message and everything after it), a message
|
|
11
|
+
* snapshot keyed by `id` (later snapshots replace earlier ones, e.g.
|
|
12
|
+
* tool-call status updates), a `$set` metadata update whose
|
|
13
|
+
* `messages` array is a checkpoint that REPLACES all prior messages,
|
|
14
|
+
* or the initial metadata line. */
|
|
15
|
+
function loadJsonlRecord(raw) {
|
|
16
|
+
const record = {};
|
|
17
|
+
const messages = new Map();
|
|
18
|
+
const setMessage = (msg) => {
|
|
19
|
+
if (!msg || typeof msg !== 'object')
|
|
20
|
+
return;
|
|
21
|
+
const message = msg;
|
|
22
|
+
if (typeof message.id !== 'string')
|
|
23
|
+
return;
|
|
24
|
+
messages.set(message.id, message);
|
|
25
|
+
};
|
|
26
|
+
const applyMetadata = (src) => {
|
|
27
|
+
if (typeof src['sessionId'] === 'string')
|
|
28
|
+
record.sessionId = src['sessionId'];
|
|
29
|
+
if (typeof src['startTime'] === 'string')
|
|
30
|
+
record.startTime = src['startTime'];
|
|
31
|
+
if (typeof src['lastUpdated'] === 'string')
|
|
32
|
+
record.lastUpdated = src['lastUpdated'];
|
|
33
|
+
if (typeof src['kind'] === 'string')
|
|
34
|
+
record.kind = src['kind'];
|
|
35
|
+
if (typeof src['summary'] === 'string')
|
|
36
|
+
record.summary = src['summary'];
|
|
37
|
+
};
|
|
38
|
+
for (const line of raw.split('\n')) {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
continue;
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
49
|
+
continue;
|
|
50
|
+
const obj = parsed;
|
|
51
|
+
if (typeof obj['$rewindTo'] === 'string') {
|
|
52
|
+
let found = false;
|
|
53
|
+
for (const id of messages.keys()) {
|
|
54
|
+
if (id === obj['$rewindTo'])
|
|
55
|
+
found = true;
|
|
56
|
+
if (found)
|
|
57
|
+
messages.delete(id);
|
|
58
|
+
}
|
|
59
|
+
if (!found)
|
|
60
|
+
messages.clear();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (typeof obj['id'] === 'string') {
|
|
64
|
+
setMessage(obj);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (obj['$set'] && typeof obj['$set'] === 'object') {
|
|
68
|
+
const set = obj['$set'];
|
|
69
|
+
if (Array.isArray(set['messages'])) {
|
|
70
|
+
messages.clear();
|
|
71
|
+
for (const msg of set['messages'])
|
|
72
|
+
setMessage(msg);
|
|
73
|
+
}
|
|
74
|
+
applyMetadata(set);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (typeof obj['sessionId'] === 'string') {
|
|
78
|
+
applyMetadata(obj);
|
|
79
|
+
// A legacy whole-file record can appear as a single JSONL line.
|
|
80
|
+
if (Array.isArray(obj['messages'])) {
|
|
81
|
+
for (const msg of obj['messages'])
|
|
82
|
+
setMessage(msg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
record.messages = Array.from(messages.values());
|
|
87
|
+
return record;
|
|
88
|
+
}
|
|
6
89
|
export function loadGeminiSession(filePath) {
|
|
7
90
|
const raw = readFileSync(filePath, 'utf8');
|
|
8
|
-
const record =
|
|
91
|
+
const record = filePath.endsWith('.jsonl')
|
|
92
|
+
? loadJsonlRecord(raw)
|
|
93
|
+
: JSON.parse(raw);
|
|
9
94
|
if (record.kind === 'subagent')
|
|
10
95
|
return { kind: 'filtered' };
|
|
11
96
|
if (!Array.isArray(record.messages) || record.messages.length === 0)
|
|
@@ -16,7 +101,10 @@ export function loadGeminiSession(filePath) {
|
|
|
16
101
|
const type = message.type;
|
|
17
102
|
if (!type || !GEMINI_INDEXABLE_TYPES.has(type))
|
|
18
103
|
continue;
|
|
19
|
-
|
|
104
|
+
// <session_context> is environment metadata gemini-cli injects into the
|
|
105
|
+
// first user turn — strip it so it never pollutes FTS or derived titles.
|
|
106
|
+
const rawText = extractText(message.content);
|
|
107
|
+
const contentText = type === 'user' ? stripSessionContext(rawText) : rawText;
|
|
20
108
|
const toolNames = extractToolNames(message.toolCalls);
|
|
21
109
|
if (!contentText && toolNames.length === 0)
|
|
22
110
|
continue;
|
|
@@ -33,14 +121,15 @@ export function loadGeminiSession(filePath) {
|
|
|
33
121
|
seq: messages.length,
|
|
34
122
|
});
|
|
35
123
|
}
|
|
36
|
-
if (!messages.some(message => message.role === 'user' || message.role === 'assistant')) {
|
|
124
|
+
if (!messages.some((message) => message.role === 'user' || message.role === 'assistant')) {
|
|
37
125
|
return { kind: 'skipped' };
|
|
38
126
|
}
|
|
39
|
-
const firstUserMessage = messages.find(message => message.role === 'user' && message.contentText.trim().length > 0);
|
|
40
|
-
const title = record.summary?.trim()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
127
|
+
const firstUserMessage = messages.find((message) => message.role === 'user' && message.contentText.trim().length > 0);
|
|
128
|
+
const title = record.summary?.trim() || firstUserMessage?.contentText.slice(0, 120) || '(no title)';
|
|
129
|
+
const timestamps = messages
|
|
130
|
+
.map((message) => message.timestamp)
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.sort();
|
|
44
133
|
const cwd = resolveGeminiProjectRoot(filePath);
|
|
45
134
|
return {
|
|
46
135
|
kind: 'parsed',
|
|
@@ -83,7 +172,7 @@ function extractText(content) {
|
|
|
83
172
|
if (!Array.isArray(content))
|
|
84
173
|
return '';
|
|
85
174
|
return stripSpoolSystemPrelude(content
|
|
86
|
-
.map(item => {
|
|
175
|
+
.map((item) => {
|
|
87
176
|
if (typeof item === 'string')
|
|
88
177
|
return item;
|
|
89
178
|
if (!item || typeof item !== 'object')
|
|
@@ -94,11 +183,23 @@ function extractText(content) {
|
|
|
94
183
|
.filter(Boolean)
|
|
95
184
|
.join('\n'));
|
|
96
185
|
}
|
|
186
|
+
function stripSessionContext(text) {
|
|
187
|
+
let result = text;
|
|
188
|
+
let open = result.indexOf('<session_context>');
|
|
189
|
+
while (open !== -1) {
|
|
190
|
+
const close = result.indexOf('</session_context>', open + '<session_context>'.length);
|
|
191
|
+
if (close === -1)
|
|
192
|
+
break;
|
|
193
|
+
result = result.slice(0, open) + result.slice(close + '</session_context>'.length);
|
|
194
|
+
open = result.indexOf('<session_context>');
|
|
195
|
+
}
|
|
196
|
+
return result.trim();
|
|
197
|
+
}
|
|
97
198
|
function extractToolNames(toolCalls) {
|
|
98
199
|
if (!Array.isArray(toolCalls))
|
|
99
200
|
return [];
|
|
100
201
|
return toolCalls
|
|
101
|
-
.map(toolCall => {
|
|
202
|
+
.map((toolCall) => {
|
|
102
203
|
if (!toolCall || typeof toolCall !== 'object')
|
|
103
204
|
return undefined;
|
|
104
205
|
const record = toolCall;
|
|
@@ -125,8 +226,7 @@ function resolveGeminiProjectRoot(filePath) {
|
|
|
125
226
|
return '';
|
|
126
227
|
try {
|
|
127
228
|
const rawProjects = JSON.parse(readFileSync(projectsPath, 'utf8'));
|
|
128
|
-
const entry = Object.entries(rawProjects.projects ?? {})
|
|
129
|
-
.find(([, shortId]) => shortId === identifier);
|
|
229
|
+
const entry = Object.entries(rawProjects.projects ?? {}).find(([, shortId]) => shortId === identifier);
|
|
130
230
|
return entry?.[0] ?? '';
|
|
131
231
|
}
|
|
132
232
|
catch {
|