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.
- package/dist/{chunk-A2NR7LCQ.js → chunk-3UNXN3BX.js} +60 -35
- package/dist/chunk-HY3FFXSQ.js +113 -0
- package/dist/{chunk-IKYXC4RJ.js → chunk-R7A3MKYO.js} +645 -329
- package/dist/config-2HH7PO34.js +20 -0
- package/dist/index.js +658 -788
- package/dist/job-runner-P4DIXXCV.js +7 -0
- package/dist/workers/entry.js +535 -316
- package/package.json +1 -1
- package/src/NAMING.md +34 -0
- package/src/agent/heartbeat-checks.ts +158 -0
- package/src/agent/heartbeat-compiler.ts +122 -0
- package/src/agent/heartbeat-types.ts +62 -0
- package/src/agent/job-analysis-poller.ts +96 -0
- package/src/agent/job-runner.ts +31 -0
- package/src/agent/proactive-monitor.ts +88 -410
- package/src/agent/processor.ts +147 -252
- package/src/agent/prompt-builder.ts +83 -0
- package/src/agent/scheduler.ts +1 -1
- package/src/agent/sdk-stream.ts +105 -0
- package/src/agent/self-analyzer.ts +21 -22
- package/src/agent/session-heartbeat.ts +34 -0
- package/src/agent/skill-db.ts +170 -0
- package/src/agent/skill-evaluator.ts +4 -13
- package/src/agent/skill-format.ts +96 -0
- package/src/agent/skill-marketplace.ts +98 -0
- package/src/agent/skill-search.ts +315 -0
- package/src/agent/skill-types.ts +68 -0
- package/src/agent/skill-utils.ts +66 -0
- package/src/agent/skills.ts +270 -568
- package/src/agent/system-prompt.ts +11 -8
- package/src/agent/task-poller.ts +101 -0
- package/src/agent/task-timeout.ts +50 -0
- package/src/browser/chrome-launcher.ts +6 -6
- package/src/browser/controller.ts +1 -2
- package/src/commands/auth.ts +4 -11
- package/src/commands/browser.ts +12 -47
- package/src/commands/credential.ts +2 -2
- package/src/commands/job.ts +3 -3
- package/src/credentials/encryption.ts +4 -10
- package/src/credentials/local-store.ts +4 -7
- package/src/credentials/program-store.ts +8 -10
- package/src/db/auth-store.ts +15 -13
- package/src/db/session-log.ts +12 -5
- package/src/mcp/agent-tools-server.ts +132 -103
- package/src/mcp/ask-user.ts +102 -0
- package/src/mcp/skill-confirmation.ts +109 -0
- package/src/orchestrator.ts +100 -350
- package/src/tools/filesystem.ts +48 -2
- package/src/tools/index.ts +1 -2
- package/src/tools/shell.ts +58 -2
- package/src/types/edsger-feedback.d.ts +18 -0
- package/src/utils/config.ts +79 -1
- package/src/utils/logger.ts +52 -12
- package/src/utils/lru-cache.ts +57 -0
- package/src/utils/schemas.ts +2 -0
- package/src/workers/base-handler.ts +7 -1
- package/src/workers/index.ts +2 -0
- package/src/workers/log-forwarder.ts +55 -0
- package/src/workers/manager.ts +82 -220
- package/src/workers/types.ts +9 -1
- package/src/workers/worker-lifecycle.ts +113 -0
- package/tests/agent/heartbeat-checks.test.ts +160 -0
- package/tests/agent/mcp-servers.test.ts +1 -1
- package/tests/agent/proactive-monitor.test.ts +42 -26
- package/tests/agent/processor.test.ts +2 -1
- package/tests/agent/sdk-stream.test.ts +181 -0
- package/tests/agent/self-analyzer.test.ts +106 -49
- package/tests/agent/session.test.ts +114 -132
- package/tests/agent/skill-format.test.ts +93 -0
- package/tests/agent/skill-search.test.ts +175 -0
- package/tests/agent/skill-utils.test.ts +86 -0
- package/tests/agent/skills.test.ts +4 -24
- package/tests/db/supabase.test.ts +3 -2
- package/tests/mcp/ask-user.test.ts +117 -0
- package/tests/mcp/skill-confirmation.integration.test.ts +216 -0
- package/tests/mcp/skill-confirmation.test.ts +66 -0
- package/tests/tools/filesystem.test.ts +38 -1
- package/tests/tools/shell.test.ts +43 -0
- package/tests/utils/config.test.ts +2 -2
- package/dist/chunk-YYSJHZSO.js +0 -47
- package/dist/config-3RWSAUAZ.js +0 -12
- package/dist/job-runner-PECVS424.js +0 -7
- 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-
|
|
20
|
+
} from "./chunk-3UNXN3BX.js";
|
|
23
21
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
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-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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/
|
|
2397
|
-
|
|
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
|
-
/**
|
|
2542
|
-
relevanceCache =
|
|
2543
|
-
|
|
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
|
-
|
|
2556
|
-
this.skills.set(skill.name, skill);
|
|
2894
|
+
this.skills.set(row.name, this.rowToSkill(row));
|
|
2557
2895
|
}
|
|
2558
|
-
this.
|
|
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:
|
|
2569
|
-
description:
|
|
2570
|
-
version:
|
|
2571
|
-
userInvocable: row.user_invocable
|
|
2572
|
-
disableModelInvocation: row.disable_model_invocation
|
|
2573
|
-
keywords:
|
|
2574
|
-
allowedTools:
|
|
2575
|
-
argumentHint:
|
|
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:
|
|
2578
|
-
content:
|
|
2915
|
+
homepage: row.homepage,
|
|
2916
|
+
content: row.content,
|
|
2579
2917
|
filePath: "",
|
|
2580
2918
|
source: row.source || "manual",
|
|
2581
|
-
dbId: row.id
|
|
2582
|
-
sourceSkillId: row.source_skill_id
|
|
2583
|
-
invocationCount:
|
|
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.
|
|
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
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
|
2646
|
-
this.
|
|
2647
|
-
|
|
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
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
const
|
|
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
|
|
2662
|
-
relevantNames = new Set(
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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)
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
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
|
|
3141
|
+
// ── DB Coordination ───────────────────────────────────────────
|
|
2837
3142
|
async syncToAgentSkills(name, description, content, version, options) {
|
|
2838
3143
|
if (!this.userId) return;
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2910
|
-
|
|
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
|
|
2913
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
3287
|
+
const dir = dbPath ? dbPath : getDataDir();
|
|
3072
3288
|
if (!existsSync3(dir)) {
|
|
3073
3289
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
3074
3290
|
}
|
|
3075
|
-
this.dbPath =
|
|
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
|
-
|
|
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
|
|
3309
|
-
|
|
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
|
-
|
|
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
|
};
|