nextclaw 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2324 @@
1
+ // src/config/brand.ts
2
+ var ENV_APP_NAME_KEY = "NEXTCLAW_APP_NAME";
3
+ var envAppName = process.env[ENV_APP_NAME_KEY]?.trim();
4
+ var APP_NAME = envAppName && envAppName.length > 0 ? envAppName : "nextclaw";
5
+ var APP_TAGLINE = "Personal AI Assistant";
6
+ var APP_TITLE = `${APP_NAME.slice(0, 1).toUpperCase()}${APP_NAME.slice(1)}`;
7
+ var ENV_HOME_KEY = "NEXTCLAW_HOME";
8
+ var DEFAULT_HOME_DIR = ".nextclaw";
9
+ var DEFAULT_CONFIG_FILE = "config.json";
10
+ var DEFAULT_WORKSPACE_DIR = "workspace";
11
+ var DEFAULT_CONFIG_PATH = `~/${DEFAULT_HOME_DIR}/${DEFAULT_CONFIG_FILE}`;
12
+ var DEFAULT_WORKSPACE_PATH = `~/${DEFAULT_HOME_DIR}/${DEFAULT_WORKSPACE_DIR}`;
13
+ var APP_USER_AGENT = APP_NAME;
14
+ var APP_REPLY_SUBJECT = `${APP_NAME} reply`;
15
+ var SKILL_METADATA_KEY = "nextclaw";
16
+
17
+ // src/utils/helpers.ts
18
+ import { existsSync, mkdirSync } from "fs";
19
+ import { homedir } from "os";
20
+ import { resolve } from "path";
21
+ function ensureDir(path) {
22
+ if (!existsSync(path)) {
23
+ mkdirSync(path, { recursive: true });
24
+ }
25
+ return path;
26
+ }
27
+ function getDataPath() {
28
+ const override = process.env[ENV_HOME_KEY]?.trim();
29
+ if (override) {
30
+ return ensureDir(resolve(override));
31
+ }
32
+ return ensureDir(resolve(homedir(), DEFAULT_HOME_DIR));
33
+ }
34
+ function getWorkspacePath(workspace) {
35
+ if (workspace) {
36
+ return ensureDir(resolve(expandHome(workspace)));
37
+ }
38
+ return ensureDir(resolve(getDataPath(), "workspace"));
39
+ }
40
+ function getSessionsPath() {
41
+ return ensureDir(resolve(getDataPath(), "sessions"));
42
+ }
43
+ function todayDate() {
44
+ const now = /* @__PURE__ */ new Date();
45
+ return now.toISOString().slice(0, 10);
46
+ }
47
+ function safeFilename(value) {
48
+ return value.replace(/[<>:"/\\|?*]/g, "_").trim();
49
+ }
50
+ function expandHome(value) {
51
+ if (value.startsWith("~/")) {
52
+ return resolve(homedir(), value.slice(2));
53
+ }
54
+ return value;
55
+ }
56
+
57
+ // src/session/manager.ts
58
+ import { readFileSync, writeFileSync, existsSync as existsSync2, readdirSync, unlinkSync } from "fs";
59
+ import { join } from "path";
60
+ var SessionManager = class {
61
+ constructor(workspace) {
62
+ this.workspace = workspace;
63
+ this.sessionsDir = getSessionsPath();
64
+ }
65
+ sessionsDir;
66
+ cache = /* @__PURE__ */ new Map();
67
+ getSessionPath(key) {
68
+ const safeKey = safeFilename(key.replace(/:/g, "_"));
69
+ return join(this.sessionsDir, `${safeKey}.jsonl`);
70
+ }
71
+ getOrCreate(key) {
72
+ const cached = this.cache.get(key);
73
+ if (cached) {
74
+ return cached;
75
+ }
76
+ const loaded = this.load(key);
77
+ const session = loaded ?? {
78
+ key,
79
+ messages: [],
80
+ createdAt: /* @__PURE__ */ new Date(),
81
+ updatedAt: /* @__PURE__ */ new Date(),
82
+ metadata: {}
83
+ };
84
+ this.cache.set(key, session);
85
+ return session;
86
+ }
87
+ addMessage(session, role, content, extra = {}) {
88
+ const msg = {
89
+ role,
90
+ content,
91
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
92
+ ...extra
93
+ };
94
+ session.messages.push(msg);
95
+ session.updatedAt = /* @__PURE__ */ new Date();
96
+ }
97
+ getHistory(session, maxMessages = 50) {
98
+ const recent = session.messages.length > maxMessages ? session.messages.slice(-maxMessages) : session.messages;
99
+ return recent.map((msg) => ({ role: msg.role, content: msg.content }));
100
+ }
101
+ clear(session) {
102
+ session.messages = [];
103
+ session.updatedAt = /* @__PURE__ */ new Date();
104
+ }
105
+ load(key) {
106
+ const path = this.getSessionPath(key);
107
+ if (!existsSync2(path)) {
108
+ return null;
109
+ }
110
+ try {
111
+ const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean);
112
+ const messages = [];
113
+ let metadata = {};
114
+ let createdAt = /* @__PURE__ */ new Date();
115
+ let updatedAt = /* @__PURE__ */ new Date();
116
+ for (const line of lines) {
117
+ const data = JSON.parse(line);
118
+ if (data._type === "metadata") {
119
+ metadata = data.metadata ?? {};
120
+ if (data.created_at) {
121
+ createdAt = new Date(String(data.created_at));
122
+ }
123
+ if (data.updated_at) {
124
+ updatedAt = new Date(String(data.updated_at));
125
+ }
126
+ } else {
127
+ messages.push(data);
128
+ }
129
+ }
130
+ return {
131
+ key,
132
+ messages,
133
+ createdAt,
134
+ updatedAt,
135
+ metadata
136
+ };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ save(session) {
142
+ const path = this.getSessionPath(session.key);
143
+ const metadataLine = {
144
+ _type: "metadata",
145
+ created_at: session.createdAt.toISOString(),
146
+ updated_at: session.updatedAt.toISOString(),
147
+ metadata: session.metadata
148
+ };
149
+ const lines = [JSON.stringify(metadataLine), ...session.messages.map((msg) => JSON.stringify(msg))].join("\n");
150
+ writeFileSync(path, `${lines}
151
+ `);
152
+ this.cache.set(session.key, session);
153
+ }
154
+ delete(key) {
155
+ this.cache.delete(key);
156
+ const path = this.getSessionPath(key);
157
+ if (existsSync2(path)) {
158
+ unlinkSync(path);
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ listSessions() {
164
+ const sessions = [];
165
+ for (const entry of readdirSync(this.sessionsDir, { withFileTypes: true })) {
166
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
167
+ continue;
168
+ }
169
+ const path = join(this.sessionsDir, entry.name);
170
+ const firstLine = readFileSync(path, "utf-8").split("\n")[0];
171
+ if (!firstLine) {
172
+ continue;
173
+ }
174
+ try {
175
+ const data = JSON.parse(firstLine);
176
+ if (data._type === "metadata") {
177
+ sessions.push({
178
+ key: entry.name.replace(/\.jsonl$/, "").replace(/_/g, ":"),
179
+ created_at: data.created_at,
180
+ updated_at: data.updated_at,
181
+ path
182
+ });
183
+ }
184
+ } catch {
185
+ continue;
186
+ }
187
+ }
188
+ return sessions;
189
+ }
190
+ };
191
+
192
+ // src/agent/context.ts
193
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
194
+ import { join as join4, extname } from "path";
195
+ import { fileURLToPath as fileURLToPath2 } from "url";
196
+
197
+ // src/agent/memory.ts
198
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
199
+ import { join as join2 } from "path";
200
+ var MemoryStore = class {
201
+ constructor(workspace) {
202
+ this.workspace = workspace;
203
+ this.memoryDir = ensureDir(join2(workspace, "memory"));
204
+ this.memoryFile = join2(this.memoryDir, "MEMORY.md");
205
+ }
206
+ memoryDir;
207
+ memoryFile;
208
+ getTodayFile() {
209
+ return join2(this.memoryDir, `${todayDate()}.md`);
210
+ }
211
+ readToday() {
212
+ const todayFile = this.getTodayFile();
213
+ if (existsSync3(todayFile)) {
214
+ return readFileSync2(todayFile, "utf-8");
215
+ }
216
+ return "";
217
+ }
218
+ appendToday(content) {
219
+ const todayFile = this.getTodayFile();
220
+ let nextContent = content;
221
+ if (existsSync3(todayFile)) {
222
+ const existing = readFileSync2(todayFile, "utf-8");
223
+ nextContent = `${existing}
224
+ ${content}`;
225
+ } else {
226
+ const header = `# ${todayDate()}
227
+
228
+ `;
229
+ nextContent = header + content;
230
+ }
231
+ writeFileSync2(todayFile, nextContent, "utf-8");
232
+ }
233
+ readLongTerm() {
234
+ if (existsSync3(this.memoryFile)) {
235
+ return readFileSync2(this.memoryFile, "utf-8");
236
+ }
237
+ return "";
238
+ }
239
+ writeLongTerm(content) {
240
+ writeFileSync2(this.memoryFile, content, "utf-8");
241
+ }
242
+ getRecentMemories(days = 7) {
243
+ const memories = [];
244
+ const today = /* @__PURE__ */ new Date();
245
+ for (let i = 0; i < days; i += 1) {
246
+ const date = new Date(today);
247
+ date.setDate(today.getDate() - i);
248
+ const dateStr = date.toISOString().slice(0, 10);
249
+ const path = join2(this.memoryDir, `${dateStr}.md`);
250
+ if (existsSync3(path)) {
251
+ memories.push(readFileSync2(path, "utf-8"));
252
+ }
253
+ }
254
+ return memories.length ? memories.join("\n\n---\n\n") : "";
255
+ }
256
+ listMemoryFiles() {
257
+ if (!existsSync3(this.memoryDir)) {
258
+ return [];
259
+ }
260
+ return readdirSync2(this.memoryDir).filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name)).sort().reverse().map((name) => join2(this.memoryDir, name));
261
+ }
262
+ getMemoryContext() {
263
+ const parts = [];
264
+ const longTerm = this.readLongTerm();
265
+ if (longTerm) {
266
+ parts.push(`## Long-term Memory
267
+ ${longTerm}`);
268
+ }
269
+ const today = this.readToday();
270
+ if (today) {
271
+ parts.push(`## Today's Notes
272
+ ${today}`);
273
+ }
274
+ return parts.length ? parts.join("\n\n") : "";
275
+ }
276
+ };
277
+
278
+ // src/agent/skills.ts
279
+ import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
280
+ import { join as join3 } from "path";
281
+ import { fileURLToPath } from "url";
282
+ var BUILTIN_SKILLS_DIR = join3(fileURLToPath(new URL(".", import.meta.url)), "skills");
283
+ var SkillsLoader = class {
284
+ constructor(workspace, builtinSkillsDir) {
285
+ this.workspace = workspace;
286
+ this.workspaceSkills = join3(workspace, "skills");
287
+ this.builtinSkills = builtinSkillsDir ?? BUILTIN_SKILLS_DIR;
288
+ }
289
+ workspaceSkills;
290
+ builtinSkills;
291
+ listSkills(filterUnavailable = true) {
292
+ const skills = [];
293
+ if (existsSync4(this.workspaceSkills)) {
294
+ for (const entry of readdirSync3(this.workspaceSkills, { withFileTypes: true })) {
295
+ if (!entry.isDirectory()) {
296
+ continue;
297
+ }
298
+ const skillFile = join3(this.workspaceSkills, entry.name, "SKILL.md");
299
+ if (existsSync4(skillFile)) {
300
+ skills.push({ name: entry.name, path: skillFile, source: "workspace" });
301
+ }
302
+ }
303
+ }
304
+ if (existsSync4(this.builtinSkills)) {
305
+ for (const entry of readdirSync3(this.builtinSkills, { withFileTypes: true })) {
306
+ if (!entry.isDirectory()) {
307
+ continue;
308
+ }
309
+ const skillFile = join3(this.builtinSkills, entry.name, "SKILL.md");
310
+ if (existsSync4(skillFile) && !skills.some((s) => s.name === entry.name)) {
311
+ skills.push({ name: entry.name, path: skillFile, source: "builtin" });
312
+ }
313
+ }
314
+ }
315
+ if (filterUnavailable) {
316
+ return skills.filter((skill) => this.checkRequirements(this.getSkillMeta(skill.name)));
317
+ }
318
+ return skills;
319
+ }
320
+ loadSkill(name) {
321
+ const workspaceSkill = join3(this.workspaceSkills, name, "SKILL.md");
322
+ if (existsSync4(workspaceSkill)) {
323
+ return readFileSync3(workspaceSkill, "utf-8");
324
+ }
325
+ const builtinSkill = join3(this.builtinSkills, name, "SKILL.md");
326
+ if (existsSync4(builtinSkill)) {
327
+ return readFileSync3(builtinSkill, "utf-8");
328
+ }
329
+ return null;
330
+ }
331
+ loadSkillsForContext(skillNames) {
332
+ const parts = [];
333
+ for (const name of skillNames) {
334
+ const content = this.loadSkill(name);
335
+ if (content) {
336
+ parts.push(`### Skill: ${name}
337
+
338
+ ${this.stripFrontmatter(content)}`);
339
+ }
340
+ }
341
+ return parts.length ? parts.join("\n\n---\n\n") : "";
342
+ }
343
+ buildSkillsSummary() {
344
+ const allSkills = this.listSkills(false);
345
+ if (!allSkills.length) {
346
+ return "";
347
+ }
348
+ const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
349
+ const lines = ["<skills>"];
350
+ for (const skill of allSkills) {
351
+ const desc = escapeXml(this.getSkillDescription(skill.name));
352
+ const meta = this.getSkillMeta(skill.name);
353
+ const available = this.checkRequirements(meta);
354
+ lines.push(` <skill available="${available}">`);
355
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
356
+ lines.push(` <description>${desc}</description>`);
357
+ lines.push(` <location>${skill.path}</location>`);
358
+ if (!available) {
359
+ const missing = this.getMissingRequirements(meta);
360
+ if (missing) {
361
+ lines.push(` <requires>${escapeXml(missing)}</requires>`);
362
+ }
363
+ }
364
+ lines.push(" </skill>");
365
+ }
366
+ lines.push("</skills>");
367
+ return lines.join("\n");
368
+ }
369
+ getAlwaysSkills() {
370
+ const result = [];
371
+ for (const skill of this.listSkills(true)) {
372
+ const meta = this.getSkillMetadata(skill.name) ?? {};
373
+ const parsed = this.parseSkillMetadata(meta.metadata ?? "");
374
+ if (parsed.always || meta.always === "true") {
375
+ result.push(skill.name);
376
+ }
377
+ }
378
+ return result;
379
+ }
380
+ getSkillMetadata(name) {
381
+ const content = this.loadSkill(name);
382
+ if (!content || !content.startsWith("---")) {
383
+ return null;
384
+ }
385
+ const match = content.match(/^---\n(.*?)\n---/s);
386
+ if (!match) {
387
+ return null;
388
+ }
389
+ const metadata = {};
390
+ for (const line of match[1].split("\n")) {
391
+ const [key, ...rest] = line.split(":");
392
+ if (!key || rest.length === 0) {
393
+ continue;
394
+ }
395
+ metadata[key.trim()] = rest.join(":").trim().replace(/^['"]|['"]$/g, "");
396
+ }
397
+ return metadata;
398
+ }
399
+ getSkillDescription(name) {
400
+ const meta = this.getSkillMetadata(name);
401
+ if (meta?.description) {
402
+ return meta.description;
403
+ }
404
+ return name;
405
+ }
406
+ stripFrontmatter(content) {
407
+ if (content.startsWith("---")) {
408
+ const match = content.match(/^---\n.*?\n---\n/s);
409
+ if (match) {
410
+ return content.slice(match[0].length).trim();
411
+ }
412
+ }
413
+ return content;
414
+ }
415
+ parseSkillMetadata(raw) {
416
+ try {
417
+ const data = JSON.parse(raw);
418
+ if (typeof data !== "object" || !data) {
419
+ return {};
420
+ }
421
+ const meta = data[SKILL_METADATA_KEY];
422
+ if (typeof meta === "object" && meta) {
423
+ return meta;
424
+ }
425
+ return {};
426
+ } catch {
427
+ return {};
428
+ }
429
+ }
430
+ getSkillMeta(name) {
431
+ const meta = this.getSkillMetadata(name) ?? {};
432
+ return this.parseSkillMetadata(meta.metadata ?? "");
433
+ }
434
+ checkRequirements(skillMeta) {
435
+ const requires = skillMeta.requires ?? {};
436
+ if (requires.bins) {
437
+ for (const bin of requires.bins) {
438
+ if (!this.which(bin)) {
439
+ return false;
440
+ }
441
+ }
442
+ }
443
+ if (requires.env) {
444
+ for (const env of requires.env) {
445
+ if (!process.env[env]) {
446
+ return false;
447
+ }
448
+ }
449
+ }
450
+ return true;
451
+ }
452
+ getMissingRequirements(skillMeta) {
453
+ const missing = [];
454
+ const requires = skillMeta.requires ?? {};
455
+ if (requires.bins) {
456
+ for (const bin of requires.bins) {
457
+ if (!this.which(bin)) {
458
+ missing.push(`CLI: ${bin}`);
459
+ }
460
+ }
461
+ }
462
+ if (requires.env) {
463
+ for (const env of requires.env) {
464
+ if (!process.env[env]) {
465
+ missing.push(`ENV: ${env}`);
466
+ }
467
+ }
468
+ }
469
+ return missing.join(", ");
470
+ }
471
+ which(binary) {
472
+ const paths = (process.env.PATH ?? "").split(":");
473
+ for (const dir of paths) {
474
+ const full = join3(dir, binary);
475
+ if (existsSync4(full)) {
476
+ return true;
477
+ }
478
+ }
479
+ return false;
480
+ }
481
+ };
482
+
483
+ // src/agent/context.ts
484
+ var BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"];
485
+ var ContextBuilder = class {
486
+ constructor(workspace) {
487
+ this.workspace = workspace;
488
+ this.memory = new MemoryStore(workspace);
489
+ this.skills = new SkillsLoader(workspace, join4(fileURLToPath2(new URL("..", import.meta.url)), "skills"));
490
+ }
491
+ memory;
492
+ skills;
493
+ buildSystemPrompt(skillNames) {
494
+ const parts = [];
495
+ parts.push(this.getIdentity());
496
+ const bootstrap = this.loadBootstrapFiles();
497
+ if (bootstrap) {
498
+ parts.push(bootstrap);
499
+ }
500
+ const memory = this.memory.getMemoryContext();
501
+ if (memory) {
502
+ parts.push(`# Memory
503
+
504
+ ${memory}`);
505
+ }
506
+ const alwaysSkills = this.skills.getAlwaysSkills();
507
+ if (alwaysSkills.length) {
508
+ const alwaysContent = this.skills.loadSkillsForContext(alwaysSkills);
509
+ if (alwaysContent) {
510
+ parts.push(`# Active Skills
511
+
512
+ ${alwaysContent}`);
513
+ }
514
+ }
515
+ if (skillNames && skillNames.length) {
516
+ const requestedContent = this.skills.loadSkillsForContext(skillNames);
517
+ if (requestedContent) {
518
+ parts.push(`# Requested Skills
519
+
520
+ ${requestedContent}`);
521
+ }
522
+ }
523
+ const skillsSummary = this.skills.buildSkillsSummary();
524
+ if (skillsSummary) {
525
+ parts.push(`# Skills
526
+
527
+ The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
528
+ Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.
529
+
530
+ ${skillsSummary}`);
531
+ }
532
+ return parts.join("\n\n---\n\n");
533
+ }
534
+ buildMessages(params) {
535
+ const messages = [];
536
+ let systemPrompt = this.buildSystemPrompt(params.skillNames);
537
+ if (params.channel && params.chatId) {
538
+ systemPrompt += `
539
+
540
+ ## Current Session
541
+ Channel: ${params.channel}
542
+ Chat ID: ${params.chatId}`;
543
+ }
544
+ messages.push({ role: "system", content: systemPrompt });
545
+ messages.push(...params.history);
546
+ const userContent = this.buildUserContent(params.currentMessage, params.media ?? []);
547
+ messages.push({ role: "user", content: userContent });
548
+ return messages;
549
+ }
550
+ addToolResult(messages, toolCallId, toolName, result) {
551
+ messages.push({
552
+ role: "tool",
553
+ tool_call_id: toolCallId,
554
+ name: toolName,
555
+ content: result
556
+ });
557
+ return messages;
558
+ }
559
+ addAssistantMessage(messages, content, toolCalls, reasoningContent) {
560
+ const msg = { role: "assistant", content: content ?? "" };
561
+ if (toolCalls?.length) {
562
+ msg.tool_calls = toolCalls;
563
+ }
564
+ if (reasoningContent) {
565
+ msg.reasoning_content = reasoningContent;
566
+ }
567
+ messages.push(msg);
568
+ return messages;
569
+ }
570
+ getIdentity() {
571
+ const now = (/* @__PURE__ */ new Date()).toLocaleString();
572
+ return `# ${APP_NAME} \u{1F916}
573
+
574
+ You are ${APP_NAME}, a helpful AI assistant. You have access to tools that allow you to:
575
+ - Read, write, and edit files
576
+ - Execute shell commands
577
+ - Search the web and fetch web pages
578
+ - Send messages to users on chat channels
579
+ - Spawn subagents for complex background tasks
580
+
581
+ ## Current Time
582
+ ${now}
583
+
584
+ ## Runtime
585
+ ${process.platform} ${process.arch}, Node ${process.version}
586
+
587
+ ## Workspace
588
+ Your workspace is at: ${this.workspace}
589
+ - Memory files: ${this.workspace}/memory/MEMORY.md
590
+ - Daily notes: ${this.workspace}/memory/YYYY-MM-DD.md
591
+ - Custom skills: ${this.workspace}/skills/{skill-name}/SKILL.md
592
+
593
+ IMPORTANT: When responding to direct questions or conversations, reply directly with your text response.
594
+ Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp).
595
+ For normal conversation, just respond with text - do not call the message tool.
596
+
597
+ Always be helpful, accurate, and concise. When using tools, explain what you're doing.
598
+ When remembering something, write to ${this.workspace}/memory/MEMORY.md`;
599
+ }
600
+ loadBootstrapFiles() {
601
+ const parts = [];
602
+ for (const filename of BOOTSTRAP_FILES) {
603
+ const filePath = join4(this.workspace, filename);
604
+ if (existsSync5(filePath)) {
605
+ const content = readFileSync4(filePath, "utf-8");
606
+ parts.push(`## ${filename}
607
+
608
+ ${content}`);
609
+ }
610
+ }
611
+ return parts.join("\n\n");
612
+ }
613
+ buildUserContent(text, media) {
614
+ if (!media.length) {
615
+ return text;
616
+ }
617
+ const images = [];
618
+ for (const path of media) {
619
+ const mime = guessImageMime(path);
620
+ if (!mime) {
621
+ continue;
622
+ }
623
+ try {
624
+ const b64 = readFileSync4(path).toString("base64");
625
+ images.push({ type: "image_url", image_url: { url: `data:${mime};base64,${b64}` } });
626
+ } catch {
627
+ continue;
628
+ }
629
+ }
630
+ if (!images.length) {
631
+ return text;
632
+ }
633
+ return [...images, { type: "text", text }];
634
+ }
635
+ };
636
+ function guessImageMime(path) {
637
+ const ext = extname(path).toLowerCase();
638
+ if (ext === ".png") return "image/png";
639
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
640
+ if (ext === ".gif") return "image/gif";
641
+ if (ext === ".webp") return "image/webp";
642
+ return null;
643
+ }
644
+
645
+ // src/agent/tools/registry.ts
646
+ var ToolRegistry = class {
647
+ tools = /* @__PURE__ */ new Map();
648
+ register(tool) {
649
+ this.tools.set(tool.name, tool);
650
+ }
651
+ unregister(name) {
652
+ this.tools.delete(name);
653
+ }
654
+ get(name) {
655
+ return this.tools.get(name);
656
+ }
657
+ has(name) {
658
+ return this.tools.has(name);
659
+ }
660
+ getDefinitions() {
661
+ return Array.from(this.tools.values()).map((tool) => tool.toSchema());
662
+ }
663
+ async execute(name, params) {
664
+ const tool = this.tools.get(name);
665
+ if (!tool) {
666
+ return `Error: Tool '${name}' not found`;
667
+ }
668
+ try {
669
+ const errors = tool.validateParams(params);
670
+ if (errors.length) {
671
+ return `Error: Invalid parameters for tool '${name}': ${errors.join("; ")}`;
672
+ }
673
+ return await tool.execute(params);
674
+ } catch (err) {
675
+ return `Error executing ${name}: ${String(err)}`;
676
+ }
677
+ }
678
+ get toolNames() {
679
+ return Array.from(this.tools.keys());
680
+ }
681
+ };
682
+
683
+ // src/agent/tools/filesystem.ts
684
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync6, readdirSync as readdirSync4, statSync } from "fs";
685
+ import { resolve as resolve2, dirname } from "path";
686
+
687
+ // src/agent/tools/base.ts
688
+ var Tool = class _Tool {
689
+ static typeMap = {
690
+ string: (v) => typeof v === "string",
691
+ integer: (v) => typeof v === "number" && Number.isInteger(v),
692
+ number: (v) => typeof v === "number",
693
+ boolean: (v) => typeof v === "boolean",
694
+ array: Array.isArray,
695
+ object: (v) => typeof v === "object" && v !== null && !Array.isArray(v)
696
+ };
697
+ validateParams(params) {
698
+ const schema = this.parameters;
699
+ if (schema?.type !== "object") {
700
+ throw new Error(`Schema must be object type, got ${schema?.type ?? "unknown"}`);
701
+ }
702
+ return this.validateValue(params, schema, "");
703
+ }
704
+ toSchema() {
705
+ return {
706
+ type: "function",
707
+ function: {
708
+ name: this.name,
709
+ description: this.description,
710
+ parameters: this.parameters
711
+ }
712
+ };
713
+ }
714
+ validateValue(value, schema, path) {
715
+ const label = path || "parameter";
716
+ if (schema.type in _Tool.typeMap && !_Tool.typeMap[schema.type](value)) {
717
+ return [`${label} should be ${schema.type}`];
718
+ }
719
+ const errors = [];
720
+ const typedValue = value;
721
+ if (schema.enum && !schema.enum.includes(value)) {
722
+ errors.push(`${label} must be one of ${JSON.stringify(schema.enum)}`);
723
+ }
724
+ if (typeof value === "number") {
725
+ if (schema.minimum !== void 0 && value < schema.minimum) {
726
+ errors.push(`${label} must be >= ${schema.minimum}`);
727
+ }
728
+ if (schema.maximum !== void 0 && value > schema.maximum) {
729
+ errors.push(`${label} must be <= ${schema.maximum}`);
730
+ }
731
+ }
732
+ if (typeof value === "string") {
733
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
734
+ errors.push(`${label} must be at least ${schema.minLength} chars`);
735
+ }
736
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
737
+ errors.push(`${label} must be at most ${schema.maxLength} chars`);
738
+ }
739
+ }
740
+ if (schema.type === "object") {
741
+ for (const key of schema.required ?? []) {
742
+ if (!(key in typedValue)) {
743
+ errors.push(`missing required ${path ? `${path}.${key}` : key}`);
744
+ }
745
+ }
746
+ const properties = schema.properties ?? {};
747
+ for (const [key, val] of Object.entries(typedValue)) {
748
+ const propSchema = properties[key];
749
+ if (propSchema) {
750
+ errors.push(...this.validateValue(val, propSchema, path ? `${path}.${key}` : key));
751
+ }
752
+ }
753
+ }
754
+ if (schema.type === "array" && schema.items) {
755
+ value.forEach((item, index) => {
756
+ errors.push(...this.validateValue(item, schema.items, `${label}[${index}]`));
757
+ });
758
+ }
759
+ return errors;
760
+ }
761
+ };
762
+
763
+ // src/agent/tools/filesystem.ts
764
+ function resolvePath(path, allowedDir) {
765
+ const resolved = resolve2(path);
766
+ if (allowedDir) {
767
+ const allowed = resolve2(allowedDir);
768
+ if (!resolved.startsWith(allowed)) {
769
+ throw new Error("Access denied: path outside allowed directory");
770
+ }
771
+ }
772
+ return resolved;
773
+ }
774
+ var ReadFileTool = class extends Tool {
775
+ constructor(allowedDir) {
776
+ super();
777
+ this.allowedDir = allowedDir;
778
+ }
779
+ get name() {
780
+ return "read_file";
781
+ }
782
+ get description() {
783
+ return "Read a file from disk";
784
+ }
785
+ get parameters() {
786
+ return {
787
+ type: "object",
788
+ properties: {
789
+ path: { type: "string", description: "Path to the file" }
790
+ },
791
+ required: ["path"]
792
+ };
793
+ }
794
+ async execute(params) {
795
+ const path = resolvePath(String(params.path), this.allowedDir);
796
+ if (!existsSync6(path)) {
797
+ return `Error: File not found: ${path}`;
798
+ }
799
+ return readFileSync5(path, "utf-8");
800
+ }
801
+ };
802
+ var WriteFileTool = class extends Tool {
803
+ constructor(allowedDir) {
804
+ super();
805
+ this.allowedDir = allowedDir;
806
+ }
807
+ get name() {
808
+ return "write_file";
809
+ }
810
+ get description() {
811
+ return "Write content to a file";
812
+ }
813
+ get parameters() {
814
+ return {
815
+ type: "object",
816
+ properties: {
817
+ path: { type: "string", description: "Path to the file" },
818
+ content: { type: "string", description: "Content to write" }
819
+ },
820
+ required: ["path", "content"]
821
+ };
822
+ }
823
+ async execute(params) {
824
+ const path = resolvePath(String(params.path), this.allowedDir);
825
+ const content = String(params.content ?? "");
826
+ const dir = dirname(path);
827
+ if (!existsSync6(dir)) {
828
+ throw new Error("Directory does not exist");
829
+ }
830
+ writeFileSync3(path, content, "utf-8");
831
+ return `Wrote ${content.length} bytes to ${path}`;
832
+ }
833
+ };
834
+ var EditFileTool = class extends Tool {
835
+ constructor(allowedDir) {
836
+ super();
837
+ this.allowedDir = allowedDir;
838
+ }
839
+ get name() {
840
+ return "edit_file";
841
+ }
842
+ get description() {
843
+ return "Edit a file by replacing a string";
844
+ }
845
+ get parameters() {
846
+ return {
847
+ type: "object",
848
+ properties: {
849
+ path: { type: "string", description: "Path to the file" },
850
+ oldText: { type: "string", description: "Text to replace" },
851
+ newText: { type: "string", description: "Replacement text" }
852
+ },
853
+ required: ["path", "oldText", "newText"]
854
+ };
855
+ }
856
+ async execute(params) {
857
+ const path = resolvePath(String(params.path), this.allowedDir);
858
+ if (!existsSync6(path)) {
859
+ return `Error: File not found: ${path}`;
860
+ }
861
+ const oldText = String(params.oldText ?? "");
862
+ const newText = String(params.newText ?? "");
863
+ const content = readFileSync5(path, "utf-8");
864
+ if (!content.includes(oldText)) {
865
+ return "Error: Text to replace not found";
866
+ }
867
+ const updated = content.replace(oldText, newText);
868
+ writeFileSync3(path, updated, "utf-8");
869
+ return `Edited ${path}`;
870
+ }
871
+ };
872
+ var ListDirTool = class extends Tool {
873
+ constructor(allowedDir) {
874
+ super();
875
+ this.allowedDir = allowedDir;
876
+ }
877
+ get name() {
878
+ return "list_dir";
879
+ }
880
+ get description() {
881
+ return "List files in a directory";
882
+ }
883
+ get parameters() {
884
+ return {
885
+ type: "object",
886
+ properties: {
887
+ path: { type: "string", description: "Path to the directory" }
888
+ },
889
+ required: ["path"]
890
+ };
891
+ }
892
+ async execute(params) {
893
+ const path = resolvePath(String(params.path), this.allowedDir);
894
+ if (!existsSync6(path)) {
895
+ return `Error: Directory not found: ${path}`;
896
+ }
897
+ const entries = readdirSync4(path, { withFileTypes: true });
898
+ const lines = entries.map((entry) => {
899
+ const full = resolve2(path, entry.name);
900
+ const stats = statSync(full);
901
+ return `${entry.name}${entry.isDirectory() ? "/" : ""} (${stats.size} bytes)`;
902
+ });
903
+ return lines.join("\n") || "(empty)";
904
+ }
905
+ };
906
+
907
+ // src/agent/tools/shell.ts
908
+ import { exec } from "child_process";
909
+ import { promisify } from "util";
910
+ import { resolve as resolve3 } from "path";
911
+ var execAsync = promisify(exec);
912
+ var ExecTool = class extends Tool {
913
+ constructor(options = {}) {
914
+ super();
915
+ this.options = options;
916
+ this.denyPatterns = (options.denyPatterns ?? [
917
+ "\\brm\\s+-[rf]{1,2}\\b",
918
+ "\\bdel\\s+/[fq]\\b",
919
+ "\\brmdir\\s+/s\\b",
920
+ "\\b(format|mkfs|diskpart)\\b",
921
+ "\\bdd\\s+if=",
922
+ ">\\s*/dev/sd",
923
+ "\\b(shutdown|reboot|poweroff)\\b",
924
+ ":\\(\\)\\s*\\{.*\\};\\s*:"
925
+ ]).map((pattern) => new RegExp(pattern, "i"));
926
+ this.allowPatterns = (options.allowPatterns ?? []).map((pattern) => new RegExp(pattern, "i"));
927
+ }
928
+ denyPatterns;
929
+ allowPatterns;
930
+ get name() {
931
+ return "exec";
932
+ }
933
+ get description() {
934
+ return "Execute a shell command and return its output. Use with caution.";
935
+ }
936
+ get parameters() {
937
+ return {
938
+ type: "object",
939
+ properties: {
940
+ command: { type: "string", description: "The shell command to execute" },
941
+ workingDir: { type: "string", description: "Optional working directory for the command" }
942
+ },
943
+ required: ["command"]
944
+ };
945
+ }
946
+ async execute(params) {
947
+ const command = String(params.command ?? "");
948
+ const cwd = String(params.workingDir ?? this.options.workingDir ?? process.cwd());
949
+ const guardError = this.guardCommand(command, cwd);
950
+ if (guardError) {
951
+ return guardError;
952
+ }
953
+ try {
954
+ const { stdout, stderr } = await execAsync(command, {
955
+ cwd,
956
+ timeout: (this.options.timeout ?? 60) * 1e3,
957
+ maxBuffer: 1e7
958
+ });
959
+ const outputParts = [];
960
+ if (stdout) {
961
+ outputParts.push(stdout);
962
+ }
963
+ if (stderr?.trim()) {
964
+ outputParts.push(`STDERR:
965
+ ${stderr}`);
966
+ }
967
+ const result = outputParts.length ? outputParts.join("\n") : "(no output)";
968
+ return truncateOutput(result);
969
+ } catch (err) {
970
+ return `Error executing command: ${String(err)}`;
971
+ }
972
+ }
973
+ guardCommand(command, cwd) {
974
+ const normalized = command.trim().toLowerCase();
975
+ for (const pattern of this.denyPatterns) {
976
+ if (pattern.test(normalized)) {
977
+ return "Error: Command blocked by safety guard (dangerous pattern detected)";
978
+ }
979
+ }
980
+ if (this.allowPatterns.length && !this.allowPatterns.some((pattern) => pattern.test(normalized))) {
981
+ return "Error: Command blocked by safety guard (not in allowlist)";
982
+ }
983
+ if (this.options.restrictToWorkspace) {
984
+ if (command.includes("../") || command.includes("..\\")) {
985
+ return "Error: Command blocked by safety guard (path traversal detected)";
986
+ }
987
+ const cwdPath = resolve3(cwd);
988
+ const matches = [...command.matchAll(/(?:^|[\s|>])([^\s"'>]+)/g)].map((match) => match[1]);
989
+ for (const raw of matches) {
990
+ if (raw.startsWith("/") || /^[A-Za-z]:\\/.test(raw)) {
991
+ const resolved = resolve3(raw);
992
+ if (!resolved.startsWith(cwdPath)) {
993
+ return "Error: Command blocked by safety guard (path outside working dir)";
994
+ }
995
+ }
996
+ }
997
+ }
998
+ return null;
999
+ }
1000
+ };
1001
+ function truncateOutput(result, maxLen = 1e4) {
1002
+ if (result.length <= maxLen) {
1003
+ return result;
1004
+ }
1005
+ return `${result.slice(0, maxLen)}
1006
+ ... (truncated, ${result.length - maxLen} more chars)`;
1007
+ }
1008
+
1009
+ // src/agent/tools/web.ts
1010
+ import { fetch } from "undici";
1011
+ var WebSearchTool = class extends Tool {
1012
+ constructor(apiKey, maxResults = 5) {
1013
+ super();
1014
+ this.apiKey = apiKey;
1015
+ this.maxResults = maxResults;
1016
+ }
1017
+ get name() {
1018
+ return "web_search";
1019
+ }
1020
+ get description() {
1021
+ return "Search the web using Brave Search API";
1022
+ }
1023
+ get parameters() {
1024
+ return {
1025
+ type: "object",
1026
+ properties: {
1027
+ query: { type: "string", description: "Search query" },
1028
+ maxResults: { type: "integer", description: "Max results" }
1029
+ },
1030
+ required: ["query"]
1031
+ };
1032
+ }
1033
+ async execute(params) {
1034
+ if (!this.apiKey) {
1035
+ return "Error: Brave Search API key not configured";
1036
+ }
1037
+ const query = String(params.query ?? "");
1038
+ const maxResults = Number(params.maxResults ?? this.maxResults);
1039
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
1040
+ url.searchParams.set("q", query);
1041
+ url.searchParams.set("count", String(maxResults));
1042
+ const response = await fetch(url.toString(), {
1043
+ headers: {
1044
+ "Accept": "application/json",
1045
+ "X-Subscription-Token": this.apiKey
1046
+ }
1047
+ });
1048
+ if (!response.ok) {
1049
+ return `Error: Brave Search request failed (${response.status})`;
1050
+ }
1051
+ const data = await response.json();
1052
+ const results = data.web?.results ?? [];
1053
+ if (!results.length) {
1054
+ return "No results found.";
1055
+ }
1056
+ const lines = results.map((item) => {
1057
+ const title = item.title ?? "";
1058
+ const urlValue = item.url ?? "";
1059
+ const description = item.description ?? "";
1060
+ return `- ${title}
1061
+ ${urlValue}
1062
+ ${description}`;
1063
+ });
1064
+ return lines.join("\n\n");
1065
+ }
1066
+ };
1067
+ var WebFetchTool = class extends Tool {
1068
+ get name() {
1069
+ return "web_fetch";
1070
+ }
1071
+ get description() {
1072
+ return "Fetch the contents of a web page";
1073
+ }
1074
+ get parameters() {
1075
+ return {
1076
+ type: "object",
1077
+ properties: {
1078
+ url: { type: "string", description: "URL to fetch" }
1079
+ },
1080
+ required: ["url"]
1081
+ };
1082
+ }
1083
+ async execute(params) {
1084
+ const url = String(params.url ?? "");
1085
+ const response = await fetch(url, { headers: { "User-Agent": APP_USER_AGENT } });
1086
+ if (!response.ok) {
1087
+ return `Error: Fetch failed (${response.status})`;
1088
+ }
1089
+ const text = await response.text();
1090
+ return text.slice(0, 12e3);
1091
+ }
1092
+ };
1093
+
1094
+ // src/agent/tools/message.ts
1095
+ var MessageTool = class extends Tool {
1096
+ constructor(sendCallback) {
1097
+ super();
1098
+ this.sendCallback = sendCallback;
1099
+ }
1100
+ channel = "cli";
1101
+ chatId = "direct";
1102
+ get name() {
1103
+ return "message";
1104
+ }
1105
+ get description() {
1106
+ return "Send a message to a chat channel";
1107
+ }
1108
+ get parameters() {
1109
+ return {
1110
+ type: "object",
1111
+ properties: {
1112
+ content: { type: "string", description: "Message to send" },
1113
+ channel: { type: "string", description: "Channel name" },
1114
+ chatId: { type: "string", description: "Chat ID" }
1115
+ },
1116
+ required: ["content"]
1117
+ };
1118
+ }
1119
+ setContext(channel, chatId) {
1120
+ this.channel = channel;
1121
+ this.chatId = chatId;
1122
+ }
1123
+ async execute(params) {
1124
+ const content = String(params.content ?? "");
1125
+ const channel = String(params.channel ?? this.channel);
1126
+ const chatId = String(params.chatId ?? this.chatId);
1127
+ await this.sendCallback({
1128
+ channel,
1129
+ chatId,
1130
+ content,
1131
+ media: [],
1132
+ metadata: {}
1133
+ });
1134
+ return `Message sent to ${channel}:${chatId}`;
1135
+ }
1136
+ };
1137
+
1138
+ // src/agent/tools/spawn.ts
1139
+ var SpawnTool = class extends Tool {
1140
+ constructor(manager) {
1141
+ super();
1142
+ this.manager = manager;
1143
+ }
1144
+ channel = "cli";
1145
+ chatId = "direct";
1146
+ get name() {
1147
+ return "spawn";
1148
+ }
1149
+ get description() {
1150
+ return "Spawn a background subagent to handle a task";
1151
+ }
1152
+ get parameters() {
1153
+ return {
1154
+ type: "object",
1155
+ properties: {
1156
+ task: { type: "string", description: "Task for the subagent" },
1157
+ label: { type: "string", description: "Optional label" }
1158
+ },
1159
+ required: ["task"]
1160
+ };
1161
+ }
1162
+ setContext(channel, chatId) {
1163
+ this.channel = channel;
1164
+ this.chatId = chatId;
1165
+ }
1166
+ async execute(params) {
1167
+ const task = String(params.task ?? "");
1168
+ const label = params.label ? String(params.label) : void 0;
1169
+ return this.manager.spawn({
1170
+ task,
1171
+ label,
1172
+ originChannel: this.channel,
1173
+ originChatId: this.chatId
1174
+ });
1175
+ }
1176
+ };
1177
+
1178
+ // src/agent/tools/cron.ts
1179
+ var CronTool = class extends Tool {
1180
+ constructor(cronService) {
1181
+ super();
1182
+ this.cronService = cronService;
1183
+ }
1184
+ channel = "cli";
1185
+ chatId = "direct";
1186
+ get name() {
1187
+ return "cron";
1188
+ }
1189
+ get description() {
1190
+ return "Schedule a task to run later";
1191
+ }
1192
+ get parameters() {
1193
+ return {
1194
+ type: "object",
1195
+ properties: {
1196
+ name: { type: "string" },
1197
+ message: { type: "string" },
1198
+ every: { type: "integer" },
1199
+ cron: { type: "string" },
1200
+ at: { type: "string" },
1201
+ deliver: { type: "boolean" }
1202
+ },
1203
+ required: ["name", "message"]
1204
+ };
1205
+ }
1206
+ setContext(channel, chatId) {
1207
+ this.channel = channel;
1208
+ this.chatId = chatId;
1209
+ }
1210
+ async execute(params) {
1211
+ const name = String(params.name ?? "");
1212
+ const message = String(params.message ?? "");
1213
+ const every = params.every ? Number(params.every) : void 0;
1214
+ const cron = params.cron ? String(params.cron) : void 0;
1215
+ const at = params.at ? String(params.at) : void 0;
1216
+ const deliver = Boolean(params.deliver ?? false);
1217
+ let schedule = null;
1218
+ if (every) {
1219
+ schedule = { kind: "every", everyMs: every * 1e3 };
1220
+ } else if (cron) {
1221
+ schedule = { kind: "cron", expr: cron };
1222
+ } else if (at) {
1223
+ const atMs = Date.parse(at);
1224
+ schedule = { kind: "at", atMs };
1225
+ }
1226
+ if (!schedule) {
1227
+ return "Error: Must specify --every, --cron, or --at";
1228
+ }
1229
+ const job = this.cronService.addJob({
1230
+ name,
1231
+ schedule,
1232
+ message,
1233
+ deliver,
1234
+ channel: this.channel,
1235
+ to: this.chatId
1236
+ });
1237
+ return `Scheduled job '${job.name}' (${job.id})`;
1238
+ }
1239
+ };
1240
+
1241
+ // src/agent/subagent.ts
1242
+ import { randomUUID } from "crypto";
1243
+ var SubagentManager = class {
1244
+ constructor(options) {
1245
+ this.options = options;
1246
+ }
1247
+ runningTasks = /* @__PURE__ */ new Map();
1248
+ async spawn(params) {
1249
+ const taskId = randomUUID().slice(0, 8);
1250
+ const displayLabel = params.label ?? `${params.task.slice(0, 30)}${params.task.length > 30 ? "..." : ""}`;
1251
+ const origin = {
1252
+ channel: params.originChannel ?? "cli",
1253
+ chatId: params.originChatId ?? "direct"
1254
+ };
1255
+ const background = this.runSubagent({
1256
+ taskId,
1257
+ task: params.task,
1258
+ label: displayLabel,
1259
+ origin
1260
+ });
1261
+ this.runningTasks.set(taskId, background);
1262
+ background.finally(() => this.runningTasks.delete(taskId));
1263
+ return `Subagent [${displayLabel}] started (id: ${taskId}). I'll notify you when it completes.`;
1264
+ }
1265
+ async runSubagent(params) {
1266
+ try {
1267
+ const tools = new ToolRegistry();
1268
+ const allowedDir = this.options.restrictToWorkspace ? this.options.workspace : void 0;
1269
+ tools.register(new ReadFileTool(allowedDir));
1270
+ tools.register(new WriteFileTool(allowedDir));
1271
+ tools.register(new ListDirTool(allowedDir));
1272
+ tools.register(
1273
+ new ExecTool({
1274
+ workingDir: this.options.workspace,
1275
+ timeout: this.options.execConfig?.timeout ?? 60,
1276
+ restrictToWorkspace: this.options.restrictToWorkspace ?? false
1277
+ })
1278
+ );
1279
+ tools.register(new WebSearchTool(this.options.braveApiKey ?? void 0));
1280
+ tools.register(new WebFetchTool());
1281
+ const systemPrompt = this.buildSubagentPrompt(params.task);
1282
+ const messages = [
1283
+ { role: "system", content: systemPrompt },
1284
+ { role: "user", content: params.task }
1285
+ ];
1286
+ let iteration = 0;
1287
+ let finalResult = null;
1288
+ while (iteration < 15) {
1289
+ iteration += 1;
1290
+ const response = await this.options.provider.chat({
1291
+ messages,
1292
+ tools: tools.getDefinitions(),
1293
+ model: this.options.model
1294
+ });
1295
+ if (response.toolCalls.length) {
1296
+ const toolCalls = response.toolCalls.map((call) => ({
1297
+ id: call.id,
1298
+ type: "function",
1299
+ function: {
1300
+ name: call.name,
1301
+ arguments: JSON.stringify(call.arguments)
1302
+ }
1303
+ }));
1304
+ messages.push({ role: "assistant", content: response.content ?? "", tool_calls: toolCalls });
1305
+ for (const call of response.toolCalls) {
1306
+ const result = await tools.execute(call.name, call.arguments);
1307
+ messages.push({ role: "tool", tool_call_id: call.id, name: call.name, content: result });
1308
+ }
1309
+ } else {
1310
+ finalResult = response.content ?? "";
1311
+ break;
1312
+ }
1313
+ }
1314
+ if (!finalResult) {
1315
+ finalResult = "Task completed but no final response was generated.";
1316
+ }
1317
+ await this.announceResult({
1318
+ label: params.label,
1319
+ task: params.task,
1320
+ result: finalResult,
1321
+ origin: params.origin,
1322
+ status: "ok"
1323
+ });
1324
+ } catch (err) {
1325
+ await this.announceResult({
1326
+ label: params.label,
1327
+ task: params.task,
1328
+ result: `Error: ${String(err)}`,
1329
+ origin: params.origin,
1330
+ status: "error"
1331
+ });
1332
+ }
1333
+ }
1334
+ async announceResult(params) {
1335
+ const statusText = params.status === "ok" ? "completed successfully" : "failed";
1336
+ const announceContent = `[Subagent '${params.label}' ${statusText}]
1337
+
1338
+ Task: ${params.task}
1339
+
1340
+ Result:
1341
+ ${params.result}
1342
+
1343
+ Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like "subagent" or task IDs.`;
1344
+ const msg = {
1345
+ channel: "system",
1346
+ senderId: "subagent",
1347
+ chatId: `${params.origin.channel}:${params.origin.chatId}`,
1348
+ content: announceContent,
1349
+ timestamp: /* @__PURE__ */ new Date(),
1350
+ media: [],
1351
+ metadata: {}
1352
+ };
1353
+ await this.options.bus.publishInbound(msg);
1354
+ }
1355
+ buildSubagentPrompt(task) {
1356
+ return `# Subagent
1357
+
1358
+ You are a subagent spawned by the main agent to complete a specific task.
1359
+
1360
+ ## Your Task
1361
+ ${task}
1362
+
1363
+ ## Rules
1364
+ 1. Stay focused - complete only the assigned task, nothing else
1365
+ 2. Your final response will be reported back to the main agent
1366
+ 3. Do not initiate conversations or take on side tasks
1367
+ 4. Be concise but informative in your findings
1368
+
1369
+ ## What You Can Do
1370
+ - Read and write files in the workspace
1371
+ - Execute shell commands
1372
+ - Search the web and fetch web pages
1373
+ - Complete the task thoroughly
1374
+
1375
+ ## What You Cannot Do
1376
+ - Send messages directly to users (no message tool available)
1377
+ - Spawn other subagents
1378
+ - Access the main agent's conversation history
1379
+
1380
+ ## Workspace
1381
+ Your workspace is at: ${this.options.workspace}
1382
+
1383
+ When you have completed the task, provide a clear summary of your findings or actions.`;
1384
+ }
1385
+ getRunningCount() {
1386
+ return this.runningTasks.size;
1387
+ }
1388
+ };
1389
+
1390
+ // src/agent/loop.ts
1391
+ var AgentLoop = class {
1392
+ constructor(options) {
1393
+ this.options = options;
1394
+ this.context = new ContextBuilder(options.workspace);
1395
+ this.sessions = options.sessionManager ?? new SessionManager(options.workspace);
1396
+ this.tools = new ToolRegistry();
1397
+ this.subagents = new SubagentManager({
1398
+ provider: options.provider,
1399
+ workspace: options.workspace,
1400
+ bus: options.bus,
1401
+ model: options.model ?? options.provider.getDefaultModel(),
1402
+ braveApiKey: options.braveApiKey ?? void 0,
1403
+ execConfig: options.execConfig ?? { timeout: 60 },
1404
+ restrictToWorkspace: options.restrictToWorkspace ?? false
1405
+ });
1406
+ this.registerDefaultTools();
1407
+ }
1408
+ context;
1409
+ sessions;
1410
+ tools;
1411
+ subagents;
1412
+ running = false;
1413
+ registerDefaultTools() {
1414
+ const allowedDir = this.options.restrictToWorkspace ? this.options.workspace : void 0;
1415
+ this.tools.register(new ReadFileTool(allowedDir));
1416
+ this.tools.register(new WriteFileTool(allowedDir));
1417
+ this.tools.register(new EditFileTool(allowedDir));
1418
+ this.tools.register(new ListDirTool(allowedDir));
1419
+ this.tools.register(
1420
+ new ExecTool({
1421
+ workingDir: this.options.workspace,
1422
+ timeout: this.options.execConfig?.timeout ?? 60,
1423
+ restrictToWorkspace: this.options.restrictToWorkspace ?? false
1424
+ })
1425
+ );
1426
+ this.tools.register(new WebSearchTool(this.options.braveApiKey ?? void 0));
1427
+ this.tools.register(new WebFetchTool());
1428
+ const messageTool = new MessageTool((msg) => this.options.bus.publishOutbound(msg));
1429
+ this.tools.register(messageTool);
1430
+ const spawnTool = new SpawnTool(this.subagents);
1431
+ this.tools.register(spawnTool);
1432
+ if (this.options.cronService) {
1433
+ const cronTool = new CronTool(this.options.cronService);
1434
+ this.tools.register(cronTool);
1435
+ }
1436
+ }
1437
+ async run() {
1438
+ this.running = true;
1439
+ while (this.running) {
1440
+ const msg = await this.options.bus.consumeInbound();
1441
+ try {
1442
+ const response = await this.processMessage(msg);
1443
+ if (response) {
1444
+ await this.options.bus.publishOutbound(response);
1445
+ }
1446
+ } catch (err) {
1447
+ await this.options.bus.publishOutbound({
1448
+ channel: msg.channel,
1449
+ chatId: msg.chatId,
1450
+ content: `Sorry, I encountered an error: ${String(err)}`,
1451
+ media: [],
1452
+ metadata: {}
1453
+ });
1454
+ }
1455
+ }
1456
+ }
1457
+ stop() {
1458
+ this.running = false;
1459
+ }
1460
+ async processDirect(params) {
1461
+ const msg = {
1462
+ channel: params.channel ?? "cli",
1463
+ senderId: "user",
1464
+ chatId: params.chatId ?? "direct",
1465
+ content: params.content,
1466
+ timestamp: /* @__PURE__ */ new Date(),
1467
+ media: [],
1468
+ metadata: {}
1469
+ };
1470
+ const response = await this.processMessage(msg, params.sessionKey);
1471
+ return response?.content ?? "";
1472
+ }
1473
+ async processMessage(msg, sessionKeyOverride) {
1474
+ if (msg.channel === "system") {
1475
+ return this.processSystemMessage(msg);
1476
+ }
1477
+ const sessionKey = sessionKeyOverride ?? `${msg.channel}:${msg.chatId}`;
1478
+ const session = this.sessions.getOrCreate(sessionKey);
1479
+ const messageTool = this.tools.get("message");
1480
+ if (messageTool instanceof MessageTool) {
1481
+ messageTool.setContext(msg.channel, msg.chatId);
1482
+ }
1483
+ const spawnTool = this.tools.get("spawn");
1484
+ if (spawnTool instanceof SpawnTool) {
1485
+ spawnTool.setContext(msg.channel, msg.chatId);
1486
+ }
1487
+ const cronTool = this.tools.get("cron");
1488
+ if (cronTool instanceof CronTool) {
1489
+ cronTool.setContext(msg.channel, msg.chatId);
1490
+ }
1491
+ const messages = this.context.buildMessages({
1492
+ history: this.sessions.getHistory(session),
1493
+ currentMessage: msg.content,
1494
+ media: msg.media,
1495
+ channel: msg.channel,
1496
+ chatId: msg.chatId
1497
+ });
1498
+ let iteration = 0;
1499
+ let finalContent = null;
1500
+ const maxIterations = this.options.maxIterations ?? 20;
1501
+ while (iteration < maxIterations) {
1502
+ iteration += 1;
1503
+ const response = await this.options.provider.chat({
1504
+ messages,
1505
+ tools: this.tools.getDefinitions(),
1506
+ model: this.options.model ?? void 0
1507
+ });
1508
+ if (response.toolCalls.length) {
1509
+ const toolCallDicts = response.toolCalls.map((call) => ({
1510
+ id: call.id,
1511
+ type: "function",
1512
+ function: {
1513
+ name: call.name,
1514
+ arguments: JSON.stringify(call.arguments)
1515
+ }
1516
+ }));
1517
+ this.context.addAssistantMessage(messages, response.content, toolCallDicts, response.reasoningContent ?? null);
1518
+ for (const call of response.toolCalls) {
1519
+ const result = await this.tools.execute(call.name, call.arguments);
1520
+ this.context.addToolResult(messages, call.id, call.name, result);
1521
+ }
1522
+ } else {
1523
+ finalContent = response.content;
1524
+ break;
1525
+ }
1526
+ }
1527
+ if (!finalContent) {
1528
+ finalContent = "I've completed processing but have no response to give.";
1529
+ }
1530
+ this.sessions.addMessage(session, "user", msg.content);
1531
+ this.sessions.addMessage(session, "assistant", finalContent);
1532
+ this.sessions.save(session);
1533
+ return {
1534
+ channel: msg.channel,
1535
+ chatId: msg.chatId,
1536
+ content: finalContent,
1537
+ media: [],
1538
+ metadata: msg.metadata ?? {}
1539
+ };
1540
+ }
1541
+ async processSystemMessage(msg) {
1542
+ const [originChannel, originChatId] = msg.chatId.includes(":") ? msg.chatId.split(":", 2) : ["cli", msg.chatId];
1543
+ const sessionKey = `${originChannel}:${originChatId}`;
1544
+ const session = this.sessions.getOrCreate(sessionKey);
1545
+ const messageTool = this.tools.get("message");
1546
+ if (messageTool instanceof MessageTool) {
1547
+ messageTool.setContext(originChannel, originChatId);
1548
+ }
1549
+ const spawnTool = this.tools.get("spawn");
1550
+ if (spawnTool instanceof SpawnTool) {
1551
+ spawnTool.setContext(originChannel, originChatId);
1552
+ }
1553
+ const cronTool = this.tools.get("cron");
1554
+ if (cronTool instanceof CronTool) {
1555
+ cronTool.setContext(originChannel, originChatId);
1556
+ }
1557
+ const messages = this.context.buildMessages({
1558
+ history: this.sessions.getHistory(session),
1559
+ currentMessage: msg.content,
1560
+ channel: originChannel,
1561
+ chatId: originChatId
1562
+ });
1563
+ let iteration = 0;
1564
+ let finalContent = null;
1565
+ const maxIterations = this.options.maxIterations ?? 20;
1566
+ while (iteration < maxIterations) {
1567
+ iteration += 1;
1568
+ const response = await this.options.provider.chat({
1569
+ messages,
1570
+ tools: this.tools.getDefinitions(),
1571
+ model: this.options.model ?? void 0
1572
+ });
1573
+ if (response.toolCalls.length) {
1574
+ const toolCallDicts = response.toolCalls.map((call) => ({
1575
+ id: call.id,
1576
+ type: "function",
1577
+ function: {
1578
+ name: call.name,
1579
+ arguments: JSON.stringify(call.arguments)
1580
+ }
1581
+ }));
1582
+ this.context.addAssistantMessage(messages, response.content, toolCallDicts, response.reasoningContent ?? null);
1583
+ for (const call of response.toolCalls) {
1584
+ const result = await this.tools.execute(call.name, call.arguments);
1585
+ this.context.addToolResult(messages, call.id, call.name, result);
1586
+ }
1587
+ } else {
1588
+ finalContent = response.content;
1589
+ break;
1590
+ }
1591
+ }
1592
+ if (!finalContent) {
1593
+ finalContent = "Background task completed.";
1594
+ }
1595
+ this.sessions.addMessage(session, "user", `[System: ${msg.senderId}] ${msg.content}`);
1596
+ this.sessions.addMessage(session, "assistant", finalContent);
1597
+ this.sessions.save(session);
1598
+ return {
1599
+ channel: originChannel,
1600
+ chatId: originChatId,
1601
+ content: finalContent,
1602
+ media: [],
1603
+ metadata: {}
1604
+ };
1605
+ }
1606
+ };
1607
+
1608
+ // src/providers/registry.ts
1609
+ var PROVIDERS = [
1610
+ {
1611
+ name: "openrouter",
1612
+ keywords: ["openrouter"],
1613
+ envKey: "OPENROUTER_API_KEY",
1614
+ displayName: "OpenRouter",
1615
+ litellmPrefix: "openrouter",
1616
+ skipPrefixes: [],
1617
+ envExtras: [],
1618
+ isGateway: true,
1619
+ isLocal: false,
1620
+ detectByKeyPrefix: "sk-or-",
1621
+ detectByBaseKeyword: "openrouter",
1622
+ defaultApiBase: "https://openrouter.ai/api/v1",
1623
+ stripModelPrefix: false,
1624
+ modelOverrides: []
1625
+ },
1626
+ {
1627
+ name: "aihubmix",
1628
+ keywords: ["aihubmix"],
1629
+ envKey: "OPENAI_API_KEY",
1630
+ displayName: "AiHubMix",
1631
+ litellmPrefix: "openai",
1632
+ skipPrefixes: [],
1633
+ envExtras: [],
1634
+ isGateway: true,
1635
+ isLocal: false,
1636
+ detectByKeyPrefix: "",
1637
+ detectByBaseKeyword: "aihubmix",
1638
+ defaultApiBase: "https://aihubmix.com/v1",
1639
+ stripModelPrefix: true,
1640
+ modelOverrides: []
1641
+ },
1642
+ {
1643
+ name: "anthropic",
1644
+ keywords: ["anthropic", "claude"],
1645
+ envKey: "ANTHROPIC_API_KEY",
1646
+ displayName: "Anthropic",
1647
+ litellmPrefix: "",
1648
+ skipPrefixes: [],
1649
+ envExtras: [],
1650
+ isGateway: false,
1651
+ isLocal: false,
1652
+ detectByKeyPrefix: "",
1653
+ detectByBaseKeyword: "",
1654
+ defaultApiBase: "",
1655
+ stripModelPrefix: false,
1656
+ modelOverrides: []
1657
+ },
1658
+ {
1659
+ name: "openai",
1660
+ keywords: ["openai", "gpt"],
1661
+ envKey: "OPENAI_API_KEY",
1662
+ displayName: "OpenAI",
1663
+ litellmPrefix: "",
1664
+ skipPrefixes: [],
1665
+ envExtras: [],
1666
+ isGateway: false,
1667
+ isLocal: false,
1668
+ detectByKeyPrefix: "",
1669
+ detectByBaseKeyword: "",
1670
+ defaultApiBase: "",
1671
+ stripModelPrefix: false,
1672
+ modelOverrides: []
1673
+ },
1674
+ {
1675
+ name: "deepseek",
1676
+ keywords: ["deepseek"],
1677
+ envKey: "DEEPSEEK_API_KEY",
1678
+ displayName: "DeepSeek",
1679
+ litellmPrefix: "deepseek",
1680
+ skipPrefixes: ["deepseek/"],
1681
+ envExtras: [],
1682
+ isGateway: false,
1683
+ isLocal: false,
1684
+ detectByKeyPrefix: "",
1685
+ detectByBaseKeyword: "",
1686
+ defaultApiBase: "",
1687
+ stripModelPrefix: false,
1688
+ modelOverrides: []
1689
+ },
1690
+ {
1691
+ name: "gemini",
1692
+ keywords: ["gemini"],
1693
+ envKey: "GEMINI_API_KEY",
1694
+ displayName: "Gemini",
1695
+ litellmPrefix: "gemini",
1696
+ skipPrefixes: ["gemini/"],
1697
+ envExtras: [],
1698
+ isGateway: false,
1699
+ isLocal: false,
1700
+ detectByKeyPrefix: "",
1701
+ detectByBaseKeyword: "",
1702
+ defaultApiBase: "",
1703
+ stripModelPrefix: false,
1704
+ modelOverrides: []
1705
+ },
1706
+ {
1707
+ name: "zhipu",
1708
+ keywords: ["zhipu", "glm", "zai"],
1709
+ envKey: "ZAI_API_KEY",
1710
+ displayName: "Zhipu AI",
1711
+ litellmPrefix: "zai",
1712
+ skipPrefixes: ["zhipu/", "zai/", "openrouter/", "hosted_vllm/"],
1713
+ envExtras: [["ZHIPUAI_API_KEY", "{api_key}"]],
1714
+ isGateway: false,
1715
+ isLocal: false,
1716
+ detectByKeyPrefix: "",
1717
+ detectByBaseKeyword: "",
1718
+ defaultApiBase: "",
1719
+ stripModelPrefix: false,
1720
+ modelOverrides: []
1721
+ },
1722
+ {
1723
+ name: "dashscope",
1724
+ keywords: ["qwen", "dashscope"],
1725
+ envKey: "DASHSCOPE_API_KEY",
1726
+ displayName: "DashScope",
1727
+ litellmPrefix: "dashscope",
1728
+ skipPrefixes: ["dashscope/", "openrouter/"],
1729
+ envExtras: [],
1730
+ isGateway: false,
1731
+ isLocal: false,
1732
+ detectByKeyPrefix: "",
1733
+ detectByBaseKeyword: "",
1734
+ defaultApiBase: "",
1735
+ stripModelPrefix: false,
1736
+ modelOverrides: []
1737
+ },
1738
+ {
1739
+ name: "moonshot",
1740
+ keywords: ["moonshot", "kimi"],
1741
+ envKey: "MOONSHOT_API_KEY",
1742
+ displayName: "Moonshot",
1743
+ litellmPrefix: "moonshot",
1744
+ skipPrefixes: ["moonshot/", "openrouter/"],
1745
+ envExtras: [["MOONSHOT_API_BASE", "{api_base}"]],
1746
+ isGateway: false,
1747
+ isLocal: false,
1748
+ detectByKeyPrefix: "",
1749
+ detectByBaseKeyword: "",
1750
+ defaultApiBase: "https://api.moonshot.ai/v1",
1751
+ stripModelPrefix: false,
1752
+ modelOverrides: [["kimi-k2.5", { temperature: 1 }]]
1753
+ },
1754
+ {
1755
+ name: "minimax",
1756
+ keywords: ["minimax"],
1757
+ envKey: "MINIMAX_API_KEY",
1758
+ displayName: "MiniMax",
1759
+ litellmPrefix: "minimax",
1760
+ skipPrefixes: ["minimax/", "openrouter/"],
1761
+ envExtras: [],
1762
+ isGateway: false,
1763
+ isLocal: false,
1764
+ detectByKeyPrefix: "",
1765
+ detectByBaseKeyword: "",
1766
+ defaultApiBase: "https://api.minimax.io/v1",
1767
+ stripModelPrefix: false,
1768
+ modelOverrides: []
1769
+ },
1770
+ {
1771
+ name: "vllm",
1772
+ keywords: ["vllm"],
1773
+ envKey: "HOSTED_VLLM_API_KEY",
1774
+ displayName: "vLLM/Local",
1775
+ litellmPrefix: "hosted_vllm",
1776
+ skipPrefixes: [],
1777
+ envExtras: [],
1778
+ isGateway: false,
1779
+ isLocal: true,
1780
+ detectByKeyPrefix: "",
1781
+ detectByBaseKeyword: "",
1782
+ defaultApiBase: "",
1783
+ stripModelPrefix: false,
1784
+ modelOverrides: []
1785
+ },
1786
+ {
1787
+ name: "groq",
1788
+ keywords: ["groq"],
1789
+ envKey: "GROQ_API_KEY",
1790
+ displayName: "Groq",
1791
+ litellmPrefix: "groq",
1792
+ skipPrefixes: ["groq/"],
1793
+ envExtras: [],
1794
+ isGateway: false,
1795
+ isLocal: false,
1796
+ detectByKeyPrefix: "",
1797
+ detectByBaseKeyword: "",
1798
+ defaultApiBase: "",
1799
+ stripModelPrefix: false,
1800
+ modelOverrides: []
1801
+ }
1802
+ ];
1803
+ function findProviderByName(name) {
1804
+ return PROVIDERS.find((spec) => spec.name === name);
1805
+ }
1806
+ function findProviderByModel(model) {
1807
+ const modelLower = model.toLowerCase();
1808
+ return PROVIDERS.find((spec) => {
1809
+ if (spec.isGateway || spec.isLocal) {
1810
+ return false;
1811
+ }
1812
+ return spec.keywords.some((keyword) => modelLower.includes(keyword));
1813
+ });
1814
+ }
1815
+ function findGateway(providerName, apiKey, apiBase) {
1816
+ if (providerName) {
1817
+ const spec = findProviderByName(providerName);
1818
+ if (spec && (spec.isGateway || spec.isLocal)) {
1819
+ return spec;
1820
+ }
1821
+ }
1822
+ for (const spec of PROVIDERS) {
1823
+ if (spec.detectByKeyPrefix && apiKey && apiKey.startsWith(spec.detectByKeyPrefix)) {
1824
+ return spec;
1825
+ }
1826
+ if (spec.detectByBaseKeyword && apiBase && apiBase.includes(spec.detectByBaseKeyword)) {
1827
+ return spec;
1828
+ }
1829
+ }
1830
+ return void 0;
1831
+ }
1832
+ function providerLabel(spec) {
1833
+ return spec.displayName || spec.name;
1834
+ }
1835
+
1836
+ // src/config/schema.ts
1837
+ import { z } from "zod";
1838
+ var allowFrom = z.array(z.string()).default([]);
1839
+ var WhatsAppConfigSchema = z.object({
1840
+ enabled: z.boolean().default(false),
1841
+ bridgeUrl: z.string().default("ws://localhost:3001"),
1842
+ allowFrom
1843
+ });
1844
+ var TelegramConfigSchema = z.object({
1845
+ enabled: z.boolean().default(false),
1846
+ token: z.string().default(""),
1847
+ allowFrom,
1848
+ proxy: z.string().nullable().default(null)
1849
+ });
1850
+ var FeishuConfigSchema = z.object({
1851
+ enabled: z.boolean().default(false),
1852
+ appId: z.string().default(""),
1853
+ appSecret: z.string().default(""),
1854
+ encryptKey: z.string().default(""),
1855
+ verificationToken: z.string().default(""),
1856
+ allowFrom
1857
+ });
1858
+ var DingTalkConfigSchema = z.object({
1859
+ enabled: z.boolean().default(false),
1860
+ clientId: z.string().default(""),
1861
+ clientSecret: z.string().default(""),
1862
+ allowFrom
1863
+ });
1864
+ var DiscordConfigSchema = z.object({
1865
+ enabled: z.boolean().default(false),
1866
+ token: z.string().default(""),
1867
+ allowFrom,
1868
+ gatewayUrl: z.string().default("wss://gateway.discord.gg/?v=10&encoding=json"),
1869
+ intents: z.number().int().default(37377)
1870
+ });
1871
+ var EmailConfigSchema = z.object({
1872
+ enabled: z.boolean().default(false),
1873
+ consentGranted: z.boolean().default(false),
1874
+ imapHost: z.string().default(""),
1875
+ imapPort: z.number().int().default(993),
1876
+ imapUsername: z.string().default(""),
1877
+ imapPassword: z.string().default(""),
1878
+ imapMailbox: z.string().default("INBOX"),
1879
+ imapUseSsl: z.boolean().default(true),
1880
+ smtpHost: z.string().default(""),
1881
+ smtpPort: z.number().int().default(587),
1882
+ smtpUsername: z.string().default(""),
1883
+ smtpPassword: z.string().default(""),
1884
+ smtpUseTls: z.boolean().default(true),
1885
+ smtpUseSsl: z.boolean().default(false),
1886
+ fromAddress: z.string().default(""),
1887
+ autoReplyEnabled: z.boolean().default(true),
1888
+ pollIntervalSeconds: z.number().int().default(30),
1889
+ markSeen: z.boolean().default(true),
1890
+ maxBodyChars: z.number().int().default(12e3),
1891
+ subjectPrefix: z.string().default("Re: "),
1892
+ allowFrom
1893
+ });
1894
+ var MochatMentionSchema = z.object({
1895
+ requireInGroups: z.boolean().default(false)
1896
+ });
1897
+ var MochatGroupRuleSchema = z.object({
1898
+ requireMention: z.boolean().default(false)
1899
+ });
1900
+ var MochatConfigSchema = z.object({
1901
+ enabled: z.boolean().default(false),
1902
+ baseUrl: z.string().default("https://mochat.io"),
1903
+ socketUrl: z.string().default(""),
1904
+ socketPath: z.string().default("/socket.io"),
1905
+ socketDisableMsgpack: z.boolean().default(false),
1906
+ socketReconnectDelayMs: z.number().int().default(1e3),
1907
+ socketMaxReconnectDelayMs: z.number().int().default(1e4),
1908
+ socketConnectTimeoutMs: z.number().int().default(1e4),
1909
+ refreshIntervalMs: z.number().int().default(3e4),
1910
+ watchTimeoutMs: z.number().int().default(25e3),
1911
+ watchLimit: z.number().int().default(100),
1912
+ retryDelayMs: z.number().int().default(500),
1913
+ maxRetryAttempts: z.number().int().default(0),
1914
+ clawToken: z.string().default(""),
1915
+ agentUserId: z.string().default(""),
1916
+ sessions: z.array(z.string()).default([]),
1917
+ panels: z.array(z.string()).default([]),
1918
+ allowFrom,
1919
+ mention: MochatMentionSchema.default({}),
1920
+ groups: z.record(MochatGroupRuleSchema).default({}),
1921
+ replyDelayMode: z.string().default("non-mention"),
1922
+ replyDelayMs: z.number().int().default(12e4)
1923
+ });
1924
+ var SlackDMSchema = z.object({
1925
+ enabled: z.boolean().default(true),
1926
+ policy: z.string().default("open"),
1927
+ allowFrom
1928
+ });
1929
+ var SlackConfigSchema = z.object({
1930
+ enabled: z.boolean().default(false),
1931
+ mode: z.string().default("socket"),
1932
+ webhookPath: z.string().default("/slack/events"),
1933
+ botToken: z.string().default(""),
1934
+ appToken: z.string().default(""),
1935
+ userTokenReadOnly: z.boolean().default(true),
1936
+ groupPolicy: z.string().default("mention"),
1937
+ groupAllowFrom: allowFrom,
1938
+ dm: SlackDMSchema.default({})
1939
+ });
1940
+ var QQConfigSchema = z.object({
1941
+ enabled: z.boolean().default(false),
1942
+ appId: z.string().default(""),
1943
+ secret: z.string().default(""),
1944
+ allowFrom
1945
+ });
1946
+ var ChannelsConfigSchema = z.object({
1947
+ whatsapp: WhatsAppConfigSchema.default({}),
1948
+ telegram: TelegramConfigSchema.default({}),
1949
+ discord: DiscordConfigSchema.default({}),
1950
+ feishu: FeishuConfigSchema.default({}),
1951
+ mochat: MochatConfigSchema.default({}),
1952
+ dingtalk: DingTalkConfigSchema.default({}),
1953
+ email: EmailConfigSchema.default({}),
1954
+ slack: SlackConfigSchema.default({}),
1955
+ qq: QQConfigSchema.default({})
1956
+ });
1957
+ var AgentDefaultsSchema = z.object({
1958
+ workspace: z.string().default(DEFAULT_WORKSPACE_PATH),
1959
+ model: z.string().default("anthropic/claude-opus-4-5"),
1960
+ maxTokens: z.number().int().default(8192),
1961
+ temperature: z.number().default(0.7),
1962
+ maxToolIterations: z.number().int().default(20)
1963
+ });
1964
+ var AgentsConfigSchema = z.object({
1965
+ defaults: AgentDefaultsSchema.default({})
1966
+ });
1967
+ var ProviderConfigSchema = z.object({
1968
+ apiKey: z.string().default(""),
1969
+ apiBase: z.string().nullable().default(null),
1970
+ extraHeaders: z.record(z.string()).nullable().default(null)
1971
+ });
1972
+ var ProvidersConfigSchema = z.object({
1973
+ anthropic: ProviderConfigSchema.default({}),
1974
+ openai: ProviderConfigSchema.default({}),
1975
+ openrouter: ProviderConfigSchema.default({}),
1976
+ deepseek: ProviderConfigSchema.default({}),
1977
+ groq: ProviderConfigSchema.default({}),
1978
+ zhipu: ProviderConfigSchema.default({}),
1979
+ dashscope: ProviderConfigSchema.default({}),
1980
+ vllm: ProviderConfigSchema.default({}),
1981
+ gemini: ProviderConfigSchema.default({}),
1982
+ moonshot: ProviderConfigSchema.default({}),
1983
+ minimax: ProviderConfigSchema.default({}),
1984
+ aihubmix: ProviderConfigSchema.default({})
1985
+ });
1986
+ var GatewayConfigSchema = z.object({
1987
+ host: z.string().default("0.0.0.0"),
1988
+ port: z.number().int().default(18790)
1989
+ });
1990
+ var UiConfigSchema = z.object({
1991
+ enabled: z.boolean().default(false),
1992
+ host: z.string().default("127.0.0.1"),
1993
+ port: z.number().int().default(18791),
1994
+ open: z.boolean().default(false)
1995
+ });
1996
+ var WebSearchConfigSchema = z.object({
1997
+ apiKey: z.string().default(""),
1998
+ maxResults: z.number().int().default(5)
1999
+ });
2000
+ var WebToolsConfigSchema = z.object({
2001
+ search: WebSearchConfigSchema.default({})
2002
+ });
2003
+ var ExecToolConfigSchema = z.object({
2004
+ timeout: z.number().int().default(60)
2005
+ });
2006
+ var ToolsConfigSchema = z.object({
2007
+ web: WebToolsConfigSchema.default({}),
2008
+ exec: ExecToolConfigSchema.default({}),
2009
+ restrictToWorkspace: z.boolean().default(false)
2010
+ });
2011
+ var ConfigSchema = z.object({
2012
+ agents: AgentsConfigSchema.default({}),
2013
+ channels: ChannelsConfigSchema.default({}),
2014
+ providers: ProvidersConfigSchema.default({}),
2015
+ gateway: GatewayConfigSchema.default({}),
2016
+ ui: UiConfigSchema.default({}),
2017
+ tools: ToolsConfigSchema.default({})
2018
+ });
2019
+ function getWorkspacePathFromConfig(config) {
2020
+ return expandHome(config.agents.defaults.workspace);
2021
+ }
2022
+ function matchProvider(config, model) {
2023
+ const modelLower = (model ?? config.agents.defaults.model).toLowerCase();
2024
+ for (const spec of PROVIDERS) {
2025
+ const provider = config.providers[spec.name];
2026
+ if (provider && provider.apiKey && spec.keywords.some((kw) => modelLower.includes(kw))) {
2027
+ return { provider, name: spec.name };
2028
+ }
2029
+ }
2030
+ for (const spec of PROVIDERS) {
2031
+ const provider = config.providers[spec.name];
2032
+ if (provider && provider.apiKey) {
2033
+ return { provider, name: spec.name };
2034
+ }
2035
+ }
2036
+ return { provider: null, name: null };
2037
+ }
2038
+ function getProvider(config, model) {
2039
+ return matchProvider(config, model).provider;
2040
+ }
2041
+ function getProviderName(config, model) {
2042
+ return matchProvider(config, model).name;
2043
+ }
2044
+ function getApiKey(config, model) {
2045
+ const provider = getProvider(config, model);
2046
+ return provider?.apiKey ?? null;
2047
+ }
2048
+ function getApiBase(config, model) {
2049
+ const { provider, name } = matchProvider(config, model);
2050
+ if (provider?.apiBase) {
2051
+ return provider.apiBase;
2052
+ }
2053
+ if (name) {
2054
+ const spec = findProviderByName(name);
2055
+ if (spec?.isGateway && spec.defaultApiBase) {
2056
+ return spec.defaultApiBase;
2057
+ }
2058
+ }
2059
+ return null;
2060
+ }
2061
+
2062
+ // src/config/loader.ts
2063
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
2064
+ import { resolve as resolve4 } from "path";
2065
+ import { z as z2 } from "zod";
2066
+ function getConfigPath() {
2067
+ return resolve4(getDataPath(), "config.json");
2068
+ }
2069
+ function getDataDir() {
2070
+ return getDataPath();
2071
+ }
2072
+ function loadConfig(configPath) {
2073
+ const path = configPath ?? getConfigPath();
2074
+ if (existsSync7(path)) {
2075
+ try {
2076
+ const raw = readFileSync6(path, "utf-8");
2077
+ const data = JSON.parse(raw);
2078
+ const migrated = migrateConfig(data);
2079
+ return ConfigSchema.parse(migrated);
2080
+ } catch (err) {
2081
+ const message = err instanceof z2.ZodError ? err.message : String(err);
2082
+ console.warn(`Warning: Failed to load config from ${path}: ${message}`);
2083
+ }
2084
+ }
2085
+ return ConfigSchema.parse({});
2086
+ }
2087
+ function saveConfig(config, configPath) {
2088
+ const path = configPath ?? getConfigPath();
2089
+ mkdirSync2(resolve4(path, ".."), { recursive: true });
2090
+ writeFileSync4(path, JSON.stringify(config, null, 2));
2091
+ }
2092
+ function migrateConfig(data) {
2093
+ const tools = data.tools ?? {};
2094
+ const execConfig = tools.exec ?? {};
2095
+ if (execConfig.restrictToWorkspace !== void 0 && tools.restrictToWorkspace === void 0) {
2096
+ tools.restrictToWorkspace = execConfig.restrictToWorkspace;
2097
+ }
2098
+ return { ...data, tools };
2099
+ }
2100
+
2101
+ // src/providers/base.ts
2102
+ var LLMProvider = class {
2103
+ apiKey;
2104
+ apiBase;
2105
+ constructor(apiKey, apiBase) {
2106
+ this.apiKey = apiKey ?? void 0;
2107
+ this.apiBase = apiBase ?? void 0;
2108
+ }
2109
+ };
2110
+
2111
+ // src/providers/openai_provider.ts
2112
+ import OpenAI from "openai";
2113
+ var OpenAICompatibleProvider = class extends LLMProvider {
2114
+ client;
2115
+ defaultModel;
2116
+ extraHeaders;
2117
+ constructor(options) {
2118
+ super(options.apiKey, options.apiBase);
2119
+ this.defaultModel = options.defaultModel;
2120
+ this.extraHeaders = options.extraHeaders ?? null;
2121
+ this.client = new OpenAI({
2122
+ apiKey: options.apiKey ?? void 0,
2123
+ baseURL: options.apiBase ?? void 0,
2124
+ defaultHeaders: options.extraHeaders ?? void 0
2125
+ });
2126
+ }
2127
+ getDefaultModel() {
2128
+ return this.defaultModel;
2129
+ }
2130
+ async chat(params) {
2131
+ const model = params.model ?? this.defaultModel;
2132
+ const temperature = params.temperature ?? 0.7;
2133
+ const maxTokens = params.maxTokens ?? 4096;
2134
+ const response = await this.client.chat.completions.create({
2135
+ model,
2136
+ messages: params.messages,
2137
+ tools: params.tools,
2138
+ tool_choice: params.tools?.length ? "auto" : void 0,
2139
+ temperature,
2140
+ max_tokens: maxTokens
2141
+ });
2142
+ const choice = response.choices[0];
2143
+ const message = choice?.message;
2144
+ const toolCalls = [];
2145
+ if (message?.tool_calls) {
2146
+ for (const toolCall of message.tool_calls) {
2147
+ if (toolCall.type !== "function") {
2148
+ continue;
2149
+ }
2150
+ let args = {};
2151
+ try {
2152
+ args = JSON.parse(toolCall.function.arguments ?? "{}");
2153
+ } catch {
2154
+ args = {};
2155
+ }
2156
+ toolCalls.push({
2157
+ id: toolCall.id,
2158
+ name: toolCall.function.name,
2159
+ arguments: args
2160
+ });
2161
+ }
2162
+ }
2163
+ const reasoningContent = message?.reasoning_content ?? message?.reasoning ?? null;
2164
+ return {
2165
+ content: message?.content ?? null,
2166
+ toolCalls,
2167
+ finishReason: choice?.finish_reason ?? "stop",
2168
+ usage: {
2169
+ prompt_tokens: response.usage?.prompt_tokens ?? 0,
2170
+ completion_tokens: response.usage?.completion_tokens ?? 0,
2171
+ total_tokens: response.usage?.total_tokens ?? 0
2172
+ },
2173
+ reasoningContent
2174
+ };
2175
+ }
2176
+ };
2177
+
2178
+ // src/providers/litellm_provider.ts
2179
+ var LiteLLMProvider = class extends LLMProvider {
2180
+ defaultModel;
2181
+ extraHeaders;
2182
+ providerName;
2183
+ gatewaySpec;
2184
+ client;
2185
+ constructor(options) {
2186
+ super(options.apiKey, options.apiBase);
2187
+ this.defaultModel = options.defaultModel;
2188
+ this.extraHeaders = options.extraHeaders ?? null;
2189
+ this.providerName = options.providerName ?? null;
2190
+ this.gatewaySpec = findGateway(this.providerName, options.apiKey ?? null, options.apiBase ?? null) ?? void 0;
2191
+ this.client = new OpenAICompatibleProvider({
2192
+ apiKey: options.apiKey ?? null,
2193
+ apiBase: options.apiBase ?? null,
2194
+ defaultModel: options.defaultModel,
2195
+ extraHeaders: options.extraHeaders ?? null
2196
+ });
2197
+ }
2198
+ getDefaultModel() {
2199
+ return this.defaultModel;
2200
+ }
2201
+ async chat(params) {
2202
+ const requestedModel = params.model ?? this.defaultModel;
2203
+ const resolvedModel = this.resolveModel(requestedModel);
2204
+ const apiModel = this.stripRoutingPrefix(resolvedModel);
2205
+ const temperature = params.temperature ?? 0.7;
2206
+ const maxTokens = params.maxTokens ?? 4096;
2207
+ const overrides = this.applyModelOverrides(apiModel, { temperature, maxTokens });
2208
+ return this.client.chat({
2209
+ messages: params.messages,
2210
+ tools: params.tools,
2211
+ model: apiModel,
2212
+ temperature: overrides.temperature,
2213
+ maxTokens: overrides.maxTokens
2214
+ });
2215
+ }
2216
+ resolveModel(model) {
2217
+ if (this.gatewaySpec) {
2218
+ let resolved = model;
2219
+ if (this.gatewaySpec.stripModelPrefix && resolved.includes("/")) {
2220
+ resolved = resolved.split("/").slice(-1)[0];
2221
+ }
2222
+ const prefix = this.gatewaySpec.litellmPrefix ?? "";
2223
+ if (prefix && !resolved.startsWith(`${prefix}/`)) {
2224
+ resolved = `${prefix}/${resolved}`;
2225
+ }
2226
+ return resolved;
2227
+ }
2228
+ const spec = this.getStandardSpec(model);
2229
+ if (!spec) {
2230
+ return model;
2231
+ }
2232
+ if (spec.litellmPrefix) {
2233
+ const skipPrefixes = spec.skipPrefixes ?? [];
2234
+ if (!skipPrefixes.some((prefix) => model.startsWith(prefix))) {
2235
+ return `${spec.litellmPrefix}/${model}`;
2236
+ }
2237
+ }
2238
+ return model;
2239
+ }
2240
+ stripRoutingPrefix(model) {
2241
+ if (this.gatewaySpec) {
2242
+ return model;
2243
+ }
2244
+ const spec = this.getStandardSpec(model);
2245
+ if (!spec?.litellmPrefix) {
2246
+ return model;
2247
+ }
2248
+ const prefix = `${spec.litellmPrefix}/`;
2249
+ if (model.startsWith(prefix)) {
2250
+ return model.slice(prefix.length);
2251
+ }
2252
+ return model;
2253
+ }
2254
+ applyModelOverrides(model, params) {
2255
+ const spec = this.getStandardSpec(model);
2256
+ if (!spec?.modelOverrides?.length) {
2257
+ return params;
2258
+ }
2259
+ const match = spec.modelOverrides.find(([pattern]) => model.toLowerCase().includes(pattern));
2260
+ if (!match) {
2261
+ return params;
2262
+ }
2263
+ const overrides = match[1];
2264
+ return {
2265
+ temperature: typeof overrides.temperature === "number" ? overrides.temperature : params.temperature,
2266
+ maxTokens: typeof overrides.max_tokens === "number" ? overrides.max_tokens : params.maxTokens
2267
+ };
2268
+ }
2269
+ getStandardSpec(model) {
2270
+ return findProviderByModel(model) ?? (this.providerName ? findProviderByName(this.providerName) : void 0);
2271
+ }
2272
+ };
2273
+
2274
+ export {
2275
+ APP_NAME,
2276
+ APP_TAGLINE,
2277
+ APP_TITLE,
2278
+ APP_REPLY_SUBJECT,
2279
+ getDataPath,
2280
+ getWorkspacePath,
2281
+ SessionManager,
2282
+ AgentLoop,
2283
+ PROVIDERS,
2284
+ findProviderByName,
2285
+ findProviderByModel,
2286
+ findGateway,
2287
+ providerLabel,
2288
+ WhatsAppConfigSchema,
2289
+ TelegramConfigSchema,
2290
+ FeishuConfigSchema,
2291
+ DingTalkConfigSchema,
2292
+ DiscordConfigSchema,
2293
+ EmailConfigSchema,
2294
+ MochatMentionSchema,
2295
+ MochatGroupRuleSchema,
2296
+ MochatConfigSchema,
2297
+ SlackDMSchema,
2298
+ SlackConfigSchema,
2299
+ QQConfigSchema,
2300
+ ChannelsConfigSchema,
2301
+ AgentDefaultsSchema,
2302
+ AgentsConfigSchema,
2303
+ ProviderConfigSchema,
2304
+ ProvidersConfigSchema,
2305
+ GatewayConfigSchema,
2306
+ UiConfigSchema,
2307
+ WebSearchConfigSchema,
2308
+ WebToolsConfigSchema,
2309
+ ExecToolConfigSchema,
2310
+ ToolsConfigSchema,
2311
+ ConfigSchema,
2312
+ getWorkspacePathFromConfig,
2313
+ matchProvider,
2314
+ getProvider,
2315
+ getProviderName,
2316
+ getApiKey,
2317
+ getApiBase,
2318
+ getConfigPath,
2319
+ getDataDir,
2320
+ loadConfig,
2321
+ saveConfig,
2322
+ LLMProvider,
2323
+ LiteLLMProvider
2324
+ };