wolverine-ai 1.0.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.
Files changed (79) hide show
  1. package/PLATFORM.md +442 -0
  2. package/README.md +475 -0
  3. package/SERVER_BEST_PRACTICES.md +62 -0
  4. package/TELEMETRY.md +108 -0
  5. package/bin/wolverine.js +95 -0
  6. package/examples/01-basic-typo.js +31 -0
  7. package/examples/02-multi-file/routes/users.js +15 -0
  8. package/examples/02-multi-file/server.js +25 -0
  9. package/examples/03-syntax-error.js +23 -0
  10. package/examples/04-secret-leak.js +14 -0
  11. package/examples/05-expired-key.js +27 -0
  12. package/examples/06-json-config/config.json +13 -0
  13. package/examples/06-json-config/server.js +28 -0
  14. package/examples/07-rate-limit-loop.js +11 -0
  15. package/examples/08-sandbox-escape.js +20 -0
  16. package/examples/buggy-server.js +39 -0
  17. package/examples/demos/01-basic-typo/index.js +20 -0
  18. package/examples/demos/01-basic-typo/routes/api.js +13 -0
  19. package/examples/demos/01-basic-typo/routes/health.js +4 -0
  20. package/examples/demos/02-multi-file/index.js +24 -0
  21. package/examples/demos/02-multi-file/routes/api.js +13 -0
  22. package/examples/demos/02-multi-file/routes/health.js +4 -0
  23. package/examples/demos/03-syntax-error/index.js +18 -0
  24. package/examples/demos/04-secret-leak/index.js +16 -0
  25. package/examples/demos/05-expired-key/index.js +21 -0
  26. package/examples/demos/06-json-config/config.json +9 -0
  27. package/examples/demos/06-json-config/index.js +20 -0
  28. package/examples/demos/07-null-crash/index.js +16 -0
  29. package/examples/run-demo.js +110 -0
  30. package/package.json +67 -0
  31. package/server/config/settings.json +62 -0
  32. package/server/index.js +33 -0
  33. package/server/routes/api.js +12 -0
  34. package/server/routes/health.js +16 -0
  35. package/server/routes/time.js +12 -0
  36. package/src/agent/agent-engine.js +727 -0
  37. package/src/agent/goal-loop.js +140 -0
  38. package/src/agent/research-agent.js +120 -0
  39. package/src/agent/sub-agents.js +176 -0
  40. package/src/backup/backup-manager.js +321 -0
  41. package/src/brain/brain.js +315 -0
  42. package/src/brain/embedder.js +131 -0
  43. package/src/brain/function-map.js +263 -0
  44. package/src/brain/vector-store.js +267 -0
  45. package/src/core/ai-client.js +387 -0
  46. package/src/core/cluster-manager.js +144 -0
  47. package/src/core/config.js +89 -0
  48. package/src/core/error-parser.js +87 -0
  49. package/src/core/health-monitor.js +129 -0
  50. package/src/core/models.js +132 -0
  51. package/src/core/patcher.js +55 -0
  52. package/src/core/runner.js +464 -0
  53. package/src/core/system-info.js +141 -0
  54. package/src/core/verifier.js +146 -0
  55. package/src/core/wolverine.js +290 -0
  56. package/src/dashboard/server.js +1332 -0
  57. package/src/index.js +94 -0
  58. package/src/logger/event-logger.js +237 -0
  59. package/src/logger/pricing.js +96 -0
  60. package/src/logger/repair-history.js +109 -0
  61. package/src/logger/token-tracker.js +277 -0
  62. package/src/mcp/mcp-client.js +224 -0
  63. package/src/mcp/mcp-registry.js +228 -0
  64. package/src/mcp/mcp-security.js +152 -0
  65. package/src/monitor/perf-monitor.js +300 -0
  66. package/src/monitor/process-monitor.js +231 -0
  67. package/src/monitor/route-prober.js +191 -0
  68. package/src/notifications/notifier.js +227 -0
  69. package/src/platform/heartbeat.js +93 -0
  70. package/src/platform/queue.js +53 -0
  71. package/src/platform/register.js +64 -0
  72. package/src/platform/telemetry.js +76 -0
  73. package/src/security/admin-auth.js +150 -0
  74. package/src/security/injection-detector.js +174 -0
  75. package/src/security/rate-limiter.js +152 -0
  76. package/src/security/sandbox.js +128 -0
  77. package/src/security/secret-redactor.js +217 -0
  78. package/src/skills/skill-registry.js +129 -0
  79. package/src/skills/sql.js +375 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Rate Limiter — prevents error explosion from draining API budget.
3
+ *
4
+ * Uses a sliding window + token bucket approach:
5
+ * - Tracks API calls in a rolling time window
6
+ * - Enforces max calls per window
7
+ * - Enforces minimum gap between calls
8
+ * - Detects error loops (same error repeating) and backs off exponentially
9
+ */
10
+ class RateLimiter {
11
+ constructor(options = {}) {
12
+ // Max API calls within the rolling window
13
+ this.maxCallsPerWindow = options.maxCallsPerWindow || 10;
14
+ // Window size in ms (default: 10 minutes)
15
+ this.windowMs = options.windowMs || 10 * 60 * 1000;
16
+ // Minimum gap between calls in ms (default: 5 seconds)
17
+ this.minGapMs = options.minGapMs || 5000;
18
+ // Max cost per hour in estimated tokens (rough budget protection)
19
+ this.maxTokensPerHour = options.maxTokensPerHour || 100000;
20
+
21
+ // Internal state
22
+ this._callLog = []; // timestamps of recent calls
23
+ this._tokenLog = []; // { timestamp, tokens } for budget tracking
24
+ this._errorSignatures = {}; // signature -> { count, lastSeen, backoffMs }
25
+ this._lastCallTime = 0;
26
+ }
27
+
28
+ /**
29
+ * Check if a call is allowed. Returns { allowed, reason, waitMs }.
30
+ * If not allowed, waitMs indicates how long to wait before retrying.
31
+ */
32
+ check(errorSignature) {
33
+ const now = Date.now();
34
+ this._pruneOldEntries(now);
35
+
36
+ // 1. Minimum gap between calls
37
+ const sinceLast = now - this._lastCallTime;
38
+ if (sinceLast < this.minGapMs) {
39
+ const waitMs = this.minGapMs - sinceLast;
40
+ return { allowed: false, reason: `Rate limit: minimum ${this.minGapMs}ms gap between calls. Wait ${waitMs}ms.`, waitMs };
41
+ }
42
+
43
+ // 2. Sliding window limit
44
+ if (this._callLog.length >= this.maxCallsPerWindow) {
45
+ const oldestInWindow = this._callLog[0];
46
+ const waitMs = oldestInWindow + this.windowMs - now;
47
+ return { allowed: false, reason: `Rate limit: ${this.maxCallsPerWindow} calls per ${this.windowMs / 1000}s window exceeded.`, waitMs };
48
+ }
49
+
50
+ // 3. Hourly token budget
51
+ const hourAgo = now - 3600000;
52
+ const recentTokens = this._tokenLog
53
+ .filter(e => e.timestamp > hourAgo)
54
+ .reduce((sum, e) => sum + e.tokens, 0);
55
+ if (recentTokens >= this.maxTokensPerHour) {
56
+ return { allowed: false, reason: `Budget limit: estimated ${recentTokens} tokens used in the last hour (max: ${this.maxTokensPerHour}).`, waitMs: 60000 };
57
+ }
58
+
59
+ // 4. Error loop detection — same error repeating gets exponential backoff
60
+ if (errorSignature) {
61
+ const sig = this._errorSignatures[errorSignature];
62
+ if (sig && sig.count >= 2) {
63
+ const timeSinceLast = now - sig.lastSeen;
64
+ if (timeSinceLast < sig.backoffMs) {
65
+ const waitMs = sig.backoffMs - timeSinceLast;
66
+ return {
67
+ allowed: false,
68
+ reason: `Error loop detected: same error seen ${sig.count} times. Backing off ${sig.backoffMs / 1000}s.`,
69
+ waitMs,
70
+ };
71
+ }
72
+ }
73
+ }
74
+
75
+ return { allowed: true };
76
+ }
77
+
78
+ /**
79
+ * Record a call was made. Call this after a successful API request.
80
+ */
81
+ record(errorSignature, estimatedTokens = 3000) {
82
+ const now = Date.now();
83
+ this._callLog.push(now);
84
+ this._tokenLog.push({ timestamp: now, tokens: estimatedTokens });
85
+ this._lastCallTime = now;
86
+
87
+ // Track error signature for loop detection
88
+ if (errorSignature) {
89
+ if (!this._errorSignatures[errorSignature]) {
90
+ this._errorSignatures[errorSignature] = { count: 0, lastSeen: 0, backoffMs: 5000 };
91
+ }
92
+ const sig = this._errorSignatures[errorSignature];
93
+ sig.count++;
94
+ sig.lastSeen = now;
95
+ // Exponential backoff: 5s, 10s, 20s, 40s, 80s... capped at 5 min
96
+ sig.backoffMs = Math.min(5000 * Math.pow(2, sig.count - 1), 300000);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Clear the error signature tracker (call after a successful fix that doesn't recur).
102
+ */
103
+ clearSignature(errorSignature) {
104
+ delete this._errorSignatures[errorSignature];
105
+ }
106
+
107
+ /**
108
+ * Generate a stable signature for an error (for loop detection).
109
+ */
110
+ static signature(errorMessage, filePath) {
111
+ // Strip line numbers and paths to create a stable key
112
+ const normalized = (errorMessage || "")
113
+ .replace(/:\d+:\d+/g, "")
114
+ .replace(/\(.+\)/g, "")
115
+ .trim();
116
+ return `${filePath || "unknown"}::${normalized}`;
117
+ }
118
+
119
+ /**
120
+ * Get current state for debugging/logging.
121
+ */
122
+ getStats() {
123
+ const now = Date.now();
124
+ this._pruneOldEntries(now);
125
+ return {
126
+ callsInWindow: this._callLog.length,
127
+ maxCallsPerWindow: this.maxCallsPerWindow,
128
+ trackedErrorSignatures: Object.keys(this._errorSignatures).length,
129
+ estimatedTokensLastHour: this._tokenLog
130
+ .filter(e => e.timestamp > now - 3600000)
131
+ .reduce((sum, e) => sum + e.tokens, 0),
132
+ };
133
+ }
134
+
135
+ _pruneOldEntries(now) {
136
+ const windowStart = now - this.windowMs;
137
+ this._callLog = this._callLog.filter(t => t > windowStart);
138
+
139
+ const hourAgo = now - 3600000;
140
+ this._tokenLog = this._tokenLog.filter(e => e.timestamp > hourAgo);
141
+
142
+ // Clean stale error signatures (not seen in 30 min)
143
+ const staleThreshold = now - 30 * 60 * 1000;
144
+ for (const key of Object.keys(this._errorSignatures)) {
145
+ if (this._errorSignatures[key].lastSeen < staleThreshold) {
146
+ delete this._errorSignatures[key];
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ module.exports = { RateLimiter };
@@ -0,0 +1,128 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+
4
+ /**
5
+ * File Sandbox — restricts all file operations to the project directory.
6
+ *
7
+ * Every file read/write in wolverine passes through this gate.
8
+ * Rejects path traversal, symlink escapes, and out-of-bounds access.
9
+ */
10
+ class Sandbox {
11
+ constructor(rootDir) {
12
+ this.rootDir = path.resolve(rootDir);
13
+ }
14
+
15
+ /**
16
+ * Resolve and validate a file path. Throws if the path escapes the sandbox.
17
+ * Returns the resolved absolute path.
18
+ */
19
+ resolve(filePath) {
20
+ // Resolve relative to sandbox root
21
+ const resolved = path.isAbsolute(filePath)
22
+ ? path.resolve(filePath)
23
+ : path.resolve(this.rootDir, filePath);
24
+
25
+ // Normalize separators for consistent comparison
26
+ const normalizedResolved = resolved.replace(/\\/g, "/").toLowerCase();
27
+ const normalizedRoot = this.rootDir.replace(/\\/g, "/").toLowerCase();
28
+
29
+ if (!normalizedResolved.startsWith(normalizedRoot + "/") && normalizedResolved !== normalizedRoot) {
30
+ throw new SandboxViolationError(
31
+ `Path "${filePath}" resolves to "${resolved}" which is outside the sandbox root "${this.rootDir}"`
32
+ );
33
+ }
34
+
35
+ // Check for symlink escape
36
+ if (fs.existsSync(resolved)) {
37
+ const real = fs.realpathSync(resolved);
38
+ const normalizedReal = real.replace(/\\/g, "/").toLowerCase();
39
+ if (!normalizedReal.startsWith(normalizedRoot + "/") && normalizedReal !== normalizedRoot) {
40
+ throw new SandboxViolationError(
41
+ `Path "${filePath}" is a symlink that escapes the sandbox (real path: "${real}")`
42
+ );
43
+ }
44
+ }
45
+
46
+ return resolved;
47
+ }
48
+
49
+ /**
50
+ * Read a file within the sandbox.
51
+ */
52
+ readFile(filePath) {
53
+ const resolved = this.resolve(filePath);
54
+ return fs.readFileSync(resolved, "utf-8");
55
+ }
56
+
57
+ /**
58
+ * Write a file within the sandbox.
59
+ */
60
+ writeFile(filePath, content) {
61
+ const resolved = this.resolve(filePath);
62
+ fs.writeFileSync(resolved, content, "utf-8");
63
+ return resolved;
64
+ }
65
+
66
+ /**
67
+ * Check if a file exists within the sandbox.
68
+ */
69
+ exists(filePath) {
70
+ try {
71
+ const resolved = this.resolve(filePath);
72
+ return fs.existsSync(resolved);
73
+ } catch (e) {
74
+ if (e instanceof SandboxViolationError) return false;
75
+ throw e;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Copy a file within the sandbox.
81
+ */
82
+ copyFile(src, dest) {
83
+ const resolvedSrc = this.resolve(src);
84
+ const resolvedDest = this.resolve(dest);
85
+ fs.copyFileSync(resolvedSrc, resolvedDest);
86
+ return resolvedDest;
87
+ }
88
+
89
+ /**
90
+ * Delete a file within the sandbox.
91
+ */
92
+ unlinkFile(filePath) {
93
+ const resolved = this.resolve(filePath);
94
+ if (fs.existsSync(resolved)) {
95
+ fs.unlinkSync(resolved);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Validate a set of AI-proposed changes — all file paths must be in sandbox.
101
+ * Returns { valid: boolean, violations: string[] }
102
+ */
103
+ validateChanges(changes) {
104
+ const violations = [];
105
+ for (const change of changes) {
106
+ try {
107
+ this.resolve(change.file);
108
+ } catch (e) {
109
+ if (e instanceof SandboxViolationError) {
110
+ violations.push(e.message);
111
+ } else {
112
+ throw e;
113
+ }
114
+ }
115
+ }
116
+ return { valid: violations.length === 0, violations };
117
+ }
118
+ }
119
+
120
+ class SandboxViolationError extends Error {
121
+ constructor(message) {
122
+ super(message);
123
+ this.name = "SandboxViolationError";
124
+ this.code = "SANDBOX_VIOLATION";
125
+ }
126
+ }
127
+
128
+ module.exports = { Sandbox, SandboxViolationError };
@@ -0,0 +1,217 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+
5
+ /**
6
+ * Secret Redactor — prevents secrets from leaking to AI, brain, logs, or dashboard.
7
+ *
8
+ * On init, reads .env.local / .env to build a map of secret values → env key names.
9
+ * Then every string that passes through redact() gets values replaced with
10
+ * `process.env.KEY_NAME` so the AI knows which secret is referenced without
11
+ * seeing the actual value.
12
+ *
13
+ * Also detects common secret patterns (API keys, tokens, connection strings)
14
+ * that might not be in .env files.
15
+ *
16
+ * This runs as a gate on EVERY outbound path:
17
+ * - Before AI calls (error messages, source code, stack traces)
18
+ * - Before brain memory storage
19
+ * - Before event logger persistence
20
+ * - Before dashboard SSE broadcast
21
+ * - Before agent tool results
22
+ */
23
+
24
+ // Patterns that look like secrets even if we don't know them from .env
25
+ const SECRET_PATTERNS = [
26
+ // API keys
27
+ { pattern: /sk-[a-zA-Z0-9_-]{20,}/g, label: "[REDACTED_API_KEY]" },
28
+ { pattern: /sk-proj-[a-zA-Z0-9_-]{20,}/g, label: "[REDACTED_OPENAI_KEY]" },
29
+ { pattern: /ghp_[a-zA-Z0-9]{36,}/g, label: "[REDACTED_GITHUB_TOKEN]" },
30
+ { pattern: /gho_[a-zA-Z0-9]{36,}/g, label: "[REDACTED_GITHUB_OAUTH]" },
31
+ { pattern: /xoxb-[a-zA-Z0-9-]+/g, label: "[REDACTED_SLACK_TOKEN]" },
32
+ { pattern: /xoxp-[a-zA-Z0-9-]+/g, label: "[REDACTED_SLACK_TOKEN]" },
33
+ // AWS
34
+ { pattern: /AKIA[0-9A-Z]{16}/g, label: "[REDACTED_AWS_KEY]" },
35
+ // Connection strings
36
+ { pattern: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s"'`]+/gi, label: "[REDACTED_CONNECTION_STRING]" },
37
+ // Bearer tokens
38
+ { pattern: /Bearer\s+[a-zA-Z0-9._-]{20,}/g, label: "Bearer [REDACTED_TOKEN]" },
39
+ // Generic long hex/base64 secrets (32+ chars of hex or base64)
40
+ { pattern: /(?:password|secret|token|key|credential|auth)['"`]?\s*[:=]\s*['"`]([a-zA-Z0-9+/=_-]{32,})['"`]/gi, label: null }, // handled specially
41
+ // JWT tokens
42
+ { pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, label: "[REDACTED_JWT]" },
43
+ ];
44
+
45
+ class SecretRedactor {
46
+ constructor(projectRoot) {
47
+ this.projectRoot = path.resolve(projectRoot);
48
+ // Map: secret value → env key name (sorted longest first for greedy matching)
49
+ this._secretMap = [];
50
+ // Set of raw secret values for fast detection
51
+ this._secretValues = new Set();
52
+
53
+ this._loadEnvSecrets();
54
+ }
55
+
56
+ /**
57
+ * Redact all secrets from a string.
58
+ * Replaces actual secret values with `process.env.KEY_NAME`.
59
+ * Also catches pattern-based secrets not in .env.
60
+ *
61
+ * @param {string} text - Any string that might contain secrets
62
+ * @returns {string} - Redacted string
63
+ */
64
+ redact(text) {
65
+ if (!text || typeof text !== "string") return text;
66
+
67
+ let result = text;
68
+
69
+ // 1. Replace known env secret values with their key names (longest first)
70
+ for (const { value, key } of this._secretMap) {
71
+ if (result.includes(value)) {
72
+ result = result.split(value).join(`process.env.${key}`);
73
+ }
74
+ }
75
+
76
+ // 2. Apply pattern-based redaction for secrets we don't know about
77
+ for (const { pattern, label } of SECRET_PATTERNS) {
78
+ if (label) {
79
+ result = result.replace(pattern, label);
80
+ } else {
81
+ // Special handling for key=value patterns — replace just the value part
82
+ result = result.replace(pattern, (match, capturedValue) => {
83
+ if (capturedValue && capturedValue.length >= 32) {
84
+ return match.replace(capturedValue, "[REDACTED_SECRET]");
85
+ }
86
+ return match;
87
+ });
88
+ }
89
+ }
90
+
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Redact secrets from a structured object (deep).
96
+ * Walks all string values in objects/arrays.
97
+ */
98
+ redactObject(obj) {
99
+ if (typeof obj === "string") return this.redact(obj);
100
+ if (Array.isArray(obj)) return obj.map(item => this.redactObject(item));
101
+ if (obj && typeof obj === "object") {
102
+ const result = {};
103
+ for (const [key, val] of Object.entries(obj)) {
104
+ result[key] = this.redactObject(val);
105
+ }
106
+ return result;
107
+ }
108
+ return obj;
109
+ }
110
+
111
+ /**
112
+ * Check if a string contains any known secrets. Fast check.
113
+ */
114
+ containsSecrets(text) {
115
+ if (!text) return false;
116
+ for (const { value } of this._secretMap) {
117
+ if (text.includes(value)) return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ /**
123
+ * Reload secrets (call if .env files change).
124
+ */
125
+ reload() {
126
+ this._secretMap = [];
127
+ this._secretValues.clear();
128
+ this._loadEnvSecrets();
129
+ }
130
+
131
+ /**
132
+ * Get stats for logging.
133
+ */
134
+ getStats() {
135
+ return {
136
+ trackedSecrets: this._secretMap.length,
137
+ envFiles: this._envFilesLoaded || 0,
138
+ };
139
+ }
140
+
141
+ // -- Private --
142
+
143
+ _loadEnvSecrets() {
144
+ const envFiles = [".env.local", ".env", ".env.production", ".env.development"];
145
+ let filesLoaded = 0;
146
+
147
+ for (const envFile of envFiles) {
148
+ const filePath = path.join(this.projectRoot, envFile);
149
+ if (!fs.existsSync(filePath)) continue;
150
+
151
+ filesLoaded++;
152
+ const content = fs.readFileSync(filePath, "utf-8");
153
+ const lines = content.split("\n");
154
+
155
+ for (const line of lines) {
156
+ // Skip comments and empty lines
157
+ const trimmed = line.trim();
158
+ if (!trimmed || trimmed.startsWith("#")) continue;
159
+
160
+ const eqIndex = trimmed.indexOf("=");
161
+ if (eqIndex === -1) continue;
162
+
163
+ const key = trimmed.slice(0, eqIndex).trim();
164
+ const value = trimmed.slice(eqIndex + 1).trim()
165
+ // Remove surrounding quotes
166
+ .replace(/^['"]|['"]$/g, "");
167
+
168
+ // Only track values that look like secrets (long enough, not just a port number)
169
+ if (value.length >= 8 && !this._isNonSecret(key, value)) {
170
+ this._secretMap.push({ key, value });
171
+ this._secretValues.add(value);
172
+ }
173
+ }
174
+ }
175
+
176
+ // Also add current process.env values for known secret keys
177
+ const knownSecretKeys = [
178
+ "OPENAI_API_KEY", "DATABASE_URL", "REDIS_URL", "JWT_SECRET",
179
+ "SESSION_SECRET", "AWS_SECRET_ACCESS_KEY", "GITHUB_TOKEN",
180
+ "SLACK_TOKEN", "STRIPE_SECRET_KEY", "SENDGRID_API_KEY",
181
+ ];
182
+
183
+ for (const key of knownSecretKeys) {
184
+ const value = process.env[key];
185
+ if (value && value.length >= 8 && !this._secretValues.has(value)) {
186
+ this._secretMap.push({ key, value });
187
+ this._secretValues.add(value);
188
+ }
189
+ }
190
+
191
+ // Sort longest values first for greedy replacement
192
+ this._secretMap.sort((a, b) => b.value.length - a.value.length);
193
+ this._envFilesLoaded = filesLoaded;
194
+ }
195
+
196
+ /**
197
+ * Heuristic: is this key/value pair NOT a secret?
198
+ * Avoids redacting things like PORT=6969 or NODE_ENV=production.
199
+ */
200
+ _isNonSecret(key, value) {
201
+ const nonSecretKeys = /^(PORT|HOST|NODE_ENV|DEBUG|LOG_LEVEL|TZ|LANG|PATH|HOME|USER|SHELL|TERM|HOSTNAME)$/i;
202
+ if (nonSecretKeys.test(key)) return true;
203
+
204
+ // Model names aren't secrets
205
+ if (key.endsWith("_MODEL")) return true;
206
+
207
+ // Pure numbers aren't secrets
208
+ if (/^\d+$/.test(value)) return true;
209
+
210
+ // Short common values
211
+ if (["true", "false", "development", "production", "staging", "test", "localhost"].includes(value.toLowerCase())) return true;
212
+
213
+ return false;
214
+ }
215
+ }
216
+
217
+ module.exports = { SecretRedactor };
@@ -0,0 +1,129 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+
5
+ /**
6
+ * Skill Registry — discovers skills and injects relevant ones into agent context.
7
+ *
8
+ * claw-code pattern:
9
+ * 1. On startup, scan all skills and build a token-searchable index
10
+ * 2. Before any AI call, match the user's command/error against skills
11
+ * 3. Inject matched skill context into the prompt (not the AI — the prompt)
12
+ *
13
+ * Skills are JS modules in src/skills/ that export:
14
+ * - name: string
15
+ * - description: string
16
+ * - keywords: string[] — tokens for matching
17
+ * - usage: string — code example to inject into agent prompts
18
+ * - middleware?: function — Express middleware to mount
19
+ */
20
+
21
+ class SkillRegistry {
22
+ constructor(options = {}) {
23
+ this.skillsDir = options.skillsDir || path.join(__dirname);
24
+ this._skills = [];
25
+ this._loaded = false;
26
+ }
27
+
28
+ /**
29
+ * Discover and load all skills from the skills directory.
30
+ */
31
+ load() {
32
+ if (!fs.existsSync(this.skillsDir)) return;
33
+
34
+ const files = fs.readdirSync(this.skillsDir).filter(f =>
35
+ f.endsWith(".js") && f !== "skill-registry.js"
36
+ );
37
+
38
+ for (const file of files) {
39
+ try {
40
+ const mod = require(path.join(this.skillsDir, file));
41
+ const skill = {
42
+ file,
43
+ name: mod.SKILL_NAME || file.replace(".js", ""),
44
+ description: mod.SKILL_DESCRIPTION || "",
45
+ keywords: mod.SKILL_KEYWORDS || [],
46
+ usage: mod.SKILL_USAGE || "",
47
+ hasMiddleware: typeof mod.sqlGuard === "function" || typeof mod.middleware === "function",
48
+ };
49
+ this._skills.push(skill);
50
+ } catch (err) {
51
+ console.log(chalk.yellow(` ⚠️ Skill load error (${file}): ${err.message}`));
52
+ }
53
+ }
54
+
55
+ this._loaded = true;
56
+ if (this._skills.length > 0) {
57
+ console.log(chalk.gray(` 🔧 Skills: ${this._skills.map(s => s.name).join(", ")}`));
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Match skills to a command/error using token scoring.
63
+ * claw-code pattern: tokenize → score against name/description/keywords → return top matches.
64
+ *
65
+ * @param {string} text — user command or error message
66
+ * @param {number} limit — max skills to return
67
+ * @returns {Array<{ name, description, usage, score }>}
68
+ */
69
+ match(text, limit = 3) {
70
+ if (!text || this._skills.length === 0) return [];
71
+
72
+ const tokens = text.toLowerCase()
73
+ .replace(/[^a-z0-9\s]/g, " ")
74
+ .split(/\s+/)
75
+ .filter(t => t.length > 2);
76
+
77
+ const scored = this._skills.map(skill => {
78
+ const haystacks = [
79
+ skill.name.toLowerCase(),
80
+ skill.description.toLowerCase(),
81
+ ...skill.keywords.map(k => k.toLowerCase()),
82
+ ].join(" ");
83
+
84
+ let score = 0;
85
+ for (const token of tokens) {
86
+ if (haystacks.includes(token)) score++;
87
+ }
88
+
89
+ return { ...skill, score };
90
+ });
91
+
92
+ return scored
93
+ .filter(s => s.score > 0)
94
+ .sort((a, b) => b.score - a.score)
95
+ .slice(0, limit);
96
+ }
97
+
98
+ /**
99
+ * Build context string for matched skills — inject into AI prompt.
100
+ * claw-code pattern: pre-enrich the prompt with tool availability.
101
+ */
102
+ buildContext(text) {
103
+ const matches = this.match(text);
104
+ if (matches.length === 0) return "";
105
+
106
+ const parts = ["\n## Available Skills"];
107
+ for (const skill of matches) {
108
+ parts.push(`### ${skill.name}`);
109
+ if (skill.description) parts.push(skill.description);
110
+ if (skill.usage) parts.push("```js\n" + skill.usage + "\n```");
111
+ }
112
+
113
+ return parts.join("\n");
114
+ }
115
+
116
+ /**
117
+ * Get all skills for dashboard display.
118
+ */
119
+ getAll() {
120
+ return this._skills.map(s => ({
121
+ name: s.name,
122
+ description: s.description,
123
+ keywords: s.keywords,
124
+ hasMiddleware: s.hasMiddleware,
125
+ }));
126
+ }
127
+ }
128
+
129
+ module.exports = { SkillRegistry };