@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 +32 -28
- package/dist/server.js +1 -1
- package/package.json +1 -1
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 {
|
|
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:
|
|
11
|
-
//
|
|
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 =
|
|
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
|
|
361
|
+
let payload;
|
|
364
362
|
try {
|
|
365
|
-
|
|
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
|
-
|
|
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.
|
|
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"]);
|