@alisio/core 0.1.0-alpha.9 → 0.1.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/README.md +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +105 -3
- package/dist/application.js +446 -18
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +293 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +206 -6
- package/dist/config.js +426 -20
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +134 -4
- package/dist/core/runner.js +780 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +50 -5
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +34 -1
- package/dist/plugins/host.js +143 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `get_goal` and `update_goal`: the model's two handles on the session goal. Both are OPT-IN
|
|
3
|
+
* tools (`core/opt-in.ts`): a run only sees them while its session has an `active` goal and the
|
|
4
|
+
* agent is not the plan agent, so their descriptions cost no tokens anywhere else. The model can
|
|
5
|
+
* only report the goal `complete` or `blocked`, with evidence; pausing, resuming, editing,
|
|
6
|
+
* clearing and the budget belong to the user and have no tool.
|
|
7
|
+
*
|
|
8
|
+
* `effect: "internal"`: reporting changes Alisio's own bookkeeping, never the workspace.
|
|
9
|
+
*/
|
|
10
|
+
import { textResult } from "@alisio/sdk";
|
|
11
|
+
import { formatTokenCount } from "../goal/budget.js";
|
|
12
|
+
import { objectSchema } from "./standard.js";
|
|
13
|
+
export const GET_GOAL_TOOL = "get_goal";
|
|
14
|
+
export const UPDATE_GOAL_TOOL = "update_goal";
|
|
15
|
+
const KINDS = ["file", "test", "log", "command", "denied", "other"];
|
|
16
|
+
const MAX_EVIDENCE = 20;
|
|
17
|
+
const MAX_DETAIL = 1000;
|
|
18
|
+
const MAX_SUMMARY = 2000;
|
|
19
|
+
/** The model-facing state (no internals, nothing the model could use to widen anything). */
|
|
20
|
+
function brief(goal) {
|
|
21
|
+
return {
|
|
22
|
+
status: goal.status,
|
|
23
|
+
...(goal.reason ? { reason: goal.reason } : {}),
|
|
24
|
+
objective: goal.objective,
|
|
25
|
+
tokens_used: goal.tokensUsed,
|
|
26
|
+
...(goal.tokenBudget !== undefined
|
|
27
|
+
? {
|
|
28
|
+
token_budget: goal.tokenBudget,
|
|
29
|
+
tokens_left: Math.max(0, goal.tokenBudget - goal.tokensUsed),
|
|
30
|
+
}
|
|
31
|
+
: { token_budget: null }),
|
|
32
|
+
turns_used: goal.turnsUsed,
|
|
33
|
+
max_turns: goal.maxTurns,
|
|
34
|
+
minutes_used: Math.round(goal.activeMs / 60_000),
|
|
35
|
+
max_minutes: Math.round(goal.maxWallMs / 60_000),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Parses the evidence list defensively (the schema is advisory for some providers). */
|
|
39
|
+
export function parseEvidence(raw) {
|
|
40
|
+
if (!Array.isArray(raw) || !raw.length)
|
|
41
|
+
return "evidence must list at least one item.";
|
|
42
|
+
if (raw.length > MAX_EVIDENCE)
|
|
43
|
+
return `evidence has more than ${MAX_EVIDENCE} items.`;
|
|
44
|
+
const items = [];
|
|
45
|
+
for (const entry of raw) {
|
|
46
|
+
const item = entry;
|
|
47
|
+
const detail = typeof item?.detail === "string" ? item.detail.trim() : "";
|
|
48
|
+
if (!detail)
|
|
49
|
+
return "every evidence item needs a detail (a path, a test name, a log line).";
|
|
50
|
+
const kind = KINDS.find((candidate) => candidate === item.kind) ?? "other";
|
|
51
|
+
items.push({ kind, detail: detail.slice(0, MAX_DETAIL) });
|
|
52
|
+
}
|
|
53
|
+
return items;
|
|
54
|
+
}
|
|
55
|
+
export function registerGoalTools(registry, deps) {
|
|
56
|
+
const { goals } = deps;
|
|
57
|
+
const sessionOf = (session) => {
|
|
58
|
+
if (!session)
|
|
59
|
+
throw new Error("The goal tools need a session.");
|
|
60
|
+
return session;
|
|
61
|
+
};
|
|
62
|
+
registry.register({
|
|
63
|
+
name: GET_GOAL_TOOL,
|
|
64
|
+
effect: "internal",
|
|
65
|
+
description: "Read the session goal: objective, status and what is left of its budget and caps.",
|
|
66
|
+
inputSchema: objectSchema({}),
|
|
67
|
+
async execute(_input, context) {
|
|
68
|
+
const goal = goals.get(sessionOf(context.session));
|
|
69
|
+
if (!goal)
|
|
70
|
+
return textResult(JSON.stringify({ status: "none" }));
|
|
71
|
+
return textResult(JSON.stringify(brief(goal)));
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
registry.register({
|
|
75
|
+
name: UPDATE_GOAL_TOOL,
|
|
76
|
+
effect: "internal",
|
|
77
|
+
description: "Report the session goal complete or blocked. Needs evidence (files, tests, logs, commands). " +
|
|
78
|
+
"Use complete only when the objective is fully met and verified. A blocker must be reported " +
|
|
79
|
+
"again next turn before the goal stops, unless it is a permission denial (evidence kind denied). " +
|
|
80
|
+
"You cannot pause, resume, edit or clear the goal or change its budget.",
|
|
81
|
+
inputSchema: objectSchema({
|
|
82
|
+
status: { enum: ["complete", "blocked"] },
|
|
83
|
+
summary: { type: "string", minLength: 1, maxLength: MAX_SUMMARY },
|
|
84
|
+
evidence: {
|
|
85
|
+
type: "array",
|
|
86
|
+
minItems: 1,
|
|
87
|
+
maxItems: MAX_EVIDENCE,
|
|
88
|
+
items: objectSchema({ kind: { enum: [...KINDS] }, detail: { type: "string", maxLength: MAX_DETAIL } }, ["kind", "detail"]),
|
|
89
|
+
},
|
|
90
|
+
}, ["status", "summary", "evidence"]),
|
|
91
|
+
async execute(input, context) {
|
|
92
|
+
const sessionId = sessionOf(context.session);
|
|
93
|
+
const status = input.status === "complete" || input.status === "blocked" ? input.status : undefined;
|
|
94
|
+
if (!status)
|
|
95
|
+
return textResult(JSON.stringify({ ok: false, error: "status must be complete or blocked." }));
|
|
96
|
+
const summary = typeof input.summary === "string" ? input.summary.trim().slice(0, MAX_SUMMARY) : "";
|
|
97
|
+
if (!summary)
|
|
98
|
+
return textResult(JSON.stringify({ ok: false, error: "summary is required." }));
|
|
99
|
+
const evidence = parseEvidence(input.evidence);
|
|
100
|
+
if (typeof evidence === "string")
|
|
101
|
+
return textResult(JSON.stringify({ ok: false, error: evidence }));
|
|
102
|
+
const result = goals.report(sessionId, context.runId, { status, summary, evidence });
|
|
103
|
+
if (!result.ok)
|
|
104
|
+
return textResult(JSON.stringify({ ok: false, error: result.message }));
|
|
105
|
+
if (status === "complete")
|
|
106
|
+
return textResult(JSON.stringify({
|
|
107
|
+
ok: true,
|
|
108
|
+
status: "complete",
|
|
109
|
+
note: "The goal is complete. Give the user a short final summary.",
|
|
110
|
+
}));
|
|
111
|
+
return textResult(JSON.stringify(result.stopped
|
|
112
|
+
? {
|
|
113
|
+
ok: true,
|
|
114
|
+
status: "blocked",
|
|
115
|
+
note: "The goal is blocked and will not continue until the user resumes it. Tell the user what you need.",
|
|
116
|
+
}
|
|
117
|
+
: {
|
|
118
|
+
ok: true,
|
|
119
|
+
status: "active",
|
|
120
|
+
note: `Blocker recorded. If it still stands next turn, report it again with fresh evidence (${result.needed ?? 2} consecutive turns in total); otherwise keep working. Budget left: ${result.goal.tokenBudget !== undefined
|
|
121
|
+
? formatTokenCount(Math.max(0, result.goal.tokenBudget - result.goal.tokensUsed))
|
|
122
|
+
: "no token budget"}.`,
|
|
123
|
+
}));
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
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/tools/standard.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Policy } from "../core/contracts.ts";
|
|
|
3
3
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
4
4
|
import type { ProjectContext } from "../resources/context.ts";
|
|
5
5
|
import type { Skills } from "../resources/skills.ts";
|
|
6
|
+
import type { PathAccess } from "../runtime/access.ts";
|
|
6
7
|
import { type WebsearchConfig } from "./search.ts";
|
|
7
8
|
export declare const objectSchema: (properties: Record<string, unknown>, required?: string[]) => JsonSchema;
|
|
8
9
|
export declare const hash: (text: string) => string;
|
|
@@ -19,4 +20,6 @@ execCtx?: {
|
|
|
19
20
|
provider: SearchProvider;
|
|
20
21
|
plugin: string;
|
|
21
22
|
} | undefined;
|
|
22
|
-
}
|
|
23
|
+
},
|
|
24
|
+
/** Mediated path policy (workspace plus declared extra directories and external approval). */
|
|
25
|
+
pathAccess?: PathAccess): void;
|
package/dist/tools/standard.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, relative } from "node:path";
|
|
3
|
+
import { dirname, extname, relative } from "node:path";
|
|
4
4
|
import { textResult, } from "@alisio/sdk";
|
|
5
|
+
import { clipLines, unifiedPatch } from "../runtime/diff.js";
|
|
5
6
|
import { fileSize, readHead, readText } from "../runtime/fs.js";
|
|
6
7
|
import { safePath } from "../runtime/paths.js";
|
|
7
|
-
import { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess } from "../runtime/process.js";
|
|
8
|
+
import { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "../runtime/process.js";
|
|
8
9
|
import { EXECUTE_TIMEOUT_MS, MAX_NESTED_CALLS, runExecute } from "./execute.js";
|
|
9
10
|
import { searchWithFallback } from "./search.js";
|
|
10
11
|
import { DEFAULT_TIMEOUT_SECONDS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_SECONDS, webfetch, } from "./webfetch.js";
|
|
@@ -30,8 +31,8 @@ async function existing(path) {
|
|
|
30
31
|
throw new Error("Editable files are limited to 1 MiB");
|
|
31
32
|
return readText(path);
|
|
32
33
|
}
|
|
33
|
-
async function atomicWrite(
|
|
34
|
-
path = await
|
|
34
|
+
async function atomicWrite(resolve, path, content, expected) {
|
|
35
|
+
path = await resolve(path);
|
|
35
36
|
await mkdir(dirname(path), { recursive: true });
|
|
36
37
|
const before = await existing(path);
|
|
37
38
|
if (before === undefined ? expected !== null : hash(before) !== expected)
|
|
@@ -40,7 +41,7 @@ async function atomicWrite(root, path, content, expected) {
|
|
|
40
41
|
try {
|
|
41
42
|
const mode = before === undefined ? 0o644 : (await stat(path)).mode & 0o777;
|
|
42
43
|
await writeFile(temporary, content, { mode });
|
|
43
|
-
await
|
|
44
|
+
await resolve(path);
|
|
44
45
|
const now = await existing(path);
|
|
45
46
|
if (now !== before)
|
|
46
47
|
throw new Error("File changed while preparing write");
|
|
@@ -49,11 +50,71 @@ async function atomicWrite(root, path, content, expected) {
|
|
|
49
50
|
finally {
|
|
50
51
|
await rm(temporary, { force: true });
|
|
51
52
|
}
|
|
53
|
+
return before;
|
|
54
|
+
}
|
|
55
|
+
/** Producer-side bounds of the web-only ui parts (spec §8.6). */
|
|
56
|
+
const DIFF_LIMIT = 200 * 1024;
|
|
57
|
+
const TERMINAL_LIMIT = 256 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Appends a display-only ui part after the tool's text (which stays the first part and is all
|
|
60
|
+
* that providers and headless output see, via `textProjection`).
|
|
61
|
+
*/
|
|
62
|
+
const withUi = (result, block) => block ? { ...result, content: [...result.content, { type: "ui", block }] } : result;
|
|
63
|
+
/** A `diff` block of a file write, bounded to 200 KB at a line boundary. */
|
|
64
|
+
function diffBlock(path, before, after) {
|
|
65
|
+
const patch = unifiedPatch(path, before, after);
|
|
66
|
+
if (!patch)
|
|
67
|
+
return undefined;
|
|
68
|
+
const { text, clipped } = clipLines(patch, DIFF_LIMIT);
|
|
69
|
+
const lang = extname(path).slice(1).toLowerCase();
|
|
70
|
+
return {
|
|
71
|
+
kind: "diff",
|
|
72
|
+
path,
|
|
73
|
+
patch: text,
|
|
74
|
+
...(lang ? { lang } : {}),
|
|
75
|
+
...(clipped ? { caption: `${path} (diff truncated to 200 KB)` } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A `terminal` block of a finished process: stdout then stderr, keeping the last 256 KB. */
|
|
79
|
+
function terminalBlock(command, result, durationMs) {
|
|
80
|
+
const separator = result.stdout && result.stderr && !result.stdout.endsWith("\n") ? "\n" : "";
|
|
81
|
+
let output = `${result.stdout}${separator}${result.stderr}`;
|
|
82
|
+
const bytes = Buffer.byteLength(output);
|
|
83
|
+
const clipped = bytes > TERMINAL_LIMIT;
|
|
84
|
+
if (clipped)
|
|
85
|
+
output = Buffer.from(output)
|
|
86
|
+
.subarray(bytes - TERMINAL_LIMIT)
|
|
87
|
+
.toString("utf8")
|
|
88
|
+
.replace(/^\uFFFD+/, "");
|
|
89
|
+
return {
|
|
90
|
+
kind: "terminal",
|
|
91
|
+
command,
|
|
92
|
+
output,
|
|
93
|
+
exitCode: result.exitCode,
|
|
94
|
+
durationMs,
|
|
95
|
+
...(result.truncated || clipped ? { truncated: true } : {}),
|
|
96
|
+
};
|
|
52
97
|
}
|
|
53
98
|
export function registerStandard(registry, workspace, skills, context, ui,
|
|
54
99
|
/** Live policy accessor for `execute`'s nested tool calls (bound once the runner exists). */
|
|
55
|
-
execCtx, websearchCtx
|
|
100
|
+
execCtx, websearchCtx,
|
|
101
|
+
/** Mediated path policy (workspace plus declared extra directories and external approval). */
|
|
102
|
+
pathAccess) {
|
|
56
103
|
const register = (tool) => registry.register(tool);
|
|
104
|
+
/**
|
|
105
|
+
* Resolves one tool path through the host-injected policy when present, then the captured
|
|
106
|
+
* `PathAccess`, and finally the workspace-only `safePath` (standalone fixtures/embedders).
|
|
107
|
+
*/
|
|
108
|
+
const resolvePath = (context, path) => context.resolvePath
|
|
109
|
+
? context.resolvePath(path)
|
|
110
|
+
: pathAccess
|
|
111
|
+
? pathAccess.resolve(path, {
|
|
112
|
+
workspace: context.workspace,
|
|
113
|
+
...(context.session ? { session: context.session } : {}),
|
|
114
|
+
...(context.label ? { label: context.label } : {}),
|
|
115
|
+
signal: context.signal,
|
|
116
|
+
})
|
|
117
|
+
: safePath(context.workspace, path);
|
|
57
118
|
const filePaths = (input) => typeof input.path === "string" ? [input.path] : [];
|
|
58
119
|
register({
|
|
59
120
|
name: "read_file",
|
|
@@ -66,7 +127,7 @@ execCtx, websearchCtx) {
|
|
|
66
127
|
}, ["path"]),
|
|
67
128
|
paths: filePaths,
|
|
68
129
|
async execute(i, c) {
|
|
69
|
-
const path = await
|
|
130
|
+
const path = await resolvePath(c, String(i.path));
|
|
70
131
|
const size = await fileSize(path);
|
|
71
132
|
if (size === undefined)
|
|
72
133
|
throw new Error(`File not found: ${String(i.path)}`);
|
|
@@ -97,8 +158,8 @@ execCtx, websearchCtx) {
|
|
|
97
158
|
const content = String(i.content);
|
|
98
159
|
if (Buffer.byteLength(content) > 1_048_576)
|
|
99
160
|
throw new Error("Write exceeds 1 MiB");
|
|
100
|
-
await atomicWrite(c
|
|
101
|
-
return textResult(JSON.stringify({ path: i.path, sha256: hash(content) }));
|
|
161
|
+
const before = await atomicWrite((path) => resolvePath(c, path), String(i.path), content, i.expectedHash);
|
|
162
|
+
return withUi(textResult(JSON.stringify({ path: i.path, sha256: hash(content) })), diffBlock(String(i.path), before, content));
|
|
102
163
|
},
|
|
103
164
|
});
|
|
104
165
|
register({
|
|
@@ -108,15 +169,15 @@ execCtx, websearchCtx) {
|
|
|
108
169
|
inputSchema: objectSchema({ path: str, oldText: { type: "string", minLength: 1 }, newText: str, expectedHash: str }, ["path", "oldText", "newText", "expectedHash"]),
|
|
109
170
|
paths: filePaths,
|
|
110
171
|
async execute(i, c) {
|
|
111
|
-
const path = await
|
|
172
|
+
const path = await resolvePath(c, String(i.path)), before = await existing(path);
|
|
112
173
|
if (before === undefined)
|
|
113
174
|
throw new Error("File not found");
|
|
114
175
|
const needle = String(i.oldText), index = before.indexOf(needle);
|
|
115
176
|
if (index < 0 || before.indexOf(needle, index + 1) >= 0)
|
|
116
177
|
throw new Error("Edit must match exactly one occurrence");
|
|
117
178
|
const after = before.slice(0, index) + String(i.newText) + before.slice(index + needle.length);
|
|
118
|
-
await atomicWrite(c
|
|
119
|
-
return textResult(JSON.stringify({ path: i.path, sha256: hash(after) }));
|
|
179
|
+
await atomicWrite((p) => resolvePath(c, p), path, after, String(i.expectedHash));
|
|
180
|
+
return withUi(textResult(JSON.stringify({ path: i.path, sha256: hash(after) })), diffBlock(String(i.path), before, after));
|
|
120
181
|
},
|
|
121
182
|
});
|
|
122
183
|
register({
|
|
@@ -130,7 +191,7 @@ execCtx, websearchCtx) {
|
|
|
130
191
|
}),
|
|
131
192
|
paths: filePaths,
|
|
132
193
|
async execute(i, c) {
|
|
133
|
-
const path = await
|
|
194
|
+
const path = await resolvePath(c, String(i.path ?? "."));
|
|
134
195
|
const result = await runRipgrep(["--files", "--glob", "!.git", "--glob", "!node_modules", "--", path], { cwd: c.workspace, signal: c.signal, maxBytes: 200_000 });
|
|
135
196
|
if (result.exitCode > 1 && !result.truncated)
|
|
136
197
|
throw new Error(result.stderr);
|
|
@@ -156,7 +217,7 @@ execCtx, websearchCtx) {
|
|
|
156
217
|
]),
|
|
157
218
|
paths: filePaths,
|
|
158
219
|
async execute(i, c) {
|
|
159
|
-
const path = await
|
|
220
|
+
const path = await resolvePath(c, String(i.path ?? "."));
|
|
160
221
|
const r = await runRipgrep([
|
|
161
222
|
"--json",
|
|
162
223
|
"--max-count",
|
|
@@ -185,12 +246,15 @@ execCtx, websearchCtx) {
|
|
|
185
246
|
timeoutMs: { type: "integer", minimum: 1, maximum: 120000 },
|
|
186
247
|
}, ["command", "args"]),
|
|
187
248
|
async execute(i, c) {
|
|
188
|
-
|
|
249
|
+
const started = Date.now();
|
|
250
|
+
const result = await runProcess(String(i.command), i.args, {
|
|
189
251
|
cwd: c.workspace,
|
|
190
252
|
signal: c.signal,
|
|
191
253
|
timeoutMs: Number(i.timeoutMs ?? 30000),
|
|
192
254
|
onData: c.emit,
|
|
193
|
-
})
|
|
255
|
+
});
|
|
256
|
+
const command = [String(i.command), ...i.args].join(" ");
|
|
257
|
+
return withUi(textResult(JSON.stringify(result)), terminalBlock(command, result, Date.now() - started));
|
|
194
258
|
},
|
|
195
259
|
});
|
|
196
260
|
register({
|
|
@@ -199,9 +263,11 @@ execCtx, websearchCtx) {
|
|
|
199
263
|
description: "Run an arbitrary shell command. Explicit process capability required; not sandboxed.",
|
|
200
264
|
inputSchema: objectSchema({ command: str }, ["command"]),
|
|
201
265
|
async execute(i, c) {
|
|
202
|
-
|
|
266
|
+
const started = Date.now();
|
|
267
|
+
const result = await runProcess(process.platform === "win32" ? "cmd.exe" : "/bin/sh", process.platform === "win32"
|
|
203
268
|
? ["/d", "/s", "/c", String(i.command)]
|
|
204
|
-
: ["-c", String(i.command)], { cwd: c.workspace, signal: c.signal, onData: c.emit })
|
|
269
|
+
: ["-c", String(i.command)], { cwd: c.workspace, signal: c.signal, onData: c.emit });
|
|
270
|
+
return withUi(textResult(JSON.stringify(result)), terminalBlock(String(i.command), result, Date.now() - started));
|
|
205
271
|
},
|
|
206
272
|
});
|
|
207
273
|
for (const [name, args] of [
|
|
@@ -412,6 +478,21 @@ execCtx, websearchCtx) {
|
|
|
412
478
|
emit: c.emit,
|
|
413
479
|
...(c.session ? { session: c.session } : {}),
|
|
414
480
|
...(c.label ? { label: c.label } : {}),
|
|
481
|
+
// Nested calls must never open a new external-directory prompt; only directories already
|
|
482
|
+
// approved for this session (or inside the workspace) are reachable.
|
|
483
|
+
...(pathAccess
|
|
484
|
+
? {
|
|
485
|
+
resolvePath: (path) => pathAccess.resolve(path, {
|
|
486
|
+
workspace: c.workspace,
|
|
487
|
+
...(c.session ? { session: c.session } : {}),
|
|
488
|
+
...(c.label ? { label: c.label } : {}),
|
|
489
|
+
signal: c.signal,
|
|
490
|
+
interactive: false,
|
|
491
|
+
}),
|
|
492
|
+
}
|
|
493
|
+
: c.resolvePath
|
|
494
|
+
? { resolvePath: c.resolvePath }
|
|
495
|
+
: {}),
|
|
415
496
|
});
|
|
416
497
|
return textResult(JSON.stringify({ result: result ?? null }));
|
|
417
498
|
},
|
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
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
|
44
|
+
"@alisio/sdk": "0.1.0",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|