@dyyz1993/pi-coding-agent 0.69.12 → 0.69.14

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 (76) hide show
  1. package/dist/core/agent-session.d.ts.map +1 -1
  2. package/dist/core/agent-session.js +13 -11
  3. package/dist/core/agent-session.js.map +1 -1
  4. package/dist/core/extensions/index.d.ts +1 -1
  5. package/dist/core/extensions/index.d.ts.map +1 -1
  6. package/dist/core/extensions/index.js.map +1 -1
  7. package/dist/core/extensions/runner.d.ts +5 -4
  8. package/dist/core/extensions/runner.d.ts.map +1 -1
  9. package/dist/core/extensions/runner.js +73 -25
  10. package/dist/core/extensions/runner.js.map +1 -1
  11. package/dist/core/extensions/server-channel.d.ts +27 -0
  12. package/dist/core/extensions/server-channel.d.ts.map +1 -0
  13. package/dist/core/extensions/server-channel.js +40 -0
  14. package/dist/core/extensions/server-channel.js.map +1 -0
  15. package/dist/core/extensions/types.d.ts +19 -29
  16. package/dist/core/extensions/types.d.ts.map +1 -1
  17. package/dist/core/extensions/types.js.map +1 -1
  18. package/dist/core/file-store/internal-git.d.ts +31 -0
  19. package/dist/core/file-store/internal-git.d.ts.map +1 -0
  20. package/dist/core/file-store/internal-git.js +176 -0
  21. package/dist/core/file-store/internal-git.js.map +1 -0
  22. package/dist/core/session-manager.d.ts +1 -0
  23. package/dist/core/session-manager.d.ts.map +1 -1
  24. package/dist/core/session-manager.js +7 -2
  25. package/dist/core/session-manager.js.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/dist/modes/interactive/interactive-mode.js +1 -0
  28. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/dist/modes/rpc/rpc-client.d.ts +25 -1
  30. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  31. package/dist/modes/rpc/rpc-client.js +26 -3
  32. package/dist/modes/rpc/rpc-client.js.map +1 -1
  33. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  34. package/dist/modes/rpc/rpc-mode.js +33 -2
  35. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  36. package/dist/modes/rpc/rpc-types.d.ts +22 -0
  37. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  38. package/dist/modes/rpc/rpc-types.js.map +1 -1
  39. package/dist/rules-engine/cache.d.ts +4 -0
  40. package/dist/rules-engine/cache.d.ts.map +1 -0
  41. package/dist/rules-engine/cache.js +32 -0
  42. package/dist/rules-engine/cache.js.map +1 -0
  43. package/dist/rules-engine/config.d.ts +8 -0
  44. package/dist/rules-engine/config.d.ts.map +1 -0
  45. package/dist/rules-engine/config.js +56 -0
  46. package/dist/rules-engine/config.js.map +1 -0
  47. package/dist/rules-engine/index.d.ts +10 -0
  48. package/dist/rules-engine/index.d.ts.map +1 -0
  49. package/dist/rules-engine/index.js +404 -0
  50. package/dist/rules-engine/index.js.map +1 -0
  51. package/dist/rules-engine/injector.d.ts +5 -0
  52. package/dist/rules-engine/injector.d.ts.map +1 -0
  53. package/dist/rules-engine/injector.js +57 -0
  54. package/dist/rules-engine/injector.js.map +1 -0
  55. package/dist/rules-engine/loader.d.ts +8 -0
  56. package/dist/rules-engine/loader.d.ts.map +1 -0
  57. package/dist/rules-engine/loader.js +190 -0
  58. package/dist/rules-engine/loader.js.map +1 -0
  59. package/dist/rules-engine/matcher.d.ts +3 -0
  60. package/dist/rules-engine/matcher.d.ts.map +1 -0
  61. package/dist/rules-engine/matcher.js +48 -0
  62. package/dist/rules-engine/matcher.js.map +1 -0
  63. package/dist/rules-engine/types.d.ts +150 -0
  64. package/dist/rules-engine/types.d.ts.map +1 -0
  65. package/dist/rules-engine/types.js +2 -0
  66. package/dist/rules-engine/types.js.map +1 -0
  67. package/docs/extensions.md +56 -56
  68. package/docs/file-rollback-design.md +287 -0
  69. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  70. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  71. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  72. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  73. package/examples/extensions/file-snapshot.ts +407 -0
  74. package/examples/extensions/with-deps/package-lock.json +2 -2
  75. package/examples/extensions/with-deps/package.json +1 -1
  76. package/package.json +4 -4
@@ -0,0 +1,190 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ function splitComma(val) {
4
+ const result = [];
5
+ let depth = 0;
6
+ let current = "";
7
+ for (const ch of val) {
8
+ if (ch === "{" || ch === "(" || ch === "[")
9
+ depth++;
10
+ else if (ch === "}" || ch === ")" || ch === "]")
11
+ depth--;
12
+ if (ch === "," && depth === 0) {
13
+ const trimmed = current.trim().replace(/^["']|["']$/g, "");
14
+ if (trimmed)
15
+ result.push(trimmed);
16
+ current = "";
17
+ }
18
+ else {
19
+ current += ch;
20
+ }
21
+ }
22
+ const trimmed = current.trim().replace(/^["']|["']$/g, "");
23
+ if (trimmed)
24
+ result.push(trimmed);
25
+ return result;
26
+ }
27
+ export function parseFrontmatter(content) {
28
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\n?---\r?\n([\s\S]*)$/;
29
+ const match = content.match(frontmatterRegex);
30
+ if (!match) {
31
+ return { data: {}, body: content };
32
+ }
33
+ const [, frontmatterStr, body] = match;
34
+ const data = {};
35
+ const lines = frontmatterStr.split("\n");
36
+ let i = 0;
37
+ while (i < lines.length) {
38
+ const line = lines[i];
39
+ const colonIndex = line.indexOf(":");
40
+ if (colonIndex === -1) {
41
+ i++;
42
+ continue;
43
+ }
44
+ const rawKey = line.slice(0, colonIndex).trim();
45
+ let value = line.slice(colonIndex + 1).trim();
46
+ if (value === "" || value === "null" || value === "undefined") {
47
+ const listItems = [];
48
+ let j = i + 1;
49
+ while (j < lines.length) {
50
+ const subLine = lines[j];
51
+ if (subLine.match(/^\s*-\s+/)) {
52
+ listItems.push(subLine
53
+ .replace(/^\s*-\s+/, "")
54
+ .trim()
55
+ .replace(/^["']|["']$/g, ""));
56
+ j++;
57
+ }
58
+ else if (subLine.trim() === "" || subLine.match(/^\s+/)) {
59
+ j++;
60
+ }
61
+ else {
62
+ break;
63
+ }
64
+ }
65
+ if (listItems.length > 0) {
66
+ const camelKey = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
67
+ data[camelKey] = listItems;
68
+ i = j;
69
+ continue;
70
+ }
71
+ value = null;
72
+ }
73
+ else if (value.startsWith("[") && value.endsWith("]")) {
74
+ try {
75
+ value = JSON.parse(value.replace(/'/g, '"'));
76
+ }
77
+ catch {
78
+ value = value
79
+ .slice(1, -1)
80
+ .split(",")
81
+ .map((v) => v.trim().replace(/^["']|["']$/g, ""));
82
+ }
83
+ }
84
+ else if (value.startsWith('"') && value.endsWith('"')) {
85
+ value = value.slice(1, -1);
86
+ }
87
+ else if (value.startsWith("'") && value.endsWith("'")) {
88
+ value = value.slice(1, -1);
89
+ }
90
+ const camelKey = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
91
+ if (camelKey === "paths" && typeof value === "string") {
92
+ data[camelKey] = splitComma(value);
93
+ }
94
+ else {
95
+ data[camelKey] = value;
96
+ }
97
+ i++;
98
+ }
99
+ return { data, body: body.trim() };
100
+ }
101
+ function extractTitle(body) {
102
+ for (const line of body.split("\n")) {
103
+ const trimmed = line.trim();
104
+ if (trimmed) {
105
+ return trimmed.replace(/^#+\s*/, "").replace(/\*\*/g, "");
106
+ }
107
+ }
108
+ return "Untitled Rule";
109
+ }
110
+ function parsePaths(raw) {
111
+ if (!raw)
112
+ return [];
113
+ if (typeof raw === "string") {
114
+ return raw
115
+ .split(",")
116
+ .map((g) => g.trim())
117
+ .filter(Boolean);
118
+ }
119
+ if (Array.isArray(raw))
120
+ return raw;
121
+ return [];
122
+ }
123
+ export function parseRuleFile(filePath, content) {
124
+ const { data, body } = parseFrontmatter(content);
125
+ const paths = parsePaths(data.paths);
126
+ const isUnconditional = paths.length === 0 || (paths.length === 1 && paths[0] === "**");
127
+ const frontmatter = {};
128
+ if (data.description && typeof data.description === "string")
129
+ frontmatter.description = data.description;
130
+ if (data.severity && typeof data.severity === "string")
131
+ frontmatter.severity = data.severity;
132
+ if (data.paths)
133
+ frontmatter.paths = paths;
134
+ if (data.allowedTools)
135
+ frontmatter.allowedTools =
136
+ typeof data.allowedTools === "string" ? [data.allowedTools] : data.allowedTools;
137
+ if (data.whenToUse && typeof data.whenToUse === "string")
138
+ frontmatter.whenToUse = data.whenToUse;
139
+ if (data.notifyOnMatch !== undefined)
140
+ frontmatter.notifyOnMatch = data.notifyOnMatch === "true" || data.notifyOnMatch === true;
141
+ if (data.skipInPrompt !== undefined)
142
+ frontmatter.skipInPrompt = data.skipInPrompt === "true" || data.skipInPrompt === true;
143
+ return {
144
+ name: path.basename(filePath, ".md"),
145
+ filePath,
146
+ title: extractTitle(body),
147
+ content: body.trim(),
148
+ scope: "project",
149
+ source: "",
150
+ frontmatter,
151
+ isUnconditional,
152
+ };
153
+ }
154
+ function scanDir(dir, files = []) {
155
+ if (!fs.existsSync(dir))
156
+ return files;
157
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
158
+ for (const entry of entries) {
159
+ const fullPath = path.join(dir, entry.name);
160
+ if (entry.isDirectory()) {
161
+ scanDir(fullPath, files);
162
+ }
163
+ else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdc"))) {
164
+ files.push(fullPath);
165
+ }
166
+ }
167
+ return files;
168
+ }
169
+ export function loadRules(rulesDir) {
170
+ const rules = [];
171
+ const unconditional = [];
172
+ const conditional = [];
173
+ if (!fs.existsSync(rulesDir)) {
174
+ return { rules, unconditional, conditional, loadedAt: Date.now() };
175
+ }
176
+ const files = scanDir(rulesDir);
177
+ for (const filePath of files) {
178
+ const content = fs.readFileSync(filePath, "utf-8");
179
+ const rule = parseRuleFile(filePath, content);
180
+ rules.push(rule);
181
+ if (rule.isUnconditional) {
182
+ unconditional.push(rule);
183
+ }
184
+ else {
185
+ conditional.push(rule);
186
+ }
187
+ }
188
+ return { rules, unconditional, conditional, loadedAt: Date.now() };
189
+ }
190
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/rules-engine/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,SAAS,UAAU,CAAC,GAAW,EAAY;IAC1C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACtB,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG;YAAE,KAAK,EAAE,CAAC;aAC/C,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG;YAAE,KAAK,EAAE,CAAC;QAEzD,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;YAC3D,IAAI,OAAO;gBAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAClC,OAAO,GAAG,EAAE,CAAC;QACd,CAAC;aAAM,CAAC;YACP,OAAO,IAAI,EAAE,CAAC;QACf,CAAC;IACF,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,OAAO;QAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,MAAM,CAAC;AAAA,CACd;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAmD;IAClG,MAAM,gBAAgB,GAAG,0CAA0C,CAAC;IACpE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAE9C,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;IACvC,MAAM,IAAI,GAA4B,EAAE,CAAC;IAEzC,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;YACvB,CAAC,EAAE,CAAC;YACJ,SAAS;QACV,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;QAChD,IAAI,KAAK,GAA6B,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAExE,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;YAC/D,MAAM,SAAS,GAAa,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACzB,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;oBAC/B,SAAS,CAAC,IAAI,CACb,OAAO;yBACL,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;yBACvB,IAAI,EAAE;yBACN,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAC7B,CAAC;oBACF,CAAC,EAAE,CAAC;gBACL,CAAC;qBAAM,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC3D,CAAC,EAAE,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACP,MAAM;gBACP,CAAC;YACF,CAAC;YACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;gBAClF,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;gBAC3B,CAAC,GAAG,CAAC,CAAC;gBACN,SAAS;YACV,CAAC;YACD,KAAK,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC;gBACJ,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;YAC9C,CAAC;YAAC,MAAM,CAAC;gBACR,KAAK,GAAI,KAAgB;qBACvB,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;qBACZ,KAAK,CAAC,GAAG,CAAC;qBACV,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC;YAC5D,CAAC;QACF,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAElF,IAAI,QAAQ,KAAK,OAAO,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvD,IAAI,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;QACxB,CAAC;QACD,CAAC,EAAE,CAAC;IACL,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AAAA,CACnC;AAED,SAAS,YAAY,CAAC,IAAY,EAAU;IAC3C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC3D,CAAC;IACF,CAAC;IACD,OAAO,eAAe,CAAC;AAAA,CACvB;AAED,SAAS,UAAU,CAAC,GAAY,EAAY;IAC3C,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,GAAG;aACR,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAe,CAAC;IAC/C,OAAO,EAAE,CAAC;AAAA,CACV;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAe,EAAc;IAC5E,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAExF,MAAM,WAAW,GAAoB,EAAE,CAAC;IACxC,IAAI,IAAI,CAAC,WAAW,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ;QAAE,WAAW,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;IACzG,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ;QACrD,WAAW,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAiD,CAAC;IAC/E,IAAI,IAAI,CAAC,KAAK;QAAE,WAAW,CAAC,KAAK,GAAG,KAAK,CAAC;IAC1C,IAAI,IAAI,CAAC,YAAY;QACpB,WAAW,CAAC,YAAY;YACvB,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,YAAyB,CAAC;IAChG,IAAI,IAAI,CAAC,SAAS,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;QAAE,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACjG,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS;QACnC,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,KAAK,MAAM,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC;IAC1F,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;QAClC,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,KAAK,MAAM,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC;IAEvF,OAAO;QACN,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;QACpC,QAAQ;QACR,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC;QACzB,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE;QACpB,KAAK,EAAE,SAAS;QAChB,MAAM,EAAE,EAAE;QACV,WAAW;QACX,eAAe;KACf,CAAC;AAAA,CACF;AAED,SAAS,OAAO,CAAC,GAAW,EAAE,KAAK,GAAa,EAAE,EAAY;IAC7D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC1F,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtB,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAa;IACtD,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,MAAM,aAAa,GAAiB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEhC,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACP,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;IACF,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAAA,CACnE","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { ParsedRule, RuleCache, RuleFrontmatter } from \"./types.js\";\n\nfunction splitComma(val: string): string[] {\n\tconst result: string[] = [];\n\tlet depth = 0;\n\tlet current = \"\";\n\tfor (const ch of val) {\n\t\tif (ch === \"{\" || ch === \"(\" || ch === \"[\") depth++;\n\t\telse if (ch === \"}\" || ch === \")\" || ch === \"]\") depth--;\n\n\t\tif (ch === \",\" && depth === 0) {\n\t\t\tconst trimmed = current.trim().replace(/^[\"']|[\"']$/g, \"\");\n\t\t\tif (trimmed) result.push(trimmed);\n\t\t\tcurrent = \"\";\n\t\t} else {\n\t\t\tcurrent += ch;\n\t\t}\n\t}\n\tconst trimmed = current.trim().replace(/^[\"']|[\"']$/g, \"\");\n\tif (trimmed) result.push(trimmed);\n\treturn result;\n}\n\nexport function parseFrontmatter(content: string): { data: Record<string, unknown>; body: string } {\n\tconst frontmatterRegex = /^---\\r?\\n([\\s\\S]*?)\\n?---\\r?\\n([\\s\\S]*)$/;\n\tconst match = content.match(frontmatterRegex);\n\n\tif (!match) {\n\t\treturn { data: {}, body: content };\n\t}\n\n\tconst [, frontmatterStr, body] = match;\n\tconst data: Record<string, unknown> = {};\n\n\tconst lines = frontmatterStr.split(\"\\n\");\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tconst colonIndex = line.indexOf(\":\");\n\t\tif (colonIndex === -1) {\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst rawKey = line.slice(0, colonIndex).trim();\n\t\tlet value: string | string[] | null = line.slice(colonIndex + 1).trim();\n\n\t\tif (value === \"\" || value === \"null\" || value === \"undefined\") {\n\t\t\tconst listItems: string[] = [];\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < lines.length) {\n\t\t\t\tconst subLine = lines[j];\n\t\t\t\tif (subLine.match(/^\\s*-\\s+/)) {\n\t\t\t\t\tlistItems.push(\n\t\t\t\t\t\tsubLine\n\t\t\t\t\t\t\t.replace(/^\\s*-\\s+/, \"\")\n\t\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t\t.replace(/^[\"']|[\"']$/g, \"\"),\n\t\t\t\t\t);\n\t\t\t\t\tj++;\n\t\t\t\t} else if (subLine.trim() === \"\" || subLine.match(/^\\s+/)) {\n\t\t\t\t\tj++;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (listItems.length > 0) {\n\t\t\t\tconst camelKey = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());\n\t\t\t\tdata[camelKey] = listItems;\n\t\t\t\ti = j;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvalue = null;\n\t\t} else if (value.startsWith(\"[\") && value.endsWith(\"]\")) {\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(value.replace(/'/g, '\"'));\n\t\t\t} catch {\n\t\t\t\tvalue = (value as string)\n\t\t\t\t\t.slice(1, -1)\n\t\t\t\t\t.split(\",\")\n\t\t\t\t\t.map((v: string) => v.trim().replace(/^[\"']|[\"']$/g, \"\"));\n\t\t\t}\n\t\t} else if (value.startsWith('\"') && value.endsWith('\"')) {\n\t\t\tvalue = value.slice(1, -1);\n\t\t} else if (value.startsWith(\"'\") && value.endsWith(\"'\")) {\n\t\t\tvalue = value.slice(1, -1);\n\t\t}\n\n\t\tconst camelKey = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());\n\n\t\tif (camelKey === \"paths\" && typeof value === \"string\") {\n\t\t\tdata[camelKey] = splitComma(value);\n\t\t} else {\n\t\t\tdata[camelKey] = value;\n\t\t}\n\t\ti++;\n\t}\n\n\treturn { data, body: body.trim() };\n}\n\nfunction extractTitle(body: string): string {\n\tfor (const line of body.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed) {\n\t\t\treturn trimmed.replace(/^#+\\s*/, \"\").replace(/\\*\\*/g, \"\");\n\t\t}\n\t}\n\treturn \"Untitled Rule\";\n}\n\nfunction parsePaths(raw: unknown): string[] {\n\tif (!raw) return [];\n\tif (typeof raw === \"string\") {\n\t\treturn raw\n\t\t\t.split(\",\")\n\t\t\t.map((g) => g.trim())\n\t\t\t.filter(Boolean);\n\t}\n\tif (Array.isArray(raw)) return raw as string[];\n\treturn [];\n}\n\nexport function parseRuleFile(filePath: string, content: string): ParsedRule {\n\tconst { data, body } = parseFrontmatter(content);\n\tconst paths = parsePaths(data.paths);\n\tconst isUnconditional = paths.length === 0 || (paths.length === 1 && paths[0] === \"**\");\n\n\tconst frontmatter: RuleFrontmatter = {};\n\tif (data.description && typeof data.description === \"string\") frontmatter.description = data.description;\n\tif (data.severity && typeof data.severity === \"string\")\n\t\tfrontmatter.severity = data.severity as ParsedRule[\"frontmatter\"][\"severity\"];\n\tif (data.paths) frontmatter.paths = paths;\n\tif (data.allowedTools)\n\t\tfrontmatter.allowedTools =\n\t\t\ttypeof data.allowedTools === \"string\" ? [data.allowedTools] : (data.allowedTools as string[]);\n\tif (data.whenToUse && typeof data.whenToUse === \"string\") frontmatter.whenToUse = data.whenToUse;\n\tif (data.notifyOnMatch !== undefined)\n\t\tfrontmatter.notifyOnMatch = data.notifyOnMatch === \"true\" || data.notifyOnMatch === true;\n\tif (data.skipInPrompt !== undefined)\n\t\tfrontmatter.skipInPrompt = data.skipInPrompt === \"true\" || data.skipInPrompt === true;\n\n\treturn {\n\t\tname: path.basename(filePath, \".md\"),\n\t\tfilePath,\n\t\ttitle: extractTitle(body),\n\t\tcontent: body.trim(),\n\t\tscope: \"project\",\n\t\tsource: \"\",\n\t\tfrontmatter,\n\t\tisUnconditional,\n\t};\n}\n\nfunction scanDir(dir: string, files: string[] = []): string[] {\n\tif (!fs.existsSync(dir)) return files;\n\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\tfor (const entry of entries) {\n\t\tconst fullPath = path.join(dir, entry.name);\n\t\tif (entry.isDirectory()) {\n\t\t\tscanDir(fullPath, files);\n\t\t} else if (entry.isFile() && (entry.name.endsWith(\".md\") || entry.name.endsWith(\".mdc\"))) {\n\t\t\tfiles.push(fullPath);\n\t\t}\n\t}\n\treturn files;\n}\n\nexport function loadRules(rulesDir: string): RuleCache {\n\tconst rules: ParsedRule[] = [];\n\tconst unconditional: ParsedRule[] = [];\n\tconst conditional: ParsedRule[] = [];\n\n\tif (!fs.existsSync(rulesDir)) {\n\t\treturn { rules, unconditional, conditional, loadedAt: Date.now() };\n\t}\n\n\tconst files = scanDir(rulesDir);\n\n\tfor (const filePath of files) {\n\t\tconst content = fs.readFileSync(filePath, \"utf-8\");\n\t\tconst rule = parseRuleFile(filePath, content);\n\t\trules.push(rule);\n\t\tif (rule.isUnconditional) {\n\t\t\tunconditional.push(rule);\n\t\t} else {\n\t\t\tconditional.push(rule);\n\t\t}\n\t}\n\n\treturn { rules, unconditional, conditional, loadedAt: Date.now() };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function matchGlob(pattern: string, target: string): boolean;
2
+ export declare function matchesAnyGlob(globs: string[], filePath: string): boolean;
3
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/rules-engine/matcher.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAoClE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIzE","sourcesContent":["export function matchGlob(pattern: string, target: string): boolean {\n\tif (!pattern || !target) return false;\n\n\tconst normalizedTarget = target.replace(/\\\\/g, \"/\");\n\tlet i = 0;\n\tlet re = \"\";\n\n\twhile (i < pattern.length) {\n\t\tconst ch = pattern[i];\n\t\tif (ch === \"*\") {\n\t\t\tif (pattern[i + 1] === \"*\") {\n\t\t\t\tre += \".*\";\n\t\t\t\ti += 2;\n\t\t\t\tif (pattern[i] === \"/\") i++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tre += \"[^/]*\";\n\t\t} else if (ch === \"?\") {\n\t\t\tre += \"[^/]\";\n\t\t} else if (ch === \".\") {\n\t\t\tre += \"\\\\.\";\n\t\t} else if (ch === \"{\") {\n\t\t\tconst close = pattern.indexOf(\"}\", i);\n\t\t\tif (close !== -1) {\n\t\t\t\tre += `(${pattern.slice(i + 1, close).replace(/,/g, \"|\")})`;\n\t\t\t\ti = close;\n\t\t\t} else {\n\t\t\t\tre += ch;\n\t\t\t}\n\t\t} else {\n\t\t\tre += ch;\n\t\t}\n\t\ti++;\n\t}\n\n\treturn new RegExp(`^${re}$`).test(normalizedTarget);\n}\n\nexport function matchesAnyGlob(globs: string[], filePath: string): boolean {\n\tif (!filePath || globs.length === 0) return false;\n\tconst normalized = filePath.replace(/\\\\/g, \"/\");\n\treturn globs.some((g) => matchGlob(g, normalized));\n}\n"]}
@@ -0,0 +1,48 @@
1
+ export function matchGlob(pattern, target) {
2
+ if (!pattern || !target)
3
+ return false;
4
+ const normalizedTarget = target.replace(/\\/g, "/");
5
+ let i = 0;
6
+ let re = "";
7
+ while (i < pattern.length) {
8
+ const ch = pattern[i];
9
+ if (ch === "*") {
10
+ if (pattern[i + 1] === "*") {
11
+ re += ".*";
12
+ i += 2;
13
+ if (pattern[i] === "/")
14
+ i++;
15
+ continue;
16
+ }
17
+ re += "[^/]*";
18
+ }
19
+ else if (ch === "?") {
20
+ re += "[^/]";
21
+ }
22
+ else if (ch === ".") {
23
+ re += "\\.";
24
+ }
25
+ else if (ch === "{") {
26
+ const close = pattern.indexOf("}", i);
27
+ if (close !== -1) {
28
+ re += `(${pattern.slice(i + 1, close).replace(/,/g, "|")})`;
29
+ i = close;
30
+ }
31
+ else {
32
+ re += ch;
33
+ }
34
+ }
35
+ else {
36
+ re += ch;
37
+ }
38
+ i++;
39
+ }
40
+ return new RegExp(`^${re}$`).test(normalizedTarget);
41
+ }
42
+ export function matchesAnyGlob(globs, filePath) {
43
+ if (!filePath || globs.length === 0)
44
+ return false;
45
+ const normalized = filePath.replace(/\\/g, "/");
46
+ return globs.some((g) => matchGlob(g, normalized));
47
+ }
48
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/rules-engine/matcher.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,MAAc,EAAW;IACnE,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAEtC,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,EAAE,GAAG,EAAE,CAAC;IAEZ,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAChB,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC5B,EAAE,IAAI,IAAI,CAAC;gBACX,CAAC,IAAI,CAAC,CAAC;gBACP,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG;oBAAE,CAAC,EAAE,CAAC;gBAC5B,SAAS;YACV,CAAC;YACD,EAAE,IAAI,OAAO,CAAC;QACf,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACvB,EAAE,IAAI,MAAM,CAAC;QACd,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACvB,EAAE,IAAI,KAAK,CAAC;QACb,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACtC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBAClB,EAAE,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC;gBAC5D,CAAC,GAAG,KAAK,CAAC;YACX,CAAC;iBAAM,CAAC;gBACP,EAAE,IAAI,EAAE,CAAC;YACV,CAAC;QACF,CAAC;aAAM,CAAC;YACP,EAAE,IAAI,EAAE,CAAC;QACV,CAAC;QACD,CAAC,EAAE,CAAC;IACL,CAAC;IAED,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;AAAA,CACpD;AAED,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,QAAgB,EAAW;IAC1E,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;AAAA,CACnD","sourcesContent":["export function matchGlob(pattern: string, target: string): boolean {\n\tif (!pattern || !target) return false;\n\n\tconst normalizedTarget = target.replace(/\\\\/g, \"/\");\n\tlet i = 0;\n\tlet re = \"\";\n\n\twhile (i < pattern.length) {\n\t\tconst ch = pattern[i];\n\t\tif (ch === \"*\") {\n\t\t\tif (pattern[i + 1] === \"*\") {\n\t\t\t\tre += \".*\";\n\t\t\t\ti += 2;\n\t\t\t\tif (pattern[i] === \"/\") i++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tre += \"[^/]*\";\n\t\t} else if (ch === \"?\") {\n\t\t\tre += \"[^/]\";\n\t\t} else if (ch === \".\") {\n\t\t\tre += \"\\\\.\";\n\t\t} else if (ch === \"{\") {\n\t\t\tconst close = pattern.indexOf(\"}\", i);\n\t\t\tif (close !== -1) {\n\t\t\t\tre += `(${pattern.slice(i + 1, close).replace(/,/g, \"|\")})`;\n\t\t\t\ti = close;\n\t\t\t} else {\n\t\t\t\tre += ch;\n\t\t\t}\n\t\t} else {\n\t\t\tre += ch;\n\t\t}\n\t\ti++;\n\t}\n\n\treturn new RegExp(`^${re}$`).test(normalizedTarget);\n}\n\nexport function matchesAnyGlob(globs: string[], filePath: string): boolean {\n\tif (!filePath || globs.length === 0) return false;\n\tconst normalized = filePath.replace(/\\\\/g, \"/\");\n\treturn globs.some((g) => matchGlob(g, normalized));\n}\n"]}
@@ -0,0 +1,150 @@
1
+ export type RuleSeverity = "critical" | "high" | "medium" | "low" | "hint";
2
+ export type RuleScope = "user" | "pi" | "project" | "managed";
3
+ export interface RuleFrontmatter {
4
+ paths?: string[];
5
+ description?: string;
6
+ severity?: RuleSeverity;
7
+ allowedTools?: string[];
8
+ whenToUse?: string;
9
+ version?: string;
10
+ model?: string;
11
+ skills?: string;
12
+ effort?: string;
13
+ userInvocable?: string;
14
+ context?: "inline" | "fork";
15
+ agent?: string;
16
+ shell?: string;
17
+ notifyOnMatch?: boolean;
18
+ skipInPrompt?: boolean;
19
+ }
20
+ export interface ParsedRule {
21
+ name: string;
22
+ filePath: string;
23
+ title: string;
24
+ content: string;
25
+ scope: RuleScope;
26
+ source: string;
27
+ frontmatter: RuleFrontmatter;
28
+ isUnconditional: boolean;
29
+ }
30
+ export interface RuleCache {
31
+ rules: ParsedRule[];
32
+ unconditional: ParsedRule[];
33
+ conditional: ParsedRule[];
34
+ loadedAt: number;
35
+ }
36
+ export interface CachedRules {
37
+ rules: ParsedRule[];
38
+ loadedAt: number;
39
+ }
40
+ export interface RulesConfig {
41
+ cacheTTL: number;
42
+ notifyOnLoad: boolean;
43
+ notifyOnMatch: boolean;
44
+ dirs?: {
45
+ user?: string[];
46
+ pi?: string[];
47
+ project?: string[];
48
+ managed?: string[];
49
+ };
50
+ }
51
+ export interface RuleDetail {
52
+ name: string;
53
+ title: string;
54
+ filePath: string;
55
+ scope: RuleScope;
56
+ source: string;
57
+ severity: RuleSeverity;
58
+ isUnconditional: boolean;
59
+ paths: string[];
60
+ description?: string;
61
+ }
62
+ export interface ScannedDir {
63
+ dir: string;
64
+ fileCount: number;
65
+ ruleNames: string[];
66
+ }
67
+ export interface MatchedRuleDetail {
68
+ name: string;
69
+ title: string;
70
+ severity: RuleSeverity;
71
+ matchedGlob: string;
72
+ }
73
+ export interface MatchRecord {
74
+ filePath: string;
75
+ ruleNames: string[];
76
+ toolName: string;
77
+ toolCallId: string;
78
+ severity: "info" | "warning";
79
+ timestamp: number;
80
+ matchedRuleDetails?: MatchedRuleDetail[];
81
+ }
82
+ export interface LifecycleEntry {
83
+ event: "loaded" | "restored" | "injected" | "reloaded" | "unloaded" | "expired";
84
+ message: string;
85
+ ruleCount?: number;
86
+ timestamp: number;
87
+ details?: {
88
+ scannedDirs?: ScannedDir[];
89
+ configSource?: string;
90
+ cacheHit?: boolean;
91
+ injectedRules?: Array<{
92
+ name: string;
93
+ promptDelta: number;
94
+ }>;
95
+ };
96
+ }
97
+ export interface SnapshotPayload {
98
+ type: "snapshot";
99
+ rules: RuleDetail[];
100
+ injectedRuleNames: string[];
101
+ totalRules: number;
102
+ unconditionalCount: number;
103
+ conditionalCount: number;
104
+ matchHistory: MatchRecord[];
105
+ lifecycleLog: LifecycleEntry[];
106
+ loadedAt: number;
107
+ cacheTTL: number;
108
+ }
109
+ export interface MatchedPayload {
110
+ type: "matched";
111
+ filePath: string;
112
+ matchedRules: MatchedRuleDetail[];
113
+ toolName: string;
114
+ toolCallId: string;
115
+ severity: "info" | "warning";
116
+ timestamp: number;
117
+ }
118
+ export interface InjectedPayload {
119
+ type: "injected";
120
+ ruleNames: string[];
121
+ systemPromptLength: number;
122
+ }
123
+ export interface ReloadedPayload {
124
+ type: "reloaded";
125
+ rules: RuleDetail[];
126
+ loadedAt: number;
127
+ }
128
+ export interface UnloadedPayload {
129
+ type: "unloaded";
130
+ reason: string;
131
+ }
132
+ export type RulesChannelEvent = SnapshotPayload | MatchedPayload | InjectedPayload | ReloadedPayload | UnloadedPayload;
133
+ export interface RulesChannelContract {
134
+ methods: {
135
+ getSnapshot: {
136
+ params: {
137
+ cwd?: string;
138
+ };
139
+ return: SnapshotPayload;
140
+ };
141
+ };
142
+ events: {
143
+ snapshot: SnapshotPayload;
144
+ matched: MatchedPayload;
145
+ injected: InjectedPayload;
146
+ reloaded: ReloadedPayload;
147
+ unloaded: UnloadedPayload;
148
+ };
149
+ }
150
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rules-engine/types.ts"],"names":[],"mappings":"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","sourcesContent":["export type RuleSeverity = \"critical\" | \"high\" | \"medium\" | \"low\" | \"hint\";\n\nexport type RuleScope = \"user\" | \"pi\" | \"project\" | \"managed\";\n\nexport interface RuleFrontmatter {\n\tpaths?: string[];\n\tdescription?: string;\n\tseverity?: RuleSeverity;\n\tallowedTools?: string[];\n\twhenToUse?: string;\n\tversion?: string;\n\tmodel?: string;\n\tskills?: string;\n\teffort?: string;\n\tuserInvocable?: string;\n\tcontext?: \"inline\" | \"fork\";\n\tagent?: string;\n\tshell?: string;\n\tnotifyOnMatch?: boolean;\n\tskipInPrompt?: boolean;\n}\n\nexport interface ParsedRule {\n\tname: string;\n\tfilePath: string;\n\ttitle: string;\n\tcontent: string;\n\tscope: RuleScope;\n\tsource: string;\n\tfrontmatter: RuleFrontmatter;\n\tisUnconditional: boolean;\n}\n\nexport interface RuleCache {\n\trules: ParsedRule[];\n\tunconditional: ParsedRule[];\n\tconditional: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface CachedRules {\n\trules: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface RulesConfig {\n\tcacheTTL: number;\n\tnotifyOnLoad: boolean;\n\tnotifyOnMatch: boolean;\n\tdirs?: {\n\t\tuser?: string[];\n\t\tpi?: string[];\n\t\tproject?: string[];\n\t\tmanaged?: string[];\n\t};\n}\n\nexport interface RuleDetail {\n\tname: string;\n\ttitle: string;\n\tfilePath: string;\n\tscope: RuleScope;\n\tsource: string;\n\tseverity: RuleSeverity;\n\tisUnconditional: boolean;\n\tpaths: string[];\n\tdescription?: string;\n}\n\nexport interface ScannedDir {\n\tdir: string;\n\tfileCount: number;\n\truleNames: string[];\n}\n\nexport interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n}\n\nexport interface MatchRecord {\n\tfilePath: string;\n\truleNames: string[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\tmatchedRuleDetails?: MatchedRuleDetail[];\n}\n\nexport interface LifecycleEntry {\n\tevent: \"loaded\" | \"restored\" | \"injected\" | \"reloaded\" | \"unloaded\" | \"expired\";\n\tmessage: string;\n\truleCount?: number;\n\ttimestamp: number;\n\tdetails?: {\n\t\tscannedDirs?: ScannedDir[];\n\t\tconfigSource?: string;\n\t\tcacheHit?: boolean;\n\t\tinjectedRules?: Array<{ name: string; promptDelta: number }>;\n\t};\n}\n\nexport interface SnapshotPayload {\n\ttype: \"snapshot\";\n\trules: RuleDetail[];\n\tinjectedRuleNames: string[];\n\ttotalRules: number;\n\tunconditionalCount: number;\n\tconditionalCount: number;\n\tmatchHistory: MatchRecord[];\n\tlifecycleLog: LifecycleEntry[];\n\tloadedAt: number;\n\tcacheTTL: number;\n}\n\nexport interface MatchedPayload {\n\ttype: \"matched\";\n\tfilePath: string;\n\tmatchedRules: MatchedRuleDetail[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n}\n\nexport interface InjectedPayload {\n\ttype: \"injected\";\n\truleNames: string[];\n\tsystemPromptLength: number;\n}\n\nexport interface ReloadedPayload {\n\ttype: \"reloaded\";\n\trules: RuleDetail[];\n\tloadedAt: number;\n}\n\nexport interface UnloadedPayload {\n\ttype: \"unloaded\";\n\treason: string;\n}\n\nexport type RulesChannelEvent = SnapshotPayload | MatchedPayload | InjectedPayload | ReloadedPayload | UnloadedPayload;\n\nexport interface RulesChannelContract {\n\tmethods: {\n\t\tgetSnapshot: {\n\t\t\tparams: { cwd?: string };\n\t\t\treturn: SnapshotPayload;\n\t\t};\n\t};\n\tevents: {\n\t\tsnapshot: SnapshotPayload;\n\t\tmatched: MatchedPayload;\n\t\tinjected: InjectedPayload;\n\t\treloaded: ReloadedPayload;\n\t\tunloaded: UnloadedPayload;\n\t};\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/rules-engine/types.ts"],"names":[],"mappings":"","sourcesContent":["export type RuleSeverity = \"critical\" | \"high\" | \"medium\" | \"low\" | \"hint\";\n\nexport type RuleScope = \"user\" | \"pi\" | \"project\" | \"managed\";\n\nexport interface RuleFrontmatter {\n\tpaths?: string[];\n\tdescription?: string;\n\tseverity?: RuleSeverity;\n\tallowedTools?: string[];\n\twhenToUse?: string;\n\tversion?: string;\n\tmodel?: string;\n\tskills?: string;\n\teffort?: string;\n\tuserInvocable?: string;\n\tcontext?: \"inline\" | \"fork\";\n\tagent?: string;\n\tshell?: string;\n\tnotifyOnMatch?: boolean;\n\tskipInPrompt?: boolean;\n}\n\nexport interface ParsedRule {\n\tname: string;\n\tfilePath: string;\n\ttitle: string;\n\tcontent: string;\n\tscope: RuleScope;\n\tsource: string;\n\tfrontmatter: RuleFrontmatter;\n\tisUnconditional: boolean;\n}\n\nexport interface RuleCache {\n\trules: ParsedRule[];\n\tunconditional: ParsedRule[];\n\tconditional: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface CachedRules {\n\trules: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface RulesConfig {\n\tcacheTTL: number;\n\tnotifyOnLoad: boolean;\n\tnotifyOnMatch: boolean;\n\tdirs?: {\n\t\tuser?: string[];\n\t\tpi?: string[];\n\t\tproject?: string[];\n\t\tmanaged?: string[];\n\t};\n}\n\nexport interface RuleDetail {\n\tname: string;\n\ttitle: string;\n\tfilePath: string;\n\tscope: RuleScope;\n\tsource: string;\n\tseverity: RuleSeverity;\n\tisUnconditional: boolean;\n\tpaths: string[];\n\tdescription?: string;\n}\n\nexport interface ScannedDir {\n\tdir: string;\n\tfileCount: number;\n\truleNames: string[];\n}\n\nexport interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n}\n\nexport interface MatchRecord {\n\tfilePath: string;\n\truleNames: string[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\tmatchedRuleDetails?: MatchedRuleDetail[];\n}\n\nexport interface LifecycleEntry {\n\tevent: \"loaded\" | \"restored\" | \"injected\" | \"reloaded\" | \"unloaded\" | \"expired\";\n\tmessage: string;\n\truleCount?: number;\n\ttimestamp: number;\n\tdetails?: {\n\t\tscannedDirs?: ScannedDir[];\n\t\tconfigSource?: string;\n\t\tcacheHit?: boolean;\n\t\tinjectedRules?: Array<{ name: string; promptDelta: number }>;\n\t};\n}\n\nexport interface SnapshotPayload {\n\ttype: \"snapshot\";\n\trules: RuleDetail[];\n\tinjectedRuleNames: string[];\n\ttotalRules: number;\n\tunconditionalCount: number;\n\tconditionalCount: number;\n\tmatchHistory: MatchRecord[];\n\tlifecycleLog: LifecycleEntry[];\n\tloadedAt: number;\n\tcacheTTL: number;\n}\n\nexport interface MatchedPayload {\n\ttype: \"matched\";\n\tfilePath: string;\n\tmatchedRules: MatchedRuleDetail[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n}\n\nexport interface InjectedPayload {\n\ttype: \"injected\";\n\truleNames: string[];\n\tsystemPromptLength: number;\n}\n\nexport interface ReloadedPayload {\n\ttype: \"reloaded\";\n\trules: RuleDetail[];\n\tloadedAt: number;\n}\n\nexport interface UnloadedPayload {\n\ttype: \"unloaded\";\n\treason: string;\n}\n\nexport type RulesChannelEvent = SnapshotPayload | MatchedPayload | InjectedPayload | ReloadedPayload | UnloadedPayload;\n\nexport interface RulesChannelContract {\n\tmethods: {\n\t\tgetSnapshot: {\n\t\t\tparams: { cwd?: string };\n\t\t\treturn: SnapshotPayload;\n\t\t};\n\t};\n\tevents: {\n\t\tsnapshot: SnapshotPayload;\n\t\tmatched: MatchedPayload;\n\t\tinjected: InjectedPayload;\n\t\treloaded: ReloadedPayload;\n\t\tunloaded: UnloadedPayload;\n\t};\n}\n"]}
@@ -832,89 +832,89 @@ Transforms chain across handlers. See [input-transform.ts](../examples/extension
832
832
 
833
833
  ### UI Interception Events
834
834
 
835
- Intercept `ctx.ui.confirm()`, `ctx.ui.select()`, and `ctx.ui.input()` calls from **any extension** or the agent itself. This allows a single extension to take over all UI dialogs -- for example, to forward permission requests to a remote service and wait for a response.
835
+ Intercept `ctx.ui.confirm()`, `ctx.ui.select()`, and `ctx.ui.input()` calls from **any extension** or the agent itself. This allows a single extension to observe and respond to all UI dialogs -- for example, to forward permission requests to a remote service and wait for a response.
836
836
 
837
837
  Without UI interception, each extension calls `ctx.ui.confirm()` independently and the results are invisible to other extensions. With UI interception, one extension can observe and respond to all UI dialogs on behalf of the user.
838
838
 
839
- #### ui_confirm
839
+ #### ui
840
840
 
841
- Fired when any code calls `ctx.ui.confirm()`. Return `{ action: "responded", confirmed }` to answer the dialog without showing it to the user. Return `undefined` to fall through to the original UI.
841
+ Fired when any code calls `ctx.ui.confirm()`, `ctx.ui.select()`, or `ctx.ui.input()`. The `method` field distinguishes which UI call triggered the event.
842
842
 
843
843
  ```typescript
844
- pi.on("ui_confirm", async (event, ctx) => {
845
- // event.title - dialog title
846
- // event.message - dialog message
847
- // event.signal - AbortSignal if the caller provided one
848
- // event.timeout - timeout in ms if the caller provided one
844
+ pi.on("ui", async (event, ctx) => {
845
+ // event.id - unique identifier for this UI request
846
+ // event.method - "confirm" | "select" | "input" | "notify"
847
+ // event.title - dialog title (for notify, this is the message)
848
+ // event.message - dialog message (confirm only)
849
+ // event.options - string[] of options (select only)
850
+ // event.placeholder - placeholder text (input only)
851
+ // event.notifyType - "info" | "warning" | "error" (notify only)
852
+ // event.signal - AbortSignal if the caller provided one
853
+ // event.timeout - timeout in ms if the caller provided one
849
854
 
850
- // Example: forward all confirmations to a remote service
851
- const response = await fetch("https://my-server/api/confirm", {
852
- method: "POST",
853
- body: JSON.stringify({ title: event.title, message: event.message }),
854
- });
855
- const decision = await response.json();
856
-
857
- return { action: "responded", confirmed: decision.allowed };
858
- });
859
- ```
860
-
861
- **Results:**
862
- - `{ action: "responded", confirmed: boolean }` - answer the dialog immediately (first handler to respond wins)
863
- - `undefined` - pass through to the original UI implementation
864
-
865
- #### ui_select
866
-
867
- Fired when any code calls `ctx.ui.select()`. Return `{ action: "responded", value }` to provide a selection without showing the UI.
868
-
869
- ```typescript
870
- pi.on("ui_select", async (event, ctx) => {
871
- // event.title - dialog title
872
- // event.options - string[] of selectable options
873
- // event.signal - AbortSignal if provided
874
- // event.timeout - timeout in ms if provided
855
+ // Example: intercept all confirmations and forward to a remote service
856
+ if (event.method === "confirm") {
857
+ const response = await fetch("https://my-server/api/confirm", {
858
+ method: "POST",
859
+ body: JSON.stringify({ id: event.id, title: event.title, message: event.message }),
860
+ });
861
+ const decision = await response.json();
862
+ return { action: "responded", confirmed: decision.allowed };
863
+ }
875
864
 
876
- // Example: only intercept dangerous permission selects
877
- if (event.title.includes("Dangerous")) {
865
+ // Example: intercept select dialogs with a remote decision
866
+ if (event.method === "select" && event.title.includes("Dangerous")) {
878
867
  const choice = await askRemote(event);
879
868
  return { action: "responded", value: choice };
880
869
  }
881
870
 
882
- return undefined; // let other UI selects pass through normally
871
+ // Example: forward notifications to a remote service (return value is ignored for notify)
872
+ if (event.method === "notify") {
873
+ sendToRemote({ type: "notification", message: event.message, level: event.notifyType });
874
+ return undefined; // original notify still fires
875
+ }
876
+
877
+ return undefined; // pass through to the original UI
883
878
  });
884
879
  ```
885
880
 
886
881
  **Results:**
887
- - `{ action: "responded", value: string | undefined }` - answer the select (undefined = dismissed)
888
- - `undefined` - pass through to the original UI
882
+ - `{ action: "responded", confirmed: boolean }` - answer a confirm dialog (first handler to respond wins)
883
+ - `{ action: "responded", value: string | undefined }` - answer a select or input dialog (undefined = dismissed/cancelled)
884
+ - `undefined` - pass through to the original UI implementation
885
+ - For `notify`, the return value is ignored. The original `notify` always fires regardless.
889
886
 
890
- #### ui_input
887
+ #### Short-circuit behavior
891
888
 
892
- Fired when any code calls `ctx.ui.input()`. Return `{ action: "responded", value }` to provide input text.
889
+ The `ui` event uses first-responder semantics: the first handler to return `{ action: "responded" }` wins. Remaining handlers are not called. If no handler responds, the original UI implementation runs (TUI dialog, RPC protocol, or no-op depending on the mode).
893
890
 
894
- ```typescript
895
- pi.on("ui_input", async (event, ctx) => {
896
- // event.title - dialog title
897
- // event.placeholder - placeholder text if provided
898
- // event.signal - AbortSignal if provided
899
- // event.timeout - timeout in ms if provided
891
+ If a handler throws, the error is reported via the error listener and the next handler is tried. If all handlers fail, the original UI runs as fallback.
900
892
 
901
- return undefined; // pass through to normal UI
902
- });
903
- ```
893
+ #### ctx.respondUI(id, result) -- async response injection
904
894
 
905
- **Results:**
906
- - `{ action: "responded", value: string | undefined }` - provide the input (undefined = cancelled)
907
- - `undefined` - pass through to the original UI
895
+ When a handler returns `undefined`, the original UI is invoked -- but the handler can also capture the `event.id` and call `ctx.respondUI(id, result)` later to inject a response asynchronously. This creates a **race** between the original UI and the async response: first one wins, the other is ignored.
908
896
 
909
- #### Short-circuit behavior
897
+ ```typescript
898
+ pi.on("ui", async (event, ctx) => {
899
+ if (event.method === "confirm") {
900
+ // Forward to remote service (fire-and-forget)
901
+ sendToRemote({ id: event.id, message: event.message });
902
+ // Return undefined to let original UI also show
903
+ return undefined;
904
+ }
905
+ });
910
906
 
911
- All three events use first-responder semantics: the first handler to return `{ action: "responded" }` wins. Remaining handlers are not called. If no handler responds, the original UI implementation runs (TUI dialog, RPC protocol, or no-op depending on the mode).
907
+ // When remote responds (e.g. via channel, webhook, etc.):
908
+ someChannel.onReceive((response) => {
909
+ ctx.respondUI(response.id, { action: "responded", confirmed: response.allowed });
910
+ });
911
+ ```
912
912
 
913
- If a handler throws, the error is reported via the error listener and the next handler is tried. If all handlers fail, the original UI runs as fallback.
913
+ If the user responds from the TUI first, the `respondUI` call is ignored. If the remote service responds first, the TUI dialog is cancelled. First response wins.
914
914
 
915
915
  #### Use cases
916
916
 
917
- - **Remote permission control** -- forward all `confirm`/`select` dialogs to a web dashboard or mobile app, hold the agent until the operator responds
917
+ - **Remote permission control** -- forward all `confirm`/`select` dialogs to a web dashboard or mobile app, race with the local UI
918
918
  - **Audit logging** -- record all UI interactions without intercepting them (return `undefined` after logging)
919
919
  - **Headless automation** -- auto-approve or auto-deny dialogs based on rules, enabling unattended operation
920
920
  - **Custom approval workflows** -- require multiple approvers, time-based auto-approval, or integration with ticketing systems