@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,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
+ }
@@ -0,0 +1,14 @@
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<{
8
+ emitBytes: false;
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>;
@@ -0,0 +1,86 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { createInterface } from "node:readline";
5
+ import { Transform } from "node:stream";
6
+ import { CANONICAL_IMPORT_MAX_RECORD_BYTES } from "./canonical.js";
7
+ import { createSessionNormalizationAccumulator } from "./normalize.js";
8
+ async function scanSession(path, format, discoveryMetadata, onEntry) {
9
+ const before = await stat(path);
10
+ const hash = createHash("sha256");
11
+ if (discoveryMetadata && format.sourceHashSeed)
12
+ hash.update(`${format.sourceHashSeed(discoveryMetadata)}\0`);
13
+ const encoder = new TextEncoder();
14
+ let entryBytes = 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
+ });
25
+ const hashingStream = new Transform({
26
+ transform(chunk, _encoding, callback) {
27
+ hash.update(chunk);
28
+ callback(null, chunk);
29
+ },
30
+ });
31
+ const input = createReadStream(path);
32
+ input.pipe(hashingStream);
33
+ const lines = createInterface({ input: hashingStream, crlfDelay: Infinity });
34
+ for await (const line of lines) {
35
+ if (!line.trim())
36
+ continue;
37
+ let record;
38
+ try {
39
+ record = JSON.parse(line);
40
+ }
41
+ catch {
42
+ record = null;
43
+ }
44
+ accumulator.add(record);
45
+ }
46
+ const after = await stat(path);
47
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
48
+ throw new Error(`Session changed while it was being read: ${path}`);
49
+ const header = accumulator.finish().metadata;
50
+ const headerBytes = encoder.encode(`${JSON.stringify(header)}\n`);
51
+ if (headerBytes.byteLength > CANONICAL_IMPORT_MAX_RECORD_BYTES)
52
+ throw new Error(`Canonical session metadata is too large: ${path}`);
53
+ return {
54
+ sourceContentHash: hash.digest("hex"),
55
+ originalBytes: before.size,
56
+ prunedBytes: headerBytes.byteLength + entryBytes,
57
+ metadata: header,
58
+ };
59
+ }
60
+ export async function pruneSession(path, format, options = {}) {
61
+ if (options.emitBytes === false)
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);
67
+ bytes.set(headerBytes);
68
+ let offset = headerBytes.byteLength;
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
+ };
86
+ }
@@ -0,0 +1,21 @@
1
+ import type { SetupEnvironment } from "../setup/agents.js";
2
+ import type { AgentSessionSource, ImportVisibility } from "./types.js";
3
+ type Receipt = Readonly<{
4
+ sessionId: string;
5
+ importedAt: string;
6
+ }>;
7
+ type ReceiptFile = {
8
+ version: 1;
9
+ receipts: Record<string, Receipt>;
10
+ };
11
+ export declare function receiptPath(context: SetupEnvironment): string;
12
+ export declare function readReceipts(context: SetupEnvironment): ReceiptFile;
13
+ export declare function receiptKey(input: {
14
+ apiBaseUrl: string;
15
+ apiKey: string;
16
+ source: AgentSessionSource;
17
+ sourceContentHash: string;
18
+ visibility: ImportVisibility;
19
+ }): string;
20
+ export declare function saveReceipt(context: SetupEnvironment, file: ReceiptFile, key: string, sessionId: string): void;
21
+ export {};
@@ -0,0 +1,44 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { readJson } from "../setup/config.js";
4
+ import { readFile, writeAtomic } from "../setup/files.js";
5
+ import { stateDir } from "../setup/agents.js";
6
+ import { CANONICALIZATION_VERSION } from "./canonical.js";
7
+ export function receiptPath(context) {
8
+ return join(stateDir(context), "session-import-receipts.json");
9
+ }
10
+ export function readReceipts(context) {
11
+ const text = readFile(receiptPath(context));
12
+ if (text === null)
13
+ return { version: 1, receipts: {} };
14
+ const value = readJson(text);
15
+ if (value.version !== 1 || !value.receipts || typeof value.receipts !== "object")
16
+ throw new Error("Groundtrack's session import receipt file is invalid.");
17
+ const receipts = {};
18
+ for (const [key, receipt] of Object.entries(value.receipts)) {
19
+ if (receipt &&
20
+ typeof receipt === "object" &&
21
+ "sessionId" in receipt &&
22
+ typeof receipt.sessionId === "string" &&
23
+ "importedAt" in receipt &&
24
+ typeof receipt.importedAt === "string")
25
+ receipts[key] = { sessionId: receipt.sessionId, importedAt: receipt.importedAt };
26
+ }
27
+ return { version: 1, receipts };
28
+ }
29
+ export function receiptKey(input) {
30
+ return createHash("sha256")
31
+ .update([
32
+ input.apiBaseUrl,
33
+ createHash("sha256").update(input.apiKey).digest("hex"),
34
+ input.source,
35
+ CANONICALIZATION_VERSION,
36
+ input.visibility,
37
+ input.sourceContentHash,
38
+ ].join("\0"))
39
+ .digest("hex");
40
+ }
41
+ export function saveReceipt(context, file, key, sessionId) {
42
+ file.receipts[key] = { sessionId, importedAt: new Date().toISOString() };
43
+ writeAtomic(receiptPath(context), `${JSON.stringify(file, null, 2)}\n`, 0o600);
44
+ }
@@ -0,0 +1,6 @@
1
+ export declare function redactCredentialText(value: string): string;
2
+ export declare function sanitizeRepositoryUrl(value: unknown): string | null;
3
+ export declare function sanitizeWorkingDirectory(value: unknown): string | null;
4
+ export declare function sanitizeSessionValue(value: string): string;
5
+ export declare function sanitizeSessionValue(value: unknown): unknown;
6
+ export declare function containsInternalInstructionText(value: string): boolean;
@@ -0,0 +1,61 @@
1
+ const sensitiveKeyPattern = /(?:^|[_-])(?:api[_-]?key|token|authorization|auth|password|passwd|secret|private[_-]?key|cookie|credentials?)(?:$|[_-])/i;
2
+ const internalInstructionMarkers = [
3
+ "<local-command-caveat>",
4
+ "<system-reminder>",
5
+ "<environment_context>",
6
+ "<recommended_plugins>",
7
+ "# AGENTS.md instructions",
8
+ "<permissions instructions>",
9
+ "<skills_instructions>",
10
+ "<app-context>",
11
+ ];
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ function isSensitiveKey(value) {
16
+ return sensitiveKeyPattern.test(value.replace(/([a-z0-9])([A-Z])/g, "$1_$2"));
17
+ }
18
+ export function redactCredentialText(value) {
19
+ return value
20
+ .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]")
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]")
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]");
25
+ }
26
+ export function sanitizeRepositoryUrl(value) {
27
+ if (typeof value !== "string" || value.length === 0)
28
+ return null;
29
+ try {
30
+ const url = new URL(value);
31
+ url.username = "";
32
+ url.password = "";
33
+ return url.toString();
34
+ }
35
+ catch {
36
+ return redactCredentialText(value);
37
+ }
38
+ }
39
+ export function sanitizeWorkingDirectory(value) {
40
+ if (typeof value !== "string" || value.length === 0)
41
+ return null;
42
+ return redactCredentialText(value)
43
+ .replace(/^\/Users\/[^/]+(?=\/|$)/, "~")
44
+ .replace(/^\/home\/[^/]+(?=\/|$)/, "~")
45
+ .replace(/^[A-Za-z]:\\Users\\[^\\]+(?=\\|$)/i, "~");
46
+ }
47
+ export function sanitizeSessionValue(value) {
48
+ if (typeof value === "string")
49
+ return redactCredentialText(value);
50
+ if (Array.isArray(value))
51
+ return value.map(sanitizeSessionValue);
52
+ if (!isRecord(value))
53
+ return value;
54
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
55
+ key,
56
+ isSensitiveKey(key) ? "[REDACTED]" : sanitizeSessionValue(nested),
57
+ ]));
58
+ }
59
+ export function containsInternalInstructionText(value) {
60
+ return internalInstructionMarkers.some((marker) => value.includes(marker));
61
+ }
@@ -0,0 +1,42 @@
1
+ import type { AgentSessionSource, SessionImportAgent } from "../agents.js";
2
+ import type { CanonicalSessionMetadata } from "./canonical.js";
3
+ export type ImportVisibility = "private" | "organization";
4
+ export type ImportAgent = SessionImportAgent;
5
+ export type { AgentSessionSource };
6
+ export type AgentSessionMetadata = Readonly<{
7
+ externalSessionId: string;
8
+ timestamp: Date | null;
9
+ cwd: string | null;
10
+ repositoryUrl: string | null;
11
+ repositoryBranch: string | null;
12
+ repositoryCommitSha: string | null;
13
+ originator: string | null;
14
+ isSubagent: boolean;
15
+ startedAt?: Date | null;
16
+ endedAt?: Date | null;
17
+ }>;
18
+ export type AgentSessionCandidate = Readonly<{
19
+ path: string;
20
+ metadata: AgentSessionMetadata;
21
+ modifiedAt?: Date;
22
+ }>;
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<{
25
+ path: string;
26
+ reason: DiscoverySkipReason;
27
+ }>;
28
+ export type FailedAgentSession = Readonly<{
29
+ path: string;
30
+ error: string;
31
+ }>;
32
+ export type AgentSessionAnalysis = Readonly<{
33
+ sourceContentHash: string;
34
+ originalBytes: number;
35
+ prunedBytes: number;
36
+ }>;
37
+ export type AgentSessionPlan = AgentSessionAnalysis & Readonly<{
38
+ metadata: CanonicalSessionMetadata;
39
+ }>;
40
+ export type PrunedAgentSession = AgentSessionAnalysis & Readonly<{
41
+ bytes: Uint8Array<ArrayBuffer>;
42
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -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/dist/setup/cli.js CHANGED
@@ -17,6 +17,7 @@ Usage:
17
17
  groundtrack setup --all-detected --yes Configure detected agents
18
18
  groundtrack status Check selected agents
19
19
  groundtrack doctor [--fix] Check or repair using the same flow
20
+ groundtrack import [options] Import local coding-agent sessions
20
21
  groundtrack version
21
22
 
22
23
  Options:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundtrack/hooks",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Lifecycle hook utilities for Groundtrack MCP clients.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -41,6 +41,48 @@
41
41
  "workerd": "./src/agents.ts",
42
42
  "development": "./src/agents.ts",
43
43
  "default": "./dist/agents.js"
44
+ },
45
+ "./session-import/redaction": {
46
+ "types": "./src/session-import/redaction.ts",
47
+ "browser": "./src/session-import/redaction.ts",
48
+ "workerd": "./src/session-import/redaction.ts",
49
+ "development": "./src/session-import/redaction.ts",
50
+ "default": "./dist/session-import/redaction.js"
51
+ },
52
+ "./session-import/codex": {
53
+ "types": "./src/session-import/codex.ts",
54
+ "browser": "./src/session-import/codex.ts",
55
+ "workerd": "./src/session-import/codex.ts",
56
+ "development": "./src/session-import/codex.ts",
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"
44
86
  }
45
87
  },
46
88
  "publishConfig": {