miladyai 2.0.0-alpha.27
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/_virtual/_rolldown/runtime.js +7 -0
- package/dist/actions/emote.js +64 -0
- package/dist/actions/restart.js +81 -0
- package/dist/actions/send-message.js +152 -0
- package/dist/agent-admin-routes.js +82 -0
- package/dist/agent-lifecycle-routes.js +79 -0
- package/dist/agent-transfer-routes.js +102 -0
- package/dist/api/agent-admin-routes.js +82 -0
- package/dist/api/agent-lifecycle-routes.js +79 -0
- package/dist/api/agent-transfer-routes.js +102 -0
- package/dist/api/apps-hyperscape-routes.js +58 -0
- package/dist/api/apps-routes.js +114 -0
- package/dist/api/auth-routes.js +56 -0
- package/dist/api/autonomy-routes.js +44 -0
- package/dist/api/bug-report-routes.js +111 -0
- package/dist/api/character-routes.js +195 -0
- package/dist/api/cloud-routes.js +330 -0
- package/dist/api/cloud-status-routes.js +155 -0
- package/dist/api/compat-utils.js +111 -0
- package/dist/api/database.js +735 -0
- package/dist/api/diagnostics-routes.js +205 -0
- package/dist/api/drop-service.js +134 -0
- package/dist/api/early-logs.js +86 -0
- package/dist/api/http-helpers.js +131 -0
- package/dist/api/knowledge-routes.js +534 -0
- package/dist/api/memory-bounds.js +71 -0
- package/dist/api/models-routes.js +28 -0
- package/dist/api/og-tracker.js +36 -0
- package/dist/api/permissions-routes.js +109 -0
- package/dist/api/plugin-validation.js +198 -0
- package/dist/api/provider-switch-config.js +41 -0
- package/dist/api/registry-routes.js +86 -0
- package/dist/api/registry-service.js +164 -0
- package/dist/api/sandbox-routes.js +1112 -0
- package/dist/api/server.js +7949 -0
- package/dist/api/subscription-routes.js +172 -0
- package/dist/api/terminal-run-limits.js +24 -0
- package/dist/api/training-routes.js +158 -0
- package/dist/api/trajectory-routes.js +300 -0
- package/dist/api/trigger-routes.js +246 -0
- package/dist/api/twitter-verify.js +134 -0
- package/dist/api/tx-service.js +108 -0
- package/dist/api/wallet-routes.js +266 -0
- package/dist/api/wallet.js +568 -0
- package/dist/api/whatsapp-routes.js +182 -0
- package/dist/api/zip-utils.js +109 -0
- package/dist/apps-hyperscape-routes.js +58 -0
- package/dist/apps-routes.js +114 -0
- package/dist/ascii.js +20 -0
- package/dist/auth/anthropic.js +44 -0
- package/dist/auth/apply-stealth.js +41 -0
- package/dist/auth/claude-code-stealth.js +78 -0
- package/dist/auth/credentials.js +156 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/openai-codex.js +66 -0
- package/dist/auth/types.js +9 -0
- package/dist/auth-routes.js +56 -0
- package/dist/autonomy-routes.js +44 -0
- package/dist/bug-report-routes.js +111 -0
- package/dist/build-info.json +6 -0
- package/dist/character-routes.js +195 -0
- package/dist/cli/argv.js +63 -0
- package/dist/cli/banner.js +34 -0
- package/dist/cli/cli-name.js +21 -0
- package/dist/cli/cli-utils.js +16 -0
- package/dist/cli/git-commit.js +78 -0
- package/dist/cli/parse-duration.js +15 -0
- package/dist/cli/plugins-cli.js +590 -0
- package/dist/cli/profile-utils.js +9 -0
- package/dist/cli/profile.js +95 -0
- package/dist/cli/program/build-program.js +17 -0
- package/dist/cli/program/command-registry.js +23 -0
- package/dist/cli/program/help.js +47 -0
- package/dist/cli/program/preaction.js +33 -0
- package/dist/cli/program/register.config.js +106 -0
- package/dist/cli/program/register.configure.js +20 -0
- package/dist/cli/program/register.dashboard.js +124 -0
- package/dist/cli/program/register.models.js +23 -0
- package/dist/cli/program/register.setup.js +36 -0
- package/dist/cli/program/register.start.js +22 -0
- package/dist/cli/program/register.subclis.js +70 -0
- package/dist/cli/program/register.tui.js +163 -0
- package/dist/cli/program/register.update.js +154 -0
- package/dist/cli/program.js +3 -0
- package/dist/cli/run-main.js +37 -0
- package/dist/cli/version.js +7 -0
- package/dist/cloud/validate-url.js +93 -0
- package/dist/cloud-routes.js +330 -0
- package/dist/cloud-status-routes.js +155 -0
- package/dist/compat-utils.js +111 -0
- package/dist/config/config.js +69 -0
- package/dist/config/env-vars.js +19 -0
- package/dist/config/includes.js +121 -0
- package/dist/config/object-utils.js +7 -0
- package/dist/config/paths.js +38 -0
- package/dist/config/plugin-auto-enable.js +231 -0
- package/dist/config/schema.js +864 -0
- package/dist/config/telegram-custom-commands.js +76 -0
- package/dist/config/zod-schema.agent-runtime.js +519 -0
- package/dist/config/zod-schema.core.js +538 -0
- package/dist/config/zod-schema.hooks.js +103 -0
- package/dist/config/zod-schema.js +488 -0
- package/dist/config/zod-schema.providers-core.js +785 -0
- package/dist/config/zod-schema.session.js +73 -0
- package/dist/core-plugins.js +37 -0
- package/dist/custom-actions.js +250 -0
- package/dist/database.js +735 -0
- package/dist/diagnostics/integration-observability.js +57 -0
- package/dist/diagnostics-routes.js +205 -0
- package/dist/drop-service.js +134 -0
- package/dist/early-logs.js +24 -0
- package/dist/eliza.js +2061 -0
- package/dist/emotes/catalog.js +271 -0
- package/dist/entry.js +40 -0
- package/dist/hooks/discovery.js +167 -0
- package/dist/hooks/eligibility.js +64 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/loader.js +147 -0
- package/dist/hooks/registry.js +55 -0
- package/dist/http-helpers.js +131 -0
- package/dist/index.js +49 -0
- package/dist/knowledge-routes.js +534 -0
- package/dist/memory-bounds.js +71 -0
- package/dist/milady-plugin.js +90 -0
- package/dist/models-routes.js +28 -0
- package/dist/onboarding-names.js +78 -0
- package/dist/onboarding-presets.js +922 -0
- package/dist/package.json +1 -0
- package/dist/permissions-routes.js +109 -0
- package/dist/plugin-validation.js +107 -0
- package/dist/plugins/whatsapp/actions.js +91 -0
- package/dist/plugins/whatsapp/index.js +16 -0
- package/dist/plugins/whatsapp/service.js +270 -0
- package/dist/provider-switch-config.js +41 -0
- package/dist/providers/admin-trust.js +46 -0
- package/dist/providers/autonomous-state.js +101 -0
- package/dist/providers/session-bridge.js +86 -0
- package/dist/providers/session-utils.js +36 -0
- package/dist/providers/simple-mode.js +50 -0
- package/dist/providers/ui-catalog.js +15 -0
- package/dist/providers/workspace-provider.js +93 -0
- package/dist/providers/workspace.js +348 -0
- package/dist/registry-routes.js +86 -0
- package/dist/registry-service.js +164 -0
- package/dist/restart.js +40 -0
- package/dist/runtime/core-plugins.js +37 -0
- package/dist/runtime/custom-actions.js +250 -0
- package/dist/runtime/eliza.js +2061 -0
- package/dist/runtime/embedding-manager-support.js +185 -0
- package/dist/runtime/embedding-manager.js +193 -0
- package/dist/runtime/embedding-presets.js +54 -0
- package/dist/runtime/embedding-state.js +8 -0
- package/dist/runtime/milady-plugin.js +90 -0
- package/dist/runtime/onboarding-names.js +78 -0
- package/dist/runtime/restart.js +40 -0
- package/dist/runtime/version.js +7 -0
- package/dist/sandbox-routes.js +1112 -0
- package/dist/security/audit-log.js +149 -0
- package/dist/security/network-policy.js +70 -0
- package/dist/server.js +7949 -0
- package/dist/services/agent-export.js +559 -0
- package/dist/services/app-manager.js +389 -0
- package/dist/services/browser-capture.js +86 -0
- package/dist/services/fallback-training-service.js +128 -0
- package/dist/services/mcp-marketplace.js +134 -0
- package/dist/services/plugin-installer.js +396 -0
- package/dist/services/plugin-manager-types.js +15 -0
- package/dist/services/registry-client-app-meta.js +144 -0
- package/dist/services/registry-client-endpoints.js +166 -0
- package/dist/services/registry-client-local.js +271 -0
- package/dist/services/registry-client-network.js +93 -0
- package/dist/services/registry-client-queries.js +70 -0
- package/dist/services/registry-client.js +157 -0
- package/dist/services/sandbox-engine.js +511 -0
- package/dist/services/sandbox-manager.js +297 -0
- package/dist/services/self-updater.js +175 -0
- package/dist/services/skill-catalog-client.js +119 -0
- package/dist/services/skill-marketplace.js +521 -0
- package/dist/services/stream-manager.js +236 -0
- package/dist/services/update-checker.js +121 -0
- package/dist/services/update-notifier.js +29 -0
- package/dist/services/version-compat.js +78 -0
- package/dist/services/whatsapp-pairing.js +196 -0
- package/dist/shared/ui-catalog-prompt.js +728 -0
- package/dist/subscription-routes.js +172 -0
- package/dist/terminal/links.js +19 -0
- package/dist/terminal/palette.js +14 -0
- package/dist/terminal/theme.js +25 -0
- package/dist/terminal-run-limits.js +24 -0
- package/dist/training-routes.js +158 -0
- package/dist/trajectory-routes.js +300 -0
- package/dist/trigger-routes.js +246 -0
- package/dist/triggers/action.js +218 -0
- package/dist/triggers/runtime.js +281 -0
- package/dist/triggers/scheduling.js +295 -0
- package/dist/triggers/types.js +5 -0
- package/dist/tui/components/assistant-message.js +76 -0
- package/dist/tui/components/chat-editor.js +34 -0
- package/dist/tui/components/embeddings-overlay.js +46 -0
- package/dist/tui/components/footer.js +60 -0
- package/dist/tui/components/index.js +15 -0
- package/dist/tui/components/modal-frame.js +45 -0
- package/dist/tui/components/modal-style.js +15 -0
- package/dist/tui/components/model-selector.js +70 -0
- package/dist/tui/components/pinned-chat-layout.js +46 -0
- package/dist/tui/components/plugins-endpoints-tab.js +196 -0
- package/dist/tui/components/plugins-installed-tab-view.js +69 -0
- package/dist/tui/components/plugins-installed-tab.js +319 -0
- package/dist/tui/components/plugins-overlay-catalog.js +81 -0
- package/dist/tui/components/plugins-overlay-data-api.js +21 -0
- package/dist/tui/components/plugins-overlay-data-shared.js +20 -0
- package/dist/tui/components/plugins-overlay-data.js +323 -0
- package/dist/tui/components/plugins-overlay.js +117 -0
- package/dist/tui/components/plugins-store-tab.js +148 -0
- package/dist/tui/components/settings-overlay.js +61 -0
- package/dist/tui/components/status-bar.js +64 -0
- package/dist/tui/components/tool-execution.js +68 -0
- package/dist/tui/components/user-message.js +22 -0
- package/dist/tui/eliza-tui-bridge.js +606 -0
- package/dist/tui/index.js +370 -0
- package/dist/tui/modal-presets.js +33 -0
- package/dist/tui/model-spec.js +46 -0
- package/dist/tui/sse-parser.js +78 -0
- package/dist/tui/theme.js +110 -0
- package/dist/tui/titlebar-spinner.js +62 -0
- package/dist/tui/tui-app.js +311 -0
- package/dist/tui/ws-client.js +215 -0
- package/dist/twitter-verify.js +134 -0
- package/dist/tx-service.js +108 -0
- package/dist/utils/exec-safety.js +17 -0
- package/dist/utils/globals.js +20 -0
- package/dist/utils/milady-root.js +61 -0
- package/dist/utils/number-parsing.js +37 -0
- package/dist/version-resolver.js +37 -0
- package/dist/version.js +7 -0
- package/dist/wallet-routes.js +266 -0
- package/dist/wallet.js +568 -0
- package/dist/whatsapp-routes.js +182 -0
- package/dist/zip-utils.js +109 -0
- package/milady.mjs +14 -0
- package/package.json +111 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { createIntegrationTelemetrySpan } from "../diagnostics/integration-observability.js";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { logger } from "@elizaos/core";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
//#region src/services/skill-marketplace.ts
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const DEFAULT_SKILLS_MARKETPLACE_URL = "https://clawhub.ai";
|
|
12
|
+
const LEGACY_SKILLSMP_HOST = "skillsmp.com";
|
|
13
|
+
const VALID_NAME = /^[a-zA-Z0-9._-]+$/;
|
|
14
|
+
const VALID_GIT_REF = /^[a-zA-Z0-9][\w./-]*$/;
|
|
15
|
+
/** Timeout for git clone/sparse-checkout (shallow + sparse should be fast). */
|
|
16
|
+
const GIT_TIMEOUT_MS = 15e3;
|
|
17
|
+
/** Timeout for marketplace API fetch calls. */
|
|
18
|
+
const FETCH_TIMEOUT_MS = 3e4;
|
|
19
|
+
/**
|
|
20
|
+
* Run a security scan on a skill directory.
|
|
21
|
+
*
|
|
22
|
+
* Checks for binary files, symlink escapes, and missing SKILL.md.
|
|
23
|
+
* This is a self-contained manifest check — the full content-level scan
|
|
24
|
+
* (code + markdown patterns) is handled by the AgentSkillsService when
|
|
25
|
+
* it loads the skill. This layer catches the most dangerous structural
|
|
26
|
+
* attacks at the marketplace install boundary.
|
|
27
|
+
*/
|
|
28
|
+
async function runSkillSecurityScan(skillDir) {
|
|
29
|
+
const fsPromises = await import("node:fs/promises");
|
|
30
|
+
const pathMod = await import("node:path");
|
|
31
|
+
const findings = [];
|
|
32
|
+
const manifestFindings = [];
|
|
33
|
+
let scannedFiles = 0;
|
|
34
|
+
const BINARY_EXTENSIONS = new Set([
|
|
35
|
+
".exe",
|
|
36
|
+
".dll",
|
|
37
|
+
".so",
|
|
38
|
+
".dylib",
|
|
39
|
+
".wasm",
|
|
40
|
+
".bin",
|
|
41
|
+
".com",
|
|
42
|
+
".bat",
|
|
43
|
+
".cmd"
|
|
44
|
+
]);
|
|
45
|
+
async function walk(dir) {
|
|
46
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.name === "node_modules") continue;
|
|
49
|
+
const fullPath = pathMod.join(dir, entry.name);
|
|
50
|
+
const relPath = pathMod.relative(skillDir, fullPath);
|
|
51
|
+
if (entry.isDirectory()) await walk(fullPath);
|
|
52
|
+
else if (entry.isFile()) {
|
|
53
|
+
scannedFiles++;
|
|
54
|
+
const ext = pathMod.extname(entry.name).toLowerCase();
|
|
55
|
+
if (BINARY_EXTENSIONS.has(ext)) manifestFindings.push({
|
|
56
|
+
ruleId: "binary-file",
|
|
57
|
+
severity: "critical",
|
|
58
|
+
file: relPath,
|
|
59
|
+
message: `Binary executable file detected (${ext})`
|
|
60
|
+
});
|
|
61
|
+
} else if (entry.isSymbolicLink()) {
|
|
62
|
+
const resolved = await fsPromises.realpath(fullPath).catch(() => "");
|
|
63
|
+
if (resolved && !resolved.startsWith(skillDir + pathMod.sep)) manifestFindings.push({
|
|
64
|
+
ruleId: "symlink-escape",
|
|
65
|
+
severity: "critical",
|
|
66
|
+
file: relPath,
|
|
67
|
+
message: `Symbolic link points outside skill directory`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await walk(skillDir);
|
|
73
|
+
const skillMdPath = pathMod.join(skillDir, "SKILL.md");
|
|
74
|
+
if (!await fsPromises.stat(skillMdPath).then((s) => s.isFile()).catch(() => false)) manifestFindings.push({
|
|
75
|
+
ruleId: "missing-skill-md",
|
|
76
|
+
severity: "critical",
|
|
77
|
+
file: "SKILL.md",
|
|
78
|
+
message: "No SKILL.md file found — invalid skill package"
|
|
79
|
+
});
|
|
80
|
+
const hasBlocking = manifestFindings.some((f) => f.ruleId === "binary-file" || f.ruleId === "symlink-escape" || f.ruleId === "missing-skill-md");
|
|
81
|
+
const critical = manifestFindings.filter((f) => f.severity === "critical").length;
|
|
82
|
+
const warn = manifestFindings.filter((f) => f.severity === "warn").length;
|
|
83
|
+
let status = "clean";
|
|
84
|
+
if (hasBlocking) status = "blocked";
|
|
85
|
+
else if (critical > 0) status = "critical";
|
|
86
|
+
else if (warn > 0) status = "warning";
|
|
87
|
+
const report = {
|
|
88
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
+
status,
|
|
90
|
+
summary: {
|
|
91
|
+
scannedFiles,
|
|
92
|
+
critical,
|
|
93
|
+
warn,
|
|
94
|
+
info: 0
|
|
95
|
+
},
|
|
96
|
+
findings,
|
|
97
|
+
manifestFindings,
|
|
98
|
+
skillPath: skillDir
|
|
99
|
+
};
|
|
100
|
+
await fsPromises.writeFile(pathMod.join(skillDir, ".scan-results.json"), JSON.stringify(report, null, 2), "utf-8");
|
|
101
|
+
return report;
|
|
102
|
+
}
|
|
103
|
+
function stateDirBase() {
|
|
104
|
+
return process.env.MILADY_STATE_DIR?.trim() || path.join(os.homedir(), ".milady");
|
|
105
|
+
}
|
|
106
|
+
function safeName(raw) {
|
|
107
|
+
const slug = raw.trim().replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
108
|
+
if (!slug) throw new Error("Invalid skill name");
|
|
109
|
+
if (!VALID_NAME.test(slug)) throw new Error(`Invalid skill name: ${raw}`);
|
|
110
|
+
return slug;
|
|
111
|
+
}
|
|
112
|
+
function validateGitRef(ref) {
|
|
113
|
+
if (!ref || !VALID_GIT_REF.test(ref)) throw new Error("Invalid git ref");
|
|
114
|
+
}
|
|
115
|
+
function sanitizeSkillPath(raw) {
|
|
116
|
+
const trimmed = raw.trim();
|
|
117
|
+
if (!trimmed) throw new Error("Invalid skill path");
|
|
118
|
+
if (trimmed.startsWith("~")) throw new Error("Invalid skill path");
|
|
119
|
+
if (path.posix.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed)) throw new Error("Invalid skill path");
|
|
120
|
+
if (trimmed.includes("\\")) throw new Error("Invalid skill path");
|
|
121
|
+
const cleaned = trimmed.replace(/^\/+/, "");
|
|
122
|
+
if (!cleaned) throw new Error("Invalid skill path");
|
|
123
|
+
if (path.posix.isAbsolute(cleaned) || path.win32.isAbsolute(cleaned)) throw new Error("Invalid skill path");
|
|
124
|
+
if (cleaned === ".") return ".";
|
|
125
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
126
|
+
if (parts.length === 0) throw new Error("Invalid skill path");
|
|
127
|
+
if (parts.some((p) => p === "." || p === "..")) throw new Error("Invalid skill path");
|
|
128
|
+
return parts.join("/");
|
|
129
|
+
}
|
|
130
|
+
function assertPathWithinRoot(rootDir, targetPath) {
|
|
131
|
+
const root = path.resolve(rootDir);
|
|
132
|
+
const target = path.resolve(targetPath);
|
|
133
|
+
if (target === root) return;
|
|
134
|
+
if (!target.startsWith(`${root}${path.sep}`)) throw new Error("Skill path escapes repository root");
|
|
135
|
+
}
|
|
136
|
+
function normalizeRepo(raw) {
|
|
137
|
+
const repo = raw.replace(/^https:\/\/github\.com\//i, "").replace(/\.git$/i, "").replace(/^github:/i, "").trim();
|
|
138
|
+
if (!/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repo)) throw new Error(`Invalid repository: ${raw}`);
|
|
139
|
+
return repo;
|
|
140
|
+
}
|
|
141
|
+
function parseGithubUrl(rawUrl) {
|
|
142
|
+
let url;
|
|
143
|
+
try {
|
|
144
|
+
url = new URL(rawUrl);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new Error(`Invalid GitHub URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
if (url.hostname !== "github.com") throw new Error("Only github.com URLs are supported for skill install");
|
|
149
|
+
const rawIndex = rawUrl.toLowerCase().indexOf("/tree/");
|
|
150
|
+
if (rawIndex !== -1) {
|
|
151
|
+
const rawPath = rawUrl.slice(rawIndex + 6).split(/[?#]/)[0];
|
|
152
|
+
let decoded = rawPath;
|
|
153
|
+
try {
|
|
154
|
+
decoded = decodeURIComponent(rawPath);
|
|
155
|
+
} catch {}
|
|
156
|
+
if (/(^|\/)\.\.(\/|$)/.test(decoded)) throw new Error("Invalid skill path");
|
|
157
|
+
}
|
|
158
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
159
|
+
if (parts.length < 2) throw new Error("GitHub URL must include owner/repo");
|
|
160
|
+
const repository = normalizeRepo(`${parts[0]}/${parts[1]}`);
|
|
161
|
+
if (parts[2] === "tree" && parts.length >= 5) {
|
|
162
|
+
const ref = parts[3];
|
|
163
|
+
validateGitRef(ref);
|
|
164
|
+
const treePath = parts.slice(4).join("/");
|
|
165
|
+
return {
|
|
166
|
+
repository,
|
|
167
|
+
path: treePath ? sanitizeSkillPath(treePath) : null,
|
|
168
|
+
ref: ref || null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
repository,
|
|
173
|
+
path: null,
|
|
174
|
+
ref: null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function installationRoot(workspaceDir) {
|
|
178
|
+
return path.join(workspaceDir, "skills", ".marketplace");
|
|
179
|
+
}
|
|
180
|
+
function installsRecordPath(workspaceDir) {
|
|
181
|
+
return path.join(workspaceDir, "skills", ".cache", "marketplace-installs.json");
|
|
182
|
+
}
|
|
183
|
+
async function ensureInstallDirs(workspaceDir) {
|
|
184
|
+
await fs.mkdir(installationRoot(workspaceDir), { recursive: true });
|
|
185
|
+
await fs.mkdir(path.dirname(installsRecordPath(workspaceDir)), { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
async function readInstallRecords(workspaceDir) {
|
|
188
|
+
try {
|
|
189
|
+
const raw = await fs.readFile(installsRecordPath(workspaceDir), "utf-8");
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
192
|
+
return parsed;
|
|
193
|
+
} catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function writeInstallRecords(workspaceDir, records) {
|
|
198
|
+
await ensureInstallDirs(workspaceDir);
|
|
199
|
+
await fs.writeFile(installsRecordPath(workspaceDir), JSON.stringify(records, null, 2), "utf-8");
|
|
200
|
+
}
|
|
201
|
+
function normalizeTags(raw) {
|
|
202
|
+
if (Array.isArray(raw)) return raw.map((t) => String(t ?? "").trim()).filter((t) => t.length > 0).slice(0, 10);
|
|
203
|
+
if (raw && typeof raw === "object") return Object.keys(raw).map((t) => t.trim()).filter((t) => t.length > 0).slice(0, 10);
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
function inferRepository(skill) {
|
|
207
|
+
const candidates = [
|
|
208
|
+
skill.repository,
|
|
209
|
+
skill.repo,
|
|
210
|
+
skill.gitRepo,
|
|
211
|
+
skill.github,
|
|
212
|
+
skill.githubRepo,
|
|
213
|
+
skill.git?.repo
|
|
214
|
+
];
|
|
215
|
+
for (const value of candidates) {
|
|
216
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
217
|
+
try {
|
|
218
|
+
return normalizeRepo(value);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
logger.debug(`[skill-marketplace] Failed to normalize repo: ${err instanceof Error ? err.message : err}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const githubUrl = skill.githubUrl;
|
|
224
|
+
if (typeof githubUrl === "string" && githubUrl.includes("github.com")) try {
|
|
225
|
+
const parts = new URL(githubUrl).pathname.split("/").filter(Boolean);
|
|
226
|
+
if (parts.length >= 2) return normalizeRepo(`${parts[0]}/${parts[1]}`);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
logger.debug(`[skill-marketplace] Failed to normalize repo: ${err instanceof Error ? err.message : err}`);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function inferPath(skill) {
|
|
233
|
+
const candidates = [
|
|
234
|
+
skill.path,
|
|
235
|
+
skill.skillPath,
|
|
236
|
+
skill.installPath,
|
|
237
|
+
skill.directory
|
|
238
|
+
];
|
|
239
|
+
for (const value of candidates) {
|
|
240
|
+
if (typeof value !== "string") continue;
|
|
241
|
+
const cleaned = value.replace(/^\/+/, "").trim();
|
|
242
|
+
if (cleaned && !cleaned.startsWith("..") && !cleaned.includes("/..")) return cleaned;
|
|
243
|
+
}
|
|
244
|
+
const githubUrl = skill.githubUrl;
|
|
245
|
+
if (typeof githubUrl === "string" && githubUrl.includes("/tree/")) {
|
|
246
|
+
const treeIndex = githubUrl.indexOf("/tree/");
|
|
247
|
+
const afterTree = githubUrl.slice(treeIndex + 6);
|
|
248
|
+
const slashIndex = afterTree.indexOf("/");
|
|
249
|
+
if (slashIndex !== -1) {
|
|
250
|
+
const pathPart = afterTree.slice(slashIndex + 1);
|
|
251
|
+
if (pathPart && !pathPart.startsWith("..") && !pathPart.includes("/..")) return pathPart;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function inferName(skill, fallbackId) {
|
|
257
|
+
const candidates = [
|
|
258
|
+
skill.displayName,
|
|
259
|
+
skill.slug,
|
|
260
|
+
skill.name,
|
|
261
|
+
skill.id,
|
|
262
|
+
skill.title
|
|
263
|
+
];
|
|
264
|
+
for (const value of candidates) {
|
|
265
|
+
if (typeof value !== "string") continue;
|
|
266
|
+
const cleaned = value.trim();
|
|
267
|
+
if (cleaned) return cleaned;
|
|
268
|
+
}
|
|
269
|
+
if (fallbackId.includes("/")) return fallbackId.split("/").pop() || fallbackId;
|
|
270
|
+
return fallbackId;
|
|
271
|
+
}
|
|
272
|
+
function inferDescription(skill) {
|
|
273
|
+
const candidates = [
|
|
274
|
+
skill.description,
|
|
275
|
+
skill.summary,
|
|
276
|
+
skill.shortDescription
|
|
277
|
+
];
|
|
278
|
+
for (const value of candidates) if (typeof value === "string" && value.trim()) return value.trim();
|
|
279
|
+
return "";
|
|
280
|
+
}
|
|
281
|
+
function resolveMarketplaceBaseUrl() {
|
|
282
|
+
return process.env.SKILLS_REGISTRY?.trim() || process.env.CLAWHUB_REGISTRY?.trim() || process.env.SKILLS_MARKETPLACE_URL?.trim() || DEFAULT_SKILLS_MARKETPLACE_URL;
|
|
283
|
+
}
|
|
284
|
+
function isLegacySkillsmp(baseUrl) {
|
|
285
|
+
try {
|
|
286
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
287
|
+
return hostname === LEGACY_SKILLSMP_HOST || hostname.endsWith(".skillsmp.com");
|
|
288
|
+
} catch {
|
|
289
|
+
return baseUrl.toLowerCase().includes(LEGACY_SKILLSMP_HOST);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function searchSkillsMarketplace(query, opts) {
|
|
293
|
+
const baseUrl = resolveMarketplaceBaseUrl();
|
|
294
|
+
const legacySkillsmp = isLegacySkillsmp(baseUrl);
|
|
295
|
+
const endpoint = legacySkillsmp ? opts?.aiSearch ? "/api/v1/skills/ai-search" : "/api/v1/skills/search" : "/api/v1/search";
|
|
296
|
+
const url = new URL(`${baseUrl}${endpoint}`);
|
|
297
|
+
if (query.trim()) url.searchParams.set("q", query.trim());
|
|
298
|
+
url.searchParams.set("limit", String(Math.max(1, Math.min(opts?.limit ?? 20, 50))));
|
|
299
|
+
const headers = { Accept: "application/json" };
|
|
300
|
+
if (legacySkillsmp) {
|
|
301
|
+
const apiKey = process.env.SKILLSMP_API_KEY?.trim();
|
|
302
|
+
if (!apiKey) throw new Error("SKILLSMP_API_KEY is not set. Add it to enable Skills marketplace search.");
|
|
303
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
304
|
+
}
|
|
305
|
+
const searchSpan = createIntegrationTelemetrySpan({
|
|
306
|
+
boundary: "marketplace",
|
|
307
|
+
operation: "search_skills_marketplace",
|
|
308
|
+
timeoutMs: FETCH_TIMEOUT_MS
|
|
309
|
+
});
|
|
310
|
+
let resp;
|
|
311
|
+
try {
|
|
312
|
+
resp = await fetch(url, {
|
|
313
|
+
headers,
|
|
314
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
searchSpan.failure({ error: err });
|
|
318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
+
throw new Error(msg.includes("aborted") || msg.includes("timeout") ? `Skills marketplace request timed out after ${FETCH_TIMEOUT_MS / 1e3}s` : `Skills marketplace network error: ${msg}`);
|
|
320
|
+
}
|
|
321
|
+
const payload = await resp.json().catch(() => ({}));
|
|
322
|
+
if (!resp.ok) {
|
|
323
|
+
searchSpan.failure({
|
|
324
|
+
statusCode: resp.status,
|
|
325
|
+
errorKind: "http_error"
|
|
326
|
+
});
|
|
327
|
+
const msg = payload.error?.message;
|
|
328
|
+
throw new Error(typeof msg === "string" && msg ? msg : `Skills marketplace request failed (${resp.status})`);
|
|
329
|
+
}
|
|
330
|
+
const buckets = [
|
|
331
|
+
payload.results,
|
|
332
|
+
payload.skills,
|
|
333
|
+
payload.data
|
|
334
|
+
];
|
|
335
|
+
let list = [];
|
|
336
|
+
for (const bucket of buckets) {
|
|
337
|
+
if (Array.isArray(bucket)) {
|
|
338
|
+
list = bucket;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
if (bucket && typeof bucket === "object" && Array.isArray(bucket.results)) {
|
|
342
|
+
list = bucket.results;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
if (bucket && typeof bucket === "object" && Array.isArray(bucket.skills)) {
|
|
346
|
+
list = bucket.skills;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const out = [];
|
|
351
|
+
for (const entry of list) {
|
|
352
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
353
|
+
const skill = entry;
|
|
354
|
+
const slug = typeof skill.slug === "string" ? skill.slug.trim() : "";
|
|
355
|
+
const repository = inferRepository(skill);
|
|
356
|
+
if (!repository && !slug) continue;
|
|
357
|
+
const name = inferName(skill, repository || slug);
|
|
358
|
+
const description = inferDescription(skill);
|
|
359
|
+
const skillPath = inferPath(skill);
|
|
360
|
+
const scoreValue = skill.score;
|
|
361
|
+
const score = typeof scoreValue === "number" && Number.isFinite(scoreValue) ? scoreValue : null;
|
|
362
|
+
const githubUrl = typeof skill.githubUrl === "string" && skill.githubUrl.trim() ? skill.githubUrl.trim() : repository ? `https://github.com/${repository}` : void 0;
|
|
363
|
+
out.push({
|
|
364
|
+
id: String(skill.id ?? slug ?? name),
|
|
365
|
+
slug: slug || void 0,
|
|
366
|
+
name,
|
|
367
|
+
description,
|
|
368
|
+
repository: repository || void 0,
|
|
369
|
+
githubUrl,
|
|
370
|
+
path: skillPath,
|
|
371
|
+
tags: normalizeTags(skill.tags ?? skill.topics),
|
|
372
|
+
score,
|
|
373
|
+
source: legacySkillsmp ? "skillsmp" : "clawhub"
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
searchSpan.success({ statusCode: resp.status });
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
async function runGitCloneSubset(repository, ref, skillPath, targetDir) {
|
|
380
|
+
validateGitRef(ref);
|
|
381
|
+
if (skillPath !== ".") sanitizeSkillPath(skillPath);
|
|
382
|
+
await withTemporarySparseCheckout(repository, ref, skillPath, async (cloneDir) => {
|
|
383
|
+
const sourceDir = path.join(cloneDir, skillPath);
|
|
384
|
+
assertPathWithinRoot(cloneDir, sourceDir);
|
|
385
|
+
const stat = await fs.stat(sourceDir).catch(() => null);
|
|
386
|
+
if (!stat || !stat.isDirectory()) throw new Error(`Skill path not found in repository: ${skillPath}`);
|
|
387
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
388
|
+
await fs.cp(sourceDir, targetDir, {
|
|
389
|
+
recursive: true,
|
|
390
|
+
errorOnExist: true,
|
|
391
|
+
force: false
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async function resolveSkillPathInRepo(repository, ref, requestedPath) {
|
|
396
|
+
validateGitRef(ref);
|
|
397
|
+
if (requestedPath) return sanitizeSkillPath(requestedPath);
|
|
398
|
+
return withTemporarySparseCheckout(repository, ref, ".", async (cloneDir) => {
|
|
399
|
+
const rootSkill = path.join(cloneDir, "SKILL.md");
|
|
400
|
+
if (await fs.stat(rootSkill).then((s) => s.isFile()).catch(() => false)) return ".";
|
|
401
|
+
const skillsDir = path.join(cloneDir, "skills");
|
|
402
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []);
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
if (!entry.isDirectory()) continue;
|
|
405
|
+
const candidate = path.join(skillsDir, entry.name, "SKILL.md");
|
|
406
|
+
if (await fs.stat(candidate).then((s) => s.isFile()).catch(() => false)) return path.posix.join("skills", entry.name);
|
|
407
|
+
}
|
|
408
|
+
throw new Error("Could not determine skill path automatically. Provide an explicit GitHub tree URL or path.");
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async function withTemporarySparseCheckout(repository, ref, checkoutPath, task) {
|
|
412
|
+
const repoUrl = `https://github.com/${repository}.git`;
|
|
413
|
+
const tmpBase = await fs.mkdtemp(path.join(stateDirBase(), "skill-probe-"));
|
|
414
|
+
const cloneDir = path.join(tmpBase, "repo");
|
|
415
|
+
try {
|
|
416
|
+
await execFileAsync("git", [
|
|
417
|
+
"clone",
|
|
418
|
+
"--depth",
|
|
419
|
+
"1",
|
|
420
|
+
"--filter=blob:none",
|
|
421
|
+
"--sparse",
|
|
422
|
+
"--branch",
|
|
423
|
+
ref,
|
|
424
|
+
repoUrl,
|
|
425
|
+
cloneDir
|
|
426
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
427
|
+
await execFileAsync("git", [
|
|
428
|
+
"-C",
|
|
429
|
+
cloneDir,
|
|
430
|
+
"sparse-checkout",
|
|
431
|
+
"set",
|
|
432
|
+
checkoutPath
|
|
433
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
434
|
+
return await task(cloneDir);
|
|
435
|
+
} finally {
|
|
436
|
+
await fs.rm(tmpBase, {
|
|
437
|
+
recursive: true,
|
|
438
|
+
force: true
|
|
439
|
+
}).catch(() => void 0);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function installMarketplaceSkill(workspaceDir, input) {
|
|
443
|
+
await ensureInstallDirs(workspaceDir);
|
|
444
|
+
let repository = input.repository?.trim() ? normalizeRepo(input.repository) : null;
|
|
445
|
+
let requestedPath = input.path?.trim() ? sanitizeSkillPath(input.path) : null;
|
|
446
|
+
let gitRef = "main";
|
|
447
|
+
if (input.githubUrl?.trim()) {
|
|
448
|
+
const parsed = parseGithubUrl(input.githubUrl.trim());
|
|
449
|
+
repository = parsed.repository;
|
|
450
|
+
if (!requestedPath && parsed.path) requestedPath = parsed.path;
|
|
451
|
+
if (parsed.ref) gitRef = parsed.ref;
|
|
452
|
+
}
|
|
453
|
+
if (!repository) throw new Error("Install requires a repository or GitHub URL");
|
|
454
|
+
const skillPath = await resolveSkillPathInRepo(repository, gitRef, requestedPath);
|
|
455
|
+
const id = safeName(input.name?.trim() || path.posix.basename(skillPath === "." ? repository.split("/")[1] : skillPath));
|
|
456
|
+
const targetDir = path.join(installationRoot(workspaceDir), id);
|
|
457
|
+
if (await fs.stat(targetDir).then(() => true).catch(() => false)) throw new Error(`Skill "${id}" is already installed`);
|
|
458
|
+
await runGitCloneSubset(repository, gitRef, skillPath, targetDir);
|
|
459
|
+
const skillDoc = path.join(targetDir, "SKILL.md");
|
|
460
|
+
if (!await fs.stat(skillDoc).then((s) => s.isFile()).catch(() => false)) {
|
|
461
|
+
await fs.rm(targetDir, {
|
|
462
|
+
recursive: true,
|
|
463
|
+
force: true
|
|
464
|
+
}).catch(() => void 0);
|
|
465
|
+
throw new Error("Installed path does not contain SKILL.md");
|
|
466
|
+
}
|
|
467
|
+
const scanReport = await runSkillSecurityScan(targetDir);
|
|
468
|
+
const scanStatus = scanReport.status;
|
|
469
|
+
if (scanReport.status === "blocked") {
|
|
470
|
+
await fs.rm(targetDir, {
|
|
471
|
+
recursive: true,
|
|
472
|
+
force: true
|
|
473
|
+
}).catch(() => void 0);
|
|
474
|
+
const reasons = [...scanReport.findings.map((f) => f.message), ...scanReport.manifestFindings.map((f) => f.message)];
|
|
475
|
+
throw new Error(`Skill "${id}" blocked by security scan: ${reasons.join("; ")}`);
|
|
476
|
+
}
|
|
477
|
+
if (scanReport.status === "critical" || scanReport.status === "warning") logger.warn(`[skills-marketplace] Security scan for "${id}": ${scanReport.status} (${scanReport.summary.critical} critical, ${scanReport.summary.warn} warnings)`);
|
|
478
|
+
const record = {
|
|
479
|
+
id,
|
|
480
|
+
name: input.name?.trim() || id,
|
|
481
|
+
description: input.description?.trim() || "",
|
|
482
|
+
repository,
|
|
483
|
+
githubUrl: `https://github.com/${repository}`,
|
|
484
|
+
path: skillPath,
|
|
485
|
+
installPath: targetDir,
|
|
486
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
487
|
+
source: input.source ?? "manual",
|
|
488
|
+
scanStatus
|
|
489
|
+
};
|
|
490
|
+
const records = await readInstallRecords(workspaceDir);
|
|
491
|
+
records[id] = record;
|
|
492
|
+
await writeInstallRecords(workspaceDir, records);
|
|
493
|
+
logger.info(`[skills-marketplace] Installed ${record.id} from ${record.repository}:${record.path} (scan: ${scanStatus ?? "skipped"})`);
|
|
494
|
+
return record;
|
|
495
|
+
}
|
|
496
|
+
async function listInstalledMarketplaceSkills(workspaceDir) {
|
|
497
|
+
const records = await readInstallRecords(workspaceDir);
|
|
498
|
+
const values = Object.values(records);
|
|
499
|
+
values.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
|
|
500
|
+
return values;
|
|
501
|
+
}
|
|
502
|
+
async function uninstallMarketplaceSkill(workspaceDir, skillId) {
|
|
503
|
+
const id = safeName(skillId);
|
|
504
|
+
const records = await readInstallRecords(workspaceDir);
|
|
505
|
+
const existing = records[id];
|
|
506
|
+
if (!existing) throw new Error(`Installed marketplace skill "${id}" not found`);
|
|
507
|
+
const expectedRoot = path.resolve(installationRoot(workspaceDir));
|
|
508
|
+
const resolvedPath = path.resolve(existing.installPath);
|
|
509
|
+
if (!resolvedPath.startsWith(`${expectedRoot}${path.sep}`) || resolvedPath === expectedRoot) throw new Error(`Refusing to remove skill outside ${expectedRoot}`);
|
|
510
|
+
await fs.rm(existing.installPath, {
|
|
511
|
+
recursive: true,
|
|
512
|
+
force: true
|
|
513
|
+
});
|
|
514
|
+
delete records[id];
|
|
515
|
+
await writeInstallRecords(workspaceDir, records);
|
|
516
|
+
logger.info(`[skills-marketplace] Uninstalled ${id}`);
|
|
517
|
+
return existing;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//#endregion
|
|
521
|
+
export { installMarketplaceSkill, listInstalledMarketplaceSkills, searchSkillsMarketplace, uninstallMarketplaceSkill };
|