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,507 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Alvin Bot TUI — Terminal Chat Interface
4
+ *
5
+ * A full-screen terminal UI that connects to the running Alvin Bot instance
6
+ * via WebSocket (same as Web UI). Features:
7
+ *
8
+ * - Streaming chat with AI responses
9
+ * - Tool use indicators
10
+ * - Model switching (/model)
11
+ * - Status bar (model, cost, uptime)
12
+ * - Color-coded messages
13
+ * - Input history (↑/↓)
14
+ * - Multi-line input (Shift+Enter)
15
+ * - i18n: English (default) / German (--lang de or ALVIN_LANG=de)
16
+ *
17
+ * Usage: alvin-bot tui [--port 3100] [--host localhost] [--lang en|de]
18
+ */
19
+ import { createInterface } from "readline";
20
+ import WebSocket from "ws";
21
+ import http from "http";
22
+ import { initI18n, t } from "../i18n.js";
23
+ // Init i18n before anything else
24
+ initI18n();
25
+ // ── ANSI Colors & Styles ────────────────────────────────
26
+ const C = {
27
+ reset: "\x1b[0m",
28
+ bold: "\x1b[1m",
29
+ dim: "\x1b[2m",
30
+ italic: "\x1b[3m",
31
+ underline: "\x1b[4m",
32
+ black: "\x1b[30m",
33
+ red: "\x1b[31m",
34
+ green: "\x1b[32m",
35
+ yellow: "\x1b[33m",
36
+ blue: "\x1b[34m",
37
+ magenta: "\x1b[35m",
38
+ cyan: "\x1b[36m",
39
+ white: "\x1b[37m",
40
+ gray: "\x1b[90m",
41
+ brightRed: "\x1b[91m",
42
+ brightGreen: "\x1b[92m",
43
+ brightYellow: "\x1b[93m",
44
+ brightBlue: "\x1b[94m",
45
+ brightMagenta: "\x1b[95m",
46
+ brightCyan: "\x1b[96m",
47
+ brightWhite: "\x1b[97m",
48
+ bgBlack: "\x1b[40m",
49
+ bgBlue: "\x1b[44m",
50
+ bgMagenta: "\x1b[45m",
51
+ bgGray: "\x1b[100m",
52
+ };
53
+ // ── State ───────────────────────────────────────────────
54
+ let ws = null;
55
+ let rl;
56
+ let connected = false;
57
+ let currentModel = "loading...";
58
+ let totalCost = 0;
59
+ let isStreaming = false;
60
+ let currentResponse = "";
61
+ let currentToolName = "";
62
+ let toolCount = 0;
63
+ const inputHistory = [];
64
+ let historyIndex = -1;
65
+ const host = process.argv.includes("--host")
66
+ ? process.argv[process.argv.indexOf("--host") + 1] || "localhost"
67
+ : "localhost";
68
+ const port = process.argv.includes("--port")
69
+ ? parseInt(process.argv[process.argv.indexOf("--port") + 1]) || 3100
70
+ : 3100;
71
+ const baseUrl = `http://${host}:${port}`;
72
+ const wsUrl = `ws://${host}:${port}`;
73
+ // Track header line count for redraw
74
+ const HEADER_LINES = 3;
75
+ // ── Screen Drawing ──────────────────────────────────────
76
+ function getWidth() {
77
+ return process.stdout.columns || 80;
78
+ }
79
+ function clearLine() {
80
+ process.stdout.write("\r\x1b[K");
81
+ }
82
+ function drawHeader() {
83
+ const w = getWidth();
84
+ const statusDot = connected ? `${C.brightGreen}●${C.reset}` : `${C.red}●${C.reset}`;
85
+ const status = connected ? t("tui.connected") : t("tui.disconnected");
86
+ const modelStr = `${C.brightMagenta}${currentModel}${C.reset}`;
87
+ const costStr = totalCost > 0 ? ` ${C.gray}· $${totalCost.toFixed(4)}${C.reset}` : "";
88
+ const title = `${C.bold}${C.brightCyan}${t("tui.title")}${C.reset}`;
89
+ const right = `${statusDot} ${status} ${C.gray}│${C.reset} ${modelStr}${costStr}`;
90
+ console.log(`${C.gray}${"─".repeat(w)}${C.reset}`);
91
+ console.log(` ${title}${"".padEnd(10)}${right}`);
92
+ console.log(`${C.gray}${"─".repeat(w)}${C.reset}`);
93
+ }
94
+ function redrawHeader() {
95
+ // Save cursor, move to top, redraw header, restore cursor
96
+ process.stdout.write("\x1b7"); // Save cursor position
97
+ process.stdout.write("\x1b[H"); // Move to top-left (1,1)
98
+ // Clear the 3 header lines
99
+ for (let i = 0; i < HEADER_LINES; i++) {
100
+ process.stdout.write("\x1b[K"); // Clear line
101
+ if (i < HEADER_LINES - 1)
102
+ process.stdout.write("\x1b[1B"); // Move down
103
+ }
104
+ process.stdout.write("\x1b[H"); // Back to top
105
+ drawHeader();
106
+ process.stdout.write("\x1b8"); // Restore cursor position
107
+ }
108
+ function drawHelp() {
109
+ console.log(`
110
+ ${C.bold}${t("help.title")}${C.reset}
111
+ ${C.cyan}/model${C.reset} ${t("help.model")}
112
+ ${C.cyan}/status${C.reset} ${t("help.status")}
113
+ ${C.cyan}/clear${C.reset} ${t("help.clear")}
114
+ ${C.cyan}/cron${C.reset} ${t("help.cron")}
115
+ ${C.cyan}/doctor${C.reset} ${t("help.doctor")}
116
+ ${C.cyan}/backup${C.reset} ${t("help.backup")}
117
+ ${C.cyan}/restart${C.reset} ${t("help.restart")}
118
+ ${C.cyan}/help${C.reset} ${t("help.help")}
119
+ ${C.cyan}/quit${C.reset} ${t("help.quit")}
120
+
121
+ ${C.dim}${t("help.footer")}${C.reset}
122
+ `);
123
+ }
124
+ function printUser(text) {
125
+ console.log(`\n${C.bold}${C.brightGreen}${t("tui.you")}:${C.reset} ${text}`);
126
+ }
127
+ function printAssistantStart() {
128
+ process.stdout.write(`\n${C.bold}${C.brightBlue}Alvin Bot:${C.reset} `);
129
+ }
130
+ function printAssistantDelta(text) {
131
+ process.stdout.write(text);
132
+ }
133
+ function printAssistantEnd(cost) {
134
+ const costStr = cost && cost > 0 ? ` ${C.dim}($${cost.toFixed(4)})${C.reset}` : "";
135
+ console.log(costStr);
136
+ }
137
+ function printTool(name) {
138
+ clearLine();
139
+ process.stdout.write(`\r ${C.yellow}⚙ ${name}...${C.reset}`);
140
+ }
141
+ function printToolDone() {
142
+ clearLine();
143
+ if (toolCount > 0) {
144
+ const label = toolCount > 1 ? t("tui.toolsUsed") : t("tui.toolUsed");
145
+ console.log(` ${C.dim}${C.yellow}⚙ ${toolCount} ${label}${C.reset}`);
146
+ }
147
+ toolCount = 0;
148
+ }
149
+ function printError(msg) {
150
+ console.log(`\n${C.red}✖ ${msg}${C.reset}`);
151
+ }
152
+ function printInfo(msg) {
153
+ console.log(`${C.cyan}ℹ ${msg}${C.reset}`);
154
+ }
155
+ function printSuccess(msg) {
156
+ console.log(`${C.green}✔ ${msg}${C.reset}`);
157
+ }
158
+ function showPrompt() {
159
+ if (!isStreaming) {
160
+ rl.setPrompt(`${C.brightGreen}❯${C.reset} `);
161
+ rl.prompt();
162
+ }
163
+ }
164
+ // ── WebSocket Connection ────────────────────────────────
165
+ function connectWebSocket() {
166
+ ws = new WebSocket(wsUrl);
167
+ ws.on("open", () => {
168
+ connected = true;
169
+ redrawHeader();
170
+ printInfo(t("tui.connectedTo"));
171
+ showPrompt();
172
+ });
173
+ ws.on("message", (data) => {
174
+ try {
175
+ const msg = JSON.parse(data.toString());
176
+ handleMessage(msg);
177
+ }
178
+ catch { /* ignore */ }
179
+ });
180
+ ws.on("close", () => {
181
+ connected = false;
182
+ isStreaming = false;
183
+ redrawHeader();
184
+ printError(t("tui.connectionLost"));
185
+ setTimeout(connectWebSocket, 3000);
186
+ });
187
+ ws.on("error", () => {
188
+ // Error is followed by close event
189
+ });
190
+ }
191
+ function handleMessage(msg) {
192
+ switch (msg.type) {
193
+ case "text":
194
+ if (!isStreaming) {
195
+ isStreaming = true;
196
+ if (currentToolName) {
197
+ printToolDone();
198
+ currentToolName = "";
199
+ }
200
+ printAssistantStart();
201
+ }
202
+ if (msg.delta) {
203
+ printAssistantDelta(msg.delta);
204
+ currentResponse += msg.delta;
205
+ }
206
+ break;
207
+ case "tool":
208
+ if (!isStreaming)
209
+ isStreaming = true;
210
+ toolCount++;
211
+ currentToolName = msg.name || "tool";
212
+ printTool(currentToolName);
213
+ break;
214
+ case "fallback":
215
+ printInfo(`${t("tui.fallback")} ${msg.from} → ${msg.to}`);
216
+ break;
217
+ case "done":
218
+ if (isStreaming) {
219
+ printAssistantEnd(msg.cost);
220
+ }
221
+ if (msg.cost)
222
+ totalCost += msg.cost;
223
+ isStreaming = false;
224
+ currentResponse = "";
225
+ currentToolName = "";
226
+ redrawHeader(); // Update cost in header
227
+ showPrompt();
228
+ break;
229
+ case "error":
230
+ printError(msg.error || "Unknown error");
231
+ isStreaming = false;
232
+ showPrompt();
233
+ break;
234
+ case "reset":
235
+ printInfo(t("tui.sessionReset"));
236
+ showPrompt();
237
+ break;
238
+ }
239
+ }
240
+ // ── API Calls ───────────────────────────────────────────
241
+ async function apiGet(path) {
242
+ return new Promise((resolve, reject) => {
243
+ http.get(`${baseUrl}${path}`, (res) => {
244
+ let data = "";
245
+ res.on("data", (c) => data += c);
246
+ res.on("end", () => {
247
+ try {
248
+ resolve(JSON.parse(data));
249
+ }
250
+ catch {
251
+ reject(new Error("Invalid JSON"));
252
+ }
253
+ });
254
+ }).on("error", reject);
255
+ });
256
+ }
257
+ async function apiPost(path, body) {
258
+ return new Promise((resolve, reject) => {
259
+ const postData = JSON.stringify(body);
260
+ const req = http.request(`${baseUrl}${path}`, {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) },
263
+ }, (res) => {
264
+ let data = "";
265
+ res.on("data", (c) => data += c);
266
+ res.on("end", () => {
267
+ try {
268
+ resolve(JSON.parse(data));
269
+ }
270
+ catch {
271
+ reject(new Error("Invalid JSON"));
272
+ }
273
+ });
274
+ });
275
+ req.on("error", reject);
276
+ req.write(postData);
277
+ req.end();
278
+ });
279
+ }
280
+ // ── Commands ────────────────────────────────────────────
281
+ async function handleCommand(cmd) {
282
+ const parts = cmd.slice(1).split(/\s+/);
283
+ const command = parts[0].toLowerCase();
284
+ switch (command) {
285
+ case "help":
286
+ case "h":
287
+ drawHelp();
288
+ break;
289
+ case "model":
290
+ case "m": {
291
+ try {
292
+ const data = await apiGet("/api/models");
293
+ console.log(`\n${C.bold}${t("tui.models")}:${C.reset}`);
294
+ if (data.models) {
295
+ for (const m of data.models) {
296
+ const active = m.key === data.active ? `${C.brightGreen} ◀ ${t("tui.active")}${C.reset}` : "";
297
+ const status = m.status === "ready" ? `${C.green}✓${C.reset}` : `${C.dim}✗${C.reset}`;
298
+ console.log(` ${status} ${C.bold}${m.key}${C.reset} ${C.dim}(${m.model || m.name})${C.reset}${active}`);
299
+ }
300
+ }
301
+ console.log(`\n${C.dim}${t("tui.switchModel")} /model <key>${C.reset}`);
302
+ if (parts[1]) {
303
+ const res = await apiPost("/api/models/switch", { key: parts[1] });
304
+ if (res.ok) {
305
+ currentModel = res.active || parts[1];
306
+ printSuccess(`${t("tui.switchedTo")}: ${currentModel}`);
307
+ redrawHeader();
308
+ }
309
+ else {
310
+ printError(res.error || t("tui.switchError"));
311
+ }
312
+ }
313
+ }
314
+ catch (err) {
315
+ printError(`${t("tui.modelsError")}: ${err.message}`);
316
+ }
317
+ break;
318
+ }
319
+ case "status":
320
+ case "s": {
321
+ try {
322
+ const data = await apiGet("/api/status");
323
+ console.log(`\n${C.bold}${C.brightCyan}${t("status.title")}${C.reset}`);
324
+ console.log(`${C.gray}${"─".repeat(40)}${C.reset}`);
325
+ if (data.model) {
326
+ console.log(` ${C.cyan}${t("status.model")}${C.reset} ${data.model.model || data.model.name || "?"}`);
327
+ console.log(` ${C.cyan}${t("status.provider")}${C.reset} ${data.model.name || "?"}`);
328
+ console.log(` ${C.cyan}${t("status.status")}${C.reset} ${data.model.status || "?"}`);
329
+ }
330
+ if (data.bot) {
331
+ const upH = Math.floor((data.bot.uptime || 0) / 3600);
332
+ const upM = Math.floor(((data.bot.uptime || 0) % 3600) / 60);
333
+ console.log(` ${C.cyan}${t("status.version")}${C.reset} ${data.bot.version || "?"}`);
334
+ console.log(` ${C.cyan}${t("status.uptime")}${C.reset} ${upH}h ${upM}m`);
335
+ }
336
+ if (data.memory) {
337
+ console.log(` ${C.cyan}${t("status.memory")}${C.reset} ${data.memory.vectors || 0} ${t("status.embeddings")}`);
338
+ }
339
+ console.log(` ${C.cyan}${t("status.plugins")}${C.reset} ${data.plugins || 0}`);
340
+ console.log(` ${C.cyan}${t("status.tools")}${C.reset} ${data.tools || 0}`);
341
+ console.log(` ${C.cyan}${t("status.users")}${C.reset} ${data.users || 0}`);
342
+ console.log("");
343
+ }
344
+ catch (err) {
345
+ printError(`${t("tui.statusError")}: ${err.message}`);
346
+ }
347
+ break;
348
+ }
349
+ case "cron": {
350
+ try {
351
+ const data = await apiGet("/api/cron");
352
+ console.log(`\n${C.bold}Cron Jobs${C.reset}`);
353
+ console.log(`${C.gray}${"─".repeat(40)}${C.reset}`);
354
+ if (!data.jobs || data.jobs.length === 0) {
355
+ console.log(` ${C.dim}${t("tui.noCronJobs")}${C.reset}`);
356
+ }
357
+ else {
358
+ for (const job of data.jobs) {
359
+ const status = job.enabled ? `${C.green}●${C.reset}` : `${C.red}●${C.reset}`;
360
+ const schedule = job.schedule || job.interval || "?";
361
+ console.log(` ${status} ${C.bold}${job.name}${C.reset} ${C.dim}(${schedule})${C.reset} — ${job.type}`);
362
+ }
363
+ }
364
+ console.log("");
365
+ }
366
+ catch (err) {
367
+ printError(`${t("tui.cronError")}: ${err.message}`);
368
+ }
369
+ break;
370
+ }
371
+ case "doctor": {
372
+ try {
373
+ printInfo(t("tui.scanning"));
374
+ const data = await apiGet("/api/doctor");
375
+ const icons = { error: `${C.red}✖`, warning: `${C.yellow}⚠`, info: `${C.blue}ℹ` };
376
+ console.log(`\n${C.bold}Health-Check${C.reset}`);
377
+ console.log(`${C.gray}${"─".repeat(40)}${C.reset}`);
378
+ for (const issue of data.issues || []) {
379
+ const icon = icons[issue.severity] || "?";
380
+ console.log(` ${icon} ${C.bold}${issue.category}${C.reset} — ${issue.message}${C.reset}`);
381
+ if (issue.fix)
382
+ console.log(` ${C.dim}💡 ${issue.fix}${C.reset}`);
383
+ }
384
+ console.log("");
385
+ }
386
+ catch (err) {
387
+ printError(`${t("tui.doctorError")}: ${err.message}`);
388
+ }
389
+ break;
390
+ }
391
+ case "backup": {
392
+ try {
393
+ printInfo(t("tui.creatingBackup"));
394
+ const data = await apiPost("/api/backups/create", {});
395
+ if (data.ok) {
396
+ printSuccess(`${t("tui.backupCreated")} "${data.id}" (${data.files.length} files)`);
397
+ }
398
+ else {
399
+ printError(data.error || t("tui.backupFailed"));
400
+ }
401
+ }
402
+ catch (err) {
403
+ printError(`${t("tui.backupError")}: ${err.message}`);
404
+ }
405
+ break;
406
+ }
407
+ case "restart": {
408
+ printInfo(t("tui.botRestarting"));
409
+ try {
410
+ await apiPost("/api/restart", {});
411
+ printSuccess(t("tui.restartTriggered"));
412
+ }
413
+ catch {
414
+ printError(t("tui.restartFailed"));
415
+ }
416
+ break;
417
+ }
418
+ case "clear":
419
+ case "c":
420
+ console.clear();
421
+ drawHeader();
422
+ if (ws?.readyState === WebSocket.OPEN) {
423
+ ws.send(JSON.stringify({ type: "reset" }));
424
+ }
425
+ break;
426
+ case "quit":
427
+ case "q":
428
+ case "exit":
429
+ console.log(`\n${C.dim}${t("tui.bye")}${C.reset}\n`);
430
+ process.exit(0);
431
+ break;
432
+ default:
433
+ sendChat(cmd);
434
+ return;
435
+ }
436
+ showPrompt();
437
+ }
438
+ function sendChat(text) {
439
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
440
+ printError(t("tui.notConnected"));
441
+ showPrompt();
442
+ return;
443
+ }
444
+ printUser(text);
445
+ ws.send(JSON.stringify({ type: "chat", text }));
446
+ if (inputHistory[0] !== text) {
447
+ inputHistory.unshift(text);
448
+ if (inputHistory.length > 100)
449
+ inputHistory.pop();
450
+ }
451
+ historyIndex = -1;
452
+ }
453
+ // ── Init ────────────────────────────────────────────────
454
+ async function fetchInitialModel() {
455
+ try {
456
+ const data = await apiGet("/api/status");
457
+ if (data.model?.model) {
458
+ currentModel = data.model.model;
459
+ }
460
+ else if (data.model?.name) {
461
+ currentModel = data.model.name;
462
+ }
463
+ }
464
+ catch { /* will get it on connect */ }
465
+ }
466
+ export async function startTUI() {
467
+ console.clear();
468
+ drawHeader();
469
+ console.log(`${C.dim}${t("tui.connecting")} ${baseUrl}...${C.reset}\n`);
470
+ drawHelp();
471
+ rl = createInterface({
472
+ input: process.stdin,
473
+ output: process.stdout,
474
+ terminal: true,
475
+ historySize: 100,
476
+ });
477
+ rl.on("line", (line) => {
478
+ const text = line.trim();
479
+ if (!text) {
480
+ showPrompt();
481
+ return;
482
+ }
483
+ if (text.startsWith("/")) {
484
+ handleCommand(text);
485
+ }
486
+ else {
487
+ sendChat(text);
488
+ }
489
+ });
490
+ rl.on("close", () => {
491
+ console.log(`\n${C.dim}${t("tui.bye")}${C.reset}\n`);
492
+ process.exit(0);
493
+ });
494
+ process.on("SIGINT", () => {
495
+ console.log(`\n${C.dim}${t("tui.bye")}${C.reset}\n`);
496
+ process.exit(0);
497
+ });
498
+ if (process.stdin.isTTY) {
499
+ process.stdin.setRawMode(false);
500
+ }
501
+ await fetchInitialModel();
502
+ connectWebSocket();
503
+ }
504
+ const isDirectRun = process.argv[1]?.includes("tui");
505
+ if (isDirectRun) {
506
+ startTUI().catch(console.error);
507
+ }
@@ -0,0 +1,30 @@
1
+ import { WebSocket } from "ws";
2
+ const canvasClients = new Set();
3
+ export function addCanvasClient(ws) {
4
+ canvasClients.add(ws);
5
+ ws.on("close", () => canvasClients.delete(ws));
6
+ }
7
+ export function canvasPresent(html) {
8
+ const msg = JSON.stringify({ type: "present", html });
9
+ for (const ws of canvasClients) {
10
+ if (ws.readyState === WebSocket.OPEN)
11
+ ws.send(msg);
12
+ }
13
+ }
14
+ export function canvasEval(js) {
15
+ const msg = JSON.stringify({ type: "eval", js });
16
+ for (const ws of canvasClients) {
17
+ if (ws.readyState === WebSocket.OPEN)
18
+ ws.send(msg);
19
+ }
20
+ }
21
+ export function canvasClear() {
22
+ const msg = JSON.stringify({ type: "clear" });
23
+ for (const ws of canvasClients) {
24
+ if (ws.readyState === WebSocket.OPEN)
25
+ ws.send(msg);
26
+ }
27
+ }
28
+ export function getCanvasClientCount() {
29
+ return canvasClients.size;
30
+ }