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 CHANGED
@@ -2,6 +2,169 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.4.1 — 2026-09-09
6
+
7
+ ### Added
8
+
9
+ - **`synartesis show <session> --live`**, and `l` in the screen. Synartesis
10
+ records what an agent does, not what happens to a file — nothing you do by
11
+ hand goes through the proxy. That is what makes the drift check work, and it
12
+ meant the commonest question about this tool had no answer: has somebody
13
+ edited that file since? You found out by attempting an undo and having it
14
+ refuse.
15
+
16
+ `--live` reads every resource the session touched as it is now and says
17
+ which of them still match what the run left. Nothing is written, no
18
+ reversing call is sent, and no row changes status. Unlike `undo --dry-run`
19
+ it does not stop at the first conflict: a session with five writes reports
20
+ on all five.
21
+
22
+ An earlier write to a resource is reported as superseded rather than
23
+ changed. Undo walks backwards, so each older write is checked against a
24
+ state the one above it restores; measuring them all against the world as it
25
+ is now would call every write but the last one changed.
26
+
27
+ ### Fixed
28
+
29
+ - **Undo acted on a session nobody was looking at.** One cursor serves four
30
+ views, and in the held-calls and connections lists its number counts
31
+ something else entirely — but `u`, `p` and `l` read it against the sessions
32
+ regardless. `g`, `j`, `u`, `y`, four keys pressed while looking at the list
33
+ of held calls, undid a session that was never on screen. That is the failure
34
+ this tool exists to prevent, committed by the tool. Those keys now do
35
+ nothing outside the session views and say why.
36
+
37
+ - **`--force` showed one conflict and overwrote several.** The ask ran a
38
+ dry-run rollback, and a rollback halts at the first drift — so with two
39
+ people's edits underneath it, one diff was printed, `--force --yes` was
40
+ typed, and both were written over. It now reads every conflict and prints
41
+ each one.
42
+
43
+ - **One broken server stopped you undoing anything at all.** `install` covers
44
+ every AI on the machine with one policy, so a manifest routinely names
45
+ servers that have nothing to do with the session in hand — and undo started
46
+ all of them. An entry whose command is not there made every session
47
+ unreadable and unundoable. Undo and `--live` now start only the servers the
48
+ session actually went to, and skip with a reported reason any that still
49
+ will not start. `--replan` still starts everything, since it re-resolves
50
+ inverses from the current policy.
51
+
52
+ - **`--force` ignored `--to`.** A change below the floor — an action the undo
53
+ would not touch — refused the whole command.
54
+
55
+ - The overwrite warning in the screen now expires with the diff that
56
+ justified it, so the second `u` cannot be answered after the evidence has
57
+ scrolled away. `--yes` without `--force` says it is being ignored rather
58
+ than being silently dropped, and `--live` no longer starts every server to
59
+ inspect a session that recorded nothing.
60
+
61
+ ## 0.4.0 — 2026-09-08
62
+
63
+ ### Added
64
+
65
+ - **`synartesis install`.** One command wraps every server your MCP client
66
+ already lists. It knows Claude Code, Claude Desktop, Cursor and Codex, finds
67
+ their config files, writes a policy covering everything it finds — adopting
68
+ the bundled policies where they fit — and points each entry at the proxy.
69
+ `uninstall` puts the configs back; `status` says what is covered.
70
+
71
+ Setting a single server up used to mean reading your client's JSON, retyping
72
+ the command into `init`, hand-editing the policy, then editing the JSON back.
73
+ Two hand edits across two files, per server, which is most of the reason
74
+ anyone gave up before seeing an undo work.
75
+
76
+ Your config is copied aside first, the write lands by rename rather than in
77
+ place, a file that does not parse is refused rather than repaired, and every
78
+ key we do not recognise is carried through. Codex's TOML is edited by line so
79
+ its comments, ordering and env subtables survive.
80
+
81
+ - **A connections screen**, on `c` in `synartesis`. Every AI on the machine,
82
+ whether its config points here, and when anything last actually came through
83
+ it — read from the journal, which records the server on every action, rather
84
+ than guessed from processes. `enter` connects one, `a` connects everything
85
+ uncovered, `r` rescans. Nothing is written without a keypress.
86
+
87
+ - **`--server <name>` on the proxy**, so one policy can back several client
88
+ entries. A proxy carrying two servers has to qualify tool names to tell them
89
+ apart, which renames every tool the agent already knows; one entry per server
90
+ keeps the names.
91
+
92
+ - **`synartesis show <session> --full`**, and `f` in the screen: every argument,
93
+ the captured snapshot and the inverse, pretty-printed with nothing elided.
94
+
95
+ ### Fixed
96
+
97
+ - **A tool a client would not call at all.** The official filesystem server
98
+ declares its `outputSchema` as JSON Schema draft-07, and a client whose
99
+ validator only knows 2020-12 refuses to call the tool — nothing reaches the
100
+ proxy, and there is nothing in the journal to explain the silence. The
101
+ dialect is now dropped from advertised output schemas. `inputSchema` is
102
+ untouched, so the tool list is byte-identical wherever a server declares no
103
+ output schema.
104
+
105
+ - **Times are shown where you are standing.** The journal stores UTC, which is
106
+ right, and every view printed that UTC string unchanged — so somebody in
107
+ Kolkata watching an agent work saw 10:05 while their own clock said 15:35.
108
+
109
+ ### Changed
110
+
111
+ - **Actions read as sentences.** `10:36:56 reversible rolled_back
112
+ sim.edit_file`, twelve times over, is a class and a status and never once
113
+ which file. Rows now say the server, the tool, what it acted on and what it
114
+ means: `15:36:56 sim edit_file loadtest.mjs undone`. Arguments are summarised
115
+ rather than truncated JSON; `--full` shows everything.
116
+
117
+ - **`synchronous = NORMAL` under WAL.** Measured on the journal alone: writes
118
+ 0.188 → 0.125 ms per action, rollback marks 0.079 → 0.039 ms. A crash of the
119
+ process still loses nothing; only the machine going down can cost the tail of
120
+ the WAL, and what is there is the record of a call, never the call.
121
+
122
+ - Headings and names are a softer ink. Bold near-white smears at terminal
123
+ sizes, and the palette only works if one thing is bright.
124
+
125
+ - **A halt says what it refused, not only that it stopped.** `halted: halted
126
+ here on an earlier attempt` explains nothing; the sentence worth reading is
127
+ the one saying somebody edited the resource since. And a dry run now re-reads
128
+ the world rather than quoting the message from the last attempt — that halt
129
+ exists so a retry cannot silently repeat what a person stopped, and a dry run
130
+ writes nothing, so it is not a retry.
131
+
132
+ - **Undo says which session, before acting on it.** Without an id it takes the
133
+ most recent, which is not necessarily the one on your screen — somebody undid
134
+ a session they were not looking at and read the result as the tool acting on
135
+ its own. It now names its pick first, and where that session is empty it says
136
+ so and names the one that did something.
137
+
138
+ ### Fixed (undo, from using it)
139
+
140
+ - **`u` in the screen appeared to do nothing.** A client that connects and calls
141
+ nothing still opens a session, so the newest one is regularly empty while the
142
+ one you mean is a line below it. Pressing undo on it confirmed, reverted zero
143
+ and said nothing about why. `u` and `p` now count what could actually be put
144
+ back first, and answer with the reason and the id of the session that has
145
+ something.
146
+
147
+ - **The screen prints the command for what `u` would do**, for the session under
148
+ the cursor: `synartesis undo <id>`. A screen that only answers keystrokes
149
+ gives you nothing to carry to another window and nothing to check when a key
150
+ seems to have done nothing.
151
+
152
+ ### Added (drift)
153
+
154
+ - **`undo --force`**, for a resource somebody changed after the run. Asked for
155
+ twice: on its own it reads the world, prints the lines it would write over,
156
+ and writes nothing; `--force --yes` goes ahead. Two flags rather than a
157
+ prompt, so it reads the same in a terminal and in a script.
158
+
159
+ A refusal with no way past it is half an answer. The halt now carries what
160
+ undoing would overwrite — not only what changed since the run, which is
161
+ history, but which of *your* lines would go — and prints the three ways on as
162
+ three commands: leave it, put the resource back and `--replan`, or `--force`.
163
+
164
+ In the screen, a conflicted session says so and names the command. `u` shows
165
+ the overwrite diff, and only a second `u` will offer to do it, with `y` still
166
+ required after that.
167
+
5
168
  ## 0.3.4 — 2026-09-03
6
169
 
7
170
  ### Changed
package/README.md CHANGED
@@ -83,6 +83,22 @@ clone on macOS run `xcode-select --install`; on Debian or Ubuntu,
83
83
  npm install -g synartesis
84
84
  ```
85
85
 
86
+ Then, from anywhere:
87
+
88
+ ```bash
89
+ synartesis install
90
+ ```
91
+
92
+ That finds what Claude Code, Claude Desktop, Cursor or Codex already list, writes one
93
+ policy covering all of it, and points each entry at the proxy. Servers it
94
+ recognises get the policy that ships for them and work immediately; the rest
95
+ are drafted with every tool held until you say how to undo it. Your config is
96
+ copied aside first, `synartesis uninstall` puts it back, and `synartesis
97
+ status` says what is covered.
98
+
99
+ Each server keeps its own entry and its own proxy, selected with `--server`,
100
+ so **no tool is renamed** — the agent sees exactly the names it saw before.
101
+
86
102
  That is for you: the screen, `undo`, `watch`, `approve`. **Your agent needs
87
103
  nothing installed** — the config block below fetches the proxy on demand.
88
104
 
@@ -510,11 +526,13 @@ was refused with the file untouched.
510
526
  | `init <server> -- <cmd>` | Introspect a server and draft a manifest |
511
527
  | `list` | Every recorded run |
512
528
  | `show <runId>` | One run's timeline, with the undo for each step |
529
+ | `show <runId> --live` | The same, plus what has changed in the world since |
513
530
  | `gates` | What is waiting for a decision |
514
531
  | `approve <actionId>` | Allow a suspended call |
515
532
  | `deny <actionId>` | Refuse one |
516
533
  | `undo <runId>` | Reverse a run, newest action first |
517
534
  | `undo <runId> --replan` | Same, but rebuild each undo from the current manifest |
535
+ | `undo <runId> --force` | Print every change it would write over; `--yes` goes ahead |
518
536
  | `check` | Load a manifest and verify it against the servers it names |
519
537
  | `prune` | Delete runs older than 30 days and reclaim the space |
520
538
  | `close [runId]` | End a run a killed proxy left open. Nothing guesses at this: several proxies can share one journal, so a run left active is indistinguishable from one still being worked on |
@@ -582,6 +600,39 @@ protocol traffic.
582
600
  is baked into every run made under it. `undo --replan` rebuilds them from a
583
601
  corrected manifest using the state already captured, which is the way out.
584
602
 
603
+ ## Has anybody touched it since?
604
+
605
+ Synartesis records what an agent does, not what happens to a file. Nothing you
606
+ do by hand goes through the proxy, so an edit of your own is invisible to the
607
+ journal — and that is what makes the drift check work: when undo reads a file
608
+ and finds bytes it never recorded, it knows somebody else has been there.
609
+
610
+ The cost used to be that you found out by walking into it. You ran the undo,
611
+ and it refused. To ask first:
612
+
613
+ ```bash
614
+ synartesis show <session> --live
615
+ ```
616
+
617
+ It reads every resource the session touched, as it is now, and says which of
618
+ them still match what the run left:
619
+
620
+ ```
621
+ 1 ← reversible applied filesystem.write_file changed since
622
+ at line 13:
623
+ + A HUMAN WAS HERE
624
+ 0 removed, 1 added.
625
+
626
+ 1 changed since this ran; undoing would write over it
627
+ ```
628
+
629
+ Nothing is written, no reversing call is sent, and no row changes status.
630
+ Unlike `undo --dry-run`, it does not stop at the first conflict — a session
631
+ with five writes reports on all five. `l` in the screen does the same thing.
632
+
633
+ If you decide the recorded value is the one worth keeping, `undo --force`
634
+ prints every line it would write over and stops; `--force --yes` goes ahead.
635
+
585
636
  ## Watching it work
586
637
 
587
638
  Synartesis is not a daemon and cannot be one. An MCP client spawns a stdio
@@ -4,9 +4,10 @@ import {
4
4
  SnapshotError,
5
5
  UpstreamError,
6
6
  describe
7
- } from "./chunk-WNLRMSDB.js";
7
+ } from "./chunk-YVOO3PTV.js";
8
8
 
9
9
  // src/invocation.ts
10
+ import { spawnSync } from "child_process";
10
11
  import { accessSync, constants } from "fs";
11
12
  import { basename, delimiter, join } from "path";
12
13
  import { fileURLToPath } from "url";
@@ -57,6 +58,17 @@ function proxyCommand() {
57
58
  }
58
59
  return `node ${fileURLToPath(new URL("cli.js", import.meta.url))} proxy`;
59
60
  }
61
+ function pathBinaryMatches(ourVersion) {
62
+ if (!onPath("synartesis")) {
63
+ return false;
64
+ }
65
+ try {
66
+ const result = spawnSync("synartesis", ["--version"], { encoding: "utf8", timeout: 1e4 });
67
+ return result.status === 0 && result.stdout.trim() === ourVersion;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
60
72
 
61
73
  // src/locate.ts
62
74
  import { existsSync } from "fs";
@@ -115,7 +127,7 @@ function findJournal(given, manifest) {
115
127
  var ESC = "\x1B[";
116
128
  var ACCENT = `${ESC}38;2;226;134;118m`;
117
129
  var ON_ACCENT = `${ESC}48;2;94;20;32m${ESC}38;2;246;233;229m`;
118
- var BRIGHT = `${ESC}38;2;246;233;229m`;
130
+ var INK = `${ESC}38;2;214;201;197m`;
119
131
  var DIM = `${ESC}2m`;
120
132
  var BOLD = `${ESC}1m`;
121
133
  var RESET = `${ESC}0m`;
@@ -129,9 +141,9 @@ function spaced(text) {
129
141
  var style = {
130
142
  /** A section label: small, capital, spaced out. */
131
143
  label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
132
- heading: (text) => paint(BRIGHT + BOLD, text.toUpperCase()),
144
+ heading: (text) => paint(INK, text.toUpperCase()),
133
145
  accent: (text) => paint(ACCENT, text),
134
- strong: (text) => paint(BOLD, text),
146
+ strong: (text) => paint(INK, text),
135
147
  quiet: (text) => paint(DIM, text),
136
148
  /** Off-white on oxblood, the way the wordmark is set. */
137
149
  plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
@@ -327,6 +339,7 @@ function openDatabase(path) {
327
339
  db.pragma("journal_mode = WAL");
328
340
  db.pragma("foreign_keys = ON");
329
341
  db.pragma("busy_timeout = 5000");
342
+ db.pragma("synchronous = NORMAL");
330
343
  if (path !== ":memory:") {
331
344
  restrictToOwner(path);
332
345
  }
@@ -1451,6 +1464,7 @@ export {
1451
1464
  cliCommand,
1452
1465
  cliCommandFrom,
1453
1466
  proxyCommand,
1467
+ pathBinaryMatches,
1454
1468
  findManifest,
1455
1469
  findJournal,
1456
1470
  style,
@@ -1479,4 +1493,4 @@ export {
1479
1493
  observeState,
1480
1494
  connectStdioUpstream
1481
1495
  };
1482
- //# sourceMappingURL=chunk-LNR5GXPG.js.map
1496
+ //# sourceMappingURL=chunk-FOA4UIDE.js.map
@@ -95,15 +95,17 @@ function brief(value) {
95
95
  return "(a value too deeply nested to print)";
96
96
  }
97
97
  }
98
+ function changedLines(expected, actual) {
99
+ const before = longestString(expected);
100
+ const after = longestString(actual);
101
+ return before !== "" && after !== "" && before !== after ? lineDiff(before, after) : ` expected: ${brief(expected)}
102
+ actual: ${brief(actual)}`;
103
+ }
98
104
  var DriftConflict = class extends SynartesisError {
99
105
  constructor(seq, expected, actual) {
100
- const before = longestString(expected);
101
- const after = longestString(actual);
102
- const body = before !== "" && after !== "" && before !== after ? lineDiff(before, after) : ` expected: ${brief(expected)}
103
- actual: ${brief(actual)}`;
104
106
  super(
105
107
  `drift at sequence ${String(seq)}: the resource is not in the state this run left it in.
106
- ${body}`
108
+ ` + changedLines(expected, actual)
107
109
  );
108
110
  this.seq = seq;
109
111
  this.expected = expected;
@@ -140,9 +142,10 @@ export {
140
142
  ManifestError,
141
143
  UpstreamError,
142
144
  SnapshotError,
145
+ changedLines,
143
146
  DriftConflict,
144
147
  RollbackHalted,
145
148
  JournalError,
146
149
  describe
147
150
  };
148
- //# sourceMappingURL=chunk-WNLRMSDB.js.map
151
+ //# sourceMappingURL=chunk-YVOO3PTV.js.map