@floomhq/floom-mcp-sync 1.0.36 → 1.0.38

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
@@ -346,7 +346,7 @@ async function manifestHasMissingTrackedFile(manifest, root) {
346
346
  }
347
347
  return false;
348
348
  }
349
- export async function autoSync(log = (message) => process.stderr.write(`${message}\n`), signal) {
349
+ export async function autoSync(log = (message) => process.stderr.write(`${message}\n`), signal, opts = {}) {
350
350
  const initialCfg = await readConfig();
351
351
  if (!initialCfg) {
352
352
  maybeAuthWarning(log, "[floom] not signed in; skipping sync (run `npx -y @floomhq/floom login`)");
@@ -354,7 +354,7 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
354
354
  }
355
355
  let cfg = initialCfg;
356
356
  await ensureSyncManifestDir();
357
- return await withSyncLock(async () => {
357
+ const result = await withSyncLock(async () => {
358
358
  const manifest = await readSyncManifest();
359
359
  const root = skillsDir();
360
360
  const apiUrl = apiUrlFromConfig(cfg);
@@ -527,7 +527,11 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
527
527
  maybeHeartbeat(log, () => `[floom] heartbeat: ${total} skills tracked, all up-to-date`);
528
528
  }
529
529
  return { synced: total, unchanged, updated, conflicts };
530
- });
530
+ }, { wait: !opts.skipIfLocked });
531
+ if (result)
532
+ return result;
533
+ log("[floom] sync already running; skipping background poll");
534
+ return { synced: 0, unchanged: 0, updated: 0, conflicts: 0 };
531
535
  }
532
536
  async function loadSyncPayload(apiUrl, token, signal) {
533
537
  const all = [];
@@ -225,10 +225,11 @@ export async function ensureSyncManifestDir() {
225
225
  throw err;
226
226
  }
227
227
  }
228
- export async function withSyncLock(fn) {
228
+ export async function withSyncLock(fn, opts = {}) {
229
229
  await ensureSyncManifestDir();
230
230
  const lockPath = join(dirname(syncManifestPath()), "sync.lock");
231
231
  const startedAt = Date.now();
232
+ const wait = opts.wait !== false;
232
233
  for (;;) {
233
234
  try {
234
235
  await mkdir(lockPath, { mode: 0o700 });
@@ -249,6 +250,8 @@ export async function withSyncLock(fn) {
249
250
  continue;
250
251
  throw statErr;
251
252
  }
253
+ if (!wait)
254
+ return null;
252
255
  if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
253
256
  throw new Error("Timed out waiting for Floom sync lock.");
254
257
  }
@@ -262,6 +265,9 @@ export async function withSyncLock(fn) {
262
265
  await rm(lockPath, { recursive: true, force: true }).catch(() => { });
263
266
  }
264
267
  }
268
+ export async function clearSyncLock() {
269
+ await rm(join(dirname(syncManifestPath()), "sync.lock"), { recursive: true, force: true }).catch(() => { });
270
+ }
265
271
  export function manifestKey(root, target) {
266
272
  const relativeTarget = relative(resolve(root), resolve(target));
267
273
  if (relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`)) {
package/dist/server.js CHANGED
@@ -2,21 +2,23 @@
2
2
  import { createInterface } from "node:readline";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { autoSync } from "./auto-sync.js";
5
+ import { clearSyncLock } from "./lib/manifest.js";
5
6
  import { getSkill } from "./tools/get.js";
6
7
  import { searchSkills } from "./tools/search.js";
7
8
  import { syncStatus } from "./tools/status.js";
8
- const SERVER_VERSION = "1.0.36";
9
+ const SERVER_VERSION = "1.0.38";
9
10
  const DEFAULT_INTERVAL_MS = 60_000;
10
11
  const MIN_INTERVAL_MS = 10_000;
11
12
  const SEARCH_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
12
13
  let syncInFlight = null;
13
14
  let syncAbortController = null;
14
- function runAutoSync() {
15
+ let shuttingDown = false;
16
+ function runAutoSync(opts = {}) {
15
17
  if (syncInFlight)
16
18
  return syncInFlight;
17
19
  const controller = new AbortController();
18
20
  syncAbortController = controller;
19
- syncInFlight = autoSync(undefined, controller.signal).finally(() => {
21
+ syncInFlight = autoSync(undefined, controller.signal, opts).finally(() => {
20
22
  if (syncAbortController === controller)
21
23
  syncAbortController = null;
22
24
  syncInFlight = null;
@@ -27,6 +29,24 @@ function abortAutoSync() {
27
29
  syncAbortController?.abort();
28
30
  syncAbortController = null;
29
31
  }
32
+ async function stopAutoSync() {
33
+ const inFlight = syncInFlight;
34
+ abortAutoSync();
35
+ if (!inFlight)
36
+ return;
37
+ try {
38
+ await Promise.race([
39
+ inFlight,
40
+ new Promise((resolve) => setTimeout(resolve, 750)),
41
+ ]);
42
+ }
43
+ catch {
44
+ // Shutdown is best-effort; autoSync already logged actionable failures.
45
+ }
46
+ finally {
47
+ await clearSyncLock();
48
+ }
49
+ }
30
50
  function usage() {
31
51
  return `
32
52
  floom-mcp-sync v${SERVER_VERSION}
@@ -94,7 +114,7 @@ function startPolling(intervalMs, state) {
94
114
  return;
95
115
  }
96
116
  state.inFlight = true;
97
- runAutoSync().catch((err) => {
117
+ runAutoSync({ skipIfLocked: true }).catch((err) => {
98
118
  process.stderr.write(`[floom] poll failed: ${err instanceof Error ? err.message : String(err)}\n`);
99
119
  }).finally(() => {
100
120
  state.inFlight = false;
@@ -273,17 +293,19 @@ async function main() {
273
293
  const intervalMs = resolvePollIntervalMs();
274
294
  process.stderr.write(`[floom] starting sync poller (interval ${intervalMs}ms)\n`);
275
295
  const syncState = { inFlight: true };
276
- void runAutoSync().catch((err) => {
296
+ void runAutoSync({ skipIfLocked: true }).catch((err) => {
277
297
  process.stderr.write(`[floom] initial sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
278
298
  }).finally(() => {
279
299
  syncState.inFlight = false;
280
300
  });
281
301
  const pollHandle = startPolling(intervalMs, syncState);
282
302
  const shutdown = (signal) => {
303
+ if (shuttingDown)
304
+ return;
305
+ shuttingDown = true;
283
306
  clearInterval(pollHandle);
284
- abortAutoSync();
285
307
  process.stderr.write(`[floom] received ${signal}, stopping sync poller\n`);
286
- process.exit(0);
308
+ void stopAutoSync().finally(() => process.exit(0));
287
309
  };
288
310
  process.once("SIGINT", () => shutdown("SIGINT"));
289
311
  process.once("SIGTERM", () => shutdown("SIGTERM"));
@@ -307,7 +329,7 @@ async function main() {
307
329
  }
308
330
  finally {
309
331
  clearInterval(pollHandle);
310
- abortAutoSync();
332
+ await stopAutoSync();
311
333
  process.stderr.write("[floom] stdin closed, stopping sync poller\n");
312
334
  }
313
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom-mcp-sync",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "Lightweight Floom MCP server for installing, publishing, and startup-syncing skills.",
5
5
  "license": "MIT",
6
6
  "type": "module",