mirror of
https://github.com/cline/cline.git
synced 2026-08-29 03:52:41 +08:00
fix(schedules): default headless routines to yolo (#12489)
* fix(schedules): default headless routines to yolo Centralize the Cline default model ID in @cline/shared while preserving the @cline/llms export. Keep explicit modes stable and disable ask_question for unattended scheduled runs. * autoapprove * fix unit test * fix(schedules): harden headless routine execution --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
@@ -148,8 +148,10 @@ export function isJsonPath(path: string): boolean {
|
||||
return path.toLowerCase().endsWith(".json");
|
||||
}
|
||||
|
||||
export function parseMode(raw: string | undefined): "act" | "plan" | undefined {
|
||||
if (raw === "act" || raw === "plan") {
|
||||
export function parseMode(
|
||||
raw: string | undefined,
|
||||
): "act" | "plan" | "yolo" | undefined {
|
||||
if (raw === "act" || raw === "plan" || raw === "yolo") {
|
||||
return raw;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import type { Command } from "commander";
|
||||
import { ensureSchedulerHub } from "./client";
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
mergeScheduleMetadata,
|
||||
parseJsonObjectFlag,
|
||||
parseList,
|
||||
parseMode,
|
||||
resolveAddress,
|
||||
toPositiveInt,
|
||||
} from "./common";
|
||||
@@ -63,8 +65,8 @@ export function registerScheduleCommands(
|
||||
.option("--disabled", "Create in disabled state")
|
||||
.option("--max-parallel <n>", "Max parallel executions", "1")
|
||||
.option("--metadata-json <json>", "Metadata as JSON object")
|
||||
.option("--mode <act|plan>", "Execution mode")
|
||||
.option("--model <model>", "Model to use", "openai/gpt-5.3-codex")
|
||||
.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("--system-prompt <text>", "System prompt override")
|
||||
.option("--tags <list>", "Comma-separated tags")
|
||||
@@ -96,7 +98,7 @@ export function registerScheduleCommands(
|
||||
prompt: opts.prompt,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
mode: opts.mode === "plan" ? "plan" : "act",
|
||||
mode: parseMode(opts.mode) ?? "yolo",
|
||||
workspaceRoot: opts.workspace,
|
||||
cwd: opts.cwd,
|
||||
systemPrompt: opts.systemPrompt,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 {
|
||||
@@ -39,7 +40,7 @@ function resolveImportedModelSelection(parsed: Record<string, unknown>): {
|
||||
modelSelection?.modelId ??
|
||||
parsed.modelId ??
|
||||
parsed.model ??
|
||||
"openai/gpt-5.3-codex",
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
).trim();
|
||||
return { provider, model };
|
||||
}
|
||||
@@ -165,7 +166,10 @@ export function registerScheduleImportCommand(
|
||||
prompt: String(parsed.prompt ?? "").trim(),
|
||||
provider,
|
||||
model,
|
||||
mode: parsed.mode === "plan" ? "plan" : "act",
|
||||
mode:
|
||||
parseMode(
|
||||
typeof parsed.mode === "string" ? parsed.mode : undefined,
|
||||
) ?? "yolo",
|
||||
workspaceRoot,
|
||||
cwd: String(parsed.cwd ?? "").trim() || undefined,
|
||||
systemPrompt:
|
||||
@@ -229,7 +233,7 @@ export function registerScheduleUpdateCommand(
|
||||
.option("--enabled", "Enable the schedule")
|
||||
.option("--max-parallel <n>", "New max parallel executions")
|
||||
.option("--metadata-json <json>", "New metadata as JSON object")
|
||||
.option("--mode <act|plan>", "New execution mode")
|
||||
.option("--mode <act|plan|yolo>", "New execution mode")
|
||||
.option("--model <model>", "New model")
|
||||
.option("--name <name>", "New name")
|
||||
.option("--pause", "Pause the schedule")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import {
|
||||
ensureSchedulerHub,
|
||||
type HubScheduleClient,
|
||||
@@ -135,10 +136,11 @@ async function actionCreate(client: HubScheduleClient): Promise<void> {
|
||||
const mode = await p.select({
|
||||
message: "Agent mode",
|
||||
options: [
|
||||
{ value: "yolo", label: "Yolo", hint: "execute without approvals" },
|
||||
{ value: "act", label: "Act", hint: "execute tasks" },
|
||||
{ value: "plan", label: "Plan", hint: "plan only" },
|
||||
],
|
||||
initialValue: "act",
|
||||
initialValue: "yolo",
|
||||
});
|
||||
if (isCancel(mode)) return;
|
||||
|
||||
@@ -214,8 +216,8 @@ async function actionCreate(client: HubScheduleClient): Promise<void> {
|
||||
cronPattern,
|
||||
prompt: (prompt as string).trim(),
|
||||
provider: provider ?? "cline",
|
||||
model: model ?? "openai/gpt-5.3-codex",
|
||||
mode: (mode as string) === "plan" ? "plan" : "act",
|
||||
model: model ?? CLINE_DEFAULT_MODEL_ID,
|
||||
mode: mode as "act" | "plan" | "yolo",
|
||||
workspaceRoot: (workspace as string).trim(),
|
||||
systemPrompt,
|
||||
maxIterations,
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
HubScheduleService,
|
||||
} from "@cline/core";
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
ONE_TIME_SCHEDULE_CRON_PATTERN,
|
||||
ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY,
|
||||
readHubScheduleMode,
|
||||
} from "@cline/shared";
|
||||
import { asTrimmedString, toPositiveInt } from "./utils";
|
||||
|
||||
@@ -65,10 +67,6 @@ function asTrimmedStringArray(value: unknown): string[] | undefined {
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
function routineScheduleMode(value: unknown): "act" | "plan" | "yolo" {
|
||||
return value === "plan" || value === "yolo" ? value : "act";
|
||||
}
|
||||
|
||||
export async function handleRoutineScheduleCommand(
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
@@ -123,9 +121,9 @@ export async function handleRoutineScheduleCommand(
|
||||
prompt,
|
||||
modelSelection: {
|
||||
providerId: asTrimmedString(args?.provider) ?? "cline",
|
||||
modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex",
|
||||
modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID,
|
||||
},
|
||||
mode: routineScheduleMode(args?.mode),
|
||||
mode: readHubScheduleMode(args, "yolo"),
|
||||
workspaceRoot: routineWorkspaceRoot,
|
||||
cwd: asTrimmedString(args?.cwd),
|
||||
systemPrompt: asTrimmedString(args?.system_prompt),
|
||||
@@ -141,6 +139,7 @@ export async function handleRoutineScheduleCommand(
|
||||
const scheduleId = asTrimmedString(args?.schedule_id);
|
||||
if (!scheduleId) throw new Error(`${command} requires schedule_id`);
|
||||
if (command === "update_routine_schedule") {
|
||||
const mode = readHubScheduleMode(args);
|
||||
const name = asTrimmedString(args?.name);
|
||||
const timing = routineScheduleTiming(args);
|
||||
const prompt = asTrimmedString(args?.prompt);
|
||||
@@ -157,9 +156,9 @@ export async function handleRoutineScheduleCommand(
|
||||
prompt,
|
||||
modelSelection: {
|
||||
providerId: asTrimmedString(args?.provider) ?? "cline",
|
||||
modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex",
|
||||
modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID,
|
||||
},
|
||||
mode: routineScheduleMode(args?.mode),
|
||||
...(mode === undefined ? {} : { mode }),
|
||||
workspaceRoot: routineWorkspaceRoot,
|
||||
cwd: asTrimmedString(args?.cwd) ?? null,
|
||||
systemPrompt:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
ONE_TIME_SCHEDULE_CRON_PATTERN,
|
||||
ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY,
|
||||
} from "@cline/shared";
|
||||
@@ -150,7 +151,7 @@ interface ProcessContext {
|
||||
}
|
||||
|
||||
const FALLBACK_PROVIDER_MODELS: Record<string, string[]> = {
|
||||
cline: ["anthropic/claude-sonnet-4.6"],
|
||||
cline: [CLINE_DEFAULT_MODEL_ID],
|
||||
anthropic: ["claude-sonnet-4-6"],
|
||||
"openai-native": ["gpt-5.3-codex"],
|
||||
openrouter: ["anthropic/claude-sonnet-4.6"],
|
||||
@@ -236,7 +237,7 @@ function getScheduleProviderModel(schedule: RoutineSchedule): {
|
||||
model:
|
||||
schedule.modelSelection?.modelId?.trim() ||
|
||||
schedule.model?.trim() ||
|
||||
"openai/gpt-5.3-codex",
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -399,7 +400,7 @@ export function RoutineSchedulesContent() {
|
||||
scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"],
|
||||
prompt: "Review PRs opened yesterday and summarize issues.",
|
||||
provider: "cline",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
model: CLINE_DEFAULT_MODEL_ID,
|
||||
workspaceRoot: "",
|
||||
systemPrompt: "",
|
||||
timeoutSeconds: "",
|
||||
@@ -797,7 +798,7 @@ export function RoutineSchedulesContent() {
|
||||
const model =
|
||||
asTrimmedFormString(createForm.model) ||
|
||||
(visibleProviderModels[provider] ?? [])[0] ||
|
||||
"openai/gpt-6-sol";
|
||||
CLINE_DEFAULT_MODEL_ID;
|
||||
const systemPrompt = asTrimmedFormString(createForm.systemPrompt);
|
||||
const timeoutSeconds = parseOptionalPositiveInt(
|
||||
createForm.timeoutSeconds,
|
||||
|
||||
@@ -52,9 +52,11 @@ import {
|
||||
updateMcpSettingsFileSync,
|
||||
} from "@cline/core";
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
getClineEnvironmentConfig,
|
||||
ONE_TIME_SCHEDULE_CRON_PATTERN,
|
||||
ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY,
|
||||
readHubScheduleMode,
|
||||
} from "@cline/shared";
|
||||
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
|
||||
import packageJson from "../package.json";
|
||||
@@ -550,10 +552,6 @@ function asTrimmedStringArray(value: unknown): string[] | undefined {
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
function routineScheduleMode(value: unknown): "act" | "plan" | "yolo" {
|
||||
return value === "plan" || value === "yolo" ? value : "act";
|
||||
}
|
||||
|
||||
async function handleRoutineScheduleCommand(
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
@@ -660,9 +658,9 @@ async function handleRoutineScheduleCommand(
|
||||
prompt,
|
||||
modelSelection: {
|
||||
providerId: asTrimmedString(args?.provider) ?? "cline",
|
||||
modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex",
|
||||
modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID,
|
||||
},
|
||||
mode: routineScheduleMode(args?.mode),
|
||||
mode: readHubScheduleMode(args, "yolo"),
|
||||
workspaceRoot,
|
||||
cwd: asTrimmedString(args?.cwd),
|
||||
systemPrompt: asTrimmedString(args?.system_prompt),
|
||||
@@ -677,6 +675,7 @@ async function handleRoutineScheduleCommand(
|
||||
const scheduleId = asTrimmedString(args?.schedule_id);
|
||||
if (!scheduleId) throw new Error(`${command} requires schedule_id`);
|
||||
if (command === "update_routine_schedule") {
|
||||
const mode = readHubScheduleMode(args);
|
||||
const name = asTrimmedString(args?.name);
|
||||
const timing = routineScheduleTiming(args);
|
||||
const prompt = asTrimmedString(args?.prompt);
|
||||
@@ -693,9 +692,9 @@ async function handleRoutineScheduleCommand(
|
||||
prompt,
|
||||
modelSelection: {
|
||||
providerId: asTrimmedString(args?.provider) ?? "cline",
|
||||
modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex",
|
||||
modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID,
|
||||
},
|
||||
mode: routineScheduleMode(args?.mode),
|
||||
...(mode === undefined ? {} : { mode }),
|
||||
workspaceRoot,
|
||||
cwd: asTrimmedString(args?.cwd) ?? null,
|
||||
systemPrompt:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared/browser";
|
||||
import {
|
||||
ArrowUp,
|
||||
Brain,
|
||||
@@ -62,7 +63,7 @@ const BUILTIN_SLASH_COMMANDS: SlashCommand[] = [
|
||||
];
|
||||
|
||||
const FALLBACK_PROVIDER_MODELS: Record<string, string[]> = {
|
||||
cline: ["anthropic/claude-sonnet-4.6"],
|
||||
cline: [CLINE_DEFAULT_MODEL_ID],
|
||||
anthropic: ["claude-sonnet-4-6"],
|
||||
"openai-native": ["gpt-5.5"],
|
||||
openrouter: ["anthropic/claude-sonnet-4.6"],
|
||||
@@ -70,7 +71,7 @@ const FALLBACK_PROVIDER_MODELS: Record<string, string[]> = {
|
||||
};
|
||||
|
||||
const FALLBACK_PROVIDER_REASONING_MODELS: Record<string, string[]> = {
|
||||
cline: ["anthropic/claude-sonnet-4.6"],
|
||||
cline: [CLINE_DEFAULT_MODEL_ID],
|
||||
anthropic: ["claude-sonnet-4-6"],
|
||||
"openai-native": ["gpt-5.5"],
|
||||
openrouter: ["anthropic/claude-sonnet-4.6"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
ONE_TIME_SCHEDULE_CRON_PATTERN,
|
||||
ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY,
|
||||
} from "@cline/shared/browser";
|
||||
@@ -166,7 +167,7 @@ interface ProcessContext {
|
||||
}
|
||||
|
||||
const FALLBACK_PROVIDER_MODELS: Record<string, string[]> = {
|
||||
cline: ["anthropic/claude-sonnet-4.6"],
|
||||
cline: [CLINE_DEFAULT_MODEL_ID],
|
||||
anthropic: ["claude-sonnet-4-6"],
|
||||
"openai-native": ["gpt-5.3-codex"],
|
||||
openrouter: ["anthropic/claude-sonnet-4.6"],
|
||||
@@ -245,7 +246,7 @@ function getScheduleProviderModel(schedule: RoutineSchedule): {
|
||||
model:
|
||||
schedule.modelSelection?.modelId?.trim() ||
|
||||
schedule.model?.trim() ||
|
||||
"openai/gpt-5.3-codex",
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -576,7 +577,7 @@ export function RoutineSchedulesContent({
|
||||
scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"],
|
||||
prompt: "Review PRs opened yesterday and summarize issues.",
|
||||
provider: "cline",
|
||||
model: "openai/gpt-5.3-codex",
|
||||
model: CLINE_DEFAULT_MODEL_ID,
|
||||
workspaceRoot: "",
|
||||
systemPrompt: "",
|
||||
timeoutSeconds: "",
|
||||
@@ -992,7 +993,7 @@ export function RoutineSchedulesContent({
|
||||
const model =
|
||||
asTrimmedFormString(createForm.model) ||
|
||||
(visibleProviderModels[provider] ?? [])[0] ||
|
||||
"openai/gpt-5.3-codex";
|
||||
CLINE_DEFAULT_MODEL_ID;
|
||||
const systemPrompt = asTrimmedFormString(createForm.systemPrompt);
|
||||
const timeoutSeconds = parseOptionalPositiveInt(
|
||||
createForm.timeoutSeconds,
|
||||
@@ -1013,7 +1014,7 @@ export function RoutineSchedulesContent({
|
||||
prompt,
|
||||
provider,
|
||||
model,
|
||||
mode: editingSchedule?.mode ?? "act",
|
||||
mode: editingSchedule?.mode ?? "yolo",
|
||||
workspace_root: workspaceRoot,
|
||||
cwd: editingSchedule ? (editingSchedule.cwd ?? null) : workspaceRoot,
|
||||
system_prompt: editingSchedule
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared/browser";
|
||||
import type { ChatSessionConfig } from "@/lib/chat-schema";
|
||||
import { readModelSelectionStorageFromWindow } from "@/lib/model-selection";
|
||||
import { normalizeProviderId } from "@/lib/provider-id";
|
||||
@@ -16,15 +17,12 @@ export const OAUTH_MANAGED_PROVIDERS = new Set([
|
||||
"openai-codex",
|
||||
]);
|
||||
|
||||
// Default Cline model — keep in sync with @cline/llms CLINE_DEFAULT_MODEL
|
||||
const CLINE_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6";
|
||||
|
||||
export const DEFAULT_CHAT_CONFIG: ChatSessionConfig = {
|
||||
sessionId: undefined,
|
||||
workspaceRoot: "",
|
||||
cwd: "",
|
||||
provider: "cline",
|
||||
model: CLINE_DEFAULT_MODEL,
|
||||
model: CLINE_DEFAULT_MODEL_ID,
|
||||
apiKey: process.env.CLINE_API_KEY || "",
|
||||
mode: "act",
|
||||
systemPrompt: undefined,
|
||||
|
||||
@@ -138,7 +138,9 @@ function makeBaseConfig(overrides: Partial<CoreSessionConfig> = {}): CoreSession
|
||||
|
||||
describe("getDefaultModelIdForProvider", () => {
|
||||
it("uses the SDK provider catalog for the Cline default model", () => {
|
||||
expect(getDefaultModelIdForProvider("cline")).toBe("anthropic/claude-sonnet-4.6")
|
||||
expect(getDefaultModelIdForProvider("cline")).toBe(
|
||||
LlmsModels.MODEL_COLLECTIONS_BY_PROVIDER_ID.cline.provider.defaultModelId,
|
||||
)
|
||||
})
|
||||
|
||||
it("uses the generated Gemini provider default", () => {
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.2",
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ChatStartSessionRequest, CronOneOffSpec } from "@cline/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { DefaultToolNames } from "../../extensions/tools/constants";
|
||||
import type { HubScheduleRuntimeHandlers } from "../service/schedule-service";
|
||||
import { SqliteCronStore } from "../store/sqlite-cron-store";
|
||||
import { CronMaterializer } from "./cron-materializer";
|
||||
@@ -15,12 +17,25 @@ import { CronRunner } from "./cron-runner";
|
||||
|
||||
function fakeHandlers(): {
|
||||
handlers: HubScheduleRuntimeHandlers;
|
||||
calls: { start: number; send: number; stop: number; prompts: string[] };
|
||||
calls: {
|
||||
start: number;
|
||||
send: number;
|
||||
stop: number;
|
||||
prompts: string[];
|
||||
startRequests: ChatStartSessionRequest[];
|
||||
};
|
||||
} {
|
||||
const calls = { start: 0, send: 0, stop: 0, prompts: [] as string[] };
|
||||
const calls = {
|
||||
start: 0,
|
||||
send: 0,
|
||||
stop: 0,
|
||||
prompts: [] as string[],
|
||||
startRequests: [] as ChatStartSessionRequest[],
|
||||
};
|
||||
const handlers: HubScheduleRuntimeHandlers = {
|
||||
async startSession(_req) {
|
||||
async startSession(req) {
|
||||
calls.start += 1;
|
||||
calls.startRequests.push(req);
|
||||
return { sessionId: `sess_${calls.start}` };
|
||||
},
|
||||
async sendSession(_sessionId, req) {
|
||||
@@ -109,6 +124,16 @@ describe("CronRunner", () => {
|
||||
expect(calls.start).toBe(1);
|
||||
expect(calls.send).toBe(1);
|
||||
expect(calls.stop).toBe(1);
|
||||
expect(calls.startRequests[0]?.mode).toBe("yolo");
|
||||
expect(calls.startRequests[0]?.toolPolicies?.["*"]).toEqual({
|
||||
autoApprove: true,
|
||||
});
|
||||
expect(
|
||||
calls.startRequests[0]?.toolPolicies?.[DefaultToolNames.ASK],
|
||||
).toEqual({ enabled: false, autoApprove: true });
|
||||
expect(
|
||||
calls.startRequests[0]?.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT],
|
||||
).toEqual({ enabled: true, autoApprove: true });
|
||||
|
||||
const run = requireValue(
|
||||
store.listRuns({ specId: upserted.record.specId })[0],
|
||||
@@ -119,6 +144,108 @@ describe("CronRunner", () => {
|
||||
expect(existsSync(reportPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to yolo for an unknown mode and disables questions", async () => {
|
||||
const { handlers, calls } = fakeHandlers();
|
||||
const upserted = store.upsertSpec({
|
||||
externalId: "headless-unknown",
|
||||
sourcePath: "headless-unknown.md",
|
||||
triggerKind: "one_off",
|
||||
sourceHash: "h",
|
||||
parseStatus: "valid",
|
||||
spec: {
|
||||
triggerKind: "one_off",
|
||||
id: "headless-unknown",
|
||||
title: "Headless unknown",
|
||||
prompt: "Do it",
|
||||
workspaceRoot,
|
||||
enabled: true,
|
||||
mode: "unknown" as CronOneOffSpec["mode"],
|
||||
tools: [DefaultToolNames.ASK, DefaultToolNames.READ_FILES],
|
||||
},
|
||||
});
|
||||
store.updateSpecNextRunAt(
|
||||
upserted.record.specId,
|
||||
new Date(Date.now() - 1_000).toISOString(),
|
||||
);
|
||||
const runner = new CronRunner({
|
||||
store,
|
||||
materializer,
|
||||
runtimeHandlers: handlers,
|
||||
workspaceRoot,
|
||||
specs: { cronSpecsDir: cronDir },
|
||||
});
|
||||
|
||||
await runner.tick();
|
||||
await runner.dispose();
|
||||
|
||||
const request = requireValue(calls.startRequests[0]);
|
||||
expect(request.mode).toBe("yolo");
|
||||
expect(request.toolPolicies?.["*"]).toEqual({
|
||||
enabled: false,
|
||||
autoApprove: true,
|
||||
});
|
||||
expect(request.toolPolicies?.[DefaultToolNames.READ_FILES]).toEqual({
|
||||
enabled: true,
|
||||
autoApprove: true,
|
||||
});
|
||||
expect(request.toolPolicies?.[DefaultToolNames.ASK]).toEqual({
|
||||
enabled: false,
|
||||
autoApprove: true,
|
||||
});
|
||||
expect(request.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT]).toEqual({
|
||||
enabled: true,
|
||||
autoApprove: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"act",
|
||||
"plan",
|
||||
"yolo",
|
||||
] as const)("preserves an explicit %s mode for scheduled runs", async (mode) => {
|
||||
const { handlers, calls } = fakeHandlers();
|
||||
const upserted = store.upsertSpec({
|
||||
externalId: `explicit-${mode}`,
|
||||
sourcePath: `explicit-${mode}.md`,
|
||||
triggerKind: "one_off",
|
||||
sourceHash: `h-${mode}`,
|
||||
parseStatus: "valid",
|
||||
spec: {
|
||||
triggerKind: "one_off",
|
||||
id: `explicit-${mode}`,
|
||||
title: `Explicit ${mode}`,
|
||||
prompt: "Do it",
|
||||
workspaceRoot,
|
||||
enabled: true,
|
||||
mode,
|
||||
},
|
||||
});
|
||||
store.updateSpecNextRunAt(
|
||||
upserted.record.specId,
|
||||
new Date(Date.now() - 1_000).toISOString(),
|
||||
);
|
||||
const runner = new CronRunner({
|
||||
store,
|
||||
materializer,
|
||||
runtimeHandlers: handlers,
|
||||
workspaceRoot,
|
||||
specs: { cronSpecsDir: cronDir },
|
||||
});
|
||||
|
||||
await runner.tick();
|
||||
await runner.dispose();
|
||||
|
||||
const request = requireValue(calls.startRequests[0]);
|
||||
expect(request.mode).toBe(mode);
|
||||
expect(request.toolPolicies?.[DefaultToolNames.ASK]).toEqual({
|
||||
enabled: false,
|
||||
autoApprove: true,
|
||||
});
|
||||
expect(request.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT]).toEqual(
|
||||
mode === "yolo" ? { enabled: true, autoApprove: true } : undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("marks runs failed when the runtime throws", async () => {
|
||||
const handlers: HubScheduleRuntimeHandlers = {
|
||||
async startSession() {
|
||||
|
||||
@@ -61,16 +61,20 @@ function cronExtensionEnabled(
|
||||
function buildToolPolicies(
|
||||
spec: CronSpecRecord,
|
||||
mode: "act" | "plan" | "yolo",
|
||||
): ChatStartSessionRequest["toolPolicies"] | undefined {
|
||||
if (spec.tools === undefined) {
|
||||
return { "*": { autoApprove: true } };
|
||||
}
|
||||
const policies: NonNullable<ChatStartSessionRequest["toolPolicies"]> = {
|
||||
"*": { enabled: false, autoApprove: true },
|
||||
};
|
||||
for (const tool of spec.tools) {
|
||||
): NonNullable<ChatStartSessionRequest["toolPolicies"]> {
|
||||
const policies: NonNullable<ChatStartSessionRequest["toolPolicies"]> =
|
||||
spec.tools === undefined
|
||||
? { "*": { autoApprove: true } }
|
||||
: { "*": { enabled: false, autoApprove: true } };
|
||||
for (const tool of spec.tools ?? []) {
|
||||
policies[tool] = { enabled: true, autoApprove: true };
|
||||
}
|
||||
// Scheduled runs are headless, so they cannot wait for a human response.
|
||||
policies[DefaultToolNames.ASK] = {
|
||||
...policies[DefaultToolNames.ASK],
|
||||
enabled: false,
|
||||
autoApprove: true,
|
||||
};
|
||||
if (mode === "yolo") {
|
||||
policies[DefaultToolNames.SUBMIT_AND_EXIT] = {
|
||||
enabled: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
HubScheduleCreateInput,
|
||||
HubScheduleUpdateInput,
|
||||
} from "@cline/shared";
|
||||
import { createSessionId } from "@cline/shared";
|
||||
import { createSessionId, readHubScheduleMode } from "@cline/shared";
|
||||
import type { HubScheduleService } from "./schedule-service";
|
||||
|
||||
function okReply(
|
||||
@@ -158,6 +158,7 @@ export class HubScheduleCommandService {
|
||||
private toCreateInput(
|
||||
payload: Record<string, unknown>,
|
||||
): HubScheduleCreateInput {
|
||||
const mode = readHubScheduleMode(payload, "yolo");
|
||||
const modelSelection =
|
||||
payload.modelSelection &&
|
||||
typeof payload.modelSelection === "object" &&
|
||||
@@ -172,12 +173,14 @@ export class HubScheduleCommandService {
|
||||
return {
|
||||
...(payload as unknown as HubScheduleCreateInput),
|
||||
modelSelection,
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
private toUpdateInput(
|
||||
payload: Record<string, unknown>,
|
||||
): HubScheduleUpdateInput {
|
||||
const mode = readHubScheduleMode(payload);
|
||||
const modelSelection =
|
||||
payload.modelSelection &&
|
||||
typeof payload.modelSelection === "object" &&
|
||||
@@ -193,6 +196,7 @@ export class HubScheduleCommandService {
|
||||
return {
|
||||
...(payload as unknown as HubScheduleUpdateInput),
|
||||
modelSelection,
|
||||
...(mode === undefined ? {} : { mode }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
type CronScheduleSpec,
|
||||
ONE_TIME_SCHEDULE_CRON_PATTERN,
|
||||
ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY,
|
||||
} from "@cline/shared";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SqliteCronStore } from "../store/sqlite-cron-store";
|
||||
import { HubScheduleCommandService } from "./schedule-command-service";
|
||||
import { HubScheduleService } from "./schedule-service";
|
||||
|
||||
@@ -80,6 +82,7 @@ describe("HubScheduleService", () => {
|
||||
timeoutSeconds: 30,
|
||||
metadata: { delivery: { threadId: "thread-1" } },
|
||||
});
|
||||
expect(created.mode).toBe("yolo");
|
||||
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("success");
|
||||
@@ -110,6 +113,57 @@ describe("HubScheduleService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
sqliteIt(
|
||||
"maps missing and unknown stored modes to yolo while preserving act",
|
||||
async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const store = new SqliteCronStore({ dbPath });
|
||||
for (const [scheduleId, mode] of [
|
||||
["sched-missing", undefined],
|
||||
["sched-unknown", "unknown"],
|
||||
["sched-act", "act"],
|
||||
] as const) {
|
||||
store.upsertSpec({
|
||||
externalId: scheduleId,
|
||||
sourcePath: `hub/schedules/${scheduleId}.cron.md`,
|
||||
triggerKind: "schedule",
|
||||
sourceHash: `hash-${scheduleId}`,
|
||||
parseStatus: "valid",
|
||||
spec: {
|
||||
triggerKind: "schedule",
|
||||
id: scheduleId,
|
||||
title: scheduleId,
|
||||
prompt: "Do the work",
|
||||
workspaceRoot: "/workspace",
|
||||
schedule: "0 * * * *",
|
||||
enabled: true,
|
||||
mode: mode as CronScheduleSpec["mode"],
|
||||
source: "hub-schedule",
|
||||
},
|
||||
});
|
||||
}
|
||||
store.close();
|
||||
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "unused" })),
|
||||
sendSession: vi.fn(async () => ({ result: { text: "unused" } })),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
});
|
||||
try {
|
||||
expect(service.getSchedule("sched-missing")?.mode).toBe("yolo");
|
||||
expect(service.getSchedule("sched-unknown")?.mode).toBe("yolo");
|
||||
expect(service.getSchedule("sched-act")?.mode).toBe("act");
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
sqliteIt("publishes failed schedule execution events", async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
@@ -237,7 +291,63 @@ describe("HubScheduleService", () => {
|
||||
expect(createdReply.ok).toBe(true);
|
||||
const created = createdReply.payload?.schedule as {
|
||||
scheduleId: string;
|
||||
mode: string;
|
||||
};
|
||||
expect(created.mode).toBe("yolo");
|
||||
|
||||
const planReply = await commands.handleCommand({
|
||||
version: "v1",
|
||||
command: "schedule.update",
|
||||
payload: { scheduleId: created.scheduleId, mode: "plan" },
|
||||
});
|
||||
expect(planReply.ok).toBe(true);
|
||||
expect((planReply.payload?.schedule as { mode: string }).mode).toBe(
|
||||
"plan",
|
||||
);
|
||||
|
||||
const omittedModeReply = await commands.handleCommand({
|
||||
version: "v1",
|
||||
command: "schedule.update",
|
||||
payload: { scheduleId: created.scheduleId, name: "Renamed routine" },
|
||||
});
|
||||
expect(omittedModeReply.ok).toBe(true);
|
||||
expect(
|
||||
(omittedModeReply.payload?.schedule as { mode: string }).mode,
|
||||
).toBe("plan");
|
||||
|
||||
for (const invalidMode of [null, "", "invalid"]) {
|
||||
const invalidUpdateReply = await commands.handleCommand({
|
||||
version: "v1",
|
||||
command: "schedule.update",
|
||||
payload: { scheduleId: created.scheduleId, mode: invalidMode },
|
||||
});
|
||||
expect(invalidUpdateReply).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "schedule_command_failed",
|
||||
message: "mode must be one of: act, plan, yolo",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const invalidCreateReply = await commands.handleCommand({
|
||||
version: "v1",
|
||||
command: "schedule.create",
|
||||
payload: {
|
||||
name: "Invalid routine",
|
||||
cronPattern: "30 * * * *",
|
||||
prompt: "Do not create",
|
||||
workspaceRoot: "/workspace",
|
||||
mode: "invalid",
|
||||
},
|
||||
});
|
||||
expect(invalidCreateReply).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "schedule_command_failed",
|
||||
message: "mode must be one of: act, plan, yolo",
|
||||
},
|
||||
});
|
||||
|
||||
const listReply = await commands.handleCommand({
|
||||
version: "v1",
|
||||
@@ -245,11 +355,13 @@ describe("HubScheduleService", () => {
|
||||
payload: { limit: 10 },
|
||||
});
|
||||
expect(listReply.ok).toBe(true);
|
||||
expect(
|
||||
(listReply.payload?.schedules as Array<{ scheduleId: string }>).some(
|
||||
(item) => item.scheduleId === created.scheduleId,
|
||||
),
|
||||
).toBe(true);
|
||||
const listedSchedule = (
|
||||
listReply.payload?.schedules as Array<{
|
||||
scheduleId: string;
|
||||
mode: string;
|
||||
}>
|
||||
).find((item) => item.scheduleId === created.scheduleId);
|
||||
expect(listedSchedule).toMatchObject({ mode: "plan" });
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ function specToSchedule(spec: CronSpecRecord): ScheduleRecord {
|
||||
}
|
||||
: undefined,
|
||||
enabled: spec.enabled && !spec.removed && spec.parseStatus === "valid",
|
||||
mode: spec.mode === "plan" ? "plan" : spec.mode === "yolo" ? "yolo" : "act",
|
||||
mode: spec.mode === "plan" ? "plan" : spec.mode === "act" ? "act" : "yolo",
|
||||
systemPrompt: spec.systemPrompt,
|
||||
maxIterations: spec.maxIterations,
|
||||
timeoutSeconds: spec.timeoutSeconds,
|
||||
|
||||
@@ -52,6 +52,7 @@ describe("computeContentHash", () => {
|
||||
expect(r.triggerKind).toBe("one_off");
|
||||
expect(r.spec?.title).toBe("Clean");
|
||||
expect(r.spec?.prompt).toBe("Remove stale files.");
|
||||
expect(r.spec?.mode).toBe("act");
|
||||
});
|
||||
|
||||
it("defaults to yolo and parses cron runtime fields", () => {
|
||||
|
||||
@@ -78,6 +78,72 @@ describe("SqliteCronStore", () => {
|
||||
expect(result.record.source).toBe("automation");
|
||||
});
|
||||
|
||||
it("defaults hub schedules to yolo and preserves explicit modes on update", () => {
|
||||
const created = store.createHubSchedule({
|
||||
name: "Routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Do the work",
|
||||
workspaceRoot: "/ws",
|
||||
});
|
||||
expect(created.mode).toBe("yolo");
|
||||
|
||||
const act = store.updateHubSchedule(created.externalId, {
|
||||
scheduleId: created.externalId,
|
||||
mode: "act",
|
||||
});
|
||||
expect(act?.mode).toBe("act");
|
||||
expect(
|
||||
store.updateHubSchedule(created.externalId, {
|
||||
scheduleId: created.externalId,
|
||||
name: "Renamed routine",
|
||||
})?.mode,
|
||||
).toBe("act");
|
||||
|
||||
expect(
|
||||
store.updateHubSchedule(created.externalId, {
|
||||
scheduleId: created.externalId,
|
||||
mode: "plan",
|
||||
})?.mode,
|
||||
).toBe("plan");
|
||||
expect(
|
||||
store.updateHubSchedule(created.externalId, {
|
||||
scheduleId: created.externalId,
|
||||
mode: "yolo",
|
||||
})?.mode,
|
||||
).toBe("yolo");
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
"unknown",
|
||||
])("falls back to yolo when updating a hub schedule with stored mode %s", (storedMode) => {
|
||||
const scheduleId = `sched-${storedMode ?? "missing"}`;
|
||||
store.upsertSpec({
|
||||
externalId: scheduleId,
|
||||
sourcePath: `hub/schedules/${scheduleId}.cron.md`,
|
||||
triggerKind: "schedule",
|
||||
sourceHash: "legacy-hash",
|
||||
parseStatus: "valid",
|
||||
spec: {
|
||||
triggerKind: "schedule",
|
||||
id: scheduleId,
|
||||
title: "Legacy routine",
|
||||
prompt: "Do the work",
|
||||
workspaceRoot: "/ws",
|
||||
schedule: "0 * * * *",
|
||||
enabled: true,
|
||||
mode: storedMode as CronScheduleSpec["mode"],
|
||||
source: "hub-schedule",
|
||||
},
|
||||
});
|
||||
|
||||
const updated = store.updateHubSchedule(scheduleId, {
|
||||
scheduleId,
|
||||
name: "Updated routine",
|
||||
});
|
||||
expect(updated?.mode).toBe("yolo");
|
||||
});
|
||||
|
||||
it("does not bump revision on cosmetic-only re-upsert with same hash", () => {
|
||||
store.upsertSpec({
|
||||
externalId: "cleanup",
|
||||
|
||||
@@ -391,7 +391,7 @@ function hubScheduleInputToCronSpec(input: HubScheduleCreateInput): CronSpec {
|
||||
title: input.name.trim(),
|
||||
prompt: input.prompt,
|
||||
workspaceRoot: input.workspaceRoot.trim(),
|
||||
mode: input.mode ?? "act",
|
||||
mode: input.mode ?? "yolo",
|
||||
systemPrompt: input.systemPrompt,
|
||||
modelSelection: input.modelSelection
|
||||
? JSON.parse(JSON.stringify(input.modelSelection))
|
||||
@@ -491,9 +491,9 @@ function cronSpecRecordToHubScheduleInput(
|
||||
updates.mode ??
|
||||
(current.mode === "plan"
|
||||
? "plan"
|
||||
: current.mode === "yolo"
|
||||
? "yolo"
|
||||
: "act"),
|
||||
: current.mode === "act"
|
||||
? "act"
|
||||
: "yolo"),
|
||||
systemPrompt:
|
||||
updates.systemPrompt === null
|
||||
? undefined
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
HubScheduleRuntimeHandlers,
|
||||
HubScheduleServiceOptions,
|
||||
} from "../../cron/service/schedule-service";
|
||||
import type { VerifySubmitExecutor } from "../../extensions/tools";
|
||||
import { LocalRuntimeHost } from "../../runtime/host/local-runtime-host";
|
||||
import { SqliteSessionStore } from "../../services/storage/sqlite-session-store";
|
||||
import { CoreSessionService } from "../../session/services/session-service";
|
||||
@@ -72,8 +73,14 @@ export interface CreateLocalHubScheduleRuntimeHandlersOptions
|
||||
export function createLocalHubScheduleRuntimeHandlers(
|
||||
options: CreateLocalHubScheduleRuntimeHandlersOptions = {},
|
||||
): HubScheduleRuntimeHandlers {
|
||||
const submitScheduledRun: VerifySubmitExecutor = async (summary) => summary;
|
||||
const sessionHost = new LocalRuntimeHost({
|
||||
sessionService: new CoreSessionService(new SqliteSessionStore()),
|
||||
capabilities: {
|
||||
toolExecutors: {
|
||||
submit: submitScheduledRun,
|
||||
},
|
||||
},
|
||||
fetch: options.fetch,
|
||||
telemetry: options.telemetry,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { RuntimeCapabilities } from "../../runtime/capabilities/runtime-capabilities";
|
||||
|
||||
const localRuntimeHostMock = vi.hoisted(() =>
|
||||
vi.fn().mockImplementation(function (this: unknown, _options: unknown) {
|
||||
@@ -17,7 +19,7 @@ vi.mock("../../runtime/host/local-runtime-host", () => ({
|
||||
LocalRuntimeHost: localRuntimeHostMock,
|
||||
}));
|
||||
|
||||
describe("hub server fetch wiring", () => {
|
||||
describe("hub runtime wiring", () => {
|
||||
it("forwards observability into the internal LocalRuntimeHost", async () => {
|
||||
localRuntimeHostMock.mockClear();
|
||||
const { HubServerTransport } = (await import(".")) as unknown as {
|
||||
@@ -141,4 +143,73 @@ describe("hub server fetch wiring", () => {
|
||||
};
|
||||
expect(constructorArgs.fetch).toBeUndefined();
|
||||
});
|
||||
|
||||
it("provides an executable headless completion tool to the real yolo runtime builder", async () => {
|
||||
localRuntimeHostMock.mockClear();
|
||||
const { createLocalHubScheduleRuntimeHandlers } = await import(
|
||||
"../daemon/runtime-handlers"
|
||||
);
|
||||
const { DefaultRuntimeBuilder } = await import(
|
||||
"../../runtime/orchestration/runtime-builder"
|
||||
);
|
||||
|
||||
createLocalHubScheduleRuntimeHandlers();
|
||||
|
||||
const constructorArgs = localRuntimeHostMock.mock.calls[0]?.[0] as {
|
||||
capabilities?: RuntimeCapabilities;
|
||||
};
|
||||
const toolExecutors = constructorArgs.capabilities?.toolExecutors;
|
||||
expect(toolExecutors?.submit).toBeTypeOf("function");
|
||||
|
||||
const runtime = await new DefaultRuntimeBuilder().build({
|
||||
config: {
|
||||
providerId: "cline",
|
||||
modelId: CLINE_DEFAULT_MODEL_ID,
|
||||
cwd: process.cwd(),
|
||||
workspaceRoot: process.cwd(),
|
||||
systemPrompt: "Run unattended.",
|
||||
mode: "yolo",
|
||||
enableTools: true,
|
||||
enableSpawnAgent: false,
|
||||
enableAgentTeams: false,
|
||||
},
|
||||
toolExecutors,
|
||||
toolPolicies: {
|
||||
"*": { enabled: false, autoApprove: true },
|
||||
submit_and_exit: { enabled: true, autoApprove: true },
|
||||
ask_question: { enabled: false, autoApprove: true },
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const toolNames = runtime.tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("submit_and_exit");
|
||||
expect(toolNames).not.toContain("ask_question");
|
||||
const submitTool = runtime.tools.find(
|
||||
(tool) => tool.name === "submit_and_exit",
|
||||
);
|
||||
if (!submitTool) {
|
||||
throw new Error("Expected submit_and_exit to be available.");
|
||||
}
|
||||
expect(submitTool.lifecycle).toEqual({ completesRun: true });
|
||||
expect(runtime.completionPolicy).toEqual({
|
||||
requireCompletionTool: true,
|
||||
});
|
||||
await expect(
|
||||
submitTool.execute(
|
||||
{
|
||||
summary: "Scheduled work completed successfully.",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
agentId: "scheduled-agent",
|
||||
conversationId: "scheduled-conversation",
|
||||
iteration: 1,
|
||||
},
|
||||
),
|
||||
).resolves.toBe("Scheduled work completed successfully.");
|
||||
} finally {
|
||||
await runtime.shutdown("test complete");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import * as LlmsModels from "@cline/llms";
|
||||
import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearLiveModelsCatalogCache } from "../llms/provider-defaults";
|
||||
import { ProviderSettingsManager } from "../storage/provider-settings-manager";
|
||||
@@ -883,7 +884,7 @@ describe("models.json model overlays", () => {
|
||||
expect(provider).toMatchObject({
|
||||
id: "cline",
|
||||
baseUrl: "https://api.cline.bot/api/v1",
|
||||
defaultModelId: "anthropic/claude-sonnet-4.6",
|
||||
defaultModelId: CLINE_DEFAULT_MODEL_ID,
|
||||
});
|
||||
|
||||
const { models } = await getLocalProviderModels("cline");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
export type {
|
||||
ModelCollection,
|
||||
ModelIdAliasRule,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
|
||||
export type {
|
||||
ModelCollection,
|
||||
ModelIdAliasRule,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CLINE_ENVIRONMENT_ENV, CLINE_ENVIRONMENTS } from "@cline/shared";
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
CLINE_ENVIRONMENT_ENV,
|
||||
CLINE_ENVIRONMENTS,
|
||||
} from "@cline/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { BUILTIN_SPECS } from "./builtins";
|
||||
import { getModelsForProvider, getProvider } from "./model-registry";
|
||||
@@ -52,6 +56,10 @@ describe("cline builtin spec defaults.baseUrl", () => {
|
||||
});
|
||||
|
||||
describe("cline builtin models", () => {
|
||||
it("exposes its canonical default model ID", () => {
|
||||
expect(findClineSpec().defaultModelId).toBe(CLINE_DEFAULT_MODEL_ID);
|
||||
});
|
||||
|
||||
it("prefers Vercel-style Z.ai model ids over equivalent OpenRouter ids", async () => {
|
||||
const models = await getModelsForProvider("cline");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
CLINE_DEFAULT_MODEL_ID,
|
||||
type GatewayModelCapability,
|
||||
type GatewayModelDefinition,
|
||||
type GatewayProviderManifest,
|
||||
@@ -45,7 +46,6 @@ export const DEFAULT_INTERNAL_OCA_BASE_URL =
|
||||
"https://code-internal.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm";
|
||||
export const DEFAULT_EXTERNAL_OCA_BASE_URL =
|
||||
"https://code.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm";
|
||||
const CLINE_DEFAULT_MODEL_ID = "anthropic/claude-sonnet-4.6";
|
||||
const CLINE_PASS_PROVIDER_ID = "cline-pass";
|
||||
const OPENAI_CODEX_DEFAULT_MODEL_ID = "gpt-5.4";
|
||||
const OPENROUTER_STICKY_SESSION_METADATA: GatewayProviderMetadata = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isHubProtocolCompatible } from "./hub";
|
||||
import { isHubProtocolCompatible, readHubScheduleMode } from "./hub";
|
||||
|
||||
describe("isHubProtocolCompatible", () => {
|
||||
it("accepts a hub whose supported client range includes the client protocol", () => {
|
||||
@@ -29,3 +29,26 @@ describe("isHubProtocolCompatible", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("readHubScheduleMode", () => {
|
||||
it("defaults only when mode is absent", () => {
|
||||
expect(readHubScheduleMode(undefined, "yolo")).toBe("yolo");
|
||||
expect(readHubScheduleMode({}, "yolo")).toBe("yolo");
|
||||
expect(readHubScheduleMode({ mode: "plan" }, "yolo")).toBe("plan");
|
||||
});
|
||||
|
||||
it("preserves omission for schedule updates", () => {
|
||||
expect(readHubScheduleMode({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
null,
|
||||
"",
|
||||
"invalid",
|
||||
])("rejects a present invalid mode: %s", (mode) => {
|
||||
expect(() => readHubScheduleMode({ mode }, "yolo")).toThrow(
|
||||
"mode must be one of: act, plan, yolo",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,6 +329,34 @@ export interface ScheduleExecutionRecord {
|
||||
export const ONE_TIME_SCHEDULE_CRON_PATTERN = "0";
|
||||
export const ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY = "__hubScheduleRunAt";
|
||||
|
||||
export const HUB_SCHEDULE_MODES = ["act", "plan", "yolo"] as const;
|
||||
export type HubScheduleMode = (typeof HUB_SCHEDULE_MODES)[number];
|
||||
|
||||
export function isHubScheduleMode(value: unknown): value is HubScheduleMode {
|
||||
return HUB_SCHEDULE_MODES.some((mode) => mode === value);
|
||||
}
|
||||
|
||||
export function readHubScheduleMode(
|
||||
payload: Record<string, unknown> | undefined,
|
||||
defaultWhenAbsent: HubScheduleMode,
|
||||
): HubScheduleMode;
|
||||
export function readHubScheduleMode(
|
||||
payload: Record<string, unknown> | undefined,
|
||||
): HubScheduleMode | undefined;
|
||||
export function readHubScheduleMode(
|
||||
payload: Record<string, unknown> | undefined,
|
||||
defaultWhenAbsent?: HubScheduleMode,
|
||||
): HubScheduleMode | undefined {
|
||||
if (!payload || !Object.hasOwn(payload, "mode")) {
|
||||
return defaultWhenAbsent;
|
||||
}
|
||||
const mode = payload.mode;
|
||||
if (isHubScheduleMode(mode)) {
|
||||
return mode;
|
||||
}
|
||||
throw new Error(`mode must be one of: ${HUB_SCHEDULE_MODES.join(", ")}`);
|
||||
}
|
||||
|
||||
export interface HubScheduleCreateInput {
|
||||
name: string;
|
||||
cronPattern: string;
|
||||
@@ -337,7 +365,7 @@ export interface HubScheduleCreateInput {
|
||||
cwd?: string;
|
||||
modelSelection?: GatewayModelSelection;
|
||||
enabled?: boolean;
|
||||
mode?: "act" | "plan" | "yolo";
|
||||
mode?: HubScheduleMode;
|
||||
systemPrompt?: string;
|
||||
maxIterations?: number;
|
||||
timeoutSeconds?: number;
|
||||
@@ -357,7 +385,7 @@ export interface HubScheduleUpdateInput {
|
||||
cwd?: string;
|
||||
modelSelection?: GatewayModelSelection;
|
||||
enabled?: boolean;
|
||||
mode?: "act" | "plan" | "yolo";
|
||||
mode?: HubScheduleMode;
|
||||
systemPrompt?: string | null;
|
||||
maxIterations?: number | null;
|
||||
timeoutSeconds?: number | null;
|
||||
|
||||
@@ -258,6 +258,7 @@ export {
|
||||
stripModeNotices,
|
||||
xmlTagsRemoval,
|
||||
} from "./prompt/format";
|
||||
export { CLINE_DEFAULT_MODEL_ID } from "./providers/defaults";
|
||||
export { isClineProvider } from "./providers/utils";
|
||||
export { REMOTE_URI_SCHEME } from "./remote-config/constants";
|
||||
export type {
|
||||
|
||||
@@ -274,6 +274,7 @@ export {
|
||||
stripModeNotices,
|
||||
xmlTagsRemoval,
|
||||
} from "./prompt/format";
|
||||
export { CLINE_DEFAULT_MODEL_ID } from "./providers/defaults";
|
||||
export { isClineProvider } from "./providers/utils";
|
||||
export {
|
||||
buildRemoteConfigSessionBlobUploadMetadata,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Canonical default model for the Cline provider. */
|
||||
export const CLINE_DEFAULT_MODEL_ID = "anthropic/claude-sonnet-5";
|
||||
Reference in New Issue
Block a user