mirror of
https://github.com/cline/cline.git
synced 2026-08-28 19:48:08 +08:00
feat(cli): use saved provider settings for schedules (#10667)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
This commit is contained in:
+4
-2
@@ -221,13 +221,15 @@ In chat surfaces, connector slash commands include `/help`, `/start`, `/new`, `/
|
||||
|
||||
Schedule agents on cron-like intervals or external events.
|
||||
|
||||
If `--provider` and `--model` are omitted, schedules use the last configured
|
||||
provider and model. If only `--provider` is given, the schedule uses that
|
||||
provider's saved model.
|
||||
|
||||
```sh
|
||||
cline schedule create "Daily code review" \
|
||||
--cron "0 9 * * MON-FRI" \
|
||||
--prompt "Review PRs opened yesterday and summarize issues." \
|
||||
--workspace /path/to/repo \
|
||||
--provider cline \
|
||||
--model openai/gpt-5.3-codex \
|
||||
--timeout 3600 \
|
||||
--tags automation,review
|
||||
|
||||
|
||||
@@ -6,11 +6,29 @@ import { createScheduleCommand } from "./schedule";
|
||||
|
||||
const mockSendHubCommand = vi.hoisted(() => vi.fn());
|
||||
const mockEnsureCliHubServer = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@cline/core", () => ({
|
||||
sendHubCommand: mockSendHubCommand,
|
||||
const mockProviderSettings = vi.hoisted(() => ({
|
||||
lastUsed: undefined as { provider?: string; model?: string } | undefined,
|
||||
providers: {} as Record<string, { provider?: string; model?: string }>,
|
||||
}));
|
||||
|
||||
vi.mock("@cline/core", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@cline/core")>("@cline/core");
|
||||
return {
|
||||
...actual,
|
||||
sendHubCommand: mockSendHubCommand,
|
||||
ProviderSettingsManager: class {
|
||||
getLastUsedProviderSettings() {
|
||||
return mockProviderSettings.lastUsed;
|
||||
}
|
||||
|
||||
getProviderSettings(providerId: string) {
|
||||
return mockProviderSettings.providers[providerId];
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../utils/hub-runtime", () => ({
|
||||
ensureCliHubServer: mockEnsureCliHubServer,
|
||||
parseHubEndpointOverride: (rawAddress: string | undefined) => {
|
||||
@@ -47,6 +65,8 @@ async function runScheduleCommand(
|
||||
describe("runScheduleCommand list output", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockProviderSettings.lastUsed = undefined;
|
||||
mockProviderSettings.providers = {};
|
||||
});
|
||||
|
||||
it('prints "No schedules found." for empty non-json list output', async () => {
|
||||
@@ -121,9 +141,158 @@ describe("runScheduleCommand list output", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("runScheduleCommand create delivery metadata", () => {
|
||||
describe("runScheduleCommand create", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockProviderSettings.lastUsed = undefined;
|
||||
mockProviderSettings.providers = {};
|
||||
});
|
||||
|
||||
it("uses the last used provider and model when both flags are omitted", async () => {
|
||||
mockProviderSettings.lastUsed = {
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
};
|
||||
mockEnsureCliHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
authToken: "test-token",
|
||||
});
|
||||
mockSendHubCommand.mockResolvedValue({
|
||||
ok: true,
|
||||
payload: { schedule: { scheduleId: "sched_123" } },
|
||||
});
|
||||
|
||||
const output: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const code = await runScheduleCommand(
|
||||
[
|
||||
"create",
|
||||
"Health check",
|
||||
"--cron",
|
||||
"0 */6 * * *",
|
||||
"--prompt",
|
||||
"Run tests",
|
||||
"--workspace",
|
||||
"/tmp/workspace",
|
||||
"--address",
|
||||
"127.0.0.1:25463",
|
||||
],
|
||||
{
|
||||
writeln: (text?: string) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: (text: string) => {
|
||||
errors.push(text);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(errors).toEqual([]);
|
||||
expect(mockSendHubCommand).toHaveBeenCalledWith(
|
||||
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
|
||||
expect.objectContaining({
|
||||
clientId: "cline-schedule",
|
||||
command: "schedule.create",
|
||||
payload: expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses an explicit provider with that provider's configured model", async () => {
|
||||
mockProviderSettings.lastUsed = {
|
||||
provider: "cline",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
};
|
||||
mockProviderSettings.providers.anthropic = {
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
};
|
||||
mockEnsureCliHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
authToken: "test-token",
|
||||
});
|
||||
mockSendHubCommand.mockResolvedValue({
|
||||
ok: true,
|
||||
payload: { schedule: { scheduleId: "sched_123" } },
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
const code = await runScheduleCommand(
|
||||
[
|
||||
"create",
|
||||
"Health check",
|
||||
"--cron",
|
||||
"0 */6 * * *",
|
||||
"--prompt",
|
||||
"Run tests",
|
||||
"--workspace",
|
||||
"/tmp/workspace",
|
||||
"--provider",
|
||||
"anthropic",
|
||||
"--address",
|
||||
"127.0.0.1:25463",
|
||||
],
|
||||
{
|
||||
writeln: () => {},
|
||||
writeErr: (text: string) => {
|
||||
errors.push(text);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(errors).toEqual([]);
|
||||
expect(mockSendHubCommand).toHaveBeenCalledWith(
|
||||
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
|
||||
expect.objectContaining({
|
||||
command: "schedule.create",
|
||||
payload: expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when an explicit provider has no configured model and no model flag", async () => {
|
||||
mockEnsureCliHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
authToken: "test-token",
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
const code = await runScheduleCommand(
|
||||
[
|
||||
"create",
|
||||
"Health check",
|
||||
"--cron",
|
||||
"0 */6 * * *",
|
||||
"--prompt",
|
||||
"Run tests",
|
||||
"--workspace",
|
||||
"/tmp/workspace",
|
||||
"--provider",
|
||||
"anthropic",
|
||||
"--address",
|
||||
"127.0.0.1:25463",
|
||||
],
|
||||
{
|
||||
writeln: () => {},
|
||||
writeErr: (text: string) => {
|
||||
errors.push(text);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(1);
|
||||
expect(errors).toEqual([
|
||||
'No model is configured for provider "anthropic". Pass --model or save a model for that provider before creating the schedule.',
|
||||
]);
|
||||
expect(mockSendHubCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps --delivery-bot to delivery.userName", async () => {
|
||||
@@ -192,6 +361,8 @@ describe("runScheduleCommand create delivery metadata", () => {
|
||||
describe("runScheduleCommand import", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockProviderSettings.lastUsed = undefined;
|
||||
mockProviderSettings.providers = {};
|
||||
});
|
||||
|
||||
it("preserves exported modelSelection providerId/modelId values", async () => {
|
||||
@@ -257,6 +428,8 @@ describe("runScheduleCommand import", () => {
|
||||
describe("runScheduleCommand export", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockProviderSettings.lastUsed = undefined;
|
||||
mockProviderSettings.providers = {};
|
||||
});
|
||||
|
||||
it("writes JSON content to the --to file path", async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import type { Command } from "commander";
|
||||
import { ensureSchedulerHub } from "./client";
|
||||
import {
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
registerScheduleImportCommand,
|
||||
registerScheduleUpdateCommand,
|
||||
} from "./import-export";
|
||||
import { resolveScheduleModelSelection } from "./model-selection";
|
||||
import type { CommandIo, ScheduleActionWrapper } from "./types";
|
||||
|
||||
export function registerScheduleCommands(
|
||||
@@ -66,8 +66,8 @@ export function registerScheduleCommands(
|
||||
.option("--max-parallel <n>", "Max parallel executions", "1")
|
||||
.option("--metadata-json <json>", "Metadata as JSON object")
|
||||
.option("--mode <act|plan|yolo>", "Execution mode", "yolo")
|
||||
.option("--model <model>", "Model to use", CLINE_DEFAULT_MODEL_ID)
|
||||
.option("--provider <id>", "Provider ID", "cline")
|
||||
.option("--model <model>", "Model to use")
|
||||
.option("--provider <id>", "Provider ID")
|
||||
.option("--system-prompt <text>", "System prompt override")
|
||||
.option("--tags <list>", "Comma-separated tags")
|
||||
.option("--timeout <seconds>", "Timeout in seconds");
|
||||
@@ -92,12 +92,16 @@ export function registerScheduleCommands(
|
||||
parseJsonObjectFlag(opts.metadataJson),
|
||||
opts,
|
||||
);
|
||||
const modelSelection = resolveScheduleModelSelection({
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
});
|
||||
const created = await client.createSchedule({
|
||||
name,
|
||||
cronPattern: opts.cron,
|
||||
prompt: opts.prompt,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
provider: modelSelection.provider,
|
||||
model: modelSelection.model,
|
||||
mode: parseMode(opts.mode) ?? "yolo",
|
||||
workspaceRoot: opts.workspace,
|
||||
cwd: opts.cwd,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import type { Command } from "commander";
|
||||
import { ensureSchedulerHub } from "./client";
|
||||
import {
|
||||
@@ -18,8 +17,13 @@ import {
|
||||
resolveAddress,
|
||||
toPositiveInt,
|
||||
} from "./common";
|
||||
import { resolveScheduleModelSelection } from "./model-selection";
|
||||
import type { CommandIo, ScheduleActionWrapper } from "./types";
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveImportedModelSelection(parsed: Record<string, unknown>): {
|
||||
provider: string;
|
||||
model: string;
|
||||
@@ -30,19 +34,16 @@ function resolveImportedModelSelection(parsed: Record<string, unknown>): {
|
||||
!Array.isArray(parsed.modelSelection)
|
||||
? (parsed.modelSelection as Record<string, unknown>)
|
||||
: undefined;
|
||||
const provider = String(
|
||||
modelSelection?.providerId ??
|
||||
parsed.providerId ??
|
||||
parsed.provider ??
|
||||
"cline",
|
||||
).trim();
|
||||
const model = String(
|
||||
modelSelection?.modelId ??
|
||||
parsed.modelId ??
|
||||
parsed.model ??
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
).trim();
|
||||
return { provider, model };
|
||||
return resolveScheduleModelSelection({
|
||||
provider:
|
||||
stringValue(modelSelection?.providerId) ??
|
||||
stringValue(parsed.providerId) ??
|
||||
stringValue(parsed.provider),
|
||||
model:
|
||||
stringValue(modelSelection?.modelId) ??
|
||||
stringValue(parsed.modelId) ??
|
||||
stringValue(parsed.model),
|
||||
});
|
||||
}
|
||||
|
||||
export function registerScheduleExportCommand(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type ProviderSettings, ProviderSettingsManager } from "@cline/core";
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
|
||||
export const DEFAULT_SCHEDULE_PROVIDER = "cline";
|
||||
|
||||
interface ProviderSettingsReader {
|
||||
getLastUsedProviderSettings(): ProviderSettings | undefined;
|
||||
getProviderSettings(providerId: string): ProviderSettings | undefined;
|
||||
}
|
||||
|
||||
function trimToUndefined(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function resolveScheduleModelSelection(
|
||||
options: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
},
|
||||
providerSettingsManager?: ProviderSettingsReader,
|
||||
): { provider: string; model: string } {
|
||||
const explicitProvider = trimToUndefined(options.provider);
|
||||
const explicitModel = trimToUndefined(options.model);
|
||||
if (explicitProvider && explicitModel) {
|
||||
return { provider: explicitProvider, model: explicitModel };
|
||||
}
|
||||
|
||||
const manager = providerSettingsManager ?? new ProviderSettingsManager();
|
||||
const lastUsedSettings = manager.getLastUsedProviderSettings();
|
||||
const provider =
|
||||
explicitProvider ??
|
||||
trimToUndefined(lastUsedSettings?.provider) ??
|
||||
DEFAULT_SCHEDULE_PROVIDER;
|
||||
const selectedProviderSettings = explicitProvider
|
||||
? manager.getProviderSettings(provider)
|
||||
: lastUsedSettings;
|
||||
const model =
|
||||
explicitModel ??
|
||||
trimToUndefined(selectedProviderSettings?.model) ??
|
||||
(provider === DEFAULT_SCHEDULE_PROVIDER
|
||||
? CLINE_DEFAULT_MODEL_ID
|
||||
: undefined);
|
||||
|
||||
if (!model) {
|
||||
throw new Error(
|
||||
`No model is configured for provider "${provider}". Pass --model or save a model for that provider before creating the schedule.`,
|
||||
);
|
||||
}
|
||||
|
||||
return { provider, model };
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import {
|
||||
ensureSchedulerHub,
|
||||
type HubScheduleClient,
|
||||
} from "../../commands/schedule/client";
|
||||
import { resolveAddress } from "../../commands/schedule/common";
|
||||
import { resolveScheduleModelSelection } from "../../commands/schedule/model-selection";
|
||||
import { CRON_PRESETS } from "./cron-presets";
|
||||
|
||||
function isCancel(value: unknown): value is symbol {
|
||||
@@ -211,12 +211,13 @@ async function actionCreate(client: HubScheduleClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const modelSelection = resolveScheduleModelSelection({ provider, model });
|
||||
const created = (await client.createSchedule({
|
||||
name: (name as string).trim(),
|
||||
cronPattern,
|
||||
prompt: (prompt as string).trim(),
|
||||
provider: provider ?? "cline",
|
||||
model: model ?? CLINE_DEFAULT_MODEL_ID,
|
||||
provider: modelSelection.provider,
|
||||
model: modelSelection.model,
|
||||
mode: mode as "act" | "plan" | "yolo",
|
||||
workspaceRoot: (workspace as string).trim(),
|
||||
systemPrompt,
|
||||
|
||||
Reference in New Issue
Block a user