@kagan-sh/opensearch 0.1.0 → 0.2.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.
package/CONTRIBUTING.md CHANGED
@@ -70,7 +70,7 @@ This repo uses `semantic-release` on `main`.
70
70
 
71
71
  - Prefer Conventional Commits for merge commits and direct commits to `main`
72
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`
73
+ - npm publishing now uses GitHub Actions trusted publishing via OIDC for `@kagan-sh/opensearch`
74
74
  - The npm package must be linked to this repository under the `kagan_sh` publisher account before the first release
75
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
76
 
@@ -95,7 +95,7 @@ This repo uses `semantic-release` on `main`.
95
95
  - CI workflow: `.github/workflows/ci.yml`
96
96
  - Release workflow: `.github/workflows/release.yml`
97
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
98
+ - npm publishing is configured to publish through OIDC without a long-lived npm token
99
99
 
100
100
  ## Pull requests
101
101
 
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  <p align="center">
2
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%">
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-light.svg">
5
+ <img alt="OpenSearch — evidence-backed search for OpenCode" src="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/mark-dark.svg" width="100%">
6
6
  </picture>
7
7
  </p>
8
8
  <p align="center">
9
+ <a href="https://www.npmjs.com/package/@kagan-sh/opensearch"><img src="https://img.shields.io/npm/v/%40kagan-sh%2Fopensearch?style=for-the-badge" alt="npm"></a>
9
10
  <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
11
  <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
12
  <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>
@@ -14,8 +15,9 @@
14
15
  <h3 align="center">
15
16
  <a href="https://kagan-sh.github.io/opensearch/">Docs</a> ·
16
17
  <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>
18
+ <a href="https://kagan-sh.github.io/opensearch/guides/searxng/">SearXNG Setup</a> ·
19
+ <a href="https://kagan-sh.github.io/opensearch/reference/result-contract/">Reference</a> ·
20
+ <a href="https://raw.githubusercontent.com/kagan-sh/opensearch/main/SKILL.md">LLM Quick Reference</a>
19
21
  </h3>
20
22
 
21
23
  ---
@@ -35,56 +37,20 @@ Add the plugin to `opencode.json`:
35
37
 
36
38
  OpenCode installs npm plugins automatically at startup.
37
39
 
38
- Full docs: **[kagan-sh.github.io/opensearch](https://kagan-sh.github.io/opensearch/)**.
40
+ Full docs: **[kagan-sh.github.io/opensearch](https://kagan-sh.github.io/opensearch/)**. Web search uses a self-hosted `SearXNG` instance; setup lives in the docs.
39
41
 
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
42
+ ## License
85
43
 
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.
44
+ [MIT](LICENSE)
87
45
 
88
- ## License
46
+ ---
89
47
 
90
- MIT
48
+ <p align="center">
49
+ <a href="https://www.star-history.com/#kagan-sh/opensearch&type=date">
50
+ <picture>
51
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date&theme=dark" />
52
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date" />
53
+ <img alt="Star History" src="https://api.star-history.com/svg?repos=kagan-sh/opensearch&type=date" width="600" />
54
+ </picture>
55
+ </a>
56
+ </p>
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: opensearch
3
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
4
  license: MIT
5
- compatibility: Requires OpenCode with @kagan-sh/opensearch installed. Web results need OPENSEARCH_WEB_KEY or EXA_API_KEY.
5
+ compatibility: Requires OpenCode with @kagan-sh/opensearch installed. The current web provider is SearXNG, so web results need OPENSEARCH_WEB_URL.
6
6
  metadata:
7
7
  version: 0.0.1
8
8
  ---
package/dist/config.js CHANGED
@@ -26,7 +26,7 @@ export function defaultConfig() {
26
26
  session: parseBoolean("OPENSEARCH_SOURCE_SESSION", process.env.OPENSEARCH_SOURCE_SESSION, true),
27
27
  web: {
28
28
  enabled: parseBoolean("OPENSEARCH_SOURCE_WEB", process.env.OPENSEARCH_SOURCE_WEB, true),
29
- key: process.env.OPENSEARCH_WEB_KEY ?? process.env.EXA_API_KEY,
29
+ url: process.env.OPENSEARCH_WEB_URL,
30
30
  },
31
31
  code: parseBoolean("OPENSEARCH_SOURCE_CODE", process.env.OPENSEARCH_SOURCE_CODE, true),
32
32
  },
@@ -50,7 +50,7 @@ export function isSourceAvailable(config, source) {
50
50
  if (source === "session")
51
51
  return config.sources.session;
52
52
  if (source === "web") {
53
- return config.sources.web.enabled && Boolean(config.sources.web.key);
53
+ return config.sources.web.enabled && Boolean(config.sources.web.url);
54
54
  }
55
55
  return config.sources.code;
56
56
  }
package/dist/index.js CHANGED
@@ -4,6 +4,19 @@ import { noResultsResult, noSourcesResult, rawResultsResult, runSourceSearches,
4
4
  import { DEPTHS, ResultSchema, SOURCE_IDS, } from "./schema";
5
5
  import { synthesize } from "./synth";
6
6
  const BRAND = "OpenSearch";
7
+ const TAGLINE = "evidence-backed search";
8
+ function sourceBadge(source) {
9
+ if (source === "session")
10
+ return "SESSION";
11
+ if (source === "web")
12
+ return "WEB";
13
+ return "CODE";
14
+ }
15
+ function sourceBadges(sources) {
16
+ if (sources.length === 0)
17
+ return ["OFFLINE"];
18
+ return sources.map(sourceBadge);
19
+ }
7
20
  function serialize(result) {
8
21
  return JSON.stringify(result, null, 2);
9
22
  }
@@ -22,28 +35,32 @@ function describeSources(sources) {
22
35
  return "session + web + code";
23
36
  }
24
37
  function runningTitle(sources) {
25
- return `${BRAND} · searching ${describeSources(sources)}`;
38
+ return `${BRAND} // scanning ${describeSources(sources)}`;
26
39
  }
27
40
  function doneTitle(result) {
28
41
  if (result.status === "no_sources")
29
- return `${BRAND} · unavailable`;
42
+ return `${BRAND} // unavailable`;
30
43
  if (result.status === "no_results")
31
- return `${BRAND} · no matches`;
44
+ return `${BRAND} // no matches`;
32
45
  if (result.status === "raw") {
33
- return `${BRAND} · ${result.meta.sources_yielded} raw result${result.meta.sources_yielded === 1 ? "" : "s"}`;
46
+ return `${BRAND} // ${result.meta.sources_yielded} raw result${result.meta.sources_yielded === 1 ? "" : "s"}`;
34
47
  }
35
48
  if (result.status === "raw_fallback")
36
- return `${BRAND} · evidence fallback`;
37
- return `${BRAND} · ${result.meta.sources_yielded} result${result.meta.sources_yielded === 1 ? "" : "s"}`;
49
+ return `${BRAND} // evidence fallback`;
50
+ return `${BRAND} // ${result.meta.sources_yielded} result${result.meta.sources_yielded === 1 ? "" : "s"}`;
38
51
  }
39
52
  function toolMetadata(input) {
40
53
  return {
41
54
  brand: BRAND,
55
+ brand_tagline: TAGLINE,
56
+ brand_origin: "@kagan-sh/opensearch",
57
+ brand_surface: "plugin",
42
58
  phase: input.phase,
43
59
  query: previewQuery(input.query),
44
60
  depth: input.depth,
45
61
  sources: input.sources,
46
62
  source_summary: describeSources(input.sources),
63
+ source_badges: sourceBadges(input.sources),
47
64
  ...(input.result
48
65
  ? {
49
66
  status: input.result.status,
@@ -84,7 +101,7 @@ export const OpensearchPlugin = async (ctx) => {
84
101
  if (input.toolID !== "opensearch")
85
102
  return;
86
103
  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.";
104
+ "OpenSearch // evidence-backed search across session history, SearXNG web search, and public code. Use it for broad investigation, comparison, docs gathering, and cross-source research.";
88
105
  },
89
106
  "tool.execute.after": async (input, output) => {
90
107
  if (input.tool !== "opensearch")
@@ -110,7 +127,7 @@ export const OpensearchPlugin = async (ctx) => {
110
127
  },
111
128
  tool: {
112
129
  opensearch: tool({
113
- description: "Universal intelligent search. Queries session history, web, and code in parallel. Returns structured evidence-backed answer.",
130
+ description: "OpenSearch // evidence-backed search. Queries session history, SearXNG web search, and public code in parallel, then returns a structured answer with explicit status and source metadata.",
114
131
  args: {
115
132
  query: tool.schema.string().describe("What to search for"),
116
133
  sources: tool.schema
@@ -185,7 +202,7 @@ export const OpensearchPlugin = async (ctx) => {
185
202
  }
186
203
  if (cfg.synth) {
187
204
  context.metadata({
188
- title: `${BRAND} · synthesizing evidence`,
205
+ title: `${BRAND} // assembling evidence`,
189
206
  metadata: {
190
207
  ...toolMetadata({
191
208
  phase: "synthesizing",
@@ -194,6 +211,7 @@ export const OpensearchPlugin = async (ctx) => {
194
211
  sources: resolved.sources,
195
212
  }),
196
213
  raw_results: search.raw.length,
214
+ status_note: "assembling evidence",
197
215
  },
198
216
  });
199
217
  try {
@@ -43,7 +43,7 @@ export async function runSourceSearches(input) {
43
43
  return searchSessions(input.client, input.directory, input.query, input.depth);
44
44
  }
45
45
  if (source === "web") {
46
- return searchWeb(input.query, input.config.sources.web.key, input.depth);
46
+ return searchWeb(input.query, input.config.sources.web.url, input.depth);
47
47
  }
48
48
  return searchCode(input.query, input.depth);
49
49
  }));
@@ -61,7 +61,7 @@ export function noSourcesResult(input) {
61
61
  sources: [],
62
62
  followups: [
63
63
  "Enable at least one search source in opensearch config",
64
- "Set OPENSEARCH_WEB_KEY or EXA_API_KEY to enable web search",
64
+ "Set OPENSEARCH_WEB_URL to enable web search",
65
65
  ],
66
66
  meta: resultMeta({
67
67
  query: input.query,
package/dist/schema.d.ts CHANGED
@@ -346,27 +346,27 @@ export declare const ConfigSchema: z.ZodObject<{
346
346
  session: z.ZodBoolean;
347
347
  web: z.ZodObject<{
348
348
  enabled: z.ZodBoolean;
349
- key: z.ZodOptional<z.ZodString>;
349
+ url: z.ZodOptional<z.ZodString>;
350
350
  }, "strict", z.ZodTypeAny, {
351
351
  enabled: boolean;
352
- key?: string | undefined;
352
+ url?: string | undefined;
353
353
  }, {
354
354
  enabled: boolean;
355
- key?: string | undefined;
355
+ url?: string | undefined;
356
356
  }>;
357
357
  code: z.ZodBoolean;
358
358
  }, "strict", z.ZodTypeAny, {
359
359
  session: boolean;
360
360
  web: {
361
361
  enabled: boolean;
362
- key?: string | undefined;
362
+ url?: string | undefined;
363
363
  };
364
364
  code: boolean;
365
365
  }, {
366
366
  session: boolean;
367
367
  web: {
368
368
  enabled: boolean;
369
- key?: string | undefined;
369
+ url?: string | undefined;
370
370
  };
371
371
  code: boolean;
372
372
  }>;
@@ -377,7 +377,7 @@ export declare const ConfigSchema: z.ZodObject<{
377
377
  session: boolean;
378
378
  web: {
379
379
  enabled: boolean;
380
- key?: string | undefined;
380
+ url?: string | undefined;
381
381
  };
382
382
  code: boolean;
383
383
  };
@@ -388,7 +388,7 @@ export declare const ConfigSchema: z.ZodObject<{
388
388
  session: boolean;
389
389
  web: {
390
390
  enabled: boolean;
391
- key?: string | undefined;
391
+ url?: string | undefined;
392
392
  };
393
393
  code: boolean;
394
394
  };
package/dist/schema.js CHANGED
@@ -82,7 +82,7 @@ export const ConfigSchema = z
82
82
  web: z
83
83
  .object({
84
84
  enabled: z.boolean(),
85
- key: z.string().optional(),
85
+ url: z.string().url().optional(),
86
86
  })
87
87
  .strict(),
88
88
  code: z.boolean(),
@@ -1,3 +1,3 @@
1
1
  import type { Depth } from "../schema";
2
2
  import { type SourceSearchOutcome } from "./shared";
3
- export declare function searchWeb(query: string, key: string | undefined, depth: Depth): Promise<SourceSearchOutcome>;
3
+ export declare function searchWeb(query: string, baseUrl: string | undefined, depth: Depth): Promise<SourceSearchOutcome>;
@@ -1,46 +1,38 @@
1
1
  import { failure, messageFromError } from "./shared";
2
- export async function searchWeb(query, key, depth) {
2
+ export async function searchWeb(query, baseUrl, depth) {
3
3
  const source = "web";
4
- if (!key) {
5
- return failure(source, "unavailable", "Web source requires OPENSEARCH_WEB_KEY or EXA_API_KEY.");
4
+ if (!baseUrl) {
5
+ return failure(source, "unavailable", "Web source requires OPENSEARCH_WEB_URL.");
6
6
  }
7
7
  try {
8
- const res = await fetch("https://api.exa.ai/search", {
9
- method: "POST",
8
+ const url = new URL("search", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
9
+ url.searchParams.set("q", query);
10
+ url.searchParams.set("format", "json");
11
+ const res = await fetch(url, {
10
12
  headers: {
11
- "x-api-key": key,
12
- "Content-Type": "application/json",
13
+ Accept: "application/json",
13
14
  },
14
- body: JSON.stringify({
15
- query,
16
- numResults: depth === "quick" ? 5 : 10,
17
- type: "auto",
18
- contents: {
19
- text: {
20
- maxCharacters: 1000,
21
- },
22
- },
23
- }),
24
15
  });
25
16
  if (!res.ok) {
26
- return failure(source, "request_failed", `Exa search failed with status ${res.status}.`);
17
+ return failure(source, "request_failed", res.status === 403
18
+ ? "SearXNG search failed with status 403. Enable JSON output on the instance."
19
+ : `SearXNG search failed with status ${res.status}.`);
27
20
  }
28
21
  const body = (await res.json());
29
22
  if (!Array.isArray(body.results)) {
30
- return failure(source, "invalid_response", "Exa search returned an invalid payload.");
23
+ return failure(source, "invalid_response", "SearXNG search returned an invalid payload.");
31
24
  }
32
- const list = body.results;
25
+ const limit = depth === "quick" ? 5 : 10;
26
+ const list = body.results.slice(0, limit);
33
27
  return {
34
28
  source,
35
29
  results: list.map((item, i) => ({
36
- id: item.id ?? `web-${i}`,
30
+ id: `web-${i}`,
37
31
  type: source,
38
32
  title: item.title ?? item.url ?? "Untitled",
39
- snippet: (item.text ?? "").slice(0, 700),
33
+ snippet: (item.content ?? item.url ?? "").slice(0, 700),
40
34
  url: item.url,
41
- relevance: typeof item.score === "number"
42
- ? Math.min(1, Math.max(0, item.score))
43
- : 0.5,
35
+ relevance: Math.max(0.1, 1 - i / Math.max(1, list.length)),
44
36
  timestamp: item.publishedDate
45
37
  ? Date.parse(item.publishedDate) || undefined
46
38
  : undefined,
@@ -48,6 +40,6 @@ export async function searchWeb(query, key, depth) {
48
40
  };
49
41
  }
50
42
  catch (error) {
51
- return failure(source, "request_failed", `Exa search failed: ${messageFromError(error)}`);
43
+ return failure(source, "request_failed", `SearXNG search failed: ${messageFromError(error)}`);
52
44
  }
53
45
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@kagan-sh/opensearch",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode plugin that combines session, web, and code search",
5
+ "license": "MIT",
5
6
  "homepage": "https://kagan-sh.github.io/opensearch/",
6
7
  "repository": {
7
8
  "type": "git",