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.
- package/.env.example +43 -0
- package/BACKLOG.md +223 -0
- package/CHANGELOG.md +63 -0
- package/CLAUDE.example.md +152 -0
- package/CODE_OF_CONDUCT.md +52 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/SECURITY.md +38 -0
- package/SOUL.example.md +60 -0
- package/TOOLS.example.md +42 -0
- package/alvin-bot.config.example.json +24 -0
- package/bin/cli.js +1088 -0
- package/dist/.metadata_never_index +0 -0
- package/dist/claude.js +102 -0
- package/dist/config.js +65 -0
- package/dist/engine.js +90 -0
- package/dist/find-claude-binary.js +98 -0
- package/dist/handlers/commands.js +1489 -0
- package/dist/handlers/document.js +187 -0
- package/dist/handlers/message.js +200 -0
- package/dist/handlers/photo.js +154 -0
- package/dist/handlers/platform-message.js +275 -0
- package/dist/handlers/video.js +237 -0
- package/dist/handlers/voice.js +148 -0
- package/dist/i18n.js +299 -0
- package/dist/index.js +442 -0
- package/dist/init-data-dir.js +81 -0
- package/dist/middleware/auth.js +215 -0
- package/dist/migrate.js +139 -0
- package/dist/paths.js +87 -0
- package/dist/platforms/discord.js +161 -0
- package/dist/platforms/index.js +130 -0
- package/dist/platforms/signal.js +205 -0
- package/dist/platforms/slack.js +318 -0
- package/dist/platforms/telegram.js +111 -0
- package/dist/platforms/types.js +8 -0
- package/dist/platforms/whatsapp.js +648 -0
- package/dist/providers/claude-sdk-provider.js +173 -0
- package/dist/providers/codex-cli-provider.js +121 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai-compatible.js +388 -0
- package/dist/providers/registry.js +209 -0
- package/dist/providers/tool-executor.js +450 -0
- package/dist/providers/types.js +205 -0
- package/dist/services/access.js +144 -0
- package/dist/services/asset-index.js +230 -0
- package/dist/services/browser-manager.js +161 -0
- package/dist/services/browser.js +121 -0
- package/dist/services/compaction.js +129 -0
- package/dist/services/cron.js +462 -0
- package/dist/services/custom-tools.js +317 -0
- package/dist/services/delivery-queue.js +154 -0
- package/dist/services/elevenlabs.js +58 -0
- package/dist/services/embeddings.js +386 -0
- package/dist/services/exec-guard.js +46 -0
- package/dist/services/fallback-order.js +151 -0
- package/dist/services/heartbeat.js +192 -0
- package/dist/services/hooks.js +44 -0
- package/dist/services/imagegen.js +72 -0
- package/dist/services/language-detect.js +144 -0
- package/dist/services/markdown.js +63 -0
- package/dist/services/mcp.js +252 -0
- package/dist/services/memory.js +133 -0
- package/dist/services/personality.js +227 -0
- package/dist/services/plugins.js +171 -0
- package/dist/services/reminders.js +97 -0
- package/dist/services/restart.js +48 -0
- package/dist/services/security-audit.js +66 -0
- package/dist/services/self-search.js +129 -0
- package/dist/services/session.js +93 -0
- package/dist/services/skills.js +287 -0
- package/dist/services/standing-orders.js +29 -0
- package/dist/services/subagents.js +142 -0
- package/dist/services/sudo.js +243 -0
- package/dist/services/telegram.js +113 -0
- package/dist/services/tool-discovery.js +214 -0
- package/dist/services/usage-tracker.js +137 -0
- package/dist/services/users.js +199 -0
- package/dist/services/voice.js +95 -0
- package/dist/tui/index.js +507 -0
- package/dist/web/canvas.js +30 -0
- package/dist/web/doctor-api.js +606 -0
- package/dist/web/openai-compat.js +252 -0
- package/dist/web/server.js +1351 -0
- package/dist/web/setup-api.js +1078 -0
- package/docs/mcp.example.json +16 -0
- package/docs/screenshots/00-Login.png +0 -0
- package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
- package/docs/screenshots/02-Chat.png +0 -0
- package/docs/screenshots/03-Dashboard-Overview.png +0 -0
- package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
- package/docs/screenshots/05-Personality-Editor.png +0 -0
- package/docs/screenshots/06-Memory-Manager.png +0 -0
- package/docs/screenshots/07-Active-Sessions.png +0 -0
- package/docs/screenshots/08-File-Browser.png +0 -0
- package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
- package/docs/screenshots/10-Custom-Tools.png +0 -0
- package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
- package/docs/screenshots/12-Messaging-Platforms.png +0 -0
- package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
- package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
- package/docs/screenshots/13-User-Management.png +0 -0
- package/docs/screenshots/14-Web-Terminal.png +0 -0
- package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
- package/docs/screenshots/16-Settings-and-Env.png +0 -0
- package/docs/screenshots/TG-commands.png +0 -0
- package/docs/screenshots/TG.png +0 -0
- package/docs/screenshots/_Mac-Installer.png +0 -0
- package/docs/tools.example.json +33 -0
- package/install.sh +165 -0
- package/package.json +190 -0
- package/plugins/calendar/index.js +270 -0
- package/plugins/email/index.js +231 -0
- package/plugins/finance/index.js +254 -0
- package/plugins/notes/index.js +227 -0
- package/plugins/smarthome/index.js +230 -0
- package/plugins/weather/index.js +122 -0
- package/skills/apple-notes/SKILL.md +31 -0
- package/skills/browse/SKILL.md +136 -0
- package/skills/code-project/SKILL.md +43 -0
- package/skills/data-analysis/SKILL.md +39 -0
- package/skills/document-creation/SKILL.md +48 -0
- package/skills/email-summary/SKILL.md +46 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/summarize/SKILL.md +28 -0
- package/skills/system-admin/SKILL.md +39 -0
- package/skills/weather/SKILL.md +34 -0
- package/skills/web-research/SKILL.md +35 -0
- package/web/public/canvas.html +52 -0
- package/web/public/css/style.css +555 -0
- package/web/public/index.html +189 -0
- package/web/public/js/app.js +3102 -0
- package/web/public/js/i18n.js +1048 -0
- package/web/public/js/icons.js +104 -0
- package/web/public/login.html +48 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Tracker — Persistent daily/weekly usage stats.
|
|
3
|
+
*
|
|
4
|
+
* Tracks token counts, costs, and query counts per provider per day.
|
|
5
|
+
* Persists to ~/.alvin-bot/usage.json. Calculates daily/weekly summaries.
|
|
6
|
+
*
|
|
7
|
+
* Also stores the last-seen rate limit headers from providers (in-memory only).
|
|
8
|
+
*/
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { DATA_DIR } from "../paths.js";
|
|
12
|
+
const USAGE_FILE = path.join(DATA_DIR, "usage.json");
|
|
13
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
14
|
+
/** Last-seen rate limit info per provider (in-memory, not persisted) */
|
|
15
|
+
const rateLimits = new Map();
|
|
16
|
+
// ── Persistence ──────────────────────────────────────────────────────
|
|
17
|
+
function loadUsage() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(USAGE_FILE, "utf-8"));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { daily: {} };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function saveUsage(data) {
|
|
26
|
+
// Prune entries older than 90 days
|
|
27
|
+
const cutoff = new Date();
|
|
28
|
+
cutoff.setDate(cutoff.getDate() - 90);
|
|
29
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
30
|
+
for (const key of Object.keys(data.daily)) {
|
|
31
|
+
if (key < cutoffStr)
|
|
32
|
+
delete data.daily[key];
|
|
33
|
+
}
|
|
34
|
+
fs.writeFileSync(USAGE_FILE, JSON.stringify(data, null, 2));
|
|
35
|
+
}
|
|
36
|
+
function todayKey() {
|
|
37
|
+
return new Date().toISOString().slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
40
|
+
/** Record a completed query's usage. Called after each provider response. */
|
|
41
|
+
export function trackUsage(providerKey, inputTokens, outputTokens, costUsd) {
|
|
42
|
+
const data = loadUsage();
|
|
43
|
+
const key = todayKey();
|
|
44
|
+
if (!data.daily[key]) {
|
|
45
|
+
data.daily[key] = { queries: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, byProvider: {} };
|
|
46
|
+
}
|
|
47
|
+
const day = data.daily[key];
|
|
48
|
+
day.queries++;
|
|
49
|
+
day.inputTokens += inputTokens;
|
|
50
|
+
day.outputTokens += outputTokens;
|
|
51
|
+
day.costUsd += costUsd;
|
|
52
|
+
if (!day.byProvider[providerKey]) {
|
|
53
|
+
day.byProvider[providerKey] = { queries: 0, inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
54
|
+
}
|
|
55
|
+
const prov = day.byProvider[providerKey];
|
|
56
|
+
prov.queries++;
|
|
57
|
+
prov.inputTokens += inputTokens;
|
|
58
|
+
prov.outputTokens += outputTokens;
|
|
59
|
+
prov.costUsd += costUsd;
|
|
60
|
+
saveUsage(data);
|
|
61
|
+
}
|
|
62
|
+
/** Get usage summary (today + last 7 days). */
|
|
63
|
+
export function getUsageSummary() {
|
|
64
|
+
const data = loadUsage();
|
|
65
|
+
const today = todayKey();
|
|
66
|
+
const emptyDay = { queries: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, byProvider: {} };
|
|
67
|
+
const todayStats = data.daily[today] || emptyDay;
|
|
68
|
+
// Week = last 7 days including today
|
|
69
|
+
const weekStats = { queries: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, byProvider: {} };
|
|
70
|
+
const now = new Date();
|
|
71
|
+
let daysWithData = 0;
|
|
72
|
+
for (let i = 0; i < 7; i++) {
|
|
73
|
+
const d = new Date(now);
|
|
74
|
+
d.setDate(d.getDate() - i);
|
|
75
|
+
const key = d.toISOString().slice(0, 10);
|
|
76
|
+
const day = data.daily[key];
|
|
77
|
+
if (day) {
|
|
78
|
+
daysWithData++;
|
|
79
|
+
weekStats.queries += day.queries;
|
|
80
|
+
weekStats.inputTokens += day.inputTokens;
|
|
81
|
+
weekStats.outputTokens += day.outputTokens;
|
|
82
|
+
weekStats.costUsd += day.costUsd;
|
|
83
|
+
for (const [pk, ps] of Object.entries(day.byProvider)) {
|
|
84
|
+
if (!weekStats.byProvider[pk]) {
|
|
85
|
+
weekStats.byProvider[pk] = { queries: 0, inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
86
|
+
}
|
|
87
|
+
weekStats.byProvider[pk].queries += ps.queries;
|
|
88
|
+
weekStats.byProvider[pk].inputTokens += ps.inputTokens;
|
|
89
|
+
weekStats.byProvider[pk].outputTokens += ps.outputTokens;
|
|
90
|
+
weekStats.byProvider[pk].costUsd += ps.costUsd;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const totalDays = Object.keys(data.daily).length;
|
|
95
|
+
const allTokens = Object.values(data.daily).reduce((s, d) => s + d.inputTokens + d.outputTokens, 0);
|
|
96
|
+
const allCost = Object.values(data.daily).reduce((s, d) => s + d.costUsd, 0);
|
|
97
|
+
return {
|
|
98
|
+
today: todayStats,
|
|
99
|
+
week: weekStats,
|
|
100
|
+
daysTracked: totalDays,
|
|
101
|
+
avgDailyTokens: totalDays > 0 ? Math.round(allTokens / totalDays) : 0,
|
|
102
|
+
avgDailyCost: totalDays > 0 ? allCost / totalDays : 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Store rate limit info from provider response headers. */
|
|
106
|
+
export function updateRateLimits(providerKey, info) {
|
|
107
|
+
rateLimits.set(providerKey, { ...info, updatedAt: Date.now() });
|
|
108
|
+
}
|
|
109
|
+
/** Get last-seen rate limits for a provider. Returns null if no data or stale (>5min). */
|
|
110
|
+
export function getRateLimits(providerKey) {
|
|
111
|
+
const info = rateLimits.get(providerKey);
|
|
112
|
+
if (!info)
|
|
113
|
+
return null;
|
|
114
|
+
// Stale after 5 minutes
|
|
115
|
+
if (Date.now() - info.updatedAt > 300_000)
|
|
116
|
+
return null;
|
|
117
|
+
return info;
|
|
118
|
+
}
|
|
119
|
+
/** Get all non-stale rate limits. */
|
|
120
|
+
export function getAllRateLimits() {
|
|
121
|
+
const result = new Map();
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
for (const [key, info] of rateLimits) {
|
|
124
|
+
if (now - info.updatedAt < 300_000) {
|
|
125
|
+
result.set(key, info);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/** Format token count for display (e.g., 45200 → "45.2K") */
|
|
131
|
+
export function formatTokens(n) {
|
|
132
|
+
if (n >= 1_000_000)
|
|
133
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
134
|
+
if (n >= 1_000)
|
|
135
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
136
|
+
return String(n);
|
|
137
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profiles Service — Multi-user support with per-user settings and memory.
|
|
3
|
+
*
|
|
4
|
+
* Each user gets:
|
|
5
|
+
* - Their own memory directory (docs/users/<userId>/)
|
|
6
|
+
* - A profile with preferences (language, effort, voice, personality)
|
|
7
|
+
* - Separate conversation context
|
|
8
|
+
*
|
|
9
|
+
* The admin/owner user uses the global docs/memory/ and docs/MEMORY.md.
|
|
10
|
+
* Additional users get isolated memory spaces.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
import { config } from "../config.js";
|
|
15
|
+
import { killSession } from "./session.js";
|
|
16
|
+
import { USERS_DIR, MEMORY_DIR } from "../paths.js";
|
|
17
|
+
// Ensure users dir exists
|
|
18
|
+
if (!fs.existsSync(USERS_DIR))
|
|
19
|
+
fs.mkdirSync(USERS_DIR, { recursive: true });
|
|
20
|
+
// ── Profile Management ──────────────────────────────────
|
|
21
|
+
function profilePath(userId) {
|
|
22
|
+
return resolve(USERS_DIR, `${userId}.json`);
|
|
23
|
+
}
|
|
24
|
+
function userMemoryDir(userId) {
|
|
25
|
+
return resolve(USERS_DIR, `${userId}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Load a user profile. Returns null if not found.
|
|
29
|
+
*/
|
|
30
|
+
export function loadProfile(userId) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(profilePath(userId), "utf-8");
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Save a user profile.
|
|
41
|
+
*/
|
|
42
|
+
export function saveProfile(profile) {
|
|
43
|
+
fs.writeFileSync(profilePath(profile.userId), JSON.stringify(profile, null, 2));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get or create a user profile.
|
|
47
|
+
* Auto-creates on first interaction.
|
|
48
|
+
*/
|
|
49
|
+
export function getOrCreateProfile(userId, name, username) {
|
|
50
|
+
let profile = loadProfile(userId);
|
|
51
|
+
if (!profile) {
|
|
52
|
+
const isOwner = config.allowedUsers.length > 0 && config.allowedUsers[0] === userId;
|
|
53
|
+
profile = {
|
|
54
|
+
userId,
|
|
55
|
+
name: name || `User ${userId}`,
|
|
56
|
+
username,
|
|
57
|
+
firstSeen: Date.now(),
|
|
58
|
+
lastActive: Date.now(),
|
|
59
|
+
totalMessages: 0,
|
|
60
|
+
language: "en",
|
|
61
|
+
isOwner,
|
|
62
|
+
notes: "",
|
|
63
|
+
langStats: { de: 0, en: 0, other: 0 },
|
|
64
|
+
langExplicit: false,
|
|
65
|
+
};
|
|
66
|
+
// Create user memory directory for non-owner users
|
|
67
|
+
if (!isOwner) {
|
|
68
|
+
const memDir = userMemoryDir(userId);
|
|
69
|
+
if (!fs.existsSync(memDir))
|
|
70
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
saveProfile(profile);
|
|
73
|
+
}
|
|
74
|
+
return profile;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update a user's activity (call on each message).
|
|
78
|
+
*/
|
|
79
|
+
export function touchProfile(userId, name, username, platform, messageText) {
|
|
80
|
+
const profile = getOrCreateProfile(userId, name, username);
|
|
81
|
+
profile.lastActive = Date.now();
|
|
82
|
+
profile.totalMessages++;
|
|
83
|
+
if (name)
|
|
84
|
+
profile.name = name;
|
|
85
|
+
if (username)
|
|
86
|
+
profile.username = username;
|
|
87
|
+
if (platform)
|
|
88
|
+
profile.lastPlatform = platform;
|
|
89
|
+
if (messageText) {
|
|
90
|
+
profile.lastMessage = messageText.length > 120 ? messageText.slice(0, 120) + "…" : messageText;
|
|
91
|
+
profile.lastMessageAt = Date.now();
|
|
92
|
+
}
|
|
93
|
+
saveProfile(profile);
|
|
94
|
+
return profile;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* List all known user profiles.
|
|
98
|
+
*/
|
|
99
|
+
export function listProfiles() {
|
|
100
|
+
const profiles = [];
|
|
101
|
+
try {
|
|
102
|
+
const files = fs.readdirSync(USERS_DIR);
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
if (file.endsWith(".json")) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = fs.readFileSync(resolve(USERS_DIR, file), "utf-8");
|
|
107
|
+
profiles.push(JSON.parse(raw));
|
|
108
|
+
}
|
|
109
|
+
catch { /* skip corrupt */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* dir doesn't exist */ }
|
|
114
|
+
return profiles.sort((a, b) => b.lastActive - a.lastActive);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get user-specific memory directory.
|
|
118
|
+
* Owner uses global memory, others get isolated dirs.
|
|
119
|
+
*/
|
|
120
|
+
export function getUserMemoryDir(userId) {
|
|
121
|
+
const profile = loadProfile(userId);
|
|
122
|
+
if (profile?.isOwner) {
|
|
123
|
+
return MEMORY_DIR;
|
|
124
|
+
}
|
|
125
|
+
const dir = userMemoryDir(userId);
|
|
126
|
+
if (!fs.existsSync(dir))
|
|
127
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
128
|
+
return dir;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add a note to a user's profile (for AI context).
|
|
132
|
+
*/
|
|
133
|
+
export function addUserNote(userId, note) {
|
|
134
|
+
const profile = getOrCreateProfile(userId);
|
|
135
|
+
const timestamp = new Date().toISOString().slice(0, 16);
|
|
136
|
+
profile.notes += `\n[${timestamp}] ${note}`;
|
|
137
|
+
profile.notes = profile.notes.trim();
|
|
138
|
+
saveProfile(profile);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Delete a user and all their data: profile, session, memory, conversation history.
|
|
142
|
+
* Returns a summary of what was deleted.
|
|
143
|
+
*/
|
|
144
|
+
export function deleteUser(userId) {
|
|
145
|
+
const deleted = [];
|
|
146
|
+
const errors = [];
|
|
147
|
+
// 1. Delete profile JSON
|
|
148
|
+
const pPath = profilePath(userId);
|
|
149
|
+
try {
|
|
150
|
+
if (fs.existsSync(pPath)) {
|
|
151
|
+
fs.unlinkSync(pPath);
|
|
152
|
+
deleted.push("Profile");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
errors.push(`Profile: ${e}`);
|
|
157
|
+
}
|
|
158
|
+
// 2. Delete user memory directory (non-owner only)
|
|
159
|
+
const memDir = userMemoryDir(userId);
|
|
160
|
+
try {
|
|
161
|
+
if (fs.existsSync(memDir) && fs.statSync(memDir).isDirectory()) {
|
|
162
|
+
fs.rmSync(memDir, { recursive: true, force: true });
|
|
163
|
+
deleted.push("Memory directory");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
errors.push(`Memory: ${e}`);
|
|
168
|
+
}
|
|
169
|
+
// 3. Kill active session
|
|
170
|
+
try {
|
|
171
|
+
const result = killSession(userId);
|
|
172
|
+
if (result.hadSession) {
|
|
173
|
+
deleted.push("Session deleted");
|
|
174
|
+
if (result.aborted) {
|
|
175
|
+
deleted.push("Running request aborted");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
errors.push(`Session: ${e}`);
|
|
181
|
+
}
|
|
182
|
+
return { deleted, errors };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build user context string for system prompt injection.
|
|
186
|
+
*/
|
|
187
|
+
export function buildUserContext(userId) {
|
|
188
|
+
const profile = loadProfile(userId);
|
|
189
|
+
if (!profile)
|
|
190
|
+
return "";
|
|
191
|
+
const parts = [];
|
|
192
|
+
parts.push(`User: ${profile.name}${profile.username ? ` (@${profile.username})` : ""}`);
|
|
193
|
+
parts.push(`Language: ${profile.language === "de" ? "Deutsch" : "English"}`);
|
|
194
|
+
parts.push(`Messages: ${profile.totalMessages}`);
|
|
195
|
+
if (profile.notes) {
|
|
196
|
+
parts.push(`\nNotes about this user:\n${profile.notes}`);
|
|
197
|
+
}
|
|
198
|
+
return parts.join("\n");
|
|
199
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import https from "https";
|
|
5
|
+
import { EdgeTTS } from "node-edge-tts";
|
|
6
|
+
import { config } from "../config.js";
|
|
7
|
+
const TEMP_DIR = path.join(os.tmpdir(), "alvin-bot");
|
|
8
|
+
if (!fs.existsSync(TEMP_DIR)) {
|
|
9
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
// ── Speech-to-Text (Groq Whisper) ──────────────────────
|
|
12
|
+
export async function transcribeAudio(audioPath) {
|
|
13
|
+
const fileBuffer = fs.readFileSync(audioPath);
|
|
14
|
+
const boundary = "----FormBoundary" + Math.random().toString(36).slice(2);
|
|
15
|
+
const fileName = path.basename(audioPath);
|
|
16
|
+
let body = "";
|
|
17
|
+
body += `--${boundary}\r\n`;
|
|
18
|
+
body += `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`;
|
|
19
|
+
body += `Content-Type: audio/ogg\r\n\r\n`;
|
|
20
|
+
const bodyStart = Buffer.from(body, "utf-8");
|
|
21
|
+
const bodyEnd = Buffer.from(`\r\n--${boundary}\r\n` +
|
|
22
|
+
`Content-Disposition: form-data; name="model"\r\n\r\n` +
|
|
23
|
+
`whisper-large-v3-turbo\r\n` +
|
|
24
|
+
`--${boundary}\r\n` +
|
|
25
|
+
`Content-Disposition: form-data; name="language"\r\n\r\n` +
|
|
26
|
+
`de\r\n` +
|
|
27
|
+
`--${boundary}--\r\n`, "utf-8");
|
|
28
|
+
const fullBody = Buffer.concat([bodyStart, fileBuffer, bodyEnd]);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const req = https.request({
|
|
31
|
+
hostname: "api.groq.com",
|
|
32
|
+
path: "/openai/v1/audio/transcriptions",
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: `Bearer ${config.apiKeys.groq}`,
|
|
36
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
37
|
+
"Content-Length": fullBody.length,
|
|
38
|
+
},
|
|
39
|
+
}, (res) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
res.on("data", (chunk) => (data += chunk));
|
|
42
|
+
res.on("end", () => {
|
|
43
|
+
try {
|
|
44
|
+
const json = JSON.parse(data);
|
|
45
|
+
resolve(json.text || "");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
reject(new Error(`Groq STT error: ${data}`));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
req.on("error", reject);
|
|
53
|
+
req.write(fullBody);
|
|
54
|
+
req.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// ── Text-to-Speech (Edge TTS via node-edge-tts) ────────
|
|
58
|
+
export async function textToSpeech(text) {
|
|
59
|
+
// Strip markdown formatting for cleaner TTS
|
|
60
|
+
let cleanText = text
|
|
61
|
+
.replace(/```[\s\S]*?```/g, " Code block skipped. ")
|
|
62
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
63
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
64
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
65
|
+
.replace(/#{1,6}\s/g, "")
|
|
66
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
67
|
+
.replace(/\n{2,}/g, ". ")
|
|
68
|
+
.replace(/\n/g, " ")
|
|
69
|
+
.trim();
|
|
70
|
+
if (!cleanText) {
|
|
71
|
+
throw new Error("No text available for TTS");
|
|
72
|
+
}
|
|
73
|
+
if (cleanText.length > 3000) {
|
|
74
|
+
cleanText = cleanText.slice(0, 3000) + "... Text truncated.";
|
|
75
|
+
}
|
|
76
|
+
// Try ElevenLabs if configured
|
|
77
|
+
if (config.ttsProvider === "elevenlabs" && config.elevenlabs.apiKey) {
|
|
78
|
+
try {
|
|
79
|
+
const { elevenLabsTTS } = await import("./elevenlabs.js");
|
|
80
|
+
return await elevenLabsTTS(cleanText);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.warn("ElevenLabs TTS failed, falling back to Edge TTS:", err instanceof Error ? err.message : err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Edge TTS (default / fallback)
|
|
87
|
+
const outputPath = path.join(TEMP_DIR, `tts_${Date.now()}.mp3`);
|
|
88
|
+
const tts = new EdgeTTS({
|
|
89
|
+
voice: "de-DE-ConradNeural",
|
|
90
|
+
lang: "de-DE",
|
|
91
|
+
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
|
92
|
+
});
|
|
93
|
+
await tts.ttsPromise(cleanText, outputPath);
|
|
94
|
+
return outputPath;
|
|
95
|
+
}
|