@floomhq/floom-mcp-sync 1.0.27 → 1.0.28

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/auto-sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { constants } from "node:fs";
2
2
  import { lstat, mkdir, open } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import { apiUrlFromConfig, readConfig } from "./lib/config.js";
4
+ import { apiUrlFromConfig, readConfig, refreshConfigIfNeeded } from "./lib/config.js";
5
5
  import { getJsonWithEtag, FloomApiError } from "./lib/api.js";
6
6
  import { sha256 } from "./lib/hash.js";
7
7
  import { assertValidSlug } from "./lib/slug.js";
@@ -349,11 +349,12 @@ async function manifestHasMissingTrackedFile(manifest, root) {
349
349
  return false;
350
350
  }
351
351
  export async function autoSync(log = (message) => process.stderr.write(`${message}\n`), signal) {
352
- const cfg = await readConfig();
353
- if (!cfg) {
352
+ const initialCfg = await readConfig();
353
+ if (!initialCfg) {
354
354
  maybeAuthWarning(log, "[floom] not signed in; skipping sync (run `npx -y @floomhq/floom login`)");
355
355
  return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
356
356
  }
357
+ let cfg = initialCfg;
357
358
  await ensureSyncManifestDir();
358
359
  return await withSyncLock(async () => {
359
360
  const manifest = await readSyncManifest();
@@ -364,11 +365,15 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
364
365
  response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag, signal);
365
366
  }
366
367
  catch (err) {
367
- if (err instanceof FloomApiError && err.status === 401) {
368
+ if (!(err instanceof FloomApiError) || err.status !== 401)
369
+ throw err;
370
+ const refreshed = await refreshConfigIfNeeded(cfg, { force: true });
371
+ if (refreshed.accessToken === cfg.accessToken) {
368
372
  maybeAuthWarning(log, "[floom] sign-in expired; skipping sync (run `npx -y @floomhq/floom login` again)");
369
373
  return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
370
374
  }
371
- throw err;
375
+ cfg = refreshed;
376
+ response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag, signal);
372
377
  }
373
378
  if (response.status === 304) {
374
379
  if (await manifestHasMissingTrackedFile(manifest, root)) {
@@ -1,4 +1,5 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
2
3
  import { configPath } from "./paths.js";
3
4
  export const DEFAULT_API_URL = "https://floom.dev";
4
5
  export function apiUrlFromConfig(cfg) {
@@ -10,13 +11,14 @@ export async function readConfig() {
10
11
  const parsed = JSON.parse(raw);
11
12
  if (typeof parsed.accessToken !== "string" || parsed.accessToken.length === 0)
12
13
  return null;
13
- return {
14
+ const cfg = {
14
15
  accessToken: parsed.accessToken,
15
16
  ...(typeof parsed.apiUrl === "string" ? { apiUrl: parsed.apiUrl } : {}),
16
17
  ...(typeof parsed.refreshToken === "string" ? { refreshToken: parsed.refreshToken } : {}),
17
18
  ...(typeof parsed.expiresAt === "number" ? { expiresAt: parsed.expiresAt } : {}),
18
19
  ...(typeof parsed.email === "string" || parsed.email === null ? { email: parsed.email } : {}),
19
20
  };
21
+ return await refreshConfigIfNeeded(cfg);
20
22
  }
21
23
  catch (err) {
22
24
  if (err.code === "ENOENT")
@@ -24,3 +26,46 @@ export async function readConfig() {
24
26
  throw err;
25
27
  }
26
28
  }
29
+ export async function refreshConfigIfNeeded(cfg, opts = {}) {
30
+ if (!cfg.refreshToken)
31
+ return cfg;
32
+ const now = Math.floor(Date.now() / 1000);
33
+ if (!opts.force && (typeof cfg.expiresAt !== "number" || cfg.expiresAt > now + 120))
34
+ return cfg;
35
+ try {
36
+ const apiUrl = apiUrlFromConfig(cfg);
37
+ const res = await fetch(`${apiUrl}/api/auth/refresh`, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify({ refresh_token: cfg.refreshToken }),
41
+ });
42
+ if (!res.ok)
43
+ throw new Error(`refresh failed with ${res.status}`);
44
+ const data = (await res.json());
45
+ if (!data.access_token || !data.refresh_token)
46
+ throw new Error("refresh response missing tokens");
47
+ const expiresIn = Number(data.expires_in ?? "3600");
48
+ const refreshed = {
49
+ ...cfg,
50
+ accessToken: data.access_token,
51
+ refreshToken: data.refresh_token,
52
+ expiresAt: Math.floor(Date.now() / 1000) + (Number.isFinite(expiresIn) ? expiresIn : 3600),
53
+ ...(typeof data.email === "string" || data.email === null
54
+ ? { email: data.email }
55
+ : typeof cfg.email === "string" || cfg.email === null
56
+ ? { email: cfg.email }
57
+ : {}),
58
+ };
59
+ await writeConfig(refreshed);
60
+ return refreshed;
61
+ }
62
+ catch {
63
+ return cfg;
64
+ }
65
+ }
66
+ async function writeConfig(cfg) {
67
+ const target = configPath();
68
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
69
+ await writeFile(target, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
70
+ await chmod(target, 0o600);
71
+ }
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { autoSync } from "./auto-sync.js";
5
5
  import { getSkill } from "./tools/get.js";
6
6
  import { searchSkills } from "./tools/search.js";
7
7
  import { syncStatus } from "./tools/status.js";
8
- const SERVER_VERSION = "1.0.27";
8
+ const SERVER_VERSION = "1.0.28";
9
9
  const DEFAULT_INTERVAL_MS = 60_000;
10
10
  const MIN_INTERVAL_MS = 10_000;
11
11
  const SEARCH_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom-mcp-sync",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "Lightweight Floom MCP server for installing, publishing, and startup-syncing skills.",
5
5
  "license": "MIT",
6
6
  "type": "module",