@alisio/core 0.1.0-alpha.10 → 0.1.0-alpha.12
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/README.md +52 -7
- package/dist/config.d.ts +4 -2
- package/dist/config.js +11 -3
- package/dist/core/runner.js +8 -2
- package/dist/sessions/children.js +11 -1
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# @alisio/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
**The embeddable core of [Alisio](https://github.com/GustavoGutierrez/alisio): a provider-agnostic
|
|
4
|
+
agent you can drive from your own program.** No TUI, no CLI — just the tool loop, compaction, MCP
|
|
5
|
+
and plugin host as a library.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
`@alisio/core` wires the agent tool loop, structured context compaction, the OpenAI-compatible
|
|
10
|
+
provider (Chat Completions and Responses), local tools behind explicit permissions, the MCP client,
|
|
11
|
+
the plugin host (hook timeouts and failure isolation) and a SQLite session store into one
|
|
12
|
+
`createApplication` entry point. It runs on Node.js >= 22.16 (`node:sqlite` with FTS5) and on Bun,
|
|
13
|
+
and uses only portable Node APIs.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm i @alisio/core # or: pnpm add @alisio/core · bun add @alisio/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
7
22
|
|
|
8
23
|
```ts
|
|
9
24
|
import { createApplication } from "@alisio/core";
|
|
@@ -18,8 +33,38 @@ try {
|
|
|
18
33
|
}
|
|
19
34
|
```
|
|
20
35
|
|
|
36
|
+
## What you get
|
|
37
|
+
|
|
38
|
+
- **Runner**: `AgentRunner` with turn limits, timeouts, token budgets and versioned `RunEvent`
|
|
39
|
+
output (`schemaVersion: 1`).
|
|
40
|
+
- **Context compaction**: `planCompaction`, `summarize`, `renderCheckpoint`,
|
|
41
|
+
`shouldCompactContext` and checkpoint helpers; memory plugins hook in via `CompactionHooks`.
|
|
42
|
+
- **Providers**: `ProviderRegistry` / `ActiveProvider` for OpenAI-compatible, DeepSeek, OpenCode
|
|
43
|
+
Console and OpenCode Go; `ProviderSettingsStore` for non-secret profiles plus a separate
|
|
44
|
+
credentials store.
|
|
45
|
+
- **Local tools**: reads by default, `--allow-write`/`--allow-process`-style policy, path checks
|
|
46
|
+
and standard tools (`ToolRegistry`, `registerStandard`).
|
|
47
|
+
- **MCP**: `McpConnector`, server discovery and tool projection.
|
|
48
|
+
- **Plugin host**: `PluginHost` with built-in plugin catalogs, npm install (`installPlugin`,
|
|
49
|
+
`cliInstall`), hook timeouts and failure isolation.
|
|
50
|
+
- **Sessions**: `SQLiteStore` with reconciliation and crash recovery.
|
|
51
|
+
- **Skills and prompts**: `Skills`, `ProjectContext` (`AGENTS.md`), `loadPromptTemplates`.
|
|
52
|
+
- **Trust**: `resolveTrust` / `setTrust` / `listTrust` for per-directory project trust.
|
|
53
|
+
- **Startup screen**: reusable `renderStartup` sections and the default mascot.
|
|
54
|
+
|
|
21
55
|
Built-in plugins are opt-in for embedders: pass `builtins` (the `alisio` CLI wires
|
|
22
|
-
`@alisio/plugin-memory`
|
|
23
|
-
|
|
56
|
+
`@alisio/plugin-memory` and `@alisio/plugin-subagents`).
|
|
57
|
+
|
|
58
|
+
## Docs
|
|
59
|
+
|
|
60
|
+
Architecture: <https://gustavogutierrez.github.io/alisio/architecture> ·
|
|
61
|
+
Configuration: <https://gustavogutierrez.github.io/alisio/configuration>.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
Node.js **>= 22.16** or Bun **>= 1.4.2**.
|
|
66
|
+
|
|
67
|
+
## License
|
|
24
68
|
|
|
25
|
-
|
|
69
|
+
MIT. Maintained by Gustavo Gutiérrez Mercado. Source: <https://github.com/GustavoGutierrez/alisio> ·
|
|
70
|
+
npm: <https://www.npmjs.com/settings/alisio/packages>.
|
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";
|
|
@@ -215,7 +216,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
215
216
|
maxOutputTokens: number;
|
|
216
217
|
};
|
|
217
218
|
websearch: {
|
|
218
|
-
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
219
|
+
provider?: "brave" | "duckduckgo-html" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
219
220
|
searxngUrl?: string | undefined;
|
|
220
221
|
apiKeyEnv?: string | undefined;
|
|
221
222
|
nativeToolType: string;
|
|
@@ -353,7 +354,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
353
354
|
maxOutputTokens: number;
|
|
354
355
|
};
|
|
355
356
|
websearch: {
|
|
356
|
-
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
357
|
+
provider?: "brave" | "duckduckgo-html" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
357
358
|
searxngUrl?: string | undefined;
|
|
358
359
|
apiKeyEnv?: string | undefined;
|
|
359
360
|
nativeToolType: string;
|
|
@@ -444,6 +445,7 @@ declare const SETTABLE_KEYS: {
|
|
|
444
445
|
readonly "tui.skillSlashCommands": z.ZodDefault<z.ZodBoolean>;
|
|
445
446
|
readonly "websearch.provider": z.ZodOptional<z.ZodEnum<{
|
|
446
447
|
brave: "brave";
|
|
448
|
+
"duckduckgo-html": "duckduckgo-html";
|
|
447
449
|
"duckduckgo-instant": "duckduckgo-instant";
|
|
448
450
|
native: "native";
|
|
449
451
|
searxng: "searxng";
|
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(),
|
|
@@ -151,7 +159,7 @@ const configObjectSchema = z
|
|
|
151
159
|
.default(() => ({ nativeToolType: "web_search" })),
|
|
152
160
|
limits: z
|
|
153
161
|
.object({
|
|
154
|
-
maxTurns: z.number().int().min(1).max(100).default(
|
|
162
|
+
maxTurns: z.number().int().min(1).max(100).default(100),
|
|
155
163
|
timeoutMs: z.number().int().min(100).default(300000),
|
|
156
164
|
maxContextChars: z.number().int().positive().default(800000),
|
|
157
165
|
/**
|
|
@@ -163,7 +171,7 @@ const configObjectSchema = z
|
|
|
163
171
|
maxTokens: z.number().int().positive().optional(),
|
|
164
172
|
})
|
|
165
173
|
.default(() => ({
|
|
166
|
-
maxTurns:
|
|
174
|
+
maxTurns: 100,
|
|
167
175
|
timeoutMs: 300000,
|
|
168
176
|
maxContextChars: 800000,
|
|
169
177
|
maxOutputTokens: 16384,
|
package/dist/core/runner.js
CHANGED
|
@@ -348,7 +348,8 @@ export class AgentRunner {
|
|
|
348
348
|
emit("run_started", { model });
|
|
349
349
|
const budget = options.maxTokens ?? o.maxTokens ?? defaultTokenBudget(o.contextWindow?.(model));
|
|
350
350
|
const compaction = o.compaction ?? {};
|
|
351
|
-
|
|
351
|
+
const maxTurns = options.maxTurns ?? o.maxTurns ?? 100;
|
|
352
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
352
353
|
combined.throwIfAborted();
|
|
353
354
|
// Input queued while the run was working (e.g. a parent's message to a child).
|
|
354
355
|
if (turn > 0)
|
|
@@ -606,7 +607,12 @@ export class AgentRunner {
|
|
|
606
607
|
if (tokens >= budget)
|
|
607
608
|
throw new Error("Token budget exhausted");
|
|
608
609
|
}
|
|
609
|
-
|
|
610
|
+
// Turn cap reached. This is NOT a failure: everything produced up to this point stays in
|
|
611
|
+
// the transcript and the run returns a successful-but-marked partial result, so the user
|
|
612
|
+
// can simply prompt again to continue in the same session. The real hard stops remain the
|
|
613
|
+
// token budget (`maxTokens`) and the timeout; the turn count is a safety rail.
|
|
614
|
+
emit("run_turns_exceeded", { turns: maxTurns, maxTurns });
|
|
615
|
+
return { sessionId, text: lastText, status: "turns-exceeded", usage: usageTotal };
|
|
610
616
|
}
|
|
611
617
|
catch (error) {
|
|
612
618
|
// On cancellation report the abort reason, not the transport's secondary error.
|
|
@@ -160,8 +160,18 @@ export class ChildSessions {
|
|
|
160
160
|
});
|
|
161
161
|
usage.input += result.usage.input;
|
|
162
162
|
usage.output += result.usage.output;
|
|
163
|
+
// A child that hit its turn limit still produced a usable partial result: it completes
|
|
164
|
+
// with a turnsExceeded marker instead of failing, so orchestration treats the text as
|
|
165
|
+
// partial output. state "failed" stays reserved for real errors/aborts.
|
|
166
|
+
const turnsExceeded = result.status === "turns-exceeded";
|
|
163
167
|
this.o.store.updateSession(id, { status: "completed", usage });
|
|
164
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
id,
|
|
170
|
+
status: "completed",
|
|
171
|
+
text: result.text,
|
|
172
|
+
usage,
|
|
173
|
+
...(turnsExceeded ? { turnsExceeded: true } : {}),
|
|
174
|
+
};
|
|
165
175
|
}
|
|
166
176
|
catch (error) {
|
|
167
177
|
const aborted = this.cancelled.has(id) || signals.some((s) => s.aborted);
|
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/dist/version.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reads the @alisio/core package version at runtime so the MCP client metadata stays in sync
|
|
3
|
-
* with the published package.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* with the published package. The manifest is resolved by walking UP from the module directory
|
|
4
|
+
* until a package.json with a string `version` is found (bounded, any depth: module, then up to
|
|
5
|
+
* 8 parents), so a deeply nested call site like `src/mcp/connector.ts` resolves the same
|
|
6
|
+
* manifest as `src/version.ts`. Standalone binaries inject ALISIO_PACKAGE_VERSION at build time
|
|
7
|
+
* (scripts/binary-build.ts).
|
|
6
8
|
*/
|
|
7
9
|
export declare function loadVersion(fromHere: string): string;
|
package/dist/version.js
CHANGED
|
@@ -6,19 +6,48 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
* A development marker, never a publish literal: it cannot silently desync from the package.
|
|
7
7
|
*/
|
|
8
8
|
const FALLBACK = "dev";
|
|
9
|
+
/**
|
|
10
|
+
* How many parent directories the loader walks up before giving up. Bounded so a missing
|
|
11
|
+
* manifest can never make the lookup climb an unbounded path.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_PARENT_HOPS = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Walks up from `dir` looking for a package manifest. Returns the version of the nearest
|
|
16
|
+
* manifest that declares a string `version`. `undefined` when no manifest with a version is
|
|
17
|
+
* reached within the bound.
|
|
18
|
+
*/
|
|
19
|
+
function findManifestVersion(dir) {
|
|
20
|
+
let nearest;
|
|
21
|
+
let current = dir;
|
|
22
|
+
for (let hop = 0; hop <= MAX_PARENT_HOPS; hop++) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
|
|
25
|
+
if (typeof parsed.version === "string" && parsed.version)
|
|
26
|
+
nearest ??= parsed.version;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// No manifest here; keep walking up.
|
|
30
|
+
}
|
|
31
|
+
const parent = dirname(current);
|
|
32
|
+
if (parent === current)
|
|
33
|
+
break; // Filesystem root.
|
|
34
|
+
current = parent;
|
|
35
|
+
}
|
|
36
|
+
return nearest;
|
|
37
|
+
}
|
|
9
38
|
/**
|
|
10
39
|
* Reads the @alisio/core package version at runtime so the MCP client metadata stays in sync
|
|
11
|
-
* with the published package.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
40
|
+
* with the published package. The manifest is resolved by walking UP from the module directory
|
|
41
|
+
* until a package.json with a string `version` is found (bounded, any depth: module, then up to
|
|
42
|
+
* 8 parents), so a deeply nested call site like `src/mcp/connector.ts` resolves the same
|
|
43
|
+
* manifest as `src/version.ts`. Standalone binaries inject ALISIO_PACKAGE_VERSION at build time
|
|
44
|
+
* (scripts/binary-build.ts).
|
|
14
45
|
*/
|
|
15
46
|
export function loadVersion(fromHere) {
|
|
16
47
|
if (process.env.ALISIO_PACKAGE_VERSION)
|
|
17
48
|
return process.env.ALISIO_PACKAGE_VERSION;
|
|
18
49
|
try {
|
|
19
|
-
|
|
20
|
-
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
21
|
-
return typeof parsed.version === "string" && parsed.version ? parsed.version : FALLBACK;
|
|
50
|
+
return findManifestVersion(dirname(fileURLToPath(fromHere))) ?? FALLBACK;
|
|
22
51
|
}
|
|
23
52
|
catch {
|
|
24
53
|
return FALLBACK;
|
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.12",
|
|
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.7",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|