@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.
Files changed (40) hide show
  1. package/README.md +28 -10
  2. package/dist/agents.d.ts +55 -0
  3. package/dist/agents.js +77 -3
  4. package/dist/groundtrack-hook.js +0 -0
  5. package/dist/groundtrack.js +0 -0
  6. package/dist/session-import/canonical.d.ts +33 -0
  7. package/dist/session-import/canonical.js +10 -0
  8. package/dist/session-import/claude.d.ts +20 -0
  9. package/dist/session-import/claude.js +237 -0
  10. package/dist/session-import/cli.d.ts +3 -2
  11. package/dist/session-import/cli.js +161 -131
  12. package/dist/session-import/client.d.ts +4 -3
  13. package/dist/session-import/client.js +4 -3
  14. package/dist/session-import/codex.d.ts +6 -33
  15. package/dist/session-import/codex.js +62 -9
  16. package/dist/session-import/cursor.d.ts +13 -0
  17. package/dist/session-import/cursor.js +268 -0
  18. package/dist/session-import/discovery.d.ts +16 -7
  19. package/dist/session-import/discovery.js +127 -70
  20. package/dist/session-import/format.d.ts +51 -0
  21. package/dist/session-import/format.js +11 -0
  22. package/dist/session-import/formats.d.ts +4 -0
  23. package/dist/session-import/formats.js +13 -0
  24. package/dist/session-import/normalize.d.ts +21 -0
  25. package/dist/session-import/normalize.js +100 -0
  26. package/dist/session-import/opencode-cli.d.ts +14 -0
  27. package/dist/session-import/opencode-cli.js +178 -0
  28. package/dist/session-import/opencode-normalizer.d.ts +8 -0
  29. package/dist/session-import/opencode-normalizer.js +230 -0
  30. package/dist/session-import/opencode.d.ts +8 -0
  31. package/dist/session-import/opencode.js +230 -0
  32. package/dist/session-import/prune.d.ts +13 -4
  33. package/dist/session-import/prune.js +45 -79
  34. package/dist/session-import/receipts.d.ts +2 -1
  35. package/dist/session-import/receipts.js +3 -3
  36. package/dist/session-import/redaction.js +5 -3
  37. package/dist/session-import/types.d.ts +21 -7
  38. package/dist/setup/agents.d.ts +32 -0
  39. package/package.json +29 -1
  40. 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 { uploadCodexSession } from "./client.js";
5
- import { discoverCodexSessions, normalizeRepositoryUrl, repositoryIdentity } from "./discovery.js";
6
- import { pruneCodexSession } from "./prune.js";
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
- import { CODEX_CANONICAL_IMPORT_MAX_BYTES } from "./codex.js";
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 codex] [--repo PATH | --all-repos] [--since DATE]
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 codex Session format to discover (default: codex)
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 carrying repository metadata
18
- --include-unscoped With --all-repos, also import sessions without repository metadata
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 agent = valueAfter(index++, arg);
71
- if (agent !== "codex")
72
- throw new Error("Session import currently supports --agent codex.");
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 Codex sessions`,
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 codexHome = resolve(context.env.CODEX_HOME || resolve(context.homeDir, ".codex"));
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 = discoverCodexSessions({
200
- codexHome,
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
- const apiKey = args.apiKey ?? context.env.GROUNDTRACK_API_KEY;
208
- const receipts = apiKey ? readReceipts(context) : { version: 1, receipts: {} };
209
- const prepared = [];
210
- const preparationErrors = [];
211
- for (const candidate of discovery.included) {
212
- try {
213
- const pruned = await pruneCodexSession(candidate.path, { emitBytes: false });
214
- if (pruned.prunedBytes > CODEX_CANONICAL_IMPORT_MAX_BYTES)
215
- throw new Error(`Pruned session exceeds ${formatBytes(CODEX_CANONICAL_IMPORT_MAX_BYTES)}`);
216
- const key = apiKey
217
- ? receiptKey({
218
- apiBaseUrl: args.apiBaseUrl,
219
- apiKey,
220
- sourceContentHash: pruned.sourceContentHash,
221
- visibility: args.visibility,
222
- })
223
- : null;
224
- prepared.push({
225
- candidate,
226
- sourceContentHash: pruned.sourceContentHash,
227
- prunedBytes: pruned.prunedBytes,
228
- originalBytes: pruned.originalBytes,
229
- receiptKey: key,
230
- alreadyImported: Boolean(key && receipts.receipts[key]),
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
- catch (error) {
234
- preparationErrors.push({
235
- path: candidate.path,
236
- error: error instanceof Error ? error.message : String(error),
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
- const scope = args.allRepositories ? "all repositories" : repository.root;
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 (preparationErrors.length)
263
- process.exitCode = 1;
264
- return;
265
- }
266
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !args.json);
267
- if (!args.json)
268
- printPlan({
269
- scope,
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 (result.duplicate)
309
- duplicate += 1;
310
- else
311
- imported += 1;
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
- catch (error) {
315
- failures.push({
316
- path: item.candidate.path,
317
- error: error instanceof Error ? error.message : String(error),
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, PrunedCodexSession } from "./types.js";
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 uploadCodexSession(input: {
6
+ export declare function uploadAgentSession(input: {
7
7
  apiBaseUrl: string;
8
8
  apiKey: string;
9
- session: PrunedCodexSession;
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 { CODEX_CANONICALIZATION_VERSION } from "./codex.js";
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 uploadCodexSession(input) {
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(CODEX_CANONICALIZATION_VERSION),
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 UnknownRecord = Record<string, unknown>;
2
- export declare const CODEX_CANONICALIZATION_VERSION = 1;
3
- export declare const CODEX_CANONICAL_IMPORT_MAX_BYTES: number;
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<CanonicalCodexMetadata, "model" | "startedAt" | "endedAt" | "statistics"> | null;
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
- export const CODEX_CANONICALIZATION_VERSION = 1;
3
- export const CODEX_CANONICAL_IMPORT_MAX_BYTES = 50 * 1024 * 1024;
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 {};