@ai-sdk/harness-cline 1.0.18 → 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.
@@ -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,
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export const cline = createCline();
10
10
  export { createCline } from './cline-harness';
11
11
  export { VERSION } from './version';
12
12
  export type { ClineHarnessSettings } from './cline-harness';
13
- export type { ClineAuthenticationMode, ClineAuthOptions } from './cline-auth';
13
+ export type { ClineAuthenticationMode } from './cline-auth';