@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-cline",
3
- "version": "1.0.19",
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.92",
29
+ "@ai-sdk/harness": "1.0.93",
30
30
  "@ai-sdk/provider": "4.0.8",
31
- "@ai-sdk/provider-utils": "5.0.32",
32
- "@cline/agents": "^0.0.72",
33
- "@cline/core": "^0.0.72"
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"
@@ -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
- ...(settings.modelId ? { modelId: settings.modelId } : {}),
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 } : {}),
@@ -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
- assertReadablePath(
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 = resolveReadablePath(inputPath);
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 = resolveReadablePath(inputPath);
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 = resolveReadablePath(inputPath);
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 = resolveReadablePath(inputPath);
361
+ const remotePath = resolvePath(inputPath);
386
362
  const target = await resolveReadableSandboxPath({
387
363
  remotePath,
388
364
  inputPath,
@@ -35,7 +35,10 @@ import {
35
35
  resolveClinePrivateSessionDirectory,
36
36
  safeClineHistoryFileName,
37
37
  } from './cline-resume-state';
38
- import { renderSkillsPromptSection, writeClineSkills } from './cline-skills';
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
- const sections = [
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 lastToolsSignature: string | undefined;
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: rebuildInput.instructions
424
- ? `${baseSystemPrompt}\n\n${rebuildInput.instructions}`
425
- : baseSystemPrompt,
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.map(t => t.name).sort(),
599
+ tools: userTools,
611
600
  responseFormat: turnOpts.responseFormat,
601
+ instructions: turnOpts.instructions,
602
+ skills: skillsRuntime.signature,
612
603
  });
613
- if (agent == null || signature !== lastToolsSignature) {
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
- lastToolsSignature = signature;
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
- hostToolNames: userTools.map(tool => tool.name),
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 }
@@ -1,59 +1,184 @@
1
1
  import path from 'node:path';
2
2
  import type { HarnessV1Skill } from '@ai-sdk/harness';
3
- import { writeSkills } from '@ai-sdk/harness/utils';
4
- import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
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
- * Materialize harness-provided skills into the sandbox HOME
8
- * (`~/.agents/skills/<name>/SKILL.md` plus any auxiliary files) and return
9
- * the root directory they were written to. The Cline runtime has no native
10
- * skill loader, so the session surfaces them to the model through a system
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 async function writeClineSkills({
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
- abortSignal?: AbortSignal;
23
- }): Promise<string | undefined> {
24
- if (skills.length === 0) return undefined;
25
- const skillRootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
26
- await writeSkills({
27
- sandbox,
28
- rootDir: skillRootDir,
29
- skills,
30
- ...(abortSignal ? { abortSignal } : {}),
31
- });
32
- return skillRootDir;
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
- * Render the system-prompt section that advertises available skills. Only the
37
- * name + description sit in context; the model loads the full skill via the
38
- * `read` tool when the task calls for it (progressive disclosure).
39
- */
40
- export function renderSkillsPromptSection(
41
- skills: ReadonlyArray<HarnessV1Skill>,
42
- skillRootDir: string,
43
- ): string {
44
- const entries = skills
45
- .map(
46
- skill =>
47
- `- ${skill.name}: ${skill.description}\n SKILL.md: ${path.posix.join(
48
- skillRootDir,
49
- skill.name,
50
- 'SKILL.md',
51
- )}`,
52
- )
53
- .join('\n');
54
- return (
55
- '## Skills\n\n' +
56
- 'The following skills are available. When a task matches a skill, read its SKILL.md with the `read` tool and follow its instructions.\n\n' +
57
- entries
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
  }
@@ -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 built-in `AgentTool`s handed to the Cline runtime.
131
- * These schemas mirror the zod declarations in `CLINE_BUILTIN_TOOLS`
132
- * (cline-harness.ts) — keep the two in sync.
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,