@clinebot/core 0.0.23 → 0.0.25
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +190 -190
- package/dist/providers/local-provider-registry.d.ts +7 -0
- package/dist/providers/local-provider-registry.d.ts.map +1 -1
- package/dist/providers/local-provider-service.d.ts +27 -1
- package/dist/providers/local-provider-service.d.ts.map +1 -1
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +4 -0
- package/src/providers/local-provider-registry.ts +8 -0
- package/src/providers/local-provider-service.test.ts +259 -1
- package/src/providers/local-provider-service.ts +267 -31
- package/src/runtime/runtime-builder.test.ts +41 -0
- package/src/runtime/runtime-builder.ts +2 -3
- package/src/session/unified-session-persistence-service.test.ts +1 -1
|
@@ -3,6 +3,7 @@ import * as LlmsProviders from "@clinebot/llms/providers";
|
|
|
3
3
|
import type {
|
|
4
4
|
RpcAddProviderActionRequest,
|
|
5
5
|
RpcOAuthProviderId,
|
|
6
|
+
RpcProviderCapability,
|
|
6
7
|
RpcProviderListItem,
|
|
7
8
|
RpcProviderModel,
|
|
8
9
|
RpcSaveProviderSettingsActionRequest,
|
|
@@ -22,6 +23,23 @@ import {
|
|
|
22
23
|
|
|
23
24
|
export { ensureCustomProvidersLoaded } from "./local-provider-registry";
|
|
24
25
|
|
|
26
|
+
export interface UpdateLocalProviderRequest {
|
|
27
|
+
providerId: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
apiKey?: string | null;
|
|
31
|
+
headers?: Record<string, string> | null;
|
|
32
|
+
timeoutMs?: number | null;
|
|
33
|
+
models?: string[];
|
|
34
|
+
defaultModelId?: string | null;
|
|
35
|
+
modelsSourceUrl?: string | null;
|
|
36
|
+
capabilities?: RpcProviderCapability[] | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeleteLocalProviderRequest {
|
|
40
|
+
providerId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
// --- Small pure helpers ---
|
|
26
44
|
|
|
27
45
|
function resolveVisibleApiKey(settings: {
|
|
@@ -169,6 +187,72 @@ async function resolveProviderModelMap(
|
|
|
169
187
|
: registeredModels;
|
|
170
188
|
}
|
|
171
189
|
|
|
190
|
+
function uniqueTrimmed(values?: string[]): string[] {
|
|
191
|
+
return [...new Set((values ?? []).map((v) => v.trim()).filter(Boolean))];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeHeaders(
|
|
195
|
+
headers: Record<string, string> | null | undefined,
|
|
196
|
+
): Record<string, string> | undefined {
|
|
197
|
+
const entries = Object.entries(headers ?? {}).filter(
|
|
198
|
+
([key]) => key.trim().length > 0,
|
|
199
|
+
);
|
|
200
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildProviderModels(
|
|
204
|
+
modelIds: string[],
|
|
205
|
+
capabilities: RpcProviderCapability[] | undefined,
|
|
206
|
+
) {
|
|
207
|
+
const supportsVision = capabilities?.includes("vision") ?? false;
|
|
208
|
+
const supportsReasoning = capabilities?.includes("reasoning") ?? false;
|
|
209
|
+
return Object.fromEntries(
|
|
210
|
+
modelIds.map((id) => [
|
|
211
|
+
id,
|
|
212
|
+
{
|
|
213
|
+
id,
|
|
214
|
+
name: id,
|
|
215
|
+
supportsVision,
|
|
216
|
+
supportsAttachments: supportsVision,
|
|
217
|
+
supportsReasoning,
|
|
218
|
+
},
|
|
219
|
+
]),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function resolveModelIds(params: {
|
|
224
|
+
providerId: string;
|
|
225
|
+
explicitModels?: string[];
|
|
226
|
+
modelsSourceUrl?: string;
|
|
227
|
+
fallbackModelIds?: string[];
|
|
228
|
+
shouldRecompute: boolean;
|
|
229
|
+
}): Promise<string[]> {
|
|
230
|
+
if (!params.shouldRecompute) {
|
|
231
|
+
return params.fallbackModelIds ?? [];
|
|
232
|
+
}
|
|
233
|
+
const fetchedModels = params.modelsSourceUrl
|
|
234
|
+
? await fetchModelIdsFromSource(params.modelsSourceUrl, params.providerId)
|
|
235
|
+
: [];
|
|
236
|
+
return [...new Set([...(params.explicitModels ?? []), ...fetchedModels])];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeProviderFromSettingsState(
|
|
240
|
+
manager: ProviderSettingsManager,
|
|
241
|
+
providerId: string,
|
|
242
|
+
): void {
|
|
243
|
+
const state = manager.read();
|
|
244
|
+
let mutated = false;
|
|
245
|
+
if (state.providers[providerId]) {
|
|
246
|
+
delete state.providers[providerId];
|
|
247
|
+
mutated = true;
|
|
248
|
+
}
|
|
249
|
+
if (state.lastUsedProvider === providerId) {
|
|
250
|
+
delete state.lastUsedProvider;
|
|
251
|
+
mutated = true;
|
|
252
|
+
}
|
|
253
|
+
if (mutated) manager.write(state);
|
|
254
|
+
}
|
|
255
|
+
|
|
172
256
|
// --- Public API ---
|
|
173
257
|
|
|
174
258
|
export async function addLocalProvider(
|
|
@@ -182,23 +266,48 @@ export async function addLocalProvider(
|
|
|
182
266
|
}> {
|
|
183
267
|
const providerId = request.providerId.trim().toLowerCase();
|
|
184
268
|
if (!providerId) throw new Error("providerId is required");
|
|
269
|
+
const baseUrl = request.baseUrl.trim();
|
|
270
|
+
const apiKey = request.apiKey?.trim() ?? "";
|
|
271
|
+
|
|
272
|
+
// Compatibility path: empty baseUrl + empty apiKey is treated as a delete request.
|
|
273
|
+
if (!baseUrl && !apiKey) {
|
|
274
|
+
const modelsPath = resolveModelsRegistryPath(manager);
|
|
275
|
+
const modelsState = await readModelsFile(modelsPath);
|
|
276
|
+
if (modelsState.providers[providerId]) {
|
|
277
|
+
const deleted = await deleteLocalProvider(manager, { providerId });
|
|
278
|
+
return {
|
|
279
|
+
providerId,
|
|
280
|
+
settingsPath: deleted.settingsPath,
|
|
281
|
+
modelsPath: deleted.modelsPath,
|
|
282
|
+
modelsCount: 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
removeProviderFromSettingsState(manager, providerId);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
providerId,
|
|
290
|
+
settingsPath: manager.getFilePath(),
|
|
291
|
+
modelsPath,
|
|
292
|
+
modelsCount: 0,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
185
296
|
if (LlmsModels.hasProvider(providerId))
|
|
186
297
|
throw new Error(`provider "${providerId}" already exists`);
|
|
187
298
|
|
|
188
299
|
const providerName = request.name.trim();
|
|
189
300
|
if (!providerName) throw new Error("name is required");
|
|
190
|
-
|
|
191
|
-
const baseUrl = request.baseUrl.trim();
|
|
192
301
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
193
302
|
|
|
194
|
-
const typedModels = (request.models
|
|
195
|
-
.map((m) => m.trim())
|
|
196
|
-
.filter(Boolean);
|
|
303
|
+
const typedModels = uniqueTrimmed(request.models);
|
|
197
304
|
const sourceUrl = request.modelsSourceUrl?.trim();
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
:
|
|
201
|
-
|
|
305
|
+
const modelIds = await resolveModelIds({
|
|
306
|
+
providerId,
|
|
307
|
+
explicitModels: typedModels,
|
|
308
|
+
modelsSourceUrl: sourceUrl,
|
|
309
|
+
shouldRecompute: true,
|
|
310
|
+
});
|
|
202
311
|
if (modelIds.length === 0) {
|
|
203
312
|
throw new Error(
|
|
204
313
|
"at least one model is required (manual or via modelsSourceUrl)",
|
|
@@ -214,19 +323,14 @@ export async function addLocalProvider(
|
|
|
214
323
|
const capabilities = request.capabilities?.length
|
|
215
324
|
? [...new Set(request.capabilities)]
|
|
216
325
|
: undefined;
|
|
217
|
-
const
|
|
218
|
-
([k]) => k.trim().length > 0,
|
|
219
|
-
);
|
|
326
|
+
const normalizedHeaders = normalizeHeaders(request.headers);
|
|
220
327
|
|
|
221
328
|
manager.saveProviderSettings(
|
|
222
329
|
{
|
|
223
330
|
provider: providerId,
|
|
224
|
-
apiKey:
|
|
331
|
+
apiKey: apiKey || undefined,
|
|
225
332
|
baseUrl,
|
|
226
|
-
headers:
|
|
227
|
-
headerEntries.length > 0
|
|
228
|
-
? Object.fromEntries(headerEntries)
|
|
229
|
-
: undefined,
|
|
333
|
+
headers: normalizedHeaders,
|
|
230
334
|
timeout: request.timeoutMs,
|
|
231
335
|
model: defaultModelId,
|
|
232
336
|
},
|
|
@@ -235,8 +339,6 @@ export async function addLocalProvider(
|
|
|
235
339
|
|
|
236
340
|
const modelsPath = resolveModelsRegistryPath(manager);
|
|
237
341
|
const modelsState = await readModelsFile(modelsPath);
|
|
238
|
-
const supportsVision = capabilities?.includes("vision") ?? false;
|
|
239
|
-
const supportsReasoning = capabilities?.includes("reasoning") ?? false;
|
|
240
342
|
|
|
241
343
|
modelsState.providers[providerId] = {
|
|
242
344
|
provider: {
|
|
@@ -246,18 +348,7 @@ export async function addLocalProvider(
|
|
|
246
348
|
capabilities,
|
|
247
349
|
modelsSourceUrl: sourceUrl,
|
|
248
350
|
},
|
|
249
|
-
models:
|
|
250
|
-
modelIds.map((id) => [
|
|
251
|
-
id,
|
|
252
|
-
{
|
|
253
|
-
id,
|
|
254
|
-
name: id,
|
|
255
|
-
supportsVision,
|
|
256
|
-
supportsAttachments: supportsVision,
|
|
257
|
-
supportsReasoning,
|
|
258
|
-
},
|
|
259
|
-
]),
|
|
260
|
-
),
|
|
351
|
+
models: buildProviderModels(modelIds, capabilities),
|
|
261
352
|
};
|
|
262
353
|
await writeModelsFile(modelsPath, modelsState);
|
|
263
354
|
registerCustomProvider(providerId, modelsState.providers[providerId]);
|
|
@@ -270,6 +361,151 @@ export async function addLocalProvider(
|
|
|
270
361
|
};
|
|
271
362
|
}
|
|
272
363
|
|
|
364
|
+
export async function updateLocalProvider(
|
|
365
|
+
manager: ProviderSettingsManager,
|
|
366
|
+
request: UpdateLocalProviderRequest,
|
|
367
|
+
): Promise<{
|
|
368
|
+
providerId: string;
|
|
369
|
+
settingsPath: string;
|
|
370
|
+
modelsPath: string;
|
|
371
|
+
modelsCount: number;
|
|
372
|
+
}> {
|
|
373
|
+
const providerId = request.providerId.trim().toLowerCase();
|
|
374
|
+
if (!providerId) throw new Error("providerId is required");
|
|
375
|
+
|
|
376
|
+
const modelsPath = resolveModelsRegistryPath(manager);
|
|
377
|
+
const modelsState = await readModelsFile(modelsPath);
|
|
378
|
+
const existingEntry = modelsState.providers[providerId];
|
|
379
|
+
if (!existingEntry) {
|
|
380
|
+
throw new Error(`provider "${providerId}" does not exist`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const providerName =
|
|
384
|
+
request.name?.trim() ?? existingEntry.provider.name.trim();
|
|
385
|
+
if (!providerName) throw new Error("name is required");
|
|
386
|
+
|
|
387
|
+
const baseUrl =
|
|
388
|
+
request.baseUrl?.trim() ?? existingEntry.provider.baseUrl.trim();
|
|
389
|
+
if (!baseUrl) throw new Error("baseUrl is required");
|
|
390
|
+
|
|
391
|
+
const capabilities =
|
|
392
|
+
request.capabilities === undefined
|
|
393
|
+
? existingEntry.provider.capabilities
|
|
394
|
+
: request.capabilities === null
|
|
395
|
+
? undefined
|
|
396
|
+
: [...new Set(request.capabilities)];
|
|
397
|
+
|
|
398
|
+
const explicitModels = uniqueTrimmed(request.models);
|
|
399
|
+
const nextModelsSourceUrl =
|
|
400
|
+
request.modelsSourceUrl === undefined
|
|
401
|
+
? existingEntry.provider.modelsSourceUrl
|
|
402
|
+
: request.modelsSourceUrl?.trim() || undefined;
|
|
403
|
+
const shouldRecomputeModels =
|
|
404
|
+
request.models !== undefined ||
|
|
405
|
+
(request.modelsSourceUrl !== undefined && !!nextModelsSourceUrl);
|
|
406
|
+
const existingModelIds = Object.keys(existingEntry.models)
|
|
407
|
+
.map((id) => id.trim())
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
const modelIds = await resolveModelIds({
|
|
410
|
+
providerId,
|
|
411
|
+
explicitModels,
|
|
412
|
+
modelsSourceUrl: nextModelsSourceUrl,
|
|
413
|
+
fallbackModelIds: existingModelIds,
|
|
414
|
+
shouldRecompute: shouldRecomputeModels,
|
|
415
|
+
});
|
|
416
|
+
if (modelIds.length === 0) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
"at least one model is required (manual or via modelsSourceUrl)",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const defaultModelCandidate =
|
|
423
|
+
request.defaultModelId === undefined
|
|
424
|
+
? existingEntry.provider.defaultModelId?.trim()
|
|
425
|
+
: request.defaultModelId?.trim();
|
|
426
|
+
const defaultModelId =
|
|
427
|
+
defaultModelCandidate && modelIds.includes(defaultModelCandidate)
|
|
428
|
+
? defaultModelCandidate
|
|
429
|
+
: modelIds[0];
|
|
430
|
+
|
|
431
|
+
const existingSettings = manager.getProviderSettings(providerId);
|
|
432
|
+
const nextSettings: Record<string, unknown> = {
|
|
433
|
+
...(existingSettings ?? {}),
|
|
434
|
+
provider: providerId,
|
|
435
|
+
baseUrl,
|
|
436
|
+
model: defaultModelId,
|
|
437
|
+
};
|
|
438
|
+
if (request.apiKey !== undefined) {
|
|
439
|
+
const apiKey = request.apiKey?.trim() ?? "";
|
|
440
|
+
if (apiKey) nextSettings.apiKey = apiKey;
|
|
441
|
+
else delete nextSettings.apiKey;
|
|
442
|
+
}
|
|
443
|
+
if (request.headers !== undefined) {
|
|
444
|
+
const normalizedHeaders = normalizeHeaders(request.headers);
|
|
445
|
+
if (normalizedHeaders) nextSettings.headers = normalizedHeaders;
|
|
446
|
+
else delete nextSettings.headers;
|
|
447
|
+
}
|
|
448
|
+
if (request.timeoutMs !== undefined) {
|
|
449
|
+
if (typeof request.timeoutMs === "number") {
|
|
450
|
+
nextSettings.timeout = request.timeoutMs;
|
|
451
|
+
} else {
|
|
452
|
+
delete nextSettings.timeout;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
manager.saveProviderSettings(nextSettings, { setLastUsed: false });
|
|
457
|
+
|
|
458
|
+
modelsState.providers[providerId] = {
|
|
459
|
+
provider: {
|
|
460
|
+
name: providerName,
|
|
461
|
+
baseUrl,
|
|
462
|
+
defaultModelId,
|
|
463
|
+
capabilities,
|
|
464
|
+
modelsSourceUrl: nextModelsSourceUrl,
|
|
465
|
+
},
|
|
466
|
+
models: buildProviderModels(modelIds, capabilities),
|
|
467
|
+
};
|
|
468
|
+
await writeModelsFile(modelsPath, modelsState);
|
|
469
|
+
registerCustomProvider(providerId, modelsState.providers[providerId]);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
providerId,
|
|
473
|
+
settingsPath: manager.getFilePath(),
|
|
474
|
+
modelsPath,
|
|
475
|
+
modelsCount: modelIds.length,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function deleteLocalProvider(
|
|
480
|
+
manager: ProviderSettingsManager,
|
|
481
|
+
request: DeleteLocalProviderRequest,
|
|
482
|
+
): Promise<{
|
|
483
|
+
providerId: string;
|
|
484
|
+
settingsPath: string;
|
|
485
|
+
modelsPath: string;
|
|
486
|
+
}> {
|
|
487
|
+
const providerId = request.providerId.trim().toLowerCase();
|
|
488
|
+
if (!providerId) throw new Error("providerId is required");
|
|
489
|
+
|
|
490
|
+
const modelsPath = resolveModelsRegistryPath(manager);
|
|
491
|
+
const modelsState = await readModelsFile(modelsPath);
|
|
492
|
+
if (!modelsState.providers[providerId]) {
|
|
493
|
+
throw new Error(`provider "${providerId}" does not exist`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
delete modelsState.providers[providerId];
|
|
497
|
+
await writeModelsFile(modelsPath, modelsState);
|
|
498
|
+
LlmsModels.unregisterProvider(providerId);
|
|
499
|
+
|
|
500
|
+
removeProviderFromSettingsState(manager, providerId);
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
providerId,
|
|
504
|
+
settingsPath: manager.getFilePath(),
|
|
505
|
+
modelsPath,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
273
509
|
export async function listLocalProviders(
|
|
274
510
|
manager: ProviderSettingsManager,
|
|
275
511
|
): Promise<{ providers: RpcProviderListItem[]; settingsPath: string }> {
|
|
@@ -246,6 +246,47 @@ Use conventional commits.`,
|
|
|
246
246
|
runtime.shutdown("test");
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
it("allows tool routing rules to disable skills even when skills exist", () => {
|
|
250
|
+
const cwd = mkdtempSync(
|
|
251
|
+
join(tmpdir(), "runtime-builder-skills-routing-disabled-"),
|
|
252
|
+
);
|
|
253
|
+
const skillDir = join(cwd, ".cline", "skills", "commit");
|
|
254
|
+
mkdirSync(skillDir, { recursive: true });
|
|
255
|
+
writeFileSync(
|
|
256
|
+
join(skillDir, "SKILL.md"),
|
|
257
|
+
`---
|
|
258
|
+
name: commit
|
|
259
|
+
description: Create commit message
|
|
260
|
+
---
|
|
261
|
+
Use conventional commits.`,
|
|
262
|
+
"utf8",
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const runtime = new DefaultRuntimeBuilder().build({
|
|
266
|
+
config: {
|
|
267
|
+
providerId: "openrouter",
|
|
268
|
+
modelId: "google/gemini-3-flash-preview",
|
|
269
|
+
apiKey: "key",
|
|
270
|
+
systemPrompt: "test",
|
|
271
|
+
cwd,
|
|
272
|
+
enableTools: true,
|
|
273
|
+
enableSpawnAgent: false,
|
|
274
|
+
enableAgentTeams: false,
|
|
275
|
+
toolRoutingRules: [
|
|
276
|
+
{
|
|
277
|
+
mode: "act",
|
|
278
|
+
providerIdIncludes: ["openrouter"],
|
|
279
|
+
modelIdIncludes: ["gemini"],
|
|
280
|
+
disableTools: ["skills"],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(runtime.tools.map((tool) => tool.name)).not.toContain("skills");
|
|
287
|
+
runtime.shutdown("test");
|
|
288
|
+
});
|
|
289
|
+
|
|
249
290
|
it("marks configured but disabled skills in executor metadata", async () => {
|
|
250
291
|
const cwd = mkdtempSync(join(tmpdir(), "runtime-builder-skills-disabled-"));
|
|
251
292
|
const enabledDir = join(cwd, ".cline", "skills", "commit");
|
|
@@ -72,8 +72,8 @@ function createBuiltinToolsList(
|
|
|
72
72
|
return createBuiltinTools({
|
|
73
73
|
cwd,
|
|
74
74
|
...preset,
|
|
75
|
-
...toolRoutingConfig,
|
|
76
75
|
enableSkills: !!skillsExecutor,
|
|
76
|
+
...toolRoutingConfig,
|
|
77
77
|
executors: {
|
|
78
78
|
...(skillsExecutor
|
|
79
79
|
? {
|
|
@@ -531,10 +531,9 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
|
|
|
531
531
|
}
|
|
532
532
|
teamToolsRegistered = true;
|
|
533
533
|
|
|
534
|
-
const leadAgentId = config.sessionId || "lead";
|
|
535
534
|
const teamBootstrap = bootstrapAgentTeams({
|
|
536
535
|
runtime: teamRuntime,
|
|
537
|
-
leadAgentId,
|
|
536
|
+
leadAgentId: "lead",
|
|
538
537
|
restoredFromPersistence: Boolean(restoredTeamState),
|
|
539
538
|
restoredTeammates: restoredTeammateSpecs,
|
|
540
539
|
createBaseTools: normalized.enableTools
|