@oh-my-pi/pi-coding-agent 3.21.0 → 3.25.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 (71) hide show
  1. package/CHANGELOG.md +55 -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-commands/bundled/wt/index.ts +3 -0
  10. package/src/core/custom-tools/wrapper.ts +0 -1
  11. package/src/core/extensions/index.ts +1 -6
  12. package/src/core/extensions/wrapper.ts +0 -7
  13. package/src/core/file-mentions.ts +5 -8
  14. package/src/core/sdk.ts +48 -111
  15. package/src/core/session-manager.ts +7 -0
  16. package/src/core/system-prompt.ts +22 -33
  17. package/src/core/tools/ask.ts +14 -7
  18. package/src/core/tools/bash-interceptor.ts +4 -4
  19. package/src/core/tools/bash.ts +19 -9
  20. package/src/core/tools/complete.ts +131 -0
  21. package/src/core/tools/context.ts +7 -0
  22. package/src/core/tools/edit.ts +8 -15
  23. package/src/core/tools/exa/render.ts +4 -16
  24. package/src/core/tools/find.ts +7 -18
  25. package/src/core/tools/git.ts +13 -3
  26. package/src/core/tools/grep.ts +7 -18
  27. package/src/core/tools/index.test.ts +188 -0
  28. package/src/core/tools/index.ts +106 -236
  29. package/src/core/tools/jtd-to-json-schema.ts +274 -0
  30. package/src/core/tools/ls.ts +4 -9
  31. package/src/core/tools/lsp/index.ts +32 -29
  32. package/src/core/tools/lsp/render.ts +7 -28
  33. package/src/core/tools/notebook.ts +3 -5
  34. package/src/core/tools/output.ts +130 -31
  35. package/src/core/tools/read.ts +8 -19
  36. package/src/core/tools/review.ts +0 -18
  37. package/src/core/tools/rulebook.ts +8 -2
  38. package/src/core/tools/task/agents.ts +28 -7
  39. package/src/core/tools/task/artifacts.ts +6 -9
  40. package/src/core/tools/task/discovery.ts +0 -6
  41. package/src/core/tools/task/executor.ts +306 -257
  42. package/src/core/tools/task/index.ts +65 -235
  43. package/src/core/tools/task/name-generator.ts +247 -0
  44. package/src/core/tools/task/render.ts +158 -19
  45. package/src/core/tools/task/types.ts +13 -11
  46. package/src/core/tools/task/worker-protocol.ts +18 -0
  47. package/src/core/tools/task/worker.ts +270 -0
  48. package/src/core/tools/web-fetch.ts +4 -36
  49. package/src/core/tools/web-search/index.ts +2 -1
  50. package/src/core/tools/web-search/render.ts +1 -4
  51. package/src/core/tools/write.ts +7 -15
  52. package/src/discovery/helpers.test.ts +1 -1
  53. package/src/index.ts +5 -16
  54. package/src/main.ts +4 -4
  55. package/src/modes/interactive/theme/theme.ts +4 -4
  56. package/src/prompts/task.md +14 -57
  57. package/src/prompts/tools/output.md +4 -3
  58. package/src/prompts/tools/task.md +70 -0
  59. package/examples/custom-tools/question/index.ts +0 -84
  60. package/examples/custom-tools/subagent/README.md +0 -172
  61. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  62. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  63. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  64. package/examples/custom-tools/subagent/agents.ts +0 -156
  65. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  66. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  67. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  68. package/examples/custom-tools/subagent/index.ts +0 -1002
  69. package/examples/sdk/05-tools.ts +0 -94
  70. package/examples/sdk/12-full-control.ts +0 -95
  71. 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,12 @@ 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[];
151
+
152
+ /** Output schema for structured completion (subagents) */
153
+ outputSchema?: unknown;
154
+ /** Whether to include the complete tool by default */
155
+ requireCompleteTool?: boolean;
167
156
 
168
157
  /** Session manager. Default: SessionManager.create(cwd) */
169
158
  sessionManager?: SessionManager;
@@ -208,21 +197,11 @@ export type { FileSlashCommand } from "./slash-commands";
208
197
  export type { Tool } from "./tools/index";
209
198
 
210
199
  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,
200
+ // Tool factories
201
+ BUILTIN_TOOLS,
202
+ createTools,
203
+ type ToolSession,
204
+ // Individual tool factories (for custom usage)
226
205
  createReadTool,
227
206
  createBashTool,
228
207
  createEditTool,
@@ -426,7 +405,6 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
426
405
  label: tool.label,
427
406
  description: tool.description,
428
407
  parameters: tool.parameters,
429
- hidden: tool.hidden,
430
408
  execute: (toolCallId, params, onUpdate, ctx, signal) =>
431
409
  tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
432
410
  onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
@@ -509,7 +487,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
509
487
  * model: myModel,
510
488
  * getApiKey: async () => process.env.MY_KEY,
511
489
  * systemPrompt: 'You are helpful.',
512
- * tools: [readTool, bashTool],
490
+ * tools: codingTools({ cwd: process.cwd() }),
513
491
  * skills: [],
514
492
  * sessionManager: SessionManager.inMemory(),
515
493
  * });
@@ -630,30 +608,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
630
608
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
631
609
  time("discoverContextFiles");
632
610
 
633
- const sessionContext = {
611
+ const toolSession: ToolSession = {
612
+ cwd,
613
+ hasUI: options.hasUI ?? false,
614
+ rulebookRules,
615
+ eventBus,
616
+ outputSchema: options.outputSchema,
617
+ requireCompleteTool: options.requireCompleteTool,
634
618
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
619
+ getSessionSpawns: () => options.spawns ?? "*",
620
+ settings: settingsManager,
635
621
  };
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
622
 
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]);
623
+ const builtinTools = await createTools(toolSession, options.toolNames);
624
+ time("createAllTools");
657
625
 
658
626
  // Discover MCP tools from .mcp.json files
659
627
  let mcpManager: MCPManager | undefined;
@@ -840,61 +808,30 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
840
808
  hasQueuedMessages: () => session.queuedMessageCount > 0,
841
809
  }));
842
810
 
811
+ // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
843
812
  const toolRegistry = new Map<string, AgentTool>();
844
- for (const [name, tool] of Object.entries(allBuiltInToolsMap)) {
845
- toolRegistry.set(name, tool as AgentTool);
813
+ for (const tool of builtinTools) {
814
+ toolRegistry.set(tool.name, tool as AgentTool);
846
815
  }
847
- for (const tool of wrappedExtensionTools as AgentTool[]) {
816
+ for (const tool of wrappedExtensionTools) {
848
817
  toolRegistry.set(tool.name, tool);
849
818
  }
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
819
  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);
820
+ for (const tool of toolRegistry.values()) {
821
+ toolRegistry.set(tool.name, wrapToolWithExtensions(tool, extensionRunner));
878
822
  }
879
823
  }
824
+ time("combineTools");
880
825
 
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);
826
+ const rebuildSystemPrompt = (toolNames: string[], tools: Map<string, AgentTool>): string => {
827
+ toolContextStore.setToolNames(toolNames);
891
828
  const defaultPrompt = buildSystemPromptInternal({
892
829
  cwd,
893
830
  skills,
894
831
  contextFiles,
895
- rulebookRules,
896
- selectedTools: validToolNames,
897
- extraToolDescriptions,
832
+ tools,
833
+ toolNames,
834
+ rules: rulebookRules,
898
835
  skillsSettings: settingsManager.getSkillsSettings(),
899
836
  });
900
837
 
@@ -906,9 +843,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
906
843
  cwd,
907
844
  skills,
908
845
  contextFiles,
909
- rulebookRules,
910
- selectedTools: validToolNames,
911
- extraToolDescriptions,
846
+ tools,
847
+ toolNames,
848
+ rules: rulebookRules,
912
849
  skillsSettings: settingsManager.getSkillsSettings(),
913
850
  customPrompt: options.systemPrompt,
914
851
  });
@@ -916,7 +853,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
916
853
  return options.systemPrompt(defaultPrompt);
917
854
  };
918
855
 
919
- const systemPrompt = rebuildSystemPrompt(initialActiveToolNames);
856
+ const systemPrompt = rebuildSystemPrompt(Array.from(toolRegistry.keys()), toolRegistry);
920
857
  time("buildSystemPrompt");
921
858
 
922
859
  const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
@@ -936,7 +873,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
936
873
  systemPrompt,
937
874
  model,
938
875
  thinkingLevel,
939
- tools: activeToolsArray,
876
+ tools: Array.from(toolRegistry.values()),
940
877
  },
941
878
  convertToLlm,
942
879
  transformContext: extensionRunner
@@ -984,7 +921,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
984
921
  customCommands: customCommandsResult.commands,
985
922
  skillsSettings: settingsManager.getSkillsSettings(),
986
923
  modelRegistry,
987
- toolRegistry: wrappedToolRegistry ?? toolRegistry,
924
+ toolRegistry,
988
925
  rebuildSystemPrompt,
989
926
  ttsrManager,
990
927
  });
@@ -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
  }