ctxpool 0.1.1 → 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.
- package/README.md +94 -103
- package/dist/cli.js +328 -184
- package/dist/cli.js.map +1 -1
- package/dist/clipboard.js +8 -10
- package/dist/clipboard.js.map +1 -1
- package/dist/format.d.ts +32 -0
- package/dist/format.js +110 -0
- package/dist/format.js.map +1 -0
- package/dist/handoff.d.ts +16 -13
- package/dist/handoff.js +441 -706
- package/dist/handoff.js.map +1 -1
- package/dist/mcp.js +99 -221
- package/dist/mcp.js.map +1 -1
- package/dist/pack.d.ts +14 -0
- package/dist/pack.js +121 -0
- package/dist/pack.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.js +35 -5
- package/dist/paths.js.map +1 -1
- package/dist/redact.d.ts +6 -0
- package/dist/redact.js +85 -0
- package/dist/redact.js.map +1 -0
- package/dist/repo.d.ts +6 -1
- package/dist/repo.js +109 -123
- package/dist/repo.js.map +1 -1
- package/dist/session.d.ts +40 -1
- package/dist/session.js +1295 -302
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +13 -6
- package/dist/store.js +88 -115
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +6 -0
- package/dist/text.js +25 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +44 -7
- package/dist/ui.d.ts +10 -1
- package/dist/ui.js +329 -383
- package/dist/ui.js.map +1 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +40 -10
- package/dist/util.js.map +1 -1
- package/dist/verify.d.ts +16 -0
- package/dist/verify.js +74 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
- package/dist-web/assets/index-BBjy_S2c.css +2 -0
- package/dist-web/assets/index-QwGbBEu2.js +17 -0
- package/dist-web/index.html +4 -3
- package/package.json +18 -17
- package/dist/logs.d.ts +0 -9
- package/dist/logs.js +0 -82
- package/dist/logs.js.map +0 -1
- package/dist-web/assets/contextpool-mark-Bu_00i5t.svg +0 -16
- package/dist-web/assets/index-BfHRocKl.js +0 -14
- package/dist-web/assets/index-l1QjYqDb.css +0 -2
package/dist/session.js
CHANGED
|
@@ -1,81 +1,600 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { oneLine, truncate } from "./text.js";
|
|
8
|
+
import { CtxError, canonicalPath, findRecentFiles, unique } from "./util.js";
|
|
9
|
+
import { commandStatus } from "./verify.js";
|
|
10
|
+
const DEFAULT_EXCERPT_TOKENS = 3_000;
|
|
11
|
+
const AMBIGUITY_WINDOW_MS = 10 * 60 * 1000;
|
|
12
|
+
const MAX_CANDIDATES = 200;
|
|
13
|
+
export function codexHome() {
|
|
14
|
+
return process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
15
|
+
}
|
|
16
|
+
export function claudeHome() {
|
|
17
|
+
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
18
|
+
}
|
|
19
|
+
/** Gemini CLI keeps its data in `.gemini` under GEMINI_CLI_HOME, or under the home directory. */
|
|
20
|
+
export function geminiHome() {
|
|
21
|
+
return join(process.env.GEMINI_CLI_HOME || homedir(), ".gemini");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Antigravity CLI (`agy`) always keeps its data in ~/.gemini/antigravity-cli. CTXPOOL_ANTIGRAVITY_DIR points
|
|
25
|
+
* ContextPool somewhere else; agy itself does not read it.
|
|
26
|
+
*/
|
|
27
|
+
export function antigravityHome() {
|
|
28
|
+
return process.env.CTXPOOL_ANTIGRAVITY_DIR || join(homedir(), ".gemini", "antigravity-cli");
|
|
29
|
+
}
|
|
30
|
+
/** Every agent ContextPool can read, in the order `--from auto` lists them. */
|
|
31
|
+
export const SOURCE_AGENTS = ["codex", "claude", "gemini", "antigravity"];
|
|
32
|
+
const SOURCE_LABELS = { codex: "Codex", claude: "Claude", gemini: "Gemini", antigravity: "Antigravity" };
|
|
33
|
+
export function sourceLabel(source) {
|
|
34
|
+
return SOURCE_LABELS[source] || source;
|
|
35
|
+
}
|
|
36
|
+
/** Where each agent keeps the sessions ContextPool reads. */
|
|
37
|
+
export function sourceSessionDirs() {
|
|
38
|
+
return {
|
|
39
|
+
codex: join(codexHome(), "sessions"),
|
|
40
|
+
claude: join(claudeHome(), "projects"),
|
|
41
|
+
gemini: join(geminiHome(), "tmp"),
|
|
42
|
+
antigravity: join(antigravityHome(), "conversations")
|
|
43
|
+
};
|
|
44
|
+
}
|
|
5
45
|
export function resolveSessionSource(options) {
|
|
6
46
|
const from = normalizeFrom(options.from || "auto");
|
|
7
|
-
const
|
|
8
|
-
if (options.session)
|
|
9
|
-
return resolveExplicitSession(from,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
second &&
|
|
23
|
-
first.matchedCwd &&
|
|
24
|
-
second.matchedCwd &&
|
|
25
|
-
first.source !== second.source &&
|
|
26
|
-
Math.abs(Date.parse(first.updatedAt) - Date.parse(second.updatedAt)) < 10 * 60 * 1000) {
|
|
27
|
-
throw new Error(`Both Codex and Claude have recent sessions for this workspace. Re-run with --from ${first.source} or --from ${second.source}.`);
|
|
47
|
+
const scope = toScope(options.cwd, options.repoRoot);
|
|
48
|
+
if (options.session)
|
|
49
|
+
return resolveExplicitSession(from, scope, options.session, options.budget);
|
|
50
|
+
const matched = listSessionCandidates({ ...scope, from }).filter((candidate) => candidate.matchedCwd);
|
|
51
|
+
const [first] = matched;
|
|
52
|
+
if (!first) {
|
|
53
|
+
const label = from === "auto" ? joinWords(SOURCE_AGENTS.map(sourceLabel), "or") : sourceLabel(from);
|
|
54
|
+
throw new CtxError(`No ${label} session found for ${scope.cwd}.`, "Run `ctxpool sessions --all` to list sessions, then pass --session <id or path>.");
|
|
55
|
+
}
|
|
56
|
+
if (from === "auto") {
|
|
57
|
+
const recent = matched.filter((candidate) => Date.parse(first.updatedAt) - Date.parse(candidate.updatedAt) < AMBIGUITY_WINDOW_MS);
|
|
58
|
+
const active = Array.from(new Set(recent.map((candidate) => candidate.source)));
|
|
59
|
+
if (active.length > 1) {
|
|
60
|
+
throw new CtxError(`${joinWords(active.map(sourceLabel), "and")} were ${active.length === 2 ? "both" : "all"} active in this workspace in the last 10 minutes.`, `Re-run with ${joinWords(active.map((source, index) => `--from ${source}${index === 0 ? " (most recent)" : ""}`), "or")}.`);
|
|
61
|
+
}
|
|
28
62
|
}
|
|
29
63
|
return parseCandidate(first, options.budget);
|
|
30
64
|
}
|
|
65
|
+
/** Primary agent sessions, most relevant first. Subagent and reviewer threads are excluded. */
|
|
66
|
+
export function listSessionCandidates(options) {
|
|
67
|
+
const scope = toScope(options.cwd, options.repoRoot);
|
|
68
|
+
const from = options.from || "auto";
|
|
69
|
+
const sources = from === "auto" ? SOURCE_AGENTS : [from];
|
|
70
|
+
return sources.flatMap((source) => CANDIDATE_FINDERS[source](scope, options.includeSecondary === true)).sort(compareCandidates);
|
|
71
|
+
}
|
|
31
72
|
function normalizeFrom(from) {
|
|
32
|
-
if (from === "auto" || from
|
|
73
|
+
if (from === "auto" || SOURCE_AGENTS.includes(from))
|
|
33
74
|
return from;
|
|
34
|
-
throw new
|
|
75
|
+
throw new CtxError(`Unsupported --from value "${from}".`, `Use ${joinWords(["auto", ...SOURCE_AGENTS], "or")}.`);
|
|
76
|
+
}
|
|
77
|
+
function resolveExplicitSession(from, scope, session, budget) {
|
|
78
|
+
const asPath = resolve(session);
|
|
79
|
+
if (existsSync(asPath) && statSync(asPath).isFile()) {
|
|
80
|
+
const source = from === "auto" ? detectSource(asPath) : from;
|
|
81
|
+
const parsed = SESSION_PARSERS[source](asPath, budget);
|
|
82
|
+
parsed.matchedCwd = matchesWorkspace(parsed.cwd, scope);
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
const match = listSessionCandidates({ ...scope, from, includeSecondary: true }).find((candidate) => candidate.id === session || candidate.id.startsWith(session) || basename(candidate.path).includes(session));
|
|
86
|
+
if (!match)
|
|
87
|
+
throw new CtxError(`No session matched "${session}".`, "Pass an id from `ctxpool sessions --all` or a path to a session file.");
|
|
88
|
+
return parseCandidate(match, budget);
|
|
89
|
+
}
|
|
90
|
+
function parseCandidate(candidate, budget) {
|
|
91
|
+
const session = SESSION_PARSERS[candidate.source](candidate.path, budget);
|
|
92
|
+
session.id = candidate.id || session.id;
|
|
93
|
+
session.cwd = session.cwd || candidate.cwd;
|
|
94
|
+
session.updatedAt = candidate.updatedAt;
|
|
95
|
+
session.matchedCwd = candidate.matchedCwd;
|
|
96
|
+
session.threadName = session.threadName || candidate.title;
|
|
97
|
+
return session;
|
|
98
|
+
}
|
|
99
|
+
function compareCandidates(a, b) {
|
|
100
|
+
if (a.matchedCwd !== b.matchedCwd)
|
|
101
|
+
return a.matchedCwd ? -1 : 1;
|
|
102
|
+
return Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
103
|
+
}
|
|
104
|
+
/** "a", "a or b", "a, b, or c". */
|
|
105
|
+
function joinWords(words, conjunction) {
|
|
106
|
+
if (words.length <= 2)
|
|
107
|
+
return words.join(` ${conjunction} `);
|
|
108
|
+
return `${words.slice(0, -1).join(", ")}, ${conjunction} ${words.at(-1)}`;
|
|
109
|
+
}
|
|
110
|
+
const CANDIDATE_FINDERS = {
|
|
111
|
+
codex: findCodexCandidates,
|
|
112
|
+
claude: findClaudeCandidates,
|
|
113
|
+
gemini: findGeminiCandidates,
|
|
114
|
+
antigravity: findAntigravityCandidates
|
|
115
|
+
};
|
|
116
|
+
const SESSION_PARSERS = {
|
|
117
|
+
codex: parseCodexSession,
|
|
118
|
+
claude: parseClaudeSession,
|
|
119
|
+
gemini: parseGeminiSession,
|
|
120
|
+
antigravity: parseAntigravitySession
|
|
121
|
+
};
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Discovery
|
|
124
|
+
function findCodexCandidates(scope, includeSecondary) {
|
|
125
|
+
const index = readCodexIndex();
|
|
126
|
+
const candidates = [];
|
|
127
|
+
for (const path of findRecentFiles(join(codexHome(), "sessions"), MAX_CANDIDATES, /\.jsonl$/i)) {
|
|
128
|
+
const meta = readCodexMeta(path);
|
|
129
|
+
if (meta.secondary && !includeSecondary)
|
|
130
|
+
continue;
|
|
131
|
+
const id = meta.id || sessionIdFromPath(path);
|
|
132
|
+
candidates.push({
|
|
133
|
+
source: "codex",
|
|
134
|
+
path,
|
|
135
|
+
id,
|
|
136
|
+
cwd: meta.cwd,
|
|
137
|
+
updatedAt: mtimeIso(path),
|
|
138
|
+
matchedCwd: matchesWorkspace(meta.cwd, scope),
|
|
139
|
+
title: index.get(id)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return candidates;
|
|
143
|
+
}
|
|
144
|
+
function findClaudeCandidates(scope, includeSecondary) {
|
|
145
|
+
const projects = join(claudeHome(), "projects");
|
|
146
|
+
const preferred = unique([scope.cwd, scope.repoRoot || ""].filter(Boolean).map((path) => join(projects, encodeClaudeProjectPath(path)))).filter(existsSync);
|
|
147
|
+
const roots = preferred.length ? preferred : [projects];
|
|
148
|
+
const skipDirs = includeSecondary ? [] : ["subagents"];
|
|
149
|
+
const candidates = [];
|
|
150
|
+
for (const path of roots.flatMap((root) => findRecentFiles(root, MAX_CANDIDATES, /\.jsonl$/i, skipDirs))) {
|
|
151
|
+
const meta = readClaudeMeta(path);
|
|
152
|
+
if (!meta.cwd && !meta.id)
|
|
153
|
+
continue;
|
|
154
|
+
candidates.push({
|
|
155
|
+
source: "claude",
|
|
156
|
+
path,
|
|
157
|
+
id: meta.id || sessionIdFromPath(path),
|
|
158
|
+
cwd: meta.cwd,
|
|
159
|
+
updatedAt: mtimeIso(path),
|
|
160
|
+
matchedCwd: matchesWorkspace(meta.cwd, scope),
|
|
161
|
+
title: meta.title
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return candidates;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Gemini CLI writes one chat per session to tmp/<project>/chats/session-<time>-<id>.jsonl (a single .json file in
|
|
168
|
+
* older builds). Subagent chats sit in a folder named after the parent session.
|
|
169
|
+
*/
|
|
170
|
+
function findGeminiCandidates(scope, includeSecondary) {
|
|
171
|
+
const tmp = join(geminiHome(), "tmp");
|
|
172
|
+
const registry = readGeminiProjectRegistry();
|
|
173
|
+
const byId = new Map();
|
|
174
|
+
for (const project of listDirectories(tmp)) {
|
|
175
|
+
const chats = join(tmp, project, "chats");
|
|
176
|
+
const projectRoot = readGeminiProjectRoot(join(tmp, project), registry);
|
|
177
|
+
for (const path of findRecentFiles(chats, MAX_CANDIDATES, /\.jsonl?$/i)) {
|
|
178
|
+
const meta = readGeminiMeta(path);
|
|
179
|
+
if (!meta.id)
|
|
180
|
+
continue;
|
|
181
|
+
if ((meta.subagent || dirname(path) !== chats) && !includeSecondary)
|
|
182
|
+
continue;
|
|
183
|
+
const cwd = projectRoot || geminiRootFromHash(meta.projectHash, scope);
|
|
184
|
+
const candidate = {
|
|
185
|
+
source: "gemini",
|
|
186
|
+
path,
|
|
187
|
+
id: meta.id,
|
|
188
|
+
cwd,
|
|
189
|
+
updatedAt: mtimeIso(path),
|
|
190
|
+
matchedCwd: matchesWorkspace(cwd, scope),
|
|
191
|
+
title: meta.title
|
|
192
|
+
};
|
|
193
|
+
// Resuming an old .json chat copies it to a .jsonl file beside the original; keep the newer copy.
|
|
194
|
+
const existing = byId.get(meta.id);
|
|
195
|
+
if (!existing || Date.parse(candidate.updatedAt) > Date.parse(existing.updatedAt))
|
|
196
|
+
byId.set(meta.id, candidate);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...byId.values()];
|
|
200
|
+
}
|
|
201
|
+
function readGeminiMeta(path) {
|
|
202
|
+
let metadata = {};
|
|
203
|
+
let firstPrompt = "";
|
|
204
|
+
if (/\.json$/i.test(path)) {
|
|
205
|
+
const conversation = readGeminiConversation(path);
|
|
206
|
+
metadata = conversation.metadata;
|
|
207
|
+
firstPrompt = conversation.messages.map(geminiUserPrompt).find(Boolean) || "";
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const line of readHead(path, 128_000).split(/\r?\n/).slice(0, 60)) {
|
|
211
|
+
const record = objectValue(parseJson(line));
|
|
212
|
+
if (typeof record.sessionId === "string" && typeof record.projectHash === "string")
|
|
213
|
+
metadata = { ...metadata, ...record };
|
|
214
|
+
else if (!firstPrompt && record.type === "user")
|
|
215
|
+
firstPrompt = geminiUserPrompt(record);
|
|
216
|
+
}
|
|
217
|
+
for (const line of readTail(path, 96_000).split(/\r?\n/)) {
|
|
218
|
+
if (!line.includes('"summary"'))
|
|
219
|
+
continue;
|
|
220
|
+
const summary = stringValue(objectValue(objectValue(parseJson(line)).$set).summary);
|
|
221
|
+
if (summary)
|
|
222
|
+
metadata.summary = summary;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
id: stringValue(metadata.sessionId) || undefined,
|
|
227
|
+
projectHash: stringValue(metadata.projectHash) || undefined,
|
|
228
|
+
subagent: metadata.kind === "subagent",
|
|
229
|
+
title: stringValue(metadata.summary) || (firstPrompt ? truncate(oneLine(firstPrompt), 80) : undefined)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** projects.json maps each project root to the short folder name Gemini uses under tmp/. */
|
|
233
|
+
function readGeminiProjectRegistry() {
|
|
234
|
+
const map = new Map();
|
|
235
|
+
const path = join(geminiHome(), "projects.json");
|
|
236
|
+
if (!existsSync(path))
|
|
237
|
+
return map;
|
|
238
|
+
for (const [root, name] of Object.entries(objectValue(objectValue(parseJson(readFileSync(path, "utf8"))).projects))) {
|
|
239
|
+
if (typeof name === "string")
|
|
240
|
+
map.set(name, root);
|
|
241
|
+
}
|
|
242
|
+
return map;
|
|
243
|
+
}
|
|
244
|
+
function readGeminiProjectRoot(projectDir, registry = readGeminiProjectRegistry()) {
|
|
245
|
+
const marker = join(projectDir, ".project_root");
|
|
246
|
+
const root = existsSync(marker) ? readFileSync(marker, "utf8").trim() : "";
|
|
247
|
+
return root || registry.get(basename(projectDir));
|
|
248
|
+
}
|
|
249
|
+
/** Older Gemini builds name project folders by the SHA-256 of the project root and leave no marker file. */
|
|
250
|
+
function geminiRootFromHash(projectHash, scope) {
|
|
251
|
+
if (!projectHash)
|
|
252
|
+
return undefined;
|
|
253
|
+
return [scope.cwd, scope.repoRoot].find((root) => root && createHash("sha256").update(root).digest("hex") === projectHash);
|
|
254
|
+
}
|
|
255
|
+
/** The tmp/<project> folder that holds a chat file, for main and subagent chats alike. */
|
|
256
|
+
function geminiProjectDir(path) {
|
|
257
|
+
let dir = dirname(path);
|
|
258
|
+
for (let depth = 0; depth < 2; depth++, dir = dirname(dir))
|
|
259
|
+
if (basename(dir) === "chats")
|
|
260
|
+
return dirname(dir);
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Antigravity CLI stores each conversation as a SQLite database, conversations/<id>.db, and indexes them in
|
|
265
|
+
* conversation_summaries.db. Subagent conversations name their parent there and in their own metadata.
|
|
266
|
+
*/
|
|
267
|
+
function findAntigravityCandidates(scope, includeSecondary) {
|
|
268
|
+
const index = readAntigravityIndex();
|
|
269
|
+
const candidates = [];
|
|
270
|
+
for (const path of findRecentFiles(join(antigravityHome(), "conversations"), MAX_CANDIDATES, /\.db$/i)) {
|
|
271
|
+
const id = antigravityId(path);
|
|
272
|
+
const summary = index.get(id) || readAntigravityMetadata(path);
|
|
273
|
+
if (summary.parentId && !includeSecondary)
|
|
274
|
+
continue;
|
|
275
|
+
candidates.push({
|
|
276
|
+
source: "antigravity",
|
|
277
|
+
path,
|
|
278
|
+
id,
|
|
279
|
+
cwd: summary.cwd,
|
|
280
|
+
updatedAt: antigravityUpdatedAt(path),
|
|
281
|
+
matchedCwd: matchesWorkspace(summary.cwd, scope),
|
|
282
|
+
title: summary.title
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return candidates;
|
|
286
|
+
}
|
|
287
|
+
function readAntigravityIndex() {
|
|
288
|
+
const index = new Map();
|
|
289
|
+
const db = openAntigravityDb(join(antigravityHome(), "conversation_summaries.db"));
|
|
290
|
+
if (!db)
|
|
291
|
+
return index;
|
|
292
|
+
try {
|
|
293
|
+
const rows = db.prepare("SELECT conversation_id, title, workspace_uris, parent_conversation_id FROM conversation_summaries").all();
|
|
294
|
+
for (const row of rows) {
|
|
295
|
+
const workspaces = parseJson(String(row.workspace_uris ?? ""));
|
|
296
|
+
index.set(String(row.conversation_id), {
|
|
297
|
+
cwd: fileUriToPath(Array.isArray(workspaces) ? stringValue(workspaces[0]) : ""),
|
|
298
|
+
title: String(row.title ?? "") || undefined,
|
|
299
|
+
parentId: String(row.parent_conversation_id ?? "") || undefined
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// An older or newer index layout; each conversation's own metadata still names its workspace.
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
db.close();
|
|
308
|
+
}
|
|
309
|
+
return index;
|
|
310
|
+
}
|
|
311
|
+
/** For conversations missing from the index: the workspace from their own metadata, titled by the first prompt still on record. */
|
|
312
|
+
function readAntigravityMetadata(path) {
|
|
313
|
+
const db = openAntigravityDb(path);
|
|
314
|
+
if (!db)
|
|
315
|
+
return {};
|
|
316
|
+
try {
|
|
317
|
+
const row = db.prepare("SELECT step_payload FROM steps WHERE step_type = ? AND status != ? ORDER BY idx LIMIT 1").get(AGY_STEP.userInput, AGY_STATUS.cleared);
|
|
318
|
+
const prompt = row?.step_payload instanceof Uint8Array ? antigravityPrompt(pbMessage(pbDecode(row.step_payload), 19)) : "";
|
|
319
|
+
return { ...antigravityTrajectoryMetadata(db), title: prompt ? truncate(oneLine(cleanText(prompt)), 80) : undefined };
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return antigravityTrajectoryMetadata(db);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
db.close();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** The CortexTrajectoryMetadata proto: workspaces[].workspace_folder_absolute_uri (1.1), workspace_uris (7), parent_conversation_id (5). */
|
|
329
|
+
function antigravityTrajectoryMetadata(db) {
|
|
330
|
+
try {
|
|
331
|
+
const row = db.prepare("SELECT data FROM trajectory_metadata_blob WHERE id = 'main'").get();
|
|
332
|
+
const metadata = pbDecode(row?.data instanceof Uint8Array ? row.data : undefined);
|
|
333
|
+
const uri = pbString(pbMessage(metadata, 1), 1) || pbString(metadata, 7);
|
|
334
|
+
return { cwd: fileUriToPath(uri), parentId: pbString(metadata, 5) || undefined };
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** agy writes recent steps to the -wal file first, so it can be newer than the database. An empty one holds nothing new. */
|
|
341
|
+
function antigravityUpdatedAt(path) {
|
|
342
|
+
const database = statSync(path).mtime;
|
|
343
|
+
const wal = existsSync(`${path}-wal`) ? statSync(`${path}-wal`) : undefined;
|
|
344
|
+
return (wal?.size && wal.mtime > database ? wal.mtime : database).toISOString();
|
|
345
|
+
}
|
|
346
|
+
function antigravityId(path) {
|
|
347
|
+
return basename(path).replace(/\.db$/i, "");
|
|
348
|
+
}
|
|
349
|
+
function fileUriToPath(uri) {
|
|
350
|
+
if (!uri)
|
|
351
|
+
return undefined;
|
|
352
|
+
try {
|
|
353
|
+
return uri.startsWith("file:") ? fileURLToPath(uri) : uri;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function readCodexMeta(path) {
|
|
360
|
+
const record = objectValue(parseJson(readFirstLine(path)));
|
|
361
|
+
if (stringValue(record.type) !== "session_meta")
|
|
362
|
+
return { secondary: false };
|
|
363
|
+
const payload = objectValue(record.payload);
|
|
364
|
+
const threadSource = stringValue(payload.thread_source);
|
|
365
|
+
const source = payload.source;
|
|
366
|
+
const secondary = threadSource === "subagent" || threadSource === "guardian_review" || (typeof source === "object" && source !== null && "subagent" in source);
|
|
367
|
+
return { id: stringValue(payload.id) || undefined, cwd: stringValue(payload.cwd) || undefined, secondary };
|
|
35
368
|
}
|
|
36
|
-
|
|
369
|
+
function readClaudeMeta(path) {
|
|
370
|
+
let id;
|
|
371
|
+
let cwd;
|
|
372
|
+
let firstPrompt;
|
|
373
|
+
for (const line of readHead(path, 128_000).split(/\r?\n/).slice(0, 60)) {
|
|
374
|
+
const record = objectValue(parseJson(line));
|
|
375
|
+
id ||= stringValue(record.sessionId) || undefined;
|
|
376
|
+
cwd ||= stringValue(record.cwd) || undefined;
|
|
377
|
+
if (!firstPrompt && record.type === "user" && !record.isMeta && !record.isSidechain) {
|
|
378
|
+
const content = objectValue(record.message).content;
|
|
379
|
+
if (typeof content === "string")
|
|
380
|
+
firstPrompt = cleanText(parseClaudeUserText(content).prompt) || undefined;
|
|
381
|
+
}
|
|
382
|
+
if (id && cwd && firstPrompt)
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
let title;
|
|
386
|
+
for (const line of readTail(path, 96_000).split(/\r?\n/)) {
|
|
387
|
+
if (!line.includes("title") && !line.includes("summary"))
|
|
388
|
+
continue;
|
|
389
|
+
const record = objectValue(parseJson(line));
|
|
390
|
+
title = claudeTitle(record) || title;
|
|
391
|
+
}
|
|
392
|
+
return { id, cwd, title: title || (firstPrompt ? truncate(oneLine(firstPrompt), 80) : undefined) };
|
|
393
|
+
}
|
|
394
|
+
function readCodexIndex() {
|
|
395
|
+
const map = new Map();
|
|
396
|
+
const indexPath = join(codexHome(), "session_index.jsonl");
|
|
397
|
+
if (!existsSync(indexPath))
|
|
398
|
+
return map;
|
|
399
|
+
for (const line of readFileSync(indexPath, "utf8").split(/\r?\n/)) {
|
|
400
|
+
const record = objectValue(parseJson(line));
|
|
401
|
+
const id = stringValue(record.id);
|
|
402
|
+
const name = stringValue(record.thread_name);
|
|
403
|
+
if (id && name)
|
|
404
|
+
map.set(id, name);
|
|
405
|
+
}
|
|
406
|
+
return map;
|
|
407
|
+
}
|
|
408
|
+
function detectSource(path) {
|
|
409
|
+
// Only Gemini writes a whole session as one JSON document, and only Antigravity keeps one in SQLite.
|
|
410
|
+
if (/\.json$/i.test(path))
|
|
411
|
+
return "gemini";
|
|
412
|
+
if (/\.db$/i.test(path))
|
|
413
|
+
return "antigravity";
|
|
414
|
+
for (const line of readHead(path, 64_000).split(/\r?\n/).slice(0, 10)) {
|
|
415
|
+
const record = objectValue(parseJson(line));
|
|
416
|
+
if (record.type === "session_meta" || "payload" in record)
|
|
417
|
+
return "codex";
|
|
418
|
+
if ("projectHash" in record || "$set" in record || "$rewindTo" in record)
|
|
419
|
+
return "gemini";
|
|
420
|
+
if ("sessionId" in record || "message" in record)
|
|
421
|
+
return "claude";
|
|
422
|
+
}
|
|
423
|
+
if (path.includes(join(".gemini", "tmp")))
|
|
424
|
+
return "gemini";
|
|
425
|
+
return path.includes(`${join(".claude", "projects")}`) ? "claude" : "codex";
|
|
426
|
+
}
|
|
427
|
+
function listDirectories(root) {
|
|
428
|
+
try {
|
|
429
|
+
return readdirSync(root, { withFileTypes: true })
|
|
430
|
+
.filter((entry) => entry.isDirectory())
|
|
431
|
+
.map((entry) => entry.name);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Parsing
|
|
439
|
+
class SessionBuilder {
|
|
440
|
+
transcript = [];
|
|
441
|
+
commands = [];
|
|
442
|
+
edits = [];
|
|
443
|
+
plan;
|
|
444
|
+
planText;
|
|
445
|
+
compactSummary;
|
|
446
|
+
links = [];
|
|
447
|
+
seq = 0;
|
|
448
|
+
pendingCommands = new Map();
|
|
449
|
+
message(role, text, timestamp) {
|
|
450
|
+
const clean = cleanText(text);
|
|
451
|
+
if (!isUsefulText(clean))
|
|
452
|
+
return;
|
|
453
|
+
const last = this.transcript.at(-1);
|
|
454
|
+
if (last && last.role === role && last.text === clean)
|
|
455
|
+
return;
|
|
456
|
+
this.seq++;
|
|
457
|
+
this.transcript.push({ role, text: clean, timestamp, kind: "message" });
|
|
458
|
+
}
|
|
459
|
+
toolCall(name, detail, timestamp) {
|
|
460
|
+
this.seq++;
|
|
461
|
+
this.transcript.push({ role: "tool", text: detail ? `${name}: ${truncate(oneLine(detail), 240)}` : name, timestamp, kind: "tool_call" });
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Record a command whose result may arrive later under `callId`. `assumeSuccess` marks logs where a
|
|
465
|
+
* result without an error flag means exit 0 (Claude); elsewhere a missing exit code stays unknown.
|
|
466
|
+
*/
|
|
467
|
+
commandCall(callId, command, timestamp, assumeSuccess = false) {
|
|
468
|
+
if (!command)
|
|
469
|
+
return;
|
|
470
|
+
this.toolCall("Shell", command, timestamp);
|
|
471
|
+
this.recordCommand(command, undefined, "");
|
|
472
|
+
if (callId)
|
|
473
|
+
this.pendingCommands.set(callId, { index: this.commands.length - 1, assumeSuccess });
|
|
474
|
+
}
|
|
475
|
+
commandResult(callId, output, options = {}) {
|
|
476
|
+
const pending = this.pendingCommands.get(callId);
|
|
477
|
+
if (!pending)
|
|
478
|
+
return;
|
|
479
|
+
this.pendingCommands.delete(callId);
|
|
480
|
+
const exitCode = options.exitCode ?? parseExitCode(output) ?? (options.isError ? 1 : pending.assumeSuccess ? 0 : undefined);
|
|
481
|
+
const run = { ...this.commands[pending.index], ...commandOutcome(this.commands[pending.index].command, exitCode, output) };
|
|
482
|
+
this.commands[pending.index] = run;
|
|
483
|
+
if (run.status === "failed")
|
|
484
|
+
this.pushFailure(run);
|
|
485
|
+
}
|
|
486
|
+
/** A command whose call and result are logged together. */
|
|
487
|
+
command(command, exitCode, output, timestamp) {
|
|
488
|
+
if (!command)
|
|
489
|
+
return;
|
|
490
|
+
this.toolCall("Shell", command, timestamp);
|
|
491
|
+
this.recordCommand(command, exitCode, output);
|
|
492
|
+
}
|
|
493
|
+
edit(path, timestamp) {
|
|
494
|
+
if (!path)
|
|
495
|
+
return;
|
|
496
|
+
this.seq++;
|
|
497
|
+
this.edits.push({ path, seq: this.seq });
|
|
498
|
+
this.transcript.push({ role: "tool", text: `Edit: ${path}`, timestamp, kind: "edit" });
|
|
499
|
+
}
|
|
500
|
+
setPlan(items) {
|
|
501
|
+
if (items.length)
|
|
502
|
+
this.plan = items;
|
|
503
|
+
}
|
|
504
|
+
updatePlan(update) {
|
|
505
|
+
if (!this.plan)
|
|
506
|
+
return;
|
|
507
|
+
const plan = this.plan.map((item) => ({ ...item }));
|
|
508
|
+
if (update.advance) {
|
|
509
|
+
const current = plan.findIndex((item) => item.status === "in_progress");
|
|
510
|
+
if (current >= 0)
|
|
511
|
+
plan[current].status = "completed";
|
|
512
|
+
const next = plan.findIndex((item, index) => index > current && item.status === "pending");
|
|
513
|
+
if (next >= 0)
|
|
514
|
+
plan[next].status = "in_progress";
|
|
515
|
+
}
|
|
516
|
+
for (const index of update.done || [])
|
|
517
|
+
if (plan[index - 1])
|
|
518
|
+
plan[index - 1].status = "completed";
|
|
519
|
+
if (update.start && plan[update.start - 1])
|
|
520
|
+
plan[update.start - 1].status = "in_progress";
|
|
521
|
+
this.plan = plan;
|
|
522
|
+
}
|
|
523
|
+
recordCommand(command, exitCode, output) {
|
|
524
|
+
this.seq++;
|
|
525
|
+
const run = { command: truncate(oneLine(command), 400), ...commandOutcome(command, exitCode, output), seq: this.seq };
|
|
526
|
+
this.commands.push(run);
|
|
527
|
+
if (run.status === "failed")
|
|
528
|
+
this.pushFailure(run);
|
|
529
|
+
}
|
|
530
|
+
pushFailure(run) {
|
|
531
|
+
const label = run.exitCode ? `Exit ${run.exitCode}` : "Failed";
|
|
532
|
+
this.transcript.push({ role: "tool", text: `${label}${run.errorLine ? `: ${run.errorLine}` : ""}`, kind: "tool_error" });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function commandOutcome(command, exitCode, output) {
|
|
536
|
+
const status = commandStatus(command, exitCode, output);
|
|
537
|
+
return { status, exitCode, errorLine: status === "failed" ? firstErrorLine(output) : undefined };
|
|
538
|
+
}
|
|
539
|
+
export function parseCodexSession(path, budget = DEFAULT_EXCERPT_TOKENS) {
|
|
37
540
|
const lines = readSessionLines(path);
|
|
38
|
-
const
|
|
541
|
+
const builder = new SessionBuilder();
|
|
39
542
|
let id = sessionIdFromPath(path);
|
|
40
543
|
let cwd;
|
|
41
|
-
|
|
544
|
+
// Newer Codex logs record typed items; older ones mirror messages across event_msg and response_item.
|
|
545
|
+
const mode = lines.some((line) => line.includes('"item_completed"'))
|
|
546
|
+
? "items"
|
|
547
|
+
: lines.some((line) => line.includes('"user_message"'))
|
|
548
|
+
? "events"
|
|
549
|
+
: "responses";
|
|
42
550
|
for (const line of lines) {
|
|
43
|
-
const
|
|
44
|
-
if (!item || typeof item !== "object")
|
|
45
|
-
continue;
|
|
46
|
-
const record = item;
|
|
551
|
+
const record = objectValue(parseJson(line));
|
|
47
552
|
const type = stringValue(record.type);
|
|
553
|
+
const payload = objectValue(record.payload);
|
|
554
|
+
const timestamp = stringValue(record.timestamp) || stringValue(payload.timestamp) || undefined;
|
|
48
555
|
if (type === "session_meta") {
|
|
49
|
-
const payload = objectValue(record.payload);
|
|
50
556
|
id = stringValue(payload.id) || id;
|
|
51
557
|
cwd = stringValue(payload.cwd) || cwd;
|
|
52
|
-
updatedAt = stringValue(payload.timestamp) || updatedAt;
|
|
53
558
|
continue;
|
|
54
559
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (payloadType === "user_message")
|
|
60
|
-
pushEntry(transcript, "user", stringValue(payload.message), timestamp, "message");
|
|
61
|
-
if (payloadType === "agent_message")
|
|
62
|
-
pushEntry(transcript, "assistant", stringValue(payload.message), timestamp, "message");
|
|
560
|
+
if (type === "compacted") {
|
|
561
|
+
const message = stringValue(payload.message);
|
|
562
|
+
if (message.trim())
|
|
563
|
+
builder.compactSummary = cleanText(message);
|
|
63
564
|
continue;
|
|
64
565
|
}
|
|
65
|
-
if (type === "
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const role = stringValue(payload.role) === "user" ? "user" : "assistant";
|
|
70
|
-
pushEntry(transcript, role, collectText(payload.content).join("\n"), timestamp, "message");
|
|
566
|
+
if (type === "event_msg") {
|
|
567
|
+
const eventType = stringValue(payload.type);
|
|
568
|
+
if (mode === "items" && eventType === "item_completed") {
|
|
569
|
+
readCodexItem(builder, objectValue(payload.item), timestamp);
|
|
71
570
|
}
|
|
72
|
-
else if (
|
|
73
|
-
|
|
74
|
-
pushEntry(transcript, "tool", `${name} ${stringValue(payload.arguments)}`.trim(), timestamp, "tool_call");
|
|
571
|
+
else if (mode === "events" && eventType === "user_message") {
|
|
572
|
+
builder.message("user", stringValue(payload.message), timestamp);
|
|
75
573
|
}
|
|
76
|
-
else if (
|
|
77
|
-
|
|
574
|
+
else if (mode === "events" && eventType === "agent_message") {
|
|
575
|
+
builder.message("assistant", stringValue(payload.message), timestamp);
|
|
78
576
|
}
|
|
577
|
+
else if (mode !== "items" && eventType === "exec_command_end") {
|
|
578
|
+
builder.commandResult(stringValue(payload.call_id), outputText(payload.aggregated_output ?? payload.stdout ?? payload.formatted_output), {
|
|
579
|
+
exitCode: numberValue(payload.exit_code)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (type !== "response_item")
|
|
585
|
+
continue;
|
|
586
|
+
const itemType = stringValue(payload.type);
|
|
587
|
+
if (itemType === "message" && mode === "responses") {
|
|
588
|
+
const role = stringValue(payload.role);
|
|
589
|
+
if (role === "user" || role === "assistant")
|
|
590
|
+
builder.message(role, collectText(payload.content).join("\n"), timestamp);
|
|
591
|
+
}
|
|
592
|
+
else if (itemType === "function_call" || itemType === "custom_tool_call" || itemType === "local_shell_call") {
|
|
593
|
+
readCodexCall(builder, payload, mode, timestamp);
|
|
594
|
+
}
|
|
595
|
+
else if ((itemType === "function_call_output" || itemType === "custom_tool_call_output") && mode !== "items") {
|
|
596
|
+
const output = outputText(payload.output);
|
|
597
|
+
builder.commandResult(stringValue(payload.call_id), output, { exitCode: parseExitCode(output) });
|
|
79
598
|
}
|
|
80
599
|
}
|
|
81
600
|
return buildSession({
|
|
@@ -83,39 +602,133 @@ export function parseCodexSession(path, budget = 12_000) {
|
|
|
83
602
|
id,
|
|
84
603
|
path,
|
|
85
604
|
cwd,
|
|
86
|
-
updatedAt,
|
|
87
|
-
matchedCwd: false,
|
|
88
605
|
lineCount: lines.length,
|
|
89
|
-
|
|
90
|
-
budget
|
|
606
|
+
builder,
|
|
607
|
+
budget,
|
|
608
|
+
threadName: undefined
|
|
91
609
|
});
|
|
92
610
|
}
|
|
93
|
-
|
|
611
|
+
function readCodexItem(builder, item, timestamp) {
|
|
612
|
+
switch (stringValue(item.type)) {
|
|
613
|
+
case "UserMessage":
|
|
614
|
+
builder.message("user", collectText(item.content).join("\n"), timestamp);
|
|
615
|
+
return;
|
|
616
|
+
case "AgentMessage":
|
|
617
|
+
builder.message("assistant", collectText(item.content).join("\n"), timestamp);
|
|
618
|
+
return;
|
|
619
|
+
case "CommandExecution": {
|
|
620
|
+
const command = Array.isArray(item.command) ? shellCommand(item.command) : stringValue(item.command);
|
|
621
|
+
builder.command(command, numberValue(item.exit_code) ?? (stringValue(item.status) === "failed" ? 1 : undefined), outputText(item.aggregated_output), timestamp);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
case "FileChange":
|
|
625
|
+
for (const path of Object.keys(objectValue(item.changes)))
|
|
626
|
+
builder.edit(path, timestamp);
|
|
627
|
+
return;
|
|
628
|
+
case "McpToolCall":
|
|
629
|
+
builder.toolCall(`${stringValue(item.server)}.${stringValue(item.tool)}`, stringValue(item.status) === "failed" ? "failed" : "", timestamp);
|
|
630
|
+
return;
|
|
631
|
+
case "TodoList": {
|
|
632
|
+
const items = Array.isArray(item.items) ? item.items.map(objectValue) : [];
|
|
633
|
+
builder.setPlan(items
|
|
634
|
+
.map((entry) => ({ text: oneLine(stringValue(entry.text)), status: entry.completed === true ? "completed" : "pending" }))
|
|
635
|
+
.filter((entry) => entry.text));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function readCodexCall(builder, payload, mode, timestamp) {
|
|
641
|
+
const name = stringValue(payload.name);
|
|
642
|
+
const callId = stringValue(payload.call_id);
|
|
643
|
+
const rawArgs = stringValue(payload.arguments) || stringValue(payload.input);
|
|
644
|
+
const args = objectValue(parseJson(rawArgs));
|
|
645
|
+
if (name === "update_plan") {
|
|
646
|
+
builder.setPlan(planItemsFrom(args.plan));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Items mode records commands and edits as typed items; skip the raw calls to avoid double counting.
|
|
650
|
+
if (mode === "items")
|
|
651
|
+
return;
|
|
652
|
+
if (name === "apply_patch") {
|
|
653
|
+
for (const path of patchFiles(rawArgs))
|
|
654
|
+
builder.edit(path, timestamp);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (stringValue(payload.type) === "local_shell_call") {
|
|
658
|
+
const action = objectValue(payload.action);
|
|
659
|
+
builder.command(Array.isArray(action.command) ? shellCommand(action.command) : "", undefined, "", timestamp);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (name === "shell" || name === "container.exec" || name === "local_shell" || name === "exec_command") {
|
|
663
|
+
const command = Array.isArray(args.command) ? shellCommand(args.command) : stringValue(args.cmd) || stringValue(args.command);
|
|
664
|
+
if (command.includes("apply_patch")) {
|
|
665
|
+
for (const path of patchFiles(command))
|
|
666
|
+
builder.edit(path, timestamp);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
builder.commandCall(callId, command, timestamp);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (name)
|
|
673
|
+
builder.toolCall(name, "", timestamp);
|
|
674
|
+
}
|
|
675
|
+
export function parseClaudeSession(path, budget = DEFAULT_EXCERPT_TOKENS) {
|
|
94
676
|
const lines = readSessionLines(path);
|
|
95
|
-
const
|
|
677
|
+
const builder = new SessionBuilder();
|
|
96
678
|
let id = sessionIdFromPath(path);
|
|
97
679
|
let cwd;
|
|
98
|
-
let
|
|
680
|
+
let title;
|
|
99
681
|
for (const line of lines) {
|
|
100
|
-
const
|
|
101
|
-
|
|
682
|
+
const record = objectValue(parseJson(line));
|
|
683
|
+
const type = stringValue(record.type);
|
|
684
|
+
const recordTitle = claudeTitle(record);
|
|
685
|
+
if (recordTitle) {
|
|
686
|
+
title = recordTitle;
|
|
102
687
|
continue;
|
|
103
|
-
|
|
104
|
-
if (
|
|
688
|
+
}
|
|
689
|
+
if (type === "pr-link" && stringValue(record.prUrl)) {
|
|
690
|
+
builder.links.push(stringValue(record.prUrl));
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (type !== "user" && type !== "assistant")
|
|
694
|
+
continue;
|
|
695
|
+
if (record.isMeta === true || record.isSidechain === true)
|
|
105
696
|
continue;
|
|
106
697
|
id = stringValue(record.sessionId) || id;
|
|
107
698
|
cwd = stringValue(record.cwd) || cwd;
|
|
108
|
-
const timestamp = stringValue(record.timestamp);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
699
|
+
const timestamp = stringValue(record.timestamp) || undefined;
|
|
700
|
+
const content = objectValue(record.message).content;
|
|
701
|
+
if (record.isCompactSummary === true) {
|
|
702
|
+
builder.compactSummary = cleanText(collectText(content).join("\n"));
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (type === "user") {
|
|
706
|
+
if (typeof content === "string") {
|
|
707
|
+
const parsed = parseClaudeUserText(content);
|
|
708
|
+
if (parsed.prompt)
|
|
709
|
+
builder.message("user", parsed.prompt, timestamp);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const texts = [];
|
|
713
|
+
for (const block of Array.isArray(content) ? content.map(objectValue) : []) {
|
|
714
|
+
if (block.type === "text") {
|
|
715
|
+
const parsed = parseClaudeUserText(stringValue(block.text));
|
|
716
|
+
if (parsed.prompt)
|
|
717
|
+
texts.push(parsed.prompt);
|
|
718
|
+
}
|
|
719
|
+
else if (block.type === "tool_result") {
|
|
720
|
+
builder.commandResult(stringValue(block.tool_use_id), collectText(block.content).join("\n"), { isError: block.is_error === true });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (texts.length)
|
|
724
|
+
builder.message("user", texts.join("\n"), timestamp);
|
|
115
725
|
continue;
|
|
116
726
|
}
|
|
117
|
-
|
|
118
|
-
|
|
727
|
+
for (const block of Array.isArray(content) ? content.map(objectValue) : []) {
|
|
728
|
+
if (block.type === "text")
|
|
729
|
+
builder.message("assistant", stringValue(block.text), timestamp);
|
|
730
|
+
else if (block.type === "tool_use")
|
|
731
|
+
readClaudeTool(builder, stringValue(block.name), objectValue(block.input), stringValue(block.id), timestamp);
|
|
119
732
|
}
|
|
120
733
|
}
|
|
121
734
|
return buildSession({
|
|
@@ -123,208 +736,498 @@ export function parseClaudeSession(path, budget = 12_000) {
|
|
|
123
736
|
id,
|
|
124
737
|
path,
|
|
125
738
|
cwd,
|
|
126
|
-
updatedAt,
|
|
127
|
-
matchedCwd: false,
|
|
128
739
|
lineCount: lines.length,
|
|
129
|
-
|
|
740
|
+
builder,
|
|
130
741
|
budget,
|
|
742
|
+
threadName: title,
|
|
131
743
|
supplementalPaths: latestClaudeSupplementalPaths()
|
|
132
744
|
});
|
|
133
745
|
}
|
|
134
|
-
function
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return parseWithMatch(parseClaudeSession(session, budget), cwd);
|
|
139
|
-
return parseWithMatch(parseCodexSession(session, budget), cwd);
|
|
746
|
+
function readClaudeTool(builder, name, input, callId, timestamp) {
|
|
747
|
+
if (name === "Bash" || name === "PowerShell") {
|
|
748
|
+
builder.commandCall(callId, stringValue(input.command), timestamp, input.run_in_background !== true);
|
|
749
|
+
return;
|
|
140
750
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
751
|
+
if (name === "Edit" || name === "Write" || name === "MultiEdit" || name === "NotebookEdit") {
|
|
752
|
+
builder.edit(stringValue(input.file_path) || stringValue(input.notebook_path), timestamp);
|
|
753
|
+
return;
|
|
143
754
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
threadName: index.get(id)
|
|
165
|
-
};
|
|
166
|
-
}).sort(compareCandidates);
|
|
167
|
-
}
|
|
168
|
-
function findClaudeCandidates(cwd) {
|
|
169
|
-
const encoded = encodeClaudeProjectPath(cwd);
|
|
170
|
-
const preferred = join(homedir(), ".claude", "projects", encoded);
|
|
171
|
-
const roots = existsSync(preferred) ? [preferred] : [join(homedir(), ".claude", "projects")];
|
|
172
|
-
return roots
|
|
173
|
-
.flatMap((root) => findRecentFiles(root, 300))
|
|
174
|
-
.filter((path) => path.endsWith(".jsonl"))
|
|
175
|
-
.map((path) => {
|
|
176
|
-
const meta = readClaudeMeta(path);
|
|
177
|
-
return {
|
|
178
|
-
source: "claude",
|
|
179
|
-
path,
|
|
180
|
-
id: meta.id || sessionIdFromPath(path),
|
|
181
|
-
cwd: meta.cwd,
|
|
182
|
-
updatedAt: meta.updatedAt || statSync(path).mtime.toISOString(),
|
|
183
|
-
matchedCwd: samePath(meta.cwd, cwd)
|
|
184
|
-
};
|
|
185
|
-
})
|
|
186
|
-
.sort(compareCandidates);
|
|
755
|
+
if (name === "ExitPlanMode") {
|
|
756
|
+
if (stringValue(input.plan))
|
|
757
|
+
builder.planText = cleanText(stringValue(input.plan));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const planItems = planItemsFrom(input.todos ?? input.items ?? input.plan);
|
|
761
|
+
if (planItems.length) {
|
|
762
|
+
builder.setPlan(planItems);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (/update_plan$/.test(name)) {
|
|
766
|
+
builder.updatePlan({
|
|
767
|
+
advance: input.advance === true,
|
|
768
|
+
done: Array.isArray(input.done) ? input.done.filter((value) => typeof value === "number") : undefined,
|
|
769
|
+
start: numberValue(input.start)
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const detail = stringValue(input.file_path) || stringValue(input.pattern) || stringValue(input.url) || stringValue(input.query) || stringValue(input.description);
|
|
774
|
+
builder.toolCall(name, detail, timestamp);
|
|
187
775
|
}
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
776
|
+
function parseClaudeUserText(text) {
|
|
777
|
+
const commandName = /<command-name>([\s\S]*?)<\/command-name>/.exec(text)?.[1]?.trim();
|
|
778
|
+
if (commandName) {
|
|
779
|
+
const args = /<command-args>([\s\S]*?)<\/command-args>/.exec(text)?.[1]?.trim();
|
|
780
|
+
return { prompt: [commandName, args].filter(Boolean).join(" ") };
|
|
781
|
+
}
|
|
782
|
+
if (/^\s*<(?:local-command-stdout|local-command-stderr|bash-input|bash-stdout|bash-stderr|task-notification)>/.test(text))
|
|
783
|
+
return { prompt: "" };
|
|
784
|
+
if (/^\[Request interrupted by user/.test(text.trim()))
|
|
785
|
+
return { prompt: "" };
|
|
786
|
+
return { prompt: text };
|
|
196
787
|
}
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
788
|
+
function claudeTitle(record) {
|
|
789
|
+
switch (record.type) {
|
|
790
|
+
case "custom-title":
|
|
791
|
+
return stringValue(record.customTitle) || undefined;
|
|
792
|
+
case "ai-title":
|
|
793
|
+
return stringValue(record.aiTitle) || undefined;
|
|
794
|
+
case "summary":
|
|
795
|
+
return stringValue(record.summary) || undefined;
|
|
796
|
+
default:
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
202
799
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
800
|
+
export function parseGeminiSession(path, budget = DEFAULT_EXCERPT_TOKENS) {
|
|
801
|
+
const { metadata, messages, lineCount } = readGeminiConversation(path);
|
|
802
|
+
const builder = new SessionBuilder();
|
|
803
|
+
const id = stringValue(metadata.sessionId) || sessionIdFromPath(path);
|
|
804
|
+
const projectDir = geminiProjectDir(path);
|
|
805
|
+
for (const message of messages) {
|
|
806
|
+
const timestamp = stringValue(message.timestamp) || undefined;
|
|
807
|
+
if (message.type === "user") {
|
|
808
|
+
builder.message("user", geminiUserPrompt(message), timestamp);
|
|
809
|
+
}
|
|
810
|
+
else if (message.type === "gemini") {
|
|
811
|
+
builder.message("assistant", geminiText(message.content), timestamp);
|
|
812
|
+
for (const call of Array.isArray(message.toolCalls) ? message.toolCalls.map(objectValue) : []) {
|
|
813
|
+
readGeminiTool(builder, call, timestamp, projectDir ? join(projectDir, id, "plans") : undefined);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// info, warning, and error records are CLI notices, not conversation.
|
|
817
|
+
}
|
|
818
|
+
return buildSession({
|
|
819
|
+
source: "gemini",
|
|
820
|
+
id,
|
|
821
|
+
path,
|
|
822
|
+
cwd: projectDir ? readGeminiProjectRoot(projectDir) : undefined,
|
|
823
|
+
lineCount,
|
|
824
|
+
builder,
|
|
825
|
+
budget,
|
|
826
|
+
threadName: stringValue(metadata.summary) || undefined
|
|
827
|
+
});
|
|
207
828
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
829
|
+
/**
|
|
830
|
+
* Replays a Gemini chat file the way Gemini CLI loads it: a message record is rewritten in place each time its id
|
|
831
|
+
* repeats, `$set` updates metadata (or replaces every message), and `$rewindTo` drops that message and all after it.
|
|
832
|
+
*/
|
|
833
|
+
function readGeminiConversation(path) {
|
|
834
|
+
if (/\.json$/i.test(path)) {
|
|
835
|
+
const record = objectValue(parseJson(readFileSync(path, "utf8")));
|
|
836
|
+
const messages = Array.isArray(record.messages) ? record.messages.map(objectValue) : [];
|
|
837
|
+
return { metadata: record, messages, lineCount: messages.length };
|
|
838
|
+
}
|
|
839
|
+
const lines = readSessionLines(path);
|
|
840
|
+
const messages = new Map();
|
|
841
|
+
const addMessages = (value) => {
|
|
842
|
+
for (const message of Array.isArray(value) ? value.map(objectValue) : [])
|
|
843
|
+
if (typeof message.id === "string")
|
|
844
|
+
messages.set(message.id, message);
|
|
221
845
|
};
|
|
846
|
+
let metadata = {};
|
|
847
|
+
for (const line of lines) {
|
|
848
|
+
const record = objectValue(parseJson(line));
|
|
849
|
+
if (typeof record.$rewindTo === "string") {
|
|
850
|
+
const ids = [...messages.keys()];
|
|
851
|
+
const index = ids.indexOf(record.$rewindTo);
|
|
852
|
+
for (const messageId of index >= 0 ? ids.slice(index) : ids)
|
|
853
|
+
messages.delete(messageId);
|
|
854
|
+
}
|
|
855
|
+
else if (typeof record.id === "string") {
|
|
856
|
+
messages.set(record.id, record);
|
|
857
|
+
}
|
|
858
|
+
else if (record.$set && typeof record.$set === "object") {
|
|
859
|
+
const update = objectValue(record.$set);
|
|
860
|
+
if (Array.isArray(update.messages)) {
|
|
861
|
+
messages.clear();
|
|
862
|
+
addMessages(update.messages);
|
|
863
|
+
}
|
|
864
|
+
metadata = { ...metadata, ...update };
|
|
865
|
+
}
|
|
866
|
+
else if (typeof record.sessionId === "string") {
|
|
867
|
+
metadata = { ...metadata, ...record };
|
|
868
|
+
addMessages(record.messages);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return { metadata, messages: [...messages.values()], lineCount: lines.length };
|
|
222
872
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (output.length > budget)
|
|
231
|
-
output = output.slice(output.length - budget).replace(/^[\s\S]*?(?=### )/, "");
|
|
232
|
-
return output || "No transcript excerpt captured.";
|
|
873
|
+
const GEMINI_REFERENCED_FILES = "--- Content from referenced files ---";
|
|
874
|
+
/** What the user typed. `displayContent` holds it when `@file` expansion rewrote `content`; tool results carry no text. */
|
|
875
|
+
function geminiUserPrompt(message) {
|
|
876
|
+
if (message.type !== "user")
|
|
877
|
+
return "";
|
|
878
|
+
const text = geminiText(message.displayContent ?? message.content).split(GEMINI_REFERENCED_FILES)[0];
|
|
879
|
+
return cleanText(text);
|
|
233
880
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const payload = objectValue(record.payload);
|
|
243
|
-
return {
|
|
244
|
-
id: stringValue(payload.id),
|
|
245
|
-
cwd: stringValue(payload.cwd),
|
|
246
|
-
updatedAt: stringValue(payload.timestamp)
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
return {};
|
|
881
|
+
function geminiText(value) {
|
|
882
|
+
if (typeof value === "string")
|
|
883
|
+
return value;
|
|
884
|
+
const parts = Array.isArray(value) ? value.map(objectValue) : [objectValue(value)];
|
|
885
|
+
return parts
|
|
886
|
+
.filter((part) => typeof part.text === "string" && part.thought !== true)
|
|
887
|
+
.map((part) => part.text)
|
|
888
|
+
.join("");
|
|
250
889
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
890
|
+
function readGeminiTool(builder, call, messageTimestamp, plansDir) {
|
|
891
|
+
const name = stringValue(call.name);
|
|
892
|
+
const args = objectValue(call.args);
|
|
893
|
+
const status = stringValue(call.status);
|
|
894
|
+
const timestamp = stringValue(call.timestamp) || messageTimestamp;
|
|
895
|
+
switch (name) {
|
|
896
|
+
case "run_shell_command": {
|
|
897
|
+
const command = stringValue(args.command);
|
|
898
|
+
if (status === "cancelled") {
|
|
899
|
+
builder.toolCall("Shell", `${command} (cancelled)`, timestamp);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const output = geminiToolOutput(call);
|
|
903
|
+
const background = args.is_background === true || output.startsWith("Command moved to background");
|
|
904
|
+
// The shell tool appends "Exit Code: N" only when N is non-zero.
|
|
905
|
+
const exitCode = parseExitCode(output) ?? (status === "error" ? 1 : status === "success" && !background ? 0 : undefined);
|
|
906
|
+
builder.command(command, exitCode, output, timestamp);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
case "replace":
|
|
910
|
+
case "write_file":
|
|
911
|
+
if (status !== "error" && status !== "cancelled")
|
|
912
|
+
builder.edit(stringValue(args.file_path), timestamp);
|
|
913
|
+
return;
|
|
914
|
+
case "write_todos":
|
|
915
|
+
builder.setPlan(planItemsFrom(args.todos));
|
|
916
|
+
return;
|
|
917
|
+
case "exit_plan_mode": {
|
|
918
|
+
const planFile = plansDir && stringValue(args.plan_filename) ? join(plansDir, basename(stringValue(args.plan_filename))) : "";
|
|
919
|
+
if (planFile && existsSync(planFile))
|
|
920
|
+
builder.planText = cleanText(readFileSync(planFile, "utf8"));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
265
923
|
}
|
|
266
|
-
|
|
924
|
+
const detail = stringValue(args.file_path) || stringValue(args.dir_path) || stringValue(args.pattern) || stringValue(args.query) || stringValue(args.url) || stringValue(args.description);
|
|
925
|
+
builder.toolCall(name, detail, timestamp);
|
|
267
926
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
927
|
+
/** The text a tool returned to the model. The shell tool prefixes the command's output with "Output: ". */
|
|
928
|
+
function geminiToolOutput(call) {
|
|
929
|
+
const texts = (Array.isArray(call.result) ? call.result.map(objectValue) : [])
|
|
930
|
+
.flatMap((part) => {
|
|
931
|
+
const response = objectValue(objectValue(part.functionResponse).response);
|
|
932
|
+
return [stringValue(response.output), stringValue(response.error), stringValue(part.text)];
|
|
933
|
+
})
|
|
934
|
+
.filter(Boolean);
|
|
935
|
+
const text = texts.length ? texts.join("\n") : stringValue(call.resultDisplay);
|
|
936
|
+
return text.replace(/^Output: /, "");
|
|
937
|
+
}
|
|
938
|
+
// Field numbers below come from the protos agy embeds: gemini_coder.Step and the exa.cortex_pb step payloads.
|
|
939
|
+
const AGY_STEP = { userInput: 14, plannerResponse: 15, errorMessage: 17, checkpoint: 23, commandStatus: 28 };
|
|
940
|
+
const AGY_STATUS = { cleared: 5, canceled: 6, error: 7 };
|
|
941
|
+
const AGY_EDIT_TOOLS = new Set(["replace_file_content", "multi_replace_file_content", "write_to_file"]);
|
|
942
|
+
export function parseAntigravitySession(path, budget = DEFAULT_EXCERPT_TOKENS) {
|
|
943
|
+
const db = openAntigravityDb(path);
|
|
944
|
+
if (!db)
|
|
945
|
+
throw new CtxError(`Could not open the Antigravity conversation ${path}.`, "Check that the file exists and is readable.");
|
|
946
|
+
const builder = new SessionBuilder();
|
|
947
|
+
const id = antigravityId(path);
|
|
948
|
+
let metadata = {};
|
|
949
|
+
let stepCount = 0;
|
|
950
|
+
let checkpointTitle;
|
|
951
|
+
try {
|
|
952
|
+
metadata = antigravityTrajectoryMetadata(db);
|
|
953
|
+
for (const row of db.prepare("SELECT step_payload FROM steps ORDER BY idx").all()) {
|
|
954
|
+
if (!(row.step_payload instanceof Uint8Array))
|
|
955
|
+
continue;
|
|
956
|
+
stepCount++;
|
|
957
|
+
checkpointTitle = readAntigravityStep(builder, pbDecode(row.step_payload)) || checkpointTitle;
|
|
958
|
+
}
|
|
282
959
|
}
|
|
283
|
-
|
|
960
|
+
finally {
|
|
961
|
+
db.close();
|
|
962
|
+
}
|
|
963
|
+
const summary = readAntigravityIndex().get(id);
|
|
964
|
+
const firstPrompt = builder.transcript.find((entry) => entry.role === "user")?.text;
|
|
965
|
+
return buildSession({
|
|
966
|
+
source: "antigravity",
|
|
967
|
+
id,
|
|
968
|
+
path,
|
|
969
|
+
cwd: metadata.cwd || summary?.cwd,
|
|
970
|
+
lineCount: stepCount,
|
|
971
|
+
builder,
|
|
972
|
+
budget,
|
|
973
|
+
threadName: summary?.title || checkpointTitle || (firstPrompt ? truncate(oneLine(firstPrompt), 80) : undefined)
|
|
974
|
+
});
|
|
284
975
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
976
|
+
/** Feeds one gemini_coder.Step into the builder; returns a conversation title when a checkpoint carries one. */
|
|
977
|
+
function readAntigravityStep(builder, step) {
|
|
978
|
+
const type = pbInt(step, 1);
|
|
979
|
+
const status = pbInt(step, 4);
|
|
980
|
+
const metadata = pbMessage(step, 5);
|
|
981
|
+
const timestamp = pbTimestamp(pbMessage(metadata, 1));
|
|
982
|
+
// Steps that Antigravity has compacted out of context keep their metadata but lose their payload, so old
|
|
983
|
+
// prompts and replies come back empty here; the latest checkpoint summarizes them.
|
|
984
|
+
switch (type) {
|
|
985
|
+
case AGY_STEP.userInput:
|
|
986
|
+
builder.message("user", antigravityPrompt(pbMessage(step, 19)), timestamp);
|
|
987
|
+
return undefined;
|
|
988
|
+
case AGY_STEP.plannerResponse: {
|
|
989
|
+
const response = pbMessage(step, 20);
|
|
990
|
+
builder.message("assistant", pbString(response, 8) || pbString(response, 1), timestamp);
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
case AGY_STEP.checkpoint: {
|
|
994
|
+
const checkpoint = pbMessage(step, 30);
|
|
995
|
+
const summary = pbString(checkpoint, 5);
|
|
996
|
+
if (summary.trim())
|
|
997
|
+
builder.compactSummary = cleanText(summary);
|
|
998
|
+
return pbString(checkpoint, 10) || undefined;
|
|
999
|
+
}
|
|
1000
|
+
case AGY_STEP.commandStatus: {
|
|
1001
|
+
const result = pbMessage(step, 37);
|
|
1002
|
+
builder.commandResult(pbString(result, 1), pbString(result, 9) || pbString(result, 3), { exitCode: pbInt(result, 5) });
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
case AGY_STEP.errorMessage:
|
|
1006
|
+
// The tool call failed before it ran.
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
const call = pbMessage(metadata, 4);
|
|
1010
|
+
const name = pbString(call, 2);
|
|
1011
|
+
// System and ephemeral messages carry no tool call.
|
|
1012
|
+
if (name)
|
|
1013
|
+
readAntigravityTool(builder, name, objectValue(parseJson(pbString(call, 3))), step, status, timestamp);
|
|
1014
|
+
return undefined;
|
|
290
1015
|
}
|
|
291
|
-
|
|
292
|
-
|
|
1016
|
+
/** CortexStepUserInput: what the user sent (2), else its text items (3), else the raw query (1). */
|
|
1017
|
+
function antigravityPrompt(input) {
|
|
1018
|
+
return pbString(input, 2) || pbMessages(input, 3).map((item) => pbString(item, 1)).join("") || pbString(input, 1);
|
|
293
1019
|
}
|
|
294
|
-
function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
1020
|
+
function readAntigravityTool(builder, name, args, step, status, timestamp) {
|
|
1021
|
+
// Newer agy builds run every tool as a generic step (140) whose result carries the typed step as a google.protobuf.Any.
|
|
1022
|
+
const result = pbMessage(pbMessage(step, 140), 2);
|
|
1023
|
+
const resultText = pbString(result, 1);
|
|
1024
|
+
const payload = pbMessage(result, 6);
|
|
1025
|
+
const typed = pbString(payload, 1).endsWith("gemini_coder.Step") ? pbDecode(pbBytes(payload, 2)) : step;
|
|
1026
|
+
if (name === "run_command") {
|
|
1027
|
+
const run = pbMessage(typed, 28);
|
|
1028
|
+
const command = stringValue(args.CommandLine) || pbString(run, 23) || pbString(run, 1);
|
|
1029
|
+
if (status === AGY_STATUS.canceled || pbInt(run, 14) === 1 || /^Encountered error in step execution: context canceled/.test(resultText.trim())) {
|
|
1030
|
+
builder.toolCall("Shell", `${command} (cancelled)`, timestamp);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const exitCode = pbInt(run, 6) ?? numberFromMatch(/exited with code (-?\d+)/.exec(resultText)) ?? (status === AGY_STATUS.error ? 1 : undefined);
|
|
1034
|
+
const output = pbString(pbMessage(run, 21), 1) || resultText;
|
|
1035
|
+
const commandId = pbString(run, 13);
|
|
1036
|
+
// A command that went to the background reports its exit code in a later command_status step.
|
|
1037
|
+
if (exitCode === undefined && commandId && status !== AGY_STATUS.cleared)
|
|
1038
|
+
builder.commandCall(commandId, command, timestamp);
|
|
1039
|
+
else
|
|
1040
|
+
builder.command(command, exitCode, output, timestamp);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (AGY_EDIT_TOOLS.has(name)) {
|
|
1044
|
+
const file = stringValue(args.TargetFile);
|
|
1045
|
+
// Plans and walkthroughs that agy writes to its own brain folder are not repo edits.
|
|
1046
|
+
if (file && status !== AGY_STATUS.canceled && status !== AGY_STATUS.error && !isInside(resolve(file), resolve(antigravityHome())))
|
|
1047
|
+
builder.edit(file, timestamp);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const detail = stringValue(args.AbsolutePath) || stringValue(args.DirectoryPath) || stringValue(args.SearchPath) || stringValue(args.Query) || stringValue(args.query) || stringValue(args.Url);
|
|
1051
|
+
builder.toolCall(name, detail, timestamp);
|
|
299
1052
|
}
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
if (stats.size <= 4_000_000)
|
|
303
|
-
return readLines(path);
|
|
304
|
-
const head = readSlice(path, 0, 512_000);
|
|
305
|
-
const tailLength = Math.min(2_000_000, stats.size);
|
|
306
|
-
const tail = readSlice(path, stats.size - tailLength, tailLength);
|
|
307
|
-
return `${head}\n${tail}`.split(/\r?\n/).filter(Boolean);
|
|
1053
|
+
function numberFromMatch(match) {
|
|
1054
|
+
return match ? Number(match[1]) : undefined;
|
|
308
1055
|
}
|
|
309
|
-
|
|
310
|
-
|
|
1056
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
1057
|
+
let sqliteModule;
|
|
1058
|
+
/** node:sqlite prints an ExperimentalWarning on load in Node 22; keep CLI output clean. */
|
|
1059
|
+
function loadSqlite() {
|
|
1060
|
+
if (sqliteModule)
|
|
1061
|
+
return sqliteModule;
|
|
1062
|
+
const emitWarning = process.emitWarning;
|
|
1063
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
1064
|
+
const message = typeof warning === "string" ? warning : warning.message;
|
|
1065
|
+
if (/SQLite is an experimental feature/i.test(message))
|
|
1066
|
+
return;
|
|
1067
|
+
return emitWarning.call(process, warning, ...rest);
|
|
1068
|
+
});
|
|
311
1069
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
1070
|
+
sqliteModule = requireBuiltin("node:sqlite");
|
|
1071
|
+
return sqliteModule;
|
|
315
1072
|
}
|
|
316
1073
|
finally {
|
|
317
|
-
|
|
1074
|
+
process.emitWarning = emitWarning;
|
|
318
1075
|
}
|
|
319
1076
|
}
|
|
320
|
-
|
|
1077
|
+
/**
|
|
1078
|
+
* Opens an Antigravity database without changing anything next to it. A running agy keeps its newest steps in the
|
|
1079
|
+
* -wal file, so read through it while agy has it open. Otherwise open the file as immutable: a plain read-only open
|
|
1080
|
+
* of a WAL database would create -wal and -shm files in agy's folder.
|
|
1081
|
+
*/
|
|
1082
|
+
function openAntigravityDb(path) {
|
|
1083
|
+
if (!existsSync(path))
|
|
1084
|
+
return undefined;
|
|
1085
|
+
const { DatabaseSync } = loadSqlite();
|
|
1086
|
+
if (existsSync(`${path}-wal`) && existsSync(`${path}-shm`)) {
|
|
1087
|
+
try {
|
|
1088
|
+
return new DatabaseSync(path, { readOnly: true });
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// Fall back to the last checkpointed state.
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
321
1094
|
try {
|
|
322
|
-
|
|
1095
|
+
const url = pathToFileURL(path);
|
|
1096
|
+
url.searchParams.set("immutable", "1");
|
|
1097
|
+
return new DatabaseSync(url, { readOnly: true });
|
|
323
1098
|
}
|
|
324
1099
|
catch {
|
|
325
1100
|
return undefined;
|
|
326
1101
|
}
|
|
327
1102
|
}
|
|
1103
|
+
function buildSession(args) {
|
|
1104
|
+
const { builder } = args;
|
|
1105
|
+
return {
|
|
1106
|
+
source: args.source,
|
|
1107
|
+
id: args.id,
|
|
1108
|
+
path: args.path,
|
|
1109
|
+
cwd: args.cwd,
|
|
1110
|
+
updatedAt: mtimeIso(args.path),
|
|
1111
|
+
matchedCwd: false,
|
|
1112
|
+
lineCount: args.lineCount,
|
|
1113
|
+
transcript: builder.transcript,
|
|
1114
|
+
transcriptExcerpt: renderTranscriptExcerpt(builder.transcript, args.budget),
|
|
1115
|
+
threadName: args.threadName,
|
|
1116
|
+
supplementalPaths: args.supplementalPaths,
|
|
1117
|
+
plan: builder.plan,
|
|
1118
|
+
planText: builder.planText,
|
|
1119
|
+
commands: builder.commands,
|
|
1120
|
+
edits: builder.edits,
|
|
1121
|
+
compactSummary: builder.compactSummary,
|
|
1122
|
+
links: unique(builder.links)
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/** Most recent conversation (and failures) that fits the token budget, oldest first. Routine tool calls are left out. */
|
|
1126
|
+
export function renderTranscriptExcerpt(entries, budgetTokens = DEFAULT_EXCERPT_TOKENS) {
|
|
1127
|
+
const maxChars = Math.max(500, budgetTokens * 4);
|
|
1128
|
+
const rendered = [];
|
|
1129
|
+
let used = 0;
|
|
1130
|
+
for (const entry of [...entries].reverse()) {
|
|
1131
|
+
if (entry.kind === "tool_call" || entry.kind === "edit")
|
|
1132
|
+
continue;
|
|
1133
|
+
const text = entry.role === "tool" ? entry.text : truncate(entry.text, 1_500);
|
|
1134
|
+
const line = `[${entry.role}] ${text}`;
|
|
1135
|
+
if (used + line.length > maxChars)
|
|
1136
|
+
break;
|
|
1137
|
+
rendered.unshift(line);
|
|
1138
|
+
used += line.length + 1;
|
|
1139
|
+
}
|
|
1140
|
+
return rendered.join("\n");
|
|
1141
|
+
}
|
|
1142
|
+
function latestClaudeSupplementalPaths() {
|
|
1143
|
+
const root = claudeHome();
|
|
1144
|
+
return unique([...findRecentFiles(join(root, "plans"), 3, /\.md$/i), ...findRecentFiles(join(root, "tasks"), 3, /\.json$/i)]);
|
|
1145
|
+
}
|
|
1146
|
+
// ---------------------------------------------------------------------------
|
|
1147
|
+
// Text helpers
|
|
1148
|
+
const NOISE_BLOCKS = [
|
|
1149
|
+
"system-reminder",
|
|
1150
|
+
"local-command-caveat",
|
|
1151
|
+
"permissions instructions",
|
|
1152
|
+
"app-context",
|
|
1153
|
+
"collaboration_mode",
|
|
1154
|
+
"skills_instructions",
|
|
1155
|
+
"plugins_instructions",
|
|
1156
|
+
"environment_context",
|
|
1157
|
+
"user_instructions",
|
|
1158
|
+
"session_context",
|
|
1159
|
+
"hook_context",
|
|
1160
|
+
"INSTRUCTIONS"
|
|
1161
|
+
];
|
|
1162
|
+
function cleanText(text) {
|
|
1163
|
+
let next = text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "");
|
|
1164
|
+
for (const tag of NOISE_BLOCKS)
|
|
1165
|
+
next = next.replace(new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, "g"), "");
|
|
1166
|
+
return next.replace(/"encrypted_content":"[^"]+"/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1167
|
+
}
|
|
1168
|
+
function isUsefulText(text) {
|
|
1169
|
+
if (!text || text.length < 2)
|
|
1170
|
+
return false;
|
|
1171
|
+
if (/^(?:# AGENTS\.md instructions|# Handoff[:\s·]|<environment_context>|<user_instructions>|<turn_aborted>)/.test(text))
|
|
1172
|
+
return false;
|
|
1173
|
+
if (/You are Codex|Knowledge cutoff:|Filesystem sandboxing defines|DO NOT respond to these messages/.test(text))
|
|
1174
|
+
return false;
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
function planItemsFrom(value) {
|
|
1178
|
+
if (!Array.isArray(value))
|
|
1179
|
+
return [];
|
|
1180
|
+
return value
|
|
1181
|
+
.map(objectValue)
|
|
1182
|
+
.map((entry) => ({
|
|
1183
|
+
text: oneLine(stringValue(entry.content) || stringValue(entry.step) || stringValue(entry.text) || stringValue(entry.subject) || stringValue(entry.description)),
|
|
1184
|
+
status: planStatus(entry.status)
|
|
1185
|
+
}))
|
|
1186
|
+
.filter((entry) => entry.text && entry.status !== undefined);
|
|
1187
|
+
}
|
|
1188
|
+
function planStatus(value) {
|
|
1189
|
+
if (value === "completed" || value === "done")
|
|
1190
|
+
return "completed";
|
|
1191
|
+
if (value === "in_progress" || value === "in-progress" || value === "active")
|
|
1192
|
+
return "in_progress";
|
|
1193
|
+
if (value === "pending" || value === "todo" || value === "not_started" || value === "blocked")
|
|
1194
|
+
return "pending";
|
|
1195
|
+
return undefined;
|
|
1196
|
+
}
|
|
1197
|
+
function patchFiles(patch) {
|
|
1198
|
+
return unique(Array.from(patch.matchAll(/\*\*\* (?:Add|Update|Delete) File: ([^\n\\"]+)/g), (match) => match[1].trim()));
|
|
1199
|
+
}
|
|
1200
|
+
function shellCommand(parts) {
|
|
1201
|
+
const strings = parts.filter((part) => typeof part === "string");
|
|
1202
|
+
if (strings.length >= 3 && /(?:^|\/)(?:ba|z|fi)?sh$/.test(strings[0]) && /^-l?c$/.test(strings[1]))
|
|
1203
|
+
return strings.slice(2).join(" ");
|
|
1204
|
+
return strings.join(" ");
|
|
1205
|
+
}
|
|
1206
|
+
function parseExitCode(output) {
|
|
1207
|
+
const match = /"exit_code"\s*:\s*(-?\d+)/.exec(output) ||
|
|
1208
|
+
/Process exited with code (-?\d+)/i.exec(output) ||
|
|
1209
|
+
/^Exit code:?\s*(-?\d+)/im.exec(output);
|
|
1210
|
+
return match ? Number(match[1]) : undefined;
|
|
1211
|
+
}
|
|
1212
|
+
function firstErrorLine(output) {
|
|
1213
|
+
const lines = output
|
|
1214
|
+
.split(/\r?\n/)
|
|
1215
|
+
.map((line) => line.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "").trim())
|
|
1216
|
+
.filter((line) => line && !/^(?:Exit code|Process exited|Wall time|Chunk ID|Output:)/i.test(line));
|
|
1217
|
+
const line = lines.find((candidate) => /(error|fail|exception|traceback|cannot|not found|denied|panic|assert|expected)/i.test(candidate)) || lines[0];
|
|
1218
|
+
return line ? truncate(line, 200) : undefined;
|
|
1219
|
+
}
|
|
1220
|
+
function outputText(value) {
|
|
1221
|
+
if (typeof value === "string") {
|
|
1222
|
+
const parsed = objectValue(parseJson(value));
|
|
1223
|
+
if (typeof parsed.output === "string") {
|
|
1224
|
+
const exit = numberValue(objectValue(parsed.metadata).exit_code);
|
|
1225
|
+
return exit === undefined ? parsed.output : `Exit code: ${exit}\n${parsed.output}`;
|
|
1226
|
+
}
|
|
1227
|
+
return value;
|
|
1228
|
+
}
|
|
1229
|
+
return collectText(value).join("\n");
|
|
1230
|
+
}
|
|
328
1231
|
function collectText(value) {
|
|
329
1232
|
if (typeof value === "string")
|
|
330
1233
|
return [value];
|
|
@@ -333,78 +1236,168 @@ function collectText(value) {
|
|
|
333
1236
|
if (!value || typeof value !== "object")
|
|
334
1237
|
return [];
|
|
335
1238
|
const record = value;
|
|
336
|
-
return ["text", "content", "
|
|
1239
|
+
return ["text", "content", "output"].flatMap((key) => collectText(record[key]));
|
|
337
1240
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
entries.push({ role, text: clean, timestamp, kind });
|
|
343
|
-
}
|
|
344
|
-
function cleanText(text) {
|
|
345
|
-
return text
|
|
346
|
-
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
347
|
-
.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, "")
|
|
348
|
-
.replace(/<permissions instructions>[\s\S]*?<\/permissions instructions>/g, "")
|
|
349
|
-
.replace(/<app-context>[\s\S]*?<\/app-context>/g, "")
|
|
350
|
-
.replace(/<collaboration_mode>[\s\S]*?<\/collaboration_mode>/g, "")
|
|
351
|
-
.replace(/<skills_instructions>[\s\S]*?<\/skills_instructions>/g, "")
|
|
352
|
-
.replace(/<plugins_instructions>[\s\S]*?<\/plugins_instructions>/g, "")
|
|
353
|
-
.replace(/"encrypted_content":"[^"]+"/g, "")
|
|
354
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
355
|
-
.trim();
|
|
1241
|
+
function sessionIdFromPath(path) {
|
|
1242
|
+
return basename(path)
|
|
1243
|
+
.replace(/\.(jsonl|json)$/i, "")
|
|
1244
|
+
.replace(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-/, "");
|
|
356
1245
|
}
|
|
357
|
-
function
|
|
358
|
-
if (!
|
|
359
|
-
return false;
|
|
360
|
-
if (text.includes("base_instructions"))
|
|
1246
|
+
function matchesWorkspace(sessionCwd, scope) {
|
|
1247
|
+
if (!sessionCwd)
|
|
361
1248
|
return false;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return false;
|
|
378
|
-
if (text.includes("# Handoff:"))
|
|
379
|
-
return false;
|
|
380
|
-
if (text.includes("DO NOT respond to these messages"))
|
|
381
|
-
return false;
|
|
382
|
-
if (text.includes("deferred_tools_delta"))
|
|
383
|
-
return false;
|
|
384
|
-
return true;
|
|
1249
|
+
const target = canonicalPath(sessionCwd);
|
|
1250
|
+
if (target === scope.cwd)
|
|
1251
|
+
return true;
|
|
1252
|
+
return Boolean(scope.repoRoot && isInside(target, scope.repoRoot));
|
|
1253
|
+
}
|
|
1254
|
+
function isInside(path, root) {
|
|
1255
|
+
const rel = relative(root, path);
|
|
1256
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
1257
|
+
}
|
|
1258
|
+
function toScope(cwd, repoRoot) {
|
|
1259
|
+
return { cwd: canonicalPath(cwd), repoRoot: repoRoot ? canonicalPath(repoRoot) : undefined };
|
|
1260
|
+
}
|
|
1261
|
+
/** Claude Code stores projects under the cwd with every non-alphanumeric character replaced by "-". */
|
|
1262
|
+
export function encodeClaudeProjectPath(cwd) {
|
|
1263
|
+
return resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
385
1264
|
}
|
|
386
1265
|
function objectValue(value) {
|
|
387
|
-
return value && typeof value === "object" ? value : {};
|
|
1266
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
388
1267
|
}
|
|
389
1268
|
function stringValue(value) {
|
|
390
1269
|
return typeof value === "string" ? value : "";
|
|
391
1270
|
}
|
|
392
|
-
function
|
|
393
|
-
return
|
|
1271
|
+
function numberValue(value) {
|
|
1272
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
394
1273
|
}
|
|
395
|
-
function
|
|
396
|
-
if (!
|
|
397
|
-
return
|
|
398
|
-
|
|
1274
|
+
function parseJson(line) {
|
|
1275
|
+
if (!line)
|
|
1276
|
+
return undefined;
|
|
1277
|
+
try {
|
|
1278
|
+
return JSON.parse(line);
|
|
1279
|
+
}
|
|
1280
|
+
catch {
|
|
1281
|
+
return undefined;
|
|
1282
|
+
}
|
|
399
1283
|
}
|
|
400
|
-
function
|
|
401
|
-
|
|
402
|
-
|
|
1284
|
+
function mtimeIso(path) {
|
|
1285
|
+
return statSync(path).mtime.toISOString();
|
|
1286
|
+
}
|
|
1287
|
+
const utf8 = new TextDecoder();
|
|
1288
|
+
/** One level of a serialized message. Nested messages stay as bytes until asked for; a malformed tail is dropped. */
|
|
1289
|
+
function pbDecode(bytes) {
|
|
1290
|
+
const fields = new Map();
|
|
1291
|
+
if (!bytes)
|
|
1292
|
+
return fields;
|
|
1293
|
+
let offset = 0;
|
|
1294
|
+
const varint = () => {
|
|
1295
|
+
let value = 0n;
|
|
1296
|
+
for (let shift = 0n; offset < bytes.length; shift += 7n) {
|
|
1297
|
+
const byte = bytes[offset++];
|
|
1298
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
1299
|
+
if (byte < 0x80)
|
|
1300
|
+
return value;
|
|
1301
|
+
}
|
|
1302
|
+
throw new Error("truncated varint");
|
|
1303
|
+
};
|
|
1304
|
+
try {
|
|
1305
|
+
while (offset < bytes.length) {
|
|
1306
|
+
const key = varint();
|
|
1307
|
+
const field = Number(key >> 3n);
|
|
1308
|
+
const wire = Number(key & 7n);
|
|
1309
|
+
let value;
|
|
1310
|
+
if (wire === 0) {
|
|
1311
|
+
value = varint();
|
|
1312
|
+
}
|
|
1313
|
+
else if (wire === 2) {
|
|
1314
|
+
const length = Number(varint());
|
|
1315
|
+
if (offset + length > bytes.length)
|
|
1316
|
+
break;
|
|
1317
|
+
value = bytes.subarray(offset, offset + length);
|
|
1318
|
+
offset += length;
|
|
1319
|
+
}
|
|
1320
|
+
else if (wire === 1 || wire === 5) {
|
|
1321
|
+
offset += wire === 1 ? 8 : 4;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
const values = fields.get(field);
|
|
1328
|
+
if (values)
|
|
1329
|
+
values.push(value);
|
|
1330
|
+
else
|
|
1331
|
+
fields.set(field, [value]);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
// Keep the fields read so far.
|
|
1336
|
+
}
|
|
1337
|
+
return fields;
|
|
1338
|
+
}
|
|
1339
|
+
function pbBytes(message, field) {
|
|
1340
|
+
const value = message.get(field)?.at(-1);
|
|
1341
|
+
return value instanceof Uint8Array ? value : undefined;
|
|
1342
|
+
}
|
|
1343
|
+
function pbString(message, field) {
|
|
1344
|
+
const bytes = pbBytes(message, field);
|
|
1345
|
+
return bytes ? utf8.decode(bytes) : "";
|
|
1346
|
+
}
|
|
1347
|
+
function pbMessage(message, field) {
|
|
1348
|
+
return pbDecode(pbBytes(message, field));
|
|
1349
|
+
}
|
|
1350
|
+
function pbMessages(message, field) {
|
|
1351
|
+
return (message.get(field) || []).filter((value) => value instanceof Uint8Array).map(pbDecode);
|
|
1352
|
+
}
|
|
1353
|
+
/** An int32/enum/bool field; undefined when absent. */
|
|
1354
|
+
function pbInt(message, field) {
|
|
1355
|
+
const value = message.get(field)?.at(-1);
|
|
1356
|
+
return typeof value === "bigint" ? Number(BigInt.asIntN(32, value)) : undefined;
|
|
1357
|
+
}
|
|
1358
|
+
/** A google.protobuf.Timestamp as an ISO string. */
|
|
1359
|
+
function pbTimestamp(timestamp) {
|
|
1360
|
+
const seconds = timestamp.get(1)?.at(-1);
|
|
1361
|
+
if (typeof seconds !== "bigint")
|
|
1362
|
+
return undefined;
|
|
1363
|
+
const nanos = timestamp.get(2)?.at(-1);
|
|
1364
|
+
return new Date(Number(seconds) * 1000 + (typeof nanos === "bigint" ? Number(nanos) / 1e6 : 0)).toISOString();
|
|
1365
|
+
}
|
|
1366
|
+
// ---------------------------------------------------------------------------
|
|
1367
|
+
// File reading
|
|
1368
|
+
function readSessionLines(path) {
|
|
1369
|
+
const size = statSync(path).size;
|
|
1370
|
+
const text = size <= 16_000_000 ? readFileSync(path, "utf8") : `${readSlice(path, 0, 2_000_000)}\n${readSlice(path, size - 10_000_000, 10_000_000)}`;
|
|
1371
|
+
return text.split(/\r?\n/).filter(Boolean);
|
|
1372
|
+
}
|
|
1373
|
+
function readFirstLine(path, maxBytes = 2_000_000) {
|
|
1374
|
+
let length = 32_000;
|
|
1375
|
+
while (true) {
|
|
1376
|
+
const chunk = readSlice(path, 0, length);
|
|
1377
|
+
const newline = chunk.indexOf("\n");
|
|
1378
|
+
if (newline >= 0)
|
|
1379
|
+
return chunk.slice(0, newline);
|
|
1380
|
+
if (chunk.length < length || length >= maxBytes)
|
|
1381
|
+
return chunk;
|
|
1382
|
+
length *= 4;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function readHead(path, bytes) {
|
|
1386
|
+
return readSlice(path, 0, bytes);
|
|
403
1387
|
}
|
|
404
|
-
function
|
|
405
|
-
|
|
1388
|
+
function readTail(path, bytes) {
|
|
1389
|
+
const size = statSync(path).size;
|
|
1390
|
+
return readSlice(path, Math.max(0, size - bytes), Math.min(bytes, size));
|
|
406
1391
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
1392
|
+
function readSlice(path, position, length) {
|
|
1393
|
+
const fd = openSync(path, "r");
|
|
1394
|
+
try {
|
|
1395
|
+
const buffer = Buffer.alloc(length);
|
|
1396
|
+
const bytesRead = readSync(fd, buffer, 0, length, position);
|
|
1397
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
1398
|
+
}
|
|
1399
|
+
finally {
|
|
1400
|
+
closeSync(fd);
|
|
1401
|
+
}
|
|
409
1402
|
}
|
|
410
1403
|
//# sourceMappingURL=session.js.map
|