@floomhq/floom-mcp-sync 1.0.28 → 1.0.30

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
@@ -2,15 +2,13 @@ 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
4
  import { apiUrlFromConfig, readConfig, refreshConfigIfNeeded } from "./lib/config.js";
5
- import { getJsonWithEtag, FloomApiError } from "./lib/api.js";
5
+ import { getJson, FloomApiError } from "./lib/api.js";
6
6
  import { sha256 } from "./lib/hash.js";
7
7
  import { assertValidSlug } from "./lib/slug.js";
8
8
  import { skillsDir, skillTargetPath } from "./lib/paths.js";
9
9
  import { ensureSyncManifestDir, manifestKey, markSynced, readSyncManifest, unmarkSynced, withSyncLock, writeSyncManifest } from "./lib/manifest.js";
10
- // Module-level cache: ETag from the last successful (non-304) response, plus
11
- // the last time we logged a heartbeat. Survives across setInterval ticks
12
- // inside a single MCP server process.
13
- let cachedEtag = null;
10
+ // Module-level cache: the last time we logged a heartbeat. Survives across
11
+ // setInterval ticks inside a single MCP server process.
14
12
  let lastHeartbeatAt = 0;
15
13
  let lastAuthWarningAt = 0;
16
14
  const HEARTBEAT_MS = 10 * 60 * 1000; // 10 minutes
@@ -118,7 +116,7 @@ const GENERATED_PACKAGE_DIRS = new Set([
118
116
  const GENERATED_PACKAGE_FILES = new Set([".DS_Store"]);
119
117
  const GENERATED_PACKAGE_FILE_SUFFIXES = [".icns", ".log", ".mov", ".mp3", ".mp4", ".pyc", ".pyo", ".wav", ".webm"];
120
118
  const FD_PATH_ROOT = "/proc/self/fd";
121
- const PACKAGE_FILE_LIMIT = 1000;
119
+ const PACKAGE_FILE_LIMIT = 100;
122
120
  const PACKAGE_TOTAL_BYTES_LIMIT = 8_000_000;
123
121
  const PACKAGE_FILE_BYTES_LIMIT = 500_000;
124
122
  const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
@@ -360,9 +358,9 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
360
358
  const manifest = await readSyncManifest();
361
359
  const root = skillsDir();
362
360
  const apiUrl = apiUrlFromConfig(cfg);
363
- let response;
361
+ let payload;
364
362
  try {
365
- response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag, signal);
363
+ payload = await loadSyncPayload(apiUrl, cfg.accessToken, signal);
366
364
  }
367
365
  catch (err) {
368
366
  if (!(err instanceof FloomApiError) || err.status !== 401)
@@ -373,26 +371,7 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
373
371
  return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
374
372
  }
375
373
  cfg = refreshed;
376
- response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag, signal);
377
- }
378
- if (response.status === 304) {
379
- if (await manifestHasMissingTrackedFile(manifest, root)) {
380
- response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, null, signal);
381
- }
382
- else {
383
- maybeHeartbeat(log);
384
- return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
385
- }
386
- }
387
- if (response.status === 304) {
388
- maybeHeartbeat(log);
389
- return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
390
- }
391
- if (response.etag)
392
- cachedEtag = response.etag;
393
- const payload = response.body;
394
- if (!payload) {
395
- return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
374
+ payload = await loadSyncPayload(apiUrl, cfg.accessToken, signal);
396
375
  }
397
376
  await mkdir(skillsDir(), { recursive: true, mode: 0o700 });
398
377
  if (!Array.isArray(payload.skills)) {
@@ -550,6 +529,31 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
550
529
  return { synced: total, unchanged, updated, conflicts };
551
530
  });
552
531
  }
532
+ async function loadSyncPayload(apiUrl, token, signal) {
533
+ const all = [];
534
+ let fullSync = false;
535
+ let cursor;
536
+ const seenCursors = new Set();
537
+ for (let page = 0; page < 1000; page += 1) {
538
+ const url = new URL(`${apiUrl}/api/v1/me/skills`);
539
+ url.searchParams.set("limit", "10");
540
+ url.searchParams.set("packages", "1");
541
+ if (cursor)
542
+ url.searchParams.set("cursor", cursor);
543
+ const payload = await getJson(url.toString(), token, signal);
544
+ if (!Array.isArray(payload.skills))
545
+ throw new Error("Invalid sync response");
546
+ all.push(...payload.skills);
547
+ fullSync = payload.full_sync === true;
548
+ if (!payload.next_cursor)
549
+ return { skills: all, full_sync: fullSync };
550
+ if (seenCursors.has(payload.next_cursor))
551
+ throw new Error("Invalid sync response");
552
+ seenCursors.add(payload.next_cursor);
553
+ cursor = payload.next_cursor;
554
+ }
555
+ throw new Error("Invalid sync response");
556
+ }
553
557
  async function emitSyncCompleted(apiUrl, token, props, signal) {
554
558
  try {
555
559
  const init = {
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.28";
8
+ const SERVER_VERSION = "1.0.30";
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.28",
3
+ "version": "1.0.30",
4
4
  "description": "Lightweight Floom MCP server for installing, publishing, and startup-syncing skills.",
5
5
  "license": "MIT",
6
6
  "type": "module",