@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.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 (114) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +6 -7
  3. package/src/cli/session-picker.ts +27 -28
  4. package/src/cli/setup-cli.ts +7 -16
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/config.ts +1 -1
  7. package/src/core/agent-session.ts +202 -37
  8. package/src/core/agent-storage.ts +1 -1
  9. package/src/core/auth-storage.ts +15 -25
  10. package/src/core/bash-executor.ts +63 -105
  11. package/src/core/custom-commands/loader.ts +1 -1
  12. package/src/core/custom-tools/loader.ts +1 -1
  13. package/src/core/custom-tools/types.ts +1 -2
  14. package/src/core/exec.ts +16 -100
  15. package/src/core/extensions/index.ts +1 -7
  16. package/src/core/extensions/loader.ts +1 -1
  17. package/src/core/extensions/runner.ts +1 -1
  18. package/src/core/extensions/types.ts +2 -2
  19. package/src/core/extensions/wrapper.ts +15 -20
  20. package/src/core/frontmatter.ts +1 -1
  21. package/src/core/history-storage.ts +3 -6
  22. package/src/core/hooks/index.ts +2 -2
  23. package/src/core/hooks/loader.ts +1 -1
  24. package/src/core/hooks/tool-wrapper.ts +14 -26
  25. package/src/core/hooks/types.ts +1 -2
  26. package/src/core/keybindings.ts +1 -1
  27. package/src/core/mcp/client.ts +13 -13
  28. package/src/core/mcp/json-rpc.ts +1 -1
  29. package/src/core/mcp/loader.ts +1 -1
  30. package/src/core/mcp/manager.ts +2 -2
  31. package/src/core/mcp/tool-cache.ts +1 -1
  32. package/src/core/mcp/transports/http.ts +32 -70
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/plugins/installer.ts +13 -11
  35. package/src/core/prompt-templates.ts +4 -9
  36. package/src/core/python-executor.ts +23 -18
  37. package/src/core/python-gateway-coordinator.ts +29 -28
  38. package/src/core/python-kernel.ts +230 -211
  39. package/src/core/sdk.ts +10 -13
  40. package/src/core/session-manager.ts +1 -1
  41. package/src/core/settings-manager.ts +22 -9
  42. package/src/core/skills.ts +1 -1
  43. package/src/core/ssh/connection-manager.ts +19 -33
  44. package/src/core/ssh/ssh-executor.ts +39 -35
  45. package/src/core/ssh/sshfs-mount.ts +14 -33
  46. package/src/core/storage-migration.ts +1 -1
  47. package/src/core/streaming-output.ts +183 -127
  48. package/src/core/system-prompt.ts +119 -79
  49. package/src/core/title-generator.ts +1 -1
  50. package/src/core/tools/ask.ts +2 -2
  51. package/src/core/tools/bash.ts +3 -3
  52. package/src/core/tools/calculator.ts +1 -1
  53. package/src/core/tools/exa/mcp-client.ts +1 -1
  54. package/src/core/tools/exa/render.ts +1 -1
  55. package/src/core/tools/find.ts +39 -71
  56. package/src/core/tools/gemini-image.ts +1 -1
  57. package/src/core/tools/grep.ts +88 -100
  58. package/src/core/tools/index.ts +1 -1
  59. package/src/core/tools/ls.ts +1 -1
  60. package/src/core/tools/lsp/client.ts +50 -50
  61. package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
  62. package/src/core/tools/lsp/config.ts +1 -1
  63. package/src/core/tools/lsp/index.ts +2 -4
  64. package/src/core/tools/lsp/lspmux.ts +1 -1
  65. package/src/core/tools/lsp/rust-analyzer.ts +2 -2
  66. package/src/core/tools/lsp/utils.ts +0 -14
  67. package/src/core/tools/notebook.ts +1 -1
  68. package/src/core/tools/patch/shared.ts +3 -4
  69. package/src/core/tools/python.ts +3 -3
  70. package/src/core/tools/read.ts +29 -68
  71. package/src/core/tools/render-utils.ts +0 -5
  72. package/src/core/tools/ssh.ts +3 -3
  73. package/src/core/tools/task/model-resolver.ts +7 -9
  74. package/src/core/tools/task/worker.ts +144 -139
  75. package/src/core/tools/todo-write.ts +1 -1
  76. package/src/core/tools/truncate.ts +2 -2
  77. package/src/core/tools/web-fetch.ts +13 -15
  78. package/src/core/tools/web-scrapers/types.ts +1 -3
  79. package/src/core/tools/web-scrapers/utils.ts +14 -13
  80. package/src/core/tools/web-scrapers/youtube.ts +39 -12
  81. package/src/core/tools/web-search/auth.ts +1 -1
  82. package/src/core/tools/write.ts +1 -1
  83. package/src/core/ttsr.ts +1 -1
  84. package/src/core/utils.ts +1 -187
  85. package/src/core/voice-controller.ts +1 -1
  86. package/src/core/voice-supervisor.ts +11 -38
  87. package/src/core/voice.ts +1 -8
  88. package/src/discovery/codex.ts +1 -1
  89. package/src/index.ts +4 -4
  90. package/src/main.ts +5 -10
  91. package/src/migrations.ts +1 -1
  92. package/src/modes/index.ts +7 -40
  93. package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
  94. package/src/modes/interactive/components/hook-editor.ts +12 -9
  95. package/src/modes/interactive/components/login-dialog.ts +24 -11
  96. package/src/modes/interactive/components/settings-defs.ts +9 -0
  97. package/src/modes/interactive/components/status-line.ts +36 -35
  98. package/src/modes/interactive/components/todo-display.ts +1 -1
  99. package/src/modes/interactive/components/tool-execution.ts +1 -1
  100. package/src/modes/interactive/controllers/command-controller.ts +50 -84
  101. package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
  102. package/src/modes/interactive/controllers/input-controller.ts +12 -11
  103. package/src/modes/interactive/interactive-mode.ts +10 -11
  104. package/src/modes/interactive/theme/theme.ts +1 -1
  105. package/src/modes/interactive/types.ts +1 -1
  106. package/src/modes/rpc/rpc-client.ts +91 -121
  107. package/src/modes/rpc/rpc-mode.ts +71 -79
  108. package/src/prompts/system/ttsr-interrupt.md +7 -0
  109. package/src/utils/clipboard.ts +57 -141
  110. package/src/utils/shell-snapshot.ts +12 -60
  111. package/src/utils/shell.ts +35 -56
  112. package/src/utils/tools-manager.ts +42 -71
  113. package/src/core/logger.ts +0 -111
  114. package/src/modes/cleanup.ts +0 -23
@@ -1,179 +1,137 @@
1
1
  /**
2
2
  * Bash command execution with streaming support and cancellation.
3
3
  *
4
- * This module provides a unified bash execution implementation used by:
5
- * - AgentSession.executeBash() for interactive and RPC modes
6
- * - Direct calls from modes that need bash execution
4
+ * Provides unified bash execution for AgentSession.executeBash() and direct calls.
7
5
  */
8
6
 
9
- import type { Subprocess } from "bun";
10
- import { getShellConfig, killProcessTree } from "../utils/shell";
7
+ import { cspawn, Exception } from "@oh-my-pi/pi-utils";
8
+ import { getShellConfig } from "../utils/shell";
11
9
  import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
12
- import { OutputSink, pumpStream } from "./streaming-output";
10
+ import { OutputSink } from "./streaming-output";
13
11
  import type { BashOperations } from "./tools/bash";
14
- import { DEFAULT_MAX_BYTES } from "./tools/truncate";
15
- import { ScopeSignal } from "./utils";
16
-
17
- // ============================================================================
18
- // Types
19
- // ============================================================================
20
12
 
21
13
  export interface BashExecutorOptions {
22
- /** Working directory for command execution */
23
14
  cwd?: string;
24
- /** Timeout in milliseconds */
25
15
  timeout?: number;
26
- /** Callback for streaming output chunks (already sanitized) */
27
16
  onChunk?: (chunk: string) => void;
28
- /** AbortSignal for cancellation */
29
17
  signal?: AbortSignal;
30
18
  }
31
19
 
32
20
  export interface BashResult {
33
- /** Combined stdout + stderr output (sanitized, possibly truncated) */
34
21
  output: string;
35
- /** Process exit code (undefined if killed/cancelled) */
36
22
  exitCode: number | undefined;
37
- /** Whether the command was cancelled via signal */
38
23
  cancelled: boolean;
39
- /** Whether the output was truncated */
40
24
  truncated: boolean;
41
- /** Path to temp file containing full output (if output exceeded truncation threshold) */
42
25
  fullOutputPath?: string;
43
26
  }
44
27
 
45
- // ============================================================================
46
- // Implementation
47
- // ============================================================================
48
-
49
- /**
50
- * Execute a bash command with optional streaming and cancellation support.
51
- *
52
- * Features:
53
- * - Streams sanitized output via onChunk callback
54
- * - Writes large output to temp file for later retrieval
55
- * - Supports cancellation via AbortSignal
56
- * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)
57
- * - Truncates output if it exceeds the default max bytes
58
- *
59
- * @param command - The bash command to execute
60
- * @param options - Optional streaming callback and abort signal
61
- * @returns Promise resolving to execution result
62
- */
63
28
  export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
64
29
  const { shell, args, env, prefix } = await getShellConfig();
65
30
 
66
- // Get or create shell snapshot (for aliases, functions, options)
67
31
  const snapshotPath = await getOrCreateSnapshot(shell, env);
68
32
  const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
69
33
 
70
- // Build final command: snapshot + prefix + command
71
34
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
72
35
  const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
73
36
 
74
- using signal = new ScopeSignal(options);
37
+ const stream = new OutputSink({ onLine: options?.onChunk });
75
38
 
76
- const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
39
+ const child = cspawn([shell, ...args, finalCommand], {
77
40
  cwd: options?.cwd,
78
- stdin: "ignore",
79
- stdout: "pipe",
80
- stderr: "pipe",
81
41
  env,
42
+ signal: options?.signal,
43
+ timeout: options?.timeout,
82
44
  });
83
45
 
84
- signal.catch(() => {
85
- killProcessTree(child.pid);
86
- });
46
+ // Pump streams - errors during abort/timeout are expected
47
+ await Promise.allSettled([
48
+ child.stdout.pipeTo(stream.createWritable()),
49
+ child.stderr.pipeTo(stream.createWritable()),
50
+ ])
51
+ .then(() => stream.close())
52
+ .catch(() => {});
87
53
 
88
- const sink = new OutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
89
-
90
- const writer = sink.getWriter();
54
+ // Wait for process exit
91
55
  try {
92
- await Promise.all([
93
- pumpStream(child.stdout as ReadableStream<Uint8Array>, writer),
94
- pumpStream(child.stderr as ReadableStream<Uint8Array>, writer),
95
- ]);
96
- } finally {
97
- await writer.close();
98
- }
99
-
100
- // Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
101
- const exitCode = await child.exited;
102
-
103
- const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
104
-
105
- if (signal.timedOut()) {
106
- const secs = Math.round(options!.timeout! / 1000);
56
+ await child.exited;
107
57
  return {
108
- exitCode: undefined,
109
- cancelled: true,
110
- ...sink.dump(`Command timed out after ${secs} seconds`),
58
+ exitCode: child.exitCode ?? 0,
59
+ cancelled: false,
60
+ ...stream.dump(),
111
61
  };
112
- }
62
+ } catch (err) {
63
+ // Exception covers NonZeroExitError, AbortError, TimeoutError
64
+ if (err instanceof Exception) {
65
+ if (err.aborted) {
66
+ const isTimeout = err.message.includes("timed out");
67
+ const annotation = isTimeout
68
+ ? `Command timed out after ${Math.round((options?.timeout ?? 0) / 1000)} seconds`
69
+ : undefined;
70
+ return {
71
+ exitCode: undefined,
72
+ cancelled: true,
73
+ ...stream.dump(annotation),
74
+ };
75
+ }
76
+
77
+ // NonZeroExitError
78
+ return {
79
+ exitCode: err.exitCode,
80
+ cancelled: false,
81
+ ...stream.dump(),
82
+ };
83
+ }
113
84
 
114
- return {
115
- exitCode: cancelled ? undefined : exitCode,
116
- cancelled,
117
- ...sink.dump(),
118
- };
85
+ throw err;
86
+ }
119
87
  }
120
88
 
121
- /**
122
- * Execute a bash command using custom BashOperations.
123
- * Used for remote execution (SSH, containers, etc.).
124
- */
125
89
  export async function executeBashWithOperations(
126
90
  command: string,
127
91
  cwd: string,
128
92
  operations: BashOperations,
129
93
  options?: BashExecutorOptions,
130
94
  ): Promise<BashResult> {
131
- const sink = new OutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
132
- const writer = sink.getWriter();
133
-
134
- // Create a ReadableStream from the callback-based operations.exec
135
- let streamController: ReadableStreamDefaultController<Uint8Array>;
136
- const dataStream = new ReadableStream<Uint8Array>({
137
- start(controller) {
138
- streamController = controller;
139
- },
140
- });
141
-
142
- const onData = (data: Buffer) => {
143
- streamController.enqueue(new Uint8Array(data));
95
+ const stream = new OutputSink({ onLine: options?.onChunk });
96
+ const writable = stream.createWritable();
97
+ const writer = writable.getWriter();
98
+
99
+ const closeStreams = async () => {
100
+ try {
101
+ await writer.close();
102
+ } catch {}
103
+ try {
104
+ await writable.close();
105
+ } catch {}
106
+ try {
107
+ await stream.close();
108
+ } catch {}
144
109
  };
145
110
 
146
- // Start pumping the stream (will complete when stream closes)
147
- const pumpPromise = pumpStream(dataStream, writer);
148
-
149
111
  try {
150
112
  const result = await operations.exec(command, cwd, {
151
- onData,
113
+ onData: (data) => writer.write(data),
152
114
  signal: options?.signal,
153
115
  timeout: options?.timeout,
154
116
  });
155
117
 
156
- streamController!.close();
157
- await pumpPromise;
158
- await writer.close();
118
+ await closeStreams();
159
119
 
160
120
  const cancelled = options?.signal?.aborted ?? false;
161
121
 
162
122
  return {
163
123
  exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
164
124
  cancelled,
165
- ...sink.dump(),
125
+ ...stream.dump(),
166
126
  };
167
127
  } catch (err) {
168
- streamController!.close();
169
- await pumpPromise;
170
- await writer.close();
128
+ await closeStreams();
171
129
 
172
130
  if (options?.signal?.aborted) {
173
131
  return {
174
132
  exitCode: undefined,
175
133
  cancelled: true,
176
- ...sink.dump(),
134
+ ...stream.dump(),
177
135
  };
178
136
  }
179
137
 
@@ -7,11 +7,11 @@
7
7
 
8
8
  import { type Dirent, existsSync, readdirSync } from "node:fs";
9
9
  import * as path from "node:path";
10
+ import { logger } from "@oh-my-pi/pi-utils";
10
11
  import * as typebox from "@sinclair/typebox";
11
12
  import { getAgentDir, getConfigDirs } from "../../config";
12
13
  import * as piCodingAgent from "../../index";
13
14
  import { execCommand } from "../exec";
14
- import { logger } from "../logger";
15
15
  import { ReviewCommand } from "./bundled/review";
16
16
  import { WorktreeCommand } from "./bundled/wt";
17
17
  import type {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as path from "node:path";
9
+ import { logger } from "@oh-my-pi/pi-utils";
9
10
  import * as typebox from "@sinclair/typebox";
10
11
  import { toolCapability } from "../../capability/tool";
11
12
  import { type CustomTool, loadCapability } from "../../discovery";
@@ -15,7 +16,6 @@ import { theme } from "../../modes/interactive/theme/theme";
15
16
  import type { ExecOptions } from "../exec";
16
17
  import { execCommand } from "../exec";
17
18
  import type { HookUIContext } from "../hooks/types";
18
- import { logger } from "../logger";
19
19
  import { getAllPluginToolPaths } from "../plugins/loader";
20
20
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
21
21
 
@@ -12,7 +12,6 @@ import type { Static, TSchema } from "@sinclair/typebox";
12
12
  import type { Theme } from "../../modes/interactive/theme/theme";
13
13
  import type { ExecOptions, ExecResult } from "../exec";
14
14
  import type { HookUIContext } from "../hooks/types";
15
- import type { Logger } from "../logger";
16
15
  import type { ModelRegistry } from "../model-registry";
17
16
  import type { ReadonlySessionManager } from "../session-manager";
18
17
 
@@ -36,7 +35,7 @@ export interface CustomToolAPI {
36
35
  /** Whether UI is available (false in print/RPC mode) */
37
36
  hasUI: boolean;
38
37
  /** File logger for error/warning/debug messages */
39
- logger: Logger;
38
+ logger: typeof import("@oh-my-pi/pi-utils").logger;
40
39
  /** Injected @sinclair/typebox module */
41
40
  typebox: typeof import("@sinclair/typebox");
42
41
  /** Injected pi-coding-agent exports */
package/src/core/exec.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  * Shared command execution utilities for hooks and custom tools.
3
3
  */
4
4
 
5
- import type { Subprocess } from "bun";
6
- import { logger } from "./logger";
5
+ import { ptree } from "@oh-my-pi/pi-utils";
7
6
 
8
7
  /**
9
8
  * Options for executing shell commands.
@@ -37,103 +36,20 @@ export async function execCommand(
37
36
  cwd: string,
38
37
  options?: ExecOptions,
39
38
  ): Promise<ExecResult> {
40
- return new Promise((resolve) => {
41
- const proc: Subprocess = Bun.spawn([command, ...args], {
42
- cwd,
43
- stdin: "ignore",
44
- stdout: "pipe",
45
- stderr: "pipe",
46
- });
47
-
48
- let stdout = "";
49
- let stderr = "";
50
- let killed = false;
51
- let timeoutId: Timer | undefined;
52
-
53
- const killProcess = () => {
54
- if (!killed) {
55
- killed = true;
56
- proc.kill();
57
- // Force kill after 5 seconds if first kill doesn't work
58
- setTimeout(() => {
59
- try {
60
- proc.kill(9);
61
- } catch {
62
- // Ignore if already dead
63
- }
64
- }, 5000);
65
- }
66
- };
67
-
68
- // Handle abort signal
69
- if (options?.signal) {
70
- if (options.signal.aborted) {
71
- killProcess();
72
- } else {
73
- options.signal.addEventListener("abort", killProcess, { once: true });
74
- }
75
- }
76
-
77
- // Handle timeout
78
- if (options?.timeout && options.timeout > 0) {
79
- timeoutId = setTimeout(() => {
80
- killProcess();
81
- }, options.timeout);
82
- }
83
-
84
- // Read streams asynchronously
85
- (async () => {
86
- try {
87
- const stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
88
- const stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader();
89
-
90
- // Read both streams and wait for process exit
91
- const [stdoutResult, stderrResult, exitCode] = await Promise.all([
92
- (async () => {
93
- const chunks: Uint8Array[] = [];
94
- try {
95
- while (true) {
96
- const { done, value } = await stdoutReader.read();
97
- if (done) break;
98
- chunks.push(value);
99
- }
100
- } finally {
101
- stdoutReader.releaseLock();
102
- }
103
- return Buffer.concat(chunks).toString();
104
- })(),
105
- (async () => {
106
- const chunks: Uint8Array[] = [];
107
- try {
108
- while (true) {
109
- const { done, value } = await stderrReader.read();
110
- if (done) break;
111
- chunks.push(value);
112
- }
113
- } finally {
114
- stderrReader.releaseLock();
115
- }
116
- return Buffer.concat(chunks).toString();
117
- })(),
118
- proc.exited,
119
- ]);
120
-
121
- stdout = stdoutResult;
122
- stderr = stderrResult;
123
-
124
- if (timeoutId) clearTimeout(timeoutId);
125
- if (options?.signal) {
126
- options.signal.removeEventListener("abort", killProcess);
127
- }
128
- resolve({ stdout, stderr, code: exitCode ?? 0, killed });
129
- } catch (err) {
130
- logger.debug("Process stream error", { error: String(err) });
131
- if (timeoutId) clearTimeout(timeoutId);
132
- if (options?.signal) {
133
- options.signal.removeEventListener("abort", killProcess);
134
- }
135
- resolve({ stdout, stderr, code: 1, killed });
136
- }
137
- })();
39
+ const proc = ptree.cspawn([command, ...args], {
40
+ cwd,
41
+ signal: options?.signal,
42
+ timeout: options?.timeout,
138
43
  });
44
+ try {
45
+ await proc.exited;
46
+ } catch {
47
+ // ChildProcess rejects on non-zero exit; we handle it below
48
+ }
49
+ return {
50
+ stdout: await proc.stdout.text(),
51
+ stderr: await proc.stderr.text(),
52
+ code: proc.exitCode ?? 0,
53
+ killed: proc.exitReason instanceof ptree.AbortError,
54
+ };
139
55
  }
@@ -110,10 +110,4 @@ export {
110
110
  isReadToolResult,
111
111
  isWriteToolResult,
112
112
  } from "./types";
113
- export {
114
- ExtensionToolWrapper,
115
- RegisteredToolAdapter,
116
- wrapRegisteredTool,
117
- wrapRegisteredTools,
118
- wrapToolWithExtensions,
119
- } from "./wrapper";
113
+ export { ExtensionToolWrapper, RegisteredToolAdapter, wrapRegisteredTool, wrapRegisteredTools } from "./wrapper";
@@ -7,6 +7,7 @@ import * as path from "node:path";
7
7
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
9
9
  import type { KeyId } from "@oh-my-pi/pi-tui";
10
+ import { logger } from "@oh-my-pi/pi-utils";
10
11
  import type { TSchema } from "@sinclair/typebox";
11
12
  import * as TypeBox from "@sinclair/typebox";
12
13
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
@@ -16,7 +17,6 @@ import * as piCodingAgent from "../../index";
16
17
  import { EventBus } from "../event-bus";
17
18
  import type { ExecOptions } from "../exec";
18
19
  import { execCommand } from "../exec";
19
- import { logger } from "../logger";
20
20
  import type { CustomMessage } from "../messages";
21
21
  import type {
22
22
  Extension,
@@ -5,8 +5,8 @@
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
7
7
  import type { KeyId } from "@oh-my-pi/pi-tui";
8
+ import { logger } from "@oh-my-pi/pi-utils";
8
9
  import { type Theme, theme } from "../../modes/interactive/theme/theme";
9
- import { logger } from "../logger";
10
10
  import type { ModelRegistry } from "../model-registry";
11
11
  import type { SessionManager } from "../session-manager";
12
12
  import type {
@@ -33,8 +33,8 @@ import type { BashOperations } from "../tools/bash";
33
33
  import type { EditToolDetails } from "../tools/patch";
34
34
 
35
35
  export type { ExecOptions, ExecResult } from "../exec";
36
- export type { AgentToolResult, AgentToolUpdateCallback };
37
36
  export type { AppAction, KeybindingsManager } from "../keybindings";
37
+ export type { AgentToolResult, AgentToolUpdateCallback };
38
38
 
39
39
  // ============================================================================
40
40
  // UI Context
@@ -633,7 +633,7 @@ export interface ExtensionAPI {
633
633
  // =========================================================================
634
634
 
635
635
  /** File logger for error/warning/debug messages */
636
- logger: typeof import("../logger").logger;
636
+ logger: typeof import("@oh-my-pi/pi-utils").logger;
637
637
 
638
638
  /** Injected @sinclair/typebox module for defining tool parameters */
639
639
  typebox: typeof import("@sinclair/typebox");
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
6
6
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
+ import type { Static, TSchema } from "@sinclair/typebox";
7
8
  import type { Theme } from "../../modes/interactive/theme/theme";
8
9
  import type { ExtensionRunner } from "./runner";
9
10
  import type { RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
@@ -70,16 +71,18 @@ export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: E
70
71
  * - Emits tool_call event before execution (can block)
71
72
  * - Emits tool_result event after execution (can modify result)
72
73
  */
73
- export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
74
+ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetails = unknown>
75
+ implements AgentTool<TParameters, TDetails>
76
+ {
74
77
  name: string;
75
78
  label: string;
76
79
  description: string;
77
- parameters: unknown;
78
- renderCall?: AgentTool["renderCall"];
79
- renderResult?: AgentTool["renderResult"];
80
+ parameters: TParameters;
81
+ renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
82
+ renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
80
83
 
81
84
  constructor(
82
- private tool: AgentTool<any, T>,
85
+ private tool: AgentTool<TParameters, TDetails>,
83
86
  private runner: ExtensionRunner,
84
87
  ) {
85
88
  this.name = tool.name;
@@ -92,9 +95,9 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
92
95
 
93
96
  async execute(
94
97
  toolCallId: string,
95
- params: Record<string, unknown>,
98
+ params: Static<TParameters>,
96
99
  signal?: AbortSignal,
97
- onUpdate?: AgentToolUpdateCallback<T>,
100
+ onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
98
101
  context?: AgentToolContext,
99
102
  ) {
100
103
  // Emit tool_call event - extensions can block execution
@@ -104,7 +107,7 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
104
107
  type: "tool_call",
105
108
  toolName: this.tool.name,
106
109
  toolCallId,
107
- input: params,
110
+ input: params as Record<string, unknown>,
108
111
  })) as ToolCallEventResult | undefined;
109
112
 
110
113
  if (callResult?.block) {
@@ -120,7 +123,7 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
120
123
  }
121
124
 
122
125
  // Execute the actual tool
123
- let result: { content: any; details?: T };
126
+ let result: { content: any; details?: TDetails };
124
127
  let executionError: Error | undefined;
125
128
 
126
129
  try {
@@ -129,7 +132,7 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
129
132
  executionError = err instanceof Error ? err : new Error(String(err));
130
133
  result = {
131
134
  content: [{ type: "text", text: executionError.message }],
132
- details: undefined as T,
135
+ details: undefined as TDetails,
133
136
  };
134
137
  }
135
138
 
@@ -139,7 +142,7 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
139
142
  type: "tool_result",
140
143
  toolName: this.tool.name,
141
144
  toolCallId,
142
- input: params,
145
+ input: params as Record<string, unknown>,
143
146
  content: result.content,
144
147
  details: result.details,
145
148
  isError: !!executionError,
@@ -147,7 +150,7 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
147
150
 
148
151
  if (resultResult) {
149
152
  const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
150
- const modifiedDetails = (resultResult.details ?? result.details) as T;
153
+ const modifiedDetails = (resultResult.details ?? result.details) as TDetails;
151
154
 
152
155
  // Extension can override error status
153
156
  if (resultResult.isError === true && !executionError) {
@@ -176,11 +179,3 @@ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
176
179
  return result;
177
180
  }
178
181
  }
179
-
180
- /**
181
- * Wrap a tool with extension callbacks for interception.
182
- * @deprecated Use `new ExtensionToolWrapper()` directly
183
- */
184
- export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: ExtensionRunner): AgentTool<any, T> {
185
- return new ExtensionToolWrapper(tool, runner);
186
- }
@@ -1,5 +1,5 @@
1
+ import { logger } from "@oh-my-pi/pi-utils";
1
2
  import { YAML } from "bun";
2
- import { logger } from "./logger";
3
3
 
4
4
  function stripHtmlComments(content: string): string {
5
5
  return content.replace(/<!--[\s\S]*?-->/g, "");
@@ -1,7 +1,8 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
4
+ import { logger } from "@oh-my-pi/pi-utils";
3
5
  import { getAgentDir } from "../config";
4
- import { logger } from "./logger";
5
6
 
6
7
  export interface HistoryEntry {
7
8
  id: number;
@@ -134,11 +135,7 @@ END;
134
135
 
135
136
  private ensureDir(dbPath: string): void {
136
137
  const dir = dirname(dbPath);
137
- const result = Bun.spawnSync(["mkdir", "-p", dir]);
138
- if (result.exitCode !== 0) {
139
- const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
140
- throw new Error(`Failed to create history directory: ${dir} ${stderr}`.trim());
141
- }
138
+ mkdirSync(dir, { recursive: true });
142
139
  }
143
140
 
144
141
  private normalizeLimit(limit: number): number {
@@ -1,4 +1,5 @@
1
1
  // biome-ignore assist/source/organizeImports: biome is not smart
2
+ export type { ReadonlySessionManager, UsageStatistics } from "../session-manager";
2
3
  export {
3
4
  discoverAndLoadHooks,
4
5
  loadHooks,
@@ -11,6 +12,5 @@ export {
11
12
  type SendMessageHandler,
12
13
  } from "./loader";
13
14
  export { execCommand, HookRunner, type HookErrorListener } from "./runner";
14
- export { HookToolWrapper, wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper";
15
+ export { HookToolWrapper } from "./tool-wrapper";
15
16
  export * from "./types";
16
- export type { UsageStatistics, ReadonlySessionManager } from "../session-manager";
@@ -3,13 +3,13 @@
3
3
  */
4
4
 
5
5
  import * as path from "node:path";
6
+ import { logger } from "@oh-my-pi/pi-utils";
6
7
  import * as typebox from "@sinclair/typebox";
7
8
  import { hookCapability } from "../../capability/hook";
8
9
  import type { Hook } from "../../discovery";
9
10
  import { loadCapability } from "../../discovery";
10
11
  import { expandPath } from "../../discovery/helpers";
11
12
  import * as piCodingAgent from "../../index";
12
- import { logger } from "../logger";
13
13
  import type { HookMessage } from "../messages";
14
14
  import type { SessionManager } from "../session-manager";
15
15
  import { execCommand } from "./runner";