@ai-sdk/harness-cline 1.0.6 → 1.0.8

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @ai-sdk/harness-cline
2
2
 
3
+ ## 1.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - cb78370: fix(harness-cline): fix bug with reading and writing files
8
+ - Updated dependencies [7f50d28]
9
+ - @ai-sdk/harness@1.0.81
10
+
11
+ ## 1.0.8
12
+
13
+ ### Patch Changes
14
+
15
+ - 016b944: fix(harness-cline): fix bug with reading and writing files
16
+ - @ai-sdk/harness@1.0.80
17
+
18
+ ## 1.0.7
19
+
20
+ ### Patch Changes
21
+
22
+ - @ai-sdk/harness@1.0.79
23
+
3
24
  ## 1.0.6
4
25
 
5
26
  ### Patch Changes
package/dist/index.js CHANGED
@@ -229,23 +229,124 @@ import path2 from "path";
229
229
  import { shellQuote } from "@ai-sdk/harness/utils";
230
230
  function createClineRemoteOps(options) {
231
231
  const { sandbox, workDir } = options;
232
- const resolvePath = (inputPath) => {
233
- const normalized = path2.posix.isAbsolute(inputPath) ? path2.posix.normalize(inputPath) : path2.posix.normalize(path2.posix.join(workDir, inputPath));
232
+ const normalizedWorkDir = path2.posix.normalize(workDir);
233
+ const readableRoots = options.readableRoots?.map((root) => path2.posix.normalize(root.sandboxDir)) ?? [];
234
+ const isInsidePath = ({
235
+ parent,
236
+ candidate
237
+ }) => {
238
+ const relative = path2.posix.relative(parent, candidate);
239
+ return relative === "" || relative !== ".." && !relative.startsWith("../") && !path2.posix.isAbsolute(relative);
240
+ };
241
+ const assertWorkspacePath = (inputPath) => {
242
+ const normalized = path2.posix.normalize(inputPath);
243
+ if (!isInsidePath({ parent: normalizedWorkDir, candidate: normalized })) {
244
+ throw new Error(`Cline path escapes the workspace: ${inputPath}`);
245
+ }
246
+ return normalized;
247
+ };
248
+ const assertReadablePath = (inputPath) => {
249
+ const normalized = path2.posix.normalize(inputPath);
250
+ if (!isInsidePath({ parent: normalizedWorkDir, candidate: normalized }) && !readableRoots.some(
251
+ (parent) => isInsidePath({ parent, candidate: normalized })
252
+ )) {
253
+ throw new Error(`Cline path escapes the readable roots: ${inputPath}`);
254
+ }
234
255
  return normalized;
235
256
  };
236
- const runShell = async (command, input) => {
257
+ const resolvePath = (inputPath) => {
258
+ const resolved = path2.posix.isAbsolute(inputPath) ? path2.posix.normalize(inputPath) : path2.posix.normalize(path2.posix.join(normalizedWorkDir, inputPath));
259
+ return assertWorkspacePath(resolved);
260
+ };
261
+ const resolveReadablePath = (inputPath) => {
262
+ if (!path2.posix.isAbsolute(inputPath)) {
263
+ return resolvePath(inputPath);
264
+ }
265
+ return assertReadablePath(inputPath);
266
+ };
267
+ const runShell = async ({
268
+ command,
269
+ signal
270
+ }) => {
237
271
  const result = await sandbox.run({
238
272
  command,
239
- workingDirectory: workDir,
240
- ...input?.signal ? { abortSignal: input.signal } : {}
273
+ workingDirectory: normalizedWorkDir,
274
+ ...signal ? { abortSignal: signal } : {}
241
275
  });
242
276
  return {
243
277
  exitCode: result.exitCode,
244
278
  output: `${result.stdout}${result.stderr}`
245
279
  };
246
280
  };
281
+ const lastOutputLine = (output) => output.trim().split("\n").filter(Boolean).at(-1);
282
+ const resolveExistingSandboxPath = async ({
283
+ remotePath,
284
+ inputPath,
285
+ missingMessage
286
+ }) => {
287
+ const result = await runShell({
288
+ command: [
289
+ `target=${shellQuote(remotePath)}`,
290
+ 'if [ ! -e "$target" ]; then echo "__CLINE_REALPATH_NOT_FOUND__"; exit 2; fi',
291
+ 'resolved=$(realpath "$target" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
292
+ `printf '%s\\n' "$resolved"`
293
+ ].join("; ")
294
+ });
295
+ if (result.output.includes("__CLINE_REALPATH_NOT_FOUND__")) {
296
+ throw new Error(missingMessage ?? `Path not found: ${inputPath}`);
297
+ }
298
+ if (result.output.includes("__CLINE_REALPATH_FAILED__") || result.exitCode !== 0) {
299
+ throw new Error(`Unable to resolve path: ${inputPath}`);
300
+ }
301
+ const resolvedPath = lastOutputLine(result.output);
302
+ if (!resolvedPath) {
303
+ throw new Error(`Unable to resolve path: ${inputPath}`);
304
+ }
305
+ return resolvedPath;
306
+ };
307
+ const resolveReadableSandboxPath = async ({
308
+ remotePath,
309
+ inputPath,
310
+ missingMessage
311
+ }) => assertReadablePath(
312
+ await resolveExistingSandboxPath({
313
+ remotePath,
314
+ inputPath,
315
+ ...missingMessage ? { missingMessage } : {}
316
+ })
317
+ );
318
+ const resolveWritableSandboxPath = async ({
319
+ remotePath,
320
+ inputPath
321
+ }) => {
322
+ const result = await runShell({
323
+ command: [
324
+ `target=${shellQuote(remotePath)}`,
325
+ `if [ -e "$target" ] || [ -L "$target" ]; then resolved=$(realpath "$target" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }; printf '%s\\n' "$resolved"; exit 0; fi`,
326
+ 'dir=$(dirname "$target")',
327
+ 'base=$(basename "$target")',
328
+ 'missing="$base"',
329
+ 'while [ ! -e "$dir" ] && [ ! -L "$dir" ]; do parent=$(dirname "$dir"); if [ "$parent" = "$dir" ]; then echo "__CLINE_REALPATH_NOT_FOUND__"; exit 2; fi; missing="$(basename "$dir")/$missing"; dir="$parent"; done',
330
+ 'resolved_dir=$(realpath "$dir" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
331
+ `printf '%s/%s\\n' "$resolved_dir" "$missing"`
332
+ ].join("; ")
333
+ });
334
+ if (result.output.includes("__CLINE_REALPATH_NOT_FOUND__") || result.output.includes("__CLINE_REALPATH_FAILED__") || result.exitCode !== 0) {
335
+ throw new Error(`Unable to resolve path: ${inputPath}`);
336
+ }
337
+ const resolvedPath = lastOutputLine(result.output);
338
+ if (!resolvedPath) {
339
+ throw new Error(`Unable to resolve path: ${inputPath}`);
340
+ }
341
+ return assertWorkspacePath(resolvedPath);
342
+ };
247
343
  const readFile = async (inputPath) => {
248
- const resolved = resolvePath(inputPath);
344
+ const remotePath = resolveReadablePath(inputPath);
345
+ const resolved = await resolveReadableSandboxPath({
346
+ remotePath,
347
+ inputPath,
348
+ missingMessage: `File not found: ${inputPath}`
349
+ });
249
350
  const content = await sandbox.readTextFile({ path: resolved });
250
351
  if (content == null) {
251
352
  throw new Error(`File not found: ${inputPath}`);
@@ -253,11 +354,26 @@ function createClineRemoteOps(options) {
253
354
  return content;
254
355
  };
255
356
  const writeFile = async (inputPath, content) => {
256
- const resolved = resolvePath(inputPath);
357
+ const remotePath = resolvePath(inputPath);
358
+ const resolved = await resolveWritableSandboxPath({
359
+ remotePath,
360
+ inputPath
361
+ });
257
362
  await sandbox.writeTextFile({ path: resolved, content });
258
363
  };
259
364
  const editFile = async (inputPath, oldText, newText) => {
260
- const current = await readFile(inputPath);
365
+ const remotePath = resolvePath(inputPath);
366
+ const resolved = assertWorkspacePath(
367
+ await resolveExistingSandboxPath({
368
+ remotePath,
369
+ inputPath,
370
+ missingMessage: `File not found: ${inputPath}`
371
+ })
372
+ );
373
+ const current = await sandbox.readTextFile({ path: resolved });
374
+ if (current == null) {
375
+ throw new Error(`File not found: ${inputPath}`);
376
+ }
261
377
  const index = current.indexOf(oldText);
262
378
  if (index === -1) {
263
379
  throw new Error(`Text to replace was not found in ${inputPath}`);
@@ -279,7 +395,10 @@ function createClineRemoteOps(options) {
279
395
  const onAbort = () => controller.abort();
280
396
  forwardedSignal?.addEventListener("abort", onAbort, { once: true });
281
397
  try {
282
- const result = await runShell(command, { signal: controller.signal });
398
+ const result = await runShell({
399
+ command,
400
+ signal: controller.signal
401
+ });
283
402
  return { output: result.output, exitCode: result.exitCode };
284
403
  } finally {
285
404
  forwardedSignal?.removeEventListener("abort", onAbort);
@@ -289,9 +408,14 @@ function createClineRemoteOps(options) {
289
408
  }
290
409
  },
291
410
  async grep(pattern, input = {}) {
292
- const target = resolvePath(input.path ?? ".");
411
+ const inputPath = input.path ?? ".";
412
+ const remotePath = resolveReadablePath(inputPath);
413
+ const target = await resolveReadableSandboxPath({
414
+ remotePath,
415
+ inputPath
416
+ });
293
417
  const flags = [
294
- "-R",
418
+ "-r",
295
419
  "-n",
296
420
  "--binary-files=without-match",
297
421
  ...input.ignoreCase ? ["-i"] : [],
@@ -300,8 +424,8 @@ function createClineRemoteOps(options) {
300
424
  ...input.glob ? ["--include", input.glob] : []
301
425
  ];
302
426
  const limit = Math.max(1, input.limit ?? 100);
303
- const result = await runShell(
304
- [
427
+ const result = await runShell({
428
+ command: [
305
429
  `if [ ! -e ${shellQuote(
306
430
  target
307
431
  )} ]; then echo "__CLINE_GREP_NOT_FOUND__"; exit 2; fi`,
@@ -309,7 +433,7 @@ function createClineRemoteOps(options) {
309
433
  pattern
310
434
  )} ${shellQuote(target)} 2>/dev/null | head -n ${limit}`
311
435
  ].join("; ")
312
- );
436
+ });
313
437
  const output = result.output.trim();
314
438
  if (output.includes("__CLINE_GREP_NOT_FOUND__")) {
315
439
  throw new Error(`Path not found: ${input.path ?? "."}`);
@@ -317,9 +441,13 @@ function createClineRemoteOps(options) {
317
441
  return output || "No matches found";
318
442
  },
319
443
  async glob(pattern, inputPath = ".", limit = 1e3) {
320
- const target = resolvePath(inputPath);
321
- const result = await runShell(
322
- [
444
+ const remotePath = resolveReadablePath(inputPath);
445
+ const target = await resolveReadableSandboxPath({
446
+ remotePath,
447
+ inputPath
448
+ });
449
+ const result = await runShell({
450
+ command: [
323
451
  `if [ ! -e ${shellQuote(
324
452
  target
325
453
  )} ]; then echo "__CLINE_FIND_NOT_FOUND__"; exit 2; fi`,
@@ -327,7 +455,7 @@ function createClineRemoteOps(options) {
327
455
  target
328
456
  )} -type f -print; else printf '%s\\n' ${shellQuote(target)}; fi`
329
457
  ].join("; ")
330
- );
458
+ });
331
459
  const output = result.output.trim();
332
460
  if (output.includes("__CLINE_FIND_NOT_FOUND__")) {
333
461
  throw new Error(`Path not found: ${inputPath}`);
@@ -344,9 +472,13 @@ function createClineRemoteOps(options) {
344
472
  ).slice(0, limit);
345
473
  },
346
474
  async ls(inputPath = ".", limit = 500) {
347
- const target = resolvePath(inputPath);
348
- const result = await runShell(
349
- [
475
+ const remotePath = resolveReadablePath(inputPath);
476
+ const target = await resolveReadableSandboxPath({
477
+ remotePath,
478
+ inputPath
479
+ });
480
+ const result = await runShell({
481
+ command: [
350
482
  `if [ ! -e ${shellQuote(
351
483
  target
352
484
  )} ]; then echo "__CLINE_LS_NOT_FOUND__"; exit 2; fi`,
@@ -356,7 +488,7 @@ function createClineRemoteOps(options) {
356
488
  `cd ${shellQuote(target)}`,
357
489
  "ls -1Ap"
358
490
  ].join("; ")
359
- );
491
+ });
360
492
  const output = result.output.trim();
361
493
  if (output.includes("__CLINE_LS_NOT_FOUND__")) {
362
494
  throw new Error(`Path not found: ${inputPath}`);
@@ -1056,13 +1188,10 @@ async function createClineSession(input) {
1056
1188
  const activeBuiltinNames = resolveActiveClineBuiltinNames(
1057
1189
  input.builtinToolFiltering
1058
1190
  );
1059
- const ops = createClineRemoteOps({
1060
- sandbox: toolSafeSandboxSession,
1061
- workDir: input.sessionWorkDir
1062
- });
1063
1191
  let skillsSection;
1192
+ let skillRootDir;
1064
1193
  if (input.skills.length > 0) {
1065
- const skillRootDir = await writeClineSkills({
1194
+ skillRootDir = await writeClineSkills({
1066
1195
  sandbox: toolSafeSandboxSession,
1067
1196
  sandboxHomeDir,
1068
1197
  skills: input.skills,
@@ -1072,6 +1201,11 @@ async function createClineSession(input) {
1072
1201
  skillsSection = renderSkillsPromptSection(input.skills, skillRootDir);
1073
1202
  }
1074
1203
  }
1204
+ const ops = createClineRemoteOps({
1205
+ sandbox: toolSafeSandboxSession,
1206
+ workDir: input.sessionWorkDir,
1207
+ ...skillRootDir ? { readableRoots: [{ sandboxDir: skillRootDir }] } : {}
1208
+ });
1075
1209
  const baseSystemPrompt = buildSystemPrompt({
1076
1210
  sessionWorkDir: input.sessionWorkDir,
1077
1211
  sandboxDescription: toolSafeSandboxSession.description,
@@ -1565,7 +1699,7 @@ ${rebuildInput.instructions}` : baseSystemPrompt,
1565
1699
  }
1566
1700
 
1567
1701
  // src/version.ts
1568
- var VERSION = true ? "1.0.6" : "0.0.0-test";
1702
+ var VERSION = true ? "1.0.8" : "0.0.0-test";
1569
1703
 
1570
1704
  // src/cline-harness.ts
1571
1705
  var CLINE_CLIENT_APP = `ai-sdk/harness-cline/${VERSION}`;