@alisio/core 0.1.0-alpha.9 → 0.1.1

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.
Files changed (187) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +105 -3
  65. package/dist/application.js +446 -18
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +306 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +206 -6
  95. package/dist/config.js +426 -20
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +134 -4
  105. package/dist/core/runner.js +780 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +50 -5
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +34 -1
  142. package/dist/plugins/host.js +143 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.js +6 -1
  148. package/dist/runtime/access.d.ts +62 -0
  149. package/dist/runtime/access.js +115 -0
  150. package/dist/runtime/blobs.d.ts +51 -0
  151. package/dist/runtime/blobs.js +177 -0
  152. package/dist/runtime/diff.d.ts +23 -0
  153. package/dist/runtime/diff.js +154 -0
  154. package/dist/runtime/ids.d.ts +8 -0
  155. package/dist/runtime/ids.js +32 -0
  156. package/dist/runtime/paths.d.ts +11 -0
  157. package/dist/runtime/paths.js +17 -1
  158. package/dist/runtime/process.d.ts +22 -0
  159. package/dist/runtime/process.js +100 -15
  160. package/dist/runtime/sqlite.d.ts +2 -0
  161. package/dist/runtime/sqlite.js +18 -0
  162. package/dist/runtime/store.d.ts +91 -3
  163. package/dist/runtime/store.js +465 -11
  164. package/dist/runtime/zip.d.ts +24 -0
  165. package/dist/runtime/zip.js +126 -0
  166. package/dist/sessions/children.js +17 -1
  167. package/dist/sessions/side-questions.d.ts +57 -0
  168. package/dist/sessions/side-questions.js +161 -0
  169. package/dist/tools/analysis.d.ts +37 -0
  170. package/dist/tools/analysis.js +573 -0
  171. package/dist/tools/artifacts.d.ts +14 -0
  172. package/dist/tools/artifacts.js +143 -0
  173. package/dist/tools/background.d.ts +7 -0
  174. package/dist/tools/background.js +165 -0
  175. package/dist/tools/data.d.ts +7 -0
  176. package/dist/tools/data.js +160 -0
  177. package/dist/tools/execute.d.ts +5 -0
  178. package/dist/tools/execute.js +15 -3
  179. package/dist/tools/goal.d.ts +19 -0
  180. package/dist/tools/goal.js +126 -0
  181. package/dist/tools/search.d.ts +17 -3
  182. package/dist/tools/search.js +153 -0
  183. package/dist/tools/standard.d.ts +4 -1
  184. package/dist/tools/standard.js +99 -18
  185. package/dist/version.d.ts +5 -3
  186. package/dist/version.js +35 -6
  187. 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
+ }
@@ -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 three need an API key
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;
@@ -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.");
@@ -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
- }): void;
23
+ },
24
+ /** Mediated path policy (workspace plus declared extra directories and external approval). */
25
+ pathAccess?: PathAccess): void;
@@ -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(root, path, content, expected) {
34
- path = await safePath(root, path);
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 safePath(root, path);
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 safePath(c.workspace, String(i.path));
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.workspace, String(i.path), content, i.expectedHash);
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 safePath(c.workspace, String(i.path)), before = await existing(path);
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.workspace, path, after, String(i.expectedHash));
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 safePath(c.workspace, String(i.path ?? "."));
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 safePath(c.workspace, String(i.path ?? "."));
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
- return textResult(JSON.stringify(await runProcess(String(i.command), i.args, {
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
- return textResult(JSON.stringify(await runProcess(process.platform === "win32" ? "cmd.exe" : "/bin/sh", process.platform === "win32"
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. Resolves the manifest relative to the module (`src/version.ts` →
4
- * `../package.json` in the source tree; `dist/version.js` → `../package.json` when installed).
5
- * Standalone binaries inject ALISIO_PACKAGE_VERSION at build time (scripts/binary-build.ts).
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. Resolves the manifest relative to the module (`src/version.ts` →
12
- * `../package.json` in the source tree; `dist/version.js` → `../package.json` when installed).
13
- * Standalone binaries inject ALISIO_PACKAGE_VERSION at build time (scripts/binary-build.ts).
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
- const manifest = join(dirname(fileURLToPath(fromHere)), "..", "package.json");
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.9",
3
+ "version": "0.1.1",
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.5",
44
+ "@alisio/sdk": "0.1.1",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",