@madh-io/alfred-ai 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bundle/index.js +1142 -107
  2. 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
- var LLMProvider;
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 (placeholder \u2014 returns mock results)",
2321
+ description: "Search the web for current information",
2078
2322
  riskLevel: "read",
2079
- version: "0.1.0",
2323
+ version: "1.1.0",
2080
2324
  inputSchema: {
2081
2325
  type: "object",
2082
2326
  properties: {
2083
2327
  query: {
2084
2328
  type: "string",
2085
- description: "Search query"
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
- return {
2094
- success: true,
2095
- data: {
2096
- note: "Web search is not yet connected to a search API"
2097
- },
2098
- display: `Web search for "${query}" is not yet implemented. This skill will be connected to a search API in a future update.`
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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&nbsp;/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 messages = this.promptBuilder.buildMessages(history);
3124
- messages.push({ role: "user", content: message.text });
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
  });
@@ -3768,6 +4554,9 @@ var init_dist7 = __esm({
3768
4554
  });
3769
4555
 
3770
4556
  // ../core/dist/alfred.js
4557
+ import fs4 from "node:fs";
4558
+ import path4 from "node:path";
4559
+ import yaml2 from "js-yaml";
3771
4560
  var Alfred;
3772
4561
  var init_alfred = __esm({
3773
4562
  "../core/dist/alfred.js"() {
@@ -3802,15 +4591,21 @@ var init_alfred = __esm({
3802
4591
  const reminderRepo = new ReminderRepository(db);
3803
4592
  this.logger.info("Storage initialized");
3804
4593
  const ruleEngine = new RuleEngine();
4594
+ const rules = this.loadSecurityRules();
4595
+ ruleEngine.loadRules(rules);
3805
4596
  const securityManager = new SecurityManager(ruleEngine, auditRepo, this.logger.child({ component: "security" }));
3806
- this.logger.info("Security engine initialized");
4597
+ this.logger.info({ ruleCount: rules.length }, "Security engine initialized");
3807
4598
  const llmProvider = createLLMProvider(this.config.llm);
3808
4599
  await llmProvider.initialize();
3809
4600
  this.logger.info({ provider: this.config.llm.provider, model: this.config.llm.model }, "LLM provider initialized");
3810
4601
  const skillRegistry = new SkillRegistry();
3811
4602
  skillRegistry.register(new CalculatorSkill());
3812
4603
  skillRegistry.register(new SystemInfoSkill());
3813
- skillRegistry.register(new WebSearchSkill());
4604
+ skillRegistry.register(new WebSearchSkill(this.config.search ? {
4605
+ provider: this.config.search.provider,
4606
+ apiKey: this.config.search.apiKey,
4607
+ baseUrl: this.config.search.baseUrl
4608
+ } : void 0));
3814
4609
  skillRegistry.register(new ReminderSkill(reminderRepo));
3815
4610
  skillRegistry.register(new NoteSkill());
3816
4611
  skillRegistry.register(new SummarizeSkill());
@@ -3819,6 +4614,11 @@ var init_alfred = __esm({
3819
4614
  skillRegistry.register(new ShellSkill());
3820
4615
  skillRegistry.register(new MemorySkill(memoryRepo));
3821
4616
  skillRegistry.register(new DelegateSkill(llmProvider));
4617
+ skillRegistry.register(new EmailSkill(this.config.email ? {
4618
+ imap: this.config.email.imap,
4619
+ smtp: this.config.email.smtp,
4620
+ auth: this.config.email.auth
4621
+ } : void 0));
3822
4622
  this.logger.info({ skills: skillRegistry.getAll().map((s) => s.metadata.name) }, "Skills registered");
3823
4623
  const skillSandbox = new SkillSandbox(this.logger.child({ component: "sandbox" }));
3824
4624
  const conversationManager = new ConversationManager(conversationRepo);
@@ -3913,6 +4713,34 @@ var init_alfred = __esm({
3913
4713
  this.logger.warn({ platform }, "Adapter disconnected");
3914
4714
  });
3915
4715
  }
4716
+ loadSecurityRules() {
4717
+ const rulesPath = path4.resolve(this.config.security.rulesPath);
4718
+ const rules = [];
4719
+ if (!fs4.existsSync(rulesPath)) {
4720
+ this.logger.warn({ rulesPath }, "Security rules directory not found, using default deny");
4721
+ return rules;
4722
+ }
4723
+ const stat = fs4.statSync(rulesPath);
4724
+ if (!stat.isDirectory()) {
4725
+ this.logger.warn({ rulesPath }, "Security rules path is not a directory");
4726
+ return rules;
4727
+ }
4728
+ const files = fs4.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
4729
+ for (const file of files) {
4730
+ try {
4731
+ const filePath = path4.join(rulesPath, file);
4732
+ const content = fs4.readFileSync(filePath, "utf-8");
4733
+ const parsed = yaml2.load(content);
4734
+ if (parsed?.rules && Array.isArray(parsed.rules)) {
4735
+ rules.push(...parsed.rules);
4736
+ this.logger.info({ file, count: parsed.rules.length }, "Loaded security rules");
4737
+ }
4738
+ } catch (err) {
4739
+ this.logger.error({ err, file }, "Failed to load security rules file");
4740
+ }
4741
+ }
4742
+ return rules;
4743
+ }
3916
4744
  };
3917
4745
  }
3918
4746
  });
@@ -3995,9 +4823,9 @@ __export(setup_exports, {
3995
4823
  });
3996
4824
  import { createInterface } from "node:readline/promises";
3997
4825
  import { stdin as input, stdout as output } from "node:process";
3998
- import fs4 from "node:fs";
3999
- import path4 from "node:path";
4000
- import yaml2 from "js-yaml";
4826
+ import fs5 from "node:fs";
4827
+ import path5 from "node:path";
4828
+ import yaml3 from "js-yaml";
4001
4829
  function green(s) {
4002
4830
  return `${GREEN}${s}${RESET}`;
4003
4831
  }
@@ -4025,20 +4853,22 @@ function loadExistingConfig(projectRoot) {
4025
4853
  const config = {};
4026
4854
  const env = {};
4027
4855
  let shellEnabled = false;
4028
- const configPath = path4.join(projectRoot, "config", "default.yml");
4029
- if (fs4.existsSync(configPath)) {
4856
+ let writeInGroups = false;
4857
+ let rateLimit = 30;
4858
+ const configPath = path5.join(projectRoot, "config", "default.yml");
4859
+ if (fs5.existsSync(configPath)) {
4030
4860
  try {
4031
- const parsed = yaml2.load(fs4.readFileSync(configPath, "utf-8"));
4861
+ const parsed = yaml3.load(fs5.readFileSync(configPath, "utf-8"));
4032
4862
  if (parsed && typeof parsed === "object") {
4033
4863
  Object.assign(config, parsed);
4034
4864
  }
4035
4865
  } catch {
4036
4866
  }
4037
4867
  }
4038
- const envPath = path4.join(projectRoot, ".env");
4039
- if (fs4.existsSync(envPath)) {
4868
+ const envPath = path5.join(projectRoot, ".env");
4869
+ if (fs5.existsSync(envPath)) {
4040
4870
  try {
4041
- const lines = fs4.readFileSync(envPath, "utf-8").split("\n");
4871
+ const lines = fs5.readFileSync(envPath, "utf-8").split("\n");
4042
4872
  for (const line of lines) {
4043
4873
  const trimmed = line.trim();
4044
4874
  if (!trimmed || trimmed.startsWith("#"))
@@ -4051,17 +4881,25 @@ function loadExistingConfig(projectRoot) {
4051
4881
  } catch {
4052
4882
  }
4053
4883
  }
4054
- const rulesPath = path4.join(projectRoot, "config", "rules", "default-rules.yml");
4055
- if (fs4.existsSync(rulesPath)) {
4884
+ const rulesPath = path5.join(projectRoot, "config", "rules", "default-rules.yml");
4885
+ if (fs5.existsSync(rulesPath)) {
4056
4886
  try {
4057
- const rulesContent = yaml2.load(fs4.readFileSync(rulesPath, "utf-8"));
4887
+ const rulesContent = yaml3.load(fs5.readFileSync(rulesPath, "utf-8"));
4058
4888
  if (rulesContent?.rules) {
4059
4889
  shellEnabled = rulesContent.rules.some((r) => r.id === "allow-owner-admin" && r.effect === "allow");
4890
+ const writeDmRule = rulesContent.rules.find((r) => r.id === "allow-write-for-dm" || r.id === "allow-write-all");
4891
+ if (writeDmRule?.id === "allow-write-all") {
4892
+ writeInGroups = true;
4893
+ }
4894
+ const rlRule = rulesContent.rules.find((r) => r.id === "rate-limit-write");
4895
+ if (rlRule?.rateLimit?.maxInvocations) {
4896
+ rateLimit = rlRule.rateLimit.maxInvocations;
4897
+ }
4060
4898
  }
4061
4899
  } catch {
4062
4900
  }
4063
4901
  }
4064
- return { config, env, shellEnabled };
4902
+ return { config, env, shellEnabled, writeInGroups, rateLimit };
4065
4903
  }
4066
4904
  async function setupCommand() {
4067
4905
  const rl = createInterface({ input, output });
@@ -4114,6 +4952,58 @@ ${bold("Which LLM provider would you like to use?")}`);
4114
4952
  const existingModel = existing.config.llm?.model ?? provider.defaultModel;
4115
4953
  console.log("");
4116
4954
  const model = await askWithDefault(rl, "Which model?", existingModel);
4955
+ const searchProviders = ["brave", "tavily", "duckduckgo", "searxng"];
4956
+ const existingSearchProvider = existing.config.search?.provider ?? existing.env["ALFRED_SEARCH_PROVIDER"] ?? "";
4957
+ const existingSearchIdx = searchProviders.indexOf(existingSearchProvider);
4958
+ const defaultSearchChoice = existingSearchIdx >= 0 ? existingSearchIdx + 1 : 0;
4959
+ console.log(`
4960
+ ${bold("Web Search provider (for searching the internet):")}`);
4961
+ const searchLabels = [
4962
+ "Brave Search \u2014 recommended, free tier (2,000/month)",
4963
+ "Tavily \u2014 built for AI agents, free tier (1,000/month)",
4964
+ "DuckDuckGo \u2014 free, no API key needed",
4965
+ "SearXNG \u2014 self-hosted, no API key needed"
4966
+ ];
4967
+ const mark = (i) => existingSearchIdx === i ? ` ${dim("(current)")}` : "";
4968
+ console.log(` ${cyan("0)")} None (disable web search)${existingSearchIdx === -1 && existingSearchProvider === "" ? ` ${dim("(current)")}` : ""}`);
4969
+ for (let i = 0; i < searchLabels.length; i++) {
4970
+ console.log(` ${cyan(String(i + 1) + ")")} ${searchLabels[i]}${mark(i)}`);
4971
+ }
4972
+ const searchChoice = await askNumber(rl, "> ", 0, searchProviders.length, defaultSearchChoice);
4973
+ let searchProvider;
4974
+ let searchApiKey = "";
4975
+ let searchBaseUrl = "";
4976
+ if (searchChoice >= 1 && searchChoice <= searchProviders.length) {
4977
+ searchProvider = searchProviders[searchChoice - 1];
4978
+ }
4979
+ if (searchProvider === "brave") {
4980
+ const existingKey = existing.env["ALFRED_SEARCH_API_KEY"] ?? "";
4981
+ if (existingKey) {
4982
+ searchApiKey = await askWithDefault(rl, " Brave Search API key", existingKey);
4983
+ } else {
4984
+ console.log(` ${dim("Get your free API key at: https://brave.com/search/api/")}`);
4985
+ searchApiKey = await askRequired(rl, " Brave Search API key");
4986
+ }
4987
+ console.log(` ${green(">")} Brave Search: ${dim(maskKey(searchApiKey))}`);
4988
+ } else if (searchProvider === "tavily") {
4989
+ const existingKey = existing.env["ALFRED_SEARCH_API_KEY"] ?? "";
4990
+ if (existingKey) {
4991
+ searchApiKey = await askWithDefault(rl, " Tavily API key", existingKey);
4992
+ } else {
4993
+ console.log(` ${dim("Get your free API key at: https://tavily.com/")}`);
4994
+ searchApiKey = await askRequired(rl, " Tavily API key");
4995
+ }
4996
+ console.log(` ${green(">")} Tavily: ${dim(maskKey(searchApiKey))}`);
4997
+ } else if (searchProvider === "duckduckgo") {
4998
+ console.log(` ${green(">")} DuckDuckGo: ${dim("no API key needed")}`);
4999
+ } else if (searchProvider === "searxng") {
5000
+ const existingSearxUrl = existing.config.search?.baseUrl ?? existing.env["ALFRED_SEARCH_BASE_URL"] ?? "http://localhost:8080";
5001
+ searchBaseUrl = await askWithDefault(rl, " SearXNG URL", existingSearxUrl);
5002
+ searchBaseUrl = searchBaseUrl.replace(/\/+$/, "");
5003
+ console.log(` ${green(">")} SearXNG: ${dim(searchBaseUrl)}`);
5004
+ } else {
5005
+ console.log(` ${dim("Web search disabled \u2014 you can configure it later.")}`);
5006
+ }
4117
5007
  const currentlyEnabled = [];
4118
5008
  for (let i = 0; i < PLATFORMS.length; i++) {
4119
5009
  const p = PLATFORMS[i];
@@ -4186,8 +5076,73 @@ ${bold(platform.label + " configuration:")}`);
4186
5076
  }
4187
5077
  platformCredentials[platform.configKey] = creds;
4188
5078
  }
5079
+ const existingEmailUser = existing.config.email?.auth?.user ?? existing.env["ALFRED_EMAIL_USER"] ?? "";
5080
+ const hasEmail = !!existingEmailUser;
5081
+ const emailDefault = hasEmail ? "Y/n" : "y/N";
5082
+ console.log(`
5083
+ ${bold("Email access (read & send emails via IMAP/SMTP)?")}`);
5084
+ console.log(`${dim("Works with Gmail, Outlook, or any IMAP/SMTP provider.")}`);
5085
+ const emailAnswer = (await rl.question(`${YELLOW}> ${RESET}${dim(`[${emailDefault}] `)}`)).trim().toLowerCase();
5086
+ const enableEmail = emailAnswer === "" ? hasEmail : emailAnswer === "y" || emailAnswer === "yes";
5087
+ let emailUser = "";
5088
+ let emailPass = "";
5089
+ let emailImapHost = "";
5090
+ let emailImapPort = 993;
5091
+ let emailSmtpHost = "";
5092
+ let emailSmtpPort = 587;
5093
+ if (enableEmail) {
5094
+ console.log("");
5095
+ emailUser = await askWithDefault(rl, " Email address", existingEmailUser || "");
5096
+ if (!emailUser) {
5097
+ emailUser = await askRequired(rl, " Email address");
5098
+ }
5099
+ const existingPass = existing.env["ALFRED_EMAIL_PASS"] ?? "";
5100
+ if (existingPass) {
5101
+ emailPass = await askWithDefault(rl, " Password / App password", existingPass);
5102
+ } else {
5103
+ console.log(` ${dim("For Gmail: use an App Password (not your regular password)")}`);
5104
+ console.log(` ${dim(" \u2192 Google Account \u2192 Security \u2192 2-Step \u2192 App passwords")}`);
5105
+ emailPass = await askRequired(rl, " Password / App password");
5106
+ }
5107
+ const domain = emailUser.split("@")[1]?.toLowerCase() ?? "";
5108
+ const presets = {
5109
+ "gmail.com": { imap: "imap.gmail.com", smtp: "smtp.gmail.com" },
5110
+ "googlemail.com": { imap: "imap.gmail.com", smtp: "smtp.gmail.com" },
5111
+ "outlook.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
5112
+ "hotmail.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
5113
+ "live.com": { imap: "outlook.office365.com", smtp: "smtp.office365.com" },
5114
+ "yahoo.com": { imap: "imap.mail.yahoo.com", smtp: "smtp.mail.yahoo.com" },
5115
+ "icloud.com": { imap: "imap.mail.me.com", smtp: "smtp.mail.me.com" },
5116
+ "me.com": { imap: "imap.mail.me.com", smtp: "smtp.mail.me.com" },
5117
+ "gmx.de": { imap: "imap.gmx.net", smtp: "mail.gmx.net" },
5118
+ "gmx.net": { imap: "imap.gmx.net", smtp: "mail.gmx.net" },
5119
+ "web.de": { imap: "imap.web.de", smtp: "smtp.web.de" },
5120
+ "posteo.de": { imap: "posteo.de", smtp: "posteo.de" },
5121
+ "mailbox.org": { imap: "imap.mailbox.org", smtp: "smtp.mailbox.org" },
5122
+ "protonmail.com": { imap: "127.0.0.1", smtp: "127.0.0.1" },
5123
+ "proton.me": { imap: "127.0.0.1", smtp: "127.0.0.1" }
5124
+ };
5125
+ const preset = presets[domain];
5126
+ const defaultImap = existing.config.email?.imap?.host ?? preset?.imap ?? `imap.${domain}`;
5127
+ const defaultSmtp = existing.config.email?.smtp?.host ?? preset?.smtp ?? `smtp.${domain}`;
5128
+ const defaultImapPort = existing.config.email?.imap?.port ?? 993;
5129
+ const defaultSmtpPort = existing.config.email?.smtp?.port ?? 587;
5130
+ if (preset) {
5131
+ console.log(` ${green(">")} Detected ${domain} \u2014 using preset server settings`);
5132
+ }
5133
+ emailImapHost = await askWithDefault(rl, " IMAP server", defaultImap);
5134
+ const imapPortStr = await askWithDefault(rl, " IMAP port", String(defaultImapPort));
5135
+ emailImapPort = parseInt(imapPortStr, 10) || 993;
5136
+ emailSmtpHost = await askWithDefault(rl, " SMTP server", defaultSmtp);
5137
+ const smtpPortStr = await askWithDefault(rl, " SMTP port", String(defaultSmtpPort));
5138
+ emailSmtpPort = parseInt(smtpPortStr, 10) || 587;
5139
+ console.log(` ${green(">")} Email: ${dim(emailUser)} via ${dim(emailImapHost)}`);
5140
+ } else {
5141
+ console.log(` ${dim("Email disabled \u2014 you can configure it later.")}`);
5142
+ }
5143
+ console.log(`
5144
+ ${bold("Security configuration:")}`);
4189
5145
  const existingOwnerId = existing.config.security?.ownerUserId ?? existing.env["ALFRED_OWNER_USER_ID"] ?? "";
4190
- console.log("");
4191
5146
  let ownerUserId;
4192
5147
  if (existingOwnerId) {
4193
5148
  ownerUserId = await askWithDefault(rl, "Owner user ID (for elevated permissions)", existingOwnerId);
@@ -4200,21 +5155,41 @@ ${bold(platform.label + " configuration:")}`);
4200
5155
  if (ownerUserId) {
4201
5156
  const shellDefault = existing.shellEnabled ? "Y/n" : "y/N";
4202
5157
  console.log("");
4203
- console.log(`${bold("Enable shell access (admin commands) for the owner?")}`);
4204
- console.log(`${dim("This allows Alfred to execute shell commands on your behalf.")}`);
4205
- console.log(`${dim("Only the owner (user ID above) will have this permission.")}`);
4206
- const shellAnswer = (await rl.question(`${YELLOW}> ${RESET}${dim(`[${shellDefault}] `)}`)).trim().toLowerCase();
5158
+ console.log(` ${bold("Enable shell access (admin commands) for the owner?")}`);
5159
+ console.log(` ${dim("Allows Alfred to execute shell commands. Only for the owner.")}`);
5160
+ const shellAnswer = (await rl.question(` ${YELLOW}> ${RESET}${dim(`[${shellDefault}] `)}`)).trim().toLowerCase();
4207
5161
  if (shellAnswer === "") {
4208
5162
  enableShell = existing.shellEnabled;
4209
5163
  } else {
4210
5164
  enableShell = shellAnswer === "y" || shellAnswer === "yes";
4211
5165
  }
4212
5166
  if (enableShell) {
4213
- console.log(` ${green(">")} Shell access ${bold("enabled")} for owner ${dim(ownerUserId)}`);
5167
+ console.log(` ${green(">")} Shell access ${bold("enabled")} for owner ${dim(ownerUserId)}`);
4214
5168
  } else {
4215
- console.log(` ${dim("Shell access disabled.")}`);
5169
+ console.log(` ${dim("Shell access disabled.")}`);
4216
5170
  }
4217
5171
  }
5172
+ const writeGroupsDefault = existing.writeInGroups ? "Y/n" : "y/N";
5173
+ console.log("");
5174
+ console.log(` ${bold("Allow write actions (notes, reminders, memory) in group chats?")}`);
5175
+ console.log(` ${dim("By default, write actions are only allowed in DMs.")}`);
5176
+ const writeGroupsAnswer = (await rl.question(` ${YELLOW}> ${RESET}${dim(`[${writeGroupsDefault}] `)}`)).trim().toLowerCase();
5177
+ let writeInGroups;
5178
+ if (writeGroupsAnswer === "") {
5179
+ writeInGroups = existing.writeInGroups;
5180
+ } else {
5181
+ writeInGroups = writeGroupsAnswer === "y" || writeGroupsAnswer === "yes";
5182
+ }
5183
+ if (writeInGroups) {
5184
+ console.log(` ${green(">")} Write actions ${bold("enabled")} in groups`);
5185
+ } else {
5186
+ console.log(` ${dim("Write actions only in DMs (default).")}`);
5187
+ }
5188
+ const existingRateLimit = existing.rateLimit ?? 30;
5189
+ console.log("");
5190
+ const rateLimitStr = await askWithDefault(rl, " Rate limit (max write actions per hour per user)", String(existingRateLimit));
5191
+ const rateLimit = Math.max(1, parseInt(rateLimitStr, 10) || 30);
5192
+ console.log(` ${green(">")} Rate limit: ${bold(String(rateLimit))} per hour`);
4218
5193
  console.log(`
4219
5194
  ${bold("Writing configuration files...")}`);
4220
5195
  const envLines = [
@@ -4239,6 +5214,27 @@ ${bold("Writing configuration files...")}`);
4239
5214
  for (const [envKey, envVal] of Object.entries(envOverrides)) {
4240
5215
  envLines.push(`${envKey}=${envVal}`);
4241
5216
  }
5217
+ envLines.push("", "# === Web Search ===", "");
5218
+ if (searchProvider) {
5219
+ envLines.push(`ALFRED_SEARCH_PROVIDER=${searchProvider}`);
5220
+ if (searchApiKey) {
5221
+ envLines.push(`ALFRED_SEARCH_API_KEY=${searchApiKey}`);
5222
+ }
5223
+ if (searchBaseUrl) {
5224
+ envLines.push(`ALFRED_SEARCH_BASE_URL=${searchBaseUrl}`);
5225
+ }
5226
+ } else {
5227
+ envLines.push("# ALFRED_SEARCH_PROVIDER=brave");
5228
+ envLines.push("# ALFRED_SEARCH_API_KEY=");
5229
+ }
5230
+ envLines.push("", "# === Email ===", "");
5231
+ if (enableEmail) {
5232
+ envLines.push(`ALFRED_EMAIL_USER=${emailUser}`);
5233
+ envLines.push(`ALFRED_EMAIL_PASS=${emailPass}`);
5234
+ } else {
5235
+ envLines.push("# ALFRED_EMAIL_USER=");
5236
+ envLines.push("# ALFRED_EMAIL_PASS=");
5237
+ }
4242
5238
  envLines.push("", "# === Security ===", "");
4243
5239
  if (ownerUserId) {
4244
5240
  envLines.push(`ALFRED_OWNER_USER_ID=${ownerUserId}`);
@@ -4246,12 +5242,12 @@ ${bold("Writing configuration files...")}`);
4246
5242
  envLines.push("# ALFRED_OWNER_USER_ID=");
4247
5243
  }
4248
5244
  envLines.push("");
4249
- const envPath = path4.join(projectRoot, ".env");
4250
- fs4.writeFileSync(envPath, envLines.join("\n"), "utf-8");
5245
+ const envPath = path5.join(projectRoot, ".env");
5246
+ fs5.writeFileSync(envPath, envLines.join("\n"), "utf-8");
4251
5247
  console.log(` ${green("+")} ${dim(".env")} written`);
4252
- const configDir = path4.join(projectRoot, "config");
4253
- if (!fs4.existsSync(configDir)) {
4254
- fs4.mkdirSync(configDir, { recursive: true });
5248
+ const configDir = path5.join(projectRoot, "config");
5249
+ if (!fs5.existsSync(configDir)) {
5250
+ fs5.mkdirSync(configDir, { recursive: true });
4255
5251
  }
4256
5252
  const config = {
4257
5253
  name: botName,
@@ -4285,6 +5281,20 @@ ${bold("Writing configuration files...")}`);
4285
5281
  temperature: 0.7,
4286
5282
  maxTokens: 4096
4287
5283
  },
5284
+ ...searchProvider ? {
5285
+ search: {
5286
+ provider: searchProvider,
5287
+ ...searchApiKey ? { apiKey: searchApiKey } : {},
5288
+ ...searchBaseUrl ? { baseUrl: searchBaseUrl } : {}
5289
+ }
5290
+ } : {},
5291
+ ...enableEmail ? {
5292
+ email: {
5293
+ imap: { host: emailImapHost, port: emailImapPort, secure: emailImapPort === 993 },
5294
+ smtp: { host: emailSmtpHost, port: emailSmtpPort, secure: emailSmtpPort === 465 },
5295
+ auth: { user: emailUser, pass: emailPass }
5296
+ }
5297
+ } : {},
4288
5298
  storage: {
4289
5299
  path: "./data/alfred.db"
4290
5300
  },
@@ -4301,13 +5311,13 @@ ${bold("Writing configuration files...")}`);
4301
5311
  if (ownerUserId) {
4302
5312
  config.security.ownerUserId = ownerUserId;
4303
5313
  }
4304
- const yamlStr = "# Alfred \u2014 Configuration\n# Generated by `alfred setup`\n# Edit manually or re-run `alfred setup` to reconfigure.\n\n" + yaml2.dump(config, { lineWidth: 120, noRefs: true, sortKeys: false });
4305
- const configPath = path4.join(configDir, "default.yml");
4306
- fs4.writeFileSync(configPath, yamlStr, "utf-8");
5314
+ const yamlStr = "# Alfred \u2014 Configuration\n# Generated by `alfred setup`\n# Edit manually or re-run `alfred setup` to reconfigure.\n\n" + yaml3.dump(config, { lineWidth: 120, noRefs: true, sortKeys: false });
5315
+ const configPath = path5.join(configDir, "default.yml");
5316
+ fs5.writeFileSync(configPath, yamlStr, "utf-8");
4307
5317
  console.log(` ${green("+")} ${dim("config/default.yml")} written`);
4308
- const rulesDir = path4.join(configDir, "rules");
4309
- if (!fs4.existsSync(rulesDir)) {
4310
- fs4.mkdirSync(rulesDir, { recursive: true });
5318
+ const rulesDir = path5.join(configDir, "rules");
5319
+ if (!fs5.existsSync(rulesDir)) {
5320
+ fs5.mkdirSync(rulesDir, { recursive: true });
4311
5321
  }
4312
5322
  const ownerAdminRule = enableShell && ownerUserId ? `
4313
5323
  # Allow admin actions (shell, etc.) for the owner only
@@ -4331,6 +5341,21 @@ ${bold("Writing configuration files...")}`);
4331
5341
  # conditions:
4332
5342
  # users: ["${ownerUserId || "YOUR_USER_ID_HERE"}"]
4333
5343
  `;
5344
+ const writeRule = writeInGroups ? ` # Allow write-level skills everywhere (DMs and groups)
5345
+ - id: allow-write-all
5346
+ effect: allow
5347
+ priority: 200
5348
+ scope: global
5349
+ actions: ["*"]
5350
+ riskLevels: [write]` : ` # Allow write-level skills in DMs only
5351
+ - id: allow-write-for-dm
5352
+ effect: allow
5353
+ priority: 200
5354
+ scope: global
5355
+ actions: ["*"]
5356
+ riskLevels: [write]
5357
+ conditions:
5358
+ chatType: dm`;
4334
5359
  const rulesYaml = `# Alfred \u2014 Default Security Rules
4335
5360
  # Rules are evaluated in priority order (lower number = higher priority).
4336
5361
  # First matching rule wins.
@@ -4344,17 +5369,9 @@ rules:
4344
5369
  actions: ["*"]
4345
5370
  riskLevels: [read]
4346
5371
 
4347
- # Allow write-level skills in DMs
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
5372
+ ${writeRule}
4356
5373
 
4357
- # Rate-limit write actions: max 30 per hour per user
5374
+ # Rate-limit write actions: max ${rateLimit} per hour per user
4358
5375
  - id: rate-limit-write
4359
5376
  effect: allow
4360
5377
  priority: 250
@@ -4362,7 +5379,7 @@ rules:
4362
5379
  actions: ["*"]
4363
5380
  riskLevels: [write]
4364
5381
  rateLimit:
4365
- maxInvocations: 30
5382
+ maxInvocations: ${rateLimit}
4366
5383
  windowSeconds: 3600
4367
5384
  ${ownerAdminRule}
4368
5385
  # Deny destructive and admin actions by default
@@ -4381,12 +5398,12 @@ ${ownerAdminRule}
4381
5398
  actions: ["*"]
4382
5399
  riskLevels: [read, write, destructive, admin]
4383
5400
  `;
4384
- const rulesPath = path4.join(rulesDir, "default-rules.yml");
4385
- fs4.writeFileSync(rulesPath, rulesYaml, "utf-8");
5401
+ const rulesPath = path5.join(rulesDir, "default-rules.yml");
5402
+ fs5.writeFileSync(rulesPath, rulesYaml, "utf-8");
4386
5403
  console.log(` ${green("+")} ${dim("config/rules/default-rules.yml")} written`);
4387
- const dataDir = path4.join(projectRoot, "data");
4388
- if (!fs4.existsSync(dataDir)) {
4389
- fs4.mkdirSync(dataDir, { recursive: true });
5404
+ const dataDir = path5.join(projectRoot, "data");
5405
+ if (!fs5.existsSync(dataDir)) {
5406
+ fs5.mkdirSync(dataDir, { recursive: true });
4390
5407
  console.log(` ${green("+")} ${dim("data/")} directory created`);
4391
5408
  }
4392
5409
  console.log("");
@@ -4404,10 +5421,28 @@ ${ownerAdminRule}
4404
5421
  } else {
4405
5422
  console.log(` ${bold("Platforms:")} none (configure later)`);
4406
5423
  }
5424
+ if (searchProvider) {
5425
+ const searchLabelMap = {
5426
+ brave: "Brave Search",
5427
+ tavily: "Tavily",
5428
+ duckduckgo: "DuckDuckGo",
5429
+ searxng: `SearXNG (${searchBaseUrl})`
5430
+ };
5431
+ console.log(` ${bold("Web search:")} ${searchLabelMap[searchProvider]}`);
5432
+ } else {
5433
+ console.log(` ${bold("Web search:")} ${dim("disabled")}`);
5434
+ }
5435
+ if (enableEmail) {
5436
+ console.log(` ${bold("Email:")} ${emailUser} (${emailImapHost})`);
5437
+ } else {
5438
+ console.log(` ${bold("Email:")} ${dim("disabled")}`);
5439
+ }
4407
5440
  if (ownerUserId) {
4408
5441
  console.log(` ${bold("Owner ID:")} ${ownerUserId}`);
4409
5442
  console.log(` ${bold("Shell access:")} ${enableShell ? green("enabled") : dim("disabled")}`);
4410
5443
  }
5444
+ console.log(` ${bold("Write scope:")} ${writeInGroups ? "DMs + Groups" : "DMs only"}`);
5445
+ console.log(` ${bold("Rate limit:")} ${rateLimit}/hour per user`);
4411
5446
  console.log("");
4412
5447
  console.log(`${CYAN}Next steps:${RESET}`);
4413
5448
  console.log(` ${bold("alfred start")} Start Alfred`);
@@ -4641,9 +5676,9 @@ var rules_exports = {};
4641
5676
  __export(rules_exports, {
4642
5677
  rulesCommand: () => rulesCommand
4643
5678
  });
4644
- import fs5 from "node:fs";
4645
- import path5 from "node:path";
4646
- import yaml3 from "js-yaml";
5679
+ import fs6 from "node:fs";
5680
+ import path6 from "node:path";
5681
+ import yaml4 from "js-yaml";
4647
5682
  async function rulesCommand() {
4648
5683
  const configLoader = new ConfigLoader();
4649
5684
  let config;
@@ -4653,18 +5688,18 @@ async function rulesCommand() {
4653
5688
  console.error("Failed to load configuration:", error.message);
4654
5689
  process.exit(1);
4655
5690
  }
4656
- const rulesPath = path5.resolve(config.security.rulesPath);
4657
- if (!fs5.existsSync(rulesPath)) {
5691
+ const rulesPath = path6.resolve(config.security.rulesPath);
5692
+ if (!fs6.existsSync(rulesPath)) {
4658
5693
  console.log(`Rules directory not found: ${rulesPath}`);
4659
5694
  console.log("No security rules loaded.");
4660
5695
  return;
4661
5696
  }
4662
- const stat = fs5.statSync(rulesPath);
5697
+ const stat = fs6.statSync(rulesPath);
4663
5698
  if (!stat.isDirectory()) {
4664
5699
  console.error(`Rules path is not a directory: ${rulesPath}`);
4665
5700
  process.exit(1);
4666
5701
  }
4667
- const files = fs5.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
5702
+ const files = fs6.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
4668
5703
  if (files.length === 0) {
4669
5704
  console.log(`No YAML rule files found in: ${rulesPath}`);
4670
5705
  return;
@@ -4673,10 +5708,10 @@ async function rulesCommand() {
4673
5708
  const allRules = [];
4674
5709
  const errors = [];
4675
5710
  for (const file of files) {
4676
- const filePath = path5.join(rulesPath, file);
5711
+ const filePath = path6.join(rulesPath, file);
4677
5712
  try {
4678
- const raw = fs5.readFileSync(filePath, "utf-8");
4679
- const parsed = yaml3.load(raw);
5713
+ const raw = fs6.readFileSync(filePath, "utf-8");
5714
+ const parsed = yaml4.load(raw);
4680
5715
  const rules = ruleLoader.loadFromObject(parsed);
4681
5716
  allRules.push(...rules);
4682
5717
  } catch (error) {
@@ -4727,9 +5762,9 @@ var status_exports = {};
4727
5762
  __export(status_exports, {
4728
5763
  statusCommand: () => statusCommand
4729
5764
  });
4730
- import fs6 from "node:fs";
4731
- import path6 from "node:path";
4732
- import yaml4 from "js-yaml";
5765
+ import fs7 from "node:fs";
5766
+ import path7 from "node:path";
5767
+ import yaml5 from "js-yaml";
4733
5768
  async function statusCommand() {
4734
5769
  const configLoader = new ConfigLoader();
4735
5770
  let config;
@@ -4785,23 +5820,23 @@ async function statusCommand() {
4785
5820
  }
4786
5821
  console.log("");
4787
5822
  console.log("Storage:");
4788
- const dbPath = path6.resolve(config.storage.path);
4789
- const dbExists = fs6.existsSync(dbPath);
5823
+ const dbPath = path7.resolve(config.storage.path);
5824
+ const dbExists = fs7.existsSync(dbPath);
4790
5825
  console.log(` Database: ${dbPath}`);
4791
5826
  console.log(` Status: ${dbExists ? "exists" : "not yet created"}`);
4792
5827
  console.log("");
4793
- const rulesPath = path6.resolve(config.security.rulesPath);
5828
+ const rulesPath = path7.resolve(config.security.rulesPath);
4794
5829
  let ruleCount = 0;
4795
5830
  let ruleFileCount = 0;
4796
- if (fs6.existsSync(rulesPath) && fs6.statSync(rulesPath).isDirectory()) {
4797
- const files = fs6.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
5831
+ if (fs7.existsSync(rulesPath) && fs7.statSync(rulesPath).isDirectory()) {
5832
+ const files = fs7.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
4798
5833
  ruleFileCount = files.length;
4799
5834
  const ruleLoader = new RuleLoader();
4800
5835
  for (const file of files) {
4801
- const filePath = path6.join(rulesPath, file);
5836
+ const filePath = path7.join(rulesPath, file);
4802
5837
  try {
4803
- const raw = fs6.readFileSync(filePath, "utf-8");
4804
- const parsed = yaml4.load(raw);
5838
+ const raw = fs7.readFileSync(filePath, "utf-8");
5839
+ const parsed = yaml5.load(raw);
4805
5840
  const rules = ruleLoader.loadFromObject(parsed);
4806
5841
  ruleCount += rules.length;
4807
5842
  } catch {
@@ -4834,8 +5869,8 @@ var logs_exports = {};
4834
5869
  __export(logs_exports, {
4835
5870
  logsCommand: () => logsCommand
4836
5871
  });
4837
- import fs7 from "node:fs";
4838
- import path7 from "node:path";
5872
+ import fs8 from "node:fs";
5873
+ import path8 from "node:path";
4839
5874
  async function logsCommand(tail) {
4840
5875
  const configLoader = new ConfigLoader();
4841
5876
  let config;
@@ -4845,8 +5880,8 @@ async function logsCommand(tail) {
4845
5880
  console.error("Failed to load configuration:", error.message);
4846
5881
  process.exit(1);
4847
5882
  }
4848
- const dbPath = path7.resolve(config.storage.path);
4849
- if (!fs7.existsSync(dbPath)) {
5883
+ const dbPath = path8.resolve(config.storage.path);
5884
+ if (!fs8.existsSync(dbPath)) {
4850
5885
  console.log(`Database not found at: ${dbPath}`);
4851
5886
  console.log("No audit log entries. Alfred has not been run yet, or the database path is incorrect.");
4852
5887
  return;