reasonix 0.30.2 → 0.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -590,10 +590,17 @@ declare class SessionStats {
590
590
  private _carryoverCost;
591
591
  /** Turn count from prior runs of a resumed session. */
592
592
  private _carryoverTurns;
593
+ private _carryoverCacheHit;
594
+ private _carryoverCacheMiss;
595
+ /** Last turn's promptTokens before exit — surfaced via summary() until the next live turn lands. */
596
+ private _carryoverLastPromptTokens;
593
597
  /** Seed totals from a resumed session's persisted meta — only call once at construction. */
594
598
  seedCarryover(opts: {
595
599
  totalCostUsd?: number;
596
600
  turnCount?: number;
601
+ cacheHitTokens?: number;
602
+ cacheMissTokens?: number;
603
+ lastPromptTokens?: number;
597
604
  }): void;
598
605
  record(turn: number, model: string, usage: Usage): TurnStats;
599
606
  get totalCost(): number;
@@ -1308,9 +1315,15 @@ interface WebFetchOptions {
1308
1315
  interface WebSearchOptions {
1309
1316
  topK?: number;
1310
1317
  signal?: AbortSignal;
1318
+ /** Backend engine: "mojeek" (scrapes Mojeek HTML) or "searxng" (self-hosted SearXNG JSON API). */
1319
+ engine?: "mojeek" | "searxng";
1320
+ /** Base URL for SearXNG. Default http://localhost:8080. */
1321
+ endpoint?: string;
1311
1322
  }
1312
1323
  /** Distinguishes "truly 0 results" from "layout changed / blocked" so callers can tell. */
1313
1324
  declare function webSearch(query: string, opts?: WebSearchOptions): Promise<SearchResult[]>;
1325
+ /** Parse SearXNG HTML search results using node-html-parser. */
1326
+ declare function parseSearxngHtmlResults(html: string): SearchResult[];
1314
1327
  /** Title-anchor + snippet-paragraph passes paired positionally — robust to attribute reorder. */
1315
1328
  declare function parseMojeekResults(html: string): SearchResult[];
1316
1329
  declare function webFetch(url: string, opts?: WebFetchOptions): Promise<PageContent>;
@@ -1320,6 +1333,10 @@ interface WebToolsOptions {
1320
1333
  defaultTopK?: number;
1321
1334
  /** Byte cap for `web_fetch` extracted text. */
1322
1335
  maxFetchChars?: number;
1336
+ /** Backend engine: "mojeek" (default, scrapes Mojeek) or "searxng" (self-hosted SearXNG). */
1337
+ webSearchEngine?: "mojeek" | "searxng";
1338
+ /** Base URL for SearXNG (default http://localhost:8080). */
1339
+ webSearchEndpoint?: string;
1323
1340
  }
1324
1341
  declare function registerWebTools(registry: ToolRegistry, opts?: WebToolsOptions): ToolRegistry;
1325
1342
  declare function formatSearchResults(query: string, results: SearchResult[]): string;
@@ -1343,6 +1360,11 @@ interface SessionMeta {
1343
1360
  workspace?: string;
1344
1361
  /** Wallet currency at last save — used to format `totalCostUsd` in the picker without re-fetching balance. */
1345
1362
  balanceCurrency?: string;
1363
+ /** Cumulative cache hit / miss tokens across the session — survives resume so /status cache% isn't 0 on a fresh boot. */
1364
+ cacheHitTokens?: number;
1365
+ cacheMissTokens?: number;
1366
+ /** Last turn's promptTokens — lets /status render the context bar before the next turn fires. */
1367
+ lastPromptTokens?: number;
1346
1368
  }
1347
1369
  declare function sessionsDir(): string;
1348
1370
  declare function sessionPath(name: string): string;
@@ -2013,6 +2035,10 @@ interface ReasonixConfig {
2013
2035
  session?: string | null;
2014
2036
  setupCompleted?: boolean;
2015
2037
  search?: boolean;
2038
+ /** Web search engine backend: "mojeek" (default, scrapes Mojeek) or "searxng" (self-hosted SearXNG). */
2039
+ webSearchEngine?: "mojeek" | "searxng";
2040
+ /** Base URL for SearXNG instance (default http://localhost:8080). */
2041
+ webSearchEndpoint?: string;
2016
2042
  projects?: {
2017
2043
  [absoluteRootDir: string]: {
2018
2044
  shellAllowed?: string[];
@@ -2169,4 +2195,4 @@ declare function aggregateUsage(records: UsageRecord[], opts?: AggregateOptions)
2169
2195
  /** File-size helper for the stats header — "1.2 MB" etc. Returns "" if missing. */
2170
2196
  declare function formatLogSize(path?: string): string;
2171
2197
 
2172
- export { AT_MENTION_PATTERN, AT_PICKER_PREFIX, type AggregateOptions, AppendOnlyLog, type AppendUsageInput, type ApplyResult, type ApplyStatus, type AtMentionExpansion, type AtMentionOptions, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, type ChoiceOption, ChoiceRequestedError, type ChoiceToolOptions, type CodeSystemPromptOptions, DEFAULT_AT_MENTION_MAX_BYTES, DEFAULT_MAX_RESULT_CHARS, DEFAULT_MAX_RESULT_TOKENS, DEFAULT_PICKER_IGNORE_DIRS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FileWithStats, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetLatestVersionOptions, type GetPromptResult, HOOK_EVENTS, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME, type HarvestOptions, type HookConfig, type HookEvent, type HookOutcome, type HookPayload, type HookReport, type HookScope, type HookSettings, type HookSpawnInput, type HookSpawnResult, type HookSpawner, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, LATEST_CACHE_TTL_MS, LATEST_FETCH_TIMEOUT_MS, type ListFilesOptions, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoadHookSettingsOptions, type LoopEvent, MCP_PROTOCOL_VERSION, MEMORY_INDEX_FILE, MEMORY_INDEX_MAX_CHARS, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type MemoryEntry, type MemoryScope, MemoryStore, type MemoryStoreOptions, type MemoryToolsOptions, type MemoryType, type WriteInput as MemoryWriteInput, NeedsConfirmationError, PROJECT_MEMORY_FILE, PROJECT_MEMORY_MAX_CHARS, type PageContent, type PickerCandidate, PlanProposedError, PlanRevisionProposedError, type PlanStep, type PlanStepRisk, type PlanToolOptions, type ProgressNotificationParams, type ProjectMemory, type RankPickerOptions, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type ResolvedHook, type RetryInfo, type RetryOptions, type Role, type RunCommandResult, type RunHooksOptions, type ScavengeOptions, type ScavengeResult, type SearchResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type ShellToolsOptions, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, type StepCompletion, StormBreaker, type StreamChunk, type StreamableHttpMcpSpec, StreamableHttpTransport, type StreamableHttpTransportOptions, type SubagentEvent, type SubagentSink, type SubagentToolOptions, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, USER_MEMORY_DIR, Usage, type UsageAggregate, type UsageBucket, type UsageRecord, VERSION, VolatileScratch, type WebFetchOptions, type WebSearchOptions, type WebToolsOptions, aggregateBranchUsage, aggregateUsage, analyzeSchema, appendSessionMessage, appendUsage, applyEditBlock, applyEditBlocks, applyMemoryStack, applyProjectMemory, applyUserMemory, bridgeMcpTools, bucketCacheHitRatio, bucketSavingsFraction, claudeEquivalentCost, codeSystemPrompt, compareVersions, computeReplayStats, costUsd, decideOutcome, defaultConfigPath, defaultSelector, defaultUsageLogPath, deleteSession, detectAtPicker, detectShellOperator, diffTranscripts, emptyPlanState, expandAtMentions, fetchWithRetry, fixToolCallPairing, flattenMcpResult, flattenSchema, forkRegistryExcluding, formatCommandResult, formatHookOutcomeMessage, formatLogSize, formatLoopError, formatSearchResults, getLatestVersion, globalSettingsPath, harvest, healLoadedMessages, healLoadedMessagesByTokens, htmlToText, injectPowerShellUtf8, inputCostUsd, inspectMcpServer, isAllowed, isJsonRpcError, isNpxInstall, isPlanStateEmpty, isPlausibleKey, listFilesSync, listFilesWithStatsAsync, listFilesWithStatsSync, listSessions, loadApiKey, loadDotenv, loadHooks, loadSessionMessages, matchesTool, memoryEnabled, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseMojeekResults, parseTranscript, prepareSpawn, projectHash, projectSettingsPath, quoteForCmdExe, rankPickerCandidates, readConfig, readProjectMemory, readTranscript, readUsageLog, recordFromLoopEvent, redactKey, registerChoiceTool, registerFilesystemTools, registerMemoryTools, registerPlanTool, registerShellTools, registerSubagentTool, registerWebTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, resolveExecutable, restoreSnapshots, runBranches, runCommand, runHooks, sanitizeMemoryName, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, tokenizeCommand, truncateForModel, truncateForModelByTokens, webFetch, webSearch, withUtf8Codepage, writeConfig, writeMeta, writeRecord };
2198
+ export { AT_MENTION_PATTERN, AT_PICKER_PREFIX, type AggregateOptions, AppendOnlyLog, type AppendUsageInput, type ApplyResult, type ApplyStatus, type AtMentionExpansion, type AtMentionOptions, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, type ChoiceOption, ChoiceRequestedError, type ChoiceToolOptions, type CodeSystemPromptOptions, DEFAULT_AT_MENTION_MAX_BYTES, DEFAULT_MAX_RESULT_CHARS, DEFAULT_MAX_RESULT_TOKENS, DEFAULT_PICKER_IGNORE_DIRS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FileWithStats, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetLatestVersionOptions, type GetPromptResult, HOOK_EVENTS, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME, type HarvestOptions, type HookConfig, type HookEvent, type HookOutcome, type HookPayload, type HookReport, type HookScope, type HookSettings, type HookSpawnInput, type HookSpawnResult, type HookSpawner, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, LATEST_CACHE_TTL_MS, LATEST_FETCH_TIMEOUT_MS, type ListFilesOptions, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoadHookSettingsOptions, type LoopEvent, MCP_PROTOCOL_VERSION, MEMORY_INDEX_FILE, MEMORY_INDEX_MAX_CHARS, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type MemoryEntry, type MemoryScope, MemoryStore, type MemoryStoreOptions, type MemoryToolsOptions, type MemoryType, type WriteInput as MemoryWriteInput, NeedsConfirmationError, PROJECT_MEMORY_FILE, PROJECT_MEMORY_MAX_CHARS, type PageContent, type PickerCandidate, PlanProposedError, PlanRevisionProposedError, type PlanStep, type PlanStepRisk, type PlanToolOptions, type ProgressNotificationParams, type ProjectMemory, type RankPickerOptions, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type ResolvedHook, type RetryInfo, type RetryOptions, type Role, type RunCommandResult, type RunHooksOptions, type ScavengeOptions, type ScavengeResult, type SearchResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type ShellToolsOptions, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, type StepCompletion, StormBreaker, type StreamChunk, type StreamableHttpMcpSpec, StreamableHttpTransport, type StreamableHttpTransportOptions, type SubagentEvent, type SubagentSink, type SubagentToolOptions, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, USER_MEMORY_DIR, Usage, type UsageAggregate, type UsageBucket, type UsageRecord, VERSION, VolatileScratch, type WebFetchOptions, type WebSearchOptions, type WebToolsOptions, aggregateBranchUsage, aggregateUsage, analyzeSchema, appendSessionMessage, appendUsage, applyEditBlock, applyEditBlocks, applyMemoryStack, applyProjectMemory, applyUserMemory, bridgeMcpTools, bucketCacheHitRatio, bucketSavingsFraction, claudeEquivalentCost, codeSystemPrompt, compareVersions, computeReplayStats, costUsd, decideOutcome, defaultConfigPath, defaultSelector, defaultUsageLogPath, deleteSession, detectAtPicker, detectShellOperator, diffTranscripts, emptyPlanState, expandAtMentions, fetchWithRetry, fixToolCallPairing, flattenMcpResult, flattenSchema, forkRegistryExcluding, formatCommandResult, formatHookOutcomeMessage, formatLogSize, formatLoopError, formatSearchResults, getLatestVersion, globalSettingsPath, harvest, healLoadedMessages, healLoadedMessagesByTokens, htmlToText, injectPowerShellUtf8, inputCostUsd, inspectMcpServer, isAllowed, isJsonRpcError, isNpxInstall, isPlanStateEmpty, isPlausibleKey, listFilesSync, listFilesWithStatsAsync, listFilesWithStatsSync, listSessions, loadApiKey, loadDotenv, loadHooks, loadSessionMessages, matchesTool, memoryEnabled, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseMojeekResults, parseSearxngHtmlResults, parseTranscript, prepareSpawn, projectHash, projectSettingsPath, quoteForCmdExe, rankPickerCandidates, readConfig, readProjectMemory, readTranscript, readUsageLog, recordFromLoopEvent, redactKey, registerChoiceTool, registerFilesystemTools, registerMemoryTools, registerPlanTool, registerShellTools, registerSubagentTool, registerWebTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, resolveExecutable, restoreSnapshots, runBranches, runCommand, runHooks, sanitizeMemoryName, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, tokenizeCommand, truncateForModel, truncateForModelByTokens, webFetch, webSearch, withUtf8Codepage, writeConfig, writeMeta, writeRecord };
package/dist/index.js CHANGED
@@ -1480,6 +1480,10 @@ var SessionStats = class {
1480
1480
  _carryoverCost = 0;
1481
1481
  /** Turn count from prior runs of a resumed session. */
1482
1482
  _carryoverTurns = 0;
1483
+ _carryoverCacheHit = 0;
1484
+ _carryoverCacheMiss = 0;
1485
+ /** Last turn's promptTokens before exit — surfaced via summary() until the next live turn lands. */
1486
+ _carryoverLastPromptTokens = 0;
1483
1487
  /** Seed totals from a resumed session's persisted meta — only call once at construction. */
1484
1488
  seedCarryover(opts) {
1485
1489
  if (typeof opts.totalCostUsd === "number" && opts.totalCostUsd > 0) {
@@ -1488,6 +1492,15 @@ var SessionStats = class {
1488
1492
  if (typeof opts.turnCount === "number" && opts.turnCount > 0) {
1489
1493
  this._carryoverTurns = opts.turnCount;
1490
1494
  }
1495
+ if (typeof opts.cacheHitTokens === "number" && opts.cacheHitTokens > 0) {
1496
+ this._carryoverCacheHit = opts.cacheHitTokens;
1497
+ }
1498
+ if (typeof opts.cacheMissTokens === "number" && opts.cacheMissTokens > 0) {
1499
+ this._carryoverCacheMiss = opts.cacheMissTokens;
1500
+ }
1501
+ if (typeof opts.lastPromptTokens === "number" && opts.lastPromptTokens > 0) {
1502
+ this._carryoverLastPromptTokens = opts.lastPromptTokens;
1503
+ }
1491
1504
  }
1492
1505
  record(turn, model, usage) {
1493
1506
  const cost = costUsd(model, usage);
@@ -1518,8 +1531,8 @@ var SessionStats = class {
1518
1531
  return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
1519
1532
  }
1520
1533
  get aggregateCacheHitRatio() {
1521
- let hit = 0;
1522
- let miss = 0;
1534
+ let hit = this._carryoverCacheHit;
1535
+ let miss = this._carryoverCacheMiss;
1523
1536
  for (const t of this.turns) {
1524
1537
  hit += t.usage.promptCacheHitTokens;
1525
1538
  miss += t.usage.promptCacheMissTokens;
@@ -1537,7 +1550,7 @@ var SessionStats = class {
1537
1550
  claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
1538
1551
  savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
1539
1552
  cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
1540
- lastPromptTokens: last?.usage.promptTokens ?? 0,
1553
+ lastPromptTokens: last?.usage.promptTokens ?? this._carryoverLastPromptTokens,
1541
1554
  lastTurnCostUsd: round(last?.cost ?? 0, 6)
1542
1555
  };
1543
1556
  }
@@ -2541,7 +2554,10 @@ var CacheFirstLoop = class {
2541
2554
  const meta = loadSessionMeta(this.sessionName);
2542
2555
  this.stats.seedCarryover({
2543
2556
  totalCostUsd: meta.totalCostUsd,
2544
- turnCount: meta.turnCount
2557
+ turnCount: meta.turnCount,
2558
+ cacheHitTokens: meta.cacheHitTokens,
2559
+ cacheMissTokens: meta.cacheMissTokens,
2560
+ lastPromptTokens: meta.lastPromptTokens
2545
2561
  });
2546
2562
  }
2547
2563
  if (healedCount > 0) {
@@ -4662,7 +4678,9 @@ function registerFilesystemTools(registry, opts) {
4662
4678
  const normRoot = pathMod3.resolve(rootDir);
4663
4679
  const rel = pathMod3.relative(normRoot, resolved);
4664
4680
  if (rel.startsWith("..") || pathMod3.isAbsolute(rel)) {
4665
- throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
4681
+ throw new Error(
4682
+ `path escapes sandbox root (${normRoot}): ${raw} \u2014 workspace is pinned at launch; quit and relaunch with \`reasonix code --dir <path>\` to work in a different folder`
4683
+ );
4666
4684
  }
4667
4685
  return resolved;
4668
4686
  };
@@ -5883,6 +5901,16 @@ function loadApiKey(path2 = defaultConfigPath()) {
5883
5901
  if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
5884
5902
  return readConfig(path2).apiKey;
5885
5903
  }
5904
+ function webSearchEngine(path2 = defaultConfigPath()) {
5905
+ const cfg = readConfig(path2).webSearchEngine;
5906
+ if (cfg === "searxng") return "searxng";
5907
+ return "mojeek";
5908
+ }
5909
+ function webSearchEndpoint(path2 = defaultConfigPath()) {
5910
+ const cfg = readConfig(path2).webSearchEndpoint;
5911
+ if (cfg && typeof cfg === "string") return cfg;
5912
+ return "http://localhost:8080";
5913
+ }
5886
5914
  function saveApiKey(key, path2 = defaultConfigPath()) {
5887
5915
  const cfg = readConfig(path2);
5888
5916
  cfg.apiKey = key.trim();
@@ -7290,6 +7318,12 @@ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
7290
7318
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
7291
7319
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
7292
7320
  async function webSearch(query, opts = {}) {
7321
+ if (opts.engine === "searxng") {
7322
+ return searchSearxng(query, opts);
7323
+ }
7324
+ return searchMojeek(query, opts);
7325
+ }
7326
+ async function searchMojeek(query, opts = {}) {
7293
7327
  const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
7294
7328
  const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
7295
7329
  headers: {
@@ -7314,6 +7348,90 @@ async function webSearch(query, opts = {}) {
7314
7348
  }
7315
7349
  return results;
7316
7350
  }
7351
+ function normalizeSearxngEndpoint(raw) {
7352
+ let url;
7353
+ try {
7354
+ url = new URL(raw.includes("://") ? raw : `http://${raw}`);
7355
+ } catch {
7356
+ throw new Error(`web_search: invalid SearXNG endpoint "${raw}"`);
7357
+ }
7358
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7359
+ throw new Error(`web_search: SearXNG endpoint must be http(s), got ${url.protocol}`);
7360
+ }
7361
+ return url.origin;
7362
+ }
7363
+ async function searchSearxng(query, opts = {}) {
7364
+ const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
7365
+ const baseUrl = normalizeSearxngEndpoint(opts.endpoint ?? "http://localhost:8080");
7366
+ const url = `${baseUrl}/search?format=html&q=${encodeURIComponent(query)}`;
7367
+ let resp;
7368
+ try {
7369
+ resp = await fetch(url, {
7370
+ headers: {
7371
+ "User-Agent": USER_AGENT,
7372
+ Accept: "text/html"
7373
+ },
7374
+ signal: opts.signal
7375
+ });
7376
+ } catch (err) {
7377
+ if (err instanceof TypeError && err.message.includes("fetch")) {
7378
+ throw new Error(
7379
+ `web_search: Cannot reach SearXNG server at ${opts.endpoint ?? "http://localhost:8080"}. Please install SearXNG (https://github.com/searxng/searxng) and start it (e.g. \`docker run -d -p 8080:8080 searxng/searxng\`), or switch to the default engine with /search-engine mojeek.`
7380
+ );
7381
+ }
7382
+ throw err;
7383
+ }
7384
+ if (!resp.ok) throw new Error(`web_search ${resp.status}`);
7385
+ const html = await resp.text();
7386
+ const results = parseSearxngHtmlResults(html).slice(0, topK);
7387
+ if (results.length === 0) {
7388
+ if (/no results found|did not match any documents/i.test(html)) return [];
7389
+ throw new Error(
7390
+ `web_search: 0 results but SearXNG response doesn't look like an empty results page (${html.length} chars)`
7391
+ );
7392
+ }
7393
+ return results;
7394
+ }
7395
+ function parseSearxngHtmlResults(html) {
7396
+ const root = parseHtml(html);
7397
+ const results = [];
7398
+ const articles = root.querySelectorAll("article.result, div.result");
7399
+ if (articles.length > 0) {
7400
+ for (const article of articles) {
7401
+ const link = article.querySelector("h3 a, h4 a, a[href^='http']");
7402
+ if (!link) continue;
7403
+ const href = link.getAttribute("href");
7404
+ if (!href) continue;
7405
+ const title = link.textContent.trim();
7406
+ if (!title) continue;
7407
+ let snippet = "";
7408
+ for (const p of article.querySelectorAll("p")) {
7409
+ const text = p.textContent.trim();
7410
+ if (text.length > 10 && !text.includes(title)) {
7411
+ snippet = text;
7412
+ break;
7413
+ }
7414
+ }
7415
+ if (!snippet) {
7416
+ const cs = article.querySelector(".content, .result-content, [class*='snippet']");
7417
+ if (cs) snippet = cs.textContent.trim();
7418
+ }
7419
+ results.push({ title, url: href, snippet });
7420
+ }
7421
+ return results;
7422
+ }
7423
+ for (const a of root.querySelectorAll("h3 a[href]")) {
7424
+ const href = a.getAttribute("href");
7425
+ if (!href || href.startsWith("#")) continue;
7426
+ const title = a.textContent.trim();
7427
+ if (!title) continue;
7428
+ let snippet = "";
7429
+ const p = a.parentNode?.parentNode?.querySelector("p");
7430
+ if (p) snippet = p.textContent.trim();
7431
+ results.push({ title, url: href, snippet });
7432
+ }
7433
+ return results;
7434
+ }
7317
7435
  function parseMojeekResults(html) {
7318
7436
  const titles = [];
7319
7437
  const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
@@ -7487,7 +7605,7 @@ function registerWebTools(registry, opts = {}) {
7487
7605
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
7488
7606
  registry.register({
7489
7607
  name: "web_search",
7490
- description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this.",
7608
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /web-search-engine mojeek|searxng.",
7491
7609
  readOnly: true,
7492
7610
  parallelSafe: true,
7493
7611
  parameters: {
@@ -7502,9 +7620,13 @@ function registerWebTools(registry, opts = {}) {
7502
7620
  required: ["query"]
7503
7621
  },
7504
7622
  fn: async (args, ctx) => {
7623
+ const engine = opts.webSearchEngine ?? webSearchEngine();
7624
+ const endpoint = opts.webSearchEndpoint ?? webSearchEndpoint();
7505
7625
  const results = await webSearch(args.query, {
7506
7626
  topK: args.topK ?? defaultTopK,
7507
- signal: ctx?.signal
7627
+ signal: ctx?.signal,
7628
+ engine,
7629
+ endpoint
7508
7630
  });
7509
7631
  return formatSearchResults(args.query, results);
7510
7632
  }
@@ -9649,6 +9771,7 @@ export {
9649
9771
  parseEditBlocks,
9650
9772
  parseMcpSpec,
9651
9773
  parseMojeekResults,
9774
+ parseSearxngHtmlResults,
9652
9775
  parseTranscript,
9653
9776
  prepareSpawn,
9654
9777
  projectHash,