pi-lean-search 0.2.4 → 0.3.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/index.ts +134 -64
- package/package.json +2 -1
package/index.ts
CHANGED
|
@@ -4,28 +4,49 @@
|
|
|
4
4
|
* Registers the `web-search` tool and a `/searxng-status` diagnostic command.
|
|
5
5
|
* Manages the `search` status bar slot with health-colored glyphs.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* ● searxng (accent/blue)
|
|
9
|
-
* ● searxng (warning/yellow)
|
|
10
|
-
* ● searxng (error/red)
|
|
11
|
-
*
|
|
12
|
-
* Portal owns the `search` slot's "off" state: when `/web off` is called,
|
|
13
|
-
* portal writes `○ searxng` (open circle). Search overrides with the
|
|
14
|
-
* health-colored glyph on session_start.
|
|
7
|
+
* Owns the `search.web` toolset (co-activated off `portal.web`):
|
|
8
|
+
* ● searxng (accent/blue) — healthy and reachable
|
|
9
|
+
* ● searxng (warning/yellow) — server up but pipeline degraded
|
|
10
|
+
* ● searxng (error/red) — unreachable
|
|
11
|
+
* ○ searxng — search tools off
|
|
15
12
|
*/
|
|
16
13
|
|
|
17
14
|
import type {
|
|
18
15
|
ExtensionAPI,
|
|
19
16
|
ExtensionContext,
|
|
17
|
+
ThemeColor,
|
|
20
18
|
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { defineToolset, TOOLSET_EVENTS } from "pi-tool-masking";
|
|
20
|
+
import type { ToolsetSpec, ToolsetChangedEvent } from "pi-tool-masking";
|
|
21
21
|
import { readSearxngUrl } from "./search-config.js";
|
|
22
22
|
import { webSearchTool } from "./web-search-tool.js";
|
|
23
23
|
|
|
24
|
-
// ───
|
|
24
|
+
// ─── Toolset spec ────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const SEARCH_WEB_SPEC: ToolsetSpec = {
|
|
27
|
+
id: "search.web",
|
|
28
|
+
names: new Set(["web-search"]),
|
|
29
|
+
persistKey: "toolset-state:search.web",
|
|
30
|
+
defaultEnabled: true,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ─── Module-level state ──────────────────────────────────────────
|
|
25
34
|
|
|
26
35
|
/** Cached SearXNG URL (read once at startup, stable mid-session). */
|
|
27
36
|
let _searxngUrl: string | undefined;
|
|
28
37
|
|
|
38
|
+
/** Last known search.web enabled state (updated by library events). */
|
|
39
|
+
let _lastSearchEnabled = true;
|
|
40
|
+
|
|
41
|
+
/** Last known health state: true=healthy, false=unreachable, null=unconfigured. */
|
|
42
|
+
let _lastHealth: boolean | null = null;
|
|
43
|
+
|
|
44
|
+
/** Whether the search pipeline is degraded (server up, aggregation broken). */
|
|
45
|
+
let _lastDegraded = false;
|
|
46
|
+
|
|
47
|
+
/** Cached ExtensionContext for event-driven glyph rendering. */
|
|
48
|
+
let _lastCtx: ExtensionContext | null = null;
|
|
49
|
+
|
|
29
50
|
// ─── Health probes ───────────────────────────────────────────────
|
|
30
51
|
|
|
31
52
|
/**
|
|
@@ -95,7 +116,7 @@ async function checkSearchReachable(
|
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
|
|
98
|
-
// ─── Status slot helpers
|
|
119
|
+
// ─── Status slot helpers ─────────────────────────────────────────
|
|
99
120
|
|
|
100
121
|
/**
|
|
101
122
|
* Normalize the SearXNG URL for display (strip protocol prefix for brevity).
|
|
@@ -105,60 +126,65 @@ function displayUrl(url: string): string {
|
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Coloring:
|
|
111
|
-
* - Healthy: accent (blue) — same as browser on
|
|
112
|
-
* - Degraded: warning (yellow/gold) — server up but pipeline broken
|
|
113
|
-
* - Unhealthy: error (red) — unreachable or unconfigured
|
|
129
|
+
* Render the `search` status slot based on toolset state + health.
|
|
114
130
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
131
|
+
* search.web off → ○ searxng
|
|
132
|
+
* search.web on + healthy → ● searxng (accent/blue)
|
|
133
|
+
* search.web on + degraded → ● searxng (warning/yellow)
|
|
134
|
+
* search.web on + unreachable → ● searxng (error/red)
|
|
135
|
+
* unconfigured → (clear slot)
|
|
117
136
|
*/
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
function renderSearchGlyph(ctx: {
|
|
138
|
+
ui: {
|
|
139
|
+
setStatus: (key: string, label: string) => void;
|
|
140
|
+
theme: { fg: (c: ThemeColor, t: string) => string };
|
|
141
|
+
};
|
|
142
|
+
}): void {
|
|
143
|
+
const safeSetStatus = (text: string) => {
|
|
125
144
|
try {
|
|
126
|
-
ctx.ui.setStatus("search",
|
|
145
|
+
ctx.ui.setStatus("search", text);
|
|
127
146
|
} catch {
|
|
128
|
-
|
|
147
|
+
/* ctx.ui may be unavailable */
|
|
129
148
|
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Unconfigured — clear slot entirely
|
|
152
|
+
if (_lastHealth === null) {
|
|
153
|
+
safeSetStatus("");
|
|
130
154
|
return;
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ctx.ui.setStatus("search", ctx.ui.theme.fg("error", "●") + " searxng");
|
|
137
|
-
} catch {
|
|
138
|
-
// ignore
|
|
139
|
-
}
|
|
157
|
+
// search.web disabled — show off state
|
|
158
|
+
if (!_lastSearchEnabled) {
|
|
159
|
+
safeSetStatus("○ searxng");
|
|
140
160
|
return;
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ctx.ui.setStatus("search", ctx.ui.theme.fg("warning", "●") + " searxng");
|
|
147
|
-
} catch {
|
|
148
|
-
// ignore
|
|
149
|
-
}
|
|
163
|
+
// search.web enabled — show health-colored glyph
|
|
164
|
+
if (!_lastHealth) {
|
|
165
|
+
safeSetStatus(ctx.ui.theme.fg("error", "●") + " searxng");
|
|
150
166
|
return;
|
|
151
167
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
ctx.ui.setStatus("search", ctx.ui.theme.fg("accent", "●") + " searxng");
|
|
156
|
-
} catch {
|
|
157
|
-
// ignore
|
|
168
|
+
if (_lastDegraded) {
|
|
169
|
+
safeSetStatus(ctx.ui.theme.fg("warning", "●") + " searxng");
|
|
170
|
+
return;
|
|
158
171
|
}
|
|
172
|
+
safeSetStatus(ctx.ui.theme.fg("accent", "●") + " searxng");
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
// ───
|
|
175
|
+
// ─── Test helpers ─────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/** @internal Reset cached state to defaults (test helper). */
|
|
178
|
+
function _resetStateForTest(): void {
|
|
179
|
+
_lastSearchEnabled = true;
|
|
180
|
+
_lastHealth = null;
|
|
181
|
+
_lastDegraded = false;
|
|
182
|
+
_lastCtx = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { _resetStateForTest };
|
|
186
|
+
|
|
187
|
+
// ─── Extension entry point ───────────────────────────────────────
|
|
162
188
|
|
|
163
189
|
export default function (pi: ExtensionAPI) {
|
|
164
190
|
// Read config at startup
|
|
@@ -167,48 +193,83 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
193
|
// ── Register the web-search tool ─────────────────────────
|
|
168
194
|
pi.registerTool(webSearchTool);
|
|
169
195
|
|
|
170
|
-
// ──
|
|
196
|
+
// ── Define the search.web toolset ────────────────────────
|
|
197
|
+
// Registers restore handler on session_start / session_tree.
|
|
198
|
+
const searchToolset = defineToolset(pi, SEARCH_WEB_SPEC);
|
|
199
|
+
|
|
200
|
+
// ── Co-activation: mirror portal.web changed events ──────
|
|
201
|
+
// Listen on changed ONLY, not restored (§10.1).
|
|
202
|
+
pi.events.on(TOOLSET_EVENTS.changed, (data: unknown) => {
|
|
203
|
+
const event = data as ToolsetChangedEvent;
|
|
204
|
+
if (event.id === "portal.web") {
|
|
205
|
+
if (event.enabled) {
|
|
206
|
+
searchToolset.enable(pi);
|
|
207
|
+
} else {
|
|
208
|
+
searchToolset.disable(pi);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ── Keep cached state in sync with library events ────────
|
|
214
|
+
const syncSearchState = (data: unknown) => {
|
|
215
|
+
const event = data as ToolsetChangedEvent;
|
|
216
|
+
if (event.id === "search.web") {
|
|
217
|
+
_lastSearchEnabled = event.enabled;
|
|
218
|
+
if (_lastCtx) renderSearchGlyph(_lastCtx);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
pi.events.on(TOOLSET_EVENTS.changed, syncSearchState);
|
|
222
|
+
pi.events.on(TOOLSET_EVENTS.restored, syncSearchState);
|
|
223
|
+
|
|
224
|
+
// ── Session start: health probe + glyph ──────────────────
|
|
171
225
|
pi.on("session_start", async (_event, ctx) => {
|
|
226
|
+
_lastCtx = ctx;
|
|
227
|
+
|
|
172
228
|
// Re-read config in case it changed between sessions
|
|
173
229
|
_searxngUrl = readSearxngUrl();
|
|
174
230
|
|
|
175
231
|
if (!_searxngUrl) {
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
_lastHealth = null;
|
|
233
|
+
renderSearchGlyph(ctx);
|
|
178
234
|
return;
|
|
179
235
|
}
|
|
180
236
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
if (!activeTools.includes("web-search")) {
|
|
185
|
-
// Tools are off — don't override portal's ○ searxng
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
237
|
+
// Note: by now the library's restore handler has already
|
|
238
|
+
// fired (registered by defineToolset), so _lastSearchEnabled
|
|
239
|
+
// reflects the restored toolset state.
|
|
188
240
|
|
|
189
241
|
const reachable = await checkServerReachable(
|
|
190
242
|
_searxngUrl,
|
|
191
243
|
ctx.signal ?? undefined,
|
|
192
244
|
);
|
|
245
|
+
_lastHealth = reachable;
|
|
246
|
+
_lastDegraded = false;
|
|
193
247
|
|
|
194
248
|
if (reachable) {
|
|
195
|
-
setSearchStatus(ctx, true, false);
|
|
196
249
|
ctx.ui.notify(
|
|
197
250
|
`🔍 SearXNG at ${displayUrl(_searxngUrl)} is available`,
|
|
198
251
|
"info",
|
|
199
252
|
);
|
|
200
253
|
} else {
|
|
201
|
-
setSearchStatus(ctx, false, false);
|
|
202
254
|
ctx.ui.notify(
|
|
203
255
|
`⚠ SearXNG at ${displayUrl(_searxngUrl)} is unreachable. ` +
|
|
204
256
|
"Web search will degrade gracefully with error messages.",
|
|
205
257
|
"warning",
|
|
206
258
|
);
|
|
207
259
|
}
|
|
260
|
+
|
|
261
|
+
renderSearchGlyph(ctx);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── Session tree: re-render with cached state ────────────
|
|
265
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
266
|
+
_lastCtx = ctx;
|
|
267
|
+
renderSearchGlyph(ctx);
|
|
208
268
|
});
|
|
209
269
|
|
|
210
270
|
// ── Session shutdown: clean up ───────────────────────────
|
|
211
271
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
272
|
+
_lastCtx = null;
|
|
212
273
|
try {
|
|
213
274
|
ctx?.ui?.setStatus?.("search", "");
|
|
214
275
|
} catch {
|
|
@@ -223,13 +284,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
223
284
|
"and update the status bar. Use when web-search returns " +
|
|
224
285
|
"errors or when you've just started/restarted SearXNG.",
|
|
225
286
|
handler: async (_args, ctx) => {
|
|
287
|
+
_lastCtx = ctx;
|
|
288
|
+
|
|
226
289
|
if (!_searxngUrl) {
|
|
227
290
|
ctx.ui.notify(
|
|
228
291
|
"❌ SearXNG is not configured. " +
|
|
229
292
|
"Set `searxng.url` in your Pi settings.json.",
|
|
230
293
|
"error",
|
|
231
294
|
);
|
|
232
|
-
|
|
295
|
+
_lastHealth = false;
|
|
296
|
+
renderSearchGlyph(ctx);
|
|
233
297
|
return;
|
|
234
298
|
}
|
|
235
299
|
|
|
@@ -240,7 +304,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
304
|
);
|
|
241
305
|
|
|
242
306
|
if (searchOk) {
|
|
243
|
-
|
|
307
|
+
_lastHealth = true;
|
|
308
|
+
_lastDegraded = false;
|
|
309
|
+
renderSearchGlyph(ctx);
|
|
244
310
|
ctx.ui.notify(
|
|
245
311
|
`✅ SearXNG search pipeline is working at ${displayUrl(_searxngUrl)}`,
|
|
246
312
|
"info",
|
|
@@ -255,7 +321,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
255
321
|
);
|
|
256
322
|
|
|
257
323
|
if (serverOk) {
|
|
258
|
-
|
|
324
|
+
_lastHealth = true;
|
|
325
|
+
_lastDegraded = true;
|
|
326
|
+
renderSearchGlyph(ctx);
|
|
259
327
|
ctx.ui.notify(
|
|
260
328
|
"⚠ SearXNG server is up but the search pipeline " +
|
|
261
329
|
"may be broken (aggregation failed). " +
|
|
@@ -263,7 +331,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
263
331
|
"warning",
|
|
264
332
|
);
|
|
265
333
|
} else {
|
|
266
|
-
|
|
334
|
+
_lastHealth = false;
|
|
335
|
+
_lastDegraded = false;
|
|
336
|
+
renderSearchGlyph(ctx);
|
|
267
337
|
ctx.ui.notify(
|
|
268
338
|
`❌ SearXNG at ${displayUrl(_searxngUrl)} is not responding. ` +
|
|
269
339
|
"Check that the SearXNG service is running.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi extension. SearXNG search tool for Pi — pairs with pi-lean-portal's /web toggle; search-only installs are valid, or add it to a portal install for the full web-tools suite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@earendil-works/pi-coding-agent": "*",
|
|
43
43
|
"@earendil-works/pi-tui": "*",
|
|
44
44
|
"pi-lean-portal": "*",
|
|
45
|
+
"pi-tool-masking": "^1.0.0",
|
|
45
46
|
"typebox": "*"
|
|
46
47
|
},
|
|
47
48
|
"peerDependenciesMeta": {
|