@humain/terminal 0.0.3-beta.0 → 0.0.3-beta.2

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 (70) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +4 -3
  4. package/dist/config.js.map +1 -1
  5. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
  6. package/dist/humain/mcp-adapter/vendor/README.md +4 -1
  7. package/dist/humain/mcp-adapter/vendor/agent-dir.ts +12 -4
  8. package/dist/humain/mcp-adapter/vendor/cli.js +25 -4
  9. package/dist/humain/mcp-adapter/vendor/config.ts +4 -4
  10. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +18 -15
  11. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +416 -60
  12. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +2 -1
  13. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +18 -8
  14. package/dist/humain/mcp-adapter/vendor/package.json +177 -176
  15. package/dist/humain/mcp-adapter/vendor/request-headers-command.ts +336 -0
  16. package/dist/humain/mcp-adapter/vendor/server-manager.ts +5 -0
  17. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +38 -18
  18. package/dist/humain/mcp-adapter/vendor/types.ts +93 -10
  19. package/dist/humain/web-access/vendor/auth-fetch.d.ts +13 -0
  20. package/dist/humain/web-access/vendor/auth-fetch.d.ts.map +1 -0
  21. package/dist/humain/web-access/vendor/auth-fetch.js +134 -0
  22. package/dist/humain/web-access/vendor/auth-fetch.js.map +1 -0
  23. package/dist/humain/web-access/vendor/chrome-cookies.d.ts +13 -0
  24. package/dist/humain/web-access/vendor/chrome-cookies.d.ts.map +1 -1
  25. package/dist/humain/web-access/vendor/chrome-cookies.js +106 -22
  26. package/dist/humain/web-access/vendor/chrome-cookies.js.map +1 -1
  27. package/dist/humain/web-access/vendor/curator-page.d.ts +1 -0
  28. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  29. package/dist/humain/web-access/vendor/curator-page.js +3 -1
  30. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  31. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  32. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  33. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  34. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  35. package/dist/humain/web-access/vendor/extract.d.ts +2 -0
  36. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  37. package/dist/humain/web-access/vendor/extract.js +79 -17
  38. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  39. package/dist/humain/web-access/vendor/fetch-params.d.ts +2 -0
  40. package/dist/humain/web-access/vendor/fetch-params.d.ts.map +1 -1
  41. package/dist/humain/web-access/vendor/fetch-params.js +17 -3
  42. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  43. package/dist/humain/web-access/vendor/firecrawl.d.ts +7 -0
  44. package/dist/humain/web-access/vendor/firecrawl.d.ts.map +1 -1
  45. package/dist/humain/web-access/vendor/firecrawl.js +141 -10
  46. package/dist/humain/web-access/vendor/firecrawl.js.map +1 -1
  47. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  48. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  49. package/dist/humain/web-access/vendor/gemini-search.js +22 -4
  50. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  51. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  52. package/dist/humain/web-access/vendor/index.js +122 -48
  53. package/dist/humain/web-access/vendor/index.js.map +1 -1
  54. package/dist/humain/web-access/vendor/summary-review.d.ts +3 -1
  55. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  56. package/dist/humain/web-access/vendor/summary-review.js +6 -3
  57. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  58. package/dist/humain/web-access/vendor/youtube-extract.d.ts.map +1 -1
  59. package/dist/humain/web-access/vendor/youtube-extract.js +2 -2
  60. package/dist/humain/web-access/vendor/youtube-extract.js.map +1 -1
  61. package/dist/utils/clipboard-image.d.ts.map +1 -1
  62. package/dist/utils/clipboard-image.js +2 -0
  63. package/dist/utils/clipboard-image.js.map +1 -1
  64. package/npm-shrinkwrap.json +7 -7
  65. package/package.json +6 -6
  66. package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
  67. package/src/humain/mcp-adapter/upstream.json +3 -3
  68. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
  69. package/src/humain/web-access/UPSTREAM.md +1 -1
  70. package/src/humain/web-access/upstream.json +4 -4
@@ -10,6 +10,7 @@ import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
10
10
  import { Type } from "typebox";
11
11
  import { StringEnum, complete } from "@earendil-works/pi-ai/compat";
12
12
  import { normalizeFetchContentParams } from "./fetch-params.js";
13
+ import { resolveAuthFetchProfile } from "./auth-fetch.js";
13
14
  import { findContent } from "./content-find.js";
14
15
  import { answerFromPage } from "./page-query.js";
15
16
  import { clearCloneCache } from "./github-extract.js";
@@ -38,6 +39,7 @@ import { isSearch1APIAvailable } from "./search1api.js";
38
39
  import { isSearchinfinityAvailable } from "./searchinfinity.js";
39
40
  import { isQueritAvailable } from "./querit.js";
40
41
  import { isTavilyAvailable } from "./tavily.js";
42
+ import { isFirecrawlAvailable } from "./firecrawl.js";
41
43
  import { isJinaSearchAvailable } from "./jina-search.js";
42
44
  import { isSerpdiveAvailable } from "./serpdive.js";
43
45
  import { isKagiAvailable } from "./kagi.js";
@@ -276,6 +278,7 @@ async function getProviderAvailability(ctx) {
276
278
  searchinfinity: isSearchinfinityAvailable(),
277
279
  querit: isQueritAvailable(),
278
280
  tavily: isTavilyAvailable(),
281
+ firecrawl: isFirecrawlAvailable(),
279
282
  jina: isJinaSearchAvailable(),
280
283
  serpdive: isSerpdiveAvailable(),
281
284
  kagi: isKagiAvailable(),
@@ -339,6 +342,8 @@ function firstAvailableProvider(available, preferOpenAI, fallback) {
339
342
  return "querit";
340
343
  if (available.tavily)
341
344
  return "tavily";
345
+ if (available.firecrawl)
346
+ return "firecrawl";
342
347
  if (available.jina)
343
348
  return "jina";
344
349
  if (available.serpdive)
@@ -397,6 +402,9 @@ function resolveProvider(provider, available, options) {
397
402
  if (provider === "tavily" && !available.tavily) {
398
403
  return firstAvailableProvider(available, preferOpenAI, "tavily");
399
404
  }
405
+ if (provider === "firecrawl" && !available.firecrawl) {
406
+ return firstAvailableProvider(available, preferOpenAI, "firecrawl");
407
+ }
400
408
  if (provider === "jina" && !available.jina) {
401
409
  return firstAvailableProvider(available, preferOpenAI, "jina");
402
410
  }
@@ -435,8 +443,8 @@ const activeCurators = new Map();
435
443
  const glimpseWins = new Map();
436
444
  const DEFAULT_MAX_INLINE_CONTENT_CHARS = 30_000;
437
445
  const MAX_INLINE_CONTENT_CHARS = 200_000;
438
- function getMaxInlineContentChars() {
439
- const value = loadConfig().maxInlineContentChars;
446
+ function getMaxInlineContentChars(config = loadConfig()) {
447
+ const value = config.maxInlineContentChars;
440
448
  if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
441
449
  return DEFAULT_MAX_INLINE_CONTENT_CHARS;
442
450
  }
@@ -445,6 +453,12 @@ function getMaxInlineContentChars() {
445
453
  function stripThumbnails(results) {
446
454
  return results.map(({ thumbnail, frames, ...rest }) => rest);
447
455
  }
456
+ function storeFetchResult(pi, responseId, data, authProfile) {
457
+ if (authProfile?.cache === "off")
458
+ return false;
459
+ pi.appendEntry("web-search-results", storeFetchedContentResult(responseId, data));
460
+ return true;
461
+ }
448
462
  function initialContentSlice(content, maxChars) {
449
463
  let endOffset = Math.min(content.length, maxChars);
450
464
  if (endOffset < content.length) {
@@ -468,6 +482,19 @@ function normalizeFindQueries(value) {
468
482
  throw new Error("findText must contain at least one non-empty string");
469
483
  return queries;
470
484
  }
485
+ function formatInputValue(value) {
486
+ if (typeof value === "string")
487
+ return JSON.stringify(value);
488
+ if (typeof value === "number")
489
+ return Number.isNaN(value) ? "NaN" : String(value);
490
+ try {
491
+ const serialized = JSON.stringify(value);
492
+ return serialized === undefined ? String(value) : serialized;
493
+ }
494
+ catch {
495
+ return String(value);
496
+ }
497
+ }
471
498
  function formatSearchSummary(results, answer) {
472
499
  if (results.length === 0) {
473
500
  return answer ? `${answer}\n\n---\n\n**Sources:**\nNo sources returned.` : "No results found.";
@@ -1420,12 +1447,12 @@ export default function (pi) {
1420
1447
  pi.registerTool({
1421
1448
  name: toolNames.webSearch,
1422
1449
  label: "Web Search",
1423
- description: `Search the web using OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, AnySearch, xAI, Bright Data, or SerpBase. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; xAI search uses a SuperGrok/X Premium subscription or xAI API key. DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, auto-selects OpenAI when suitable and available, then Exa, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web. When SearXNG is configured, it is preferred first for local/private search.`,
1450
+ description: `Search the web using OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, AnySearch, xAI, Bright Data, or SerpBase. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; xAI search uses a SuperGrok/X Premium subscription or xAI API key. DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, auto-selects OpenAI when suitable and available, then Exa, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web. When SearXNG is configured, it is preferred first for local/private search.`,
1424
1451
  promptSnippet: "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
1425
1452
  parameters: Type.Object({
1426
1453
  query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
1427
1454
  queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched in sequence, each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
1428
- numResults: Type.Optional(Type.Number({ description: "Results per query (default: 5, max: 20)" })),
1455
+ numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
1429
1456
  includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
1430
1457
  recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" })),
1431
1458
  domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
@@ -1928,7 +1955,7 @@ export default function (pi) {
1928
1955
  parameters: Type.Object({
1929
1956
  claim: Type.String({ description: "The assertion to check against web sources." }),
1930
1957
  queries: Type.Optional(Type.Array(Type.String(), { description: "Search queries (default: the claim)." })),
1931
- numResults: Type.Optional(Type.Number({ description: "Results per query (default: 5, max: 20)." })),
1958
+ numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
1932
1959
  fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
1933
1960
  recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
1934
1961
  domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
@@ -2051,6 +2078,9 @@ export default function (pi) {
2051
2078
  model: Type.Optional(Type.String({
2052
2079
  description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-3.6-flash'). Defaults to config or gemini-3.6-flash.",
2053
2080
  })),
2081
+ auth: Type.Optional(Type.Union([Type.String(), Type.Boolean()], {
2082
+ description: "Opt into an authFetch profile for local browser-cookie fetching. Use a profile name, or true only when exactly one profile exists.",
2083
+ })),
2054
2084
  }),
2055
2085
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
2056
2086
  let normalized;
@@ -2075,6 +2105,19 @@ export default function (pi) {
2075
2105
  if (mode === "answer" && options.model) {
2076
2106
  return { content: [{ type: "text", text: "Error: use answerModel, not model, with mode answer." }], details: { error: "model is incompatible with mode answer" } };
2077
2107
  }
2108
+ if (mode === "answer" && options.auth !== undefined) {
2109
+ return { content: [{ type: "text", text: "Error: auth cannot be combined with mode answer." }], details: { error: "auth cannot be combined with mode answer" } };
2110
+ }
2111
+ let authFetchProfile;
2112
+ if (options.auth !== undefined) {
2113
+ try {
2114
+ authFetchProfile = resolveAuthFetchProfile(options.auth);
2115
+ }
2116
+ catch (err) {
2117
+ const error = err instanceof Error ? err.message : String(err);
2118
+ return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2119
+ }
2120
+ }
2078
2121
  if (urlList.length === 0) {
2079
2122
  return {
2080
2123
  content: [{ type: "text", text: "Error: No URL provided." }],
@@ -2085,13 +2128,13 @@ export default function (pi) {
2085
2128
  content: [{ type: "text", text: `Fetching ${urlList.length} URL(s)...` }],
2086
2129
  details: { phase: "fetch", progress: 0 },
2087
2130
  });
2088
- const { answerModel: _answerModel, ...extractionOptions } = options;
2131
+ const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = options;
2089
2132
  const fetchOptions = mode === "answer"
2090
2133
  ? (() => {
2091
2134
  const { prompt: _prompt, ...rest } = extractionOptions;
2092
- return rest;
2135
+ return { ...rest, ...(authFetchProfile ? { authFetchProfile } : {}) };
2093
2136
  })()
2094
- : extractionOptions;
2137
+ : { ...extractionOptions, ...(authFetchProfile ? { authFetchProfile } : {}) };
2095
2138
  const fetchResults = await fetchAllContent(urlList, signal, fetchOptions);
2096
2139
  const presentedResults = mode === "answer"
2097
2140
  ? await Promise.all(fetchResults.map(async result => {
@@ -2116,7 +2159,6 @@ export default function (pi) {
2116
2159
  : fetchResults;
2117
2160
  const successful = presentedResults.filter((r) => !r.error).length;
2118
2161
  const totalChars = presentedResults.reduce((sum, r) => sum + r.content.length, 0);
2119
- // ALWAYS store results (even for single URL)
2120
2162
  const responseId = generateId();
2121
2163
  const data = {
2122
2164
  id: responseId,
@@ -2124,14 +2166,14 @@ export default function (pi) {
2124
2166
  timestamp: Date.now(),
2125
2167
  urls: stripThumbnails(fetchResults),
2126
2168
  };
2127
- pi.appendEntry("web-search-results", storeFetchedContentResult(responseId, data));
2169
+ const storedContent = storeFetchResult(pi, responseId, data, authFetchProfile);
2128
2170
  // Single URL: return content directly (possibly truncated) with responseId
2129
2171
  if (urlList.length === 1) {
2130
2172
  const result = presentedResults[0];
2131
2173
  if (result.error) {
2132
2174
  return {
2133
2175
  content: [{ type: "text", text: `Error: ${result.error}` }],
2134
- details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, responseId, prompt: params.prompt, timestamp: params.timestamp, frames: params.frames },
2176
+ details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, ...(storedContent ? { responseId } : {}), prompt: params.prompt, timestamp: params.timestamp, frames: params.frames },
2135
2177
  };
2136
2178
  }
2137
2179
  const fullLength = result.content.length;
@@ -2140,9 +2182,11 @@ export default function (pi) {
2140
2182
  let output = slice.text;
2141
2183
  if (truncated) {
2142
2184
  output += `\n\n---\nShowing ${slice.endOffset} of ${fullLength} chars, ${slice.shownBytes} of ${slice.totalBytes} bytes, and ${slice.shownLines} of ${slice.totalLines} lines. `;
2143
- output += getSearchContentEnabled
2144
- ? `Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0, offset: ${slice.endOffset} }) for the next slice.`
2145
- : "Content retrieval is not registered.";
2185
+ output += storedContent
2186
+ ? getSearchContentEnabled
2187
+ ? `Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0, offset: ${slice.endOffset} }) for the next slice.`
2188
+ : "Content retrieval is not registered."
2189
+ : "Authenticated fetch cache is off; repeat the fetch to read more.";
2146
2190
  }
2147
2191
  const content = [];
2148
2192
  if (result.frames?.length) {
@@ -2164,7 +2208,7 @@ export default function (pi) {
2164
2208
  successful: 1,
2165
2209
  totalChars: fullLength,
2166
2210
  title: result.title,
2167
- responseId,
2211
+ ...(storedContent ? { responseId } : {}),
2168
2212
  truncated,
2169
2213
  hasImage: imageCount > 0,
2170
2214
  imageCount,
@@ -2192,17 +2236,19 @@ export default function (pi) {
2192
2236
  output += `- ${title || url} (${content.length} chars)\n`;
2193
2237
  }
2194
2238
  }
2195
- output += getSearchContentEnabled
2196
- ? `\n---\nUse ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 }) to retrieve bounded content slices.`
2197
- : "\n---\nContent retrieval is not registered.";
2239
+ output += storedContent
2240
+ ? getSearchContentEnabled
2241
+ ? `\n---\nUse ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 }) to retrieve bounded content slices.`
2242
+ : "\n---\nContent retrieval is not registered."
2243
+ : "\n---\nAuthenticated fetch cache is off; repeat the fetch to read content.";
2198
2244
  return {
2199
2245
  content: [{ type: "text", text: output }],
2200
- details: { urls: urlList, urlCount: urlList.length, successful, totalChars, responseId },
2246
+ details: { urls: urlList, urlCount: urlList.length, successful, totalChars, ...(storedContent ? { responseId } : {}) },
2201
2247
  };
2202
2248
  },
2203
2249
  renderCall(args, theme) {
2204
2250
  const { urlList, options } = normalizeFetchContentParams(args);
2205
- const { prompt, timestamp, frames, model, mode, answerModel } = options;
2251
+ const { prompt, timestamp, frames, model, mode, answerModel, auth } = options;
2206
2252
  if (urlList.length === 0) {
2207
2253
  return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(no URL)"), 0, 0);
2208
2254
  }
@@ -2240,6 +2286,9 @@ export default function (pi) {
2240
2286
  if (answerModel) {
2241
2287
  lines.push(theme.fg("dim", " answer model: ") + theme.fg("warning", answerModel));
2242
2288
  }
2289
+ if (auth !== undefined) {
2290
+ lines.push(theme.fg("dim", " auth: ") + theme.fg("warning", auth === true ? "true" : auth));
2291
+ }
2243
2292
  return new Text(lines.join("\n"), 0, 0);
2244
2293
  },
2245
2294
  renderResult(result, { expanded, isPartial }, theme) {
@@ -2313,7 +2362,8 @@ export default function (pi) {
2313
2362
  return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
2314
2363
  },
2315
2364
  });
2316
- if (getSearchContentEnabled)
2365
+ if (getSearchContentEnabled) {
2366
+ const maxInlineContentChars = getMaxInlineContentChars(initConfig);
2317
2367
  pi.registerTool({
2318
2368
  name: toolNames.getSearchContent,
2319
2369
  label: "Get Search Content",
@@ -2322,11 +2372,11 @@ export default function (pi) {
2322
2372
  parameters: Type.Object({
2323
2373
  responseId: Type.String({ description: `The responseId from ${storedContentSources}` }),
2324
2374
  query: Type.Optional(Type.String({ description: searchQueryDescription })),
2325
- queryIndex: Type.Optional(Type.Number({ description: "Get content for query at index" })),
2375
+ queryIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for query at index" })),
2326
2376
  url: Type.Optional(Type.String({ description: "Get content for this URL" })),
2327
- urlIndex: Type.Optional(Type.Number({ description: "Get content for URL at index" })),
2328
- offset: Type.Optional(Type.Number({ description: "Character offset for fetched URL content slices (default 0). Cannot be combined with findText." })),
2329
- limit: Type.Optional(Type.Number({ description: "Maximum characters to return for fetched URL content slices (default and max are set by maxInlineContentChars). Cannot be combined with findText." })),
2377
+ urlIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for URL at index" })),
2378
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset for fetched URL content slices (default 0). Cannot be combined with findText." })),
2379
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Maximum characters to return for fetched URL content slices (default and max are set by maxInlineContentChars). Cannot be combined with findText." })),
2330
2380
  findText: Type.Optional(Type.Union([
2331
2381
  Type.String({ minLength: 1, maxLength: 500 }),
2332
2382
  Type.Array(Type.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 }),
@@ -2335,15 +2385,23 @@ export default function (pi) {
2335
2385
  }),
2336
2386
  async execute(_toolCallId, params) {
2337
2387
  if (params.findText !== undefined && (params.offset !== undefined || params.limit !== undefined)) {
2338
- return { content: [{ type: "text", text: "findText cannot be combined with offset or limit" }], details: { error: "Incompatible find options" } };
2388
+ const offset = formatInputValue(params.offset);
2389
+ const limit = formatInputValue(params.limit);
2390
+ return {
2391
+ content: [{ type: "text", text: `findText cannot be combined with offset or limit. Received offset=${offset}, limit=${limit}; omit offset and limit when using findText.` }],
2392
+ details: { error: "Incompatible find options" },
2393
+ };
2339
2394
  }
2340
2395
  if (params.findMode !== undefined && params.findText === undefined) {
2341
- return { content: [{ type: "text", text: "findMode requires findText" }], details: { error: "findMode requires findText" } };
2396
+ return {
2397
+ content: [{ type: "text", text: `findMode ${formatInputValue(params.findMode)} requires findText; provide findText or omit findMode.` }],
2398
+ details: { error: "findMode requires findText" },
2399
+ };
2342
2400
  }
2343
2401
  const data = getResult(params.responseId);
2344
2402
  if (!data) {
2345
2403
  return {
2346
- content: [{ type: "text", text: `Error: No stored results for "${params.responseId}"` }],
2404
+ content: [{ type: "text", text: `Error: No stored results for responseId ${formatInputValue(params.responseId)}. Use a responseId returned by ${storedContentSources}.` }],
2347
2405
  details: { error: "Not found", responseId: params.responseId },
2348
2406
  };
2349
2407
  }
@@ -2351,22 +2409,30 @@ export default function (pi) {
2351
2409
  const artifact = getResearchArtifact(params.responseId);
2352
2410
  if (!artifact) {
2353
2411
  return {
2354
- content: [{ type: "text", text: `Error: artifact ${params.responseId} not found` }],
2412
+ content: [{ type: "text", text: `Error: stored research artifact for responseId ${formatInputValue(params.responseId)} was not found. Use a responseId returned by ${storedContentSources}.` }],
2355
2413
  details: { error: "Artifact not found", responseId: params.responseId },
2356
2414
  };
2357
2415
  }
2358
2416
  const serialized = JSON.stringify(artifact, null, 2);
2359
- const maxInlineContentChars = getMaxInlineContentChars();
2360
2417
  const offset = params.offset ?? 0;
2361
2418
  const limit = params.limit ?? maxInlineContentChars;
2362
2419
  if (!Number.isInteger(offset) || offset < 0) {
2363
- return { content: [{ type: "text", text: "offset must be a non-negative integer" }], details: { error: "Invalid offset", offset } };
2420
+ return {
2421
+ content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for responseId ${formatInputValue(params.responseId)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
2422
+ details: { error: "Invalid offset", offset },
2423
+ };
2364
2424
  }
2365
2425
  if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
2366
- return { content: [{ type: "text", text: `limit must be an integer from 1 to ${maxInlineContentChars}` }], details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars } };
2426
+ return {
2427
+ content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for responseId ${formatInputValue(params.responseId)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
2428
+ details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
2429
+ };
2367
2430
  }
2368
2431
  if (offset > serialized.length) {
2369
- return { content: [{ type: "text", text: `offset ${offset} is out of range (0-${serialized.length})` }], details: { error: "Offset out of range", offset, contentLength: serialized.length } };
2432
+ return {
2433
+ content: [{ type: "text", text: `Offset ${offset} is out of range for responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${serialized.length}. Use an offset within that range.` }],
2434
+ details: { error: "Offset out of range", offset, contentLength: serialized.length },
2435
+ };
2370
2436
  }
2371
2437
  const endOffset = Math.min(offset + limit, serialized.length);
2372
2438
  const artifactSlice = serialized.slice(offset, endOffset);
@@ -2383,7 +2449,7 @@ export default function (pi) {
2383
2449
  if (!queryData) {
2384
2450
  const available = data.queries.map((q) => `"${q.query}"`).join(", ");
2385
2451
  return {
2386
- content: [{ type: "text", text: `Query "${params.query}" not found. Available: ${available}` }],
2452
+ content: [{ type: "text", text: `Query ${formatInputValue(params.query)} was not found for responseId ${formatInputValue(params.responseId)}. Received query=${formatInputValue(params.query)}. Available queries: ${available || "none"}. Use one of the available queries or queryIndex.` }],
2387
2453
  details: { error: "Query not found" },
2388
2454
  };
2389
2455
  }
@@ -2391,8 +2457,9 @@ export default function (pi) {
2391
2457
  else if (params.queryIndex !== undefined) {
2392
2458
  queryData = data.queries[params.queryIndex];
2393
2459
  if (!queryData) {
2460
+ const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
2394
2461
  return {
2395
- content: [{ type: "text", text: `Index ${params.queryIndex} out of range (0-${data.queries.length - 1})` }],
2462
+ content: [{ type: "text", text: `Query index ${formatInputValue(params.queryIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received queryIndex=${formatInputValue(params.queryIndex)}; valid indexes are 0-${data.queries.length - 1}. Available queries: ${available || "none"}. Use one of the available indexes.` }],
2396
2463
  details: { error: "Index out of range" },
2397
2464
  };
2398
2465
  }
@@ -2400,13 +2467,13 @@ export default function (pi) {
2400
2467
  else {
2401
2468
  const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
2402
2469
  return {
2403
- content: [{ type: "text", text: `Specify query or queryIndex. Available: ${available}` }],
2470
+ content: [{ type: "text", text: `Specify query or queryIndex for responseId ${formatInputValue(params.responseId)}. Available queries: ${available || "none"}.` }],
2404
2471
  details: { error: "No query specified" },
2405
2472
  };
2406
2473
  }
2407
2474
  if (queryData.error) {
2408
2475
  return {
2409
- content: [{ type: "text", text: `Error for "${queryData.query}": ${queryData.error}` }],
2476
+ content: [{ type: "text", text: `Error retrieving query ${formatInputValue(queryData.query)} from responseId ${formatInputValue(params.responseId)}: ${queryData.error}. Check the stored search result and retry with another query or queryIndex if needed.` }],
2410
2477
  details: { error: queryData.error, query: queryData.query },
2411
2478
  };
2412
2479
  }
@@ -2422,7 +2489,10 @@ export default function (pi) {
2422
2489
  }
2423
2490
  catch (err) {
2424
2491
  const error = err instanceof Error ? err.message : String(err);
2425
- return { content: [{ type: "text", text: error }], details: { error, query: queryData.query } };
2492
+ return {
2493
+ content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in query ${formatInputValue(queryData.query)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
2494
+ details: { error, query: queryData.query },
2495
+ };
2426
2496
  }
2427
2497
  }
2428
2498
  return {
@@ -2439,7 +2509,7 @@ export default function (pi) {
2439
2509
  if (!urlData) {
2440
2510
  const available = data.urls.map((u) => u.url).join("\n ");
2441
2511
  return {
2442
- content: [{ type: "text", text: `URL not found. Available:\n ${available}` }],
2512
+ content: [{ type: "text", text: `URL ${formatInputValue(params.url)} was not found for responseId ${formatInputValue(params.responseId)}. Received url=${formatInputValue(params.url)}. Available URLs:\n ${available || " none"}\nUse one of the available URLs or urlIndex.` }],
2443
2513
  details: { error: "URL not found" },
2444
2514
  };
2445
2515
  }
@@ -2448,8 +2518,9 @@ export default function (pi) {
2448
2518
  selectedUrlIndex = params.urlIndex;
2449
2519
  urlData = data.urls[selectedUrlIndex];
2450
2520
  if (!urlData) {
2521
+ const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
2451
2522
  return {
2452
- content: [{ type: "text", text: `Index ${params.urlIndex} out of range (0-${data.urls.length - 1})` }],
2523
+ content: [{ type: "text", text: `URL index ${formatInputValue(params.urlIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received urlIndex=${formatInputValue(params.urlIndex)}; valid indexes are 0-${data.urls.length - 1}. Available URLs:\n ${available || " none"}\nUse one of the available indexes.` }],
2453
2524
  details: { error: "Index out of range" },
2454
2525
  };
2455
2526
  }
@@ -2457,13 +2528,13 @@ export default function (pi) {
2457
2528
  else {
2458
2529
  const available = data.urls.map((u, i) => `${i}: ${u.url}`).join("\n ");
2459
2530
  return {
2460
- content: [{ type: "text", text: `Specify url or urlIndex. Available:\n ${available}` }],
2531
+ content: [{ type: "text", text: `Specify url or urlIndex for responseId ${formatInputValue(params.responseId)}. Available URLs:\n ${available || " none"}` }],
2461
2532
  details: { error: "No URL specified" },
2462
2533
  };
2463
2534
  }
2464
2535
  if (urlData.error) {
2465
2536
  return {
2466
- content: [{ type: "text", text: `Error for ${urlData.url}: ${urlData.error}` }],
2537
+ content: [{ type: "text", text: `Error retrieving URL ${formatInputValue(urlData.url)} from responseId ${formatInputValue(params.responseId)}: ${urlData.error}. Check the stored fetch result and retry with another URL or urlIndex if needed.` }],
2467
2538
  details: { error: urlData.error, url: urlData.url },
2468
2539
  };
2469
2540
  }
@@ -2478,27 +2549,29 @@ export default function (pi) {
2478
2549
  }
2479
2550
  catch (err) {
2480
2551
  const error = err instanceof Error ? err.message : String(err);
2481
- return { content: [{ type: "text", text: error }], details: { error, url: urlData.url } };
2552
+ return {
2553
+ content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in URL ${formatInputValue(urlData.url)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
2554
+ details: { error, url: urlData.url },
2555
+ };
2482
2556
  }
2483
2557
  }
2484
- const maxInlineContentChars = getMaxInlineContentChars();
2485
2558
  const offset = params.offset ?? 0;
2486
2559
  const limit = params.limit ?? maxInlineContentChars;
2487
2560
  if (!Number.isInteger(offset) || offset < 0) {
2488
2561
  return {
2489
- content: [{ type: "text", text: "offset must be a non-negative integer" }],
2562
+ content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for URL ${formatInputValue(urlData.url)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
2490
2563
  details: { error: "Invalid offset", offset },
2491
2564
  };
2492
2565
  }
2493
2566
  if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
2494
2567
  return {
2495
- content: [{ type: "text", text: `limit must be an integer from 1 to ${maxInlineContentChars}` }],
2568
+ content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for URL ${formatInputValue(urlData.url)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
2496
2569
  details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
2497
2570
  };
2498
2571
  }
2499
2572
  if (offset > urlData.content.length) {
2500
2573
  return {
2501
- content: [{ type: "text", text: `offset ${offset} is out of range (0-${urlData.content.length})` }],
2574
+ content: [{ type: "text", text: `Offset ${offset} is out of range for URL ${formatInputValue(urlData.url)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${urlData.content.length}. Use an offset within that range.` }],
2502
2575
  details: { error: "Offset out of range", offset, contentLength: urlData.content.length },
2503
2576
  };
2504
2577
  }
@@ -2527,7 +2600,7 @@ export default function (pi) {
2527
2600
  };
2528
2601
  }
2529
2602
  return {
2530
- content: [{ type: "text", text: "Invalid stored data format" }],
2603
+ content: [{ type: "text", text: `Invalid stored data for responseId ${formatInputValue(params.responseId)}: received type ${formatInputValue(data.type)}. Use a responseId returned by ${storedContentSources}.` }],
2531
2604
  details: { error: "Invalid data" },
2532
2605
  };
2533
2606
  },
@@ -2589,6 +2662,7 @@ export default function (pi) {
2589
2662
  return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
2590
2663
  },
2591
2664
  });
2665
+ }
2592
2666
  if (isCommandEnabled(initConfig, "websearch"))
2593
2667
  pi.registerCommand("websearch", {
2594
2668
  description: "Open web search curator",