@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
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
6
+ import type { Static, TSchema } from "@sinclair/typebox";
6
7
  import type { HookRunner } from "./runner";
7
8
  import type { ToolCallEventResult, ToolResultEventResult } from "./types";
8
9
 
@@ -14,16 +15,18 @@ import type { ToolCallEventResult, ToolResultEventResult } from "./types";
14
15
  * - Emits tool_result event after execution (can modify result)
15
16
  * - Forwards onUpdate callback to wrapped tool for progress streaming
16
17
  */
17
- export class HookToolWrapper<T> implements AgentTool<any, T> {
18
+ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = unknown>
19
+ implements AgentTool<TParameters, TDetails>
20
+ {
18
21
  name: string;
19
22
  label: string;
20
23
  description: string;
21
- parameters: unknown;
22
- renderCall?: AgentTool["renderCall"];
23
- renderResult?: AgentTool["renderResult"];
24
+ parameters: TParameters;
25
+ renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
26
+ renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
24
27
 
25
28
  constructor(
26
- private tool: AgentTool<any, T>,
29
+ private tool: AgentTool<TParameters, TDetails>,
27
30
  private hookRunner: HookRunner,
28
31
  ) {
29
32
  this.name = tool.name;
@@ -36,9 +39,9 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
36
39
 
37
40
  async execute(
38
41
  toolCallId: string,
39
- params: Record<string, unknown>,
42
+ params: Static<TParameters>,
40
43
  signal?: AbortSignal,
41
- onUpdate?: AgentToolUpdateCallback<T>,
44
+ onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
42
45
  context?: AgentToolContext,
43
46
  ) {
44
47
  // Emit tool_call event - hooks can block execution
@@ -49,7 +52,7 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
49
52
  type: "tool_call",
50
53
  toolName: this.tool.name,
51
54
  toolCallId,
52
- input: params,
55
+ input: params as Record<string, unknown>,
53
56
  })) as ToolCallEventResult | undefined;
54
57
 
55
58
  if (callResult?.block) {
@@ -75,7 +78,7 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
75
78
  type: "tool_result",
76
79
  toolName: this.tool.name,
77
80
  toolCallId,
78
- input: params,
81
+ input: params as Record<string, unknown>,
79
82
  content: result.content,
80
83
  details: result.details,
81
84
  isError: false,
@@ -85,7 +88,7 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
85
88
  if (resultResult) {
86
89
  return {
87
90
  content: resultResult.content ?? result.content,
88
- details: (resultResult.details ?? result.details) as T,
91
+ details: (resultResult.details ?? result.details) as TDetails,
89
92
  };
90
93
  }
91
94
  }
@@ -98,7 +101,7 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
98
101
  type: "tool_result",
99
102
  toolName: this.tool.name,
100
103
  toolCallId,
101
- input: params,
104
+ input: params as Record<string, unknown>,
102
105
  content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
103
106
  details: undefined,
104
107
  isError: true,
@@ -108,18 +111,3 @@ export class HookToolWrapper<T> implements AgentTool<any, T> {
108
111
  }
109
112
  }
110
113
  }
111
-
112
- /**
113
- * Wrap all tools with hook callbacks.
114
- */
115
- export function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {
116
- return tools.map((tool) => new HookToolWrapper(tool, hookRunner));
117
- }
118
-
119
- /**
120
- * Backward compatibility alias - use HookToolWrapper directly.
121
- * @deprecated Use HookToolWrapper class instead
122
- */
123
- export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {
124
- return new HookToolWrapper(tool, hookRunner);
125
- }
@@ -11,7 +11,6 @@ import type { Component, TUI } from "@oh-my-pi/pi-tui";
11
11
  import type { Theme } from "../../modes/interactive/theme/theme";
12
12
  import type { CompactionPreparation, CompactionResult } from "../compaction/index";
13
13
  import type { ExecOptions, ExecResult } from "../exec";
14
- import type { Logger } from "../logger";
15
14
  import type { HookMessage } from "../messages";
16
15
  import type { ModelRegistry } from "../model-registry";
17
16
  import type {
@@ -742,7 +741,7 @@ export interface HookAPI {
742
741
  exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
743
742
 
744
743
  /** File logger for error/warning/debug messages */
745
- logger: Logger;
744
+ logger: typeof import("@oh-my-pi/pi-utils").logger;
746
745
  /** Injected @sinclair/typebox module */
747
746
  typebox: typeof import("@sinclair/typebox");
748
747
  /** Injected pi-coding-agent exports */
@@ -9,8 +9,8 @@ import {
9
9
  matchesKey,
10
10
  setEditorKeybindings,
11
11
  } from "@oh-my-pi/pi-tui";
12
+ import { logger } from "@oh-my-pi/pi-utils";
12
13
  import { getAgentDir } from "../config";
13
- import { logger } from "./logger";
14
14
 
15
15
  /**
16
16
  * Application-level actions (coding agent specific).
@@ -36,19 +36,19 @@ const CLIENT_INFO = {
36
36
 
37
37
  /** Wrap a promise with a timeout */
38
38
  function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
39
- return new Promise((resolve, reject) => {
40
- const timer = setTimeout(() => reject(new Error(message)), ms);
41
- promise.then(
42
- (value) => {
43
- clearTimeout(timer);
44
- resolve(value);
45
- },
46
- (error) => {
47
- clearTimeout(timer);
48
- reject(error);
49
- },
50
- );
51
- });
39
+ const { promise: wrapped, resolve, reject } = Promise.withResolvers<T>();
40
+ const timer = setTimeout(() => reject(new Error(message)), ms);
41
+ promise.then(
42
+ (value) => {
43
+ clearTimeout(timer);
44
+ resolve(value);
45
+ },
46
+ (error) => {
47
+ clearTimeout(timer);
48
+ reject(error);
49
+ },
50
+ );
51
+ return wrapped;
52
52
  }
53
53
 
54
54
  /**
@@ -5,7 +5,7 @@
5
5
  * without maintaining persistent connections.
6
6
  */
7
7
 
8
- import { logger } from "../logger";
8
+ import { logger } from "@oh-my-pi/pi-utils";
9
9
 
10
10
  /** Parse SSE response format (lines starting with "data: ") */
11
11
  export function parseSSE(text: string): unknown {
@@ -4,9 +4,9 @@
4
4
  * Integrates MCP tool discovery with the custom tools system.
5
5
  */
6
6
 
7
+ import { logger } from "@oh-my-pi/pi-utils";
7
8
  import { AgentStorage } from "../agent-storage";
8
9
  import type { LoadedCustomTool } from "../custom-tools/types";
9
- import { logger } from "../logger";
10
10
  import { type MCPLoadResult, MCPManager } from "./manager";
11
11
  import { parseMCPToolName } from "./tool-bridge";
12
12
  import { MCPToolCache } from "./tool-cache";
@@ -5,9 +5,9 @@
5
5
  * Handles tool loading and lifecycle.
6
6
  */
7
7
 
8
+ import { logger } from "@oh-my-pi/pi-utils";
8
9
  import type { TSchema } from "@sinclair/typebox";
9
10
  import type { CustomTool } from "../custom-tools/types";
10
- import { logger } from "../logger";
11
11
  import { connectToServer, disconnectServer, listTools } from "./client";
12
12
  import { loadAllMCPConfigs, validateServerConfig } from "./config";
13
13
  import type { MCPToolDetails } from "./tool-bridge";
@@ -47,7 +47,7 @@ function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
47
47
  }
48
48
 
49
49
  function delay(ms: number): Promise<void> {
50
- return new Promise((resolve) => setTimeout(resolve, ms));
50
+ return Bun.sleep(ms);
51
51
  }
52
52
 
53
53
  /** Result of loading MCP tools */
@@ -4,8 +4,8 @@
4
4
  * Stores tool definitions per server in agent.db for fast startup.
5
5
  */
6
6
 
7
+ import { logger } from "@oh-my-pi/pi-utils";
7
8
  import type { AgentStorage } from "../agent-storage";
8
- import { logger } from "../logger";
9
9
  import type { MCPServerConfig, MCPToolDefinition } from "./types";
10
10
 
11
11
  const CACHE_VERSION = 1;
@@ -12,19 +12,7 @@ function generateId(): string {
12
12
  return Math.random().toString(36).slice(2) + Date.now().toString(36);
13
13
  }
14
14
 
15
- /** Parse SSE data line */
16
- function parseSSELine(line: string): { event?: string; data?: string; id?: string } | null {
17
- if (line.startsWith("data:")) {
18
- return { data: line.slice(5).trim() };
19
- }
20
- if (line.startsWith("event:")) {
21
- return { event: line.slice(6).trim() };
22
- }
23
- if (line.startsWith("id:")) {
24
- return { id: line.slice(3).trim() };
25
- }
26
- return null;
27
- }
15
+ import { readSseEvents } from "@oh-my-pi/pi-utils";
28
16
 
29
17
  /**
30
18
  * HTTP transport for MCP servers.
@@ -95,30 +83,17 @@ export class HttpTransport implements MCPTransport {
95
83
  }
96
84
 
97
85
  // Read SSE stream
98
- const reader = response.body.getReader();
99
- const decoder = new TextDecoder();
100
- let buffer = "";
101
-
102
- while (this._connected) {
103
- const { done, value } = await reader.read();
104
- if (done) break;
105
-
106
- buffer += decoder.decode(value, { stream: true });
107
- const lines = buffer.split("\n");
108
- buffer = lines.pop() ?? "";
109
-
110
- for (const line of lines) {
111
- const parsed = parseSSELine(line);
112
- if (parsed?.data && parsed.data !== "[DONE]") {
113
- try {
114
- const message = JSON.parse(parsed.data);
115
- if ("method" in message && !("id" in message)) {
116
- this.onNotification?.(message.method, message.params);
117
- }
118
- } catch {
119
- // Ignore parse errors
120
- }
86
+ for await (const event of readSseEvents(response.body)) {
87
+ if (!this._connected) break;
88
+ const data = event.data?.trim();
89
+ if (!data || data === "[DONE]") continue;
90
+ try {
91
+ const message = JSON.parse(data);
92
+ if ("method" in message && !("id" in message)) {
93
+ this.onNotification?.(message.method, message.params);
121
94
  }
95
+ } catch {
96
+ // Ignore parse errors
122
97
  }
123
98
  }
124
99
  } catch (error) {
@@ -192,44 +167,31 @@ export class HttpTransport implements MCPTransport {
192
167
  throw new Error("No response body");
193
168
  }
194
169
 
195
- const reader = response.body.getReader();
196
- const decoder = new TextDecoder();
197
- let buffer = "";
198
170
  let result: T | undefined;
199
171
 
200
- while (true) {
201
- const { done, value } = await reader.read();
202
- if (done) break;
203
-
204
- buffer += decoder.decode(value, { stream: true });
205
- const lines = buffer.split("\n");
206
- buffer = lines.pop() ?? "";
207
-
208
- for (const line of lines) {
209
- const parsed = parseSSELine(line);
210
- if (parsed?.data && parsed.data !== "[DONE]") {
211
- try {
212
- const message = JSON.parse(parsed.data) as JsonRpcResponse;
213
-
214
- // Handle our response
215
- if ("id" in message && message.id === expectedId) {
216
- if (message.error) {
217
- throw new Error(`MCP error ${message.error.code}: ${message.error.message}`);
218
- }
219
- result = message.result as T;
220
- }
221
- // Handle notifications
222
- else if ("method" in message && !("id" in message)) {
223
- const notification = message as { method: string; params?: unknown };
224
- this.onNotification?.(notification.method, notification.params);
225
- }
226
- } catch (error) {
227
- if (error instanceof Error && error.message.startsWith("MCP error")) {
228
- throw error;
229
- }
230
- // Ignore other parse errors
172
+ for await (const event of readSseEvents(response.body)) {
173
+ const data = event.data?.trim();
174
+ if (!data || data === "[DONE]") continue;
175
+ try {
176
+ const message = JSON.parse(data) as JsonRpcResponse;
177
+
178
+ // Handle our response
179
+ if ("id" in message && message.id === expectedId) {
180
+ if (message.error) {
181
+ throw new Error(`MCP error ${message.error.code}: ${message.error.message}`);
231
182
  }
183
+ result = message.result as T;
184
+ }
185
+ // Handle notifications
186
+ else if ("method" in message && !("id" in message)) {
187
+ const notification = message as { method: string; params?: unknown };
188
+ this.onNotification?.(notification.method, notification.params);
189
+ }
190
+ } catch (error) {
191
+ if (error instanceof Error && error.message.startsWith("MCP error")) {
192
+ throw error;
232
193
  }
194
+ // Ignore other parse errors
233
195
  }
234
196
  }
235
197
 
@@ -11,10 +11,10 @@ import {
11
11
  type Model,
12
12
  normalizeDomain,
13
13
  } from "@oh-my-pi/pi-ai";
14
+ import { logger } from "@oh-my-pi/pi-utils";
14
15
  import { type Static, Type } from "@sinclair/typebox";
15
16
  import AjvModule from "ajv";
16
17
  import type { AuthStorage } from "./auth-storage";
17
- import { logger } from "./logger";
18
18
 
19
19
  const Ajv = (AjvModule as any).default || AjvModule;
20
20
 
@@ -38,8 +38,9 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
38
38
 
39
39
  // Initialize package.json if it doesn't exist
40
40
  const pkgJsonPath = join(PLUGINS_DIR, "package.json");
41
- if (!(await Bun.file(pkgJsonPath).exists())) {
42
- await Bun.write(pkgJsonPath, JSON.stringify({ name: "omp-plugins", private: true, dependencies: {} }, null, 2));
41
+ const pkgJson = Bun.file(pkgJsonPath);
42
+ if (!(await pkgJson.exists())) {
43
+ await pkgJson.write(JSON.stringify({ name: "omp-plugins", private: true, dependencies: {} }, null, 2));
43
44
  }
44
45
 
45
46
  // Run npm install in plugins directory
@@ -98,24 +99,25 @@ export async function uninstallPlugin(name: string): Promise<void> {
98
99
  }
99
100
 
100
101
  export async function listPlugins(): Promise<InstalledPlugin[]> {
101
- const pkgJsonPath = join(PLUGINS_DIR, "package.json");
102
- if (!(await Bun.file(pkgJsonPath).exists())) {
102
+ const pkgJsonPath = Bun.file(join(PLUGINS_DIR, "package.json"));
103
+ if (!(await pkgJsonPath.exists())) {
103
104
  return [];
104
105
  }
105
106
 
106
- const pkg = await Bun.file(pkgJsonPath).json();
107
+ const pkg = await pkgJsonPath.json();
107
108
  const deps = pkg.dependencies || {};
108
109
 
109
110
  const plugins: InstalledPlugin[] = [];
110
111
  for (const [name, _version] of Object.entries(deps)) {
111
- const pluginPkgPath = join(PLUGINS_DIR, "node_modules", name, "package.json");
112
- if (await Bun.file(pluginPkgPath).exists()) {
113
- const pluginPkg = await Bun.file(pluginPkgPath).json();
112
+ const path = join(PLUGINS_DIR, "node_modules", name);
113
+ const fpkg = Bun.file(join(path, "package.json"));
114
+ if (await fpkg.exists()) {
115
+ const pkg = await fpkg.json();
114
116
  plugins.push({
115
117
  name,
116
- version: pluginPkg.version,
117
- path: join(PLUGINS_DIR, "node_modules", name),
118
- manifest: pluginPkg.omp || pluginPkg.pi || { version: pluginPkg.version },
118
+ version: pkg.version,
119
+ path,
120
+ manifest: pkg.omp || pkg.pi || { version: pkg.version },
119
121
  enabledFeatures: null,
120
122
  enabled: true,
121
123
  });
@@ -1,8 +1,8 @@
1
1
  import { join, resolve } from "node:path";
2
+ import { logger } from "@oh-my-pi/pi-utils";
2
3
  import Handlebars from "handlebars";
3
4
  import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
4
5
  import { parseFrontmatter } from "./frontmatter";
5
- import { logger } from "./logger";
6
6
 
7
7
  /**
8
8
  * Represents a prompt template loaded from a markdown file
@@ -371,14 +371,6 @@ async function loadTemplatesFromDir(
371
371
  subdir: string = "",
372
372
  ): Promise<PromptTemplate[]> {
373
373
  const templates: PromptTemplate[] = [];
374
-
375
- try {
376
- const stat = await Bun.file(`${dir}/.`).exists();
377
- if (!stat) return templates;
378
- } catch {
379
- return templates;
380
- }
381
-
382
374
  try {
383
375
  const glob = new Bun.Glob("**/*");
384
376
  const entries = [];
@@ -440,6 +432,9 @@ async function loadTemplatesFromDir(
440
432
  }
441
433
  }
442
434
  } catch (error) {
435
+ if (!Bun.file(dir).exists()) {
436
+ return [];
437
+ }
443
438
  logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
444
439
  }
445
440
 
@@ -1,4 +1,4 @@
1
- import { logger } from "./logger";
1
+ import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
2
2
  import {
3
3
  checkPythonKernelAvailability,
4
4
  type KernelDisplayOutput,
@@ -7,9 +7,7 @@ import {
7
7
  type PreludeHelper,
8
8
  PythonKernel,
9
9
  } from "./python-kernel";
10
- import { OutputSink, sanitizeText } from "./streaming-output";
11
- import { DEFAULT_MAX_BYTES } from "./tools/truncate";
12
-
10
+ import { OutputSink } from "./streaming-output";
13
11
  export type PythonKernelMode = "session" | "per-call";
14
12
 
15
13
  export interface PythonExecutorOptions {
@@ -212,21 +210,30 @@ async function executeWithKernel(
212
210
  code: string,
213
211
  options: PythonExecutorOptions | undefined,
214
212
  ): Promise<PythonResult> {
215
- const sink = new OutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
216
- const writer = sink.getWriter();
213
+ const sink = new OutputSink({ onLine: options?.onChunk });
217
214
  const displayOutputs: KernelDisplayOutput[] = [];
218
215
 
219
216
  try {
220
- const result = await kernel.execute(code, {
221
- signal: options?.signal,
222
- timeoutMs: options?.timeout,
223
- onChunk: async (text) => {
224
- await writer.write(sanitizeText(text));
225
- },
226
- onDisplay: async (output) => {
227
- displayOutputs.push(output);
228
- },
229
- });
217
+ const writable = sink.createStringWritable();
218
+ const writer = writable.getWriter();
219
+ let result: KernelExecuteResult;
220
+ try {
221
+ result = await kernel.execute(code, {
222
+ signal: options?.signal,
223
+ timeoutMs: options?.timeout,
224
+ onChunk: (text) => {
225
+ writer.write(sanitizeText(text));
226
+ },
227
+ onDisplay: (output) => {
228
+ displayOutputs.push(output);
229
+ },
230
+ });
231
+ } catch (err) {
232
+ await writer.abort(err);
233
+ throw err;
234
+ } finally {
235
+ await writer.close().catch(() => {});
236
+ }
230
237
 
231
238
  if (result.cancelled) {
232
239
  const secs = options?.timeout ? Math.round(options.timeout / 1000) : undefined;
@@ -263,8 +270,6 @@ async function executeWithKernel(
263
270
  const error = err instanceof Error ? err : new Error(String(err));
264
271
  logger.error("Python execution failed", { error: error.message });
265
272
  throw error;
266
- } finally {
267
- await writer.close();
268
273
  }
269
274
  }
270
275
 
@@ -13,11 +13,11 @@ import {
13
13
  } from "node:fs";
14
14
  import { createServer } from "node:net";
15
15
  import { delimiter, join } from "node:path";
16
+ import { logger } from "@oh-my-pi/pi-utils";
16
17
  import type { Subprocess } from "bun";
17
18
  import { getAgentDir } from "../config";
18
19
  import { getShellConfig, killProcessTree } from "../utils/shell";
19
20
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
20
- import { logger } from "./logger";
21
21
 
22
22
  const GATEWAY_DIR_NAME = "python-gateway";
23
23
  const GATEWAY_INFO_FILE = "gateway.json";
@@ -213,27 +213,28 @@ async function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
213
213
  }
214
214
 
215
215
  async function allocatePort(): Promise<number> {
216
- return await new Promise((resolve, reject) => {
217
- const server = createServer();
218
- server.unref();
219
- server.on("error", reject);
220
- server.listen(0, "127.0.0.1", () => {
221
- const address = server.address();
222
- if (address && typeof address === "object") {
223
- const port = address.port;
224
- server.close((err: Error | null | undefined) => {
225
- if (err) {
226
- reject(err);
227
- } else {
228
- resolve(port);
229
- }
230
- });
231
- } else {
232
- server.close();
233
- reject(new Error("Failed to allocate port"));
234
- }
235
- });
216
+ const { promise, resolve, reject } = Promise.withResolvers<number>();
217
+ const server = createServer();
218
+ server.unref();
219
+ server.on("error", reject);
220
+ server.listen(0, "127.0.0.1", () => {
221
+ const address = server.address();
222
+ if (address && typeof address === "object") {
223
+ const port = address.port;
224
+ server.close((err: Error | null | undefined) => {
225
+ if (err) {
226
+ reject(err);
227
+ } else {
228
+ resolve(port);
229
+ }
230
+ });
231
+ } else {
232
+ server.close();
233
+ reject(new Error("Failed to allocate port"));
234
+ }
236
235
  });
236
+
237
+ return promise;
237
238
  }
238
239
 
239
240
  function getGatewayDir(): string {
@@ -587,7 +588,7 @@ async function startGatewayProcess(
587
588
  await Bun.sleep(100);
588
589
  }
589
590
 
590
- killProcessTree(gatewayProcess.pid);
591
+ await killProcessTree(gatewayProcess.pid);
591
592
  throw new Error("Gateway startup timeout");
592
593
  }
593
594
 
@@ -613,10 +614,10 @@ function scheduleIdleShutdown(): void {
613
614
  }
614
615
  logger.debug("Shutting down idle shared gateway", { pid: info.pid });
615
616
  if (localGatewayProcess) {
616
- shutdownLocalGateway();
617
+ await shutdownLocalGateway();
617
618
  } else if (isPidRunning(info.pid)) {
618
619
  try {
619
- killProcessTree(info.pid);
620
+ await killProcessTree(info.pid);
620
621
  } catch (err) {
621
622
  logger.warn("Failed to kill idle shared gateway", {
622
623
  error: err instanceof Error ? err.message : String(err),
@@ -644,10 +645,10 @@ function cancelIdleShutdown(): void {
644
645
  }
645
646
  }
646
647
 
647
- function shutdownLocalGateway(): void {
648
+ async function shutdownLocalGateway(): Promise<void> {
648
649
  if (localGatewayProcess) {
649
650
  try {
650
- killProcessTree(localGatewayProcess.pid);
651
+ await killProcessTree(localGatewayProcess.pid);
651
652
  } catch (err) {
652
653
  logger.warn("Failed to kill shared gateway process", {
653
654
  error: err instanceof Error ? err.message : String(err),
@@ -701,7 +702,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
701
702
  logger.debug("Cleaning up stale gateway info", { pid: existingInfo.pid });
702
703
  if (isPidRunning(existingInfo.pid)) {
703
704
  try {
704
- killProcessTree(existingInfo.pid);
705
+ await killProcessTree(existingInfo.pid);
705
706
  } catch (err) {
706
707
  logger.warn("Failed to kill stale shared gateway process", {
707
708
  error: err instanceof Error ? err.message : String(err),
@@ -826,7 +827,7 @@ export async function shutdownSharedGateway(): Promise<void> {
826
827
  error: err instanceof Error ? err.message : String(err),
827
828
  });
828
829
  } finally {
829
- shutdownLocalGateway();
830
+ await shutdownLocalGateway();
830
831
  isCoordinatorInitialized = false;
831
832
  }
832
833
  }