codebot-ai 1.7.0 → 1.9.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/dist/agent.d.ts CHANGED
@@ -2,6 +2,8 @@ import { Message, AgentEvent, LLMProvider } from './types';
2
2
  import { AuditLogger } from './audit';
3
3
  import { PolicyEnforcer } from './policy';
4
4
  import { TokenTracker } from './telemetry';
5
+ import { MetricsCollector } from './metrics';
6
+ import { RiskScorer } from './risk';
5
7
  export declare class Agent {
6
8
  private provider;
7
9
  private tools;
@@ -15,6 +17,9 @@ export declare class Agent {
15
17
  private auditLogger;
16
18
  private policyEnforcer;
17
19
  private tokenTracker;
20
+ private metricsCollector;
21
+ private riskScorer;
22
+ private branchCreated;
18
23
  private askPermission;
19
24
  private onMessage?;
20
25
  constructor(opts: {
@@ -43,6 +48,10 @@ export declare class Agent {
43
48
  getPolicyEnforcer(): PolicyEnforcer;
44
49
  /** Get the audit logger for verification */
45
50
  getAuditLogger(): AuditLogger;
51
+ /** Get the metrics collector for session metrics */
52
+ getMetrics(): MetricsCollector;
53
+ /** Get the risk scorer for risk assessment history */
54
+ getRiskScorer(): RiskScorer;
46
55
  /**
47
56
  * Validate and repair message history to prevent OpenAI 400 errors.
48
57
  * Handles three types of corruption:
@@ -54,6 +63,15 @@ export declare class Agent {
54
63
  * or session resume corruption.
55
64
  */
56
65
  private repairToolCallMessages;
66
+ /**
67
+ * Auto-create a feature branch when always_branch is enabled and on main/master.
68
+ * Called before the first write/edit operation. Fail-open: if branching fails, continue.
69
+ */
70
+ private ensureBranch;
71
+ /** Sanitize user message into a branch-safe slug. */
72
+ private sanitizeSlug;
73
+ /** Check capability-based restrictions before tool execution. Returns reason string or null. */
74
+ private checkToolCapabilities;
57
75
  private buildSystemPrompt;
58
76
  }
59
77
  //# sourceMappingURL=agent.d.ts.map
package/dist/agent.js CHANGED
@@ -48,6 +48,8 @@ const rate_limiter_1 = require("./rate-limiter");
48
48
  const audit_1 = require("./audit");
49
49
  const policy_1 = require("./policy");
50
50
  const telemetry_1 = require("./telemetry");
51
+ const metrics_1 = require("./metrics");
52
+ const risk_1 = require("./risk");
51
53
  /** Lightweight schema validation — returns error string or null if valid */
52
54
  function validateToolArgs(args, schema) {
53
55
  const props = schema.properties;
@@ -104,15 +106,18 @@ class Agent {
104
106
  auditLogger;
105
107
  policyEnforcer;
106
108
  tokenTracker;
109
+ metricsCollector;
110
+ riskScorer;
111
+ branchCreated = false;
107
112
  askPermission;
108
113
  onMessage;
109
114
  constructor(opts) {
110
115
  this.provider = opts.provider;
111
116
  this.model = opts.model;
112
- this.tools = new tools_1.ToolRegistry(process.cwd());
113
- this.context = new manager_1.ContextManager(opts.model, opts.provider);
114
- // Load policy
117
+ // Load policy FIRST — tools need it for filesystem/git enforcement
115
118
  this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
119
+ this.tools = new tools_1.ToolRegistry(process.cwd(), this.policyEnforcer);
120
+ this.context = new manager_1.ContextManager(opts.model, opts.provider);
116
121
  // Use policy-defined max iterations as default, CLI overrides
117
122
  this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
118
123
  this.autoApprove = opts.autoApprove || false;
@@ -123,6 +128,8 @@ class Agent {
123
128
  this.auditLogger = new audit_1.AuditLogger();
124
129
  // Token & cost tracking
125
130
  this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
131
+ this.metricsCollector = new metrics_1.MetricsCollector();
132
+ this.riskScorer = new risk_1.RiskScorer();
126
133
  const costLimit = this.policyEnforcer.getCostLimitUsd();
127
134
  if (costLimit > 0)
128
135
  this.tokenTracker.setCostLimit(costLimit);
@@ -195,6 +202,9 @@ class Agent {
195
202
  // Track tokens and cost
196
203
  if (event.usage) {
197
204
  this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
205
+ this.metricsCollector.increment('llm_requests_total');
206
+ this.metricsCollector.increment('llm_tokens_total', { direction: 'input' }, event.usage.inputTokens || 0);
207
+ this.metricsCollector.increment('llm_tokens_total', { direction: 'output' }, event.usage.outputTokens || 0);
198
208
  }
199
209
  yield { type: 'usage', usage: event.usage };
200
210
  break;
@@ -281,10 +291,17 @@ class Agent {
281
291
  prepared.push({ tc, tool, args, denied: false, error: `Error: ${validationError} for ${toolName}` });
282
292
  continue;
283
293
  }
284
- yield { type: 'tool_call', toolCall: { name: toolName, args } };
285
- // Permission check: policy override > tool default
294
+ // Compute risk score before execution
286
295
  const policyPermission = this.policyEnforcer.getToolPermission(toolName);
287
296
  const effectivePermission = policyPermission || tool.permission;
297
+ const riskAssessment = this.riskScorer.assess(toolName, args, effectivePermission);
298
+ yield { type: 'tool_call', toolCall: { name: toolName, args }, risk: { score: riskAssessment.score, level: riskAssessment.level } };
299
+ // Log risk breakdown for high-risk calls
300
+ if (riskAssessment.score > 50) {
301
+ const breakdown = riskAssessment.factors.map(f => `${f.name}=${f.rawScore}`).join(', ');
302
+ this.auditLogger.log({ tool: toolName, action: 'execute', args, result: `risk:${riskAssessment.score}`, reason: breakdown });
303
+ }
304
+ // Permission check: policy override > tool default
288
305
  const needsPermission = effectivePermission === 'always-ask' ||
289
306
  (effectivePermission === 'prompt' && !this.autoApprove);
290
307
  let denied = false;
@@ -293,6 +310,7 @@ class Agent {
293
310
  if (!approved) {
294
311
  denied = true;
295
312
  this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
313
+ this.metricsCollector.increment('permission_denials_total', { tool: toolName });
296
314
  }
297
315
  }
298
316
  prepared.push({ tc, tool, args, denied });
@@ -323,27 +341,53 @@ class Agent {
323
341
  parallelBatch.push(item);
324
342
  }
325
343
  }
326
- // Helper to execute a single tool with cache + rate limiting
344
+ // Helper to execute a single tool with cache + rate limiting + metrics
327
345
  const executeTool = async (prep) => {
328
346
  const toolName = prep.tc.function.name;
347
+ const toolStartTime = Date.now();
348
+ // Auto-branch on first write/edit when always_branch is enabled (v1.8.0)
349
+ if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
350
+ const branchName = await this.ensureBranch();
351
+ if (branchName) {
352
+ this.auditLogger.log({ tool: 'git', action: 'execute', args: { branch: branchName }, result: 'auto-branch' });
353
+ }
354
+ }
355
+ // Capability check: fine-grained resource restrictions (v1.8.0)
356
+ const capBlock = this.checkToolCapabilities(toolName, prep.args);
357
+ if (capBlock) {
358
+ this.auditLogger.log({ tool: toolName, action: 'capability_block', args: prep.args, reason: capBlock });
359
+ this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'capability' });
360
+ return { content: `Error: ${capBlock}`, is_error: true };
361
+ }
329
362
  // Check cache first
330
363
  if (prep.tool.cacheable) {
331
364
  const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
332
365
  const cached = this.cache.get(cacheKey);
333
366
  if (cached !== null) {
367
+ this.metricsCollector.increment('cache_hits_total', { tool: toolName });
334
368
  return { content: cached };
335
369
  }
370
+ this.metricsCollector.increment('cache_misses_total', { tool: toolName });
336
371
  }
337
372
  // Rate limit
338
373
  await this.rateLimiter.throttle(toolName);
339
374
  try {
340
375
  const output = await prep.tool.execute(prep.args);
376
+ // Record tool latency
377
+ const latencyMs = Date.now() - toolStartTime;
378
+ this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
379
+ this.metricsCollector.increment('tool_calls_total', { tool: toolName });
341
380
  // Audit log: successful execution
342
381
  this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
343
382
  // Telemetry: track tool calls and file modifications
344
383
  this.tokenTracker.recordToolCall();
345
384
  if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
346
385
  this.tokenTracker.recordFileModified(prep.args.path);
386
+ this.metricsCollector.increment('files_written_total', { tool: toolName });
387
+ }
388
+ // Track commands executed
389
+ if (toolName === 'execute') {
390
+ this.metricsCollector.increment('commands_executed_total');
347
391
  }
348
392
  // Store in cache for cacheable tools
349
393
  if (prep.tool.cacheable) {
@@ -359,11 +403,16 @@ class Agent {
359
403
  // Audit log: check if tool returned a security block
360
404
  if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
361
405
  this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
406
+ this.metricsCollector.increment('security_blocks_total', { tool: toolName, type: 'security' });
362
407
  }
363
408
  return { content: output };
364
409
  }
365
410
  catch (err) {
366
411
  const errMsg = err instanceof Error ? err.message : String(err);
412
+ // Record latency even on error
413
+ const latencyMs = Date.now() - toolStartTime;
414
+ this.metricsCollector.observe('tool_latency_seconds', latencyMs / 1000, { tool: toolName });
415
+ this.metricsCollector.increment('errors_total', { tool: toolName });
367
416
  // Audit log: error
368
417
  this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
369
418
  return { content: `Error: ${errMsg}`, is_error: true };
@@ -434,6 +483,14 @@ class Agent {
434
483
  getAuditLogger() {
435
484
  return this.auditLogger;
436
485
  }
486
+ /** Get the metrics collector for session metrics */
487
+ getMetrics() {
488
+ return this.metricsCollector;
489
+ }
490
+ /** Get the risk scorer for risk assessment history */
491
+ getRiskScorer() {
492
+ return this.riskScorer;
493
+ }
437
494
  /**
438
495
  * Validate and repair message history to prevent OpenAI 400 errors.
439
496
  * Handles three types of corruption:
@@ -501,6 +558,78 @@ class Agent {
501
558
  }
502
559
  }
503
560
  }
561
+ /**
562
+ * Auto-create a feature branch when always_branch is enabled and on main/master.
563
+ * Called before the first write/edit operation. Fail-open: if branching fails, continue.
564
+ */
565
+ async ensureBranch() {
566
+ if (this.branchCreated)
567
+ return null;
568
+ if (!this.policyEnforcer.shouldAlwaysBranch())
569
+ return null;
570
+ try {
571
+ const { execSync } = require('child_process');
572
+ const cwd = process.cwd();
573
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
574
+ cwd, encoding: 'utf-8', timeout: 5000,
575
+ }).trim();
576
+ if (currentBranch !== 'main' && currentBranch !== 'master') {
577
+ this.branchCreated = true;
578
+ return null; // Already on a feature branch
579
+ }
580
+ // Generate branch name from first user message
581
+ const firstUserMsg = this.messages.find(m => m.role === 'user');
582
+ const prefix = this.policyEnforcer.getBranchPrefix();
583
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
584
+ const slug = this.sanitizeSlug(firstUserMsg?.content || 'task');
585
+ const branchName = `${prefix}${timestamp}-${slug}`;
586
+ execSync(`git checkout -b "${branchName}"`, {
587
+ cwd, encoding: 'utf-8', timeout: 10000,
588
+ });
589
+ this.branchCreated = true;
590
+ return branchName;
591
+ }
592
+ catch {
593
+ // Don't block the operation if branching fails (not in a git repo, etc.)
594
+ this.branchCreated = true; // Don't retry
595
+ return null;
596
+ }
597
+ }
598
+ /** Sanitize user message into a branch-safe slug. */
599
+ sanitizeSlug(message) {
600
+ return message
601
+ .toLowerCase()
602
+ .replace(/[^a-z0-9\s-]/g, '')
603
+ .replace(/\s+/g, '-')
604
+ .substring(0, 30)
605
+ .replace(/-+$/, '') || 'task';
606
+ }
607
+ /** Check capability-based restrictions before tool execution. Returns reason string or null. */
608
+ checkToolCapabilities(toolName, args) {
609
+ // fs_write check for write/edit tools
610
+ if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && args.path) {
611
+ const check = this.policyEnforcer.checkCapability(toolName, 'fs_write', args.path);
612
+ if (!check.allowed)
613
+ return check.reason || 'Capability blocked';
614
+ }
615
+ // shell_commands check for execute tool
616
+ if (toolName === 'execute' && args.command) {
617
+ const check = this.policyEnforcer.checkCapability(toolName, 'shell_commands', args.command);
618
+ if (!check.allowed)
619
+ return check.reason || 'Capability blocked';
620
+ }
621
+ // net_access check for web tools
622
+ if ((toolName === 'web_fetch' || toolName === 'http_client') && args.url) {
623
+ try {
624
+ const domain = new URL(args.url).hostname;
625
+ const check = this.policyEnforcer.checkCapability(toolName, 'net_access', domain);
626
+ if (!check.allowed)
627
+ return check.reason || 'Capability blocked';
628
+ }
629
+ catch { /* invalid URL handled by the tool itself */ }
630
+ }
631
+ return null;
632
+ }
504
633
  buildSystemPrompt(supportsTools) {
505
634
  let repoMap = '';
506
635
  try {
package/dist/audit.d.ts CHANGED
@@ -15,7 +15,7 @@ export interface AuditEntry {
15
15
  sessionId: string;
16
16
  sequence: number;
17
17
  tool: string;
18
- action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
18
+ action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block' | 'capability_block';
19
19
  args: Record<string, unknown>;
20
20
  result?: string;
21
21
  reason?: string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Capability-Based Tool Permissions for CodeBot v1.8.0
3
+ *
4
+ * Fine-grained, per-tool resource restrictions.
5
+ * Configured via .codebot/policy.json → tools.capabilities.
6
+ *
7
+ * Example:
8
+ * {
9
+ * "execute": {
10
+ * "shell_commands": ["npm", "node", "git", "tsc"],
11
+ * "max_output_kb": 500
12
+ * },
13
+ * "write_file": {
14
+ * "fs_write": ["./src/**", "./tests/**"]
15
+ * }
16
+ * }
17
+ */
18
+ export interface ToolCapabilities {
19
+ fs_read?: string[];
20
+ fs_write?: string[];
21
+ net_access?: string[];
22
+ shell_commands?: string[];
23
+ max_output_kb?: number;
24
+ }
25
+ export type CapabilityConfig = Record<string, ToolCapabilities>;
26
+ export declare class CapabilityChecker {
27
+ private capabilities;
28
+ private projectRoot;
29
+ constructor(capabilities: CapabilityConfig, projectRoot: string);
30
+ /** Get capabilities for a tool. undefined = no restrictions. */
31
+ getToolCapabilities(toolName: string): ToolCapabilities | undefined;
32
+ /** Check if a specific capability is allowed. */
33
+ checkCapability(toolName: string, capabilityType: keyof ToolCapabilities, value: string | number): {
34
+ allowed: boolean;
35
+ reason?: string;
36
+ };
37
+ /** Check if a path matches any of the allowed glob patterns. */
38
+ private checkGlobs;
39
+ /** Check if a domain is in the allowed list. */
40
+ private checkDomain;
41
+ /** Check if a command starts with an allowed prefix. */
42
+ private checkCommandPrefix;
43
+ /** Check if output size is within the cap. */
44
+ private checkOutputSize;
45
+ /** Simple glob matching (** = any depth, * = one segment). */
46
+ private matchGlob;
47
+ }
48
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Capability-Based Tool Permissions for CodeBot v1.8.0
4
+ *
5
+ * Fine-grained, per-tool resource restrictions.
6
+ * Configured via .codebot/policy.json → tools.capabilities.
7
+ *
8
+ * Example:
9
+ * {
10
+ * "execute": {
11
+ * "shell_commands": ["npm", "node", "git", "tsc"],
12
+ * "max_output_kb": 500
13
+ * },
14
+ * "write_file": {
15
+ * "fs_write": ["./src/**", "./tests/**"]
16
+ * }
17
+ * }
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.CapabilityChecker = void 0;
54
+ const path = __importStar(require("path"));
55
+ // ── Capability Checker ──
56
+ class CapabilityChecker {
57
+ capabilities;
58
+ projectRoot;
59
+ constructor(capabilities, projectRoot) {
60
+ this.capabilities = capabilities;
61
+ this.projectRoot = projectRoot;
62
+ }
63
+ /** Get capabilities for a tool. undefined = no restrictions. */
64
+ getToolCapabilities(toolName) {
65
+ return this.capabilities[toolName];
66
+ }
67
+ /** Check if a specific capability is allowed. */
68
+ checkCapability(toolName, capabilityType, value) {
69
+ const caps = this.capabilities[toolName];
70
+ if (!caps)
71
+ return { allowed: true }; // No caps defined = unrestricted
72
+ switch (capabilityType) {
73
+ case 'fs_read':
74
+ return this.checkGlobs(caps.fs_read, value, 'fs_read', toolName);
75
+ case 'fs_write':
76
+ return this.checkGlobs(caps.fs_write, value, 'fs_write', toolName);
77
+ case 'net_access':
78
+ return this.checkDomain(caps.net_access, value, toolName);
79
+ case 'shell_commands':
80
+ return this.checkCommandPrefix(caps.shell_commands, value, toolName);
81
+ case 'max_output_kb':
82
+ return this.checkOutputSize(caps.max_output_kb, value, toolName);
83
+ default:
84
+ return { allowed: true };
85
+ }
86
+ }
87
+ /** Check if a path matches any of the allowed glob patterns. */
88
+ checkGlobs(patterns, filePath, capName, toolName) {
89
+ if (!patterns || patterns.length === 0)
90
+ return { allowed: true };
91
+ const resolved = path.resolve(filePath);
92
+ const relative = path.relative(this.projectRoot, resolved);
93
+ // Don't restrict paths outside the project (those are handled by security.ts)
94
+ if (relative.startsWith('..'))
95
+ return { allowed: true };
96
+ for (const pattern of patterns) {
97
+ if (this.matchGlob(relative, pattern)) {
98
+ return { allowed: true };
99
+ }
100
+ }
101
+ return {
102
+ allowed: false,
103
+ reason: `Tool "${toolName}" ${capName} capability blocks "${relative}" (allowed: ${patterns.join(', ')})`,
104
+ };
105
+ }
106
+ /** Check if a domain is in the allowed list. */
107
+ checkDomain(allowed, domain, toolName) {
108
+ if (allowed === undefined)
109
+ return { allowed: true }; // undefined = unrestricted
110
+ if (allowed.length === 0) {
111
+ // Empty array = no network access allowed
112
+ return {
113
+ allowed: false,
114
+ reason: `Tool "${toolName}" has no allowed network domains`,
115
+ };
116
+ }
117
+ if (allowed.includes('*'))
118
+ return { allowed: true };
119
+ const normalizedDomain = domain.toLowerCase();
120
+ for (const d of allowed) {
121
+ const nd = d.toLowerCase();
122
+ if (normalizedDomain === nd)
123
+ return { allowed: true };
124
+ if (normalizedDomain.endsWith('.' + nd))
125
+ return { allowed: true };
126
+ }
127
+ return {
128
+ allowed: false,
129
+ reason: `Tool "${toolName}" cannot access domain "${domain}" (allowed: ${allowed.join(', ')})`,
130
+ };
131
+ }
132
+ /** Check if a command starts with an allowed prefix. */
133
+ checkCommandPrefix(allowed, command, toolName) {
134
+ if (!allowed || allowed.length === 0)
135
+ return { allowed: true };
136
+ const cmd = command.trim();
137
+ for (const prefix of allowed) {
138
+ if (cmd === prefix || cmd.startsWith(prefix + ' ')) {
139
+ return { allowed: true };
140
+ }
141
+ }
142
+ // Extract first word for the error message
143
+ const firstWord = cmd.split(/\s+/)[0] || cmd.substring(0, 30);
144
+ return {
145
+ allowed: false,
146
+ reason: `Tool "${toolName}" cannot run "${firstWord}" (allowed commands: ${allowed.join(', ')})`,
147
+ };
148
+ }
149
+ /** Check if output size is within the cap. */
150
+ checkOutputSize(maxKb, actualKb, toolName) {
151
+ if (maxKb === undefined || maxKb <= 0)
152
+ return { allowed: true };
153
+ if (actualKb <= maxKb)
154
+ return { allowed: true };
155
+ return {
156
+ allowed: false,
157
+ reason: `Tool "${toolName}" output ${actualKb}KB exceeds cap of ${maxKb}KB`,
158
+ };
159
+ }
160
+ /** Simple glob matching (** = any depth, * = one segment). */
161
+ matchGlob(relativePath, pattern) {
162
+ const cleanPattern = pattern.replace(/^\.\//, '');
163
+ const cleanPath = relativePath.replace(/^\.\//, '');
164
+ // Exact match
165
+ if (cleanPath === cleanPattern)
166
+ return true;
167
+ // Prefix match (directory)
168
+ if (cleanPath.startsWith(cleanPattern + '/'))
169
+ return true;
170
+ if (cleanPath.startsWith(cleanPattern + path.sep))
171
+ return true;
172
+ // Glob expansion
173
+ if (pattern.includes('*')) {
174
+ const regex = new RegExp('^' +
175
+ cleanPattern
176
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
177
+ .replace(/\*\*/g, '<<GLOBSTAR>>')
178
+ .replace(/\*/g, '[^/]*')
179
+ .replace(/<<GLOBSTAR>>/g, '.*') +
180
+ '$');
181
+ return regex.test(cleanPath);
182
+ }
183
+ return false;
184
+ }
185
+ }
186
+ exports.CapabilityChecker = CapabilityChecker;
187
+ //# sourceMappingURL=capabilities.js.map