@oh-my-pi/pi-coding-agent 11.8.3 → 11.9.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.
@@ -47,6 +47,7 @@ import { CommandController } from "./controllers/command-controller";
47
47
  import { EventController } from "./controllers/event-controller";
48
48
  import { ExtensionUiController } from "./controllers/extension-ui-controller";
49
49
  import { InputController } from "./controllers/input-controller";
50
+ import { MCPCommandController } from "./controllers/mcp-command-controller";
50
51
  import { SelectorController } from "./controllers/selector-controller";
51
52
  import { setMermaidRenderCallback } from "./theme/mermaid-cache";
52
53
  import type { Theme } from "./theme/theme";
@@ -922,6 +923,11 @@ export class InteractiveMode implements InteractiveModeContext {
922
923
  return this.#commandController.handlePythonCommand(code, excludeFromContext);
923
924
  }
924
925
 
926
+ async handleMCPCommand(text: string): Promise<void> {
927
+ const controller = new MCPCommandController(this);
928
+ await controller.handle(text);
929
+ }
930
+
925
931
  handleCompactCommand(customInstructions?: string): Promise<void> {
926
932
  return this.#commandController.handleCompactCommand(customInstructions);
927
933
  }
@@ -147,6 +147,7 @@ export interface InteractiveModeContext {
147
147
  handleForkCommand(): Promise<void>;
148
148
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
149
149
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
150
+ handleMCPCommand(text: string): Promise<void>;
150
151
  handleCompactCommand(customInstructions?: string): Promise<void>;
151
152
  handleHandoffCommand(customInstructions?: string): Promise<void>;
152
153
  executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
package/src/sdk.ts CHANGED
@@ -710,6 +710,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
710
710
  // Always filter Exa - we have native integration
711
711
  filterExa: true,
712
712
  cacheStorage: settingsInstance.getStorage(),
713
+ authStorage,
713
714
  });
714
715
  time("discoverAndLoadMCPTools");
715
716
  debugStartup("sdk:discoverAndLoadMCPTools");
@@ -44,6 +44,8 @@ import { type BashResult, executeBash as executeBashCommand } from "../exec/bash
44
44
  import { exportSessionToHtml } from "../export/html";
45
45
  import type { TtsrManager } from "../export/ttsr";
46
46
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
47
+ import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
48
+ import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
47
49
  import type {
48
50
  ExtensionCommandContext,
49
51
  ExtensionRunner,
@@ -57,6 +59,7 @@ import type {
57
59
  TurnStartEvent,
58
60
  } from "../extensibility/extensions";
59
61
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
62
+ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
60
63
  import type { HookCommandContext } from "../extensibility/hooks/types";
61
64
  import type { Skill, SkillWarning } from "../extensibility/skills";
62
65
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
@@ -965,6 +968,52 @@ export class AgentSession {
965
968
  }
966
969
  }
967
970
 
971
+ /**
972
+ * Replace MCP tools in the registry and activate the latest MCP tool set immediately.
973
+ * This allows /mcp add/remove/reauth to take effect without restarting the session.
974
+ */
975
+ async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
976
+ const prefix = "mcp_";
977
+ const existingNames = Array.from(this.#toolRegistry.keys());
978
+ for (const name of existingNames) {
979
+ if (name.startsWith(prefix)) {
980
+ this.#toolRegistry.delete(name);
981
+ }
982
+ }
983
+
984
+ const getCustomToolContext = (): CustomToolContext => ({
985
+ sessionManager: this.sessionManager,
986
+ modelRegistry: this.#modelRegistry,
987
+ model: this.model,
988
+ isIdle: () => !this.isStreaming,
989
+ hasQueuedMessages: () => this.queuedMessageCount > 0,
990
+ abort: () => {
991
+ this.agent.abort();
992
+ },
993
+ });
994
+
995
+ for (const customTool of mcpTools) {
996
+ const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
997
+ const finalTool = (
998
+ this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
999
+ ) as AgentTool;
1000
+ this.#toolRegistry.set(finalTool.name, finalTool);
1001
+ }
1002
+
1003
+ const currentActive = this.getActiveToolNames().filter(
1004
+ name => !name.startsWith(prefix) && this.#toolRegistry.has(name),
1005
+ );
1006
+ const mcpToolNames = Array.from(this.#toolRegistry.keys()).filter(name => name.startsWith(prefix));
1007
+ const nextActive = [...currentActive];
1008
+ for (const name of mcpToolNames) {
1009
+ if (!nextActive.includes(name)) {
1010
+ nextActive.push(name);
1011
+ }
1012
+ }
1013
+
1014
+ await this.setActiveToolsByName(nextActive);
1015
+ }
1016
+
968
1017
  /** Whether auto-compaction is currently running */
969
1018
  get isCompacting(): boolean {
970
1019
  return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
@@ -4,9 +4,8 @@
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives";
7
- import { $env } from "@oh-my-pi/pi-utils";
7
+ import { $env, logger } from "@oh-my-pi/pi-utils";
8
8
  import { $ } from "bun";
9
- import chalk from "chalk";
10
9
  import { contextFileCapability } from "./capability/context-file";
11
10
  import { systemPromptCapability } from "./capability/system-prompt";
12
11
  import { renderPromptTemplate } from "./config/prompt-templates";
@@ -355,7 +354,7 @@ export async function resolvePromptInput(input: string | undefined, description:
355
354
  try {
356
355
  return await file.text();
357
356
  } catch (error) {
358
- console.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));
357
+ logger.warn(`Could not read ${description} file`, { path: input, error: String(error) });
359
358
  return input;
360
359
  }
361
360
  }
@@ -71,47 +71,35 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
71
71
  }
72
72
 
73
73
  function withAbortTimeout<T>(promise: Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<T> {
74
- return new Promise((resolve, reject) => {
75
- let settled = false;
76
- const timeoutId = setTimeout(() => {
77
- if (settled) return;
78
- settled = true;
79
- reject(new Error(`MCP tool call timed out after ${timeoutMs}ms`));
80
- }, timeoutMs);
81
-
82
- const onAbort = () => {
83
- if (settled) return;
84
- settled = true;
85
- clearTimeout(timeoutId);
86
- reject(new ToolAbortError());
87
- };
74
+ if (signal?.aborted) {
75
+ return Promise.reject(new ToolAbortError());
76
+ }
88
77
 
89
- if (signal) {
90
- if (signal.aborted) {
91
- clearTimeout(timeoutId);
92
- reject(new ToolAbortError());
93
- return;
94
- }
95
- signal.addEventListener("abort", onAbort, { once: true });
96
- }
78
+ const { promise: wrappedPromise, resolve, reject } = Promise.withResolvers<T>();
79
+ let settled = false;
80
+ const timeoutId = setTimeout(() => {
81
+ if (settled) return;
82
+ settled = true;
83
+ reject(new Error(`MCP tool call timed out after ${timeoutMs}ms`));
84
+ }, timeoutMs);
97
85
 
98
- promise.then(
99
- value => {
100
- if (settled) return;
101
- settled = true;
102
- clearTimeout(timeoutId);
103
- if (signal) signal.removeEventListener("abort", onAbort);
104
- resolve(value);
105
- },
106
- error => {
107
- if (settled) return;
108
- settled = true;
109
- clearTimeout(timeoutId);
110
- if (signal) signal.removeEventListener("abort", onAbort);
111
- reject(error);
112
- },
113
- );
86
+ const onAbort = () => {
87
+ if (settled) return;
88
+ settled = true;
89
+ clearTimeout(timeoutId);
90
+ reject(new ToolAbortError());
91
+ };
92
+
93
+ if (signal) {
94
+ signal.addEventListener("abort", onAbort, { once: true });
95
+ }
96
+
97
+ promise.then(resolve, reject).finally(() => {
98
+ if (signal) signal.removeEventListener("abort", onAbort);
99
+ clearTimeout(timeoutId);
114
100
  });
101
+
102
+ return wrappedPromise;
115
103
  }
116
104
 
117
105
  function getReportFindingKey(value: unknown): string | null {
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import path from "node:path";
4
- import { Snowflake } from "@oh-my-pi/pi-utils";
4
+ import { isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
5
5
  import { $ } from "bun";
6
6
 
7
7
  export interface WorktreeBaseline {
@@ -88,10 +88,13 @@ export async function applyBaseline(worktreeDir: string, baseline: WorktreeBasel
88
88
  for (const entry of baseline.untracked) {
89
89
  const source = path.join(baseline.repoRoot, entry);
90
90
  const destination = path.join(worktreeDir, entry);
91
- const exists = await Bun.file(source).exists();
92
- if (!exists) continue;
93
- await fs.mkdir(path.dirname(destination), { recursive: true });
94
- await fs.cp(source, destination, { recursive: true });
91
+ try {
92
+ await fs.mkdir(path.dirname(destination), { recursive: true });
93
+ await fs.cp(source, destination, { recursive: true });
94
+ } catch (err) {
95
+ if (isEnoent(err)) continue;
96
+ throw err;
97
+ }
95
98
  }
96
99
  }
97
100
 
package/src/tools/bash.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type * as fs from "node:fs";
1
+ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
+ import { isEnoent } from "@oh-my-pi/pi-utils";
6
7
  import { type Static, Type } from "@sinclair/typebox";
7
8
  import { renderPromptTemplate } from "../config/prompt-templates";
8
9
  import { type BashExecutorOptions, executeBash } from "../exec/bash-executor";
@@ -90,9 +91,12 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
90
91
  const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
91
92
  let cwdStat: fs.Stats;
92
93
  try {
93
- cwdStat = await Bun.file(commandCwd).stat();
94
- } catch {
95
- throw new ToolError(`Working directory does not exist: ${commandCwd}`);
94
+ cwdStat = await fs.promises.stat(commandCwd);
95
+ } catch (err) {
96
+ if (isEnoent(err)) {
97
+ throw new ToolError(`Working directory does not exist: ${commandCwd}`);
98
+ }
99
+ throw err;
96
100
  }
97
101
  if (!cwdStat.isDirectory()) {
98
102
  throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
@@ -1,4 +1,4 @@
1
- import { tmpdir } from "node:os";
1
+ import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import { Readability } from "@mozilla/readability";
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
@@ -1127,8 +1127,11 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
1127
1127
  const page = await this.#ensurePage(params);
1128
1128
  const value = (await untilAborted(signal, () =>
1129
1129
  page.evaluate((source: string) => {
1130
- const evaluator = new Function(`return (${source});`);
1131
- return evaluator();
1130
+ try {
1131
+ return new Function(`return (${source});`)();
1132
+ } catch {
1133
+ return new Function(source)();
1134
+ }
1132
1135
  }, script),
1133
1136
  )) as unknown;
1134
1137
  const output = formatEvaluateResult(value);
@@ -1289,7 +1292,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
1289
1292
  { maxBytes: 0.75 * 1024 * 1024 },
1290
1293
  );
1291
1294
  const dimensionNote = formatDimensionNote(resized);
1292
- const tempFile = path.join(tmpdir(), `omp-sshots-${Snowflake.next()}.png`);
1295
+ const tempFile = path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.png`);
1293
1296
  await Bun.write(tempFile, resized.buffer);
1294
1297
  details.screenshotPath = tempFile;
1295
1298
  details.mimeType = resized.mimeType;
package/src/tools/grep.ts CHANGED
@@ -51,15 +51,6 @@ export interface GrepToolDetails {
51
51
  error?: string;
52
52
  }
53
53
 
54
- export interface GrepOperations {
55
- isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
56
- readFile: (absolutePath: string) => Promise<string> | string;
57
- }
58
-
59
- export interface GrepToolOptions {
60
- operations?: GrepOperations;
61
- }
62
-
63
54
  type GrepParams = Static<typeof grepSchema>;
64
55
 
65
56
  export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
@@ -68,10 +59,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
68
59
  readonly description: string;
69
60
  readonly parameters = grepSchema;
70
61
 
71
- constructor(
72
- private readonly session: ToolSession,
73
- _options?: GrepToolOptions,
74
- ) {
62
+ constructor(private readonly session: ToolSession) {
75
63
  this.description = renderPromptTemplate(grepDescription);
76
64
  }
77
65
 
@@ -74,7 +74,7 @@ export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
74
74
  export { FetchTool, type FetchToolDetails } from "./fetch";
75
75
  export { type FindOperations, FindTool, type FindToolDetails, type FindToolInput, type FindToolOptions } from "./find";
76
76
  export { setPreferredImageProvider } from "./gemini-image";
77
- export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolInput, type GrepToolOptions } from "./grep";
77
+ export { GrepTool, type GrepToolDetails, type GrepToolInput } from "./grep";
78
78
  export { NotebookTool, type NotebookToolDetails } from "./notebook";
79
79
  export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
80
80
  export { ReadTool, type ReadToolDetails, type ReadToolInput } from "./read";
@@ -1,3 +1,5 @@
1
+ import { logger } from "@oh-my-pi/pi-utils";
2
+
1
3
  export class EventBus {
2
4
  readonly #listeners = new Map<string, Set<(data: unknown) => void>>();
3
5
 
@@ -18,7 +20,7 @@ export class EventBus {
18
20
  try {
19
21
  await handler(data);
20
22
  } catch (err) {
21
- console.error(`Event handler error (${channel}):`, err);
23
+ logger.error("Event handler error", { channel, error: String(err) });
22
24
  }
23
25
  };
24
26
  this.#listeners.get(channel)!.add(safeHandler);