@trycua/cua 0.2.0

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.
Files changed (91) hide show
  1. package/README.md +121 -0
  2. package/bin/cua.js +37 -0
  3. package/browser/cua_sdk-ffi.d.ts +6 -0
  4. package/browser/cua_sdk-ffi.js +3 -0
  5. package/browser/cua_sdk-ffi.ts +14 -0
  6. package/browser/cua_sdk.d.ts +2237 -0
  7. package/browser/cua_sdk.js +2607 -0
  8. package/browser/cua_sdk.ts +3644 -0
  9. package/browser/cyclops_sdk_schema-ffi.d.ts +6 -0
  10. package/browser/cyclops_sdk_schema-ffi.js +3 -0
  11. package/browser/cyclops_sdk_schema-ffi.ts +14 -0
  12. package/browser/cyclops_sdk_schema.d.ts +996 -0
  13. package/browser/cyclops_sdk_schema.js +2194 -0
  14. package/browser/cyclops_sdk_schema.ts +2934 -0
  15. package/browser/fleet_sdk-ffi.d.ts +28 -0
  16. package/browser/fleet_sdk-ffi.js +3 -0
  17. package/browser/fleet_sdk-ffi.ts +35 -0
  18. package/browser/fleet_sdk.d.ts +3056 -0
  19. package/browser/fleet_sdk.js +5088 -0
  20. package/browser/fleet_sdk.ts +6830 -0
  21. package/browser/index.d.ts +2 -0
  22. package/browser/index.js +18 -0
  23. package/browser/index.web.ts +34 -0
  24. package/browser/tsconfig.json +20 -0
  25. package/browser/wasm-bindgen/index.d.ts +2346 -0
  26. package/browser/wasm-bindgen/index.js +6642 -0
  27. package/browser/wasm-bindgen/index_bg.wasm +0 -0
  28. package/browser/wasm-bindgen/index_bg.wasm.d.ts +1154 -0
  29. package/dist/index.d.ts +118 -0
  30. package/dist/index.js +207 -0
  31. package/dist/mcp.d.ts +33 -0
  32. package/dist/mcp.js +41 -0
  33. package/dist/native/cua_sdk-ffi.d.ts +1038 -0
  34. package/dist/native/cua_sdk-ffi.js +5095 -0
  35. package/dist/native/cua_sdk.d.ts +15906 -0
  36. package/dist/native/cua_sdk.js +27061 -0
  37. package/dist/native/index.d.ts +7 -0
  38. package/dist/native/index.js +12 -0
  39. package/dist/native/node-runtime.d.ts +72 -0
  40. package/dist/native/node-runtime.js +35 -0
  41. package/dist/spaces/cursorArt.d.ts +27 -0
  42. package/dist/spaces/cursorArt.js +59 -0
  43. package/dist/spaces/errors.d.ts +81 -0
  44. package/dist/spaces/errors.js +102 -0
  45. package/dist/spaces/events.d.ts +154 -0
  46. package/dist/spaces/events.js +182 -0
  47. package/dist/spaces/groups.d.ts +130 -0
  48. package/dist/spaces/groups.js +275 -0
  49. package/dist/spaces/host.d.ts +71 -0
  50. package/dist/spaces/host.js +154 -0
  51. package/dist/spaces/index.d.ts +48 -0
  52. package/dist/spaces/index.js +44 -0
  53. package/dist/spaces/pip.d.ts +128 -0
  54. package/dist/spaces/pip.js +250 -0
  55. package/dist/spaces/presence.d.ts +187 -0
  56. package/dist/spaces/presence.js +449 -0
  57. package/dist/spaces/presenceTypes.check.d.ts +12 -0
  58. package/dist/spaces/presenceTypes.check.js +1 -0
  59. package/dist/spaces/presenceTypes.d.ts +47 -0
  60. package/dist/spaces/presenceTypes.js +7 -0
  61. package/dist/spaces/routines.d.ts +158 -0
  62. package/dist/spaces/routines.js +339 -0
  63. package/dist/spaces/thread.d.ts +147 -0
  64. package/dist/spaces/thread.js +285 -0
  65. package/dist/spaces/transport/http.d.ts +51 -0
  66. package/dist/spaces/transport/http.js +113 -0
  67. package/dist/spaces/transport/index.d.ts +19 -0
  68. package/dist/spaces/transport/index.js +19 -0
  69. package/dist/spaces/transport/session.d.ts +120 -0
  70. package/dist/spaces/transport/session.js +188 -0
  71. package/dist/spaces/transport/tauri.d.ts +50 -0
  72. package/dist/spaces/transport/tauri.js +90 -0
  73. package/dist/spaces/transport/types.d.ts +123 -0
  74. package/dist/spaces/transport/types.js +59 -0
  75. package/dist/teleport/controller.d.ts +34 -0
  76. package/dist/teleport/controller.js +107 -0
  77. package/dist/teleport/drop.d.ts +62 -0
  78. package/dist/teleport/drop.js +138 -0
  79. package/dist/teleport/dropZone.d.ts +59 -0
  80. package/dist/teleport/dropZone.js +166 -0
  81. package/dist/teleport/element.d.ts +28 -0
  82. package/dist/teleport/element.js +340 -0
  83. package/dist/teleport/index.d.ts +32 -0
  84. package/dist/teleport/index.js +32 -0
  85. package/dist/teleport/install.d.ts +31 -0
  86. package/dist/teleport/install.js +65 -0
  87. package/dist/teleport/model.d.ts +245 -0
  88. package/dist/teleport/model.js +334 -0
  89. package/dist/teleport/windowDrag.d.ts +86 -0
  90. package/dist/teleport/windowDrag.js +71 -0
  91. package/package.json +93 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The presence records of the native binding (`../native`), restated
3
+ * structurally so the browser-safe presence model type-checks without the
4
+ * native module (whose runtime loader imports `node:*`). The native records
5
+ * are assignable to these; `presenceTypes.check.ts` keeps them in step.
6
+ */
7
+ /** A participant (`PresenceParticipant`). */
8
+ export type PresenceParticipant = {
9
+ participantId: string;
10
+ principalId: string;
11
+ displayName: string;
12
+ color: string;
13
+ kind: string;
14
+ };
15
+ /** A cursor (`PresenceCursor`). */
16
+ export type PresenceCursor = {
17
+ displayId: string;
18
+ windowId?: string;
19
+ x: number;
20
+ y: number;
21
+ visible: boolean;
22
+ pressed: boolean;
23
+ shape: string;
24
+ shapeSource: string;
25
+ atMs: number;
26
+ receivedMs: number;
27
+ };
28
+ /** One presence event (`PresenceEvent`). */
29
+ export type PresenceEvent = {
30
+ kind: string;
31
+ participant?: PresenceParticipant;
32
+ participantId?: string;
33
+ cursor?: PresenceCursor;
34
+ shape?: string;
35
+ shapeSource?: string;
36
+ reason?: string;
37
+ participantIds?: Array<string>;
38
+ };
39
+ /** A roster member at join (`PresenceMember`). */
40
+ export type PresenceMember = {
41
+ participant: PresenceParticipant;
42
+ cursor?: PresenceCursor;
43
+ };
44
+ /** The part of a presence session `waitForPresence` reads. */
45
+ export interface PresenceEventSource {
46
+ nextEvent(timeoutMs: bigint | undefined): Promise<PresenceEvent | undefined>;
47
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The presence records of the native binding (`../native`), restated
3
+ * structurally so the browser-safe presence model type-checks without the
4
+ * native module (whose runtime loader imports `node:*`). The native records
5
+ * are assignable to these; `presenceTypes.check.ts` keeps them in step.
6
+ */
7
+ export {};
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `@trycua/cua/spaces/routines`: recurring tasks a Bot runs on a schedule.
3
+ * No native library, no Node imports, so it runs in a browser or a webview too.
4
+ *
5
+ * A routine is a saved prompt plus a clock, owned by one Bot (one long-lived
6
+ * agent thread). When it comes due the store hands it to a
7
+ * {@link RoutineRunner}, which gives that Bot another turn in the shared
8
+ * Space. The store keeps three things apart:
9
+ *
10
+ * - **persistence**: every mutation is written through a {@link RoutineStorage}
11
+ * (a JSON array, the same shape the Swift and Rust SDKs write);
12
+ * - **the clock**: {@link RoutineStore.tick} takes the instant to evaluate, so
13
+ * the scheduler is testable without sleeping;
14
+ * - **the firing**: delegated to the runner, so the store never sees a Space.
15
+ *
16
+ * ```ts
17
+ * import { RoutineStore, memoryStorage } from "@trycua/cua/spaces/routines"
18
+ *
19
+ * const routines = new RoutineStore(memoryStorage(), {
20
+ * fire: async (r) => ({ kind: "started", runId: await startTurn(r.botID, r.prompt) }),
21
+ * })
22
+ * routines.create({ botID: "inbox", title: "Morning sweep", prompt: "Triage the inbox",
23
+ * schedule: { kind: "dailyAt", hour: 8, minute: 0 } })
24
+ * routines.startScheduler(15_000)
25
+ * ```
26
+ *
27
+ * A scheduler that was asleep fires a due routine **once** on waking, never
28
+ * once per missed slot, and a refused firing still uses its slot.
29
+ */
30
+ /** The three recurrence shapes. `weekday` is 1 = Sunday … 7 = Saturday. */
31
+ export type RoutineSchedule = {
32
+ kind: "everyMinutes";
33
+ minutes: number;
34
+ } | {
35
+ kind: "dailyAt";
36
+ hour: number;
37
+ minute: number;
38
+ } | {
39
+ kind: "weeklyOn";
40
+ weekday: number;
41
+ hour: number;
42
+ minute: number;
43
+ };
44
+ /** One routine. Dates are ISO 8601 (UTC, whole seconds). */
45
+ export interface Routine {
46
+ id: string;
47
+ /** The Bot that runs it. A routine never spans Bots; a group chat does. */
48
+ botID: string;
49
+ title: string;
50
+ /** The text handed to the Bot when it fires. */
51
+ prompt: string;
52
+ schedule: RoutineSchedule;
53
+ isEnabled: boolean;
54
+ createdAt: string;
55
+ /** When the scheduler last started a firing (persisted, so a restart does not re-fire the past). */
56
+ lastFiredAt?: string;
57
+ /** The agent run the last firing produced. */
58
+ lastRunID?: string;
59
+ /** The scheduler's words about the last firing: started, refused, failed. */
60
+ lastOutcome?: string;
61
+ }
62
+ /** What happened when a routine fired. A refusal is not a failure. */
63
+ export type RoutineFiring = {
64
+ kind: "started";
65
+ runId: string;
66
+ } | {
67
+ kind: "refused";
68
+ reason: string;
69
+ } | {
70
+ kind: "failed";
71
+ reason: string;
72
+ };
73
+ /** Gives a Bot its routine turn. Never throws: a refusal is a result. */
74
+ export interface RoutineRunner {
75
+ fire(routine: Routine): Promise<RoutineFiring>;
76
+ }
77
+ /** Where the routine list lives. `load` returns `undefined` when nothing was saved. */
78
+ export interface RoutineStorage {
79
+ load(): string | undefined;
80
+ save(json: string): void;
81
+ }
82
+ /** One line of the scheduler's log, newest first. */
83
+ export interface FiringRecord {
84
+ routineID: string;
85
+ title: string;
86
+ at: string;
87
+ firing: RoutineFiring;
88
+ }
89
+ /** Marks a routine-originated turn in a transcript. */
90
+ export declare const ROUTINE_PREFIX = "[routine]";
91
+ /** The text a runner hands the Bot: `[routine] <title>: <prompt>`. */
92
+ export declare function routineTurnText(routine: Pick<Routine, "title" | "prompt">): string;
93
+ /** A {@link RoutineStorage} in memory. */
94
+ export declare function memoryStorage(initial?: string): RoutineStorage & {
95
+ value: string | undefined;
96
+ };
97
+ /** ISO 8601 in UTC with whole seconds, the portable form every SDK reads. */
98
+ export declare function isoSeconds(d: Date): string;
99
+ /** The first slot strictly after `reference`, in local time; `undefined` for an invalid schedule. */
100
+ export declare function nextFireDate(schedule: RoutineSchedule, reference: Date): Date | undefined;
101
+ /** When `routine` next fires after `reference`, or `undefined` when disabled. */
102
+ export declare function routineNextFire(routine: Routine, reference: Date): Date | undefined;
103
+ /**
104
+ * Whether the scheduler should fire `routine` at `now`. Measured from the
105
+ * last firing, not the tick, so a sleeping scheduler fires once on waking.
106
+ */
107
+ export declare function isDue(routine: Routine, now: Date): boolean;
108
+ /** `8:05 AM`. */
109
+ export declare function clockLabel(hour: number, minute: number): string;
110
+ /** The one-line description of a schedule: `Every day at 8:00 AM`. */
111
+ export declare function scheduleLabel(schedule: RoutineSchedule): string;
112
+ /** `started run <id>`, `refused: <why>`, `failed: <why>`. */
113
+ export declare function firingSummary(f: RoutineFiring): string;
114
+ /** Parses a saved routine list. Throws on anything malformed rather than guessing. */
115
+ export declare function parseRoutines(json: string): Routine[];
116
+ /** Routines: storage, editing, and the clock that fires them. */
117
+ export declare class RoutineStore {
118
+ readonly storage: RoutineStorage;
119
+ private runner?;
120
+ routines: Routine[];
121
+ /** What the scheduler did, newest first (at most 50). */
122
+ log: FiringRecord[];
123
+ private timer;
124
+ private ticking;
125
+ private readonly listeners;
126
+ constructor(storage: RoutineStorage, runner?: RoutineRunner | undefined);
127
+ attach(runner: RoutineRunner): void;
128
+ /** Called after every change. Returns an unsubscribe. */
129
+ subscribe(fn: () => void): () => void;
130
+ private changed;
131
+ /** Reads the list back. A corrupt file starts empty, with a logged complaint. */
132
+ load(): void;
133
+ save(): boolean;
134
+ routinesFor(botID: string): Routine[];
135
+ routine(id: string): Routine | undefined;
136
+ create(input: {
137
+ botID: string;
138
+ title: string;
139
+ prompt: string;
140
+ schedule: RoutineSchedule;
141
+ enabled?: boolean;
142
+ now?: Date;
143
+ }): Routine;
144
+ update(routine: Routine): void;
145
+ delete(id: string): void;
146
+ /** Enable or disable without deleting; the firing history is kept. */
147
+ setEnabled(id: string, enabled: boolean): void;
148
+ due(now: Date): Routine[];
149
+ /** Evaluates the clock once and fires whatever is due; returns what it fired. */
150
+ tick(now?: Date): Promise<FiringRecord[]>;
151
+ /** Fires one routine now, whatever the clock says ("Run now", and the scheduler's step). */
152
+ fire(routine: Routine, now?: Date): Promise<FiringRecord>;
153
+ /** One loop for every routine. Ticks never overlap. */
154
+ startScheduler(intervalMs?: number, clock?: () => Date): void;
155
+ stopScheduler(): void;
156
+ get isSchedulerRunning(): boolean;
157
+ private note;
158
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * `@trycua/cua/spaces/routines`: recurring tasks a Bot runs on a schedule.
3
+ * No native library, no Node imports, so it runs in a browser or a webview too.
4
+ *
5
+ * A routine is a saved prompt plus a clock, owned by one Bot (one long-lived
6
+ * agent thread). When it comes due the store hands it to a
7
+ * {@link RoutineRunner}, which gives that Bot another turn in the shared
8
+ * Space. The store keeps three things apart:
9
+ *
10
+ * - **persistence**: every mutation is written through a {@link RoutineStorage}
11
+ * (a JSON array, the same shape the Swift and Rust SDKs write);
12
+ * - **the clock**: {@link RoutineStore.tick} takes the instant to evaluate, so
13
+ * the scheduler is testable without sleeping;
14
+ * - **the firing**: delegated to the runner, so the store never sees a Space.
15
+ *
16
+ * ```ts
17
+ * import { RoutineStore, memoryStorage } from "@trycua/cua/spaces/routines"
18
+ *
19
+ * const routines = new RoutineStore(memoryStorage(), {
20
+ * fire: async (r) => ({ kind: "started", runId: await startTurn(r.botID, r.prompt) }),
21
+ * })
22
+ * routines.create({ botID: "inbox", title: "Morning sweep", prompt: "Triage the inbox",
23
+ * schedule: { kind: "dailyAt", hour: 8, minute: 0 } })
24
+ * routines.startScheduler(15_000)
25
+ * ```
26
+ *
27
+ * A scheduler that was asleep fires a due routine **once** on waking, never
28
+ * once per missed slot, and a refused firing still uses its slot.
29
+ */
30
+ /** Marks a routine-originated turn in a transcript. */
31
+ export const ROUTINE_PREFIX = "[routine]";
32
+ /** The text a runner hands the Bot: `[routine] <title>: <prompt>`. */
33
+ export function routineTurnText(routine) {
34
+ return `${ROUTINE_PREFIX} ${routine.title}: ${routine.prompt}`;
35
+ }
36
+ /** A {@link RoutineStorage} in memory. */
37
+ export function memoryStorage(initial) {
38
+ const s = {
39
+ value: initial,
40
+ load: () => s.value,
41
+ save: (json) => {
42
+ s.value = json;
43
+ },
44
+ };
45
+ return s;
46
+ }
47
+ /** ISO 8601 in UTC with whole seconds, the portable form every SDK reads. */
48
+ export function isoSeconds(d) {
49
+ return new Date(Math.floor(d.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
50
+ }
51
+ function at(d, hour, minute) {
52
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), hour, minute, 0, 0);
53
+ }
54
+ function addDays(d, n) {
55
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, d.getHours(), d.getMinutes(), 0, 0);
56
+ }
57
+ /** The first slot strictly after `reference`, in local time; `undefined` for an invalid schedule. */
58
+ export function nextFireDate(schedule, reference) {
59
+ switch (schedule.kind) {
60
+ case "everyMinutes":
61
+ if (!(schedule.minutes > 0))
62
+ return undefined;
63
+ return new Date(reference.getTime() + schedule.minutes * 60_000);
64
+ case "dailyAt": {
65
+ let c = at(reference, schedule.hour, schedule.minute);
66
+ if (c.getTime() <= reference.getTime())
67
+ c = at(addDays(c, 1), schedule.hour, schedule.minute);
68
+ return c;
69
+ }
70
+ case "weeklyOn": {
71
+ const want = (((schedule.weekday - 1) % 7) + 7) % 7;
72
+ const ahead = (want - reference.getDay() + 7) % 7;
73
+ let c = at(addDays(reference, ahead), schedule.hour, schedule.minute);
74
+ if (c.getTime() <= reference.getTime())
75
+ c = at(addDays(c, 7), schedule.hour, schedule.minute);
76
+ return c;
77
+ }
78
+ }
79
+ }
80
+ /** When `routine` next fires after `reference`, or `undefined` when disabled. */
81
+ export function routineNextFire(routine, reference) {
82
+ if (!routine.isEnabled)
83
+ return undefined;
84
+ const last = routine.lastFiredAt ? new Date(routine.lastFiredAt) : undefined;
85
+ const from = last && last.getTime() > reference.getTime() ? last : reference;
86
+ return nextFireDate(routine.schedule, from);
87
+ }
88
+ /**
89
+ * Whether the scheduler should fire `routine` at `now`. Measured from the
90
+ * last firing, not the tick, so a sleeping scheduler fires once on waking.
91
+ */
92
+ export function isDue(routine, now) {
93
+ if (!routine.isEnabled)
94
+ return false;
95
+ const from = new Date(routine.lastFiredAt ?? routine.createdAt);
96
+ const next = nextFireDate(routine.schedule, from);
97
+ return next !== undefined && next.getTime() <= now.getTime();
98
+ }
99
+ const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
100
+ /** `8:05 AM`. */
101
+ export function clockLabel(hour, minute) {
102
+ const h = hour % 12 === 0 ? 12 : hour % 12;
103
+ return `${h}:${String(minute).padStart(2, "0")} ${hour < 12 ? "AM" : "PM"}`;
104
+ }
105
+ /** The one-line description of a schedule: `Every day at 8:00 AM`. */
106
+ export function scheduleLabel(schedule) {
107
+ switch (schedule.kind) {
108
+ case "everyMinutes": {
109
+ const m = schedule.minutes;
110
+ if (m === 1)
111
+ return "Every minute";
112
+ if (m % 60 === 0)
113
+ return m === 60 ? "Every hour" : `Every ${m / 60} hours`;
114
+ return `Every ${m} minutes`;
115
+ }
116
+ case "dailyAt":
117
+ return `Every day at ${clockLabel(schedule.hour, schedule.minute)}`;
118
+ case "weeklyOn":
119
+ return `Every ${WEEKDAYS[(((schedule.weekday - 1) % 7) + 7) % 7]} at ${clockLabel(schedule.hour, schedule.minute)}`;
120
+ }
121
+ }
122
+ /** `started run <id>`, `refused: <why>`, `failed: <why>`. */
123
+ export function firingSummary(f) {
124
+ return f.kind === "started" ? `started run ${f.runId}` : `${f.kind}: ${f.reason}`;
125
+ }
126
+ function validSchedule(s) {
127
+ if (!s || typeof s !== "object")
128
+ return false;
129
+ const o = s;
130
+ const int = (k) => Number.isInteger(o[k]);
131
+ switch (o.kind) {
132
+ case "everyMinutes":
133
+ return int("minutes");
134
+ case "dailyAt":
135
+ return int("hour") && int("minute");
136
+ case "weeklyOn":
137
+ return int("weekday") && int("hour") && int("minute");
138
+ default:
139
+ return false;
140
+ }
141
+ }
142
+ /** Parses a saved routine list. Throws on anything malformed rather than guessing. */
143
+ export function parseRoutines(json) {
144
+ const v = JSON.parse(json);
145
+ if (!Array.isArray(v))
146
+ throw new Error("routines: expected a JSON array");
147
+ return v.map((r, i) => {
148
+ const o = r;
149
+ for (const k of ["id", "botID", "title", "prompt", "createdAt"]) {
150
+ if (typeof o[k] !== "string")
151
+ throw new Error(`routines[${i}].${k} is missing`);
152
+ }
153
+ if (!validSchedule(o.schedule))
154
+ throw new Error(`routines[${i}].schedule is not a schedule`);
155
+ const out = {
156
+ id: o.id,
157
+ botID: o.botID,
158
+ title: o.title,
159
+ prompt: o.prompt,
160
+ schedule: o.schedule,
161
+ isEnabled: o.isEnabled !== false,
162
+ createdAt: o.createdAt,
163
+ };
164
+ if (typeof o.lastFiredAt === "string")
165
+ out.lastFiredAt = o.lastFiredAt;
166
+ if (typeof o.lastRunID === "string")
167
+ out.lastRunID = o.lastRunID;
168
+ if (typeof o.lastOutcome === "string")
169
+ out.lastOutcome = o.lastOutcome;
170
+ return out;
171
+ });
172
+ }
173
+ function newId() {
174
+ return globalThis.crypto.randomUUID().toUpperCase();
175
+ }
176
+ /** Routines: storage, editing, and the clock that fires them. */
177
+ export class RoutineStore {
178
+ storage;
179
+ runner;
180
+ routines = [];
181
+ /** What the scheduler did, newest first (at most 50). */
182
+ log = [];
183
+ timer;
184
+ ticking = false;
185
+ listeners = new Set();
186
+ constructor(storage, runner) {
187
+ this.storage = storage;
188
+ this.runner = runner;
189
+ this.load();
190
+ }
191
+ attach(runner) {
192
+ this.runner = runner;
193
+ }
194
+ /** Called after every change. Returns an unsubscribe. */
195
+ subscribe(fn) {
196
+ this.listeners.add(fn);
197
+ return () => this.listeners.delete(fn);
198
+ }
199
+ changed() {
200
+ for (const l of this.listeners)
201
+ l();
202
+ }
203
+ /** Reads the list back. A corrupt file starts empty, with a logged complaint. */
204
+ load() {
205
+ const raw = this.storage.load();
206
+ if (raw === undefined || raw.trim() === "") {
207
+ this.routines = [];
208
+ return;
209
+ }
210
+ try {
211
+ this.routines = parseRoutines(raw);
212
+ }
213
+ catch (e) {
214
+ this.routines = [];
215
+ this.note(`routines file could not be read (${e instanceof Error ? e.message : String(e)}); starting empty`);
216
+ }
217
+ }
218
+ save() {
219
+ try {
220
+ this.storage.save(JSON.stringify(this.routines, null, 2));
221
+ return true;
222
+ }
223
+ catch (e) {
224
+ this.note(`could not save routines: ${e instanceof Error ? e.message : String(e)}`);
225
+ return false;
226
+ }
227
+ }
228
+ routinesFor(botID) {
229
+ return this.routines.filter((r) => r.botID === botID).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
230
+ }
231
+ routine(id) {
232
+ return this.routines.find((r) => r.id === id);
233
+ }
234
+ create(input) {
235
+ const r = {
236
+ id: newId(),
237
+ botID: input.botID,
238
+ title: input.title,
239
+ prompt: input.prompt,
240
+ schedule: input.schedule,
241
+ isEnabled: input.enabled ?? true,
242
+ createdAt: isoSeconds(input.now ?? new Date()),
243
+ };
244
+ this.routines.push(r);
245
+ this.save();
246
+ this.changed();
247
+ return r;
248
+ }
249
+ update(routine) {
250
+ const i = this.routines.findIndex((r) => r.id === routine.id);
251
+ if (i < 0)
252
+ return;
253
+ this.routines[i] = routine;
254
+ this.save();
255
+ this.changed();
256
+ }
257
+ delete(id) {
258
+ this.routines = this.routines.filter((r) => r.id !== id);
259
+ this.save();
260
+ this.changed();
261
+ }
262
+ /** Enable or disable without deleting; the firing history is kept. */
263
+ setEnabled(id, enabled) {
264
+ const r = this.routine(id);
265
+ if (!r)
266
+ return;
267
+ r.isEnabled = enabled;
268
+ this.save();
269
+ this.changed();
270
+ }
271
+ due(now) {
272
+ return this.routines.filter((r) => isDue(r, now));
273
+ }
274
+ /** Evaluates the clock once and fires whatever is due; returns what it fired. */
275
+ async tick(now = new Date()) {
276
+ const fired = [];
277
+ for (const r of this.due(now))
278
+ fired.push(await this.fire(r, now));
279
+ return fired;
280
+ }
281
+ /** Fires one routine now, whatever the clock says ("Run now", and the scheduler's step). */
282
+ async fire(routine, now = new Date()) {
283
+ let firing;
284
+ if (!this.runner)
285
+ firing = { kind: "failed", reason: "no runner attached: not connected to a Space" };
286
+ else {
287
+ try {
288
+ firing = await this.runner.fire(routine);
289
+ }
290
+ catch (e) {
291
+ firing = { kind: "failed", reason: e instanceof Error ? e.message : String(e) };
292
+ }
293
+ }
294
+ // The slot is used even on a refusal; otherwise a busy Bot is hammered every tick.
295
+ const r = this.routine(routine.id);
296
+ if (r) {
297
+ r.lastFiredAt = isoSeconds(now);
298
+ if (firing.kind === "started")
299
+ r.lastRunID = firing.runId;
300
+ r.lastOutcome = firingSummary(firing);
301
+ this.save();
302
+ }
303
+ const record = { routineID: routine.id, title: routine.title, at: isoSeconds(now), firing };
304
+ this.log.unshift(record);
305
+ if (this.log.length > 50)
306
+ this.log.length = 50;
307
+ this.changed();
308
+ return record;
309
+ }
310
+ /** One loop for every routine. Ticks never overlap. */
311
+ startScheduler(intervalMs = 15_000, clock = () => new Date()) {
312
+ this.stopScheduler();
313
+ const loop = async () => {
314
+ if (!this.ticking) {
315
+ this.ticking = true;
316
+ try {
317
+ await this.tick(clock());
318
+ }
319
+ finally {
320
+ this.ticking = false;
321
+ }
322
+ }
323
+ if (this.timer !== undefined)
324
+ this.timer = setTimeout(loop, intervalMs);
325
+ };
326
+ this.timer = setTimeout(loop, 0);
327
+ }
328
+ stopScheduler() {
329
+ if (this.timer !== undefined)
330
+ clearTimeout(this.timer);
331
+ this.timer = undefined;
332
+ }
333
+ get isSchedulerRunning() {
334
+ return this.timer !== undefined;
335
+ }
336
+ note(text) {
337
+ this.log.unshift({ routineID: "", title: "Routines", at: isoSeconds(new Date()), firing: { kind: "failed", reason: text } });
338
+ }
339
+ }