synartesis 0.8.8 → 0.9.0

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