@dobby.ai/dobby 0.1.0 → 0.1.2

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 (156) hide show
  1. package/README.md +84 -39
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
@@ -1,61 +0,0 @@
1
- import { CronExpressionParser } from "cron-parser";
2
- import type { JobSchedule } from "./types.js";
3
-
4
- const BACKOFF_STEPS_MS = [30_000, 60_000, 5 * 60_000, 15 * 60_000] as const;
5
-
6
- function parseAtTimestamp(at: string): number {
7
- const timestamp = Date.parse(at);
8
- if (!Number.isFinite(timestamp)) {
9
- throw new Error(`Invalid at schedule timestamp '${at}'`);
10
- }
11
- return timestamp;
12
- }
13
-
14
- function nextCronAtMs(expr: string, currentDateMs: number, tz?: string): number {
15
- const parsed = CronExpressionParser.parse(expr, {
16
- currentDate: new Date(currentDateMs),
17
- ...(tz ? { tz } : {}),
18
- });
19
- return parsed.next().toDate().getTime();
20
- }
21
-
22
- export function computeInitialNextRunAtMs(schedule: JobSchedule, nowMs: number): number | undefined {
23
- if (schedule.kind === "at") {
24
- return parseAtTimestamp(schedule.at);
25
- }
26
-
27
- if (schedule.kind === "every") {
28
- return nowMs + schedule.everyMs;
29
- }
30
-
31
- return nextCronAtMs(schedule.expr, nowMs, schedule.tz);
32
- }
33
-
34
- export function computeNextRunAfterSuccessMs(schedule: JobSchedule, nowMs: number): number | undefined {
35
- if (schedule.kind === "at") {
36
- return undefined;
37
- }
38
-
39
- if (schedule.kind === "every") {
40
- return nowMs + schedule.everyMs;
41
- }
42
-
43
- return nextCronAtMs(schedule.expr, nowMs, schedule.tz);
44
- }
45
-
46
- export function computeBackoffDelayMs(consecutiveErrors: number): number {
47
- const safeErrors = Number.isFinite(consecutiveErrors) && consecutiveErrors > 0 ? Math.floor(consecutiveErrors) : 1;
48
- const index = Math.min(safeErrors - 1, BACKOFF_STEPS_MS.length - 1);
49
- const fallback = BACKOFF_STEPS_MS[BACKOFF_STEPS_MS.length - 1] as number;
50
- return BACKOFF_STEPS_MS[index] ?? fallback;
51
- }
52
-
53
- export function describeSchedule(schedule: JobSchedule): string {
54
- if (schedule.kind === "at") {
55
- return `at ${schedule.at}`;
56
- }
57
- if (schedule.kind === "every") {
58
- return `every ${schedule.everyMs}ms`;
59
- }
60
- return schedule.tz ? `cron '${schedule.expr}' (tz=${schedule.tz})` : `cron '${schedule.expr}'`;
61
- }
@@ -1,249 +0,0 @@
1
- import type { Gateway } from "../core/gateway.js";
2
- import type { GatewayLogger } from "../core/types.js";
3
- import { computeBackoffDelayMs, computeInitialNextRunAtMs, computeNextRunAfterSuccessMs } from "./schedule.js";
4
- import { CronStore } from "./store.js";
5
- import type { CronConfig, CronRunLogRecord, ScheduledJob } from "./types.js";
6
-
7
- interface CronServiceOptions {
8
- config: CronConfig;
9
- store: CronStore;
10
- gateway: Gateway;
11
- logger: GatewayLogger;
12
- }
13
-
14
- interface ScheduledRunContext {
15
- runId: string;
16
- jobId: string;
17
- scheduledAtMs: number;
18
- }
19
-
20
- export class CronService {
21
- private timer: NodeJS.Timeout | null = null;
22
- private readonly activeRuns = new Map<string, Promise<void>>();
23
- private tickInFlight = false;
24
- private started = false;
25
- private stopping = false;
26
-
27
- constructor(private readonly options: CronServiceOptions) {}
28
-
29
- async start(): Promise<void> {
30
- if (this.started) {
31
- return;
32
- }
33
-
34
- await this.options.store.load();
35
- await this.recoverOnStartup();
36
-
37
- if (!this.options.config.enabled) {
38
- this.options.logger.info("Cron scheduler is disabled by config");
39
- this.started = true;
40
- return;
41
- }
42
-
43
- const intervalMs = Math.min(this.options.config.pollIntervalMs, 60_000);
44
- await this.tick();
45
- this.timer = setInterval(() => {
46
- void this.tick();
47
- }, intervalMs);
48
-
49
- this.started = true;
50
- this.options.logger.info(
51
- {
52
- pollIntervalMs: intervalMs,
53
- maxConcurrentRuns: this.options.config.maxConcurrentRuns,
54
- },
55
- "Cron scheduler started",
56
- );
57
- }
58
-
59
- async stop(): Promise<void> {
60
- this.stopping = true;
61
- if (this.timer) {
62
- clearInterval(this.timer);
63
- this.timer = null;
64
- }
65
-
66
- await Promise.allSettled(this.activeRuns.values());
67
- this.activeRuns.clear();
68
- this.started = false;
69
- }
70
-
71
- async triggerNow(jobId: string): Promise<void> {
72
- await this.options.store.load();
73
- const job = this.options.store.getJob(jobId);
74
- if (!job) {
75
- throw new Error(`Cron job '${jobId}' does not exist`);
76
- }
77
-
78
- await this.enqueueJob(job, Date.now());
79
- const latestRun = [...this.activeRuns.values()].at(-1);
80
- if (latestRun) {
81
- await latestRun;
82
- }
83
- }
84
-
85
- private async recoverOnStartup(): Promise<void> {
86
- const now = Date.now();
87
- for (const job of this.options.store.listJobs()) {
88
- let changed = false;
89
- const next = structuredClone(job);
90
-
91
- if (next.state.runningAtMs !== undefined) {
92
- next.state.runningAtMs = undefined;
93
- next.state.lastStatus = "error";
94
- next.state.lastError = "Recovered stale running state after restart";
95
- changed = true;
96
- }
97
-
98
- if (next.state.nextRunAtMs === undefined) {
99
- next.state.nextRunAtMs = computeInitialNextRunAtMs(next.schedule, now);
100
- changed = true;
101
- }
102
-
103
- if (
104
- this.options.config.runMissedOnStartup
105
- && next.enabled
106
- && next.state.nextRunAtMs !== undefined
107
- && next.state.nextRunAtMs <= now
108
- ) {
109
- next.state.nextRunAtMs = now;
110
- changed = true;
111
- }
112
-
113
- if (changed) {
114
- next.updatedAtMs = now;
115
- await this.options.store.upsertJob(next);
116
- }
117
- }
118
- }
119
-
120
- private async tick(): Promise<void> {
121
- if (this.stopping || !this.options.config.enabled) {
122
- return;
123
- }
124
- if (this.tickInFlight) {
125
- return;
126
- }
127
-
128
- this.tickInFlight = true;
129
- try {
130
- // Reload from disk on every tick so CLI mutations (cron run/update/pause/resume)
131
- // made by a separate process become visible to the long-running scheduler.
132
- await this.options.store.load();
133
-
134
- const now = Date.now();
135
- const dueJobs = this.options.store.listJobs().filter((job) =>
136
- job.enabled
137
- && job.state.runningAtMs === undefined
138
- && job.state.nextRunAtMs !== undefined
139
- && job.state.nextRunAtMs <= now
140
- );
141
-
142
- for (const job of dueJobs) {
143
- if (this.activeRuns.size >= this.options.config.maxConcurrentRuns) {
144
- break;
145
- }
146
- await this.enqueueJob(job, now);
147
- }
148
- } finally {
149
- this.tickInFlight = false;
150
- }
151
- }
152
-
153
- private async enqueueJob(job: ScheduledJob, now: number): Promise<void> {
154
- const scheduledAtMs = job.state.nextRunAtMs ?? now;
155
- const runContext: ScheduledRunContext = {
156
- runId: `${job.id}:${scheduledAtMs}`,
157
- jobId: job.id,
158
- scheduledAtMs,
159
- };
160
-
161
- await this.options.store.updateJob(job.id, (current) => ({
162
- ...current,
163
- updatedAtMs: now,
164
- state: {
165
- ...current.state,
166
- runningAtMs: now,
167
- },
168
- }));
169
-
170
- const runPromise = this.executeJobRun(runContext)
171
- .catch((error) => {
172
- this.options.logger.warn(
173
- { err: error, jobId: runContext.jobId, runId: runContext.runId },
174
- "Cron run failed",
175
- );
176
- })
177
- .finally(() => {
178
- this.activeRuns.delete(runContext.runId);
179
- });
180
-
181
- this.activeRuns.set(runContext.runId, runPromise);
182
- }
183
-
184
- private async executeJobRun(run: ScheduledRunContext): Promise<void> {
185
- const startedAtMs = Date.now();
186
- const job = this.options.store.getJob(run.jobId);
187
- if (!job) {
188
- return;
189
- }
190
-
191
- let status: CronRunLogRecord["status"] = "ok";
192
- let errorMessage: string | undefined;
193
-
194
- try {
195
- await this.options.gateway.handleScheduled({
196
- jobId: job.id,
197
- runId: run.runId,
198
- prompt: job.prompt,
199
- connectorId: job.delivery.connectorId,
200
- routeId: job.delivery.routeId,
201
- channelId: job.delivery.channelId,
202
- ...(job.delivery.threadId ? { threadId: job.delivery.threadId } : {}),
203
- timeoutMs: this.options.config.jobTimeoutMs,
204
- });
205
- } catch (error) {
206
- status = "error";
207
- errorMessage = error instanceof Error ? error.message : String(error);
208
- }
209
-
210
- const endedAtMs = Date.now();
211
- await this.options.store.updateJob(job.id, (current) => {
212
- const isSuccess = status === "ok";
213
- const previousErrors = current.state.consecutiveErrors ?? 0;
214
- const nextErrorCount = isSuccess ? 0 : previousErrors + 1;
215
-
216
- const nextRunAtMs = isSuccess
217
- ? computeNextRunAfterSuccessMs(current.schedule, endedAtMs)
218
- : current.schedule.kind === "at"
219
- ? undefined
220
- : endedAtMs + computeBackoffDelayMs(nextErrorCount);
221
-
222
- return {
223
- ...current,
224
- enabled: current.schedule.kind === "at" ? false : current.enabled,
225
- updatedAtMs: endedAtMs,
226
- state: {
227
- ...current.state,
228
- runningAtMs: undefined,
229
- lastRunAtMs: endedAtMs,
230
- lastStatus: isSuccess ? "ok" : "error",
231
- lastError: isSuccess ? undefined : errorMessage,
232
- consecutiveErrors: nextErrorCount,
233
- nextRunAtMs,
234
- },
235
- };
236
- });
237
-
238
- const runRecord: CronRunLogRecord = {
239
- runId: run.runId,
240
- jobId: job.id,
241
- jobName: job.name,
242
- startedAtMs,
243
- endedAtMs,
244
- status,
245
- ...(errorMessage ? { error: errorMessage } : {}),
246
- };
247
- await this.options.store.appendRunLog(runRecord);
248
- }
249
- }
package/src/cron/store.ts DELETED
@@ -1,155 +0,0 @@
1
- import { access, appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
- import { z } from "zod";
4
- import type { GatewayLogger } from "../core/types.js";
5
- import type { CronJobsSnapshot, CronRunLogRecord, JobSchedule, ScheduledJob } from "./types.js";
6
-
7
- const jobScheduleSchema: z.ZodType<JobSchedule> = z.discriminatedUnion("kind", [
8
- z.object({
9
- kind: z.literal("at"),
10
- at: z.string().min(1),
11
- }),
12
- z.object({
13
- kind: z.literal("every"),
14
- everyMs: z.number().int().positive(),
15
- }),
16
- z.object({
17
- kind: z.literal("cron"),
18
- expr: z.string().min(1),
19
- tz: z.string().min(1).optional(),
20
- }),
21
- ]);
22
-
23
- const scheduledJobSchema: z.ZodType<ScheduledJob> = z.object({
24
- id: z.string().min(1),
25
- name: z.string().min(1),
26
- enabled: z.boolean(),
27
- schedule: jobScheduleSchema,
28
- sessionPolicy: z.enum(["stateless", "shared-session"]).optional(),
29
- prompt: z.string(),
30
- delivery: z.object({
31
- connectorId: z.string().min(1),
32
- routeId: z.string().min(1),
33
- channelId: z.string().min(1),
34
- threadId: z.string().min(1).optional(),
35
- }),
36
- createdAtMs: z.number().int().nonnegative(),
37
- updatedAtMs: z.number().int().nonnegative(),
38
- state: z.object({
39
- nextRunAtMs: z.number().int().nonnegative().optional(),
40
- runningAtMs: z.number().int().nonnegative().optional(),
41
- lastRunAtMs: z.number().int().nonnegative().optional(),
42
- lastStatus: z.enum(["ok", "error", "skipped"]).optional(),
43
- lastError: z.string().optional(),
44
- consecutiveErrors: z.number().int().nonnegative().optional(),
45
- }),
46
- });
47
-
48
- const snapshotSchema = z.object({
49
- version: z.literal(1),
50
- jobs: z.array(scheduledJobSchema),
51
- });
52
-
53
- function cloneJob(job: ScheduledJob): ScheduledJob {
54
- return structuredClone(job);
55
- }
56
-
57
- async function fileExists(filePath: string): Promise<boolean> {
58
- try {
59
- await access(filePath);
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- async function writeAtomic(filePath: string, payload: string): Promise<void> {
67
- await mkdir(dirname(filePath), { recursive: true });
68
- const tempPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
69
- await writeFile(tempPath, payload, "utf-8");
70
- await rename(tempPath, filePath);
71
- }
72
-
73
- export class CronStore {
74
- private readonly jobs = new Map<string, ScheduledJob>();
75
-
76
- constructor(
77
- private readonly jobsFilePath: string,
78
- private readonly runLogFilePath: string,
79
- private readonly logger: GatewayLogger,
80
- ) {}
81
-
82
- async load(): Promise<void> {
83
- this.jobs.clear();
84
- const absolutePath = resolve(this.jobsFilePath);
85
- if (await fileExists(absolutePath)) {
86
- try {
87
- const raw = await readFile(absolutePath, "utf-8");
88
- const parsed = snapshotSchema.parse(JSON.parse(raw));
89
- for (const job of parsed.jobs) {
90
- this.jobs.set(job.id, job);
91
- }
92
- } catch (error) {
93
- this.logger.warn({ err: error, filePath: absolutePath }, "Failed to load cron jobs store; starting empty");
94
- }
95
- }
96
- }
97
-
98
- listJobs(): ScheduledJob[] {
99
- return [...this.jobs.values()]
100
- .map((job) => cloneJob(job))
101
- .sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id));
102
- }
103
-
104
- getJob(jobId: string): ScheduledJob | null {
105
- const found = this.jobs.get(jobId);
106
- return found ? cloneJob(found) : null;
107
- }
108
-
109
- async upsertJob(job: ScheduledJob): Promise<void> {
110
- const normalized = scheduledJobSchema.parse(job);
111
- this.jobs.set(normalized.id, cloneJob(normalized));
112
- await this.flush();
113
- }
114
-
115
- async updateJob(jobId: string, updater: (current: ScheduledJob) => ScheduledJob): Promise<ScheduledJob> {
116
- const current = this.jobs.get(jobId);
117
- if (!current) {
118
- throw new Error(`Cron job '${jobId}' does not exist`);
119
- }
120
-
121
- const next = scheduledJobSchema.parse(updater(cloneJob(current)));
122
- this.jobs.set(jobId, cloneJob(next));
123
- await this.flush();
124
- return cloneJob(next);
125
- }
126
-
127
- async removeJob(jobId: string): Promise<boolean> {
128
- const deleted = this.jobs.delete(jobId);
129
- if (!deleted) {
130
- return false;
131
- }
132
- await this.flush();
133
- return true;
134
- }
135
-
136
- async appendRunLog(record: CronRunLogRecord): Promise<void> {
137
- const line = `${JSON.stringify(record)}\n`;
138
- try {
139
- await mkdir(dirname(this.runLogFilePath), { recursive: true });
140
- await appendFile(this.runLogFilePath, line, "utf-8");
141
- } catch (error) {
142
- this.logger.warn({ err: error, runLogFilePath: this.runLogFilePath }, "Failed to append cron run log");
143
- }
144
- }
145
-
146
- private async flush(): Promise<void> {
147
- const snapshot: CronJobsSnapshot = {
148
- version: 1,
149
- jobs: [...this.jobs.values()]
150
- .map((job) => cloneJob(job))
151
- .sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id)),
152
- };
153
- await writeAtomic(this.jobsFilePath, `${JSON.stringify(snapshot, null, 2)}\n`);
154
- }
155
- }
package/src/cron/types.ts DELETED
@@ -1,60 +0,0 @@
1
- export type CronSessionPolicy = "stateless" | "shared-session";
2
-
3
- export type JobSchedule =
4
- | { kind: "at"; at: string }
5
- | { kind: "every"; everyMs: number }
6
- | { kind: "cron"; expr: string; tz?: string | undefined };
7
-
8
- export interface JobDelivery {
9
- connectorId: string;
10
- routeId: string;
11
- channelId: string;
12
- threadId?: string | undefined;
13
- }
14
-
15
- export interface ScheduledJobState {
16
- nextRunAtMs?: number | undefined;
17
- runningAtMs?: number | undefined;
18
- lastRunAtMs?: number | undefined;
19
- lastStatus?: "ok" | "error" | "skipped" | undefined;
20
- lastError?: string | undefined;
21
- consecutiveErrors?: number | undefined;
22
- }
23
-
24
- export interface ScheduledJob {
25
- id: string;
26
- name: string;
27
- enabled: boolean;
28
- schedule: JobSchedule;
29
- sessionPolicy?: CronSessionPolicy | undefined;
30
- prompt: string;
31
- delivery: JobDelivery;
32
- createdAtMs: number;
33
- updatedAtMs: number;
34
- state: ScheduledJobState;
35
- }
36
-
37
- export interface CronConfig {
38
- enabled: boolean;
39
- storeFile: string;
40
- runLogFile: string;
41
- pollIntervalMs: number;
42
- maxConcurrentRuns: number;
43
- runMissedOnStartup: boolean;
44
- jobTimeoutMs: number;
45
- }
46
-
47
- export interface CronRunLogRecord {
48
- runId: string;
49
- jobId: string;
50
- jobName: string;
51
- startedAtMs: number;
52
- endedAtMs: number;
53
- status: "ok" | "error" | "timeout";
54
- error?: string;
55
- }
56
-
57
- export interface CronJobsSnapshot {
58
- version: 1;
59
- jobs: ScheduledJob[];
60
- }
@@ -1,145 +0,0 @@
1
- import { access } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
3
- import { createRequire } from "node:module";
4
- import { pathToFileURL } from "node:url";
5
- import type {
6
- ExtensionContributionManifest,
7
- ExtensionContributionModule,
8
- ExtensionManifest,
9
- ExtensionPackageConfig,
10
- GatewayLogger,
11
- } from "../core/types.js";
12
- import { readExtensionManifest } from "./manifest.js";
13
-
14
- export interface LoadedExtensionContribution {
15
- manifest: ExtensionContributionManifest;
16
- module: ExtensionContributionModule;
17
- }
18
-
19
- export interface LoadedExtensionPackage {
20
- packageName: string;
21
- manifest: ExtensionManifest;
22
- contributions: LoadedExtensionContribution[];
23
- }
24
-
25
- interface ExtensionLoaderOptions {
26
- extensionsDir: string;
27
- }
28
-
29
- function isJavaScriptEntry(entry: string): boolean {
30
- return entry.endsWith(".js") || entry.endsWith(".mjs") || entry.endsWith(".cjs");
31
- }
32
-
33
- function assertWithinRoot(pathToCheck: string, rootDir: string): void {
34
- const normalizedRoot = resolve(rootDir);
35
- const normalizedPath = resolve(pathToCheck);
36
- if (normalizedPath === normalizedRoot) {
37
- return;
38
- }
39
-
40
- const rootPrefix = normalizedRoot.endsWith("/") || normalizedRoot.endsWith("\\")
41
- ? normalizedRoot
42
- : `${normalizedRoot}${process.platform === "win32" ? "\\" : "/"}`;
43
- if (!normalizedPath.startsWith(rootPrefix)) {
44
- throw new Error(`Path '${normalizedPath}' escapes package root '${normalizedRoot}'`);
45
- }
46
- }
47
-
48
- function pickContributionModule(loadedModule: Record<string, unknown>): unknown {
49
- if ("default" in loadedModule) {
50
- return loadedModule.default;
51
- }
52
- if ("contribution" in loadedModule) {
53
- return loadedModule.contribution;
54
- }
55
- return undefined;
56
- }
57
-
58
- export class ExtensionLoader {
59
- private readonly extensionRequire: NodeJS.Require;
60
-
61
- constructor(
62
- private readonly logger: GatewayLogger,
63
- private readonly options: ExtensionLoaderOptions,
64
- ) {
65
- this.extensionRequire = createRequire(join(this.options.extensionsDir, "package.json"));
66
- }
67
-
68
- async loadAllowList(allowList: ExtensionPackageConfig[]): Promise<LoadedExtensionPackage[]> {
69
- const loaded: LoadedExtensionPackage[] = [];
70
-
71
- for (const packageConfig of allowList) {
72
- if (packageConfig.enabled === false) {
73
- this.logger.info({ package: packageConfig.package }, "Skipping disabled extension package");
74
- continue;
75
- }
76
-
77
- loaded.push(await this.loadExternalPackage(packageConfig.package));
78
- }
79
-
80
- return loaded;
81
- }
82
-
83
- private async loadExternalPackage(packageName: string): Promise<LoadedExtensionPackage> {
84
- let packageJsonPath: string;
85
- try {
86
- packageJsonPath = this.extensionRequire.resolve(`${packageName}/package.json`);
87
- } catch (error) {
88
- throw new Error(
89
- `Extension package '${packageName}' is not installed in '${this.options.extensionsDir}'. ` +
90
- `Install it with: dobby extension install ${packageName}. ` +
91
- `Resolver error: ${error instanceof Error ? error.message : String(error)}`,
92
- );
93
- }
94
-
95
- const packageRoot = dirname(packageJsonPath);
96
- const manifestPath = resolve(join(packageRoot, "dobby.manifest.json"));
97
- const manifest = await readExtensionManifest(manifestPath);
98
-
99
- const contributions: LoadedExtensionContribution[] = [];
100
- for (const contributionManifest of manifest.contributions) {
101
- if (!isJavaScriptEntry(contributionManifest.entry)) {
102
- throw new Error(
103
- `Contribution '${contributionManifest.id}' in package '${packageName}' must use a built JavaScript entry, got '${contributionManifest.entry}'`,
104
- );
105
- }
106
-
107
- const entryPath = resolve(packageRoot, contributionManifest.entry);
108
- assertWithinRoot(entryPath, packageRoot);
109
- try {
110
- await access(entryPath);
111
- } catch {
112
- throw new Error(
113
- `Contribution '${contributionManifest.id}' in package '${packageName}' points to missing entry '${contributionManifest.entry}'`,
114
- );
115
- }
116
-
117
- const loadedModule = (await import(pathToFileURL(entryPath).href)) as Record<string, unknown>;
118
- const contributionModule = pickContributionModule(loadedModule);
119
-
120
- if (!contributionModule || typeof contributionModule !== "object") {
121
- throw new Error(
122
- `Extension contribution '${contributionManifest.id}' from package '${packageName}' does not export a valid module`,
123
- );
124
- }
125
-
126
- const kind = (contributionModule as { kind?: string }).kind;
127
- if (kind !== contributionManifest.kind) {
128
- throw new Error(
129
- `Contribution kind mismatch for '${contributionManifest.id}' in package '${packageName}': manifest=${contributionManifest.kind}, module=${kind ?? "unknown"}`,
130
- );
131
- }
132
-
133
- contributions.push({
134
- manifest: contributionManifest,
135
- module: contributionModule as ExtensionContributionModule,
136
- });
137
- }
138
-
139
- return {
140
- packageName,
141
- manifest,
142
- contributions,
143
- };
144
- }
145
- }