@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.
- package/README.md +84 -39
- package/dist/src/agent/event-forwarder.js +185 -16
- package/dist/src/cli/commands/cron.js +39 -35
- package/dist/src/cli/commands/doctor.js +81 -2
- package/dist/src/cli/commands/extension.js +3 -1
- package/dist/src/cli/commands/init.js +43 -173
- package/dist/src/cli/commands/topology.js +38 -14
- package/dist/src/cli/program.js +15 -137
- package/dist/src/cli/shared/config-io.js +3 -31
- package/dist/src/cli/shared/config-mutators.js +33 -9
- package/dist/src/cli/shared/configure-sections.js +52 -12
- package/dist/src/cli/shared/init-catalog.js +89 -46
- package/dist/src/cli/shared/local-extension-specs.js +85 -0
- package/dist/src/cli/shared/schema-prompts.js +26 -2
- package/dist/src/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- package/dist/src/core/types.js +2 -0
- package/dist/src/cron/config.js +2 -2
- package/dist/src/cron/service.js +87 -23
- package/dist/src/cron/store.js +1 -1
- package/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/package.json +11 -4
- package/.env.example +0 -9
- package/AGENTS.md +0 -267
- package/ROADMAP.md +0 -34
- package/config/cron.example.json +0 -9
- package/config/gateway.example.json +0 -128
- package/config/models.custom.example.json +0 -27
- package/dist/src/agent/tests/event-forwarder.test.js +0 -113
- package/dist/src/cli/shared/config-path.js +0 -207
- package/dist/src/cli/shared/init-models-file.js +0 -65
- package/dist/src/cli/shared/presets.js +0 -86
- package/dist/src/cli/tests/config-command.test.js +0 -42
- package/dist/src/cli/tests/config-io.test.js +0 -64
- package/dist/src/cli/tests/config-mutators.test.js +0 -47
- package/dist/src/cli/tests/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/doctor.test.js +0 -107
- package/dist/src/cli/tests/init-catalog.test.js +0 -87
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/program-options.test.js +0 -92
- package/dist/src/cli/tests/routing-config.test.js +0 -199
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/control-command.test.js +0 -17
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- package/dist/src/core/tests/runtime-registry.test.js +0 -116
- package/dist/src/core/tests/typing-controller.test.js +0 -103
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
- package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
- package/docs/MVP.md +0 -135
- package/docs/RUNBOOK.md +0 -242
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
- package/plugins/connector-discord/dobby.manifest.json +0 -18
- package/plugins/connector-discord/index.js +0 -1
- package/plugins/connector-discord/package-lock.json +0 -360
- package/plugins/connector-discord/package.json +0 -38
- package/plugins/connector-discord/src/connector.ts +0 -350
- package/plugins/connector-discord/src/contribution.ts +0 -21
- package/plugins/connector-discord/src/mapper.ts +0 -102
- package/plugins/connector-discord/tsconfig.json +0 -19
- package/plugins/connector-feishu/dobby.manifest.json +0 -18
- package/plugins/connector-feishu/index.js +0 -1
- package/plugins/connector-feishu/package-lock.json +0 -618
- package/plugins/connector-feishu/package.json +0 -38
- package/plugins/connector-feishu/src/connector.ts +0 -343
- package/plugins/connector-feishu/src/contribution.ts +0 -26
- package/plugins/connector-feishu/src/mapper.ts +0 -401
- package/plugins/connector-feishu/tsconfig.json +0 -19
- package/plugins/plugin-sdk/index.d.ts +0 -261
- package/plugins/plugin-sdk/index.js +0 -1
- package/plugins/plugin-sdk/package-lock.json +0 -12
- package/plugins/plugin-sdk/package.json +0 -22
- package/plugins/provider-claude/dobby.manifest.json +0 -17
- package/plugins/provider-claude/index.js +0 -1
- package/plugins/provider-claude/package-lock.json +0 -3398
- package/plugins/provider-claude/package.json +0 -39
- package/plugins/provider-claude/src/contribution.ts +0 -1018
- package/plugins/provider-claude/tsconfig.json +0 -19
- package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
- package/plugins/provider-claude-cli/index.js +0 -1
- package/plugins/provider-claude-cli/package-lock.json +0 -2898
- package/plugins/provider-claude-cli/package.json +0 -38
- package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
- package/plugins/provider-claude-cli/tsconfig.json +0 -19
- package/plugins/provider-pi/dobby.manifest.json +0 -17
- package/plugins/provider-pi/index.js +0 -1
- package/plugins/provider-pi/package-lock.json +0 -3877
- package/plugins/provider-pi/package.json +0 -40
- package/plugins/provider-pi/src/contribution.ts +0 -476
- package/plugins/provider-pi/tsconfig.json +0 -19
- package/plugins/sandbox-core/boxlite.js +0 -1
- package/plugins/sandbox-core/dobby.manifest.json +0 -17
- package/plugins/sandbox-core/docker.js +0 -1
- package/plugins/sandbox-core/package-lock.json +0 -136
- package/plugins/sandbox-core/package.json +0 -39
- package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
- package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
- package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
- package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
- package/plugins/sandbox-core/src/docker-executor.ts +0 -217
- package/plugins/sandbox-core/tsconfig.json +0 -19
- package/scripts/local-extensions.mjs +0 -168
- package/src/agent/event-forwarder.ts +0 -414
- package/src/cli/commands/config.ts +0 -328
- package/src/cli/commands/configure.ts +0 -92
- package/src/cli/commands/cron.ts +0 -410
- package/src/cli/commands/doctor.ts +0 -230
- package/src/cli/commands/extension.ts +0 -205
- package/src/cli/commands/init.ts +0 -396
- package/src/cli/commands/start.ts +0 -223
- package/src/cli/commands/topology.ts +0 -383
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -465
- package/src/cli/shared/config-io.ts +0 -277
- package/src/cli/shared/config-mutators.ts +0 -440
- package/src/cli/shared/config-schema.ts +0 -228
- package/src/cli/shared/config-types.ts +0 -121
- package/src/cli/shared/configure-sections.ts +0 -551
- package/src/cli/shared/discord-config.ts +0 -14
- package/src/cli/shared/init-catalog.ts +0 -189
- package/src/cli/shared/init-models-file.ts +0 -77
- package/src/cli/shared/runtime.ts +0 -33
- package/src/cli/shared/schema-prompts.ts +0 -414
- package/src/cli/tests/config-command.test.ts +0 -56
- package/src/cli/tests/config-io.test.ts +0 -92
- package/src/cli/tests/config-mutators.test.ts +0 -59
- package/src/cli/tests/doctor.test.ts +0 -120
- package/src/cli/tests/init-catalog.test.ts +0 -96
- package/src/cli/tests/program-options.test.ts +0 -113
- package/src/cli/tests/routing-config.test.ts +0 -209
- package/src/core/control-command.ts +0 -12
- package/src/core/dedup-store.ts +0 -103
- package/src/core/gateway.ts +0 -607
- package/src/core/routing.ts +0 -379
- package/src/core/runtime-registry.ts +0 -141
- package/src/core/tests/control-command.test.ts +0 -20
- package/src/core/tests/runtime-registry.test.ts +0 -140
- package/src/core/tests/typing-controller.test.ts +0 -129
- package/src/core/types.ts +0 -318
- package/src/core/typing-controller.ts +0 -119
- package/src/cron/config.ts +0 -154
- package/src/cron/schedule.ts +0 -61
- package/src/cron/service.ts +0 -249
- package/src/cron/store.ts +0 -155
- package/src/cron/types.ts +0 -60
- package/src/extension/loader.ts +0 -145
- package/src/extension/manager.ts +0 -355
- package/src/extension/manifest.ts +0 -26
- package/src/extension/registry.ts +0 -229
- package/src/main.ts +0 -8
- package/src/sandbox/executor.ts +0 -44
- package/src/sandbox/host-executor.ts +0 -118
- package/tsconfig.json +0 -18
package/src/cron/schedule.ts
DELETED
|
@@ -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
|
-
}
|
package/src/cron/service.ts
DELETED
|
@@ -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
|
-
}
|
package/src/extension/loader.ts
DELETED
|
@@ -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
|
-
}
|