assistme 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-A2NR7LCQ.js → chunk-3UNXN3BX.js} +60 -35
- package/dist/chunk-HY3FFXSQ.js +113 -0
- package/dist/{chunk-IKYXC4RJ.js → chunk-UHGTMSLZ.js} +640 -327
- 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 +529 -315
- 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 +265 -566
- 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 +126 -102
- 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
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
const totalSkills = this.skills.size || 1;
|
|
2595
|
-
for (const skill of this.skills.values()) {
|
|
2596
|
-
const allText = `${skill.name} ${skill.description} ${skill.content} ${skill.keywords.join(" ")}`.toLowerCase();
|
|
2597
|
-
const words = new Set(tokenize(allText));
|
|
2598
|
-
for (const w of words) {
|
|
2599
|
-
docFreq.set(w, (docFreq.get(w) || 0) + 1);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
for (const [word, df] of docFreq) {
|
|
2603
|
-
this.idfCache.set(word, Math.log(totalSkills / df) + 1);
|
|
2604
|
-
}
|
|
2926
|
+
this.discoverCache.clear();
|
|
2927
|
+
this.rebuildIdfCache();
|
|
2928
|
+
}
|
|
2929
|
+
rebuildIdfCache() {
|
|
2930
|
+
this.idfCache = buildIdfMap(this.skills.values());
|
|
2605
2931
|
}
|
|
2932
|
+
// ── Read ────────────────────────────────────────────────────────
|
|
2606
2933
|
getAll() {
|
|
2607
2934
|
return Array.from(this.skills.values());
|
|
2608
2935
|
}
|
|
@@ -2610,89 +2937,83 @@ var SkillManager = class {
|
|
|
2610
2937
|
return this.skills.get(name);
|
|
2611
2938
|
}
|
|
2612
2939
|
findRelevant(prompt, maxResults = 3) {
|
|
2940
|
+
return this.findRelevantWithScore(prompt, maxResults).results;
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Find relevant skills and return both results and the top score.
|
|
2944
|
+
* Avoids duplicate scoring when both relevance results and the max score are needed.
|
|
2945
|
+
*/
|
|
2946
|
+
findRelevantWithScore(prompt, maxResults = 3) {
|
|
2613
2947
|
const cacheKey = prompt.toLowerCase();
|
|
2614
2948
|
const cached = this.relevanceCache.get(cacheKey);
|
|
2615
2949
|
if (cached && cached.maxResults >= maxResults) {
|
|
2616
|
-
return
|
|
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
3018
|
if (!this.userId) return null;
|
|
2698
3019
|
try {
|
|
@@ -2752,13 +3073,19 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2752
3073
|
);
|
|
2753
3074
|
const row = result.skill;
|
|
2754
3075
|
const agentSkillRow = result.agent_skill && typeof result.agent_skill === "object" ? result.agent_skill : row;
|
|
2755
|
-
const
|
|
3076
|
+
const merged = {
|
|
2756
3077
|
...agentSkillRow,
|
|
2757
3078
|
name: row.name,
|
|
2758
3079
|
description: row.description,
|
|
2759
3080
|
content: row.content,
|
|
2760
3081
|
source_skill_id: skillId
|
|
2761
|
-
}
|
|
3082
|
+
};
|
|
3083
|
+
const parsed = safeParse(SkillRowSchema, merged);
|
|
3084
|
+
if (!parsed) {
|
|
3085
|
+
log.debug(`addSkill: failed to parse merged skill row for "${row.name}"`);
|
|
3086
|
+
return null;
|
|
3087
|
+
}
|
|
3088
|
+
const skill = this.rowToSkill(parsed);
|
|
2762
3089
|
this.skills.set(skill.name, skill);
|
|
2763
3090
|
this.invalidateCaches();
|
|
2764
3091
|
log.info(`Skill "${row.name}" added to user's collection`);
|
|
@@ -2773,39 +3100,11 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2773
3100
|
if (!skill) return false;
|
|
2774
3101
|
this.skills.delete(name);
|
|
2775
3102
|
this.invalidateCaches();
|
|
2776
|
-
|
|
3103
|
+
removeSkillFromDb(name).catch((err) => {
|
|
3104
|
+
log.debug(`Failed to remove skill "${name}" from DB: ${err}`);
|
|
2777
3105
|
});
|
|
2778
3106
|
return true;
|
|
2779
3107
|
}
|
|
2780
|
-
listFormatted() {
|
|
2781
|
-
const skills = this.getAll();
|
|
2782
|
-
if (skills.length === 0) return "No skills in your collection.";
|
|
2783
|
-
return skills.map((s) => {
|
|
2784
|
-
const emoji = s.metadata.emoji || "";
|
|
2785
|
-
return ` ${emoji ? emoji + " " : ""}${s.name} (${s.version}) [${s.source}]
|
|
2786
|
-
${s.description}`;
|
|
2787
|
-
}).join("\n\n");
|
|
2788
|
-
}
|
|
2789
|
-
findSimilar(name) {
|
|
2790
|
-
if (this.skills.has(name)) return this.skills.get(name);
|
|
2791
|
-
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2792
|
-
for (const [existingName, skill] of this.skills) {
|
|
2793
|
-
const normalizedExisting = existingName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2794
|
-
if (normalizedName.includes(normalizedExisting) || normalizedExisting.includes(normalizedName)) {
|
|
2795
|
-
return skill;
|
|
2796
|
-
}
|
|
2797
|
-
const nameWords = new Set(name.split("-"));
|
|
2798
|
-
const existingWords = new Set(existingName.split("-"));
|
|
2799
|
-
let overlap = 0;
|
|
2800
|
-
for (const w of nameWords) {
|
|
2801
|
-
if (existingWords.has(w)) overlap++;
|
|
2802
|
-
}
|
|
2803
|
-
if (overlap >= 2 || overlap >= 1 && nameWords.size <= 2) {
|
|
2804
|
-
return skill;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
return null;
|
|
2808
|
-
}
|
|
2809
3108
|
update(name, newContent, description) {
|
|
2810
3109
|
const skill = this.skills.get(name);
|
|
2811
3110
|
if (!skill) return false;
|
|
@@ -2817,191 +3116,113 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2817
3116
|
skill.description = newDescription;
|
|
2818
3117
|
skill.version = newVersion;
|
|
2819
3118
|
this.invalidateCaches();
|
|
2820
|
-
|
|
3119
|
+
upsertAgentSkill({
|
|
3120
|
+
name,
|
|
3121
|
+
description: newDescription,
|
|
3122
|
+
content: newContent,
|
|
3123
|
+
version: newVersion,
|
|
2821
3124
|
source: "auto_improved"
|
|
2822
|
-
}).catch(() => {
|
|
3125
|
+
}).catch((err) => {
|
|
3126
|
+
log.debug(`Failed to sync skill "${name}" update to DB: ${err}`);
|
|
2823
3127
|
});
|
|
2824
3128
|
if (skill.sourceSkillId && this.userId) {
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}).catch(() => {
|
|
2831
|
-
});
|
|
3129
|
+
updateSourceSkill(skill.sourceSkillId, newContent, newDescription, newVersion).catch(
|
|
3130
|
+
(err) => {
|
|
3131
|
+
log.debug(`Failed to update source skill for "${name}": ${err}`);
|
|
3132
|
+
}
|
|
3133
|
+
);
|
|
2832
3134
|
}
|
|
2833
3135
|
log.info(`Skill "${name}" updated to v${newVersion}`);
|
|
2834
3136
|
return true;
|
|
2835
3137
|
}
|
|
2836
|
-
// ── DB
|
|
3138
|
+
// ── DB Coordination ───────────────────────────────────────────
|
|
2837
3139
|
async syncToAgentSkills(name, description, content, version, options) {
|
|
2838
3140
|
if (!this.userId) return;
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3141
|
+
const id = await upsertAgentSkill({
|
|
3142
|
+
name,
|
|
3143
|
+
description,
|
|
3144
|
+
content,
|
|
3145
|
+
version,
|
|
3146
|
+
source: options?.source,
|
|
3147
|
+
emoji: options?.emoji,
|
|
3148
|
+
keywords: options?.keywords,
|
|
3149
|
+
changeSummary: options?.changeSummary,
|
|
3150
|
+
sourceSkillId: options?.sourceSkillId
|
|
3151
|
+
});
|
|
3152
|
+
if (id) {
|
|
2851
3153
|
const skill = this.skills.get(name);
|
|
2852
|
-
if (skill
|
|
2853
|
-
skill.dbId = data.id;
|
|
2854
|
-
}
|
|
2855
|
-
log.debug(`Skill "${name}" synced to agent_skills`);
|
|
2856
|
-
} catch (err) {
|
|
2857
|
-
log.debug(`DB skill sync error for "${name}": ${err}`);
|
|
3154
|
+
if (skill) skill.dbId = id;
|
|
2858
3155
|
}
|
|
2859
3156
|
}
|
|
2860
3157
|
async logInvocation(skillName, options) {
|
|
2861
3158
|
if (!this.userId) return;
|
|
2862
3159
|
const skill = this.skills.get(skillName);
|
|
2863
|
-
|
|
2864
|
-
if (!skillDbId) {
|
|
3160
|
+
if (!skill?.dbId) {
|
|
2865
3161
|
log.debug(`Cannot log invocation: skill "${skillName}" has no DB ID`);
|
|
2866
3162
|
return;
|
|
2867
3163
|
}
|
|
2868
|
-
|
|
2869
|
-
await callMcpHandler("skill.log_invocation", {
|
|
2870
|
-
skill_id: skillDbId,
|
|
2871
|
-
message_id: options?.messageId || null,
|
|
2872
|
-
session_id: options?.sessionId || null,
|
|
2873
|
-
task_prompt: options?.taskPrompt?.slice(0, 500) || null,
|
|
2874
|
-
arguments: options?.arguments || null,
|
|
2875
|
-
success: options?.success ?? null
|
|
2876
|
-
});
|
|
2877
|
-
} catch (err) {
|
|
2878
|
-
log.debug(`Invocation log error: ${err}`);
|
|
2879
|
-
}
|
|
3164
|
+
await logSkillInvocation(skill.dbId, options);
|
|
2880
3165
|
}
|
|
2881
3166
|
async searchDb(query, limit = 10) {
|
|
3167
|
+
let agentResults = null;
|
|
2882
3168
|
if (this.userId) {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3169
|
+
agentResults = await searchSkillsInDb(query, limit);
|
|
3170
|
+
}
|
|
3171
|
+
if (!agentResults) {
|
|
3172
|
+
agentResults = this.findRelevant(query, limit).map((s) => ({
|
|
3173
|
+
name: s.name,
|
|
3174
|
+
description: s.description,
|
|
3175
|
+
emoji: s.metadata.emoji || "",
|
|
3176
|
+
source: s.source,
|
|
3177
|
+
invocationCount: 0
|
|
3178
|
+
}));
|
|
3179
|
+
}
|
|
3180
|
+
if (agentResults.length === 0 && this.userId) {
|
|
3181
|
+
const discovered = await discoverSkills(query, limit, []);
|
|
3182
|
+
if (discovered && discovered.length > 0) {
|
|
3183
|
+
for (const d of discovered) {
|
|
3184
|
+
agentResults.push({
|
|
3185
|
+
name: d.name,
|
|
3186
|
+
description: d.description || "",
|
|
3187
|
+
emoji: d.emoji || "",
|
|
3188
|
+
source: d.source || "external",
|
|
3189
|
+
invocationCount: 0,
|
|
3190
|
+
marketplaceId: d.id
|
|
3191
|
+
});
|
|
2896
3192
|
}
|
|
2897
|
-
} catch {
|
|
2898
3193
|
}
|
|
2899
3194
|
}
|
|
2900
|
-
|
|
2901
|
-
return results.map((s) => ({
|
|
2902
|
-
name: s.name,
|
|
2903
|
-
description: s.description,
|
|
2904
|
-
emoji: s.metadata.emoji || "",
|
|
2905
|
-
source: s.source,
|
|
2906
|
-
invocationCount: 0
|
|
2907
|
-
}));
|
|
3195
|
+
return agentResults;
|
|
2908
3196
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
3197
|
+
// ── Marketplace Discovery ────────────────────────────────────
|
|
3198
|
+
/**
|
|
3199
|
+
* Preview a marketplace skill by name without adding it.
|
|
3200
|
+
* Returns the DiscoverResult if an exact match is found, so the caller
|
|
3201
|
+
* can show details to the user for confirmation before adding.
|
|
3202
|
+
*/
|
|
3203
|
+
async previewMarketplaceSkill(name) {
|
|
3204
|
+
if (!this.userId) return null;
|
|
2911
3205
|
try {
|
|
2912
|
-
await
|
|
2913
|
-
|
|
3206
|
+
const discovered = await discoverSkills(name, 5, []);
|
|
3207
|
+
if (!discovered || discovered.length === 0) return null;
|
|
3208
|
+
const match = discovered.find((d) => d.name === name);
|
|
3209
|
+
return match || null;
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
log.debug(`previewMarketplaceSkill error for "${name}": ${err}`);
|
|
3212
|
+
return null;
|
|
2914
3213
|
}
|
|
2915
3214
|
}
|
|
2916
|
-
// ── Marketplace
|
|
3215
|
+
// ── Marketplace (delegated) ───────────────────────────────────
|
|
2917
3216
|
async publish(name, options) {
|
|
2918
3217
|
if (!this.userId) return null;
|
|
2919
3218
|
const skill = this.skills.get(name);
|
|
2920
3219
|
if (!skill) return null;
|
|
2921
|
-
|
|
2922
|
-
log.debug(`Cannot publish external skill "${name}"`);
|
|
2923
|
-
return null;
|
|
2924
|
-
}
|
|
2925
|
-
try {
|
|
2926
|
-
const data = await callMcpHandler("skill.publish", {
|
|
2927
|
-
name: skill.name,
|
|
2928
|
-
description: skill.description,
|
|
2929
|
-
version: skill.version,
|
|
2930
|
-
emoji: skill.metadata.emoji || null,
|
|
2931
|
-
content: skill.content,
|
|
2932
|
-
argument_hint: skill.argumentHint || null,
|
|
2933
|
-
keywords: skill.keywords,
|
|
2934
|
-
allowed_tools: skill.allowedTools,
|
|
2935
|
-
author_name: options?.authorName || null,
|
|
2936
|
-
metadata: skill.metadata,
|
|
2937
|
-
homepage: skill.homepage || null,
|
|
2938
|
-
category: options?.category || null,
|
|
2939
|
-
source: skill.source
|
|
2940
|
-
});
|
|
2941
|
-
log.info(`Skill "${name}" published to marketplace`);
|
|
2942
|
-
return data;
|
|
2943
|
-
} catch (err) {
|
|
2944
|
-
log.debug(`Publish error: ${err}`);
|
|
2945
|
-
return null;
|
|
2946
|
-
}
|
|
3220
|
+
return publishSkill(skill, options);
|
|
2947
3221
|
}
|
|
2948
3222
|
async browse(options) {
|
|
2949
|
-
|
|
2950
|
-
const data = await callMcpHandler("skill.browse", {
|
|
2951
|
-
query: options?.query || null,
|
|
2952
|
-
category: options?.category || null,
|
|
2953
|
-
sort: options?.sort || "popular",
|
|
2954
|
-
limit: options?.limit || 20,
|
|
2955
|
-
offset: options?.offset || 0
|
|
2956
|
-
});
|
|
2957
|
-
return (data || []).map((r) => safeParse(BrowseSkillRowSchema, r)).filter(Boolean).map((r) => ({
|
|
2958
|
-
id: r.id,
|
|
2959
|
-
name: r.name,
|
|
2960
|
-
description: r.description,
|
|
2961
|
-
emoji: r.emoji,
|
|
2962
|
-
version: r.version,
|
|
2963
|
-
authorName: r.author_name,
|
|
2964
|
-
category: r.category,
|
|
2965
|
-
installCount: r.install_count,
|
|
2966
|
-
avgRating: r.avg_rating ?? null,
|
|
2967
|
-
ratingCount: r.rating_count
|
|
2968
|
-
}));
|
|
2969
|
-
} catch {
|
|
2970
|
-
return [];
|
|
2971
|
-
}
|
|
3223
|
+
return browseSkills(options);
|
|
2972
3224
|
}
|
|
2973
3225
|
};
|
|
2974
|
-
function validateSkillName(name) {
|
|
2975
|
-
if (!name || name.length === 0) return "name is empty";
|
|
2976
|
-
if (name.length > 64) return `name too long (${name.length}/64 chars)`;
|
|
2977
|
-
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
2978
|
-
return `name must be lowercase kebab-case (a-z, 0-9, hyphens), no leading/trailing/consecutive hyphens. Got: "${name}"`;
|
|
2979
|
-
}
|
|
2980
|
-
return null;
|
|
2981
|
-
}
|
|
2982
|
-
function normalizeSkillName(name) {
|
|
2983
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64);
|
|
2984
|
-
}
|
|
2985
|
-
function substituteArguments(content, args) {
|
|
2986
|
-
const parts = args.split(/\s+/);
|
|
2987
|
-
content = content.replace(/\$ARGUMENTS/g, args);
|
|
2988
|
-
content = content.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, i) => parts[parseInt(i)] || "");
|
|
2989
|
-
content = content.replace(/\$(\d+)(?!\w)/g, (_, i) => parts[parseInt(i)] || "");
|
|
2990
|
-
return content;
|
|
2991
|
-
}
|
|
2992
|
-
var SAFE_DYNAMIC_COMMANDS = /^(date|whoami|hostname|uname|pwd|echo|node\s+--version|npm\s+--version|git\s+(branch|rev-parse|log\s+--oneline)|cat\s+)/;
|
|
2993
|
-
function preprocessDynamicContext(content, cwd) {
|
|
2994
|
-
return content.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
2995
|
-
if (!SAFE_DYNAMIC_COMMANDS.test(cmd.trim())) {
|
|
2996
|
-
return `[command blocked: ${cmd}]`;
|
|
2997
|
-
}
|
|
2998
|
-
try {
|
|
2999
|
-
return execSync2(cmd, { timeout: 1e4, encoding: "utf-8", cwd }).trim();
|
|
3000
|
-
} catch {
|
|
3001
|
-
return `[command failed: ${cmd}]`;
|
|
3002
|
-
}
|
|
3003
|
-
});
|
|
3004
|
-
}
|
|
3005
3226
|
|
|
3006
3227
|
// src/credentials/credential-store.ts
|
|
3007
3228
|
import { randomUUID } from "crypto";
|
|
@@ -3011,7 +3232,7 @@ import { dirname } from "path";
|
|
|
3011
3232
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
|
|
3012
3233
|
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
3013
3234
|
import { join as join2 } from "path";
|
|
3014
|
-
import { homedir as homedir2
|
|
3235
|
+
import { hostname, userInfo, homedir as homedir2 } from "os";
|
|
3015
3236
|
var ALGORITHM = "aes-256-gcm";
|
|
3016
3237
|
var KEY_LENGTH = 32;
|
|
3017
3238
|
var IV_LENGTH = 12;
|
|
@@ -3035,10 +3256,7 @@ function deriveKey(basePath) {
|
|
|
3035
3256
|
function encrypt(plaintext, key) {
|
|
3036
3257
|
const iv = randomBytes(IV_LENGTH);
|
|
3037
3258
|
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3038
|
-
const encrypted = Buffer.concat([
|
|
3039
|
-
cipher.update(plaintext, "utf-8"),
|
|
3040
|
-
cipher.final()
|
|
3041
|
-
]);
|
|
3259
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
3042
3260
|
return {
|
|
3043
3261
|
iv: iv.toString("base64"),
|
|
3044
3262
|
data: encrypted.toString("base64"),
|
|
@@ -3051,28 +3269,23 @@ function decrypt(payload, key) {
|
|
|
3051
3269
|
const tag = Buffer.from(payload.tag, "base64");
|
|
3052
3270
|
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3053
3271
|
decipher.setAuthTag(tag);
|
|
3054
|
-
return Buffer.concat([
|
|
3055
|
-
decipher.update(data),
|
|
3056
|
-
decipher.final()
|
|
3057
|
-
]).toString("utf-8");
|
|
3272
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf-8");
|
|
3058
3273
|
}
|
|
3059
3274
|
|
|
3060
3275
|
// src/credentials/local-store.ts
|
|
3061
3276
|
import Database from "better-sqlite3";
|
|
3062
3277
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
3063
3278
|
import { join as join3 } from "path";
|
|
3064
|
-
import { homedir as homedir3 } from "os";
|
|
3065
|
-
var DEFAULT_DB_DIR = join3(homedir3(), ".config", "assistme");
|
|
3066
3279
|
var DEFAULT_DB_NAME = "local.db";
|
|
3067
3280
|
var LocalStore = class {
|
|
3068
3281
|
db;
|
|
3069
3282
|
dbPath;
|
|
3070
3283
|
constructor(dbPath) {
|
|
3071
|
-
const dir = dbPath ? dbPath :
|
|
3284
|
+
const dir = dbPath ? dbPath : getDataDir();
|
|
3072
3285
|
if (!existsSync3(dir)) {
|
|
3073
3286
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
3074
3287
|
}
|
|
3075
|
-
this.dbPath =
|
|
3288
|
+
this.dbPath = join3(dir, DEFAULT_DB_NAME);
|
|
3076
3289
|
this.db = new Database(this.dbPath);
|
|
3077
3290
|
this.db.pragma("journal_mode = WAL");
|
|
3078
3291
|
this.db.pragma("foreign_keys = ON");
|
|
@@ -3273,8 +3486,72 @@ function getCredentialStore() {
|
|
|
3273
3486
|
return _instance2;
|
|
3274
3487
|
}
|
|
3275
3488
|
|
|
3489
|
+
// src/agent/sdk-stream.ts
|
|
3490
|
+
async function consumeSDKStream(stream, handlers) {
|
|
3491
|
+
let response = "";
|
|
3492
|
+
let sessionId;
|
|
3493
|
+
let tokenUsage;
|
|
3494
|
+
let costUsd;
|
|
3495
|
+
let numTurns;
|
|
3496
|
+
let structuredOutput;
|
|
3497
|
+
const errors = [];
|
|
3498
|
+
for await (const message of stream) {
|
|
3499
|
+
const msg = message;
|
|
3500
|
+
switch (msg.type) {
|
|
3501
|
+
case "assistant": {
|
|
3502
|
+
const assistantMsg = message;
|
|
3503
|
+
for (const block of assistantMsg.message.content) {
|
|
3504
|
+
if (block.type === "text") {
|
|
3505
|
+
response += block.text;
|
|
3506
|
+
if (handlers?.onText) await handlers.onText(block.text);
|
|
3507
|
+
} else if (block.type === "thinking" && "thinking" in block) {
|
|
3508
|
+
const thinkingText = block.thinking;
|
|
3509
|
+
if (handlers?.onThinking) await handlers.onThinking(thinkingText);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
break;
|
|
3513
|
+
}
|
|
3514
|
+
case "result": {
|
|
3515
|
+
const resultMsg = message;
|
|
3516
|
+
tokenUsage = {
|
|
3517
|
+
input_tokens: resultMsg.usage.input_tokens,
|
|
3518
|
+
output_tokens: resultMsg.usage.output_tokens
|
|
3519
|
+
};
|
|
3520
|
+
if (resultMsg.subtype === "success") {
|
|
3521
|
+
const successMsg = resultMsg;
|
|
3522
|
+
if (!response && successMsg.result) {
|
|
3523
|
+
response = successMsg.result;
|
|
3524
|
+
}
|
|
3525
|
+
sessionId = successMsg.session_id;
|
|
3526
|
+
costUsd = successMsg.total_cost_usd;
|
|
3527
|
+
numTurns = successMsg.num_turns;
|
|
3528
|
+
structuredOutput = successMsg.structured_output;
|
|
3529
|
+
} else {
|
|
3530
|
+
const errMsg = resultMsg;
|
|
3531
|
+
for (const err of errMsg.errors) {
|
|
3532
|
+
errors.push(err);
|
|
3533
|
+
if (handlers?.onError) await handlers.onError(err);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
break;
|
|
3537
|
+
}
|
|
3538
|
+
default: {
|
|
3539
|
+
if (msg.type === "system" && "subtype" in msg) {
|
|
3540
|
+
const sysMsg = msg;
|
|
3541
|
+
if (sysMsg.subtype === "init" && sysMsg.session_id) {
|
|
3542
|
+
sessionId = sysMsg.session_id;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
break;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
return { response, sessionId, tokenUsage, costUsd, numTurns, structuredOutput, errors };
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3276
3552
|
// src/tools/shell.ts
|
|
3277
3553
|
import { exec } from "child_process";
|
|
3554
|
+
import { resolve, relative, sep } from "path";
|
|
3278
3555
|
var BLOCKED_PATTERNS = [
|
|
3279
3556
|
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
|
|
3280
3557
|
// rm -rf /, rm -fr /, etc.
|
|
@@ -3297,16 +3574,54 @@ var BLOCKED_PATTERNS = [
|
|
|
3297
3574
|
/\bsystemctl\s+(start|stop|disable|mask)\b/i
|
|
3298
3575
|
// dangerous systemctl ops
|
|
3299
3576
|
];
|
|
3577
|
+
var SHELL_WRAPPER_PATTERNS = [
|
|
3578
|
+
/\b(?:bash|sh|zsh|dash|ksh|csh)\s+(?:-\w+\s+)*-c\s+/i,
|
|
3579
|
+
// bash -c "...", sh -c "..."
|
|
3580
|
+
/\beval\s+/i,
|
|
3581
|
+
// eval "..."
|
|
3582
|
+
/\bexec\s+/i,
|
|
3583
|
+
// exec "..."
|
|
3584
|
+
/\bsource\s+\/dev\/stdin/i
|
|
3585
|
+
// source /dev/stdin <<< "..."
|
|
3586
|
+
];
|
|
3587
|
+
function extractInnerCommand(command) {
|
|
3588
|
+
for (const pattern of SHELL_WRAPPER_PATTERNS) {
|
|
3589
|
+
const match = command.match(pattern);
|
|
3590
|
+
if (match) {
|
|
3591
|
+
const rest = command.slice(match.index + match[0].length);
|
|
3592
|
+
const trimmed = rest.trim();
|
|
3593
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
3594
|
+
return trimmed.slice(1, -1);
|
|
3595
|
+
}
|
|
3596
|
+
return trimmed;
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
return null;
|
|
3600
|
+
}
|
|
3300
3601
|
function isBlocked(command) {
|
|
3301
|
-
|
|
3602
|
+
if (BLOCKED_PATTERNS.some((pattern) => pattern.test(command))) {
|
|
3603
|
+
return true;
|
|
3604
|
+
}
|
|
3605
|
+
const inner = extractInnerCommand(command);
|
|
3606
|
+
if (inner && isBlocked(inner)) {
|
|
3607
|
+
return true;
|
|
3608
|
+
}
|
|
3609
|
+
return false;
|
|
3302
3610
|
}
|
|
3303
3611
|
async function executeShell(command, cwd) {
|
|
3304
3612
|
if (isBlocked(command)) {
|
|
3305
3613
|
throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
|
|
3306
3614
|
}
|
|
3307
3615
|
const config = getConfig();
|
|
3308
|
-
const workDir = cwd
|
|
3309
|
-
|
|
3616
|
+
const workDir = cwd ? resolve(cwd) : config.workspacePath;
|
|
3617
|
+
const rel = relative(config.workspacePath, workDir);
|
|
3618
|
+
if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
|
|
3619
|
+
throw new AppError(
|
|
3620
|
+
`Access denied: cwd "${cwd}" is outside workspace "${config.workspacePath}"`,
|
|
3621
|
+
"PATH_TRAVERSAL"
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
return new Promise((resolve2) => {
|
|
3310
3625
|
exec(
|
|
3311
3626
|
command,
|
|
3312
3627
|
{
|
|
@@ -3334,7 +3649,7 @@ ${stderr}` : "";
|
|
|
3334
3649
|
|
|
3335
3650
|
[Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
|
|
3336
3651
|
}
|
|
3337
|
-
|
|
3652
|
+
resolve2(output || "(no output)");
|
|
3338
3653
|
}
|
|
3339
3654
|
);
|
|
3340
3655
|
});
|
|
@@ -3371,13 +3686,11 @@ export {
|
|
|
3371
3686
|
listScheduledTasks,
|
|
3372
3687
|
toggleScheduledTask,
|
|
3373
3688
|
deleteScheduledTask,
|
|
3689
|
+
consumeSDKStream,
|
|
3374
3690
|
executeShell,
|
|
3375
3691
|
MemoryManager,
|
|
3692
|
+
LRUCache,
|
|
3376
3693
|
SkillManager,
|
|
3377
|
-
validateSkillName,
|
|
3378
|
-
normalizeSkillName,
|
|
3379
|
-
substituteArguments,
|
|
3380
|
-
preprocessDynamicContext,
|
|
3381
3694
|
getLocalStore,
|
|
3382
3695
|
getCredentialStore
|
|
3383
3696
|
};
|