attestari 0.0.0-stage → 2.9.1
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 +55 -2
- package/dist/hook.js +2416 -0
- package/package.json +29 -4
package/dist/hook.js
ADDED
|
@@ -0,0 +1,2416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/main.ts
|
|
5
|
+
var import_node_fs11 = require("node:fs");
|
|
6
|
+
|
|
7
|
+
// src/adapters/claude-code.ts
|
|
8
|
+
var import_node_path6 = require("node:path");
|
|
9
|
+
|
|
10
|
+
// src/call.ts
|
|
11
|
+
var import_node_crypto2 = require("node:crypto");
|
|
12
|
+
|
|
13
|
+
// src/cache.ts
|
|
14
|
+
var import_node_crypto = require("node:crypto");
|
|
15
|
+
var import_node_fs = require("node:fs");
|
|
16
|
+
var import_node_path2 = require("node:path");
|
|
17
|
+
|
|
18
|
+
// src/paths.ts
|
|
19
|
+
var import_node_os = require("node:os");
|
|
20
|
+
var import_node_path = require("node:path");
|
|
21
|
+
function home() {
|
|
22
|
+
return process.env.HOME || process.env.USERPROFILE || (0, import_node_os.homedir)();
|
|
23
|
+
}
|
|
24
|
+
function attestariDir() {
|
|
25
|
+
return (0, import_node_path.join)(home(), ".attestari");
|
|
26
|
+
}
|
|
27
|
+
function agentDir(agent) {
|
|
28
|
+
return (0, import_node_path.join)(attestariDir(), agent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/validate.ts
|
|
32
|
+
function isObject(value) {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
function isString(value) {
|
|
36
|
+
return typeof value === "string";
|
|
37
|
+
}
|
|
38
|
+
function isEntry(value) {
|
|
39
|
+
return isObject(value) && isString(value.key) && isString(value.source) && isString(value.name) && (value.version === null || isString(value.version)) && (value.decision === "allow" || value.decision === "block" || value.decision === "warn") && isString(value.rule) && (value.grade === null || isString(value.grade));
|
|
40
|
+
}
|
|
41
|
+
function isCompiled(value) {
|
|
42
|
+
if (!isObject(value) || value.format !== 1) return false;
|
|
43
|
+
const policy = value.policy;
|
|
44
|
+
if (!isObject(policy) || typeof policy.version !== "number" || !isString(policy.minimum_grade) || typeof policy.block_not_examined !== "boolean" || typeof policy.block_not_gradeable !== "boolean") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const unlisted = value.unlisted;
|
|
48
|
+
if (!isObject(unlisted) || unlisted.decision !== "block" || unlisted.rule !== "not_checked") return false;
|
|
49
|
+
return isString(value.etag) && value.etag !== "" && isString(value.compiled_at) && Array.isArray(value.not_enforced) && value.not_enforced.every(isString) && Array.isArray(value.servers) && value.servers.every(isEntry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/cache.ts
|
|
53
|
+
var MAX_EVENTS = 1e3;
|
|
54
|
+
function cacheDir(agent) {
|
|
55
|
+
return agentDir(agent);
|
|
56
|
+
}
|
|
57
|
+
function ensureDir(agent) {
|
|
58
|
+
const dir = cacheDir(agent);
|
|
59
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
function writeAtomic(file, data, mode) {
|
|
63
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
(0, import_node_fs.writeFileSync)(temporary, data, { mode });
|
|
66
|
+
try {
|
|
67
|
+
(0, import_node_fs.chmodSync)(temporary, mode);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
(0, import_node_fs.renameSync)(temporary, file);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
try {
|
|
73
|
+
(0, import_node_fs.unlinkSync)(temporary);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function readText(file) {
|
|
80
|
+
try {
|
|
81
|
+
return (0, import_node_fs.readFileSync)(file, "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function readPolicy(agent) {
|
|
87
|
+
const file = (0, import_node_path2.join)(cacheDir(agent), "policy.json");
|
|
88
|
+
const text = readText(file);
|
|
89
|
+
if (text === null) return null;
|
|
90
|
+
let document;
|
|
91
|
+
try {
|
|
92
|
+
document = JSON.parse(text);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!isCompiled(document)) return null;
|
|
97
|
+
let modified;
|
|
98
|
+
try {
|
|
99
|
+
modified = (0, import_node_fs.statSync)(file).mtimeMs;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return { policy: document, ageMs: Math.max(0, Date.now() - modified) };
|
|
104
|
+
}
|
|
105
|
+
function isResolved(value) {
|
|
106
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
107
|
+
const r = value;
|
|
108
|
+
return typeof r.key === "string" && typeof r.source === "string" && typeof r.name === "string" && (r.version === null || typeof r.version === "string") && typeof r.local === "string";
|
|
109
|
+
}
|
|
110
|
+
function writePolicy(agent, policy, etag) {
|
|
111
|
+
const dir = ensureDir(agent);
|
|
112
|
+
writeAtomic((0, import_node_path2.join)(dir, "policy.json"), `${JSON.stringify(policy, null, 2)}
|
|
113
|
+
`, 384);
|
|
114
|
+
writeAtomic((0, import_node_path2.join)(dir, "policy.etag"), etag, 384);
|
|
115
|
+
}
|
|
116
|
+
function readEtag(agent) {
|
|
117
|
+
const etag = readText((0, import_node_path2.join)(cacheDir(agent), "policy.etag"))?.trim();
|
|
118
|
+
return etag ? etag : null;
|
|
119
|
+
}
|
|
120
|
+
function readToken(agent) {
|
|
121
|
+
const text = readText((0, import_node_path2.join)(cacheDir(agent), "token"));
|
|
122
|
+
const token = text?.trim();
|
|
123
|
+
return token ? token : null;
|
|
124
|
+
}
|
|
125
|
+
function writeToken(agent, token) {
|
|
126
|
+
writeAtomic((0, import_node_path2.join)(ensureDir(agent), "token"), token, 384);
|
|
127
|
+
}
|
|
128
|
+
function appendEvent(agent, event) {
|
|
129
|
+
const file = (0, import_node_path2.join)(ensureDir(agent), "events.jsonl");
|
|
130
|
+
(0, import_node_fs.appendFileSync)(file, `${JSON.stringify(event)}
|
|
131
|
+
`, { mode: 384 });
|
|
132
|
+
const lines = (readText(file) ?? "").split("\n").filter((line) => line !== "");
|
|
133
|
+
if (lines.length > MAX_EVENTS) {
|
|
134
|
+
writeAtomic(file, `${lines.slice(-MAX_EVENTS).join("\n")}
|
|
135
|
+
`, 384);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function readEvents(agent) {
|
|
139
|
+
const events = [];
|
|
140
|
+
for (const line of (readText((0, import_node_path2.join)(cacheDir(agent), "events.jsonl")) ?? "").split("\n")) {
|
|
141
|
+
if (line === "") continue;
|
|
142
|
+
try {
|
|
143
|
+
events.push(JSON.parse(line));
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return events;
|
|
148
|
+
}
|
|
149
|
+
function removeEvents(agent, ids) {
|
|
150
|
+
const file = (0, import_node_path2.join)(cacheDir(agent), "events.jsonl");
|
|
151
|
+
const text = readText(file);
|
|
152
|
+
if (text === null) return;
|
|
153
|
+
const sent2 = new Set(ids);
|
|
154
|
+
const keep = readEvents(agent).filter((event) => !sent2.has(event.id));
|
|
155
|
+
if (keep.length === 0) {
|
|
156
|
+
try {
|
|
157
|
+
(0, import_node_fs.unlinkSync)(file);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
writeAtomic(file, `${keep.map((event) => JSON.stringify(event)).join("\n")}
|
|
163
|
+
`, 384);
|
|
164
|
+
}
|
|
165
|
+
function folderId(cwd) {
|
|
166
|
+
return (0, import_node_crypto.createHash)("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
|
|
167
|
+
}
|
|
168
|
+
var FOLDER_FILE = /^([0-9a-f]{16})\.json$/;
|
|
169
|
+
function parseResolution(id, text, modifiedMs) {
|
|
170
|
+
if (text === null) return null;
|
|
171
|
+
let document;
|
|
172
|
+
try {
|
|
173
|
+
document = JSON.parse(text);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (typeof document !== "object" || document === null || Array.isArray(document)) return null;
|
|
178
|
+
const { complete, servers, cwd } = document;
|
|
179
|
+
if (typeof complete !== "boolean" || !Array.isArray(servers) || !servers.every(isResolved)) return null;
|
|
180
|
+
const path = typeof cwd === "string" && cwd !== "" && folderId(cwd) === id ? { cwd } : {};
|
|
181
|
+
const when = modifiedMs === void 0 ? {} : { modifiedMs };
|
|
182
|
+
return { id, complete, servers: servers.map((r) => ({ ...r, folder: id.slice(0, 8) })), ...path, ...when };
|
|
183
|
+
}
|
|
184
|
+
function readResolution(agent, cwd) {
|
|
185
|
+
return readResolutionOf(agent, folderId(cwd));
|
|
186
|
+
}
|
|
187
|
+
function readResolutionOf(agent, id) {
|
|
188
|
+
if (!/^[0-9a-f]{16}$/.test(id)) return null;
|
|
189
|
+
return parseResolution(id, readText((0, import_node_path2.join)(cacheDir(agent), "resolved", `${id}.json`)));
|
|
190
|
+
}
|
|
191
|
+
function sessionFile(agent, sessionId) {
|
|
192
|
+
const name = /^[A-Za-z0-9_-]{1,128}$/.test(sessionId) ? sessionId : (0, import_node_crypto.createHash)("sha256").update(sessionId, "utf8").digest("hex");
|
|
193
|
+
return (0, import_node_path2.join)(cacheDir(agent), "sessions", name);
|
|
194
|
+
}
|
|
195
|
+
function writeSession(agent, sessionId, cwd) {
|
|
196
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path2.join)(ensureDir(agent), "sessions"), { recursive: true, mode: 448 });
|
|
197
|
+
writeAtomic(sessionFile(agent, sessionId), `${JSON.stringify({ folder: folderId(cwd), cwd })}
|
|
198
|
+
`, 384);
|
|
199
|
+
}
|
|
200
|
+
function readSession(agent, sessionId) {
|
|
201
|
+
const text = readText(sessionFile(agent, sessionId));
|
|
202
|
+
if (text === null) return null;
|
|
203
|
+
try {
|
|
204
|
+
const value = JSON.parse(text);
|
|
205
|
+
if (typeof value !== "object" || value === null) return null;
|
|
206
|
+
const { folder, cwd } = value;
|
|
207
|
+
return typeof folder === "string" && typeof cwd === "string" && cwd !== "" && folderId(cwd) === folder ? { folder, cwd } : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function pruneSessions(agent, maxAgeMs, now = Date.now()) {
|
|
213
|
+
const dir = (0, import_node_path2.join)(cacheDir(agent), "sessions");
|
|
214
|
+
let names;
|
|
215
|
+
try {
|
|
216
|
+
names = (0, import_node_fs.readdirSync)(dir);
|
|
217
|
+
} catch {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
let removed = 0;
|
|
221
|
+
for (const name of names) {
|
|
222
|
+
try {
|
|
223
|
+
const file = (0, import_node_path2.join)(dir, name);
|
|
224
|
+
if (now - (0, import_node_fs.statSync)(file).mtimeMs > maxAgeMs) {
|
|
225
|
+
(0, import_node_fs.unlinkSync)(file);
|
|
226
|
+
removed += 1;
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return removed;
|
|
232
|
+
}
|
|
233
|
+
function readResolutions(agent) {
|
|
234
|
+
let names;
|
|
235
|
+
try {
|
|
236
|
+
names = (0, import_node_fs.readdirSync)((0, import_node_path2.join)(cacheDir(agent), "resolved")).sort();
|
|
237
|
+
} catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
const id = FOLDER_FILE.exec(name)?.[1];
|
|
243
|
+
if (id === void 0) continue;
|
|
244
|
+
const file = (0, import_node_path2.join)(cacheDir(agent), "resolved", name);
|
|
245
|
+
let modifiedMs;
|
|
246
|
+
try {
|
|
247
|
+
modifiedMs = (0, import_node_fs.statSync)(file).mtimeMs;
|
|
248
|
+
} catch {
|
|
249
|
+
modifiedMs = void 0;
|
|
250
|
+
}
|
|
251
|
+
const resolution = parseResolution(id, readText(file), modifiedMs);
|
|
252
|
+
if (resolution !== null) out.push(resolution);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function writeResolution(agent, cwd, servers, complete) {
|
|
257
|
+
const dir = (0, import_node_path2.join)(ensureDir(agent), "resolved");
|
|
258
|
+
const file = (0, import_node_path2.join)(dir, `${folderId(cwd)}.json`);
|
|
259
|
+
try {
|
|
260
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
261
|
+
const kept = servers.map(({ folder: _folder, ...r }) => r);
|
|
262
|
+
writeAtomic(file, `${JSON.stringify({ complete, cwd, servers: kept }, null, 2)}
|
|
263
|
+
`, 384);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
try {
|
|
266
|
+
(0, import_node_fs.rmSync)(file, { force: true });
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function readWritten(agent, id) {
|
|
273
|
+
const text = readText((0, import_node_path2.join)(cacheDir(agent), "written", `${id}.json`));
|
|
274
|
+
if (text === null) return [];
|
|
275
|
+
try {
|
|
276
|
+
const value = JSON.parse(text);
|
|
277
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : [];
|
|
278
|
+
} catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function dropResolution(agent, id) {
|
|
283
|
+
if (!/^[0-9a-f]{16}$/.test(id)) return;
|
|
284
|
+
(0, import_node_fs.rmSync)((0, import_node_path2.join)(cacheDir(agent), "resolved", `${id}.json`), { force: true });
|
|
285
|
+
(0, import_node_fs.rmSync)((0, import_node_path2.join)(cacheDir(agent), "written", `${id}.json`), { force: true });
|
|
286
|
+
}
|
|
287
|
+
function writeWritten(agent, id, written) {
|
|
288
|
+
const dir = (0, import_node_path2.join)(ensureDir(agent), "written");
|
|
289
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
290
|
+
writeAtomic((0, import_node_path2.join)(dir, `${id}.json`), `${JSON.stringify(written, null, 2)}
|
|
291
|
+
`, 384);
|
|
292
|
+
}
|
|
293
|
+
function deleteToken(agent) {
|
|
294
|
+
try {
|
|
295
|
+
(0, import_node_fs.unlinkSync)((0, import_node_path2.join)(cacheDir(agent), "token"));
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function deletePolicy(agent) {
|
|
300
|
+
for (const name of ["policy.json", "policy.etag"]) {
|
|
301
|
+
try {
|
|
302
|
+
(0, import_node_fs.unlinkSync)((0, import_node_path2.join)(cacheDir(agent), name));
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function deleteStamp(agent, name) {
|
|
308
|
+
try {
|
|
309
|
+
(0, import_node_fs.unlinkSync)((0, import_node_path2.join)(cacheDir(agent), name));
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function writeStamp(agent, name, at = /* @__PURE__ */ new Date(), word) {
|
|
314
|
+
writeAtomic((0, import_node_path2.join)(ensureDir(agent), name), `${at.toISOString()}${word === void 0 ? "" : ` ${word}`}
|
|
315
|
+
`, 384);
|
|
316
|
+
}
|
|
317
|
+
function readStamp(agent, name) {
|
|
318
|
+
const parts = (readText((0, import_node_path2.join)(cacheDir(agent), name)) ?? "").trim().split(/\s+/);
|
|
319
|
+
const at = Date.parse(parts[0]);
|
|
320
|
+
if (parts[0] === "" || Number.isNaN(at)) return null;
|
|
321
|
+
return { at, word: parts[1] ?? null };
|
|
322
|
+
}
|
|
323
|
+
function stampAge(agent, name, now = Date.now()) {
|
|
324
|
+
const stamp = readStamp(agent, name);
|
|
325
|
+
if (stamp === null || stamp.at > now) return null;
|
|
326
|
+
return now - stamp.at;
|
|
327
|
+
}
|
|
328
|
+
var SECRET_BYTES = 32;
|
|
329
|
+
function readSecret(agent) {
|
|
330
|
+
const file = (0, import_node_path2.join)(ensureDir(agent), "secret");
|
|
331
|
+
const held = () => {
|
|
332
|
+
try {
|
|
333
|
+
const bytes = (0, import_node_fs.readFileSync)(file);
|
|
334
|
+
return bytes.length === SECRET_BYTES ? bytes : null;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const existing = held();
|
|
340
|
+
if (existing !== null) return existing;
|
|
341
|
+
const fresh = (0, import_node_crypto.randomBytes)(SECRET_BYTES);
|
|
342
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
343
|
+
(0, import_node_fs.writeFileSync)(temporary, fresh, { mode: 384 });
|
|
344
|
+
try {
|
|
345
|
+
try {
|
|
346
|
+
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
(0, import_node_fs.linkSync)(temporary, file);
|
|
351
|
+
return fresh;
|
|
352
|
+
} catch {
|
|
353
|
+
const other = held();
|
|
354
|
+
if (other !== null) return other;
|
|
355
|
+
(0, import_node_fs.renameSync)(temporary, file);
|
|
356
|
+
return fresh;
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
try {
|
|
360
|
+
(0, import_node_fs.unlinkSync)(temporary);
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/decide.ts
|
|
367
|
+
var STALE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
368
|
+
var NOT_CHECKED = "not_checked";
|
|
369
|
+
var PACKAGES = /* @__PURE__ */ new Set(["npm", "pypi", "remote"]);
|
|
370
|
+
var BY_KEY = /* @__PURE__ */ new Set(["local", "skill"]);
|
|
371
|
+
var REPORTS = /* @__PURE__ */ new Set(["npm", "pypi"]);
|
|
372
|
+
var STRICTEST = { block: 0, warn: 1, allow: 2 };
|
|
373
|
+
function reducedKey(key) {
|
|
374
|
+
return Array.from(key, (c) => /^[A-Za-z0-9@/._-]$/.test(c) ? c : "_").join("").slice(0, 214);
|
|
375
|
+
}
|
|
376
|
+
function suffixedKey(reduced, folder) {
|
|
377
|
+
return `${reduced.slice(0, 214 - folder.length - 1)}/${folder}`;
|
|
378
|
+
}
|
|
379
|
+
function oneLine(text) {
|
|
380
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
381
|
+
}
|
|
382
|
+
function named(name, version) {
|
|
383
|
+
return version === null ? name : `${name} ${version}`;
|
|
384
|
+
}
|
|
385
|
+
function report(source, name, version) {
|
|
386
|
+
if (!REPORTS.has(source)) return "app.attestari.ai/tools";
|
|
387
|
+
const path = name.split("/").map(encodeURIComponent).join("/");
|
|
388
|
+
return `app.attestari.ai/tools/${source}/${path}${version === null ? "" : `/${encodeURIComponent(version)}`}`;
|
|
389
|
+
}
|
|
390
|
+
var BLOCKED = "Blocked by your organization's policy:";
|
|
391
|
+
var REASONS = /* @__PURE__ */ new Map([
|
|
392
|
+
["below_minimum_grade", (e, p) => `${BLOCKED} ${named(e.name, e.version)} is graded ${e.grade === null ? "" : `${e.grade}, `}below the minimum ${p.policy.minimum_grade}.`],
|
|
393
|
+
["not_examined", (e) => `${BLOCKED} ${named(e.name, e.version)} has not been examined by Attestari yet.`],
|
|
394
|
+
["under_re_examination", (e) => `${BLOCKED} ${named(e.name, e.version)} is being re-examined by Attestari.`],
|
|
395
|
+
["not_gradeable", (e) => `${BLOCKED} ${named(e.name, e.version)} was examined and could not be graded.`]
|
|
396
|
+
]);
|
|
397
|
+
function blocked(entry, policy) {
|
|
398
|
+
const words = REASONS.get(entry.rule);
|
|
399
|
+
const said = words ? words(entry, policy) : `${BLOCKED} ${named(entry.name, entry.version)} (${entry.rule}).`;
|
|
400
|
+
return `${said} ${report(entry.source, entry.name, entry.version)}`;
|
|
401
|
+
}
|
|
402
|
+
function matches(entry, resolved) {
|
|
403
|
+
if (entry.source !== resolved.source) return false;
|
|
404
|
+
if (BY_KEY.has(resolved.source)) {
|
|
405
|
+
const reduced = reducedKey(resolved.key);
|
|
406
|
+
return entry.key === reduced || resolved.folder !== void 0 && entry.key === suffixedKey(reduced, resolved.folder);
|
|
407
|
+
}
|
|
408
|
+
if (PACKAGES.has(resolved.source)) return entry.name === resolved.name && (resolved.version === null || entry.version === resolved.version);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
function entryFor(policy, resolved) {
|
|
412
|
+
return policy.servers.filter((e) => matches(e, resolved)).sort((a, b) => STRICTEST[a.decision] - STRICTEST[b.decision])[0];
|
|
413
|
+
}
|
|
414
|
+
function deny(rule, reason) {
|
|
415
|
+
return { allow: false, reason: oneLine(reason), rule };
|
|
416
|
+
}
|
|
417
|
+
function decide(policy, ageMs, resolved, key = resolved?.key) {
|
|
418
|
+
const tool = resolved !== null ? named(resolved.name, resolved.version) : key !== void 0 ? `the server "${reducedKey(key)}"` : "this server";
|
|
419
|
+
if (policy === null) {
|
|
420
|
+
return deny(NOT_CHECKED, `Not checked by Attestari: this agent holds no Attestari policy, so ${tool} is blocked. Pair it at app.attestari.ai/agents`);
|
|
421
|
+
}
|
|
422
|
+
if (ageMs > STALE_MS) {
|
|
423
|
+
return deny(NOT_CHECKED, `Attestari blocked ${tool}: not checked in for 7 days: open app.attestari.ai`);
|
|
424
|
+
}
|
|
425
|
+
const entry = resolved === null ? void 0 : entryFor(policy, resolved);
|
|
426
|
+
if (entry === void 0) {
|
|
427
|
+
return deny(policy.unlisted.rule, `Not yet checked by Attestari: ${tool} is not in your organization's policy yet. app.attestari.ai/agents`);
|
|
428
|
+
}
|
|
429
|
+
if (entry.decision === "allow") return { allow: true };
|
|
430
|
+
if (entry.decision === "warn") return { allow: true, warn: { rule: entry.rule } };
|
|
431
|
+
return deny(entry.rule, blocked(entry, policy));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/refresh.ts
|
|
435
|
+
var import_node_child_process = require("node:child_process");
|
|
436
|
+
var import_node_fs2 = require("node:fs");
|
|
437
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
438
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
439
|
+
var CHECKED_IN = "checked_in";
|
|
440
|
+
var LOOKUP_UNAVAILABLE = "lookup_unavailable";
|
|
441
|
+
var REFUSED = "refused";
|
|
442
|
+
function say(line) {
|
|
443
|
+
process.stderr.write(`attestari-hook: ${line.replace(/\s+/g, " ").trim()}
|
|
444
|
+
`);
|
|
445
|
+
}
|
|
446
|
+
function message(error) {
|
|
447
|
+
return error instanceof Error ? error.message : String(error);
|
|
448
|
+
}
|
|
449
|
+
function attempt(what, fn) {
|
|
450
|
+
try {
|
|
451
|
+
fn();
|
|
452
|
+
} catch (error) {
|
|
453
|
+
say(`${what}: ${message(error)}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function usableFolder(folder) {
|
|
457
|
+
try {
|
|
458
|
+
return typeof folder === "string" && folder !== "" && (0, import_node_fs2.statSync)(folder).isDirectory() ? folder : void 0;
|
|
459
|
+
} catch {
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function spawnRefresh(agent, cwd) {
|
|
464
|
+
const at = /* @__PURE__ */ new Date();
|
|
465
|
+
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at));
|
|
466
|
+
const folder = usableFolder(cwd);
|
|
467
|
+
let pid;
|
|
468
|
+
try {
|
|
469
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [__filename, agent, "refresh", ...cwd !== void 0 && cwd !== "" ? ["--cwd", cwd] : []], {
|
|
470
|
+
cwd: folder,
|
|
471
|
+
detached: true,
|
|
472
|
+
stdio: "ignore",
|
|
473
|
+
windowsHide: true
|
|
474
|
+
});
|
|
475
|
+
child.on("error", () => {
|
|
476
|
+
});
|
|
477
|
+
child.unref();
|
|
478
|
+
pid = child.pid;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
say(`the refresh could not be started: ${message(error)}`);
|
|
481
|
+
}
|
|
482
|
+
if (pid === void 0) {
|
|
483
|
+
attempt("refresh_started_at was not removed", () => deleteStamp(agent, "refresh_started_at"));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const started = pid;
|
|
487
|
+
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at, started));
|
|
488
|
+
}
|
|
489
|
+
function heldBack(agent, now, onlyAfterRefusal) {
|
|
490
|
+
const next = readStamp(agent, "next_refresh_at");
|
|
491
|
+
if (next === null || next.at <= now) return false;
|
|
492
|
+
return !onlyAfterRefusal || next.word !== CHECKED_IN;
|
|
493
|
+
}
|
|
494
|
+
function refreshDue(agent, now = Date.now()) {
|
|
495
|
+
if (readToken(agent) === null) return false;
|
|
496
|
+
const checked = stampAge(agent, "checked_in_at", now);
|
|
497
|
+
if (checked !== null && checked < HOUR_MS) return false;
|
|
498
|
+
const started = stampAge(agent, "refresh_started_at", now);
|
|
499
|
+
if (started !== null && started < HOUR_MS) return false;
|
|
500
|
+
return !heldBack(agent, now, false);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/call.ts
|
|
504
|
+
var UNREADABLE = "Attestari could not read the tool call";
|
|
505
|
+
var UNREADABLE_VERDICT = { allow: false, reason: UNREADABLE, rule: "unreadable" };
|
|
506
|
+
function record(adapter, decision, rule, tool) {
|
|
507
|
+
try {
|
|
508
|
+
appendEvent(adapter.name, { id: (0, import_node_crypto2.randomUUID)(), at: (/* @__PURE__ */ new Date()).toISOString(), decision, rule, tool, agent_client: adapter.client });
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function refreshWhenDue(agent, cwd) {
|
|
513
|
+
try {
|
|
514
|
+
if (refreshDue(agent)) spawnRefresh(agent, cwd);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function decideCall(adapter, event, stdinText) {
|
|
519
|
+
const parsed = adapter.parse(event, stdinText);
|
|
520
|
+
if (parsed.kind === "unreadable") return adapter.deny(event, UNREADABLE_VERDICT);
|
|
521
|
+
if (parsed.kind === "other") return adapter.allow(event);
|
|
522
|
+
const agent = adapter.name;
|
|
523
|
+
const session = parsed.sessionId !== void 0 && parsed.sessionId !== "" ? readSession(agent, parsed.sessionId) : null;
|
|
524
|
+
const cwd = session?.cwd ?? (parsed.cwd !== void 0 && parsed.cwd !== "" ? parsed.cwd : void 0);
|
|
525
|
+
refreshWhenDue(agent, cwd);
|
|
526
|
+
const resolution = session !== null ? readResolutionOf(agent, session.folder) : cwd !== void 0 ? readResolution(agent, cwd) : null;
|
|
527
|
+
const same2 = adapter.matches ?? ((a, b) => a === b);
|
|
528
|
+
const held = parsed.plugin ? [] : (resolution?.servers ?? []).filter((r) => same2(parsed.key, r.key));
|
|
529
|
+
const resolved = held.length === 1 ? held[0] : null;
|
|
530
|
+
const cached = readPolicy(agent);
|
|
531
|
+
const verdict = decide(cached?.policy ?? null, cached?.ageMs ?? 0, resolved, parsed.shown ?? parsed.key);
|
|
532
|
+
const tool = resolved && { source: resolved.source, name: resolved.name, version: resolved.version };
|
|
533
|
+
if (!verdict.allow) {
|
|
534
|
+
if (tool) record(adapter, "blocked", verdict.rule, tool);
|
|
535
|
+
return adapter.deny(event, verdict);
|
|
536
|
+
}
|
|
537
|
+
if (verdict.warn && tool) record(adapter, "warned", verdict.warn.rule, tool);
|
|
538
|
+
return adapter.allow(event);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/denies.ts
|
|
542
|
+
var import_node_fs3 = require("node:fs");
|
|
543
|
+
var import_node_path3 = require("node:path");
|
|
544
|
+
function isObject2(value) {
|
|
545
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
|
+
}
|
|
547
|
+
function denyOf(r) {
|
|
548
|
+
const launch = r.launch;
|
|
549
|
+
if (!isObject2(launch)) return null;
|
|
550
|
+
const command = launch.command;
|
|
551
|
+
if (Array.isArray(command) && command.length > 0 && command.every((c) => typeof c === "string")) return { serverCommand: command };
|
|
552
|
+
if (typeof launch.url === "string" && launch.url !== "") return { serverUrl: launch.url };
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
function blockedForms(resolved, policy) {
|
|
556
|
+
const forms = resolved.filter((r) => entryFor(policy, r)?.decision === "block").map(denyOf).filter((e) => e !== null);
|
|
557
|
+
return [...new Set(forms.map((e) => JSON.stringify(e)))];
|
|
558
|
+
}
|
|
559
|
+
function indentOf(text) {
|
|
560
|
+
const first = /\n([ \t]+)\S/.exec(text);
|
|
561
|
+
if (first === null) return 2;
|
|
562
|
+
return first[1].startsWith(" ") ? " " : Math.min(first[1].length, 8);
|
|
563
|
+
}
|
|
564
|
+
function bytesOf(file) {
|
|
565
|
+
try {
|
|
566
|
+
return (0, import_node_fs3.readFileSync)(file);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (error.code === "ENOENT") return null;
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function same(a, b) {
|
|
573
|
+
return a === null || b === null ? a === b : a.equals(b);
|
|
574
|
+
}
|
|
575
|
+
var beforeRename = null;
|
|
576
|
+
function writeBeside(file, data, mode, read) {
|
|
577
|
+
const temporary = `${file}.${process.pid}.attestari.tmp`;
|
|
578
|
+
try {
|
|
579
|
+
(0, import_node_fs3.writeFileSync)(temporary, data, mode === void 0 ? {} : { mode });
|
|
580
|
+
if (mode !== void 0) {
|
|
581
|
+
try {
|
|
582
|
+
(0, import_node_fs3.chmodSync)(temporary, mode);
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
beforeRename?.();
|
|
587
|
+
if (!same(bytesOf(file), read)) {
|
|
588
|
+
(0, import_node_fs3.unlinkSync)(temporary);
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
(0, import_node_fs3.renameSync)(temporary, file);
|
|
592
|
+
return true;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
try {
|
|
595
|
+
(0, import_node_fs3.unlinkSync)(temporary);
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function formatLike(text, value) {
|
|
602
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
603
|
+
const crlf = text !== null && text.includes("\r\n");
|
|
604
|
+
const newline = text === null || /\n$/.test(text);
|
|
605
|
+
let out = JSON.stringify(value, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
606
|
+
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
607
|
+
if (bom) out = `\uFEFF${out}`;
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
function modeOf(file, text) {
|
|
611
|
+
try {
|
|
612
|
+
return text === null ? void 0 : (0, import_node_fs3.statSync)(file).mode & 511;
|
|
613
|
+
} catch {
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function say2(line) {
|
|
618
|
+
process.stderr.write(`attestari-hook: ${line}
|
|
619
|
+
`);
|
|
620
|
+
}
|
|
621
|
+
function writeDenies(agent, settingsPath, resolved, policy, written, takeBack = true, elsewhere = /* @__PURE__ */ new Set()) {
|
|
622
|
+
void agent;
|
|
623
|
+
const wanted = blockedForms(resolved, policy);
|
|
624
|
+
let read;
|
|
625
|
+
try {
|
|
626
|
+
read = bytesOf(settingsPath);
|
|
627
|
+
} catch {
|
|
628
|
+
say2(`${settingsPath} could not be read; its deny entries are left as they were`);
|
|
629
|
+
return written;
|
|
630
|
+
}
|
|
631
|
+
const text = read === null ? null : read.toString("utf8");
|
|
632
|
+
if (text === null) {
|
|
633
|
+
if (wanted.length === 0) return [];
|
|
634
|
+
if (!(0, import_node_fs3.existsSync)((0, import_node_path3.dirname)(settingsPath))) {
|
|
635
|
+
say2(`${(0, import_node_path3.dirname)(settingsPath)} does not exist; no deny entry is written`);
|
|
636
|
+
return written;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
640
|
+
let settings;
|
|
641
|
+
try {
|
|
642
|
+
settings = text === null ? {} : JSON.parse(bom ? text.slice(1) : text);
|
|
643
|
+
} catch {
|
|
644
|
+
settings = void 0;
|
|
645
|
+
}
|
|
646
|
+
if (!isObject2(settings) || settings.deniedMcpServers !== void 0 && !Array.isArray(settings.deniedMcpServers)) {
|
|
647
|
+
say2(`${settingsPath} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
648
|
+
return written;
|
|
649
|
+
}
|
|
650
|
+
const existing = settings.deniedMcpServers ?? [];
|
|
651
|
+
const mine = new Set(written);
|
|
652
|
+
const want = new Set(wanted);
|
|
653
|
+
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
654
|
+
const kept = existing.filter((e) => {
|
|
655
|
+
const form = JSON.stringify(e);
|
|
656
|
+
return !(takeBack && mine.has(form) && !applies(form));
|
|
657
|
+
});
|
|
658
|
+
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
659
|
+
const added = wanted.filter((form) => !present.has(form));
|
|
660
|
+
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
661
|
+
if (added.length === 0 && kept.length === existing.length) return now;
|
|
662
|
+
const next = { ...settings, deniedMcpServers: [...kept, ...added.map((form) => JSON.parse(form))] };
|
|
663
|
+
const crlf = text !== null && text.includes("\r\n");
|
|
664
|
+
const newline = text === null || /\n$/.test(text);
|
|
665
|
+
let out = JSON.stringify(next, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
666
|
+
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
667
|
+
if (bom) out = `\uFEFF${out}`;
|
|
668
|
+
let mode;
|
|
669
|
+
try {
|
|
670
|
+
mode = text === null ? void 0 : (0, import_node_fs3.statSync)(settingsPath).mode & 511;
|
|
671
|
+
} catch {
|
|
672
|
+
mode = void 0;
|
|
673
|
+
}
|
|
674
|
+
if (!writeBeside(settingsPath, out, mode, read)) {
|
|
675
|
+
say2(`${settingsPath} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
676
|
+
return written;
|
|
677
|
+
}
|
|
678
|
+
return now;
|
|
679
|
+
}
|
|
680
|
+
function writeDenyList(file, get, put, wanted, written, takeBack = true, elsewhere = /* @__PURE__ */ new Set()) {
|
|
681
|
+
let read;
|
|
682
|
+
try {
|
|
683
|
+
read = bytesOf(file);
|
|
684
|
+
} catch {
|
|
685
|
+
say2(`${file} could not be read; its deny entries are left as they were`);
|
|
686
|
+
return written;
|
|
687
|
+
}
|
|
688
|
+
const text = read === null ? null : read.toString("utf8");
|
|
689
|
+
if (text === null) {
|
|
690
|
+
if (wanted.length === 0) return [];
|
|
691
|
+
if (!(0, import_node_fs3.existsSync)((0, import_node_path3.dirname)(file))) {
|
|
692
|
+
say2(`${(0, import_node_path3.dirname)(file)} does not exist; no deny entry is written`);
|
|
693
|
+
return written;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
697
|
+
let settings;
|
|
698
|
+
try {
|
|
699
|
+
settings = text === null ? {} : JSON.parse(bom ? text.slice(1) : text);
|
|
700
|
+
} catch {
|
|
701
|
+
settings = void 0;
|
|
702
|
+
}
|
|
703
|
+
const existing = isObject2(settings) ? get(settings) : null;
|
|
704
|
+
if (!isObject2(settings) || existing === null) {
|
|
705
|
+
say2(`${file} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
706
|
+
return written;
|
|
707
|
+
}
|
|
708
|
+
const current = existing ?? [];
|
|
709
|
+
const mine = new Set(written);
|
|
710
|
+
const want = new Set(wanted);
|
|
711
|
+
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
712
|
+
const kept = current.filter((e) => {
|
|
713
|
+
const form = JSON.stringify(e);
|
|
714
|
+
return !(takeBack && mine.has(form) && !applies(form));
|
|
715
|
+
});
|
|
716
|
+
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
717
|
+
const added = wanted.filter((form) => !present.has(form));
|
|
718
|
+
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
719
|
+
if (added.length === 0 && kept.length === current.length) return now;
|
|
720
|
+
const next = put(settings, [...kept, ...added.map((form) => JSON.parse(form))]);
|
|
721
|
+
const out = formatLike(text, next);
|
|
722
|
+
if (!writeBeside(file, bom ? `\uFEFF${out}` : out, modeOf(file, text), read)) {
|
|
723
|
+
say2(`${file} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
724
|
+
return written;
|
|
725
|
+
}
|
|
726
|
+
return now;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/plugin.ts
|
|
730
|
+
var import_node_child_process2 = require("node:child_process");
|
|
731
|
+
var import_node_fs4 = require("node:fs");
|
|
732
|
+
var import_node_path4 = require("node:path");
|
|
733
|
+
var DEFAULT_MARKETPLACE = "https://attestari.ai/plugins/marketplace.json";
|
|
734
|
+
var PLUGIN = "attestari@attestari";
|
|
735
|
+
var TIMEOUT_MS = 6e4;
|
|
736
|
+
var WINDOWS_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
737
|
+
var CMD_UNSAFE = /["%^&|<>!\r\n]/;
|
|
738
|
+
function marketplace() {
|
|
739
|
+
return process.env.ATTESTARI_MARKETPLACE || DEFAULT_MARKETPLACE;
|
|
740
|
+
}
|
|
741
|
+
function steps(market) {
|
|
742
|
+
return [
|
|
743
|
+
["plugin", "marketplace", "add", market],
|
|
744
|
+
["plugin", "install", PLUGIN, "--scope", "user"]
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
function runnable(file, windows) {
|
|
748
|
+
try {
|
|
749
|
+
if (!(0, import_node_fs4.statSync)(file).isFile()) return false;
|
|
750
|
+
if (!windows) (0, import_node_fs4.accessSync)(file, import_node_fs4.constants.X_OK);
|
|
751
|
+
return true;
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function findOnPath(name, path = process.env.PATH ?? "", windows = process.platform === "win32") {
|
|
757
|
+
const named2 = (process.env.PATHEXT ?? "").split(";").map((e) => e.trim().toLowerCase()).filter((e) => WINDOWS_EXTENSIONS.includes(e));
|
|
758
|
+
const extensions = named2.length > 0 ? named2 : WINDOWS_EXTENSIONS;
|
|
759
|
+
for (const folder of path.split(import_node_path4.delimiter)) {
|
|
760
|
+
if (folder.trim() === "") continue;
|
|
761
|
+
const candidates = windows ? extensions.map((e) => (0, import_node_path4.join)(folder, `${name}${e}`)) : [(0, import_node_path4.join)(folder, name)];
|
|
762
|
+
for (const candidate of candidates) if (runnable(candidate, windows)) return candidate;
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
function run(file, args) {
|
|
767
|
+
const options = { timeout: TIMEOUT_MS, stdio: "pipe", windowsHide: true };
|
|
768
|
+
let result;
|
|
769
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(file)) {
|
|
770
|
+
const all = [file, ...args];
|
|
771
|
+
if (all.some((a) => CMD_UNSAFE.test(a))) return "a character cmd.exe would read";
|
|
772
|
+
const line = all.map((a) => `"${a}"`).join(" ");
|
|
773
|
+
result = (0, import_node_child_process2.spawnSync)(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${line}"`], { ...options, windowsVerbatimArguments: true });
|
|
774
|
+
} else {
|
|
775
|
+
result = (0, import_node_child_process2.spawnSync)(file, args, options);
|
|
776
|
+
}
|
|
777
|
+
const code = result.error?.code;
|
|
778
|
+
if (code === "ETIMEDOUT") return "timed out";
|
|
779
|
+
if (result.error) return code ?? "did not run";
|
|
780
|
+
if (result.status === 0) return null;
|
|
781
|
+
return result.status !== null ? `exit ${result.status}` : result.signal ?? "did not run";
|
|
782
|
+
}
|
|
783
|
+
function installPlugin() {
|
|
784
|
+
const claude = findOnPath("claude");
|
|
785
|
+
if (claude === null) return { installed: false, why: "Claude Code (claude) is not on the PATH" };
|
|
786
|
+
for (const args of steps(marketplace())) {
|
|
787
|
+
const failed2 = run(claude, args);
|
|
788
|
+
if (failed2 !== null) return { installed: false, why: `\`claude ${args.slice(0, args[1] === "marketplace" ? 3 : 2).join(" ")}\` failed (${failed2})` };
|
|
789
|
+
}
|
|
790
|
+
return { installed: true };
|
|
791
|
+
}
|
|
792
|
+
function installCommands() {
|
|
793
|
+
const market = marketplace().replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
794
|
+
const shown = /\s/.test(market) ? `"${market}"` : market;
|
|
795
|
+
return steps(shown).map((args) => ` claude ${args.join(" ")}
|
|
796
|
+
`).join("");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/resolve.ts
|
|
800
|
+
var import_node_crypto3 = require("node:crypto");
|
|
801
|
+
var import_node_fs5 = require("node:fs");
|
|
802
|
+
var import_node_path5 = require("node:path");
|
|
803
|
+
var SERVERS_AT_MOST = 200;
|
|
804
|
+
var NPM_NAME = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/;
|
|
805
|
+
var PYPI_NAME = /^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$/;
|
|
806
|
+
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
807
|
+
var SOURCES = /* @__PURE__ */ new Set(["npm", "pypi", "local", "remote"]);
|
|
808
|
+
var NPM_EXACT = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
|
|
809
|
+
var PYPI_EXACT = /^[0-9][0-9A-Za-z.+!-]*$/;
|
|
810
|
+
function isStrings(value) {
|
|
811
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
812
|
+
}
|
|
813
|
+
function runner(command) {
|
|
814
|
+
const file = command.split(/[\\/]/).pop() ?? command;
|
|
815
|
+
return file.toLowerCase().replace(/\.(cmd|exe|bat|ps1)$/, "");
|
|
816
|
+
}
|
|
817
|
+
var NPM_WANTED = /^[0-9A-Za-z.^~<>=|*+ -]*$/;
|
|
818
|
+
function npmSpec(spec) {
|
|
819
|
+
const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
|
|
820
|
+
const name = at > 0 ? spec.slice(0, at) : spec;
|
|
821
|
+
const wanted = at > 0 ? spec.slice(at + 1) : "";
|
|
822
|
+
if (!NPM_NAME.test(name) || name.length > 214 || !NPM_WANTED.test(wanted)) return null;
|
|
823
|
+
const exact = NPM_EXACT.exec(wanted);
|
|
824
|
+
return { source: "npm", name, version: exact ? exact[1] : null };
|
|
825
|
+
}
|
|
826
|
+
var RUNNER_NO_VALUE = /* @__PURE__ */ new Set(["-y", "--yes", "-q", "--quiet"]);
|
|
827
|
+
function npmRunner(args) {
|
|
828
|
+
const packages = [];
|
|
829
|
+
let i = 0;
|
|
830
|
+
for (; i < args.length; i++) {
|
|
831
|
+
const a = args[i];
|
|
832
|
+
if (a === "--") {
|
|
833
|
+
i++;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
if (a === "-p" || a === "--package") {
|
|
837
|
+
if (i + 1 >= args.length) return null;
|
|
838
|
+
packages.push(args[++i]);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (a.startsWith("--package=")) {
|
|
842
|
+
packages.push(a.slice("--package=".length));
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (RUNNER_NO_VALUE.has(a)) continue;
|
|
846
|
+
if (a.startsWith("-")) return null;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
if (packages.length > 1) return null;
|
|
850
|
+
const spec = packages[0] ?? args[i];
|
|
851
|
+
return spec === void 0 ? null : npmSpec(spec);
|
|
852
|
+
}
|
|
853
|
+
function pypiSpec(spec) {
|
|
854
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?(.*)$/.exec(spec.trim());
|
|
855
|
+
if (match === null || !PYPI_NAME.test(match[1]) || match[1].length > 214) return null;
|
|
856
|
+
const rest = match[2].trim();
|
|
857
|
+
if (/[:/]/.test(rest)) return null;
|
|
858
|
+
const exact = /^(?:==|@)\s*([^\s,;<>=!~*@]+)$/.exec(rest);
|
|
859
|
+
if (rest !== "" && exact === null && !/^[<>=!~]/.test(rest)) return null;
|
|
860
|
+
return { source: "pypi", name: match[1], version: exact && PYPI_EXACT.test(exact[1]) ? exact[1] : null };
|
|
861
|
+
}
|
|
862
|
+
function pypiRunner(args, from) {
|
|
863
|
+
let chosen = null;
|
|
864
|
+
let i = 0;
|
|
865
|
+
for (; i < args.length; i++) {
|
|
866
|
+
const a = args[i];
|
|
867
|
+
if (a === from || a.startsWith(`${from}=`)) {
|
|
868
|
+
const value = a === from ? args[++i] : a.slice(from.length + 1);
|
|
869
|
+
if (value === void 0 || chosen !== null) return null;
|
|
870
|
+
chosen = value;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (RUNNER_NO_VALUE.has(a)) continue;
|
|
874
|
+
if (from === "--from" && (a === "--python" || a === "-p")) {
|
|
875
|
+
if (i + 1 >= args.length) return null;
|
|
876
|
+
i++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (from === "--from" && a.startsWith("--python=")) continue;
|
|
880
|
+
if (a.startsWith("-")) return null;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
const spec = chosen ?? args[i];
|
|
884
|
+
return spec === void 0 ? null : pypiSpec(spec);
|
|
885
|
+
}
|
|
886
|
+
function unwrapped(command, args) {
|
|
887
|
+
if (runner(command) === "cmd" && args.length >= 2 && /^\/[ck]$/i.test(args[0])) return unwrapped(args[1], args.slice(2));
|
|
888
|
+
return [command, args];
|
|
889
|
+
}
|
|
890
|
+
function registryRunner(configuredCommand, configuredArgs) {
|
|
891
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
892
|
+
const run3 = runner(command);
|
|
893
|
+
if (run3 === "npx" || run3 === "bunx" || run3 === "pnpx" || run3 === "uvx") return true;
|
|
894
|
+
if ((run3 === "pnpm" || run3 === "yarn") && args[0] === "dlx") return true;
|
|
895
|
+
return run3 === "pipx" && args[0] === "run";
|
|
896
|
+
}
|
|
897
|
+
function packageOf(configuredCommand, configuredArgs) {
|
|
898
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
899
|
+
const run3 = runner(command);
|
|
900
|
+
if (run3 === "npx" || run3 === "bunx" || run3 === "pnpx") return npmRunner(args);
|
|
901
|
+
if ((run3 === "pnpm" || run3 === "yarn") && args[0] === "dlx") return npmRunner(args.slice(1));
|
|
902
|
+
if (run3 === "uvx") return pypiRunner(args, "--from");
|
|
903
|
+
if (run3 === "pipx" && args[0] === "run") return pypiRunner(args.slice(1), "--spec");
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
function secretLike(segment) {
|
|
907
|
+
return /[%@=+]/.test(segment) || segment.length >= 16 && /[0-9]/.test(segment) && /[A-Za-z]/.test(segment);
|
|
908
|
+
}
|
|
909
|
+
function remoteName(address) {
|
|
910
|
+
let url2;
|
|
911
|
+
try {
|
|
912
|
+
url2 = new URL(address.trim());
|
|
913
|
+
} catch {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
if (!["http:", "https:", "ws:", "wss:"].includes(url2.protocol) || url2.hostname === "") return null;
|
|
917
|
+
const port = url2.port === "" ? "" : `:${url2.port}`;
|
|
918
|
+
const path = url2.pathname.replace(/\/+$/, "").split("/").map((segment) => secretLike(segment) ? "*" : segment).join("/");
|
|
919
|
+
return `${url2.hostname.toLowerCase()}${port}${path}`;
|
|
920
|
+
}
|
|
921
|
+
function pathLike(arg) {
|
|
922
|
+
if (arg.startsWith("-") || /^\/[A-Za-z?]$/.test(arg)) return false;
|
|
923
|
+
if (/[=?#@]/.test(arg) || arg.replace(/^[A-Za-z]:(?=[\\/])/, "").includes(":")) return false;
|
|
924
|
+
return /[\\/]/.test(arg) || arg.startsWith("~") || /\.(?:[cm]?[jt]s|py)$/i.test(arg);
|
|
925
|
+
}
|
|
926
|
+
var FILE_NAME = /^[A-Za-z0-9._-]{1,200}$/;
|
|
927
|
+
function fileName(file) {
|
|
928
|
+
if (!FILE_NAME.test(file) || secretLike(file)) return false;
|
|
929
|
+
return !(file.length >= 16 && !file.includes(".") && /[a-z]/.test(file) && /[A-Z]/.test(file));
|
|
930
|
+
}
|
|
931
|
+
function fileAndFolder(path, home2) {
|
|
932
|
+
const expanded = path === "~" || path.startsWith("~/") || path.startsWith("~\\") ? `${home2}${path.slice(1)}` : path;
|
|
933
|
+
const parts = expanded.split(/[\\/]/);
|
|
934
|
+
const file = parts.pop() ?? "";
|
|
935
|
+
return { file, folder: parts.join("/") };
|
|
936
|
+
}
|
|
937
|
+
function labelOf(folder, home2) {
|
|
938
|
+
const slashed = folder.replace(/\\/g, "/");
|
|
939
|
+
const root = home2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
940
|
+
const fold = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
941
|
+
if (root !== "" && (fold(slashed) === fold(root) || fold(slashed).startsWith(`${fold(root)}/`))) return `~${slashed.slice(root.length)}`;
|
|
942
|
+
return slashed;
|
|
943
|
+
}
|
|
944
|
+
function localName(configuredCommand, configuredArgs, home2, secret) {
|
|
945
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
946
|
+
const path = args.find(pathLike);
|
|
947
|
+
const fromArg = path === void 0 ? null : fileAndFolder(path, home2);
|
|
948
|
+
const chosen = fromArg !== null && fileName(fromArg.file) ? fromArg : fileAndFolder(command, home2);
|
|
949
|
+
const file = fileName(chosen.file) ? chosen.file : "local";
|
|
950
|
+
const hash = (0, import_node_crypto3.createHmac)("sha256", secret).update(labelOf(chosen.folder, home2), "utf8").digest("hex").slice(0, 12);
|
|
951
|
+
return `${file}#${hash}`;
|
|
952
|
+
}
|
|
953
|
+
function resolve(key, entry, home2, secret) {
|
|
954
|
+
const type = entry.type;
|
|
955
|
+
const command = entry.command;
|
|
956
|
+
const remote = type === "http" || type === "sse" || type === "ws" || type === void 0 && typeof command !== "string";
|
|
957
|
+
if (remote) {
|
|
958
|
+
const url2 = entry.url;
|
|
959
|
+
if (typeof url2 !== "string") return null;
|
|
960
|
+
const name = remoteName(url2);
|
|
961
|
+
if (name === null) return null;
|
|
962
|
+
const launch2 = { url: url2 };
|
|
963
|
+
return { key, source: "remote", name, version: null, local: url2, launch: launch2 };
|
|
964
|
+
}
|
|
965
|
+
if (type !== void 0 && type !== "stdio") return null;
|
|
966
|
+
const args = entry.args === void 0 ? [] : entry.args;
|
|
967
|
+
if (typeof command !== "string" || command === "" || !isStrings(args)) return null;
|
|
968
|
+
const launch = { command: [command, ...args] };
|
|
969
|
+
const local = [command, ...args].join(" ");
|
|
970
|
+
const found = packageOf(command, args);
|
|
971
|
+
if (found !== null) return { key, ...found, local, launch };
|
|
972
|
+
if (registryRunner(command, args)) return null;
|
|
973
|
+
return { key, source: "local", name: localName(command, args, home2, secret), version: null, local, launch };
|
|
974
|
+
}
|
|
975
|
+
function oneLineName(value) {
|
|
976
|
+
return value.length >= 1 && value.length <= 214 && !/\s/.test(value) && !CONTROL.test(value);
|
|
977
|
+
}
|
|
978
|
+
function reportable(r) {
|
|
979
|
+
if (!SOURCES.has(r.source) || !oneLineName(r.name)) return false;
|
|
980
|
+
if (r.version !== null && (r.version.length < 1 || r.version.length > 64 || /\s/.test(r.version) || CONTROL.test(r.version))) return false;
|
|
981
|
+
if (r.source === "remote" && (/[?#@]/.test(r.name) || r.name.includes("://"))) return false;
|
|
982
|
+
if (r.source === "npm" && !NPM_NAME.test(r.name)) return false;
|
|
983
|
+
if (r.source === "pypi" && !PYPI_NAME.test(r.name)) return false;
|
|
984
|
+
if (r.source === "local" && !LOCAL_NAME.test(r.name)) return false;
|
|
985
|
+
return reducedKey(r.key) !== "";
|
|
986
|
+
}
|
|
987
|
+
var LOCAL_NAME = /^[A-Za-z0-9._-]{1,200}#[0-9a-f]{12}$/;
|
|
988
|
+
function byKey(a, b) {
|
|
989
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
990
|
+
}
|
|
991
|
+
function finished(servers) {
|
|
992
|
+
const counts = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const s of servers) counts.set(s.key, (counts.get(s.key) ?? 0) + 1);
|
|
994
|
+
return servers.filter((s) => counts.get(s.key) === 1).sort((a, b) => byKey(a.key, b.key)).slice(0, SERVERS_AT_MOST);
|
|
995
|
+
}
|
|
996
|
+
var sent = (r, key) => ({ key, source: r.source, name: r.name, version: r.version });
|
|
997
|
+
function reportedAll(folders) {
|
|
998
|
+
const byReduced = /* @__PURE__ */ new Map();
|
|
999
|
+
for (const { folder, servers } of folders) {
|
|
1000
|
+
for (const r of servers.filter(reportable)) {
|
|
1001
|
+
const key = reducedKey(r.key);
|
|
1002
|
+
byReduced.set(key, [...byReduced.get(key) ?? [], { folder, r }]);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const out = [];
|
|
1006
|
+
for (const [key, held] of byReduced) {
|
|
1007
|
+
const [first] = held;
|
|
1008
|
+
const same2 = held.every(({ r }) => r.source === first.r.source && r.name === first.r.name && r.version === first.r.version);
|
|
1009
|
+
if (same2) out.push(sent(first.r, key));
|
|
1010
|
+
else for (const { folder, r } of held) out.push(sent(r, suffixedKey(key, folder)));
|
|
1011
|
+
}
|
|
1012
|
+
return finished(out);
|
|
1013
|
+
}
|
|
1014
|
+
function isObject3(value) {
|
|
1015
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1016
|
+
}
|
|
1017
|
+
function readJson(file) {
|
|
1018
|
+
let text;
|
|
1019
|
+
try {
|
|
1020
|
+
text = (0, import_node_fs5.readFileSync)(file, "utf8");
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const value = JSON.parse(text.replace(/^\ufeff/, ""));
|
|
1026
|
+
return isObject3(value) ? value : null;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function serversOf(holder) {
|
|
1032
|
+
if (holder === null) return null;
|
|
1033
|
+
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
1034
|
+
return isObject3(holder.mcpServers) ? holder.mcpServers : null;
|
|
1035
|
+
}
|
|
1036
|
+
function claudeCodeScopes(home2, cwd) {
|
|
1037
|
+
const claude = readJson((0, import_node_path5.join)(home2, ".claude.json"));
|
|
1038
|
+
let project;
|
|
1039
|
+
if (claude === null) project = null;
|
|
1040
|
+
else if (claude !== void 0 && claude.projects !== void 0) {
|
|
1041
|
+
const projects = claude.projects;
|
|
1042
|
+
if (!isObject3(projects)) project = null;
|
|
1043
|
+
else {
|
|
1044
|
+
const trimmed = cwd.replace(/[\\/]+$/, "");
|
|
1045
|
+
const forms = [cwd, cwd.replace(/\\/g, "/"), ...trimmed === "" ? [] : [trimmed, trimmed.replace(/\\/g, "/")]];
|
|
1046
|
+
const held = forms.map((form) => projects[form]).find((value) => value !== void 0);
|
|
1047
|
+
project = held === void 0 ? void 0 : isObject3(held) ? held : null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return [
|
|
1051
|
+
{ label: "~/.claude.json (this project)", servers: serversOf(project) },
|
|
1052
|
+
{ label: ".mcp.json", servers: serversOf(readJson((0, import_node_path5.join)(cwd, ".mcp.json"))) },
|
|
1053
|
+
{ label: "~/.claude.json", servers: serversOf(claude) }
|
|
1054
|
+
];
|
|
1055
|
+
}
|
|
1056
|
+
function unnameable(entry) {
|
|
1057
|
+
if (!isObject3(entry) || entry.type !== void 0 && entry.type !== "stdio") return false;
|
|
1058
|
+
const args = entry.args === void 0 ? [] : entry.args;
|
|
1059
|
+
return typeof entry.command === "string" && entry.command !== "" && isStrings(args) && registryRunner(entry.command, args);
|
|
1060
|
+
}
|
|
1061
|
+
function resolveConfiguration(home2, cwd, secret, scopes4 = claudeCodeScopes(home2, cwd)) {
|
|
1062
|
+
const notes = [];
|
|
1063
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1064
|
+
let complete = true;
|
|
1065
|
+
for (const scope of scopes4) {
|
|
1066
|
+
if (scope.servers === null) {
|
|
1067
|
+
notes.push(`${scope.label} could not be read: its servers, and those of every scope below it, are not checked`);
|
|
1068
|
+
complete = false;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
for (const [key, entry] of Object.entries(scope.servers ?? {})) if (!entries.has(key)) entries.set(key, entry);
|
|
1072
|
+
}
|
|
1073
|
+
const resolved = [];
|
|
1074
|
+
for (const [key, entry] of entries) {
|
|
1075
|
+
const shown = reducedKey(key) || "(empty)";
|
|
1076
|
+
if (key.includes("__") || reducedKey(key) === "") {
|
|
1077
|
+
notes.push(`the server "${shown}" is not checked: its key cannot name one server`);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const r = isObject3(entry) ? resolve(key, entry, home2, secret) : null;
|
|
1081
|
+
if (r === null && unnameable(entry)) notes.push(`the server is not checked: its package could not be named: ${shown}`);
|
|
1082
|
+
else if (r === null) notes.push(`the server "${shown}" is not checked: neither a command nor an address`);
|
|
1083
|
+
else if (!reportable(r)) notes.push(`the server "${shown}" is not checked: its name is not one the device API takes`);
|
|
1084
|
+
else resolved.push(r);
|
|
1085
|
+
}
|
|
1086
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1087
|
+
for (const r of resolved) counts.set(reducedKey(r.key), (counts.get(reducedKey(r.key)) ?? 0) + 1);
|
|
1088
|
+
const unique = resolved.filter((r) => {
|
|
1089
|
+
if (counts.get(reducedKey(r.key)) === 1) return true;
|
|
1090
|
+
notes.push(`the server "${reducedKey(r.key)}" is not checked: two keys reduce to the same string`);
|
|
1091
|
+
return false;
|
|
1092
|
+
});
|
|
1093
|
+
unique.sort((a, b) => byKey(reducedKey(a.key), reducedKey(b.key)));
|
|
1094
|
+
if (unique.length > SERVERS_AT_MOST) {
|
|
1095
|
+
notes.push(`${unique.length} servers: the first ${SERVERS_AT_MOST} by key are checked, the rest are not`);
|
|
1096
|
+
}
|
|
1097
|
+
return { resolved: unique.slice(0, SERVERS_AT_MOST), notes, complete };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/adapters/claude-code.ts
|
|
1101
|
+
var PREFIX = "mcp__";
|
|
1102
|
+
function serverKeyOf(toolName) {
|
|
1103
|
+
if (!toolName.startsWith(PREFIX)) return null;
|
|
1104
|
+
const rest = toolName.slice(PREFIX.length);
|
|
1105
|
+
const end = rest.indexOf("__");
|
|
1106
|
+
if (end <= 0 || end + 2 >= rest.length) return null;
|
|
1107
|
+
return rest.slice(0, end);
|
|
1108
|
+
}
|
|
1109
|
+
function isObject4(value) {
|
|
1110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1111
|
+
}
|
|
1112
|
+
function parse(_event, stdinText) {
|
|
1113
|
+
let input;
|
|
1114
|
+
try {
|
|
1115
|
+
input = JSON.parse(stdinText);
|
|
1116
|
+
} catch {
|
|
1117
|
+
return { kind: "unreadable" };
|
|
1118
|
+
}
|
|
1119
|
+
if (!isObject4(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1120
|
+
if (typeof input.cursor_version === "string") return { kind: "other" };
|
|
1121
|
+
const server = isObject4(input.mcp_server) ? input.mcp_server : null;
|
|
1122
|
+
const parsed = serverKeyOf(input.tool_name);
|
|
1123
|
+
if (parsed === null) return server === null ? { kind: "other" } : { kind: "unreadable" };
|
|
1124
|
+
const key = typeof server?.name === "string" && server.name !== "" ? server.name : parsed;
|
|
1125
|
+
const plugin = server !== null ? server.source === "plugin" : input.tool_name.startsWith(`${PREFIX}plugin_`);
|
|
1126
|
+
return {
|
|
1127
|
+
kind: "mcp",
|
|
1128
|
+
key,
|
|
1129
|
+
plugin,
|
|
1130
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : void 0,
|
|
1131
|
+
cwd: typeof input.cwd === "string" ? input.cwd : void 0
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function deny2(_event, verdict) {
|
|
1135
|
+
const output = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: verdict.reason } };
|
|
1136
|
+
return { stdout: `${JSON.stringify(output)}
|
|
1137
|
+
`, exitCode: 0 };
|
|
1138
|
+
}
|
|
1139
|
+
var allow = () => ({ stdout: "", exitCode: 0 });
|
|
1140
|
+
var claudeCode = {
|
|
1141
|
+
name: "claude-code",
|
|
1142
|
+
client: "Claude Code",
|
|
1143
|
+
events: ["PreToolUse", "SessionStart"],
|
|
1144
|
+
parse,
|
|
1145
|
+
deny: deny2,
|
|
1146
|
+
allow,
|
|
1147
|
+
scopes: claudeCodeScopes,
|
|
1148
|
+
// Claude Code's own deny form: deniedMcpServers in ~/.claude/settings.json (denies.ts).
|
|
1149
|
+
writeDenies: (resolved, policy, written, takeBack, elsewhere) => writeDenies("claude-code", (0, import_node_path6.join)(home(), ".claude", "settings.json"), resolved, policy, written, takeBack, elsewhere),
|
|
1150
|
+
// Installed by plugin (D8), not by a file the hook writes.
|
|
1151
|
+
install: () => ({ commands: installCommands().split("\n").map((line) => line.trim()).filter((line) => line !== "") })
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// src/adapters/codex.ts
|
|
1155
|
+
var import_node_fs6 = require("node:fs");
|
|
1156
|
+
var import_node_path7 = require("node:path");
|
|
1157
|
+
|
|
1158
|
+
// src/toml.ts
|
|
1159
|
+
function isObject5(value) {
|
|
1160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1161
|
+
}
|
|
1162
|
+
var Fail = class extends Error {
|
|
1163
|
+
};
|
|
1164
|
+
function unescape(text) {
|
|
1165
|
+
return text.replace(/\\(u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|.)/g, (_m, e) => {
|
|
1166
|
+
switch (e[0]) {
|
|
1167
|
+
case "n":
|
|
1168
|
+
return "\n";
|
|
1169
|
+
case "t":
|
|
1170
|
+
return " ";
|
|
1171
|
+
case "r":
|
|
1172
|
+
return "\r";
|
|
1173
|
+
case "b":
|
|
1174
|
+
return "\b";
|
|
1175
|
+
case "f":
|
|
1176
|
+
return "\f";
|
|
1177
|
+
case '"':
|
|
1178
|
+
return '"';
|
|
1179
|
+
case "\\":
|
|
1180
|
+
return "\\";
|
|
1181
|
+
case "u":
|
|
1182
|
+
case "U":
|
|
1183
|
+
return String.fromCodePoint(parseInt(e.slice(1), 16));
|
|
1184
|
+
default:
|
|
1185
|
+
throw new Fail("escape");
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
var Reader = class {
|
|
1190
|
+
constructor(text) {
|
|
1191
|
+
this.text = text;
|
|
1192
|
+
}
|
|
1193
|
+
text;
|
|
1194
|
+
skipSpace(at) {
|
|
1195
|
+
while (at < this.text.length && (this.text[at] === " " || this.text[at] === " ")) at++;
|
|
1196
|
+
return at;
|
|
1197
|
+
}
|
|
1198
|
+
// One key segment: bare, or quoted (basic or literal).
|
|
1199
|
+
key(at) {
|
|
1200
|
+
const t = this.text;
|
|
1201
|
+
if (t[at] === '"') return this.basicString(at);
|
|
1202
|
+
if (t[at] === "'") return this.literalString(at);
|
|
1203
|
+
let end = at;
|
|
1204
|
+
while (end < t.length && /[A-Za-z0-9_-]/.test(t[end])) end++;
|
|
1205
|
+
if (end === at) throw new Fail("key");
|
|
1206
|
+
return [t.slice(at, end), end];
|
|
1207
|
+
}
|
|
1208
|
+
// A dotted key: segments separated by dots, spaces allowed around them.
|
|
1209
|
+
dottedKey(at) {
|
|
1210
|
+
const parts = [];
|
|
1211
|
+
let i = at;
|
|
1212
|
+
for (; ; ) {
|
|
1213
|
+
i = this.skipSpace(i);
|
|
1214
|
+
const [part, end] = this.key(i);
|
|
1215
|
+
parts.push(part);
|
|
1216
|
+
i = this.skipSpace(end);
|
|
1217
|
+
if (this.text[i] !== ".") return [parts, i];
|
|
1218
|
+
i++;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
basicString(at) {
|
|
1222
|
+
const t = this.text;
|
|
1223
|
+
if (t.startsWith('"""', at)) throw new Fail("multi-line");
|
|
1224
|
+
let i = at + 1;
|
|
1225
|
+
while (i < t.length && t[i] !== '"') {
|
|
1226
|
+
if (t[i] === "\n") throw new Fail("newline in string");
|
|
1227
|
+
i += t[i] === "\\" ? 2 : 1;
|
|
1228
|
+
}
|
|
1229
|
+
if (i >= t.length) throw new Fail("unterminated");
|
|
1230
|
+
return [unescape(t.slice(at + 1, i)), i + 1];
|
|
1231
|
+
}
|
|
1232
|
+
literalString(at) {
|
|
1233
|
+
const t = this.text;
|
|
1234
|
+
if (t.startsWith("'''", at)) throw new Fail("multi-line");
|
|
1235
|
+
const end = t.indexOf("'", at + 1);
|
|
1236
|
+
if (end === -1 || t.slice(at + 1, end).includes("\n")) throw new Fail("unterminated");
|
|
1237
|
+
return [t.slice(at + 1, end), end + 1];
|
|
1238
|
+
}
|
|
1239
|
+
value(at) {
|
|
1240
|
+
const t = this.text;
|
|
1241
|
+
const c = t[at];
|
|
1242
|
+
if (c === '"') return this.basicString(at);
|
|
1243
|
+
if (c === "'") return this.literalString(at);
|
|
1244
|
+
if (c === "[") return this.array(at);
|
|
1245
|
+
if (c === "{") return this.inlineTable(at);
|
|
1246
|
+
const word = /^(true|false|[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9]+)?)(?=[\s,\]}#]|$)/.exec(t.slice(at));
|
|
1247
|
+
if (word === null) throw new Fail("value");
|
|
1248
|
+
const w = word[1];
|
|
1249
|
+
const value = w === "true" ? true : w === "false" ? false : Number(w.replace(/_/g, ""));
|
|
1250
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new Fail("number");
|
|
1251
|
+
return [value, at + w.length];
|
|
1252
|
+
}
|
|
1253
|
+
// Whitespace, newlines and comments inside an array.
|
|
1254
|
+
skipInArray(at) {
|
|
1255
|
+
const t = this.text;
|
|
1256
|
+
for (; ; ) {
|
|
1257
|
+
while (at < t.length && /[ \t\r\n]/.test(t[at])) at++;
|
|
1258
|
+
if (t[at] === "#") {
|
|
1259
|
+
while (at < t.length && t[at] !== "\n") at++;
|
|
1260
|
+
} else return at;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
array(at) {
|
|
1264
|
+
const t = this.text;
|
|
1265
|
+
const out = [];
|
|
1266
|
+
let i = at + 1;
|
|
1267
|
+
for (; ; ) {
|
|
1268
|
+
i = this.skipInArray(i);
|
|
1269
|
+
if (i >= t.length) throw new Fail("unterminated array");
|
|
1270
|
+
if (t[i] === "]") return [out, i + 1];
|
|
1271
|
+
const [value, end] = this.value(i);
|
|
1272
|
+
out.push(value);
|
|
1273
|
+
i = this.skipInArray(end);
|
|
1274
|
+
if (t[i] === ",") i++;
|
|
1275
|
+
else if (t[i] !== "]") throw new Fail("array");
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
inlineTable(at) {
|
|
1279
|
+
const t = this.text;
|
|
1280
|
+
const out = {};
|
|
1281
|
+
let i = this.skipSpace(at + 1);
|
|
1282
|
+
if (t[i] === "}") return [out, i + 1];
|
|
1283
|
+
for (; ; ) {
|
|
1284
|
+
const [parts, afterKey] = this.dottedKey(i);
|
|
1285
|
+
if (t[afterKey] !== "=") throw new Fail("inline table");
|
|
1286
|
+
const [value, end] = this.value(this.skipSpace(afterKey + 1));
|
|
1287
|
+
assign(out, parts, value);
|
|
1288
|
+
i = this.skipSpace(end);
|
|
1289
|
+
if (t[i] === ",") i = this.skipSpace(i + 1);
|
|
1290
|
+
else if (t[i] === "}") return [out, i + 1];
|
|
1291
|
+
else throw new Fail("inline table");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
function assign(table, parts, value) {
|
|
1296
|
+
let here = table;
|
|
1297
|
+
for (const part of parts.slice(0, -1)) {
|
|
1298
|
+
const next = here[part];
|
|
1299
|
+
if (next === void 0) {
|
|
1300
|
+
const made = {};
|
|
1301
|
+
here[part] = made;
|
|
1302
|
+
here = made;
|
|
1303
|
+
} else if (isObject5(next)) here = next;
|
|
1304
|
+
else throw new Fail("key reused");
|
|
1305
|
+
}
|
|
1306
|
+
const last = parts[parts.length - 1];
|
|
1307
|
+
if (Object.hasOwn(here, last)) throw new Fail("key defined twice");
|
|
1308
|
+
here[last] = value;
|
|
1309
|
+
}
|
|
1310
|
+
function tableAt(root, parts) {
|
|
1311
|
+
let here = root;
|
|
1312
|
+
for (const part of parts) {
|
|
1313
|
+
const next = here[part];
|
|
1314
|
+
if (next === void 0) {
|
|
1315
|
+
const made = {};
|
|
1316
|
+
here[part] = made;
|
|
1317
|
+
here = made;
|
|
1318
|
+
} else if (isObject5(next)) here = next;
|
|
1319
|
+
else throw new Fail("table reused");
|
|
1320
|
+
}
|
|
1321
|
+
return here;
|
|
1322
|
+
}
|
|
1323
|
+
function parseToml(input) {
|
|
1324
|
+
const text = input.replace(/^/, "");
|
|
1325
|
+
try {
|
|
1326
|
+
const root = {};
|
|
1327
|
+
let current = root;
|
|
1328
|
+
const reader = new Reader(text);
|
|
1329
|
+
let pos = 0;
|
|
1330
|
+
while (pos < text.length) {
|
|
1331
|
+
let lineEnd = text.indexOf("\n", pos);
|
|
1332
|
+
if (lineEnd === -1) lineEnd = text.length;
|
|
1333
|
+
const line = text.slice(pos, lineEnd).replace(/\r$/, "");
|
|
1334
|
+
const trimmed = line.trim();
|
|
1335
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
1336
|
+
pos = lineEnd + 1;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (trimmed.startsWith("[[")) throw new Fail("array table");
|
|
1340
|
+
if (trimmed.startsWith("[")) {
|
|
1341
|
+
const close = trimmed.lastIndexOf("]");
|
|
1342
|
+
const rest = trimmed.slice(close + 1).trim();
|
|
1343
|
+
if (close === -1 || rest !== "" && !rest.startsWith("#")) throw new Fail("table header");
|
|
1344
|
+
const header = trimmed.slice(1, close);
|
|
1345
|
+
const inner = new Reader(header);
|
|
1346
|
+
const [parts2, end2] = inner.dottedKey(0);
|
|
1347
|
+
if (inner.skipSpace(end2) !== header.length) throw new Fail("table header");
|
|
1348
|
+
current = tableAt(root, parts2);
|
|
1349
|
+
pos = lineEnd + 1;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const [parts, afterKey] = reader.dottedKey(pos);
|
|
1353
|
+
if (text[afterKey] !== "=") throw new Fail("assignment");
|
|
1354
|
+
const [value, end] = reader.value(reader.skipSpace(afterKey + 1));
|
|
1355
|
+
let tailEnd = text.indexOf("\n", end);
|
|
1356
|
+
if (tailEnd === -1) tailEnd = text.length;
|
|
1357
|
+
const tail = text.slice(end, tailEnd).replace(/\r$/, "").trim();
|
|
1358
|
+
if (tail !== "" && !tail.startsWith("#")) throw new Fail("trailing");
|
|
1359
|
+
assign(current, parts, value);
|
|
1360
|
+
pos = tailEnd + 1;
|
|
1361
|
+
}
|
|
1362
|
+
return root;
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error instanceof Fail) return null;
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/adapters/codex.ts
|
|
1370
|
+
var PREFIX2 = "mcp__";
|
|
1371
|
+
var TIMEOUT_S = 10;
|
|
1372
|
+
var COPY = (0, import_node_path7.join)(".attestari", "hook.js");
|
|
1373
|
+
var STATUS = "Attestari is checking this MCP call";
|
|
1374
|
+
function isObject6(value) {
|
|
1375
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1376
|
+
}
|
|
1377
|
+
function sanitized(key) {
|
|
1378
|
+
return key.replace(/[^A-Za-z0-9_]/g, "_");
|
|
1379
|
+
}
|
|
1380
|
+
function parse2(_event, stdinText) {
|
|
1381
|
+
let input;
|
|
1382
|
+
try {
|
|
1383
|
+
input = JSON.parse(stdinText);
|
|
1384
|
+
} catch {
|
|
1385
|
+
return { kind: "unreadable" };
|
|
1386
|
+
}
|
|
1387
|
+
if (!isObject6(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1388
|
+
const name = input.tool_name;
|
|
1389
|
+
if (!name.startsWith(PREFIX2)) return { kind: "other" };
|
|
1390
|
+
const rest = name.slice(PREFIX2.length);
|
|
1391
|
+
if (rest === "" || !rest.includes("__")) return { kind: "unreadable" };
|
|
1392
|
+
return {
|
|
1393
|
+
kind: "mcp",
|
|
1394
|
+
key: rest,
|
|
1395
|
+
shown: rest.slice(0, rest.indexOf("__")),
|
|
1396
|
+
plugin: false,
|
|
1397
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : void 0,
|
|
1398
|
+
cwd: typeof input.cwd === "string" ? input.cwd : void 0
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function matches2(parsedKey, configuredKey) {
|
|
1402
|
+
const head = `${sanitized(configuredKey)}__`;
|
|
1403
|
+
return parsedKey.startsWith(head) && parsedKey.length > head.length;
|
|
1404
|
+
}
|
|
1405
|
+
function deny3(_event, verdict) {
|
|
1406
|
+
const output = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: verdict.reason } };
|
|
1407
|
+
return { stdout: `${JSON.stringify(output)}
|
|
1408
|
+
`, exitCode: 0 };
|
|
1409
|
+
}
|
|
1410
|
+
var allow2 = () => ({ stdout: "", exitCode: 0 });
|
|
1411
|
+
function serversOfToml(file) {
|
|
1412
|
+
let text;
|
|
1413
|
+
try {
|
|
1414
|
+
text = (0, import_node_fs6.readFileSync)(file, "utf8");
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
1417
|
+
}
|
|
1418
|
+
const table = parseToml(text);
|
|
1419
|
+
if (table === null) return null;
|
|
1420
|
+
const servers = table.mcp_servers;
|
|
1421
|
+
if (servers === void 0) return void 0;
|
|
1422
|
+
if (!isObject6(servers)) return null;
|
|
1423
|
+
const live = {};
|
|
1424
|
+
for (const [key, entry] of Object.entries(servers)) if (!(isObject6(entry) && entry.enabled === false)) live[key] = entry;
|
|
1425
|
+
return live;
|
|
1426
|
+
}
|
|
1427
|
+
function scopes(home2, cwd) {
|
|
1428
|
+
return [
|
|
1429
|
+
{ label: "~/.codex/managed_config.toml", servers: serversOfToml((0, import_node_path7.join)(home2, ".codex", "managed_config.toml")) },
|
|
1430
|
+
{ label: ".codex/config.toml", servers: serversOfToml((0, import_node_path7.join)(cwd, ".codex", "config.toml")) },
|
|
1431
|
+
{ label: "~/.codex/config.toml", servers: serversOfToml((0, import_node_path7.join)(home2, ".codex", "config.toml")) }
|
|
1432
|
+
];
|
|
1433
|
+
}
|
|
1434
|
+
function quoted(path) {
|
|
1435
|
+
return /[\s"]/.test(path) ? `"${path.replace(/"/g, '\\"')}"` : path;
|
|
1436
|
+
}
|
|
1437
|
+
function hookCommands(node, hook2) {
|
|
1438
|
+
return {
|
|
1439
|
+
preToolUse: `${quoted(node)} ${quoted(hook2)} codex pre-tool-use`,
|
|
1440
|
+
sessionStart: `${quoted(node)} ${quoted(hook2)} codex session-start`
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
var MINE = /attestari.*\bcodex (pre-tool-use|session-start)\b/;
|
|
1444
|
+
function isMine(handler) {
|
|
1445
|
+
return isObject6(handler) && typeof handler.command === "string" && MINE.test(handler.command);
|
|
1446
|
+
}
|
|
1447
|
+
function withHandler(groups, matcher, handler) {
|
|
1448
|
+
const current = Array.isArray(groups) ? groups : [];
|
|
1449
|
+
const cleaned = current.map((g) => isObject6(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !isMine(h)) } : g).filter((g) => !(isObject6(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
1450
|
+
const at = cleaned.findIndex((g) => isObject6(g) && g.matcher === matcher && Array.isArray(g.hooks));
|
|
1451
|
+
if (at === -1) return [...cleaned, matcher === void 0 ? { hooks: [handler] } : { matcher, hooks: [handler] }];
|
|
1452
|
+
const group = cleaned[at];
|
|
1453
|
+
cleaned[at] = { ...group, hooks: [...group.hooks, handler] };
|
|
1454
|
+
return cleaned;
|
|
1455
|
+
}
|
|
1456
|
+
function install(home2, hookFile = __filename, node = process.execPath) {
|
|
1457
|
+
const copy = (0, import_node_path7.join)(home2, COPY);
|
|
1458
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(home2, ".attestari"), { recursive: true });
|
|
1459
|
+
(0, import_node_fs6.copyFileSync)(hookFile, copy);
|
|
1460
|
+
const commands = hookCommands(node, copy);
|
|
1461
|
+
const file = (0, import_node_path7.join)(home2, ".codex", "hooks.json");
|
|
1462
|
+
const read = bytesOf(file);
|
|
1463
|
+
const text = read === null ? null : read.toString("utf8");
|
|
1464
|
+
let settings = {};
|
|
1465
|
+
if (text !== null) {
|
|
1466
|
+
try {
|
|
1467
|
+
settings = JSON.parse(text.replace(/^/, ""));
|
|
1468
|
+
} catch {
|
|
1469
|
+
throw new Error(`${file} could not be read as Codex's hooks file; the hook was not installed`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (!isObject6(settings) || settings.hooks !== void 0 && !isObject6(settings.hooks)) {
|
|
1473
|
+
throw new Error(`${file} is not Codex's hooks file; the hook was not installed`);
|
|
1474
|
+
}
|
|
1475
|
+
const hooks = isObject6(settings.hooks) ? settings.hooks : {};
|
|
1476
|
+
const next = {
|
|
1477
|
+
...settings,
|
|
1478
|
+
hooks: {
|
|
1479
|
+
...hooks,
|
|
1480
|
+
PreToolUse: withHandler(hooks.PreToolUse, "^mcp__", { type: "command", command: commands.preToolUse, timeout: TIMEOUT_S, statusMessage: STATUS }),
|
|
1481
|
+
SessionStart: withHandler(hooks.SessionStart, void 0, { type: "command", command: commands.sessionStart, timeout: TIMEOUT_S })
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
const after = formatLike(text, next);
|
|
1485
|
+
if (text !== after) {
|
|
1486
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(home2, ".codex"), { recursive: true });
|
|
1487
|
+
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1488
|
+
}
|
|
1489
|
+
return { path: file, before: text, after };
|
|
1490
|
+
}
|
|
1491
|
+
var codex = {
|
|
1492
|
+
name: "codex",
|
|
1493
|
+
client: "Codex",
|
|
1494
|
+
events: ["PreToolUse", "SessionStart"],
|
|
1495
|
+
parse: parse2,
|
|
1496
|
+
matches: matches2,
|
|
1497
|
+
deny: deny3,
|
|
1498
|
+
allow: allow2,
|
|
1499
|
+
scopes,
|
|
1500
|
+
// Codex's own deny form would be `enabled = false` in the person's server table: not written. The hook decides.
|
|
1501
|
+
install
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
// src/adapters/cursor.ts
|
|
1505
|
+
var import_node_fs7 = require("node:fs");
|
|
1506
|
+
var import_node_path8 = require("node:path");
|
|
1507
|
+
|
|
1508
|
+
// src/jsonc.ts
|
|
1509
|
+
function stripJsonc(text) {
|
|
1510
|
+
let out = "";
|
|
1511
|
+
let i = 0;
|
|
1512
|
+
const n = text.length;
|
|
1513
|
+
while (i < n) {
|
|
1514
|
+
const c = text[i];
|
|
1515
|
+
if (c === '"') {
|
|
1516
|
+
let j = i + 1;
|
|
1517
|
+
while (j < n && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
|
|
1518
|
+
out += text.slice(i, j + 1);
|
|
1519
|
+
i = j + 1;
|
|
1520
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
1521
|
+
while (i < n && text[i] !== "\n") i++;
|
|
1522
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
1523
|
+
const end = text.indexOf("*/", i + 2);
|
|
1524
|
+
i = end === -1 ? n : end + 2;
|
|
1525
|
+
} else if (c === ",") {
|
|
1526
|
+
let j = i + 1;
|
|
1527
|
+
for (; ; ) {
|
|
1528
|
+
while (j < n && /\s/.test(text[j])) j++;
|
|
1529
|
+
if (text[j] === "/" && text[j + 1] === "/") {
|
|
1530
|
+
while (j < n && text[j] !== "\n") j++;
|
|
1531
|
+
} else if (text[j] === "/" && text[j + 1] === "*") {
|
|
1532
|
+
const end = text.indexOf("*/", j + 2);
|
|
1533
|
+
j = end === -1 ? n : end + 2;
|
|
1534
|
+
} else break;
|
|
1535
|
+
}
|
|
1536
|
+
if (text[j] === "}" || text[j] === "]") i++;
|
|
1537
|
+
else out += text[i++];
|
|
1538
|
+
} else {
|
|
1539
|
+
out += text[i++];
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
function parseJsonc(text) {
|
|
1545
|
+
return JSON.parse(stripJsonc(text.replace(/^/, "")));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/adapters/cursor.ts
|
|
1549
|
+
var TIMEOUT_S2 = 10;
|
|
1550
|
+
var COPY2 = (0, import_node_path8.join)(".attestari", "hook.js");
|
|
1551
|
+
function isObject7(value) {
|
|
1552
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1553
|
+
}
|
|
1554
|
+
function isStrings2(value) {
|
|
1555
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
1556
|
+
}
|
|
1557
|
+
function parse3(_event, stdinText) {
|
|
1558
|
+
let input;
|
|
1559
|
+
try {
|
|
1560
|
+
input = JSON.parse(stdinText);
|
|
1561
|
+
} catch {
|
|
1562
|
+
return { kind: "unreadable" };
|
|
1563
|
+
}
|
|
1564
|
+
if (!isObject7(input)) return { kind: "unreadable" };
|
|
1565
|
+
const key = input.mcp_server_name;
|
|
1566
|
+
if (typeof key !== "string" || key === "") return { kind: "unreadable" };
|
|
1567
|
+
const roots = isStrings2(input.workspace_roots) ? input.workspace_roots : [];
|
|
1568
|
+
return {
|
|
1569
|
+
kind: "mcp",
|
|
1570
|
+
key,
|
|
1571
|
+
plugin: false,
|
|
1572
|
+
sessionId: typeof input.conversation_id === "string" ? input.conversation_id : void 0,
|
|
1573
|
+
cwd: roots.length > 0 && roots[0] !== "" ? roots[0] : void 0
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function deny4(_event, verdict) {
|
|
1577
|
+
return { stdout: `${JSON.stringify({ permission: "deny", user_message: verdict.reason, agent_message: verdict.reason })}
|
|
1578
|
+
`, exitCode: 0 };
|
|
1579
|
+
}
|
|
1580
|
+
var allow3 = () => ({ stdout: `${JSON.stringify({ permission: "allow" })}
|
|
1581
|
+
`, exitCode: 0 });
|
|
1582
|
+
function readCursorJson(file) {
|
|
1583
|
+
let text;
|
|
1584
|
+
try {
|
|
1585
|
+
text = (0, import_node_fs7.readFileSync)(file, "utf8");
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const value = parseJsonc(text);
|
|
1591
|
+
return isObject7(value) ? value : null;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function serversOf2(holder) {
|
|
1597
|
+
if (holder === null) return null;
|
|
1598
|
+
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
1599
|
+
return isObject7(holder.mcpServers) ? holder.mcpServers : null;
|
|
1600
|
+
}
|
|
1601
|
+
function scopes2(home2, cwd) {
|
|
1602
|
+
return [
|
|
1603
|
+
{ label: ".cursor/mcp.json", servers: serversOf2(readCursorJson((0, import_node_path8.join)(cwd, ".cursor", "mcp.json"))) },
|
|
1604
|
+
{ label: "~/.cursor/mcp.json", servers: serversOf2(readCursorJson((0, import_node_path8.join)(home2, ".cursor", "mcp.json"))) }
|
|
1605
|
+
];
|
|
1606
|
+
}
|
|
1607
|
+
var denyToken = (key) => `Mcp(${key}:*)`;
|
|
1608
|
+
function cursorDenyForms(resolved, policy) {
|
|
1609
|
+
const keys = resolved.filter((r) => entryFor(policy, r)?.decision === "block").map((r) => denyToken(r.key));
|
|
1610
|
+
return [...new Set(keys.map((k) => JSON.stringify(k)))];
|
|
1611
|
+
}
|
|
1612
|
+
function writeDenies2(resolved, policy, written, takeBack, elsewhere) {
|
|
1613
|
+
return writeDenyList(
|
|
1614
|
+
(0, import_node_path8.join)(home(), ".cursor", "cli-config.json"),
|
|
1615
|
+
(settings) => {
|
|
1616
|
+
const permissions = settings.permissions;
|
|
1617
|
+
if (permissions === void 0) return void 0;
|
|
1618
|
+
if (!isObject7(permissions)) return null;
|
|
1619
|
+
if (permissions.deny === void 0) return void 0;
|
|
1620
|
+
return Array.isArray(permissions.deny) ? permissions.deny : null;
|
|
1621
|
+
},
|
|
1622
|
+
(settings, list) => ({ ...settings, permissions: { ...isObject7(settings.permissions) ? settings.permissions : {}, deny: list } }),
|
|
1623
|
+
cursorDenyForms(resolved, policy),
|
|
1624
|
+
written,
|
|
1625
|
+
takeBack,
|
|
1626
|
+
elsewhere
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
function quoted2(path) {
|
|
1630
|
+
return /[\s"]/.test(path) ? `"${path.replace(/"/g, '\\"')}"` : path;
|
|
1631
|
+
}
|
|
1632
|
+
function hookCommands2(node, hook2) {
|
|
1633
|
+
return {
|
|
1634
|
+
preToolUse: `${quoted2(node)} ${quoted2(hook2)} cursor pre-tool-use`,
|
|
1635
|
+
sessionStart: `${quoted2(node)} ${quoted2(hook2)} cursor session-start`
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
var MINE2 = /attestari.*\bcursor (pre-tool-use|session-start)\b/;
|
|
1639
|
+
function withEntry(list, entry) {
|
|
1640
|
+
const current = Array.isArray(list) ? list : [];
|
|
1641
|
+
const others = current.filter((e) => !(isObject7(e) && typeof e.command === "string" && MINE2.test(e.command)));
|
|
1642
|
+
return [...others, entry];
|
|
1643
|
+
}
|
|
1644
|
+
function install2(home2, hookFile = __filename, node = process.execPath) {
|
|
1645
|
+
const copy = (0, import_node_path8.join)(home2, COPY2);
|
|
1646
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(home2, ".attestari"), { recursive: true });
|
|
1647
|
+
(0, import_node_fs7.copyFileSync)(hookFile, copy);
|
|
1648
|
+
const commands = hookCommands2(node, copy);
|
|
1649
|
+
const file = (0, import_node_path8.join)(home2, ".cursor", "hooks.json");
|
|
1650
|
+
const read = bytesOf(file);
|
|
1651
|
+
const text = read === null ? null : read.toString("utf8");
|
|
1652
|
+
let settings = {};
|
|
1653
|
+
if (text !== null) {
|
|
1654
|
+
try {
|
|
1655
|
+
settings = parseJsonc(text);
|
|
1656
|
+
} catch {
|
|
1657
|
+
throw new Error(`${file} could not be read as Cursor's hooks file; the hook was not installed`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (!isObject7(settings)) throw new Error(`${file} is not Cursor's hooks file; the hook was not installed`);
|
|
1661
|
+
const hooks = isObject7(settings.hooks) ? settings.hooks : {};
|
|
1662
|
+
const next = {
|
|
1663
|
+
...settings,
|
|
1664
|
+
version: typeof settings.version === "number" ? settings.version : 1,
|
|
1665
|
+
hooks: {
|
|
1666
|
+
...hooks,
|
|
1667
|
+
beforeMCPExecution: withEntry(hooks.beforeMCPExecution, { command: commands.preToolUse, timeout: TIMEOUT_S2, failClosed: true }),
|
|
1668
|
+
sessionStart: withEntry(hooks.sessionStart, { command: commands.sessionStart, timeout: TIMEOUT_S2 })
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
const after = formatLike(text, next);
|
|
1672
|
+
if (text !== after) {
|
|
1673
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(home2, ".cursor"), { recursive: true });
|
|
1674
|
+
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1675
|
+
}
|
|
1676
|
+
return { path: file, before: text, after };
|
|
1677
|
+
}
|
|
1678
|
+
var cursor = {
|
|
1679
|
+
name: "cursor",
|
|
1680
|
+
client: "Cursor",
|
|
1681
|
+
events: ["beforeMCPExecution", "sessionStart"],
|
|
1682
|
+
parse: parse3,
|
|
1683
|
+
deny: deny4,
|
|
1684
|
+
allow: allow3,
|
|
1685
|
+
scopes: scopes2,
|
|
1686
|
+
writeDenies: writeDenies2,
|
|
1687
|
+
install: install2
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// src/adapters/gemini.ts
|
|
1691
|
+
var import_node_fs8 = require("node:fs");
|
|
1692
|
+
var import_node_path9 = require("node:path");
|
|
1693
|
+
var PREFIX3 = "mcp_";
|
|
1694
|
+
var TIMEOUT_MS2 = 1e4;
|
|
1695
|
+
var COPY3 = (0, import_node_path9.join)(".attestari", "hook.js");
|
|
1696
|
+
var HANDLER = "attestari";
|
|
1697
|
+
var SESSION_HANDLER = "attestari-session";
|
|
1698
|
+
function isObject8(value) {
|
|
1699
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1700
|
+
}
|
|
1701
|
+
function parse4(_event, stdinText) {
|
|
1702
|
+
let input;
|
|
1703
|
+
try {
|
|
1704
|
+
input = JSON.parse(stdinText);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return { kind: "unreadable" };
|
|
1707
|
+
}
|
|
1708
|
+
if (!isObject8(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1709
|
+
const context = isObject8(input.mcp_context) ? input.mcp_context : null;
|
|
1710
|
+
if (context === null) return input.tool_name.startsWith(PREFIX3) ? { kind: "unreadable" } : { kind: "other" };
|
|
1711
|
+
const key = context.server_name;
|
|
1712
|
+
if (typeof key !== "string" || key === "") return { kind: "unreadable" };
|
|
1713
|
+
return {
|
|
1714
|
+
kind: "mcp",
|
|
1715
|
+
key,
|
|
1716
|
+
plugin: false,
|
|
1717
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : void 0,
|
|
1718
|
+
cwd: typeof input.cwd === "string" ? input.cwd : void 0
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
function deny5(_event, verdict) {
|
|
1722
|
+
return { stdout: `${JSON.stringify({ decision: "deny", reason: verdict.reason })}
|
|
1723
|
+
`, exitCode: 0 };
|
|
1724
|
+
}
|
|
1725
|
+
var allow4 = () => ({ stdout: "", exitCode: 0 });
|
|
1726
|
+
function geminiEntry(entry) {
|
|
1727
|
+
if (!isObject8(entry)) return entry;
|
|
1728
|
+
const address = typeof entry.httpUrl === "string" ? entry.httpUrl : typeof entry.url === "string" ? entry.url : null;
|
|
1729
|
+
if (address === null) return entry;
|
|
1730
|
+
const { command: _c, args: _a, httpUrl: _h, url: _u, type: _t, ...rest } = entry;
|
|
1731
|
+
return { ...rest, type: "http", url: address };
|
|
1732
|
+
}
|
|
1733
|
+
function serversOf3(holder) {
|
|
1734
|
+
if (holder === null) return null;
|
|
1735
|
+
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
1736
|
+
if (!isObject8(holder.mcpServers)) return null;
|
|
1737
|
+
return Object.fromEntries(Object.entries(holder.mcpServers).map(([key, entry]) => [key, geminiEntry(entry)]));
|
|
1738
|
+
}
|
|
1739
|
+
function systemSettingsPath(platform2 = process.platform, env = process.env) {
|
|
1740
|
+
if (env.GEMINI_CLI_SYSTEM_SETTINGS_PATH) return env.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
|
|
1741
|
+
if (platform2 === "win32") return import_node_path9.win32.join(env.ProgramData ?? "C:\\ProgramData", "gemini-cli", "settings.json");
|
|
1742
|
+
if (platform2 === "darwin") return "/Library/Application Support/GeminiCli/settings.json";
|
|
1743
|
+
return "/etc/gemini-cli/settings.json";
|
|
1744
|
+
}
|
|
1745
|
+
function scopes3(home2, cwd) {
|
|
1746
|
+
return [
|
|
1747
|
+
{ label: "the system settings.json", servers: serversOf3(readJson(systemSettingsPath())) },
|
|
1748
|
+
{ label: ".gemini/settings.json", servers: serversOf3(readJson((0, import_node_path9.join)(cwd, ".gemini", "settings.json"))) },
|
|
1749
|
+
{ label: "~/.gemini/settings.json", servers: serversOf3(readJson((0, import_node_path9.join)(home2, ".gemini", "settings.json"))) }
|
|
1750
|
+
];
|
|
1751
|
+
}
|
|
1752
|
+
function geminiDenyForms(resolved, policy) {
|
|
1753
|
+
const keys = resolved.filter((r) => entryFor(policy, r)?.decision === "block").map((r) => r.key);
|
|
1754
|
+
return [...new Set(keys.map((k) => JSON.stringify(k)))];
|
|
1755
|
+
}
|
|
1756
|
+
function writeDenies3(resolved, policy, written, takeBack, elsewhere) {
|
|
1757
|
+
return writeDenyList(
|
|
1758
|
+
(0, import_node_path9.join)(home(), ".gemini", "settings.json"),
|
|
1759
|
+
(settings) => {
|
|
1760
|
+
const mcp = settings.mcp;
|
|
1761
|
+
if (mcp === void 0) return void 0;
|
|
1762
|
+
if (!isObject8(mcp)) return null;
|
|
1763
|
+
if (mcp.excluded === void 0) return void 0;
|
|
1764
|
+
return Array.isArray(mcp.excluded) ? mcp.excluded : null;
|
|
1765
|
+
},
|
|
1766
|
+
(settings, list) => ({ ...settings, mcp: { ...isObject8(settings.mcp) ? settings.mcp : {}, excluded: list } }),
|
|
1767
|
+
geminiDenyForms(resolved, policy),
|
|
1768
|
+
written,
|
|
1769
|
+
takeBack,
|
|
1770
|
+
elsewhere
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
function quoted3(path) {
|
|
1774
|
+
return /[\s"]/.test(path) ? `"${path}"` : path;
|
|
1775
|
+
}
|
|
1776
|
+
var LAUNCHER = (0, import_node_path9.join)(".attestari", "gemini-hook.cmd");
|
|
1777
|
+
function geminiShell(platform2 = process.platform, env = process.env) {
|
|
1778
|
+
if (platform2 !== "win32") return "sh";
|
|
1779
|
+
const comSpec = (env.ComSpec ?? "").toLowerCase();
|
|
1780
|
+
if (comSpec.endsWith("powershell.exe") || comSpec.endsWith("pwsh.exe")) return "powershell";
|
|
1781
|
+
return findOnPath("pwsh", env.PATH ?? "", true) !== null ? "powershell" : "cmd";
|
|
1782
|
+
}
|
|
1783
|
+
function hookCommands3(node, hook2, shell, launcher) {
|
|
1784
|
+
const program = shell === "sh" || launcher === void 0 ? `${quoted3(node)} ${quoted3(hook2)}` : quoted3(launcher);
|
|
1785
|
+
const call = shell === "powershell" && program.startsWith('"') ? `& ${program}` : program;
|
|
1786
|
+
return { preToolUse: `${call} gemini pre-tool-use`, sessionStart: `${call} gemini session-start` };
|
|
1787
|
+
}
|
|
1788
|
+
function launcherText(node, hook2) {
|
|
1789
|
+
return `@"${node}" "${hook2}" %*\r
|
|
1790
|
+
`;
|
|
1791
|
+
}
|
|
1792
|
+
function isMine2(handler) {
|
|
1793
|
+
return isObject8(handler) && (handler.name === HANDLER || handler.name === SESSION_HANDLER || typeof handler.command === "string" && /attestari.*\bgemini (pre-tool-use|session-start)\b/.test(handler.command));
|
|
1794
|
+
}
|
|
1795
|
+
function withHandler2(groups, matcher, handler) {
|
|
1796
|
+
const current = Array.isArray(groups) ? groups : [];
|
|
1797
|
+
const cleaned = current.map((g) => isObject8(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !isMine2(h)) } : g).filter((g) => !(isObject8(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
1798
|
+
const at = cleaned.findIndex((g) => isObject8(g) && g.matcher === matcher && Array.isArray(g.hooks));
|
|
1799
|
+
if (at === -1) return [...cleaned, matcher === void 0 ? { hooks: [handler] } : { matcher, hooks: [handler] }];
|
|
1800
|
+
const group = cleaned[at];
|
|
1801
|
+
cleaned[at] = { ...group, hooks: [...group.hooks, handler] };
|
|
1802
|
+
return cleaned;
|
|
1803
|
+
}
|
|
1804
|
+
function install3(home2, hookFile = __filename, node = process.execPath) {
|
|
1805
|
+
const copy = (0, import_node_path9.join)(home2, COPY3);
|
|
1806
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(home2, ".attestari"), { recursive: true });
|
|
1807
|
+
(0, import_node_fs8.copyFileSync)(hookFile, copy);
|
|
1808
|
+
const shell = geminiShell();
|
|
1809
|
+
let launcher;
|
|
1810
|
+
if (shell !== "sh") {
|
|
1811
|
+
launcher = (0, import_node_path9.join)(home2, LAUNCHER);
|
|
1812
|
+
(0, import_node_fs8.writeFileSync)(launcher, launcherText(node, copy));
|
|
1813
|
+
}
|
|
1814
|
+
const commands = hookCommands3(node, copy, shell, launcher);
|
|
1815
|
+
const file = (0, import_node_path9.join)(home2, ".gemini", "settings.json");
|
|
1816
|
+
const read = bytesOf(file);
|
|
1817
|
+
const text = read === null ? null : read.toString("utf8");
|
|
1818
|
+
let settings = {};
|
|
1819
|
+
if (text !== null) {
|
|
1820
|
+
try {
|
|
1821
|
+
settings = JSON.parse(text.replace(/^\ufeff/, ""));
|
|
1822
|
+
} catch {
|
|
1823
|
+
throw new Error(`${file} could not be read as Gemini CLI's settings file; the hook was not installed`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!isObject8(settings) || settings.hooks !== void 0 && !isObject8(settings.hooks)) {
|
|
1827
|
+
throw new Error(`${file} is not Gemini CLI's settings file; the hook was not installed`);
|
|
1828
|
+
}
|
|
1829
|
+
const hooks = isObject8(settings.hooks) ? settings.hooks : {};
|
|
1830
|
+
const next = {
|
|
1831
|
+
...settings,
|
|
1832
|
+
hooks: {
|
|
1833
|
+
...hooks,
|
|
1834
|
+
BeforeTool: withHandler2(hooks.BeforeTool, "mcp_.*", { name: HANDLER, type: "command", command: commands.preToolUse, timeout: TIMEOUT_MS2, description: "Attestari checks the server before the call" }),
|
|
1835
|
+
SessionStart: withHandler2(hooks.SessionStart, void 0, { name: SESSION_HANDLER, type: "command", command: commands.sessionStart, timeout: TIMEOUT_MS2 })
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const after = formatLike(text, next);
|
|
1839
|
+
if (text !== after) {
|
|
1840
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(home2, ".gemini"), { recursive: true });
|
|
1841
|
+
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1842
|
+
}
|
|
1843
|
+
return { path: file, before: text, after };
|
|
1844
|
+
}
|
|
1845
|
+
var gemini = {
|
|
1846
|
+
name: "gemini",
|
|
1847
|
+
client: "Gemini CLI",
|
|
1848
|
+
events: ["BeforeTool", "SessionStart"],
|
|
1849
|
+
parse: parse4,
|
|
1850
|
+
deny: deny5,
|
|
1851
|
+
allow: allow4,
|
|
1852
|
+
scopes: scopes3,
|
|
1853
|
+
writeDenies: writeDenies3,
|
|
1854
|
+
install: install3
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
// src/adapters/registry.ts
|
|
1858
|
+
var ADAPTERS = /* @__PURE__ */ new Map([
|
|
1859
|
+
[claudeCode.name, claudeCode],
|
|
1860
|
+
[cursor.name, cursor],
|
|
1861
|
+
[codex.name, codex],
|
|
1862
|
+
[gemini.name, gemini]
|
|
1863
|
+
]);
|
|
1864
|
+
function adapterFor(name) {
|
|
1865
|
+
return ADAPTERS.get(name) ?? null;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/session.ts
|
|
1869
|
+
var import_node_fs9 = require("node:fs");
|
|
1870
|
+
var import_node_os2 = require("node:os");
|
|
1871
|
+
|
|
1872
|
+
// src/api.ts
|
|
1873
|
+
var import_node_http = require("node:http");
|
|
1874
|
+
var import_node_https = require("node:https");
|
|
1875
|
+
|
|
1876
|
+
// src/version.ts
|
|
1877
|
+
var HOOK_VERSION = "2.9.1";
|
|
1878
|
+
|
|
1879
|
+
// src/api.ts
|
|
1880
|
+
var timeoutMs = 1e4;
|
|
1881
|
+
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
1882
|
+
var ApiError = class extends Error {
|
|
1883
|
+
status;
|
|
1884
|
+
error;
|
|
1885
|
+
retryAfterSeconds;
|
|
1886
|
+
said;
|
|
1887
|
+
constructor(status2, error, retryAfterSeconds = null, said = null) {
|
|
1888
|
+
super(`${status2} ${error}`);
|
|
1889
|
+
this.name = "ApiError";
|
|
1890
|
+
this.status = status2;
|
|
1891
|
+
this.error = error;
|
|
1892
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
1893
|
+
this.said = said;
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
function send(url2, method, headers, body) {
|
|
1897
|
+
return new Promise((resolve2, reject) => {
|
|
1898
|
+
const unreachable = () => new ApiError(0, "unreachable");
|
|
1899
|
+
let target;
|
|
1900
|
+
try {
|
|
1901
|
+
target = new URL(url2);
|
|
1902
|
+
} catch {
|
|
1903
|
+
reject(unreachable());
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const request = target.protocol === "https:" ? import_node_https.request : target.protocol === "http:" ? import_node_http.request : null;
|
|
1907
|
+
if (request === null) {
|
|
1908
|
+
reject(unreachable());
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const allHeaders = { Accept: "application/json", "User-Agent": `attestari-hook/${HOOK_VERSION}`, ...headers };
|
|
1912
|
+
if (body !== null) {
|
|
1913
|
+
allHeaders["Content-Type"] = "application/json";
|
|
1914
|
+
allHeaders["Content-Length"] = String(Buffer.byteLength(body));
|
|
1915
|
+
}
|
|
1916
|
+
let settled = false;
|
|
1917
|
+
const settle = (fn) => {
|
|
1918
|
+
if (settled) return;
|
|
1919
|
+
settled = true;
|
|
1920
|
+
clearTimeout(deadline);
|
|
1921
|
+
fn();
|
|
1922
|
+
};
|
|
1923
|
+
let req;
|
|
1924
|
+
try {
|
|
1925
|
+
req = request(target, { method, headers: allHeaders }, (res) => {
|
|
1926
|
+
const chunks = [];
|
|
1927
|
+
let size = 0;
|
|
1928
|
+
res.on("data", (chunk) => {
|
|
1929
|
+
size += chunk.length;
|
|
1930
|
+
if (size > MAX_BODY_BYTES) {
|
|
1931
|
+
settle(() => reject(new ApiError(res.statusCode ?? 0, "unexpected_response")));
|
|
1932
|
+
req.destroy();
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
chunks.push(chunk);
|
|
1936
|
+
});
|
|
1937
|
+
res.on("end", () => settle(() => resolve2({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })));
|
|
1938
|
+
res.on("error", () => settle(() => reject(unreachable())));
|
|
1939
|
+
});
|
|
1940
|
+
} catch {
|
|
1941
|
+
reject(unreachable());
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const deadline = setTimeout(() => {
|
|
1945
|
+
settle(() => reject(unreachable()));
|
|
1946
|
+
req.destroy();
|
|
1947
|
+
}, timeoutMs);
|
|
1948
|
+
req.on("error", () => settle(() => reject(unreachable())));
|
|
1949
|
+
req.end(body ?? void 0);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
function parseObject(text) {
|
|
1953
|
+
try {
|
|
1954
|
+
const value = JSON.parse(text);
|
|
1955
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1956
|
+
} catch {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function retryAfter(reply) {
|
|
1961
|
+
const raw = reply.headers["retry-after"];
|
|
1962
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
1963
|
+
if (value === void 0 || !/^\d+$/.test(value.trim())) return null;
|
|
1964
|
+
return Number.parseInt(value.trim(), 10);
|
|
1965
|
+
}
|
|
1966
|
+
function failure(reply) {
|
|
1967
|
+
const word = parseObject(reply.text)?.error;
|
|
1968
|
+
const said = typeof word === "string" && word !== "" ? word : null;
|
|
1969
|
+
if (reply.status === 401) return new ApiError(401, "invalid_token", null, said);
|
|
1970
|
+
return new ApiError(reply.status, said ?? "unexpected_response", retryAfter(reply), said);
|
|
1971
|
+
}
|
|
1972
|
+
function ok(reply) {
|
|
1973
|
+
if (reply.status !== 200) throw failure(reply);
|
|
1974
|
+
const body = parseObject(reply.text);
|
|
1975
|
+
if (body === null) throw new ApiError(reply.status, "unexpected_response");
|
|
1976
|
+
return body;
|
|
1977
|
+
}
|
|
1978
|
+
function url(base, path) {
|
|
1979
|
+
return `${base.replace(/\/+$/, "")}${path}`;
|
|
1980
|
+
}
|
|
1981
|
+
function bearer(token) {
|
|
1982
|
+
return { Authorization: `Bearer ${token}` };
|
|
1983
|
+
}
|
|
1984
|
+
async function redeem(base, code, about) {
|
|
1985
|
+
const body = JSON.stringify({ client_id: "attestari-hook", code, ...about });
|
|
1986
|
+
const answer = ok(await send(url(base, "/v1/device/redeem"), "POST", {}, body));
|
|
1987
|
+
const organization = answer.organization;
|
|
1988
|
+
if (typeof answer.access_token !== "string" || answer.access_token === "" || typeof organization !== "object" || organization === null) {
|
|
1989
|
+
throw new ApiError(200, "unexpected_response");
|
|
1990
|
+
}
|
|
1991
|
+
return { token: answer.access_token, organization };
|
|
1992
|
+
}
|
|
1993
|
+
async function checkin(base, token, body, etag) {
|
|
1994
|
+
const headers = bearer(token);
|
|
1995
|
+
if (etag !== null) headers["If-None-Match"] = etag;
|
|
1996
|
+
const answer = ok(await send(url(base, "/v1/device/checkin"), "POST", headers, JSON.stringify(body)));
|
|
1997
|
+
if (typeof answer.policy_version !== "number" || !(answer.compiled === null || isCompiled(answer.compiled))) {
|
|
1998
|
+
throw new ApiError(200, "unexpected_response");
|
|
1999
|
+
}
|
|
2000
|
+
return { policy_version: answer.policy_version, compiled: answer.compiled };
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/session.ts
|
|
2004
|
+
var EVENTS_AT_MOST = 200;
|
|
2005
|
+
var SESSION_MS = 30 * DAY_MS;
|
|
2006
|
+
var FOLDER_MS = 30 * DAY_MS;
|
|
2007
|
+
var PAIRING_LINE = "Attestari: this agent is not protected. Run `npx -y attestari@2.9 pair --code <code from app.attestari.ai/agents>`";
|
|
2008
|
+
var INVALID_CODE = "That code is not valid or has expired. Make a new one at app.attestari.ai/agents.";
|
|
2009
|
+
var TOKEN_REFUSED = "Pairing did not complete: Attestari refused the new token. Make a new code at app.attestari.ai/agents.";
|
|
2010
|
+
var PAIR_USAGE = "usage: attestari-hook pair --code <code> [--agent claude-code|cursor|codex|gemini]";
|
|
2011
|
+
var INSTALL_USAGE = "usage: attestari-hook install --agent claude-code|cursor|codex|gemini";
|
|
2012
|
+
var ACCOUNTS = "https://accounts.attestari.ai";
|
|
2013
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
2014
|
+
function accountsBase() {
|
|
2015
|
+
const wanted = process.env.ATTESTARI_ACCOUNTS_URL;
|
|
2016
|
+
if (!wanted) return ACCOUNTS;
|
|
2017
|
+
let host;
|
|
2018
|
+
try {
|
|
2019
|
+
host = new URL(wanted).hostname.toLowerCase();
|
|
2020
|
+
} catch {
|
|
2021
|
+
host = null;
|
|
2022
|
+
}
|
|
2023
|
+
if (host !== null && LOOPBACK.has(host)) return wanted;
|
|
2024
|
+
say("ATTESTARI_ACCOUNTS_URL is ignored: only a loopback address (127.0.0.1, ::1, localhost) may replace accounts.attestari.ai");
|
|
2025
|
+
return ACCOUNTS;
|
|
2026
|
+
}
|
|
2027
|
+
function isObject9(value) {
|
|
2028
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2029
|
+
}
|
|
2030
|
+
function resolveAndKeep(adapter, cwd) {
|
|
2031
|
+
const agent = adapter.name;
|
|
2032
|
+
const { resolved, notes, complete } = resolveConfiguration(home(), cwd, readSecret(agent), adapter.scopes(home(), cwd));
|
|
2033
|
+
for (const note of notes) say(note);
|
|
2034
|
+
attempt("the resolution was not written", () => writeResolution(agent, cwd, resolved, complete));
|
|
2035
|
+
}
|
|
2036
|
+
function sessionStart(stdinText, adapter) {
|
|
2037
|
+
const agent = adapter.name;
|
|
2038
|
+
if (readToken(agent) === null) return { stdout: `${PAIRING_LINE}
|
|
2039
|
+
`, exitCode: 0 };
|
|
2040
|
+
let input = null;
|
|
2041
|
+
try {
|
|
2042
|
+
input = JSON.parse(stdinText);
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
const cwd = isObject9(input) && typeof input.cwd === "string" && input.cwd !== "" ? input.cwd : process.cwd();
|
|
2046
|
+
attempt("the configuration was not resolved", () => resolveAndKeep(adapter, cwd));
|
|
2047
|
+
if (isObject9(input) && typeof input.session_id === "string" && input.session_id !== "") {
|
|
2048
|
+
const sessionId = input.session_id;
|
|
2049
|
+
attempt("the session was not bound to its folder", () => writeSession(agent, sessionId, cwd));
|
|
2050
|
+
}
|
|
2051
|
+
attempt("the refresh was not started", () => {
|
|
2052
|
+
if (!heldBack(agent, Date.now(), true)) spawnRefresh(agent, cwd);
|
|
2053
|
+
});
|
|
2054
|
+
return { stdout: "", exitCode: 0 };
|
|
2055
|
+
}
|
|
2056
|
+
function nextAt(agent, ms, word) {
|
|
2057
|
+
attempt("next_refresh_at was not written", () => writeStamp(agent, "next_refresh_at", new Date(Date.now() + ms), word));
|
|
2058
|
+
}
|
|
2059
|
+
async function refresh(adapter, cwd) {
|
|
2060
|
+
const agent = adapter.name;
|
|
2061
|
+
const token = readToken(agent);
|
|
2062
|
+
if (token === null) return null;
|
|
2063
|
+
if (cwd !== void 0 && cwd !== "" && readResolution(agent, cwd) === null) {
|
|
2064
|
+
try {
|
|
2065
|
+
resolveAndKeep(adapter, cwd);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
say(`the configuration was not resolved: ${message(error)}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
pruneSessions(agent, SESSION_MS);
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
const now = Date.now();
|
|
2075
|
+
const all = readResolutions(agent);
|
|
2076
|
+
const stale = all.filter((r) => r.cwd !== void 0 && folderGone(r.cwd) || r.modifiedMs !== void 0 && now - r.modifiedMs > FOLDER_MS);
|
|
2077
|
+
const resolutions = all.filter((r) => !stale.includes(r));
|
|
2078
|
+
const held = readPolicy(agent);
|
|
2079
|
+
const etag = held === null ? null : readEtag(agent);
|
|
2080
|
+
let events = [];
|
|
2081
|
+
try {
|
|
2082
|
+
events = readEvents(agent).slice(0, EVENTS_AT_MOST);
|
|
2083
|
+
} catch {
|
|
2084
|
+
}
|
|
2085
|
+
const servers = reportedAll(resolutions.map((r) => ({ folder: r.id.slice(0, 8), servers: r.servers })));
|
|
2086
|
+
let answer;
|
|
2087
|
+
try {
|
|
2088
|
+
answer = await checkin(
|
|
2089
|
+
accountsBase(),
|
|
2090
|
+
token,
|
|
2091
|
+
{ hook_version: HOOK_VERSION, agent_client: adapter.client, policy_version: held?.policy.policy.version ?? 0, servers, events },
|
|
2092
|
+
held !== null && etag === held.policy.etag ? etag : null
|
|
2093
|
+
);
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
if (!(error instanceof ApiError)) say(`the check-in failed: ${message(error)}`);
|
|
2096
|
+
else if (error.status === 401) {
|
|
2097
|
+
if (error.said === "invalid_token") {
|
|
2098
|
+
deleteToken(agent);
|
|
2099
|
+
deletePolicy(agent);
|
|
2100
|
+
}
|
|
2101
|
+
} else if (error.status === 503) nextAt(agent, Math.min(Math.max((error.retryAfterSeconds ?? 0) * 1e3, HOUR_MS), DAY_MS), LOOKUP_UNAVAILABLE);
|
|
2102
|
+
else if (error.status === 403) nextAt(agent, HOUR_MS, REFUSED);
|
|
2103
|
+
else if (error.status !== 0) say(`the check-in was refused: ${error.status} ${error.error}`);
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
let policy = held?.policy ?? null;
|
|
2107
|
+
if (answer.compiled !== null) {
|
|
2108
|
+
const document = answer.compiled;
|
|
2109
|
+
policy = document;
|
|
2110
|
+
attempt("policy.json was not written", () => writePolicy(agent, document, document.etag));
|
|
2111
|
+
} else if (held !== null) {
|
|
2112
|
+
const current = held.policy;
|
|
2113
|
+
attempt("policy.json was not confirmed", () => writePolicy(agent, current, current.etag));
|
|
2114
|
+
}
|
|
2115
|
+
try {
|
|
2116
|
+
removeEvents(agent, events.map((e) => e.id));
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
2119
|
+
attempt("checked_in_at was not written", () => writeStamp(agent, "checked_in_at"));
|
|
2120
|
+
nextAt(agent, HOUR_MS, CHECKED_IN);
|
|
2121
|
+
if (policy !== null && adapter.writeDenies !== void 0) {
|
|
2122
|
+
dropStale(agent, adapter.writeDenies, stale, resolutions, policy);
|
|
2123
|
+
writeAllDenies(agent, adapter.writeDenies, resolutions, policy);
|
|
2124
|
+
}
|
|
2125
|
+
return { servers: servers.length, policy };
|
|
2126
|
+
}
|
|
2127
|
+
function folderGone(folder) {
|
|
2128
|
+
try {
|
|
2129
|
+
return !(0, import_node_fs9.statSync)(folder).isDirectory();
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
const code = error.code;
|
|
2132
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function dropStale(agent, write, stale, live, policy) {
|
|
2136
|
+
const blocked2 = new Map(live.map((r) => [r.id, blockedForms(r.servers, policy)]));
|
|
2137
|
+
const elsewhere = new Set([...blocked2.values()].flat());
|
|
2138
|
+
for (const resolution of stale) {
|
|
2139
|
+
attempt("a folder's resolution was not dropped", () => {
|
|
2140
|
+
if (resolution.complete) {
|
|
2141
|
+
const left = write([], policy, readWritten(agent, resolution.id), true, elsewhere);
|
|
2142
|
+
const heirs = left.map((form) => [form, live.find((r) => blocked2.get(r.id)?.includes(form))]);
|
|
2143
|
+
if (heirs.some(([, heir]) => heir === void 0)) return;
|
|
2144
|
+
for (const [form, heir] of heirs) {
|
|
2145
|
+
if (heir === void 0) continue;
|
|
2146
|
+
const theirs = readWritten(agent, heir.id);
|
|
2147
|
+
if (!theirs.includes(form)) writeWritten(agent, heir.id, [...theirs, form]);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
dropResolution(agent, resolution.id);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
function writeAllDenies(agent, write, resolutions, policy) {
|
|
2155
|
+
const blocked2 = new Map(resolutions.map((r) => [r.id, blockedForms(r.servers, policy)]));
|
|
2156
|
+
for (const resolution of resolutions) {
|
|
2157
|
+
const elsewhere = new Set(resolutions.filter((r) => r.id !== resolution.id).flatMap((r) => blocked2.get(r.id) ?? []));
|
|
2158
|
+
attempt("the deny entries were not written", () => {
|
|
2159
|
+
const written = write(resolution.servers, policy, readWritten(agent, resolution.id), resolution.complete, elsewhere);
|
|
2160
|
+
writeWritten(agent, resolution.id, written);
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
function oneLine2(text) {
|
|
2165
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
2166
|
+
}
|
|
2167
|
+
async function pair(args) {
|
|
2168
|
+
let code;
|
|
2169
|
+
let agent = "claude-code";
|
|
2170
|
+
for (let i = 0; i < args.length; i++) {
|
|
2171
|
+
const a = args[i];
|
|
2172
|
+
if ((a === "--code" || a === "--agent") && i + 1 < args.length) {
|
|
2173
|
+
if (a === "--code") code = args[++i];
|
|
2174
|
+
else agent = args[++i];
|
|
2175
|
+
} else if (a.startsWith("--code=")) code = a.slice("--code=".length);
|
|
2176
|
+
else if (a.startsWith("--agent=")) agent = a.slice("--agent=".length);
|
|
2177
|
+
else {
|
|
2178
|
+
process.stderr.write(`${PAIR_USAGE}
|
|
2179
|
+
`);
|
|
2180
|
+
return 2;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (code === void 0 || code === "") {
|
|
2184
|
+
process.stderr.write(`${PAIR_USAGE}
|
|
2185
|
+
`);
|
|
2186
|
+
return 2;
|
|
2187
|
+
}
|
|
2188
|
+
const adapter = adapterFor(agent);
|
|
2189
|
+
if (adapter === null) {
|
|
2190
|
+
process.stderr.write(`attestari-hook: pairing ${oneLine2(agent)} is not built yet
|
|
2191
|
+
${PAIR_USAGE}
|
|
2192
|
+
`);
|
|
2193
|
+
return 2;
|
|
2194
|
+
}
|
|
2195
|
+
let redeemed;
|
|
2196
|
+
try {
|
|
2197
|
+
redeemed = await redeem(accountsBase(), code, {
|
|
2198
|
+
agent_client: adapter.client,
|
|
2199
|
+
device_name: oneLine2((0, import_node_os2.hostname)()).trim().slice(0, 80) || void 0,
|
|
2200
|
+
os: `${(0, import_node_os2.platform)()} ${(0, import_node_os2.release)()}`.slice(0, 80),
|
|
2201
|
+
hook_version: HOOK_VERSION
|
|
2202
|
+
});
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
if (!(error instanceof ApiError)) throw error;
|
|
2205
|
+
if (error.error === "invalid_grant") process.stderr.write(`${INVALID_CODE}
|
|
2206
|
+
`);
|
|
2207
|
+
else if (error.status === 0) process.stderr.write("Attestari could not be reached. Try again in a moment.\n");
|
|
2208
|
+
else process.stderr.write(`Pairing was refused: ${error.error}.
|
|
2209
|
+
`);
|
|
2210
|
+
return 1;
|
|
2211
|
+
}
|
|
2212
|
+
try {
|
|
2213
|
+
writeToken(agent, redeemed.token);
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
process.stderr.write(`The token could not be kept: ${oneLine2(message(error))}
|
|
2216
|
+
`);
|
|
2217
|
+
return 1;
|
|
2218
|
+
}
|
|
2219
|
+
const organization = oneLine2(String(redeemed.organization.name ?? "")).trim() || "your organization";
|
|
2220
|
+
const cwd = process.cwd();
|
|
2221
|
+
let refreshed = null;
|
|
2222
|
+
try {
|
|
2223
|
+
resolveAndKeep(adapter, cwd);
|
|
2224
|
+
refreshed = await refresh(adapter, cwd);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
say(`the first check-in failed: ${message(error)}`);
|
|
2227
|
+
}
|
|
2228
|
+
if (readToken(agent) === null) {
|
|
2229
|
+
process.stderr.write(`${TOKEN_REFUSED}
|
|
2230
|
+
`);
|
|
2231
|
+
return 1;
|
|
2232
|
+
}
|
|
2233
|
+
if (refreshed !== null && refreshed.policy !== null) {
|
|
2234
|
+
process.stdout.write(`Paired with ${organization}: ${refreshed.servers} servers, policy ${refreshed.policy.policy.version}.
|
|
2235
|
+
`);
|
|
2236
|
+
} else {
|
|
2237
|
+
const servers = readResolution(agent, cwd)?.servers.length ?? 0;
|
|
2238
|
+
process.stdout.write(`Paired with ${organization}: ${servers} servers, no policy yet (it is asked for at the next session).
|
|
2239
|
+
`);
|
|
2240
|
+
}
|
|
2241
|
+
installHook(adapter);
|
|
2242
|
+
return 0;
|
|
2243
|
+
}
|
|
2244
|
+
function afterInstall(agent) {
|
|
2245
|
+
switch (agent) {
|
|
2246
|
+
case "cursor":
|
|
2247
|
+
return ["Cursor reads ~/.cursor/hooks.json itself; nothing to restart."];
|
|
2248
|
+
case "codex":
|
|
2249
|
+
return ["Codex runs no user hook until you trust it: in Codex, run /hooks and trust the Attestari handlers."];
|
|
2250
|
+
case "gemini":
|
|
2251
|
+
return ["Gemini CLI runs hooks in trusted folders only; the hook decides there."];
|
|
2252
|
+
default:
|
|
2253
|
+
return [];
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function installHook(adapter) {
|
|
2257
|
+
if (adapter.name === "claude-code") {
|
|
2258
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
2259
|
+
say("the Attestari plugin is installed in Claude Code already; the hook decides from the next session");
|
|
2260
|
+
return true;
|
|
2261
|
+
}
|
|
2262
|
+
let plugin;
|
|
2263
|
+
try {
|
|
2264
|
+
plugin = installPlugin();
|
|
2265
|
+
} catch (error) {
|
|
2266
|
+
plugin = { installed: false, why: `The install failed (${oneLine2(message(error))})` };
|
|
2267
|
+
}
|
|
2268
|
+
if (plugin.installed) say("the Attestari plugin is installed in Claude Code; it runs from the next session");
|
|
2269
|
+
else process.stderr.write(`${plugin.why}, so the Attestari plugin was not installed. Run:
|
|
2270
|
+
${installCommands()}`);
|
|
2271
|
+
return plugin.installed;
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
const done = adapter.install(home());
|
|
2275
|
+
if ("path" in done) say(`the hook is installed in ${done.path}${done.before === done.after ? " (it was already)" : ""}`);
|
|
2276
|
+
for (const line of afterInstall(adapter.name)) say(line);
|
|
2277
|
+
return true;
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
process.stderr.write(`The hook was not installed: ${oneLine2(message(error))}. Fix that and run: npx -y attestari@2.9 install --agent ${adapter.name}
|
|
2280
|
+
`);
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
function install4(args) {
|
|
2285
|
+
let agent;
|
|
2286
|
+
for (let i = 0; i < args.length; i++) {
|
|
2287
|
+
const a = args[i];
|
|
2288
|
+
if (a === "--agent" && i + 1 < args.length) agent = args[++i];
|
|
2289
|
+
else if (a.startsWith("--agent=")) agent = a.slice("--agent=".length);
|
|
2290
|
+
else {
|
|
2291
|
+
process.stderr.write(`${INSTALL_USAGE}
|
|
2292
|
+
`);
|
|
2293
|
+
return 2;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const adapter = agent === void 0 ? null : adapterFor(agent);
|
|
2297
|
+
if (adapter === null) {
|
|
2298
|
+
process.stderr.write(`${INSTALL_USAGE}
|
|
2299
|
+
`);
|
|
2300
|
+
return 2;
|
|
2301
|
+
}
|
|
2302
|
+
return installHook(adapter) ? 0 : 1;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/status.ts
|
|
2306
|
+
var import_node_fs10 = require("node:fs");
|
|
2307
|
+
var import_node_path10 = require("node:path");
|
|
2308
|
+
function age(ms) {
|
|
2309
|
+
const minutes = Math.floor(ms / 6e4);
|
|
2310
|
+
if (minutes < 60) return `${Math.max(minutes, 0)} min`;
|
|
2311
|
+
const hours = Math.floor(minutes / 60);
|
|
2312
|
+
if (hours < 48) return `${hours} h`;
|
|
2313
|
+
return `${Math.floor(hours / 24)} days`;
|
|
2314
|
+
}
|
|
2315
|
+
function policyLine(dir) {
|
|
2316
|
+
const file = (0, import_node_path10.join)(dir, "policy.json");
|
|
2317
|
+
if (!(0, import_node_fs10.existsSync)(file)) return null;
|
|
2318
|
+
let version = "unreadable";
|
|
2319
|
+
try {
|
|
2320
|
+
const document = JSON.parse((0, import_node_fs10.readFileSync)(file, "utf8"));
|
|
2321
|
+
const v = document?.policy?.version;
|
|
2322
|
+
if (typeof v === "number") version = String(v);
|
|
2323
|
+
} catch {
|
|
2324
|
+
}
|
|
2325
|
+
return `policy ${version}, ${age(Date.now() - (0, import_node_fs10.statSync)(file).mtimeMs)} old`;
|
|
2326
|
+
}
|
|
2327
|
+
function status() {
|
|
2328
|
+
const root = attestariDir();
|
|
2329
|
+
const agents = (0, import_node_fs10.existsSync)(root) ? (0, import_node_fs10.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort() : [];
|
|
2330
|
+
if (agents.length === 0) {
|
|
2331
|
+
console.log("not paired");
|
|
2332
|
+
return 0;
|
|
2333
|
+
}
|
|
2334
|
+
for (const agent of agents) {
|
|
2335
|
+
const dir = agentDir(agent);
|
|
2336
|
+
const token = (0, import_node_fs10.existsSync)((0, import_node_path10.join)(dir, "token")) ? "token present" : "no token";
|
|
2337
|
+
const policy = policyLine(dir) ?? "no policy";
|
|
2338
|
+
console.log(`${agent}: ${token}; ${policy}`);
|
|
2339
|
+
}
|
|
2340
|
+
return 0;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/main.ts
|
|
2344
|
+
var AGENTS = ["claude-code", "cursor", "codex", "gemini"];
|
|
2345
|
+
var EVENTS = ["session-start", "pre-tool-use", "refresh"];
|
|
2346
|
+
var USAGE = [
|
|
2347
|
+
"usage: attestari-hook <agent> <event>",
|
|
2348
|
+
" attestari-hook pair --code <code> [--agent <agent>]",
|
|
2349
|
+
" attestari-hook install --agent <agent>",
|
|
2350
|
+
" attestari-hook status",
|
|
2351
|
+
`agents: ${AGENTS.join(", ")}; events: ${EVENTS.join(", ")}`
|
|
2352
|
+
].join("\n");
|
|
2353
|
+
function notBuilt(name) {
|
|
2354
|
+
console.error(`attestari-hook: ${name} is not built yet`);
|
|
2355
|
+
return 2;
|
|
2356
|
+
}
|
|
2357
|
+
function stdin() {
|
|
2358
|
+
return (0, import_node_fs11.readFileSync)(0, "utf8");
|
|
2359
|
+
}
|
|
2360
|
+
var COMMANDS = {
|
|
2361
|
+
status: () => status(),
|
|
2362
|
+
pair: (args) => pair(args),
|
|
2363
|
+
install: (args) => install4(args)
|
|
2364
|
+
};
|
|
2365
|
+
function hook(agent, event, args) {
|
|
2366
|
+
const adapter = adapterFor(agent);
|
|
2367
|
+
if (adapter === null) return notBuilt(`${agent} ${event}`);
|
|
2368
|
+
switch (event) {
|
|
2369
|
+
case "pre-tool-use": {
|
|
2370
|
+
const outcome = decideCall(adapter, adapter.events[0], stdin());
|
|
2371
|
+
process.stdout.write(outcome.stdout);
|
|
2372
|
+
return outcome.exitCode;
|
|
2373
|
+
}
|
|
2374
|
+
case "session-start": {
|
|
2375
|
+
const outcome = sessionStart(stdin(), adapter);
|
|
2376
|
+
process.stdout.write(outcome.stdout);
|
|
2377
|
+
return outcome.exitCode;
|
|
2378
|
+
}
|
|
2379
|
+
// Detached, its output ignored: whatever happens it exits 0 and the next session or hour tries again.
|
|
2380
|
+
// `--cwd <folder>`: the session's working folder, resolved first when it has no resolution.
|
|
2381
|
+
case "refresh": {
|
|
2382
|
+
const at = args.indexOf("--cwd");
|
|
2383
|
+
return refresh(adapter, at >= 0 ? args[at + 1] : void 0).then(() => 0);
|
|
2384
|
+
}
|
|
2385
|
+
default:
|
|
2386
|
+
return notBuilt(`${agent} ${event}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function run2(argv) {
|
|
2390
|
+
const [first, second] = argv;
|
|
2391
|
+
const command = first !== void 0 && Object.hasOwn(COMMANDS, first) ? COMMANDS[first] : void 0;
|
|
2392
|
+
if (command) return command(argv.slice(1));
|
|
2393
|
+
if (first !== void 0 && AGENTS.includes(first) && second !== void 0 && EVENTS.includes(second)) {
|
|
2394
|
+
return hook(first, second, argv.slice(2));
|
|
2395
|
+
}
|
|
2396
|
+
console.error(USAGE);
|
|
2397
|
+
return 2;
|
|
2398
|
+
}
|
|
2399
|
+
function failed(error) {
|
|
2400
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
2401
|
+
console.error(`attestari-hook: ${message2.replace(/\s+/g, " ").trim() || "failed"}`);
|
|
2402
|
+
process.exitCode = 2;
|
|
2403
|
+
}
|
|
2404
|
+
function failedLater(error) {
|
|
2405
|
+
failed(error);
|
|
2406
|
+
process.exit(2);
|
|
2407
|
+
}
|
|
2408
|
+
process.on("uncaughtException", failedLater);
|
|
2409
|
+
process.on("unhandledRejection", failedLater);
|
|
2410
|
+
try {
|
|
2411
|
+
const exit = run2(process.argv.slice(2));
|
|
2412
|
+
if (typeof exit === "number") process.exitCode = exit;
|
|
2413
|
+
else exit.then((code) => process.exitCode = code, failedLater);
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
failed(error);
|
|
2416
|
+
}
|