@yagni-app/code 1.0.6 → 1.0.7

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.
Files changed (85) hide show
  1. package/README.md +84 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +13 -0
  12. package/dist/extension/index.js +230 -43
  13. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  14. package/dist/extension/mcp/cliConfig.js +1 -1
  15. package/dist/extension/mcp/config.d.ts +24 -2
  16. package/dist/extension/mcp/config.js +75 -3
  17. package/dist/extension/mcp/manager.d.ts +3 -1
  18. package/dist/extension/mcp/manager.js +2 -2
  19. package/dist/extension/mcp/panel.d.ts +0 -1
  20. package/dist/extension/mcp/panel.js +13 -3
  21. package/dist/extension/mcp/startup.js +8 -6
  22. package/dist/extension/permission/gate.d.ts +97 -2
  23. package/dist/extension/permission/gate.js +375 -26
  24. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  25. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  26. package/dist/extension/permissionRules/engine.d.ts +50 -0
  27. package/dist/extension/permissionRules/engine.js +238 -0
  28. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  29. package/dist/extension/permissionRules/loadConfig.js +90 -0
  30. package/dist/extension/permissionRules/parser.d.ts +38 -0
  31. package/dist/extension/permissionRules/parser.js +136 -0
  32. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  33. package/dist/extension/permissionRules/pathRules.js +120 -0
  34. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  35. package/dist/extension/permissionRules/shellRules.js +221 -0
  36. package/dist/extension/pipeline/invocation.d.ts +3 -6
  37. package/dist/extension/pipeline/invocation.js +3 -6
  38. package/dist/extension/pipeline/runner.d.ts +0 -1
  39. package/dist/extension/pipeline/runner.js +6 -14
  40. package/dist/extension/plugins/inventory.d.ts +88 -0
  41. package/dist/extension/plugins/inventory.js +144 -0
  42. package/dist/extension/plugins/panel.d.ts +45 -0
  43. package/dist/extension/plugins/panel.js +293 -0
  44. package/dist/extension/sandbox/bash.d.ts +99 -0
  45. package/dist/extension/sandbox/bash.js +190 -0
  46. package/dist/extension/sandbox/config.d.ts +114 -0
  47. package/dist/extension/sandbox/config.js +366 -0
  48. package/dist/extension/sandbox/manager.d.ts +98 -0
  49. package/dist/extension/sandbox/manager.js +216 -0
  50. package/dist/extension/sandbox/panel.d.ts +111 -0
  51. package/dist/extension/sandbox/panel.js +342 -0
  52. package/dist/extension/sandbox/session.d.ts +85 -0
  53. package/dist/extension/sandbox/session.js +775 -0
  54. package/dist/extension/telemetry/attrs.d.ts +96 -0
  55. package/dist/extension/telemetry/attrs.js +149 -0
  56. package/dist/extension/telemetry/config.d.ts +99 -0
  57. package/dist/extension/telemetry/config.js +193 -0
  58. package/dist/extension/telemetry/index.d.ts +7 -0
  59. package/dist/extension/telemetry/index.js +7 -0
  60. package/dist/extension/telemetry/probe.d.ts +29 -0
  61. package/dist/extension/telemetry/probe.js +122 -0
  62. package/dist/extension/telemetry/register.d.ts +40 -0
  63. package/dist/extension/telemetry/register.js +192 -0
  64. package/dist/extension/telemetry/sdk.d.ts +63 -0
  65. package/dist/extension/telemetry/sdk.js +207 -0
  66. package/dist/extension/telemetry/tracker.d.ts +131 -0
  67. package/dist/extension/telemetry/tracker.js +551 -0
  68. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  69. package/dist/extension/vendor/ignore.d.ts +86 -0
  70. package/dist/extension/vendor/ignore.js +788 -0
  71. package/dist/goHeadless.d.ts +1 -1
  72. package/dist/goHeadless.js +2 -2
  73. package/dist/launch.d.ts +4 -3
  74. package/dist/launch.js +7 -4
  75. package/dist/mcpCommand.d.ts +10 -1
  76. package/dist/mcpCommand.js +42 -10
  77. package/dist/otel.d.ts +67 -90
  78. package/dist/otel.js +152 -195
  79. package/dist/paths.d.ts +13 -0
  80. package/dist/paths.js +18 -0
  81. package/dist/pluginCommand.d.ts +43 -0
  82. package/dist/pluginCommand.js +499 -0
  83. package/dist/pluginStore.d.ts +170 -0
  84. package/dist/pluginStore.js +554 -0
  85. package/package.json +19 -3
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Permission-rule settings loader.
3
+ *
4
+ * Reads `permissions: { allow, deny, ask }` from the two YAGNI Code settings
5
+ * files, mirroring the hooks loader (hooks.ts) shape exactly:
6
+ * - user: ~/.yagni-code/config.json — always active
7
+ * - project: .yagni-code/config.json — deny/ask always active;
8
+ * allow gated on workspace trust (Claude Code semantics:
9
+ * "deny and ask rules apply right away; allow rules from a
10
+ * project file wait for trust")
11
+ *
12
+ * Lists UNION across sources (Claude Code: lists merge, never replace).
13
+ * User-source rules are ordered before project-source rules so path-pattern
14
+ * negation (`!exception`) behaves like gitignore lines across files.
15
+ *
16
+ * Fail-soft: a malformed config file is skipped whole with a warning (same
17
+ * posture as hooks.ts). Malformed individual rule strings parse as bare
18
+ * tool names (parser degrades, never throws) and are flagged.
19
+ */
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { codeStateHome } from "../stateHome.js";
23
+ import { parseRuleString } from "./parser.js";
24
+ /** The tools a rule can actually be consulted for at the gate. */
25
+ export const KNOWN_RULE_TOOLS = new Set([
26
+ "read", "edit", "write", "grep", "find", "ls", "bash", "web_fetch",
27
+ "ask_yagni", "ask_advisor", "ask_user_question", "file_ticket",
28
+ "update_ticket_status", "review_business_match", "suggest_next_work",
29
+ "record_engineering_context", "record_decision",
30
+ ]);
31
+ function readRulesFromFile(path, source, behavior, out, warnings) {
32
+ try {
33
+ if (!existsSync(path))
34
+ return;
35
+ const raw = readFileSync(path, "utf-8");
36
+ if (raw.trim() === "")
37
+ return;
38
+ const parsed = JSON.parse(raw);
39
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40
+ warnings.push(`${source} settings: not a JSON object — permissions skipped (${path})`);
41
+ return;
42
+ }
43
+ const permissions = parsed.permissions;
44
+ if (permissions === undefined)
45
+ return;
46
+ if (!permissions || typeof permissions !== "object" || Array.isArray(permissions)) {
47
+ warnings.push(`${source} settings: "permissions" is not an object — skipped (${path})`);
48
+ return;
49
+ }
50
+ const entries = permissions[behavior];
51
+ if (entries === undefined)
52
+ return;
53
+ const list = Array.isArray(entries) ? entries : [entries];
54
+ for (const entry of list) {
55
+ if (typeof entry !== "string") {
56
+ warnings.push(`${source} settings: non-string ${behavior} rule skipped (${path})`);
57
+ continue;
58
+ }
59
+ const { toolName, ruleContent } = parseRuleString(entry);
60
+ out.push({ behavior, source, toolName, ruleContent, raw: entry });
61
+ }
62
+ }
63
+ catch {
64
+ warnings.push(`${source} settings: malformed JSON — permissions skipped (${path})`);
65
+ }
66
+ }
67
+ /** Load + union rules from both files. Pure I/O; no throw. */
68
+ export function loadPermissionRules(opts = {}) {
69
+ const warnings = [];
70
+ const rules = [];
71
+ const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
72
+ const userPath = join(stateHome, "config.json");
73
+ const projectPath = join(opts.cwd ?? process.cwd(), ".yagni-code", "config.json");
74
+ // User first, project second — ordering matters for path-rule negation.
75
+ for (const behavior of ["deny", "ask", "allow"]) {
76
+ readRulesFromFile(userPath, "user", behavior, rules, warnings);
77
+ readRulesFromFile(projectPath, "project", behavior, rules, warnings);
78
+ }
79
+ const neverConsultedTools = [
80
+ ...new Set(rules.map((r) => r.toolName).filter((t) => !KNOWN_RULE_TOOLS.has(t) && !t.startsWith("mcp__"))),
81
+ ];
82
+ return { rules, diagnostics: { warnings, neverConsultedTools } };
83
+ }
84
+ /** Filter rules by behavior + trust (project allow needs trust; deny/ask always). */
85
+ export function effectiveRules(rules, isProjectTrusted) {
86
+ if (isProjectTrusted)
87
+ return [...rules];
88
+ return rules.filter((r) => !(r.source === "project" && r.behavior === "allow"));
89
+ }
90
+ //# sourceMappingURL=loadConfig.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Permission-rule string parser.
3
+ *
4
+ * Parses `ToolName` / `ToolName(content)` rule strings — the exact format
5
+ * Claude Code uses in settings.json `permissions.allow/deny/ask` arrays —
6
+ * so a rule pasted from Claude Code works unchanged.
7
+ *
8
+ * Tool-name aliasing: Claude Code names (Read/Edit/Write/…) map onto this
9
+ * agent's tool names (read/edit/write/…). Both spellings are accepted; rules
10
+ * for tools that do not exist here parse fine but are flagged
11
+ * never-consulted by the loader's diagnostics.
12
+ *
13
+ * Ported from Claude Code's permissionRuleParser.ts (escaping semantics,
14
+ * `ToolName(*)` ≡ tool-wide) with the alias table added.
15
+ */
16
+ export interface ParsedRuleValue {
17
+ /** Normalized tool name this rule targets (after aliasing). */
18
+ toolName: string;
19
+ /** The raw content inside the parens, unescaped; undefined for bare rules. */
20
+ ruleContent?: string;
21
+ }
22
+ /** Normalize a rule's tool name: lowercase → alias → MCP passthrough. */
23
+ export declare function normalizeRuleToolName(raw: string): string;
24
+ /** Escape parens/backslashes in rule content for storage (parser inverse). */
25
+ export declare function escapeRuleContent(content: string): string;
26
+ /** Unescape rule content after parsing. */
27
+ export declare function unescapeRuleContent(content: string): string;
28
+ /**
29
+ * Parse a rule string into its components. Malformed shapes degrade the same
30
+ * way Claude Code's parser degrades them: treat the whole string as a bare
31
+ * tool name (never throw) — the loader's diagnostics flag them.
32
+ *
33
+ * `Bash(*)` and `Bash()` are tool-wide rules, same as bare `Bash`.
34
+ */
35
+ export declare function parseRuleString(ruleString: string): ParsedRuleValue;
36
+ /** Serialize a parsed rule back to its string form (loader diagnostics). */
37
+ export declare function ruleValueToString(value: ParsedRuleValue): string;
38
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Permission-rule string parser.
3
+ *
4
+ * Parses `ToolName` / `ToolName(content)` rule strings — the exact format
5
+ * Claude Code uses in settings.json `permissions.allow/deny/ask` arrays —
6
+ * so a rule pasted from Claude Code works unchanged.
7
+ *
8
+ * Tool-name aliasing: Claude Code names (Read/Edit/Write/…) map onto this
9
+ * agent's tool names (read/edit/write/…). Both spellings are accepted; rules
10
+ * for tools that do not exist here parse fine but are flagged
11
+ * never-consulted by the loader's diagnostics.
12
+ *
13
+ * Ported from Claude Code's permissionRuleParser.ts (escaping semantics,
14
+ * `ToolName(*)` ≡ tool-wide) with the alias table added.
15
+ */
16
+ import { mcpInfoFromString } from "../mcp/names.js";
17
+ /**
18
+ * Claude Code tool names → this agent's tool names. Keys are matched
19
+ * case-insensitively at parse time (Claude Code rules use PascalCase; our
20
+ * registered tools are lowercase). MCP names (`mcp__server__tool`) pass
21
+ * through untouched — naming is already byte-identical (mcp/names.ts).
22
+ */
23
+ const TOOL_NAME_ALIASES = {
24
+ read: "read",
25
+ edit: "edit",
26
+ write: "write",
27
+ grep: "grep",
28
+ find: "find",
29
+ glob: "find", // Claude Code's Glob ≈ our find (pattern-based file search)
30
+ ls: "ls",
31
+ bash: "bash",
32
+ webfetch: "web_fetch", // Claude Code's WebFetch → our web_fetch
33
+ // YAGNI-registered tools, accepted under their own names (aliases exist so
34
+ // a rule copied from a Claude Code setup that wrapped the same capability
35
+ // under a different name still lands on ours where sensible).
36
+ ask_yagni: "ask_yagni",
37
+ ask_advisor: "ask_advisor",
38
+ ask_user_question: "ask_user_question",
39
+ web_fetch: "web_fetch",
40
+ file_ticket: "file_ticket",
41
+ update_ticket_status: "update_ticket_status",
42
+ review_business_match: "review_business_match",
43
+ suggest_next_work: "suggest_next_work",
44
+ record_engineering_context: "record_engineering_context",
45
+ record_decision: "record_decision",
46
+ };
47
+ /** Normalize a rule's tool name: lowercase → alias → MCP passthrough. */
48
+ export function normalizeRuleToolName(raw) {
49
+ const lower = raw.toLowerCase();
50
+ const aliased = TOOL_NAME_ALIASES[lower];
51
+ if (aliased)
52
+ return aliased;
53
+ // MCP names are lowercase by construction (mcp/names.ts); no aliasing.
54
+ if (mcpInfoFromString(raw) !== null)
55
+ return raw;
56
+ // Unknown tool: keep the lowercased name so diagnostics can name it.
57
+ return lower;
58
+ }
59
+ /** Escape parens/backslashes in rule content for storage (parser inverse). */
60
+ export function escapeRuleContent(content) {
61
+ return content
62
+ .replace(/\\/g, "\\\\")
63
+ .replace(/\(/g, "\\(")
64
+ .replace(/\)/g, "\\)");
65
+ }
66
+ /** Unescape rule content after parsing. */
67
+ export function unescapeRuleContent(content) {
68
+ return content
69
+ .replace(/\\\(/g, "(")
70
+ .replace(/\\\)/g, ")")
71
+ .replace(/\\\\/g, "\\");
72
+ }
73
+ /** Index of the first occurrence of `ch` not escaped by an odd backslash run. */
74
+ function findFirstUnescapedChar(str, ch) {
75
+ for (let i = 0; i < str.length; i++) {
76
+ if (str[i] !== ch)
77
+ continue;
78
+ let backslashes = 0;
79
+ let j = i - 1;
80
+ while (j >= 0 && str[j] === "\\") {
81
+ backslashes++;
82
+ j--;
83
+ }
84
+ if (backslashes % 2 === 0)
85
+ return i;
86
+ }
87
+ return -1;
88
+ }
89
+ function findLastUnescapedChar(str, ch) {
90
+ for (let i = str.length - 1; i >= 0; i--) {
91
+ if (str[i] !== ch)
92
+ continue;
93
+ let backslashes = 0;
94
+ let j = i - 1;
95
+ while (j >= 0 && str[j] === "\\") {
96
+ backslashes++;
97
+ j--;
98
+ }
99
+ if (backslashes % 2 === 0)
100
+ return i;
101
+ }
102
+ return -1;
103
+ }
104
+ /**
105
+ * Parse a rule string into its components. Malformed shapes degrade the same
106
+ * way Claude Code's parser degrades them: treat the whole string as a bare
107
+ * tool name (never throw) — the loader's diagnostics flag them.
108
+ *
109
+ * `Bash(*)` and `Bash()` are tool-wide rules, same as bare `Bash`.
110
+ */
111
+ export function parseRuleString(ruleString) {
112
+ const openIdx = findFirstUnescapedChar(ruleString, "(");
113
+ if (openIdx === -1) {
114
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
115
+ }
116
+ const closeIdx = findLastUnescapedChar(ruleString, ")");
117
+ if (closeIdx === -1 || closeIdx <= openIdx || closeIdx !== ruleString.length - 1) {
118
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
119
+ }
120
+ const toolName = ruleString.slice(0, openIdx).trim();
121
+ if (!toolName) {
122
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
123
+ }
124
+ const rawContent = ruleString.slice(openIdx + 1, closeIdx);
125
+ if (rawContent === "" || rawContent === "*") {
126
+ return { toolName: normalizeRuleToolName(toolName) };
127
+ }
128
+ return { toolName: normalizeRuleToolName(toolName), ruleContent: unescapeRuleContent(rawContent) };
129
+ }
130
+ /** Serialize a parsed rule back to its string form (loader diagnostics). */
131
+ export function ruleValueToString(value) {
132
+ if (value.ruleContent === undefined)
133
+ return value.toolName;
134
+ return `${value.toolName}(${escapeRuleContent(value.ruleContent)})`;
135
+ }
136
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Path-based permission rules — ported from Claude Code's
3
+ * filesystem.ts (patternWithRoot + matchingRuleForInput), powered by the
4
+ * vendored `ignore` package (the gitignore engine) so pattern semantics are
5
+ * byte-compatible with both git's .gitignore and Claude Code.
6
+ *
7
+ * Pattern anchors (Claude Code parity):
8
+ * //path absolute from filesystem root
9
+ * ~/path relative to the user's home directory
10
+ * /path relative to the SETTINGS FILE's directory (user config →
11
+ * ~/.yagni-code/, project config → the project root)
12
+ * path relative to the session cwd; a bare filename matches at ANY
13
+ * depth (`.env` equals `**` / `.env` in glob terms)
14
+ *
15
+ * Compile semantics: all patterns for one (tool-class, behavior) compile
16
+ * into ONE ordered `ignore` instance — user-source patterns first, then
17
+ * project — so gitignore's later-line-wins and `!exception` negation work
18
+ * across rules from both files (Claude Code's matchingRuleForInput builds
19
+ * one ignore().add(patterns) per root the same way).
20
+ *
21
+ * Behavior asymmetry (Claude Code parity): allow rules with a rootless
22
+ * single-segment pattern anchor at the settings dir (narrow); deny/ask
23
+ * rules match at any depth (broad — the fail-safe direction).
24
+ */
25
+ import type { RuleSource } from "./loadConfig.js";
26
+ export interface PathMatchOptions {
27
+ cwd: string;
28
+ /** Overrides ~ expansion (tests); defaults to os.homedir(). */
29
+ homeDir?: string;
30
+ }
31
+ export type PathToolClass = "read" | "edit";
32
+ /** Which tools a rule's toolName maps to for path purposes. */
33
+ export declare function pathClassForTool(toolName: string): PathToolClass | null;
34
+ /** One path-pattern rule, resolved to a root + relative pattern. */
35
+ export interface ResolvedPathPattern {
36
+ relativePattern: string;
37
+ /** The directory the pattern resolves against; null = cwd-relative. */
38
+ root: string | null;
39
+ source: RuleSource;
40
+ }
41
+ /**
42
+ * Split an anchored pattern into (root, relativePattern). Mirrors Claude
43
+ * Code's patternWithRoot. Project-source `/` anchors at the project root
44
+ * (passed in); user-source `/` anchors at the YAGNI Code state home
45
+ * (~/.yagni-code).
46
+ */
47
+ export declare function resolvePatternRoot(pattern: string, source: RuleSource, opts: {
48
+ userStateHome: string;
49
+ projectRoot: string | null;
50
+ homeDir?: string;
51
+ }): ResolvedPathPattern;
52
+ /**
53
+ * Does the (absolute or cwd-relative) target path match the given patterns?
54
+ * Patterns are tried in order; later patterns win (gitignore semantics) —
55
+ * so the caller must pass user-source patterns before project-source.
56
+ */
57
+ export declare function pathMatches(patterns: readonly ResolvedPathPattern[], targetPath: string, opts: PathMatchOptions): boolean;
58
+ //# sourceMappingURL=pathRules.d.ts.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Path-based permission rules — ported from Claude Code's
3
+ * filesystem.ts (patternWithRoot + matchingRuleForInput), powered by the
4
+ * vendored `ignore` package (the gitignore engine) so pattern semantics are
5
+ * byte-compatible with both git's .gitignore and Claude Code.
6
+ *
7
+ * Pattern anchors (Claude Code parity):
8
+ * //path absolute from filesystem root
9
+ * ~/path relative to the user's home directory
10
+ * /path relative to the SETTINGS FILE's directory (user config →
11
+ * ~/.yagni-code/, project config → the project root)
12
+ * path relative to the session cwd; a bare filename matches at ANY
13
+ * depth (`.env` equals `**` / `.env` in glob terms)
14
+ *
15
+ * Compile semantics: all patterns for one (tool-class, behavior) compile
16
+ * into ONE ordered `ignore` instance — user-source patterns first, then
17
+ * project — so gitignore's later-line-wins and `!exception` negation work
18
+ * across rules from both files (Claude Code's matchingRuleForInput builds
19
+ * one ignore().add(patterns) per root the same way).
20
+ *
21
+ * Behavior asymmetry (Claude Code parity): allow rules with a rootless
22
+ * single-segment pattern anchor at the settings dir (narrow); deny/ask
23
+ * rules match at any depth (broad — the fail-safe direction).
24
+ */
25
+ import { homedir } from "node:os";
26
+ import { isAbsolute, posix, sep } from "node:path";
27
+ import ignore from "../vendor/ignore.js";
28
+ /** Which tools a rule's toolName maps to for path purposes. */
29
+ export function pathClassForTool(toolName) {
30
+ switch (toolName) {
31
+ case "read":
32
+ case "grep":
33
+ case "find":
34
+ case "ls":
35
+ return "read";
36
+ case "edit":
37
+ case "write":
38
+ return "edit";
39
+ default:
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Split an anchored pattern into (root, relativePattern). Mirrors Claude
45
+ * Code's patternWithRoot. Project-source `/` anchors at the project root
46
+ * (passed in); user-source `/` anchors at the YAGNI Code state home
47
+ * (~/.yagni-code).
48
+ */
49
+ export function resolvePatternRoot(pattern, source, opts) {
50
+ if (pattern.startsWith("//")) {
51
+ return { relativePattern: pattern.slice(1), root: "/", source };
52
+ }
53
+ if (pattern.startsWith("~/") || pattern === "~") {
54
+ const rel = pattern === "~" ? "" : pattern.slice(1);
55
+ return { relativePattern: rel, root: opts.homeDir ?? homedir(), source };
56
+ }
57
+ if (pattern.startsWith("/") && !pattern.startsWith("//")) {
58
+ const base = source === "project" ? opts.projectRoot : opts.userStateHome;
59
+ return { relativePattern: pattern, root: base ?? opts.userStateHome, source };
60
+ }
61
+ // Rootless: `./x` normalized to `x`; bare names keep any-depth semantics.
62
+ let normalized = pattern;
63
+ if (normalized.startsWith("./"))
64
+ normalized = normalized.slice(2);
65
+ return { relativePattern: normalized, root: null, source };
66
+ }
67
+ /** Strip a trailing `/**` — the ignore lib treats `path` as dir+children. */
68
+ function forIgnore(relativePattern) {
69
+ if (relativePattern.endsWith("/**"))
70
+ return relativePattern.slice(0, -3);
71
+ return relativePattern;
72
+ }
73
+ /**
74
+ * Does the (absolute or cwd-relative) target path match the given patterns?
75
+ * Patterns are tried in order; later patterns win (gitignore semantics) —
76
+ * so the caller must pass user-source patterns before project-source.
77
+ */
78
+ export function pathMatches(patterns, targetPath, opts) {
79
+ if (patterns.length === 0)
80
+ return false;
81
+ const target = toPosix(targetPath);
82
+ const groups = new Map();
83
+ for (const p of patterns) {
84
+ const key = p.root;
85
+ const list = groups.get(key) ?? [];
86
+ list.push(forIgnore(p.relativePattern));
87
+ groups.set(key, list);
88
+ }
89
+ for (const [root, plist] of groups) {
90
+ const ig = ignore().add(plist);
91
+ const base = root ?? opts.cwd;
92
+ // Lexical normalization BEFORE relativization: `a/../.env` and `a//b`
93
+ // must resolve against the same relative form the pattern matches, or a
94
+ // traversal-shaped spelling of a denied path slips past (probe:
95
+ // `/work/proj/../.env` was NOT caught by `Read(./.env)` without this).
96
+ // posix.normalize keeps this lexical — no filesystem access, so a
97
+ // symlinked target still needs its own rule (documented limitation,
98
+ // same as pi's own tools resolving paths literally).
99
+ const absTarget = isAbsolute(target) ? posix.normalize(target) : posix.normalize(posix.join(opts.cwd, target));
100
+ const rel = toPosix(posixRelative(base, absTarget));
101
+ if (rel.startsWith(".."))
102
+ continue; // outside this root — not matched
103
+ // ignore() rejects empty paths; an empty relative means the target IS
104
+ // the root itself (e.g. `rg` defaulting to "."). Nothing matches that
105
+ // unless a pattern covers "" — treat as not-matched (safe default: a
106
+ // cwd-wide read goes through the normal gate, not a deny).
107
+ if (rel === "" || rel === ".")
108
+ continue;
109
+ if (ig.ignores(rel))
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ function toPosix(p) {
115
+ return sep === "\\" ? p.replaceAll("\\", "/") : p;
116
+ }
117
+ function posixRelative(from, to) {
118
+ return posix.relative(toPosix(from), toPosix(to));
119
+ }
120
+ //# sourceMappingURL=pathRules.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Bash command-rule matching — ported from Claude Code's
3
+ * shellRuleMatching.ts + the matching half of BashTool/bashPermissions.ts.
4
+ *
5
+ * A bash rule's content is one of:
6
+ * - exact: `npm run build` — matches that exact command string
7
+ * - prefix: `npm install:*` — `:*` suffix = trailing wildcard
8
+ * - wildcard: `git log *` — `*` anywhere; `*` matches any text
9
+ * including spaces; `\*` literal
10
+ *
11
+ * Security invariants (Claude Code parity, enforced here):
12
+ * - compound commands are split on ;, &&, ||, |, and newlines, and EVERY
13
+ * subcommand must pass the rule check independently. Wildcards and
14
+ * prefixes never run against the raw compound string (`cd /x && rm -rf`
15
+ * must not ride a `cd:*` allow).
16
+ * - prefix matching requires a word boundary: `ls:*` matches `ls` and
17
+ * `ls -la` but never `lsof`.
18
+ * - a trailing ` *` wildcard that is the rule's ONLY wildcard also matches
19
+ * the bare command (`git log *` matches `git log`).
20
+ * - safe wrappers (timeout/time/nice/nohup) are stripped before matching,
21
+ * so `Bash(npm install:*)` matches `timeout 10 npm install foo`.
22
+ * - for DENY and ASK rules, ALL leading env-var assignments are stripped
23
+ * (fixed-point, mirroring Claude's stripAllLeadingEnvVars) so
24
+ * `FOO=bar denied_cmd` stays denied. For ALLOW rules only the safe-list
25
+ * env prefixes are stripped — an env var must never widen an allow
26
+ * (Claude's asymmetry, HackerOne #3543050).
27
+ */
28
+ export type ShellRuleShape = {
29
+ type: "exact";
30
+ command: string;
31
+ } | {
32
+ type: "prefix";
33
+ prefix: string;
34
+ } | {
35
+ type: "wildcard";
36
+ pattern: string;
37
+ };
38
+ export declare function parseShellRule(ruleContent: string): ShellRuleShape;
39
+ /** Match a command string against a `*`-wildcard pattern. */
40
+ export declare function matchWildcardPattern(pattern: string, command: string): boolean;
41
+ /** Split a command into its subcommand strings (operators are boundaries). */
42
+ export declare function splitSubcommands(command: string): string[];
43
+ /** Strip ALL leading env assignments — deny/ask rules only. Fixed-point. */
44
+ export declare function stripAllEnvVars(command: string): string;
45
+ /**
46
+ * Does this bash rule match the full command? Mirrors Claude Code's
47
+ * matchingRulesForInput: compound commands split, every subcommand must
48
+ * pass, candidates include stripped variants, and deny/ask additionally
49
+ * try all-env-stripped variants.
50
+ */
51
+ export declare function shellRuleMatches(ruleContent: string, command: string, behavior: "allow" | "deny" | "ask"): boolean;
52
+ //# sourceMappingURL=shellRules.d.ts.map