synartesis 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  observeState,
16
16
  openJournal,
17
17
  parseManifest,
18
+ pathBinaryMatches,
18
19
  planInverse,
19
20
  planRead,
20
21
  proxyCommand,
@@ -24,19 +25,20 @@ import {
24
25
  toPayload,
25
26
  verifyAgainstServers,
26
27
  wasRefused
27
- } from "./chunk-LNR5GXPG.js";
28
+ } from "./chunk-FOA4UIDE.js";
28
29
  import {
29
30
  DriftConflict,
30
31
  ManifestError,
31
32
  RollbackHalted,
32
33
  SynartesisError,
33
34
  UpstreamError,
35
+ changedLines,
34
36
  describe
35
- } from "./chunk-WNLRMSDB.js";
37
+ } from "./chunk-YVOO3PTV.js";
36
38
 
37
39
  // src/cli.ts
38
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
39
- import { dirname, join, resolve } from "path";
40
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
41
+ import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
40
42
  import { fileURLToPath as fileURLToPath2 } from "url";
41
43
 
42
44
  // src/init/draft.ts
@@ -285,9 +287,9 @@ async function draftManifest(options) {
285
287
  return adopted === void 0 || known === void 0 ? { yaml: merged } : { yaml: merged, adopted: { server: known.name, tools: adopted.covered } };
286
288
  }
287
289
  function mergeInto(existing, server, policies, name) {
288
- const serversAt = existing.indexOf("\nservers:");
290
+ const serversAt2 = existing.indexOf("\nservers:");
289
291
  const toolsAt = existing.indexOf("\ntools:");
290
- if (serversAt === -1 || toolsAt === -1 || toolsAt < serversAt) {
292
+ if (serversAt2 === -1 || toolsAt === -1 || toolsAt < serversAt2) {
291
293
  throw new ManifestError(
292
294
  "the existing manifest does not have a servers: block followed by a tools: block, so it cannot be extended automatically"
293
295
  );
@@ -321,7 +323,7 @@ var toolResult = z2.looseObject({ isError: z2.boolean().default(false) });
321
323
  function sameState(a, b) {
322
324
  return canonical(a) === canonical(b);
323
325
  }
324
- function classify(action, replanning) {
326
+ function classify(action, replanning, goAhead) {
325
327
  switch (action.status) {
326
328
  case "rolled_back":
327
329
  return { kind: "already-reverted", reason: "already rolled back", verified: true };
@@ -342,7 +344,7 @@ function classify(action, replanning) {
342
344
  if (action.inverse === void 0) {
343
345
  return void 0;
344
346
  }
345
- return replanning ? void 0 : { kind: "halt", reason: "halted here on an earlier attempt", verified: false };
347
+ return replanning || goAhead ? void 0 : { kind: "halt", reason: "halted here on an earlier attempt", verified: false };
346
348
  case "applied":
347
349
  case "rolling_back":
348
350
  return void 0;
@@ -351,6 +353,7 @@ function classify(action, replanning) {
351
353
  async function rollback(options) {
352
354
  const { journal, router, runId } = options;
353
355
  const dryRun = options.dryRun ?? false;
356
+ const force = options.force ?? false;
354
357
  const signal = options.signal ?? new AbortController().signal;
355
358
  const policies = options.replanWith === void 0 ? void 0 : createPolicyResolver(options.replanWith);
356
359
  const replan = (action) => {
@@ -382,13 +385,14 @@ async function rollback(options) {
382
385
  let halted;
383
386
  let leftInPlace = false;
384
387
  for (const action of inScope) {
385
- const early = classify(action, policies !== void 0);
388
+ let forcedOver;
389
+ const early = classify(action, policies !== void 0, force || dryRun);
386
390
  if (early?.kind === "halt") {
387
391
  const seen = action.error ?? "";
388
- const detail = action.status === "unrecoverable" && seen !== "" ? `what it saw when it halted, which may no longer hold:
389
- ${seen}
390
- Resolve the conflict, then run undo --replan to check it against the world as it is now.` : seen;
391
- halted = { seq: action.seq, reason: early.reason, detail };
392
+ const conflicted = action.status === "unrecoverable" && seen !== "";
393
+ const detail = conflicted ? `what it saw last time, which may no longer hold:
394
+ ${seen}` : seen;
395
+ halted = { seq: action.seq, reason: early.reason, detail, ...conflicted ? { conflict: true } : {} };
392
396
  steps.push({ ...describeStep(action), ...early });
393
397
  break;
394
398
  }
@@ -440,9 +444,18 @@ Resolve the conflict, then run undo --replan to check it against the world as it
440
444
  journal.markRolledBack(action.id);
441
445
  }
442
446
  continue;
443
- } else {
447
+ } else if (!force) {
444
448
  const conflict = new DriftConflict(action.seq, recordedPost.data, current);
445
- halted = { seq: action.seq, reason: "drift detected", detail: conflict.message };
449
+ halted = {
450
+ seq: action.seq,
451
+ reason: "drift detected",
452
+ detail: conflict.message,
453
+ conflict: true,
454
+ // What the person deciding actually needs: not only that it changed,
455
+ // but which lines undoing would write over. A halt that shows the
456
+ // first and hides the second leaves them choosing blind.
457
+ overwrites: overwriteText(current, action)
458
+ };
446
459
  steps.push({
447
460
  ...describeStep(action),
448
461
  kind: "halt",
@@ -454,6 +467,8 @@ Resolve the conflict, then run undo --replan to check it against the world as it
454
467
  journal.markUnrecoverable(action.id, conflict.message);
455
468
  }
456
469
  break;
470
+ } else {
471
+ forcedOver = "the resource had changed since; that change was overwritten";
457
472
  }
458
473
  }
459
474
  if (!verified && action.status === "rolling_back") {
@@ -468,7 +483,7 @@ Resolve the conflict, then run undo --replan to check it against the world as it
468
483
  steps.push({
469
484
  ...describeStep(action),
470
485
  kind: "revert",
471
- reason: verified ? "state matches; applying inverse" : unverifiedBecause(action),
486
+ reason: verified ? "state matches; applying inverse" : forcedOver ?? unverifiedBecause(action),
472
487
  verified,
473
488
  plan,
474
489
  ...rebuilt.inverse === void 0 ? {} : { replanned: true }
@@ -537,6 +552,13 @@ function unverifiedBecause(action) {
537
552
  function describeStep(action) {
538
553
  return { seq: action.seq, server: action.server, tool: action.tool };
539
554
  }
555
+ function overwriteText(current, action) {
556
+ const intended2 = intendedAfterInverse(action);
557
+ if (intended2 === void 0) {
558
+ return "";
559
+ }
560
+ return changedLines(current, intended2);
561
+ }
540
562
  function intendedAfterInverse(action) {
541
563
  return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
542
564
  }
@@ -573,6 +595,98 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
573
595
  return { ok: true };
574
596
  }
575
597
 
598
+ // src/rollback/inspect.ts
599
+ import { z as z3 } from "zod";
600
+ var inversePlan2 = z3.object({
601
+ server: z3.string(),
602
+ tool: z3.string(),
603
+ args: z3.record(z3.string(), z3.unknown())
604
+ });
605
+ var observation2 = z3.union([
606
+ z3.object({ present: z3.literal(true), value: z3.unknown() }),
607
+ z3.object({ present: z3.literal(false) })
608
+ ]);
609
+ function sameState2(a, b) {
610
+ return canonical(a) === canonical(b);
611
+ }
612
+ function intended(action) {
613
+ return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
614
+ }
615
+ function resourceKey(plan) {
616
+ return canonical({ server: plan.server, tool: plan.tool, args: plan.args });
617
+ }
618
+ var SETTLED = /* @__PURE__ */ new Set(["failed", "denied", "gated", "approved"]);
619
+ async function inspect(options) {
620
+ const { journal, router, runId } = options;
621
+ const signal = options.signal ?? new AbortController().signal;
622
+ const actions = journal.getActions(runId);
623
+ const resources = [];
624
+ const seen = /* @__PURE__ */ new Set();
625
+ for (const action of [...actions].reverse()) {
626
+ const at = { seq: action.seq, server: action.server, tool: action.tool };
627
+ if (action.class === "readonly") {
628
+ continue;
629
+ }
630
+ if (SETTLED.has(action.status)) {
631
+ resources.push({ ...at, condition: "not-applied", note: action.status });
632
+ continue;
633
+ }
634
+ if (action.status === "rolled_back") {
635
+ resources.push({ ...at, condition: "restored" });
636
+ continue;
637
+ }
638
+ const verify = inversePlan2.safeParse(action.verify);
639
+ const post = observation2.safeParse(action.postSnapshot);
640
+ if (!verify.success || !post.success) {
641
+ resources.push({
642
+ ...at,
643
+ condition: "unknowable",
644
+ note: action.inverse === void 0 ? "nothing was captured to restore" : "no pre-read was declared"
645
+ });
646
+ continue;
647
+ }
648
+ const key = resourceKey(verify.data);
649
+ if (seen.has(key)) {
650
+ resources.push({ ...at, condition: "superseded" });
651
+ continue;
652
+ }
653
+ seen.add(key);
654
+ let current;
655
+ try {
656
+ current = await observeState(router, verify.data, signal);
657
+ } catch (error) {
658
+ resources.push({ ...at, condition: "unknowable", note: `could not read it: ${describe(error)}` });
659
+ continue;
660
+ }
661
+ if (sameState2(current, post.data)) {
662
+ resources.push({ ...at, condition: "unchanged" });
663
+ continue;
664
+ }
665
+ const back = intended(action);
666
+ if (back !== void 0 && sameState2(current, back)) {
667
+ resources.push({ ...at, condition: "restored", note: "put back outside this journal" });
668
+ continue;
669
+ }
670
+ resources.push({
671
+ ...at,
672
+ condition: "changed",
673
+ diff: changedLines(post.data, current)
674
+ });
675
+ }
676
+ return { runId, resources: resources.reverse() };
677
+ }
678
+ function verdict(inspection) {
679
+ const changed = inspection.resources.filter((r) => r.condition === "changed").length;
680
+ const undoable = inspection.resources.filter((r) => r.condition === "unchanged").length;
681
+ if (changed > 0) {
682
+ return `${String(changed)} changed since this ran; undoing would write over ${changed === 1 ? "it" : "them"}`;
683
+ }
684
+ if (undoable > 0) {
685
+ return `nothing has been touched since; all ${String(undoable)} would undo cleanly`;
686
+ }
687
+ return "nothing here is still applied";
688
+ }
689
+
576
690
  // src/watch.ts
577
691
  import { existsSync as existsSync2 } from "fs";
578
692
 
@@ -591,23 +705,159 @@ function keysIn(chunk) {
591
705
  return keys;
592
706
  }
593
707
 
708
+ // src/clock.ts
709
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
710
+ var pad = (value) => String(value).padStart(2, "0");
711
+ function isToday(when, now) {
712
+ return when.getFullYear() === now.getFullYear() && when.getMonth() === now.getMonth() && when.getDate() === now.getDate();
713
+ }
714
+ function shortTime(iso, now = /* @__PURE__ */ new Date()) {
715
+ const when = new Date(iso);
716
+ if (Number.isNaN(when.getTime())) {
717
+ return iso.slice(11, 19).padEnd(12);
718
+ }
719
+ const time = `${pad(when.getHours())}:${pad(when.getMinutes())}`;
720
+ if (isToday(when, now)) {
721
+ return `${time}:${pad(when.getSeconds())}`.padEnd(12);
722
+ }
723
+ return `${pad(when.getDate())} ${MONTHS[when.getMonth()] ?? ""} ${time}`.padEnd(12);
724
+ }
725
+ function fullTime(iso, now = /* @__PURE__ */ new Date()) {
726
+ const when = new Date(iso);
727
+ if (Number.isNaN(when.getTime())) {
728
+ return iso;
729
+ }
730
+ const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}:${pad(when.getSeconds())}`;
731
+ if (isToday(when, now)) {
732
+ return `today at ${clock}`;
733
+ }
734
+ return `${pad(when.getDate())} ${MONTHS[when.getMonth()] ?? ""} ${String(when.getFullYear())}, ${clock}`;
735
+ }
736
+ function ago(iso, now = /* @__PURE__ */ new Date()) {
737
+ const when = new Date(iso);
738
+ if (Number.isNaN(when.getTime())) {
739
+ return "";
740
+ }
741
+ const seconds = Math.max(0, Math.round((now.getTime() - when.getTime()) / 1e3));
742
+ if (seconds < 10) {
743
+ return "just now";
744
+ }
745
+ if (seconds < 60) {
746
+ return `${String(seconds)}s ago`;
747
+ }
748
+ const minutes = Math.round(seconds / 60);
749
+ if (minutes < 60) {
750
+ return `${String(minutes)}m ago`;
751
+ }
752
+ const hours = Math.round(minutes / 60);
753
+ if (hours < 24) {
754
+ return `${String(hours)}h ago`;
755
+ }
756
+ return `${String(Math.round(hours / 24))}d ago`;
757
+ }
758
+
759
+ // src/describe.ts
760
+ import { basename } from "path";
761
+ var SUBJECT_KEYS = [
762
+ "path",
763
+ "file_path",
764
+ "source",
765
+ "destination",
766
+ "id",
767
+ "name",
768
+ "key",
769
+ "entity",
770
+ "query"
771
+ ];
772
+ function isRecord(value) {
773
+ return typeof value === "object" && value !== null && !Array.isArray(value);
774
+ }
775
+ function subject(args) {
776
+ if (!isRecord(args)) {
777
+ return "";
778
+ }
779
+ const record = args;
780
+ for (const key of SUBJECT_KEYS) {
781
+ const value = record[key];
782
+ if (typeof value === "string" && value !== "") {
783
+ return value.includes("/") ? basename(value) : value;
784
+ }
785
+ }
786
+ for (const value of Object.values(record)) {
787
+ if (Array.isArray(value) && value.length > 0) {
788
+ return `${String(value.length)} items`;
789
+ }
790
+ }
791
+ return "";
792
+ }
793
+ function plainly(action) {
794
+ switch (action.status) {
795
+ case "gated":
796
+ return { text: "waiting for you", needs: true };
797
+ case "applied":
798
+ return action.class === "readonly" ? { text: "read", needs: false } : action.inverse === void 0 ? { text: "done, cannot undo", needs: false } : { text: "done, can undo", needs: false };
799
+ case "rolled_back":
800
+ return { text: "undone", needs: false };
801
+ case "rolling_back":
802
+ return { text: "undoing", needs: false };
803
+ case "denied":
804
+ return { text: "refused", needs: false };
805
+ case "failed":
806
+ return { text: "failed", needs: false };
807
+ case "approved":
808
+ return { text: "approved, not yet sent", needs: true };
809
+ case "pending":
810
+ return { text: "sent, outcome unknown", needs: true };
811
+ case "unrecoverable":
812
+ return action.inverse === void 0 ? { text: "cannot be undone", needs: true } : { text: "changed since; not safe to undo", needs: true };
813
+ default:
814
+ return { text: action.status, needs: false };
815
+ }
816
+ }
817
+ function size(bytes) {
818
+ if (bytes < 1024) {
819
+ return `${String(bytes)} B`;
820
+ }
821
+ if (bytes < 1024 * 1024) {
822
+ return `${(bytes / 1024).toFixed(1)} kB`;
823
+ }
824
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
825
+ }
826
+ function summariseArgs(args, limit = 60) {
827
+ if (!isRecord(args)) {
828
+ return args === void 0 ? "" : JSON.stringify(args);
829
+ }
830
+ const parts = [];
831
+ for (const [key, value] of Object.entries(args)) {
832
+ if (typeof value === "string") {
833
+ parts.push(value.length > 48 ? `${key} ${size(Buffer.byteLength(value))}` : `${key} ${value}`);
834
+ continue;
835
+ }
836
+ if (Array.isArray(value)) {
837
+ parts.push(`${key} ${String(value.length)} items`);
838
+ continue;
839
+ }
840
+ if (value === null || typeof value !== "object") {
841
+ parts.push(`${key} ${String(value)}`);
842
+ continue;
843
+ }
844
+ parts.push(`${key} ${size(Buffer.byteLength(JSON.stringify(value)))}`);
845
+ }
846
+ const line2 = parts.join(" ");
847
+ return line2.length <= limit ? line2 : `${line2.slice(0, limit - 1)}\u2026`;
848
+ }
849
+
594
850
  // src/watch.ts
595
851
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
596
- var MARK = {
597
- readonly: "\xB7",
598
- reversible: "\u2190",
599
- compensable: "\u2248",
600
- irreversible: "!",
601
- unclassified: "?"
602
- };
603
852
  var NOTICE_TICKS = 26;
604
- function line(action) {
605
- const mark = MARK[action.class] ?? "?";
606
- const badge = `${mark} ${action.class}`.padEnd(14);
607
- const when = action.ts.slice(11, 19);
608
- const label = labelFor(action).padEnd(13);
609
- const status = action.status === "gated" ? style.strong(label) : wasRefused(action) ? style.accent(label) : style.quiet(label);
610
- return ` ${style.quiet(when)} ${style.quiet(badge)} ${status} ${action.server}.${action.tool}`;
853
+ function line(action, now) {
854
+ const when = shortTime(action.ts, now);
855
+ const where = action.server.padEnd(10);
856
+ const what = action.tool.padEnd(20);
857
+ const on = subject(action.args);
858
+ const state = plainly(action);
859
+ const said = state.needs || wasRefused(action) ? style.accent(state.text) : style.quiet(state.text);
860
+ return ` ${style.quiet(when)} ${style.quiet(where)} ${style.strong(what)} ${style.quiet(on.padEnd(20))} ${said}`;
611
861
  }
612
862
  function waitingForJournal(options, tick) {
613
863
  const spinner = options.live ? `${style.accent(FRAMES[tick % FRAMES.length] ?? "")} ` : "";
@@ -625,6 +875,7 @@ function waitingForJournal(options, tick) {
625
875
  function render(journal, options, tick, view) {
626
876
  const runs = journal.listRuns();
627
877
  const recent = journal.recentActions(12);
878
+ const now = /* @__PURE__ */ new Date();
628
879
  const waiting = journal.listGated();
629
880
  const active = runs.filter((run) => run.status === "active").length;
630
881
  const out2 = [];
@@ -641,7 +892,7 @@ function render(journal, options, tick, view) {
641
892
  out2.push(` ${style.quiet("No agent has done anything through this journal yet.")}`);
642
893
  } else {
643
894
  for (const action of recent) {
644
- out2.push(line(action));
895
+ out2.push(line(action, now));
645
896
  }
646
897
  }
647
898
  if (waiting.length > 0) {
@@ -789,7 +1040,7 @@ async function watch(options) {
789
1040
  if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
790
1041
  break;
791
1042
  }
792
- await new Promise((resolve2) => setTimeout(resolve2, interval));
1043
+ await new Promise((resolve4) => setTimeout(resolve4, interval));
793
1044
  }
794
1045
  return 0;
795
1046
  } finally {
@@ -804,14 +1055,675 @@ async function watch(options) {
804
1055
  await reader?.return?.(void 0);
805
1056
  await reading;
806
1057
  })(),
807
- new Promise((resolve2) => setTimeout(resolve2, 50).unref())
1058
+ new Promise((resolve4) => setTimeout(resolve4, 50).unref())
808
1059
  ]);
809
1060
  journal?.close();
810
1061
  }
811
1062
  }
812
1063
 
813
1064
  // src/console.ts
814
- import { existsSync as existsSync3 } from "fs";
1065
+ import { existsSync as existsSync6 } from "fs";
1066
+
1067
+ // src/install/connections.ts
1068
+ import { existsSync as existsSync5 } from "fs";
1069
+
1070
+ // src/install/clients.ts
1071
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
1072
+ import { homedir, platform } from "os";
1073
+ import { basename as basename2, dirname, join, resolve } from "path";
1074
+
1075
+ // src/install/toml.ts
1076
+ var HEADER = /^\s*\[(?!\[)([^[\]]+)\]\s*$/;
1077
+ function serverTables(lines) {
1078
+ const tables = [];
1079
+ let open;
1080
+ const close = (at) => {
1081
+ if (open !== void 0) {
1082
+ tables.push({ name: open.name, start: open.start, end: at });
1083
+ open = void 0;
1084
+ }
1085
+ };
1086
+ lines.forEach((line2, index) => {
1087
+ const header2 = HEADER.exec(line2)?.[1];
1088
+ if (header2 === void 0) {
1089
+ return;
1090
+ }
1091
+ const parts = header2.split(".");
1092
+ if (parts[0] === "mcp_servers" && parts.length === 2 && parts[1] !== void 0) {
1093
+ close(index);
1094
+ open = { name: unquote(parts[1]), start: index };
1095
+ return;
1096
+ }
1097
+ close(index);
1098
+ });
1099
+ close(lines.length);
1100
+ return tables;
1101
+ }
1102
+ function unquote(text) {
1103
+ const trimmed = text.trim();
1104
+ if (/^'.*'$/s.test(trimmed)) {
1105
+ return trimmed.slice(1, -1);
1106
+ }
1107
+ if (!/^".*"$/s.test(trimmed)) {
1108
+ return trimmed;
1109
+ }
1110
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
1111
+ }
1112
+ function readKey(lines, table, key) {
1113
+ const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`);
1114
+ for (let index = table.start + 1; index < table.end; index += 1) {
1115
+ const line2 = lines[index];
1116
+ if (line2 === void 0 || HEADER.test(line2)) {
1117
+ break;
1118
+ }
1119
+ const value = pattern.exec(line2)?.[1];
1120
+ if (value !== void 0) {
1121
+ return value.trim();
1122
+ }
1123
+ }
1124
+ return void 0;
1125
+ }
1126
+ function splitItems(inner) {
1127
+ const items = [];
1128
+ let current = "";
1129
+ let quote3;
1130
+ let escaped = false;
1131
+ for (const character of inner) {
1132
+ if (escaped) {
1133
+ current += character;
1134
+ escaped = false;
1135
+ continue;
1136
+ }
1137
+ if (character === "\\" && quote3 === '"') {
1138
+ current += character;
1139
+ escaped = true;
1140
+ continue;
1141
+ }
1142
+ if (quote3 === void 0 && (character === '"' || character === "'")) {
1143
+ quote3 = character;
1144
+ current += character;
1145
+ continue;
1146
+ }
1147
+ if (character === quote3) {
1148
+ quote3 = void 0;
1149
+ current += character;
1150
+ continue;
1151
+ }
1152
+ if (character === "," && quote3 === void 0) {
1153
+ items.push(current);
1154
+ current = "";
1155
+ continue;
1156
+ }
1157
+ current += character;
1158
+ }
1159
+ items.push(current);
1160
+ return items;
1161
+ }
1162
+ function parseArray(value) {
1163
+ if (value === void 0 || !value.startsWith("[")) {
1164
+ return void 0;
1165
+ }
1166
+ if (!value.endsWith("]")) {
1167
+ return void 0;
1168
+ }
1169
+ const inner = value.slice(1, -1).trim();
1170
+ if (inner === "") {
1171
+ return [];
1172
+ }
1173
+ return splitItems(inner).map((item) => item.trim()).filter((item, index, all) => item !== "" || index !== all.length - 1).map(unquote);
1174
+ }
1175
+ function readServers(text) {
1176
+ const lines = text.split("\n");
1177
+ const servers = {};
1178
+ for (const table of serverTables(lines)) {
1179
+ const command = readKey(lines, table, "command");
1180
+ const entry = {};
1181
+ if (command !== void 0) {
1182
+ entry["command"] = unquote(command);
1183
+ }
1184
+ const args = parseArray(readKey(lines, table, "args"));
1185
+ if (args !== void 0) {
1186
+ entry["args"] = args;
1187
+ }
1188
+ const url = readKey(lines, table, "url");
1189
+ if (url !== void 0) {
1190
+ entry["url"] = unquote(url);
1191
+ }
1192
+ const enabled = readKey(lines, table, "enabled");
1193
+ if (enabled !== void 0) {
1194
+ entry["enabled"] = enabled.trim() === "true";
1195
+ }
1196
+ servers[table.name] = entry;
1197
+ }
1198
+ return servers;
1199
+ }
1200
+ var quote2 = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1201
+ function writeServers(text, servers) {
1202
+ const lines = text.split("\n");
1203
+ const current = readServers(text);
1204
+ for (const table of serverTables(lines).reverse()) {
1205
+ const wanted = servers[table.name];
1206
+ if (wanted === void 0 || wanted.command === void 0) {
1207
+ continue;
1208
+ }
1209
+ const now = current[table.name];
1210
+ if (now?.command === wanted.command && JSON.stringify(now.args ?? []) === JSON.stringify(wanted.args ?? [])) {
1211
+ continue;
1212
+ }
1213
+ setKey(lines, table, "command", quote2(wanted.command));
1214
+ setKey(lines, table, "args", `[${(wanted.args ?? []).map(quote2).join(", ")}]`);
1215
+ }
1216
+ return lines.join("\n");
1217
+ }
1218
+ function setKey(lines, table, key, value) {
1219
+ const pattern = new RegExp(`^(\\s*)${key}\\s*=`);
1220
+ for (let index = table.start + 1; index < table.end; index += 1) {
1221
+ const line2 = lines[index];
1222
+ if (line2 === void 0 || HEADER.test(line2)) {
1223
+ break;
1224
+ }
1225
+ const indent = pattern.exec(line2)?.[1];
1226
+ if (indent !== void 0) {
1227
+ lines[index] = `${indent}${key} = ${value}`;
1228
+ return;
1229
+ }
1230
+ }
1231
+ lines.splice(table.start + 1, 0, `${key} = ${value}`);
1232
+ }
1233
+
1234
+ // src/install/clients.ts
1235
+ var LABELS = {
1236
+ "claude-code": "Claude Code",
1237
+ "claude-desktop": "Claude Desktop",
1238
+ cursor: "Cursor",
1239
+ codex: "Codex"
1240
+ };
1241
+ function claudeDesktopPath() {
1242
+ const home = homedir();
1243
+ switch (platform()) {
1244
+ case "darwin":
1245
+ return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1246
+ case "win32":
1247
+ return join(process.env["APPDATA"] ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1248
+ default:
1249
+ return join(process.env["XDG_CONFIG_HOME"] ?? join(home, ".config"), "Claude", "claude_desktop_config.json");
1250
+ }
1251
+ }
1252
+ function discover(cwd) {
1253
+ const home = homedir();
1254
+ const sites = [];
1255
+ const claudeCode = join(home, ".claude.json");
1256
+ if (existsSync3(claudeCode)) {
1257
+ const document = readJson(claudeCode);
1258
+ const projects = document?.["projects"];
1259
+ const here = resolve(cwd);
1260
+ if (isRecord2(projects) && Object.prototype.hasOwnProperty.call(projects, here)) {
1261
+ sites.push({
1262
+ client: "claude-code",
1263
+ label: LABELS["claude-code"],
1264
+ format: "json",
1265
+ path: claudeCode,
1266
+ scope: `project ${here}`,
1267
+ at: ["projects", here, "mcpServers"]
1268
+ });
1269
+ }
1270
+ sites.push({
1271
+ client: "claude-code",
1272
+ label: LABELS["claude-code"],
1273
+ format: "json",
1274
+ path: claudeCode,
1275
+ scope: "global",
1276
+ at: ["mcpServers"]
1277
+ });
1278
+ }
1279
+ const projectFile = join(resolve(cwd), ".mcp.json");
1280
+ if (existsSync3(projectFile)) {
1281
+ sites.push({
1282
+ client: "claude-code",
1283
+ label: LABELS["claude-code"],
1284
+ format: "json",
1285
+ path: projectFile,
1286
+ scope: "project file",
1287
+ at: ["mcpServers"]
1288
+ });
1289
+ }
1290
+ const desktop = claudeDesktopPath();
1291
+ if (existsSync3(desktop)) {
1292
+ sites.push({
1293
+ client: "claude-desktop",
1294
+ label: LABELS["claude-desktop"],
1295
+ format: "json",
1296
+ path: desktop,
1297
+ scope: "global",
1298
+ at: ["mcpServers"]
1299
+ });
1300
+ }
1301
+ const codex = join(process.env["CODEX_HOME"] ?? join(home, ".codex"), "config.toml");
1302
+ if (existsSync3(codex)) {
1303
+ sites.push({
1304
+ client: "codex",
1305
+ label: LABELS.codex,
1306
+ format: "toml",
1307
+ path: codex,
1308
+ scope: "global",
1309
+ at: ["mcp_servers"]
1310
+ });
1311
+ }
1312
+ for (const [path, scope] of [
1313
+ [join(resolve(cwd), ".cursor", "mcp.json"), "project"],
1314
+ [join(home, ".cursor", "mcp.json"), "global"]
1315
+ ]) {
1316
+ if (existsSync3(path)) {
1317
+ sites.push({ client: "cursor", label: LABELS.cursor, format: "json", path, scope, at: ["mcpServers"] });
1318
+ }
1319
+ }
1320
+ return sites;
1321
+ }
1322
+ function isRecord2(value) {
1323
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1324
+ }
1325
+ function readJson(path) {
1326
+ try {
1327
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1328
+ return isRecord2(parsed) ? parsed : void 0;
1329
+ } catch {
1330
+ return void 0;
1331
+ }
1332
+ }
1333
+ var ConfigError = class extends Error {
1334
+ };
1335
+ function readDocument(site) {
1336
+ let text;
1337
+ try {
1338
+ text = readFileSync2(site.path, "utf8");
1339
+ } catch (error) {
1340
+ throw new ConfigError(`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`);
1341
+ }
1342
+ let parsed;
1343
+ try {
1344
+ parsed = JSON.parse(text);
1345
+ } catch (error) {
1346
+ throw new ConfigError(
1347
+ `${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.`
1348
+ );
1349
+ }
1350
+ if (!isRecord2(parsed)) {
1351
+ throw new ConfigError(`${site.path} is not a JSON object, so it has no server list to change`);
1352
+ }
1353
+ return parsed;
1354
+ }
1355
+ function readServers2(document, at) {
1356
+ let node = document;
1357
+ for (const key of at) {
1358
+ if (!isRecord2(node)) {
1359
+ return {};
1360
+ }
1361
+ node = node[key];
1362
+ }
1363
+ if (!isRecord2(node)) {
1364
+ return {};
1365
+ }
1366
+ const servers = {};
1367
+ for (const [name, entry] of Object.entries(node)) {
1368
+ if (isRecord2(entry)) {
1369
+ servers[name] = entry;
1370
+ }
1371
+ }
1372
+ return servers;
1373
+ }
1374
+ function withServers(document, at, servers) {
1375
+ const head = at[0];
1376
+ if (head === void 0) {
1377
+ throw new ConfigError("no path to the server list");
1378
+ }
1379
+ const rest = at.slice(1);
1380
+ const below = document[head];
1381
+ const child = rest.length === 0 ? servers : withServers(isRecord2(below) ? below : {}, rest, servers);
1382
+ return { ...document, [head]: child };
1383
+ }
1384
+ function indentOf(path) {
1385
+ try {
1386
+ const line2 = /\n([ \t]+)"/.exec(readFileSync2(path, "utf8"));
1387
+ const found = line2?.[1];
1388
+ if (found === void 0) {
1389
+ return 2;
1390
+ }
1391
+ return found.startsWith(" ") ? " " : found.length;
1392
+ } catch {
1393
+ return 2;
1394
+ }
1395
+ }
1396
+ function backupPathFor(path) {
1397
+ return `${path}.synartesis-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1398
+ }
1399
+ var KEEP_BACKUPS = 5;
1400
+ function pruneBackups(path) {
1401
+ try {
1402
+ const dir = dirname(path);
1403
+ const prefix = `${basename2(path)}.synartesis-backup-`;
1404
+ const ours = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
1405
+ for (const name of ours.slice(0, Math.max(0, ours.length - KEEP_BACKUPS))) {
1406
+ rmSync(join(dir, name), { force: true });
1407
+ }
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ function writeDocument(site, document) {
1412
+ return writeText(site, `${JSON.stringify(document, void 0, indentOf(site.path))}
1413
+ `);
1414
+ }
1415
+ function writeText(site, text) {
1416
+ const backup = backupPathFor(site.path);
1417
+ const original = readFileSync2(site.path);
1418
+ writeFileSync(backup, original);
1419
+ pruneBackups(site.path);
1420
+ const temporary = join(dirname(site.path), `.synartesis-write-${String(process.pid)}.tmp`);
1421
+ try {
1422
+ writeFileSync(temporary, text);
1423
+ renameSync(temporary, site.path);
1424
+ } catch (error) {
1425
+ try {
1426
+ unlinkSync(temporary);
1427
+ } catch {
1428
+ }
1429
+ throw new ConfigError(
1430
+ `could not write ${site.path}: ${error instanceof Error ? error.message : String(error)}. The original is untouched, and a copy is at ${backup}.`
1431
+ );
1432
+ }
1433
+ return backup;
1434
+ }
1435
+ function serversAt(site) {
1436
+ if (site.format === "toml") {
1437
+ try {
1438
+ return readServers(readFileSync2(site.path, "utf8"));
1439
+ } catch (error) {
1440
+ throw new ConfigError(
1441
+ `cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`
1442
+ );
1443
+ }
1444
+ }
1445
+ return readServers2(readDocument(site), site.at);
1446
+ }
1447
+ function saveServers(site, servers) {
1448
+ if (site.format === "toml") {
1449
+ const text = readFileSync2(site.path, "utf8");
1450
+ return writeText(site, writeServers(text, servers));
1451
+ }
1452
+ return writeDocument(site, withServers(readDocument(site), site.at, servers));
1453
+ }
1454
+
1455
+ // src/install/install.ts
1456
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1457
+ import { dirname as dirname2, resolve as resolve2 } from "path";
1458
+ function recordPathFor(manifestPath) {
1459
+ return resolve2(dirname2(manifestPath), "installed.json");
1460
+ }
1461
+ var EMPTY = { version: 1, wrapped: {} };
1462
+ function isRecord3(value) {
1463
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1464
+ }
1465
+ function asRecord(value) {
1466
+ if (!isRecord3(value)) {
1467
+ return void 0;
1468
+ }
1469
+ const wrapped2 = value["wrapped"];
1470
+ if (!isRecord3(wrapped2)) {
1471
+ return void 0;
1472
+ }
1473
+ const kept = {};
1474
+ for (const [key, entry] of Object.entries(wrapped2)) {
1475
+ if (!isRecord3(entry)) {
1476
+ continue;
1477
+ }
1478
+ const original = entry["original"];
1479
+ const at = entry["at"];
1480
+ if (!isRecord3(original)) {
1481
+ continue;
1482
+ }
1483
+ if (!Array.isArray(at) || !at.every((step) => typeof step === "string")) {
1484
+ continue;
1485
+ }
1486
+ kept[key] = { original, at };
1487
+ }
1488
+ return { version: 1, wrapped: kept };
1489
+ }
1490
+ function keyFor(site, server) {
1491
+ return [site.path, site.scope, server].join("");
1492
+ }
1493
+ function readRecord(manifestPath) {
1494
+ const path = recordPathFor(manifestPath);
1495
+ if (!existsSync4(path)) {
1496
+ return EMPTY;
1497
+ }
1498
+ try {
1499
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1500
+ const record = asRecord(parsed);
1501
+ if (record !== void 0) {
1502
+ return record;
1503
+ }
1504
+ } catch {
1505
+ }
1506
+ return EMPTY;
1507
+ }
1508
+ function writeRecord(manifestPath, record) {
1509
+ mkdirSync(dirname2(recordPathFor(manifestPath)), { recursive: true, mode: 448 });
1510
+ writeFileSync2(recordPathFor(manifestPath), `${JSON.stringify(record, void 0, 2)}
1511
+ `);
1512
+ }
1513
+ function proxyEntry(manifestPath, server, original, invoker) {
1514
+ const command = { command: invoker.command, args: [...invoker.args] };
1515
+ return {
1516
+ ...command,
1517
+ args: [...command.args, "--manifest", resolve2(manifestPath), "--server", server],
1518
+ // The agent's environment, not ours: the upstream is started by the proxy
1519
+ // from the manifest, but a client that set `env` here meant it for the
1520
+ // server, and the manifest reads `${VAR}` out of exactly this environment.
1521
+ ...original.env === void 0 ? {} : { env: original.env },
1522
+ ...original.cwd === void 0 ? {} : { cwd: original.cwd }
1523
+ };
1524
+ }
1525
+ function isWrapped(entry) {
1526
+ const args = entry.args ?? [];
1527
+ 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")));
1528
+ }
1529
+ function invokerFor(ourVersion, cliPath) {
1530
+ if (pathBinaryMatches(ourVersion)) {
1531
+ return { command: "synartesis", args: ["proxy"] };
1532
+ }
1533
+ return {
1534
+ command: process.execPath,
1535
+ args: [cliPath, "proxy"],
1536
+ note: "the synartesis on your PATH is a different build, so the entries name this one directly"
1537
+ };
1538
+ }
1539
+ async function planInstall(sites, manifestPath, invoker) {
1540
+ let yaml = existsSync4(manifestPath) ? readFileSync3(manifestPath, "utf8") : void 0;
1541
+ const plans = [];
1542
+ const claimed = new Set(
1543
+ yaml === void 0 ? [] : Object.keys(parseManifest(yaml, manifestPath).servers)
1544
+ );
1545
+ for (const site of sites) {
1546
+ const servers = serversAt(site);
1547
+ const planned = [];
1548
+ const skipped = [];
1549
+ for (const [name, entry] of Object.entries(servers)) {
1550
+ if (isWrapped(entry)) {
1551
+ skipped.push({ name, why: "already covered" });
1552
+ continue;
1553
+ }
1554
+ if (entry.command === void 0) {
1555
+ skipped.push({ name, why: entry.url === void 0 ? "no command to start" : "remote (http); stdio only today" });
1556
+ continue;
1557
+ }
1558
+ if (entry.enabled === false) {
1559
+ skipped.push({ name, why: "switched off in the config" });
1560
+ continue;
1561
+ }
1562
+ const key = claimed.has(name) ? `${name}-${site.client}` : name;
1563
+ if (claimed.has(key)) {
1564
+ skipped.push({ name, why: `already in the policy as ${key}` });
1565
+ continue;
1566
+ }
1567
+ let draft;
1568
+ try {
1569
+ draft = await draftManifest({
1570
+ name: key,
1571
+ command: entry.command,
1572
+ args: [...entry.args ?? []],
1573
+ ...yaml === void 0 ? {} : { existing: yaml }
1574
+ });
1575
+ } catch (error) {
1576
+ skipped.push({
1577
+ name,
1578
+ why: `will not start: ${(error instanceof Error ? error.message : String(error)).slice(0, 60)}`
1579
+ });
1580
+ continue;
1581
+ }
1582
+ yaml = draft.yaml;
1583
+ claimed.add(key);
1584
+ planned.push({
1585
+ name,
1586
+ original: entry,
1587
+ wrapped: proxyEntry(manifestPath, key, entry, invoker),
1588
+ ...draft.adopted === void 0 ? {} : { adopted: draft.adopted.server, tools: draft.adopted.tools }
1589
+ });
1590
+ }
1591
+ plans.push({ site, servers: planned, skipped });
1592
+ }
1593
+ return { plans, yaml: yaml ?? "" };
1594
+ }
1595
+ function applyInstall(plans, manifestPath, yaml) {
1596
+ if (!plans.some((plan) => plan.servers.length > 0)) {
1597
+ return [];
1598
+ }
1599
+ parseManifest(yaml, manifestPath);
1600
+ mkdirSync(dirname2(resolve2(manifestPath)), { recursive: true, mode: 448 });
1601
+ writeFileSync2(manifestPath, yaml);
1602
+ const record = readRecord(manifestPath);
1603
+ const wrapped2 = { ...record.wrapped };
1604
+ const applied = [];
1605
+ for (const plan of plans) {
1606
+ if (plan.servers.length === 0) {
1607
+ continue;
1608
+ }
1609
+ const servers = { ...serversAt(plan.site) };
1610
+ for (const server of plan.servers) {
1611
+ servers[server.name] = server.wrapped;
1612
+ wrapped2[keyFor(plan.site, server.name)] = { original: server.original, at: plan.site.at };
1613
+ }
1614
+ writeRecord(manifestPath, { version: 1, wrapped: wrapped2 });
1615
+ const backup = saveServers(plan.site, servers);
1616
+ applied.push({ site: plan.site, backup, servers: plan.servers.map((server) => server.name) });
1617
+ }
1618
+ return applied;
1619
+ }
1620
+ function applyUninstall(sites, manifestPath) {
1621
+ const record = readRecord(manifestPath);
1622
+ const restoredKeys = /* @__PURE__ */ new Set();
1623
+ const restored = [];
1624
+ for (const site of sites) {
1625
+ const servers = { ...serversAt(site) };
1626
+ const put = [];
1627
+ const unknown = [];
1628
+ for (const [name, entry] of Object.entries(servers)) {
1629
+ if (!isWrapped(entry)) {
1630
+ continue;
1631
+ }
1632
+ const known = record.wrapped[keyFor(site, name)];
1633
+ if (known === void 0) {
1634
+ unknown.push(name);
1635
+ continue;
1636
+ }
1637
+ servers[name] = known.original;
1638
+ put.push(name);
1639
+ restoredKeys.add(keyFor(site, name));
1640
+ }
1641
+ if (put.length === 0 && unknown.length === 0) {
1642
+ continue;
1643
+ }
1644
+ const backup = put.length === 0 ? "" : saveServers(site, servers);
1645
+ restored.push({ site, backup, servers: put, unknown });
1646
+ }
1647
+ const remaining = Object.fromEntries(
1648
+ Object.entries(record.wrapped).filter(([key]) => !restoredKeys.has(key))
1649
+ );
1650
+ writeRecord(manifestPath, { version: 1, wrapped: remaining });
1651
+ return restored;
1652
+ }
1653
+
1654
+ // src/install/connections.ts
1655
+ var ACTIVE_WITHIN_MS = 2 * 60 * 1e3;
1656
+ function lastSeenByServer(journal) {
1657
+ const seen = /* @__PURE__ */ new Map();
1658
+ for (const action of journal.recentActions(500)) {
1659
+ const known = seen.get(action.server);
1660
+ if (known === void 0 || action.ts > known) {
1661
+ seen.set(action.server, action.ts);
1662
+ }
1663
+ }
1664
+ return seen;
1665
+ }
1666
+ function commandMissing(command) {
1667
+ if (command === void 0) {
1668
+ return false;
1669
+ }
1670
+ return (command.includes("/") || command.includes("\\")) && !existsSync5(command);
1671
+ }
1672
+ function scan(journal, cwd) {
1673
+ const seen = journal === void 0 ? /* @__PURE__ */ new Map() : lastSeenByServer(journal);
1674
+ const groups = [];
1675
+ for (const site of discover(cwd)) {
1676
+ let servers;
1677
+ try {
1678
+ servers = serversAt(site);
1679
+ } catch (error) {
1680
+ groups.push({
1681
+ label: site.label,
1682
+ scope: site.scope,
1683
+ path: site.path,
1684
+ connections: [],
1685
+ problem: error instanceof Error ? error.message : String(error)
1686
+ });
1687
+ continue;
1688
+ }
1689
+ const connections = Object.entries(servers).map(([server, entry]) => {
1690
+ const covered = isWrapped(entry);
1691
+ const lastSeen = seen.get(server);
1692
+ return {
1693
+ client: site.client,
1694
+ scope: site.scope,
1695
+ path: site.path,
1696
+ server,
1697
+ covered,
1698
+ missing: commandMissing(entry.command),
1699
+ ...lastSeen === void 0 ? {} : { lastSeen },
1700
+ site
1701
+ };
1702
+ });
1703
+ groups.push({ label: site.label, scope: site.scope, path: site.path, connections });
1704
+ }
1705
+ return groups;
1706
+ }
1707
+ function stateOf(connection, now = /* @__PURE__ */ new Date()) {
1708
+ if (connection.missing) {
1709
+ return "cannot start; the command is not there";
1710
+ }
1711
+ if (!connection.covered) {
1712
+ return "not covered";
1713
+ }
1714
+ if (connection.lastSeen === void 0) {
1715
+ return "covered, nothing through it yet";
1716
+ }
1717
+ const since = now.getTime() - new Date(connection.lastSeen).getTime();
1718
+ return since <= ACTIVE_WITHIN_MS ? "covered, active now" : `covered, last used ${ago(connection.lastSeen, now)}`;
1719
+ }
1720
+ function needsConnecting(groups) {
1721
+ return groups.flatMap(
1722
+ (group) => group.connections.filter((connection) => !connection.covered && !connection.missing)
1723
+ );
1724
+ }
1725
+
1726
+ // src/console.ts
815
1727
  var FRAMES2 = [
816
1728
  "\u280B",
817
1729
  "\u2819",
@@ -824,17 +1736,11 @@ var FRAMES2 = [
824
1736
  "\u2807",
825
1737
  "\u280F"
826
1738
  ];
827
- var MARK2 = {
828
- readonly: "\xB7",
829
- reversible: "\u2190",
830
- compensable: "\u2248",
831
- irreversible: "!",
832
- unclassified: "?"
833
- };
834
1739
  var CURSOR = "\u276F";
835
1740
  var DOT = "\xB7";
836
1741
  var ESC = "\x1B";
837
1742
  var NOTICE_TICKS2 = 26;
1743
+ var READING_TICKS = 200;
838
1744
  function roomFor(options) {
839
1745
  const rows = options.rows ?? rowsOf(process.stdout.rows) ?? 24;
840
1746
  return Math.max(3, rows - 11);
@@ -853,6 +1759,9 @@ function windowed(lines, at, room) {
853
1759
  ...below === 0 ? [] : [` ${style.quiet(`${String(below)} more below`)}`]
854
1760
  ];
855
1761
  }
1762
+ function firstLine(text) {
1763
+ return text.split("\n")[0]?.trim() ?? "";
1764
+ }
856
1765
  function truncate2(text, limit) {
857
1766
  return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
858
1767
  }
@@ -876,6 +1785,8 @@ function modeLabel(screen) {
876
1785
  return "one run, in the order it happened";
877
1786
  case "gates":
878
1787
  return "held until a person decides";
1788
+ case "connections":
1789
+ return "every AI on this machine, and whether it goes through Synartesis";
879
1790
  }
880
1791
  }
881
1792
  function header(options, screen, tick) {
@@ -905,15 +1816,35 @@ function runsView(journal, screen, options) {
905
1816
  const here = index === at && canPress(options);
906
1817
  const name = (run.label ?? "an agent").padEnd(24);
907
1818
  const note = held === 0 ? "" : ` ${style.accent(`${String(held)} awaiting approval`)}`;
908
- return ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(name) : style.strong(name)} ${style.quiet(run.startedAt.slice(0, 19).replace("T", " "))} ${style.quiet(run.status.padEnd(11))} ${style.quiet(`${String(actions.length)} actions`)}${note}`;
1819
+ return ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(name) : style.strong(name)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${style.quiet(run.status.padEnd(11))} ${style.quiet(`${String(actions.length)} actions`)}${note}`;
909
1820
  });
910
1821
  }
911
- function statusOf(action) {
912
- const text = labelFor(action).padEnd(13);
913
- if (wasRefused(action)) {
914
- return style.accent(text);
1822
+ function standing(actions) {
1823
+ let undoable = 0;
1824
+ let conflicted = 0;
1825
+ for (const action of actions) {
1826
+ if (action.inverse === void 0) {
1827
+ continue;
1828
+ }
1829
+ if (action.status === "applied") {
1830
+ undoable += 1;
1831
+ } else if (action.status === "unrecoverable") {
1832
+ conflicted += 1;
1833
+ }
915
1834
  }
916
- return action.status === "gated" ? style.strong(text) : style.quiet(text);
1835
+ return { undoable, conflicted };
1836
+ }
1837
+ function elsewhere(journal, exceptId) {
1838
+ for (const run of [...journal.listRuns()].reverse()) {
1839
+ if (run.id === exceptId) {
1840
+ continue;
1841
+ }
1842
+ const found = standing(journal.getActions(run.id));
1843
+ if (found.undoable > 0 || found.conflicted > 0) {
1844
+ return run;
1845
+ }
1846
+ }
1847
+ return void 0;
917
1848
  }
918
1849
  function runView(journal, screen) {
919
1850
  const runId = screen.openRun;
@@ -923,22 +1854,86 @@ function runView(journal, screen) {
923
1854
  const run = journal.getRun(runId);
924
1855
  const actions = journal.getActions(runId);
925
1856
  const out2 = [
926
- ` ${style.label("run")} ${style.strong(run?.label ?? "an agent")} ${style.quiet(runId.slice(0, 8))}`,
1857
+ // "RUN claude 5dce5bda" was read as an instruction to go and run
1858
+ // something: a spaced capital heading followed by two words looks exactly
1859
+ // like a command with two arguments. "Session" is only ever a noun.
1860
+ ` ${style.label("session")} ${style.strong(run?.label ?? "an agent")} ${style.quiet(runId.slice(0, 8))}`,
927
1861
  ""
928
1862
  ];
929
1863
  if (actions.length === 0) {
930
- out2.push(` ${style.quiet("nothing was recorded in this run")}`);
1864
+ out2.push(` ${style.quiet("nothing was recorded in this run, so there is nothing here to undo")}`);
1865
+ const other = elsewhere(journal, runId);
1866
+ if (other !== void 0) {
1867
+ out2.push("");
1868
+ out2.push(
1869
+ ` ${style.quiet("the session that did something:")} ${style.strong(other.label ?? "an agent")} ${style.accent(other.id.slice(0, 8))}`
1870
+ );
1871
+ out2.push(
1872
+ ` ${style.quiet("esc, then j/k onto it -- or run:")} ` + style.strong(`${cliCommand()} undo ${other.id.slice(0, 8)}`)
1873
+ );
1874
+ }
931
1875
  return out2;
932
1876
  }
933
1877
  for (const action of actions) {
934
- const badge = `${MARK2[action.class] ?? "?"} ${action.class}`.padEnd(14);
1878
+ const state = plainly(action);
1879
+ const said = state.needs || wasRefused(action) ? style.accent(state.text) : style.quiet(state.text);
935
1880
  out2.push(
936
- ` ${style.quiet(String(action.seq).padStart(3))} ${style.quiet(badge)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}`
1881
+ ` ${style.quiet(String(action.seq).padStart(3))} ${style.quiet(action.server.padEnd(10))} ${style.strong(action.tool.padEnd(20))} ${style.quiet(subject(action.args).padEnd(20))} ${said}`
937
1882
  );
938
- out2.push(` ${style.quiet(truncate2(JSON.stringify(action.args), 62))}`);
939
- if (action.inverse !== void 0) {
940
- out2.push(` ${style.quiet(`undo: ${truncate2(JSON.stringify(action.inverse), 56)}`)}`);
1883
+ if (screen.expanded) {
1884
+ for (const line2 of JSON.stringify(action.args, void 0, 2).split("\n")) {
1885
+ out2.push(` ${style.quiet(line2)}`);
1886
+ }
1887
+ if (action.inverse !== void 0) {
1888
+ out2.push(` ${style.quiet("undo")}`);
1889
+ for (const line2 of JSON.stringify(action.inverse, void 0, 2).split("\n")) {
1890
+ out2.push(` ${style.quiet(line2)}`);
1891
+ }
1892
+ }
1893
+ } else {
1894
+ out2.push(` ${style.quiet(summariseArgs(action.args, 62))}`);
1895
+ }
1896
+ }
1897
+ return out2;
1898
+ }
1899
+ function connectionRows(screen) {
1900
+ return screen.groups.flatMap((group) => group.connections);
1901
+ }
1902
+ function connectionsView(screen, options) {
1903
+ if (screen.groups.length === 0) {
1904
+ return [
1905
+ ` ${style.quiet("No MCP client config was found on this machine.")}`,
1906
+ "",
1907
+ ` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`
1908
+ ];
1909
+ }
1910
+ const rows = connectionRows(screen);
1911
+ const at = Math.min(screen.cursor, Math.max(0, rows.length - 1));
1912
+ const now = /* @__PURE__ */ new Date();
1913
+ const out2 = [];
1914
+ let index = 0;
1915
+ for (const group of screen.groups) {
1916
+ out2.push(` ${style.strong(group.label)} ${style.quiet(group.scope)}`);
1917
+ if (group.problem !== void 0) {
1918
+ out2.push(` ${style.accent(group.problem)}`);
1919
+ out2.push("");
1920
+ continue;
941
1921
  }
1922
+ if (group.connections.length === 0) {
1923
+ out2.push(` ${style.quiet("no servers listed")}`);
1924
+ out2.push("");
1925
+ continue;
1926
+ }
1927
+ for (const connection of group.connections) {
1928
+ const here = index === at && canPress(options);
1929
+ const state = stateOf(connection, now);
1930
+ const shown = connection.covered ? style.quiet(state) : style.accent(state);
1931
+ out2.push(
1932
+ ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(connection.server.padEnd(20)) : style.strong(connection.server.padEnd(20))} ${shown}`
1933
+ );
1934
+ index += 1;
1935
+ }
1936
+ out2.push("");
942
1937
  }
943
1938
  return out2;
944
1939
  }
@@ -961,19 +1956,18 @@ function footer(screen, options) {
961
1956
  return [];
962
1957
  }
963
1958
  if (screen.confirming !== void 0) {
964
- return [
965
- "",
966
- ` ${style.accent("undo this whole run?")} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`
967
- ];
1959
+ const what = screen.confirmingForce ? `undo ${screen.confirmingLabel ?? "this session"} anyway, losing that change?` : `undo ${screen.confirmingLabel ?? "this session"}?`;
1960
+ return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
968
1961
  }
969
- const keys = screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
1962
+ const keys = screen.mode === "connections" ? [keyHint2("enter", "connect"), keyHint2("a", "connect all"), keyHint2("r", "rescan"), keyHint2("j/k", "move"), keyHint2("h", "back")] : screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
1963
+ keyHint2("l", "check now"),
970
1964
  keyHint2("p", "preview undo"),
971
1965
  keyHint2("u", "undo"),
972
- keyHint2("esc", "back"),
973
- keyHint2("g", "held")
1966
+ keyHint2("f", "expand"),
1967
+ keyHint2("esc", "back")
974
1968
  ] : [
975
1969
  keyHint2("enter", "open"),
976
- keyHint2("p", "preview undo"),
1970
+ keyHint2("l", "check now"),
977
1971
  keyHint2("u", "undo"),
978
1972
  keyHint2("j/k", "move"),
979
1973
  keyHint2("g", "held")
@@ -1016,7 +2010,7 @@ async function* terminalKeys2() {
1016
2010
  async function openConsole(options) {
1017
2011
  let journal;
1018
2012
  const open = () => {
1019
- if (journal === void 0 && existsSync3(options.journalPath)) {
2013
+ if (journal === void 0 && existsSync6(options.journalPath)) {
1020
2014
  journal = openJournal(options.journalPath, { mustExist: true });
1021
2015
  }
1022
2016
  return journal;
@@ -1027,32 +2021,41 @@ async function openConsole(options) {
1027
2021
  cursor: 0,
1028
2022
  openRun: void 0,
1029
2023
  confirming: void 0,
2024
+ confirmingLabel: void 0,
2025
+ confirmingForce: false,
2026
+ warned: void 0,
2027
+ expanded: false,
2028
+ groups: [],
1030
2029
  busy: void 0,
1031
2030
  notice: "",
1032
2031
  noticeUntil: 0
1033
2032
  };
1034
2033
  let tick = 0;
1035
2034
  const stopped = () => screen.stop;
1036
- const say = (text) => {
2035
+ const say = (text, ticks = NOTICE_TICKS2) => {
1037
2036
  screen.notice = text;
1038
- screen.noticeUntil = tick + NOTICE_TICKS2;
2037
+ screen.noticeUntil = tick + ticks;
1039
2038
  };
1040
2039
  const frame = () => {
1041
2040
  const ready = open();
1042
2041
  if (ready === void 0) {
1043
2042
  return waitingForJournal2(options, tick);
1044
2043
  }
1045
- const room = roomFor(options);
1046
- const body = screen.mode === "runs" ? windowed(runsView(ready, screen, options), screen.cursor, room) : screen.mode === "run" ? windowed(runView(ready, screen), 0, room) : windowed(gatesView(ready, screen, options), screen.cursor, room);
1047
- const notice = screen.notice === "" ? [] : ["", ` ${style.accent(screen.notice)}`];
2044
+ const tail = command(ready);
2045
+ const shout = screen.notice === "" ? 0 : screen.notice.split("\n").length;
2046
+ const room = Math.max(3, roomFor(options) - tail.length - shout);
2047
+ const body = screen.mode === "runs" ? windowed(runsView(ready, screen, options), screen.cursor, room) : screen.mode === "run" ? windowed(runView(ready, screen), 0, room) : screen.mode === "connections" ? windowed(connectionsView(screen, options), screen.cursor, room) : windowed(gatesView(ready, screen, options), screen.cursor, room);
2048
+ const notice = screen.notice === "" ? [] : ["", ...screen.notice.split("\n").map((line2) => ` ${style.accent(line2)}`)];
1048
2049
  return [
1049
2050
  ...header(options, screen, tick),
1050
2051
  ...body,
1051
2052
  ...notice,
2053
+ ...tail,
1052
2054
  ...footer(screen, options),
1053
2055
  ""
1054
2056
  ].join("\n");
1055
2057
  };
2058
+ const onASession = () => screen.mode === "runs" || screen.mode === "run";
1056
2059
  const selectedRun = (ready) => {
1057
2060
  if (screen.mode === "run" && screen.openRun !== void 0) {
1058
2061
  return ready.getRun(screen.openRun);
@@ -1060,6 +2063,40 @@ async function openConsole(options) {
1060
2063
  const runs = [...ready.listRuns()].reverse();
1061
2064
  return runs[Math.min(screen.cursor, runs.length - 1)];
1062
2065
  };
2066
+ const nothingToUndo = (ready, run) => {
2067
+ const actions = ready.getActions(run.id);
2068
+ const why = actions.length === 0 ? "nothing was recorded in this session" : actions.every((action) => action.status === "rolled_back" || action.class === "readonly") ? "this session has already been undone" : "nothing in this session can be undone";
2069
+ const other = elsewhere(ready, run.id);
2070
+ return other === void 0 ? why : `${why} ${DOT} try ${other.label ?? "an agent"} ${other.id.slice(0, 8)}`;
2071
+ };
2072
+ const command = (ready) => {
2073
+ if (!canPress(options) || screen.confirming !== void 0) {
2074
+ return [];
2075
+ }
2076
+ if (screen.mode !== "runs" && screen.mode !== "run") {
2077
+ return [];
2078
+ }
2079
+ const run = selectedRun(ready);
2080
+ if (run === void 0) {
2081
+ return [];
2082
+ }
2083
+ const here = standing(ready.getActions(run.id));
2084
+ const id = run.id.slice(0, 8);
2085
+ if (here.undoable > 0) {
2086
+ return [
2087
+ "",
2088
+ ` ${style.quiet("u undoes it here")} ${style.quiet(DOT)} ${style.quiet("or from any terminal:")} ` + style.strong(`${cliCommand()} undo ${id}`)
2089
+ ];
2090
+ }
2091
+ if (here.conflicted > 0) {
2092
+ return [
2093
+ "",
2094
+ ` ${style.accent(`${String(here.conflicted)} changed since this ran`)} ${style.quiet(DOT)} ` + style.quiet("u shows what undoing would write over"),
2095
+ ` ${style.quiet("or from any terminal:")} ${style.strong(`${cliCommand()} undo ${id} --force`)}`
2096
+ ];
2097
+ }
2098
+ return ["", ` ${style.quiet("nothing to undo in this session")}`];
2099
+ };
1063
2100
  const decide = (approve) => {
1064
2101
  const ready = open();
1065
2102
  if (ready === void 0) {
@@ -1076,7 +2113,7 @@ async function openConsole(options) {
1076
2113
  );
1077
2114
  screen.cursor = 0;
1078
2115
  };
1079
- const perform = async (runId, dryRun) => {
2116
+ const perform = async (runId, dryRun, force = false) => {
1080
2117
  if (options.undo === void 0) {
1081
2118
  say("no way to undo was configured");
1082
2119
  return;
@@ -1087,11 +2124,18 @@ async function openConsole(options) {
1087
2124
  }
1088
2125
  screen.busy = dryRun ? "reading the current state..." : "putting it back...";
1089
2126
  try {
1090
- const report2 = await options.undo(runId, dryRun);
2127
+ const report2 = await options.undo(runId, dryRun, force);
1091
2128
  const reverted = report2.steps.filter((step) => step.kind === "revert").length;
1092
- const halted = report2.halted === void 0 ? "" : ` ${DOT} halted: ${report2.halted.reason}`;
2129
+ const why = report2.halted === void 0 ? "" : ` ${DOT} halted: ${firstLine(report2.halted.detail) || report2.halted.reason}`;
2130
+ const headline = dryRun ? `${String(reverted)} would be reverted ${DOT} nothing changed${why}` : `${report2.status} ${DOT} ${String(reverted)} reverted${why}`;
2131
+ const over = report2.halted?.overwrites ?? "";
2132
+ if (over === "") {
2133
+ say(headline);
2134
+ return;
2135
+ }
1093
2136
  say(
1094
- dryRun ? `${String(reverted)} would be reverted ${DOT} nothing changed${halted}` : `${report2.status} ${DOT} ${String(reverted)} reverted${halted}`
2137
+ ["changed since this ran, so nothing was written", "", "undoing anyway would write:", ...over.split("\n"), "", "u again to do it"].join("\n"),
2138
+ READING_TICKS
1095
2139
  );
1096
2140
  } catch (error) {
1097
2141
  say(error instanceof Error ? error.message : "the undo failed");
@@ -1099,12 +2143,63 @@ async function openConsole(options) {
1099
2143
  screen.busy = void 0;
1100
2144
  }
1101
2145
  };
2146
+ const look = async (runId) => {
2147
+ if (options.check === void 0) {
2148
+ say("no way to read the current state was configured");
2149
+ return;
2150
+ }
2151
+ screen.busy = "reading how things are now...";
2152
+ try {
2153
+ const found = await options.check(runId);
2154
+ const changed = found.resources.filter((one) => one.condition === "changed");
2155
+ say(
2156
+ [
2157
+ verdict(found),
2158
+ ...changed.length === 0 ? [] : ["", ...changed.flatMap((one) => [`${String(one.seq)} ${one.server}.${one.tool}`, ...(one.diff ?? "").split("\n")])]
2159
+ ].join("\n"),
2160
+ changed.length === 0 ? NOTICE_TICKS2 : READING_TICKS
2161
+ );
2162
+ } catch (error) {
2163
+ say(error instanceof Error ? error.message : "could not read the current state");
2164
+ } finally {
2165
+ screen.busy = void 0;
2166
+ }
2167
+ };
2168
+ const rescan = () => {
2169
+ screen.groups = options.scan?.() ?? [];
2170
+ };
2171
+ const connect = async (targets) => {
2172
+ if (options.connect === void 0) {
2173
+ say("no way to connect was configured");
2174
+ return;
2175
+ }
2176
+ if (targets.length === 0) {
2177
+ say("everything here is already covered");
2178
+ return;
2179
+ }
2180
+ if (screen.busy !== void 0) {
2181
+ return;
2182
+ }
2183
+ screen.busy = `connecting ${String(targets.length)}${targets.length === 1 ? " server" : " servers"}`;
2184
+ try {
2185
+ say(await options.connect(targets));
2186
+ } catch (error) {
2187
+ say(error instanceof Error ? error.message : "connecting failed");
2188
+ } finally {
2189
+ screen.busy = void 0;
2190
+ rescan();
2191
+ }
2192
+ };
1102
2193
  const press = (key) => {
1103
2194
  if (screen.confirming !== void 0) {
1104
2195
  const runId = screen.confirming;
2196
+ const forcing = screen.confirmingForce;
1105
2197
  screen.confirming = void 0;
2198
+ screen.confirmingForce = false;
2199
+ screen.confirmingLabel = void 0;
1106
2200
  if (key === "y") {
1107
- void perform(runId, false);
2201
+ screen.warned = void 0;
2202
+ void perform(runId, false, forcing);
1108
2203
  } else {
1109
2204
  say("left alone");
1110
2205
  }
@@ -1127,7 +2222,23 @@ async function openConsole(options) {
1127
2222
  screen.mode = "gates";
1128
2223
  screen.cursor = 0;
1129
2224
  return;
2225
+ case "c":
2226
+ screen.mode = "connections";
2227
+ screen.cursor = 0;
2228
+ rescan();
2229
+ return;
2230
+ case "f":
2231
+ if (screen.mode === "run") {
2232
+ screen.expanded = !screen.expanded;
2233
+ say(screen.expanded ? "showing everything recorded" : "back to a summary");
2234
+ }
2235
+ return;
1130
2236
  case "r":
2237
+ if (screen.mode === "connections") {
2238
+ rescan();
2239
+ say("rescanned");
2240
+ return;
2241
+ }
1131
2242
  screen.mode = "runs";
1132
2243
  screen.cursor = 0;
1133
2244
  return;
@@ -1138,6 +2249,11 @@ async function openConsole(options) {
1138
2249
  return;
1139
2250
  case "\r":
1140
2251
  case "\n": {
2252
+ if (screen.mode === "connections") {
2253
+ const chosen = connectionRows(screen)[screen.cursor];
2254
+ void connect(chosen === void 0 ? [] : [chosen]);
2255
+ return;
2256
+ }
1141
2257
  const ready = open();
1142
2258
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1143
2259
  if (run !== void 0) {
@@ -1149,6 +2265,10 @@ async function openConsole(options) {
1149
2265
  case "a":
1150
2266
  if (screen.mode === "gates") {
1151
2267
  decide(true);
2268
+ return;
2269
+ }
2270
+ if (screen.mode === "connections") {
2271
+ void connect(needsConnecting(screen.groups));
1152
2272
  }
1153
2273
  return;
1154
2274
  case "d":
@@ -1156,24 +2276,73 @@ async function openConsole(options) {
1156
2276
  decide(false);
1157
2277
  }
1158
2278
  return;
2279
+ case "l": {
2280
+ if (!onASession()) {
2281
+ return;
2282
+ }
2283
+ if (screen.busy !== void 0) {
2284
+ say("still working on the last one");
2285
+ return;
2286
+ }
2287
+ const ready = open();
2288
+ const run = ready === void 0 ? void 0 : selectedRun(ready);
2289
+ if (ready === void 0 || run === void 0) {
2290
+ return;
2291
+ }
2292
+ void look(run.id);
2293
+ return;
2294
+ }
1159
2295
  case "p": {
2296
+ if (!onASession()) {
2297
+ return;
2298
+ }
1160
2299
  const ready = open();
1161
2300
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1162
- if (run !== void 0) {
1163
- void perform(run.id, true);
2301
+ if (ready === void 0 || run === void 0) {
2302
+ return;
2303
+ }
2304
+ const here = standing(ready.getActions(run.id));
2305
+ if (here.undoable === 0 && here.conflicted === 0) {
2306
+ say(nothingToUndo(ready, run));
2307
+ return;
1164
2308
  }
2309
+ void perform(run.id, true);
1165
2310
  return;
1166
2311
  }
1167
2312
  case "u": {
2313
+ if (!onASession()) {
2314
+ say("undo works on a session; r for the list");
2315
+ return;
2316
+ }
1168
2317
  if (screen.busy !== void 0) {
1169
2318
  say("still working on the last one");
1170
2319
  return;
1171
2320
  }
1172
2321
  const ready = open();
1173
2322
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1174
- if (run !== void 0) {
2323
+ if (ready === void 0 || run === void 0) {
2324
+ return;
2325
+ }
2326
+ const here = standing(ready.getActions(run.id));
2327
+ if (here.undoable === 0 && here.conflicted === 0) {
2328
+ say(nothingToUndo(ready, run));
2329
+ return;
2330
+ }
2331
+ const label = `${run.label ?? "an agent"} ${shortTime(run.startedAt).trim()}`;
2332
+ if (here.undoable === 0) {
2333
+ if (screen.warned !== run.id) {
2334
+ screen.warned = run.id;
2335
+ void perform(run.id, true);
2336
+ return;
2337
+ }
1175
2338
  screen.confirming = run.id;
2339
+ screen.confirmingForce = true;
2340
+ screen.confirmingLabel = label;
2341
+ return;
1176
2342
  }
2343
+ screen.confirming = run.id;
2344
+ screen.confirmingForce = false;
2345
+ screen.confirmingLabel = label;
1177
2346
  return;
1178
2347
  }
1179
2348
  default:
@@ -1211,12 +2380,13 @@ async function openConsole(options) {
1211
2380
  for (; !screen.stop; tick += 1) {
1212
2381
  if (screen.notice !== "" && tick >= screen.noticeUntil) {
1213
2382
  screen.notice = "";
2383
+ screen.warned = void 0;
1214
2384
  }
1215
2385
  options.write(clear + frame());
1216
2386
  if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
1217
2387
  break;
1218
2388
  }
1219
- await new Promise((resolve2) => setTimeout(resolve2, interval));
2389
+ await new Promise((resolve4) => setTimeout(resolve4, interval));
1220
2390
  }
1221
2391
  return 0;
1222
2392
  } finally {
@@ -1232,7 +2402,7 @@ async function openConsole(options) {
1232
2402
  await reader?.return?.(void 0);
1233
2403
  await reading;
1234
2404
  })(),
1235
- new Promise((resolve2) => setTimeout(resolve2, 50).unref())
2405
+ new Promise((resolve4) => setTimeout(resolve4, 50).unref())
1236
2406
  ]);
1237
2407
  journal?.close();
1238
2408
  }
@@ -1248,24 +2418,36 @@ if (NODE_MAJOR < 22) {
1248
2418
  process.exit(2);
1249
2419
  }
1250
2420
  var COMMANDS = `
1251
- synartesis the screen; everything below,
1252
- driven with the arrow keys
2421
+ synartesis start here. Live activity,
2422
+ what is waiting for you, and
2423
+ undo -- all in one place, with
2424
+ the arrow keys. Everything
2425
+ below can be done from it.
2426
+ synartesis install [--client <name>] [--dry-run] [--print]
2427
+ synartesis uninstall [--client <name>]
2428
+ synartesis status
1253
2429
  synartesis init <server> -- <command> [args...] [--manifest <path>]
1254
2430
  synartesis check [--manifest <path>]
1255
2431
  synartesis list [--journal <path>]
1256
- synartesis show <runId> [--journal <path>]
2432
+ synartesis show <runId> [--full] [--live] [--journal <path>]
1257
2433
  synartesis gates [--journal <path>]
1258
2434
  synartesis close [runId] [--journal <path>]
1259
2435
  synartesis prune [--older-than <days>] [--dry-run] [--journal <path>]
1260
- synartesis proxy --manifest <path> [--journal <path>] what your agent runs
2436
+ synartesis proxy --manifest <path> [--server <name>] what your agent runs
2437
+ [--journal <path>]
1261
2438
  [--http <port> --token <secret>] for a client that
1262
2439
  cannot start one
1263
2440
  synartesis watch [--by <name>] [--journal <path>]
1264
2441
  synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
1265
2442
  synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
1266
- synartesis undo [runId] [--to <seq>] [--dry-run] [--replan]
2443
+ synartesis undo [runId] [--to <seq>] [--dry-run] [--replan] [--force [--yes]]
1267
2444
  [--manifest <path>] [--journal <path>]
1268
2445
 
2446
+ install is the short way in: it finds what Claude Code, Claude Desktop,
2447
+ Cursor or Codex already list, writes a policy covering all of it -- using the ones that
2448
+ ship where they fit -- and points each entry at the proxy. The original config
2449
+ is copied aside first, and uninstall puts it back. status says what is covered.
2450
+
1269
2451
  close ends a run left active by a proxy that was killed; nothing guesses at
1270
2452
  that, since several proxies can share one journal.
1271
2453
 
@@ -1280,6 +2462,15 @@ adds to an existing manifest rather than replacing it.
1280
2462
  watch is the one to leave running. Anything held for approval appears there,
1281
2463
  and a and d answer it without a second terminal or an id to copy.
1282
2464
 
2465
+ undo stops when somebody has changed the resource since, rather than writing
2466
+ over them. Three ways past that, and it prints all three: leave it, put the
2467
+ resource back as the run left it and --replan, or --force to overwrite.
2468
+
2469
+ --client claude-code, claude-desktop, cursor or codex; all by default
2470
+ --print show the entries install would write, and write nothing
2471
+ --full show every argument, snapshot and inverse in full, nothing elided
2472
+ --live read each resource as it is now and say what has changed since
2473
+ --server serve one server from the manifest, keeping its tool names
1283
2474
  --manifest synartesis.yaml, looked for here and upwards, then in the home
1284
2475
  --journal beside the manifest, or the one in the home
1285
2476
  --to lowest sequence to undo; earlier actions are left alone
@@ -1290,6 +2481,8 @@ and a and d answer it without a second terminal or an id to copy.
1290
2481
  --dry-run read current state and print the plan without changing anything
1291
2482
  --replan rebuild each undo from the current manifest, for a run recorded
1292
2483
  under a policy that turned out to be wrong
2484
+ --force undo even where the resource changed after the run. On its own it
2485
+ prints the lines it would write over and stops; add --yes to do it
1293
2486
  --older-than days of history prune keeps; defaults to 30
1294
2487
  --version print the version and exit
1295
2488
 
@@ -1314,7 +2507,7 @@ function flag(argv, name) {
1314
2507
  return value;
1315
2508
  }
1316
2509
  function positional(argv) {
1317
- const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than"]);
2510
+ const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than", "--client"]);
1318
2511
  const values = [];
1319
2512
  const end = argv.indexOf("--");
1320
2513
  const ours = end === -1 ? argv : argv.slice(0, end);
@@ -1369,6 +2562,197 @@ async function runCheck(argv) {
1369
2562
  out("");
1370
2563
  return 0;
1371
2564
  }
2565
+ async function runInstall(argv) {
2566
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2567
+ const only = flag(argv, "--client");
2568
+ const dryRun = argv.includes("--dry-run");
2569
+ const printOnly = argv.includes("--print");
2570
+ const sites = discover(process.cwd()).filter(
2571
+ (site) => only === void 0 || site.client === only
2572
+ );
2573
+ if (sites.length === 0) {
2574
+ out("");
2575
+ out(` ${style.quiet("No MCP client config was found on this machine.")}`);
2576
+ out(` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`);
2577
+ out("");
2578
+ return 0;
2579
+ }
2580
+ const invoker = invokerFor(version(), fileURLToPath2(import.meta.url));
2581
+ const { plans, yaml } = await planInstall(sites, manifestPath, invoker);
2582
+ const total = plans.reduce((sum, plan) => sum + plan.servers.length, 0);
2583
+ out("");
2584
+ if (invoker.note !== void 0 && total > 0) {
2585
+ out(` ${style.accent("note")} ${style.quiet(invoker.note)}`);
2586
+ }
2587
+ out(` ${style.label(dryRun || printOnly ? "would cover" : "covering")} ${style.strong(manifestPath)}`);
2588
+ out(` ${rule(60)}`);
2589
+ for (const plan of plans) {
2590
+ out("");
2591
+ out(` ${style.strong(plan.site.label)} ${style.quiet(plan.site.scope)}`);
2592
+ out(` ${style.quiet(plan.site.path)}`);
2593
+ for (const server of plan.servers) {
2594
+ const note = server.adopted === void 0 ? style.accent("drafted, every tool held until you say how to undo it") : style.quiet(`the policy that ships for ${server.adopted} (${String(server.tools ?? 0)} tools)`);
2595
+ out(` ${style.strong(server.name.padEnd(18))} ${note}`);
2596
+ }
2597
+ for (const skip of plan.skipped) {
2598
+ out(` ${style.quiet(skip.name.padEnd(18))} ${style.quiet(skip.why)}`);
2599
+ }
2600
+ if (plan.servers.length === 0 && plan.skipped.length === 0) {
2601
+ out(` ${style.quiet("no servers listed")}`);
2602
+ }
2603
+ }
2604
+ if (printOnly) {
2605
+ out("");
2606
+ out(` ${style.label("entries")}`);
2607
+ for (const plan of plans) {
2608
+ for (const server of plan.servers) {
2609
+ out(` ${JSON.stringify({ [server.name]: server.wrapped }, void 0, 2)}`);
2610
+ }
2611
+ }
2612
+ out("");
2613
+ return 0;
2614
+ }
2615
+ if (total === 0) {
2616
+ out("");
2617
+ out(` ${style.quiet("Nothing to do; everything found is already covered.")}`);
2618
+ out("");
2619
+ return 0;
2620
+ }
2621
+ if (dryRun) {
2622
+ out("");
2623
+ out(` ${style.quiet("Nothing was written. Run without --dry-run to apply.")}`);
2624
+ out("");
2625
+ return 0;
2626
+ }
2627
+ const applied = applyInstall(plans, manifestPath, yaml);
2628
+ out("");
2629
+ for (const entry of applied) {
2630
+ out(` ${style.quiet("backed up to")} ${entry.backup}`);
2631
+ }
2632
+ out("");
2633
+ out(` ${style.quiet("Restart your client, and its servers now run through Synartesis.")}`);
2634
+ out("");
2635
+ out(` ${style.quiet("One command shows everything and does everything:")}`);
2636
+ out(` ${style.accent(cliCommand())}`);
2637
+ out("");
2638
+ out(
2639
+ ` ${style.quiet("Live activity, what is held for approval, and undo, all from there.")}`
2640
+ );
2641
+ out(` ${style.quiet("You do not need a second terminal unless you want one.")}`);
2642
+ out("");
2643
+ return 0;
2644
+ }
2645
+ async function runUninstall(argv) {
2646
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2647
+ const only = flag(argv, "--client");
2648
+ const sites = discover(process.cwd()).filter(
2649
+ (site) => only === void 0 || site.client === only
2650
+ );
2651
+ const restored = applyUninstall(sites, manifestPath);
2652
+ out("");
2653
+ if (restored.length === 0) {
2654
+ out(` ${style.quiet("Nothing was covered, so nothing was changed.")}`);
2655
+ out("");
2656
+ return 0;
2657
+ }
2658
+ out(` ${style.label("restored")}`);
2659
+ out(` ${rule(60)}`);
2660
+ for (const entry of restored) {
2661
+ out("");
2662
+ out(` ${style.strong(entry.site.label)} ${style.quiet(entry.site.scope)}`);
2663
+ for (const name of entry.servers) {
2664
+ out(` ${style.strong(name)}`);
2665
+ }
2666
+ for (const name of entry.unknown) {
2667
+ out(
2668
+ ` ${style.accent(name)} ${style.quiet("is wrapped but its original was not recorded; left as it is")}`
2669
+ );
2670
+ }
2671
+ if (entry.backup !== "") {
2672
+ out(` ${style.quiet(`backed up to ${entry.backup}`)}`);
2673
+ }
2674
+ }
2675
+ out("");
2676
+ out(` ${style.quiet("The policy and journal were left alone.")}`);
2677
+ out("");
2678
+ return await Promise.resolve(0);
2679
+ }
2680
+ function openIfPresent(journalPath) {
2681
+ try {
2682
+ return existsSync7(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
2683
+ } catch {
2684
+ return void 0;
2685
+ }
2686
+ }
2687
+ async function connectThese(targets, manifestPath) {
2688
+ const invoker = invokerFor(version(), fileURLToPath2(import.meta.url));
2689
+ const wanted = /* @__PURE__ */ new Map();
2690
+ const sites = /* @__PURE__ */ new Map();
2691
+ for (const target of targets) {
2692
+ const key = `${target.site.path}${target.site.scope}`;
2693
+ sites.set(key, target.site);
2694
+ (wanted.get(key) ?? wanted.set(key, /* @__PURE__ */ new Set()).get(key))?.add(target.server);
2695
+ }
2696
+ const { plans, yaml } = await planInstall([...sites.values()], manifestPath, invoker);
2697
+ const narrowed = plans.map((plan) => ({
2698
+ ...plan,
2699
+ servers: plan.servers.filter(
2700
+ (server) => wanted.get(`${plan.site.path}${plan.site.scope}`)?.has(server.name) === true
2701
+ )
2702
+ }));
2703
+ const applied = applyInstall(narrowed, manifestPath, yaml);
2704
+ const count = applied.reduce((sum, entry) => sum + entry.servers.length, 0);
2705
+ if (count === 0) {
2706
+ return "nothing was connected; see the reasons above";
2707
+ }
2708
+ return `connected ${String(count)}${count === 1 ? " server" : " servers"} \xB7 restart the client to pick it up`;
2709
+ }
2710
+ function runStatus(argv) {
2711
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2712
+ const journalPath = findJournal(flag(argv, "--journal"), manifestPath);
2713
+ out("");
2714
+ out(
2715
+ ` ${style.label("policy")} ${existsSync7(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
2716
+ );
2717
+ out(
2718
+ ` ${style.label("journal")} ${bytesOf(journalPath) === void 0 ? style.quiet(`${journalPath} (none yet)`) : `${style.strong(journalPath)} ${style.quiet(sizeOf(journalPath))}`}`
2719
+ );
2720
+ out("");
2721
+ const journal = openIfPresent(journalPath);
2722
+ try {
2723
+ const groups = scan(journal, process.cwd());
2724
+ if (groups.length === 0) {
2725
+ out(` ${style.quiet("No MCP client config found.")}`);
2726
+ out(` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`);
2727
+ out("");
2728
+ return 0;
2729
+ }
2730
+ const now = /* @__PURE__ */ new Date();
2731
+ for (const group of groups) {
2732
+ out(` ${style.strong(group.label)} ${style.quiet(group.scope)}`);
2733
+ if (group.problem !== void 0) {
2734
+ out(` ${style.accent(group.problem)}`);
2735
+ } else if (group.connections.length === 0) {
2736
+ out(` ${style.quiet("no servers listed")}`);
2737
+ }
2738
+ for (const connection of group.connections) {
2739
+ const state = stateOf(connection, now);
2740
+ out(
2741
+ ` ${connection.server.padEnd(20)} ${connection.covered ? style.quiet(state) : style.accent(state)}`
2742
+ );
2743
+ }
2744
+ out("");
2745
+ }
2746
+ const waiting = needsConnecting(groups).length;
2747
+ out(
2748
+ waiting === 0 ? ` ${style.quiet("Everything found is covered.")}` : ` ${style.accent(`${String(waiting)} not covered.`)} ${style.quiet(`${cliCommand()} install covers them.`)}`
2749
+ );
2750
+ out("");
2751
+ } finally {
2752
+ journal?.close();
2753
+ }
2754
+ return 0;
2755
+ }
1372
2756
  async function runInit(argv) {
1373
2757
  const name = positional(argv)[1];
1374
2758
  const separator = argv.indexOf("--");
@@ -1381,7 +2765,7 @@ async function runInit(argv) {
1381
2765
  }
1382
2766
  const path = findManifest(flag(argv, "--manifest"));
1383
2767
  const force = argv.includes("--force");
1384
- const present = existsSync4(path);
2768
+ const present = existsSync7(path);
1385
2769
  if (present && force) {
1386
2770
  throw new UsageError(
1387
2771
  `--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
@@ -1391,11 +2775,11 @@ async function runInit(argv) {
1391
2775
  name,
1392
2776
  command,
1393
2777
  args: argv.slice(separator + 2),
1394
- ...present ? { existing: readFileSync2(path, "utf8") } : {}
2778
+ ...present ? { existing: readFileSync4(path, "utf8") } : {}
1395
2779
  });
1396
2780
  parseManifest(draft.yaml, path);
1397
- mkdirSync(dirname(resolve(path)), { recursive: true, mode: 448 });
1398
- writeFileSync(path, draft.yaml);
2781
+ mkdirSync2(dirname3(resolve3(path)), { recursive: true, mode: 448 });
2782
+ writeFileSync3(path, draft.yaml);
1399
2783
  out("");
1400
2784
  out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
1401
2785
  out(` ${rule(54)}`);
@@ -1410,7 +2794,7 @@ async function runInit(argv) {
1410
2794
  out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
1411
2795
  }
1412
2796
  out("");
1413
- out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
2797
+ out(` ${style.accent(`${proxyCommand()} --manifest ${resolve3(path)}`)}`);
1414
2798
  out("");
1415
2799
  return 0;
1416
2800
  }
@@ -1472,12 +2856,12 @@ function runList(journal, asJson, journalPath) {
1472
2856
  return 0;
1473
2857
  }
1474
2858
  out("");
1475
- out(` ${style.label("runs")} ${style.quiet("most recent first")}`);
2859
+ out(` ${style.label("sessions")} ${style.quiet("most recent first")}`);
1476
2860
  out(` ${rule(96)}`);
1477
2861
  out("");
1478
2862
  out(
1479
2863
  style.quiet(
1480
- ` ${"run".padEnd(36)} ${"started".padEnd(24)} ${"status".padEnd(12)} actions agent`
2864
+ ` ${"session".padEnd(36)} ${"started".padEnd(15)} ${"status".padEnd(12)} actions agent`
1481
2865
  )
1482
2866
  );
1483
2867
  for (const run of runs) {
@@ -1490,7 +2874,7 @@ function runList(journal, asJson, journalPath) {
1490
2874
  ].filter((note2) => note2 !== "");
1491
2875
  const note = notes.length === 0 ? "" : ` ${style.accent(`(${notes.join("; ")})`)}`;
1492
2876
  out(
1493
- ` ${style.strong(run.id)} ${style.quiet(run.startedAt)} ${run.status.padEnd(12)} ${String(actions.length).padStart(7)} ${run.label ?? "-"}${note}`
2877
+ ` ${style.strong(run.id)} ${style.quiet(shortTime(run.startedAt).trimEnd().padEnd(13))} ${run.status.padEnd(12)} ${String(actions.length).padStart(7)} ${run.label ?? "-"}${note}`
1494
2878
  );
1495
2879
  }
1496
2880
  out("");
@@ -1500,22 +2884,34 @@ function runList(journal, asJson, journalPath) {
1500
2884
  }
1501
2885
  return 0;
1502
2886
  }
1503
- function runShow(argv, journal, asJson) {
2887
+ async function runShow(argv, journal, asJson) {
2888
+ const full = argv.includes("--full");
1504
2889
  const runs = [...journal.listRuns()].reverse();
1505
2890
  const run = pick(runs, positional(argv)[1], RUN, true);
1506
2891
  const runId = run.id;
2892
+ const inspection = argv.includes("--live") && journal.getActions(runId).length > 0 ? await withUpstreams(
2893
+ findManifest(flag(argv, "--manifest")),
2894
+ async (router) => await inspect({ journal, router, runId }),
2895
+ serversUsedBy(journal, runId)
2896
+ ) : void 0;
1507
2897
  if (asJson) {
1508
- out(JSON.stringify({ run, actions: journal.getActions(runId) }));
2898
+ out(
2899
+ JSON.stringify({
2900
+ run,
2901
+ actions: journal.getActions(runId),
2902
+ ...inspection === void 0 ? {} : { live: inspection.resources }
2903
+ })
2904
+ );
1509
2905
  return 0;
1510
2906
  }
1511
2907
  out("");
1512
- out(` ${style.label("run")} ${style.strong(run.id)}`);
2908
+ out(` ${style.label("session")} ${style.strong(run.label ?? "an agent")} ${style.quiet(run.id)}`);
1513
2909
  out(` ${rule(54)}`);
1514
2910
  out("");
1515
2911
  out(` ${style.quiet("agent ")} ${run.label ?? "-"}`);
1516
- out(` ${style.quiet("started")} ${run.startedAt}`);
2912
+ out(` ${style.quiet("started")} ${fullTime(run.startedAt)} ${style.quiet(ago(run.startedAt))}`);
1517
2913
  out(
1518
- ` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${run.endedAt}`))
2914
+ ` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
1519
2915
  );
1520
2916
  const actions = journal.getActions(runId);
1521
2917
  if (actions.length === 0) {
@@ -1528,29 +2924,87 @@ function runShow(argv, journal, asJson) {
1528
2924
  out(` ${style.label("timeline")}`);
1529
2925
  out(` ${rule(72)}`);
1530
2926
  out("");
2927
+ const live = new Map((inspection?.resources ?? []).map((found) => [found.seq, found]));
1531
2928
  for (const action of actions) {
2929
+ const now = live.get(action.seq);
1532
2930
  out(
1533
- ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf2(action)} ${style.strong(`${action.server}.${action.tool}`)}`
2931
+ ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}` + (now === void 0 ? "" : ` ${conditionOf(now)}`)
1534
2932
  );
1535
- out(` ${style.quiet(truncate3(JSON.stringify(action.args), 96))}`);
2933
+ if (now?.diff !== void 0) {
2934
+ for (const line2 of now.diff.split("\n")) {
2935
+ out(` ${style.quiet(line2)}`);
2936
+ }
2937
+ }
2938
+ if (full) {
2939
+ out(` ${style.quiet("arguments")}`);
2940
+ for (const line2 of block(action.args)) {
2941
+ out(` ${line2}`);
2942
+ }
2943
+ } else {
2944
+ out(` ${style.quiet(summariseArgs(action.args, 96))}`);
2945
+ }
1536
2946
  if (action.approvedAt !== void 0) {
1537
2947
  const verb = action.status === "denied" ? "denied" : "approved";
1538
2948
  out(
1539
- ` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${action.approvedAt}`)}`
2949
+ ` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${fullTime(action.approvedAt)}`)}`
1540
2950
  );
1541
2951
  }
1542
2952
  if (action.error !== void 0) {
1543
- out(` ${style.quiet(`note: ${truncate3(action.error, 200)}`)}`);
2953
+ out(` ${style.quiet(`note: ${full ? action.error : truncate3(action.error, 200)}`)}`);
2954
+ }
2955
+ if (action.snapshot !== void 0 && full) {
2956
+ out(` ${style.quiet("what it replaced")}`);
2957
+ for (const line2 of block(action.snapshot)) {
2958
+ out(` ${line2}`);
2959
+ }
1544
2960
  }
1545
2961
  if (action.inverse !== void 0) {
1546
- out(` ${style.quiet("undo:")} ${truncate3(JSON.stringify(action.inverse), 200)}`);
2962
+ if (full) {
2963
+ out(` ${style.quiet("undo")}`);
2964
+ for (const line2 of block(action.inverse)) {
2965
+ out(` ${line2}`);
2966
+ }
2967
+ } else {
2968
+ out(` ${style.quiet("undo:")} ${style.quiet(summariseArgs(inverseArgs(action.inverse), 90))}`);
2969
+ }
1547
2970
  }
1548
2971
  }
1549
2972
  out("");
1550
2973
  out(` ${summarise2(actions)}`);
2974
+ if (inspection !== void 0) {
2975
+ out("");
2976
+ const spoiled = inspection.resources.some((found) => found.condition === "changed");
2977
+ out(` ${spoiled ? style.accent(verdict(inspection)) : style.quiet(verdict(inspection))}`);
2978
+ if (spoiled) {
2979
+ out(
2980
+ ` ${style.quiet("undo stops at the first of them; ")}${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force`)}${style.quiet(" shows what it would write")}`
2981
+ );
2982
+ }
2983
+ } else {
2984
+ out("");
2985
+ out(
2986
+ ` ${style.quiet("has anything changed since? ")}${style.strong(`${cliCommand()} show ${runId.slice(0, 8)} --live`)}`
2987
+ );
2988
+ }
1551
2989
  out("");
1552
2990
  return 0;
1553
2991
  }
2992
+ function conditionOf(found) {
2993
+ switch (found.condition) {
2994
+ case "changed":
2995
+ return style.accent("changed since");
2996
+ case "unchanged":
2997
+ return style.quiet("unchanged");
2998
+ case "restored":
2999
+ return style.quiet(found.note === void 0 ? "back to before" : `back to before, ${found.note}`);
3000
+ case "superseded":
3001
+ return style.quiet("older write to the same thing");
3002
+ case "not-applied":
3003
+ return style.quiet(`never applied (${found.note ?? "settled"})`);
3004
+ default:
3005
+ return style.quiet(found.note ?? "cannot tell");
3006
+ }
3007
+ }
1554
3008
  var CLASS_MARK = {
1555
3009
  readonly: "\xB7",
1556
3010
  reversible: "\u2190",
@@ -1563,7 +3017,7 @@ function badgeOf(action) {
1563
3017
  const plain = `${CLASS_MARK[action.class]} ${action.class}`.padEnd(BADGE_WIDTH);
1564
3018
  return action.class === "irreversible" ? style.accent(plain) : style.quiet(plain);
1565
3019
  }
1566
- function statusOf2(action) {
3020
+ function statusOf(action) {
1567
3021
  const text = labelFor(action).padEnd(13);
1568
3022
  if (wasRefused(action)) {
1569
3023
  return style.accent(text);
@@ -1591,6 +3045,15 @@ function wrapped(text, width) {
1591
3045
  }
1592
3046
  return lines;
1593
3047
  }
3048
+ function block(value) {
3049
+ return JSON.stringify(value, void 0, 2).split("\n").map((line2) => style.quiet(line2));
3050
+ }
3051
+ function inverseArgs(inverse) {
3052
+ if (typeof inverse === "object" && inverse !== null && "args" in inverse) {
3053
+ return inverse.args;
3054
+ }
3055
+ return inverse;
3056
+ }
1594
3057
  function truncate3(text, limit) {
1595
3058
  return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
1596
3059
  }
@@ -1761,7 +3224,7 @@ function runDecision(argv, journal, approving) {
1761
3224
  }
1762
3225
  return failed === 0 ? 0 : 1;
1763
3226
  }
1764
- function report(result) {
3227
+ function report(result, alreadyForcing = false) {
1765
3228
  out("");
1766
3229
  out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
1767
3230
  out(` ${rule(72)}`);
@@ -1786,15 +3249,37 @@ function report(result) {
1786
3249
  }
1787
3250
  }
1788
3251
  if (result.halted !== void 0) {
3252
+ const halt = result.halted;
1789
3253
  out("");
1790
3254
  out(
1791
- ` ${style.accent("halted")} ${style.quiet(`at sequence ${String(result.halted.seq)}`)} ${result.halted.reason}`
3255
+ ` ${style.accent("halted")} ${style.quiet(`at ${String(halt.seq)}`)} ${halt.reason}
3256
+ ${style.quiet("nothing was written here")}`
1792
3257
  );
1793
- if (result.halted.detail !== "") {
1794
- for (const line2 of result.halted.detail.split("\n")) {
3258
+ if (halt.detail !== "") {
3259
+ out("");
3260
+ for (const line2 of halt.detail.split("\n")) {
3261
+ out(` ${style.quiet(line2)}`);
3262
+ }
3263
+ }
3264
+ if (halt.overwrites !== void 0 && halt.overwrites !== "") {
3265
+ out("");
3266
+ out(` ${style.accent("undoing anyway would write:")}`);
3267
+ for (const line2 of halt.overwrites.split("\n")) {
1795
3268
  out(` ${style.quiet(line2)}`);
1796
3269
  }
1797
3270
  }
3271
+ if (halt.conflict === true && !alreadyForcing) {
3272
+ const self = cliCommand();
3273
+ const id = result.runId.slice(0, 8);
3274
+ out("");
3275
+ out(` ${style.quiet("keep the change, drop the undo:")} ${style.quiet("nothing to do")}`);
3276
+ out(
3277
+ ` ${style.quiet("put it back as the run left it:")} ${style.strong(`${self} undo ${id} --replan`)}`
3278
+ );
3279
+ out(
3280
+ ` ${style.quiet("undo anyway, losing the change:")} ${style.strong(`${self} undo ${id} --force`)}`
3281
+ );
3282
+ }
1798
3283
  }
1799
3284
  const permanent = result.steps.filter((step) => step.kind === "permanent");
1800
3285
  if (permanent.length > 0) {
@@ -1810,37 +3295,99 @@ function report(result) {
1810
3295
  out("");
1811
3296
  return result.status === "rolled_back" ? 0 : 1;
1812
3297
  }
1813
- async function performUndo(manifestPath, journal, runId, options) {
3298
+ async function withUpstreams(manifestPath, use, only) {
1814
3299
  const manifest = loadManifest(manifestPath);
1815
3300
  const upstreams = [];
3301
+ const missing = [];
1816
3302
  try {
1817
3303
  for (const [name, spec] of Object.entries(manifest.servers)) {
1818
- upstreams.push(
1819
- await connectStdioUpstream({
1820
- name,
1821
- command: spec.command,
1822
- args: spec.args,
1823
- stderr: "capture",
1824
- ...spec.env === void 0 ? {} : { env: spec.env }
1825
- })
1826
- );
3304
+ if (only !== void 0 && !only.has(name)) {
3305
+ continue;
3306
+ }
3307
+ try {
3308
+ upstreams.push(
3309
+ await connectStdioUpstream({
3310
+ name,
3311
+ command: spec.command,
3312
+ args: spec.args,
3313
+ stderr: "capture",
3314
+ ...spec.env === void 0 ? {} : { env: spec.env }
3315
+ })
3316
+ );
3317
+ } catch (error) {
3318
+ missing.push(`${name}: ${describe(error)}`);
3319
+ }
1827
3320
  }
1828
- return await rollback({
1829
- journal,
1830
- router: createRouter(upstreams, manifest),
1831
- runId,
1832
- ...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
1833
- dryRun: options.dryRun,
1834
- ...options.replan === true ? { replanWith: manifest } : {}
1835
- });
3321
+ if (upstreams.length === 0 && missing.length > 0) {
3322
+ throw new ManifestError(`no server could be started. ${missing.join("; ")}`);
3323
+ }
3324
+ for (const why of missing) {
3325
+ process.stderr.write(`synartesis: ${why}; anything through it cannot be reached
3326
+ `);
3327
+ }
3328
+ return await use(createRouter(upstreams, manifest), manifest);
1836
3329
  } finally {
1837
3330
  for (const upstream of upstreams) {
1838
3331
  await upstream.close();
1839
3332
  }
1840
3333
  }
1841
3334
  }
3335
+ function serversUsedBy(journal, runId) {
3336
+ return new Set(journal.getActions(runId).map((action) => action.server));
3337
+ }
3338
+ async function performUndo(manifestPath, journal, runId, options) {
3339
+ return await withUpstreams(
3340
+ manifestPath,
3341
+ async (router, manifest) => await rollback({
3342
+ journal,
3343
+ router,
3344
+ runId,
3345
+ ...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
3346
+ ...options.replan === true ? { replanWith: manifest } : {},
3347
+ dryRun: options.dryRun,
3348
+ ...options.force === true ? { force: true } : {}
3349
+ }),
3350
+ // A replan re-resolves inverses from the current policy, which may name a
3351
+ // server this run never used; everything else needs only what it touched.
3352
+ options.replan === true ? void 0 : serversUsedBy(journal, runId)
3353
+ );
3354
+ }
1842
3355
  async function runUndo(argv, journal) {
1843
- const runId = pick([...journal.listRuns()].reverse(), positional(argv)[1], RUN, true).id;
3356
+ const given = positional(argv)[1];
3357
+ const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
3358
+ const runId = chosen.id;
3359
+ if (given === void 0) {
3360
+ const actions = journal.getActions(runId);
3361
+ const left = actions.filter((action) => action.status === "applied").length;
3362
+ out("");
3363
+ out(
3364
+ ` ${style.quiet("no session named, so the most recent:")} ${style.strong(runId.slice(0, 8))} ` + style.quiet(`${chosen.label ?? "an agent"}, ${shortTime(chosen.startedAt).trim()}`)
3365
+ );
3366
+ if (left === 0) {
3367
+ out("");
3368
+ out(
3369
+ ` ${style.quiet(
3370
+ actions.length === 0 ? "Nothing was recorded in it, so there is nothing to undo." : "Nothing in it is still applied; it has already been undone."
3371
+ )}`
3372
+ );
3373
+ const other = [...journal.listRuns()].reverse().find(
3374
+ (run) => run.id !== runId && journal.getActions(run.id).some((action) => action.status === "applied" && action.inverse !== void 0)
3375
+ );
3376
+ if (other !== void 0) {
3377
+ out("");
3378
+ out(
3379
+ ` ${style.quiet("The session that did something:")} ${style.strong(other.id.slice(0, 8))} ` + style.quiet(`${other.label ?? "an agent"}, ${shortTime(other.startedAt).trim()}`)
3380
+ );
3381
+ out(` ${style.quiet(`${cliCommand()} undo ${other.id.slice(0, 8)}`)}`);
3382
+ } else {
3383
+ out(
3384
+ ` ${style.quiet(`Name one to undo a different session: ${cliCommand()} undo <session>`)}`
3385
+ );
3386
+ }
3387
+ out("");
3388
+ return 0;
3389
+ }
3390
+ }
1844
3391
  const rawTo = flag(argv, "--to");
1845
3392
  const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
1846
3393
  if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
@@ -1854,15 +3401,60 @@ async function runUndo(argv, journal) {
1854
3401
  );
1855
3402
  }
1856
3403
  }
3404
+ const forcing = argv.includes("--force");
3405
+ const said = argv.includes("--yes");
3406
+ const manifestPath = findManifest(flag(argv, "--manifest"));
3407
+ if (said && !forcing) {
3408
+ process.stderr.write("synartesis: --yes only means anything with --force; ignoring it\n");
3409
+ }
3410
+ if (forcing && !said) {
3411
+ const over = (await withUpstreams(
3412
+ manifestPath,
3413
+ async (router) => await inspect({ journal, router, runId }),
3414
+ serversUsedBy(journal, runId)
3415
+ )).resources.filter(
3416
+ // Below --to nothing is undone, so a change down there is not something
3417
+ // this command would write over and must not stand in its way.
3418
+ (one) => one.condition === "changed" && (toSeq === void 0 || one.seq >= toSeq)
3419
+ );
3420
+ if (over.length > 0) {
3421
+ out("");
3422
+ out(
3423
+ ` ${style.accent(`${String(over.length)} changed since this ran`)} ` + style.quiet(`undoing would write over ${over.length === 1 ? "it" : "them"}`)
3424
+ );
3425
+ for (const one of over) {
3426
+ out("");
3427
+ out(` ${style.quiet(String(one.seq).padStart(3))} ${style.strong(`${one.server}.${one.tool}`)}`);
3428
+ for (const line2 of (one.diff ?? "").split("\n")) {
3429
+ out(` ${style.quiet(line2)}`);
3430
+ }
3431
+ }
3432
+ out("");
3433
+ out(` ${style.quiet("nothing has been written. To go ahead and lose that:")}`);
3434
+ out(` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`);
3435
+ out("");
3436
+ return 1;
3437
+ }
3438
+ }
1857
3439
  return report(
1858
- await performUndo(findManifest(flag(argv, "--manifest")), journal, runId, {
3440
+ await performUndo(manifestPath, journal, runId, {
1859
3441
  dryRun: argv.includes("--dry-run"),
1860
3442
  ...toSeq === void 0 ? {} : { toSeq },
1861
- replan: argv.includes("--replan")
1862
- })
3443
+ replan: argv.includes("--replan"),
3444
+ ...forcing && said ? { force: true } : {}
3445
+ }),
3446
+ forcing
1863
3447
  );
1864
3448
  }
1865
3449
  var FLAGS = /* @__PURE__ */ new Set([
3450
+ // Two lists have to agree about a flag: this one decides whether it is
3451
+ // accepted at all, and the skip set in positional() decides whether its
3452
+ // value is mistaken for a command. --client was in one and not the other,
3453
+ // so `install --client codex` printed the help instead of installing.
3454
+ "--client",
3455
+ "--print",
3456
+ "--full",
3457
+ "--live",
1866
3458
  "--manifest",
1867
3459
  "--journal",
1868
3460
  "--to",
@@ -1874,6 +3466,7 @@ var FLAGS = /* @__PURE__ */ new Set([
1874
3466
  "--replan",
1875
3467
  "--reason",
1876
3468
  "--force",
3469
+ "--yes",
1877
3470
  "--older-than",
1878
3471
  "--help",
1879
3472
  "-h",
@@ -1882,8 +3475,8 @@ var FLAGS = /* @__PURE__ */ new Set([
1882
3475
  ]);
1883
3476
  function version() {
1884
3477
  try {
1885
- const root = dirname(fileURLToPath2(import.meta.url));
1886
- const parsed = JSON.parse(readFileSync2(join(root, "..", "package.json"), "utf8"));
3478
+ const root = dirname3(fileURLToPath2(import.meta.url));
3479
+ const parsed = JSON.parse(readFileSync4(join2(root, "..", "package.json"), "utf8"));
1887
3480
  const found = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
1888
3481
  return typeof found === "string" ? found : "unknown";
1889
3482
  } catch {
@@ -1901,7 +3494,7 @@ function rejectUnknownFlags(argv) {
1901
3494
  }
1902
3495
  }
1903
3496
  function openJournalOrExplain(journalPath) {
1904
- if (!existsSync4(journalPath)) {
3497
+ if (!existsSync7(journalPath)) {
1905
3498
  throw new UsageError(
1906
3499
  `nothing has been recorded yet: there is no journal at ${journalPath}. One appears the first time an agent calls a tool through synartesis proxy.`
1907
3500
  );
@@ -1930,13 +3523,30 @@ ${COMMANDS}`);
1930
3523
  const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
1931
3524
  return await openConsole({
1932
3525
  journalPath: journalPath2,
3526
+ scan: () => scan(openIfPresent(journalPath2), process.cwd()),
3527
+ connect: async (targets) => await connectThese(targets, manifestPath),
1933
3528
  write: (text) => process.stdout.write(text),
1934
3529
  live: process.stdout.isTTY,
1935
3530
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown",
1936
- undo: async (runId, dryRun) => {
3531
+ check: async (runId) => {
3532
+ const journal2 = openJournal(journalPath2, { mustExist: true });
3533
+ try {
3534
+ return await withUpstreams(
3535
+ manifestPath,
3536
+ async (router) => await inspect({ journal: journal2, router, runId }),
3537
+ serversUsedBy(journal2, runId)
3538
+ );
3539
+ } finally {
3540
+ journal2.close();
3541
+ }
3542
+ },
3543
+ undo: async (runId, dryRun, force) => {
1937
3544
  const journal2 = openJournal(journalPath2, { mustExist: true });
1938
3545
  try {
1939
- return await performUndo(manifestPath, journal2, runId, { dryRun });
3546
+ return await performUndo(manifestPath, journal2, runId, {
3547
+ dryRun,
3548
+ ...force === true ? { force: true } : {}
3549
+ });
1940
3550
  } finally {
1941
3551
  journal2.close();
1942
3552
  }
@@ -1949,6 +3559,15 @@ ${COMMANDS}`);
1949
3559
  if (command === "check") {
1950
3560
  return await runCheck(argv);
1951
3561
  }
3562
+ if (command === "install") {
3563
+ return await runInstall(argv);
3564
+ }
3565
+ if (command === "uninstall") {
3566
+ return await runUninstall(argv);
3567
+ }
3568
+ if (command === "status") {
3569
+ return runStatus(argv);
3570
+ }
1952
3571
  const asJson = argv.includes("--json");
1953
3572
  const given = flag(argv, "--journal");
1954
3573
  const journalPath = findJournal(given, findManifest(flag(argv, "--manifest")));
@@ -1964,8 +3583,8 @@ ${COMMANDS}`);
1964
3583
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
1965
3584
  });
1966
3585
  }
1967
- journalArg = given === void 0 ? "" : ` --journal ${resolve(given)}`;
1968
- if (!existsSync4(journalPath) && (command === "list" || command === "show" || command === "gates")) {
3586
+ journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
3587
+ if (!existsSync7(journalPath) && (command === "list" || command === "show" || command === "gates")) {
1969
3588
  if (asJson) {
1970
3589
  out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
1971
3590
  return 0;
@@ -1985,7 +3604,7 @@ ${COMMANDS}`);
1985
3604
  case "list":
1986
3605
  return runList(journal, asJson, journalPath);
1987
3606
  case "show":
1988
- return runShow(argv, journal, asJson);
3607
+ return await runShow(argv, journal, asJson);
1989
3608
  case "close":
1990
3609
  return runClose(argv, journal);
1991
3610
  case "prune":