@stroq/cli 0.19.2 → 0.20.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 +1 -0
- package/dist/index.js +702 -187
- package/openclaw-plugin/openclaw.plugin.json +1 -1
- package/openclaw-plugin/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ MCP proxy note: for clients with no hook API, `--agent mcp` rewrites the client'
|
|
|
51
51
|
| Command | What it does |
|
|
52
52
|
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
53
|
| `stroq init [--agent <name>] [--user] [--dry-run]` | Install hooks for `claude-code`, `cursor`, `codex`, `copilot`, `openclaw`, `windsurf` or `antigravity`, or wrap an MCP client's stdio servers with `--agent mcp --client <name>` or `--config <path>` (`--user` for the home-directory copy) |
|
|
54
|
+
| `stroq uninstall [--agent <name>] [--user] [--dry-run]` | Take Stroq's hooks out of an agent's config, leaving everything else as it was |
|
|
54
55
|
| `stroq hook <agent>` | Hook entrypoint (reads the event on stdin; `copilot` and `openclaw` take a `pre`/`post` argument, the others do not) |
|
|
55
56
|
| `stroq mcp --server <name> -- <cmd>` | MCP stdio proxy: judges every `tools/call` and scans every result for one wrapped server |
|
|
56
57
|
| `stroq doctor` | Check Node version, rules, hooks for every agent, self-test |
|
package/dist/index.js
CHANGED
|
@@ -1488,8 +1488,27 @@ function isSelfConfigReadOnly(segment2, word) {
|
|
|
1488
1488
|
var OPENCLAW_PLUGIN_OFF = /\bopenclaw\s+plugins\s+(?:disable|remove|uninstall)\b/;
|
|
1489
1489
|
var OPENCLAW_PLUGIN_CONFIG_WRITE = /\bopenclaw\s+config\s+set\s+plugins\./;
|
|
1490
1490
|
var disablesAgentPlugin = (segment2) => OPENCLAW_PLUGIN_OFF.test(segment2) || OPENCLAW_PLUGIN_CONFIG_WRITE.test(segment2);
|
|
1491
|
+
var STROQ_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "bunx", "yarn", "npm", "node"]);
|
|
1492
|
+
var STROQ_PACKAGE = /^@stroq\/cli(?:@\S*)?$/;
|
|
1493
|
+
var STROQ_ENTRY = /[\\/]@stroq[\\/]cli[\\/]dist[\\/]index\.js$/;
|
|
1494
|
+
function changesStroqState(segment2) {
|
|
1495
|
+
const word = commandWord(segment2);
|
|
1496
|
+
if (word !== "stroq" && !STROQ_RUNNERS.has(word)) return false;
|
|
1497
|
+
const tokens = tokenize(segment2).map((token) => token.replace(/["']/g, ""));
|
|
1498
|
+
const at = tokens.findIndex(
|
|
1499
|
+
(token) => word === "stroq" ? token.replace(/^.*[\\/]/, "") === "stroq" : STROQ_PACKAGE.test(token) || STROQ_ENTRY.test(token)
|
|
1500
|
+
);
|
|
1501
|
+
if (at === -1) return false;
|
|
1502
|
+
const args = tokens.slice(at + 1).filter((token) => token !== "--");
|
|
1503
|
+
if (args.includes("--dry-run")) return false;
|
|
1504
|
+
const sub = args.find((token) => !token.startsWith("-"));
|
|
1505
|
+
if (sub === "untaint" || sub === "init" || sub === "uninstall") return true;
|
|
1506
|
+
if (sub !== "trust") return false;
|
|
1507
|
+
const after = args.slice(args.indexOf("trust") + 1);
|
|
1508
|
+
return after.includes("--remove") || after.some((token) => !token.startsWith("-"));
|
|
1509
|
+
}
|
|
1491
1510
|
function classifySelfConfigSegment(segment2) {
|
|
1492
|
-
if (disablesAgentPlugin(segment2)) return "deny";
|
|
1511
|
+
if (disablesAgentPlugin(segment2) || changesStroqState(segment2)) return "deny";
|
|
1493
1512
|
const word = commandWord(segment2);
|
|
1494
1513
|
if (!touchesSelfConfig(segment2, word)) return null;
|
|
1495
1514
|
if (isSelfConfigWriteIntent(segment2, word)) return "deny";
|
|
@@ -1534,7 +1553,9 @@ function selfTamperSignals(segments) {
|
|
|
1534
1553
|
for (const segment2 of segments) {
|
|
1535
1554
|
const verdict = classifySelfConfigSegment(segment2);
|
|
1536
1555
|
if (verdict === "deny" || verdict === "ask" && interpreterInlineElsewhere) {
|
|
1537
|
-
deny.push(
|
|
1556
|
+
deny.push(
|
|
1557
|
+
changesStroqState(segment2) ? "stroq-state-change" : disablesAgentPlugin(segment2) ? "self-config-disable" : "self-config-write"
|
|
1558
|
+
);
|
|
1538
1559
|
} else if (verdict === "ask") {
|
|
1539
1560
|
ask.push("self-config-touch");
|
|
1540
1561
|
}
|
|
@@ -1963,10 +1984,9 @@ var FileTrustStore = class {
|
|
|
1963
1984
|
this.cache = parseTrustList(raw);
|
|
1964
1985
|
return this.cache;
|
|
1965
1986
|
}
|
|
1966
|
-
trusts(
|
|
1967
|
-
if (source === "") return false;
|
|
1987
|
+
trusts(text) {
|
|
1968
1988
|
const digest = trustDigest(text);
|
|
1969
|
-
return this.load().entries.some((e) => e.
|
|
1989
|
+
return this.load().entries.some((e) => e.sha256 === digest);
|
|
1970
1990
|
}
|
|
1971
1991
|
list() {
|
|
1972
1992
|
return this.load().entries;
|
|
@@ -2019,7 +2039,7 @@ var DEFAULT_POLICY = {
|
|
|
2019
2039
|
{
|
|
2020
2040
|
id: "deny-self-tamper",
|
|
2021
2041
|
effect: "deny",
|
|
2022
|
-
reason: "Modifying agent security configuration is blocked",
|
|
2042
|
+
reason: "Modifying agent security configuration is blocked; that change is yours to make, outside the agent (to take Stroq out: stroq uninstall)",
|
|
2023
2043
|
when: { classes: ["config.self"], taint: "any" }
|
|
2024
2044
|
},
|
|
2025
2045
|
{
|
|
@@ -2041,7 +2061,7 @@ var DEFAULT_POLICY = {
|
|
|
2041
2061
|
{
|
|
2042
2062
|
id: "deny-origin-suspect",
|
|
2043
2063
|
effect: "deny",
|
|
2044
|
-
reason: "Action was dictated by content Stroq flagged as suspicious; blocked
|
|
2064
|
+
reason: "Action was dictated by content Stroq flagged as suspicious; blocked",
|
|
2045
2065
|
when: { classes: ["origin.suspect"], taint: "any" }
|
|
2046
2066
|
},
|
|
2047
2067
|
{
|
|
@@ -2636,6 +2656,53 @@ function addCanaryFile(file, absolutePath) {
|
|
|
2636
2656
|
renameSync(temp, file);
|
|
2637
2657
|
}
|
|
2638
2658
|
|
|
2659
|
+
// ../core/src/util/controls.ts
|
|
2660
|
+
var UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f--]/g;
|
|
2661
|
+
function neutralizeControls(text) {
|
|
2662
|
+
return text.replace(UNSAFE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// ../core/src/provenance/describe.ts
|
|
2666
|
+
function ageLabel(fromIso, now) {
|
|
2667
|
+
const from = Date.parse(fromIso);
|
|
2668
|
+
if (Number.isNaN(from)) return "unknown time";
|
|
2669
|
+
const seconds = Math.max(0, Math.round((now.getTime() - from) / 1e3));
|
|
2670
|
+
if (seconds < 90) return `${seconds} s`;
|
|
2671
|
+
if (seconds < 5400) return `${Math.round(seconds / 60)} min`;
|
|
2672
|
+
return `${Math.round(seconds / 3600)} h`;
|
|
2673
|
+
}
|
|
2674
|
+
function toEvidence(hit) {
|
|
2675
|
+
const { kind, excerpt, tool, source, at, suspect } = hit.record;
|
|
2676
|
+
return { kind, excerpt, tool, source, at, suspect };
|
|
2677
|
+
}
|
|
2678
|
+
function describeEvidence(evidence, now) {
|
|
2679
|
+
const flagged = evidence.suspect ? "Stroq flagged that content as suspicious." : "that content was not flagged, but tool output is data, not instructions.";
|
|
2680
|
+
const quoted = neutralizeControls(
|
|
2681
|
+
`"${evidence.excerpt}" appeared in the output of ${evidence.tool} (${evidence.source})`
|
|
2682
|
+
);
|
|
2683
|
+
return `${quoted} ${ageLabel(evidence.at, now)} ago; ${flagged}`;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// ../core/src/policy/way-out.ts
|
|
2687
|
+
var SAFE_WORD = /^[\w@%+=:,./-]+$/;
|
|
2688
|
+
var shellQuote = (word) => SAFE_WORD.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2689
|
+
function withWayOut(decision, policy, taint, sessionId, now) {
|
|
2690
|
+
if (decision.effect === "allow" || decision.ruleId === null) return decision;
|
|
2691
|
+
const rule = policy.rules.find((r) => r.id === decision.ruleId);
|
|
2692
|
+
if (rule === void 0) return decision;
|
|
2693
|
+
const byTaint = rule.when.taint === "suspect" && taint !== null;
|
|
2694
|
+
const byOrigin = rule.when.classes.includes("origin.suspect");
|
|
2695
|
+
if (!byTaint && !byOrigin) return decision;
|
|
2696
|
+
const source = byTaint ? taint.sources.at(-1) : void 0;
|
|
2697
|
+
const path = source?.source === void 0 ? "" : neutralizeControls(source.source);
|
|
2698
|
+
const what = source === void 0 ? "" : ` Tainted by ${source.tool}${path === "" ? "" : ` ${path}`} ${ageLabel(source.at, now)} ago.`;
|
|
2699
|
+
const trust = source?.tool === "Read" && path !== "" ? `; to stop that file tainting it again: stroq trust ${shellQuote(path)}` : "";
|
|
2700
|
+
return {
|
|
2701
|
+
...decision,
|
|
2702
|
+
reason: `${decision.reason.replace(/\.$/, "")}.${what} If that was a false positive, clear it yourself, outside the agent: stroq untaint --session ${shellQuote(sessionId)}${trust}`
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2639
2706
|
// ../core/src/provenance/action-atoms.ts
|
|
2640
2707
|
import { readdirSync } from "node:fs";
|
|
2641
2708
|
import { join as join3 } from "node:path";
|
|
@@ -2944,33 +3011,6 @@ function originClasses(hits, classes) {
|
|
|
2944
3011
|
return { classes: out, counted };
|
|
2945
3012
|
}
|
|
2946
3013
|
|
|
2947
|
-
// ../core/src/util/controls.ts
|
|
2948
|
-
var UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f--]/g;
|
|
2949
|
-
function neutralizeControls(text) {
|
|
2950
|
-
return text.replace(UNSAFE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
// ../core/src/provenance/describe.ts
|
|
2954
|
-
function ageLabel(fromIso, now) {
|
|
2955
|
-
const from = Date.parse(fromIso);
|
|
2956
|
-
if (Number.isNaN(from)) return "unknown time";
|
|
2957
|
-
const seconds = Math.max(0, Math.round((now.getTime() - from) / 1e3));
|
|
2958
|
-
if (seconds < 90) return `${seconds} s`;
|
|
2959
|
-
if (seconds < 5400) return `${Math.round(seconds / 60)} min`;
|
|
2960
|
-
return `${Math.round(seconds / 3600)} h`;
|
|
2961
|
-
}
|
|
2962
|
-
function toEvidence(hit) {
|
|
2963
|
-
const { kind, excerpt, tool, source, at, suspect } = hit.record;
|
|
2964
|
-
return { kind, excerpt, tool, source, at, suspect };
|
|
2965
|
-
}
|
|
2966
|
-
function describeEvidence(evidence, now) {
|
|
2967
|
-
const flagged = evidence.suspect ? "Stroq flagged that content as suspicious." : "that content was not flagged, but tool output is data, not instructions.";
|
|
2968
|
-
const quoted = neutralizeControls(
|
|
2969
|
-
`"${evidence.excerpt}" appeared in the output of ${evidence.tool} (${evidence.source})`
|
|
2970
|
-
);
|
|
2971
|
-
return `${quoted} ${ageLabel(evidence.at, now)} ago; ${flagged}`;
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
3014
|
// ../core/src/secrets/extract.ts
|
|
2975
3015
|
var MIN_SECRET_LENGTH = 12;
|
|
2976
3016
|
var MAX_TEXT_CHARS = 262144;
|
|
@@ -3355,7 +3395,13 @@ var StroqEngine = class {
|
|
|
3355
3395
|
...secrets.length > 0 ? ["secret.egress"] : [],
|
|
3356
3396
|
...unscannable ? ["secret.unscannable"] : []
|
|
3357
3397
|
];
|
|
3358
|
-
const decision =
|
|
3398
|
+
const decision = withWayOut(
|
|
3399
|
+
evaluatePolicy(this.opts.policy, classes, state.taint?.level ?? null),
|
|
3400
|
+
this.opts.policy,
|
|
3401
|
+
state.taint,
|
|
3402
|
+
event.sessionId,
|
|
3403
|
+
new Date(this.now())
|
|
3404
|
+
);
|
|
3359
3405
|
const provenance = origin2.counted.map(toEvidence);
|
|
3360
3406
|
const summary = await this.safeSummary(
|
|
3361
3407
|
summarizeInput(event.toolName, event.auditInput ?? event.toolInput),
|
|
@@ -3407,7 +3453,7 @@ var StroqEngine = class {
|
|
|
3407
3453
|
);
|
|
3408
3454
|
const ruleIds = [...new Set(scan.matches.map((m) => m.ruleId))];
|
|
3409
3455
|
const source = redact(summary).slice(0, MAX_STORED_CHARS);
|
|
3410
|
-
const trusted = scan.verdict === "suspect" && this.opts.trust?.trusts(
|
|
3456
|
+
const trusted = scan.verdict === "suspect" && this.opts.trust?.trusts(event.toolResultText) === true;
|
|
3411
3457
|
await this.opts.audit.append({
|
|
3412
3458
|
sessionId: event.sessionId,
|
|
3413
3459
|
phase: "post",
|
|
@@ -6942,9 +6988,9 @@ var openclawInstallArgv = (dir) => [
|
|
|
6942
6988
|
["plugins", "install", "--link", dir],
|
|
6943
6989
|
["plugins", "enable", OPENCLAW_PLUGIN_ID]
|
|
6944
6990
|
];
|
|
6945
|
-
var
|
|
6946
|
-
var
|
|
6947
|
-
var openclawInstallCommands = (dir) => openclawInstallArgv(dir).map((argv) => [OPENCLAW_BIN, ...argv].map(
|
|
6991
|
+
var SAFE_WORD2 = /^[\w@%+=:,./-]+$/;
|
|
6992
|
+
var shellQuote2 = (arg) => SAFE_WORD2.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
6993
|
+
var openclawInstallCommands = (dir) => openclawInstallArgv(dir).map((argv) => [OPENCLAW_BIN, ...argv].map(shellQuote2).join(" "));
|
|
6948
6994
|
function openclawOnPath(env = process.env, plat = process.platform) {
|
|
6949
6995
|
const names = plat === "win32" ? (env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((ext) => ext !== "").map((ext) => `${OPENCLAW_BIN}${ext.toLowerCase()}`) : [OPENCLAW_BIN];
|
|
6950
6996
|
for (const entry2 of (env["PATH"] ?? "").split(delimiter)) {
|
|
@@ -7016,12 +7062,12 @@ function mcpConfigPath(client, scope, cwd = process.cwd()) {
|
|
|
7016
7062
|
}
|
|
7017
7063
|
var readMcpConfig = (file) => readJsonObject(file);
|
|
7018
7064
|
var hasValidMcpServers = (config) => config.mcpServers === void 0 || isPlainObject(config.mcpServers);
|
|
7019
|
-
var
|
|
7065
|
+
var STROQ_ENTRY2 = /(^|[\\/])(index\.(?:js|ts|mjs|cjs)|stroq(?:\.js|\.cmd)?)$/;
|
|
7020
7066
|
function wrapperIndex(args) {
|
|
7021
7067
|
for (let i = 1; i < args.length; i += 1) {
|
|
7022
7068
|
if (args[i] !== "mcp" || args[i + 1] !== "--server" || args[i + 3] !== "--client") continue;
|
|
7023
7069
|
const entry2 = args[i - 1];
|
|
7024
|
-
if (typeof entry2 !== "string" || !
|
|
7070
|
+
if (typeof entry2 !== "string" || !STROQ_ENTRY2.test(entry2)) continue;
|
|
7025
7071
|
if (args.indexOf("--", i) === -1) continue;
|
|
7026
7072
|
return i;
|
|
7027
7073
|
}
|
|
@@ -7511,6 +7557,9 @@ async function runInit(args) {
|
|
|
7511
7557
|
const code = install[agent](scope, command, dryRun);
|
|
7512
7558
|
if (code === 0 && !dryRun) {
|
|
7513
7559
|
recordInstall(agent, scope, agent === "openclaw" ? hookArgv(node, entry2).join(" ") : command);
|
|
7560
|
+
const flags = `${agent === "claude-code" ? "" : ` --agent ${agent}`}${scope === "user" ? " --user" : ""}`;
|
|
7561
|
+
process.stdout.write(`To remove them: stroq uninstall${flags}
|
|
7562
|
+
`);
|
|
7514
7563
|
}
|
|
7515
7564
|
return code;
|
|
7516
7565
|
}
|
|
@@ -8241,7 +8290,7 @@ function flaggedTools(tools) {
|
|
|
8241
8290
|
).map((t) => typeof t.name === "string" ? t.name : "(unnamed)");
|
|
8242
8291
|
}
|
|
8243
8292
|
async function probeOne(spec, timeoutMs) {
|
|
8244
|
-
return new Promise((
|
|
8293
|
+
return new Promise((resolve15) => {
|
|
8245
8294
|
const empty = { server: spec.name, tools: 0, flagged: [] };
|
|
8246
8295
|
let child;
|
|
8247
8296
|
try {
|
|
@@ -8251,7 +8300,7 @@ async function probeOne(spec, timeoutMs) {
|
|
|
8251
8300
|
stdio: ["pipe", "pipe", "ignore"]
|
|
8252
8301
|
});
|
|
8253
8302
|
} catch (err) {
|
|
8254
|
-
|
|
8303
|
+
resolve15({ ...empty, error: err.message });
|
|
8255
8304
|
return;
|
|
8256
8305
|
}
|
|
8257
8306
|
let buf = "";
|
|
@@ -8261,7 +8310,7 @@ async function probeOne(spec, timeoutMs) {
|
|
|
8261
8310
|
done = true;
|
|
8262
8311
|
clearTimeout(timer);
|
|
8263
8312
|
killChildTree(child, "SIGTERM");
|
|
8264
|
-
|
|
8313
|
+
resolve15(result);
|
|
8265
8314
|
};
|
|
8266
8315
|
const timer = setTimeout(
|
|
8267
8316
|
() => finish({ ...empty, error: `no tools/list response within ${timeoutMs} ms` }),
|
|
@@ -9709,8 +9758,8 @@ async function withDeadline(work, ms, onTimeout) {
|
|
|
9709
9758
|
try {
|
|
9710
9759
|
return await Promise.race([
|
|
9711
9760
|
work,
|
|
9712
|
-
new Promise((
|
|
9713
|
-
timer = setTimeout(() =>
|
|
9761
|
+
new Promise((resolve15) => {
|
|
9762
|
+
timer = setTimeout(() => resolve15(onTimeout()), ms);
|
|
9714
9763
|
})
|
|
9715
9764
|
]);
|
|
9716
9765
|
} finally {
|
|
@@ -10307,12 +10356,12 @@ async function writeBackpressured(sink, text, source) {
|
|
|
10307
10356
|
return;
|
|
10308
10357
|
}
|
|
10309
10358
|
source.pause();
|
|
10310
|
-
await new Promise((
|
|
10359
|
+
await new Promise((resolve15) => {
|
|
10311
10360
|
const done = () => {
|
|
10312
10361
|
sink.off("drain", done);
|
|
10313
10362
|
sink.off("error", done);
|
|
10314
10363
|
sink.off("close", done);
|
|
10315
|
-
|
|
10364
|
+
resolve15();
|
|
10316
10365
|
};
|
|
10317
10366
|
sink.once("drain", done);
|
|
10318
10367
|
sink.once("error", done);
|
|
@@ -10877,14 +10926,14 @@ async function runMcpProxy(options) {
|
|
|
10877
10926
|
const onSigterm = relay("SIGTERM");
|
|
10878
10927
|
process.on("SIGINT", onSigint);
|
|
10879
10928
|
process.on("SIGTERM", onSigterm);
|
|
10880
|
-
const code = await new Promise((
|
|
10929
|
+
const code = await new Promise((resolve15) => {
|
|
10881
10930
|
child.on("error", (err) => {
|
|
10882
10931
|
options.stderr.write(`stroq mcp: cannot start the MCP server: ${err.message}
|
|
10883
10932
|
`);
|
|
10884
|
-
|
|
10933
|
+
resolve15(1);
|
|
10885
10934
|
});
|
|
10886
10935
|
child.on("close", (exitCode, signal) => {
|
|
10887
|
-
|
|
10936
|
+
resolve15(signal === null ? exitCode ?? 1 : 1);
|
|
10888
10937
|
});
|
|
10889
10938
|
});
|
|
10890
10939
|
shuttingDown = true;
|
|
@@ -11088,7 +11137,7 @@ async function feedLines(path, parser) {
|
|
|
11088
11137
|
return parser.finish();
|
|
11089
11138
|
}
|
|
11090
11139
|
function projectSlug(cwd) {
|
|
11091
|
-
return cwd.replace(/[
|
|
11140
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
11092
11141
|
}
|
|
11093
11142
|
var transcriptRoot = () => join22(homedir20(), ".claude", "projects");
|
|
11094
11143
|
async function findTranscripts(cwd) {
|
|
@@ -11217,12 +11266,18 @@ function buildReplay(allEntries, sessionId) {
|
|
|
11217
11266
|
};
|
|
11218
11267
|
}
|
|
11219
11268
|
var RULE_PREVIEW = 3;
|
|
11220
|
-
function verdictTag(entry2) {
|
|
11269
|
+
function verdictTag(entry2, voice) {
|
|
11221
11270
|
const d = entry2.decision;
|
|
11222
11271
|
if (!d) return "";
|
|
11223
|
-
|
|
11224
|
-
if (
|
|
11225
|
-
|
|
11272
|
+
const rule = d.ruleId ?? "default";
|
|
11273
|
+
if (voice === "replayed") {
|
|
11274
|
+
if (d.effect === "deny") return `WOULD DENY ${rule}`;
|
|
11275
|
+
if (d.effect === "ask") return `WOULD ASK ${rule}`;
|
|
11276
|
+
return `would allow ${rule}`;
|
|
11277
|
+
}
|
|
11278
|
+
if (d.effect === "deny") return `DENIED ${rule}`;
|
|
11279
|
+
if (d.effect === "ask") return `ASKED ${rule}`;
|
|
11280
|
+
return `allowed ${rule}`;
|
|
11226
11281
|
}
|
|
11227
11282
|
function readLine(src) {
|
|
11228
11283
|
const scan = src.read?.scan;
|
|
@@ -11238,24 +11293,25 @@ function readLine(src) {
|
|
|
11238
11293
|
return `SUSPECT ${scan.score.toFixed(2)} \u2014 ${scan.ruleIds.length} rules: ${ids}${more}${waived}`;
|
|
11239
11294
|
}
|
|
11240
11295
|
var secretLine = (s) => `${s.name} from ${s.source}${s.canary ? " (canary)" : ""}`;
|
|
11241
|
-
function consequenceBlock(c, last) {
|
|
11296
|
+
function consequenceBlock(c, last, voice) {
|
|
11242
11297
|
const elbow = last ? " \u2514\u2500\u25BA" : " \u251C\u2500\u25BA";
|
|
11243
11298
|
const rail = last ? " " : " \u2502 ";
|
|
11244
11299
|
const gap = ageLabel(c.evidence.at, new Date(c.action.ts));
|
|
11300
|
+
const tag = verdictTag(c.action, voice);
|
|
11245
11301
|
return [
|
|
11246
11302
|
`${elbow} #${c.action.seq} ${c.action.tool} ${c.action.summary}`,
|
|
11247
|
-
`${rail} ${
|
|
11303
|
+
`${rail} ${tag}${" ".repeat(Math.max(1, 34 - tag.length))}${gap} later`,
|
|
11248
11304
|
`${rail} carried over: "${c.evidence.excerpt}" (${c.evidence.kind})` + (c.alsoCarried > 0 ? ` and ${c.alsoCarried} more` : "")
|
|
11249
11305
|
];
|
|
11250
11306
|
}
|
|
11251
|
-
function sourceBlock(src) {
|
|
11307
|
+
function sourceBlock(src, voice) {
|
|
11252
11308
|
const seq = src.read ? `#${src.read.seq} ` : "";
|
|
11253
11309
|
const head = [` \u25A0 ${seq}${src.tool} ${src.source}`, ` ${readLine(src)}`];
|
|
11254
11310
|
if (src.consequences.length === 0) {
|
|
11255
11311
|
return [...head, " nothing traced back to it", ""];
|
|
11256
11312
|
}
|
|
11257
11313
|
const body = src.consequences.flatMap(
|
|
11258
|
-
(c, i) => consequenceBlock(c, i === src.consequences.length - 1)
|
|
11314
|
+
(c, i) => consequenceBlock(c, i === src.consequences.length - 1, voice)
|
|
11259
11315
|
);
|
|
11260
11316
|
return [...head, " \u2502", ...body, ""];
|
|
11261
11317
|
}
|
|
@@ -11263,7 +11319,7 @@ function duration(model) {
|
|
|
11263
11319
|
if (!model.first || !model.last) return "";
|
|
11264
11320
|
return ` \xB7 ${ageLabel(model.first, new Date(model.last))} long`;
|
|
11265
11321
|
}
|
|
11266
|
-
function formatReplay(model) {
|
|
11322
|
+
function formatReplay(model, voice = "recorded") {
|
|
11267
11323
|
if (model.total === 0) {
|
|
11268
11324
|
return `no audit entries for session ${model.sessionId}
|
|
11269
11325
|
`;
|
|
@@ -11272,18 +11328,18 @@ function formatReplay(model) {
|
|
|
11272
11328
|
const lines = [
|
|
11273
11329
|
"stroq replay \u2014 what the agent did, and what told it to do it",
|
|
11274
11330
|
"",
|
|
11275
|
-
` session ${model.sessionId} \xB7 ${model.total} events${duration(model)} \xB7 ${model.denied} denied \xB7 ${model.asked} asked
|
|
11331
|
+
` session ${model.sessionId} \xB7 ${model.total} events${duration(model)} \xB7 ` + (voice === "replayed" ? `${model.denied} would be denied \xB7 ${model.asked} would ask (today's policy)` : `${model.denied} denied \xB7 ${model.asked} asked`),
|
|
11276
11332
|
""
|
|
11277
11333
|
];
|
|
11278
11334
|
if (model.sources.length > 0) {
|
|
11279
11335
|
lines.push("CONTENT THE AGENT READ, AND WHAT CAME OUT OF IT", "");
|
|
11280
|
-
for (const src of model.sources) lines.push(...sourceBlock(src));
|
|
11336
|
+
for (const src of model.sources) lines.push(...sourceBlock(src, voice));
|
|
11281
11337
|
}
|
|
11282
11338
|
if (model.secretActions.length > 0) {
|
|
11283
11339
|
lines.push("ACTIONS CARRYING A KNOWN SECRET VALUE", "");
|
|
11284
11340
|
for (const a of model.secretActions) {
|
|
11285
11341
|
lines.push(` \u25CF #${a.seq} ${a.tool} ${a.summary}`);
|
|
11286
|
-
lines.push(` ${verdictTag(a)}`);
|
|
11342
|
+
lines.push(` ${verdictTag(a, voice)}`);
|
|
11287
11343
|
for (const s of a.secrets ?? []) lines.push(` ${secretLine(s)}`);
|
|
11288
11344
|
lines.push("");
|
|
11289
11345
|
}
|
|
@@ -11291,7 +11347,7 @@ function formatReplay(model) {
|
|
|
11291
11347
|
if (model.unlinked.length > 0) {
|
|
11292
11348
|
lines.push(`ACTIONS WITH NO UNTRUSTED ORIGIN (${model.unlinked.length})`, "");
|
|
11293
11349
|
for (const a of model.unlinked) {
|
|
11294
|
-
const tag = a.decision ? a
|
|
11350
|
+
const tag = a.decision ? verdictTag(a, voice) : "-";
|
|
11295
11351
|
lines.push(` \u25CB #${a.seq} ${a.tool} ${a.summary}`);
|
|
11296
11352
|
lines.push(` ${tag}`);
|
|
11297
11353
|
}
|
|
@@ -11301,6 +11357,12 @@ function formatReplay(model) {
|
|
|
11301
11357
|
lines.push(
|
|
11302
11358
|
traced === 0 ? "No action in this session traced back to content the agent read." : `${traced} of ${judged} judged actions traced back to content the agent read.`
|
|
11303
11359
|
);
|
|
11360
|
+
if (voice === "replayed")
|
|
11361
|
+
lines.push(
|
|
11362
|
+
"",
|
|
11363
|
+
"These are the verdicts today's policy gives the recording \u2014",
|
|
11364
|
+
"not a record of what was blocked at the time. Guard the next session: stroq init"
|
|
11365
|
+
);
|
|
11304
11366
|
return `${lines.join("\n")}
|
|
11305
11367
|
`;
|
|
11306
11368
|
}
|
|
@@ -11371,7 +11433,7 @@ async function runReplay(args) {
|
|
|
11371
11433
|
`);
|
|
11372
11434
|
return 0;
|
|
11373
11435
|
}
|
|
11374
|
-
process.stdout.write(`${formatReplay(model2)}
|
|
11436
|
+
process.stdout.write(`${formatReplay(model2, "replayed")}
|
|
11375
11437
|
replayed from ${path}
|
|
11376
11438
|
`);
|
|
11377
11439
|
return 0;
|
|
@@ -11466,7 +11528,7 @@ async function launch(options) {
|
|
|
11466
11528
|
const relays = RELAYED.map((signal) => ({ signal, listener: () => kill(child, signal) }));
|
|
11467
11529
|
for (const { signal, listener } of relays) signals.on(signal, listener);
|
|
11468
11530
|
try {
|
|
11469
|
-
return await new Promise((
|
|
11531
|
+
return await new Promise((resolve15) => {
|
|
11470
11532
|
child.on("error", (err) => {
|
|
11471
11533
|
const code = err.code;
|
|
11472
11534
|
stderr.write(
|
|
@@ -11474,9 +11536,9 @@ async function launch(options) {
|
|
|
11474
11536
|
` : `stroq run: cannot start "${options.command}": ${err.message}
|
|
11475
11537
|
`
|
|
11476
11538
|
);
|
|
11477
|
-
|
|
11539
|
+
resolve15(code === "ENOENT" ? NOT_FOUND : 1);
|
|
11478
11540
|
});
|
|
11479
|
-
child.on("close", (code, signal) =>
|
|
11541
|
+
child.on("close", (code, signal) => resolve15(exitCodeFor(code, signal, plat)));
|
|
11480
11542
|
});
|
|
11481
11543
|
} finally {
|
|
11482
11544
|
for (const { signal, listener } of relays) signals.off(signal, listener);
|
|
@@ -11793,6 +11855,7 @@ stroq run \u2014 ${invocation.command}${agent === null ? "" : ` (${agent})`}`);
|
|
|
11793
11855
|
|
|
11794
11856
|
// src/commands/sent.ts
|
|
11795
11857
|
import { homedir as homedir25 } from "node:os";
|
|
11858
|
+
import { isAbsolute as isAbsolute6, relative as relative6, resolve as resolve13 } from "node:path";
|
|
11796
11859
|
import { parseArgs as parseArgs9 } from "node:util";
|
|
11797
11860
|
|
|
11798
11861
|
// src/sent/format.ts
|
|
@@ -11908,10 +11971,18 @@ function formatSent(report2) {
|
|
|
11908
11971
|
} else {
|
|
11909
11972
|
lines.push(...MEANING, "");
|
|
11910
11973
|
}
|
|
11911
|
-
lines.push(...coverageLines(report2));
|
|
11974
|
+
lines.push(...coverageLines(report2), "", ...nextLines(report2.credentials.length > 0));
|
|
11912
11975
|
return `${lines.join("\n")}
|
|
11913
11976
|
`;
|
|
11914
11977
|
}
|
|
11978
|
+
function nextLines(found) {
|
|
11979
|
+
return [
|
|
11980
|
+
"NEXT",
|
|
11981
|
+
...found ? [" Rotate each credential named above that is still live."] : [],
|
|
11982
|
+
" stroq replay --last what the session read, and which later actions matched it",
|
|
11983
|
+
" stroq init guard the next session (--agent <name> for another agent)"
|
|
11984
|
+
];
|
|
11985
|
+
}
|
|
11915
11986
|
|
|
11916
11987
|
// src/sent/readers.ts
|
|
11917
11988
|
import { open as open3 } from "node:fs/promises";
|
|
@@ -12865,6 +12936,10 @@ know what to look for, and prints names and sources only, never a value. A
|
|
|
12865
12936
|
finding exits 0 by default: a session that already happened cannot be
|
|
12866
12937
|
changed by today's commit. A transcript match does not confirm provider delivery.
|
|
12867
12938
|
`;
|
|
12939
|
+
function isWithin(dir, root) {
|
|
12940
|
+
const rel = relative6(resolve13(root), resolve13(dir));
|
|
12941
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
|
|
12942
|
+
}
|
|
12868
12943
|
async function runSent(args) {
|
|
12869
12944
|
let parsed;
|
|
12870
12945
|
try {
|
|
@@ -12921,6 +12996,15 @@ ${USAGE}`);
|
|
|
12921
12996
|
return 1;
|
|
12922
12997
|
}
|
|
12923
12998
|
const transcript = await found.reader.read(found.path);
|
|
12999
|
+
if (values.transcript === void 0 && transcript.cwd !== null && !isWithin(cwd, transcript.cwd)) {
|
|
13000
|
+
process.stdout.write(
|
|
13001
|
+
`no agent session recorded in ${cwd}.
|
|
13002
|
+
The newest one on this machine ran in ${transcript.cwd}: run \`stroq sent --last\` there,
|
|
13003
|
+
or name a session with --transcript <path>.
|
|
13004
|
+
`
|
|
13005
|
+
);
|
|
13006
|
+
return 1;
|
|
13007
|
+
}
|
|
12924
13008
|
if (transcript.events.length === 0) {
|
|
12925
13009
|
process.stdout.write(`no tool calls recorded in ${found.path}
|
|
12926
13010
|
`);
|
|
@@ -12947,7 +13031,7 @@ ${USAGE}`);
|
|
|
12947
13031
|
|
|
12948
13032
|
// src/commands/trust.ts
|
|
12949
13033
|
import { chmodSync as chmodSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
|
|
12950
|
-
import { dirname as dirname11, resolve as
|
|
13034
|
+
import { dirname as dirname11, resolve as resolve14 } from "node:path";
|
|
12951
13035
|
import { parseArgs as parseArgs10 } from "node:util";
|
|
12952
13036
|
function read2(file) {
|
|
12953
13037
|
try {
|
|
@@ -13010,7 +13094,7 @@ function runTrust(argv, now = () => /* @__PURE__ */ new Date()) {
|
|
|
13010
13094
|
});
|
|
13011
13095
|
const file = trustFile();
|
|
13012
13096
|
if (values.remove !== void 0) {
|
|
13013
|
-
const path =
|
|
13097
|
+
const path = resolve14(values.remove);
|
|
13014
13098
|
const current = read2(file);
|
|
13015
13099
|
const entries = current.entries.filter((e) => e.source !== path);
|
|
13016
13100
|
if (entries.length === current.entries.length) {
|
|
@@ -13031,24 +13115,154 @@ function runTrust(argv, now = () => /* @__PURE__ */ new Date()) {
|
|
|
13031
13115
|
);
|
|
13032
13116
|
return 0;
|
|
13033
13117
|
}
|
|
13034
|
-
const { code, message } = add(file,
|
|
13118
|
+
const { code, message } = add(file, resolve14(positionals[0]), now);
|
|
13035
13119
|
process.stdout.write(message);
|
|
13036
13120
|
return code;
|
|
13037
13121
|
}
|
|
13038
13122
|
|
|
13123
|
+
// src/commands/uninstall.ts
|
|
13124
|
+
import { existsSync as existsSync15, rmSync as rmSync3 } from "node:fs";
|
|
13125
|
+
import { parseArgs as parseArgs11 } from "node:util";
|
|
13126
|
+
var ownedBy = (agent) => (handler2) => isPlainObject(handler2) && typeof handler2["command"] === "string" && new RegExp(` hook ${agent}(?: \\S+)?$`).test(handler2["command"]);
|
|
13127
|
+
function stripEvent(entries, ours) {
|
|
13128
|
+
return entries.flatMap((entry2) => {
|
|
13129
|
+
if (ours(entry2)) return [];
|
|
13130
|
+
if (!isPlainObject(entry2) || !Array.isArray(entry2["hooks"])) return [entry2];
|
|
13131
|
+
const hooks = entry2["hooks"];
|
|
13132
|
+
const kept = hooks.filter((handler2) => !ours(handler2));
|
|
13133
|
+
if (kept.length === hooks.length) return [entry2];
|
|
13134
|
+
return kept.length > 0 ? [{ ...entry2, hooks: kept }] : [];
|
|
13135
|
+
});
|
|
13136
|
+
}
|
|
13137
|
+
function stripEvents(events, ours) {
|
|
13138
|
+
const out = {};
|
|
13139
|
+
for (const [event, value] of Object.entries(events)) {
|
|
13140
|
+
if (!Array.isArray(value)) {
|
|
13141
|
+
out[event] = value;
|
|
13142
|
+
continue;
|
|
13143
|
+
}
|
|
13144
|
+
const kept = stripEvent(value, ours);
|
|
13145
|
+
if (kept.length > 0 || value.length === 0) out[event] = kept;
|
|
13146
|
+
}
|
|
13147
|
+
return out;
|
|
13148
|
+
}
|
|
13149
|
+
function stripHooksKey(settings, ours) {
|
|
13150
|
+
const { hooks, ...rest } = settings;
|
|
13151
|
+
if (!isPlainObject(hooks)) return settings;
|
|
13152
|
+
const kept = stripEvents(hooks, ours);
|
|
13153
|
+
return Object.keys(kept).length > 0 ? { ...rest, hooks: kept } : rest;
|
|
13154
|
+
}
|
|
13155
|
+
var JSON_TARGETS = {
|
|
13156
|
+
"claude-code": {
|
|
13157
|
+
path: settingsPath,
|
|
13158
|
+
strip: (s) => stripHooksKey(s, ownedBy("claude-code"))
|
|
13159
|
+
},
|
|
13160
|
+
cursor: { path: cursorHooksPath, strip: (s) => stripHooksKey(s, ownedBy("cursor")) },
|
|
13161
|
+
windsurf: { path: windsurfHooksPath, strip: (s) => stripHooksKey(s, ownedBy("windsurf")) },
|
|
13162
|
+
// Codex also reads events kept at the file's root, so both levels are stripped.
|
|
13163
|
+
codex: {
|
|
13164
|
+
path: codexHooksPath,
|
|
13165
|
+
strip: (s) => stripEvents(stripHooksKey(s, ownedBy("codex")), ownedBy("codex"))
|
|
13166
|
+
},
|
|
13167
|
+
antigravity: {
|
|
13168
|
+
path: antigravityHooksPath,
|
|
13169
|
+
strip: ({ [ANTIGRAVITY_HOOK_NAME]: _ours, ...rest }) => rest
|
|
13170
|
+
}
|
|
13171
|
+
};
|
|
13172
|
+
var nothing = (file) => ({
|
|
13173
|
+
removed: false,
|
|
13174
|
+
message: `No Stroq hooks in ${file}; nothing to remove.
|
|
13175
|
+
`
|
|
13176
|
+
});
|
|
13177
|
+
function uninstallJson(target, file, dryRun) {
|
|
13178
|
+
if (!existsSync15(file)) return nothing(file);
|
|
13179
|
+
const before = readJsonObject(file);
|
|
13180
|
+
const after = target.strip(before);
|
|
13181
|
+
if (JSON.stringify(after) === JSON.stringify(before)) return nothing(file);
|
|
13182
|
+
if (dryRun) return { removed: true, message: `${JSON.stringify(after, null, 2)}
|
|
13183
|
+
` };
|
|
13184
|
+
writeJsonObject(file, after);
|
|
13185
|
+
return {
|
|
13186
|
+
removed: true,
|
|
13187
|
+
message: `Removed Stroq's hooks from ${file}. Anything else in it is unchanged.
|
|
13188
|
+
Run "stroq doctor" to check.
|
|
13189
|
+
`
|
|
13190
|
+
};
|
|
13191
|
+
}
|
|
13192
|
+
function uninstallCopilot(file, dryRun) {
|
|
13193
|
+
if (!existsSync15(file)) return nothing(file);
|
|
13194
|
+
const text = JSON.stringify(readJsonObject(file));
|
|
13195
|
+
if (!/ hook copilot (?:pre|post)\b/.test(text)) return nothing(file);
|
|
13196
|
+
if (dryRun) return { removed: true, message: `would delete ${file}
|
|
13197
|
+
` };
|
|
13198
|
+
rmSync3(file);
|
|
13199
|
+
return {
|
|
13200
|
+
removed: true,
|
|
13201
|
+
message: `Deleted ${file}, which held only Stroq's hooks.
|
|
13202
|
+
Run "stroq doctor" to check.
|
|
13203
|
+
`
|
|
13204
|
+
};
|
|
13205
|
+
}
|
|
13206
|
+
function uninstallAgent(agent, scope, cwd, dryRun) {
|
|
13207
|
+
if (agent === "openclaw")
|
|
13208
|
+
return {
|
|
13209
|
+
removed: false,
|
|
13210
|
+
message: "The OpenClaw plugin is registered with your Gateway. Switch it off with:\n openclaw plugins disable stroq\n"
|
|
13211
|
+
};
|
|
13212
|
+
if (agent === "copilot") return uninstallCopilot(copilotHooksPath(scope, cwd), dryRun);
|
|
13213
|
+
const target = JSON_TARGETS[agent];
|
|
13214
|
+
return uninstallJson(target, target.path(scope, cwd), dryRun);
|
|
13215
|
+
}
|
|
13216
|
+
async function runUninstall(args) {
|
|
13217
|
+
const { values } = parseArgs11({
|
|
13218
|
+
args: [...args],
|
|
13219
|
+
options: {
|
|
13220
|
+
agent: { type: "string" },
|
|
13221
|
+
user: { type: "boolean" },
|
|
13222
|
+
"dry-run": { type: "boolean" },
|
|
13223
|
+
client: { type: "string" },
|
|
13224
|
+
config: { type: "string" }
|
|
13225
|
+
}
|
|
13226
|
+
});
|
|
13227
|
+
const agent = values.agent ?? "claude-code";
|
|
13228
|
+
if (!isInitAgent(agent)) {
|
|
13229
|
+
process.stderr.write(`stroq uninstall: unknown agent "${agent}"
|
|
13230
|
+
`);
|
|
13231
|
+
return 2;
|
|
13232
|
+
}
|
|
13233
|
+
if (agent === "mcp")
|
|
13234
|
+
return runInit([
|
|
13235
|
+
"--agent",
|
|
13236
|
+
"mcp",
|
|
13237
|
+
"--unwrap",
|
|
13238
|
+
...values.user === true ? ["--user"] : [],
|
|
13239
|
+
...values["dry-run"] === true ? ["--dry-run"] : [],
|
|
13240
|
+
...values.client === void 0 ? [] : ["--client", values.client],
|
|
13241
|
+
...values.config === void 0 ? [] : ["--config", values.config]
|
|
13242
|
+
]);
|
|
13243
|
+
const result = uninstallAgent(
|
|
13244
|
+
agent,
|
|
13245
|
+
values.user === true ? "user" : "project",
|
|
13246
|
+
process.cwd(),
|
|
13247
|
+
values["dry-run"] === true
|
|
13248
|
+
);
|
|
13249
|
+
process.stdout.write(result.message);
|
|
13250
|
+
return 0;
|
|
13251
|
+
}
|
|
13252
|
+
|
|
13039
13253
|
// src/commands/untaint.ts
|
|
13040
|
-
import { existsSync as
|
|
13254
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6 } from "node:fs";
|
|
13041
13255
|
import { rm as rm6 } from "node:fs/promises";
|
|
13042
13256
|
import { join as join28 } from "node:path";
|
|
13043
|
-
import { parseArgs as
|
|
13257
|
+
import { parseArgs as parseArgs12 } from "node:util";
|
|
13044
13258
|
var USAGE2 = "usage: stroq untaint --session <id> | --all\n";
|
|
13045
13259
|
async function clearAll() {
|
|
13046
13260
|
const dir = sessionsDir();
|
|
13047
|
-
if (!
|
|
13261
|
+
if (!existsSync16(dir)) return;
|
|
13048
13262
|
await Promise.all(readdirSync6(dir).map((f) => rm6(join28(dir, f), { force: true })));
|
|
13049
13263
|
}
|
|
13050
13264
|
async function runUntaint(args) {
|
|
13051
|
-
const { values } =
|
|
13265
|
+
const { values } = parseArgs12({
|
|
13052
13266
|
args: [...args],
|
|
13053
13267
|
options: {
|
|
13054
13268
|
session: { type: "string" },
|
|
@@ -13085,7 +13299,7 @@ async function runVerify() {
|
|
|
13085
13299
|
}
|
|
13086
13300
|
|
|
13087
13301
|
// src/commands/why.ts
|
|
13088
|
-
import { parseArgs as
|
|
13302
|
+
import { parseArgs as parseArgs13 } from "node:util";
|
|
13089
13303
|
function verdictLine(entry2) {
|
|
13090
13304
|
if (entry2.decision) {
|
|
13091
13305
|
return `${entry2.decision.effect} by ${entry2.decision.ruleId ?? "default"}: ${entry2.decision.reason}`;
|
|
@@ -13118,7 +13332,7 @@ function formatWhy(entry2, state, now) {
|
|
|
13118
13332
|
`;
|
|
13119
13333
|
}
|
|
13120
13334
|
async function runWhy(args) {
|
|
13121
|
-
const { values } =
|
|
13335
|
+
const { values } = parseArgs13({ args: [...args], options: { seq: { type: "string" } } });
|
|
13122
13336
|
const entries = await new AuditLog(auditFile()).readAll();
|
|
13123
13337
|
const seq = values.seq === void 0 ? null : Number.parseInt(values.seq, 10);
|
|
13124
13338
|
const target = seq === null ? [...entries].reverse().find((e) => e.decision !== void 0 && e.decision.effect !== "allow") : entries.find((e) => e.seq === seq);
|
|
@@ -13134,76 +13348,369 @@ async function runWhy(args) {
|
|
|
13134
13348
|
return 0;
|
|
13135
13349
|
}
|
|
13136
13350
|
|
|
13137
|
-
// src/
|
|
13138
|
-
var
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
|
|
13146
|
-
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
13155
|
-
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13351
|
+
// src/help.ts
|
|
13352
|
+
var COMMANDS = [
|
|
13353
|
+
{
|
|
13354
|
+
name: "init",
|
|
13355
|
+
synopsis: "init [--agent <name>] [--user] [--dry-run]",
|
|
13356
|
+
about: [
|
|
13357
|
+
"install hooks for an agent, in this project by default,",
|
|
13358
|
+
"or wrap an MCP client's stdio servers in Stroq's proxy (--agent mcp)"
|
|
13359
|
+
],
|
|
13360
|
+
flags: [
|
|
13361
|
+
[
|
|
13362
|
+
"--agent <name>",
|
|
13363
|
+
"claude-code (default), cursor, codex, copilot, openclaw, windsurf, antigravity or mcp"
|
|
13364
|
+
],
|
|
13365
|
+
["--user", "install into the user's config instead of this project's"],
|
|
13366
|
+
["--dry-run", "print the change and write nothing"],
|
|
13367
|
+
["--client <name>", "with --agent mcp: claude-desktop, windsurf, cursor or claude-code"],
|
|
13368
|
+
["--config <path>", 'with --agent mcp: any file with an "mcpServers" object'],
|
|
13369
|
+
["--unwrap", "with --agent mcp: put every wrapped server back the way it was"],
|
|
13370
|
+
[
|
|
13371
|
+
"--cloak",
|
|
13372
|
+
"with --agent mcp: replace detected values in results before the model reads them"
|
|
13373
|
+
]
|
|
13374
|
+
]
|
|
13375
|
+
},
|
|
13376
|
+
{
|
|
13377
|
+
name: "uninstall",
|
|
13378
|
+
synopsis: "uninstall [--agent <name>] [--user] [--dry-run]",
|
|
13379
|
+
about: [
|
|
13380
|
+
"take Stroq's hooks out of an agent's config, leaving everything else in it as it",
|
|
13381
|
+
"was; with --agent mcp, put every wrapped MCP server back"
|
|
13382
|
+
],
|
|
13383
|
+
flags: [
|
|
13384
|
+
["--agent <name>", "the agent to remove Stroq from, as for init (default claude-code)"],
|
|
13385
|
+
["--user", "remove from the user's config instead of this project's"],
|
|
13386
|
+
["--dry-run", "print the result and change nothing"],
|
|
13387
|
+
["--client <name>", "with --agent mcp: the client whose servers to unwrap"],
|
|
13388
|
+
["--config <path>", "with --agent mcp: the config file to unwrap"]
|
|
13389
|
+
]
|
|
13390
|
+
},
|
|
13391
|
+
{
|
|
13392
|
+
name: "hook",
|
|
13393
|
+
synopsis: "hook <agent> [<phase>]",
|
|
13394
|
+
about: [
|
|
13395
|
+
"the entrypoint an agent runs on every tool call: reads the event JSON on stdin and",
|
|
13396
|
+
"prints a decision. Agents: claude-code, cursor, codex, windsurf, copilot <pre|post>,",
|
|
13397
|
+
"openclaw <pre|post>, antigravity <pre|post|preinvocation>. `stroq init` writes it"
|
|
13398
|
+
],
|
|
13399
|
+
flags: []
|
|
13400
|
+
},
|
|
13401
|
+
{
|
|
13402
|
+
name: "run",
|
|
13403
|
+
synopsis: "run [--sandbox] [--agent <id>] -- <agent> \u2026",
|
|
13404
|
+
about: [
|
|
13405
|
+
"start an agent already confined: sets the git settings that stop a repository",
|
|
13406
|
+
'running a command during the startup "git status", refuses to launch into a',
|
|
13407
|
+
"repository that runs something before you could approve it, and checks that",
|
|
13408
|
+
"Stroq's hooks are installed for that agent"
|
|
13409
|
+
],
|
|
13410
|
+
flags: [
|
|
13411
|
+
["--agent <id>", "the agent whose hooks to check, when the command does not say"],
|
|
13412
|
+
[
|
|
13413
|
+
"--sandbox",
|
|
13414
|
+
"also wrap the launch in Anthropic's srt, with this machine's credential files unreadable"
|
|
13415
|
+
],
|
|
13416
|
+
["--allow-domain <host>", "with --sandbox: a host the agent may reach; repeat for more"],
|
|
13417
|
+
["--no-inspect", "skip reading what the repository runs"],
|
|
13418
|
+
["--force", "launch even after printing a refusal"],
|
|
13419
|
+
["--dry-run", "print what would run and start nothing"]
|
|
13420
|
+
],
|
|
13421
|
+
passThrough: true
|
|
13422
|
+
},
|
|
13423
|
+
{
|
|
13424
|
+
name: "mcp",
|
|
13425
|
+
synopsis: "mcp --server <name> [--cloak] -- <command> \u2026",
|
|
13426
|
+
about: [
|
|
13427
|
+
"the stdio MCP proxy `init --agent mcp` writes into a client config: judges every",
|
|
13428
|
+
"tools/call against your policy and scans every result on its way back"
|
|
13429
|
+
],
|
|
13430
|
+
flags: [
|
|
13431
|
+
["--server <name>", "the server name from the client config"],
|
|
13432
|
+
["--client <name>", "the client, for the session id and the audit"],
|
|
13433
|
+
["--cwd <dir>", "the project directory decisions are made for"],
|
|
13434
|
+
["--session <id>", "the session id; default mcp:<client>"],
|
|
13435
|
+
["--pass-env <names>", "comma-separated variables the server is started with"],
|
|
13436
|
+
["--cloak", "replace detected values in results before the model reads them"]
|
|
13437
|
+
],
|
|
13438
|
+
passThrough: true
|
|
13439
|
+
},
|
|
13440
|
+
{
|
|
13441
|
+
name: "doctor",
|
|
13442
|
+
synopsis: "doctor [--all]",
|
|
13443
|
+
about: ["check the installation: Node, rules, the hooks of every agent, a self-test"],
|
|
13444
|
+
flags: [["--all", "list every agent and scope, installed or not"]]
|
|
13445
|
+
},
|
|
13446
|
+
{
|
|
13447
|
+
name: "log",
|
|
13448
|
+
synopsis: "log [--count 20] [--json]",
|
|
13449
|
+
about: ["show recent audit entries"],
|
|
13450
|
+
flags: [
|
|
13451
|
+
["--count <n>", "how many entries, newest last (default 20)"],
|
|
13452
|
+
["--json", "one JSON object per line"]
|
|
13453
|
+
]
|
|
13454
|
+
},
|
|
13455
|
+
{
|
|
13456
|
+
name: "verify",
|
|
13457
|
+
synopsis: "verify",
|
|
13458
|
+
about: ["verify the hash chain of the audit log"],
|
|
13459
|
+
flags: []
|
|
13460
|
+
},
|
|
13461
|
+
{
|
|
13462
|
+
name: "untaint",
|
|
13463
|
+
synopsis: "untaint [--session <id>] [--all]",
|
|
13464
|
+
about: [
|
|
13465
|
+
"clear a false-positive session's taint and provenance, or every session's.",
|
|
13466
|
+
"`stroq why` and `stroq log` show the session id"
|
|
13467
|
+
],
|
|
13468
|
+
flags: [
|
|
13469
|
+
["--session <id>", "the session to clear"],
|
|
13470
|
+
["--all", "clear every session"]
|
|
13471
|
+
]
|
|
13472
|
+
},
|
|
13473
|
+
{
|
|
13474
|
+
name: "why",
|
|
13475
|
+
synopsis: "why [--seq <n>]",
|
|
13476
|
+
about: ["explain the most recent denied or asked action: rule, provenance, taint"],
|
|
13477
|
+
flags: [["--seq <n>", "explain the audit entry with this sequence number instead"]]
|
|
13478
|
+
},
|
|
13479
|
+
{
|
|
13480
|
+
name: "replay",
|
|
13481
|
+
synopsis: "replay [<session>] [--last] [--transcript <path>] [--json] [--list]",
|
|
13482
|
+
about: [
|
|
13483
|
+
"rebuild the recorded sequence: which content the agent read, and which later",
|
|
13484
|
+
"actions matched it. --last reads the agent's own transcript, so it works on",
|
|
13485
|
+
"sessions that ran before you installed"
|
|
13486
|
+
],
|
|
13487
|
+
flags: [
|
|
13488
|
+
["--last", "the newest session recorded for this directory"],
|
|
13489
|
+
["--transcript <path>", "a specific transcript"],
|
|
13490
|
+
["--json", "machine-readable output"],
|
|
13491
|
+
["--list", "list the sessions in the audit log"]
|
|
13492
|
+
]
|
|
13493
|
+
},
|
|
13494
|
+
{
|
|
13495
|
+
name: "sent",
|
|
13496
|
+
synopsis: "sent [<session>] [--last] [--transcript <path>] [--json] [--fail-on-finding]",
|
|
13497
|
+
about: [
|
|
13498
|
+
"which credentials appear in a recorded agent session, and in which tool result",
|
|
13499
|
+
"or call. Reads names and sources only, never a value"
|
|
13500
|
+
],
|
|
13501
|
+
flags: [
|
|
13502
|
+
["--last", "the newest session recorded for this directory"],
|
|
13503
|
+
["--transcript <path>", "a specific transcript or rollout"],
|
|
13504
|
+
["--json", "machine-readable output"],
|
|
13505
|
+
["--fail-on-finding", "exit 1 when a credential is found"]
|
|
13506
|
+
]
|
|
13507
|
+
},
|
|
13508
|
+
{
|
|
13509
|
+
name: "canary",
|
|
13510
|
+
synopsis: "canary [--name <NAME>] [--file <path>]",
|
|
13511
|
+
about: [
|
|
13512
|
+
"print a canary secret to plant, or plant it as a decoy file. Its outbound use, or",
|
|
13513
|
+
"any call naming the file, is denied and taints the session"
|
|
13514
|
+
],
|
|
13515
|
+
flags: [
|
|
13516
|
+
["--name <NAME>", "the variable name to print it under (default STROQ_CANARY_KEY)"],
|
|
13517
|
+
["--file <path>", "create a credentials-shaped decoy file holding it"]
|
|
13518
|
+
]
|
|
13519
|
+
},
|
|
13520
|
+
{
|
|
13521
|
+
name: "attack",
|
|
13522
|
+
synopsis: "attack [--json] [--only <id>] [--fuzz]",
|
|
13523
|
+
about: ["replay recorded attacks against your policy; exit 1 if any gets through"],
|
|
13524
|
+
flags: [
|
|
13525
|
+
["--json", "machine-readable output"],
|
|
13526
|
+
["--only <id>", "one scenario, e.g. 05"],
|
|
13527
|
+
["--fuzz", "cross every scenario with every mutation and print the ones that escape"]
|
|
13528
|
+
]
|
|
13529
|
+
},
|
|
13530
|
+
{
|
|
13531
|
+
name: "exposure",
|
|
13532
|
+
synopsis: "exposure [--probe] [--share] [--json] [--verbose]",
|
|
13533
|
+
about: ["map this machine's agent surface and report what reaches you; exit 1 on a finding"],
|
|
13534
|
+
flags: [
|
|
13535
|
+
["--probe", "start your MCP servers once to read their tool descriptions"],
|
|
13536
|
+
["--share", "print a redacted summary you can paste"],
|
|
13537
|
+
["--json", "machine-readable output"],
|
|
13538
|
+
["--verbose", "list every file behind a count"]
|
|
13539
|
+
]
|
|
13540
|
+
},
|
|
13541
|
+
{
|
|
13542
|
+
name: "inspect",
|
|
13543
|
+
synopsis: "inspect [<dir>] [--json|--sarif] [--env]",
|
|
13544
|
+
about: ["read what a repository runs before you open it with an agent"],
|
|
13545
|
+
flags: [
|
|
13546
|
+
["--json", "machine-readable output"],
|
|
13547
|
+
["--sarif", "SARIF 2.1.0, for code scanning"],
|
|
13548
|
+
["--env", "print the git settings that neutralise it"]
|
|
13549
|
+
]
|
|
13550
|
+
},
|
|
13551
|
+
{
|
|
13552
|
+
name: "trust",
|
|
13553
|
+
synopsis: "trust [<file>] [--list] [--remove <file>] [--json]",
|
|
13554
|
+
about: [
|
|
13555
|
+
"waive a false positive on a file's exact content; any change to the file taints",
|
|
13556
|
+
"again. Without arguments, list what is trusted"
|
|
13557
|
+
],
|
|
13558
|
+
flags: [
|
|
13559
|
+
["--list", "list trusted files"],
|
|
13560
|
+
["--remove <file>", "stop trusting a file"],
|
|
13561
|
+
["--json", "machine-readable output"]
|
|
13562
|
+
]
|
|
13563
|
+
},
|
|
13564
|
+
{
|
|
13565
|
+
name: "bench",
|
|
13566
|
+
synopsis: "bench [--corpus <dir>] [--json] [--verbose]",
|
|
13567
|
+
about: ["measure how much benign developer text the rule set flags"],
|
|
13568
|
+
flags: [
|
|
13569
|
+
["--corpus <dir>", "measure your own files instead of the shipped corpus"],
|
|
13570
|
+
["--json", "machine-readable output"],
|
|
13571
|
+
["--verbose", "list the flagged files"]
|
|
13572
|
+
]
|
|
13573
|
+
},
|
|
13574
|
+
{
|
|
13575
|
+
name: "coverage",
|
|
13576
|
+
synopsis: "coverage [--format table|navigator] [--json]",
|
|
13577
|
+
about: ["the control mapping against MITRE ATLAS and OWASP ASI"],
|
|
13578
|
+
flags: [
|
|
13579
|
+
["--format <table|navigator>", "navigator emits an ATT&CK Navigator layer"],
|
|
13580
|
+
["--json", "machine-readable output"]
|
|
13581
|
+
]
|
|
13582
|
+
}
|
|
13583
|
+
];
|
|
13584
|
+
var byName = new Map(COMMANDS.map((c) => [c.name, c]));
|
|
13585
|
+
var INDENT = 37;
|
|
13586
|
+
function usage() {
|
|
13587
|
+
const blocks = COMMANDS.map((c) => {
|
|
13588
|
+
const head = ` ${c.synopsis}`;
|
|
13589
|
+
const [first = "", ...more] = c.about;
|
|
13590
|
+
const lines = head.length < INDENT - 1 ? [`${head.padEnd(INDENT)}${first}`, ...more.map((l) => `${" ".repeat(INDENT)}${l}`)] : [head, ...c.about.map((l) => `${" ".repeat(INDENT)}${l}`)];
|
|
13591
|
+
return lines.join("\n");
|
|
13592
|
+
});
|
|
13593
|
+
return [
|
|
13594
|
+
"stroq <command>",
|
|
13595
|
+
"",
|
|
13596
|
+
"Commands:",
|
|
13597
|
+
...blocks,
|
|
13598
|
+
`${" help [<command>]".padEnd(INDENT)}this list, or one command's options`,
|
|
13599
|
+
`${" --version".padEnd(INDENT)}print the CLI version`,
|
|
13600
|
+
"",
|
|
13601
|
+
'Run "stroq <command> --help" for its options.',
|
|
13602
|
+
""
|
|
13603
|
+
].join("\n");
|
|
13604
|
+
}
|
|
13605
|
+
function commandHelp(name) {
|
|
13606
|
+
const c = byName.get(name);
|
|
13607
|
+
if (c === void 0) return null;
|
|
13608
|
+
const width = Math.max(0, ...c.flags.map(([spelling]) => spelling.length)) + 4;
|
|
13609
|
+
return [
|
|
13610
|
+
`stroq ${c.synopsis}`,
|
|
13611
|
+
"",
|
|
13612
|
+
...c.about.map((line3) => ` ${line3}`),
|
|
13613
|
+
...c.flags.length === 0 ? [] : [
|
|
13614
|
+
"",
|
|
13615
|
+
"Options:",
|
|
13616
|
+
...c.flags.map(([spelling, meaning]) => ` ${spelling.padEnd(width)}${meaning}`)
|
|
13617
|
+
],
|
|
13618
|
+
""
|
|
13619
|
+
].join("\n");
|
|
13620
|
+
}
|
|
13621
|
+
function ownArgs(name, args) {
|
|
13622
|
+
if (byName.get(name)?.passThrough !== true) return args;
|
|
13623
|
+
const end = args.indexOf("--");
|
|
13624
|
+
return end === -1 ? args : args.slice(0, end);
|
|
13625
|
+
}
|
|
13626
|
+
function wantsHelp(name, args) {
|
|
13627
|
+
return ownArgs(name, args).some((arg) => arg === "--help" || arg === "-h");
|
|
13628
|
+
}
|
|
13629
|
+
function unknownOption(name, args) {
|
|
13630
|
+
const c = byName.get(name);
|
|
13631
|
+
if (c === void 0) return null;
|
|
13632
|
+
const known = /* @__PURE__ */ new Set([
|
|
13633
|
+
"--help",
|
|
13634
|
+
"-h",
|
|
13635
|
+
...c.flags.map(([spelling]) => spelling.split(/[\s|<]/)[0])
|
|
13636
|
+
]);
|
|
13637
|
+
for (const arg of ownArgs(name, args)) {
|
|
13638
|
+
if (!arg.startsWith("-") || arg === "-" || arg === "--") continue;
|
|
13639
|
+
const option = arg.split("=")[0] ?? arg;
|
|
13640
|
+
if (!known.has(option)) return option;
|
|
13641
|
+
}
|
|
13642
|
+
return null;
|
|
13643
|
+
}
|
|
13644
|
+
function usageError(name, problem) {
|
|
13645
|
+
return `stroq ${name}: ${problem}
|
|
13646
|
+
Run "stroq ${name} --help" to see its options.
|
|
13202
13647
|
`;
|
|
13648
|
+
}
|
|
13649
|
+
function parseArgsProblem(err) {
|
|
13650
|
+
const code = err?.code;
|
|
13651
|
+
if (typeof code !== "string" || !code.startsWith("ERR_PARSE_ARGS_")) return null;
|
|
13652
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13653
|
+
const first = message.split(/(?<=\.)\s/)[0] ?? message;
|
|
13654
|
+
return first.replace(/\.$/, "");
|
|
13655
|
+
}
|
|
13656
|
+
function suggestCommand(typed) {
|
|
13657
|
+
let best = null;
|
|
13658
|
+
let bestDistance = 3;
|
|
13659
|
+
for (const { name } of COMMANDS) {
|
|
13660
|
+
const d = distance(typed.toLowerCase(), name);
|
|
13661
|
+
if (d < bestDistance) {
|
|
13662
|
+
best = name;
|
|
13663
|
+
bestDistance = d;
|
|
13664
|
+
}
|
|
13665
|
+
}
|
|
13666
|
+
return best;
|
|
13667
|
+
}
|
|
13668
|
+
function distance(a, b) {
|
|
13669
|
+
const rows = Array.from(
|
|
13670
|
+
{ length: a.length + 1 },
|
|
13671
|
+
(_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
13672
|
+
);
|
|
13673
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
13674
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
13675
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
13676
|
+
const row2 = rows[i];
|
|
13677
|
+
const above = rows[i - 1];
|
|
13678
|
+
row2[j] = Math.min(above[j] + 1, row2[j - 1] + 1, above[j - 1] + cost);
|
|
13679
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
13680
|
+
row2[j] = Math.min(row2[j], rows[i - 2][j - 2] + 1);
|
|
13681
|
+
}
|
|
13682
|
+
}
|
|
13683
|
+
return rows[a.length][b.length];
|
|
13684
|
+
}
|
|
13685
|
+
|
|
13686
|
+
// src/index.ts
|
|
13687
|
+
var COMMANDS2 = {
|
|
13688
|
+
inspect: runInspect,
|
|
13689
|
+
init: runInit,
|
|
13690
|
+
uninstall: runUninstall,
|
|
13691
|
+
run: runRun,
|
|
13692
|
+
doctor: runDoctor,
|
|
13693
|
+
log: runLog,
|
|
13694
|
+
verify: () => runVerify(),
|
|
13695
|
+
trust: runTrust,
|
|
13696
|
+
untaint: runUntaint,
|
|
13697
|
+
replay: runReplay,
|
|
13698
|
+
why: runWhy,
|
|
13699
|
+
sent: runSent,
|
|
13700
|
+
canary: runCanary,
|
|
13701
|
+
attack: runAttackCommand,
|
|
13702
|
+
exposure: runExposure,
|
|
13703
|
+
bench: runBenchCommand,
|
|
13704
|
+
coverage: runCoverageCommand
|
|
13705
|
+
};
|
|
13203
13706
|
async function main(argv) {
|
|
13204
13707
|
const [command, ...rest] = argv;
|
|
13205
13708
|
switch (command) {
|
|
13206
13709
|
case "hook": {
|
|
13710
|
+
if (rest[0] === void 0 || wantsHelp("hook", rest)) {
|
|
13711
|
+
process.stdout.write(commandHelp("hook") ?? "");
|
|
13712
|
+
return rest[0] === void 0 ? 2 : 0;
|
|
13713
|
+
}
|
|
13207
13714
|
const out = await runHookCommand(rest[0] ?? "", rest[1] ?? "");
|
|
13208
13715
|
if (out.stdout) process.stdout.write(out.stdout);
|
|
13209
13716
|
if (out.stderr) process.stderr.write(out.stderr);
|
|
@@ -13211,66 +13718,74 @@ async function main(argv) {
|
|
|
13211
13718
|
return out.exitCode;
|
|
13212
13719
|
}
|
|
13213
13720
|
case "mcp":
|
|
13721
|
+
if (wantsHelp("mcp", rest)) {
|
|
13722
|
+
process.stdout.write(commandHelp("mcp") ?? "");
|
|
13723
|
+
return 0;
|
|
13724
|
+
}
|
|
13214
13725
|
return runMcp(rest);
|
|
13215
13726
|
default:
|
|
13216
13727
|
return withSafeOutput(() => report(command, rest));
|
|
13217
13728
|
}
|
|
13218
13729
|
}
|
|
13219
13730
|
async function report(command, rest) {
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
return runRun(rest);
|
|
13227
|
-
case "doctor":
|
|
13228
|
-
return runDoctor(rest);
|
|
13229
|
-
case "log":
|
|
13230
|
-
return runLog(rest);
|
|
13231
|
-
case "verify":
|
|
13232
|
-
return runVerify();
|
|
13233
|
-
case "trust":
|
|
13234
|
-
return runTrust(rest);
|
|
13235
|
-
case "untaint":
|
|
13236
|
-
return runUntaint(rest);
|
|
13237
|
-
case "replay":
|
|
13238
|
-
return runReplay(rest);
|
|
13239
|
-
case "why":
|
|
13240
|
-
return runWhy(rest);
|
|
13241
|
-
case "sent":
|
|
13242
|
-
return runSent(rest);
|
|
13243
|
-
case "canary":
|
|
13244
|
-
return runCanary(rest);
|
|
13245
|
-
case "attack":
|
|
13246
|
-
return runAttackCommand(rest);
|
|
13247
|
-
case "exposure":
|
|
13248
|
-
return runExposure(rest);
|
|
13249
|
-
case "bench":
|
|
13250
|
-
return runBenchCommand(rest);
|
|
13251
|
-
case "coverage":
|
|
13252
|
-
return runCoverageCommand(rest);
|
|
13253
|
-
case "--version":
|
|
13254
|
-
case "-v":
|
|
13255
|
-
case "version":
|
|
13256
|
-
process.stdout.write(`${stroqVersion()}
|
|
13731
|
+
if (command === void 0 || command === "--help" || command === "-h") {
|
|
13732
|
+
process.stdout.write(usage());
|
|
13733
|
+
return 0;
|
|
13734
|
+
}
|
|
13735
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
13736
|
+
process.stdout.write(`${stroqVersion()}
|
|
13257
13737
|
`);
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13738
|
+
return 0;
|
|
13739
|
+
}
|
|
13740
|
+
if (command === "help") {
|
|
13741
|
+
const help = rest[0] === void 0 ? usage() : commandHelp(rest[0]);
|
|
13742
|
+
if (help === null) return unknownCommand(rest[0] ?? "");
|
|
13743
|
+
process.stdout.write(help);
|
|
13744
|
+
return 0;
|
|
13745
|
+
}
|
|
13746
|
+
const runner = COMMANDS2[command];
|
|
13747
|
+
if (runner === void 0) return unknownCommand(command);
|
|
13748
|
+
if (command !== "sent" && wantsHelp(command, rest)) {
|
|
13749
|
+
process.stdout.write(commandHelp(command) ?? "");
|
|
13750
|
+
return 0;
|
|
13751
|
+
}
|
|
13752
|
+
const unknown = unknownOption(command, rest);
|
|
13753
|
+
if (unknown !== null) {
|
|
13754
|
+
process.stderr.write(usageError(command, `unknown option ${unknown}`));
|
|
13755
|
+
return 2;
|
|
13756
|
+
}
|
|
13757
|
+
try {
|
|
13758
|
+
return await runner(rest);
|
|
13759
|
+
} catch (err) {
|
|
13760
|
+
const problem = parseArgsProblem(err);
|
|
13761
|
+
if (problem === null) throw err;
|
|
13762
|
+
process.stderr.write(usageError(command, problem));
|
|
13763
|
+
return 2;
|
|
13262
13764
|
}
|
|
13263
13765
|
}
|
|
13766
|
+
function unknownCommand(typed) {
|
|
13767
|
+
const suggestion = suggestCommand(typed);
|
|
13768
|
+
process.stderr.write(
|
|
13769
|
+
`stroq: unknown command "${typed}".${suggestion === null ? "" : ` Did you mean "${suggestion}"?`}
|
|
13770
|
+
Run "stroq --help" for the list of commands.
|
|
13771
|
+
`
|
|
13772
|
+
);
|
|
13773
|
+
return 1;
|
|
13774
|
+
}
|
|
13264
13775
|
async function exitNow(code) {
|
|
13265
13776
|
await Promise.all(
|
|
13266
13777
|
[process.stdout, process.stderr].map(
|
|
13267
|
-
(stream) => new Promise((
|
|
13268
|
-
stream.write("", () =>
|
|
13778
|
+
(stream) => new Promise((resolve15) => {
|
|
13779
|
+
stream.write("", () => resolve15());
|
|
13269
13780
|
})
|
|
13270
13781
|
)
|
|
13271
13782
|
);
|
|
13272
13783
|
process.exit(code);
|
|
13273
13784
|
}
|
|
13785
|
+
process.stdout.on("error", (err) => {
|
|
13786
|
+
if (err.code === "EPIPE") process.exit(process.exitCode ?? 0);
|
|
13787
|
+
throw err;
|
|
13788
|
+
});
|
|
13274
13789
|
main(process.argv.slice(2)).then(
|
|
13275
13790
|
(code) => {
|
|
13276
13791
|
process.exitCode = code;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "stroq",
|
|
3
3
|
"name": "Stroq",
|
|
4
4
|
"description": "Local action firewall for OpenClaw: scans what the agent reads, taints the session, blocks or asks before dangerous tool calls.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.20.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED