@ai-sdk/harness-pi 1.0.118 → 1.0.120
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 +20 -0
- package/dist/index.js +157 -29
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/pi-remote-ops.ts +169 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.120",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.118",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.45",
|
|
31
31
|
"@earendil-works/pi-ai": "0.74.2",
|
|
32
32
|
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
33
33
|
"pi-mcp-adapter": "2.12.1",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"zod": "^3.25.76 || ^4.1.8"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@ai-sdk/sandbox-just-bash": "1.0.
|
|
40
|
+
"@ai-sdk/sandbox-just-bash": "1.0.118",
|
|
41
41
|
"@types/node": "22.19.19",
|
|
42
42
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
43
43
|
"tsup": "^8.5.1",
|
package/src/pi-remote-ops.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { shellQuote } from '@ai-sdk/harness/utils';
|
|
3
4
|
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
@@ -63,10 +64,46 @@ interface RunShellInput {
|
|
|
63
64
|
interface RunShellResult {
|
|
64
65
|
exitCode: number | null;
|
|
65
66
|
output: Buffer;
|
|
67
|
+
stdout: string;
|
|
68
|
+
stderr: string;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
const MAX_GREP_DIAGNOSTIC_BYTES = 8_192;
|
|
72
|
+
const REALPATH_FRAME = '__PI_REALPATH_FRAME__';
|
|
73
|
+
const BASE64_PATH_PATTERN =
|
|
74
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
75
|
+
|
|
76
|
+
function realpathShellLines(targetVariable: string): string[] {
|
|
77
|
+
return [
|
|
78
|
+
'set -o pipefail',
|
|
79
|
+
`realpath_marker=${shellQuote(REALPATH_FRAME)}`,
|
|
80
|
+
`realpath_framed=$(realpath "$${targetVariable}" 2>/dev/null; realpath_status=$?; printf '%s' "$realpath_marker"; exit "$realpath_status")`,
|
|
81
|
+
'realpath_status=$?',
|
|
82
|
+
'if [ "$realpath_status" -ne 0 ]; then echo "__PI_REALPATH_FAILED__"; exit 3; fi',
|
|
83
|
+
'resolved=${realpath_framed%$realpath_marker}',
|
|
84
|
+
"resolved=${resolved%$'\\n'}",
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function framePathShell(pathExpression: string): string {
|
|
89
|
+
return `if ! printf '%s' ${pathExpression} | base64 | tr -d '\\r\\n'; then exit 4; fi; printf '%s' "$realpath_marker"`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseFramedPath(output: string): string | undefined {
|
|
93
|
+
if (!output.endsWith(REALPATH_FRAME)) return undefined;
|
|
94
|
+
|
|
95
|
+
const encodedPath = output.slice(0, -REALPATH_FRAME.length);
|
|
96
|
+
if (!encodedPath || !BASE64_PATH_PATTERN.test(encodedPath)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(
|
|
102
|
+
Buffer.from(encodedPath, 'base64'),
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
70
107
|
}
|
|
71
108
|
|
|
72
109
|
export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
@@ -86,8 +123,7 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
86
123
|
...(input.signal ? { abortSignal: input.signal } : {}),
|
|
87
124
|
});
|
|
88
125
|
|
|
89
|
-
const
|
|
90
|
-
const output = Buffer.from(combined, 'utf8');
|
|
126
|
+
const output = Buffer.from(`${result.stdout}${result.stderr}`, 'utf8');
|
|
91
127
|
if (output.length > 0) {
|
|
92
128
|
input.onData?.(output);
|
|
93
129
|
}
|
|
@@ -95,6 +131,8 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
95
131
|
return {
|
|
96
132
|
exitCode: result.exitCode,
|
|
97
133
|
output,
|
|
134
|
+
stdout: result.stdout,
|
|
135
|
+
stderr: result.stderr,
|
|
98
136
|
};
|
|
99
137
|
};
|
|
100
138
|
|
|
@@ -105,9 +143,9 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
105
143
|
const result = await runShell(
|
|
106
144
|
[
|
|
107
145
|
`target=${shellQuote(remotePath)}`,
|
|
108
|
-
|
|
109
|
-
`
|
|
110
|
-
|
|
146
|
+
...realpathShellLines('target'),
|
|
147
|
+
`if [ ! -e "$resolved" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi`,
|
|
148
|
+
framePathShell('"$resolved"'),
|
|
111
149
|
].join('; '),
|
|
112
150
|
);
|
|
113
151
|
|
|
@@ -119,7 +157,7 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
119
157
|
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
120
158
|
}
|
|
121
159
|
|
|
122
|
-
const resolvedPath =
|
|
160
|
+
const resolvedPath = parseFramedPath(result.stdout);
|
|
123
161
|
if (!resolvedPath) {
|
|
124
162
|
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
125
163
|
}
|
|
@@ -141,13 +179,14 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
141
179
|
const result = await runShell(
|
|
142
180
|
[
|
|
143
181
|
`target=${shellQuote(remotePath)}`,
|
|
144
|
-
`if [ -e "$target" ] || [ -L "$target" ]; then
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
`if [ -e "$target" ] || [ -L "$target" ]; then ${realpathShellLines('target').join('; ')}; ${framePathShell('"$resolved"')}; exit 0; fi`,
|
|
183
|
+
'dir=${target%/*}',
|
|
184
|
+
'base=${target##*/}',
|
|
185
|
+
'[ -n "$dir" ] || dir=/',
|
|
147
186
|
`missing="$base"`,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
'while [ ! -e "$dir" ] && [ ! -L "$dir" ]; do parent=${dir%/*}; [ -n "$parent" ] || parent=/; if [ "$parent" = "$dir" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi; missing=${dir##*/}/$missing; dir=$parent; done',
|
|
188
|
+
...realpathShellLines('dir'),
|
|
189
|
+
framePathShell('"$resolved/$missing"'),
|
|
151
190
|
].join('; '),
|
|
152
191
|
);
|
|
153
192
|
|
|
@@ -160,7 +199,7 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
160
199
|
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
const resolvedPath =
|
|
202
|
+
const resolvedPath = parseFramedPath(result.stdout);
|
|
164
203
|
if (!resolvedPath) {
|
|
165
204
|
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
166
205
|
}
|
|
@@ -318,24 +357,106 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
318
357
|
const targetPath =
|
|
319
358
|
relativeTarget.startsWith('../') || path.posix.isAbsolute(relativeTarget)
|
|
320
359
|
? resolvedPath
|
|
321
|
-
: relativeTarget
|
|
322
|
-
|
|
323
|
-
|
|
360
|
+
: relativeTarget.startsWith('-')
|
|
361
|
+
? `./${relativeTarget}`
|
|
362
|
+
: relativeTarget;
|
|
363
|
+
const limit = Math.max(1, input.limit ?? 100);
|
|
364
|
+
const commonFlags = [
|
|
324
365
|
'-n',
|
|
325
|
-
'--binary-files=without-match',
|
|
326
366
|
...(input.ignoreCase ? ['-i'] : []),
|
|
327
367
|
...(input.literal ? ['-F'] : []),
|
|
328
368
|
...(typeof input.context === 'number' && input.context > 0
|
|
329
369
|
? ['-C', String(input.context)]
|
|
330
370
|
: []),
|
|
331
|
-
...(input.glob ? ['--include', input.glob] : []),
|
|
332
371
|
];
|
|
333
|
-
const
|
|
372
|
+
const recursiveFlags = [
|
|
373
|
+
'-r',
|
|
374
|
+
...commonFlags,
|
|
375
|
+
'-m',
|
|
376
|
+
String(limit),
|
|
377
|
+
...(input.glob ? [`--include=${input.glob}`] : []),
|
|
378
|
+
];
|
|
379
|
+
const temporaryPathPrefix = `/tmp/.ai-sdk-harness-pi-grep-${randomUUID()}`;
|
|
380
|
+
const outputPath = `${temporaryPathPrefix}.stdout`;
|
|
381
|
+
const stderrPath = `${temporaryPathPrefix}.stderr`;
|
|
382
|
+
const statusPath = `${temporaryPathPrefix}.status`;
|
|
383
|
+
const fileOutputPath = `${temporaryPathPrefix}.file.stdout`;
|
|
384
|
+
const fileStderrPath = `${temporaryPathPrefix}.file.stderr`;
|
|
385
|
+
const findStderrPath = `${temporaryPathPrefix}.find.stderr`;
|
|
386
|
+
const boundedFileGrepScript = [
|
|
387
|
+
`grep_limit=${limit}`,
|
|
388
|
+
`grep_output=${shellQuote(outputPath)}`,
|
|
389
|
+
`grep_stderr=${shellQuote(stderrPath)}`,
|
|
390
|
+
`grep_status=${shellQuote(statusPath)}`,
|
|
391
|
+
`grep_file_output=${shellQuote(fileOutputPath)}`,
|
|
392
|
+
`grep_file_stderr=${shellQuote(fileStderrPath)}`,
|
|
393
|
+
'grep_output_lines=$(wc -l < "$grep_output")',
|
|
394
|
+
'grep_overall_status=$(cat "$grep_status")',
|
|
395
|
+
'for grep_file in "$@"; do',
|
|
396
|
+
'grep_remaining=$((grep_limit - grep_output_lines))',
|
|
397
|
+
'if [ "$grep_remaining" -le 0 ]; then break; fi',
|
|
398
|
+
`grep ${commonFlags.map(shellQuote).join(' ')} -m "$grep_remaining" -e ${shellQuote(pattern)} "$grep_file" > "$grep_file_output" 2> "$grep_file_stderr"`,
|
|
399
|
+
'grep_file_status=$?',
|
|
400
|
+
'if [ "$grep_file_status" -eq 0 ]; then',
|
|
401
|
+
'grep_display_file=${grep_file#./}',
|
|
402
|
+
'grep_file_lines=0',
|
|
403
|
+
'while IFS= read -r grep_line; do',
|
|
404
|
+
'if [ "$grep_file_lines" -ge "$grep_remaining" ]; then break; fi',
|
|
405
|
+
'case "$grep_line" in',
|
|
406
|
+
`'--') printf '%s\\n' "$grep_line" ;;`,
|
|
407
|
+
`[0-9]*:*) printf '%s:%s\\n' "$grep_display_file" "$grep_line" ;;`,
|
|
408
|
+
`[0-9]*-*) printf '%s-%s\\n' "$grep_display_file" "$grep_line" ;;`,
|
|
409
|
+
`*) printf '%s:%s\\n' "$grep_display_file" "$grep_line" ;;`,
|
|
410
|
+
'esac',
|
|
411
|
+
'grep_file_lines=$((grep_file_lines + 1))',
|
|
412
|
+
'done < "$grep_file_output" >> "$grep_output"',
|
|
413
|
+
'grep_output_lines=$((grep_output_lines + grep_file_lines))',
|
|
414
|
+
'grep_overall_status=0',
|
|
415
|
+
'elif [ "$grep_file_status" -gt 1 ]; then',
|
|
416
|
+
`grep_diagnostic_size=$(wc -c < "$grep_stderr")`,
|
|
417
|
+
`grep_diagnostic_remaining=$((${MAX_GREP_DIAGNOSTIC_BYTES} - grep_diagnostic_size))`,
|
|
418
|
+
'if [ "$grep_diagnostic_remaining" -gt 0 ]; then head -c "$grep_diagnostic_remaining" "$grep_file_stderr" >> "$grep_stderr"; fi',
|
|
419
|
+
'grep_overall_status=2',
|
|
420
|
+
'break',
|
|
421
|
+
'fi',
|
|
422
|
+
'done',
|
|
423
|
+
'printf \'%s\\n\' "$grep_overall_status" > "$grep_status"',
|
|
424
|
+
].join('\n');
|
|
425
|
+
const findFlags = [
|
|
426
|
+
shellQuote(targetPath),
|
|
427
|
+
'-type',
|
|
428
|
+
'f',
|
|
429
|
+
...(input.glob ? ['-name', shellQuote(input.glob)] : []),
|
|
430
|
+
].join(' ');
|
|
334
431
|
const result = await runShell(
|
|
335
432
|
[
|
|
336
433
|
`if [ ! -e ${shellQuote(resolvedPath)} ]; then echo "__PI_GREP_NOT_FOUND__"; exit 2; fi`,
|
|
337
434
|
`cd ${shellQuote(options.paths.sandboxWorkDir)}`,
|
|
338
|
-
|
|
435
|
+
// Preserve binary skipping where grep supports it without passing an
|
|
436
|
+
// unsupported option to just-bash.
|
|
437
|
+
`binary_option_error=$(grep --binary-files=without-match -e '' /dev/null 2>&1)`,
|
|
438
|
+
`if [ -z "$binary_option_error" ]; then binary_option='--binary-files=without-match'; else binary_option=''; fi`,
|
|
439
|
+
`grep_stderr=${shellQuote(stderrPath)}`,
|
|
440
|
+
`grep_output=${shellQuote(outputPath)}`,
|
|
441
|
+
`grep_status_file=${shellQuote(statusPath)}`,
|
|
442
|
+
`grep_find_stderr=${shellQuote(findStderrPath)}`,
|
|
443
|
+
': > "$grep_stderr"',
|
|
444
|
+
// just-bash buffers every pipeline stage and applies grep -m per file.
|
|
445
|
+
// Search files individually there so the producer never emits more
|
|
446
|
+
// than the remaining global result limit.
|
|
447
|
+
`if [ -n "$binary_option" ] || [ ! -d ${shellQuote(targetPath)} ]; then set -o pipefail; grep $binary_option ${recursiveFlags.map(shellQuote).join(' ')} -e ${shellQuote(pattern)} ${shellQuote(targetPath)} 2>"$grep_stderr" | head -n ${limit}; grep_status=$?; else : > "$grep_output"; printf '1\\n' > "$grep_status_file"; find ${findFlags} -exec bash -c ${shellQuote(boundedFileGrepScript)} bash {} + 2>"$grep_find_stderr"; find_status=$?; grep_diagnostic_size=$(wc -c < "$grep_stderr"); grep_diagnostic_remaining=$((${MAX_GREP_DIAGNOSTIC_BYTES} - grep_diagnostic_size)); if [ "$grep_diagnostic_remaining" -gt 0 ]; then head -c "$grep_diagnostic_remaining" "$grep_find_stderr" >> "$grep_stderr"; fi; grep_status=$(cat "$grep_status_file"); if [ "$find_status" -ne 0 ]; then grep_status=2; fi; cat "$grep_output"; fi`,
|
|
448
|
+
`head -c ${MAX_GREP_DIAGNOSTIC_BYTES} "$grep_stderr" >&2`,
|
|
449
|
+
`rm -f ${[
|
|
450
|
+
outputPath,
|
|
451
|
+
stderrPath,
|
|
452
|
+
statusPath,
|
|
453
|
+
fileOutputPath,
|
|
454
|
+
fileStderrPath,
|
|
455
|
+
findStderrPath,
|
|
456
|
+
]
|
|
457
|
+
.map(shellQuote)
|
|
458
|
+
.join(' ')}`,
|
|
459
|
+
'exit "$grep_status"',
|
|
339
460
|
].join('; '),
|
|
340
461
|
);
|
|
341
462
|
|
|
@@ -343,8 +464,33 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
343
464
|
if (output.includes('__PI_GREP_NOT_FOUND__')) {
|
|
344
465
|
throw new Error(`Path not found: ${input.path ?? '.'}`);
|
|
345
466
|
}
|
|
467
|
+
const stdout = result.stdout.trim();
|
|
468
|
+
const stderr = result.stderr.trim();
|
|
469
|
+
|
|
470
|
+
if (
|
|
471
|
+
result.exitCode === 0 ||
|
|
472
|
+
result.exitCode === 1 ||
|
|
473
|
+
// GNU grep can receive SIGPIPE after head reaches the requested limit.
|
|
474
|
+
result.exitCode === 141
|
|
475
|
+
) {
|
|
476
|
+
if (stdout) {
|
|
477
|
+
return [stdout, stderr].filter(Boolean).join('\n');
|
|
478
|
+
}
|
|
479
|
+
if (stderr) {
|
|
480
|
+
throw new Error(stderr);
|
|
481
|
+
}
|
|
482
|
+
return 'No matches found';
|
|
483
|
+
}
|
|
346
484
|
|
|
347
|
-
|
|
485
|
+
// GNU grep exits 2 when recursive traversal encounters unreadable entries,
|
|
486
|
+
// even if it also found useful matches in readable files.
|
|
487
|
+
if (result.exitCode === 2 && stdout && stderr) {
|
|
488
|
+
return `${stdout}\n${stderr}`;
|
|
489
|
+
}
|
|
490
|
+
if (stderr) {
|
|
491
|
+
throw new Error(stderr);
|
|
492
|
+
}
|
|
493
|
+
throw new Error(output || `grep failed with exit code ${result.exitCode}`);
|
|
348
494
|
};
|
|
349
495
|
|
|
350
496
|
return {
|