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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # opencode-context
2
2
 
3
- Smart file finder for codebases with relevance scoring. Includes an OpenCode plugin.
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
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA4M1C,OAAO,EAAE,WAAW,EAAE,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, content) {
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
- reasons.push({
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
- reasons.push({
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 formatResults(result, detailed = false) {
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("Smart file finder for codebases - semantic search with confidence scoring").version("1.0.3");
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, content) {
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
- reasons.push({
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
- reasons.push({
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
@@ -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,EAOZ,MAAM,YAAY,CAAC;AAEpB,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,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiB7E;AAgQD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,OAAO,GACrB,OAAO,CAAC,SAAS,CAAC,CAmCpB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAiE/E"}
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[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.7",
4
- "description": "Smart file finder for codebases - semantic search with confidence scoring. Also available as an OpenCode plugin.",
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": {