pretticlaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +123 -0
- package/README.md +150 -0
- package/assets/logo.png +0 -0
- package/dist/agent/context.d.ts +22 -0
- package/dist/agent/context.js +85 -0
- package/dist/agent/loop.d.ts +63 -0
- package/dist/agent/loop.js +244 -0
- package/dist/agent/memory.d.ts +16 -0
- package/dist/agent/memory.js +98 -0
- package/dist/agent/skills.d.ts +18 -0
- package/dist/agent/skills.js +121 -0
- package/dist/agent/subagent.d.ts +30 -0
- package/dist/agent/subagent.js +92 -0
- package/dist/agent/tools/base.d.ts +10 -0
- package/dist/agent/tools/base.js +58 -0
- package/dist/agent/tools/cron.d.ts +43 -0
- package/dist/agent/tools/cron.js +83 -0
- package/dist/agent/tools/filesystem.d.ts +79 -0
- package/dist/agent/tools/filesystem.js +125 -0
- package/dist/agent/tools/message.d.ts +41 -0
- package/dist/agent/tools/message.js +55 -0
- package/dist/agent/tools/registry.d.ts +9 -0
- package/dist/agent/tools/registry.js +33 -0
- package/dist/agent/tools/shell.d.ts +26 -0
- package/dist/agent/tools/shell.js +78 -0
- package/dist/agent/tools/spawn.d.ts +27 -0
- package/dist/agent/tools/spawn.js +35 -0
- package/dist/agent/tools/web.d.ts +50 -0
- package/dist/agent/tools/web.js +119 -0
- package/dist/bus/async-queue.d.ts +7 -0
- package/dist/bus/async-queue.js +20 -0
- package/dist/bus/events.d.ts +19 -0
- package/dist/bus/events.js +3 -0
- package/dist/bus/queue.d.ts +12 -0
- package/dist/bus/queue.js +23 -0
- package/dist/channels/base.d.ts +22 -0
- package/dist/channels/base.js +35 -0
- package/dist/channels/discord.d.ts +24 -0
- package/dist/channels/discord.js +133 -0
- package/dist/channels/manager.d.ts +17 -0
- package/dist/channels/manager.js +67 -0
- package/dist/channels/stub.d.ts +10 -0
- package/dist/channels/stub.js +18 -0
- package/dist/channels/telegram.d.ts +20 -0
- package/dist/channels/telegram.js +93 -0
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.js +552 -0
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.js +55 -0
- package/dist/config/schema.d.ts +246 -0
- package/dist/config/schema.js +94 -0
- package/dist/cron/service.d.ts +33 -0
- package/dist/cron/service.js +195 -0
- package/dist/cron/types.d.ts +47 -0
- package/dist/cron/types.js +1 -0
- package/dist/dashboard/index.html +1567 -0
- package/dist/heartbeat/service.d.ts +21 -0
- package/dist/heartbeat/service.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/providers/base.d.ts +23 -0
- package/dist/providers/base.js +21 -0
- package/dist/providers/custom-provider.d.ts +16 -0
- package/dist/providers/custom-provider.js +49 -0
- package/dist/providers/litellm-provider.d.ts +19 -0
- package/dist/providers/litellm-provider.js +128 -0
- package/dist/providers/registry.d.ts +5 -0
- package/dist/providers/registry.js +45 -0
- package/dist/session/manager.d.ts +31 -0
- package/dist/session/manager.js +116 -0
- package/dist/skills/README.md +25 -0
- package/dist/skills/clawhub/SKILL.md +53 -0
- package/dist/skills/cron/SKILL.md +57 -0
- package/dist/skills/github/SKILL.md +48 -0
- package/dist/skills/memory/SKILL.md +31 -0
- package/dist/skills/skill-creator/SKILL.md +371 -0
- package/dist/skills/summarize/SKILL.md +67 -0
- package/dist/skills/tmux/SKILL.md +121 -0
- package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
- package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/dist/skills/weather/SKILL.md +49 -0
- package/dist/templates/AGENTS.md +23 -0
- package/dist/templates/HEARTBEAT.md +16 -0
- package/dist/templates/SOUL.md +21 -0
- package/dist/templates/TOOLS.md +15 -0
- package/dist/templates/USER.md +49 -0
- package/dist/templates/memory/MEMORY.md +23 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +3 -0
- package/dist/utils/helpers.d.ts +5 -0
- package/dist/utils/helpers.js +53 -0
- package/dist/web/server.d.ts +15 -0
- package/dist/web/server.js +169 -0
- package/package.json +37 -0
- package/scripts/copy-assets.mjs +21 -0
- package/src/agent/context.ts +90 -0
- package/src/agent/loop.ts +291 -0
- package/src/agent/memory.ts +104 -0
- package/src/agent/skills.ts +121 -0
- package/src/agent/subagent.ts +96 -0
- package/src/agent/tools/base.ts +59 -0
- package/src/agent/tools/cron.ts +79 -0
- package/src/agent/tools/filesystem.ts +93 -0
- package/src/agent/tools/message.ts +57 -0
- package/src/agent/tools/registry.ts +36 -0
- package/src/agent/tools/shell.ts +69 -0
- package/src/agent/tools/spawn.ts +37 -0
- package/src/agent/tools/web.ts +108 -0
- package/src/bus/async-queue.ts +20 -0
- package/src/bus/events.ts +23 -0
- package/src/bus/queue.ts +31 -0
- package/src/channels/base.ts +36 -0
- package/src/channels/discord.ts +156 -0
- package/src/channels/manager.ts +70 -0
- package/src/channels/stub.ts +20 -0
- package/src/channels/telegram.ts +120 -0
- package/src/cli/commands.ts +581 -0
- package/src/config/loader.ts +58 -0
- package/src/config/schema.ts +144 -0
- package/src/cron/service.ts +190 -0
- package/src/cron/types.ts +36 -0
- package/src/dashboard/index.html +1567 -0
- package/src/heartbeat/service.ts +95 -0
- package/src/index.ts +6 -0
- package/src/providers/base.ts +43 -0
- package/src/providers/custom-provider.ts +46 -0
- package/src/providers/litellm-provider.ts +131 -0
- package/src/providers/registry.ts +48 -0
- package/src/session/manager.ts +129 -0
- package/src/skills/README.md +25 -0
- package/src/skills/clawhub/SKILL.md +53 -0
- package/src/skills/cron/SKILL.md +57 -0
- package/src/skills/github/SKILL.md +48 -0
- package/src/skills/memory/SKILL.md +31 -0
- package/src/skills/skill-creator/SKILL.md +371 -0
- package/src/skills/summarize/SKILL.md +67 -0
- package/src/skills/tmux/SKILL.md +121 -0
- package/src/skills/tmux/scripts/find-sessions.sh +112 -0
- package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/src/skills/weather/SKILL.md +49 -0
- package/src/templates/AGENTS.md +23 -0
- package/src/templates/HEARTBEAT.md +16 -0
- package/src/templates/SOUL.md +21 -0
- package/src/templates/TOOLS.md +15 -0
- package/src/templates/USER.md +49 -0
- package/src/templates/memory/MEMORY.md +23 -0
- package/src/types/prompts.d.ts +14 -0
- package/src/types/ws.d.ts +15 -0
- package/src/types.ts +5 -0
- package/src/utils/helpers.ts +55 -0
- package/src/web/server.ts +198 -0
- package/test/context.test.ts +27 -0
- package/test/cron-service.test.ts +31 -0
- package/test/message-tool.test.ts +10 -0
- package/test/providers.test.ts +43 -0
- package/test/tool-validation.test.ts +61 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiBase: string | null;
|
|
6
|
+
extraHeaders: Record<string, string> | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChannelsConfig {
|
|
10
|
+
sendProgress: boolean;
|
|
11
|
+
sendToolHints: boolean;
|
|
12
|
+
whatsapp: { enabled: boolean; bridgeUrl: string; bridgeToken: string; allowFrom: string[] };
|
|
13
|
+
telegram: { enabled: boolean; token: string; allowFrom: string[]; proxy: string | null; replyToMessage: boolean };
|
|
14
|
+
discord: { enabled: boolean; token: string; allowFrom: string[]; gatewayUrl: string; intents: number };
|
|
15
|
+
feishu: { enabled: boolean; appId: string; appSecret: string; allowFrom: string[] };
|
|
16
|
+
mochat: { enabled: boolean; baseUrl: string; allowFrom: string[] };
|
|
17
|
+
dingtalk: { enabled: boolean; clientId: string; clientSecret: string; allowFrom: string[] };
|
|
18
|
+
email: { enabled: boolean; consentGranted: boolean; imapHost: string; allowFrom: string[] };
|
|
19
|
+
slack: { enabled: boolean; botToken: string; appToken: string; groupPolicy: string; groupAllowFrom: string[] };
|
|
20
|
+
qq: { enabled: boolean; appId: string; secret: string; allowFrom: string[] };
|
|
21
|
+
matrix: { enabled: boolean; homeserver: string; accessToken: string; userId: string; deviceId: string; allowFrom: string[] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Config {
|
|
25
|
+
agents: {
|
|
26
|
+
defaults: {
|
|
27
|
+
workspace: string;
|
|
28
|
+
model: string;
|
|
29
|
+
provider: string;
|
|
30
|
+
maxTokens: number;
|
|
31
|
+
temperature: number;
|
|
32
|
+
maxToolIterations: number;
|
|
33
|
+
memoryWindow: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
channels: ChannelsConfig;
|
|
37
|
+
providers: Record<string, ProviderConfig>;
|
|
38
|
+
gateway: {
|
|
39
|
+
host: string;
|
|
40
|
+
port: number;
|
|
41
|
+
heartbeat: { enabled: boolean; intervalS: number };
|
|
42
|
+
};
|
|
43
|
+
tools: {
|
|
44
|
+
web: { search: { apiKey: string; maxResults: number } };
|
|
45
|
+
exec: { timeout: number; pathAppend: string };
|
|
46
|
+
restrictToWorkspace: boolean;
|
|
47
|
+
mcpServers: Record<string, { command: string; args: string[]; env: Record<string, string>; url: string; headers: Record<string, string>; toolTimeout: number }>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const PROVIDERS = [
|
|
52
|
+
{ name: "custom", keywords: [], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
53
|
+
{ name: "openrouter", keywords: ["openrouter"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://openrouter.ai/api/v1", detectByKeyPrefix: "sk-or-", detectByBaseKeyword: "openrouter" },
|
|
54
|
+
{ name: "aihubmix", keywords: ["aihubmix"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://aihubmix.com/v1", detectByBaseKeyword: "aihubmix" },
|
|
55
|
+
{ name: "siliconflow", keywords: ["siliconflow"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://api.siliconflow.cn/v1", detectByBaseKeyword: "siliconflow" },
|
|
56
|
+
{ name: "volcengine", keywords: ["volcengine", "volces", "ark"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://ark.cn-beijing.volces.com/api/v3", detectByBaseKeyword: "volces" },
|
|
57
|
+
{ name: "anthropic", keywords: ["anthropic", "claude"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
58
|
+
{ name: "openai", keywords: ["openai", "gpt"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
59
|
+
{ name: "openai_codex", keywords: ["openai-codex", "codex"], isOauth: true, isGateway: false, isLocal: false, defaultApiBase: "https://chatgpt.com/backend-api" },
|
|
60
|
+
{ name: "github_copilot", keywords: ["github_copilot", "copilot"], isOauth: true, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
61
|
+
{ name: "deepseek", keywords: ["deepseek"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
62
|
+
{ name: "gemini", keywords: ["gemini"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
63
|
+
{ name: "zhipu", keywords: ["zhipu", "glm", "zai"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
64
|
+
{ name: "dashscope", keywords: ["qwen", "dashscope"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
65
|
+
{ name: "moonshot", keywords: ["moonshot", "kimi"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "https://api.moonshot.ai/v1" },
|
|
66
|
+
{ name: "minimax", keywords: ["minimax"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "https://api.minimax.io/v1" },
|
|
67
|
+
{ name: "vllm", keywords: ["vllm"], isOauth: false, isGateway: false, isLocal: true, defaultApiBase: "" },
|
|
68
|
+
{ name: "groq", keywords: ["groq"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
const providerDefaults = Object.fromEntries(PROVIDERS.map((p) => [p.name, { apiKey: "", apiBase: null, extraHeaders: null }])) as Record<string, ProviderConfig>;
|
|
72
|
+
|
|
73
|
+
export const DEFAULT_CONFIG: Config = {
|
|
74
|
+
agents: {
|
|
75
|
+
defaults: {
|
|
76
|
+
workspace: path.join("~", ".pretticlaw", "workspace"),
|
|
77
|
+
model: "anthropic/claude-sonnet-4",
|
|
78
|
+
provider: "auto",
|
|
79
|
+
maxTokens: 8192,
|
|
80
|
+
temperature: 0.1,
|
|
81
|
+
maxToolIterations: 40,
|
|
82
|
+
memoryWindow: 100,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
channels: {
|
|
86
|
+
sendProgress: true,
|
|
87
|
+
sendToolHints: false,
|
|
88
|
+
whatsapp: { enabled: false, bridgeUrl: "ws://localhost:3001", bridgeToken: "", allowFrom: [] },
|
|
89
|
+
telegram: { enabled: false, token: "", allowFrom: [], proxy: null, replyToMessage: false },
|
|
90
|
+
discord: { enabled: false, token: "", allowFrom: [], gatewayUrl: "wss://gateway.discord.gg/?v=10&encoding=json", intents: 37377 },
|
|
91
|
+
feishu: { enabled: false, appId: "", appSecret: "", allowFrom: [] },
|
|
92
|
+
mochat: { enabled: false, baseUrl: "https://mochat.io", allowFrom: [] },
|
|
93
|
+
dingtalk: { enabled: false, clientId: "", clientSecret: "", allowFrom: [] },
|
|
94
|
+
email: { enabled: false, consentGranted: false, imapHost: "", allowFrom: [] },
|
|
95
|
+
slack: { enabled: false, botToken: "", appToken: "", groupPolicy: "mention", groupAllowFrom: [] },
|
|
96
|
+
qq: { enabled: false, appId: "", secret: "", allowFrom: [] },
|
|
97
|
+
matrix: { enabled: false, homeserver: "https://matrix.org", accessToken: "", userId: "", deviceId: "", allowFrom: [] },
|
|
98
|
+
},
|
|
99
|
+
providers: providerDefaults,
|
|
100
|
+
gateway: { host: "0.0.0.0", port: 18790, heartbeat: { enabled: true, intervalS: 1800 } },
|
|
101
|
+
tools: { web: { search: { apiKey: "", maxResults: 5 } }, exec: { timeout: 60, pathAppend: "" }, restrictToWorkspace: false, mcpServers: {} },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function normalize(name: string): string {
|
|
105
|
+
return name.toLowerCase().replace(/-/g, "_");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getProviderName(config: Config, model?: string): string | null {
|
|
109
|
+
const forced = config.agents.defaults.provider;
|
|
110
|
+
if (forced !== "auto") return config.providers[forced] ? forced : null;
|
|
111
|
+
|
|
112
|
+
const m = (model ?? config.agents.defaults.model).toLowerCase();
|
|
113
|
+
const mNorm = normalize(m);
|
|
114
|
+
const prefix = m.includes("/") ? m.split("/", 1)[0] : "";
|
|
115
|
+
|
|
116
|
+
for (const spec of PROVIDERS) {
|
|
117
|
+
const p = config.providers[spec.name];
|
|
118
|
+
if (prefix && normalize(prefix) === spec.name && (spec.isOauth || p?.apiKey)) return spec.name;
|
|
119
|
+
}
|
|
120
|
+
for (const spec of PROVIDERS) {
|
|
121
|
+
const p = config.providers[spec.name];
|
|
122
|
+
if (spec.keywords.some((kw) => m.includes(kw) || mNorm.includes(normalize(kw))) && (spec.isOauth || p?.apiKey)) return spec.name;
|
|
123
|
+
}
|
|
124
|
+
for (const spec of PROVIDERS) {
|
|
125
|
+
if (spec.isOauth) continue;
|
|
126
|
+
const p = config.providers[spec.name];
|
|
127
|
+
if (p?.apiKey) return spec.name;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getProvider(config: Config, model?: string): ProviderConfig | null {
|
|
133
|
+
const name = getProviderName(config, model);
|
|
134
|
+
return name ? config.providers[name] ?? null : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getApiBase(config: Config, model?: string): string | null {
|
|
138
|
+
const name = getProviderName(config, model);
|
|
139
|
+
if (!name) return null;
|
|
140
|
+
const p = config.providers[name];
|
|
141
|
+
if (p?.apiBase) return p.apiBase;
|
|
142
|
+
const spec = PROVIDERS.find((s) => s.name === name);
|
|
143
|
+
return spec?.isGateway ? spec.defaultApiBase : null;
|
|
144
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import parser from "cron-parser";
|
|
5
|
+
import type { CronJob, CronSchedule, CronStore } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function nowMs(): number { return Date.now(); }
|
|
8
|
+
|
|
9
|
+
function computeNextRun(schedule: CronSchedule, now: number): number | null {
|
|
10
|
+
if (schedule.kind === "at") return schedule.atMs > now ? schedule.atMs : null;
|
|
11
|
+
if (schedule.kind === "every") return schedule.everyMs > 0 ? now + schedule.everyMs : null;
|
|
12
|
+
if (schedule.kind === "cron") {
|
|
13
|
+
try {
|
|
14
|
+
const it = parser.parseExpression(schedule.expr, { currentDate: new Date(now), tz: schedule.tz });
|
|
15
|
+
return it.next().toDate().getTime();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateScheduleForAdd(schedule: CronSchedule): void {
|
|
24
|
+
if ("tz" in schedule && schedule.tz && schedule.kind !== "cron") throw new Error("tz can only be used with cron schedules");
|
|
25
|
+
if (schedule.kind === "cron" && schedule.tz) {
|
|
26
|
+
try {
|
|
27
|
+
new Intl.DateTimeFormat("en-US", { timeZone: schedule.tz });
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`unknown timezone '${schedule.tz}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class CronService {
|
|
35
|
+
private store: CronStore | null = null;
|
|
36
|
+
private timer: NodeJS.Timeout | null = null;
|
|
37
|
+
private running = false;
|
|
38
|
+
|
|
39
|
+
constructor(private readonly storePath: string, public onJob?: (job: CronJob) => Promise<string | null>) {}
|
|
40
|
+
|
|
41
|
+
private loadStore(): CronStore {
|
|
42
|
+
if (this.store) return this.store;
|
|
43
|
+
if (!fs.existsSync(this.storePath)) {
|
|
44
|
+
this.store = { version: 1, jobs: [] };
|
|
45
|
+
return this.store;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(this.storePath, "utf8")) as CronStore;
|
|
49
|
+
this.store = parsed;
|
|
50
|
+
} catch {
|
|
51
|
+
this.store = { version: 1, jobs: [] };
|
|
52
|
+
}
|
|
53
|
+
return this.store;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private saveStore(): void {
|
|
57
|
+
if (!this.store) return;
|
|
58
|
+
fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
|
|
59
|
+
fs.writeFileSync(this.storePath, JSON.stringify(this.store, null, 2), "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private getNextWakeMs(): number | null {
|
|
63
|
+
const s = this.loadStore();
|
|
64
|
+
const times = s.jobs.filter((j) => j.enabled && j.state.nextRunAtMs).map((j) => j.state.nextRunAtMs as number);
|
|
65
|
+
return times.length ? Math.min(...times) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private armTimer(): void {
|
|
69
|
+
if (this.timer) clearTimeout(this.timer);
|
|
70
|
+
const next = this.getNextWakeMs();
|
|
71
|
+
if (!this.running || !next) return;
|
|
72
|
+
this.timer = setTimeout(() => void this.onTimer(), Math.max(0, next - nowMs()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async onTimer(): Promise<void> {
|
|
76
|
+
const s = this.loadStore();
|
|
77
|
+
const now = nowMs();
|
|
78
|
+
const due = s.jobs.filter((j) => j.enabled && j.state.nextRunAtMs && now >= j.state.nextRunAtMs);
|
|
79
|
+
for (const job of due) await this.executeJob(job);
|
|
80
|
+
this.saveStore();
|
|
81
|
+
this.armTimer();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async executeJob(job: CronJob): Promise<void> {
|
|
85
|
+
const start = nowMs();
|
|
86
|
+
try {
|
|
87
|
+
if (this.onJob) await this.onJob(job);
|
|
88
|
+
job.state.lastStatus = "ok";
|
|
89
|
+
job.state.lastError = null;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
job.state.lastStatus = "error";
|
|
92
|
+
job.state.lastError = String(err);
|
|
93
|
+
}
|
|
94
|
+
job.state.lastRunAtMs = start;
|
|
95
|
+
job.updatedAtMs = nowMs();
|
|
96
|
+
if (job.schedule.kind === "at") {
|
|
97
|
+
if (job.deleteAfterRun) {
|
|
98
|
+
this.store!.jobs = this.store!.jobs.filter((j) => j.id !== job.id);
|
|
99
|
+
} else {
|
|
100
|
+
job.enabled = false;
|
|
101
|
+
job.state.nextRunAtMs = null;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async start(): Promise<void> {
|
|
109
|
+
this.running = true;
|
|
110
|
+
const s = this.loadStore();
|
|
111
|
+
for (const job of s.jobs) if (job.enabled) job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
|
|
112
|
+
this.saveStore();
|
|
113
|
+
this.armTimer();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
stop(): void {
|
|
117
|
+
this.running = false;
|
|
118
|
+
if (this.timer) clearTimeout(this.timer);
|
|
119
|
+
this.timer = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
listJobs(includeDisabled = false): CronJob[] {
|
|
123
|
+
const s = this.loadStore();
|
|
124
|
+
const jobs = includeDisabled ? s.jobs : s.jobs.filter((j) => j.enabled);
|
|
125
|
+
return [...jobs].sort((a, b) => (a.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER) - (b.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
addJob(input: { name: string; schedule: CronSchedule; message: string; deliver?: boolean; channel?: string; to?: string; deleteAfterRun?: boolean }): CronJob {
|
|
129
|
+
const s = this.loadStore();
|
|
130
|
+
validateScheduleForAdd(input.schedule);
|
|
131
|
+
const now = nowMs();
|
|
132
|
+
const job: CronJob = {
|
|
133
|
+
id: randomUUID().slice(0, 8),
|
|
134
|
+
name: input.name,
|
|
135
|
+
enabled: true,
|
|
136
|
+
schedule: input.schedule,
|
|
137
|
+
payload: { kind: "agent_turn", message: input.message, deliver: !!input.deliver, channel: input.channel, to: input.to },
|
|
138
|
+
state: { nextRunAtMs: computeNextRun(input.schedule, now), lastRunAtMs: null, lastStatus: null, lastError: null },
|
|
139
|
+
createdAtMs: now,
|
|
140
|
+
updatedAtMs: now,
|
|
141
|
+
deleteAfterRun: !!input.deleteAfterRun,
|
|
142
|
+
};
|
|
143
|
+
s.jobs.push(job);
|
|
144
|
+
this.saveStore();
|
|
145
|
+
this.armTimer();
|
|
146
|
+
return job;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
removeJob(jobId: string): boolean {
|
|
150
|
+
const s = this.loadStore();
|
|
151
|
+
const before = s.jobs.length;
|
|
152
|
+
s.jobs = s.jobs.filter((j) => j.id !== jobId);
|
|
153
|
+
const removed = s.jobs.length < before;
|
|
154
|
+
if (removed) {
|
|
155
|
+
this.saveStore();
|
|
156
|
+
this.armTimer();
|
|
157
|
+
}
|
|
158
|
+
return removed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
enableJob(jobId: string, enabled = true): CronJob | null {
|
|
162
|
+
const s = this.loadStore();
|
|
163
|
+
const job = s.jobs.find((j) => j.id === jobId);
|
|
164
|
+
if (!job) return null;
|
|
165
|
+
job.enabled = enabled;
|
|
166
|
+
job.updatedAtMs = nowMs();
|
|
167
|
+
job.state.nextRunAtMs = enabled ? computeNextRun(job.schedule, nowMs()) : null;
|
|
168
|
+
this.saveStore();
|
|
169
|
+
this.armTimer();
|
|
170
|
+
return job;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async runJob(jobId: string, force = false): Promise<boolean> {
|
|
174
|
+
const s = this.loadStore();
|
|
175
|
+
const job = s.jobs.find((j) => j.id === jobId);
|
|
176
|
+
if (!job) return false;
|
|
177
|
+
if (!force && !job.enabled) return false;
|
|
178
|
+
await this.executeJob(job);
|
|
179
|
+
this.saveStore();
|
|
180
|
+
this.armTimer();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
status(): Record<string, unknown> {
|
|
185
|
+
const s = this.loadStore();
|
|
186
|
+
return { enabled: this.running, jobs: s.jobs.length, nextWakeAtMs: this.getNextWakeMs() };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { validateScheduleForAdd };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type CronSchedule =
|
|
2
|
+
| { kind: "at"; atMs: number; everyMs?: never; expr?: never; tz?: string }
|
|
3
|
+
| { kind: "every"; everyMs: number; atMs?: never; expr?: never; tz?: string }
|
|
4
|
+
| { kind: "cron"; expr: string; tz?: string; atMs?: never; everyMs?: never };
|
|
5
|
+
|
|
6
|
+
export interface CronPayload {
|
|
7
|
+
kind: "system_event" | "agent_turn";
|
|
8
|
+
message: string;
|
|
9
|
+
deliver: boolean;
|
|
10
|
+
channel?: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CronJobState {
|
|
15
|
+
nextRunAtMs: number | null;
|
|
16
|
+
lastRunAtMs: number | null;
|
|
17
|
+
lastStatus: "ok" | "error" | "skipped" | null;
|
|
18
|
+
lastError: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CronJob {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
schedule: CronSchedule;
|
|
26
|
+
payload: CronPayload;
|
|
27
|
+
state: CronJobState;
|
|
28
|
+
createdAtMs: number;
|
|
29
|
+
updatedAtMs: number;
|
|
30
|
+
deleteAfterRun: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CronStore {
|
|
34
|
+
version: number;
|
|
35
|
+
jobs: CronJob[];
|
|
36
|
+
}
|