@cursor/july 0.1.28 → 0.1.29
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/bin/agent-serve.js +20 -2
- package/dist/channels/slack/slack-channel.d.ts +19 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +29 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.CqjrYaT2.js → app.QdunVQKg.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGe61XHA.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.ag-MwUel.js → VPLocalSearchBox.B6EpUXYf.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BS-DJWat.js → theme.BOTJVqh7.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/cloud-turn-cost.d.ts +34 -0
- package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
- package/dist/internal/cloud-turn-cost.js +106 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/event-log.d.ts +6 -0
- package/dist/internal/event-log.d.ts.map +1 -1
- package/dist/internal/event-log.js +16 -0
- package/dist/internal/host-platforms.d.ts +13 -0
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +25 -16
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +3 -1
- package/dist/internal/local-control-plane.d.ts +25 -0
- package/dist/internal/local-control-plane.d.ts.map +1 -0
- package/dist/internal/local-control-plane.js +172 -0
- package/dist/internal/platform-schedule-sync.d.ts +17 -0
- package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
- package/dist/internal/platform-schedule-sync.js +72 -0
- package/dist/internal/platform-timers.d.ts +28 -0
- package/dist/internal/platform-timers.d.ts.map +1 -0
- package/dist/internal/platform-timers.js +120 -0
- package/dist/internal/reminder-runner.d.ts +16 -12
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +112 -61
- package/dist/internal/reminder-store.d.ts +5 -4
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +5 -4
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +44 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +105 -11
- package/dist/internal/session-engine.d.ts +22 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +110 -4
- package/dist/internal/storage-coordinator.d.ts +4 -15
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +4 -30
- package/dist/playground/assets/index-CquRB-l0.js +85 -0
- package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +0 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +0 -3
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +21 -2
- package/src/channels/slack/slack-channel.ts +46 -0
- package/src/internal/cloud-turn-cost.ts +158 -0
- package/src/internal/discovery.ts +1 -0
- package/src/internal/event-log.ts +17 -0
- package/src/internal/host-platforms.ts +40 -18
- package/src/internal/http-channel.ts +3 -1
- package/src/internal/local-control-plane.ts +188 -0
- package/src/internal/platform-schedule-sync.ts +81 -0
- package/src/internal/platform-timers.ts +173 -0
- package/src/internal/reminder-runner.ts +131 -67
- package/src/internal/reminder-store.ts +5 -4
- package/src/internal/sdk-runner.ts +49 -1
- package/src/internal/server.ts +121 -6
- package/src/internal/session-engine.ts +138 -3
- package/src/internal/storage-coordinator.ts +4 -41
- package/src/storage.ts +0 -5
- package/src/types.ts +11 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +0 -1
- package/dist/playground/assets/index-CkN74U_L.js +0 -87
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
tryResolveGhAuthToken,
|
|
14
14
|
} from "../channels/github/auth.js";
|
|
15
15
|
import type { GitHubChannelCredentials } from "../channels/github/types.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
slackEnvKeys,
|
|
18
|
+
tryResolveBotToken,
|
|
19
|
+
} from "../channels/slack/credentials.js";
|
|
20
|
+
import type { SlackCredentials } from "../channels/slack/types.js";
|
|
17
21
|
import type { HostGitHubClient, HostSlackClient } from "../types.js";
|
|
18
22
|
|
|
19
23
|
export interface HostPlatforms {
|
|
@@ -35,6 +39,37 @@ export type HostGitHubAuthSource =
|
|
|
35
39
|
|
|
36
40
|
export interface HostPlatformsOptions {
|
|
37
41
|
githubCredentials?: GitHubChannelCredentials;
|
|
42
|
+
/**
|
|
43
|
+
* Per-agent Slack credentials (usually from a channel `envPrefix`).
|
|
44
|
+
* When omitted, `getClient()` falls back to `SLACK_BOT_TOKEN`.
|
|
45
|
+
*/
|
|
46
|
+
slackCredentials?: SlackCredentials;
|
|
47
|
+
/** Env prefix used only to name the missing-token error. */
|
|
48
|
+
slackEnvPrefix?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Bot-token Slack client. Pass channel credentials so `envPrefix` is honored. */
|
|
52
|
+
export function createHostSlackClient(
|
|
53
|
+
options: { credentials?: SlackCredentials; envPrefix?: string } = {}
|
|
54
|
+
): HostSlackClient {
|
|
55
|
+
let slackPromise: ReturnType<HostSlackClient["getClient"]> | undefined;
|
|
56
|
+
return {
|
|
57
|
+
getClient: async () => {
|
|
58
|
+
slackPromise ??= (async () => {
|
|
59
|
+
const token = await tryResolveBotToken(options.credentials);
|
|
60
|
+
if (token === undefined) {
|
|
61
|
+
const botKey = slackEnvKeys(options.envPrefix).bot;
|
|
62
|
+
const hint =
|
|
63
|
+
options.envPrefix === undefined || options.envPrefix.trim() === ""
|
|
64
|
+
? `${botKey} or a channel envPrefix`
|
|
65
|
+
: botKey;
|
|
66
|
+
throw new Error(`No Slack bot token available (set ${hint}).`);
|
|
67
|
+
}
|
|
68
|
+
return new WebClient(token);
|
|
69
|
+
})();
|
|
70
|
+
return slackPromise;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
38
73
|
}
|
|
39
74
|
|
|
40
75
|
/**
|
|
@@ -46,7 +81,6 @@ export function createHostPlatforms(
|
|
|
46
81
|
options: HostPlatformsOptions = {}
|
|
47
82
|
): HostPlatforms {
|
|
48
83
|
let octokitPromise: ReturnType<HostGitHubClient["getOctokit"]> | undefined;
|
|
49
|
-
let slackPromise: ReturnType<HostSlackClient["getClient"]> | undefined;
|
|
50
84
|
|
|
51
85
|
return {
|
|
52
86
|
github: {
|
|
@@ -57,22 +91,10 @@ export function createHostPlatforms(
|
|
|
57
91
|
return octokitPromise;
|
|
58
92
|
},
|
|
59
93
|
},
|
|
60
|
-
slack: {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const token = await tryResolveBotToken(undefined);
|
|
65
|
-
if (token === undefined) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
"No Slack bot token available (set SLACK_BOT_TOKEN or a channel envPrefix)."
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
return new WebClient(token);
|
|
71
|
-
})();
|
|
72
|
-
}
|
|
73
|
-
return slackPromise;
|
|
74
|
-
},
|
|
75
|
-
},
|
|
94
|
+
slack: createHostSlackClient({
|
|
95
|
+
credentials: options.slackCredentials,
|
|
96
|
+
envPrefix: options.slackEnvPrefix,
|
|
97
|
+
}),
|
|
76
98
|
};
|
|
77
99
|
}
|
|
78
100
|
|
|
@@ -301,7 +301,9 @@ export function buildHttpChannelRoutes(args: {
|
|
|
301
301
|
}
|
|
302
302
|
const url = new URL(request.url);
|
|
303
303
|
const asJson = url.searchParams.get("format") === "json";
|
|
304
|
-
const snapshot = await engine.listEvents(sessionId, 0
|
|
304
|
+
const snapshot = await engine.listEvents(sessionId, 0, {
|
|
305
|
+
backfillCloudCost: true,
|
|
306
|
+
});
|
|
305
307
|
if (asJson) {
|
|
306
308
|
return jsonResponse({
|
|
307
309
|
sessionId,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process control plane started with local `serve`. Owns schedule clocks
|
|
3
|
+
* and reminder documents + clocks, then fires into the kit the same way the
|
|
4
|
+
* Cursor backend does over HTTP when `--no-control-plane` is set.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { type CronExpression, parseCron } from "./cron.js";
|
|
9
|
+
import type { PlatformTimers } from "./platform-timers.js";
|
|
10
|
+
import { MAX_TIMER_MS } from "./reminder-schedule.js";
|
|
11
|
+
import { type ReminderRecord, ReminderStore } from "./reminder-store.js";
|
|
12
|
+
|
|
13
|
+
export interface LocalControlPlane extends PlatformTimers {
|
|
14
|
+
/** Arm persisted reminder clocks after the kit fire callback is live. */
|
|
15
|
+
startClocks(): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function createLocalControlPlane(opts: {
|
|
20
|
+
fireSchedule: (name: string) => Promise<unknown>;
|
|
21
|
+
fireReminder: (id: string, fireCount: number) => Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Persist reminder documents under `<stateDir>/reminders`. Local serve
|
|
24
|
+
* passes `<stateRoot>/control-plane`; records previously stored at
|
|
25
|
+
* `<stateRoot>/reminders` are migrated in on first boot.
|
|
26
|
+
*/
|
|
27
|
+
stateDir?: string;
|
|
28
|
+
/** Arm deadline timers. Off in `--dev`. Defaults to true. */
|
|
29
|
+
armReminderClocks?: boolean;
|
|
30
|
+
logger?: (line: string) => void;
|
|
31
|
+
}): Promise<LocalControlPlane> {
|
|
32
|
+
const logger = opts.logger ?? (() => {});
|
|
33
|
+
const armReminderClocks = opts.armReminderClocks !== false;
|
|
34
|
+
const store =
|
|
35
|
+
opts.stateDir === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: new ReminderStore(join(opts.stateDir, "reminders"));
|
|
38
|
+
const schedules = new Map<string, CronExpression>();
|
|
39
|
+
const reminderTimers = new Map<string, NodeJS.Timeout>();
|
|
40
|
+
const reminders = new Map<string, ReminderRecord>();
|
|
41
|
+
let scheduleTimer: NodeJS.Timeout | undefined;
|
|
42
|
+
let lastFiredMinute = -1;
|
|
43
|
+
|
|
44
|
+
if (store !== undefined) {
|
|
45
|
+
for (const record of await store.list()) {
|
|
46
|
+
reminders.set(record.id, record);
|
|
47
|
+
}
|
|
48
|
+
if (opts.stateDir !== undefined) {
|
|
49
|
+
const legacyStore = new ReminderStore(
|
|
50
|
+
join(dirname(opts.stateDir), "reminders")
|
|
51
|
+
);
|
|
52
|
+
for (const record of await legacyStore.list()) {
|
|
53
|
+
if (reminders.has(record.id)) {
|
|
54
|
+
await legacyStore.delete(record.id);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
reminders.set(record.id, record);
|
|
58
|
+
await store.save(record);
|
|
59
|
+
await legacyStore.delete(record.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function armScheduleTicker(): void {
|
|
65
|
+
if (scheduleTimer !== undefined || schedules.size === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const nextMinute = (Math.floor(now / 60_000) + 1) * 60_000;
|
|
70
|
+
scheduleTimer = setTimeout(() => tickSchedules(), nextMinute - now + 250);
|
|
71
|
+
scheduleTimer.unref?.();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stopScheduleTicker(): void {
|
|
75
|
+
if (scheduleTimer === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
clearTimeout(scheduleTimer);
|
|
79
|
+
scheduleTimer = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tickSchedules(): void {
|
|
83
|
+
scheduleTimer = undefined;
|
|
84
|
+
const now = new Date();
|
|
85
|
+
const minute = Math.floor(now.getTime() / 60_000);
|
|
86
|
+
if (minute !== lastFiredMinute) {
|
|
87
|
+
lastFiredMinute = minute;
|
|
88
|
+
for (const [name, cron] of schedules) {
|
|
89
|
+
if (!cron.matches(now)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
void opts.fireSchedule(name).catch((error) => {
|
|
93
|
+
logger(
|
|
94
|
+
`[agentkit] schedule "${name}" dispatch failed: ${
|
|
95
|
+
error instanceof Error ? error.message : String(error)
|
|
96
|
+
}`
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
armScheduleTicker();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearReminderTimer(id: string): void {
|
|
105
|
+
const timer = reminderTimers.get(id);
|
|
106
|
+
if (timer !== undefined) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
reminderTimers.delete(id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function armReminder(record: ReminderRecord): void {
|
|
113
|
+
clearReminderTimer(record.id);
|
|
114
|
+
const delay = Math.min(
|
|
115
|
+
MAX_TIMER_MS,
|
|
116
|
+
Math.max(0, record.nextFireAtMs - Date.now())
|
|
117
|
+
);
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
reminderTimers.delete(record.id);
|
|
120
|
+
const current = reminders.get(record.id);
|
|
121
|
+
if (current === undefined || current.status !== "armed") {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (current.nextFireAtMs > Date.now() + 50) {
|
|
125
|
+
armReminder(current);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
void opts.fireReminder(current.id, current.fireCount).catch((error) => {
|
|
129
|
+
logger(
|
|
130
|
+
`[agentkit] reminder "${current.id}" dispatch failed: ${
|
|
131
|
+
error instanceof Error ? error.message : String(error)
|
|
132
|
+
}`
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}, delay);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
reminderTimers.set(record.id, timer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
async upsertSchedule(input) {
|
|
142
|
+
schedules.set(input.name, parseCron(input.cron));
|
|
143
|
+
armScheduleTicker();
|
|
144
|
+
},
|
|
145
|
+
async deleteSchedule(name) {
|
|
146
|
+
schedules.delete(name);
|
|
147
|
+
if (schedules.size === 0) {
|
|
148
|
+
stopScheduleTicker();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async putReminder(record) {
|
|
152
|
+
reminders.set(record.id, record);
|
|
153
|
+
await store?.save(record);
|
|
154
|
+
if (armReminderClocks && record.status === "armed") {
|
|
155
|
+
armReminder(record);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
clearReminderTimer(record.id);
|
|
159
|
+
},
|
|
160
|
+
async getReminder(id) {
|
|
161
|
+
return reminders.get(id);
|
|
162
|
+
},
|
|
163
|
+
async listReminders() {
|
|
164
|
+
return [...reminders.values()];
|
|
165
|
+
},
|
|
166
|
+
async deleteReminder(id) {
|
|
167
|
+
clearReminderTimer(id);
|
|
168
|
+
reminders.delete(id);
|
|
169
|
+
await store?.delete(id);
|
|
170
|
+
},
|
|
171
|
+
startClocks() {
|
|
172
|
+
if (!armReminderClocks) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const record of reminders.values()) {
|
|
176
|
+
if (record.status === "armed") {
|
|
177
|
+
armReminder(record);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
stop() {
|
|
182
|
+
stopScheduleTicker();
|
|
183
|
+
for (const id of [...reminderTimers.keys()]) {
|
|
184
|
+
clearReminderTimer(id);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep control-plane Temporal calendars aligned with authored
|
|
3
|
+
* `agent/schedules/*`. Upsert current names and delete ones that disappeared
|
|
4
|
+
* since the last successful sync (persisted under the engine state root).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
|
|
10
|
+
import type { PlatformTimers } from "./platform-timers.js";
|
|
11
|
+
|
|
12
|
+
export const PLATFORM_SCHEDULE_CATALOG_FILENAME =
|
|
13
|
+
"platform-schedule-names.json";
|
|
14
|
+
|
|
15
|
+
export async function syncHostedPlatformSchedules(args: {
|
|
16
|
+
timers: PlatformTimers;
|
|
17
|
+
schedules: ReadonlyArray<{ name: string; cron: string }>;
|
|
18
|
+
catalogPath: string;
|
|
19
|
+
logger: (line: string) => void;
|
|
20
|
+
}): Promise<void> {
|
|
21
|
+
const previous = await readScheduleCatalog(args.catalogPath);
|
|
22
|
+
const currentNames = args.schedules.map((schedule) => schedule.name);
|
|
23
|
+
const currentSet = new Set(currentNames);
|
|
24
|
+
|
|
25
|
+
const failedDeletes: string[] = [];
|
|
26
|
+
for (const name of previous) {
|
|
27
|
+
if (currentSet.has(name)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await args.timers.deleteSchedule(name);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
failedDeletes.push(name);
|
|
34
|
+
args.logger(
|
|
35
|
+
`[agentkit] platform timer delete failed for schedule "${name}": ${
|
|
36
|
+
error instanceof Error ? error.message : String(error)
|
|
37
|
+
}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const schedule of args.schedules) {
|
|
43
|
+
try {
|
|
44
|
+
await args.timers.upsertSchedule({
|
|
45
|
+
name: schedule.name,
|
|
46
|
+
cron: schedule.cron,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
args.logger(
|
|
50
|
+
`[agentkit] platform timer upsert failed for schedule "${schedule.name}": ${
|
|
51
|
+
error instanceof Error ? error.message : String(error)
|
|
52
|
+
}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Keep failed removals in the catalog so the next boot retries them.
|
|
58
|
+
await writeScheduleCatalog(args.catalogPath, [
|
|
59
|
+
...new Set([...currentNames, ...failedDeletes]),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readScheduleCatalog(catalogPath: string): Promise<string[]> {
|
|
64
|
+
try {
|
|
65
|
+
const parsed: unknown = JSON.parse(await readFile(catalogPath, "utf8"));
|
|
66
|
+
if (!Array.isArray(parsed)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return parsed.filter((entry): entry is string => typeof entry === "string");
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeScheduleCatalog(
|
|
76
|
+
catalogPath: string,
|
|
77
|
+
names: readonly string[]
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
await mkdir(dirname(catalogPath), { recursive: true });
|
|
80
|
+
await writeFile(catalogPath, `${JSON.stringify(names)}\n`, "utf8");
|
|
81
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed-work client used by the kit. Local serve implements this in-process
|
|
3
|
+
* (`createLocalControlPlane`). Cursor hosting (`--no-control-plane`) uses
|
|
4
|
+
* the HTTP client below against the Cursor backend. Authors never see this
|
|
5
|
+
* surface.
|
|
6
|
+
*
|
|
7
|
+
* The control plane owns reminder *documents* and clocks. The kit only
|
|
8
|
+
* executes fires.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isReminderRecord, type ReminderRecord } from "./reminder-store.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
|
|
14
|
+
|
|
15
|
+
export interface PlatformTimers {
|
|
16
|
+
upsertSchedule(input: { name: string; cron: string }): Promise<void>;
|
|
17
|
+
deleteSchedule(name: string): Promise<void>;
|
|
18
|
+
putReminder(record: ReminderRecord): Promise<void>;
|
|
19
|
+
getReminder(id: string): Promise<ReminderRecord | undefined>;
|
|
20
|
+
listReminders(): Promise<ReminderRecord[]>;
|
|
21
|
+
deleteReminder(id: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CursorHostedPlatformTimersOptions {
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveApiKey(options: CursorHostedPlatformTimersOptions): string {
|
|
31
|
+
const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
|
|
32
|
+
if (key === undefined || key === "") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"platform timers require CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveBaseUrl(options: CursorHostedPlatformTimersOptions): string {
|
|
41
|
+
const raw = (
|
|
42
|
+
options.baseUrl ??
|
|
43
|
+
process.env.CURSOR_API_BASE_URL ??
|
|
44
|
+
DEFAULT_CURSOR_EXTERNAL_API_URL
|
|
45
|
+
).trim();
|
|
46
|
+
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function timedWorkFetch(
|
|
50
|
+
options: CursorHostedPlatformTimersOptions,
|
|
51
|
+
method: "GET" | "PUT" | "DELETE",
|
|
52
|
+
path: string,
|
|
53
|
+
body?: unknown
|
|
54
|
+
): Promise<Response> {
|
|
55
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
56
|
+
if (typeof fetchImpl !== "function") {
|
|
57
|
+
throw new Error("platform timers require fetch");
|
|
58
|
+
}
|
|
59
|
+
return fetchImpl(`${resolveBaseUrl(options)}${path}`, {
|
|
60
|
+
method,
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${resolveApiKey(options)}`,
|
|
63
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
64
|
+
},
|
|
65
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function timedWorkRpc(
|
|
70
|
+
options: CursorHostedPlatformTimersOptions,
|
|
71
|
+
method: "PUT" | "DELETE",
|
|
72
|
+
path: string,
|
|
73
|
+
body?: unknown
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const response = await timedWorkFetch(options, method, path, body);
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
let detail = `HTTP ${response.status}`;
|
|
81
|
+
try {
|
|
82
|
+
const parsed: unknown = text.length === 0 ? {} : JSON.parse(text);
|
|
83
|
+
if (
|
|
84
|
+
parsed !== null &&
|
|
85
|
+
typeof parsed === "object" &&
|
|
86
|
+
"error" in parsed &&
|
|
87
|
+
typeof (parsed as { error: unknown }).error === "string"
|
|
88
|
+
) {
|
|
89
|
+
detail = (parsed as { error: string }).error;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Keep the status-only detail.
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`platform timers ${method} ${path}: ${detail}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createCursorHostedPlatformTimers(
|
|
98
|
+
options: CursorHostedPlatformTimersOptions = {}
|
|
99
|
+
): PlatformTimers {
|
|
100
|
+
return {
|
|
101
|
+
async upsertSchedule(input) {
|
|
102
|
+
await timedWorkRpc(
|
|
103
|
+
options,
|
|
104
|
+
"PUT",
|
|
105
|
+
`/internal/agent-serve/timed-work/schedules/${encodeURIComponent(input.name)}`,
|
|
106
|
+
{ cron: input.cron }
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
async deleteSchedule(name) {
|
|
110
|
+
await timedWorkRpc(
|
|
111
|
+
options,
|
|
112
|
+
"DELETE",
|
|
113
|
+
`/internal/agent-serve/timed-work/schedules/${encodeURIComponent(name)}`
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
async putReminder(record) {
|
|
117
|
+
await timedWorkRpc(
|
|
118
|
+
options,
|
|
119
|
+
"PUT",
|
|
120
|
+
`/internal/agent-serve/timed-work/reminders/${encodeURIComponent(record.id)}`,
|
|
121
|
+
record
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
async getReminder(id) {
|
|
125
|
+
const response = await timedWorkFetch(
|
|
126
|
+
options,
|
|
127
|
+
"GET",
|
|
128
|
+
`/internal/agent-serve/timed-work/reminders/${encodeURIComponent(id)}`
|
|
129
|
+
);
|
|
130
|
+
if (response.status === 404) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`platform timers GET /reminders/${id}: HTTP ${response.status}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const parsed: unknown = await response.json();
|
|
139
|
+
return isReminderRecord(parsed) ? parsed : undefined;
|
|
140
|
+
},
|
|
141
|
+
async listReminders() {
|
|
142
|
+
const response = await timedWorkFetch(
|
|
143
|
+
options,
|
|
144
|
+
"GET",
|
|
145
|
+
"/internal/agent-serve/timed-work/reminders"
|
|
146
|
+
);
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`platform timers GET /reminders: HTTP ${response.status}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const parsed: unknown = await response.json();
|
|
153
|
+
if (
|
|
154
|
+
parsed === null ||
|
|
155
|
+
typeof parsed !== "object" ||
|
|
156
|
+
!("reminders" in parsed) ||
|
|
157
|
+
!Array.isArray((parsed as { reminders: unknown }).reminders)
|
|
158
|
+
) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
return (parsed as { reminders: unknown[] }).reminders.filter(
|
|
162
|
+
isReminderRecord
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
async deleteReminder(id) {
|
|
166
|
+
await timedWorkRpc(
|
|
167
|
+
options,
|
|
168
|
+
"DELETE",
|
|
169
|
+
`/internal/agent-serve/timed-work/reminders/${encodeURIComponent(id)}`
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|