vigiles 12.4.0 → 12.5.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.
@@ -50,4 +50,35 @@ export declare function isReadOnlyBash(command: string): boolean;
50
50
  * native `Bash(git:*)` glob (issue #30519) and a hand-written `grep` both miss.
51
51
  */
52
52
  export declare function leafCommands(command: string): string[][];
53
+ /** A single simple command, normalized to its operation form. */
54
+ export interface NormalizedLeaf {
55
+ /** Head normalized to its basename, backslash-stripped: `/bin/rm`→`rm`, `\rm`→`rm`. */
56
+ readonly head: string;
57
+ /** `[head, ...args]` — every word quote-unwrapped and $HOME/~-canonicalized. */
58
+ readonly argv: readonly string[];
59
+ /** The normalized args (argv without the head). */
60
+ readonly args: readonly string[];
61
+ /**
62
+ * Canonical flag tokens present on this leaf. Short clusters are split
63
+ * (`-rf`→`r`,`f`) and each flag is recorded in BOTH short and long form via the
64
+ * alias table, so a caller can test `--force`/`-f` or `--no-verify`/`-n`
65
+ * uniformly. Long values are split on `=` (`--index-url=x`→`index-url`).
66
+ */
67
+ readonly flags: ReadonlySet<string>;
68
+ /** True iff ANY of `names` is present in {@link flags} (short or long). */
69
+ hasFlag(...names: readonly string[]): boolean;
70
+ }
71
+ /**
72
+ * Extract every simple command as a {@link NormalizedLeaf} — the operation-level
73
+ * twin of {@link leafCommands}. Same AST-backed structural coverage (a leaf nested
74
+ * in a pipeline / `&&` / subshell is still found), but each word is quote-unwrapped
75
+ * and $HOME-canonicalized, the head is reduced to its backslash-stripped basename,
76
+ * and flags are expanded to short+long canonical forms. A matcher built on this
77
+ * compares against the OPERATION rather than the surface tokens, so it is robust
78
+ * to quoting, interpreter path, backslash escaping, flag aliasing, and $HOME/~.
79
+ *
80
+ * Purely additive: reuses the same mvdan-sh parse, changes nothing above. Parse
81
+ * failure → []. A leaf with a dynamic head is skipped (can't be normalized).
82
+ */
83
+ export declare function leafCommandsNormalized(command: string): NormalizedLeaf[];
53
84
  //# sourceMappingURL=bash-effects.d.ts.map
@@ -27,6 +27,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
27
27
  exports.classifyBashCommand = classifyBashCommand;
28
28
  exports.isReadOnlyBash = isReadOnlyBash;
29
29
  exports.leafCommands = leafCommands;
30
+ exports.leafCommandsNormalized = leafCommandsNormalized;
30
31
  // mvdan-sh is a CJS package (GopherJS build) with no bundled TypeScript types.
31
32
  // The project compiles to CommonJS (Node16, no "type":"module"), so plain
32
33
  // require() works and is the idiomatic pattern here (see linters.ts).
@@ -433,4 +434,136 @@ function leafCommands(command) {
433
434
  });
434
435
  return out;
435
436
  }
437
+ /**
438
+ * Known short↔long flag aliases. Deliberately small and operation-relevant:
439
+ * a caller always gates on the head (e.g. only treats `index-url` as supply-chain
440
+ * when the head is `pip`), so recording both forms unconditionally is safe.
441
+ */
442
+ const SHORT_TO_LONG = {
443
+ f: "force",
444
+ n: "no-verify",
445
+ r: "recursive",
446
+ i: "index-url",
447
+ };
448
+ const LONG_TO_SHORT = {
449
+ force: "f",
450
+ "no-verify": "n",
451
+ recursive: "r",
452
+ "index-url": "i",
453
+ };
454
+ /**
455
+ * Reconstruct a Word's static text, unwrapping single/double quotes and
456
+ * canonicalizing `$HOME`/`${HOME}` to `~`. Returns null if the word contains a
457
+ * truly dynamic segment (command substitution, arithmetic, a non-HOME parameter)
458
+ * — such a word can't be soundly reduced to a literal operation token.
459
+ */
460
+ function normalizeParts(parts) {
461
+ if (!parts)
462
+ return null;
463
+ let out = "";
464
+ for (const p of parts) {
465
+ const t = sh.syntax.NodeType(p);
466
+ if (t === "Lit" || t === "SglQuoted") {
467
+ out += p.Value ?? "";
468
+ }
469
+ else if (t === "DblQuoted") {
470
+ const inner = normalizeParts(p.Parts);
471
+ if (inner === null)
472
+ return null;
473
+ out += inner;
474
+ }
475
+ else if (t === "ParamExp") {
476
+ // Canonicalize the home directory; any other parameter is dynamic.
477
+ if (p.Param?.Value === "HOME")
478
+ out += "~";
479
+ else
480
+ return null;
481
+ }
482
+ else {
483
+ return null; // CmdSubst / ArithmExp / ProcSubst / … → dynamic
484
+ }
485
+ }
486
+ return out;
487
+ }
488
+ /** Normalize a command head to its basename, stripping one leading backslash. */
489
+ function normalizeHead(raw) {
490
+ const unescaped = raw.startsWith("\\") ? raw.slice(1) : raw;
491
+ const slash = unescaped.lastIndexOf("/");
492
+ return slash >= 0 ? unescaped.slice(slash + 1) : unescaped;
493
+ }
494
+ /** Build the canonical flag set for a leaf's args (short-cluster + alias expansion). */
495
+ function buildFlags(args) {
496
+ const flags = new Set();
497
+ const add = (name) => {
498
+ if (!name)
499
+ return;
500
+ flags.add(name);
501
+ if (name.length === 1 && SHORT_TO_LONG[name])
502
+ flags.add(SHORT_TO_LONG[name]);
503
+ const short = LONG_TO_SHORT[name];
504
+ if (short)
505
+ flags.add(short);
506
+ };
507
+ for (const a of args) {
508
+ if (a.startsWith("--")) {
509
+ add(a.slice(2).split("=")[0] ?? "");
510
+ }
511
+ else if (a.length > 1 && a[0] === "-") {
512
+ for (const ch of a.slice(1)) {
513
+ if (/[a-zA-Z]/.test(ch))
514
+ add(ch);
515
+ }
516
+ }
517
+ }
518
+ return flags;
519
+ }
520
+ /**
521
+ * Extract every simple command as a {@link NormalizedLeaf} — the operation-level
522
+ * twin of {@link leafCommands}. Same AST-backed structural coverage (a leaf nested
523
+ * in a pipeline / `&&` / subshell is still found), but each word is quote-unwrapped
524
+ * and $HOME-canonicalized, the head is reduced to its backslash-stripped basename,
525
+ * and flags are expanded to short+long canonical forms. A matcher built on this
526
+ * compares against the OPERATION rather than the surface tokens, so it is robust
527
+ * to quoting, interpreter path, backslash escaping, flag aliasing, and $HOME/~.
528
+ *
529
+ * Purely additive: reuses the same mvdan-sh parse, changes nothing above. Parse
530
+ * failure → []. A leaf with a dynamic head is skipped (can't be normalized).
531
+ */
532
+ function leafCommandsNormalized(command) {
533
+ let file;
534
+ try {
535
+ file = sh.syntax.NewParser().Parse(command, "cmd.sh");
536
+ }
537
+ catch {
538
+ return [];
539
+ }
540
+ const out = [];
541
+ sh.syntax.Walk(file, (node) => {
542
+ const leaf = normalizeCallExpr(node);
543
+ if (leaf)
544
+ out.push(leaf);
545
+ return true;
546
+ });
547
+ return out;
548
+ }
549
+ /** Normalize a single CallExpr node to a {@link NormalizedLeaf}, or null if it isn't one / has a dynamic head. */
550
+ function normalizeCallExpr(node) {
551
+ if (sh.syntax.NodeType(node) !== "CallExpr" || !node.Args?.length)
552
+ return null;
553
+ const headRaw = normalizeParts(node.Args[0]?.Parts);
554
+ if (headRaw === null)
555
+ return null; // dynamic head → not normalizable
556
+ const head = normalizeHead(headRaw);
557
+ const args = node.Args.slice(1)
558
+ .map((w) => normalizeParts(w.Parts))
559
+ .filter((w) => w !== null);
560
+ const flags = buildFlags(args);
561
+ return {
562
+ head,
563
+ argv: [head, ...args],
564
+ args,
565
+ flags,
566
+ hasFlag: (...names) => names.some((n) => flags.has(n)),
567
+ };
568
+ }
436
569
  //# sourceMappingURL=bash-effects.js.map
package/dist/hook.d.ts CHANGED
@@ -49,4 +49,6 @@ export { defineHook, defineFileGate, definePromptGate, defineStopGate, tool, too
49
49
  export type { Decision, HookMode, GateAction, CommandView, PathView, ResponseView, BashToolEvent, FileToolEvent, PromptEvent, StopEvent, ReactEvent, SessionEvent, HookProgram, FileGateHook, PromptGateHook, StopGateHook, InjectHook, ReactHook, AnyHook, DispatchKind, Injection, Reaction, RunReaction, CompiledHookProgram, CompileHookOptions, RawHookEvent, HookProgramOutcome, } from "./core/hook-program.js";
50
50
  export { provide, dangerously, defineProvider, provider, } from "./core/hook-providers.js";
51
51
  export type { ProviderName, ProviderResults, HookCtx, NeedSpec, InlineProvider, RegisteredProvider, RegisteredRef, ProviderRegistry, } from "./core/hook-providers.js";
52
+ export { leafCommandsNormalized } from "./core/bash-effects.js";
53
+ export type { NormalizedLeaf } from "./core/bash-effects.js";
52
54
  //# sourceMappingURL=hook.d.ts.map
package/dist/hook.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.defineReact = exports.inject = exports.defineInject = exports.hookMode = exports.gateAction = exports.pathView = exports.commandView = exports.ask = exports.deny = exports.allow = exports.tools = exports.tool = exports.defineStopGate = exports.definePromptGate = exports.defineFileGate = exports.defineHook = void 0;
3
+ exports.leafCommandsNormalized = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.defineReact = exports.inject = exports.defineInject = exports.hookMode = exports.gateAction = exports.pathView = exports.commandView = exports.ask = exports.deny = exports.allow = exports.tools = exports.tool = exports.defineStopGate = exports.definePromptGate = exports.defineFileGate = exports.defineHook = void 0;
4
4
  /**
5
5
  * `vigiles/hook` — the **closed vocabulary** for authoring a compiled hook.
6
6
  *
@@ -95,4 +95,8 @@ Object.defineProperty(exports, "provide", { enumerable: true, get: function () {
95
95
  Object.defineProperty(exports, "dangerously", { enumerable: true, get: function () { return hook_providers_js_1.dangerously; } });
96
96
  Object.defineProperty(exports, "defineProvider", { enumerable: true, get: function () { return hook_providers_js_1.defineProvider; } });
97
97
  Object.defineProperty(exports, "provider", { enumerable: true, get: function () { return hook_providers_js_1.provider; } });
98
+ // Operation-normalized leaf extraction — the robust matching primitive a
99
+ // hardened guard is built on (see examples/harness/safe-bash-guard-v2.mjs).
100
+ var bash_effects_js_1 = require("./core/bash-effects.js");
101
+ Object.defineProperty(exports, "leafCommandsNormalized", { enumerable: true, get: function () { return bash_effects_js_1.leafCommandsNormalized; } });
98
102
  //# sourceMappingURL=hook.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "12.4.0",
3
+ "version": "12.5.0",
4
4
  "description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
5
5
  "keywords": [
6
6
  "claude-code",