context-slice 1.8.2
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/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/src/cli.js +199 -0
- package/dist/src/indexer/index.js +325 -0
- package/dist/src/languages/adapter.js +23 -0
- package/dist/src/languages/go/index.js +15 -0
- package/dist/src/languages/go/parse.js +443 -0
- package/dist/src/languages/go/resolve.js +313 -0
- package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
- package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
- package/dist/src/languages/java/enterprise/registry.js +22 -0
- package/dist/src/languages/java/enterprise/spring-data.js +239 -0
- package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
- package/dist/src/languages/java/enterprise/transactions.js +110 -0
- package/dist/src/languages/java.js +84 -0
- package/dist/src/languages/javascript/index.js +25 -0
- package/dist/src/languages/python/index.js +29 -0
- package/dist/src/languages/python/parse.js +415 -0
- package/dist/src/languages/python/resolve.js +413 -0
- package/dist/src/languages/rust/calls-resolve.js +1405 -0
- package/dist/src/languages/rust/index.js +12 -0
- package/dist/src/languages/rust/parse.js +545 -0
- package/dist/src/languages/rust/resolve.js +284 -0
- package/dist/src/languages/typescript/index.js +25 -0
- package/dist/src/languages/typescript/parse.js +793 -0
- package/dist/src/languages/typescript/resolve.js +463 -0
- package/dist/src/package-info.js +16 -0
- package/dist/src/parser/java-parser.js +339 -0
- package/dist/src/planner/budget.js +1 -0
- package/dist/src/planner/composition.js +372 -0
- package/dist/src/planner/rank.js +10 -0
- package/dist/src/render/compact-context.js +11 -0
- package/dist/src/server/mcp-server.js +154 -0
- package/dist/src/storage/sqlite.js +105 -0
- package/dist/src/types/enterprise.js +1 -0
- package/dist/src/types/model.js +1 -0
- package/dist/src/workflow/errors.js +10 -0
- package/dist/src/workflow/preview.js +220 -0
- package/dist/src/workflow/repository.js +56 -0
- package/mcp.json +10 -0
- package/package.json +76 -0
- package/plugin.json +15 -0
- package/queries/java/annotations.scm +1 -0
- package/queries/java/calls.scm +1 -0
- package/queries/java/imports.scm +1 -0
- package/queries/java/symbols.scm +2 -0
- package/skills/context-slice/SKILL.md +60 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { estimateTokens } from "../planner/budget.js";
|
|
4
|
+
import { rankSymbol } from "../planner/rank.js";
|
|
5
|
+
import { renderSkeleton } from "../render/compact-context.js";
|
|
6
|
+
import { composeDependencyContext, composeImportContext, composeJpaContext, composeRouteContext, composeSiblings, composeTransactionContext, } from "../planner/composition.js";
|
|
7
|
+
import { WorkflowError } from "./errors.js";
|
|
8
|
+
/**
|
|
9
|
+
* Composition may use at most this share of the budget, so sibling context
|
|
10
|
+
* fills spare capacity and never crowds out the target, callers or callees.
|
|
11
|
+
*/
|
|
12
|
+
export const COMPOSITION_BUDGET_SHARE = 0.35;
|
|
13
|
+
/** Reads each distinct included file once, in full, as the "without context-slice" cost.
|
|
14
|
+
* A file that can't be read (renamed/deleted since indexing) is skipped rather than guessed —
|
|
15
|
+
* the resulting reduction then understates savings, never overstates them. */
|
|
16
|
+
function wholeFileBaseline(index, included, estimatedTokens) {
|
|
17
|
+
const filePaths = new Set(included.map((item) => item.filePath));
|
|
18
|
+
let wholeFileTokens = 0;
|
|
19
|
+
for (const filePath of filePaths) {
|
|
20
|
+
try {
|
|
21
|
+
wholeFileTokens += estimateTokens(readFileSync(join(index.root, filePath), "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Excluded, not guessed — see doc comment above.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const reduction = wholeFileTokens > 0 ? Math.min(1, Math.max(0, 1 - estimatedTokens / wholeFileTokens)) : 0;
|
|
28
|
+
return { files: filePaths.size, wholeFileTokens, reduction };
|
|
29
|
+
}
|
|
30
|
+
function parameterCount(symbol) {
|
|
31
|
+
const parameters = symbol.signature?.match(/\(([^)]*)\)/)?.[1].trim() ?? "";
|
|
32
|
+
return parameters ? parameters.split(",").length : 0;
|
|
33
|
+
}
|
|
34
|
+
/** Test sources answer "how is this tested", not "how does this work". */
|
|
35
|
+
const isTestPath = (filePath) => /(^|\/)tests?\//i.test(filePath) ||
|
|
36
|
+
/(^|\/)test_[^/]*$/.test(filePath) ||
|
|
37
|
+
/_test\.[^/]+$/.test(filePath) ||
|
|
38
|
+
/Tests?\.java$/.test(filePath) ||
|
|
39
|
+
/\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
40
|
+
/** Keep production candidates when there are any; otherwise keep everything. */
|
|
41
|
+
function preferProduction(symbols) {
|
|
42
|
+
const production = symbols.filter((symbol) => !isTestPath(symbol.filePath));
|
|
43
|
+
return production.length ? production : symbols;
|
|
44
|
+
}
|
|
45
|
+
function chooseTarget(index, task) {
|
|
46
|
+
const exact = index.resolveSymbol(task);
|
|
47
|
+
if (exact.length === 1)
|
|
48
|
+
return exact[0];
|
|
49
|
+
const normalized = task.toLowerCase();
|
|
50
|
+
const named = new RegExp(`\\b(${task.match(/[A-Za-z_][\w$]*/g)?.join("|") ?? ""})\\b`);
|
|
51
|
+
const exactName = preferProduction(index.symbols.filter((symbol) => (symbol.kind === "method" ||
|
|
52
|
+
symbol.kind === "constructor" ||
|
|
53
|
+
symbol.kind === "function") &&
|
|
54
|
+
normalized.includes(symbol.name.toLowerCase()) &&
|
|
55
|
+
named.test(symbol.name))).sort((a, b) => b.name.length - a.name.length ||
|
|
56
|
+
parameterCount(a) - parameterCount(b) ||
|
|
57
|
+
a.id.localeCompare(b.id));
|
|
58
|
+
if (exactName.length)
|
|
59
|
+
return exactName[0];
|
|
60
|
+
const ranked = index
|
|
61
|
+
.search(task, 10)
|
|
62
|
+
.map((result) => index.symbols.find((symbol) => symbol.id === result.id))
|
|
63
|
+
.filter((symbol) => Boolean(symbol));
|
|
64
|
+
const target = preferProduction(ranked)[0];
|
|
65
|
+
if (target)
|
|
66
|
+
return target;
|
|
67
|
+
throw new WorkflowError("SYMBOL_NOT_FOUND", `No indexed symbol matches task: ${task}`, "Run context-slice index, then use a task that names a method, type, or qualified symbol.");
|
|
68
|
+
}
|
|
69
|
+
function renderedSkeleton(index, symbol, relation) {
|
|
70
|
+
const calls = index.calls
|
|
71
|
+
.filter((call) => call.callerId === symbol.id)
|
|
72
|
+
.map((call) => `${call.receiverText ? `${call.receiverText}.` : ""}${call.calleeName}(…)${call.externalPackage ? ` [external: ${call.externalPackage}]` : ""}`);
|
|
73
|
+
return `// ${relation}\n${renderSkeleton(symbol, calls)}`;
|
|
74
|
+
}
|
|
75
|
+
function ranked(symbols, task) {
|
|
76
|
+
return [...symbols].sort((a, b) => rankSymbol(b, task, task) - rankSymbol(a, task, task) ||
|
|
77
|
+
a.id.localeCompare(b.id));
|
|
78
|
+
}
|
|
79
|
+
export function buildPreview(index, task, options = {}) {
|
|
80
|
+
if (!task.trim()) {
|
|
81
|
+
throw new WorkflowError("SYMBOL_NOT_FOUND", "Preview requires a non-empty developer task.", 'Pass a task such as: context-slice preview "explain retryPayment".');
|
|
82
|
+
}
|
|
83
|
+
if (!index.symbols.length) {
|
|
84
|
+
throw new WorkflowError("INDEX_STALE", "No loaded index is available for preview.", "Run context-slice index before requesting a preview.");
|
|
85
|
+
}
|
|
86
|
+
const target = chooseTarget(index, task);
|
|
87
|
+
const targetTokens = estimateTokens(target.source);
|
|
88
|
+
const budget = options.budget ?? Math.max(1200, targetTokens);
|
|
89
|
+
if (budget < targetTokens) {
|
|
90
|
+
throw new WorkflowError("BUDGET_TOO_SMALL", `Budget ${budget} cannot include target ${target.qualifiedName ?? target.name} (${targetTokens} tokens).`, `Increase --budget to at least ${targetTokens}, or select a smaller target.`);
|
|
91
|
+
}
|
|
92
|
+
const included = [];
|
|
93
|
+
const omitted = [];
|
|
94
|
+
const composition = {
|
|
95
|
+
"task target": 0,
|
|
96
|
+
"direct caller": 0,
|
|
97
|
+
"direct callee": 0,
|
|
98
|
+
"enclosing type": 0,
|
|
99
|
+
"enterprise relation": 0,
|
|
100
|
+
"file imports": 0,
|
|
101
|
+
};
|
|
102
|
+
let estimatedTokens = 0;
|
|
103
|
+
const add = (symbol, reason, rendered, explanation, extra = {}) => {
|
|
104
|
+
const tokens = estimateTokens(rendered);
|
|
105
|
+
if (estimatedTokens + tokens > budget) {
|
|
106
|
+
omitted.push({
|
|
107
|
+
symbolId: symbol.id,
|
|
108
|
+
symbol: symbol.qualifiedName ?? symbol.name,
|
|
109
|
+
reason: "context budget",
|
|
110
|
+
estimatedTokens: tokens,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
included.push({
|
|
115
|
+
symbolId: symbol.id,
|
|
116
|
+
symbol: symbol.qualifiedName ?? symbol.name,
|
|
117
|
+
filePath: symbol.filePath,
|
|
118
|
+
reason,
|
|
119
|
+
explanation,
|
|
120
|
+
estimatedTokens: tokens,
|
|
121
|
+
rendered,
|
|
122
|
+
...extra,
|
|
123
|
+
});
|
|
124
|
+
estimatedTokens += tokens;
|
|
125
|
+
composition[reason] += tokens;
|
|
126
|
+
};
|
|
127
|
+
add(target, "task target", target.source, `Selected because the task names ${target.name}.`);
|
|
128
|
+
const related = [
|
|
129
|
+
...ranked(index.callersAtDepth(target, options.depth ?? 1), task).map((symbol) => ({
|
|
130
|
+
symbol,
|
|
131
|
+
reason: "direct caller",
|
|
132
|
+
relation: "Direct caller",
|
|
133
|
+
})),
|
|
134
|
+
...ranked(index.dependenciesAtDepth(target, options.depth ?? 1), task).map((symbol) => ({
|
|
135
|
+
symbol,
|
|
136
|
+
reason: "direct callee",
|
|
137
|
+
relation: "Direct callee",
|
|
138
|
+
})),
|
|
139
|
+
].sort((a, b) => rankSymbol(b.symbol, task, options.intent ?? "") -
|
|
140
|
+
rankSymbol(a.symbol, task, options.intent ?? "") ||
|
|
141
|
+
a.symbol.id.localeCompare(b.symbol.id));
|
|
142
|
+
const includedIds = new Set([target.id]);
|
|
143
|
+
for (const item of related) {
|
|
144
|
+
if (includedIds.has(item.symbol.id))
|
|
145
|
+
continue;
|
|
146
|
+
includedIds.add(item.symbol.id);
|
|
147
|
+
add(item.symbol, item.reason, renderedSkeleton(index, item.symbol, item.relation), `${item.relation} of ${target.qualifiedName ?? target.name}.`);
|
|
148
|
+
}
|
|
149
|
+
const relatedFiles = new Set([...includedIds]
|
|
150
|
+
.map((id) => index.symbols.find((s) => s.id === id)?.filePath)
|
|
151
|
+
.filter((file) => Boolean(file) && file !== target.filePath));
|
|
152
|
+
// Same-enclosing-type and import-context composition run after callers and
|
|
153
|
+
// callees, so they can only use budget they left, and never replace them.
|
|
154
|
+
let compositionTokens = 0;
|
|
155
|
+
const compositionAllowance = Math.floor(budget * COMPOSITION_BUDGET_SHARE);
|
|
156
|
+
const siblings = options.composition === false
|
|
157
|
+
? []
|
|
158
|
+
: composeSiblings(index, target, includedIds);
|
|
159
|
+
const importCandidates = options.composition === false
|
|
160
|
+
? []
|
|
161
|
+
: composeImportContext(index, target, relatedFiles, includedIds);
|
|
162
|
+
const relatedIds = new Set([...includedIds].filter((id) => id !== target.id));
|
|
163
|
+
const routeCandidates = options.composition === false
|
|
164
|
+
? []
|
|
165
|
+
: composeRouteContext(index, target, relatedIds, includedIds);
|
|
166
|
+
const dependencyCandidates = options.composition === false
|
|
167
|
+
? []
|
|
168
|
+
: composeDependencyContext(index, target, relatedIds, includedIds);
|
|
169
|
+
const transactionCandidates = options.composition === false
|
|
170
|
+
? []
|
|
171
|
+
: composeTransactionContext(index, target, relatedIds, includedIds);
|
|
172
|
+
const jpaCandidates = options.composition === false
|
|
173
|
+
? []
|
|
174
|
+
: composeJpaContext(index, target, relatedIds, includedIds);
|
|
175
|
+
for (const candidate of [
|
|
176
|
+
...siblings,
|
|
177
|
+
...importCandidates,
|
|
178
|
+
...routeCandidates,
|
|
179
|
+
...dependencyCandidates,
|
|
180
|
+
...transactionCandidates,
|
|
181
|
+
...jpaCandidates,
|
|
182
|
+
]) {
|
|
183
|
+
if (candidate.symbol && includedIds.has(candidate.symbol.id))
|
|
184
|
+
continue;
|
|
185
|
+
if (compositionTokens + candidate.estimatedTokens > compositionAllowance) {
|
|
186
|
+
omitted.push({
|
|
187
|
+
symbolId: candidate.symbol?.id,
|
|
188
|
+
symbol: candidate.label,
|
|
189
|
+
reason: "composition budget share",
|
|
190
|
+
estimatedTokens: candidate.estimatedTokens,
|
|
191
|
+
evidence: candidate.evidence,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (candidate.symbol)
|
|
196
|
+
includedIds.add(candidate.symbol.id);
|
|
197
|
+
compositionTokens += candidate.estimatedTokens;
|
|
198
|
+
add(candidate.symbol ?? target, candidate.reason, candidate.rendered, candidate.evidence.join("; "), {
|
|
199
|
+
evidence: candidate.evidence,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const unresolved = index.calls
|
|
203
|
+
.filter((call) => call.callerId === target.id && !call.resolvedTargetId)
|
|
204
|
+
.map((call) => ({ calleeName: call.calleeName, evidence: call.evidence }))
|
|
205
|
+
.sort((a, b) => a.calleeName.localeCompare(b.calleeName));
|
|
206
|
+
return {
|
|
207
|
+
task,
|
|
208
|
+
target,
|
|
209
|
+
budget,
|
|
210
|
+
estimatedTokens,
|
|
211
|
+
rendered: included.map((item) => item.rendered).join("\n\n"),
|
|
212
|
+
included,
|
|
213
|
+
omitted,
|
|
214
|
+
unresolved,
|
|
215
|
+
confidence: unresolved.length ? "mixed" : "high",
|
|
216
|
+
freshness: index.inspect(),
|
|
217
|
+
composition,
|
|
218
|
+
baseline: wholeFileBaseline(index, included, estimatedTokens),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { adapterFor, ignoredDirectories } from "../languages/adapter.js";
|
|
4
|
+
import "../languages/java.js";
|
|
5
|
+
import "../languages/typescript/index.js";
|
|
6
|
+
import "../languages/javascript/index.js";
|
|
7
|
+
import "../languages/python/index.js";
|
|
8
|
+
import "../languages/go/index.js";
|
|
9
|
+
import { WorkflowError } from "./errors.js";
|
|
10
|
+
const ignored = new Set([
|
|
11
|
+
".git",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"target",
|
|
14
|
+
"build",
|
|
15
|
+
"dist",
|
|
16
|
+
"out",
|
|
17
|
+
".gradle",
|
|
18
|
+
".idea",
|
|
19
|
+
".vscode",
|
|
20
|
+
".context-slice",
|
|
21
|
+
...ignoredDirectories(),
|
|
22
|
+
]);
|
|
23
|
+
function hasSupportedSource(directory) {
|
|
24
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
25
|
+
if (ignored.has(entry.name))
|
|
26
|
+
continue;
|
|
27
|
+
const path = resolve(directory, entry.name);
|
|
28
|
+
if (entry.isFile() && adapterFor(entry.name))
|
|
29
|
+
return true;
|
|
30
|
+
if (entry.isDirectory() && hasSupportedSource(path))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function nearestGitRoot(start) {
|
|
36
|
+
let current = resolve(start);
|
|
37
|
+
while (true) {
|
|
38
|
+
if (existsSync(resolve(current, ".git")))
|
|
39
|
+
return current;
|
|
40
|
+
const parent = dirname(current);
|
|
41
|
+
if (parent === current)
|
|
42
|
+
return undefined;
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function resolveRepositoryRoot(options = {}) {
|
|
47
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
48
|
+
const root = options.repository
|
|
49
|
+
? resolve(options.repository)
|
|
50
|
+
: (nearestGitRoot(cwd) ?? cwd);
|
|
51
|
+
if (!existsSync(root))
|
|
52
|
+
throw new WorkflowError("REPOSITORY_NOT_FOUND", `Repository not found: ${root}`, "Pass an existing directory with --repo.");
|
|
53
|
+
if (!hasSupportedSource(root))
|
|
54
|
+
throw new WorkflowError("NO_SUPPORTED_SOURCE", `No supported source found in: ${root}`, "Run ContextSlice in a Java or TypeScript repository, or pass --repo.");
|
|
55
|
+
return root;
|
|
56
|
+
}
|
package/mcp.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-slice",
|
|
3
|
+
"version": "1.8.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/nvxtien/context-slice.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/nvxtien/context-slice/issues"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/src",
|
|
18
|
+
"queries",
|
|
19
|
+
"skills",
|
|
20
|
+
"plugin.json",
|
|
21
|
+
"mcp.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"context-slice": "dist/src/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"prepack": "npm run build",
|
|
31
|
+
"start": "node dist/src/server/mcp-server.js",
|
|
32
|
+
"cli": "tsx src/cli.ts",
|
|
33
|
+
"package-smoke": "tsx scripts/package-smoke-test.ts",
|
|
34
|
+
"release:rc": "tsx scripts/clean-room-rc.ts",
|
|
35
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
36
|
+
"benchmark": "tsx benchmarks/benchmark.ts",
|
|
37
|
+
"benchmark:checkouts": "tsx benchmarks/fetch-checkouts.ts",
|
|
38
|
+
"benchmark:v03": "npm run benchmark:checkouts && tsx benchmarks/v0.3-real-repositories.ts",
|
|
39
|
+
"benchmark:v04": "npm run benchmark:v03 && tsx benchmarks/v0.4-symbol-index-hardening.ts",
|
|
40
|
+
"benchmark:v05": "npm run benchmark:v04 && tsx benchmarks/v0.5-semantic-call-resolution.ts",
|
|
41
|
+
"benchmark:v06": "npm run benchmark:v05 && tsx benchmarks/v0.6-developer-context-efficiency.ts",
|
|
42
|
+
"benchmark:v07": "tsx benchmarks/v0.7-developer-workflow.ts",
|
|
43
|
+
"benchmark:v08": "npm run package-smoke",
|
|
44
|
+
"format": "prettier --write .",
|
|
45
|
+
"format:check": "prettier --check .",
|
|
46
|
+
"benchmark:v11": "npm run benchmark:checkouts && tsx benchmarks/v1.1-typescript-support.ts",
|
|
47
|
+
"benchmark:v12": "npm run benchmark:checkouts && tsx benchmarks/v1.2-context-composition.ts",
|
|
48
|
+
"benchmark:v13": "npm run benchmark:checkouts && tsx benchmarks/v1.3-python-support.ts",
|
|
49
|
+
"benchmark:v16": "npm run benchmark:checkouts && tsx benchmarks/v1.6-go-support.ts",
|
|
50
|
+
"benchmark:v17": "npm run benchmark:checkouts && tsx benchmarks/v1.7-javascript-support.ts",
|
|
51
|
+
"benchmark:v15-phase1": "npm run benchmark:checkouts && tsx benchmarks/v1.5-rust-real-repositories.ts",
|
|
52
|
+
"benchmark:v15-phase3": "npm run benchmark:checkouts && tsx benchmarks/v1.5-rust-tasks.ts",
|
|
53
|
+
"benchmark:v14-phase1": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase1-spring-mvc.ts",
|
|
54
|
+
"benchmark:v14-phase2": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase2-dependency-injection.ts",
|
|
55
|
+
"benchmark:v14-phase3": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase3-transactions.ts",
|
|
56
|
+
"benchmark:v14-phase4": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase4-jpa-spring-data.ts"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
60
|
+
"better-sqlite3": "^11.8.1",
|
|
61
|
+
"tree-sitter": "^0.21.1",
|
|
62
|
+
"tree-sitter-go": "^0.23.4",
|
|
63
|
+
"tree-sitter-java": "^0.23.5",
|
|
64
|
+
"tree-sitter-python": "^0.21.0",
|
|
65
|
+
"tree-sitter-rust": "0.21.0",
|
|
66
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
67
|
+
"zod": "^3.25.76"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
71
|
+
"@types/node": "^22.15.21",
|
|
72
|
+
"prettier": "^3.9.8",
|
|
73
|
+
"tsx": "^4.19.4",
|
|
74
|
+
"typescript": "^5.8.3"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "context-slice",
|
|
4
|
+
"version": "1.8.2",
|
|
5
|
+
"description": "Structural code-context slicing for Java, TypeScript/TSX, JavaScript, Python, Rust, and Go. Serves a budget-bounded, relevance-ranked slice of a repository instead of whole files.",
|
|
6
|
+
"skills": "./skills/",
|
|
7
|
+
"mcpServers": "./mcp.json",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "tien.nguyen"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/nvxtien/context-slice",
|
|
12
|
+
"repository": "https://github.com/nvxtien/context-slice",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"keywords": ["context", "code-search", "mcp", "tree-sitter", "context-window"]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(annotation) @annotation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(method_invocation name: (identifier) @call.name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(import_declaration) @import
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: context-slice
|
|
3
|
+
description: Use before reading source files to understand, implement, or explain something in a Java, TypeScript/TSX, JavaScript, Python, Rust, or Go repository. Serves a budget-bounded, relevance-ranked slice of the codebase (target symbol plus ranked callers/callees) through MCP tools instead of whole-file reads, shrinking how much context window the task consumes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Context Slice
|
|
7
|
+
|
|
8
|
+
Context Slice is an MCP server that indexes a repository's symbols and call
|
|
9
|
+
graph (via Tree-sitter, no LLM involved) and serves a compact, explainable
|
|
10
|
+
slice of it — not whole files. Prefer its tools over `Read`/`Grep` on source
|
|
11
|
+
files whenever the task is about a specific symbol, function, class, or
|
|
12
|
+
behavior in a supported repository, so the model spends tokens on the
|
|
13
|
+
relevant code instead of re-reading entire files to find it.
|
|
14
|
+
|
|
15
|
+
**Supported languages:** Java, TypeScript, TSX, JavaScript, Python, Rust, Go.
|
|
16
|
+
For any other language, or a question with no clear target symbol (e.g.
|
|
17
|
+
"what does this project do overall"), fall back to normal file reading.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. **Start with `context.preview`** for any implementation or explanation
|
|
22
|
+
task: `{ task: "<the task in the user's own words>" }`. It returns a
|
|
23
|
+
selected target symbol, its rendered body, ranked direct callers/callees
|
|
24
|
+
included under the token budget, inclusion/omission explanations, and any
|
|
25
|
+
unresolved calls. Read the explanations — they say *why* each piece was
|
|
26
|
+
included, and what was left out and why.
|
|
27
|
+
2. **Do not assume an unresolved call has a concrete implementation.**
|
|
28
|
+
Tree-sitter analysis cannot prove runtime dispatch (reflection, DI
|
|
29
|
+
proxies, framework-generated code, Python/Go dynamic dispatch). An
|
|
30
|
+
unresolved call in the result means exactly that — unresolved, not
|
|
31
|
+
"has no implementation."
|
|
32
|
+
3. **Drill in with the other tools once you know the target symbol's id or
|
|
33
|
+
name:**
|
|
34
|
+
- `context.symbol` — read one symbol as `signature` / `skeleton` / `body`
|
|
35
|
+
/ `full` source. Use the smallest `detail` level that answers the
|
|
36
|
+
question; only use `full` when the exact source text matters.
|
|
37
|
+
- `context.callers` — bounded-depth callers of a symbol (`depth` 1-5).
|
|
38
|
+
- `context.slice` — a strict-budget slice centered on one symbol instead
|
|
39
|
+
of a task description.
|
|
40
|
+
- `context.search` — find candidate symbols by name/text when you don't
|
|
41
|
+
yet know the exact target.
|
|
42
|
+
- `context.diff` — a `git diff` under a strict token budget, when the
|
|
43
|
+
task is about recent changes.
|
|
44
|
+
4. **Only fall back to `Read`/`Grep` on raw source files** when: the
|
|
45
|
+
language isn't supported, the repository has no index yet and a quick
|
|
46
|
+
one-off answer is needed, or a tool result's explanation says the
|
|
47
|
+
information isn't in the index (e.g. a file outside the checked-out
|
|
48
|
+
source, or an unresolved dynamic call you must manually verify).
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
- The index lives in `.context-slice/` inside the target repository and
|
|
53
|
+
refreshes automatically on every tool call — changed files are never
|
|
54
|
+
served stale, and there is no separate "build the index" step to run
|
|
55
|
+
first.
|
|
56
|
+
- Every tool call targets the project Claude Code currently has open
|
|
57
|
+
(`CONTEXT_SLICE_ROOT`), not the context-slice plugin's own source.
|
|
58
|
+
- All tool output is structural (symbols, call edges, source text) derived
|
|
59
|
+
from the repository itself — never benchmark answers, expected results, or
|
|
60
|
+
anything from outside the checked-out source.
|