synartesis 0.6.13 → 0.6.15
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 +140 -0
- package/dist/{chunk-6X7GPUGJ.js → chunk-OODDXM36.js} +146 -21
- package/dist/cli.js +322 -48
- package/dist/proxy.js +19 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,126 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.6.15 — 2026-09-15
|
|
6
|
+
|
|
7
|
+
One gap, named by a stranger on Reddit and real.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **A retry of a call whose outcome nobody could establish is now held for a
|
|
12
|
+
person.** The tri-state was already there -- a write that times out is not
|
|
13
|
+
called `failed`, the resource is re-read to see which way it went, and what
|
|
14
|
+
cannot be settled is left `pending`, which undo refuses to step over. What
|
|
15
|
+
was missing was the other half: nothing stopped the agent from simply making
|
|
16
|
+
the call again.
|
|
17
|
+
|
|
18
|
+
The idempotency key cannot close this. It is `runId:seq`, so a retry is a new
|
|
19
|
+
row under a different key and no upstream can tell the two attempts were one
|
|
20
|
+
intention -- checked against the real proxy, which records `seq=1 key=…:1`
|
|
21
|
+
and `seq=2 key=…:2`. A comment beside the forward call claimed otherwise and
|
|
22
|
+
has been corrected.
|
|
23
|
+
|
|
24
|
+
So the person who can go and look is asked before a second side effect
|
|
25
|
+
exists. It reuses the gate rather than inventing a second mechanism: the call
|
|
26
|
+
appears in `synartesis gates` with a reason that says what happened, rather
|
|
27
|
+
than the generic policy text.
|
|
28
|
+
|
|
29
|
+
Three things it deliberately does not do. It never holds a **read** -- a read
|
|
30
|
+
that timed out is left `pending` like anything else, but reading twice
|
|
31
|
+
changes nothing, and an agent may always look freely. It holds only the
|
|
32
|
+
**identical** call, matched on server, tool and arguments: the uncertainty is
|
|
33
|
+
about one call, not about the tool. And approving the retry **does not
|
|
34
|
+
resolve the first attempt** -- that row stays `pending` and undo still stops
|
|
35
|
+
on it, because saying yes to going forward is not a claim about what already
|
|
36
|
+
happened.
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
- `findPending` runs on the way in to every write, and left to choose sqlite
|
|
41
|
+
walked every action in the run -- rows that carry the snapshots -- to find
|
|
42
|
+
the handful that are pending. A partial index over exactly those rows, named
|
|
43
|
+
explicitly, on a single run of twenty thousand actions with two-kilobyte
|
|
44
|
+
snapshots: **24ms a call to 0.004ms**.
|
|
45
|
+
|
|
46
|
+
## 0.6.14 — 2026-09-15 (tagged, never published)
|
|
47
|
+
|
|
48
|
+
The terminal now says what to do next, and the argument parser stopped
|
|
49
|
+
disagreeing with itself.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **Most commands end by naming the one thing worth doing next**, with the
|
|
54
|
+
session id already filled in:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
fs.create_directory is held, and the agent is waiting on you: synartesis approve f0c9935d
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It is worked out from the journal rather than from what was typed, so it
|
|
61
|
+
changes as the state does. A call held for approval outranks everything else
|
|
62
|
+
-- that is an agent stopped mid-task waiting on a person who may not know it
|
|
63
|
+
is waiting. Below that: the session that changed something, what is not
|
|
64
|
+
pinned, and `watch`. There is at most one, there is none when nothing
|
|
65
|
+
applies, and `--json` never carries it. `SYNARTESIS_NO_HINTS` turns it off.
|
|
66
|
+
|
|
67
|
+
The session it names is not simply the newest one. A client that connects
|
|
68
|
+
and reads opens a session like any other, so "the last thing that happened"
|
|
69
|
+
is regularly a session in which nothing happened; a session that only read,
|
|
70
|
+
or was already undone, or was held and never ran, is never offered for undo.
|
|
71
|
+
|
|
72
|
+
- **A mistyped command gets the word that was meant.** `synartesis lst` used to
|
|
73
|
+
answer with forty lines of help, thirty-nine of them about something the
|
|
74
|
+
person was not doing. It now says `did you mean list?` and lists five
|
|
75
|
+
commands. Flags too: `--jounral`, `--dryrun`, `--forse` all resolve.
|
|
76
|
+
|
|
77
|
+
- **A flag that belongs to `proxy` is named as such.** `--http`, `--token`,
|
|
78
|
+
`--server` and `--log-level` are in the help page, and answering one of them
|
|
79
|
+
with "unknown flag" sent people hunting through that page for a flag already
|
|
80
|
+
in it.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **A wrapped server's own flags were read as ours.** `synartesis init db --
|
|
85
|
+
some-server --manifest audit.yaml` handed `--manifest` to the server
|
|
86
|
+
correctly and *also* took it, writing the policy to a path nobody asked for
|
|
87
|
+
-- on a machine where that path is real, one they already had. `positional`
|
|
88
|
+
and the unknown-flag check had always stopped at the bare `--`; the flag
|
|
89
|
+
reader had not.
|
|
90
|
+
|
|
91
|
+
- **The value of a flag was read as a flag.** `deny --reason "-see ticket 42"`
|
|
92
|
+
answered `unknown flag -see ticket 42`, and `prune --older-than -5` answered
|
|
93
|
+
`unknown flag -5` instead of the message that knows what `--older-than` is
|
|
94
|
+
for. Three separate lists had to agree about which flags take a value and
|
|
95
|
+
did not; there is one now, and a test holds it against the help page.
|
|
96
|
+
|
|
97
|
+
- **`undo --to 0` announced the session before refusing.** It printed "no
|
|
98
|
+
session named, so the most recent: a42bf93a" and only then rejected the
|
|
99
|
+
flag, which reads as though that session had been acted on.
|
|
100
|
+
|
|
101
|
+
- **A hint that could not be pasted.** `list --journal ./bench.db` named a
|
|
102
|
+
session, and the `show` it offered read the *default* journal and answered
|
|
103
|
+
"no run matches". Hints now carry `--journal` and `--manifest` when those
|
|
104
|
+
were not the obvious ones, and `undo --replan --dry-run` offers a command
|
|
105
|
+
that still replans.
|
|
106
|
+
|
|
107
|
+
- **Guessing at a typo was too eager.** A ceiling of half the word answered
|
|
108
|
+
`--server` with `--live` and `--token` with `--to`, neither of which anybody
|
|
109
|
+
meant. Two edits at most now, never for a word under three characters, and a
|
|
110
|
+
swapped pair counts as the one mistake it feels like -- which is what keeps
|
|
111
|
+
`shwo`, `pruen` and `--jounral` reachable at that ceiling.
|
|
112
|
+
|
|
113
|
+
- **Colour was decided for both streams from stdout**, so `synartesis nonsense
|
|
114
|
+
2> errors.log` in a terminal wrote escape sequences into the file.
|
|
115
|
+
|
|
116
|
+
### Performance
|
|
117
|
+
|
|
118
|
+
- **`newestUndoable` runs on every `list`** and could only be answered by
|
|
119
|
+
reading the action rows, which carry the snapshots -- so the cost of one
|
|
120
|
+
screen of sessions grew with the size of the data rather than the number of
|
|
121
|
+
sessions. A partial index over exactly the rows it wants fixes that, but only
|
|
122
|
+
when named: left to choose, sqlite takes `actions_gated` and reads the rows
|
|
123
|
+
anyway. Measured on a 246MB journal: **56ms to 0.01ms**.
|
|
124
|
+
|
|
5
125
|
## 0.6.13 — 2026-09-14
|
|
6
126
|
|
|
7
127
|
Four things, all of them from one comment by a stranger on Reddit who had
|
|
@@ -10,6 +130,26 @@ would have announced itself.
|
|
|
10
130
|
|
|
11
131
|
### Security
|
|
12
132
|
|
|
133
|
+
- **A server that would not start said nothing useful about why, and could
|
|
134
|
+
hang the proxy outright.** Two faults in the same few lines, found by hitting
|
|
135
|
+
the first one in real use.
|
|
136
|
+
|
|
137
|
+
What it printed was `code: 'MODULE_NOT_FOUND', requireStack: [], },
|
|
138
|
+
Node.js v22.16.0` -- the last four lines of node's output, which are the four
|
|
139
|
+
least informative. `Error: Cannot find module '/path/to/thing'` sits in the
|
|
140
|
+
middle and was thrown away, so the message named neither the file nor
|
|
141
|
+
anything to act on. A line that states a fault now leads, stack frames are
|
|
142
|
+
dropped, and the tail is only the fallback when nothing states one.
|
|
143
|
+
|
|
144
|
+
The second was worse and was found while testing the first: nothing read the
|
|
145
|
+
server's stderr until after the connection failed. A pipe nobody reads fills
|
|
146
|
+
at around 64kB and blocks the writer, so a server that said more than that
|
|
147
|
+
before dying never exited, the connect never rejected, and `check` sat there
|
|
148
|
+
for ever with no output at all. Measured against a server writing 2.4MB to
|
|
149
|
+
stderr: **killed at 30 seconds, against 0.8 seconds and the right error**.
|
|
150
|
+
Stderr is now read as it arrives, capped so a logging loop cannot make the
|
|
151
|
+
proxy the thing that runs out of memory.
|
|
152
|
+
|
|
13
153
|
- **The one remaining advisory is gone.** `esbuild` reached `pnpm audit`
|
|
14
154
|
through vite, in the renderer build only -- never in anything published, and
|
|
15
155
|
`--prod` was already clean. Pinned anyway, the same way as 0.6.12's three:
|
|
@@ -6,6 +6,19 @@ import {
|
|
|
6
6
|
describe
|
|
7
7
|
} from "./chunk-YVOO3PTV.js";
|
|
8
8
|
|
|
9
|
+
// src/proxy/flags.ts
|
|
10
|
+
var PROXY_FLAGS = [
|
|
11
|
+
"--manifest",
|
|
12
|
+
"--journal",
|
|
13
|
+
"--server",
|
|
14
|
+
"--gate-timeout",
|
|
15
|
+
"--log-level",
|
|
16
|
+
"--http",
|
|
17
|
+
"--http-host",
|
|
18
|
+
"--http-idle",
|
|
19
|
+
"--token"
|
|
20
|
+
];
|
|
21
|
+
|
|
9
22
|
// src/invocation.ts
|
|
10
23
|
import { spawnSync } from "child_process";
|
|
11
24
|
import { accessSync, constants } from "fs";
|
|
@@ -131,23 +144,25 @@ var INK = `${ESC}38;2;214;201;197m`;
|
|
|
131
144
|
var DIM = `${ESC}2m`;
|
|
132
145
|
var BOLD = `${ESC}1m`;
|
|
133
146
|
var RESET = `${ESC}0m`;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return enabled ? `${codes}${text}${RESET}` : text;
|
|
147
|
+
function usable(stream) {
|
|
148
|
+
return process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb" && stream.isTTY;
|
|
137
149
|
}
|
|
138
150
|
function spaced(text) {
|
|
139
151
|
return Array.from(text).join(" ");
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
};
|
|
153
|
+
function palette(on) {
|
|
154
|
+
const paint = (codes, text) => on ? `${codes}${text}${RESET}` : text;
|
|
155
|
+
return {
|
|
156
|
+
label: (text) => paint(ACCENT + DIM, spaced(text.toUpperCase())),
|
|
157
|
+
heading: (text) => paint(INK, text.toUpperCase()),
|
|
158
|
+
accent: (text) => paint(ACCENT, text),
|
|
159
|
+
strong: (text) => paint(INK, text),
|
|
160
|
+
quiet: (text) => paint(DIM, text),
|
|
161
|
+
plate: (text) => paint(ON_ACCENT + BOLD, ` ${text} `)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
var style = palette(usable(process.stdout));
|
|
165
|
+
var errorStyle = palette(usable(process.stderr));
|
|
151
166
|
var WORDMARK = spaced("SYNARTESIS");
|
|
152
167
|
function meander(width) {
|
|
153
168
|
const unit = "\u2517\u2501\u2513\u250F\u2501\u251B";
|
|
@@ -270,6 +285,34 @@ CREATE INDEX IF NOT EXISTS actions_gated ON actions(status, ts);
|
|
|
270
285
|
-- no meaning changes, IF NOT EXISTS makes it idempotent, and an older build
|
|
271
286
|
-- opening the same file afterwards neither notices nor cares.
|
|
272
287
|
CREATE INDEX IF NOT EXISTS actions_run_status ON actions(run_id, status);
|
|
288
|
+
|
|
289
|
+
-- Partial, which is what makes it small: newestUndoable asks for the newest run
|
|
290
|
+
-- holding something an undo would reverse, and that is a handful of rows out of
|
|
291
|
+
-- a table where every other row is a read, a refusal or something already put
|
|
292
|
+
-- back. Without it the question can only be answered by reaching into the table
|
|
293
|
+
-- to test inverse_json on every applied row -- and those rows carry the
|
|
294
|
+
-- snapshots, so the cost of the hint at the foot of a session list grew with
|
|
295
|
+
-- the size of the data rather than with the number of sessions, which is
|
|
296
|
+
-- exactly what the index above was added to stop. Measured on sixty runs of a
|
|
297
|
+
-- thousand actions with two-kilobyte snapshots, a 246MB journal: 56ms to
|
|
298
|
+
-- 0.01ms.
|
|
299
|
+
--
|
|
300
|
+
-- newestUndoable names this index with INDEXED BY, and has to: left to choose,
|
|
301
|
+
-- sqlite takes actions_gated for the status seek and then reads the rows. See
|
|
302
|
+
-- the query for why that is preferred to running ANALYZE.
|
|
303
|
+
--
|
|
304
|
+
-- Added the same way as the three above and for the same reason: no row
|
|
305
|
+
-- changes, no meaning changes, IF NOT EXISTS makes it idempotent, and an older
|
|
306
|
+
-- build opening the same file afterwards neither notices nor cares.
|
|
307
|
+
CREATE INDEX IF NOT EXISTS actions_undoable ON actions(run_id)
|
|
308
|
+
WHERE status = 'applied' AND inverse_json IS NOT NULL;
|
|
309
|
+
|
|
310
|
+
-- Partial for the same reason: findPending runs on the way in to every write,
|
|
311
|
+
-- looking for an earlier attempt at that exact call whose outcome was never
|
|
312
|
+
-- established. Those are rare, so the index holds almost nothing -- while the
|
|
313
|
+
-- rows it saves reading are the ones carrying the snapshots.
|
|
314
|
+
CREATE INDEX IF NOT EXISTS actions_unresolved ON actions(run_id, server, tool)
|
|
315
|
+
WHERE status = 'pending';
|
|
273
316
|
`;
|
|
274
317
|
|
|
275
318
|
// src/journal/journal.ts
|
|
@@ -681,6 +724,17 @@ var SqliteJournal = class {
|
|
|
681
724
|
return rows.find((row) => canonical(row.args) === wanted);
|
|
682
725
|
});
|
|
683
726
|
}
|
|
727
|
+
findPending(query) {
|
|
728
|
+
return this.#run("findPending", () => {
|
|
729
|
+
const rows = this.#db.prepare(
|
|
730
|
+
`SELECT * FROM actions INDEXED BY actions_unresolved
|
|
731
|
+
WHERE run_id = ? AND server = ? AND tool = ? AND status = 'pending'
|
|
732
|
+
ORDER BY seq`
|
|
733
|
+
).all(query.runId, query.server, query.tool).map(toAction);
|
|
734
|
+
const wanted = canonical(query.args ?? {});
|
|
735
|
+
return rows.find((row) => canonical(row.args) === wanted);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
684
738
|
getAction(actionId) {
|
|
685
739
|
return this.#run("getAction", () => {
|
|
686
740
|
const raw = this.#db.prepare("SELECT * FROM actions WHERE id = ?").get(actionId);
|
|
@@ -743,6 +797,33 @@ var SqliteJournal = class {
|
|
|
743
797
|
return tally;
|
|
744
798
|
});
|
|
745
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* The newest run that still holds something an undo would put back.
|
|
802
|
+
*
|
|
803
|
+
* Not simply the newest run, and not the newest run with an applied action
|
|
804
|
+
* either. A client that connects and reads opens a session like any other,
|
|
805
|
+
* and reads are recorded and left `applied` -- rollback steps past them --
|
|
806
|
+
* so `applied` alone regularly points at a session in which nothing
|
|
807
|
+
* happened. What makes a run worth offering is an action that is still
|
|
808
|
+
* applied and carries the inverse that would reverse it, which is the same
|
|
809
|
+
* test `undo` itself uses when it goes looking for the session somebody
|
|
810
|
+
* meant. That inverse is also what excludes the reads: the manifest loader
|
|
811
|
+
* refuses an inverse on a readonly tool, so a read can never carry one, and
|
|
812
|
+
* a second condition on the class would be a condition that cannot fire.
|
|
813
|
+
*/
|
|
814
|
+
newestUndoable() {
|
|
815
|
+
return this.#run("newestUndoable", () => {
|
|
816
|
+
const raw = this.#db.prepare(
|
|
817
|
+
`SELECT * FROM runs
|
|
818
|
+
WHERE id IN (SELECT run_id FROM actions INDEXED BY actions_undoable
|
|
819
|
+
WHERE status = 'applied'
|
|
820
|
+
AND inverse_json IS NOT NULL)
|
|
821
|
+
ORDER BY started_at DESC, rowid DESC
|
|
822
|
+
LIMIT 1`
|
|
823
|
+
).get();
|
|
824
|
+
return raw === void 0 ? void 0 : toRun(raw);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
746
827
|
recentActions(limit) {
|
|
747
828
|
return this.#run(
|
|
748
829
|
"recentActions",
|
|
@@ -1526,6 +1607,25 @@ function describeError(error) {
|
|
|
1526
1607
|
return error instanceof Error ? error.message : String(error);
|
|
1527
1608
|
}
|
|
1528
1609
|
var PROXY_CLIENT_INFO = { name: "synartesis-proxy", version: "0.0.0" };
|
|
1610
|
+
var STDERR_KEPT = 256 * 1024;
|
|
1611
|
+
var STDERR_SETTLE_MS = 200;
|
|
1612
|
+
function settled(stream, ms) {
|
|
1613
|
+
if (stream.readableEnded === true) {
|
|
1614
|
+
return Promise.resolve();
|
|
1615
|
+
}
|
|
1616
|
+
return new Promise((resolve2) => {
|
|
1617
|
+
const done = () => {
|
|
1618
|
+
clearTimeout(timer);
|
|
1619
|
+
resolve2();
|
|
1620
|
+
};
|
|
1621
|
+
const timer = setTimeout(done, ms);
|
|
1622
|
+
timer.unref?.();
|
|
1623
|
+
stream.on("end", done);
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
function isReadable(stream) {
|
|
1627
|
+
return typeof stream === "object" && stream !== null && "on" in stream && typeof stream.on === "function";
|
|
1628
|
+
}
|
|
1529
1629
|
function bufferedText(stream) {
|
|
1530
1630
|
if (typeof stream !== "object" || stream === null || !("read" in stream)) {
|
|
1531
1631
|
return "";
|
|
@@ -1534,14 +1634,26 @@ function bufferedText(stream) {
|
|
|
1534
1634
|
if (typeof read2 !== "function") {
|
|
1535
1635
|
return "";
|
|
1536
1636
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1637
|
+
let text = "";
|
|
1638
|
+
for (; ; ) {
|
|
1639
|
+
const chunk = read2.call(stream);
|
|
1640
|
+
if (typeof chunk === "string") {
|
|
1641
|
+
text += chunk;
|
|
1642
|
+
} else if (Buffer.isBuffer(chunk)) {
|
|
1643
|
+
text += chunk.toString("utf8");
|
|
1644
|
+
} else {
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1540
1647
|
}
|
|
1541
|
-
return
|
|
1648
|
+
return text;
|
|
1542
1649
|
}
|
|
1650
|
+
var NAMES_A_FAULT = /^(?:[A-Za-z]*(?:Error|Exception)\b|Cannot find |ENOENT\b|EACCES\b|EADDRINUSE\b|.*: command not found)/;
|
|
1543
1651
|
function lastWords(text) {
|
|
1544
|
-
const lines = text.split("\n").map((line) => line.
|
|
1652
|
+
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !/^at\s/.test(line));
|
|
1653
|
+
const named = lines.find((line) => NAMES_A_FAULT.test(line));
|
|
1654
|
+
if (named !== void 0) {
|
|
1655
|
+
return named;
|
|
1656
|
+
}
|
|
1545
1657
|
const kept = lines.slice(-4).join("; ");
|
|
1546
1658
|
return kept === "" ? void 0 : kept;
|
|
1547
1659
|
}
|
|
@@ -1573,11 +1685,22 @@ async function start(spec) {
|
|
|
1573
1685
|
});
|
|
1574
1686
|
const client = new Client({ ...PROXY_CLIENT_INFO });
|
|
1575
1687
|
let said = "";
|
|
1688
|
+
const stderr = transport.stderr;
|
|
1689
|
+
const listening = isReadable(stderr);
|
|
1690
|
+
if (listening) {
|
|
1691
|
+
stderr.on("data", (chunk) => {
|
|
1692
|
+
if (chunk !== void 0 && said.length < STDERR_KEPT) {
|
|
1693
|
+
said += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1576
1697
|
try {
|
|
1577
1698
|
await client.connect(transport);
|
|
1578
1699
|
} catch (error) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1700
|
+
if (listening) {
|
|
1701
|
+
await settled(stderr, STDERR_SETTLE_MS);
|
|
1702
|
+
}
|
|
1703
|
+
const reason = lastWords(said === "" ? bufferedText(transport.stderr) : said);
|
|
1581
1704
|
throw new UpstreamError(
|
|
1582
1705
|
spec.name,
|
|
1583
1706
|
"connect",
|
|
@@ -1741,6 +1864,7 @@ async function observeState(router, read2, signal) {
|
|
|
1741
1864
|
}
|
|
1742
1865
|
|
|
1743
1866
|
export {
|
|
1867
|
+
PROXY_FLAGS,
|
|
1744
1868
|
cliCommand,
|
|
1745
1869
|
cliCommandFrom,
|
|
1746
1870
|
proxyCommand,
|
|
@@ -1748,6 +1872,7 @@ export {
|
|
|
1748
1872
|
findManifest,
|
|
1749
1873
|
findJournal,
|
|
1750
1874
|
style,
|
|
1875
|
+
errorStyle,
|
|
1751
1876
|
WORDMARK,
|
|
1752
1877
|
rule,
|
|
1753
1878
|
mark,
|
|
@@ -1782,4 +1907,4 @@ export {
|
|
|
1782
1907
|
observeState,
|
|
1783
1908
|
connectStdioUpstream
|
|
1784
1909
|
};
|
|
1785
|
-
//# sourceMappingURL=chunk-
|
|
1910
|
+
//# sourceMappingURL=chunk-OODDXM36.js.map
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
IDEMPOTENCY_META_KEY,
|
|
4
4
|
NOTHING_RECORDED_YET,
|
|
5
|
+
PROXY_FLAGS,
|
|
5
6
|
WORDMARK,
|
|
6
7
|
banner,
|
|
7
8
|
canonical,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
createPolicyResolver,
|
|
11
12
|
createRouter,
|
|
12
13
|
describeStanding,
|
|
14
|
+
errorStyle,
|
|
13
15
|
findJournal,
|
|
14
16
|
findManifest,
|
|
15
17
|
labelFor,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
verifyAgainstServers,
|
|
35
37
|
warnUntested,
|
|
36
38
|
wasRefused
|
|
37
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-OODDXM36.js";
|
|
38
40
|
import {
|
|
39
41
|
DriftConflict,
|
|
40
42
|
ManifestError,
|
|
@@ -786,24 +788,6 @@ function verdict(inspection) {
|
|
|
786
788
|
return "nothing was recorded in this session";
|
|
787
789
|
}
|
|
788
790
|
|
|
789
|
-
// src/watch.ts
|
|
790
|
-
import { existsSync as existsSync2 } from "fs";
|
|
791
|
-
|
|
792
|
-
// src/keys.ts
|
|
793
|
-
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
794
|
-
function keysIn(chunk) {
|
|
795
|
-
const keys = [];
|
|
796
|
-
let at = 0;
|
|
797
|
-
while (at < chunk.length) {
|
|
798
|
-
const rest = chunk.slice(at);
|
|
799
|
-
const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
|
|
800
|
-
const key = sequence?.[0] ?? rest.slice(0, 1);
|
|
801
|
-
keys.push(key);
|
|
802
|
-
at += key.length;
|
|
803
|
-
}
|
|
804
|
-
return keys;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
791
|
// src/clock.ts
|
|
808
792
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
809
793
|
var pad = (value) => String(value).padStart(2, "0");
|
|
@@ -855,6 +839,169 @@ function ago(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
855
839
|
return `${String(Math.round(hours / 24))}d ago`;
|
|
856
840
|
}
|
|
857
841
|
|
|
842
|
+
// src/hints.ts
|
|
843
|
+
function hintsWanted() {
|
|
844
|
+
return process.env["SYNARTESIS_NO_HINTS"] === void 0;
|
|
845
|
+
}
|
|
846
|
+
function hintLine(hint2, paths = "") {
|
|
847
|
+
if (hint2.run === void 0) {
|
|
848
|
+
return ` ${style.quiet(hint2.why)}`;
|
|
849
|
+
}
|
|
850
|
+
return ` ${style.quiet(`${hint2.why}:`)} ${style.strong(`${cliCommand()} ${hint2.run}${paths}`)}`;
|
|
851
|
+
}
|
|
852
|
+
function short(id) {
|
|
853
|
+
return id.slice(0, 8);
|
|
854
|
+
}
|
|
855
|
+
function heldCalls(journal) {
|
|
856
|
+
const waiting = journal.listGated();
|
|
857
|
+
const [first, ...rest] = waiting;
|
|
858
|
+
if (first === void 0) {
|
|
859
|
+
return void 0;
|
|
860
|
+
}
|
|
861
|
+
if (rest.length === 0) {
|
|
862
|
+
return {
|
|
863
|
+
why: `${first.server}.${first.tool} is held, and the agent is waiting on you`,
|
|
864
|
+
run: `approve ${short(first.id)}`,
|
|
865
|
+
needs: ["journal"]
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
why: `${String(waiting.length)} calls are held, and the agent is waiting on you`,
|
|
870
|
+
run: "approve --all",
|
|
871
|
+
needs: ["journal"]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function whatChanged(journal) {
|
|
875
|
+
const run = journal.newestUndoable();
|
|
876
|
+
if (run === void 0) {
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
why: `${run.label ?? "an agent"} changed something ${ago(run.startedAt)}`,
|
|
881
|
+
run: `show ${short(run.id)}`,
|
|
882
|
+
needs: ["journal"]
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function notPinned(manifest) {
|
|
886
|
+
const pinned = Object.keys(manifest.pins ?? {});
|
|
887
|
+
const loose = Object.keys(manifest.servers).filter((name) => !pinned.includes(name));
|
|
888
|
+
if (loose.length === 0) {
|
|
889
|
+
return void 0;
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
why: pinned.length === 0 ? "no tool here is pinned, so a server that changes shape keeps its old policy" : `${loose.join(", ")} ${loose.length === 1 ? "is" : "are"} not pinned`,
|
|
893
|
+
run: "pin",
|
|
894
|
+
needs: ["manifest"]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var LEAVE_IT_RUNNING = {
|
|
898
|
+
why: "anything an agent holds will appear here as it happens",
|
|
899
|
+
run: "watch",
|
|
900
|
+
needs: ["journal"]
|
|
901
|
+
};
|
|
902
|
+
function afterStatus(journal) {
|
|
903
|
+
if (journal === void 0) {
|
|
904
|
+
return LEAVE_IT_RUNNING;
|
|
905
|
+
}
|
|
906
|
+
return firstOf(
|
|
907
|
+
() => heldCalls(journal),
|
|
908
|
+
() => whatChanged(journal),
|
|
909
|
+
() => LEAVE_IT_RUNNING
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
function firstOf(...candidates2) {
|
|
913
|
+
for (const candidate of candidates2) {
|
|
914
|
+
const hint2 = candidate();
|
|
915
|
+
if (hint2 !== void 0) {
|
|
916
|
+
return hint2;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
function didYouMean(typed, known) {
|
|
922
|
+
if (typed.length < 3) {
|
|
923
|
+
return void 0;
|
|
924
|
+
}
|
|
925
|
+
const ceiling = typed.length <= 4 ? 1 : 2;
|
|
926
|
+
let best;
|
|
927
|
+
let bestAt = ceiling + 1;
|
|
928
|
+
for (const candidate of known) {
|
|
929
|
+
const gap = distance(typed, candidate);
|
|
930
|
+
if (gap < bestAt) {
|
|
931
|
+
best = candidate;
|
|
932
|
+
bestAt = gap;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return bestAt <= ceiling ? best : void 0;
|
|
936
|
+
}
|
|
937
|
+
function distance(from, to) {
|
|
938
|
+
const rows = [];
|
|
939
|
+
for (let i = 0; i <= from.length; i += 1) {
|
|
940
|
+
const row = new Array(to.length + 1).fill(0);
|
|
941
|
+
row[0] = i;
|
|
942
|
+
rows.push(row);
|
|
943
|
+
}
|
|
944
|
+
const top = rows[0];
|
|
945
|
+
for (let j = 0; j <= to.length && top !== void 0; j += 1) {
|
|
946
|
+
top[j] = j;
|
|
947
|
+
}
|
|
948
|
+
for (let i = 1; i <= from.length; i += 1) {
|
|
949
|
+
for (let j = 1; j <= to.length; j += 1) {
|
|
950
|
+
const same = from[i - 1] === to[j - 1];
|
|
951
|
+
let best = Math.min(
|
|
952
|
+
(rows[i - 1]?.[j] ?? 0) + 1,
|
|
953
|
+
(rows[i]?.[j - 1] ?? 0) + 1,
|
|
954
|
+
(rows[i - 1]?.[j - 1] ?? 0) + (same ? 0 : 1)
|
|
955
|
+
);
|
|
956
|
+
if (i > 1 && j > 1 && from[i - 1] === to[j - 2] && from[i - 2] === to[j - 1]) {
|
|
957
|
+
best = Math.min(best, (rows[i - 2]?.[j - 2] ?? 0) + 1);
|
|
958
|
+
}
|
|
959
|
+
const row = rows[i];
|
|
960
|
+
if (row !== void 0) {
|
|
961
|
+
row[j] = best;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return rows[from.length]?.[to.length] ?? to.length;
|
|
966
|
+
}
|
|
967
|
+
var EVERYDAY = [
|
|
968
|
+
["install", "cover the servers your client already lists"],
|
|
969
|
+
["watch", "see what an agent does, and answer what it holds"],
|
|
970
|
+
["list", "the sessions recorded so far"],
|
|
971
|
+
["show", "what one session did, and whether it still holds"],
|
|
972
|
+
["undo", "put back what one of them changed"]
|
|
973
|
+
];
|
|
974
|
+
function shortList() {
|
|
975
|
+
const self = cliCommand();
|
|
976
|
+
return [
|
|
977
|
+
"",
|
|
978
|
+
...EVERYDAY.map(
|
|
979
|
+
([name, said]) => ` ${errorStyle.strong(`${self} ${name.padEnd(8)}`)} ${errorStyle.quiet(said)}`
|
|
980
|
+
),
|
|
981
|
+
"",
|
|
982
|
+
` ${errorStyle.quiet(`${self} --help for every command.`)}`,
|
|
983
|
+
""
|
|
984
|
+
].join("\n");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/watch.ts
|
|
988
|
+
import { existsSync as existsSync2 } from "fs";
|
|
989
|
+
|
|
990
|
+
// src/keys.ts
|
|
991
|
+
var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
|
|
992
|
+
function keysIn(chunk) {
|
|
993
|
+
const keys = [];
|
|
994
|
+
let at = 0;
|
|
995
|
+
while (at < chunk.length) {
|
|
996
|
+
const rest = chunk.slice(at);
|
|
997
|
+
const sequence = rest.startsWith("\x1B") ? SEQUENCE.exec(rest) : null;
|
|
998
|
+
const key = sequence?.[0] ?? rest.slice(0, 1);
|
|
999
|
+
keys.push(key);
|
|
1000
|
+
at += key.length;
|
|
1001
|
+
}
|
|
1002
|
+
return keys;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
858
1005
|
// src/describe.ts
|
|
859
1006
|
import { basename } from "path";
|
|
860
1007
|
var SUBJECT_KEYS = [
|
|
@@ -1513,8 +1660,8 @@ function pruneBackups(path) {
|
|
|
1513
1660
|
try {
|
|
1514
1661
|
const dir = dirname(path);
|
|
1515
1662
|
const prefix = `${basename2(path)}.synartesis-backup-`;
|
|
1516
|
-
const
|
|
1517
|
-
for (const name of
|
|
1663
|
+
const ours2 = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
|
|
1664
|
+
for (const name of ours2.slice(0, Math.max(0, ours2.length - KEEP_BACKUPS))) {
|
|
1518
1665
|
rmSync(join(dir, name), { force: true });
|
|
1519
1666
|
}
|
|
1520
1667
|
} catch {
|
|
@@ -2668,6 +2815,10 @@ it and is found from any directory inside it, the way a version control tool
|
|
|
2668
2815
|
finds its root; anything else lives in ~/.synartesis, which is where the
|
|
2669
2816
|
journal is too. Set SYNARTESIS_HOME to put that somewhere else.
|
|
2670
2817
|
|
|
2818
|
+
Most commands end by naming the one thing worth doing next, worked out from
|
|
2819
|
+
what is actually in the journal rather than from what was typed. Set
|
|
2820
|
+
SYNARTESIS_NO_HINTS to turn that off; --json never carries it.
|
|
2821
|
+
|
|
2671
2822
|
Exit codes: 0 complete, 1 halted or partial, 2 bad usage or configuration.
|
|
2672
2823
|
`;
|
|
2673
2824
|
var UsageError = class extends Error {
|
|
@@ -2685,25 +2836,37 @@ var UsageError = class extends Error {
|
|
|
2685
2836
|
this.listCommands = listCommands;
|
|
2686
2837
|
}
|
|
2687
2838
|
};
|
|
2839
|
+
function ours(argv) {
|
|
2840
|
+
const end = argv.indexOf("--");
|
|
2841
|
+
return end === -1 ? argv : argv.slice(0, end);
|
|
2842
|
+
}
|
|
2688
2843
|
function flag(argv, name) {
|
|
2689
|
-
const
|
|
2844
|
+
const mine = ours(argv);
|
|
2845
|
+
const at = mine.indexOf(name);
|
|
2690
2846
|
if (at === -1) {
|
|
2691
2847
|
return void 0;
|
|
2692
2848
|
}
|
|
2693
|
-
const value =
|
|
2849
|
+
const value = mine[at + 1];
|
|
2694
2850
|
if (value === void 0 || value.startsWith("--")) {
|
|
2695
2851
|
throw new UsageError(`${name} needs a value`);
|
|
2696
2852
|
}
|
|
2697
2853
|
return value;
|
|
2698
2854
|
}
|
|
2855
|
+
var TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
2856
|
+
"--manifest",
|
|
2857
|
+
"--journal",
|
|
2858
|
+
"--to",
|
|
2859
|
+
"--by",
|
|
2860
|
+
"--reason",
|
|
2861
|
+
"--older-than",
|
|
2862
|
+
"--client"
|
|
2863
|
+
]);
|
|
2699
2864
|
function positional(argv) {
|
|
2700
|
-
const skip = /* @__PURE__ */ new Set(["--manifest", "--journal", "--to", "--by", "--reason", "--gate-timeout", "--older-than", "--client"]);
|
|
2701
2865
|
const values = [];
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
if (skip.has(token)) {
|
|
2866
|
+
const mine = ours(argv);
|
|
2867
|
+
for (let i = 0; i < mine.length; i += 1) {
|
|
2868
|
+
const token = mine[i] ?? "";
|
|
2869
|
+
if (TAKES_VALUE.has(token)) {
|
|
2707
2870
|
i += 1;
|
|
2708
2871
|
continue;
|
|
2709
2872
|
}
|
|
@@ -2804,6 +2967,16 @@ async function runCheck(argv) {
|
|
|
2804
2967
|
out("");
|
|
2805
2968
|
out(` ${style.quiet("Anything not mentioned here is treated as irreversible and guarded.")}`);
|
|
2806
2969
|
out("");
|
|
2970
|
+
hint(
|
|
2971
|
+
firstOf(
|
|
2972
|
+
() => notPinned(manifest),
|
|
2973
|
+
() => ({
|
|
2974
|
+
why: "this is sound; to see which clients it covers",
|
|
2975
|
+
run: "status",
|
|
2976
|
+
needs: ["manifest", "journal"]
|
|
2977
|
+
})
|
|
2978
|
+
)
|
|
2979
|
+
);
|
|
2807
2980
|
return 0;
|
|
2808
2981
|
}
|
|
2809
2982
|
async function runInstall(argv) {
|
|
@@ -2997,6 +3170,9 @@ function runStatus(argv) {
|
|
|
2997
3170
|
waiting === 0 ? ` ${style.quiet("Everything found is covered.")}` : ` ${style.accent(`${String(waiting)} not covered.`)} ${style.quiet(`${cliCommand()} install covers them.`)}`
|
|
2998
3171
|
);
|
|
2999
3172
|
out("");
|
|
3173
|
+
if (waiting === 0) {
|
|
3174
|
+
hint(afterStatus(journal));
|
|
3175
|
+
}
|
|
3000
3176
|
} finally {
|
|
3001
3177
|
journal?.close();
|
|
3002
3178
|
}
|
|
@@ -3096,6 +3272,13 @@ function out(line2) {
|
|
|
3096
3272
|
process.stdout.write(`${line2}
|
|
3097
3273
|
`);
|
|
3098
3274
|
}
|
|
3275
|
+
function hint(candidate) {
|
|
3276
|
+
if (candidate === void 0 || !hintsWanted()) {
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
out(hintLine(candidate, pathArgs(candidate.needs)));
|
|
3280
|
+
out("");
|
|
3281
|
+
}
|
|
3099
3282
|
function runList(journal, asJson, journalPath) {
|
|
3100
3283
|
const runs = [...journal.listRuns()].reverse();
|
|
3101
3284
|
const tally2 = journal.tallyRuns();
|
|
@@ -3134,6 +3317,10 @@ function runList(journal, asJson, journalPath) {
|
|
|
3134
3317
|
out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
|
|
3135
3318
|
out("");
|
|
3136
3319
|
}
|
|
3320
|
+
hint(firstOf(
|
|
3321
|
+
() => heldCalls(journal),
|
|
3322
|
+
() => whatChanged(journal)
|
|
3323
|
+
));
|
|
3137
3324
|
return 0;
|
|
3138
3325
|
}
|
|
3139
3326
|
async function runShow(argv, journal, asJson) {
|
|
@@ -3231,6 +3418,16 @@ async function runShow(argv, journal, asJson) {
|
|
|
3231
3418
|
out(
|
|
3232
3419
|
` ${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")}`
|
|
3233
3420
|
);
|
|
3421
|
+
} else if (actions.some((action) => action.status === "applied" && action.inverse !== void 0)) {
|
|
3422
|
+
out("");
|
|
3423
|
+
hint({
|
|
3424
|
+
// Not a restatement of the verdict directly above it, which has just
|
|
3425
|
+
// said in its own words that nothing has moved. A hint that repeats
|
|
3426
|
+
// the line above is a line nobody reads twice.
|
|
3427
|
+
why: "to see the plan, before anything is written",
|
|
3428
|
+
run: `undo ${runId.slice(0, 8)} --dry-run`,
|
|
3429
|
+
needs: ["manifest", "journal"]
|
|
3430
|
+
});
|
|
3234
3431
|
}
|
|
3235
3432
|
} else {
|
|
3236
3433
|
out("");
|
|
@@ -3320,6 +3517,13 @@ function summarise2(actions) {
|
|
|
3320
3517
|
return `${String(actions.length)} actions: ${parts.join(", ")} | ${String(undoable)} with a recorded undo`;
|
|
3321
3518
|
}
|
|
3322
3519
|
var journalArg = "";
|
|
3520
|
+
var manifestArg = "";
|
|
3521
|
+
function pathArgs(needs) {
|
|
3522
|
+
if (needs === void 0) {
|
|
3523
|
+
return "";
|
|
3524
|
+
}
|
|
3525
|
+
return needs.map((what) => what === "journal" ? journalArg : manifestArg).join("");
|
|
3526
|
+
}
|
|
3323
3527
|
var PRUNE_NAG_BYTES = 100 * 1024 * 1024;
|
|
3324
3528
|
function bytesOf(path) {
|
|
3325
3529
|
try {
|
|
@@ -3398,6 +3602,10 @@ function runClose(argv, journal) {
|
|
|
3398
3602
|
out("");
|
|
3399
3603
|
out(` ${style.quiet("nothing is open; every run has ended cleanly")}`);
|
|
3400
3604
|
out("");
|
|
3605
|
+
hint(firstOf(
|
|
3606
|
+
() => heldCalls(journal),
|
|
3607
|
+
() => whatChanged(journal)
|
|
3608
|
+
));
|
|
3401
3609
|
return 0;
|
|
3402
3610
|
}
|
|
3403
3611
|
const run = given === void 0 ? pick(active, void 0, RUN, true) : pick([...all].reverse(), given, RUN, true);
|
|
@@ -3419,6 +3627,10 @@ function runGates(journal, asJson) {
|
|
|
3419
3627
|
out("");
|
|
3420
3628
|
out(` ${style.quiet("Nothing is waiting for a decision.")}`);
|
|
3421
3629
|
out("");
|
|
3630
|
+
hint(firstOf(
|
|
3631
|
+
() => whatChanged(journal),
|
|
3632
|
+
() => LEAVE_IT_RUNNING
|
|
3633
|
+
));
|
|
3422
3634
|
return 0;
|
|
3423
3635
|
}
|
|
3424
3636
|
out("");
|
|
@@ -3447,10 +3659,10 @@ function runDecision(argv, journal, approving) {
|
|
|
3447
3659
|
const by = flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown";
|
|
3448
3660
|
const reason = flag(argv, "--reason") ?? "denied by operator";
|
|
3449
3661
|
if (given !== void 0) {
|
|
3450
|
-
const
|
|
3451
|
-
if (
|
|
3662
|
+
const settled2 = journal.getAction(given);
|
|
3663
|
+
if (settled2 !== void 0 && settled2.status !== "gated") {
|
|
3452
3664
|
process.stderr.write(
|
|
3453
|
-
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(
|
|
3665
|
+
`synartesis: ${given} is no longer awaiting approval (it is ${labelFor(settled2)})
|
|
3454
3666
|
`
|
|
3455
3667
|
);
|
|
3456
3668
|
return 1;
|
|
@@ -3462,6 +3674,7 @@ function runDecision(argv, journal, approving) {
|
|
|
3462
3674
|
return 0;
|
|
3463
3675
|
}
|
|
3464
3676
|
let failed = 0;
|
|
3677
|
+
let settled = 0;
|
|
3465
3678
|
for (const action of targets) {
|
|
3466
3679
|
const changed = approving ? journal.approve(action.id, by) : journal.deny(action.id, by, reason);
|
|
3467
3680
|
if (!changed) {
|
|
@@ -3473,13 +3686,23 @@ function runDecision(argv, journal, approving) {
|
|
|
3473
3686
|
failed += 1;
|
|
3474
3687
|
continue;
|
|
3475
3688
|
}
|
|
3689
|
+
settled += 1;
|
|
3476
3690
|
out(
|
|
3477
3691
|
` ${style.accent(approving ? "approved" : "denied")} ${style.strong(`${action.server}.${action.tool}`)} ${style.quiet(action.id)}`
|
|
3478
3692
|
);
|
|
3479
3693
|
}
|
|
3694
|
+
if (settled > 0) {
|
|
3695
|
+
out("");
|
|
3696
|
+
hint(
|
|
3697
|
+
firstOf(
|
|
3698
|
+
() => heldCalls(journal),
|
|
3699
|
+
() => approving ? { why: "the agent can make that call again now, and it will go through" } : { why: "the call was refused; the agent is told, and decides what to do next" }
|
|
3700
|
+
)
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3480
3703
|
return failed === 0 ? 0 : 1;
|
|
3481
3704
|
}
|
|
3482
|
-
function report(result, alreadyForcing = false) {
|
|
3705
|
+
function report(result, alreadyForcing = false, as = "") {
|
|
3483
3706
|
out("");
|
|
3484
3707
|
out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
|
|
3485
3708
|
out(` ${rule(72)}`);
|
|
@@ -3552,6 +3775,13 @@ function report(result, alreadyForcing = false) {
|
|
|
3552
3775
|
` ${style.label("result")} ${result.status === "rolled_back" ? result.status : style.accent(result.status)}`
|
|
3553
3776
|
);
|
|
3554
3777
|
out("");
|
|
3778
|
+
if (result.dryRun && result.status === "rolled_back") {
|
|
3779
|
+
hint({
|
|
3780
|
+
why: "nothing was written; to do exactly this for real",
|
|
3781
|
+
run: `undo ${result.runId.slice(0, 8)}${as}`,
|
|
3782
|
+
needs: ["manifest", "journal"]
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3555
3785
|
return result.status === "rolled_back" ? 0 : 1;
|
|
3556
3786
|
}
|
|
3557
3787
|
async function withUpstreams(manifestPath, use, only) {
|
|
@@ -3612,6 +3842,11 @@ async function performUndo(manifestPath, journal, runId, options) {
|
|
|
3612
3842
|
);
|
|
3613
3843
|
}
|
|
3614
3844
|
async function runUndo(argv, journal) {
|
|
3845
|
+
const rawTo = flag(argv, "--to");
|
|
3846
|
+
const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
|
|
3847
|
+
if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
|
|
3848
|
+
throw new UsageError("--to needs a positive whole number");
|
|
3849
|
+
}
|
|
3615
3850
|
const given = positional(argv)[1];
|
|
3616
3851
|
const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
|
|
3617
3852
|
const runId = chosen.id;
|
|
@@ -3647,11 +3882,6 @@ async function runUndo(argv, journal) {
|
|
|
3647
3882
|
return 0;
|
|
3648
3883
|
}
|
|
3649
3884
|
}
|
|
3650
|
-
const rawTo = flag(argv, "--to");
|
|
3651
|
-
const toSeq = rawTo === void 0 ? void 0 : Number(rawTo);
|
|
3652
|
-
if (toSeq !== void 0 && (!Number.isInteger(toSeq) || toSeq < 1)) {
|
|
3653
|
-
throw new UsageError("--to needs a positive whole number");
|
|
3654
|
-
}
|
|
3655
3885
|
if (toSeq !== void 0) {
|
|
3656
3886
|
const highest = journal.getActions(runId).reduce((top, action) => Math.max(top, action.seq), 0);
|
|
3657
3887
|
if (toSeq > highest) {
|
|
@@ -3695,16 +3925,42 @@ async function runUndo(argv, journal) {
|
|
|
3695
3925
|
return 1;
|
|
3696
3926
|
}
|
|
3697
3927
|
}
|
|
3928
|
+
const replan = argv.includes("--replan");
|
|
3698
3929
|
return report(
|
|
3699
3930
|
await performUndo(manifestPath, journal, runId, {
|
|
3700
3931
|
dryRun: argv.includes("--dry-run"),
|
|
3701
3932
|
...toSeq === void 0 ? {} : { toSeq },
|
|
3702
|
-
replan
|
|
3933
|
+
replan,
|
|
3703
3934
|
...forcing && said ? { force: true } : {}
|
|
3704
3935
|
}),
|
|
3705
|
-
forcing
|
|
3936
|
+
forcing,
|
|
3937
|
+
// --to is deliberately absent, and cannot reach here: a floor leaves
|
|
3938
|
+
// actions below it alone, which makes the result `partial`, and the hint
|
|
3939
|
+
// is only offered on `rolled_back`.
|
|
3940
|
+
`${replan ? " --replan" : ""}${forcing && said ? " --force --yes" : ""}`
|
|
3706
3941
|
);
|
|
3707
3942
|
}
|
|
3943
|
+
var KNOWN_COMMANDS = [
|
|
3944
|
+
"install",
|
|
3945
|
+
"uninstall",
|
|
3946
|
+
"status",
|
|
3947
|
+
"init",
|
|
3948
|
+
"check",
|
|
3949
|
+
"pin",
|
|
3950
|
+
"list",
|
|
3951
|
+
"show",
|
|
3952
|
+
"gates",
|
|
3953
|
+
"close",
|
|
3954
|
+
"prune",
|
|
3955
|
+
"proxy",
|
|
3956
|
+
"desktop",
|
|
3957
|
+
"watch",
|
|
3958
|
+
"approve",
|
|
3959
|
+
"deny",
|
|
3960
|
+
"undo",
|
|
3961
|
+
"help",
|
|
3962
|
+
"version"
|
|
3963
|
+
];
|
|
3708
3964
|
var FLAGS = /* @__PURE__ */ new Set([
|
|
3709
3965
|
// Two lists have to agree about a flag: this one decides whether it is
|
|
3710
3966
|
// accepted at all, and the skip set in positional() decides whether its
|
|
@@ -3743,13 +3999,26 @@ function version() {
|
|
|
3743
3999
|
return "unknown";
|
|
3744
4000
|
}
|
|
3745
4001
|
}
|
|
4002
|
+
function unknownCommand(typed) {
|
|
4003
|
+
const meant = didYouMean(typed, KNOWN_COMMANDS);
|
|
4004
|
+
return meant === void 0 ? `unknown command ${typed}` : `unknown command ${typed}; did you mean ${meant}?`;
|
|
4005
|
+
}
|
|
3746
4006
|
function rejectUnknownFlags(argv) {
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
4007
|
+
const mine = ours(argv);
|
|
4008
|
+
for (let i = 0; i < mine.length; i += 1) {
|
|
4009
|
+
const token = mine[i] ?? "";
|
|
4010
|
+
if (TAKES_VALUE.has(token)) {
|
|
4011
|
+
i += 1;
|
|
4012
|
+
continue;
|
|
3750
4013
|
}
|
|
3751
4014
|
if (token.startsWith("-") && token !== "-" && !FLAGS.has(token)) {
|
|
3752
|
-
|
|
4015
|
+
if (PROXY_FLAGS.includes(token)) {
|
|
4016
|
+
throw new UsageError(`${token} is a flag for ${cliCommand()} proxy, not for this`);
|
|
4017
|
+
}
|
|
4018
|
+
const meant = didYouMean(token, [...FLAGS]);
|
|
4019
|
+
throw new UsageError(
|
|
4020
|
+
meant === void 0 ? `unknown flag ${token}` : `unknown flag ${token}; did you mean ${meant}?`
|
|
4021
|
+
);
|
|
3753
4022
|
}
|
|
3754
4023
|
}
|
|
3755
4024
|
}
|
|
@@ -3779,6 +4048,13 @@ ${COMMANDS}`);
|
|
|
3779
4048
|
return 0;
|
|
3780
4049
|
}
|
|
3781
4050
|
rejectUnknownFlags(argv);
|
|
4051
|
+
if (command !== void 0 && !KNOWN_COMMANDS.includes(command)) {
|
|
4052
|
+
throw new UsageError(unknownCommand(command));
|
|
4053
|
+
}
|
|
4054
|
+
const givenJournal = flag(argv, "--journal");
|
|
4055
|
+
const givenManifest = flag(argv, "--manifest");
|
|
4056
|
+
journalArg = givenJournal === void 0 ? "" : ` --journal ${resolve3(givenJournal)}`;
|
|
4057
|
+
manifestArg = givenManifest === void 0 ? "" : ` --manifest ${resolve3(givenManifest)}`;
|
|
3782
4058
|
if (command === void 0) {
|
|
3783
4059
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3784
4060
|
const journalPath2 = findJournal(flag(argv, "--journal"), manifestPath);
|
|
@@ -3847,7 +4123,6 @@ ${COMMANDS}`);
|
|
|
3847
4123
|
decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown"
|
|
3848
4124
|
});
|
|
3849
4125
|
}
|
|
3850
|
-
journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
|
|
3851
4126
|
if (!existsSync8(journalPath) && (command === "list" || command === "show" || command === "gates")) {
|
|
3852
4127
|
if (asJson) {
|
|
3853
4128
|
out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
|
|
@@ -3885,7 +4160,7 @@ ${COMMANDS}`);
|
|
|
3885
4160
|
case "undo":
|
|
3886
4161
|
return await runUndo(argv, journal);
|
|
3887
4162
|
default:
|
|
3888
|
-
throw new UsageError(
|
|
4163
|
+
throw new UsageError(unknownCommand(command));
|
|
3889
4164
|
}
|
|
3890
4165
|
} finally {
|
|
3891
4166
|
journal.close();
|
|
@@ -3912,8 +4187,7 @@ try {
|
|
|
3912
4187
|
if (error instanceof UsageError) {
|
|
3913
4188
|
process.stderr.write(
|
|
3914
4189
|
error.listCommands ? `synartesis: ${error.message}
|
|
3915
|
-
|
|
3916
|
-
${COMMANDS}` : `synartesis: ${error.message}
|
|
4190
|
+
${shortList()}` : `synartesis: ${error.message}
|
|
3917
4191
|
`
|
|
3918
4192
|
);
|
|
3919
4193
|
process.exitCode = 2;
|
package/dist/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
PROXY_FLAGS,
|
|
3
4
|
canonical,
|
|
4
5
|
cliCommandFrom,
|
|
5
6
|
connectStdioUpstream,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
verifyAgainstServers,
|
|
23
24
|
warnUntested,
|
|
24
25
|
withIdempotencyKey
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-OODDXM36.js";
|
|
26
27
|
import {
|
|
27
28
|
SnapshotError,
|
|
28
29
|
UpstreamError,
|
|
@@ -585,14 +586,21 @@ function createProxyServer(options) {
|
|
|
585
586
|
enter();
|
|
586
587
|
try {
|
|
587
588
|
const wantsGate = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
|
|
588
|
-
const
|
|
589
|
+
const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
|
|
590
|
+
runId: activeRun,
|
|
591
|
+
server: route.upstream.name,
|
|
592
|
+
tool: route.tool,
|
|
593
|
+
args
|
|
594
|
+
});
|
|
595
|
+
const mustAsk = wantsGate || unresolved !== void 0;
|
|
596
|
+
const granted = mustAsk ? journal.findApproval({
|
|
589
597
|
server: route.upstream.name,
|
|
590
598
|
tool: route.tool,
|
|
591
599
|
args,
|
|
592
600
|
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
593
601
|
}) : void 0;
|
|
594
602
|
const inherited = granted !== void 0 && granted.runId !== activeRun ? granted : void 0;
|
|
595
|
-
const waiting = granted === void 0 &&
|
|
603
|
+
const waiting = granted === void 0 && mustAsk ? journal.findGated({
|
|
596
604
|
runId: activeRun,
|
|
597
605
|
server: route.upstream.name,
|
|
598
606
|
tool: route.tool,
|
|
@@ -676,9 +684,13 @@ function createProxyServer(options) {
|
|
|
676
684
|
);
|
|
677
685
|
}
|
|
678
686
|
};
|
|
679
|
-
const askedAlready =
|
|
680
|
-
if (
|
|
681
|
-
|
|
687
|
+
const askedAlready = mustAsk;
|
|
688
|
+
if (mustAsk && (granted === void 0 || !spent)) {
|
|
689
|
+
const because = [
|
|
690
|
+
unresolved === void 0 ? void 0 : `an earlier attempt at this exact call, action ${String(unresolved.seq)}, went out and its outcome could never be established, so sending it again may apply the change a second time`,
|
|
691
|
+
wantsGate ? "this action cannot be undone" : void 0
|
|
692
|
+
].filter((one) => one !== void 0);
|
|
693
|
+
await decide(because.join(", and "));
|
|
682
694
|
}
|
|
683
695
|
let snapshot;
|
|
684
696
|
let verify;
|
|
@@ -1062,7 +1074,7 @@ function parseArgv(argv) {
|
|
|
1062
1074
|
const at = argv.indexOf(flag);
|
|
1063
1075
|
return at === -1 ? void 0 : argv[at + 1];
|
|
1064
1076
|
};
|
|
1065
|
-
const known =
|
|
1077
|
+
const known = PROXY_FLAGS;
|
|
1066
1078
|
const unknown = argv.find((token) => token.startsWith("--") && !known.includes(token));
|
|
1067
1079
|
if (unknown !== void 0) {
|
|
1068
1080
|
throw new Error(`unknown flag ${unknown}; expected one of ${known.join(", ")}`);
|