opencode-plugin-search 0.0.3 → 0.0.4
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 +10 -0
- package/dist/index.js +310 -3
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -142,6 +142,16 @@ See [ast-grep rule documentation](https://ast-grep.github.io/guide/rule-config.h
|
|
|
142
142
|
|
|
143
143
|
The plugin automatically looks for `sgconfig.yaml` in the project root to support custom languages and rule directories for ast-grep search functionality. See [ast-grep documentation](https://ast-grep.github.io/advanced/custom-language.html) for configuration details.
|
|
144
144
|
|
|
145
|
+
## Future Plans
|
|
146
|
+
|
|
147
|
+
See [ROADMAP.md](ROADMAP.md) for detailed plans about upcoming features including:
|
|
148
|
+
|
|
149
|
+
- **Pre-configured rule library** - Ready-to-use templates for common patterns
|
|
150
|
+
- **Batch query optimization** - Multiple patterns in single calls
|
|
151
|
+
- **Context-aware search** - Richer results with surrounding code context
|
|
152
|
+
- **Semantic search enhancement** - Component usage tracking and dependency analysis
|
|
153
|
+
- **Performance optimizations** - Parallel execution and smart caching
|
|
154
|
+
|
|
145
155
|
## Development
|
|
146
156
|
|
|
147
157
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __export = (target, all) => {
|
|
3
4
|
for (var name in all)
|
|
@@ -8,6 +9,7 @@ var __export = (target, all) => {
|
|
|
8
9
|
set: (newValue) => all[name] = () => newValue
|
|
9
10
|
});
|
|
10
11
|
};
|
|
12
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
11
13
|
|
|
12
14
|
// node_modules/zod/v4/classic/external.js
|
|
13
15
|
var exports_external = {};
|
|
@@ -15014,7 +15016,7 @@ var jsYaml = {
|
|
|
15014
15016
|
safeDump
|
|
15015
15017
|
};
|
|
15016
15018
|
|
|
15017
|
-
// src/utils.ts
|
|
15019
|
+
// src/astgrep/utils.ts
|
|
15018
15020
|
import { spawn } from "node:child_process";
|
|
15019
15021
|
import { existsSync } from "node:fs";
|
|
15020
15022
|
import { join } from "node:path";
|
|
@@ -15113,7 +15115,7 @@ async function executeAstGrep(command, args, options) {
|
|
|
15113
15115
|
return { matches: [], stdout, stderr };
|
|
15114
15116
|
}
|
|
15115
15117
|
|
|
15116
|
-
// src/tools.ts
|
|
15118
|
+
// src/astgrep/tools.ts
|
|
15117
15119
|
var patternSchema = tool.schema.union([
|
|
15118
15120
|
tool.schema.string(),
|
|
15119
15121
|
tool.schema.object({
|
|
@@ -15296,7 +15298,311 @@ ${JSON.stringify({
|
|
|
15296
15298
|
}
|
|
15297
15299
|
});
|
|
15298
15300
|
}
|
|
15301
|
+
// src/websearch/duckduckgo.ts
|
|
15302
|
+
async function searchDuckDuckGo(query, options) {
|
|
15303
|
+
try {
|
|
15304
|
+
const response = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&no_redirect=1`);
|
|
15305
|
+
if (!response.ok) {
|
|
15306
|
+
throw new Error(`DuckDuckGo API error: ${response.statusText}`);
|
|
15307
|
+
}
|
|
15308
|
+
const data = await response.json();
|
|
15309
|
+
const results = [];
|
|
15310
|
+
if (data.Abstract && data.AbstractText) {
|
|
15311
|
+
results.push({
|
|
15312
|
+
title: data.Heading || data.AbstractSource || "Instant Answer",
|
|
15313
|
+
link: data.AbstractURL || "",
|
|
15314
|
+
snippet: data.AbstractText
|
|
15315
|
+
});
|
|
15316
|
+
}
|
|
15317
|
+
if (data.Results && Array.isArray(data.Results)) {
|
|
15318
|
+
data.Results.forEach((item) => {
|
|
15319
|
+
if (item.FirstURL) {
|
|
15320
|
+
results.push({
|
|
15321
|
+
title: item.Text || "",
|
|
15322
|
+
link: item.FirstURL,
|
|
15323
|
+
snippet: ""
|
|
15324
|
+
});
|
|
15325
|
+
}
|
|
15326
|
+
});
|
|
15327
|
+
}
|
|
15328
|
+
if (data.RelatedTopics && Array.isArray(data.RelatedTopics)) {
|
|
15329
|
+
data.RelatedTopics.forEach((item) => {
|
|
15330
|
+
if (item.FirstURL && item.Text) {
|
|
15331
|
+
results.push({
|
|
15332
|
+
title: item.Text.split(" - ")[0] || item.Text,
|
|
15333
|
+
link: item.FirstURL,
|
|
15334
|
+
snippet: item.Text.split(" - ").slice(1).join(" - ") || ""
|
|
15335
|
+
});
|
|
15336
|
+
}
|
|
15337
|
+
});
|
|
15338
|
+
}
|
|
15339
|
+
return results.slice(0, options.limit);
|
|
15340
|
+
} catch (error45) {
|
|
15341
|
+
console.error("DuckDuckGo search error:", error45);
|
|
15342
|
+
throw new Error(`DuckDuckGo search failed: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15299
15345
|
|
|
15346
|
+
// src/websearch/google.ts
|
|
15347
|
+
async function searchGoogle(query, options) {
|
|
15348
|
+
try {
|
|
15349
|
+
const playwright = await import("playwright");
|
|
15350
|
+
const { chromium } = playwright;
|
|
15351
|
+
const browser = await chromium.launch({
|
|
15352
|
+
headless: options.headless ?? true,
|
|
15353
|
+
args: [
|
|
15354
|
+
"--disable-blink-features=AutomationControlled",
|
|
15355
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
15356
|
+
"--disable-site-isolation-trials",
|
|
15357
|
+
"--no-sandbox",
|
|
15358
|
+
"--disable-setuid-sandbox",
|
|
15359
|
+
"--disable-dev-shm-usage",
|
|
15360
|
+
"--disable-accelerated-2d-canvas",
|
|
15361
|
+
"--no-first-run",
|
|
15362
|
+
"--no-zygote",
|
|
15363
|
+
"--disable-gpu",
|
|
15364
|
+
"--hide-scrollbars",
|
|
15365
|
+
"--mute-audio"
|
|
15366
|
+
],
|
|
15367
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
15368
|
+
});
|
|
15369
|
+
try {
|
|
15370
|
+
const context = await browser.newContext({
|
|
15371
|
+
viewport: { width: 1920, height: 1080 },
|
|
15372
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
15373
|
+
locale: options.locale,
|
|
15374
|
+
timezoneId: "America/New_York"
|
|
15375
|
+
});
|
|
15376
|
+
await context.addInitScript(() => {
|
|
15377
|
+
Object.defineProperty(navigator, "webdriver", { get: () => false });
|
|
15378
|
+
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
|
|
15379
|
+
Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
|
|
15380
|
+
window.chrome = {
|
|
15381
|
+
runtime: {},
|
|
15382
|
+
loadTimes: () => {},
|
|
15383
|
+
csi: () => {},
|
|
15384
|
+
app: {}
|
|
15385
|
+
};
|
|
15386
|
+
});
|
|
15387
|
+
const page = await context.newPage();
|
|
15388
|
+
const googleDomain = options.country ? `https://www.google.${options.country.toLowerCase()}` : "https://www.google.com";
|
|
15389
|
+
await page.goto(googleDomain, { waitUntil: "networkidle", timeout: options.timeout });
|
|
15390
|
+
const currentUrl = page.url();
|
|
15391
|
+
if (currentUrl.includes("sorry") || currentUrl.includes("captcha") || currentUrl.includes("recaptcha")) {
|
|
15392
|
+
throw new Error("Google CAPTCHA detected. Try enabling headless: false or using a different IP.");
|
|
15393
|
+
}
|
|
15394
|
+
const searchInput = await page.waitForSelector('textarea[name="q"], input[name="q"]', {
|
|
15395
|
+
timeout: options.timeout / 2
|
|
15396
|
+
});
|
|
15397
|
+
await searchInput.click();
|
|
15398
|
+
await searchInput.fill(query);
|
|
15399
|
+
await searchInput.press("Enter");
|
|
15400
|
+
await page.waitForLoadState("networkidle", { timeout: options.timeout });
|
|
15401
|
+
const searchUrl = page.url();
|
|
15402
|
+
if (searchUrl.includes("sorry") || searchUrl.includes("captcha") || searchUrl.includes("recaptcha")) {
|
|
15403
|
+
throw new Error("Google CAPTCHA detected after search. Try enabling headless: false or using a different IP.");
|
|
15404
|
+
}
|
|
15405
|
+
const results = await page.evaluate((limit) => {
|
|
15406
|
+
const extracted = [];
|
|
15407
|
+
const seenUrls = new Set;
|
|
15408
|
+
const selectorSets = [
|
|
15409
|
+
{ container: "#search div[data-hveid]", title: "h3", snippet: 'div[role="text"]' },
|
|
15410
|
+
{ container: "#rso div[data-hveid]", title: "h3", snippet: 'div[style*="webkit-line-clamp"]' },
|
|
15411
|
+
{ container: ".g", title: "h3", snippet: "div" },
|
|
15412
|
+
{ container: "div[jscontroller][data-hveid]", title: "h3", snippet: 'div[role="text"]' }
|
|
15413
|
+
];
|
|
15414
|
+
const alternativeSnippetSelectors = ['div[role="text"]', 'div[style*="webkit-line-clamp"]', "div"];
|
|
15415
|
+
for (const selectors of selectorSets) {
|
|
15416
|
+
if (extracted.length >= limit)
|
|
15417
|
+
break;
|
|
15418
|
+
const containers = Array.from(document.querySelectorAll(selectors.container));
|
|
15419
|
+
for (const container of containers) {
|
|
15420
|
+
if (extracted.length >= limit)
|
|
15421
|
+
break;
|
|
15422
|
+
const titleElement = container.querySelector(selectors.title);
|
|
15423
|
+
if (!titleElement)
|
|
15424
|
+
continue;
|
|
15425
|
+
const title = (titleElement.textContent || "").trim();
|
|
15426
|
+
let link = "";
|
|
15427
|
+
const linkInTitle = titleElement.querySelector("a");
|
|
15428
|
+
if (linkInTitle) {
|
|
15429
|
+
link = linkInTitle.href;
|
|
15430
|
+
} else {
|
|
15431
|
+
let current = titleElement;
|
|
15432
|
+
while (current && current.tagName !== "A") {
|
|
15433
|
+
current = current.parentElement;
|
|
15434
|
+
}
|
|
15435
|
+
if (current && current instanceof HTMLAnchorElement) {
|
|
15436
|
+
link = current.href;
|
|
15437
|
+
} else {
|
|
15438
|
+
const containerLink = container.querySelector("a");
|
|
15439
|
+
if (containerLink) {
|
|
15440
|
+
link = containerLink.href;
|
|
15441
|
+
}
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
15444
|
+
if (!link || !link.startsWith("http") || seenUrls.has(link))
|
|
15445
|
+
continue;
|
|
15446
|
+
let snippet2 = "";
|
|
15447
|
+
const snippetElement = container.querySelector(selectors.snippet);
|
|
15448
|
+
if (snippetElement) {
|
|
15449
|
+
snippet2 = (snippetElement.textContent || "").trim();
|
|
15450
|
+
} else {
|
|
15451
|
+
for (const altSelector of alternativeSnippetSelectors) {
|
|
15452
|
+
const element = container.querySelector(altSelector);
|
|
15453
|
+
if (element) {
|
|
15454
|
+
snippet2 = (element.textContent || "").trim();
|
|
15455
|
+
break;
|
|
15456
|
+
}
|
|
15457
|
+
}
|
|
15458
|
+
if (!snippet2) {
|
|
15459
|
+
const textNodes = Array.from(container.querySelectorAll("div")).filter((el) => !el.querySelector("h3") && (el.textContent || "").trim().length > 20);
|
|
15460
|
+
if (textNodes.length > 0) {
|
|
15461
|
+
snippet2 = (textNodes[0]?.textContent || "").trim();
|
|
15462
|
+
}
|
|
15463
|
+
}
|
|
15464
|
+
}
|
|
15465
|
+
if (title && link) {
|
|
15466
|
+
extracted.push({ title, link, snippet: snippet2 });
|
|
15467
|
+
seenUrls.add(link);
|
|
15468
|
+
}
|
|
15469
|
+
}
|
|
15470
|
+
}
|
|
15471
|
+
if (extracted.length < limit) {
|
|
15472
|
+
const anchorElements = Array.from(document.querySelectorAll("a[href^='http']"));
|
|
15473
|
+
for (const el of anchorElements) {
|
|
15474
|
+
if (extracted.length >= limit)
|
|
15475
|
+
break;
|
|
15476
|
+
if (!(el instanceof HTMLAnchorElement))
|
|
15477
|
+
continue;
|
|
15478
|
+
const link = el.href;
|
|
15479
|
+
if (!link || seenUrls.has(link) || link.includes("google.com/") || link.includes("accounts.google") || link.includes("support.google")) {
|
|
15480
|
+
continue;
|
|
15481
|
+
}
|
|
15482
|
+
const title = (el.textContent || "").trim();
|
|
15483
|
+
if (!title)
|
|
15484
|
+
continue;
|
|
15485
|
+
let snippet2 = "";
|
|
15486
|
+
let parent = el.parentElement;
|
|
15487
|
+
for (let i2 = 0;i2 < 3 && parent; i2++) {
|
|
15488
|
+
const text = (parent.textContent || "").trim();
|
|
15489
|
+
if (text.length > 20 && text !== title) {
|
|
15490
|
+
snippet2 = text;
|
|
15491
|
+
break;
|
|
15492
|
+
}
|
|
15493
|
+
parent = parent.parentElement;
|
|
15494
|
+
}
|
|
15495
|
+
extracted.push({ title, link, snippet: snippet2 });
|
|
15496
|
+
seenUrls.add(link);
|
|
15497
|
+
}
|
|
15498
|
+
}
|
|
15499
|
+
return extracted.slice(0, limit);
|
|
15500
|
+
}, options.limit);
|
|
15501
|
+
await browser.close();
|
|
15502
|
+
return results;
|
|
15503
|
+
} catch (error45) {
|
|
15504
|
+
await browser.close();
|
|
15505
|
+
throw new Error(`Google search failed: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
15506
|
+
}
|
|
15507
|
+
} catch (error45) {
|
|
15508
|
+
if (error45 instanceof Error && error45.message.includes("Cannot find module")) {
|
|
15509
|
+
throw new Error(`Google search requires Playwright to be installed. Please install it with: npm install playwright@latest
|
|
15510
|
+
Note: Playwright requires downloading browser binaries (~180MB).`);
|
|
15511
|
+
}
|
|
15512
|
+
throw error45;
|
|
15513
|
+
}
|
|
15514
|
+
}
|
|
15515
|
+
|
|
15516
|
+
// src/websearch/tools.ts
|
|
15517
|
+
var searchEngineOptionsSchema = tool.schema.object({
|
|
15518
|
+
duckduckgo: tool.schema.object({
|
|
15519
|
+
safe_search: tool.schema.boolean().optional(),
|
|
15520
|
+
region: tool.schema.string().optional(),
|
|
15521
|
+
time_range: tool.schema.enum(["d", "w", "m", "y"]).optional()
|
|
15522
|
+
}).optional(),
|
|
15523
|
+
google: tool.schema.object({
|
|
15524
|
+
safe_search: tool.schema.boolean().optional(),
|
|
15525
|
+
country: tool.schema.string().optional(),
|
|
15526
|
+
headless: tool.schema.boolean().optional(),
|
|
15527
|
+
use_saved_state: tool.schema.boolean().optional()
|
|
15528
|
+
}).optional()
|
|
15529
|
+
}).refine((obj) => obj.duckduckgo !== undefined || obj.google !== undefined, {
|
|
15530
|
+
message: "At least one search engine must be specified (duckduckgo or google)"
|
|
15531
|
+
});
|
|
15532
|
+
function createWebSearchTool() {
|
|
15533
|
+
return tool({
|
|
15534
|
+
description: "Search the web using Google and/or DuckDuckGo. If multiple engines are specified, queries run in parallel.",
|
|
15535
|
+
args: {
|
|
15536
|
+
query: tool.schema.string(),
|
|
15537
|
+
engines: searchEngineOptionsSchema,
|
|
15538
|
+
limit: tool.schema.number().int().positive().max(50).optional(),
|
|
15539
|
+
timeout: tool.schema.number().int().positive().max(120000).optional(),
|
|
15540
|
+
locale: tool.schema.string().optional()
|
|
15541
|
+
},
|
|
15542
|
+
async execute(args, _context) {
|
|
15543
|
+
const { query, engines, limit = 10, timeout = 30000, locale = "en-US" } = args;
|
|
15544
|
+
const results = [];
|
|
15545
|
+
const sources = {};
|
|
15546
|
+
const searchPromises = [];
|
|
15547
|
+
if (engines.google) {
|
|
15548
|
+
searchPromises.push(searchGoogle(query, { ...engines.google, limit, timeout, locale }).then((googleResults) => {
|
|
15549
|
+
sources.google = { count: googleResults.length, success: true };
|
|
15550
|
+
googleResults.forEach((result, index) => {
|
|
15551
|
+
results.push({
|
|
15552
|
+
...result,
|
|
15553
|
+
source: "google",
|
|
15554
|
+
rank: index + 1
|
|
15555
|
+
});
|
|
15556
|
+
});
|
|
15557
|
+
}).catch((error45) => {
|
|
15558
|
+
sources.google = {
|
|
15559
|
+
count: 0,
|
|
15560
|
+
success: false,
|
|
15561
|
+
error: error45 instanceof Error ? error45.message : String(error45)
|
|
15562
|
+
};
|
|
15563
|
+
}));
|
|
15564
|
+
}
|
|
15565
|
+
if (engines.duckduckgo) {
|
|
15566
|
+
searchPromises.push(searchDuckDuckGo(query, { ...engines.duckduckgo, limit, timeout, locale }).then((ddResults) => {
|
|
15567
|
+
sources.duckduckgo = { count: ddResults.length, success: true };
|
|
15568
|
+
ddResults.forEach((result, index) => {
|
|
15569
|
+
results.push({
|
|
15570
|
+
...result,
|
|
15571
|
+
source: "duckduckgo",
|
|
15572
|
+
rank: index + 1
|
|
15573
|
+
});
|
|
15574
|
+
});
|
|
15575
|
+
}).catch((error45) => {
|
|
15576
|
+
sources.duckduckgo = {
|
|
15577
|
+
count: 0,
|
|
15578
|
+
success: false,
|
|
15579
|
+
error: error45 instanceof Error ? error45.message : String(error45)
|
|
15580
|
+
};
|
|
15581
|
+
}));
|
|
15582
|
+
}
|
|
15583
|
+
await Promise.allSettled(searchPromises);
|
|
15584
|
+
results.sort((a, b) => {
|
|
15585
|
+
if (a.source === b.source) {
|
|
15586
|
+
return a.rank - b.rank;
|
|
15587
|
+
}
|
|
15588
|
+
return a.source === "google" ? -1 : 1;
|
|
15589
|
+
});
|
|
15590
|
+
if (results.length === 0) {
|
|
15591
|
+
const errors3 = Object.entries(sources).filter(([_, info]) => !info?.success).map(([engine, info]) => `${engine}: ${info?.error}`).join("; ");
|
|
15592
|
+
return `No results found. ${errors3 ? `Errors: ${errors3}` : "Try adjusting your search terms."}`;
|
|
15593
|
+
}
|
|
15594
|
+
const formatted = results.map((r, i2) => `${i2 + 1}. [${r.source.toUpperCase()}] ${r.title}
|
|
15595
|
+
${r.link}
|
|
15596
|
+
${r.snippet || "No description"}
|
|
15597
|
+
`).join(`
|
|
15598
|
+
`);
|
|
15599
|
+
const summary = `Found ${results.length} results from: ${Object.entries(sources).filter(([_, info]) => info?.success).map(([engine, info]) => `${engine}(${info?.count})`).join(", ") || "none"}`;
|
|
15600
|
+
return `${summary}
|
|
15601
|
+
|
|
15602
|
+
${formatted}`;
|
|
15603
|
+
}
|
|
15604
|
+
});
|
|
15605
|
+
}
|
|
15300
15606
|
// src/plugin.ts
|
|
15301
15607
|
var SearchPlugin = async ({ directory }) => {
|
|
15302
15608
|
return {
|
|
@@ -15304,7 +15610,8 @@ var SearchPlugin = async ({ directory }) => {
|
|
|
15304
15610
|
ast_grep_find: createFindTool(directory),
|
|
15305
15611
|
ast_grep_find_by_rule: createFindByRuleTool(directory),
|
|
15306
15612
|
ast_grep_dump_syntax: createDumpSyntaxTool(),
|
|
15307
|
-
ast_grep_test_rule: createTestRuleTool(directory)
|
|
15613
|
+
ast_grep_test_rule: createTestRuleTool(directory),
|
|
15614
|
+
web_search: createWebSearchTool()
|
|
15308
15615
|
}
|
|
15309
15616
|
};
|
|
15310
15617
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-plugin-search",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "An OpenCode plugin providing advanced code search capabilities including AST-based structural search using ast-grep.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"build": "bun build ./src/index.ts --outfile ./dist/index.js --target node",
|
|
12
12
|
"watch": "bun build ./src/index.ts --outfile ./dist/index.js --target node --watch",
|
|
13
13
|
"typecheck": "biome check && tsc --noEmit",
|
|
14
|
-
"auto": "biome check --fix && tsc --noEmit"
|
|
14
|
+
"auto": "biome check --fix && tsc --noEmit",
|
|
15
|
+
"test:pack": "bun pm pack --dry-run"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
@@ -35,7 +36,13 @@
|
|
|
35
36
|
"homepage": "https://github.com/elyzov/opencode-plugin-search#readme",
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"@opencode-ai/plugin": "^1.1.19",
|
|
38
|
-
"@opencode-ai/sdk": "^1.1.19"
|
|
39
|
+
"@opencode-ai/sdk": "^1.1.19",
|
|
40
|
+
"playwright": "^1.49.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"playwright": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
39
46
|
},
|
|
40
47
|
"devDependencies": {
|
|
41
48
|
"@tsconfig/bun": "^1.0.10",
|