@dobby.ai/dobby 0.1.1 → 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 (136) hide show
  1. package/README.md +20 -7
  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/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. 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
- }