@oh-my-pi/pi-coding-agent 15.10.5 → 15.10.6

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 (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/types/exa/index.d.ts +1 -19
  3. package/dist/types/exa/mcp-client.d.ts +10 -3
  4. package/dist/types/exa/types.d.ts +0 -83
  5. package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
  6. package/dist/types/modes/interactive-mode.d.ts +8 -0
  7. package/dist/types/modes/types.d.ts +1 -0
  8. package/dist/types/task/render.d.ts +1 -0
  9. package/dist/types/tools/index.d.ts +0 -2
  10. package/dist/types/utils/git.d.ts +6 -0
  11. package/package.json +9 -9
  12. package/src/cli/auth-gateway-cli.ts +3 -2
  13. package/src/commit/agentic/tools/split-commit.ts +8 -1
  14. package/src/config/model-provider-priority.ts +1 -0
  15. package/src/exa/index.ts +1 -26
  16. package/src/exa/mcp-client.ts +10 -10
  17. package/src/exa/types.ts +0 -97
  18. package/src/internal-urls/docs-index.generated.ts +1 -1
  19. package/src/modes/components/agent-dashboard.ts +6 -4
  20. package/src/modes/controllers/event-controller.ts +8 -0
  21. package/src/modes/controllers/input-controller.ts +24 -1
  22. package/src/modes/controllers/mcp-command-controller.ts +24 -5
  23. package/src/modes/interactive-mode.ts +33 -1
  24. package/src/modes/types.ts +1 -0
  25. package/src/session/agent-session.ts +77 -41
  26. package/src/slash-commands/builtin-registry.ts +8 -0
  27. package/src/task/index.ts +9 -1
  28. package/src/task/render.ts +22 -12
  29. package/src/tools/index.ts +0 -4
  30. package/src/utils/git.ts +41 -0
  31. package/dist/types/exa/factory.d.ts +0 -13
  32. package/dist/types/exa/render.d.ts +0 -19
  33. package/dist/types/exa/researcher.d.ts +0 -9
  34. package/dist/types/exa/search.d.ts +0 -9
  35. package/dist/types/exa/websets.d.ts +0 -9
  36. package/src/exa/factory.ts +0 -60
  37. package/src/exa/render.ts +0 -244
  38. package/src/exa/researcher.ts +0 -36
  39. package/src/exa/search.ts +0 -47
  40. package/src/exa/websets.ts +0 -248
@@ -65,7 +65,12 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
65
65
  import type { Goal, GoalModeState } from "../goals/state";
66
66
  import { resolveLocalUrlToPath } from "../internal-urls";
67
67
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
68
- import { humanizePlanTitle, type PlanApprovalDetails, resolveApprovedPlan } from "../plan-mode/approved-plan";
68
+ import {
69
+ humanizePlanTitle,
70
+ type PlanApprovalDetails,
71
+ resolveApprovedPlan,
72
+ resolvePlanTitle,
73
+ } from "../plan-mode/approved-plan";
69
74
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
70
75
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
71
76
  type: "text",
@@ -2265,6 +2270,33 @@ export class InteractiveMode implements InteractiveModeContext {
2265
2270
  await this.#startGoalFromObjective(objective);
2266
2271
  }
2267
2272
 
2273
+ /** Manually (re-)open the plan-review overlay — bound to `/plan-review`. Lets
2274
+ * the operator pull the review back up after dismissing it, or review a plan
2275
+ * the agent wrote without calling `resolve`. There is no fixed plan filename:
2276
+ * `getPlanReferencePath()` is empty until a plan is actually approved (and does
2277
+ * not survive a restart), so this drives off the newest `local://<slug>-plan.md`
2278
+ * the agent wrote — the files persist in the session artifacts dir, so the scan
2279
+ * works before any review and across restarts. */
2280
+ async openPlanReview(): Promise<void> {
2281
+ if (!this.planModeEnabled) {
2282
+ this.showWarning("Plan mode is not active.");
2283
+ return;
2284
+ }
2285
+ const noPlan = "No plan to review yet — write one to a local://<slug>-plan.md file first.";
2286
+ const [planFilePath] = await this.#listLocalPlanFiles();
2287
+ if (!planFilePath) {
2288
+ this.showWarning(noPlan);
2289
+ return;
2290
+ }
2291
+ const planContent = await this.#readPlanFile(planFilePath);
2292
+ if (planContent === null) {
2293
+ this.showWarning(noPlan);
2294
+ return;
2295
+ }
2296
+ const { title } = resolvePlanTitle({ planContent, planFilePath });
2297
+ await this.handlePlanApproval({ planFilePath, title, planExists: true });
2298
+ }
2299
+
2268
2300
  async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
2269
2301
  if (!this.planModeEnabled) {
2270
2302
  this.showWarning("Plan mode is not active.");
@@ -318,6 +318,7 @@ export interface InteractiveModeContext {
318
318
  disableLoopMode(): void;
319
319
  pauseLoop(): void;
320
320
  handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
321
+ openPlanReview(): Promise<void>;
321
322
 
322
323
  // Hook UI methods
323
324
  initHooksAndCustomTools(): Promise<void>;
@@ -229,6 +229,7 @@ import {
229
229
  type PythonExecutionMessage,
230
230
  readPendingDisplayTag,
231
231
  SILENT_ABORT_MARKER,
232
+ SKILL_PROMPT_MESSAGE_TYPE,
232
233
  stripImagesFromMessage,
233
234
  } from "./messages";
234
235
  import { formatSessionDumpText } from "./session-dump-format";
@@ -4335,6 +4336,44 @@ export class AgentSession {
4335
4336
  return { ...message, content: normalized } as T;
4336
4337
  }
4337
4338
 
4339
+ #createMagicKeywordNotices(text: string): CustomMessage[] {
4340
+ const timestamp = Date.now();
4341
+ const turnBudget = parseTurnBudget(text);
4342
+ this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4343
+ const keywordNotices: CustomMessage[] = [];
4344
+ if (containsUltrathink(text)) {
4345
+ keywordNotices.push({
4346
+ role: "custom",
4347
+ customType: "ultrathink-notice",
4348
+ content: ULTRATHINK_NOTICE,
4349
+ display: false,
4350
+ attribution: "user",
4351
+ timestamp,
4352
+ });
4353
+ }
4354
+ if (containsOrchestrate(text)) {
4355
+ keywordNotices.push({
4356
+ role: "custom",
4357
+ customType: "orchestrate-notice",
4358
+ content: ORCHESTRATE_NOTICE,
4359
+ display: false,
4360
+ attribution: "user",
4361
+ timestamp,
4362
+ });
4363
+ }
4364
+ if (containsWorkflow(text)) {
4365
+ keywordNotices.push({
4366
+ role: "custom",
4367
+ customType: "workflow-notice",
4368
+ content: WORKFLOW_NOTICE,
4369
+ display: false,
4370
+ attribution: "user",
4371
+ timestamp,
4372
+ });
4373
+ }
4374
+ return keywordNotices;
4375
+ }
4376
+
4338
4377
  /**
4339
4378
  * Send a prompt to the agent.
4340
4379
  * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
@@ -4376,42 +4415,7 @@ export class AgentSession {
4376
4415
  // Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
4377
4416
  // user's message that steer this turn. User-authored prompts only — synthetic /
4378
4417
  // agent-initiated turns never trigger them.
4379
- const keywordNotices: CustomMessage[] = [];
4380
- if (!options?.synthetic) {
4381
- const timestamp = Date.now();
4382
- const turnBudget = parseTurnBudget(expandedText);
4383
- this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4384
- if (containsUltrathink(expandedText)) {
4385
- keywordNotices.push({
4386
- role: "custom",
4387
- customType: "ultrathink-notice",
4388
- content: ULTRATHINK_NOTICE,
4389
- display: false,
4390
- attribution: "user",
4391
- timestamp,
4392
- });
4393
- }
4394
- if (containsOrchestrate(expandedText)) {
4395
- keywordNotices.push({
4396
- role: "custom",
4397
- customType: "orchestrate-notice",
4398
- content: ORCHESTRATE_NOTICE,
4399
- display: false,
4400
- attribution: "user",
4401
- timestamp,
4402
- });
4403
- }
4404
- if (containsWorkflow(expandedText)) {
4405
- keywordNotices.push({
4406
- role: "custom",
4407
- customType: "workflow-notice",
4408
- content: WORKFLOW_NOTICE,
4409
- display: false,
4410
- attribution: "user",
4411
- timestamp,
4412
- });
4413
- }
4414
- }
4418
+ const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
4415
4419
 
4416
4420
  // If streaming, queue via steer() or followUp() based on option
4417
4421
  if (this.isStreaming) {
@@ -4481,11 +4485,24 @@ export class AgentSession {
4481
4485
  .map(content => content.text)
4482
4486
  .join("");
4483
4487
 
4488
+ let keywordNotices: CustomMessage[] = [];
4489
+ if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
4490
+ const details = message.details;
4491
+ let skillArgs = "";
4492
+ if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
4493
+ skillArgs = details.args;
4494
+ }
4495
+ keywordNotices = this.#createMagicKeywordNotices(skillArgs);
4496
+ }
4497
+
4484
4498
  if (this.isStreaming) {
4485
4499
  if (!options?.streamingBehavior) {
4486
4500
  throw new AgentBusyError();
4487
4501
  }
4488
4502
  await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
4503
+ for (const notice of keywordNotices) {
4504
+ await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4505
+ }
4489
4506
  return;
4490
4507
  }
4491
4508
 
@@ -4499,7 +4516,10 @@ export class AgentSession {
4499
4516
  timestamp: Date.now(),
4500
4517
  };
4501
4518
 
4502
- await this.#promptWithMessage(customMessage, textContent, options);
4519
+ await this.#promptWithMessage(customMessage, textContent, {
4520
+ ...options,
4521
+ appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4522
+ });
4503
4523
  }
4504
4524
 
4505
4525
  async #promptWithMessage(
@@ -7837,16 +7857,32 @@ export class AgentSession {
7837
7857
  return "handled";
7838
7858
  }
7839
7859
  const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
7840
- // Overflow needs the input to actually shrink before the retry; if shake
7841
- // reclaimed nothing, summarization is the only remaining recovery.
7842
- if (reason === "overflow" && !reclaimed) {
7860
+ // Detect the dead-loop reported in issue #2119: the threshold check fires,
7861
+ // shake runs, but the resulting context is still above the configured
7862
+ // threshold. The next agent_end would re-trigger shake, which has nothing
7863
+ // new to drop on the second pass, so the loop spins until the user kills it.
7864
+ // Same hazard for "incomplete" (the retry would re-hit the length cap) and
7865
+ // for the existing "overflow + nothing reclaimed" case. In every recovery
7866
+ // reason we hand off to the summarization-driven context-full path so the
7867
+ // situation actually resolves; "idle" is exempt because its 60s+ timer
7868
+ // re-checks usage before re-firing and cannot dead-loop on its own.
7869
+ const contextWindow = this.model?.contextWindow ?? 0;
7870
+ const compactionSettings = this.settings.getGroup("compaction");
7871
+ const postShakeTokens = contextWindow > 0 ? this.#estimatePendingPromptTokens([]) : 0;
7872
+ const stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
7873
+ const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
7874
+ if (shouldFallBack) {
7875
+ const errorMessage = reclaimed
7876
+ ? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
7877
+ : "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
7843
7878
  await this.#emitSessionEvent({
7844
7879
  type: "auto_compaction_end",
7845
7880
  action,
7846
7881
  result: undefined,
7847
7882
  aborted: false,
7848
7883
  willRetry: false,
7849
- skipped: true,
7884
+ skipped: !reclaimed,
7885
+ errorMessage,
7850
7886
  });
7851
7887
  return "fallback";
7852
7888
  }
@@ -100,6 +100,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
100
100
  runtime.ctx.editor.setText("");
101
101
  },
102
102
  },
103
+ {
104
+ name: "plan-review",
105
+ description: "Re-open the plan review for the latest plan (plan mode only)",
106
+ handleTui: async (_command, runtime) => {
107
+ await runtime.ctx.openPlanReview();
108
+ runtime.ctx.editor.setText("");
109
+ },
110
+ },
103
111
  {
104
112
  name: "goal",
105
113
  description: "Toggle goal mode (persistent autonomous objective for this session)",
package/src/task/index.ts CHANGED
@@ -158,6 +158,8 @@ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
158
158
  "search_tool_bm25",
159
159
  ]);
160
160
 
161
+ const PLAN_MODE_AGENT_TOOL_ALLOWLIST: ReadonlySet<string> = new Set(["ast_grep", "report_finding"]);
162
+
161
163
  export function isReadOnlyAgent(agent: AgentDefinition): boolean {
162
164
  return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
163
165
  }
@@ -677,7 +679,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
677
679
  }
678
680
 
679
681
  const planModeState = this.session.getPlanModeState?.();
680
- const planModeTools = ["read", "search", "find", "lsp", "web_search"];
682
+ const planModeBaseTools = ["read", "search", "find", "lsp", "web_search"];
683
+ const planModeTools = [
684
+ ...planModeBaseTools,
685
+ ...(agent.tools ?? []).filter(
686
+ tool => PLAN_MODE_AGENT_TOOL_ALLOWLIST.has(tool) && !planModeBaseTools.includes(tool),
687
+ ),
688
+ ];
681
689
  const effectiveAgent: typeof agent = planModeState?.enabled
682
690
  ? {
683
691
  ...agent,
@@ -632,7 +632,7 @@ function renderAgentProgress(
632
632
  const indent = prefix ? `${prefix} ` : "";
633
633
  let statusLine: string;
634
634
  if (progress.status === "running") {
635
- const bullet = theme.fg("accent", "");
635
+ const bullet = theme.styledSymbol("status.done", "text");
636
636
  const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
637
637
  statusLine = `${indent}${bullet} ${name}`;
638
638
  if (description) {
@@ -640,7 +640,9 @@ function renderAgentProgress(
640
640
  statusLine += `${theme.fg("accent", ":")} ${desc}`;
641
641
  }
642
642
  } else {
643
- statusLine = `${indent}${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
643
+ const glyph =
644
+ progress.status === "completed" ? theme.styledSymbol("status.done", "accent") : theme.fg(iconColor, icon);
645
+ statusLine = `${indent}${glyph} ${theme.fg("accent", titlePart)}`;
644
646
  }
645
647
 
646
648
  // Show retry-blocked badge so the parent immediately sees that a child
@@ -809,7 +811,7 @@ function renderReviewResult(
809
811
  const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
810
812
  const isCorrect = summary.overall_correctness === "correct";
811
813
  const verdictIcon = isCorrect
812
- ? theme.styledSymbol("tool.task", "accent")
814
+ ? theme.styledSymbol("status.done", "accent")
813
815
  : theme.fg(verdictColor, theme.status.error);
814
816
  lines.push(
815
817
  `${continuePrefix} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${verdictIcon} ${theme.fg(
@@ -916,7 +918,7 @@ function renderAgentResult(
916
918
  : needsWarning
917
919
  ? theme.status.warning
918
920
  : success
919
- ? theme.styledSymbol("tool.task", "accent")
921
+ ? theme.styledSymbol("status.done", "accent")
920
922
  : theme.status.error;
921
923
  const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
922
924
  const statusText = aborted
@@ -1074,7 +1076,7 @@ function renderAgentResult(
1074
1076
  * Render the tool result.
1075
1077
  */
1076
1078
  export function renderResult(
1077
- result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails },
1079
+ result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails; isError?: boolean },
1078
1080
  options: RenderResultOptions,
1079
1081
  theme: Theme,
1080
1082
  args?: TaskParams,
@@ -1085,18 +1087,25 @@ export function renderResult(
1085
1087
 
1086
1088
  if (!details) {
1087
1089
  const text = result.content.find(c => c.type === "text")?.text || "";
1088
- const header = renderStatusLine(
1089
- { iconOverride: theme.styledSymbol("tool.task", "accent"), title: "Task" },
1090
- theme,
1091
- );
1090
+ const errored = result.isError === true;
1091
+ const header = errored
1092
+ ? renderStatusLine({ icon: "error", title: "Task", description: args?.agent }, theme)
1093
+ : renderStatusLine(
1094
+ {
1095
+ iconOverride: theme.styledSymbol("status.done", "accent"),
1096
+ title: "Task",
1097
+ description: args?.agent,
1098
+ },
1099
+ theme,
1100
+ );
1092
1101
  return framedBlock(theme, width => ({
1093
1102
  header,
1094
1103
  sections: [
1095
1104
  ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1096
1105
  ...(text ? [{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
1097
1106
  ],
1098
- state: "success",
1099
- borderColor: "borderMuted",
1107
+ state: errored ? "error" : "success",
1108
+ borderColor: errored ? "error" : "borderMuted",
1100
1109
  width,
1101
1110
  }));
1102
1111
  }
@@ -1116,7 +1125,8 @@ export function renderResult(
1116
1125
  const metaLabel = countLabel ? (agentName ? `${countLabel}: ${agentName}` : countLabel) : agentName;
1117
1126
  const header = renderStatusLine(
1118
1127
  {
1119
- icon,
1128
+ icon: icon === "success" ? undefined : icon,
1129
+ iconOverride: icon === "success" ? theme.styledSymbol("status.done", "accent") : undefined,
1120
1130
  title: "Task",
1121
1131
  meta: metaLabel ? [metaLabel] : undefined,
1122
1132
  },
@@ -59,11 +59,7 @@ import { type TodoPhase, TodoTool } from "./todo";
59
59
  import { WriteTool } from "./write";
60
60
  import { YieldTool } from "./yield";
61
61
 
62
- // Exa MCP tools (22 tools)
63
-
64
62
  export * from "../edit";
65
- export * from "../exa";
66
- export type * from "../exa/types";
67
63
  export * from "../goals";
68
64
  export * from "../lsp";
69
65
  export * from "../session/streaming-output";
package/src/utils/git.ts CHANGED
@@ -45,6 +45,10 @@ export interface StageHunksOptions {
45
45
  readonly rawDiff?: string;
46
46
  readonly signal?: AbortSignal;
47
47
  }
48
+ export interface HunkSelectionValidationError {
49
+ readonly path: string;
50
+ readonly message: string;
51
+ }
48
52
 
49
53
  export interface DiffOptions {
50
54
  readonly allowFailure?: boolean;
@@ -678,6 +682,43 @@ function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHun
678
682
  return file.hunks;
679
683
  }
680
684
 
685
+ export function createHunkSelectionValidator(
686
+ rawDiff: string,
687
+ ): (selections: readonly HunkSelection[]) => HunkSelectionValidationError[] {
688
+ const fileDiffMap = new Map(parseFileDiffs(rawDiff).map(entry => [entry.filename, entry]));
689
+ return selections => validateHunkSelectionsFromMap(fileDiffMap, selections);
690
+ }
691
+
692
+ function validateHunkSelectionsFromMap(
693
+ fileDiffMap: ReadonlyMap<string, FileDiff>,
694
+ selections: readonly HunkSelection[],
695
+ ): HunkSelectionValidationError[] {
696
+ const errors: HunkSelectionValidationError[] = [];
697
+
698
+ for (const selection of selections) {
699
+ const fileDiff = fileDiffMap.get(selection.path);
700
+ if (!fileDiff) continue;
701
+ if (selection.hunks.type === "all") continue;
702
+ if (fileDiff.isBinary) {
703
+ errors.push({ path: selection.path, message: `Cannot select hunks for binary file ${selection.path}` });
704
+ continue;
705
+ }
706
+ const selected = selectHunks(parseFileHunks(fileDiff), selection.hunks);
707
+ if (selected.length === 0) {
708
+ errors.push({ path: selection.path, message: `No hunks selected for ${selection.path}` });
709
+ }
710
+ }
711
+
712
+ return errors;
713
+ }
714
+
715
+ export function validateHunkSelections(
716
+ rawDiff: string,
717
+ selections: readonly HunkSelection[],
718
+ ): HunkSelectionValidationError[] {
719
+ return createHunkSelectionValidator(rawDiff)(selections);
720
+ }
721
+
681
722
  function parseStatusPorcelain(text: string): GitStatusSummary {
682
723
  let staged = 0;
683
724
  let unstaged = 0;
@@ -1,13 +0,0 @@
1
- /**
2
- * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
- */
4
- import type { TSchema } from "@oh-my-pi/pi-ai";
5
- import type { CustomTool } from "../extensibility/custom-tools/types";
6
- import type { ExaRenderDetails } from "./types";
7
- /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
8
- export declare function createExaTool(name: string, label: string, description: string, parameters: TSchema, mcpToolName: string, options?: {
9
- /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
10
- formatResponse?: boolean;
11
- /** Transform params before passing to callExaTool */
12
- transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
13
- }): CustomTool<TSchema, ExaRenderDetails>;
@@ -1,19 +0,0 @@
1
- /**
2
- * Exa TUI Rendering
3
- *
4
- * Tree-based rendering with collapsed/expanded states for Exa search results.
5
- */
6
- import type { Component } from "@oh-my-pi/pi-tui";
7
- import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
- import type { Theme } from "../modes/theme/theme";
9
- import type { ExaRenderDetails } from "./types";
10
- /** Render Exa result with tree-based layout */
11
- export declare function renderExaResult(result: {
12
- content: Array<{
13
- type: string;
14
- text?: string;
15
- }>;
16
- details?: ExaRenderDetails;
17
- }, options: RenderResultOptions, uiTheme: Theme): Component;
18
- /** Render Exa call (query/args preview) */
19
- export declare function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component;
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Researcher Tools
3
- *
4
- * Async research tasks with polling for completion.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const researcherTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Search Tools
3
- *
4
- * Basic neural/keyword search, deep research, code search, and URL crawling.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const searchTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,9 +0,0 @@
1
- /**
2
- * Exa Websets Tools
3
- *
4
- * CRUD operations for websets, items, searches, enrichments, and monitoring.
5
- */
6
- import type { TSchema } from "@oh-my-pi/pi-ai";
7
- import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import type { ExaRenderDetails } from "./types";
9
- export declare const websetsTools: CustomTool<TSchema, ExaRenderDetails>[];
@@ -1,60 +0,0 @@
1
- /**
2
- * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
- */
4
- import type { TSchema } from "@oh-my-pi/pi-ai";
5
- import type { CustomTool } from "../extensibility/custom-tools/types";
6
- import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
7
- import type { ExaRenderDetails } from "./types";
8
-
9
- /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
10
- export function createExaTool(
11
- name: string,
12
- label: string,
13
- description: string,
14
- parameters: TSchema,
15
- mcpToolName: string,
16
- options?: {
17
- /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
18
- formatResponse?: boolean;
19
- /** Transform params before passing to callExaTool */
20
- transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
21
- },
22
- ): CustomTool<TSchema, ExaRenderDetails> {
23
- const formatResponse = options?.formatResponse ?? true;
24
- const transformParams = options?.transformParams;
25
-
26
- return {
27
- name,
28
- label,
29
- description,
30
- parameters,
31
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
32
- try {
33
- const apiKey = findApiKey();
34
- // Exa MCP endpoint is publicly accessible; API key is optional
35
- const rawArgs = params as Record<string, unknown>;
36
- const args = transformParams ? transformParams(rawArgs) : rawArgs;
37
- const response = await callExaTool(mcpToolName, args, apiKey);
38
-
39
- if (formatResponse && isSearchResponse(response)) {
40
- const formatted = formatSearchResults(response);
41
- return {
42
- content: [{ type: "text" as const, text: formatted }],
43
- details: { response, toolName: name },
44
- };
45
- }
46
-
47
- return {
48
- content: [{ type: "text" as const, text: formatGenericResponse(response) }],
49
- details: { raw: response, toolName: name },
50
- };
51
- } catch (error) {
52
- const message = error instanceof Error ? error.message : String(error);
53
- return {
54
- content: [{ type: "text" as const, text: `Error: ${message}` }],
55
- details: { error: message, toolName: name },
56
- };
57
- }
58
- },
59
- };
60
- }