@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 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.replace(/\/$/, "")];
2403
- const primary = (preferred ?? getApiUrl()).replace(/\/$/, "");
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.4";
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 resolveToken() {
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 apiWithToken(token, path, query) {
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: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }
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 apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}`);
3426
- 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);
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 resolveToken();
3514
+ const token = await resolveRequiredToken();
3484
3515
  const workspaceSlug = workspace ?? library;
3485
- const result = await apiWithToken(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
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 resolveToken();
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 apiWithToken(token, `/skills/${parsed.owner}/${parsed.slug}`);
3493
- 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`);
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 resolveToken();
3500
- const result = await apiWithToken(token, "/libraries");
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 resolveToken();
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 checks2 = [pass("cli_version", VERSION), auth2?.token ? pass("auth", `@${auth2.handle}`) : fail("auth", "not logged in")];
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
- if (!token) {
3544
- checks.push(warn("fresh_agent_auth", "missing token; API-backed tool calls skipped"));
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 (!token) {
3577
- checks.push(warn("mcp_api_calls", "skipped because no FLOOM_API_TOKEN or ~/.floom/auth.json was available"));
3578
- emitDoctor(checks, opts.json);
3579
- return;
3580
- }
3581
- const workspaces = await client.callTool({ name: "list_workspaces", arguments: {} });
3582
- checks.push(textOf(workspaces).length > 0 ? pass("mcp_list_workspaces", `${textOf(workspaces).length} chars`) : fail("mcp_list_workspaces", "empty response"));
3583
- const search = await client.callTool({ name: "search_skills", arguments: { query: opts.query ?? "pdf" } });
3584
- checks.push(textOf(search).length > 0 ? pass("mcp_search_skills", `${textOf(search).length} chars`) : fail("mcp_search_skills", "empty response"));
3585
- if (opts.ref) {
3586
- const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
3587
- checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
3588
- const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
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 {