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.
Files changed (158) hide show
  1. package/CONTRIBUTING.md +123 -0
  2. package/README.md +150 -0
  3. package/assets/logo.png +0 -0
  4. package/dist/agent/context.d.ts +22 -0
  5. package/dist/agent/context.js +85 -0
  6. package/dist/agent/loop.d.ts +63 -0
  7. package/dist/agent/loop.js +244 -0
  8. package/dist/agent/memory.d.ts +16 -0
  9. package/dist/agent/memory.js +98 -0
  10. package/dist/agent/skills.d.ts +18 -0
  11. package/dist/agent/skills.js +121 -0
  12. package/dist/agent/subagent.d.ts +30 -0
  13. package/dist/agent/subagent.js +92 -0
  14. package/dist/agent/tools/base.d.ts +10 -0
  15. package/dist/agent/tools/base.js +58 -0
  16. package/dist/agent/tools/cron.d.ts +43 -0
  17. package/dist/agent/tools/cron.js +83 -0
  18. package/dist/agent/tools/filesystem.d.ts +79 -0
  19. package/dist/agent/tools/filesystem.js +125 -0
  20. package/dist/agent/tools/message.d.ts +41 -0
  21. package/dist/agent/tools/message.js +55 -0
  22. package/dist/agent/tools/registry.d.ts +9 -0
  23. package/dist/agent/tools/registry.js +33 -0
  24. package/dist/agent/tools/shell.d.ts +26 -0
  25. package/dist/agent/tools/shell.js +78 -0
  26. package/dist/agent/tools/spawn.d.ts +27 -0
  27. package/dist/agent/tools/spawn.js +35 -0
  28. package/dist/agent/tools/web.d.ts +50 -0
  29. package/dist/agent/tools/web.js +119 -0
  30. package/dist/bus/async-queue.d.ts +7 -0
  31. package/dist/bus/async-queue.js +20 -0
  32. package/dist/bus/events.d.ts +19 -0
  33. package/dist/bus/events.js +3 -0
  34. package/dist/bus/queue.d.ts +12 -0
  35. package/dist/bus/queue.js +23 -0
  36. package/dist/channels/base.d.ts +22 -0
  37. package/dist/channels/base.js +35 -0
  38. package/dist/channels/discord.d.ts +24 -0
  39. package/dist/channels/discord.js +133 -0
  40. package/dist/channels/manager.d.ts +17 -0
  41. package/dist/channels/manager.js +67 -0
  42. package/dist/channels/stub.d.ts +10 -0
  43. package/dist/channels/stub.js +18 -0
  44. package/dist/channels/telegram.d.ts +20 -0
  45. package/dist/channels/telegram.js +93 -0
  46. package/dist/cli/commands.d.ts +2 -0
  47. package/dist/cli/commands.js +552 -0
  48. package/dist/config/loader.d.ts +5 -0
  49. package/dist/config/loader.js +55 -0
  50. package/dist/config/schema.d.ts +246 -0
  51. package/dist/config/schema.js +94 -0
  52. package/dist/cron/service.d.ts +33 -0
  53. package/dist/cron/service.js +195 -0
  54. package/dist/cron/types.d.ts +47 -0
  55. package/dist/cron/types.js +1 -0
  56. package/dist/dashboard/index.html +1567 -0
  57. package/dist/heartbeat/service.d.ts +21 -0
  58. package/dist/heartbeat/service.js +101 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.js +5 -0
  61. package/dist/providers/base.d.ts +23 -0
  62. package/dist/providers/base.js +21 -0
  63. package/dist/providers/custom-provider.d.ts +16 -0
  64. package/dist/providers/custom-provider.js +49 -0
  65. package/dist/providers/litellm-provider.d.ts +19 -0
  66. package/dist/providers/litellm-provider.js +128 -0
  67. package/dist/providers/registry.d.ts +5 -0
  68. package/dist/providers/registry.js +45 -0
  69. package/dist/session/manager.d.ts +31 -0
  70. package/dist/session/manager.js +116 -0
  71. package/dist/skills/README.md +25 -0
  72. package/dist/skills/clawhub/SKILL.md +53 -0
  73. package/dist/skills/cron/SKILL.md +57 -0
  74. package/dist/skills/github/SKILL.md +48 -0
  75. package/dist/skills/memory/SKILL.md +31 -0
  76. package/dist/skills/skill-creator/SKILL.md +371 -0
  77. package/dist/skills/summarize/SKILL.md +67 -0
  78. package/dist/skills/tmux/SKILL.md +121 -0
  79. package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
  80. package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
  81. package/dist/skills/weather/SKILL.md +49 -0
  82. package/dist/templates/AGENTS.md +23 -0
  83. package/dist/templates/HEARTBEAT.md +16 -0
  84. package/dist/templates/SOUL.md +21 -0
  85. package/dist/templates/TOOLS.md +15 -0
  86. package/dist/templates/USER.md +49 -0
  87. package/dist/templates/memory/MEMORY.md +23 -0
  88. package/dist/types.d.ts +4 -0
  89. package/dist/types.js +3 -0
  90. package/dist/utils/helpers.d.ts +5 -0
  91. package/dist/utils/helpers.js +53 -0
  92. package/dist/web/server.d.ts +15 -0
  93. package/dist/web/server.js +169 -0
  94. package/package.json +37 -0
  95. package/scripts/copy-assets.mjs +21 -0
  96. package/src/agent/context.ts +90 -0
  97. package/src/agent/loop.ts +291 -0
  98. package/src/agent/memory.ts +104 -0
  99. package/src/agent/skills.ts +121 -0
  100. package/src/agent/subagent.ts +96 -0
  101. package/src/agent/tools/base.ts +59 -0
  102. package/src/agent/tools/cron.ts +79 -0
  103. package/src/agent/tools/filesystem.ts +93 -0
  104. package/src/agent/tools/message.ts +57 -0
  105. package/src/agent/tools/registry.ts +36 -0
  106. package/src/agent/tools/shell.ts +69 -0
  107. package/src/agent/tools/spawn.ts +37 -0
  108. package/src/agent/tools/web.ts +108 -0
  109. package/src/bus/async-queue.ts +20 -0
  110. package/src/bus/events.ts +23 -0
  111. package/src/bus/queue.ts +31 -0
  112. package/src/channels/base.ts +36 -0
  113. package/src/channels/discord.ts +156 -0
  114. package/src/channels/manager.ts +70 -0
  115. package/src/channels/stub.ts +20 -0
  116. package/src/channels/telegram.ts +120 -0
  117. package/src/cli/commands.ts +581 -0
  118. package/src/config/loader.ts +58 -0
  119. package/src/config/schema.ts +144 -0
  120. package/src/cron/service.ts +190 -0
  121. package/src/cron/types.ts +36 -0
  122. package/src/dashboard/index.html +1567 -0
  123. package/src/heartbeat/service.ts +95 -0
  124. package/src/index.ts +6 -0
  125. package/src/providers/base.ts +43 -0
  126. package/src/providers/custom-provider.ts +46 -0
  127. package/src/providers/litellm-provider.ts +131 -0
  128. package/src/providers/registry.ts +48 -0
  129. package/src/session/manager.ts +129 -0
  130. package/src/skills/README.md +25 -0
  131. package/src/skills/clawhub/SKILL.md +53 -0
  132. package/src/skills/cron/SKILL.md +57 -0
  133. package/src/skills/github/SKILL.md +48 -0
  134. package/src/skills/memory/SKILL.md +31 -0
  135. package/src/skills/skill-creator/SKILL.md +371 -0
  136. package/src/skills/summarize/SKILL.md +67 -0
  137. package/src/skills/tmux/SKILL.md +121 -0
  138. package/src/skills/tmux/scripts/find-sessions.sh +112 -0
  139. package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
  140. package/src/skills/weather/SKILL.md +49 -0
  141. package/src/templates/AGENTS.md +23 -0
  142. package/src/templates/HEARTBEAT.md +16 -0
  143. package/src/templates/SOUL.md +21 -0
  144. package/src/templates/TOOLS.md +15 -0
  145. package/src/templates/USER.md +49 -0
  146. package/src/templates/memory/MEMORY.md +23 -0
  147. package/src/types/prompts.d.ts +14 -0
  148. package/src/types/ws.d.ts +15 -0
  149. package/src/types.ts +5 -0
  150. package/src/utils/helpers.ts +55 -0
  151. package/src/web/server.ts +198 -0
  152. package/test/context.test.ts +27 -0
  153. package/test/cron-service.test.ts +31 -0
  154. package/test/message-tool.test.ts +10 -0
  155. package/test/providers.test.ts +43 -0
  156. package/test/tool-validation.test.ts +61 -0
  157. package/tsconfig.json +16 -0
  158. 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
+ }