@oh-my-pi/pi-coding-agent 14.7.1 → 14.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +7 -7
  3. package/src/config/model-equivalence.ts +1 -0
  4. package/src/config/model-registry.ts +108 -22
  5. package/src/config/settings-schema.ts +36 -1
  6. package/src/discovery/helpers.ts +4 -3
  7. package/src/edit/index.ts +1 -0
  8. package/src/eval/py/gateway-coordinator.ts +2 -3
  9. package/src/eval/py/runtime.ts +1 -0
  10. package/src/internal-urls/docs-index.generated.ts +1 -1
  11. package/src/lsp/index.ts +2 -0
  12. package/src/mcp/discoverable-tool-metadata.ts +24 -202
  13. package/src/modes/components/extensions/extension-dashboard.ts +26 -2
  14. package/src/modes/components/extensions/state-manager.ts +41 -0
  15. package/src/modes/controllers/selector-controller.ts +3 -0
  16. package/src/modes/interactive-mode.ts +26 -1
  17. package/src/prompts/tools/search-tool-bm25.md +14 -14
  18. package/src/prompts/tools/todo-write.md +1 -0
  19. package/src/sdk.ts +69 -8
  20. package/src/session/agent-session.ts +177 -1
  21. package/src/slash-commands/builtin-registry.ts +11 -0
  22. package/src/task/index.ts +2 -0
  23. package/src/tool-discovery/tool-index.ts +377 -0
  24. package/src/tools/ask.ts +2 -0
  25. package/src/tools/ast-edit.ts +2 -0
  26. package/src/tools/ast-grep.ts +2 -0
  27. package/src/tools/bash.ts +1 -0
  28. package/src/tools/browser.ts +2 -0
  29. package/src/tools/calculator.ts +2 -0
  30. package/src/tools/checkpoint.ts +4 -0
  31. package/src/tools/debug.ts +2 -0
  32. package/src/tools/eval.ts +2 -0
  33. package/src/tools/find.ts +2 -0
  34. package/src/tools/gh.ts +2 -0
  35. package/src/tools/hindsight-recall.ts +2 -0
  36. package/src/tools/hindsight-reflect.ts +2 -0
  37. package/src/tools/hindsight-retain.ts +2 -0
  38. package/src/tools/index.ts +74 -14
  39. package/src/tools/inspect-image.ts +2 -0
  40. package/src/tools/irc.ts +2 -1
  41. package/src/tools/job.ts +2 -1
  42. package/src/tools/notebook.ts +2 -0
  43. package/src/tools/read.ts +1 -0
  44. package/src/tools/recipe/index.ts +2 -0
  45. package/src/tools/render-mermaid.ts +2 -0
  46. package/src/tools/search-tool-bm25.ts +128 -42
  47. package/src/tools/search.ts +2 -0
  48. package/src/tools/ssh.ts +2 -0
  49. package/src/tools/todo-write.ts +2 -1
  50. package/src/tools/write.ts +2 -0
  51. package/src/web/search/index.ts +2 -0
  52. package/src/web/search/providers/searxng.ts +8 -0
@@ -10,13 +10,13 @@ import type { Skill } from "../extensibility/skills";
10
10
  import type { HindsightSessionState } from "../hindsight/state";
11
11
  import type { InternalUrlRouter } from "../internal-urls";
12
12
  import { LspTool } from "../lsp";
13
- import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
14
13
  import type { PlanModeState } from "../plan-mode/state";
15
14
  import type { AgentRegistry } from "../registry/agent-registry";
16
15
  import type { CustomMessage } from "../session/messages";
17
16
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
18
17
  import { TaskTool } from "../task";
19
18
  import type { AgentOutputManager } from "../task/output-manager";
19
+ import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
20
20
  import type { EventBus } from "../utils/event-bus";
21
21
  import { WebSearchTool } from "../web/search";
22
22
  import { AskTool } from "./ask";
@@ -105,6 +105,12 @@ export type ContextFileEntry = {
105
105
  };
106
106
 
107
107
  export type { DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
108
+ export type {
109
+ DiscoverableTool,
110
+ DiscoverableToolSearchIndex,
111
+ DiscoverableToolSearchResult,
112
+ DiscoverableToolSource,
113
+ } from "../tool-discovery/tool-index";
108
114
 
109
115
  /** Session context for tool factories */
110
116
  export interface ToolSession {
@@ -184,14 +190,29 @@ export interface ToolSession {
184
190
  setTodoPhases?: (phases: TodoPhase[]) => void;
185
191
  /** Whether MCP tool discovery is active for this session. */
186
192
  isMCPDiscoveryEnabled?: () => boolean;
187
- /** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks. */
188
- getDiscoverableMCPTools?: () => DiscoverableMCPTool[];
189
- /** Get the cached discoverable MCP search index for search_tool_bm25 execution. */
190
- getDiscoverableMCPSearchIndex?: () => DiscoverableMCPSearchIndex;
193
+ /** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks.
194
+ * @deprecated Use getDiscoverableTools with source filter instead. */
195
+ getDiscoverableMCPTools?: () => import("../mcp/discoverable-tool-metadata").DiscoverableMCPTool[];
196
+ /** Get the cached discoverable MCP search index for search_tool_bm25 execution.
197
+ * @deprecated Use getDiscoverableToolSearchIndex instead. */
198
+ getDiscoverableMCPSearchIndex?: () => import("../tool-discovery/tool-index").DiscoverableMCPSearchIndex;
191
199
  /** Get MCP tools activated by prior search_tool_bm25 calls. */
192
200
  getSelectedMCPToolNames?: () => string[];
193
201
  /** Merge MCP tool selections into the active session tool set. */
194
202
  activateDiscoveredMCPTools?: (toolNames: string[]) => Promise<string[]>;
203
+ // ── Generic tool discovery (unified — covers built-in + MCP + extension) ──
204
+ /** Whether any form of tool discovery is active (tools.discoveryMode !== "off" or mcp.discoveryMode). */
205
+ isToolDiscoveryEnabled?: () => boolean;
206
+ /** Get all hidden-but-discoverable tools for search_tool_bm25 prompts. */
207
+ getDiscoverableTools?: (filter?: {
208
+ source?: import("../tool-discovery/tool-index").DiscoverableToolSource;
209
+ }) => DiscoverableTool[];
210
+ /** Get the cached generic discoverable search index. */
211
+ getDiscoverableToolSearchIndex?: () => DiscoverableToolSearchIndex;
212
+ /** Get tool names activated by prior search_tool_bm25 calls (all sources). */
213
+ getSelectedDiscoveredToolNames?: () => string[];
214
+ /** Merge tool selections into the active session tool set. */
215
+ activateDiscoveredTools?: (toolNames: string[]) => Promise<string[]>;
195
216
  /** The tool-choice queue used to force forthcoming tool invocations and carry invocation handlers. */
196
217
  getToolChoiceQueue?(): ToolChoiceQueue;
197
218
  /** Build a model-provider-specific ToolChoice that targets the named tool, or undefined if unsupported. */
@@ -209,25 +230,48 @@ export interface ToolSession {
209
230
  queueDeferredMessage?(message: CustomMessage): void;
210
231
  }
211
232
 
212
- type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
233
+ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
213
234
 
235
+ export type BuiltinToolLoadMode = "essential" | "discoverable";
236
+
237
+ /** Default essential tool names when tools.essentialOverride is empty. */
238
+ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
239
+
240
+ /**
241
+ * Resolve the active essential built-in tool names from settings.
242
+ * Returns `tools.essentialOverride` if non-empty (filtered to known built-ins),
243
+ * otherwise `DEFAULT_ESSENTIAL_TOOL_NAMES`.
244
+ */
245
+ export function computeEssentialBuiltinNames(settings: Settings): string[] {
246
+ const override = settings.get("tools.essentialOverride") ?? [];
247
+ const cleaned = override.map(name => name.trim()).filter(Boolean);
248
+ if (cleaned.length > 0) {
249
+ return cleaned.filter(name => name in BUILTIN_TOOLS);
250
+ }
251
+ return [...DEFAULT_ESSENTIAL_TOOL_NAMES];
252
+ }
253
+
254
+ /**
255
+ * Public callable factory map. External callers may invoke `BUILTIN_TOOLS.read(session)` or
256
+ * `BUILTIN_TOOLS[name](session)` to construct a tool directly.
257
+ */
214
258
  export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
259
+ read: s => new ReadTool(s),
260
+ bash: s => new BashTool(s),
261
+ edit: s => new EditTool(s),
215
262
  ast_grep: s => new AstGrepTool(s),
216
263
  ast_edit: s => new AstEditTool(s),
217
264
  render_mermaid: s => new RenderMermaidTool(s),
218
265
  ask: AskTool.createIf,
219
- bash: s => new BashTool(s),
220
266
  debug: DebugTool.createIf,
221
267
  eval: s => new EvalTool(s),
222
268
  calc: s => new CalculatorTool(s),
223
269
  ssh: loadSshTool,
224
- edit: s => new EditTool(s),
225
270
  github: GithubTool.createIf,
226
271
  find: s => new FindTool(s),
227
272
  search: s => new SearchTool(s),
228
273
  lsp: LspTool.createIf,
229
274
  notebook: s => new NotebookTool(s),
230
- read: s => new ReadTool(s),
231
275
  inspect_image: s => new InspectImageTool(s),
232
276
  browser: s => new BrowserTool(s),
233
277
  checkpoint: CheckpointTool.createIf,
@@ -299,7 +343,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
299
343
  const enableLsp = session.enableLsp ?? true;
300
344
  const requestedTools =
301
345
  toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
302
- if (requestedTools && !requestedTools.includes("exit_plan_mode")) {
346
+ const planEnabled = session.settings.get("plan.enabled");
347
+ if (planEnabled && requestedTools && !requestedTools.includes("exit_plan_mode")) {
303
348
  requestedTools.push("exit_plan_mode");
304
349
  }
305
350
  const backends = resolveEvalBackends(session);
@@ -360,8 +405,20 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
360
405
  }
361
406
  }
362
407
  }
408
+ // Resolve effective tool discovery mode.
409
+ // tools.discoveryMode takes precedence; mcp.discoveryMode is a back-compat alias for "mcp-only".
410
+ const toolsDiscoveryMode = session.settings.get("tools.discoveryMode");
411
+ const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
412
+ toolsDiscoveryMode !== "off"
413
+ ? (toolsDiscoveryMode as "off" | "mcp-only" | "all")
414
+ : session.settings.get("mcp.discoveryMode")
415
+ ? "mcp-only"
416
+ : "off";
417
+ const discoveryActive = effectiveDiscoveryMode !== "off";
418
+
363
419
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
364
420
  const isToolAllowed = (name: string) => {
421
+ if (name === "exit_plan_mode") return planEnabled;
365
422
  if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
366
423
  if (name === "bash") return true;
367
424
  if (name === "eval") return allowEval;
@@ -376,7 +433,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
376
433
  if (name === "notebook") return session.settings.get("notebook.enabled");
377
434
  if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
378
435
  if (name === "web_search") return session.settings.get("web_search.enabled");
379
- if (name === "search_tool_bm25") return session.settings.get("mcp.discoveryMode");
436
+ // search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
437
+ if (name === "search_tool_bm25") return discoveryActive;
380
438
  if (name === "calc") return session.settings.get("calc.enabled");
381
439
  if (name === "browser") return session.settings.get("browser.enabled");
382
440
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
@@ -401,14 +459,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
401
459
  filteredRequestedTools !== undefined
402
460
  ? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
403
461
  : [
404
- ...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
462
+ ...Object.entries(BUILTIN_TOOLS)
463
+ .filter(([name]) => isToolAllowed(name))
464
+ .map(([name, factory]) => [name, factory] as const),
405
465
  ...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
406
- ...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
466
+ ...(planEnabled ? ([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const) : []),
407
467
  ];
408
468
 
409
469
  const baseResults = await Promise.all(
410
470
  baseEntries.map(async ([name, factory]) => {
411
- const tool = await logger.time(`createTools:${name}`, factory, session);
471
+ const tool = await logger.time(`createTools:${name}`, factory as ToolFactory, session);
412
472
  return tool ? wrapToolWithMetaNotice(tool) : null;
413
473
  }),
414
474
  );
@@ -41,6 +41,8 @@ function extractResponseText(message: AssistantMessage): string {
41
41
  export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
42
42
  readonly name = "inspect_image";
43
43
  readonly label = "InspectImage";
44
+ readonly loadMode = "discoverable";
45
+ readonly summary = "Describe or analyze an image file";
44
46
  readonly description: string;
45
47
  readonly parameters = inspectImageSchema;
46
48
  readonly strict = false;
package/src/tools/irc.ts CHANGED
@@ -74,10 +74,11 @@ export interface IrcDetails {
74
74
  export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
75
75
  readonly name = "irc";
76
76
  readonly label = "IRC";
77
+ readonly summary = "Send and receive messages between agents over IRC-like channels";
77
78
  readonly description: string;
78
79
  readonly parameters = ircSchema;
79
80
  readonly strict = true;
80
-
81
+ readonly loadMode = "discoverable";
81
82
  constructor(private readonly session: ToolSession) {
82
83
  this.description = prompt.render(ircDescription);
83
84
  }
package/src/tools/job.ts CHANGED
@@ -76,10 +76,11 @@ export interface JobToolDetails {
76
76
  export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
77
77
  readonly name = "job";
78
78
  readonly label = "Job";
79
+ readonly summary = "Manage long-running background jobs (async bash/python)";
79
80
  readonly description: string;
80
81
  readonly parameters = jobSchema;
81
82
  readonly strict = true;
82
-
83
+ readonly loadMode = "discoverable";
83
84
  constructor(private readonly session: ToolSession) {
84
85
  this.description = prompt.render(jobDescription);
85
86
  }
@@ -64,6 +64,8 @@ type NotebookParams = Static<typeof notebookSchema>;
64
64
  export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
65
65
  readonly name = "notebook";
66
66
  readonly label = "Notebook";
67
+ readonly loadMode = "discoverable";
68
+ readonly summary = "Read and execute Jupyter notebooks";
67
69
  readonly description = "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based.";
68
70
  readonly parameters = notebookSchema;
69
71
  readonly strict = true;
package/src/tools/read.ts CHANGED
@@ -512,6 +512,7 @@ interface ResolvedSqliteReadPath {
512
512
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
513
513
  readonly name = "read";
514
514
  readonly label = "Read";
515
+ readonly loadMode = "essential";
515
516
  readonly description: string;
516
517
  readonly parameters = readSchema;
517
518
  readonly nonAbortable = true;
@@ -33,6 +33,8 @@ export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetail
33
33
  readonly parameters = recipeSchema;
34
34
  readonly strict = true;
35
35
  readonly concurrency = "exclusive";
36
+ readonly loadMode = "discoverable";
37
+ readonly summary = "Execute a saved bash recipe (multi-step shell command preset)";
36
38
  readonly mergeCallAndResult = true;
37
39
  readonly inline = true;
38
40
  readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
@@ -35,9 +35,11 @@ export interface RenderMermaidToolDetails {
35
35
  export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
36
36
  readonly name = "render_mermaid";
37
37
  readonly label = "RenderMermaid";
38
+ readonly summary = "Render a Mermaid diagram to an image";
38
39
  readonly description: string;
39
40
  readonly parameters = renderMermaidSchema;
40
41
  readonly strict = true;
42
+ readonly loadMode = "discoverable";
41
43
 
42
44
  constructor(private readonly session: ToolSession) {
43
45
  this.description = prompt.render(renderMermaidDescription);
@@ -3,21 +3,27 @@ import { type Component, Text } from "@oh-my-pi/pi-tui";
3
3
  import { prompt } from "@oh-my-pi/pi-utils";
4
4
  import { type Static, Type } from "@sinclair/typebox";
5
5
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
6
- import {
7
- buildDiscoverableMCPSearchIndex,
8
- type DiscoverableMCPSearchIndex,
9
- type DiscoverableMCPTool,
10
- formatDiscoverableMCPToolServerSummary,
11
- searchDiscoverableMCPTools,
12
- summarizeDiscoverableMCPTools,
13
- } from "../mcp/discoverable-tool-metadata";
14
6
  import type { Theme } from "../modes/theme/theme";
15
7
  import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
8
+ import {
9
+ buildDiscoverableToolSearchIndex,
10
+ type DiscoverableTool,
11
+ type DiscoverableToolSearchIndex,
12
+ formatDiscoverableToolServerSummary,
13
+ searchDiscoverableTools,
14
+ summarizeDiscoverableTools,
15
+ } from "../tool-discovery/tool-index";
16
16
  import { renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
17
17
  import type { ToolSession } from ".";
18
18
  import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
19
19
  import { ToolError } from "./tool-errors";
20
20
 
21
+ // Re-export legacy MCP types for back-compat (tests and external callers may reference them)
22
+ export type {
23
+ DiscoverableMCPSearchIndex,
24
+ DiscoverableMCPTool,
25
+ } from "../mcp/discoverable-tool-metadata";
26
+
21
27
  const DEFAULT_LIMIT = 8;
22
28
  const TOOL_DISCOVERY_TITLE = "Tool Discovery";
23
29
  const COLLAPSED_MATCH_LIMIT = 5;
@@ -25,7 +31,10 @@ const MATCH_LABEL_LEN = 72;
25
31
  const MATCH_DESCRIPTION_LEN = 96;
26
32
 
27
33
  const searchToolBm25Schema = Type.Object({
28
- query: Type.String({ description: "mcp search query", examples: ["kubernetes pod", "image processing"] }),
34
+ query: Type.String({
35
+ description: "tool search query",
36
+ examples: ["kubernetes pod", "image processing", "git commit"],
37
+ }),
29
38
  limit: Type.Optional(Type.Integer({ description: "max matches", minimum: 1 })),
30
39
  });
31
40
 
@@ -50,11 +59,11 @@ export interface SearchToolBm25Details {
50
59
  tools: SearchToolBm25Match[];
51
60
  }
52
61
 
53
- function formatMatch(tool: DiscoverableMCPTool, score: number): SearchToolBm25Match {
62
+ function formatMatch(tool: DiscoverableTool, score: number): SearchToolBm25Match {
54
63
  return {
55
64
  name: tool.name,
56
65
  label: tool.label,
57
- description: tool.description,
66
+ description: tool.summary,
58
67
  server_name: tool.serverName,
59
68
  mcp_tool_name: tool.mcpToolName,
60
69
  schema_keys: tool.schemaKeys,
@@ -71,41 +80,99 @@ function buildSearchToolBm25Content(details: SearchToolBm25Details): string {
71
80
  });
72
81
  }
73
82
 
74
- function getDiscoverableMCPToolsForDescription(session: ToolSession): DiscoverableMCPTool[] {
83
+ /** Get discoverable tools for description rendering. Falls back to empty array on error. */
84
+ function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableTool[] {
75
85
  try {
76
- return session.getDiscoverableMCPTools?.() ?? [];
86
+ // Prefer generic method; fall back to legacy MCP-only
87
+ if (session.getDiscoverableTools) {
88
+ return session.getDiscoverableTools();
89
+ }
90
+ // Legacy MCP path — adapt DiscoverableMCPTool (with `description`) → DiscoverableTool.
91
+ const legacy = session.getDiscoverableMCPTools?.() ?? [];
92
+ return legacy.map(t => ({
93
+ name: t.name,
94
+ label: t.label,
95
+ summary: t.description,
96
+ source: "mcp" as const,
97
+ serverName: t.serverName,
98
+ mcpToolName: t.mcpToolName,
99
+ schemaKeys: t.schemaKeys,
100
+ }));
77
101
  } catch {
78
102
  return [];
79
103
  }
80
104
  }
81
105
 
82
- function getDiscoverableMCPSearchIndexForExecution(session: ToolSession): DiscoverableMCPSearchIndex {
106
+ function getDiscoverableToolSearchIndexForExecution(session: ToolSession): DiscoverableToolSearchIndex {
83
107
  try {
84
- const cached = session.getDiscoverableMCPSearchIndex?.();
85
- if (cached) return cached;
108
+ // Prefer generic cached index
109
+ if (session.getDiscoverableToolSearchIndex) {
110
+ const cached = session.getDiscoverableToolSearchIndex();
111
+ if (cached) return cached;
112
+ }
113
+ // Legacy MCP: use cached MCP index. Its documents expose `tool.description` as well as
114
+ // `tool.summary`, so it is structurally compatible with DiscoverableToolSearchIndex.
115
+ const mcpCached = session.getDiscoverableMCPSearchIndex?.();
116
+ if (mcpCached) return mcpCached as unknown as DiscoverableToolSearchIndex;
86
117
  } catch {}
87
- return buildDiscoverableMCPSearchIndex(session.getDiscoverableMCPTools?.() ?? []);
118
+ return buildDiscoverableToolSearchIndex(getDiscoverableToolsForDescription(session));
119
+ }
120
+
121
+ /** Resolve the effective selected tool names (generic or legacy MCP). */
122
+ function getSelectedToolNames(session: ToolSession): string[] {
123
+ if (session.getSelectedDiscoveredToolNames) {
124
+ return session.getSelectedDiscoveredToolNames();
125
+ }
126
+ return session.getSelectedMCPToolNames?.() ?? [];
88
127
  }
89
128
 
90
- type MCPDiscoveryExecutionSession = ToolSession & {
91
- isMCPDiscoveryEnabled: () => boolean;
92
- getSelectedMCPToolNames: () => string[];
93
- activateDiscoveredMCPTools: (toolNames: string[]) => Promise<string[]>;
129
+ /** Activate tools (generic or legacy MCP fallback). */
130
+ async function activateTools(session: ToolSession, toolNames: string[]): Promise<string[]> {
131
+ if (session.activateDiscoveredTools) {
132
+ return session.activateDiscoveredTools(toolNames);
133
+ }
134
+ if (session.activateDiscoveredMCPTools) {
135
+ return session.activateDiscoveredMCPTools(toolNames);
136
+ }
137
+ return [];
138
+ }
139
+
140
+ type DiscoveryExecutionSession = ToolSession & {
141
+ _supportsDiscoveryExecution: true;
94
142
  };
95
143
 
96
- function supportsMCPToolDiscoveryExecution(session: ToolSession): session is MCPDiscoveryExecutionSession {
97
- return (
144
+ function supportsToolDiscoveryExecution(session: ToolSession): session is DiscoveryExecutionSession {
145
+ // Supports generic discovery
146
+ if (
147
+ typeof session.isToolDiscoveryEnabled === "function" &&
148
+ typeof session.getSelectedDiscoveredToolNames === "function" &&
149
+ typeof session.activateDiscoveredTools === "function"
150
+ ) {
151
+ return true;
152
+ }
153
+ // Supports legacy MCP discovery
154
+ if (
98
155
  typeof session.isMCPDiscoveryEnabled === "function" &&
99
156
  typeof session.getSelectedMCPToolNames === "function" &&
100
157
  typeof session.activateDiscoveredMCPTools === "function"
101
- );
158
+ ) {
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function isDiscoveryEnabled(session: ToolSession): boolean {
165
+ if (typeof session.isToolDiscoveryEnabled === "function") {
166
+ return session.isToolDiscoveryEnabled();
167
+ }
168
+ return session.isMCPDiscoveryEnabled?.() ?? false;
102
169
  }
103
170
 
104
- export function renderSearchToolBm25Description(discoverableTools: DiscoverableMCPTool[] = []): string {
105
- const summary = summarizeDiscoverableMCPTools(discoverableTools);
171
+ export function renderSearchToolBm25Description(discoverableTools: DiscoverableTool[] = []): string {
172
+ const summary = summarizeDiscoverableTools(discoverableTools);
106
173
  return prompt.render(searchToolBm25Description, {
107
174
  discoverableMCPToolCount: summary.toolCount,
108
- discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableMCPToolServerSummary),
175
+ discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableToolServerSummary),
109
176
  hasDiscoverableMCPServers: summary.servers.length > 0,
110
177
  });
111
178
  }
@@ -134,11 +201,18 @@ function renderFallbackResult(text: string, theme: Theme): Component {
134
201
  return new Text([header, ...bodyLines].join("\n"), 0, 0);
135
202
  }
136
203
 
204
+ /**
205
+ * SearchToolsTool — wire name `search_tool_bm25` (preserved for persisted session back-compat).
206
+ *
207
+ * When tools.discoveryMode === "all", this covers both MCP tools and built-in discoverable tools.
208
+ * When tools.discoveryMode === "mcp-only" or mcp.discoveryMode === true, only MCP tools are searched.
209
+ */
137
210
  export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
138
211
  readonly name = "search_tool_bm25";
139
- readonly label = "SearchToolBm25";
212
+ readonly label = "SearchTools";
213
+ readonly loadMode = "essential";
140
214
  get description(): string {
141
- return renderSearchToolBm25Description(getDiscoverableMCPToolsForDescription(this.session));
215
+ return renderSearchToolBm25Description(getDiscoverableToolsForDescription(this.session));
142
216
  }
143
217
  readonly parameters = searchToolBm25Schema;
144
218
  readonly strict = true;
@@ -146,8 +220,13 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
146
220
  constructor(private readonly session: ToolSession) {}
147
221
 
148
222
  static createIf(session: ToolSession): SearchToolBm25Tool | null {
149
- if (!session.settings.get("mcp.discoveryMode")) return null;
150
- return supportsMCPToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
223
+ // Active when new tools.discoveryMode is non-"off" or legacy mcp.discoveryMode is true
224
+ const toolsDiscoveryMode = session.settings.get("tools.discoveryMode");
225
+ const active =
226
+ (toolsDiscoveryMode !== undefined && toolsDiscoveryMode !== "off") ||
227
+ session.settings.get("mcp.discoveryMode") === true;
228
+ if (!active) return null;
229
+ return supportsToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
151
230
  }
152
231
 
153
232
  async execute(
@@ -157,11 +236,13 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
157
236
  _onUpdate?: AgentToolUpdateCallback<SearchToolBm25Details>,
158
237
  _context?: AgentToolContext,
159
238
  ): Promise<AgentToolResult<SearchToolBm25Details>> {
160
- if (!supportsMCPToolDiscoveryExecution(this.session)) {
161
- throw new ToolError("MCP tool discovery is unavailable in this session.");
239
+ if (!supportsToolDiscoveryExecution(this.session)) {
240
+ throw new ToolError("Tool discovery is unavailable in this session.");
162
241
  }
163
- if (!this.session.isMCPDiscoveryEnabled()) {
164
- throw new ToolError("MCP tool discovery is disabled. Enable mcp.discoveryMode to use search_tool_bm25.");
242
+ if (!isDiscoveryEnabled(this.session)) {
243
+ throw new ToolError(
244
+ "Tool discovery is disabled. Enable tools.discoveryMode or mcp.discoveryMode to use search_tool_bm25.",
245
+ );
165
246
  }
166
247
 
167
248
  const query = params.query.trim();
@@ -173,11 +254,11 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
173
254
  throw new ToolError("Limit must be a positive integer.");
174
255
  }
175
256
 
176
- const searchIndex = getDiscoverableMCPSearchIndexForExecution(this.session);
177
- const selectedToolNames = new Set(this.session.getSelectedMCPToolNames());
178
- let ranked: Array<{ tool: DiscoverableMCPTool; score: number }> = [];
257
+ const searchIndex = getDiscoverableToolSearchIndexForExecution(this.session);
258
+ const selectedToolNames = new Set(getSelectedToolNames(this.session));
259
+ let ranked: Array<{ tool: DiscoverableTool; score: number }> = [];
179
260
  try {
180
- ranked = searchDiscoverableMCPTools(searchIndex, query, searchIndex.documents.length)
261
+ ranked = searchDiscoverableTools(searchIndex, query, searchIndex.documents.length)
181
262
  .filter(result => !selectedToolNames.has(result.tool.name))
182
263
  .slice(0, limit);
183
264
  } catch (error) {
@@ -187,14 +268,19 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
187
268
  throw error;
188
269
  }
189
270
  const activated =
190
- ranked.length > 0 ? await this.session.activateDiscoveredMCPTools(ranked.map(result => result.tool.name)) : [];
271
+ ranked.length > 0
272
+ ? await activateTools(
273
+ this.session,
274
+ ranked.map(result => result.tool.name),
275
+ )
276
+ : [];
191
277
 
192
278
  const details: SearchToolBm25Details = {
193
279
  query,
194
280
  limit,
195
281
  total_tools: searchIndex.documents.length,
196
282
  activated_tools: activated,
197
- active_selected_tools: this.session.getSelectedMCPToolNames(),
283
+ active_selected_tools: getSelectedToolNames(this.session),
198
284
  tools: ranked.map(result => formatMatch(result.tool, result.score)),
199
285
  };
200
286
 
@@ -252,7 +338,7 @@ export const searchToolBm25Renderer = {
252
338
  );
253
339
  if (details.tools.length === 0) {
254
340
  const emptyMessage =
255
- details.total_tools === 0 ? "No discoverable MCP tools are currently loaded." : "No matching tools found.";
341
+ details.total_tools === 0 ? "No discoverable tools are currently loaded." : "No matching tools found.";
256
342
  return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
257
343
  }
258
344
 
@@ -80,6 +80,8 @@ type SearchParams = Static<typeof searchSchema>;
80
80
  export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
81
81
  readonly name = "search";
82
82
  readonly label = "Search";
83
+ readonly loadMode = "discoverable";
84
+ readonly summary = "Search file contents using ripgrep (fast text search)";
83
85
  readonly description: string;
84
86
  readonly parameters = searchSchema;
85
87
  readonly strict = true;
package/src/tools/ssh.ts CHANGED
@@ -120,6 +120,8 @@ type SshToolParams = Static<typeof sshSchema>;
120
120
 
121
121
  export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
122
122
  readonly name = "ssh";
123
+ readonly summary = "Execute a command on a remote host over SSH";
124
+ readonly loadMode = "discoverable";
123
125
  readonly label = "SSH";
124
126
  readonly parameters = sshSchema;
125
127
  readonly concurrency = "exclusive";
@@ -503,11 +503,12 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
503
503
  export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
504
504
  readonly name = "todo_write";
505
505
  readonly label = "Todo Write";
506
+ readonly summary = "Write a structured todo list to track progress within a session";
506
507
  readonly description: string;
507
508
  readonly parameters = todoWriteSchema;
508
509
  readonly concurrency = "exclusive";
509
510
  readonly strict = true;
510
-
511
+ readonly loadMode = "discoverable";
511
512
  constructor(private readonly session: ToolSession) {
512
513
  this.description = prompt.render(todoWriteDescription);
513
514
  }
@@ -166,6 +166,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
166
166
  readonly nonAbortable = true;
167
167
  readonly strict = true;
168
168
  readonly concurrency = "exclusive";
169
+ readonly loadMode = "discoverable";
170
+ readonly summary = "Write content to a file (creates or overwrites)";
169
171
 
170
172
  readonly #writethrough: WritethroughCallback;
171
173
 
@@ -211,6 +211,8 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
211
211
  readonly description: string;
212
212
  readonly parameters = webSearchSchema;
213
213
  readonly strict = true;
214
+ readonly loadMode = "discoverable";
215
+ readonly summary = "Search the web for up-to-date information";
214
216
 
215
217
  constructor(_session: ToolSession) {
216
218
  this.description = prompt.render(webSearchDescription);
@@ -119,6 +119,11 @@ function buildBasicAuthValue(username: string, password: string): string {
119
119
  return Buffer.from(`${username}:${password}`, "utf-8").toString("base64");
120
120
  }
121
121
 
122
+ /** RFC 7617 forbids C0 and C1 control characters in Basic auth credentials. */
123
+ function hasControlCharacters(value: string): boolean {
124
+ return /[\u0000-\u001F\u007F-\u009F]/u.test(value);
125
+ }
126
+
122
127
  /** Find SearXNG authentication from settings or environment. Basic auth takes precedence over bearer tokens. */
123
128
  function findAuth(): SearXNGAuth | null {
124
129
  const basicUsername = findBasicUsername();
@@ -132,6 +137,9 @@ function findAuth(): SearXNGAuth | null {
132
137
  if (basicUsername.includes(":")) {
133
138
  throw new Error("SearXNG Basic auth username cannot contain ':' because RFC 7617 uses it as the separator.");
134
139
  }
140
+ if (hasControlCharacters(basicUsername) || hasControlCharacters(basicPassword)) {
141
+ throw new Error("SearXNG Basic auth credentials must not contain RFC 7617 control characters.");
142
+ }
135
143
  return { type: "basic", value: buildBasicAuthValue(basicUsername, basicPassword) };
136
144
  }
137
145