synartesis 0.1.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.
@@ -0,0 +1,1383 @@
1
+ import {
2
+ JournalError,
3
+ ManifestError,
4
+ SnapshotError,
5
+ UpstreamError,
6
+ describe
7
+ } from "./chunk-K3QIPVBY.js";
8
+
9
+ // src/invocation.ts
10
+ import { accessSync, constants } from "fs";
11
+ import { basename, delimiter, join } from "path";
12
+ import { fileURLToPath } from "url";
13
+ function onPath(command) {
14
+ const dirs = (process.env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "");
15
+ return dirs.some((dir) => {
16
+ try {
17
+ accessSync(join(dir, command), constants.X_OK);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ });
23
+ }
24
+ var cached;
25
+ function cliCommand() {
26
+ if (cached !== void 0) {
27
+ return cached;
28
+ }
29
+ const invokedAs = process.argv[1];
30
+ if (invokedAs !== void 0 && basename(invokedAs) === "synartesis") {
31
+ cached = "synartesis";
32
+ return cached;
33
+ }
34
+ if (onPath("synartesis")) {
35
+ cached = "synartesis";
36
+ return cached;
37
+ }
38
+ cached = `node ${invokedAs ?? "dist/cli.js"}`;
39
+ return cached;
40
+ }
41
+ function cliCommandFrom(moduleUrl) {
42
+ if (onPath("synartesis")) {
43
+ return "synartesis";
44
+ }
45
+ return `node ${fileURLToPath(new URL("cli.js", moduleUrl))}`;
46
+ }
47
+ function proxyCommand() {
48
+ if (onPath("synartesis")) {
49
+ return "synartesis proxy";
50
+ }
51
+ if (onPath("synartesis-proxy")) {
52
+ return "synartesis-proxy";
53
+ }
54
+ const invokedAs = process.argv[1];
55
+ if (invokedAs !== void 0 && invokedAs.endsWith("cli.js")) {
56
+ return `node ${invokedAs} proxy`;
57
+ }
58
+ return `node ${fileURLToPath(new URL("cli.js", import.meta.url))} proxy`;
59
+ }
60
+
61
+ // src/locate.ts
62
+ import { existsSync } from "fs";
63
+ import { homedir } from "os";
64
+ import { dirname, join as join2, resolve } from "path";
65
+ var MANIFEST_NAME = "synartesis.yaml";
66
+ var JOURNAL_NAME = "journal.db";
67
+ var NESTED_JOURNAL = join2(".synartesis", JOURNAL_NAME);
68
+ function home() {
69
+ return process.env["SYNARTESIS_HOME"] ?? join2(homedir(), ".synartesis");
70
+ }
71
+ function walkUp(from, name) {
72
+ let dir = resolve(from);
73
+ for (; ; ) {
74
+ const candidate = join2(dir, name);
75
+ if (existsSync(candidate)) {
76
+ return candidate;
77
+ }
78
+ const parent = dirname(dir);
79
+ if (parent === dir) {
80
+ return void 0;
81
+ }
82
+ dir = parent;
83
+ }
84
+ }
85
+ function findManifest(given) {
86
+ if (given !== void 0) {
87
+ return given;
88
+ }
89
+ return walkUp(process.cwd(), MANIFEST_NAME) ?? join2(home(), MANIFEST_NAME);
90
+ }
91
+ function findJournal(given, manifest) {
92
+ if (given !== void 0) {
93
+ return given;
94
+ }
95
+ const near = manifest === void 0 ? void 0 : dirname(resolve(manifest));
96
+ for (const dir of [near, process.cwd()]) {
97
+ if (dir === void 0) {
98
+ continue;
99
+ }
100
+ for (const name of [JOURNAL_NAME, NESTED_JOURNAL]) {
101
+ const candidate = join2(dir, name);
102
+ if (existsSync(candidate)) {
103
+ return candidate;
104
+ }
105
+ }
106
+ }
107
+ const found = walkUp(process.cwd(), NESTED_JOURNAL) ?? walkUp(process.cwd(), JOURNAL_NAME);
108
+ if (found !== void 0) {
109
+ return found;
110
+ }
111
+ return near === void 0 ? join2(home(), JOURNAL_NAME) : join2(near, JOURNAL_NAME);
112
+ }
113
+
114
+ // src/style.ts
115
+ var ESC = "\x1B[";
116
+ var ACCENT = `${ESC}38;2;226;134;118m`;
117
+ var ON_ACCENT = `${ESC}48;2;94;20;32m${ESC}38;2;246;233;229m`;
118
+ var BRIGHT = `${ESC}38;2;246;233;229m`;
119
+ var DIM = `${ESC}2m`;
120
+ var BOLD = `${ESC}1m`;
121
+ var RESET = `${ESC}0m`;
122
+ var enabled = process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && process.stdout.isTTY;
123
+ function paint(codes, text) {
124
+ return enabled ? `${codes}${text}${RESET}` : text;
125
+ }
126
+ function spaced(text) {
127
+ return Array.from(text).join(" ");
128
+ }
129
+ var style = {
130
+ /** A section label: small, capital, spaced out. */
131
+ label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
132
+ heading: (text) => paint(BRIGHT + BOLD, text.toUpperCase()),
133
+ accent: (text) => paint(ACCENT, text),
134
+ strong: (text) => paint(BOLD, text),
135
+ quiet: (text) => paint(DIM, text),
136
+ /** Off-white on oxblood, the way the wordmark is set. */
137
+ plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
138
+ };
139
+ var WORDMARK = spaced("SYNARTESIS");
140
+ function meander(width) {
141
+ const unit = "\u2517\u2501\u2513\u250F\u2501\u251B";
142
+ return unit.repeat(Math.max(1, Math.ceil(width / unit.length))).slice(0, width);
143
+ }
144
+ function rule(width = 48) {
145
+ return style.quiet(meander(width));
146
+ }
147
+ var GREEK = spaced("\u03A3\u03A5\u039D\u0391\u03A1\u03A4\u0397\u03A3\u0399\u03A3");
148
+ var MEANING = "a fastening together";
149
+ var TAGLINE = "an undo layer for AI agents";
150
+ function mark() {
151
+ return `
152
+ ${style.plate(WORDMARK)} ${style.quiet(MEANING)}
153
+
154
+ `;
155
+ }
156
+ function banner() {
157
+ return [
158
+ "",
159
+ ` ${style.plate(WORDMARK)}`,
160
+ ` ${style.accent(meander(24))}`,
161
+ "",
162
+ ` ${style.quiet(GREEK)} ${style.quiet("\xB7")} ${style.quiet(MEANING)}`,
163
+ "",
164
+ ` ${style.accent(spaced(TAGLINE.toUpperCase()))}`,
165
+ ` ${style.quiet("Every action is bound to the action that undoes it.")}`,
166
+ ""
167
+ ].join("\n");
168
+ }
169
+
170
+ // src/journal/journal.ts
171
+ import { existsSync as existsSync2, mkdirSync } from "fs";
172
+ import { dirname as dirname2 } from "path";
173
+ import Database from "better-sqlite3";
174
+ import { z } from "zod";
175
+
176
+ // src/canonical.ts
177
+ function canonical(value) {
178
+ if (value === void 0) {
179
+ return "undefined";
180
+ }
181
+ if (value === null || typeof value !== "object") {
182
+ return JSON.stringify(value);
183
+ }
184
+ if (Array.isArray(value)) {
185
+ return `[${value.map(canonical).join(",")}]`;
186
+ }
187
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
188
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
189
+ }
190
+
191
+ // src/journal/schema.ts
192
+ var SCHEMA_VERSION = 3;
193
+ var SCHEMA_SQL = `
194
+ CREATE TABLE IF NOT EXISTS runs (
195
+ id TEXT PRIMARY KEY,
196
+ label TEXT,
197
+ started_at TEXT NOT NULL,
198
+ ended_at TEXT,
199
+ status TEXT NOT NULL CHECK (status IN ('active','complete','rolled_back','partial'))
200
+ );
201
+
202
+ CREATE TABLE IF NOT EXISTS actions (
203
+ id TEXT PRIMARY KEY,
204
+ run_id TEXT NOT NULL REFERENCES runs(id),
205
+ seq INTEGER NOT NULL,
206
+ server TEXT NOT NULL,
207
+ tool TEXT NOT NULL,
208
+ args_json TEXT NOT NULL,
209
+ class TEXT NOT NULL,
210
+ snapshot_json TEXT,
211
+ post_snapshot_json TEXT,
212
+ result_json TEXT,
213
+ inverse_json TEXT,
214
+ verify_json TEXT,
215
+ error TEXT,
216
+ idempotency_key TEXT NOT NULL UNIQUE,
217
+ status TEXT NOT NULL CHECK (status IN
218
+ ('pending','gated','approved','denied','applied','failed',
219
+ 'rolling_back','rolled_back','unrecoverable')),
220
+ approved_by TEXT,
221
+ approved_at TEXT,
222
+ ts TEXT NOT NULL,
223
+ UNIQUE(run_id, seq)
224
+ );
225
+
226
+ CREATE INDEX IF NOT EXISTS actions_by_run ON actions(run_id, seq);
227
+ `;
228
+
229
+ // src/journal/journal.ts
230
+ var SPENT_APPROVAL = "approval was used by action";
231
+ var runSchema = z.object({
232
+ id: z.string(),
233
+ label: z.string().nullable(),
234
+ started_at: z.string(),
235
+ ended_at: z.string().nullable(),
236
+ status: z.enum(["active", "complete", "rolled_back", "partial"])
237
+ });
238
+ var actionSchema = z.object({
239
+ id: z.string(),
240
+ run_id: z.string(),
241
+ seq: z.number(),
242
+ server: z.string(),
243
+ tool: z.string(),
244
+ args_json: z.string(),
245
+ class: z.enum(["unclassified", "readonly", "reversible", "compensable", "irreversible"]),
246
+ snapshot_json: z.string().nullable(),
247
+ post_snapshot_json: z.string().nullable(),
248
+ result_json: z.string().nullable(),
249
+ inverse_json: z.string().nullable(),
250
+ verify_json: z.string().nullable(),
251
+ error: z.string().nullable(),
252
+ idempotency_key: z.string(),
253
+ status: z.enum([
254
+ "pending",
255
+ "gated",
256
+ "approved",
257
+ "denied",
258
+ "applied",
259
+ "failed",
260
+ "rolling_back",
261
+ "rolled_back",
262
+ "unrecoverable"
263
+ ]),
264
+ approved_by: z.string().nullable(),
265
+ approved_at: z.string().nullable(),
266
+ ts: z.string()
267
+ });
268
+ function decode(value) {
269
+ return value === null ? void 0 : JSON.parse(value);
270
+ }
271
+ function orUndefined(value) {
272
+ return value === null ? void 0 : value;
273
+ }
274
+ function toRun(raw) {
275
+ const row = runSchema.parse(raw);
276
+ return {
277
+ id: row.id,
278
+ label: orUndefined(row.label),
279
+ startedAt: row.started_at,
280
+ endedAt: orUndefined(row.ended_at),
281
+ status: row.status
282
+ };
283
+ }
284
+ function toAction(raw) {
285
+ const row = actionSchema.parse(raw);
286
+ return {
287
+ id: row.id,
288
+ runId: row.run_id,
289
+ seq: row.seq,
290
+ server: row.server,
291
+ tool: row.tool,
292
+ args: decode(row.args_json),
293
+ class: row.class,
294
+ snapshot: decode(row.snapshot_json),
295
+ postSnapshot: decode(row.post_snapshot_json),
296
+ result: decode(row.result_json),
297
+ inverse: decode(row.inverse_json),
298
+ verify: decode(row.verify_json),
299
+ error: orUndefined(row.error),
300
+ idempotencyKey: row.idempotency_key,
301
+ status: row.status,
302
+ approvedBy: orUndefined(row.approved_by),
303
+ approvedAt: orUndefined(row.approved_at),
304
+ ts: row.ts
305
+ };
306
+ }
307
+ function openDatabase(path) {
308
+ try {
309
+ const db = new Database(path);
310
+ db.pragma("journal_mode = WAL");
311
+ db.pragma("foreign_keys = ON");
312
+ return db;
313
+ } catch (error) {
314
+ const detail = error instanceof Error ? error.message : String(error);
315
+ if (detail.includes("not a database")) {
316
+ throw new JournalError(
317
+ "open",
318
+ `${path} is not a Synartesis journal. Point --journal at a journal, or at a new file to start one.`
319
+ );
320
+ }
321
+ throw new JournalError("open", `the journal at ${path} could not be opened: ${detail}`);
322
+ }
323
+ }
324
+ var SqliteJournal = class {
325
+ #db;
326
+ constructor(path) {
327
+ if (path !== ":memory:") {
328
+ mkdirSync(dirname2(path), { recursive: true });
329
+ }
330
+ this.#db = openDatabase(path);
331
+ const existing = z.number().parse(this.#db.pragma("user_version", { simple: true }));
332
+ const populated = z.object({ count: z.number() }).parse(
333
+ this.#db.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name = 'runs'").get()
334
+ ).count > 0;
335
+ if (populated && existing !== SCHEMA_VERSION) {
336
+ throw new JournalError(
337
+ "open",
338
+ `journal at ${path} was written by schema version ${String(existing)}, but this build expects ${String(SCHEMA_VERSION)}. Point --journal at a new file to carry on, and keep this one: everything an agent did is in it. Delete it only once you are sure you do not want that history.`
339
+ );
340
+ }
341
+ this.#db.exec(SCHEMA_SQL);
342
+ this.#db.pragma(`user_version = ${String(SCHEMA_VERSION)}`);
343
+ }
344
+ beginRun(label) {
345
+ const id = crypto.randomUUID();
346
+ this.#run("beginRun", () => {
347
+ this.#db.prepare("INSERT INTO runs (id, label, started_at, status) VALUES (?, ?, ?, 'active')").run(id, label ?? null, (/* @__PURE__ */ new Date()).toISOString());
348
+ });
349
+ return id;
350
+ }
351
+ endRun(runId, status) {
352
+ this.#run("endRun", () => {
353
+ this.#db.prepare("UPDATE runs SET ended_at = ?, status = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), status, runId);
354
+ });
355
+ }
356
+ closeAbandonedRun(runId) {
357
+ return this.#run("closeAbandonedRun", () => {
358
+ const last = z.object({ ts: z.string().nullable() }).parse(
359
+ this.#db.prepare("SELECT MAX(ts) AS ts FROM actions WHERE run_id = ?").get(runId) ?? { ts: null }
360
+ ).ts;
361
+ const result = this.#db.prepare("UPDATE runs SET ended_at = ?, status = 'complete' WHERE id = ? AND status = 'active'").run(last ?? (/* @__PURE__ */ new Date()).toISOString(), runId);
362
+ return result.changes === 1;
363
+ });
364
+ }
365
+ setRunLabel(runId, label) {
366
+ this.#run("setRunLabel", () => {
367
+ this.#db.prepare("UPDATE runs SET label = ? WHERE id = ?").run(label, runId);
368
+ });
369
+ }
370
+ recordPending(input) {
371
+ return this.#run("recordPending", () => {
372
+ const insert = this.#db.transaction(() => {
373
+ const next = this.#db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM actions WHERE run_id = ?").get(input.runId);
374
+ const seq = z.object({ seq: z.number() }).parse(next).seq;
375
+ const actionId = crypto.randomUUID();
376
+ const idempotencyKey = `${input.runId}:${String(seq)}`;
377
+ this.#db.prepare(
378
+ `INSERT INTO actions
379
+ (id, run_id, seq, server, tool, args_json, class, idempotency_key, status, ts)
380
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`
381
+ ).run(
382
+ actionId,
383
+ input.runId,
384
+ seq,
385
+ input.server,
386
+ input.tool,
387
+ JSON.stringify(input.args ?? {}),
388
+ input.class,
389
+ idempotencyKey,
390
+ (/* @__PURE__ */ new Date()).toISOString()
391
+ );
392
+ return { actionId, seq, idempotencyKey };
393
+ });
394
+ return insert();
395
+ });
396
+ }
397
+ attachSnapshot(actionId, snapshot) {
398
+ this.#run("attachSnapshot", () => {
399
+ this.#db.prepare("UPDATE actions SET snapshot_json = ? WHERE id = ?").run(JSON.stringify(snapshot ?? null), actionId);
400
+ });
401
+ }
402
+ markApplied(actionId, outcome) {
403
+ this.#run("markApplied", () => {
404
+ this.#db.prepare(
405
+ `UPDATE actions
406
+ SET status = 'applied',
407
+ result_json = ?,
408
+ inverse_json = ?,
409
+ verify_json = ?,
410
+ post_snapshot_json = ?,
411
+ error = ?
412
+ WHERE id = ?`
413
+ ).run(
414
+ JSON.stringify(outcome.result ?? null),
415
+ outcome.inverse === void 0 ? null : JSON.stringify(outcome.inverse),
416
+ outcome.verify === void 0 ? null : JSON.stringify(outcome.verify),
417
+ outcome.postSnapshot === void 0 ? null : JSON.stringify(outcome.postSnapshot),
418
+ outcome.warning ?? null,
419
+ actionId
420
+ );
421
+ });
422
+ }
423
+ markFailed(actionId, error) {
424
+ this.#run("markFailed", () => {
425
+ this.#db.prepare("UPDATE actions SET status = 'failed', error = ? WHERE id = ?").run(error, actionId);
426
+ });
427
+ }
428
+ /**
429
+ * The call was interrupted, so whether the upstream applied it is genuinely
430
+ * unknown. The row deliberately stays `pending`: recording it as failed
431
+ * would assert something we cannot know, and section 3.1 wants exactly this
432
+ * case surfaced rather than resolved by guesswork.
433
+ */
434
+ markUnknown(actionId, error) {
435
+ this.#run("markUnknown", () => {
436
+ this.#db.prepare("UPDATE actions SET status = 'pending', error = ? WHERE id = ?").run(error, actionId);
437
+ });
438
+ }
439
+ markRollingBack(actionId) {
440
+ return this.#run("markRollingBack", () => {
441
+ const result = this.#db.prepare("UPDATE actions SET status = 'rolling_back' WHERE id = ? AND status = 'applied'").run(actionId);
442
+ return result.changes === 1;
443
+ });
444
+ }
445
+ markRolledBack(actionId) {
446
+ this.#run("markRolledBack", () => {
447
+ this.#db.prepare("UPDATE actions SET status = 'rolled_back' WHERE id = ?").run(actionId);
448
+ });
449
+ }
450
+ /**
451
+ * The upstream processed the inverse and refused it, so nothing was applied
452
+ * and the action still needs undoing. Distinct from `unrecoverable`, which
453
+ * means a human has to look: a refused inverse may simply be a server that
454
+ * was briefly unwell, and rollback is expected to be retried (D7).
455
+ */
456
+ markInverseRejected(actionId, error) {
457
+ this.#run("markInverseRejected", () => {
458
+ this.#db.prepare("UPDATE actions SET status = 'applied', error = ? WHERE id = ?").run(error, actionId);
459
+ });
460
+ }
461
+ /**
462
+ * The inverse may or may not have reached the upstream. The row stays in
463
+ * `rolling_back` so the next attempt knows to resolve it by reading the
464
+ * current state rather than assuming either way.
465
+ */
466
+ markUnknownInverse(actionId, error) {
467
+ this.#run("markUnknownInverse", () => {
468
+ this.#db.prepare("UPDATE actions SET status = 'rolling_back', error = ? WHERE id = ?").run(error, actionId);
469
+ });
470
+ }
471
+ markGated(actionId, why) {
472
+ this.#run("markGated", () => {
473
+ this.#db.prepare("UPDATE actions SET status = 'gated', error = ? WHERE id = ?").run(why ?? null, actionId);
474
+ });
475
+ }
476
+ markInFlight(actionId) {
477
+ this.#run("markInFlight", () => {
478
+ this.#db.prepare("UPDATE actions SET status = 'pending' WHERE id = ?").run(actionId);
479
+ });
480
+ }
481
+ /**
482
+ * Conditional on the row still being gated, so a decision made at the same
483
+ * moment as a timeout resolves one way rather than both.
484
+ */
485
+ approve(actionId, by) {
486
+ return this.#run("approve", () => {
487
+ const result = this.#db.prepare(
488
+ `UPDATE actions SET status = 'approved', approved_by = ?, approved_at = ?, error = NULL
489
+ WHERE id = ? AND status = 'gated'`
490
+ ).run(by, (/* @__PURE__ */ new Date()).toISOString(), actionId);
491
+ return result.changes === 1;
492
+ });
493
+ }
494
+ deny(actionId, by, reason) {
495
+ return this.#run("deny", () => {
496
+ const result = this.#db.prepare(
497
+ `UPDATE actions SET status = 'denied', approved_by = ?, approved_at = ?, error = ?
498
+ WHERE id = ? AND status = 'gated'`
499
+ ).run(by ?? null, (/* @__PURE__ */ new Date()).toISOString(), reason, actionId);
500
+ return result.changes === 1;
501
+ });
502
+ }
503
+ settleAsDenied(actionId, by, reason) {
504
+ this.#run("settleAsDenied", () => {
505
+ this.#db.prepare(
506
+ "UPDATE actions SET status = 'denied', approved_by = ?, approved_at = ?, error = ? WHERE id = ?"
507
+ ).run(by ?? null, (/* @__PURE__ */ new Date()).toISOString(), reason, actionId);
508
+ });
509
+ }
510
+ adoptApproval(actionId, granted) {
511
+ this.#run("adoptApproval", () => {
512
+ const move = this.#db.transaction(() => {
513
+ this.#db.prepare("UPDATE actions SET approved_by = ?, approved_at = ? WHERE id = ?").run(granted.approvedBy ?? null, granted.approvedAt ?? null, actionId);
514
+ this.#db.prepare("UPDATE actions SET status = 'denied', error = ? WHERE id = ?").run(`${SPENT_APPROVAL} ${actionId}`, granted.id);
515
+ });
516
+ move();
517
+ });
518
+ }
519
+ listGated() {
520
+ return this.#run(
521
+ "listGated",
522
+ () => this.#db.prepare("SELECT * FROM actions WHERE status = 'gated' ORDER BY ts").all().map(toAction)
523
+ );
524
+ }
525
+ findApproval(query) {
526
+ return this.#run("findApproval", () => {
527
+ const rows = this.#db.prepare(
528
+ `SELECT * FROM actions
529
+ WHERE server = ? AND tool = ?
530
+ AND status = 'approved'
531
+ AND approved_at >= ?
532
+ ORDER BY approved_at DESC`
533
+ ).all(query.server, query.tool, query.notBefore).map(toAction);
534
+ const wanted = canonical(query.args ?? {});
535
+ return rows.find((row) => canonical(row.args) === wanted);
536
+ });
537
+ }
538
+ findGated(query) {
539
+ return this.#run("findGated", () => {
540
+ const rows = this.#db.prepare(
541
+ `SELECT * FROM actions
542
+ WHERE run_id = ? AND server = ? AND tool = ? AND status = 'gated'
543
+ ORDER BY seq`
544
+ ).all(query.runId, query.server, query.tool).map(toAction);
545
+ const wanted = canonical(query.args ?? {});
546
+ return rows.find((row) => canonical(row.args) === wanted);
547
+ });
548
+ }
549
+ getAction(actionId) {
550
+ return this.#run("getAction", () => {
551
+ const raw = this.#db.prepare("SELECT * FROM actions WHERE id = ?").get(actionId);
552
+ return raw === void 0 ? void 0 : toAction(raw);
553
+ });
554
+ }
555
+ markUnrecoverable(actionId, error) {
556
+ this.#run("markUnrecoverable", () => {
557
+ this.#db.prepare("UPDATE actions SET status = 'unrecoverable', error = ? WHERE id = ?").run(error, actionId);
558
+ });
559
+ }
560
+ listRuns() {
561
+ return this.#run(
562
+ "listRuns",
563
+ () => (
564
+ // Insertion order as the tiebreak, not the id. Two runs that start in
565
+ // the same millisecond have equal timestamps, and a uuid orders them at
566
+ // random -- which decides which one `show` and `undo` mean by "the most
567
+ // recent", so the answer has to come from when they were written rather
568
+ // than from what they happen to be called.
569
+ this.#db.prepare("SELECT * FROM runs ORDER BY started_at, rowid").all().map(toRun)
570
+ )
571
+ );
572
+ }
573
+ getRun(runId) {
574
+ return this.#run("getRun", () => {
575
+ const raw = this.#db.prepare("SELECT * FROM runs WHERE id = ?").get(runId);
576
+ return raw === void 0 ? void 0 : toRun(raw);
577
+ });
578
+ }
579
+ getActions(runId) {
580
+ return this.#run(
581
+ "getActions",
582
+ () => this.#db.prepare("SELECT * FROM actions WHERE run_id = ? ORDER BY seq").all(runId).map(toAction)
583
+ );
584
+ }
585
+ recentActions(limit) {
586
+ return this.#run(
587
+ "recentActions",
588
+ () => this.#db.prepare("SELECT * FROM actions ORDER BY ts DESC, seq DESC LIMIT ?").all(limit).map(toAction).reverse()
589
+ );
590
+ }
591
+ pragma(name) {
592
+ return this.#db.pragma(name, { simple: true });
593
+ }
594
+ close() {
595
+ this.#db.close();
596
+ }
597
+ /**
598
+ * A failed journal write means the record of what the agent did is
599
+ * incomplete. It is never swallowed and never merely logged.
600
+ */
601
+ #run(operation, body) {
602
+ try {
603
+ return body();
604
+ } catch (error) {
605
+ throw new JournalError(operation, error);
606
+ }
607
+ }
608
+ };
609
+ function openJournal(path, options = {}) {
610
+ if (options.mustExist === true && path !== ":memory:" && !existsSync2(path)) {
611
+ throw new JournalError("open", `there is no journal at ${path}`);
612
+ }
613
+ return new SqliteJournal(path);
614
+ }
615
+ function labelFor(action) {
616
+ return action.status === "denied" && (action.error ?? "").startsWith(SPENT_APPROVAL) ? "used" : action.status;
617
+ }
618
+ function wasRefused(action) {
619
+ return action.status === "unrecoverable" || labelFor(action) === "denied";
620
+ }
621
+
622
+ // src/manifest/load.ts
623
+ import { readFileSync } from "fs";
624
+ import { LineCounter, isNode, parseDocument } from "yaml";
625
+ import { z as z2 } from "zod";
626
+
627
+ // src/manifest/template.ts
628
+ var NAMESPACES = ["snapshot", "result"];
629
+ function parseReference(raw) {
630
+ if (!raw.startsWith("$")) {
631
+ return void 0;
632
+ }
633
+ if (raw === "$") {
634
+ return { namespace: "args", path: "" };
635
+ }
636
+ if (raw.startsWith("$.") || raw.startsWith("$[")) {
637
+ return { namespace: "args", path: raw.slice(raw[1] === "." ? 2 : 1) };
638
+ }
639
+ for (const namespace of NAMESPACES) {
640
+ if (raw === `$${namespace}`) {
641
+ return { namespace, path: "" };
642
+ }
643
+ const head = `$${namespace}`;
644
+ const after = raw.startsWith(head) ? raw.slice(head.length) : void 0;
645
+ if (after !== void 0 && (after.startsWith(".") || after.startsWith("["))) {
646
+ return { namespace, path: after.startsWith(".") ? after.slice(1) : after };
647
+ }
648
+ }
649
+ throw new ManifestError(
650
+ `unknown interpolation namespace in ${raw}; expected $., $snapshot. or $result.`
651
+ );
652
+ }
653
+ function segments(path, reference) {
654
+ const parts = [];
655
+ for (const chunk of path.split(".")) {
656
+ const match = /^([^[\]]*)((?:\[\d*\])*)$/.exec(chunk);
657
+ if (match === null) {
658
+ throw new ManifestError(`malformed path in ${reference}`);
659
+ }
660
+ const [, head = "", brackets = ""] = match;
661
+ if (head !== "") {
662
+ parts.push({ kind: "key", key: head });
663
+ }
664
+ for (const bracket of brackets.matchAll(/\[(\d*)\]/g)) {
665
+ const index = bracket[1] ?? "";
666
+ parts.push(index === "" ? { kind: "each" } : { kind: "index", index: Number(index) });
667
+ }
668
+ }
669
+ if (parts.length === 0) {
670
+ throw new ManifestError(`empty path in ${reference}`);
671
+ }
672
+ return parts;
673
+ }
674
+ function walk(current, parts, at, reference) {
675
+ const segment = parts[at];
676
+ if (segment === void 0) {
677
+ return current;
678
+ }
679
+ if (current === null || current === void 0) {
680
+ throw new ManifestError(`${reference} is unresolvable: nothing to read from`);
681
+ }
682
+ switch (segment.kind) {
683
+ case "each": {
684
+ if (!Array.isArray(current)) {
685
+ throw new ManifestError(`${reference} is unresolvable: [] needs a list to walk`);
686
+ }
687
+ return current.map((item) => walk(item, parts, at + 1, reference));
688
+ }
689
+ case "index": {
690
+ if (!Array.isArray(current) || segment.index >= current.length) {
691
+ throw new ManifestError(
692
+ `${reference} is unresolvable: index ${String(segment.index)} is absent`
693
+ );
694
+ }
695
+ return walk(current[segment.index], parts, at + 1, reference);
696
+ }
697
+ case "key": {
698
+ if (typeof current !== "object" || !(segment.key in current)) {
699
+ throw new ManifestError(`${reference} is unresolvable: ${segment.key} is absent`);
700
+ }
701
+ const next = Object.getOwnPropertyDescriptor(current, segment.key)?.value;
702
+ return walk(next, parts, at + 1, reference);
703
+ }
704
+ }
705
+ }
706
+ function read(root, path, reference) {
707
+ return walk(root, segments(path, reference), 0, reference);
708
+ }
709
+ var EMBEDDED = /\$(?:snapshot|result)?\.[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d*\])*/g;
710
+ var ESCAPE = "\0synartesis-dollar\0";
711
+ function stringify(value) {
712
+ return typeof value === "string" ? value : JSON.stringify(value);
713
+ }
714
+ function resolveString(raw, context) {
715
+ const whole = raw.startsWith("$$") ? void 0 : parseReference(raw);
716
+ if (whole !== void 0) {
717
+ return readNamespace(whole, raw, context);
718
+ }
719
+ const escaped = raw.split("$$").join(ESCAPE);
720
+ const substituted = escaped.replace(EMBEDDED, (token) => {
721
+ const reference = parseReference(token);
722
+ if (reference === void 0) {
723
+ return token;
724
+ }
725
+ return stringify(readNamespace(reference, token, context));
726
+ });
727
+ return substituted.split(ESCAPE).join("$");
728
+ }
729
+ function readNamespace(reference, raw, context) {
730
+ const root = context[reference.namespace];
731
+ if (root === void 0) {
732
+ throw new ManifestError(
733
+ `${raw} refers to ${reference.namespace}, which is not available at this point`
734
+ );
735
+ }
736
+ return reference.path === "" ? root : read(root, reference.path, raw);
737
+ }
738
+ function isTemplateArray(value) {
739
+ return Array.isArray(value);
740
+ }
741
+ function resolveTemplate(template, context) {
742
+ if (typeof template === "string") {
743
+ return resolveString(template, context);
744
+ }
745
+ if (isTemplateArray(template)) {
746
+ return template.map((item) => resolveTemplate(item, context));
747
+ }
748
+ if (template !== null && typeof template === "object") {
749
+ return Object.fromEntries(
750
+ Object.entries(template).map(([key, value]) => [key, resolveTemplate(value, context)])
751
+ );
752
+ }
753
+ return template;
754
+ }
755
+ function referencesIn(template) {
756
+ if (typeof template === "string") {
757
+ if (template.startsWith("$$")) {
758
+ return [];
759
+ }
760
+ if (parseReference(template) !== void 0) {
761
+ return [template];
762
+ }
763
+ return template.split("$$").join(ESCAPE).match(EMBEDDED) ?? [];
764
+ }
765
+ if (isTemplateArray(template)) {
766
+ return template.flatMap(referencesIn);
767
+ }
768
+ if (template !== null && typeof template === "object") {
769
+ return Object.values(template).flatMap(referencesIn);
770
+ }
771
+ return [];
772
+ }
773
+
774
+ // src/manifest/load.ts
775
+ var templateValue = z2.lazy(
776
+ () => z2.union([
777
+ z2.string(),
778
+ z2.number(),
779
+ z2.boolean(),
780
+ z2.null(),
781
+ z2.array(templateValue),
782
+ z2.record(z2.string(), templateValue)
783
+ ])
784
+ );
785
+ var callTemplate = z2.strictObject({
786
+ tool: z2.string().min(1),
787
+ args: z2.record(z2.string(), templateValue).default({})
788
+ });
789
+ var toolPolicy = z2.strictObject({
790
+ match: z2.string().min(1),
791
+ class: z2.enum(["readonly", "reversible", "compensable", "irreversible"]),
792
+ gate: z2.enum(["always", "on_write", "never"]).optional(),
793
+ snapshot: callTemplate.optional(),
794
+ inverse: callTemplate.optional()
795
+ });
796
+ var serverSpec = z2.strictObject({
797
+ command: z2.string().min(1),
798
+ args: z2.array(z2.string()).default([]),
799
+ env: z2.record(z2.string(), z2.string()).optional()
800
+ });
801
+ var manifestSchema = z2.strictObject({
802
+ version: z2.literal(1),
803
+ servers: z2.record(z2.string(), serverSpec),
804
+ tools: z2.array(toolPolicy).default([])
805
+ });
806
+ var Source = class {
807
+ constructor(doc, lines, file) {
808
+ this.doc = doc;
809
+ this.lines = lines;
810
+ this.file = file;
811
+ }
812
+ doc;
813
+ lines;
814
+ file;
815
+ /** Narrows to the deepest node that still exists, so a location is always given. */
816
+ locate(path) {
817
+ for (let depth = path.length; depth >= 0; depth -= 1) {
818
+ const node = depth === 0 ? this.doc.contents : this.doc.getIn(path.slice(0, depth), true);
819
+ const range = isNode(node) ? node.range : void 0;
820
+ if (range != null) {
821
+ const position = this.lines.linePos(range[0]);
822
+ return { file: this.file, line: position.line, column: position.col };
823
+ }
824
+ }
825
+ return { file: this.file, line: 1, column: 1 };
826
+ }
827
+ fail(path, message) {
828
+ throw new ManifestError(message, this.locate(path));
829
+ }
830
+ };
831
+ var REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
832
+ function expandEnvironment(source, path, env) {
833
+ const expanded = {};
834
+ for (const [key, value] of Object.entries(env)) {
835
+ expanded[key] = value.replace(REFERENCE, (whole, name) => {
836
+ const found = process.env[name];
837
+ if (found === void 0) {
838
+ source.fail(
839
+ [...path, "env", key],
840
+ `${whole} is not set in this environment; export ${name} before starting, or write the value here`
841
+ );
842
+ }
843
+ return found;
844
+ });
845
+ }
846
+ return expanded;
847
+ }
848
+ function serverSegment(pattern) {
849
+ const dot = pattern.indexOf(".");
850
+ return dot === -1 ? "" : pattern.slice(0, dot);
851
+ }
852
+ function matchesAnyServer(segment, servers) {
853
+ if (!segment.includes("*")) {
854
+ return servers.includes(segment);
855
+ }
856
+ const source = segment.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
857
+ const test = new RegExp(`^${source}$`);
858
+ return servers.some((name) => test.test(name));
859
+ }
860
+ function checkCall(source, path, call, servers, allowed) {
861
+ const segment = serverSegment(call.tool);
862
+ if (segment === "" || call.tool.endsWith(".")) {
863
+ source.fail([...path, "tool"], `${call.tool} must be qualified as server.tool`);
864
+ }
865
+ if (segment.includes("*")) {
866
+ source.fail([...path, "tool"], `${call.tool} must name one server, not a pattern`);
867
+ }
868
+ if (!servers.includes(segment)) {
869
+ source.fail([...path, "tool"], `${call.tool} names server ${segment}, which is not declared`);
870
+ }
871
+ for (const reference of referencesIn(call.args)) {
872
+ const namespace = /^\$(\w*)(?:[.[]|$)/.exec(reference)?.[1] ?? "";
873
+ const label = namespace === "" ? "$." : `$${namespace}.`;
874
+ if (!allowed.includes(label)) {
875
+ source.fail(
876
+ [...path, "args"],
877
+ `${reference} uses ${label}, which is not available here; allowed: ${allowed.join(", ")}`
878
+ );
879
+ }
880
+ }
881
+ }
882
+ function validate(source, manifest) {
883
+ const servers = Object.keys(manifest.servers);
884
+ if (servers.length === 0) {
885
+ source.fail(["servers"], "at least one server must be declared");
886
+ }
887
+ const seen = /* @__PURE__ */ new Map();
888
+ manifest.tools.forEach((policy, index) => {
889
+ const path = ["tools", index];
890
+ const previous = seen.get(policy.match);
891
+ if (previous !== void 0) {
892
+ source.fail(
893
+ [...path, "match"],
894
+ `duplicate match pattern ${policy.match}; it is already declared at tools[${String(previous)}]`
895
+ );
896
+ }
897
+ seen.set(policy.match, index);
898
+ const segment = serverSegment(policy.match);
899
+ if (segment === "") {
900
+ source.fail([...path, "match"], `${policy.match} must be qualified as server.tool`);
901
+ }
902
+ if (!matchesAnyServer(segment, servers)) {
903
+ source.fail(
904
+ [...path, "match"],
905
+ `${policy.match} names server ${segment}, which is not declared`
906
+ );
907
+ }
908
+ const needsInverse = policy.class === "reversible" || policy.class === "compensable";
909
+ if (needsInverse && policy.inverse === void 0) {
910
+ source.fail(path, `a ${policy.class} tool must declare an inverse`);
911
+ }
912
+ if (!needsInverse && policy.inverse !== void 0) {
913
+ source.fail([...path, "inverse"], `a ${policy.class} tool must not declare an inverse`);
914
+ }
915
+ const needsSnapshot = policy.inverse !== void 0 && referencesIn(policy.inverse.args).some(
916
+ (reference) => reference === "$snapshot" || reference.startsWith("$snapshot.")
917
+ );
918
+ if (policy.class === "reversible" && needsSnapshot && policy.snapshot === void 0) {
919
+ source.fail(path, "this inverse reads $snapshot, so a snapshot must be declared");
920
+ }
921
+ if (policy.class === "readonly" && policy.snapshot !== void 0) {
922
+ source.fail([...path, "snapshot"], "a readonly tool must not declare a snapshot");
923
+ }
924
+ if (policy.snapshot !== void 0) {
925
+ checkCall(source, [...path, "snapshot"], policy.snapshot, servers, ["$."]);
926
+ }
927
+ if (policy.inverse !== void 0) {
928
+ const allowed = ["$.", "$result."];
929
+ if (policy.snapshot !== void 0) {
930
+ allowed.push("$snapshot.");
931
+ }
932
+ checkCall(source, [...path, "inverse"], policy.inverse, servers, allowed);
933
+ }
934
+ });
935
+ }
936
+ function withGate(policy) {
937
+ const gate = policy.gate ?? (policy.class === "irreversible" ? "always" : "never");
938
+ return {
939
+ match: policy.match,
940
+ class: policy.class,
941
+ gate,
942
+ ...policy.snapshot === void 0 ? {} : { snapshot: policy.snapshot },
943
+ ...policy.inverse === void 0 ? {} : { inverse: policy.inverse }
944
+ };
945
+ }
946
+ function parseManifest(text, file) {
947
+ const lines = new LineCounter();
948
+ const doc = parseDocument(text, { lineCounter: lines });
949
+ const syntaxError = doc.errors[0];
950
+ if (syntaxError !== void 0) {
951
+ const position = lines.linePos(syntaxError.pos[0]);
952
+ throw new ManifestError(syntaxError.message, {
953
+ file,
954
+ line: position.line,
955
+ column: position.col
956
+ });
957
+ }
958
+ const source = new Source(doc, lines, file);
959
+ const parsed = manifestSchema.safeParse(doc.toJS());
960
+ if (!parsed.success) {
961
+ const issue = parsed.error.issues[0];
962
+ if (issue === void 0) {
963
+ throw new ManifestError("manifest failed validation", source.locate([]));
964
+ }
965
+ const path = issue.path.filter(
966
+ (segment) => typeof segment !== "symbol"
967
+ );
968
+ const where = path.length === 0 ? "" : `${path.join(".")}: `;
969
+ throw new ManifestError(`${where}${issue.message}`, source.locate(path));
970
+ }
971
+ const manifest = {
972
+ version: parsed.data.version,
973
+ servers: Object.fromEntries(
974
+ Object.entries(parsed.data.servers).map(([name, spec]) => [
975
+ name,
976
+ {
977
+ command: spec.command,
978
+ args: spec.args,
979
+ ...spec.env === void 0 ? {} : { env: expandEnvironment(source, ["servers", name], spec.env) }
980
+ }
981
+ ])
982
+ ),
983
+ tools: parsed.data.tools.map(withGate)
984
+ };
985
+ validate(source, manifest);
986
+ return manifest;
987
+ }
988
+ function loadManifest(path) {
989
+ let text;
990
+ try {
991
+ text = readFileSync(path, "utf8");
992
+ } catch (error) {
993
+ throw new ManifestError(`cannot read manifest at ${path}: ${describe(error)}`);
994
+ }
995
+ return parseManifest(text, path);
996
+ }
997
+
998
+ // src/manifest/verify.ts
999
+ import { z as z3 } from "zod";
1000
+
1001
+ // src/manifest/types.ts
1002
+ function qualify(server, tool) {
1003
+ return `${server}.${tool}`;
1004
+ }
1005
+ function splitQualified(qualified) {
1006
+ const dot = qualified.indexOf(".");
1007
+ if (dot <= 0 || dot === qualified.length - 1) {
1008
+ return void 0;
1009
+ }
1010
+ return { server: qualified.slice(0, dot), tool: qualified.slice(dot + 1) };
1011
+ }
1012
+
1013
+ // src/manifest/verify.ts
1014
+ var listSchema = z3.looseObject({
1015
+ tools: z3.array(z3.looseObject({ name: z3.string() })),
1016
+ nextCursor: z3.string().optional()
1017
+ });
1018
+ async function toolNames(upstream) {
1019
+ const names = /* @__PURE__ */ new Set();
1020
+ let cursor;
1021
+ do {
1022
+ const page = listSchema.parse(
1023
+ await upstream.client.request(
1024
+ { method: "tools/list", params: cursor === void 0 ? {} : { cursor } },
1025
+ z3.looseObject({})
1026
+ )
1027
+ );
1028
+ for (const tool of page.tools) {
1029
+ names.add(tool.name);
1030
+ }
1031
+ cursor = page.nextCursor;
1032
+ } while (cursor !== void 0);
1033
+ return names;
1034
+ }
1035
+ async function verifyAgainstServers(upstreams, manifest) {
1036
+ const available = /* @__PURE__ */ new Map();
1037
+ for (const upstream of upstreams) {
1038
+ available.set(upstream.name, await toolNames(upstream));
1039
+ }
1040
+ const problems = [];
1041
+ const check = (qualified, role, match) => {
1042
+ const target = splitQualified(qualified);
1043
+ if (target === void 0) {
1044
+ return;
1045
+ }
1046
+ const names = available.get(target.server);
1047
+ if (names === void 0) {
1048
+ problems.push(`${match}: its ${role} names server ${target.server}, which is not connected`);
1049
+ return;
1050
+ }
1051
+ if (!names.has(target.tool)) {
1052
+ problems.push(
1053
+ `${match}: its ${role} calls ${qualified}, which ${target.server} does not expose`
1054
+ );
1055
+ }
1056
+ };
1057
+ for (const policy of manifest.tools) {
1058
+ if (policy.snapshot !== void 0) {
1059
+ check(policy.snapshot.tool, "snapshot", policy.match);
1060
+ }
1061
+ if (policy.inverse !== void 0) {
1062
+ check(policy.inverse.tool, "inverse", policy.match);
1063
+ }
1064
+ }
1065
+ if (problems.length > 0) {
1066
+ throw new ManifestError(`the manifest calls tools that do not exist:
1067
+ ${problems.join("\n ")}`);
1068
+ }
1069
+ }
1070
+
1071
+ // src/proxy/routing.ts
1072
+ var SEPARATOR = "__";
1073
+ function createRouter(upstreams, manifest) {
1074
+ if (upstreams.length === 0) {
1075
+ throw new ManifestError("no upstream servers were connected");
1076
+ }
1077
+ for (const upstream of upstreams) {
1078
+ if (!(upstream.name in manifest.servers)) {
1079
+ throw new ManifestError(
1080
+ `upstream ${upstream.name} is connected but not declared in the manifest`
1081
+ );
1082
+ }
1083
+ if (upstream.name.includes(SEPARATOR) || upstream.name.includes(".")) {
1084
+ throw new ManifestError(
1085
+ `server name ${upstream.name} may not contain "." or "${SEPARATOR}"; both are reserved for qualifying tool names`
1086
+ );
1087
+ }
1088
+ }
1089
+ const byName = new Map(upstreams.map((upstream) => [upstream.name, upstream]));
1090
+ if (byName.size !== upstreams.length) {
1091
+ throw new ManifestError("two upstreams were connected under the same name");
1092
+ }
1093
+ const prefixed = upstreams.length > 1;
1094
+ const keys = [...byName.keys()].sort((a, b) => b.length - a.length);
1095
+ return {
1096
+ prefixed,
1097
+ upstreams,
1098
+ expose(server, name) {
1099
+ return prefixed ? `${server}${SEPARATOR}${name}` : name;
1100
+ },
1101
+ route(exposed) {
1102
+ if (!prefixed) {
1103
+ const only = upstreams[0];
1104
+ if (only === void 0) {
1105
+ return void 0;
1106
+ }
1107
+ const prefix = `${only.name}${SEPARATOR}`;
1108
+ return exposed.startsWith(prefix) ? { upstream: only, tool: exposed.slice(prefix.length) } : { upstream: only, tool: exposed };
1109
+ }
1110
+ for (const key of keys) {
1111
+ const prefix = `${key}${SEPARATOR}`;
1112
+ if (exposed.startsWith(prefix)) {
1113
+ const upstream = byName.get(key);
1114
+ if (upstream !== void 0) {
1115
+ return { upstream, tool: exposed.slice(prefix.length) };
1116
+ }
1117
+ }
1118
+ }
1119
+ return void 0;
1120
+ },
1121
+ byName(server) {
1122
+ return byName.get(server);
1123
+ }
1124
+ };
1125
+ }
1126
+
1127
+ // src/proxy/upstream.ts
1128
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1129
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1130
+ function describeError(error) {
1131
+ return error instanceof Error ? error.message : String(error);
1132
+ }
1133
+ var PROXY_CLIENT_INFO = { name: "synartesis-proxy", version: "0.0.0" };
1134
+ function bufferedText(stream) {
1135
+ if (typeof stream !== "object" || stream === null || !("read" in stream)) {
1136
+ return "";
1137
+ }
1138
+ const read2 = stream.read;
1139
+ if (typeof read2 !== "function") {
1140
+ return "";
1141
+ }
1142
+ const chunk = read2.call(stream);
1143
+ if (typeof chunk === "string") {
1144
+ return chunk;
1145
+ }
1146
+ return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : "";
1147
+ }
1148
+ function lastWords(text) {
1149
+ const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
1150
+ const kept = lines.slice(-4).join("; ");
1151
+ return kept === "" ? void 0 : kept;
1152
+ }
1153
+ async function connectStdioUpstream(spec) {
1154
+ const started = await start(spec);
1155
+ let current = started;
1156
+ return {
1157
+ name: spec.name,
1158
+ get client() {
1159
+ return current.client;
1160
+ },
1161
+ async reconnect() {
1162
+ await current.client.close().catch(() => void 0);
1163
+ current = await start(spec);
1164
+ },
1165
+ close: async () => {
1166
+ await current.client.close();
1167
+ }
1168
+ };
1169
+ }
1170
+ async function start(spec) {
1171
+ const wanted = spec.stderr ?? "inherit";
1172
+ const transport = new StdioClientTransport({
1173
+ command: spec.command,
1174
+ args: [...spec.args ?? []],
1175
+ ...spec.env === void 0 ? {} : { env: { ...spec.env } },
1176
+ // "pipe" is what the sdk calls it; captured here so a failure can quote it.
1177
+ stderr: wanted === "capture" ? "pipe" : wanted
1178
+ });
1179
+ const client = new Client({ ...PROXY_CLIENT_INFO });
1180
+ let said = "";
1181
+ try {
1182
+ await client.connect(transport);
1183
+ } catch (error) {
1184
+ said = bufferedText(transport.stderr);
1185
+ const reason = lastWords(said);
1186
+ throw new UpstreamError(
1187
+ spec.name,
1188
+ "connect",
1189
+ reason === void 0 ? error : `${describeError(error)} \u2014 the server said: ${reason}`
1190
+ );
1191
+ }
1192
+ return { client };
1193
+ }
1194
+
1195
+ // src/manifest/match.ts
1196
+ function toRegExp(pattern) {
1197
+ const source = pattern.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
1198
+ return new RegExp(`^${source}$`);
1199
+ }
1200
+ function literalLength(pattern) {
1201
+ return pattern.length - pattern.split("*").length + 1;
1202
+ }
1203
+ function failClosed(qualifiedName) {
1204
+ return { match: qualifiedName, class: "irreversible", gate: "always" };
1205
+ }
1206
+ function createPolicyResolver(manifest) {
1207
+ const compiled = manifest.tools.map((policy) => ({
1208
+ policy,
1209
+ test: toRegExp(policy.match),
1210
+ specificity: literalLength(policy.match),
1211
+ wildcards: policy.match.split("*").length - 1
1212
+ })).sort((a, b) => b.specificity - a.specificity || a.wildcards - b.wildcards);
1213
+ const cache = /* @__PURE__ */ new Map();
1214
+ return {
1215
+ resolve(qualifiedName) {
1216
+ const cached2 = cache.get(qualifiedName);
1217
+ if (cached2 !== void 0) {
1218
+ return cached2;
1219
+ }
1220
+ const hit = compiled.find((candidate) => candidate.test.test(qualifiedName));
1221
+ const match = hit === void 0 ? { policy: failClosed(qualifiedName), matched: false } : { policy: hit.policy, matched: true };
1222
+ cache.set(qualifiedName, match);
1223
+ return match;
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ // src/proxy/snapshot.ts
1229
+ import { z as z4 } from "zod";
1230
+ var ToolResult = z4.looseObject({
1231
+ isError: z4.boolean().default(false),
1232
+ content: z4.array(z4.looseObject({ type: z4.string() })).default([])
1233
+ });
1234
+ function refusal(result) {
1235
+ const parsed = ToolResult.safeParse(result);
1236
+ if (!parsed.success || !parsed.data.isError) {
1237
+ return void 0;
1238
+ }
1239
+ const said = parsed.data.content.map((block) => typeof block["text"] === "string" ? block["text"] : "").filter((text) => text !== "").join(" ");
1240
+ return said === "" ? JSON.stringify(result) : said;
1241
+ }
1242
+ function isRecord(value) {
1243
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1244
+ }
1245
+ function toPayload(result) {
1246
+ if (!isRecord(result)) {
1247
+ return result;
1248
+ }
1249
+ const structured = result["structuredContent"];
1250
+ if (structured !== void 0) {
1251
+ return structured;
1252
+ }
1253
+ const content = result["content"];
1254
+ if (Array.isArray(content) && content.length === 1) {
1255
+ const block = content[0];
1256
+ if (isRecord(block) && block["type"] === "text" && typeof block["text"] === "string") {
1257
+ const text = block["text"];
1258
+ try {
1259
+ return JSON.parse(text);
1260
+ } catch {
1261
+ return text;
1262
+ }
1263
+ }
1264
+ }
1265
+ return result;
1266
+ }
1267
+ function resolveArgs(call, context) {
1268
+ const resolved = resolveTemplate(call.args, context);
1269
+ if (!isRecord(resolved)) {
1270
+ throw new ManifestError(`${call.tool} resolved to arguments that are not an object`);
1271
+ }
1272
+ return resolved;
1273
+ }
1274
+ function planInverse(call, context) {
1275
+ const target = splitQualified(call.tool);
1276
+ if (target === void 0) {
1277
+ throw new ManifestError(`inverse tool ${call.tool} is not qualified as server.tool`);
1278
+ }
1279
+ return { server: target.server, tool: target.tool, args: resolveArgs(call, context) };
1280
+ }
1281
+ function planRead(call, context) {
1282
+ const target = splitQualified(call.tool);
1283
+ if (target === void 0) {
1284
+ throw new SnapshotError(call.tool, "the snapshot tool is not qualified as server.tool");
1285
+ }
1286
+ try {
1287
+ return { server: target.server, tool: target.tool, args: resolveArgs(call, context) };
1288
+ } catch (error) {
1289
+ throw new SnapshotError(call.tool, describe(error), { cause: error });
1290
+ }
1291
+ }
1292
+ function isDisconnected(error) {
1293
+ const message = error instanceof Error ? error.message : String(error);
1294
+ return message.includes("Not connected") || message.includes("Connection closed");
1295
+ }
1296
+ function mayHaveArrived(error) {
1297
+ const message = error instanceof Error ? error.message : String(error);
1298
+ return message.includes("Connection closed");
1299
+ }
1300
+ async function runRead(router, read2, signal) {
1301
+ const label = `${read2.server}.${read2.tool}`;
1302
+ const upstream = router.byName(read2.server);
1303
+ if (upstream === void 0) {
1304
+ throw new SnapshotError(label, `server ${read2.server} is not connected`);
1305
+ }
1306
+ const { tool, args } = read2;
1307
+ const ask = () => upstream.client.request(
1308
+ { method: "tools/call", params: { name: tool, arguments: args } },
1309
+ z4.looseObject({}),
1310
+ { signal }
1311
+ );
1312
+ let raw;
1313
+ try {
1314
+ raw = await ask();
1315
+ } catch (error) {
1316
+ if (!isDisconnected(error) || upstream.reconnect === void 0) {
1317
+ throw new SnapshotError(label, describe(error), { cause: error });
1318
+ }
1319
+ try {
1320
+ await upstream.reconnect();
1321
+ raw = await ask();
1322
+ } catch (retry) {
1323
+ if (isDisconnected(retry)) {
1324
+ await upstream.reconnect().catch(() => void 0);
1325
+ }
1326
+ throw new SnapshotError(
1327
+ label,
1328
+ `${describe(error)} (the connection to ${read2.server} was restarted and the read failed again: ${describe(retry)})`,
1329
+ { cause: retry }
1330
+ );
1331
+ }
1332
+ }
1333
+ const parsed = ToolResult.safeParse(raw);
1334
+ if (parsed.success && parsed.data.isError) {
1335
+ throw new SnapshotError(label, `the read reported an error: ${JSON.stringify(raw)}`, {
1336
+ absent: true
1337
+ });
1338
+ }
1339
+ return toPayload(raw);
1340
+ }
1341
+ async function observeState(router, read2, signal) {
1342
+ try {
1343
+ return { present: true, value: await runRead(router, read2, signal) };
1344
+ } catch (error) {
1345
+ if (error instanceof SnapshotError && error.absent) {
1346
+ return { present: false };
1347
+ }
1348
+ throw error;
1349
+ }
1350
+ }
1351
+
1352
+ export {
1353
+ cliCommand,
1354
+ cliCommandFrom,
1355
+ proxyCommand,
1356
+ findManifest,
1357
+ findJournal,
1358
+ style,
1359
+ WORDMARK,
1360
+ rule,
1361
+ mark,
1362
+ banner,
1363
+ canonical,
1364
+ openJournal,
1365
+ labelFor,
1366
+ wasRefused,
1367
+ parseManifest,
1368
+ loadManifest,
1369
+ qualify,
1370
+ verifyAgainstServers,
1371
+ createPolicyResolver,
1372
+ createRouter,
1373
+ refusal,
1374
+ toPayload,
1375
+ planInverse,
1376
+ planRead,
1377
+ isDisconnected,
1378
+ mayHaveArrived,
1379
+ runRead,
1380
+ observeState,
1381
+ connectStdioUpstream
1382
+ };
1383
+ //# sourceMappingURL=chunk-X4VQNEP5.js.map