@floomhq/skills 0.2.9 → 0.2.10
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/index.js +106 -30
- package/dist/index.js.map +3 -3
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2014,6 +2014,19 @@ var scrypt = promisify(scryptCb);
|
|
|
2014
2014
|
// ../shared/src/install-targets.ts
|
|
2015
2015
|
import { homedir } from "node:os";
|
|
2016
2016
|
import { join } from "node:path";
|
|
2017
|
+
var INSTALL_TARGETS = [
|
|
2018
|
+
"generic",
|
|
2019
|
+
"all",
|
|
2020
|
+
"claude",
|
|
2021
|
+
"codex",
|
|
2022
|
+
"cursor",
|
|
2023
|
+
"gemini",
|
|
2024
|
+
"opencode",
|
|
2025
|
+
"kimi"
|
|
2026
|
+
];
|
|
2027
|
+
function isInstallTarget(value) {
|
|
2028
|
+
return !!value && INSTALL_TARGETS.includes(value);
|
|
2029
|
+
}
|
|
2017
2030
|
var COMPATIBLE_AGENTS = {
|
|
2018
2031
|
generic: ["Claude Code", "Codex CLI", "Cursor", "Gemini CLI", "OpenCode", "Kimi CLI"],
|
|
2019
2032
|
all: ["Claude Code", "Codex CLI", "Cursor", "Gemini CLI", "OpenCode", "Kimi CLI"],
|
|
@@ -2061,24 +2074,24 @@ function presetDir(target, opts) {
|
|
|
2061
2074
|
}
|
|
2062
2075
|
}
|
|
2063
2076
|
function resolveInstallDir(args) {
|
|
2077
|
+
const target = args.target ?? "generic";
|
|
2064
2078
|
if (args.to) {
|
|
2065
2079
|
return {
|
|
2066
|
-
target
|
|
2080
|
+
target,
|
|
2067
2081
|
dir: args.to,
|
|
2068
2082
|
origin: "explicit",
|
|
2069
|
-
compatibleAgents: COMPATIBLE_AGENTS[
|
|
2083
|
+
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2070
2084
|
};
|
|
2071
2085
|
}
|
|
2072
|
-
const targetEnvDir = envDirForTarget(
|
|
2086
|
+
const targetEnvDir = envDirForTarget(target);
|
|
2073
2087
|
if (targetEnvDir) {
|
|
2074
2088
|
return {
|
|
2075
|
-
target
|
|
2089
|
+
target,
|
|
2076
2090
|
dir: targetEnvDir,
|
|
2077
2091
|
origin: "env",
|
|
2078
|
-
compatibleAgents: COMPATIBLE_AGENTS[
|
|
2092
|
+
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2079
2093
|
};
|
|
2080
2094
|
}
|
|
2081
|
-
const target = args.target ?? "generic";
|
|
2082
2095
|
return {
|
|
2083
2096
|
target,
|
|
2084
2097
|
dir: presetDir(target, { global: args.global, cwd: args.cwd }),
|
|
@@ -2381,7 +2394,8 @@ var CONFIG_DIR = join3(homedir2(), ".floom");
|
|
|
2381
2394
|
var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
|
|
2382
2395
|
var DEFAULT_APP_URL = "https://skills.floom.dev";
|
|
2383
2396
|
var DEFAULT_API_URL = "https://skills.floom.dev/api/v1";
|
|
2384
|
-
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app"]);
|
|
2397
|
+
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app", "skills.wasm.floom.dev"]);
|
|
2398
|
+
var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["skills.floom.dev", "skills.wasm.floom.dev", "localhost", "127.0.0.1", "::1"]);
|
|
2385
2399
|
async function ensureDir() {
|
|
2386
2400
|
await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
2387
2401
|
}
|
|
@@ -2395,12 +2409,15 @@ async function readRawAuth() {
|
|
|
2395
2409
|
return JSON.parse(raw);
|
|
2396
2410
|
} catch (e) {
|
|
2397
2411
|
if (e.code === "ENOENT") return null;
|
|
2412
|
+
if (e instanceof SyntaxError) {
|
|
2413
|
+
throw new FloomError("AUTH_REQUIRED", "Invalid ~/.floom/auth.json. Run: floom login to refresh local auth.");
|
|
2414
|
+
}
|
|
2398
2415
|
throw e;
|
|
2399
2416
|
}
|
|
2400
2417
|
}
|
|
2401
2418
|
async function writeAuth(state) {
|
|
2402
2419
|
await ensureDir();
|
|
2403
|
-
await writeFile(AUTH_FILE, JSON.stringify({ ...state, apiUrl:
|
|
2420
|
+
await writeFile(AUTH_FILE, JSON.stringify({ ...state, apiUrl: trustedApiUrlOrDefault(state.apiUrl) }, null, 2), { mode: 384 });
|
|
2404
2421
|
try {
|
|
2405
2422
|
await chmod(AUTH_FILE, 384);
|
|
2406
2423
|
} catch {
|
|
@@ -2423,14 +2440,16 @@ function getAppUrl() {
|
|
|
2423
2440
|
}
|
|
2424
2441
|
function getApiBaseUrls(preferred) {
|
|
2425
2442
|
const explicitApiUrl = process.env.FLOOM_API_URL?.trim();
|
|
2426
|
-
if (explicitApiUrl) return [
|
|
2427
|
-
const primary =
|
|
2443
|
+
if (explicitApiUrl) return [trustedApiUrlOrDefault(explicitApiUrl)];
|
|
2444
|
+
const primary = trustedApiUrlOrDefault(preferred ?? getApiUrl());
|
|
2428
2445
|
const bases = [primary];
|
|
2429
2446
|
if (preferred && !process.env.FLOOM_APP_URL) bases.push(DEFAULT_API_URL);
|
|
2430
2447
|
return Array.from(new Set(bases));
|
|
2431
2448
|
}
|
|
2432
2449
|
function normalizeApiUrl(apiUrl) {
|
|
2433
|
-
|
|
2450
|
+
if (typeof apiUrl !== "string") return DEFAULT_API_URL;
|
|
2451
|
+
const trimmed = apiUrl.trim().replace(/\/$/, "");
|
|
2452
|
+
if (!trimmed) return DEFAULT_API_URL;
|
|
2434
2453
|
try {
|
|
2435
2454
|
const url = new URL(trimmed);
|
|
2436
2455
|
if (LEGACY_API_HOSTS.has(url.hostname)) return DEFAULT_API_URL;
|
|
@@ -2439,6 +2458,22 @@ function normalizeApiUrl(apiUrl) {
|
|
|
2439
2458
|
}
|
|
2440
2459
|
return trimmed;
|
|
2441
2460
|
}
|
|
2461
|
+
function allowsCustomApiUrl() {
|
|
2462
|
+
const raw = process.env.FLOOM_ALLOW_CUSTOM_API_URL?.trim().toLowerCase();
|
|
2463
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
2464
|
+
}
|
|
2465
|
+
function isTrustedApiUrl(apiUrl) {
|
|
2466
|
+
try {
|
|
2467
|
+
const url = new URL(apiUrl);
|
|
2468
|
+
return TRUSTED_API_HOSTS.has(url.hostname) || allowsCustomApiUrl();
|
|
2469
|
+
} catch {
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
function trustedApiUrlOrDefault(apiUrl) {
|
|
2474
|
+
const normalized = normalizeApiUrl(apiUrl);
|
|
2475
|
+
return isTrustedApiUrl(normalized) ? normalized : DEFAULT_API_URL;
|
|
2476
|
+
}
|
|
2442
2477
|
function isLegacyApiUrl(apiUrl) {
|
|
2443
2478
|
if (!apiUrl) return false;
|
|
2444
2479
|
try {
|
|
@@ -2449,7 +2484,7 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2449
2484
|
}
|
|
2450
2485
|
|
|
2451
2486
|
// src/version.ts
|
|
2452
|
-
var VERSION = "0.2.
|
|
2487
|
+
var VERSION = "0.2.10";
|
|
2453
2488
|
|
|
2454
2489
|
// src/api-client.ts
|
|
2455
2490
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2491,7 +2526,8 @@ async function api(path, opts = {}) {
|
|
|
2491
2526
|
"User-Agent": `floom-cli/${VERSION}`,
|
|
2492
2527
|
"x-floom-cli-version": VERSION
|
|
2493
2528
|
};
|
|
2494
|
-
|
|
2529
|
+
const requestToken = opts.tokenOverride ?? token;
|
|
2530
|
+
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
|
|
2495
2531
|
let res;
|
|
2496
2532
|
try {
|
|
2497
2533
|
res = await fetchWithTimeout(url.toString(), {
|
|
@@ -2588,8 +2624,9 @@ async function loginCommand() {
|
|
|
2588
2624
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2589
2625
|
while (Date.now() < deadline) {
|
|
2590
2626
|
await new Promise((r) => setTimeout(r, interval));
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2627
|
+
const poll = await api(`/cli/sessions/${session.session_id}`, {
|
|
2628
|
+
tokenOverride: session.device_code
|
|
2629
|
+
});
|
|
2593
2630
|
if (poll.status === "approved" && poll.token && poll.handle && poll.email) {
|
|
2594
2631
|
await writeAuth({
|
|
2595
2632
|
token: poll.token,
|
|
@@ -2958,7 +2995,8 @@ async function publishCommand(opts = {}) {
|
|
|
2958
2995
|
log.ok(`Published ${complete.ref}`);
|
|
2959
2996
|
log.blank();
|
|
2960
2997
|
log.info("View:");
|
|
2961
|
-
|
|
2998
|
+
const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
|
|
2999
|
+
log.kv("", `${displayApiUrl.replace("/api/v1", "")}/@${handle}/${manifest.name}`);
|
|
2962
3000
|
log.info("Install:");
|
|
2963
3001
|
log.kv("", complete.install_command);
|
|
2964
3002
|
}
|
|
@@ -3069,6 +3107,12 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3069
3107
|
log.info("Expected: @owner/slug, workspace-slug/slug, or with @version suffix");
|
|
3070
3108
|
process.exit(1);
|
|
3071
3109
|
}
|
|
3110
|
+
if (opts.for && !isInstallTarget(opts.for)) {
|
|
3111
|
+
log.err(`Invalid install target: ${opts.for}`);
|
|
3112
|
+
log.info("Expected: claude | codex | cursor | gemini | opencode | kimi | all");
|
|
3113
|
+
process.exit(1);
|
|
3114
|
+
}
|
|
3115
|
+
const installTarget = isInstallTarget(opts.for) ? opts.for : "generic";
|
|
3072
3116
|
let info;
|
|
3073
3117
|
try {
|
|
3074
3118
|
info = await api(`/skills/${ref.owner}/${ref.slug}`);
|
|
@@ -3093,7 +3137,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3093
3137
|
process.exit(1);
|
|
3094
3138
|
}
|
|
3095
3139
|
const target = resolveInstallDir({
|
|
3096
|
-
target:
|
|
3140
|
+
target: installTarget,
|
|
3097
3141
|
to: opts.to,
|
|
3098
3142
|
global: opts.global
|
|
3099
3143
|
});
|
|
@@ -3154,7 +3198,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3154
3198
|
bundle_sha256: dl.bundle_sha256,
|
|
3155
3199
|
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3156
3200
|
path: destFolder.replace(projectDir + "/", ""),
|
|
3157
|
-
preset:
|
|
3201
|
+
preset: installTarget
|
|
3158
3202
|
});
|
|
3159
3203
|
await writeLock(projectDir, next);
|
|
3160
3204
|
log.blank();
|
|
@@ -3935,6 +3979,15 @@ import { z as z3 } from "zod";
|
|
|
3935
3979
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3936
3980
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3937
3981
|
var API_TIMEOUT_MS = 2e4;
|
|
3982
|
+
function semverGte2(a, b) {
|
|
3983
|
+
const pa = a.split(".").map(Number);
|
|
3984
|
+
const pb = b.split(".").map(Number);
|
|
3985
|
+
for (let i = 0; i < 3; i++) {
|
|
3986
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
3987
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
3988
|
+
}
|
|
3989
|
+
return true;
|
|
3990
|
+
}
|
|
3938
3991
|
async function fetchWithTimeout2(url, init = {}) {
|
|
3939
3992
|
const controller = new AbortController();
|
|
3940
3993
|
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
@@ -3992,10 +4045,14 @@ async function apiRequest(token, path, query) {
|
|
|
3992
4045
|
}
|
|
3993
4046
|
throw lastError ?? new Error("API request failed");
|
|
3994
4047
|
}
|
|
3995
|
-
async function installViaApi(token, refText, target) {
|
|
4048
|
+
async function installViaApi(token, refText, target, options = {}) {
|
|
3996
4049
|
const parsed = parseSkillRef(refText);
|
|
3997
4050
|
if (!parsed) throw new Error(`Invalid ref: ${refText}`);
|
|
4051
|
+
if (!isInstallTarget(target)) throw new Error(`Invalid install target: ${target}`);
|
|
3998
4052
|
const info = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
4053
|
+
if (info.min_floom_version && !semverGte2(VERSION, info.min_floom_version)) {
|
|
4054
|
+
throw new Error(`This skill requires Floom CLI >= ${info.min_floom_version} (you have ${VERSION}).`);
|
|
4055
|
+
}
|
|
3999
4056
|
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`, parsed.version ? { version: parsed.version } : void 0);
|
|
4000
4057
|
const bundle = await rawGet(dl.download.url);
|
|
4001
4058
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
@@ -4003,6 +4060,9 @@ async function installViaApi(token, refText, target) {
|
|
|
4003
4060
|
await mkdir9(install.dir, { recursive: true });
|
|
4004
4061
|
const dest = join13(install.dir, parsed.slug);
|
|
4005
4062
|
const exists = await readdir4(dest).then(() => true).catch(() => false);
|
|
4063
|
+
if (exists && !options.force) {
|
|
4064
|
+
throw new Error(`Folder already exists at ${dest}. Call install_skill with force=true to overwrite after reviewing local changes.`);
|
|
4065
|
+
}
|
|
4006
4066
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
4007
4067
|
const temp = await mkdtemp(join13(tmpdir3(), `floom-mcp-${parsed.slug}-`));
|
|
4008
4068
|
try {
|
|
@@ -4024,7 +4084,7 @@ async function installViaApi(token, refText, target) {
|
|
|
4024
4084
|
preset: target
|
|
4025
4085
|
});
|
|
4026
4086
|
await writeLock(process.cwd(), next);
|
|
4027
|
-
return { path: dest, version: dl.version, ref: info.ref ?? ref };
|
|
4087
|
+
return { path: dest, version: dl.version, ref: info.ref ?? ref, has_scripts: !!dl.has_scripts };
|
|
4028
4088
|
}
|
|
4029
4089
|
async function parseSkillBundle(bundle) {
|
|
4030
4090
|
const tmp = await mkdtemp(join13(tmpdir3(), "floom-mcp-read-"));
|
|
@@ -4123,9 +4183,13 @@ async function mcpCommand() {
|
|
|
4123
4183
|
const result = await apiRequest(token, `/libraries/${workspaceSlug}/pins`, { target });
|
|
4124
4184
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4125
4185
|
});
|
|
4126
|
-
server.tool("install_skill", {
|
|
4186
|
+
server.tool("install_skill", {
|
|
4187
|
+
ref: z3.string().min(3),
|
|
4188
|
+
target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]),
|
|
4189
|
+
force: z3.boolean().optional().default(false)
|
|
4190
|
+
}, async ({ ref, target, force }) => {
|
|
4127
4191
|
const token = await resolveOptionalToken();
|
|
4128
|
-
const installed = await installViaApi(token, ref, target);
|
|
4192
|
+
const installed = await installViaApi(token, ref, target, { force });
|
|
4129
4193
|
return { content: [{ type: "text", text: JSON.stringify(installed) }] };
|
|
4130
4194
|
});
|
|
4131
4195
|
const transport = new StdioServerTransport();
|
|
@@ -4166,6 +4230,15 @@ function warn(name, detail) {
|
|
|
4166
4230
|
function fail(name, detail) {
|
|
4167
4231
|
return { name, ok: false, detail };
|
|
4168
4232
|
}
|
|
4233
|
+
function apiUrlCheck(rawApiUrl, label = "api_url") {
|
|
4234
|
+
const normalized = normalizeApiUrl(rawApiUrl);
|
|
4235
|
+
const trusted = trustedApiUrlOrDefault(rawApiUrl);
|
|
4236
|
+
if (isLegacyApiUrl(rawApiUrl)) return warn(label, `legacy URL ${rawApiUrl}; using ${DEFAULT_API_URL}`);
|
|
4237
|
+
if (normalized !== trusted) {
|
|
4238
|
+
return warn(label, `ignored untrusted API URL ${normalized}; set FLOOM_ALLOW_CUSTOM_API_URL=1 only for a trusted self-hosted Floom API`);
|
|
4239
|
+
}
|
|
4240
|
+
return pass(label, trusted);
|
|
4241
|
+
}
|
|
4169
4242
|
async function validateCurrentToken(token) {
|
|
4170
4243
|
if (!token) return warn("fresh_agent_auth", "missing token; authenticated MCP calls skipped");
|
|
4171
4244
|
try {
|
|
@@ -4195,7 +4268,7 @@ async function doctorCommand(opts = {}) {
|
|
|
4195
4268
|
const checks2 = [
|
|
4196
4269
|
pass("cli_version", VERSION),
|
|
4197
4270
|
authCheck2,
|
|
4198
|
-
process.env.FLOOM_API_URL ?
|
|
4271
|
+
process.env.FLOOM_API_URL ? apiUrlCheck(process.env.FLOOM_API_URL) : isLegacyApiUrl(rawAuth?.apiUrl) ? warn("auth_api_url", `legacy URL in ~/.floom/auth.json; using ${DEFAULT_API_URL}`) : apiUrlCheck(auth2?.apiUrl ?? DEFAULT_API_URL)
|
|
4199
4272
|
];
|
|
4200
4273
|
emitDoctor(checks2, opts.json);
|
|
4201
4274
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
@@ -4225,7 +4298,7 @@ async function doctorCommand(opts = {}) {
|
|
|
4225
4298
|
HOME: tmpHome,
|
|
4226
4299
|
FLOOM_SKILLS_DIR: tmpSkills,
|
|
4227
4300
|
...hasValidToken && token ? { FLOOM_API_TOKEN: token } : {},
|
|
4228
|
-
...process.env.FLOOM_API_URL ? { FLOOM_API_URL:
|
|
4301
|
+
...process.env.FLOOM_API_URL ? { FLOOM_API_URL: trustedApiUrlOrDefault(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: trustedApiUrlOrDefault(auth.apiUrl) } : {}
|
|
4229
4302
|
},
|
|
4230
4303
|
stderr: "pipe"
|
|
4231
4304
|
});
|
|
@@ -4251,12 +4324,15 @@ async function doctorCommand(opts = {}) {
|
|
|
4251
4324
|
} else {
|
|
4252
4325
|
checks.push(warn("mcp_authenticated_tools", "skipped list_workspaces/search_skills because no valid token is available"));
|
|
4253
4326
|
}
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4327
|
+
if (opts.ref) {
|
|
4328
|
+
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
4329
|
+
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
4330
|
+
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
4331
|
+
const entries = await readdir5(tmpSkills);
|
|
4332
|
+
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
4333
|
+
} else {
|
|
4334
|
+
checks.push(warn("mcp_public_skill", "skipped; pass --ref <public-skill-ref> to verify get_skill/install_skill against a known public skill"));
|
|
4335
|
+
}
|
|
4260
4336
|
} catch (e) {
|
|
4261
4337
|
checks.push(fail("fresh_agent_mcp", e.message));
|
|
4262
4338
|
} finally {
|