@ai-sdk/harness-cline 1.0.19 → 1.0.21
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 +28 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +193 -98
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/cline-harness.ts +15 -2
- package/src/cline-remote-ops.ts +5 -29
- package/src/cline-session.ts +65 -52
- package/src/cline-skills.ts +173 -48
- package/src/cline-tools.ts +5 -3
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.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider": "4.0.
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
32
|
-
"@cline/agents": "^0.0.
|
|
33
|
-
"@cline/core": "^0.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.94",
|
|
30
|
+
"@ai-sdk/provider": "4.0.9",
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.34",
|
|
32
|
+
"@cline/agents": "^0.0.79",
|
|
33
|
+
"@cline/core": "^0.0.79"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/cline-harness.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type ClineHarnessSettings = {
|
|
|
37
37
|
/**
|
|
38
38
|
* Model id for the configured provider. When omitted, Cline selects the
|
|
39
39
|
* provider's default model.
|
|
40
|
+
*
|
|
41
|
+
* @deprecated Use `model` on `HarnessAgent` instead.
|
|
40
42
|
*/
|
|
41
43
|
readonly modelId?: string;
|
|
42
44
|
/**
|
|
@@ -140,6 +142,18 @@ const CLINE_BUILTIN_TOOLS = {
|
|
|
140
142
|
nativeName: 'ls',
|
|
141
143
|
toolUseKind: 'readonly',
|
|
142
144
|
} as HarnessV1BuiltinTool,
|
|
145
|
+
skills: {
|
|
146
|
+
...tool({
|
|
147
|
+
description: 'Execute a configured skill by name.',
|
|
148
|
+
inputSchema: z.object({
|
|
149
|
+
skill: z.string(),
|
|
150
|
+
args: z.string().optional(),
|
|
151
|
+
}),
|
|
152
|
+
outputSchema: z.string(),
|
|
153
|
+
}),
|
|
154
|
+
nativeName: 'skills',
|
|
155
|
+
toolUseKind: 'readonly',
|
|
156
|
+
} as HarnessV1BuiltinTool,
|
|
143
157
|
} as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
|
|
144
158
|
|
|
145
159
|
export function createCline(
|
|
@@ -163,14 +177,13 @@ export function createCline(
|
|
|
163
177
|
sessionId: startOpts.sessionId,
|
|
164
178
|
sandboxSession: startOpts.sandboxSession,
|
|
165
179
|
sessionWorkDir: startOpts.sessionWorkDir,
|
|
166
|
-
skills: startOpts.skills ?? [],
|
|
167
180
|
settings: {
|
|
168
181
|
authEnv,
|
|
169
182
|
isAuthenticationEnvironmentOverride:
|
|
170
183
|
isHarnessAuthenticationEnvironment(settings.auth),
|
|
171
184
|
...(settings.mcpServers ? { mcpServers: settings.mcpServers } : {}),
|
|
172
185
|
...(settings.providerId ? { providerId: settings.providerId } : {}),
|
|
173
|
-
...(settings.modelId ? { modelId: settings.modelId }
|
|
186
|
+
...(settings.modelId == null ? {} : { modelId: settings.modelId }),
|
|
174
187
|
...(settings.apiKey ? { apiKey: settings.apiKey } : {}),
|
|
175
188
|
...(settings.baseUrl ? { baseUrl: settings.baseUrl } : {}),
|
|
176
189
|
...(settings.headers ? { headers: settings.headers } : {}),
|
package/src/cline-remote-ops.ts
CHANGED
|
@@ -14,7 +14,6 @@ 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 }>;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export interface ClineRemoteOps {
|
|
@@ -50,9 +49,6 @@ export function createClineRemoteOps(
|
|
|
50
49
|
): ClineRemoteOps {
|
|
51
50
|
const { sandbox, workDir } = options;
|
|
52
51
|
const normalizedWorkDir = path.posix.normalize(workDir);
|
|
53
|
-
const readableRoots =
|
|
54
|
-
options.readableRoots?.map(root => path.posix.normalize(root.sandboxDir)) ??
|
|
55
|
-
[];
|
|
56
52
|
|
|
57
53
|
const isInsidePath = ({
|
|
58
54
|
parent,
|
|
@@ -78,19 +74,6 @@ export function createClineRemoteOps(
|
|
|
78
74
|
return normalized;
|
|
79
75
|
};
|
|
80
76
|
|
|
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
|
-
};
|
|
93
|
-
|
|
94
77
|
const resolvePath = (inputPath: string): string => {
|
|
95
78
|
const resolved = path.posix.isAbsolute(inputPath)
|
|
96
79
|
? path.posix.normalize(inputPath)
|
|
@@ -98,13 +81,6 @@ export function createClineRemoteOps(
|
|
|
98
81
|
return assertWorkspacePath(resolved);
|
|
99
82
|
};
|
|
100
83
|
|
|
101
|
-
const resolveReadablePath = (inputPath: string): string => {
|
|
102
|
-
if (!path.posix.isAbsolute(inputPath)) {
|
|
103
|
-
return resolvePath(inputPath);
|
|
104
|
-
}
|
|
105
|
-
return assertReadablePath(inputPath);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
84
|
const runShell = async ({
|
|
109
85
|
command,
|
|
110
86
|
signal,
|
|
@@ -172,7 +148,7 @@ export function createClineRemoteOps(
|
|
|
172
148
|
inputPath: string;
|
|
173
149
|
missingMessage?: string;
|
|
174
150
|
}): Promise<string> =>
|
|
175
|
-
|
|
151
|
+
assertWorkspacePath(
|
|
176
152
|
await resolveExistingSandboxPath({
|
|
177
153
|
remotePath,
|
|
178
154
|
inputPath,
|
|
@@ -216,7 +192,7 @@ export function createClineRemoteOps(
|
|
|
216
192
|
};
|
|
217
193
|
|
|
218
194
|
const readFile = async (inputPath: string): Promise<string> => {
|
|
219
|
-
const remotePath =
|
|
195
|
+
const remotePath = resolvePath(inputPath);
|
|
220
196
|
const resolved = await resolveReadableSandboxPath({
|
|
221
197
|
remotePath,
|
|
222
198
|
inputPath,
|
|
@@ -305,7 +281,7 @@ export function createClineRemoteOps(
|
|
|
305
281
|
|
|
306
282
|
async grep(pattern, input = {}) {
|
|
307
283
|
const inputPath = input.path ?? '.';
|
|
308
|
-
const remotePath =
|
|
284
|
+
const remotePath = resolvePath(inputPath);
|
|
309
285
|
const target = await resolveReadableSandboxPath({
|
|
310
286
|
remotePath,
|
|
311
287
|
inputPath,
|
|
@@ -341,7 +317,7 @@ export function createClineRemoteOps(
|
|
|
341
317
|
},
|
|
342
318
|
|
|
343
319
|
async glob(pattern, inputPath = '.', limit = 1_000) {
|
|
344
|
-
const remotePath =
|
|
320
|
+
const remotePath = resolvePath(inputPath);
|
|
345
321
|
const target = await resolveReadableSandboxPath({
|
|
346
322
|
remotePath,
|
|
347
323
|
inputPath,
|
|
@@ -382,7 +358,7 @@ export function createClineRemoteOps(
|
|
|
382
358
|
},
|
|
383
359
|
|
|
384
360
|
async ls(inputPath = '.', limit = 500) {
|
|
385
|
-
const remotePath =
|
|
361
|
+
const remotePath = resolvePath(inputPath);
|
|
386
362
|
const target = await resolveReadableSandboxPath({
|
|
387
363
|
remotePath,
|
|
388
364
|
inputPath,
|
package/src/cline-session.ts
CHANGED
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
resolveClinePrivateSessionDirectory,
|
|
36
36
|
safeClineHistoryFileName,
|
|
37
37
|
} from './cline-resume-state';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
createClineSkillsRuntime,
|
|
40
|
+
type ClineSkillsRuntime,
|
|
41
|
+
} from './cline-skills';
|
|
39
42
|
import {
|
|
40
43
|
buildBuiltinAgentTools,
|
|
41
44
|
buildUserAgentTools,
|
|
@@ -97,7 +100,6 @@ export interface CreateClineSessionInput {
|
|
|
97
100
|
readonly sessionId: string;
|
|
98
101
|
readonly sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
99
102
|
readonly sessionWorkDir: string;
|
|
100
|
-
readonly skills: ReadonlyArray<HarnessV1Skill>;
|
|
101
103
|
readonly settings: ClineSessionSettings;
|
|
102
104
|
readonly clientApp: string;
|
|
103
105
|
readonly isResume: boolean;
|
|
@@ -154,17 +156,12 @@ function clineBuiltinToolRequiresApproval(input: {
|
|
|
154
156
|
function buildSystemPrompt(input: {
|
|
155
157
|
sessionWorkDir: string;
|
|
156
158
|
sandboxDescription: string;
|
|
157
|
-
skillsSection?: string;
|
|
158
159
|
}): string {
|
|
159
|
-
|
|
160
|
+
return [
|
|
160
161
|
'You are Cline, an autonomous coding agent operating inside a sandboxed workspace.',
|
|
161
162
|
`## Workspace\n\nThe workspace root is \`${input.sessionWorkDir}\`. All relative file paths and shell commands resolve against it. Use the provided tools (read, write, edit, bash, grep, glob, ls) to inspect and modify the workspace.`,
|
|
162
163
|
`## Sandbox\n\n${input.sandboxDescription}`,
|
|
163
|
-
];
|
|
164
|
-
if (input.skillsSection) {
|
|
165
|
-
sections.push(input.skillsSection);
|
|
166
|
-
}
|
|
167
|
-
return sections.join('\n\n');
|
|
164
|
+
].join('\n\n');
|
|
168
165
|
}
|
|
169
166
|
|
|
170
167
|
function toClineBackendProviderBaseUrl(baseUrl: string): string {
|
|
@@ -179,9 +176,11 @@ function toAiGatewayProviderBaseUrl(baseUrl: string): string {
|
|
|
179
176
|
function createClineAgentModel({
|
|
180
177
|
settings,
|
|
181
178
|
clientApp,
|
|
179
|
+
modelId,
|
|
182
180
|
}: {
|
|
183
181
|
settings: ClineSessionSettings;
|
|
184
182
|
clientApp: string;
|
|
183
|
+
modelId: string | undefined;
|
|
185
184
|
}): { model: AgentModel; providerId: string } {
|
|
186
185
|
const gatewayBaseUrl = settings.authEnv.AI_GATEWAY_BASE_URL;
|
|
187
186
|
const isAiGateway = gatewayBaseUrl != null;
|
|
@@ -218,7 +217,7 @@ function createClineAgentModel({
|
|
|
218
217
|
});
|
|
219
218
|
const modelSelection = {
|
|
220
219
|
providerId,
|
|
221
|
-
...(
|
|
220
|
+
...(modelId ? { modelId } : {}),
|
|
222
221
|
};
|
|
223
222
|
const modelOptions =
|
|
224
223
|
settings.reasoningEffort === 'none'
|
|
@@ -271,32 +270,9 @@ export async function createClineSession(
|
|
|
271
270
|
input.builtinToolFiltering,
|
|
272
271
|
);
|
|
273
272
|
|
|
274
|
-
// Materialize harness-provided skills into sandbox HOME (not the workspace)
|
|
275
|
-
// and advertise them via a system prompt section.
|
|
276
|
-
let skillsSection: string | undefined;
|
|
277
|
-
let skillRootDir: string | undefined;
|
|
278
|
-
if (input.skills.length > 0) {
|
|
279
|
-
skillRootDir = await writeClineSkills({
|
|
280
|
-
sandbox: toolSafeSandboxSession,
|
|
281
|
-
sandboxHomeDir,
|
|
282
|
-
skills: input.skills,
|
|
283
|
-
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
284
|
-
});
|
|
285
|
-
if (skillRootDir) {
|
|
286
|
-
skillsSection = renderSkillsPromptSection(input.skills, skillRootDir);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
273
|
const ops = createClineRemoteOps({
|
|
291
274
|
sandbox: toolSafeSandboxSession,
|
|
292
275
|
workDir: input.sessionWorkDir,
|
|
293
|
-
...(skillRootDir ? { readableRoots: [{ sandboxDir: skillRootDir }] } : {}),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const baseSystemPrompt = buildSystemPrompt({
|
|
297
|
-
sessionWorkDir: input.sessionWorkDir,
|
|
298
|
-
sandboxDescription: toolSafeSandboxSession.description,
|
|
299
|
-
...(skillsSection ? { skillsSection } : {}),
|
|
300
276
|
});
|
|
301
277
|
|
|
302
278
|
// On resume: pull the persisted conversation history from the sandbox so
|
|
@@ -316,15 +292,17 @@ export async function createClineSession(
|
|
|
316
292
|
const mcpRuntime = await createClineMcpRuntime({
|
|
317
293
|
mcpServers: input.settings.mcpServers,
|
|
318
294
|
});
|
|
319
|
-
|
|
295
|
+
let activeModelId = input.settings.modelId;
|
|
296
|
+
let agentModel = createClineAgentModel({
|
|
320
297
|
settings: input.settings,
|
|
321
298
|
clientApp: input.clientApp,
|
|
299
|
+
modelId: activeModelId,
|
|
322
300
|
});
|
|
323
301
|
|
|
324
302
|
// Per-session mutable state we hold across prompts.
|
|
325
303
|
let agent: Agent | undefined;
|
|
326
304
|
let unsubscribe: (() => void) | undefined;
|
|
327
|
-
let
|
|
305
|
+
let lastAgentConfigurationSignature: string | undefined;
|
|
328
306
|
let agentHasRun = false;
|
|
329
307
|
let stopped = false;
|
|
330
308
|
/*
|
|
@@ -404,25 +382,32 @@ export async function createClineSession(
|
|
|
404
382
|
|
|
405
383
|
function rebuildAgent(rebuildInput: {
|
|
406
384
|
userTools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
385
|
+
skillsRuntime: ClineSkillsRuntime;
|
|
407
386
|
instructions?: string;
|
|
408
387
|
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
409
388
|
}): void {
|
|
410
389
|
unsubscribe?.();
|
|
411
390
|
unsubscribe = undefined;
|
|
412
391
|
agent = new Agent({
|
|
413
|
-
model,
|
|
414
|
-
...(
|
|
392
|
+
model: agentModel.model,
|
|
393
|
+
...(activeModelId
|
|
415
394
|
? {
|
|
416
395
|
messageModelInfo: {
|
|
417
|
-
id:
|
|
418
|
-
provider: providerId,
|
|
396
|
+
id: activeModelId,
|
|
397
|
+
provider: agentModel.providerId,
|
|
419
398
|
},
|
|
420
399
|
}
|
|
421
400
|
: {}),
|
|
422
401
|
sessionId: input.sessionId,
|
|
423
|
-
systemPrompt:
|
|
424
|
-
|
|
425
|
-
|
|
402
|
+
systemPrompt: (() => {
|
|
403
|
+
const baseSystemPrompt = buildSystemPrompt({
|
|
404
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
405
|
+
sandboxDescription: toolSafeSandboxSession.description,
|
|
406
|
+
});
|
|
407
|
+
return rebuildInput.instructions
|
|
408
|
+
? `${baseSystemPrompt}\n\n${rebuildInput.instructions}`
|
|
409
|
+
: baseSystemPrompt;
|
|
410
|
+
})(),
|
|
426
411
|
initialMessages: currentMessages,
|
|
427
412
|
toolExecution: 'parallel',
|
|
428
413
|
...(input.settings.maxIterations !== undefined
|
|
@@ -430,6 +415,10 @@ export async function createClineSession(
|
|
|
430
415
|
: {}),
|
|
431
416
|
tools: [
|
|
432
417
|
...buildBuiltinAgentTools({ ops, activeNames: activeBuiltinNames }),
|
|
418
|
+
...(rebuildInput.skillsRuntime.tool &&
|
|
419
|
+
activeBuiltinNames.includes('skills')
|
|
420
|
+
? [rebuildInput.skillsRuntime.tool]
|
|
421
|
+
: []),
|
|
433
422
|
...mcpRuntime.tools,
|
|
434
423
|
...buildUserAgentTools({
|
|
435
424
|
specs: rebuildInput.userTools,
|
|
@@ -572,19 +561,24 @@ export async function createClineSession(
|
|
|
572
561
|
* Shared by `doPromptTurn` (a fresh user prompt) and `doContinueTurn`
|
|
573
562
|
* (no prompt — the runtime continues its own thread after a rerun resume).
|
|
574
563
|
*/
|
|
575
|
-
function runTurn(turnOpts: {
|
|
564
|
+
async function runTurn(turnOpts: {
|
|
576
565
|
text: string | undefined;
|
|
566
|
+
model?: string;
|
|
567
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
577
568
|
tools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
578
569
|
instructions?: string;
|
|
579
570
|
emit: (part: HarnessV1StreamPart) => void;
|
|
580
571
|
abortSignal?: AbortSignal;
|
|
581
572
|
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
582
|
-
}): HarnessV1PromptControl {
|
|
573
|
+
}): Promise<HarnessV1PromptControl> {
|
|
583
574
|
if (stopped) {
|
|
584
575
|
throw new Error('Cline session has been stopped.');
|
|
585
576
|
}
|
|
586
577
|
|
|
587
578
|
const userTools = turnOpts.tools;
|
|
579
|
+
const skillsRuntime = createClineSkillsRuntime({
|
|
580
|
+
skills: turnOpts.skills,
|
|
581
|
+
});
|
|
588
582
|
if (
|
|
589
583
|
turnOpts.responseFormat?.type === 'json' &&
|
|
590
584
|
turnOpts.responseFormat.schema == null
|
|
@@ -597,7 +591,7 @@ export async function createClineSession(
|
|
|
597
591
|
}
|
|
598
592
|
if (
|
|
599
593
|
turnOpts.responseFormat?.type === 'json' &&
|
|
600
|
-
providerId === 'openai-codex-cli'
|
|
594
|
+
agentModel.providerId === 'openai-codex-cli'
|
|
601
595
|
) {
|
|
602
596
|
throw new HarnessCapabilityUnsupportedError({
|
|
603
597
|
message:
|
|
@@ -606,26 +600,42 @@ export async function createClineSession(
|
|
|
606
600
|
});
|
|
607
601
|
}
|
|
608
602
|
|
|
603
|
+
if (turnOpts.model != null && turnOpts.model !== activeModelId) {
|
|
604
|
+
activeModelId = turnOpts.model;
|
|
605
|
+
agentModel = createClineAgentModel({
|
|
606
|
+
settings: input.settings,
|
|
607
|
+
clientApp: input.clientApp,
|
|
608
|
+
modelId: activeModelId,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
609
612
|
const signature = JSON.stringify({
|
|
610
|
-
|
|
613
|
+
modelId: activeModelId,
|
|
614
|
+
tools: userTools,
|
|
611
615
|
responseFormat: turnOpts.responseFormat,
|
|
616
|
+
instructions: turnOpts.instructions,
|
|
617
|
+
skills: skillsRuntime.signature,
|
|
612
618
|
});
|
|
613
|
-
if (agent == null || signature !==
|
|
619
|
+
if (agent == null || signature !== lastAgentConfigurationSignature) {
|
|
614
620
|
currentMessages = agent?.snapshot().messages ?? currentMessages;
|
|
615
621
|
rebuildAgent({
|
|
616
622
|
userTools,
|
|
623
|
+
skillsRuntime,
|
|
617
624
|
responseFormat: turnOpts.responseFormat,
|
|
618
625
|
...(turnOpts.instructions
|
|
619
626
|
? { instructions: turnOpts.instructions }
|
|
620
627
|
: {}),
|
|
621
628
|
});
|
|
622
|
-
|
|
629
|
+
lastAgentConfigurationSignature = signature;
|
|
623
630
|
}
|
|
624
631
|
|
|
625
632
|
currentEmit = turnOpts.emit;
|
|
633
|
+
const userToolNames = new Set(userTools.map(tool => tool.name));
|
|
626
634
|
translatorState = createClineTranslatorState({
|
|
627
|
-
builtinToolNames: activeBuiltinNames
|
|
628
|
-
|
|
635
|
+
builtinToolNames: activeBuiltinNames.filter(
|
|
636
|
+
name => !userToolNames.has(name),
|
|
637
|
+
),
|
|
638
|
+
hostToolNames: userToolNames,
|
|
629
639
|
ignoredToolNames:
|
|
630
640
|
turnOpts.responseFormat?.type === 'json' ? ['structured_output'] : [],
|
|
631
641
|
mcpToolNames: mcpRuntime.toolNames,
|
|
@@ -633,7 +643,7 @@ export async function createClineSession(
|
|
|
633
643
|
|
|
634
644
|
turnOpts.emit({
|
|
635
645
|
type: 'stream-start',
|
|
636
|
-
...(
|
|
646
|
+
...(activeModelId ? { modelId: activeModelId } : {}),
|
|
637
647
|
});
|
|
638
648
|
|
|
639
649
|
acceptingUserMessages = true;
|
|
@@ -798,7 +808,6 @@ export async function createClineSession(
|
|
|
798
808
|
const sessionImpl: HarnessV1Session = {
|
|
799
809
|
sessionId: input.sessionId,
|
|
800
810
|
isResume: input.isResume,
|
|
801
|
-
...(input.settings.modelId ? { modelId: input.settings.modelId } : {}),
|
|
802
811
|
|
|
803
812
|
// The Cline runtime has no bridge to attach to and no in-sandbox event
|
|
804
813
|
// log to replay; its only cross-process resume path is restoring the
|
|
@@ -809,6 +818,8 @@ export async function createClineSession(
|
|
|
809
818
|
): Promise<HarnessV1PromptControl> => {
|
|
810
819
|
return runTurn({
|
|
811
820
|
text: extractUserText(promptOpts.prompt),
|
|
821
|
+
...(promptOpts.model ? { model: promptOpts.model } : {}),
|
|
822
|
+
skills: promptOpts.skills,
|
|
812
823
|
tools: promptOpts.tools ?? [],
|
|
813
824
|
...(promptOpts.instructions
|
|
814
825
|
? { instructions: promptOpts.instructions }
|
|
@@ -843,6 +854,8 @@ export async function createClineSession(
|
|
|
843
854
|
*/
|
|
844
855
|
return runTurn({
|
|
845
856
|
text: undefined,
|
|
857
|
+
...(continueOpts.model ? { model: continueOpts.model } : {}),
|
|
858
|
+
skills: continueOpts.skills,
|
|
846
859
|
tools: continueOpts.tools ?? [],
|
|
847
860
|
...(continueOpts.instructions
|
|
848
861
|
? { instructions: continueOpts.instructions }
|
package/src/cline-skills.ts
CHANGED
|
@@ -1,59 +1,184 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import type { HarnessV1Skill } from '@ai-sdk/harness';
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
3
|
+
import type { AgentTool } from '@cline/agents';
|
|
4
|
+
import { createDefaultTools, type ToolExecutors } from '@cline/core';
|
|
5
|
+
|
|
6
|
+
const CLINE_SKILL_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
7
|
+
|
|
8
|
+
type ProjectedClineSkill = {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly description: string;
|
|
12
|
+
readonly content: string;
|
|
13
|
+
readonly files: ReadonlyArray<{
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly content: string;
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ClineSkillsRuntime = {
|
|
20
|
+
readonly signature: string;
|
|
21
|
+
readonly tool?: AgentTool;
|
|
22
|
+
};
|
|
5
23
|
|
|
6
24
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
* prompt section (see `renderSkillsPromptSection`) that points at these files.
|
|
25
|
+
* Build Cline's native skills tool around an immutable in-host projection of
|
|
26
|
+
* the harness skills. The low-level Agent does not discover skills itself, so
|
|
27
|
+
* the adapter provides the same tool contract that Cline's core orchestration
|
|
28
|
+
* layer uses without involving the sandbox filesystem.
|
|
12
29
|
*/
|
|
13
|
-
export
|
|
14
|
-
sandbox,
|
|
15
|
-
sandboxHomeDir,
|
|
30
|
+
export function createClineSkillsRuntime({
|
|
16
31
|
skills,
|
|
17
|
-
abortSignal,
|
|
18
32
|
}: {
|
|
19
|
-
sandbox: Experimental_SandboxSession;
|
|
20
|
-
sandboxHomeDir: string;
|
|
21
33
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
}): ClineSkillsRuntime {
|
|
35
|
+
const projectedSkills = projectClineSkills({ skills });
|
|
36
|
+
const signature = JSON.stringify(projectedSkills);
|
|
37
|
+
if (projectedSkills.length === 0) {
|
|
38
|
+
return { signature };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const executor: NonNullable<ToolExecutors['skills']> = async (
|
|
42
|
+
...executorArguments
|
|
43
|
+
) => {
|
|
44
|
+
const [requestedSkill, args] = executorArguments;
|
|
45
|
+
const skill = projectedSkills.find(
|
|
46
|
+
candidate => candidate.id === normalizeSkillToken(requestedSkill),
|
|
47
|
+
);
|
|
48
|
+
if (skill == null) {
|
|
49
|
+
const availableSkills = projectedSkills.map(skill => skill.name);
|
|
50
|
+
return availableSkills.length > 0
|
|
51
|
+
? `Skill "${requestedSkill}" not found. Available skills: ${availableSkills.join(', ')}`
|
|
52
|
+
: 'No skills are currently available.';
|
|
53
|
+
}
|
|
54
|
+
return renderSkillInstructions({ skill, args });
|
|
55
|
+
};
|
|
56
|
+
executor.configuredSkills = projectedSkills.map(skill => ({
|
|
57
|
+
id: skill.id,
|
|
58
|
+
name: skill.name,
|
|
59
|
+
description: skill.description,
|
|
60
|
+
disabled: false,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const tool = createDefaultTools({
|
|
64
|
+
executors: { skills: executor },
|
|
65
|
+
enableReadFiles: false,
|
|
66
|
+
enableSearch: false,
|
|
67
|
+
enableBash: false,
|
|
68
|
+
enableWebFetch: false,
|
|
69
|
+
enableApplyPatch: false,
|
|
70
|
+
enableEditor: false,
|
|
71
|
+
enableSkills: true,
|
|
72
|
+
enableAskQuestion: false,
|
|
73
|
+
enableSubmitAndExit: false,
|
|
74
|
+
}).find(tool => tool.name === 'skills');
|
|
75
|
+
if (tool == null) {
|
|
76
|
+
throw new Error('Cline did not create its native skills tool.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { signature, tool };
|
|
33
80
|
}
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
function projectClineSkills({
|
|
83
|
+
skills,
|
|
84
|
+
}: {
|
|
85
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
86
|
+
}): ReadonlyArray<ProjectedClineSkill> {
|
|
87
|
+
const names = new Set<string>();
|
|
88
|
+
const ids = new Set<string>();
|
|
89
|
+
return skills
|
|
90
|
+
.map(skill => {
|
|
91
|
+
if (
|
|
92
|
+
!CLINE_SKILL_NAME_PATTERN.test(skill.name) ||
|
|
93
|
+
skill.name === '.' ||
|
|
94
|
+
skill.name === '..'
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(`Invalid Cline skill name: ${skill.name}`);
|
|
97
|
+
}
|
|
98
|
+
if (names.has(skill.name)) {
|
|
99
|
+
throw new Error(`Duplicate Cline skill name: ${skill.name}`);
|
|
100
|
+
}
|
|
101
|
+
names.add(skill.name);
|
|
102
|
+
|
|
103
|
+
const id = normalizeSkillToken(skill.name);
|
|
104
|
+
if (ids.has(id)) {
|
|
105
|
+
throw new Error(`Duplicate Cline skill identifier: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
ids.add(id);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
name: skill.name,
|
|
112
|
+
description: skill.description,
|
|
113
|
+
content: skill.content,
|
|
114
|
+
files: (skill.files ?? [])
|
|
115
|
+
.map(file => ({
|
|
116
|
+
path: normalizeSkillFilePath({
|
|
117
|
+
skillName: skill.name,
|
|
118
|
+
filePath: file.path,
|
|
119
|
+
}),
|
|
120
|
+
content: file.content,
|
|
121
|
+
}))
|
|
122
|
+
.sort(
|
|
123
|
+
(left, right) =>
|
|
124
|
+
left.path.localeCompare(right.path) ||
|
|
125
|
+
left.content.localeCompare(right.content),
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeSkillToken(skill: string): string {
|
|
133
|
+
return skill.trim().replace(/^\/+/, '').toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeSkillFilePath({
|
|
137
|
+
skillName,
|
|
138
|
+
filePath,
|
|
139
|
+
}: {
|
|
140
|
+
skillName: string;
|
|
141
|
+
filePath: string;
|
|
142
|
+
}): string {
|
|
143
|
+
const normalized = path.posix.normalize(filePath);
|
|
144
|
+
if (
|
|
145
|
+
normalized === '' ||
|
|
146
|
+
normalized === '.' ||
|
|
147
|
+
normalized.startsWith('../') ||
|
|
148
|
+
normalized.includes('/../') ||
|
|
149
|
+
normalized.endsWith('/..') ||
|
|
150
|
+
path.posix.isAbsolute(normalized)
|
|
151
|
+
) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Invalid Cline skill file path for ${skillName}: ${filePath}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderSkillInstructions({
|
|
160
|
+
skill,
|
|
161
|
+
args,
|
|
162
|
+
}: {
|
|
163
|
+
skill: ProjectedClineSkill;
|
|
164
|
+
args: string | undefined;
|
|
165
|
+
}): string {
|
|
166
|
+
const trimmedArgs = args?.trim();
|
|
167
|
+
const argsTag = trimmedArgs
|
|
168
|
+
? `\n<command-args>${trimmedArgs}</command-args>`
|
|
169
|
+
: '';
|
|
170
|
+
const description = skill.description.trim()
|
|
171
|
+
? `Description: ${skill.description.trim()}\n\n`
|
|
172
|
+
: '';
|
|
173
|
+
const files =
|
|
174
|
+
skill.files.length === 0
|
|
175
|
+
? ''
|
|
176
|
+
: `\n\n<skill-files>\n${skill.files
|
|
177
|
+
.map(
|
|
178
|
+
file =>
|
|
179
|
+
`<skill-file path=${JSON.stringify(file.path)}>\n${file.content}\n</skill-file>`,
|
|
180
|
+
)
|
|
181
|
+
.join('\n')}\n</skill-files>`;
|
|
182
|
+
|
|
183
|
+
return `<command-name>${skill.name}</command-name>${argsTag}\n<command-instructions>\n${description}${skill.content}${files}\n</command-instructions>`;
|
|
59
184
|
}
|