smart-context-mcp 1.18.1 → 1.20.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 +24 -13
- package/package.json +5 -2
- package/server.json +2 -2
- package/src/embeddings/embedder.js +28 -0
- package/src/embeddings/hashing.js +77 -0
- package/src/embeddings/index.js +91 -0
- package/src/embeddings/tokenize.js +46 -0
- package/src/global-memory/scrub.js +46 -0
- package/src/global-memory/store.js +375 -0
- package/src/index-watcher.js +224 -0
- package/src/index.js +91 -9
- package/src/orchestration/base-orchestrator.js +37 -1
- package/src/parsers/registry.js +26 -0
- package/src/playbooks/builtin/debug-flake.yaml +17 -0
- package/src/playbooks/builtin/doc-sync.yaml +16 -0
- package/src/playbooks/builtin/preflight-merge.yaml +20 -0
- package/src/playbooks/builtin/ramp-up.yaml +14 -0
- package/src/playbooks/builtin/refactor-safe.yaml +18 -0
- package/src/playbooks/loader.js +123 -0
- package/src/playbooks/runner.js +182 -0
- package/src/playbooks/yaml-mini.js +162 -0
- package/src/server.js +108 -13
- package/src/storage/sqlite.js +75 -1
- package/src/task-runner.js +4 -0
- package/src/tools/global-memory.js +110 -0
- package/src/tools/smart-context.js +18 -4
- package/src/tools/smart-playbook.js +63 -0
- package/src/tools/smart-read-batch.js +26 -3
- package/src/tools/smart-read.js +128 -15
- package/src/tools/smart-search.js +692 -55
- package/src/tools/smart-status.js +13 -0
- package/src/tools/smart-turn.js +88 -4
- package/src/turn/next-actions.js +4 -1
- package/src/utils/task-budget.js +116 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Restart your AI client. Done.
|
|
|
56
56
|
# Check installed version
|
|
57
57
|
npm list -g smart-context-mcp
|
|
58
58
|
|
|
59
|
-
# Should show: smart-context-mcp@1.
|
|
59
|
+
# Should show: smart-context-mcp@1.20.0 (or later)
|
|
60
60
|
|
|
61
61
|
# Update to latest version
|
|
62
62
|
npm update -g smart-context-mcp
|
|
@@ -278,22 +278,30 @@ Check actual usage:
|
|
|
278
278
|
|
|
279
279
|
Provides **two key components**:
|
|
280
280
|
|
|
281
|
-
### 1. Specialized Tools (
|
|
281
|
+
### 1. Specialized Tools (20 tools)
|
|
282
282
|
|
|
283
283
|
| Tool | Purpose | Savings |
|
|
284
284
|
|------|---------|---------|
|
|
285
|
-
| `smart_read` | Read files in outline/signatures mode | 90% |
|
|
285
|
+
| `smart_read` | Read files in outline / signatures / symbol / explain mode | 90% |
|
|
286
286
|
| `smart_read_batch` | Read multiple files in one call | 90% |
|
|
287
|
-
| `smart_search` | Intent-aware code search with ranking | 95% |
|
|
288
|
-
| `smart_context` | One-call context builder | 85% |
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
| `
|
|
293
|
-
| `build_index` | Symbol index builder | - |
|
|
287
|
+
| `smart_search` | Intent-aware code search with ranking and `kinds` filter (incl. ADRs) | 95% |
|
|
288
|
+
| `smart_context` | One-call context builder; `paths: { from, to }` mode traverses the import graph | 85% |
|
|
289
|
+
| `smart_shell` | Safe command execution (TAP / git-log / diff compression) | 94% |
|
|
290
|
+
| `smart_test` | Affected tests via graph + sandboxed runner + persisted `last_failure` | - |
|
|
291
|
+
| `smart_review` | One-call review preflight: diff + callers + tests + heuristic findings | - |
|
|
292
|
+
| `build_index` | Symbol index builder (incremental) | - |
|
|
294
293
|
| `warm_cache` | File preloading (5x faster cold start) | - |
|
|
295
294
|
| `git_blame` | Function-level code attribution | - |
|
|
296
295
|
| `cross_project` | Multi-project context | - |
|
|
296
|
+
| `smart_summary` | Task checkpoint management with rolling window | 98% |
|
|
297
|
+
| `smart_status` | Quick session / project state inspection | - |
|
|
298
|
+
| `smart_doctor` | Health checks for storage, index, hooks | - |
|
|
299
|
+
| `smart_edit` | Targeted symbol-aware edits | - |
|
|
300
|
+
| `smart_turn` | Turn boundary + `nextActions[]` machine-readable plan | - |
|
|
301
|
+
| `smart_resume` | Lightweight alias for `smart_turn(phase: 'start', verbosity: 'minimal')` | - |
|
|
302
|
+
| `smart_metrics` | Token usage inspection | - |
|
|
303
|
+
| `smart_playbook` | Declarative workflows: composes `smart_*` tools in one call (preflight-merge, debug-flake, refactor-safe, doc-sync, ramp-up) | - |
|
|
304
|
+
| `global_memory` | Cross-project memory in `~/.devctx/global.db` (opt-in, scrubbed for secrets, semantic recall) | - |
|
|
297
305
|
|
|
298
306
|
### 2. Agent Rules (Task-Specific Guidance)
|
|
299
307
|
|
|
@@ -646,9 +654,12 @@ npm run verify
|
|
|
646
654
|
## Storage
|
|
647
655
|
|
|
648
656
|
Data stored in `.devctx/`:
|
|
649
|
-
- `index.json` - Symbol index
|
|
650
|
-
- `state.sqlite` -
|
|
651
|
-
- `metrics.jsonl` -
|
|
657
|
+
- `index.json` - Symbol index (`INDEX_VERSION 7`: ADR + ADR sections, richer Python/Go)
|
|
658
|
+
- `state.sqlite` - Sessions, metrics, patterns, task handoffs, test failures, explain cache (Node 22+)
|
|
659
|
+
- `metrics.jsonl` - Opt-in legacy file, only when `DEVCTX_METRICS_FILE=path.jsonl` is set
|
|
660
|
+
|
|
661
|
+
Cross-project (opt-in via `DEVCTX_GLOBAL_MEMORY=true`):
|
|
662
|
+
- `~/.devctx/global.db` - Scrubbed decisions, patterns, playbooks, notes with semantic recall
|
|
652
663
|
|
|
653
664
|
Add to `.gitignore`:
|
|
654
665
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
3
|
"mcpName": "io.github.Arrayo/smart-context-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.20.0",
|
|
5
5
|
"description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
|
|
6
6
|
"author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
|
|
7
7
|
"type": "module",
|
|
@@ -69,8 +69,10 @@
|
|
|
69
69
|
"eval:context": "node ./evals/harness.js --tool=context",
|
|
70
70
|
"eval:both": "node ./evals/harness.js --tool=both",
|
|
71
71
|
"eval:self": "node ./evals/harness.js --root=../.. --corpus=./evals/corpus/self-tasks.json",
|
|
72
|
+
"eval:self:json": "node ./evals/harness.js --root=../.. --corpus=./evals/corpus/self-tasks.json --json",
|
|
72
73
|
"eval:realworld": "node ./evals/realworld-eval.js",
|
|
73
74
|
"eval:realworld:json": "node ./evals/realworld-eval.js --json",
|
|
75
|
+
"eval:kpi:baseline": "node ./evals/kpi-baseline.js",
|
|
74
76
|
"eval:report": "node ./evals/report.js",
|
|
75
77
|
"report:metrics": "node ./scripts/report-metrics.js",
|
|
76
78
|
"report:workflows": "node ./scripts/report-workflow-metrics.js",
|
|
@@ -83,5 +85,6 @@
|
|
|
83
85
|
"js-tiktoken": "^1.0.21",
|
|
84
86
|
"typescript": "^6.0.2",
|
|
85
87
|
"zod": "^4.1.5"
|
|
86
|
-
}
|
|
88
|
+
},
|
|
89
|
+
"packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
|
|
87
90
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/Arrayo/smart-context-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.20.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "smart-context-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.20.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { embed as hashingEmbed, cosineSimilarity, buildCorpusIdf, DEFAULT_DIMENSIONS } from './hashing.js';
|
|
2
|
+
|
|
3
|
+
const HASHING_EMBEDDER = {
|
|
4
|
+
id: 'hashing-v1',
|
|
5
|
+
dimensions: DEFAULT_DIMENSIONS,
|
|
6
|
+
embed: (text, options = {}) => hashingEmbed(text, options),
|
|
7
|
+
similarity: cosineSimilarity,
|
|
8
|
+
buildCorpusIdf,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let activeEmbedder = HASHING_EMBEDDER;
|
|
12
|
+
|
|
13
|
+
export const getEmbedder = () => activeEmbedder;
|
|
14
|
+
|
|
15
|
+
export const setEmbedder = (embedder) => {
|
|
16
|
+
if (!embedder || typeof embedder.embed !== 'function' || typeof embedder.similarity !== 'function') {
|
|
17
|
+
throw new Error('Embedder must implement embed(text, opts) and similarity(a, b)');
|
|
18
|
+
}
|
|
19
|
+
activeEmbedder = {
|
|
20
|
+
id: embedder.id ?? 'custom',
|
|
21
|
+
dimensions: embedder.dimensions ?? DEFAULT_DIMENSIONS,
|
|
22
|
+
embed: embedder.embed,
|
|
23
|
+
similarity: embedder.similarity,
|
|
24
|
+
buildCorpusIdf: embedder.buildCorpusIdf ?? buildCorpusIdf,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const resetEmbedder = () => { activeEmbedder = HASHING_EMBEDDER; };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { tokenize } from './tokenize.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DIMENSIONS = 256;
|
|
4
|
+
|
|
5
|
+
const fnv1a = (str) => {
|
|
6
|
+
let hash = 0x811c9dc5;
|
|
7
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
8
|
+
hash ^= str.charCodeAt(i);
|
|
9
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
10
|
+
}
|
|
11
|
+
return hash;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const signedBucket = (token, dims) => {
|
|
15
|
+
const h = fnv1a(token);
|
|
16
|
+
const bucket = h % dims;
|
|
17
|
+
const sign = ((h >>> 16) & 1) === 0 ? 1 : -1;
|
|
18
|
+
return { bucket, sign };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const l2Normalize = (vector) => {
|
|
22
|
+
let sumSq = 0;
|
|
23
|
+
for (let i = 0; i < vector.length; i += 1) sumSq += vector[i] * vector[i];
|
|
24
|
+
if (sumSq === 0) return vector;
|
|
25
|
+
const norm = Math.sqrt(sumSq);
|
|
26
|
+
for (let i = 0; i < vector.length; i += 1) vector[i] /= norm;
|
|
27
|
+
return vector;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const embed = (text, { dimensions = DEFAULT_DIMENSIONS, idf = null } = {}) => {
|
|
31
|
+
const tokens = tokenize(text);
|
|
32
|
+
const vector = new Float32Array(dimensions);
|
|
33
|
+
if (tokens.length === 0) return vector;
|
|
34
|
+
|
|
35
|
+
const counts = new Map();
|
|
36
|
+
for (const token of tokens) {
|
|
37
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [token, count] of counts) {
|
|
41
|
+
const tf = 1 + Math.log(count);
|
|
42
|
+
const weight = idf ? tf * (idf.get(token) ?? 1) : tf;
|
|
43
|
+
const { bucket, sign } = signedBucket(token, dimensions);
|
|
44
|
+
vector[bucket] += sign * weight;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return l2Normalize(vector);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const cosineSimilarity = (a, b) => {
|
|
51
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
52
|
+
let dot = 0;
|
|
53
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
54
|
+
return dot;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const buildCorpusIdf = (documents) => {
|
|
58
|
+
const df = new Map();
|
|
59
|
+
let docCount = 0;
|
|
60
|
+
for (const doc of documents) {
|
|
61
|
+
docCount += 1;
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const token of tokenize(doc)) {
|
|
64
|
+
if (!seen.has(token)) {
|
|
65
|
+
seen.add(token);
|
|
66
|
+
df.set(token, (df.get(token) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const idf = new Map();
|
|
71
|
+
for (const [token, freq] of df) {
|
|
72
|
+
idf.set(token, Math.log((docCount + 1) / (freq + 1)) + 1);
|
|
73
|
+
}
|
|
74
|
+
return idf;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const _internal = { fnv1a, signedBucket, l2Normalize };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getEmbedder } from './embedder.js';
|
|
2
|
+
|
|
3
|
+
const symbolToText = (symbol, filePath) => {
|
|
4
|
+
const parts = [];
|
|
5
|
+
parts.push(symbol.name);
|
|
6
|
+
if (symbol.parent) parts.push(symbol.parent);
|
|
7
|
+
if (symbol.kind) parts.push(symbol.kind);
|
|
8
|
+
if (symbol.signature) parts.push(symbol.signature);
|
|
9
|
+
if (symbol.snippet) parts.push(symbol.snippet);
|
|
10
|
+
if (Array.isArray(symbol.decorators)) parts.push(...symbol.decorators);
|
|
11
|
+
if (filePath) {
|
|
12
|
+
const segments = filePath.replace(/\\/g, '/').split('/');
|
|
13
|
+
parts.push(...segments);
|
|
14
|
+
}
|
|
15
|
+
return parts.join(' ');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const fileToText = (relPath, fileInfo) => {
|
|
19
|
+
const parts = [relPath];
|
|
20
|
+
for (const symbol of fileInfo?.symbols ?? []) {
|
|
21
|
+
parts.push(symbol.name);
|
|
22
|
+
if (symbol.signature) parts.push(symbol.signature);
|
|
23
|
+
}
|
|
24
|
+
return parts.join(' ');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const buildIndexCorpusIdf = (index) => {
|
|
28
|
+
const embedder = getEmbedder();
|
|
29
|
+
const docs = [];
|
|
30
|
+
for (const [relPath, fileInfo] of Object.entries(index?.files ?? {})) {
|
|
31
|
+
docs.push(fileToText(relPath, fileInfo));
|
|
32
|
+
for (const symbol of fileInfo.symbols ?? []) {
|
|
33
|
+
docs.push(symbolToText(symbol, relPath));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return embedder.buildCorpusIdf(docs);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const embedQuery = (query, options = {}) => {
|
|
40
|
+
const embedder = getEmbedder();
|
|
41
|
+
return embedder.embed(query, options);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const embedFile = (relPath, fileInfo, options = {}) => {
|
|
45
|
+
const embedder = getEmbedder();
|
|
46
|
+
return embedder.embed(fileToText(relPath, fileInfo), options);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const embedSymbol = (symbol, relPath, options = {}) => {
|
|
50
|
+
const embedder = getEmbedder();
|
|
51
|
+
return embedder.embed(symbolToText(symbol, relPath), options);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const semanticRankSymbols = ({ query, index, limit = 10, idf = null }) => {
|
|
55
|
+
if (!query || !index) return [];
|
|
56
|
+
const embedder = getEmbedder();
|
|
57
|
+
const queryVec = embedder.embed(query, { idf });
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const [relPath, fileInfo] of Object.entries(index.files ?? {})) {
|
|
60
|
+
for (const symbol of fileInfo.symbols ?? []) {
|
|
61
|
+
const vec = embedder.embed(symbolToText(symbol, relPath), { idf });
|
|
62
|
+
const score = embedder.similarity(queryVec, vec);
|
|
63
|
+
if (score > 0) {
|
|
64
|
+
results.push({ score, path: relPath, symbol });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
results.sort((a, b) => b.score - a.score);
|
|
69
|
+
return results.slice(0, limit);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const semanticRankFiles = ({ query, index, limit = 10, idf = null }) => {
|
|
73
|
+
if (!query || !index) return [];
|
|
74
|
+
const embedder = getEmbedder();
|
|
75
|
+
const queryVec = embedder.embed(query, { idf });
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const [relPath, fileInfo] of Object.entries(index.files ?? {})) {
|
|
78
|
+
const vec = embedder.embed(fileToText(relPath, fileInfo), { idf });
|
|
79
|
+
const score = embedder.similarity(queryVec, vec);
|
|
80
|
+
if (score > 0) {
|
|
81
|
+
results.push({ score, path: relPath, symbolCount: fileInfo.symbols?.length ?? 0 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.sort((a, b) => b.score - a.score);
|
|
85
|
+
return results.slice(0, limit);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export { getEmbedder, setEmbedder, resetEmbedder } from './embedder.js';
|
|
89
|
+
export { tokenize } from './tokenize.js';
|
|
90
|
+
export { embed, cosineSimilarity, buildCorpusIdf, DEFAULT_DIMENSIONS } from './hashing.js';
|
|
91
|
+
export const _internal = { symbolToText, fileToText };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const STOP_WORDS = new Set([
|
|
2
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'is', 'in', 'on', 'at', 'to', 'for', 'of', 'with',
|
|
3
|
+
'as', 'by', 'from', 'this', 'that', 'these', 'those', 'it', 'its', 'be', 'been', 'being',
|
|
4
|
+
'are', 'was', 'were', 'has', 'have', 'had', 'do', 'does', 'did', 'will', 'would', 'should',
|
|
5
|
+
'can', 'could', 'may', 'might', 'must', 'shall', 'not', 'no', 'yes', 'if', 'else', 'then',
|
|
6
|
+
'return', 'true', 'false', 'null', 'undefined', 'self', 'this', 'use', 'using', 'used', 'via',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const CAMEL_RE = /([a-z0-9])([A-Z])/g;
|
|
10
|
+
const SNAKE_RE = /[_-]+/g;
|
|
11
|
+
const NON_WORD_RE = /[^A-Za-z0-9_]+/g;
|
|
12
|
+
const NUMBER_RE = /^\d+$/;
|
|
13
|
+
|
|
14
|
+
const splitIdentifier = (token) => {
|
|
15
|
+
if (!token) return [];
|
|
16
|
+
const camelExpanded = token.replace(CAMEL_RE, '$1 $2');
|
|
17
|
+
const parts = camelExpanded.replace(SNAKE_RE, ' ').split(/\s+/).filter(Boolean);
|
|
18
|
+
const out = new Set();
|
|
19
|
+
out.add(token.toLowerCase());
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
const lower = part.toLowerCase();
|
|
22
|
+
if (!lower || NUMBER_RE.test(lower)) continue;
|
|
23
|
+
if (STOP_WORDS.has(lower)) continue;
|
|
24
|
+
if (lower.length < 2) continue;
|
|
25
|
+
out.add(lower);
|
|
26
|
+
}
|
|
27
|
+
return [...out];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const tokenize = (text) => {
|
|
31
|
+
if (!text || typeof text !== 'string') return [];
|
|
32
|
+
const raw = text.replace(NON_WORD_RE, ' ').split(/\s+/).filter(Boolean);
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const token of raw) {
|
|
35
|
+
if (NUMBER_RE.test(token)) continue;
|
|
36
|
+
const expanded = splitIdentifier(token);
|
|
37
|
+
for (const part of expanded) {
|
|
38
|
+
if (!STOP_WORDS.has(part) && part.length >= 2) {
|
|
39
|
+
out.push(part);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const _internal = { STOP_WORDS, splitIdentifier };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/(api[_-]?key|secret|token|password|passwd|pwd|bearer|authorization)\s*[:=]\s*['"]?([A-Za-z0-9_\-+/=]{8,})['"]?/gi,
|
|
3
|
+
/(aws_access_key_id|aws_secret_access_key)\s*[:=]\s*['"]?([A-Za-z0-9_\-+/=]{8,})['"]?/gi,
|
|
4
|
+
/sk-[A-Za-z0-9]{20,}/g,
|
|
5
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
6
|
+
/xoxb-[A-Za-z0-9-]{20,}/g,
|
|
7
|
+
/AIza[A-Za-z0-9_-]{30,}/g,
|
|
8
|
+
/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
9
|
+
/-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
|
|
10
|
+
/(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s'"]+/gi,
|
|
11
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const HOME_PATH_RE = /\/home\/[^/\s'"]+/g;
|
|
15
|
+
const USER_PATH_RE = /\/Users\/[^/\s'"]+/g;
|
|
16
|
+
const WIN_USER_RE = /C:\\Users\\[^\\/\s'"]+/g;
|
|
17
|
+
|
|
18
|
+
const REDACTION = '[REDACTED]';
|
|
19
|
+
|
|
20
|
+
export const scrubContent = (content) => {
|
|
21
|
+
if (typeof content !== 'string') return content;
|
|
22
|
+
let cleaned = content;
|
|
23
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
24
|
+
cleaned = cleaned.replace(pattern, REDACTION);
|
|
25
|
+
}
|
|
26
|
+
cleaned = cleaned.replace(HOME_PATH_RE, '~').replace(USER_PATH_RE, '~').replace(WIN_USER_RE, '~');
|
|
27
|
+
return cleaned;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const containsLikelySecret = (content) => {
|
|
31
|
+
if (typeof content !== 'string') return false;
|
|
32
|
+
return SECRET_PATTERNS.some((re) => {
|
|
33
|
+
re.lastIndex = 0;
|
|
34
|
+
return re.test(content);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const hashProjectPath = (absolutePath) => {
|
|
39
|
+
if (!absolutePath || typeof absolutePath !== 'string') return null;
|
|
40
|
+
let hash = 0x811c9dc5;
|
|
41
|
+
for (let i = 0; i < absolutePath.length; i += 1) {
|
|
42
|
+
hash ^= absolutePath.charCodeAt(i);
|
|
43
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
return `proj-${hash.toString(36)}`;
|
|
46
|
+
};
|