@floomhq/skills 0.2.5 → 0.2.7
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 +81 -54
- package/dist/index.js.map +3 -3
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2024,6 +2024,23 @@ var COMPATIBLE_AGENTS = {
|
|
|
2024
2024
|
opencode: ["OpenCode", "Claude Code", "Codex CLI", "Kimi CLI"],
|
|
2025
2025
|
kimi: ["Kimi CLI", "Claude Code", "OpenCode"]
|
|
2026
2026
|
};
|
|
2027
|
+
var TARGET_ENV_DIRS = {
|
|
2028
|
+
generic: ["FLOOM_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2029
|
+
all: ["FLOOM_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2030
|
+
claude: ["FLOOM_SKILLS_DIR", "CLAUDE_SKILLS_DIR"],
|
|
2031
|
+
codex: ["FLOOM_SKILLS_DIR", "CODEX_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2032
|
+
cursor: ["FLOOM_SKILLS_DIR", "CURSOR_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2033
|
+
gemini: ["FLOOM_SKILLS_DIR", "GEMINI_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2034
|
+
opencode: ["FLOOM_SKILLS_DIR", "OPENCODE_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2035
|
+
kimi: ["FLOOM_SKILLS_DIR", "KIMI_SKILLS_DIR", "AGENTS_SKILLS_DIR"]
|
|
2036
|
+
};
|
|
2037
|
+
function envDirForTarget(target) {
|
|
2038
|
+
for (const key of TARGET_ENV_DIRS[target]) {
|
|
2039
|
+
const value = process.env[key]?.trim();
|
|
2040
|
+
if (value) return value;
|
|
2041
|
+
}
|
|
2042
|
+
return null;
|
|
2043
|
+
}
|
|
2027
2044
|
function presetDir(target, opts) {
|
|
2028
2045
|
const cwd = opts.cwd ?? process.cwd();
|
|
2029
2046
|
const root = opts.global ? homedir() : cwd;
|
|
@@ -2051,10 +2068,11 @@ function resolveInstallDir(args) {
|
|
|
2051
2068
|
compatibleAgents: COMPATIBLE_AGENTS[args.target ?? "generic"]
|
|
2052
2069
|
};
|
|
2053
2070
|
}
|
|
2054
|
-
|
|
2071
|
+
const targetEnvDir = envDirForTarget(args.target ?? "generic");
|
|
2072
|
+
if (targetEnvDir) {
|
|
2055
2073
|
return {
|
|
2056
2074
|
target: args.target ?? "generic",
|
|
2057
|
-
dir:
|
|
2075
|
+
dir: targetEnvDir,
|
|
2058
2076
|
origin: "env",
|
|
2059
2077
|
compatibleAgents: COMPATIBLE_AGENTS[args.target ?? "generic"]
|
|
2060
2078
|
};
|
|
@@ -2430,7 +2448,7 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2430
2448
|
}
|
|
2431
2449
|
|
|
2432
2450
|
// src/version.ts
|
|
2433
|
-
var VERSION = "0.2.
|
|
2451
|
+
var VERSION = "0.2.7";
|
|
2434
2452
|
|
|
2435
2453
|
// src/api-client.ts
|
|
2436
2454
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2621,6 +2639,7 @@ async function whoamiCommand() {
|
|
|
2621
2639
|
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
2622
2640
|
if (!auth && !envToken) {
|
|
2623
2641
|
log.info("Not logged in. Run: floom login");
|
|
2642
|
+
process.exitCode = 1;
|
|
2624
2643
|
return;
|
|
2625
2644
|
}
|
|
2626
2645
|
let me;
|
|
@@ -3405,14 +3424,19 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
3405
3424
|
clearTimeout(timer);
|
|
3406
3425
|
}
|
|
3407
3426
|
}
|
|
3408
|
-
async function
|
|
3427
|
+
async function resolveOptionalToken() {
|
|
3409
3428
|
const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
|
|
3410
3429
|
if (fromEnv) return fromEnv;
|
|
3411
3430
|
const auth = await readAuth();
|
|
3412
3431
|
if (auth?.token) return auth.token;
|
|
3432
|
+
return null;
|
|
3433
|
+
}
|
|
3434
|
+
async function resolveRequiredToken() {
|
|
3435
|
+
const token = await resolveOptionalToken();
|
|
3436
|
+
if (token) return token;
|
|
3413
3437
|
throw new Error("Authentication required. Run `floom login` or set FLOOM_API_TOKEN.");
|
|
3414
3438
|
}
|
|
3415
|
-
async function
|
|
3439
|
+
async function apiRequest(token, path, query) {
|
|
3416
3440
|
const auth = await readAuth();
|
|
3417
3441
|
let lastError = null;
|
|
3418
3442
|
for (const base of getApiBaseUrls(auth?.apiUrl)) {
|
|
@@ -3423,7 +3447,10 @@ async function apiWithToken(token, path, query) {
|
|
|
3423
3447
|
let res;
|
|
3424
3448
|
try {
|
|
3425
3449
|
res = await fetchWithTimeout2(url, {
|
|
3426
|
-
headers: {
|
|
3450
|
+
headers: {
|
|
3451
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
3452
|
+
"Content-Type": "application/json"
|
|
3453
|
+
}
|
|
3427
3454
|
});
|
|
3428
3455
|
} catch (e) {
|
|
3429
3456
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
@@ -3445,8 +3472,8 @@ async function apiWithToken(token, path, query) {
|
|
|
3445
3472
|
async function installViaApi(token, refText, target) {
|
|
3446
3473
|
const parsed = parseSkillRef(refText);
|
|
3447
3474
|
if (!parsed) throw new Error(`Invalid ref: ${refText}`);
|
|
3448
|
-
const info = await
|
|
3449
|
-
const dl = await
|
|
3475
|
+
const info = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
3476
|
+
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`, parsed.version ? { version: parsed.version } : void 0);
|
|
3450
3477
|
const bundle = await rawGet(dl.download.url);
|
|
3451
3478
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
3452
3479
|
const install = resolveInstallDir({ target });
|
|
@@ -3503,30 +3530,30 @@ async function parseSkillBundle(bundle) {
|
|
|
3503
3530
|
async function mcpCommand() {
|
|
3504
3531
|
const server = new McpServer({ name: "floom", version: VERSION });
|
|
3505
3532
|
server.tool("search_skills", { query: z2.string().min(1), workspace: z2.string().optional(), library: z2.string().optional() }, async ({ query, workspace, library }) => {
|
|
3506
|
-
const token = await
|
|
3533
|
+
const token = await resolveRequiredToken();
|
|
3507
3534
|
const workspaceSlug = workspace ?? library;
|
|
3508
|
-
const result = await
|
|
3535
|
+
const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
|
|
3509
3536
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3510
3537
|
});
|
|
3511
3538
|
server.tool("get_skill", { ref: z2.string().min(3) }, async ({ ref }) => {
|
|
3512
|
-
const token = await
|
|
3539
|
+
const token = await resolveOptionalToken();
|
|
3513
3540
|
const parsed = parseSkillRef(ref);
|
|
3514
3541
|
if (!parsed) throw new Error("Invalid ref. Expected @owner/slug or workspace/slug");
|
|
3515
|
-
const meta = await
|
|
3516
|
-
const dl = await
|
|
3542
|
+
const meta = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
3543
|
+
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`);
|
|
3517
3544
|
const buf = await rawGet(dl.download.url);
|
|
3518
3545
|
const parsedBundle = await parseSkillBundle(buf);
|
|
3519
3546
|
return { content: [{ type: "text", text: JSON.stringify({ ref, meta, ...parsedBundle }) }] };
|
|
3520
3547
|
});
|
|
3521
3548
|
async function listWorkspaces() {
|
|
3522
|
-
const token = await
|
|
3523
|
-
const result = await
|
|
3549
|
+
const token = await resolveRequiredToken();
|
|
3550
|
+
const result = await apiRequest(token, "/libraries");
|
|
3524
3551
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3525
3552
|
}
|
|
3526
3553
|
server.tool("list_workspaces", {}, listWorkspaces);
|
|
3527
3554
|
server.tool("list_libraries", {}, listWorkspaces);
|
|
3528
3555
|
server.tool("install_skill", { ref: z2.string().min(3), target: z2.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]) }, async ({ ref, target }) => {
|
|
3529
|
-
const token = await
|
|
3556
|
+
const token = await resolveOptionalToken();
|
|
3530
3557
|
const installed = await installViaApi(token, ref, target);
|
|
3531
3558
|
return { content: [{ type: "text", text: JSON.stringify(installed) }] };
|
|
3532
3559
|
});
|
|
@@ -3568,26 +3595,35 @@ function warn(name, detail) {
|
|
|
3568
3595
|
function fail(name, detail) {
|
|
3569
3596
|
return { name, ok: false, detail };
|
|
3570
3597
|
}
|
|
3598
|
+
async function validateCurrentToken(token) {
|
|
3599
|
+
if (!token) return warn("fresh_agent_auth", "missing token; authenticated MCP calls skipped");
|
|
3600
|
+
try {
|
|
3601
|
+
const me = await api("/me", { authRequired: true });
|
|
3602
|
+
return pass("fresh_agent_auth", `@${me.user.handle}`);
|
|
3603
|
+
} catch (e) {
|
|
3604
|
+
return warn("fresh_agent_auth", `saved login rejected; authenticated MCP calls skipped: ${e.message}`);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3571
3607
|
async function doctorCommand(opts = {}) {
|
|
3572
3608
|
if (!opts.freshAgent) {
|
|
3573
3609
|
const auth2 = await readAuth();
|
|
3574
3610
|
const rawAuth = await readRawAuth();
|
|
3575
|
-
let
|
|
3611
|
+
let authCheck2;
|
|
3576
3612
|
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
3577
3613
|
if (!auth2?.token && !envToken) {
|
|
3578
|
-
|
|
3614
|
+
authCheck2 = fail("auth", "not logged in");
|
|
3579
3615
|
} else {
|
|
3580
3616
|
try {
|
|
3581
3617
|
const me = await api("/me", { authRequired: true });
|
|
3582
3618
|
const authSource = envToken ? "FLOOM_API_TOKEN" : "~/.floom/auth.json";
|
|
3583
|
-
|
|
3619
|
+
authCheck2 = pass("auth", `@${me.user.handle} via ${authSource}`);
|
|
3584
3620
|
} catch (e) {
|
|
3585
|
-
|
|
3621
|
+
authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
|
|
3586
3622
|
}
|
|
3587
3623
|
}
|
|
3588
3624
|
const checks2 = [
|
|
3589
3625
|
pass("cli_version", VERSION),
|
|
3590
|
-
|
|
3626
|
+
authCheck2,
|
|
3591
3627
|
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)
|
|
3592
3628
|
];
|
|
3593
3629
|
emitDoctor(checks2, opts.json);
|
|
@@ -3597,9 +3633,9 @@ async function doctorCommand(opts = {}) {
|
|
|
3597
3633
|
const checks = [];
|
|
3598
3634
|
const auth = await readAuth();
|
|
3599
3635
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3636
|
+
const authCheck = await validateCurrentToken(token);
|
|
3637
|
+
const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
|
|
3638
|
+
checks.push(authCheck);
|
|
3603
3639
|
const cliPath = process.argv[1];
|
|
3604
3640
|
if (!cliPath) {
|
|
3605
3641
|
checks.push(fail("fresh_agent_cli_path", "process.argv[1] is empty"));
|
|
@@ -3617,7 +3653,7 @@ async function doctorCommand(opts = {}) {
|
|
|
3617
3653
|
...process.env,
|
|
3618
3654
|
HOME: tmpHome,
|
|
3619
3655
|
FLOOM_SKILLS_DIR: tmpSkills,
|
|
3620
|
-
...token ? { FLOOM_API_TOKEN: token } : {},
|
|
3656
|
+
...hasValidToken && token ? { FLOOM_API_TOKEN: token } : {},
|
|
3621
3657
|
...process.env.FLOOM_API_URL ? { FLOOM_API_URL: normalizeApiUrl(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
|
|
3622
3658
|
},
|
|
3623
3659
|
stderr: "pipe"
|
|
@@ -3627,38 +3663,29 @@ async function doctorCommand(opts = {}) {
|
|
|
3627
3663
|
await client.connect(transport);
|
|
3628
3664
|
const tools = await client.listTools();
|
|
3629
3665
|
const toolNames = tools.tools.map((tool) => tool.name).sort();
|
|
3630
|
-
const expected = ["get_skill", "install_skill", "list_workspaces", "search_skills"];
|
|
3666
|
+
const expected = ["get_skill", "install_skill", "list_libraries", "list_workspaces", "search_skills"];
|
|
3631
3667
|
const missing = expected.filter((name) => !toolNames.includes(name));
|
|
3632
3668
|
checks.push(missing.length === 0 ? pass("mcp_tools", toolNames.join(", ")) : fail("mcp_tools", `missing ${missing.join(", ")}`));
|
|
3633
|
-
if (
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
const searchOk = !searchError && skillCount !== null;
|
|
3647
|
-
checks.push(searchOk ? pass("mcp_search_skills", `${skillCount} skills`) : fail("mcp_search_skills", searchError ?? "unexpected response shape"));
|
|
3648
|
-
if (opts.ref) {
|
|
3649
|
-
if (!workspacesOk || !searchOk) {
|
|
3650
|
-
checks.push(warn("mcp_get_skill", "skipped because authenticated MCP API prechecks failed"));
|
|
3651
|
-
checks.push(warn("mcp_install_skill", "skipped because authenticated MCP API prechecks failed"));
|
|
3652
|
-
emitDoctor(checks, opts.json);
|
|
3653
|
-
if (checks.some((check) => !check.ok)) process.exit(1);
|
|
3654
|
-
return;
|
|
3655
|
-
}
|
|
3656
|
-
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
3657
|
-
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
3658
|
-
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
3659
|
-
const entries = await readdir5(tmpSkills);
|
|
3660
|
-
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
3669
|
+
if (hasValidToken) {
|
|
3670
|
+
const workspaces = await client.callTool({ name: "list_workspaces", arguments: {} });
|
|
3671
|
+
const workspaceError = toolError(workspaces);
|
|
3672
|
+
const workspaceCount = jsonArrayLength(workspaces, "libraries");
|
|
3673
|
+
const workspacesOk = !workspaceError && workspaceCount !== null;
|
|
3674
|
+
checks.push(workspacesOk ? pass("mcp_list_workspaces", `${workspaceCount} workspaces`) : fail("mcp_list_workspaces", workspaceError ?? "unexpected response shape"));
|
|
3675
|
+
const search = await client.callTool({ name: "search_skills", arguments: { query: opts.query ?? "pdf" } });
|
|
3676
|
+
const searchError = toolError(search);
|
|
3677
|
+
const skillCount = jsonArrayLength(search, "skills");
|
|
3678
|
+
const searchOk = !searchError && skillCount !== null;
|
|
3679
|
+
checks.push(searchOk ? pass("mcp_search_skills", `${skillCount} skills`) : fail("mcp_search_skills", searchError ?? "unexpected response shape"));
|
|
3680
|
+
} else {
|
|
3681
|
+
checks.push(warn("mcp_authenticated_tools", "skipped list_workspaces/search_skills because no valid token is available"));
|
|
3661
3682
|
}
|
|
3683
|
+
const ref = opts.ref ?? "floom-demo/brand-voice";
|
|
3684
|
+
const skill = await client.callTool({ name: "get_skill", arguments: { ref } });
|
|
3685
|
+
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
3686
|
+
const installed = await client.callTool({ name: "install_skill", arguments: { ref, target: opts.target ?? "codex" } });
|
|
3687
|
+
const entries = await readdir5(tmpSkills);
|
|
3688
|
+
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
3662
3689
|
} catch (e) {
|
|
3663
3690
|
checks.push(fail("fresh_agent_mcp", e.message));
|
|
3664
3691
|
} finally {
|