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,55 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { DEFAULT_CONFIG } from "./schema.js";
5
+ import { getDataPath } from "../utils/helpers.js";
6
+ export function getConfigPath() {
7
+ return path.join(os.homedir(), ".pretticlaw", "config.json");
8
+ }
9
+ export function getDataDir() {
10
+ return getDataPath();
11
+ }
12
+ function deepMerge(base, patch) {
13
+ if (!patch || typeof patch !== "object")
14
+ return base;
15
+ const out = Array.isArray(base) ? [...base] : { ...base };
16
+ for (const [k, v] of Object.entries(patch)) {
17
+ if (v && typeof v === "object" && !Array.isArray(v) && out[k] && typeof out[k] === "object" && !Array.isArray(out[k])) {
18
+ out[k] = deepMerge(out[k], v);
19
+ }
20
+ else {
21
+ out[k] = v;
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ function migrateConfig(data) {
27
+ const tools = data.tools ?? {};
28
+ const exec = tools.exec ?? {};
29
+ if ("restrictToWorkspace" in exec && !("restrictToWorkspace" in tools)) {
30
+ tools.restrictToWorkspace = exec.restrictToWorkspace;
31
+ delete exec.restrictToWorkspace;
32
+ }
33
+ tools.exec = exec;
34
+ data.tools = tools;
35
+ return data;
36
+ }
37
+ export function loadConfig(configPath) {
38
+ const p = configPath ?? getConfigPath();
39
+ if (!fs.existsSync(p))
40
+ return structuredClone(DEFAULT_CONFIG);
41
+ try {
42
+ const raw = fs.readFileSync(p, "utf8");
43
+ const parsed = migrateConfig(JSON.parse(raw));
44
+ return deepMerge(structuredClone(DEFAULT_CONFIG), parsed);
45
+ }
46
+ catch (err) {
47
+ console.warn(`Warning: Failed to load config from ${p}: ${String(err)}`);
48
+ return structuredClone(DEFAULT_CONFIG);
49
+ }
50
+ }
51
+ export function saveConfig(config, configPath) {
52
+ const p = configPath ?? getConfigPath();
53
+ fs.mkdirSync(path.dirname(p), { recursive: true });
54
+ fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf8");
55
+ }
@@ -0,0 +1,246 @@
1
+ export interface ProviderConfig {
2
+ apiKey: string;
3
+ apiBase: string | null;
4
+ extraHeaders: Record<string, string> | null;
5
+ }
6
+ export interface ChannelsConfig {
7
+ sendProgress: boolean;
8
+ sendToolHints: boolean;
9
+ whatsapp: {
10
+ enabled: boolean;
11
+ bridgeUrl: string;
12
+ bridgeToken: string;
13
+ allowFrom: string[];
14
+ };
15
+ telegram: {
16
+ enabled: boolean;
17
+ token: string;
18
+ allowFrom: string[];
19
+ proxy: string | null;
20
+ replyToMessage: boolean;
21
+ };
22
+ discord: {
23
+ enabled: boolean;
24
+ token: string;
25
+ allowFrom: string[];
26
+ gatewayUrl: string;
27
+ intents: number;
28
+ };
29
+ feishu: {
30
+ enabled: boolean;
31
+ appId: string;
32
+ appSecret: string;
33
+ allowFrom: string[];
34
+ };
35
+ mochat: {
36
+ enabled: boolean;
37
+ baseUrl: string;
38
+ allowFrom: string[];
39
+ };
40
+ dingtalk: {
41
+ enabled: boolean;
42
+ clientId: string;
43
+ clientSecret: string;
44
+ allowFrom: string[];
45
+ };
46
+ email: {
47
+ enabled: boolean;
48
+ consentGranted: boolean;
49
+ imapHost: string;
50
+ allowFrom: string[];
51
+ };
52
+ slack: {
53
+ enabled: boolean;
54
+ botToken: string;
55
+ appToken: string;
56
+ groupPolicy: string;
57
+ groupAllowFrom: string[];
58
+ };
59
+ qq: {
60
+ enabled: boolean;
61
+ appId: string;
62
+ secret: string;
63
+ allowFrom: string[];
64
+ };
65
+ matrix: {
66
+ enabled: boolean;
67
+ homeserver: string;
68
+ accessToken: string;
69
+ userId: string;
70
+ deviceId: string;
71
+ allowFrom: string[];
72
+ };
73
+ }
74
+ export interface Config {
75
+ agents: {
76
+ defaults: {
77
+ workspace: string;
78
+ model: string;
79
+ provider: string;
80
+ maxTokens: number;
81
+ temperature: number;
82
+ maxToolIterations: number;
83
+ memoryWindow: number;
84
+ };
85
+ };
86
+ channels: ChannelsConfig;
87
+ providers: Record<string, ProviderConfig>;
88
+ gateway: {
89
+ host: string;
90
+ port: number;
91
+ heartbeat: {
92
+ enabled: boolean;
93
+ intervalS: number;
94
+ };
95
+ };
96
+ tools: {
97
+ web: {
98
+ search: {
99
+ apiKey: string;
100
+ maxResults: number;
101
+ };
102
+ };
103
+ exec: {
104
+ timeout: number;
105
+ pathAppend: string;
106
+ };
107
+ restrictToWorkspace: boolean;
108
+ mcpServers: Record<string, {
109
+ command: string;
110
+ args: string[];
111
+ env: Record<string, string>;
112
+ url: string;
113
+ headers: Record<string, string>;
114
+ toolTimeout: number;
115
+ }>;
116
+ };
117
+ }
118
+ export declare const PROVIDERS: readonly [{
119
+ readonly name: "custom";
120
+ readonly keywords: readonly [];
121
+ readonly isOauth: false;
122
+ readonly isGateway: false;
123
+ readonly isLocal: false;
124
+ readonly defaultApiBase: "";
125
+ }, {
126
+ readonly name: "openrouter";
127
+ readonly keywords: readonly ["openrouter"];
128
+ readonly isOauth: false;
129
+ readonly isGateway: true;
130
+ readonly isLocal: false;
131
+ readonly defaultApiBase: "https://openrouter.ai/api/v1";
132
+ readonly detectByKeyPrefix: "sk-or-";
133
+ readonly detectByBaseKeyword: "openrouter";
134
+ }, {
135
+ readonly name: "aihubmix";
136
+ readonly keywords: readonly ["aihubmix"];
137
+ readonly isOauth: false;
138
+ readonly isGateway: true;
139
+ readonly isLocal: false;
140
+ readonly defaultApiBase: "https://aihubmix.com/v1";
141
+ readonly detectByBaseKeyword: "aihubmix";
142
+ }, {
143
+ readonly name: "siliconflow";
144
+ readonly keywords: readonly ["siliconflow"];
145
+ readonly isOauth: false;
146
+ readonly isGateway: true;
147
+ readonly isLocal: false;
148
+ readonly defaultApiBase: "https://api.siliconflow.cn/v1";
149
+ readonly detectByBaseKeyword: "siliconflow";
150
+ }, {
151
+ readonly name: "volcengine";
152
+ readonly keywords: readonly ["volcengine", "volces", "ark"];
153
+ readonly isOauth: false;
154
+ readonly isGateway: true;
155
+ readonly isLocal: false;
156
+ readonly defaultApiBase: "https://ark.cn-beijing.volces.com/api/v3";
157
+ readonly detectByBaseKeyword: "volces";
158
+ }, {
159
+ readonly name: "anthropic";
160
+ readonly keywords: readonly ["anthropic", "claude"];
161
+ readonly isOauth: false;
162
+ readonly isGateway: false;
163
+ readonly isLocal: false;
164
+ readonly defaultApiBase: "";
165
+ }, {
166
+ readonly name: "openai";
167
+ readonly keywords: readonly ["openai", "gpt"];
168
+ readonly isOauth: false;
169
+ readonly isGateway: false;
170
+ readonly isLocal: false;
171
+ readonly defaultApiBase: "";
172
+ }, {
173
+ readonly name: "openai_codex";
174
+ readonly keywords: readonly ["openai-codex", "codex"];
175
+ readonly isOauth: true;
176
+ readonly isGateway: false;
177
+ readonly isLocal: false;
178
+ readonly defaultApiBase: "https://chatgpt.com/backend-api";
179
+ }, {
180
+ readonly name: "github_copilot";
181
+ readonly keywords: readonly ["github_copilot", "copilot"];
182
+ readonly isOauth: true;
183
+ readonly isGateway: false;
184
+ readonly isLocal: false;
185
+ readonly defaultApiBase: "";
186
+ }, {
187
+ readonly name: "deepseek";
188
+ readonly keywords: readonly ["deepseek"];
189
+ readonly isOauth: false;
190
+ readonly isGateway: false;
191
+ readonly isLocal: false;
192
+ readonly defaultApiBase: "";
193
+ }, {
194
+ readonly name: "gemini";
195
+ readonly keywords: readonly ["gemini"];
196
+ readonly isOauth: false;
197
+ readonly isGateway: false;
198
+ readonly isLocal: false;
199
+ readonly defaultApiBase: "";
200
+ }, {
201
+ readonly name: "zhipu";
202
+ readonly keywords: readonly ["zhipu", "glm", "zai"];
203
+ readonly isOauth: false;
204
+ readonly isGateway: false;
205
+ readonly isLocal: false;
206
+ readonly defaultApiBase: "";
207
+ }, {
208
+ readonly name: "dashscope";
209
+ readonly keywords: readonly ["qwen", "dashscope"];
210
+ readonly isOauth: false;
211
+ readonly isGateway: false;
212
+ readonly isLocal: false;
213
+ readonly defaultApiBase: "";
214
+ }, {
215
+ readonly name: "moonshot";
216
+ readonly keywords: readonly ["moonshot", "kimi"];
217
+ readonly isOauth: false;
218
+ readonly isGateway: false;
219
+ readonly isLocal: false;
220
+ readonly defaultApiBase: "https://api.moonshot.ai/v1";
221
+ }, {
222
+ readonly name: "minimax";
223
+ readonly keywords: readonly ["minimax"];
224
+ readonly isOauth: false;
225
+ readonly isGateway: false;
226
+ readonly isLocal: false;
227
+ readonly defaultApiBase: "https://api.minimax.io/v1";
228
+ }, {
229
+ readonly name: "vllm";
230
+ readonly keywords: readonly ["vllm"];
231
+ readonly isOauth: false;
232
+ readonly isGateway: false;
233
+ readonly isLocal: true;
234
+ readonly defaultApiBase: "";
235
+ }, {
236
+ readonly name: "groq";
237
+ readonly keywords: readonly ["groq"];
238
+ readonly isOauth: false;
239
+ readonly isGateway: false;
240
+ readonly isLocal: false;
241
+ readonly defaultApiBase: "";
242
+ }];
243
+ export declare const DEFAULT_CONFIG: Config;
244
+ export declare function getProviderName(config: Config, model?: string): string | null;
245
+ export declare function getProvider(config: Config, model?: string): ProviderConfig | null;
246
+ export declare function getApiBase(config: Config, model?: string): string | null;
@@ -0,0 +1,94 @@
1
+ import path from "node:path";
2
+ export const PROVIDERS = [
3
+ { name: "custom", keywords: [], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
4
+ { name: "openrouter", keywords: ["openrouter"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://openrouter.ai/api/v1", detectByKeyPrefix: "sk-or-", detectByBaseKeyword: "openrouter" },
5
+ { name: "aihubmix", keywords: ["aihubmix"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://aihubmix.com/v1", detectByBaseKeyword: "aihubmix" },
6
+ { name: "siliconflow", keywords: ["siliconflow"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://api.siliconflow.cn/v1", detectByBaseKeyword: "siliconflow" },
7
+ { name: "volcengine", keywords: ["volcengine", "volces", "ark"], isOauth: false, isGateway: true, isLocal: false, defaultApiBase: "https://ark.cn-beijing.volces.com/api/v3", detectByBaseKeyword: "volces" },
8
+ { name: "anthropic", keywords: ["anthropic", "claude"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
9
+ { name: "openai", keywords: ["openai", "gpt"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
10
+ { name: "openai_codex", keywords: ["openai-codex", "codex"], isOauth: true, isGateway: false, isLocal: false, defaultApiBase: "https://chatgpt.com/backend-api" },
11
+ { name: "github_copilot", keywords: ["github_copilot", "copilot"], isOauth: true, isGateway: false, isLocal: false, defaultApiBase: "" },
12
+ { name: "deepseek", keywords: ["deepseek"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
13
+ { name: "gemini", keywords: ["gemini"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
14
+ { name: "zhipu", keywords: ["zhipu", "glm", "zai"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
15
+ { name: "dashscope", keywords: ["qwen", "dashscope"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
16
+ { name: "moonshot", keywords: ["moonshot", "kimi"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "https://api.moonshot.ai/v1" },
17
+ { name: "minimax", keywords: ["minimax"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "https://api.minimax.io/v1" },
18
+ { name: "vllm", keywords: ["vllm"], isOauth: false, isGateway: false, isLocal: true, defaultApiBase: "" },
19
+ { name: "groq", keywords: ["groq"], isOauth: false, isGateway: false, isLocal: false, defaultApiBase: "" },
20
+ ];
21
+ const providerDefaults = Object.fromEntries(PROVIDERS.map((p) => [p.name, { apiKey: "", apiBase: null, extraHeaders: null }]));
22
+ export const DEFAULT_CONFIG = {
23
+ agents: {
24
+ defaults: {
25
+ workspace: path.join("~", ".pretticlaw", "workspace"),
26
+ model: "anthropic/claude-sonnet-4",
27
+ provider: "auto",
28
+ maxTokens: 8192,
29
+ temperature: 0.1,
30
+ maxToolIterations: 40,
31
+ memoryWindow: 100,
32
+ },
33
+ },
34
+ channels: {
35
+ sendProgress: true,
36
+ sendToolHints: false,
37
+ whatsapp: { enabled: false, bridgeUrl: "ws://localhost:3001", bridgeToken: "", allowFrom: [] },
38
+ telegram: { enabled: false, token: "", allowFrom: [], proxy: null, replyToMessage: false },
39
+ discord: { enabled: false, token: "", allowFrom: [], gatewayUrl: "wss://gateway.discord.gg/?v=10&encoding=json", intents: 37377 },
40
+ feishu: { enabled: false, appId: "", appSecret: "", allowFrom: [] },
41
+ mochat: { enabled: false, baseUrl: "https://mochat.io", allowFrom: [] },
42
+ dingtalk: { enabled: false, clientId: "", clientSecret: "", allowFrom: [] },
43
+ email: { enabled: false, consentGranted: false, imapHost: "", allowFrom: [] },
44
+ slack: { enabled: false, botToken: "", appToken: "", groupPolicy: "mention", groupAllowFrom: [] },
45
+ qq: { enabled: false, appId: "", secret: "", allowFrom: [] },
46
+ matrix: { enabled: false, homeserver: "https://matrix.org", accessToken: "", userId: "", deviceId: "", allowFrom: [] },
47
+ },
48
+ providers: providerDefaults,
49
+ gateway: { host: "0.0.0.0", port: 18790, heartbeat: { enabled: true, intervalS: 1800 } },
50
+ tools: { web: { search: { apiKey: "", maxResults: 5 } }, exec: { timeout: 60, pathAppend: "" }, restrictToWorkspace: false, mcpServers: {} },
51
+ };
52
+ function normalize(name) {
53
+ return name.toLowerCase().replace(/-/g, "_");
54
+ }
55
+ export function getProviderName(config, model) {
56
+ const forced = config.agents.defaults.provider;
57
+ if (forced !== "auto")
58
+ return config.providers[forced] ? forced : null;
59
+ const m = (model ?? config.agents.defaults.model).toLowerCase();
60
+ const mNorm = normalize(m);
61
+ const prefix = m.includes("/") ? m.split("/", 1)[0] : "";
62
+ for (const spec of PROVIDERS) {
63
+ const p = config.providers[spec.name];
64
+ if (prefix && normalize(prefix) === spec.name && (spec.isOauth || p?.apiKey))
65
+ return spec.name;
66
+ }
67
+ for (const spec of PROVIDERS) {
68
+ const p = config.providers[spec.name];
69
+ if (spec.keywords.some((kw) => m.includes(kw) || mNorm.includes(normalize(kw))) && (spec.isOauth || p?.apiKey))
70
+ return spec.name;
71
+ }
72
+ for (const spec of PROVIDERS) {
73
+ if (spec.isOauth)
74
+ continue;
75
+ const p = config.providers[spec.name];
76
+ if (p?.apiKey)
77
+ return spec.name;
78
+ }
79
+ return null;
80
+ }
81
+ export function getProvider(config, model) {
82
+ const name = getProviderName(config, model);
83
+ return name ? config.providers[name] ?? null : null;
84
+ }
85
+ export function getApiBase(config, model) {
86
+ const name = getProviderName(config, model);
87
+ if (!name)
88
+ return null;
89
+ const p = config.providers[name];
90
+ if (p?.apiBase)
91
+ return p.apiBase;
92
+ const spec = PROVIDERS.find((s) => s.name === name);
93
+ return spec?.isGateway ? spec.defaultApiBase : null;
94
+ }
@@ -0,0 +1,33 @@
1
+ import type { CronJob, CronSchedule } from "./types.js";
2
+ declare function validateScheduleForAdd(schedule: CronSchedule): void;
3
+ export declare class CronService {
4
+ private readonly storePath;
5
+ onJob?: ((job: CronJob) => Promise<string | null>) | undefined;
6
+ private store;
7
+ private timer;
8
+ private running;
9
+ constructor(storePath: string, onJob?: ((job: CronJob) => Promise<string | null>) | undefined);
10
+ private loadStore;
11
+ private saveStore;
12
+ private getNextWakeMs;
13
+ private armTimer;
14
+ private onTimer;
15
+ private executeJob;
16
+ start(): Promise<void>;
17
+ stop(): void;
18
+ listJobs(includeDisabled?: boolean): CronJob[];
19
+ addJob(input: {
20
+ name: string;
21
+ schedule: CronSchedule;
22
+ message: string;
23
+ deliver?: boolean;
24
+ channel?: string;
25
+ to?: string;
26
+ deleteAfterRun?: boolean;
27
+ }): CronJob;
28
+ removeJob(jobId: string): boolean;
29
+ enableJob(jobId: string, enabled?: boolean): CronJob | null;
30
+ runJob(jobId: string, force?: boolean): Promise<boolean>;
31
+ status(): Record<string, unknown>;
32
+ }
33
+ export { validateScheduleForAdd };
@@ -0,0 +1,195 @@
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
+ function nowMs() { return Date.now(); }
6
+ function computeNextRun(schedule, now) {
7
+ if (schedule.kind === "at")
8
+ return schedule.atMs > now ? schedule.atMs : null;
9
+ if (schedule.kind === "every")
10
+ return schedule.everyMs > 0 ? now + schedule.everyMs : null;
11
+ if (schedule.kind === "cron") {
12
+ try {
13
+ const it = parser.parseExpression(schedule.expr, { currentDate: new Date(now), tz: schedule.tz });
14
+ return it.next().toDate().getTime();
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ function validateScheduleForAdd(schedule) {
23
+ if ("tz" in schedule && schedule.tz && schedule.kind !== "cron")
24
+ 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
+ }
29
+ catch {
30
+ throw new Error(`unknown timezone '${schedule.tz}'`);
31
+ }
32
+ }
33
+ }
34
+ export class CronService {
35
+ storePath;
36
+ onJob;
37
+ store = null;
38
+ timer = null;
39
+ running = false;
40
+ constructor(storePath, onJob) {
41
+ this.storePath = storePath;
42
+ this.onJob = onJob;
43
+ }
44
+ loadStore() {
45
+ if (this.store)
46
+ return this.store;
47
+ if (!fs.existsSync(this.storePath)) {
48
+ this.store = { version: 1, jobs: [] };
49
+ return this.store;
50
+ }
51
+ try {
52
+ const parsed = JSON.parse(fs.readFileSync(this.storePath, "utf8"));
53
+ this.store = parsed;
54
+ }
55
+ catch {
56
+ this.store = { version: 1, jobs: [] };
57
+ }
58
+ return this.store;
59
+ }
60
+ saveStore() {
61
+ if (!this.store)
62
+ return;
63
+ fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
64
+ fs.writeFileSync(this.storePath, JSON.stringify(this.store, null, 2), "utf8");
65
+ }
66
+ getNextWakeMs() {
67
+ const s = this.loadStore();
68
+ const times = s.jobs.filter((j) => j.enabled && j.state.nextRunAtMs).map((j) => j.state.nextRunAtMs);
69
+ return times.length ? Math.min(...times) : null;
70
+ }
71
+ armTimer() {
72
+ if (this.timer)
73
+ clearTimeout(this.timer);
74
+ const next = this.getNextWakeMs();
75
+ if (!this.running || !next)
76
+ return;
77
+ this.timer = setTimeout(() => void this.onTimer(), Math.max(0, next - nowMs()));
78
+ }
79
+ async onTimer() {
80
+ const s = this.loadStore();
81
+ const now = nowMs();
82
+ const due = s.jobs.filter((j) => j.enabled && j.state.nextRunAtMs && now >= j.state.nextRunAtMs);
83
+ for (const job of due)
84
+ await this.executeJob(job);
85
+ this.saveStore();
86
+ this.armTimer();
87
+ }
88
+ async executeJob(job) {
89
+ const start = nowMs();
90
+ try {
91
+ if (this.onJob)
92
+ await this.onJob(job);
93
+ job.state.lastStatus = "ok";
94
+ job.state.lastError = null;
95
+ }
96
+ catch (err) {
97
+ job.state.lastStatus = "error";
98
+ job.state.lastError = String(err);
99
+ }
100
+ job.state.lastRunAtMs = start;
101
+ job.updatedAtMs = nowMs();
102
+ if (job.schedule.kind === "at") {
103
+ if (job.deleteAfterRun) {
104
+ this.store.jobs = this.store.jobs.filter((j) => j.id !== job.id);
105
+ }
106
+ else {
107
+ job.enabled = false;
108
+ job.state.nextRunAtMs = null;
109
+ }
110
+ }
111
+ else {
112
+ job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
113
+ }
114
+ }
115
+ async start() {
116
+ this.running = true;
117
+ const s = this.loadStore();
118
+ for (const job of s.jobs)
119
+ if (job.enabled)
120
+ job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
121
+ this.saveStore();
122
+ this.armTimer();
123
+ }
124
+ stop() {
125
+ this.running = false;
126
+ if (this.timer)
127
+ clearTimeout(this.timer);
128
+ this.timer = null;
129
+ }
130
+ listJobs(includeDisabled = false) {
131
+ const s = this.loadStore();
132
+ const jobs = includeDisabled ? s.jobs : s.jobs.filter((j) => j.enabled);
133
+ return [...jobs].sort((a, b) => (a.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER) - (b.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER));
134
+ }
135
+ addJob(input) {
136
+ const s = this.loadStore();
137
+ validateScheduleForAdd(input.schedule);
138
+ const now = nowMs();
139
+ const job = {
140
+ id: randomUUID().slice(0, 8),
141
+ name: input.name,
142
+ enabled: true,
143
+ schedule: input.schedule,
144
+ payload: { kind: "agent_turn", message: input.message, deliver: !!input.deliver, channel: input.channel, to: input.to },
145
+ state: { nextRunAtMs: computeNextRun(input.schedule, now), lastRunAtMs: null, lastStatus: null, lastError: null },
146
+ createdAtMs: now,
147
+ updatedAtMs: now,
148
+ deleteAfterRun: !!input.deleteAfterRun,
149
+ };
150
+ s.jobs.push(job);
151
+ this.saveStore();
152
+ this.armTimer();
153
+ return job;
154
+ }
155
+ removeJob(jobId) {
156
+ const s = this.loadStore();
157
+ const before = s.jobs.length;
158
+ s.jobs = s.jobs.filter((j) => j.id !== jobId);
159
+ const removed = s.jobs.length < before;
160
+ if (removed) {
161
+ this.saveStore();
162
+ this.armTimer();
163
+ }
164
+ return removed;
165
+ }
166
+ enableJob(jobId, enabled = true) {
167
+ const s = this.loadStore();
168
+ const job = s.jobs.find((j) => j.id === jobId);
169
+ if (!job)
170
+ return null;
171
+ job.enabled = enabled;
172
+ job.updatedAtMs = nowMs();
173
+ job.state.nextRunAtMs = enabled ? computeNextRun(job.schedule, nowMs()) : null;
174
+ this.saveStore();
175
+ this.armTimer();
176
+ return job;
177
+ }
178
+ async runJob(jobId, force = false) {
179
+ const s = this.loadStore();
180
+ const job = s.jobs.find((j) => j.id === jobId);
181
+ if (!job)
182
+ return false;
183
+ if (!force && !job.enabled)
184
+ return false;
185
+ await this.executeJob(job);
186
+ this.saveStore();
187
+ this.armTimer();
188
+ return true;
189
+ }
190
+ status() {
191
+ const s = this.loadStore();
192
+ return { enabled: this.running, jobs: s.jobs.length, nextWakeAtMs: this.getNextWakeMs() };
193
+ }
194
+ }
195
+ export { validateScheduleForAdd };
@@ -0,0 +1,47 @@
1
+ export type CronSchedule = {
2
+ kind: "at";
3
+ atMs: number;
4
+ everyMs?: never;
5
+ expr?: never;
6
+ tz?: string;
7
+ } | {
8
+ kind: "every";
9
+ everyMs: number;
10
+ atMs?: never;
11
+ expr?: never;
12
+ tz?: string;
13
+ } | {
14
+ kind: "cron";
15
+ expr: string;
16
+ tz?: string;
17
+ atMs?: never;
18
+ everyMs?: never;
19
+ };
20
+ export interface CronPayload {
21
+ kind: "system_event" | "agent_turn";
22
+ message: string;
23
+ deliver: boolean;
24
+ channel?: string;
25
+ to?: string;
26
+ }
27
+ export interface CronJobState {
28
+ nextRunAtMs: number | null;
29
+ lastRunAtMs: number | null;
30
+ lastStatus: "ok" | "error" | "skipped" | null;
31
+ lastError: string | null;
32
+ }
33
+ export interface CronJob {
34
+ id: string;
35
+ name: string;
36
+ enabled: boolean;
37
+ schedule: CronSchedule;
38
+ payload: CronPayload;
39
+ state: CronJobState;
40
+ createdAtMs: number;
41
+ updatedAtMs: number;
42
+ deleteAfterRun: boolean;
43
+ }
44
+ export interface CronStore {
45
+ version: number;
46
+ jobs: CronJob[];
47
+ }
@@ -0,0 +1 @@
1
+ export {};