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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alvin Bot — Multi-Model Provider Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Unified interfaces for different LLM backends.
|
|
5
|
+
* Every provider implements the same interface, making model switching seamless.
|
|
6
|
+
*/
|
|
7
|
+
// ── Provider Presets (common configurations) ────────────
|
|
8
|
+
export const PROVIDER_PRESETS = {
|
|
9
|
+
// OpenAI (via Codex CLI — full tool use)
|
|
10
|
+
"codex-cli": {
|
|
11
|
+
type: "codex-cli",
|
|
12
|
+
name: "Codex CLI (OpenAI)",
|
|
13
|
+
model: "gpt-5.4",
|
|
14
|
+
supportsTools: true,
|
|
15
|
+
supportsVision: false,
|
|
16
|
+
supportsStreaming: true,
|
|
17
|
+
},
|
|
18
|
+
// Anthropic (via Agent SDK — full tool use)
|
|
19
|
+
"claude-sdk": {
|
|
20
|
+
type: "claude-sdk",
|
|
21
|
+
name: "Claude (Agent SDK)",
|
|
22
|
+
supportsTools: true,
|
|
23
|
+
supportsVision: true,
|
|
24
|
+
supportsStreaming: true,
|
|
25
|
+
},
|
|
26
|
+
// Anthropic API (via OpenAI-compatible endpoint — no Agent SDK needed)
|
|
27
|
+
"claude-opus": {
|
|
28
|
+
type: "openai-compatible",
|
|
29
|
+
name: "Claude Opus 4",
|
|
30
|
+
model: "claude-opus-4-6",
|
|
31
|
+
baseUrl: "https://api.anthropic.com/v1/",
|
|
32
|
+
supportsVision: true,
|
|
33
|
+
supportsStreaming: true,
|
|
34
|
+
supportsTools: true,
|
|
35
|
+
},
|
|
36
|
+
"claude-sonnet": {
|
|
37
|
+
type: "openai-compatible",
|
|
38
|
+
name: "Claude Sonnet 4",
|
|
39
|
+
model: "claude-sonnet-4-20250514",
|
|
40
|
+
baseUrl: "https://api.anthropic.com/v1/",
|
|
41
|
+
supportsVision: true,
|
|
42
|
+
supportsStreaming: true,
|
|
43
|
+
supportsTools: true,
|
|
44
|
+
},
|
|
45
|
+
"claude-haiku": {
|
|
46
|
+
type: "openai-compatible",
|
|
47
|
+
name: "Claude 3.5 Haiku",
|
|
48
|
+
model: "claude-3-5-haiku-20241022",
|
|
49
|
+
baseUrl: "https://api.anthropic.com/v1/",
|
|
50
|
+
supportsVision: true,
|
|
51
|
+
supportsStreaming: true,
|
|
52
|
+
supportsTools: true,
|
|
53
|
+
},
|
|
54
|
+
// Groq (fast inference, free tier, supports function calling)
|
|
55
|
+
"groq": {
|
|
56
|
+
type: "openai-compatible",
|
|
57
|
+
name: "Groq (Llama 3.3 70B)",
|
|
58
|
+
model: "llama-3.3-70b-versatile",
|
|
59
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
60
|
+
supportsVision: false,
|
|
61
|
+
supportsStreaming: true,
|
|
62
|
+
supportsTools: true,
|
|
63
|
+
},
|
|
64
|
+
// OpenAI (supports function calling)
|
|
65
|
+
"gpt-4o": {
|
|
66
|
+
type: "openai-compatible",
|
|
67
|
+
name: "GPT-4o",
|
|
68
|
+
model: "gpt-4o",
|
|
69
|
+
baseUrl: "https://api.openai.com/v1",
|
|
70
|
+
supportsVision: true,
|
|
71
|
+
supportsStreaming: true,
|
|
72
|
+
supportsTools: true,
|
|
73
|
+
},
|
|
74
|
+
"gpt-4o-mini": {
|
|
75
|
+
type: "openai-compatible",
|
|
76
|
+
name: "GPT-4o Mini",
|
|
77
|
+
model: "gpt-4o-mini",
|
|
78
|
+
baseUrl: "https://api.openai.com/v1",
|
|
79
|
+
supportsVision: true,
|
|
80
|
+
supportsStreaming: true,
|
|
81
|
+
supportsTools: true,
|
|
82
|
+
},
|
|
83
|
+
// Google Gemini (via OpenAI-compatible endpoint, supports function calling)
|
|
84
|
+
"gemini-2.5-pro": {
|
|
85
|
+
type: "openai-compatible",
|
|
86
|
+
name: "Gemini 2.5 Pro",
|
|
87
|
+
model: "gemini-2.5-pro",
|
|
88
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
89
|
+
supportsVision: true,
|
|
90
|
+
supportsStreaming: true,
|
|
91
|
+
supportsTools: true,
|
|
92
|
+
},
|
|
93
|
+
"gemini-2.5-flash": {
|
|
94
|
+
type: "openai-compatible",
|
|
95
|
+
name: "Gemini 2.5 Flash",
|
|
96
|
+
model: "gemini-2.5-flash",
|
|
97
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
98
|
+
supportsVision: true,
|
|
99
|
+
supportsStreaming: true,
|
|
100
|
+
supportsTools: true,
|
|
101
|
+
},
|
|
102
|
+
"gemini-3-pro": {
|
|
103
|
+
type: "openai-compatible",
|
|
104
|
+
name: "Gemini 3 Pro (Preview)",
|
|
105
|
+
model: "gemini-3-pro-preview",
|
|
106
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
107
|
+
supportsVision: true,
|
|
108
|
+
supportsStreaming: true,
|
|
109
|
+
supportsTools: true,
|
|
110
|
+
},
|
|
111
|
+
"gemini-3-flash": {
|
|
112
|
+
type: "openai-compatible",
|
|
113
|
+
name: "Gemini 3 Flash (Preview)",
|
|
114
|
+
model: "gemini-3-flash-preview",
|
|
115
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
116
|
+
supportsVision: true,
|
|
117
|
+
supportsStreaming: true,
|
|
118
|
+
supportsTools: true,
|
|
119
|
+
},
|
|
120
|
+
// OpenAI newer models
|
|
121
|
+
"gpt-4.1": {
|
|
122
|
+
type: "openai-compatible",
|
|
123
|
+
name: "GPT-4.1",
|
|
124
|
+
model: "gpt-4.1",
|
|
125
|
+
baseUrl: "https://api.openai.com/v1",
|
|
126
|
+
supportsVision: true,
|
|
127
|
+
supportsStreaming: true,
|
|
128
|
+
supportsTools: true,
|
|
129
|
+
},
|
|
130
|
+
"gpt-4.1-mini": {
|
|
131
|
+
type: "openai-compatible",
|
|
132
|
+
name: "GPT-4.1 Mini",
|
|
133
|
+
model: "gpt-4.1-mini",
|
|
134
|
+
baseUrl: "https://api.openai.com/v1",
|
|
135
|
+
supportsVision: true,
|
|
136
|
+
supportsStreaming: true,
|
|
137
|
+
supportsTools: true,
|
|
138
|
+
},
|
|
139
|
+
"o3-mini": {
|
|
140
|
+
type: "openai-compatible",
|
|
141
|
+
name: "o3 Mini",
|
|
142
|
+
model: "o3-mini",
|
|
143
|
+
baseUrl: "https://api.openai.com/v1",
|
|
144
|
+
supportsVision: false,
|
|
145
|
+
supportsStreaming: true,
|
|
146
|
+
supportsTools: true,
|
|
147
|
+
},
|
|
148
|
+
// Groq additional models
|
|
149
|
+
"groq-llama-3.1-8b": {
|
|
150
|
+
type: "openai-compatible",
|
|
151
|
+
name: "Llama 3.1 8B (Groq)",
|
|
152
|
+
model: "llama-3.1-8b-instant",
|
|
153
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
154
|
+
supportsVision: false,
|
|
155
|
+
supportsStreaming: true,
|
|
156
|
+
supportsTools: true,
|
|
157
|
+
},
|
|
158
|
+
"groq-mixtral": {
|
|
159
|
+
type: "openai-compatible",
|
|
160
|
+
name: "Mixtral 8x7B (Groq)",
|
|
161
|
+
model: "mixtral-8x7b-32768",
|
|
162
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
163
|
+
supportsVision: false,
|
|
164
|
+
supportsStreaming: true,
|
|
165
|
+
supportsTools: true,
|
|
166
|
+
},
|
|
167
|
+
// NVIDIA NIM (150+ free models)
|
|
168
|
+
"nvidia-llama-3.3-70b": {
|
|
169
|
+
type: "openai-compatible",
|
|
170
|
+
name: "Llama 3.3 70B (NVIDIA)",
|
|
171
|
+
model: "meta/llama-3.3-70b-instruct",
|
|
172
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
173
|
+
supportsVision: false,
|
|
174
|
+
supportsStreaming: true,
|
|
175
|
+
supportsTools: true,
|
|
176
|
+
},
|
|
177
|
+
"nvidia-kimi-k2.5": {
|
|
178
|
+
type: "openai-compatible",
|
|
179
|
+
name: "Kimi K2.5 (NVIDIA)",
|
|
180
|
+
model: "moonshotai/kimi-k2.5",
|
|
181
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
182
|
+
supportsVision: true,
|
|
183
|
+
supportsStreaming: true,
|
|
184
|
+
supportsTools: true,
|
|
185
|
+
},
|
|
186
|
+
// Ollama (local models)
|
|
187
|
+
"ollama": {
|
|
188
|
+
type: "openai-compatible",
|
|
189
|
+
name: "Gemma 4 E4B (Ollama)",
|
|
190
|
+
model: "gemma4:e4b",
|
|
191
|
+
baseUrl: "http://localhost:11434/v1",
|
|
192
|
+
supportsVision: true,
|
|
193
|
+
supportsStreaming: true,
|
|
194
|
+
},
|
|
195
|
+
// OpenRouter (any model, one API, supports function calling)
|
|
196
|
+
"openrouter": {
|
|
197
|
+
type: "openai-compatible",
|
|
198
|
+
name: "OpenRouter",
|
|
199
|
+
model: "anthropic/claude-sonnet-4",
|
|
200
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
201
|
+
supportsVision: true,
|
|
202
|
+
supportsStreaming: true,
|
|
203
|
+
supportsTools: true,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control Service — Manages group approvals and security.
|
|
3
|
+
*
|
|
4
|
+
* Security model:
|
|
5
|
+
* - DMs: only ALLOWED_USERS can interact (unchanged)
|
|
6
|
+
* - Groups: must be explicitly approved by an admin before bot responds
|
|
7
|
+
* - New groups: bot sends approval request to admin, stays silent until approved
|
|
8
|
+
* - Admin can block/unblock groups at any time
|
|
9
|
+
* - Forwarded message processing can be toggled
|
|
10
|
+
*/
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { ACCESS_FILE, RUNTIME_DIR } from "../paths.js";
|
|
13
|
+
// Ensure data dir exists
|
|
14
|
+
if (!fs.existsSync(RUNTIME_DIR))
|
|
15
|
+
fs.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
16
|
+
let config = {
|
|
17
|
+
groups: {},
|
|
18
|
+
settings: {
|
|
19
|
+
allowForwards: true,
|
|
20
|
+
autoApproveGroups: false,
|
|
21
|
+
groupRateLimitPerHour: 30,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
// Load on startup
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(ACCESS_FILE, "utf-8");
|
|
27
|
+
config = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
save(); // Create default file
|
|
31
|
+
}
|
|
32
|
+
function save() {
|
|
33
|
+
fs.writeFileSync(ACCESS_FILE, JSON.stringify(config, null, 2));
|
|
34
|
+
}
|
|
35
|
+
// ── Group Management ─────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Check if a group chat is approved.
|
|
38
|
+
* Returns: "approved" | "pending" | "blocked" | "new"
|
|
39
|
+
*/
|
|
40
|
+
export function getGroupStatus(chatId) {
|
|
41
|
+
const key = String(chatId);
|
|
42
|
+
const group = config.groups[key];
|
|
43
|
+
if (!group)
|
|
44
|
+
return "new";
|
|
45
|
+
return group.status;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Register a new group (first time the bot sees it).
|
|
49
|
+
* Returns the group info.
|
|
50
|
+
*/
|
|
51
|
+
export function registerGroup(chatId, title, addedBy) {
|
|
52
|
+
const key = String(chatId);
|
|
53
|
+
if (config.groups[key]) {
|
|
54
|
+
// Update title if changed
|
|
55
|
+
config.groups[key].title = title;
|
|
56
|
+
save();
|
|
57
|
+
return config.groups[key];
|
|
58
|
+
}
|
|
59
|
+
const group = {
|
|
60
|
+
chatId,
|
|
61
|
+
title,
|
|
62
|
+
addedBy,
|
|
63
|
+
firstSeen: Date.now(),
|
|
64
|
+
status: config.settings.autoApproveGroups ? "approved" : "pending",
|
|
65
|
+
statusChanged: Date.now(),
|
|
66
|
+
messageCount: 0,
|
|
67
|
+
};
|
|
68
|
+
config.groups[key] = group;
|
|
69
|
+
save();
|
|
70
|
+
return group;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Approve a group.
|
|
74
|
+
*/
|
|
75
|
+
export function approveGroup(chatId) {
|
|
76
|
+
const key = String(chatId);
|
|
77
|
+
const group = config.groups[key];
|
|
78
|
+
if (!group)
|
|
79
|
+
return false;
|
|
80
|
+
group.status = "approved";
|
|
81
|
+
group.statusChanged = Date.now();
|
|
82
|
+
save();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Block a group.
|
|
87
|
+
*/
|
|
88
|
+
export function blockGroup(chatId) {
|
|
89
|
+
const key = String(chatId);
|
|
90
|
+
const group = config.groups[key];
|
|
91
|
+
if (!group)
|
|
92
|
+
return false;
|
|
93
|
+
group.status = "blocked";
|
|
94
|
+
group.statusChanged = Date.now();
|
|
95
|
+
save();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Increment message count for a group.
|
|
100
|
+
*/
|
|
101
|
+
export function trackGroupMessage(chatId) {
|
|
102
|
+
const key = String(chatId);
|
|
103
|
+
if (config.groups[key]) {
|
|
104
|
+
config.groups[key].messageCount++;
|
|
105
|
+
// Save periodically (every 10 messages to reduce I/O)
|
|
106
|
+
if (config.groups[key].messageCount % 10 === 0)
|
|
107
|
+
save();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get all groups.
|
|
112
|
+
*/
|
|
113
|
+
export function listGroups() {
|
|
114
|
+
return Object.values(config.groups).sort((a, b) => b.firstSeen - a.firstSeen);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Remove a group from tracking.
|
|
118
|
+
*/
|
|
119
|
+
export function removeGroup(chatId) {
|
|
120
|
+
const key = String(chatId);
|
|
121
|
+
if (!config.groups[key])
|
|
122
|
+
return false;
|
|
123
|
+
delete config.groups[key];
|
|
124
|
+
save();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
// ── Settings ─────────────────────────────────────────
|
|
128
|
+
export function isForwardingAllowed() {
|
|
129
|
+
return config.settings.allowForwards;
|
|
130
|
+
}
|
|
131
|
+
export function setForwardingAllowed(allowed) {
|
|
132
|
+
config.settings.allowForwards = allowed;
|
|
133
|
+
save();
|
|
134
|
+
}
|
|
135
|
+
export function isAutoApproveEnabled() {
|
|
136
|
+
return config.settings.autoApproveGroups;
|
|
137
|
+
}
|
|
138
|
+
export function setAutoApprove(enabled) {
|
|
139
|
+
config.settings.autoApproveGroups = enabled;
|
|
140
|
+
save();
|
|
141
|
+
}
|
|
142
|
+
export function getSettings() {
|
|
143
|
+
return { ...config.settings };
|
|
144
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Index — Scans ~/.alvin-bot/assets/ and builds a searchable registry.
|
|
3
|
+
*
|
|
4
|
+
* Produces:
|
|
5
|
+
* - INDEX.json — machine-readable, used by self-search and skill injection
|
|
6
|
+
* - INDEX.md — human-readable, injected into prompts for asset awareness
|
|
7
|
+
*
|
|
8
|
+
* Scan is filesystem-only (<5ms for ~60 files). No API calls.
|
|
9
|
+
* Re-scans only when ASSETS_DIR has changed since last scan.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { ASSETS_DIR, ASSETS_INDEX_JSON, ASSETS_INDEX_MD } from "../paths.js";
|
|
14
|
+
// ── Cache ───────────────────────────────────────────────
|
|
15
|
+
let cachedIndex = null;
|
|
16
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Walk a directory recursively, yielding file entries.
|
|
19
|
+
*/
|
|
20
|
+
function walkDir(dir) {
|
|
21
|
+
const results = [];
|
|
22
|
+
function walk(currentDir) {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.resolve(currentDir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
walk(fullPath);
|
|
34
|
+
}
|
|
35
|
+
else if (entry.isFile()) {
|
|
36
|
+
// Skip INDEX.json and INDEX.md at ASSETS_DIR root
|
|
37
|
+
if (currentDir === dir && (entry.name === "INDEX.json" || entry.name === "INDEX.md")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
results.push({
|
|
41
|
+
name: entry.name,
|
|
42
|
+
path: fullPath,
|
|
43
|
+
relativePath: path.relative(dir, fullPath),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(dir);
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate a human-readable description from a filename.
|
|
53
|
+
* "acme-cover-letter.html" → "Cover Letter: Acme"
|
|
54
|
+
* "profile-photo.jpeg" → "Profile Photo"
|
|
55
|
+
*/
|
|
56
|
+
function descriptionFromFilename(filename, category) {
|
|
57
|
+
const name = filename.replace(/\.[^.]+$/, ""); // strip extension
|
|
58
|
+
const words = name.replace(/[-_]/g, " ").trim();
|
|
59
|
+
// Special handling for known patterns
|
|
60
|
+
if (category === "cover-letters") {
|
|
61
|
+
const company = words.replace(/cover letter/i, "").replace(/^Cover_Letter_[A-Za-z_]+_/i, "").trim();
|
|
62
|
+
return `Cover Letter: ${company || words}`;
|
|
63
|
+
}
|
|
64
|
+
if (category === "cv-templates") {
|
|
65
|
+
return `CV Template: ${words}`;
|
|
66
|
+
}
|
|
67
|
+
// Default: capitalize words
|
|
68
|
+
return words.replace(/\b\w/g, c => c.toUpperCase());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Determine category for a file.
|
|
72
|
+
* Files in subdirectories get the directory name as category.
|
|
73
|
+
* Root-level files get a special category based on naming.
|
|
74
|
+
*/
|
|
75
|
+
function categorize(relativePath) {
|
|
76
|
+
const parts = relativePath.split(path.sep);
|
|
77
|
+
if (parts.length > 1) {
|
|
78
|
+
return parts[0]; // directory name
|
|
79
|
+
}
|
|
80
|
+
// Root-level file — categorize by pattern
|
|
81
|
+
const filename = parts[0].toLowerCase();
|
|
82
|
+
if (filename.includes("signature"))
|
|
83
|
+
return "signatures";
|
|
84
|
+
return "misc";
|
|
85
|
+
}
|
|
86
|
+
// ── Public API ──────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Scan ASSETS_DIR and write INDEX.json + INDEX.md.
|
|
89
|
+
* Only re-scans if directory has changed since last scan.
|
|
90
|
+
* Returns the asset index.
|
|
91
|
+
*/
|
|
92
|
+
export function scanAssets() {
|
|
93
|
+
if (!fs.existsSync(ASSETS_DIR)) {
|
|
94
|
+
const empty = { lastScan: new Date().toISOString(), assets: [] };
|
|
95
|
+
cachedIndex = empty;
|
|
96
|
+
return empty;
|
|
97
|
+
}
|
|
98
|
+
// Check if re-scan needed
|
|
99
|
+
if (fs.existsSync(ASSETS_INDEX_JSON)) {
|
|
100
|
+
try {
|
|
101
|
+
const existing = JSON.parse(fs.readFileSync(ASSETS_INDEX_JSON, "utf-8"));
|
|
102
|
+
const dirStat = fs.statSync(ASSETS_DIR);
|
|
103
|
+
const lastScanTime = new Date(existing.lastScan).getTime();
|
|
104
|
+
// Also check subdirectory mtimes (adding a file to a subdir changes subdir mtime, not parent)
|
|
105
|
+
let newestMtime = dirStat.mtimeMs;
|
|
106
|
+
try {
|
|
107
|
+
const subdirs = fs.readdirSync(ASSETS_DIR, { withFileTypes: true });
|
|
108
|
+
for (const d of subdirs) {
|
|
109
|
+
if (d.isDirectory()) {
|
|
110
|
+
const subStat = fs.statSync(path.resolve(ASSETS_DIR, d.name));
|
|
111
|
+
if (subStat.mtimeMs > newestMtime)
|
|
112
|
+
newestMtime = subStat.mtimeMs;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
if (newestMtime <= lastScanTime) {
|
|
118
|
+
cachedIndex = existing;
|
|
119
|
+
return existing;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Corrupted index — re-scan
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Full scan
|
|
127
|
+
const files = walkDir(ASSETS_DIR);
|
|
128
|
+
const assets = [];
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
try {
|
|
131
|
+
const stat = fs.statSync(file.path);
|
|
132
|
+
const category = categorize(file.relativePath);
|
|
133
|
+
assets.push({
|
|
134
|
+
path: file.relativePath,
|
|
135
|
+
absolutePath: file.path,
|
|
136
|
+
category,
|
|
137
|
+
filename: file.name,
|
|
138
|
+
ext: path.extname(file.name),
|
|
139
|
+
size: stat.size,
|
|
140
|
+
modified: new Date(stat.mtimeMs).toISOString(),
|
|
141
|
+
description: descriptionFromFilename(file.name, category),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// File disappeared between readdir and stat — skip
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Sort by category then filename
|
|
149
|
+
assets.sort((a, b) => a.category.localeCompare(b.category) || a.filename.localeCompare(b.filename));
|
|
150
|
+
const index = {
|
|
151
|
+
lastScan: new Date().toISOString(),
|
|
152
|
+
assets,
|
|
153
|
+
};
|
|
154
|
+
// Write INDEX.json
|
|
155
|
+
fs.writeFileSync(ASSETS_INDEX_JSON, JSON.stringify(index, null, 2));
|
|
156
|
+
// Write INDEX.md
|
|
157
|
+
const md = generateIndexMd(index);
|
|
158
|
+
fs.writeFileSync(ASSETS_INDEX_MD, md);
|
|
159
|
+
cachedIndex = index;
|
|
160
|
+
return index;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Load asset index from disk (cached after first call).
|
|
164
|
+
*/
|
|
165
|
+
export function loadAssetIndex() {
|
|
166
|
+
if (cachedIndex)
|
|
167
|
+
return cachedIndex;
|
|
168
|
+
if (fs.existsSync(ASSETS_INDEX_JSON)) {
|
|
169
|
+
try {
|
|
170
|
+
cachedIndex = JSON.parse(fs.readFileSync(ASSETS_INDEX_JSON, "utf-8"));
|
|
171
|
+
return cachedIndex;
|
|
172
|
+
}
|
|
173
|
+
catch { /* fall through */ }
|
|
174
|
+
}
|
|
175
|
+
// No index — run scan
|
|
176
|
+
return scanAssets();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get INDEX.md content for prompt injection.
|
|
180
|
+
*/
|
|
181
|
+
export function getAssetIndexMd() {
|
|
182
|
+
if (fs.existsSync(ASSETS_INDEX_MD)) {
|
|
183
|
+
return fs.readFileSync(ASSETS_INDEX_MD, "utf-8");
|
|
184
|
+
}
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Find assets by category name.
|
|
189
|
+
*/
|
|
190
|
+
export function findAssetsByCategory(category) {
|
|
191
|
+
const index = loadAssetIndex();
|
|
192
|
+
return index.assets.filter(a => a.category === category);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Find assets by keyword match on filename, category, or description.
|
|
196
|
+
*/
|
|
197
|
+
export function findAssetsByKeyword(keywords) {
|
|
198
|
+
const index = loadAssetIndex();
|
|
199
|
+
const lower = keywords.map(k => k.toLowerCase());
|
|
200
|
+
return index.assets.filter(a => lower.some(k => a.filename.toLowerCase().includes(k) ||
|
|
201
|
+
a.category.toLowerCase().includes(k) ||
|
|
202
|
+
a.description.toLowerCase().includes(k)));
|
|
203
|
+
}
|
|
204
|
+
// ── INDEX.md Generator ──────────────────────────────────
|
|
205
|
+
function generateIndexMd(index) {
|
|
206
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
207
|
+
const lines = [`## Your Assets (~/.alvin-bot/assets/) — ${date}\n`];
|
|
208
|
+
// Group by category
|
|
209
|
+
const byCategory = new Map();
|
|
210
|
+
for (const a of index.assets) {
|
|
211
|
+
const list = byCategory.get(a.category) || [];
|
|
212
|
+
list.push(a);
|
|
213
|
+
byCategory.set(a.category, list);
|
|
214
|
+
}
|
|
215
|
+
// Sort categories alphabetically
|
|
216
|
+
const sortedCategories = [...byCategory.keys()].sort();
|
|
217
|
+
for (const cat of sortedCategories) {
|
|
218
|
+
const assets = byCategory.get(cat);
|
|
219
|
+
const names = assets.map(a => a.filename);
|
|
220
|
+
// Compact display: show up to 6 names, then "..."
|
|
221
|
+
const display = names.length > 6
|
|
222
|
+
? names.slice(0, 5).join(", ") + `, ... (+${names.length - 5} more)`
|
|
223
|
+
: names.join(", ");
|
|
224
|
+
lines.push(`- **${cat}/** (${assets.length}): ${display}`);
|
|
225
|
+
}
|
|
226
|
+
const totalSize = index.assets.reduce((sum, a) => sum + a.size, 0);
|
|
227
|
+
const sizeMB = (totalSize / 1_048_576).toFixed(1);
|
|
228
|
+
lines.push(`\nTotal: ${index.assets.length} files, ${sizeMB} MB`);
|
|
229
|
+
return lines.join("\n") + "\n";
|
|
230
|
+
}
|