opencode-context 1.0.6 → 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,38 @@
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
+
5
+ ---
6
+
7
+ ## Quick Reference for AI Agents
8
+
9
+ **Tool:** `find_files`
10
+ **Purpose:** Find relevant files before reading them
11
+ **When to use:** User asks to find, locate, search, or "where is X"
12
+
13
+ ### Arguments
14
+
15
+ | Name | Type | Required | Default | Description |
16
+ |------|------|----------|---------|-------------|
17
+ | query | string | **YES** | - | What to find (e.g., "auth", "database models") |
18
+ | maxFiles | number | no | 5 | Number of results |
19
+ | minScore | number | no | 15 | Relevance threshold (0-100) |
20
+ | includeTests | boolean | no | false | Include *.test.*, *.spec.* |
21
+ | includeConfigs | boolean | no | false | Include *.config.*, .rc files |
22
+ | includeDocs | boolean | no | false | Include *.md, docs/ |
23
+
24
+ ### Examples
25
+
26
+ ```
27
+ find_files query="authentication middleware"
28
+ find_files query="database" maxFiles=10 includeTests=true
29
+ find_files query="config" includeConfigs=true
30
+ ```
31
+
32
+ **Scoring:** Filename (highest) > Path > Content > Imports/Exports
33
+ **Tip:** If no results, try `minScore=5` for broader search
34
+
35
+ ---
4
36
 
5
37
  ## Installation
6
38
 
@@ -65,11 +97,14 @@ The plugin registers the `find_files` tool.
65
97
  | `includeTests` | boolean | false | Include test files |
66
98
  | `includeConfigs` | boolean | false | Include config files |
67
99
  | `includeDocs` | boolean | false | Include documentation |
100
+ | `includeLinePreviews` | boolean | false | Show matching line snippets |
101
+ | `maxSnippetsPerFile` | number | 3 | Max line snippets per file |
68
102
 
69
103
  **Example:**
70
104
 
71
105
  ```
72
106
  find_files query="auth middleware" maxFiles=5
107
+ find_files query="database" includeLinePreviews=true maxSnippetsPerFile=5
73
108
  ```
74
109
 
75
110
  **Scoring algorithm considers:**
@@ -101,15 +136,17 @@ opencode-context -q "database" --json
101
136
  | `-j, --json` | JSON output | false |
102
137
  | `-d, --detailed` | Show match reasons | false |
103
138
  | `-i, --interactive` | Interactive mode | false |
139
+ | `--line-previews` | Show matching line snippets | false |
140
+ | `--max-snippets` | Max snippets per file | 3 |
104
141
 
105
142
  ## How It Works
106
143
 
107
144
  1. Scans directory with fast-glob
108
- 2. Extracts metadata (size, language, exports, imports)
145
+ 2. Extracts metadata (size, language, exports, imports, **line index**)
109
146
  3. Scores each file against query
110
- 4. Returns ranked results
147
+ 4. Returns ranked results with optional line-level previews
111
148
 
112
- 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.
113
150
 
114
151
  ## Uninstallation
115
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/install.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- export {};
2
+ declare function updateAgentsMd(): Promise<void>;
3
+ declare function updateOpencodeConfig(): Promise<void>;
4
+ export { updateAgentsMd, updateOpencodeConfig };
3
5
  //# sourceMappingURL=install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":";AAuGA,iBAAe,cAAc,kBAyB5B;AAED,iBAAe,oBAAoB,kBA+BlC;AAwBD,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,CAAC"}
package/dist/install.js CHANGED
@@ -4,7 +4,9 @@
4
4
  // src/install.ts
5
5
  import { promises as fs } from "fs";
6
6
  import { join, dirname } from "path";
7
- var CONFIG_PATH = join(process.env.HOME || "", ".config/opencode/opencode.json");
7
+ import { homedir } from "os";
8
+ var CONFIG_PATH = join(process.env.HOME || homedir(), ".config/opencode/opencode.json");
9
+ var AGENTS_PATH = join(process.env.HOME || homedir(), ".config/opencode/AGENTS.md");
8
10
  async function ensureConfigDir() {
9
11
  const dir = dirname(CONFIG_PATH);
10
12
  try {
@@ -25,31 +27,136 @@ async function writeConfig(config) {
25
27
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + `
26
28
  `);
27
29
  }
28
- async function install() {
29
- console.log(`Installing opencode-context plugin...
30
- `);
31
- await ensureConfigDir();
30
+ async function readAgentsMd() {
31
+ try {
32
+ return await fs.readFile(AGENTS_PATH, "utf-8");
33
+ } catch {
34
+ return "";
35
+ }
36
+ }
37
+ async function writeAgentsMd(content) {
38
+ await fs.writeFile(AGENTS_PATH, content);
39
+ }
40
+ var TOOL_DOCUMENTATION = `
41
+ ## find_files Tool (opencode-context plugin)
42
+
43
+ **Purpose:** Semantic file search for locating relevant code files before reading them.
44
+
45
+ **When to use:**
46
+ - User asks to find, locate, or search for files
47
+ - Before reading multiple files to understand a codebase
48
+ - Looking for specific functionality (auth, database, config, etc.)
49
+ - User mentions "where is X defined" or "find Y"
50
+
51
+ **Arguments:**
52
+
53
+ | Name | Type | Required | Default | Description |
54
+ |------|------|----------|---------|-------------|
55
+ | query | string | YES | - | Search query describing what to find (e.g., "auth middleware", "user model") |
56
+ | maxFiles | number | NO | 5 | Maximum number of files to return |
57
+ | minScore | number | NO | 15 | Minimum relevance score (0-100). Lower = more results |
58
+ | includeTests | boolean | NO | false | Include test files (*.test.*, *.spec.*, etc.) |
59
+ | includeConfigs | boolean | NO | false | Include configuration files (*.config.*, .rc files) |
60
+ | includeDocs | boolean | NO | false | Include documentation (*.md, README, docs/) |
61
+
62
+ **Usage examples:**
63
+
64
+ \`\`\`
65
+ # Basic search
66
+ find_files query="authentication"
67
+
68
+ # More results with tests
69
+ find_files query="database" maxFiles=10 includeTests=true
70
+
71
+ # Find config files
72
+ find_files query="webpack" includeConfigs=true
73
+
74
+ # Low threshold for broader search
75
+ find_files query="utils" minScore=5
76
+ \`\`\`
77
+
78
+ **Scoring algorithm:**
79
+ - Filename matches (highest weight)
80
+ - Directory path matches
81
+ - Content matches (function names, class names)
82
+ - Import/export statements
83
+ - File type (tests ranked lower unless requested)
84
+
85
+ **Best practices:**
86
+ 1. Call this BEFORE reading files when user asks to find something
87
+ 2. Use maxFiles=10+ when doing initial codebase exploration
88
+ 3. Set includeTests=true only when user specifically asks about tests
89
+ 4. If no results, try lowering minScore to 5 or using broader terms
90
+ 5. Results include relevance scores - files above 50 are highly relevant
91
+ `;
92
+ async function updateAgentsMd() {
93
+ const existingContent = await readAgentsMd();
94
+ const toolMarker = "## find_files Tool (opencode-context plugin)";
95
+ if (existingContent.includes(toolMarker)) {
96
+ const beforeTool = existingContent.split(toolMarker)[0];
97
+ const afterTool = existingContent.split(toolMarker)[1];
98
+ const nextHeading = afterTool.search(/\n## /);
99
+ const rest = nextHeading >= 0 ? afterTool.slice(nextHeading) : "";
100
+ const newContent = beforeTool + TOOL_DOCUMENTATION + `
101
+ ` + rest;
102
+ await writeAgentsMd(newContent);
103
+ console.log("\u2713 Updated AGENTS.md with find_files documentation");
104
+ } else {
105
+ const header = existingContent.includes("# Available Tools") ? existingContent : existingContent.trim() ? existingContent + `
106
+
107
+ # Available Tools
108
+ ` : `# Available Tools
109
+ `;
110
+ const newContent = header + `
111
+ ` + TOOL_DOCUMENTATION;
112
+ await writeAgentsMd(newContent);
113
+ console.log("\u2713 Created/updated AGENTS.md with find_files documentation");
114
+ }
115
+ }
116
+ async function updateOpencodeConfig() {
32
117
  const config = await readConfig();
33
118
  if (!config.plugin) {
34
119
  config.plugin = [];
35
120
  }
36
121
  const pluginName = "opencode-context@latest";
37
- if (config.plugin.includes(pluginName)) {
38
- console.log("\u2713 opencode-context already in plugins");
39
- } else {
122
+ if (!config.plugin.includes(pluginName)) {
40
123
  config.plugin.push(pluginName);
41
124
  console.log("\u2713 Added opencode-context@latest to plugins");
125
+ } else {
126
+ console.log("\u2713 opencode-context already in plugins");
127
+ }
128
+ if (!config.instructions) {
129
+ config.instructions = [];
130
+ }
131
+ const agentsRef = "~/.config/opencode/AGENTS.md";
132
+ if (!config.instructions.includes(agentsRef)) {
133
+ config.instructions.push(agentsRef);
134
+ console.log("\u2713 Added AGENTS.md to instructions");
135
+ } else {
136
+ console.log("\u2713 AGENTS.md already in instructions");
42
137
  }
43
138
  await writeConfig(config);
139
+ }
140
+ async function install() {
141
+ console.log(`Installing opencode-context plugin...
142
+ `);
143
+ await ensureConfigDir();
144
+ await updateOpencodeConfig();
145
+ await updateAgentsMd();
44
146
  console.log(`
45
147
  \u2713 Installation complete!`);
46
148
  console.log(`
47
149
  To use:`);
48
- console.log(" 1. Run opencode in your project");
49
- console.log(' 2. Ask: "Find files related to auth"');
50
- console.log(` 3. The agent will use find_files tool
150
+ console.log(" 1. Restart opencode if running");
151
+ console.log(" 2. The AI agent will now see find_files in available tools");
152
+ console.log(` 3. Try: "Find files related to authentication"
51
153
  `);
52
154
  }
53
- if (__require.main == __require.module) {
155
+ var isMainModule = process.argv[1]?.includes("install.js") || process.argv[1]?.includes("install.ts");
156
+ if (isMainModule) {
54
157
  install().catch(console.error);
55
158
  }
159
+ export {
160
+ updateOpencodeConfig,
161
+ updateAgentsMd
162
+ };
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":"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;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,SAAS,GACjB,UAAU,GACV,UAAU,GACV,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,QAAQ,GACR,MAAM,GACN,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,eAAe,EAAE,cAa7B,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAuBxD,CAAC;AAEF,eAAO,MAAM,wBAAwB,UAcpC,CAAC;AAEF,eAAO,MAAM,aAAa,UAWzB,CAAC;AAEF,eAAO,MAAM,eAAe,UAO3B,CAAC;AAEF,eAAO,MAAM,YAAY,UAUxB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-context",
3
- "version": "1.0.6",
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": {
@@ -14,13 +14,15 @@
14
14
  "files": [
15
15
  "dist/",
16
16
  "README.md",
17
- "LICENSE"
17
+ "LICENSE",
18
+ "plugin.json"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "bun build ./src/index.ts --outfile ./dist/index.js --target node && bun build ./src/plugin.ts --outfile ./dist/plugin.js --target node && bun build ./src/install.ts --outfile ./dist/install.js --target node && echo '#!/usr/bin/env node' | cat - ./dist/index.js > temp && mv temp ./dist/index.js && chmod +x ./dist/index.js",
21
22
  "build:types": "tsc --emitDeclarationOnly",
22
23
  "dev": "bun run src/index.ts",
23
24
  "test": "bun test",
25
+ "postinstall": "node dist/install.js || echo 'Run: bunx opencode-context@latest install'",
24
26
  "prepublishOnly": "bun run build && bun run build:types"
25
27
  },
26
28
  "dependencies": {
package/plugin.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "opencode-context",
3
+ "version": "1.0.6",
4
+ "description": "Smart file finder for codebases with semantic search and relevance scoring",
5
+ "main": "dist/plugin.js",
6
+ "tools": [
7
+ {
8
+ "name": "find_files",
9
+ "description": "Find relevant files in the codebase based on a semantic search query. Use this to locate files related to a task before reading them. Returns ranked results with relevance scores.",
10
+ "whenToUse": [
11
+ "User asks to find, locate, or search for files",
12
+ "Before reading multiple files to understand a codebase",
13
+ "Looking for specific functionality (auth, database, config, etc.)",
14
+ "User mentions 'where is X defined' or 'find Y'"
15
+ ],
16
+ "args": {
17
+ "query": {
18
+ "type": "string",
19
+ "required": true,
20
+ "description": "Search query describing what to find (e.g., 'auth middleware', 'user model')"
21
+ },
22
+ "maxFiles": {
23
+ "type": "number",
24
+ "required": false,
25
+ "default": 5,
26
+ "description": "Maximum number of files to return"
27
+ },
28
+ "minScore": {
29
+ "type": "number",
30
+ "required": false,
31
+ "default": 15,
32
+ "description": "Minimum relevance score (0-100). Lower = more results"
33
+ },
34
+ "includeTests": {
35
+ "type": "boolean",
36
+ "required": false,
37
+ "default": false,
38
+ "description": "Include test files (*.test.*, *.spec.*, etc.)"
39
+ },
40
+ "includeConfigs": {
41
+ "type": "boolean",
42
+ "required": false,
43
+ "default": false,
44
+ "description": "Include configuration files (*.config.*, .rc files)"
45
+ },
46
+ "includeDocs": {
47
+ "type": "boolean",
48
+ "required": false,
49
+ "default": false,
50
+ "description": "Include documentation (*.md, README, docs/)"
51
+ }
52
+ },
53
+ "examples": [
54
+ {
55
+ "description": "Basic search",
56
+ "usage": "find_files query='authentication'"
57
+ },
58
+ {
59
+ "description": "More results with tests",
60
+ "usage": "find_files query='database' maxFiles=10 includeTests=true"
61
+ },
62
+ {
63
+ "description": "Find config files",
64
+ "usage": "find_files query='webpack' includeConfigs=true"
65
+ },
66
+ {
67
+ "description": "Low threshold for broader search",
68
+ "usage": "find_files query='utils' minScore=5"
69
+ }
70
+ ],
71
+ "bestPractices": [
72
+ "Call this BEFORE reading files when user asks to find something",
73
+ "Use maxFiles=10+ when doing initial codebase exploration",
74
+ "Set includeTests=true only when user specifically asks about tests",
75
+ "If no results, try lowering minScore to 5 or using broader terms",
76
+ "Results include relevance scores - files above 50 are highly relevant"
77
+ ],
78
+ "scoringFactors": [
79
+ "Filename matches (highest weight)",
80
+ "Directory path matches",
81
+ "Content matches (function names, class names)",
82
+ "Import/export statements",
83
+ "File type (tests ranked lower unless requested)"
84
+ ]
85
+ }
86
+ ]
87
+ }