@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-cline",
3
- "version": "1.0.18",
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.91",
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"
package/src/cline-auth.ts CHANGED
@@ -1,31 +1,45 @@
1
- import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils';
1
+ import type { HarnessV1Authentication } from '@ai-sdk/harness';
2
+ import {
3
+ getAiGatewayAuthFromEnv,
4
+ isHarnessAuthenticationEnvironment,
5
+ } from '@ai-sdk/harness/utils';
2
6
 
3
- export type ClineAuthenticationMode = 'auto' | 'direct' | 'ai-gateway';
4
-
5
- export type ClineAuthOptions = ClineAuthenticationMode;
7
+ export type ClineAuthenticationMode = HarnessV1Authentication;
6
8
 
7
9
  export function resolveClineEnv({
8
10
  auth = 'auto',
9
11
  env = process.env,
10
12
  }: {
11
- auth?: ClineAuthOptions;
13
+ auth?: ClineAuthenticationMode;
12
14
  env?: Record<string, string | undefined>;
13
15
  }): Record<string, string> {
16
+ if (isHarnessAuthenticationEnvironment(auth)) {
17
+ const gatewayAuth = getAiGatewayAuthFromEnv({ env: auth });
18
+ return gatewayAuth.apiKey
19
+ ? toGatewayClineEnv(gatewayAuth)
20
+ : pickDirectClineEnv({ env: auth });
21
+ }
14
22
  if (auth === 'direct') {
15
23
  return pickDirectClineEnv({ env });
16
24
  }
17
25
 
18
26
  const gatewayAuth = getAiGatewayAuthFromEnv({ env });
19
27
  if (auth === 'ai-gateway' || gatewayAuth.apiKey) {
20
- return {
21
- ...(gatewayAuth.apiKey ? { AI_GATEWAY_API_KEY: gatewayAuth.apiKey } : {}),
22
- AI_GATEWAY_BASE_URL: gatewayAuth.baseUrl,
23
- };
28
+ return toGatewayClineEnv(gatewayAuth);
24
29
  }
25
30
 
26
31
  return pickDirectClineEnv({ env });
27
32
  }
28
33
 
34
+ function toGatewayClineEnv(
35
+ gatewayAuth: ReturnType<typeof getAiGatewayAuthFromEnv>,
36
+ ): Record<string, string> {
37
+ return {
38
+ ...(gatewayAuth.apiKey ? { AI_GATEWAY_API_KEY: gatewayAuth.apiKey } : {}),
39
+ AI_GATEWAY_BASE_URL: gatewayAuth.baseUrl,
40
+ };
41
+ }
42
+
29
43
  function pickDirectClineEnv({
30
44
  env,
31
45
  }: {
@@ -3,9 +3,10 @@ import {
3
3
  type HarnessV1,
4
4
  type HarnessV1BuiltinTool,
5
5
  } from '@ai-sdk/harness';
6
+ import { isHarnessAuthenticationEnvironment } from '@ai-sdk/harness/utils';
6
7
  import { tool } from '@ai-sdk/provider-utils';
7
8
  import { z } from 'zod/v4';
8
- import { resolveClineEnv, type ClineAuthOptions } from './cline-auth';
9
+ import { resolveClineEnv, type ClineAuthenticationMode } from './cline-auth';
9
10
  import { clineResumeStateSchema } from './cline-resume-state';
10
11
  import { createClineSession, type ClineReasoningEffort } from './cline-session';
11
12
  import { VERSION } from './version';
@@ -22,7 +23,7 @@ const CLINE_CLIENT_APP = `ai-sdk/harness-cline/${VERSION}`;
22
23
  */
23
24
  export type ClineHarnessSettings = {
24
25
  /** Where Cline sources direct or AI Gateway credentials from. */
25
- readonly auth?: ClineAuthOptions;
26
+ readonly auth?: ClineAuthenticationMode;
26
27
  /**
27
28
  * MCP server definitions keyed by server name. Each definition uses the
28
29
  * underlying runtime's native MCP server configuration format.
@@ -36,11 +37,14 @@ export type ClineHarnessSettings = {
36
37
  /**
37
38
  * Model id for the configured provider. When omitted, Cline selects the
38
39
  * provider's default model.
40
+ *
41
+ * @deprecated Use `model` on `HarnessAgent` instead.
39
42
  */
40
43
  readonly modelId?: string;
41
44
  /**
42
45
  * Provider API key. When omitted, the Cline gateway falls back to the
43
46
  * provider's environment variable (e.g. `ANTHROPIC_API_KEY`).
47
+ * A record-shaped authentication override disables that fallback.
44
48
  */
45
49
  readonly apiKey?: string;
46
50
  /** Custom provider endpoint. */
@@ -138,6 +142,18 @@ const CLINE_BUILTIN_TOOLS = {
138
142
  nativeName: 'ls',
139
143
  toolUseKind: 'readonly',
140
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,
141
157
  } as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
142
158
 
143
159
  export function createCline(
@@ -151,6 +167,7 @@ export function createCline(
151
167
  supportsBuiltinToolFiltering: true,
152
168
  lifecycleStateSchema: clineResumeStateSchema,
153
169
  doStart: async startOpts => {
170
+ const modelId = startOpts.model ?? settings.modelId;
154
171
  const authEnv = resolveClineEnv({ auth: settings.auth });
155
172
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
156
173
  const resumeData = lifecycleState?.data as
@@ -161,12 +178,13 @@ export function createCline(
161
178
  sessionId: startOpts.sessionId,
162
179
  sandboxSession: startOpts.sandboxSession,
163
180
  sessionWorkDir: startOpts.sessionWorkDir,
164
- skills: startOpts.skills ?? [],
165
181
  settings: {
166
182
  authEnv,
183
+ isAuthenticationEnvironmentOverride:
184
+ isHarnessAuthenticationEnvironment(settings.auth),
167
185
  ...(settings.mcpServers ? { mcpServers: settings.mcpServers } : {}),
168
186
  ...(settings.providerId ? { providerId: settings.providerId } : {}),
169
- ...(settings.modelId ? { modelId: settings.modelId } : {}),
187
+ ...(modelId == null ? {} : { modelId }),
170
188
  ...(settings.apiKey ? { apiKey: settings.apiKey } : {}),
171
189
  ...(settings.baseUrl ? { baseUrl: settings.baseUrl } : {}),
172
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,
@@ -82,6 +85,7 @@ const parkedClineSessions = new Map<string, HarnessV1Session>();
82
85
 
83
86
  export interface ClineSessionSettings {
84
87
  readonly authEnv: Record<string, string>;
88
+ readonly isAuthenticationEnvironmentOverride: boolean;
85
89
  readonly mcpServers?: Record<string, unknown>;
86
90
  readonly providerId?: string;
87
91
  readonly modelId?: string;
@@ -96,7 +100,6 @@ export interface CreateClineSessionInput {
96
100
  readonly sessionId: string;
97
101
  readonly sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
98
102
  readonly sessionWorkDir: string;
99
- readonly skills: ReadonlyArray<HarnessV1Skill>;
100
103
  readonly settings: ClineSessionSettings;
101
104
  readonly clientApp: string;
102
105
  readonly isResume: boolean;
@@ -153,17 +156,12 @@ function clineBuiltinToolRequiresApproval(input: {
153
156
  function buildSystemPrompt(input: {
154
157
  sessionWorkDir: string;
155
158
  sandboxDescription: string;
156
- skillsSection?: string;
157
159
  }): string {
158
- const sections = [
160
+ return [
159
161
  'You are Cline, an autonomous coding agent operating inside a sandboxed workspace.',
160
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.`,
161
163
  `## Sandbox\n\n${input.sandboxDescription}`,
162
- ];
163
- if (input.skillsSection) {
164
- sections.push(input.skillsSection);
165
- }
166
- return sections.join('\n\n');
164
+ ].join('\n\n');
167
165
  }
168
166
 
169
167
  function toClineBackendProviderBaseUrl(baseUrl: string): string {
@@ -209,7 +207,9 @@ function createClineAgentModel({
209
207
  ...(apiKey ? { apiKey } : {}),
210
208
  ...(baseUrl ? { baseUrl } : {}),
211
209
  ...(headers ? { headers } : {}),
212
- ...(isAiGateway ? { apiKeyEnv: [] } : {}),
210
+ ...(isAiGateway || settings.isAuthenticationEnvironmentOverride
211
+ ? { apiKeyEnv: [] }
212
+ : {}),
213
213
  },
214
214
  ],
215
215
  });
@@ -268,32 +268,9 @@ export async function createClineSession(
268
268
  input.builtinToolFiltering,
269
269
  );
270
270
 
271
- // Materialize harness-provided skills into sandbox HOME (not the workspace)
272
- // and advertise them via a system prompt section.
273
- let skillsSection: string | undefined;
274
- let skillRootDir: string | undefined;
275
- if (input.skills.length > 0) {
276
- skillRootDir = await writeClineSkills({
277
- sandbox: toolSafeSandboxSession,
278
- sandboxHomeDir,
279
- skills: input.skills,
280
- ...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
281
- });
282
- if (skillRootDir) {
283
- skillsSection = renderSkillsPromptSection(input.skills, skillRootDir);
284
- }
285
- }
286
-
287
271
  const ops = createClineRemoteOps({
288
272
  sandbox: toolSafeSandboxSession,
289
273
  workDir: input.sessionWorkDir,
290
- ...(skillRootDir ? { readableRoots: [{ sandboxDir: skillRootDir }] } : {}),
291
- });
292
-
293
- const baseSystemPrompt = buildSystemPrompt({
294
- sessionWorkDir: input.sessionWorkDir,
295
- sandboxDescription: toolSafeSandboxSession.description,
296
- ...(skillsSection ? { skillsSection } : {}),
297
274
  });
298
275
 
299
276
  // On resume: pull the persisted conversation history from the sandbox so
@@ -321,7 +298,7 @@ export async function createClineSession(
321
298
  // Per-session mutable state we hold across prompts.
322
299
  let agent: Agent | undefined;
323
300
  let unsubscribe: (() => void) | undefined;
324
- let lastToolsSignature: string | undefined;
301
+ let lastAgentConfigurationSignature: string | undefined;
325
302
  let agentHasRun = false;
326
303
  let stopped = false;
327
304
  /*
@@ -401,6 +378,7 @@ export async function createClineSession(
401
378
 
402
379
  function rebuildAgent(rebuildInput: {
403
380
  userTools: ReadonlyArray<HarnessV1ToolSpec>;
381
+ skillsRuntime: ClineSkillsRuntime;
404
382
  instructions?: string;
405
383
  responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
406
384
  }): void {
@@ -417,9 +395,15 @@ export async function createClineSession(
417
395
  }
418
396
  : {}),
419
397
  sessionId: input.sessionId,
420
- systemPrompt: rebuildInput.instructions
421
- ? `${baseSystemPrompt}\n\n${rebuildInput.instructions}`
422
- : 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
+ })(),
423
407
  initialMessages: currentMessages,
424
408
  toolExecution: 'parallel',
425
409
  ...(input.settings.maxIterations !== undefined
@@ -427,6 +411,10 @@ export async function createClineSession(
427
411
  : {}),
428
412
  tools: [
429
413
  ...buildBuiltinAgentTools({ ops, activeNames: activeBuiltinNames }),
414
+ ...(rebuildInput.skillsRuntime.tool &&
415
+ activeBuiltinNames.includes('skills')
416
+ ? [rebuildInput.skillsRuntime.tool]
417
+ : []),
430
418
  ...mcpRuntime.tools,
431
419
  ...buildUserAgentTools({
432
420
  specs: rebuildInput.userTools,
@@ -569,19 +557,23 @@ export async function createClineSession(
569
557
  * Shared by `doPromptTurn` (a fresh user prompt) and `doContinueTurn`
570
558
  * (no prompt — the runtime continues its own thread after a rerun resume).
571
559
  */
572
- function runTurn(turnOpts: {
560
+ async function runTurn(turnOpts: {
573
561
  text: string | undefined;
562
+ skills: ReadonlyArray<HarnessV1Skill>;
574
563
  tools: ReadonlyArray<HarnessV1ToolSpec>;
575
564
  instructions?: string;
576
565
  emit: (part: HarnessV1StreamPart) => void;
577
566
  abortSignal?: AbortSignal;
578
567
  responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
579
- }): HarnessV1PromptControl {
568
+ }): Promise<HarnessV1PromptControl> {
580
569
  if (stopped) {
581
570
  throw new Error('Cline session has been stopped.');
582
571
  }
583
572
 
584
573
  const userTools = turnOpts.tools;
574
+ const skillsRuntime = createClineSkillsRuntime({
575
+ skills: turnOpts.skills,
576
+ });
585
577
  if (
586
578
  turnOpts.responseFormat?.type === 'json' &&
587
579
  turnOpts.responseFormat.schema == null
@@ -604,25 +596,31 @@ export async function createClineSession(
604
596
  }
605
597
 
606
598
  const signature = JSON.stringify({
607
- tools: userTools.map(t => t.name).sort(),
599
+ tools: userTools,
608
600
  responseFormat: turnOpts.responseFormat,
601
+ instructions: turnOpts.instructions,
602
+ skills: skillsRuntime.signature,
609
603
  });
610
- if (agent == null || signature !== lastToolsSignature) {
604
+ if (agent == null || signature !== lastAgentConfigurationSignature) {
611
605
  currentMessages = agent?.snapshot().messages ?? currentMessages;
612
606
  rebuildAgent({
613
607
  userTools,
608
+ skillsRuntime,
614
609
  responseFormat: turnOpts.responseFormat,
615
610
  ...(turnOpts.instructions
616
611
  ? { instructions: turnOpts.instructions }
617
612
  : {}),
618
613
  });
619
- lastToolsSignature = signature;
614
+ lastAgentConfigurationSignature = signature;
620
615
  }
621
616
 
622
617
  currentEmit = turnOpts.emit;
618
+ const userToolNames = new Set(userTools.map(tool => tool.name));
623
619
  translatorState = createClineTranslatorState({
624
- builtinToolNames: activeBuiltinNames,
625
- hostToolNames: userTools.map(tool => tool.name),
620
+ builtinToolNames: activeBuiltinNames.filter(
621
+ name => !userToolNames.has(name),
622
+ ),
623
+ hostToolNames: userToolNames,
626
624
  ignoredToolNames:
627
625
  turnOpts.responseFormat?.type === 'json' ? ['structured_output'] : [],
628
626
  mcpToolNames: mcpRuntime.toolNames,
@@ -806,6 +804,7 @@ export async function createClineSession(
806
804
  ): Promise<HarnessV1PromptControl> => {
807
805
  return runTurn({
808
806
  text: extractUserText(promptOpts.prompt),
807
+ skills: promptOpts.skills,
809
808
  tools: promptOpts.tools ?? [],
810
809
  ...(promptOpts.instructions
811
810
  ? { instructions: promptOpts.instructions }
@@ -840,6 +839,7 @@ export async function createClineSession(
840
839
  */
841
840
  return runTurn({
842
841
  text: undefined,
842
+ skills: continueOpts.skills,
843
843
  tools: continueOpts.tools ?? [],
844
844
  ...(continueOpts.instructions
845
845
  ? { instructions: continueOpts.instructions }