synartesis 0.8.8 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,77 @@ var PROXY_FLAGS = [
19
19
  "--token"
20
20
  ];
21
21
 
22
+ // src/notify.ts
23
+ import { spawn } from "child_process";
24
+ import { platform } from "os";
25
+ var SILENT = () => void 0;
26
+ var NAME_MAX = 60;
27
+ function shown(name) {
28
+ const clean = name.replace(/[\u0000-\u001f\u007f‪-‮⁦-⁩]/g, "");
29
+ return clean.length > NAME_MAX ? `${clean.slice(0, NAME_MAX - 1)}\u2026` : clean;
30
+ }
31
+ function unmarked(text) {
32
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
33
+ }
34
+ function words(notice) {
35
+ return {
36
+ title: "Synartesis: a call is waiting for you",
37
+ body: `${shown(notice.server)}.${shown(notice.tool)} -- ${notice.approve}`
38
+ };
39
+ }
40
+ var KILL_AFTER_MS = 5e3;
41
+ function launch(command, args) {
42
+ try {
43
+ const child = spawn(command, [...args], { stdio: "ignore" });
44
+ child.on("error", () => void 0);
45
+ const timer = setTimeout(() => child.kill(), KILL_AFTER_MS);
46
+ timer.unref();
47
+ child.on("exit", () => {
48
+ clearTimeout(timer);
49
+ });
50
+ child.unref();
51
+ } catch {
52
+ }
53
+ }
54
+ function desktopNotifier(env = process.env, os = platform()) {
55
+ if (env["SYNARTESIS_NOTIFY"] === "0") {
56
+ return SILENT;
57
+ }
58
+ if (os === "darwin") {
59
+ return (notice) => {
60
+ const { title, body } = words(notice);
61
+ launch("osascript", [
62
+ "-e",
63
+ "on run argv",
64
+ "-e",
65
+ "display notification (item 2 of argv) with title (item 1 of argv)",
66
+ "-e",
67
+ "end run",
68
+ "--",
69
+ title,
70
+ body
71
+ ]);
72
+ };
73
+ }
74
+ if (os === "linux") {
75
+ return (notice) => {
76
+ const { title, body } = words(notice);
77
+ launch("notify-send", ["--app-name=Synartesis", "--", title, unmarked(body)]);
78
+ };
79
+ }
80
+ return SILENT;
81
+ }
82
+ function canNotify(env = process.env) {
83
+ if (env["SYNARTESIS_NOTIFY"] === "0") {
84
+ return "switched off by SYNARTESIS_NOTIFY=0";
85
+ }
86
+ const os = platform();
87
+ if (os === "darwin" || os === "linux") {
88
+ return void 0;
89
+ }
90
+ return `there is no notifier for ${os} yet; use synartesis watch to see what is waiting`;
91
+ }
92
+
22
93
  // src/invocation.ts
23
94
  import { spawnSync } from "child_process";
24
95
  import { accessSync, constants } from "fs";
@@ -202,6 +273,7 @@ var NOTHING_RECORDED_YET = [
202
273
  ];
203
274
 
204
275
  // src/journal/journal.ts
276
+ import { randomBytes } from "crypto";
205
277
  import { chmodSync, existsSync as existsSync2, mkdirSync } from "fs";
206
278
  import { hostname } from "os";
207
279
  import { dirname as dirname2 } from "path";
@@ -289,6 +361,84 @@ CREATE TABLE IF NOT EXISTS leases (
289
361
  claimed_at TEXT NOT NULL
290
362
  );
291
363
 
364
+ -- What each session's servers were started with, so an undo can tell whether
365
+ -- it is about to act on the same thing.
366
+ --
367
+ -- An undo starts the server again, reading its environment from the client
368
+ -- entry that wraps it. If that entry has changed since -- a memory server now
369
+ -- pointed at a different file -- the undo reaches a different store from the
370
+ -- one the session wrote to, sends its inverse there, and reports success. It
371
+ -- cannot tell, because nothing recorded what the session's server was given.
372
+ --
373
+ -- The working directory, and for every variable the client entry or the policy
374
+ -- declares, its name and a keyed HMAC of its value -- never the value. These
375
+ -- are mostly tokens, and a token is a different class of secret from the file
376
+ -- contents the rest of this journal holds: it must not land here in any form
377
+ -- that gives it back. A keyed hash does not, for anything with a token's
378
+ -- entropy, even to someone holding this file. The key is kept in the journal
379
+ -- itself (the secrets table below) rather than beside it: this file is already
380
+ -- the sensitive one and already owner-only, and a second file would be one
381
+ -- more thing to protect and to lose.
382
+ --
383
+ -- Not a schema version bump, for the reason the leases table above is not.
384
+ CREATE TABLE IF NOT EXISTS secrets (
385
+ name TEXT PRIMARY KEY,
386
+ value BLOB NOT NULL
387
+ );
388
+
389
+ CREATE TABLE IF NOT EXISTS run_servers (
390
+ run_id TEXT NOT NULL REFERENCES runs(id),
391
+ server TEXT NOT NULL,
392
+ cwd TEXT,
393
+ fingerprints TEXT NOT NULL,
394
+ PRIMARY KEY (run_id, server)
395
+ );
396
+
397
+ -- A person's no, kept apart from every other way a row ends up denied.
398
+ --
399
+ -- The status alone cannot say it. A spent approval is stored as denied, and so
400
+ -- is an approval the client stopped waiting for, and a desktop timeout -- some
401
+ -- of them with the approver's name on the row. So "has anybody refused this
402
+ -- exact call?" asked of the status would tell an agent that arhaan said no to
403
+ -- a call arhaan had approved. Only a person's deny writes here.
404
+ --
405
+ -- lifted_at is a person changing their mind: approving the same row afterwards
406
+ -- lifts the denial rather than leaving two contradictory answers standing.
407
+ --
408
+ -- Not a schema version bump, for the reason the tables above are not.
409
+ CREATE TABLE IF NOT EXISTS denials (
410
+ action_id TEXT PRIMARY KEY REFERENCES actions(id),
411
+ server TEXT NOT NULL,
412
+ tool TEXT NOT NULL,
413
+ denied_by TEXT NOT NULL,
414
+ reason TEXT NOT NULL,
415
+ denied_at TEXT NOT NULL,
416
+ lifted_at TEXT,
417
+ lifted_by TEXT
418
+ );
419
+ CREATE INDEX IF NOT EXISTS denials_recent ON denials(server, tool, denied_at);
420
+
421
+ -- A person saying "stop asking me about this tool" for a while.
422
+ --
423
+ -- Before this the only way to stop being asked was to edit the policy by hand
424
+ -- and restart the client, in the middle of whatever the agent was doing. This
425
+ -- takes effect on the next call, with no reload, and runs out by itself: a
426
+ -- yes that outlives the session it was given for is a yes nobody remembers
427
+ -- giving. stopped_at is the person taking it back before then.
428
+ --
429
+ -- Not a schema version bump, for the reason the tables above are not.
430
+ CREATE TABLE IF NOT EXISTS allows (
431
+ id INTEGER PRIMARY KEY,
432
+ server TEXT NOT NULL,
433
+ tool TEXT NOT NULL,
434
+ allowed_by TEXT NOT NULL,
435
+ allowed_at TEXT NOT NULL,
436
+ until TEXT NOT NULL,
437
+ stopped_at TEXT,
438
+ stopped_by TEXT
439
+ );
440
+ CREATE INDEX IF NOT EXISTS allows_current ON allows(server, tool, until);
441
+
292
442
  CREATE INDEX IF NOT EXISTS actions_by_run ON actions(run_id, seq);
293
443
 
294
444
  -- Deliberately not a schema version bump. Adding an index changes no row and
@@ -424,6 +574,26 @@ var runSchema = z.object({
424
574
  });
425
575
  var seenSchema = z.object({ server: z.string(), ts: z.string() });
426
576
  var countedSchema = z.object({ run_id: z.string(), n: z.number() });
577
+ var allowanceSchema = z.object({
578
+ server: z.string(),
579
+ tool: z.string(),
580
+ allowed_by: z.string(),
581
+ allowed_at: z.string(),
582
+ until: z.string()
583
+ });
584
+ function toAllowance(raw) {
585
+ const row = allowanceSchema.parse(raw);
586
+ return { server: row.server, tool: row.tool, by: row.allowed_by, at: row.allowed_at, until: row.until };
587
+ }
588
+ var denialSchema = z.object({
589
+ denied_by: z.string(),
590
+ reason: z.string(),
591
+ denied_at: z.string()
592
+ });
593
+ var runServerSchema = z.object({
594
+ cwd: z.string().nullable(),
595
+ fingerprints: z.string()
596
+ });
427
597
  var leaseSchema = z.object({
428
598
  action_id: z.string(),
429
599
  host: z.string(),
@@ -730,7 +900,7 @@ var SqliteJournal = class {
730
900
  * resource is contested.
731
901
  */
732
902
  markRollingBack(actionId, from = ["applied"]) {
733
- return this.#run("markRollingBack", () => {
903
+ return this.#run("markRollingBack", () => this.#db.transaction(() => {
734
904
  const slots = from.map(() => "?").join(",");
735
905
  const result = this.#db.prepare(
736
906
  `UPDATE actions SET status = 'rolling_back' WHERE id = ? AND status IN (${slots})`
@@ -744,7 +914,7 @@ var SqliteJournal = class {
744
914
  pid = excluded.pid, claimed_at = excluded.claimed_at`
745
915
  ).run(actionId, hostname(), process.pid, (/* @__PURE__ */ new Date()).toISOString());
746
916
  return true;
747
- });
917
+ }).immediate());
748
918
  }
749
919
  leaseFor(actionId) {
750
920
  return this.#run("leaseFor", () => {
@@ -1108,7 +1278,17 @@ var SqliteJournal = class {
1108
1278
  let actions = 0;
1109
1279
  const dropActions = this.#db.prepare("DELETE FROM actions WHERE run_id = ?");
1110
1280
  const dropRun = this.#db.prepare("DELETE FROM runs WHERE id = ?");
1281
+ const dropServers = this.#db.prepare("DELETE FROM run_servers WHERE run_id = ?");
1282
+ const dropLeases = this.#db.prepare(
1283
+ "DELETE FROM leases WHERE action_id IN (SELECT id FROM actions WHERE run_id = ?)"
1284
+ );
1285
+ const dropDenials = this.#db.prepare(
1286
+ "DELETE FROM denials WHERE action_id IN (SELECT id FROM actions WHERE run_id = ?)"
1287
+ );
1111
1288
  for (const id of ids) {
1289
+ dropServers.run(id);
1290
+ dropLeases.run(id);
1291
+ dropDenials.run(id);
1112
1292
  actions += dropActions.run(id).changes;
1113
1293
  runs += dropRun.run(id).changes;
1114
1294
  }
@@ -1117,6 +1297,131 @@ var SqliteJournal = class {
1117
1297
  return remove.immediate(runIds);
1118
1298
  });
1119
1299
  }
1300
+ denyByPerson(actionId, by, reason) {
1301
+ return this.#run(
1302
+ "denyByPerson",
1303
+ () => this.#db.transaction(() => {
1304
+ const changed = this.deny(actionId, by, reason);
1305
+ if (!changed) {
1306
+ return false;
1307
+ }
1308
+ const row = this.getAction(actionId);
1309
+ if (row === void 0) {
1310
+ return false;
1311
+ }
1312
+ this.#db.prepare(
1313
+ `INSERT INTO denials (action_id, server, tool, denied_by, reason, denied_at)
1314
+ VALUES (?, ?, ?, ?, ?, ?)`
1315
+ ).run(actionId, row.server, row.tool, by, reason, (/* @__PURE__ */ new Date()).toISOString());
1316
+ return true;
1317
+ }).immediate()
1318
+ );
1319
+ }
1320
+ findDenial(query) {
1321
+ return this.#run("findDenial", () => {
1322
+ const raws = this.#db.prepare(
1323
+ `SELECT a.*, d.denied_by, d.reason, d.denied_at
1324
+ FROM denials d INDEXED BY denials_recent
1325
+ JOIN actions a ON a.id = d.action_id
1326
+ WHERE d.server = ? AND d.tool = ? AND d.lifted_at IS NULL AND d.denied_at >= ?
1327
+ ORDER BY d.denied_at DESC`
1328
+ ).all(query.server, query.tool, query.notBefore);
1329
+ const rows = raws.map((raw) => ({ action: toAction(raw), denial: denialSchema.parse(raw) }));
1330
+ const found = sameCall(
1331
+ rows.map((row) => row.action),
1332
+ query.args
1333
+ );
1334
+ const hit = found === void 0 ? void 0 : rows.find((row) => row.action.id === found.id);
1335
+ return hit === void 0 ? void 0 : { action: hit.action, by: hit.denial.denied_by, reason: hit.denial.reason, at: hit.denial.denied_at };
1336
+ });
1337
+ }
1338
+ reverseDenial(actionId, by) {
1339
+ return this.#run(
1340
+ "reverseDenial",
1341
+ () => this.#db.transaction(() => {
1342
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1343
+ const standing2 = this.#db.prepare("SELECT 1 FROM denials WHERE action_id = ? AND lifted_at IS NULL").get(actionId);
1344
+ if (standing2 === void 0) {
1345
+ return false;
1346
+ }
1347
+ const flipped = this.#db.prepare(
1348
+ `UPDATE actions SET status = 'approved', approved_by = ?, approved_at = ?, error = NULL
1349
+ WHERE id = ? AND status = 'denied'`
1350
+ ).run(by, now, actionId);
1351
+ if (flipped.changes !== 1) {
1352
+ return false;
1353
+ }
1354
+ this.#db.prepare("UPDATE denials SET lifted_at = ?, lifted_by = ? WHERE action_id = ?").run(now, by, actionId);
1355
+ return true;
1356
+ }).immediate()
1357
+ );
1358
+ }
1359
+ allow(server, tool, by, until) {
1360
+ this.#run("allow", () => {
1361
+ this.#db.prepare(
1362
+ "INSERT INTO allows (server, tool, allowed_by, allowed_at, until) VALUES (?, ?, ?, ?, ?)"
1363
+ ).run(server, tool, by, (/* @__PURE__ */ new Date()).toISOString(), until);
1364
+ });
1365
+ }
1366
+ findAllowance(server, tool, now) {
1367
+ return this.#run("findAllowance", () => {
1368
+ const raw = this.#db.prepare(
1369
+ `SELECT * FROM allows INDEXED BY allows_current
1370
+ WHERE server = ? AND tool = ? AND until > ? AND stopped_at IS NULL
1371
+ ORDER BY until DESC LIMIT 1`
1372
+ ).get(server, tool, now);
1373
+ return raw === void 0 ? void 0 : toAllowance(raw);
1374
+ });
1375
+ }
1376
+ listAllowances(now) {
1377
+ return this.#run(
1378
+ "listAllowances",
1379
+ () => this.#db.prepare("SELECT * FROM allows WHERE until > ? AND stopped_at IS NULL ORDER BY until").all(now).map(toAllowance)
1380
+ );
1381
+ }
1382
+ stopAllowance(server, tool, by, now) {
1383
+ return this.#run("stopAllowance", () => {
1384
+ const result = this.#db.prepare(
1385
+ `UPDATE allows SET stopped_at = ?, stopped_by = ?
1386
+ WHERE server = ? AND tool = ? AND until > ? AND stopped_at IS NULL`
1387
+ ).run(now, by, server, tool, now);
1388
+ return result.changes > 0;
1389
+ });
1390
+ }
1391
+ markAllowed(actionId, by) {
1392
+ this.#run("markAllowed", () => {
1393
+ this.#db.prepare(
1394
+ "UPDATE actions SET approved_by = ?, approved_at = ? WHERE id = ? AND status = 'pending'"
1395
+ ).run(by, (/* @__PURE__ */ new Date()).toISOString(), actionId);
1396
+ });
1397
+ }
1398
+ recordRunServer(runId, server, cwd, fingerprints) {
1399
+ this.#run("recordRunServer", () => {
1400
+ this.#db.prepare(
1401
+ `INSERT INTO run_servers (run_id, server, cwd, fingerprints) VALUES (?, ?, ?, ?)
1402
+ ON CONFLICT(run_id, server) DO UPDATE SET cwd = excluded.cwd,
1403
+ fingerprints = excluded.fingerprints`
1404
+ ).run(runId, server, cwd ?? null, JSON.stringify(fingerprints));
1405
+ });
1406
+ }
1407
+ runServer(runId, server) {
1408
+ return this.#run("runServer", () => {
1409
+ const raw = this.#db.prepare("SELECT cwd, fingerprints FROM run_servers WHERE run_id = ? AND server = ?").get(runId, server);
1410
+ if (raw === void 0) {
1411
+ return void 0;
1412
+ }
1413
+ const row = runServerSchema.parse(raw);
1414
+ const fingerprints = z.record(z.string(), z.string()).parse(JSON.parse(row.fingerprints));
1415
+ return { ...row.cwd === null ? {} : { cwd: row.cwd }, fingerprints };
1416
+ });
1417
+ }
1418
+ fingerprintKey() {
1419
+ return this.#run("fingerprintKey", () => {
1420
+ this.#db.prepare("INSERT OR IGNORE INTO secrets (name, value) VALUES ('fingerprint', ?)").run(randomBytes(32));
1421
+ const row = z.object({ value: z.instanceof(Buffer) }).parse(this.#db.prepare("SELECT value FROM secrets WHERE name = 'fingerprint'").get());
1422
+ return row.value;
1423
+ });
1424
+ }
1120
1425
  vacuum() {
1121
1426
  this.#run("vacuum", () => {
1122
1427
  this.#db.exec("VACUUM");
@@ -1224,7 +1529,7 @@ function walk(current, parts, at, reference) {
1224
1529
  return walk(current[segment.index], parts, at + 1, reference);
1225
1530
  }
1226
1531
  case "key": {
1227
- if (typeof current !== "object" || !(segment.key in current)) {
1532
+ if (typeof current !== "object" || !Object.hasOwn(current, segment.key)) {
1228
1533
  throw new ManifestError(`${reference} is unresolvable: ${segment.key} is absent`);
1229
1534
  }
1230
1535
  const next = Object.getOwnPropertyDescriptor(current, segment.key)?.value;
@@ -1333,11 +1638,39 @@ var serverSpec = z2.strictObject({
1333
1638
  command: z2.string().min(1),
1334
1639
  args: z2.array(z2.string()).default([]),
1335
1640
  env: z2.record(z2.string(), z2.string()).optional(),
1336
- provenance: z2.enum(["live", "documented"]).optional()
1641
+ provenance: z2.enum(["live", "documented"]).optional(),
1642
+ trust_annotations: z2.boolean().optional()
1643
+ });
1644
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1645
+ var remoteSpec = z2.strictObject({
1646
+ url: z2.string().refine((value) => URL.canParse(value), "url must be a full address, like https://example.com/mcp").refine((value) => {
1647
+ const url = new URL(value);
1648
+ return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK.has(url.hostname);
1649
+ }, "url must be https, since it carries a token; plain http is accepted only for this machine"),
1650
+ transport: z2.enum(["auto", "http", "sse"]).default("auto"),
1651
+ headers: z2.record(z2.string(), z2.string()).optional(),
1652
+ env: z2.record(z2.string(), z2.string()).optional(),
1653
+ provenance: z2.enum(["live", "documented"]).optional(),
1654
+ trust_annotations: z2.boolean().optional()
1655
+ });
1656
+ var anyServer = z2.unknown().transform((value, context) => {
1657
+ const has = (key) => typeof value === "object" && value !== null && key in value;
1658
+ if (has("command") && has("url")) {
1659
+ context.addIssue({ code: "custom", message: "give command or url, not both" });
1660
+ return z2.NEVER;
1661
+ }
1662
+ const parsed = (has("url") ? remoteSpec : serverSpec).safeParse(value);
1663
+ if (!parsed.success) {
1664
+ for (const issue of parsed.error.issues) {
1665
+ context.addIssue({ ...issue, code: "custom", message: issue.message });
1666
+ }
1667
+ return z2.NEVER;
1668
+ }
1669
+ return parsed.data;
1337
1670
  });
1338
1671
  var manifestSchema = z2.strictObject({
1339
1672
  version: z2.literal(1),
1340
- servers: z2.record(z2.string(), serverSpec),
1673
+ servers: z2.record(z2.string(), anyServer),
1341
1674
  tools: z2.array(toolPolicy).default([]),
1342
1675
  pins: z2.record(z2.string(), z2.record(z2.string(), z2.string().min(1))).optional()
1343
1676
  });
@@ -1366,22 +1699,17 @@ var Source = class {
1366
1699
  throw new ManifestError(message, this.locate(path));
1367
1700
  }
1368
1701
  };
1369
- var REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
1370
- function expandEnvironment(source, path, env) {
1371
- const expanded = {};
1702
+ var MALFORMED = /\$\{(?![A-Za-z_][A-Za-z0-9_]*\})/;
1703
+ function checkReferences(source, path, env, field = "env") {
1372
1704
  for (const [key, value] of Object.entries(env)) {
1373
- expanded[key] = value.replace(REFERENCE, (whole, name) => {
1374
- const found = process.env[name];
1375
- if (found === void 0) {
1376
- source.fail(
1377
- [...path, "env", key],
1378
- `${whole} is not set in this environment; export ${name} before starting, or write the value here`
1379
- );
1380
- }
1381
- return found;
1382
- });
1705
+ if (MALFORMED.test(value)) {
1706
+ source.fail(
1707
+ [...path, field, key],
1708
+ "a reference is written ${NAME}: letters, digits and underscores, not starting with a digit"
1709
+ );
1710
+ }
1383
1711
  }
1384
- return expanded;
1712
+ return env;
1385
1713
  }
1386
1714
  function serverSegment(pattern) {
1387
1715
  const dot = pattern.indexOf(".");
@@ -1395,6 +1723,47 @@ function matchesAnyServer(segment, servers) {
1395
1723
  const test = new RegExp(`^${source}$`);
1396
1724
  return servers.some((name) => test.test(name));
1397
1725
  }
1726
+ function stringsIn(value, path) {
1727
+ if (typeof value === "string") {
1728
+ return [[path, value]];
1729
+ }
1730
+ if (Array.isArray(value)) {
1731
+ return value.flatMap((item, index) => stringsIn(item, [...path, index]));
1732
+ }
1733
+ if (value !== null && typeof value === "object") {
1734
+ return Object.entries(value).flatMap(([key, item]) => stringsIn(item, [...path, key]));
1735
+ }
1736
+ return [];
1737
+ }
1738
+ function spelled(inner) {
1739
+ const trimmed = inner.trim().replace(/^\$/, "");
1740
+ for (const [from, to] of [["args.", "$."], ["snapshot.", "$snapshot."], ["result.", "$result."]]) {
1741
+ if (trimmed.startsWith(from)) {
1742
+ return `"${to}${trimmed.slice(from.length)}"`;
1743
+ }
1744
+ }
1745
+ return `"$.${trimmed}"`;
1746
+ }
1747
+ function checkSpelling(source, path, call) {
1748
+ if (call === void 0) {
1749
+ return;
1750
+ }
1751
+ for (const [at, text] of stringsIn(call.args, [...path, "args"])) {
1752
+ const braces = /\{\{\s*([^}]*?)\s*\}\}/.exec(text);
1753
+ if (braces !== null) {
1754
+ source.fail(at, `${braces[0]} is sent as written; write ${spelled(braces[1] ?? "")} instead`);
1755
+ }
1756
+ const dollar = /^\$\{([^}]*)\}$/.exec(text);
1757
+ if (dollar !== null) {
1758
+ source.fail(at, `${text} is not a reference here; write ${spelled(dollar[1] ?? "")} instead`);
1759
+ }
1760
+ try {
1761
+ referencesIn(text);
1762
+ } catch (error) {
1763
+ source.fail(at, error instanceof Error ? error.message : String(error));
1764
+ }
1765
+ }
1766
+ }
1398
1767
  function checkCall(source, path, call, servers, allowed) {
1399
1768
  const segment = serverSegment(call.tool);
1400
1769
  if (segment === "" || call.tool.endsWith(".")) {
@@ -1438,6 +1807,9 @@ function validate(source, manifest) {
1438
1807
  );
1439
1808
  }
1440
1809
  seen.set(policy.match, index);
1810
+ checkSpelling(source, [...path, "snapshot"], policy.snapshot);
1811
+ checkSpelling(source, [...path, "inverse"], policy.inverse);
1812
+ checkSpelling(source, [...path, "verify"], policy.verify);
1441
1813
  const segment = serverSegment(policy.match);
1442
1814
  if (segment === "") {
1443
1815
  source.fail([...path, "match"], `${policy.match} must be qualified as server.tool`);
@@ -1550,10 +1922,14 @@ function parseManifest(text, file) {
1550
1922
  Object.entries(parsed.data.servers).map(([name, spec]) => [
1551
1923
  name,
1552
1924
  {
1553
- command: spec.command,
1554
- args: spec.args,
1555
- ...spec.env === void 0 ? {} : { env: expandEnvironment(source, ["servers", name], spec.env) },
1556
- ...spec.provenance === void 0 ? {} : { provenance: spec.provenance }
1925
+ ..."url" in spec ? {
1926
+ url: spec.url,
1927
+ transport: spec.transport,
1928
+ ...spec.headers === void 0 ? {} : { headers: checkReferences(source, ["servers", name], spec.headers, "headers") }
1929
+ } : { command: spec.command, args: spec.args },
1930
+ ...spec.env === void 0 ? {} : { env: checkReferences(source, ["servers", name], spec.env) },
1931
+ ...spec.provenance === void 0 ? {} : { provenance: spec.provenance },
1932
+ ...spec.trust_annotations === void 0 ? {} : { trustAnnotations: spec.trust_annotations }
1557
1933
  }
1558
1934
  ])
1559
1935
  ),
@@ -1581,9 +1957,6 @@ function loadManifest(path) {
1581
1957
  return parseManifest(text, path);
1582
1958
  }
1583
1959
 
1584
- // src/manifest/pin.ts
1585
- import { createHash } from "crypto";
1586
-
1587
1960
  // src/manifest/match.ts
1588
1961
  function toRegExp(pattern) {
1589
1962
  const source = pattern.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
@@ -1618,6 +1991,9 @@ function createPolicyResolver(manifest) {
1618
1991
  }
1619
1992
 
1620
1993
  // src/manifest/types.ts
1994
+ function isRemote(spec) {
1995
+ return spec.url !== void 0;
1996
+ }
1621
1997
  function qualify(server, tool) {
1622
1998
  return `${server}.${tool}`;
1623
1999
  }
@@ -1630,6 +2006,7 @@ function splitQualified(qualified) {
1630
2006
  }
1631
2007
 
1632
2008
  // src/manifest/pin.ts
2009
+ import { createHash } from "crypto";
1633
2010
  function fingerprint(inputSchema) {
1634
2011
  return `sha256:${createHash("sha256").update(canonical(inputSchema)).digest("hex")}`;
1635
2012
  }
@@ -1640,9 +2017,9 @@ function auditPins(server, advertised, manifest) {
1640
2017
  }
1641
2018
  const resolver = createPolicyResolver(manifest);
1642
2019
  const faults = [];
1643
- const present = /* @__PURE__ */ new Set();
2020
+ const present2 = /* @__PURE__ */ new Set();
1644
2021
  for (const tool of advertised) {
1645
- present.add(tool.name);
2022
+ present2.add(tool.name);
1646
2023
  if (!resolver.resolve(qualify(server, tool.name)).matched) {
1647
2024
  continue;
1648
2025
  }
@@ -1655,7 +2032,7 @@ function auditPins(server, advertised, manifest) {
1655
2032
  }
1656
2033
  }
1657
2034
  for (const name of Object.keys(pins)) {
1658
- if (!present.has(name)) {
2035
+ if (!present2.has(name)) {
1659
2036
  faults.push({ kind: "gone", tool: name });
1660
2037
  }
1661
2038
  }
@@ -1694,8 +2071,19 @@ function explainPins(server, faults) {
1694
2071
 
1695
2072
  // src/manifest/verify.ts
1696
2073
  import { z as z3 } from "zod";
2074
+ async function listAll(upstreams) {
2075
+ return new Map(
2076
+ await Promise.all(upstreams.map(async (upstream) => [upstream.name, await toolShapes(upstream)]))
2077
+ );
2078
+ }
1697
2079
  var listSchema = z3.looseObject({
1698
- tools: z3.array(z3.looseObject({ name: z3.string(), inputSchema: z3.unknown() })),
2080
+ tools: z3.array(
2081
+ z3.looseObject({
2082
+ name: z3.string(),
2083
+ inputSchema: z3.unknown(),
2084
+ annotations: z3.looseObject({ readOnlyHint: z3.unknown().optional() }).optional().catch(void 0)
2085
+ })
2086
+ ),
1699
2087
  nextCursor: z3.string().optional()
1700
2088
  });
1701
2089
  async function toolShapes(upstream) {
@@ -1709,17 +2097,21 @@ async function toolShapes(upstream) {
1709
2097
  )
1710
2098
  );
1711
2099
  for (const tool of page.tools) {
1712
- shapes.push({ name: tool.name, inputSchema: tool.inputSchema });
2100
+ shapes.push({
2101
+ name: tool.name,
2102
+ inputSchema: tool.inputSchema,
2103
+ ...tool.annotations?.readOnlyHint === true ? { readOnly: true } : {}
2104
+ });
1713
2105
  }
1714
2106
  cursor = page.nextCursor;
1715
2107
  } while (cursor !== void 0);
1716
2108
  return shapes;
1717
2109
  }
1718
- async function verifyAgainstServers(upstreams, manifest) {
2110
+ async function verifyAgainstServers(upstreams, manifest, listed) {
1719
2111
  const shapes = /* @__PURE__ */ new Map();
1720
2112
  const available = /* @__PURE__ */ new Map();
1721
2113
  for (const upstream of upstreams) {
1722
- const advertised = await toolShapes(upstream);
2114
+ const advertised = listed?.get(upstream.name) ?? await toolShapes(upstream);
1723
2115
  shapes.set(upstream.name, advertised);
1724
2116
  available.set(upstream.name, new Set(advertised.map((tool) => tool.name)));
1725
2117
  }
@@ -1766,6 +2158,37 @@ async function verifyAgainstServers(upstreams, manifest) {
1766
2158
  );
1767
2159
  }
1768
2160
  }
2161
+ async function withoutMissingTools(upstreams, manifest, listed) {
2162
+ const available = /* @__PURE__ */ new Map();
2163
+ for (const upstream of upstreams) {
2164
+ const advertised = listed?.get(upstream.name) ?? await toolShapes(upstream);
2165
+ available.set(upstream.name, new Set(advertised.map((tool) => tool.name)));
2166
+ }
2167
+ const missing = (qualified) => {
2168
+ const target = splitQualified(qualified);
2169
+ if (target === void 0) {
2170
+ return false;
2171
+ }
2172
+ const names = available.get(target.server);
2173
+ return names !== void 0 && !names.has(target.tool);
2174
+ };
2175
+ const disabled = [];
2176
+ const tools = manifest.tools.map((policy) => {
2177
+ const gone = [
2178
+ ["snapshot", policy.snapshot?.tool],
2179
+ ["inverse", policy.inverse?.tool],
2180
+ ["verify", policy.verify?.tool]
2181
+ ].filter((pair) => pair[1] !== void 0 && missing(pair[1]));
2182
+ if (gone.length === 0) {
2183
+ return policy;
2184
+ }
2185
+ disabled.push(
2186
+ `${policy.match}: its ${gone.map(([role, tool]) => `${role} ${tool}`).join(" and ")} ${gone.length === 1 ? "is" : "are"} gone from the server, so it is held until the policy is updated`
2187
+ );
2188
+ return { match: policy.match, class: "irreversible", gate: "always", refusal: "uncertain" };
2189
+ });
2190
+ return { manifest: { ...manifest, tools }, disabled };
2191
+ }
1769
2192
 
1770
2193
  // src/manifest/standing.ts
1771
2194
  function standing(manifest) {
@@ -1793,6 +2216,9 @@ function warnUntested(servers) {
1793
2216
  const these = servers.length === 1 ? "this policy has" : "these policies have";
1794
2217
  return `${names}: ${these} never been run against the real server. The classes and inverses here come from documentation, so undo may not work where it says it will. Run \`synartesis check\` against your own credentials, and expect to correct something.`;
1795
2218
  }
2219
+ function trustsMarks(manifest, server) {
2220
+ return manifest.servers[server]?.trustAnnotations !== false && manifest.pins?.[server] === void 0;
2221
+ }
1796
2222
  function ungoverned(manifest, advertised) {
1797
2223
  const resolver = createPolicyResolver(manifest);
1798
2224
  const found = [];
@@ -1861,9 +2287,73 @@ function createRouter(upstreams, manifest) {
1861
2287
  };
1862
2288
  }
1863
2289
 
2290
+ // src/proxy/environment.ts
2291
+ import { createHmac } from "crypto";
2292
+ var LAUNCHER = /^(SYNARTESIS_|npm_)/;
2293
+ var LAUNCHER_EXACT = /* @__PURE__ */ new Set(["INIT_CWD", "NODE"]);
2294
+ function ownedByTheLauncher(name) {
2295
+ return LAUNCHER.test(name) || LAUNCHER_EXACT.has(name);
2296
+ }
2297
+ var REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
2298
+ function expandReferences(server, key, value, lookup) {
2299
+ return value.replace(REFERENCE, (whole, name) => {
2300
+ const found = lookup(name);
2301
+ if (found === void 0) {
2302
+ throw new ManifestError(
2303
+ `server ${server} needs ${whole} for ${key}, and it is not set; set ${name} where this server is started, or write the value in the policy`
2304
+ );
2305
+ }
2306
+ return found;
2307
+ });
2308
+ }
2309
+ function present(env) {
2310
+ const out = {};
2311
+ for (const [key, value] of Object.entries(env)) {
2312
+ if (value !== void 0) {
2313
+ out[key] = value;
2314
+ }
2315
+ }
2316
+ return out;
2317
+ }
2318
+ function upstreamEnv(server, spec, source) {
2319
+ const own = Object.fromEntries(
2320
+ Object.entries(present(process.env)).filter(([name]) => !ownedByTheLauncher(name))
2321
+ );
2322
+ const base = source.kind === "inherit" ? own : source.kind === "client" ? { ...source.own === false ? {} : own, ...source.env } : {};
2323
+ const lookup = (name) => base[name] ?? process.env[name];
2324
+ const declared = {};
2325
+ for (const [key, value] of Object.entries(spec.env ?? {})) {
2326
+ declared[key] = expandReferences(server, key, value, lookup);
2327
+ }
2328
+ if (source.kind === "manifest") {
2329
+ return spec.env === void 0 ? void 0 : declared;
2330
+ }
2331
+ return { ...base, ...declared };
2332
+ }
2333
+ var ABSENT = "-";
2334
+ function fingerprint2(key, env, names) {
2335
+ const out = {};
2336
+ for (const name of [...new Set(names)].sort()) {
2337
+ const value = env?.[name];
2338
+ out[name] = value === void 0 ? ABSENT : createHmac("sha256", key).update(value).digest("hex");
2339
+ }
2340
+ return out;
2341
+ }
2342
+ function differing(recorded, now) {
2343
+ return Object.keys(recorded).filter((name) => recorded[name] !== now[name]).sort();
2344
+ }
2345
+ function declaredNames(spec, client) {
2346
+ return [.../* @__PURE__ */ new Set([...Object.keys(spec.env ?? {}), ...Object.keys(client ?? {})])].sort();
2347
+ }
2348
+
1864
2349
  // src/proxy/upstream.ts
1865
2350
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2351
+ import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js";
1866
2352
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2353
+ import {
2354
+ StreamableHTTPClientTransport,
2355
+ StreamableHTTPError
2356
+ } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1867
2357
  function describeError(error) {
1868
2358
  return error instanceof Error ? error.message : String(error);
1869
2359
  }
@@ -1874,10 +2364,10 @@ function settled(stream, ms) {
1874
2364
  if (stream.readableEnded === true) {
1875
2365
  return Promise.resolve();
1876
2366
  }
1877
- return new Promise((resolve2) => {
2367
+ return new Promise((resolve5) => {
1878
2368
  const done = () => {
1879
2369
  clearTimeout(timer);
1880
- resolve2();
2370
+ resolve5();
1881
2371
  };
1882
2372
  const timer = setTimeout(done, ms);
1883
2373
  timer.unref?.();
@@ -1915,8 +2405,22 @@ function lastWords(text) {
1915
2405
  if (named !== void 0) {
1916
2406
  return named;
1917
2407
  }
1918
- const kept = lines.slice(-4).join("; ");
1919
- return kept === "" ? void 0 : kept;
2408
+ const kept2 = lines.slice(-4).join("; ");
2409
+ return kept2 === "" ? void 0 : kept2;
2410
+ }
2411
+ async function connectUpstream(name, spec, options) {
2412
+ const env = upstreamEnv(name, spec, options.env);
2413
+ if (isRemote(spec)) {
2414
+ return await connectRemoteUpstream(name, spec, (variable) => env?.[variable] ?? process.env[variable]);
2415
+ }
2416
+ return await connectStdioUpstream({
2417
+ name,
2418
+ command: spec.command,
2419
+ args: spec.args,
2420
+ ...env === void 0 ? {} : { env },
2421
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd },
2422
+ ...options.stderr === void 0 ? {} : { stderr: options.stderr }
2423
+ });
1920
2424
  }
1921
2425
  async function connectStdioUpstream(spec) {
1922
2426
  const started = await start(spec);
@@ -1941,6 +2445,7 @@ async function start(spec) {
1941
2445
  command: spec.command,
1942
2446
  args: [...spec.args ?? []],
1943
2447
  ...spec.env === void 0 ? {} : { env: { ...spec.env } },
2448
+ ...spec.cwd === void 0 ? {} : { cwd: spec.cwd },
1944
2449
  // "pipe" is what the sdk calls it; captured here so a failure can quote it.
1945
2450
  stderr: wanted === "capture" ? "pipe" : wanted
1946
2451
  });
@@ -1970,39 +2475,1309 @@ async function start(spec) {
1970
2475
  }
1971
2476
  return { client };
1972
2477
  }
1973
-
1974
- // src/idempotency.ts
1975
- var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
1976
- function withIdempotencyKey(meta, key) {
1977
- const merged = {};
1978
- if (typeof meta === "object" && meta !== null) {
1979
- for (const [name, value] of Object.entries(meta)) {
1980
- merged[name] = value;
2478
+ async function connectRemoteUpstream(name, spec, lookup) {
2479
+ const headers = {};
2480
+ for (const [key, value] of Object.entries(spec.headers ?? {})) {
2481
+ headers[key] = expandReferences(name, key, value, lookup);
2482
+ }
2483
+ const url = new URL(spec.url);
2484
+ const requestInit = { headers, redirect: "error" };
2485
+ const open = async (kind2) => {
2486
+ const client = new Client({ ...PROXY_CLIENT_INFO });
2487
+ if (kind2 === "http") {
2488
+ await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
2489
+ } else {
2490
+ await client.connect(new SSEClientTransport(url, { requestInit }));
2491
+ }
2492
+ return client;
2493
+ };
2494
+ let kind = spec.transport === "sse" ? "sse" : "http";
2495
+ let current;
2496
+ try {
2497
+ current = await open(kind);
2498
+ } catch (error) {
2499
+ const code = statusOf(error);
2500
+ if (spec.transport === "auto" && (code === 400 || code === 404 || code === 405)) {
2501
+ kind = "sse";
2502
+ try {
2503
+ current = await open(kind);
2504
+ } catch (fallback) {
2505
+ throw new UpstreamError(name, "connect", fallback);
2506
+ }
2507
+ } else {
2508
+ throw new UpstreamError(name, "connect", refusal(error) ?? error);
1981
2509
  }
1982
2510
  }
1983
- merged[IDEMPOTENCY_META_KEY] = key;
1984
- return merged;
2511
+ return {
2512
+ name,
2513
+ get client() {
2514
+ return current;
2515
+ },
2516
+ async reconnect() {
2517
+ await current.close().catch(() => void 0);
2518
+ current = await open(kind);
2519
+ },
2520
+ classify(error) {
2521
+ const code = statusOf(error);
2522
+ if (code === void 0 || code < 400 || code >= 500) {
2523
+ return void 0;
2524
+ }
2525
+ return code === 404 ? "lost" : "not-sent";
2526
+ },
2527
+ close: async () => {
2528
+ await current.close();
2529
+ }
2530
+ };
2531
+ }
2532
+ function statusOf(error) {
2533
+ if (error instanceof StreamableHTTPError || error instanceof SseError) {
2534
+ return typeof error.code === "number" && error.code > 0 ? error.code : void 0;
2535
+ }
2536
+ return void 0;
2537
+ }
2538
+ function refusal(error) {
2539
+ const code = statusOf(error);
2540
+ if (code === 401 || code === 403) {
2541
+ return `the server refused the credentials it was given (HTTP ${String(code)}); check the token its headers name`;
2542
+ }
2543
+ return void 0;
2544
+ }
2545
+ async function startTogether(items, start2) {
2546
+ const settled2 = await Promise.allSettled(
2547
+ items.map(async (item) => ({ item, upstream: await start2(item) }))
2548
+ );
2549
+ const started = [];
2550
+ const failed = [];
2551
+ items.forEach((item, index) => {
2552
+ const result = settled2[index];
2553
+ if (result?.status === "fulfilled") {
2554
+ started.push(result.value.upstream);
2555
+ } else if (result !== void 0) {
2556
+ failed.push({ item, error: result.reason });
2557
+ }
2558
+ });
2559
+ return { started, failed };
2560
+ }
2561
+ async function startAll(items, start2) {
2562
+ const { started, failed } = await startTogether(items, start2);
2563
+ const first = failed[0];
2564
+ if (first !== void 0) {
2565
+ await Promise.all(started.map((upstream) => upstream.close().catch(() => void 0)));
2566
+ throw first.error;
2567
+ }
2568
+ return started;
1985
2569
  }
1986
2570
 
1987
- // src/proxy/snapshot.ts
1988
- import { z as z4 } from "zod";
1989
- var ToolResult = z4.looseObject({
1990
- isError: z4.boolean().default(false),
1991
- content: z4.array(z4.looseObject({ type: z4.string() })).default([])
1992
- });
1993
- function refusal(result) {
1994
- const parsed = ToolResult.safeParse(result);
1995
- if (!parsed.success || !parsed.data.isError) {
2571
+ // src/install/clients.ts
2572
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
2573
+ import { homedir as homedir2, platform as platform2 } from "os";
2574
+ import { basename as basename2, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
2575
+
2576
+ // src/install/toml.ts
2577
+ var HEADER = /^\s*\[(?!\[)([^[\]]+)\]\s*$/;
2578
+ function serverTables(lines) {
2579
+ const tables = [];
2580
+ let open;
2581
+ const close = (at) => {
2582
+ if (open !== void 0) {
2583
+ tables.push({ name: open.name, start: open.start, end: at });
2584
+ open = void 0;
2585
+ }
2586
+ };
2587
+ lines.forEach((line, index) => {
2588
+ const header = HEADER.exec(line)?.[1];
2589
+ if (header === void 0) {
2590
+ return;
2591
+ }
2592
+ const parts = header.split(".");
2593
+ if (parts[0] === "mcp_servers" && parts.length === 2 && parts[1] !== void 0) {
2594
+ close(index);
2595
+ open = { name: unquote(parts[1]), start: index };
2596
+ return;
2597
+ }
2598
+ close(index);
2599
+ });
2600
+ close(lines.length);
2601
+ return tables;
2602
+ }
2603
+ function unquote(text) {
2604
+ const trimmed = text.trim();
2605
+ if (/^'.*'$/s.test(trimmed)) {
2606
+ return trimmed.slice(1, -1);
2607
+ }
2608
+ if (!/^".*"$/s.test(trimmed)) {
2609
+ return trimmed;
2610
+ }
2611
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
2612
+ }
2613
+ function readKey(lines, table, key) {
2614
+ const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`);
2615
+ for (let index = table.start + 1; index < table.end; index += 1) {
2616
+ const line = lines[index];
2617
+ if (line === void 0 || HEADER.test(line)) {
2618
+ break;
2619
+ }
2620
+ const value = pattern.exec(line)?.[1];
2621
+ if (value !== void 0) {
2622
+ return value.trim();
2623
+ }
2624
+ }
2625
+ return void 0;
2626
+ }
2627
+ function splitItems(inner) {
2628
+ const items = [];
2629
+ let current = "";
2630
+ let quote3;
2631
+ let escaped = false;
2632
+ for (const character of inner) {
2633
+ if (escaped) {
2634
+ current += character;
2635
+ escaped = false;
2636
+ continue;
2637
+ }
2638
+ if (character === "\\" && quote3 === '"') {
2639
+ current += character;
2640
+ escaped = true;
2641
+ continue;
2642
+ }
2643
+ if (quote3 === void 0 && (character === '"' || character === "'")) {
2644
+ quote3 = character;
2645
+ current += character;
2646
+ continue;
2647
+ }
2648
+ if (character === quote3) {
2649
+ quote3 = void 0;
2650
+ current += character;
2651
+ continue;
2652
+ }
2653
+ if (character === "," && quote3 === void 0) {
2654
+ items.push(current);
2655
+ current = "";
2656
+ continue;
2657
+ }
2658
+ current += character;
2659
+ }
2660
+ items.push(current);
2661
+ return items;
2662
+ }
2663
+ function parseArray(value) {
2664
+ if (value === void 0 || !value.startsWith("[")) {
1996
2665
  return void 0;
1997
2666
  }
1998
- const said = parsed.data.content.map((block) => typeof block["text"] === "string" ? block["text"] : "").filter((text) => text !== "").join(" ");
1999
- return said === "" ? JSON.stringify(result) : said;
2667
+ if (!value.endsWith("]")) {
2668
+ return void 0;
2669
+ }
2670
+ const inner = value.slice(1, -1).trim();
2671
+ if (inner === "") {
2672
+ return [];
2673
+ }
2674
+ return splitItems(inner).map((item) => item.trim()).filter((item, index, all) => item !== "" || index !== all.length - 1).map(unquote);
2675
+ }
2676
+ function readServers(text) {
2677
+ const lines = text.split("\n");
2678
+ const servers = {};
2679
+ for (const table of serverTables(lines)) {
2680
+ const command = readKey(lines, table, "command");
2681
+ const entry = {};
2682
+ if (command !== void 0) {
2683
+ entry["command"] = unquote(command);
2684
+ }
2685
+ const rawArgs = readKey(lines, table, "args");
2686
+ const args = parseArray(rawArgs);
2687
+ if (args !== void 0) {
2688
+ entry["args"] = args;
2689
+ } else if (rawArgs !== void 0) {
2690
+ entry["unreadable"] = "its args array spans several lines, which this cannot read exactly";
2691
+ }
2692
+ const url = readKey(lines, table, "url");
2693
+ if (url !== void 0) {
2694
+ entry["url"] = unquote(url);
2695
+ }
2696
+ const enabled = readKey(lines, table, "enabled");
2697
+ if (enabled !== void 0) {
2698
+ entry["enabled"] = enabled.trim() === "true";
2699
+ }
2700
+ servers[table.name] = entry;
2701
+ }
2702
+ return servers;
2703
+ }
2704
+ var quote = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
2705
+ function writeServers(text, servers) {
2706
+ const lines = text.split("\n");
2707
+ const current = readServers(text);
2708
+ for (const table of serverTables(lines).reverse()) {
2709
+ const wanted = servers[table.name];
2710
+ if (wanted === void 0 || wanted.command === void 0) {
2711
+ continue;
2712
+ }
2713
+ const now = current[table.name];
2714
+ const unreadable = now?.["unreadable"];
2715
+ if (typeof unreadable === "string") {
2716
+ throw new Error(`cannot rewrite [mcp_servers.${table.name}]: ${unreadable}`);
2717
+ }
2718
+ if (now?.command === wanted.command && JSON.stringify(now.args ?? []) === JSON.stringify(wanted.args ?? [])) {
2719
+ continue;
2720
+ }
2721
+ setKey(lines, table, "command", quote(wanted.command));
2722
+ setKey(lines, table, "args", `[${(wanted.args ?? []).map(quote).join(", ")}]`);
2723
+ }
2724
+ return lines.join("\n");
2000
2725
  }
2001
- function isRecord(value) {
2726
+ function setKey(lines, table, key, value) {
2727
+ const pattern = new RegExp(`^(\\s*)${key}\\s*=`);
2728
+ for (let index = table.start + 1; index < table.end; index += 1) {
2729
+ const line = lines[index];
2730
+ if (line === void 0 || HEADER.test(line)) {
2731
+ break;
2732
+ }
2733
+ const indent = pattern.exec(line)?.[1];
2734
+ if (indent !== void 0) {
2735
+ lines[index] = `${indent}${key} = ${value}`;
2736
+ return;
2737
+ }
2738
+ }
2739
+ lines.splice(table.start + 1, 0, `${key} = ${value}`);
2740
+ }
2741
+
2742
+ // src/install/clients.ts
2743
+ var LABELS = {
2744
+ "claude-code": "Claude Code",
2745
+ "claude-desktop": "Claude Desktop",
2746
+ cursor: "Cursor",
2747
+ codex: "Codex",
2748
+ devin: "Devin Desktop",
2749
+ windsurf: "Windsurf",
2750
+ "gemini-cli": "Gemini CLI",
2751
+ "copilot-cli": "Copilot CLI",
2752
+ antigravity: "Antigravity"
2753
+ };
2754
+ var LOOKED_FOR = `Looked for ${Object.values(LABELS).slice(0, -1).join(", ")} and ${Object.values(LABELS).slice(-1).join("")}.`;
2755
+ var CLIENT_IDS = Object.keys(LABELS).filter(
2756
+ (name) => name in LABELS
2757
+ );
2758
+ function isClientId(value) {
2759
+ return CLIENT_IDS.some((known) => known === value);
2760
+ }
2761
+ function claudeDesktopPath() {
2762
+ const home2 = homedir2();
2763
+ switch (platform2()) {
2764
+ case "darwin":
2765
+ return join3(home2, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2766
+ case "win32":
2767
+ return join3(process.env["APPDATA"] ?? join3(home2, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2768
+ default:
2769
+ return join3(process.env["XDG_CONFIG_HOME"] ?? join3(home2, ".config"), "Claude", "claude_desktop_config.json");
2770
+ }
2771
+ }
2772
+ function expandForClient(client, value) {
2773
+ if (client === "devin" || client === "windsurf") {
2774
+ return value.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_whole, name) => process.env[name] ?? "").replace(/\{\{env:([A-Za-z_][A-Za-z0-9_]*)\}\}/g, (_whole, name) => process.env[name] ?? "");
2775
+ }
2776
+ if (client === "gemini-cli") {
2777
+ return value.replace(
2778
+ /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g,
2779
+ (_whole, braced, bare) => process.env[braced ?? bare ?? ""] ?? ""
2780
+ );
2781
+ }
2782
+ if (client === "claude-code") {
2783
+ return value.replace(
2784
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
2785
+ (whole, name, fallback) => process.env[name] ?? fallback ?? whole
2786
+ );
2787
+ }
2788
+ if (client === "cursor") {
2789
+ return value.replace(
2790
+ /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,
2791
+ (whole, name) => process.env[name] ?? whole
2792
+ );
2793
+ }
2794
+ return value;
2795
+ }
2796
+ function discover(cwd) {
2797
+ const home2 = homedir2();
2798
+ const sites = [];
2799
+ const claudeCode = join3(home2, ".claude.json");
2800
+ if (existsSync3(claudeCode)) {
2801
+ const document = readJson(claudeCode);
2802
+ const projects = document?.["projects"];
2803
+ const here = resolve2(cwd);
2804
+ if (isRecord(projects)) {
2805
+ const others = Object.keys(projects).filter((path) => path !== here).sort();
2806
+ for (const project of Object.prototype.hasOwnProperty.call(projects, here) ? [here, ...others] : others) {
2807
+ const entry = projects[project];
2808
+ const servers = isRecord(entry) ? entry["mcpServers"] : void 0;
2809
+ if (project !== here && !(isRecord(servers) && Object.keys(servers).length > 0)) {
2810
+ continue;
2811
+ }
2812
+ sites.push({
2813
+ client: "claude-code",
2814
+ label: LABELS["claude-code"],
2815
+ format: "json",
2816
+ path: claudeCode,
2817
+ scope: `project ${project}`,
2818
+ at: ["projects", project, "mcpServers"]
2819
+ });
2820
+ }
2821
+ }
2822
+ sites.push({
2823
+ client: "claude-code",
2824
+ label: LABELS["claude-code"],
2825
+ format: "json",
2826
+ path: claudeCode,
2827
+ scope: "global",
2828
+ at: ["mcpServers"]
2829
+ });
2830
+ }
2831
+ const projectFile = join3(resolve2(cwd), ".mcp.json");
2832
+ if (existsSync3(projectFile)) {
2833
+ sites.push({
2834
+ client: "claude-code",
2835
+ label: LABELS["claude-code"],
2836
+ format: "json",
2837
+ path: projectFile,
2838
+ scope: "project file",
2839
+ at: ["mcpServers"]
2840
+ });
2841
+ }
2842
+ const desktop = claudeDesktopPath();
2843
+ if (existsSync3(desktop)) {
2844
+ sites.push({
2845
+ client: "claude-desktop",
2846
+ label: LABELS["claude-desktop"],
2847
+ format: "json",
2848
+ path: desktop,
2849
+ scope: "global",
2850
+ at: ["mcpServers"]
2851
+ });
2852
+ }
2853
+ const codex = join3(process.env["CODEX_HOME"] ?? join3(home2, ".codex"), "config.toml");
2854
+ if (existsSync3(codex)) {
2855
+ sites.push({
2856
+ client: "codex",
2857
+ label: LABELS.codex,
2858
+ format: "toml",
2859
+ path: codex,
2860
+ scope: "global",
2861
+ at: ["mcp_servers"]
2862
+ });
2863
+ }
2864
+ const config = process.env["XDG_CONFIG_HOME"] ?? join3(home2, ".config");
2865
+ const plain = [
2866
+ ["cursor", join3(resolve2(cwd), ".cursor", "mcp.json"), "project"],
2867
+ ["cursor", join3(home2, ".cursor", "mcp.json"), "global"],
2868
+ // Windsurf is Devin Desktop now, which reads its own directory; an older
2869
+ // Windsurf keeps the one it always had.
2870
+ [
2871
+ "devin",
2872
+ platform2() === "win32" ? join3(process.env["APPDATA"] ?? join3(home2, "AppData", "Roaming"), "devin", "mcp_config.json") : join3(config, "devin", "mcp_config.json"),
2873
+ "global"
2874
+ ],
2875
+ ["windsurf", join3(home2, ".codeium", "windsurf", "mcp_config.json"), "global"],
2876
+ ["gemini-cli", join3(resolve2(cwd), ".gemini", "settings.json"), "project"],
2877
+ ["gemini-cli", join3(home2, ".gemini", "settings.json"), "global"],
2878
+ ["copilot-cli", join3(process.env["COPILOT_HOME"] ?? join3(home2, ".copilot"), "mcp-config.json"), "global"],
2879
+ ["antigravity", join3(resolve2(cwd), ".agents", "mcp_config.json"), "workspace"],
2880
+ ["antigravity", join3(home2, ".gemini", "config", "mcp_config.json"), "global"]
2881
+ ];
2882
+ for (const [client, path, scope] of plain) {
2883
+ if (existsSync3(path)) {
2884
+ sites.push({ client, label: LABELS[client], format: "json", path, scope, at: ["mcpServers"] });
2885
+ }
2886
+ }
2887
+ const seen = /* @__PURE__ */ new Set();
2888
+ return sites.filter((site) => {
2889
+ const key = `${resolve2(site.path)}\0${site.at.join("\0")}`;
2890
+ if (seen.has(key)) {
2891
+ return false;
2892
+ }
2893
+ seen.add(key);
2894
+ return true;
2895
+ });
2896
+ }
2897
+ function isRecord(value) {
2898
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2899
+ }
2900
+ function readJson(path) {
2901
+ try {
2902
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
2903
+ return isRecord(parsed) ? parsed : void 0;
2904
+ } catch {
2905
+ return void 0;
2906
+ }
2907
+ }
2908
+ var ConfigError = class extends Error {
2909
+ };
2910
+ function readDocument(site) {
2911
+ let text;
2912
+ try {
2913
+ text = readFileSync2(site.path, "utf8");
2914
+ } catch (error) {
2915
+ throw new ConfigError(`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`);
2916
+ }
2917
+ let parsed;
2918
+ try {
2919
+ parsed = JSON.parse(text);
2920
+ } catch (error) {
2921
+ throw new ConfigError(
2922
+ `${site.path} is not valid JSON (${error instanceof Error ? error.message : String(error)}). Fix it or move it aside; synartesis will not rewrite a file it cannot read.`
2923
+ );
2924
+ }
2925
+ if (!isRecord(parsed)) {
2926
+ throw new ConfigError(`${site.path} is not a JSON object, so it has no server list to change`);
2927
+ }
2928
+ return parsed;
2929
+ }
2930
+ function readServers2(document, at) {
2931
+ let node = document;
2932
+ for (const key of at) {
2933
+ if (!isRecord(node)) {
2934
+ return {};
2935
+ }
2936
+ node = node[key];
2937
+ }
2938
+ if (!isRecord(node)) {
2939
+ return {};
2940
+ }
2941
+ const servers = {};
2942
+ for (const [name, entry] of Object.entries(node)) {
2943
+ if (isRecord(entry)) {
2944
+ servers[name] = entry;
2945
+ }
2946
+ }
2947
+ return servers;
2948
+ }
2949
+ function withServers(document, at, servers) {
2950
+ const head = at[0];
2951
+ if (head === void 0) {
2952
+ throw new ConfigError("no path to the server list");
2953
+ }
2954
+ const rest = at.slice(1);
2955
+ const below = document[head];
2956
+ const child = rest.length === 0 ? servers : withServers(isRecord(below) ? below : {}, rest, servers);
2957
+ return { ...document, [head]: child };
2958
+ }
2959
+ function indentOf(path) {
2960
+ try {
2961
+ const line = /\n([ \t]+)"/.exec(readFileSync2(path, "utf8"));
2962
+ const found = line?.[1];
2963
+ if (found === void 0) {
2964
+ return 2;
2965
+ }
2966
+ return found.startsWith(" ") ? " " : found.length;
2967
+ } catch {
2968
+ return 2;
2969
+ }
2970
+ }
2971
+ function backupPathFor(path) {
2972
+ return `${path}.synartesis-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
2973
+ }
2974
+ var KEEP_BACKUPS = 5;
2975
+ function pruneBackups(path) {
2976
+ try {
2977
+ const dir = dirname3(path);
2978
+ const prefix = `${basename2(path)}.synartesis-backup-`;
2979
+ const ours = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
2980
+ for (const name of ours.slice(0, Math.max(0, ours.length - KEEP_BACKUPS))) {
2981
+ rmSync(join3(dir, name), { force: true });
2982
+ }
2983
+ } catch {
2984
+ }
2985
+ }
2986
+ function writeDocument(site, document) {
2987
+ return writeText(site, `${JSON.stringify(document, void 0, indentOf(site.path))}
2988
+ `);
2989
+ }
2990
+ function writeText(site, text) {
2991
+ const backup = backupPathFor(site.path);
2992
+ const original = readFileSync2(site.path);
2993
+ writeFileSync(backup, original);
2994
+ pruneBackups(site.path);
2995
+ const temporary = join3(dirname3(site.path), `.synartesis-write-${String(process.pid)}.tmp`);
2996
+ try {
2997
+ writeFileSync(temporary, text);
2998
+ renameSync(temporary, site.path);
2999
+ } catch (error) {
3000
+ try {
3001
+ unlinkSync(temporary);
3002
+ } catch {
3003
+ }
3004
+ throw new ConfigError(
3005
+ `could not write ${site.path}: ${error instanceof Error ? error.message : String(error)}. The original is untouched, and a copy is at ${backup}.`
3006
+ );
3007
+ }
3008
+ return backup;
3009
+ }
3010
+ function serversAt(site) {
3011
+ if (site.format === "toml") {
3012
+ try {
3013
+ return readServers(readFileSync2(site.path, "utf8"));
3014
+ } catch (error) {
3015
+ throw new ConfigError(
3016
+ `cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`
3017
+ );
3018
+ }
3019
+ }
3020
+ return readServers2(readDocument(site), site.at);
3021
+ }
3022
+ function saveServers(site, servers) {
3023
+ if (site.format === "toml") {
3024
+ const text = readFileSync2(site.path, "utf8");
3025
+ return writeText(site, writeServers(text, servers));
3026
+ }
3027
+ return writeDocument(site, withServers(readDocument(site), site.at, servers));
3028
+ }
3029
+
3030
+ // src/init/draft.ts
3031
+ import { z as z4 } from "zod";
3032
+
3033
+ // src/init/known.ts
3034
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
3035
+ import { fileURLToPath as fileURLToPath2 } from "url";
3036
+ var KNOWN = [
3037
+ { marker: "server-filesystem", manifest: "filesystem" },
3038
+ { marker: "server-github", manifest: "github" },
3039
+ { marker: "github-mcp-server", manifest: "github" },
3040
+ { marker: "server-memory", manifest: "memory" },
3041
+ { marker: "mcp-server-git", manifest: "git" },
3042
+ { marker: "server-git", manifest: "git" },
3043
+ { marker: "@playwright/mcp", manifest: "playwright" },
3044
+ { marker: "chrome-devtools-mcp", manifest: "chrome-devtools" },
3045
+ // Servers that only read, so their whole policy is "let it look".
3046
+ { marker: "mcp-server-fetch", manifest: "fetch" },
3047
+ { marker: "brave-search-mcp-server", manifest: "brave" },
3048
+ { marker: "exa-mcp-server", manifest: "exa" },
3049
+ { marker: "tavily-mcp", manifest: "tavily" },
3050
+ { marker: "aws-documentation-mcp-server", manifest: "aws-docs" }
3051
+ ];
3052
+ function manifestsDir() {
3053
+ for (const up of ["../manifests/", "../../manifests/"]) {
3054
+ const candidate = fileURLToPath2(new URL(up, import.meta.url));
3055
+ if (existsSync4(candidate)) {
3056
+ return candidate;
3057
+ }
3058
+ }
3059
+ return void 0;
3060
+ }
3061
+ function knownPolicyFor(command, args) {
3062
+ const line = [command, ...args].join(" ");
3063
+ const hit = KNOWN.find((entry) => line.includes(entry.marker));
3064
+ const dir = manifestsDir();
3065
+ if (hit === void 0 || dir === void 0) {
3066
+ return void 0;
3067
+ }
3068
+ const path = `${dir}${hit.manifest}.yaml`;
3069
+ if (!existsSync4(path)) {
3070
+ return void 0;
3071
+ }
3072
+ try {
3073
+ const text = readFileSync3(path, "utf8");
3074
+ const source = toolsBlock(text);
3075
+ const key = serverKey(text);
3076
+ if (source === void 0 || key === void 0) {
3077
+ return void 0;
3078
+ }
3079
+ const rules = parseManifest(
3080
+ `version: 1
3081
+ servers:
3082
+ ${key}:
3083
+ command: "true"
3084
+ tools:
3085
+ ${source}
3086
+ `,
3087
+ path
3088
+ ).tools;
3089
+ const claimed = /^\s*provenance:\s*(live|documented)\s*$/m.exec(text)?.[1];
3090
+ return {
3091
+ key,
3092
+ rules,
3093
+ name: hit.manifest,
3094
+ source,
3095
+ ...claimed === "live" || claimed === "documented" ? { provenance: claimed } : {}
3096
+ };
3097
+ } catch {
3098
+ return void 0;
3099
+ }
3100
+ }
3101
+ function toolsBlock(text) {
3102
+ const at = text.search(/^tools:[ \t]*$/m);
3103
+ if (at === -1) {
3104
+ return void 0;
3105
+ }
3106
+ const body = text.slice(text.indexOf("\n", at) + 1);
3107
+ const lines = [];
3108
+ for (const line of body.split("\n")) {
3109
+ if (/^[^\s#]/.test(line)) {
3110
+ break;
3111
+ }
3112
+ lines.push(line);
3113
+ }
3114
+ return lines.join("\n").replace(/\s+$/, "");
3115
+ }
3116
+ function serverKey(text) {
3117
+ const at = text.search(/^servers:[ \t]*$/m);
3118
+ if (at === -1) {
3119
+ return void 0;
3120
+ }
3121
+ const body = text.slice(text.indexOf("\n", at) + 1);
3122
+ for (const line of body.split("\n")) {
3123
+ if (/^[^\s#]/.test(line)) {
3124
+ return void 0;
3125
+ }
3126
+ const named = /^ {2}([A-Za-z0-9_-]+):[ \t]*$/.exec(line);
3127
+ if (named?.[1] !== void 0) {
3128
+ return named[1];
3129
+ }
3130
+ }
3131
+ return void 0;
3132
+ }
3133
+ function toolsReferencedBy(rule2, key) {
3134
+ const local = (qualified) => qualified === void 0 || !qualified.startsWith(`${key}.`) ? void 0 : qualified.slice(key.length + 1);
3135
+ return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
3136
+ (name) => name !== void 0
3137
+ );
3138
+ }
3139
+
3140
+ // src/init/draft.ts
3141
+ var toolSchema = z4.looseObject({
3142
+ name: z4.string(),
3143
+ description: z4.string().optional(),
3144
+ annotations: z4.looseObject({
3145
+ readOnlyHint: z4.boolean().optional(),
3146
+ destructiveHint: z4.boolean().optional(),
3147
+ idempotentHint: z4.boolean().optional()
3148
+ }).optional()
3149
+ });
3150
+ var listSchema2 = z4.looseObject({
3151
+ tools: z4.array(toolSchema),
3152
+ nextCursor: z4.string().optional()
3153
+ });
3154
+ function quote2(value) {
3155
+ return JSON.stringify(value);
3156
+ }
3157
+ function summarise(text) {
3158
+ if (text === void 0) {
3159
+ return "";
3160
+ }
3161
+ const single = text.replace(/\s+/g, " ").trim();
3162
+ return single.length > 96 ? `${single.slice(0, 93)}...` : single;
3163
+ }
3164
+ function draftTool(server, tool) {
3165
+ const match = `${server}.${tool.name}`;
3166
+ const lines = [];
3167
+ const description = summarise(tool.description);
3168
+ if (description !== "") {
3169
+ lines.push(` # ${description}`);
3170
+ }
3171
+ if (tool.annotations?.readOnlyHint === true) {
3172
+ lines.push(` # classified readonly from the server's readOnlyHint; verify it before relying on it.`);
3173
+ lines.push(` - match: ${quote2(match)}`);
3174
+ lines.push(` class: readonly`);
3175
+ return lines.join("\n");
3176
+ }
3177
+ lines.push(` # TODO: this is gated on every call until you describe how to undo it.`);
3178
+ lines.push(` # reversible needs a snapshot (a pre-read) and an inverse.`);
3179
+ lines.push(` # compensable needs an inverse only, usually built from $result.`);
3180
+ lines.push(` # irreversible is correct when neither exists; leave gate: always.`);
3181
+ lines.push(` - match: ${quote2(match)}`);
3182
+ lines.push(` class: irreversible`);
3183
+ lines.push(` gate: always`);
3184
+ return lines.join("\n");
3185
+ }
3186
+ function patternFor(match) {
3187
+ const source = match.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
3188
+ return new RegExp(`^${source}$`);
3189
+ }
3190
+ function adopt(known, name, tools) {
3191
+ const advertised = new Set(tools.map((tool) => tool.name));
3192
+ const covered = /* @__PURE__ */ new Set();
3193
+ for (const rule2 of known.rules) {
3194
+ for (const needed of toolsReferencedBy(rule2, known.key)) {
3195
+ if (!advertised.has(needed)) {
3196
+ return void 0;
3197
+ }
3198
+ }
3199
+ const test = patternFor(rule2.match);
3200
+ for (const tool of tools) {
3201
+ if (test.test(`${known.key}.${tool.name}`)) {
3202
+ covered.add(tool.name);
3203
+ }
3204
+ }
3205
+ }
3206
+ if (covered.size === 0) {
3207
+ return void 0;
3208
+ }
3209
+ const renamed = known.source.replaceAll(`"${known.key}.`, `"${name}.`);
3210
+ const missing = tools.filter((tool) => !covered.has(tool.name));
3211
+ const extra = missing.length === 0 ? "" : [
3212
+ "",
3213
+ ` # Not mentioned by the bundled ${known.name} policy, so gated until you say otherwise.`,
3214
+ ...missing.map((tool) => draftTool(name, tool))
3215
+ ].join("\n");
3216
+ return { source: `${renamed}${extra}`, covered: covered.size };
3217
+ }
3218
+ async function draftManifest(options) {
3219
+ const upstream = await connectUpstream(
3220
+ options.name,
3221
+ options.remote === void 0 ? { command: options.command, args: options.args } : { ...options.remote },
3222
+ {
3223
+ env: options.env === void 0 ? { kind: "inherit" } : { kind: "client", env: options.env },
3224
+ stderr: "capture",
3225
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd }
3226
+ }
3227
+ );
3228
+ let tools;
3229
+ try {
3230
+ const collected = [];
3231
+ let cursor;
3232
+ do {
3233
+ const page = listSchema2.parse(
3234
+ await upstream.client.request(
3235
+ { method: "tools/list", params: cursor === void 0 ? {} : { cursor } },
3236
+ z4.looseObject({})
3237
+ )
3238
+ );
3239
+ collected.push(...page.tools);
3240
+ cursor = page.nextCursor;
3241
+ } while (cursor !== void 0);
3242
+ tools = collected;
3243
+ } catch (error) {
3244
+ throw new UpstreamError(options.name, "tools/list", error);
3245
+ } finally {
3246
+ await upstream.close();
3247
+ }
3248
+ if (tools.length === 0) {
3249
+ throw new ManifestError(`${options.name} exposes no tools, so there is no policy to write`);
3250
+ }
3251
+ const existing = options.existing?.trimEnd();
3252
+ if (existing !== void 0 && existing.includes(`
3253
+ ${options.name}:`)) {
3254
+ throw new ManifestError(
3255
+ `${options.name} is already declared in the manifest; remove it first or choose another name`
3256
+ );
3257
+ }
3258
+ const known = options.remote === void 0 ? knownPolicyFor(options.command, options.args) : void 0;
3259
+ const claim = known?.provenance === void 0 ? [] : [
3260
+ ...known.provenance === "documented" ? [
3261
+ ` # This policy has never been run against the real server. Check it`,
3262
+ ` # against your own setup before trusting undo on it.`
3263
+ ] : [],
3264
+ ` provenance: ${known.provenance}`
3265
+ ];
3266
+ const reach = options.remote === void 0 ? [` command: ${quote2(options.command)}`, ` args: [${options.args.map(quote2).join(", ")}]`] : [
3267
+ ` url: ${quote2(options.remote.url)}`,
3268
+ ` transport: ${options.remote.transport}`,
3269
+ ...Object.keys(options.remote.headers).length === 0 ? [] : [
3270
+ ` # Names, not values: each is filled in from the client entry's env`,
3271
+ ` # when the server is reached, so the token never lands in this file.`,
3272
+ ` headers:`,
3273
+ ...Object.entries(options.remote.headers).map(
3274
+ ([key, value]) => ` ${quote2(key)}: ${quote2(value)}`
3275
+ )
3276
+ ]
3277
+ ];
3278
+ const server = [` ${options.name}:`, ...reach, ...claim].join("\n");
3279
+ const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
3280
+ const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
3281
+ if (existing === void 0) {
3282
+ const yaml = [
3283
+ `# Generated by synartesis init from ${options.name}'s tools/list.`,
3284
+ ...adopted === void 0 ? [
3285
+ `# Every tool starts gated. Working through the TODOs is the whole job:`,
3286
+ `# a tool with no inverse is one an agent cannot use unsupervised.`
3287
+ ] : [
3288
+ `# ${String(adopted.covered)} of its tools were recognised, so the policy that ships`,
3289
+ `# with Synartesis for ${known?.name ?? "this server"} was used and checked against what this`,
3290
+ `# server actually advertises. Read it before trusting it: it is a starting`,
3291
+ `# point that happens to be finished, not a promise about your setup.`
3292
+ ],
3293
+ ``,
3294
+ `version: 1`,
3295
+ ``,
3296
+ `servers:`,
3297
+ server,
3298
+ ``,
3299
+ `tools:`,
3300
+ policies,
3301
+ ``
3302
+ ].join("\n");
3303
+ return adopted === void 0 || known === void 0 ? { yaml } : {
3304
+ yaml,
3305
+ adopted: {
3306
+ server: known.name,
3307
+ tools: adopted.covered,
3308
+ ...known.provenance === void 0 ? {} : { provenance: known.provenance }
3309
+ }
3310
+ };
3311
+ }
3312
+ const merged = mergeInto(existing, server, policies, options.name);
3313
+ return adopted === void 0 || known === void 0 ? { yaml: merged } : {
3314
+ yaml: merged,
3315
+ adopted: {
3316
+ server: known.name,
3317
+ tools: adopted.covered,
3318
+ ...known.provenance === void 0 ? {} : { provenance: known.provenance }
3319
+ }
3320
+ };
3321
+ }
3322
+ function mergeInto(existing, server, policies, name) {
3323
+ const serversAt2 = existing.indexOf("\nservers:");
3324
+ const toolsAt = existing.indexOf("\ntools:");
3325
+ if (serversAt2 === -1 || toolsAt === -1 || toolsAt < serversAt2) {
3326
+ throw new ManifestError(
3327
+ "the existing manifest does not have a servers: block followed by a tools: block, so it cannot be extended automatically"
3328
+ );
3329
+ }
3330
+ const head = existing.slice(0, toolsAt);
3331
+ const tail = existing.slice(toolsAt);
3332
+ return [
3333
+ head.trimEnd(),
3334
+ server,
3335
+ tail.trimEnd(),
3336
+ ``,
3337
+ ` # --- added by synartesis init for ${name} ---`,
3338
+ policies,
3339
+ ``
3340
+ ].join("\n");
3341
+ }
3342
+
3343
+ // src/install/install.ts
3344
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
3345
+ import { dirname as dirname4, resolve as resolve3 } from "path";
3346
+ function recordPathFor(manifestPath) {
3347
+ return resolve3(dirname4(manifestPath), "installed.json");
3348
+ }
3349
+ var EMPTY = { version: 1, wrapped: {} };
3350
+ function isRecord2(value) {
3351
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3352
+ }
3353
+ function asRecord(value) {
3354
+ if (!isRecord2(value)) {
3355
+ return void 0;
3356
+ }
3357
+ const wrapped = value["wrapped"];
3358
+ if (!isRecord2(wrapped)) {
3359
+ return void 0;
3360
+ }
3361
+ const kept2 = {};
3362
+ for (const [key, entry] of Object.entries(wrapped)) {
3363
+ if (!isRecord2(entry)) {
3364
+ continue;
3365
+ }
3366
+ const original = entry["original"];
3367
+ const at = entry["at"];
3368
+ if (!isRecord2(original)) {
3369
+ continue;
3370
+ }
3371
+ if (!Array.isArray(at) || !at.every((step) => typeof step === "string")) {
3372
+ continue;
3373
+ }
3374
+ kept2[key] = { original, at };
3375
+ }
3376
+ return { version: 1, wrapped: kept2 };
3377
+ }
3378
+ function keyFor(site, server) {
3379
+ return [site.path, site.scope, server].join("");
3380
+ }
3381
+ function readRecord(manifestPath) {
3382
+ const path = recordPathFor(manifestPath);
3383
+ if (!existsSync5(path)) {
3384
+ return EMPTY;
3385
+ }
3386
+ try {
3387
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
3388
+ const record = asRecord(parsed);
3389
+ if (record !== void 0) {
3390
+ return record;
3391
+ }
3392
+ } catch {
3393
+ }
3394
+ return EMPTY;
3395
+ }
3396
+ function writeRecord(manifestPath, record) {
3397
+ mkdirSync2(dirname4(recordPathFor(manifestPath)), { recursive: true, mode: 448 });
3398
+ const path = recordPathFor(manifestPath);
3399
+ writeFileSync2(path, `${JSON.stringify(record, void 0, 2)}
3400
+ `, { mode: 384 });
3401
+ chmodSync2(path, 384);
3402
+ }
3403
+ function proxyEntry(manifestPath, server, original, invoker) {
3404
+ const command = { command: invoker.command, args: [...invoker.args] };
3405
+ return {
3406
+ // What the client keeps about a server besides how to start it: Copilot
3407
+ // CLI's `tools` list, Gemini CLI's `trust` and timeout, a stdio `type`.
3408
+ // Dropped, a client that requires one would stop offering the server.
3409
+ // Not the ways of reaching it, which are all replaced by the proxy.
3410
+ ...Object.fromEntries(Object.entries(original).filter(([key, value]) => kept(key, value))),
3411
+ ...command,
3412
+ args: [...command.args, "--manifest", resolve3(manifestPath), "--server", server],
3413
+ // The agent's environment, not ours: the upstream is started by the proxy
3414
+ // from the manifest, but a client that set `env` here meant it for the
3415
+ // server, and the manifest reads `${VAR}` out of exactly this environment.
3416
+ ...original.env === void 0 ? {} : { env: original.env },
3417
+ ...original.cwd === void 0 ? {} : { cwd: original.cwd }
3418
+ };
3419
+ }
3420
+ var REPLACED = /* @__PURE__ */ new Set(["command", "args", "env", "cwd", "url", "serverUrl", "httpUrl", "headers"]);
3421
+ function kept(key, value) {
3422
+ if (key === "type") {
3423
+ return value === "stdio" || value === "local";
3424
+ }
3425
+ return !REPLACED.has(key);
3426
+ }
3427
+ function isWrapped(entry) {
3428
+ const args = entry.args ?? [];
3429
+ return args.includes("proxy") && (entry.command === "synartesis" || entry.command === "synartesis-proxy" || args.includes("synartesis") || args.some((arg) => arg.endsWith("dist/cli.js") || arg.endsWith("dist/proxy.js")));
3430
+ }
3431
+ function invokerFor(ourVersion, cliPath) {
3432
+ if (pathBinaryMatches(ourVersion)) {
3433
+ return { command: "synartesis", args: ["proxy"] };
3434
+ }
3435
+ return {
3436
+ command: process.execPath,
3437
+ args: [cliPath, "proxy"],
3438
+ note: "the synartesis on your PATH is a different build, so the entries name this one directly"
3439
+ };
3440
+ }
3441
+ function headersOf(entry) {
3442
+ const headers = entry["headers"];
3443
+ if (typeof headers !== "object" || headers === null || Array.isArray(headers)) {
3444
+ return void 0;
3445
+ }
3446
+ const text = Object.entries(headers).filter(
3447
+ (pair) => typeof pair[1] === "string"
3448
+ );
3449
+ return text.length === 0 ? void 0 : Object.fromEntries(text);
3450
+ }
3451
+ function headerVariable(server, header) {
3452
+ const part = (text) => text.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3453
+ const name = `${part(server)}_MCP_${part(header)}`;
3454
+ return /^[A-Z_]/.test(name) ? name : `S_${name}`;
3455
+ }
3456
+ function transportOf(site, entry) {
3457
+ if (entry.type === "sse") {
3458
+ return "sse";
3459
+ }
3460
+ if (typeof entry["httpUrl"] === "string" || entry.type === "http" || entry.type === "streamable-http") {
3461
+ return "http";
3462
+ }
3463
+ if (site.client === "gemini-cli" && typeof entry.url === "string") {
3464
+ return "sse";
3465
+ }
3466
+ return "auto";
3467
+ }
3468
+ function bridgeFor(url) {
3469
+ return { command: "npx", args: ["-y", "mcp-remote", url] };
3470
+ }
3471
+ function unbridgeable(site, entry, remote) {
3472
+ if (site.format === "toml") {
3473
+ return "hosted; covering one in Codex's config is not supported yet";
3474
+ }
3475
+ if (!remote) {
3476
+ return "hosted; install --remote covers it through mcp-remote, which signs you in through your browser";
3477
+ }
3478
+ return void 0;
3479
+ }
3480
+ async function planInstall(sites, manifestPath, invoker, only, options = {}) {
3481
+ let yaml = existsSync5(manifestPath) ? readFileSync4(manifestPath, "utf8") : void 0;
3482
+ const plans = [];
3483
+ const existing = yaml === void 0 ? {} : parseManifest(yaml, manifestPath).servers;
3484
+ const claimed = new Set(Object.keys(existing));
3485
+ const pointedAt = /* @__PURE__ */ new Set();
3486
+ for (const site of sites) {
3487
+ let entries;
3488
+ try {
3489
+ entries = serversAt(site);
3490
+ } catch {
3491
+ continue;
3492
+ }
3493
+ for (const entry of Object.values(entries)) {
3494
+ const args = entry.args ?? [];
3495
+ const manifest = args[args.indexOf("--manifest") + 1];
3496
+ const server = args[args.indexOf("--server") + 1];
3497
+ if (isWrapped(entry) && args.includes("--server") && manifest !== void 0 && server !== void 0 && resolve3(manifest) === resolve3(manifestPath)) {
3498
+ pointedAt.add(server);
3499
+ }
3500
+ }
3501
+ }
3502
+ for (const site of sites) {
3503
+ const servers = serversAt(site);
3504
+ const planned = [];
3505
+ const skipped = [];
3506
+ for (const [name, original] of Object.entries(servers)) {
3507
+ if (only !== void 0 && !only(site, name)) {
3508
+ continue;
3509
+ }
3510
+ if (isWrapped(original)) {
3511
+ skipped.push({ name, why: "already covered" });
3512
+ continue;
3513
+ }
3514
+ const address = [original.url, original["serverUrl"], original["httpUrl"]].find(
3515
+ (one) => typeof one === "string"
3516
+ );
3517
+ const hosted = original.command === void 0 && address !== void 0;
3518
+ const headers = headersOf(original);
3519
+ const native = hosted && headers !== void 0 && site.format !== "toml" ? {
3520
+ url: address,
3521
+ transport: transportOf(site, original),
3522
+ headers: Object.fromEntries(
3523
+ Object.keys(headers).map((header) => [header, `\${${headerVariable(name, header)}}`])
3524
+ ),
3525
+ env: Object.fromEntries(
3526
+ Object.entries(headers).map(([header, value]) => [headerVariable(name, header), value])
3527
+ )
3528
+ } : void 0;
3529
+ if (hosted && native === void 0) {
3530
+ const why = unbridgeable(site, original, options.remote === true);
3531
+ if (why !== void 0) {
3532
+ skipped.push({ name, why });
3533
+ continue;
3534
+ }
3535
+ }
3536
+ const entry = native !== void 0 ? { ...original, env: { ...original.env ?? {}, ...native.env } } : hosted ? (options.bridge ?? bridgeFor)(address) : original;
3537
+ if (entry.command === void 0 && native === void 0) {
3538
+ skipped.push({ name, why: "no command to start" });
3539
+ continue;
3540
+ }
3541
+ if (original.enabled === false || original.disabled === true) {
3542
+ skipped.push({ name, why: "switched off in the config" });
3543
+ continue;
3544
+ }
3545
+ if (typeof original["unreadable"] === "string") {
3546
+ skipped.push({ name, why: `left alone: ${original["unreadable"]}. Put args on one line, or wrap it by hand` });
3547
+ continue;
3548
+ }
3549
+ const again = [name, `${name}-${site.client}`].find((candidate) => {
3550
+ const spec = existing[candidate];
3551
+ const args = entry.args ?? [];
3552
+ if (native !== void 0) {
3553
+ return spec?.url === native.url && !pointedAt.has(candidate);
3554
+ }
3555
+ return spec !== void 0 && spec.url === void 0 && !pointedAt.has(candidate) && spec.command === entry.command && spec.args.length === args.length && spec.args.every((arg, index) => arg === args[index]);
3556
+ });
3557
+ if (again !== void 0) {
3558
+ pointedAt.add(again);
3559
+ planned.push({
3560
+ name,
3561
+ original,
3562
+ wrapped: proxyEntry(manifestPath, again, entry, invoker),
3563
+ again: true
3564
+ });
3565
+ continue;
3566
+ }
3567
+ const key = claimed.has(name) ? `${name}-${site.client}` : name;
3568
+ if (claimed.has(key)) {
3569
+ skipped.push({ name, why: `already in the policy as ${key}` });
3570
+ continue;
3571
+ }
3572
+ if (options.start === false) {
3573
+ claimed.add(key);
3574
+ const known = native === void 0 ? knownPolicyFor(entry.command ?? "", entry.args ?? []) : void 0;
3575
+ planned.push({
3576
+ name,
3577
+ original,
3578
+ wrapped: proxyEntry(manifestPath, key, entry, invoker),
3579
+ unstarted: true,
3580
+ ...hosted && native === void 0 ? { bridged: address } : {},
3581
+ ...native === void 0 ? {} : { direct: native.url },
3582
+ ...known === void 0 ? {} : { adopted: known.name }
3583
+ });
3584
+ continue;
3585
+ }
3586
+ options.starting?.(name);
3587
+ let draft;
3588
+ try {
3589
+ draft = await draftManifest({
3590
+ name: key,
3591
+ command: entry.command ?? "",
3592
+ args: [...entry.args ?? []],
3593
+ ...native === void 0 ? {} : { remote: { url: native.url, transport: native.transport, headers: native.headers } },
3594
+ // As the client would start it, so a server that needs its token to
3595
+ // list its tools is drafted rather than reported as broken.
3596
+ env: Object.fromEntries(
3597
+ Object.entries(entry.env ?? {}).map(([k, v]) => [k, expandForClient(site.client, v)])
3598
+ ),
3599
+ ...entry.cwd === void 0 ? {} : { cwd: expandForClient(site.client, entry.cwd) },
3600
+ ...yaml === void 0 ? {} : { existing: yaml }
3601
+ });
3602
+ } catch (error) {
3603
+ skipped.push({
3604
+ name,
3605
+ // Whole. The sentence worth reading comes last -- the server's own
3606
+ // "Please set SLACK_BOT_TOKEN" -- and cutting at sixty characters
3607
+ // kept only the SDK's preamble ahead of it.
3608
+ why: `will not start: ${error instanceof Error ? error.message : String(error)}`
3609
+ });
3610
+ continue;
3611
+ }
3612
+ yaml = draft.yaml;
3613
+ claimed.add(key);
3614
+ planned.push({
3615
+ name,
3616
+ original,
3617
+ wrapped: proxyEntry(manifestPath, key, entry, invoker),
3618
+ ...hosted && native === void 0 ? { bridged: address } : {},
3619
+ ...native === void 0 ? {} : { direct: native.url },
3620
+ ...draft.adopted === void 0 ? {} : {
3621
+ adopted: draft.adopted.server,
3622
+ tools: draft.adopted.tools,
3623
+ ...draft.adopted.provenance === void 0 ? {} : { provenance: draft.adopted.provenance }
3624
+ }
3625
+ });
3626
+ }
3627
+ plans.push({ site, servers: planned, skipped });
3628
+ }
3629
+ return { plans, yaml: yaml ?? "" };
3630
+ }
3631
+ function applyInstall(plans, manifestPath, yaml) {
3632
+ if (!plans.some((plan) => plan.servers.length > 0)) {
3633
+ return [];
3634
+ }
3635
+ parseManifest(yaml, manifestPath);
3636
+ mkdirSync2(dirname4(resolve3(manifestPath)), { recursive: true, mode: 448 });
3637
+ writeFileSync2(manifestPath, yaml);
3638
+ const record = readRecord(manifestPath);
3639
+ const wrapped = { ...record.wrapped };
3640
+ const applied = [];
3641
+ for (const plan of plans) {
3642
+ if (plan.servers.length === 0) {
3643
+ continue;
3644
+ }
3645
+ const servers = { ...serversAt(plan.site) };
3646
+ for (const server of plan.servers) {
3647
+ servers[server.name] = server.wrapped;
3648
+ wrapped[keyFor(plan.site, server.name)] = { original: server.original, at: plan.site.at };
3649
+ }
3650
+ writeRecord(manifestPath, { version: 1, wrapped });
3651
+ const backup = saveServers(plan.site, servers);
3652
+ applied.push({ site: plan.site, backup, servers: plan.servers.map((server) => server.name) });
3653
+ }
3654
+ return applied;
3655
+ }
3656
+ function applyUninstall(sites, manifestPath) {
3657
+ const record = readRecord(manifestPath);
3658
+ const restoredKeys = /* @__PURE__ */ new Set();
3659
+ const restored = [];
3660
+ for (const site of sites) {
3661
+ const servers = { ...serversAt(site) };
3662
+ const put = [];
3663
+ const unknown = [];
3664
+ for (const [name, entry] of Object.entries(servers)) {
3665
+ if (!isWrapped(entry)) {
3666
+ continue;
3667
+ }
3668
+ const known = record.wrapped[keyFor(site, name)];
3669
+ if (known === void 0) {
3670
+ unknown.push(name);
3671
+ continue;
3672
+ }
3673
+ servers[name] = known.original;
3674
+ put.push(name);
3675
+ restoredKeys.add(keyFor(site, name));
3676
+ }
3677
+ if (put.length === 0 && unknown.length === 0) {
3678
+ continue;
3679
+ }
3680
+ const backup = put.length === 0 ? "" : saveServers(site, servers);
3681
+ restored.push({ site, backup, servers: put, unknown });
3682
+ }
3683
+ const remaining = Object.fromEntries(
3684
+ Object.entries(record.wrapped).filter(([key]) => !restoredKeys.has(key))
3685
+ );
3686
+ writeRecord(manifestPath, { version: 1, wrapped: remaining });
3687
+ return restored;
3688
+ }
3689
+
3690
+ // src/install/entry-env.ts
3691
+ import { resolve as resolve4 } from "path";
3692
+ function argAfter(args, flag) {
3693
+ const at = args.indexOf(flag);
3694
+ return at === -1 ? void 0 : args[at + 1];
3695
+ }
3696
+ function expanded(client, env) {
3697
+ const out = {};
3698
+ for (const [key, value] of Object.entries(env ?? {})) {
3699
+ out[key] = expandForClient(client, value);
3700
+ }
3701
+ return out;
3702
+ }
3703
+ function sameEnv(a, b) {
3704
+ const keys = Object.keys(a);
3705
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
3706
+ }
3707
+ function clientEnvFor(manifestPath, server, cwd = process.cwd()) {
3708
+ const wanted = resolve4(manifestPath);
3709
+ const found = [];
3710
+ for (const site of discover(cwd)) {
3711
+ let entries;
3712
+ try {
3713
+ entries = serversAt(site);
3714
+ } catch {
3715
+ continue;
3716
+ }
3717
+ for (const [name, entry] of Object.entries(entries)) {
3718
+ if (!isWrapped(entry)) {
3719
+ continue;
3720
+ }
3721
+ const args = entry.args ?? [];
3722
+ const manifest = argAfter(args, "--manifest");
3723
+ if (argAfter(args, "--server") !== server || manifest === void 0) {
3724
+ continue;
3725
+ }
3726
+ if (resolve4(manifest) !== wanted) {
3727
+ continue;
3728
+ }
3729
+ found.push({
3730
+ env: expanded(site.client, entry.env),
3731
+ ...entry.cwd === void 0 ? {} : { cwd: expandForClient(site.client, entry.cwd) },
3732
+ from: `${site.label} (${site.scope}) entry ${name}`
3733
+ });
3734
+ }
3735
+ }
3736
+ const [first, ...rest] = found;
3737
+ if (first === void 0) {
3738
+ return void 0;
3739
+ }
3740
+ const differing2 = rest.filter((other) => !sameEnv(other.env, first.env) || other.cwd !== first.cwd);
3741
+ if (differing2.length > 0) {
3742
+ throw new ConfigError(
3743
+ `server ${server} is wrapped by more than one client entry, with different settings: ` + [first, ...differing2].map((one) => one.from).join("; ") + `. Starting it with one of them could act on a different store from the one a session used, so it is not started.`
3744
+ );
3745
+ }
3746
+ return first;
3747
+ }
3748
+
3749
+ // src/idempotency.ts
3750
+ var IDEMPOTENCY_META_KEY = "synartesis.dev/idempotency-key";
3751
+ function withIdempotencyKey(meta, key) {
3752
+ const merged = {};
3753
+ if (typeof meta === "object" && meta !== null) {
3754
+ for (const [name, value] of Object.entries(meta)) {
3755
+ merged[name] = value;
3756
+ }
3757
+ }
3758
+ merged[IDEMPOTENCY_META_KEY] = key;
3759
+ return merged;
3760
+ }
3761
+
3762
+ // src/proxy/snapshot.ts
3763
+ import { z as z5 } from "zod";
3764
+ var ToolResult = z5.looseObject({
3765
+ isError: z5.boolean().default(false),
3766
+ content: z5.array(z5.looseObject({ type: z5.string() })).default([])
3767
+ });
3768
+ function refusal2(result) {
3769
+ const parsed = ToolResult.safeParse(result);
3770
+ if (!parsed.success || !parsed.data.isError) {
3771
+ return void 0;
3772
+ }
3773
+ const said = parsed.data.content.map((block) => typeof block["text"] === "string" ? block["text"] : "").filter((text) => text !== "").join(" ");
3774
+ return said === "" ? JSON.stringify(result) : said;
3775
+ }
3776
+ function isRecord3(value) {
2002
3777
  return typeof value === "object" && value !== null && !Array.isArray(value);
2003
3778
  }
2004
3779
  function toPayload(result) {
2005
- if (!isRecord(result)) {
3780
+ if (!isRecord3(result)) {
2006
3781
  return result;
2007
3782
  }
2008
3783
  const structured = result["structuredContent"];
@@ -2012,7 +3787,7 @@ function toPayload(result) {
2012
3787
  const content = result["content"];
2013
3788
  if (Array.isArray(content) && content.length === 1) {
2014
3789
  const block = content[0];
2015
- if (isRecord(block) && block["type"] === "text" && typeof block["text"] === "string") {
3790
+ if (isRecord3(block) && block["type"] === "text" && typeof block["text"] === "string") {
2016
3791
  const text = block["text"];
2017
3792
  try {
2018
3793
  return JSON.parse(text);
@@ -2025,7 +3800,7 @@ function toPayload(result) {
2025
3800
  }
2026
3801
  function resolveArgs(call, context) {
2027
3802
  const resolved = resolveTemplate(call.args, context);
2028
- if (!isRecord(resolved)) {
3803
+ if (!isRecord3(resolved)) {
2029
3804
  throw new ManifestError(`${call.tool} resolved to arguments that are not an object`);
2030
3805
  }
2031
3806
  return resolved;
@@ -2037,11 +3812,11 @@ function planInverse(call, context) {
2037
3812
  }
2038
3813
  return { server: target.server, tool: target.tool, args: resolveArgs(call, context) };
2039
3814
  }
2040
- var resolvedRead = z4.object({
2041
- server: z4.string(),
2042
- tool: z4.string(),
2043
- args: z4.record(z4.string(), z4.unknown()),
2044
- absentWhen: z4.array(z4.string()).optional()
3815
+ var resolvedRead = z5.object({
3816
+ server: z5.string(),
3817
+ tool: z5.string(),
3818
+ args: z5.record(z5.string(), z5.unknown()),
3819
+ absentWhen: z5.array(z5.string()).optional()
2045
3820
  });
2046
3821
  function toResolvedRead(parsed) {
2047
3822
  return {
@@ -2080,14 +3855,15 @@ async function runRead(router, read2, signal) {
2080
3855
  const { tool, args } = read2;
2081
3856
  const ask = () => upstream.client.request(
2082
3857
  { method: "tools/call", params: { name: tool, arguments: args } },
2083
- z4.looseObject({}),
3858
+ z5.looseObject({}),
2084
3859
  { signal }
2085
3860
  );
2086
3861
  let raw;
2087
3862
  try {
2088
3863
  raw = await ask();
2089
3864
  } catch (error) {
2090
- if (!isDisconnected(error) || upstream.reconnect === void 0) {
3865
+ const lost = isDisconnected(error) || upstream.classify?.(error) === "lost";
3866
+ if (!lost || upstream.reconnect === void 0) {
2091
3867
  throw new SnapshotError(label, describe(error), { cause: error });
2092
3868
  }
2093
3869
  try {
@@ -2126,10 +3902,12 @@ async function observeState(router, read2, signal) {
2126
3902
 
2127
3903
  export {
2128
3904
  PROXY_FLAGS,
3905
+ SILENT,
3906
+ desktopNotifier,
3907
+ canNotify,
2129
3908
  cliCommand,
2130
3909
  cliCommandFrom,
2131
3910
  proxyCommand,
2132
- pathBinaryMatches,
2133
3911
  findManifest,
2134
3912
  findJournal,
2135
3913
  counted,
@@ -2148,19 +3926,24 @@ export {
2148
3926
  loadManifest,
2149
3927
  createPolicyResolver,
2150
3928
  qualify,
3929
+ splitQualified,
3930
+ fingerprint,
2151
3931
  pinBlock,
3932
+ listAll,
2152
3933
  toolShapes,
2153
3934
  verifyAgainstServers,
3935
+ withoutMissingTools,
2154
3936
  standing,
2155
3937
  untested,
2156
3938
  describeStanding,
2157
3939
  LIVE_IS_NOT_RECOVERY,
2158
3940
  warnUntested,
3941
+ trustsMarks,
2159
3942
  ungoverned,
2160
3943
  IDEMPOTENCY_META_KEY,
2161
3944
  withIdempotencyKey,
2162
3945
  createRouter,
2163
- refusal,
3946
+ refusal2 as refusal,
2164
3947
  toPayload,
2165
3948
  planInverse,
2166
3949
  resolvedRead,
@@ -2169,6 +3952,25 @@ export {
2169
3952
  isDisconnected,
2170
3953
  runRead,
2171
3954
  observeState,
2172
- connectStdioUpstream
3955
+ upstreamEnv,
3956
+ fingerprint2,
3957
+ differing,
3958
+ declaredNames,
3959
+ connectUpstream,
3960
+ startTogether,
3961
+ startAll,
3962
+ LOOKED_FOR,
3963
+ CLIENT_IDS,
3964
+ isClientId,
3965
+ discover,
3966
+ ConfigError,
3967
+ serversAt,
3968
+ draftManifest,
3969
+ isWrapped,
3970
+ invokerFor,
3971
+ planInstall,
3972
+ applyInstall,
3973
+ applyUninstall,
3974
+ clientEnvFor
2173
3975
  };
2174
- //# sourceMappingURL=chunk-JE7MOCZO.js.map
3976
+ //# sourceMappingURL=chunk-H4SPMXQW.js.map