@groundtrack/hooks 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/dist/agents.d.ts +55 -0
- package/dist/agents.js +77 -3
- package/dist/groundtrack-hook.js +0 -0
- package/dist/groundtrack.js +0 -0
- package/dist/session-import/canonical.d.ts +33 -0
- package/dist/session-import/canonical.js +10 -0
- package/dist/session-import/claude.d.ts +20 -0
- package/dist/session-import/claude.js +237 -0
- package/dist/session-import/cli.d.ts +3 -2
- package/dist/session-import/cli.js +161 -131
- package/dist/session-import/client.d.ts +4 -3
- package/dist/session-import/client.js +4 -3
- package/dist/session-import/codex.d.ts +6 -33
- package/dist/session-import/codex.js +62 -9
- package/dist/session-import/cursor.d.ts +13 -0
- package/dist/session-import/cursor.js +268 -0
- package/dist/session-import/discovery.d.ts +16 -7
- package/dist/session-import/discovery.js +127 -70
- package/dist/session-import/format.d.ts +51 -0
- package/dist/session-import/format.js +11 -0
- package/dist/session-import/formats.d.ts +4 -0
- package/dist/session-import/formats.js +13 -0
- package/dist/session-import/normalize.d.ts +21 -0
- package/dist/session-import/normalize.js +100 -0
- package/dist/session-import/opencode-cli.d.ts +14 -0
- package/dist/session-import/opencode-cli.js +178 -0
- package/dist/session-import/opencode-normalizer.d.ts +8 -0
- package/dist/session-import/opencode-normalizer.js +230 -0
- package/dist/session-import/opencode.d.ts +8 -0
- package/dist/session-import/opencode.js +230 -0
- package/dist/session-import/prune.d.ts +13 -4
- package/dist/session-import/prune.js +45 -79
- package/dist/session-import/receipts.d.ts +2 -1
- package/dist/session-import/receipts.js +3 -3
- package/dist/session-import/redaction.js +5 -3
- package/dist/session-import/types.d.ts +21 -7
- package/dist/setup/agents.d.ts +32 -0
- package/package.json +29 -1
- package/src/agents.ts +104 -3
|
@@ -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 {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
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
|
-
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
51
|
+
if (headerBytes.byteLength > CANONICAL_IMPORT_MAX_RECORD_BYTES)
|
|
104
52
|
throw new Error(`Canonical session metadata is too large: ${path}`);
|
|
105
|
-
|
|
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
|
|
112
|
-
const
|
|
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
|
-
|
|
116
|
-
bytes.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
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]{
|
|
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
|
|
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
|
|
18
|
+
export type AgentSessionCandidate = Readonly<{
|
|
13
19
|
path: string;
|
|
14
|
-
metadata:
|
|
20
|
+
metadata: AgentSessionMetadata;
|
|
21
|
+
modifiedAt?: Date;
|
|
15
22
|
}>;
|
|
16
|
-
export type DiscoverySkipReason = "invalid_metadata" | "live" | "subagent" | "before_since" | "other_repository" | "unscoped";
|
|
17
|
-
export type
|
|
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
|
|
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
|
|
37
|
+
export type AgentSessionPlan = AgentSessionAnalysis & Readonly<{
|
|
38
|
+
metadata: CanonicalSessionMetadata;
|
|
39
|
+
}>;
|
|
40
|
+
export type PrunedAgentSession = AgentSessionAnalysis & Readonly<{
|
|
27
41
|
bytes: Uint8Array<ArrayBuffer>;
|
|
28
42
|
}>;
|
package/dist/setup/agents.d.ts
CHANGED
|
@@ -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.
|
|
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": {
|