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/CHANGELOG.md +163 -0
- package/README.md +51 -0
- package/dist/{chunk-LNR5GXPG.js → chunk-FOA4UIDE.js} +19 -5
- package/dist/{chunk-WNLRMSDB.js → chunk-YVOO3PTV.js} +9 -6
- package/dist/cli.js +1756 -137
- package/dist/proxy.js +46 -6
- package/package.json +1 -1
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-
|
|
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-
|
|
37
|
+
} from "./chunk-YVOO3PTV.js";
|
|
36
38
|
|
|
37
39
|
// src/cli.ts
|
|
38
|
-
import { existsSync as
|
|
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
|
|
290
|
+
const serversAt2 = existing.indexOf("\nservers:");
|
|
289
291
|
const toolsAt = existing.indexOf("\ntools:");
|
|
290
|
-
if (
|
|
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
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
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 = {
|
|
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
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
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((
|
|
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((
|
|
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
|
|
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.
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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("
|
|
973
|
-
keyHint2("
|
|
1966
|
+
keyHint2("f", "expand"),
|
|
1967
|
+
keyHint2("esc", "back")
|
|
974
1968
|
] : [
|
|
975
1969
|
keyHint2("enter", "open"),
|
|
976
|
-
keyHint2("
|
|
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 &&
|
|
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 +
|
|
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
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1163
|
-
|
|
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
|
|
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((
|
|
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((
|
|
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
|
|
1252
|
-
|
|
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> [--
|
|
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 =
|
|
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:
|
|
2778
|
+
...present ? { existing: readFileSync4(path, "utf8") } : {}
|
|
1395
2779
|
});
|
|
1396
2780
|
parseManifest(draft.yaml, path);
|
|
1397
|
-
|
|
1398
|
-
|
|
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 ${
|
|
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("
|
|
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
|
-
` ${"
|
|
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(
|
|
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("
|
|
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)} ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3255
|
+
` ${style.accent("halted")} ${style.quiet(`at ${String(halt.seq)}`)} ${halt.reason}
|
|
3256
|
+
${style.quiet("nothing was written here")}`
|
|
1792
3257
|
);
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
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
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
1886
|
-
const parsed = JSON.parse(
|
|
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 (!
|
|
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
|
-
|
|
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, {
|
|
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 ${
|
|
1968
|
-
if (!
|
|
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":
|