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,209 @@
1
+ /**
2
+ * Provider Registry — Model selection, fallback chain, and runtime switching.
3
+ *
4
+ * This is the central hub for multi-model support. It manages:
5
+ * - Which providers are configured and available
6
+ * - The active provider (switchable at runtime via /model)
7
+ * - Fallback chain when the active provider fails
8
+ */
9
+ import { ClaudeSDKProvider } from "./claude-sdk-provider.js";
10
+ import { CodexCLIProvider } from "./codex-cli-provider.js";
11
+ import { OpenAICompatibleProvider } from "./openai-compatible.js";
12
+ import { PROVIDER_PRESETS } from "./types.js";
13
+ export class ProviderRegistry {
14
+ providers = new Map();
15
+ primaryKey;
16
+ fallbackKeys;
17
+ activeKey;
18
+ constructor(config) {
19
+ this.primaryKey = config.primary;
20
+ this.fallbackKeys = config.fallbacks || [];
21
+ this.activeKey = config.primary;
22
+ // Register all configured providers
23
+ for (const [key, providerConfig] of Object.entries(config.providers)) {
24
+ this.register(key, providerConfig);
25
+ }
26
+ }
27
+ /**
28
+ * Register a provider by key.
29
+ */
30
+ register(key, config) {
31
+ const provider = this.createProvider(config);
32
+ this.providers.set(key, provider);
33
+ }
34
+ /**
35
+ * Get the currently active provider.
36
+ */
37
+ getActive() {
38
+ const provider = this.providers.get(this.activeKey);
39
+ if (!provider) {
40
+ throw new Error(`Active provider "${this.activeKey}" not found`);
41
+ }
42
+ return provider;
43
+ }
44
+ /**
45
+ * Get a specific provider by key.
46
+ */
47
+ get(key) {
48
+ return this.providers.get(key);
49
+ }
50
+ /**
51
+ * Switch the active provider (e.g., via /model command).
52
+ */
53
+ switchTo(key) {
54
+ if (!this.providers.has(key))
55
+ return false;
56
+ this.activeKey = key;
57
+ return true;
58
+ }
59
+ /**
60
+ * Get the active provider key.
61
+ */
62
+ getActiveKey() {
63
+ return this.activeKey;
64
+ }
65
+ /**
66
+ * List all registered providers with their status.
67
+ */
68
+ async listAll() {
69
+ const result = [];
70
+ for (const [key, provider] of this.providers) {
71
+ const info = provider.getInfo();
72
+ result.push({
73
+ key,
74
+ ...info,
75
+ active: key === this.activeKey,
76
+ });
77
+ }
78
+ return result;
79
+ }
80
+ /**
81
+ * Query with automatic fallback.
82
+ * Tries the active provider first, then fallbacks in order.
83
+ */
84
+ async *queryWithFallback(options) {
85
+ const chain = [this.activeKey, ...this.fallbackKeys.filter(k => k !== this.activeKey)];
86
+ const errors = [];
87
+ for (const key of chain) {
88
+ const provider = this.providers.get(key);
89
+ if (!provider)
90
+ continue;
91
+ // Check availability before trying
92
+ const available = await provider.isAvailable().catch(() => false);
93
+ if (!available) {
94
+ console.log(`Provider "${key}" not available, trying next...`);
95
+ errors.push({ key, error: "not available (check auth/config)" });
96
+ continue;
97
+ }
98
+ let hadError = false;
99
+ let lastError = "";
100
+ try {
101
+ for await (const chunk of provider.query(options)) {
102
+ if (chunk.type === "error") {
103
+ hadError = true;
104
+ lastError = chunk.error || "Unknown error";
105
+ break;
106
+ }
107
+ yield chunk;
108
+ if (chunk.type === "done")
109
+ return;
110
+ }
111
+ }
112
+ catch (err) {
113
+ hadError = true;
114
+ lastError = err instanceof Error ? err.message : String(err);
115
+ }
116
+ if (hadError) {
117
+ console.log(`Provider "${key}" failed: ${lastError}. Trying next...`);
118
+ errors.push({ key, error: lastError });
119
+ // Find next provider to notify about fallback
120
+ const nextIdx = chain.indexOf(key) + 1;
121
+ if (nextIdx < chain.length) {
122
+ const nextProvider = this.providers.get(chain[nextIdx]);
123
+ if (nextProvider) {
124
+ yield {
125
+ type: "fallback",
126
+ failedProvider: provider.getInfo().name,
127
+ providerName: nextProvider.getInfo().name,
128
+ error: lastError,
129
+ };
130
+ }
131
+ }
132
+ continue;
133
+ }
134
+ // If we got here without done or error, something's off
135
+ return;
136
+ }
137
+ // All providers failed — show specific errors
138
+ const errorDetail = errors.map(e => ` ${e.key}: ${e.error}`).join("\n");
139
+ yield {
140
+ type: "error",
141
+ error: `No provider available.\n${errorDetail}\n\nFix: alvin-bot setup | Telegram: /model`,
142
+ };
143
+ }
144
+ /**
145
+ * Reset to primary provider.
146
+ */
147
+ resetToDefault() {
148
+ this.activeKey = this.primaryKey;
149
+ }
150
+ // ── Private ─────────────────────────────────────────
151
+ createProvider(config) {
152
+ switch (config.type) {
153
+ case "claude-sdk":
154
+ return new ClaudeSDKProvider(config);
155
+ case "codex-cli":
156
+ return new CodexCLIProvider(config);
157
+ case "openai-compatible":
158
+ return new OpenAICompatibleProvider(config);
159
+ default:
160
+ throw new Error(`Unknown provider type: ${config.type}`);
161
+ }
162
+ }
163
+ }
164
+ /**
165
+ * Create a ProviderRegistry from a simple, user-friendly config.
166
+ * Auto-configures providers based on available API keys.
167
+ */
168
+ export function createRegistry(config) {
169
+ const providers = {};
170
+ // Register Codex CLI if referenced
171
+ if (config.primary === "codex-cli" || config.fallbacks?.includes("codex-cli")) {
172
+ providers["codex-cli"] = {
173
+ ...PROVIDER_PRESETS["codex-cli"],
174
+ type: "codex-cli",
175
+ name: "Codex CLI (OpenAI)",
176
+ model: "gpt-5.4",
177
+ };
178
+ }
179
+ // Always register Claude SDK if it's referenced
180
+ if (config.primary === "claude-sdk" || config.fallbacks?.includes("claude-sdk")) {
181
+ providers["claude-sdk"] = {
182
+ ...PROVIDER_PRESETS["claude-sdk"],
183
+ type: "claude-sdk",
184
+ name: "Claude (Agent SDK)",
185
+ model: "claude-opus-4-6",
186
+ };
187
+ }
188
+ // Auto-register Google Gemini (single model) if key is available
189
+ if (config.apiKeys?.google) {
190
+ providers["google"] = {
191
+ ...PROVIDER_PRESETS["gemini-2.5-flash"],
192
+ name: "Google Gemini",
193
+ apiKey: config.apiKeys.google,
194
+ };
195
+ }
196
+ // Always try to detect local Ollama
197
+ providers["ollama"] = {
198
+ ...PROVIDER_PRESETS["ollama"],
199
+ };
200
+ // Add custom providers
201
+ if (config.customProviders) {
202
+ Object.assign(providers, config.customProviders);
203
+ }
204
+ return new ProviderRegistry({
205
+ primary: config.primary,
206
+ fallbacks: config.fallbacks,
207
+ providers,
208
+ });
209
+ }
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Tool Executor — Executes tool calls for non-SDK providers.
3
+ *
4
+ * Provides core agent capabilities (shell, file read/write, web fetch)
5
+ * to any OpenAI-compatible provider that supports function calling.
6
+ *
7
+ * This bridges the gap between Claude SDK (built-in tools) and other
8
+ * providers (Groq, NVIDIA, Gemini, etc.) — giving them all agent powers.
9
+ */
10
+ import { execSync } from "child_process";
11
+ import fs from "fs";
12
+ import { resolve } from "path";
13
+ import { isSelfRestartCommand, scheduleGracefulRestart } from "../services/restart.js";
14
+ // ── Tool Definitions (OpenAI function calling format) ───────────────────────
15
+ export const AGENT_TOOLS = [
16
+ {
17
+ type: "function",
18
+ function: {
19
+ name: "run_shell",
20
+ description: "Execute a shell command and return the output. Use for: running CLI tools, checking system state, installing packages, processing files, git operations, etc. Timeout: 30 seconds.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {
24
+ command: {
25
+ type: "string",
26
+ description: "The shell command to execute (bash). Example: 'ls -la', 'which ffmpeg', 'curl wttr.in/Berlin'"
27
+ },
28
+ workingDir: {
29
+ type: "string",
30
+ description: "Working directory (optional, defaults to user's configured dir)"
31
+ }
32
+ },
33
+ required: ["command"],
34
+ },
35
+ },
36
+ },
37
+ {
38
+ type: "function",
39
+ function: {
40
+ name: "read_file",
41
+ description: "Read the contents of a file. Returns the text content. Use for: reading configs, code files, documents, logs, memory files, etc.",
42
+ parameters: {
43
+ type: "object",
44
+ properties: {
45
+ path: {
46
+ type: "string",
47
+ description: "Absolute or relative path to the file"
48
+ },
49
+ maxLines: {
50
+ type: "number",
51
+ description: "Maximum number of lines to read (optional, default: all)"
52
+ }
53
+ },
54
+ required: ["path"],
55
+ },
56
+ },
57
+ },
58
+ {
59
+ type: "function",
60
+ function: {
61
+ name: "write_file",
62
+ description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Use for: creating files, saving results, writing memory, updating configs.",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {
66
+ path: {
67
+ type: "string",
68
+ description: "Absolute or relative path to the file"
69
+ },
70
+ content: {
71
+ type: "string",
72
+ description: "Content to write"
73
+ },
74
+ append: {
75
+ type: "boolean",
76
+ description: "Append instead of overwrite (default: false)"
77
+ }
78
+ },
79
+ required: ["path", "content"],
80
+ },
81
+ },
82
+ },
83
+ {
84
+ type: "function",
85
+ function: {
86
+ name: "web_fetch",
87
+ description: "Fetch a URL and return the content as text/markdown. Use for: reading web pages, APIs, documentation, search results.",
88
+ parameters: {
89
+ type: "object",
90
+ properties: {
91
+ url: {
92
+ type: "string",
93
+ description: "URL to fetch (http or https)"
94
+ },
95
+ maxChars: {
96
+ type: "number",
97
+ description: "Maximum characters to return (default: 10000)"
98
+ }
99
+ },
100
+ required: ["url"],
101
+ },
102
+ },
103
+ },
104
+ {
105
+ type: "function",
106
+ function: {
107
+ name: "web_search",
108
+ description: "Search the web and return results. Use for: looking up information, finding answers, research.",
109
+ parameters: {
110
+ type: "object",
111
+ properties: {
112
+ query: {
113
+ type: "string",
114
+ description: "Search query"
115
+ }
116
+ },
117
+ required: ["query"],
118
+ },
119
+ },
120
+ },
121
+ {
122
+ type: "function",
123
+ function: {
124
+ name: "list_directory",
125
+ description: "List files and directories at a given path. Returns names, types (file/dir), and sizes. Use for: exploring project structures, finding files, checking what exists.",
126
+ parameters: {
127
+ type: "object",
128
+ properties: {
129
+ path: {
130
+ type: "string",
131
+ description: "Directory path to list (default: current working directory)"
132
+ },
133
+ recursive: {
134
+ type: "boolean",
135
+ description: "List recursively (max 3 levels deep, default: false)"
136
+ }
137
+ },
138
+ required: [],
139
+ },
140
+ },
141
+ },
142
+ {
143
+ type: "function",
144
+ function: {
145
+ name: "python_execute",
146
+ description: "Execute a Python 3 script and return stdout/stderr. Use for: data processing, creating Excel/CSV files, complex calculations, JSON/XML transformation, image processing, PDF generation, chart creation, and any task that benefits from Python libraries (openpyxl, pandas, matplotlib, Pillow, etc.).",
147
+ parameters: {
148
+ type: "object",
149
+ properties: {
150
+ code: {
151
+ type: "string",
152
+ description: "Python 3 code to execute. Can use installed pip packages. Use print() for output."
153
+ },
154
+ workingDir: {
155
+ type: "string",
156
+ description: "Working directory for the script (optional)"
157
+ }
158
+ },
159
+ required: ["code"],
160
+ },
161
+ },
162
+ },
163
+ {
164
+ type: "function",
165
+ function: {
166
+ name: "edit_file",
167
+ description: "Make a precise edit to a file by replacing exact text. More surgical than write_file — preserves the rest of the file. Use for: fixing bugs, updating configs, changing specific lines.",
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ path: {
172
+ type: "string",
173
+ description: "Path to the file to edit"
174
+ },
175
+ oldText: {
176
+ type: "string",
177
+ description: "Exact text to find (must match exactly including whitespace)"
178
+ },
179
+ newText: {
180
+ type: "string",
181
+ description: "Replacement text"
182
+ }
183
+ },
184
+ required: ["path", "oldText", "newText"],
185
+ },
186
+ },
187
+ },
188
+ ];
189
+ /**
190
+ * Execute a tool call and return the result.
191
+ */
192
+ export function executeTool(name, args, workingDir) {
193
+ try {
194
+ switch (name) {
195
+ case "run_shell":
196
+ return executeShell(args.command, args.workingDir || workingDir);
197
+ case "read_file":
198
+ return executeReadFile(args.path, args.maxLines, workingDir);
199
+ case "write_file":
200
+ return executeWriteFile(args.path, args.content, args.append, workingDir);
201
+ case "web_fetch":
202
+ return executeWebFetch(args.url, args.maxChars);
203
+ case "web_search":
204
+ return executeWebSearch(args.query);
205
+ case "list_directory":
206
+ return executeListDirectory(args.path || workingDir, args.recursive, workingDir);
207
+ case "python_execute":
208
+ return executePython(args.code, args.workingDir || workingDir);
209
+ case "edit_file":
210
+ return executeEditFile(args.path, args.oldText, args.newText, workingDir);
211
+ default:
212
+ return { name, result: `Unknown tool: ${name}`, error: true };
213
+ }
214
+ }
215
+ catch (err) {
216
+ return {
217
+ name,
218
+ result: `Error: ${err instanceof Error ? err.message : String(err)}`,
219
+ error: true,
220
+ };
221
+ }
222
+ }
223
+ // ── Individual Tool Implementations ─────────────────────────────────────────
224
+ function executeShell(command, cwd) {
225
+ // Intercept self-restart: use graceful internal restart instead of pm2 kill
226
+ if (isSelfRestartCommand(command)) {
227
+ scheduleGracefulRestart();
228
+ return { name: "run_shell", result: "Bot restart scheduled. Grammy will commit the Telegram offset before exiting." };
229
+ }
230
+ // Security: block obviously dangerous commands
231
+ const blocked = ["rm -rf /", "mkfs", "dd if=/dev/zero", "> /dev/sda"];
232
+ if (blocked.some(b => command.includes(b))) {
233
+ return { name: "run_shell", result: "Command blocked for safety.", error: true };
234
+ }
235
+ try {
236
+ const output = execSync(command, {
237
+ encoding: "utf-8",
238
+ cwd: cwd || process.cwd(),
239
+ timeout: 30_000,
240
+ maxBuffer: 1024 * 1024, // 1MB
241
+ env: { ...process.env, LANG: "en_US.UTF-8" },
242
+ });
243
+ // Truncate very long output
244
+ const truncated = output.length > 8000
245
+ ? output.substring(0, 8000) + `\n... (truncated, ${output.length} chars total)`
246
+ : output;
247
+ return { name: "run_shell", result: truncated || "(no output)" };
248
+ }
249
+ catch (err) {
250
+ const stderr = err.stderr ? err.stderr.toString().substring(0, 2000) : "";
251
+ const stdout = err.stdout ? err.stdout.toString().substring(0, 2000) : "";
252
+ return {
253
+ name: "run_shell",
254
+ result: `Exit code ${err.status || 1}\n${stdout}\n${stderr}`.trim(),
255
+ error: true,
256
+ };
257
+ }
258
+ }
259
+ function executeReadFile(path, maxLines, cwd) {
260
+ const fullPath = path.startsWith("/") ? path : resolve(cwd || process.cwd(), path);
261
+ try {
262
+ let content = fs.readFileSync(fullPath, "utf-8");
263
+ if (maxLines && maxLines > 0) {
264
+ const lines = content.split("\n");
265
+ if (lines.length > maxLines) {
266
+ content = lines.slice(0, maxLines).join("\n") + `\n... (${lines.length} lines total)`;
267
+ }
268
+ }
269
+ if (content.length > 20000) {
270
+ content = content.substring(0, 20000) + `\n... (truncated, ${content.length} chars)`;
271
+ }
272
+ return { name: "read_file", result: content };
273
+ }
274
+ catch (err) {
275
+ return { name: "read_file", result: `File not found or not readable: ${fullPath}`, error: true };
276
+ }
277
+ }
278
+ function executeWriteFile(path, content, append, cwd) {
279
+ const fullPath = path.startsWith("/") ? path : resolve(cwd || process.cwd(), path);
280
+ try {
281
+ // Ensure directory exists
282
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
283
+ if (dir && !fs.existsSync(dir)) {
284
+ fs.mkdirSync(dir, { recursive: true });
285
+ }
286
+ if (append) {
287
+ fs.appendFileSync(fullPath, content);
288
+ }
289
+ else {
290
+ fs.writeFileSync(fullPath, content);
291
+ }
292
+ return { name: "write_file", result: `✅ Written to ${fullPath} (${content.length} chars)` };
293
+ }
294
+ catch (err) {
295
+ return { name: "write_file", result: `Write failed: ${err instanceof Error ? err.message : err}`, error: true };
296
+ }
297
+ }
298
+ function executeWebFetch(url, maxChars) {
299
+ try {
300
+ // Use curl for simplicity and reliability
301
+ const max = maxChars || 10000;
302
+ const output = execSync(`curl -sL --max-time 15 --max-filesize 5000000 "${url}" | head -c ${max * 2}`, { encoding: "utf-8", timeout: 20_000, maxBuffer: 5 * 1024 * 1024 });
303
+ // Basic HTML → text conversion
304
+ let text = output
305
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
306
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
307
+ .replace(/<[^>]+>/g, " ")
308
+ .replace(/\s+/g, " ")
309
+ .trim();
310
+ if (text.length > max)
311
+ text = text.substring(0, max) + "...";
312
+ return { name: "web_fetch", result: text || "(empty response)" };
313
+ }
314
+ catch (err) {
315
+ return { name: "web_fetch", result: `Fetch failed: ${err instanceof Error ? err.message : err}`, error: true };
316
+ }
317
+ }
318
+ function executeWebSearch(query) {
319
+ try {
320
+ // Use DuckDuckGo instant answer API (no key needed)
321
+ const encoded = encodeURIComponent(query);
322
+ const output = execSync(`curl -sL "https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1&skip_disambig=1"`, { encoding: "utf-8", timeout: 10_000 });
323
+ const data = JSON.parse(output);
324
+ const results = [];
325
+ if (data.AbstractText) {
326
+ results.push(`📝 ${data.AbstractText}`);
327
+ if (data.AbstractURL)
328
+ results.push(` Source: ${data.AbstractURL}`);
329
+ }
330
+ if (data.RelatedTopics) {
331
+ for (const topic of data.RelatedTopics.slice(0, 5)) {
332
+ if (topic.Text) {
333
+ results.push(`• ${topic.Text}`);
334
+ if (topic.FirstURL)
335
+ results.push(` ${topic.FirstURL}`);
336
+ }
337
+ }
338
+ }
339
+ if (results.length === 0) {
340
+ // Fallback: use curl with a search engine
341
+ const fallback = execSync(`curl -sL "https://html.duckduckgo.com/html/?q=${encoded}" | grep -oP '<a rel="nofollow" class="result__a" href="[^"]*">[^<]*</a>' | head -5 | sed 's/<[^>]*>//g'`, { encoding: "utf-8", timeout: 10_000 }).trim();
342
+ if (fallback)
343
+ return { name: "web_search", result: fallback };
344
+ return { name: "web_search", result: `No results for "${query}". Try a different query or use web_fetch with a specific URL.` };
345
+ }
346
+ return { name: "web_search", result: results.join("\n") };
347
+ }
348
+ catch (err) {
349
+ return { name: "web_search", result: `Search failed: ${err instanceof Error ? err.message : err}`, error: true };
350
+ }
351
+ }
352
+ function executeListDirectory(dirPath, recursive, cwd) {
353
+ const fullPath = dirPath?.startsWith("/") ? dirPath : resolve(cwd || process.cwd(), dirPath || ".");
354
+ try {
355
+ if (!fs.existsSync(fullPath)) {
356
+ return { name: "list_directory", result: `Directory not found: ${fullPath}`, error: true };
357
+ }
358
+ const entries = [];
359
+ function listDir(dir, depth) {
360
+ const items = fs.readdirSync(dir, { withFileTypes: true });
361
+ const indent = " ".repeat(depth);
362
+ for (const item of items) {
363
+ if (item.name.startsWith(".") && depth === 0 && items.length > 20)
364
+ continue; // skip dotfiles in large dirs
365
+ const itemPath = resolve(dir, item.name);
366
+ if (item.isDirectory()) {
367
+ entries.push(`${indent}📁 ${item.name}/`);
368
+ if (recursive && depth < 3) {
369
+ listDir(itemPath, depth + 1);
370
+ }
371
+ }
372
+ else {
373
+ try {
374
+ const stats = fs.statSync(itemPath);
375
+ const size = stats.size < 1024 ? `${stats.size}B`
376
+ : stats.size < 1048576 ? `${(stats.size / 1024).toFixed(1)}KB`
377
+ : `${(stats.size / 1048576).toFixed(1)}MB`;
378
+ entries.push(`${indent}📄 ${item.name} (${size})`);
379
+ }
380
+ catch {
381
+ entries.push(`${indent}📄 ${item.name}`);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ listDir(fullPath, 0);
387
+ const result = entries.length > 0
388
+ ? `${fullPath}:\n${entries.join("\n")}`
389
+ : `${fullPath}: (empty directory)`;
390
+ // Truncate if huge
391
+ return { name: "list_directory", result: result.length > 8000 ? result.substring(0, 8000) + "\n..." : result };
392
+ }
393
+ catch (err) {
394
+ return { name: "list_directory", result: `Error listing directory: ${err instanceof Error ? err.message : err}`, error: true };
395
+ }
396
+ }
397
+ function executePython(code, cwd) {
398
+ try {
399
+ // Write code to temp file to avoid shell escaping issues
400
+ const tmpFile = `/tmp/alvin-bot-py-${Date.now()}.py`;
401
+ fs.writeFileSync(tmpFile, code);
402
+ try {
403
+ const output = execSync(`python3 "${tmpFile}"`, {
404
+ encoding: "utf-8",
405
+ cwd: cwd || process.cwd(),
406
+ timeout: 60_000, // 60s for Python (may need to install packages, process data)
407
+ maxBuffer: 5 * 1024 * 1024, // 5MB
408
+ env: { ...process.env, LANG: "en_US.UTF-8", PYTHONIOENCODING: "utf-8" },
409
+ });
410
+ const truncated = output.length > 10000
411
+ ? output.substring(0, 10000) + `\n... (truncated, ${output.length} chars total)`
412
+ : output;
413
+ return { name: "python_execute", result: truncated || "(no output)" };
414
+ }
415
+ finally {
416
+ // Cleanup temp file
417
+ try {
418
+ fs.unlinkSync(tmpFile);
419
+ }
420
+ catch { /* ignore */ }
421
+ }
422
+ }
423
+ catch (err) {
424
+ const stderr = err.stderr ? err.stderr.toString().substring(0, 3000) : "";
425
+ const stdout = err.stdout ? err.stdout.toString().substring(0, 3000) : "";
426
+ return {
427
+ name: "python_execute",
428
+ result: `Python error (exit ${err.status || 1}):\n${stderr}\n${stdout}`.trim(),
429
+ error: true,
430
+ };
431
+ }
432
+ }
433
+ function executeEditFile(filePath, oldText, newText, cwd) {
434
+ const fullPath = filePath.startsWith("/") ? filePath : resolve(cwd || process.cwd(), filePath);
435
+ try {
436
+ if (!fs.existsSync(fullPath)) {
437
+ return { name: "edit_file", result: `File not found: ${fullPath}`, error: true };
438
+ }
439
+ const content = fs.readFileSync(fullPath, "utf-8");
440
+ if (!content.includes(oldText)) {
441
+ return { name: "edit_file", result: `oldText not found in ${fullPath}. Make sure it matches exactly (including whitespace).`, error: true };
442
+ }
443
+ const newContent = content.replace(oldText, newText);
444
+ fs.writeFileSync(fullPath, newContent);
445
+ return { name: "edit_file", result: `✅ Edited ${fullPath} — replaced ${oldText.length} chars with ${newText.length} chars` };
446
+ }
447
+ catch (err) {
448
+ return { name: "edit_file", result: `Edit failed: ${err instanceof Error ? err.message : err}`, error: true };
449
+ }
450
+ }