synartesis 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 intended = intendedAfterInverse(action);
557
+ if (intended === void 0) {
558
+ return "";
559
+ }
560
+ return changedLines(current, intended);
561
+ }
540
562
  function intendedAfterInverse(action) {
541
563
  return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
542
564
  }
@@ -591,23 +613,159 @@ function keysIn(chunk) {
591
613
  return keys;
592
614
  }
593
615
 
616
+ // src/clock.ts
617
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
618
+ var pad = (value) => String(value).padStart(2, "0");
619
+ function isToday(when, now) {
620
+ return when.getFullYear() === now.getFullYear() && when.getMonth() === now.getMonth() && when.getDate() === now.getDate();
621
+ }
622
+ function shortTime(iso, now = /* @__PURE__ */ new Date()) {
623
+ const when = new Date(iso);
624
+ if (Number.isNaN(when.getTime())) {
625
+ return iso.slice(11, 19).padEnd(12);
626
+ }
627
+ const time = `${pad(when.getHours())}:${pad(when.getMinutes())}`;
628
+ if (isToday(when, now)) {
629
+ return `${time}:${pad(when.getSeconds())}`.padEnd(12);
630
+ }
631
+ return `${pad(when.getDate())} ${MONTHS[when.getMonth()] ?? ""} ${time}`.padEnd(12);
632
+ }
633
+ function fullTime(iso, now = /* @__PURE__ */ new Date()) {
634
+ const when = new Date(iso);
635
+ if (Number.isNaN(when.getTime())) {
636
+ return iso;
637
+ }
638
+ const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}:${pad(when.getSeconds())}`;
639
+ if (isToday(when, now)) {
640
+ return `today at ${clock}`;
641
+ }
642
+ return `${pad(when.getDate())} ${MONTHS[when.getMonth()] ?? ""} ${String(when.getFullYear())}, ${clock}`;
643
+ }
644
+ function ago(iso, now = /* @__PURE__ */ new Date()) {
645
+ const when = new Date(iso);
646
+ if (Number.isNaN(when.getTime())) {
647
+ return "";
648
+ }
649
+ const seconds = Math.max(0, Math.round((now.getTime() - when.getTime()) / 1e3));
650
+ if (seconds < 10) {
651
+ return "just now";
652
+ }
653
+ if (seconds < 60) {
654
+ return `${String(seconds)}s ago`;
655
+ }
656
+ const minutes = Math.round(seconds / 60);
657
+ if (minutes < 60) {
658
+ return `${String(minutes)}m ago`;
659
+ }
660
+ const hours = Math.round(minutes / 60);
661
+ if (hours < 24) {
662
+ return `${String(hours)}h ago`;
663
+ }
664
+ return `${String(Math.round(hours / 24))}d ago`;
665
+ }
666
+
667
+ // src/describe.ts
668
+ import { basename } from "path";
669
+ var SUBJECT_KEYS = [
670
+ "path",
671
+ "file_path",
672
+ "source",
673
+ "destination",
674
+ "id",
675
+ "name",
676
+ "key",
677
+ "entity",
678
+ "query"
679
+ ];
680
+ function isRecord(value) {
681
+ return typeof value === "object" && value !== null && !Array.isArray(value);
682
+ }
683
+ function subject(args) {
684
+ if (!isRecord(args)) {
685
+ return "";
686
+ }
687
+ const record = args;
688
+ for (const key of SUBJECT_KEYS) {
689
+ const value = record[key];
690
+ if (typeof value === "string" && value !== "") {
691
+ return value.includes("/") ? basename(value) : value;
692
+ }
693
+ }
694
+ for (const value of Object.values(record)) {
695
+ if (Array.isArray(value) && value.length > 0) {
696
+ return `${String(value.length)} items`;
697
+ }
698
+ }
699
+ return "";
700
+ }
701
+ function plainly(action) {
702
+ switch (action.status) {
703
+ case "gated":
704
+ return { text: "waiting for you", needs: true };
705
+ case "applied":
706
+ return action.class === "readonly" ? { text: "read", needs: false } : action.inverse === void 0 ? { text: "done, cannot undo", needs: false } : { text: "done, can undo", needs: false };
707
+ case "rolled_back":
708
+ return { text: "undone", needs: false };
709
+ case "rolling_back":
710
+ return { text: "undoing", needs: false };
711
+ case "denied":
712
+ return { text: "refused", needs: false };
713
+ case "failed":
714
+ return { text: "failed", needs: false };
715
+ case "approved":
716
+ return { text: "approved, not yet sent", needs: true };
717
+ case "pending":
718
+ return { text: "sent, outcome unknown", needs: true };
719
+ case "unrecoverable":
720
+ return action.inverse === void 0 ? { text: "cannot be undone", needs: true } : { text: "changed since; not safe to undo", needs: true };
721
+ default:
722
+ return { text: action.status, needs: false };
723
+ }
724
+ }
725
+ function size(bytes) {
726
+ if (bytes < 1024) {
727
+ return `${String(bytes)} B`;
728
+ }
729
+ if (bytes < 1024 * 1024) {
730
+ return `${(bytes / 1024).toFixed(1)} kB`;
731
+ }
732
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
733
+ }
734
+ function summariseArgs(args, limit = 60) {
735
+ if (!isRecord(args)) {
736
+ return args === void 0 ? "" : JSON.stringify(args);
737
+ }
738
+ const parts = [];
739
+ for (const [key, value] of Object.entries(args)) {
740
+ if (typeof value === "string") {
741
+ parts.push(value.length > 48 ? `${key} ${size(Buffer.byteLength(value))}` : `${key} ${value}`);
742
+ continue;
743
+ }
744
+ if (Array.isArray(value)) {
745
+ parts.push(`${key} ${String(value.length)} items`);
746
+ continue;
747
+ }
748
+ if (value === null || typeof value !== "object") {
749
+ parts.push(`${key} ${String(value)}`);
750
+ continue;
751
+ }
752
+ parts.push(`${key} ${size(Buffer.byteLength(JSON.stringify(value)))}`);
753
+ }
754
+ const line2 = parts.join(" ");
755
+ return line2.length <= limit ? line2 : `${line2.slice(0, limit - 1)}\u2026`;
756
+ }
757
+
594
758
  // src/watch.ts
595
759
  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
760
  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}`;
761
+ function line(action, now) {
762
+ const when = shortTime(action.ts, now);
763
+ const where = action.server.padEnd(10);
764
+ const what = action.tool.padEnd(20);
765
+ const on = subject(action.args);
766
+ const state = plainly(action);
767
+ const said = state.needs || wasRefused(action) ? style.accent(state.text) : style.quiet(state.text);
768
+ return ` ${style.quiet(when)} ${style.quiet(where)} ${style.strong(what)} ${style.quiet(on.padEnd(20))} ${said}`;
611
769
  }
612
770
  function waitingForJournal(options, tick) {
613
771
  const spinner = options.live ? `${style.accent(FRAMES[tick % FRAMES.length] ?? "")} ` : "";
@@ -625,6 +783,7 @@ function waitingForJournal(options, tick) {
625
783
  function render(journal, options, tick, view) {
626
784
  const runs = journal.listRuns();
627
785
  const recent = journal.recentActions(12);
786
+ const now = /* @__PURE__ */ new Date();
628
787
  const waiting = journal.listGated();
629
788
  const active = runs.filter((run) => run.status === "active").length;
630
789
  const out2 = [];
@@ -641,7 +800,7 @@ function render(journal, options, tick, view) {
641
800
  out2.push(` ${style.quiet("No agent has done anything through this journal yet.")}`);
642
801
  } else {
643
802
  for (const action of recent) {
644
- out2.push(line(action));
803
+ out2.push(line(action, now));
645
804
  }
646
805
  }
647
806
  if (waiting.length > 0) {
@@ -789,7 +948,7 @@ async function watch(options) {
789
948
  if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
790
949
  break;
791
950
  }
792
- await new Promise((resolve2) => setTimeout(resolve2, interval));
951
+ await new Promise((resolve4) => setTimeout(resolve4, interval));
793
952
  }
794
953
  return 0;
795
954
  } finally {
@@ -804,14 +963,675 @@ async function watch(options) {
804
963
  await reader?.return?.(void 0);
805
964
  await reading;
806
965
  })(),
807
- new Promise((resolve2) => setTimeout(resolve2, 50).unref())
966
+ new Promise((resolve4) => setTimeout(resolve4, 50).unref())
808
967
  ]);
809
968
  journal?.close();
810
969
  }
811
970
  }
812
971
 
813
972
  // src/console.ts
814
- import { existsSync as existsSync3 } from "fs";
973
+ import { existsSync as existsSync6 } from "fs";
974
+
975
+ // src/install/connections.ts
976
+ import { existsSync as existsSync5 } from "fs";
977
+
978
+ // src/install/clients.ts
979
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
980
+ import { homedir, platform } from "os";
981
+ import { basename as basename2, dirname, join, resolve } from "path";
982
+
983
+ // src/install/toml.ts
984
+ var HEADER = /^\s*\[(?!\[)([^[\]]+)\]\s*$/;
985
+ function serverTables(lines) {
986
+ const tables = [];
987
+ let open;
988
+ const close = (at) => {
989
+ if (open !== void 0) {
990
+ tables.push({ name: open.name, start: open.start, end: at });
991
+ open = void 0;
992
+ }
993
+ };
994
+ lines.forEach((line2, index) => {
995
+ const header2 = HEADER.exec(line2)?.[1];
996
+ if (header2 === void 0) {
997
+ return;
998
+ }
999
+ const parts = header2.split(".");
1000
+ if (parts[0] === "mcp_servers" && parts.length === 2 && parts[1] !== void 0) {
1001
+ close(index);
1002
+ open = { name: unquote(parts[1]), start: index };
1003
+ return;
1004
+ }
1005
+ close(index);
1006
+ });
1007
+ close(lines.length);
1008
+ return tables;
1009
+ }
1010
+ function unquote(text) {
1011
+ const trimmed = text.trim();
1012
+ if (/^'.*'$/s.test(trimmed)) {
1013
+ return trimmed.slice(1, -1);
1014
+ }
1015
+ if (!/^".*"$/s.test(trimmed)) {
1016
+ return trimmed;
1017
+ }
1018
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
1019
+ }
1020
+ function readKey(lines, table, key) {
1021
+ const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`);
1022
+ for (let index = table.start + 1; index < table.end; index += 1) {
1023
+ const line2 = lines[index];
1024
+ if (line2 === void 0 || HEADER.test(line2)) {
1025
+ break;
1026
+ }
1027
+ const value = pattern.exec(line2)?.[1];
1028
+ if (value !== void 0) {
1029
+ return value.trim();
1030
+ }
1031
+ }
1032
+ return void 0;
1033
+ }
1034
+ function splitItems(inner) {
1035
+ const items = [];
1036
+ let current = "";
1037
+ let quote3;
1038
+ let escaped = false;
1039
+ for (const character of inner) {
1040
+ if (escaped) {
1041
+ current += character;
1042
+ escaped = false;
1043
+ continue;
1044
+ }
1045
+ if (character === "\\" && quote3 === '"') {
1046
+ current += character;
1047
+ escaped = true;
1048
+ continue;
1049
+ }
1050
+ if (quote3 === void 0 && (character === '"' || character === "'")) {
1051
+ quote3 = character;
1052
+ current += character;
1053
+ continue;
1054
+ }
1055
+ if (character === quote3) {
1056
+ quote3 = void 0;
1057
+ current += character;
1058
+ continue;
1059
+ }
1060
+ if (character === "," && quote3 === void 0) {
1061
+ items.push(current);
1062
+ current = "";
1063
+ continue;
1064
+ }
1065
+ current += character;
1066
+ }
1067
+ items.push(current);
1068
+ return items;
1069
+ }
1070
+ function parseArray(value) {
1071
+ if (value === void 0 || !value.startsWith("[")) {
1072
+ return void 0;
1073
+ }
1074
+ if (!value.endsWith("]")) {
1075
+ return void 0;
1076
+ }
1077
+ const inner = value.slice(1, -1).trim();
1078
+ if (inner === "") {
1079
+ return [];
1080
+ }
1081
+ return splitItems(inner).map((item) => item.trim()).filter((item, index, all) => item !== "" || index !== all.length - 1).map(unquote);
1082
+ }
1083
+ function readServers(text) {
1084
+ const lines = text.split("\n");
1085
+ const servers = {};
1086
+ for (const table of serverTables(lines)) {
1087
+ const command = readKey(lines, table, "command");
1088
+ const entry = {};
1089
+ if (command !== void 0) {
1090
+ entry["command"] = unquote(command);
1091
+ }
1092
+ const args = parseArray(readKey(lines, table, "args"));
1093
+ if (args !== void 0) {
1094
+ entry["args"] = args;
1095
+ }
1096
+ const url = readKey(lines, table, "url");
1097
+ if (url !== void 0) {
1098
+ entry["url"] = unquote(url);
1099
+ }
1100
+ const enabled = readKey(lines, table, "enabled");
1101
+ if (enabled !== void 0) {
1102
+ entry["enabled"] = enabled.trim() === "true";
1103
+ }
1104
+ servers[table.name] = entry;
1105
+ }
1106
+ return servers;
1107
+ }
1108
+ var quote2 = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1109
+ function writeServers(text, servers) {
1110
+ const lines = text.split("\n");
1111
+ const current = readServers(text);
1112
+ for (const table of serverTables(lines).reverse()) {
1113
+ const wanted = servers[table.name];
1114
+ if (wanted === void 0 || wanted.command === void 0) {
1115
+ continue;
1116
+ }
1117
+ const now = current[table.name];
1118
+ if (now?.command === wanted.command && JSON.stringify(now.args ?? []) === JSON.stringify(wanted.args ?? [])) {
1119
+ continue;
1120
+ }
1121
+ setKey(lines, table, "command", quote2(wanted.command));
1122
+ setKey(lines, table, "args", `[${(wanted.args ?? []).map(quote2).join(", ")}]`);
1123
+ }
1124
+ return lines.join("\n");
1125
+ }
1126
+ function setKey(lines, table, key, value) {
1127
+ const pattern = new RegExp(`^(\\s*)${key}\\s*=`);
1128
+ for (let index = table.start + 1; index < table.end; index += 1) {
1129
+ const line2 = lines[index];
1130
+ if (line2 === void 0 || HEADER.test(line2)) {
1131
+ break;
1132
+ }
1133
+ const indent = pattern.exec(line2)?.[1];
1134
+ if (indent !== void 0) {
1135
+ lines[index] = `${indent}${key} = ${value}`;
1136
+ return;
1137
+ }
1138
+ }
1139
+ lines.splice(table.start + 1, 0, `${key} = ${value}`);
1140
+ }
1141
+
1142
+ // src/install/clients.ts
1143
+ var LABELS = {
1144
+ "claude-code": "Claude Code",
1145
+ "claude-desktop": "Claude Desktop",
1146
+ cursor: "Cursor",
1147
+ codex: "Codex"
1148
+ };
1149
+ function claudeDesktopPath() {
1150
+ const home = homedir();
1151
+ switch (platform()) {
1152
+ case "darwin":
1153
+ return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1154
+ case "win32":
1155
+ return join(process.env["APPDATA"] ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1156
+ default:
1157
+ return join(process.env["XDG_CONFIG_HOME"] ?? join(home, ".config"), "Claude", "claude_desktop_config.json");
1158
+ }
1159
+ }
1160
+ function discover(cwd) {
1161
+ const home = homedir();
1162
+ const sites = [];
1163
+ const claudeCode = join(home, ".claude.json");
1164
+ if (existsSync3(claudeCode)) {
1165
+ const document = readJson(claudeCode);
1166
+ const projects = document?.["projects"];
1167
+ const here = resolve(cwd);
1168
+ if (isRecord2(projects) && Object.prototype.hasOwnProperty.call(projects, here)) {
1169
+ sites.push({
1170
+ client: "claude-code",
1171
+ label: LABELS["claude-code"],
1172
+ format: "json",
1173
+ path: claudeCode,
1174
+ scope: `project ${here}`,
1175
+ at: ["projects", here, "mcpServers"]
1176
+ });
1177
+ }
1178
+ sites.push({
1179
+ client: "claude-code",
1180
+ label: LABELS["claude-code"],
1181
+ format: "json",
1182
+ path: claudeCode,
1183
+ scope: "global",
1184
+ at: ["mcpServers"]
1185
+ });
1186
+ }
1187
+ const projectFile = join(resolve(cwd), ".mcp.json");
1188
+ if (existsSync3(projectFile)) {
1189
+ sites.push({
1190
+ client: "claude-code",
1191
+ label: LABELS["claude-code"],
1192
+ format: "json",
1193
+ path: projectFile,
1194
+ scope: "project file",
1195
+ at: ["mcpServers"]
1196
+ });
1197
+ }
1198
+ const desktop = claudeDesktopPath();
1199
+ if (existsSync3(desktop)) {
1200
+ sites.push({
1201
+ client: "claude-desktop",
1202
+ label: LABELS["claude-desktop"],
1203
+ format: "json",
1204
+ path: desktop,
1205
+ scope: "global",
1206
+ at: ["mcpServers"]
1207
+ });
1208
+ }
1209
+ const codex = join(process.env["CODEX_HOME"] ?? join(home, ".codex"), "config.toml");
1210
+ if (existsSync3(codex)) {
1211
+ sites.push({
1212
+ client: "codex",
1213
+ label: LABELS.codex,
1214
+ format: "toml",
1215
+ path: codex,
1216
+ scope: "global",
1217
+ at: ["mcp_servers"]
1218
+ });
1219
+ }
1220
+ for (const [path, scope] of [
1221
+ [join(resolve(cwd), ".cursor", "mcp.json"), "project"],
1222
+ [join(home, ".cursor", "mcp.json"), "global"]
1223
+ ]) {
1224
+ if (existsSync3(path)) {
1225
+ sites.push({ client: "cursor", label: LABELS.cursor, format: "json", path, scope, at: ["mcpServers"] });
1226
+ }
1227
+ }
1228
+ return sites;
1229
+ }
1230
+ function isRecord2(value) {
1231
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1232
+ }
1233
+ function readJson(path) {
1234
+ try {
1235
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1236
+ return isRecord2(parsed) ? parsed : void 0;
1237
+ } catch {
1238
+ return void 0;
1239
+ }
1240
+ }
1241
+ var ConfigError = class extends Error {
1242
+ };
1243
+ function readDocument(site) {
1244
+ let text;
1245
+ try {
1246
+ text = readFileSync2(site.path, "utf8");
1247
+ } catch (error) {
1248
+ throw new ConfigError(`cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`);
1249
+ }
1250
+ let parsed;
1251
+ try {
1252
+ parsed = JSON.parse(text);
1253
+ } catch (error) {
1254
+ throw new ConfigError(
1255
+ `${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.`
1256
+ );
1257
+ }
1258
+ if (!isRecord2(parsed)) {
1259
+ throw new ConfigError(`${site.path} is not a JSON object, so it has no server list to change`);
1260
+ }
1261
+ return parsed;
1262
+ }
1263
+ function readServers2(document, at) {
1264
+ let node = document;
1265
+ for (const key of at) {
1266
+ if (!isRecord2(node)) {
1267
+ return {};
1268
+ }
1269
+ node = node[key];
1270
+ }
1271
+ if (!isRecord2(node)) {
1272
+ return {};
1273
+ }
1274
+ const servers = {};
1275
+ for (const [name, entry] of Object.entries(node)) {
1276
+ if (isRecord2(entry)) {
1277
+ servers[name] = entry;
1278
+ }
1279
+ }
1280
+ return servers;
1281
+ }
1282
+ function withServers(document, at, servers) {
1283
+ const head = at[0];
1284
+ if (head === void 0) {
1285
+ throw new ConfigError("no path to the server list");
1286
+ }
1287
+ const rest = at.slice(1);
1288
+ const below = document[head];
1289
+ const child = rest.length === 0 ? servers : withServers(isRecord2(below) ? below : {}, rest, servers);
1290
+ return { ...document, [head]: child };
1291
+ }
1292
+ function indentOf(path) {
1293
+ try {
1294
+ const line2 = /\n([ \t]+)"/.exec(readFileSync2(path, "utf8"));
1295
+ const found = line2?.[1];
1296
+ if (found === void 0) {
1297
+ return 2;
1298
+ }
1299
+ return found.startsWith(" ") ? " " : found.length;
1300
+ } catch {
1301
+ return 2;
1302
+ }
1303
+ }
1304
+ function backupPathFor(path) {
1305
+ return `${path}.synartesis-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1306
+ }
1307
+ var KEEP_BACKUPS = 5;
1308
+ function pruneBackups(path) {
1309
+ try {
1310
+ const dir = dirname(path);
1311
+ const prefix = `${basename2(path)}.synartesis-backup-`;
1312
+ const ours = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
1313
+ for (const name of ours.slice(0, Math.max(0, ours.length - KEEP_BACKUPS))) {
1314
+ rmSync(join(dir, name), { force: true });
1315
+ }
1316
+ } catch {
1317
+ }
1318
+ }
1319
+ function writeDocument(site, document) {
1320
+ return writeText(site, `${JSON.stringify(document, void 0, indentOf(site.path))}
1321
+ `);
1322
+ }
1323
+ function writeText(site, text) {
1324
+ const backup = backupPathFor(site.path);
1325
+ const original = readFileSync2(site.path);
1326
+ writeFileSync(backup, original);
1327
+ pruneBackups(site.path);
1328
+ const temporary = join(dirname(site.path), `.synartesis-write-${String(process.pid)}.tmp`);
1329
+ try {
1330
+ writeFileSync(temporary, text);
1331
+ renameSync(temporary, site.path);
1332
+ } catch (error) {
1333
+ try {
1334
+ unlinkSync(temporary);
1335
+ } catch {
1336
+ }
1337
+ throw new ConfigError(
1338
+ `could not write ${site.path}: ${error instanceof Error ? error.message : String(error)}. The original is untouched, and a copy is at ${backup}.`
1339
+ );
1340
+ }
1341
+ return backup;
1342
+ }
1343
+ function serversAt(site) {
1344
+ if (site.format === "toml") {
1345
+ try {
1346
+ return readServers(readFileSync2(site.path, "utf8"));
1347
+ } catch (error) {
1348
+ throw new ConfigError(
1349
+ `cannot read ${site.path}: ${error instanceof Error ? error.message : String(error)}`
1350
+ );
1351
+ }
1352
+ }
1353
+ return readServers2(readDocument(site), site.at);
1354
+ }
1355
+ function saveServers(site, servers) {
1356
+ if (site.format === "toml") {
1357
+ const text = readFileSync2(site.path, "utf8");
1358
+ return writeText(site, writeServers(text, servers));
1359
+ }
1360
+ return writeDocument(site, withServers(readDocument(site), site.at, servers));
1361
+ }
1362
+
1363
+ // src/install/install.ts
1364
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1365
+ import { dirname as dirname2, resolve as resolve2 } from "path";
1366
+ function recordPathFor(manifestPath) {
1367
+ return resolve2(dirname2(manifestPath), "installed.json");
1368
+ }
1369
+ var EMPTY = { version: 1, wrapped: {} };
1370
+ function isRecord3(value) {
1371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1372
+ }
1373
+ function asRecord(value) {
1374
+ if (!isRecord3(value)) {
1375
+ return void 0;
1376
+ }
1377
+ const wrapped2 = value["wrapped"];
1378
+ if (!isRecord3(wrapped2)) {
1379
+ return void 0;
1380
+ }
1381
+ const kept = {};
1382
+ for (const [key, entry] of Object.entries(wrapped2)) {
1383
+ if (!isRecord3(entry)) {
1384
+ continue;
1385
+ }
1386
+ const original = entry["original"];
1387
+ const at = entry["at"];
1388
+ if (!isRecord3(original)) {
1389
+ continue;
1390
+ }
1391
+ if (!Array.isArray(at) || !at.every((step) => typeof step === "string")) {
1392
+ continue;
1393
+ }
1394
+ kept[key] = { original, at };
1395
+ }
1396
+ return { version: 1, wrapped: kept };
1397
+ }
1398
+ function keyFor(site, server) {
1399
+ return [site.path, site.scope, server].join("");
1400
+ }
1401
+ function readRecord(manifestPath) {
1402
+ const path = recordPathFor(manifestPath);
1403
+ if (!existsSync4(path)) {
1404
+ return EMPTY;
1405
+ }
1406
+ try {
1407
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1408
+ const record = asRecord(parsed);
1409
+ if (record !== void 0) {
1410
+ return record;
1411
+ }
1412
+ } catch {
1413
+ }
1414
+ return EMPTY;
1415
+ }
1416
+ function writeRecord(manifestPath, record) {
1417
+ mkdirSync(dirname2(recordPathFor(manifestPath)), { recursive: true, mode: 448 });
1418
+ writeFileSync2(recordPathFor(manifestPath), `${JSON.stringify(record, void 0, 2)}
1419
+ `);
1420
+ }
1421
+ function proxyEntry(manifestPath, server, original, invoker) {
1422
+ const command = { command: invoker.command, args: [...invoker.args] };
1423
+ return {
1424
+ ...command,
1425
+ args: [...command.args, "--manifest", resolve2(manifestPath), "--server", server],
1426
+ // The agent's environment, not ours: the upstream is started by the proxy
1427
+ // from the manifest, but a client that set `env` here meant it for the
1428
+ // server, and the manifest reads `${VAR}` out of exactly this environment.
1429
+ ...original.env === void 0 ? {} : { env: original.env },
1430
+ ...original.cwd === void 0 ? {} : { cwd: original.cwd }
1431
+ };
1432
+ }
1433
+ function isWrapped(entry) {
1434
+ const args = entry.args ?? [];
1435
+ 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")));
1436
+ }
1437
+ function invokerFor(ourVersion, cliPath) {
1438
+ if (pathBinaryMatches(ourVersion)) {
1439
+ return { command: "synartesis", args: ["proxy"] };
1440
+ }
1441
+ return {
1442
+ command: process.execPath,
1443
+ args: [cliPath, "proxy"],
1444
+ note: "the synartesis on your PATH is a different build, so the entries name this one directly"
1445
+ };
1446
+ }
1447
+ async function planInstall(sites, manifestPath, invoker) {
1448
+ let yaml = existsSync4(manifestPath) ? readFileSync3(manifestPath, "utf8") : void 0;
1449
+ const plans = [];
1450
+ const claimed = new Set(
1451
+ yaml === void 0 ? [] : Object.keys(parseManifest(yaml, manifestPath).servers)
1452
+ );
1453
+ for (const site of sites) {
1454
+ const servers = serversAt(site);
1455
+ const planned = [];
1456
+ const skipped = [];
1457
+ for (const [name, entry] of Object.entries(servers)) {
1458
+ if (isWrapped(entry)) {
1459
+ skipped.push({ name, why: "already covered" });
1460
+ continue;
1461
+ }
1462
+ if (entry.command === void 0) {
1463
+ skipped.push({ name, why: entry.url === void 0 ? "no command to start" : "remote (http); stdio only today" });
1464
+ continue;
1465
+ }
1466
+ if (entry.enabled === false) {
1467
+ skipped.push({ name, why: "switched off in the config" });
1468
+ continue;
1469
+ }
1470
+ const key = claimed.has(name) ? `${name}-${site.client}` : name;
1471
+ if (claimed.has(key)) {
1472
+ skipped.push({ name, why: `already in the policy as ${key}` });
1473
+ continue;
1474
+ }
1475
+ let draft;
1476
+ try {
1477
+ draft = await draftManifest({
1478
+ name: key,
1479
+ command: entry.command,
1480
+ args: [...entry.args ?? []],
1481
+ ...yaml === void 0 ? {} : { existing: yaml }
1482
+ });
1483
+ } catch (error) {
1484
+ skipped.push({
1485
+ name,
1486
+ why: `will not start: ${(error instanceof Error ? error.message : String(error)).slice(0, 60)}`
1487
+ });
1488
+ continue;
1489
+ }
1490
+ yaml = draft.yaml;
1491
+ claimed.add(key);
1492
+ planned.push({
1493
+ name,
1494
+ original: entry,
1495
+ wrapped: proxyEntry(manifestPath, key, entry, invoker),
1496
+ ...draft.adopted === void 0 ? {} : { adopted: draft.adopted.server, tools: draft.adopted.tools }
1497
+ });
1498
+ }
1499
+ plans.push({ site, servers: planned, skipped });
1500
+ }
1501
+ return { plans, yaml: yaml ?? "" };
1502
+ }
1503
+ function applyInstall(plans, manifestPath, yaml) {
1504
+ if (!plans.some((plan) => plan.servers.length > 0)) {
1505
+ return [];
1506
+ }
1507
+ parseManifest(yaml, manifestPath);
1508
+ mkdirSync(dirname2(resolve2(manifestPath)), { recursive: true, mode: 448 });
1509
+ writeFileSync2(manifestPath, yaml);
1510
+ const record = readRecord(manifestPath);
1511
+ const wrapped2 = { ...record.wrapped };
1512
+ const applied = [];
1513
+ for (const plan of plans) {
1514
+ if (plan.servers.length === 0) {
1515
+ continue;
1516
+ }
1517
+ const servers = { ...serversAt(plan.site) };
1518
+ for (const server of plan.servers) {
1519
+ servers[server.name] = server.wrapped;
1520
+ wrapped2[keyFor(plan.site, server.name)] = { original: server.original, at: plan.site.at };
1521
+ }
1522
+ writeRecord(manifestPath, { version: 1, wrapped: wrapped2 });
1523
+ const backup = saveServers(plan.site, servers);
1524
+ applied.push({ site: plan.site, backup, servers: plan.servers.map((server) => server.name) });
1525
+ }
1526
+ return applied;
1527
+ }
1528
+ function applyUninstall(sites, manifestPath) {
1529
+ const record = readRecord(manifestPath);
1530
+ const restoredKeys = /* @__PURE__ */ new Set();
1531
+ const restored = [];
1532
+ for (const site of sites) {
1533
+ const servers = { ...serversAt(site) };
1534
+ const put = [];
1535
+ const unknown = [];
1536
+ for (const [name, entry] of Object.entries(servers)) {
1537
+ if (!isWrapped(entry)) {
1538
+ continue;
1539
+ }
1540
+ const known = record.wrapped[keyFor(site, name)];
1541
+ if (known === void 0) {
1542
+ unknown.push(name);
1543
+ continue;
1544
+ }
1545
+ servers[name] = known.original;
1546
+ put.push(name);
1547
+ restoredKeys.add(keyFor(site, name));
1548
+ }
1549
+ if (put.length === 0 && unknown.length === 0) {
1550
+ continue;
1551
+ }
1552
+ const backup = put.length === 0 ? "" : saveServers(site, servers);
1553
+ restored.push({ site, backup, servers: put, unknown });
1554
+ }
1555
+ const remaining = Object.fromEntries(
1556
+ Object.entries(record.wrapped).filter(([key]) => !restoredKeys.has(key))
1557
+ );
1558
+ writeRecord(manifestPath, { version: 1, wrapped: remaining });
1559
+ return restored;
1560
+ }
1561
+
1562
+ // src/install/connections.ts
1563
+ var ACTIVE_WITHIN_MS = 2 * 60 * 1e3;
1564
+ function lastSeenByServer(journal) {
1565
+ const seen = /* @__PURE__ */ new Map();
1566
+ for (const action of journal.recentActions(500)) {
1567
+ const known = seen.get(action.server);
1568
+ if (known === void 0 || action.ts > known) {
1569
+ seen.set(action.server, action.ts);
1570
+ }
1571
+ }
1572
+ return seen;
1573
+ }
1574
+ function commandMissing(command) {
1575
+ if (command === void 0) {
1576
+ return false;
1577
+ }
1578
+ return (command.includes("/") || command.includes("\\")) && !existsSync5(command);
1579
+ }
1580
+ function scan(journal, cwd) {
1581
+ const seen = journal === void 0 ? /* @__PURE__ */ new Map() : lastSeenByServer(journal);
1582
+ const groups = [];
1583
+ for (const site of discover(cwd)) {
1584
+ let servers;
1585
+ try {
1586
+ servers = serversAt(site);
1587
+ } catch (error) {
1588
+ groups.push({
1589
+ label: site.label,
1590
+ scope: site.scope,
1591
+ path: site.path,
1592
+ connections: [],
1593
+ problem: error instanceof Error ? error.message : String(error)
1594
+ });
1595
+ continue;
1596
+ }
1597
+ const connections = Object.entries(servers).map(([server, entry]) => {
1598
+ const covered = isWrapped(entry);
1599
+ const lastSeen = seen.get(server);
1600
+ return {
1601
+ client: site.client,
1602
+ scope: site.scope,
1603
+ path: site.path,
1604
+ server,
1605
+ covered,
1606
+ missing: commandMissing(entry.command),
1607
+ ...lastSeen === void 0 ? {} : { lastSeen },
1608
+ site
1609
+ };
1610
+ });
1611
+ groups.push({ label: site.label, scope: site.scope, path: site.path, connections });
1612
+ }
1613
+ return groups;
1614
+ }
1615
+ function stateOf(connection, now = /* @__PURE__ */ new Date()) {
1616
+ if (connection.missing) {
1617
+ return "cannot start; the command is not there";
1618
+ }
1619
+ if (!connection.covered) {
1620
+ return "not covered";
1621
+ }
1622
+ if (connection.lastSeen === void 0) {
1623
+ return "covered, nothing through it yet";
1624
+ }
1625
+ const since = now.getTime() - new Date(connection.lastSeen).getTime();
1626
+ return since <= ACTIVE_WITHIN_MS ? "covered, active now" : `covered, last used ${ago(connection.lastSeen, now)}`;
1627
+ }
1628
+ function needsConnecting(groups) {
1629
+ return groups.flatMap(
1630
+ (group) => group.connections.filter((connection) => !connection.covered && !connection.missing)
1631
+ );
1632
+ }
1633
+
1634
+ // src/console.ts
815
1635
  var FRAMES2 = [
816
1636
  "\u280B",
817
1637
  "\u2819",
@@ -824,17 +1644,11 @@ var FRAMES2 = [
824
1644
  "\u2807",
825
1645
  "\u280F"
826
1646
  ];
827
- var MARK2 = {
828
- readonly: "\xB7",
829
- reversible: "\u2190",
830
- compensable: "\u2248",
831
- irreversible: "!",
832
- unclassified: "?"
833
- };
834
1647
  var CURSOR = "\u276F";
835
1648
  var DOT = "\xB7";
836
1649
  var ESC = "\x1B";
837
1650
  var NOTICE_TICKS2 = 26;
1651
+ var READING_TICKS = 200;
838
1652
  function roomFor(options) {
839
1653
  const rows = options.rows ?? rowsOf(process.stdout.rows) ?? 24;
840
1654
  return Math.max(3, rows - 11);
@@ -853,6 +1667,9 @@ function windowed(lines, at, room) {
853
1667
  ...below === 0 ? [] : [` ${style.quiet(`${String(below)} more below`)}`]
854
1668
  ];
855
1669
  }
1670
+ function firstLine(text) {
1671
+ return text.split("\n")[0]?.trim() ?? "";
1672
+ }
856
1673
  function truncate2(text, limit) {
857
1674
  return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
858
1675
  }
@@ -876,6 +1693,8 @@ function modeLabel(screen) {
876
1693
  return "one run, in the order it happened";
877
1694
  case "gates":
878
1695
  return "held until a person decides";
1696
+ case "connections":
1697
+ return "every AI on this machine, and whether it goes through Synartesis";
879
1698
  }
880
1699
  }
881
1700
  function header(options, screen, tick) {
@@ -905,15 +1724,35 @@ function runsView(journal, screen, options) {
905
1724
  const here = index === at && canPress(options);
906
1725
  const name = (run.label ?? "an agent").padEnd(24);
907
1726
  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}`;
1727
+ 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
1728
  });
910
1729
  }
911
- function statusOf(action) {
912
- const text = labelFor(action).padEnd(13);
913
- if (wasRefused(action)) {
914
- return style.accent(text);
1730
+ function standing(actions) {
1731
+ let undoable = 0;
1732
+ let conflicted = 0;
1733
+ for (const action of actions) {
1734
+ if (action.inverse === void 0) {
1735
+ continue;
1736
+ }
1737
+ if (action.status === "applied") {
1738
+ undoable += 1;
1739
+ } else if (action.status === "unrecoverable") {
1740
+ conflicted += 1;
1741
+ }
1742
+ }
1743
+ return { undoable, conflicted };
1744
+ }
1745
+ function elsewhere(journal, exceptId) {
1746
+ for (const run of [...journal.listRuns()].reverse()) {
1747
+ if (run.id === exceptId) {
1748
+ continue;
1749
+ }
1750
+ const found = standing(journal.getActions(run.id));
1751
+ if (found.undoable > 0 || found.conflicted > 0) {
1752
+ return run;
1753
+ }
915
1754
  }
916
- return action.status === "gated" ? style.strong(text) : style.quiet(text);
1755
+ return void 0;
917
1756
  }
918
1757
  function runView(journal, screen) {
919
1758
  const runId = screen.openRun;
@@ -923,22 +1762,86 @@ function runView(journal, screen) {
923
1762
  const run = journal.getRun(runId);
924
1763
  const actions = journal.getActions(runId);
925
1764
  const out2 = [
926
- ` ${style.label("run")} ${style.strong(run?.label ?? "an agent")} ${style.quiet(runId.slice(0, 8))}`,
1765
+ // "RUN claude 5dce5bda" was read as an instruction to go and run
1766
+ // something: a spaced capital heading followed by two words looks exactly
1767
+ // like a command with two arguments. "Session" is only ever a noun.
1768
+ ` ${style.label("session")} ${style.strong(run?.label ?? "an agent")} ${style.quiet(runId.slice(0, 8))}`,
927
1769
  ""
928
1770
  ];
929
1771
  if (actions.length === 0) {
930
- out2.push(` ${style.quiet("nothing was recorded in this run")}`);
1772
+ out2.push(` ${style.quiet("nothing was recorded in this run, so there is nothing here to undo")}`);
1773
+ const other = elsewhere(journal, runId);
1774
+ if (other !== void 0) {
1775
+ out2.push("");
1776
+ out2.push(
1777
+ ` ${style.quiet("the session that did something:")} ${style.strong(other.label ?? "an agent")} ${style.accent(other.id.slice(0, 8))}`
1778
+ );
1779
+ out2.push(
1780
+ ` ${style.quiet("esc, then j/k onto it -- or run:")} ` + style.strong(`${cliCommand()} undo ${other.id.slice(0, 8)}`)
1781
+ );
1782
+ }
931
1783
  return out2;
932
1784
  }
933
1785
  for (const action of actions) {
934
- const badge = `${MARK2[action.class] ?? "?"} ${action.class}`.padEnd(14);
1786
+ const state = plainly(action);
1787
+ const said = state.needs || wasRefused(action) ? style.accent(state.text) : style.quiet(state.text);
935
1788
  out2.push(
936
- ` ${style.quiet(String(action.seq).padStart(3))} ${style.quiet(badge)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}`
1789
+ ` ${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
1790
  );
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)}`)}`);
1791
+ if (screen.expanded) {
1792
+ for (const line2 of JSON.stringify(action.args, void 0, 2).split("\n")) {
1793
+ out2.push(` ${style.quiet(line2)}`);
1794
+ }
1795
+ if (action.inverse !== void 0) {
1796
+ out2.push(` ${style.quiet("undo")}`);
1797
+ for (const line2 of JSON.stringify(action.inverse, void 0, 2).split("\n")) {
1798
+ out2.push(` ${style.quiet(line2)}`);
1799
+ }
1800
+ }
1801
+ } else {
1802
+ out2.push(` ${style.quiet(summariseArgs(action.args, 62))}`);
1803
+ }
1804
+ }
1805
+ return out2;
1806
+ }
1807
+ function connectionRows(screen) {
1808
+ return screen.groups.flatMap((group) => group.connections);
1809
+ }
1810
+ function connectionsView(screen, options) {
1811
+ if (screen.groups.length === 0) {
1812
+ return [
1813
+ ` ${style.quiet("No MCP client config was found on this machine.")}`,
1814
+ "",
1815
+ ` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`
1816
+ ];
1817
+ }
1818
+ const rows = connectionRows(screen);
1819
+ const at = Math.min(screen.cursor, Math.max(0, rows.length - 1));
1820
+ const now = /* @__PURE__ */ new Date();
1821
+ const out2 = [];
1822
+ let index = 0;
1823
+ for (const group of screen.groups) {
1824
+ out2.push(` ${style.strong(group.label)} ${style.quiet(group.scope)}`);
1825
+ if (group.problem !== void 0) {
1826
+ out2.push(` ${style.accent(group.problem)}`);
1827
+ out2.push("");
1828
+ continue;
1829
+ }
1830
+ if (group.connections.length === 0) {
1831
+ out2.push(` ${style.quiet("no servers listed")}`);
1832
+ out2.push("");
1833
+ continue;
1834
+ }
1835
+ for (const connection of group.connections) {
1836
+ const here = index === at && canPress(options);
1837
+ const state = stateOf(connection, now);
1838
+ const shown = connection.covered ? style.quiet(state) : style.accent(state);
1839
+ out2.push(
1840
+ ` ${here ? style.accent(CURSOR) : " "} ${here ? style.accent(connection.server.padEnd(20)) : style.strong(connection.server.padEnd(20))} ${shown}`
1841
+ );
1842
+ index += 1;
941
1843
  }
1844
+ out2.push("");
942
1845
  }
943
1846
  return out2;
944
1847
  }
@@ -961,12 +1864,10 @@ function footer(screen, options) {
961
1864
  return [];
962
1865
  }
963
1866
  if (screen.confirming !== void 0) {
964
- return [
965
- "",
966
- ` ${style.accent("undo this whole run?")} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`
967
- ];
1867
+ const what = screen.confirmingForce ? `undo ${screen.confirmingLabel ?? "this session"} anyway, losing that change?` : `undo ${screen.confirmingLabel ?? "this session"}?`;
1868
+ return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
968
1869
  }
969
- const keys = screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
1870
+ 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" ? [
970
1871
  keyHint2("p", "preview undo"),
971
1872
  keyHint2("u", "undo"),
972
1873
  keyHint2("esc", "back"),
@@ -1016,7 +1917,7 @@ async function* terminalKeys2() {
1016
1917
  async function openConsole(options) {
1017
1918
  let journal;
1018
1919
  const open = () => {
1019
- if (journal === void 0 && existsSync3(options.journalPath)) {
1920
+ if (journal === void 0 && existsSync6(options.journalPath)) {
1020
1921
  journal = openJournal(options.journalPath, { mustExist: true });
1021
1922
  }
1022
1923
  return journal;
@@ -1027,28 +1928,36 @@ async function openConsole(options) {
1027
1928
  cursor: 0,
1028
1929
  openRun: void 0,
1029
1930
  confirming: void 0,
1931
+ confirmingLabel: void 0,
1932
+ confirmingForce: false,
1933
+ warned: void 0,
1934
+ expanded: false,
1935
+ groups: [],
1030
1936
  busy: void 0,
1031
1937
  notice: "",
1032
1938
  noticeUntil: 0
1033
1939
  };
1034
1940
  let tick = 0;
1035
1941
  const stopped = () => screen.stop;
1036
- const say = (text) => {
1942
+ const say = (text, ticks = NOTICE_TICKS2) => {
1037
1943
  screen.notice = text;
1038
- screen.noticeUntil = tick + NOTICE_TICKS2;
1944
+ screen.noticeUntil = tick + ticks;
1039
1945
  };
1040
1946
  const frame = () => {
1041
1947
  const ready = open();
1042
1948
  if (ready === void 0) {
1043
1949
  return waitingForJournal2(options, tick);
1044
1950
  }
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)}`];
1951
+ const tail = command(ready);
1952
+ const shout = screen.notice === "" ? 0 : screen.notice.split("\n").length;
1953
+ const room = Math.max(3, roomFor(options) - tail.length - shout);
1954
+ 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);
1955
+ const notice = screen.notice === "" ? [] : ["", ...screen.notice.split("\n").map((line2) => ` ${style.accent(line2)}`)];
1048
1956
  return [
1049
1957
  ...header(options, screen, tick),
1050
1958
  ...body,
1051
1959
  ...notice,
1960
+ ...tail,
1052
1961
  ...footer(screen, options),
1053
1962
  ""
1054
1963
  ].join("\n");
@@ -1060,6 +1969,40 @@ async function openConsole(options) {
1060
1969
  const runs = [...ready.listRuns()].reverse();
1061
1970
  return runs[Math.min(screen.cursor, runs.length - 1)];
1062
1971
  };
1972
+ const nothingToUndo = (ready, run) => {
1973
+ const actions = ready.getActions(run.id);
1974
+ 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";
1975
+ const other = elsewhere(ready, run.id);
1976
+ return other === void 0 ? why : `${why} ${DOT} try ${other.label ?? "an agent"} ${other.id.slice(0, 8)}`;
1977
+ };
1978
+ const command = (ready) => {
1979
+ if (!canPress(options) || screen.confirming !== void 0) {
1980
+ return [];
1981
+ }
1982
+ if (screen.mode !== "runs" && screen.mode !== "run") {
1983
+ return [];
1984
+ }
1985
+ const run = selectedRun(ready);
1986
+ if (run === void 0) {
1987
+ return [];
1988
+ }
1989
+ const here = standing(ready.getActions(run.id));
1990
+ const id = run.id.slice(0, 8);
1991
+ if (here.undoable > 0) {
1992
+ return [
1993
+ "",
1994
+ ` ${style.quiet("u undoes it here")} ${style.quiet(DOT)} ${style.quiet("or from any terminal:")} ` + style.strong(`${cliCommand()} undo ${id}`)
1995
+ ];
1996
+ }
1997
+ if (here.conflicted > 0) {
1998
+ return [
1999
+ "",
2000
+ ` ${style.accent(`${String(here.conflicted)} changed since this ran`)} ${style.quiet(DOT)} ` + style.quiet("u shows what undoing would write over"),
2001
+ ` ${style.quiet("or from any terminal:")} ${style.strong(`${cliCommand()} undo ${id} --force`)}`
2002
+ ];
2003
+ }
2004
+ return ["", ` ${style.quiet("nothing to undo in this session")}`];
2005
+ };
1063
2006
  const decide = (approve) => {
1064
2007
  const ready = open();
1065
2008
  if (ready === void 0) {
@@ -1076,7 +2019,7 @@ async function openConsole(options) {
1076
2019
  );
1077
2020
  screen.cursor = 0;
1078
2021
  };
1079
- const perform = async (runId, dryRun) => {
2022
+ const perform = async (runId, dryRun, force = false) => {
1080
2023
  if (options.undo === void 0) {
1081
2024
  say("no way to undo was configured");
1082
2025
  return;
@@ -1087,11 +2030,18 @@ async function openConsole(options) {
1087
2030
  }
1088
2031
  screen.busy = dryRun ? "reading the current state..." : "putting it back...";
1089
2032
  try {
1090
- const report2 = await options.undo(runId, dryRun);
2033
+ const report2 = await options.undo(runId, dryRun, force);
1091
2034
  const reverted = report2.steps.filter((step) => step.kind === "revert").length;
1092
- const halted = report2.halted === void 0 ? "" : ` ${DOT} halted: ${report2.halted.reason}`;
2035
+ const why = report2.halted === void 0 ? "" : ` ${DOT} halted: ${firstLine(report2.halted.detail) || report2.halted.reason}`;
2036
+ const headline = dryRun ? `${String(reverted)} would be reverted ${DOT} nothing changed${why}` : `${report2.status} ${DOT} ${String(reverted)} reverted${why}`;
2037
+ const over = report2.halted?.overwrites ?? "";
2038
+ if (over === "") {
2039
+ say(headline);
2040
+ return;
2041
+ }
1093
2042
  say(
1094
- dryRun ? `${String(reverted)} would be reverted ${DOT} nothing changed${halted}` : `${report2.status} ${DOT} ${String(reverted)} reverted${halted}`
2043
+ ["changed since this ran, so nothing was written", "", "undoing anyway would write:", ...over.split("\n"), "", "u again to do it"].join("\n"),
2044
+ READING_TICKS
1095
2045
  );
1096
2046
  } catch (error) {
1097
2047
  say(error instanceof Error ? error.message : "the undo failed");
@@ -1099,12 +2049,41 @@ async function openConsole(options) {
1099
2049
  screen.busy = void 0;
1100
2050
  }
1101
2051
  };
2052
+ const rescan = () => {
2053
+ screen.groups = options.scan?.() ?? [];
2054
+ };
2055
+ const connect = async (targets) => {
2056
+ if (options.connect === void 0) {
2057
+ say("no way to connect was configured");
2058
+ return;
2059
+ }
2060
+ if (targets.length === 0) {
2061
+ say("everything here is already covered");
2062
+ return;
2063
+ }
2064
+ if (screen.busy !== void 0) {
2065
+ return;
2066
+ }
2067
+ screen.busy = `connecting ${String(targets.length)}${targets.length === 1 ? " server" : " servers"}`;
2068
+ try {
2069
+ say(await options.connect(targets));
2070
+ } catch (error) {
2071
+ say(error instanceof Error ? error.message : "connecting failed");
2072
+ } finally {
2073
+ screen.busy = void 0;
2074
+ rescan();
2075
+ }
2076
+ };
1102
2077
  const press = (key) => {
1103
2078
  if (screen.confirming !== void 0) {
1104
2079
  const runId = screen.confirming;
2080
+ const forcing = screen.confirmingForce;
1105
2081
  screen.confirming = void 0;
2082
+ screen.confirmingForce = false;
2083
+ screen.confirmingLabel = void 0;
1106
2084
  if (key === "y") {
1107
- void perform(runId, false);
2085
+ screen.warned = void 0;
2086
+ void perform(runId, false, forcing);
1108
2087
  } else {
1109
2088
  say("left alone");
1110
2089
  }
@@ -1127,7 +2106,23 @@ async function openConsole(options) {
1127
2106
  screen.mode = "gates";
1128
2107
  screen.cursor = 0;
1129
2108
  return;
2109
+ case "c":
2110
+ screen.mode = "connections";
2111
+ screen.cursor = 0;
2112
+ rescan();
2113
+ return;
2114
+ case "f":
2115
+ if (screen.mode === "run") {
2116
+ screen.expanded = !screen.expanded;
2117
+ say(screen.expanded ? "showing everything recorded" : "back to a summary");
2118
+ }
2119
+ return;
1130
2120
  case "r":
2121
+ if (screen.mode === "connections") {
2122
+ rescan();
2123
+ say("rescanned");
2124
+ return;
2125
+ }
1131
2126
  screen.mode = "runs";
1132
2127
  screen.cursor = 0;
1133
2128
  return;
@@ -1138,6 +2133,11 @@ async function openConsole(options) {
1138
2133
  return;
1139
2134
  case "\r":
1140
2135
  case "\n": {
2136
+ if (screen.mode === "connections") {
2137
+ const chosen = connectionRows(screen)[screen.cursor];
2138
+ void connect(chosen === void 0 ? [] : [chosen]);
2139
+ return;
2140
+ }
1141
2141
  const ready = open();
1142
2142
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1143
2143
  if (run !== void 0) {
@@ -1149,6 +2149,10 @@ async function openConsole(options) {
1149
2149
  case "a":
1150
2150
  if (screen.mode === "gates") {
1151
2151
  decide(true);
2152
+ return;
2153
+ }
2154
+ if (screen.mode === "connections") {
2155
+ void connect(needsConnecting(screen.groups));
1152
2156
  }
1153
2157
  return;
1154
2158
  case "d":
@@ -1159,9 +2163,15 @@ async function openConsole(options) {
1159
2163
  case "p": {
1160
2164
  const ready = open();
1161
2165
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1162
- if (run !== void 0) {
1163
- void perform(run.id, true);
2166
+ if (ready === void 0 || run === void 0) {
2167
+ return;
2168
+ }
2169
+ const here = standing(ready.getActions(run.id));
2170
+ if (here.undoable === 0 && here.conflicted === 0) {
2171
+ say(nothingToUndo(ready, run));
2172
+ return;
1164
2173
  }
2174
+ void perform(run.id, true);
1165
2175
  return;
1166
2176
  }
1167
2177
  case "u": {
@@ -1171,9 +2181,29 @@ async function openConsole(options) {
1171
2181
  }
1172
2182
  const ready = open();
1173
2183
  const run = ready === void 0 ? void 0 : selectedRun(ready);
1174
- if (run !== void 0) {
2184
+ if (ready === void 0 || run === void 0) {
2185
+ return;
2186
+ }
2187
+ const here = standing(ready.getActions(run.id));
2188
+ if (here.undoable === 0 && here.conflicted === 0) {
2189
+ say(nothingToUndo(ready, run));
2190
+ return;
2191
+ }
2192
+ const label = `${run.label ?? "an agent"} ${shortTime(run.startedAt).trim()}`;
2193
+ if (here.undoable === 0) {
2194
+ if (screen.warned !== run.id) {
2195
+ screen.warned = run.id;
2196
+ void perform(run.id, true);
2197
+ return;
2198
+ }
1175
2199
  screen.confirming = run.id;
2200
+ screen.confirmingForce = true;
2201
+ screen.confirmingLabel = label;
2202
+ return;
1176
2203
  }
2204
+ screen.confirming = run.id;
2205
+ screen.confirmingForce = false;
2206
+ screen.confirmingLabel = label;
1177
2207
  return;
1178
2208
  }
1179
2209
  default:
@@ -1216,7 +2246,7 @@ async function openConsole(options) {
1216
2246
  if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
1217
2247
  break;
1218
2248
  }
1219
- await new Promise((resolve2) => setTimeout(resolve2, interval));
2249
+ await new Promise((resolve4) => setTimeout(resolve4, interval));
1220
2250
  }
1221
2251
  return 0;
1222
2252
  } finally {
@@ -1232,7 +2262,7 @@ async function openConsole(options) {
1232
2262
  await reader?.return?.(void 0);
1233
2263
  await reading;
1234
2264
  })(),
1235
- new Promise((resolve2) => setTimeout(resolve2, 50).unref())
2265
+ new Promise((resolve4) => setTimeout(resolve4, 50).unref())
1236
2266
  ]);
1237
2267
  journal?.close();
1238
2268
  }
@@ -1248,24 +2278,36 @@ if (NODE_MAJOR < 22) {
1248
2278
  process.exit(2);
1249
2279
  }
1250
2280
  var COMMANDS = `
1251
- synartesis the screen; everything below,
1252
- driven with the arrow keys
2281
+ synartesis start here. Live activity,
2282
+ what is waiting for you, and
2283
+ undo -- all in one place, with
2284
+ the arrow keys. Everything
2285
+ below can be done from it.
2286
+ synartesis install [--client <name>] [--dry-run] [--print]
2287
+ synartesis uninstall [--client <name>]
2288
+ synartesis status
1253
2289
  synartesis init <server> -- <command> [args...] [--manifest <path>]
1254
2290
  synartesis check [--manifest <path>]
1255
2291
  synartesis list [--journal <path>]
1256
- synartesis show <runId> [--journal <path>]
2292
+ synartesis show <runId> [--full] [--journal <path>]
1257
2293
  synartesis gates [--journal <path>]
1258
2294
  synartesis close [runId] [--journal <path>]
1259
2295
  synartesis prune [--older-than <days>] [--dry-run] [--journal <path>]
1260
- synartesis proxy --manifest <path> [--journal <path>] what your agent runs
2296
+ synartesis proxy --manifest <path> [--server <name>] what your agent runs
2297
+ [--journal <path>]
1261
2298
  [--http <port> --token <secret>] for a client that
1262
2299
  cannot start one
1263
2300
  synartesis watch [--by <name>] [--journal <path>]
1264
2301
  synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
1265
2302
  synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
1266
- synartesis undo [runId] [--to <seq>] [--dry-run] [--replan]
2303
+ synartesis undo [runId] [--to <seq>] [--dry-run] [--replan] [--force [--yes]]
1267
2304
  [--manifest <path>] [--journal <path>]
1268
2305
 
2306
+ install is the short way in: it finds what Claude Code, Claude Desktop,
2307
+ Cursor or Codex already list, writes a policy covering all of it -- using the ones that
2308
+ ship where they fit -- and points each entry at the proxy. The original config
2309
+ is copied aside first, and uninstall puts it back. status says what is covered.
2310
+
1269
2311
  close ends a run left active by a proxy that was killed; nothing guesses at
1270
2312
  that, since several proxies can share one journal.
1271
2313
 
@@ -1280,6 +2322,14 @@ adds to an existing manifest rather than replacing it.
1280
2322
  watch is the one to leave running. Anything held for approval appears there,
1281
2323
  and a and d answer it without a second terminal or an id to copy.
1282
2324
 
2325
+ undo stops when somebody has changed the resource since, rather than writing
2326
+ over them. Three ways past that, and it prints all three: leave it, put the
2327
+ resource back as the run left it and --replan, or --force to overwrite.
2328
+
2329
+ --client claude-code, claude-desktop, cursor or codex; all by default
2330
+ --print show the entries install would write, and write nothing
2331
+ --full show every argument, snapshot and inverse in full, nothing elided
2332
+ --server serve one server from the manifest, keeping its tool names
1283
2333
  --manifest synartesis.yaml, looked for here and upwards, then in the home
1284
2334
  --journal beside the manifest, or the one in the home
1285
2335
  --to lowest sequence to undo; earlier actions are left alone
@@ -1290,6 +2340,8 @@ and a and d answer it without a second terminal or an id to copy.
1290
2340
  --dry-run read current state and print the plan without changing anything
1291
2341
  --replan rebuild each undo from the current manifest, for a run recorded
1292
2342
  under a policy that turned out to be wrong
2343
+ --force undo even where the resource changed after the run. On its own it
2344
+ prints the lines it would write over and stops; add --yes to do it
1293
2345
  --older-than days of history prune keeps; defaults to 30
1294
2346
  --version print the version and exit
1295
2347
 
@@ -1314,7 +2366,7 @@ function flag(argv, name) {
1314
2366
  return value;
1315
2367
  }
1316
2368
  function positional(argv) {
1317
- const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than"]);
2369
+ const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than", "--client"]);
1318
2370
  const values = [];
1319
2371
  const end = argv.indexOf("--");
1320
2372
  const ours = end === -1 ? argv : argv.slice(0, end);
@@ -1369,6 +2421,197 @@ async function runCheck(argv) {
1369
2421
  out("");
1370
2422
  return 0;
1371
2423
  }
2424
+ async function runInstall(argv) {
2425
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2426
+ const only = flag(argv, "--client");
2427
+ const dryRun = argv.includes("--dry-run");
2428
+ const printOnly = argv.includes("--print");
2429
+ const sites = discover(process.cwd()).filter(
2430
+ (site) => only === void 0 || site.client === only
2431
+ );
2432
+ if (sites.length === 0) {
2433
+ out("");
2434
+ out(` ${style.quiet("No MCP client config was found on this machine.")}`);
2435
+ out(` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`);
2436
+ out("");
2437
+ return 0;
2438
+ }
2439
+ const invoker = invokerFor(version(), fileURLToPath2(import.meta.url));
2440
+ const { plans, yaml } = await planInstall(sites, manifestPath, invoker);
2441
+ const total = plans.reduce((sum, plan) => sum + plan.servers.length, 0);
2442
+ out("");
2443
+ if (invoker.note !== void 0 && total > 0) {
2444
+ out(` ${style.accent("note")} ${style.quiet(invoker.note)}`);
2445
+ }
2446
+ out(` ${style.label(dryRun || printOnly ? "would cover" : "covering")} ${style.strong(manifestPath)}`);
2447
+ out(` ${rule(60)}`);
2448
+ for (const plan of plans) {
2449
+ out("");
2450
+ out(` ${style.strong(plan.site.label)} ${style.quiet(plan.site.scope)}`);
2451
+ out(` ${style.quiet(plan.site.path)}`);
2452
+ for (const server of plan.servers) {
2453
+ 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)`);
2454
+ out(` ${style.strong(server.name.padEnd(18))} ${note}`);
2455
+ }
2456
+ for (const skip of plan.skipped) {
2457
+ out(` ${style.quiet(skip.name.padEnd(18))} ${style.quiet(skip.why)}`);
2458
+ }
2459
+ if (plan.servers.length === 0 && plan.skipped.length === 0) {
2460
+ out(` ${style.quiet("no servers listed")}`);
2461
+ }
2462
+ }
2463
+ if (printOnly) {
2464
+ out("");
2465
+ out(` ${style.label("entries")}`);
2466
+ for (const plan of plans) {
2467
+ for (const server of plan.servers) {
2468
+ out(` ${JSON.stringify({ [server.name]: server.wrapped }, void 0, 2)}`);
2469
+ }
2470
+ }
2471
+ out("");
2472
+ return 0;
2473
+ }
2474
+ if (total === 0) {
2475
+ out("");
2476
+ out(` ${style.quiet("Nothing to do; everything found is already covered.")}`);
2477
+ out("");
2478
+ return 0;
2479
+ }
2480
+ if (dryRun) {
2481
+ out("");
2482
+ out(` ${style.quiet("Nothing was written. Run without --dry-run to apply.")}`);
2483
+ out("");
2484
+ return 0;
2485
+ }
2486
+ const applied = applyInstall(plans, manifestPath, yaml);
2487
+ out("");
2488
+ for (const entry of applied) {
2489
+ out(` ${style.quiet("backed up to")} ${entry.backup}`);
2490
+ }
2491
+ out("");
2492
+ out(` ${style.quiet("Restart your client, and its servers now run through Synartesis.")}`);
2493
+ out("");
2494
+ out(` ${style.quiet("One command shows everything and does everything:")}`);
2495
+ out(` ${style.accent(cliCommand())}`);
2496
+ out("");
2497
+ out(
2498
+ ` ${style.quiet("Live activity, what is held for approval, and undo, all from there.")}`
2499
+ );
2500
+ out(` ${style.quiet("You do not need a second terminal unless you want one.")}`);
2501
+ out("");
2502
+ return 0;
2503
+ }
2504
+ async function runUninstall(argv) {
2505
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2506
+ const only = flag(argv, "--client");
2507
+ const sites = discover(process.cwd()).filter(
2508
+ (site) => only === void 0 || site.client === only
2509
+ );
2510
+ const restored = applyUninstall(sites, manifestPath);
2511
+ out("");
2512
+ if (restored.length === 0) {
2513
+ out(` ${style.quiet("Nothing was covered, so nothing was changed.")}`);
2514
+ out("");
2515
+ return 0;
2516
+ }
2517
+ out(` ${style.label("restored")}`);
2518
+ out(` ${rule(60)}`);
2519
+ for (const entry of restored) {
2520
+ out("");
2521
+ out(` ${style.strong(entry.site.label)} ${style.quiet(entry.site.scope)}`);
2522
+ for (const name of entry.servers) {
2523
+ out(` ${style.strong(name)}`);
2524
+ }
2525
+ for (const name of entry.unknown) {
2526
+ out(
2527
+ ` ${style.accent(name)} ${style.quiet("is wrapped but its original was not recorded; left as it is")}`
2528
+ );
2529
+ }
2530
+ if (entry.backup !== "") {
2531
+ out(` ${style.quiet(`backed up to ${entry.backup}`)}`);
2532
+ }
2533
+ }
2534
+ out("");
2535
+ out(` ${style.quiet("The policy and journal were left alone.")}`);
2536
+ out("");
2537
+ return await Promise.resolve(0);
2538
+ }
2539
+ function openIfPresent(journalPath) {
2540
+ try {
2541
+ return existsSync7(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
2542
+ } catch {
2543
+ return void 0;
2544
+ }
2545
+ }
2546
+ async function connectThese(targets, manifestPath) {
2547
+ const invoker = invokerFor(version(), fileURLToPath2(import.meta.url));
2548
+ const wanted = /* @__PURE__ */ new Map();
2549
+ const sites = /* @__PURE__ */ new Map();
2550
+ for (const target of targets) {
2551
+ const key = `${target.site.path}${target.site.scope}`;
2552
+ sites.set(key, target.site);
2553
+ (wanted.get(key) ?? wanted.set(key, /* @__PURE__ */ new Set()).get(key))?.add(target.server);
2554
+ }
2555
+ const { plans, yaml } = await planInstall([...sites.values()], manifestPath, invoker);
2556
+ const narrowed = plans.map((plan) => ({
2557
+ ...plan,
2558
+ servers: plan.servers.filter(
2559
+ (server) => wanted.get(`${plan.site.path}${plan.site.scope}`)?.has(server.name) === true
2560
+ )
2561
+ }));
2562
+ const applied = applyInstall(narrowed, manifestPath, yaml);
2563
+ const count = applied.reduce((sum, entry) => sum + entry.servers.length, 0);
2564
+ if (count === 0) {
2565
+ return "nothing was connected; see the reasons above";
2566
+ }
2567
+ return `connected ${String(count)}${count === 1 ? " server" : " servers"} \xB7 restart the client to pick it up`;
2568
+ }
2569
+ function runStatus(argv) {
2570
+ const manifestPath = findManifest(flag(argv, "--manifest"));
2571
+ const journalPath = findJournal(flag(argv, "--journal"), manifestPath);
2572
+ out("");
2573
+ out(
2574
+ ` ${style.label("policy")} ${existsSync7(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
2575
+ );
2576
+ out(
2577
+ ` ${style.label("journal")} ${bytesOf(journalPath) === void 0 ? style.quiet(`${journalPath} (none yet)`) : `${style.strong(journalPath)} ${style.quiet(sizeOf(journalPath))}`}`
2578
+ );
2579
+ out("");
2580
+ const journal = openIfPresent(journalPath);
2581
+ try {
2582
+ const groups = scan(journal, process.cwd());
2583
+ if (groups.length === 0) {
2584
+ out(` ${style.quiet("No MCP client config found.")}`);
2585
+ out(` ${style.quiet("Looked for Claude Code, Claude Desktop, Cursor and Codex.")}`);
2586
+ out("");
2587
+ return 0;
2588
+ }
2589
+ const now = /* @__PURE__ */ new Date();
2590
+ for (const group of groups) {
2591
+ out(` ${style.strong(group.label)} ${style.quiet(group.scope)}`);
2592
+ if (group.problem !== void 0) {
2593
+ out(` ${style.accent(group.problem)}`);
2594
+ } else if (group.connections.length === 0) {
2595
+ out(` ${style.quiet("no servers listed")}`);
2596
+ }
2597
+ for (const connection of group.connections) {
2598
+ const state = stateOf(connection, now);
2599
+ out(
2600
+ ` ${connection.server.padEnd(20)} ${connection.covered ? style.quiet(state) : style.accent(state)}`
2601
+ );
2602
+ }
2603
+ out("");
2604
+ }
2605
+ const waiting = needsConnecting(groups).length;
2606
+ out(
2607
+ waiting === 0 ? ` ${style.quiet("Everything found is covered.")}` : ` ${style.accent(`${String(waiting)} not covered.`)} ${style.quiet(`${cliCommand()} install covers them.`)}`
2608
+ );
2609
+ out("");
2610
+ } finally {
2611
+ journal?.close();
2612
+ }
2613
+ return 0;
2614
+ }
1372
2615
  async function runInit(argv) {
1373
2616
  const name = positional(argv)[1];
1374
2617
  const separator = argv.indexOf("--");
@@ -1381,7 +2624,7 @@ async function runInit(argv) {
1381
2624
  }
1382
2625
  const path = findManifest(flag(argv, "--manifest"));
1383
2626
  const force = argv.includes("--force");
1384
- const present = existsSync4(path);
2627
+ const present = existsSync7(path);
1385
2628
  if (present && force) {
1386
2629
  throw new UsageError(
1387
2630
  `--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
@@ -1391,11 +2634,11 @@ async function runInit(argv) {
1391
2634
  name,
1392
2635
  command,
1393
2636
  args: argv.slice(separator + 2),
1394
- ...present ? { existing: readFileSync2(path, "utf8") } : {}
2637
+ ...present ? { existing: readFileSync4(path, "utf8") } : {}
1395
2638
  });
1396
2639
  parseManifest(draft.yaml, path);
1397
- mkdirSync(dirname(resolve(path)), { recursive: true, mode: 448 });
1398
- writeFileSync(path, draft.yaml);
2640
+ mkdirSync2(dirname3(resolve3(path)), { recursive: true, mode: 448 });
2641
+ writeFileSync3(path, draft.yaml);
1399
2642
  out("");
1400
2643
  out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
1401
2644
  out(` ${rule(54)}`);
@@ -1410,7 +2653,7 @@ async function runInit(argv) {
1410
2653
  out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
1411
2654
  }
1412
2655
  out("");
1413
- out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
2656
+ out(` ${style.accent(`${proxyCommand()} --manifest ${resolve3(path)}`)}`);
1414
2657
  out("");
1415
2658
  return 0;
1416
2659
  }
@@ -1472,12 +2715,12 @@ function runList(journal, asJson, journalPath) {
1472
2715
  return 0;
1473
2716
  }
1474
2717
  out("");
1475
- out(` ${style.label("runs")} ${style.quiet("most recent first")}`);
2718
+ out(` ${style.label("sessions")} ${style.quiet("most recent first")}`);
1476
2719
  out(` ${rule(96)}`);
1477
2720
  out("");
1478
2721
  out(
1479
2722
  style.quiet(
1480
- ` ${"run".padEnd(36)} ${"started".padEnd(24)} ${"status".padEnd(12)} actions agent`
2723
+ ` ${"session".padEnd(36)} ${"started".padEnd(15)} ${"status".padEnd(12)} actions agent`
1481
2724
  )
1482
2725
  );
1483
2726
  for (const run of runs) {
@@ -1490,7 +2733,7 @@ function runList(journal, asJson, journalPath) {
1490
2733
  ].filter((note2) => note2 !== "");
1491
2734
  const note = notes.length === 0 ? "" : ` ${style.accent(`(${notes.join("; ")})`)}`;
1492
2735
  out(
1493
- ` ${style.strong(run.id)} ${style.quiet(run.startedAt)} ${run.status.padEnd(12)} ${String(actions.length).padStart(7)} ${run.label ?? "-"}${note}`
2736
+ ` ${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
2737
  );
1495
2738
  }
1496
2739
  out("");
@@ -1501,6 +2744,7 @@ function runList(journal, asJson, journalPath) {
1501
2744
  return 0;
1502
2745
  }
1503
2746
  function runShow(argv, journal, asJson) {
2747
+ const full = argv.includes("--full");
1504
2748
  const runs = [...journal.listRuns()].reverse();
1505
2749
  const run = pick(runs, positional(argv)[1], RUN, true);
1506
2750
  const runId = run.id;
@@ -1509,13 +2753,13 @@ function runShow(argv, journal, asJson) {
1509
2753
  return 0;
1510
2754
  }
1511
2755
  out("");
1512
- out(` ${style.label("run")} ${style.strong(run.id)}`);
2756
+ out(` ${style.label("session")} ${style.strong(run.label ?? "an agent")} ${style.quiet(run.id)}`);
1513
2757
  out(` ${rule(54)}`);
1514
2758
  out("");
1515
2759
  out(` ${style.quiet("agent ")} ${run.label ?? "-"}`);
1516
- out(` ${style.quiet("started")} ${run.startedAt}`);
2760
+ out(` ${style.quiet("started")} ${fullTime(run.startedAt)} ${style.quiet(ago(run.startedAt))}`);
1517
2761
  out(
1518
- ` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${run.endedAt}`))
2762
+ ` ${style.quiet("status ")} ${run.status}` + (run.endedAt === void 0 ? "" : style.quiet(` ended ${fullTime(run.endedAt)}`))
1519
2763
  );
1520
2764
  const actions = journal.getActions(runId);
1521
2765
  if (actions.length === 0) {
@@ -1530,20 +2774,40 @@ function runShow(argv, journal, asJson) {
1530
2774
  out("");
1531
2775
  for (const action of actions) {
1532
2776
  out(
1533
- ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf2(action)} ${style.strong(`${action.server}.${action.tool}`)}`
2777
+ ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}`
1534
2778
  );
1535
- out(` ${style.quiet(truncate3(JSON.stringify(action.args), 96))}`);
2779
+ if (full) {
2780
+ out(` ${style.quiet("arguments")}`);
2781
+ for (const line2 of block(action.args)) {
2782
+ out(` ${line2}`);
2783
+ }
2784
+ } else {
2785
+ out(` ${style.quiet(summariseArgs(action.args, 96))}`);
2786
+ }
1536
2787
  if (action.approvedAt !== void 0) {
1537
2788
  const verb = action.status === "denied" ? "denied" : "approved";
1538
2789
  out(
1539
- ` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${action.approvedAt}`)}`
2790
+ ` ${style.accent(`${verb} by ${action.approvedBy ?? "nobody"}`)} ${style.quiet(`at ${fullTime(action.approvedAt)}`)}`
1540
2791
  );
1541
2792
  }
1542
2793
  if (action.error !== void 0) {
1543
- out(` ${style.quiet(`note: ${truncate3(action.error, 200)}`)}`);
2794
+ out(` ${style.quiet(`note: ${full ? action.error : truncate3(action.error, 200)}`)}`);
2795
+ }
2796
+ if (action.snapshot !== void 0 && full) {
2797
+ out(` ${style.quiet("what it replaced")}`);
2798
+ for (const line2 of block(action.snapshot)) {
2799
+ out(` ${line2}`);
2800
+ }
1544
2801
  }
1545
2802
  if (action.inverse !== void 0) {
1546
- out(` ${style.quiet("undo:")} ${truncate3(JSON.stringify(action.inverse), 200)}`);
2803
+ if (full) {
2804
+ out(` ${style.quiet("undo")}`);
2805
+ for (const line2 of block(action.inverse)) {
2806
+ out(` ${line2}`);
2807
+ }
2808
+ } else {
2809
+ out(` ${style.quiet("undo:")} ${style.quiet(summariseArgs(inverseArgs(action.inverse), 90))}`);
2810
+ }
1547
2811
  }
1548
2812
  }
1549
2813
  out("");
@@ -1563,7 +2827,7 @@ function badgeOf(action) {
1563
2827
  const plain = `${CLASS_MARK[action.class]} ${action.class}`.padEnd(BADGE_WIDTH);
1564
2828
  return action.class === "irreversible" ? style.accent(plain) : style.quiet(plain);
1565
2829
  }
1566
- function statusOf2(action) {
2830
+ function statusOf(action) {
1567
2831
  const text = labelFor(action).padEnd(13);
1568
2832
  if (wasRefused(action)) {
1569
2833
  return style.accent(text);
@@ -1591,6 +2855,15 @@ function wrapped(text, width) {
1591
2855
  }
1592
2856
  return lines;
1593
2857
  }
2858
+ function block(value) {
2859
+ return JSON.stringify(value, void 0, 2).split("\n").map((line2) => style.quiet(line2));
2860
+ }
2861
+ function inverseArgs(inverse) {
2862
+ if (typeof inverse === "object" && inverse !== null && "args" in inverse) {
2863
+ return inverse.args;
2864
+ }
2865
+ return inverse;
2866
+ }
1594
2867
  function truncate3(text, limit) {
1595
2868
  return text.length <= limit ? text : `${text.slice(0, limit - 3)}...`;
1596
2869
  }
@@ -1761,7 +3034,7 @@ function runDecision(argv, journal, approving) {
1761
3034
  }
1762
3035
  return failed === 0 ? 0 : 1;
1763
3036
  }
1764
- function report(result) {
3037
+ function report(result, alreadyForcing = false) {
1765
3038
  out("");
1766
3039
  out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
1767
3040
  out(` ${rule(72)}`);
@@ -1786,15 +3059,37 @@ function report(result) {
1786
3059
  }
1787
3060
  }
1788
3061
  if (result.halted !== void 0) {
3062
+ const halt = result.halted;
1789
3063
  out("");
1790
3064
  out(
1791
- ` ${style.accent("halted")} ${style.quiet(`at sequence ${String(result.halted.seq)}`)} ${result.halted.reason}`
3065
+ ` ${style.accent("halted")} ${style.quiet(`at ${String(halt.seq)}`)} ${halt.reason}
3066
+ ${style.quiet("nothing was written here")}`
1792
3067
  );
1793
- if (result.halted.detail !== "") {
1794
- for (const line2 of result.halted.detail.split("\n")) {
3068
+ if (halt.detail !== "") {
3069
+ out("");
3070
+ for (const line2 of halt.detail.split("\n")) {
1795
3071
  out(` ${style.quiet(line2)}`);
1796
3072
  }
1797
3073
  }
3074
+ if (halt.overwrites !== void 0 && halt.overwrites !== "") {
3075
+ out("");
3076
+ out(` ${style.accent("undoing anyway would write:")}`);
3077
+ for (const line2 of halt.overwrites.split("\n")) {
3078
+ out(` ${style.quiet(line2)}`);
3079
+ }
3080
+ }
3081
+ if (halt.conflict === true && !alreadyForcing) {
3082
+ const self = cliCommand();
3083
+ const id = result.runId.slice(0, 8);
3084
+ out("");
3085
+ out(` ${style.quiet("keep the change, drop the undo:")} ${style.quiet("nothing to do")}`);
3086
+ out(
3087
+ ` ${style.quiet("put it back as the run left it:")} ${style.strong(`${self} undo ${id} --replan`)}`
3088
+ );
3089
+ out(
3090
+ ` ${style.quiet("undo anyway, losing the change:")} ${style.strong(`${self} undo ${id} --force`)}`
3091
+ );
3092
+ }
1798
3093
  }
1799
3094
  const permanent = result.steps.filter((step) => step.kind === "permanent");
1800
3095
  if (permanent.length > 0) {
@@ -1825,13 +3120,23 @@ async function performUndo(manifestPath, journal, runId, options) {
1825
3120
  })
1826
3121
  );
1827
3122
  }
1828
- return await rollback({
3123
+ const base = {
1829
3124
  journal,
1830
3125
  router: createRouter(upstreams, manifest),
1831
3126
  runId,
1832
3127
  ...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
1833
- dryRun: options.dryRun,
1834
3128
  ...options.replan === true ? { replanWith: manifest } : {}
3129
+ };
3130
+ if (options.preflight === true) {
3131
+ const seen = await rollback({ ...base, dryRun: true });
3132
+ if (seen.halted?.conflict === true) {
3133
+ return seen;
3134
+ }
3135
+ }
3136
+ return await rollback({
3137
+ ...base,
3138
+ dryRun: options.dryRun,
3139
+ ...options.force === true ? { force: true } : {}
1835
3140
  });
1836
3141
  } finally {
1837
3142
  for (const upstream of upstreams) {
@@ -1840,7 +3145,41 @@ async function performUndo(manifestPath, journal, runId, options) {
1840
3145
  }
1841
3146
  }
1842
3147
  async function runUndo(argv, journal) {
1843
- const runId = pick([...journal.listRuns()].reverse(), positional(argv)[1], RUN, true).id;
3148
+ const given = positional(argv)[1];
3149
+ const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
3150
+ const runId = chosen.id;
3151
+ if (given === void 0) {
3152
+ const actions = journal.getActions(runId);
3153
+ const left = actions.filter((action) => action.status === "applied").length;
3154
+ out("");
3155
+ out(
3156
+ ` ${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()}`)
3157
+ );
3158
+ if (left === 0) {
3159
+ out("");
3160
+ out(
3161
+ ` ${style.quiet(
3162
+ 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."
3163
+ )}`
3164
+ );
3165
+ const other = [...journal.listRuns()].reverse().find(
3166
+ (run) => run.id !== runId && journal.getActions(run.id).some((action) => action.status === "applied" && action.inverse !== void 0)
3167
+ );
3168
+ if (other !== void 0) {
3169
+ out("");
3170
+ out(
3171
+ ` ${style.quiet("The session that did something:")} ${style.strong(other.id.slice(0, 8))} ` + style.quiet(`${other.label ?? "an agent"}, ${shortTime(other.startedAt).trim()}`)
3172
+ );
3173
+ out(` ${style.quiet(`${cliCommand()} undo ${other.id.slice(0, 8)}`)}`);
3174
+ } else {
3175
+ out(
3176
+ ` ${style.quiet(`Name one to undo a different session: ${cliCommand()} undo <session>`)}`
3177
+ );
3178
+ }
3179
+ out("");
3180
+ return 0;
3181
+ }
3182
+ }
1844
3183
  const rawTo = flag(argv, "--to");
1845
3184
  const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
1846
3185
  if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
@@ -1854,15 +3193,32 @@ async function runUndo(argv, journal) {
1854
3193
  );
1855
3194
  }
1856
3195
  }
1857
- return report(
1858
- await performUndo(findManifest(flag(argv, "--manifest")), journal, runId, {
1859
- dryRun: argv.includes("--dry-run"),
1860
- ...toSeq === void 0 ? {} : { toSeq },
1861
- replan: argv.includes("--replan")
1862
- })
1863
- );
3196
+ const forcing = argv.includes("--force");
3197
+ const said = argv.includes("--yes");
3198
+ const result = await performUndo(findManifest(flag(argv, "--manifest")), journal, runId, {
3199
+ dryRun: argv.includes("--dry-run"),
3200
+ ...toSeq === void 0 ? {} : { toSeq },
3201
+ replan: argv.includes("--replan"),
3202
+ ...forcing ? { force: said, preflight: !said } : {}
3203
+ });
3204
+ const code = report(result, forcing);
3205
+ if (forcing && !said && result.halted?.conflict === true) {
3206
+ out(` ${style.quiet("nothing has been written. To go ahead and lose that change:")}`);
3207
+ out(
3208
+ ` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`
3209
+ );
3210
+ out("");
3211
+ }
3212
+ return code;
1864
3213
  }
1865
3214
  var FLAGS = /* @__PURE__ */ new Set([
3215
+ // Two lists have to agree about a flag: this one decides whether it is
3216
+ // accepted at all, and the skip set in positional() decides whether its
3217
+ // value is mistaken for a command. --client was in one and not the other,
3218
+ // so `install --client codex` printed the help instead of installing.
3219
+ "--client",
3220
+ "--print",
3221
+ "--full",
1866
3222
  "--manifest",
1867
3223
  "--journal",
1868
3224
  "--to",
@@ -1874,6 +3230,7 @@ var FLAGS = /* @__PURE__ */ new Set([
1874
3230
  "--replan",
1875
3231
  "--reason",
1876
3232
  "--force",
3233
+ "--yes",
1877
3234
  "--older-than",
1878
3235
  "--help",
1879
3236
  "-h",
@@ -1882,8 +3239,8 @@ var FLAGS = /* @__PURE__ */ new Set([
1882
3239
  ]);
1883
3240
  function version() {
1884
3241
  try {
1885
- const root = dirname(fileURLToPath2(import.meta.url));
1886
- const parsed = JSON.parse(readFileSync2(join(root, "..", "package.json"), "utf8"));
3242
+ const root = dirname3(fileURLToPath2(import.meta.url));
3243
+ const parsed = JSON.parse(readFileSync4(join2(root, "..", "package.json"), "utf8"));
1887
3244
  const found = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
1888
3245
  return typeof found === "string" ? found : "unknown";
1889
3246
  } catch {
@@ -1901,7 +3258,7 @@ function rejectUnknownFlags(argv) {
1901
3258
  }
1902
3259
  }
1903
3260
  function openJournalOrExplain(journalPath) {
1904
- if (!existsSync4(journalPath)) {
3261
+ if (!existsSync7(journalPath)) {
1905
3262
  throw new UsageError(
1906
3263
  `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
3264
  );
@@ -1930,13 +3287,18 @@ ${COMMANDS}`);
1930
3287
  const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
1931
3288
  return await openConsole({
1932
3289
  journalPath: journalPath2,
3290
+ scan: () => scan(openIfPresent(journalPath2), process.cwd()),
3291
+ connect: async (targets) => await connectThese(targets, manifestPath),
1933
3292
  write: (text) => process.stdout.write(text),
1934
3293
  live: process.stdout.isTTY,
1935
3294
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown",
1936
- undo: async (runId, dryRun) => {
3295
+ undo: async (runId, dryRun, force) => {
1937
3296
  const journal2 = openJournal(journalPath2, { mustExist: true });
1938
3297
  try {
1939
- return await performUndo(manifestPath, journal2, runId, { dryRun });
3298
+ return await performUndo(manifestPath, journal2, runId, {
3299
+ dryRun,
3300
+ ...force === true ? { force: true } : {}
3301
+ });
1940
3302
  } finally {
1941
3303
  journal2.close();
1942
3304
  }
@@ -1949,6 +3311,15 @@ ${COMMANDS}`);
1949
3311
  if (command === "check") {
1950
3312
  return await runCheck(argv);
1951
3313
  }
3314
+ if (command === "install") {
3315
+ return await runInstall(argv);
3316
+ }
3317
+ if (command === "uninstall") {
3318
+ return await runUninstall(argv);
3319
+ }
3320
+ if (command === "status") {
3321
+ return runStatus(argv);
3322
+ }
1952
3323
  const asJson = argv.includes("--json");
1953
3324
  const given = flag(argv, "--journal");
1954
3325
  const journalPath = findJournal(given, findManifest(flag(argv, "--manifest")));
@@ -1964,8 +3335,8 @@ ${COMMANDS}`);
1964
3335
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
1965
3336
  });
1966
3337
  }
1967
- journalArg = given === void 0 ? "" : ` --journal ${resolve(given)}`;
1968
- if (!existsSync4(journalPath) && (command === "list" || command === "show" || command === "gates")) {
3338
+ journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
3339
+ if (!existsSync7(journalPath) && (command === "list" || command === "show" || command === "gates")) {
1969
3340
  if (asJson) {
1970
3341
  out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
1971
3342
  return 0;