@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 +21 -0
- package/dist/index.js +162 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cline-remote-ops.ts +204 -26
- package/src/cline-session.ts +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-cline",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@cline/agents": "^0.0.72",
|
|
30
30
|
"@cline/core": "^0.0.72",
|
|
31
|
-
"@ai-sdk/harness": "1.0.
|
|
31
|
+
"@ai-sdk/harness": "1.0.81",
|
|
32
32
|
"@ai-sdk/provider": "4.0.7",
|
|
33
33
|
"@ai-sdk/provider-utils": "5.0.28"
|
|
34
34
|
},
|
package/src/cline-remote-ops.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ClineRemoteOpsOptions {
|
|
|
14
14
|
readonly sandbox: Experimental_SandboxSession;
|
|
15
15
|
/** Absolute sandbox path of the session's working directory. */
|
|
16
16
|
readonly workDir: string;
|
|
17
|
+
readonly readableRoots?: ReadonlyArray<{ readonly sandboxDir: string }>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface ClineRemoteOps {
|
|
@@ -48,24 +49,75 @@ export function createClineRemoteOps(
|
|
|
48
49
|
options: ClineRemoteOpsOptions,
|
|
49
50
|
): ClineRemoteOps {
|
|
50
51
|
const { sandbox, workDir } = options;
|
|
52
|
+
const normalizedWorkDir = path.posix.normalize(workDir);
|
|
53
|
+
const readableRoots =
|
|
54
|
+
options.readableRoots?.map(root => path.posix.normalize(root.sandboxDir)) ??
|
|
55
|
+
[];
|
|
56
|
+
|
|
57
|
+
const isInsidePath = ({
|
|
58
|
+
parent,
|
|
59
|
+
candidate,
|
|
60
|
+
}: {
|
|
61
|
+
parent: string;
|
|
62
|
+
candidate: string;
|
|
63
|
+
}): boolean => {
|
|
64
|
+
const relative = path.posix.relative(parent, candidate);
|
|
65
|
+
return (
|
|
66
|
+
relative === '' ||
|
|
67
|
+
(relative !== '..' &&
|
|
68
|
+
!relative.startsWith('../') &&
|
|
69
|
+
!path.posix.isAbsolute(relative))
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const assertWorkspacePath = (inputPath: string): string => {
|
|
74
|
+
const normalized = path.posix.normalize(inputPath);
|
|
75
|
+
if (!isInsidePath({ parent: normalizedWorkDir, candidate: normalized })) {
|
|
76
|
+
throw new Error(`Cline path escapes the workspace: ${inputPath}`);
|
|
77
|
+
}
|
|
78
|
+
return normalized;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const assertReadablePath = (inputPath: string): string => {
|
|
82
|
+
const normalized = path.posix.normalize(inputPath);
|
|
83
|
+
if (
|
|
84
|
+
!isInsidePath({ parent: normalizedWorkDir, candidate: normalized }) &&
|
|
85
|
+
!readableRoots.some(parent =>
|
|
86
|
+
isInsidePath({ parent, candidate: normalized }),
|
|
87
|
+
)
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(`Cline path escapes the readable roots: ${inputPath}`);
|
|
90
|
+
}
|
|
91
|
+
return normalized;
|
|
92
|
+
};
|
|
51
93
|
|
|
52
94
|
const resolvePath = (inputPath: string): string => {
|
|
53
|
-
const
|
|
95
|
+
const resolved = path.posix.isAbsolute(inputPath)
|
|
54
96
|
? path.posix.normalize(inputPath)
|
|
55
|
-
: path.posix.normalize(path.posix.join(
|
|
56
|
-
return
|
|
97
|
+
: path.posix.normalize(path.posix.join(normalizedWorkDir, inputPath));
|
|
98
|
+
return assertWorkspacePath(resolved);
|
|
57
99
|
};
|
|
58
100
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
const resolveReadablePath = (inputPath: string): string => {
|
|
102
|
+
if (!path.posix.isAbsolute(inputPath)) {
|
|
103
|
+
return resolvePath(inputPath);
|
|
104
|
+
}
|
|
105
|
+
return assertReadablePath(inputPath);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const runShell = async ({
|
|
109
|
+
command,
|
|
110
|
+
signal,
|
|
111
|
+
}: {
|
|
112
|
+
command: string;
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}): Promise<{ exitCode: number; output: string }> => {
|
|
63
115
|
// `sandbox.run({ command })` already wraps in a shell; interpolated
|
|
64
116
|
// paths/values inside `command` are quoted with `shellQuote` by callers.
|
|
65
117
|
const result = await sandbox.run({
|
|
66
118
|
command,
|
|
67
|
-
workingDirectory:
|
|
68
|
-
...(
|
|
119
|
+
workingDirectory: normalizedWorkDir,
|
|
120
|
+
...(signal ? { abortSignal: signal } : {}),
|
|
69
121
|
});
|
|
70
122
|
return {
|
|
71
123
|
exitCode: result.exitCode,
|
|
@@ -73,8 +125,103 @@ export function createClineRemoteOps(
|
|
|
73
125
|
};
|
|
74
126
|
};
|
|
75
127
|
|
|
128
|
+
const lastOutputLine = (output: string): string | undefined =>
|
|
129
|
+
output.trim().split('\n').filter(Boolean).at(-1);
|
|
130
|
+
|
|
131
|
+
const resolveExistingSandboxPath = async ({
|
|
132
|
+
remotePath,
|
|
133
|
+
inputPath,
|
|
134
|
+
missingMessage,
|
|
135
|
+
}: {
|
|
136
|
+
remotePath: string;
|
|
137
|
+
inputPath: string;
|
|
138
|
+
missingMessage?: string;
|
|
139
|
+
}): Promise<string> => {
|
|
140
|
+
const result = await runShell({
|
|
141
|
+
command: [
|
|
142
|
+
`target=${shellQuote(remotePath)}`,
|
|
143
|
+
'if [ ! -e "$target" ]; then echo "__CLINE_REALPATH_NOT_FOUND__"; exit 2; fi',
|
|
144
|
+
'resolved=$(realpath "$target" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
|
|
145
|
+
'printf \'%s\\n\' "$resolved"',
|
|
146
|
+
].join('; '),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result.output.includes('__CLINE_REALPATH_NOT_FOUND__')) {
|
|
150
|
+
throw new Error(missingMessage ?? `Path not found: ${inputPath}`);
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
result.output.includes('__CLINE_REALPATH_FAILED__') ||
|
|
154
|
+
result.exitCode !== 0
|
|
155
|
+
) {
|
|
156
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const resolvedPath = lastOutputLine(result.output);
|
|
160
|
+
if (!resolvedPath) {
|
|
161
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
162
|
+
}
|
|
163
|
+
return resolvedPath;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const resolveReadableSandboxPath = async ({
|
|
167
|
+
remotePath,
|
|
168
|
+
inputPath,
|
|
169
|
+
missingMessage,
|
|
170
|
+
}: {
|
|
171
|
+
remotePath: string;
|
|
172
|
+
inputPath: string;
|
|
173
|
+
missingMessage?: string;
|
|
174
|
+
}): Promise<string> =>
|
|
175
|
+
assertReadablePath(
|
|
176
|
+
await resolveExistingSandboxPath({
|
|
177
|
+
remotePath,
|
|
178
|
+
inputPath,
|
|
179
|
+
...(missingMessage ? { missingMessage } : {}),
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const resolveWritableSandboxPath = async ({
|
|
184
|
+
remotePath,
|
|
185
|
+
inputPath,
|
|
186
|
+
}: {
|
|
187
|
+
remotePath: string;
|
|
188
|
+
inputPath: string;
|
|
189
|
+
}): Promise<string> => {
|
|
190
|
+
const result = await runShell({
|
|
191
|
+
command: [
|
|
192
|
+
`target=${shellQuote(remotePath)}`,
|
|
193
|
+
'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',
|
|
194
|
+
'dir=$(dirname "$target")',
|
|
195
|
+
'base=$(basename "$target")',
|
|
196
|
+
'missing="$base"',
|
|
197
|
+
'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',
|
|
198
|
+
'resolved_dir=$(realpath "$dir" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
|
|
199
|
+
'printf \'%s/%s\\n\' "$resolved_dir" "$missing"',
|
|
200
|
+
].join('; '),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
result.output.includes('__CLINE_REALPATH_NOT_FOUND__') ||
|
|
205
|
+
result.output.includes('__CLINE_REALPATH_FAILED__') ||
|
|
206
|
+
result.exitCode !== 0
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const resolvedPath = lastOutputLine(result.output);
|
|
212
|
+
if (!resolvedPath) {
|
|
213
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
214
|
+
}
|
|
215
|
+
return assertWorkspacePath(resolvedPath);
|
|
216
|
+
};
|
|
217
|
+
|
|
76
218
|
const readFile = async (inputPath: string): Promise<string> => {
|
|
77
|
-
const
|
|
219
|
+
const remotePath = resolveReadablePath(inputPath);
|
|
220
|
+
const resolved = await resolveReadableSandboxPath({
|
|
221
|
+
remotePath,
|
|
222
|
+
inputPath,
|
|
223
|
+
missingMessage: `File not found: ${inputPath}`,
|
|
224
|
+
});
|
|
78
225
|
const content = await sandbox.readTextFile({ path: resolved });
|
|
79
226
|
if (content == null) {
|
|
80
227
|
throw new Error(`File not found: ${inputPath}`);
|
|
@@ -86,7 +233,11 @@ export function createClineRemoteOps(
|
|
|
86
233
|
inputPath: string,
|
|
87
234
|
content: string,
|
|
88
235
|
): Promise<void> => {
|
|
89
|
-
const
|
|
236
|
+
const remotePath = resolvePath(inputPath);
|
|
237
|
+
const resolved = await resolveWritableSandboxPath({
|
|
238
|
+
remotePath,
|
|
239
|
+
inputPath,
|
|
240
|
+
});
|
|
90
241
|
// `writeTextFile` creates parent directories recursively per the
|
|
91
242
|
// SandboxSession contract, so no explicit mkdir is needed.
|
|
92
243
|
await sandbox.writeTextFile({ path: resolved, content });
|
|
@@ -97,7 +248,18 @@ export function createClineRemoteOps(
|
|
|
97
248
|
oldText: string,
|
|
98
249
|
newText: string,
|
|
99
250
|
): Promise<void> => {
|
|
100
|
-
const
|
|
251
|
+
const remotePath = resolvePath(inputPath);
|
|
252
|
+
const resolved = assertWorkspacePath(
|
|
253
|
+
await resolveExistingSandboxPath({
|
|
254
|
+
remotePath,
|
|
255
|
+
inputPath,
|
|
256
|
+
missingMessage: `File not found: ${inputPath}`,
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
const current = await sandbox.readTextFile({ path: resolved });
|
|
260
|
+
if (current == null) {
|
|
261
|
+
throw new Error(`File not found: ${inputPath}`);
|
|
262
|
+
}
|
|
101
263
|
const index = current.indexOf(oldText);
|
|
102
264
|
if (index === -1) {
|
|
103
265
|
throw new Error(`Text to replace was not found in ${inputPath}`);
|
|
@@ -128,7 +290,10 @@ export function createClineRemoteOps(
|
|
|
128
290
|
forwardedSignal?.addEventListener('abort', onAbort, { once: true });
|
|
129
291
|
|
|
130
292
|
try {
|
|
131
|
-
const result = await runShell(
|
|
293
|
+
const result = await runShell({
|
|
294
|
+
command,
|
|
295
|
+
signal: controller.signal,
|
|
296
|
+
});
|
|
132
297
|
return { output: result.output, exitCode: result.exitCode };
|
|
133
298
|
} finally {
|
|
134
299
|
forwardedSignal?.removeEventListener('abort', onAbort);
|
|
@@ -139,9 +304,14 @@ export function createClineRemoteOps(
|
|
|
139
304
|
},
|
|
140
305
|
|
|
141
306
|
async grep(pattern, input = {}) {
|
|
142
|
-
const
|
|
307
|
+
const inputPath = input.path ?? '.';
|
|
308
|
+
const remotePath = resolveReadablePath(inputPath);
|
|
309
|
+
const target = await resolveReadableSandboxPath({
|
|
310
|
+
remotePath,
|
|
311
|
+
inputPath,
|
|
312
|
+
});
|
|
143
313
|
const flags = [
|
|
144
|
-
'-
|
|
314
|
+
'-r',
|
|
145
315
|
'-n',
|
|
146
316
|
'--binary-files=without-match',
|
|
147
317
|
...(input.ignoreCase ? ['-i'] : []),
|
|
@@ -152,8 +322,8 @@ export function createClineRemoteOps(
|
|
|
152
322
|
...(input.glob ? ['--include', input.glob] : []),
|
|
153
323
|
];
|
|
154
324
|
const limit = Math.max(1, input.limit ?? 100);
|
|
155
|
-
const result = await runShell(
|
|
156
|
-
[
|
|
325
|
+
const result = await runShell({
|
|
326
|
+
command: [
|
|
157
327
|
`if [ ! -e ${shellQuote(
|
|
158
328
|
target,
|
|
159
329
|
)} ]; then echo "__CLINE_GREP_NOT_FOUND__"; exit 2; fi`,
|
|
@@ -161,7 +331,7 @@ export function createClineRemoteOps(
|
|
|
161
331
|
pattern,
|
|
162
332
|
)} ${shellQuote(target)} 2>/dev/null | head -n ${limit}`,
|
|
163
333
|
].join('; '),
|
|
164
|
-
);
|
|
334
|
+
});
|
|
165
335
|
|
|
166
336
|
const output = result.output.trim();
|
|
167
337
|
if (output.includes('__CLINE_GREP_NOT_FOUND__')) {
|
|
@@ -171,9 +341,13 @@ export function createClineRemoteOps(
|
|
|
171
341
|
},
|
|
172
342
|
|
|
173
343
|
async glob(pattern, inputPath = '.', limit = 1_000) {
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
344
|
+
const remotePath = resolveReadablePath(inputPath);
|
|
345
|
+
const target = await resolveReadableSandboxPath({
|
|
346
|
+
remotePath,
|
|
347
|
+
inputPath,
|
|
348
|
+
});
|
|
349
|
+
const result = await runShell({
|
|
350
|
+
command: [
|
|
177
351
|
`if [ ! -e ${shellQuote(
|
|
178
352
|
target,
|
|
179
353
|
)} ]; then echo "__CLINE_FIND_NOT_FOUND__"; exit 2; fi`,
|
|
@@ -181,7 +355,7 @@ export function createClineRemoteOps(
|
|
|
181
355
|
target,
|
|
182
356
|
)} -type f -print; else printf '%s\\n' ${shellQuote(target)}; fi`,
|
|
183
357
|
].join('; '),
|
|
184
|
-
);
|
|
358
|
+
});
|
|
185
359
|
|
|
186
360
|
const output = result.output.trim();
|
|
187
361
|
if (output.includes('__CLINE_FIND_NOT_FOUND__')) {
|
|
@@ -208,9 +382,13 @@ export function createClineRemoteOps(
|
|
|
208
382
|
},
|
|
209
383
|
|
|
210
384
|
async ls(inputPath = '.', limit = 500) {
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
385
|
+
const remotePath = resolveReadablePath(inputPath);
|
|
386
|
+
const target = await resolveReadableSandboxPath({
|
|
387
|
+
remotePath,
|
|
388
|
+
inputPath,
|
|
389
|
+
});
|
|
390
|
+
const result = await runShell({
|
|
391
|
+
command: [
|
|
214
392
|
`if [ ! -e ${shellQuote(
|
|
215
393
|
target,
|
|
216
394
|
)} ]; then echo "__CLINE_LS_NOT_FOUND__"; exit 2; fi`,
|
|
@@ -220,7 +398,7 @@ export function createClineRemoteOps(
|
|
|
220
398
|
`cd ${shellQuote(target)}`,
|
|
221
399
|
'ls -1Ap',
|
|
222
400
|
].join('; '),
|
|
223
|
-
);
|
|
401
|
+
});
|
|
224
402
|
|
|
225
403
|
const output = result.output.trim();
|
|
226
404
|
if (output.includes('__CLINE_LS_NOT_FOUND__')) {
|
package/src/cline-session.ts
CHANGED
|
@@ -268,16 +268,12 @@ export async function createClineSession(
|
|
|
268
268
|
input.builtinToolFiltering,
|
|
269
269
|
);
|
|
270
270
|
|
|
271
|
-
const ops = createClineRemoteOps({
|
|
272
|
-
sandbox: toolSafeSandboxSession,
|
|
273
|
-
workDir: input.sessionWorkDir,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
271
|
// Materialize harness-provided skills into sandbox HOME (not the workspace)
|
|
277
272
|
// and advertise them via a system prompt section.
|
|
278
273
|
let skillsSection: string | undefined;
|
|
274
|
+
let skillRootDir: string | undefined;
|
|
279
275
|
if (input.skills.length > 0) {
|
|
280
|
-
|
|
276
|
+
skillRootDir = await writeClineSkills({
|
|
281
277
|
sandbox: toolSafeSandboxSession,
|
|
282
278
|
sandboxHomeDir,
|
|
283
279
|
skills: input.skills,
|
|
@@ -288,6 +284,12 @@ export async function createClineSession(
|
|
|
288
284
|
}
|
|
289
285
|
}
|
|
290
286
|
|
|
287
|
+
const ops = createClineRemoteOps({
|
|
288
|
+
sandbox: toolSafeSandboxSession,
|
|
289
|
+
workDir: input.sessionWorkDir,
|
|
290
|
+
...(skillRootDir ? { readableRoots: [{ sandboxDir: skillRootDir }] } : {}),
|
|
291
|
+
});
|
|
292
|
+
|
|
291
293
|
const baseSystemPrompt = buildSystemPrompt({
|
|
292
294
|
sessionWorkDir: input.sessionWorkDir,
|
|
293
295
|
sandboxDescription: toolSafeSandboxSession.description,
|