getclaros 0.1.1 → 0.3.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/README.md +97 -7
- package/dist/cli.js +60 -52
- package/dist/hooks.js +348 -0
- package/dist/projects.js +179 -0
- package/dist/setup.js +347 -48
- package/dist/ui.js +1 -1
- package/dist/verdict.js +54 -0
- package/package.json +2 -2
package/dist/setup.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
// getclaros init | test | undo | status (| export: a map's rules as markdown): puts the guard in front of
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// getclaros init | test | undo | doctor (= status) (| export: a map's rules as markdown): puts the guard in front of
|
|
2
|
+
// the MCP servers an agent client already runs, and in front of the browser agents (Playwright, browser-use) in this
|
|
3
|
+
// project. Edits only existing config files (plus, for Playwright Test, one new fixture file and CLAROS_KEY in a
|
|
4
|
+
// gitignored .env), backs each one up first (~/.config/getclaros/backups/<time>/, 0600), writes atomically, and never
|
|
5
|
+
// prints the key.
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
8
|
import { homedir, hostname } from "node:os";
|
|
7
|
-
import { basename, join } from "node:path";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
8
10
|
import { createInterface } from "node:readline/promises";
|
|
9
11
|
import { parseArgs } from "node:util";
|
|
10
12
|
import { addClaros, argvOf, CLAROS, detect, launcher, load, removeEntry, sha, unwrapServer, wrapServer, writeAtomic, } from "./clients.js";
|
|
11
|
-
import { cfgDir, loadConfig, relocate, resolve, saveConfig } from "./config.js";
|
|
13
|
+
import { cfgDir, DEFAULT_WEB, loadConfig, relocate, resolve, saveConfig } from "./config.js";
|
|
14
|
+
import { addHooks, hasHooks, hookAgents, hookFiles, removeHooks } from "./hooks.js";
|
|
15
|
+
import { detectBrowser, envTarget, envValue, fixtureImport, fixtureText, setEnv, short } from "./projects.js";
|
|
12
16
|
import { c, dur, header, mark, say, spinner, VERSION, wrap } from "./ui.js";
|
|
13
17
|
const NAME = launcher().at(-1) === "getclaros" ? "npx getclaros" : "getclaros";
|
|
14
18
|
const warn = (s) => process.stderr.write(s + "\n");
|
|
15
19
|
const tilde = (p) => p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p;
|
|
16
20
|
const keyId = (k) => k.split("_").slice(0, 2).join("_") + "_…"; // clr_<prefix>_… (the prefix is the lookup id, not the secret)
|
|
17
21
|
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
18
|
-
export const USAGE = `usage: ${NAME} init [--key clr_… | env CLAROS_KEY | --paste] [--no-browser] [--url <claros server> | --web <claros web app>] [--yes] [--servers a,b] [--project <dir>] [--no-mcp]
|
|
22
|
+
export const USAGE = `usage: ${NAME} init [--key clr_… | env CLAROS_KEY | --paste] [--no-browser] [--url <claros server> | --web <claros web app>] [--yes] [--servers a,b] [--project <dir>] [--no-mcp] [--no-browser-hooks]
|
|
23
|
+
[--no-hooks] [--user] [--fail-closed] (coding-agent hooks: project scope, --user for all projects)
|
|
19
24
|
${NAME} test [--app <server>]
|
|
20
|
-
${NAME} status
|
|
25
|
+
${NAME} doctor [--project <dir>] (status: the same)
|
|
21
26
|
${NAME} undo [--all]
|
|
22
27
|
${NAME} export <workflow_id> [--format agents|design|skill] [> AGENTS.md]
|
|
23
28
|
${NAME} override <check_id> --reason "…" [--rule <rule id>] [--verdict wrong_stop|wrong_nudge|missed|right]`;
|
|
@@ -28,8 +33,9 @@ export async function run(argv) {
|
|
|
28
33
|
({ values: v, positionals: pos } = parseArgs({ args: rest, allowPositionals: cmd === "export" || cmd === "override", options: {
|
|
29
34
|
url: { type: "string" }, web: { type: "string" }, key: { type: "string" }, yes: { type: "boolean", short: "y" }, servers: { type: "string" },
|
|
30
35
|
project: { type: "string" }, all: { type: "boolean" }, "no-mcp": { type: "boolean" }, app: { type: "string" },
|
|
31
|
-
paste: { type: "boolean" }, "no-browser": { type: "boolean" }, format: { type: "string" },
|
|
36
|
+
paste: { type: "boolean" }, "no-browser": { type: "boolean" }, "no-browser-hooks": { type: "boolean" }, format: { type: "string" },
|
|
32
37
|
reason: { type: "string" }, rule: { type: "string" }, verdict: { type: "string" },
|
|
38
|
+
user: { type: "boolean" }, "no-hooks": { type: "boolean" }, "fail-closed": { type: "boolean" },
|
|
33
39
|
} }));
|
|
34
40
|
}
|
|
35
41
|
catch (e) {
|
|
@@ -41,8 +47,8 @@ export async function run(argv) {
|
|
|
41
47
|
return init(v, project);
|
|
42
48
|
if (cmd === "test")
|
|
43
49
|
return test(v, project);
|
|
44
|
-
if (cmd === "status")
|
|
45
|
-
return
|
|
50
|
+
if (cmd === "status" || cmd === "doctor")
|
|
51
|
+
return doctor(v, project);
|
|
46
52
|
if (cmd === "undo")
|
|
47
53
|
return undo(v, project);
|
|
48
54
|
if (cmd === "export" && pos.length === 1 && ["agents", "design", "skill"].includes(v.format ?? "agents")) {
|
|
@@ -209,16 +215,20 @@ function askHidden(q) {
|
|
|
209
215
|
}
|
|
210
216
|
const backupsDir = () => join(cfgDir(), "backups");
|
|
211
217
|
const newRun = (suffix = "") => ({ dir: join(backupsDir(), new Date().toISOString().replace(/[:.]/g, "-") + suffix), files: [] });
|
|
212
|
-
/** Backs up `before
|
|
213
|
-
function commit(run, path, before, after) {
|
|
218
|
+
/** Backs up `before` (null: a new file), writes `after` atomically, records both hashes. */
|
|
219
|
+
function commit(run, path, before, after, vars) {
|
|
214
220
|
if (!run.files.length) {
|
|
215
221
|
mkdirSync(cfgDir(), { recursive: true, mode: 0o700 });
|
|
216
222
|
mkdirSync(run.dir, { recursive: true, mode: 0o700 });
|
|
217
223
|
}
|
|
218
224
|
const backup = join(run.dir, `${run.files.length}-${basename(path)}`);
|
|
219
|
-
writeFileSync(backup, before, { mode: 0o600 });
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
writeFileSync(backup, before ?? "", { mode: 0o600 });
|
|
226
|
+
if (before === null)
|
|
227
|
+
writeFileSync(path, after, { flag: "wx" });
|
|
228
|
+
else
|
|
229
|
+
writeAtomic(path, after);
|
|
230
|
+
run.files.push({ path, backup, before: sha(before ?? ""), after: sha(after), ...(before === null ? { created: true } : {}),
|
|
231
|
+
...(vars ? { vars } : {}) });
|
|
222
232
|
writeFileSync(join(run.dir, "manifest.json"), JSON.stringify({ files: run.files }, null, 2), { mode: 0o600 });
|
|
223
233
|
}
|
|
224
234
|
function runs() {
|
|
@@ -285,12 +295,52 @@ async function init(v, project) {
|
|
|
285
295
|
? join(cfgDir(), "config.json") : saveConfig({ url, web, key, ...(workspace ? { workspace } : {}) });
|
|
286
296
|
say(` ${c.ask(mark.ok)} Signed in${workspace?.name ? ` to workspace ${c.bold(`"${workspace.name}"`)}` : ""} ${c.dim(`key ${keyId(key)}`)}`);
|
|
287
297
|
say(c.dim(` ${url} ${mark.dot} key saved in ${tilde(cfg)}, readable only by you`));
|
|
288
|
-
const files = detect(project);
|
|
289
|
-
if (!files.length) {
|
|
298
|
+
const files = detect(project), browsers = detectBrowser(project), agents = v["no-hooks"] ? [] : hookAgents(project);
|
|
299
|
+
if (!files.length && !browsers.length && !agents.length) {
|
|
290
300
|
say(`\n See a stop now: ${c.bold(`${NAME} test`)}`);
|
|
291
|
-
say(c.dim(wrap(" To guard an agent
|
|
301
|
+
say(c.dim(wrap(" To guard an agent: add an MCP server to Claude Code, Claude Desktop, Cursor, VS Code or Codex (none has one yet), or run init in a Playwright or browser-use project, then run init again.", 2)));
|
|
292
302
|
return 1;
|
|
293
303
|
}
|
|
304
|
+
const run = newRun();
|
|
305
|
+
let mcp = { changed: [], guardedIn: new Map(), added: [] };
|
|
306
|
+
if (files.length) {
|
|
307
|
+
const r = await mcpSetup(v, files, url, key, run);
|
|
308
|
+
if (typeof r === "number")
|
|
309
|
+
return r;
|
|
310
|
+
mcp = r;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
say(`\n ${c.bold("MCP agents")}`);
|
|
314
|
+
say(c.dim(wrap(" none found (Claude Code, Claude Desktop, Cursor, VS Code, Codex); add an MCP server to one, then run init again", 4)));
|
|
315
|
+
}
|
|
316
|
+
const hk = await hookSetup(v, agents, project, run);
|
|
317
|
+
const br = await browserSetup(v, browsers, project, at, key, run);
|
|
318
|
+
const { guardedIn, added } = mcp, changed = [...mcp.changed, ...hk.clients];
|
|
319
|
+
if (!changed.length && !br.done.length) {
|
|
320
|
+
if (br.todo)
|
|
321
|
+
say(` ${c.bold("Next")} add the hook shown above, then run your browser agent`);
|
|
322
|
+
say(" Nothing to change.");
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
const list = (xs) => [...new Set(xs)].join(", ");
|
|
326
|
+
for (const [n, cl] of guardedIn)
|
|
327
|
+
say(` ${c.ask(mark.ok)} Guarded ${c.bold(n)} ${c.dim(`in ${list(cl)}`)}`);
|
|
328
|
+
if (added.length)
|
|
329
|
+
say(` ${c.ask(mark.ok)} Added the Claros MCP server ${c.dim(`in ${list(added)}`)}`);
|
|
330
|
+
for (const l of hk.done)
|
|
331
|
+
say(` ${c.ask(mark.ok)} ${l}`);
|
|
332
|
+
for (const l of br.done)
|
|
333
|
+
say(` ${c.ask(mark.ok)} ${l}`);
|
|
334
|
+
if (run.files.length)
|
|
335
|
+
say(c.dim(` every file backed up first: ${tilde(run.dir)}`));
|
|
336
|
+
say(`\n ${c.bold("Next")} ${changed.length ? `restart ${list(changed)}, then run ${c.violet(`${NAME} test`)}` : `run ${c.violet(`${NAME} test`)}`}`);
|
|
337
|
+
if (br.todo)
|
|
338
|
+
say(` add the browser hook shown above, then run your browser agent`);
|
|
339
|
+
say(c.dim(` check the setup: ${NAME} doctor ${mark.dot} undo anytime: ${NAME} undo`));
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
/** MCP agents: list each client's servers, ask which to guard, wrap them. A number = exit with it. */
|
|
343
|
+
async function mcpSetup(v, files, url, key, run) {
|
|
294
344
|
const maps = await api(url, key, "/api/workflows").then((r) => (Array.isArray(r.json) ? r.json : []), () => []);
|
|
295
345
|
const mapOf = (name) => maps.find((m) => (m.apps ?? []).some((a) => norm(a) && norm(name) && (norm(a).includes(norm(name)) || norm(name).includes(norm(a)))));
|
|
296
346
|
// per client: the servers it can guard (numbered, ● = matches a Work Map), the ones already guarded; the remote
|
|
@@ -298,7 +348,7 @@ async function init(v, project) {
|
|
|
298
348
|
const cands = [], gaps = new Map();
|
|
299
349
|
const all = files.flatMap((f) => f.servers);
|
|
300
350
|
const w = Math.min(28, Math.max(8, ...all.map((s) => (s.name + where(s)).length)));
|
|
301
|
-
say(`\n ${c.bold("MCP
|
|
351
|
+
say(`\n ${c.bold("MCP agents")}`);
|
|
302
352
|
for (const f of files) {
|
|
303
353
|
say(`\n ${c.bold(f.client)} ${c.dim(tilde(f.path))}`);
|
|
304
354
|
if (f.error)
|
|
@@ -375,7 +425,6 @@ async function init(v, project) {
|
|
|
375
425
|
else if (!picks.length && cands.length) {
|
|
376
426
|
say(c.dim(wrap(" None of these servers matches a Work Map yet, so none is guarded. Pick them with --servers a,b.", 2)));
|
|
377
427
|
}
|
|
378
|
-
const run = newRun();
|
|
379
428
|
const changed = [], guardedIn = new Map(), added = [];
|
|
380
429
|
const userClients = new Set(files.filter((f) => f.user && !f.error).map((f) => f.client));
|
|
381
430
|
for (const f of files.filter((x) => !x.error)) {
|
|
@@ -413,19 +462,131 @@ async function init(v, project) {
|
|
|
413
462
|
added.push(f.client);
|
|
414
463
|
}
|
|
415
464
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
465
|
+
return { changed, guardedIn, added };
|
|
466
|
+
}
|
|
467
|
+
// ---- coding-agent hooks (Claude Code, Codex, Cursor): shell commands and file edits checked before they run ----
|
|
468
|
+
/** Adds `getclaros hook <agent>` to each detected agent's hooks file (project scope; --user: the user's), merged
|
|
469
|
+
* into what is there. Never asks: like the Claros MCP entry, it is part of init (--no-hooks skips it). */
|
|
470
|
+
async function hookSetup(v, agents, project, run) {
|
|
471
|
+
const done = [], clients = [];
|
|
472
|
+
if (!agents.length)
|
|
473
|
+
return { done, clients };
|
|
474
|
+
say(`\n ${c.bold("Coding-agent hooks")} ${c.dim("shell commands and file edits checked before they run")}`);
|
|
475
|
+
const cmd = [...launcher(), "hook"].join(" ");
|
|
476
|
+
for (const a of agents) {
|
|
477
|
+
const f = hookFiles(project).find((x) => x.agent === a && x.user === !!v.user);
|
|
478
|
+
const before = existsSync(f.path) ? readFileSync(f.path, "utf8") : null;
|
|
479
|
+
let after;
|
|
480
|
+
try {
|
|
481
|
+
after = await addHooks(before ?? "", a, `${cmd} ${a}${v["fail-closed"] ? " --fail-closed" : ""}`);
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
warn(` ${tilde(f.path)}: ${e.message}; left unchanged`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (after === before) {
|
|
488
|
+
say(c.dim(` ${f.client}: already hooked in ${tilde(f.path)}`));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
say(` ${f.client.padEnd(12)} ${c.dim(tilde(f.path))}`);
|
|
492
|
+
if (before === null)
|
|
493
|
+
mkdirSync(dirname(f.path), { recursive: true });
|
|
494
|
+
commit(run, f.path, before, after);
|
|
495
|
+
done.push(`Hooked ${f.client} ${c.dim(`in ${tilde(f.path)}`)}`);
|
|
496
|
+
clients.push(f.client);
|
|
497
|
+
}
|
|
498
|
+
if (clients.includes("Codex"))
|
|
499
|
+
say(c.dim(wrap(" Codex runs a new hook only after you trust it: open /hooks in Codex once (project hooks also need a trusted project).", 4)));
|
|
500
|
+
return { done, clients };
|
|
501
|
+
}
|
|
502
|
+
const isHookPath = (p, project) => /[\\/]\.(claude[\\/]settings|codex[\\/]hooks|cursor[\\/]hooks)\.json$/.test(p) || hookFiles(project).some((f) => f.path === p);
|
|
503
|
+
// ---- browser agents (Playwright, browser-use) in this project ----
|
|
504
|
+
/** A shell line that exports the saved key without printing it. */
|
|
505
|
+
const exportKey = () => `export CLAROS_KEY="$(node -p "require('${join(cfgDir(), "config.json").replace(homedir(), "$HOME")}').key")"`;
|
|
506
|
+
/** The 2-line hook for a project, as lines to print (Playwright Test: the import of the fixture init writes). */
|
|
507
|
+
function hookLines(p) {
|
|
508
|
+
if (p.fixture)
|
|
509
|
+
return [fixtureImport(p)];
|
|
510
|
+
if (p.kind === "Playwright")
|
|
511
|
+
return ['import { guard } from "getclaros-browser";', "await guard(page); // right after you create the page"];
|
|
512
|
+
return ["from claros_browser import guard_tools", "guard_tools(tools) # the Tools() you pass to Agent(..., tools=tools)"];
|
|
513
|
+
}
|
|
514
|
+
/** Each detected project's plan (install, fixture, key, hook), a question per project (Enter = yes; --yes; skipped
|
|
515
|
+
* with --no-browser-hooks), then the install, the fixture file and CLAROS_KEY in a gitignored .env. User source
|
|
516
|
+
* files are never edited: the hook is printed. */
|
|
517
|
+
async function browserSetup(v, ps, project, at, key, run) {
|
|
518
|
+
const done = [];
|
|
519
|
+
let todo = false, wantKey = false;
|
|
520
|
+
if (!ps.length)
|
|
521
|
+
return { done, todo };
|
|
522
|
+
const env = envTarget(project);
|
|
523
|
+
const vars = { CLAROS_KEY: key,
|
|
524
|
+
...(at.source === "pinned" ? { CLAROS_URL: at.url } : at.web !== DEFAULT_WEB ? { CLAROS_WEB_URL: at.web } : {}) };
|
|
525
|
+
const keySet = Object.entries(vars).every(([k, x]) => process.env[k] === x || (env.ok && envValue(env.path, k) === x));
|
|
526
|
+
const rel = (f) => short(f, project);
|
|
527
|
+
say(`\n ${c.bold("Browser agents in this project")}`);
|
|
528
|
+
for (const p of ps) {
|
|
529
|
+
const row = (what, s) => say(` ${c.dim(what.padEnd(8))} ${s}`);
|
|
530
|
+
say(`\n ${c.bold(p.kind)} ${c.dim(`${tilde(p.dir)} ${mark.dot} ${p.via}`)}`);
|
|
531
|
+
row("install", p.installed ? c.dim("already installed") : p.install ? c.violet(p.install.map(tilde).join(" ")) : c.dim(p.installNote));
|
|
532
|
+
if (p.fixture && !existsSync(p.fixture))
|
|
533
|
+
row("write", `${rel(p.fixture)} ${c.dim("a fixture: every test's page goes through the guard")}`);
|
|
534
|
+
row("key", keySet ? c.dim("CLAROS_KEY already set") : env.ok ? `CLAROS_KEY ${mark.to} .env ${c.dim("gitignored")}`
|
|
535
|
+
: c.dim(`CLAROS_KEY: export it (${env.why})`));
|
|
536
|
+
if (p.hooked)
|
|
537
|
+
row("hook", c.dim("already in place"));
|
|
538
|
+
else {
|
|
539
|
+
say(` ${c.dim("hook".padEnd(8))} ${p.fixture ? "in" : "add to"} ${p.hookFile ? c.bold(rel(p.hookFile)) : p.fixture ? "each spec" : p.kind === "Playwright"
|
|
540
|
+
? "the file that opens the page" : "the file that builds your Agent"}${p.more ? ` (and ${p.more} more spec${p.more > 1 ? "s" : ""})` : ""}` +
|
|
541
|
+
c.dim(p.fixture ? `, instead of "@playwright/test":` : ":"));
|
|
542
|
+
for (const l of hookLines(p))
|
|
543
|
+
say(` ${c.violet(l)}`);
|
|
544
|
+
}
|
|
545
|
+
if (v["no-browser-hooks"]) {
|
|
546
|
+
say(c.dim(" skipped (--no-browser-hooks)"));
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (!v.yes) {
|
|
550
|
+
if (!process.stdin.isTTY) {
|
|
551
|
+
say(c.dim(` not set up: not a terminal, pass --yes`));
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const a = (await ask(` ${c.bold(`Set up ${p.kind}?`)} ${c.dim(`Enter = yes ${mark.dot} n = skip`)} ${c.violet(mark.arrow)} `)).trim().toLowerCase();
|
|
555
|
+
if (a && !a.startsWith("y"))
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!p.installed && p.install) {
|
|
559
|
+
say(c.dim(` running ${p.install.map(tilde).join(" ")}`));
|
|
560
|
+
const r = spawnSync(p.install[0], p.install.slice(1), { cwd: p.dir, stdio: ["ignore", 2, 2], shell: process.platform === "win32" });
|
|
561
|
+
if (r.status === 0)
|
|
562
|
+
done.push(`Installed ${p.kind === "Playwright" ? "getclaros-browser" : "claros-browser"} ${c.dim(`with ${p.install[0].includes("python") ? "pip" : p.install[0]}`)}`);
|
|
563
|
+
else
|
|
564
|
+
warn(` ${p.install.map(tilde).join(" ")} failed (${r.error?.message ?? `exit ${r.status}`}); run it yourself`);
|
|
565
|
+
}
|
|
566
|
+
if (p.fixture && !existsSync(p.fixture)) {
|
|
567
|
+
commit(run, p.fixture, null, fixtureText(p.fixture));
|
|
568
|
+
done.push(`Wrote ${rel(p.fixture)}`);
|
|
569
|
+
}
|
|
570
|
+
wantKey = true;
|
|
571
|
+
todo ||= !p.hooked;
|
|
419
572
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
573
|
+
say();
|
|
574
|
+
if (wantKey && !keySet) {
|
|
575
|
+
if (env.ok) {
|
|
576
|
+
const before = readFileSync(env.path, "utf8");
|
|
577
|
+
commit(run, env.path, before, setEnv(before, vars), Object.keys(vars));
|
|
578
|
+
done.push(`Set ${Object.keys(vars).join(", ")} in .env ${c.dim(`key ${keyId(key)} ${mark.dot} gitignored`)}`);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
say(` ${c.bold("CLAROS_KEY")} ${c.dim(`not written to a file (${env.why}); in the shell that runs the agent:`)}`);
|
|
582
|
+
say(` ${c.violet(exportKey())} ${c.dim(`# key ${keyId(key)}`)}`);
|
|
583
|
+
for (const [k, x] of Object.entries(vars))
|
|
584
|
+
if (k !== "CLAROS_KEY")
|
|
585
|
+
say(` ${c.violet(`export ${k}=${x}`)}`);
|
|
586
|
+
say();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { done, todo };
|
|
429
590
|
}
|
|
430
591
|
// ---- test ----
|
|
431
592
|
async function test(v, project) {
|
|
@@ -452,7 +613,10 @@ async function test(v, project) {
|
|
|
452
613
|
}
|
|
453
614
|
if (ex.status !== 200)
|
|
454
615
|
throw new Error(`HTTP ${ex.status}`);
|
|
455
|
-
const e = ex.json
|
|
616
|
+
const e = ex.json;
|
|
617
|
+
if (e.demo && e.examples?.length > 1)
|
|
618
|
+
return await demo(e, url, key, guarded);
|
|
619
|
+
const t0 = Date.now();
|
|
456
620
|
const r = await api(url, key, "/v1/check", { workflow_id: e.workflow_id, app: e.app ?? guarded[0] ?? undefined,
|
|
457
621
|
tool: "getclaros_test", actor: "getclaros test", state: e.state });
|
|
458
622
|
if (r.status !== 200)
|
|
@@ -466,14 +630,7 @@ async function test(v, project) {
|
|
|
466
630
|
const held = r.json.result === "ask", ink = held ? c.ask : c.stop;
|
|
467
631
|
say(` ${ink(c.bold(`${held ? mark.ask : mark.stop} ${held ? "Held to ask first" : "Stopped"} in ${dur(ms)}`))} ` +
|
|
468
632
|
c.dim(`Work Map "${e.name}" ${mark.dot} check ${r.json.check_id}`));
|
|
469
|
-
|
|
470
|
-
const ask = x.action === "ask", p = ask ? c.ask : c.stop;
|
|
471
|
-
say();
|
|
472
|
-
say(` ${p(`${ask ? mark.ask : mark.stop} ${ask ? "ask " : "stop"}`)} ${c.bold(wrap(x.rule, 12))}`);
|
|
473
|
-
if (x.quote)
|
|
474
|
-
say(` ${wrap(`“${x.quote}”`, 12)}${x.expert ? c.dim(` ${mark.dash} ${x.expert}`) : ""}`);
|
|
475
|
-
say(` ${c.dim("Who to ask:")} ${x.who_to_ask || x.owner || x.expert || "the process owner"}`);
|
|
476
|
-
}
|
|
633
|
+
stopCards(r.json);
|
|
477
634
|
say();
|
|
478
635
|
say(c.dim(wrap(` Test action for ${e.app ?? "a demo app"}, made up, only checked: ${Object.entries(e.state).map(([k, x]) => `${k} = ${x}`).join(", ")}`, 4)));
|
|
479
636
|
say(wrap(` Your agent gets the rule as the tool's error. Nothing reached ${e.app ?? "any app"}.`, 2));
|
|
@@ -486,6 +643,57 @@ async function test(v, project) {
|
|
|
486
643
|
return 3;
|
|
487
644
|
}
|
|
488
645
|
}
|
|
646
|
+
/** a card per stop / ask rule the check broke: the rule, the expert's words, who to ask */
|
|
647
|
+
function stopCards(r) {
|
|
648
|
+
for (const x of r.violations.filter((x) => x.action === "stop" || x.action === "ask")) {
|
|
649
|
+
const ask = x.action === "ask", p = ask ? c.ask : c.stop;
|
|
650
|
+
say();
|
|
651
|
+
say(` ${p(`${ask ? mark.ask : mark.stop} ${ask ? "ask " : "stop"}`)} ${c.bold(wrap(x.rule, 12))}`);
|
|
652
|
+
if (x.quote)
|
|
653
|
+
say(` ${wrap(`“${x.quote}”`, 12)}${x.expert ? c.dim(` ${mark.dash} ${x.expert}`) : ""}`);
|
|
654
|
+
say(` ${c.dim("Who to ask:")} ${x.who_to_ask || x.owner || x.expert || "the process owner"}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/** a made-up record on one line: whitespace in strings collapsed, long values cut */
|
|
658
|
+
const record = (tool, state) => `${tool ? `${tool} ` : ""}${Object.entries(state).map(([k, x]) => {
|
|
659
|
+
const v = typeof x === "string" ? x.replace(/\s+/g, " ").trim() : String(x);
|
|
660
|
+
return `${k} = ${v.length > 80 ? v.slice(0, 79) + "…" : v}`;
|
|
661
|
+
}).join(", ")}`;
|
|
662
|
+
/** demo (no guarded server matches a map): the starter maps' examples, a stop then a nudge, checked side by side */
|
|
663
|
+
async function demo(e, url, key, guarded) {
|
|
664
|
+
const t0 = Date.now();
|
|
665
|
+
const rs = await Promise.all(e.examples.map((x) => api(url, key, "/v1/check", { workflow_id: x.workflow_id, tool: x.tool, actor: "getclaros test", state: x.state })));
|
|
666
|
+
const bad = rs.find((r) => r.status !== 200);
|
|
667
|
+
if (bad)
|
|
668
|
+
throw new Error(`HTTP ${bad.status}`);
|
|
669
|
+
const ms = Date.now() - t0, [stop, ...rest] = rs.map((r) => r.json), [se, ...re] = e.examples;
|
|
670
|
+
if (stop.allowed) {
|
|
671
|
+
say(` ${c.stop(mark.no)} Claros let it through (check ${stop.check_id}). It should have been stopped by rule ${se.guardrail_id}; tell the Claros team.`);
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
const held = stop.result === "ask", nudged = rest.some((r) => r.result === "nudge");
|
|
675
|
+
const outcome = [(held ? c.ask : c.stop)(c.bold(`${held ? mark.ask : mark.stop} ${held ? "Held to ask first" : "Stopped"}`)),
|
|
676
|
+
...(nudged ? [c.nudge(c.bold(`${mark.nudge} Nudged`))] : [])].join(` ${mark.dot} `);
|
|
677
|
+
say(` ${outcome} ${c.dim(`${rs.length} checks in ${dur(ms)}`)}`);
|
|
678
|
+
stopCards(stop);
|
|
679
|
+
say(c.dim(` ${wrap(`Test action: ${record(se.tool, se.state)}`, 12)}`));
|
|
680
|
+
rest.forEach((r, i) => {
|
|
681
|
+
const x = r.violations.find((v) => v.action === "nudge"), ex = re[i];
|
|
682
|
+
say();
|
|
683
|
+
say(` ${c.nudge(`${mark.nudge} nudge`)} ${c.bold(wrap(x?.rule ?? ex.rule ?? `Work Map "${ex.name}"`, 12))}`);
|
|
684
|
+
if (x?.quote)
|
|
685
|
+
say(` ${wrap(`“${x.quote}”`, 12)}${x.expert ? c.dim(` ${mark.dash} ${x.expert}`) : ""}`);
|
|
686
|
+
if (x?.fix)
|
|
687
|
+
say(` ${c.nudge(mark.to)} ${wrap(x.fix, 14)}`);
|
|
688
|
+
else if (!x)
|
|
689
|
+
say(c.dim(` Not judged this time (${r.judge?.error ?? r.judge?.status ?? "the server's judge is off"}).`));
|
|
690
|
+
say(c.dim(` ${wrap(`Test action: ${record(ex.tool, ex.state)}`, 12)}`));
|
|
691
|
+
});
|
|
692
|
+
say();
|
|
693
|
+
say(wrap(" Your agent gets a stop as the tool's error; a nudge lets the call through, with the fix. Nothing reached any app.", 2));
|
|
694
|
+
say(c.dim(wrap(` Demo: ${guarded.length ? `none of your guarded servers (${guarded.join(", ")}) matches a Work Map` : "no server is guarded yet"}, so this used the Starter maps (examples, not enforced; delete them on the web).`, 2)));
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
489
697
|
// ---- export ----
|
|
490
698
|
/** A Work Map's rules as markdown on stdout (GET /v1/maps/<id>/export): redirect it into AGENTS.md, DESIGN.md or a
|
|
491
699
|
* skill's SKILL.md. An advisory copy: Claros still enforces the rules when an agent acts. */
|
|
@@ -542,8 +750,10 @@ async function override(v, checkId) {
|
|
|
542
750
|
return 3;
|
|
543
751
|
}
|
|
544
752
|
}
|
|
545
|
-
// ---- status ----
|
|
546
|
-
|
|
753
|
+
// ---- doctor (status is the same command) ----
|
|
754
|
+
/** Per agent type: is it guarded and can it reach Claros, with the command that fixes what isn't.
|
|
755
|
+
* Exit: 0 all good, 1 a browser agent here isn't set up, 2 no key, 3 Claros unreachable or the key rejected. */
|
|
756
|
+
async function doctor(v, project) {
|
|
547
757
|
header();
|
|
548
758
|
const at = await locate(v);
|
|
549
759
|
if (!at)
|
|
@@ -556,7 +766,16 @@ async function status(v, project) {
|
|
|
556
766
|
const fine = up && ks === "valid";
|
|
557
767
|
say(` ${ok(fine, fine ? mark.ok : mark.no)} ${c.bold("Claros")} ${url} ${ok(up, up ? "reachable" : "unreachable")}` +
|
|
558
768
|
` key ${key ? keyId(key) + " " : ""}${ok(ks === "valid", ks)}${ws ? ` workspace "${ws}"` : ""}`);
|
|
559
|
-
|
|
769
|
+
const fix = (s) => say(` ${c.violet(mark.to)} ${s}`);
|
|
770
|
+
if (!key || /rejected|scope/.test(ks))
|
|
771
|
+
fix(`${NAME} init ${c.dim("signs in and saves an agent key")}`);
|
|
772
|
+
else if (!up)
|
|
773
|
+
fix(c.dim(`check your network; a moved server is followed on the next run (or pass --url)`));
|
|
774
|
+
const files = detect(project);
|
|
775
|
+
say(`\n ${c.bold("MCP agents")}`);
|
|
776
|
+
if (!files.length)
|
|
777
|
+
say(c.dim(" none found (Claude Code, Claude Desktop, Cursor, VS Code, Codex)"));
|
|
778
|
+
for (const f of files) {
|
|
560
779
|
say(`\n ${c.bold(f.client)} ${c.dim(tilde(f.path))}${f.error ? ` couldn't read it (${f.error})` : ""}`);
|
|
561
780
|
for (const s of f.servers) {
|
|
562
781
|
const [m, state] = s.ours ? [c.violet(mark.dot), c.dim("Claros MCP server")] : s.guarded ? [c.ask(mark.ok), c.ask("guarded")]
|
|
@@ -564,9 +783,67 @@ async function status(v, project) {
|
|
|
564
783
|
say(` ${m} ${(s.name + where(s)).padEnd(24)} ${state}`);
|
|
565
784
|
}
|
|
566
785
|
}
|
|
567
|
-
|
|
786
|
+
const servers = files.flatMap((f) => f.servers.filter((s) => !s.ours));
|
|
787
|
+
if (servers.some((s) => !s.guarded && !s.gap))
|
|
788
|
+
fix(`${NAME} init ${c.dim(`guards the servers you pick${servers.some((s) => s.guarded) ? "" : " (none is guarded yet)"}`)}`);
|
|
789
|
+
// coding-agent hooks: per agent, the files that hold ours
|
|
790
|
+
const agents = hookAgents(project), hooked = new Map();
|
|
791
|
+
for (const f of hookFiles(project))
|
|
792
|
+
if (await hasHooks(f.path))
|
|
793
|
+
hooked.set(f.client, [...(hooked.get(f.client) ?? []), tilde(f.path)]);
|
|
794
|
+
const named = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor" };
|
|
795
|
+
const clientsHere = [...new Set([...agents.map((a) => named[a]), ...hooked.keys()])];
|
|
796
|
+
if (clientsHere.length)
|
|
797
|
+
say(`\n ${c.bold("Coding-agent hooks")} ${c.dim("shell commands and file edits")}`);
|
|
798
|
+
for (const cl of clientsHere) {
|
|
799
|
+
const at = hooked.get(cl);
|
|
800
|
+
say(` ${at ? c.ask(mark.ok) : c.dim(mark.off)} ${cl.padEnd(24)} ${at ? `${c.ask("hooked")} ${c.dim(at.join(", "))}` : "not hooked"}`);
|
|
801
|
+
}
|
|
802
|
+
if (clientsHere.some((cl) => !hooked.has(cl)))
|
|
803
|
+
fix(`${NAME} init ${c.dim("adds the hooks (project scope; --user for all projects)")}`);
|
|
804
|
+
// browser agents: package installed, hook present, a key the agent can read (env or a safe .env)
|
|
805
|
+
let broken = false;
|
|
806
|
+
const ps = detectBrowser(project);
|
|
807
|
+
if (ps.length)
|
|
808
|
+
say(`\n ${c.bold("Browser agents in this project")}`);
|
|
809
|
+
const env = envTarget(project), fromFile = envValue(env.path, "CLAROS_KEY");
|
|
810
|
+
const agentKey = process.env.CLAROS_KEY || fromFile;
|
|
811
|
+
const aks = !ps.length || !agentKey ? "" : agentKey === key ? ks : await keyStatus(url, agentKey);
|
|
812
|
+
for (const p of ps) {
|
|
813
|
+
const row = (good, s, hint) => {
|
|
814
|
+
broken ||= !good;
|
|
815
|
+
say(` ${ok(good, good ? mark.ok : mark.no)} ${s}`);
|
|
816
|
+
if (!good && hint)
|
|
817
|
+
fix(hint);
|
|
818
|
+
};
|
|
819
|
+
say(`\n ${c.bold(p.kind)} ${c.dim(`${tilde(p.dir)} ${mark.dot} ${p.via}`)}`);
|
|
820
|
+
const pkg = p.kind === "Playwright" ? "getclaros-browser" : "claros-browser";
|
|
821
|
+
row(p.installed, `${pkg} ${p.installed ? "installed" : "not installed"}`, p.install ? `${p.install.map(tilde).join(" ")} ${c.dim(`(or ${NAME} init)`)}` : p.installNote);
|
|
822
|
+
row(p.hooked, p.hooked ? `hook in place${p.fixture ? ` (${short(p.fixture, project)})` : ""}` : "no hook yet", p.fixture && !existsSync(p.fixture) ? `${NAME} init ${c.dim(`writes ${short(p.fixture, project)}; then import from it in your specs`)}`
|
|
823
|
+
: `${p.hookFile ? `in ${short(p.hookFile, project)}` : "add"}:\n${hookLines(p).map((l) => ` ${l}`).join("\n")}`);
|
|
824
|
+
if (!agentKey)
|
|
825
|
+
row(false, "CLAROS_KEY not set", env.ok ? `${NAME} init ${c.dim("writes it to .env (gitignored)")}` : `${exportKey()} ${c.dim(`(${env.why})`)}`);
|
|
826
|
+
else if (fromFile && !process.env.CLAROS_KEY && !env.ok) {
|
|
827
|
+
row(false, `CLAROS_KEY is in .env, but ${env.why}`, `move it out of .env, and make a new key if it was ever committed`);
|
|
828
|
+
}
|
|
829
|
+
else
|
|
830
|
+
row(aks === "valid", `CLAROS_KEY ${keyId(agentKey)} from ${process.env.CLAROS_KEY ? "the environment" : ".env"} ${aks}`, `${NAME} init ${c.dim("writes a valid key")}`);
|
|
831
|
+
}
|
|
832
|
+
return !key ? 2 : !(up && ks === "valid") ? 3 : broken ? 1 : 0;
|
|
568
833
|
}
|
|
569
834
|
// ---- undo ----
|
|
835
|
+
/** Our hook entries out of a hooks file that changed since init; the rest stays. */
|
|
836
|
+
async function unhook(safety, path, cur) {
|
|
837
|
+
try {
|
|
838
|
+
const t = await removeHooks(cur);
|
|
839
|
+
if (t !== cur)
|
|
840
|
+
commit(safety, path, cur, t);
|
|
841
|
+
say(` ${c.ask(mark.ok)} ${tilde(path)}: changed since init, so removed the Claros hooks in place and kept the other edits`);
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
warn(`${tilde(path)}: couldn't read it (${e.message}); remove the getclaros hook entries yourself`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
570
847
|
/** Unwraps every guarded server and removes our Claros entry in place; the rest of the file stays. */
|
|
571
848
|
function unwrapAll(f) {
|
|
572
849
|
let n = 0;
|
|
@@ -601,10 +878,29 @@ async function undo(v, project) {
|
|
|
601
878
|
}
|
|
602
879
|
const cur = readFileSync(path, "utf8");
|
|
603
880
|
const same = sha(cur) === chain.at(-1).after && chain.every((b, i) => !i || b.before === chain[i - 1].after);
|
|
604
|
-
|
|
881
|
+
const vars = chain.flatMap((b) => b.vars ?? []);
|
|
882
|
+
if (chain[0].created) { // a file init wrote (the Playwright fixture): deleted only while it's exactly what init wrote
|
|
883
|
+
if (same) {
|
|
884
|
+
unlinkSync(path);
|
|
885
|
+
say(` ${c.ask(mark.ok)} ${tilde(path)}: removed (init wrote it)`);
|
|
886
|
+
}
|
|
887
|
+
else if (isHookPath(path, project))
|
|
888
|
+
await unhook(safety, path, cur);
|
|
889
|
+
else
|
|
890
|
+
warn(`${tilde(path)}: changed since init wrote it; left in place (delete it yourself)`);
|
|
891
|
+
}
|
|
892
|
+
else if (!same && vars.length) { // a .env edited since: drop only the lines init set
|
|
893
|
+
const t = cur.replace(new RegExp(`^(export\\s+)?(${vars.join("|")})=.*(\\r?\\n|$)`, "gm"), "");
|
|
894
|
+
if (t !== cur)
|
|
895
|
+
commit(safety, path, cur, t);
|
|
896
|
+
say(` ${c.ask(mark.ok)} ${tilde(path)}: changed since init, so removed ${vars.join(", ")} and kept the other lines`);
|
|
897
|
+
}
|
|
898
|
+
else if (same) {
|
|
605
899
|
commit(safety, path, cur, readFileSync(chain[0].backup, "utf8"));
|
|
606
900
|
say(` ${c.ask(mark.ok)} ${tilde(path)}: restored from the backup taken before init (unchanged since)`);
|
|
607
901
|
}
|
|
902
|
+
else if (isHookPath(path, project))
|
|
903
|
+
await unhook(safety, path, cur);
|
|
608
904
|
else {
|
|
609
905
|
const f = byPath.get(path) ?? detect(project).find((x) => x.path === path);
|
|
610
906
|
if (!f || f.error) {
|
|
@@ -619,6 +915,9 @@ async function undo(v, project) {
|
|
|
619
915
|
byPath.delete(path);
|
|
620
916
|
}
|
|
621
917
|
if (v.all) {
|
|
918
|
+
for (const f of hookFiles(project))
|
|
919
|
+
if (!chains.has(f.path) && await hasHooks(f.path))
|
|
920
|
+
await unhook(safety, f.path, readFileSync(f.path, "utf8"));
|
|
622
921
|
for (const f of byPath.values()) {
|
|
623
922
|
if (f.error)
|
|
624
923
|
continue;
|
package/dist/ui.js
CHANGED
|
@@ -27,7 +27,7 @@ const U = S.uni;
|
|
|
27
27
|
/** stop = the seal, ask = the thread hooked to a person, nudge = the proofreader's pencil */
|
|
28
28
|
export const mark = {
|
|
29
29
|
stop: U ? "◉" : "x", ask: U ? "↳" : "?", nudge: U ? "✎" : "~", ok: U ? "✓" : "+", no: U ? "✗" : "x",
|
|
30
|
-
on: U ? "●" : "*", off: U ? "○" : "o", dot: U ? "·" : "-", arrow: U ? "›" : ">", dash: U ? "—" : "-",
|
|
30
|
+
on: U ? "●" : "*", off: U ? "○" : "o", dot: U ? "·" : "-", arrow: U ? "›" : ">", dash: U ? "—" : "-", to: U ? "→" : "->",
|
|
31
31
|
};
|
|
32
32
|
export const say = (s = "") => process.stdout.write(s + "\n");
|
|
33
33
|
/** claros” and the version: only on a terminal (piped output starts with what it says) */
|
package/dist/verdict.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// What an agent reads when Claros stops, holds or nudges an action: shared by the MCP guard (cli.ts) and the coding-agent
|
|
2
|
+
// hooks (hooks.ts).
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
const NAME = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).name;
|
|
5
|
+
/** `head` replaces the first line (a decided ask: who said no, or that nobody answered). */
|
|
6
|
+
export function stopText(r, head) {
|
|
7
|
+
const hits = (r.violations || []).filter((v) => v.action === "stop" || v.action === "ask");
|
|
8
|
+
const maps = [...new Set(hits.map((v) => v.workflow_id ?? r.workflow_id))];
|
|
9
|
+
const from = `expert-approved Work Map${maps.length > 1 ? "s" : ""} ${maps.map((m) => `"${m}"`).join(", ")}`;
|
|
10
|
+
const out = [head ?? (r.result === "ask" // ask only: not refused for good, waiting on a person
|
|
11
|
+
? `Claros held this action: ${hits.length === 1 ? "a rule" : "rules"} from the ${from} ${hits.length === 1 ? "says" : "say"} to ask first. Do not run it without that answer.`
|
|
12
|
+
: `Claros stopped this action: it breaks ${hits.length === 1 ? "a rule" : "rules"} from the ${from}. Do not retry it as is.`)];
|
|
13
|
+
for (const v of hits) {
|
|
14
|
+
const who = v.who_to_ask || v.owner || v.expert || "the process owner";
|
|
15
|
+
out.push("", `Rule: ${v.rule}`);
|
|
16
|
+
if (v.reason)
|
|
17
|
+
out.push(`Why Claros stopped it: ${v.reason}`); // fuzzy rules: the judge's one-line reason
|
|
18
|
+
if (v.quote)
|
|
19
|
+
out.push(`${v.source === "doc" ? "In the written policy" : "In the expert's words"}${v.expert ? ` (${v.expert})` : ""}: "${v.quote}"`);
|
|
20
|
+
out.push(`Who to ask: ${who}`);
|
|
21
|
+
out.push(v.action === "ask"
|
|
22
|
+
? `Next: stop and ask ${who} before doing this; continue only with their answer.`
|
|
23
|
+
: `Next: change the record so the rule holds, or ask ${who} to approve an exception. Tell the user why.`);
|
|
24
|
+
}
|
|
25
|
+
// an ask opened a question to the rule's owner (server: knowledge/approvals.py); its answer is at the decision URL
|
|
26
|
+
if (r.question?.decision_url && !head)
|
|
27
|
+
out.push("", `Claros asked the rule's owner (check ${r.check_id}); their answer: GET ${r.question.decision_url}`);
|
|
28
|
+
// an override goes to the rule's expert as a report; the rule stays as it is until they confirm a change
|
|
29
|
+
if (r.check_id) {
|
|
30
|
+
out.push("", "If this stop is wrong, tell the rule's expert why (the rule stays until they change it):");
|
|
31
|
+
for (const v of hits)
|
|
32
|
+
out.push(`npx ${NAME} override ${r.check_id} --rule ${v.guardrail_id} --reason "<why it was fine>"`);
|
|
33
|
+
}
|
|
34
|
+
return out.join("\n");
|
|
35
|
+
}
|
|
36
|
+
// a nudge lets the call through; "warn" is its old name (older Claros servers)
|
|
37
|
+
export const nudgesOf = (r) => (r.violations || []).filter((v) => v.action === "nudge" || v.action === "warn");
|
|
38
|
+
/** The extra text item on a nudged call's result: the rule, the expert's words and the fix, so the agent can
|
|
39
|
+
* correct this record (if it still can) and the next ones. */
|
|
40
|
+
export function nudgeText(r) {
|
|
41
|
+
const hits = nudgesOf(r);
|
|
42
|
+
const out = [`Claros nudge: the call went through, but it doesn't follow ${hits.length === 1 ? "a rule" : "rules"} ` +
|
|
43
|
+
`your team holds. Fix it if you still can, and follow ${hits.length === 1 ? "it" : "them"} from now on.`];
|
|
44
|
+
for (const v of hits) {
|
|
45
|
+
out.push("", `Rule: ${v.rule}`);
|
|
46
|
+
if (v.reason)
|
|
47
|
+
out.push(`Why: ${v.reason}`);
|
|
48
|
+
if (v.quote)
|
|
49
|
+
out.push(`${v.source === "doc" ? "In the written policy" : "In the expert's words"}${v.expert ? ` (${v.expert})` : ""}: "${v.quote}"`);
|
|
50
|
+
if (v.fix)
|
|
51
|
+
out.push(`Suggested fix: ${v.fix}`);
|
|
52
|
+
}
|
|
53
|
+
return out.join("\n");
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getclaros",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claros guard: checks an agent's MCP tool calls against your experts' rules before they run",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Claros guard: checks an agent's MCP tool calls, shell commands and file edits against your experts' rules before they run",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|