assistme 0.8.2 → 0.8.4

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-R7A3MKYO.js} +645 -329
  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 +535 -316
  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 +270 -568
  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 +132 -103
  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();
2605
2928
  }
2929
+ rebuildIdfCache() {
2930
+ this.idfCache = buildIdfMap(this.skills.values());
2931
+ }
2932
+ // ── Read ────────────────────────────────────────────────────────
2606
2933
  getAll() {
2607
2934
  return Array.from(this.skills.values());
2608
2935
  }
@@ -2610,91 +2937,88 @@ 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
- if (!this.userId) return null;
3018
+ if (!this.userId) {
3019
+ log.warn(`Skill create skipped for "${name}": no userId set`);
3020
+ return null;
3021
+ }
2698
3022
  try {
2699
3023
  const metadata = options?.emoji ? { openclaw: { emoji: options.emoji } } : {};
2700
3024
  const data = await callMcpHandler(
@@ -2739,7 +3063,7 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2739
3063
  log.info(`Skill "${skillName}" created in skills table (pending approval)`);
2740
3064
  return { id, name: skillName };
2741
3065
  } catch (err) {
2742
- log.debug(`Skill create error: ${err}`);
3066
+ log.warn(`Skill create error for "${name}": ${err instanceof Error ? err.message : err}`);
2743
3067
  return null;
2744
3068
  }
2745
3069
  }
@@ -2752,13 +3076,19 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2752
3076
  );
2753
3077
  const row = result.skill;
2754
3078
  const agentSkillRow = result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row;
2755
- const skill = this.rowToSkill({
3079
+ const merged = {
2756
3080
  ...agentSkillRow,
2757
3081
  name: row.name,
2758
3082
  description: row.description,
2759
3083
  content: row.content,
2760
3084
  source_skill_id: skillId
2761
- });
3085
+ };
3086
+ const parsed = safeParse(SkillRowSchema, merged);
3087
+ if (!parsed) {
3088
+ log.debug(`addSkill: failed to parse merged skill row for "${row.name}"`);
3089
+ return null;
3090
+ }
3091
+ const skill = this.rowToSkill(parsed);
2762
3092
  this.skills.set(skill.name, skill);
2763
3093
  this.invalidateCaches();
2764
3094
  log.info(`Skill "${row.name}" added to user's collection`);
@@ -2773,39 +3103,11 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2773
3103
  if (!skill) return false;
2774
3104
  this.skills.delete(name);
2775
3105
  this.invalidateCaches();
2776
- this.removeFromDb(name).catch(() => {
3106
+ removeSkillFromDb(name).catch((err) => {
3107
+ log.debug(`Failed to remove skill "${name}" from DB: ${err}`);
2777
3108
  });
2778
3109
  return true;
2779
3110
  }
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
3111
  update(name, newContent, description) {
2810
3112
  const skill = this.skills.get(name);
2811
3113
  if (!skill) return false;
@@ -2817,191 +3119,113 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
2817
3119
  skill.description = newDescription;
2818
3120
  skill.version = newVersion;
2819
3121
  this.invalidateCaches();
2820
- this.syncToAgentSkills(name, newDescription, newContent, newVersion, {
3122
+ upsertAgentSkill({
3123
+ name,
3124
+ description: newDescription,
3125
+ content: newContent,
3126
+ version: newVersion,
2821
3127
  source: "auto_improved"
2822
- }).catch(() => {
3128
+ }).catch((err) => {
3129
+ log.debug(`Failed to sync skill "${name}" update to DB: ${err}`);
2823
3130
  });
2824
3131
  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
- });
3132
+ updateSourceSkill(skill.sourceSkillId, newContent, newDescription, newVersion).catch(
3133
+ (err) => {
3134
+ log.debug(`Failed to update source skill for "${name}": ${err}`);
3135
+ }
3136
+ );
2832
3137
  }
2833
3138
  log.info(`Skill "${name}" updated to v${newVersion}`);
2834
3139
  return true;
2835
3140
  }
2836
- // ── DB Integration ─────────────────────────────────────────────────
3141
+ // ── DB Coordination ───────────────────────────────────────────
2837
3142
  async syncToAgentSkills(name, description, content, version, options) {
2838
3143
  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
- });
3144
+ const id = await upsertAgentSkill({
3145
+ name,
3146
+ description,
3147
+ content,
3148
+ version,
3149
+ source: options?.source,
3150
+ emoji: options?.emoji,
3151
+ keywords: options?.keywords,
3152
+ changeSummary: options?.changeSummary,
3153
+ sourceSkillId: options?.sourceSkillId
3154
+ });
3155
+ if (id) {
2851
3156
  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}`);
3157
+ if (skill) skill.dbId = id;
2858
3158
  }
2859
3159
  }
2860
3160
  async logInvocation(skillName, options) {
2861
3161
  if (!this.userId) return;
2862
3162
  const skill = this.skills.get(skillName);
2863
- const skillDbId = skill?.dbId;
2864
- if (!skillDbId) {
3163
+ if (!skill?.dbId) {
2865
3164
  log.debug(`Cannot log invocation: skill "${skillName}" has no DB ID`);
2866
3165
  return;
2867
3166
  }
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
- }
3167
+ await logSkillInvocation(skill.dbId, options);
2880
3168
  }
2881
3169
  async searchDb(query, limit = 10) {
3170
+ let agentResults = null;
2882
3171
  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
- }));
3172
+ agentResults = await searchSkillsInDb(query, limit);
3173
+ }
3174
+ if (!agentResults) {
3175
+ agentResults = this.findRelevant(query, limit).map((s) => ({
3176
+ name: s.name,
3177
+ description: s.description,
3178
+ emoji: s.metadata.emoji || "",
3179
+ source: s.source,
3180
+ invocationCount: 0
3181
+ }));
3182
+ }
3183
+ if (agentResults.length === 0 && this.userId) {
3184
+ const discovered = await discoverSkills(query, limit, []);
3185
+ if (discovered && discovered.length > 0) {
3186
+ for (const d of discovered) {
3187
+ agentResults.push({
3188
+ name: d.name,
3189
+ description: d.description || "",
3190
+ emoji: d.emoji || "",
3191
+ source: d.source || "external",
3192
+ invocationCount: 0,
3193
+ marketplaceId: d.id
3194
+ });
2896
3195
  }
2897
- } catch {
2898
3196
  }
2899
3197
  }
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
- }));
3198
+ return agentResults;
2908
3199
  }
2909
- async removeFromDb(name) {
2910
- if (!this.userId) return;
3200
+ // ── Marketplace Discovery ────────────────────────────────────
3201
+ /**
3202
+ * Preview a marketplace skill by name without adding it.
3203
+ * Returns the DiscoverResult if an exact match is found, so the caller
3204
+ * can show details to the user for confirmation before adding.
3205
+ */
3206
+ async previewMarketplaceSkill(name) {
3207
+ if (!this.userId) return null;
2911
3208
  try {
2912
- await callMcpHandler("skill.remove", { name });
2913
- } catch {
3209
+ const discovered = await discoverSkills(name, 5, []);
3210
+ if (!discovered || discovered.length === 0) return null;
3211
+ const match = discovered.find((d) => d.name === name);
3212
+ return match || null;
3213
+ } catch (err) {
3214
+ log.debug(`previewMarketplaceSkill error for "${name}": ${err}`);
3215
+ return null;
2914
3216
  }
2915
3217
  }
2916
- // ── Marketplace ────────────────────────────────────────────────────
3218
+ // ── Marketplace (delegated) ───────────────────────────────────
2917
3219
  async publish(name, options) {
2918
3220
  if (!this.userId) return null;
2919
3221
  const skill = this.skills.get(name);
2920
3222
  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
- }
3223
+ return publishSkill(skill, options);
2947
3224
  }
2948
3225
  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
- }
3226
+ return browseSkills(options);
2972
3227
  }
2973
3228
  };
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
3229
 
3006
3230
  // src/credentials/credential-store.ts
3007
3231
  import { randomUUID } from "crypto";
@@ -3011,7 +3235,7 @@ import { dirname } from "path";
3011
3235
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
3012
3236
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
3013
3237
  import { join as join2 } from "path";
3014
- import { homedir as homedir2, hostname, userInfo } from "os";
3238
+ import { hostname, userInfo, homedir as homedir2 } from "os";
3015
3239
  var ALGORITHM = "aes-256-gcm";
3016
3240
  var KEY_LENGTH = 32;
3017
3241
  var IV_LENGTH = 12;
@@ -3035,10 +3259,7 @@ function deriveKey(basePath) {
3035
3259
  function encrypt(plaintext, key) {
3036
3260
  const iv = randomBytes(IV_LENGTH);
3037
3261
  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
- ]);
3262
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
3042
3263
  return {
3043
3264
  iv: iv.toString("base64"),
3044
3265
  data: encrypted.toString("base64"),
@@ -3051,28 +3272,23 @@ function decrypt(payload, key) {
3051
3272
  const tag = Buffer.from(payload.tag, "base64");
3052
3273
  const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
3053
3274
  decipher.setAuthTag(tag);
3054
- return Buffer.concat([
3055
- decipher.update(data),
3056
- decipher.final()
3057
- ]).toString("utf-8");
3275
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf-8");
3058
3276
  }
3059
3277
 
3060
3278
  // src/credentials/local-store.ts
3061
3279
  import Database from "better-sqlite3";
3062
3280
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
3063
3281
  import { join as join3 } from "path";
3064
- import { homedir as homedir3 } from "os";
3065
- var DEFAULT_DB_DIR = join3(homedir3(), ".config", "assistme");
3066
3282
  var DEFAULT_DB_NAME = "local.db";
3067
3283
  var LocalStore = class {
3068
3284
  db;
3069
3285
  dbPath;
3070
3286
  constructor(dbPath) {
3071
- const dir = dbPath ? dbPath : DEFAULT_DB_DIR;
3287
+ const dir = dbPath ? dbPath : getDataDir();
3072
3288
  if (!existsSync3(dir)) {
3073
3289
  mkdirSync3(dir, { recursive: true, mode: 448 });
3074
3290
  }
3075
- this.dbPath = dbPath ? join3(dbPath, DEFAULT_DB_NAME) : join3(DEFAULT_DB_DIR, DEFAULT_DB_NAME);
3291
+ this.dbPath = join3(dir, DEFAULT_DB_NAME);
3076
3292
  this.db = new Database(this.dbPath);
3077
3293
  this.db.pragma("journal_mode = WAL");
3078
3294
  this.db.pragma("foreign_keys = ON");
@@ -3273,8 +3489,72 @@ function getCredentialStore() {
3273
3489
  return _instance2;
3274
3490
  }
3275
3491
 
3492
+ // src/agent/sdk-stream.ts
3493
+ async function consumeSDKStream(stream, handlers) {
3494
+ let response = "";
3495
+ let sessionId;
3496
+ let tokenUsage;
3497
+ let costUsd;
3498
+ let numTurns;
3499
+ let structuredOutput;
3500
+ const errors = [];
3501
+ for await (const message of stream) {
3502
+ const msg = message;
3503
+ switch (msg.type) {
3504
+ case "assistant": {
3505
+ const assistantMsg = message;
3506
+ for (const block of assistantMsg.message.content) {
3507
+ if (block.type === "text") {
3508
+ response += block.text;
3509
+ if (handlers?.onText) await handlers.onText(block.text);
3510
+ } else if (block.type === "thinking" && "thinking" in block) {
3511
+ const thinkingText = block.thinking;
3512
+ if (handlers?.onThinking) await handlers.onThinking(thinkingText);
3513
+ }
3514
+ }
3515
+ break;
3516
+ }
3517
+ case "result": {
3518
+ const resultMsg = message;
3519
+ tokenUsage = {
3520
+ input_tokens: resultMsg.usage.input_tokens,
3521
+ output_tokens: resultMsg.usage.output_tokens
3522
+ };
3523
+ if (resultMsg.subtype === "success") {
3524
+ const successMsg = resultMsg;
3525
+ if (!response && successMsg.result) {
3526
+ response = successMsg.result;
3527
+ }
3528
+ sessionId = successMsg.session_id;
3529
+ costUsd = successMsg.total_cost_usd;
3530
+ numTurns = successMsg.num_turns;
3531
+ structuredOutput = successMsg.structured_output;
3532
+ } else {
3533
+ const errMsg = resultMsg;
3534
+ for (const err of errMsg.errors) {
3535
+ errors.push(err);
3536
+ if (handlers?.onError) await handlers.onError(err);
3537
+ }
3538
+ }
3539
+ break;
3540
+ }
3541
+ default: {
3542
+ if (msg.type === "system" && "subtype" in msg) {
3543
+ const sysMsg = msg;
3544
+ if (sysMsg.subtype === "init" && sysMsg.session_id) {
3545
+ sessionId = sysMsg.session_id;
3546
+ }
3547
+ }
3548
+ break;
3549
+ }
3550
+ }
3551
+ }
3552
+ return { response, sessionId, tokenUsage, costUsd, numTurns, structuredOutput, errors };
3553
+ }
3554
+
3276
3555
  // src/tools/shell.ts
3277
3556
  import { exec } from "child_process";
3557
+ import { resolve, relative, sep } from "path";
3278
3558
  var BLOCKED_PATTERNS = [
3279
3559
  /rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
3280
3560
  // rm -rf /, rm -fr /, etc.
@@ -3297,16 +3577,54 @@ var BLOCKED_PATTERNS = [
3297
3577
  /\bsystemctl\s+(start|stop|disable|mask)\b/i
3298
3578
  // dangerous systemctl ops
3299
3579
  ];
3580
+ var SHELL_WRAPPER_PATTERNS = [
3581
+ /\b(?:bash|sh|zsh|dash|ksh|csh)\s+(?:-\w+\s+)*-c\s+/i,
3582
+ // bash -c "...", sh -c "..."
3583
+ /\beval\s+/i,
3584
+ // eval "..."
3585
+ /\bexec\s+/i,
3586
+ // exec "..."
3587
+ /\bsource\s+\/dev\/stdin/i
3588
+ // source /dev/stdin <<< "..."
3589
+ ];
3590
+ function extractInnerCommand(command) {
3591
+ for (const pattern of SHELL_WRAPPER_PATTERNS) {
3592
+ const match = command.match(pattern);
3593
+ if (match) {
3594
+ const rest = command.slice(match.index + match[0].length);
3595
+ const trimmed = rest.trim();
3596
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
3597
+ return trimmed.slice(1, -1);
3598
+ }
3599
+ return trimmed;
3600
+ }
3601
+ }
3602
+ return null;
3603
+ }
3300
3604
  function isBlocked(command) {
3301
- return BLOCKED_PATTERNS.some((pattern) => pattern.test(command));
3605
+ if (BLOCKED_PATTERNS.some((pattern) => pattern.test(command))) {
3606
+ return true;
3607
+ }
3608
+ const inner = extractInnerCommand(command);
3609
+ if (inner && isBlocked(inner)) {
3610
+ return true;
3611
+ }
3612
+ return false;
3302
3613
  }
3303
3614
  async function executeShell(command, cwd) {
3304
3615
  if (isBlocked(command)) {
3305
3616
  throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
3306
3617
  }
3307
3618
  const config = getConfig();
3308
- const workDir = cwd || config.workspacePath;
3309
- return new Promise((resolve) => {
3619
+ const workDir = cwd ? resolve(cwd) : config.workspacePath;
3620
+ const rel = relative(config.workspacePath, workDir);
3621
+ if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
3622
+ throw new AppError(
3623
+ `Access denied: cwd "${cwd}" is outside workspace "${config.workspacePath}"`,
3624
+ "PATH_TRAVERSAL"
3625
+ );
3626
+ }
3627
+ return new Promise((resolve2) => {
3310
3628
  exec(
3311
3629
  command,
3312
3630
  {
@@ -3334,7 +3652,7 @@ ${stderr}` : "";
3334
3652
 
3335
3653
  [Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
3336
3654
  }
3337
- resolve(output || "(no output)");
3655
+ resolve2(output || "(no output)");
3338
3656
  }
3339
3657
  );
3340
3658
  });
@@ -3371,13 +3689,11 @@ export {
3371
3689
  listScheduledTasks,
3372
3690
  toggleScheduledTask,
3373
3691
  deleteScheduledTask,
3692
+ consumeSDKStream,
3374
3693
  executeShell,
3375
3694
  MemoryManager,
3695
+ LRUCache,
3376
3696
  SkillManager,
3377
- validateSkillName,
3378
- normalizeSkillName,
3379
- substituteArguments,
3380
- preprocessDynamicContext,
3381
3697
  getLocalStore,
3382
3698
  getCredentialStore
3383
3699
  };