@ai-sdk/harness-cline 1.0.19 → 1.0.20
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 +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +170 -87
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/cline-harness.ts +16 -2
- package/src/cline-remote-ops.ts +5 -29
- package/src/cline-session.ts +40 -43
- 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.20",
|
|
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.
|
|
29
|
+
"@ai-sdk/harness": "1.0.93",
|
|
30
30
|
"@ai-sdk/provider": "4.0.8",
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
32
|
-
"@cline/agents": "^0.0.
|
|
33
|
-
"@cline/core": "^0.0.
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.33",
|
|
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(
|
|
@@ -153,6 +167,7 @@ export function createCline(
|
|
|
153
167
|
supportsBuiltinToolFiltering: true,
|
|
154
168
|
lifecycleStateSchema: clineResumeStateSchema,
|
|
155
169
|
doStart: async startOpts => {
|
|
170
|
+
const modelId = startOpts.model ?? settings.modelId;
|
|
156
171
|
const authEnv = resolveClineEnv({ auth: settings.auth });
|
|
157
172
|
const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
|
|
158
173
|
const resumeData = lifecycleState?.data as
|
|
@@ -163,14 +178,13 @@ export function createCline(
|
|
|
163
178
|
sessionId: startOpts.sessionId,
|
|
164
179
|
sandboxSession: startOpts.sandboxSession,
|
|
165
180
|
sessionWorkDir: startOpts.sessionWorkDir,
|
|
166
|
-
skills: startOpts.skills ?? [],
|
|
167
181
|
settings: {
|
|
168
182
|
authEnv,
|
|
169
183
|
isAuthenticationEnvironmentOverride:
|
|
170
184
|
isHarnessAuthenticationEnvironment(settings.auth),
|
|
171
185
|
...(settings.mcpServers ? { mcpServers: settings.mcpServers } : {}),
|
|
172
186
|
...(settings.providerId ? { providerId: settings.providerId } : {}),
|
|
173
|
-
...(
|
|
187
|
+
...(modelId == null ? {} : { modelId }),
|
|
174
188
|
...(settings.apiKey ? { apiKey: settings.apiKey } : {}),
|
|
175
189
|
...(settings.baseUrl ? { baseUrl: settings.baseUrl } : {}),
|
|
176
190
|
...(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 {
|
|
@@ -271,32 +268,9 @@ export async function createClineSession(
|
|
|
271
268
|
input.builtinToolFiltering,
|
|
272
269
|
);
|
|
273
270
|
|
|
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
271
|
const ops = createClineRemoteOps({
|
|
291
272
|
sandbox: toolSafeSandboxSession,
|
|
292
273
|
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
274
|
});
|
|
301
275
|
|
|
302
276
|
// On resume: pull the persisted conversation history from the sandbox so
|
|
@@ -324,7 +298,7 @@ export async function createClineSession(
|
|
|
324
298
|
// Per-session mutable state we hold across prompts.
|
|
325
299
|
let agent: Agent | undefined;
|
|
326
300
|
let unsubscribe: (() => void) | undefined;
|
|
327
|
-
let
|
|
301
|
+
let lastAgentConfigurationSignature: string | undefined;
|
|
328
302
|
let agentHasRun = false;
|
|
329
303
|
let stopped = false;
|
|
330
304
|
/*
|
|
@@ -404,6 +378,7 @@ export async function createClineSession(
|
|
|
404
378
|
|
|
405
379
|
function rebuildAgent(rebuildInput: {
|
|
406
380
|
userTools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
381
|
+
skillsRuntime: ClineSkillsRuntime;
|
|
407
382
|
instructions?: string;
|
|
408
383
|
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
409
384
|
}): void {
|
|
@@ -420,9 +395,15 @@ export async function createClineSession(
|
|
|
420
395
|
}
|
|
421
396
|
: {}),
|
|
422
397
|
sessionId: input.sessionId,
|
|
423
|
-
systemPrompt:
|
|
424
|
-
|
|
425
|
-
|
|
398
|
+
systemPrompt: (() => {
|
|
399
|
+
const baseSystemPrompt = buildSystemPrompt({
|
|
400
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
401
|
+
sandboxDescription: toolSafeSandboxSession.description,
|
|
402
|
+
});
|
|
403
|
+
return rebuildInput.instructions
|
|
404
|
+
? `${baseSystemPrompt}\n\n${rebuildInput.instructions}`
|
|
405
|
+
: baseSystemPrompt;
|
|
406
|
+
})(),
|
|
426
407
|
initialMessages: currentMessages,
|
|
427
408
|
toolExecution: 'parallel',
|
|
428
409
|
...(input.settings.maxIterations !== undefined
|
|
@@ -430,6 +411,10 @@ export async function createClineSession(
|
|
|
430
411
|
: {}),
|
|
431
412
|
tools: [
|
|
432
413
|
...buildBuiltinAgentTools({ ops, activeNames: activeBuiltinNames }),
|
|
414
|
+
...(rebuildInput.skillsRuntime.tool &&
|
|
415
|
+
activeBuiltinNames.includes('skills')
|
|
416
|
+
? [rebuildInput.skillsRuntime.tool]
|
|
417
|
+
: []),
|
|
433
418
|
...mcpRuntime.tools,
|
|
434
419
|
...buildUserAgentTools({
|
|
435
420
|
specs: rebuildInput.userTools,
|
|
@@ -572,19 +557,23 @@ export async function createClineSession(
|
|
|
572
557
|
* Shared by `doPromptTurn` (a fresh user prompt) and `doContinueTurn`
|
|
573
558
|
* (no prompt — the runtime continues its own thread after a rerun resume).
|
|
574
559
|
*/
|
|
575
|
-
function runTurn(turnOpts: {
|
|
560
|
+
async function runTurn(turnOpts: {
|
|
576
561
|
text: string | undefined;
|
|
562
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
577
563
|
tools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
578
564
|
instructions?: string;
|
|
579
565
|
emit: (part: HarnessV1StreamPart) => void;
|
|
580
566
|
abortSignal?: AbortSignal;
|
|
581
567
|
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
582
|
-
}): HarnessV1PromptControl {
|
|
568
|
+
}): Promise<HarnessV1PromptControl> {
|
|
583
569
|
if (stopped) {
|
|
584
570
|
throw new Error('Cline session has been stopped.');
|
|
585
571
|
}
|
|
586
572
|
|
|
587
573
|
const userTools = turnOpts.tools;
|
|
574
|
+
const skillsRuntime = createClineSkillsRuntime({
|
|
575
|
+
skills: turnOpts.skills,
|
|
576
|
+
});
|
|
588
577
|
if (
|
|
589
578
|
turnOpts.responseFormat?.type === 'json' &&
|
|
590
579
|
turnOpts.responseFormat.schema == null
|
|
@@ -607,25 +596,31 @@ export async function createClineSession(
|
|
|
607
596
|
}
|
|
608
597
|
|
|
609
598
|
const signature = JSON.stringify({
|
|
610
|
-
tools: userTools
|
|
599
|
+
tools: userTools,
|
|
611
600
|
responseFormat: turnOpts.responseFormat,
|
|
601
|
+
instructions: turnOpts.instructions,
|
|
602
|
+
skills: skillsRuntime.signature,
|
|
612
603
|
});
|
|
613
|
-
if (agent == null || signature !==
|
|
604
|
+
if (agent == null || signature !== lastAgentConfigurationSignature) {
|
|
614
605
|
currentMessages = agent?.snapshot().messages ?? currentMessages;
|
|
615
606
|
rebuildAgent({
|
|
616
607
|
userTools,
|
|
608
|
+
skillsRuntime,
|
|
617
609
|
responseFormat: turnOpts.responseFormat,
|
|
618
610
|
...(turnOpts.instructions
|
|
619
611
|
? { instructions: turnOpts.instructions }
|
|
620
612
|
: {}),
|
|
621
613
|
});
|
|
622
|
-
|
|
614
|
+
lastAgentConfigurationSignature = signature;
|
|
623
615
|
}
|
|
624
616
|
|
|
625
617
|
currentEmit = turnOpts.emit;
|
|
618
|
+
const userToolNames = new Set(userTools.map(tool => tool.name));
|
|
626
619
|
translatorState = createClineTranslatorState({
|
|
627
|
-
builtinToolNames: activeBuiltinNames
|
|
628
|
-
|
|
620
|
+
builtinToolNames: activeBuiltinNames.filter(
|
|
621
|
+
name => !userToolNames.has(name),
|
|
622
|
+
),
|
|
623
|
+
hostToolNames: userToolNames,
|
|
629
624
|
ignoredToolNames:
|
|
630
625
|
turnOpts.responseFormat?.type === 'json' ? ['structured_output'] : [],
|
|
631
626
|
mcpToolNames: mcpRuntime.toolNames,
|
|
@@ -809,6 +804,7 @@ export async function createClineSession(
|
|
|
809
804
|
): Promise<HarnessV1PromptControl> => {
|
|
810
805
|
return runTurn({
|
|
811
806
|
text: extractUserText(promptOpts.prompt),
|
|
807
|
+
skills: promptOpts.skills,
|
|
812
808
|
tools: promptOpts.tools ?? [],
|
|
813
809
|
...(promptOpts.instructions
|
|
814
810
|
? { instructions: promptOpts.instructions }
|
|
@@ -843,6 +839,7 @@ export async function createClineSession(
|
|
|
843
839
|
*/
|
|
844
840
|
return runTurn({
|
|
845
841
|
text: undefined,
|
|
842
|
+
skills: continueOpts.skills,
|
|
846
843
|
tools: continueOpts.tools ?? [],
|
|
847
844
|
...(continueOpts.instructions
|
|
848
845
|
? { 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
|
}
|
package/src/cline-tools.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const CLINE_NATIVE_BUILTIN_NAMES = [
|
|
|
24
24
|
'grep',
|
|
25
25
|
'glob',
|
|
26
26
|
'ls',
|
|
27
|
+
'skills',
|
|
27
28
|
] as const;
|
|
28
29
|
|
|
29
30
|
export type ClineNativeBuiltinName =
|
|
@@ -39,6 +40,7 @@ export const CLINE_NATIVE_TOOL_KINDS: Readonly<
|
|
|
39
40
|
grep: 'readonly',
|
|
40
41
|
glob: 'readonly',
|
|
41
42
|
ls: 'readonly',
|
|
43
|
+
skills: 'readonly',
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export function isClineBuiltinToolName(
|
|
@@ -127,9 +129,9 @@ async function guarded<T>(run: () => Promise<T>): Promise<ClineToolResult> {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/**
|
|
130
|
-
* Build the sandbox-backed
|
|
131
|
-
*
|
|
132
|
-
* (cline-harness.ts)
|
|
132
|
+
* Build the sandbox-backed coding tools handed to the Cline runtime. These
|
|
133
|
+
* schemas mirror their declarations in `CLINE_BUILTIN_TOOLS`
|
|
134
|
+
* (cline-harness.ts). The in-host skills tool is built separately.
|
|
133
135
|
*/
|
|
134
136
|
export function buildBuiltinAgentTools({
|
|
135
137
|
ops,
|