@groundtrack/hooks 0.1.9 → 0.1.11
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 +28 -10
- package/dist/agents.d.ts +55 -0
- package/dist/agents.js +77 -3
- package/dist/groundtrack-hook.js +0 -0
- package/dist/groundtrack.js +0 -0
- package/dist/session-import/canonical.d.ts +33 -0
- package/dist/session-import/canonical.js +10 -0
- package/dist/session-import/claude.d.ts +20 -0
- package/dist/session-import/claude.js +237 -0
- package/dist/session-import/cli.d.ts +3 -2
- package/dist/session-import/cli.js +161 -131
- package/dist/session-import/client.d.ts +4 -3
- package/dist/session-import/client.js +4 -3
- package/dist/session-import/codex.d.ts +6 -33
- package/dist/session-import/codex.js +62 -9
- package/dist/session-import/cursor.d.ts +13 -0
- package/dist/session-import/cursor.js +268 -0
- package/dist/session-import/discovery.d.ts +16 -7
- package/dist/session-import/discovery.js +127 -70
- package/dist/session-import/format.d.ts +51 -0
- package/dist/session-import/format.js +11 -0
- package/dist/session-import/formats.d.ts +4 -0
- package/dist/session-import/formats.js +13 -0
- package/dist/session-import/normalize.d.ts +21 -0
- package/dist/session-import/normalize.js +100 -0
- package/dist/session-import/opencode-cli.d.ts +14 -0
- package/dist/session-import/opencode-cli.js +178 -0
- package/dist/session-import/opencode-normalizer.d.ts +8 -0
- package/dist/session-import/opencode-normalizer.js +230 -0
- package/dist/session-import/opencode.d.ts +8 -0
- package/dist/session-import/opencode.js +230 -0
- package/dist/session-import/prune.d.ts +13 -4
- package/dist/session-import/prune.js +45 -79
- package/dist/session-import/receipts.d.ts +2 -1
- package/dist/session-import/receipts.js +3 -3
- package/dist/session-import/redaction.js +5 -3
- package/dist/session-import/types.d.ts +21 -7
- package/dist/setup/agents.d.ts +32 -0
- package/package.json +29 -1
- package/src/agents.ts +104 -3
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import * as prompts from "@clack/prompts";
|
|
3
|
+
import { DEFAULT_SESSION_IMPORT_AGENT, isSessionImportAgent, SESSION_IMPORT_AGENTS, } from "../agents.js";
|
|
3
4
|
import { environment } from "../setup/agents.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CANONICAL_IMPORT_MAX_BYTES } from "./canonical.js";
|
|
6
|
+
import { uploadAgentSession } from "./client.js";
|
|
7
|
+
import { discoverSessions, normalizeRepositoryUrl, repositoryIdentity } from "./discovery.js";
|
|
8
|
+
import { sessionImportFormat } from "./formats.js";
|
|
9
|
+
import { pruneSession } from "./prune.js";
|
|
7
10
|
import { readReceipts, receiptKey, saveReceipt } from "./receipts.js";
|
|
8
|
-
|
|
11
|
+
const importAgentIds = SESSION_IMPORT_AGENTS.map((agent) => agent.id);
|
|
12
|
+
const importAgentChoices = importAgentIds.join("|");
|
|
13
|
+
const importAgentDescription = importAgentIds.join(", ");
|
|
9
14
|
export const IMPORT_USAGE = `Usage:
|
|
10
|
-
groundtrack import [--agent
|
|
15
|
+
groundtrack import [--agent ${importAgentChoices}] [--repo PATH | --all-repos] [--since DATE]
|
|
11
16
|
[--include-subagents] [--include-unscoped]
|
|
12
17
|
[--visibility private|organization] [--dry-run] [--yes] [--json]
|
|
13
18
|
|
|
14
19
|
Import options:
|
|
15
|
-
--agent
|
|
20
|
+
--agent VALUE ${importAgentDescription} (default: ${DEFAULT_SESSION_IMPORT_AGENT})
|
|
16
21
|
--repo PATH Import sessions for this Git repository (default: current repository)
|
|
17
|
-
--all-repos Import sessions
|
|
18
|
-
--include-unscoped With --all-repos, also import sessions
|
|
22
|
+
--all-repos Import sessions associated with any Git repository
|
|
23
|
+
--include-unscoped With --all-repos, also import sessions outside a Git repository
|
|
19
24
|
--since DATE Import sessions at or after an ISO timestamp or local YYYY-MM-DD
|
|
20
25
|
--include-subagents Include subagent, forked-agent, and guardian sessions
|
|
21
26
|
--visibility VALUE private (default) or organization
|
|
@@ -53,6 +58,7 @@ export function parseImportArguments(argv) {
|
|
|
53
58
|
};
|
|
54
59
|
let repoDir;
|
|
55
60
|
let homeDir;
|
|
61
|
+
let agent = DEFAULT_SESSION_IMPORT_AGENT;
|
|
56
62
|
let allRepositories = false;
|
|
57
63
|
let includeUnscoped = false;
|
|
58
64
|
let includeSubagents = false;
|
|
@@ -67,9 +73,10 @@ export function parseImportArguments(argv) {
|
|
|
67
73
|
for (let index = 0; index < args.length; index += 1) {
|
|
68
74
|
const arg = args[index];
|
|
69
75
|
if (arg === "--agent") {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
throw new Error(
|
|
76
|
+
const value = valueAfter(index++, arg);
|
|
77
|
+
if (!isSessionImportAgent(value))
|
|
78
|
+
throw new Error(`Unsupported session import agent: ${value}.`);
|
|
79
|
+
agent = value;
|
|
73
80
|
}
|
|
74
81
|
else if (arg === "--repo")
|
|
75
82
|
repoDir = valueAfter(index++, arg);
|
|
@@ -109,6 +116,7 @@ export function parseImportArguments(argv) {
|
|
|
109
116
|
if (includeUnscoped && !allRepositories)
|
|
110
117
|
throw new Error("Use --include-unscoped together with --all-repos.");
|
|
111
118
|
return {
|
|
119
|
+
agent,
|
|
112
120
|
repoDir,
|
|
113
121
|
homeDir,
|
|
114
122
|
allRepositories,
|
|
@@ -148,8 +156,10 @@ function printPlan(input) {
|
|
|
148
156
|
.map((item) => item.candidate.metadata.timestamp?.getTime())
|
|
149
157
|
.filter((value) => value !== undefined);
|
|
150
158
|
process.stdout.write([
|
|
151
|
-
`\nGroundtrack · import
|
|
159
|
+
`\nGroundtrack · import ${input.displayName} sessions`,
|
|
152
160
|
`Scope: ${input.scope}`,
|
|
161
|
+
`Transcript visibility: ${input.visibility}`,
|
|
162
|
+
"Learning: high-confidence lessons may enter your organization's shared knowledge",
|
|
153
163
|
`Sessions: ${input.sessions.length} (${input.sessions.filter((item) => item.alreadyImported).length} in local receipts)`,
|
|
154
164
|
...(timestamps.length
|
|
155
165
|
? [
|
|
@@ -194,142 +204,162 @@ export async function runImportCli(argv) {
|
|
|
194
204
|
return;
|
|
195
205
|
}
|
|
196
206
|
const context = environment({ repoDir: args.repoDir, homeDir: args.homeDir });
|
|
197
|
-
const
|
|
207
|
+
const format = sessionImportFormat(args.agent);
|
|
208
|
+
const home = resolve(context.env[format.homeEnvironmentVariable] ||
|
|
209
|
+
resolve(context.homeDir, format.defaultHomeDirectory));
|
|
198
210
|
const repository = args.allRepositories ? undefined : repositoryIdentity(context.repoDir);
|
|
199
|
-
const discovery =
|
|
200
|
-
|
|
211
|
+
const discovery = discoverSessions({
|
|
212
|
+
home,
|
|
213
|
+
format,
|
|
201
214
|
repository,
|
|
202
215
|
allRepositories: args.allRepositories,
|
|
203
216
|
includeUnscoped: args.includeUnscoped,
|
|
204
217
|
includeSubagents: args.includeSubagents,
|
|
205
218
|
since: args.since,
|
|
219
|
+
userHome: context.homeDir,
|
|
220
|
+
env: context.env,
|
|
221
|
+
platform: context.platform,
|
|
222
|
+
workingDirectory: context.repoDir,
|
|
206
223
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
try {
|
|
225
|
+
const apiKey = args.apiKey ?? context.env.GROUNDTRACK_API_KEY;
|
|
226
|
+
const receipts = apiKey ? readReceipts(context) : { version: 1, receipts: {} };
|
|
227
|
+
const prepared = [];
|
|
228
|
+
const preparationErrors = [...discovery.failed];
|
|
229
|
+
for (const candidate of discovery.included) {
|
|
230
|
+
try {
|
|
231
|
+
const pruned = await pruneSession(candidate.path, format, {
|
|
232
|
+
emitBytes: false,
|
|
233
|
+
metadata: candidate.metadata,
|
|
234
|
+
});
|
|
235
|
+
if (pruned.prunedBytes > CANONICAL_IMPORT_MAX_BYTES)
|
|
236
|
+
throw new Error(`Pruned session exceeds ${formatBytes(CANONICAL_IMPORT_MAX_BYTES)}`);
|
|
237
|
+
const key = apiKey
|
|
238
|
+
? receiptKey({
|
|
239
|
+
apiBaseUrl: args.apiBaseUrl,
|
|
240
|
+
apiKey,
|
|
241
|
+
source: format.source,
|
|
242
|
+
sourceContentHash: pruned.sourceContentHash,
|
|
243
|
+
visibility: args.visibility,
|
|
244
|
+
})
|
|
245
|
+
: null;
|
|
246
|
+
prepared.push({
|
|
247
|
+
...pruned,
|
|
248
|
+
candidate,
|
|
249
|
+
receiptKey: key,
|
|
250
|
+
alreadyImported: Boolean(key && receipts.receipts[key]),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
preparationErrors.push({
|
|
255
|
+
path: candidate.path,
|
|
256
|
+
error: error instanceof Error ? error.message : String(error),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
232
259
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
const scope = args.allRepositories ? "all repositories" : repository.root;
|
|
261
|
+
const plan = {
|
|
262
|
+
agent: args.agent,
|
|
263
|
+
scope,
|
|
264
|
+
selected: prepared.length,
|
|
265
|
+
alreadyImported: prepared.filter((item) => item.alreadyImported).length,
|
|
266
|
+
originalBytes: prepared.reduce((sum, item) => sum + item.originalBytes, 0),
|
|
267
|
+
prunedBytes: prepared.reduce((sum, item) => sum + item.prunedBytes, 0),
|
|
268
|
+
repositories: sessionRepositoryCounts(prepared),
|
|
269
|
+
dateRange: sessionDateRange(prepared),
|
|
270
|
+
skipped: skippedCounts(discovery.skipped),
|
|
271
|
+
failed: preparationErrors,
|
|
272
|
+
};
|
|
273
|
+
if (args.dryRun) {
|
|
274
|
+
if (args.json)
|
|
275
|
+
process.stdout.write(`${JSON.stringify({ dryRun: true, plan }, null, 2)}\n`);
|
|
276
|
+
else
|
|
277
|
+
printPlan({
|
|
278
|
+
displayName: format.displayName,
|
|
279
|
+
scope,
|
|
280
|
+
visibility: args.visibility,
|
|
281
|
+
sessions: prepared,
|
|
282
|
+
skipped: discovery.skipped,
|
|
283
|
+
failed: preparationErrors.length,
|
|
284
|
+
});
|
|
285
|
+
if (preparationErrors.length)
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
238
288
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const plan = {
|
|
242
|
-
scope,
|
|
243
|
-
selected: prepared.length,
|
|
244
|
-
alreadyImported: prepared.filter((item) => item.alreadyImported).length,
|
|
245
|
-
originalBytes: prepared.reduce((sum, item) => sum + item.originalBytes, 0),
|
|
246
|
-
prunedBytes: prepared.reduce((sum, item) => sum + item.prunedBytes, 0),
|
|
247
|
-
repositories: sessionRepositoryCounts(prepared),
|
|
248
|
-
dateRange: sessionDateRange(prepared),
|
|
249
|
-
skipped: skippedCounts(discovery.skipped),
|
|
250
|
-
failed: preparationErrors,
|
|
251
|
-
};
|
|
252
|
-
if (args.dryRun) {
|
|
253
|
-
if (args.json)
|
|
254
|
-
process.stdout.write(`${JSON.stringify({ dryRun: true, plan }, null, 2)}\n`);
|
|
255
|
-
else
|
|
289
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !args.json);
|
|
290
|
+
if (!args.json)
|
|
256
291
|
printPlan({
|
|
292
|
+
displayName: format.displayName,
|
|
257
293
|
scope,
|
|
294
|
+
visibility: args.visibility,
|
|
258
295
|
sessions: prepared,
|
|
259
296
|
skipped: discovery.skipped,
|
|
260
297
|
failed: preparationErrors.length,
|
|
261
298
|
});
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
sessions: prepared,
|
|
271
|
-
skipped: discovery.skipped,
|
|
272
|
-
failed: preparationErrors.length,
|
|
273
|
-
});
|
|
274
|
-
if (!apiKey)
|
|
275
|
-
throw new Error("Set GROUNDTRACK_API_KEY or pass --api-key before uploading.");
|
|
276
|
-
if (!args.yes && !interactive)
|
|
277
|
-
throw new Error("Import needs confirmation. Use --yes, or run it in an interactive terminal.");
|
|
278
|
-
if (!args.yes) {
|
|
279
|
-
const approved = await prompts.confirm({
|
|
280
|
-
message: `Upload ${prepared.filter((item) => !item.alreadyImported).length} Codex sessions?`,
|
|
281
|
-
initialValue: false,
|
|
282
|
-
});
|
|
283
|
-
if (prompts.isCancel(approved) || !approved) {
|
|
284
|
-
prompts.cancel("Nothing uploaded.");
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
let imported = 0;
|
|
289
|
-
let duplicate = 0;
|
|
290
|
-
let receiptSkipped = 0;
|
|
291
|
-
const failures = [...preparationErrors];
|
|
292
|
-
for (const item of prepared) {
|
|
293
|
-
if (item.alreadyImported) {
|
|
294
|
-
receiptSkipped += 1;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const session = await pruneCodexSession(item.candidate.path);
|
|
299
|
-
if (session.sourceContentHash !== item.sourceContentHash ||
|
|
300
|
-
session.prunedBytes !== item.prunedBytes)
|
|
301
|
-
throw new Error("Session changed after the import plan was created.");
|
|
302
|
-
const result = await uploadCodexSession({
|
|
303
|
-
apiBaseUrl: args.apiBaseUrl,
|
|
304
|
-
apiKey,
|
|
305
|
-
session,
|
|
306
|
-
visibility: args.visibility,
|
|
299
|
+
if (!apiKey)
|
|
300
|
+
throw new Error("Set GROUNDTRACK_API_KEY or pass --api-key before uploading.");
|
|
301
|
+
if (!args.yes && !interactive)
|
|
302
|
+
throw new Error("Import needs confirmation. Use --yes, or run it in an interactive terminal.");
|
|
303
|
+
if (!args.yes) {
|
|
304
|
+
const approved = await prompts.confirm({
|
|
305
|
+
message: `Upload ${prepared.filter((item) => !item.alreadyImported).length} ${format.displayName} sessions?`,
|
|
306
|
+
initialValue: false,
|
|
307
307
|
});
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
saveReceipt(context, receipts, item.receiptKey, result.sessionId);
|
|
308
|
+
if (prompts.isCancel(approved) || !approved) {
|
|
309
|
+
prompts.cancel("Nothing uploaded.");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
313
312
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
313
|
+
let imported = 0;
|
|
314
|
+
let duplicate = 0;
|
|
315
|
+
let receiptSkipped = 0;
|
|
316
|
+
const failures = [...preparationErrors];
|
|
317
|
+
for (const item of prepared) {
|
|
318
|
+
if (item.alreadyImported) {
|
|
319
|
+
receiptSkipped += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const session = await pruneSession(item.candidate.path, format, {
|
|
324
|
+
plan: item,
|
|
325
|
+
metadata: item.candidate.metadata,
|
|
326
|
+
});
|
|
327
|
+
const result = await uploadAgentSession({
|
|
328
|
+
apiBaseUrl: args.apiBaseUrl,
|
|
329
|
+
apiKey,
|
|
330
|
+
source: format.source,
|
|
331
|
+
session,
|
|
332
|
+
visibility: args.visibility,
|
|
333
|
+
});
|
|
334
|
+
if (result.duplicate)
|
|
335
|
+
duplicate += 1;
|
|
336
|
+
else
|
|
337
|
+
imported += 1;
|
|
338
|
+
saveReceipt(context, receipts, item.receiptKey, result.sessionId);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
failures.push({
|
|
342
|
+
path: item.candidate.path,
|
|
343
|
+
error: error instanceof Error ? error.message : String(error),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
319
346
|
}
|
|
347
|
+
const result = {
|
|
348
|
+
imported,
|
|
349
|
+
duplicate,
|
|
350
|
+
skipped: receiptSkipped + discovery.skipped.length,
|
|
351
|
+
failed: failures,
|
|
352
|
+
sessionsUrl: args.apiBaseUrl.replace(/\/api$/, "/sessions"),
|
|
353
|
+
plan,
|
|
354
|
+
};
|
|
355
|
+
if (args.json)
|
|
356
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
357
|
+
else
|
|
358
|
+
process.stdout.write(`Imported ${imported}; duplicates ${duplicate}; skipped ${result.skipped}; failed ${failures.length}.\n${result.sessionsUrl}\n`);
|
|
359
|
+
if (failures.length)
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
discovery.cleanup?.();
|
|
320
364
|
}
|
|
321
|
-
const result = {
|
|
322
|
-
imported,
|
|
323
|
-
duplicate,
|
|
324
|
-
skipped: receiptSkipped + discovery.skipped.length,
|
|
325
|
-
failed: failures,
|
|
326
|
-
sessionsUrl: args.apiBaseUrl.replace(/\/api$/, "/sessions"),
|
|
327
|
-
plan,
|
|
328
|
-
};
|
|
329
|
-
if (args.json)
|
|
330
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
331
|
-
else
|
|
332
|
-
process.stdout.write(`Imported ${imported}; duplicates ${duplicate}; skipped ${result.skipped}; failed ${failures.length}.\n${result.sessionsUrl}\n`);
|
|
333
|
-
if (failures.length)
|
|
334
|
-
process.exitCode = 1;
|
|
335
365
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { ImportVisibility,
|
|
1
|
+
import type { AgentSessionSource, ImportVisibility, PrunedAgentSession } from "./types.js";
|
|
2
2
|
type ImportResponse = Readonly<{
|
|
3
3
|
duplicate: boolean;
|
|
4
4
|
sessionId: string;
|
|
5
5
|
}>;
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function uploadAgentSession(input: {
|
|
7
7
|
apiBaseUrl: string;
|
|
8
8
|
apiKey: string;
|
|
9
|
-
|
|
9
|
+
source: AgentSessionSource;
|
|
10
|
+
session: PrunedAgentSession;
|
|
10
11
|
visibility: ImportVisibility;
|
|
11
12
|
fetch?: typeof globalThis.fetch;
|
|
12
13
|
}): Promise<ImportResponse>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CANONICALIZATION_VERSION } from "./canonical.js";
|
|
2
2
|
function retryable(status) {
|
|
3
3
|
return status === 408 || status === 429 || status >= 500;
|
|
4
4
|
}
|
|
@@ -22,7 +22,7 @@ async function responseError(response) {
|
|
|
22
22
|
}
|
|
23
23
|
return response.statusText || `HTTP ${response.status}`;
|
|
24
24
|
}
|
|
25
|
-
export async function
|
|
25
|
+
export async function uploadAgentSession(input) {
|
|
26
26
|
const request = input.fetch ?? globalThis.fetch;
|
|
27
27
|
let lastError = null;
|
|
28
28
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -32,8 +32,9 @@ export async function uploadCodexSession(input) {
|
|
|
32
32
|
headers: {
|
|
33
33
|
"content-type": "application/vnd.groundtrack.session+jsonl; version=1",
|
|
34
34
|
"x-api-key": input.apiKey,
|
|
35
|
-
"x-groundtrack-canonicalization": String(
|
|
35
|
+
"x-groundtrack-canonicalization": String(CANONICALIZATION_VERSION),
|
|
36
36
|
"x-groundtrack-content-hash": input.session.sourceContentHash,
|
|
37
|
+
"x-groundtrack-session-source": input.source,
|
|
37
38
|
"x-groundtrack-visibility": input.visibility,
|
|
38
39
|
},
|
|
39
40
|
body: input.session.bytes,
|
|
@@ -1,37 +1,10 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare const CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES: number;
|
|
5
|
-
export type CanonicalSessionEntry = Readonly<{
|
|
6
|
-
kind: "user_message" | "assistant_message" | "tool_call" | "tool_result";
|
|
7
|
-
occurredAt: string | null;
|
|
8
|
-
payload: Record<string, unknown>;
|
|
9
|
-
}>;
|
|
10
|
-
export type CanonicalCodexMetadata = Readonly<{
|
|
11
|
-
type: "session";
|
|
12
|
-
version: 1;
|
|
13
|
-
externalSessionId: string;
|
|
14
|
-
repositoryUrl: string | null;
|
|
15
|
-
repositoryBranch: string | null;
|
|
16
|
-
repositoryCommitSha: string | null;
|
|
17
|
-
workingDirectory: string | null;
|
|
18
|
-
originator: string | null;
|
|
19
|
-
model: string | null;
|
|
20
|
-
startedAt: string | null;
|
|
21
|
-
endedAt: string | null;
|
|
22
|
-
statistics: {
|
|
23
|
-
totalRecordCount: number;
|
|
24
|
-
importedEntryCount: number;
|
|
25
|
-
skippedRecordCount: number;
|
|
26
|
-
unsupportedRecordCount: number;
|
|
27
|
-
malformedRecordCount: number;
|
|
28
|
-
sensitiveRecordCount: number;
|
|
29
|
-
};
|
|
30
|
-
}>;
|
|
31
|
-
export declare function isSessionRecord(value: unknown): value is UnknownRecord;
|
|
32
|
-
export declare function sessionString(value: unknown): string | null;
|
|
1
|
+
import { type CanonicalSessionEntry, type CanonicalSessionMetadata, type SessionRecord } from "./canonical.js";
|
|
2
|
+
import { type SessionImportFormat, type SessionNormalizer } from "./format.js";
|
|
3
|
+
type UnknownRecord = SessionRecord;
|
|
33
4
|
export declare function isSensitiveCodexRecord(record: UnknownRecord): boolean;
|
|
34
5
|
export declare function isKnownSkippedCodexRecord(record: UnknownRecord): boolean;
|
|
35
6
|
export declare function normalizeCodexResponseItem(record: UnknownRecord): CanonicalSessionEntry | null;
|
|
36
|
-
export declare function codexSessionMetadata(record: UnknownRecord): Omit<
|
|
7
|
+
export declare function codexSessionMetadata(record: UnknownRecord): Omit<CanonicalSessionMetadata, "model" | "startedAt" | "endedAt" | "statistics"> | null;
|
|
8
|
+
export declare function createCodexSessionNormalizer(): SessionNormalizer;
|
|
9
|
+
export declare const codexSessionFormat: SessionImportFormat;
|
|
37
10
|
export {};
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { containsInternalInstructionText, redactCredentialText, sanitizeRepositoryUrl, sanitizeSessionValue, sanitizeWorkingDirectory, } from "./redaction.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
5
|
-
export function isSessionRecord(value) {
|
|
6
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
export function sessionString(value) {
|
|
9
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
10
|
-
}
|
|
2
|
+
import { isSessionRecord, sessionString, } from "./canonical.js";
|
|
3
|
+
import { sessionImportFormatIdentity, } from "./format.js";
|
|
11
4
|
function messageText(content) {
|
|
12
5
|
if (!Array.isArray(content))
|
|
13
6
|
return null;
|
|
@@ -132,3 +125,63 @@ export function codexSessionMetadata(record) {
|
|
|
132
125
|
originator: originator ? redactCredentialText(originator) : null,
|
|
133
126
|
};
|
|
134
127
|
}
|
|
128
|
+
export function createCodexSessionNormalizer() {
|
|
129
|
+
let metadata = null;
|
|
130
|
+
let model = null;
|
|
131
|
+
return {
|
|
132
|
+
normalizeRecord: (record) => {
|
|
133
|
+
metadata ??= codexSessionMetadata(record);
|
|
134
|
+
if (record.type === "turn_context" && isSessionRecord(record.payload))
|
|
135
|
+
model ??= sessionString(record.payload.model);
|
|
136
|
+
if (isSensitiveCodexRecord(record))
|
|
137
|
+
return { entries: [], sensitive: true, unsupported: false };
|
|
138
|
+
const entry = normalizeCodexResponseItem(record);
|
|
139
|
+
if (entry)
|
|
140
|
+
return { entries: [entry], sensitive: false, unsupported: false };
|
|
141
|
+
return {
|
|
142
|
+
entries: [],
|
|
143
|
+
sensitive: false,
|
|
144
|
+
unsupported: !isKnownSkippedCodexRecord(record),
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
canonicalMetadata: () => (metadata ? { ...metadata, model } : null),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function isSubagentMetadata(payload) {
|
|
151
|
+
if (sessionString(payload.parent_thread_id) || sessionString(payload.agent_path))
|
|
152
|
+
return true;
|
|
153
|
+
if (isSessionRecord(payload.source) && "subagent" in payload.source)
|
|
154
|
+
return true;
|
|
155
|
+
return ["subagent", "agent_forked_thread", "guardian_review"].includes(sessionString(payload.thread_source) ?? "");
|
|
156
|
+
}
|
|
157
|
+
function discoveryMetadata(record) {
|
|
158
|
+
if (record.type !== "session_meta" || !isSessionRecord(record.payload))
|
|
159
|
+
return null;
|
|
160
|
+
const payload = record.payload;
|
|
161
|
+
const git = isSessionRecord(payload.git) ? payload.git : {};
|
|
162
|
+
const externalSessionId = sessionString(payload.session_id) ?? sessionString(payload.id);
|
|
163
|
+
if (!externalSessionId)
|
|
164
|
+
return null;
|
|
165
|
+
const timestampValue = sessionString(payload.timestamp) ?? sessionString(record.timestamp);
|
|
166
|
+
const timestamp = timestampValue ? new Date(timestampValue) : null;
|
|
167
|
+
return {
|
|
168
|
+
externalSessionId,
|
|
169
|
+
timestamp: timestamp && !Number.isNaN(timestamp.getTime()) ? timestamp : null,
|
|
170
|
+
cwd: sessionString(payload.cwd),
|
|
171
|
+
repositoryUrl: sessionString(git.repository_url),
|
|
172
|
+
repositoryBranch: sessionString(git.branch),
|
|
173
|
+
repositoryCommitSha: sessionString(git.commit_hash),
|
|
174
|
+
originator: sessionString(payload.originator),
|
|
175
|
+
isSubagent: isSubagentMetadata(payload),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export const codexSessionFormat = {
|
|
179
|
+
...sessionImportFormatIdentity("codex"),
|
|
180
|
+
discovery: "files",
|
|
181
|
+
homeEnvironmentVariable: "CODEX_HOME",
|
|
182
|
+
defaultHomeDirectory: ".codex",
|
|
183
|
+
discoveryRecordLimit: 1,
|
|
184
|
+
sessionRoots: (home) => [`${home}/sessions`, `${home}/archived_sessions`],
|
|
185
|
+
parseDiscoveryMetadata: (records) => (records[0] ? discoveryMetadata(records[0]) : null),
|
|
186
|
+
createNormalizer: createCodexSessionNormalizer,
|
|
187
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SessionDiscoveryCandidates, type SessionDiscoveryContext, type SessionImportFormat, type SessionNormalizer } from "./format.js";
|
|
2
|
+
import type { AgentSessionMetadata } from "./types.js";
|
|
3
|
+
type CursorHeader = Readonly<{
|
|
4
|
+
composerId: unknown;
|
|
5
|
+
createdAt: unknown;
|
|
6
|
+
lastUpdatedAt: unknown;
|
|
7
|
+
isSubagent: unknown;
|
|
8
|
+
workingDirectory: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function cursorCandidates(context: SessionDiscoveryContext, headers?: readonly CursorHeader[]): SessionDiscoveryCandidates;
|
|
11
|
+
export declare function createCursorSessionNormalizer(metadata?: AgentSessionMetadata): SessionNormalizer;
|
|
12
|
+
export declare const cursorSessionFormat: SessionImportFormat;
|
|
13
|
+
export {};
|