@floomhq/skills 0.2.5 → 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 CHANGED
@@ -2430,7 +2430,7 @@ function isLegacyApiUrl(apiUrl) {
2430
2430
  }
2431
2431
 
2432
2432
  // src/version.ts
2433
- var VERSION = "0.2.5";
2433
+ var VERSION = "0.2.6";
2434
2434
 
2435
2435
  // src/api-client.ts
2436
2436
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -3405,14 +3405,19 @@ async function fetchWithTimeout2(url, init = {}) {
3405
3405
  clearTimeout(timer);
3406
3406
  }
3407
3407
  }
3408
- async function resolveToken() {
3408
+ async function resolveOptionalToken() {
3409
3409
  const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
3410
3410
  if (fromEnv) return fromEnv;
3411
3411
  const auth = await readAuth();
3412
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;
3413
3418
  throw new Error("Authentication required. Run `floom login` or set FLOOM_API_TOKEN.");
3414
3419
  }
3415
- async function apiWithToken(token, path, query) {
3420
+ async function apiRequest(token, path, query) {
3416
3421
  const auth = await readAuth();
3417
3422
  let lastError = null;
3418
3423
  for (const base of getApiBaseUrls(auth?.apiUrl)) {
@@ -3423,7 +3428,10 @@ async function apiWithToken(token, path, query) {
3423
3428
  let res;
3424
3429
  try {
3425
3430
  res = await fetchWithTimeout2(url, {
3426
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
3431
+ headers: {
3432
+ ...token ? { Authorization: `Bearer ${token}` } : {},
3433
+ "Content-Type": "application/json"
3434
+ }
3427
3435
  });
3428
3436
  } catch (e) {
3429
3437
  lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
@@ -3445,8 +3453,8 @@ async function apiWithToken(token, path, query) {
3445
3453
  async function installViaApi(token, refText, target) {
3446
3454
  const parsed = parseSkillRef(refText);
3447
3455
  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);
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);
3450
3458
  const bundle = await rawGet(dl.download.url);
3451
3459
  if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
3452
3460
  const install = resolveInstallDir({ target });
@@ -3503,30 +3511,30 @@ async function parseSkillBundle(bundle) {
3503
3511
  async function mcpCommand() {
3504
3512
  const server = new McpServer({ name: "floom", version: VERSION });
3505
3513
  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();
3514
+ const token = await resolveRequiredToken();
3507
3515
  const workspaceSlug = workspace ?? library;
3508
- const result = await apiWithToken(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
3516
+ const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
3509
3517
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
3510
3518
  });
3511
3519
  server.tool("get_skill", { ref: z2.string().min(3) }, async ({ ref }) => {
3512
- const token = await resolveToken();
3520
+ const token = await resolveOptionalToken();
3513
3521
  const parsed = parseSkillRef(ref);
3514
3522
  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`);
3523
+ const meta = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
3524
+ const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`);
3517
3525
  const buf = await rawGet(dl.download.url);
3518
3526
  const parsedBundle = await parseSkillBundle(buf);
3519
3527
  return { content: [{ type: "text", text: JSON.stringify({ ref, meta, ...parsedBundle }) }] };
3520
3528
  });
3521
3529
  async function listWorkspaces() {
3522
- const token = await resolveToken();
3523
- const result = await apiWithToken(token, "/libraries");
3530
+ const token = await resolveRequiredToken();
3531
+ const result = await apiRequest(token, "/libraries");
3524
3532
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
3525
3533
  }
3526
3534
  server.tool("list_workspaces", {}, listWorkspaces);
3527
3535
  server.tool("list_libraries", {}, listWorkspaces);
3528
3536
  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();
3537
+ const token = await resolveOptionalToken();
3530
3538
  const installed = await installViaApi(token, ref, target);
3531
3539
  return { content: [{ type: "text", text: JSON.stringify(installed) }] };
3532
3540
  });
@@ -3568,26 +3576,35 @@ function warn(name, detail) {
3568
3576
  function fail(name, detail) {
3569
3577
  return { name, ok: false, detail };
3570
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
+ }
3571
3588
  async function doctorCommand(opts = {}) {
3572
3589
  if (!opts.freshAgent) {
3573
3590
  const auth2 = await readAuth();
3574
3591
  const rawAuth = await readRawAuth();
3575
- let authCheck;
3592
+ let authCheck2;
3576
3593
  const envToken = process.env.FLOOM_API_TOKEN?.trim();
3577
3594
  if (!auth2?.token && !envToken) {
3578
- authCheck = fail("auth", "not logged in");
3595
+ authCheck2 = fail("auth", "not logged in");
3579
3596
  } else {
3580
3597
  try {
3581
3598
  const me = await api("/me", { authRequired: true });
3582
3599
  const authSource = envToken ? "FLOOM_API_TOKEN" : "~/.floom/auth.json";
3583
- authCheck = pass("auth", `@${me.user.handle} via ${authSource}`);
3600
+ authCheck2 = pass("auth", `@${me.user.handle} via ${authSource}`);
3584
3601
  } catch (e) {
3585
- authCheck = fail("auth", `saved login rejected by API: ${e.message}`);
3602
+ authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
3586
3603
  }
3587
3604
  }
3588
3605
  const checks2 = [
3589
3606
  pass("cli_version", VERSION),
3590
- authCheck,
3607
+ authCheck2,
3591
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)
3592
3609
  ];
3593
3610
  emitDoctor(checks2, opts.json);
@@ -3597,9 +3614,9 @@ async function doctorCommand(opts = {}) {
3597
3614
  const checks = [];
3598
3615
  const auth = await readAuth();
3599
3616
  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
- }
3617
+ const authCheck = await validateCurrentToken(token);
3618
+ const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
3619
+ checks.push(authCheck);
3603
3620
  const cliPath = process.argv[1];
3604
3621
  if (!cliPath) {
3605
3622
  checks.push(fail("fresh_agent_cli_path", "process.argv[1] is empty"));
@@ -3617,7 +3634,7 @@ async function doctorCommand(opts = {}) {
3617
3634
  ...process.env,
3618
3635
  HOME: tmpHome,
3619
3636
  FLOOM_SKILLS_DIR: tmpSkills,
3620
- ...token ? { FLOOM_API_TOKEN: token } : {},
3637
+ ...hasValidToken && token ? { FLOOM_API_TOKEN: token } : {},
3621
3638
  ...process.env.FLOOM_API_URL ? { FLOOM_API_URL: normalizeApiUrl(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
3622
3639
  },
3623
3640
  stderr: "pipe"
@@ -3627,38 +3644,29 @@ async function doctorCommand(opts = {}) {
3627
3644
  await client.connect(transport);
3628
3645
  const tools = await client.listTools();
3629
3646
  const toolNames = tools.tools.map((tool) => tool.name).sort();
3630
- const expected = ["get_skill", "install_skill", "list_workspaces", "search_skills"];
3647
+ const expected = ["get_skill", "install_skill", "list_libraries", "list_workspaces", "search_skills"];
3631
3648
  const missing = expected.filter((name) => !toolNames.includes(name));
3632
3649
  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"));
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"));
3661
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"));
3662
3670
  } catch (e) {
3663
3671
  checks.push(fail("fresh_agent_mcp", e.message));
3664
3672
  } finally {