attestari 2.9.3 → 2.9.4

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