iosm-cli 0.2.7 → 0.2.9

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 (149) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +4 -4
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +12 -4
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-profiles.d.ts.map +1 -1
  7. package/dist/core/agent-profiles.js +15 -2
  8. package/dist/core/agent-profiles.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +3 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +214 -2
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/sdk.d.ts +2 -2
  14. package/dist/core/sdk.d.ts.map +1 -1
  15. package/dist/core/sdk.js +7 -4
  16. package/dist/core/sdk.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +57 -0
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +197 -0
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/shadow-guard.d.ts.map +1 -1
  22. package/dist/core/shadow-guard.js +12 -1
  23. package/dist/core/shadow-guard.js.map +1 -1
  24. package/dist/core/system-prompt.d.ts.map +1 -1
  25. package/dist/core/system-prompt.js +109 -4
  26. package/dist/core/system-prompt.js.map +1 -1
  27. package/dist/core/tools/db-run.d.ts +84 -0
  28. package/dist/core/tools/db-run.d.ts.map +1 -0
  29. package/dist/core/tools/db-run.js +690 -0
  30. package/dist/core/tools/db-run.js.map +1 -0
  31. package/dist/core/tools/git-common.d.ts +45 -0
  32. package/dist/core/tools/git-common.d.ts.map +1 -0
  33. package/dist/core/tools/git-common.js +185 -0
  34. package/dist/core/tools/git-common.js.map +1 -0
  35. package/dist/core/tools/git-read.d.ts +15 -13
  36. package/dist/core/tools/git-read.d.ts.map +1 -1
  37. package/dist/core/tools/git-read.js +101 -153
  38. package/dist/core/tools/git-read.js.map +1 -1
  39. package/dist/core/tools/git-write.d.ts +75 -0
  40. package/dist/core/tools/git-write.d.ts.map +1 -0
  41. package/dist/core/tools/git-write.js +298 -0
  42. package/dist/core/tools/git-write.js.map +1 -0
  43. package/dist/core/tools/index.d.ts +91 -1
  44. package/dist/core/tools/index.d.ts.map +1 -1
  45. package/dist/core/tools/index.js +26 -0
  46. package/dist/core/tools/index.js.map +1 -1
  47. package/dist/core/tools/lint-run.d.ts +42 -0
  48. package/dist/core/tools/lint-run.d.ts.map +1 -0
  49. package/dist/core/tools/lint-run.js +276 -0
  50. package/dist/core/tools/lint-run.js.map +1 -0
  51. package/dist/core/tools/task.js +1 -1
  52. package/dist/core/tools/task.js.map +1 -1
  53. package/dist/core/tools/test-run.d.ts +36 -0
  54. package/dist/core/tools/test-run.d.ts.map +1 -0
  55. package/dist/core/tools/test-run.js +255 -0
  56. package/dist/core/tools/test-run.js.map +1 -0
  57. package/dist/core/tools/typecheck-run.d.ts +44 -0
  58. package/dist/core/tools/typecheck-run.d.ts.map +1 -0
  59. package/dist/core/tools/typecheck-run.js +343 -0
  60. package/dist/core/tools/typecheck-run.js.map +1 -0
  61. package/dist/core/tools/verification-runner.d.ts +53 -0
  62. package/dist/core/tools/verification-runner.d.ts.map +1 -0
  63. package/dist/core/tools/verification-runner.js +235 -0
  64. package/dist/core/tools/verification-runner.js.map +1 -0
  65. package/dist/core/tools/web-search.d.ts +72 -0
  66. package/dist/core/tools/web-search.d.ts.map +1 -0
  67. package/dist/core/tools/web-search.js +702 -0
  68. package/dist/core/tools/web-search.js.map +1 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/branch-summary-message.js +2 -1
  75. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  76. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/compaction-summary-message.js +2 -1
  78. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  79. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  80. package/dist/modes/interactive/components/config-selector.js +7 -2
  81. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  82. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/custom-message.js +2 -1
  84. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  85. package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/mcp-selector.js +3 -1
  87. package/dist/modes/interactive/components/mcp-selector.js.map +1 -1
  88. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/model-selector.js +12 -2
  90. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  91. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  92. package/dist/modes/interactive/components/oauth-selector.js +11 -0
  93. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  94. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  95. package/dist/modes/interactive/components/scoped-models-selector.js +16 -5
  96. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  97. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/session-selector.js +4 -2
  99. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  100. package/dist/modes/interactive/components/settings-selector.d.ts +25 -0
  101. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/settings-selector.js +182 -2
  103. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  104. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/show-images-selector.js +7 -2
  106. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  107. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  108. package/dist/modes/interactive/components/skill-invocation-message.js +4 -2
  109. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  110. package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/subagent-message.js +3 -1
  112. package/dist/modes/interactive/components/subagent-message.js.map +1 -1
  113. package/dist/modes/interactive/components/task-plan-message.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/task-plan-message.js +2 -1
  115. package/dist/modes/interactive/components/task-plan-message.js.map +1 -1
  116. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/theme-selector.js +7 -2
  118. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  119. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/thinking-selector.js +7 -2
  121. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  122. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  123. package/dist/modes/interactive/components/tool-execution.js +25 -7
  124. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  125. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/tree-selector.js +18 -3
  127. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  128. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/user-message-selector.js +8 -0
  130. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  131. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/user-message.js +2 -1
  133. package/dist/modes/interactive/components/user-message.js.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.d.ts +8 -0
  135. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/dist/modes/interactive/interactive-mode.js +622 -11
  137. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/dist/modes/interactive/theme/dark.json +39 -38
  139. package/dist/modes/interactive/theme/light.json +29 -29
  140. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  141. package/dist/modes/interactive/theme/theme.js +16 -25
  142. package/dist/modes/interactive/theme/theme.js.map +1 -1
  143. package/dist/modes/interactive/theme/universal.json +85 -0
  144. package/docs/cli-reference.md +32 -2
  145. package/docs/configuration.md +86 -2
  146. package/docs/development-and-testing.md +1 -1
  147. package/docs/interactive-mode.md +8 -3
  148. package/docs/rpc-json-sdk.md +1 -1
  149. package/package.json +1 -1
@@ -0,0 +1,702 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead } from "./truncate.js";
3
+ const webSearchSchema = Type.Object({
4
+ query: Type.String({ description: "Search query text." }),
5
+ max_results: Type.Optional(Type.Number({ description: "Maximum number of results to return." })),
6
+ include_domains: Type.Optional(Type.Array(Type.String(), { description: "Only include results from these domains." })),
7
+ exclude_domains: Type.Optional(Type.Array(Type.String(), { description: "Exclude results from these domains." })),
8
+ topic: Type.Optional(Type.String({ description: "Optional topic hint (e.g. general, news)." })),
9
+ days: Type.Optional(Type.Number({ description: "Optional recency filter in days (provider support varies)." })),
10
+ search_depth: Type.Optional(Type.Union([Type.Literal("basic"), Type.Literal("advanced")], {
11
+ description: "Search depth hint for providers that support it (default: basic).",
12
+ })),
13
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20)." })),
14
+ });
15
+ export const DEFAULT_WEB_SEARCH_MAX_RESULTS = 8;
16
+ export const DEFAULT_WEB_SEARCH_TIMEOUT_SECONDS = 20;
17
+ const DEFAULT_PROVIDER_MODE = "auto";
18
+ const DEFAULT_FALLBACK_MODE = "searxng_ddg";
19
+ const DEFAULT_SAFE_SEARCH = "moderate";
20
+ const DEFAULT_WEB_SEARCH_ENABLED = true;
21
+ const TAVILY_SEARCH_URL = "https://api.tavily.com/search";
22
+ const DUCKDUCKGO_HTML_URL = "https://duckduckgo.com/html/";
23
+ const MAX_PROVIDER_BODY_BYTES = 512 * 1024;
24
+ const MAX_RESULTS_CAP = 20;
25
+ function normalizePositiveInt(raw, fallback, field) {
26
+ if (raw === undefined)
27
+ return fallback;
28
+ const value = Math.floor(raw);
29
+ if (!Number.isFinite(value) || value <= 0) {
30
+ throw new Error(`${field} must be a positive number.`);
31
+ }
32
+ return value;
33
+ }
34
+ function normalizeBoolean(raw, fallback) {
35
+ if (typeof raw === "boolean")
36
+ return raw;
37
+ return fallback;
38
+ }
39
+ function normalizeProviderMode(raw) {
40
+ return raw === "tavily" ? "tavily" : "auto";
41
+ }
42
+ function normalizeFallbackMode(raw) {
43
+ if (raw === "searxng_only" || raw === "none")
44
+ return raw;
45
+ return "searxng_ddg";
46
+ }
47
+ function normalizeSafeSearch(raw) {
48
+ if (raw === "off" || raw === "strict")
49
+ return raw;
50
+ return "moderate";
51
+ }
52
+ function normalizeRuntimeConfig(config, defaultMaxResults, defaultTimeoutSeconds) {
53
+ const maxResults = Math.max(1, Math.min(MAX_RESULTS_CAP, normalizePositiveInt(typeof config?.maxResults === "number" ? config.maxResults : undefined, defaultMaxResults, "maxResults")));
54
+ const timeoutSeconds = normalizePositiveInt(typeof config?.timeoutSeconds === "number" ? config.timeoutSeconds : undefined, defaultTimeoutSeconds, "timeoutSeconds");
55
+ return {
56
+ enabled: normalizeBoolean(config?.enabled, DEFAULT_WEB_SEARCH_ENABLED),
57
+ providerMode: normalizeProviderMode(config?.providerMode),
58
+ fallbackMode: normalizeFallbackMode(config?.fallbackMode),
59
+ safeSearch: normalizeSafeSearch(config?.safeSearch),
60
+ maxResults,
61
+ timeoutSeconds,
62
+ };
63
+ }
64
+ function normalizeQuery(query) {
65
+ const normalized = query.trim();
66
+ if (!normalized) {
67
+ throw new Error("query must be a non-empty string.");
68
+ }
69
+ return normalized;
70
+ }
71
+ function normalizeDomain(raw) {
72
+ const compact = raw.trim().toLowerCase();
73
+ if (!compact)
74
+ return undefined;
75
+ const noProtocol = compact.replace(/^https?:\/\//, "");
76
+ const hostPart = noProtocol.split("/")[0] ?? "";
77
+ const withoutPort = hostPart.split(":")[0] ?? "";
78
+ const normalized = withoutPort.replace(/^\*\./, "").replace(/^www\./, "");
79
+ return normalized || undefined;
80
+ }
81
+ function normalizeDomains(raw) {
82
+ if (!raw || raw.length === 0)
83
+ return [];
84
+ const seen = new Set();
85
+ const result = [];
86
+ for (const domain of raw) {
87
+ const normalized = normalizeDomain(domain);
88
+ if (!normalized || seen.has(normalized))
89
+ continue;
90
+ seen.add(normalized);
91
+ result.push(normalized);
92
+ }
93
+ return result;
94
+ }
95
+ function hostMatchesDomain(host, domain) {
96
+ return host === domain || host.endsWith(`.${domain}`);
97
+ }
98
+ function normalizeUrl(rawUrl) {
99
+ let candidate = decodeHtmlEntities(rawUrl.trim());
100
+ if (!candidate)
101
+ return undefined;
102
+ if (candidate.startsWith("//")) {
103
+ candidate = `https:${candidate}`;
104
+ }
105
+ const duckRedirectPrefixes = ["/l/?", "https://duckduckgo.com/l/?", "http://duckduckgo.com/l/?"];
106
+ if (duckRedirectPrefixes.some((prefix) => candidate.startsWith(prefix))) {
107
+ try {
108
+ const redirectUrl = new URL(candidate, "https://duckduckgo.com");
109
+ const resolved = redirectUrl.searchParams.get("uddg");
110
+ if (resolved)
111
+ candidate = resolved;
112
+ }
113
+ catch {
114
+ // Keep candidate as-is and try best effort parsing below.
115
+ }
116
+ }
117
+ try {
118
+ return new URL(candidate).toString();
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ function decodeHtmlEntities(input) {
125
+ return input
126
+ .replace(/&quot;/g, '"')
127
+ .replace(/&#39;/g, "'")
128
+ .replace(/&amp;/g, "&")
129
+ .replace(/&lt;/g, "<")
130
+ .replace(/&gt;/g, ">");
131
+ }
132
+ function stripHtml(input) {
133
+ return decodeHtmlEntities(input.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
134
+ }
135
+ function normalizeResult(result) {
136
+ const url = result.url ? normalizeUrl(result.url) : undefined;
137
+ if (!url)
138
+ return undefined;
139
+ const title = (result.title ?? "").trim() || url;
140
+ const snippet = (result.snippet ?? "").trim();
141
+ const source = (result.source ?? "").trim() || "unknown";
142
+ return {
143
+ title,
144
+ url,
145
+ snippet,
146
+ source,
147
+ };
148
+ }
149
+ function dedupeResults(results) {
150
+ const seen = new Set();
151
+ const deduped = [];
152
+ for (const result of results) {
153
+ const key = result.url.toLowerCase();
154
+ if (seen.has(key))
155
+ continue;
156
+ seen.add(key);
157
+ deduped.push(result);
158
+ }
159
+ return deduped;
160
+ }
161
+ function applyDomainFilters(results, includeDomains, excludeDomains) {
162
+ return results.filter((result) => {
163
+ let hostname = "";
164
+ try {
165
+ hostname = new URL(result.url).hostname.toLowerCase().replace(/^www\./, "");
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ if (excludeDomains.some((domain) => hostMatchesDomain(hostname, domain))) {
171
+ return false;
172
+ }
173
+ if (includeDomains.length === 0) {
174
+ return true;
175
+ }
176
+ return includeDomains.some((domain) => hostMatchesDomain(hostname, domain));
177
+ });
178
+ }
179
+ function appendDomainOperators(query, includeDomains, excludeDomains) {
180
+ const tokens = [query];
181
+ for (const domain of includeDomains) {
182
+ tokens.push(`site:${domain}`);
183
+ }
184
+ for (const domain of excludeDomains) {
185
+ tokens.push(`-site:${domain}`);
186
+ }
187
+ return tokens.join(" ").trim();
188
+ }
189
+ function safeSearchToSearxng(value) {
190
+ if (value === "off")
191
+ return "0";
192
+ if (value === "strict")
193
+ return "2";
194
+ return "1";
195
+ }
196
+ function safeSearchToDuckDuckGo(value) {
197
+ if (value === "off")
198
+ return "-2";
199
+ if (value === "strict")
200
+ return "1";
201
+ return "-1";
202
+ }
203
+ function resolveTavilyApiKeyFromEnv() {
204
+ const key = process.env.TAVILY_API_KEY?.trim();
205
+ return key ? key : undefined;
206
+ }
207
+ function resolveSearxngBaseUrlFromEnv() {
208
+ const fromPrimary = process.env.IOSM_WEB_SEARCH_SEARXNG_URL?.trim();
209
+ if (fromPrimary)
210
+ return fromPrimary;
211
+ const fromLegacy = process.env.PI_WEB_SEARCH_SEARXNG_URL?.trim();
212
+ return fromLegacy ? fromLegacy : undefined;
213
+ }
214
+ function formatAttemptTrace(attempts) {
215
+ return attempts
216
+ .map((attempt) => {
217
+ const base = `${attempt.provider}:${attempt.status}`;
218
+ if (!attempt.reason)
219
+ return base;
220
+ return `${base} (${attempt.reason})`;
221
+ })
222
+ .join(" -> ");
223
+ }
224
+ function formatResultsOutput(params) {
225
+ const lines = [];
226
+ lines.push(`Search query: ${params.query}`);
227
+ lines.push(`Provider: ${params.provider}`);
228
+ lines.push(`Attempts: ${formatAttemptTrace(params.attempts)}`);
229
+ lines.push("");
230
+ for (const [index, result] of params.results.entries()) {
231
+ lines.push(`${index + 1}. ${result.title}`);
232
+ lines.push(` ${result.url}`);
233
+ if (result.snippet) {
234
+ lines.push(` ${result.snippet}`);
235
+ }
236
+ lines.push(` source: ${result.source}`);
237
+ }
238
+ return lines.join("\n");
239
+ }
240
+ async function readResponseBodyWithLimit(response, maxBytes) {
241
+ if (!response.body) {
242
+ return { buffer: Buffer.alloc(0), truncated: false };
243
+ }
244
+ const reader = response.body.getReader();
245
+ const chunks = [];
246
+ let total = 0;
247
+ let truncated = false;
248
+ while (true) {
249
+ const { value, done } = await reader.read();
250
+ if (done)
251
+ break;
252
+ if (!value)
253
+ continue;
254
+ const chunk = Buffer.from(value);
255
+ if (total + chunk.length > maxBytes) {
256
+ const remaining = maxBytes - total;
257
+ if (remaining > 0) {
258
+ chunks.push(chunk.subarray(0, remaining));
259
+ total += remaining;
260
+ }
261
+ truncated = true;
262
+ await reader.cancel().catch(() => { });
263
+ break;
264
+ }
265
+ chunks.push(chunk);
266
+ total += chunk.length;
267
+ }
268
+ return { buffer: Buffer.concat(chunks, total), truncated };
269
+ }
270
+ async function parseJsonBody(response) {
271
+ const captured = await readResponseBodyWithLimit(response, MAX_PROVIDER_BODY_BYTES);
272
+ let data;
273
+ try {
274
+ data = JSON.parse(captured.buffer.toString("utf-8"));
275
+ }
276
+ catch (error) {
277
+ throw new Error(`Invalid JSON response: ${error?.message ?? "parse failed"}`);
278
+ }
279
+ return { data, bodyTruncated: captured.truncated };
280
+ }
281
+ async function parseTextBody(response) {
282
+ const captured = await readResponseBodyWithLimit(response, MAX_PROVIDER_BODY_BYTES);
283
+ return { text: captured.buffer.toString("utf-8"), bodyTruncated: captured.truncated };
284
+ }
285
+ async function searchWithTavily(params) {
286
+ const payload = {
287
+ api_key: params.apiKey,
288
+ query: params.query,
289
+ max_results: params.maxResults,
290
+ search_depth: params.searchDepth,
291
+ };
292
+ if (params.includeDomains.length > 0)
293
+ payload.include_domains = params.includeDomains;
294
+ if (params.excludeDomains.length > 0)
295
+ payload.exclude_domains = params.excludeDomains;
296
+ if (params.topic)
297
+ payload.topic = params.topic;
298
+ if (params.days !== undefined)
299
+ payload.days = params.days;
300
+ const response = await params.fetchImpl(TAVILY_SEARCH_URL, {
301
+ method: "POST",
302
+ headers: {
303
+ "content-type": "application/json",
304
+ accept: "application/json",
305
+ },
306
+ body: JSON.stringify(payload),
307
+ signal: params.signal,
308
+ });
309
+ if (!response.ok) {
310
+ throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
311
+ }
312
+ const { data, bodyTruncated } = await parseJsonBody(response);
313
+ const rawResults = Array.isArray(data.results) ? data.results : [];
314
+ const results = rawResults
315
+ .map((entry) => {
316
+ const item = entry;
317
+ return normalizeResult({
318
+ title: typeof item.title === "string" ? item.title : undefined,
319
+ url: typeof item.url === "string" ? item.url : undefined,
320
+ snippet: typeof item.content === "string"
321
+ ? item.content
322
+ : typeof item.snippet === "string"
323
+ ? item.snippet
324
+ : undefined,
325
+ source: "tavily",
326
+ });
327
+ })
328
+ .filter((item) => item !== undefined);
329
+ return { results: dedupeResults(results), notice: bodyTruncated ? "response body truncated" : undefined };
330
+ }
331
+ async function searchWithSearxng(params) {
332
+ const url = new URL(params.baseUrl);
333
+ const normalizedPath = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
334
+ url.pathname = normalizedPath;
335
+ const endpoint = new URL("search", url);
336
+ endpoint.searchParams.set("q", params.query);
337
+ endpoint.searchParams.set("format", "json");
338
+ endpoint.searchParams.set("safesearch", safeSearchToSearxng(params.safeSearch));
339
+ endpoint.searchParams.set("language", "en-US");
340
+ if (params.topic) {
341
+ endpoint.searchParams.set("categories", params.topic);
342
+ }
343
+ if (params.days !== undefined) {
344
+ if (params.days <= 1)
345
+ endpoint.searchParams.set("time_range", "day");
346
+ else if (params.days <= 7)
347
+ endpoint.searchParams.set("time_range", "week");
348
+ else if (params.days <= 31)
349
+ endpoint.searchParams.set("time_range", "month");
350
+ else
351
+ endpoint.searchParams.set("time_range", "year");
352
+ }
353
+ const response = await params.fetchImpl(endpoint, {
354
+ method: "GET",
355
+ headers: { accept: "application/json" },
356
+ signal: params.signal,
357
+ });
358
+ if (!response.ok) {
359
+ throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
360
+ }
361
+ const { data, bodyTruncated } = await parseJsonBody(response);
362
+ const rawResults = Array.isArray(data.results) ? data.results : [];
363
+ const results = rawResults
364
+ .slice(0, Math.max(params.maxResults * 3, params.maxResults))
365
+ .map((entry) => {
366
+ const item = entry;
367
+ return normalizeResult({
368
+ title: typeof item.title === "string" ? item.title : undefined,
369
+ url: typeof item.url === "string" ? item.url : undefined,
370
+ snippet: typeof item.content === "string" ? item.content : undefined,
371
+ source: typeof item.engine === "string" && item.engine.trim() ? item.engine : "searxng",
372
+ });
373
+ })
374
+ .filter((item) => item !== undefined);
375
+ return { results: dedupeResults(results), notice: bodyTruncated ? "response body truncated" : undefined };
376
+ }
377
+ function parseDuckDuckGoResults(html) {
378
+ const results = [];
379
+ const seen = new Set();
380
+ const primaryRegex = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
381
+ let match;
382
+ while ((match = primaryRegex.exec(html)) !== null) {
383
+ const rawUrl = match[1] ?? "";
384
+ const resolvedUrl = normalizeUrl(rawUrl);
385
+ if (!resolvedUrl || seen.has(resolvedUrl))
386
+ continue;
387
+ seen.add(resolvedUrl);
388
+ const title = stripHtml(match[2] ?? "") || resolvedUrl;
389
+ const tail = html.slice(match.index, Math.min(match.index + 2200, html.length));
390
+ const snippetMatch = tail.match(/class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/[^>]+>/i) ??
391
+ tail.match(/class="[^"]*result__extras__url[^"]*"[^>]*>([\s\S]*?)<\/[^>]+>/i);
392
+ const snippet = snippetMatch ? stripHtml(snippetMatch[1] ?? "") : "";
393
+ results.push({
394
+ title,
395
+ url: resolvedUrl,
396
+ snippet,
397
+ source: "duckduckgo",
398
+ });
399
+ }
400
+ if (results.length > 0) {
401
+ return results;
402
+ }
403
+ const liteRegex = /<a[^>]*class="result-link"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
404
+ while ((match = liteRegex.exec(html)) !== null) {
405
+ const resolvedUrl = normalizeUrl(match[1] ?? "");
406
+ if (!resolvedUrl || seen.has(resolvedUrl))
407
+ continue;
408
+ seen.add(resolvedUrl);
409
+ results.push({
410
+ title: stripHtml(match[2] ?? "") || resolvedUrl,
411
+ url: resolvedUrl,
412
+ snippet: "",
413
+ source: "duckduckgo",
414
+ });
415
+ }
416
+ return results;
417
+ }
418
+ async function searchWithDuckDuckGo(params) {
419
+ const endpoint = new URL(DUCKDUCKGO_HTML_URL);
420
+ endpoint.searchParams.set("q", params.query);
421
+ endpoint.searchParams.set("kp", safeSearchToDuckDuckGo(params.safeSearch));
422
+ endpoint.searchParams.set("kl", "us-en");
423
+ const response = await params.fetchImpl(endpoint, {
424
+ method: "GET",
425
+ headers: {
426
+ accept: "text/html,application/xhtml+xml",
427
+ },
428
+ signal: params.signal,
429
+ });
430
+ if (!response.ok) {
431
+ throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
432
+ }
433
+ const { text, bodyTruncated } = await parseTextBody(response);
434
+ return {
435
+ results: dedupeResults(parseDuckDuckGoResults(text)),
436
+ notice: bodyTruncated ? "response body truncated" : undefined,
437
+ };
438
+ }
439
+ function buildProviderOrder(fallbackMode) {
440
+ const providers = ["tavily"];
441
+ if (fallbackMode === "searxng_ddg") {
442
+ providers.push("searxng", "duckduckgo");
443
+ }
444
+ else if (fallbackMode === "searxng_only") {
445
+ providers.push("searxng");
446
+ }
447
+ return providers;
448
+ }
449
+ function toErrorMessage(error) {
450
+ if (error instanceof Error) {
451
+ return error.message;
452
+ }
453
+ return String(error);
454
+ }
455
+ function capSnippet(snippet, maxLength = 320) {
456
+ if (snippet.length <= maxLength)
457
+ return snippet;
458
+ return `${snippet.slice(0, maxLength - 1).trimEnd()}…`;
459
+ }
460
+ export function createWebSearchTool(cwd, options) {
461
+ const fetchImpl = options?.fetchImpl ?? fetch;
462
+ const permissionGuard = options?.permissionGuard;
463
+ const resolveTavilyApiKey = () => {
464
+ const fromOptions = options?.resolveTavilyApiKey?.()?.trim();
465
+ return fromOptions && fromOptions.length > 0 ? fromOptions : resolveTavilyApiKeyFromEnv();
466
+ };
467
+ const resolveSearxngBaseUrl = () => {
468
+ const fromOptions = options?.resolveSearxngBaseUrl?.()?.trim();
469
+ return fromOptions && fromOptions.length > 0 ? fromOptions : resolveSearxngBaseUrlFromEnv();
470
+ };
471
+ const defaultMaxResults = Math.max(1, Math.min(MAX_RESULTS_CAP, normalizePositiveInt(options?.defaultMaxResults, DEFAULT_WEB_SEARCH_MAX_RESULTS, "defaultMaxResults")));
472
+ const defaultTimeoutSeconds = normalizePositiveInt(options?.defaultTimeoutSeconds, DEFAULT_WEB_SEARCH_TIMEOUT_SECONDS, "defaultTimeoutSeconds");
473
+ return {
474
+ name: "web_search",
475
+ label: "web_search",
476
+ description: "Search the web for discovery (provider chain: Tavily -> SearXNG -> DuckDuckGo). Use fetch for reading specific pages.",
477
+ parameters: webSearchSchema,
478
+ execute: async (_toolCallId, input, signal) => {
479
+ const query = normalizeQuery(input.query);
480
+ const runtimeConfig = normalizeRuntimeConfig(options?.resolveRuntimeConfig?.(), defaultMaxResults, defaultTimeoutSeconds);
481
+ if (!runtimeConfig.enabled) {
482
+ throw new Error("web_search is disabled in settings.");
483
+ }
484
+ const maxResults = Math.max(1, Math.min(MAX_RESULTS_CAP, normalizePositiveInt(input.max_results, runtimeConfig.maxResults, "max_results")));
485
+ const timeoutSeconds = normalizePositiveInt(input.timeout, runtimeConfig.timeoutSeconds, "timeout");
486
+ const searchDepth = input.search_depth === "advanced" ? "advanced" : "basic";
487
+ const includeDomains = normalizeDomains(input.include_domains);
488
+ const excludeDomains = normalizeDomains(input.exclude_domains);
489
+ const days = input.days === undefined ? undefined : normalizePositiveInt(input.days, 1, "days");
490
+ const queryWithDomains = appendDomainOperators(query, includeDomains, excludeDomains);
491
+ const providerOrder = buildProviderOrder(runtimeConfig.fallbackMode);
492
+ if (permissionGuard) {
493
+ const allowed = await permissionGuard({
494
+ toolName: "web_search",
495
+ cwd,
496
+ input: {
497
+ query,
498
+ maxResults,
499
+ timeoutSeconds,
500
+ providerMode: runtimeConfig.providerMode,
501
+ fallbackMode: runtimeConfig.fallbackMode,
502
+ safeSearch: runtimeConfig.safeSearch,
503
+ includeDomains,
504
+ excludeDomains,
505
+ },
506
+ summary: `query="${query}"`,
507
+ });
508
+ if (!allowed) {
509
+ throw new Error("Permission denied for web_search operation.");
510
+ }
511
+ }
512
+ const timeoutSignal = AbortSignal.timeout(timeoutSeconds * 1000);
513
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
514
+ const providerRequestLimit = Math.min(MAX_RESULTS_CAP, Math.max(maxResults, maxResults * 2));
515
+ const attempts = [];
516
+ let selectedProvider;
517
+ let selectedResults = [];
518
+ const providerNotices = [];
519
+ for (const provider of providerOrder) {
520
+ if (provider === "tavily") {
521
+ const apiKey = resolveTavilyApiKey();
522
+ if (!apiKey) {
523
+ attempts.push({
524
+ provider: "tavily",
525
+ status: runtimeConfig.providerMode === "tavily" ? "error" : "skipped",
526
+ reason: "Tavily API key is not configured",
527
+ });
528
+ continue;
529
+ }
530
+ try {
531
+ const providerResult = await searchWithTavily({
532
+ fetchImpl,
533
+ apiKey,
534
+ query,
535
+ maxResults: providerRequestLimit,
536
+ includeDomains,
537
+ excludeDomains,
538
+ topic: input.topic,
539
+ days,
540
+ searchDepth,
541
+ signal: requestSignal,
542
+ });
543
+ if (providerResult.notice)
544
+ providerNotices.push(`tavily: ${providerResult.notice}`);
545
+ const filtered = applyDomainFilters(providerResult.results, includeDomains, excludeDomains).slice(0, maxResults);
546
+ if (filtered.length === 0) {
547
+ attempts.push({
548
+ provider: "tavily",
549
+ status: "empty",
550
+ reason: providerResult.results.length === 0 ? "no results returned" : "all results filtered out",
551
+ });
552
+ continue;
553
+ }
554
+ selectedProvider = "tavily";
555
+ selectedResults = filtered;
556
+ attempts.push({
557
+ provider: "tavily",
558
+ status: "success",
559
+ resultCount: filtered.length,
560
+ });
561
+ break;
562
+ }
563
+ catch (error) {
564
+ attempts.push({
565
+ provider: "tavily",
566
+ status: "error",
567
+ reason: toErrorMessage(error),
568
+ });
569
+ continue;
570
+ }
571
+ }
572
+ if (provider === "searxng") {
573
+ const baseUrl = resolveSearxngBaseUrl();
574
+ if (!baseUrl) {
575
+ attempts.push({
576
+ provider: "searxng",
577
+ status: "skipped",
578
+ reason: "SearXNG base URL is not configured",
579
+ });
580
+ continue;
581
+ }
582
+ try {
583
+ const providerResult = await searchWithSearxng({
584
+ fetchImpl,
585
+ baseUrl,
586
+ query: queryWithDomains,
587
+ maxResults: providerRequestLimit,
588
+ topic: input.topic,
589
+ days,
590
+ safeSearch: runtimeConfig.safeSearch,
591
+ signal: requestSignal,
592
+ });
593
+ if (providerResult.notice)
594
+ providerNotices.push(`searxng: ${providerResult.notice}`);
595
+ const filtered = applyDomainFilters(providerResult.results, includeDomains, excludeDomains).slice(0, maxResults);
596
+ if (filtered.length === 0) {
597
+ attempts.push({
598
+ provider: "searxng",
599
+ status: "empty",
600
+ reason: providerResult.results.length === 0 ? "no results returned" : "all results filtered out",
601
+ });
602
+ continue;
603
+ }
604
+ selectedProvider = "searxng";
605
+ selectedResults = filtered;
606
+ attempts.push({
607
+ provider: "searxng",
608
+ status: "success",
609
+ resultCount: filtered.length,
610
+ });
611
+ break;
612
+ }
613
+ catch (error) {
614
+ attempts.push({
615
+ provider: "searxng",
616
+ status: "error",
617
+ reason: toErrorMessage(error),
618
+ });
619
+ continue;
620
+ }
621
+ }
622
+ try {
623
+ const providerResult = await searchWithDuckDuckGo({
624
+ fetchImpl,
625
+ query: queryWithDomains,
626
+ safeSearch: runtimeConfig.safeSearch,
627
+ signal: requestSignal,
628
+ });
629
+ if (providerResult.notice)
630
+ providerNotices.push(`duckduckgo: ${providerResult.notice}`);
631
+ const filtered = applyDomainFilters(providerResult.results, includeDomains, excludeDomains).slice(0, maxResults);
632
+ if (filtered.length === 0) {
633
+ attempts.push({
634
+ provider: "duckduckgo",
635
+ status: "empty",
636
+ reason: providerResult.results.length === 0 ? "no results returned" : "all results filtered out",
637
+ });
638
+ continue;
639
+ }
640
+ selectedProvider = "duckduckgo";
641
+ selectedResults = filtered;
642
+ attempts.push({
643
+ provider: "duckduckgo",
644
+ status: "success",
645
+ resultCount: filtered.length,
646
+ });
647
+ break;
648
+ }
649
+ catch (error) {
650
+ attempts.push({
651
+ provider: "duckduckgo",
652
+ status: "error",
653
+ reason: toErrorMessage(error),
654
+ });
655
+ }
656
+ }
657
+ if (!selectedProvider || selectedResults.length === 0) {
658
+ throw new Error(`web_search failed. Attempts: ${formatAttemptTrace(attempts)}`);
659
+ }
660
+ const output = formatResultsOutput({
661
+ query,
662
+ provider: selectedProvider,
663
+ results: selectedResults.map((item) => ({ ...item, snippet: capSnippet(item.snippet) })),
664
+ attempts,
665
+ });
666
+ const truncation = truncateHead(output, {
667
+ maxBytes: Math.max(DEFAULT_MAX_BYTES, 96 * 1024),
668
+ maxLines: DEFAULT_MAX_LINES,
669
+ });
670
+ let finalOutput = truncation.content;
671
+ const notices = [];
672
+ if (truncation.truncated) {
673
+ notices.push(`output truncated by ${truncation.truncatedBy === "lines" ? "line" : "byte"} limit (showing up to ${DEFAULT_MAX_LINES} lines)`);
674
+ }
675
+ if (providerNotices.length > 0) {
676
+ notices.push(...providerNotices);
677
+ }
678
+ if (notices.length > 0) {
679
+ finalOutput += `\n\n[${notices.join(". ")}]`;
680
+ }
681
+ const details = {
682
+ query,
683
+ provider: selectedProvider,
684
+ maxResults,
685
+ timeoutSeconds,
686
+ safeSearch: runtimeConfig.safeSearch,
687
+ providerMode: runtimeConfig.providerMode,
688
+ fallbackMode: runtimeConfig.fallbackMode,
689
+ includeDomains,
690
+ excludeDomains,
691
+ attempts,
692
+ truncation: truncation.truncated ? truncation : undefined,
693
+ };
694
+ return {
695
+ content: [{ type: "text", text: finalOutput }],
696
+ details,
697
+ };
698
+ },
699
+ };
700
+ }
701
+ export const webSearchTool = createWebSearchTool(process.cwd());
702
+ //# sourceMappingURL=web-search.js.map