@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +74 -31
  2. package/dist/types/capability/skill.d.ts +7 -0
  3. package/dist/types/config/settings-schema.d.ts +36 -22
  4. package/dist/types/debug/protocol-probe.d.ts +38 -0
  5. package/dist/types/debug/terminal-info.d.ts +34 -0
  6. package/dist/types/export/html/template.generated.d.ts +1 -1
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  8. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -1
  10. package/dist/types/index.d.ts +1 -0
  11. package/dist/types/mcp/transports/stdio.d.ts +16 -7
  12. package/dist/types/modes/components/assistant-message.d.ts +3 -2
  13. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  14. package/dist/types/modes/components/todo-reminder.d.ts +1 -1
  15. package/dist/types/modes/components/transcript-container.d.ts +36 -0
  16. package/dist/types/modes/interactive-mode.d.ts +2 -1
  17. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  18. package/dist/types/modes/theme/theme.d.ts +20 -1
  19. package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
  20. package/dist/types/session/agent-session.d.ts +1 -1
  21. package/dist/types/session/indexed-session-storage.d.ts +59 -0
  22. package/dist/types/session/redis-session-storage.d.ts +12 -85
  23. package/dist/types/session/session-manager.d.ts +20 -0
  24. package/dist/types/session/session-storage.d.ts +5 -7
  25. package/dist/types/session/sql-session-storage.d.ts +16 -85
  26. package/dist/types/task/executor.d.ts +9 -0
  27. package/dist/types/task/index.d.ts +3 -1
  28. package/dist/types/telemetry-export.d.ts +19 -0
  29. package/dist/types/tiny/compiled-runtime.d.ts +35 -0
  30. package/dist/types/tools/ask.d.ts +1 -0
  31. package/dist/types/tools/index.d.ts +4 -2
  32. package/dist/types/tools/path-utils.d.ts +1 -1
  33. package/dist/types/tools/search.d.ts +2 -2
  34. package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
  35. package/dist/types/utils/session-color.d.ts +7 -2
  36. package/dist/types/web/search/index.d.ts +1 -1
  37. package/dist/types/web/search/provider.d.ts +2 -2
  38. package/dist/types/web/search/types.d.ts +65 -1
  39. package/package.json +15 -9
  40. package/scripts/build-binary.ts +12 -0
  41. package/src/capability/skill.ts +7 -0
  42. package/src/cli/args.ts +1 -1
  43. package/src/config/settings-schema.ts +22 -55
  44. package/src/debug/index.ts +67 -1
  45. package/src/debug/protocol-probe.ts +267 -0
  46. package/src/debug/terminal-info.ts +127 -0
  47. package/src/export/html/template.generated.ts +1 -1
  48. package/src/export/html/template.js +3 -3
  49. package/src/extensibility/custom-tools/types.ts +1 -1
  50. package/src/extensibility/extensions/types.ts +11 -0
  51. package/src/extensibility/shared-events.ts +1 -1
  52. package/src/extensibility/skills.ts +3 -3
  53. package/src/index.ts +1 -0
  54. package/src/internal-urls/docs-index.generated.ts +7 -7
  55. package/src/main.ts +12 -0
  56. package/src/mcp/transports/stdio.ts +37 -12
  57. package/src/modes/acp/acp-event-mapper.ts +7 -7
  58. package/src/modes/components/assistant-message.ts +3 -2
  59. package/src/modes/components/hook-selector.ts +149 -14
  60. package/src/modes/components/session-selector.ts +36 -5
  61. package/src/modes/components/status-line/segments.ts +2 -1
  62. package/src/modes/components/status-line.ts +1 -1
  63. package/src/modes/components/tips.txt +1 -1
  64. package/src/modes/components/todo-reminder.ts +1 -1
  65. package/src/modes/components/tool-execution.ts +9 -4
  66. package/src/modes/components/transcript-container.ts +91 -0
  67. package/src/modes/controllers/event-controller.ts +12 -7
  68. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  69. package/src/modes/controllers/todo-command-controller.ts +1 -1
  70. package/src/modes/interactive-mode.ts +18 -6
  71. package/src/modes/print-mode.ts +5 -0
  72. package/src/modes/rpc/rpc-types.ts +1 -1
  73. package/src/modes/theme/theme.ts +48 -8
  74. package/src/modes/utils/ui-helpers.ts +1 -0
  75. package/src/plan-mode/plan-handoff.ts +37 -0
  76. package/src/priority.json +4 -0
  77. package/src/prompts/goals/goal-continuation.md +1 -1
  78. package/src/prompts/system/eager-todo.md +3 -3
  79. package/src/prompts/system/orchestrate-notice.md +5 -5
  80. package/src/prompts/system/plan-mode-approved.md +14 -17
  81. package/src/prompts/system/plan-mode-reference.md +3 -6
  82. package/src/prompts/system/subagent-system-prompt.md +11 -0
  83. package/src/prompts/system/workflow-notice.md +1 -1
  84. package/src/prompts/tools/browser.md +5 -2
  85. package/src/prompts/tools/search.md +1 -1
  86. package/src/prompts/tools/task.md +3 -1
  87. package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
  88. package/src/sdk.ts +1 -0
  89. package/src/session/agent-session.ts +14 -14
  90. package/src/session/indexed-session-storage.ts +430 -0
  91. package/src/session/redis-session-storage.ts +66 -377
  92. package/src/session/session-manager.ts +102 -22
  93. package/src/session/session-storage.ts +148 -68
  94. package/src/session/sql-session-storage.ts +131 -382
  95. package/src/slash-commands/helpers/todo.ts +2 -2
  96. package/src/task/executor.ts +9 -1
  97. package/src/task/index.ts +51 -1
  98. package/src/telemetry-export.ts +126 -0
  99. package/src/tiny/compiled-runtime.ts +179 -0
  100. package/src/tiny/worker.ts +24 -2
  101. package/src/tools/ask.ts +133 -87
  102. package/src/tools/fetch.ts +17 -4
  103. package/src/tools/find.ts +2 -2
  104. package/src/tools/index.ts +6 -4
  105. package/src/tools/path-utils.ts +16 -7
  106. package/src/tools/renderers.ts +2 -2
  107. package/src/tools/search.ts +6 -3
  108. package/src/tools/{todo-write.ts → todo.ts} +32 -35
  109. package/src/utils/session-color.ts +39 -14
  110. package/src/web/search/index.ts +1 -1
  111. package/src/web/search/provider.ts +18 -34
  112. package/src/web/search/types.ts +73 -32
@@ -6,7 +6,7 @@ import chalk from "chalk";
6
6
  import * as z from "zod/v4";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
- import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
9
+ import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
10
10
  import type { ToolSession } from "../sdk";
11
11
  import type { SessionEntry } from "../session/session-manager";
12
12
  import { renderStatusLine, renderTreeList } from "../tui";
@@ -40,7 +40,7 @@ export interface TodoCompletionTransition {
40
40
  content: string;
41
41
  }
42
42
 
43
- export interface TodoWriteToolDetails {
43
+ export interface TodoToolDetails {
44
44
  phases: TodoPhase[];
45
45
  storage: "session" | "memory";
46
46
  completedTasks?: TodoCompletionTransition[];
@@ -68,14 +68,14 @@ const TodoOpEntry = z.object({
68
68
  text: z.string().optional().describe("note text"),
69
69
  });
70
70
 
71
- const todoWriteSchema = z
71
+ const todoSchema = z
72
72
  .object({
73
73
  ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
74
74
  })
75
75
  .describe("apply ordered todo operations");
76
76
 
77
- type TodoWriteParams = z.infer<typeof todoWriteSchema>;
78
- type TodoOpEntryValue = TodoWriteParams["ops"][number];
77
+ type TodoParams = z.infer<typeof todoSchema>;
78
+ type TodoOpEntryValue = TodoParams["ops"][number];
79
79
 
80
80
  // =============================================================================
81
81
  // State helpers
@@ -159,7 +159,7 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
159
159
  }
160
160
  if (entry.type !== "message") continue;
161
161
  const message = entry.message as { role?: string; toolName?: string; details?: unknown; isError?: boolean };
162
- if (message.role !== "toolResult" || message.toolName !== "todo_write" || message.isError) continue;
162
+ if (message.role !== "toolResult" || message.toolName !== "todo" || message.isError) continue;
163
163
 
164
164
  const details = message.details as { phases?: unknown } | undefined;
165
165
  if (!details || !Array.isArray(details.phases)) continue;
@@ -178,7 +178,7 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
178
178
  * the caller can render a `+N more` hint. When every task in `tasks` is
179
179
  * closed (completed or abandoned), returns the trailing `maxVisible` tasks
180
180
  * with `hiddenOpenCount = 0`, so the panel keeps useful context until the
181
- * active-phase pointer advances on the next `todo_write`.
181
+ * active-phase pointer advances on the next `todo`.
182
182
  *
183
183
  * Task identity and order are preserved — this is a slice, never a sort.
184
184
  */
@@ -383,7 +383,7 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
383
383
  }
384
384
  }
385
385
 
386
- function applyParams(phases: TodoPhase[], params: TodoWriteParams): { phases: TodoPhase[]; errors: string[] } {
386
+ function applyParams(phases: TodoPhase[], params: TodoParams): { phases: TodoPhase[]; errors: string[] } {
387
387
  const errors: string[] = [];
388
388
  let next = phases;
389
389
  for (const entry of params.ops) {
@@ -393,10 +393,10 @@ function applyParams(phases: TodoPhase[], params: TodoWriteParams): { phases: To
393
393
  return { phases: next, errors };
394
394
  }
395
395
 
396
- /** Apply an array of `todo_write`-style ops to existing phases. Used by /todo slash command. */
396
+ /** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
397
397
  export function applyOpsToPhases(
398
398
  currentPhases: TodoPhase[],
399
- ops: TodoWriteParams["ops"],
399
+ ops: TodoParams["ops"],
400
400
  ): { phases: TodoPhase[]; errors: string[] } {
401
401
  return applyParams(clonePhases(currentPhases), { ops });
402
402
  }
@@ -586,33 +586,33 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
586
586
  // Tool Class
587
587
  // =============================================================================
588
588
 
589
- export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
590
- readonly name = "todo_write";
589
+ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
590
+ readonly name = "todo";
591
591
  readonly approval = "read" as const;
592
- readonly label = "Todo Write";
592
+ readonly label = "Todo";
593
593
  readonly summary = "Write a structured todo list to track progress within a session";
594
594
  readonly description: string;
595
- readonly parameters = todoWriteSchema;
595
+ readonly parameters = todoSchema;
596
596
  readonly concurrency = "exclusive";
597
597
  readonly strict = true;
598
598
  readonly loadMode = "discoverable";
599
599
  constructor(private readonly session: ToolSession) {
600
- this.description = prompt.render(todoWriteDescription);
600
+ this.description = prompt.render(todoDescription);
601
601
  }
602
602
 
603
603
  async execute(
604
604
  _toolCallId: string,
605
- params: TodoWriteParams,
605
+ params: TodoParams,
606
606
  _signal?: AbortSignal,
607
- _onUpdate?: AgentToolUpdateCallback<TodoWriteToolDetails>,
607
+ _onUpdate?: AgentToolUpdateCallback<TodoToolDetails>,
608
608
  _context?: AgentToolContext,
609
- ): Promise<AgentToolResult<TodoWriteToolDetails>> {
609
+ ): Promise<AgentToolResult<TodoToolDetails>> {
610
610
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
611
611
  const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
612
612
  const completedTasks = getCompletionTransitions(previousPhases, updated);
613
613
  this.session.setTodoPhases?.(updated);
614
614
  const storage = this.session.getSessionFile() ? "session" : "memory";
615
- const details: TodoWriteToolDetails = { phases: updated, storage };
615
+ const details: TodoToolDetails = { phases: updated, storage };
616
616
  if (completedTasks.length > 0) details.completedTasks = completedTasks;
617
617
 
618
618
  return {
@@ -627,7 +627,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
627
627
  // TUI Renderer
628
628
  // =============================================================================
629
629
 
630
- type TodoWriteRenderArgs = {
630
+ type TodoRenderArgs = {
631
631
  ops?: Array<{
632
632
  op?: string;
633
633
  task?: string;
@@ -701,9 +701,9 @@ function noteMarker(count: number, uiTheme: Theme): string {
701
701
  return uiTheme.fg("dim", chalk.italic(` \u207a${toSuperscript(count)}`));
702
702
  }
703
703
 
704
- export const TODO_WRITE_STRIKE_HOLD_FRAMES = 2;
705
- export const TODO_WRITE_STRIKE_REVEAL_FRAMES = 12;
706
- export const TODO_WRITE_STRIKE_TOTAL_FRAMES = TODO_WRITE_STRIKE_HOLD_FRAMES + TODO_WRITE_STRIKE_REVEAL_FRAMES;
704
+ export const TODO_STRIKE_HOLD_FRAMES = 2;
705
+ export const TODO_STRIKE_REVEAL_FRAMES = 12;
706
+ export const TODO_STRIKE_TOTAL_FRAMES = TODO_STRIKE_HOLD_FRAMES + TODO_STRIKE_REVEAL_FRAMES;
707
707
  const EMPTY_COMPLETION_KEYS = new Set<string>();
708
708
  const STRIKE_START = "\x1b[9m";
709
709
  const STRIKE_END = "\x1b[29m";
@@ -721,11 +721,11 @@ function partialStrikethrough(text: string, visibleChars: number): string {
721
721
 
722
722
  function strikeRevealCount(text: string, frame: number | undefined): number | undefined {
723
723
  if (frame === undefined) return undefined;
724
- if (frame <= TODO_WRITE_STRIKE_HOLD_FRAMES) return 0;
724
+ if (frame <= TODO_STRIKE_HOLD_FRAMES) return 0;
725
725
  const chars = [...text];
726
726
  if (chars.length === 0) return undefined;
727
- const revealFrame = Math.min(frame - TODO_WRITE_STRIKE_HOLD_FRAMES, TODO_WRITE_STRIKE_REVEAL_FRAMES);
728
- return Math.ceil((chars.length * revealFrame) / TODO_WRITE_STRIKE_REVEAL_FRAMES);
727
+ const revealFrame = Math.min(frame - TODO_STRIKE_HOLD_FRAMES, TODO_STRIKE_REVEAL_FRAMES);
728
+ return Math.ceil((chars.length * revealFrame) / TODO_STRIKE_REVEAL_FRAMES);
729
729
  }
730
730
 
731
731
  function formatTodoLine(
@@ -775,8 +775,8 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
775
775
  return lines;
776
776
  }
777
777
 
778
- export const todoWriteToolRenderer = {
779
- renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
778
+ export const todoToolRenderer = {
779
+ renderCall(args: TodoRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
780
780
  const ops = args?.ops?.map(entry => {
781
781
  const parts = [entry.op ?? "update"];
782
782
  if (entry.task) parts.push(entry.task);
@@ -784,15 +784,15 @@ export const todoWriteToolRenderer = {
784
784
  if (entry.items?.length) parts.push(`${entry.items.length} item${entry.items.length === 1 ? "" : "s"}`);
785
785
  return parts.join(" ");
786
786
  }) ?? ["update"];
787
- const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta: ops }, uiTheme);
787
+ const text = renderStatusLine({ icon: "pending", title: "Todo", meta: ops }, uiTheme);
788
788
  return new Text(text, 0, 0);
789
789
  },
790
790
 
791
791
  renderResult(
792
- result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails },
792
+ result: { content: Array<{ type: string; text?: string }>; details?: TodoToolDetails },
793
793
  options: RenderResultOptions,
794
794
  uiTheme: Theme,
795
- _args?: TodoWriteRenderArgs,
795
+ _args?: TodoRenderArgs,
796
796
  ): Component {
797
797
  const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
798
798
  const completedTasks = result.details?.completedTasks ?? [];
@@ -806,10 +806,7 @@ export const todoWriteToolRenderer = {
806
806
  keys.add(task.content);
807
807
  }
808
808
  const allTasks = phases.flatMap(phase => phase.tasks);
809
- const header = renderStatusLine(
810
- { icon: "success", title: "Todo Write", meta: [`${allTasks.length} tasks`] },
811
- uiTheme,
812
- );
809
+ const header = renderStatusLine({ icon: "success", title: "Todo", meta: [`${allTasks.length} tasks`] }, uiTheme);
813
810
  if (allTasks.length === 0) {
814
811
  const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
815
812
  return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
@@ -1,3 +1,5 @@
1
+ import { hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
2
+
1
3
  /**
2
4
  * Derive a stable hue (0-359) from a string using djb2 hash.
3
5
  */
@@ -10,27 +12,50 @@ function nameToHue(name: string): number {
10
12
  return hash % 360;
11
13
  }
12
14
 
15
+ const ACCENT_SATURATION = 0.9;
16
+ const ACCENT_DARK_LIGHTNESS = 0.72;
17
+ /** Minimum contrast ratio (WCAG AA large text) between a light-theme accent and its surface. */
18
+ const ACCENT_MIN_CONTRAST = 3;
19
+
13
20
  /**
14
- * Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
21
+ * Largest relative luminance an accent may have while still meeting
22
+ * {@link ACCENT_MIN_CONTRAST} against a surface of the given luminance.
15
23
  */
16
- function hslToHex(h: number, s: number, l: number): string {
17
- const a = s * Math.min(l, 1 - l);
18
- const f = (n: number) => {
19
- const k = (n + h / 30) % 12;
20
- const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
21
- return Math.round(255 * color)
22
- .toString(16)
23
- .padStart(2, "0");
24
- };
25
- return `#${f(0)}${f(8)}${f(4)}`;
24
+ function accentLuminanceCap(surfaceLuminance: number): number {
25
+ return Math.max(0, (surfaceLuminance + 0.05) / ACCENT_MIN_CONTRAST - 0.05);
26
26
  }
27
27
 
28
28
  /**
29
29
  * Derive a stable CSS hex accent color from a session name.
30
- * High saturation, vivid — suitable for both status bar text and border coloring.
30
+ *
31
+ * On dark themes (`surfaceLuminance` undefined) the accent is vivid (high
32
+ * saturation, high lightness). On light themes the lightness is reduced until the
33
+ * accent's perceived luminance clears {@link ACCENT_MIN_CONTRAST} against the
34
+ * actual surface it renders on — so it stays legible on near-white *and* mid-light
35
+ * backgrounds — while keeping the same per-session hue.
31
36
  */
32
- export function getSessionAccentHex(name: string): string {
33
- return hslToHex(nameToHue(name), 0.9, 0.72);
37
+ export function getSessionAccentHex(name: string, surfaceLuminance?: number): string {
38
+ const hue = nameToHue(name);
39
+ if (surfaceLuminance === undefined) {
40
+ return hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
41
+ }
42
+
43
+ const cap = accentLuminanceCap(surfaceLuminance);
44
+ const top = hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
45
+ if ((relativeLuminance(top) ?? 0) <= cap) return top;
46
+
47
+ // Bisect lightness: `lo` always yields luminance <= cap, `hi` always above it.
48
+ let lo = 0;
49
+ let hi = ACCENT_DARK_LIGHTNESS;
50
+ for (let i = 0; i < 20; i++) {
51
+ const mid = (lo + hi) / 2;
52
+ if ((relativeLuminance(hslToHex(hue, ACCENT_SATURATION, mid)) ?? 0) > cap) {
53
+ hi = mid;
54
+ } else {
55
+ lo = mid;
56
+ }
57
+ }
58
+ return hslToHex(hue, ACCENT_SATURATION, lo);
34
59
  }
35
60
 
36
61
  /**
@@ -220,7 +220,7 @@ export async function runSearchQuery(
220
220
  /**
221
221
  * Web search tool implementation.
222
222
  *
223
- * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
223
+ * Supports the configured web-search provider chain with automatic fallback.
224
224
  */
225
225
  export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
226
226
  readonly name = "web_search";
@@ -5,15 +5,16 @@
5
5
  // fetch/parse/format helpers) and only one — at most — is needed per session,
6
6
  // so eager construction was wasted work at startup.
7
7
  //
8
- // The `label`/`id` metadata is kept inline so callers needing a display name
9
- // (error formatting, UI listings) do not force a load.
8
+ // Provider modules are loaded lazily; display metadata lives in types.ts so UI
9
+ // listings can share it without importing provider implementations.
10
10
 
11
11
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
12
12
  import type { SearchProvider } from "./providers/base";
13
- import type { SearchProviderId } from "./types";
13
+ import { SEARCH_PROVIDER_LABELS, SEARCH_PROVIDER_ORDER, type SearchProviderId } from "./types";
14
14
 
15
15
  export type { SearchParams } from "./providers/base";
16
16
  export { SearchProvider } from "./providers/base";
17
+ export { SEARCH_PROVIDER_ORDER } from "./types";
17
18
 
18
19
  interface ProviderMeta {
19
20
  id: SearchProviderId;
@@ -25,72 +26,72 @@ interface ProviderMeta {
25
26
  const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
26
27
  exa: {
27
28
  id: "exa",
28
- label: "Exa",
29
+ label: SEARCH_PROVIDER_LABELS.exa,
29
30
  load: async () => new (await import("./providers/exa")).ExaProvider(),
30
31
  },
31
32
  brave: {
32
33
  id: "brave",
33
- label: "Brave",
34
+ label: SEARCH_PROVIDER_LABELS.brave,
34
35
  load: async () => new (await import("./providers/brave")).BraveProvider(),
35
36
  },
36
37
  jina: {
37
38
  id: "jina",
38
- label: "Jina",
39
+ label: SEARCH_PROVIDER_LABELS.jina,
39
40
  load: async () => new (await import("./providers/jina")).JinaProvider(),
40
41
  },
41
42
  perplexity: {
42
43
  id: "perplexity",
43
- label: "Perplexity",
44
+ label: SEARCH_PROVIDER_LABELS.perplexity,
44
45
  load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
45
46
  },
46
47
  kimi: {
47
48
  id: "kimi",
48
- label: "Kimi",
49
+ label: SEARCH_PROVIDER_LABELS.kimi,
49
50
  load: async () => new (await import("./providers/kimi")).KimiProvider(),
50
51
  },
51
52
  zai: {
52
53
  id: "zai",
53
- label: "Z.AI",
54
+ label: SEARCH_PROVIDER_LABELS.zai,
54
55
  load: async () => new (await import("./providers/zai")).ZaiProvider(),
55
56
  },
56
57
  anthropic: {
57
58
  id: "anthropic",
58
- label: "Anthropic",
59
+ label: SEARCH_PROVIDER_LABELS.anthropic,
59
60
  load: async () => new (await import("./providers/anthropic")).AnthropicProvider(),
60
61
  },
61
62
  gemini: {
62
63
  id: "gemini",
63
- label: "Gemini",
64
+ label: SEARCH_PROVIDER_LABELS.gemini,
64
65
  load: async () => new (await import("./providers/gemini")).GeminiProvider(),
65
66
  },
66
67
  codex: {
67
68
  id: "codex",
68
- label: "OpenAI",
69
+ label: SEARCH_PROVIDER_LABELS.codex,
69
70
  load: async () => new (await import("./providers/codex")).CodexProvider(),
70
71
  },
71
72
  tavily: {
72
73
  id: "tavily",
73
- label: "Tavily",
74
+ label: SEARCH_PROVIDER_LABELS.tavily,
74
75
  load: async () => new (await import("./providers/tavily")).TavilyProvider(),
75
76
  },
76
77
  parallel: {
77
78
  id: "parallel",
78
- label: "Parallel",
79
+ label: SEARCH_PROVIDER_LABELS.parallel,
79
80
  load: async () => new (await import("./providers/parallel")).ParallelProvider(),
80
81
  },
81
82
  kagi: {
82
83
  id: "kagi",
83
- label: "Kagi",
84
+ label: SEARCH_PROVIDER_LABELS.kagi,
84
85
  load: async () => new (await import("./providers/kagi")).KagiProvider(),
85
86
  },
86
87
  synthetic: {
87
88
  id: "synthetic",
88
- label: "Synthetic",
89
+ label: SEARCH_PROVIDER_LABELS.synthetic,
89
90
  load: async () => new (await import("./providers/synthetic")).SyntheticProvider(),
90
91
  },
91
92
  searxng: {
92
93
  id: "searxng",
93
- label: "SearXNG",
94
+ label: SEARCH_PROVIDER_LABELS.searxng,
94
95
  load: async () => new (await import("./providers/searxng")).SearXNGProvider(),
95
96
  },
96
97
  };
@@ -118,23 +119,6 @@ export async function getSearchProvider(id: SearchProviderId): Promise<SearchPro
118
119
  return provider;
119
120
  }
120
121
 
121
- export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
122
- "tavily",
123
- "perplexity",
124
- "brave",
125
- "jina",
126
- "kimi",
127
- "anthropic",
128
- "gemini",
129
- "codex",
130
- "zai",
131
- "exa",
132
- "parallel",
133
- "kagi",
134
- "synthetic",
135
- "searxng",
136
- ];
137
-
138
122
  /** Preferred provider set via settings (default: auto) */
139
123
  let preferredProvId: SearchProviderId | "auto" = "auto";
140
124
 
@@ -4,44 +4,85 @@
4
4
  * Unified types for web search responses across supported providers.
5
5
  */
6
6
 
7
+ export const SEARCH_PROVIDER_ORDER = [
8
+ "tavily",
9
+ "perplexity",
10
+ "brave",
11
+ "jina",
12
+ "kimi",
13
+ "anthropic",
14
+ "gemini",
15
+ "codex",
16
+ "zai",
17
+ "exa",
18
+ "parallel",
19
+ "kagi",
20
+ "synthetic",
21
+ "searxng",
22
+ ] as const;
23
+
7
24
  /** Supported web search providers */
8
- export type SearchProviderId =
9
- | "exa"
10
- | "brave"
11
- | "jina"
12
- | "kimi"
13
- | "zai"
14
- | "anthropic"
15
- | "perplexity"
16
- | "gemini"
17
- | "codex"
18
- | "tavily"
19
- | "parallel"
20
- | "kagi"
21
- | "synthetic"
22
- | "searxng";
25
+ export type SearchProviderId = (typeof SEARCH_PROVIDER_ORDER)[number];
26
+
27
+ export const SEARCH_PROVIDER_PREFERENCES = ["auto", ...SEARCH_PROVIDER_ORDER] as const;
28
+
29
+ export const SEARCH_PROVIDER_OPTIONS = [
30
+ {
31
+ value: "auto",
32
+ label: "Auto",
33
+ description: "Automatically uses the first configured web-search provider",
34
+ },
35
+ { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
36
+ { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
37
+ { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
38
+ { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
39
+ { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
40
+ {
41
+ value: "anthropic",
42
+ label: "Anthropic",
43
+ description: "Claude's native web_search tool (uses Anthropic OAuth or ANTHROPIC_API_KEY)",
44
+ },
45
+ {
46
+ value: "gemini",
47
+ label: "Gemini",
48
+ description: "Google Search grounding via Gemini (uses google-gemini-cli or google-antigravity OAuth)",
49
+ },
50
+ {
51
+ value: "codex",
52
+ label: "OpenAI",
53
+ description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
54
+ },
55
+ { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
56
+ { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
57
+ { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
58
+ { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
59
+ { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
60
+ { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
61
+ ] as const;
62
+
63
+ export const SEARCH_PROVIDER_LABELS: Record<SearchProviderId, string> = {
64
+ tavily: "Tavily",
65
+ perplexity: "Perplexity",
66
+ brave: "Brave",
67
+ jina: "Jina",
68
+ kimi: "Kimi",
69
+ anthropic: "Anthropic",
70
+ gemini: "Gemini",
71
+ codex: "OpenAI",
72
+ zai: "Z.AI",
73
+ exa: "Exa",
74
+ parallel: "Parallel",
75
+ kagi: "Kagi",
76
+ synthetic: "Synthetic",
77
+ searxng: "SearXNG",
78
+ };
23
79
 
24
80
  export function isSearchProviderId(value: string): value is SearchProviderId {
25
- return [
26
- "exa",
27
- "brave",
28
- "jina",
29
- "kimi",
30
- "zai",
31
- "anthropic",
32
- "perplexity",
33
- "gemini",
34
- "codex",
35
- "tavily",
36
- "parallel",
37
- "kagi",
38
- "synthetic",
39
- "searxng",
40
- ].includes(value);
81
+ return SEARCH_PROVIDER_ORDER.includes(value as SearchProviderId);
41
82
  }
42
83
 
43
84
  export function isSearchProviderPreference(value: string): value is SearchProviderId | "auto" {
44
- return value === "auto" || isSearchProviderId(value);
85
+ return SEARCH_PROVIDER_PREFERENCES.includes(value as SearchProviderId | "auto");
45
86
  }
46
87
 
47
88
  /** Source returned by search (all providers) */