@yagni-app/code 1.0.6 → 1.0.8

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 (92) hide show
  1. package/README.md +91 -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/askUserQuestionTool.js +7 -2
  10. package/dist/extension/condensedTools.d.ts +12 -1
  11. package/dist/extension/condensedTools.js +17 -9
  12. package/dist/extension/config.d.ts +6 -0
  13. package/dist/extension/hooks.d.ts +3 -3
  14. package/dist/extension/hooks.js +30 -5
  15. package/dist/extension/index.d.ts +19 -0
  16. package/dist/extension/index.js +248 -43
  17. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  18. package/dist/extension/mcp/cliConfig.js +1 -1
  19. package/dist/extension/mcp/config.d.ts +24 -2
  20. package/dist/extension/mcp/config.js +75 -3
  21. package/dist/extension/mcp/manager.d.ts +3 -1
  22. package/dist/extension/mcp/manager.js +2 -2
  23. package/dist/extension/mcp/panel.d.ts +0 -1
  24. package/dist/extension/mcp/panel.js +13 -3
  25. package/dist/extension/mcp/startup.js +8 -6
  26. package/dist/extension/permission/gate.d.ts +101 -2
  27. package/dist/extension/permission/gate.js +485 -42
  28. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  29. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  30. package/dist/extension/permissionRules/engine.d.ts +50 -0
  31. package/dist/extension/permissionRules/engine.js +238 -0
  32. package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
  33. package/dist/extension/permissionRules/loadConfig.js +105 -0
  34. package/dist/extension/permissionRules/parser.d.ts +38 -0
  35. package/dist/extension/permissionRules/parser.js +136 -0
  36. package/dist/extension/permissionRules/pathRules.d.ts +60 -0
  37. package/dist/extension/permissionRules/pathRules.js +122 -0
  38. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  39. package/dist/extension/permissionRules/shellRules.js +221 -0
  40. package/dist/extension/pipeline/invocation.d.ts +3 -6
  41. package/dist/extension/pipeline/invocation.js +3 -6
  42. package/dist/extension/pipeline/runner.d.ts +0 -1
  43. package/dist/extension/pipeline/runner.js +6 -14
  44. package/dist/extension/plugins/inventory.d.ts +88 -0
  45. package/dist/extension/plugins/inventory.js +144 -0
  46. package/dist/extension/plugins/panel.d.ts +45 -0
  47. package/dist/extension/plugins/panel.js +293 -0
  48. package/dist/extension/sandbox/bash.d.ts +99 -0
  49. package/dist/extension/sandbox/bash.js +190 -0
  50. package/dist/extension/sandbox/config.d.ts +115 -0
  51. package/dist/extension/sandbox/config.js +388 -0
  52. package/dist/extension/sandbox/manager.d.ts +108 -0
  53. package/dist/extension/sandbox/manager.js +243 -0
  54. package/dist/extension/sandbox/panel.d.ts +111 -0
  55. package/dist/extension/sandbox/panel.js +342 -0
  56. package/dist/extension/sandbox/session.d.ts +85 -0
  57. package/dist/extension/sandbox/session.js +829 -0
  58. package/dist/extension/settingsFiles.d.ts +50 -0
  59. package/dist/extension/settingsFiles.js +206 -0
  60. package/dist/extension/telemetry/attrs.d.ts +96 -0
  61. package/dist/extension/telemetry/attrs.js +149 -0
  62. package/dist/extension/telemetry/config.d.ts +103 -0
  63. package/dist/extension/telemetry/config.js +193 -0
  64. package/dist/extension/telemetry/index.d.ts +7 -0
  65. package/dist/extension/telemetry/index.js +7 -0
  66. package/dist/extension/telemetry/probe.d.ts +29 -0
  67. package/dist/extension/telemetry/probe.js +122 -0
  68. package/dist/extension/telemetry/register.d.ts +47 -0
  69. package/dist/extension/telemetry/register.js +207 -0
  70. package/dist/extension/telemetry/sdk.d.ts +63 -0
  71. package/dist/extension/telemetry/sdk.js +207 -0
  72. package/dist/extension/telemetry/tracker.d.ts +131 -0
  73. package/dist/extension/telemetry/tracker.js +551 -0
  74. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  75. package/dist/extension/vendor/ignore.d.ts +86 -0
  76. package/dist/extension/vendor/ignore.js +788 -0
  77. package/dist/goHeadless.d.ts +1 -1
  78. package/dist/goHeadless.js +2 -2
  79. package/dist/launch.d.ts +4 -3
  80. package/dist/launch.js +7 -4
  81. package/dist/mcpCommand.d.ts +10 -1
  82. package/dist/mcpCommand.js +42 -10
  83. package/dist/otel.d.ts +67 -90
  84. package/dist/otel.js +152 -195
  85. package/dist/paths.d.ts +13 -0
  86. package/dist/paths.js +18 -0
  87. package/dist/pluginCommand.d.ts +43 -0
  88. package/dist/pluginCommand.js +499 -0
  89. package/dist/pluginStore.d.ts +170 -0
  90. package/dist/pluginStore.js +554 -0
  91. package/dist/upgrade.js +10 -1
  92. package/package.json +19 -3
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Permission-rule settings loader.
3
+ *
4
+ * Reads `permissions: { allow, deny, ask }` from the three YAGNI Code
5
+ * settings 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
+ * - local: .yagni-code/config.local.json — the personal per-project
12
+ * tier; trust-gated EXACTLY like project (a hostile repo can
13
+ * commit a local file — the gitignore convention only covers
14
+ * untracked files). Deliberately stricter than Claude Code,
15
+ * which does not trust-gate localSettings.
16
+ *
17
+ * Lists UNION across sources (Claude Code: lists merge, never replace).
18
+ * User-source rules are ordered before project-source rules, project before
19
+ * local, so path-pattern negation (`!exception`) behaves like gitignore
20
+ * lines across files — higher-precedence sources come later (later wins).
21
+ *
22
+ * Fail-soft: a malformed config file is skipped whole with a warning (same
23
+ * posture as hooks.ts). Malformed individual rule strings parse as bare
24
+ * tool names (parser degrades, never throws) and are flagged.
25
+ */
26
+ import { existsSync, readFileSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { codeStateHome } from "../stateHome.js";
29
+ import { parseRuleString } from "./parser.js";
30
+ /** The tools a rule can actually be consulted for at the gate. */
31
+ export const KNOWN_RULE_TOOLS = new Set([
32
+ "read", "edit", "write", "grep", "find", "ls", "bash", "web_fetch",
33
+ "ask_yagni", "ask_advisor", "ask_user_question", "file_ticket",
34
+ "update_ticket_status", "review_business_match", "suggest_next_work",
35
+ "record_engineering_context", "record_decision",
36
+ ]);
37
+ function readRulesFromFile(path, source, behavior, out, warnings) {
38
+ try {
39
+ if (!existsSync(path))
40
+ return;
41
+ const raw = readFileSync(path, "utf-8");
42
+ if (raw.trim() === "")
43
+ return;
44
+ const parsed = JSON.parse(raw);
45
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
46
+ warnings.push(`${source} settings: not a JSON object — permissions skipped (${path})`);
47
+ return;
48
+ }
49
+ const permissions = parsed.permissions;
50
+ if (permissions === undefined)
51
+ return;
52
+ if (!permissions || typeof permissions !== "object" || Array.isArray(permissions)) {
53
+ warnings.push(`${source} settings: "permissions" is not an object — skipped (${path})`);
54
+ return;
55
+ }
56
+ const entries = permissions[behavior];
57
+ if (entries === undefined)
58
+ return;
59
+ const list = Array.isArray(entries) ? entries : [entries];
60
+ for (const entry of list) {
61
+ if (typeof entry !== "string") {
62
+ warnings.push(`${source} settings: non-string ${behavior} rule skipped (${path})`);
63
+ continue;
64
+ }
65
+ const { toolName, ruleContent } = parseRuleString(entry);
66
+ out.push({ behavior, source, toolName, ruleContent, raw: entry });
67
+ }
68
+ }
69
+ catch {
70
+ warnings.push(`${source} settings: malformed JSON — permissions skipped (${path})`);
71
+ }
72
+ }
73
+ /** Load + union rules from all three files. Pure I/O; no throw. */
74
+ export function loadPermissionRules(opts = {}) {
75
+ const warnings = [];
76
+ const rules = [];
77
+ const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
78
+ const cwd = opts.cwd ?? process.cwd();
79
+ const userPath = join(stateHome, "config.json");
80
+ const projectPath = join(cwd, ".yagni-code", "config.json");
81
+ const localPath = join(cwd, ".yagni-code", "config.local.json");
82
+ // User, then project, then local — ordering matters for path-rule
83
+ // negation (later sources win, matching scalar precedence).
84
+ for (const behavior of ["deny", "ask", "allow"]) {
85
+ readRulesFromFile(userPath, "user", behavior, rules, warnings);
86
+ readRulesFromFile(projectPath, "project", behavior, rules, warnings);
87
+ readRulesFromFile(localPath, "local", behavior, rules, warnings);
88
+ }
89
+ const neverConsultedTools = [
90
+ ...new Set(rules.map((r) => r.toolName).filter((t) => !KNOWN_RULE_TOOLS.has(t) && !t.startsWith("mcp__"))),
91
+ ];
92
+ return { rules, diagnostics: { warnings, neverConsultedTools } };
93
+ }
94
+ /**
95
+ * Filter rules by behavior + trust. Untrusted repos keep deny/ask from every
96
+ * source but lose allow rules from BOTH project and local files — a
97
+ * committed config.local.json must not smuggle allow rules past the trust
98
+ * gate (see the module header; stricter than Claude Code's localSettings).
99
+ */
100
+ export function effectiveRules(rules, isProjectTrusted) {
101
+ if (isProjectTrusted)
102
+ return [...rules];
103
+ return rules.filter((r) => !(r.source !== "user" && r.behavior === "allow"));
104
+ }
105
+ //# 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,60 @@
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 AND local configs → the project root —
12
+ * local lives in the same .yagni-code dir as project)
13
+ * path relative to the session cwd; a bare filename matches at ANY
14
+ * depth (`.env` equals `**` / `.env` in glob terms)
15
+ *
16
+ * Compile semantics: all patterns for one (tool-class, behavior) compile
17
+ * into ONE ordered `ignore` instance — user-source patterns first, then
18
+ * project, then local — so gitignore's later-line-wins and `!exception`
19
+ * negation work across rules from all three files (Claude Code's
20
+ * matchingRuleForInput builds one ignore().add(patterns) per root the same
21
+ * way; local patterns are the last/highest-precedence "lines").
22
+ *
23
+ * Behavior asymmetry (Claude Code parity): allow rules with a rootless
24
+ * single-segment pattern anchor at the settings dir (narrow); deny/ask
25
+ * rules match at any depth (broad — the fail-safe direction).
26
+ */
27
+ import type { RuleSource } from "./loadConfig.js";
28
+ export interface PathMatchOptions {
29
+ cwd: string;
30
+ /** Overrides ~ expansion (tests); defaults to os.homedir(). */
31
+ homeDir?: string;
32
+ }
33
+ export type PathToolClass = "read" | "edit";
34
+ /** Which tools a rule's toolName maps to for path purposes. */
35
+ export declare function pathClassForTool(toolName: string): PathToolClass | null;
36
+ /** One path-pattern rule, resolved to a root + relative pattern. */
37
+ export interface ResolvedPathPattern {
38
+ relativePattern: string;
39
+ /** The directory the pattern resolves against; null = cwd-relative. */
40
+ root: string | null;
41
+ source: RuleSource;
42
+ }
43
+ /**
44
+ * Split an anchored pattern into (root, relativePattern). Mirrors Claude
45
+ * Code's patternWithRoot. Project- and local-source `/` anchors at the
46
+ * project root (passed in) — local shares project's .yagni-code dir;
47
+ * user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
48
+ */
49
+ export declare function resolvePatternRoot(pattern: string, source: RuleSource, opts: {
50
+ userStateHome: string;
51
+ projectRoot: string | null;
52
+ homeDir?: string;
53
+ }): ResolvedPathPattern;
54
+ /**
55
+ * Does the (absolute or cwd-relative) target path match the given patterns?
56
+ * Patterns are tried in order; later patterns win (gitignore semantics) —
57
+ * so the caller must pass user-source patterns before project-source.
58
+ */
59
+ export declare function pathMatches(patterns: readonly ResolvedPathPattern[], targetPath: string, opts: PathMatchOptions): boolean;
60
+ //# sourceMappingURL=pathRules.d.ts.map
@@ -0,0 +1,122 @@
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 AND local configs → the project root —
12
+ * local lives in the same .yagni-code dir as project)
13
+ * path relative to the session cwd; a bare filename matches at ANY
14
+ * depth (`.env` equals `**` / `.env` in glob terms)
15
+ *
16
+ * Compile semantics: all patterns for one (tool-class, behavior) compile
17
+ * into ONE ordered `ignore` instance — user-source patterns first, then
18
+ * project, then local — so gitignore's later-line-wins and `!exception`
19
+ * negation work across rules from all three files (Claude Code's
20
+ * matchingRuleForInput builds one ignore().add(patterns) per root the same
21
+ * way; local patterns are the last/highest-precedence "lines").
22
+ *
23
+ * Behavior asymmetry (Claude Code parity): allow rules with a rootless
24
+ * single-segment pattern anchor at the settings dir (narrow); deny/ask
25
+ * rules match at any depth (broad — the fail-safe direction).
26
+ */
27
+ import { homedir } from "node:os";
28
+ import { isAbsolute, posix, sep } from "node:path";
29
+ import ignore from "../vendor/ignore.js";
30
+ /** Which tools a rule's toolName maps to for path purposes. */
31
+ export function pathClassForTool(toolName) {
32
+ switch (toolName) {
33
+ case "read":
34
+ case "grep":
35
+ case "find":
36
+ case "ls":
37
+ return "read";
38
+ case "edit":
39
+ case "write":
40
+ return "edit";
41
+ default:
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Split an anchored pattern into (root, relativePattern). Mirrors Claude
47
+ * Code's patternWithRoot. Project- and local-source `/` anchors at the
48
+ * project root (passed in) — local shares project's .yagni-code dir;
49
+ * user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
50
+ */
51
+ export function resolvePatternRoot(pattern, source, opts) {
52
+ if (pattern.startsWith("//")) {
53
+ return { relativePattern: pattern.slice(1), root: "/", source };
54
+ }
55
+ if (pattern.startsWith("~/") || pattern === "~") {
56
+ const rel = pattern === "~" ? "" : pattern.slice(1);
57
+ return { relativePattern: rel, root: opts.homeDir ?? homedir(), source };
58
+ }
59
+ if (pattern.startsWith("/") && !pattern.startsWith("//")) {
60
+ const base = source === "user" ? opts.userStateHome : opts.projectRoot;
61
+ return { relativePattern: pattern, root: base ?? opts.userStateHome, source };
62
+ }
63
+ // Rootless: `./x` normalized to `x`; bare names keep any-depth semantics.
64
+ let normalized = pattern;
65
+ if (normalized.startsWith("./"))
66
+ normalized = normalized.slice(2);
67
+ return { relativePattern: normalized, root: null, source };
68
+ }
69
+ /** Strip a trailing `/**` — the ignore lib treats `path` as dir+children. */
70
+ function forIgnore(relativePattern) {
71
+ if (relativePattern.endsWith("/**"))
72
+ return relativePattern.slice(0, -3);
73
+ return relativePattern;
74
+ }
75
+ /**
76
+ * Does the (absolute or cwd-relative) target path match the given patterns?
77
+ * Patterns are tried in order; later patterns win (gitignore semantics) —
78
+ * so the caller must pass user-source patterns before project-source.
79
+ */
80
+ export function pathMatches(patterns, targetPath, opts) {
81
+ if (patterns.length === 0)
82
+ return false;
83
+ const target = toPosix(targetPath);
84
+ const groups = new Map();
85
+ for (const p of patterns) {
86
+ const key = p.root;
87
+ const list = groups.get(key) ?? [];
88
+ list.push(forIgnore(p.relativePattern));
89
+ groups.set(key, list);
90
+ }
91
+ for (const [root, plist] of groups) {
92
+ const ig = ignore().add(plist);
93
+ const base = root ?? opts.cwd;
94
+ // Lexical normalization BEFORE relativization: `a/../.env` and `a//b`
95
+ // must resolve against the same relative form the pattern matches, or a
96
+ // traversal-shaped spelling of a denied path slips past (probe:
97
+ // `/work/proj/../.env` was NOT caught by `Read(./.env)` without this).
98
+ // posix.normalize keeps this lexical — no filesystem access, so a
99
+ // symlinked target still needs its own rule (documented limitation,
100
+ // same as pi's own tools resolving paths literally).
101
+ const absTarget = isAbsolute(target) ? posix.normalize(target) : posix.normalize(posix.join(opts.cwd, target));
102
+ const rel = toPosix(posixRelative(base, absTarget));
103
+ if (rel.startsWith(".."))
104
+ continue; // outside this root — not matched
105
+ // ignore() rejects empty paths; an empty relative means the target IS
106
+ // the root itself (e.g. `rg` defaulting to "."). Nothing matches that
107
+ // unless a pattern covers "" — treat as not-matched (safe default: a
108
+ // cwd-wide read goes through the normal gate, not a deny).
109
+ if (rel === "" || rel === ".")
110
+ continue;
111
+ if (ig.ignores(rel))
112
+ return true;
113
+ }
114
+ return false;
115
+ }
116
+ function toPosix(p) {
117
+ return sep === "\\" ? p.replaceAll("\\", "/") : p;
118
+ }
119
+ function posixRelative(from, to) {
120
+ return posix.relative(toPosix(from), toPosix(to));
121
+ }
122
+ //# 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