@groundtrack/hooks 0.1.8 → 0.1.10

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 (42) hide show
  1. package/README.md +42 -4
  2. package/dist/agents.d.ts +55 -0
  3. package/dist/agents.js +77 -3
  4. package/dist/groundtrack.js +5 -2
  5. package/dist/session-import/canonical.d.ts +33 -0
  6. package/dist/session-import/canonical.js +10 -0
  7. package/dist/session-import/claude.d.ts +20 -0
  8. package/dist/session-import/claude.js +237 -0
  9. package/dist/session-import/cli.d.ts +22 -0
  10. package/dist/session-import/cli.js +365 -0
  11. package/dist/session-import/client.d.ts +14 -0
  12. package/dist/session-import/client.js +74 -0
  13. package/dist/session-import/codex.d.ts +10 -0
  14. package/dist/session-import/codex.js +187 -0
  15. package/dist/session-import/cursor.d.ts +13 -0
  16. package/dist/session-import/cursor.js +268 -0
  17. package/dist/session-import/discovery.d.ts +31 -0
  18. package/dist/session-import/discovery.js +241 -0
  19. package/dist/session-import/format.d.ts +51 -0
  20. package/dist/session-import/format.js +11 -0
  21. package/dist/session-import/formats.d.ts +4 -0
  22. package/dist/session-import/formats.js +13 -0
  23. package/dist/session-import/normalize.d.ts +21 -0
  24. package/dist/session-import/normalize.js +96 -0
  25. package/dist/session-import/opencode-cli.d.ts +14 -0
  26. package/dist/session-import/opencode-cli.js +178 -0
  27. package/dist/session-import/opencode-normalizer.d.ts +8 -0
  28. package/dist/session-import/opencode-normalizer.js +230 -0
  29. package/dist/session-import/opencode.d.ts +8 -0
  30. package/dist/session-import/opencode.js +230 -0
  31. package/dist/session-import/prune.d.ts +14 -0
  32. package/dist/session-import/prune.js +86 -0
  33. package/dist/session-import/receipts.d.ts +21 -0
  34. package/dist/session-import/receipts.js +44 -0
  35. package/dist/session-import/redaction.d.ts +6 -0
  36. package/dist/session-import/redaction.js +61 -0
  37. package/dist/session-import/types.d.ts +42 -0
  38. package/dist/session-import/types.js +1 -0
  39. package/dist/setup/agents.d.ts +32 -0
  40. package/dist/setup/cli.js +1 -0
  41. package/package.json +43 -1
  42. package/src/agents.ts +104 -3
@@ -0,0 +1,365 @@
1
+ import { resolve } from "node:path";
2
+ import * as prompts from "@clack/prompts";
3
+ import { DEFAULT_SESSION_IMPORT_AGENT, isSessionImportAgent, SESSION_IMPORT_AGENTS, } from "../agents.js";
4
+ import { environment } from "../setup/agents.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";
10
+ import { readReceipts, receiptKey, saveReceipt } from "./receipts.js";
11
+ const importAgentIds = SESSION_IMPORT_AGENTS.map((agent) => agent.id);
12
+ const importAgentChoices = importAgentIds.join("|");
13
+ const importAgentDescription = importAgentIds.join(", ");
14
+ export const IMPORT_USAGE = `Usage:
15
+ groundtrack import [--agent ${importAgentChoices}] [--repo PATH | --all-repos] [--since DATE]
16
+ [--include-subagents] [--include-unscoped]
17
+ [--visibility private|organization] [--dry-run] [--yes] [--json]
18
+
19
+ Import options:
20
+ --agent VALUE ${importAgentDescription} (default: ${DEFAULT_SESSION_IMPORT_AGENT})
21
+ --repo PATH Import sessions for this Git repository (default: current repository)
22
+ --all-repos Import sessions associated with any Git repository
23
+ --include-unscoped With --all-repos, also import sessions outside a Git repository
24
+ --since DATE Import sessions at or after an ISO timestamp or local YYYY-MM-DD
25
+ --include-subagents Include subagent, forked-agent, and guardian sessions
26
+ --visibility VALUE private (default) or organization
27
+ --api-key KEY API key; prefer the GROUNDTRACK_API_KEY environment variable
28
+ --url URL Groundtrack app or API URL (default: https://app.groundtrack.dev)
29
+ --dry-run Discover and prune without authentication or network calls
30
+ --yes, -y Upload without another confirmation
31
+ --json Emit exactly one machine-readable result object
32
+ `;
33
+ function parseDate(value) {
34
+ const date = /^\d{4}-\d{2}-\d{2}$/.test(value) ? new Date(`${value}T00:00:00`) : new Date(value);
35
+ if (Number.isNaN(date.getTime()))
36
+ throw new Error(`Invalid --since date: ${value}`);
37
+ return date;
38
+ }
39
+ export function normalizeApiBaseUrl(value) {
40
+ const url = new URL(value);
41
+ if ((url.protocol !== "https:" &&
42
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) ||
43
+ url.username ||
44
+ url.password ||
45
+ url.search ||
46
+ url.hash)
47
+ throw new Error("Use an HTTPS Groundtrack URL (or local HTTP), without credentials or query parameters.");
48
+ url.pathname = url.pathname.replace(/\/(?:api(?:\/mcp)?)\/?$/, "");
49
+ return `${url.href.replace(/\/$/, "")}/api`;
50
+ }
51
+ export function parseImportArguments(argv) {
52
+ const args = [...argv];
53
+ const valueAfter = (index, flag) => {
54
+ const value = args[index + 1];
55
+ if (!value || value.startsWith("-"))
56
+ throw new Error(`${flag} requires a value.`);
57
+ return value;
58
+ };
59
+ let repoDir;
60
+ let homeDir;
61
+ let agent = DEFAULT_SESSION_IMPORT_AGENT;
62
+ let allRepositories = false;
63
+ let includeUnscoped = false;
64
+ let includeSubagents = false;
65
+ let visibility = "private";
66
+ let since = null;
67
+ let dryRun = false;
68
+ let yes = false;
69
+ let json = false;
70
+ let apiKey;
71
+ let apiBaseUrl = normalizeApiBaseUrl("https://app.groundtrack.dev");
72
+ let help = false;
73
+ for (let index = 0; index < args.length; index += 1) {
74
+ const arg = args[index];
75
+ if (arg === "--agent") {
76
+ const value = valueAfter(index++, arg);
77
+ if (!isSessionImportAgent(value))
78
+ throw new Error(`Unsupported session import agent: ${value}.`);
79
+ agent = value;
80
+ }
81
+ else if (arg === "--repo")
82
+ repoDir = valueAfter(index++, arg);
83
+ else if (arg === "--home")
84
+ homeDir = valueAfter(index++, arg);
85
+ else if (arg === "--all-repos")
86
+ allRepositories = true;
87
+ else if (arg === "--include-unscoped")
88
+ includeUnscoped = true;
89
+ else if (arg === "--include-subagents")
90
+ includeSubagents = true;
91
+ else if (arg === "--visibility") {
92
+ const value = valueAfter(index++, arg);
93
+ if (value !== "private" && value !== "organization")
94
+ throw new Error("--visibility must be private or organization.");
95
+ visibility = value;
96
+ }
97
+ else if (arg === "--since")
98
+ since = parseDate(valueAfter(index++, arg));
99
+ else if (arg === "--api-key")
100
+ apiKey = valueAfter(index++, arg);
101
+ else if (arg === "--url")
102
+ apiBaseUrl = normalizeApiBaseUrl(valueAfter(index++, arg));
103
+ else if (arg === "--dry-run")
104
+ dryRun = true;
105
+ else if (arg === "--yes" || arg === "-y")
106
+ yes = true;
107
+ else if (arg === "--json")
108
+ json = true;
109
+ else if (arg === "--help" || arg === "-h")
110
+ help = true;
111
+ else
112
+ throw new Error(`Unknown import option: ${arg}. Run groundtrack import --help.`);
113
+ }
114
+ if (allRepositories && repoDir)
115
+ throw new Error("Choose --repo or --all-repos, not both.");
116
+ if (includeUnscoped && !allRepositories)
117
+ throw new Error("Use --include-unscoped together with --all-repos.");
118
+ return {
119
+ agent,
120
+ repoDir,
121
+ homeDir,
122
+ allRepositories,
123
+ includeUnscoped,
124
+ includeSubagents,
125
+ visibility,
126
+ since,
127
+ dryRun,
128
+ yes,
129
+ json,
130
+ apiKey,
131
+ apiBaseUrl,
132
+ help,
133
+ };
134
+ }
135
+ function formatBytes(bytes) {
136
+ if (bytes < 1024)
137
+ return `${bytes} B`;
138
+ if (bytes < 1024 ** 2)
139
+ return `${(bytes / 1024).toFixed(1)} KB`;
140
+ if (bytes < 1024 ** 3)
141
+ return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
142
+ return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
143
+ }
144
+ function skippedCounts(skipped) {
145
+ const result = {};
146
+ for (const item of skipped)
147
+ result[item.reason] = (result[item.reason] ?? 0) + 1;
148
+ return result;
149
+ }
150
+ function printPlan(input) {
151
+ const originalBytes = input.sessions.reduce((sum, session) => sum + session.originalBytes, 0);
152
+ const prunedBytes = input.sessions.reduce((sum, session) => sum + session.prunedBytes, 0);
153
+ const skippedByReason = skippedCounts(input.skipped);
154
+ const repositories = sessionRepositoryCounts(input.sessions);
155
+ const timestamps = input.sessions
156
+ .map((item) => item.candidate.metadata.timestamp?.getTime())
157
+ .filter((value) => value !== undefined);
158
+ process.stdout.write([
159
+ `\nGroundtrack · import ${input.displayName} sessions`,
160
+ `Scope: ${input.scope}`,
161
+ `Transcript visibility: ${input.visibility}`,
162
+ "Learning: high-confidence lessons may enter your organization's shared knowledge",
163
+ `Sessions: ${input.sessions.length} (${input.sessions.filter((item) => item.alreadyImported).length} in local receipts)`,
164
+ ...(timestamps.length
165
+ ? [
166
+ `Date range: ${new Date(Math.min(...timestamps)).toISOString()} to ${new Date(Math.max(...timestamps)).toISOString()}`,
167
+ ]
168
+ : []),
169
+ ...Object.entries(repositories).map(([repository, count]) => ` ${repository}: ${count}`),
170
+ `Upload size: ${formatBytes(prunedBytes)} after pruning (${formatBytes(originalBytes)} raw)`,
171
+ `Skipped: ${input.skipped.length}${Object.entries(skippedByReason)
172
+ .map(([reason, count]) => ` · ${reason} ${count}`)
173
+ .join("")}`,
174
+ ...(input.failed ? [`Unreadable/oversized: ${input.failed}`] : []),
175
+ "",
176
+ ].join("\n"));
177
+ }
178
+ function sessionRepositoryCounts(sessions) {
179
+ const result = {};
180
+ for (const session of sessions) {
181
+ const repository = session.candidate.metadata.repositoryUrl
182
+ ? normalizeRepositoryUrl(session.candidate.metadata.repositoryUrl)
183
+ : null;
184
+ const label = repository ?? session.candidate.metadata.cwd ?? "(no repository)";
185
+ result[label] = (result[label] ?? 0) + 1;
186
+ }
187
+ return result;
188
+ }
189
+ function sessionDateRange(sessions) {
190
+ const timestamps = sessions
191
+ .map((item) => item.candidate.metadata.timestamp?.getTime())
192
+ .filter((value) => value !== undefined);
193
+ return timestamps.length
194
+ ? {
195
+ from: new Date(Math.min(...timestamps)).toISOString(),
196
+ to: new Date(Math.max(...timestamps)).toISOString(),
197
+ }
198
+ : null;
199
+ }
200
+ export async function runImportCli(argv) {
201
+ const args = parseImportArguments(argv);
202
+ if (args.help) {
203
+ process.stdout.write(IMPORT_USAGE);
204
+ return;
205
+ }
206
+ const context = environment({ repoDir: args.repoDir, homeDir: args.homeDir });
207
+ const format = sessionImportFormat(args.agent);
208
+ const home = resolve(context.env[format.homeEnvironmentVariable] ||
209
+ resolve(context.homeDir, format.defaultHomeDirectory));
210
+ const repository = args.allRepositories ? undefined : repositoryIdentity(context.repoDir);
211
+ const discovery = discoverSessions({
212
+ home,
213
+ format,
214
+ repository,
215
+ allRepositories: args.allRepositories,
216
+ includeUnscoped: args.includeUnscoped,
217
+ includeSubagents: args.includeSubagents,
218
+ since: args.since,
219
+ userHome: context.homeDir,
220
+ env: context.env,
221
+ platform: context.platform,
222
+ workingDirectory: context.repoDir,
223
+ });
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
+ }
259
+ }
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;
288
+ }
289
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !args.json);
290
+ if (!args.json)
291
+ printPlan({
292
+ displayName: format.displayName,
293
+ scope,
294
+ visibility: args.visibility,
295
+ sessions: prepared,
296
+ skipped: discovery.skipped,
297
+ failed: preparationErrors.length,
298
+ });
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
+ });
308
+ if (prompts.isCancel(approved) || !approved) {
309
+ prompts.cancel("Nothing uploaded.");
310
+ return;
311
+ }
312
+ }
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
+ }
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?.();
364
+ }
365
+ }
@@ -0,0 +1,14 @@
1
+ import type { AgentSessionSource, ImportVisibility, PrunedAgentSession } from "./types.js";
2
+ type ImportResponse = Readonly<{
3
+ duplicate: boolean;
4
+ sessionId: string;
5
+ }>;
6
+ export declare function uploadAgentSession(input: {
7
+ apiBaseUrl: string;
8
+ apiKey: string;
9
+ source: AgentSessionSource;
10
+ session: PrunedAgentSession;
11
+ visibility: ImportVisibility;
12
+ fetch?: typeof globalThis.fetch;
13
+ }): Promise<ImportResponse>;
14
+ export {};
@@ -0,0 +1,74 @@
1
+ import { CANONICALIZATION_VERSION } from "./canonical.js";
2
+ function retryable(status) {
3
+ return status === 408 || status === 429 || status >= 500;
4
+ }
5
+ function retryDelay(response, attempt) {
6
+ const retryAfter = response.headers.get("retry-after");
7
+ if (retryAfter && /^\d+$/.test(retryAfter))
8
+ return Math.min(Number(retryAfter) * 1000, 10_000);
9
+ return 250 * 2 ** attempt;
10
+ }
11
+ function wait(milliseconds) {
12
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
13
+ }
14
+ async function responseError(response) {
15
+ try {
16
+ const body = await response.json();
17
+ if (body && typeof body === "object" && "error" in body && typeof body.error === "string")
18
+ return body.error;
19
+ }
20
+ catch {
21
+ // Fall through to the status text.
22
+ }
23
+ return response.statusText || `HTTP ${response.status}`;
24
+ }
25
+ export async function uploadAgentSession(input) {
26
+ const request = input.fetch ?? globalThis.fetch;
27
+ let lastError = null;
28
+ for (let attempt = 0; attempt < 3; attempt += 1) {
29
+ try {
30
+ const response = await request(`${input.apiBaseUrl}/sessions/imports`, {
31
+ method: "POST",
32
+ headers: {
33
+ "content-type": "application/vnd.groundtrack.session+jsonl; version=1",
34
+ "x-api-key": input.apiKey,
35
+ "x-groundtrack-canonicalization": String(CANONICALIZATION_VERSION),
36
+ "x-groundtrack-content-hash": input.session.sourceContentHash,
37
+ "x-groundtrack-session-source": input.source,
38
+ "x-groundtrack-visibility": input.visibility,
39
+ },
40
+ body: input.session.bytes,
41
+ signal: AbortSignal.timeout(300_000),
42
+ });
43
+ if (!response.ok) {
44
+ const error = new Error(`Import failed (${response.status}): ${await responseError(response)}`);
45
+ if (!retryable(response.status) || attempt === 2)
46
+ throw error;
47
+ await wait(retryDelay(response, attempt));
48
+ lastError = error;
49
+ continue;
50
+ }
51
+ const body = await response.json();
52
+ if (!body ||
53
+ typeof body !== "object" ||
54
+ !("duplicate" in body) ||
55
+ typeof body.duplicate !== "boolean" ||
56
+ !("session" in body) ||
57
+ !body.session ||
58
+ typeof body.session !== "object" ||
59
+ !("id" in body.session) ||
60
+ typeof body.session.id !== "string")
61
+ throw new Error("Import endpoint returned an invalid response.");
62
+ return { duplicate: body.duplicate, sessionId: body.session.id };
63
+ }
64
+ catch (error) {
65
+ lastError = error instanceof Error ? error : new Error(String(error));
66
+ if (attempt === 2 ||
67
+ (lastError.message.startsWith("Import failed") &&
68
+ !/\((408|429|5\d\d)\)/.test(lastError.message)))
69
+ throw lastError;
70
+ await wait(250 * 2 ** attempt);
71
+ }
72
+ }
73
+ throw lastError ?? new Error("Import failed.");
74
+ }
@@ -0,0 +1,10 @@
1
+ import { type CanonicalSessionEntry, type CanonicalSessionMetadata, type SessionRecord } from "./canonical.js";
2
+ import { type SessionImportFormat, type SessionNormalizer } from "./format.js";
3
+ type UnknownRecord = SessionRecord;
4
+ export declare function isSensitiveCodexRecord(record: UnknownRecord): boolean;
5
+ export declare function isKnownSkippedCodexRecord(record: UnknownRecord): boolean;
6
+ export declare function normalizeCodexResponseItem(record: UnknownRecord): CanonicalSessionEntry | 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;
10
+ export {};
@@ -0,0 +1,187 @@
1
+ import { containsInternalInstructionText, redactCredentialText, sanitizeRepositoryUrl, sanitizeSessionValue, sanitizeWorkingDirectory, } from "./redaction.js";
2
+ import { isSessionRecord, sessionString, } from "./canonical.js";
3
+ import { sessionImportFormatIdentity, } from "./format.js";
4
+ function messageText(content) {
5
+ if (!Array.isArray(content))
6
+ return null;
7
+ const text = content
8
+ .filter(isSessionRecord)
9
+ .filter((part) => part.type === "input_text" || part.type === "output_text")
10
+ .map((part) => sessionString(part.text))
11
+ .filter((part) => part !== null)
12
+ .join("\n");
13
+ return text ? sanitizeSessionValue(text) : null;
14
+ }
15
+ function messageContainsInternalInstructions(payload) {
16
+ if (!Array.isArray(payload.content))
17
+ return false;
18
+ return payload.content
19
+ .filter(isSessionRecord)
20
+ .map((part) => sessionString(part.text) ?? "")
21
+ .some(containsInternalInstructionText);
22
+ }
23
+ export function isSensitiveCodexRecord(record) {
24
+ const type = sessionString(record.type);
25
+ const payload = isSessionRecord(record.payload) ? record.payload : {};
26
+ const payloadType = sessionString(payload.type);
27
+ const role = sessionString(payload.role);
28
+ return (type === "turn_context" ||
29
+ type === "world_state" ||
30
+ type === "compacted" ||
31
+ type === "inter_agent_communication_metadata" ||
32
+ payloadType === "reasoning" ||
33
+ payloadType === "agent_reasoning" ||
34
+ payloadType === "agent_message" ||
35
+ role === "system" ||
36
+ role === "developer" ||
37
+ (type === "response_item" &&
38
+ payloadType === "message" &&
39
+ messageContainsInternalInstructions(payload)));
40
+ }
41
+ export function isKnownSkippedCodexRecord(record) {
42
+ const type = sessionString(record.type);
43
+ const payload = isSessionRecord(record.payload) ? record.payload : {};
44
+ return (type === "session_meta" ||
45
+ type === "token_usage_record" ||
46
+ type === "event_msg" ||
47
+ sessionString(payload.type) === "reasoning");
48
+ }
49
+ function structuredValue(value) {
50
+ if (typeof value !== "string")
51
+ return sanitizeSessionValue(value);
52
+ try {
53
+ return sanitizeSessionValue(JSON.parse(value));
54
+ }
55
+ catch {
56
+ return sanitizeSessionValue(value);
57
+ }
58
+ }
59
+ export function normalizeCodexResponseItem(record) {
60
+ if (record.type !== "response_item" || !isSessionRecord(record.payload))
61
+ return null;
62
+ const payload = record.payload;
63
+ const payloadType = sessionString(payload.type);
64
+ const role = sessionString(payload.role);
65
+ const occurredAt = sessionString(record.timestamp);
66
+ if (payloadType === "message") {
67
+ if ((role !== "user" && role !== "assistant") || messageContainsInternalInstructions(payload))
68
+ return null;
69
+ const text = messageText(payload.content);
70
+ if (!text)
71
+ return null;
72
+ const phase = role === "assistant" ? sessionString(payload.phase) : null;
73
+ return {
74
+ kind: role === "user" ? "user_message" : "assistant_message",
75
+ occurredAt,
76
+ payload: {
77
+ text,
78
+ ...(phase ? { phase } : {}),
79
+ },
80
+ };
81
+ }
82
+ if (payloadType === "custom_tool_call" || payloadType === "function_call") {
83
+ const toolName = sessionString(payload.name);
84
+ if (!toolName)
85
+ return null;
86
+ return {
87
+ kind: "tool_call",
88
+ occurredAt,
89
+ payload: {
90
+ callId: sessionString(payload.call_id),
91
+ toolName,
92
+ input: structuredValue(payload.input ?? payload.arguments),
93
+ },
94
+ };
95
+ }
96
+ if (payloadType === "custom_tool_call_output" || payloadType === "function_call_output") {
97
+ return {
98
+ kind: "tool_result",
99
+ occurredAt,
100
+ payload: {
101
+ callId: sessionString(payload.call_id),
102
+ output: structuredValue(payload.output),
103
+ },
104
+ };
105
+ }
106
+ return null;
107
+ }
108
+ export function codexSessionMetadata(record) {
109
+ if (record.type !== "session_meta" || !isSessionRecord(record.payload))
110
+ return null;
111
+ const payload = record.payload;
112
+ const git = isSessionRecord(payload.git) ? payload.git : {};
113
+ const externalSessionId = sessionString(payload.session_id) ?? sessionString(payload.id);
114
+ if (!externalSessionId)
115
+ return null;
116
+ const originator = sessionString(payload.originator);
117
+ return {
118
+ type: "session",
119
+ version: 1,
120
+ externalSessionId,
121
+ repositoryUrl: sanitizeRepositoryUrl(git.repository_url),
122
+ repositoryBranch: sessionString(git.branch),
123
+ repositoryCommitSha: sessionString(git.commit_hash),
124
+ workingDirectory: sanitizeWorkingDirectory(payload.cwd),
125
+ originator: originator ? redactCredentialText(originator) : null,
126
+ };
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 {};