ccjk 3.7.3 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -896
- package/dist/chunks/ccr.mjs +1 -0
- package/dist/chunks/doctor.mjs +58 -0
- package/dist/chunks/index.mjs +6 -0
- package/dist/chunks/package.mjs +1 -1
- package/dist/chunks/permissions.mjs +164 -342
- package/dist/chunks/thinking.mjs +615 -0
- package/dist/chunks/vim.mjs +891 -0
- package/dist/cli.mjs +49 -0
- package/dist/i18n/locales/en/configuration.json +97 -1
- package/dist/i18n/locales/en/lsp.json +78 -0
- package/dist/i18n/locales/en/mcp.json +11 -0
- package/dist/i18n/locales/en/permissions.json +53 -1
- package/dist/i18n/locales/en/thinking.json +65 -0
- package/dist/i18n/locales/en/vim.json +169 -0
- package/dist/i18n/locales/zh-CN/configuration.json +97 -1
- package/dist/i18n/locales/zh-CN/lsp.json +78 -0
- package/dist/i18n/locales/zh-CN/mcp.json +11 -0
- package/dist/i18n/locales/zh-CN/permissions.json +53 -1
- package/dist/i18n/locales/zh-CN/thinking.json +65 -0
- package/dist/i18n/locales/zh-CN/vim.json +169 -0
- package/dist/shared/ccjk.pi0nsyn3.mjs +1242 -0
- package/package.json +55 -55
- package/templates/claude-code/common/settings.json +63 -30
- package/templates/CLAUDE.md +0 -219
- package/templates/claude-code/CLAUDE.md +0 -250
- package/templates/claude-code/en/workflow/bmad/commands/bmad-init.md +0 -165
- package/templates/claude-code/en/workflow/common/agents/get-current-datetime.md +0 -29
- package/templates/claude-code/en/workflow/common/agents/init-architect.md +0 -114
- package/templates/claude-code/en/workflow/common/commands/init-project.md +0 -53
- package/templates/claude-code/en/workflow/essential/agents/get-current-datetime.md +0 -29
- package/templates/claude-code/en/workflow/essential/agents/init-architect.md +0 -114
- package/templates/claude-code/en/workflow/essential/agents/planner.md +0 -116
- package/templates/claude-code/en/workflow/essential/agents/ui-ux-designer.md +0 -91
- package/templates/claude-code/en/workflow/essential/commands/feat.md +0 -250
- package/templates/claude-code/en/workflow/essential/commands/init-project.md +0 -53
- package/templates/claude-code/en/workflow/plan/agents/planner.md +0 -116
- package/templates/claude-code/en/workflow/plan/agents/ui-ux-designer.md +0 -91
- package/templates/claude-code/en/workflow/plan/commands/feat.md +0 -105
- package/templates/claude-code/zh-CN/workflow/bmad/commands/bmad-init.md +0 -172
- package/templates/claude-code/zh-CN/workflow/common/agents/get-current-datetime.md +0 -29
- package/templates/claude-code/zh-CN/workflow/common/agents/init-architect.md +0 -114
- package/templates/claude-code/zh-CN/workflow/common/commands/init-project.md +0 -53
- package/templates/claude-code/zh-CN/workflow/essential/agents/get-current-datetime.md +0 -29
- package/templates/claude-code/zh-CN/workflow/essential/agents/init-architect.md +0 -114
- package/templates/claude-code/zh-CN/workflow/essential/agents/planner.md +0 -116
- package/templates/claude-code/zh-CN/workflow/essential/agents/ui-ux-designer.md +0 -91
- package/templates/claude-code/zh-CN/workflow/essential/commands/feat.md +0 -248
- package/templates/claude-code/zh-CN/workflow/essential/commands/init-project.md +0 -53
- package/templates/claude-code/zh-CN/workflow/plan/agents/planner.md +0 -116
- package/templates/claude-code/zh-CN/workflow/plan/agents/ui-ux-designer.md +0 -91
- package/templates/claude-code/zh-CN/workflow/plan/commands/feat.md +0 -105
- package/templates/codex/common/config.toml +0 -0
- package/templates/common/output-styles/en/casual-friendly.md +0 -97
- package/templates/common/output-styles/en/expert-concise.md +0 -93
- package/templates/common/output-styles/en/pair-programmer.md +0 -177
- package/templates/common/output-styles/en/senior-architect.md +0 -121
- package/templates/common/output-styles/en/speed-coder.md +0 -185
- package/templates/common/output-styles/en/teaching-mode.md +0 -102
- package/templates/common/output-styles/en/technical-precise.md +0 -101
- package/templates/common/output-styles/zh-CN/pair-programmer.md +0 -177
- package/templates/common/output-styles/zh-CN/senior-architect.md +0 -297
- package/templates/common/output-styles/zh-CN/speed-coder.md +0 -185
- package/templates/common/skills/code-review.md +0 -343
- package/templates/common/skills/en/agent-browser.md +0 -258
- package/templates/common/skills/en/brainstorming.md +0 -64
- package/templates/common/skills/en/code-review.md +0 -81
- package/templates/common/skills/en/documentation-gen.md +0 -808
- package/templates/common/skills/en/executing-plans.md +0 -75
- package/templates/common/skills/en/git-commit.md +0 -216
- package/templates/common/skills/en/interview.md +0 -223
- package/templates/common/skills/en/migration-assistant.md +0 -312
- package/templates/common/skills/en/performance-profiling.md +0 -576
- package/templates/common/skills/en/pr-review.md +0 -341
- package/templates/common/skills/en/refactoring.md +0 -384
- package/templates/common/skills/en/security-audit.md +0 -462
- package/templates/common/skills/en/systematic-debugging.md +0 -82
- package/templates/common/skills/en/tdd-workflow.md +0 -93
- package/templates/common/skills/en/verification.md +0 -81
- package/templates/common/skills/en/workflow.md +0 -370
- package/templates/common/skills/en/writing-plans.md +0 -78
- package/templates/common/skills/summarize.md +0 -312
- package/templates/common/skills/translate.md +0 -202
- package/templates/common/skills/zh-CN/agent-browser.md +0 -260
- package/templates/common/skills/zh-CN/documentation-gen.md +0 -807
- package/templates/common/skills/zh-CN/migration-assistant.md +0 -318
- package/templates/common/skills/zh-CN/performance-profiling.md +0 -746
- package/templates/common/skills/zh-CN/pr-review.md +0 -341
- package/templates/common/skills/zh-CN/refactoring.md +0 -384
- package/templates/common/skills/zh-CN/security-audit.md +0 -462
- package/templates/common/smart-guide/en/smart-guide.md +0 -72
- package/templates/common/smart-guide/zh-CN/smart-guide.md +0 -72
- package/templates/common/workflow/git/en/git-cleanBranches.md +0 -102
- package/templates/common/workflow/git/en/git-commit.md +0 -205
- package/templates/common/workflow/git/en/git-rollback.md +0 -90
- package/templates/common/workflow/git/en/git-worktree.md +0 -276
- package/templates/common/workflow/git/zh-CN/git-cleanBranches.md +0 -102
- package/templates/common/workflow/git/zh-CN/git-commit.md +0 -205
- package/templates/common/workflow/git/zh-CN/git-rollback.md +0 -90
- package/templates/common/workflow/git/zh-CN/git-worktree.md +0 -276
- package/templates/common/workflow/interview/en/interview.md +0 -212
- package/templates/common/workflow/interview/zh-CN/interview.md +0 -212
- package/templates/common/workflow/sixStep/en/workflow.md +0 -357
- package/templates/common/workflow/sixStep/zh-CN/workflow.md +0 -357
- package/templates/industry/devops/en/ci-cd-pipeline.md +0 -410
- package/templates/industry/web-dev/en/api-design.md +0 -299
- package/templates/industry/web-dev/en/react-nextjs-setup.md +0 -236
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'pathe';
|
|
4
|
+
|
|
5
|
+
class WildcardPatternMatcher {
|
|
6
|
+
patternCache = /* @__PURE__ */ new Map();
|
|
7
|
+
maxCacheSize;
|
|
8
|
+
cacheHits = 0;
|
|
9
|
+
cacheMisses = 0;
|
|
10
|
+
constructor(maxCacheSize = 1e3) {
|
|
11
|
+
this.maxCacheSize = maxCacheSize;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compile a pattern to a regex and cache it
|
|
15
|
+
*/
|
|
16
|
+
compilePattern(pattern) {
|
|
17
|
+
const cached = this.patternCache.get(pattern);
|
|
18
|
+
if (cached) {
|
|
19
|
+
this.cacheHits++;
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
this.cacheMisses++;
|
|
23
|
+
const compiled = this.analyzeAndCompile(pattern);
|
|
24
|
+
if (this.patternCache.size >= this.maxCacheSize) {
|
|
25
|
+
const firstKey = this.patternCache.keys().next().value;
|
|
26
|
+
if (firstKey) {
|
|
27
|
+
this.patternCache.delete(firstKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.patternCache.set(pattern, compiled);
|
|
31
|
+
return compiled;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Test if a target matches a pattern
|
|
35
|
+
*/
|
|
36
|
+
match(pattern, target) {
|
|
37
|
+
const compiled = this.compilePattern(pattern);
|
|
38
|
+
return compiled.regex.test(target);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Test if a target matches any of the given patterns
|
|
42
|
+
*/
|
|
43
|
+
matchAny(patterns, target) {
|
|
44
|
+
for (const pattern of patterns) {
|
|
45
|
+
if (this.match(pattern, target)) {
|
|
46
|
+
return { matched: true, pattern };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { matched: false };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get all patterns that match a target
|
|
53
|
+
*/
|
|
54
|
+
getAllMatches(patterns, target) {
|
|
55
|
+
return patterns.filter((pattern) => this.match(pattern, target));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Clear the pattern cache
|
|
59
|
+
*/
|
|
60
|
+
clearCache() {
|
|
61
|
+
this.patternCache.clear();
|
|
62
|
+
this.cacheHits = 0;
|
|
63
|
+
this.cacheMisses = 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get cache statistics
|
|
67
|
+
*/
|
|
68
|
+
getCacheStats() {
|
|
69
|
+
const total = this.cacheHits + this.cacheMisses;
|
|
70
|
+
return {
|
|
71
|
+
size: this.patternCache.size,
|
|
72
|
+
hits: this.cacheHits,
|
|
73
|
+
misses: this.cacheMisses,
|
|
74
|
+
hitRate: total > 0 ? this.cacheHits / total : 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Analyze pattern type and compile to regex
|
|
79
|
+
*/
|
|
80
|
+
analyzeAndCompile(pattern) {
|
|
81
|
+
const wildcardPositions = [];
|
|
82
|
+
let specificity = 0;
|
|
83
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
84
|
+
if (pattern[i] === "*" || pattern[i] === "?") {
|
|
85
|
+
wildcardPositions.push(i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let type;
|
|
89
|
+
const hasWildcards = wildcardPositions.length > 0;
|
|
90
|
+
const hasDoubleWildcard = pattern.includes("**");
|
|
91
|
+
if (!hasWildcards) {
|
|
92
|
+
type = "exact" /* Exact */;
|
|
93
|
+
specificity = 100;
|
|
94
|
+
} else if (pattern.startsWith("mcp__") && pattern.includes("__*")) {
|
|
95
|
+
type = "mcp" /* Mcp */;
|
|
96
|
+
specificity = this.calculateMcpSpecificity(pattern);
|
|
97
|
+
} else if (pattern.startsWith("Bash(") && pattern.includes(" ")) {
|
|
98
|
+
type = "bash" /* Bash */;
|
|
99
|
+
specificity = this.calculateBashSpecificity(pattern);
|
|
100
|
+
} else if (hasDoubleWildcard) {
|
|
101
|
+
type = "nested" /* Nested */;
|
|
102
|
+
specificity = this.calculateNestedSpecificity(pattern, wildcardPositions);
|
|
103
|
+
} else if (wildcardPositions.length === 1 && pattern.endsWith("*")) {
|
|
104
|
+
type = "prefix" /* Prefix */;
|
|
105
|
+
specificity = this.calculatePrefixSpecificity(pattern);
|
|
106
|
+
} else if (wildcardPositions.length === 1 && pattern.startsWith("*")) {
|
|
107
|
+
type = "suffix" /* Suffix */;
|
|
108
|
+
specificity = this.calculateSuffixSpecificity(pattern);
|
|
109
|
+
} else if (wildcardPositions.length > 1) {
|
|
110
|
+
type = "complex" /* Complex */;
|
|
111
|
+
specificity = this.calculateComplexSpecificity(pattern, wildcardPositions);
|
|
112
|
+
} else {
|
|
113
|
+
type = "middle" /* Middle */;
|
|
114
|
+
specificity = this.calculateMiddleSpecificity(pattern, wildcardPositions[0]);
|
|
115
|
+
}
|
|
116
|
+
const regex = this.patternToRegex(pattern, type);
|
|
117
|
+
const hash = this.generateHash(pattern);
|
|
118
|
+
return {
|
|
119
|
+
original: pattern,
|
|
120
|
+
regex,
|
|
121
|
+
type,
|
|
122
|
+
wildcardPositions,
|
|
123
|
+
specificity,
|
|
124
|
+
hash
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Convert pattern to regex based on type
|
|
129
|
+
*/
|
|
130
|
+
patternToRegex(pattern, type) {
|
|
131
|
+
let regexStr;
|
|
132
|
+
switch (type) {
|
|
133
|
+
case "mcp" /* Mcp */: {
|
|
134
|
+
regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/__/g, "_{2}").replace(/\*/g, "[^_]*");
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "bash" /* Bash */: {
|
|
138
|
+
regexStr = `^${this.escapeRegex(pattern).replace(/\*/g, ".*").replace(/\s+/g, "\\s+")}$`;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "nested" /* Nested */: {
|
|
142
|
+
regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
143
|
+
if (pattern.endsWith("**")) {
|
|
144
|
+
regexStr = `${pattern.slice(0, -2).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}.*`;
|
|
145
|
+
} else {
|
|
146
|
+
regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return new RegExp(`^${regexStr}$`, "i");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Calculate specificity for MCP patterns
|
|
159
|
+
*/
|
|
160
|
+
calculateMcpSpecificity(pattern) {
|
|
161
|
+
const parts = pattern.split("__").length;
|
|
162
|
+
if (pattern.endsWith("*")) {
|
|
163
|
+
return 30 + parts * 10;
|
|
164
|
+
}
|
|
165
|
+
return 40 + parts * 15;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Calculate specificity for Bash patterns
|
|
169
|
+
*/
|
|
170
|
+
calculateBashSpecificity(pattern) {
|
|
171
|
+
const match = pattern.match(/^Bash\((.*)\)$/);
|
|
172
|
+
if (!match)
|
|
173
|
+
return 20;
|
|
174
|
+
const inner = match[1];
|
|
175
|
+
if (!inner.includes("*")) {
|
|
176
|
+
return 90;
|
|
177
|
+
}
|
|
178
|
+
const segments = inner.split(" ");
|
|
179
|
+
let specificity = 30;
|
|
180
|
+
for (const seg of segments) {
|
|
181
|
+
if (seg && seg !== "*") {
|
|
182
|
+
specificity += 15;
|
|
183
|
+
} else if (seg === "*") {
|
|
184
|
+
specificity += 5;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return specificity;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Calculate specificity for prefix patterns
|
|
191
|
+
*/
|
|
192
|
+
calculatePrefixSpecificity(pattern) {
|
|
193
|
+
const baseLen = pattern.length - 1;
|
|
194
|
+
return Math.min(50 + baseLen, 80);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Calculate specificity for suffix patterns
|
|
198
|
+
*/
|
|
199
|
+
calculateSuffixSpecificity(_pattern) {
|
|
200
|
+
return 45;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Calculate specificity for middle wildcards
|
|
204
|
+
*/
|
|
205
|
+
calculateMiddleSpecificity(pattern, pos) {
|
|
206
|
+
const beforeLen = pos;
|
|
207
|
+
const afterLen = pattern.length - pos - 1;
|
|
208
|
+
return 40 + beforeLen + afterLen;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Calculate specificity for complex patterns
|
|
212
|
+
*/
|
|
213
|
+
calculateComplexSpecificity(pattern, positions) {
|
|
214
|
+
const nonWildcard = pattern.length - positions.length;
|
|
215
|
+
return 30 + nonWildcard * 2;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Calculate specificity for nested path patterns
|
|
219
|
+
*/
|
|
220
|
+
calculateNestedSpecificity(pattern, positions) {
|
|
221
|
+
let score = 25;
|
|
222
|
+
const segments = pattern.split("/").filter((s) => s && s !== "**");
|
|
223
|
+
score += segments.length * 10;
|
|
224
|
+
for (const seg of segments) {
|
|
225
|
+
if (seg !== "**" && !seg.includes("*")) {
|
|
226
|
+
score += 10;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return Math.min(score, 95);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Escape special regex characters
|
|
233
|
+
*/
|
|
234
|
+
escapeRegex(str) {
|
|
235
|
+
return str.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Generate a simple hash for the pattern
|
|
239
|
+
*/
|
|
240
|
+
generateHash(pattern) {
|
|
241
|
+
let hash = 0;
|
|
242
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
243
|
+
const char = pattern.charCodeAt(i);
|
|
244
|
+
hash = (hash << 5) - hash + char;
|
|
245
|
+
hash = hash & hash;
|
|
246
|
+
}
|
|
247
|
+
return Math.abs(hash).toString(36);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Validate if a pattern string is well-formed
|
|
251
|
+
*/
|
|
252
|
+
validatePattern(pattern) {
|
|
253
|
+
if (!pattern || pattern.trim().length === 0) {
|
|
254
|
+
return { valid: false, error: "Pattern cannot be empty" };
|
|
255
|
+
}
|
|
256
|
+
let parenDepth = 0;
|
|
257
|
+
for (const char of pattern) {
|
|
258
|
+
if (char === "(")
|
|
259
|
+
parenDepth++;
|
|
260
|
+
if (char === ")")
|
|
261
|
+
parenDepth--;
|
|
262
|
+
if (parenDepth < 0) {
|
|
263
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (parenDepth !== 0) {
|
|
267
|
+
return { valid: false, error: "Unbalanced parentheses" };
|
|
268
|
+
}
|
|
269
|
+
if (pattern.includes("***")) {
|
|
270
|
+
return { valid: false, error: "Invalid wildcard sequence (***" };
|
|
271
|
+
}
|
|
272
|
+
if (pattern.startsWith("Bash(")) {
|
|
273
|
+
if (!pattern.endsWith(")")) {
|
|
274
|
+
return { valid: false, error: "Bash pattern must end with )" };
|
|
275
|
+
}
|
|
276
|
+
const inner = pattern.slice(5, -1);
|
|
277
|
+
if (inner.length === 0) {
|
|
278
|
+
return { valid: false, error: "Bash pattern cannot be empty" };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (pattern.startsWith("mcp__") && pattern.includes("__*")) {
|
|
282
|
+
const parts = pattern.split("__");
|
|
283
|
+
if (parts.length < 3) {
|
|
284
|
+
return { valid: false, error: "MCP pattern must have at least 3 parts" };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { valid: true };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get pattern type as a human-readable string
|
|
291
|
+
*/
|
|
292
|
+
getPatternType(pattern) {
|
|
293
|
+
const compiled = this.compilePattern(pattern);
|
|
294
|
+
return compiled.type;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
class WildcardPermissionRules {
|
|
298
|
+
matcher;
|
|
299
|
+
rules = [];
|
|
300
|
+
config;
|
|
301
|
+
beforeHooks = [];
|
|
302
|
+
afterHooks = [];
|
|
303
|
+
constructor(config = {}) {
|
|
304
|
+
this.matcher = new WildcardPatternMatcher(config.maxCacheSize);
|
|
305
|
+
this.config = {
|
|
306
|
+
allowUnsandboxedCommands: false,
|
|
307
|
+
disallowedTools: [],
|
|
308
|
+
maxCacheSize: 1e3,
|
|
309
|
+
enableDiagnostics: false,
|
|
310
|
+
...config
|
|
311
|
+
};
|
|
312
|
+
if (config.beforeCheck) {
|
|
313
|
+
this.beforeHooks.push(config.beforeCheck);
|
|
314
|
+
}
|
|
315
|
+
if (config.afterCheck) {
|
|
316
|
+
this.afterHooks.push(config.afterCheck);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Add a permission rule
|
|
321
|
+
*/
|
|
322
|
+
addRule(rule) {
|
|
323
|
+
const validation = this.matcher.validatePattern(rule.pattern);
|
|
324
|
+
if (!validation.valid) {
|
|
325
|
+
throw new Error(`Invalid pattern: ${validation.error}`);
|
|
326
|
+
}
|
|
327
|
+
const existingIndex = this.rules.findIndex(
|
|
328
|
+
(r) => r.pattern === rule.pattern && r.type === rule.type
|
|
329
|
+
);
|
|
330
|
+
const newRule = {
|
|
331
|
+
...rule,
|
|
332
|
+
createdAt: rule.createdAt ?? Date.now(),
|
|
333
|
+
modifiedAt: Date.now(),
|
|
334
|
+
enabled: rule.enabled ?? true,
|
|
335
|
+
priority: rule.priority ?? this.calculateDefaultPriority(rule)
|
|
336
|
+
};
|
|
337
|
+
if (existingIndex >= 0) {
|
|
338
|
+
this.rules[existingIndex] = {
|
|
339
|
+
...this.rules[existingIndex],
|
|
340
|
+
...newRule,
|
|
341
|
+
createdAt: this.rules[existingIndex].createdAt
|
|
342
|
+
};
|
|
343
|
+
} else {
|
|
344
|
+
this.rules.push(newRule);
|
|
345
|
+
this.sortRulesByPriority();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Remove a permission rule
|
|
350
|
+
*/
|
|
351
|
+
removeRule(pattern, type) {
|
|
352
|
+
const initialLength = this.rules.length;
|
|
353
|
+
this.rules = this.rules.filter((rule) => {
|
|
354
|
+
if (type && rule.type !== type) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return rule.pattern !== pattern;
|
|
358
|
+
});
|
|
359
|
+
return this.rules.length < initialLength;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Check if a target is allowed
|
|
363
|
+
*/
|
|
364
|
+
async checkPermission(target, context = {}) {
|
|
365
|
+
const fullContext = {
|
|
366
|
+
action: "check",
|
|
367
|
+
target,
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
...context
|
|
370
|
+
};
|
|
371
|
+
for (const hook of this.beforeHooks) {
|
|
372
|
+
await hook(fullContext, { allowed: false, reason: "Checking..." });
|
|
373
|
+
}
|
|
374
|
+
let result;
|
|
375
|
+
if (this.config.disallowedTools?.some((tool) => target.includes(tool))) {
|
|
376
|
+
result = {
|
|
377
|
+
allowed: false,
|
|
378
|
+
reason: `Tool is in disallowed list: ${this.config.disallowedTools.join(", ")}`
|
|
379
|
+
};
|
|
380
|
+
} else {
|
|
381
|
+
const denyRule = this.findMatchingRule(target, "deny");
|
|
382
|
+
if (denyRule && denyRule.enabled !== false) {
|
|
383
|
+
result = {
|
|
384
|
+
allowed: false,
|
|
385
|
+
matchedRule: denyRule,
|
|
386
|
+
matchedPattern: denyRule.pattern,
|
|
387
|
+
reason: `Denied by rule: ${denyRule.pattern}`,
|
|
388
|
+
source: denyRule.source
|
|
389
|
+
};
|
|
390
|
+
} else {
|
|
391
|
+
const allowRule = this.findMatchingRule(target, "allow");
|
|
392
|
+
if (allowRule && allowRule.enabled !== false) {
|
|
393
|
+
result = {
|
|
394
|
+
allowed: true,
|
|
395
|
+
matchedRule: allowRule,
|
|
396
|
+
matchedPattern: allowRule.pattern,
|
|
397
|
+
reason: `Allowed by rule: ${allowRule.pattern}`,
|
|
398
|
+
source: allowRule.source
|
|
399
|
+
};
|
|
400
|
+
} else {
|
|
401
|
+
result = {
|
|
402
|
+
allowed: false,
|
|
403
|
+
reason: "No matching allow rule found (default deny)"
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const hook of this.afterHooks) {
|
|
409
|
+
await hook(fullContext, result);
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Find the highest priority matching rule for a target
|
|
415
|
+
*/
|
|
416
|
+
findMatchingRule(target, type) {
|
|
417
|
+
const matchingRules = this.rules.filter(
|
|
418
|
+
(rule) => rule.type === type && rule.enabled !== false && this.matcher.match(rule.pattern, target)
|
|
419
|
+
);
|
|
420
|
+
if (matchingRules.length === 0) {
|
|
421
|
+
return void 0;
|
|
422
|
+
}
|
|
423
|
+
if (matchingRules.length === 1) {
|
|
424
|
+
return matchingRules[0];
|
|
425
|
+
}
|
|
426
|
+
return matchingRules.reduce((best, current) => {
|
|
427
|
+
const bestCompiled = this.matcher.compilePattern(best.pattern);
|
|
428
|
+
const currentCompiled = this.matcher.compilePattern(current.pattern);
|
|
429
|
+
if (currentCompiled.specificity > bestCompiled.specificity) {
|
|
430
|
+
return current;
|
|
431
|
+
}
|
|
432
|
+
return best;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get all rules
|
|
437
|
+
*/
|
|
438
|
+
getAllRules() {
|
|
439
|
+
return [...this.rules];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get rules by type
|
|
443
|
+
*/
|
|
444
|
+
getRulesByType(type) {
|
|
445
|
+
return this.rules.filter((rule) => rule.type === type);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get rules by category
|
|
449
|
+
*/
|
|
450
|
+
getRulesByCategory(category) {
|
|
451
|
+
return this.rules.filter((rule) => rule.category === category);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Clear all rules
|
|
455
|
+
*/
|
|
456
|
+
clearRules() {
|
|
457
|
+
this.rules = [];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Test a pattern against sample targets
|
|
461
|
+
*/
|
|
462
|
+
testPattern(pattern, targets) {
|
|
463
|
+
const validation = this.matcher.validatePattern(pattern);
|
|
464
|
+
if (!validation.valid) {
|
|
465
|
+
return {
|
|
466
|
+
pattern,
|
|
467
|
+
matched: [],
|
|
468
|
+
notMatched: [],
|
|
469
|
+
errors: [validation.error],
|
|
470
|
+
valid: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const matched = [];
|
|
474
|
+
const notMatched = [];
|
|
475
|
+
const errors = [];
|
|
476
|
+
for (const target of targets) {
|
|
477
|
+
try {
|
|
478
|
+
if (this.matcher.match(pattern, target)) {
|
|
479
|
+
matched.push(target);
|
|
480
|
+
} else {
|
|
481
|
+
notMatched.push(target);
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
pattern,
|
|
489
|
+
matched,
|
|
490
|
+
notMatched,
|
|
491
|
+
errors,
|
|
492
|
+
valid: errors.length === 0
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Get diagnostics for a specific rule
|
|
497
|
+
*/
|
|
498
|
+
getDiagnostics(rulePattern) {
|
|
499
|
+
const rule = this.rules.find((r) => r.pattern === rulePattern);
|
|
500
|
+
if (!rule) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const shadowedBy = [];
|
|
504
|
+
const shadows = [];
|
|
505
|
+
const conflicts = [];
|
|
506
|
+
const suggestions = [];
|
|
507
|
+
const ruleCompiled = this.matcher.compilePattern(rule.pattern);
|
|
508
|
+
for (const other of this.rules) {
|
|
509
|
+
if (other === rule)
|
|
510
|
+
continue;
|
|
511
|
+
const otherCompiled = this.matcher.compilePattern(other.pattern);
|
|
512
|
+
if (other.type === rule.type) {
|
|
513
|
+
if (otherCompiled.specificity < ruleCompiled.specificity) {
|
|
514
|
+
if (this.matcher.match(other.pattern, rule.pattern)) {
|
|
515
|
+
shadowedBy.push(other);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (other.pattern === rule.pattern && other.type !== rule.type) {
|
|
520
|
+
conflicts.push({
|
|
521
|
+
rule: other,
|
|
522
|
+
conflict: `Conflicting rule type: ${other.type} vs ${rule.type}`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (shadowedBy.length > 0) {
|
|
527
|
+
suggestions.push(`Rule is shadowed by ${shadowedBy.length} other rule(s) with higher priority`);
|
|
528
|
+
suggestions.push("Consider increasing the priority of this rule");
|
|
529
|
+
suggestions.push("Or remove/reduce the specificity of shadowing rules");
|
|
530
|
+
for (const shadow of shadowedBy) {
|
|
531
|
+
if (shadow.pattern === "*") {
|
|
532
|
+
suggestions.push(`Remove or narrow the catch-all rule: ${shadow.pattern}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
let reachable = true;
|
|
537
|
+
const testTargets = this.generateTestTargets(rule.category);
|
|
538
|
+
const testResult = this.testPattern(rule.pattern, testTargets);
|
|
539
|
+
if (testResult.matched.length === 0 && testResult.notMatched.length === testTargets.length) {
|
|
540
|
+
reachable = false;
|
|
541
|
+
suggestions.push("Pattern does not match any common targets");
|
|
542
|
+
suggestions.push("Verify the pattern syntax and expected resource format");
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
rule,
|
|
546
|
+
reachable,
|
|
547
|
+
shadowedBy,
|
|
548
|
+
shadows,
|
|
549
|
+
suggestions,
|
|
550
|
+
conflicts
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get diagnostics for all rules
|
|
555
|
+
*/
|
|
556
|
+
getAllDiagnostics() {
|
|
557
|
+
return this.rules.map((rule) => this.getDiagnostics(rule.pattern)).filter((d) => d !== null);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get unreachable rules
|
|
561
|
+
*/
|
|
562
|
+
getUnreachableRules() {
|
|
563
|
+
return this.getAllDiagnostics().filter((d) => !d.reachable).map((d) => d.rule);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Sort rules by priority (highest first)
|
|
567
|
+
*/
|
|
568
|
+
sortRulesByPriority() {
|
|
569
|
+
this.rules.sort((a, b) => {
|
|
570
|
+
const priorityA = a.priority ?? this.calculateDefaultPriority(a);
|
|
571
|
+
const priorityB = b.priority ?? this.calculateDefaultPriority(b);
|
|
572
|
+
if (priorityA !== priorityB) {
|
|
573
|
+
return priorityB - priorityA;
|
|
574
|
+
}
|
|
575
|
+
const specificityA = this.matcher.compilePattern(a.pattern).specificity;
|
|
576
|
+
const specificityB = this.matcher.compilePattern(b.pattern).specificity;
|
|
577
|
+
return specificityB - specificityA;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Calculate default priority for a rule
|
|
582
|
+
*/
|
|
583
|
+
calculateDefaultPriority(rule) {
|
|
584
|
+
const compiled = this.matcher.compilePattern(rule.pattern);
|
|
585
|
+
return compiled.specificity;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Check if pattern1 is more general than pattern2
|
|
589
|
+
*/
|
|
590
|
+
isMoreGeneralPattern(pattern1, pattern2) {
|
|
591
|
+
const compiled1 = this.matcher.compilePattern(pattern1);
|
|
592
|
+
const compiled2 = this.matcher.compilePattern(pattern2);
|
|
593
|
+
return compiled1.specificity < compiled2.specificity;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Generate test targets for a category
|
|
597
|
+
*/
|
|
598
|
+
generateTestTargets(category) {
|
|
599
|
+
const commonTargets = {
|
|
600
|
+
bash: ["npm install", "npm test", "git status", "ls -la", "cat file.txt"],
|
|
601
|
+
mcp: ["mcp__server__tool1", "mcp__server__tool2", "mcp__other__func"],
|
|
602
|
+
filesystem: ["/path/to/file.txt", "/home/user/.bashrc", "/etc/config"],
|
|
603
|
+
network: ["https://api.example.com", "https://github.com/*", "wss://socket.server"],
|
|
604
|
+
tool: ["Read", "Write", "Edit", "Bash", "WebSearch"],
|
|
605
|
+
command: ["init", "update", "doctor", "permissions"],
|
|
606
|
+
workflow: ["sixStep", "featPlan", "bmad"],
|
|
607
|
+
provider: ["302ai", "glm", "minimax", "kimi"],
|
|
608
|
+
model: ["claude-opus", "claude-sonnet", "gpt-4"]
|
|
609
|
+
};
|
|
610
|
+
return commonTargets[category] || [];
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Add a before-check hook
|
|
614
|
+
*/
|
|
615
|
+
addBeforeHook(hook) {
|
|
616
|
+
this.beforeHooks.push(hook);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Add an after-check hook
|
|
620
|
+
*/
|
|
621
|
+
addAfterHook(hook) {
|
|
622
|
+
this.afterHooks.push(hook);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get cache statistics
|
|
626
|
+
*/
|
|
627
|
+
getCacheStats() {
|
|
628
|
+
return this.matcher.getCacheStats();
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Clear the pattern cache
|
|
632
|
+
*/
|
|
633
|
+
clearCache() {
|
|
634
|
+
this.matcher.clearCache();
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Import rules from configuration
|
|
638
|
+
*/
|
|
639
|
+
importFromConfig(config, defaultSource = "config") {
|
|
640
|
+
if (config.allow) {
|
|
641
|
+
for (const pattern of config.allow) {
|
|
642
|
+
try {
|
|
643
|
+
this.addRule({
|
|
644
|
+
type: "allow",
|
|
645
|
+
pattern,
|
|
646
|
+
category: this.inferCategory(pattern),
|
|
647
|
+
source: defaultSource
|
|
648
|
+
});
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (config.deny) {
|
|
654
|
+
for (const pattern of config.deny) {
|
|
655
|
+
try {
|
|
656
|
+
this.addRule({
|
|
657
|
+
type: "deny",
|
|
658
|
+
pattern,
|
|
659
|
+
category: this.inferCategory(pattern),
|
|
660
|
+
source: defaultSource
|
|
661
|
+
});
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Export rules to configuration format
|
|
669
|
+
*/
|
|
670
|
+
exportToConfig() {
|
|
671
|
+
return {
|
|
672
|
+
allow: this.rules.filter((r) => r.type === "allow").map((r) => r.pattern),
|
|
673
|
+
deny: this.rules.filter((r) => r.type === "deny").map((r) => r.pattern)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Validate a pattern string (public wrapper)
|
|
678
|
+
*/
|
|
679
|
+
validatePattern(pattern) {
|
|
680
|
+
return this.matcher.validatePattern(pattern);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get pattern type (public wrapper)
|
|
684
|
+
*/
|
|
685
|
+
getPatternType(pattern) {
|
|
686
|
+
return this.matcher.getPatternType(pattern);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Infer category from pattern
|
|
690
|
+
*/
|
|
691
|
+
inferCategory(pattern) {
|
|
692
|
+
if (pattern.startsWith("Bash(")) {
|
|
693
|
+
return "bash";
|
|
694
|
+
}
|
|
695
|
+
if (pattern.startsWith("mcp__")) {
|
|
696
|
+
return "mcp";
|
|
697
|
+
}
|
|
698
|
+
if (pattern.startsWith("http://") || pattern.startsWith("https://") || pattern.startsWith("ws://") || pattern.startsWith("wss://")) {
|
|
699
|
+
return "network";
|
|
700
|
+
}
|
|
701
|
+
if (pattern.startsWith("/")) {
|
|
702
|
+
return "filesystem";
|
|
703
|
+
}
|
|
704
|
+
if (["Read", "Write", "Edit", "Bash", "WebSearch"].includes(pattern)) {
|
|
705
|
+
return "tool";
|
|
706
|
+
}
|
|
707
|
+
return "command";
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Match a pattern against a target string
|
|
711
|
+
*/
|
|
712
|
+
match(pattern, target) {
|
|
713
|
+
return this.matcher.match(pattern, target);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Get statistics about the rules
|
|
717
|
+
*/
|
|
718
|
+
getStats() {
|
|
719
|
+
const byCategory = {};
|
|
720
|
+
const bySource = {};
|
|
721
|
+
for (const rule of this.rules) {
|
|
722
|
+
byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
|
|
723
|
+
bySource[rule.source] = (bySource[rule.source] || 0) + 1;
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
total: this.rules.length,
|
|
727
|
+
allow: this.rules.filter((r) => r.type === "allow").length,
|
|
728
|
+
deny: this.rules.filter((r) => r.type === "deny").length,
|
|
729
|
+
enabled: this.rules.filter((r) => r.enabled !== false).length,
|
|
730
|
+
disabled: this.rules.filter((r) => r.enabled === false).length,
|
|
731
|
+
byCategory,
|
|
732
|
+
bySource
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
let singletonInstance = null;
|
|
737
|
+
function getWildcardPermissionRules(config) {
|
|
738
|
+
if (!singletonInstance) {
|
|
739
|
+
singletonInstance = new WildcardPermissionRules(config);
|
|
740
|
+
}
|
|
741
|
+
return singletonInstance;
|
|
742
|
+
}
|
|
743
|
+
const SAMPLE_PATTERNS = {
|
|
744
|
+
// Bash command patterns
|
|
745
|
+
bash: [
|
|
746
|
+
"Bash(npm *)",
|
|
747
|
+
"Bash(npm install)",
|
|
748
|
+
"Bash(npm test)",
|
|
749
|
+
"Bash(git *)",
|
|
750
|
+
"Bash(git status)",
|
|
751
|
+
"Bash(* install)"
|
|
752
|
+
// Any * install command
|
|
753
|
+
],
|
|
754
|
+
// MCP tool patterns
|
|
755
|
+
mcp: [
|
|
756
|
+
"mcp__server__*",
|
|
757
|
+
"mcp__filesystem__*",
|
|
758
|
+
"mcp__github__*",
|
|
759
|
+
"mcp__*__*"
|
|
760
|
+
// Any MCP tool
|
|
761
|
+
],
|
|
762
|
+
// Filesystem patterns
|
|
763
|
+
filesystem: [
|
|
764
|
+
"/home/user/*",
|
|
765
|
+
"/home/user/**/*.txt",
|
|
766
|
+
"*.md",
|
|
767
|
+
"/tmp/*"
|
|
768
|
+
],
|
|
769
|
+
// Network patterns
|
|
770
|
+
network: [
|
|
771
|
+
"https://api.example.com/*",
|
|
772
|
+
"https://github.com/*",
|
|
773
|
+
"wss://socket.example.com"
|
|
774
|
+
]
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
class PermissionManager {
|
|
778
|
+
wildcardRules;
|
|
779
|
+
configPath;
|
|
780
|
+
settingsPath;
|
|
781
|
+
legacyMode = false;
|
|
782
|
+
constructor(configPath, settingsPath) {
|
|
783
|
+
this.configPath = configPath || join(homedir(), ".ccjk", "permissions.json");
|
|
784
|
+
this.settingsPath = settingsPath || join(homedir(), ".claude", "settings.json");
|
|
785
|
+
const config = {
|
|
786
|
+
maxCacheSize: 1e3,
|
|
787
|
+
enableDiagnostics: true
|
|
788
|
+
};
|
|
789
|
+
this.wildcardRules = new WildcardPermissionRules(config);
|
|
790
|
+
this.loadPermissions();
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Load permissions from config files
|
|
794
|
+
* Loads from both legacy config and Claude Code settings.json
|
|
795
|
+
*/
|
|
796
|
+
loadPermissions() {
|
|
797
|
+
this.loadFromSettingsJson();
|
|
798
|
+
this.loadFromLegacyConfig();
|
|
799
|
+
this.loadFromClaudePermissions();
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Load permissions from Claude Code settings.json
|
|
803
|
+
*/
|
|
804
|
+
loadFromSettingsJson() {
|
|
805
|
+
try {
|
|
806
|
+
if (!existsSync(this.settingsPath)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const content = readFileSync(this.settingsPath, "utf-8");
|
|
810
|
+
const settings = JSON.parse(content);
|
|
811
|
+
if (settings.experimental?.allowUnsandboxedCommands) {
|
|
812
|
+
this.wildcardRules = getWildcardPermissionRules({
|
|
813
|
+
allowUnsandboxedCommands: true
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (settings.experimental?.disallowedTools) {
|
|
817
|
+
this.wildcardRules = getWildcardPermissionRules({
|
|
818
|
+
disallowedTools: settings.experimental.disallowedTools
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
if (settings.chat?.alwaysApprove) {
|
|
822
|
+
for (const pattern of settings.chat.alwaysApprove) {
|
|
823
|
+
try {
|
|
824
|
+
this.wildcardRules.addRule({
|
|
825
|
+
type: "allow",
|
|
826
|
+
pattern,
|
|
827
|
+
category: this.inferCategory(pattern),
|
|
828
|
+
source: "settings",
|
|
829
|
+
description: "From chat.alwaysApprove"
|
|
830
|
+
});
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Load from legacy CCJK config
|
|
840
|
+
*/
|
|
841
|
+
loadFromLegacyConfig() {
|
|
842
|
+
try {
|
|
843
|
+
if (!existsSync(this.configPath)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const content = readFileSync(this.configPath, "utf-8");
|
|
847
|
+
const config = JSON.parse(content);
|
|
848
|
+
if (config.allow || config.deny) {
|
|
849
|
+
this.wildcardRules.importFromConfig(config, "config");
|
|
850
|
+
this.legacyMode = true;
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Load from Claude Code 2.0.70+ permissions format
|
|
857
|
+
*/
|
|
858
|
+
loadFromClaudePermissions() {
|
|
859
|
+
try {
|
|
860
|
+
if (!existsSync(this.settingsPath)) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const content = readFileSync(this.settingsPath, "utf-8");
|
|
864
|
+
const settings = JSON.parse(content);
|
|
865
|
+
if (settings.permissions) {
|
|
866
|
+
this.wildcardRules.importFromConfig(settings.permissions, "settings");
|
|
867
|
+
}
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Save permissions to config files
|
|
873
|
+
*/
|
|
874
|
+
savePermissions() {
|
|
875
|
+
try {
|
|
876
|
+
this.saveToSettingsJson();
|
|
877
|
+
if (this.legacyMode) {
|
|
878
|
+
this.saveToLegacyConfig();
|
|
879
|
+
}
|
|
880
|
+
} catch (error) {
|
|
881
|
+
throw new Error(`Failed to save permissions: ${error instanceof Error ? error.message : String(error)}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Save to Claude Code settings.json
|
|
886
|
+
*/
|
|
887
|
+
saveToSettingsJson() {
|
|
888
|
+
try {
|
|
889
|
+
let settings = {};
|
|
890
|
+
if (existsSync(this.settingsPath)) {
|
|
891
|
+
const content = readFileSync(this.settingsPath, "utf-8");
|
|
892
|
+
settings = JSON.parse(content);
|
|
893
|
+
}
|
|
894
|
+
const exported = this.wildcardRules.exportToConfig();
|
|
895
|
+
settings.permissions = {
|
|
896
|
+
allow: exported.allow,
|
|
897
|
+
deny: exported.deny
|
|
898
|
+
};
|
|
899
|
+
writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
900
|
+
} catch (error) {
|
|
901
|
+
throw new Error(`Failed to save to settings.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Save to legacy CCJK config
|
|
906
|
+
*/
|
|
907
|
+
saveToLegacyConfig() {
|
|
908
|
+
try {
|
|
909
|
+
const exported = this.wildcardRules.exportToConfig();
|
|
910
|
+
writeFileSync(this.configPath, JSON.stringify(exported, null, 2), "utf-8");
|
|
911
|
+
} catch (error) {
|
|
912
|
+
throw new Error(`Failed to save to legacy config: ${error instanceof Error ? error.message : String(error)}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Check if an action on a resource is permitted
|
|
917
|
+
* @param action - The action to check (e.g., "read", "write", "execute")
|
|
918
|
+
* @param resource - The resource identifier (e.g., "Bash(npm install)", "mcp__server__tool")
|
|
919
|
+
* @returns Permission check result
|
|
920
|
+
*/
|
|
921
|
+
async checkPermission(action, resource) {
|
|
922
|
+
const context = {
|
|
923
|
+
action,
|
|
924
|
+
target: resource,
|
|
925
|
+
timestamp: Date.now()
|
|
926
|
+
};
|
|
927
|
+
return await this.wildcardRules.checkPermission(resource, context);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Legacy checkPermission method for backward compatibility
|
|
931
|
+
*/
|
|
932
|
+
checkPermissionSync(action, resource) {
|
|
933
|
+
const result = {
|
|
934
|
+
allowed: false,
|
|
935
|
+
reason: "Use async checkPermission for full wildcard support"
|
|
936
|
+
};
|
|
937
|
+
const target = `${resource}:${action}`;
|
|
938
|
+
const denyRule = this.wildcardRules.findMatchingRule(target, "deny");
|
|
939
|
+
if (denyRule) {
|
|
940
|
+
return {
|
|
941
|
+
allowed: false,
|
|
942
|
+
matchedRule: denyRule,
|
|
943
|
+
matchedPattern: denyRule.pattern,
|
|
944
|
+
reason: `Denied by rule: ${denyRule.pattern}`,
|
|
945
|
+
source: denyRule.source
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
const allowRule = this.wildcardRules.findMatchingRule(target, "allow");
|
|
949
|
+
if (allowRule) {
|
|
950
|
+
return {
|
|
951
|
+
allowed: true,
|
|
952
|
+
matchedRule: allowRule,
|
|
953
|
+
matchedPattern: allowRule.pattern,
|
|
954
|
+
reason: `Allowed by rule: ${allowRule.pattern}`,
|
|
955
|
+
source: allowRule.source
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Match a pattern against a target string (legacy method)
|
|
962
|
+
*/
|
|
963
|
+
matchPattern(pattern, target) {
|
|
964
|
+
return this.wildcardRules.match(pattern, target);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Add a permission rule
|
|
968
|
+
*/
|
|
969
|
+
addPermission(permission) {
|
|
970
|
+
this.wildcardRules.addRule({
|
|
971
|
+
type: permission.type,
|
|
972
|
+
pattern: permission.pattern,
|
|
973
|
+
category: this.inferCategory(permission.pattern),
|
|
974
|
+
source: "user",
|
|
975
|
+
description: permission.description,
|
|
976
|
+
priority: permission.pattern === "*" ? 0 : void 0
|
|
977
|
+
});
|
|
978
|
+
this.savePermissions();
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Add a wildcard permission rule (new API)
|
|
982
|
+
*/
|
|
983
|
+
addRule(rule) {
|
|
984
|
+
this.wildcardRules.addRule(rule);
|
|
985
|
+
this.savePermissions();
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Remove a permission rule by pattern
|
|
989
|
+
*/
|
|
990
|
+
removePermission(pattern, type) {
|
|
991
|
+
const removed = this.wildcardRules.removeRule(pattern, type);
|
|
992
|
+
if (removed) {
|
|
993
|
+
this.savePermissions();
|
|
994
|
+
}
|
|
995
|
+
return removed;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* List all permissions (legacy format)
|
|
999
|
+
*/
|
|
1000
|
+
listPermissions(type, scope) {
|
|
1001
|
+
const rules = this.wildcardRules.getAllRules();
|
|
1002
|
+
let filtered = rules;
|
|
1003
|
+
if (type) {
|
|
1004
|
+
filtered = filtered.filter((r) => r.type === type);
|
|
1005
|
+
}
|
|
1006
|
+
return filtered.map((r) => ({
|
|
1007
|
+
type: r.type,
|
|
1008
|
+
pattern: r.pattern,
|
|
1009
|
+
scope: "global",
|
|
1010
|
+
description: r.description,
|
|
1011
|
+
createdAt: r.createdAt ? new Date(r.createdAt).toISOString() : void 0
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get all wildcard rules
|
|
1016
|
+
*/
|
|
1017
|
+
getAllRules() {
|
|
1018
|
+
return this.wildcardRules.getAllRules();
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Get rules by type
|
|
1022
|
+
*/
|
|
1023
|
+
getRulesByType(type) {
|
|
1024
|
+
return this.wildcardRules.getRulesByType(type);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get rules by category
|
|
1028
|
+
*/
|
|
1029
|
+
getRulesByCategory(category) {
|
|
1030
|
+
return this.wildcardRules.getRulesByCategory(category);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Clear all permissions
|
|
1034
|
+
*/
|
|
1035
|
+
clearPermissions(type) {
|
|
1036
|
+
if (type) {
|
|
1037
|
+
const rulesToRemove = this.wildcardRules.getRulesByType(type);
|
|
1038
|
+
for (const rule of rulesToRemove) {
|
|
1039
|
+
this.wildcardRules.removeRule(rule.pattern, rule.type);
|
|
1040
|
+
}
|
|
1041
|
+
} else {
|
|
1042
|
+
this.wildcardRules.clearRules();
|
|
1043
|
+
}
|
|
1044
|
+
this.savePermissions();
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Get permission statistics
|
|
1048
|
+
*/
|
|
1049
|
+
getStats() {
|
|
1050
|
+
const stats = this.wildcardRules.getStats();
|
|
1051
|
+
return {
|
|
1052
|
+
total: stats.total,
|
|
1053
|
+
allow: stats.allow,
|
|
1054
|
+
deny: stats.deny
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Export permissions to JSON (legacy format)
|
|
1059
|
+
*/
|
|
1060
|
+
exportPermissions() {
|
|
1061
|
+
return this.wildcardRules.exportToConfig();
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Import permissions from JSON (legacy format)
|
|
1065
|
+
*/
|
|
1066
|
+
importPermissions(config, merge = false) {
|
|
1067
|
+
if (!merge) {
|
|
1068
|
+
this.wildcardRules.clearRules();
|
|
1069
|
+
}
|
|
1070
|
+
this.wildcardRules.importFromConfig(config, "config");
|
|
1071
|
+
this.savePermissions();
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Test a pattern against sample targets
|
|
1075
|
+
*/
|
|
1076
|
+
testPattern(pattern, targets) {
|
|
1077
|
+
const defaultTargets = [
|
|
1078
|
+
"npm install",
|
|
1079
|
+
"npm test",
|
|
1080
|
+
"git status",
|
|
1081
|
+
"mcp__server__tool",
|
|
1082
|
+
"Read",
|
|
1083
|
+
"Write",
|
|
1084
|
+
"Edit"
|
|
1085
|
+
];
|
|
1086
|
+
return this.wildcardRules.testPattern(pattern, targets || defaultTargets);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Get diagnostics for a specific rule
|
|
1090
|
+
*/
|
|
1091
|
+
getDiagnostics(pattern) {
|
|
1092
|
+
return this.wildcardRules.getDiagnostics(pattern);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Get diagnostics for all rules
|
|
1096
|
+
*/
|
|
1097
|
+
getAllDiagnostics() {
|
|
1098
|
+
return this.wildcardRules.getAllDiagnostics();
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Get unreachable rules (rules that can never match)
|
|
1102
|
+
*/
|
|
1103
|
+
getUnreachableRules() {
|
|
1104
|
+
return this.wildcardRules.getUnreachableRules();
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Add a before-check hook
|
|
1108
|
+
*/
|
|
1109
|
+
addBeforeHook(hook) {
|
|
1110
|
+
this.wildcardRules.addBeforeHook(hook);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Add an after-check hook
|
|
1114
|
+
*/
|
|
1115
|
+
addAfterHook(hook) {
|
|
1116
|
+
this.wildcardRules.addAfterHook(hook);
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Set allowUnsandboxedCommands flag
|
|
1120
|
+
*/
|
|
1121
|
+
setAllowUnsandboxedCommands(allow) {
|
|
1122
|
+
this.wildcardRules = getWildcardPermissionRules({
|
|
1123
|
+
allowUnsandboxedCommands: allow
|
|
1124
|
+
});
|
|
1125
|
+
this.updateSettingsExperimental({
|
|
1126
|
+
allowUnsandboxedCommands: allow
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Set disallowed tools list
|
|
1131
|
+
*/
|
|
1132
|
+
setDisallowedTools(tools) {
|
|
1133
|
+
this.wildcardRules = getWildcardPermissionRules({
|
|
1134
|
+
disallowedTools: tools
|
|
1135
|
+
});
|
|
1136
|
+
this.updateSettingsExperimental({
|
|
1137
|
+
disallowedTools: tools
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Get current configuration
|
|
1142
|
+
*/
|
|
1143
|
+
getConfig() {
|
|
1144
|
+
this.wildcardRules.getStats();
|
|
1145
|
+
return {
|
|
1146
|
+
disallowedTools: [],
|
|
1147
|
+
maxCacheSize: 1e3,
|
|
1148
|
+
enableDiagnostics: true
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Search for rules by pattern
|
|
1153
|
+
*/
|
|
1154
|
+
searchRules(query) {
|
|
1155
|
+
const lowerQuery = query.toLowerCase();
|
|
1156
|
+
return this.wildcardRules.getAllRules().filter(
|
|
1157
|
+
(rule) => rule.pattern.toLowerCase().includes(lowerQuery) || rule.description?.toLowerCase().includes(lowerQuery) || rule.category.toLowerCase().includes(lowerQuery)
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Validate a pattern string
|
|
1162
|
+
*/
|
|
1163
|
+
validatePattern(pattern) {
|
|
1164
|
+
return this.wildcardRules.validatePattern(pattern);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get pattern type
|
|
1168
|
+
*/
|
|
1169
|
+
getPatternType(pattern) {
|
|
1170
|
+
return this.wildcardRules.getPatternType(pattern);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Get cache statistics
|
|
1174
|
+
*/
|
|
1175
|
+
getCacheStats() {
|
|
1176
|
+
return this.wildcardRules.getCacheStats();
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Clear the pattern cache
|
|
1180
|
+
*/
|
|
1181
|
+
clearCache() {
|
|
1182
|
+
this.wildcardRules.clearCache();
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Update experimental settings in settings.json
|
|
1186
|
+
*/
|
|
1187
|
+
updateSettingsExperimental(updates) {
|
|
1188
|
+
try {
|
|
1189
|
+
let settings = {};
|
|
1190
|
+
if (existsSync(this.settingsPath)) {
|
|
1191
|
+
const content = readFileSync(this.settingsPath, "utf-8");
|
|
1192
|
+
settings = JSON.parse(content);
|
|
1193
|
+
}
|
|
1194
|
+
if (!settings.experimental) {
|
|
1195
|
+
settings.experimental = {};
|
|
1196
|
+
}
|
|
1197
|
+
Object.assign(settings.experimental, updates);
|
|
1198
|
+
writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
console.error("Failed to update settings.json:", error);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Infer category from pattern
|
|
1205
|
+
*/
|
|
1206
|
+
inferCategory(pattern) {
|
|
1207
|
+
if (pattern.startsWith("Bash(")) {
|
|
1208
|
+
return "bash";
|
|
1209
|
+
}
|
|
1210
|
+
if (pattern.startsWith("mcp__")) {
|
|
1211
|
+
return "mcp";
|
|
1212
|
+
}
|
|
1213
|
+
if (pattern.startsWith("http://") || pattern.startsWith("https://") || pattern.startsWith("ws://") || pattern.startsWith("wss://")) {
|
|
1214
|
+
return "network";
|
|
1215
|
+
}
|
|
1216
|
+
if (pattern.startsWith("/")) {
|
|
1217
|
+
return "filesystem";
|
|
1218
|
+
}
|
|
1219
|
+
if (["Read", "Write", "Edit", "Bash", "WebSearch"].includes(pattern)) {
|
|
1220
|
+
return "tool";
|
|
1221
|
+
}
|
|
1222
|
+
if (["init", "update", "doctor", "permissions"].includes(pattern)) {
|
|
1223
|
+
return "command";
|
|
1224
|
+
}
|
|
1225
|
+
return "command";
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Get sample patterns for reference
|
|
1229
|
+
*/
|
|
1230
|
+
static getSamplePatterns() {
|
|
1231
|
+
return SAMPLE_PATTERNS;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
let instance = null;
|
|
1235
|
+
function getPermissionManager(configPath, settingsPath) {
|
|
1236
|
+
if (!instance) {
|
|
1237
|
+
instance = new PermissionManager(configPath, settingsPath);
|
|
1238
|
+
}
|
|
1239
|
+
return instance;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export { getPermissionManager as g };
|