@madh-io/alfred-ai 0.2.0 → 0.3.0
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/bundle/index.js +1055 -53
- package/package.json +14 -12
package/bundle/index.js
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// ../config/dist/schema.js
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
var TelegramConfigSchema, DiscordConfigSchema, WhatsAppConfigSchema, MatrixConfigSchema, SignalConfigSchema, StorageConfigSchema, LoggerConfigSchema, SecurityConfigSchema, LLMProviderConfigSchema, AlfredConfigSchema;
|
|
14
|
+
var TelegramConfigSchema, DiscordConfigSchema, WhatsAppConfigSchema, MatrixConfigSchema, SignalConfigSchema, StorageConfigSchema, LoggerConfigSchema, SecurityConfigSchema, LLMProviderConfigSchema, SearchConfigSchema, EmailConfigSchema, AlfredConfigSchema;
|
|
15
15
|
var init_schema = __esm({
|
|
16
16
|
"../config/dist/schema.js"() {
|
|
17
17
|
"use strict";
|
|
@@ -59,6 +59,27 @@ var init_schema = __esm({
|
|
|
59
59
|
temperature: z.number().optional(),
|
|
60
60
|
maxTokens: z.number().optional()
|
|
61
61
|
});
|
|
62
|
+
SearchConfigSchema = z.object({
|
|
63
|
+
provider: z.enum(["brave", "searxng", "tavily", "duckduckgo"]),
|
|
64
|
+
apiKey: z.string().optional(),
|
|
65
|
+
baseUrl: z.string().optional()
|
|
66
|
+
});
|
|
67
|
+
EmailConfigSchema = z.object({
|
|
68
|
+
imap: z.object({
|
|
69
|
+
host: z.string(),
|
|
70
|
+
port: z.number(),
|
|
71
|
+
secure: z.boolean()
|
|
72
|
+
}),
|
|
73
|
+
smtp: z.object({
|
|
74
|
+
host: z.string(),
|
|
75
|
+
port: z.number(),
|
|
76
|
+
secure: z.boolean()
|
|
77
|
+
}),
|
|
78
|
+
auth: z.object({
|
|
79
|
+
user: z.string(),
|
|
80
|
+
pass: z.string()
|
|
81
|
+
})
|
|
82
|
+
});
|
|
62
83
|
AlfredConfigSchema = z.object({
|
|
63
84
|
name: z.string(),
|
|
64
85
|
telegram: TelegramConfigSchema,
|
|
@@ -69,7 +90,9 @@ var init_schema = __esm({
|
|
|
69
90
|
llm: LLMProviderConfigSchema,
|
|
70
91
|
storage: StorageConfigSchema,
|
|
71
92
|
logger: LoggerConfigSchema,
|
|
72
|
-
security: SecurityConfigSchema
|
|
93
|
+
security: SecurityConfigSchema,
|
|
94
|
+
search: SearchConfigSchema.optional(),
|
|
95
|
+
email: EmailConfigSchema.optional()
|
|
73
96
|
});
|
|
74
97
|
}
|
|
75
98
|
});
|
|
@@ -184,7 +207,12 @@ var init_loader = __esm({
|
|
|
184
207
|
ALFRED_LLM_BASE_URL: ["llm", "baseUrl"],
|
|
185
208
|
ALFRED_STORAGE_PATH: ["storage", "path"],
|
|
186
209
|
ALFRED_LOG_LEVEL: ["logger", "level"],
|
|
187
|
-
ALFRED_OWNER_USER_ID: ["security", "ownerUserId"]
|
|
210
|
+
ALFRED_OWNER_USER_ID: ["security", "ownerUserId"],
|
|
211
|
+
ALFRED_SEARCH_PROVIDER: ["search", "provider"],
|
|
212
|
+
ALFRED_SEARCH_API_KEY: ["search", "apiKey"],
|
|
213
|
+
ALFRED_SEARCH_BASE_URL: ["search", "baseUrl"],
|
|
214
|
+
ALFRED_EMAIL_USER: ["email", "auth", "user"],
|
|
215
|
+
ALFRED_EMAIL_PASS: ["email", "auth", "pass"]
|
|
188
216
|
};
|
|
189
217
|
ConfigLoader = class {
|
|
190
218
|
loadConfig(configPath) {
|
|
@@ -255,6 +283,131 @@ var init_dist2 = __esm({
|
|
|
255
283
|
}
|
|
256
284
|
});
|
|
257
285
|
|
|
286
|
+
// ../storage/dist/migrations/migrator.js
|
|
287
|
+
var Migrator;
|
|
288
|
+
var init_migrator = __esm({
|
|
289
|
+
"../storage/dist/migrations/migrator.js"() {
|
|
290
|
+
"use strict";
|
|
291
|
+
Migrator = class {
|
|
292
|
+
db;
|
|
293
|
+
constructor(db) {
|
|
294
|
+
this.db = db;
|
|
295
|
+
this.ensureMigrationsTable();
|
|
296
|
+
}
|
|
297
|
+
ensureMigrationsTable() {
|
|
298
|
+
this.db.exec(`
|
|
299
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
300
|
+
version INTEGER PRIMARY KEY,
|
|
301
|
+
description TEXT,
|
|
302
|
+
applied_at TEXT NOT NULL
|
|
303
|
+
)
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
/** Get current schema version */
|
|
307
|
+
getCurrentVersion() {
|
|
308
|
+
const row = this.db.prepare("SELECT MAX(version) as version FROM _migrations").get();
|
|
309
|
+
return row?.version ?? 0;
|
|
310
|
+
}
|
|
311
|
+
/** Run all pending migrations */
|
|
312
|
+
migrate(migrations) {
|
|
313
|
+
const sorted = [...migrations].sort((a, b) => a.version - b.version);
|
|
314
|
+
const currentVersion = this.getCurrentVersion();
|
|
315
|
+
for (const migration of sorted) {
|
|
316
|
+
if (migration.version <= currentVersion) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const run = this.db.transaction(() => {
|
|
320
|
+
migration.up(this.db);
|
|
321
|
+
this.db.prepare("INSERT INTO _migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, (/* @__PURE__ */ new Date()).toISOString());
|
|
322
|
+
});
|
|
323
|
+
run();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Get list of applied migrations */
|
|
327
|
+
getAppliedMigrations() {
|
|
328
|
+
const rows = this.db.prepare("SELECT version, applied_at FROM _migrations ORDER BY version ASC").all();
|
|
329
|
+
return rows.map((row) => ({
|
|
330
|
+
version: row.version,
|
|
331
|
+
appliedAt: row.applied_at
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ../storage/dist/migrations/index.js
|
|
339
|
+
var MIGRATIONS;
|
|
340
|
+
var init_migrations = __esm({
|
|
341
|
+
"../storage/dist/migrations/index.js"() {
|
|
342
|
+
"use strict";
|
|
343
|
+
init_migrator();
|
|
344
|
+
MIGRATIONS = [
|
|
345
|
+
{
|
|
346
|
+
version: 1,
|
|
347
|
+
description: "Initial schema \u2014 conversations, messages, users, audit_log",
|
|
348
|
+
up(_db) {
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
version: 2,
|
|
353
|
+
description: "Add plugin_skills table for tracking loaded external plugins",
|
|
354
|
+
up(db) {
|
|
355
|
+
db.exec(`
|
|
356
|
+
CREATE TABLE IF NOT EXISTS plugin_skills (
|
|
357
|
+
name TEXT PRIMARY KEY,
|
|
358
|
+
file_path TEXT NOT NULL,
|
|
359
|
+
version TEXT NOT NULL,
|
|
360
|
+
loaded_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
361
|
+
enabled INTEGER NOT NULL DEFAULT 1
|
|
362
|
+
)
|
|
363
|
+
`);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
version: 3,
|
|
368
|
+
description: "Add memories and reminders tables",
|
|
369
|
+
up(db) {
|
|
370
|
+
db.exec(`
|
|
371
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
372
|
+
id TEXT PRIMARY KEY,
|
|
373
|
+
user_id TEXT NOT NULL,
|
|
374
|
+
key TEXT NOT NULL,
|
|
375
|
+
value TEXT NOT NULL,
|
|
376
|
+
category TEXT NOT NULL DEFAULT 'general',
|
|
377
|
+
created_at TEXT NOT NULL,
|
|
378
|
+
updated_at TEXT NOT NULL,
|
|
379
|
+
UNIQUE(user_id, key)
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
CREATE INDEX IF NOT EXISTS idx_memories_user
|
|
383
|
+
ON memories(user_id, updated_at DESC);
|
|
384
|
+
|
|
385
|
+
CREATE INDEX IF NOT EXISTS idx_memories_user_category
|
|
386
|
+
ON memories(user_id, category);
|
|
387
|
+
|
|
388
|
+
CREATE TABLE IF NOT EXISTS reminders (
|
|
389
|
+
id TEXT PRIMARY KEY,
|
|
390
|
+
user_id TEXT NOT NULL,
|
|
391
|
+
platform TEXT NOT NULL,
|
|
392
|
+
chat_id TEXT NOT NULL,
|
|
393
|
+
message TEXT NOT NULL,
|
|
394
|
+
trigger_at TEXT NOT NULL,
|
|
395
|
+
created_at TEXT NOT NULL,
|
|
396
|
+
fired INTEGER NOT NULL DEFAULT 0
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_due
|
|
400
|
+
ON reminders(fired, trigger_at);
|
|
401
|
+
|
|
402
|
+
CREATE INDEX IF NOT EXISTS idx_reminders_user
|
|
403
|
+
ON reminders(user_id, fired);
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
258
411
|
// ../storage/dist/database.js
|
|
259
412
|
import BetterSqlite3 from "better-sqlite3";
|
|
260
413
|
import fs2 from "node:fs";
|
|
@@ -263,6 +416,8 @@ var Database;
|
|
|
263
416
|
var init_database = __esm({
|
|
264
417
|
"../storage/dist/database.js"() {
|
|
265
418
|
"use strict";
|
|
419
|
+
init_migrator();
|
|
420
|
+
init_migrations();
|
|
266
421
|
Database = class {
|
|
267
422
|
db;
|
|
268
423
|
constructor(dbPath) {
|
|
@@ -271,6 +426,7 @@ var init_database = __esm({
|
|
|
271
426
|
this.db = new BetterSqlite3(dbPath);
|
|
272
427
|
this.db.pragma("journal_mode = WAL");
|
|
273
428
|
this.initTables();
|
|
429
|
+
this.runMigrations();
|
|
274
430
|
}
|
|
275
431
|
initTables() {
|
|
276
432
|
this.db.exec(`
|
|
@@ -325,6 +481,10 @@ var init_database = __esm({
|
|
|
325
481
|
ON users(platform, platform_user_id);
|
|
326
482
|
`);
|
|
327
483
|
}
|
|
484
|
+
runMigrations() {
|
|
485
|
+
const migrator = new Migrator(this.db);
|
|
486
|
+
migrator.migrate(MIGRATIONS);
|
|
487
|
+
}
|
|
328
488
|
getDb() {
|
|
329
489
|
return this.db;
|
|
330
490
|
}
|
|
@@ -630,21 +790,6 @@ var init_memory_repository = __esm({
|
|
|
630
790
|
}
|
|
631
791
|
});
|
|
632
792
|
|
|
633
|
-
// ../storage/dist/migrations/migrator.js
|
|
634
|
-
var init_migrator = __esm({
|
|
635
|
-
"../storage/dist/migrations/migrator.js"() {
|
|
636
|
-
"use strict";
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
// ../storage/dist/migrations/index.js
|
|
641
|
-
var init_migrations = __esm({
|
|
642
|
-
"../storage/dist/migrations/index.js"() {
|
|
643
|
-
"use strict";
|
|
644
|
-
init_migrator();
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
|
|
648
793
|
// ../storage/dist/repositories/reminder-repository.js
|
|
649
794
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
650
795
|
var ReminderRepository;
|
|
@@ -721,15 +866,56 @@ var init_dist3 = __esm({
|
|
|
721
866
|
});
|
|
722
867
|
|
|
723
868
|
// ../llm/dist/provider.js
|
|
724
|
-
|
|
869
|
+
function lookupContextWindow(model) {
|
|
870
|
+
if (KNOWN_CONTEXT_WINDOWS[model])
|
|
871
|
+
return KNOWN_CONTEXT_WINDOWS[model];
|
|
872
|
+
for (const [key, value] of Object.entries(KNOWN_CONTEXT_WINDOWS)) {
|
|
873
|
+
if (model.startsWith(key))
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
return void 0;
|
|
877
|
+
}
|
|
878
|
+
var KNOWN_CONTEXT_WINDOWS, DEFAULT_CONTEXT_WINDOW, LLMProvider;
|
|
725
879
|
var init_provider = __esm({
|
|
726
880
|
"../llm/dist/provider.js"() {
|
|
727
881
|
"use strict";
|
|
882
|
+
KNOWN_CONTEXT_WINDOWS = {
|
|
883
|
+
// Anthropic
|
|
884
|
+
"claude-opus-4-20250514": { maxInputTokens: 2e5, maxOutputTokens: 32e3 },
|
|
885
|
+
"claude-sonnet-4-20250514": { maxInputTokens: 2e5, maxOutputTokens: 16e3 },
|
|
886
|
+
"claude-haiku-3-5-20241022": { maxInputTokens: 2e5, maxOutputTokens: 8192 },
|
|
887
|
+
// OpenAI
|
|
888
|
+
"gpt-4o": { maxInputTokens: 128e3, maxOutputTokens: 16384 },
|
|
889
|
+
"gpt-4o-mini": { maxInputTokens: 128e3, maxOutputTokens: 16384 },
|
|
890
|
+
"gpt-4-turbo": { maxInputTokens: 128e3, maxOutputTokens: 4096 },
|
|
891
|
+
"gpt-4": { maxInputTokens: 8192, maxOutputTokens: 4096 },
|
|
892
|
+
"gpt-3.5-turbo": { maxInputTokens: 16384, maxOutputTokens: 4096 },
|
|
893
|
+
"o1": { maxInputTokens: 2e5, maxOutputTokens: 1e5 },
|
|
894
|
+
"o1-mini": { maxInputTokens: 128e3, maxOutputTokens: 65536 },
|
|
895
|
+
"o3-mini": { maxInputTokens: 2e5, maxOutputTokens: 1e5 },
|
|
896
|
+
// Common Ollama models
|
|
897
|
+
"llama3.2": { maxInputTokens: 128e3, maxOutputTokens: 4096 },
|
|
898
|
+
"llama3.1": { maxInputTokens: 128e3, maxOutputTokens: 4096 },
|
|
899
|
+
"llama3": { maxInputTokens: 8192, maxOutputTokens: 4096 },
|
|
900
|
+
"mistral": { maxInputTokens: 32e3, maxOutputTokens: 4096 },
|
|
901
|
+
"mistral-small": { maxInputTokens: 32e3, maxOutputTokens: 4096 },
|
|
902
|
+
"mixtral": { maxInputTokens: 32e3, maxOutputTokens: 4096 },
|
|
903
|
+
"gemma2": { maxInputTokens: 8192, maxOutputTokens: 4096 },
|
|
904
|
+
"qwen2.5": { maxInputTokens: 128e3, maxOutputTokens: 4096 },
|
|
905
|
+
"phi3": { maxInputTokens: 128e3, maxOutputTokens: 4096 },
|
|
906
|
+
"deepseek-r1": { maxInputTokens: 128e3, maxOutputTokens: 8192 },
|
|
907
|
+
"command-r": { maxInputTokens: 128e3, maxOutputTokens: 4096 }
|
|
908
|
+
};
|
|
909
|
+
DEFAULT_CONTEXT_WINDOW = { maxInputTokens: 8192, maxOutputTokens: 4096 };
|
|
728
910
|
LLMProvider = class {
|
|
729
911
|
config;
|
|
912
|
+
contextWindow = DEFAULT_CONTEXT_WINDOW;
|
|
730
913
|
constructor(config) {
|
|
731
914
|
this.config = config;
|
|
732
915
|
}
|
|
916
|
+
getContextWindow() {
|
|
917
|
+
return this.contextWindow;
|
|
918
|
+
}
|
|
733
919
|
};
|
|
734
920
|
}
|
|
735
921
|
});
|
|
@@ -748,6 +934,9 @@ var init_anthropic = __esm({
|
|
|
748
934
|
}
|
|
749
935
|
async initialize() {
|
|
750
936
|
this.client = new Anthropic({ apiKey: this.config.apiKey });
|
|
937
|
+
const cw = lookupContextWindow(this.config.model);
|
|
938
|
+
if (cw)
|
|
939
|
+
this.contextWindow = cw;
|
|
751
940
|
}
|
|
752
941
|
async complete(request) {
|
|
753
942
|
const messages = this.mapMessages(request.messages);
|
|
@@ -886,6 +1075,9 @@ var init_openai = __esm({
|
|
|
886
1075
|
apiKey: this.config.apiKey,
|
|
887
1076
|
baseURL: this.config.baseUrl
|
|
888
1077
|
});
|
|
1078
|
+
const cw = lookupContextWindow(this.config.model);
|
|
1079
|
+
if (cw)
|
|
1080
|
+
this.contextWindow = cw;
|
|
889
1081
|
}
|
|
890
1082
|
async complete(request) {
|
|
891
1083
|
const messages = this.mapMessages(request.messages, request.system);
|
|
@@ -1132,6 +1324,34 @@ var init_ollama = __esm({
|
|
|
1132
1324
|
const raw = this.config.baseUrl ?? "http://localhost:11434";
|
|
1133
1325
|
this.baseUrl = raw.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
1134
1326
|
this.apiKey = this.config.apiKey ?? "";
|
|
1327
|
+
const cw = lookupContextWindow(this.config.model);
|
|
1328
|
+
if (cw) {
|
|
1329
|
+
this.contextWindow = cw;
|
|
1330
|
+
} else {
|
|
1331
|
+
await this.fetchModelContextWindow();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async fetchModelContextWindow() {
|
|
1335
|
+
try {
|
|
1336
|
+
const res = await fetch(`${this.baseUrl}/api/show`, {
|
|
1337
|
+
method: "POST",
|
|
1338
|
+
headers: this.getHeaders(),
|
|
1339
|
+
body: JSON.stringify({ name: this.config.model })
|
|
1340
|
+
});
|
|
1341
|
+
if (!res.ok)
|
|
1342
|
+
return;
|
|
1343
|
+
const data = await res.json();
|
|
1344
|
+
const info = data.model_info ?? {};
|
|
1345
|
+
const ctxKey = Object.keys(info).find((k) => k.includes("context_length") || k === "num_ctx");
|
|
1346
|
+
const ctxLen = ctxKey ? Number(info[ctxKey]) : 0;
|
|
1347
|
+
if (ctxLen > 0) {
|
|
1348
|
+
this.contextWindow = {
|
|
1349
|
+
maxInputTokens: ctxLen,
|
|
1350
|
+
maxOutputTokens: Math.min(ctxLen, 4096)
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1135
1355
|
}
|
|
1136
1356
|
getHeaders() {
|
|
1137
1357
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -1414,6 +1634,29 @@ var init_provider_factory = __esm({
|
|
|
1414
1634
|
});
|
|
1415
1635
|
|
|
1416
1636
|
// ../llm/dist/prompt-builder.js
|
|
1637
|
+
function estimateTokens(text) {
|
|
1638
|
+
return Math.ceil(text.length / 3.5);
|
|
1639
|
+
}
|
|
1640
|
+
function estimateMessageTokens(msg) {
|
|
1641
|
+
if (typeof msg.content === "string") {
|
|
1642
|
+
return estimateTokens(msg.content) + 4;
|
|
1643
|
+
}
|
|
1644
|
+
let tokens = 4;
|
|
1645
|
+
for (const block of msg.content) {
|
|
1646
|
+
switch (block.type) {
|
|
1647
|
+
case "text":
|
|
1648
|
+
tokens += estimateTokens(block.text);
|
|
1649
|
+
break;
|
|
1650
|
+
case "tool_use":
|
|
1651
|
+
tokens += estimateTokens(block.name) + estimateTokens(JSON.stringify(block.input));
|
|
1652
|
+
break;
|
|
1653
|
+
case "tool_result":
|
|
1654
|
+
tokens += estimateTokens(block.content);
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return tokens;
|
|
1659
|
+
}
|
|
1417
1660
|
var PromptBuilder;
|
|
1418
1661
|
var init_prompt_builder = __esm({
|
|
1419
1662
|
"../llm/dist/prompt-builder.js"() {
|
|
@@ -2072,31 +2315,212 @@ var init_web_search = __esm({
|
|
|
2072
2315
|
"use strict";
|
|
2073
2316
|
init_skill();
|
|
2074
2317
|
WebSearchSkill = class extends Skill {
|
|
2318
|
+
config;
|
|
2075
2319
|
metadata = {
|
|
2076
2320
|
name: "web_search",
|
|
2077
|
-
description: "Search the web
|
|
2321
|
+
description: "Search the web for current information",
|
|
2078
2322
|
riskLevel: "read",
|
|
2079
|
-
version: "
|
|
2323
|
+
version: "1.1.0",
|
|
2080
2324
|
inputSchema: {
|
|
2081
2325
|
type: "object",
|
|
2082
2326
|
properties: {
|
|
2083
2327
|
query: {
|
|
2084
2328
|
type: "string",
|
|
2085
|
-
description: "
|
|
2329
|
+
description: "The search query"
|
|
2330
|
+
},
|
|
2331
|
+
count: {
|
|
2332
|
+
type: "number",
|
|
2333
|
+
description: "Number of results to return (default: 5, max: 10)"
|
|
2086
2334
|
}
|
|
2087
2335
|
},
|
|
2088
2336
|
required: ["query"]
|
|
2089
2337
|
}
|
|
2090
2338
|
};
|
|
2339
|
+
constructor(config) {
|
|
2340
|
+
super();
|
|
2341
|
+
this.config = config;
|
|
2342
|
+
}
|
|
2091
2343
|
async execute(input2, _context) {
|
|
2092
2344
|
const query = input2.query;
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2345
|
+
const count = Math.min(Math.max(1, input2.count || 5), 10);
|
|
2346
|
+
if (!query || typeof query !== "string") {
|
|
2347
|
+
return { success: false, error: 'Invalid input: "query" must be a non-empty string' };
|
|
2348
|
+
}
|
|
2349
|
+
if (!this.config) {
|
|
2350
|
+
return {
|
|
2351
|
+
success: false,
|
|
2352
|
+
error: "Web search is not configured. Run `alfred setup` to configure a search provider."
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
const needsKey = this.config.provider === "brave" || this.config.provider === "tavily";
|
|
2356
|
+
if (needsKey && !this.config.apiKey) {
|
|
2357
|
+
return {
|
|
2358
|
+
success: false,
|
|
2359
|
+
error: `Web search requires an API key for ${this.config.provider}. Run \`alfred setup\` to configure it.`
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
try {
|
|
2363
|
+
let results;
|
|
2364
|
+
switch (this.config.provider) {
|
|
2365
|
+
case "brave":
|
|
2366
|
+
results = await this.searchBrave(query, count);
|
|
2367
|
+
break;
|
|
2368
|
+
case "searxng":
|
|
2369
|
+
results = await this.searchSearXNG(query, count);
|
|
2370
|
+
break;
|
|
2371
|
+
case "tavily":
|
|
2372
|
+
results = await this.searchTavily(query, count);
|
|
2373
|
+
break;
|
|
2374
|
+
case "duckduckgo":
|
|
2375
|
+
results = await this.searchDuckDuckGo(query, count);
|
|
2376
|
+
break;
|
|
2377
|
+
default:
|
|
2378
|
+
return { success: false, error: `Unknown search provider: ${this.config.provider}` };
|
|
2379
|
+
}
|
|
2380
|
+
if (results.length === 0) {
|
|
2381
|
+
return {
|
|
2382
|
+
success: true,
|
|
2383
|
+
data: { results: [] },
|
|
2384
|
+
display: `No results found for "${query}".`
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
const display = results.map((r, i) => `${i + 1}. **${r.title}**
|
|
2388
|
+
${r.url}
|
|
2389
|
+
${r.snippet}`).join("\n\n");
|
|
2390
|
+
return {
|
|
2391
|
+
success: true,
|
|
2392
|
+
data: { query, results },
|
|
2393
|
+
display: `Search results for "${query}":
|
|
2394
|
+
|
|
2395
|
+
${display}`
|
|
2396
|
+
};
|
|
2397
|
+
} catch (err) {
|
|
2398
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2399
|
+
return { success: false, error: `Search failed: ${msg}` };
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
// ── Brave Search ──────────────────────────────────────────────
|
|
2403
|
+
async searchBrave(query, count) {
|
|
2404
|
+
const url = new URL("https://api.search.brave.com/res/v1/web/search");
|
|
2405
|
+
url.searchParams.set("q", query);
|
|
2406
|
+
url.searchParams.set("count", String(count));
|
|
2407
|
+
const response = await fetch(url.toString(), {
|
|
2408
|
+
headers: {
|
|
2409
|
+
"Accept": "application/json",
|
|
2410
|
+
"Accept-Encoding": "gzip",
|
|
2411
|
+
"X-Subscription-Token": this.config.apiKey
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
if (!response.ok) {
|
|
2415
|
+
throw new Error(`Brave Search API returned ${response.status}: ${response.statusText}`);
|
|
2416
|
+
}
|
|
2417
|
+
const data = await response.json();
|
|
2418
|
+
return (data.web?.results ?? []).slice(0, count).map((r) => ({
|
|
2419
|
+
title: r.title,
|
|
2420
|
+
url: r.url,
|
|
2421
|
+
snippet: r.description
|
|
2422
|
+
}));
|
|
2423
|
+
}
|
|
2424
|
+
// ── SearXNG ───────────────────────────────────────────────────
|
|
2425
|
+
async searchSearXNG(query, count) {
|
|
2426
|
+
const base = (this.config.baseUrl ?? "http://localhost:8080").replace(/\/+$/, "");
|
|
2427
|
+
const url = new URL(`${base}/search`);
|
|
2428
|
+
url.searchParams.set("q", query);
|
|
2429
|
+
url.searchParams.set("format", "json");
|
|
2430
|
+
url.searchParams.set("pageno", "1");
|
|
2431
|
+
const response = await fetch(url.toString(), {
|
|
2432
|
+
headers: { "Accept": "application/json" }
|
|
2433
|
+
});
|
|
2434
|
+
if (!response.ok) {
|
|
2435
|
+
throw new Error(`SearXNG returned ${response.status}: ${response.statusText}`);
|
|
2436
|
+
}
|
|
2437
|
+
const data = await response.json();
|
|
2438
|
+
return (data.results ?? []).slice(0, count).map((r) => ({
|
|
2439
|
+
title: r.title,
|
|
2440
|
+
url: r.url,
|
|
2441
|
+
snippet: r.content
|
|
2442
|
+
}));
|
|
2443
|
+
}
|
|
2444
|
+
// ── Tavily ────────────────────────────────────────────────────
|
|
2445
|
+
async searchTavily(query, count) {
|
|
2446
|
+
const response = await fetch("https://api.tavily.com/search", {
|
|
2447
|
+
method: "POST",
|
|
2448
|
+
headers: { "Content-Type": "application/json" },
|
|
2449
|
+
body: JSON.stringify({
|
|
2450
|
+
api_key: this.config.apiKey,
|
|
2451
|
+
query,
|
|
2452
|
+
max_results: count,
|
|
2453
|
+
include_answer: false
|
|
2454
|
+
})
|
|
2455
|
+
});
|
|
2456
|
+
if (!response.ok) {
|
|
2457
|
+
throw new Error(`Tavily API returned ${response.status}: ${response.statusText}`);
|
|
2458
|
+
}
|
|
2459
|
+
const data = await response.json();
|
|
2460
|
+
return (data.results ?? []).slice(0, count).map((r) => ({
|
|
2461
|
+
title: r.title,
|
|
2462
|
+
url: r.url,
|
|
2463
|
+
snippet: r.content
|
|
2464
|
+
}));
|
|
2465
|
+
}
|
|
2466
|
+
// ── DuckDuckGo (HTML scraping, no API key) ────────────────────
|
|
2467
|
+
async searchDuckDuckGo(query, count) {
|
|
2468
|
+
const url = new URL("https://html.duckduckgo.com/html/");
|
|
2469
|
+
url.searchParams.set("q", query);
|
|
2470
|
+
const response = await fetch(url.toString(), {
|
|
2471
|
+
headers: {
|
|
2472
|
+
"User-Agent": "Mozilla/5.0 (compatible; Alfred/1.0)"
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
if (!response.ok) {
|
|
2476
|
+
throw new Error(`DuckDuckGo returned ${response.status}: ${response.statusText}`);
|
|
2477
|
+
}
|
|
2478
|
+
const html = await response.text();
|
|
2479
|
+
return this.parseDuckDuckGoHtml(html, count);
|
|
2480
|
+
}
|
|
2481
|
+
parseDuckDuckGoHtml(html, count) {
|
|
2482
|
+
const results = [];
|
|
2483
|
+
const linkRegex = /<a[^>]+class="result__a"[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
2484
|
+
const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
2485
|
+
const links = [];
|
|
2486
|
+
let match;
|
|
2487
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
2488
|
+
const rawUrl = match[1];
|
|
2489
|
+
const title = this.stripHtml(match[2]).trim();
|
|
2490
|
+
const actualUrl = this.extractDdgUrl(rawUrl);
|
|
2491
|
+
if (title && actualUrl) {
|
|
2492
|
+
links.push({ url: actualUrl, title });
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const snippets = [];
|
|
2496
|
+
while ((match = snippetRegex.exec(html)) !== null) {
|
|
2497
|
+
snippets.push(this.stripHtml(match[1]).trim());
|
|
2498
|
+
}
|
|
2499
|
+
for (let i = 0; i < Math.min(links.length, count); i++) {
|
|
2500
|
+
results.push({
|
|
2501
|
+
title: links[i].title,
|
|
2502
|
+
url: links[i].url,
|
|
2503
|
+
snippet: snippets[i] ?? ""
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
return results;
|
|
2507
|
+
}
|
|
2508
|
+
extractDdgUrl(rawUrl) {
|
|
2509
|
+
try {
|
|
2510
|
+
if (rawUrl.includes("uddg=")) {
|
|
2511
|
+
const parsed = new URL(rawUrl, "https://duckduckgo.com");
|
|
2512
|
+
const uddg = parsed.searchParams.get("uddg");
|
|
2513
|
+
if (uddg)
|
|
2514
|
+
return decodeURIComponent(uddg);
|
|
2515
|
+
}
|
|
2516
|
+
} catch {
|
|
2517
|
+
}
|
|
2518
|
+
if (rawUrl.startsWith("http"))
|
|
2519
|
+
return rawUrl;
|
|
2520
|
+
return "";
|
|
2521
|
+
}
|
|
2522
|
+
stripHtml(html) {
|
|
2523
|
+
return html.replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ");
|
|
2100
2524
|
}
|
|
2101
2525
|
};
|
|
2102
2526
|
}
|
|
@@ -3026,6 +3450,328 @@ Additional context: ${additionalContext}`;
|
|
|
3026
3450
|
}
|
|
3027
3451
|
});
|
|
3028
3452
|
|
|
3453
|
+
// ../skills/dist/built-in/email.js
|
|
3454
|
+
var EmailSkill;
|
|
3455
|
+
var init_email = __esm({
|
|
3456
|
+
"../skills/dist/built-in/email.js"() {
|
|
3457
|
+
"use strict";
|
|
3458
|
+
init_skill();
|
|
3459
|
+
EmailSkill = class extends Skill {
|
|
3460
|
+
config;
|
|
3461
|
+
metadata = {
|
|
3462
|
+
name: "email",
|
|
3463
|
+
description: "Read, search, and send emails via IMAP/SMTP",
|
|
3464
|
+
riskLevel: "write",
|
|
3465
|
+
version: "1.0.0",
|
|
3466
|
+
inputSchema: {
|
|
3467
|
+
type: "object",
|
|
3468
|
+
properties: {
|
|
3469
|
+
action: {
|
|
3470
|
+
type: "string",
|
|
3471
|
+
enum: ["inbox", "read", "search", "send"],
|
|
3472
|
+
description: "The email action to perform"
|
|
3473
|
+
},
|
|
3474
|
+
count: {
|
|
3475
|
+
type: "number",
|
|
3476
|
+
description: "Number of emails to fetch (for inbox, default: 10)"
|
|
3477
|
+
},
|
|
3478
|
+
messageId: {
|
|
3479
|
+
type: "string",
|
|
3480
|
+
description: "Message sequence number to read (for read action)"
|
|
3481
|
+
},
|
|
3482
|
+
query: {
|
|
3483
|
+
type: "string",
|
|
3484
|
+
description: "Search query (for search action)"
|
|
3485
|
+
},
|
|
3486
|
+
to: {
|
|
3487
|
+
type: "string",
|
|
3488
|
+
description: "Recipient email address (for send action)"
|
|
3489
|
+
},
|
|
3490
|
+
subject: {
|
|
3491
|
+
type: "string",
|
|
3492
|
+
description: "Email subject (for send action)"
|
|
3493
|
+
},
|
|
3494
|
+
body: {
|
|
3495
|
+
type: "string",
|
|
3496
|
+
description: "Email body text (for send action)"
|
|
3497
|
+
}
|
|
3498
|
+
},
|
|
3499
|
+
required: ["action"]
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
constructor(config) {
|
|
3503
|
+
super();
|
|
3504
|
+
this.config = config;
|
|
3505
|
+
}
|
|
3506
|
+
async execute(input2, _context) {
|
|
3507
|
+
if (!this.config) {
|
|
3508
|
+
return {
|
|
3509
|
+
success: false,
|
|
3510
|
+
error: "Email is not configured. Run `alfred setup` to configure email access."
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
const action = input2.action;
|
|
3514
|
+
try {
|
|
3515
|
+
switch (action) {
|
|
3516
|
+
case "inbox":
|
|
3517
|
+
return await this.fetchInbox(input2.count);
|
|
3518
|
+
case "read":
|
|
3519
|
+
return await this.readMessage(input2.messageId);
|
|
3520
|
+
case "search":
|
|
3521
|
+
return await this.searchMessages(input2.query, input2.count);
|
|
3522
|
+
case "send":
|
|
3523
|
+
return await this.sendMessage(input2.to, input2.subject, input2.body);
|
|
3524
|
+
default:
|
|
3525
|
+
return { success: false, error: `Unknown action: ${action}. Use: inbox, read, search, send` };
|
|
3526
|
+
}
|
|
3527
|
+
} catch (err) {
|
|
3528
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3529
|
+
return { success: false, error: `Email error: ${msg}` };
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
// ── IMAP: Fetch inbox ──────────────────────────────────────────
|
|
3533
|
+
async fetchInbox(count) {
|
|
3534
|
+
const limit = Math.min(Math.max(1, count ?? 10), 50);
|
|
3535
|
+
const { ImapFlow } = await import("imapflow");
|
|
3536
|
+
const client = new ImapFlow({
|
|
3537
|
+
host: this.config.imap.host,
|
|
3538
|
+
port: this.config.imap.port,
|
|
3539
|
+
secure: this.config.imap.secure,
|
|
3540
|
+
auth: this.config.auth,
|
|
3541
|
+
logger: false
|
|
3542
|
+
});
|
|
3543
|
+
try {
|
|
3544
|
+
await client.connect();
|
|
3545
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
3546
|
+
try {
|
|
3547
|
+
const messages = [];
|
|
3548
|
+
const mb = client.mailbox;
|
|
3549
|
+
const totalMessages = mb && typeof mb === "object" ? mb.exists ?? 0 : 0;
|
|
3550
|
+
if (totalMessages === 0) {
|
|
3551
|
+
return { success: true, data: { messages: [] }, display: "Inbox is empty." };
|
|
3552
|
+
}
|
|
3553
|
+
const startSeq = Math.max(1, totalMessages - limit + 1);
|
|
3554
|
+
const range = `${startSeq}:*`;
|
|
3555
|
+
for await (const msg of client.fetch(range, {
|
|
3556
|
+
envelope: true,
|
|
3557
|
+
flags: true
|
|
3558
|
+
})) {
|
|
3559
|
+
const from = msg.envelope?.from?.[0];
|
|
3560
|
+
const fromStr = from ? from.name ? `${from.name} <${from.address}>` : from.address ?? "unknown" : "unknown";
|
|
3561
|
+
messages.push({
|
|
3562
|
+
seq: msg.seq,
|
|
3563
|
+
from: fromStr,
|
|
3564
|
+
subject: msg.envelope?.subject ?? "(no subject)",
|
|
3565
|
+
date: msg.envelope?.date?.toISOString() ?? "",
|
|
3566
|
+
seen: msg.flags?.has("\\Seen") ?? false
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
messages.reverse();
|
|
3570
|
+
const display = messages.length === 0 ? "No messages found." : messages.map((m, i) => {
|
|
3571
|
+
const unread = m.seen ? "" : " [UNREAD]";
|
|
3572
|
+
return `${i + 1}. [#${m.seq}]${unread} ${m.subject}
|
|
3573
|
+
From: ${m.from}
|
|
3574
|
+
Date: ${m.date}`;
|
|
3575
|
+
}).join("\n\n");
|
|
3576
|
+
const unreadCount = messages.filter((m) => !m.seen).length;
|
|
3577
|
+
return {
|
|
3578
|
+
success: true,
|
|
3579
|
+
data: { messages, totalMessages, unreadCount },
|
|
3580
|
+
display: `Inbox (${totalMessages} total, ${unreadCount} unread):
|
|
3581
|
+
|
|
3582
|
+
${display}`
|
|
3583
|
+
};
|
|
3584
|
+
} finally {
|
|
3585
|
+
lock.release();
|
|
3586
|
+
}
|
|
3587
|
+
} finally {
|
|
3588
|
+
await client.logout();
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
// ── IMAP: Read single message ──────────────────────────────────
|
|
3592
|
+
async readMessage(messageId) {
|
|
3593
|
+
if (!messageId) {
|
|
3594
|
+
return { success: false, error: "messageId is required. Use the sequence number from inbox." };
|
|
3595
|
+
}
|
|
3596
|
+
const seq = parseInt(messageId, 10);
|
|
3597
|
+
if (isNaN(seq) || seq < 1) {
|
|
3598
|
+
return { success: false, error: "messageId must be a positive number (sequence number)." };
|
|
3599
|
+
}
|
|
3600
|
+
const { ImapFlow } = await import("imapflow");
|
|
3601
|
+
const client = new ImapFlow({
|
|
3602
|
+
host: this.config.imap.host,
|
|
3603
|
+
port: this.config.imap.port,
|
|
3604
|
+
secure: this.config.imap.secure,
|
|
3605
|
+
auth: this.config.auth,
|
|
3606
|
+
logger: false
|
|
3607
|
+
});
|
|
3608
|
+
try {
|
|
3609
|
+
await client.connect();
|
|
3610
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
3611
|
+
try {
|
|
3612
|
+
const msg = await client.fetchOne(String(seq), {
|
|
3613
|
+
envelope: true,
|
|
3614
|
+
source: true
|
|
3615
|
+
});
|
|
3616
|
+
if (!msg) {
|
|
3617
|
+
return { success: false, error: `Message #${seq} not found.` };
|
|
3618
|
+
}
|
|
3619
|
+
const from = msg.envelope?.from?.[0];
|
|
3620
|
+
const fromStr = from ? from.name ? `${from.name} <${from.address}>` : from.address ?? "unknown" : "unknown";
|
|
3621
|
+
const to = msg.envelope?.to?.map((t) => t.name ? `${t.name} <${t.address}>` : t.address ?? "").join(", ") ?? "";
|
|
3622
|
+
const rawSource = msg.source?.toString() ?? "";
|
|
3623
|
+
const body = this.extractTextBody(rawSource);
|
|
3624
|
+
return {
|
|
3625
|
+
success: true,
|
|
3626
|
+
data: {
|
|
3627
|
+
seq,
|
|
3628
|
+
from: fromStr,
|
|
3629
|
+
to,
|
|
3630
|
+
subject: msg.envelope?.subject ?? "(no subject)",
|
|
3631
|
+
date: msg.envelope?.date?.toISOString() ?? "",
|
|
3632
|
+
body
|
|
3633
|
+
},
|
|
3634
|
+
display: [
|
|
3635
|
+
`From: ${fromStr}`,
|
|
3636
|
+
`To: ${to}`,
|
|
3637
|
+
`Subject: ${msg.envelope?.subject ?? "(no subject)"}`,
|
|
3638
|
+
`Date: ${msg.envelope?.date?.toISOString() ?? ""}`,
|
|
3639
|
+
"",
|
|
3640
|
+
body.slice(0, 3e3) + (body.length > 3e3 ? "\n\n... (truncated)" : "")
|
|
3641
|
+
].join("\n")
|
|
3642
|
+
};
|
|
3643
|
+
} finally {
|
|
3644
|
+
lock.release();
|
|
3645
|
+
}
|
|
3646
|
+
} finally {
|
|
3647
|
+
await client.logout();
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
// ── IMAP: Search messages ──────────────────────────────────────
|
|
3651
|
+
async searchMessages(query, count) {
|
|
3652
|
+
if (!query) {
|
|
3653
|
+
return { success: false, error: "query is required for search." };
|
|
3654
|
+
}
|
|
3655
|
+
const limit = Math.min(Math.max(1, count ?? 10), 50);
|
|
3656
|
+
const { ImapFlow } = await import("imapflow");
|
|
3657
|
+
const client = new ImapFlow({
|
|
3658
|
+
host: this.config.imap.host,
|
|
3659
|
+
port: this.config.imap.port,
|
|
3660
|
+
secure: this.config.imap.secure,
|
|
3661
|
+
auth: this.config.auth,
|
|
3662
|
+
logger: false
|
|
3663
|
+
});
|
|
3664
|
+
try {
|
|
3665
|
+
await client.connect();
|
|
3666
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
3667
|
+
try {
|
|
3668
|
+
const rawResult = await client.search({
|
|
3669
|
+
or: [
|
|
3670
|
+
{ subject: query },
|
|
3671
|
+
{ from: query },
|
|
3672
|
+
{ body: query }
|
|
3673
|
+
]
|
|
3674
|
+
});
|
|
3675
|
+
const searchResult = Array.isArray(rawResult) ? rawResult : [];
|
|
3676
|
+
if (searchResult.length === 0) {
|
|
3677
|
+
return { success: true, data: { results: [] }, display: `No emails found for "${query}".` };
|
|
3678
|
+
}
|
|
3679
|
+
const seqNums = searchResult.slice(-limit);
|
|
3680
|
+
const messages = [];
|
|
3681
|
+
for await (const msg of client.fetch(seqNums, { envelope: true })) {
|
|
3682
|
+
const from = msg.envelope?.from?.[0];
|
|
3683
|
+
const fromStr = from ? from.name ? `${from.name} <${from.address}>` : from.address ?? "unknown" : "unknown";
|
|
3684
|
+
messages.push({
|
|
3685
|
+
seq: msg.seq,
|
|
3686
|
+
from: fromStr,
|
|
3687
|
+
subject: msg.envelope?.subject ?? "(no subject)",
|
|
3688
|
+
date: msg.envelope?.date?.toISOString() ?? ""
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
messages.reverse();
|
|
3692
|
+
const display = messages.map((m, i) => `${i + 1}. [#${m.seq}] ${m.subject}
|
|
3693
|
+
From: ${m.from}
|
|
3694
|
+
Date: ${m.date}`).join("\n\n");
|
|
3695
|
+
return {
|
|
3696
|
+
success: true,
|
|
3697
|
+
data: { query, results: messages, totalMatches: seqNums.length },
|
|
3698
|
+
display: `Search results for "${query}" (${seqNums.length} matches):
|
|
3699
|
+
|
|
3700
|
+
${display}`
|
|
3701
|
+
};
|
|
3702
|
+
} finally {
|
|
3703
|
+
lock.release();
|
|
3704
|
+
}
|
|
3705
|
+
} finally {
|
|
3706
|
+
await client.logout();
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
// ── SMTP: Send message ─────────────────────────────────────────
|
|
3710
|
+
async sendMessage(to, subject, body) {
|
|
3711
|
+
if (!to)
|
|
3712
|
+
return { success: false, error: '"to" (recipient email) is required.' };
|
|
3713
|
+
if (!subject)
|
|
3714
|
+
return { success: false, error: '"subject" is required.' };
|
|
3715
|
+
if (!body)
|
|
3716
|
+
return { success: false, error: '"body" is required.' };
|
|
3717
|
+
const nodemailer = await import("nodemailer");
|
|
3718
|
+
const transport = nodemailer.createTransport({
|
|
3719
|
+
host: this.config.smtp.host,
|
|
3720
|
+
port: this.config.smtp.port,
|
|
3721
|
+
secure: this.config.smtp.secure,
|
|
3722
|
+
auth: this.config.auth
|
|
3723
|
+
});
|
|
3724
|
+
const info = await transport.sendMail({
|
|
3725
|
+
from: this.config.auth.user,
|
|
3726
|
+
to,
|
|
3727
|
+
subject,
|
|
3728
|
+
text: body
|
|
3729
|
+
});
|
|
3730
|
+
return {
|
|
3731
|
+
success: true,
|
|
3732
|
+
data: { messageId: info.messageId, to, subject },
|
|
3733
|
+
display: `Email sent to ${to}
|
|
3734
|
+
Subject: ${subject}
|
|
3735
|
+
Message ID: ${info.messageId}`
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
// ── Helper: extract text body from raw email source ────────────
|
|
3739
|
+
extractTextBody(rawSource) {
|
|
3740
|
+
const parts = rawSource.split(/\r?\n\r?\n/);
|
|
3741
|
+
if (parts.length < 2)
|
|
3742
|
+
return rawSource;
|
|
3743
|
+
const headers = parts[0].toLowerCase();
|
|
3744
|
+
if (!headers.includes("multipart")) {
|
|
3745
|
+
return this.decodeBody(parts.slice(1).join("\n\n"));
|
|
3746
|
+
}
|
|
3747
|
+
const boundaryMatch = headers.match(/boundary="?([^"\s;]+)"?/i) ?? rawSource.match(/boundary="?([^"\s;]+)"?/i);
|
|
3748
|
+
if (!boundaryMatch) {
|
|
3749
|
+
return parts.slice(1).join("\n\n").slice(0, 5e3);
|
|
3750
|
+
}
|
|
3751
|
+
const boundary = boundaryMatch[1];
|
|
3752
|
+
const sections = rawSource.split(`--${boundary}`);
|
|
3753
|
+
for (const section of sections) {
|
|
3754
|
+
const sectionLower = section.toLowerCase();
|
|
3755
|
+
if (sectionLower.includes("content-type: text/plain") || sectionLower.includes("content-type:text/plain")) {
|
|
3756
|
+
const bodyStart = section.indexOf("\n\n");
|
|
3757
|
+
if (bodyStart >= 0) {
|
|
3758
|
+
return this.decodeBody(section.slice(bodyStart + 2));
|
|
3759
|
+
}
|
|
3760
|
+
const bodyStartCr = section.indexOf("\r\n\r\n");
|
|
3761
|
+
if (bodyStartCr >= 0) {
|
|
3762
|
+
return this.decodeBody(section.slice(bodyStartCr + 4));
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return this.decodeBody(parts.slice(1).join("\n\n").slice(0, 5e3));
|
|
3767
|
+
}
|
|
3768
|
+
decodeBody(body) {
|
|
3769
|
+
return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).trim();
|
|
3770
|
+
}
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
});
|
|
3774
|
+
|
|
3029
3775
|
// ../skills/dist/index.js
|
|
3030
3776
|
var init_dist6 = __esm({
|
|
3031
3777
|
"../skills/dist/index.js"() {
|
|
@@ -3045,6 +3791,7 @@ var init_dist6 = __esm({
|
|
|
3045
3791
|
init_shell();
|
|
3046
3792
|
init_memory();
|
|
3047
3793
|
init_delegate();
|
|
3794
|
+
init_email();
|
|
3048
3795
|
}
|
|
3049
3796
|
});
|
|
3050
3797
|
|
|
@@ -3077,12 +3824,13 @@ var init_conversation_manager = __esm({
|
|
|
3077
3824
|
});
|
|
3078
3825
|
|
|
3079
3826
|
// ../core/dist/message-pipeline.js
|
|
3080
|
-
var MAX_TOOL_ITERATIONS, MessagePipeline;
|
|
3827
|
+
var MAX_TOOL_ITERATIONS, TOKEN_BUDGET_RATIO, MessagePipeline;
|
|
3081
3828
|
var init_message_pipeline = __esm({
|
|
3082
3829
|
"../core/dist/message-pipeline.js"() {
|
|
3083
3830
|
"use strict";
|
|
3084
3831
|
init_dist4();
|
|
3085
3832
|
MAX_TOOL_ITERATIONS = 10;
|
|
3833
|
+
TOKEN_BUDGET_RATIO = 0.85;
|
|
3086
3834
|
MessagePipeline = class {
|
|
3087
3835
|
llm;
|
|
3088
3836
|
conversationManager;
|
|
@@ -3110,7 +3858,7 @@ var init_message_pipeline = __esm({
|
|
|
3110
3858
|
try {
|
|
3111
3859
|
const user = this.users.findOrCreate(message.platform, message.userId, message.userName, message.displayName);
|
|
3112
3860
|
const conversation = this.conversationManager.getOrCreateConversation(message.platform, message.chatId, user.id);
|
|
3113
|
-
const history = this.conversationManager.getHistory(conversation.id);
|
|
3861
|
+
const history = this.conversationManager.getHistory(conversation.id, 50);
|
|
3114
3862
|
this.conversationManager.addMessage(conversation.id, "user", message.text);
|
|
3115
3863
|
let memories;
|
|
3116
3864
|
if (this.memoryRepo) {
|
|
@@ -3120,8 +3868,9 @@ var init_message_pipeline = __esm({
|
|
|
3120
3868
|
}
|
|
3121
3869
|
}
|
|
3122
3870
|
const system = this.promptBuilder.buildSystemPrompt(memories);
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3871
|
+
const allMessages = this.promptBuilder.buildMessages(history);
|
|
3872
|
+
allMessages.push({ role: "user", content: message.text });
|
|
3873
|
+
const messages = this.trimToContextWindow(system, allMessages);
|
|
3125
3874
|
const tools = this.skillRegistry ? this.promptBuilder.buildTools(this.skillRegistry.getAll().map((s) => s.metadata)) : void 0;
|
|
3126
3875
|
let response;
|
|
3127
3876
|
let iteration = 0;
|
|
@@ -3219,6 +3968,43 @@ var init_message_pipeline = __esm({
|
|
|
3219
3968
|
return { content: `Skill execution failed: ${msg}`, isError: true };
|
|
3220
3969
|
}
|
|
3221
3970
|
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Trim messages to fit within the LLM's context window.
|
|
3973
|
+
* Keeps the system prompt, the latest user message, and as many
|
|
3974
|
+
* recent history messages as possible. Drops oldest messages first.
|
|
3975
|
+
* Injects a summary note when messages are trimmed.
|
|
3976
|
+
*/
|
|
3977
|
+
trimToContextWindow(system, messages) {
|
|
3978
|
+
const contextWindow = this.llm.getContextWindow();
|
|
3979
|
+
const maxInputTokens = Math.floor(contextWindow.maxInputTokens * TOKEN_BUDGET_RATIO);
|
|
3980
|
+
const systemTokens = estimateTokens(system);
|
|
3981
|
+
const latestMsg = messages[messages.length - 1];
|
|
3982
|
+
const latestTokens = estimateMessageTokens(latestMsg);
|
|
3983
|
+
const reservedTokens = systemTokens + latestTokens + 200;
|
|
3984
|
+
let availableTokens = maxInputTokens - reservedTokens;
|
|
3985
|
+
if (availableTokens <= 0) {
|
|
3986
|
+
this.logger.warn({ maxInputTokens, systemTokens, latestTokens }, "Context window very tight, sending only latest message");
|
|
3987
|
+
return [latestMsg];
|
|
3988
|
+
}
|
|
3989
|
+
const keptMessages = [];
|
|
3990
|
+
for (let i = messages.length - 2; i >= 0; i--) {
|
|
3991
|
+
const msgTokens = estimateMessageTokens(messages[i]);
|
|
3992
|
+
if (msgTokens > availableTokens)
|
|
3993
|
+
break;
|
|
3994
|
+
availableTokens -= msgTokens;
|
|
3995
|
+
keptMessages.unshift(messages[i]);
|
|
3996
|
+
}
|
|
3997
|
+
const trimmedCount = messages.length - 1 - keptMessages.length;
|
|
3998
|
+
if (trimmedCount > 0) {
|
|
3999
|
+
this.logger.info({ trimmedCount, totalMessages: messages.length, maxInputTokens }, "Trimmed conversation history to fit context window");
|
|
4000
|
+
keptMessages.unshift({
|
|
4001
|
+
role: "user",
|
|
4002
|
+
content: `[System note: ${trimmedCount} older message(s) were omitted to fit the context window. The conversation continues from the most recent messages.]`
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
keptMessages.push(latestMsg);
|
|
4006
|
+
return keptMessages;
|
|
4007
|
+
}
|
|
3222
4008
|
};
|
|
3223
4009
|
}
|
|
3224
4010
|
});
|
|
@@ -3810,7 +4596,11 @@ var init_alfred = __esm({
|
|
|
3810
4596
|
const skillRegistry = new SkillRegistry();
|
|
3811
4597
|
skillRegistry.register(new CalculatorSkill());
|
|
3812
4598
|
skillRegistry.register(new SystemInfoSkill());
|
|
3813
|
-
skillRegistry.register(new WebSearchSkill(
|
|
4599
|
+
skillRegistry.register(new WebSearchSkill(this.config.search ? {
|
|
4600
|
+
provider: this.config.search.provider,
|
|
4601
|
+
apiKey: this.config.search.apiKey,
|
|
4602
|
+
baseUrl: this.config.search.baseUrl
|
|
4603
|
+
} : void 0));
|
|
3814
4604
|
skillRegistry.register(new ReminderSkill(reminderRepo));
|
|
3815
4605
|
skillRegistry.register(new NoteSkill());
|
|
3816
4606
|
skillRegistry.register(new SummarizeSkill());
|
|
@@ -3819,6 +4609,11 @@ var init_alfred = __esm({
|
|
|
3819
4609
|
skillRegistry.register(new ShellSkill());
|
|
3820
4610
|
skillRegistry.register(new MemorySkill(memoryRepo));
|
|
3821
4611
|
skillRegistry.register(new DelegateSkill(llmProvider));
|
|
4612
|
+
skillRegistry.register(new EmailSkill(this.config.email ? {
|
|
4613
|
+
imap: this.config.email.imap,
|
|
4614
|
+
smtp: this.config.email.smtp,
|
|
4615
|
+
auth: this.config.email.auth
|
|
4616
|
+
} : void 0));
|
|
3822
4617
|
this.logger.info({ skills: skillRegistry.getAll().map((s) => s.metadata.name) }, "Skills registered");
|
|
3823
4618
|
const skillSandbox = new SkillSandbox(this.logger.child({ component: "sandbox" }));
|
|
3824
4619
|
const conversationManager = new ConversationManager(conversationRepo);
|
|
@@ -4025,6 +4820,8 @@ function loadExistingConfig(projectRoot) {
|
|
|
4025
4820
|
const config = {};
|
|
4026
4821
|
const env = {};
|
|
4027
4822
|
let shellEnabled = false;
|
|
4823
|
+
let writeInGroups = false;
|
|
4824
|
+
let rateLimit = 30;
|
|
4028
4825
|
const configPath = path4.join(projectRoot, "config", "default.yml");
|
|
4029
4826
|
if (fs4.existsSync(configPath)) {
|
|
4030
4827
|
try {
|
|
@@ -4057,11 +4854,19 @@ function loadExistingConfig(projectRoot) {
|
|
|
4057
4854
|
const rulesContent = yaml2.load(fs4.readFileSync(rulesPath, "utf-8"));
|
|
4058
4855
|
if (rulesContent?.rules) {
|
|
4059
4856
|
shellEnabled = rulesContent.rules.some((r) => r.id === "allow-owner-admin" && r.effect === "allow");
|
|
4857
|
+
const writeDmRule = rulesContent.rules.find((r) => r.id === "allow-write-for-dm" || r.id === "allow-write-all");
|
|
4858
|
+
if (writeDmRule?.id === "allow-write-all") {
|
|
4859
|
+
writeInGroups = true;
|
|
4860
|
+
}
|
|
4861
|
+
const rlRule = rulesContent.rules.find((r) => r.id === "rate-limit-write");
|
|
4862
|
+
if (rlRule?.rateLimit?.maxInvocations) {
|
|
4863
|
+
rateLimit = rlRule.rateLimit.maxInvocations;
|
|
4864
|
+
}
|
|
4060
4865
|
}
|
|
4061
4866
|
} catch {
|
|
4062
4867
|
}
|
|
4063
4868
|
}
|
|
4064
|
-
return { config, env, shellEnabled };
|
|
4869
|
+
return { config, env, shellEnabled, writeInGroups, rateLimit };
|
|
4065
4870
|
}
|
|
4066
4871
|
async function setupCommand() {
|
|
4067
4872
|
const rl = createInterface({ input, output });
|
|
@@ -4114,6 +4919,58 @@ ${bold("Which LLM provider would you like to use?")}`);
|
|
|
4114
4919
|
const existingModel = existing.config.llm?.model ?? provider.defaultModel;
|
|
4115
4920
|
console.log("");
|
|
4116
4921
|
const model = await askWithDefault(rl, "Which model?", existingModel);
|
|
4922
|
+
const searchProviders = ["brave", "tavily", "duckduckgo", "searxng"];
|
|
4923
|
+
const existingSearchProvider = existing.config.search?.provider ?? existing.env["ALFRED_SEARCH_PROVIDER"] ?? "";
|
|
4924
|
+
const existingSearchIdx = searchProviders.indexOf(existingSearchProvider);
|
|
4925
|
+
const defaultSearchChoice = existingSearchIdx >= 0 ? existingSearchIdx + 1 : 0;
|
|
4926
|
+
console.log(`
|
|
4927
|
+
${bold("Web Search provider (for searching the internet):")}`);
|
|
4928
|
+
const searchLabels = [
|
|
4929
|
+
"Brave Search \u2014 recommended, free tier (2,000/month)",
|
|
4930
|
+
"Tavily \u2014 built for AI agents, free tier (1,000/month)",
|
|
4931
|
+
"DuckDuckGo \u2014 free, no API key needed",
|
|
4932
|
+
"SearXNG \u2014 self-hosted, no API key needed"
|
|
4933
|
+
];
|
|
4934
|
+
const mark = (i) => existingSearchIdx === i ? ` ${dim("(current)")}` : "";
|
|
4935
|
+
console.log(` ${cyan("0)")} None (disable web search)${existingSearchIdx === -1 && existingSearchProvider === "" ? ` ${dim("(current)")}` : ""}`);
|
|
4936
|
+
for (let i = 0; i < searchLabels.length; i++) {
|
|
4937
|
+
console.log(` ${cyan(String(i + 1) + ")")} ${searchLabels[i]}${mark(i)}`);
|
|
4938
|
+
}
|
|
4939
|
+
const searchChoice = await askNumber(rl, "> ", 0, searchProviders.length, defaultSearchChoice);
|
|
4940
|
+
let searchProvider;
|
|
4941
|
+
let searchApiKey = "";
|
|
4942
|
+
let searchBaseUrl = "";
|
|
4943
|
+
if (searchChoice >= 1 && searchChoice <= searchProviders.length) {
|
|
4944
|
+
searchProvider = searchProviders[searchChoice - 1];
|
|
4945
|
+
}
|
|
4946
|
+
if (searchProvider === "brave") {
|
|
4947
|
+
const existingKey = existing.env["ALFRED_SEARCH_API_KEY"] ?? "";
|
|
4948
|
+
if (existingKey) {
|
|
4949
|
+
searchApiKey = await askWithDefault(rl, " Brave Search API key", existingKey);
|
|
4950
|
+
} else {
|
|
4951
|
+
console.log(` ${dim("Get your free API key at: https://brave.com/search/api/")}`);
|
|
4952
|
+
searchApiKey = await askRequired(rl, " Brave Search API key");
|
|
4953
|
+
}
|
|
4954
|
+
console.log(` ${green(">")} Brave Search: ${dim(maskKey(searchApiKey))}`);
|
|
4955
|
+
} else if (searchProvider === "tavily") {
|
|
4956
|
+
const existingKey = existing.env["ALFRED_SEARCH_API_KEY"] ?? "";
|
|
4957
|
+
if (existingKey) {
|
|
4958
|
+
searchApiKey = await askWithDefault(rl, " Tavily API key", existingKey);
|
|
4959
|
+
} else {
|
|
4960
|
+
console.log(` ${dim("Get your free API key at: https://tavily.com/")}`);
|
|
4961
|
+
searchApiKey = await askRequired(rl, " Tavily API key");
|
|
4962
|
+
}
|
|
4963
|
+
console.log(` ${green(">")} Tavily: ${dim(maskKey(searchApiKey))}`);
|
|
4964
|
+
} else if (searchProvider === "duckduckgo") {
|
|
4965
|
+
console.log(` ${green(">")} DuckDuckGo: ${dim("no API key needed")}`);
|
|
4966
|
+
} else if (searchProvider === "searxng") {
|
|
4967
|
+
const existingSearxUrl = existing.config.search?.baseUrl ?? existing.env["ALFRED_SEARCH_BASE_URL"] ?? "http://localhost:8080";
|
|
4968
|
+
searchBaseUrl = await askWithDefault(rl, " SearXNG URL", existingSearxUrl);
|
|
4969
|
+
searchBaseUrl = searchBaseUrl.replace(/\/+$/, "");
|
|
4970
|
+
console.log(` ${green(">")} SearXNG: ${dim(searchBaseUrl)}`);
|
|
4971
|
+
} else {
|
|
4972
|
+
console.log(` ${dim("Web search disabled \u2014 you can configure it later.")}`);
|
|
4973
|
+
}
|
|
4117
4974
|
const currentlyEnabled = [];
|
|
4118
4975
|
for (let i = 0; i < PLATFORMS.length; i++) {
|
|
4119
4976
|
const p = PLATFORMS[i];
|
|
@@ -4186,8 +5043,73 @@ ${bold(platform.label + " configuration:")}`);
|
|
|
4186
5043
|
}
|
|
4187
5044
|
platformCredentials[platform.configKey] = creds;
|
|
4188
5045
|
}
|
|
5046
|
+
const existingEmailUser = existing.config.email?.auth?.user ?? existing.env["ALFRED_EMAIL_USER"] ?? "";
|
|
5047
|
+
const hasEmail = !!existingEmailUser;
|
|
5048
|
+
const emailDefault = hasEmail ? "Y/n" : "y/N";
|
|
5049
|
+
console.log(`
|
|
5050
|
+
${bold("Email access (read & send emails via IMAP/SMTP)?")}`);
|
|
5051
|
+
console.log(`${dim("Works with Gmail, Outlook, or any IMAP/SMTP provider.")}`);
|
|
5052
|
+
const emailAnswer = (await rl.question(`${YELLOW}> ${RESET}${dim(`[${emailDefault}] `)}`)).trim().toLowerCase();
|
|
5053
|
+
const enableEmail = emailAnswer === "" ? hasEmail : emailAnswer === "y" || emailAnswer === "yes";
|
|
5054
|
+
let emailUser = "";
|
|
5055
|
+
let emailPass = "";
|
|
5056
|
+
let emailImapHost = "";
|
|
5057
|
+
let emailImapPort = 993;
|
|
5058
|
+
let emailSmtpHost = "";
|
|
5059
|
+
let emailSmtpPort = 587;
|
|
5060
|
+
if (enableEmail) {
|
|
5061
|
+
console.log("");
|
|
5062
|
+
emailUser = await askWithDefault(rl, " Email address", existingEmailUser || "");
|
|
5063
|
+
if (!emailUser) {
|
|
5064
|
+
emailUser = await askRequired(rl, " Email address");
|
|
5065
|
+
}
|
|
5066
|
+
const existingPass = existing.env["ALFRED_EMAIL_PASS"] ?? "";
|
|
5067
|
+
if (existingPass) {
|
|
5068
|
+
emailPass = await askWithDefault(rl, " Password / App password", existingPass);
|
|
5069
|
+
} else {
|
|
5070
|
+
console.log(` ${dim("For Gmail: use an App Password (not your regular password)")}`);
|
|
5071
|
+
console.log(` ${dim(" \u2192 Google Account \u2192 Security \u2192 2-Step \u2192 App passwords")}`);
|
|
5072
|
+
emailPass = await askRequired(rl, " Password / App password");
|
|
5073
|
+
}
|
|
5074
|
+
const domain = emailUser.split("@")[1]?.toLowerCase() ?? "";
|
|
5075
|
+
const presets = {
|
|
5076
|
+
"gmail.com": { imap: "imap.gmail.com", smtp: "smtp.gmail.com" },
|
|
5077
|
+
"googlemail.com": { imap: "imap.gmail.com", smtp: "smtp.gmail.com" },
|
|
5078
|
+
"outlook.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
|
|
5079
|
+
"hotmail.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
|
|
5080
|
+
"live.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
|
|
5081
|
+
"yahoo.com": { imap: "imap.mail.yahoo.com", smtp: "smtp.mail.yahoo.com" },
|
|
5082
|
+
"icloud.com": { imap: "imap.mail.me.com", smtp: "smtp.mail.me.com" },
|
|
5083
|
+
"me.com": { imap: "imap.mail.me.com", smtp: "smtp.mail.me.com" },
|
|
5084
|
+
"gmx.de": { imap: "imap.gmx.net", smtp: "mail.gmx.net" },
|
|
5085
|
+
"gmx.net": { imap: "imap.gmx.net", smtp: "mail.gmx.net" },
|
|
5086
|
+
"web.de": { imap: "imap.web.de", smtp: "smtp.web.de" },
|
|
5087
|
+
"posteo.de": { imap: "posteo.de", smtp: "posteo.de" },
|
|
5088
|
+
"mailbox.org": { imap: "imap.mailbox.org", smtp: "smtp.mailbox.org" },
|
|
5089
|
+
"protonmail.com": { imap: "127.0.0.1", smtp: "127.0.0.1" },
|
|
5090
|
+
"proton.me": { imap: "127.0.0.1", smtp: "127.0.0.1" }
|
|
5091
|
+
};
|
|
5092
|
+
const preset = presets[domain];
|
|
5093
|
+
const defaultImap = existing.config.email?.imap?.host ?? preset?.imap ?? `imap.${domain}`;
|
|
5094
|
+
const defaultSmtp = existing.config.email?.smtp?.host ?? preset?.smtp ?? `smtp.${domain}`;
|
|
5095
|
+
const defaultImapPort = existing.config.email?.imap?.port ?? 993;
|
|
5096
|
+
const defaultSmtpPort = existing.config.email?.smtp?.port ?? 587;
|
|
5097
|
+
if (preset) {
|
|
5098
|
+
console.log(` ${green(">")} Detected ${domain} \u2014 using preset server settings`);
|
|
5099
|
+
}
|
|
5100
|
+
emailImapHost = await askWithDefault(rl, " IMAP server", defaultImap);
|
|
5101
|
+
const imapPortStr = await askWithDefault(rl, " IMAP port", String(defaultImapPort));
|
|
5102
|
+
emailImapPort = parseInt(imapPortStr, 10) || 993;
|
|
5103
|
+
emailSmtpHost = await askWithDefault(rl, " SMTP server", defaultSmtp);
|
|
5104
|
+
const smtpPortStr = await askWithDefault(rl, " SMTP port", String(defaultSmtpPort));
|
|
5105
|
+
emailSmtpPort = parseInt(smtpPortStr, 10) || 587;
|
|
5106
|
+
console.log(` ${green(">")} Email: ${dim(emailUser)} via ${dim(emailImapHost)}`);
|
|
5107
|
+
} else {
|
|
5108
|
+
console.log(` ${dim("Email disabled \u2014 you can configure it later.")}`);
|
|
5109
|
+
}
|
|
5110
|
+
console.log(`
|
|
5111
|
+
${bold("Security configuration:")}`);
|
|
4189
5112
|
const existingOwnerId = existing.config.security?.ownerUserId ?? existing.env["ALFRED_OWNER_USER_ID"] ?? "";
|
|
4190
|
-
console.log("");
|
|
4191
5113
|
let ownerUserId;
|
|
4192
5114
|
if (existingOwnerId) {
|
|
4193
5115
|
ownerUserId = await askWithDefault(rl, "Owner user ID (for elevated permissions)", existingOwnerId);
|
|
@@ -4200,21 +5122,41 @@ ${bold(platform.label + " configuration:")}`);
|
|
|
4200
5122
|
if (ownerUserId) {
|
|
4201
5123
|
const shellDefault = existing.shellEnabled ? "Y/n" : "y/N";
|
|
4202
5124
|
console.log("");
|
|
4203
|
-
console.log(
|
|
4204
|
-
console.log(
|
|
4205
|
-
|
|
4206
|
-
const shellAnswer = (await rl.question(`${YELLOW}> ${RESET}${dim(`[${shellDefault}] `)}`)).trim().toLowerCase();
|
|
5125
|
+
console.log(` ${bold("Enable shell access (admin commands) for the owner?")}`);
|
|
5126
|
+
console.log(` ${dim("Allows Alfred to execute shell commands. Only for the owner.")}`);
|
|
5127
|
+
const shellAnswer = (await rl.question(` ${YELLOW}> ${RESET}${dim(`[${shellDefault}] `)}`)).trim().toLowerCase();
|
|
4207
5128
|
if (shellAnswer === "") {
|
|
4208
5129
|
enableShell = existing.shellEnabled;
|
|
4209
5130
|
} else {
|
|
4210
5131
|
enableShell = shellAnswer === "y" || shellAnswer === "yes";
|
|
4211
5132
|
}
|
|
4212
5133
|
if (enableShell) {
|
|
4213
|
-
console.log(`
|
|
5134
|
+
console.log(` ${green(">")} Shell access ${bold("enabled")} for owner ${dim(ownerUserId)}`);
|
|
4214
5135
|
} else {
|
|
4215
|
-
console.log(`
|
|
5136
|
+
console.log(` ${dim("Shell access disabled.")}`);
|
|
4216
5137
|
}
|
|
4217
5138
|
}
|
|
5139
|
+
const writeGroupsDefault = existing.writeInGroups ? "Y/n" : "y/N";
|
|
5140
|
+
console.log("");
|
|
5141
|
+
console.log(` ${bold("Allow write actions (notes, reminders, memory) in group chats?")}`);
|
|
5142
|
+
console.log(` ${dim("By default, write actions are only allowed in DMs.")}`);
|
|
5143
|
+
const writeGroupsAnswer = (await rl.question(` ${YELLOW}> ${RESET}${dim(`[${writeGroupsDefault}] `)}`)).trim().toLowerCase();
|
|
5144
|
+
let writeInGroups;
|
|
5145
|
+
if (writeGroupsAnswer === "") {
|
|
5146
|
+
writeInGroups = existing.writeInGroups;
|
|
5147
|
+
} else {
|
|
5148
|
+
writeInGroups = writeGroupsAnswer === "y" || writeGroupsAnswer === "yes";
|
|
5149
|
+
}
|
|
5150
|
+
if (writeInGroups) {
|
|
5151
|
+
console.log(` ${green(">")} Write actions ${bold("enabled")} in groups`);
|
|
5152
|
+
} else {
|
|
5153
|
+
console.log(` ${dim("Write actions only in DMs (default).")}`);
|
|
5154
|
+
}
|
|
5155
|
+
const existingRateLimit = existing.rateLimit ?? 30;
|
|
5156
|
+
console.log("");
|
|
5157
|
+
const rateLimitStr = await askWithDefault(rl, " Rate limit (max write actions per hour per user)", String(existingRateLimit));
|
|
5158
|
+
const rateLimit = Math.max(1, parseInt(rateLimitStr, 10) || 30);
|
|
5159
|
+
console.log(` ${green(">")} Rate limit: ${bold(String(rateLimit))} per hour`);
|
|
4218
5160
|
console.log(`
|
|
4219
5161
|
${bold("Writing configuration files...")}`);
|
|
4220
5162
|
const envLines = [
|
|
@@ -4239,6 +5181,27 @@ ${bold("Writing configuration files...")}`);
|
|
|
4239
5181
|
for (const [envKey, envVal] of Object.entries(envOverrides)) {
|
|
4240
5182
|
envLines.push(`${envKey}=${envVal}`);
|
|
4241
5183
|
}
|
|
5184
|
+
envLines.push("", "# === Web Search ===", "");
|
|
5185
|
+
if (searchProvider) {
|
|
5186
|
+
envLines.push(`ALFRED_SEARCH_PROVIDER=${searchProvider}`);
|
|
5187
|
+
if (searchApiKey) {
|
|
5188
|
+
envLines.push(`ALFRED_SEARCH_API_KEY=${searchApiKey}`);
|
|
5189
|
+
}
|
|
5190
|
+
if (searchBaseUrl) {
|
|
5191
|
+
envLines.push(`ALFRED_SEARCH_BASE_URL=${searchBaseUrl}`);
|
|
5192
|
+
}
|
|
5193
|
+
} else {
|
|
5194
|
+
envLines.push("# ALFRED_SEARCH_PROVIDER=brave");
|
|
5195
|
+
envLines.push("# ALFRED_SEARCH_API_KEY=");
|
|
5196
|
+
}
|
|
5197
|
+
envLines.push("", "# === Email ===", "");
|
|
5198
|
+
if (enableEmail) {
|
|
5199
|
+
envLines.push(`ALFRED_EMAIL_USER=${emailUser}`);
|
|
5200
|
+
envLines.push(`ALFRED_EMAIL_PASS=${emailPass}`);
|
|
5201
|
+
} else {
|
|
5202
|
+
envLines.push("# ALFRED_EMAIL_USER=");
|
|
5203
|
+
envLines.push("# ALFRED_EMAIL_PASS=");
|
|
5204
|
+
}
|
|
4242
5205
|
envLines.push("", "# === Security ===", "");
|
|
4243
5206
|
if (ownerUserId) {
|
|
4244
5207
|
envLines.push(`ALFRED_OWNER_USER_ID=${ownerUserId}`);
|
|
@@ -4285,6 +5248,20 @@ ${bold("Writing configuration files...")}`);
|
|
|
4285
5248
|
temperature: 0.7,
|
|
4286
5249
|
maxTokens: 4096
|
|
4287
5250
|
},
|
|
5251
|
+
...searchProvider ? {
|
|
5252
|
+
search: {
|
|
5253
|
+
provider: searchProvider,
|
|
5254
|
+
...searchApiKey ? { apiKey: searchApiKey } : {},
|
|
5255
|
+
...searchBaseUrl ? { baseUrl: searchBaseUrl } : {}
|
|
5256
|
+
}
|
|
5257
|
+
} : {},
|
|
5258
|
+
...enableEmail ? {
|
|
5259
|
+
email: {
|
|
5260
|
+
imap: { host: emailImapHost, port: emailImapPort, secure: emailImapPort === 993 },
|
|
5261
|
+
smtp: { host: emailSmtpHost, port: emailSmtpPort, secure: emailSmtpPort === 465 },
|
|
5262
|
+
auth: { user: emailUser, pass: emailPass }
|
|
5263
|
+
}
|
|
5264
|
+
} : {},
|
|
4288
5265
|
storage: {
|
|
4289
5266
|
path: "./data/alfred.db"
|
|
4290
5267
|
},
|
|
@@ -4331,6 +5308,21 @@ ${bold("Writing configuration files...")}`);
|
|
|
4331
5308
|
# conditions:
|
|
4332
5309
|
# users: ["${ownerUserId || "YOUR_USER_ID_HERE"}"]
|
|
4333
5310
|
`;
|
|
5311
|
+
const writeRule = writeInGroups ? ` # Allow write-level skills everywhere (DMs and groups)
|
|
5312
|
+
- id: allow-write-all
|
|
5313
|
+
effect: allow
|
|
5314
|
+
priority: 200
|
|
5315
|
+
scope: global
|
|
5316
|
+
actions: ["*"]
|
|
5317
|
+
riskLevels: [write]` : ` # Allow write-level skills in DMs only
|
|
5318
|
+
- id: allow-write-for-dm
|
|
5319
|
+
effect: allow
|
|
5320
|
+
priority: 200
|
|
5321
|
+
scope: global
|
|
5322
|
+
actions: ["*"]
|
|
5323
|
+
riskLevels: [write]
|
|
5324
|
+
conditions:
|
|
5325
|
+
chatType: dm`;
|
|
4334
5326
|
const rulesYaml = `# Alfred \u2014 Default Security Rules
|
|
4335
5327
|
# Rules are evaluated in priority order (lower number = higher priority).
|
|
4336
5328
|
# First matching rule wins.
|
|
@@ -4344,17 +5336,9 @@ rules:
|
|
|
4344
5336
|
actions: ["*"]
|
|
4345
5337
|
riskLevels: [read]
|
|
4346
5338
|
|
|
4347
|
-
|
|
4348
|
-
- id: allow-write-for-dm
|
|
4349
|
-
effect: allow
|
|
4350
|
-
priority: 200
|
|
4351
|
-
scope: global
|
|
4352
|
-
actions: ["*"]
|
|
4353
|
-
riskLevels: [write]
|
|
4354
|
-
conditions:
|
|
4355
|
-
chatType: dm
|
|
5339
|
+
${writeRule}
|
|
4356
5340
|
|
|
4357
|
-
# Rate-limit write actions: max
|
|
5341
|
+
# Rate-limit write actions: max ${rateLimit} per hour per user
|
|
4358
5342
|
- id: rate-limit-write
|
|
4359
5343
|
effect: allow
|
|
4360
5344
|
priority: 250
|
|
@@ -4362,7 +5346,7 @@ rules:
|
|
|
4362
5346
|
actions: ["*"]
|
|
4363
5347
|
riskLevels: [write]
|
|
4364
5348
|
rateLimit:
|
|
4365
|
-
maxInvocations:
|
|
5349
|
+
maxInvocations: ${rateLimit}
|
|
4366
5350
|
windowSeconds: 3600
|
|
4367
5351
|
${ownerAdminRule}
|
|
4368
5352
|
# Deny destructive and admin actions by default
|
|
@@ -4404,10 +5388,28 @@ ${ownerAdminRule}
|
|
|
4404
5388
|
} else {
|
|
4405
5389
|
console.log(` ${bold("Platforms:")} none (configure later)`);
|
|
4406
5390
|
}
|
|
5391
|
+
if (searchProvider) {
|
|
5392
|
+
const searchLabelMap = {
|
|
5393
|
+
brave: "Brave Search",
|
|
5394
|
+
tavily: "Tavily",
|
|
5395
|
+
duckduckgo: "DuckDuckGo",
|
|
5396
|
+
searxng: `SearXNG (${searchBaseUrl})`
|
|
5397
|
+
};
|
|
5398
|
+
console.log(` ${bold("Web search:")} ${searchLabelMap[searchProvider]}`);
|
|
5399
|
+
} else {
|
|
5400
|
+
console.log(` ${bold("Web search:")} ${dim("disabled")}`);
|
|
5401
|
+
}
|
|
5402
|
+
if (enableEmail) {
|
|
5403
|
+
console.log(` ${bold("Email:")} ${emailUser} (${emailImapHost})`);
|
|
5404
|
+
} else {
|
|
5405
|
+
console.log(` ${bold("Email:")} ${dim("disabled")}`);
|
|
5406
|
+
}
|
|
4407
5407
|
if (ownerUserId) {
|
|
4408
5408
|
console.log(` ${bold("Owner ID:")} ${ownerUserId}`);
|
|
4409
5409
|
console.log(` ${bold("Shell access:")} ${enableShell ? green("enabled") : dim("disabled")}`);
|
|
4410
5410
|
}
|
|
5411
|
+
console.log(` ${bold("Write scope:")} ${writeInGroups ? "DMs + Groups" : "DMs only"}`);
|
|
5412
|
+
console.log(` ${bold("Rate limit:")} ${rateLimit}/hour per user`);
|
|
4411
5413
|
console.log("");
|
|
4412
5414
|
console.log(`${CYAN}Next steps:${RESET}`);
|
|
4413
5415
|
console.log(` ${bold("alfred start")} Start Alfred`);
|