@howaboua/pi-codex-conversion 1.0.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/LICENSE +21 -0
- package/README.md +99 -0
- package/available-tools.png +0 -0
- package/package.json +62 -0
- package/src/adapter/codex-model.ts +22 -0
- package/src/adapter/tool-set.ts +7 -0
- package/src/index.ts +112 -0
- package/src/patch/core.ts +220 -0
- package/src/patch/parser.ts +422 -0
- package/src/patch/paths.ts +56 -0
- package/src/patch/types.ts +44 -0
- package/src/prompt/build-system-prompt.ts +111 -0
- package/src/shell/parse.ts +297 -0
- package/src/shell/summary.ts +62 -0
- package/src/shell/tokenize.ts +125 -0
- package/src/shell/types.ts +10 -0
- package/src/tools/apply-patch-tool.ts +84 -0
- package/src/tools/codex-rendering.ts +95 -0
- package/src/tools/exec-command-state.ts +43 -0
- package/src/tools/exec-command-tool.ts +107 -0
- package/src/tools/exec-session-manager.ts +478 -0
- package/src/tools/unified-exec-format.ts +28 -0
- package/src/tools/view-image-tool.ts +171 -0
- package/src/tools/write-stdin-tool.ts +145 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { normalizePatchPath } from "./paths.ts";
|
|
2
|
+
import { DiffError, type Chunk, type ParseMode, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
function parserIsDone({ state, prefixes }: { state: ParserState; prefixes?: string[] }): boolean {
|
|
5
|
+
if (state.index >= state.lines.length) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (prefixes && prefixes.some((prefix) => state.lines[state.index].startsWith(prefix))) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parserStartsWith({ state, prefix }: { state: ParserState; prefix: string }): boolean {
|
|
15
|
+
if (state.index >= state.lines.length) {
|
|
16
|
+
throw new DiffError(`Index: ${state.index} >= ${state.lines.length}`);
|
|
17
|
+
}
|
|
18
|
+
return state.lines[state.index].startsWith(prefix);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parserReadStr({
|
|
22
|
+
state,
|
|
23
|
+
prefix,
|
|
24
|
+
returnEverything,
|
|
25
|
+
}: {
|
|
26
|
+
state: ParserState;
|
|
27
|
+
prefix?: string;
|
|
28
|
+
returnEverything?: boolean;
|
|
29
|
+
}): string {
|
|
30
|
+
if (state.index >= state.lines.length) {
|
|
31
|
+
throw new DiffError(`Index: ${state.index} >= ${state.lines.length}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const expectedPrefix = prefix ?? "";
|
|
35
|
+
if (state.lines[state.index].startsWith(expectedPrefix)) {
|
|
36
|
+
const text = returnEverything ? state.lines[state.index] : state.lines[state.index].slice(expectedPrefix.length);
|
|
37
|
+
state.index += 1;
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitFileLines(text: string): string[] {
|
|
44
|
+
const lines = text.split("\n");
|
|
45
|
+
if (lines.at(-1) === "") {
|
|
46
|
+
lines.pop();
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function linesEqual({ left, right }: { left: string[]; right: string[] }): boolean {
|
|
52
|
+
if (left.length !== right.length) return false;
|
|
53
|
+
for (let index = 0; index < left.length; index++) {
|
|
54
|
+
if (left[index] !== right[index]) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findContextCore({ lines, context, start }: { lines: string[]; context: string[]; start: number }): {
|
|
60
|
+
newIndex: number;
|
|
61
|
+
fuzz: number;
|
|
62
|
+
} {
|
|
63
|
+
if (context.length === 0) {
|
|
64
|
+
return { newIndex: start, fuzz: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let index = start; index < lines.length; index++) {
|
|
68
|
+
if (linesEqual({ left: lines.slice(index, index + context.length), right: context })) {
|
|
69
|
+
return { newIndex: index, fuzz: 0 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let index = start; index < lines.length; index++) {
|
|
74
|
+
const left = lines.slice(index, index + context.length).map((line) => line.trimEnd());
|
|
75
|
+
const right = context.map((line) => line.trimEnd());
|
|
76
|
+
if (linesEqual({ left, right })) {
|
|
77
|
+
return { newIndex: index, fuzz: 1 };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let index = start; index < lines.length; index++) {
|
|
82
|
+
const left = lines.slice(index, index + context.length).map((line) => line.trim());
|
|
83
|
+
const right = context.map((line) => line.trim());
|
|
84
|
+
if (linesEqual({ left, right })) {
|
|
85
|
+
return { newIndex: index, fuzz: 100 };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { newIndex: -1, fuzz: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findContext({
|
|
93
|
+
lines,
|
|
94
|
+
context,
|
|
95
|
+
start,
|
|
96
|
+
eof,
|
|
97
|
+
}: {
|
|
98
|
+
lines: string[];
|
|
99
|
+
context: string[];
|
|
100
|
+
start: number;
|
|
101
|
+
eof: boolean;
|
|
102
|
+
}): { newIndex: number; fuzz: number } {
|
|
103
|
+
if (eof) {
|
|
104
|
+
const nearEnd = Math.max(lines.length - context.length, 0);
|
|
105
|
+
const preferred = findContextCore({ lines, context, start: nearEnd });
|
|
106
|
+
if (preferred.newIndex !== -1) {
|
|
107
|
+
return preferred;
|
|
108
|
+
}
|
|
109
|
+
const fallback = findContextCore({ lines, context, start });
|
|
110
|
+
return { newIndex: fallback.newIndex, fuzz: fallback.fuzz + 10000 };
|
|
111
|
+
}
|
|
112
|
+
return findContextCore({ lines, context, start });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function peekNextSection({ lines, index }: { lines: string[]; index: number }): {
|
|
116
|
+
nextChunkContext: string[];
|
|
117
|
+
chunks: Chunk[];
|
|
118
|
+
endPatchIndex: number;
|
|
119
|
+
eof: boolean;
|
|
120
|
+
} {
|
|
121
|
+
const old: string[] = [];
|
|
122
|
+
let delLines: string[] = [];
|
|
123
|
+
let insLines: string[] = [];
|
|
124
|
+
const chunks: Chunk[] = [];
|
|
125
|
+
let mode: ParseMode = "keep";
|
|
126
|
+
const origIndex = index;
|
|
127
|
+
|
|
128
|
+
while (index < lines.length) {
|
|
129
|
+
const rawLine = lines[index];
|
|
130
|
+
if (
|
|
131
|
+
rawLine.startsWith("@@") ||
|
|
132
|
+
rawLine.startsWith("*** End Patch") ||
|
|
133
|
+
rawLine.startsWith("*** Update File:") ||
|
|
134
|
+
rawLine.startsWith("*** Delete File:") ||
|
|
135
|
+
rawLine.startsWith("*** Add File:") ||
|
|
136
|
+
rawLine.startsWith("*** End of File")
|
|
137
|
+
) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (rawLine === "***") {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (rawLine.startsWith("***")) {
|
|
145
|
+
throw new DiffError(`Invalid Line: ${rawLine}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
index += 1;
|
|
149
|
+
const lastMode: ParseMode = mode;
|
|
150
|
+
let line = rawLine;
|
|
151
|
+
if (line === "") {
|
|
152
|
+
line = " ";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (line[0] === "+") {
|
|
156
|
+
mode = "add";
|
|
157
|
+
} else if (line[0] === "-") {
|
|
158
|
+
mode = "delete";
|
|
159
|
+
} else if (line[0] === " ") {
|
|
160
|
+
mode = "keep";
|
|
161
|
+
} else {
|
|
162
|
+
throw new DiffError(`Invalid Line: ${line}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const value = line.slice(1);
|
|
166
|
+
if (mode === "keep" && lastMode !== mode) {
|
|
167
|
+
if (insLines.length > 0 || delLines.length > 0) {
|
|
168
|
+
chunks.push({
|
|
169
|
+
origIndex: old.length - delLines.length,
|
|
170
|
+
delLines,
|
|
171
|
+
insLines,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
delLines = [];
|
|
175
|
+
insLines = [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (mode === "delete") {
|
|
179
|
+
delLines.push(value);
|
|
180
|
+
old.push(value);
|
|
181
|
+
} else if (mode === "add") {
|
|
182
|
+
insLines.push(value);
|
|
183
|
+
} else {
|
|
184
|
+
old.push(value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (insLines.length > 0 || delLines.length > 0) {
|
|
189
|
+
chunks.push({
|
|
190
|
+
origIndex: old.length - delLines.length,
|
|
191
|
+
delLines,
|
|
192
|
+
insLines,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (index < lines.length && lines[index] === "*** End of File") {
|
|
197
|
+
return {
|
|
198
|
+
nextChunkContext: old,
|
|
199
|
+
chunks,
|
|
200
|
+
endPatchIndex: index + 1,
|
|
201
|
+
eof: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (index === origIndex) {
|
|
206
|
+
throw new DiffError(`Nothing in this section - index=${index} ${lines[index] ?? ""}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
nextChunkContext: old,
|
|
211
|
+
chunks,
|
|
212
|
+
endPatchIndex: index,
|
|
213
|
+
eof: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseAddFile({ state }: { state: ParserState }): PatchAction {
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
while (
|
|
220
|
+
!parserIsDone({
|
|
221
|
+
state,
|
|
222
|
+
prefixes: ["*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:"],
|
|
223
|
+
})
|
|
224
|
+
) {
|
|
225
|
+
const value = parserReadStr({ state, prefix: "" });
|
|
226
|
+
if (!value.startsWith("+")) {
|
|
227
|
+
throw new DiffError(`Invalid Add File Line: ${value}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push(value.slice(1));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
type: "add",
|
|
234
|
+
newFile: lines.length === 0 ? "" : `${lines.join("\n")}\n`,
|
|
235
|
+
chunks: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function parseUpdateFile({ state, text, path }: { state: ParserState; text: string; path: string }): PatchAction {
|
|
240
|
+
const action: PatchAction = {
|
|
241
|
+
type: "update",
|
|
242
|
+
chunks: [],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const lines = splitFileLines(text);
|
|
246
|
+
let index = 0;
|
|
247
|
+
|
|
248
|
+
while (
|
|
249
|
+
!parserIsDone({
|
|
250
|
+
state,
|
|
251
|
+
prefixes: ["*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:", "*** End of File"],
|
|
252
|
+
})
|
|
253
|
+
) {
|
|
254
|
+
const defStr = parserReadStr({ state, prefix: "@@ " });
|
|
255
|
+
let sectionStr = "";
|
|
256
|
+
if (!defStr && state.index < state.lines.length && state.lines[state.index] === "@@") {
|
|
257
|
+
sectionStr = state.lines[state.index];
|
|
258
|
+
state.index += 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!(defStr || sectionStr || index === 0)) {
|
|
262
|
+
throw new DiffError(`Invalid Line:\n${state.lines[state.index]}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (defStr.trim().length > 0) {
|
|
266
|
+
let found = false;
|
|
267
|
+
|
|
268
|
+
const exactAlreadySeen = lines.slice(0, index).some((line) => line === defStr);
|
|
269
|
+
if (!exactAlreadySeen) {
|
|
270
|
+
for (let lineIndex = index; lineIndex < lines.length; lineIndex++) {
|
|
271
|
+
if (lines[lineIndex] === defStr) {
|
|
272
|
+
index = lineIndex + 1;
|
|
273
|
+
found = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!found) {
|
|
280
|
+
const trimAlreadySeen = lines.slice(0, index).some((line) => line.trim() === defStr.trim());
|
|
281
|
+
if (!trimAlreadySeen) {
|
|
282
|
+
for (let lineIndex = index; lineIndex < lines.length; lineIndex++) {
|
|
283
|
+
if (lines[lineIndex].trim() === defStr.trim()) {
|
|
284
|
+
index = lineIndex + 1;
|
|
285
|
+
state.fuzz += 1;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { nextChunkContext, chunks, endPatchIndex, eof } = peekNextSection({ lines: state.lines, index: state.index });
|
|
294
|
+
const nextChunkText = nextChunkContext.join("\n");
|
|
295
|
+
const { newIndex, fuzz } = findContext({
|
|
296
|
+
lines,
|
|
297
|
+
context: nextChunkContext,
|
|
298
|
+
start: index,
|
|
299
|
+
eof,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (newIndex === -1) {
|
|
303
|
+
throw new DiffError(`Failed to find expected lines in ${path}:\n${nextChunkText}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
state.fuzz += fuzz;
|
|
307
|
+
|
|
308
|
+
for (const chunk of chunks) {
|
|
309
|
+
action.chunks.push({
|
|
310
|
+
origIndex: chunk.origIndex + newIndex,
|
|
311
|
+
delLines: chunk.delLines,
|
|
312
|
+
insLines: chunk.insLines,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
index = newIndex + nextChunkContext.length;
|
|
317
|
+
state.index = endPatchIndex;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return action;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const VALID_HUNK_HEADERS = [
|
|
324
|
+
"'*** Add File: {path}'",
|
|
325
|
+
"'*** Delete File: {path}'",
|
|
326
|
+
"'*** Update File: {path}'",
|
|
327
|
+
].join(", ");
|
|
328
|
+
|
|
329
|
+
export function parsePatchActions({ text }: { text: string }): ParsedPatchAction[] {
|
|
330
|
+
const lines = text.trim().split("\n");
|
|
331
|
+
if (lines.length < 2 || !lines[0].startsWith("*** Begin Patch") || lines[lines.length - 1] !== "*** End Patch") {
|
|
332
|
+
throw new DiffError("Invalid patch text");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const actions: ParsedPatchAction[] = [];
|
|
336
|
+
const seenPaths = new Set<string>();
|
|
337
|
+
let index = 1;
|
|
338
|
+
|
|
339
|
+
while (index < lines.length - 1) {
|
|
340
|
+
const line = lines[index];
|
|
341
|
+
const lineNumber = index + 1;
|
|
342
|
+
|
|
343
|
+
if (line.startsWith("*** Update File: ")) {
|
|
344
|
+
const updatePath = normalizePatchPath({ path: line.slice("*** Update File: ".length) });
|
|
345
|
+
if (seenPaths.has(updatePath)) {
|
|
346
|
+
throw new DiffError(`Update File Error: Duplicate Path: ${updatePath}`);
|
|
347
|
+
}
|
|
348
|
+
seenPaths.add(updatePath);
|
|
349
|
+
index += 1;
|
|
350
|
+
let movePath: string | undefined;
|
|
351
|
+
if (index < lines.length - 1 && lines[index].startsWith("*** Move to: ")) {
|
|
352
|
+
movePath = normalizePatchPath({ path: lines[index].slice("*** Move to: ".length) });
|
|
353
|
+
index += 1;
|
|
354
|
+
}
|
|
355
|
+
const bodyStart = index;
|
|
356
|
+
while (
|
|
357
|
+
index < lines.length - 1 &&
|
|
358
|
+
!lines[index].startsWith("*** Update File: ") &&
|
|
359
|
+
!lines[index].startsWith("*** Delete File: ") &&
|
|
360
|
+
!lines[index].startsWith("*** Add File: ")
|
|
361
|
+
) {
|
|
362
|
+
index += 1;
|
|
363
|
+
}
|
|
364
|
+
const bodyLines = lines.slice(bodyStart, index);
|
|
365
|
+
if (bodyLines.length === 0) {
|
|
366
|
+
throw new DiffError(`Invalid patch hunk on line ${lineNumber}: Update file hunk for path '${updatePath}' is empty`);
|
|
367
|
+
}
|
|
368
|
+
actions.push({
|
|
369
|
+
type: "update",
|
|
370
|
+
path: updatePath,
|
|
371
|
+
movePath,
|
|
372
|
+
lines: bodyLines,
|
|
373
|
+
});
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (line.startsWith("*** Delete File: ")) {
|
|
378
|
+
const deletePath = normalizePatchPath({ path: line.slice("*** Delete File: ".length) });
|
|
379
|
+
if (seenPaths.has(deletePath)) {
|
|
380
|
+
throw new DiffError(`Delete File Error: Duplicate Path: ${deletePath}`);
|
|
381
|
+
}
|
|
382
|
+
seenPaths.add(deletePath);
|
|
383
|
+
actions.push({
|
|
384
|
+
type: "delete",
|
|
385
|
+
path: deletePath,
|
|
386
|
+
});
|
|
387
|
+
index += 1;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (line.startsWith("*** Add File: ")) {
|
|
392
|
+
const addPath = normalizePatchPath({ path: line.slice("*** Add File: ".length) });
|
|
393
|
+
if (seenPaths.has(addPath)) {
|
|
394
|
+
throw new DiffError(`Add File Error: Duplicate Path: ${addPath}`);
|
|
395
|
+
}
|
|
396
|
+
seenPaths.add(addPath);
|
|
397
|
+
const state: ParserState = {
|
|
398
|
+
lines,
|
|
399
|
+
index: index + 1,
|
|
400
|
+
fuzz: 0,
|
|
401
|
+
};
|
|
402
|
+
const action = parseAddFile({ state });
|
|
403
|
+
actions.push({
|
|
404
|
+
type: "add",
|
|
405
|
+
path: addPath,
|
|
406
|
+
newFile: action.newFile,
|
|
407
|
+
});
|
|
408
|
+
index = state.index;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
throw new DiffError(
|
|
413
|
+
`Invalid patch hunk on line ${lineNumber}: '${line}' is not a valid hunk header. Valid hunk headers: ${VALID_HUNK_HEADERS}`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (actions.length === 0) {
|
|
418
|
+
throw new DiffError("No files were modified.");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return actions;
|
|
422
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { DiffError } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function normalizePatchPath({ path }: { path: string }): string {
|
|
6
|
+
const trimmed = path.trim();
|
|
7
|
+
const withoutAt = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
8
|
+
return withoutAt.replace(/^['"]|['"]$/g, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Patch paths are intentionally confined to ctx.cwd so the adapter cannot write
|
|
12
|
+
// outside the active workspace even if the incoming patch text is malicious.
|
|
13
|
+
export function resolvePatchPath({ cwd, patchPath }: { cwd: string; patchPath: string }): string {
|
|
14
|
+
const normalized = normalizePatchPath({ path: patchPath });
|
|
15
|
+
if (!normalized) {
|
|
16
|
+
throw new DiffError("Patch path cannot be empty");
|
|
17
|
+
}
|
|
18
|
+
if (isAbsolute(normalized)) {
|
|
19
|
+
throw new DiffError("We do not support absolute paths.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const absolutePath = resolve(cwd, normalized);
|
|
23
|
+
const rel = relative(cwd, absolutePath);
|
|
24
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
25
|
+
throw new DiffError(`Path escapes working directory: ${normalized}`);
|
|
26
|
+
}
|
|
27
|
+
return absolutePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function openFileAtPath({ cwd, path }: { cwd: string; path: string }): string {
|
|
31
|
+
const absolutePath = resolvePatchPath({ cwd, patchPath: path });
|
|
32
|
+
if (!existsSync(absolutePath)) {
|
|
33
|
+
throw new DiffError(`File not found: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
return readFileSync(absolutePath, "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function writeFileAtPath({ cwd, path, content }: { cwd: string; path: string; content: string }): { created: boolean } {
|
|
39
|
+
const absolutePath = resolvePatchPath({ cwd, patchPath: path });
|
|
40
|
+
const created = !existsSync(absolutePath);
|
|
41
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
42
|
+
writeFileSync(absolutePath, content, "utf8");
|
|
43
|
+
return { created };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function removeFileAtPath({ cwd, path }: { cwd: string; path: string }): void {
|
|
47
|
+
const absolutePath = resolvePatchPath({ cwd, patchPath: path });
|
|
48
|
+
if (!existsSync(absolutePath)) {
|
|
49
|
+
throw new DiffError(`File not found: ${path}`);
|
|
50
|
+
}
|
|
51
|
+
unlinkSync(absolutePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function pathExists({ cwd, path }: { cwd: string; path: string }): boolean {
|
|
55
|
+
return existsSync(resolvePatchPath({ cwd, patchPath: path }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ActionType = "add" | "delete" | "update";
|
|
2
|
+
export type ParseMode = "keep" | "add" | "delete";
|
|
3
|
+
|
|
4
|
+
export interface Chunk {
|
|
5
|
+
origIndex: number;
|
|
6
|
+
delLines: string[];
|
|
7
|
+
insLines: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PatchAction {
|
|
11
|
+
type: ActionType;
|
|
12
|
+
newFile?: string;
|
|
13
|
+
chunks: Chunk[];
|
|
14
|
+
movePath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParsedPatchAction {
|
|
18
|
+
type: ActionType;
|
|
19
|
+
path: string;
|
|
20
|
+
newFile?: string;
|
|
21
|
+
lines?: string[];
|
|
22
|
+
movePath?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ParserState {
|
|
26
|
+
lines: string[];
|
|
27
|
+
index: number;
|
|
28
|
+
fuzz: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExecutePatchResult {
|
|
32
|
+
changedFiles: string[];
|
|
33
|
+
createdFiles: string[];
|
|
34
|
+
deletedFiles: string[];
|
|
35
|
+
movedFiles: string[];
|
|
36
|
+
fuzz: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DiffError extends Error {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "DiffError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface PromptSkill {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const PI_INTRO =
|
|
8
|
+
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
9
|
+
const CODEX_INTRO =
|
|
10
|
+
"You are Codex running inside pi, a coding agent harness. Work directly in the user's workspace and finish the task end-to-end when feasible.";
|
|
11
|
+
|
|
12
|
+
const CODEX_GUIDELINES = [
|
|
13
|
+
"Use `parallel` only when tool calls are independent and can safely run at the same time.",
|
|
14
|
+
"Use `write_stdin` when an exec session returns `session_id`, and continue until `exit_code` is present.",
|
|
15
|
+
"Do not request `tty` unless interactive terminal behavior is required.",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function rewriteIntro(prompt: string): string {
|
|
19
|
+
if (!prompt.startsWith(PI_INTRO)) {
|
|
20
|
+
return prompt;
|
|
21
|
+
}
|
|
22
|
+
return `${CODEX_INTRO}${prompt.slice(PI_INTRO.length)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function insertBeforeTrailingContext(prompt: string, section: string): string {
|
|
26
|
+
const currentDateIndex = prompt.lastIndexOf("\nCurrent date:");
|
|
27
|
+
if (currentDateIndex !== -1) {
|
|
28
|
+
return `${prompt.slice(0, currentDateIndex)}\n\n${section}${prompt.slice(currentDateIndex)}`;
|
|
29
|
+
}
|
|
30
|
+
return `${prompt}\n\n${section}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decodeXml(text: string): string {
|
|
34
|
+
return text
|
|
35
|
+
.replace(/'/g, "'")
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/>/g, ">")
|
|
38
|
+
.replace(/</g, "<")
|
|
39
|
+
.replace(/&/g, "&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extractPiPromptSkills(prompt: string): PromptSkill[] {
|
|
43
|
+
const skillsBlockMatch = prompt.match(/<available_skills>\n([\s\S]*?)\n<\/available_skills>/);
|
|
44
|
+
if (!skillsBlockMatch) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const skillMatches = skillsBlockMatch[1].matchAll(
|
|
49
|
+
/<skill>\n\s*<name>([\s\S]*?)<\/name>\n\s*<description>([\s\S]*?)<\/description>\n\s*<location>([\s\S]*?)<\/location>\n\s*<\/skill>/g,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return Array.from(skillMatches, (match) => ({
|
|
53
|
+
name: decodeXml(match[1].trim()),
|
|
54
|
+
description: decodeXml(match[2].trim()),
|
|
55
|
+
filePath: decodeXml(match[3].trim()),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function injectSkills(prompt: string, skills: PromptSkill[]): string {
|
|
60
|
+
if (skills.length === 0 || /\n## Skills\b/.test(prompt) || /<skills_instructions>/.test(prompt)) {
|
|
61
|
+
return prompt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lines = [
|
|
65
|
+
"<skills_instructions>",
|
|
66
|
+
"## Skills",
|
|
67
|
+
"A skill is a set of local instructions in a `SKILL.md` file.",
|
|
68
|
+
"### Available skills",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (const skill of skills) {
|
|
72
|
+
lines.push(`- ${skill.name}: ${skill.description} (file: ${skill.filePath})`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push("### How to use skills");
|
|
76
|
+
lines.push("- Use a skill when the user names it (`$SkillName` or plain text) or when the request clearly matches its description.");
|
|
77
|
+
lines.push("- Use the minimal required set of skills. If multiple apply, use them together and state the order briefly.");
|
|
78
|
+
lines.push("- For each selected skill, open its `SKILL.md`, resolve relative paths from the skill directory first, load only the files you need, and prefer existing scripts/assets/templates over recreating them.");
|
|
79
|
+
lines.push("### Fallback");
|
|
80
|
+
lines.push("- If a skill is missing or its path cannot be read, say so briefly and continue with the best fallback approach.");
|
|
81
|
+
lines.push("</skills_instructions>");
|
|
82
|
+
|
|
83
|
+
return insertBeforeTrailingContext(prompt, lines.join("\n"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function injectGuidelines(prompt: string): string {
|
|
87
|
+
const match = prompt.match(/(^Guidelines:\n)([\s\S]*?)(\n\n(?:Pi documentation:|# Project Context|# Skills|Current date:))/m);
|
|
88
|
+
if (!match || match.index === undefined) {
|
|
89
|
+
const fallbackSection = `Codex mode guidelines:\n${CODEX_GUIDELINES.map((line) => `- ${line}`).join("\n")}`;
|
|
90
|
+
return insertBeforeTrailingContext(prompt, fallbackSection);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const [, header, body, suffix] = match;
|
|
94
|
+
const existingLines = body
|
|
95
|
+
.split("\n")
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.filter((line) => line.startsWith("- "));
|
|
98
|
+
const existing = new Set(existingLines.map((line) => line.slice(2)));
|
|
99
|
+
const additions = CODEX_GUIDELINES.filter((line) => !existing.has(line)).map((line) => `- ${line}`);
|
|
100
|
+
if (additions.length === 0) {
|
|
101
|
+
return prompt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const normalizedBody = body.trimEnd();
|
|
105
|
+
const replacement = `${header}${normalizedBody}\n${additions.join("\n")}${suffix}`;
|
|
106
|
+
return `${prompt.slice(0, match.index)}${replacement}${prompt.slice(match.index + match[0].length)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildCodexSystemPrompt(basePrompt: string, options: { skills?: PromptSkill[] } = {}): string {
|
|
110
|
+
return injectSkills(injectGuidelines(rewriteIntro(basePrompt)), options.skills ?? []);
|
|
111
|
+
}
|