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,93 @@
1
+ import { BaseChannel } from "./base.js";
2
+ function splitMessage(content, maxLen = 4000) {
3
+ if (content.length <= maxLen)
4
+ return [content];
5
+ const chunks = [];
6
+ let rest = content;
7
+ while (rest.length > maxLen) {
8
+ let cut = rest.slice(0, maxLen);
9
+ let idx = Math.max(cut.lastIndexOf("\n"), cut.lastIndexOf(" "));
10
+ if (idx <= 0)
11
+ idx = maxLen;
12
+ chunks.push(rest.slice(0, idx));
13
+ rest = rest.slice(idx).trimStart();
14
+ }
15
+ if (rest)
16
+ chunks.push(rest);
17
+ return chunks;
18
+ }
19
+ export class TelegramChannel extends BaseChannel {
20
+ name = "telegram";
21
+ offset = 0;
22
+ constructor(config, bus) {
23
+ super(config, bus);
24
+ }
25
+ api(path) {
26
+ return `https://api.telegram.org/bot${this.config.token}/${path}`;
27
+ }
28
+ async start() {
29
+ if (!this.config.token) {
30
+ console.error("Telegram token not configured");
31
+ return;
32
+ }
33
+ this.running = true;
34
+ while (this.running) {
35
+ try {
36
+ const url = new URL(this.api("getUpdates"));
37
+ url.searchParams.set("timeout", "25");
38
+ url.searchParams.set("offset", String(this.offset));
39
+ url.searchParams.set("allowed_updates", JSON.stringify(["message"]));
40
+ const res = await fetch(url);
41
+ if (!res.ok) {
42
+ await new Promise((r) => setTimeout(r, 1500));
43
+ continue;
44
+ }
45
+ const data = (await res.json());
46
+ if (!data.ok)
47
+ continue;
48
+ for (const update of data.result) {
49
+ this.offset = update.update_id + 1;
50
+ const msg = update.message;
51
+ if (!msg)
52
+ continue;
53
+ if (msg.from?.is_bot)
54
+ continue;
55
+ const senderBase = String(msg.from?.id ?? "unknown");
56
+ const sender = msg.from?.username ? `${senderBase}|@${msg.from.username}` : senderBase;
57
+ const content = msg.text ?? msg.caption ?? "[Unsupported message]";
58
+ await this.handleMessage({
59
+ senderId: sender,
60
+ chatId: String(msg.chat.id),
61
+ content,
62
+ metadata: { message_id: msg.message_id },
63
+ });
64
+ }
65
+ }
66
+ catch {
67
+ await new Promise((r) => setTimeout(r, 1500));
68
+ }
69
+ }
70
+ }
71
+ async stop() {
72
+ this.running = false;
73
+ }
74
+ async send(msg) {
75
+ if (!this.config.token)
76
+ return;
77
+ const chunks = splitMessage(msg.content || "");
78
+ for (const chunk of chunks) {
79
+ const payload = {
80
+ chat_id: msg.chatId,
81
+ text: chunk,
82
+ };
83
+ const replyTo = msg.metadata?.message_id;
84
+ if (this.config.replyToMessage && replyTo)
85
+ payload.reply_parameters = { message_id: replyTo };
86
+ await fetch(this.api("sendMessage"), {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify(payload),
90
+ }).catch(() => undefined);
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildProgram(): Command;
@@ -0,0 +1,552 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import readline from "node:readline/promises";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { Command } from "commander";
6
+ import prompts from "prompts";
7
+ import chalk from "chalk";
8
+ import { loadConfig, saveConfig, getConfigPath, getDataDir } from "../config/loader.js";
9
+ import { DEFAULT_CONFIG, getProviderName } from "../config/schema.js";
10
+ import { syncWorkspaceTemplates, getWorkspacePath } from "../utils/helpers.js";
11
+ import { MessageBus } from "../bus/queue.js";
12
+ import { AgentLoop } from "../agent/loop.js";
13
+ import { SessionManager } from "../session/manager.js";
14
+ import { CronService } from "../cron/service.js";
15
+ import { HeartbeatService } from "../heartbeat/service.js";
16
+ import { ChannelManager } from "../channels/manager.js";
17
+ import { makeProvider } from "../providers/registry.js";
18
+ import { startDashboardServer } from "../web/server.js";
19
+ const MODEL_CHOICES = {
20
+ groq: [
21
+ "openai/gpt-oss-120b",
22
+ "openai/gpt-oss-20b",
23
+ "meta-llama/llama-4-maverick-17b-128e-instruct",
24
+ "groq/compound",
25
+ "groq/compound-mini",
26
+ ],
27
+ openrouter: [
28
+ "anthropic/claude-opus-4-1",
29
+ "anthropic/claude-sonnet-4",
30
+ "openai/gpt-4.1",
31
+ "google/gemini-2.5-pro",
32
+ ],
33
+ anthropic: [
34
+ "anthropic/claude-opus-4-5",
35
+ "anthropic/claude-sonnet-4",
36
+ ],
37
+ openai: [
38
+ "gpt-5.2",
39
+ "gpt-5.2-pro",
40
+ "gpt-5.3-codex",
41
+ "gpt-5.2-codex",
42
+ "gpt-5.1",
43
+ "gpt-5.1-codex",
44
+ "gpt-5 mini",
45
+ "gpt-5 nano",
46
+ "gpt-4.1",
47
+ "gpt-4.1 mini",
48
+ "gpt-4.1 nano",
49
+ "gpt-4o",
50
+ "gpt-4o mini",
51
+ ],
52
+ deepseek: [
53
+ "deepseek/deepseek-chat",
54
+ "deepseek/deepseek-reasoner",
55
+ ],
56
+ gemini: [
57
+ "gemini/gemini-2.5-pro",
58
+ "gemini/gemini-2.5-flash",
59
+ ],
60
+ moonshot: [
61
+ "moonshot/kimi-k2.5",
62
+ ],
63
+ minimax: [
64
+ "minimax/MiniMax-M2.1",
65
+ ],
66
+ dashscope: [
67
+ "dashscope/qwen-max",
68
+ ],
69
+ zhipu: [
70
+ "zai/glm-4.5",
71
+ ],
72
+ siliconflow: [
73
+ "openai/deepseek-ai/DeepSeek-R1",
74
+ ],
75
+ volcengine: [
76
+ "volcengine/deepseek-r1-250120",
77
+ ],
78
+ vllm: [
79
+ "hosted_vllm/llama-3.1-8b-instruct",
80
+ ],
81
+ custom: [
82
+ "custom/model-name",
83
+ ],
84
+ };
85
+ export function buildProgram() {
86
+ const program = new Command();
87
+ program
88
+ .name("pretticlaw")
89
+ .description("pretticlaw - Personal AI Assistant")
90
+ .version("0.1.0", "-v, --version", "show version");
91
+ program.command("onboard").description("Initialize pretticlaw configuration and workspace").action(async () => {
92
+ const configPath = getConfigPath();
93
+ let config = loadConfig();
94
+ if (fs.existsSync(configPath)) {
95
+ console.log(`Config already exists at ${configPath}`);
96
+ const rl = readline.createInterface({ input, output });
97
+ const ans = (await rl.question("Overwrite? [y/N] ")).trim().toLowerCase();
98
+ rl.close();
99
+ if (ans === "y") {
100
+ config = structuredClone(DEFAULT_CONFIG);
101
+ saveConfig(config);
102
+ console.log(`Config reset to defaults at ${configPath}`);
103
+ }
104
+ else {
105
+ saveConfig(config);
106
+ console.log(`Config refreshed at ${configPath} (existing values preserved)`);
107
+ }
108
+ }
109
+ else {
110
+ saveConfig(config);
111
+ console.log(`Created config at ${configPath}`);
112
+ }
113
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
114
+ if (!fs.existsSync(workspace)) {
115
+ fs.mkdirSync(workspace, { recursive: true });
116
+ console.log(`Created workspace at ${workspace}`);
117
+ }
118
+ syncWorkspaceTemplates(workspace);
119
+ if (process.stdin.isTTY && process.stdout.isTTY) {
120
+ console.log(chalk.cyan("\npretticlaw Setup Wizard"));
121
+ console.log(chalk.gray("Use arrow keys to pick options.\n"));
122
+ const providerRes = await prompts({
123
+ type: "select",
124
+ name: "provider",
125
+ message: "Choose your provider",
126
+ choices: [
127
+ { title: "OpenRouter (recommended)", value: "openrouter" },
128
+ { title: "OpenAI", value: "openai" },
129
+ { title: "DeepSeek", value: "deepseek" },
130
+ { title: "Groq", value: "groq" },
131
+ { title: "Moonshot", value: "moonshot" },
132
+ { title: "MiniMax", value: "minimax" },
133
+ { title: "DashScope", value: "dashscope" },
134
+ { title: "Zhipu", value: "zhipu" },
135
+ { title: "SiliconFlow", value: "siliconflow" },
136
+ { title: "VolcEngine", value: "volcengine" },
137
+ { title: "vLLM/Local", value: "vllm" },
138
+ { title: "Custom OpenAI-Compatible", value: "custom" },
139
+ ],
140
+ });
141
+ const provider = String(providerRes.provider || "openrouter");
142
+ const models = MODEL_CHOICES[provider] ?? [config.agents.defaults.model];
143
+ const modelRes = await prompts({
144
+ type: "select",
145
+ name: "model",
146
+ message: "Choose your model",
147
+ choices: models.map((m) => ({ title: m, value: m })),
148
+ });
149
+ const model = String(modelRes.model || models[0]);
150
+ let apiKey = "";
151
+ if (provider !== "vllm") {
152
+ const keyRes = await prompts({
153
+ type: "password",
154
+ name: "apiKey",
155
+ message: "Enter API key",
156
+ validate: (v) => (v && v.trim().length > 0 ? true : "API key cannot be empty"),
157
+ });
158
+ apiKey = String(keyRes.apiKey || "");
159
+ }
160
+ config.agents.defaults.provider = provider;
161
+ config.agents.defaults.model = model;
162
+ if (provider in config.providers) {
163
+ config.providers[provider].apiKey = apiKey;
164
+ }
165
+ if (provider === "custom" || provider === "vllm") {
166
+ const baseRes = await prompts({
167
+ type: "text",
168
+ name: "apiBase",
169
+ message: "Enter API base URL",
170
+ initial: provider === "vllm" ? "http://localhost:8000/v1" : "https://api.openai.com/v1",
171
+ });
172
+ const apiBase = String(baseRes.apiBase || "");
173
+ if (provider in config.providers) {
174
+ config.providers[provider].apiBase = apiBase.trim();
175
+ }
176
+ }
177
+ const channelRes = await prompts({
178
+ type: "confirm",
179
+ name: "wantChannel",
180
+ message: "Do you want to configure a chat channel now?",
181
+ initial: true,
182
+ });
183
+ if (channelRes.wantChannel) {
184
+ const chRes = await prompts({
185
+ type: "select",
186
+ name: "channel",
187
+ message: "Choose chat channel",
188
+ choices: [
189
+ { title: "Telegram", value: "telegram" },
190
+ { title: "WhatsApp", value: "whatsapp" },
191
+ ],
192
+ });
193
+ const channel = String(chRes.channel || "telegram");
194
+ if (channel === "telegram") {
195
+ const tokenRes = await prompts({
196
+ type: "password",
197
+ name: "token",
198
+ message: "Enter Telegram Bot Token",
199
+ validate: (v) => (v && v.trim().length > 0 ? true : "Token cannot be empty"),
200
+ });
201
+ config.channels.telegram.enabled = true;
202
+ config.channels.telegram.token = String(tokenRes.token || "").trim();
203
+ }
204
+ else if (channel === "whatsapp") {
205
+ const tokenRes = await prompts({
206
+ type: "password",
207
+ name: "token",
208
+ message: "Enter WhatsApp bridge token",
209
+ validate: (v) => (v && v.trim().length > 0 ? true : "Token cannot be empty"),
210
+ });
211
+ config.channels.whatsapp.enabled = true;
212
+ config.channels.whatsapp.bridgeToken = String(tokenRes.token || "").trim();
213
+ }
214
+ }
215
+ saveConfig(config);
216
+ console.log(chalk.green("\nConfigs created."));
217
+ console.log(chalk.yellow("Run prettiflow gateway"));
218
+ console.log(chalk.yellow("or chat with agent: pretticlaw agent"));
219
+ return;
220
+ }
221
+ console.log("\npretticlaw is ready!");
222
+ console.log("Config is non-interactive in this terminal.");
223
+ console.log("Edit ~/.pretticlaw/config.json, then run prettiflow gateway or pretticlaw agent.");
224
+ });
225
+ program
226
+ .command("agent")
227
+ .description("Interact with the agent directly")
228
+ .option("-m, --message <message>", "Message to send to the agent")
229
+ .option("-s, --session <session>", "Session ID", "cli:direct")
230
+ .option("--no-markdown", "Disable markdown rendering")
231
+ .action(async (opts) => {
232
+ const config = loadConfig();
233
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
234
+ syncWorkspaceTemplates(workspace, true);
235
+ const bus = new MessageBus();
236
+ let provider;
237
+ try {
238
+ provider = makeProvider(config);
239
+ }
240
+ catch (err) {
241
+ console.log(chalk.red(String(err)));
242
+ return;
243
+ }
244
+ const sessionManager = new SessionManager(workspace);
245
+ const cronPath = path.join(getDataDir(), "cron", "jobs.json");
246
+ const cron = new CronService(cronPath);
247
+ const loop = new AgentLoop({
248
+ bus,
249
+ provider,
250
+ workspace,
251
+ model: config.agents.defaults.model,
252
+ temperature: config.agents.defaults.temperature,
253
+ maxTokens: config.agents.defaults.maxTokens,
254
+ maxIterations: config.agents.defaults.maxToolIterations,
255
+ memoryWindow: config.agents.defaults.memoryWindow,
256
+ braveApiKey: config.tools.web.search.apiKey || null,
257
+ execConfig: config.tools.exec,
258
+ cronService: cron,
259
+ restrictToWorkspace: config.tools.restrictToWorkspace,
260
+ channelsConfig: config.channels,
261
+ });
262
+ if (opts.message) {
263
+ const response = await loop.processDirect(String(opts.message), String(opts.session));
264
+ console.log(`\npretticlaw\n${response}\n`);
265
+ }
266
+ else {
267
+ console.log("Interactive mode (type exit to quit)");
268
+ const rl = readline.createInterface({ input, output });
269
+ while (true) {
270
+ const line = await rl.question("You: ");
271
+ const command = line.trim().toLowerCase();
272
+ if (["exit", "quit", "/exit", "/quit", ":q"].includes(command))
273
+ break;
274
+ const response = await loop.processDirect(line, String(opts.session));
275
+ console.log(`\npretticlaw\n${response}\n`);
276
+ }
277
+ rl.close();
278
+ }
279
+ });
280
+ program.command("gateway")
281
+ .description("Start the pretticlaw gateway")
282
+ .option("-p, --port <port>", "Gateway port", "18790")
283
+ .action(async () => {
284
+ const config = loadConfig();
285
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
286
+ syncWorkspaceTemplates(workspace, true);
287
+ const bus = new MessageBus();
288
+ let provider;
289
+ try {
290
+ provider = makeProvider(config);
291
+ }
292
+ catch (err) {
293
+ console.log(chalk.red(String(err)));
294
+ return;
295
+ }
296
+ const cronPath = path.join(getDataDir(), "cron", "jobs.json");
297
+ const cron = new CronService(cronPath);
298
+ const sessionManager = new SessionManager(workspace);
299
+ const loop = new AgentLoop({
300
+ bus,
301
+ provider,
302
+ workspace,
303
+ sessionManager,
304
+ model: config.agents.defaults.model,
305
+ temperature: config.agents.defaults.temperature,
306
+ maxTokens: config.agents.defaults.maxTokens,
307
+ maxIterations: config.agents.defaults.maxToolIterations,
308
+ memoryWindow: config.agents.defaults.memoryWindow,
309
+ braveApiKey: config.tools.web.search.apiKey || null,
310
+ execConfig: config.tools.exec,
311
+ cronService: cron,
312
+ restrictToWorkspace: config.tools.restrictToWorkspace,
313
+ channelsConfig: config.channels,
314
+ });
315
+ cron.onJob = async (job) => {
316
+ const response = await loop.processDirect(job.payload.message, `cron:${job.id}`, job.payload.channel ?? "cli", job.payload.to ?? "direct");
317
+ if (job.payload.deliver && job.payload.to) {
318
+ await bus.publishOutbound({ channel: job.payload.channel ?? "cli", chatId: job.payload.to, content: response });
319
+ }
320
+ return response;
321
+ };
322
+ const channels = new ChannelManager(config, bus);
323
+ const dashboard = startDashboardServer({ agent: loop, cron, config, port: 6767, sessionManager, sessionKey: "web:dashboard" });
324
+ const heartbeat = new HeartbeatService(workspace, provider, loop.model, async (tasks) => {
325
+ const sessions = sessionManager.listSessions();
326
+ const first = sessions.find((s) => (s.key ?? "").includes(":"))?.key ?? "cli:direct";
327
+ const [channel, chatId] = first.split(/:(.*)/s, 2);
328
+ return loop.processDirect(tasks, "heartbeat", channel || "cli", chatId || "direct");
329
+ }, async (response) => {
330
+ const sessions = sessionManager.listSessions();
331
+ const first = sessions.find((s) => (s.key ?? "").includes(":"))?.key ?? "";
332
+ const [channel, chatId] = first ? first.split(/:(.*)/s, 2) : ["cli", "direct"];
333
+ if (channel !== "cli")
334
+ await bus.publishOutbound({ channel, chatId, content: response });
335
+ }, config.gateway.heartbeat.intervalS, config.gateway.heartbeat.enabled);
336
+ console.log(chalk.cyan(`Starting pretticlaw gateway on port ${config.gateway.port}...`));
337
+ if (channels.enabledChannels.length) {
338
+ console.log(chalk.green(`Channels enabled: ${channels.enabledChannels.join(", ")}`));
339
+ }
340
+ else {
341
+ console.log(chalk.yellow("No channels enabled. Gateway will stay idle until channels are configured."));
342
+ }
343
+ console.log(chalk.green(`Dashboard: http://localhost:${dashboard.port}/chat`));
344
+ console.log(chalk.gray("Gateway running. Press Ctrl+C to stop."));
345
+ await cron.start();
346
+ await heartbeat.start();
347
+ await Promise.all([loop.run(), channels.startAll()]);
348
+ });
349
+ const channels = program.command("channels").description("Manage channels");
350
+ channels.command("status").action(() => {
351
+ const config = loadConfig();
352
+ const table = [
353
+ ["WhatsApp", config.channels.whatsapp.enabled ? "yes" : "no", config.channels.whatsapp.bridgeUrl],
354
+ ["Telegram", config.channels.telegram.enabled ? "yes" : "no", config.channels.telegram.token ? `token: ${config.channels.telegram.token.slice(0, 10)}...` : "not configured"],
355
+ ["Discord", config.channels.discord.enabled ? "yes" : "no", config.channels.discord.gatewayUrl],
356
+ ["Feishu", config.channels.feishu.enabled ? "yes" : "no", config.channels.feishu.appId ? `app_id: ${config.channels.feishu.appId.slice(0, 10)}...` : "not configured"],
357
+ ["Mochat", config.channels.mochat.enabled ? "yes" : "no", config.channels.mochat.baseUrl || "not configured"],
358
+ ["DingTalk", config.channels.dingtalk.enabled ? "yes" : "no", config.channels.dingtalk.clientId ? `client_id: ${config.channels.dingtalk.clientId.slice(0, 10)}...` : "not configured"],
359
+ ["Email", config.channels.email.enabled ? "yes" : "no", config.channels.email.imapHost || "not configured"],
360
+ ["Slack", config.channels.slack.enabled ? "yes" : "no", config.channels.slack.appToken && config.channels.slack.botToken ? "socket" : "not configured"],
361
+ ["QQ", config.channels.qq.enabled ? "yes" : "no", config.channels.qq.appId ? `app_id: ${config.channels.qq.appId.slice(0, 10)}...` : "not configured"],
362
+ ["Matrix", config.channels.matrix.enabled ? "yes" : "no", config.channels.matrix.homeserver || "not configured"],
363
+ ];
364
+ for (const row of table)
365
+ console.log(`${row[0].padEnd(12)} ${row[1].padEnd(3)} ${row[2]}`);
366
+ });
367
+ channels.command("login").description("Link device via QR code").action(() => {
368
+ console.log("Bridge login is not yet implemented in TypeScript port. Use the existing Python bridge flow.");
369
+ });
370
+ const cron = program.command("cron").description("Manage scheduled tasks");
371
+ cron.command("list").option("-a, --all", "Include disabled jobs", false).action((opts) => {
372
+ const service = new CronService(path.join(getDataDir(), "cron", "jobs.json"));
373
+ const jobs = service.listJobs(!!opts.all);
374
+ if (!jobs.length)
375
+ return console.log("No scheduled jobs.");
376
+ for (const job of jobs) {
377
+ const sched = job.schedule.kind === "every" ? `every ${job.schedule.everyMs / 1000}s` : job.schedule.kind === "cron" ? `${job.schedule.expr} (${job.schedule.tz ?? "local"})` : "one-time";
378
+ const next = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "";
379
+ console.log(`${job.id} | ${job.name} | ${sched} | ${job.enabled ? "enabled" : "disabled"} | ${next}`);
380
+ }
381
+ });
382
+ cron.command("add")
383
+ .requiredOption("-n, --name <name>")
384
+ .requiredOption("-m, --message <message>")
385
+ .option("-e, --every <seconds>")
386
+ .option("-c, --cron <expr>")
387
+ .option("--tz <tz>")
388
+ .option("--at <iso>")
389
+ .option("-d, --deliver", "Deliver response to channel", false)
390
+ .option("--to <recipient>")
391
+ .option("--channel <channel>")
392
+ .action((opts) => {
393
+ if (opts.tz && !opts.cron) {
394
+ console.log("Error: --tz can only be used with --cron");
395
+ process.exitCode = 1;
396
+ return;
397
+ }
398
+ let schedule;
399
+ if (opts.every)
400
+ schedule = { kind: "every", everyMs: Number(opts.every) * 1000 };
401
+ else if (opts.cron)
402
+ schedule = { kind: "cron", expr: String(opts.cron), tz: opts.tz ? String(opts.tz) : undefined };
403
+ else if (opts.at) {
404
+ const dt = new Date(String(opts.at));
405
+ if (Number.isNaN(dt.getTime())) {
406
+ console.log("Error: invalid --at datetime");
407
+ process.exitCode = 1;
408
+ return;
409
+ }
410
+ schedule = { kind: "at", atMs: dt.getTime() };
411
+ }
412
+ else {
413
+ console.log("Error: Must specify --every, --cron, or --at");
414
+ process.exitCode = 1;
415
+ return;
416
+ }
417
+ const service = new CronService(path.join(getDataDir(), "cron", "jobs.json"));
418
+ try {
419
+ const job = service.addJob({ name: String(opts.name), schedule, message: String(opts.message), deliver: !!opts.deliver, to: opts.to ? String(opts.to) : undefined, channel: opts.channel ? String(opts.channel) : undefined });
420
+ console.log(`Added job '${job.name}' (${job.id})`);
421
+ }
422
+ catch (err) {
423
+ console.log(`Error: ${String(err).replace(/^Error:\s*/, "")}`);
424
+ process.exitCode = 1;
425
+ }
426
+ });
427
+ cron.command("remove").argument("<jobId>").action((jobId) => {
428
+ const service = new CronService(path.join(getDataDir(), "cron", "jobs.json"));
429
+ if (service.removeJob(jobId))
430
+ console.log(`Removed job ${jobId}`);
431
+ else
432
+ console.log(`Job ${jobId} not found`);
433
+ });
434
+ cron.command("enable").argument("<jobId>").option("--disable", "Disable instead", false).action((jobId, opts) => {
435
+ const service = new CronService(path.join(getDataDir(), "cron", "jobs.json"));
436
+ const job = service.enableJob(jobId, !opts.disable);
437
+ if (!job)
438
+ return console.log(`Job ${jobId} not found`);
439
+ console.log(`Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
440
+ });
441
+ cron.command("run").argument("<jobId>").option("-f, --force", "Run even if disabled", false).action(async (jobId, opts) => {
442
+ const config = loadConfig();
443
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
444
+ let provider;
445
+ try {
446
+ provider = makeProvider(config);
447
+ }
448
+ catch (err) {
449
+ console.log(chalk.red(String(err)));
450
+ process.exitCode = 1;
451
+ return;
452
+ }
453
+ const bus = new MessageBus();
454
+ const loop = new AgentLoop({
455
+ bus,
456
+ provider,
457
+ workspace,
458
+ model: config.agents.defaults.model,
459
+ temperature: config.agents.defaults.temperature,
460
+ maxTokens: config.agents.defaults.maxTokens,
461
+ maxIterations: config.agents.defaults.maxToolIterations,
462
+ memoryWindow: config.agents.defaults.memoryWindow,
463
+ braveApiKey: config.tools.web.search.apiKey || null,
464
+ execConfig: config.tools.exec,
465
+ restrictToWorkspace: config.tools.restrictToWorkspace,
466
+ channelsConfig: config.channels,
467
+ });
468
+ const service = new CronService(path.join(getDataDir(), "cron", "jobs.json"));
469
+ const resultHolder = [];
470
+ service.onJob = async (job) => {
471
+ const response = await loop.processDirect(job.payload.message, `cron:${job.id}`, job.payload.channel ?? "cli", job.payload.to ?? "direct");
472
+ resultHolder.push(response);
473
+ return response;
474
+ };
475
+ if (await service.runJob(jobId, !!opts.force)) {
476
+ console.log("Job executed");
477
+ if (resultHolder.length)
478
+ console.log(resultHolder[0]);
479
+ }
480
+ else {
481
+ console.log(`Failed to run job ${jobId}`);
482
+ process.exitCode = 1;
483
+ }
484
+ });
485
+ program.command("status").description("Show pretticlaw status").action(() => {
486
+ const configPath = getConfigPath();
487
+ const config = loadConfig();
488
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
489
+ console.log("pretticlaw Status\n");
490
+ console.log(`Config: ${configPath} ${fs.existsSync(configPath) ? "yes" : "no"}`);
491
+ console.log(`Workspace: ${workspace} ${fs.existsSync(workspace) ? "yes" : "no"}`);
492
+ console.log(`Model: ${config.agents.defaults.model}`);
493
+ const names = Object.keys(config.providers);
494
+ for (const name of names) {
495
+ const p = config.providers[name];
496
+ const oauth = ["openai_codex", "github_copilot"].includes(name);
497
+ if (oauth)
498
+ console.log(`${name}: OAuth`);
499
+ else if (p.apiBase && !p.apiKey)
500
+ console.log(`${name}: ${p.apiBase}`);
501
+ else
502
+ console.log(`${name}: ${p.apiKey ? "set" : "not set"}`);
503
+ }
504
+ console.log(`Resolved provider: ${getProviderName(config) ?? "none"}`);
505
+ });
506
+ program.command("doctor").description("Validate provider/model configuration and connectivity").action(async () => {
507
+ const config = loadConfig();
508
+ const providerName = getProviderName(config);
509
+ console.log(chalk.cyan("pretticlaw doctor\n"));
510
+ console.log(`Resolved provider: ${providerName ?? "none"}`);
511
+ console.log(`Model: ${config.agents.defaults.model}`);
512
+ let provider;
513
+ try {
514
+ provider = makeProvider(config);
515
+ }
516
+ catch (err) {
517
+ console.log(chalk.red(String(err)));
518
+ process.exitCode = 1;
519
+ return;
520
+ }
521
+ try {
522
+ const result = await provider.chat({
523
+ messages: [{ role: "user", content: "Reply with: OK" }],
524
+ model: config.agents.defaults.model,
525
+ maxTokens: 16,
526
+ temperature: 0,
527
+ });
528
+ if ((result.content || "").toLowerCase().includes("error")) {
529
+ console.log(chalk.red(`LLM check failed: ${result.content}`));
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+ console.log(chalk.green(`LLM check passed: ${result.content ?? "(empty response)"}`));
534
+ console.log(chalk.green("Config looks good."));
535
+ }
536
+ catch (err) {
537
+ console.log(chalk.red(`LLM check failed: ${String(err)}`));
538
+ process.exitCode = 1;
539
+ }
540
+ });
541
+ const provider = program.command("provider").description("Manage providers");
542
+ provider.command("login").argument("<provider>").action((providerName) => {
543
+ const key = providerName.replace(/-/g, "_");
544
+ if (!["openai_codex", "github_copilot"].includes(key)) {
545
+ console.log(`Unknown OAuth provider: ${providerName}. Supported: openai-codex, github-copilot`);
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+ console.log(`OAuth login for ${providerName} is not yet implemented in TypeScript port.`);
550
+ });
551
+ return program;
552
+ }
@@ -0,0 +1,5 @@
1
+ import { type Config } from "./schema.js";
2
+ export declare function getConfigPath(): string;
3
+ export declare function getDataDir(): string;
4
+ export declare function loadConfig(configPath?: string): Config;
5
+ export declare function saveConfig(config: Config, configPath?: string): void;