ziprin-context-optimizer 8.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.
- package/README.md +48 -0
- package/bin/ziprin-context-mcp.js +34 -0
- package/bin/ziprin-context-setup.js +10 -0
- package/install-source.json +5 -0
- package/media/context-activity.svg +7 -0
- package/package.json +154 -0
- package/releases/ziprin-context-optimizer-8.0.0.vsix +0 -0
- package/scripts/install.mjs +204 -0
- package/scripts/package-vsix.mjs +38 -0
- package/scripts/paths.mjs +40 -0
- package/scripts/publish.mjs +37 -0
- package/scripts/update.mjs +101 -0
- package/src/adaptive-budget.js +46 -0
- package/src/analyzer.js +456 -0
- package/src/audit-history.js +137 -0
- package/src/bm25.js +132 -0
- package/src/collector.js +343 -0
- package/src/compression.js +67 -0
- package/src/config.js +28 -0
- package/src/context-memory.js +151 -0
- package/src/context-profiles.js +124 -0
- package/src/dependency-graph.js +90 -0
- package/src/estimate.js +109 -0
- package/src/eval-harness.js +76 -0
- package/src/extension.js +322 -0
- package/src/firewall.js +35 -0
- package/src/fts-index.js +319 -0
- package/src/gateway-api.js +404 -0
- package/src/git-recency.js +43 -0
- package/src/glob.js +42 -0
- package/src/identifier.js +37 -0
- package/src/inspector-panel.js +470 -0
- package/src/language.js +157 -0
- package/src/mcp-event-stream.js +179 -0
- package/src/mcp-health-cli.js +34 -0
- package/src/mcp-lifecycle-manager.js +427 -0
- package/src/mcp-server.js +372 -0
- package/src/mmr.js +57 -0
- package/src/pagerank.js +176 -0
- package/src/profiles.js +74 -0
- package/src/pruner.js +130 -0
- package/src/quality-guard.js +122 -0
- package/src/query-rewrite.js +32 -0
- package/src/relevance-scorer.js +197 -0
- package/src/repo-map.js +134 -0
- package/src/retrieve.js +350 -0
- package/src/serena.js +126 -0
- package/src/session-ledger.js +83 -0
- package/src/session-store.js +78 -0
- package/src/skeleton.js +129 -0
- package/src/slice-pack.js +70 -0
- package/src/supervisor.js +113 -0
- package/src/task-analyzer.js +189 -0
- package/src/tool-router.js +92 -0
- package/src/version.js +5 -0
- package/templates/ziprin-context-mcp.mjs +68 -0
package/src/skeleton.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Function-level skeletons + unfold one body.
|
|
5
|
+
* Optional web-tree-sitter if installed; regex otherwise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const START_RE =
|
|
12
|
+
/^(export\s+)?(async\s+)?(function|class|const|type|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
|
13
|
+
const ARROW_RE =
|
|
14
|
+
/^(export\s+)?(const|let)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(async\s*)?\(/;
|
|
15
|
+
|
|
16
|
+
function tryTreeSitter() {
|
|
17
|
+
try {
|
|
18
|
+
return require('web-tree-sitter');
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function braceEnd(lines, startIdx) {
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let started = false;
|
|
27
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
for (const ch of line) {
|
|
30
|
+
if (ch === '{') {
|
|
31
|
+
depth++;
|
|
32
|
+
started = true;
|
|
33
|
+
} else if (ch === '}') {
|
|
34
|
+
depth--;
|
|
35
|
+
if (started && depth <= 0) return i;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!started && /;\s*$/.test(line) && i > startIdx) return i;
|
|
39
|
+
}
|
|
40
|
+
return Math.min(lines.length - 1, startIdx + 40);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractSignatures(text) {
|
|
44
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
45
|
+
const sigs = [];
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const trim = lines[i].trim();
|
|
48
|
+
let m = START_RE.exec(trim) || ARROW_RE.exec(trim);
|
|
49
|
+
if (!m) continue;
|
|
50
|
+
const name = m[4] || m[3];
|
|
51
|
+
const kind = m[3] === 'const' || m[2] === 'const' || m[2] === 'let' ? 'const' : (m[3] || m[2] || 'fn');
|
|
52
|
+
const end = braceEnd(lines, i);
|
|
53
|
+
sigs.push({
|
|
54
|
+
name,
|
|
55
|
+
kind,
|
|
56
|
+
startLine: i + 1,
|
|
57
|
+
endLine: end + 1,
|
|
58
|
+
signature: trim.replace(/\s*\{.*$/, '').slice(0, 180),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return sigs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function skeletonFile(root, rel, maxSigs = 24) {
|
|
65
|
+
const abs = path.join(root, rel);
|
|
66
|
+
let text = '';
|
|
67
|
+
try {
|
|
68
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
69
|
+
} catch {
|
|
70
|
+
return { path: rel, mode: 'missing', signatures: [], text: '', tokenEstimate: 0 };
|
|
71
|
+
}
|
|
72
|
+
const ts = tryTreeSitter();
|
|
73
|
+
const signatures = extractSignatures(text).slice(0, maxSigs);
|
|
74
|
+
const importLines = text.split(/\r?\n/).filter((l) => /^\s*import\s/.test(l)).slice(0, 20);
|
|
75
|
+
const body = [
|
|
76
|
+
`/* SKELETON ${rel} — ${signatures.length} symbols${ts ? ' (tree-sitter available)' : ''} */`,
|
|
77
|
+
...importLines,
|
|
78
|
+
...signatures.map((s) => `${s.signature} /* L${s.startLine}-${s.endLine} */`),
|
|
79
|
+
'/* unfold via ziprin_unfold_symbol */',
|
|
80
|
+
].join('\n');
|
|
81
|
+
return {
|
|
82
|
+
path: rel,
|
|
83
|
+
mode: 'skeleton',
|
|
84
|
+
signatures,
|
|
85
|
+
text: body,
|
|
86
|
+
tokenEstimate: Math.ceil(body.length / 3.6),
|
|
87
|
+
treeSitter: Boolean(ts),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function unfoldSymbol(root, rel, name) {
|
|
92
|
+
const abs = path.join(root, rel);
|
|
93
|
+
let text = '';
|
|
94
|
+
try {
|
|
95
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
96
|
+
} catch {
|
|
97
|
+
return { path: rel, name, mode: 'missing', text: '', tokenEstimate: 0 };
|
|
98
|
+
}
|
|
99
|
+
const sigs = extractSignatures(text);
|
|
100
|
+
const hit = sigs.find((s) => s.name === name) || sigs.find((s) => s.name.toLowerCase() === String(name).toLowerCase());
|
|
101
|
+
if (!hit) {
|
|
102
|
+
return {
|
|
103
|
+
path: rel,
|
|
104
|
+
name,
|
|
105
|
+
mode: 'not_found',
|
|
106
|
+
signatures: sigs.map((s) => s.name),
|
|
107
|
+
text: '',
|
|
108
|
+
tokenEstimate: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const lines = text.split(/\r?\n/);
|
|
112
|
+
const slice = lines.slice(hit.startLine - 1, hit.endLine).join('\n');
|
|
113
|
+
return {
|
|
114
|
+
path: rel,
|
|
115
|
+
name: hit.name,
|
|
116
|
+
mode: 'unfold',
|
|
117
|
+
startLine: hit.startLine,
|
|
118
|
+
endLine: hit.endLine,
|
|
119
|
+
text: slice,
|
|
120
|
+
tokenEstimate: Math.ceil(slice.length / 3.6),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
extractSignatures,
|
|
126
|
+
skeletonFile,
|
|
127
|
+
unfoldSymbol,
|
|
128
|
+
tryTreeSitter,
|
|
129
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vertical feature-slice packing.
|
|
5
|
+
* Prefer one complete slice (screen+api+mapper+types+local tests) over 14 unrelated files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const COMPANION_RE = [
|
|
9
|
+
{ re: /\.(tsx|jsx)$/, add: ['.ts', '.api.ts', '.mapper.ts', '.types.ts', '.test.tsx', '.test.ts'] },
|
|
10
|
+
{ re: /-screen\.(tsx|ts)$/, add: ['.api.ts', '.mapper.ts', '.types.ts', '.test.tsx'] },
|
|
11
|
+
{ re: /\.api\.(ts|tsx)$/, add: ['.types.ts', '.mapper.ts', '.test.ts'] },
|
|
12
|
+
{ re: /\.mapper\.(ts|tsx)$/, add: ['.types.ts', '.api.ts', '.test.ts'] },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function stem(p) {
|
|
16
|
+
return p.replace(/\.(test|spec)\.(tsx?|jsx?)$/, '').replace(/\.(tsx?|jsx?|css)$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dirOf(p) {
|
|
20
|
+
const i = p.lastIndexOf('/');
|
|
21
|
+
return i === -1 ? '' : p.slice(0, i);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function companionPaths(seed, allPaths) {
|
|
25
|
+
const set = new Set(allPaths);
|
|
26
|
+
const out = [];
|
|
27
|
+
const d = dirOf(seed);
|
|
28
|
+
const s = stem(seed);
|
|
29
|
+
const sameDir = allPaths.filter((p) => dirOf(p) === d);
|
|
30
|
+
for (const p of sameDir) {
|
|
31
|
+
if (p === seed) continue;
|
|
32
|
+
const ps = stem(p);
|
|
33
|
+
if (ps === s || ps.startsWith(s) || s.startsWith(ps)) out.push(p);
|
|
34
|
+
if (/\.(api|mapper|types|test|spec)\./.test(p) && (p.includes(s.split('/').pop() || '___') || dirOf(p) === d)) {
|
|
35
|
+
if (!out.includes(p)) out.push(p);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const rule of COMPANION_RE) {
|
|
39
|
+
if (!rule.re.test(seed)) continue;
|
|
40
|
+
for (const extra of rule.add) {
|
|
41
|
+
const guess = s + extra.replace(/^\./, '.');
|
|
42
|
+
if (set.has(guess) && !out.includes(guess)) out.push(guess);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function packSlice(seeds, allPaths, opts = {}) {
|
|
49
|
+
const maxFiles = opts.maxFiles || 8;
|
|
50
|
+
const packed = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const push = (p) => {
|
|
53
|
+
if (!p || seen.has(p) || packed.length >= maxFiles) return;
|
|
54
|
+
seen.add(p);
|
|
55
|
+
packed.push(p);
|
|
56
|
+
};
|
|
57
|
+
for (const seed of seeds.slice(0, 4)) {
|
|
58
|
+
push(seed);
|
|
59
|
+
for (const c of companionPaths(seed, allPaths)) push(c);
|
|
60
|
+
if (packed.length >= maxFiles) break;
|
|
61
|
+
}
|
|
62
|
+
return packed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isLocalTest(p, action) {
|
|
66
|
+
if (!/\/(test|spec|__tests__)\//.test(p) && !/\.(test|spec)\./.test(p)) return false;
|
|
67
|
+
return action === 'fix' || action === 'review' || /test/.test(String(action));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { companionPaths, packSlice, stem, isLocalTest };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/** Only alwaysApply file allowed after migration — replaces 6+ heavy mdc files. */
|
|
7
|
+
const STUB_MDC = `---
|
|
8
|
+
description: Ziprin always-on supervisor delegate — token budget. Super Guard owns rules.
|
|
9
|
+
alwaysApply: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Ziprin supervisor (delegate)
|
|
13
|
+
|
|
14
|
+
Live enforcement is the **Ziprin Super Guard** extension (onStartupFinished + diagnostics).
|
|
15
|
+
Do **not** load other \`.cursor/rules/*.mdc\`, agents, or skills from this repository into chat.
|
|
16
|
+
|
|
17
|
+
- Prefer Serena symbols, then Super Guard findings, then the fewest files.
|
|
18
|
+
- Never dump whole folders. Never invent backend contracts. Never revive mock-server.
|
|
19
|
+
- Auth JSON contracts and \`*.policy.json\` stay in the repo (CI). Corpus lives in Super Guard pack.
|
|
20
|
+
- Call tools/skills only when they reduce tokens without harming answer quality.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const STUB_REL = '.cursor/rules/ziprin-supervisor.mdc';
|
|
24
|
+
|
|
25
|
+
const CURSORIGNORE_BLOCK = `
|
|
26
|
+
# Ziprin Context Optimizer — token bombs
|
|
27
|
+
tooling/project-index.json
|
|
28
|
+
tooling/context-optimizer/project-index.json
|
|
29
|
+
tooling/governance/project-must-follow-catalog.json
|
|
30
|
+
tooling/super-guard/*.vsix
|
|
31
|
+
**/reports/**/*.json
|
|
32
|
+
.cursor/_tmp_*
|
|
33
|
+
.cursor/agents/**
|
|
34
|
+
.cursor/skills/**
|
|
35
|
+
.cursor/sprint-manifests/
|
|
36
|
+
**/node_modules/**
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
function ensureCursorIgnore(existing) {
|
|
40
|
+
if (/Context Optimizer/.test(existing) || /tooling\/project-index\.json/.test(existing)) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
return (existing || '').trimEnd() + '\n' + CURSORIGNORE_BLOCK;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function flipAlwaysApplyText(text) {
|
|
47
|
+
return text.replace(/alwaysApply:\s*true/g, 'alwaysApply: false');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function flipAlwaysApplyFiles(root, { write = false } = {}) {
|
|
51
|
+
const dir = path.join(root, '.cursor/rules');
|
|
52
|
+
const flipped = [];
|
|
53
|
+
if (!fs.existsSync(dir)) return { found: 0, flipped };
|
|
54
|
+
let found = 0;
|
|
55
|
+
for (const name of fs.readdirSync(dir)) {
|
|
56
|
+
if (!name.endsWith('.mdc')) continue;
|
|
57
|
+
if (name === 'ziprin-supervisor.mdc') continue;
|
|
58
|
+
const abs = path.join(dir, name);
|
|
59
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
60
|
+
if (!/alwaysApply:\s*true/.test(text)) continue;
|
|
61
|
+
found++;
|
|
62
|
+
const next = flipAlwaysApplyText(text);
|
|
63
|
+
if (next !== text) {
|
|
64
|
+
if (write) fs.writeFileSync(abs, next, 'utf8');
|
|
65
|
+
flipped.push(path.relative(root, abs).replace(/\\/g, '/'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { found, flipped };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Install token budget: one stub alwaysApply + cursorignore + optionally strip other mdc.
|
|
73
|
+
*/
|
|
74
|
+
function installTokenBudget(root, { write = false, stripOtherMdc = false } = {}) {
|
|
75
|
+
const report = { stub: STUB_REL, written: [], flipped: [], ignored: false, stripped: [] };
|
|
76
|
+
const stubAbs = path.join(root, STUB_REL);
|
|
77
|
+
if (write) {
|
|
78
|
+
fs.mkdirSync(path.dirname(stubAbs), { recursive: true });
|
|
79
|
+
fs.writeFileSync(stubAbs, STUB_MDC, 'utf8');
|
|
80
|
+
report.written.push(STUB_REL);
|
|
81
|
+
}
|
|
82
|
+
const ignorePath = path.join(root, '.cursorignore');
|
|
83
|
+
const prev = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
|
|
84
|
+
const next = ensureCursorIgnore(prev);
|
|
85
|
+
if (write && next !== prev) {
|
|
86
|
+
fs.writeFileSync(ignorePath, next, 'utf8');
|
|
87
|
+
report.ignored = true;
|
|
88
|
+
}
|
|
89
|
+
const flip = flipAlwaysApplyFiles(root, { write });
|
|
90
|
+
report.flipped = flip.flipped;
|
|
91
|
+
if (stripOtherMdc && write) {
|
|
92
|
+
const rulesDir = path.join(root, '.cursor/rules');
|
|
93
|
+
if (fs.existsSync(rulesDir)) {
|
|
94
|
+
for (const name of fs.readdirSync(rulesDir)) {
|
|
95
|
+
if (!name.endsWith('.mdc')) continue;
|
|
96
|
+
if (name === 'ziprin-supervisor.mdc') continue;
|
|
97
|
+
fs.unlinkSync(path.join(rulesDir, name));
|
|
98
|
+
report.stripped.push('.cursor/rules/' + name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return report;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
STUB_MDC,
|
|
107
|
+
STUB_REL,
|
|
108
|
+
CURSORIGNORE_BLOCK,
|
|
109
|
+
ensureCursorIgnore,
|
|
110
|
+
flipAlwaysApplyText,
|
|
111
|
+
flipAlwaysApplyFiles,
|
|
112
|
+
installTokenBudget,
|
|
113
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
tokenize,
|
|
5
|
+
expandSynonyms,
|
|
6
|
+
extractSymbols,
|
|
7
|
+
extractPathHints,
|
|
8
|
+
detectDomain: detectDomainLang,
|
|
9
|
+
analyzePrompt,
|
|
10
|
+
} = require('./language');
|
|
11
|
+
|
|
12
|
+
const CATEGORIES = {
|
|
13
|
+
frontend: {
|
|
14
|
+
id: 'frontend',
|
|
15
|
+
label: 'Frontend UI',
|
|
16
|
+
includeHints: ['react', 'components', 'design', 'styles', 'tsx', 'css', 'layout', 'ui', 'کامپوننت', 'صفحه', 'فرم'],
|
|
17
|
+
deprioritize: ['database', 'migration', 'repository'],
|
|
18
|
+
profileId: 'frontend',
|
|
19
|
+
},
|
|
20
|
+
backend: {
|
|
21
|
+
id: 'backend',
|
|
22
|
+
label: 'Backend',
|
|
23
|
+
includeHints: ['service', 'controller', 'repository', 'schema', 'api', 'server', 'rtk', 'endpoint', 'ایپیای'],
|
|
24
|
+
deprioritize: ['css', 'tailwind', 'figma', 'layout'],
|
|
25
|
+
profileId: 'backend',
|
|
26
|
+
},
|
|
27
|
+
architecture: {
|
|
28
|
+
id: 'architecture',
|
|
29
|
+
label: 'Architecture',
|
|
30
|
+
includeHints: ['architecture', 'governance', 'dependency', 'boundary', 'policy', 'معماری', 'لایه', 'قانون'],
|
|
31
|
+
deprioritize: [],
|
|
32
|
+
profileId: 'architecture',
|
|
33
|
+
},
|
|
34
|
+
mixed: {
|
|
35
|
+
id: 'mixed',
|
|
36
|
+
label: 'Mixed',
|
|
37
|
+
includeHints: [],
|
|
38
|
+
deprioritize: [],
|
|
39
|
+
profileId: 'standard',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DOMAIN_RULES = [
|
|
44
|
+
{
|
|
45
|
+
id: 'bss_vendor',
|
|
46
|
+
match: /\b(bss|vendor|product.?status|dashboard\/bss|portals\/vendor|فروشنده|افزودن محصول)\b/i,
|
|
47
|
+
priorityPaths: ['apps/dashboard/bss/src/portals/vendor', 'apps/dashboard/bss/src'],
|
|
48
|
+
label: 'BSS Vendor',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'bss_dashboard',
|
|
52
|
+
match: /\b(bss|dashboard|تیکت|ticket|پنل)\b/i,
|
|
53
|
+
priorityPaths: ['apps/dashboard/bss/src', 'apps/dashboard'],
|
|
54
|
+
label: 'BSS Dashboard',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'marketplace',
|
|
58
|
+
match: /\b(marketplace|shop|cart|checkout|catalog|مارکت|بازار|سبد)\b/i,
|
|
59
|
+
priorityPaths: ['apps/marketplace/src'],
|
|
60
|
+
label: 'Marketplace',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function scoreCategory(tokens, category) {
|
|
65
|
+
let score = 0;
|
|
66
|
+
for (const hint of category.includeHints) {
|
|
67
|
+
if (tokens.some((t) => t.includes(hint) || hint.includes(t))) score += 10;
|
|
68
|
+
}
|
|
69
|
+
for (const bad of category.deprioritize) {
|
|
70
|
+
if (tokens.some((t) => t.includes(bad))) score -= 5;
|
|
71
|
+
}
|
|
72
|
+
return score;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function detectDomain(prompt) {
|
|
76
|
+
return detectDomainLang(prompt);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectAction(prompt) {
|
|
80
|
+
const p = String(prompt || '').toLowerCase();
|
|
81
|
+
if (/\bfix\b|\bbug\b|\berror\b|\bcrash\b|\bbroken\b|باگ|خطا|ارور|خراب|فیکس/.test(p)) return 'fix';
|
|
82
|
+
if (/\breview\b|\baudit\b|\binspect\b|بررسی/.test(p)) return 'review';
|
|
83
|
+
if (/\brefactor\b|cleanup|simplify|\bextract\b|بازسازی/.test(p)) return 'refactor';
|
|
84
|
+
if (/\badd\b|\bimplement\b|\bcreate\b|\bbuild\b|\bwire\b|اضافه|بساز|پیاده/.test(p)) return 'implement';
|
|
85
|
+
if (/\bmigrate\b|\bupgrade\b|\bmove\b/.test(p)) return 'migrate';
|
|
86
|
+
if (/architecture|structure|boundary|\bdesign\b|معماری|لایه/.test(p)) return 'architecture';
|
|
87
|
+
return 'general';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectScope(prompt, domain) {
|
|
91
|
+
const p = String(prompt || '');
|
|
92
|
+
if (/entire|whole|across|all apps|monorepo|کل پروژه/.test(p)) return 'monorepo';
|
|
93
|
+
if (domain?.id) return domain.id;
|
|
94
|
+
if (/marketplace|مارکت/.test(p)) return 'marketplace';
|
|
95
|
+
if (/bss|dashboard|vendor|فروشنده|تیکت/.test(p)) return 'bss';
|
|
96
|
+
return 'feature';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function detectRisk(prompt, complexity) {
|
|
100
|
+
if (/auth|payment|checkout|password|secret|credential|پرداخت|ورود/.test(prompt)) return 'high';
|
|
101
|
+
if (complexity === 'architecture_review') return 'medium';
|
|
102
|
+
if (/delete|remove|migrate|breaking|حذف/.test(prompt)) return 'medium';
|
|
103
|
+
return 'low';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildIntent(prompt, domain, complexity) {
|
|
107
|
+
return {
|
|
108
|
+
action: detectAction(prompt),
|
|
109
|
+
domain: domain?.id || null,
|
|
110
|
+
domainLabel: domain?.label || null,
|
|
111
|
+
scope: detectScope(prompt, domain),
|
|
112
|
+
complexity,
|
|
113
|
+
risk: detectRisk(prompt, complexity),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function analyzeTask(prompt, preferredProfileId, memory) {
|
|
118
|
+
const lang = analyzePrompt(prompt, memory);
|
|
119
|
+
const tokens = lang.expanded.length ? lang.expanded : lang.tokens;
|
|
120
|
+
const scores = {
|
|
121
|
+
frontend: scoreCategory(tokens, CATEGORIES.frontend),
|
|
122
|
+
backend: scoreCategory(tokens, CATEGORIES.backend),
|
|
123
|
+
architecture: scoreCategory(tokens, CATEGORIES.architecture),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let category = 'mixed';
|
|
127
|
+
let best = 0;
|
|
128
|
+
for (const [id, s] of Object.entries(scores)) {
|
|
129
|
+
if (s > best) {
|
|
130
|
+
best = s;
|
|
131
|
+
category = id;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (best < 10) category = 'mixed';
|
|
135
|
+
|
|
136
|
+
const cat = CATEGORIES[category];
|
|
137
|
+
const profileId =
|
|
138
|
+
preferredProfileId && preferredProfileId !== 'standard' ? preferredProfileId : cat.profileId;
|
|
139
|
+
|
|
140
|
+
const complexity =
|
|
141
|
+
tokens.length > 40 || /refactor|architecture|migrate|entire|across|معماری/i.test(prompt)
|
|
142
|
+
? 'architecture_review'
|
|
143
|
+
: category === 'frontend' && !/api|server|service|database|repository/i.test(prompt)
|
|
144
|
+
? 'simple_ui_fix'
|
|
145
|
+
: /bug|error|fix|crash|fail|باگ|خطا/i.test(prompt) || category === 'backend'
|
|
146
|
+
? 'backend_bug'
|
|
147
|
+
: 'mixed';
|
|
148
|
+
|
|
149
|
+
const domain = lang.domain;
|
|
150
|
+
const symbols = lang.symbols;
|
|
151
|
+
const pathHints = lang.pathHints;
|
|
152
|
+
const intent = buildIntent(prompt, domain, complexity);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
category: cat.id,
|
|
156
|
+
label: cat.label,
|
|
157
|
+
scores,
|
|
158
|
+
profileId,
|
|
159
|
+
complexity,
|
|
160
|
+
tokens,
|
|
161
|
+
includeHints: cat.includeHints,
|
|
162
|
+
deprioritize: cat.deprioritize,
|
|
163
|
+
strategy: `Focus ${cat.label}${domain ? ` / ${domain.label}` : ''}; action=${intent.action}; deprioritize ${cat.deprioritize.join(', ') || 'none'}.`,
|
|
164
|
+
domain,
|
|
165
|
+
symbols,
|
|
166
|
+
pathHints,
|
|
167
|
+
intent,
|
|
168
|
+
language: {
|
|
169
|
+
originalTokens: lang.tokens,
|
|
170
|
+
expanded: lang.expanded,
|
|
171
|
+
identifierParts: lang.identifierParts || [],
|
|
172
|
+
rewrite: lang.rewrite || null,
|
|
173
|
+
},
|
|
174
|
+
identifierParts: lang.identifierParts || [],
|
|
175
|
+
rewrite: lang.rewrite || null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
analyzeTask,
|
|
181
|
+
CATEGORIES,
|
|
182
|
+
DOMAIN_RULES,
|
|
183
|
+
tokenize,
|
|
184
|
+
extractSymbols,
|
|
185
|
+
extractPathHints,
|
|
186
|
+
detectDomain,
|
|
187
|
+
buildIntent,
|
|
188
|
+
expandSynonyms,
|
|
189
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decide whether calling a tool/skill/plugin first reduces tokens without quality loss.
|
|
5
|
+
* Principle: structural lookup tools beat dumping agents/skills/rules into the prompt.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ROUTES = [
|
|
9
|
+
{
|
|
10
|
+
id: 'serena-symbol',
|
|
11
|
+
match: /symbol|definition|where is|find class|find function|پیادهسازی|کجا تعریف/i,
|
|
12
|
+
tool: 'serena',
|
|
13
|
+
savesTokens: 'high',
|
|
14
|
+
qualitySafe: true,
|
|
15
|
+
why: 'Symbol graph answers location without loading whole files',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'grep-exact',
|
|
19
|
+
match: /grep|search string|find usage|where used|جستجو|استفاده شده/i,
|
|
20
|
+
tool: 'grep',
|
|
21
|
+
savesTokens: 'high',
|
|
22
|
+
qualitySafe: true,
|
|
23
|
+
why: 'Exact string search is cheaper than loading folders',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'super-guard-scan',
|
|
27
|
+
match: /lint|rule|architecture|boundary|violation|قانون|معماری|نقض/i,
|
|
28
|
+
tool: 'ziprinSuperGuard.scanCurrentFile',
|
|
29
|
+
savesTokens: 'medium',
|
|
30
|
+
qualitySafe: true,
|
|
31
|
+
why: 'Local rule engine replaces loading all .mdc into chat',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'read-single',
|
|
35
|
+
match: /open file|read this|show me|این فایل/i,
|
|
36
|
+
tool: 'read_file',
|
|
37
|
+
savesTokens: 'medium',
|
|
38
|
+
qualitySafe: true,
|
|
39
|
+
why: 'Targeted read beats @folder',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'avoid-agents-dump',
|
|
43
|
+
match: /./,
|
|
44
|
+
tool: null,
|
|
45
|
+
skip: ['.cursor/agents', '.cursor/skills', 'tooling/project-index.json'],
|
|
46
|
+
savesTokens: 'high',
|
|
47
|
+
qualitySafe: true,
|
|
48
|
+
why: 'Agents/skills corpus is in Super Guard pack — do not paste into chat',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function routeTask(task) {
|
|
53
|
+
const t = String(task || '');
|
|
54
|
+
const hits = [];
|
|
55
|
+
for (const r of ROUTES) {
|
|
56
|
+
if (r.match.test(t)) {
|
|
57
|
+
hits.push({
|
|
58
|
+
id: r.id,
|
|
59
|
+
tool: r.tool,
|
|
60
|
+
skip: r.skip || [],
|
|
61
|
+
savesTokens: r.savesTokens,
|
|
62
|
+
qualitySafe: r.qualitySafe,
|
|
63
|
+
why: r.why,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Prefer concrete tools over the catch-all avoid dump
|
|
68
|
+
const concrete = hits.filter((h) => h.tool);
|
|
69
|
+
const plan = concrete.length ? concrete : hits;
|
|
70
|
+
return {
|
|
71
|
+
task: t,
|
|
72
|
+
plan,
|
|
73
|
+
recommendation: plan[0] || null,
|
|
74
|
+
neverDump: ['.cursor/agents/**', '.cursor/skills/**', '**/reports/**', 'tooling/project-index.json'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shouldInstallSkill(skillName, task) {
|
|
79
|
+
// Only "install"/invoke a skill if it is task-local and quality-safe
|
|
80
|
+
const name = String(skillName || '').toLowerCase();
|
|
81
|
+
const t = String(task || '').toLowerCase();
|
|
82
|
+
if (!name || !t) return { ok: false, why: 'missing skill or task' };
|
|
83
|
+
if (name.includes('agent') && t.length < 20) {
|
|
84
|
+
return { ok: false, why: 'Short tasks should not pull whole agents' };
|
|
85
|
+
}
|
|
86
|
+
if (t.includes(name) || name.split(/[-_]/).some((p) => p.length > 3 && t.includes(p))) {
|
|
87
|
+
return { ok: true, why: 'Skill name appears relevant to task; invoke on-demand only' };
|
|
88
|
+
}
|
|
89
|
+
return { ok: false, why: 'Skill not clearly relevant — skip to save tokens' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { ROUTES, routeTask, shouldInstallSkill };
|
package/src/version.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin MCP launcher — plugin runtime in ~/.ziprin (installed via npm setup).
|
|
4
|
+
* Install/update: npx @ziprin/context-optimizer setup --workspace /path/to/repo
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
const manifestFile = path.join(home, ".ziprin", "ziprin-context-install.json");
|
|
15
|
+
const defaultRoot = path.join(home, ".ziprin", "ziprin-context-optimizer");
|
|
16
|
+
|
|
17
|
+
function resolveRoot() {
|
|
18
|
+
if (process.env.ZIPRIN_CONTEXT_PLUGIN) {
|
|
19
|
+
return path.resolve(process.env.ZIPRIN_CONTEXT_PLUGIN);
|
|
20
|
+
}
|
|
21
|
+
if (fs.existsSync(manifestFile)) {
|
|
22
|
+
try {
|
|
23
|
+
const m = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
24
|
+
if (m.pluginRoot && fs.existsSync(path.join(m.pluginRoot, "src", "mcp-server.js"))) {
|
|
25
|
+
return m.pluginRoot;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
/* ignore */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return defaultRoot;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workspace = process.env.ZIPRIN_WORKSPACE
|
|
35
|
+
? path.resolve(process.env.ZIPRIN_WORKSPACE)
|
|
36
|
+
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
|
+
const pluginRoot = resolveRoot();
|
|
38
|
+
const entry = path.join(pluginRoot, "src", "mcp-server.js");
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(entry)) {
|
|
41
|
+
console.error(
|
|
42
|
+
`[ziprin-context] MCP not installed. Run:\n npx @ziprin/context-optimizer setup --workspace "${workspace}"`,
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const child = spawn(process.execPath, [entry, "--project", workspace], {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
env: { ...process.env, ZIPRIN_WORKSPACE: workspace, ZIPRIN_CONTEXT_PLUGIN: pluginRoot },
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.on("exit", (code, signal) => {
|
|
54
|
+
if (signal) {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(process.pid, signal);
|
|
57
|
+
} catch {
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
process.exit(code ?? 1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on("error", (err) => {
|
|
66
|
+
console.error("[ziprin-context]", err.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|