alvin-bot 4.4.1

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 (136) hide show
  1. package/.env.example +43 -0
  2. package/BACKLOG.md +223 -0
  3. package/CHANGELOG.md +63 -0
  4. package/CLAUDE.example.md +152 -0
  5. package/CODE_OF_CONDUCT.md +52 -0
  6. package/CONTRIBUTING.md +72 -0
  7. package/LICENSE +21 -0
  8. package/README.md +529 -0
  9. package/SECURITY.md +38 -0
  10. package/SOUL.example.md +60 -0
  11. package/TOOLS.example.md +42 -0
  12. package/alvin-bot.config.example.json +24 -0
  13. package/bin/cli.js +1088 -0
  14. package/dist/.metadata_never_index +0 -0
  15. package/dist/claude.js +102 -0
  16. package/dist/config.js +65 -0
  17. package/dist/engine.js +90 -0
  18. package/dist/find-claude-binary.js +98 -0
  19. package/dist/handlers/commands.js +1489 -0
  20. package/dist/handlers/document.js +187 -0
  21. package/dist/handlers/message.js +200 -0
  22. package/dist/handlers/photo.js +154 -0
  23. package/dist/handlers/platform-message.js +275 -0
  24. package/dist/handlers/video.js +237 -0
  25. package/dist/handlers/voice.js +148 -0
  26. package/dist/i18n.js +299 -0
  27. package/dist/index.js +442 -0
  28. package/dist/init-data-dir.js +81 -0
  29. package/dist/middleware/auth.js +215 -0
  30. package/dist/migrate.js +139 -0
  31. package/dist/paths.js +87 -0
  32. package/dist/platforms/discord.js +161 -0
  33. package/dist/platforms/index.js +130 -0
  34. package/dist/platforms/signal.js +205 -0
  35. package/dist/platforms/slack.js +318 -0
  36. package/dist/platforms/telegram.js +111 -0
  37. package/dist/platforms/types.js +8 -0
  38. package/dist/platforms/whatsapp.js +648 -0
  39. package/dist/providers/claude-sdk-provider.js +173 -0
  40. package/dist/providers/codex-cli-provider.js +121 -0
  41. package/dist/providers/index.js +7 -0
  42. package/dist/providers/openai-compatible.js +388 -0
  43. package/dist/providers/registry.js +209 -0
  44. package/dist/providers/tool-executor.js +450 -0
  45. package/dist/providers/types.js +205 -0
  46. package/dist/services/access.js +144 -0
  47. package/dist/services/asset-index.js +230 -0
  48. package/dist/services/browser-manager.js +161 -0
  49. package/dist/services/browser.js +121 -0
  50. package/dist/services/compaction.js +129 -0
  51. package/dist/services/cron.js +462 -0
  52. package/dist/services/custom-tools.js +317 -0
  53. package/dist/services/delivery-queue.js +154 -0
  54. package/dist/services/elevenlabs.js +58 -0
  55. package/dist/services/embeddings.js +386 -0
  56. package/dist/services/exec-guard.js +46 -0
  57. package/dist/services/fallback-order.js +151 -0
  58. package/dist/services/heartbeat.js +192 -0
  59. package/dist/services/hooks.js +44 -0
  60. package/dist/services/imagegen.js +72 -0
  61. package/dist/services/language-detect.js +144 -0
  62. package/dist/services/markdown.js +63 -0
  63. package/dist/services/mcp.js +252 -0
  64. package/dist/services/memory.js +133 -0
  65. package/dist/services/personality.js +227 -0
  66. package/dist/services/plugins.js +171 -0
  67. package/dist/services/reminders.js +97 -0
  68. package/dist/services/restart.js +48 -0
  69. package/dist/services/security-audit.js +66 -0
  70. package/dist/services/self-search.js +129 -0
  71. package/dist/services/session.js +93 -0
  72. package/dist/services/skills.js +287 -0
  73. package/dist/services/standing-orders.js +29 -0
  74. package/dist/services/subagents.js +142 -0
  75. package/dist/services/sudo.js +243 -0
  76. package/dist/services/telegram.js +113 -0
  77. package/dist/services/tool-discovery.js +214 -0
  78. package/dist/services/usage-tracker.js +137 -0
  79. package/dist/services/users.js +199 -0
  80. package/dist/services/voice.js +95 -0
  81. package/dist/tui/index.js +507 -0
  82. package/dist/web/canvas.js +30 -0
  83. package/dist/web/doctor-api.js +606 -0
  84. package/dist/web/openai-compat.js +252 -0
  85. package/dist/web/server.js +1351 -0
  86. package/dist/web/setup-api.js +1078 -0
  87. package/docs/mcp.example.json +16 -0
  88. package/docs/screenshots/00-Login.png +0 -0
  89. package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
  90. package/docs/screenshots/02-Chat.png +0 -0
  91. package/docs/screenshots/03-Dashboard-Overview.png +0 -0
  92. package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
  93. package/docs/screenshots/05-Personality-Editor.png +0 -0
  94. package/docs/screenshots/06-Memory-Manager.png +0 -0
  95. package/docs/screenshots/07-Active-Sessions.png +0 -0
  96. package/docs/screenshots/08-File-Browser.png +0 -0
  97. package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
  98. package/docs/screenshots/10-Custom-Tools.png +0 -0
  99. package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
  100. package/docs/screenshots/12-Messaging-Platforms.png +0 -0
  101. package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
  102. package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
  103. package/docs/screenshots/13-User-Management.png +0 -0
  104. package/docs/screenshots/14-Web-Terminal.png +0 -0
  105. package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
  106. package/docs/screenshots/16-Settings-and-Env.png +0 -0
  107. package/docs/screenshots/TG-commands.png +0 -0
  108. package/docs/screenshots/TG.png +0 -0
  109. package/docs/screenshots/_Mac-Installer.png +0 -0
  110. package/docs/tools.example.json +33 -0
  111. package/install.sh +165 -0
  112. package/package.json +190 -0
  113. package/plugins/calendar/index.js +270 -0
  114. package/plugins/email/index.js +231 -0
  115. package/plugins/finance/index.js +254 -0
  116. package/plugins/notes/index.js +227 -0
  117. package/plugins/smarthome/index.js +230 -0
  118. package/plugins/weather/index.js +122 -0
  119. package/skills/apple-notes/SKILL.md +31 -0
  120. package/skills/browse/SKILL.md +136 -0
  121. package/skills/code-project/SKILL.md +43 -0
  122. package/skills/data-analysis/SKILL.md +39 -0
  123. package/skills/document-creation/SKILL.md +48 -0
  124. package/skills/email-summary/SKILL.md +46 -0
  125. package/skills/github/SKILL.md +42 -0
  126. package/skills/summarize/SKILL.md +28 -0
  127. package/skills/system-admin/SKILL.md +39 -0
  128. package/skills/weather/SKILL.md +34 -0
  129. package/skills/web-research/SKILL.md +35 -0
  130. package/web/public/canvas.html +52 -0
  131. package/web/public/css/style.css +555 -0
  132. package/web/public/index.html +189 -0
  133. package/web/public/js/app.js +3102 -0
  134. package/web/public/js/i18n.js +1048 -0
  135. package/web/public/js/icons.js +104 -0
  136. package/web/public/login.html +48 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Calendar Plugin — View and manage calendar events.
3
+ *
4
+ * Supports:
5
+ * - Google Calendar (via API, needs GOOGLE_CALENDAR_API_KEY or OAuth)
6
+ * - iCal URL import (any .ics feed)
7
+ * - Local event storage (docs/calendar.json) as fallback
8
+ *
9
+ * For Google Calendar: set GOOGLE_CALENDAR_ID and GOOGLE_API_KEY in .env
10
+ * For iCal: set ICAL_URL in .env
11
+ * Without either: uses local JSON storage
12
+ */
13
+
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { fileURLToPath } from "url";
17
+
18
+ const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
19
+ const CALENDAR_FILE = path.resolve(PLUGIN_ROOT, "docs", "calendar.json");
20
+
21
+ // ── Local Event Storage ─────────────────────────────────
22
+
23
+ function loadEvents() {
24
+ try {
25
+ return JSON.parse(fs.readFileSync(CALENDAR_FILE, "utf-8"));
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ function saveEvents(events) {
32
+ fs.writeFileSync(CALENDAR_FILE, JSON.stringify(events, null, 2));
33
+ }
34
+
35
+ function formatDate(date) {
36
+ return new Date(date).toLocaleDateString("de-DE", {
37
+ weekday: "short", day: "2-digit", month: "2-digit", year: "numeric",
38
+ hour: "2-digit", minute: "2-digit",
39
+ });
40
+ }
41
+
42
+ function parseDateTime(input) {
43
+ // Try common formats
44
+ // "morgen 14:00" "2026-03-01 09:00" "in 2h" etc.
45
+ const now = new Date();
46
+
47
+ if (input.startsWith("in ")) {
48
+ const match = input.match(/in (\d+)\s*(m|min|h|std|d|tag)/i);
49
+ if (match) {
50
+ const amount = parseInt(match[1]);
51
+ const unit = match[2].toLowerCase();
52
+ const ms = unit.startsWith("m") ? amount * 60000 :
53
+ unit.startsWith("h") || unit.startsWith("s") ? amount * 3600000 :
54
+ amount * 86400000;
55
+ return new Date(now.getTime() + ms);
56
+ }
57
+ }
58
+
59
+ if (input.startsWith("morgen")) {
60
+ const tomorrow = new Date(now);
61
+ tomorrow.setDate(tomorrow.getDate() + 1);
62
+ const timePart = input.replace("morgen", "").trim();
63
+ if (timePart) {
64
+ const [h, m] = timePart.split(":").map(Number);
65
+ tomorrow.setHours(h || 9, m || 0, 0, 0);
66
+ } else {
67
+ tomorrow.setHours(9, 0, 0, 0);
68
+ }
69
+ return tomorrow;
70
+ }
71
+
72
+ if (input.startsWith("heute")) {
73
+ const today = new Date(now);
74
+ const timePart = input.replace("heute", "").trim();
75
+ if (timePart) {
76
+ const [h, m] = timePart.split(":").map(Number);
77
+ today.setHours(h || 9, m || 0, 0, 0);
78
+ }
79
+ return today;
80
+ }
81
+
82
+ // ISO or other parseable format
83
+ const parsed = new Date(input);
84
+ if (!isNaN(parsed.getTime())) return parsed;
85
+
86
+ return null;
87
+ }
88
+
89
+ export default {
90
+ name: "calendar",
91
+ description: "Create, view and manage calendar events",
92
+ version: "1.0.0",
93
+ author: "Alvin Bot",
94
+
95
+ commands: [
96
+ {
97
+ command: "cal",
98
+ description: "View/manage calendar",
99
+ handler: async (ctx, args) => {
100
+ // /cal — show upcoming events
101
+ if (!args) {
102
+ const events = loadEvents()
103
+ .filter(e => new Date(e.date) >= new Date())
104
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
105
+ .slice(0, 10);
106
+
107
+ if (events.length === 0) {
108
+ await ctx.reply("📅 No upcoming events.\nNew: `/cal add morgen 14:00 | Meeting`", { parse_mode: "Markdown" });
109
+ return;
110
+ }
111
+
112
+ const lines = events.map((e, i) => {
113
+ const date = formatDate(e.date);
114
+ const loc = e.location ? ` 📍 ${e.location}` : "";
115
+ return `${i + 1}. 📅 *${e.title}*\n ${date}${loc}`;
116
+ });
117
+
118
+ await ctx.reply(`📅 *Upcoming events:*\n\n${lines.join("\n\n")}`, { parse_mode: "Markdown" });
119
+ return;
120
+ }
121
+
122
+ // /cal add <datetime> | <title> [| location]
123
+ if (args.startsWith("add ")) {
124
+ const text = args.slice(4).trim();
125
+ const parts = text.split("|").map(s => s.trim());
126
+
127
+ if (parts.length < 2) {
128
+ await ctx.reply("Format: `/cal add morgen 14:00 | Event title | Location (optional)`", { parse_mode: "Markdown" });
129
+ return;
130
+ }
131
+
132
+ const dateStr = parts[0];
133
+ const title = parts[1];
134
+ const location = parts[2] || "";
135
+
136
+ const date = parseDateTime(dateStr);
137
+ if (!date) {
138
+ await ctx.reply(`❌ Date not recognized: "${dateStr}"\nTry: \`morgen 14:00\`, \`heute 18:00\`, \`in 2h\`, \`2026-03-01 09:00\``, { parse_mode: "Markdown" });
139
+ return;
140
+ }
141
+
142
+ const event = {
143
+ id: Date.now().toString(36),
144
+ title,
145
+ date: date.toISOString(),
146
+ location,
147
+ created: new Date().toISOString(),
148
+ };
149
+
150
+ const events = loadEvents();
151
+ events.push(event);
152
+ saveEvents(events);
153
+
154
+ await ctx.reply(
155
+ `✅ *Event created:*\n\n📅 ${title}\n🕐 ${formatDate(date)}${location ? `\n📍 ${location}` : ""}`,
156
+ { parse_mode: "Markdown" }
157
+ );
158
+ return;
159
+ }
160
+
161
+ // /cal delete <number>
162
+ if (args.startsWith("delete ") || args.startsWith("del ")) {
163
+ const idx = parseInt(args.split(" ")[1]) - 1;
164
+ const events = loadEvents()
165
+ .filter(e => new Date(e.date) >= new Date())
166
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
167
+
168
+ if (isNaN(idx) || idx < 0 || idx >= events.length) {
169
+ await ctx.reply("❌ Invalid number. Use `/cal` for the list.", { parse_mode: "Markdown" });
170
+ return;
171
+ }
172
+
173
+ const toDelete = events[idx];
174
+ const allEvents = loadEvents().filter(e => e.id !== toDelete.id);
175
+ saveEvents(allEvents);
176
+
177
+ await ctx.reply(`🗑️ Deleted: *${toDelete.title}*`, { parse_mode: "Markdown" });
178
+ return;
179
+ }
180
+
181
+ // /cal today
182
+ if (args === "today" || args === "heute") {
183
+ const now = new Date();
184
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
185
+ const todayEnd = new Date(todayStart.getTime() + 86400000);
186
+
187
+ const events = loadEvents()
188
+ .filter(e => {
189
+ const d = new Date(e.date);
190
+ return d >= todayStart && d < todayEnd;
191
+ })
192
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
193
+
194
+ if (events.length === 0) {
195
+ await ctx.reply("📅 No events today.");
196
+ return;
197
+ }
198
+
199
+ const lines = events.map(e => {
200
+ const time = new Date(e.date).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
201
+ return `🕐 ${time} — *${e.title}*`;
202
+ });
203
+
204
+ await ctx.reply(`📅 *Today:*\n\n${lines.join("\n")}`, { parse_mode: "Markdown" });
205
+ return;
206
+ }
207
+
208
+ await ctx.reply(
209
+ "📅 *Calendar commands:*\n\n" +
210
+ "`/cal` — Upcoming events\n" +
211
+ "`/cal heute` — Today's events\n" +
212
+ "`/cal add morgen 14:00 | Title | Location` — Create event\n" +
213
+ "`/cal delete 1` — Delete event",
214
+ { parse_mode: "Markdown" }
215
+ );
216
+ },
217
+ },
218
+ ],
219
+
220
+ tools: [
221
+ {
222
+ name: "list_events",
223
+ description: "List upcoming calendar events",
224
+ parameters: {
225
+ type: "object",
226
+ properties: {
227
+ days: { type: "number", description: "Number of days to look ahead (default: 7)" },
228
+ },
229
+ },
230
+ execute: async (params) => {
231
+ const days = params.days || 7;
232
+ const cutoff = new Date(Date.now() + days * 86400000);
233
+ const events = loadEvents()
234
+ .filter(e => new Date(e.date) >= new Date() && new Date(e.date) <= cutoff)
235
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
236
+ return JSON.stringify(events);
237
+ },
238
+ },
239
+ {
240
+ name: "create_event",
241
+ description: "Create a calendar event",
242
+ parameters: {
243
+ type: "object",
244
+ properties: {
245
+ title: { type: "string", description: "Event title" },
246
+ date: { type: "string", description: "Date/time (ISO or natural language)" },
247
+ location: { type: "string", description: "Location (optional)" },
248
+ },
249
+ required: ["title", "date"],
250
+ },
251
+ execute: async (params) => {
252
+ const date = parseDateTime(params.date) || new Date(params.date);
253
+ if (isNaN(date.getTime())) return `Invalid date: ${params.date}`;
254
+
255
+ const event = {
256
+ id: Date.now().toString(36),
257
+ title: params.title,
258
+ date: date.toISOString(),
259
+ location: params.location || "",
260
+ created: new Date().toISOString(),
261
+ };
262
+
263
+ const events = loadEvents();
264
+ events.push(event);
265
+ saveEvents(events);
266
+ return `Event created: ${params.title} at ${date.toISOString()}`;
267
+ },
268
+ },
269
+ ],
270
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Email Plugin — Read and send emails via IMAP/SMTP.
3
+ *
4
+ * Lightweight implementation using raw IMAP/SMTP commands.
5
+ * For full email support, configure in .env:
6
+ * EMAIL_IMAP_HOST=imap.mail.me.com
7
+ * EMAIL_IMAP_PORT=993
8
+ * EMAIL_SMTP_HOST=smtp.mail.me.com
9
+ * EMAIL_SMTP_PORT=587
10
+ * EMAIL_USER=your-email@provider.com
11
+ * EMAIL_PASS=app-specific-password
12
+ *
13
+ * Alternative: Uses `himalaya` CLI if installed (more reliable).
14
+ */
15
+
16
+ import { execSync } from "child_process";
17
+
18
+ function hasHimalaya() {
19
+ try {
20
+ execSync("which himalaya", { stdio: "pipe" });
21
+ return true;
22
+ } catch { return false; }
23
+ }
24
+
25
+ function runHimalaya(args, timeout = 15000) {
26
+ try {
27
+ const result = execSync(`himalaya ${args}`, {
28
+ stdio: "pipe",
29
+ timeout,
30
+ env: { ...process.env, NO_COLOR: "1" },
31
+ });
32
+ return result.toString().trim();
33
+ } catch (err) {
34
+ throw new Error(err.stderr?.toString()?.trim() || err.message);
35
+ }
36
+ }
37
+
38
+ function parseEmailList(output) {
39
+ // himalaya list outputs a table — parse it
40
+ const lines = output.split("\n").filter(l => l.trim());
41
+ if (lines.length < 2) return [];
42
+
43
+ // Skip header line
44
+ return lines.slice(1).map(line => {
45
+ // Format: ID | FLAGS | FROM | SUBJECT | DATE
46
+ const parts = line.split("|").map(s => s.trim());
47
+ if (parts.length >= 4) {
48
+ return {
49
+ id: parts[0],
50
+ flags: parts[1],
51
+ from: parts[2],
52
+ subject: parts[3],
53
+ date: parts[4] || "",
54
+ };
55
+ }
56
+ return null;
57
+ }).filter(Boolean);
58
+ }
59
+
60
+ export default {
61
+ name: "email",
62
+ description: "E-Mails lesen und senden (via himalaya CLI oder IMAP/SMTP)",
63
+ version: "1.0.0",
64
+ author: "Alvin Bot",
65
+
66
+ onInit: () => {
67
+ if (!hasHimalaya()) {
68
+ console.warn("Email plugin: himalaya CLI not found. Install with: brew install himalaya");
69
+ }
70
+ },
71
+
72
+ commands: [
73
+ {
74
+ command: "email",
75
+ description: "E-Mails verwalten",
76
+ handler: async (ctx, args) => {
77
+ if (!hasHimalaya()) {
78
+ await ctx.reply(
79
+ "📧 *Email Plugin*\n\n" +
80
+ "`himalaya` CLI nicht installiert.\n" +
81
+ "Installiere mit: `brew install himalaya`\n" +
82
+ "Konfiguriere mit: `himalaya account configure`",
83
+ { parse_mode: "Markdown" }
84
+ );
85
+ return;
86
+ }
87
+
88
+ // /email — list inbox
89
+ if (!args || args === "inbox") {
90
+ try {
91
+ await ctx.api.sendChatAction(ctx.chat.id, "typing");
92
+ const output = runHimalaya("list -s 10");
93
+ const emails = parseEmailList(output);
94
+
95
+ if (emails.length === 0) {
96
+ await ctx.reply("📭 Keine E-Mails im Posteingang.");
97
+ return;
98
+ }
99
+
100
+ const lines = emails.map((e, i) => {
101
+ const unread = e.flags?.includes("Seen") ? "" : "🆕 ";
102
+ return `${unread}*${i + 1}.* ${e.from}\n ${e.subject}`;
103
+ });
104
+
105
+ await ctx.reply(`📧 *Posteingang (${emails.length}):*\n\n${lines.join("\n\n")}`, { parse_mode: "Markdown" });
106
+ } catch (err) {
107
+ await ctx.reply(`❌ Fehler: ${err.message}`);
108
+ }
109
+ return;
110
+ }
111
+
112
+ // /email read <id>
113
+ if (args.startsWith("read ")) {
114
+ const id = args.slice(5).trim();
115
+ try {
116
+ await ctx.api.sendChatAction(ctx.chat.id, "typing");
117
+ const output = runHimalaya(`read ${id}`);
118
+ const truncated = output.length > 3500 ? output.slice(0, 3500) + "\n\n_[...truncated]_" : output;
119
+ await ctx.reply(`📧 *E-Mail #${id}:*\n\n${truncated}`, { parse_mode: "Markdown" });
120
+ } catch (err) {
121
+ await ctx.reply(`❌ ${err.message}`);
122
+ }
123
+ return;
124
+ }
125
+
126
+ // /email send <to> | <subject> | <body>
127
+ if (args.startsWith("send ")) {
128
+ const text = args.slice(5).trim();
129
+ const parts = text.split("|").map(s => s.trim());
130
+
131
+ if (parts.length < 3) {
132
+ await ctx.reply("Format: `/email send to@example.com | Betreff | Text`", { parse_mode: "Markdown" });
133
+ return;
134
+ }
135
+
136
+ const [to, subject, ...bodyParts] = parts;
137
+ const body = bodyParts.join("|");
138
+
139
+ try {
140
+ await ctx.api.sendChatAction(ctx.chat.id, "typing");
141
+ // Use himalaya write + send
142
+ const mml = `From: \nTo: ${to}\nSubject: ${subject}\n\n${body}`;
143
+ execSync(`echo '${mml.replace(/'/g, "'\\''")}' | himalaya send`, {
144
+ stdio: "pipe",
145
+ timeout: 30000,
146
+ });
147
+ await ctx.reply(`✅ E-Mail gesendet an ${to}`, { parse_mode: "Markdown" });
148
+ } catch (err) {
149
+ await ctx.reply(`❌ Senden fehlgeschlagen: ${err.message}`);
150
+ }
151
+ return;
152
+ }
153
+
154
+ // /email search <query>
155
+ if (args.startsWith("search ")) {
156
+ const query = args.slice(7).trim();
157
+ try {
158
+ await ctx.api.sendChatAction(ctx.chat.id, "typing");
159
+ const output = runHimalaya(`search "${query}"`, 30000);
160
+ const truncated = output.length > 3000 ? output.slice(0, 3000) + "\n..." : output;
161
+ await ctx.reply(`🔍 *Suche: "${query}"*\n\n${truncated}`, { parse_mode: "Markdown" });
162
+ } catch (err) {
163
+ await ctx.reply(`❌ ${err.message}`);
164
+ }
165
+ return;
166
+ }
167
+
168
+ await ctx.reply(
169
+ "📧 *Email-Befehle:*\n\n" +
170
+ "`/email` — Posteingang (letzte 10)\n" +
171
+ "`/email read 123` — E-Mail lesen\n" +
172
+ "`/email send to@x.com | Betreff | Text` — Senden\n" +
173
+ "`/email search Suchbegriff` — Suchen",
174
+ { parse_mode: "Markdown" }
175
+ );
176
+ },
177
+ },
178
+ ],
179
+
180
+ tools: [
181
+ {
182
+ name: "list_emails",
183
+ description: "List recent emails from inbox",
184
+ parameters: {
185
+ type: "object",
186
+ properties: {
187
+ count: { type: "number", description: "Number of emails (default: 10)" },
188
+ },
189
+ },
190
+ execute: async (params) => {
191
+ if (!hasHimalaya()) return "himalaya CLI not installed";
192
+ const count = params.count || 10;
193
+ return runHimalaya(`list -s ${count}`);
194
+ },
195
+ },
196
+ {
197
+ name: "read_email",
198
+ description: "Read a specific email by ID",
199
+ parameters: {
200
+ type: "object",
201
+ properties: {
202
+ id: { type: "string", description: "Email ID" },
203
+ },
204
+ required: ["id"],
205
+ },
206
+ execute: async (params) => {
207
+ if (!hasHimalaya()) return "himalaya CLI not installed";
208
+ return runHimalaya(`read ${params.id}`);
209
+ },
210
+ },
211
+ {
212
+ name: "send_email",
213
+ description: "Send an email",
214
+ parameters: {
215
+ type: "object",
216
+ properties: {
217
+ to: { type: "string", description: "Recipient email" },
218
+ subject: { type: "string", description: "Email subject" },
219
+ body: { type: "string", description: "Email body text" },
220
+ },
221
+ required: ["to", "subject", "body"],
222
+ },
223
+ execute: async (params) => {
224
+ if (!hasHimalaya()) return "himalaya CLI not installed";
225
+ const mml = `From: \nTo: ${params.to}\nSubject: ${params.subject}\n\n${params.body}`;
226
+ execSync(`echo '${mml.replace(/'/g, "'\\''")}' | himalaya send`, { stdio: "pipe", timeout: 30000 });
227
+ return `Email sent to ${params.to}`;
228
+ },
229
+ },
230
+ ],
231
+ };