@floomhq/skills 0.2.4 → 0.2.6
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 +117 -39
- package/dist/index.js.map +3 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2362,10 +2362,15 @@ var CONFIG_DIR = join3(homedir2(), ".floom");
|
|
|
2362
2362
|
var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
|
|
2363
2363
|
var DEFAULT_APP_URL = "https://skills.floom.dev";
|
|
2364
2364
|
var DEFAULT_API_URL = "https://skills.floom.dev/api/v1";
|
|
2365
|
+
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app"]);
|
|
2365
2366
|
async function ensureDir() {
|
|
2366
2367
|
await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
2367
2368
|
}
|
|
2368
2369
|
async function readAuth() {
|
|
2370
|
+
const parsed = await readRawAuth();
|
|
2371
|
+
return parsed ? { ...parsed, apiUrl: normalizeApiUrl(parsed.apiUrl) } : null;
|
|
2372
|
+
}
|
|
2373
|
+
async function readRawAuth() {
|
|
2369
2374
|
try {
|
|
2370
2375
|
const raw = await readFile3(AUTH_FILE, "utf8");
|
|
2371
2376
|
return JSON.parse(raw);
|
|
@@ -2376,7 +2381,7 @@ async function readAuth() {
|
|
|
2376
2381
|
}
|
|
2377
2382
|
async function writeAuth(state) {
|
|
2378
2383
|
await ensureDir();
|
|
2379
|
-
await writeFile(AUTH_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
2384
|
+
await writeFile(AUTH_FILE, JSON.stringify({ ...state, apiUrl: normalizeApiUrl(state.apiUrl) }, null, 2), { mode: 384 });
|
|
2380
2385
|
try {
|
|
2381
2386
|
await chmod(AUTH_FILE, 384);
|
|
2382
2387
|
} catch {
|
|
@@ -2399,15 +2404,33 @@ function getAppUrl() {
|
|
|
2399
2404
|
}
|
|
2400
2405
|
function getApiBaseUrls(preferred) {
|
|
2401
2406
|
const explicitApiUrl = process.env.FLOOM_API_URL?.trim();
|
|
2402
|
-
if (explicitApiUrl) return [explicitApiUrl
|
|
2403
|
-
const primary = (preferred ?? getApiUrl())
|
|
2407
|
+
if (explicitApiUrl) return [normalizeApiUrl(explicitApiUrl)];
|
|
2408
|
+
const primary = normalizeApiUrl(preferred ?? getApiUrl());
|
|
2404
2409
|
const bases = [primary];
|
|
2405
2410
|
if (preferred && !process.env.FLOOM_APP_URL) bases.push(DEFAULT_API_URL);
|
|
2406
2411
|
return Array.from(new Set(bases));
|
|
2407
2412
|
}
|
|
2413
|
+
function normalizeApiUrl(apiUrl) {
|
|
2414
|
+
const trimmed = apiUrl.replace(/\/$/, "");
|
|
2415
|
+
try {
|
|
2416
|
+
const url = new URL(trimmed);
|
|
2417
|
+
if (LEGACY_API_HOSTS.has(url.hostname)) return DEFAULT_API_URL;
|
|
2418
|
+
} catch {
|
|
2419
|
+
return DEFAULT_API_URL;
|
|
2420
|
+
}
|
|
2421
|
+
return trimmed;
|
|
2422
|
+
}
|
|
2423
|
+
function isLegacyApiUrl(apiUrl) {
|
|
2424
|
+
if (!apiUrl) return false;
|
|
2425
|
+
try {
|
|
2426
|
+
return LEGACY_API_HOSTS.has(new URL(apiUrl).hostname);
|
|
2427
|
+
} catch {
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2408
2431
|
|
|
2409
2432
|
// src/version.ts
|
|
2410
|
-
var VERSION = "0.2.
|
|
2433
|
+
var VERSION = "0.2.6";
|
|
2411
2434
|
|
|
2412
2435
|
// src/api-client.ts
|
|
2413
2436
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -3382,14 +3405,19 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
3382
3405
|
clearTimeout(timer);
|
|
3383
3406
|
}
|
|
3384
3407
|
}
|
|
3385
|
-
async function
|
|
3408
|
+
async function resolveOptionalToken() {
|
|
3386
3409
|
const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
|
|
3387
3410
|
if (fromEnv) return fromEnv;
|
|
3388
3411
|
const auth = await readAuth();
|
|
3389
3412
|
if (auth?.token) return auth.token;
|
|
3413
|
+
return null;
|
|
3414
|
+
}
|
|
3415
|
+
async function resolveRequiredToken() {
|
|
3416
|
+
const token = await resolveOptionalToken();
|
|
3417
|
+
if (token) return token;
|
|
3390
3418
|
throw new Error("Authentication required. Run `floom login` or set FLOOM_API_TOKEN.");
|
|
3391
3419
|
}
|
|
3392
|
-
async function
|
|
3420
|
+
async function apiRequest(token, path, query) {
|
|
3393
3421
|
const auth = await readAuth();
|
|
3394
3422
|
let lastError = null;
|
|
3395
3423
|
for (const base of getApiBaseUrls(auth?.apiUrl)) {
|
|
@@ -3400,7 +3428,10 @@ async function apiWithToken(token, path, query) {
|
|
|
3400
3428
|
let res;
|
|
3401
3429
|
try {
|
|
3402
3430
|
res = await fetchWithTimeout2(url, {
|
|
3403
|
-
headers: {
|
|
3431
|
+
headers: {
|
|
3432
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
3433
|
+
"Content-Type": "application/json"
|
|
3434
|
+
}
|
|
3404
3435
|
});
|
|
3405
3436
|
} catch (e) {
|
|
3406
3437
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
@@ -3422,8 +3453,8 @@ async function apiWithToken(token, path, query) {
|
|
|
3422
3453
|
async function installViaApi(token, refText, target) {
|
|
3423
3454
|
const parsed = parseSkillRef(refText);
|
|
3424
3455
|
if (!parsed) throw new Error(`Invalid ref: ${refText}`);
|
|
3425
|
-
const info = await
|
|
3426
|
-
const dl = await
|
|
3456
|
+
const info = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
3457
|
+
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`, parsed.version ? { version: parsed.version } : void 0);
|
|
3427
3458
|
const bundle = await rawGet(dl.download.url);
|
|
3428
3459
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
3429
3460
|
const install = resolveInstallDir({ target });
|
|
@@ -3480,30 +3511,30 @@ async function parseSkillBundle(bundle) {
|
|
|
3480
3511
|
async function mcpCommand() {
|
|
3481
3512
|
const server = new McpServer({ name: "floom", version: VERSION });
|
|
3482
3513
|
server.tool("search_skills", { query: z2.string().min(1), workspace: z2.string().optional(), library: z2.string().optional() }, async ({ query, workspace, library }) => {
|
|
3483
|
-
const token = await
|
|
3514
|
+
const token = await resolveRequiredToken();
|
|
3484
3515
|
const workspaceSlug = workspace ?? library;
|
|
3485
|
-
const result = await
|
|
3516
|
+
const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
|
|
3486
3517
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3487
3518
|
});
|
|
3488
3519
|
server.tool("get_skill", { ref: z2.string().min(3) }, async ({ ref }) => {
|
|
3489
|
-
const token = await
|
|
3520
|
+
const token = await resolveOptionalToken();
|
|
3490
3521
|
const parsed = parseSkillRef(ref);
|
|
3491
3522
|
if (!parsed) throw new Error("Invalid ref. Expected @owner/slug or workspace/slug");
|
|
3492
|
-
const meta = await
|
|
3493
|
-
const dl = await
|
|
3523
|
+
const meta = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
3524
|
+
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`);
|
|
3494
3525
|
const buf = await rawGet(dl.download.url);
|
|
3495
3526
|
const parsedBundle = await parseSkillBundle(buf);
|
|
3496
3527
|
return { content: [{ type: "text", text: JSON.stringify({ ref, meta, ...parsedBundle }) }] };
|
|
3497
3528
|
});
|
|
3498
3529
|
async function listWorkspaces() {
|
|
3499
|
-
const token = await
|
|
3500
|
-
const result = await
|
|
3530
|
+
const token = await resolveRequiredToken();
|
|
3531
|
+
const result = await apiRequest(token, "/libraries");
|
|
3501
3532
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3502
3533
|
}
|
|
3503
3534
|
server.tool("list_workspaces", {}, listWorkspaces);
|
|
3504
3535
|
server.tool("list_libraries", {}, listWorkspaces);
|
|
3505
3536
|
server.tool("install_skill", { ref: z2.string().min(3), target: z2.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]) }, async ({ ref, target }) => {
|
|
3506
|
-
const token = await
|
|
3537
|
+
const token = await resolveOptionalToken();
|
|
3507
3538
|
const installed = await installViaApi(token, ref, target);
|
|
3508
3539
|
return { content: [{ type: "text", text: JSON.stringify(installed) }] };
|
|
3509
3540
|
});
|
|
@@ -3520,6 +3551,22 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
3520
3551
|
function textOf(result) {
|
|
3521
3552
|
return String(result?.content?.[0]?.text ?? "");
|
|
3522
3553
|
}
|
|
3554
|
+
function toolError(result) {
|
|
3555
|
+
const text = textOf(result);
|
|
3556
|
+
if (result?.isError) return text || "tool returned an MCP error";
|
|
3557
|
+
if (/authentication required|forbidden|not found|invalid token/i.test(text)) return text;
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
function jsonArrayLength(result, key) {
|
|
3561
|
+
if (toolError(result)) return null;
|
|
3562
|
+
try {
|
|
3563
|
+
const parsed = JSON.parse(textOf(result));
|
|
3564
|
+
const value = parsed?.[key];
|
|
3565
|
+
return Array.isArray(value) ? value.length : null;
|
|
3566
|
+
} catch {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3523
3570
|
function pass(name, detail) {
|
|
3524
3571
|
return { name, ok: true, detail };
|
|
3525
3572
|
}
|
|
@@ -3529,10 +3576,37 @@ function warn(name, detail) {
|
|
|
3529
3576
|
function fail(name, detail) {
|
|
3530
3577
|
return { name, ok: false, detail };
|
|
3531
3578
|
}
|
|
3579
|
+
async function validateCurrentToken(token) {
|
|
3580
|
+
if (!token) return warn("fresh_agent_auth", "missing token; authenticated MCP calls skipped");
|
|
3581
|
+
try {
|
|
3582
|
+
const me = await api("/me", { authRequired: true });
|
|
3583
|
+
return pass("fresh_agent_auth", `@${me.user.handle}`);
|
|
3584
|
+
} catch (e) {
|
|
3585
|
+
return warn("fresh_agent_auth", `saved login rejected; authenticated MCP calls skipped: ${e.message}`);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3532
3588
|
async function doctorCommand(opts = {}) {
|
|
3533
3589
|
if (!opts.freshAgent) {
|
|
3534
3590
|
const auth2 = await readAuth();
|
|
3535
|
-
const
|
|
3591
|
+
const rawAuth = await readRawAuth();
|
|
3592
|
+
let authCheck2;
|
|
3593
|
+
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
3594
|
+
if (!auth2?.token && !envToken) {
|
|
3595
|
+
authCheck2 = fail("auth", "not logged in");
|
|
3596
|
+
} else {
|
|
3597
|
+
try {
|
|
3598
|
+
const me = await api("/me", { authRequired: true });
|
|
3599
|
+
const authSource = envToken ? "FLOOM_API_TOKEN" : "~/.floom/auth.json";
|
|
3600
|
+
authCheck2 = pass("auth", `@${me.user.handle} via ${authSource}`);
|
|
3601
|
+
} catch (e) {
|
|
3602
|
+
authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
const checks2 = [
|
|
3606
|
+
pass("cli_version", VERSION),
|
|
3607
|
+
authCheck2,
|
|
3608
|
+
process.env.FLOOM_API_URL ? isLegacyApiUrl(process.env.FLOOM_API_URL) ? warn("api_url", `legacy FLOOM_API_URL ${process.env.FLOOM_API_URL}; using ${DEFAULT_API_URL}`) : pass("api_url", process.env.FLOOM_API_URL) : isLegacyApiUrl(rawAuth?.apiUrl) ? warn("auth_api_url", `legacy URL in ~/.floom/auth.json; using ${DEFAULT_API_URL}`) : pass("api_url", auth2?.apiUrl ?? DEFAULT_API_URL)
|
|
3609
|
+
];
|
|
3536
3610
|
emitDoctor(checks2, opts.json);
|
|
3537
3611
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
3538
3612
|
return;
|
|
@@ -3540,9 +3614,9 @@ async function doctorCommand(opts = {}) {
|
|
|
3540
3614
|
const checks = [];
|
|
3541
3615
|
const auth = await readAuth();
|
|
3542
3616
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3617
|
+
const authCheck = await validateCurrentToken(token);
|
|
3618
|
+
const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
|
|
3619
|
+
checks.push(authCheck);
|
|
3546
3620
|
const cliPath = process.argv[1];
|
|
3547
3621
|
if (!cliPath) {
|
|
3548
3622
|
checks.push(fail("fresh_agent_cli_path", "process.argv[1] is empty"));
|
|
@@ -3560,8 +3634,8 @@ async function doctorCommand(opts = {}) {
|
|
|
3560
3634
|
...process.env,
|
|
3561
3635
|
HOME: tmpHome,
|
|
3562
3636
|
FLOOM_SKILLS_DIR: tmpSkills,
|
|
3563
|
-
...token ? { FLOOM_API_TOKEN: token } : {},
|
|
3564
|
-
...auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
|
|
3637
|
+
...hasValidToken && token ? { FLOOM_API_TOKEN: token } : {},
|
|
3638
|
+
...process.env.FLOOM_API_URL ? { FLOOM_API_URL: normalizeApiUrl(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
|
|
3565
3639
|
},
|
|
3566
3640
|
stderr: "pipe"
|
|
3567
3641
|
});
|
|
@@ -3570,25 +3644,29 @@ async function doctorCommand(opts = {}) {
|
|
|
3570
3644
|
await client.connect(transport);
|
|
3571
3645
|
const tools = await client.listTools();
|
|
3572
3646
|
const toolNames = tools.tools.map((tool) => tool.name).sort();
|
|
3573
|
-
const expected = ["get_skill", "install_skill", "list_workspaces", "search_skills"];
|
|
3647
|
+
const expected = ["get_skill", "install_skill", "list_libraries", "list_workspaces", "search_skills"];
|
|
3574
3648
|
const missing = expected.filter((name) => !toolNames.includes(name));
|
|
3575
3649
|
checks.push(missing.length === 0 ? pass("mcp_tools", toolNames.join(", ")) : fail("mcp_tools", `missing ${missing.join(", ")}`));
|
|
3576
|
-
if (
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
const entries = await readdir5(tmpSkills);
|
|
3590
|
-
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
3650
|
+
if (hasValidToken) {
|
|
3651
|
+
const workspaces = await client.callTool({ name: "list_workspaces", arguments: {} });
|
|
3652
|
+
const workspaceError = toolError(workspaces);
|
|
3653
|
+
const workspaceCount = jsonArrayLength(workspaces, "libraries");
|
|
3654
|
+
const workspacesOk = !workspaceError && workspaceCount !== null;
|
|
3655
|
+
checks.push(workspacesOk ? pass("mcp_list_workspaces", `${workspaceCount} workspaces`) : fail("mcp_list_workspaces", workspaceError ?? "unexpected response shape"));
|
|
3656
|
+
const search = await client.callTool({ name: "search_skills", arguments: { query: opts.query ?? "pdf" } });
|
|
3657
|
+
const searchError = toolError(search);
|
|
3658
|
+
const skillCount = jsonArrayLength(search, "skills");
|
|
3659
|
+
const searchOk = !searchError && skillCount !== null;
|
|
3660
|
+
checks.push(searchOk ? pass("mcp_search_skills", `${skillCount} skills`) : fail("mcp_search_skills", searchError ?? "unexpected response shape"));
|
|
3661
|
+
} else {
|
|
3662
|
+
checks.push(warn("mcp_authenticated_tools", "skipped list_workspaces/search_skills because no valid token is available"));
|
|
3591
3663
|
}
|
|
3664
|
+
const ref = opts.ref ?? "floom-demo/brand-voice";
|
|
3665
|
+
const skill = await client.callTool({ name: "get_skill", arguments: { ref } });
|
|
3666
|
+
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
3667
|
+
const installed = await client.callTool({ name: "install_skill", arguments: { ref, target: opts.target ?? "codex" } });
|
|
3668
|
+
const entries = await readdir5(tmpSkills);
|
|
3669
|
+
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
3592
3670
|
} catch (e) {
|
|
3593
3671
|
checks.push(fail("fresh_agent_mcp", e.message));
|
|
3594
3672
|
} finally {
|