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,161 @@
1
+ /**
2
+ * Multi-Strategy Browser Manager
3
+ *
4
+ * Auto-selects between three browser strategies:
5
+ * - CLI: Headless Playwright, one-shot (screenshots, text extraction, PDF)
6
+ * - Gateway: Persistent HTTP browser server (interactive browsing, form-filling)
7
+ * - CDP: Attach to user's live Chrome via DevTools Protocol
8
+ */
9
+ import { spawn } from "child_process";
10
+ import http from "http";
11
+ import fs from "fs";
12
+ import { config } from "../config.js";
13
+ import { BROWSE_SERVER_SCRIPT } from "../paths.js";
14
+ import { screenshotUrl, extractText, generatePdf } from "./browser.js";
15
+ /** Auto-select the best browser strategy for a task */
16
+ export function selectStrategy(task = {}) {
17
+ if (task.useUserBrowser || config.cdpUrl)
18
+ return "cdp";
19
+ if (task.interactive || task.multiStep)
20
+ return "gateway";
21
+ return "cli";
22
+ }
23
+ // ── Gateway Management ────────────────────────────────────────────────
24
+ let gatewayProcess = null;
25
+ async function gatewayRequest(path, params = {}) {
26
+ const query = new URLSearchParams(params).toString();
27
+ const url = `http://127.0.0.1:${config.browseServerPort}${path}${query ? "?" + query : ""}`;
28
+ return new Promise((resolve, reject) => {
29
+ http.get(url, (res) => {
30
+ let data = "";
31
+ res.on("data", chunk => data += chunk);
32
+ res.on("end", () => {
33
+ try {
34
+ resolve(JSON.parse(data));
35
+ }
36
+ catch {
37
+ reject(new Error(`Invalid JSON from gateway: ${data.slice(0, 200)}`));
38
+ }
39
+ });
40
+ }).on("error", reject);
41
+ });
42
+ }
43
+ async function ensureGateway() {
44
+ // Check if already running
45
+ try {
46
+ const health = await gatewayRequest("/health");
47
+ if (health.ok)
48
+ return true;
49
+ }
50
+ catch { /* not running */ }
51
+ // Start it
52
+ if (!fs.existsSync(BROWSE_SERVER_SCRIPT))
53
+ return false;
54
+ gatewayProcess = spawn("node", [BROWSE_SERVER_SCRIPT, String(config.browseServerPort)], {
55
+ stdio: "pipe",
56
+ detached: false,
57
+ });
58
+ gatewayProcess.on("exit", () => { gatewayProcess = null; });
59
+ // Wait for startup (max 10s)
60
+ for (let i = 0; i < 20; i++) {
61
+ await new Promise(r => setTimeout(r, 500));
62
+ try {
63
+ const health = await gatewayRequest("/health");
64
+ if (health.ok)
65
+ return true;
66
+ }
67
+ catch { /* still starting */ }
68
+ }
69
+ return false;
70
+ }
71
+ // ── Unified Operations ────────────────────────────────────────────────
72
+ /** Navigate to URL using best strategy */
73
+ export async function navigate(url, task = {}) {
74
+ const strategy = selectStrategy(task);
75
+ if (strategy === "gateway") {
76
+ await ensureGateway();
77
+ return gatewayRequest("/navigate", { url });
78
+ }
79
+ if (strategy === "cdp") {
80
+ // CDP: use playwright connectOverCDP
81
+ const { chromium } = await import("playwright");
82
+ const browser = await chromium.connectOverCDP(config.cdpUrl);
83
+ const contexts = browser.contexts();
84
+ const page = contexts[0]?.pages()[0] || await contexts[0]?.newPage() || await browser.newPage();
85
+ await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
86
+ const title = await page.title();
87
+ return { title, url: page.url() };
88
+ }
89
+ // CLI: simple text extraction
90
+ const text = await extractText(url);
91
+ return { title: url, url, tree: [text.slice(0, 500)] };
92
+ }
93
+ /** Take a screenshot */
94
+ export async function screenshot(url, options = {}) {
95
+ const strategy = selectStrategy();
96
+ if (strategy === "gateway") {
97
+ await ensureGateway();
98
+ if (url)
99
+ await gatewayRequest("/navigate", { url });
100
+ const result = await gatewayRequest("/screenshot", options.fullPage ? { full: "true" } : {});
101
+ return result.path;
102
+ }
103
+ // CLI fallback
104
+ return screenshotUrl(url, { fullPage: options.fullPage });
105
+ }
106
+ /** Get accessibility tree (gateway only) */
107
+ export async function getTree(limit = 100) {
108
+ await ensureGateway();
109
+ return gatewayRequest("/tree", { limit: String(limit) });
110
+ }
111
+ /** Click element by ref (gateway only) */
112
+ export async function click(ref) {
113
+ await ensureGateway();
114
+ return gatewayRequest("/click", { ref });
115
+ }
116
+ /** Fill input (gateway only) */
117
+ export async function fill(ref, value) {
118
+ await ensureGateway();
119
+ await gatewayRequest("/fill", { ref, value });
120
+ }
121
+ /** Type text (gateway only) */
122
+ export async function type(ref, text) {
123
+ await ensureGateway();
124
+ await gatewayRequest("/type", { ref, text });
125
+ }
126
+ /** Press key (gateway only) */
127
+ export async function press(key, ref) {
128
+ await ensureGateway();
129
+ await gatewayRequest("/press", ref ? { key, ref } : { key });
130
+ }
131
+ /** Scroll page (gateway only) */
132
+ export async function scroll(direction, amount = 600) {
133
+ await ensureGateway();
134
+ return gatewayRequest("/scroll", { direction, amount: String(amount) });
135
+ }
136
+ /** Evaluate JS (gateway only) */
137
+ export async function evaluate(js) {
138
+ await ensureGateway();
139
+ const result = await gatewayRequest("/eval", { js });
140
+ return result.result;
141
+ }
142
+ /** Generate PDF from URL */
143
+ export async function pdf(url) {
144
+ return generatePdf(url);
145
+ }
146
+ /** Close browser / stop gateway */
147
+ export async function close() {
148
+ try {
149
+ await gatewayRequest("/close");
150
+ }
151
+ catch { }
152
+ if (gatewayProcess) {
153
+ gatewayProcess.kill();
154
+ gatewayProcess = null;
155
+ }
156
+ }
157
+ /** Get current page info (gateway) */
158
+ export async function info() {
159
+ await ensureGateway();
160
+ return gatewayRequest("/info");
161
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Browser Service — Web browsing via Playwright.
3
+ *
4
+ * Capabilities:
5
+ * - Screenshot a URL
6
+ * - Extract text content from a URL
7
+ * - Fill forms (basic)
8
+ * - PDF generation
9
+ *
10
+ * Playwright is an optional dependency — browser features are only available if installed.
11
+ */
12
+ import { execSync } from "child_process";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import os from "os";
16
+ const TEMP_DIR = path.join(os.tmpdir(), "alvin-bot", "browser");
17
+ if (!fs.existsSync(TEMP_DIR))
18
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
19
+ /** Check if Playwright is available */
20
+ export function hasPlaywright() {
21
+ try {
22
+ execSync("npx playwright --version", { stdio: "pipe", timeout: 10000 });
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * Take a screenshot of a URL.
31
+ * Returns path to the screenshot image.
32
+ */
33
+ export async function screenshotUrl(url, options = {}) {
34
+ const { fullPage = false, width = 1280, height = 720 } = options;
35
+ const outputPath = path.join(TEMP_DIR, `screenshot_${Date.now()}.png`);
36
+ // Use a standalone Node script to avoid importing playwright at module level
37
+ const script = `
38
+ const { chromium } = require('playwright');
39
+ (async () => {
40
+ const browser = await chromium.launch({ headless: true });
41
+ const page = await browser.newPage({ viewport: { width: ${width}, height: ${height} } });
42
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: 30000 });
43
+ await page.screenshot({ path: ${JSON.stringify(outputPath)}, fullPage: ${fullPage} });
44
+ await browser.close();
45
+ })().catch(err => { console.error(err.message); process.exit(1); });
46
+ `;
47
+ try {
48
+ execSync(`node -e '${script.replace(/'/g, "\\'")}'`, {
49
+ stdio: "pipe",
50
+ timeout: 45000,
51
+ env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: "0" },
52
+ });
53
+ if (!fs.existsSync(outputPath))
54
+ throw new Error("Screenshot not created");
55
+ return outputPath;
56
+ }
57
+ catch (err) {
58
+ const error = err;
59
+ throw new Error(`Screenshot failed: ${error.stderr?.toString()?.trim() || error.message}`);
60
+ }
61
+ }
62
+ /**
63
+ * Extract text content from a URL.
64
+ * Returns the visible text content.
65
+ */
66
+ export async function extractText(url) {
67
+ const script = `
68
+ const { chromium } = require('playwright');
69
+ (async () => {
70
+ const browser = await chromium.launch({ headless: true });
71
+ const page = await browser.newPage();
72
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: 30000 });
73
+ const text = await page.evaluate(() => document.body.innerText);
74
+ console.log(text);
75
+ await browser.close();
76
+ })().catch(err => { console.error(err.message); process.exit(1); });
77
+ `;
78
+ try {
79
+ const result = execSync(`node -e '${script.replace(/'/g, "\\'")}'`, {
80
+ stdio: "pipe",
81
+ timeout: 45000,
82
+ env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: "0" },
83
+ });
84
+ return result.toString().trim();
85
+ }
86
+ catch (err) {
87
+ const error = err;
88
+ throw new Error(`Text extraction failed: ${error.stderr?.toString()?.trim() || error.message}`);
89
+ }
90
+ }
91
+ /**
92
+ * Generate PDF from a URL.
93
+ * Returns path to the PDF file.
94
+ */
95
+ export async function generatePdf(url) {
96
+ const outputPath = path.join(TEMP_DIR, `page_${Date.now()}.pdf`);
97
+ const script = `
98
+ const { chromium } = require('playwright');
99
+ (async () => {
100
+ const browser = await chromium.launch({ headless: true });
101
+ const page = await browser.newPage();
102
+ await page.goto(${JSON.stringify(url)}, { waitUntil: 'networkidle', timeout: 30000 });
103
+ await page.pdf({ path: ${JSON.stringify(outputPath)}, format: 'A4', printBackground: true });
104
+ await browser.close();
105
+ })().catch(err => { console.error(err.message); process.exit(1); });
106
+ `;
107
+ try {
108
+ execSync(`node -e '${script.replace(/'/g, "\\'")}'`, {
109
+ stdio: "pipe",
110
+ timeout: 45000,
111
+ env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: "0" },
112
+ });
113
+ if (!fs.existsSync(outputPath))
114
+ throw new Error("PDF not created");
115
+ return outputPath;
116
+ }
117
+ catch (err) {
118
+ const error = err;
119
+ throw new Error(`PDF generation failed: ${error.stderr?.toString()?.trim() || error.message}`);
120
+ }
121
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Context Compaction Service — Auto-summarize long non-SDK sessions.
3
+ *
4
+ * When conversation history grows too long (by message count or token count),
5
+ * older entries are summarized via AI, flushed to daily memory log, and replaced
6
+ * with a compact system summary message.
7
+ */
8
+ import { config } from "../config.js";
9
+ import { appendDailyLog } from "./memory.js";
10
+ import { getRegistry } from "../engine.js";
11
+ /** How many recent messages to keep verbatim after compaction. */
12
+ const KEEP_LAST = 10;
13
+ /** Fallback: if AI summary fails, keep this many recent messages. */
14
+ const FALLBACK_KEEP = 5;
15
+ /** Max chars per message when building the summary input. */
16
+ const MAX_CHARS_PER_ENTRY = 500;
17
+ /**
18
+ * Check whether a session needs compaction.
19
+ * Returns true if history is long enough or token usage is high.
20
+ */
21
+ export function shouldCompact(session) {
22
+ if (session.history.length <= KEEP_LAST)
23
+ return false;
24
+ return (session.history.length >= 25 ||
25
+ session.totalInputTokens >= config.compactionThreshold);
26
+ }
27
+ /**
28
+ * Compact a session's conversation history.
29
+ *
30
+ * 1. Separate history into "to summarize" (older) and "to keep" (recent).
31
+ * 2. Flush a textual summary of the older entries to daily memory log.
32
+ * 3. Try to generate an AI summary; fall back to raw truncation on failure.
33
+ * 4. Replace session.history with [summary system message, ...kept messages].
34
+ */
35
+ export async function compactSession(session) {
36
+ const history = session.history;
37
+ // Nothing to compact if we have fewer messages than we'd keep
38
+ if (history.length <= KEEP_LAST) {
39
+ return { removedEntries: 0, summaryTokens: 0, flushedToMemory: false };
40
+ }
41
+ const toSummarize = history.slice(0, history.length - KEEP_LAST);
42
+ const toKeep = history.slice(history.length - KEEP_LAST);
43
+ // Build text representation of entries to summarize
44
+ const summaryInput = toSummarize
45
+ .map((msg) => {
46
+ const content = msg.content.length > MAX_CHARS_PER_ENTRY
47
+ ? msg.content.slice(0, MAX_CHARS_PER_ENTRY) + "..."
48
+ : msg.content;
49
+ return `${msg.role}: ${content}`;
50
+ })
51
+ .join("\n\n");
52
+ // Flush to daily memory BEFORE removing entries
53
+ let flushedToMemory = false;
54
+ try {
55
+ const flushText = [
56
+ `**Context Compaction** — ${toSummarize.length} messages archived:`,
57
+ "",
58
+ summaryInput.length > 2000
59
+ ? summaryInput.slice(0, 2000) + "\n[...truncated]"
60
+ : summaryInput,
61
+ ].join("\n");
62
+ appendDailyLog(flushText);
63
+ flushedToMemory = true;
64
+ }
65
+ catch (err) {
66
+ console.error("Compaction: failed to flush to memory:", err);
67
+ }
68
+ // Try AI-powered summary
69
+ let summaryText = null;
70
+ try {
71
+ summaryText = await generateAISummary(summaryInput);
72
+ }
73
+ catch (err) {
74
+ console.warn("Compaction: AI summary failed, using fallback:", err);
75
+ }
76
+ let summaryMessage;
77
+ let removedEntries;
78
+ if (summaryText) {
79
+ // AI summary succeeded — replace old entries with a single system message
80
+ summaryMessage = {
81
+ role: "system",
82
+ content: `[Conversation summary of ${toSummarize.length} earlier messages]\n\n${summaryText}`,
83
+ };
84
+ removedEntries = toSummarize.length;
85
+ session.history = [summaryMessage, ...toKeep];
86
+ }
87
+ else {
88
+ // Fallback — just keep the last FALLBACK_KEEP entries from the to-keep set
89
+ // plus a minimal note that earlier context was dropped
90
+ const fallbackKeep = history.slice(-FALLBACK_KEEP);
91
+ summaryMessage = {
92
+ role: "system",
93
+ content: `[Earlier conversation context (${history.length - FALLBACK_KEEP} messages) was compacted due to length. Recent context follows.]`,
94
+ };
95
+ removedEntries = history.length - FALLBACK_KEEP;
96
+ session.history = [summaryMessage, ...fallbackKeep];
97
+ }
98
+ const summaryTokens = Math.ceil(summaryMessage.content.length / 4); // rough estimate
99
+ return {
100
+ removedEntries,
101
+ summaryTokens,
102
+ flushedToMemory,
103
+ };
104
+ }
105
+ /**
106
+ * Generate an AI-powered summary of conversation entries using the active provider.
107
+ * Uses effort "low" to keep cost minimal.
108
+ */
109
+ async function generateAISummary(text) {
110
+ const registry = getRegistry();
111
+ const opts = {
112
+ prompt: `Summarize the following conversation in under 300 words. Focus on key topics, decisions, and any action items. Be concise.\n\n${text}`,
113
+ systemPrompt: "You are a conversation summarizer. Output only the summary, no preamble.",
114
+ effort: "low",
115
+ };
116
+ let result = "";
117
+ for await (const chunk of registry.queryWithFallback(opts)) {
118
+ if (chunk.type === "text" && chunk.text) {
119
+ result = chunk.text;
120
+ }
121
+ if (chunk.type === "error") {
122
+ throw new Error(chunk.error || "AI summary generation failed");
123
+ }
124
+ }
125
+ if (!result.trim()) {
126
+ throw new Error("AI summary returned empty result");
127
+ }
128
+ return result.trim();
129
+ }