@howaboua/pi-codex-conversion 1.0.16 → 1.0.18

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/README.md CHANGED
@@ -8,6 +8,7 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
8
8
  - preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
9
9
  - renders exec activity with Codex-style command and background-terminal labels
10
10
  - renders `apply_patch` calls with Codex-style `Added` / `Edited` / `Deleted` diff blocks and Pi-style colored diff lines
11
+ - targets modern Pi tool/rendering APIs and is aligned with Pi `0.67.x`
11
12
 
12
13
  ![Available tools](./available-tools.png)
13
14
 
@@ -139,6 +140,7 @@ That keeps the prompt much closer to `pi-mono` while still steering the model to
139
140
  - `web_search` is exposed only for the `openai-codex` provider and is forwarded as the native OpenAI Codex Responses web search tool.
140
141
  - `apply_patch` paths stay restricted to the current working directory.
141
142
  - partial `apply_patch` failures stay in the original patch block and highlight the failed entry instead of adding a second warning row.
143
+ - `apply_patch` uses Pi's self-rendered tool shell mode for more stable large patch previews on current Pi versions.
142
144
  - `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
143
145
  - tiny `exec_command` waits are clamped for non-interactive commands so short runs do not burn an avoidable follow-up tool call.
144
146
  - empty `write_stdin` polls are clamped to a meaningful minimum wait so long-running processes are not repolled too aggressively.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -51,11 +51,13 @@
51
51
  "access": "public"
52
52
  },
53
53
  "peerDependencies": {
54
- "@mariozechner/pi-coding-agent": "^0.62.0",
55
- "@mariozechner/pi-tui": "^0.62.0",
54
+ "@mariozechner/pi-coding-agent": "^0.67.0",
55
+ "@mariozechner/pi-tui": "^0.67.0",
56
56
  "@sinclair/typebox": "*"
57
57
  },
58
58
  "devDependencies": {
59
+ "@mariozechner/pi-coding-agent": "^0.67.3",
60
+ "@mariozechner/pi-tui": "^0.67.3",
59
61
  "tsx": "^4.20.5",
60
62
  "typescript": "^5.9.3"
61
63
  },
package/src/patch/core.ts CHANGED
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
3
3
  import { linesMatch } from "./matching.ts";
4
4
  import { parsePatchActions, parseUpdateFile } from "./parser.ts";
5
5
  import { openFileAtPath, pathExists, removeFileAtPath, resolvePatchPath, writeFileAtPath } from "./paths.ts";
6
- import { DiffError, ExecutePatchError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
6
+ import { DiffError, ExecutePatchError, type ExecutePatchFailure, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
7
7
 
8
8
  export const patchFsOps = {
9
9
  mkdirSync: fs.mkdirSync,
@@ -218,6 +218,14 @@ function applyAction({
218
218
  return fuzz;
219
219
  }
220
220
 
221
+ function getActionPaths(action: ParsedPatchAction): string[] {
222
+ return [action.path, action.type === "update" ? action.movePath : undefined].filter((path): path is string => typeof path === "string");
223
+ }
224
+
225
+ function getCanonicalActionPaths({ cwd, action }: { cwd: string; action: ParsedPatchAction }): string[] {
226
+ return getActionPaths(action).map((path) => resolvePatchPath({ cwd, patchPath: path }));
227
+ }
228
+
221
229
  export function executePatch({ cwd, patchText }: { cwd: string; patchText: string }): ExecutePatchResult {
222
230
  if (!patchText.startsWith("*** Begin Patch")) {
223
231
  throw new DiffError("Patch must start with '*** Begin Patch'");
@@ -228,9 +236,22 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
228
236
  const createdFiles = new Set<string>();
229
237
  const deletedFiles = new Set<string>();
230
238
  const movedFiles = new Set<string>();
239
+ const blockedPaths = new Set<string>();
240
+ const failures: ExecutePatchFailure[] = [];
231
241
  let fuzz = 0;
232
242
 
233
243
  for (const action of actions) {
244
+ const actionPaths = getActionPaths(action);
245
+ const canonicalActionPaths = getCanonicalActionPaths({ cwd, action });
246
+ const overlappingPaths = canonicalActionPaths.filter((path) => blockedPaths.has(path));
247
+ if (overlappingPaths.length > 0) {
248
+ failures.push({
249
+ action,
250
+ message: `Skipped because an earlier failed action affected ${actionPaths.filter((_, index) => overlappingPaths.includes(canonicalActionPaths[index])).join(", ")}`,
251
+ });
252
+ continue;
253
+ }
254
+
234
255
  try {
235
256
  fuzz += applyAction({
236
257
  cwd,
@@ -242,20 +263,31 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
242
263
  });
243
264
  } catch (error) {
244
265
  const message = error instanceof Error ? error.message : String(error);
245
- throw new ExecutePatchError(
246
- message,
247
- buildExecutePatchResult({
248
- changedFiles,
249
- createdFiles,
250
- deletedFiles,
251
- movedFiles,
252
- fuzz,
253
- }),
254
- action,
255
- );
266
+ for (const path of canonicalActionPaths) {
267
+ blockedPaths.add(path);
268
+ }
269
+ failures.push({ action, message });
256
270
  }
257
271
  }
258
272
 
273
+ if (failures.length > 0) {
274
+ const message =
275
+ failures.length === 1
276
+ ? failures[0].message
277
+ : failures.map(({ action, message: failureMessage }) => `${action.path}: ${failureMessage}`).join("\n");
278
+ throw new ExecutePatchError(
279
+ message,
280
+ buildExecutePatchResult({
281
+ changedFiles,
282
+ createdFiles,
283
+ deletedFiles,
284
+ movedFiles,
285
+ fuzz,
286
+ }),
287
+ failures,
288
+ );
289
+ }
290
+
259
291
  return buildExecutePatchResult({
260
292
  changedFiles,
261
293
  createdFiles,
@@ -36,6 +36,11 @@ export interface ExecutePatchResult {
36
36
  fuzz: number;
37
37
  }
38
38
 
39
+ export interface ExecutePatchFailure {
40
+ action: ParsedPatchAction;
41
+ message: string;
42
+ }
43
+
39
44
  export class DiffError extends Error {
40
45
  constructor(message: string) {
41
46
  super(message);
@@ -46,12 +51,14 @@ export class DiffError extends Error {
46
51
  export class ExecutePatchError extends DiffError {
47
52
  result: ExecutePatchResult;
48
53
  failedAction?: ParsedPatchAction;
54
+ failures: ExecutePatchFailure[];
49
55
 
50
- constructor(message: string, result: ExecutePatchResult, failedAction?: ParsedPatchAction) {
56
+ constructor(message: string, result: ExecutePatchResult, failures: ExecutePatchFailure[] = []) {
51
57
  super(message);
52
58
  this.name = "ExecutePatchError";
53
59
  this.result = result;
54
- this.failedAction = failedAction;
60
+ this.failures = failures;
61
+ this.failedAction = failures[0]?.action;
55
62
  }
56
63
 
57
64
  hasPartialSuccess(): boolean {
@@ -17,7 +17,7 @@ interface ApplyPatchRenderState {
17
17
  collapsed: string;
18
18
  expanded: string;
19
19
  status: "pending" | "partial_failure" | "failed";
20
- failedTarget?: string;
20
+ failedTargets?: string[];
21
21
  }
22
22
 
23
23
  interface ApplyPatchSuccessDetails {
@@ -29,7 +29,7 @@ interface ApplyPatchPartialFailureDetails {
29
29
  status: "partial_failure";
30
30
  result: ExecutePatchResult;
31
31
  error: string;
32
- failedTarget?: string;
32
+ failedTargets?: string[];
33
33
  appliedFiles: string[];
34
34
  failedFiles: string[];
35
35
  recoveryInstructions: {
@@ -56,6 +56,21 @@ function parseApplyPatchParams(params: unknown): { patchText: string } {
56
56
  return { patchText: params.input };
57
57
  }
58
58
 
59
+ function prepareApplyPatchArguments(args: unknown): { input: string } {
60
+ if (args && typeof args === "object") {
61
+ if ("input" in args && typeof args.input === "string") {
62
+ return { input: args.input };
63
+ }
64
+ if ("patchText" in args && typeof args.patchText === "string") {
65
+ return { input: args.patchText };
66
+ }
67
+ if ("patch" in args && typeof args.patch === "string") {
68
+ return { input: args.patch };
69
+ }
70
+ }
71
+ return args as { input: string };
72
+ }
73
+
59
74
  function isApplyPatchToolDetails(details: unknown): details is ApplyPatchToolDetails {
60
75
  return typeof details === "object" && details !== null && "status" in details && "result" in details;
61
76
  }
@@ -65,7 +80,7 @@ function setApplyPatchRenderState(
65
80
  patchText: string,
66
81
  cwd: string,
67
82
  status: "pending" | "partial_failure" | "failed" = "pending",
68
- failedTarget?: string,
83
+ failedTargets?: string[],
69
84
  ): void {
70
85
  const collapsed = formatApplyPatchSummary(patchText, cwd);
71
86
  const expanded = renderApplyPatchCall(patchText, cwd);
@@ -75,15 +90,15 @@ function setApplyPatchRenderState(
75
90
  collapsed,
76
91
  expanded,
77
92
  status,
78
- failedTarget,
93
+ failedTargets,
79
94
  });
80
95
  }
81
96
 
82
- function markApplyPatchPartialFailure(toolCallId: string, failedTarget?: string): void {
83
- markApplyPatchFailure(toolCallId, "partial_failure", failedTarget);
97
+ function markApplyPatchPartialFailure(toolCallId: string, failedTargets?: string[]): void {
98
+ markApplyPatchFailure(toolCallId, "partial_failure", failedTargets);
84
99
  }
85
100
 
86
- function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTarget?: string): void {
101
+ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTargets?: string[]): void {
87
102
  const existing = applyPatchRenderStates.get(toolCallId);
88
103
  if (!existing) {
89
104
  return;
@@ -91,14 +106,14 @@ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "
91
106
  applyPatchRenderStates.set(toolCallId, {
92
107
  ...existing,
93
108
  status,
94
- failedTarget,
109
+ failedTargets,
95
110
  });
96
111
  }
97
112
 
98
113
  function renderPartialFailureCall(
99
114
  text: string,
100
115
  theme: { fg(role: string, text: string): string },
101
- failedTarget?: string,
116
+ failedTargets?: string[],
102
117
  ): string {
103
118
  const lines = text.split("\n");
104
119
  if (lines.length === 0) {
@@ -106,12 +121,15 @@ function renderPartialFailureCall(
106
121
  }
107
122
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit partially failed");
108
123
  const failedLineIndexes = new Set<number>();
109
- if (failedTarget) {
124
+ if (failedTargets) {
110
125
  for (let i = 0; i < lines.length; i += 1) {
111
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
112
- if (failedLine) {
113
- lines[i] = failedLine;
114
- failedLineIndexes.add(i);
126
+ for (const failedTarget of failedTargets) {
127
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
128
+ if (failedLine) {
129
+ lines[i] = failedLine;
130
+ failedLineIndexes.add(i);
131
+ break;
132
+ }
115
133
  }
116
134
  }
117
135
  }
@@ -131,7 +149,7 @@ function renderPartialFailureCall(
131
149
  function renderFailedCall(
132
150
  text: string,
133
151
  theme: { fg(role: string, text: string): string },
134
- failedTarget?: string,
152
+ failedTargets?: string[],
135
153
  ): string {
136
154
  const lines = text.split("\n");
137
155
  if (lines.length === 0) {
@@ -139,12 +157,15 @@ function renderFailedCall(
139
157
  }
140
158
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit failed");
141
159
  const failedLineIndexes = new Set<number>();
142
- if (failedTarget) {
160
+ if (failedTargets) {
143
161
  for (let i = 0; i < lines.length; i += 1) {
144
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
145
- if (failedLine) {
146
- lines[i] = failedLine;
147
- failedLineIndexes.add(i);
162
+ for (const failedTarget of failedTargets) {
163
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
164
+ if (failedLine) {
165
+ lines[i] = failedLine;
166
+ failedLineIndexes.add(i);
167
+ break;
168
+ }
148
169
  }
149
170
  }
150
171
  }
@@ -188,13 +209,9 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
188
209
  }
189
210
 
190
211
  function getFailedPaths(error: ExecutePatchError): string[] {
191
- if (!error.failedAction) {
192
- return [];
193
- }
194
- return uniqueStrings([
195
- error.failedAction.path,
196
- error.failedAction.type === "update" ? error.failedAction.movePath : undefined,
197
- ]);
212
+ return uniqueStrings(
213
+ error.failures.flatMap(({ action }) => [action.path, action.type === "update" ? action.movePath : undefined]),
214
+ );
198
215
  }
199
216
 
200
217
  function getAppliedPaths(result: ExecutePatchResult, failedFiles: string[]): string[] {
@@ -214,11 +231,10 @@ function buildPartialFailureMessage(message: string, failedFiles: string[], appl
214
231
  return lines.join("\n");
215
232
  }
216
233
 
217
- function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
218
- if (!error.failedAction) {
219
- return undefined;
220
- }
221
- return formatPatchTarget(error.failedAction.path, error.failedAction.type === "update" ? error.failedAction.movePath : undefined, cwd);
234
+ function describeFailedActions(error: ExecutePatchError, cwd: string): string[] {
235
+ return uniqueStrings(
236
+ error.failures.map(({ action }) => formatPatchTarget(action.path, action.type === "update" ? action.movePath : undefined, cwd)),
237
+ );
222
238
  }
223
239
 
224
240
  export type { ExecutePatchResult } from "../patch/types.ts";
@@ -253,9 +269,9 @@ const renderApplyPatchCallWithOptionalContext: any = (
253
269
  }
254
270
  const text =
255
271
  cached?.status === "partial_failure"
256
- ? renderPartialFailureCall(baseText, theme, cached.failedTarget)
272
+ ? renderPartialFailureCall(baseText, theme, cached.failedTargets)
257
273
  : cached?.status === "failed"
258
- ? renderFailedCall(baseText, theme, cached.failedTarget)
274
+ ? renderFailedCall(baseText, theme, cached.failedTargets)
259
275
  : baseText;
260
276
  return new Text(text, 0, 0);
261
277
  };
@@ -271,6 +287,8 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
271
287
  "When one task needs coordinated edits across multiple files, send them in a single apply_patch call when one coherent patch will do.",
272
288
  ],
273
289
  parameters: APPLY_PATCH_PARAMETERS,
290
+ renderShell: "self",
291
+ prepareArguments: prepareApplyPatchArguments,
274
292
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
275
293
  if (signal?.aborted) {
276
294
  throw new Error("apply_patch aborted");
@@ -284,23 +302,24 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
284
302
  } catch (error) {
285
303
  if (error instanceof ExecutePatchError) {
286
304
  const partial = error.hasPartialSuccess();
287
- const failedTarget = describeFailedAction(error, ctx.cwd);
305
+ const failedTargets = describeFailedActions(error, ctx.cwd);
306
+ const failedTargetSummary = failedTargets.join(", ");
288
307
  const prefix = partial
289
308
  ? `apply_patch partially failed after ${summarizePatchCounts(error.result)}`
290
309
  : "apply_patch failed";
291
- const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
310
+ const message = failedTargetSummary ? `${prefix} while patching ${failedTargetSummary}: ${error.message}` : `${prefix}: ${error.message}`;
292
311
  if (partial) {
293
312
  const failedFiles = getFailedPaths(error);
294
313
  const appliedFiles = getAppliedPaths(error.result, failedFiles);
295
314
  const recoveryMessage = buildPartialFailureMessage(message, failedFiles, appliedFiles);
296
- markApplyPatchPartialFailure(toolCallId, failedTarget);
315
+ markApplyPatchPartialFailure(toolCallId, failedTargets);
297
316
  return {
298
317
  content: [{ type: "text", text: recoveryMessage }],
299
318
  details: {
300
319
  status: "partial_failure",
301
320
  result: error.result,
302
321
  error: recoveryMessage,
303
- failedTarget,
322
+ failedTargets,
304
323
  appliedFiles,
305
324
  failedFiles,
306
325
  recoveryInstructions: {
@@ -310,7 +329,7 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
310
329
  } satisfies ApplyPatchPartialFailureDetails,
311
330
  };
312
331
  }
313
- markApplyPatchFailure(toolCallId, "failed", failedTarget);
332
+ markApplyPatchFailure(toolCallId, "failed", failedTargets);
314
333
  throw new Error(message);
315
334
  }
316
335
  markApplyPatchFailure(toolCallId, "failed");
@@ -30,6 +30,26 @@ interface ExecCommandParams {
30
30
  login?: boolean;
31
31
  }
32
32
 
33
+ function prepareExecCommandArguments(args: unknown): ExecCommandParams {
34
+ if (!args || typeof args !== "object") {
35
+ return args as ExecCommandParams;
36
+ }
37
+
38
+ const record = args as Record<string, unknown>;
39
+ const prepared: Record<string, unknown> = { ...record };
40
+ if (!("cmd" in prepared) && "command" in prepared) {
41
+ prepared.cmd = prepared.command;
42
+ }
43
+ if (!("workdir" in prepared)) {
44
+ if ("cwd" in prepared) {
45
+ prepared.workdir = prepared.cwd;
46
+ } else if ("working_directory" in prepared) {
47
+ prepared.workdir = prepared.working_directory;
48
+ }
49
+ }
50
+ return prepared as unknown as ExecCommandParams;
51
+ }
52
+
33
53
  function parseExecCommandParams(params: unknown): ExecCommandParams {
34
54
  if (!params || typeof params !== "object") {
35
55
  throw new Error("exec_command requires an object parameter");
@@ -125,6 +145,7 @@ export function registerExecCommandTool(pi: ExtensionAPI, tracker: ExecCommandTr
125
145
  "Keep tty disabled unless the command truly needs interactive terminal behavior.",
126
146
  ],
127
147
  parameters: EXEC_COMMAND_PARAMETERS,
148
+ prepareArguments: prepareExecCommandArguments,
128
149
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
129
150
  if (signal?.aborted) {
130
151
  throw new Error("exec_command aborted");
@@ -68,6 +68,23 @@ export function parseViewImageParams(params: unknown): ViewImageParams {
68
68
  return { path: params.path, detail };
69
69
  }
70
70
 
71
+ function prepareViewImageArguments(args: unknown): Record<string, unknown> {
72
+ if (!args || typeof args !== "object") {
73
+ return args as Record<string, unknown>;
74
+ }
75
+
76
+ const record = args as Record<string, unknown>;
77
+ const prepared: Record<string, unknown> = { ...record };
78
+ if (!("path" in prepared)) {
79
+ if ("file_path" in prepared) {
80
+ prepared.path = prepared.file_path;
81
+ } else if ("image_path" in prepared) {
82
+ prepared.path = prepared.image_path;
83
+ }
84
+ }
85
+ return prepared;
86
+ }
87
+
71
88
  function resolveViewImagePath(path: string, cwd: string): string {
72
89
  return isAbsolute(path) ? path : resolve(cwd, path);
73
90
  }
@@ -131,6 +148,7 @@ export function createViewImageTool(options: CreateViewImageToolOptions = {}): T
131
148
  promptSnippet: "View a local image from the filesystem.",
132
149
  promptGuidelines: ["Use view_image only for image files. Use exec_command for text-file inspection."],
133
150
  parameters,
151
+ prepareArguments: prepareViewImageArguments,
134
152
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
135
153
  if (!supportsImageInputs(ctx.model)) {
136
154
  throw new Error(VIEW_IMAGE_UNSUPPORTED_MESSAGE);
@@ -106,6 +106,7 @@ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETE
106
106
  promptSnippet:
107
107
  "Search the web for sources relevant to the current task. Use it when you need up-to-date information, external references, or broader context beyond the workspace.",
108
108
  parameters: WEB_SEARCH_PARAMETERS,
109
+ prepareArguments: () => ({}),
109
110
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
110
111
  if (!supportsNativeWebSearch(ctx.model)) {
111
112
  throw new Error(WEB_SEARCH_UNSUPPORTED_MESSAGE);