@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
@@ -0,0 +1,230 @@
1
+ import { isSessionRecord, sessionString, } from "./canonical.js";
2
+ import { containsInternalInstructionText, redactCredentialText, sanitizeSessionValue, sanitizeWorkingDirectory, } from "./redaction.js";
3
+ function unixMillisecondsDate(value) {
4
+ if (!Number.isSafeInteger(value) || value < 0)
5
+ return null;
6
+ const date = new Date(value);
7
+ return Number.isNaN(date.getTime()) ? null : date;
8
+ }
9
+ function messageTimestamp(message) {
10
+ const info = isSessionRecord(message.info) ? message.info : {};
11
+ const time = isSessionRecord(info.time) ? info.time : {};
12
+ return (unixMillisecondsDate(time.completed)?.toISOString() ??
13
+ unixMillisecondsDate(time.created)?.toISOString() ??
14
+ null);
15
+ }
16
+ export function parseOpenCodeSessionExport(value) {
17
+ if (!isSessionRecord(value) || !isSessionRecord(value.info) || !Array.isArray(value.messages))
18
+ return null;
19
+ const externalSessionId = sessionString(value.info.id);
20
+ if (!externalSessionId)
21
+ return null;
22
+ const time = isSessionRecord(value.info.time) ? value.info.time : {};
23
+ const startedAt = unixMillisecondsDate(time.created);
24
+ const endedAt = unixMillisecondsDate(time.updated);
25
+ return {
26
+ metadata: {
27
+ externalSessionId,
28
+ timestamp: endedAt ?? startedAt,
29
+ cwd: sessionString(value.info.directory),
30
+ repositoryUrl: null,
31
+ repositoryBranch: null,
32
+ repositoryCommitSha: null,
33
+ originator: "opencode",
34
+ isSubagent: sessionString(value.info.parentID) !== null,
35
+ startedAt,
36
+ endedAt,
37
+ },
38
+ records: [
39
+ {
40
+ type: "opencode_session",
41
+ timestamp: startedAt?.toISOString() ?? null,
42
+ info: {
43
+ id: value.info.id,
44
+ directory: value.info.directory,
45
+ model: value.info.model,
46
+ version: value.info.version,
47
+ },
48
+ },
49
+ ...value.messages.map((message) => isSessionRecord(message)
50
+ ? {
51
+ type: "opencode_message",
52
+ timestamp: messageTimestamp(message),
53
+ info: message.info,
54
+ parts: message.parts,
55
+ }
56
+ : message),
57
+ ],
58
+ };
59
+ }
60
+ function timestampIso(value) {
61
+ if (!Number.isSafeInteger(value) || value < 0)
62
+ return null;
63
+ const date = new Date(value);
64
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
65
+ }
66
+ function modelName(value) {
67
+ if (typeof value === "string")
68
+ return value || null;
69
+ if (!isSessionRecord(value))
70
+ return null;
71
+ const provider = sessionString(value.providerID);
72
+ const model = sessionString(value.modelID) ?? sessionString(value.id);
73
+ return provider && model ? `${provider}/${model}` : model;
74
+ }
75
+ function containsInternalInstructions(value) {
76
+ if (typeof value === "string")
77
+ return containsInternalInstructionText(value);
78
+ if (Array.isArray(value))
79
+ return value.some(containsInternalInstructions);
80
+ if (!isSessionRecord(value))
81
+ return false;
82
+ return Object.values(value).some(containsInternalInstructions);
83
+ }
84
+ function redactHomePaths(value) {
85
+ if (typeof value === "string")
86
+ return value
87
+ .replace(/\/Users\/[^/\s"'`]+/g, "~")
88
+ .replace(/\/home\/[^/\s"'`]+/g, "~")
89
+ .replace(/[A-Za-z]:\\Users\\[^\\\s"'`]+/gi, "~");
90
+ if (Array.isArray(value))
91
+ return value.map(redactHomePaths);
92
+ if (!isSessionRecord(value))
93
+ return value;
94
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, redactHomePaths(nested)]));
95
+ }
96
+ function sanitizeOpenCodeValue(value) {
97
+ return redactHomePaths(sanitizeSessionValue(value));
98
+ }
99
+ function safeValue(value) {
100
+ return containsInternalInstructions(value)
101
+ ? "[REDACTED INTERNAL INSTRUCTIONS]"
102
+ : sanitizeOpenCodeValue(value);
103
+ }
104
+ function partTimestamp(part, key, fallback) {
105
+ const state = isSessionRecord(part.state) ? part.state : {};
106
+ const time = isSessionRecord(state.time) ? state.time : {};
107
+ return timestampIso(time[key]) ?? fallback;
108
+ }
109
+ function normalizeOpenCodeMessage(record, entryBudget) {
110
+ const info = isSessionRecord(record.info) ? record.info : null;
111
+ const role = info ? sessionString(info.role) : null;
112
+ if (!info || (role !== "user" && role !== "assistant") || !Array.isArray(record.parts))
113
+ return { entries: [], sensitive: false, unsupported: true };
114
+ const occurredAt = sessionString(record.timestamp);
115
+ const entries = [];
116
+ let sensitive = false;
117
+ let unsupported = false;
118
+ for (const part of record.parts) {
119
+ if (entries.length > entryBudget)
120
+ break;
121
+ if (!isSessionRecord(part)) {
122
+ unsupported = true;
123
+ continue;
124
+ }
125
+ const type = sessionString(part.type);
126
+ if (type === "reasoning" || type === "file" || type === "patch" || type === "snapshot") {
127
+ sensitive = true;
128
+ continue;
129
+ }
130
+ if (type === "text") {
131
+ const text = sessionString(part.text);
132
+ if (!text || part.ignored === true)
133
+ continue;
134
+ if (part.synthetic === true || containsInternalInstructionText(text))
135
+ sensitive = true;
136
+ else
137
+ entries.push({
138
+ kind: role === "user" ? "user_message" : "assistant_message",
139
+ occurredAt,
140
+ payload: { text: sanitizeOpenCodeValue(text) },
141
+ });
142
+ continue;
143
+ }
144
+ if (type === "tool" && role === "assistant") {
145
+ const toolName = sessionString(part.tool);
146
+ const state = isSessionRecord(part.state) ? part.state : null;
147
+ if (!toolName || !state) {
148
+ unsupported = true;
149
+ continue;
150
+ }
151
+ const inputContainsInternalInstructions = containsInternalInstructions(state.input);
152
+ sensitive ||= inputContainsInternalInstructions;
153
+ entries.push({
154
+ kind: "tool_call",
155
+ occurredAt: partTimestamp(part, "start", occurredAt),
156
+ payload: {
157
+ callId: sessionString(part.callID),
158
+ toolName: redactCredentialText(toolName),
159
+ input: inputContainsInternalInstructions
160
+ ? "[REDACTED INTERNAL INSTRUCTIONS]"
161
+ : sanitizeOpenCodeValue(state.input),
162
+ },
163
+ });
164
+ const status = sessionString(state.status);
165
+ if (status === "completed" || status === "error") {
166
+ const output = status === "completed" ? state.output : state.error;
167
+ const outputContainsInternalInstructions = containsInternalInstructions(output);
168
+ sensitive ||= outputContainsInternalInstructions;
169
+ entries.push({
170
+ kind: "tool_result",
171
+ occurredAt: partTimestamp(part, "end", occurredAt),
172
+ payload: {
173
+ callId: sessionString(part.callID),
174
+ output: safeValue(output),
175
+ ...(status === "error" ? { isError: true } : {}),
176
+ },
177
+ });
178
+ }
179
+ continue;
180
+ }
181
+ if (type === "step-start" ||
182
+ type === "step-finish" ||
183
+ type === "retry" ||
184
+ type === "compaction")
185
+ continue;
186
+ if (type === "subtask" || type === "agent")
187
+ sensitive = true;
188
+ else
189
+ unsupported = true;
190
+ }
191
+ const overflowed = entries.length > entryBudget;
192
+ return {
193
+ entries: overflowed ? entries.slice(0, entryBudget) : entries,
194
+ sensitive,
195
+ unsupported,
196
+ overflowed,
197
+ };
198
+ }
199
+ export function createOpenCodeSessionNormalizer(metadata) {
200
+ let model = null;
201
+ return {
202
+ normalizeRecord: (record, entryBudget = Number.POSITIVE_INFINITY) => {
203
+ if (record.type === "opencode_session") {
204
+ const info = isSessionRecord(record.info) ? record.info : {};
205
+ model ??= modelName(info.model);
206
+ return { entries: [], sensitive: false, unsupported: false };
207
+ }
208
+ if (record.type !== "opencode_message")
209
+ return { entries: [], sensitive: false, unsupported: true };
210
+ const info = isSessionRecord(record.info) ? record.info : {};
211
+ const provider = sessionString(info.providerID);
212
+ const modelId = sessionString(info.modelID);
213
+ model ??= provider && modelId ? `${provider}/${modelId}` : modelId;
214
+ return normalizeOpenCodeMessage(record, entryBudget);
215
+ },
216
+ canonicalMetadata: () => metadata
217
+ ? {
218
+ type: "session",
219
+ version: 1,
220
+ externalSessionId: metadata.externalSessionId,
221
+ repositoryUrl: metadata.repositoryUrl,
222
+ repositoryBranch: metadata.repositoryBranch,
223
+ repositoryCommitSha: metadata.repositoryCommitSha,
224
+ workingDirectory: sanitizeWorkingDirectory(metadata.cwd),
225
+ originator: "opencode",
226
+ model: model ? redactCredentialText(model) : null,
227
+ }
228
+ : null,
229
+ };
230
+ }
@@ -1,5 +1,14 @@
1
- import type { CodexSessionAnalysis, PrunedCodexSession } from "./types.js";
2
- export declare function pruneCodexSession(path: string): Promise<PrunedCodexSession>;
3
- export declare function pruneCodexSession(path: string, options: Readonly<{
1
+ import type { SessionImportFormat } from "./format.js";
2
+ import type { AgentSessionMetadata, AgentSessionPlan, PrunedAgentSession } from "./types.js";
3
+ export declare function pruneSession(path: string, format: SessionImportFormat): Promise<PrunedAgentSession>;
4
+ export declare function pruneSession(path: string, format: SessionImportFormat, options: Readonly<{
5
+ metadata: AgentSessionMetadata;
6
+ }>): Promise<PrunedAgentSession>;
7
+ export declare function pruneSession(path: string, format: SessionImportFormat, options: Readonly<{
4
8
  emitBytes: false;
5
- }>): Promise<CodexSessionAnalysis>;
9
+ metadata?: AgentSessionMetadata;
10
+ }>): Promise<AgentSessionPlan>;
11
+ export declare function pruneSession(path: string, format: SessionImportFormat, options: Readonly<{
12
+ plan: AgentSessionPlan;
13
+ metadata?: AgentSessionMetadata;
14
+ }>): Promise<PrunedAgentSession>;
@@ -3,28 +3,25 @@ import { createReadStream } from "node:fs";
3
3
  import { stat } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline";
5
5
  import { Transform } from "node:stream";
6
- import { CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES, codexSessionMetadata, isSessionRecord, isKnownSkippedCodexRecord, isSensitiveCodexRecord, normalizeCodexResponseItem, sessionString, } from "./codex.js";
7
- function recordTimestamp(record) {
8
- const value = sessionString(record.timestamp);
9
- if (!value)
10
- return null;
11
- const date = new Date(value);
12
- return Number.isNaN(date.getTime()) ? null : date;
13
- }
14
- export async function pruneCodexSession(path, options = {}) {
6
+ import { CANONICAL_IMPORT_MAX_RECORD_BYTES } from "./canonical.js";
7
+ import { createSessionNormalizationAccumulator } from "./normalize.js";
8
+ async function scanSession(path, format, discoveryMetadata, onEntry) {
15
9
  const before = await stat(path);
16
10
  const hash = createHash("sha256");
11
+ if (discoveryMetadata && format.sourceHashSeed)
12
+ hash.update(`${format.sourceHashSeed(discoveryMetadata)}\0`);
17
13
  const encoder = new TextEncoder();
18
- const entryChunks = [];
19
14
  let entryBytes = 0;
20
- let entryCount = 0;
21
- const timestamps = [];
22
- let metadata = null;
23
- let model = null;
24
- let originalRecords = 0;
25
- let malformedRecordCount = 0;
26
- let sensitiveRecordCount = 0;
27
- let unsupportedRecordCount = 0;
15
+ const accumulator = createSessionNormalizationAccumulator(format.createNormalizer(discoveryMetadata), {
16
+ discoveryMetadata,
17
+ onEntry: (entry) => {
18
+ const bytes = encoder.encode(`${JSON.stringify(entry)}\n`);
19
+ if (bytes.byteLength > CANONICAL_IMPORT_MAX_RECORD_BYTES)
20
+ throw new Error(`Canonical session record is too large: ${path}`);
21
+ onEntry?.(bytes);
22
+ entryBytes += bytes.byteLength;
23
+ },
24
+ });
28
25
  const hashingStream = new Transform({
29
26
  transform(chunk, _encoding, callback) {
30
27
  hash.update(chunk);
@@ -37,84 +34,53 @@ export async function pruneCodexSession(path, options = {}) {
37
34
  for await (const line of lines) {
38
35
  if (!line.trim())
39
36
  continue;
40
- originalRecords += 1;
41
37
  let record;
42
38
  try {
43
39
  record = JSON.parse(line);
44
40
  }
45
41
  catch {
46
- malformedRecordCount += 1;
47
- continue;
48
- }
49
- if (!isSessionRecord(record)) {
50
- malformedRecordCount += 1;
51
- continue;
52
- }
53
- metadata ??= codexSessionMetadata(record);
54
- const timestamp = recordTimestamp(record);
55
- if (timestamp)
56
- timestamps.push(timestamp);
57
- if (record.type === "turn_context" && isSessionRecord(record.payload))
58
- model ??= sessionString(record.payload.model);
59
- const sensitive = isSensitiveCodexRecord(record);
60
- if (sensitive)
61
- sensitiveRecordCount += 1;
62
- const entry = normalizeCodexResponseItem(record);
63
- if (entry) {
64
- const chunk = encoder.encode(`${JSON.stringify(entry)}\n`);
65
- if (chunk.byteLength > CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES)
66
- throw new Error(`Canonical session record is too large: ${path}`);
67
- if (options.emitBytes !== false)
68
- entryChunks.push(chunk);
69
- entryBytes += chunk.byteLength;
70
- entryCount += 1;
42
+ record = null;
71
43
  }
72
- else if (!isKnownSkippedCodexRecord(record) && !sensitive)
73
- unsupportedRecordCount += 1;
44
+ accumulator.add(record);
74
45
  }
75
46
  const after = await stat(path);
76
47
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
77
48
  throw new Error(`Session changed while it was being read: ${path}`);
78
- if (!metadata)
79
- throw new Error(`Session metadata is missing: ${path}`);
80
- if (entryCount === 0)
81
- throw new Error(`Session has no supported trace entries: ${path}`);
82
- const startedAt = timestamps.length
83
- ? new Date(Math.min(...timestamps.map((timestamp) => timestamp.getTime()))).toISOString()
84
- : null;
85
- const endedAt = timestamps.length
86
- ? new Date(Math.max(...timestamps.map((timestamp) => timestamp.getTime()))).toISOString()
87
- : null;
88
- const header = {
89
- ...metadata,
90
- model,
91
- startedAt,
92
- endedAt,
93
- statistics: {
94
- totalRecordCount: originalRecords,
95
- importedEntryCount: entryCount,
96
- skippedRecordCount: originalRecords - entryCount,
97
- unsupportedRecordCount,
98
- malformedRecordCount,
99
- sensitiveRecordCount,
100
- },
101
- };
49
+ const header = accumulator.finish().metadata;
102
50
  const headerBytes = encoder.encode(`${JSON.stringify(header)}\n`);
103
- if (headerBytes.byteLength > CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES)
51
+ if (headerBytes.byteLength > CANONICAL_IMPORT_MAX_RECORD_BYTES)
104
52
  throw new Error(`Canonical session metadata is too large: ${path}`);
105
- const analysis = {
53
+ return {
106
54
  sourceContentHash: hash.digest("hex"),
107
55
  originalBytes: before.size,
108
56
  prunedBytes: headerBytes.byteLength + entryBytes,
57
+ metadata: header,
109
58
  };
59
+ }
60
+ export async function pruneSession(path, format, options = {}) {
110
61
  if (options.emitBytes === false)
111
- return analysis;
112
- const bytes = new Uint8Array(analysis.prunedBytes);
62
+ return scanSession(path, format, options.metadata);
63
+ const plan = options.plan ?? (await scanSession(path, format, options.metadata));
64
+ const encoder = new TextEncoder();
65
+ const headerBytes = encoder.encode(`${JSON.stringify(plan.metadata)}\n`);
66
+ const bytes = new Uint8Array(plan.prunedBytes);
113
67
  bytes.set(headerBytes);
114
68
  let offset = headerBytes.byteLength;
115
- for (const chunk of entryChunks) {
116
- bytes.set(chunk, offset);
117
- offset += chunk.byteLength;
118
- }
119
- return { ...analysis, bytes };
69
+ const rendered = await scanSession(path, format, options.metadata, (entry) => {
70
+ if (offset + entry.byteLength > bytes.byteLength)
71
+ throw new Error(`Session changed after it was analyzed: ${path}`);
72
+ bytes.set(entry, offset);
73
+ offset += entry.byteLength;
74
+ });
75
+ if (offset !== bytes.byteLength ||
76
+ rendered.sourceContentHash !== plan.sourceContentHash ||
77
+ rendered.prunedBytes !== plan.prunedBytes ||
78
+ JSON.stringify(rendered.metadata) !== JSON.stringify(plan.metadata))
79
+ throw new Error(`Session changed after it was analyzed: ${path}`);
80
+ return {
81
+ sourceContentHash: plan.sourceContentHash,
82
+ originalBytes: plan.originalBytes,
83
+ prunedBytes: plan.prunedBytes,
84
+ bytes,
85
+ };
120
86
  }
@@ -1,5 +1,5 @@
1
1
  import type { SetupEnvironment } from "../setup/agents.js";
2
- import type { ImportVisibility } from "./types.js";
2
+ import type { AgentSessionSource, ImportVisibility } from "./types.js";
3
3
  type Receipt = Readonly<{
4
4
  sessionId: string;
5
5
  importedAt: string;
@@ -13,6 +13,7 @@ export declare function readReceipts(context: SetupEnvironment): ReceiptFile;
13
13
  export declare function receiptKey(input: {
14
14
  apiBaseUrl: string;
15
15
  apiKey: string;
16
+ source: AgentSessionSource;
16
17
  sourceContentHash: string;
17
18
  visibility: ImportVisibility;
18
19
  }): string;
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { readJson } from "../setup/config.js";
4
4
  import { readFile, writeAtomic } from "../setup/files.js";
5
5
  import { stateDir } from "../setup/agents.js";
6
- import { CODEX_CANONICALIZATION_VERSION } from "./codex.js";
6
+ import { CANONICALIZATION_VERSION } from "./canonical.js";
7
7
  export function receiptPath(context) {
8
8
  return join(stateDir(context), "session-import-receipts.json");
9
9
  }
@@ -31,8 +31,8 @@ export function receiptKey(input) {
31
31
  .update([
32
32
  input.apiBaseUrl,
33
33
  createHash("sha256").update(input.apiKey).digest("hex"),
34
- "codex_cli",
35
- CODEX_CANONICALIZATION_VERSION,
34
+ input.source,
35
+ CANONICALIZATION_VERSION,
36
36
  input.visibility,
37
37
  input.sourceContentHash,
38
38
  ].join("\0"))
@@ -1,4 +1,4 @@
1
- const sensitiveKeyPattern = /(?:^|[_-])(?:api[_-]?key|token|authorization|auth|password|passwd|secret|private[_-]?key|cookie|credentials?)(?:$|[_-])/i;
1
+ const sensitiveKeyPattern = /(?:^|[_-])(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|token|authorization|auth|password|passwd|secret|private[_-]?key|cookie|credentials?|database[_-]?url)(?:$|[_-])/i;
2
2
  const internalInstructionMarkers = [
3
3
  "<local-command-caveat>",
4
4
  "<system-reminder>",
@@ -19,9 +19,11 @@ export function redactCredentialText(value) {
19
19
  return value
20
20
  .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]")
21
21
  .replace(/\b(authorization\s*:\s*(?:bearer|basic)\s+)[^\s,;]+/gi, "$1[REDACTED]")
22
- .replace(/\b((?:[a-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|private[_-]?key|credentials?))\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]")
22
+ .replace(/\b((?:[a-z0-9_-]*(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|token|password|passwd|secret|private[_-]?key|credentials?|database[_-]?url))\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]")
23
23
  .replace(/(?:^|\s)((?:--)?(?:api-key|token|password|secret)\s+)[^\s,;]+/gi, "$1$2[REDACTED]")
24
- .replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/g, "[REDACTED]");
24
+ .replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|glpat-[A-Za-z0-9_-]{12,}|npm_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{12,}|(?:AKIA|ASIA)[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{20,})\b/g, "[REDACTED]")
25
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED]")
26
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1[REDACTED]@");
25
27
  }
26
28
  export function sanitizeRepositoryUrl(value) {
27
29
  if (typeof value !== "string" || value.length === 0)
@@ -1,5 +1,9 @@
1
+ import type { AgentSessionSource, SessionImportAgent } from "../agents.js";
2
+ import type { CanonicalSessionMetadata } from "./canonical.js";
1
3
  export type ImportVisibility = "private" | "organization";
2
- export type CodexSessionMetadata = Readonly<{
4
+ export type ImportAgent = SessionImportAgent;
5
+ export type { AgentSessionSource };
6
+ export type AgentSessionMetadata = Readonly<{
3
7
  externalSessionId: string;
4
8
  timestamp: Date | null;
5
9
  cwd: string | null;
@@ -8,21 +12,31 @@ export type CodexSessionMetadata = Readonly<{
8
12
  repositoryCommitSha: string | null;
9
13
  originator: string | null;
10
14
  isSubagent: boolean;
15
+ startedAt?: Date | null;
16
+ endedAt?: Date | null;
11
17
  }>;
12
- export type CodexSessionCandidate = Readonly<{
18
+ export type AgentSessionCandidate = Readonly<{
13
19
  path: string;
14
- metadata: CodexSessionMetadata;
20
+ metadata: AgentSessionMetadata;
21
+ modifiedAt?: Date;
15
22
  }>;
16
- export type DiscoverySkipReason = "invalid_metadata" | "live" | "subagent" | "before_since" | "other_repository" | "unscoped";
17
- export type SkippedCodexSession = Readonly<{
23
+ export type DiscoverySkipReason = "invalid_metadata" | "live" | "subagent" | "before_since" | "other_repository" | "unscoped" | "empty_trace" | "missing_metadata" | "workspace_mismatch" | "conflicting_duplicates";
24
+ export type SkippedAgentSession = Readonly<{
18
25
  path: string;
19
26
  reason: DiscoverySkipReason;
20
27
  }>;
21
- export type CodexSessionAnalysis = Readonly<{
28
+ export type FailedAgentSession = Readonly<{
29
+ path: string;
30
+ error: string;
31
+ }>;
32
+ export type AgentSessionAnalysis = Readonly<{
22
33
  sourceContentHash: string;
23
34
  originalBytes: number;
24
35
  prunedBytes: number;
25
36
  }>;
26
- export type PrunedCodexSession = CodexSessionAnalysis & Readonly<{
37
+ export type AgentSessionPlan = AgentSessionAnalysis & Readonly<{
38
+ metadata: CanonicalSessionMetadata;
39
+ }>;
40
+ export type PrunedAgentSession = AgentSessionAnalysis & Readonly<{
27
41
  bytes: Uint8Array<ArrayBuffer>;
28
42
  }>;
@@ -25,6 +25,15 @@ export declare function discoverAgents(context: SetupEnvironment, url?: string):
25
25
  aliases: readonly ["claude-code"];
26
26
  source: "claude_code";
27
27
  enrichment: "command";
28
+ sessionImport: {
29
+ readonly cli: true;
30
+ readonly web: {
31
+ readonly extension: ".jsonl";
32
+ readonly accept: ".jsonl";
33
+ readonly fileLabel: "Claude Code session JSONL";
34
+ readonly location: "~/.claude/projects/<project>/*.jsonl";
35
+ };
36
+ };
28
37
  detected: boolean;
29
38
  configured: boolean;
30
39
  issue: string | null;
@@ -34,6 +43,15 @@ export declare function discoverAgents(context: SetupEnvironment, url?: string):
34
43
  aliases: readonly [];
35
44
  source: "codex";
36
45
  enrichment: "command";
46
+ sessionImport: {
47
+ readonly cli: true;
48
+ readonly web: {
49
+ readonly extension: ".jsonl";
50
+ readonly accept: ".jsonl";
51
+ readonly fileLabel: "Codex rollout JSONL";
52
+ readonly location: "~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl";
53
+ };
54
+ };
37
55
  detected: boolean;
38
56
  configured: boolean;
39
57
  issue: string | null;
@@ -43,6 +61,10 @@ export declare function discoverAgents(context: SetupEnvironment, url?: string):
43
61
  aliases: readonly [];
44
62
  source: "cursor";
45
63
  enrichment: "command";
64
+ sessionImport: {
65
+ readonly cli: true;
66
+ readonly web: null;
67
+ };
46
68
  detected: boolean;
47
69
  configured: boolean;
48
70
  issue: string | null;
@@ -52,6 +74,16 @@ export declare function discoverAgents(context: SetupEnvironment, url?: string):
52
74
  aliases: readonly [];
53
75
  source: "opencode";
54
76
  enrichment: "plugin";
77
+ sessionImport: {
78
+ readonly cli: true;
79
+ readonly web: {
80
+ readonly extension: ".json";
81
+ readonly accept: ".json,application/json";
82
+ readonly fileLabel: "OpenCode session export JSON";
83
+ readonly location: "opencode export <session-id> --pure > opencode-session.json";
84
+ readonly note: "Use direct file redirection as shown. Do not add --sanitize; Groundtrack removes sensitive records during import.";
85
+ };
86
+ };
55
87
  detected: boolean;
56
88
  configured: boolean;
57
89
  issue: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundtrack/hooks",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Lifecycle hook utilities for Groundtrack MCP clients.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -55,6 +55,34 @@
55
55
  "workerd": "./src/session-import/codex.ts",
56
56
  "development": "./src/session-import/codex.ts",
57
57
  "default": "./dist/session-import/codex.js"
58
+ },
59
+ "./session-import/canonical": {
60
+ "types": "./src/session-import/canonical.ts",
61
+ "browser": "./src/session-import/canonical.ts",
62
+ "workerd": "./src/session-import/canonical.ts",
63
+ "development": "./src/session-import/canonical.ts",
64
+ "default": "./dist/session-import/canonical.js"
65
+ },
66
+ "./session-import/claude": {
67
+ "types": "./src/session-import/claude.ts",
68
+ "browser": "./src/session-import/claude.ts",
69
+ "workerd": "./src/session-import/claude.ts",
70
+ "development": "./src/session-import/claude.ts",
71
+ "default": "./dist/session-import/claude.js"
72
+ },
73
+ "./session-import/opencode": {
74
+ "types": "./src/session-import/opencode.ts",
75
+ "browser": "./src/session-import/opencode.ts",
76
+ "workerd": "./src/session-import/opencode.ts",
77
+ "development": "./src/session-import/opencode.ts",
78
+ "default": "./dist/session-import/opencode.js"
79
+ },
80
+ "./session-import/normalize": {
81
+ "types": "./src/session-import/normalize.ts",
82
+ "browser": "./src/session-import/normalize.ts",
83
+ "workerd": "./src/session-import/normalize.ts",
84
+ "development": "./src/session-import/normalize.ts",
85
+ "default": "./dist/session-import/normalize.js"
58
86
  }
59
87
  },
60
88
  "publishConfig": {