@kagan-sh/opensearch 0.1.0

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.
@@ -0,0 +1,104 @@
1
+ # Contributing to @kagan-sh/opensearch
2
+
3
+ Keep the published README end-user facing. Put local setup, source-based plugin loading, and maintainer workflow details here.
4
+
5
+ ## Requirements
6
+
7
+ - Bun 1.2+
8
+ - Node 20+
9
+ - OpenCode CLI (`opencode`)
10
+
11
+ ## Local setup
12
+
13
+ ```bash
14
+ bun install
15
+ npm install -g opencode-ai
16
+ ```
17
+
18
+ ## Run the plugin from source
19
+
20
+ For local development, point `opencode.json` at the source entrypoint:
21
+
22
+ ```json
23
+ {
24
+ "$schema": "https://opencode.ai/config.json",
25
+ "plugin": ["file:///absolute/path/to/opensearch/src/index.ts"]
26
+ }
27
+ ```
28
+
29
+ This keeps the published package path in `README.md` while still giving contributors a zero-publish development loop.
30
+
31
+ ## Validate changes
32
+
33
+ Run the full validation pipeline before opening a pull request:
34
+
35
+ ```bash
36
+ bun run check
37
+ ```
38
+
39
+ That runs:
40
+
41
+ 1. `bun run typecheck`
42
+ 2. `bun run test`
43
+ 3. `bun run build`
44
+
45
+ Acceptance tests are integration-heavy and boot a real OpenCode server process.
46
+
47
+ ## Docs
48
+
49
+ Install docs dependencies:
50
+
51
+ ```bash
52
+ python3 -m pip install -r requirements-docs.txt
53
+ ```
54
+
55
+ Run the local docs server:
56
+
57
+ ```bash
58
+ mkdocs serve
59
+ ```
60
+
61
+ Build docs with strict link validation:
62
+
63
+ ```bash
64
+ mkdocs build --strict
65
+ ```
66
+
67
+ ## Release automation
68
+
69
+ This repo uses `semantic-release` on `main`.
70
+
71
+ - Prefer Conventional Commits for merge commits and direct commits to `main`
72
+ - Run `bun run release:dry-run` locally if you want to preview the next release
73
+ - npm publishing is configured for GitHub Actions trusted publishing via OIDC for `@kagan-sh/opensearch`
74
+ - The npm package must be linked to this repository under the `kagan_sh` publisher account before the first release
75
+ - Local `semantic-release --dry-run` still fails auth checks unless GitHub and npm credentials are present; the OIDC npm path is validated in GitHub Actions, not in a regular local shell
76
+
77
+ ## Project structure
78
+
79
+ - `src/index.ts` plugin entrypoint, source selection, and response assembly
80
+ - `src/schema.ts` zod schemas and JSON schema export
81
+ - `src/sources/*` source adapters for session, web, and code search
82
+ - `src/synth.ts` structured synthesis through `session.prompt`
83
+ - `tests/*` vitest coverage for schema and end-to-end plugin behavior
84
+ - `SKILL.md` optional skill guidance that nudges agent workflows toward `opensearch` for broad research tasks
85
+
86
+ ## Testing policy
87
+
88
+ - Prefer acceptance-first coverage for behavior changes
89
+ - Test observable outcomes instead of internal wiring
90
+ - Avoid tautological tests and unnecessary mocks
91
+ - Mock only when the real contract cannot be exercised in runtime tests
92
+
93
+ ## Release workflow
94
+
95
+ - CI workflow: `.github/workflows/ci.yml`
96
+ - Release workflow: `.github/workflows/release.yml`
97
+ - GitHub releases are automated with `semantic-release`
98
+ - npm publishing is ready for trusted publishing via OIDC once the package is configured on npm
99
+
100
+ ## Pull requests
101
+
102
+ - Keep changes small and focused
103
+ - Add or update tests for behavior changes
104
+ - Keep `README.md` user-facing and keep contributor workflow details in `CONTRIBUTING.md`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/logo-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/logo-light.svg">
5
+ <img alt="OpenSearch — evidence-backed search for OpenCode" src="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/logo-dark.svg" width="100%">
6
+ </picture>
7
+ </p>
8
+ <p align="center">
9
+ <a href="https://github.com/kagan-sh/opensearch/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/kagan-sh/opensearch/ci.yml?style=for-the-badge&label=CI" alt="CI"></a>
10
+ <a href="https://kagan-sh.github.io/opensearch/"><img src="https://img.shields.io/badge/docs-github%20pages-181717?style=for-the-badge&logo=github" alt="Docs"></a>
11
+ <a href="https://opensource.org/license/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
12
+ <a href="https://github.com/kagan-sh/opensearch/stargazers"><img src="https://img.shields.io/github/stars/kagan-sh/opensearch?style=for-the-badge" alt="Stars"></a>
13
+ </p>
14
+ <h3 align="center">
15
+ <a href="https://kagan-sh.github.io/opensearch/">Docs</a> ·
16
+ <a href="https://kagan-sh.github.io/opensearch/quickstart/">Quickstart</a> ·
17
+ <a href="https://kagan-sh.github.io/opensearch/reference/result-contract/">Result Contract</a> ·
18
+ <a href="https://github.com/kagan-sh/opensearch/issues/new?template=feature-request.yml">Feature Requests</a>
19
+ </h3>
20
+
21
+ ---
22
+
23
+ `@kagan-sh/opensearch` is an OpenCode plugin for broad investigation. It searches session history, the live web, and public code in parallel, then returns a structured evidence-backed response your agent can act on.
24
+
25
+ ## Install
26
+
27
+ Add the plugin to `opencode.json`:
28
+
29
+ ```json
30
+ {
31
+ "$schema": "https://opencode.ai/config.json",
32
+ "plugin": ["@kagan-sh/opensearch"]
33
+ }
34
+ ```
35
+
36
+ OpenCode installs npm plugins automatically at startup.
37
+
38
+ Full docs: **[kagan-sh.github.io/opensearch](https://kagan-sh.github.io/opensearch/)**.
39
+
40
+ ## Configuration
41
+
42
+ Control runtime behavior with environment variables:
43
+
44
+ - `OPENSEARCH_SYNTH=true|false`
45
+ - `OPENSEARCH_DEPTH=quick|thorough`
46
+ - `OPENSEARCH_SOURCE_SESSION=true|false`
47
+ - `OPENSEARCH_SOURCE_WEB=true|false`
48
+ - `OPENSEARCH_SOURCE_CODE=true|false`
49
+ - `OPENSEARCH_WEB_KEY=<exa_api_key>`
50
+ - `EXA_API_KEY=<exa_api_key>`
51
+
52
+ `OPENSEARCH_WEB_KEY` takes precedence over `EXA_API_KEY`. Web search is skipped when neither key is set.
53
+
54
+ ## Tool
55
+
56
+ The plugin exposes a single tool: `opensearch`.
57
+
58
+ Arguments:
59
+
60
+ - `query: string`
61
+ - `sources?: ("session" | "web" | "code")[]`
62
+ - `depth?: "quick" | "thorough"`
63
+
64
+ The tool returns strict JSON with `status`, `answer`, `confidence`, `evidence[]`, `sources[]`, `followups[]`, and `meta`.
65
+
66
+ `meta` includes:
67
+
68
+ - `sources_requested`
69
+ - `sources_queried`
70
+ - `sources_yielded`
71
+ - `sources_unavailable[]`
72
+ - `source_errors[]`
73
+
74
+ Invalid plugin config is reported explicitly instead of being ignored.
75
+
76
+ ## Contributing
77
+
78
+ For local source development, validation commands, and release workflow details, see `CONTRIBUTING.md`.
79
+
80
+ ## Documentation
81
+
82
+ Published docs live at `https://kagan-sh.github.io/opensearch/`. MkDocs source lives in `docs/` with site config in `mkdocs.yml`.
83
+
84
+ ## Skill
85
+
86
+ The repo includes `SKILL.md` for agent environments that support installable skills. It increases the chance that agents reach for `opensearch` on research-heavy prompts, but it does not force tool selection.
87
+
88
+ ## License
89
+
90
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: opensearch
3
+ description: Use OpenSearch for broad, evidence-backed investigation across session history, the web, and public code when a user asks to search, research, compare sources, or gather official docs and examples.
4
+ license: MIT
5
+ compatibility: Requires OpenCode with @kagan-sh/opensearch installed. Web results need OPENSEARCH_WEB_KEY or EXA_API_KEY.
6
+ metadata:
7
+ version: 0.0.1
8
+ ---
9
+
10
+ # OpenSearch Skill
11
+
12
+ Use this skill when the task is primarily about finding, comparing, and synthesizing evidence rather than editing code immediately.
13
+
14
+ ## When to Use It
15
+
16
+ - The user says `search`, `look up`, `research`, `investigate`, or `find examples`
17
+ - The answer should combine project context, official docs, and public code patterns
18
+ - The task needs contradictory-source checking before implementation
19
+ - The user wants official references, best practices, or GitHub examples
20
+
21
+ ## When Not to Use It
22
+
23
+ - A local `read`, `grep`, or `glob` is enough to answer the question
24
+ - The task is a straightforward edit in a known file
25
+ - The user explicitly wants a pure code change with no research step
26
+
27
+ ## Default Workflow
28
+
29
+ 1. Call `opensearch` first for broad or ambiguous research questions.
30
+ 2. Prefer `depth: "thorough"` for broad or ambiguous research; keep `quick` for focused lookups.
31
+ 3. Use all sources when external context matters; narrow sources only when the user asks for repo-local or session-local evidence.
32
+ 4. Summarize what the tool found, highlight contradictions or gaps, then continue with implementation or recommendation.
33
+
34
+ ## Query Patterns
35
+
36
+ - Official docs and examples:
37
+ - `React Server Components caching behavior official docs GitHub examples`
38
+ - Repo plus external evidence:
39
+ - `OpenCode plugin loading order project behavior official docs`
40
+ - Contradiction check:
41
+ - `semantic-release trusted publishing npm GitHub Actions official guidance 2026`
42
+
43
+ ## Source Selection Guide
44
+
45
+ - `session` for prior decisions, earlier runs, and local conversation history
46
+ - `web` for official docs, changelogs, and current vendor guidance
47
+ - `code` for public implementation examples and real usage patterns
48
+
49
+ ## Output Expectations
50
+
51
+ - Return the strongest evidence first
52
+ - Call out missing or unavailable sources explicitly
53
+ - Keep citations grounded in the tool response instead of guessing
@@ -0,0 +1,9 @@
1
+ import type { Config, SourceId } from "./schema";
2
+ export declare function defaultConfig(): Config;
3
+ export declare function parsePluginConfig(input: unknown): Config | undefined;
4
+ export declare function isSourceAvailable(config: Config, source: SourceId): boolean;
5
+ export declare function resolveSources(config: Config, requested?: SourceId[]): {
6
+ requested: ("session" | "web" | "code")[];
7
+ sources: ("session" | "web" | "code")[];
8
+ unavailable: ("session" | "web" | "code")[];
9
+ };
package/dist/config.js ADDED
@@ -0,0 +1,66 @@
1
+ import { ConfigSchema, SOURCE_IDS } from "./schema";
2
+ function parseBoolean(name, value, fallback) {
3
+ if (value === undefined)
4
+ return fallback;
5
+ if (value === "true")
6
+ return true;
7
+ if (value === "false")
8
+ return false;
9
+ throw new Error(`Invalid value for ${name}: expected true or false, got ${value}`);
10
+ }
11
+ function parseDepth(value) {
12
+ if (value === undefined || value === "quick")
13
+ return "quick";
14
+ if (value === "thorough")
15
+ return "thorough";
16
+ throw new Error(`Invalid value for OPENSEARCH_DEPTH: expected quick or thorough, got ${value}`);
17
+ }
18
+ function formatIssuePath(path) {
19
+ if (path.length === 0)
20
+ return "opensearch";
21
+ return `opensearch.${path.join(".")}`;
22
+ }
23
+ export function defaultConfig() {
24
+ return {
25
+ sources: {
26
+ session: parseBoolean("OPENSEARCH_SOURCE_SESSION", process.env.OPENSEARCH_SOURCE_SESSION, true),
27
+ web: {
28
+ enabled: parseBoolean("OPENSEARCH_SOURCE_WEB", process.env.OPENSEARCH_SOURCE_WEB, true),
29
+ key: process.env.OPENSEARCH_WEB_KEY ?? process.env.EXA_API_KEY,
30
+ },
31
+ code: parseBoolean("OPENSEARCH_SOURCE_CODE", process.env.OPENSEARCH_SOURCE_CODE, true),
32
+ },
33
+ depth: parseDepth(process.env.OPENSEARCH_DEPTH),
34
+ synth: parseBoolean("OPENSEARCH_SYNTH", process.env.OPENSEARCH_SYNTH, true),
35
+ };
36
+ }
37
+ export function parsePluginConfig(input) {
38
+ if (!input || typeof input !== "object" || !("opensearch" in input)) {
39
+ return undefined;
40
+ }
41
+ const parsed = ConfigSchema.safeParse(input.opensearch);
42
+ if (parsed.success)
43
+ return parsed.data;
44
+ const issues = parsed.error.issues
45
+ .map((issue) => `${formatIssuePath(issue.path)} ${issue.message}`)
46
+ .join("; ");
47
+ throw new Error(`Invalid opensearch config: ${issues}`);
48
+ }
49
+ export function isSourceAvailable(config, source) {
50
+ if (source === "session")
51
+ return config.sources.session;
52
+ if (source === "web") {
53
+ return config.sources.web.enabled && Boolean(config.sources.web.key);
54
+ }
55
+ return config.sources.code;
56
+ }
57
+ export function resolveSources(config, requested) {
58
+ const requestedSources = Array.from(new Set(requested ?? SOURCE_IDS));
59
+ const sources = requestedSources.filter((source) => isSourceAvailable(config, source));
60
+ const unavailable = requestedSources.filter((source) => !isSourceAvailable(config, source));
61
+ return {
62
+ requested: requestedSources,
63
+ sources,
64
+ unavailable,
65
+ };
66
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const OpensearchPlugin: Plugin;
3
+ export default OpensearchPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,276 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { defaultConfig, parsePluginConfig, resolveSources } from "./config";
3
+ import { noResultsResult, noSourcesResult, rawResultsResult, runSourceSearches, synthesizedResult, } from "./orchestrator";
4
+ import { DEPTHS, ResultSchema, SOURCE_IDS, } from "./schema";
5
+ import { synthesize } from "./synth";
6
+ const BRAND = "OpenSearch";
7
+ function serialize(result) {
8
+ return JSON.stringify(result, null, 2);
9
+ }
10
+ function previewQuery(query, max = 64) {
11
+ if (query.length <= max)
12
+ return query;
13
+ return `${query.slice(0, max - 1)}...`;
14
+ }
15
+ function describeSources(sources) {
16
+ if (sources.length === 0)
17
+ return "no sources";
18
+ if (sources.length === 1)
19
+ return `${sources[0]} only`;
20
+ if (sources.length === 2)
21
+ return `${sources[0]} + ${sources[1]}`;
22
+ return "session + web + code";
23
+ }
24
+ function runningTitle(sources) {
25
+ return `${BRAND} · searching ${describeSources(sources)}`;
26
+ }
27
+ function doneTitle(result) {
28
+ if (result.status === "no_sources")
29
+ return `${BRAND} · unavailable`;
30
+ if (result.status === "no_results")
31
+ return `${BRAND} · no matches`;
32
+ if (result.status === "raw") {
33
+ return `${BRAND} · ${result.meta.sources_yielded} raw result${result.meta.sources_yielded === 1 ? "" : "s"}`;
34
+ }
35
+ if (result.status === "raw_fallback")
36
+ return `${BRAND} · evidence fallback`;
37
+ return `${BRAND} · ${result.meta.sources_yielded} result${result.meta.sources_yielded === 1 ? "" : "s"}`;
38
+ }
39
+ function toolMetadata(input) {
40
+ return {
41
+ brand: BRAND,
42
+ phase: input.phase,
43
+ query: previewQuery(input.query),
44
+ depth: input.depth,
45
+ sources: input.sources,
46
+ source_summary: describeSources(input.sources),
47
+ ...(input.result
48
+ ? {
49
+ status: input.result.status,
50
+ answer: input.result.answer,
51
+ duration_ms: input.result.meta.duration,
52
+ sources_requested: input.result.meta.sources_requested,
53
+ sources_queried: input.result.meta.sources_queried,
54
+ sources_yielded: input.result.meta.sources_yielded,
55
+ source_errors: input.result.meta.source_errors.length,
56
+ sources_unavailable: input.result.meta.sources_unavailable,
57
+ }
58
+ : {}),
59
+ };
60
+ }
61
+ function parseResultOutput(output) {
62
+ try {
63
+ const parsed = ResultSchema.safeParse(JSON.parse(output));
64
+ return parsed.success ? parsed.data : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ function parseRequestedSources(value) {
71
+ if (!Array.isArray(value))
72
+ return undefined;
73
+ return value.filter((source) => typeof source === "string" && SOURCE_IDS.includes(source));
74
+ }
75
+ export const OpensearchPlugin = async (ctx) => {
76
+ let cfg = defaultConfig();
77
+ return {
78
+ async config(input) {
79
+ const next = parsePluginConfig(input);
80
+ if (next)
81
+ cfg = next;
82
+ },
83
+ "tool.definition": async (input, output) => {
84
+ if (input.toolID !== "opensearch")
85
+ return;
86
+ output.description =
87
+ "OpenSearch: use for broad investigation when the user asks to search, compare evidence, gather official docs, or combine session, web, and code results.";
88
+ },
89
+ "tool.execute.after": async (input, output) => {
90
+ if (input.tool !== "opensearch")
91
+ return;
92
+ const result = parseResultOutput(output.output);
93
+ if (!result)
94
+ return;
95
+ const requested = parseRequestedSources(input.args?.sources);
96
+ const resolved = resolveSources(cfg, requested);
97
+ output.title = doneTitle(result);
98
+ output.metadata = {
99
+ ...(output.metadata ?? {}),
100
+ ...toolMetadata({
101
+ phase: "completed",
102
+ query: result.meta.query,
103
+ depth: input.args && typeof input.args.depth === "string"
104
+ ? input.args.depth
105
+ : cfg.depth,
106
+ sources: requested ?? resolved.sources,
107
+ result,
108
+ }),
109
+ };
110
+ },
111
+ tool: {
112
+ opensearch: tool({
113
+ description: "Universal intelligent search. Queries session history, web, and code in parallel. Returns structured evidence-backed answer.",
114
+ args: {
115
+ query: tool.schema.string().describe("What to search for"),
116
+ sources: tool.schema
117
+ .array(tool.schema.enum(SOURCE_IDS))
118
+ .optional()
119
+ .describe("Sources to query. Defaults to all enabled."),
120
+ depth: tool.schema
121
+ .enum(DEPTHS)
122
+ .optional()
123
+ .describe("Search depth. Default: quick"),
124
+ },
125
+ async execute(args, context) {
126
+ const start = Date.now();
127
+ const searchDepth = args.depth ?? cfg.depth;
128
+ const resolved = resolveSources(cfg, args.sources);
129
+ context.metadata({
130
+ title: runningTitle(resolved.sources),
131
+ metadata: toolMetadata({
132
+ phase: "searching",
133
+ query: args.query,
134
+ depth: searchDepth,
135
+ sources: resolved.sources,
136
+ }),
137
+ });
138
+ if (resolved.sources.length === 0) {
139
+ const result = noSourcesResult({
140
+ query: args.query,
141
+ start,
142
+ requested: resolved.requested,
143
+ unavailable: resolved.unavailable,
144
+ });
145
+ context.metadata({
146
+ title: doneTitle(result),
147
+ metadata: toolMetadata({
148
+ phase: "completed",
149
+ query: args.query,
150
+ depth: searchDepth,
151
+ sources: resolved.sources,
152
+ result,
153
+ }),
154
+ });
155
+ return serialize(result);
156
+ }
157
+ const search = await runSourceSearches({
158
+ client: ctx.client,
159
+ directory: context.directory,
160
+ config: cfg,
161
+ query: args.query,
162
+ depth: searchDepth,
163
+ sources: resolved.sources,
164
+ });
165
+ if (search.raw.length === 0) {
166
+ const result = noResultsResult({
167
+ query: args.query,
168
+ start,
169
+ requested: resolved.requested,
170
+ queried: resolved.sources,
171
+ unavailable: resolved.unavailable,
172
+ sourceErrors: search.sourceErrors,
173
+ });
174
+ context.metadata({
175
+ title: doneTitle(result),
176
+ metadata: toolMetadata({
177
+ phase: "completed",
178
+ query: args.query,
179
+ depth: searchDepth,
180
+ sources: resolved.sources,
181
+ result,
182
+ }),
183
+ });
184
+ return serialize(result);
185
+ }
186
+ if (cfg.synth) {
187
+ context.metadata({
188
+ title: `${BRAND} · synthesizing evidence`,
189
+ metadata: {
190
+ ...toolMetadata({
191
+ phase: "synthesizing",
192
+ query: args.query,
193
+ depth: searchDepth,
194
+ sources: resolved.sources,
195
+ }),
196
+ raw_results: search.raw.length,
197
+ },
198
+ });
199
+ try {
200
+ const synthesis = await synthesize(ctx.client, context.directory, search.raw, args.query);
201
+ const result = synthesizedResult({
202
+ query: args.query,
203
+ start,
204
+ requested: resolved.requested,
205
+ queried: resolved.sources,
206
+ unavailable: resolved.unavailable,
207
+ sourceErrors: search.sourceErrors,
208
+ raw: search.raw,
209
+ synthesis,
210
+ });
211
+ context.metadata({
212
+ title: doneTitle(result),
213
+ metadata: toolMetadata({
214
+ phase: "completed",
215
+ query: args.query,
216
+ depth: searchDepth,
217
+ sources: resolved.sources,
218
+ result,
219
+ }),
220
+ });
221
+ return serialize(result);
222
+ }
223
+ catch {
224
+ const result = rawResultsResult({
225
+ query: args.query,
226
+ start,
227
+ requested: resolved.requested,
228
+ queried: resolved.sources,
229
+ unavailable: resolved.unavailable,
230
+ sourceErrors: search.sourceErrors,
231
+ raw: search.raw,
232
+ status: "raw_fallback",
233
+ });
234
+ context.metadata({
235
+ title: doneTitle(result),
236
+ metadata: {
237
+ ...toolMetadata({
238
+ phase: "completed",
239
+ query: args.query,
240
+ depth: searchDepth,
241
+ sources: resolved.sources,
242
+ result,
243
+ }),
244
+ fallback: "synthesis_error",
245
+ },
246
+ });
247
+ return serialize(result);
248
+ }
249
+ }
250
+ const result = rawResultsResult({
251
+ query: args.query,
252
+ start,
253
+ requested: resolved.requested,
254
+ queried: resolved.sources,
255
+ unavailable: resolved.unavailable,
256
+ sourceErrors: search.sourceErrors,
257
+ raw: search.raw,
258
+ status: "raw",
259
+ });
260
+ context.metadata({
261
+ title: doneTitle(result),
262
+ metadata: toolMetadata({
263
+ phase: "completed",
264
+ query: args.query,
265
+ depth: searchDepth,
266
+ sources: resolved.sources,
267
+ result,
268
+ }),
269
+ });
270
+ return serialize(result);
271
+ },
272
+ }),
273
+ },
274
+ };
275
+ };
276
+ export default OpensearchPlugin;
@@ -0,0 +1,60 @@
1
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
2
+ import type { Config, RawResult, Result, Source, SourceError, SourceId, Synthesis } from "./schema";
3
+ export declare function normalize(raw: RawResult): Source;
4
+ export declare function runSourceSearches(input: {
5
+ client: ReturnType<typeof createOpencodeClient>;
6
+ directory: string;
7
+ config: Config;
8
+ query: string;
9
+ depth: Config["depth"];
10
+ sources: SourceId[];
11
+ }): Promise<{
12
+ raw: {
13
+ id: string;
14
+ type: "session" | "web" | "code";
15
+ title: string;
16
+ snippet: string;
17
+ relevance: number;
18
+ url?: string | undefined;
19
+ timestamp?: number | undefined;
20
+ }[];
21
+ sourceErrors: {
22
+ code: "unavailable" | "request_failed" | "invalid_response";
23
+ message: string;
24
+ source: "session" | "web" | "code";
25
+ }[];
26
+ }>;
27
+ export declare function noSourcesResult(input: {
28
+ query: string;
29
+ start: number;
30
+ requested: SourceId[];
31
+ unavailable: SourceId[];
32
+ }): Result;
33
+ export declare function noResultsResult(input: {
34
+ query: string;
35
+ start: number;
36
+ requested: SourceId[];
37
+ queried: SourceId[];
38
+ unavailable: SourceId[];
39
+ sourceErrors: SourceError[];
40
+ }): Result;
41
+ export declare function rawResultsResult(input: {
42
+ query: string;
43
+ start: number;
44
+ requested: SourceId[];
45
+ queried: SourceId[];
46
+ unavailable: SourceId[];
47
+ sourceErrors: SourceError[];
48
+ raw: RawResult[];
49
+ status: "raw" | "raw_fallback";
50
+ }): Result;
51
+ export declare function synthesizedResult(input: {
52
+ query: string;
53
+ start: number;
54
+ requested: SourceId[];
55
+ queried: SourceId[];
56
+ unavailable: SourceId[];
57
+ sourceErrors: SourceError[];
58
+ raw: RawResult[];
59
+ synthesis: Synthesis;
60
+ }): Result;