@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 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
- if (process.env.FLOOM_SKILLS_DIR) {
2071
+ const targetEnvDir = envDirForTarget(args.target ?? "generic");
2072
+ if (targetEnvDir) {
2055
2073
  return {
2056
2074
  target: args.target ?? "generic",
2057
- dir: process.env.FLOOM_SKILLS_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.5";
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 resolveToken() {
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 apiWithToken(token, path, query) {
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: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
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 apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}`);
3449
- const dl = await apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}/download`, parsed.version ? { version: parsed.version } : void 0);
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 resolveToken();
3533
+ const token = await resolveRequiredToken();
3507
3534
  const workspaceSlug = workspace ?? library;
3508
- const result = await apiWithToken(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
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 resolveToken();
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 apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}`);
3516
- const dl = await apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}/download`);
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 resolveToken();
3523
- const result = await apiWithToken(token, "/libraries");
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 resolveToken();
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 authCheck;
3611
+ let authCheck2;
3576
3612
  const envToken = process.env.FLOOM_API_TOKEN?.trim();
3577
3613
  if (!auth2?.token && !envToken) {
3578
- authCheck = fail("auth", "not logged in");
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
- authCheck = pass("auth", `@${me.user.handle} via ${authSource}`);
3619
+ authCheck2 = pass("auth", `@${me.user.handle} via ${authSource}`);
3584
3620
  } catch (e) {
3585
- authCheck = fail("auth", `saved login rejected by API: ${e.message}`);
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
- authCheck,
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
- if (!token) {
3601
- checks.push(warn("fresh_agent_auth", "missing token; API-backed tool calls skipped"));
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 (!token) {
3634
- checks.push(warn("mcp_api_calls", "skipped because no FLOOM_API_TOKEN or ~/.floom/auth.json was available"));
3635
- emitDoctor(checks, opts.json);
3636
- return;
3637
- }
3638
- const workspaces = await client.callTool({ name: "list_workspaces", arguments: {} });
3639
- const workspaceError = toolError(workspaces);
3640
- const workspaceCount = jsonArrayLength(workspaces, "libraries");
3641
- const workspacesOk = !workspaceError && workspaceCount !== null;
3642
- checks.push(workspacesOk ? pass("mcp_list_workspaces", `${workspaceCount} workspaces`) : fail("mcp_list_workspaces", workspaceError ?? "unexpected response shape"));
3643
- const search = await client.callTool({ name: "search_skills", arguments: { query: opts.query ?? "pdf" } });
3644
- const searchError = toolError(search);
3645
- const skillCount = jsonArrayLength(search, "skills");
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 {