daemora 1.0.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 (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Browser Automation — Playwright-based web interaction.
3
+ * Upgraded: multi-tab, navigation guard, dialog handling, waitFor, cookies.
4
+ */
5
+
6
+ let browser = null;
7
+ let browserContext = null;
8
+ const pages = []; // Multi-tab support
9
+
10
+ // Blocked navigation patterns — SSRF / security guard
11
+ const NAV_BLOCKLIST = [
12
+ /^file:\/\//i,
13
+ /^(http:\/\/|https:\/\/)(127\.|0\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.)/,
14
+ /^(http:\/\/|https:\/\/)localhost/i,
15
+ ];
16
+
17
+ function isBlockedUrl(url) {
18
+ return NAV_BLOCKLIST.some((pattern) => pattern.test(url));
19
+ }
20
+
21
+ async function ensureBrowser() {
22
+ if (browser && browser.isConnected()) {
23
+ if (pages.length === 0 || pages[0].isClosed()) {
24
+ pages[0] = await browserContext.newPage();
25
+ pages[0].setDefaultTimeout(15000);
26
+ }
27
+ return pages[0];
28
+ }
29
+
30
+ try {
31
+ const { chromium } = await import("playwright");
32
+ browser = await chromium.launch({ headless: true });
33
+ browserContext = await browser.newContext({
34
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
35
+ viewport: { width: 1280, height: 720 },
36
+ });
37
+
38
+ // Auto-handle dialogs (accept by default)
39
+ browserContext.on("dialog", async (dialog) => {
40
+ console.log(` [browser] Auto-dismissed dialog: ${dialog.type()} — "${dialog.message().slice(0, 80)}"`);
41
+ await dialog.dismiss();
42
+ });
43
+
44
+ const page = await browserContext.newPage();
45
+ page.setDefaultTimeout(15000);
46
+ pages.push(page);
47
+ return pages[0];
48
+ } catch (error) {
49
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.message.includes("playwright")) {
50
+ throw new Error("Playwright not installed. Run: pnpm add playwright && npx playwright install chromium");
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ function currentPage() {
57
+ if (pages.length === 0) throw new Error("No browser open. Use navigate first.");
58
+ const activePage = pages[pages.length - 1];
59
+ if (activePage.isClosed()) throw new Error("Current page is closed. Navigate to a URL first.");
60
+ return activePage;
61
+ }
62
+
63
+ export async function browserAction(action, param1, param2) {
64
+ console.log(` [browser] ${action}: ${param1 || ""}`);
65
+
66
+ try {
67
+ switch (action) {
68
+ case "navigate":
69
+ case "openPage": {
70
+ if (!param1) return "Error: URL is required.";
71
+ if (isBlockedUrl(param1)) return `Error: Navigation to "${param1}" is blocked for security (private/local addresses not allowed).`;
72
+ const p = await ensureBrowser();
73
+ await p.goto(param1, { waitUntil: "domcontentloaded" });
74
+ const title = await p.title();
75
+ return `Navigated to: ${param1}\nTitle: ${title}`;
76
+ }
77
+
78
+ case "click": {
79
+ if (!param1) return "Error: selector is required.";
80
+ await currentPage().click(param1);
81
+ return `Clicked: ${param1}`;
82
+ }
83
+
84
+ case "fill": {
85
+ if (!param1 || param2 === undefined) return "Error: selector and value are required.";
86
+ await currentPage().fill(param1, param2);
87
+ return `Filled "${param1}" with "${param2}"`;
88
+ }
89
+
90
+ case "getText": {
91
+ const selector = param1 || "body";
92
+ const text = await currentPage().textContent(selector);
93
+ const trimmed = (text || "").trim().slice(0, 10000);
94
+ return trimmed || "(empty)";
95
+ }
96
+
97
+ case "screenshot": {
98
+ const p = await ensureBrowser();
99
+ const path = param1 || `/tmp/screenshot-${Date.now()}.png`;
100
+ await p.screenshot({ path, fullPage: param2 === "full" });
101
+ return `Screenshot saved to: ${path}`;
102
+ }
103
+
104
+ case "evaluate": {
105
+ if (!param1) return "Error: JavaScript expression is required.";
106
+ const result = await currentPage().evaluate(param1);
107
+ return JSON.stringify(result, null, 2);
108
+ }
109
+
110
+ case "getLinks": {
111
+ const links = await currentPage().evaluate(() =>
112
+ Array.from(document.querySelectorAll("a[href]"))
113
+ .slice(0, 50)
114
+ .map((a) => ({ text: a.textContent.trim().slice(0, 80), href: a.href }))
115
+ );
116
+ return links.map((l) => `${l.text} → ${l.href}`).join("\n") || "(no links found)";
117
+ }
118
+
119
+ // Multi-tab support
120
+ case "newTab": {
121
+ const url = param1;
122
+ if (url && isBlockedUrl(url)) return `Error: Navigation to "${url}" is blocked.`;
123
+ if (!browserContext) await ensureBrowser();
124
+ const newPage = await browserContext.newPage();
125
+ newPage.setDefaultTimeout(15000);
126
+ pages.push(newPage);
127
+ if (url) {
128
+ await newPage.goto(url, { waitUntil: "domcontentloaded" });
129
+ return `Opened new tab (${pages.length - 1}) at: ${url}`;
130
+ }
131
+ return `Opened new blank tab (index: ${pages.length - 1})`;
132
+ }
133
+
134
+ case "switchTab": {
135
+ const idx = parseInt(param1 || "0");
136
+ if (isNaN(idx) || idx < 0 || idx >= pages.length) {
137
+ return `Error: Tab index ${idx} out of range. Open tabs: 0–${pages.length - 1}`;
138
+ }
139
+ // Bring to focus (Playwright doesn't have focus concept, but we track active)
140
+ pages.push(pages.splice(idx, 1)[0]); // Move selected to end (= current)
141
+ return `Switched to tab ${idx} (now active)`;
142
+ }
143
+
144
+ case "listTabs": {
145
+ if (pages.length === 0) return "No open tabs.";
146
+ const titles = await Promise.all(
147
+ pages.map(async (p, i) => {
148
+ if (p.isClosed()) return ` ${i}: [closed]`;
149
+ const title = await p.title().catch(() => "?");
150
+ const url = p.url();
151
+ return ` ${i}${i === pages.length - 1 ? " (active)" : ""}: ${title} — ${url}`;
152
+ })
153
+ );
154
+ return `Open tabs (${pages.length}):\n${titles.join("\n")}`;
155
+ }
156
+
157
+ case "closeTab": {
158
+ const idx = parseInt(param1 || `${pages.length - 1}`);
159
+ if (idx < 0 || idx >= pages.length) return `Error: Tab index ${idx} out of range.`;
160
+ await pages[idx].close();
161
+ pages.splice(idx, 1);
162
+ return `Closed tab ${idx}. Open tabs: ${pages.length}`;
163
+ }
164
+
165
+ // Waiting
166
+ case "waitFor": {
167
+ if (!param1) return "Error: selector is required.";
168
+ const timeout = param2 ? parseInt(param2) : 10000;
169
+ await currentPage().waitForSelector(param1, { timeout });
170
+ return `Element "${param1}" found.`;
171
+ }
172
+
173
+ // Dialog handling
174
+ case "handleDialog": {
175
+ // Override default dismiss behavior for next dialog
176
+ const action = param1 || "accept"; // accept | dismiss
177
+ const text = param2 || "";
178
+ currentPage().once("dialog", async (dialog) => {
179
+ if (action === "accept") await dialog.accept(text);
180
+ else await dialog.dismiss();
181
+ });
182
+ return `Next dialog will be ${action}ed${text ? ` with text: "${text}"` : ""}.`;
183
+ }
184
+
185
+ // Cookies
186
+ case "getCookies": {
187
+ if (!browserContext) return "No browser open.";
188
+ const cookies = await browserContext.cookies();
189
+ const filtered = param1
190
+ ? cookies.filter((c) => c.domain.includes(param1))
191
+ : cookies;
192
+ return JSON.stringify(filtered.slice(0, 20), null, 2);
193
+ }
194
+
195
+ case "setCookie": {
196
+ if (!param1) return "Error: cookie JSON is required (e.g., {\"name\":\"token\",\"value\":\"abc\",\"domain\":\"example.com\"}).";
197
+ if (!browserContext) await ensureBrowser();
198
+ const cookie = JSON.parse(param1);
199
+ await browserContext.addCookies([cookie]);
200
+ return `Cookie "${cookie.name}" set.`;
201
+ }
202
+
203
+ case "close": {
204
+ if (browser) {
205
+ await browser.close();
206
+ browser = null;
207
+ browserContext = null;
208
+ pages.length = 0;
209
+ }
210
+ return "Browser closed.";
211
+ }
212
+
213
+ default:
214
+ return `Unknown action: "${action}". Available: navigate, click, fill, getText, screenshot, evaluate, getLinks, newTab, switchTab, listTabs, closeTab, waitFor, handleDialog, getCookies, setCookie, close`;
215
+ }
216
+ } catch (error) {
217
+ console.log(` [browser] Error: ${error.message}`);
218
+ return `Browser error: ${error.message}`;
219
+ }
220
+ }
221
+
222
+ export const browserActionDescription =
223
+ 'browserAction(action: string, param1?: string, param2?: string) - Browser automation via Playwright. Actions: navigate(url), click(selector), fill(selector,value), getText(selector), screenshot(path,full?), evaluate(js), getLinks, newTab(url?), switchTab(index), listTabs, closeTab(index), waitFor(selector,timeoutMs?), handleDialog(accept|dismiss,text?), getCookies(domain?), setCookie(json), close.';
@@ -0,0 +1,265 @@
1
+ import { writeFileSync, mkdirSync, readFileSync } from "fs";
2
+ import { dirname } from "path";
3
+
4
+ /**
5
+ * Create Document — creates Markdown, text, PDF, or DOCX documents.
6
+ * Upgraded: better PDF rendering (bold, italic, code, tables, numbered lists),
7
+ * optional DOCX support via 'docx' package.
8
+ */
9
+
10
+ export async function createDocument(filePath, content, format) {
11
+ const fmt = (format || "markdown").toLowerCase();
12
+ console.log(` [createDocument] Creating ${fmt}: ${filePath}`);
13
+
14
+ if (!filePath || !content) {
15
+ return "Error: filePath and content are required.";
16
+ }
17
+
18
+ try {
19
+ mkdirSync(dirname(filePath), { recursive: true });
20
+
21
+ if (fmt === "pdf") {
22
+ return await createPDF(filePath, content);
23
+ }
24
+
25
+ if (fmt === "docx") {
26
+ return await createDOCX(filePath, content);
27
+ }
28
+
29
+ // Markdown / text / html — write as-is
30
+ writeFileSync(filePath, content, "utf-8");
31
+ console.log(` [createDocument] Written: ${filePath} (${content.length} chars)`);
32
+ return `Document created: ${filePath} (${content.length} characters)`;
33
+ } catch (error) {
34
+ console.log(` [createDocument] Failed: ${error.message}`);
35
+ return `Failed to create document: ${error.message}`;
36
+ }
37
+ }
38
+
39
+ // ─── PDF ──────────────────────────────────────────────────────────────────────
40
+
41
+ async function createPDF(filePath, content) {
42
+ try {
43
+ const PDFDocument = (await import("pdfkit")).default;
44
+
45
+ return new Promise((resolve, reject) => {
46
+ const doc = new PDFDocument({ margin: 50 });
47
+ const chunks = [];
48
+
49
+ doc.on("data", (chunk) => chunks.push(chunk));
50
+ doc.on("end", () => {
51
+ const buffer = Buffer.concat(chunks);
52
+ writeFileSync(filePath, buffer);
53
+ console.log(` [createDocument] PDF written: ${filePath} (${buffer.length} bytes)`);
54
+ resolve(`PDF created: ${filePath} (${buffer.length} bytes)`);
55
+ });
56
+ doc.on("error", reject);
57
+
58
+ renderMarkdownToPDF(doc, content);
59
+ doc.end();
60
+ });
61
+ } catch (error) {
62
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.message.includes("pdfkit")) {
63
+ return "PDF requires pdfkit. Install with: pnpm add pdfkit";
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ function renderMarkdownToPDF(doc, content) {
70
+ const lines = content.split("\n");
71
+ let inCodeBlock = false;
72
+ let codeLines = [];
73
+ let tableBuffer = [];
74
+
75
+ function flushTable() {
76
+ if (tableBuffer.length === 0) return;
77
+ // Simple table: pipe-delimited
78
+ for (const row of tableBuffer) {
79
+ const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
80
+ if (cells.length === 0) continue;
81
+ if (cells.every((c) => /^[-:]+$/.test(c))) continue; // separator row
82
+ doc.fontSize(10).font("Courier").text(cells.join(" "), { paragraphGap: 2 });
83
+ }
84
+ tableBuffer = [];
85
+ doc.moveDown(0.3);
86
+ }
87
+
88
+ function flushCode() {
89
+ if (codeLines.length === 0) return;
90
+ doc.fontSize(9).font("Courier")
91
+ .fillColor("#333333")
92
+ .text(codeLines.join("\n"), { lineGap: 2, paragraphGap: 6 });
93
+ doc.fillColor("black");
94
+ codeLines = [];
95
+ }
96
+
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i];
99
+
100
+ // Code block toggle
101
+ if (line.startsWith("```")) {
102
+ if (inCodeBlock) {
103
+ flushCode();
104
+ inCodeBlock = false;
105
+ } else {
106
+ flushTable();
107
+ inCodeBlock = true;
108
+ }
109
+ continue;
110
+ }
111
+
112
+ if (inCodeBlock) {
113
+ codeLines.push(line);
114
+ continue;
115
+ }
116
+
117
+ // Table rows
118
+ if (line.startsWith("|")) {
119
+ tableBuffer.push(line);
120
+ continue;
121
+ } else if (tableBuffer.length > 0) {
122
+ flushTable();
123
+ }
124
+
125
+ // Headings
126
+ if (line.startsWith("# ")) {
127
+ doc.fontSize(24).font("Helvetica-Bold").fillColor("#000000")
128
+ .text(line.slice(2), { paragraphGap: 10 });
129
+ } else if (line.startsWith("## ")) {
130
+ doc.fontSize(18).font("Helvetica-Bold").fillColor("#111111")
131
+ .text(line.slice(3), { paragraphGap: 8 });
132
+ } else if (line.startsWith("### ")) {
133
+ doc.fontSize(14).font("Helvetica-Bold").fillColor("#222222")
134
+ .text(line.slice(4), { paragraphGap: 6 });
135
+ } else if (line.startsWith("#### ")) {
136
+ doc.fontSize(12).font("Helvetica-Bold").fillColor("#333333")
137
+ .text(line.slice(5), { paragraphGap: 4 });
138
+
139
+ // Horizontal rule
140
+ } else if (/^---+$/.test(line.trim())) {
141
+ doc.moveDown(0.3).moveTo(50, doc.y).lineTo(545, doc.y).stroke().moveDown(0.3);
142
+
143
+ // Unordered lists
144
+ } else if (/^(\s*)[*\-+] /.test(line)) {
145
+ const indent = line.match(/^(\s*)/)[1].length;
146
+ const text = line.replace(/^\s*[*\-+] /, "");
147
+ doc.fontSize(12).font("Helvetica").fillColor("#000000")
148
+ .text(`${" ".repeat(indent / 2)}• ${renderInline(text)}`, { paragraphGap: 3, indent: indent * 6 });
149
+
150
+ // Ordered lists
151
+ } else if (/^\d+\. /.test(line.trim())) {
152
+ const num = line.match(/^(\d+)\./)[1];
153
+ const text = line.replace(/^\d+\. /, "");
154
+ doc.fontSize(12).font("Helvetica").fillColor("#000000")
155
+ .text(`${num}. ${renderInline(text)}`, { paragraphGap: 3 });
156
+
157
+ // Blockquote
158
+ } else if (line.startsWith("> ")) {
159
+ doc.fontSize(11).font("Helvetica-Oblique").fillColor("#555555")
160
+ .text(`"${line.slice(2)}"`, { indent: 20, paragraphGap: 3 });
161
+ doc.fillColor("#000000");
162
+
163
+ // Blank line
164
+ } else if (line.trim() === "") {
165
+ doc.moveDown(0.5);
166
+
167
+ // Normal paragraph
168
+ } else {
169
+ doc.fontSize(12).font("Helvetica").fillColor("#000000")
170
+ .text(renderInline(line), { paragraphGap: 3 });
171
+ }
172
+ }
173
+
174
+ // Flush any remaining
175
+ if (inCodeBlock) flushCode();
176
+ if (tableBuffer.length > 0) flushTable();
177
+ }
178
+
179
+ /** Strip bold/italic/code markdown for inline text (PDFKit doesn't do inline styles easily) */
180
+ function renderInline(text) {
181
+ return text
182
+ .replace(/`([^`]+)`/g, (_, code) => `[${code}]`) // `code` → [code]
183
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // **bold** → bold
184
+ .replace(/\*([^*]+)\*/g, "$1") // *italic* → italic
185
+ .replace(/__([^_]+)__/g, "$1") // __bold__ → bold
186
+ .replace(/_([^_]+)_/g, "$1") // _italic_ → italic
187
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // [text](url) → text
188
+ }
189
+
190
+ // ─── DOCX ─────────────────────────────────────────────────────────────────────
191
+
192
+ async function createDOCX(filePath, content) {
193
+ try {
194
+ const docx = await import("docx");
195
+ const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = docx;
196
+
197
+ const children = [];
198
+ const lines = content.split("\n");
199
+
200
+ for (const line of lines) {
201
+ if (line.startsWith("# ")) {
202
+ children.push(new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 }));
203
+ } else if (line.startsWith("## ")) {
204
+ children.push(new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 }));
205
+ } else if (line.startsWith("### ")) {
206
+ children.push(new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 }));
207
+ } else if (/^[*\-+] /.test(line)) {
208
+ children.push(new Paragraph({
209
+ text: line.replace(/^[*\-+] /, ""),
210
+ bullet: { level: 0 },
211
+ }));
212
+ } else if (line.trim() === "") {
213
+ children.push(new Paragraph({ text: "" }));
214
+ } else {
215
+ // Parse inline bold/italic
216
+ const runs = parseInlineDocx(line, TextRun);
217
+ children.push(new Paragraph({ children: runs }));
218
+ }
219
+ }
220
+
221
+ const doc = new Document({ sections: [{ properties: {}, children }] });
222
+ const buffer = await Packer.toBuffer(doc);
223
+ writeFileSync(filePath, buffer);
224
+
225
+ console.log(` [createDocument] DOCX written: ${filePath} (${buffer.length} bytes)`);
226
+ return `DOCX created: ${filePath} (${buffer.length} bytes)`;
227
+ } catch (error) {
228
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.message.includes("docx")) {
229
+ return "DOCX requires the docx package. Install with: pnpm add docx";
230
+ }
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ function parseInlineDocx(text, TextRun) {
236
+ // Split by **bold** and *italic* patterns
237
+ const runs = [];
238
+ const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
239
+ let last = 0;
240
+ let match;
241
+
242
+ while ((match = regex.exec(text)) !== null) {
243
+ if (match.index > last) {
244
+ runs.push(new TextRun({ text: text.slice(last, match.index) }));
245
+ }
246
+ const token = match[0];
247
+ if (token.startsWith("**")) {
248
+ runs.push(new TextRun({ text: token.slice(2, -2), bold: true }));
249
+ } else if (token.startsWith("*")) {
250
+ runs.push(new TextRun({ text: token.slice(1, -1), italics: true }));
251
+ } else if (token.startsWith("`")) {
252
+ runs.push(new TextRun({ text: token.slice(1, -1), font: "Courier New", size: 20 }));
253
+ }
254
+ last = match.index + token.length;
255
+ }
256
+
257
+ if (last < text.length) {
258
+ runs.push(new TextRun({ text: text.slice(last) }));
259
+ }
260
+
261
+ return runs.length > 0 ? runs : [new TextRun({ text })];
262
+ }
263
+
264
+ export const createDocumentDescription =
265
+ 'createDocument(filePath: string, content: string, format?: string) - Create a document. Formats: "markdown" (default), "pdf" (requires pdfkit), "docx" (requires docx). PDF/DOCX support # headings, ## h2, ### h3, - bullets, 1. numbered lists, **bold**, *italic*, `code`, tables, > blockquotes, --- rules, ```code blocks```.';
@@ -0,0 +1,105 @@
1
+ /**
2
+ * cron(action, paramsJson?) — Schedule recurring tasks from within the agent.
3
+ * Bridge to the existing Scheduler. Inspired by OpenClaw's cron tool.
4
+ *
5
+ * Actions:
6
+ * status — show scheduler status
7
+ * list — list all schedules
8
+ * add — create a new schedule
9
+ * update — patch an existing schedule (change expression, name, or taskInput)
10
+ * enable — re-enable a disabled schedule
11
+ * disable — pause a schedule without deleting it
12
+ * remove — delete a schedule permanently
13
+ * run — trigger a schedule immediately (regardless of cron timing)
14
+ */
15
+ import scheduler from "../scheduler/Scheduler.js";
16
+
17
+ export function cron(action, paramsJson) {
18
+ try {
19
+ const params = paramsJson ? JSON.parse(paramsJson) : {};
20
+
21
+ switch (action) {
22
+ case "status": {
23
+ const schedules = scheduler.list();
24
+ return JSON.stringify({
25
+ running: scheduler.running,
26
+ total: schedules.length,
27
+ enabled: schedules.filter((s) => s.enabled).length,
28
+ disabled: schedules.filter((s) => !s.enabled).length,
29
+ });
30
+ }
31
+
32
+ case "list": {
33
+ const schedules = scheduler.list();
34
+ if (schedules.length === 0) return "No schedules configured.";
35
+ return schedules
36
+ .map(
37
+ (s) =>
38
+ `• ${s.id.slice(0, 8)} — "${s.name}" | ${s.cronExpression} | ${s.enabled ? "enabled" : "DISABLED"} | runs: ${s.runCount} | last: ${s.lastRun || "never"}`
39
+ )
40
+ .join("\n");
41
+ }
42
+
43
+ case "add": {
44
+ if (!params.cronExpression) return 'Error: cronExpression is required. Example: "0 9 * * *" for daily at 9am.';
45
+ if (!params.taskInput) return "Error: taskInput is required — the task/message to send when triggered.";
46
+ const schedule = scheduler.create({
47
+ cronExpression: params.cronExpression,
48
+ taskInput: params.taskInput,
49
+ name: params.name,
50
+ channel: params.channel,
51
+ model: params.model,
52
+ });
53
+ return `Schedule created: "${schedule.name}" | ${schedule.cronExpression} | ID: ${schedule.id.slice(0, 8)}`;
54
+ }
55
+
56
+ case "update": {
57
+ if (!params.id) return 'Error: id is required. Use cron("list") to see schedule IDs.';
58
+ // Allow partial prefix ID match (first 8 chars is enough)
59
+ const schedule = scheduler.update(params.id, {
60
+ cronExpression: params.cronExpression,
61
+ taskInput: params.taskInput,
62
+ name: params.name,
63
+ });
64
+ return `Schedule updated: "${schedule.name}" | ${schedule.cronExpression} | ID: ${schedule.id.slice(0, 8)}`;
65
+ }
66
+
67
+ case "enable": {
68
+ if (!params.id) return 'Error: id is required.';
69
+ const schedule = scheduler.update(params.id, { enabled: true });
70
+ return `Schedule "${schedule.name}" (${params.id.slice(0, 8)}) enabled — will run on next trigger.`;
71
+ }
72
+
73
+ case "disable": {
74
+ if (!params.id) return 'Error: id is required.';
75
+ const schedule = scheduler.update(params.id, { enabled: false });
76
+ return `Schedule "${schedule.name}" (${params.id.slice(0, 8)}) disabled — cron job paused, not deleted.`;
77
+ }
78
+
79
+ case "remove": {
80
+ if (!params.id) return 'Error: id is required. Use cron("list") to see schedule IDs.';
81
+ scheduler.delete(params.id);
82
+ return `Schedule ${params.id.slice(0, 8)} removed.`;
83
+ }
84
+
85
+ case "run": {
86
+ if (!params.id) return 'Error: id is required. Use cron("list") to see schedule IDs.';
87
+ scheduler.triggerSchedule(params.id);
88
+ return `Schedule ${params.id.slice(0, 8)} triggered manually.`;
89
+ }
90
+
91
+ default:
92
+ return `Unknown action: "${action}". Available: status, list, add, update, enable, disable, remove, run`;
93
+ }
94
+ } catch (error) {
95
+ return `Cron error: ${error.message}`;
96
+ }
97
+ }
98
+
99
+ export const cronDescription =
100
+ 'cron(action, paramsJson?) — Manage scheduled recurring tasks. ' +
101
+ 'Actions: "status", "list", ' +
102
+ '"add" ({"cronExpression":"0 9 * * *","taskInput":"Check emails","name":"Morning"}), ' +
103
+ '"update" ({"id":"...","cronExpression":"0 10 * * *"}), ' +
104
+ '"enable" ({"id":"..."}), "disable" ({"id":"..."}), ' +
105
+ '"remove" ({"id":"..."}), "run" ({"id":"..."}).';