@floomhq/skills 0.2.4 → 0.2.5

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.5";
2411
2434
 
2412
2435
  // src/api-client.ts
2413
2436
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -3520,6 +3543,22 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3520
3543
  function textOf(result) {
3521
3544
  return String(result?.content?.[0]?.text ?? "");
3522
3545
  }
3546
+ function toolError(result) {
3547
+ const text = textOf(result);
3548
+ if (result?.isError) return text || "tool returned an MCP error";
3549
+ if (/authentication required|forbidden|not found|invalid token/i.test(text)) return text;
3550
+ return null;
3551
+ }
3552
+ function jsonArrayLength(result, key) {
3553
+ if (toolError(result)) return null;
3554
+ try {
3555
+ const parsed = JSON.parse(textOf(result));
3556
+ const value = parsed?.[key];
3557
+ return Array.isArray(value) ? value.length : null;
3558
+ } catch {
3559
+ return null;
3560
+ }
3561
+ }
3523
3562
  function pass(name, detail) {
3524
3563
  return { name, ok: true, detail };
3525
3564
  }
@@ -3532,7 +3571,25 @@ function fail(name, detail) {
3532
3571
  async function doctorCommand(opts = {}) {
3533
3572
  if (!opts.freshAgent) {
3534
3573
  const auth2 = await readAuth();
3535
- const checks2 = [pass("cli_version", VERSION), auth2?.token ? pass("auth", `@${auth2.handle}`) : fail("auth", "not logged in")];
3574
+ const rawAuth = await readRawAuth();
3575
+ let authCheck;
3576
+ const envToken = process.env.FLOOM_API_TOKEN?.trim();
3577
+ if (!auth2?.token && !envToken) {
3578
+ authCheck = fail("auth", "not logged in");
3579
+ } else {
3580
+ try {
3581
+ const me = await api("/me", { authRequired: true });
3582
+ const authSource = envToken ? "FLOOM_API_TOKEN" : "~/.floom/auth.json";
3583
+ authCheck = pass("auth", `@${me.user.handle} via ${authSource}`);
3584
+ } catch (e) {
3585
+ authCheck = fail("auth", `saved login rejected by API: ${e.message}`);
3586
+ }
3587
+ }
3588
+ const checks2 = [
3589
+ pass("cli_version", VERSION),
3590
+ authCheck,
3591
+ 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
+ ];
3536
3593
  emitDoctor(checks2, opts.json);
3537
3594
  if (checks2.some((check) => !check.ok)) process.exit(1);
3538
3595
  return;
@@ -3561,7 +3618,7 @@ async function doctorCommand(opts = {}) {
3561
3618
  HOME: tmpHome,
3562
3619
  FLOOM_SKILLS_DIR: tmpSkills,
3563
3620
  ...token ? { FLOOM_API_TOKEN: token } : {},
3564
- ...auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
3621
+ ...process.env.FLOOM_API_URL ? { FLOOM_API_URL: normalizeApiUrl(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: auth.apiUrl } : {}
3565
3622
  },
3566
3623
  stderr: "pipe"
3567
3624
  });
@@ -3579,10 +3636,23 @@ async function doctorCommand(opts = {}) {
3579
3636
  return;
3580
3637
  }
3581
3638
  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"));
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"));
3583
3643
  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"));
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"));
3585
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
+ }
3586
3656
  const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
3587
3657
  checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
3588
3658
  const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });