@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.
- package/CHANGELOG.md +42 -0
- package/package.json +7 -7
- package/src/capability/mcp.ts +9 -0
- package/src/config/file-lock.ts +1 -1
- package/src/discovery/builtin.ts +48 -0
- package/src/discovery/mcp-json.ts +33 -0
- package/src/extensibility/slash-commands.ts +1 -0
- package/src/index.ts +0 -2
- package/src/mcp/config-writer.ts +194 -0
- package/src/mcp/config.ts +20 -6
- package/src/mcp/index.ts +4 -0
- package/src/mcp/loader.ts +6 -0
- package/src/mcp/manager.ts +92 -3
- package/src/mcp/oauth-discovery.ts +274 -0
- package/src/mcp/oauth-flow.ts +229 -0
- package/src/mcp/tool-bridge.ts +8 -8
- package/src/mcp/transports/http.ts +76 -35
- package/src/mcp/transports/stdio.ts +31 -16
- package/src/mcp/types.ts +15 -1
- package/src/modes/components/mcp-add-wizard.ts +1286 -0
- package/src/modes/components/tool-execution.ts +12 -24
- package/src/modes/controllers/input-controller.ts +8 -0
- package/src/modes/controllers/mcp-command-controller.ts +1223 -0
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +1 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +49 -0
- package/src/system-prompt.ts +2 -3
- package/src/task/executor.ts +26 -38
- package/src/task/worktree.ts +8 -5
- package/src/tools/bash.ts +8 -4
- package/src/tools/browser.ts +7 -4
- package/src/tools/grep.ts +1 -13
- package/src/tools/index.ts +1 -1
- package/src/utils/event-bus.ts +3 -1
|
@@ -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
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -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;
|
package/src/system-prompt.ts
CHANGED
|
@@ -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
|
-
|
|
357
|
+
logger.warn(`Could not read ${description} file`, { path: input, error: String(error) });
|
|
359
358
|
return input;
|
|
360
359
|
}
|
|
361
360
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 {
|
package/src/task/worktree.ts
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
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}`);
|
package/src/tools/browser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
1131
|
-
|
|
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
|
|
package/src/tools/index.ts
CHANGED
|
@@ -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 {
|
|
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";
|
package/src/utils/event-bus.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
+
logger.error("Event handler error", { channel, error: String(err) });
|
|
22
24
|
}
|
|
23
25
|
};
|
|
24
26
|
this.#listeners.get(channel)!.add(safeHandler);
|