mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
* feat(core, desktop): add durable todo agenda * fix(desktop): secure todo approvals and track tool usage * fix(desktop): clean up failed approval delivery * fix(desktop): authenticate approval connections * fix(desktop): cancel approvals on broadcast failure * fix(desktop): authenticate development approvals * fix(desktop): harden development approvals * test(core): make task paths cross-platform * fix(desktop): serialize approval readiness * refactor(core): unify todo and schedule tools * feat(core): distinguish user todos from agent suggestions * fix(core): hide tasks tool in yolo mode * fix(core): enforce schedule workspace scope * fix(core): bind schedule scope to hub connection * fix(core): establish task scope at hub startup * fix(core): scope task automation by workspace * test(core): normalize workspace path expectations * test(core): serialize Windows CI workers * fix(core): reject unregistered schedule authority * fix(desktop): guard task execution commands * fix(core): avoid polynomial regex in mention parsing * fix(core): address schedule tool review feedback * fix(core): bind websocket clients to hub workspace * fix(core): flatten tasks tool input schema * fix(core): authorize multi-workspace hub clients * test(core): type hub transport authority mock * fix(cli): register a workspace client for remote schedule commands (#13398) --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
245 lines
6.3 KiB
TypeScript
245 lines
6.3 KiB
TypeScript
import {
|
|
createLocalHubScheduleRuntimeHandlers,
|
|
HubScheduleCommandService,
|
|
HubScheduleService,
|
|
NodeHubClient,
|
|
} from "@cline/core";
|
|
import {
|
|
ensureCliHubServer,
|
|
parseHubEndpointOverride,
|
|
} from "../../utils/hub-runtime";
|
|
import type { CommandIo } from "./types";
|
|
|
|
export class HubScheduleClient {
|
|
private hub: Promise<NodeHubClient> | undefined;
|
|
|
|
constructor(
|
|
private readonly url: string,
|
|
private readonly workspaceRoot: string,
|
|
private readonly authToken?: string,
|
|
) {}
|
|
|
|
close(): void {
|
|
const hub = this.hub;
|
|
this.hub = undefined;
|
|
void hub?.then((client) => client.close()).catch(() => undefined);
|
|
}
|
|
|
|
// Schedule commands are authorized against the workspace bound to the
|
|
// connection's client registration, so all commands must share one
|
|
// registered connection instead of fire-and-forget envelopes.
|
|
private connectedHub(): Promise<NodeHubClient> {
|
|
this.hub ??= (async () => {
|
|
const client = new NodeHubClient({
|
|
url: this.url,
|
|
clientType: "cli-schedule",
|
|
displayName: "Cline CLI scheduler",
|
|
workspaceRoot: this.workspaceRoot,
|
|
cwd: this.workspaceRoot,
|
|
authToken: this.authToken,
|
|
});
|
|
try {
|
|
await client.connect();
|
|
} catch (error) {
|
|
client.close();
|
|
this.hub = undefined;
|
|
throw error;
|
|
}
|
|
return client;
|
|
})();
|
|
return this.hub;
|
|
}
|
|
|
|
private async command(
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
const client = await this.connectedHub();
|
|
const reply = await client.command(command as never, payload);
|
|
return (reply.payload ?? {}) as Record<string, unknown>;
|
|
}
|
|
|
|
async getActiveScheduledExecutions() {
|
|
return (await this.command("schedule.active")).executions;
|
|
}
|
|
|
|
async createSchedule(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.create", payload)).schedule;
|
|
}
|
|
|
|
async deleteSchedule(scheduleId: string) {
|
|
return (
|
|
(await this.command("schedule.delete", { scheduleId })).deleted === true
|
|
);
|
|
}
|
|
|
|
async getSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.get", { scheduleId })).schedule;
|
|
}
|
|
|
|
async listScheduleExecutions(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.list_executions", payload)).executions;
|
|
}
|
|
|
|
async listSchedules(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.list", payload)).schedules;
|
|
}
|
|
|
|
async pauseSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.disable", { scheduleId })).schedule;
|
|
}
|
|
|
|
async resumeSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.enable", { scheduleId })).schedule;
|
|
}
|
|
|
|
async getScheduleStats(scheduleId: string) {
|
|
return (await this.command("schedule.stats", { scheduleId })).stats;
|
|
}
|
|
|
|
async triggerScheduleNow(scheduleId: string) {
|
|
return (await this.command("schedule.trigger", { scheduleId })).execution;
|
|
}
|
|
|
|
async getUpcomingScheduledRuns(limit: number) {
|
|
return (await this.command("schedule.upcoming", { limit })).runs;
|
|
}
|
|
|
|
async updateSchedule(scheduleId: string, payload: Record<string, unknown>) {
|
|
return (
|
|
await this.command("schedule.update", {
|
|
scheduleId,
|
|
...payload,
|
|
})
|
|
).schedule;
|
|
}
|
|
}
|
|
|
|
export class LocalScheduleClient {
|
|
private readonly service = new HubScheduleService({
|
|
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
|
});
|
|
private readonly commands = new HubScheduleCommandService(this.service);
|
|
constructor(private readonly workspaceRoot: string) {}
|
|
|
|
close(): void {
|
|
void this.service.dispose();
|
|
}
|
|
|
|
private async command(
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
): Promise<Record<string, unknown>> {
|
|
const reply = await this.commands.handleCommand(
|
|
{
|
|
version: "v1",
|
|
clientId: "cline-schedule-local",
|
|
command: command as never,
|
|
payload,
|
|
},
|
|
{
|
|
clientId: "cline-schedule-local",
|
|
workspaceContext: {
|
|
workspaceRoot: this.workspaceRoot,
|
|
cwd: this.workspaceRoot,
|
|
},
|
|
},
|
|
);
|
|
if (!reply.ok) {
|
|
throw new Error(reply.error?.message ?? `hub command failed: ${command}`);
|
|
}
|
|
return (reply.payload ?? {}) as Record<string, unknown>;
|
|
}
|
|
|
|
async getActiveScheduledExecutions() {
|
|
return (await this.command("schedule.active")).executions;
|
|
}
|
|
|
|
async createSchedule(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.create", payload)).schedule;
|
|
}
|
|
|
|
async deleteSchedule(scheduleId: string) {
|
|
return (
|
|
(await this.command("schedule.delete", { scheduleId })).deleted === true
|
|
);
|
|
}
|
|
|
|
async getSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.get", { scheduleId })).schedule;
|
|
}
|
|
|
|
async listScheduleExecutions(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.list_executions", payload)).executions;
|
|
}
|
|
|
|
async listSchedules(payload: Record<string, unknown>) {
|
|
return (await this.command("schedule.list", payload)).schedules;
|
|
}
|
|
|
|
async pauseSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.disable", { scheduleId })).schedule;
|
|
}
|
|
|
|
async resumeSchedule(scheduleId: string) {
|
|
return (await this.command("schedule.enable", { scheduleId })).schedule;
|
|
}
|
|
|
|
async getScheduleStats(scheduleId: string) {
|
|
return (await this.command("schedule.stats", { scheduleId })).stats;
|
|
}
|
|
|
|
async triggerScheduleNow(scheduleId: string) {
|
|
return (await this.command("schedule.trigger", { scheduleId })).execution;
|
|
}
|
|
|
|
async getUpcomingScheduledRuns(limit: number) {
|
|
return (await this.command("schedule.upcoming", { limit })).runs;
|
|
}
|
|
|
|
async updateSchedule(scheduleId: string, payload: Record<string, unknown>) {
|
|
return (
|
|
await this.command("schedule.update", {
|
|
scheduleId,
|
|
...payload,
|
|
})
|
|
).schedule;
|
|
}
|
|
}
|
|
|
|
export async function ensureSchedulerHub(
|
|
address: string | undefined,
|
|
workspaceRoot: string,
|
|
_io: CommandIo,
|
|
): Promise<{
|
|
ok: boolean;
|
|
client: HubScheduleClient;
|
|
}> {
|
|
if (!address?.trim()) {
|
|
return {
|
|
ok: true,
|
|
client: new LocalScheduleClient(
|
|
workspaceRoot,
|
|
) as unknown as HubScheduleClient,
|
|
};
|
|
}
|
|
try {
|
|
const requestedEndpoint = parseHubEndpointOverride(address);
|
|
const { url: hubUrl, authToken } = await ensureCliHubServer(
|
|
workspaceRoot,
|
|
requestedEndpoint,
|
|
);
|
|
return {
|
|
ok: true,
|
|
client: new HubScheduleClient(hubUrl, workspaceRoot, authToken),
|
|
};
|
|
} catch (_error) {
|
|
return {
|
|
ok: true,
|
|
client: new LocalScheduleClient(
|
|
workspaceRoot,
|
|
) as unknown as HubScheduleClient,
|
|
};
|
|
}
|
|
}
|