@zhijiewang/openharness 2.22.1 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  * Flushed messages flow to scrollback; live area is rewritten in-place
4
4
  * right after the scrollback content each frame (no absolute positioning gap).
5
5
  */
6
+ import { recordApproval } from "../harness/approvals.js";
6
7
  import { getTheme } from "../utils/theme-data.js";
7
8
  import { summarizeToolArgs } from "../utils/tool-summary.js";
8
9
  import { CellGrid } from "./cells.js";
@@ -62,6 +63,7 @@ export class TerminalRenderer {
62
63
  questionPrompt: null,
63
64
  autocomplete: [],
64
65
  autocompleteDescriptions: [],
66
+ autocompleteCategories: [],
65
67
  autocompleteIndex: -1,
66
68
  manualScroll: 0,
67
69
  codeBlocksExpanded: false,
@@ -195,9 +197,10 @@ export class TerminalRenderer {
195
197
  this.state.bannerLines = lines;
196
198
  this.scheduleRender();
197
199
  }
198
- setAutocomplete(suggestions, index, descriptions) {
200
+ setAutocomplete(suggestions, index, descriptions, categories) {
199
201
  this.state.autocomplete = suggestions;
200
202
  this.state.autocompleteDescriptions = descriptions ?? [];
203
+ this.state.autocompleteCategories = categories ?? [];
201
204
  this.state.autocompleteIndex = index;
202
205
  this.scheduleRender();
203
206
  }
@@ -366,20 +369,48 @@ export class TerminalRenderer {
366
369
  this.animationCallback = handler;
367
370
  }
368
371
  // ── Input routing ──
369
- /** Handle permission prompt keys (Y/N/D). Returns true if key was consumed. */
372
+ /**
373
+ * Handle permission prompt keys (Y/N/A/D).
374
+ * - Y / N: approve or deny this single call.
375
+ * - A: approve AND persist a `toolPermissions: { tool, action: "allow" }`
376
+ * rule to `.oh/config.yaml` so future calls to this tool skip the prompt
377
+ * entirely (audit U-A2). Mirrors Claude Code's "yes, don't ask again".
378
+ * - D: toggle inline diff (when available).
379
+ *
380
+ * Returns true if key was consumed.
381
+ */
370
382
  handlePermissionKey(key) {
371
383
  if (!this.permissionResolve)
372
384
  return false;
373
385
  const k = key.char.toLowerCase();
374
- if (k === "y" || k === "n") {
386
+ if (k === "y" || k === "n" || k === "a") {
375
387
  const resolve = this.permissionResolve;
388
+ const toolName = this.state.permissionBox?.toolName;
376
389
  this.permissionResolve = null;
377
390
  this.permissionPrompt = null;
378
391
  this.state.permissionBox = null;
379
392
  this.state.permissionDiffVisible = false;
380
393
  this.state.permissionDiffInfo = null;
394
+ // Persist before resolving — any error in the write should not block
395
+ // the resolution. The persist call itself is no-op when no .oh/config.yaml
396
+ // exists (we don't auto-create on first interaction).
397
+ if (k === "a" && toolName) {
398
+ try {
399
+ // Lazy import to avoid pulling config into the renderer bundle
400
+ // for callers that don't trip the permission path.
401
+ const { appendToolPermission } = require("../harness/config.js");
402
+ appendToolPermission(toolName);
403
+ }
404
+ catch {
405
+ /* persistence failure must not block the agent */
406
+ }
407
+ // Audit U-B5: log the "always allow this tool" rule promotion as a
408
+ // supplementary record so /permissions log shows the user upgraded
409
+ // from a one-shot allow to a persistent rule.
410
+ recordApproval({ tool: toolName, decision: "always", source: "user", cwd: process.cwd() });
411
+ }
381
412
  this.scheduleRender();
382
- resolve(k === "y");
413
+ resolve(k === "y" || k === "a");
383
414
  }
384
415
  else if (k === "d" && this.state.permissionDiffInfo) {
385
416
  this.state.permissionDiffVisible = !this.state.permissionDiffVisible;
@@ -626,6 +657,8 @@ export class TerminalRenderer {
626
657
  if (this.state.statusLine)
627
658
  rows += 1;
628
659
  rows += this.state.autocomplete.length;
660
+ // Audit U-A3: one row per distinct category header.
661
+ rows += new Set((this.state.autocompleteCategories ?? []).filter((c) => c && c.length > 0)).size;
629
662
  if (this.state.permissionBox) {
630
663
  rows += 3;
631
664
  if (this.state.permissionDiffVisible && this.state.permissionDiffInfo)
@@ -84,6 +84,13 @@ export function parseKey(data, offset) {
84
84
  return { event: key("", "pageup", seq.slice(0, 4)), consumed: 4 };
85
85
  if (seq.startsWith("\x1b[6~"))
86
86
  return { event: key("", "pagedown", seq.slice(0, 4)), consumed: 4 };
87
+ // Shift+Tab (xterm "backtab"): ESC [ Z. Used as a quick-toggle for
88
+ // permission mode (mirrors Claude Code's Shift+Tab cycler).
89
+ if (seq.startsWith("\x1b[Z"))
90
+ return {
91
+ event: { char: "", name: "tab", ctrl: false, meta: false, shift: true, sequence: seq.slice(0, 3) },
92
+ consumed: 3,
93
+ };
87
94
  // Shift+Arrow: ESC [ 1 ; 2 A/B/C/D
88
95
  if (seq.startsWith("\x1b[1;2A"))
89
96
  return {
@@ -187,7 +187,7 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
187
187
  for (const line of visible) {
188
188
  if (r >= limit)
189
189
  break;
190
- grid.writeText(r, 6, line.slice(0, w - 8), S_DIM);
190
+ grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), S_DIM, w - 2);
191
191
  r++;
192
192
  }
193
193
  }
@@ -205,7 +205,7 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
205
205
  if (r >= limit)
206
206
  break;
207
207
  const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
208
- grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
208
+ grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), lineStyle, w - 2);
209
209
  r++;
210
210
  }
211
211
  if (outLines.length > maxOut && r < limit) {
@@ -280,6 +280,13 @@ export function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
280
280
  kc += 1;
281
281
  grid.writeText(nextRow, kc, "o", S_DIM);
282
282
  kc += 1;
283
+ grid.writeText(nextRow, kc, " ", S_DIM);
284
+ kc += 2;
285
+ // Audit U-A2: "always allow this tool" — persists toolPermissions rule.
286
+ grid.writeText(nextRow, kc, "A", S_KEY_GREEN);
287
+ kc += 1;
288
+ grid.writeText(nextRow, kc, "lways", S_DIM);
289
+ kc += 5;
283
290
  if (state.permissionDiffInfo) {
284
291
  grid.writeText(nextRow, kc, " ", S_DIM);
285
292
  kc += 2;
@@ -300,10 +307,12 @@ export function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
300
307
  grid.writeText(nextRow, 1, "Y", S_KEY_GREEN);
301
308
  grid.writeText(nextRow, 2, "es ", S_DIM);
302
309
  grid.writeText(nextRow, 6, "N", S_KEY_RED);
303
- grid.writeText(nextRow, 7, "o", S_DIM);
310
+ grid.writeText(nextRow, 7, "o ", S_DIM);
311
+ grid.writeText(nextRow, 10, "A", S_KEY_GREEN);
312
+ grid.writeText(nextRow, 11, "lways", S_DIM);
304
313
  if (state.permissionDiffInfo) {
305
- grid.writeText(nextRow, 10, "D", S_KEY_CYAN);
306
- grid.writeText(nextRow, 11, "iff", S_DIM);
314
+ grid.writeText(nextRow, 18, "D", S_KEY_CYAN);
315
+ grid.writeText(nextRow, 19, "iff", S_DIM);
307
316
  }
308
317
  nextRow++;
309
318
  if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
@@ -375,7 +384,20 @@ export function renderAutocompleteSection(state, grid, nextRow, limit, promptWid
375
384
  if (state.autocomplete.length === 0)
376
385
  return nextRow;
377
386
  const w = grid.width;
387
+ let lastCategory = "";
378
388
  for (let ai = 0; ai < state.autocomplete.length; ai++) {
389
+ if (nextRow >= limit)
390
+ break;
391
+ // Category header — draw whenever the category changes between entries.
392
+ // First-entry header is drawn when the category is non-empty (audit U-A3).
393
+ const cat = state.autocompleteCategories?.[ai] ?? "";
394
+ if (cat && cat !== lastCategory) {
395
+ if (nextRow >= limit)
396
+ break;
397
+ grid.writeText(nextRow, promptWidth, `── ${cat} ──`, S_DIM);
398
+ nextRow++;
399
+ lastCategory = cat;
400
+ }
379
401
  if (nextRow >= limit)
380
402
  break;
381
403
  const cmd = state.autocomplete[ai];
@@ -57,6 +57,14 @@ export type LayoutState = {
57
57
  } | null;
58
58
  autocomplete: string[];
59
59
  autocompleteDescriptions: string[];
60
+ /**
61
+ * Optional category label per autocomplete entry (audit U-A3). When two
62
+ * adjacent entries differ in category, the renderer draws a header line
63
+ * before the second. Empty / missing category strings render flat (the
64
+ * pre-A3 behavior). Optional so older test fixtures + non-REPL callers
65
+ * don't need to thread an empty array.
66
+ */
67
+ autocompleteCategories?: string[];
60
68
  autocompleteIndex: number;
61
69
  manualScroll: number;
62
70
  codeBlocksExpanded: boolean;
@@ -28,7 +28,11 @@ export function rasterize(state, grid) {
28
28
  const questionHeight = state.questionPrompt ? 4 + (state.questionPrompt.options?.length ?? 0) : 0;
29
29
  const statusLineHeight = state.statusLine ? 1 : 0;
30
30
  const contextWarningHeight = state.contextWarning ? 1 : 0;
31
- const autocompleteHeight = state.autocomplete.length;
31
+ // Autocomplete height — each entry is one row, plus one extra row per
32
+ // distinct category (audit U-A3 header lines). Distinct-category count
33
+ // is bounded by entry count so this stays cheap.
34
+ const distinctCategories = new Set((state.autocompleteCategories ?? []).filter((c) => c && c.length > 0));
35
+ const autocompleteHeight = state.autocomplete.length + distinctCategories.size;
32
36
  const inputLineCount = Math.min(5, (state.inputText.match(/\n/g)?.length ?? 0) + 1);
33
37
  const rawFooterHeight = Math.max(2 + inputLineCount + statusLineHeight + autocompleteHeight, companionHeight + 1) +
34
38
  permissionHeight +
@@ -241,7 +241,10 @@ function parseInline(text, baseStyle) {
241
241
  // Link: [text](url)
242
242
  const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
243
243
  if (linkMatch) {
244
- segments.push({ text: linkMatch[1], style: { ...baseStyle, underline: true, fg: "cyan" } });
244
+ segments.push({
245
+ text: linkMatch[1],
246
+ style: { ...baseStyle, underline: true, fg: "cyan", hyperlink: linkMatch[2] },
247
+ });
245
248
  segments.push({ text: ` (${linkMatch[2]})`, style: { ...baseStyle, dim: true } });
246
249
  remaining = remaining.slice(linkMatch[0].length);
247
250
  continue;
package/dist/repl.js CHANGED
@@ -14,8 +14,10 @@ import { readOhConfig, writeOhConfig } from "./harness/config.js";
14
14
  import { estimateMessageTokens, getContextWarning } from "./harness/context-warning.js";
15
15
  import { CostTracker, estimateCost, getContextWindow } from "./harness/cost.js";
16
16
  import { createSession, loadSession, saveSession } from "./harness/session.js";
17
+ import { runStatusLineScript } from "./harness/status-line-script.js";
17
18
  import { createStore } from "./harness/store.js";
18
19
  import { handleUserInput } from "./harness/submit-handler.js";
20
+ import { isTrusted, trustSystemActive } from "./harness/trust.js";
19
21
  import { query } from "./query/index.js";
20
22
  import { resetDiffStyleCache } from "./renderer/diff.js";
21
23
  import { TerminalRenderer } from "./renderer/index.js";
@@ -23,6 +25,7 @@ import { resetStyleCache } from "./renderer/layout.js";
23
25
  import { resetMdStyleCache } from "./renderer/markdown.js";
24
26
  import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
25
27
  import { formatTokenCount } from "./utils/format.js";
28
+ import { fuzzyFilter } from "./utils/fuzzy.js";
26
29
  import { setActiveTheme } from "./utils/theme-data.js";
27
30
  import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
28
31
  export async function startREPL(config) {
@@ -105,6 +108,9 @@ export async function startREPL(config) {
105
108
  let fastMode = s().fastMode;
106
109
  let acSuggestions = s().acSuggestions;
107
110
  let acDescriptions = s().acDescriptions;
111
+ // Audit U-A3: parallel category array for the picker. Local-only — no
112
+ // need to round-trip through `store` since no other consumer reads it.
113
+ let acCategories = [];
108
114
  let acIndex = s().acIndex;
109
115
  let acTokenStart = s().acTokenStart;
110
116
  let acIsPath = s().acIsPath;
@@ -128,13 +134,15 @@ export async function startREPL(config) {
128
134
  function updateAutocomplete() {
129
135
  acIsPath = false;
130
136
  if (inputText.startsWith("/") && inputText.length > 1 && !inputText.includes(" ")) {
131
- // Slash command autocomplete
132
- const prefix = inputText.slice(1).toLowerCase();
133
- const entries = getCommandEntries()
134
- .filter((e) => e.name.startsWith(prefix))
135
- .slice(0, 5);
136
- acSuggestions = entries.map((e) => e.name);
137
- acDescriptions = entries.map((e) => e.description);
137
+ // Slash command autocomplete (audit U-B3): subsequence-match scoring,
138
+ // not a startsWith filter. Prefix matches still rank first via the
139
+ // bonus in `fuzzyScore`, but the user can type "gst" to surface
140
+ // "/git-status" or "perm" to surface "/permissions".
141
+ const query = inputText.slice(1);
142
+ const ranked = fuzzyFilter(query, getCommandEntries()).slice(0, 8);
143
+ acSuggestions = ranked.map((r) => r.entry.name);
144
+ acDescriptions = ranked.map((r) => r.entry.description);
145
+ acCategories = ranked.map((r) => r.entry.category);
138
146
  acTokenStart = 0;
139
147
  acIndex = -1;
140
148
  }
@@ -176,26 +184,30 @@ export async function startREPL(config) {
176
184
  return "";
177
185
  }
178
186
  });
187
+ acCategories = [];
179
188
  acIsPath = acSuggestions.length > 0;
180
189
  }
181
190
  catch {
182
191
  acSuggestions = [];
183
192
  acDescriptions = [];
193
+ acCategories = [];
184
194
  }
185
195
  acIndex = -1;
186
196
  }
187
197
  else {
188
198
  acSuggestions = [];
189
199
  acDescriptions = [];
200
+ acCategories = [];
190
201
  acIndex = -1;
191
202
  }
192
203
  }
193
204
  else {
194
205
  acSuggestions = [];
195
206
  acDescriptions = [];
207
+ acCategories = [];
196
208
  acIndex = -1;
197
209
  }
198
- renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions);
210
+ renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions, acCategories);
199
211
  }
200
212
  // Companion
201
213
  let companionVisible = true;
@@ -263,8 +275,38 @@ export async function startREPL(config) {
263
275
  const pct = Math.max(1, Math.ceil(usage * 100));
264
276
  ctxStr = `ctx [${bar}] ${pct}%`;
265
277
  }
266
- // Use template if configured, otherwise default format
267
- if (cachedConfig?.statusLineFormat) {
278
+ // Resolution priority: script (audit U-B1) → template → default.
279
+ //
280
+ // Script path: spawn user-configured shell with a JSON envelope on
281
+ // stdin; gated through the workspace-trust system from audit U-A4 so
282
+ // a hostile project can't auto-execute on first launch. Cached for
283
+ // `refreshMs` (default 1s) inside `status-line-script.ts` so the
284
+ // script doesn't run on every keypress. Failure → fall through to
285
+ // the template / default below.
286
+ let scriptLine = null;
287
+ const sl = cachedConfig?.statusLine;
288
+ if (sl?.command) {
289
+ const cwd = process.cwd();
290
+ if (trustSystemActive() && !isTrusted(cwd)) {
291
+ scriptLine = null; // untrusted — silently skip; user can /trust
292
+ }
293
+ else {
294
+ const ctxPct = ctxWindow > 0 && estimatedTokenCount > 0 ? estimatedTokenCount / ctxWindow : 0;
295
+ scriptLine = runStatusLineScript({
296
+ model: currentModel || "",
297
+ tokens: { input: inTok, output: outTok },
298
+ cost: totalCostVal,
299
+ contextPercent: ctxPct,
300
+ sessionId: session.id,
301
+ cwd,
302
+ gitBranch: session.gitBranch,
303
+ }, sl);
304
+ }
305
+ }
306
+ if (scriptLine !== null) {
307
+ renderer.setStatusLine(scriptLine);
308
+ }
309
+ else if (cachedConfig?.statusLineFormat) {
268
310
  const line = cachedConfig.statusLineFormat
269
311
  .replace("{model}", currentModel || "")
270
312
  .replace("{tokens}", tokensStr)
@@ -516,6 +558,14 @@ export async function startREPL(config) {
516
558
  }
517
559
  if (key.name === "pageup" || key.name === "pagedown" || key.name === "mouse")
518
560
  return;
561
+ // Shift+Tab: cycle permission mode (audit U-A1). Mirrors Claude Code's
562
+ // quick-toggle. Cycles ask → acceptEdits → plan → trust → ask. The
563
+ // session-level mode is mutated on `config` so all downstream callers
564
+ // (`query()`, `cronExecutor`, status line) read the new value.
565
+ if (key.name === "tab" && key.shift) {
566
+ cyclePermissionMode();
567
+ return;
568
+ }
519
569
  // Tab: autocomplete slash commands or file paths, or cycle tool call expansion
520
570
  if (key.name === "tab" && !loading) {
521
571
  if (acSuggestions.length > 0) {
@@ -533,7 +583,7 @@ export async function startREPL(config) {
533
583
  }
534
584
  renderer.setInputText(inputText);
535
585
  renderer.setInputCursor(inputCursor);
536
- renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions);
586
+ renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions, acCategories);
537
587
  return;
538
588
  }
539
589
  renderer.cycleToolCallExpansion();
@@ -621,6 +671,26 @@ export async function startREPL(config) {
621
671
  acIsPath,
622
672
  });
623
673
  });
674
+ /**
675
+ * Cycle the session permission mode (audit U-A1, Shift+Tab). The cycle
676
+ * intentionally covers the four interactive modes a user is likely to
677
+ * toggle between — `ask`, `acceptEdits`, `plan`, `trust`. The other modes
678
+ * (`deny`, `auto`, `bypassPermissions`) stay reachable via `/permissions
679
+ * <mode>` but aren't on the quick-cycle path because they're either
680
+ * destructive (`bypassPermissions`) or seldom-used.
681
+ *
682
+ * Mutates `config.permissionMode` directly so every existing read site
683
+ * (the `query()` call sites, `cronExecutor`, status hints) sees the new
684
+ * value without extra plumbing.
685
+ */
686
+ function cyclePermissionMode() {
687
+ const cycle = ["ask", "acceptEdits", "plan", "trust"];
688
+ const idx = cycle.indexOf(config.permissionMode);
689
+ const next = cycle[(idx === -1 ? 0 : idx + 1) % cycle.length];
690
+ config.permissionMode = next;
691
+ messages.push(createInfoMessage(`Permission mode → ${next}`));
692
+ syncRenderer();
693
+ }
624
694
  function navigateHistory(dir) {
625
695
  if (dir < 0 && historyIndex < inputHistory.length - 1) {
626
696
  historyIndex++;
@@ -1063,5 +1133,39 @@ export async function startREPL(config) {
1063
1133
  renderer.start();
1064
1134
  // Banner is already printed to stdout by main.tsx (visible in terminal scrollback)
1065
1135
  syncRenderer();
1136
+ // Workspace-trust prompt (audit U-A4). Fires once per session when:
1137
+ // - the cwd isn't already on the trust list, AND
1138
+ // - `.oh/config.yaml` defines at least one shell-executing hook
1139
+ // (command/http) — `prompt` hooks don't trip the gate.
1140
+ // Untrusted cwd silently skips command/http hooks via the gate in
1141
+ // `harness/hooks.ts`. The prompt is non-blocking: we fire-and-forget
1142
+ // the askQuestion so the REPL stays responsive while the question is
1143
+ // displayed.
1144
+ void (async () => {
1145
+ try {
1146
+ const { isTrusted, trust } = await import("./harness/trust.js");
1147
+ if (isTrusted(process.cwd()))
1148
+ return;
1149
+ const cfgWithHooks = readOhConfig();
1150
+ const hooks = cfgWithHooks?.hooks;
1151
+ if (!hooks)
1152
+ return;
1153
+ const hasShellHook = Object.values(hooks).some((defs) => Array.isArray(defs) && defs.some((d) => d.command || d.http));
1154
+ if (!hasShellHook)
1155
+ return;
1156
+ const answer = await renderer.askQuestion(`Trust this workspace? Shell hooks are configured in ${process.cwd()}. (yes/no)`);
1157
+ if (answer.toLowerCase().startsWith("y")) {
1158
+ trust(process.cwd());
1159
+ messages.push(createInfoMessage(`Trusted ${process.cwd()} — shell hooks will now execute.`));
1160
+ }
1161
+ else {
1162
+ messages.push(createInfoMessage(`Workspace not trusted — shell hooks are silently skipped. Run /trust to grant.`));
1163
+ }
1164
+ syncRenderer();
1165
+ }
1166
+ catch {
1167
+ /* trust prompt is best-effort; never block the REPL */
1168
+ }
1169
+ })();
1066
1170
  }
1067
1171
  //# sourceMappingURL=repl.js.map
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "../../Tool.js";
3
+ declare const SEARCH_TYPES: readonly ["auto", "neural", "fast", "keyword"];
4
+ declare const CATEGORIES: readonly ["company", "research paper", "news", "personal site", "financial report", "people"];
5
+ declare const inputSchema: z.ZodObject<{
6
+ query: z.ZodString;
7
+ num_results: z.ZodOptional<z.ZodNumber>;
8
+ type: z.ZodOptional<z.ZodEnum<["auto", "neural", "fast", "keyword"]>>;
9
+ category: z.ZodOptional<z.ZodEnum<["company", "research paper", "news", "personal site", "financial report", "people"]>>;
10
+ include_domains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
11
+ exclude_domains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
12
+ include_text: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ exclude_text: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
+ start_published_date: z.ZodOptional<z.ZodString>;
15
+ end_published_date: z.ZodOptional<z.ZodString>;
16
+ user_location: z.ZodOptional<z.ZodString>;
17
+ text: z.ZodOptional<z.ZodBoolean>;
18
+ highlights: z.ZodOptional<z.ZodBoolean>;
19
+ summary: z.ZodOptional<z.ZodBoolean>;
20
+ summary_query: z.ZodOptional<z.ZodString>;
21
+ max_text_chars: z.ZodOptional<z.ZodNumber>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ query: string;
24
+ type?: "auto" | "fast" | "neural" | "keyword" | undefined;
25
+ text?: boolean | undefined;
26
+ summary?: boolean | undefined;
27
+ num_results?: number | undefined;
28
+ category?: "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | undefined;
29
+ include_domains?: string[] | undefined;
30
+ exclude_domains?: string[] | undefined;
31
+ include_text?: string[] | undefined;
32
+ exclude_text?: string[] | undefined;
33
+ start_published_date?: string | undefined;
34
+ end_published_date?: string | undefined;
35
+ user_location?: string | undefined;
36
+ highlights?: boolean | undefined;
37
+ summary_query?: string | undefined;
38
+ max_text_chars?: number | undefined;
39
+ }, {
40
+ query: string;
41
+ type?: "auto" | "fast" | "neural" | "keyword" | undefined;
42
+ text?: boolean | undefined;
43
+ summary?: boolean | undefined;
44
+ num_results?: number | undefined;
45
+ category?: "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | undefined;
46
+ include_domains?: string[] | undefined;
47
+ exclude_domains?: string[] | undefined;
48
+ include_text?: string[] | undefined;
49
+ exclude_text?: string[] | undefined;
50
+ start_published_date?: string | undefined;
51
+ end_published_date?: string | undefined;
52
+ user_location?: string | undefined;
53
+ highlights?: boolean | undefined;
54
+ summary_query?: string | undefined;
55
+ max_text_chars?: number | undefined;
56
+ }>;
57
+ type ExaContents = {
58
+ text?: boolean | {
59
+ maxCharacters?: number;
60
+ };
61
+ highlights?: boolean | {
62
+ maxCharacters?: number;
63
+ };
64
+ summary?: boolean | {
65
+ query?: string;
66
+ };
67
+ };
68
+ type ExaRequest = {
69
+ query: string;
70
+ numResults: number;
71
+ type?: (typeof SEARCH_TYPES)[number];
72
+ category?: (typeof CATEGORIES)[number];
73
+ includeDomains?: string[];
74
+ excludeDomains?: string[];
75
+ includeText?: string[];
76
+ excludeText?: string[];
77
+ startPublishedDate?: string;
78
+ endPublishedDate?: string;
79
+ userLocation?: string;
80
+ contents?: ExaContents;
81
+ };
82
+ type ExaResultItem = {
83
+ id?: string;
84
+ url: string;
85
+ title?: string | null;
86
+ publishedDate?: string;
87
+ author?: string | null;
88
+ text?: string;
89
+ highlights?: string[];
90
+ summary?: string;
91
+ };
92
+ type ExaResponse = {
93
+ results: ExaResultItem[];
94
+ requestId?: string;
95
+ };
96
+ export declare function buildRequestBody(input: z.infer<typeof inputSchema>): ExaRequest;
97
+ export declare function extractSnippet(item: ExaResultItem): string;
98
+ export declare function formatResults(response: ExaResponse): string;
99
+ export declare const ExaSearchTool: Tool<typeof inputSchema>;
100
+ export {};
101
+ //# sourceMappingURL=index.d.ts.map