@oh-my-pi/pi-coding-agent 13.13.2 → 13.14.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 +26 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +10 -3
- package/src/config/settings-schema.ts +3 -0
- package/src/discovery/helpers.ts +9 -2
- package/src/mcp/client.ts +29 -4
- package/src/mcp/manager.ts +256 -57
- package/src/mcp/tool-bridge.ts +189 -106
- package/src/mcp/transports/http.ts +8 -3
- package/src/modes/controllers/mcp-command-controller.ts +45 -0
- package/src/modes/interactive-mode.ts +9 -5
- package/src/patch/index.ts +15 -7
- package/src/prompts/agents/explore.md +4 -67
- package/src/session/agent-session.ts +7 -16
- package/src/slash-commands/builtin-registry.ts +1 -0
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import { sanitizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
|
|
8
|
+
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
10
|
import type { SourceMeta } from "../capability/types";
|
|
10
11
|
import type {
|
|
@@ -17,22 +18,43 @@ import type { Theme } from "../modes/theme/theme";
|
|
|
17
18
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
18
19
|
import { callTool } from "./client";
|
|
19
20
|
import { renderMCPCall, renderMCPResult } from "./render";
|
|
20
|
-
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types";
|
|
21
|
+
import type { MCPContent, MCPServerConnection, MCPToolCallParams, MCPToolCallResult, MCPToolDefinition } from "./types";
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (signal.aborted) {
|
|
25
|
-
return Promise.reject(signal.reason instanceof Error ? signal.reason : new ToolAbortError());
|
|
26
|
-
}
|
|
23
|
+
/** Reconnect callback: tears down stale connection, returns new one or null. */
|
|
24
|
+
export type MCPReconnect = () => Promise<MCPServerConnection | null>;
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Network-level and stale-session errors that warrant a reconnect + single retry.
|
|
28
|
+
* Conservative: only catches errors where the server is likely alive but the
|
|
29
|
+
* connection object is stale (dead SSE, expired session, refused after restart).
|
|
30
|
+
*/
|
|
31
|
+
const RETRIABLE_PATTERNS = [
|
|
32
|
+
"econnrefused",
|
|
33
|
+
"econnreset",
|
|
34
|
+
"epipe",
|
|
35
|
+
"enetunreach",
|
|
36
|
+
"ehostunreach",
|
|
37
|
+
"fetch failed",
|
|
38
|
+
"transport not connected",
|
|
39
|
+
"transport closed",
|
|
40
|
+
"network error",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export function isRetriableConnectionError(error: unknown): boolean {
|
|
44
|
+
if (!(error instanceof Error)) return false;
|
|
45
|
+
const msg = error.message.toLowerCase();
|
|
46
|
+
// Stale session (server restarted, old session ID is gone)
|
|
47
|
+
if (/^http (404|502|503):/.test(msg)) return true;
|
|
48
|
+
return RETRIABLE_PATTERNS.some(p => msg.includes(p));
|
|
49
|
+
}
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
type MCPToolArgs = NonNullable<MCPToolCallParams["arguments"]>;
|
|
52
|
+
|
|
53
|
+
function normalizeToolArgs(value: unknown): MCPToolArgs {
|
|
54
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
return value as MCPToolArgs;
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
/** Details included in MCP tool results for rendering */
|
|
@@ -77,6 +99,60 @@ function formatMCPContent(content: MCPContent[]): string {
|
|
|
77
99
|
return parts.join("\n\n");
|
|
78
100
|
}
|
|
79
101
|
|
|
102
|
+
/** Build a CustomToolResult from a callTool response. */
|
|
103
|
+
function buildResult(
|
|
104
|
+
result: MCPToolCallResult,
|
|
105
|
+
serverName: string,
|
|
106
|
+
mcpToolName: string,
|
|
107
|
+
provider?: string,
|
|
108
|
+
providerName?: string,
|
|
109
|
+
): CustomToolResult<MCPToolDetails> {
|
|
110
|
+
const text = formatMCPContent(result.content);
|
|
111
|
+
const details: MCPToolDetails = {
|
|
112
|
+
serverName,
|
|
113
|
+
mcpToolName,
|
|
114
|
+
isError: result.isError,
|
|
115
|
+
rawContent: result.content,
|
|
116
|
+
provider,
|
|
117
|
+
providerName,
|
|
118
|
+
};
|
|
119
|
+
if (result.isError) {
|
|
120
|
+
return { content: [{ type: "text", text: `Error: ${text}` }], details };
|
|
121
|
+
}
|
|
122
|
+
return { content: [{ type: "text", text }], details };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Build an error CustomToolResult from a caught exception. */
|
|
126
|
+
function buildErrorResult(
|
|
127
|
+
error: unknown,
|
|
128
|
+
serverName: string,
|
|
129
|
+
mcpToolName: string,
|
|
130
|
+
provider?: string,
|
|
131
|
+
providerName?: string,
|
|
132
|
+
): CustomToolResult<MCPToolDetails> {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
136
|
+
details: { serverName, mcpToolName, isError: true, provider, providerName },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Re-throw abort-related errors so they bypass error-result handling. */
|
|
141
|
+
function rethrowIfAborted(error: unknown, signal?: AbortSignal): void {
|
|
142
|
+
if (error instanceof ToolAbortError) throw error;
|
|
143
|
+
if (error instanceof Error && error.name === "AbortError") throw new ToolAbortError();
|
|
144
|
+
if (signal?.aborted) throw new ToolAbortError();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function reconnectWithAbort(reconnect: MCPReconnect, signal?: AbortSignal): Promise<MCPServerConnection | null> {
|
|
148
|
+
try {
|
|
149
|
+
return await untilAborted(signal, reconnect);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
rethrowIfAborted(error, signal);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
80
156
|
/**
|
|
81
157
|
* Create a unique tool name for an MCP tool.
|
|
82
158
|
*
|
|
@@ -143,13 +219,14 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
143
219
|
readonly mcpServerName: string;
|
|
144
220
|
|
|
145
221
|
/** Create MCPTool instances for all tools from an MCP server connection */
|
|
146
|
-
static fromTools(connection: MCPServerConnection, tools: MCPToolDefinition[]): MCPTool[] {
|
|
147
|
-
return tools.map(tool => new MCPTool(connection, tool));
|
|
222
|
+
static fromTools(connection: MCPServerConnection, tools: MCPToolDefinition[], reconnect?: MCPReconnect): MCPTool[] {
|
|
223
|
+
return tools.map(tool => new MCPTool(connection, tool, reconnect));
|
|
148
224
|
}
|
|
149
225
|
|
|
150
226
|
constructor(
|
|
151
|
-
private
|
|
227
|
+
private connection: MCPServerConnection,
|
|
152
228
|
private readonly tool: MCPToolDefinition,
|
|
229
|
+
private readonly reconnect?: MCPReconnect,
|
|
153
230
|
) {
|
|
154
231
|
this.name = createMCPToolName(connection.name, tool.name);
|
|
155
232
|
this.label = `${connection.name}/${tool.name}`;
|
|
@@ -160,11 +237,11 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
160
237
|
}
|
|
161
238
|
|
|
162
239
|
renderCall(args: unknown, _options: RenderResultOptions, theme: Theme) {
|
|
163
|
-
return renderMCPCall((args
|
|
240
|
+
return renderMCPCall(normalizeToolArgs(args), theme, this.label);
|
|
164
241
|
}
|
|
165
242
|
|
|
166
243
|
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
|
167
|
-
return renderMCPResult(result, options, theme, (args
|
|
244
|
+
return renderMCPResult(result, options, theme, normalizeToolArgs(args));
|
|
168
245
|
}
|
|
169
246
|
|
|
170
247
|
async execute(
|
|
@@ -175,51 +252,38 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
175
252
|
signal?: AbortSignal,
|
|
176
253
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
177
254
|
throwIfAborted(signal);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const text = formatMCPContent(result.content);
|
|
182
|
-
const details: MCPToolDetails = {
|
|
183
|
-
serverName: this.connection.name,
|
|
184
|
-
mcpToolName: this.tool.name,
|
|
185
|
-
isError: result.isError,
|
|
186
|
-
rawContent: result.content,
|
|
187
|
-
provider: this.connection._source?.provider,
|
|
188
|
-
providerName: this.connection._source?.providerName,
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
if (result.isError) {
|
|
192
|
-
return {
|
|
193
|
-
content: [{ type: "text", text: `Error: ${text}` }],
|
|
194
|
-
details,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
255
|
+
const args = normalizeToolArgs(params);
|
|
256
|
+
const provider = this.connection._source?.provider;
|
|
257
|
+
const providerName = this.connection._source?.providerName;
|
|
197
258
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
};
|
|
259
|
+
try {
|
|
260
|
+
const result = await callTool(this.connection, this.tool.name, args, { signal });
|
|
261
|
+
return buildResult(result, this.connection.name, this.tool.name, provider, providerName);
|
|
202
262
|
} catch (error) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
rethrowIfAborted(error, signal);
|
|
264
|
+
if (this.reconnect && isRetriableConnectionError(error)) {
|
|
265
|
+
const newConn = await reconnectWithAbort(this.reconnect, signal);
|
|
266
|
+
if (newConn) {
|
|
267
|
+
// Rebind so subsequent calls on this instance use the fresh connection
|
|
268
|
+
this.connection = newConn;
|
|
269
|
+
const retryProvider = newConn._source?.provider ?? provider;
|
|
270
|
+
const retryProviderName = newConn._source?.providerName ?? providerName;
|
|
271
|
+
try {
|
|
272
|
+
const result = await callTool(newConn, this.tool.name, args, { signal });
|
|
273
|
+
return buildResult(result, newConn.name, this.tool.name, retryProvider, retryProviderName);
|
|
274
|
+
} catch (retryError) {
|
|
275
|
+
rethrowIfAborted(retryError, signal);
|
|
276
|
+
return buildErrorResult(
|
|
277
|
+
retryError,
|
|
278
|
+
this.connection.name,
|
|
279
|
+
this.tool.name,
|
|
280
|
+
retryProvider,
|
|
281
|
+
retryProviderName,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
211
285
|
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
215
|
-
details: {
|
|
216
|
-
serverName: this.connection.name,
|
|
217
|
-
mcpToolName: this.tool.name,
|
|
218
|
-
isError: true,
|
|
219
|
-
provider: this.connection._source?.provider,
|
|
220
|
-
providerName: this.connection._source?.providerName,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
286
|
+
return buildErrorResult(error, this.connection.name, this.tool.name, provider, providerName);
|
|
223
287
|
}
|
|
224
288
|
}
|
|
225
289
|
}
|
|
@@ -245,8 +309,9 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
245
309
|
tools: MCPToolDefinition[],
|
|
246
310
|
getConnection: () => Promise<MCPServerConnection>,
|
|
247
311
|
source?: SourceMeta,
|
|
312
|
+
reconnect?: MCPReconnect,
|
|
248
313
|
): DeferredMCPTool[] {
|
|
249
|
-
return tools.map(tool => new DeferredMCPTool(serverName, tool, getConnection, source));
|
|
314
|
+
return tools.map(tool => new DeferredMCPTool(serverName, tool, getConnection, source, reconnect));
|
|
250
315
|
}
|
|
251
316
|
|
|
252
317
|
constructor(
|
|
@@ -254,6 +319,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
254
319
|
private readonly tool: MCPToolDefinition,
|
|
255
320
|
private readonly getConnection: () => Promise<MCPServerConnection>,
|
|
256
321
|
source?: SourceMeta,
|
|
322
|
+
private readonly reconnect?: MCPReconnect,
|
|
257
323
|
) {
|
|
258
324
|
this.name = createMCPToolName(serverName, tool.name);
|
|
259
325
|
this.label = `${serverName}/${tool.name}`;
|
|
@@ -266,11 +332,11 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
266
332
|
}
|
|
267
333
|
|
|
268
334
|
renderCall(args: unknown, _options: RenderResultOptions, theme: Theme) {
|
|
269
|
-
return renderMCPCall((args
|
|
335
|
+
return renderMCPCall(normalizeToolArgs(args), theme, this.label);
|
|
270
336
|
}
|
|
271
337
|
|
|
272
338
|
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
|
273
|
-
return renderMCPResult(result, options, theme, (args
|
|
339
|
+
return renderMCPResult(result, options, theme, normalizeToolArgs(args));
|
|
274
340
|
}
|
|
275
341
|
|
|
276
342
|
async execute(
|
|
@@ -281,53 +347,70 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
281
347
|
signal?: AbortSignal,
|
|
282
348
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
283
349
|
throwIfAborted(signal);
|
|
350
|
+
const args = normalizeToolArgs(params);
|
|
351
|
+
const provider = this.#fallbackProvider;
|
|
352
|
+
const providerName = this.#fallbackProviderName;
|
|
353
|
+
|
|
284
354
|
try {
|
|
285
|
-
const connection = await
|
|
355
|
+
const connection = await untilAborted(signal, () => this.getConnection());
|
|
286
356
|
throwIfAborted(signal);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
357
|
+
try {
|
|
358
|
+
const result = await callTool(connection, this.tool.name, args, { signal });
|
|
359
|
+
return buildResult(
|
|
360
|
+
result,
|
|
361
|
+
this.serverName,
|
|
362
|
+
this.tool.name,
|
|
363
|
+
connection._source?.provider ?? provider,
|
|
364
|
+
connection._source?.providerName ?? providerName,
|
|
365
|
+
);
|
|
366
|
+
} catch (callError) {
|
|
367
|
+
rethrowIfAborted(callError, signal);
|
|
368
|
+
if (this.reconnect && isRetriableConnectionError(callError)) {
|
|
369
|
+
const newConn = await reconnectWithAbort(this.reconnect, signal);
|
|
370
|
+
if (newConn) {
|
|
371
|
+
const retryProvider = newConn._source?.provider ?? provider;
|
|
372
|
+
const retryProviderName = newConn._source?.providerName ?? providerName;
|
|
373
|
+
try {
|
|
374
|
+
const result = await callTool(newConn, this.tool.name, args, { signal });
|
|
375
|
+
return buildResult(result, this.serverName, this.tool.name, retryProvider, retryProviderName);
|
|
376
|
+
} catch (retryError) {
|
|
377
|
+
rethrowIfAborted(retryError, signal);
|
|
378
|
+
return buildErrorResult(
|
|
379
|
+
retryError,
|
|
380
|
+
this.serverName,
|
|
381
|
+
this.tool.name,
|
|
382
|
+
retryProvider,
|
|
383
|
+
retryProviderName,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return buildErrorResult(callError, this.serverName, this.tool.name, provider, providerName);
|
|
316
389
|
}
|
|
317
|
-
|
|
318
|
-
|
|
390
|
+
} catch (connError) {
|
|
391
|
+
// getConnection() failed — server never connected or connection lost.
|
|
392
|
+
// This is always worth a reconnect attempt for deferred tools, since the
|
|
393
|
+
// error ("MCP server not connected") isn't a network error from callTool.
|
|
394
|
+
rethrowIfAborted(connError, signal);
|
|
395
|
+
if (this.reconnect) {
|
|
396
|
+
const newConn = await reconnectWithAbort(this.reconnect, signal);
|
|
397
|
+
if (newConn) {
|
|
398
|
+
try {
|
|
399
|
+
const result = await callTool(newConn, this.tool.name, args, { signal });
|
|
400
|
+
return buildResult(
|
|
401
|
+
result,
|
|
402
|
+
this.serverName,
|
|
403
|
+
this.tool.name,
|
|
404
|
+
newConn._source?.provider ?? provider,
|
|
405
|
+
newConn._source?.providerName ?? providerName,
|
|
406
|
+
);
|
|
407
|
+
} catch (retryError) {
|
|
408
|
+
rethrowIfAborted(retryError, signal);
|
|
409
|
+
return buildErrorResult(retryError, this.serverName, this.tool.name, provider, providerName);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
319
412
|
}
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
323
|
-
details: {
|
|
324
|
-
serverName: this.serverName,
|
|
325
|
-
mcpToolName: this.tool.name,
|
|
326
|
-
isError: true,
|
|
327
|
-
provider: this.#fallbackProvider,
|
|
328
|
-
providerName: this.#fallbackProviderName,
|
|
329
|
-
},
|
|
330
|
-
};
|
|
413
|
+
return buildErrorResult(connError, this.serverName, this.tool.name, provider, providerName);
|
|
331
414
|
}
|
|
332
415
|
}
|
|
333
416
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
|
-
import { readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { logger, readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type {
|
|
9
9
|
JsonRpcError,
|
|
10
10
|
JsonRpcMessage,
|
|
@@ -91,13 +91,16 @@ export class HttpTransport implements MCPTransport {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// Connection established — read messages in background
|
|
94
|
+
// Connection established — read messages in background.
|
|
95
|
+
// If the stream ends unexpectedly (server restart, network drop),
|
|
96
|
+
// fire onClose so the manager can trigger reconnection.
|
|
95
97
|
const signal = this.#sseConnection.signal;
|
|
96
98
|
void this.#readSSEStream(response.body!, signal).finally(() => {
|
|
99
|
+
const wasConnected = this.#connected;
|
|
97
100
|
this.#sseConnection = null;
|
|
101
|
+
if (wasConnected) this.onClose?.();
|
|
98
102
|
});
|
|
99
103
|
}
|
|
100
|
-
|
|
101
104
|
async #readSSEStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
|
|
102
105
|
try {
|
|
103
106
|
for await (const message of readSseJson<JsonRpcMessage>(body, signal)) {
|
|
@@ -106,6 +109,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
106
109
|
}
|
|
107
110
|
} catch (error) {
|
|
108
111
|
if (error instanceof Error && error.name !== "AbortError") {
|
|
112
|
+
logger.debug("HTTP SSE stream error", { url: this.config.url, error: error.message });
|
|
109
113
|
this.onError?.(error);
|
|
110
114
|
}
|
|
111
115
|
}
|
|
@@ -457,6 +461,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
457
461
|
}
|
|
458
462
|
|
|
459
463
|
this.onClose?.();
|
|
464
|
+
this.onClose = undefined;
|
|
460
465
|
}
|
|
461
466
|
}
|
|
462
467
|
|
|
@@ -127,6 +127,9 @@ export class MCPCommandController {
|
|
|
127
127
|
case "smithery-logout":
|
|
128
128
|
await this.#handleSmitheryLogout();
|
|
129
129
|
break;
|
|
130
|
+
case "reconnect":
|
|
131
|
+
await this.#handleReconnect(parts[2]);
|
|
132
|
+
break;
|
|
130
133
|
case "reload":
|
|
131
134
|
await this.#handleReload();
|
|
132
135
|
break;
|
|
@@ -159,6 +162,7 @@ export class MCPCommandController {
|
|
|
159
162
|
" Search Smithery registry and deploy from picker",
|
|
160
163
|
" /mcp smithery-login Login to Smithery and cache API key",
|
|
161
164
|
" /mcp smithery-logout Remove cached Smithery API key",
|
|
165
|
+
" /mcp reconnect <name> Reconnect to a specific MCP server",
|
|
162
166
|
" /mcp reload Force reload and rediscover MCP runtime tools",
|
|
163
167
|
" /mcp resources List available resources from connected servers",
|
|
164
168
|
" /mcp prompts List available prompts from connected servers",
|
|
@@ -1372,6 +1376,47 @@ export class MCPCommandController {
|
|
|
1372
1376
|
}
|
|
1373
1377
|
}
|
|
1374
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* Handle /mcp reconnect <name> - Reconnect to a specific server.
|
|
1381
|
+
*/
|
|
1382
|
+
async #handleReconnect(name: string | undefined): Promise<void> {
|
|
1383
|
+
if (!name) {
|
|
1384
|
+
this.ctx.showError("Server name required. Usage: /mcp reconnect <name>");
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (!this.ctx.mcpManager) {
|
|
1388
|
+
this.ctx.showError("MCP manager not available.");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
this.#showMessage(["", theme.fg("muted", `Reconnecting to "${name}"...`), ""].join("\n"));
|
|
1393
|
+
|
|
1394
|
+
try {
|
|
1395
|
+
const connection = await this.ctx.mcpManager.reconnectServer(name);
|
|
1396
|
+
if (connection) {
|
|
1397
|
+
// refreshMCPTools re-registers tools and preserves the user's prior
|
|
1398
|
+
// MCP tool selection. No need to call activateDiscoveredMCPTools —
|
|
1399
|
+
// that would broaden the selection to all server tools.
|
|
1400
|
+
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1401
|
+
const serverTools = this.ctx.mcpManager.getTools().filter(t => t.mcpServerName === name);
|
|
1402
|
+
this.#showMessage(
|
|
1403
|
+
[
|
|
1404
|
+
"\n",
|
|
1405
|
+
theme.fg("success", `\u2713 Reconnected to "${name}"`),
|
|
1406
|
+
` Tools: ${serverTools.length}`,
|
|
1407
|
+
"\n",
|
|
1408
|
+
].join("\n"),
|
|
1409
|
+
);
|
|
1410
|
+
} else {
|
|
1411
|
+
this.ctx.showError(`Failed to reconnect to "${name}". Check server status and logs.`);
|
|
1412
|
+
}
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
this.ctx.showError(
|
|
1415
|
+
`Failed to reconnect to "${name}": ${error instanceof Error ? error.message : String(error)}`,
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1375
1420
|
/**
|
|
1376
1421
|
* Reload MCP manager with new configs
|
|
1377
1422
|
*/
|
|
@@ -6,7 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { Component, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { KeybindingsManager } from "../config/keybindings";
|
|
@@ -1111,8 +1111,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1111
1111
|
this.#startMicAnimation();
|
|
1112
1112
|
} else if (state === "transcribing") {
|
|
1113
1113
|
this.#stopMicAnimation();
|
|
1114
|
-
this
|
|
1115
|
-
this.editor.cursorOverrideWidth = 1;
|
|
1114
|
+
this.#setMicCursor({ r: 200, g: 200, b: 200 });
|
|
1116
1115
|
} else {
|
|
1117
1116
|
this.#cleanupMicAnimation();
|
|
1118
1117
|
}
|
|
@@ -1122,10 +1121,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1122
1121
|
});
|
|
1123
1122
|
}
|
|
1124
1123
|
|
|
1124
|
+
#setMicCursor(color: { r: number; g: number; b: number }): void {
|
|
1125
|
+
this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
|
|
1126
|
+
// Theme symbols can be wide (for example, 🎤), so measure the rendered override.
|
|
1127
|
+
this.editor.cursorOverrideWidth = visibleWidth(this.editor.cursorOverride);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1125
1130
|
#updateMicIcon(): void {
|
|
1126
1131
|
const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
|
|
1127
|
-
this
|
|
1128
|
-
this.editor.cursorOverrideWidth = 1;
|
|
1132
|
+
this.#setMicCursor({ r, g, b });
|
|
1129
1133
|
}
|
|
1130
1134
|
|
|
1131
1135
|
#startMicAnimation(): void {
|
package/src/patch/index.ts
CHANGED
|
@@ -97,8 +97,12 @@ const patchEditSchema = Type.Object({
|
|
|
97
97
|
export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
98
98
|
export type PatchParams = Static<typeof patchEditSchema>;
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Pattern matching hashline display format prefixes: `LINE#ID:CONTENT`, `#ID:CONTENT`, and `+ID:CONTENT`.
|
|
102
|
+
* A plus-prefixed form appears in diff-like output and should be treated as hashline metadata too.
|
|
103
|
+
*/
|
|
104
|
+
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:\+?\s*(?:\d+\s*#\s*|#\s*)|\+)\s*[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
105
|
+
const HASHLINE_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*(?:\d+\s*#\s*|#\s*)?[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
102
106
|
|
|
103
107
|
/** Pattern matching a unified-diff added-line `+` prefix (but not `++`). Does NOT match `-` to avoid corrupting Markdown list items. */
|
|
104
108
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
@@ -111,27 +115,31 @@ const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
|
111
115
|
* output file. This strips them heuristically before application.
|
|
112
116
|
*/
|
|
113
117
|
export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
114
|
-
// Hashline prefixes are highly specific to read output and
|
|
115
|
-
//
|
|
116
|
-
//
|
|
118
|
+
// Hashline prefixes are highly specific to read output and are usually stripped only when
|
|
119
|
+
// *every* non-empty line carries one. If a line is prefixed as `+ID:`, strip that
|
|
120
|
+
// prefix while leaving other `+` lines untouched to avoid corrupting mixed snippets.
|
|
117
121
|
let hashPrefixCount = 0;
|
|
122
|
+
let diffPlusHashPrefixCount = 0;
|
|
118
123
|
let diffPlusCount = 0;
|
|
119
124
|
let nonEmpty = 0;
|
|
120
125
|
for (const l of lines) {
|
|
121
126
|
if (l.length === 0) continue;
|
|
122
127
|
nonEmpty++;
|
|
123
128
|
if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
|
|
129
|
+
if (HASHLINE_PREFIX_PLUS_RE.test(l)) diffPlusHashPrefixCount++;
|
|
124
130
|
if (DIFF_PLUS_RE.test(l)) diffPlusCount++;
|
|
125
131
|
}
|
|
126
132
|
if (nonEmpty === 0) return lines;
|
|
127
133
|
|
|
128
134
|
const stripHash = hashPrefixCount > 0 && hashPrefixCount === nonEmpty;
|
|
129
|
-
const stripPlus =
|
|
130
|
-
|
|
135
|
+
const stripPlus =
|
|
136
|
+
!stripHash && diffPlusHashPrefixCount === 0 && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
|
|
137
|
+
if (!stripHash && !stripPlus && diffPlusHashPrefixCount === 0) return lines;
|
|
131
138
|
|
|
132
139
|
return lines.map(l => {
|
|
133
140
|
if (stripHash) return l.replace(HASHLINE_PREFIX_RE, "");
|
|
134
141
|
if (stripPlus) return l.replace(DIFF_PLUS_RE, "");
|
|
142
|
+
if (diffPlusHashPrefixCount > 0 && HASHLINE_PREFIX_PLUS_RE.test(l)) return l.replace(HASHLINE_PREFIX_RE, "");
|
|
135
143
|
return l;
|
|
136
144
|
});
|
|
137
145
|
}
|
|
@@ -3,7 +3,7 @@ name: explore
|
|
|
3
3
|
description: Fast read-only codebase scout returning compressed context for handoff
|
|
4
4
|
tools: read, grep, find, fetch, web_search
|
|
5
5
|
model: pi/smol
|
|
6
|
-
thinking-level:
|
|
6
|
+
thinking-level: med
|
|
7
7
|
output:
|
|
8
8
|
properties:
|
|
9
9
|
summary:
|
|
@@ -12,84 +12,21 @@ output:
|
|
|
12
12
|
type: string
|
|
13
13
|
files:
|
|
14
14
|
metadata:
|
|
15
|
-
description: Files examined with
|
|
15
|
+
description: Files examined with relevant code references
|
|
16
16
|
elements:
|
|
17
17
|
properties:
|
|
18
|
-
|
|
18
|
+
ref:
|
|
19
19
|
metadata:
|
|
20
|
-
description:
|
|
20
|
+
description: Project-relative path or paths to the most relevant code reference(s), optionally suffixed with line ranges like `:12-34` when relevant
|
|
21
21
|
type: string
|
|
22
|
-
line_start:
|
|
23
|
-
metadata:
|
|
24
|
-
description: First line read (1-indexed)
|
|
25
|
-
type: number
|
|
26
|
-
line_end:
|
|
27
|
-
metadata:
|
|
28
|
-
description: Last line read (1-indexed)
|
|
29
|
-
type: number
|
|
30
22
|
description:
|
|
31
23
|
metadata:
|
|
32
24
|
description: Section contents
|
|
33
25
|
type: string
|
|
34
|
-
code:
|
|
35
|
-
metadata:
|
|
36
|
-
description: Critical types/interfaces/functions extracted verbatim
|
|
37
|
-
elements:
|
|
38
|
-
properties:
|
|
39
|
-
path:
|
|
40
|
-
metadata:
|
|
41
|
-
description: Absolute path to source file
|
|
42
|
-
type: string
|
|
43
|
-
line_start:
|
|
44
|
-
metadata:
|
|
45
|
-
description: Excerpt first line (1-indexed)
|
|
46
|
-
type: number
|
|
47
|
-
line_end:
|
|
48
|
-
metadata:
|
|
49
|
-
description: Excerpt last line (1-indexed)
|
|
50
|
-
type: number
|
|
51
|
-
language:
|
|
52
|
-
metadata:
|
|
53
|
-
description: Language id for syntax highlighting
|
|
54
|
-
type: string
|
|
55
|
-
content:
|
|
56
|
-
metadata:
|
|
57
|
-
description: Verbatim code excerpt
|
|
58
|
-
type: string
|
|
59
26
|
architecture:
|
|
60
27
|
metadata:
|
|
61
28
|
description: Brief explanation of how pieces connect
|
|
62
29
|
type: string
|
|
63
|
-
dependencies:
|
|
64
|
-
metadata:
|
|
65
|
-
description: Key internal and external dependencies relevant to the task
|
|
66
|
-
elements:
|
|
67
|
-
properties:
|
|
68
|
-
name:
|
|
69
|
-
metadata:
|
|
70
|
-
description: Package or module name
|
|
71
|
-
type: string
|
|
72
|
-
role:
|
|
73
|
-
metadata:
|
|
74
|
-
description: What it provides in context of the task
|
|
75
|
-
type: string
|
|
76
|
-
risks:
|
|
77
|
-
metadata:
|
|
78
|
-
description: Gotchas, edge cases, or constraints the receiving agent should know
|
|
79
|
-
elements:
|
|
80
|
-
type: string
|
|
81
|
-
start_here:
|
|
82
|
-
metadata:
|
|
83
|
-
description: Recommended entry point for receiving agent
|
|
84
|
-
properties:
|
|
85
|
-
path:
|
|
86
|
-
metadata:
|
|
87
|
-
description: Absolute path to start reading
|
|
88
|
-
type: string
|
|
89
|
-
reason:
|
|
90
|
-
metadata:
|
|
91
|
-
description: Why this file best starting point
|
|
92
|
-
type: string
|
|
93
30
|
---
|
|
94
31
|
|
|
95
32
|
You are a file search specialist and a codebase scout.
|