synartesis 0.1.0 → 0.2.0

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/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  toPayload,
24
24
  verifyAgainstServers,
25
25
  wasRefused
26
- } from "./chunk-X4VQNEP5.js";
26
+ } from "./chunk-7AUGXEWC.js";
27
27
  import {
28
28
  DriftConflict,
29
29
  ManifestError,
@@ -34,11 +34,105 @@ import {
34
34
  } from "./chunk-K3QIPVBY.js";
35
35
 
36
36
  // src/cli.ts
37
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
37
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
38
38
  import { dirname, resolve } from "path";
39
39
 
40
40
  // src/init/draft.ts
41
41
  import { z } from "zod";
42
+
43
+ // src/init/known.ts
44
+ import { existsSync, readFileSync } from "fs";
45
+ import { fileURLToPath } from "url";
46
+ var KNOWN = [
47
+ { marker: "server-filesystem", manifest: "filesystem" },
48
+ { marker: "server-github", manifest: "github" },
49
+ { marker: "github-mcp-server", manifest: "github" },
50
+ { marker: "server-memory", manifest: "memory" },
51
+ { marker: "mcp-server-git", manifest: "git" },
52
+ { marker: "server-git", manifest: "git" }
53
+ ];
54
+ function manifestsDir() {
55
+ for (const up of ["../manifests/", "../../manifests/"]) {
56
+ const candidate = fileURLToPath(new URL(up, import.meta.url));
57
+ if (existsSync(candidate)) {
58
+ return candidate;
59
+ }
60
+ }
61
+ return void 0;
62
+ }
63
+ function knownPolicyFor(command, args) {
64
+ const line2 = [command, ...args].join(" ");
65
+ const hit = KNOWN.find((entry) => line2.includes(entry.marker));
66
+ const dir = manifestsDir();
67
+ if (hit === void 0 || dir === void 0) {
68
+ return void 0;
69
+ }
70
+ const path = `${dir}${hit.manifest}.yaml`;
71
+ if (!existsSync(path)) {
72
+ return void 0;
73
+ }
74
+ try {
75
+ const text = readFileSync(path, "utf8");
76
+ const source = toolsBlock(text);
77
+ const key = serverKey(text);
78
+ if (source === void 0 || key === void 0) {
79
+ return void 0;
80
+ }
81
+ const rules = parseManifest(
82
+ `version: 1
83
+ servers:
84
+ ${key}:
85
+ command: "true"
86
+ tools:
87
+ ${source}
88
+ `,
89
+ path
90
+ ).tools;
91
+ return { key, rules, name: hit.manifest, source };
92
+ } catch {
93
+ return void 0;
94
+ }
95
+ }
96
+ function toolsBlock(text) {
97
+ const at = text.search(/^tools:[ \t]*$/m);
98
+ if (at === -1) {
99
+ return void 0;
100
+ }
101
+ const body = text.slice(text.indexOf("\n", at) + 1);
102
+ const lines = [];
103
+ for (const line2 of body.split("\n")) {
104
+ if (/^[^\s#]/.test(line2)) {
105
+ break;
106
+ }
107
+ lines.push(line2);
108
+ }
109
+ return lines.join("\n").replace(/\s+$/, "");
110
+ }
111
+ function serverKey(text) {
112
+ const at = text.search(/^servers:[ \t]*$/m);
113
+ if (at === -1) {
114
+ return void 0;
115
+ }
116
+ const body = text.slice(text.indexOf("\n", at) + 1);
117
+ for (const line2 of body.split("\n")) {
118
+ if (/^[^\s#]/.test(line2)) {
119
+ return void 0;
120
+ }
121
+ const named = /^ {2}([A-Za-z0-9_-]+):[ \t]*$/.exec(line2);
122
+ if (named?.[1] !== void 0) {
123
+ return named[1];
124
+ }
125
+ }
126
+ return void 0;
127
+ }
128
+ function toolsReferencedBy(rule2, key) {
129
+ const local = (qualified) => qualified === void 0 || !qualified.startsWith(`${key}.`) ? void 0 : qualified.slice(key.length + 1);
130
+ return [local(rule2.snapshot?.tool), local(rule2.inverse?.tool)].filter(
131
+ (name) => name !== void 0
132
+ );
133
+ }
134
+
135
+ // src/init/draft.ts
42
136
  var toolSchema = z.looseObject({
43
137
  name: z.string(),
44
138
  description: z.string().optional(),
@@ -84,6 +178,38 @@ function draftTool(server, tool) {
84
178
  lines.push(` gate: always`);
85
179
  return lines.join("\n");
86
180
  }
181
+ function patternFor(match) {
182
+ const source = match.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*");
183
+ return new RegExp(`^${source}$`);
184
+ }
185
+ function adopt(known, name, tools) {
186
+ const advertised = new Set(tools.map((tool) => tool.name));
187
+ const covered = /* @__PURE__ */ new Set();
188
+ for (const rule2 of known.rules) {
189
+ for (const needed of toolsReferencedBy(rule2, known.key)) {
190
+ if (!advertised.has(needed)) {
191
+ return void 0;
192
+ }
193
+ }
194
+ const test = patternFor(rule2.match);
195
+ for (const tool of tools) {
196
+ if (test.test(`${known.key}.${tool.name}`)) {
197
+ covered.add(tool.name);
198
+ }
199
+ }
200
+ }
201
+ if (covered.size === 0) {
202
+ return void 0;
203
+ }
204
+ const renamed = known.source.replaceAll(`"${known.key}.`, `"${name}.`);
205
+ const missing = tools.filter((tool) => !covered.has(tool.name));
206
+ const extra = missing.length === 0 ? "" : [
207
+ "",
208
+ ` # Not mentioned by the bundled ${known.name} policy, so gated until you say otherwise.`,
209
+ ...missing.map((tool) => draftTool(name, tool))
210
+ ].join("\n");
211
+ return { source: `${renamed}${extra}`, covered: covered.size };
212
+ }
87
213
  async function draftManifest(options) {
88
214
  const upstream = await connectStdioUpstream({
89
215
  name: options.name,
@@ -126,12 +252,21 @@ async function draftManifest(options) {
126
252
  ` command: ${quote(options.command)}`,
127
253
  ` args: [${options.args.map(quote).join(", ")}]`
128
254
  ].join("\n");
129
- const policies = tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
255
+ const known = knownPolicyFor(options.command, options.args);
256
+ const adopted = known === void 0 ? void 0 : adopt(known, options.name, tools);
257
+ const policies = adopted?.source ?? tools.map((tool) => draftTool(options.name, tool)).join("\n\n");
130
258
  if (existing === void 0) {
131
- return [
259
+ const yaml = [
132
260
  `# Generated by synartesis init from ${options.name}'s tools/list.`,
133
- `# Every tool starts gated. Working through the TODOs is the whole job:`,
134
- `# a tool with no inverse is one an agent cannot use unsupervised.`,
261
+ ...adopted === void 0 ? [
262
+ `# Every tool starts gated. Working through the TODOs is the whole job:`,
263
+ `# a tool with no inverse is one an agent cannot use unsupervised.`
264
+ ] : [
265
+ `# ${String(adopted.covered)} of its tools were recognised, so the policy that ships`,
266
+ `# with Synartesis for ${known?.name ?? "this server"} was used and checked against what this`,
267
+ `# server actually advertises. Read it before trusting it: it is a starting`,
268
+ `# point that happens to be finished, not a promise about your setup.`
269
+ ],
135
270
  ``,
136
271
  `version: 1`,
137
272
  ``,
@@ -142,8 +277,10 @@ async function draftManifest(options) {
142
277
  policies,
143
278
  ``
144
279
  ].join("\n");
280
+ return adopted === void 0 || known === void 0 ? { yaml } : { yaml, adopted: { server: known.name, tools: adopted.covered } };
145
281
  }
146
- return mergeInto(existing, server, policies, options.name);
282
+ const merged = mergeInto(existing, server, policies, options.name);
283
+ return adopted === void 0 || known === void 0 ? { yaml: merged } : { yaml: merged, adopted: { server: known.name, tools: adopted.covered } };
147
284
  }
148
285
  function mergeInto(existing, server, policies, name) {
149
286
  const serversAt = existing.indexOf("\nservers:");
@@ -234,8 +371,12 @@ async function rollback(options) {
234
371
  }
235
372
  };
236
373
  const all = journal.getActions(runId);
237
- const inScope = [...all].filter((action) => options.toSeq === void 0 || action.seq >= options.toSeq).sort((a, b) => b.seq - a.seq);
374
+ const floor = options.toSeq;
375
+ const inScope = [...all].filter((action) => floor === void 0 || action.seq >= floor).sort((a, b) => b.seq - a.seq);
238
376
  const steps = [];
377
+ const kept = (floor === void 0 ? [] : [...all].filter((action) => action.seq < floor)).sort(
378
+ (a, b) => b.seq - a.seq
379
+ );
239
380
  let halted;
240
381
  let leftInPlace = false;
241
382
  for (const action of inScope) {
@@ -367,6 +508,14 @@ Resolve the conflict, then run undo --replan to check it against the world as it
367
508
  };
368
509
  break;
369
510
  }
511
+ for (const action of kept) {
512
+ steps.push({
513
+ ...describeStep(action),
514
+ kind: "kept",
515
+ reason: `below --to ${String(floor ?? 0)}, so it is left as it is`,
516
+ verified: true
517
+ });
518
+ }
370
519
  const completedWholeRun = halted === void 0 && !leftInPlace && options.toSeq === void 0;
371
520
  const status = completedWholeRun ? "rolled_back" : "partial";
372
521
  if (!dryRun) {
@@ -423,7 +572,7 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
423
572
  }
424
573
 
425
574
  // src/watch.ts
426
- import { existsSync } from "fs";
575
+ import { existsSync as existsSync2 } from "fs";
427
576
 
428
577
  // src/keys.ts
429
578
  var SEQUENCE = /^\u001b(\[[0-9;?]*[ -\/]*[@-~]|O[@-~])/;
@@ -552,7 +701,7 @@ async function* terminalKeys() {
552
701
  async function watch(options) {
553
702
  let journal;
554
703
  const open = () => {
555
- if (journal === void 0 && existsSync(options.journalPath)) {
704
+ if (journal === void 0 && existsSync2(options.journalPath)) {
556
705
  journal = openJournal(options.journalPath, { mustExist: true });
557
706
  }
558
707
  return journal;
@@ -661,7 +810,7 @@ async function watch(options) {
661
810
  }
662
811
 
663
812
  // src/console.ts
664
- import { existsSync as existsSync2 } from "fs";
813
+ import { existsSync as existsSync3 } from "fs";
665
814
  var FRAMES2 = [
666
815
  "\u280B",
667
816
  "\u2819",
@@ -867,7 +1016,7 @@ async function* terminalKeys2() {
867
1016
  async function openConsole(options) {
868
1017
  let journal;
869
1018
  const open = () => {
870
- if (journal === void 0 && existsSync2(options.journalPath)) {
1019
+ if (journal === void 0 && existsSync3(options.journalPath)) {
871
1020
  journal = openJournal(options.journalPath, { mustExist: true });
872
1021
  }
873
1022
  return journal;
@@ -1108,6 +1257,8 @@ var COMMANDS = `
1108
1257
  synartesis gates [--journal <path>]
1109
1258
  synartesis close [runId] [--journal <path>]
1110
1259
  synartesis proxy --manifest <path> [--journal <path>] what your agent runs
1260
+ [--http <port> --token <secret>] for a client that
1261
+ cannot start one
1111
1262
  synartesis watch [--by <name>] [--journal <path>]
1112
1263
  synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
1113
1264
  synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
@@ -1223,27 +1374,34 @@ async function runInit(argv) {
1223
1374
  }
1224
1375
  const path = findManifest(flag(argv, "--manifest"));
1225
1376
  const force = argv.includes("--force");
1226
- const present = existsSync3(path);
1377
+ const present = existsSync4(path);
1227
1378
  if (present && force) {
1228
1379
  throw new UsageError(
1229
1380
  `--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
1230
1381
  );
1231
1382
  }
1232
- const yaml = await draftManifest({
1383
+ const draft = await draftManifest({
1233
1384
  name,
1234
1385
  command,
1235
1386
  args: argv.slice(separator + 2),
1236
- ...present ? { existing: readFileSync(path, "utf8") } : {}
1387
+ ...present ? { existing: readFileSync2(path, "utf8") } : {}
1237
1388
  });
1238
- parseManifest(yaml, path);
1389
+ parseManifest(draft.yaml, path);
1239
1390
  mkdirSync(dirname(resolve(path)), { recursive: true });
1240
- writeFileSync(path, yaml);
1391
+ writeFileSync(path, draft.yaml);
1241
1392
  out("");
1242
1393
  out(` ${style.label(present ? "extended" : "wrote")} ${style.strong(path)}`);
1243
1394
  out(` ${rule(54)}`);
1244
1395
  out("");
1245
- out(` ${style.quiet("Every tool is guarded until you say how to undo it.")}`);
1246
- out(` ${style.quiet("Work through the TODOs, then point your MCP client at:")}`);
1396
+ if (draft.adopted === void 0) {
1397
+ out(` ${style.quiet("Every tool is guarded until you say how to undo it.")}`);
1398
+ out(` ${style.quiet("Work through the TODOs, then point your MCP client at:")}`);
1399
+ } else {
1400
+ out(
1401
+ ` ${style.quiet(`Recognised ${String(draft.adopted.tools)} tools, so the policy that ships for`)} ${style.strong(draft.adopted.server)} ${style.quiet("was used.")}`
1402
+ );
1403
+ out(` ${style.quiet("Read it before you trust it, then point your MCP client at:")}`);
1404
+ }
1247
1405
  out("");
1248
1406
  out(` ${style.accent(`${proxyCommand()} --manifest ${resolve(path)}`)}`);
1249
1407
  out("");
@@ -1518,7 +1676,13 @@ function report(result) {
1518
1676
  out(` ${style.label(result.dryRun ? "dry run" : "undo")} ${style.strong(result.runId)}`);
1519
1677
  out(` ${rule(72)}`);
1520
1678
  out("");
1679
+ let separated = false;
1521
1680
  for (const step of result.steps) {
1681
+ if (step.kind === "kept" && !separated) {
1682
+ separated = true;
1683
+ out("");
1684
+ out(` ${style.quiet("left alone")}`);
1685
+ }
1522
1686
  const unverified = step.kind === "revert" && !step.verified ? ` ${style.accent("[unverified]")}` : "";
1523
1687
  const kind = step.kind === "halt" || step.kind === "permanent" ? style.accent(step.kind.padEnd(16)) : step.kind.padEnd(16);
1524
1688
  out(