@oh-my-pi/pi-coding-agent 3.21.0 → 3.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/docs/sdk.md +47 -50
  3. package/examples/custom-tools/README.md +0 -15
  4. package/examples/hooks/custom-compaction.ts +1 -3
  5. package/examples/sdk/README.md +6 -10
  6. package/package.json +5 -5
  7. package/src/cli/args.ts +9 -6
  8. package/src/core/agent-session.ts +3 -3
  9. package/src/core/custom-tools/wrapper.ts +0 -1
  10. package/src/core/extensions/index.ts +1 -6
  11. package/src/core/extensions/wrapper.ts +0 -7
  12. package/src/core/file-mentions.ts +5 -8
  13. package/src/core/sdk.ts +41 -111
  14. package/src/core/session-manager.ts +7 -0
  15. package/src/core/system-prompt.ts +22 -33
  16. package/src/core/tools/ask.ts +14 -7
  17. package/src/core/tools/bash-interceptor.ts +4 -4
  18. package/src/core/tools/bash.ts +19 -9
  19. package/src/core/tools/context.ts +7 -0
  20. package/src/core/tools/edit.ts +8 -15
  21. package/src/core/tools/exa/render.ts +4 -16
  22. package/src/core/tools/find.ts +7 -18
  23. package/src/core/tools/git.ts +13 -3
  24. package/src/core/tools/grep.ts +7 -18
  25. package/src/core/tools/index.test.ts +180 -0
  26. package/src/core/tools/index.ts +94 -237
  27. package/src/core/tools/ls.ts +4 -9
  28. package/src/core/tools/lsp/index.ts +32 -29
  29. package/src/core/tools/lsp/render.ts +7 -28
  30. package/src/core/tools/notebook.ts +3 -5
  31. package/src/core/tools/output.ts +5 -17
  32. package/src/core/tools/read.ts +8 -19
  33. package/src/core/tools/review.ts +0 -18
  34. package/src/core/tools/rulebook.ts +8 -2
  35. package/src/core/tools/task/agents.ts +28 -7
  36. package/src/core/tools/task/discovery.ts +0 -6
  37. package/src/core/tools/task/executor.ts +264 -254
  38. package/src/core/tools/task/index.ts +45 -220
  39. package/src/core/tools/task/render.ts +21 -11
  40. package/src/core/tools/task/types.ts +6 -11
  41. package/src/core/tools/task/worker-protocol.ts +17 -0
  42. package/src/core/tools/task/worker.ts +238 -0
  43. package/src/core/tools/web-fetch.ts +4 -36
  44. package/src/core/tools/web-search/index.ts +2 -1
  45. package/src/core/tools/web-search/render.ts +1 -4
  46. package/src/core/tools/write.ts +7 -15
  47. package/src/discovery/helpers.test.ts +1 -1
  48. package/src/index.ts +5 -16
  49. package/src/main.ts +4 -4
  50. package/src/modes/interactive/theme/theme.ts +4 -4
  51. package/src/prompts/task.md +0 -7
  52. package/src/prompts/tools/output.md +2 -2
  53. package/src/prompts/tools/task.md +68 -0
  54. package/examples/custom-tools/question/index.ts +0 -84
  55. package/examples/custom-tools/subagent/README.md +0 -172
  56. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  57. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  58. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  59. package/examples/custom-tools/subagent/agents.ts +0 -156
  60. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  61. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  62. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  63. package/examples/custom-tools/subagent/index.ts +0 -1002
  64. package/examples/sdk/05-tools.ts +0 -94
  65. package/examples/sdk/12-full-control.ts +0 -95
  66. package/src/prompts/browser.md +0 -71
package/src/core/sdk.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * const session = await createAgentSession({
19
19
  * model: myModel,
20
20
  * getApiKey: async () => process.env.MY_KEY,
21
- * tools: [readTool, bashTool],
21
+ * toolNames: ["read", "bash", "edit", "write"], // Filter tools
22
22
  * extensions: [],
23
23
  * skills: [],
24
24
  * sessionFile: false,
@@ -55,7 +55,7 @@ import {
55
55
  loadExtensionFromFactory,
56
56
  type ToolDefinition,
57
57
  wrapRegisteredTools,
58
- wrapToolsWithExtensions,
58
+ wrapToolWithExtensions,
59
59
  } from "./extensions/index";
60
60
  import { logger } from "./logger";
61
61
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index";
@@ -75,48 +75,34 @@ import { time } from "./timings";
75
75
  import { createToolContextStore } from "./tools/context";
76
76
  import { getGeminiImageTools } from "./tools/gemini-image";
77
77
  import {
78
- allTools,
79
- applyBashInterception,
80
- baseCodingToolNames,
81
- bashTool,
82
- codingTools,
83
- createAllTools,
78
+ BUILTIN_TOOLS,
84
79
  createBashTool,
85
- createCodingTools,
86
80
  createEditTool,
87
81
  createFindTool,
88
82
  createGitTool,
89
83
  createGrepTool,
90
84
  createLsTool,
91
- createReadOnlyTools,
92
85
  createReadTool,
93
- createRulebookTool,
86
+ createTools,
94
87
  createWriteTool,
95
- editTool,
96
88
  filterRulebookRules,
97
- findTool,
98
89
  getWebSearchTools,
99
- gitTool,
100
- grepTool,
101
- lsTool,
102
- readOnlyTools,
103
- readTool,
104
90
  setPreferredImageProvider,
105
91
  setPreferredWebSearchProvider,
106
92
  type Tool,
107
- type ToolName,
93
+ type ToolSession,
108
94
  warmupLspServers,
109
- writeTool,
110
95
  } from "./tools/index";
111
96
  import { createTtsrManager } from "./ttsr";
112
97
 
113
98
  // Types
114
-
115
99
  export interface CreateAgentSessionOptions {
116
100
  /** Working directory for project-local discovery. Default: process.cwd() */
117
101
  cwd?: string;
118
102
  /** Global config directory. Default: ~/.omp/agent */
119
103
  agentDir?: string;
104
+ /** Spawns to allow. Default: "*" */
105
+ spawns?: string;
120
106
 
121
107
  /** Auth storage for credentials. Default: discoverAuthStorage(agentDir) */
122
108
  authStorage?: AuthStorage;
@@ -133,8 +119,6 @@ export interface CreateAgentSessionOptions {
133
119
  /** System prompt. String replaces default, function receives default and returns final. */
134
120
  systemPrompt?: string | ((defaultPrompt: string) => string);
135
121
 
136
- /** Built-in tools to use. Default: all coding tools (read, bash, edit, write, grep, find, ls, lsp, notebook, output, task, web_fetch, web_search) */
137
- tools?: Tool[];
138
122
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
139
123
  customTools?: (CustomTool | ToolDefinition)[];
140
124
  /** Inline extensions (merged with discovery). */
@@ -163,7 +147,7 @@ export interface CreateAgentSessionOptions {
163
147
  enableMCP?: boolean;
164
148
 
165
149
  /** Tool names explicitly requested (enables disabled-by-default tools) */
166
- explicitTools?: string[];
150
+ toolNames?: string[];
167
151
 
168
152
  /** Session manager. Default: SessionManager.create(cwd) */
169
153
  sessionManager?: SessionManager;
@@ -208,21 +192,11 @@ export type { FileSlashCommand } from "./slash-commands";
208
192
  export type { Tool } from "./tools/index";
209
193
 
210
194
  export {
211
- // Pre-built tools (use process.cwd())
212
- readTool,
213
- bashTool,
214
- editTool,
215
- writeTool,
216
- grepTool,
217
- findTool,
218
- gitTool,
219
- lsTool,
220
- codingTools,
221
- readOnlyTools,
222
- allTools as allBuiltInTools,
223
- // Tool factories (for custom cwd)
224
- createCodingTools,
225
- createReadOnlyTools,
195
+ // Tool factories
196
+ BUILTIN_TOOLS,
197
+ createTools,
198
+ type ToolSession,
199
+ // Individual tool factories (for custom usage)
226
200
  createReadTool,
227
201
  createBashTool,
228
202
  createEditTool,
@@ -426,7 +400,6 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
426
400
  label: tool.label,
427
401
  description: tool.description,
428
402
  parameters: tool.parameters,
429
- hidden: tool.hidden,
430
403
  execute: (toolCallId, params, onUpdate, ctx, signal) =>
431
404
  tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
432
405
  onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
@@ -509,7 +482,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
509
482
  * model: myModel,
510
483
  * getApiKey: async () => process.env.MY_KEY,
511
484
  * systemPrompt: 'You are helpful.',
512
- * tools: [readTool, bashTool],
485
+ * tools: codingTools({ cwd: process.cwd() }),
513
486
  * skills: [],
514
487
  * sessionManager: SessionManager.inMemory(),
515
488
  * });
@@ -630,30 +603,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
630
603
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
631
604
  time("discoverContextFiles");
632
605
 
633
- const sessionContext = {
606
+ const toolSession: ToolSession = {
607
+ cwd,
608
+ hasUI: options.hasUI ?? false,
609
+ rulebookRules,
610
+ eventBus,
634
611
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
612
+ getSessionSpawns: () => options.spawns ?? "*",
613
+ settings: settingsManager,
635
614
  };
636
- const allBuiltInToolsMap = await createAllTools(cwd, sessionContext, {
637
- lspFormatOnWrite: settingsManager.getLspFormatOnWrite(),
638
- lspDiagnosticsOnWrite: settingsManager.getLspDiagnosticsOnWrite(),
639
- lspDiagnosticsOnEdit: settingsManager.getLspDiagnosticsOnEdit(),
640
- editFuzzyMatch: settingsManager.getEditFuzzyMatch(),
641
- readAutoResizeImages: settingsManager.getImageAutoResize(),
642
- });
643
- time("createAllTools");
644
615
 
645
- // Determine which tools to include based on settings
646
- let baseToolNames = options.tools
647
- ? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allBuiltInToolsMap)
648
- : [...baseCodingToolNames];
649
-
650
- // Filter out git tool if disabled in settings
651
- if (!settingsManager.getGitToolEnabled()) {
652
- baseToolNames = baseToolNames.filter((name) => name !== "git");
653
- }
654
-
655
- const initialActiveToolNames: ToolName[] = baseToolNames;
656
- const initialActiveBuiltInTools = initialActiveToolNames.map((name) => allBuiltInToolsMap[name]);
616
+ const builtinTools = await createTools(toolSession, options.toolNames);
617
+ time("createAllTools");
657
618
 
658
619
  // Discover MCP tools from .mcp.json files
659
620
  let mcpManager: MCPManager | undefined;
@@ -840,61 +801,30 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
840
801
  hasQueuedMessages: () => session.queuedMessageCount > 0,
841
802
  }));
842
803
 
804
+ // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
843
805
  const toolRegistry = new Map<string, AgentTool>();
844
- for (const [name, tool] of Object.entries(allBuiltInToolsMap)) {
845
- toolRegistry.set(name, tool as AgentTool);
806
+ for (const tool of builtinTools) {
807
+ toolRegistry.set(tool.name, tool as AgentTool);
846
808
  }
847
- for (const tool of wrappedExtensionTools as AgentTool[]) {
809
+ for (const tool of wrappedExtensionTools) {
848
810
  toolRegistry.set(tool.name, tool);
849
811
  }
850
-
851
- let activeToolsArray: Tool[] = [...initialActiveBuiltInTools, ...wrappedExtensionTools];
852
-
853
- if (rulebookRules.length > 0) {
854
- activeToolsArray.push(createRulebookTool(rulebookRules));
855
- }
856
-
857
- if (options.explicitTools) {
858
- const explicitSet = new Set(options.explicitTools);
859
- activeToolsArray = activeToolsArray.filter((tool) => !tool.hidden || explicitSet.has(tool.name));
860
- } else {
861
- activeToolsArray = activeToolsArray.filter((tool) => !tool.hidden);
862
- }
863
- time("combineTools");
864
-
865
- if (settingsManager.getBashInterceptorEnabled()) {
866
- activeToolsArray = applyBashInterception(activeToolsArray);
867
- }
868
- time("applyBashInterception");
869
-
870
- let wrappedToolRegistry: Map<string, AgentTool> | undefined;
871
812
  if (extensionRunner) {
872
- activeToolsArray = wrapToolsWithExtensions(activeToolsArray as AgentTool[], extensionRunner);
873
- const allRegistryTools = Array.from(toolRegistry.values());
874
- const wrappedAllTools = wrapToolsWithExtensions(allRegistryTools, extensionRunner);
875
- wrappedToolRegistry = new Map<string, AgentTool>();
876
- for (const tool of wrappedAllTools) {
877
- wrappedToolRegistry.set(tool.name, tool);
813
+ for (const tool of toolRegistry.values()) {
814
+ toolRegistry.set(tool.name, wrapToolWithExtensions(tool, extensionRunner));
878
815
  }
879
816
  }
817
+ time("combineTools");
880
818
 
881
- const rebuildSystemPrompt = (toolNames: string[]): string => {
882
- const validToolNames = toolNames.filter((n): n is ToolName => n in allBuiltInToolsMap);
883
- const extraToolDescriptions = toolNames
884
- .filter((name) => !(name in allBuiltInToolsMap))
885
- .map((name) => {
886
- const tool = toolRegistry.get(name);
887
- if (!tool) return null;
888
- return { name, description: tool.description || tool.label || "Custom tool" };
889
- })
890
- .filter((tool): tool is { name: string; description: string } => tool !== null);
819
+ const rebuildSystemPrompt = (toolNames: string[], tools: Map<string, AgentTool>): string => {
820
+ toolContextStore.setToolNames(toolNames);
891
821
  const defaultPrompt = buildSystemPromptInternal({
892
822
  cwd,
893
823
  skills,
894
824
  contextFiles,
895
- rulebookRules,
896
- selectedTools: validToolNames,
897
- extraToolDescriptions,
825
+ tools,
826
+ toolNames,
827
+ rules: rulebookRules,
898
828
  skillsSettings: settingsManager.getSkillsSettings(),
899
829
  });
900
830
 
@@ -906,9 +836,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
906
836
  cwd,
907
837
  skills,
908
838
  contextFiles,
909
- rulebookRules,
910
- selectedTools: validToolNames,
911
- extraToolDescriptions,
839
+ tools,
840
+ toolNames,
841
+ rules: rulebookRules,
912
842
  skillsSettings: settingsManager.getSkillsSettings(),
913
843
  customPrompt: options.systemPrompt,
914
844
  });
@@ -916,7 +846,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
916
846
  return options.systemPrompt(defaultPrompt);
917
847
  };
918
848
 
919
- const systemPrompt = rebuildSystemPrompt(initialActiveToolNames);
849
+ const systemPrompt = rebuildSystemPrompt(Array.from(toolRegistry.keys()), toolRegistry);
920
850
  time("buildSystemPrompt");
921
851
 
922
852
  const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
@@ -936,7 +866,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
936
866
  systemPrompt,
937
867
  model,
938
868
  thinkingLevel,
939
- tools: activeToolsArray,
869
+ tools: Array.from(toolRegistry.values()),
940
870
  },
941
871
  convertToLlm,
942
872
  transformContext: extensionRunner
@@ -984,7 +914,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
984
914
  customCommands: customCommandsResult.commands,
985
915
  skillsSettings: settingsManager.getSkillsSettings(),
986
916
  modelRegistry,
987
- toolRegistry: wrappedToolRegistry ?? toolRegistry,
917
+ toolRegistry,
988
918
  rebuildSystemPrompt,
989
919
  ttsrManager,
990
920
  });
@@ -698,6 +698,13 @@ async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
698
698
  let changed = false;
699
699
  const result: Record<string, unknown> = {};
700
700
  for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
701
+ // Strip transient/redundant properties that shouldn't be persisted
702
+ // - partialJson: streaming accumulator for tool call JSON parsing
703
+ // - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
704
+ if (k === "partialJson" || k === "jsonlEvents") {
705
+ changed = true;
706
+ continue;
707
+ }
701
708
  const newV = await truncateForPersistence(v, k);
702
709
  result[k] = newV;
703
710
  if (newV !== v) changed = true;
@@ -256,10 +256,10 @@ export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): st
256
256
  export interface BuildSystemPromptOptions {
257
257
  /** Custom system prompt (replaces default). */
258
258
  customPrompt?: string;
259
- /** Tools to include in prompt. Default: [read, bash, edit, write] */
260
- selectedTools?: ToolName[];
261
- /** Extra tool descriptions to include in prompt (non built-in tools). */
262
- extraToolDescriptions?: Array<{ name: string; description: string }>;
259
+ /** Tools to include in prompt. */
260
+ tools?: Map<string, { description: string; label: string }>;
261
+ /** Tool names to include in prompt. */
262
+ toolNames?: string[];
263
263
  /** Text to append to system prompt. */
264
264
  appendSystemPrompt?: string;
265
265
  /** Skills settings for discovery. */
@@ -271,21 +271,21 @@ export interface BuildSystemPromptOptions {
271
271
  /** Pre-loaded skills (skips discovery if provided). */
272
272
  skills?: Skill[];
273
273
  /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
274
- rulebookRules?: Rule[];
274
+ rules?: Rule[];
275
275
  }
276
276
 
277
277
  /** Build the system prompt with tools, guidelines, and context */
278
278
  export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
279
279
  const {
280
280
  customPrompt,
281
- selectedTools,
282
- extraToolDescriptions = [],
281
+ tools,
283
282
  appendSystemPrompt,
284
283
  skillsSettings,
284
+ toolNames,
285
285
  cwd,
286
286
  contextFiles: providedContextFiles,
287
287
  skills: providedSkills,
288
- rulebookRules,
288
+ rules: rulebookRules,
289
289
  } = options;
290
290
  const resolvedCwd = cwd ?? process.cwd();
291
291
  const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
@@ -311,6 +311,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
311
311
  // Resolve context files: use provided or discover
312
312
  const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
313
313
 
314
+ // Build tools list based on selected tools
315
+ const toolsList = toolNames?.map((name) => `- ${name}: ${toolDescriptions[name as ToolName]}`).join("\n") ?? "";
316
+
314
317
  // Resolve skills: use provided or discover
315
318
  const skills =
316
319
  providedSkills ??
@@ -335,9 +338,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
335
338
  }
336
339
 
337
340
  // Append custom tool descriptions if provided
338
- if (extraToolDescriptions.length > 0) {
339
- prompt += "\n\n# Additional Tools\n\n";
340
- prompt += extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
341
+ if (tools && tools.size > 0) {
342
+ prompt += "\n\n# Tools\n\n";
343
+ prompt += Array.from(tools.entries())
344
+ .map(([name, { description }]) => `- ${name}: ${description}`)
345
+ .join("\n");
341
346
  }
342
347
 
343
348
  // Append git context if in a git repo
@@ -347,8 +352,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
347
352
  }
348
353
 
349
354
  // Append skills section (only if read tool is available)
350
- const customPromptHasRead = !selectedTools || selectedTools.includes("read");
351
- if (customPromptHasRead && skills.length > 0) {
355
+ if (tools?.has("read") && skills.length > 0) {
352
356
  prompt += formatSkillsForPrompt(skills);
353
357
  }
354
358
 
@@ -369,25 +373,16 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
369
373
  const docsPath = getDocsPath();
370
374
  const examplesPath = getExamplesPath();
371
375
 
372
- // Build tools list based on selected tools
373
- const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
374
- const builtInToolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
375
- const extraToolsList =
376
- extraToolDescriptions.length > 0
377
- ? extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n")
378
- : "";
379
- const toolsList = [builtInToolsList, extraToolsList].filter(Boolean).join("\n");
380
-
381
376
  // Generate anti-bash rules (returns null if not applicable)
382
- const antiBashSection = generateAntiBashRules(tools);
377
+ const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
383
378
 
384
379
  // Build guidelines based on which tools are actually available
385
380
  const guidelinesList: string[] = [];
386
381
 
387
- const hasBash = tools.includes("bash");
388
- const hasEdit = tools.includes("edit");
389
- const hasWrite = tools.includes("write");
390
- const hasRead = tools.includes("read");
382
+ const hasBash = tools?.has("bash");
383
+ const hasEdit = tools?.has("edit");
384
+ const hasWrite = tools?.has("write");
385
+ const hasRead = tools?.has("read");
391
386
 
392
387
  // Read-only mode notice (no bash, edit, or write)
393
388
  if (!hasBash && !hasEdit && !hasWrite) {
@@ -454,12 +449,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
454
449
  }
455
450
  }
456
451
 
457
- // Append custom tool descriptions if provided
458
- if (extraToolDescriptions.length > 0) {
459
- prompt += "\n\n# Additional Tools\n\n";
460
- prompt += extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
461
- }
462
-
463
452
  // Append git context if in a git repo
464
453
  const gitContext = loadGitContext(resolvedCwd);
465
454
  if (gitContext) {
@@ -22,6 +22,7 @@ import { Type } from "@sinclair/typebox";
22
22
  import { type Theme, theme } from "../../modes/interactive/theme/theme";
23
23
  import askDescription from "../../prompts/tools/ask.md" with { type: "text" };
24
24
  import type { RenderResultOptions } from "../custom-tools/types";
25
+ import type { ToolSession } from "./index";
25
26
  import { formatErrorMessage, formatMeta } from "./render-utils";
26
27
 
27
28
  // =============================================================================
@@ -67,7 +68,10 @@ function getDoneOptionLabel(): string {
67
68
  // Tool Implementation
68
69
  // =============================================================================
69
70
 
70
- export function createAskTool(_cwd: string): AgentTool<typeof askSchema, AskToolDetails> {
71
+ export function createAskTool(session: ToolSession): null | AgentTool<typeof askSchema, AskToolDetails> {
72
+ if (!session.hasUI) {
73
+ return null;
74
+ }
71
75
  return {
72
76
  name: "ask",
73
77
  label: "Ask",
@@ -193,8 +197,14 @@ export function createAskTool(_cwd: string): AgentTool<typeof askSchema, AskTool
193
197
  };
194
198
  }
195
199
 
196
- /** Default ask tool using process.cwd() - for backwards compatibility (no UI) */
197
- export const askTool = createAskTool(process.cwd());
200
+ /** Default ask tool - returns null when no UI */
201
+ export const askTool = createAskTool({
202
+ cwd: process.cwd(),
203
+ hasUI: false,
204
+ rulebookRules: [],
205
+ getSessionFile: () => null,
206
+ getSessionSpawns: () => "*",
207
+ });
198
208
 
199
209
  // =============================================================================
200
210
  // TUI Renderer
@@ -225,10 +235,7 @@ export const askToolRenderer = {
225
235
  const opt = args.options[i];
226
236
  const isLast = i === args.options.length - 1;
227
237
  const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
228
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
229
- "dim",
230
- uiTheme.checkbox.unchecked,
231
- )} ${uiTheme.fg("muted", opt.label)}`;
238
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${uiTheme.fg("muted", opt.label)}`;
232
239
  }
233
240
  }
234
241
 
@@ -80,13 +80,13 @@ const forbiddenPatterns: Array<{
80
80
  * @param availableTools Set of tool names that are available
81
81
  * @returns InterceptionResult indicating if the command should be blocked
82
82
  */
83
- export function checkBashInterception(command: string, availableTools: Set<string>): InterceptionResult {
83
+ export function checkBashInterception(command: string, availableTools: string[]): InterceptionResult {
84
84
  // Normalize command for pattern matching
85
85
  const normalizedCommand = command.trim();
86
86
 
87
87
  for (const { pattern, tool, message } of forbiddenPatterns) {
88
88
  // Only block if the suggested tool is actually available
89
- if (!availableTools.has(tool)) {
89
+ if (!availableTools.includes(tool)) {
90
90
  continue;
91
91
  }
92
92
 
@@ -106,8 +106,8 @@ export function checkBashInterception(command: string, availableTools: Set<strin
106
106
  * Check if a command is a simple directory listing that should use `ls` tool.
107
107
  * Only applies to bare `ls` without complex flags.
108
108
  */
109
- export function checkSimpleLsInterception(command: string, availableTools: Set<string>): InterceptionResult {
110
- if (!availableTools.has("ls")) {
109
+ export function checkSimpleLsInterception(command: string, availableTools: string[]): InterceptionResult {
110
+ if (!availableTools.includes("ls")) {
111
111
  return { block: false };
112
112
  }
113
113
 
@@ -1,4 +1,4 @@
1
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
1
+ import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { Type } from "@sinclair/typebox";
@@ -6,6 +6,8 @@ import type { Theme } from "../../modes/interactive/theme/theme";
6
6
  import bashDescription from "../../prompts/tools/bash.md" with { type: "text" };
7
7
  import { executeBash } from "../bash-executor";
8
8
  import type { RenderResultOptions } from "../custom-tools/types";
9
+ import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
10
+ import type { ToolSession } from "./index";
9
11
  import { formatBytes, wrapBrackets } from "./render-utils";
10
12
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
11
13
 
@@ -19,7 +21,7 @@ export interface BashToolDetails {
19
21
  fullOutputPath?: string;
20
22
  }
21
23
 
22
- export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
24
+ export function createBashTool(session: ToolSession): AgentTool<typeof bashSchema> {
23
25
  return {
24
26
  name: "bash",
25
27
  label: "Bash",
@@ -30,12 +32,25 @@ export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
30
32
  { command, timeout }: { command: string; timeout?: number },
31
33
  signal?: AbortSignal,
32
34
  onUpdate?,
35
+ ctx?: AgentToolContext,
33
36
  ) => {
37
+ // Check interception if enabled and available tools are known
38
+ if (session.settings?.getBashInterceptorEnabled()) {
39
+ const interception = checkBashInterception(command, ctx?.toolNames ?? []);
40
+ if (interception.block) {
41
+ throw new Error(interception.message);
42
+ }
43
+ const lsInterception = checkSimpleLsInterception(command, ctx?.toolNames ?? []);
44
+ if (lsInterception.block) {
45
+ throw new Error(lsInterception.message);
46
+ }
47
+ }
48
+
34
49
  // Track output for streaming updates
35
50
  let currentOutput = "";
36
51
 
37
52
  const result = await executeBash(command, {
38
- cwd,
53
+ cwd: session.cwd,
39
54
  timeout: timeout ? timeout * 1000 : undefined, // Convert to milliseconds
40
55
  signal,
41
56
  onChunk: (chunk) => {
@@ -92,9 +107,6 @@ export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
92
107
  };
93
108
  }
94
109
 
95
- /** Default bash tool using process.cwd() - for backwards compatibility */
96
- export const bashTool = createBashTool(process.cwd());
97
-
98
110
  // =============================================================================
99
111
  // TUI Renderer
100
112
  // =============================================================================
@@ -183,9 +195,7 @@ export const bashToolRenderer = {
183
195
  warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
184
196
  } else {
185
197
  warnings.push(
186
- `Truncated: ${truncation.outputLines} lines shown (${formatBytes(
187
- truncation.maxBytes ?? DEFAULT_MAX_BYTES,
188
- )} limit)`,
198
+ `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
189
199
  );
190
200
  }
191
201
  }
@@ -6,27 +6,34 @@ declare module "@oh-my-pi/pi-agent-core" {
6
6
  interface AgentToolContext extends CustomToolContext {
7
7
  ui?: ExtensionUIContext;
8
8
  hasUI?: boolean;
9
+ toolNames?: string[];
9
10
  }
10
11
  }
11
12
 
12
13
  export interface ToolContextStore {
13
14
  getContext(): AgentToolContext;
14
15
  setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
16
+ setToolNames(names: string[]): void;
15
17
  }
16
18
 
17
19
  export function createToolContextStore(getBaseContext: () => CustomToolContext): ToolContextStore {
18
20
  let uiContext: ExtensionUIContext | undefined;
19
21
  let hasUI = false;
22
+ let toolNames: string[] = [];
20
23
 
21
24
  return {
22
25
  getContext: () => ({
23
26
  ...getBaseContext(),
24
27
  ui: uiContext,
25
28
  hasUI,
29
+ toolNames,
26
30
  }),
27
31
  setUIContext: (context, uiAvailable) => {
28
32
  uiContext = context;
29
33
  hasUI = uiAvailable;
30
34
  },
35
+ setToolNames: (names) => {
36
+ toolNames = names;
37
+ },
31
38
  };
32
39
  }
@@ -17,7 +17,8 @@ import {
17
17
  restoreLineEndings,
18
18
  stripBom,
19
19
  } from "./edit-diff";
20
- import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
20
+ import type { ToolSession } from "./index";
21
+ import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
21
22
  import { resolveToCwd } from "./path-utils";
22
23
  import {
23
24
  formatDiagnostics,
@@ -46,16 +47,11 @@ export interface EditToolDetails {
46
47
  diagnostics?: FileDiagnosticsResult;
47
48
  }
48
49
 
49
- export interface EditToolOptions {
50
- /** Whether to accept high-confidence fuzzy matches for whitespace/indentation (default: true) */
51
- fuzzyMatch?: boolean;
52
- /** Writethrough callback to get LSP diagnostics after editing a file */
53
- writethrough?: WritethroughCallback;
54
- }
55
-
56
- export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
57
- const allowFuzzy = options.fuzzyMatch ?? true;
58
- const writethrough = options.writethrough ?? writethroughNoop;
50
+ export function createEditTool(session: ToolSession): AgentTool<typeof editSchema> {
51
+ const allowFuzzy = session.settings?.getEditFuzzyMatch() ?? true;
52
+ const enableDiagnostics = session.settings?.getLspDiagnosticsOnEdit() ?? false;
53
+ const enableFormat = session.settings?.getLspFormatOnWrite() ?? true;
54
+ const writethrough = createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics });
59
55
  return {
60
56
  name: "edit",
61
57
  label: "Edit",
@@ -71,7 +67,7 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
71
67
  throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
72
68
  }
73
69
 
74
- const absolutePath = resolveToCwd(path, cwd);
70
+ const absolutePath = resolveToCwd(path, session.cwd);
75
71
 
76
72
  const file = Bun.file(absolutePath);
77
73
  if (!(await file.exists())) {
@@ -203,9 +199,6 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
203
199
  };
204
200
  }
205
201
 
206
- /** Default edit tool using process.cwd() - for backwards compatibility */
207
- export const editTool = createEditTool(process.cwd());
208
-
209
202
  // =============================================================================
210
203
  // TUI Renderer
211
204
  // =============================================================================