@oh-my-pi/pi-coding-agent 13.10.0 → 13.10.1
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 +9 -0
- package/package.json +7 -7
- package/src/main.ts +25 -14
- package/src/mcp/manager.ts +40 -2
- package/src/mcp/oauth-flow.ts +41 -0
- package/src/mcp/transports/http.ts +23 -0
- package/src/mcp/types.ts +6 -0
- package/src/modes/components/mcp-add-wizard.ts +12 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +9 -1
- package/src/session/session-manager.ts +5 -9
- package/src/tools/json-tree.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.10.1] - 2026-03-10
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Exported `submitInteractiveInput()` function for programmatic submission of user input in interactive mode
|
|
9
|
+
- Added proactive OAuth token refresh for MCP server connections with 5-minute expiry buffer
|
|
10
|
+
- Added reactive 401/403 retry with automatic token refresh on HTTP MCP transports
|
|
11
|
+
- Added `refreshMCPOAuthToken()` for standard OAuth 2.0 refresh_token grants
|
|
12
|
+
- Persisted `tokenUrl`, `clientId`, and `clientSecret` in MCP auth config for cross-session token refresh
|
|
13
|
+
|
|
5
14
|
## [13.10.0] - 2026-03-10
|
|
6
15
|
### Fixed
|
|
7
16
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.10.
|
|
4
|
+
"version": "13.10.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.10.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.10.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.10.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.10.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.10.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.10.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.10.1",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.10.1",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.10.1",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.10.1",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.10.1",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.10.1",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
package/src/main.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { exportFromFile } from "./export/html";
|
|
|
26
26
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
27
27
|
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes";
|
|
28
28
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
29
|
+
import type { SubmittedUserInput } from "./modes/types";
|
|
29
30
|
import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
|
|
30
31
|
import type { AgentSession } from "./session/agent-session";
|
|
31
32
|
import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
|
|
@@ -66,6 +67,29 @@ export interface InteractiveModeNotify {
|
|
|
66
67
|
message: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
export async function submitInteractiveInput(
|
|
71
|
+
mode: Pick<InteractiveMode, "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError">,
|
|
72
|
+
session: Pick<AgentSession, "prompt">,
|
|
73
|
+
input: SubmittedUserInput,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
if (input.cancelled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Continue shortcuts submit an already-started empty prompt with no optimistic user message.
|
|
81
|
+
if (!input.started && !mode.markPendingSubmissionStarted(input)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await session.prompt(input.text, { images: input.images });
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
87
|
+
mode.showError(errorMessage);
|
|
88
|
+
} finally {
|
|
89
|
+
mode.finishPendingSubmission(input);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
async function runInteractiveMode(
|
|
70
94
|
session: AgentSession,
|
|
71
95
|
version: string,
|
|
@@ -126,20 +150,7 @@ async function runInteractiveMode(
|
|
|
126
150
|
|
|
127
151
|
while (true) {
|
|
128
152
|
const input = await mode.getUserInput();
|
|
129
|
-
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
if (!mode.markPendingSubmissionStarted(input)) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
await session.prompt(input.text, { images: input.images });
|
|
137
|
-
} catch (error: unknown) {
|
|
138
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
139
|
-
mode.showError(errorMessage);
|
|
140
|
-
} finally {
|
|
141
|
-
mode.finishPendingSubmission(input);
|
|
142
|
-
}
|
|
153
|
+
await submitInteractiveInput(mode, session, input);
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
156
|
|
package/src/mcp/manager.ts
CHANGED
|
@@ -25,9 +25,11 @@ import {
|
|
|
25
25
|
unsubscribeFromResources,
|
|
26
26
|
} from "./client";
|
|
27
27
|
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
28
|
+
import { refreshMCPOAuthToken } from "./oauth-flow";
|
|
28
29
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
29
30
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
30
31
|
import type { MCPToolCache } from "./tool-cache";
|
|
32
|
+
import { HttpTransport } from "./transports/http";
|
|
31
33
|
import type {
|
|
32
34
|
MCPGetPromptResult,
|
|
33
35
|
MCPPrompt,
|
|
@@ -315,6 +317,18 @@ export class MCPManager {
|
|
|
315
317
|
this.#pendingConnections.delete(name);
|
|
316
318
|
this.#connections.set(name, connection);
|
|
317
319
|
}
|
|
320
|
+
|
|
321
|
+
// Wire auth refresh for HTTP transports so 401s trigger token refresh
|
|
322
|
+
if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
|
|
323
|
+
connection.transport.onAuthError = async () => {
|
|
324
|
+
const refreshed = await this.#resolveAuthConfig(config, true);
|
|
325
|
+
if (refreshed.type === "http" || refreshed.type === "sse") {
|
|
326
|
+
return refreshed.headers ?? null;
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
return connection;
|
|
319
333
|
},
|
|
320
334
|
error => {
|
|
@@ -814,15 +828,39 @@ export class MCPManager {
|
|
|
814
828
|
/**
|
|
815
829
|
* Resolve OAuth credentials and shell commands in config.
|
|
816
830
|
*/
|
|
817
|
-
async #resolveAuthConfig(config: MCPServerConfig): Promise<MCPServerConfig> {
|
|
831
|
+
async #resolveAuthConfig(config: MCPServerConfig, forceRefresh = false): Promise<MCPServerConfig> {
|
|
818
832
|
let resolved: MCPServerConfig = { ...config };
|
|
819
833
|
|
|
820
834
|
const auth = config.auth;
|
|
821
835
|
if (auth?.type === "oauth" && auth.credentialId && this.#authStorage) {
|
|
822
836
|
const credentialId = auth.credentialId;
|
|
823
837
|
try {
|
|
824
|
-
|
|
838
|
+
let credential = this.#authStorage.get(credentialId);
|
|
825
839
|
if (credential?.type === "oauth") {
|
|
840
|
+
// Proactive refresh: 5-minute buffer before expiry
|
|
841
|
+
// Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
|
|
842
|
+
const REFRESH_BUFFER_MS = 5 * 60_000;
|
|
843
|
+
const shouldRefresh =
|
|
844
|
+
forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
|
|
845
|
+
if (shouldRefresh && credential.refresh && auth.tokenUrl) {
|
|
846
|
+
try {
|
|
847
|
+
const refreshed = await refreshMCPOAuthToken(
|
|
848
|
+
auth.tokenUrl,
|
|
849
|
+
credential.refresh,
|
|
850
|
+
auth.clientId,
|
|
851
|
+
auth.clientSecret,
|
|
852
|
+
);
|
|
853
|
+
const refreshedCredential = { type: "oauth" as const, ...refreshed };
|
|
854
|
+
await this.#authStorage.set(credentialId, refreshedCredential);
|
|
855
|
+
credential = refreshedCredential;
|
|
856
|
+
} catch (refreshError) {
|
|
857
|
+
logger.warn("MCP OAuth refresh failed, using existing token", {
|
|
858
|
+
credentialId,
|
|
859
|
+
error: refreshError,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
826
864
|
if (resolved.type === "http" || resolved.type === "sse") {
|
|
827
865
|
resolved = {
|
|
828
866
|
...resolved,
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -254,3 +254,44 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Refresh an MCP OAuth token using the standard refresh_token grant.
|
|
260
|
+
* Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
|
|
261
|
+
*/
|
|
262
|
+
export async function refreshMCPOAuthToken(
|
|
263
|
+
tokenUrl: string,
|
|
264
|
+
refreshToken: string,
|
|
265
|
+
clientId?: string,
|
|
266
|
+
clientSecret?: string,
|
|
267
|
+
): Promise<OAuthCredentials> {
|
|
268
|
+
const params = new URLSearchParams({
|
|
269
|
+
grant_type: "refresh_token",
|
|
270
|
+
refresh_token: refreshToken,
|
|
271
|
+
});
|
|
272
|
+
if (clientId) params.set("client_id", clientId);
|
|
273
|
+
if (clientSecret) params.set("client_secret", clientSecret);
|
|
274
|
+
|
|
275
|
+
const response = await fetch(tokenUrl, {
|
|
276
|
+
method: "POST",
|
|
277
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
278
|
+
body: params.toString(),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
const text = await response.text();
|
|
283
|
+
throw new Error(`MCP OAuth refresh failed: ${response.status} ${text}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const data = (await response.json()) as {
|
|
287
|
+
access_token: string;
|
|
288
|
+
refresh_token?: string;
|
|
289
|
+
expires_in?: number;
|
|
290
|
+
};
|
|
291
|
+
const expiresIn = data.expires_in ?? 3600;
|
|
292
|
+
return {
|
|
293
|
+
access: data.access_token,
|
|
294
|
+
refresh: data.refresh_token ?? refreshToken,
|
|
295
|
+
expires: Date.now() + expiresIn * 1000,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -26,6 +26,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
26
26
|
onClose?: () => void;
|
|
27
27
|
onError?: (error: Error) => void;
|
|
28
28
|
onNotification?: (method: string, params: unknown) => void;
|
|
29
|
+
/** Called on 401/403 to attempt token refresh. Returns updated headers or null. */
|
|
30
|
+
onAuthError?: () => Promise<Record<string, string> | null>;
|
|
29
31
|
|
|
30
32
|
constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
|
|
31
33
|
|
|
@@ -102,6 +104,27 @@ export class HttpTransport implements MCPTransport {
|
|
|
102
104
|
method: string,
|
|
103
105
|
params?: Record<string, unknown>,
|
|
104
106
|
options?: MCPRequestOptions,
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
try {
|
|
109
|
+
return await this.#executeRequest<T>(method, params, options);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Retry once on auth failure if onAuthError is wired
|
|
112
|
+
if (this.onAuthError && error instanceof Error && /^HTTP (401|403):/.test(error.message)) {
|
|
113
|
+
const newHeaders = await this.onAuthError();
|
|
114
|
+
if (newHeaders) {
|
|
115
|
+
// Persist refreshed headers so subsequent requests use them directly
|
|
116
|
+
this.config = { ...this.config, headers: newHeaders };
|
|
117
|
+
return this.#executeRequest<T>(method, params, options);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async #executeRequest<T>(
|
|
125
|
+
method: string,
|
|
126
|
+
params: Record<string, unknown> | undefined,
|
|
127
|
+
options: MCPRequestOptions | undefined,
|
|
105
128
|
): Promise<T> {
|
|
106
129
|
if (!this.#connected) {
|
|
107
130
|
throw new Error("Transport not connected");
|
package/src/mcp/types.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface MCPAuthConfig {
|
|
|
49
49
|
type: "oauth" | "apikey";
|
|
50
50
|
/** Credential ID for OAuth (references agent.db) */
|
|
51
51
|
credentialId?: string;
|
|
52
|
+
/** Token endpoint URL — persisted for proactive token refresh */
|
|
53
|
+
tokenUrl?: string;
|
|
54
|
+
/** Client ID — persisted for token refresh */
|
|
55
|
+
clientId?: string;
|
|
56
|
+
/** Client secret — persisted for token refresh */
|
|
57
|
+
clientSecret?: string;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** Base server config with shared options */
|
|
@@ -1030,6 +1030,9 @@ export class MCPAddWizard extends Container {
|
|
|
1030
1030
|
config.auth = {
|
|
1031
1031
|
type: "oauth",
|
|
1032
1032
|
credentialId: this.#state.oauthCredentialId,
|
|
1033
|
+
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1034
|
+
clientId: this.#state.oauthClientId || undefined,
|
|
1035
|
+
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1033
1036
|
};
|
|
1034
1037
|
}
|
|
1035
1038
|
|
|
@@ -1054,6 +1057,9 @@ export class MCPAddWizard extends Container {
|
|
|
1054
1057
|
config.auth = {
|
|
1055
1058
|
type: "oauth",
|
|
1056
1059
|
credentialId: this.#state.oauthCredentialId,
|
|
1060
|
+
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1061
|
+
clientId: this.#state.oauthClientId || undefined,
|
|
1062
|
+
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1057
1063
|
};
|
|
1058
1064
|
}
|
|
1059
1065
|
|
|
@@ -1250,6 +1256,9 @@ export class MCPAddWizard extends Container {
|
|
|
1250
1256
|
config.auth = {
|
|
1251
1257
|
type: "oauth",
|
|
1252
1258
|
credentialId: this.#state.oauthCredentialId,
|
|
1259
|
+
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1260
|
+
clientId: this.#state.oauthClientId || undefined,
|
|
1261
|
+
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1253
1262
|
};
|
|
1254
1263
|
}
|
|
1255
1264
|
|
|
@@ -1275,6 +1284,9 @@ export class MCPAddWizard extends Container {
|
|
|
1275
1284
|
config.auth = {
|
|
1276
1285
|
type: "oauth",
|
|
1277
1286
|
credentialId: this.#state.oauthCredentialId,
|
|
1287
|
+
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1288
|
+
clientId: this.#state.oauthClientId || undefined,
|
|
1289
|
+
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1278
1290
|
};
|
|
1279
1291
|
}
|
|
1280
1292
|
|
|
@@ -226,7 +226,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
226
226
|
label: "Auto",
|
|
227
227
|
description: "Preferred web-search provider",
|
|
228
228
|
},
|
|
229
|
-
{ value: "exa", label: "Exa", description: "
|
|
229
|
+
{ value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set; falls back to Exa MCP" },
|
|
230
230
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
231
231
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
232
232
|
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
@@ -413,6 +413,9 @@ export class MCPCommandController {
|
|
|
413
413
|
auth: {
|
|
414
414
|
type: "oauth",
|
|
415
415
|
credentialId,
|
|
416
|
+
tokenUrl: oauth.tokenUrl,
|
|
417
|
+
clientId: oauth.clientId ?? finalConfig.oauth?.clientId,
|
|
418
|
+
clientSecret: undefined,
|
|
416
419
|
},
|
|
417
420
|
};
|
|
418
421
|
} catch (oauthError) {
|
|
@@ -1296,7 +1299,9 @@ export class MCPCommandController {
|
|
|
1296
1299
|
}
|
|
1297
1300
|
|
|
1298
1301
|
const currentAuth = (
|
|
1299
|
-
found.config as MCPServerConfig & {
|
|
1302
|
+
found.config as MCPServerConfig & {
|
|
1303
|
+
auth?: { type: "oauth" | "apikey"; credentialId?: string; clientSecret?: string };
|
|
1304
|
+
}
|
|
1300
1305
|
).auth;
|
|
1301
1306
|
if (currentAuth?.type === "oauth") {
|
|
1302
1307
|
await this.#removeManagedOAuthCredential(currentAuth.credentialId);
|
|
@@ -1321,6 +1326,9 @@ export class MCPCommandController {
|
|
|
1321
1326
|
auth: {
|
|
1322
1327
|
type: "oauth",
|
|
1323
1328
|
credentialId,
|
|
1329
|
+
tokenUrl: oauth.tokenUrl,
|
|
1330
|
+
clientId: oauth.clientId ?? found.config.oauth?.clientId,
|
|
1331
|
+
clientSecret: currentAuth?.clientSecret,
|
|
1324
1332
|
},
|
|
1325
1333
|
};
|
|
1326
1334
|
await updateMCPServer(found.filePath, name, updated);
|
|
@@ -629,15 +629,11 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
|
629
629
|
const terminalId = getTerminalId();
|
|
630
630
|
if (!terminalId) return;
|
|
631
631
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
void Bun.write(breadcrumbFile, content);
|
|
638
|
-
} catch {
|
|
639
|
-
// Best-effort — don't break session creation if breadcrumb fails
|
|
640
|
-
}
|
|
632
|
+
const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
|
|
633
|
+
const breadcrumbFile = path.join(breadcrumbDir, terminalId);
|
|
634
|
+
const content = `${cwd}\n${sessionFile}\n`;
|
|
635
|
+
// Best-effort — don't break session creation if breadcrumb fails
|
|
636
|
+
Bun.write(breadcrumbFile, content).catch(() => {});
|
|
641
637
|
}
|
|
642
638
|
|
|
643
639
|
/**
|
package/src/tools/json-tree.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
|
|
|
14
14
|
export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
|
|
15
15
|
|
|
16
16
|
/** Keys injected by the harness that should not be displayed to users */
|
|
17
|
-
const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD]);
|
|
17
|
+
const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]);
|
|
18
18
|
|
|
19
19
|
/** Strip harness-internal keys from tool args for display */
|
|
20
20
|
export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
|