assistme 0.8.2 → 0.8.3

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 (83) hide show
  1. package/dist/{chunk-A2NR7LCQ.js → chunk-3UNXN3BX.js} +60 -35
  2. package/dist/chunk-HY3FFXSQ.js +113 -0
  3. package/dist/{chunk-IKYXC4RJ.js → chunk-UHGTMSLZ.js} +640 -327
  4. package/dist/config-2HH7PO34.js +20 -0
  5. package/dist/index.js +658 -788
  6. package/dist/job-runner-P4DIXXCV.js +7 -0
  7. package/dist/workers/entry.js +529 -315
  8. package/package.json +1 -1
  9. package/src/NAMING.md +34 -0
  10. package/src/agent/heartbeat-checks.ts +158 -0
  11. package/src/agent/heartbeat-compiler.ts +122 -0
  12. package/src/agent/heartbeat-types.ts +62 -0
  13. package/src/agent/job-analysis-poller.ts +96 -0
  14. package/src/agent/job-runner.ts +31 -0
  15. package/src/agent/proactive-monitor.ts +88 -410
  16. package/src/agent/processor.ts +147 -252
  17. package/src/agent/prompt-builder.ts +83 -0
  18. package/src/agent/scheduler.ts +1 -1
  19. package/src/agent/sdk-stream.ts +105 -0
  20. package/src/agent/self-analyzer.ts +21 -22
  21. package/src/agent/session-heartbeat.ts +34 -0
  22. package/src/agent/skill-db.ts +170 -0
  23. package/src/agent/skill-evaluator.ts +4 -13
  24. package/src/agent/skill-format.ts +96 -0
  25. package/src/agent/skill-marketplace.ts +98 -0
  26. package/src/agent/skill-search.ts +315 -0
  27. package/src/agent/skill-types.ts +68 -0
  28. package/src/agent/skill-utils.ts +66 -0
  29. package/src/agent/skills.ts +265 -566
  30. package/src/agent/system-prompt.ts +11 -8
  31. package/src/agent/task-poller.ts +101 -0
  32. package/src/agent/task-timeout.ts +50 -0
  33. package/src/browser/chrome-launcher.ts +6 -6
  34. package/src/browser/controller.ts +1 -2
  35. package/src/commands/auth.ts +4 -11
  36. package/src/commands/browser.ts +12 -47
  37. package/src/commands/credential.ts +2 -2
  38. package/src/commands/job.ts +3 -3
  39. package/src/credentials/encryption.ts +4 -10
  40. package/src/credentials/local-store.ts +4 -7
  41. package/src/credentials/program-store.ts +8 -10
  42. package/src/db/auth-store.ts +15 -13
  43. package/src/db/session-log.ts +12 -5
  44. package/src/mcp/agent-tools-server.ts +126 -102
  45. package/src/mcp/ask-user.ts +102 -0
  46. package/src/mcp/skill-confirmation.ts +109 -0
  47. package/src/orchestrator.ts +100 -350
  48. package/src/tools/filesystem.ts +48 -2
  49. package/src/tools/index.ts +1 -2
  50. package/src/tools/shell.ts +58 -2
  51. package/src/types/edsger-feedback.d.ts +18 -0
  52. package/src/utils/config.ts +79 -1
  53. package/src/utils/logger.ts +52 -12
  54. package/src/utils/lru-cache.ts +57 -0
  55. package/src/utils/schemas.ts +2 -0
  56. package/src/workers/base-handler.ts +7 -1
  57. package/src/workers/index.ts +2 -0
  58. package/src/workers/log-forwarder.ts +55 -0
  59. package/src/workers/manager.ts +82 -220
  60. package/src/workers/types.ts +9 -1
  61. package/src/workers/worker-lifecycle.ts +113 -0
  62. package/tests/agent/heartbeat-checks.test.ts +160 -0
  63. package/tests/agent/mcp-servers.test.ts +1 -1
  64. package/tests/agent/proactive-monitor.test.ts +42 -26
  65. package/tests/agent/processor.test.ts +2 -1
  66. package/tests/agent/sdk-stream.test.ts +181 -0
  67. package/tests/agent/self-analyzer.test.ts +106 -49
  68. package/tests/agent/session.test.ts +114 -132
  69. package/tests/agent/skill-format.test.ts +93 -0
  70. package/tests/agent/skill-search.test.ts +175 -0
  71. package/tests/agent/skill-utils.test.ts +86 -0
  72. package/tests/agent/skills.test.ts +4 -24
  73. package/tests/db/supabase.test.ts +3 -2
  74. package/tests/mcp/ask-user.test.ts +117 -0
  75. package/tests/mcp/skill-confirmation.integration.test.ts +216 -0
  76. package/tests/mcp/skill-confirmation.test.ts +66 -0
  77. package/tests/tools/filesystem.test.ts +38 -1
  78. package/tests/tools/shell.test.ts +43 -0
  79. package/tests/utils/config.test.ts +2 -2
  80. package/dist/chunk-YYSJHZSO.js +0 -47
  81. package/dist/config-3RWSAUAZ.js +0 -12
  82. package/dist/job-runner-PECVS424.js +0 -7
  83. package/src/agent/session.ts +0 -348
@@ -1,5 +1,4 @@
1
1
  import {
2
- AppError,
3
2
  BrowseSkillRowSchema,
4
3
  CDP_COMMAND_TIMEOUT_MS,
5
4
  FRAME_CONTEXTS_MAX_SIZE,
@@ -14,15 +13,17 @@ import {
14
13
  SkillRowSchema,
15
14
  WS_CONNECT_TIMEOUT_MS,
16
15
  callMcpHandler,
17
- errorMessage,
18
16
  log,
19
17
  readAuthStore,
20
18
  safeParse,
21
19
  writeAuthStore
22
- } from "./chunk-A2NR7LCQ.js";
20
+ } from "./chunk-3UNXN3BX.js";
23
21
  import {
24
- getConfig
25
- } from "./chunk-YYSJHZSO.js";
22
+ AppError,
23
+ errorMessage,
24
+ getConfig,
25
+ getDataDir
26
+ } from "./chunk-HY3FFXSQ.js";
26
27
 
27
28
  // src/db/auth.ts
28
29
  async function loginWithToken(mcpToken) {
@@ -52,7 +53,7 @@ async function logout() {
52
53
 
53
54
  // src/db/session.ts
54
55
  async function createSession(sessionName, workspacePath, version) {
55
- const { getConfig: getConfig2 } = await import("./config-3RWSAUAZ.js");
56
+ const { getConfig: getConfig2 } = await import("./config-2HH7PO34.js");
56
57
  const data = await callMcpHandler("session.create", {
57
58
  session_name: sessionName,
58
59
  workspace_path: workspacePath,
@@ -287,7 +288,7 @@ var BrowserController = class {
287
288
  throw new Error("Tab does not expose a WebSocket debugger URL.");
288
289
  }
289
290
  this.currentTabId = targetTab.id;
290
- return new Promise((resolve, reject) => {
291
+ return new Promise((resolve2, reject) => {
291
292
  let settled = false;
292
293
  this.ws = new WebSocket(targetTab.webSocketDebuggerUrl);
293
294
  const connectTimeout = setTimeout(() => {
@@ -308,7 +309,7 @@ var BrowserController = class {
308
309
  });
309
310
  this.send("DOM.enable").catch(() => {
310
311
  });
311
- resolve(`Connected to tab: "${targetTab.title}" (${targetTab.url})`);
312
+ resolve2(`Connected to tab: "${targetTab.title}" (${targetTab.url})`);
312
313
  });
313
314
  this.ws.on("message", (data) => {
314
315
  try {
@@ -356,7 +357,7 @@ var BrowserController = class {
356
357
  return await res.json();
357
358
  }
358
359
  send(method, params) {
359
- return new Promise((resolve, reject) => {
360
+ return new Promise((resolve2, reject) => {
360
361
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
361
362
  reject(new Error("Not connected to browser. Call browser_connect first."));
362
363
  return;
@@ -371,7 +372,7 @@ var BrowserController = class {
371
372
  if (response.error) {
372
373
  reject(new Error(`CDP error: ${response.error.message}`));
373
374
  } else {
374
- resolve(response.result || {});
375
+ resolve2(response.result || {});
375
376
  }
376
377
  });
377
378
  this.ws.send(JSON.stringify({ id, method, params }));
@@ -644,7 +645,7 @@ URL: ${info.url}`;
644
645
  };
645
646
  const parts = key.split("+");
646
647
  let modifiers = 0;
647
- let actualKey = parts[parts.length - 1];
648
+ const actualKey = parts[parts.length - 1];
648
649
  for (let i = 0; i < parts.length - 1; i++) {
649
650
  const mod = modifierMap[parts[i]];
650
651
  if (mod) modifiers |= mod;
@@ -1922,8 +1923,7 @@ function getDefaultProfileDir(chromePath) {
1922
1923
  return join(home, ".config", "google-chrome");
1923
1924
  }
1924
1925
  function getDebugProfileDir(chromePath) {
1925
- const home = homedir();
1926
- const debugDir = join(home, ".assistme", "browser-profile");
1926
+ const debugDir = join(getDataDir(), "browser-profile");
1927
1927
  if (!existsSync(debugDir)) {
1928
1928
  mkdirSync(debugDir, { recursive: true });
1929
1929
  log.debug(`Created debug profile directory: ${debugDir}`);
@@ -2079,7 +2079,9 @@ async function ensureBrowserAvailable(port = 9222) {
2079
2079
  success: false,
2080
2080
  action: "launch_failed",
2081
2081
  chromePath,
2082
- detail: "Could not start browser with remote debugging. Possible causes:\n 1) Another assistme debug browser is already using port " + port + "\n 2) The browser crashed on startup\nTry: rm -rf ~/.assistme/browser-profile && assistme"
2082
+ detail: "Could not start browser with remote debugging. Possible causes:\n 1) Another assistme debug browser is already using port " + port + `
2083
+ 2) The browser crashed on startup
2084
+ Try: rm -rf ${join(getDataDir(), "browser-profile")} && assistme`
2083
2085
  };
2084
2086
  }
2085
2087
  var browserInstances = /* @__PURE__ */ new Map();
@@ -2107,7 +2109,7 @@ function getNextRunTime(cronExpr, timezone, fromDate) {
2107
2109
  if (err instanceof Error && err.message.includes("No future run time")) {
2108
2110
  throw err;
2109
2111
  }
2110
- throw new Error(`Invalid cron expression "${cronExpr}": ${errorMessage(err)}`);
2112
+ throw new Error(`Invalid cron expression "${cronExpr}": ${errorMessage(err)}`, { cause: err });
2111
2113
  }
2112
2114
  }
2113
2115
  var Scheduler = class {
@@ -2393,8 +2395,47 @@ function computeWordOverlap(a, b) {
2393
2395
  return union === 0 ? 0 : intersection / union;
2394
2396
  }
2395
2397
 
2396
- // src/agent/skills.ts
2397
- import { execSync as execSync2 } from "child_process";
2398
+ // src/utils/lru-cache.ts
2399
+ var LRUCache = class {
2400
+ constructor(maxSize) {
2401
+ this.maxSize = maxSize;
2402
+ if (maxSize < 1) throw new Error("LRUCache maxSize must be >= 1");
2403
+ }
2404
+ cache = /* @__PURE__ */ new Map();
2405
+ get(key) {
2406
+ const value = this.cache.get(key);
2407
+ if (value === void 0) return void 0;
2408
+ this.cache.delete(key);
2409
+ this.cache.set(key, value);
2410
+ return value;
2411
+ }
2412
+ set(key, value) {
2413
+ if (this.cache.has(key)) {
2414
+ this.cache.delete(key);
2415
+ }
2416
+ this.cache.set(key, value);
2417
+ while (this.cache.size > this.maxSize) {
2418
+ const oldest = this.cache.keys().next().value;
2419
+ if (oldest !== void 0) {
2420
+ this.cache.delete(oldest);
2421
+ }
2422
+ }
2423
+ }
2424
+ has(key) {
2425
+ return this.cache.has(key);
2426
+ }
2427
+ delete(key) {
2428
+ return this.cache.delete(key);
2429
+ }
2430
+ clear() {
2431
+ this.cache.clear();
2432
+ }
2433
+ get size() {
2434
+ return this.cache.size;
2435
+ }
2436
+ };
2437
+
2438
+ // src/agent/skill-search.ts
2398
2439
  var STOP_WORDS = /* @__PURE__ */ new Set([
2399
2440
  "the",
2400
2441
  "a",
@@ -2520,6 +2561,151 @@ function bigrams(tokens) {
2520
2561
  }
2521
2562
  return result;
2522
2563
  }
2564
+ function buildIdfMap(skills) {
2565
+ const docFreq = /* @__PURE__ */ new Map();
2566
+ let totalSkills = 0;
2567
+ for (const skill of skills) {
2568
+ totalSkills++;
2569
+ const allText = `${skill.name} ${skill.description} ${skill.content} ${skill.keywords.join(" ")}`.toLowerCase();
2570
+ const words = new Set(tokenize(allText));
2571
+ for (const w of words) {
2572
+ docFreq.set(w, (docFreq.get(w) || 0) + 1);
2573
+ }
2574
+ }
2575
+ const total = totalSkills || 1;
2576
+ const idfMap = /* @__PURE__ */ new Map();
2577
+ for (const [word, df] of docFreq) {
2578
+ idfMap.set(word, Math.log(total / df) + 1);
2579
+ }
2580
+ return idfMap;
2581
+ }
2582
+ function createSearchContext(prompt, idfMap) {
2583
+ const lower = prompt.toLowerCase();
2584
+ const tokens = tokenize(lower);
2585
+ return {
2586
+ lower,
2587
+ tokens,
2588
+ tokenSet: new Set(tokens),
2589
+ bigramSet: bigrams(tokens),
2590
+ idf: (word) => idfMap.get(word) || 1
2591
+ };
2592
+ }
2593
+ function findSimilarSkill(name, skills) {
2594
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
2595
+ const nameWords = new Set(name.split("-"));
2596
+ for (const [existingName, skill] of skills) {
2597
+ const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
2598
+ if (normalizedName.includes(normalizedExisting) || normalizedExisting.includes(normalizedName)) {
2599
+ return skill;
2600
+ }
2601
+ const existingWords = new Set(existingName.split("-"));
2602
+ let overlap = 0;
2603
+ for (const w of nameWords) {
2604
+ if (existingWords.has(w)) overlap++;
2605
+ }
2606
+ if (overlap >= 2 || overlap >= 1 && nameWords.size <= 2) {
2607
+ return skill;
2608
+ }
2609
+ }
2610
+ return null;
2611
+ }
2612
+ var skillTokensCache = /* @__PURE__ */ new WeakMap();
2613
+ function getSkillTokens(skill) {
2614
+ let cached = skillTokensCache.get(skill);
2615
+ if (cached) return cached;
2616
+ const descTokens = tokenize(skill.description.toLowerCase());
2617
+ cached = {
2618
+ descTokens,
2619
+ descBigramSet: bigrams(descTokens),
2620
+ contentTokens: tokenize(skill.content.toLowerCase()),
2621
+ nameLower: skill.name.toLowerCase(),
2622
+ keywordsLower: skill.keywords.map((kw) => kw.toLowerCase())
2623
+ };
2624
+ skillTokensCache.set(skill, cached);
2625
+ return cached;
2626
+ }
2627
+ function scoreSkillRelevance(skill, ctx) {
2628
+ if (skill.disableModelInvocation) return -1;
2629
+ const st = getSkillTokens(skill);
2630
+ let score = 0;
2631
+ if (ctx.lower.includes(st.nameLower)) score += 10;
2632
+ for (const kw of st.keywordsLower) {
2633
+ if (ctx.lower.includes(kw)) score += 8;
2634
+ }
2635
+ for (const word of st.descTokens) {
2636
+ if (ctx.tokenSet.has(word)) score += 3 * ctx.idf(word);
2637
+ }
2638
+ for (const word of st.contentTokens) {
2639
+ if (ctx.tokenSet.has(word)) score += 0.5 * ctx.idf(word);
2640
+ }
2641
+ for (const bg of st.descBigramSet) {
2642
+ if (ctx.bigramSet.has(bg)) score += 5;
2643
+ }
2644
+ return score;
2645
+ }
2646
+
2647
+ // src/agent/skill-format.ts
2648
+ function formatSkillDescriptions(skills, relevantNames, budget) {
2649
+ const active = skills.filter((s) => !s.disableModelInvocation);
2650
+ if (active.length === 0) return "";
2651
+ const alwaysSkills = active.filter((s) => s.metadata.always);
2652
+ const rest = active.filter((s) => !s.metadata.always);
2653
+ const sorted = rest.sort((a, b) => {
2654
+ if (relevantNames) {
2655
+ const aRelevant = relevantNames.has(a.name);
2656
+ const bRelevant = relevantNames.has(b.name);
2657
+ if (aRelevant && !bRelevant) return -1;
2658
+ if (!aRelevant && bRelevant) return 1;
2659
+ }
2660
+ return (b.invocationCount || 0) - (a.invocationCount || 0);
2661
+ });
2662
+ const ordered = [...alwaysSkills, ...sorted];
2663
+ let remaining = budget;
2664
+ let prompt = "\n\n## Your Skills\n";
2665
+ prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
2666
+ prompt += "If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
2667
+ let included = 0;
2668
+ for (const skill of ordered) {
2669
+ const emoji = skill.metadata.emoji || "";
2670
+ const hint = skill.argumentHint ? ` (${skill.argumentHint})` : "";
2671
+ const line = `- **${emoji ? emoji + " " : ""}${skill.name}**${hint}: ${skill.description}
2672
+ `;
2673
+ if (remaining - line.length < 0) break;
2674
+ remaining -= line.length;
2675
+ prompt += line;
2676
+ included++;
2677
+ }
2678
+ if (included < ordered.length) {
2679
+ prompt += `
2680
+ _(${ordered.length - included} additional skills available \u2014 use skill_search to find more)_
2681
+ `;
2682
+ }
2683
+ return prompt;
2684
+ }
2685
+ function formatDiscoveredSkills(discovered) {
2686
+ if (discovered.length === 0) return "";
2687
+ let prompt = "\n\n## Suggested Skills from Marketplace\n";
2688
+ prompt += "These public skills may be relevant. Use skill_invoke with the exact name to add and use one.\n";
2689
+ prompt += "Only use them if they genuinely match the user's request.\n";
2690
+ prompt += "**Note:** Marketplace skills require user confirmation before use \u2014 the system will prompt the user to approve or deny. Each skill requires separate confirmation. If the user denies, do NOT retry \u2014 try alternatives or complete the task without it.\n\n";
2691
+ for (const d of discovered) {
2692
+ const emoji = d.emoji ? `${d.emoji} ` : "";
2693
+ const installs = d.install_count > 0 ? ` (${d.install_count} installs)` : "";
2694
+ prompt += `- **${emoji}${d.name}**${installs}: ${d.description}
2695
+ `;
2696
+ }
2697
+ return prompt;
2698
+ }
2699
+ function formatSkillList(skills) {
2700
+ if (skills.length === 0) return "No skills in your collection.";
2701
+ return skills.map((s) => {
2702
+ const emoji = s.metadata.emoji || "";
2703
+ return ` ${emoji ? emoji + " " : ""}${s.name} (${s.version}) [${s.source}]
2704
+ ${s.description}`;
2705
+ }).join("\n\n");
2706
+ }
2707
+
2708
+ // src/agent/skill-types.ts
2523
2709
  function parseDbMetadata(raw) {
2524
2710
  if (!raw || typeof raw !== "object") return {};
2525
2711
  const obj = raw;
@@ -2534,13 +2720,166 @@ function parseDbMetadata(raw) {
2534
2720
  credentials: openclaw.credentials
2535
2721
  };
2536
2722
  }
2723
+
2724
+ // src/agent/skill-db.ts
2725
+ async function upsertAgentSkill(params) {
2726
+ try {
2727
+ const data = await callMcpHandler("skill.upsert", {
2728
+ name: params.name,
2729
+ description: params.description,
2730
+ content: params.content,
2731
+ version: params.version,
2732
+ source: params.source || "manual",
2733
+ emoji: params.emoji || null,
2734
+ keywords: params.keywords || [],
2735
+ change_summary: params.changeSummary || null,
2736
+ source_skill_id: params.sourceSkillId || null
2737
+ });
2738
+ if (data && typeof data === "object" && "id" in data) {
2739
+ log.debug(`Skill "${params.name}" synced to agent_skills`);
2740
+ return data.id;
2741
+ }
2742
+ log.debug(`Skill "${params.name}" synced to agent_skills (no id returned)`);
2743
+ return null;
2744
+ } catch (err) {
2745
+ log.debug(`DB skill sync error for "${params.name}": ${err}`);
2746
+ return null;
2747
+ }
2748
+ }
2749
+ async function logSkillInvocation(skillDbId, options) {
2750
+ try {
2751
+ await callMcpHandler("skill.log_invocation", {
2752
+ skill_id: skillDbId,
2753
+ message_id: options?.messageId || null,
2754
+ session_id: options?.sessionId || null,
2755
+ task_prompt: options?.taskPrompt?.slice(0, 500) || null,
2756
+ arguments: options?.arguments || null,
2757
+ success: options?.success ?? null
2758
+ });
2759
+ } catch (err) {
2760
+ log.debug(`Invocation log error: ${err}`);
2761
+ }
2762
+ }
2763
+ async function searchSkillsInDb(query, limit) {
2764
+ try {
2765
+ const data = await callMcpHandler("skill.search", {
2766
+ query,
2767
+ limit
2768
+ });
2769
+ if (data) {
2770
+ return data.map((row) => ({
2771
+ name: String(row.name),
2772
+ description: String(row.description ?? ""),
2773
+ emoji: String(row.emoji ?? ""),
2774
+ source: String(row.source ?? "manual"),
2775
+ invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
2776
+ }));
2777
+ }
2778
+ return null;
2779
+ } catch {
2780
+ return null;
2781
+ }
2782
+ }
2783
+ async function discoverSkills(query, limit, excludeNames) {
2784
+ try {
2785
+ const data = await callMcpHandler("skill.discover", {
2786
+ query,
2787
+ limit,
2788
+ exclude_names: excludeNames
2789
+ });
2790
+ return data || null;
2791
+ } catch {
2792
+ return null;
2793
+ }
2794
+ }
2795
+ async function removeSkillFromDb(name) {
2796
+ try {
2797
+ await callMcpHandler("skill.remove", { name });
2798
+ } catch (err) {
2799
+ log.debug(`Failed to remove skill "${name}" from DB: ${err}`);
2800
+ }
2801
+ }
2802
+ async function updateSourceSkill(sourceSkillId, content, description, version) {
2803
+ try {
2804
+ await callMcpHandler("skill.update_source", {
2805
+ source_skill_id: sourceSkillId,
2806
+ content,
2807
+ description,
2808
+ version
2809
+ });
2810
+ } catch (err) {
2811
+ log.debug(`Failed to update source skill "${sourceSkillId}": ${err}`);
2812
+ }
2813
+ }
2814
+
2815
+ // src/agent/skill-marketplace.ts
2816
+ async function publishSkill(skill, options) {
2817
+ if (skill.source === "external") {
2818
+ log.debug(`Cannot publish external skill "${skill.name}"`);
2819
+ return null;
2820
+ }
2821
+ try {
2822
+ const data = await callMcpHandler("skill.publish", {
2823
+ name: skill.name,
2824
+ description: skill.description,
2825
+ version: skill.version,
2826
+ emoji: skill.metadata.emoji || null,
2827
+ content: skill.content,
2828
+ argument_hint: skill.argumentHint || null,
2829
+ keywords: skill.keywords,
2830
+ allowed_tools: skill.allowedTools,
2831
+ author_name: options?.authorName || null,
2832
+ metadata: skill.metadata,
2833
+ homepage: skill.homepage || null,
2834
+ category: options?.category || null,
2835
+ source: skill.source
2836
+ });
2837
+ log.info(`Skill "${skill.name}" published to marketplace`);
2838
+ return data;
2839
+ } catch (err) {
2840
+ log.debug(`Publish error: ${err}`);
2841
+ return null;
2842
+ }
2843
+ }
2844
+ async function browseSkills(options) {
2845
+ try {
2846
+ const data = await callMcpHandler("skill.browse", {
2847
+ query: options?.query || null,
2848
+ category: options?.category || null,
2849
+ sort: options?.sort || "popular",
2850
+ limit: options?.limit || 20,
2851
+ offset: options?.offset || 0
2852
+ });
2853
+ return (data || []).map((r) => safeParse(BrowseSkillRowSchema, r)).filter(Boolean).map((r) => ({
2854
+ id: r.id,
2855
+ name: r.name,
2856
+ description: r.description,
2857
+ emoji: r.emoji,
2858
+ version: r.version,
2859
+ authorName: r.author_name,
2860
+ category: r.category,
2861
+ installCount: r.install_count,
2862
+ avgRating: r.avg_rating ?? null,
2863
+ ratingCount: r.rating_count
2864
+ }));
2865
+ } catch {
2866
+ return [];
2867
+ }
2868
+ }
2869
+
2870
+ // src/agent/skills.ts
2871
+ var MAX_RELEVANCE_CACHE_ENTRIES = 100;
2872
+ var DISCOVER_TIMEOUT_MS = 3e3;
2873
+ var DISCOVER_RELEVANCE_THRESHOLD = 5;
2874
+ var DISCOVER_CACHE_TTL_MS = 6e4;
2537
2875
  var SkillManager = class {
2538
2876
  skills = /* @__PURE__ */ new Map();
2539
2877
  idfCache = /* @__PURE__ */ new Map();
2540
2878
  userId = null;
2541
- /** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
2542
- relevanceCache = /* @__PURE__ */ new Map();
2543
- DESCRIPTION_BUDGET_CHARS = SKILL_DESCRIPTION_BUDGET_CHARS;
2879
+ /** Bounded cache for findRelevant() — keyed by normalized prompt, invalidated on skill changes. */
2880
+ relevanceCache = new LRUCache(MAX_RELEVANCE_CACHE_ENTRIES);
2881
+ /** Bounded, TTL-aware cache for marketplace discover results. */
2882
+ discoverCache = new LRUCache(50);
2544
2883
  setUserId(userId) {
2545
2884
  this.userId = userId;
2546
2885
  }
@@ -2552,10 +2891,9 @@ var SkillManager = class {
2552
2891
  for (const raw of data || []) {
2553
2892
  const row = safeParse(SkillRowSchema, raw);
2554
2893
  if (!row) continue;
2555
- const skill = this.rowToSkill(row);
2556
- this.skills.set(skill.name, skill);
2894
+ this.skills.set(row.name, this.rowToSkill(row));
2557
2895
  }
2558
- this.buildIdfCache();
2896
+ this.rebuildIdfCache();
2559
2897
  if (this.skills.size > 0) {
2560
2898
  log.info(`Loaded ${this.skills.size} skill(s) from DB`);
2561
2899
  }
@@ -2565,44 +2903,33 @@ var SkillManager = class {
2565
2903
  }
2566
2904
  rowToSkill(row) {
2567
2905
  return {
2568
- name: String(row.name),
2569
- description: String(row.description ?? ""),
2570
- version: String(row.version ?? "1.0.0"),
2571
- userInvocable: row.user_invocable !== false,
2572
- disableModelInvocation: row.disable_model_invocation === true,
2573
- keywords: Array.isArray(row.keywords) ? row.keywords : [],
2574
- allowedTools: Array.isArray(row.allowed_tools) ? row.allowed_tools : [],
2575
- argumentHint: String(row.argument_hint ?? ""),
2906
+ name: row.name,
2907
+ description: row.description,
2908
+ version: row.version,
2909
+ userInvocable: row.user_invocable,
2910
+ disableModelInvocation: row.disable_model_invocation,
2911
+ keywords: row.keywords,
2912
+ allowedTools: row.allowed_tools,
2913
+ argumentHint: row.argument_hint,
2576
2914
  metadata: parseDbMetadata(row.metadata),
2577
- homepage: String(row.homepage ?? ""),
2578
- content: String(row.content ?? ""),
2915
+ homepage: row.homepage,
2916
+ content: row.content,
2579
2917
  filePath: "",
2580
2918
  source: row.source || "manual",
2581
- dbId: row.id != null ? String(row.id) : void 0,
2582
- sourceSkillId: row.source_skill_id != null ? String(row.source_skill_id) : void 0,
2583
- invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
2919
+ dbId: row.id,
2920
+ sourceSkillId: row.source_skill_id ?? void 0,
2921
+ invocationCount: row.invocation_count
2584
2922
  };
2585
2923
  }
2586
- /** Invalidate caches when skills change (create, add, update, remove). */
2587
2924
  invalidateCaches() {
2588
2925
  this.relevanceCache.clear();
2589
- this.buildIdfCache();
2590
- }
2591
- buildIdfCache() {
2592
- this.idfCache.clear();
2593
- const docFreq = /* @__PURE__ */ new Map();
2594
- const totalSkills = this.skills.size || 1;
2595
- for (const skill of this.skills.values()) {
2596
- const allText = `${skill.name} ${skill.description} ${skill.content} ${skill.keywords.join(" ")}`.toLowerCase();
2597
- const words = new Set(tokenize(allText));
2598
- for (const w of words) {
2599
- docFreq.set(w, (docFreq.get(w) || 0) + 1);
2600
- }
2601
- }
2602
- for (const [word, df] of docFreq) {
2603
- this.idfCache.set(word, Math.log(totalSkills / df) + 1);
2604
- }
2926
+ this.discoverCache.clear();
2927
+ this.rebuildIdfCache();
2928
+ }
2929
+ rebuildIdfCache() {
2930
+ this.idfCache = buildIdfMap(this.skills.values());
2605
2931
  }
2932
+ // ── Read ────────────────────────────────────────────────────────
2606
2933
  getAll() {
2607
2934
  return Array.from(this.skills.values());
2608
2935
  }
@@ -2610,89 +2937,83 @@ var SkillManager = class {
2610
2937
  return this.skills.get(name);
2611
2938
  }
2612
2939
  findRelevant(prompt, maxResults = 3) {
2940
+ return this.findRelevantWithScore(prompt, maxResults).results;
2941
+ }
2942
+ /**
2943
+ * Find relevant skills and return both results and the top score.
2944
+ * Avoids duplicate scoring when both relevance results and the max score are needed.
2945
+ */
2946
+ findRelevantWithScore(prompt, maxResults = 3) {
2613
2947
  const cacheKey = prompt.toLowerCase();
2614
2948
  const cached = this.relevanceCache.get(cacheKey);
2615
2949
  if (cached && cached.maxResults >= maxResults) {
2616
- return cached.results.slice(0, maxResults);
2617
- }
2618
- const lower = cacheKey;
2619
- const promptTokens = tokenize(lower);
2620
- const promptTokenSet = new Set(promptTokens);
2621
- const idf = (word) => this.idfCache.get(word) || 1;
2622
- const scored = [];
2623
- for (const skill of this.skills.values()) {
2624
- if (skill.disableModelInvocation) continue;
2625
- let score = 0;
2626
- if (lower.includes(skill.name.toLowerCase())) score += 10;
2627
- for (const kw of skill.keywords) {
2628
- if (lower.includes(kw.toLowerCase())) score += 8;
2629
- }
2630
- const descTokens = tokenize(skill.description.toLowerCase());
2631
- for (const word of descTokens) {
2632
- if (promptTokenSet.has(word)) score += 3 * idf(word);
2633
- }
2634
- const contentTokens = tokenize(skill.content.toLowerCase());
2635
- for (const word of contentTokens) {
2636
- if (promptTokenSet.has(word)) score += 0.5 * idf(word);
2637
- }
2638
- const promptBigrams = bigrams(promptTokens);
2639
- const descBigrams = bigrams(descTokens);
2640
- for (const bg of descBigrams) {
2641
- if (promptBigrams.has(bg)) score += 5;
2642
- }
2643
- if (score > 0) scored.push({ skill, score });
2950
+ return {
2951
+ results: cached.results.slice(0, maxResults),
2952
+ topScore: cached.topScore
2953
+ };
2644
2954
  }
2645
- const results = scored.sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => s.skill);
2646
- this.relevanceCache.set(cacheKey, { results, maxResults });
2647
- return results;
2955
+ const ctx = createSearchContext(prompt, this.idfCache);
2956
+ const scored = Array.from(this.skills.values()).map((skill) => ({ skill, score: scoreSkillRelevance(skill, ctx) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score);
2957
+ const topScore = scored.length > 0 ? scored[0].score : 0;
2958
+ const results = scored.slice(0, maxResults).map(({ skill }) => skill);
2959
+ this.relevanceCache.set(cacheKey, { results, maxResults, topScore });
2960
+ return { results, topScore };
2648
2961
  }
2649
- /**
2650
- * Build lightweight skill descriptions for the system prompt.
2651
- * When a taskPrompt is provided, relevant skills are prioritized to the top;
2652
- * remaining skills are sorted by usage frequency (invocationCount).
2653
- */
2654
- buildSkillDescriptions(taskPrompt) {
2655
- const all = this.getAll().filter((s) => !s.disableModelInvocation);
2656
- if (all.length === 0) return "";
2657
- const alwaysSkills = all.filter((s) => s.metadata.always);
2658
- const rest = all.filter((s) => !s.metadata.always);
2962
+ findSimilar(name) {
2963
+ if (this.skills.has(name)) return this.skills.get(name);
2964
+ return findSimilarSkill(name, this.skills);
2965
+ }
2966
+ async buildSkillDescriptions(taskPrompt) {
2967
+ const all = this.getAll();
2968
+ const hasActive = all.some((s) => !s.disableModelInvocation);
2659
2969
  let relevantNames = null;
2970
+ let topScore = 0;
2660
2971
  if (taskPrompt) {
2661
- const relevant = this.findRelevant(taskPrompt, 10);
2662
- relevantNames = new Set(relevant.map((s) => s.name));
2663
- }
2664
- const sorted = rest.sort((a, b) => {
2665
- if (relevantNames) {
2666
- const aRelevant = relevantNames.has(a.name);
2667
- const bRelevant = relevantNames.has(b.name);
2668
- if (aRelevant && !bRelevant) return -1;
2669
- if (!aRelevant && bRelevant) return 1;
2972
+ const { results, topScore: ts } = this.findRelevantWithScore(taskPrompt, 10);
2973
+ relevantNames = new Set(results.map((s) => s.name));
2974
+ topScore = ts;
2975
+ }
2976
+ let prompt = "";
2977
+ if (hasActive) {
2978
+ prompt += formatSkillDescriptions(all, relevantNames, SKILL_DESCRIPTION_BUDGET_CHARS);
2979
+ }
2980
+ if (taskPrompt && this.userId && topScore < DISCOVER_RELEVANCE_THRESHOLD) {
2981
+ const discovered = await this.discoverWithTimeout(
2982
+ taskPrompt,
2983
+ 5,
2984
+ all.map((s) => s.name)
2985
+ );
2986
+ if (discovered && discovered.length > 0) {
2987
+ prompt += formatDiscoveredSkills(discovered);
2670
2988
  }
2671
- return (b.invocationCount || 0) - (a.invocationCount || 0);
2672
- });
2673
- const skills = [...alwaysSkills, ...sorted];
2674
- let budget = this.DESCRIPTION_BUDGET_CHARS;
2675
- let prompt = "\n\n## Your Skills\n";
2676
- prompt += "These are your approved skills. Use skill_invoke to load full instructions when a task matches.\n";
2677
- prompt += "If no skill matches but the task is a reusable pattern, consider creating one with skill_create.\n\n";
2678
- let included = 0;
2679
- for (const skill of skills) {
2680
- const emoji = skill.metadata.emoji || "";
2681
- const hint = skill.argumentHint ? ` (${skill.argumentHint})` : "";
2682
- const line = `- **${emoji ? emoji + " " : ""}${skill.name}**${hint}: ${skill.description}
2683
- `;
2684
- if (budget - line.length < 0) break;
2685
- budget -= line.length;
2686
- prompt += line;
2687
- included++;
2688
- }
2689
- if (included < skills.length) {
2690
- prompt += `
2691
- _(${skills.length - included} additional skills available \u2014 use skill_search to find more)_
2692
- `;
2693
2989
  }
2694
2990
  return prompt;
2695
2991
  }
2992
+ /**
2993
+ * Discover marketplace skills with timeout and short-term caching.
2994
+ * Returns null on timeout or error (non-blocking).
2995
+ */
2996
+ async discoverWithTimeout(query, limit, excludeNames) {
2997
+ const cacheKey = query.toLowerCase().slice(0, 200);
2998
+ const cached = this.discoverCache.get(cacheKey);
2999
+ if (cached && Date.now() - cached.ts < DISCOVER_CACHE_TTL_MS) {
3000
+ return cached.results;
3001
+ }
3002
+ try {
3003
+ const result = await Promise.race([
3004
+ discoverSkills(query, limit, excludeNames),
3005
+ new Promise((resolve2) => setTimeout(() => resolve2(null), DISCOVER_TIMEOUT_MS))
3006
+ ]);
3007
+ this.discoverCache.set(cacheKey, { results: result || [], ts: Date.now() });
3008
+ return result;
3009
+ } catch {
3010
+ return null;
3011
+ }
3012
+ }
3013
+ listFormatted() {
3014
+ return formatSkillList(this.getAll());
3015
+ }
3016
+ // ── Create / Update / Remove ──────────────────────────────────
2696
3017
  async create(name, description, content, options) {
2697
3018
  if (!this.userId) return null;
2698
3019
  try {
@@ -2752,13 +3073,19 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2752
3073
  );
2753
3074
  const row = result.skill;
2754
3075
  const agentSkillRow = result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row;
2755
- const skill = this.rowToSkill({
3076
+ const merged = {
2756
3077
  ...agentSkillRow,
2757
3078
  name: row.name,
2758
3079
  description: row.description,
2759
3080
  content: row.content,
2760
3081
  source_skill_id: skillId
2761
- });
3082
+ };
3083
+ const parsed = safeParse(SkillRowSchema, merged);
3084
+ if (!parsed) {
3085
+ log.debug(`addSkill: failed to parse merged skill row for "${row.name}"`);
3086
+ return null;
3087
+ }
3088
+ const skill = this.rowToSkill(parsed);
2762
3089
  this.skills.set(skill.name, skill);
2763
3090
  this.invalidateCaches();
2764
3091
  log.info(`Skill "${row.name}" added to user's collection`);
@@ -2773,39 +3100,11 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2773
3100
  if (!skill) return false;
2774
3101
  this.skills.delete(name);
2775
3102
  this.invalidateCaches();
2776
- this.removeFromDb(name).catch(() => {
3103
+ removeSkillFromDb(name).catch((err) => {
3104
+ log.debug(`Failed to remove skill "${name}" from DB: ${err}`);
2777
3105
  });
2778
3106
  return true;
2779
3107
  }
2780
- listFormatted() {
2781
- const skills = this.getAll();
2782
- if (skills.length === 0) return "No skills in your collection.";
2783
- return skills.map((s) => {
2784
- const emoji = s.metadata.emoji || "";
2785
- return ` ${emoji ? emoji + " " : ""}${s.name} (${s.version}) [${s.source}]
2786
- ${s.description}`;
2787
- }).join("\n\n");
2788
- }
2789
- findSimilar(name) {
2790
- if (this.skills.has(name)) return this.skills.get(name);
2791
- const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
2792
- for (const [existingName, skill] of this.skills) {
2793
- const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
2794
- if (normalizedName.includes(normalizedExisting) || normalizedExisting.includes(normalizedName)) {
2795
- return skill;
2796
- }
2797
- const nameWords = new Set(name.split("-"));
2798
- const existingWords = new Set(existingName.split("-"));
2799
- let overlap = 0;
2800
- for (const w of nameWords) {
2801
- if (existingWords.has(w)) overlap++;
2802
- }
2803
- if (overlap >= 2 || overlap >= 1 && nameWords.size <= 2) {
2804
- return skill;
2805
- }
2806
- }
2807
- return null;
2808
- }
2809
3108
  update(name, newContent, description) {
2810
3109
  const skill = this.skills.get(name);
2811
3110
  if (!skill) return false;
@@ -2817,191 +3116,113 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2817
3116
  skill.description = newDescription;
2818
3117
  skill.version = newVersion;
2819
3118
  this.invalidateCaches();
2820
- this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
3119
+ upsertAgentSkill({
3120
+ name,
3121
+ description: newDescription,
3122
+ content: newContent,
3123
+ version: newVersion,
2821
3124
  source: "auto_improved"
2822
- }).catch(() => {
3125
+ }).catch((err) => {
3126
+ log.debug(`Failed to sync skill "${name}" update to DB: ${err}`);
2823
3127
  });
2824
3128
  if (skill.sourceSkillId && this.userId) {
2825
- callMcpHandler("skill.update_source", {
2826
- source_skill_id: skill.sourceSkillId,
2827
- content: newContent,
2828
- description: newDescription,
2829
- version: newVersion
2830
- }).catch(() => {
2831
- });
3129
+ updateSourceSkill(skill.sourceSkillId, newContent, newDescription, newVersion).catch(
3130
+ (err) => {
3131
+ log.debug(`Failed to update source skill for "${name}": ${err}`);
3132
+ }
3133
+ );
2832
3134
  }
2833
3135
  log.info(`Skill "${name}" updated to v${newVersion}`);
2834
3136
  return true;
2835
3137
  }
2836
- // ── DB Integration ─────────────────────────────────────────────────
3138
+ // ── DB Coordination ───────────────────────────────────────────
2837
3139
  async syncToAgentSkills(name, description, content, version, options) {
2838
3140
  if (!this.userId) return;
2839
- try {
2840
- const data = await callMcpHandler("skill.upsert", {
2841
- name,
2842
- description,
2843
- content,
2844
- version,
2845
- source: options?.source || "manual",
2846
- emoji: options?.emoji || null,
2847
- keywords: options?.keywords || [],
2848
- change_summary: options?.changeSummary || null,
2849
- source_skill_id: options?.sourceSkillId || null
2850
- });
3141
+ const id = await upsertAgentSkill({
3142
+ name,
3143
+ description,
3144
+ content,
3145
+ version,
3146
+ source: options?.source,
3147
+ emoji: options?.emoji,
3148
+ keywords: options?.keywords,
3149
+ changeSummary: options?.changeSummary,
3150
+ sourceSkillId: options?.sourceSkillId
3151
+ });
3152
+ if (id) {
2851
3153
  const skill = this.skills.get(name);
2852
- if (skill && data && typeof data === "object" && "id" in data) {
2853
- skill.dbId = data.id;
2854
- }
2855
- log.debug(`Skill "${name}" synced to agent_skills`);
2856
- } catch (err) {
2857
- log.debug(`DB skill sync error for "${name}": ${err}`);
3154
+ if (skill) skill.dbId = id;
2858
3155
  }
2859
3156
  }
2860
3157
  async logInvocation(skillName, options) {
2861
3158
  if (!this.userId) return;
2862
3159
  const skill = this.skills.get(skillName);
2863
- const skillDbId = skill?.dbId;
2864
- if (!skillDbId) {
3160
+ if (!skill?.dbId) {
2865
3161
  log.debug(`Cannot log invocation: skill "${skillName}" has no DB ID`);
2866
3162
  return;
2867
3163
  }
2868
- try {
2869
- await callMcpHandler("skill.log_invocation", {
2870
- skill_id: skillDbId,
2871
- message_id: options?.messageId || null,
2872
- session_id: options?.sessionId || null,
2873
- task_prompt: options?.taskPrompt?.slice(0, 500) || null,
2874
- arguments: options?.arguments || null,
2875
- success: options?.success ?? null
2876
- });
2877
- } catch (err) {
2878
- log.debug(`Invocation log error: ${err}`);
2879
- }
3164
+ await logSkillInvocation(skill.dbId, options);
2880
3165
  }
2881
3166
  async searchDb(query, limit = 10) {
3167
+ let agentResults = null;
2882
3168
  if (this.userId) {
2883
- try {
2884
- const data = await callMcpHandler("skill.search", {
2885
- query,
2886
- limit
2887
- });
2888
- if (data) {
2889
- return data.map((row) => ({
2890
- name: String(row.name),
2891
- description: String(row.description ?? ""),
2892
- emoji: String(row.emoji ?? ""),
2893
- source: String(row.source ?? "manual"),
2894
- invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0
2895
- }));
3169
+ agentResults = await searchSkillsInDb(query, limit);
3170
+ }
3171
+ if (!agentResults) {
3172
+ agentResults = this.findRelevant(query, limit).map((s) => ({
3173
+ name: s.name,
3174
+ description: s.description,
3175
+ emoji: s.metadata.emoji || "",
3176
+ source: s.source,
3177
+ invocationCount: 0
3178
+ }));
3179
+ }
3180
+ if (agentResults.length === 0 && this.userId) {
3181
+ const discovered = await discoverSkills(query, limit, []);
3182
+ if (discovered && discovered.length > 0) {
3183
+ for (const d of discovered) {
3184
+ agentResults.push({
3185
+ name: d.name,
3186
+ description: d.description || "",
3187
+ emoji: d.emoji || "",
3188
+ source: d.source || "external",
3189
+ invocationCount: 0,
3190
+ marketplaceId: d.id
3191
+ });
2896
3192
  }
2897
- } catch {
2898
3193
  }
2899
3194
  }
2900
- const results = this.findRelevant(query, limit);
2901
- return results.map((s) => ({
2902
- name: s.name,
2903
- description: s.description,
2904
- emoji: s.metadata.emoji || "",
2905
- source: s.source,
2906
- invocationCount: 0
2907
- }));
3195
+ return agentResults;
2908
3196
  }
2909
- async removeFromDb(name) {
2910
- if (!this.userId) return;
3197
+ // ── Marketplace Discovery ────────────────────────────────────
3198
+ /**
3199
+ * Preview a marketplace skill by name without adding it.
3200
+ * Returns the DiscoverResult if an exact match is found, so the caller
3201
+ * can show details to the user for confirmation before adding.
3202
+ */
3203
+ async previewMarketplaceSkill(name) {
3204
+ if (!this.userId) return null;
2911
3205
  try {
2912
- await callMcpHandler("skill.remove", { name });
2913
- } catch {
3206
+ const discovered = await discoverSkills(name, 5, []);
3207
+ if (!discovered || discovered.length === 0) return null;
3208
+ const match = discovered.find((d) => d.name === name);
3209
+ return match || null;
3210
+ } catch (err) {
3211
+ log.debug(`previewMarketplaceSkill error for "${name}": ${err}`);
3212
+ return null;
2914
3213
  }
2915
3214
  }
2916
- // ── Marketplace ────────────────────────────────────────────────────
3215
+ // ── Marketplace (delegated) ───────────────────────────────────
2917
3216
  async publish(name, options) {
2918
3217
  if (!this.userId) return null;
2919
3218
  const skill = this.skills.get(name);
2920
3219
  if (!skill) return null;
2921
- if (skill.source === "external") {
2922
- log.debug(`Cannot publish external skill "${name}"`);
2923
- return null;
2924
- }
2925
- try {
2926
- const data = await callMcpHandler("skill.publish", {
2927
- name: skill.name,
2928
- description: skill.description,
2929
- version: skill.version,
2930
- emoji: skill.metadata.emoji || null,
2931
- content: skill.content,
2932
- argument_hint: skill.argumentHint || null,
2933
- keywords: skill.keywords,
2934
- allowed_tools: skill.allowedTools,
2935
- author_name: options?.authorName || null,
2936
- metadata: skill.metadata,
2937
- homepage: skill.homepage || null,
2938
- category: options?.category || null,
2939
- source: skill.source
2940
- });
2941
- log.info(`Skill "${name}" published to marketplace`);
2942
- return data;
2943
- } catch (err) {
2944
- log.debug(`Publish error: ${err}`);
2945
- return null;
2946
- }
3220
+ return publishSkill(skill, options);
2947
3221
  }
2948
3222
  async browse(options) {
2949
- try {
2950
- const data = await callMcpHandler("skill.browse", {
2951
- query: options?.query || null,
2952
- category: options?.category || null,
2953
- sort: options?.sort || "popular",
2954
- limit: options?.limit || 20,
2955
- offset: options?.offset || 0
2956
- });
2957
- return (data || []).map((r) => safeParse(BrowseSkillRowSchema, r)).filter(Boolean).map((r) => ({
2958
- id: r.id,
2959
- name: r.name,
2960
- description: r.description,
2961
- emoji: r.emoji,
2962
- version: r.version,
2963
- authorName: r.author_name,
2964
- category: r.category,
2965
- installCount: r.install_count,
2966
- avgRating: r.avg_rating ?? null,
2967
- ratingCount: r.rating_count
2968
- }));
2969
- } catch {
2970
- return [];
2971
- }
3223
+ return browseSkills(options);
2972
3224
  }
2973
3225
  };
2974
- function validateSkillName(name) {
2975
- if (!name || name.length === 0) return "name is empty";
2976
- if (name.length > 64) return `name too long (${name.length}/64 chars)`;
2977
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
2978
- return `name must be lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens. Got: "${name}"`;
2979
- }
2980
- return null;
2981
- }
2982
- function normalizeSkillName(name) {
2983
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64);
2984
- }
2985
- function substituteArguments(content, args) {
2986
- const parts = args.split(/\s+/);
2987
- content = content.replace(/\$ARGUMENTS/g, args);
2988
- content = content.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, i) => parts[parseInt(i)] || "");
2989
- content = content.replace(/\$(\d+)(?!\w)/g, (_, i) => parts[parseInt(i)] || "");
2990
- return content;
2991
- }
2992
- var SAFE_DYNAMIC_COMMANDS = /^(date|whoami|hostname|uname|pwd|echo|node\s+--version|npm\s+--version|git\s+(branch|rev-parse|log\s+--oneline)|cat\s+)/;
2993
- function preprocessDynamicContext(content, cwd) {
2994
- return content.replace(/!`([^`]+)`/g, (_, cmd) => {
2995
- if (!SAFE_DYNAMIC_COMMANDS.test(cmd.trim())) {
2996
- return `[command blocked: ${cmd}]`;
2997
- }
2998
- try {
2999
- return execSync2(cmd, { timeout: 1e4, encoding: "utf-8", cwd }).trim();
3000
- } catch {
3001
- return `[command failed: ${cmd}]`;
3002
- }
3003
- });
3004
- }
3005
3226
 
3006
3227
  // src/credentials/credential-store.ts
3007
3228
  import { randomUUID } from "crypto";
@@ -3011,7 +3232,7 @@ import { dirname } from "path";
3011
3232
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
3012
3233
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3013
3234
  import { join as join2 } from "path";
3014
- import { homedir as homedir2, hostname, userInfo } from "os";
3235
+ import { hostname, userInfo, homedir as homedir2 } from "os";
3015
3236
  var ALGORITHM = "aes-256-gcm";
3016
3237
  var KEY_LENGTH = 32;
3017
3238
  var IV_LENGTH = 12;
@@ -3035,10 +3256,7 @@ function deriveKey(basePath) {
3035
3256
  function encrypt(plaintext, key) {
3036
3257
  const iv = randomBytes(IV_LENGTH);
3037
3258
  const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
3038
- const encrypted = Buffer.concat([
3039
- cipher.update(plaintext, "utf-8"),
3040
- cipher.final()
3041
- ]);
3259
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
3042
3260
  return {
3043
3261
  iv: iv.toString("base64"),
3044
3262
  data: encrypted.toString("base64"),
@@ -3051,28 +3269,23 @@ function decrypt(payload, key) {
3051
3269
  const tag = Buffer.from(payload.tag, "base64");
3052
3270
  const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
3053
3271
  decipher.setAuthTag(tag);
3054
- return Buffer.concat([
3055
- decipher.update(data),
3056
- decipher.final()
3057
- ]).toString("utf-8");
3272
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf-8");
3058
3273
  }
3059
3274
 
3060
3275
  // src/credentials/local-store.ts
3061
3276
  import Database from "better-sqlite3";
3062
3277
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
3063
3278
  import { join as join3 } from "path";
3064
- import { homedir as homedir3 } from "os";
3065
- var DEFAULT_DB_DIR = join3(homedir3(), ".config", "assistme");
3066
3279
  var DEFAULT_DB_NAME = "local.db";
3067
3280
  var LocalStore = class {
3068
3281
  db;
3069
3282
  dbPath;
3070
3283
  constructor(dbPath) {
3071
- const dir = dbPath ? dbPath : DEFAULT_DB_DIR;
3284
+ const dir = dbPath ? dbPath : getDataDir();
3072
3285
  if (!existsSync3(dir)) {
3073
3286
  mkdirSync3(dir, { recursive: true, mode: 448 });
3074
3287
  }
3075
- this.dbPath = dbPath ? join3(dbPath, DEFAULT_DB_NAME) : join3(DEFAULT_DB_DIR, DEFAULT_DB_NAME);
3288
+ this.dbPath = join3(dir, DEFAULT_DB_NAME);
3076
3289
  this.db = new Database(this.dbPath);
3077
3290
  this.db.pragma("journal_mode = WAL");
3078
3291
  this.db.pragma("foreign_keys = ON");
@@ -3273,8 +3486,72 @@ function getCredentialStore() {
3273
3486
  return _instance2;
3274
3487
  }
3275
3488
 
3489
+ // src/agent/sdk-stream.ts
3490
+ async function consumeSDKStream(stream, handlers) {
3491
+ let response = "";
3492
+ let sessionId;
3493
+ let tokenUsage;
3494
+ let costUsd;
3495
+ let numTurns;
3496
+ let structuredOutput;
3497
+ const errors = [];
3498
+ for await (const message of stream) {
3499
+ const msg = message;
3500
+ switch (msg.type) {
3501
+ case "assistant": {
3502
+ const assistantMsg = message;
3503
+ for (const block of assistantMsg.message.content) {
3504
+ if (block.type === "text") {
3505
+ response += block.text;
3506
+ if (handlers?.onText) await handlers.onText(block.text);
3507
+ } else if (block.type === "thinking" && "thinking" in block) {
3508
+ const thinkingText = block.thinking;
3509
+ if (handlers?.onThinking) await handlers.onThinking(thinkingText);
3510
+ }
3511
+ }
3512
+ break;
3513
+ }
3514
+ case "result": {
3515
+ const resultMsg = message;
3516
+ tokenUsage = {
3517
+ input_tokens: resultMsg.usage.input_tokens,
3518
+ output_tokens: resultMsg.usage.output_tokens
3519
+ };
3520
+ if (resultMsg.subtype === "success") {
3521
+ const successMsg = resultMsg;
3522
+ if (!response && successMsg.result) {
3523
+ response = successMsg.result;
3524
+ }
3525
+ sessionId = successMsg.session_id;
3526
+ costUsd = successMsg.total_cost_usd;
3527
+ numTurns = successMsg.num_turns;
3528
+ structuredOutput = successMsg.structured_output;
3529
+ } else {
3530
+ const errMsg = resultMsg;
3531
+ for (const err of errMsg.errors) {
3532
+ errors.push(err);
3533
+ if (handlers?.onError) await handlers.onError(err);
3534
+ }
3535
+ }
3536
+ break;
3537
+ }
3538
+ default: {
3539
+ if (msg.type === "system" && "subtype" in msg) {
3540
+ const sysMsg = msg;
3541
+ if (sysMsg.subtype === "init" && sysMsg.session_id) {
3542
+ sessionId = sysMsg.session_id;
3543
+ }
3544
+ }
3545
+ break;
3546
+ }
3547
+ }
3548
+ }
3549
+ return { response, sessionId, tokenUsage, costUsd, numTurns, structuredOutput, errors };
3550
+ }
3551
+
3276
3552
  // src/tools/shell.ts
3277
3553
  import { exec } from "child_process";
3554
+ import { resolve, relative, sep } from "path";
3278
3555
  var BLOCKED_PATTERNS = [
3279
3556
  /rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
3280
3557
  // rm -rf /, rm -fr /, etc.
@@ -3297,16 +3574,54 @@ var BLOCKED_PATTERNS = [
3297
3574
  /\bsystemctl\s+(start|stop|disable|mask)\b/i
3298
3575
  // dangerous systemctl ops
3299
3576
  ];
3577
+ var SHELL_WRAPPER_PATTERNS = [
3578
+ /\b(?:bash|sh|zsh|dash|ksh|csh)\s+(?:-\w+\s+)*-c\s+/i,
3579
+ // bash -c "...", sh -c "..."
3580
+ /\beval\s+/i,
3581
+ // eval "..."
3582
+ /\bexec\s+/i,
3583
+ // exec "..."
3584
+ /\bsource\s+\/dev\/stdin/i
3585
+ // source /dev/stdin <<< "..."
3586
+ ];
3587
+ function extractInnerCommand(command) {
3588
+ for (const pattern of SHELL_WRAPPER_PATTERNS) {
3589
+ const match = command.match(pattern);
3590
+ if (match) {
3591
+ const rest = command.slice(match.index + match[0].length);
3592
+ const trimmed = rest.trim();
3593
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
3594
+ return trimmed.slice(1, -1);
3595
+ }
3596
+ return trimmed;
3597
+ }
3598
+ }
3599
+ return null;
3600
+ }
3300
3601
  function isBlocked(command) {
3301
- return BLOCKED_PATTERNS.some((pattern) => pattern.test(command));
3602
+ if (BLOCKED_PATTERNS.some((pattern) => pattern.test(command))) {
3603
+ return true;
3604
+ }
3605
+ const inner = extractInnerCommand(command);
3606
+ if (inner && isBlocked(inner)) {
3607
+ return true;
3608
+ }
3609
+ return false;
3302
3610
  }
3303
3611
  async function executeShell(command, cwd) {
3304
3612
  if (isBlocked(command)) {
3305
3613
  throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
3306
3614
  }
3307
3615
  const config = getConfig();
3308
- const workDir = cwd || config.workspacePath;
3309
- return new Promise((resolve) => {
3616
+ const workDir = cwd ? resolve(cwd) : config.workspacePath;
3617
+ const rel = relative(config.workspacePath, workDir);
3618
+ if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
3619
+ throw new AppError(
3620
+ `Access denied: cwd "${cwd}" is outside workspace "${config.workspacePath}"`,
3621
+ "PATH_TRAVERSAL"
3622
+ );
3623
+ }
3624
+ return new Promise((resolve2) => {
3310
3625
  exec(
3311
3626
  command,
3312
3627
  {
@@ -3334,7 +3649,7 @@ ${stderr}` : "";
3334
3649
 
3335
3650
  [Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
3336
3651
  }
3337
- resolve(output || "(no output)");
3652
+ resolve2(output || "(no output)");
3338
3653
  }
3339
3654
  );
3340
3655
  });
@@ -3371,13 +3686,11 @@ export {
3371
3686
  listScheduledTasks,
3372
3687
  toggleScheduledTask,
3373
3688
  deleteScheduledTask,
3689
+ consumeSDKStream,
3374
3690
  executeShell,
3375
3691
  MemoryManager,
3692
+ LRUCache,
3376
3693
  SkillManager,
3377
- validateSkillName,
3378
- normalizeSkillName,
3379
- substituteArguments,
3380
- preprocessDynamicContext,
3381
3694
  getLocalStore,
3382
3695
  getCredentialStore
3383
3696
  };