synartesis 0.9.1 → 0.9.2
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 +48 -0
- package/README.md +43 -17
- package/dist/{chunk-H4SPMXQW.js → chunk-MTPUG26S.js} +85 -7
- package/dist/cli.js +160 -14
- package/dist/proxy.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.9.2 — 2026-09-24
|
|
6
|
+
|
|
7
|
+
For someone who has never used a terminal. Every screen was walked through as a
|
|
8
|
+
new user would meet it, in a real terminal, from a fresh machine. Each place
|
|
9
|
+
they would have got stuck is fixed.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`synartesis clean`**: one command, no questions to answer first. It clears
|
|
14
|
+
every finished session with nothing left to undo, and closes sessions left
|
|
15
|
+
open by an app that has since exited. It keeps anything you could still undo,
|
|
16
|
+
anything waiting for you, and anything whose outcome is unknown. Your
|
|
17
|
+
connected apps and your policy are not touched. It shows what it will do and
|
|
18
|
+
asks once. On a journal three weeks old it cleared 119 of 126 sessions.
|
|
19
|
+
`list` suggests it once there is clutter worth clearing.
|
|
20
|
+
- Sessions now record which process opened them, so one left open by an app
|
|
21
|
+
that crashed or was force-quit can be told apart from one still in use.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **The first screen was a dead end.** `synartesis` on a fresh machine said
|
|
26
|
+
"point your client at it" and accepted no keys. It now opens on the AI apps
|
|
27
|
+
found and their tools: `a` covers all of them, `enter` covers one. When the
|
|
28
|
+
first session arrives it switches to the sessions by itself.
|
|
29
|
+
- **`watch` ignored keys.** With nothing waiting it showed no keys at all, not
|
|
30
|
+
even how to quit, and silently swallowed every key pressed. It always shows
|
|
31
|
+
its keys now, says so when there is nothing to approve yet, and answers a key
|
|
32
|
+
that does nothing instead of ignoring it. The console does the same.
|
|
33
|
+
- **Opening `synartesis` because of a notification landed on the wrong
|
|
34
|
+
screen.** Approving was a screen away, behind a key labelled "held", so `a`
|
|
35
|
+
did nothing. It opens on the waiting call now, shows why it was held, and
|
|
36
|
+
flags new ones from any other screen.
|
|
37
|
+
- **The notification said to type an approve command** with an id and an
|
|
38
|
+
absolute path, cut off before its end. It now says to open a terminal and run
|
|
39
|
+
`synartesis`.
|
|
40
|
+
- After covering apps from the screen, it names which apps to quit and reopen,
|
|
41
|
+
and says why any server was not covered. It used to say "see the reasons
|
|
42
|
+
above" on a screen with nothing above.
|
|
43
|
+
- The proxy wrote a routine schema note into the client's log as a warning, once
|
|
44
|
+
per tool on every start (fourteen for the filesystem server). It is debug
|
|
45
|
+
level now.
|
|
46
|
+
|
|
47
|
+
### Docs
|
|
48
|
+
|
|
49
|
+
- The README's Install section is three steps for somebody who has never used a
|
|
50
|
+
terminal, then what to do when something needs your OK, how to undo, how to
|
|
51
|
+
clean up, and how to take it all out.
|
|
52
|
+
|
|
5
53
|
## 0.9.1 — 2026-09-24
|
|
6
54
|
|
|
7
55
|
This release attacks undo instead of describing it. The new tests generate the
|
package/README.md
CHANGED
|
@@ -92,23 +92,48 @@ and `--replan`, or `--force` to overwrite deliberately.
|
|
|
92
92
|
---
|
|
93
93
|
## Install
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
**Three steps, if you have never used a terminal.** Open the Terminal app
|
|
96
|
+
(on a Mac: press ⌘ Space, type *Terminal*, press Return), then:
|
|
97
|
+
|
|
98
|
+
1. If `npm --version` says *command not found*, install Node.js from
|
|
99
|
+
[nodejs.org](https://nodejs.org) first (the LTS button), and open a new
|
|
100
|
+
Terminal window.
|
|
101
|
+
2. Install Synartesis:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm install -g synartesis
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
3. Start it:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
synartesis
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
It lists the AI apps on your computer and the tools they use. Press **a** to
|
|
114
|
+
cover all of them, then **quit and reopen** those apps (it names them).
|
|
115
|
+
|
|
116
|
+
That is the whole setup. From then on:
|
|
117
|
+
|
|
118
|
+
- **Something needs your OK.** A notification says a call is waiting. Open
|
|
119
|
+
Terminal, type `synartesis`: it opens on that call. **a** approves, **d**
|
|
120
|
+
denies, **A** approves and stops asking about that tool for an hour.
|
|
121
|
+
- **Undo what an agent did.** Type `synartesis`, press **r** if it is showing
|
|
122
|
+
waiting calls, move to the session with the arrow keys, press **u**, then
|
|
123
|
+
**y**. Press **p** first to see what it would do without changing anything.
|
|
124
|
+
- **Too many old sessions.** `synartesis clean` clears the finished ones and
|
|
125
|
+
keeps anything you could still undo or still need to decide.
|
|
126
|
+
- **Take it all back out.** `synartesis uninstall` restores every app's
|
|
127
|
+
settings exactly as they were.
|
|
128
|
+
|
|
129
|
+
The same thing without the screen: `synartesis install`. It finds what Claude
|
|
130
|
+
Code, Claude Desktop, Cursor, Codex, Gemini CLI, Copilot CLI, Antigravity or
|
|
131
|
+
Devin Desktop (Windsurf) already list, writes one policy covering all of it,
|
|
132
|
+
and points each entry at the proxy. Servers it recognises get the policy that
|
|
133
|
+
ships for them and work immediately; the rest are drafted with every tool held
|
|
134
|
+
until you say how to undo it. Your config is copied aside first,
|
|
135
|
+
`synartesis uninstall` puts it back, and `synartesis status` says what is
|
|
136
|
+
covered.
|
|
112
137
|
|
|
113
138
|
Each server keeps its own entry and its own proxy, so **no tool is renamed** —
|
|
114
139
|
the agent sees exactly the names it saw before. **Your agent needs nothing
|
|
@@ -487,6 +512,7 @@ get it if it is not installed.
|
|
|
487
512
|
| `undo <id> --replan` | Rebuild each undo from the current manifest |
|
|
488
513
|
| `undo <id> --force [--yes]` | Print what it would write over; `--yes` goes ahead |
|
|
489
514
|
| `watch` | Live activity, with approvals answerable in place |
|
|
515
|
+
| `clean` | Clear finished sessions; keep anything you could undo or still need to decide |
|
|
490
516
|
| `prune` | Delete sessions older than 30 days and reclaim the space |
|
|
491
517
|
| `close [id]` | End a session a killed proxy left open |
|
|
492
518
|
|
|
@@ -31,10 +31,10 @@ function shown(name) {
|
|
|
31
31
|
function unmarked(text) {
|
|
32
32
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
33
33
|
}
|
|
34
|
-
function words(notice) {
|
|
34
|
+
function words(notice, open) {
|
|
35
35
|
return {
|
|
36
36
|
title: "Synartesis: a call is waiting for you",
|
|
37
|
-
body: `${shown(notice.server)}.${shown(notice.tool)} -- ${notice.approve}`
|
|
37
|
+
body: `${shown(notice.server)}.${shown(notice.tool)} -- open a terminal and run: ${open ?? notice.approve}`
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
var KILL_AFTER_MS = 5e3;
|
|
@@ -51,13 +51,13 @@ function launch(command, args) {
|
|
|
51
51
|
} catch {
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
function desktopNotifier(env = process.env, os = platform()) {
|
|
54
|
+
function desktopNotifier(env = process.env, os = platform(), open) {
|
|
55
55
|
if (env["SYNARTESIS_NOTIFY"] === "0") {
|
|
56
56
|
return SILENT;
|
|
57
57
|
}
|
|
58
58
|
if (os === "darwin") {
|
|
59
59
|
return (notice) => {
|
|
60
|
-
const { title, body } = words(notice);
|
|
60
|
+
const { title, body } = words(notice, open);
|
|
61
61
|
launch("osascript", [
|
|
62
62
|
"-e",
|
|
63
63
|
"on run argv",
|
|
@@ -73,7 +73,7 @@ function desktopNotifier(env = process.env, os = platform()) {
|
|
|
73
73
|
}
|
|
74
74
|
if (os === "linux") {
|
|
75
75
|
return (notice) => {
|
|
76
|
-
const { title, body } = words(notice);
|
|
76
|
+
const { title, body } = words(notice, open);
|
|
77
77
|
launch("notify-send", ["--app-name=Synartesis", "--", title, unmarked(body)]);
|
|
78
78
|
};
|
|
79
79
|
}
|
|
@@ -439,6 +439,19 @@ CREATE TABLE IF NOT EXISTS allows (
|
|
|
439
439
|
);
|
|
440
440
|
CREATE INDEX IF NOT EXISTS allows_current ON allows(server, tool, until);
|
|
441
441
|
|
|
442
|
+
-- Which process opened each session, so a session left open by an app that
|
|
443
|
+
-- has since exited can be told from one still in use. Without it, closing an
|
|
444
|
+
-- abandoned session was a person's guess (synartesis close), and every
|
|
445
|
+
-- session a crashed or force-quit app left behind stayed "still open" in the
|
|
446
|
+
-- list for ever. host and pid for the reason leases have both.
|
|
447
|
+
--
|
|
448
|
+
-- Not a schema version bump, for the reason the tables above are not.
|
|
449
|
+
CREATE TABLE IF NOT EXISTS run_owners (
|
|
450
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(id),
|
|
451
|
+
host TEXT NOT NULL,
|
|
452
|
+
pid INTEGER NOT NULL
|
|
453
|
+
);
|
|
454
|
+
|
|
442
455
|
CREATE INDEX IF NOT EXISTS actions_by_run ON actions(run_id, seq);
|
|
443
456
|
|
|
444
457
|
-- Deliberately not a schema version bump. Adding an index changes no row and
|
|
@@ -752,7 +765,10 @@ var SqliteJournal = class {
|
|
|
752
765
|
beginRun(label) {
|
|
753
766
|
const id = crypto.randomUUID();
|
|
754
767
|
this.#run("beginRun", () => {
|
|
755
|
-
this.#db.
|
|
768
|
+
this.#db.transaction(() => {
|
|
769
|
+
this.#db.prepare("INSERT INTO runs (id, label, started_at, status) VALUES (?, ?, ?, 'active')").run(id, label ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
770
|
+
this.#db.prepare("INSERT INTO run_owners (run_id, host, pid) VALUES (?, ?, ?)").run(id, hostname(), process.pid);
|
|
771
|
+
})();
|
|
756
772
|
});
|
|
757
773
|
return id;
|
|
758
774
|
}
|
|
@@ -1230,6 +1246,64 @@ var SqliteJournal = class {
|
|
|
1230
1246
|
close() {
|
|
1231
1247
|
this.#db.close();
|
|
1232
1248
|
}
|
|
1249
|
+
cleanable(idleBefore) {
|
|
1250
|
+
return this.#run("cleanable", () => {
|
|
1251
|
+
const here = hostname();
|
|
1252
|
+
const open = z.array(
|
|
1253
|
+
z.object({
|
|
1254
|
+
id: z.string(),
|
|
1255
|
+
host: z.string().nullable(),
|
|
1256
|
+
pid: z.number().nullable(),
|
|
1257
|
+
last: z.string()
|
|
1258
|
+
})
|
|
1259
|
+
).parse(
|
|
1260
|
+
this.#db.prepare(
|
|
1261
|
+
`SELECT r.id, o.host, o.pid,
|
|
1262
|
+
COALESCE((SELECT MAX(a.ts) FROM actions a WHERE a.run_id = r.id), r.started_at) AS last
|
|
1263
|
+
FROM runs r LEFT JOIN run_owners o ON o.run_id = r.id
|
|
1264
|
+
WHERE r.status = 'active'`
|
|
1265
|
+
).all()
|
|
1266
|
+
);
|
|
1267
|
+
const abandoned = open.filter(
|
|
1268
|
+
(run) => run.pid === null ? run.last < idleBefore : (
|
|
1269
|
+
// Only a process on this machine can be asked about; one opened
|
|
1270
|
+
// elsewhere on a shared journal is left alone.
|
|
1271
|
+
run.host === here && !running(run.pid)
|
|
1272
|
+
)
|
|
1273
|
+
).map((run) => run.id);
|
|
1274
|
+
const closing = new Set(abandoned);
|
|
1275
|
+
const finished = z.array(
|
|
1276
|
+
z.object({
|
|
1277
|
+
id: z.string(),
|
|
1278
|
+
label: z.string().nullable(),
|
|
1279
|
+
status: z.enum(["active", "complete", "rolled_back", "partial"]),
|
|
1280
|
+
at: z.string(),
|
|
1281
|
+
actions: z.number()
|
|
1282
|
+
})
|
|
1283
|
+
).parse(
|
|
1284
|
+
this.#db.prepare(
|
|
1285
|
+
`SELECT r.id, r.label, r.status, COALESCE(r.ended_at, r.started_at) AS at,
|
|
1286
|
+
(SELECT COUNT(*) FROM actions a WHERE a.run_id = r.id) AS actions
|
|
1287
|
+
FROM runs r
|
|
1288
|
+
WHERE NOT EXISTS (
|
|
1289
|
+
SELECT 1 FROM actions a
|
|
1290
|
+
WHERE a.run_id = r.id
|
|
1291
|
+
AND a.status IN ('pending','gated','approved','rolling_back','unrecoverable'))
|
|
1292
|
+
AND NOT EXISTS (
|
|
1293
|
+
SELECT 1 FROM actions a
|
|
1294
|
+
WHERE a.run_id = r.id AND a.status = 'applied' AND a.inverse_json IS NOT NULL)
|
|
1295
|
+
ORDER BY at, r.rowid`
|
|
1296
|
+
).all()
|
|
1297
|
+
).filter((run) => run.status !== "active" || closing.has(run.id)).map((run) => ({
|
|
1298
|
+
id: run.id,
|
|
1299
|
+
label: run.label ?? void 0,
|
|
1300
|
+
at: run.at,
|
|
1301
|
+
status: run.status === "active" ? "complete" : run.status,
|
|
1302
|
+
actions: run.actions
|
|
1303
|
+
}));
|
|
1304
|
+
return { abandoned, finished };
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1233
1307
|
prunableRuns(before) {
|
|
1234
1308
|
return this.#run(
|
|
1235
1309
|
"prunableRuns",
|
|
@@ -1285,7 +1359,9 @@ var SqliteJournal = class {
|
|
|
1285
1359
|
const dropDenials = this.#db.prepare(
|
|
1286
1360
|
"DELETE FROM denials WHERE action_id IN (SELECT id FROM actions WHERE run_id = ?)"
|
|
1287
1361
|
);
|
|
1362
|
+
const dropOwner = this.#db.prepare("DELETE FROM run_owners WHERE run_id = ?");
|
|
1288
1363
|
for (const id of ids) {
|
|
1364
|
+
dropOwner.run(id);
|
|
1289
1365
|
dropServers.run(id);
|
|
1290
1366
|
dropLeases.run(id);
|
|
1291
1367
|
dropDenials.run(id);
|
|
@@ -3908,6 +3984,8 @@ export {
|
|
|
3908
3984
|
cliCommand,
|
|
3909
3985
|
cliCommandFrom,
|
|
3910
3986
|
proxyCommand,
|
|
3987
|
+
JOURNAL_NAME,
|
|
3988
|
+
home,
|
|
3911
3989
|
findManifest,
|
|
3912
3990
|
findJournal,
|
|
3913
3991
|
counted,
|
|
@@ -3973,4 +4051,4 @@ export {
|
|
|
3973
4051
|
applyUninstall,
|
|
3974
4052
|
clientEnvFor
|
|
3975
4053
|
};
|
|
3976
|
-
//# sourceMappingURL=chunk-
|
|
4054
|
+
//# sourceMappingURL=chunk-MTPUG26S.js.map
|
package/dist/cli.js
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
verifyAgainstServers,
|
|
63
63
|
warnUntested,
|
|
64
64
|
wasRefused
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-MTPUG26S.js";
|
|
66
66
|
import {
|
|
67
67
|
DriftConflict,
|
|
68
68
|
ManifestError,
|
|
@@ -1126,7 +1126,12 @@ function render(journal, options, tick, view) {
|
|
|
1126
1126
|
});
|
|
1127
1127
|
out2.push("");
|
|
1128
1128
|
out2.push(
|
|
1129
|
-
canDecide(options) ? ` ${keyHint("a", "approve")} ${keyHint("A", "
|
|
1129
|
+
canDecide(options) ? ` ${keyHint("a", "approve")} ${keyHint("A", "approve for an hour")} ${keyHint("d", "deny")} ${keyHint("j/k", "move")} ${keyHint("q", "quit")}` : ` ${style.quiet(`${options.approveWith} approve --all`)}`
|
|
1130
|
+
);
|
|
1131
|
+
} else if (canDecide(options)) {
|
|
1132
|
+
out2.push("");
|
|
1133
|
+
out2.push(
|
|
1134
|
+
` ${keyHint("q", "quit")} ${style.quiet("A held call appears here; answer it with")} ${style.strong("a")} ${style.quiet("approve,")} ${style.strong("A")} ${style.quiet("approve for an hour, or")} ${style.strong("d")} ${style.quiet("deny.")}`
|
|
1130
1135
|
);
|
|
1131
1136
|
}
|
|
1132
1137
|
if (view.notice !== "") {
|
|
@@ -1193,6 +1198,8 @@ async function watch(options) {
|
|
|
1193
1198
|
const waiting = ready.listGated();
|
|
1194
1199
|
const action = waiting[Math.min(view.cursor, waiting.length - 1)];
|
|
1195
1200
|
if (action === void 0) {
|
|
1201
|
+
view.notice = "Nothing is waiting right now. When an agent's call is held, it appears above and this key answers it.";
|
|
1202
|
+
view.noticeUntil = tick + NOTICE_TICKS;
|
|
1196
1203
|
return;
|
|
1197
1204
|
}
|
|
1198
1205
|
const changed = approve ? ready.approve(action.id, options.decideAs) : ready.denyByPerson(action.id, options.decideAs, "denied from the watch view");
|
|
@@ -1227,6 +1234,10 @@ async function watch(options) {
|
|
|
1227
1234
|
view.cursor = Math.max(0, view.cursor - 1);
|
|
1228
1235
|
return;
|
|
1229
1236
|
default:
|
|
1237
|
+
if (key.length === 1 && key >= " ") {
|
|
1238
|
+
view.notice = `${key} does nothing here. Keys: a approve, A approve for an hour, d deny, j/k move, q quit`;
|
|
1239
|
+
view.noticeUntil = tick + NOTICE_TICKS;
|
|
1240
|
+
}
|
|
1230
1241
|
return;
|
|
1231
1242
|
}
|
|
1232
1243
|
};
|
|
@@ -1570,12 +1581,13 @@ function gatesView(journal, screen, options) {
|
|
|
1570
1581
|
return [` ${style.quiet("Nothing is waiting for a decision.")}`];
|
|
1571
1582
|
}
|
|
1572
1583
|
const at = Math.min(screen.cursor, waiting.length - 1);
|
|
1573
|
-
return waiting.
|
|
1584
|
+
return waiting.flatMap((action, index) => {
|
|
1574
1585
|
const here = index === at && canPress(options);
|
|
1575
1586
|
const name = `${action.server}.${action.tool}`;
|
|
1576
1587
|
const shown = here ? style.accent(name) : style.quiet(name);
|
|
1577
1588
|
const args = style.quiet(truncate2(JSON.stringify(action.args), 54));
|
|
1578
|
-
|
|
1589
|
+
const why = here && action.error !== void 0 ? [` ${style.quiet(truncate2(action.error, 90))}`] : [];
|
|
1590
|
+
return [` ${here ? style.accent(CURSOR) : " "} ${shown} ${args}`, ...why];
|
|
1579
1591
|
});
|
|
1580
1592
|
}
|
|
1581
1593
|
function footer(screen, options) {
|
|
@@ -1586,7 +1598,7 @@ function footer(screen, options) {
|
|
|
1586
1598
|
const what = screen.confirmingForce ? `undo ${screen.confirmingLabel ?? "this session"} anyway, losing that change?` : `undo ${screen.confirmingLabel ?? "this session"}?`;
|
|
1587
1599
|
return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
|
|
1588
1600
|
}
|
|
1589
|
-
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("A", "
|
|
1601
|
+
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("A", "approve for an hour"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
|
|
1590
1602
|
keyHint2("l", "check now"),
|
|
1591
1603
|
keyHint2("p", "preview undo"),
|
|
1592
1604
|
keyHint2("u", "undo"),
|
|
@@ -1597,10 +1609,38 @@ function footer(screen, options) {
|
|
|
1597
1609
|
keyHint2("l", "check now"),
|
|
1598
1610
|
keyHint2("u", "undo"),
|
|
1599
1611
|
keyHint2("j/k", "move"),
|
|
1600
|
-
keyHint2("g", "
|
|
1612
|
+
keyHint2("g", "waiting calls")
|
|
1601
1613
|
];
|
|
1602
1614
|
return ["", ` ${keys.join(" ")} ${keyHint2("q", "quit")}`];
|
|
1603
1615
|
}
|
|
1616
|
+
function welcomeFrame(screen, options, tick) {
|
|
1617
|
+
const spinner = options.live ? `${style.accent(FRAMES2[tick % FRAMES2.length] ?? "")} ` : "";
|
|
1618
|
+
const waiting = needsConnecting(screen.groups).length;
|
|
1619
|
+
const lead = screen.groups.length === 0 ? [] : waiting === 0 ? [
|
|
1620
|
+
` ${style.strong("Everything below is covered.")} ${style.quiet("Restart those apps if you have not,")}`,
|
|
1621
|
+
` ${style.quiet("then use your agent as usual. Its sessions will appear here.")}`
|
|
1622
|
+
] : [
|
|
1623
|
+
` ${style.strong("Welcome.")} ${style.quiet("These are the AI apps on this machine and the tools they use.")}`,
|
|
1624
|
+
` ${style.quiet("Press")} ${style.strong("a")} ${style.quiet("to cover all of them, or move to one and press")} ${style.strong("enter")}${style.quiet(".")}`,
|
|
1625
|
+
` ${style.quiet("Nothing else on your machine is changed, and")} ${style.strong("synartesis uninstall")} ${style.quiet("puts it all back.")}`
|
|
1626
|
+
];
|
|
1627
|
+
const notice = screen.notice === "" ? [] : ["", ...screen.notice.split("\n").map((line2) => ` ${style.accent(line2)}`)];
|
|
1628
|
+
return [
|
|
1629
|
+
"",
|
|
1630
|
+
` ${style.plate(WORDMARK)}`,
|
|
1631
|
+
` ${rule(70)}`,
|
|
1632
|
+
"",
|
|
1633
|
+
` ${spinner}${style.quiet(screen.busy ?? "nothing has run through Synartesis yet")}`,
|
|
1634
|
+
"",
|
|
1635
|
+
...lead,
|
|
1636
|
+
...lead.length === 0 ? [] : [""],
|
|
1637
|
+
...windowed(connectionsView(screen, options), screen.cursor, roomFor(options)),
|
|
1638
|
+
...notice,
|
|
1639
|
+
"",
|
|
1640
|
+
` ${[keyHint2("a", "cover all"), keyHint2("enter", "cover this one"), keyHint2("j/k", "move"), keyHint2("r", "look again"), keyHint2("q", "quit")].join(" ")}`,
|
|
1641
|
+
""
|
|
1642
|
+
].join("\n");
|
|
1643
|
+
}
|
|
1604
1644
|
function waitingForJournal2(options, tick) {
|
|
1605
1645
|
const spinner = options.live ? `${style.accent(FRAMES2[tick % FRAMES2.length] ?? "")} ` : "";
|
|
1606
1646
|
return [
|
|
@@ -1655,7 +1695,8 @@ async function openConsole(options) {
|
|
|
1655
1695
|
groups: [],
|
|
1656
1696
|
busy: void 0,
|
|
1657
1697
|
notice: "",
|
|
1658
|
-
noticeUntil: 0
|
|
1698
|
+
noticeUntil: 0,
|
|
1699
|
+
welcome: false
|
|
1659
1700
|
};
|
|
1660
1701
|
let tick = 0;
|
|
1661
1702
|
const stopped = () => screen.stop;
|
|
@@ -1666,15 +1707,30 @@ async function openConsole(options) {
|
|
|
1666
1707
|
const frame = () => {
|
|
1667
1708
|
const ready = open();
|
|
1668
1709
|
if (ready === void 0) {
|
|
1710
|
+
if (options.scan !== void 0 && canPress(options)) {
|
|
1711
|
+
return welcomeFrame(screen, options, tick);
|
|
1712
|
+
}
|
|
1669
1713
|
return waitingForJournal2(options, tick);
|
|
1670
1714
|
}
|
|
1715
|
+
if (screen.welcome) {
|
|
1716
|
+
screen.welcome = false;
|
|
1717
|
+
screen.mode = "runs";
|
|
1718
|
+
screen.cursor = 0;
|
|
1719
|
+
say("the first session has arrived");
|
|
1720
|
+
}
|
|
1671
1721
|
const tail = command(ready);
|
|
1672
1722
|
const shout = screen.notice === "" ? 0 : screen.notice.split("\n").length;
|
|
1673
1723
|
const room = Math.max(3, roomFor(options) - tail.length - shout);
|
|
1674
1724
|
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);
|
|
1675
1725
|
const notice = screen.notice === "" ? [] : ["", ...screen.notice.split("\n").map((line2) => ` ${style.accent(line2)}`)];
|
|
1726
|
+
const held2 = screen.mode === "gates" || screen.mode === "connections" ? 0 : ready.listGated().length;
|
|
1727
|
+
const alert = held2 === 0 ? [] : [
|
|
1728
|
+
` ${style.accent(`${String(held2)} ${held2 === 1 ? "call is" : "calls are"} waiting for you`)} ${style.quiet("press")} ${style.strong("g")} ${style.quiet("to answer")}`,
|
|
1729
|
+
""
|
|
1730
|
+
];
|
|
1676
1731
|
return [
|
|
1677
1732
|
...header(options, screen, tick),
|
|
1733
|
+
...alert,
|
|
1678
1734
|
...body,
|
|
1679
1735
|
...notice,
|
|
1680
1736
|
...tail,
|
|
@@ -1798,6 +1854,13 @@ async function openConsole(options) {
|
|
|
1798
1854
|
const rescan = () => {
|
|
1799
1855
|
screen.groups = options.scan?.() ?? [];
|
|
1800
1856
|
};
|
|
1857
|
+
if (!existsSync3(options.journalPath) && options.scan !== void 0) {
|
|
1858
|
+
screen.welcome = true;
|
|
1859
|
+
screen.mode = "connections";
|
|
1860
|
+
rescan();
|
|
1861
|
+
} else if (canPress(options) && (open()?.listGated().length ?? 0) > 0) {
|
|
1862
|
+
screen.mode = "gates";
|
|
1863
|
+
}
|
|
1801
1864
|
const connect = async (targets) => {
|
|
1802
1865
|
if (options.connect === void 0) {
|
|
1803
1866
|
say("no way to connect was configured");
|
|
@@ -1981,6 +2044,9 @@ async function openConsole(options) {
|
|
|
1981
2044
|
return;
|
|
1982
2045
|
}
|
|
1983
2046
|
default:
|
|
2047
|
+
if (key.length === 1 && key >= " ") {
|
|
2048
|
+
say(`${key} does nothing here; the keys that work are listed on the bottom line`);
|
|
2049
|
+
}
|
|
1984
2050
|
return;
|
|
1985
2051
|
}
|
|
1986
2052
|
};
|
|
@@ -1998,6 +2064,9 @@ async function openConsole(options) {
|
|
|
1998
2064
|
return;
|
|
1999
2065
|
}
|
|
2000
2066
|
press(next.value);
|
|
2067
|
+
if (screen.welcome && open() === void 0) {
|
|
2068
|
+
screen.mode = "connections";
|
|
2069
|
+
}
|
|
2001
2070
|
if (stopped()) {
|
|
2002
2071
|
return;
|
|
2003
2072
|
}
|
|
@@ -2108,11 +2177,12 @@ if (NODE_MAJOR < 22) {
|
|
|
2108
2177
|
process.exit(2);
|
|
2109
2178
|
}
|
|
2110
2179
|
var COMMANDS = `
|
|
2111
|
-
synartesis start here.
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2180
|
+
synartesis start here. The first time,
|
|
2181
|
+
it lists your AI apps: press a
|
|
2182
|
+
to cover them. After that it
|
|
2183
|
+
opens on anything waiting for
|
|
2184
|
+
you, and on what agents did,
|
|
2185
|
+
with undo on the arrow keys.
|
|
2116
2186
|
synartesis install [--client <name>] [--remote] [--dry-run] [--print]
|
|
2117
2187
|
synartesis uninstall [--client <name>]
|
|
2118
2188
|
synartesis status
|
|
@@ -2123,6 +2193,8 @@ var COMMANDS = `
|
|
|
2123
2193
|
synartesis show <runId> [--full] [--live] [--journal <path>]
|
|
2124
2194
|
synartesis gates [--journal <path>]
|
|
2125
2195
|
synartesis close [runId] [--journal <path>]
|
|
2196
|
+
synartesis clean [--dry-run] [--yes] clear finished sessions,
|
|
2197
|
+
keep what still matters
|
|
2126
2198
|
synartesis prune [--older-than <days>] [--dry-run] [--journal <path>]
|
|
2127
2199
|
synartesis proxy --manifest <path> [--server <name>] what your agent runs
|
|
2128
2200
|
[--journal <path>]
|
|
@@ -2644,10 +2716,16 @@ async function connectThese(targets, manifestPath) {
|
|
|
2644
2716
|
);
|
|
2645
2717
|
const applied = applyInstall(plans, manifestPath, yaml);
|
|
2646
2718
|
const count = applied.reduce((sum, entry) => sum + entry.servers.length, 0);
|
|
2719
|
+
const skipped = plans.flatMap((plan) => plan.skipped.map((skip) => `${skip.name}: ${skip.why}`));
|
|
2647
2720
|
if (count === 0) {
|
|
2648
|
-
return "nothing
|
|
2721
|
+
return skipped.length === 0 ? "nothing needed connecting" : `not connected -- ${skipped.join("; ")}`;
|
|
2649
2722
|
}
|
|
2650
|
-
|
|
2723
|
+
const apps = [...new Set(applied.map((entry) => entry.site.label))];
|
|
2724
|
+
const restart = `quit and reopen ${apps.join(" and ")} to start using it`;
|
|
2725
|
+
return [
|
|
2726
|
+
`covered ${String(count)}${count === 1 ? " server" : " servers"} \xB7 ${restart}`,
|
|
2727
|
+
...skipped.length === 0 ? [] : [`not covered -- ${skipped.join("; ")}`]
|
|
2728
|
+
].join("\n");
|
|
2651
2729
|
}
|
|
2652
2730
|
function runStatus(argv) {
|
|
2653
2731
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
@@ -2934,10 +3012,15 @@ function runList(journal, asJson, journalPath) {
|
|
|
2934
3012
|
}
|
|
2935
3013
|
hint(firstOf(
|
|
2936
3014
|
() => heldCalls(journal),
|
|
3015
|
+
() => {
|
|
3016
|
+
const clearable = journal.cleanable(new Date(Date.now() - CLEAN_IDLE_MS).toISOString()).finished.length;
|
|
3017
|
+
return clearable >= CLEAN_NAG_SESSIONS ? { why: `${String(clearable)} of these are finished with nothing left to undo; to clear them`, run: "clean", needs: ["journal"] } : void 0;
|
|
3018
|
+
},
|
|
2937
3019
|
() => whatChanged(journal)
|
|
2938
3020
|
));
|
|
2939
3021
|
return 0;
|
|
2940
3022
|
}
|
|
3023
|
+
var CLEAN_NAG_SESSIONS = 10;
|
|
2941
3024
|
async function runShow(argv, journal, asJson) {
|
|
2942
3025
|
const full = argv.includes("--full");
|
|
2943
3026
|
const runs = [...journal.listRuns()].reverse();
|
|
@@ -3227,6 +3310,66 @@ function runPrune(argv, journal, journalPath) {
|
|
|
3227
3310
|
out("");
|
|
3228
3311
|
return 0;
|
|
3229
3312
|
}
|
|
3313
|
+
var CLEAN_IDLE_MS = 24 * 60 * 60 * 1e3;
|
|
3314
|
+
async function runClean(argv, journal, journalPath) {
|
|
3315
|
+
const idleBefore = new Date(Date.now() - CLEAN_IDLE_MS).toISOString();
|
|
3316
|
+
const { abandoned, finished } = journal.cleanable(idleBefore);
|
|
3317
|
+
const total = journal.listRuns().length;
|
|
3318
|
+
const empty = finished.filter((run) => run.actions === 0).length;
|
|
3319
|
+
const kept = total - finished.length;
|
|
3320
|
+
const sizeBefore = sizeOf(journalPath);
|
|
3321
|
+
out("");
|
|
3322
|
+
out(` ${style.label("clean")} ${style.quiet(journalPath)}`);
|
|
3323
|
+
out(` ${rule(54)}`);
|
|
3324
|
+
out("");
|
|
3325
|
+
if (finished.length === 0 && abandoned.length === 0) {
|
|
3326
|
+
out(` ${style.quiet("Nothing to clear: every session here is still in use, or still has")}`);
|
|
3327
|
+
out(` ${style.quiet("something you could undo or decide.")}`);
|
|
3328
|
+
out("");
|
|
3329
|
+
return 0;
|
|
3330
|
+
}
|
|
3331
|
+
out(
|
|
3332
|
+
` ${style.strong("clears")} ${counted(finished.length, "finished session")} with nothing left to undo` + (empty > 0 ? style.quiet(` (${String(empty)} of them empty)`) : "")
|
|
3333
|
+
);
|
|
3334
|
+
if (abandoned.length > 0) {
|
|
3335
|
+
out(` ${style.strong("closes")} ${counted(abandoned.length, "session")} left open by an app that is no longer running`);
|
|
3336
|
+
}
|
|
3337
|
+
out(
|
|
3338
|
+
` ${style.strong("keeps ")} ${counted(kept, "session")} ${style.quiet("still running, or with something to undo or decide")}`
|
|
3339
|
+
);
|
|
3340
|
+
out(` ${style.quiet(" your connected apps and your policy are not touched")}`);
|
|
3341
|
+
out("");
|
|
3342
|
+
if (argv.includes("--dry-run")) {
|
|
3343
|
+
out(` ${style.quiet("Nothing was changed.")}`);
|
|
3344
|
+
out("");
|
|
3345
|
+
return 0;
|
|
3346
|
+
}
|
|
3347
|
+
if (!argv.includes("--yes")) {
|
|
3348
|
+
if (!process.stdin.isTTY) {
|
|
3349
|
+
out(` ${style.quiet("Nothing was changed. Run it in a terminal to be asked, or add --yes.")}`);
|
|
3350
|
+
out("");
|
|
3351
|
+
return 1;
|
|
3352
|
+
}
|
|
3353
|
+
const answer = (await ask(" Go ahead? [y/N] ")).trim().toLowerCase();
|
|
3354
|
+
if (answer !== "y" && answer !== "yes") {
|
|
3355
|
+
out(` ${style.quiet("Nothing was changed.")}`);
|
|
3356
|
+
out("");
|
|
3357
|
+
return 0;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
for (const id of abandoned) {
|
|
3361
|
+
journal.closeAbandonedRun(id);
|
|
3362
|
+
}
|
|
3363
|
+
const now = journal.cleanable(idleBefore).finished;
|
|
3364
|
+
const removed = journal.deleteRuns(now.map((run) => run.id));
|
|
3365
|
+
journal.vacuum();
|
|
3366
|
+
out("");
|
|
3367
|
+
out(
|
|
3368
|
+
` ${style.accent(`cleared ${counted(removed.runs, "session")}`)}` + style.quiet(`${abandoned.length > 0 ? `, closed ${String(abandoned.length)}` : ""} \xB7 journal ${sizeBefore} \u2192 ${sizeOf(journalPath)}`)
|
|
3369
|
+
);
|
|
3370
|
+
out("");
|
|
3371
|
+
return 0;
|
|
3372
|
+
}
|
|
3230
3373
|
function runResolve(argv, journal) {
|
|
3231
3374
|
const applied = argv.includes("--applied");
|
|
3232
3375
|
const failed = argv.includes("--failed");
|
|
@@ -3914,6 +4057,7 @@ var KNOWN_COMMANDS = [
|
|
|
3914
4057
|
"show",
|
|
3915
4058
|
"gates",
|
|
3916
4059
|
"close",
|
|
4060
|
+
"clean",
|
|
3917
4061
|
"prune",
|
|
3918
4062
|
"proxy",
|
|
3919
4063
|
"desktop",
|
|
@@ -4139,6 +4283,8 @@ ${COMMANDS}`);
|
|
|
4139
4283
|
return runClose(argv, journal);
|
|
4140
4284
|
case "prune":
|
|
4141
4285
|
return runPrune(argv, journal, journalPath);
|
|
4286
|
+
case "clean":
|
|
4287
|
+
return await runClean(argv, journal, journalPath);
|
|
4142
4288
|
case "gates":
|
|
4143
4289
|
return runGates(journal, asJson);
|
|
4144
4290
|
case "resolve":
|
package/dist/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
JOURNAL_NAME,
|
|
3
4
|
PROXY_FLAGS,
|
|
4
5
|
SILENT,
|
|
5
6
|
canonical,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
findJournal,
|
|
14
15
|
findManifest,
|
|
15
16
|
fingerprint2 as fingerprint,
|
|
17
|
+
home,
|
|
16
18
|
isDisconnected,
|
|
17
19
|
listAll,
|
|
18
20
|
loadManifest,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
warnUntested,
|
|
35
37
|
withIdempotencyKey,
|
|
36
38
|
withoutMissingTools
|
|
37
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-MTPUG26S.js";
|
|
38
40
|
import {
|
|
39
41
|
SnapshotError,
|
|
40
42
|
UpstreamError,
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
} from "./chunk-YVOO3PTV.js";
|
|
43
45
|
|
|
44
46
|
// src/proxy/stdio.ts
|
|
45
|
-
import { resolve } from "path";
|
|
47
|
+
import { join, resolve } from "path";
|
|
46
48
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
47
49
|
|
|
48
50
|
// src/proxy/http.ts
|
|
@@ -642,7 +644,7 @@ function createProxyServer(options) {
|
|
|
642
644
|
}
|
|
643
645
|
tools.push({
|
|
644
646
|
...compatible(tool, (dialect) => {
|
|
645
|
-
log?.
|
|
647
|
+
log?.debug(
|
|
646
648
|
`${upstream.name}.${tool.name} declares JSON Schema ${dialect}; the dialect was dropped so clients that only accept 2020-12 can call it`
|
|
647
649
|
);
|
|
648
650
|
}),
|
|
@@ -1362,7 +1364,13 @@ async function main() {
|
|
|
1362
1364
|
logger: log,
|
|
1363
1365
|
// Absolute, because whoever approves may be in any directory at all.
|
|
1364
1366
|
approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,
|
|
1365
|
-
|
|
1367
|
+
// Plain `synartesis` when this is the journal it finds by itself, which
|
|
1368
|
+
// is the usual case; otherwise told where to look.
|
|
1369
|
+
notify: desktopNotifier(
|
|
1370
|
+
process.env,
|
|
1371
|
+
void 0,
|
|
1372
|
+
resolve(argv.journal) === resolve(join(home(), JOURNAL_NAME)) ? cliCommandFrom(import.meta.url) : `${cliCommandFrom(import.meta.url)} --journal ${resolve(argv.journal)}`
|
|
1373
|
+
)
|
|
1366
1374
|
});
|
|
1367
1375
|
proxy2.ready.then((runId) => {
|
|
1368
1376
|
for (const [server, started] of startedWith) {
|