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/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-
|
|
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
|
|
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(
|
|
144
|
+
heading: (text) => paint(INK, text.toUpperCase()),
|
|
133
145
|
accent: (text) => paint(ACCENT, text),
|
|
134
|
-
strong: (text) => paint(
|
|
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-
|
|
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
|
-
|
|
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-
|
|
151
|
+
//# sourceMappingURL=chunk-YVOO3PTV.js.map
|