@floomhq/skills 0.2.3 → 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
@@ -2360,12 +2360,17 @@ import { join as join3 } from "node:path";
2360
2360
  import { mkdir as mkdir2, readFile as readFile3, writeFile, chmod } from "node:fs/promises";
2361
2361
  var CONFIG_DIR = join3(homedir2(), ".floom");
2362
2362
  var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
2363
- var DEFAULT_APP_URL = "https://floom.dev";
2364
- var DEFAULT_API_URL = "https://floom-v0.vercel.app/api/v1";
2363
+ var DEFAULT_APP_URL = "https://skills.floom.dev";
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.3";
2433
+ var VERSION = "0.2.5";
2411
2434
 
2412
2435
  // src/api-client.ts
2413
2436
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2431,7 +2454,8 @@ async function fetchWithTimeout(url, init = {}) {
2431
2454
  }
2432
2455
  async function api(path, opts = {}) {
2433
2456
  const auth = await readAuth();
2434
- if (opts.authRequired && !auth) {
2457
+ const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
2458
+ if (opts.authRequired && !token) {
2435
2459
  throw new FloomError("AUTH_REQUIRED", "Not logged in. Run: floom login");
2436
2460
  }
2437
2461
  let lastError = null;
@@ -2448,7 +2472,7 @@ async function api(path, opts = {}) {
2448
2472
  "User-Agent": `floom-cli/${VERSION}`,
2449
2473
  "x-floom-cli-version": VERSION
2450
2474
  };
2451
- if (auth) headers.Authorization = `Bearer ${auth.token}`;
2475
+ if (token) headers.Authorization = `Bearer ${token}`;
2452
2476
  let res;
2453
2477
  try {
2454
2478
  res = await fetchWithTimeout(url.toString(), {
@@ -2594,22 +2618,25 @@ async function logoutCommand() {
2594
2618
  // src/commands/whoami.ts
2595
2619
  async function whoamiCommand() {
2596
2620
  const auth = await readAuth();
2597
- if (!auth) {
2621
+ const envToken = process.env.FLOOM_API_TOKEN?.trim();
2622
+ if (!auth && !envToken) {
2598
2623
  log.info("Not logged in. Run: floom login");
2599
2624
  return;
2600
2625
  }
2626
+ let me;
2601
2627
  try {
2602
- await api("/me", { authRequired: true });
2628
+ me = await api("/me", { authRequired: true });
2603
2629
  } catch (e) {
2604
- log.err(`Stored login is not accepted by the Floom API: ${e.message}`);
2630
+ log.err(`Login is not accepted by the Floom API: ${e.message}`);
2605
2631
  log.info("Run: floom login");
2606
2632
  process.exitCode = 1;
2607
2633
  return;
2608
2634
  }
2609
2635
  log.heading("Logged in as:");
2610
- log.kv("handle", `@${auth.handle}`);
2611
- log.kv("email", auth.email);
2612
- log.kv("api url", auth.apiUrl);
2636
+ log.kv("handle", `@${me.user.handle}`);
2637
+ log.kv("email", me.user.email);
2638
+ log.kv("api url", process.env.FLOOM_API_URL ?? auth?.apiUrl ?? "default");
2639
+ if (envToken) log.kv("auth", "FLOOM_API_TOKEN");
2613
2640
  }
2614
2641
 
2615
2642
  // src/commands/init.ts
@@ -3516,6 +3543,22 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3516
3543
  function textOf(result) {
3517
3544
  return String(result?.content?.[0]?.text ?? "");
3518
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
+ }
3519
3562
  function pass(name, detail) {
3520
3563
  return { name, ok: true, detail };
3521
3564
  }
@@ -3528,7 +3571,25 @@ function fail(name, detail) {
3528
3571
  async function doctorCommand(opts = {}) {
3529
3572
  if (!opts.freshAgent) {
3530
3573
  const auth2 = await readAuth();
3531
- 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
+ ];
3532
3593
  emitDoctor(checks2, opts.json);
3533
3594
  if (checks2.some((check) => !check.ok)) process.exit(1);
3534
3595
  return;
@@ -3557,7 +3618,7 @@ async function doctorCommand(opts = {}) {
3557
3618
  HOME: tmpHome,
3558
3619
  FLOOM_SKILLS_DIR: tmpSkills,
3559
3620
  ...token ? { FLOOM_API_TOKEN: token } : {},
3560
- ...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 } : {}
3561
3622
  },
3562
3623
  stderr: "pipe"
3563
3624
  });
@@ -3575,10 +3636,23 @@ async function doctorCommand(opts = {}) {
3575
3636
  return;
3576
3637
  }
3577
3638
  const workspaces = await client.callTool({ name: "list_workspaces", arguments: {} });
3578
- 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"));
3579
3643
  const search = await client.callTool({ name: "search_skills", arguments: { query: opts.query ?? "pdf" } });
3580
- 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"));
3581
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
+ }
3582
3656
  const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
3583
3657
  checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
3584
3658
  const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });