@oh-my-pi/pi-coding-agent 1.341.0 → 2.1.1337

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 (158) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +10 -9
  5. package/src/bun-imports.d.ts +16 -0
  6. package/src/cli/args.ts +5 -6
  7. package/src/cli/file-processor.ts +3 -3
  8. package/src/cli/list-models.ts +2 -2
  9. package/src/cli/plugin-cli.ts +1 -1
  10. package/src/cli/session-picker.ts +2 -2
  11. package/src/cli/update-cli.ts +273 -0
  12. package/src/cli.ts +1 -1
  13. package/src/config.ts +23 -75
  14. package/src/core/agent-session.ts +158 -16
  15. package/src/core/auth-storage.ts +2 -3
  16. package/src/core/bash-executor.ts +50 -10
  17. package/src/core/compaction/branch-summarization.ts +5 -5
  18. package/src/core/compaction/compaction.ts +3 -3
  19. package/src/core/compaction/index.ts +3 -3
  20. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  21. package/src/core/custom-commands/index.ts +15 -0
  22. package/src/core/custom-commands/loader.ts +232 -0
  23. package/src/core/custom-commands/types.ts +112 -0
  24. package/src/core/custom-tools/index.ts +3 -3
  25. package/src/core/custom-tools/loader.ts +10 -8
  26. package/src/core/custom-tools/types.ts +11 -6
  27. package/src/core/custom-tools/wrapper.ts +2 -1
  28. package/src/core/exec.ts +22 -12
  29. package/src/core/export-html/index.ts +38 -123
  30. package/src/core/export-html/template.css +0 -7
  31. package/src/core/export-html/template.html +3 -4
  32. package/src/core/export-html/template.macro.ts +24 -0
  33. package/src/core/file-mentions.ts +54 -0
  34. package/src/core/hooks/index.ts +5 -5
  35. package/src/core/hooks/loader.ts +21 -16
  36. package/src/core/hooks/runner.ts +6 -6
  37. package/src/core/hooks/tool-wrapper.ts +2 -2
  38. package/src/core/hooks/types.ts +12 -15
  39. package/src/core/index.ts +6 -6
  40. package/src/core/logger.ts +112 -0
  41. package/src/core/mcp/client.ts +3 -3
  42. package/src/core/mcp/config.ts +1 -1
  43. package/src/core/mcp/index.ts +12 -12
  44. package/src/core/mcp/loader.ts +2 -2
  45. package/src/core/mcp/manager.ts +6 -6
  46. package/src/core/mcp/tool-bridge.ts +3 -3
  47. package/src/core/mcp/transports/http.ts +1 -1
  48. package/src/core/mcp/transports/index.ts +2 -2
  49. package/src/core/mcp/transports/stdio.ts +1 -1
  50. package/src/core/messages.ts +22 -0
  51. package/src/core/model-registry.ts +2 -2
  52. package/src/core/model-resolver.ts +2 -2
  53. package/src/core/plugins/doctor.ts +1 -1
  54. package/src/core/plugins/index.ts +6 -6
  55. package/src/core/plugins/installer.ts +4 -4
  56. package/src/core/plugins/loader.ts +4 -9
  57. package/src/core/plugins/manager.ts +5 -5
  58. package/src/core/plugins/paths.ts +3 -3
  59. package/src/core/sdk.ts +77 -35
  60. package/src/core/session-manager.ts +6 -6
  61. package/src/core/settings-manager.ts +16 -3
  62. package/src/core/skills.ts +5 -5
  63. package/src/core/slash-commands.ts +60 -45
  64. package/src/core/system-prompt.ts +6 -6
  65. package/src/core/title-generator.ts +2 -2
  66. package/src/core/tools/bash.ts +32 -155
  67. package/src/core/tools/context.ts +2 -2
  68. package/src/core/tools/edit-diff.ts +3 -3
  69. package/src/core/tools/edit.ts +18 -5
  70. package/src/core/tools/exa/company.ts +3 -3
  71. package/src/core/tools/exa/index.ts +16 -17
  72. package/src/core/tools/exa/linkedin.ts +3 -3
  73. package/src/core/tools/exa/mcp-client.ts +9 -9
  74. package/src/core/tools/exa/render.ts +5 -5
  75. package/src/core/tools/exa/researcher.ts +3 -3
  76. package/src/core/tools/exa/search.ts +6 -5
  77. package/src/core/tools/exa/types.ts +5 -6
  78. package/src/core/tools/exa/websets.ts +3 -3
  79. package/src/core/tools/find.ts +3 -3
  80. package/src/core/tools/grep.ts +3 -3
  81. package/src/core/tools/index.ts +48 -34
  82. package/src/core/tools/ls.ts +4 -4
  83. package/src/core/tools/lsp/client.ts +161 -90
  84. package/src/core/tools/lsp/config.ts +1 -1
  85. package/src/core/tools/lsp/edits.ts +2 -2
  86. package/src/core/tools/lsp/index.ts +15 -13
  87. package/src/core/tools/lsp/render.ts +2 -2
  88. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  89. package/src/core/tools/lsp/utils.ts +1 -1
  90. package/src/core/tools/notebook.ts +1 -1
  91. package/src/core/tools/output.ts +175 -0
  92. package/src/core/tools/read.ts +7 -7
  93. package/src/core/tools/renderers.ts +92 -13
  94. package/src/core/tools/review.ts +268 -0
  95. package/src/core/tools/task/agents.ts +22 -38
  96. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  97. package/src/core/tools/task/commands.ts +31 -10
  98. package/src/core/tools/task/discovery.ts +2 -2
  99. package/src/core/tools/task/executor.ts +145 -28
  100. package/src/core/tools/task/index.ts +78 -30
  101. package/src/core/tools/task/model-resolver.ts +30 -20
  102. package/src/core/tools/task/parallel.ts +1 -1
  103. package/src/core/tools/task/render.ts +219 -30
  104. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  105. package/src/core/tools/task/types.ts +36 -2
  106. package/src/core/tools/web-fetch.ts +5 -3
  107. package/src/core/tools/web-search/auth.ts +1 -1
  108. package/src/core/tools/web-search/index.ts +17 -15
  109. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  110. package/src/core/tools/web-search/providers/exa.ts +3 -5
  111. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  112. package/src/core/tools/web-search/render.ts +3 -3
  113. package/src/core/tools/write.ts +4 -4
  114. package/src/index.ts +29 -18
  115. package/src/main.ts +50 -33
  116. package/src/migrations.ts +3 -3
  117. package/src/modes/index.ts +5 -5
  118. package/src/modes/interactive/components/armin.ts +1 -1
  119. package/src/modes/interactive/components/assistant-message.ts +1 -1
  120. package/src/modes/interactive/components/bash-execution.ts +4 -4
  121. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  122. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  123. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  124. package/src/modes/interactive/components/diff.ts +1 -1
  125. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  126. package/src/modes/interactive/components/footer.ts +5 -5
  127. package/src/modes/interactive/components/hook-editor.ts +2 -2
  128. package/src/modes/interactive/components/hook-input.ts +2 -2
  129. package/src/modes/interactive/components/hook-message.ts +3 -3
  130. package/src/modes/interactive/components/hook-selector.ts +2 -2
  131. package/src/modes/interactive/components/model-selector.ts +281 -59
  132. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  133. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  134. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  135. package/src/modes/interactive/components/session-selector.ts +4 -4
  136. package/src/modes/interactive/components/settings-defs.ts +1 -1
  137. package/src/modes/interactive/components/settings-selector.ts +5 -5
  138. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  139. package/src/modes/interactive/components/theme-selector.ts +2 -2
  140. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  141. package/src/modes/interactive/components/tool-execution.ts +26 -8
  142. package/src/modes/interactive/components/tree-selector.ts +3 -3
  143. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  144. package/src/modes/interactive/components/user-message.ts +1 -1
  145. package/src/modes/interactive/components/welcome.ts +2 -2
  146. package/src/modes/interactive/interactive-mode.ts +86 -42
  147. package/src/modes/interactive/theme/theme.ts +15 -17
  148. package/src/modes/print-mode.ts +4 -3
  149. package/src/modes/rpc/rpc-client.ts +4 -4
  150. package/src/modes/rpc/rpc-mode.ts +22 -12
  151. package/src/modes/rpc/rpc-types.ts +3 -3
  152. package/src/utils/changelog.ts +2 -2
  153. package/src/utils/clipboard.ts +1 -1
  154. package/src/utils/shell-snapshot.ts +218 -0
  155. package/src/utils/shell.ts +93 -13
  156. package/src/utils/tools-manager.ts +1 -1
  157. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  158. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,5 +1,3 @@
1
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
- import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
3
1
  import {
4
2
  appendFileSync,
5
3
  closeSync,
@@ -11,16 +9,18 @@ import {
11
9
  readSync,
12
10
  statSync,
13
11
  writeFileSync,
14
- } from "fs";
15
- import { join, resolve } from "path";
16
- import { getAgentDir as getDefaultAgentDir } from "../config.js";
12
+ } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
15
+ import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
16
+ import { getAgentDir as getDefaultAgentDir } from "../config";
17
17
  import {
18
18
  type BashExecutionMessage,
19
19
  createBranchSummaryMessage,
20
20
  createCompactionSummaryMessage,
21
21
  createHookMessage,
22
22
  type HookMessage,
23
- } from "./messages.js";
23
+ } from "./messages";
24
24
 
25
25
  export const CURRENT_SESSION_VERSION = 2;
26
26
 
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { dirname, join } from "path";
3
- import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CONFIG_DIR_NAME, getAgentDir } from "../config";
4
4
 
5
5
  export interface CompactionSettings {
6
6
  enabled?: boolean; // default: true
@@ -30,6 +30,11 @@ export interface SkillsSettings {
30
30
  includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
31
31
  }
32
32
 
33
+ export interface CommandsSettings {
34
+ enableClaudeUser?: boolean; // default: true (load from ~/.claude/commands/)
35
+ enableClaudeProject?: boolean; // default: true (load from .claude/commands/)
36
+ }
37
+
33
38
  export interface TerminalSettings {
34
39
  showImages?: boolean; // default: true (only relevant if terminal supports images)
35
40
  }
@@ -78,6 +83,7 @@ export interface Settings {
78
83
  hooks?: string[]; // Array of hook file paths
79
84
  customTools?: string[]; // Array of custom tool file paths
80
85
  skills?: SkillsSettings;
86
+ commands?: CommandsSettings;
81
87
  terminal?: TerminalSettings;
82
88
  enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
83
89
  exa?: ExaSettings;
@@ -399,6 +405,13 @@ export class SettingsManager {
399
405
  };
400
406
  }
401
407
 
408
+ getCommandsSettings(): Required<CommandsSettings> {
409
+ return {
410
+ enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
411
+ enableClaudeProject: this.settings.commands?.enableClaudeProject ?? true,
412
+ };
413
+ }
414
+
402
415
  getShowImages(): boolean {
403
416
  return this.settings.terminal?.showImages ?? true;
404
417
  }
@@ -1,9 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
2
4
  import { minimatch } from "minimatch";
3
- import { homedir } from "os";
4
- import { basename, dirname, join, resolve } from "path";
5
- import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
6
- import type { SkillsSettings } from "./settings-manager.js";
5
+ import { CONFIG_DIR_NAME, getAgentDir } from "../config";
6
+ import type { SkillsSettings } from "./settings-manager";
7
7
 
8
8
  /**
9
9
  * Standard frontmatter fields per Agent Skills spec.
@@ -1,6 +1,8 @@
1
- import { existsSync, readdirSync, readFileSync } from "fs";
2
- import { join, resolve } from "path";
3
- import { CONFIG_DIR_NAME, getCommandsDir } from "../config.js";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { CONFIG_DIR_NAME, getCommandsDir } from "../config";
5
+ import { logger } from "./logger";
4
6
 
5
7
  /**
6
8
  * Represents a custom slash command loaded from a file
@@ -98,14 +100,12 @@ export function substituteArgs(content: string, args: string[]): string {
98
100
  return result;
99
101
  }
100
102
 
103
+ type CommandSource = "builtin" | "claude-user" | "claude-project" | "user" | "project";
104
+
101
105
  /**
102
106
  * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands
103
107
  */
104
- function loadCommandsFromDir(
105
- dir: string,
106
- source: "builtin" | "user" | "project",
107
- subdir: string = "",
108
- ): FileSlashCommand[] {
108
+ function loadCommandsFromDir(dir: string, source: CommandSource, subdir: string = ""): FileSlashCommand[] {
109
109
  const commands: FileSlashCommand[] = [];
110
110
 
111
111
  if (!existsSync(dir)) {
@@ -129,15 +129,18 @@ function loadCommandsFromDir(
129
129
 
130
130
  const name = entry.name.slice(0, -3); // Remove .md extension
131
131
 
132
- // Build source string
133
- let sourceStr: string;
134
- if (source === "builtin") {
135
- sourceStr = subdir ? `(builtin:${subdir})` : "(builtin)";
136
- } else if (source === "user") {
137
- sourceStr = subdir ? `(user:${subdir})` : "(user)";
138
- } else {
139
- sourceStr = subdir ? `(project:${subdir})` : "(project)";
140
- }
132
+ // Build source string based on source type
133
+ const sourceLabel =
134
+ source === "builtin"
135
+ ? "builtin"
136
+ : source === "claude-user"
137
+ ? "claude-user"
138
+ : source === "claude-project"
139
+ ? "claude-project"
140
+ : source === "user"
141
+ ? "user"
142
+ : "project";
143
+ const sourceStr = subdir ? `(${sourceLabel}:${subdir})` : `(${sourceLabel})`;
141
144
 
142
145
  // Get description from frontmatter or first non-empty line
143
146
  let description = frontmatter.description || "";
@@ -159,13 +162,13 @@ function loadCommandsFromDir(
159
162
  content,
160
163
  source: sourceStr,
161
164
  });
162
- } catch (_error) {
163
- // Silently skip files that can't be read
165
+ } catch (err) {
166
+ logger.debug("Failed to read slash command file", { error: String(err) });
164
167
  }
165
168
  }
166
169
  }
167
- } catch (_error) {
168
- // Silently skip directories that can't be read
170
+ } catch (err) {
171
+ logger.debug("Failed to read slash command directory", { error: String(err) });
169
172
  }
170
173
 
171
174
  return commands;
@@ -176,54 +179,66 @@ export interface LoadSlashCommandsOptions {
176
179
  cwd?: string;
177
180
  /** Agent config directory for global commands. Default: from getCommandsDir() */
178
181
  agentDir?: string;
182
+ /** Enable loading from ~/.claude/commands/. Default: true */
183
+ enableClaudeUser?: boolean;
184
+ /** Enable loading from .claude/commands/. Default: true */
185
+ enableClaudeProject?: boolean;
179
186
  }
180
187
 
181
188
  /**
182
189
  * Load all custom slash commands from:
183
190
  * 1. Builtin: package commands/
184
- * 2. Global: agentDir/commands/
185
- * 3. Project: cwd/{CONFIG_DIR_NAME}/commands/
191
+ * 2. Claude user: ~/.claude/commands/
192
+ * 3. Claude project: .claude/commands/
193
+ * 4. Pi user: agentDir/commands/
194
+ * 5. Pi project: cwd/{CONFIG_DIR_NAME}/commands/
195
+ *
196
+ * First occurrence wins (earlier sources have priority).
186
197
  */
187
198
  export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
188
199
  const resolvedCwd = options.cwd ?? process.cwd();
189
200
  const resolvedAgentDir = options.agentDir ?? getCommandsDir();
201
+ const enableClaudeUser = options.enableClaudeUser ?? true;
202
+ const enableClaudeProject = options.enableClaudeProject ?? true;
190
203
 
191
204
  const commands: FileSlashCommand[] = [];
192
205
  const seenNames = new Set<string>();
193
206
 
194
- // 1. Builtin commands (from package)
195
- const builtinDir = join(import.meta.dir, "../commands");
196
- if (existsSync(builtinDir)) {
197
- const builtinCommands = loadCommandsFromDir(builtinDir, "builtin");
198
- for (const cmd of builtinCommands) {
207
+ const addCommands = (newCommands: FileSlashCommand[]) => {
208
+ for (const cmd of newCommands) {
199
209
  if (!seenNames.has(cmd.name)) {
200
210
  commands.push(cmd);
201
211
  seenNames.add(cmd.name);
202
212
  }
203
213
  }
214
+ };
215
+
216
+ // 1. Builtin commands (from package)
217
+ const builtinDir = join(import.meta.dir, "../commands");
218
+ if (existsSync(builtinDir)) {
219
+ addCommands(loadCommandsFromDir(builtinDir, "builtin"));
204
220
  }
205
221
 
206
- // 2. Load global commands from agentDir/commands/
207
- // Note: if agentDir is provided, it should be the agent dir, not the commands dir
208
- const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
209
- const globalCommands = loadCommandsFromDir(globalCommandsDir, "user");
210
- for (const cmd of globalCommands) {
211
- if (!seenNames.has(cmd.name)) {
212
- commands.push(cmd);
213
- seenNames.add(cmd.name);
214
- }
222
+ // 2. Claude user commands (~/.claude/commands/)
223
+ if (enableClaudeUser) {
224
+ const claudeUserDir = join(homedir(), ".claude", "commands");
225
+ addCommands(loadCommandsFromDir(claudeUserDir, "claude-user"));
215
226
  }
216
227
 
217
- // 3. Load project commands from cwd/{CONFIG_DIR_NAME}/commands/
218
- const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
219
- const projectCommands = loadCommandsFromDir(projectCommandsDir, "project");
220
- for (const cmd of projectCommands) {
221
- if (!seenNames.has(cmd.name)) {
222
- commands.push(cmd);
223
- seenNames.add(cmd.name);
224
- }
228
+ // 3. Claude project commands (.claude/commands/)
229
+ if (enableClaudeProject) {
230
+ const claudeProjectDir = resolve(resolvedCwd, ".claude", "commands");
231
+ addCommands(loadCommandsFromDir(claudeProjectDir, "claude-project"));
225
232
  }
226
233
 
234
+ // 4. Pi user commands (agentDir/commands/)
235
+ const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
236
+ addCommands(loadCommandsFromDir(globalCommandsDir, "user"));
237
+
238
+ // 5. Pi project commands (cwd/{CONFIG_DIR_NAME}/commands/)
239
+ const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
240
+ addCommands(loadCommandsFromDir(projectCommandsDir, "project"));
241
+
227
242
  return commands;
228
243
  }
229
244
 
@@ -2,13 +2,13 @@
2
2
  * System prompt construction and project context loading
3
3
  */
4
4
 
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
5
7
  import chalk from "chalk";
6
- import { existsSync, readFileSync } from "fs";
7
- import { join, resolve } from "path";
8
- import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
9
- import type { SkillsSettings } from "./settings-manager.js";
10
- import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
11
- import type { ToolName } from "./tools/index.js";
8
+ import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config";
9
+ import type { SkillsSettings } from "./settings-manager";
10
+ import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
11
+ import type { ToolName } from "./tools/index";
12
12
 
13
13
  /**
14
14
  * Execute a git command synchronously and return stdout or null on failure.
@@ -4,8 +4,8 @@
4
4
 
5
5
  import type { Model } from "@oh-my-pi/pi-ai";
6
6
  import { completeSimple } from "@oh-my-pi/pi-ai";
7
- import type { ModelRegistry } from "./model-registry.js";
8
- import { findSmolModel } from "./model-resolver.js";
7
+ import type { ModelRegistry } from "./model-registry";
8
+ import { findSmolModel } from "./model-resolver";
9
9
 
10
10
  const TITLE_SYSTEM_PROMPT = `Generate a very short title (3-6 words) for a coding session based on the user's first message. The title should capture the main task or topic. Output ONLY the title, nothing else. No quotes, no punctuation at the end.
11
11
 
@@ -1,20 +1,7 @@
1
- import { createWriteStream } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
5
2
  import { Type } from "@sinclair/typebox";
6
- import type { Subprocess } from "bun";
7
- import { getShellConfig, killProcessTree } from "../../utils/shell.js";
8
- import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
9
-
10
- /**
11
- * Generate a unique temp file path for bash output
12
- */
13
- function getTempFilePath(): string {
14
- const randomId = crypto.getRandomValues(new Uint8Array(8));
15
- const id = Array.from(randomId, (b) => b.toString(16).padStart(2, "0")).join("");
16
- return join(tmpdir(), `pi-bash-${id}.log`);
17
- }
3
+ import { executeBash } from "../bash-executor";
4
+ import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
18
5
 
19
6
  const bashSchema = Type.Object({
20
7
  command: Type.String({ description: "Bash command to execute" }),
@@ -74,140 +61,34 @@ Usage notes:
74
61
  signal?: AbortSignal,
75
62
  onUpdate?,
76
63
  ) => {
77
- const { shell, args } = getShellConfig();
78
- const child: Subprocess = Bun.spawn([shell, ...args, command], {
79
- cwd,
80
- stdin: "ignore",
81
- stdout: "pipe",
82
- stderr: "pipe",
83
- });
84
-
85
- // We'll stream to a temp file if output gets large
86
- let tempFilePath: string | undefined;
87
- let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
88
- let totalBytes = 0;
89
-
90
- // Keep a rolling buffer of the last chunks for tail truncation
91
- const chunks: Buffer[] = [];
92
- let chunksBytes = 0;
93
- const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
94
-
95
- let timedOut = false;
96
- let aborted = false;
97
-
98
- // Handle abort signal
99
- const onAbort = () => {
100
- aborted = true;
101
- if (child.pid) {
102
- killProcessTree(child.pid);
103
- }
104
- };
64
+ // Track output for streaming updates
65
+ let currentOutput = "";
105
66
 
106
- if (signal) {
107
- if (signal.aborted) {
108
- child.kill();
109
- throw new Error("Command aborted");
110
- }
111
- signal.addEventListener("abort", onAbort, { once: true });
112
- }
113
-
114
- // Set timeout if provided
115
- let timeoutHandle: Timer | undefined;
116
- if (timeout !== undefined && timeout > 0) {
117
- timeoutHandle = setTimeout(() => {
118
- timedOut = true;
119
- onAbort();
120
- }, timeout * 1000);
121
- }
122
-
123
- const handleData = (data: Buffer) => {
124
- totalBytes += data.length;
125
-
126
- // Start writing to temp file once we exceed the threshold
127
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
128
- tempFilePath = getTempFilePath();
129
- tempFileStream = createWriteStream(tempFilePath);
130
- for (const chunk of chunks) {
131
- tempFileStream.write(chunk);
132
- }
133
- }
134
-
135
- if (tempFileStream) {
136
- tempFileStream.write(data);
137
- }
138
-
139
- // Keep rolling buffer of recent data
140
- chunks.push(data);
141
- chunksBytes += data.length;
142
-
143
- while (chunksBytes > maxChunksBytes && chunks.length > 1) {
144
- const removed = chunks.shift()!;
145
- chunksBytes -= removed.length;
146
- }
147
-
148
- // Stream partial output to callback
149
- if (onUpdate) {
150
- const fullBuffer = Buffer.concat(chunks);
151
- const fullText = fullBuffer.toString("utf-8");
152
- const truncation = truncateTail(fullText);
153
- onUpdate({
154
- content: [{ type: "text", text: truncation.content || "" }],
155
- details: {
156
- truncation: truncation.truncated ? truncation : undefined,
157
- fullOutputPath: tempFilePath,
158
- },
159
- });
160
- }
161
- };
162
-
163
- // Read streams using Bun's ReadableStream API
164
- const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
165
- const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
166
-
167
- await Promise.all([
168
- (async () => {
169
- while (true) {
170
- const { done, value } = await stdoutReader.read();
171
- if (done) break;
172
- handleData(Buffer.from(value));
173
- }
174
- })(),
175
- (async () => {
176
- while (true) {
177
- const { done, value } = await stderrReader.read();
178
- if (done) break;
179
- handleData(Buffer.from(value));
67
+ const result = await executeBash(command, {
68
+ cwd,
69
+ timeout: timeout ? timeout * 1000 : undefined, // Convert to milliseconds
70
+ signal,
71
+ onChunk: (chunk) => {
72
+ currentOutput += chunk;
73
+ if (onUpdate) {
74
+ const truncation = truncateTail(currentOutput);
75
+ onUpdate({
76
+ content: [{ type: "text", text: truncation.content || "" }],
77
+ details: {
78
+ truncation: truncation.truncated ? truncation : undefined,
79
+ },
80
+ });
180
81
  }
181
- })(),
182
- ]);
183
-
184
- const exitCode = await child.exited;
185
-
186
- // Cleanup
187
- if (timeoutHandle) clearTimeout(timeoutHandle);
188
- if (signal) signal.removeEventListener("abort", onAbort);
189
- if (tempFileStream) tempFileStream.end();
190
-
191
- // Combine all buffered chunks
192
- const fullBuffer = Buffer.concat(chunks);
193
- const fullOutput = fullBuffer.toString("utf-8");
194
-
195
- if (aborted && !timedOut) {
196
- let output = fullOutput;
197
- if (output) output += "\n\n";
198
- output += "Command aborted";
199
- throw new Error(output);
200
- }
82
+ },
83
+ });
201
84
 
202
- if (timedOut) {
203
- let output = fullOutput;
204
- if (output) output += "\n\n";
205
- output += `Command timed out after ${timeout} seconds`;
206
- throw new Error(output);
85
+ // Handle errors
86
+ if (result.cancelled) {
87
+ throw new Error(result.output || "Command aborted");
207
88
  }
208
89
 
209
- // Apply tail truncation
210
- const truncation = truncateTail(fullOutput);
90
+ // Apply tail truncation for final output
91
+ const truncation = truncateTail(result.output);
211
92
  let outputText = truncation.content || "(no output)";
212
93
 
213
94
  let details: BashToolDetails | undefined;
@@ -215,28 +96,24 @@ Usage notes:
215
96
  if (truncation.truncated) {
216
97
  details = {
217
98
  truncation,
218
- fullOutputPath: tempFilePath,
99
+ fullOutputPath: result.fullOutputPath,
219
100
  };
220
101
 
221
102
  const startLine = truncation.totalLines - truncation.outputLines + 1;
222
103
  const endLine = truncation.totalLines;
223
104
 
224
105
  if (truncation.lastLinePartial) {
225
- const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
226
- outputText += `\n\n[Showing last ${formatSize(
227
- truncation.outputBytes,
228
- )} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
106
+ const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
107
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
229
108
  } else if (truncation.truncatedBy === "lines") {
230
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
109
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
231
110
  } else {
232
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(
233
- DEFAULT_MAX_BYTES,
234
- )} limit). Full output: ${tempFilePath}]`;
111
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
235
112
  }
236
113
  }
237
114
 
238
- if (exitCode !== 0 && exitCode !== null) {
239
- outputText += `\n\nCommand exited with code ${exitCode}`;
115
+ if (result.exitCode !== 0 && result.exitCode !== undefined) {
116
+ outputText += `\n\nCommand exited with code ${result.exitCode}`;
240
117
  throw new Error(outputText);
241
118
  }
242
119
 
@@ -1,6 +1,6 @@
1
1
  import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
- import type { CustomToolContext } from "../custom-tools/types.js";
3
- import type { HookUIContext } from "../hooks/types.js";
2
+ import type { CustomToolContext } from "../custom-tools/types";
3
+ import type { HookUIContext } from "../hooks/types";
4
4
 
5
5
  declare module "@oh-my-pi/pi-agent-core" {
6
6
  interface AgentToolContext extends CustomToolContext {
@@ -3,10 +3,10 @@
3
3
  * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
4
4
  */
5
5
 
6
+ import { constants } from "node:fs";
7
+ import { access, readFile } from "node:fs/promises";
6
8
  import * as Diff from "diff";
7
- import { constants } from "fs";
8
- import { access, readFile } from "fs/promises";
9
- import { resolveToCwd } from "./path-utils.js";
9
+ import { resolveToCwd } from "./path-utils";
10
10
 
11
11
  export function detectLineEnding(content: string): "\r\n" | "\n" {
12
12
  const crlfIdx = content.indexOf("\r\n");
@@ -1,7 +1,7 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readFile, writeFile } from "node:fs/promises";
1
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
4
  import { Type } from "@sinclair/typebox";
3
- import { constants } from "fs";
4
- import { access, readFile, writeFile } from "fs/promises";
5
5
  import {
6
6
  DEFAULT_FUZZY_THRESHOLD,
7
7
  detectLineEnding,
@@ -11,9 +11,9 @@ import {
11
11
  normalizeToLF,
12
12
  restoreLineEndings,
13
13
  stripBom,
14
- } from "./edit-diff.js";
15
- import type { FileDiagnosticsResult } from "./lsp/index.js";
16
- import { resolveToCwd } from "./path-utils.js";
14
+ } from "./edit-diff";
15
+ import type { FileDiagnosticsResult } from "./lsp/index";
16
+ import { resolveToCwd } from "./path-utils";
17
17
 
18
18
  const editSchema = Type.Object({
19
19
  path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
@@ -63,6 +63,19 @@ Usage:
63
63
  ) => {
64
64
  const absolutePath = resolveToCwd(path, cwd);
65
65
 
66
+ // Reject .ipynb files - use NotebookEdit tool instead
67
+ if (absolutePath.endsWith(".ipynb")) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: "Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.",
73
+ },
74
+ ],
75
+ details: undefined,
76
+ };
77
+ }
78
+
66
79
  return new Promise<{
67
80
  content: Array<{ type: "text"; text: string }>;
68
81
  details: EditToolDetails | undefined;
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import { Type } from "@sinclair/typebox";
8
- import type { CustomTool } from "../../custom-tools/types.js";
9
- import type { ExaRenderDetails } from "./types.js";
8
+ import type { CustomTool } from "../../custom-tools/types";
9
+ import type { ExaRenderDetails } from "./types";
10
10
 
11
11
  /** exa_company - Company research */
12
12
  export const companyTool: CustomTool<any, ExaRenderDetails> = {
@@ -34,7 +34,7 @@ Parameters:
34
34
  details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
35
35
  };
36
36
  }
37
- const response = await callExaTool("company_research_exa", params, apiKey);
37
+ const response = await callExaTool("company_research", params, apiKey);
38
38
 
39
39
  if (isSearchResponse(response)) {
40
40
  const formatted = formatSearchResults(response);
@@ -9,14 +9,14 @@
9
9
  * - 14 websets tools (CRUD, items, search, enrichment, monitor)
10
10
  */
11
11
 
12
- import type { CustomTool } from "../../custom-tools/types.js";
13
- import type { ExaSettings } from "../../settings-manager.js";
14
- import { companyTool } from "./company.js";
15
- import { linkedinTool } from "./linkedin.js";
16
- import { researcherTools } from "./researcher.js";
17
- import { searchTools } from "./search.js";
18
- import type { ExaRenderDetails } from "./types.js";
19
- import { websetsTools } from "./websets.js";
12
+ import type { CustomTool } from "../../custom-tools/types";
13
+ import type { ExaSettings } from "../../settings-manager";
14
+ import { companyTool } from "./company";
15
+ import { linkedinTool } from "./linkedin";
16
+ import { researcherTools } from "./researcher";
17
+ import { searchTools } from "./search";
18
+ import type { ExaRenderDetails } from "./types";
19
+ import { websetsTools } from "./websets";
20
20
 
21
21
  /** All Exa tools (22 total) - static export for backward compatibility */
22
22
  export const exaTools: CustomTool<any, ExaRenderDetails>[] = [
@@ -42,9 +42,8 @@ export function getExaTools(settings: Required<ExaSettings>): CustomTool<any, Ex
42
42
  return tools;
43
43
  }
44
44
 
45
- export { companyTool } from "./company.js";
46
- export { linkedinTool } from "./linkedin.js";
47
- export { logExaError, logViewError } from "./logger.js";
45
+ export { companyTool } from "./company";
46
+ export { linkedinTool } from "./linkedin";
48
47
  export {
49
48
  callExaTool,
50
49
  callWebsetsTool,
@@ -54,11 +53,11 @@ export {
54
53
  findApiKey,
55
54
  formatSearchResults,
56
55
  isSearchResponse,
57
- } from "./mcp-client.js";
58
- export { renderExaCall, renderExaResult } from "./render.js";
59
- export { researcherTools } from "./researcher.js";
56
+ } from "./mcp-client";
57
+ export { renderExaCall, renderExaResult } from "./render";
58
+ export { researcherTools } from "./researcher";
60
59
  // Re-export individual modules for selective importing
61
- export { searchTools } from "./search.js";
60
+ export { searchTools } from "./search";
62
61
  // Re-export types and utilities
63
- export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types.js";
64
- export { websetsTools } from "./websets.js";
62
+ export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types";
63
+ export { websetsTools } from "./websets";