opencode-context 1.0.7 → 1.0.8
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 +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +126 -22
- package/dist/plugin.js +79 -14
- package/dist/search.d.ts +2 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Semantic code search with ranked file matches and contextual line snippets showing exactly where matches occur. Available as CLI and OpenCode plugin.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -97,11 +97,14 @@ The plugin registers the `find_files` tool.
|
|
|
97
97
|
| `includeTests` | boolean | false | Include test files |
|
|
98
98
|
| `includeConfigs` | boolean | false | Include config files |
|
|
99
99
|
| `includeDocs` | boolean | false | Include documentation |
|
|
100
|
+
| `includeLinePreviews` | boolean | false | Show matching line snippets |
|
|
101
|
+
| `maxSnippetsPerFile` | number | 3 | Max line snippets per file |
|
|
100
102
|
|
|
101
103
|
**Example:**
|
|
102
104
|
|
|
103
105
|
```
|
|
104
106
|
find_files query="auth middleware" maxFiles=5
|
|
107
|
+
find_files query="database" includeLinePreviews=true maxSnippetsPerFile=5
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
**Scoring algorithm considers:**
|
|
@@ -133,15 +136,17 @@ opencode-context -q "database" --json
|
|
|
133
136
|
| `-j, --json` | JSON output | false |
|
|
134
137
|
| `-d, --detailed` | Show match reasons | false |
|
|
135
138
|
| `-i, --interactive` | Interactive mode | false |
|
|
139
|
+
| `--line-previews` | Show matching line snippets | false |
|
|
140
|
+
| `--max-snippets` | Max snippets per file | 3 |
|
|
136
141
|
|
|
137
142
|
## How It Works
|
|
138
143
|
|
|
139
144
|
1. Scans directory with fast-glob
|
|
140
|
-
2. Extracts metadata (size, language, exports, imports)
|
|
145
|
+
2. Extracts metadata (size, language, exports, imports, **line index**)
|
|
141
146
|
3. Scores each file against query
|
|
142
|
-
4. Returns ranked results
|
|
147
|
+
4. Returns ranked results with optional line-level previews
|
|
143
148
|
|
|
144
|
-
No index persistence - scans fresh on each query.
|
|
149
|
+
No index persistence - scans fresh on each query. Line previews show the exact context where matches occur, with surrounding lines for full context.
|
|
145
150
|
|
|
146
151
|
## Uninstallation
|
|
147
152
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiQ1C,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7960,6 +7960,61 @@ var DOC_PATTERNS = [
|
|
|
7960
7960
|
];
|
|
7961
7961
|
|
|
7962
7962
|
// src/search.ts
|
|
7963
|
+
function tokenize(text) {
|
|
7964
|
+
return text.toLowerCase().split(/[^a-zA-Z0-9_]+/).filter((w2) => w2.length > 2);
|
|
7965
|
+
}
|
|
7966
|
+
function buildLineIndex(lines) {
|
|
7967
|
+
const index = new Map;
|
|
7968
|
+
lines.forEach((line, idx) => {
|
|
7969
|
+
const terms = tokenize(line);
|
|
7970
|
+
terms.forEach((term) => {
|
|
7971
|
+
if (!index.has(term)) {
|
|
7972
|
+
index.set(term, []);
|
|
7973
|
+
}
|
|
7974
|
+
index.get(term).push(idx + 1);
|
|
7975
|
+
});
|
|
7976
|
+
});
|
|
7977
|
+
return index;
|
|
7978
|
+
}
|
|
7979
|
+
function extractLineSnippets(content, lineIndex, query, maxSnippets = 3) {
|
|
7980
|
+
const matchedLines = new Set;
|
|
7981
|
+
for (const term of [...query.terms, ...query.exactTerms]) {
|
|
7982
|
+
const positions = lineIndex.get(term.toLowerCase());
|
|
7983
|
+
if (positions) {
|
|
7984
|
+
positions.forEach((lineNum) => matchedLines.add(lineNum));
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
if (matchedLines.size === 0) {
|
|
7988
|
+
return [];
|
|
7989
|
+
}
|
|
7990
|
+
const lines = content.split(`
|
|
7991
|
+
`);
|
|
7992
|
+
const sortedLines = Array.from(matchedLines).sort((a2, b2) => a2 - b2);
|
|
7993
|
+
const clusters = [];
|
|
7994
|
+
let currentCluster = [sortedLines[0]];
|
|
7995
|
+
for (let i = 1;i < sortedLines.length; i++) {
|
|
7996
|
+
if (sortedLines[i] - sortedLines[i - 1] <= 3) {
|
|
7997
|
+
currentCluster.push(sortedLines[i]);
|
|
7998
|
+
} else {
|
|
7999
|
+
clusters.push(currentCluster);
|
|
8000
|
+
currentCluster = [sortedLines[i]];
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
clusters.push(currentCluster);
|
|
8004
|
+
const topClusters = clusters.sort((a2, b2) => b2.length - a2.length).slice(0, maxSnippets);
|
|
8005
|
+
return topClusters.map((cluster) => {
|
|
8006
|
+
const centerLine = cluster[Math.floor(cluster.length / 2)];
|
|
8007
|
+
const lineIdx = centerLine - 1;
|
|
8008
|
+
return {
|
|
8009
|
+
lineNumber: centerLine,
|
|
8010
|
+
content: lines[lineIdx]?.trim() || "",
|
|
8011
|
+
context: {
|
|
8012
|
+
before: lines.slice(Math.max(0, lineIdx - 1), lineIdx).map((l2) => l2.trim()),
|
|
8013
|
+
after: lines.slice(lineIdx + 1, Math.min(lines.length, lineIdx + 2)).map((l2) => l2.trim())
|
|
8014
|
+
}
|
|
8015
|
+
};
|
|
8016
|
+
});
|
|
8017
|
+
}
|
|
7963
8018
|
function parseQuery(query) {
|
|
7964
8019
|
if (!query || typeof query !== "string") {
|
|
7965
8020
|
return {
|
|
@@ -8043,7 +8098,7 @@ function detectLanguage(filePath) {
|
|
|
8043
8098
|
}
|
|
8044
8099
|
return;
|
|
8045
8100
|
}
|
|
8046
|
-
async function extractMetadata(filePath) {
|
|
8101
|
+
async function extractMetadata(filePath, includeLineIndex = false) {
|
|
8047
8102
|
const stats = await fs.stat(filePath);
|
|
8048
8103
|
const content = await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
8049
8104
|
const lines = content.split(`
|
|
@@ -8058,7 +8113,9 @@ async function extractMetadata(filePath) {
|
|
|
8058
8113
|
isDoc: isDocFile(filePath),
|
|
8059
8114
|
language: detectLanguage(filePath),
|
|
8060
8115
|
exports: extractExports(content),
|
|
8061
|
-
imports: extractImports(content)
|
|
8116
|
+
imports: extractImports(content),
|
|
8117
|
+
lineIndex: includeLineIndex ? buildLineIndex(lines) : undefined,
|
|
8118
|
+
content: includeLineIndex ? content : undefined
|
|
8062
8119
|
};
|
|
8063
8120
|
}
|
|
8064
8121
|
function extractExports(content) {
|
|
@@ -8165,33 +8222,39 @@ function calculateFilepathScore(filePath, query) {
|
|
|
8165
8222
|
}
|
|
8166
8223
|
return { score: totalScore, reasons };
|
|
8167
8224
|
}
|
|
8168
|
-
async function calculateContentScore(filePath, query,
|
|
8225
|
+
async function calculateContentScore(filePath, query, metadata, maxSnippets) {
|
|
8169
8226
|
const reasons = [];
|
|
8170
8227
|
let totalScore = 0;
|
|
8171
|
-
const fileContent = content ?? await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
8228
|
+
const fileContent = metadata?.content ?? await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
8172
8229
|
const normalizedContent = fileContent.toLowerCase();
|
|
8173
|
-
const lines = fileContent.split(`
|
|
8174
|
-
`);
|
|
8175
8230
|
for (const term of query.terms) {
|
|
8176
8231
|
const occurrences = (normalizedContent.match(new RegExp(term, "g")) || []).length;
|
|
8177
8232
|
if (occurrences > 0) {
|
|
8178
8233
|
const contribution = Math.min(DEFAULT_WEIGHTS.content, DEFAULT_WEIGHTS.content * (occurrences / 5));
|
|
8179
8234
|
totalScore += contribution;
|
|
8180
|
-
|
|
8235
|
+
const reason = {
|
|
8181
8236
|
type: "content",
|
|
8182
8237
|
description: `Content contains "${term}" (${occurrences} occurrences)`,
|
|
8183
8238
|
contribution
|
|
8184
|
-
}
|
|
8239
|
+
};
|
|
8240
|
+
if (metadata?.lineIndex && metadata.content && maxSnippets && maxSnippets > 0) {
|
|
8241
|
+
reason.lineSnippets = extractLineSnippets(metadata.content, metadata.lineIndex, query, maxSnippets);
|
|
8242
|
+
}
|
|
8243
|
+
reasons.push(reason);
|
|
8185
8244
|
}
|
|
8186
8245
|
}
|
|
8187
8246
|
for (const exact of query.exactTerms) {
|
|
8188
8247
|
if (normalizedContent.includes(exact.toLowerCase())) {
|
|
8189
8248
|
totalScore += DEFAULT_WEIGHTS.content * 1.5;
|
|
8190
|
-
|
|
8249
|
+
const reason = {
|
|
8191
8250
|
type: "content",
|
|
8192
8251
|
description: `Exact phrase in content: "${exact}"`,
|
|
8193
8252
|
contribution: DEFAULT_WEIGHTS.content * 1.5
|
|
8194
|
-
}
|
|
8253
|
+
};
|
|
8254
|
+
if (metadata?.lineIndex && metadata.content && maxSnippets && maxSnippets > 0) {
|
|
8255
|
+
reason.lineSnippets = extractLineSnippets(metadata.content, metadata.lineIndex, query, maxSnippets);
|
|
8256
|
+
}
|
|
8257
|
+
reasons.push(reason);
|
|
8195
8258
|
}
|
|
8196
8259
|
}
|
|
8197
8260
|
for (const term of query.terms) {
|
|
@@ -8279,14 +8342,14 @@ function calculateMetadataScore(metadata, query) {
|
|
|
8279
8342
|
}
|
|
8280
8343
|
return { score: totalScore, reasons };
|
|
8281
8344
|
}
|
|
8282
|
-
async function calculateFileScore(filePath, query, metadata, rootPath, searchContent) {
|
|
8345
|
+
async function calculateFileScore(filePath, query, metadata, rootPath, searchContent, maxSnippets) {
|
|
8283
8346
|
const relativePath = relative(rootPath, filePath);
|
|
8284
8347
|
const filenameResult = calculateFilenameScore(filePath, query);
|
|
8285
8348
|
const filepathResult = calculateFilepathScore(filePath, query);
|
|
8286
8349
|
const metadataResult = calculateMetadataScore(metadata, query);
|
|
8287
8350
|
let contentResult = { score: 0, reasons: [] };
|
|
8288
8351
|
if (searchContent && metadata.size < 1024 * 1024) {
|
|
8289
|
-
contentResult = await calculateContentScore(filePath, query);
|
|
8352
|
+
contentResult = await calculateContentScore(filePath, query, metadata, maxSnippets);
|
|
8290
8353
|
}
|
|
8291
8354
|
const totalScore = Math.min(100, filenameResult.score + filepathResult.score + metadataResult.score + contentResult.score);
|
|
8292
8355
|
const allReasons = [
|
|
@@ -8312,6 +8375,8 @@ async function searchFiles(options) {
|
|
|
8312
8375
|
const minScore = options.minScore || 15;
|
|
8313
8376
|
const searchContent = options.searchContent ?? true;
|
|
8314
8377
|
const maxFileSize = options.maxFileSize || 1024 * 1024;
|
|
8378
|
+
const includeLinePreviews = options.includeLinePreviews ?? false;
|
|
8379
|
+
const maxSnippetsPerFile = options.maxSnippetsPerFile || 3;
|
|
8315
8380
|
const excludePatterns = [
|
|
8316
8381
|
...DEFAULT_EXCLUDE_PATTERNS,
|
|
8317
8382
|
...options.exclude || []
|
|
@@ -8338,8 +8403,8 @@ async function searchFiles(options) {
|
|
|
8338
8403
|
const stats = await fs.stat(filePath);
|
|
8339
8404
|
if (stats.size > maxFileSize)
|
|
8340
8405
|
continue;
|
|
8341
|
-
const metadata = await extractMetadata(filePath);
|
|
8342
|
-
const match = await calculateFileScore(filePath, parsedQuery, metadata, rootPath, searchContent);
|
|
8406
|
+
const metadata = await extractMetadata(filePath, includeLinePreviews);
|
|
8407
|
+
const match = await calculateFileScore(filePath, parsedQuery, metadata, rootPath, searchContent, includeLinePreviews ? maxSnippetsPerFile : 0);
|
|
8343
8408
|
if (match.score >= minScore) {
|
|
8344
8409
|
matches.push(match);
|
|
8345
8410
|
}
|
|
@@ -8371,7 +8436,25 @@ function formatFileSize(bytes) {
|
|
|
8371
8436
|
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
8372
8437
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
8373
8438
|
}
|
|
8374
|
-
function
|
|
8439
|
+
function formatLineSnippet(snippet, maxWidth = 80) {
|
|
8440
|
+
const lines = [];
|
|
8441
|
+
const separator = import_picocolors3.default.gray("│");
|
|
8442
|
+
snippet.context.before.forEach((line, idx) => {
|
|
8443
|
+
const lineNum = snippet.lineNumber - snippet.context.before.length + idx;
|
|
8444
|
+
const trimmed = line.slice(0, maxWidth - 10);
|
|
8445
|
+
lines.push(` ${import_picocolors3.default.gray(String(lineNum).padStart(4))} ${separator} ${import_picocolors3.default.gray(trimmed)}`);
|
|
8446
|
+
});
|
|
8447
|
+
const mainLine = snippet.content.slice(0, maxWidth - 10);
|
|
8448
|
+
lines.push(` ${import_picocolors3.default.cyan(String(snippet.lineNumber).padStart(4))} ${separator} ${mainLine}`);
|
|
8449
|
+
snippet.context.after.forEach((line, idx) => {
|
|
8450
|
+
const lineNum = snippet.lineNumber + 1 + idx;
|
|
8451
|
+
const trimmed = line.slice(0, maxWidth - 10);
|
|
8452
|
+
lines.push(` ${import_picocolors3.default.gray(String(lineNum).padStart(4))} ${separator} ${import_picocolors3.default.gray(trimmed)}`);
|
|
8453
|
+
});
|
|
8454
|
+
return lines.join(`
|
|
8455
|
+
`);
|
|
8456
|
+
}
|
|
8457
|
+
function formatResults(result, detailed = false, showLinePreviews = false) {
|
|
8375
8458
|
console.log("");
|
|
8376
8459
|
if (result.files.length === 0) {
|
|
8377
8460
|
console.log(import_picocolors3.default.yellow("No matching files found."));
|
|
@@ -8390,8 +8473,17 @@ function formatResults(result, detailed = false) {
|
|
|
8390
8473
|
for (const reason of file.reasons) {
|
|
8391
8474
|
const icon = reason.type === "filename" ? "\uD83D\uDCDD" : reason.type === "filepath" ? "\uD83D\uDCC1" : reason.type === "content" ? "\uD83D\uDCC4" : reason.type === "function" ? "⚡" : reason.type === "class" ? "\uD83C\uDFD7️" : reason.type === "export" ? "↗️" : reason.type === "test" ? "\uD83E\uDDEA" : reason.type === "config" ? "⚙️" : "•";
|
|
8392
8475
|
console.log(` ${icon} ${import_picocolors3.default.gray(reason.description)}`);
|
|
8476
|
+
if (showLinePreviews && reason.lineSnippets && reason.lineSnippets.length > 0) {
|
|
8477
|
+
console.log("");
|
|
8478
|
+
for (const snippet of reason.lineSnippets) {
|
|
8479
|
+
console.log(formatLineSnippet(snippet));
|
|
8480
|
+
}
|
|
8481
|
+
console.log("");
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8484
|
+
if (!showLinePreviews || !file.reasons.some((r2) => r2.lineSnippets && r2.lineSnippets.length > 0)) {
|
|
8485
|
+
console.log("");
|
|
8393
8486
|
}
|
|
8394
|
-
console.log("");
|
|
8395
8487
|
}
|
|
8396
8488
|
}
|
|
8397
8489
|
}
|
|
@@ -8444,6 +8536,14 @@ async function interactiveMode() {
|
|
|
8444
8536
|
fe(import_picocolors3.default.yellow("Cancelled"));
|
|
8445
8537
|
process.exit(0);
|
|
8446
8538
|
}
|
|
8539
|
+
const showLinePreviews = await me({
|
|
8540
|
+
message: "Show matching line snippets?",
|
|
8541
|
+
initialValue: false
|
|
8542
|
+
});
|
|
8543
|
+
if (BD(showLinePreviews)) {
|
|
8544
|
+
fe(import_picocolors3.default.yellow("Cancelled"));
|
|
8545
|
+
process.exit(0);
|
|
8546
|
+
}
|
|
8447
8547
|
const s = L2();
|
|
8448
8548
|
s.start("Searching...");
|
|
8449
8549
|
try {
|
|
@@ -8453,11 +8553,13 @@ async function interactiveMode() {
|
|
|
8453
8553
|
includeTests: includeType === "all" || includeType === "tests",
|
|
8454
8554
|
includeConfigs: includeType === "all" || includeType === "configs",
|
|
8455
8555
|
includeDocs: includeType === "all",
|
|
8456
|
-
searchContent: true
|
|
8556
|
+
searchContent: true,
|
|
8557
|
+
includeLinePreviews: showLinePreviews,
|
|
8558
|
+
maxSnippetsPerFile: 3
|
|
8457
8559
|
};
|
|
8458
8560
|
const result = await searchFiles(options);
|
|
8459
8561
|
s.stop(`Search complete`);
|
|
8460
|
-
formatResults(result, detailed);
|
|
8562
|
+
formatResults(result, detailed, showLinePreviews);
|
|
8461
8563
|
fe(import_picocolors3.default.green(`Found ${result.files.length} files`));
|
|
8462
8564
|
} catch (error) {
|
|
8463
8565
|
s.stop("Search failed");
|
|
@@ -8468,8 +8570,8 @@ async function interactiveMode() {
|
|
|
8468
8570
|
}
|
|
8469
8571
|
function runCLI() {
|
|
8470
8572
|
const program2 = new Command;
|
|
8471
|
-
program2.name("opencode-context").description("
|
|
8472
|
-
program2.option("-q, --query <query>", "Search query").option("-n, --max-files <number>", "Maximum number of results", "5").option("--min-score <score>", "Minimum relevance score (0-100)", "15").option("-p, --path <path>", "Root path to search from", process.cwd()).option("--include-tests", "Include test files", false).option("--include-configs", "Include configuration files", false).option("--include-docs", "Include documentation files", false).option("--no-content", "Skip content search (faster)").option("--max-size <bytes>", "Maximum file size to read", "1048576").option("-j, --json", "Output as JSON", false).option("-d, --detailed", "Show detailed match reasons", false).option("-i, --interactive", "Interactive mode", false).action(async (options) => {
|
|
8573
|
+
program2.name("opencode-context").description("Semantic code search with ranked file matches and contextual line snippets").version("1.0.8");
|
|
8574
|
+
program2.option("-q, --query <query>", "Search query").option("-n, --max-files <number>", "Maximum number of results", "5").option("--min-score <score>", "Minimum relevance score (0-100)", "15").option("-p, --path <path>", "Root path to search from", process.cwd()).option("--include-tests", "Include test files", false).option("--include-configs", "Include configuration files", false).option("--include-docs", "Include documentation files", false).option("--no-content", "Skip content search (faster)").option("--max-size <bytes>", "Maximum file size to read", "1048576").option("--line-previews", "Show matching line snippets", false).option("--max-snippets <number>", "Maximum snippets per file", "3").option("-j, --json", "Output as JSON", false).option("-d, --detailed", "Show detailed match reasons", false).option("-i, --interactive", "Interactive mode", false).action(async (options) => {
|
|
8473
8575
|
try {
|
|
8474
8576
|
if (options.interactive || !options.query) {
|
|
8475
8577
|
await interactiveMode();
|
|
@@ -8484,13 +8586,15 @@ function runCLI() {
|
|
|
8484
8586
|
includeConfigs: options.includeConfigs,
|
|
8485
8587
|
includeDocs: options.includeDocs,
|
|
8486
8588
|
searchContent: options.content !== false,
|
|
8487
|
-
maxFileSize: parseInt(options.maxSize, 10)
|
|
8589
|
+
maxFileSize: parseInt(options.maxSize, 10),
|
|
8590
|
+
includeLinePreviews: options.linePreviews,
|
|
8591
|
+
maxSnippetsPerFile: parseInt(options.maxSnippets, 10)
|
|
8488
8592
|
};
|
|
8489
8593
|
const result = await searchFiles(searchOptions);
|
|
8490
8594
|
if (options.json) {
|
|
8491
8595
|
console.log(JSON.stringify(result, null, 2));
|
|
8492
8596
|
} else {
|
|
8493
|
-
formatResults(result, options.detailed);
|
|
8597
|
+
formatResults(result, options.detailed, options.linePreviews);
|
|
8494
8598
|
}
|
|
8495
8599
|
} catch (error) {
|
|
8496
8600
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/plugin.js
CHANGED
|
@@ -17706,6 +17706,61 @@ var DOC_PATTERNS = [
|
|
|
17706
17706
|
];
|
|
17707
17707
|
|
|
17708
17708
|
// src/search.ts
|
|
17709
|
+
function tokenize(text) {
|
|
17710
|
+
return text.toLowerCase().split(/[^a-zA-Z0-9_]+/).filter((w) => w.length > 2);
|
|
17711
|
+
}
|
|
17712
|
+
function buildLineIndex(lines) {
|
|
17713
|
+
const index = new Map;
|
|
17714
|
+
lines.forEach((line, idx) => {
|
|
17715
|
+
const terms = tokenize(line);
|
|
17716
|
+
terms.forEach((term) => {
|
|
17717
|
+
if (!index.has(term)) {
|
|
17718
|
+
index.set(term, []);
|
|
17719
|
+
}
|
|
17720
|
+
index.get(term).push(idx + 1);
|
|
17721
|
+
});
|
|
17722
|
+
});
|
|
17723
|
+
return index;
|
|
17724
|
+
}
|
|
17725
|
+
function extractLineSnippets(content, lineIndex, query, maxSnippets = 3) {
|
|
17726
|
+
const matchedLines = new Set;
|
|
17727
|
+
for (const term of [...query.terms, ...query.exactTerms]) {
|
|
17728
|
+
const positions = lineIndex.get(term.toLowerCase());
|
|
17729
|
+
if (positions) {
|
|
17730
|
+
positions.forEach((lineNum) => matchedLines.add(lineNum));
|
|
17731
|
+
}
|
|
17732
|
+
}
|
|
17733
|
+
if (matchedLines.size === 0) {
|
|
17734
|
+
return [];
|
|
17735
|
+
}
|
|
17736
|
+
const lines = content.split(`
|
|
17737
|
+
`);
|
|
17738
|
+
const sortedLines = Array.from(matchedLines).sort((a, b) => a - b);
|
|
17739
|
+
const clusters = [];
|
|
17740
|
+
let currentCluster = [sortedLines[0]];
|
|
17741
|
+
for (let i = 1;i < sortedLines.length; i++) {
|
|
17742
|
+
if (sortedLines[i] - sortedLines[i - 1] <= 3) {
|
|
17743
|
+
currentCluster.push(sortedLines[i]);
|
|
17744
|
+
} else {
|
|
17745
|
+
clusters.push(currentCluster);
|
|
17746
|
+
currentCluster = [sortedLines[i]];
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
clusters.push(currentCluster);
|
|
17750
|
+
const topClusters = clusters.sort((a, b) => b.length - a.length).slice(0, maxSnippets);
|
|
17751
|
+
return topClusters.map((cluster) => {
|
|
17752
|
+
const centerLine = cluster[Math.floor(cluster.length / 2)];
|
|
17753
|
+
const lineIdx = centerLine - 1;
|
|
17754
|
+
return {
|
|
17755
|
+
lineNumber: centerLine,
|
|
17756
|
+
content: lines[lineIdx]?.trim() || "",
|
|
17757
|
+
context: {
|
|
17758
|
+
before: lines.slice(Math.max(0, lineIdx - 1), lineIdx).map((l) => l.trim()),
|
|
17759
|
+
after: lines.slice(lineIdx + 1, Math.min(lines.length, lineIdx + 2)).map((l) => l.trim())
|
|
17760
|
+
}
|
|
17761
|
+
};
|
|
17762
|
+
});
|
|
17763
|
+
}
|
|
17709
17764
|
function parseQuery(query) {
|
|
17710
17765
|
if (!query || typeof query !== "string") {
|
|
17711
17766
|
return {
|
|
@@ -17789,7 +17844,7 @@ function detectLanguage(filePath) {
|
|
|
17789
17844
|
}
|
|
17790
17845
|
return;
|
|
17791
17846
|
}
|
|
17792
|
-
async function extractMetadata(filePath) {
|
|
17847
|
+
async function extractMetadata(filePath, includeLineIndex = false) {
|
|
17793
17848
|
const stats = await fs.stat(filePath);
|
|
17794
17849
|
const content = await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
17795
17850
|
const lines = content.split(`
|
|
@@ -17804,7 +17859,9 @@ async function extractMetadata(filePath) {
|
|
|
17804
17859
|
isDoc: isDocFile(filePath),
|
|
17805
17860
|
language: detectLanguage(filePath),
|
|
17806
17861
|
exports: extractExports(content),
|
|
17807
|
-
imports: extractImports(content)
|
|
17862
|
+
imports: extractImports(content),
|
|
17863
|
+
lineIndex: includeLineIndex ? buildLineIndex(lines) : undefined,
|
|
17864
|
+
content: includeLineIndex ? content : undefined
|
|
17808
17865
|
};
|
|
17809
17866
|
}
|
|
17810
17867
|
function extractExports(content) {
|
|
@@ -17911,33 +17968,39 @@ function calculateFilepathScore(filePath, query) {
|
|
|
17911
17968
|
}
|
|
17912
17969
|
return { score: totalScore, reasons };
|
|
17913
17970
|
}
|
|
17914
|
-
async function calculateContentScore(filePath, query,
|
|
17971
|
+
async function calculateContentScore(filePath, query, metadata, maxSnippets) {
|
|
17915
17972
|
const reasons = [];
|
|
17916
17973
|
let totalScore = 0;
|
|
17917
|
-
const fileContent = content ?? await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
17974
|
+
const fileContent = metadata?.content ?? await fs.readFile(filePath, "utf-8").catch(() => "");
|
|
17918
17975
|
const normalizedContent = fileContent.toLowerCase();
|
|
17919
|
-
const lines = fileContent.split(`
|
|
17920
|
-
`);
|
|
17921
17976
|
for (const term of query.terms) {
|
|
17922
17977
|
const occurrences = (normalizedContent.match(new RegExp(term, "g")) || []).length;
|
|
17923
17978
|
if (occurrences > 0) {
|
|
17924
17979
|
const contribution = Math.min(DEFAULT_WEIGHTS.content, DEFAULT_WEIGHTS.content * (occurrences / 5));
|
|
17925
17980
|
totalScore += contribution;
|
|
17926
|
-
|
|
17981
|
+
const reason = {
|
|
17927
17982
|
type: "content",
|
|
17928
17983
|
description: `Content contains "${term}" (${occurrences} occurrences)`,
|
|
17929
17984
|
contribution
|
|
17930
|
-
}
|
|
17985
|
+
};
|
|
17986
|
+
if (metadata?.lineIndex && metadata.content && maxSnippets && maxSnippets > 0) {
|
|
17987
|
+
reason.lineSnippets = extractLineSnippets(metadata.content, metadata.lineIndex, query, maxSnippets);
|
|
17988
|
+
}
|
|
17989
|
+
reasons.push(reason);
|
|
17931
17990
|
}
|
|
17932
17991
|
}
|
|
17933
17992
|
for (const exact of query.exactTerms) {
|
|
17934
17993
|
if (normalizedContent.includes(exact.toLowerCase())) {
|
|
17935
17994
|
totalScore += DEFAULT_WEIGHTS.content * 1.5;
|
|
17936
|
-
|
|
17995
|
+
const reason = {
|
|
17937
17996
|
type: "content",
|
|
17938
17997
|
description: `Exact phrase in content: "${exact}"`,
|
|
17939
17998
|
contribution: DEFAULT_WEIGHTS.content * 1.5
|
|
17940
|
-
}
|
|
17999
|
+
};
|
|
18000
|
+
if (metadata?.lineIndex && metadata.content && maxSnippets && maxSnippets > 0) {
|
|
18001
|
+
reason.lineSnippets = extractLineSnippets(metadata.content, metadata.lineIndex, query, maxSnippets);
|
|
18002
|
+
}
|
|
18003
|
+
reasons.push(reason);
|
|
17941
18004
|
}
|
|
17942
18005
|
}
|
|
17943
18006
|
for (const term of query.terms) {
|
|
@@ -18025,14 +18088,14 @@ function calculateMetadataScore(metadata, query) {
|
|
|
18025
18088
|
}
|
|
18026
18089
|
return { score: totalScore, reasons };
|
|
18027
18090
|
}
|
|
18028
|
-
async function calculateFileScore(filePath, query, metadata, rootPath, searchContent) {
|
|
18091
|
+
async function calculateFileScore(filePath, query, metadata, rootPath, searchContent, maxSnippets) {
|
|
18029
18092
|
const relativePath = relative(rootPath, filePath);
|
|
18030
18093
|
const filenameResult = calculateFilenameScore(filePath, query);
|
|
18031
18094
|
const filepathResult = calculateFilepathScore(filePath, query);
|
|
18032
18095
|
const metadataResult = calculateMetadataScore(metadata, query);
|
|
18033
18096
|
let contentResult = { score: 0, reasons: [] };
|
|
18034
18097
|
if (searchContent && metadata.size < 1024 * 1024) {
|
|
18035
|
-
contentResult = await calculateContentScore(filePath, query);
|
|
18098
|
+
contentResult = await calculateContentScore(filePath, query, metadata, maxSnippets);
|
|
18036
18099
|
}
|
|
18037
18100
|
const totalScore = Math.min(100, filenameResult.score + filepathResult.score + metadataResult.score + contentResult.score);
|
|
18038
18101
|
const allReasons = [
|
|
@@ -18058,6 +18121,8 @@ async function searchFiles(options) {
|
|
|
18058
18121
|
const minScore = options.minScore || 15;
|
|
18059
18122
|
const searchContent = options.searchContent ?? true;
|
|
18060
18123
|
const maxFileSize = options.maxFileSize || 1024 * 1024;
|
|
18124
|
+
const includeLinePreviews = options.includeLinePreviews ?? false;
|
|
18125
|
+
const maxSnippetsPerFile = options.maxSnippetsPerFile || 3;
|
|
18061
18126
|
const excludePatterns = [
|
|
18062
18127
|
...DEFAULT_EXCLUDE_PATTERNS,
|
|
18063
18128
|
...options.exclude || []
|
|
@@ -18084,8 +18149,8 @@ async function searchFiles(options) {
|
|
|
18084
18149
|
const stats = await fs.stat(filePath);
|
|
18085
18150
|
if (stats.size > maxFileSize)
|
|
18086
18151
|
continue;
|
|
18087
|
-
const metadata = await extractMetadata(filePath);
|
|
18088
|
-
const match = await calculateFileScore(filePath, parsedQuery, metadata, rootPath, searchContent);
|
|
18152
|
+
const metadata = await extractMetadata(filePath, includeLinePreviews);
|
|
18153
|
+
const match = await calculateFileScore(filePath, parsedQuery, metadata, rootPath, searchContent, includeLinePreviews ? maxSnippetsPerFile : 0);
|
|
18089
18154
|
if (match.score >= minScore) {
|
|
18090
18155
|
matches.push(match);
|
|
18091
18156
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function isTestFile(filePath: string): boolean;
|
|
|
4
4
|
export declare function isConfigFile(filePath: string): boolean;
|
|
5
5
|
export declare function isDocFile(filePath: string): boolean;
|
|
6
6
|
export declare function detectLanguage(filePath: string): string | undefined;
|
|
7
|
-
export declare function extractMetadata(filePath: string): Promise<FileMetadata>;
|
|
8
|
-
export declare function calculateFileScore(filePath: string, query: ParsedQuery, metadata: FileMetadata, rootPath: string, searchContent: boolean): Promise<FileMatch>;
|
|
7
|
+
export declare function extractMetadata(filePath: string, includeLineIndex?: boolean): Promise<FileMetadata>;
|
|
8
|
+
export declare function calculateFileScore(filePath: string, query: ParsedQuery, metadata: FileMetadata, rootPath: string, searchContent: boolean, maxSnippets?: number): Promise<FileMatch>;
|
|
9
9
|
export declare function searchFiles(options: SearchOptions): Promise<SearchResult>;
|
|
10
10
|
//# sourceMappingURL=search.d.ts.map
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,SAAS,EAET,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,SAAS,EAET,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EAQZ,MAAM,YAAY,CAAC;AAkFpB,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CA+DrD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMtD;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMnD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQnE;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,gBAAgB,GAAE,OAAe,GAChC,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAgRD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,OAAO,EACtB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,SAAS,CAAC,CAmCpB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAoE/E"}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,8 +10,17 @@ export interface MatchReason {
|
|
|
10
10
|
description: string;
|
|
11
11
|
matchedContent?: string;
|
|
12
12
|
contribution: number;
|
|
13
|
+
lineSnippets?: LineSnippet[];
|
|
13
14
|
}
|
|
14
15
|
export type MatchType = 'filename' | 'filepath' | 'content' | 'export' | 'import' | 'function' | 'class' | 'interface' | 'comment' | 'config' | 'test' | 'related';
|
|
16
|
+
export interface LineSnippet {
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
content: string;
|
|
19
|
+
context: {
|
|
20
|
+
before: string[];
|
|
21
|
+
after: string[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
15
24
|
export interface FileMetadata {
|
|
16
25
|
size: number;
|
|
17
26
|
lastModified: Date;
|
|
@@ -23,6 +32,8 @@ export interface FileMetadata {
|
|
|
23
32
|
language?: string;
|
|
24
33
|
exports?: string[];
|
|
25
34
|
imports?: string[];
|
|
35
|
+
lineIndex?: Map<string, number[]>;
|
|
36
|
+
content?: string;
|
|
26
37
|
}
|
|
27
38
|
export interface SearchOptions {
|
|
28
39
|
query: string;
|
|
@@ -36,6 +47,8 @@ export interface SearchOptions {
|
|
|
36
47
|
includeDocs?: boolean;
|
|
37
48
|
searchContent?: boolean;
|
|
38
49
|
maxFileSize?: number;
|
|
50
|
+
includeLinePreviews?: boolean;
|
|
51
|
+
maxSnippetsPerFile?: number;
|
|
39
52
|
}
|
|
40
53
|
export interface SearchResult {
|
|
41
54
|
files: FileMatch[];
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Semantic code search with ranked file matches and contextual line snippets showing exactly where matches occur. Available as CLI and OpenCode plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
7
7
|
"exports": {
|