searxng, icecast, photos: full nox modules (ADR 0044/0046)
searxng: a search tool — tools-only, a stateless search has nothing to observe. icecast: status tool, emits stream.started/.stopped by diffing live mountpoints. photos: status/albums/recent tools, emits item.added (immich-shaped API, coded defensively since the stub ships a placeholder image — flagged in the code). Typecheck; manifests parse.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// SearXNG's API client — searxng's own code, living in the module (novox/hq ADR 0044). SearXNG is a
|
||||
// privacy-respecting metasearch engine: it forwards a query to many upstream engines and returns the
|
||||
// merged results. Its JSON API (`/search?q=...&format=json`) is what makes a `searxng_search` tool
|
||||
// useful; the client speaks only that. No credential — the instance is reached inside the mesh.
|
||||
|
||||
export interface SearxResult {
|
||||
title: string;
|
||||
url: string;
|
||||
content?: string;
|
||||
engine?: string;
|
||||
category?: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface SearxSearch {
|
||||
query: string;
|
||||
numberOfResults: number;
|
||||
results: SearxResult[];
|
||||
suggestions: string[];
|
||||
answers: string[];
|
||||
}
|
||||
|
||||
export interface SearxOptions {
|
||||
categories?: string;
|
||||
language?: string;
|
||||
pageno?: number;
|
||||
}
|
||||
|
||||
export class SearxngClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.baseUrl = url.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/** Build from the module's environment. No key: SearXNG's search API is open on the mesh, so a URL
|
||||
* is all it takes — defaulting to the container's own listen port. */
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): SearxngClient {
|
||||
const url = env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`;
|
||||
return new SearxngClient(url);
|
||||
}
|
||||
|
||||
async search(query: string, opts: SearxOptions = {}): Promise<SearxSearch> {
|
||||
const params = new URLSearchParams({ q: query, format: "json" });
|
||||
if (opts.categories) params.set("categories", opts.categories);
|
||||
if (opts.language) params.set("language", opts.language);
|
||||
if (opts.pageno) params.set("pageno", String(opts.pageno));
|
||||
|
||||
const res = await fetch(`${this.baseUrl}/search?${params.toString()}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) throw new Error(`SearXNG search: ${res.status} ${await res.text()}`);
|
||||
const data = (await res.json()) as {
|
||||
results?: SearxResult[];
|
||||
suggestions?: string[];
|
||||
answers?: string[];
|
||||
number_of_results?: number;
|
||||
};
|
||||
const results = data.results ?? [];
|
||||
return {
|
||||
query,
|
||||
// SearXNG's own count is often 0 even with results; fall back to what we actually got.
|
||||
numberOfResults: data.number_of_results || results.length,
|
||||
results: results.map((r) => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
content: r.content,
|
||||
engine: r.engine,
|
||||
category: r.category,
|
||||
score: r.score,
|
||||
})),
|
||||
suggestions: data.suggestions ?? [],
|
||||
answers: data.answers ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@novox/module-searxng",
|
||||
"version": "0.1.0",
|
||||
"description": "searxng — privacy-respecting metasearch. Its API client and tools live here (novox/hq ADR 0044).",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@novox/mesh-sdk": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// searxng's tools (novox/hq ADR 0044), importing searxng's own client. A stateless metasearch: one
|
||||
// query in, merged results out — genuinely useful, and with nothing to observe over time it stays
|
||||
// tools-only (no events, no broker).
|
||||
|
||||
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import { SearxngClient } from "../client.js";
|
||||
|
||||
export function getSearxngTools(searxng: SearxngClient): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: "searxng_search",
|
||||
description: "Search the web through SearXNG's privacy-respecting metasearch — merged results from many engines.",
|
||||
input: {
|
||||
query: { type: "string", description: "the search query" },
|
||||
categories: { type: "string", description: "comma-separated categories, e.g. 'general', 'news', 'images'" },
|
||||
language: { type: "string", description: "language code, e.g. 'en' or 'en-US'" },
|
||||
},
|
||||
run: async (args) => {
|
||||
const found = await searxng.search(String(args.query), {
|
||||
categories: args.categories ? String(args.categories) : undefined,
|
||||
language: args.language ? String(args.language) : undefined,
|
||||
});
|
||||
return found;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// A URL always resolves (it defaults), so this normally contributes its tool; the try/catch keeps a
|
||||
// misconfiguration from taking the whole runtime down.
|
||||
registerModuleTools("searxng", (env) => {
|
||||
try {
|
||||
return getSearxngTools(SearxngClient.fromEnv(env));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["client.ts", "tools/index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user