@oh-my-pi/pi-coding-agent 13.9.16 → 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 +14 -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/prompts/tools/hashline.md +29 -48
- package/src/session/compaction/compaction.ts +4 -1
- package/src/session/session-manager.ts +5 -9
- package/src/tools/json-tree.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
14
|
+
## [13.10.0] - 2026-03-10
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Preserved text signature metadata (id and phase) when building OpenAI native history during session compaction
|
|
18
|
+
|
|
5
19
|
## [13.9.16] - 2026-03-10
|
|
6
20
|
### Breaking Changes
|
|
7
21
|
|
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.
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
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);
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift.
|
|
2
2
|
|
|
3
|
-
<critical>
|
|
4
|
-
- Never anchor insertions on blank lines or lone closing delimiters like `}`, `]`, `)`, `};`, or `),` — they are mechanically valid tags but semantically unstable edit boundaries.
|
|
5
|
-
- For `append`/`prepend`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding braces, brackets, parentheses, or sibling declarations that already exist in the file.
|
|
6
|
-
- `append`/`prepend` are for self-contained new content only: sibling declarations, new object/list members, new test cases, or similar additions whose surrounding structure stays unchanged.
|
|
7
|
-
- When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
|
|
8
|
-
- When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
|
|
9
|
-
- If any inserted line is just a closing delimiter, stop and re-check the edit shape. A closing line is only valid when it belongs to newly introduced structure; if it belongs to surrounding existing structure, your edit should be a `replace` that consumes the old boundary.
|
|
10
|
-
</critical>
|
|
11
|
-
|
|
12
3
|
<workflow>
|
|
13
4
|
Follow these steps in order for every edit:
|
|
14
5
|
1. You **SHOULD** issue a `read` call before editing to get fresh `LINE#ID` tags. Editing without current tags causes mismatches because other edits or external changes may have shifted line numbers since your last read.
|
|
@@ -21,15 +12,14 @@ Before choosing the payload, answer these questions in order:
|
|
|
21
12
|
1. **Am I replacing existing lines or inserting new ones?** If any existing line changes, use `replace` for the full changed span.
|
|
22
13
|
2. **What declaration or block owns this anchor line?** Prefer declaration/header lines over blank lines or delimiters.
|
|
23
14
|
3. **Am I inserting self-contained new content, or changing an existing block?** Use `append`/`prepend` only for self-contained additions. If surrounding code, indentation, or closers also change, use `replace`.
|
|
24
|
-
4. **Am I editing near a block tail or closing delimiter?** If yes,
|
|
25
|
-
5. **Does `lines` contain only new content?** For `append`/`prepend`, do not include existing closing braces or other surrounding syntax from the file.
|
|
26
|
-
6. **Would the replacement duplicate the line immediately after `end`?** If yes, extend the range to consume the old boundary.
|
|
15
|
+
4. **Am I editing near a block tail or closing delimiter?** If yes, use shape (a) or (b) from the block-boundaries rule: either stay entirely inside the body, or own the full block including header and closer. Never set `end` at a closer without re-emitting it, and never re-emit a closer without including it in `end`.
|
|
27
16
|
</checklist>
|
|
28
17
|
|
|
29
18
|
<operations>
|
|
30
19
|
**`path`** — the path to the file to edit.
|
|
31
20
|
**`move`** — if set, move the file to the given path.
|
|
32
21
|
**`delete`** — if true, delete the file.
|
|
22
|
+
|
|
33
23
|
**`edits[n].pos`** — the anchor line. Meaning depends on `op`:
|
|
34
24
|
- if `replace`: line to rewrite
|
|
35
25
|
- if `prepend`: line to insert new lines **before**; omit for beginning of file
|
|
@@ -40,24 +30,16 @@ Before choosing the payload, answer these questions in order:
|
|
|
40
30
|
- `[""]` — blank line
|
|
41
31
|
- `null` or `[]` — delete if replace, no-op if append or prepend
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
Ops are applied bottom-up. Tags **MUST** be referenced from the most recent `read` output.
|
|
44
34
|
</operations>
|
|
45
35
|
|
|
46
36
|
<rules>
|
|
47
|
-
1. **
|
|
48
|
-
2. **
|
|
49
|
-
3. **
|
|
50
|
-
4. **
|
|
51
|
-
5. **Consume the old closing boundary when your replacement emits one.** If the replacement's final line is a closing delimiter like `}`, `]`, or `)`, the `end` line **MUST** include the original matching closer that would otherwise remain in the file. Before submitting, compare the replacement's last line with the line immediately after `end`; if they would be the same boundary, extend the range so the old closer is removed.
|
|
52
|
-
6. **If you expect a second tiny cleanup edit for `}`, `};`, indentation, or a duplicated boundary, your first edit shape is wrong.** Expand the first `replace` so it owns the structural tail in one shot.
|
|
37
|
+
1. **Use `prepend`/`append` only for self-contained additions whose surrounding structure stays unchanged.** If you are adding a sibling declaration, prefer `prepend` on the next sibling declaration instead of `append` on the previous block closer.
|
|
38
|
+
2. **If the change touches existing code near a block tail, use range `replace` over the owned span.** Do not patch just the final line(s) before a closing delimiter when the surrounding structure, indentation, or control flow is also changing.
|
|
39
|
+
3. **Match surrounding indentation for new lines.** When inserting via `prepend`/`append`, look at the anchor line and its neighbors in the `read` output. New `lines` entries **MUST** carry the same leading whitespace. If the context uses tabs at depth 1 (`\t`), your inserted declarations need `\t` and bodies need `\t\t`. Inserting at indent level 0 inside an indented block is always wrong.
|
|
40
|
+
4. **Block boundaries travel together — never split them.** See the block-boundaries rule in `<critical>`. The two valid shapes are: replace only the body (leave header and closer untouched), or replace the whole block (header through closer, re-emit all in `lines`). Do not set `end` to a closer and omit it from `lines` (deletes it). Do not emit a closer in `lines` without including it in `end` (duplicates it).
|
|
53
41
|
</rules>
|
|
54
42
|
|
|
55
|
-
<recovery>
|
|
56
|
-
Edits can fail in two ways. Here is exactly what to do for each:
|
|
57
|
-
1. **Tag mismatch (`>>>`):** The file changed since your last read, so the tag no longer matches. You **MUST** retry using the fresh tags from the error snippet. If the snippet lacks enough context, or if you fail repeatedly, you **MUST** re-read the entire file and submit a simpler, single-op edit.
|
|
58
|
-
2. **No-op (`identical`):** Your replacement is identical to the existing content — nothing changed. You **MUST NOT** resubmit the same edit. Re-read the target lines to understand what is actually there, then adjust your edit.
|
|
59
|
-
</recovery>
|
|
60
|
-
|
|
61
43
|
<examples>
|
|
62
44
|
All examples below reference the same file, `util.ts`:
|
|
63
45
|
```ts
|
|
@@ -135,63 +117,61 @@ Blank out a line without removing it:
|
|
|
135
117
|
```
|
|
136
118
|
</example>
|
|
137
119
|
|
|
138
|
-
<example name="rewrite a block">
|
|
139
|
-
Replace the catch body with smarter error handling:
|
|
120
|
+
<example name="rewrite a block body — shape (a)">
|
|
121
|
+
Replace the catch body with smarter error handling. Shape (a): `pos` is the first body line, `end` is the last body line. The catch header (line 14) and its closer (line 17) are outside the range and stay untouched.
|
|
140
122
|
```
|
|
141
123
|
{
|
|
142
124
|
path: "util.ts",
|
|
143
125
|
edits: [{
|
|
144
126
|
op: "replace",
|
|
145
127
|
pos: {{hlineref 15 "\t\tconsole.error(err);"}},
|
|
146
|
-
end: {{hlineref
|
|
128
|
+
end: {{hlineref 16 "\t\treturn null;"}},
|
|
147
129
|
lines: [
|
|
148
130
|
"\t\tif (isEnoent(err)) return null;",
|
|
149
|
-
"\t\tthrow err;"
|
|
150
|
-
"\t}"
|
|
131
|
+
"\t\tthrow err;"
|
|
151
132
|
]
|
|
152
133
|
}]
|
|
153
134
|
}
|
|
154
135
|
```
|
|
155
136
|
</example>
|
|
156
137
|
|
|
157
|
-
<example name="
|
|
158
|
-
When changing the
|
|
138
|
+
<example name="span the full body, not a single line">
|
|
139
|
+
When changing body content, replace the entire body span — not just one line inside it. Patching one line leaves the rest of the body stale.
|
|
159
140
|
|
|
160
|
-
Bad —
|
|
141
|
+
Bad — appends after one body line, leaving the original `return null` in place:
|
|
161
142
|
```
|
|
162
143
|
{
|
|
163
144
|
path: "util.ts",
|
|
164
145
|
edits: [{
|
|
165
146
|
op: "append",
|
|
166
|
-
pos: {{hlineref
|
|
147
|
+
pos: {{hlineref 15 "\t\tconsole.error(err);"}},
|
|
167
148
|
lines: [
|
|
168
149
|
"\t\treturn fallback;"
|
|
169
150
|
]
|
|
170
151
|
}]
|
|
171
152
|
}
|
|
172
153
|
```
|
|
173
|
-
Good — replace the
|
|
154
|
+
Good — shape (a): replace the full body span. Header and closer stay untouched:
|
|
174
155
|
```
|
|
175
156
|
{
|
|
176
157
|
path: "util.ts",
|
|
177
158
|
edits: [{
|
|
178
159
|
op: "replace",
|
|
179
160
|
pos: {{hlineref 15 "\t\tconsole.error(err);"}},
|
|
180
|
-
end: {{hlineref
|
|
161
|
+
end: {{hlineref 16 "\t\treturn null;"}},
|
|
181
162
|
lines: [
|
|
182
163
|
"\t\tif (isEnoent(err)) return null;",
|
|
183
|
-
"\t\treturn fallback;"
|
|
184
|
-
"\t}"
|
|
164
|
+
"\t\treturn fallback;"
|
|
185
165
|
]
|
|
186
166
|
}]
|
|
187
167
|
}
|
|
188
168
|
```
|
|
189
169
|
</example>
|
|
190
170
|
|
|
191
|
-
<example name="
|
|
192
|
-
Simplify `beta()` to a one-liner.
|
|
171
|
+
<example name="replace whole block — shape (b)">
|
|
172
|
+
Simplify `beta()` to a one-liner. Shape (b): `pos`=header, `end`=closer, re-emit all in `lines`.
|
|
193
173
|
|
|
194
|
-
Bad — `end` stops at
|
|
174
|
+
Bad — `end` stops at the inner `\t}` on line 17, so the outer `}` on line 18 survives. Result: two consecutive `}` lines.
|
|
195
175
|
```
|
|
196
176
|
{
|
|
197
177
|
path: "util.ts",
|
|
@@ -207,7 +187,7 @@ Bad — `end` stops at line 17 (`\t}`), so the replacement adds `}` and the orig
|
|
|
207
187
|
}]
|
|
208
188
|
}
|
|
209
189
|
```
|
|
210
|
-
Good —
|
|
190
|
+
Good — `end` includes the function's own `}` on line 18, so the old closer is consumed:
|
|
211
191
|
```
|
|
212
192
|
{
|
|
213
193
|
path: "util.ts",
|
|
@@ -284,11 +264,12 @@ Good — prepend before the next declaration so the new sibling is anchored on a
|
|
|
284
264
|
</examples>
|
|
285
265
|
|
|
286
266
|
<critical>
|
|
287
|
-
-
|
|
267
|
+
- You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead.
|
|
288
268
|
- Every tag **MUST** be copied exactly from your most recent `read` output as `N#ID`. Stale or mistyped tags cause mismatches.
|
|
289
|
-
-
|
|
290
|
-
-
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
269
|
+
- Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
|
|
270
|
+
- For `append`/`prepend`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding content, or terminators that already exist.
|
|
271
|
+
- When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
|
|
272
|
+
- When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
|
|
273
|
+
- **Block boundaries travel together.** For a block `{ header / body / closer }`, there are exactly two valid replace shapes: (a) replace only the body — `pos`=first body line, `end`=last body line, leave the header and closer untouched; or (b) replace the whole block — `pos`=header, `end`=closer, re-emit all three in `lines`. Never split them: do not set `end` to the closer while omitting it from `lines` (deletes it), and do not emit the closer in `lines` without including it in `end` (duplicates it). This applies to every block terminator: `}`, `continue`, `break`, `return`, `throw`.
|
|
274
|
+
- `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use a real tab character.
|
|
294
275
|
</critical>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
OPENAI_HEADER_VALUES,
|
|
13
13
|
OPENAI_HEADERS,
|
|
14
14
|
} from "@oh-my-pi/pi-ai/providers/openai-codex/constants";
|
|
15
|
+
import { parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
|
|
15
16
|
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
16
17
|
import {
|
|
17
18
|
getOpenAIResponsesHistoryItems,
|
|
@@ -739,7 +740,8 @@ function buildOpenAiNativeHistory(
|
|
|
739
740
|
|
|
740
741
|
if (block.type === "text") {
|
|
741
742
|
if (!block.text || block.text.trim().length === 0) continue;
|
|
742
|
-
|
|
743
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
744
|
+
let msgId = parsedSignature?.id;
|
|
743
745
|
if (!msgId) {
|
|
744
746
|
msgId = `msg_${msgIndex}`;
|
|
745
747
|
} else if (msgId.length > 64) {
|
|
@@ -751,6 +753,7 @@ function buildOpenAiNativeHistory(
|
|
|
751
753
|
content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
|
|
752
754
|
status: "completed",
|
|
753
755
|
id: msgId,
|
|
756
|
+
phase: parsedSignature?.phase,
|
|
754
757
|
});
|
|
755
758
|
continue;
|
|
756
759
|
}
|
|
@@ -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> {
|