@alisio/core 0.1.0-alpha.11 → 0.1.0-alpha.13
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/dist/application.d.ts +2 -0
- package/dist/application.js +12 -0
- package/dist/config.d.ts +18 -2
- package/dist/config.js +33 -1
- package/dist/core/compaction.js +1 -0
- package/dist/core/runner.d.ts +7 -0
- package/dist/core/runner.js +3 -0
- package/dist/plugins/host.d.ts +6 -0
- package/dist/plugins/host.js +8 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/package.json +2 -2
package/dist/application.d.ts
CHANGED
|
@@ -171,6 +171,8 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
171
171
|
* (next instructions load), `websearch.provider` mutates the shared config object the search
|
|
172
172
|
* chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
|
|
173
173
|
* and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
|
|
174
|
+
* `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
|
|
175
|
+
* the next `/agents`/`/effort` selection and the next prompt already read the live config.
|
|
174
176
|
* `mcp.allow` is deliberately not routed here: it flows through
|
|
175
177
|
* `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
|
|
176
178
|
* Throws under `--read-only`. Returns the written config file.
|
package/dist/application.js
CHANGED
|
@@ -743,6 +743,8 @@ export async function createApplication(options = {}) {
|
|
|
743
743
|
* (next instructions load), `websearch.provider` mutates the shared config object the search
|
|
744
744
|
* chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
|
|
745
745
|
* and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
|
|
746
|
+
* `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
|
|
747
|
+
* the next `/agents`/`/effort` selection and the next prompt already read the live config.
|
|
746
748
|
* `mcp.allow` is deliberately not routed here: it flows through
|
|
747
749
|
* `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
|
|
748
750
|
* Throws under `--read-only`. Returns the written config file.
|
|
@@ -790,6 +792,16 @@ export async function createApplication(options = {}) {
|
|
|
790
792
|
case "tui.skillSlashCommands":
|
|
791
793
|
config.tui = { ...config.tui, skillSlashCommands: value === true };
|
|
792
794
|
break;
|
|
795
|
+
case "agents.active":
|
|
796
|
+
config.agents = { ...config.agents, active: String(value) };
|
|
797
|
+
break;
|
|
798
|
+
case "agents.effort":
|
|
799
|
+
// Clearing (undefined) drops the leaf from the live config and the JSON file.
|
|
800
|
+
config.agents =
|
|
801
|
+
value === undefined
|
|
802
|
+
? { active: config.agents.active }
|
|
803
|
+
: { ...config.agents, effort: String(value) };
|
|
804
|
+
break;
|
|
793
805
|
case "websearch.provider":
|
|
794
806
|
// Mutated in place (same object identity) so the tool chain's per-call
|
|
795
807
|
// `websearchCtx.config` sees the new provider on the very next search call.
|
package/dist/config.d.ts
CHANGED
|
@@ -155,6 +155,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
155
155
|
websearch: z.ZodDefault<z.ZodObject<{
|
|
156
156
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
157
157
|
brave: "brave";
|
|
158
|
+
"duckduckgo-html": "duckduckgo-html";
|
|
158
159
|
"duckduckgo-instant": "duckduckgo-instant";
|
|
159
160
|
native: "native";
|
|
160
161
|
searxng: "searxng";
|
|
@@ -176,6 +177,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
176
177
|
paddingX: z.ZodDefault<z.ZodNumber>;
|
|
177
178
|
skillSlashCommands: z.ZodDefault<z.ZodBoolean>;
|
|
178
179
|
}, z.core.$strict>>;
|
|
180
|
+
agents: z.ZodDefault<z.ZodObject<{
|
|
181
|
+
active: z.ZodDefault<z.ZodString>;
|
|
182
|
+
effort: z.ZodOptional<z.ZodString>;
|
|
183
|
+
}, z.core.$strict>>;
|
|
179
184
|
}, z.core.$strict>, z.ZodTransform<{
|
|
180
185
|
schemaVersion: 1;
|
|
181
186
|
provider: {
|
|
@@ -215,7 +220,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
215
220
|
maxOutputTokens: number;
|
|
216
221
|
};
|
|
217
222
|
websearch: {
|
|
218
|
-
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
223
|
+
provider?: "brave" | "duckduckgo-html" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
219
224
|
searxngUrl?: string | undefined;
|
|
220
225
|
apiKeyEnv?: string | undefined;
|
|
221
226
|
nativeToolType: string;
|
|
@@ -231,6 +236,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
231
236
|
paddingX: number;
|
|
232
237
|
skillSlashCommands: boolean;
|
|
233
238
|
};
|
|
239
|
+
agents: {
|
|
240
|
+
active: string;
|
|
241
|
+
effort?: string | undefined;
|
|
242
|
+
};
|
|
234
243
|
mcp: {
|
|
235
244
|
allow?: boolean | undefined;
|
|
236
245
|
servers: {
|
|
@@ -353,7 +362,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
353
362
|
maxOutputTokens: number;
|
|
354
363
|
};
|
|
355
364
|
websearch: {
|
|
356
|
-
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
365
|
+
provider?: "brave" | "duckduckgo-html" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
357
366
|
searxngUrl?: string | undefined;
|
|
358
367
|
apiKeyEnv?: string | undefined;
|
|
359
368
|
nativeToolType: string;
|
|
@@ -369,6 +378,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
369
378
|
paddingX: number;
|
|
370
379
|
skillSlashCommands: boolean;
|
|
371
380
|
};
|
|
381
|
+
agents: {
|
|
382
|
+
active: string;
|
|
383
|
+
effort?: string | undefined;
|
|
384
|
+
};
|
|
372
385
|
}>>;
|
|
373
386
|
export type Config = z.infer<typeof configSchema>;
|
|
374
387
|
export type ServerConfig = z.infer<typeof serverSchema>;
|
|
@@ -444,12 +457,15 @@ declare const SETTABLE_KEYS: {
|
|
|
444
457
|
readonly "tui.skillSlashCommands": z.ZodDefault<z.ZodBoolean>;
|
|
445
458
|
readonly "websearch.provider": z.ZodOptional<z.ZodEnum<{
|
|
446
459
|
brave: "brave";
|
|
460
|
+
"duckduckgo-html": "duckduckgo-html";
|
|
447
461
|
"duckduckgo-instant": "duckduckgo-instant";
|
|
448
462
|
native: "native";
|
|
449
463
|
searxng: "searxng";
|
|
450
464
|
serpapi: "serpapi";
|
|
451
465
|
tavily: "tavily";
|
|
452
466
|
}>>;
|
|
467
|
+
readonly "agents.active": z.ZodDefault<z.ZodString>;
|
|
468
|
+
readonly "agents.effort": z.ZodOptional<z.ZodString>;
|
|
453
469
|
};
|
|
454
470
|
export type SettableSettingKey = keyof typeof SETTABLE_KEYS;
|
|
455
471
|
export declare function isSettableSettingKey(key: string): key is SettableSettingKey;
|
package/dist/config.js
CHANGED
|
@@ -140,7 +140,15 @@ const configObjectSchema = z
|
|
|
140
140
|
websearch: z
|
|
141
141
|
.object({
|
|
142
142
|
provider: z
|
|
143
|
-
.enum([
|
|
143
|
+
.enum([
|
|
144
|
+
"searxng",
|
|
145
|
+
"duckduckgo-instant",
|
|
146
|
+
"duckduckgo-html",
|
|
147
|
+
"tavily",
|
|
148
|
+
"brave",
|
|
149
|
+
"serpapi",
|
|
150
|
+
"native",
|
|
151
|
+
])
|
|
144
152
|
.optional(),
|
|
145
153
|
searxngUrl: z.string().optional(),
|
|
146
154
|
apiKeyEnv: z.string().optional(),
|
|
@@ -180,6 +188,27 @@ const configObjectSchema = z
|
|
|
180
188
|
})
|
|
181
189
|
.strict()
|
|
182
190
|
.default(() => ({ paddingX: 1, skillSlashCommands: true })),
|
|
191
|
+
/**
|
|
192
|
+
* The ACTIVE agent drives the main session: its system prompt is appended to every prompt and
|
|
193
|
+
* its optional model selector is offered when the agent is chosen (`/agents`). `build` is the
|
|
194
|
+
* built-in default (full-power, no added persona); `plan` is the built-in read-only planner.
|
|
195
|
+
* `effort` holds a reasoning effort level chosen with `/effort` for a model that advertises
|
|
196
|
+
* `effort.supportedLevels`; it is validated against the ACTIVE model on use, falling back to
|
|
197
|
+
* the model's default level when unsupported.
|
|
198
|
+
*/
|
|
199
|
+
agents: z
|
|
200
|
+
.object({
|
|
201
|
+
active: z
|
|
202
|
+
.string()
|
|
203
|
+
.trim()
|
|
204
|
+
.min(1)
|
|
205
|
+
.max(100)
|
|
206
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/, "invalid agent name")
|
|
207
|
+
.default("build"),
|
|
208
|
+
effort: z.string().trim().min(1).max(50).optional(),
|
|
209
|
+
})
|
|
210
|
+
.strict()
|
|
211
|
+
.default(() => ({ active: "build" })),
|
|
183
212
|
})
|
|
184
213
|
.strict()
|
|
185
214
|
.superRefine((config, context) => {
|
|
@@ -426,6 +455,7 @@ const SETTABLE_SECTIONS = {
|
|
|
426
455
|
pluginHooks: configObjectSchema.shape.pluginHooks.removeDefault(),
|
|
427
456
|
tui: configObjectSchema.shape.tui.removeDefault(),
|
|
428
457
|
websearch: configObjectSchema.shape.websearch.removeDefault(),
|
|
458
|
+
agents: configObjectSchema.shape.agents.removeDefault(),
|
|
429
459
|
};
|
|
430
460
|
const SETTABLE_KEYS = {
|
|
431
461
|
"compaction.auto": SETTABLE_SECTIONS.compaction.shape.auto,
|
|
@@ -442,6 +472,8 @@ const SETTABLE_KEYS = {
|
|
|
442
472
|
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
443
473
|
"tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
|
|
444
474
|
"websearch.provider": SETTABLE_SECTIONS.websearch.shape.provider,
|
|
475
|
+
"agents.active": SETTABLE_SECTIONS.agents.shape.active,
|
|
476
|
+
"agents.effort": SETTABLE_SECTIONS.agents.shape.effort,
|
|
445
477
|
};
|
|
446
478
|
export function isSettableSettingKey(key) {
|
|
447
479
|
return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
|
package/dist/core/compaction.js
CHANGED
|
@@ -381,6 +381,7 @@ async function completeTextWithMeta(provider, request) {
|
|
|
381
381
|
signal: request.signal,
|
|
382
382
|
...(request.model ? { model: request.model } : {}),
|
|
383
383
|
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
384
|
+
...(request.reasoningEffort ? { reasoningEffort: request.reasoningEffort } : {}),
|
|
384
385
|
})) {
|
|
385
386
|
request.signal.throwIfAborted();
|
|
386
387
|
if (event.type === "completed") {
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface RunnerOptions {
|
|
|
42
42
|
* runner; only meaningful for a provider that documents an equivalent server-side tool.
|
|
43
43
|
*/
|
|
44
44
|
nativeTools?: Array<Record<string, unknown>>;
|
|
45
|
+
/**
|
|
46
|
+
* Default reasoning effort level (a value from the current model's `ModelInfo.effort`), sent on
|
|
47
|
+
* every run unless overridden per-run. Providers that do not advertise effort levels ignore it.
|
|
48
|
+
*/
|
|
49
|
+
reasoningEffort?: string;
|
|
45
50
|
}
|
|
46
51
|
/** The settings a running TUI can change live; merged into the runner options on apply. */
|
|
47
52
|
export interface RunnerSettingsPatch {
|
|
@@ -80,6 +85,8 @@ export interface RunOptions {
|
|
|
80
85
|
/** Per-call output token budget for this run; beats the runner-level budget when set. */
|
|
81
86
|
maxOutputTokens?: number;
|
|
82
87
|
timeoutMs?: number;
|
|
88
|
+
/** Reasoning effort level for this run; beats the runner-level default when set. */
|
|
89
|
+
reasoningEffort?: string;
|
|
83
90
|
}
|
|
84
91
|
/**
|
|
85
92
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
package/dist/core/runner.js
CHANGED
|
@@ -418,6 +418,9 @@ export class AgentRunner {
|
|
|
418
418
|
model,
|
|
419
419
|
sessionId,
|
|
420
420
|
...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
|
|
421
|
+
...((options.reasoningEffort ?? o.reasoningEffort)
|
|
422
|
+
? { reasoningEffort: options.reasoningEffort ?? o.reasoningEffort }
|
|
423
|
+
: {}),
|
|
421
424
|
})) {
|
|
422
425
|
combined.throwIfAborted();
|
|
423
426
|
if (e.type === "text_delta")
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -72,6 +72,12 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
72
72
|
askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
|
|
73
73
|
open(sessionId: string): boolean;
|
|
74
74
|
}): void;
|
|
75
|
+
/**
|
|
76
|
+
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
77
|
+
* uses this to consume plugin-published data (for example the main-capable agent definitions the
|
|
78
|
+
* built-in subagents plugin publishes) without importing the plugin package.
|
|
79
|
+
*/
|
|
80
|
+
pluginState(plugin: string, key: string): unknown;
|
|
75
81
|
get sessions(): PluginAPI["sessions"];
|
|
76
82
|
/**
|
|
77
83
|
* Live accessor for core standard tools (e.g. `ask_user_question`), which are not "a plugin"
|
package/dist/plugins/host.js
CHANGED
|
@@ -60,6 +60,14 @@ export class PluginHost {
|
|
|
60
60
|
setInteractiveUI(ui) {
|
|
61
61
|
this.uiImpl = ui;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
65
|
+
* uses this to consume plugin-published data (for example the main-capable agent definitions the
|
|
66
|
+
* built-in subagents plugin publishes) without importing the plugin package.
|
|
67
|
+
*/
|
|
68
|
+
pluginState(plugin, key) {
|
|
69
|
+
return this.state.getState(plugin, key);
|
|
70
|
+
}
|
|
63
71
|
get sessions() {
|
|
64
72
|
if (!this.sessionsImpl)
|
|
65
73
|
throw new Error("Child sessions are not available yet");
|
package/dist/tools/search.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* A throwing plugin provider falls back to the built-in chain with a diagnostic, never a hard
|
|
9
9
|
* failure of the tool call.
|
|
10
10
|
* 2. `websearch.provider` from config, when set: `"searxng"` (with a custom `searxngUrl`),
|
|
11
|
-
* `"duckduckgo-instant"`, `"tavily"`, `"brave"` or `"serpapi"` (the last
|
|
12
|
-
* env var).
|
|
11
|
+
* `"duckduckgo-instant"`, `"duckduckgo-html"`, `"tavily"`, `"brave"` or `"serpapi"` (the last
|
|
12
|
+
* three need an API key env var).
|
|
13
13
|
* 3. Nothing configured: a public SearXNG instance (see `DEFAULT_SEARXNG_URL`) — genuinely free
|
|
14
14
|
* and keyless, but empirically unreliable: while building this tool, nearly every public
|
|
15
15
|
* instance tried (searx.be and several others from https://searx.space) rate-limited or
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { SearchProvider, SearchResult } from "@alisio/sdk";
|
|
25
25
|
export interface WebsearchConfig {
|
|
26
|
-
provider?: "searxng" | "duckduckgo-instant" | "tavily" | "brave" | "serpapi" | "native";
|
|
26
|
+
provider?: "searxng" | "duckduckgo-instant" | "duckduckgo-html" | "tavily" | "brave" | "serpapi" | "native";
|
|
27
27
|
/** Overrides the default public instance; self-hosted or another public instance. */
|
|
28
28
|
searxngUrl?: string;
|
|
29
29
|
/** Environment variable holding the API key for `tavily`, `brave` or `serpapi`. */
|
|
@@ -50,6 +50,20 @@ export declare function searxngProvider(baseUrl: string): SearchProvider;
|
|
|
50
50
|
* search engine. Callers must not treat an empty result as "nothing found on the web".
|
|
51
51
|
*/
|
|
52
52
|
export declare function duckDuckGoInstantProvider(): SearchProvider;
|
|
53
|
+
/**
|
|
54
|
+
* Parses DuckDuckGo lite HTML into `SearchResult`s (exported so tests can exercise it without the
|
|
55
|
+
* network). Ignores every anchor that is not a `result-link` whose href starts with
|
|
56
|
+
* `//duckduckgo.com/l/?uddg=` — ads and related-search links never count. Snippets are taken from
|
|
57
|
+
* the `result-snippet` cell that follows each result and precedes the next one. Returns [] for a
|
|
58
|
+
* lite page that legitimately has no results.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseDuckDuckGoLiteHtml(html: string): SearchResult[];
|
|
61
|
+
/**
|
|
62
|
+
* DuckDuckGo's keyless HTML "lite" endpoint: a real general web search (unlike the Instant Answer
|
|
63
|
+
* API), scraped without an API key or cookie machinery. Works when the default public SearXNG
|
|
64
|
+
* instance is bot-blocked. See `parseDuckDuckGoLiteHtml` for the HTML parsing contract.
|
|
65
|
+
*/
|
|
66
|
+
export declare function duckDuckGoHtmlProvider(): SearchProvider;
|
|
53
67
|
export declare function tavilyProvider(apiKey: string): SearchProvider;
|
|
54
68
|
export declare function braveProvider(apiKey: string): SearchProvider;
|
|
55
69
|
export declare function serpApiProvider(apiKey: string): SearchProvider;
|
package/dist/tools/search.js
CHANGED
|
@@ -116,6 +116,150 @@ export function duckDuckGoInstantProvider() {
|
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
+
// --- DuckDuckGo lite (HTML) ---
|
|
120
|
+
// The keyless `lite.duckduckgo.com/lite/` page answers a plain GET with real, general web-search
|
|
121
|
+
// results (unlike the Instant Answer API). It is a scrape of HTML that DuckDuckGo may change at any
|
|
122
|
+
// time, so the parser below is deliberately tolerant: only `result-link` anchors whose href goes
|
|
123
|
+
// through the `//duckduckgo.com/l/?uddg=` redirector are trusted as results; ads/related links and
|
|
124
|
+
// the surrounding table/div markup are ignored. Verified live (2026-09): the full
|
|
125
|
+
// `html.duckduckgo.com/html/` endpoint returns HTTP 202 with a captcha challenge for automated
|
|
126
|
+
// requests, while `lite.duckduckgo.com/lite/` answers HTTP 200 with results and no key.
|
|
127
|
+
const DUCKDUCKGO_LITE_UA = "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0";
|
|
128
|
+
const DUCKDUCKGO_LITE_URL = "https://lite.duckduckgo.com/lite/";
|
|
129
|
+
/** Minimal local HTML-entity decoder (no dependency): the common named ones plus numeric forms. */
|
|
130
|
+
function decodeHtmlEntities(text) {
|
|
131
|
+
return text.replace(/&(#x?[0-9a-fA-F]+|amp|lt|gt|quot|apos|nbsp);/g, (raw, entity) => {
|
|
132
|
+
const lower = entity.toLowerCase();
|
|
133
|
+
if (lower === "amp")
|
|
134
|
+
return "&";
|
|
135
|
+
if (lower === "lt")
|
|
136
|
+
return "<";
|
|
137
|
+
if (lower === "gt")
|
|
138
|
+
return ">";
|
|
139
|
+
if (lower === "quot")
|
|
140
|
+
return '"';
|
|
141
|
+
if (lower === "apos")
|
|
142
|
+
return "'";
|
|
143
|
+
if (lower === "nbsp")
|
|
144
|
+
return "\u00a0";
|
|
145
|
+
try {
|
|
146
|
+
const code = lower.startsWith("#x")
|
|
147
|
+
? parseInt(lower.slice(2), 16)
|
|
148
|
+
: lower.startsWith("#")
|
|
149
|
+
? parseInt(lower.slice(1), 10)
|
|
150
|
+
: NaN;
|
|
151
|
+
return Number.isFinite(code) && code >= 0 && code <= 0x10ffff
|
|
152
|
+
? String.fromCodePoint(code)
|
|
153
|
+
: raw;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return raw;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Strips tags and collapses whitespace, keeping the text usable as a title or snippet. */
|
|
161
|
+
function htmlToText(fragment) {
|
|
162
|
+
return decodeHtmlEntities(fragment.replace(/<[^>]*>/g, ""))
|
|
163
|
+
.replace(/\s+/g, " ")
|
|
164
|
+
.trim();
|
|
165
|
+
}
|
|
166
|
+
const RESULT_LINK = /<a\b[^>]*class=['"]result-link['"][^>]*>([\s\S]*?)<\/a>/gi;
|
|
167
|
+
const HREF_ATTR = /href\s*=\s*"([^"]*)"/i;
|
|
168
|
+
const SNIPPET_CELL = /class=['"]result-snippet['"]>([\s\S]*?)<\/td>/gi;
|
|
169
|
+
const UDDG_PARAM = /uddg=([^&"]+)/i;
|
|
170
|
+
/**
|
|
171
|
+
* Parses DuckDuckGo lite HTML into `SearchResult`s (exported so tests can exercise it without the
|
|
172
|
+
* network). Ignores every anchor that is not a `result-link` whose href starts with
|
|
173
|
+
* `//duckduckgo.com/l/?uddg=` — ads and related-search links never count. Snippets are taken from
|
|
174
|
+
* the `result-snippet` cell that follows each result and precedes the next one. Returns [] for a
|
|
175
|
+
* lite page that legitimately has no results.
|
|
176
|
+
*/
|
|
177
|
+
export function parseDuckDuckGoLiteHtml(html) {
|
|
178
|
+
const anchors = [...html.matchAll(RESULT_LINK)];
|
|
179
|
+
const snippets = [...html.matchAll(SNIPPET_CELL)];
|
|
180
|
+
const results = [];
|
|
181
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
182
|
+
const anchor = anchors[i];
|
|
183
|
+
const tag = anchor[0];
|
|
184
|
+
const href = HREF_ATTR.exec(tag)?.[1] ?? "";
|
|
185
|
+
if (!href.startsWith("//duckduckgo.com/l/?uddg="))
|
|
186
|
+
continue;
|
|
187
|
+
const raw = UDDG_PARAM.exec(href)?.[1];
|
|
188
|
+
let url = "";
|
|
189
|
+
if (raw) {
|
|
190
|
+
try {
|
|
191
|
+
url = decodeURIComponent(raw);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
url = "";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!url)
|
|
198
|
+
continue;
|
|
199
|
+
const title = htmlToText(anchor[1] ?? "");
|
|
200
|
+
if (!title)
|
|
201
|
+
continue;
|
|
202
|
+
let snippet = "";
|
|
203
|
+
const limit = anchors[i + 1] ? anchors[i + 1].index : html.length;
|
|
204
|
+
for (const cell of snippets) {
|
|
205
|
+
if (cell.index !== undefined && cell.index > (anchor.index ?? 0) && cell.index < limit) {
|
|
206
|
+
snippet = htmlToText(cell[1] ?? "");
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
results.push({ title, url, snippet });
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* DuckDuckGo's keyless HTML "lite" endpoint: a real general web search (unlike the Instant Answer
|
|
216
|
+
* API), scraped without an API key or cookie machinery. Works when the default public SearXNG
|
|
217
|
+
* instance is bot-blocked. See `parseDuckDuckGoLiteHtml` for the HTML parsing contract.
|
|
218
|
+
*/
|
|
219
|
+
export function duckDuckGoHtmlProvider() {
|
|
220
|
+
return {
|
|
221
|
+
id: "duckduckgo-html",
|
|
222
|
+
async search(query, options) {
|
|
223
|
+
const url = new URL(DUCKDUCKGO_LITE_URL);
|
|
224
|
+
url.searchParams.set("q", query);
|
|
225
|
+
url.searchParams.set("kl", "us-en");
|
|
226
|
+
let res;
|
|
227
|
+
try {
|
|
228
|
+
res = await fetch(url, {
|
|
229
|
+
headers: { "User-Agent": DUCKDUCKGO_LITE_UA, Accept: "text/html" },
|
|
230
|
+
// Never follow a redirect blindly: on this endpoint a redirect is a bot-check detour,
|
|
231
|
+
// not a moved page, and following it would just land on a captcha.
|
|
232
|
+
redirect: "error",
|
|
233
|
+
signal: options?.signal,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
throw new Error(`DuckDuckGo lite search request failed: ${error instanceof Error ? error.message : String(error)}. The provider is duckduckgo-html (${DUCKDUCKGO_LITE_URL}); possible causes: a network ` +
|
|
238
|
+
"failure, a redirect (treated as a bot-check detour), or rate limiting. Try again later " +
|
|
239
|
+
"or switch websearch.provider via /settings.");
|
|
240
|
+
}
|
|
241
|
+
if (res.status !== 200) {
|
|
242
|
+
throw new Error(`DuckDuckGo lite search failed: HTTP ${res.status}. The endpoint may be showing a ` +
|
|
243
|
+
"bot-check/captcha challenge instead of results (the full html.duckduckgo.com endpoint " +
|
|
244
|
+
"returns HTTP 202 for automated requests). Try again later, or switch websearch.provider " +
|
|
245
|
+
"via /settings (e.g. searxng with your own instance).");
|
|
246
|
+
}
|
|
247
|
+
const html = await res.text();
|
|
248
|
+
if (html.length <= 200) {
|
|
249
|
+
throw new Error("DuckDuckGo lite search returned an unexpectedly empty response (no HTML to parse); the " +
|
|
250
|
+
"endpoint may be bot-blocked. Try again later or switch websearch.provider via /settings.");
|
|
251
|
+
}
|
|
252
|
+
if (!html.includes("result-link") &&
|
|
253
|
+
!html.includes("result-snippet") &&
|
|
254
|
+
!/action=['"]\/lite\/['"]/.test(html)) {
|
|
255
|
+
throw new Error("DuckDuckGo lite did not return recognizable DuckDuckGo lite HTML results; it may be " +
|
|
256
|
+
"showing a bot-check/captcha challenge instead. Try again later or switch " +
|
|
257
|
+
"websearch.provider via /settings.");
|
|
258
|
+
}
|
|
259
|
+
return parseDuckDuckGoLiteHtml(html).slice(0, 10);
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
119
263
|
export function tavilyProvider(apiKey) {
|
|
120
264
|
return {
|
|
121
265
|
id: "tavily",
|
|
@@ -197,6 +341,15 @@ function resolveBuiltin(config) {
|
|
|
197
341
|
limitation: "DuckDuckGo's Instant Answer API only answers direct factual/infobox-style queries, not " +
|
|
198
342
|
"general web search. An empty result here does not mean nothing exists on the web.",
|
|
199
343
|
};
|
|
344
|
+
if (provider === "duckduckgo-html")
|
|
345
|
+
return {
|
|
346
|
+
provider: duckDuckGoHtmlProvider(),
|
|
347
|
+
source: "duckduckgo-html",
|
|
348
|
+
limitation: "DuckDuckGo lite HTML provider: keyless and free, but it scrapes HTML DuckDuckGo may " +
|
|
349
|
+
"change at any time, and heavy automation can be bot-checked. Use it as a keyless fallback " +
|
|
350
|
+
"when the default public SearXNG instance is bot-blocked; self-hosted SearXNG remains the " +
|
|
351
|
+
"most reliable option.",
|
|
352
|
+
};
|
|
200
353
|
if (provider === "native")
|
|
201
354
|
throw new Error('websearch.provider is "native": search runs server-side through the model provider; ' +
|
|
202
355
|
"this tool should never be registered or called in that mode.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.13",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
44
|
+
"@alisio/sdk": "0.1.0-alpha.8",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|