@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 +2 -2
- package/README.md +20 -54
- package/SKILL.md +1 -1
- package/dist/config.js +2 -2
- package/dist/index.js +27 -9
- package/dist/orchestrator.js +2 -2
- package/dist/schema.d.ts +7 -7
- package/dist/schema.js +1 -1
- package/dist/sources/web.d.ts +1 -1
- package/dist/sources/web.js +18 -26
- package/package.json +2 -1
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
|
|
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
|
|
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/
|
|
4
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/
|
|
5
|
-
<img alt="OpenSearch — evidence-backed search for OpenCode" src="https://raw.githubusercontent.com/kagan-sh/opensearch/main/.github/assets/
|
|
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/
|
|
18
|
-
<a href="https://
|
|
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
|
-
##
|
|
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
|
-
|
|
44
|
+
[MIT](LICENSE)
|
|
87
45
|
|
|
88
|
-
|
|
46
|
+
---
|
|
89
47
|
|
|
90
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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}
|
|
38
|
+
return `${BRAND} // scanning ${describeSources(sources)}`;
|
|
26
39
|
}
|
|
27
40
|
function doneTitle(result) {
|
|
28
41
|
if (result.status === "no_sources")
|
|
29
|
-
return `${BRAND}
|
|
42
|
+
return `${BRAND} // unavailable`;
|
|
30
43
|
if (result.status === "no_results")
|
|
31
|
-
return `${BRAND}
|
|
44
|
+
return `${BRAND} // no matches`;
|
|
32
45
|
if (result.status === "raw") {
|
|
33
|
-
return `${BRAND}
|
|
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}
|
|
37
|
-
return `${BRAND}
|
|
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
|
|
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: "
|
|
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}
|
|
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 {
|
package/dist/orchestrator.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
349
|
+
url: z.ZodOptional<z.ZodString>;
|
|
350
350
|
}, "strict", z.ZodTypeAny, {
|
|
351
351
|
enabled: boolean;
|
|
352
|
-
|
|
352
|
+
url?: string | undefined;
|
|
353
353
|
}, {
|
|
354
354
|
enabled: boolean;
|
|
355
|
-
|
|
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
|
-
|
|
362
|
+
url?: string | undefined;
|
|
363
363
|
};
|
|
364
364
|
code: boolean;
|
|
365
365
|
}, {
|
|
366
366
|
session: boolean;
|
|
367
367
|
web: {
|
|
368
368
|
enabled: boolean;
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
391
|
+
url?: string | undefined;
|
|
392
392
|
};
|
|
393
393
|
code: boolean;
|
|
394
394
|
};
|
package/dist/schema.js
CHANGED
package/dist/sources/web.d.ts
CHANGED
|
@@ -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,
|
|
3
|
+
export declare function searchWeb(query: string, baseUrl: string | undefined, depth: Depth): Promise<SourceSearchOutcome>;
|
package/dist/sources/web.js
CHANGED
|
@@ -1,46 +1,38 @@
|
|
|
1
1
|
import { failure, messageFromError } from "./shared";
|
|
2
|
-
export async function searchWeb(query,
|
|
2
|
+
export async function searchWeb(query, baseUrl, depth) {
|
|
3
3
|
const source = "web";
|
|
4
|
-
if (!
|
|
5
|
-
return failure(source, "unavailable", "Web source requires
|
|
4
|
+
if (!baseUrl) {
|
|
5
|
+
return failure(source, "unavailable", "Web source requires OPENSEARCH_WEB_URL.");
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
-
const
|
|
9
|
-
|
|
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
|
-
"
|
|
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",
|
|
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", "
|
|
23
|
+
return failure(source, "invalid_response", "SearXNG search returned an invalid payload.");
|
|
31
24
|
}
|
|
32
|
-
const
|
|
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:
|
|
30
|
+
id: `web-${i}`,
|
|
37
31
|
type: source,
|
|
38
32
|
title: item.title ?? item.url ?? "Untitled",
|
|
39
|
-
snippet: (item.
|
|
33
|
+
snippet: (item.content ?? item.url ?? "").slice(0, 700),
|
|
40
34
|
url: item.url,
|
|
41
|
-
relevance:
|
|
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", `
|
|
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.
|
|
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",
|