@madh-io/alfred-ai 0.4.0 → 0.6.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.
Files changed (2) hide show
  1. package/bundle/index.js +2059 -534
  2. package/package.json +2 -1
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, SearchConfigSchema, EmailConfigSchema, AlfredConfigSchema;
14
+ var TelegramConfigSchema, DiscordConfigSchema, WhatsAppConfigSchema, MatrixConfigSchema, SignalConfigSchema, StorageConfigSchema, LoggerConfigSchema, SecurityConfigSchema, LLMProviderConfigSchema, SearchConfigSchema, EmailConfigSchema, SpeechConfigSchema, AlfredConfigSchema;
15
15
  var init_schema = __esm({
16
16
  "../config/dist/schema.js"() {
17
17
  "use strict";
@@ -80,6 +80,11 @@ var init_schema = __esm({
80
80
  pass: z.string()
81
81
  })
82
82
  });
83
+ SpeechConfigSchema = z.object({
84
+ provider: z.enum(["openai", "groq"]),
85
+ apiKey: z.string(),
86
+ baseUrl: z.string().optional()
87
+ });
83
88
  AlfredConfigSchema = z.object({
84
89
  name: z.string(),
85
90
  telegram: TelegramConfigSchema,
@@ -92,7 +97,8 @@ var init_schema = __esm({
92
97
  logger: LoggerConfigSchema,
93
98
  security: SecurityConfigSchema,
94
99
  search: SearchConfigSchema.optional(),
95
- email: EmailConfigSchema.optional()
100
+ email: EmailConfigSchema.optional(),
101
+ speech: SpeechConfigSchema.optional()
96
102
  });
97
103
  }
98
104
  });
@@ -212,7 +218,10 @@ var init_loader = __esm({
212
218
  ALFRED_SEARCH_API_KEY: ["search", "apiKey"],
213
219
  ALFRED_SEARCH_BASE_URL: ["search", "baseUrl"],
214
220
  ALFRED_EMAIL_USER: ["email", "auth", "user"],
215
- ALFRED_EMAIL_PASS: ["email", "auth", "pass"]
221
+ ALFRED_EMAIL_PASS: ["email", "auth", "pass"],
222
+ ALFRED_SPEECH_PROVIDER: ["speech", "provider"],
223
+ ALFRED_SPEECH_API_KEY: ["speech", "apiKey"],
224
+ ALFRED_SPEECH_BASE_URL: ["speech", "baseUrl"]
216
225
  };
217
226
  ConfigLoader = class {
218
227
  loadConfig(configPath) {
@@ -403,6 +412,25 @@ var init_migrations = __esm({
403
412
  ON reminders(user_id, fired);
404
413
  `);
405
414
  }
415
+ },
416
+ {
417
+ version: 4,
418
+ description: "Add notes table for persistent note storage",
419
+ up(db) {
420
+ db.exec(`
421
+ CREATE TABLE IF NOT EXISTS notes (
422
+ id TEXT PRIMARY KEY,
423
+ user_id TEXT NOT NULL,
424
+ title TEXT NOT NULL,
425
+ content TEXT NOT NULL,
426
+ created_at TEXT NOT NULL,
427
+ updated_at TEXT NOT NULL
428
+ );
429
+
430
+ CREATE INDEX IF NOT EXISTS idx_notes_user
431
+ ON notes(user_id, updated_at DESC);
432
+ `);
433
+ }
406
434
  }
407
435
  ];
408
436
  }
@@ -850,6 +878,64 @@ var init_reminder_repository = __esm({
850
878
  }
851
879
  });
852
880
 
881
+ // ../storage/dist/repositories/note-repository.js
882
+ import { randomUUID as randomUUID3 } from "node:crypto";
883
+ var NoteRepository;
884
+ var init_note_repository = __esm({
885
+ "../storage/dist/repositories/note-repository.js"() {
886
+ "use strict";
887
+ NoteRepository = class {
888
+ db;
889
+ constructor(db) {
890
+ this.db = db;
891
+ }
892
+ save(userId, title, content) {
893
+ const now = (/* @__PURE__ */ new Date()).toISOString();
894
+ const id = randomUUID3();
895
+ this.db.prepare("INSERT INTO notes (id, user_id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(id, userId, title, content, now, now);
896
+ return { id, userId, title, content, createdAt: now, updatedAt: now };
897
+ }
898
+ getById(noteId) {
899
+ const row = this.db.prepare("SELECT * FROM notes WHERE id = ?").get(noteId);
900
+ return row ? this.mapRow(row) : void 0;
901
+ }
902
+ list(userId, limit = 50) {
903
+ const rows = this.db.prepare("SELECT * FROM notes WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?").all(userId, limit);
904
+ return rows.map((r) => this.mapRow(r));
905
+ }
906
+ search(userId, query) {
907
+ const pattern = `%${query}%`;
908
+ const rows = this.db.prepare("SELECT * FROM notes WHERE user_id = ? AND (title LIKE ? OR content LIKE ?) ORDER BY updated_at DESC").all(userId, pattern, pattern);
909
+ return rows.map((r) => this.mapRow(r));
910
+ }
911
+ update(noteId, title, content) {
912
+ const existing = this.getById(noteId);
913
+ if (!existing)
914
+ return void 0;
915
+ const now = (/* @__PURE__ */ new Date()).toISOString();
916
+ const newTitle = title ?? existing.title;
917
+ const newContent = content ?? existing.content;
918
+ this.db.prepare("UPDATE notes SET title = ?, content = ?, updated_at = ? WHERE id = ?").run(newTitle, newContent, now, noteId);
919
+ return { ...existing, title: newTitle, content: newContent, updatedAt: now };
920
+ }
921
+ delete(noteId) {
922
+ const result = this.db.prepare("DELETE FROM notes WHERE id = ?").run(noteId);
923
+ return result.changes > 0;
924
+ }
925
+ mapRow(row) {
926
+ return {
927
+ id: row.id,
928
+ userId: row.user_id,
929
+ title: row.title,
930
+ content: row.content,
931
+ createdAt: row.created_at,
932
+ updatedAt: row.updated_at
933
+ };
934
+ }
935
+ };
936
+ }
937
+ });
938
+
853
939
  // ../storage/dist/index.js
854
940
  var init_dist3 = __esm({
855
941
  "../storage/dist/index.js"() {
@@ -862,6 +948,7 @@ var init_dist3 = __esm({
862
948
  init_migrator();
863
949
  init_migrations();
864
950
  init_reminder_repository();
951
+ init_note_repository();
865
952
  }
866
953
  });
867
954
 
@@ -1004,6 +1091,15 @@ var init_anthropic = __esm({
1004
1091
  switch (block.type) {
1005
1092
  case "text":
1006
1093
  return { type: "text", text: block.text };
1094
+ case "image":
1095
+ return {
1096
+ type: "image",
1097
+ source: {
1098
+ type: "base64",
1099
+ media_type: block.source.media_type,
1100
+ data: block.source.data
1101
+ }
1102
+ };
1007
1103
  case "tool_use":
1008
1104
  return {
1009
1105
  type: "tool_use",
@@ -1200,6 +1296,14 @@ var init_openai = __esm({
1200
1296
  case "text":
1201
1297
  textParts.push({ type: "text", text: block.text });
1202
1298
  break;
1299
+ case "image":
1300
+ textParts.push({
1301
+ type: "image_url",
1302
+ image_url: {
1303
+ url: `data:${block.source.media_type};base64,${block.source.data}`
1304
+ }
1305
+ });
1306
+ break;
1203
1307
  case "tool_use":
1204
1308
  toolUseParts.push({
1205
1309
  id: block.id,
@@ -1558,11 +1662,15 @@ var init_ollama = __esm({
1558
1662
  }
1559
1663
  mapContentBlocks(role, blocks) {
1560
1664
  const textParts = [];
1665
+ const images = [];
1561
1666
  for (const block of blocks) {
1562
1667
  switch (block.type) {
1563
1668
  case "text":
1564
1669
  textParts.push(block.text);
1565
1670
  break;
1671
+ case "image":
1672
+ images.push(block.source.data);
1673
+ break;
1566
1674
  case "tool_use":
1567
1675
  textParts.push(`[Tool call: ${block.name}(${JSON.stringify(block.input)})]`);
1568
1676
  break;
@@ -1571,7 +1679,11 @@ var init_ollama = __esm({
1571
1679
  break;
1572
1680
  }
1573
1681
  }
1574
- return { role, content: textParts.join("\n") };
1682
+ const msg = { role, content: textParts.join("\n") };
1683
+ if (images.length > 0) {
1684
+ msg.images = images;
1685
+ }
1686
+ return msg;
1575
1687
  }
1576
1688
  mapTools(tools) {
1577
1689
  return tools.map((tool) => ({
@@ -1647,6 +1759,9 @@ function estimateMessageTokens(msg) {
1647
1759
  case "text":
1648
1760
  tokens += estimateTokens(block.text);
1649
1761
  break;
1762
+ case "image":
1763
+ tokens += 1e3;
1764
+ break;
1650
1765
  case "tool_use":
1651
1766
  tokens += estimateTokens(block.name) + estimateTokens(JSON.stringify(block.input));
1652
1767
  break;
@@ -1663,9 +1778,9 @@ var init_prompt_builder = __esm({
1663
1778
  "use strict";
1664
1779
  PromptBuilder = class {
1665
1780
  buildSystemPrompt(memories, skills) {
1666
- const os = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
1781
+ const os3 = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
1667
1782
  const homeDir = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1668
- let prompt = `You are Alfred, a personal AI assistant. You run on ${os} (home: ${homeDir}).
1783
+ let prompt = `You are Alfred, a personal AI assistant. You run on ${os3} (home: ${homeDir}).
1669
1784
 
1670
1785
  ## Core principles
1671
1786
  - ACT, don't just talk. When the user asks you to do something, USE YOUR TOOLS immediately. Never say "I could do X" \u2014 just do X.
@@ -1681,7 +1796,7 @@ For complex tasks, work through multiple steps:
1681
1796
  4. **Summarize** the final result clearly.
1682
1797
 
1683
1798
  ## Environment
1684
- - OS: ${os}
1799
+ - OS: ${os3}
1685
1800
  - Home: ${homeDir}
1686
1801
  - Documents: ${homeDir}/Documents
1687
1802
  - Desktop: ${homeDir}/Desktop
@@ -2692,18 +2807,18 @@ ${reminderList.map((r) => `- ${r.reminderId}: "${r.message}" (triggers at ${r.tr
2692
2807
  });
2693
2808
 
2694
2809
  // ../skills/dist/built-in/note.js
2695
- import { randomUUID as randomUUID3 } from "node:crypto";
2696
2810
  var NoteSkill;
2697
2811
  var init_note = __esm({
2698
2812
  "../skills/dist/built-in/note.js"() {
2699
2813
  "use strict";
2700
2814
  init_skill();
2701
2815
  NoteSkill = class extends Skill {
2816
+ noteRepo;
2702
2817
  metadata = {
2703
2818
  name: "note",
2704
- description: "Save, list, search, or delete persistent notes. Use when the user wants to write down or retrieve text notes, lists, or ideas.",
2819
+ description: "Save, list, search, or delete persistent notes (stored in SQLite). Use when the user wants to write down or retrieve text notes, lists, or ideas.",
2705
2820
  riskLevel: "write",
2706
- version: "1.0.0",
2821
+ version: "2.0.0",
2707
2822
  inputSchema: {
2708
2823
  type: "object",
2709
2824
  properties: {
@@ -2732,7 +2847,10 @@ var init_note = __esm({
2732
2847
  required: ["action"]
2733
2848
  }
2734
2849
  };
2735
- notes = /* @__PURE__ */ new Map();
2850
+ constructor(noteRepo) {
2851
+ super();
2852
+ this.noteRepo = noteRepo;
2853
+ }
2736
2854
  async execute(input2, context) {
2737
2855
  const action = input2.action;
2738
2856
  switch (action) {
@@ -2755,394 +2873,101 @@ var init_note = __esm({
2755
2873
  const title = input2.title;
2756
2874
  const content = input2.content;
2757
2875
  if (!title || typeof title !== "string") {
2758
- return {
2759
- success: false,
2760
- error: 'Missing required field "title" for save action'
2761
- };
2876
+ return { success: false, error: 'Missing required field "title" for save action' };
2762
2877
  }
2763
2878
  if (!content || typeof content !== "string") {
2764
- return {
2765
- success: false,
2766
- error: 'Missing required field "content" for save action'
2767
- };
2879
+ return { success: false, error: 'Missing required field "content" for save action' };
2768
2880
  }
2769
- const noteId = randomUUID3();
2770
- const createdAt = Date.now();
2771
- this.notes.set(noteId, {
2772
- noteId,
2773
- userId: context.userId,
2774
- title,
2775
- content,
2776
- createdAt
2777
- });
2881
+ const entry = this.noteRepo.save(context.userId, title, content);
2778
2882
  return {
2779
2883
  success: true,
2780
- data: { noteId, title, createdAt },
2781
- display: `Note saved (${noteId}): "${title}"`
2884
+ data: { noteId: entry.id, title: entry.title },
2885
+ display: `Note saved: "${title}"`
2782
2886
  };
2783
2887
  }
2784
2888
  listNotes(context) {
2785
- const userNotes = [];
2786
- for (const [, entry] of this.notes) {
2787
- if (entry.userId === context.userId) {
2788
- userNotes.push({
2789
- noteId: entry.noteId,
2790
- title: entry.title,
2791
- createdAt: entry.createdAt
2792
- });
2793
- }
2889
+ const notes = this.noteRepo.list(context.userId);
2890
+ if (notes.length === 0) {
2891
+ return { success: true, data: [], display: "No notes found." };
2794
2892
  }
2795
- return {
2796
- success: true,
2797
- data: userNotes,
2798
- display: userNotes.length === 0 ? "No notes found." : `Notes:
2799
- ${userNotes.map((n) => `- ${n.noteId}: "${n.title}"`).join("\n")}`
2800
- };
2893
+ const display = notes.map((n) => `- **${n.title}** (${n.id.slice(0, 8)}\u2026)
2894
+ ${n.content.slice(0, 100)}${n.content.length > 100 ? "\u2026" : ""}`).join("\n");
2895
+ return { success: true, data: notes, display: `${notes.length} note(s):
2896
+ ${display}` };
2801
2897
  }
2802
2898
  searchNotes(input2, context) {
2803
2899
  const query = input2.query;
2804
2900
  if (!query || typeof query !== "string") {
2805
- return {
2806
- success: false,
2807
- error: 'Missing required field "query" for search action'
2808
- };
2901
+ return { success: false, error: 'Missing required field "query" for search action' };
2809
2902
  }
2810
- const lowerQuery = query.toLowerCase();
2811
- const matches = [];
2812
- for (const [, entry] of this.notes) {
2813
- if (entry.userId !== context.userId) {
2814
- continue;
2815
- }
2816
- if (entry.title.toLowerCase().includes(lowerQuery) || entry.content.toLowerCase().includes(lowerQuery)) {
2817
- matches.push({
2818
- noteId: entry.noteId,
2819
- title: entry.title,
2820
- content: entry.content
2821
- });
2822
- }
2903
+ const matches = this.noteRepo.search(context.userId, query);
2904
+ if (matches.length === 0) {
2905
+ return { success: true, data: [], display: `No notes matching "${query}".` };
2823
2906
  }
2824
- return {
2825
- success: true,
2826
- data: matches,
2827
- display: matches.length === 0 ? `No notes matching "${query}".` : `Found ${matches.length} note(s):
2828
- ${matches.map((n) => `- ${n.noteId}: "${n.title}"`).join("\n")}`
2829
- };
2907
+ const display = matches.map((n) => `- **${n.title}** (${n.id.slice(0, 8)}\u2026)
2908
+ ${n.content.slice(0, 100)}${n.content.length > 100 ? "\u2026" : ""}`).join("\n");
2909
+ return { success: true, data: matches, display: `Found ${matches.length} note(s):
2910
+ ${display}` };
2830
2911
  }
2831
2912
  deleteNote(input2) {
2832
2913
  const noteId = input2.noteId;
2833
2914
  if (!noteId || typeof noteId !== "string") {
2834
- return {
2835
- success: false,
2836
- error: 'Missing required field "noteId" for delete action'
2837
- };
2838
- }
2839
- const entry = this.notes.get(noteId);
2840
- if (!entry) {
2841
- return {
2842
- success: false,
2843
- error: `Note "${noteId}" not found`
2844
- };
2845
- }
2846
- this.notes.delete(noteId);
2847
- return {
2848
- success: true,
2849
- data: { noteId },
2850
- display: `Note "${noteId}" deleted.`
2851
- };
2852
- }
2853
- };
2854
- }
2855
- });
2856
-
2857
- // ../skills/dist/built-in/summarize.js
2858
- var DEFAULT_MAX_LENGTH, SummarizeSkill;
2859
- var init_summarize = __esm({
2860
- "../skills/dist/built-in/summarize.js"() {
2861
- "use strict";
2862
- init_skill();
2863
- DEFAULT_MAX_LENGTH = 280;
2864
- SummarizeSkill = class extends Skill {
2865
- metadata = {
2866
- name: "summarize",
2867
- description: "Produce an extractive summary of the given text",
2868
- riskLevel: "read",
2869
- version: "1.0.0",
2870
- inputSchema: {
2871
- type: "object",
2872
- properties: {
2873
- text: {
2874
- type: "string",
2875
- description: "The text to summarize"
2876
- },
2877
- maxLength: {
2878
- type: "number",
2879
- description: "Maximum character length for the summary (default: 280)"
2880
- }
2881
- },
2882
- required: ["text"]
2883
- }
2884
- };
2885
- async execute(input2, _context) {
2886
- const text = input2.text;
2887
- const maxLength = input2.maxLength ?? DEFAULT_MAX_LENGTH;
2888
- if (!text || typeof text !== "string") {
2889
- return {
2890
- success: false,
2891
- error: 'Invalid input: "text" must be a non-empty string'
2892
- };
2893
- }
2894
- if (text.length <= maxLength) {
2895
- return {
2896
- success: true,
2897
- data: { summary: text },
2898
- display: text
2899
- };
2900
- }
2901
- const summary = this.extractiveSummarize(text, maxLength);
2902
- return {
2903
- success: true,
2904
- data: { summary },
2905
- display: summary
2906
- };
2907
- }
2908
- extractiveSummarize(text, maxLength) {
2909
- const sentences = text.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
2910
- if (sentences.length === 0) {
2911
- return text.slice(0, maxLength);
2912
- }
2913
- const wordFrequency = this.buildWordFrequency(text);
2914
- const scored = sentences.map((sentence, index) => ({
2915
- sentence,
2916
- index,
2917
- score: this.scoreSentence(sentence, wordFrequency)
2918
- }));
2919
- const ranked = [...scored].sort((a, b) => b.score - a.score);
2920
- const selected = [];
2921
- let currentLength = 0;
2922
- for (const entry of ranked) {
2923
- const addition = currentLength === 0 ? entry.sentence.length : entry.sentence.length + 1;
2924
- if (currentLength + addition > maxLength) {
2925
- continue;
2926
- }
2927
- selected.push(entry);
2928
- currentLength += addition;
2929
- }
2930
- if (selected.length === 0) {
2931
- return sentences[0].slice(0, maxLength);
2932
- }
2933
- selected.sort((a, b) => a.index - b.index);
2934
- return selected.map((s) => s.sentence).join(" ");
2935
- }
2936
- buildWordFrequency(text) {
2937
- const stopWords = /* @__PURE__ */ new Set([
2938
- "the",
2939
- "a",
2940
- "an",
2941
- "is",
2942
- "are",
2943
- "was",
2944
- "were",
2945
- "be",
2946
- "been",
2947
- "being",
2948
- "have",
2949
- "has",
2950
- "had",
2951
- "do",
2952
- "does",
2953
- "did",
2954
- "will",
2955
- "would",
2956
- "could",
2957
- "should",
2958
- "may",
2959
- "might",
2960
- "shall",
2961
- "can",
2962
- "to",
2963
- "of",
2964
- "in",
2965
- "for",
2966
- "on",
2967
- "with",
2968
- "at",
2969
- "by",
2970
- "from",
2971
- "as",
2972
- "into",
2973
- "through",
2974
- "during",
2975
- "before",
2976
- "after",
2977
- "and",
2978
- "but",
2979
- "or",
2980
- "nor",
2981
- "not",
2982
- "so",
2983
- "yet",
2984
- "both",
2985
- "either",
2986
- "neither",
2987
- "each",
2988
- "every",
2989
- "all",
2990
- "any",
2991
- "few",
2992
- "more",
2993
- "most",
2994
- "other",
2995
- "some",
2996
- "such",
2997
- "no",
2998
- "only",
2999
- "own",
3000
- "same",
3001
- "than",
3002
- "too",
3003
- "very",
3004
- "just",
3005
- "because",
3006
- "if",
3007
- "when",
3008
- "where",
3009
- "how",
3010
- "what",
3011
- "which",
3012
- "who",
3013
- "whom",
3014
- "this",
3015
- "that",
3016
- "these",
3017
- "those",
3018
- "it",
3019
- "its",
3020
- "i",
3021
- "me",
3022
- "my",
3023
- "we",
3024
- "our",
3025
- "you",
3026
- "your",
3027
- "he",
3028
- "him",
3029
- "his",
3030
- "she",
3031
- "her",
3032
- "they",
3033
- "them",
3034
- "their"
3035
- ]);
3036
- const frequency = /* @__PURE__ */ new Map();
3037
- const words = text.toLowerCase().match(/\b[a-z]+\b/g) ?? [];
3038
- for (const word of words) {
3039
- if (stopWords.has(word) || word.length < 3) {
3040
- continue;
3041
- }
3042
- frequency.set(word, (frequency.get(word) ?? 0) + 1);
3043
- }
3044
- return frequency;
3045
- }
3046
- scoreSentence(sentence, wordFrequency) {
3047
- const words = sentence.toLowerCase().match(/\b[a-z]+\b/g) ?? [];
3048
- let score = 0;
3049
- for (const word of words) {
3050
- score += wordFrequency.get(word) ?? 0;
3051
- }
3052
- return score;
3053
- }
3054
- };
3055
- }
3056
- });
3057
-
3058
- // ../skills/dist/built-in/translate.js
3059
- var TranslateSkill;
3060
- var init_translate = __esm({
3061
- "../skills/dist/built-in/translate.js"() {
3062
- "use strict";
3063
- init_skill();
3064
- TranslateSkill = class extends Skill {
3065
- metadata = {
3066
- name: "translate",
3067
- description: "Translate text between languages (placeholder \u2014 requires external API)",
3068
- riskLevel: "read",
3069
- version: "0.1.0",
3070
- inputSchema: {
3071
- type: "object",
3072
- properties: {
3073
- text: {
3074
- type: "string",
3075
- description: "The text to translate"
3076
- },
3077
- targetLanguage: {
3078
- type: "string",
3079
- description: 'The language to translate into (e.g. "es", "fr", "de")'
3080
- },
3081
- sourceLanguage: {
3082
- type: "string",
3083
- description: "The source language (optional, auto-detected if omitted)"
3084
- }
3085
- },
3086
- required: ["text", "targetLanguage"]
2915
+ return { success: false, error: 'Missing required field "noteId" for delete action' };
3087
2916
  }
3088
- };
3089
- async execute(input2, _context) {
3090
- const text = input2.text;
3091
- const targetLanguage = input2.targetLanguage;
3092
- const sourceLanguage = input2.sourceLanguage;
3093
- if (!text || typeof text !== "string") {
3094
- return {
3095
- success: false,
3096
- error: 'Invalid input: "text" must be a non-empty string'
3097
- };
3098
- }
3099
- if (!targetLanguage || typeof targetLanguage !== "string") {
3100
- return {
3101
- success: false,
3102
- error: 'Invalid input: "targetLanguage" must be a non-empty string'
3103
- };
2917
+ const deleted = this.noteRepo.delete(noteId);
2918
+ if (!deleted) {
2919
+ return { success: false, error: `Note "${noteId}" not found` };
3104
2920
  }
3105
- const sourceLabel = sourceLanguage ? ` from "${sourceLanguage}"` : "";
3106
- return {
3107
- success: true,
3108
- data: {
3109
- note: "Translation is not yet connected to a translation API",
3110
- text,
3111
- targetLanguage,
3112
- sourceLanguage: sourceLanguage ?? "auto"
3113
- },
3114
- display: `Translation${sourceLabel} to "${targetLanguage}" is not yet implemented. This skill will be connected to a translation API in a future update.
3115
-
3116
- Requested text: "${text}"`
3117
- };
2921
+ return { success: true, data: { noteId }, display: `Note deleted.` };
3118
2922
  }
3119
2923
  };
3120
2924
  }
3121
2925
  });
3122
2926
 
3123
2927
  // ../skills/dist/built-in/weather.js
3124
- var WeatherSkill;
2928
+ var WEATHER_CODES, WeatherSkill;
3125
2929
  var init_weather = __esm({
3126
2930
  "../skills/dist/built-in/weather.js"() {
3127
2931
  "use strict";
3128
2932
  init_skill();
2933
+ WEATHER_CODES = {
2934
+ 0: "Clear sky",
2935
+ 1: "Mainly clear",
2936
+ 2: "Partly cloudy",
2937
+ 3: "Overcast",
2938
+ 45: "Foggy",
2939
+ 48: "Depositing rime fog",
2940
+ 51: "Light drizzle",
2941
+ 53: "Moderate drizzle",
2942
+ 55: "Dense drizzle",
2943
+ 61: "Slight rain",
2944
+ 63: "Moderate rain",
2945
+ 65: "Heavy rain",
2946
+ 71: "Slight snow",
2947
+ 73: "Moderate snow",
2948
+ 75: "Heavy snow",
2949
+ 77: "Snow grains",
2950
+ 80: "Slight rain showers",
2951
+ 81: "Moderate rain showers",
2952
+ 82: "Violent rain showers",
2953
+ 85: "Slight snow showers",
2954
+ 86: "Heavy snow showers",
2955
+ 95: "Thunderstorm",
2956
+ 96: "Thunderstorm with slight hail",
2957
+ 99: "Thunderstorm with heavy hail"
2958
+ };
3129
2959
  WeatherSkill = class extends Skill {
3130
2960
  metadata = {
3131
2961
  name: "weather",
3132
- description: "Get weather information for a location (placeholder \u2014 requires API key)",
2962
+ description: "Get current weather for any location. Uses Open-Meteo (free, no API key). Use when the user asks about weather, temperature, or conditions somewhere.",
3133
2963
  riskLevel: "read",
3134
- version: "0.1.0",
2964
+ version: "2.0.0",
3135
2965
  inputSchema: {
3136
2966
  type: "object",
3137
2967
  properties: {
3138
2968
  location: {
3139
2969
  type: "string",
3140
- description: 'The location to get weather for (e.g. "London", "New York, NY")'
3141
- },
3142
- units: {
3143
- type: "string",
3144
- enum: ["metric", "imperial"],
3145
- description: "Unit system for temperature (default: metric)"
2970
+ description: 'City or place name (e.g. "Vienna", "New York", "Tokyo")'
3146
2971
  }
3147
2972
  },
3148
2973
  required: ["location"]
@@ -3150,22 +2975,49 @@ var init_weather = __esm({
3150
2975
  };
3151
2976
  async execute(input2, _context) {
3152
2977
  const location = input2.location;
3153
- const units = input2.units ?? "metric";
3154
2978
  if (!location || typeof location !== "string") {
3155
- return {
3156
- success: false,
3157
- error: 'Invalid input: "location" must be a non-empty string'
2979
+ return { success: false, error: 'Missing required field "location"' };
2980
+ }
2981
+ try {
2982
+ const geo = await this.geocode(location);
2983
+ if (!geo) {
2984
+ return { success: false, error: `Location "${location}" not found` };
2985
+ }
2986
+ const weather = await this.fetchWeather(geo.latitude, geo.longitude);
2987
+ const condition = WEATHER_CODES[weather.weathercode] ?? `Code ${weather.weathercode}`;
2988
+ const locationLabel = geo.admin1 ? `${geo.name}, ${geo.admin1}, ${geo.country}` : `${geo.name}, ${geo.country}`;
2989
+ const data = {
2990
+ location: locationLabel,
2991
+ temperature: weather.temperature,
2992
+ unit: "\xB0C",
2993
+ condition,
2994
+ windSpeed: weather.windspeed,
2995
+ windDirection: weather.winddirection,
2996
+ isDay: weather.is_day === 1
3158
2997
  };
2998
+ const display = `${locationLabel}: ${weather.temperature}\xB0C, ${condition}
2999
+ Wind: ${weather.windspeed} km/h`;
3000
+ return { success: true, data, display };
3001
+ } catch (err) {
3002
+ const msg = err instanceof Error ? err.message : String(err);
3003
+ return { success: false, error: `Weather fetch failed: ${msg}` };
3159
3004
  }
3160
- return {
3161
- success: true,
3162
- data: {
3163
- note: "Weather data is not yet available \u2014 API key configuration required",
3164
- location,
3165
- units
3166
- },
3167
- display: `Weather for "${location}" (${units}) is not yet implemented. This skill requires a weather API key to be configured.`
3168
- };
3005
+ }
3006
+ async geocode(query) {
3007
+ const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=en&format=json`;
3008
+ const res = await fetch(url);
3009
+ if (!res.ok)
3010
+ throw new Error(`Geocoding API returned ${res.status}`);
3011
+ const data = await res.json();
3012
+ return data.results?.[0];
3013
+ }
3014
+ async fetchWeather(lat, lon) {
3015
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&timezone=auto`;
3016
+ const res = await fetch(url);
3017
+ if (!res.ok)
3018
+ throw new Error(`Weather API returned ${res.status}`);
3019
+ const data = await res.json();
3020
+ return data.current_weather;
3169
3021
  }
3170
3022
  };
3171
3023
  }
@@ -3420,38 +3272,45 @@ ${entries.map((e) => `- [${e.category}] ${e.key}: "${e.value}"`).join("\n")}`
3420
3272
  });
3421
3273
 
3422
3274
  // ../skills/dist/built-in/delegate.js
3423
- var DelegateSkill;
3275
+ var MAX_SUB_AGENT_ITERATIONS, DelegateSkill;
3424
3276
  var init_delegate = __esm({
3425
3277
  "../skills/dist/built-in/delegate.js"() {
3426
3278
  "use strict";
3427
3279
  init_skill();
3280
+ MAX_SUB_AGENT_ITERATIONS = 5;
3428
3281
  DelegateSkill = class extends Skill {
3429
3282
  llm;
3283
+ skillRegistry;
3284
+ skillSandbox;
3285
+ securityManager;
3430
3286
  metadata = {
3431
3287
  name: "delegate",
3432
- description: "Delegate a complex sub-task to a separate AI agent. The sub-agent will process the task independently and return a result. Use this for tasks that require focused attention or multiple steps.",
3288
+ description: 'Delegate a complex sub-task to an autonomous sub-agent that has full tool access. The sub-agent can use shell, web search, calculator, memory, email, and all other tools. Use when a task is independent enough to run in parallel or when it requires a focused, multi-step workflow (e.g. "research X and summarize", "find all TODO files and list them", "check the weather and draft a packing list"). The sub-agent runs up to 5 tool iterations autonomously.',
3433
3289
  riskLevel: "write",
3434
- version: "1.0.0",
3290
+ version: "2.0.0",
3435
3291
  inputSchema: {
3436
3292
  type: "object",
3437
3293
  properties: {
3438
3294
  task: {
3439
3295
  type: "string",
3440
- description: "The task to delegate to a sub-agent"
3296
+ description: "The task to delegate to the sub-agent. Be specific about what you want."
3441
3297
  },
3442
3298
  context: {
3443
3299
  type: "string",
3444
- description: "Additional context for the sub-agent (optional)"
3300
+ description: "Additional context the sub-agent needs (optional)"
3445
3301
  }
3446
3302
  },
3447
3303
  required: ["task"]
3448
3304
  }
3449
3305
  };
3450
- constructor(llm) {
3306
+ constructor(llm, skillRegistry, skillSandbox, securityManager) {
3451
3307
  super();
3452
3308
  this.llm = llm;
3309
+ this.skillRegistry = skillRegistry;
3310
+ this.skillSandbox = skillSandbox;
3311
+ this.securityManager = securityManager;
3453
3312
  }
3454
- async execute(input2, _context) {
3313
+ async execute(input2, context) {
3455
3314
  const task = input2.task;
3456
3315
  const additionalContext = input2.context;
3457
3316
  if (!task || typeof task !== "string") {
@@ -3460,7 +3319,8 @@ var init_delegate = __esm({
3460
3319
  error: 'Missing required field "task"'
3461
3320
  };
3462
3321
  }
3463
- const systemPrompt = "You are a sub-agent of Alfred. Complete the following task concisely and return the result. Do not use tools.";
3322
+ const tools = this.buildSubAgentTools();
3323
+ const systemPrompt = "You are a sub-agent of Alfred, a personal AI assistant. Complete the assigned task using the tools available to you. Work step by step: use tools to gather information, then synthesize a clear result. Be concise and return only the final answer when done.";
3464
3324
  let userContent = task;
3465
3325
  if (additionalContext && typeof additionalContext === "string") {
3466
3326
  userContent = `${task}
@@ -3468,22 +3328,58 @@ var init_delegate = __esm({
3468
3328
  Additional context: ${additionalContext}`;
3469
3329
  }
3470
3330
  const messages = [
3471
- {
3472
- role: "user",
3473
- content: userContent
3474
- }
3331
+ { role: "user", content: userContent }
3475
3332
  ];
3476
3333
  try {
3477
- const response = await this.llm.complete({
3478
- messages,
3479
- system: systemPrompt,
3480
- maxTokens: 2048
3481
- });
3482
- return {
3483
- success: true,
3484
- data: { response: response.content, usage: response.usage },
3485
- display: response.content
3486
- };
3334
+ let iteration = 0;
3335
+ let totalInputTokens = 0;
3336
+ let totalOutputTokens = 0;
3337
+ while (true) {
3338
+ const response = await this.llm.complete({
3339
+ messages,
3340
+ system: systemPrompt,
3341
+ tools: tools.length > 0 ? tools : void 0,
3342
+ maxTokens: 2048
3343
+ });
3344
+ totalInputTokens += response.usage.inputTokens;
3345
+ totalOutputTokens += response.usage.outputTokens;
3346
+ if (!response.toolCalls || response.toolCalls.length === 0 || iteration >= MAX_SUB_AGENT_ITERATIONS) {
3347
+ return {
3348
+ success: true,
3349
+ data: {
3350
+ response: response.content,
3351
+ iterations: iteration,
3352
+ usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens }
3353
+ },
3354
+ display: response.content
3355
+ };
3356
+ }
3357
+ iteration++;
3358
+ const assistantContent = [];
3359
+ if (response.content) {
3360
+ assistantContent.push({ type: "text", text: response.content });
3361
+ }
3362
+ for (const tc of response.toolCalls) {
3363
+ assistantContent.push({
3364
+ type: "tool_use",
3365
+ id: tc.id,
3366
+ name: tc.name,
3367
+ input: tc.input
3368
+ });
3369
+ }
3370
+ messages.push({ role: "assistant", content: assistantContent });
3371
+ const toolResultBlocks = [];
3372
+ for (const toolCall of response.toolCalls) {
3373
+ const result = await this.executeSubAgentTool(toolCall, context);
3374
+ toolResultBlocks.push({
3375
+ type: "tool_result",
3376
+ tool_use_id: toolCall.id,
3377
+ content: result.content,
3378
+ is_error: result.isError
3379
+ });
3380
+ }
3381
+ messages.push({ role: "user", content: toolResultBlocks });
3382
+ }
3487
3383
  } catch (err) {
3488
3384
  const errorMessage = err instanceof Error ? err.message : String(err);
3489
3385
  return {
@@ -3492,15 +3388,63 @@ Additional context: ${additionalContext}`;
3492
3388
  };
3493
3389
  }
3494
3390
  }
3495
- };
3496
- }
3497
- });
3498
-
3499
- // ../skills/dist/built-in/email.js
3500
- var EmailSkill;
3501
- var init_email = __esm({
3502
- "../skills/dist/built-in/email.js"() {
3503
- "use strict";
3391
+ buildSubAgentTools() {
3392
+ if (!this.skillRegistry)
3393
+ return [];
3394
+ return this.skillRegistry.getAll().filter((s) => s.metadata.name !== "delegate").map((s) => ({
3395
+ name: s.metadata.name,
3396
+ description: s.metadata.description,
3397
+ inputSchema: s.metadata.inputSchema
3398
+ }));
3399
+ }
3400
+ async executeSubAgentTool(toolCall, context) {
3401
+ const skill = this.skillRegistry?.get(toolCall.name);
3402
+ if (!skill) {
3403
+ return { content: `Error: Unknown tool "${toolCall.name}"`, isError: true };
3404
+ }
3405
+ if (this.securityManager) {
3406
+ const evaluation = this.securityManager.evaluate({
3407
+ userId: context.userId,
3408
+ action: toolCall.name,
3409
+ riskLevel: skill.metadata.riskLevel,
3410
+ platform: context.platform,
3411
+ chatId: context.chatId,
3412
+ chatType: context.chatType
3413
+ });
3414
+ if (!evaluation.allowed) {
3415
+ return {
3416
+ content: `Access denied: ${evaluation.reason}`,
3417
+ isError: true
3418
+ };
3419
+ }
3420
+ }
3421
+ if (this.skillSandbox) {
3422
+ const result = await this.skillSandbox.execute(skill, toolCall.input, context);
3423
+ return {
3424
+ content: result.display ?? (result.success ? JSON.stringify(result.data) : result.error ?? "Unknown error"),
3425
+ isError: !result.success
3426
+ };
3427
+ }
3428
+ try {
3429
+ const result = await skill.execute(toolCall.input, context);
3430
+ return {
3431
+ content: result.display ?? (result.success ? JSON.stringify(result.data) : result.error ?? "Unknown error"),
3432
+ isError: !result.success
3433
+ };
3434
+ } catch (error) {
3435
+ const msg = error instanceof Error ? error.message : String(error);
3436
+ return { content: `Skill execution failed: ${msg}`, isError: true };
3437
+ }
3438
+ }
3439
+ };
3440
+ }
3441
+ });
3442
+
3443
+ // ../skills/dist/built-in/email.js
3444
+ var EmailSkill;
3445
+ var init_email = __esm({
3446
+ "../skills/dist/built-in/email.js"() {
3447
+ "use strict";
3504
3448
  init_skill();
3505
3449
  EmailSkill = class extends Skill {
3506
3450
  config;
@@ -3809,10 +3753,806 @@ Message ID: ${info.messageId}`
3809
3753
  }
3810
3754
  }
3811
3755
  }
3812
- return this.decodeBody(parts.slice(1).join("\n\n").slice(0, 5e3));
3813
- }
3814
- decodeBody(body) {
3815
- return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).trim();
3756
+ return this.decodeBody(parts.slice(1).join("\n\n").slice(0, 5e3));
3757
+ }
3758
+ decodeBody(body) {
3759
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).trim();
3760
+ }
3761
+ };
3762
+ }
3763
+ });
3764
+
3765
+ // ../skills/dist/built-in/http.js
3766
+ var MAX_RESPONSE_SIZE, HttpSkill;
3767
+ var init_http = __esm({
3768
+ "../skills/dist/built-in/http.js"() {
3769
+ "use strict";
3770
+ init_skill();
3771
+ MAX_RESPONSE_SIZE = 1e5;
3772
+ HttpSkill = class extends Skill {
3773
+ metadata = {
3774
+ name: "http",
3775
+ description: "Make HTTP requests to fetch web pages or call REST APIs. Use when you need to read a URL, call an API endpoint, or fetch data from the web. Supports GET, POST, PUT, PATCH, DELETE methods.",
3776
+ riskLevel: "write",
3777
+ version: "1.0.0",
3778
+ inputSchema: {
3779
+ type: "object",
3780
+ properties: {
3781
+ url: {
3782
+ type: "string",
3783
+ description: "The URL to request"
3784
+ },
3785
+ method: {
3786
+ type: "string",
3787
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
3788
+ description: "HTTP method (default: GET)"
3789
+ },
3790
+ headers: {
3791
+ type: "object",
3792
+ description: "Request headers as key-value pairs (optional)"
3793
+ },
3794
+ body: {
3795
+ type: "string",
3796
+ description: "Request body for POST/PUT/PATCH (optional)"
3797
+ }
3798
+ },
3799
+ required: ["url"]
3800
+ }
3801
+ };
3802
+ async execute(input2, _context) {
3803
+ const url = input2.url;
3804
+ const method = (input2.method ?? "GET").toUpperCase();
3805
+ const headers = input2.headers;
3806
+ const body = input2.body;
3807
+ if (!url || typeof url !== "string") {
3808
+ return { success: false, error: 'Missing required field "url"' };
3809
+ }
3810
+ try {
3811
+ new URL(url);
3812
+ } catch {
3813
+ return { success: false, error: `Invalid URL: "${url}"` };
3814
+ }
3815
+ try {
3816
+ const fetchOptions = {
3817
+ method,
3818
+ headers: {
3819
+ "User-Agent": "Alfred/1.0",
3820
+ ...headers ?? {}
3821
+ },
3822
+ signal: AbortSignal.timeout(15e3)
3823
+ };
3824
+ if (body && ["POST", "PUT", "PATCH"].includes(method)) {
3825
+ fetchOptions.body = body;
3826
+ if (!headers?.["Content-Type"] && !headers?.["content-type"]) {
3827
+ fetchOptions.headers["Content-Type"] = "application/json";
3828
+ }
3829
+ }
3830
+ const res = await fetch(url, fetchOptions);
3831
+ const contentType = res.headers.get("content-type") ?? "";
3832
+ const text = await res.text();
3833
+ const truncated = text.length > MAX_RESPONSE_SIZE;
3834
+ const responseBody = truncated ? text.slice(0, MAX_RESPONSE_SIZE) + "\n\n[... truncated]" : text;
3835
+ let display = responseBody;
3836
+ if (contentType.includes("text/html")) {
3837
+ display = this.stripHtml(responseBody).slice(0, 1e4);
3838
+ }
3839
+ const data = {
3840
+ status: res.status,
3841
+ statusText: res.statusText,
3842
+ contentType,
3843
+ bodyLength: text.length,
3844
+ truncated,
3845
+ body: responseBody
3846
+ };
3847
+ if (!res.ok) {
3848
+ return {
3849
+ success: true,
3850
+ data,
3851
+ display: `HTTP ${res.status} ${res.statusText}
3852
+
3853
+ ${display.slice(0, 2e3)}`
3854
+ };
3855
+ }
3856
+ return {
3857
+ success: true,
3858
+ data,
3859
+ display: `HTTP ${res.status} OK (${text.length} bytes)
3860
+
3861
+ ${display.slice(0, 5e3)}`
3862
+ };
3863
+ } catch (err) {
3864
+ const msg = err instanceof Error ? err.message : String(err);
3865
+ return { success: false, error: `HTTP request failed: ${msg}` };
3866
+ }
3867
+ }
3868
+ stripHtml(html) {
3869
+ return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
3870
+ }
3871
+ };
3872
+ }
3873
+ });
3874
+
3875
+ // ../skills/dist/built-in/file.js
3876
+ import fs4 from "node:fs";
3877
+ import path4 from "node:path";
3878
+ var MAX_READ_SIZE, FileSkill;
3879
+ var init_file = __esm({
3880
+ "../skills/dist/built-in/file.js"() {
3881
+ "use strict";
3882
+ init_skill();
3883
+ MAX_READ_SIZE = 5e5;
3884
+ FileSkill = class extends Skill {
3885
+ metadata = {
3886
+ name: "file",
3887
+ description: 'Read, write, move, or copy files. Use for reading file contents, writing text to files, saving binary data, listing directory contents, moving/copying files, or getting file info. Prefer this over shell for file operations. When a user sends a file attachment, it is saved to the inbox \u2014 use "move" to relocate it.',
3888
+ riskLevel: "write",
3889
+ version: "2.0.0",
3890
+ inputSchema: {
3891
+ type: "object",
3892
+ properties: {
3893
+ action: {
3894
+ type: "string",
3895
+ enum: ["read", "write", "write_binary", "append", "list", "info", "exists", "move", "copy", "delete"],
3896
+ description: "The file operation to perform"
3897
+ },
3898
+ path: {
3899
+ type: "string",
3900
+ description: "Absolute or relative file/directory path (~ expands to home)"
3901
+ },
3902
+ destination: {
3903
+ type: "string",
3904
+ description: "Destination path for move/copy actions (~ expands to home)"
3905
+ },
3906
+ content: {
3907
+ type: "string",
3908
+ description: "Content to write (required for write/append; base64-encoded for write_binary)"
3909
+ }
3910
+ },
3911
+ required: ["action", "path"]
3912
+ }
3913
+ };
3914
+ async execute(input2, _context) {
3915
+ const action = input2.action;
3916
+ const rawPath = input2.path;
3917
+ const content = input2.content;
3918
+ const destination = input2.destination;
3919
+ if (!action || !rawPath) {
3920
+ return { success: false, error: 'Missing required fields "action" and "path"' };
3921
+ }
3922
+ const resolvedPath = this.resolvePath(rawPath);
3923
+ switch (action) {
3924
+ case "read":
3925
+ return this.readFile(resolvedPath);
3926
+ case "write":
3927
+ return this.writeFile(resolvedPath, content);
3928
+ case "write_binary":
3929
+ return this.writeBinaryFile(resolvedPath, content);
3930
+ case "append":
3931
+ return this.appendFile(resolvedPath, content);
3932
+ case "list":
3933
+ return this.listDir(resolvedPath);
3934
+ case "info":
3935
+ return this.fileInfo(resolvedPath);
3936
+ case "exists":
3937
+ return this.fileExists(resolvedPath);
3938
+ case "move":
3939
+ return this.moveFile(resolvedPath, destination);
3940
+ case "copy":
3941
+ return this.copyFile(resolvedPath, destination);
3942
+ case "delete":
3943
+ return this.deleteFile(resolvedPath);
3944
+ default:
3945
+ return { success: false, error: `Unknown action "${action}". Valid: read, write, write_binary, append, list, info, exists, move, copy, delete` };
3946
+ }
3947
+ }
3948
+ resolvePath(raw) {
3949
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
3950
+ const expanded = raw.startsWith("~") ? raw.replace("~", home) : raw;
3951
+ return path4.resolve(expanded);
3952
+ }
3953
+ readFile(filePath) {
3954
+ try {
3955
+ const stat = fs4.statSync(filePath);
3956
+ if (stat.isDirectory()) {
3957
+ return { success: false, error: `"${filePath}" is a directory, not a file. Use action "list" instead.` };
3958
+ }
3959
+ if (stat.size > MAX_READ_SIZE) {
3960
+ const content2 = fs4.readFileSync(filePath, "utf-8").slice(0, MAX_READ_SIZE);
3961
+ return {
3962
+ success: true,
3963
+ data: { path: filePath, size: stat.size, truncated: true },
3964
+ display: `${filePath} (${stat.size} bytes, truncated to ${MAX_READ_SIZE}):
3965
+
3966
+ ${content2}`
3967
+ };
3968
+ }
3969
+ const content = fs4.readFileSync(filePath, "utf-8");
3970
+ return {
3971
+ success: true,
3972
+ data: { path: filePath, size: stat.size, content },
3973
+ display: content
3974
+ };
3975
+ } catch (err) {
3976
+ return { success: false, error: `Cannot read "${filePath}": ${err.message}` };
3977
+ }
3978
+ }
3979
+ writeFile(filePath, content) {
3980
+ if (content === void 0 || content === null) {
3981
+ return { success: false, error: 'Missing "content" for write action' };
3982
+ }
3983
+ try {
3984
+ const dir = path4.dirname(filePath);
3985
+ fs4.mkdirSync(dir, { recursive: true });
3986
+ fs4.writeFileSync(filePath, content, "utf-8");
3987
+ return {
3988
+ success: true,
3989
+ data: { path: filePath, bytes: Buffer.byteLength(content) },
3990
+ display: `Written ${Buffer.byteLength(content)} bytes to ${filePath}`
3991
+ };
3992
+ } catch (err) {
3993
+ return { success: false, error: `Cannot write "${filePath}": ${err.message}` };
3994
+ }
3995
+ }
3996
+ appendFile(filePath, content) {
3997
+ if (content === void 0 || content === null) {
3998
+ return { success: false, error: 'Missing "content" for append action' };
3999
+ }
4000
+ try {
4001
+ fs4.appendFileSync(filePath, content, "utf-8");
4002
+ return {
4003
+ success: true,
4004
+ data: { path: filePath, appendedBytes: Buffer.byteLength(content) },
4005
+ display: `Appended ${Buffer.byteLength(content)} bytes to ${filePath}`
4006
+ };
4007
+ } catch (err) {
4008
+ return { success: false, error: `Cannot append to "${filePath}": ${err.message}` };
4009
+ }
4010
+ }
4011
+ listDir(dirPath) {
4012
+ try {
4013
+ const entries = fs4.readdirSync(dirPath, { withFileTypes: true });
4014
+ const items = entries.map((e) => ({
4015
+ name: e.name,
4016
+ type: e.isDirectory() ? "dir" : e.isSymbolicLink() ? "symlink" : "file"
4017
+ }));
4018
+ const display = items.length === 0 ? `${dirPath}: (empty)` : items.map((i) => `${i.type === "dir" ? "\u{1F4C1}" : "\u{1F4C4}"} ${i.name}`).join("\n");
4019
+ return { success: true, data: { path: dirPath, entries: items }, display };
4020
+ } catch (err) {
4021
+ return { success: false, error: `Cannot list "${dirPath}": ${err.message}` };
4022
+ }
4023
+ }
4024
+ fileInfo(filePath) {
4025
+ try {
4026
+ const stat = fs4.statSync(filePath);
4027
+ const info = {
4028
+ path: filePath,
4029
+ type: stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "other",
4030
+ size: stat.size,
4031
+ created: stat.birthtime.toISOString(),
4032
+ modified: stat.mtime.toISOString(),
4033
+ permissions: stat.mode.toString(8)
4034
+ };
4035
+ return {
4036
+ success: true,
4037
+ data: info,
4038
+ display: `${info.type}: ${filePath}
4039
+ Size: ${stat.size} bytes
4040
+ Modified: ${info.modified}`
4041
+ };
4042
+ } catch (err) {
4043
+ return { success: false, error: `Cannot stat "${filePath}": ${err.message}` };
4044
+ }
4045
+ }
4046
+ fileExists(filePath) {
4047
+ const exists = fs4.existsSync(filePath);
4048
+ return {
4049
+ success: true,
4050
+ data: { path: filePath, exists },
4051
+ display: exists ? `Yes, "${filePath}" exists` : `No, "${filePath}" does not exist`
4052
+ };
4053
+ }
4054
+ writeBinaryFile(filePath, base64Content) {
4055
+ if (!base64Content) {
4056
+ return { success: false, error: 'Missing "content" (base64-encoded) for write_binary action' };
4057
+ }
4058
+ try {
4059
+ const dir = path4.dirname(filePath);
4060
+ fs4.mkdirSync(dir, { recursive: true });
4061
+ const buffer = Buffer.from(base64Content, "base64");
4062
+ fs4.writeFileSync(filePath, buffer);
4063
+ return {
4064
+ success: true,
4065
+ data: { path: filePath, bytes: buffer.length },
4066
+ display: `Written ${buffer.length} bytes (binary) to ${filePath}`
4067
+ };
4068
+ } catch (err) {
4069
+ return { success: false, error: `Cannot write "${filePath}": ${err.message}` };
4070
+ }
4071
+ }
4072
+ moveFile(source, destination) {
4073
+ if (!destination) {
4074
+ return { success: false, error: 'Missing "destination" for move action' };
4075
+ }
4076
+ const resolvedDest = this.resolvePath(destination);
4077
+ try {
4078
+ const destDir = path4.dirname(resolvedDest);
4079
+ fs4.mkdirSync(destDir, { recursive: true });
4080
+ fs4.renameSync(source, resolvedDest);
4081
+ return {
4082
+ success: true,
4083
+ data: { from: source, to: resolvedDest },
4084
+ display: `Moved ${source} \u2192 ${resolvedDest}`
4085
+ };
4086
+ } catch (err) {
4087
+ try {
4088
+ fs4.copyFileSync(source, resolvedDest);
4089
+ fs4.unlinkSync(source);
4090
+ return {
4091
+ success: true,
4092
+ data: { from: source, to: resolvedDest },
4093
+ display: `Moved ${source} \u2192 ${resolvedDest}`
4094
+ };
4095
+ } catch (err2) {
4096
+ return { success: false, error: `Cannot move "${source}" to "${resolvedDest}": ${err2.message}` };
4097
+ }
4098
+ }
4099
+ }
4100
+ copyFile(source, destination) {
4101
+ if (!destination) {
4102
+ return { success: false, error: 'Missing "destination" for copy action' };
4103
+ }
4104
+ const resolvedDest = this.resolvePath(destination);
4105
+ try {
4106
+ const destDir = path4.dirname(resolvedDest);
4107
+ fs4.mkdirSync(destDir, { recursive: true });
4108
+ fs4.copyFileSync(source, resolvedDest);
4109
+ return {
4110
+ success: true,
4111
+ data: { from: source, to: resolvedDest },
4112
+ display: `Copied ${source} \u2192 ${resolvedDest}`
4113
+ };
4114
+ } catch (err) {
4115
+ return { success: false, error: `Cannot copy "${source}" to "${resolvedDest}": ${err.message}` };
4116
+ }
4117
+ }
4118
+ deleteFile(filePath) {
4119
+ try {
4120
+ if (!fs4.existsSync(filePath)) {
4121
+ return { success: false, error: `"${filePath}" does not exist` };
4122
+ }
4123
+ const stat = fs4.statSync(filePath);
4124
+ if (stat.isDirectory()) {
4125
+ return { success: false, error: `"${filePath}" is a directory. Use shell for directory deletion.` };
4126
+ }
4127
+ fs4.unlinkSync(filePath);
4128
+ return {
4129
+ success: true,
4130
+ data: { path: filePath },
4131
+ display: `Deleted ${filePath}`
4132
+ };
4133
+ } catch (err) {
4134
+ return { success: false, error: `Cannot delete "${filePath}": ${err.message}` };
4135
+ }
4136
+ }
4137
+ };
4138
+ }
4139
+ });
4140
+
4141
+ // ../skills/dist/built-in/clipboard.js
4142
+ import { execSync } from "node:child_process";
4143
+ var ClipboardSkill;
4144
+ var init_clipboard = __esm({
4145
+ "../skills/dist/built-in/clipboard.js"() {
4146
+ "use strict";
4147
+ init_skill();
4148
+ ClipboardSkill = class extends Skill {
4149
+ metadata = {
4150
+ name: "clipboard",
4151
+ description: "Read or write the system clipboard. Use when the user asks to copy something, paste from clipboard, or check what is in their clipboard.",
4152
+ riskLevel: "write",
4153
+ version: "1.0.0",
4154
+ inputSchema: {
4155
+ type: "object",
4156
+ properties: {
4157
+ action: {
4158
+ type: "string",
4159
+ enum: ["read", "write"],
4160
+ description: '"read" to get clipboard contents, "write" to set clipboard contents'
4161
+ },
4162
+ text: {
4163
+ type: "string",
4164
+ description: "Text to copy to clipboard (required for write)"
4165
+ }
4166
+ },
4167
+ required: ["action"]
4168
+ }
4169
+ };
4170
+ async execute(input2, _context) {
4171
+ const action = input2.action;
4172
+ switch (action) {
4173
+ case "read":
4174
+ return this.readClipboard();
4175
+ case "write":
4176
+ return this.writeClipboard(input2.text);
4177
+ default:
4178
+ return { success: false, error: `Unknown action "${action}". Valid: read, write` };
4179
+ }
4180
+ }
4181
+ readClipboard() {
4182
+ try {
4183
+ let content;
4184
+ switch (process.platform) {
4185
+ case "darwin":
4186
+ content = execSync("pbpaste", { encoding: "utf-8", timeout: 5e3 });
4187
+ break;
4188
+ case "win32":
4189
+ content = execSync("powershell -NoProfile -Command Get-Clipboard", {
4190
+ encoding: "utf-8",
4191
+ timeout: 5e3
4192
+ }).replace(/\r\n$/, "");
4193
+ break;
4194
+ default:
4195
+ content = execSync("xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output", {
4196
+ encoding: "utf-8",
4197
+ timeout: 5e3
4198
+ });
4199
+ break;
4200
+ }
4201
+ if (!content || content.trim().length === 0) {
4202
+ return { success: true, data: { content: "" }, display: "Clipboard is empty." };
4203
+ }
4204
+ return {
4205
+ success: true,
4206
+ data: { content },
4207
+ display: content.length > 2e3 ? content.slice(0, 2e3) + "\n\n[... truncated]" : content
4208
+ };
4209
+ } catch (err) {
4210
+ return { success: false, error: `Failed to read clipboard: ${err.message}` };
4211
+ }
4212
+ }
4213
+ writeClipboard(text) {
4214
+ if (!text || typeof text !== "string") {
4215
+ return { success: false, error: 'Missing "text" for write action' };
4216
+ }
4217
+ try {
4218
+ switch (process.platform) {
4219
+ case "darwin":
4220
+ execSync("pbcopy", { input: text, timeout: 5e3 });
4221
+ break;
4222
+ case "win32":
4223
+ execSync('powershell -NoProfile -Command "$input | Set-Clipboard"', {
4224
+ input: text,
4225
+ timeout: 5e3
4226
+ });
4227
+ break;
4228
+ default:
4229
+ execSync("xclip -selection clipboard 2>/dev/null || xsel --clipboard --input", {
4230
+ input: text,
4231
+ timeout: 5e3
4232
+ });
4233
+ break;
4234
+ }
4235
+ return {
4236
+ success: true,
4237
+ data: { copiedLength: text.length },
4238
+ display: `Copied ${text.length} characters to clipboard.`
4239
+ };
4240
+ } catch (err) {
4241
+ return { success: false, error: `Failed to write clipboard: ${err.message}` };
4242
+ }
4243
+ }
4244
+ };
4245
+ }
4246
+ });
4247
+
4248
+ // ../skills/dist/built-in/screenshot.js
4249
+ import { execSync as execSync2 } from "node:child_process";
4250
+ import path5 from "node:path";
4251
+ import os from "node:os";
4252
+ var ScreenshotSkill;
4253
+ var init_screenshot = __esm({
4254
+ "../skills/dist/built-in/screenshot.js"() {
4255
+ "use strict";
4256
+ init_skill();
4257
+ ScreenshotSkill = class extends Skill {
4258
+ metadata = {
4259
+ name: "screenshot",
4260
+ description: "Take a screenshot of the current screen and save it to a file. Use when the user asks to capture their screen or take a screenshot.",
4261
+ riskLevel: "write",
4262
+ version: "1.0.0",
4263
+ inputSchema: {
4264
+ type: "object",
4265
+ properties: {
4266
+ path: {
4267
+ type: "string",
4268
+ description: "Output file path (optional, defaults to ~/Desktop/screenshot-<timestamp>.png)"
4269
+ }
4270
+ }
4271
+ }
4272
+ };
4273
+ async execute(input2, _context) {
4274
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4275
+ const defaultDir = path5.join(os.homedir(), "Desktop");
4276
+ const outputPath = input2.path || path5.join(defaultDir, `screenshot-${timestamp}.png`);
4277
+ try {
4278
+ switch (process.platform) {
4279
+ case "darwin":
4280
+ execSync2(`screencapture -x "${outputPath}"`, { timeout: 1e4 });
4281
+ break;
4282
+ case "win32":
4283
+ execSync2(`powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height); $graphics = [System.Drawing.Graphics]::FromImage($bitmap); $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size); $bitmap.Save('${outputPath.replace(/'/g, "''")}'); $graphics.Dispose(); $bitmap.Dispose()"`, { timeout: 1e4 });
4284
+ break;
4285
+ default:
4286
+ try {
4287
+ execSync2(`scrot "${outputPath}"`, { timeout: 1e4 });
4288
+ } catch {
4289
+ try {
4290
+ execSync2(`import -window root "${outputPath}"`, { timeout: 1e4 });
4291
+ } catch {
4292
+ execSync2(`gnome-screenshot -f "${outputPath}"`, { timeout: 1e4 });
4293
+ }
4294
+ }
4295
+ break;
4296
+ }
4297
+ return {
4298
+ success: true,
4299
+ data: { path: outputPath },
4300
+ display: `Screenshot saved to ${outputPath}`
4301
+ };
4302
+ } catch (err) {
4303
+ return { success: false, error: `Screenshot failed: ${err.message}` };
4304
+ }
4305
+ }
4306
+ };
4307
+ }
4308
+ });
4309
+
4310
+ // ../skills/dist/built-in/browser.js
4311
+ import path6 from "node:path";
4312
+ import os2 from "node:os";
4313
+ var MAX_TEXT_LENGTH, BrowserSkill;
4314
+ var init_browser = __esm({
4315
+ "../skills/dist/built-in/browser.js"() {
4316
+ "use strict";
4317
+ init_skill();
4318
+ MAX_TEXT_LENGTH = 5e4;
4319
+ BrowserSkill = class extends Skill {
4320
+ browser = null;
4321
+ page = null;
4322
+ metadata = {
4323
+ name: "browser",
4324
+ description: "Open web pages in a real browser (Puppeteer/Chromium). Renders JavaScript, so it works with SPAs and dynamic sites. Can also interact with pages: click buttons, fill forms, take screenshots. Use when http skill returns empty/broken content, or when you need to interact with a web page.",
4325
+ riskLevel: "write",
4326
+ version: "1.0.0",
4327
+ inputSchema: {
4328
+ type: "object",
4329
+ properties: {
4330
+ action: {
4331
+ type: "string",
4332
+ enum: ["open", "screenshot", "click", "type", "evaluate", "close"],
4333
+ description: "open = navigate to URL and return page text. screenshot = save screenshot of current page. click = click element by CSS selector. type = type text into input by CSS selector. evaluate = run JavaScript on the page. close = close the browser."
4334
+ },
4335
+ url: {
4336
+ type: "string",
4337
+ description: 'URL to open (required for "open", optional for "screenshot")'
4338
+ },
4339
+ selector: {
4340
+ type: "string",
4341
+ description: 'CSS selector for the element (required for "click" and "type")'
4342
+ },
4343
+ text: {
4344
+ type: "string",
4345
+ description: 'Text to type (required for "type")'
4346
+ },
4347
+ script: {
4348
+ type: "string",
4349
+ description: 'JavaScript code to evaluate (required for "evaluate")'
4350
+ },
4351
+ path: {
4352
+ type: "string",
4353
+ description: "File path to save screenshot (optional, defaults to Desktop)"
4354
+ }
4355
+ },
4356
+ required: ["action"]
4357
+ }
4358
+ };
4359
+ async execute(input2, _context) {
4360
+ const action = input2.action;
4361
+ if (action === "close") {
4362
+ return this.closeBrowser();
4363
+ }
4364
+ const pup = await this.loadPuppeteer();
4365
+ if (!pup) {
4366
+ return {
4367
+ success: false,
4368
+ error: "Puppeteer is not installed. Run: npm install -g puppeteer\nOr add it to Alfred: npm install puppeteer"
4369
+ };
4370
+ }
4371
+ switch (action) {
4372
+ case "open":
4373
+ return this.openPage(pup, input2);
4374
+ case "screenshot":
4375
+ return this.screenshotPage(pup, input2);
4376
+ case "click":
4377
+ return this.clickElement(input2);
4378
+ case "type":
4379
+ return this.typeText(input2);
4380
+ case "evaluate":
4381
+ return this.evaluateScript(input2);
4382
+ default:
4383
+ return { success: false, error: `Unknown action "${action}". Valid: open, screenshot, click, type, evaluate, close` };
4384
+ }
4385
+ }
4386
+ async loadPuppeteer() {
4387
+ try {
4388
+ const mod = await Function('return import("puppeteer")')();
4389
+ return this.resolvePuppeteerModule(mod);
4390
+ } catch {
4391
+ try {
4392
+ const mod = await Function('return import("puppeteer-core")')();
4393
+ return this.resolvePuppeteerModule(mod);
4394
+ } catch {
4395
+ return null;
4396
+ }
4397
+ }
4398
+ }
4399
+ resolvePuppeteerModule(mod) {
4400
+ const m = mod;
4401
+ if (typeof m.launch === "function")
4402
+ return m;
4403
+ const def = m.default;
4404
+ return def;
4405
+ }
4406
+ async ensureBrowser(pup) {
4407
+ if (this.browser && this.browser.connected) {
4408
+ return this.browser;
4409
+ }
4410
+ this.browser = await pup.launch({
4411
+ headless: true,
4412
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
4413
+ });
4414
+ return this.browser;
4415
+ }
4416
+ async ensurePage(pup) {
4417
+ const browser = await this.ensureBrowser(pup);
4418
+ if (!this.page) {
4419
+ this.page = await browser.newPage();
4420
+ await this.page.setViewport({ width: 1280, height: 900 });
4421
+ }
4422
+ return this.page;
4423
+ }
4424
+ async openPage(pup, input2) {
4425
+ const url = input2.url;
4426
+ if (!url) {
4427
+ return { success: false, error: 'Missing "url" for open action' };
4428
+ }
4429
+ try {
4430
+ const page = await this.ensurePage(pup);
4431
+ await page.goto(url, { waitUntil: "networkidle2", timeout: 3e4 });
4432
+ const title = await page.title();
4433
+ const text = await page.evaluate(`
4434
+ (() => {
4435
+ document.querySelectorAll('script, style, noscript').forEach(el => el.remove());
4436
+ return document.body?.innerText ?? '';
4437
+ })()
4438
+ `);
4439
+ const trimmed = text.length > MAX_TEXT_LENGTH ? text.slice(0, MAX_TEXT_LENGTH) + "\n\n[... truncated]" : text;
4440
+ const cleaned = trimmed.replace(/\n{3,}/g, "\n\n").trim();
4441
+ return {
4442
+ success: true,
4443
+ data: { url: page.url(), title, length: text.length },
4444
+ display: `**${title}** (${page.url()})
4445
+
4446
+ ${cleaned}`
4447
+ };
4448
+ } catch (err) {
4449
+ return { success: false, error: `Failed to open "${url}": ${err.message}` };
4450
+ }
4451
+ }
4452
+ async screenshotPage(pup, input2) {
4453
+ try {
4454
+ const page = await this.ensurePage(pup);
4455
+ const url = input2.url;
4456
+ if (url) {
4457
+ await page.goto(url, { waitUntil: "networkidle2", timeout: 3e4 });
4458
+ }
4459
+ const currentUrl = page.url();
4460
+ if (currentUrl === "about:blank") {
4461
+ return { success: false, error: 'No page is open. Use action "open" with a URL first, or provide a URL.' };
4462
+ }
4463
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4464
+ const outputPath = input2.path || path6.join(os2.homedir(), "Desktop", `browser-${timestamp}.png`);
4465
+ await page.screenshot({ path: outputPath, fullPage: false });
4466
+ return {
4467
+ success: true,
4468
+ data: { path: outputPath, url: currentUrl },
4469
+ display: `Screenshot saved to ${outputPath}`
4470
+ };
4471
+ } catch (err) {
4472
+ return { success: false, error: `Screenshot failed: ${err.message}` };
4473
+ }
4474
+ }
4475
+ async clickElement(input2) {
4476
+ const selector = input2.selector;
4477
+ if (!selector) {
4478
+ return { success: false, error: 'Missing "selector" for click action' };
4479
+ }
4480
+ if (!this.page) {
4481
+ return { success: false, error: 'No page is open. Use action "open" first.' };
4482
+ }
4483
+ try {
4484
+ await this.page.waitForSelector(selector, { timeout: 5e3 });
4485
+ await this.page.click(selector);
4486
+ try {
4487
+ await this.page.waitForNavigation({ timeout: 3e3 });
4488
+ } catch {
4489
+ }
4490
+ const title = await this.page.title();
4491
+ return {
4492
+ success: true,
4493
+ data: { selector, url: this.page.url(), title },
4494
+ display: `Clicked "${selector}" \u2014 now on: ${title} (${this.page.url()})`
4495
+ };
4496
+ } catch (err) {
4497
+ return { success: false, error: `Click failed on "${selector}": ${err.message}` };
4498
+ }
4499
+ }
4500
+ async typeText(input2) {
4501
+ const selector = input2.selector;
4502
+ const text = input2.text;
4503
+ if (!selector)
4504
+ return { success: false, error: 'Missing "selector" for type action' };
4505
+ if (!text)
4506
+ return { success: false, error: 'Missing "text" for type action' };
4507
+ if (!this.page) {
4508
+ return { success: false, error: 'No page is open. Use action "open" first.' };
4509
+ }
4510
+ try {
4511
+ await this.page.waitForSelector(selector, { timeout: 5e3 });
4512
+ await this.page.click(selector);
4513
+ await this.page.type(selector, text, { delay: 50 });
4514
+ return {
4515
+ success: true,
4516
+ data: { selector, textLength: text.length },
4517
+ display: `Typed ${text.length} characters into "${selector}"`
4518
+ };
4519
+ } catch (err) {
4520
+ return { success: false, error: `Type failed on "${selector}": ${err.message}` };
4521
+ }
4522
+ }
4523
+ async evaluateScript(input2) {
4524
+ const script = input2.script;
4525
+ if (!script) {
4526
+ return { success: false, error: 'Missing "script" for evaluate action' };
4527
+ }
4528
+ if (!this.page) {
4529
+ return { success: false, error: 'No page is open. Use action "open" first.' };
4530
+ }
4531
+ try {
4532
+ const result = await this.page.evaluate(script);
4533
+ const output2 = typeof result === "string" ? result : JSON.stringify(result, null, 2);
4534
+ return {
4535
+ success: true,
4536
+ data: { result },
4537
+ display: output2?.slice(0, 1e4) ?? "(no output)"
4538
+ };
4539
+ } catch (err) {
4540
+ return { success: false, error: `Evaluate failed: ${err.message}` };
4541
+ }
4542
+ }
4543
+ async closeBrowser() {
4544
+ try {
4545
+ this.page = null;
4546
+ if (this.browser) {
4547
+ await this.browser.close();
4548
+ this.browser = null;
4549
+ }
4550
+ return { success: true, display: "Browser closed." };
4551
+ } catch (err) {
4552
+ this.browser = null;
4553
+ this.page = null;
4554
+ return { success: false, error: `Close failed: ${err.message}` };
4555
+ }
3816
4556
  }
3817
4557
  };
3818
4558
  }
@@ -3831,13 +4571,16 @@ var init_dist6 = __esm({
3831
4571
  init_web_search();
3832
4572
  init_reminder();
3833
4573
  init_note();
3834
- init_summarize();
3835
- init_translate();
3836
4574
  init_weather();
3837
4575
  init_shell();
3838
4576
  init_memory();
3839
4577
  init_delegate();
3840
4578
  init_email();
4579
+ init_http();
4580
+ init_file();
4581
+ init_clipboard();
4582
+ init_screenshot();
4583
+ init_browser();
3841
4584
  }
3842
4585
  });
3843
4586
 
@@ -3870,13 +4613,16 @@ var init_conversation_manager = __esm({
3870
4613
  });
3871
4614
 
3872
4615
  // ../core/dist/message-pipeline.js
3873
- var MAX_TOOL_ITERATIONS, TOKEN_BUDGET_RATIO, MessagePipeline;
4616
+ import fs5 from "node:fs";
4617
+ import path7 from "node:path";
4618
+ var MAX_TOOL_ITERATIONS, TOKEN_BUDGET_RATIO, MAX_INLINE_FILE_SIZE, MessagePipeline;
3874
4619
  var init_message_pipeline = __esm({
3875
4620
  "../core/dist/message-pipeline.js"() {
3876
4621
  "use strict";
3877
4622
  init_dist4();
3878
4623
  MAX_TOOL_ITERATIONS = 10;
3879
4624
  TOKEN_BUDGET_RATIO = 0.85;
4625
+ MAX_INLINE_FILE_SIZE = 1e5;
3880
4626
  MessagePipeline = class {
3881
4627
  llm;
3882
4628
  conversationManager;
@@ -3886,8 +4632,10 @@ var init_message_pipeline = __esm({
3886
4632
  skillSandbox;
3887
4633
  securityManager;
3888
4634
  memoryRepo;
4635
+ speechTranscriber;
4636
+ inboxPath;
3889
4637
  promptBuilder;
3890
- constructor(llm, conversationManager, users, logger, skillRegistry, skillSandbox, securityManager, memoryRepo) {
4638
+ constructor(llm, conversationManager, users, logger, skillRegistry, skillSandbox, securityManager, memoryRepo, speechTranscriber, inboxPath) {
3891
4639
  this.llm = llm;
3892
4640
  this.conversationManager = conversationManager;
3893
4641
  this.users = users;
@@ -3896,6 +4644,8 @@ var init_message_pipeline = __esm({
3896
4644
  this.skillSandbox = skillSandbox;
3897
4645
  this.securityManager = securityManager;
3898
4646
  this.memoryRepo = memoryRepo;
4647
+ this.speechTranscriber = speechTranscriber;
4648
+ this.inboxPath = inboxPath;
3899
4649
  this.promptBuilder = new PromptBuilder();
3900
4650
  }
3901
4651
  async process(message, onProgress) {
@@ -3917,7 +4667,8 @@ var init_message_pipeline = __esm({
3917
4667
  const tools = skillMetas ? this.promptBuilder.buildTools(skillMetas) : void 0;
3918
4668
  const system = this.promptBuilder.buildSystemPrompt(memories, skillMetas);
3919
4669
  const allMessages = this.promptBuilder.buildMessages(history);
3920
- allMessages.push({ role: "user", content: message.text });
4670
+ const userContent = await this.buildUserContent(message, onProgress);
4671
+ allMessages.push({ role: "user", content: userContent });
3921
4672
  const messages = this.trimToContextWindow(system, allMessages);
3922
4673
  let response;
3923
4674
  let iteration = 0;
@@ -4048,6 +4799,20 @@ var init_message_pipeline = __esm({
4048
4799
  return `Getting system info...`;
4049
4800
  case "delegate":
4050
4801
  return `Delegating sub-task...`;
4802
+ case "http":
4803
+ return `Fetching: ${String(input2.url ?? "").slice(0, 60)}`;
4804
+ case "file":
4805
+ return `File: ${String(input2.action ?? "")} ${String(input2.path ?? "").slice(0, 50)}`;
4806
+ case "clipboard":
4807
+ return `Clipboard: ${String(input2.action ?? "")}`;
4808
+ case "screenshot":
4809
+ return `Taking screenshot...`;
4810
+ case "browser":
4811
+ return `Browser: ${String(input2.action ?? "")} ${String(input2.url ?? "").slice(0, 50)}`;
4812
+ case "weather":
4813
+ return `Weather: ${String(input2.location ?? "")}`;
4814
+ case "note":
4815
+ return `Note: ${String(input2.action ?? "")}`;
4051
4816
  default:
4052
4817
  return `Using ${toolName}...`;
4053
4818
  }
@@ -4089,6 +4854,135 @@ var init_message_pipeline = __esm({
4089
4854
  keptMessages.push(latestMsg);
4090
4855
  return keptMessages;
4091
4856
  }
4857
+ /**
4858
+ * Build the user content for the LLM request.
4859
+ * Handles images (as vision blocks), audio (transcribed via Whisper),
4860
+ * documents/files (saved to inbox), and plain text.
4861
+ */
4862
+ async buildUserContent(message, onProgress) {
4863
+ const attachments = message.attachments?.filter((a) => a.data) ?? [];
4864
+ if (attachments.length === 0) {
4865
+ return message.text;
4866
+ }
4867
+ const blocks = [];
4868
+ for (const attachment of attachments) {
4869
+ if (attachment.type === "image" && attachment.data) {
4870
+ blocks.push({
4871
+ type: "image",
4872
+ source: {
4873
+ type: "base64",
4874
+ media_type: attachment.mimeType ?? "image/jpeg",
4875
+ data: attachment.data.toString("base64")
4876
+ }
4877
+ });
4878
+ this.logger.info({ mimeType: attachment.mimeType, size: attachment.size }, "Image attached to LLM request");
4879
+ } else if (attachment.type === "audio" && attachment.data) {
4880
+ if (this.speechTranscriber) {
4881
+ onProgress?.("Transcribing voice...");
4882
+ try {
4883
+ const transcript = await this.speechTranscriber.transcribe(attachment.data, attachment.mimeType ?? "audio/ogg");
4884
+ const label = message.text === "[Voice message]" ? "" : `${message.text}
4885
+
4886
+ `;
4887
+ blocks.push({
4888
+ type: "text",
4889
+ text: `${label}[Voice transcript]: ${transcript}`
4890
+ });
4891
+ this.logger.info({ transcriptLength: transcript.length }, "Voice message transcribed");
4892
+ return blocks.length === 1 ? blocks[0].type === "text" ? blocks[0].text : blocks : blocks;
4893
+ } catch (err) {
4894
+ this.logger.error({ err }, "Voice transcription failed");
4895
+ blocks.push({
4896
+ type: "text",
4897
+ text: "[Voice message could not be transcribed]"
4898
+ });
4899
+ }
4900
+ } else {
4901
+ blocks.push({
4902
+ type: "text",
4903
+ text: "[Voice message received but speech-to-text is not configured. Add speech config to enable transcription.]"
4904
+ });
4905
+ }
4906
+ } else if ((attachment.type === "document" || attachment.type === "video" || attachment.type === "other") && attachment.data) {
4907
+ const savedPath = this.saveToInbox(attachment);
4908
+ if (savedPath) {
4909
+ const isTextFile = this.isTextMimeType(attachment.mimeType);
4910
+ let fileNote = `[File received: "${attachment.fileName ?? "unknown"}" (${this.formatBytes(attachment.data.length)}, ${attachment.mimeType ?? "unknown type"})]
4911
+ [Saved to: ${savedPath}]`;
4912
+ if (isTextFile && attachment.data.length <= MAX_INLINE_FILE_SIZE) {
4913
+ const textContent = attachment.data.toString("utf-8");
4914
+ fileNote += `
4915
+ [File content]:
4916
+ ${textContent}`;
4917
+ }
4918
+ blocks.push({ type: "text", text: fileNote });
4919
+ this.logger.info({ fileName: attachment.fileName, savedPath, size: attachment.data.length }, "File saved to inbox");
4920
+ }
4921
+ }
4922
+ }
4923
+ const skipTexts = ["[Photo]", "[Voice message]", "[Video]", "[Video note]", "[Document]", "[File]"];
4924
+ if (message.text && !skipTexts.includes(message.text)) {
4925
+ blocks.push({ type: "text", text: message.text });
4926
+ } else if (blocks.some((b) => b.type === "image") && !blocks.some((b) => b.type === "text")) {
4927
+ blocks.push({ type: "text", text: "What do you see in this image?" });
4928
+ } else if (blocks.length === 0) {
4929
+ blocks.push({ type: "text", text: message.text || "(empty message)" });
4930
+ }
4931
+ return blocks;
4932
+ }
4933
+ /**
4934
+ * Save an attachment to the inbox directory.
4935
+ * Returns the saved file path, or undefined on failure.
4936
+ */
4937
+ saveToInbox(attachment) {
4938
+ if (!attachment.data)
4939
+ return void 0;
4940
+ const inboxDir = this.inboxPath ?? path7.resolve("./data/inbox");
4941
+ try {
4942
+ fs5.mkdirSync(inboxDir, { recursive: true });
4943
+ } catch {
4944
+ this.logger.error({ inboxDir }, "Cannot create inbox directory");
4945
+ return void 0;
4946
+ }
4947
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4948
+ const originalName = attachment.fileName ?? `file_${timestamp}`;
4949
+ const safeName = originalName.replace(/[<>:"/\\|?*]/g, "_");
4950
+ const fileName = `${timestamp}_${safeName}`;
4951
+ const filePath = path7.join(inboxDir, fileName);
4952
+ try {
4953
+ fs5.writeFileSync(filePath, attachment.data);
4954
+ return filePath;
4955
+ } catch (err) {
4956
+ this.logger.error({ err, filePath }, "Failed to save file to inbox");
4957
+ return void 0;
4958
+ }
4959
+ }
4960
+ isTextMimeType(mimeType) {
4961
+ if (!mimeType)
4962
+ return false;
4963
+ const textTypes = [
4964
+ "text/",
4965
+ "application/json",
4966
+ "application/xml",
4967
+ "application/javascript",
4968
+ "application/typescript",
4969
+ "application/x-yaml",
4970
+ "application/yaml",
4971
+ "application/toml",
4972
+ "application/x-sh",
4973
+ "application/sql",
4974
+ "application/csv",
4975
+ "application/x-csv"
4976
+ ];
4977
+ return textTypes.some((t) => mimeType.startsWith(t));
4978
+ }
4979
+ formatBytes(bytes) {
4980
+ if (bytes < 1024)
4981
+ return `${bytes} B`;
4982
+ if (bytes < 1024 * 1024)
4983
+ return `${(bytes / 1024).toFixed(1)} KB`;
4984
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4985
+ }
4092
4986
  };
4093
4987
  }
4094
4988
  });
@@ -4142,6 +5036,64 @@ var init_reminder_scheduler = __esm({
4142
5036
  }
4143
5037
  });
4144
5038
 
5039
+ // ../core/dist/speech-transcriber.js
5040
+ var SpeechTranscriber;
5041
+ var init_speech_transcriber = __esm({
5042
+ "../core/dist/speech-transcriber.js"() {
5043
+ "use strict";
5044
+ SpeechTranscriber = class {
5045
+ logger;
5046
+ apiKey;
5047
+ baseUrl;
5048
+ constructor(config, logger) {
5049
+ this.logger = logger;
5050
+ this.apiKey = config.apiKey;
5051
+ if (config.provider === "groq") {
5052
+ this.baseUrl = config.baseUrl ?? "https://api.groq.com/openai/v1";
5053
+ } else {
5054
+ this.baseUrl = config.baseUrl ?? "https://api.openai.com/v1";
5055
+ }
5056
+ }
5057
+ async transcribe(audioBuffer, mimeType) {
5058
+ const ext = this.mimeToExtension(mimeType);
5059
+ const formData = new FormData();
5060
+ formData.append("file", new Blob([audioBuffer], { type: mimeType }), `audio.${ext}`);
5061
+ formData.append("model", "whisper-1");
5062
+ try {
5063
+ const response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
5064
+ method: "POST",
5065
+ headers: {
5066
+ "Authorization": `Bearer ${this.apiKey}`
5067
+ },
5068
+ body: formData
5069
+ });
5070
+ if (!response.ok) {
5071
+ const errorText = await response.text();
5072
+ throw new Error(`Whisper API ${response.status}: ${errorText}`);
5073
+ }
5074
+ const data = await response.json();
5075
+ this.logger.info({ textLength: data.text.length }, "Voice transcribed");
5076
+ return data.text;
5077
+ } catch (err) {
5078
+ this.logger.error({ err }, "Voice transcription failed");
5079
+ throw err;
5080
+ }
5081
+ }
5082
+ mimeToExtension(mimeType) {
5083
+ const map = {
5084
+ "audio/ogg": "ogg",
5085
+ "audio/mpeg": "mp3",
5086
+ "audio/mp4": "m4a",
5087
+ "audio/wav": "wav",
5088
+ "audio/webm": "webm",
5089
+ "audio/x-m4a": "m4a"
5090
+ };
5091
+ return map[mimeType] ?? "ogg";
5092
+ }
5093
+ };
5094
+ }
5095
+ });
5096
+
4145
5097
  // ../messaging/dist/adapter.js
4146
5098
  import { EventEmitter } from "node:events";
4147
5099
  var MessagingAdapter;
@@ -4150,6 +5102,12 @@ var init_adapter = __esm({
4150
5102
  "use strict";
4151
5103
  MessagingAdapter = class extends EventEmitter {
4152
5104
  status = "disconnected";
5105
+ async sendPhoto(_chatId, _photo, _caption) {
5106
+ return void 0;
5107
+ }
5108
+ async sendFile(_chatId, _file, _fileName, _caption) {
5109
+ return void 0;
5110
+ }
4153
5111
  getStatus() {
4154
5112
  return this.status;
4155
5113
  }
@@ -4158,7 +5116,7 @@ var init_adapter = __esm({
4158
5116
  });
4159
5117
 
4160
5118
  // ../messaging/dist/adapters/telegram.js
4161
- import { Bot } from "grammy";
5119
+ import { Bot, InputFile } from "grammy";
4162
5120
  function mapParseMode(mode) {
4163
5121
  if (mode === "markdown")
4164
5122
  return "MarkdownV2";
@@ -4181,21 +5139,65 @@ var init_telegram = __esm({
4181
5139
  async connect() {
4182
5140
  this.status = "connecting";
4183
5141
  this.bot.on("message:text", (ctx) => {
5142
+ this.emit("message", this.normalizeMessage(ctx.message, ctx.message.text));
5143
+ });
5144
+ this.bot.on("message:photo", async (ctx) => {
4184
5145
  const msg = ctx.message;
4185
- const normalized = {
4186
- id: String(msg.message_id),
4187
- platform: "telegram",
4188
- chatId: String(msg.chat.id),
4189
- chatType: msg.chat.type === "private" ? "dm" : "group",
4190
- userId: String(msg.from.id),
4191
- userName: msg.from.username ?? String(msg.from.id),
4192
- displayName: [msg.from.first_name, msg.from.last_name].filter(Boolean).join(" "),
4193
- text: msg.text,
4194
- timestamp: new Date(msg.date * 1e3),
4195
- replyToMessageId: msg.reply_to_message ? String(msg.reply_to_message.message_id) : void 0
4196
- };
5146
+ const caption = msg.caption ?? "";
5147
+ const text = caption || "[Photo]";
5148
+ const photo = msg.photo[msg.photo.length - 1];
5149
+ const attachment = await this.downloadAttachment(photo.file_id, "image", "image/jpeg");
5150
+ const normalized = this.normalizeMessage(msg, text);
5151
+ normalized.attachments = attachment ? [attachment] : void 0;
5152
+ this.emit("message", normalized);
5153
+ });
5154
+ this.bot.on("message:voice", async (ctx) => {
5155
+ const msg = ctx.message;
5156
+ const attachment = await this.downloadAttachment(msg.voice.file_id, "audio", msg.voice.mime_type ?? "audio/ogg");
5157
+ const normalized = this.normalizeMessage(msg, "[Voice message]");
5158
+ normalized.attachments = attachment ? [attachment] : void 0;
5159
+ this.emit("message", normalized);
5160
+ });
5161
+ this.bot.on("message:audio", async (ctx) => {
5162
+ const msg = ctx.message;
5163
+ const caption = msg.caption ?? "";
5164
+ const text = caption || `[Audio: ${msg.audio.file_name ?? "audio"}]`;
5165
+ const attachment = await this.downloadAttachment(msg.audio.file_id, "audio", msg.audio.mime_type ?? "audio/mpeg");
5166
+ const normalized = this.normalizeMessage(msg, text);
5167
+ normalized.attachments = attachment ? [attachment] : void 0;
5168
+ this.emit("message", normalized);
5169
+ });
5170
+ this.bot.on("message:video", async (ctx) => {
5171
+ const msg = ctx.message;
5172
+ const caption = msg.caption ?? "";
5173
+ const text = caption || "[Video]";
5174
+ const attachment = await this.downloadAttachment(msg.video.file_id, "video", msg.video.mime_type ?? "video/mp4");
5175
+ const normalized = this.normalizeMessage(msg, text);
5176
+ normalized.attachments = attachment ? [attachment] : void 0;
5177
+ this.emit("message", normalized);
5178
+ });
5179
+ this.bot.on("message:document", async (ctx) => {
5180
+ const msg = ctx.message;
5181
+ const doc = msg.document;
5182
+ const caption = msg.caption ?? "";
5183
+ const text = caption || `[Document: ${doc.file_name ?? "file"}]`;
5184
+ const attachment = await this.downloadAttachment(doc.file_id, "document", doc.mime_type ?? "application/octet-stream", doc.file_name);
5185
+ const normalized = this.normalizeMessage(msg, text);
5186
+ normalized.attachments = attachment ? [attachment] : void 0;
5187
+ this.emit("message", normalized);
5188
+ });
5189
+ this.bot.on("message:video_note", async (ctx) => {
5190
+ const msg = ctx.message;
5191
+ const attachment = await this.downloadAttachment(msg.video_note.file_id, "video", "video/mp4");
5192
+ const normalized = this.normalizeMessage(msg, "[Video note]");
5193
+ normalized.attachments = attachment ? [attachment] : void 0;
4197
5194
  this.emit("message", normalized);
4198
5195
  });
5196
+ this.bot.on("message:sticker", (ctx) => {
5197
+ const msg = ctx.message;
5198
+ const emoji = msg.sticker.emoji ?? "\u{1F3F7}\uFE0F";
5199
+ this.emit("message", this.normalizeMessage(msg, `[Sticker: ${emoji}]`));
5200
+ });
4199
5201
  this.bot.catch((err) => {
4200
5202
  this.emit("error", err.error);
4201
5203
  });
@@ -4224,6 +5226,50 @@ var init_telegram = __esm({
4224
5226
  async deleteMessage(chatId, messageId) {
4225
5227
  await this.bot.api.deleteMessage(Number(chatId), Number(messageId));
4226
5228
  }
5229
+ async sendPhoto(chatId, photo, caption) {
5230
+ const result = await this.bot.api.sendPhoto(Number(chatId), new InputFile(photo, "image.png"), { caption });
5231
+ return String(result.message_id);
5232
+ }
5233
+ async sendFile(chatId, file, fileName, caption) {
5234
+ const result = await this.bot.api.sendDocument(Number(chatId), new InputFile(file, fileName), { caption });
5235
+ return String(result.message_id);
5236
+ }
5237
+ normalizeMessage(msg, text) {
5238
+ return {
5239
+ id: String(msg.message_id),
5240
+ platform: "telegram",
5241
+ chatId: String(msg.chat.id),
5242
+ chatType: msg.chat.type === "private" ? "dm" : "group",
5243
+ userId: String(msg.from.id),
5244
+ userName: msg.from.username ?? String(msg.from.id),
5245
+ displayName: [msg.from.first_name, msg.from.last_name].filter(Boolean).join(" "),
5246
+ text,
5247
+ timestamp: new Date(msg.date * 1e3),
5248
+ replyToMessageId: msg.reply_to_message ? String(msg.reply_to_message.message_id) : void 0
5249
+ };
5250
+ }
5251
+ async downloadAttachment(fileId, type, mimeType, fileName) {
5252
+ try {
5253
+ const file = await this.bot.api.getFile(fileId);
5254
+ const filePath = file.file_path;
5255
+ if (!filePath)
5256
+ return void 0;
5257
+ const url = `https://api.telegram.org/file/bot${this.bot.token}/${filePath}`;
5258
+ const response = await fetch(url);
5259
+ if (!response.ok)
5260
+ return void 0;
5261
+ const buffer = Buffer.from(await response.arrayBuffer());
5262
+ return {
5263
+ type,
5264
+ mimeType,
5265
+ fileName: fileName ?? filePath.split("/").pop(),
5266
+ size: buffer.length,
5267
+ data: buffer
5268
+ };
5269
+ } catch {
5270
+ return void 0;
5271
+ }
5272
+ }
4227
5273
  };
4228
5274
  }
4229
5275
  });
@@ -4253,22 +5299,29 @@ var init_discord = __esm({
4253
5299
  GatewayIntentBits.DirectMessages
4254
5300
  ]
4255
5301
  });
4256
- this.client.on(Events.MessageCreate, (message) => {
5302
+ this.client.on(Events.MessageCreate, async (message) => {
4257
5303
  if (message.author.bot)
4258
5304
  return;
4259
- const normalized = {
4260
- id: message.id,
4261
- platform: "discord",
4262
- chatId: message.channelId,
4263
- chatType: message.channel.isDMBased() ? "dm" : "group",
4264
- userId: message.author.id,
4265
- userName: message.author.username,
4266
- displayName: message.author.displayName,
4267
- text: message.content,
4268
- timestamp: message.createdAt,
4269
- replyToMessageId: message.reference?.messageId ?? void 0
4270
- };
4271
- this.emit("message", normalized);
5305
+ try {
5306
+ const attachments = await this.downloadAttachments(message);
5307
+ const text = message.content || this.inferTextFromAttachments(attachments);
5308
+ const normalized = {
5309
+ id: message.id,
5310
+ platform: "discord",
5311
+ chatId: message.channelId,
5312
+ chatType: message.channel.isDMBased() ? "dm" : "group",
5313
+ userId: message.author.id,
5314
+ userName: message.author.username,
5315
+ displayName: message.author.displayName,
5316
+ text,
5317
+ timestamp: message.createdAt,
5318
+ replyToMessageId: message.reference?.messageId ?? void 0,
5319
+ attachments: attachments.length > 0 ? attachments : void 0
5320
+ };
5321
+ this.emit("message", normalized);
5322
+ } catch (err) {
5323
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
5324
+ }
4272
5325
  });
4273
5326
  this.client.on(Events.ClientReady, () => {
4274
5327
  this.status = "connected";
@@ -4320,6 +5373,82 @@ var init_discord = __esm({
4320
5373
  const message = await channel.messages.fetch(messageId);
4321
5374
  await message.delete();
4322
5375
  }
5376
+ async sendPhoto(chatId, photo, caption) {
5377
+ if (!this.client)
5378
+ return void 0;
5379
+ const channel = await this.client.channels.fetch(chatId);
5380
+ if (!channel?.isTextBased() || !("send" in channel))
5381
+ return void 0;
5382
+ const msg = await channel.send({
5383
+ content: caption,
5384
+ files: [{ attachment: photo, name: "image.png" }]
5385
+ });
5386
+ return msg.id;
5387
+ }
5388
+ async sendFile(chatId, file, fileName, caption) {
5389
+ if (!this.client)
5390
+ return void 0;
5391
+ const channel = await this.client.channels.fetch(chatId);
5392
+ if (!channel?.isTextBased() || !("send" in channel))
5393
+ return void 0;
5394
+ const msg = await channel.send({
5395
+ content: caption,
5396
+ files: [{ attachment: file, name: fileName }]
5397
+ });
5398
+ return msg.id;
5399
+ }
5400
+ // ── Private helpers ──────────────────────────────────────────────
5401
+ async downloadAttachments(message) {
5402
+ const result = [];
5403
+ const discordAttachments = message.attachments;
5404
+ if (!discordAttachments || discordAttachments.size === 0)
5405
+ return result;
5406
+ for (const [, att] of discordAttachments) {
5407
+ try {
5408
+ const res = await fetch(att.url);
5409
+ if (!res.ok)
5410
+ continue;
5411
+ const arrayBuffer = await res.arrayBuffer();
5412
+ const data = Buffer.from(arrayBuffer);
5413
+ const type = this.classifyContentType(att.contentType);
5414
+ result.push({
5415
+ type,
5416
+ url: att.url,
5417
+ mimeType: att.contentType ?? void 0,
5418
+ fileName: att.name ?? void 0,
5419
+ size: att.size ?? data.length,
5420
+ data
5421
+ });
5422
+ } catch {
5423
+ }
5424
+ }
5425
+ return result;
5426
+ }
5427
+ classifyContentType(contentType) {
5428
+ if (!contentType)
5429
+ return "other";
5430
+ if (contentType.startsWith("image/"))
5431
+ return "image";
5432
+ if (contentType.startsWith("audio/"))
5433
+ return "audio";
5434
+ if (contentType.startsWith("video/"))
5435
+ return "video";
5436
+ return "document";
5437
+ }
5438
+ inferTextFromAttachments(attachments) {
5439
+ if (attachments.length === 0)
5440
+ return "";
5441
+ const types = attachments.map((a) => a.type);
5442
+ if (types.includes("image"))
5443
+ return "[Photo]";
5444
+ if (types.includes("audio"))
5445
+ return "[Voice message]";
5446
+ if (types.includes("video"))
5447
+ return "[Video]";
5448
+ if (types.includes("document"))
5449
+ return "[Document]";
5450
+ return "[File]";
5451
+ }
4323
5452
  };
4324
5453
  }
4325
5454
  });
@@ -4338,7 +5467,7 @@ var init_matrix = __esm({
4338
5467
  botUserId;
4339
5468
  constructor(homeserverUrl, accessToken, botUserId) {
4340
5469
  super();
4341
- this.homeserverUrl = homeserverUrl;
5470
+ this.homeserverUrl = homeserverUrl.replace(/\/+$/, "");
4342
5471
  this.accessToken = accessToken;
4343
5472
  this.botUserId = botUserId;
4344
5473
  }
@@ -4348,23 +5477,20 @@ var init_matrix = __esm({
4348
5477
  const storageProvider = new SimpleFsStorageProvider("./data/matrix-storage");
4349
5478
  this.client = new MatrixClient(this.homeserverUrl, this.accessToken, storageProvider);
4350
5479
  AutojoinRoomsMixin.setupOnClient(this.client);
4351
- this.client.on("room.message", (roomId, event) => {
5480
+ this.client.on("room.message", async (roomId, event) => {
4352
5481
  if (event.sender === this.botUserId)
4353
5482
  return;
4354
- if (event.content?.msgtype !== "m.text")
5483
+ const msgtype = event.content?.msgtype;
5484
+ if (!msgtype)
4355
5485
  return;
4356
- const normalized = {
4357
- id: event.event_id,
4358
- platform: "matrix",
4359
- chatId: roomId,
4360
- chatType: "group",
4361
- userId: event.sender,
4362
- userName: event.sender.split(":")[0].slice(1),
4363
- text: event.content.body,
4364
- timestamp: new Date(event.origin_server_ts),
4365
- replyToMessageId: event.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
4366
- };
4367
- this.emit("message", normalized);
5486
+ try {
5487
+ const message = await this.normalizeEvent(roomId, event, msgtype);
5488
+ if (message) {
5489
+ this.emit("message", message);
5490
+ }
5491
+ } catch (err) {
5492
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
5493
+ }
4368
5494
  });
4369
5495
  await this.client.start();
4370
5496
  this.status = "connected";
@@ -4396,6 +5522,144 @@ var init_matrix = __esm({
4396
5522
  async deleteMessage(chatId, messageId) {
4397
5523
  await this.client.redactEvent(chatId, messageId);
4398
5524
  }
5525
+ async sendPhoto(chatId, photo, caption) {
5526
+ const mxcUrl = await this.client.uploadContent(photo, "image/png", "image.png");
5527
+ const content = {
5528
+ msgtype: "m.image",
5529
+ body: caption ?? "image.png",
5530
+ url: mxcUrl,
5531
+ info: {
5532
+ mimetype: "image/png",
5533
+ size: photo.length
5534
+ }
5535
+ };
5536
+ const eventId = await this.client.sendEvent(chatId, "m.room.message", content);
5537
+ return eventId;
5538
+ }
5539
+ async sendFile(chatId, file, fileName, caption) {
5540
+ const mimeType = this.guessMimeType(fileName);
5541
+ const mxcUrl = await this.client.uploadContent(file, mimeType, fileName);
5542
+ const content = {
5543
+ msgtype: "m.file",
5544
+ body: caption ?? fileName,
5545
+ filename: fileName,
5546
+ url: mxcUrl,
5547
+ info: {
5548
+ mimetype: mimeType,
5549
+ size: file.length
5550
+ }
5551
+ };
5552
+ const eventId = await this.client.sendEvent(chatId, "m.room.message", content);
5553
+ return eventId;
5554
+ }
5555
+ // ── Private helpers ──────────────────────────────────────────────
5556
+ async normalizeEvent(roomId, event, msgtype) {
5557
+ const base = {
5558
+ id: event.event_id,
5559
+ platform: "matrix",
5560
+ chatId: roomId,
5561
+ chatType: "group",
5562
+ userId: event.sender,
5563
+ userName: event.sender.split(":")[0].slice(1),
5564
+ timestamp: new Date(event.origin_server_ts),
5565
+ replyToMessageId: event.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
5566
+ };
5567
+ switch (msgtype) {
5568
+ case "m.text":
5569
+ return { ...base, text: event.content.body };
5570
+ case "m.image": {
5571
+ const attachment = await this.downloadAttachment(event.content, "image");
5572
+ return {
5573
+ ...base,
5574
+ text: event.content.body ?? "[Photo]",
5575
+ attachments: attachment ? [attachment] : void 0
5576
+ };
5577
+ }
5578
+ case "m.audio": {
5579
+ const attachment = await this.downloadAttachment(event.content, "audio");
5580
+ return {
5581
+ ...base,
5582
+ text: event.content.body ?? "[Voice message]",
5583
+ attachments: attachment ? [attachment] : void 0
5584
+ };
5585
+ }
5586
+ case "m.video": {
5587
+ const attachment = await this.downloadAttachment(event.content, "video");
5588
+ return {
5589
+ ...base,
5590
+ text: event.content.body ?? "[Video]",
5591
+ attachments: attachment ? [attachment] : void 0
5592
+ };
5593
+ }
5594
+ case "m.file": {
5595
+ const attachment = await this.downloadAttachment(event.content, "document");
5596
+ return {
5597
+ ...base,
5598
+ text: event.content.body ?? "[Document]",
5599
+ attachments: attachment ? [attachment] : void 0
5600
+ };
5601
+ }
5602
+ default:
5603
+ if (event.content.body) {
5604
+ return { ...base, text: event.content.body };
5605
+ }
5606
+ return void 0;
5607
+ }
5608
+ }
5609
+ /**
5610
+ * Download a Matrix media file from an mxc:// URL.
5611
+ * Uses the /_matrix/media/v3/download endpoint.
5612
+ */
5613
+ async downloadAttachment(content, type) {
5614
+ const mxcUrl = content.url;
5615
+ if (!mxcUrl || !mxcUrl.startsWith("mxc://"))
5616
+ return void 0;
5617
+ const info = content.info ?? {};
5618
+ const mimeType = info.mimetype;
5619
+ const size = info.size;
5620
+ const fileName = content.filename ?? content.body ?? "file";
5621
+ try {
5622
+ const mxcParts = mxcUrl.slice(6);
5623
+ const downloadUrl = `${this.homeserverUrl}/_matrix/media/v3/download/${mxcParts}`;
5624
+ const res = await fetch(downloadUrl, {
5625
+ headers: { Authorization: `Bearer ${this.accessToken}` }
5626
+ });
5627
+ if (!res.ok)
5628
+ return void 0;
5629
+ const arrayBuffer = await res.arrayBuffer();
5630
+ const data = Buffer.from(arrayBuffer);
5631
+ return {
5632
+ type,
5633
+ mimeType,
5634
+ fileName,
5635
+ size: size ?? data.length,
5636
+ data
5637
+ };
5638
+ } catch {
5639
+ return void 0;
5640
+ }
5641
+ }
5642
+ guessMimeType(fileName) {
5643
+ const ext = fileName.split(".").pop()?.toLowerCase();
5644
+ const mimeMap = {
5645
+ pdf: "application/pdf",
5646
+ txt: "text/plain",
5647
+ json: "application/json",
5648
+ csv: "text/csv",
5649
+ png: "image/png",
5650
+ jpg: "image/jpeg",
5651
+ jpeg: "image/jpeg",
5652
+ gif: "image/gif",
5653
+ mp3: "audio/mpeg",
5654
+ ogg: "audio/ogg",
5655
+ mp4: "video/mp4",
5656
+ zip: "application/zip",
5657
+ doc: "application/msword",
5658
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5659
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
5660
+ };
5661
+ return mimeMap[ext ?? ""] ?? "application/octet-stream";
5662
+ }
4399
5663
  };
4400
5664
  }
4401
5665
  });
@@ -4409,6 +5673,7 @@ var init_whatsapp = __esm({
4409
5673
  WhatsAppAdapter = class extends MessagingAdapter {
4410
5674
  platform = "whatsapp";
4411
5675
  socket;
5676
+ downloadMedia;
4412
5677
  dataPath;
4413
5678
  constructor(dataPath) {
4414
5679
  super();
@@ -4417,7 +5682,9 @@ var init_whatsapp = __esm({
4417
5682
  async connect() {
4418
5683
  this.status = "connecting";
4419
5684
  const baileys = await import("@whiskeysockets/baileys");
4420
- const { makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys.default ?? baileys;
5685
+ const mod = baileys.default ?? baileys;
5686
+ const { makeWASocket, useMultiFileAuthState, DisconnectReason, downloadMediaMessage } = mod;
5687
+ this.downloadMedia = downloadMediaMessage;
4421
5688
  const { state, saveCreds } = await useMultiFileAuthState(this.dataPath);
4422
5689
  this.socket = makeWASocket({
4423
5690
  auth: state,
@@ -4447,21 +5714,9 @@ var init_whatsapp = __esm({
4447
5714
  continue;
4448
5715
  if (message.key.fromMe)
4449
5716
  continue;
4450
- const text = message.message.conversation ?? message.message.extendedTextMessage?.text;
4451
- if (!text)
4452
- continue;
4453
- const normalized = {
4454
- id: message.key.id ?? "",
4455
- platform: "whatsapp",
4456
- chatId: message.key.remoteJid ?? "",
4457
- chatType: message.key.remoteJid?.endsWith("@g.us") ? "group" : "dm",
4458
- userId: message.key.participant ?? message.key.remoteJid ?? "",
4459
- userName: message.pushName ?? message.key.participant ?? message.key.remoteJid ?? "",
4460
- text,
4461
- timestamp: new Date(message.messageTimestamp * 1e3),
4462
- replyToMessageId: message.message.extendedTextMessage?.contextInfo?.stanzaId ?? void 0
4463
- };
4464
- this.emit("message", normalized);
5717
+ this.processMessage(message).catch((err) => {
5718
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
5719
+ });
4465
5720
  }
4466
5721
  });
4467
5722
  }
@@ -4499,6 +5754,128 @@ var init_whatsapp = __esm({
4499
5754
  }
4500
5755
  });
4501
5756
  }
5757
+ async sendPhoto(chatId, photo, caption) {
5758
+ const msg = await this.socket.sendMessage(chatId, {
5759
+ image: photo,
5760
+ caption
5761
+ });
5762
+ return msg?.key?.id;
5763
+ }
5764
+ async sendFile(chatId, file, fileName, caption) {
5765
+ const msg = await this.socket.sendMessage(chatId, {
5766
+ document: file,
5767
+ fileName,
5768
+ caption,
5769
+ mimetype: this.guessMimeType(fileName)
5770
+ });
5771
+ return msg?.key?.id;
5772
+ }
5773
+ // ── Private helpers ──────────────────────────────────────────────
5774
+ async processMessage(message) {
5775
+ const msg = message.message;
5776
+ const text = msg.conversation ?? msg.extendedTextMessage?.text ?? msg.imageMessage?.caption ?? msg.videoMessage?.caption ?? msg.documentMessage?.caption ?? "";
5777
+ const attachments = [];
5778
+ let fallbackText = text;
5779
+ if (msg.imageMessage) {
5780
+ const data = await this.downloadMediaSafe(message);
5781
+ if (data) {
5782
+ attachments.push({
5783
+ type: "image",
5784
+ mimeType: msg.imageMessage.mimetype ?? "image/jpeg",
5785
+ size: msg.imageMessage.fileLength ?? data.length,
5786
+ data
5787
+ });
5788
+ }
5789
+ if (!fallbackText)
5790
+ fallbackText = "[Photo]";
5791
+ } else if (msg.audioMessage) {
5792
+ const data = await this.downloadMediaSafe(message);
5793
+ if (data) {
5794
+ attachments.push({
5795
+ type: "audio",
5796
+ mimeType: msg.audioMessage.mimetype ?? "audio/ogg",
5797
+ size: msg.audioMessage.fileLength ?? data.length,
5798
+ data
5799
+ });
5800
+ }
5801
+ if (!fallbackText)
5802
+ fallbackText = "[Voice message]";
5803
+ } else if (msg.videoMessage) {
5804
+ const data = await this.downloadMediaSafe(message);
5805
+ if (data) {
5806
+ attachments.push({
5807
+ type: "video",
5808
+ mimeType: msg.videoMessage.mimetype ?? "video/mp4",
5809
+ size: msg.videoMessage.fileLength ?? data.length,
5810
+ data
5811
+ });
5812
+ }
5813
+ if (!fallbackText)
5814
+ fallbackText = "[Video]";
5815
+ } else if (msg.documentMessage) {
5816
+ const data = await this.downloadMediaSafe(message);
5817
+ if (data) {
5818
+ attachments.push({
5819
+ type: "document",
5820
+ mimeType: msg.documentMessage.mimetype ?? "application/octet-stream",
5821
+ fileName: msg.documentMessage.fileName ?? "document",
5822
+ size: msg.documentMessage.fileLength ?? data.length,
5823
+ data
5824
+ });
5825
+ }
5826
+ if (!fallbackText)
5827
+ fallbackText = "[Document]";
5828
+ } else if (msg.stickerMessage) {
5829
+ if (!text)
5830
+ return;
5831
+ }
5832
+ if (!fallbackText && attachments.length === 0)
5833
+ return;
5834
+ const normalized = {
5835
+ id: message.key.id ?? "",
5836
+ platform: "whatsapp",
5837
+ chatId: message.key.remoteJid ?? "",
5838
+ chatType: message.key.remoteJid?.endsWith("@g.us") ? "group" : "dm",
5839
+ userId: message.key.participant ?? message.key.remoteJid ?? "",
5840
+ userName: message.pushName ?? message.key.participant ?? message.key.remoteJid ?? "",
5841
+ text: fallbackText,
5842
+ timestamp: new Date(message.messageTimestamp * 1e3),
5843
+ replyToMessageId: msg.extendedTextMessage?.contextInfo?.stanzaId ?? void 0,
5844
+ attachments: attachments.length > 0 ? attachments : void 0
5845
+ };
5846
+ this.emit("message", normalized);
5847
+ }
5848
+ async downloadMediaSafe(message) {
5849
+ try {
5850
+ if (!this.downloadMedia)
5851
+ return void 0;
5852
+ const buffer = await this.downloadMedia(message, "buffer", {});
5853
+ return Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
5854
+ } catch {
5855
+ return void 0;
5856
+ }
5857
+ }
5858
+ guessMimeType(fileName) {
5859
+ const ext = fileName.split(".").pop()?.toLowerCase();
5860
+ const mimeMap = {
5861
+ pdf: "application/pdf",
5862
+ txt: "text/plain",
5863
+ json: "application/json",
5864
+ csv: "text/csv",
5865
+ png: "image/png",
5866
+ jpg: "image/jpeg",
5867
+ jpeg: "image/jpeg",
5868
+ gif: "image/gif",
5869
+ mp3: "audio/mpeg",
5870
+ ogg: "audio/ogg",
5871
+ mp4: "video/mp4",
5872
+ zip: "application/zip",
5873
+ doc: "application/msword",
5874
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5875
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
5876
+ };
5877
+ return mimeMap[ext ?? ""] ?? "application/octet-stream";
5878
+ }
4502
5879
  };
4503
5880
  }
4504
5881
  });
@@ -4593,10 +5970,24 @@ var init_signal = __esm({
4593
5970
  const messages = await res.json();
4594
5971
  for (const envelope of messages) {
4595
5972
  const dataMessage = envelope.envelope?.dataMessage;
4596
- if (!dataMessage?.message)
5973
+ if (!dataMessage)
5974
+ continue;
5975
+ if (!dataMessage.message && (!dataMessage.attachments || dataMessage.attachments.length === 0))
4597
5976
  continue;
4598
5977
  const data = envelope.envelope;
4599
5978
  const chatId = dataMessage.groupInfo?.groupId ? `group.${dataMessage.groupInfo.groupId}` : data.sourceNumber ?? data.source ?? "";
5979
+ const attachments = [];
5980
+ if (dataMessage.attachments) {
5981
+ for (const att of dataMessage.attachments) {
5982
+ const downloaded = await this.downloadAttachment(att);
5983
+ if (downloaded) {
5984
+ attachments.push(downloaded);
5985
+ }
5986
+ }
5987
+ }
5988
+ const text = dataMessage.message || this.inferTextFromAttachments(attachments) || "";
5989
+ if (!text && attachments.length === 0)
5990
+ continue;
4600
5991
  const normalized = {
4601
5992
  id: String(dataMessage.timestamp ?? Date.now()),
4602
5993
  platform: "signal",
@@ -4605,12 +5996,59 @@ var init_signal = __esm({
4605
5996
  userId: data.sourceNumber ?? data.source ?? "",
4606
5997
  userName: data.sourceName ?? data.sourceNumber ?? data.source ?? "",
4607
5998
  displayName: data.sourceName,
4608
- text: dataMessage.message,
4609
- timestamp: new Date(dataMessage.timestamp ?? Date.now())
5999
+ text,
6000
+ timestamp: new Date(dataMessage.timestamp ?? Date.now()),
6001
+ attachments: attachments.length > 0 ? attachments : void 0
4610
6002
  };
4611
6003
  this.emit("message", normalized);
4612
6004
  }
4613
6005
  }
6006
+ async downloadAttachment(att) {
6007
+ if (!att.id)
6008
+ return void 0;
6009
+ try {
6010
+ const res = await fetch(`${this.apiUrl}/v1/attachments/${att.id}`);
6011
+ if (!res.ok)
6012
+ return void 0;
6013
+ const arrayBuffer = await res.arrayBuffer();
6014
+ const data = Buffer.from(arrayBuffer);
6015
+ const type = this.classifyContentType(att.contentType);
6016
+ return {
6017
+ type,
6018
+ mimeType: att.contentType ?? void 0,
6019
+ fileName: att.filename ?? void 0,
6020
+ size: att.size ?? data.length,
6021
+ data
6022
+ };
6023
+ } catch {
6024
+ return void 0;
6025
+ }
6026
+ }
6027
+ classifyContentType(contentType) {
6028
+ if (!contentType)
6029
+ return "other";
6030
+ if (contentType.startsWith("image/"))
6031
+ return "image";
6032
+ if (contentType.startsWith("audio/"))
6033
+ return "audio";
6034
+ if (contentType.startsWith("video/"))
6035
+ return "video";
6036
+ return "document";
6037
+ }
6038
+ inferTextFromAttachments(attachments) {
6039
+ if (attachments.length === 0)
6040
+ return "";
6041
+ const types = attachments.map((a) => a.type);
6042
+ if (types.includes("image"))
6043
+ return "[Photo]";
6044
+ if (types.includes("audio"))
6045
+ return "[Voice message]";
6046
+ if (types.includes("video"))
6047
+ return "[Video]";
6048
+ if (types.includes("document"))
6049
+ return "[Document]";
6050
+ return "[File]";
6051
+ }
4614
6052
  };
4615
6053
  }
4616
6054
  });
@@ -4638,8 +6076,8 @@ var init_dist7 = __esm({
4638
6076
  });
4639
6077
 
4640
6078
  // ../core/dist/alfred.js
4641
- import fs4 from "node:fs";
4642
- import path4 from "node:path";
6079
+ import fs6 from "node:fs";
6080
+ import path8 from "node:path";
4643
6081
  import yaml2 from "js-yaml";
4644
6082
  var Alfred;
4645
6083
  var init_alfred = __esm({
@@ -4653,6 +6091,7 @@ var init_alfred = __esm({
4653
6091
  init_conversation_manager();
4654
6092
  init_message_pipeline();
4655
6093
  init_reminder_scheduler();
6094
+ init_speech_transcriber();
4656
6095
  Alfred = class {
4657
6096
  config;
4658
6097
  logger;
@@ -4673,6 +6112,7 @@ var init_alfred = __esm({
4673
6112
  const auditRepo = new AuditRepository(db);
4674
6113
  const memoryRepo = new MemoryRepository(db);
4675
6114
  const reminderRepo = new ReminderRepository(db);
6115
+ const noteRepo = new NoteRepository(db);
4676
6116
  this.logger.info("Storage initialized");
4677
6117
  const ruleEngine = new RuleEngine();
4678
6118
  const rules = this.loadSecurityRules();
@@ -4682,6 +6122,7 @@ var init_alfred = __esm({
4682
6122
  const llmProvider = createLLMProvider(this.config.llm);
4683
6123
  await llmProvider.initialize();
4684
6124
  this.logger.info({ provider: this.config.llm.provider, model: this.config.llm.model }, "LLM provider initialized");
6125
+ const skillSandbox = new SkillSandbox(this.logger.child({ component: "sandbox" }));
4685
6126
  const skillRegistry = new SkillRegistry();
4686
6127
  skillRegistry.register(new CalculatorSkill());
4687
6128
  skillRegistry.register(new SystemInfoSkill());
@@ -4691,22 +6132,30 @@ var init_alfred = __esm({
4691
6132
  baseUrl: this.config.search.baseUrl
4692
6133
  } : void 0));
4693
6134
  skillRegistry.register(new ReminderSkill(reminderRepo));
4694
- skillRegistry.register(new NoteSkill());
4695
- skillRegistry.register(new SummarizeSkill());
4696
- skillRegistry.register(new TranslateSkill());
6135
+ skillRegistry.register(new NoteSkill(noteRepo));
4697
6136
  skillRegistry.register(new WeatherSkill());
4698
6137
  skillRegistry.register(new ShellSkill());
4699
6138
  skillRegistry.register(new MemorySkill(memoryRepo));
4700
- skillRegistry.register(new DelegateSkill(llmProvider));
6139
+ skillRegistry.register(new DelegateSkill(llmProvider, skillRegistry, skillSandbox, securityManager));
4701
6140
  skillRegistry.register(new EmailSkill(this.config.email ? {
4702
6141
  imap: this.config.email.imap,
4703
6142
  smtp: this.config.email.smtp,
4704
6143
  auth: this.config.email.auth
4705
6144
  } : void 0));
6145
+ skillRegistry.register(new HttpSkill());
6146
+ skillRegistry.register(new FileSkill());
6147
+ skillRegistry.register(new ClipboardSkill());
6148
+ skillRegistry.register(new ScreenshotSkill());
6149
+ skillRegistry.register(new BrowserSkill());
4706
6150
  this.logger.info({ skills: skillRegistry.getAll().map((s) => s.metadata.name) }, "Skills registered");
4707
- const skillSandbox = new SkillSandbox(this.logger.child({ component: "sandbox" }));
6151
+ let speechTranscriber;
6152
+ if (this.config.speech?.apiKey) {
6153
+ speechTranscriber = new SpeechTranscriber(this.config.speech, this.logger.child({ component: "speech" }));
6154
+ this.logger.info({ provider: this.config.speech.provider }, "Speech-to-text initialized");
6155
+ }
4708
6156
  const conversationManager = new ConversationManager(conversationRepo);
4709
- this.pipeline = new MessagePipeline(llmProvider, conversationManager, userRepo, this.logger.child({ component: "pipeline" }), skillRegistry, skillSandbox, securityManager, memoryRepo);
6157
+ const inboxPath = path8.resolve(path8.dirname(this.config.storage.path), "inbox");
6158
+ this.pipeline = new MessagePipeline(llmProvider, conversationManager, userRepo, this.logger.child({ component: "pipeline" }), skillRegistry, skillSandbox, securityManager, memoryRepo, speechTranscriber, inboxPath);
4710
6159
  this.reminderScheduler = new ReminderScheduler(reminderRepo, async (platform, chatId, text) => {
4711
6160
  const adapter = this.adapters.get(platform);
4712
6161
  if (adapter) {
@@ -4821,22 +6270,22 @@ var init_alfred = __esm({
4821
6270
  });
4822
6271
  }
4823
6272
  loadSecurityRules() {
4824
- const rulesPath = path4.resolve(this.config.security.rulesPath);
6273
+ const rulesPath = path8.resolve(this.config.security.rulesPath);
4825
6274
  const rules = [];
4826
- if (!fs4.existsSync(rulesPath)) {
6275
+ if (!fs6.existsSync(rulesPath)) {
4827
6276
  this.logger.warn({ rulesPath }, "Security rules directory not found, using default deny");
4828
6277
  return rules;
4829
6278
  }
4830
- const stat = fs4.statSync(rulesPath);
6279
+ const stat = fs6.statSync(rulesPath);
4831
6280
  if (!stat.isDirectory()) {
4832
6281
  this.logger.warn({ rulesPath }, "Security rules path is not a directory");
4833
6282
  return rules;
4834
6283
  }
4835
- const files = fs4.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
6284
+ const files = fs6.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
4836
6285
  for (const file of files) {
4837
6286
  try {
4838
- const filePath = path4.join(rulesPath, file);
4839
- const content = fs4.readFileSync(filePath, "utf-8");
6287
+ const filePath = path8.join(rulesPath, file);
6288
+ const content = fs6.readFileSync(filePath, "utf-8");
4840
6289
  const parsed = yaml2.load(content);
4841
6290
  if (parsed?.rules && Array.isArray(parsed.rules)) {
4842
6291
  rules.push(...parsed.rules);
@@ -4860,6 +6309,7 @@ var init_dist8 = __esm({
4860
6309
  init_message_pipeline();
4861
6310
  init_conversation_manager();
4862
6311
  init_reminder_scheduler();
6312
+ init_speech_transcriber();
4863
6313
  }
4864
6314
  });
4865
6315
 
@@ -4930,8 +6380,8 @@ __export(setup_exports, {
4930
6380
  });
4931
6381
  import { createInterface } from "node:readline/promises";
4932
6382
  import { stdin as input, stdout as output } from "node:process";
4933
- import fs5 from "node:fs";
4934
- import path5 from "node:path";
6383
+ import fs7 from "node:fs";
6384
+ import path9 from "node:path";
4935
6385
  import yaml3 from "js-yaml";
4936
6386
  function green(s) {
4937
6387
  return `${GREEN}${s}${RESET}`;
@@ -4962,20 +6412,20 @@ function loadExistingConfig(projectRoot) {
4962
6412
  let shellEnabled = false;
4963
6413
  let writeInGroups = false;
4964
6414
  let rateLimit = 30;
4965
- const configPath = path5.join(projectRoot, "config", "default.yml");
4966
- if (fs5.existsSync(configPath)) {
6415
+ const configPath = path9.join(projectRoot, "config", "default.yml");
6416
+ if (fs7.existsSync(configPath)) {
4967
6417
  try {
4968
- const parsed = yaml3.load(fs5.readFileSync(configPath, "utf-8"));
6418
+ const parsed = yaml3.load(fs7.readFileSync(configPath, "utf-8"));
4969
6419
  if (parsed && typeof parsed === "object") {
4970
6420
  Object.assign(config, parsed);
4971
6421
  }
4972
6422
  } catch {
4973
6423
  }
4974
6424
  }
4975
- const envPath = path5.join(projectRoot, ".env");
4976
- if (fs5.existsSync(envPath)) {
6425
+ const envPath = path9.join(projectRoot, ".env");
6426
+ if (fs7.existsSync(envPath)) {
4977
6427
  try {
4978
- const lines = fs5.readFileSync(envPath, "utf-8").split("\n");
6428
+ const lines = fs7.readFileSync(envPath, "utf-8").split("\n");
4979
6429
  for (const line of lines) {
4980
6430
  const trimmed = line.trim();
4981
6431
  if (!trimmed || trimmed.startsWith("#"))
@@ -4988,10 +6438,10 @@ function loadExistingConfig(projectRoot) {
4988
6438
  } catch {
4989
6439
  }
4990
6440
  }
4991
- const rulesPath = path5.join(projectRoot, "config", "rules", "default-rules.yml");
4992
- if (fs5.existsSync(rulesPath)) {
6441
+ const rulesPath = path9.join(projectRoot, "config", "rules", "default-rules.yml");
6442
+ if (fs7.existsSync(rulesPath)) {
4993
6443
  try {
4994
- const rulesContent = yaml3.load(fs5.readFileSync(rulesPath, "utf-8"));
6444
+ const rulesContent = yaml3.load(fs7.readFileSync(rulesPath, "utf-8"));
4995
6445
  if (rulesContent?.rules) {
4996
6446
  shellEnabled = rulesContent.rules.some((r) => r.id === "allow-owner-admin" && r.effect === "allow");
4997
6447
  const writeDmRule = rulesContent.rules.find((r) => r.id === "allow-write-for-dm" || r.id === "allow-write-all");
@@ -5247,6 +6697,54 @@ ${bold("Email access (read & send emails via IMAP/SMTP)?")}`);
5247
6697
  } else {
5248
6698
  console.log(` ${dim("Email disabled \u2014 you can configure it later.")}`);
5249
6699
  }
6700
+ const speechProviders = ["openai", "groq"];
6701
+ const existingSpeechProvider = existing.config.speech?.provider ?? existing.env["ALFRED_SPEECH_PROVIDER"] ?? "";
6702
+ const existingSpeechIdx = speechProviders.indexOf(existingSpeechProvider);
6703
+ const defaultSpeechChoice = existingSpeechIdx >= 0 ? existingSpeechIdx + 1 : 0;
6704
+ console.log(`
6705
+ ${bold("Voice message transcription (Speech-to-Text via Whisper)?")}`);
6706
+ console.log(`${dim("Transcribes voice messages from Telegram, Discord, etc.")}`);
6707
+ const speechLabels = [
6708
+ "OpenAI Whisper \u2014 best quality",
6709
+ "Groq Whisper \u2014 fast & free"
6710
+ ];
6711
+ console.log(` ${cyan("0)")} None (disable voice transcription)${existingSpeechIdx === -1 ? ` ${dim("(current)")}` : ""}`);
6712
+ for (let i = 0; i < speechLabels.length; i++) {
6713
+ const cur = existingSpeechIdx === i ? ` ${dim("(current)")}` : "";
6714
+ console.log(` ${cyan(String(i + 1) + ")")} ${speechLabels[i]}${cur}`);
6715
+ }
6716
+ const speechChoice = await askNumber(rl, "> ", 0, speechProviders.length, defaultSpeechChoice);
6717
+ let speechProvider;
6718
+ let speechApiKey = "";
6719
+ let speechBaseUrl = "";
6720
+ if (speechChoice >= 1 && speechChoice <= speechProviders.length) {
6721
+ speechProvider = speechProviders[speechChoice - 1];
6722
+ }
6723
+ if (speechProvider === "openai") {
6724
+ const existingKey = existing.env["ALFRED_SPEECH_API_KEY"] ?? "";
6725
+ if (existingKey) {
6726
+ speechApiKey = await askWithDefault(rl, " OpenAI API key (for Whisper)", existingKey);
6727
+ } else {
6728
+ console.log(` ${dim("Uses your OpenAI API key for Whisper transcription.")}`);
6729
+ speechApiKey = await askRequired(rl, " OpenAI API key");
6730
+ }
6731
+ console.log(` ${green(">")} OpenAI Whisper: ${dim(maskKey(speechApiKey))}`);
6732
+ } else if (speechProvider === "groq") {
6733
+ const existingKey = existing.env["ALFRED_SPEECH_API_KEY"] ?? "";
6734
+ if (existingKey) {
6735
+ speechApiKey = await askWithDefault(rl, " Groq API key", existingKey);
6736
+ } else {
6737
+ console.log(` ${dim("Get your free API key at: https://console.groq.com/")}`);
6738
+ speechApiKey = await askRequired(rl, " Groq API key");
6739
+ }
6740
+ const existingUrl = existing.env["ALFRED_SPEECH_BASE_URL"] ?? "";
6741
+ if (existingUrl) {
6742
+ speechBaseUrl = await askWithDefault(rl, " Groq API URL", existingUrl);
6743
+ }
6744
+ console.log(` ${green(">")} Groq Whisper: ${dim(maskKey(speechApiKey))}`);
6745
+ } else {
6746
+ console.log(` ${dim("Voice transcription disabled \u2014 you can configure it later.")}`);
6747
+ }
5250
6748
  console.log(`
5251
6749
  ${bold("Security configuration:")}`);
5252
6750
  const existingOwnerId = existing.config.security?.ownerUserId ?? existing.env["ALFRED_OWNER_USER_ID"] ?? "";
@@ -5342,6 +6840,17 @@ ${bold("Writing configuration files...")}`);
5342
6840
  envLines.push("# ALFRED_EMAIL_USER=");
5343
6841
  envLines.push("# ALFRED_EMAIL_PASS=");
5344
6842
  }
6843
+ envLines.push("", "# === Speech-to-Text ===", "");
6844
+ if (speechProvider) {
6845
+ envLines.push(`ALFRED_SPEECH_PROVIDER=${speechProvider}`);
6846
+ envLines.push(`ALFRED_SPEECH_API_KEY=${speechApiKey}`);
6847
+ if (speechBaseUrl) {
6848
+ envLines.push(`ALFRED_SPEECH_BASE_URL=${speechBaseUrl}`);
6849
+ }
6850
+ } else {
6851
+ envLines.push("# ALFRED_SPEECH_PROVIDER=groq");
6852
+ envLines.push("# ALFRED_SPEECH_API_KEY=");
6853
+ }
5345
6854
  envLines.push("", "# === Security ===", "");
5346
6855
  if (ownerUserId) {
5347
6856
  envLines.push(`ALFRED_OWNER_USER_ID=${ownerUserId}`);
@@ -5349,12 +6858,12 @@ ${bold("Writing configuration files...")}`);
5349
6858
  envLines.push("# ALFRED_OWNER_USER_ID=");
5350
6859
  }
5351
6860
  envLines.push("");
5352
- const envPath = path5.join(projectRoot, ".env");
5353
- fs5.writeFileSync(envPath, envLines.join("\n"), "utf-8");
6861
+ const envPath = path9.join(projectRoot, ".env");
6862
+ fs7.writeFileSync(envPath, envLines.join("\n"), "utf-8");
5354
6863
  console.log(` ${green("+")} ${dim(".env")} written`);
5355
- const configDir = path5.join(projectRoot, "config");
5356
- if (!fs5.existsSync(configDir)) {
5357
- fs5.mkdirSync(configDir, { recursive: true });
6864
+ const configDir = path9.join(projectRoot, "config");
6865
+ if (!fs7.existsSync(configDir)) {
6866
+ fs7.mkdirSync(configDir, { recursive: true });
5358
6867
  }
5359
6868
  const config = {
5360
6869
  name: botName,
@@ -5402,6 +6911,13 @@ ${bold("Writing configuration files...")}`);
5402
6911
  auth: { user: emailUser, pass: emailPass }
5403
6912
  }
5404
6913
  } : {},
6914
+ ...speechProvider ? {
6915
+ speech: {
6916
+ provider: speechProvider,
6917
+ apiKey: speechApiKey,
6918
+ ...speechBaseUrl ? { baseUrl: speechBaseUrl } : {}
6919
+ }
6920
+ } : {},
5405
6921
  storage: {
5406
6922
  path: "./data/alfred.db"
5407
6923
  },
@@ -5419,12 +6935,12 @@ ${bold("Writing configuration files...")}`);
5419
6935
  config.security.ownerUserId = ownerUserId;
5420
6936
  }
5421
6937
  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 });
5422
- const configPath = path5.join(configDir, "default.yml");
5423
- fs5.writeFileSync(configPath, yamlStr, "utf-8");
6938
+ const configPath = path9.join(configDir, "default.yml");
6939
+ fs7.writeFileSync(configPath, yamlStr, "utf-8");
5424
6940
  console.log(` ${green("+")} ${dim("config/default.yml")} written`);
5425
- const rulesDir = path5.join(configDir, "rules");
5426
- if (!fs5.existsSync(rulesDir)) {
5427
- fs5.mkdirSync(rulesDir, { recursive: true });
6941
+ const rulesDir = path9.join(configDir, "rules");
6942
+ if (!fs7.existsSync(rulesDir)) {
6943
+ fs7.mkdirSync(rulesDir, { recursive: true });
5428
6944
  }
5429
6945
  const ownerAdminRule = enableShell && ownerUserId ? `
5430
6946
  # Allow admin actions (shell, etc.) for the owner only
@@ -5505,12 +7021,12 @@ ${ownerAdminRule}
5505
7021
  actions: ["*"]
5506
7022
  riskLevels: [read, write, destructive, admin]
5507
7023
  `;
5508
- const rulesPath = path5.join(rulesDir, "default-rules.yml");
5509
- fs5.writeFileSync(rulesPath, rulesYaml, "utf-8");
7024
+ const rulesPath = path9.join(rulesDir, "default-rules.yml");
7025
+ fs7.writeFileSync(rulesPath, rulesYaml, "utf-8");
5510
7026
  console.log(` ${green("+")} ${dim("config/rules/default-rules.yml")} written`);
5511
- const dataDir = path5.join(projectRoot, "data");
5512
- if (!fs5.existsSync(dataDir)) {
5513
- fs5.mkdirSync(dataDir, { recursive: true });
7027
+ const dataDir = path9.join(projectRoot, "data");
7028
+ if (!fs7.existsSync(dataDir)) {
7029
+ fs7.mkdirSync(dataDir, { recursive: true });
5514
7030
  console.log(` ${green("+")} ${dim("data/")} directory created`);
5515
7031
  }
5516
7032
  console.log("");
@@ -5544,6 +7060,15 @@ ${ownerAdminRule}
5544
7060
  } else {
5545
7061
  console.log(` ${bold("Email:")} ${dim("disabled")}`);
5546
7062
  }
7063
+ if (speechProvider) {
7064
+ const speechLabelMap = {
7065
+ openai: "OpenAI Whisper",
7066
+ groq: "Groq Whisper"
7067
+ };
7068
+ console.log(` ${bold("Voice:")} ${speechLabelMap[speechProvider]}`);
7069
+ } else {
7070
+ console.log(` ${bold("Voice:")} ${dim("disabled")}`);
7071
+ }
5547
7072
  if (ownerUserId) {
5548
7073
  console.log(` ${bold("Owner ID:")} ${ownerUserId}`);
5549
7074
  console.log(` ${bold("Shell access:")} ${enableShell ? green("enabled") : dim("disabled")}`);
@@ -5783,8 +7308,8 @@ var rules_exports = {};
5783
7308
  __export(rules_exports, {
5784
7309
  rulesCommand: () => rulesCommand
5785
7310
  });
5786
- import fs6 from "node:fs";
5787
- import path6 from "node:path";
7311
+ import fs8 from "node:fs";
7312
+ import path10 from "node:path";
5788
7313
  import yaml4 from "js-yaml";
5789
7314
  async function rulesCommand() {
5790
7315
  const configLoader = new ConfigLoader();
@@ -5795,18 +7320,18 @@ async function rulesCommand() {
5795
7320
  console.error("Failed to load configuration:", error.message);
5796
7321
  process.exit(1);
5797
7322
  }
5798
- const rulesPath = path6.resolve(config.security.rulesPath);
5799
- if (!fs6.existsSync(rulesPath)) {
7323
+ const rulesPath = path10.resolve(config.security.rulesPath);
7324
+ if (!fs8.existsSync(rulesPath)) {
5800
7325
  console.log(`Rules directory not found: ${rulesPath}`);
5801
7326
  console.log("No security rules loaded.");
5802
7327
  return;
5803
7328
  }
5804
- const stat = fs6.statSync(rulesPath);
7329
+ const stat = fs8.statSync(rulesPath);
5805
7330
  if (!stat.isDirectory()) {
5806
7331
  console.error(`Rules path is not a directory: ${rulesPath}`);
5807
7332
  process.exit(1);
5808
7333
  }
5809
- const files = fs6.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
7334
+ const files = fs8.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
5810
7335
  if (files.length === 0) {
5811
7336
  console.log(`No YAML rule files found in: ${rulesPath}`);
5812
7337
  return;
@@ -5815,9 +7340,9 @@ async function rulesCommand() {
5815
7340
  const allRules = [];
5816
7341
  const errors = [];
5817
7342
  for (const file of files) {
5818
- const filePath = path6.join(rulesPath, file);
7343
+ const filePath = path10.join(rulesPath, file);
5819
7344
  try {
5820
- const raw = fs6.readFileSync(filePath, "utf-8");
7345
+ const raw = fs8.readFileSync(filePath, "utf-8");
5821
7346
  const parsed = yaml4.load(raw);
5822
7347
  const rules = ruleLoader.loadFromObject(parsed);
5823
7348
  allRules.push(...rules);
@@ -5869,8 +7394,8 @@ var status_exports = {};
5869
7394
  __export(status_exports, {
5870
7395
  statusCommand: () => statusCommand
5871
7396
  });
5872
- import fs7 from "node:fs";
5873
- import path7 from "node:path";
7397
+ import fs9 from "node:fs";
7398
+ import path11 from "node:path";
5874
7399
  import yaml5 from "js-yaml";
5875
7400
  async function statusCommand() {
5876
7401
  const configLoader = new ConfigLoader();
@@ -5927,22 +7452,22 @@ async function statusCommand() {
5927
7452
  }
5928
7453
  console.log("");
5929
7454
  console.log("Storage:");
5930
- const dbPath = path7.resolve(config.storage.path);
5931
- const dbExists = fs7.existsSync(dbPath);
7455
+ const dbPath = path11.resolve(config.storage.path);
7456
+ const dbExists = fs9.existsSync(dbPath);
5932
7457
  console.log(` Database: ${dbPath}`);
5933
7458
  console.log(` Status: ${dbExists ? "exists" : "not yet created"}`);
5934
7459
  console.log("");
5935
- const rulesPath = path7.resolve(config.security.rulesPath);
7460
+ const rulesPath = path11.resolve(config.security.rulesPath);
5936
7461
  let ruleCount = 0;
5937
7462
  let ruleFileCount = 0;
5938
- if (fs7.existsSync(rulesPath) && fs7.statSync(rulesPath).isDirectory()) {
5939
- const files = fs7.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
7463
+ if (fs9.existsSync(rulesPath) && fs9.statSync(rulesPath).isDirectory()) {
7464
+ const files = fs9.readdirSync(rulesPath).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
5940
7465
  ruleFileCount = files.length;
5941
7466
  const ruleLoader = new RuleLoader();
5942
7467
  for (const file of files) {
5943
- const filePath = path7.join(rulesPath, file);
7468
+ const filePath = path11.join(rulesPath, file);
5944
7469
  try {
5945
- const raw = fs7.readFileSync(filePath, "utf-8");
7470
+ const raw = fs9.readFileSync(filePath, "utf-8");
5946
7471
  const parsed = yaml5.load(raw);
5947
7472
  const rules = ruleLoader.loadFromObject(parsed);
5948
7473
  ruleCount += rules.length;
@@ -5976,8 +7501,8 @@ var logs_exports = {};
5976
7501
  __export(logs_exports, {
5977
7502
  logsCommand: () => logsCommand
5978
7503
  });
5979
- import fs8 from "node:fs";
5980
- import path8 from "node:path";
7504
+ import fs10 from "node:fs";
7505
+ import path12 from "node:path";
5981
7506
  async function logsCommand(tail) {
5982
7507
  const configLoader = new ConfigLoader();
5983
7508
  let config;
@@ -5987,8 +7512,8 @@ async function logsCommand(tail) {
5987
7512
  console.error("Failed to load configuration:", error.message);
5988
7513
  process.exit(1);
5989
7514
  }
5990
- const dbPath = path8.resolve(config.storage.path);
5991
- if (!fs8.existsSync(dbPath)) {
7515
+ const dbPath = path12.resolve(config.storage.path);
7516
+ if (!fs10.existsSync(dbPath)) {
5992
7517
  console.log(`Database not found at: ${dbPath}`);
5993
7518
  console.log("No audit log entries. Alfred has not been run yet, or the database path is incorrect.");
5994
7519
  return;