@ai-sdk/harness-pi 1.0.119 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-pi",
3
- "version": "1.0.119",
3
+ "version": "1.0.120",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.117",
29
+ "@ai-sdk/harness": "1.0.118",
30
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",
@@ -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.117",
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",
@@ -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
- function lastOutputLine(output: Buffer): string | undefined {
69
- return output.toString('utf8').trim().split('\n').filter(Boolean).at(-1);
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 combined = `${result.stdout}${result.stderr}`;
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
- `if [ ! -e "$target" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi`,
109
- `resolved=$(realpath "$target" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }`,
110
- `printf '%s\\n' "$resolved"`,
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 = lastOutputLine(result.output);
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 resolved=$(realpath "$target" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }; printf '%s\\n' "$resolved"; exit 0; fi`,
145
- `dir=$(dirname "$target")`,
146
- `base=$(basename "$target")`,
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
- `while [ ! -e "$dir" ] && [ ! -L "$dir" ]; do parent=$(dirname "$dir"); if [ "$parent" = "$dir" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi; missing="$(basename "$dir")/$missing"; dir="$parent"; done`,
149
- `resolved_dir=$(realpath "$dir" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }`,
150
- `printf '%s/%s\\n' "$resolved_dir" "$missing"`,
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 = lastOutputLine(result.output);
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
- const flags = [
323
- '-r',
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 limit = Math.max(1, input.limit ?? 100);
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
- `grep ${flags.map(shellQuote).join(' ')} -- ${shellQuote(pattern)} ${shellQuote(targetPath)} 2>/dev/null | head -n ${limit}`,
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
- return output || 'No matches found';
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 {