assistme 0.2.3 → 0.2.5

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.
@@ -1,16 +1,3 @@
1
- import { getConfig } from "../utils/config.js";
2
- import { log } from "../utils/logger.js";
3
-
4
- /**
5
- * Extracted skill from LLM analysis of a multi-step task.
6
- */
7
- export interface ExtractedSkill {
8
- name: string; // kebab-case, e.g. "price-comparison-amazon"
9
- description: string; // One-line description
10
- steps: string; // Markdown step-by-step instructions
11
- emoji: string; // Single emoji for this skill
12
- }
13
-
14
1
  /**
15
2
  * A record of a tool call made during task execution.
16
3
  */
@@ -19,449 +6,3 @@ export interface ToolCallRecord {
19
6
  input: Record<string, unknown>;
20
7
  result: string;
21
8
  }
22
-
23
- const EXTRACTION_PROMPT = `You are a skill extraction system for an AI agent that controls a real web browser (Chrome via CDP) and can read/write files. Your job is to analyze completed task conversations and extract REUSABLE WORKFLOWS as skills.
24
-
25
- A skill is a step-by-step instruction template that teaches the agent how to handle a TYPE of task (not a specific instance).
26
-
27
- Rules:
28
- - Only extract skills for tasks that involved 3+ distinct steps (multi-step workflows)
29
- - The skill must be GENERALIZABLE — it should work for similar future tasks, not just this specific one
30
- - Replace specific values (URLs, product names, prices) with generic placeholders like {product}, {website}, {query}
31
- - Focus on the WORKFLOW PATTERN, not the specific data
32
- - Write clear markdown instructions that another AI agent could follow
33
- - If the task was too simple or too specific to generalize, return null
34
- - The name must be kebab-case (lowercase, hyphens)
35
- - Description should be one concise line
36
-
37
- DO NOT extract skills for:
38
- - Simple single-step tasks (just opening one page)
39
- - Tasks that failed or had errors
40
- - Tasks that are too specific to generalize (e.g. "buy exactly this item at this price")
41
-
42
- Respond with ONLY valid JSON — no markdown wrapping, no explanation:
43
-
44
- If a reusable skill was found:
45
- {"name": "skill-name", "description": "What this skill does", "steps": "## Workflow\\n\\n1. **Step one**\\n...", "emoji": "🔧"}
46
-
47
- If nothing worth extracting:
48
- null`;
49
-
50
- /**
51
- * Use Haiku to analyze a completed multi-step task and extract a reusable skill.
52
- * Only called when a task involved 3+ tool calls (multi-step workflow).
53
- *
54
- * Cost: ~$0.002 per extraction (slightly more context than memory extraction).
55
- */
56
- export async function extractSkillWithLLM(
57
- taskPrompt: string,
58
- taskResult: string,
59
- toolCalls: ToolCallRecord[]
60
- ): Promise<ExtractedSkill | null> {
61
- const config = getConfig();
62
- const apiKey = config.anthropicApiKey;
63
-
64
- if (!apiKey) {
65
- log.debug("No API key, skipping skill extraction");
66
- return null;
67
- }
68
-
69
- // Build a summary of tool calls (not the full data, just the flow)
70
- const toolFlow = toolCalls
71
- .map((tc, i) => {
72
- const inputSummary = Object.entries(tc.input)
73
- .map(([k, v]) => `${k}=${String(v).slice(0, 100)}`)
74
- .join(", ");
75
- const resultSummary = tc.result.slice(0, 150);
76
- return `${i + 1}. ${tc.name}(${inputSummary}) → ${resultSummary}`;
77
- })
78
- .slice(0, 20) // Max 20 tool calls in summary
79
- .join("\n");
80
-
81
- const userMessage = `<task_prompt>
82
- ${taskPrompt.slice(0, 1500)}
83
- </task_prompt>
84
-
85
- <tool_calls_flow>
86
- ${toolFlow.slice(0, 3000)}
87
- </tool_calls_flow>
88
-
89
- <final_result>
90
- ${taskResult.slice(0, 2000)}
91
- </final_result>`;
92
-
93
- try {
94
- const response = await fetch("https://api.anthropic.com/v1/messages", {
95
- method: "POST",
96
- headers: {
97
- "Content-Type": "application/json",
98
- "x-api-key": apiKey,
99
- "anthropic-version": "2023-06-01",
100
- },
101
- body: JSON.stringify({
102
- model: "claude-haiku-4-5-20251001",
103
- max_tokens: 1024,
104
- system: EXTRACTION_PROMPT,
105
- messages: [{ role: "user", content: userMessage }],
106
- }),
107
- });
108
-
109
- if (!response.ok) {
110
- const errText = await response.text();
111
- log.debug(
112
- `Skill extraction API error: ${response.status} ${errText.slice(0, 200)}`
113
- );
114
- return null;
115
- }
116
-
117
- const data = await response.json();
118
-
119
- const text =
120
- data.content?.[0]?.type === "text" ? data.content[0].text : "";
121
-
122
- if (!text || text.trim() === "null") {
123
- log.debug("Skill extraction: nothing reusable found");
124
- return null;
125
- }
126
-
127
- // Parse JSON
128
- const jsonStr = text
129
- .replace(/```json\n?/g, "")
130
- .replace(/```\n?/g, "")
131
- .trim();
132
- const skill: ExtractedSkill = JSON.parse(jsonStr);
133
-
134
- // Validate
135
- if (!skill || !skill.name || !skill.steps || !skill.description) {
136
- return null;
137
- }
138
-
139
- // Sanitize name to kebab-case
140
- skill.name = skill.name
141
- .toLowerCase()
142
- .replace(/[^a-z0-9-]/g, "-")
143
- .replace(/-+/g, "-")
144
- .replace(/^-|-$/g, "");
145
-
146
- if (skill.name.length < 3) return null;
147
-
148
- return skill;
149
- } catch (err) {
150
- log.debug(
151
- `Skill extraction failed: ${err instanceof Error ? err.message : err}`
152
- );
153
- return null;
154
- }
155
- }
156
-
157
- // ── Proactive Skill Generation (from description, not from completed tasks) ──
158
-
159
- /**
160
- * A skill specification decomposed from a work profile.
161
- */
162
- export interface SkillSpec {
163
- name: string;
164
- description: string;
165
- category: string; // e.g. "data-processing", "communication", "reporting"
166
- priority: "high" | "medium" | "low";
167
- automatable: boolean; // whether the agent can fully automate this
168
- }
169
-
170
- const DECOMPOSE_JOB_PROMPT = `You are a job analysis system. Given a person's job description, decompose their work into individual AUTOMATABLE SKILLS that an AI agent could learn and execute.
171
-
172
- The AI agent has these capabilities:
173
- - Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
174
- - Read/write files on the local machine
175
- - Execute shell commands
176
- - Store memories and learn from interactions
177
-
178
- Rules:
179
- - Focus on RECURRING, REPETITIVE tasks that benefit from automation
180
- - Each skill should be a single, well-defined workflow
181
- - Prioritize tasks the person does frequently
182
- - Mark tasks as automatable:true only if the agent can handle them end-to-end
183
- - Name skills in kebab-case
184
- - Be specific — "check-email" is too vague, "summarize-unread-gmail" is better
185
- - Generate 5-15 skills depending on job complexity
186
- - Consider both web-based and file-based workflows
187
-
188
- Respond with ONLY valid JSON array — no markdown wrapping:
189
- [{"name": "skill-name", "description": "What this skill does", "category": "category", "priority": "high|medium|low", "automatable": true|false}]`;
190
-
191
- /**
192
- * Decompose a job description into individual skill specifications.
193
- * This is the first step: understand what the person does and break it into automatable units.
194
- *
195
- * Cost: ~$0.003 per call (Haiku, moderate context).
196
- */
197
- export async function decomposeJob(
198
- jobDescription: string,
199
- existingSkillNames: string[] = []
200
- ): Promise<SkillSpec[]> {
201
- const config = getConfig();
202
- const apiKey = config.anthropicApiKey;
203
- if (!apiKey) return [];
204
-
205
- const existingNote =
206
- existingSkillNames.length > 0
207
- ? `\n\nThe agent already has these skills (do NOT duplicate): ${existingSkillNames.join(", ")}`
208
- : "";
209
-
210
- try {
211
- const response = await fetch("https://api.anthropic.com/v1/messages", {
212
- method: "POST",
213
- headers: {
214
- "Content-Type": "application/json",
215
- "x-api-key": apiKey,
216
- "anthropic-version": "2023-06-01",
217
- },
218
- body: JSON.stringify({
219
- model: "claude-haiku-4-5-20251001",
220
- max_tokens: 2048,
221
- system: DECOMPOSE_JOB_PROMPT,
222
- messages: [
223
- {
224
- role: "user",
225
- content: `<job_description>\n${jobDescription.slice(0, 3000)}\n</job_description>${existingNote}`,
226
- },
227
- ],
228
- }),
229
- });
230
-
231
- if (!response.ok) {
232
- const errText = await response.text().catch(() => "");
233
- log.debug(`decomposeJob API error: ${response.status} ${errText.slice(0, 300)}`);
234
- return [];
235
- }
236
-
237
- const data = await response.json();
238
- const text =
239
- data.content?.[0]?.type === "text" ? data.content[0].text : "";
240
- if (!text) {
241
- log.debug(`decomposeJob: empty response from API`);
242
- return [];
243
- }
244
-
245
- const jsonStr = text
246
- .replace(/```json\n?/g, "")
247
- .replace(/```\n?/g, "")
248
- .trim();
249
- const specs: SkillSpec[] = JSON.parse(jsonStr);
250
-
251
- if (!Array.isArray(specs)) {
252
- log.debug(`decomposeJob: response is not an array: ${jsonStr.slice(0, 200)}`);
253
- return [];
254
- }
255
-
256
- // Sanitize names
257
- return specs
258
- .filter((s) => s.name && s.description)
259
- .map((s) => ({
260
- ...s,
261
- name: s.name
262
- .toLowerCase()
263
- .replace(/[^a-z0-9-]/g, "-")
264
- .replace(/-+/g, "-")
265
- .replace(/^-|-$/g, ""),
266
- }));
267
- } catch (err) {
268
- log.debug(`Job decomposition failed: ${err instanceof Error ? err.message : err}`);
269
- return [];
270
- }
271
- }
272
-
273
- const GENERATE_PROMPT = `You are a skill authoring system for an AI agent. Given a skill name, description, and context about the user's work, generate detailed step-by-step instructions that the AI agent can follow to execute this skill.
274
-
275
- The AI agent has these capabilities:
276
- - Control a real Chrome browser (navigate, click, type, read pages, take screenshots)
277
- - Read/write files on the local machine
278
- - Execute shell commands (Bash)
279
- - Store memories about the user
280
-
281
- Rules:
282
- - Write clear, actionable markdown instructions
283
- - Use numbered steps with bold action names
284
- - Include error handling (what to do if a page doesn't load, element not found, etc.)
285
- - Use placeholders like {query}, {date}, {recipient} for variable inputs
286
- - Include a $ARGUMENTS line at the top if the skill accepts parameters
287
- - Be specific about which browser tools to use (browser_navigate, browser_click, etc.)
288
- - Include validation steps (verify the action worked)
289
- - Keep instructions thorough but concise (10-25 steps)
290
-
291
- Respond with ONLY valid JSON — no markdown wrapping:
292
- {"name": "skill-name", "description": "One-line description", "steps": "## Workflow\\n\\n$ARGUMENTS: describe expected arguments\\n\\n1. **Step one**\\n...", "emoji": "🔧", "keywords": ["keyword1", "keyword2"]}`;
293
-
294
- /**
295
- * Generate a complete skill (with full instructions) from a description.
296
- * Unlike extractSkillWithLLM which requires a completed task, this generates proactively.
297
- *
298
- * Cost: ~$0.003 per generation (Haiku, moderate output).
299
- */
300
- export async function generateSkillFromDescription(
301
- name: string,
302
- description: string,
303
- jobContext?: string
304
- ): Promise<ExtractedSkill & { keywords?: string[] } | null> {
305
- const config = getConfig();
306
- const apiKey = config.anthropicApiKey;
307
- if (!apiKey) return null;
308
-
309
- const contextBlock = jobContext
310
- ? `\n\n<user_job_context>\n${jobContext.slice(0, 1000)}\n</user_job_context>`
311
- : "";
312
-
313
- try {
314
- const response = await fetch("https://api.anthropic.com/v1/messages", {
315
- method: "POST",
316
- headers: {
317
- "Content-Type": "application/json",
318
- "x-api-key": apiKey,
319
- "anthropic-version": "2023-06-01",
320
- },
321
- body: JSON.stringify({
322
- model: "claude-haiku-4-5-20251001",
323
- max_tokens: 2048,
324
- system: GENERATE_PROMPT,
325
- messages: [
326
- {
327
- role: "user",
328
- content: `Generate a skill for:\nName: ${name}\nDescription: ${description}${contextBlock}`,
329
- },
330
- ],
331
- }),
332
- });
333
-
334
- if (!response.ok) {
335
- const errText = await response.text().catch(() => "");
336
- log.debug(`generateSkill API error: ${response.status} ${errText.slice(0, 300)}`);
337
- return null;
338
- }
339
-
340
- const data = await response.json();
341
- const text =
342
- data.content?.[0]?.type === "text" ? data.content[0].text : "";
343
- if (!text) {
344
- log.debug(`generateSkill: empty response from API`);
345
- return null;
346
- }
347
-
348
- const jsonStr = text
349
- .replace(/```json\n?/g, "")
350
- .replace(/```\n?/g, "")
351
- .trim();
352
- const skill = JSON.parse(jsonStr);
353
-
354
- if (!skill || !skill.name || !skill.steps) {
355
- log.debug(`generateSkill: invalid response structure: ${jsonStr.slice(0, 200)}`);
356
- return null;
357
- }
358
-
359
- skill.name = skill.name
360
- .toLowerCase()
361
- .replace(/[^a-z0-9-]/g, "-")
362
- .replace(/-+/g, "-")
363
- .replace(/^-|-$/g, "");
364
-
365
- return skill;
366
- } catch (err) {
367
- log.debug(`Skill generation failed: ${err instanceof Error ? err.message : err}`);
368
- return null;
369
- }
370
- }
371
-
372
- const IMPROVEMENT_PROMPT = `You are a skill improvement system. You have an EXISTING skill (workflow instructions for an AI agent) and a NEW task execution that used this skill. Analyze if the new execution discovered a BETTER approach than what the current skill describes.
373
-
374
- Rules:
375
- - Only suggest improvements if the new execution found a genuinely better workflow
376
- - Merge the best parts of the old skill and new execution
377
- - Keep instructions general/reusable (use placeholders like {product}, {query})
378
- - Preserve the overall structure but update specific steps that improved
379
- - If the existing skill is already optimal, return null
380
- - The output must be the FULL updated skill content (not a diff)
381
-
382
- Respond with ONLY valid JSON — no markdown wrapping:
383
-
384
- If improvement found:
385
- {"improved_steps": "## Workflow\\n\\n1. **Step one**\\n...", "change_summary": "Brief description of what changed"}
386
-
387
- If no improvement needed:
388
- null`;
389
-
390
- /**
391
- * Analyze whether a skill should be improved based on a new task execution.
392
- * Called when a task used an existing skill and completed successfully.
393
- */
394
- export async function analyzeSkillImprovement(
395
- existingSkillContent: string,
396
- taskPrompt: string,
397
- taskResult: string,
398
- toolCalls: ToolCallRecord[]
399
- ): Promise<{ improved_steps: string; change_summary: string } | null> {
400
- const config = getConfig();
401
- const apiKey = config.anthropicApiKey;
402
-
403
- if (!apiKey) return null;
404
-
405
- const toolFlow = toolCalls
406
- .map((tc, i) => {
407
- const inputSummary = Object.entries(tc.input)
408
- .map(([k, v]) => `${k}=${String(v).slice(0, 80)}`)
409
- .join(", ");
410
- return `${i + 1}. ${tc.name}(${inputSummary})`;
411
- })
412
- .slice(0, 15)
413
- .join("\n");
414
-
415
- const userMessage = `<existing_skill>
416
- ${existingSkillContent.slice(0, 2000)}
417
- </existing_skill>
418
-
419
- <new_task>
420
- ${taskPrompt.slice(0, 1000)}
421
- </new_task>
422
-
423
- <actual_tool_flow>
424
- ${toolFlow.slice(0, 2000)}
425
- </actual_tool_flow>
426
-
427
- <task_result>
428
- ${taskResult.slice(0, 1500)}
429
- </task_result>`;
430
-
431
- try {
432
- const response = await fetch("https://api.anthropic.com/v1/messages", {
433
- method: "POST",
434
- headers: {
435
- "Content-Type": "application/json",
436
- "x-api-key": apiKey,
437
- "anthropic-version": "2023-06-01",
438
- },
439
- body: JSON.stringify({
440
- model: "claude-haiku-4-5-20251001",
441
- max_tokens: 1024,
442
- system: IMPROVEMENT_PROMPT,
443
- messages: [{ role: "user", content: userMessage }],
444
- }),
445
- });
446
-
447
- if (!response.ok) return null;
448
-
449
- const data = await response.json();
450
- const text =
451
- data.content?.[0]?.type === "text" ? data.content[0].text : "";
452
-
453
- if (!text || text.trim() === "null") return null;
454
-
455
- const jsonStr = text
456
- .replace(/```json\n?/g, "")
457
- .replace(/```\n?/g, "")
458
- .trim();
459
- const result = JSON.parse(jsonStr);
460
-
461
- if (!result || !result.improved_steps) return null;
462
-
463
- return result;
464
- } catch {
465
- return null;
466
- }
467
- }
@@ -388,14 +388,18 @@ export class SkillManager {
388
388
  return null;
389
389
  }
390
390
 
391
- const row = Array.isArray(data) ? data[0] : data;
391
+ const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
392
392
  if (!row) {
393
393
  log.debug(`Skill create returned no data for "${name}"`);
394
394
  return null;
395
395
  }
396
396
 
397
- log.info(`Skill "${name}" created in skills table (pending approval)`);
398
- return row as { id: string; name: string };
397
+ // RPC returns out_id/out_name to avoid column ambiguity
398
+ const id = (row.out_id || row.id) as string;
399
+ const skillName = (row.out_name || row.name) as string;
400
+
401
+ log.info(`Skill "${skillName}" created in skills table (pending approval)`);
402
+ return { id, name: skillName };
399
403
  } catch (err) {
400
404
  log.debug(`Skill create error: ${err}`);
401
405
  return null;
@@ -418,7 +418,8 @@ export type EventType =
418
418
  | "tool_result"
419
419
  | "thinking"
420
420
  | "error"
421
- | "status_change";
421
+ | "status_change"
422
+ | "user_action_request";
422
423
 
423
424
  let eventSequence = 0;
424
425
 
@@ -459,3 +460,30 @@ export async function emitEvents(
459
460
  });
460
461
  if (error) log.warn(`Failed to emit events batch: ${error.message}`);
461
462
  }
463
+
464
+ // ── Action Request Helpers ──────────────────────────────────────────
465
+
466
+ export async function setActionRequest(
467
+ messageId: string,
468
+ actionData: Record<string, unknown>
469
+ ): Promise<void> {
470
+ const sb = getSupabase();
471
+ const { error } = await sb.rpc("mcp_set_action_request", {
472
+ p_token_hash: getTokenHash(),
473
+ p_message_id: messageId,
474
+ p_action_data: actionData,
475
+ });
476
+ if (error) throw new Error(`Failed to set action request: ${error.message}`);
477
+ }
478
+
479
+ export async function pollActionResponse(
480
+ messageId: string
481
+ ): Promise<Record<string, unknown> | null> {
482
+ const sb = getSupabase();
483
+ const { data, error } = await sb.rpc("mcp_poll_action_response", {
484
+ p_token_hash: getTokenHash(),
485
+ p_message_id: messageId,
486
+ });
487
+ if (error) throw new Error(`Failed to poll action response: ${error.message}`);
488
+ return data as Record<string, unknown> | null;
489
+ }
@@ -1,128 +0,0 @@
1
- import { getConfig } from "../utils/config.js";
2
- import { log } from "../utils/logger.js";
3
- import type { MemoryCategory } from "./memory.js";
4
-
5
- /**
6
- * Extracted memory from LLM analysis of a conversation.
7
- */
8
- export interface ExtractedMemory {
9
- content: string;
10
- category: MemoryCategory;
11
- importance: number;
12
- tags: string[];
13
- }
14
-
15
- const EXTRACTION_PROMPT = `You are a memory extraction system. Analyze the following conversation between a user and an AI assistant. Extract any facts worth remembering about the user for future conversations.
16
-
17
- Rules:
18
- - Only extract FACTUAL information about the user, not about the task itself
19
- - Be concise: each memory should be a single clear statement
20
- - Don't extract trivial/obvious things (e.g. "user asked a question")
21
- - Categories: "preference" (likes/dislikes/habits), "instruction" (standing orders like "always do X"), "context" (work/life info), "fact" (specific facts), "general" (other)
22
- - Importance: 3-4 for minor preferences, 5-6 for useful context, 7-8 for strong preferences/instructions, 9-10 for critical standing instructions
23
- - If nothing worth remembering, return an empty array
24
- - Max 3 memories per conversation
25
- - Support both English and Chinese content
26
-
27
- Respond with ONLY valid JSON — no markdown, no explanation:
28
- [{"content": "...", "category": "...", "importance": N, "tags": ["..."]}]
29
-
30
- If nothing to remember: []`;
31
-
32
- /**
33
- * Use Haiku to analyze a task conversation and extract memories.
34
- * This runs as a fire-and-forget background task after each completed task.
35
- * Cost: ~$0.001 per extraction (Haiku is very cheap).
36
- */
37
- export async function extractMemoriesWithLLM(
38
- taskPrompt: string,
39
- taskResult: string
40
- ): Promise<ExtractedMemory[]> {
41
- const config = getConfig();
42
- const apiKey = config.anthropicApiKey;
43
-
44
- if (!apiKey) {
45
- log.debug("No API key, skipping LLM memory extraction");
46
- return [];
47
- }
48
-
49
- // Truncate to keep costs low — Haiku context is cheap but no need to waste
50
- const truncatedPrompt = taskPrompt.slice(0, 2000);
51
- const truncatedResult = taskResult.slice(0, 3000);
52
-
53
- const userMessage = `<user_request>
54
- ${truncatedPrompt}
55
- </user_request>
56
-
57
- <assistant_response>
58
- ${truncatedResult}
59
- </assistant_response>`;
60
-
61
- try {
62
- const response = await fetch("https://api.anthropic.com/v1/messages", {
63
- method: "POST",
64
- headers: {
65
- "Content-Type": "application/json",
66
- "x-api-key": apiKey,
67
- "anthropic-version": "2023-06-01",
68
- },
69
- body: JSON.stringify({
70
- model: "claude-haiku-4-5-20251001",
71
- max_tokens: 512,
72
- system: EXTRACTION_PROMPT,
73
- messages: [{ role: "user", content: userMessage }],
74
- }),
75
- });
76
-
77
- if (!response.ok) {
78
- const errText = await response.text();
79
- log.debug(`Memory extraction API error: ${response.status} ${errText.slice(0, 200)}`);
80
- return [];
81
- }
82
-
83
- const data = await response.json();
84
-
85
- // Extract text from response
86
- const text =
87
- data.content?.[0]?.type === "text" ? data.content[0].text : "";
88
-
89
- if (!text || text.trim() === "[]") {
90
- log.debug("LLM memory extraction: nothing to remember");
91
- return [];
92
- }
93
-
94
- // Parse JSON — handle potential markdown wrapping
95
- const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
96
- const memories: ExtractedMemory[] = JSON.parse(jsonStr);
97
-
98
- // Validate
99
- const validCategories: MemoryCategory[] = [
100
- "general",
101
- "preference",
102
- "instruction",
103
- "context",
104
- "skill_learned",
105
- "fact",
106
- ];
107
-
108
- return memories
109
- .filter(
110
- (m) =>
111
- m.content &&
112
- m.content.length > 5 &&
113
- validCategories.includes(m.category)
114
- )
115
- .map((m) => ({
116
- content: m.content.slice(0, 500),
117
- category: m.category,
118
- importance: Math.max(1, Math.min(10, m.importance || 5)),
119
- tags: Array.isArray(m.tags) ? m.tags.slice(0, 5) : [],
120
- }))
121
- .slice(0, 3); // Max 3 memories per conversation
122
- } catch (err) {
123
- log.debug(
124
- `LLM memory extraction failed: ${err instanceof Error ? err.message : err}`
125
- );
126
- return [];
127
- }
128
- }