@groundtrack/hooks 0.1.7 → 0.1.9
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 +124 -43
- package/dist/agent-instructions.d.ts +2 -1
- package/dist/agents.d.ts +36 -0
- package/dist/agents.js +27 -0
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.js +9 -6
- package/dist/enrich-store-event.js +2 -1
- package/dist/groundtrack.js +9 -195
- package/dist/session-import/cli.d.ts +21 -0
- package/dist/session-import/cli.js +335 -0
- package/dist/session-import/client.d.ts +13 -0
- package/dist/session-import/client.js +73 -0
- package/dist/session-import/codex.d.ts +37 -0
- package/dist/session-import/codex.js +134 -0
- package/dist/session-import/discovery.d.ts +22 -0
- package/dist/session-import/discovery.js +188 -0
- package/dist/session-import/prune.d.ts +5 -0
- package/dist/session-import/prune.js +120 -0
- package/dist/session-import/receipts.d.ts +20 -0
- package/dist/session-import/receipts.js +44 -0
- package/dist/session-import/redaction.d.ts +6 -0
- package/dist/session-import/redaction.js +61 -0
- package/dist/session-import/types.d.ts +28 -0
- package/dist/session-import/types.js +1 -0
- package/dist/setup/agents.d.ts +59 -0
- package/dist/setup/agents.js +160 -0
- package/dist/setup/cli.d.ts +16 -0
- package/dist/setup/cli.js +332 -0
- package/dist/setup/config.d.ts +9 -0
- package/dist/setup/config.js +143 -0
- package/dist/setup/connections.d.ts +26 -0
- package/dist/setup/connections.js +210 -0
- package/dist/setup/files.d.ts +18 -0
- package/dist/setup/files.js +104 -0
- package/dist/setup/plan.d.ts +30 -0
- package/dist/setup/plan.js +324 -0
- package/package.json +29 -2
- package/src/agents.ts +37 -0
- package/templates/AGENTS.md +2 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync, readdirSync, realpathSync, } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function stringValue(value) {
|
|
8
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
9
|
+
}
|
|
10
|
+
function firstLine(path, maxBytes = 1024 * 1024) {
|
|
11
|
+
const file = openSync(path, "r");
|
|
12
|
+
try {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let offset = 0;
|
|
15
|
+
while (offset < maxBytes) {
|
|
16
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, maxBytes - offset));
|
|
17
|
+
const read = readSync(file, chunk, 0, chunk.length, offset);
|
|
18
|
+
if (read === 0)
|
|
19
|
+
break;
|
|
20
|
+
const bytes = chunk.subarray(0, read);
|
|
21
|
+
const newline = bytes.indexOf(10);
|
|
22
|
+
chunks.push(newline >= 0 ? bytes.subarray(0, newline) : bytes);
|
|
23
|
+
offset += newline >= 0 ? newline : read;
|
|
24
|
+
if (newline >= 0)
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
closeSync(file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isSubagentMetadata(payload) {
|
|
34
|
+
if (stringValue(payload.parent_thread_id) || stringValue(payload.agent_path))
|
|
35
|
+
return true;
|
|
36
|
+
if (isRecord(payload.source) && "subagent" in payload.source)
|
|
37
|
+
return true;
|
|
38
|
+
return ["subagent", "agent_forked_thread", "guardian_review"].includes(stringValue(payload.thread_source) ?? "");
|
|
39
|
+
}
|
|
40
|
+
export function parseCodexMetadata(line) {
|
|
41
|
+
try {
|
|
42
|
+
const record = JSON.parse(line);
|
|
43
|
+
if (!isRecord(record) || record.type !== "session_meta" || !isRecord(record.payload))
|
|
44
|
+
return null;
|
|
45
|
+
const payload = record.payload;
|
|
46
|
+
const git = isRecord(payload.git) ? payload.git : {};
|
|
47
|
+
const externalSessionId = stringValue(payload.session_id) ?? stringValue(payload.id);
|
|
48
|
+
if (!externalSessionId)
|
|
49
|
+
return null;
|
|
50
|
+
const timestampValue = stringValue(payload.timestamp) ?? stringValue(record.timestamp);
|
|
51
|
+
const timestamp = timestampValue ? new Date(timestampValue) : null;
|
|
52
|
+
return {
|
|
53
|
+
externalSessionId,
|
|
54
|
+
timestamp: timestamp && !Number.isNaN(timestamp.getTime()) ? timestamp : null,
|
|
55
|
+
cwd: stringValue(payload.cwd),
|
|
56
|
+
repositoryUrl: stringValue(git.repository_url),
|
|
57
|
+
repositoryBranch: stringValue(git.branch),
|
|
58
|
+
repositoryCommitSha: stringValue(git.commit_hash),
|
|
59
|
+
originator: stringValue(payload.originator),
|
|
60
|
+
isSubagent: isSubagentMetadata(payload),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function walkJsonl(root) {
|
|
68
|
+
if (!existsSync(root))
|
|
69
|
+
return [];
|
|
70
|
+
const result = [];
|
|
71
|
+
const visit = (directory) => {
|
|
72
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
73
|
+
const path = resolve(directory, entry.name);
|
|
74
|
+
if (entry.isDirectory())
|
|
75
|
+
visit(path);
|
|
76
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".jsonl"))
|
|
77
|
+
result.push(path);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
visit(root);
|
|
81
|
+
return result.sort();
|
|
82
|
+
}
|
|
83
|
+
export function normalizeRepositoryUrl(value) {
|
|
84
|
+
const trimmed = value
|
|
85
|
+
.trim()
|
|
86
|
+
.replace(/\.git\/?$/i, "")
|
|
87
|
+
.replace(/\/$/, "");
|
|
88
|
+
const scp = trimmed.match(/^[^@\s]+@([^:\s]+):(.+)$/);
|
|
89
|
+
const path = scp?.[2] ??
|
|
90
|
+
(() => {
|
|
91
|
+
try {
|
|
92
|
+
return new URL(trimmed).pathname;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return trimmed.includes("/") ? trimmed : null;
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
if (!path)
|
|
99
|
+
return null;
|
|
100
|
+
const parts = path.split("/").filter(Boolean);
|
|
101
|
+
if (parts.length < 2)
|
|
102
|
+
return null;
|
|
103
|
+
return parts.slice(-2).join("/").toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
export function repositoryIdentity(path) {
|
|
106
|
+
const root = realpathSync(execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
109
|
+
timeout: 5_000,
|
|
110
|
+
}).trim());
|
|
111
|
+
const names = execFileSync("git", ["-C", root, "remote"], {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
114
|
+
timeout: 5_000,
|
|
115
|
+
})
|
|
116
|
+
.split(/\r?\n/)
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
const remotes = new Set();
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const urls = execFileSync("git", ["-C", root, "remote", "get-url", "--all", name], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
123
|
+
timeout: 5_000,
|
|
124
|
+
}).split(/\r?\n/);
|
|
125
|
+
for (const url of urls) {
|
|
126
|
+
const normalized = normalizeRepositoryUrl(url);
|
|
127
|
+
if (normalized)
|
|
128
|
+
remotes.add(normalized);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { root, remotes };
|
|
132
|
+
}
|
|
133
|
+
function isInside(candidate, root) {
|
|
134
|
+
if (!isAbsolute(candidate))
|
|
135
|
+
return false;
|
|
136
|
+
const child = relative(root, resolve(candidate));
|
|
137
|
+
return child === "" || (!child.startsWith(`..${sep}`) && child !== "..");
|
|
138
|
+
}
|
|
139
|
+
export function discoverCodexSessions(options) {
|
|
140
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
141
|
+
const liveWindowMs = options.liveWindowMs ?? 5 * 60_000;
|
|
142
|
+
const roots = [
|
|
143
|
+
resolve(options.codexHome, "sessions"),
|
|
144
|
+
resolve(options.codexHome, "archived_sessions"),
|
|
145
|
+
];
|
|
146
|
+
const included = [];
|
|
147
|
+
const skipped = [];
|
|
148
|
+
for (const path of roots.flatMap(walkJsonl)) {
|
|
149
|
+
const metadata = parseCodexMetadata(firstLine(path));
|
|
150
|
+
if (!metadata) {
|
|
151
|
+
skipped.push({ path, reason: "invalid_metadata" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const stat = lstatSync(path);
|
|
155
|
+
if (!stat.isFile())
|
|
156
|
+
continue;
|
|
157
|
+
if (nowMs - stat.mtimeMs < liveWindowMs) {
|
|
158
|
+
skipped.push({ path, reason: "live" });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!options.includeSubagents && metadata.isSubagent) {
|
|
162
|
+
skipped.push({ path, reason: "subagent" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (options.since && (!metadata.timestamp || metadata.timestamp < options.since)) {
|
|
166
|
+
skipped.push({ path, reason: "before_since" });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const normalizedRemote = metadata.repositoryUrl
|
|
170
|
+
? normalizeRepositoryUrl(metadata.repositoryUrl)
|
|
171
|
+
: null;
|
|
172
|
+
const matchesCurrentRepository = options.repository
|
|
173
|
+
? normalizedRemote
|
|
174
|
+
? options.repository.remotes.has(normalizedRemote)
|
|
175
|
+
: Boolean(metadata.cwd && isInside(metadata.cwd, options.repository.root))
|
|
176
|
+
: false;
|
|
177
|
+
if (!options.allRepositories && !matchesCurrentRepository) {
|
|
178
|
+
skipped.push({ path, reason: "other_repository" });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (options.allRepositories && !normalizedRemote && !options.includeUnscoped) {
|
|
182
|
+
skipped.push({ path, reason: "unscoped" });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
included.push({ path, metadata });
|
|
186
|
+
}
|
|
187
|
+
return { included, skipped };
|
|
188
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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<{
|
|
4
|
+
emitBytes: false;
|
|
5
|
+
}>): Promise<CodexSessionAnalysis>;
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { 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 = {}) {
|
|
15
|
+
const before = await stat(path);
|
|
16
|
+
const hash = createHash("sha256");
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const entryChunks = [];
|
|
19
|
+
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;
|
|
28
|
+
const hashingStream = new Transform({
|
|
29
|
+
transform(chunk, _encoding, callback) {
|
|
30
|
+
hash.update(chunk);
|
|
31
|
+
callback(null, chunk);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const input = createReadStream(path);
|
|
35
|
+
input.pipe(hashingStream);
|
|
36
|
+
const lines = createInterface({ input: hashingStream, crlfDelay: Infinity });
|
|
37
|
+
for await (const line of lines) {
|
|
38
|
+
if (!line.trim())
|
|
39
|
+
continue;
|
|
40
|
+
originalRecords += 1;
|
|
41
|
+
let record;
|
|
42
|
+
try {
|
|
43
|
+
record = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
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;
|
|
71
|
+
}
|
|
72
|
+
else if (!isKnownSkippedCodexRecord(record) && !sensitive)
|
|
73
|
+
unsupportedRecordCount += 1;
|
|
74
|
+
}
|
|
75
|
+
const after = await stat(path);
|
|
76
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
77
|
+
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
|
+
};
|
|
102
|
+
const headerBytes = encoder.encode(`${JSON.stringify(header)}\n`);
|
|
103
|
+
if (headerBytes.byteLength > CODEX_CANONICAL_IMPORT_MAX_RECORD_BYTES)
|
|
104
|
+
throw new Error(`Canonical session metadata is too large: ${path}`);
|
|
105
|
+
const analysis = {
|
|
106
|
+
sourceContentHash: hash.digest("hex"),
|
|
107
|
+
originalBytes: before.size,
|
|
108
|
+
prunedBytes: headerBytes.byteLength + entryBytes,
|
|
109
|
+
};
|
|
110
|
+
if (options.emitBytes === false)
|
|
111
|
+
return analysis;
|
|
112
|
+
const bytes = new Uint8Array(analysis.prunedBytes);
|
|
113
|
+
bytes.set(headerBytes);
|
|
114
|
+
let offset = headerBytes.byteLength;
|
|
115
|
+
for (const chunk of entryChunks) {
|
|
116
|
+
bytes.set(chunk, offset);
|
|
117
|
+
offset += chunk.byteLength;
|
|
118
|
+
}
|
|
119
|
+
return { ...analysis, bytes };
|
|
120
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SetupEnvironment } from "../setup/agents.js";
|
|
2
|
+
import type { 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
|
+
sourceContentHash: string;
|
|
17
|
+
visibility: ImportVisibility;
|
|
18
|
+
}): string;
|
|
19
|
+
export declare function saveReceipt(context: SetupEnvironment, file: ReceiptFile, key: string, sessionId: string): void;
|
|
20
|
+
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 { CODEX_CANONICALIZATION_VERSION } from "./codex.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
|
+
"codex_cli",
|
|
35
|
+
CODEX_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,28 @@
|
|
|
1
|
+
export type ImportVisibility = "private" | "organization";
|
|
2
|
+
export type CodexSessionMetadata = Readonly<{
|
|
3
|
+
externalSessionId: string;
|
|
4
|
+
timestamp: Date | null;
|
|
5
|
+
cwd: string | null;
|
|
6
|
+
repositoryUrl: string | null;
|
|
7
|
+
repositoryBranch: string | null;
|
|
8
|
+
repositoryCommitSha: string | null;
|
|
9
|
+
originator: string | null;
|
|
10
|
+
isSubagent: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
export type CodexSessionCandidate = Readonly<{
|
|
13
|
+
path: string;
|
|
14
|
+
metadata: CodexSessionMetadata;
|
|
15
|
+
}>;
|
|
16
|
+
export type DiscoverySkipReason = "invalid_metadata" | "live" | "subagent" | "before_since" | "other_repository" | "unscoped";
|
|
17
|
+
export type SkippedCodexSession = Readonly<{
|
|
18
|
+
path: string;
|
|
19
|
+
reason: DiscoverySkipReason;
|
|
20
|
+
}>;
|
|
21
|
+
export type CodexSessionAnalysis = Readonly<{
|
|
22
|
+
sourceContentHash: string;
|
|
23
|
+
originalBytes: number;
|
|
24
|
+
prunedBytes: number;
|
|
25
|
+
}>;
|
|
26
|
+
export type PrunedCodexSession = CodexSessionAnalysis & Readonly<{
|
|
27
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Agent } from "../agents.js";
|
|
2
|
+
export declare const MCP_URL = "https://app.groundtrack.dev/api/mcp";
|
|
3
|
+
export type SetupEnvironment = {
|
|
4
|
+
repoDir: string;
|
|
5
|
+
homeDir: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
platform: NodeJS.Platform;
|
|
8
|
+
};
|
|
9
|
+
export declare function environment(options?: Partial<SetupEnvironment>): SetupEnvironment;
|
|
10
|
+
export declare function agentId(value: string): Agent;
|
|
11
|
+
export declare function executable(name: string, context: SetupEnvironment): string | null;
|
|
12
|
+
export declare function stateDir(context: SetupEnvironment): string;
|
|
13
|
+
export declare function configPaths(agent: Agent, context: SetupEnvironment): {
|
|
14
|
+
project: string;
|
|
15
|
+
user: string;
|
|
16
|
+
presence: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function serverPath(agent: Agent, config: Record<string, unknown>): string[];
|
|
19
|
+
export declare function readConfig(agent: Agent, text: string): Record<string, unknown>;
|
|
20
|
+
export declare function serverMap(agent: Agent, config: Record<string, unknown>): Record<string, unknown>;
|
|
21
|
+
export declare function isGroundtrack(alias: string, value: unknown, url: string): boolean;
|
|
22
|
+
export declare function discoverAgents(context: SetupEnvironment, url?: string): ({
|
|
23
|
+
id: "claude";
|
|
24
|
+
name: "Claude Code";
|
|
25
|
+
aliases: readonly ["claude-code"];
|
|
26
|
+
source: "claude_code";
|
|
27
|
+
enrichment: "command";
|
|
28
|
+
detected: boolean;
|
|
29
|
+
configured: boolean;
|
|
30
|
+
issue: string | null;
|
|
31
|
+
} | {
|
|
32
|
+
id: "codex";
|
|
33
|
+
name: "Codex";
|
|
34
|
+
aliases: readonly [];
|
|
35
|
+
source: "codex";
|
|
36
|
+
enrichment: "command";
|
|
37
|
+
detected: boolean;
|
|
38
|
+
configured: boolean;
|
|
39
|
+
issue: string | null;
|
|
40
|
+
} | {
|
|
41
|
+
id: "cursor";
|
|
42
|
+
name: "Cursor";
|
|
43
|
+
aliases: readonly [];
|
|
44
|
+
source: "cursor";
|
|
45
|
+
enrichment: "command";
|
|
46
|
+
detected: boolean;
|
|
47
|
+
configured: boolean;
|
|
48
|
+
issue: string | null;
|
|
49
|
+
} | {
|
|
50
|
+
id: "opencode";
|
|
51
|
+
name: "OpenCode";
|
|
52
|
+
aliases: readonly [];
|
|
53
|
+
source: "opencode";
|
|
54
|
+
enrichment: "plugin";
|
|
55
|
+
detected: boolean;
|
|
56
|
+
configured: boolean;
|
|
57
|
+
issue: string | null;
|
|
58
|
+
})[];
|
|
59
|
+
export declare function savedAgents(context: SetupEnvironment): Agent[] | null;
|