@floomhq/floom 2.0.5 → 2.0.6

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
@@ -111,13 +111,13 @@ var require_bcrypt = __commonJS({
111
111
  throw Error("Illegal callback: " + typeof callback);
112
112
  _async(callback);
113
113
  } else
114
- return new Promise(function(resolve3, reject) {
114
+ return new Promise(function(resolve4, reject) {
115
115
  _async(function(err, res) {
116
116
  if (err) {
117
117
  reject(err);
118
118
  return;
119
119
  }
120
- resolve3(res);
120
+ resolve4(res);
121
121
  });
122
122
  });
123
123
  };
@@ -146,13 +146,13 @@ var require_bcrypt = __commonJS({
146
146
  throw Error("Illegal callback: " + typeof callback);
147
147
  _async(callback);
148
148
  } else
149
- return new Promise(function(resolve3, reject) {
149
+ return new Promise(function(resolve4, reject) {
150
150
  _async(function(err, res) {
151
151
  if (err) {
152
152
  reject(err);
153
153
  return;
154
154
  }
155
- resolve3(res);
155
+ resolve4(res);
156
156
  });
157
157
  });
158
158
  };
@@ -197,13 +197,13 @@ var require_bcrypt = __commonJS({
197
197
  throw Error("Illegal callback: " + typeof callback);
198
198
  _async(callback);
199
199
  } else
200
- return new Promise(function(resolve3, reject) {
200
+ return new Promise(function(resolve4, reject) {
201
201
  _async(function(err, res) {
202
202
  if (err) {
203
203
  reject(err);
204
204
  return;
205
205
  }
206
- resolve3(res);
206
+ resolve4(res);
207
207
  });
208
208
  });
209
209
  };
@@ -431,21 +431,21 @@ var require_bcrypt = __commonJS({
431
431
  var utfx2 = {};
432
432
  utfx2.MAX_CODEPOINT = 1114111;
433
433
  utfx2.encodeUTF8 = function(src, dst) {
434
- var cp2 = null;
434
+ var cp3 = null;
435
435
  if (typeof src === "number")
436
- cp2 = src, src = function() {
436
+ cp3 = src, src = function() {
437
437
  return null;
438
438
  };
439
- while (cp2 !== null || (cp2 = src()) !== null) {
440
- if (cp2 < 128)
441
- dst(cp2 & 127);
442
- else if (cp2 < 2048)
443
- dst(cp2 >> 6 & 31 | 192), dst(cp2 & 63 | 128);
444
- else if (cp2 < 65536)
445
- dst(cp2 >> 12 & 15 | 224), dst(cp2 >> 6 & 63 | 128), dst(cp2 & 63 | 128);
439
+ while (cp3 !== null || (cp3 = src()) !== null) {
440
+ if (cp3 < 128)
441
+ dst(cp3 & 127);
442
+ else if (cp3 < 2048)
443
+ dst(cp3 >> 6 & 31 | 192), dst(cp3 & 63 | 128);
444
+ else if (cp3 < 65536)
445
+ dst(cp3 >> 12 & 15 | 224), dst(cp3 >> 6 & 63 | 128), dst(cp3 & 63 | 128);
446
446
  else
447
- dst(cp2 >> 18 & 7 | 240), dst(cp2 >> 12 & 63 | 128), dst(cp2 >> 6 & 63 | 128), dst(cp2 & 63 | 128);
448
- cp2 = null;
447
+ dst(cp3 >> 18 & 7 | 240), dst(cp3 >> 12 & 63 | 128), dst(cp3 >> 6 & 63 | 128), dst(cp3 & 63 | 128);
448
+ cp3 = null;
449
449
  }
450
450
  };
451
451
  utfx2.decodeUTF8 = function(src, dst) {
@@ -487,43 +487,43 @@ var require_bcrypt = __commonJS({
487
487
  if (c2 !== null) dst(c2);
488
488
  };
489
489
  utfx2.UTF8toUTF16 = function(src, dst) {
490
- var cp2 = null;
490
+ var cp3 = null;
491
491
  if (typeof src === "number")
492
- cp2 = src, src = function() {
492
+ cp3 = src, src = function() {
493
493
  return null;
494
494
  };
495
- while (cp2 !== null || (cp2 = src()) !== null) {
496
- if (cp2 <= 65535)
497
- dst(cp2);
495
+ while (cp3 !== null || (cp3 = src()) !== null) {
496
+ if (cp3 <= 65535)
497
+ dst(cp3);
498
498
  else
499
- cp2 -= 65536, dst((cp2 >> 10) + 55296), dst(cp2 % 1024 + 56320);
500
- cp2 = null;
499
+ cp3 -= 65536, dst((cp3 >> 10) + 55296), dst(cp3 % 1024 + 56320);
500
+ cp3 = null;
501
501
  }
502
502
  };
503
503
  utfx2.encodeUTF16toUTF8 = function(src, dst) {
504
- utfx2.UTF16toUTF8(src, function(cp2) {
505
- utfx2.encodeUTF8(cp2, dst);
504
+ utfx2.UTF16toUTF8(src, function(cp3) {
505
+ utfx2.encodeUTF8(cp3, dst);
506
506
  });
507
507
  };
508
508
  utfx2.decodeUTF8toUTF16 = function(src, dst) {
509
- utfx2.decodeUTF8(src, function(cp2) {
510
- utfx2.UTF8toUTF16(cp2, dst);
509
+ utfx2.decodeUTF8(src, function(cp3) {
510
+ utfx2.UTF8toUTF16(cp3, dst);
511
511
  });
512
512
  };
513
- utfx2.calculateCodePoint = function(cp2) {
514
- return cp2 < 128 ? 1 : cp2 < 2048 ? 2 : cp2 < 65536 ? 3 : 4;
513
+ utfx2.calculateCodePoint = function(cp3) {
514
+ return cp3 < 128 ? 1 : cp3 < 2048 ? 2 : cp3 < 65536 ? 3 : 4;
515
515
  };
516
516
  utfx2.calculateUTF8 = function(src) {
517
- var cp2, l = 0;
518
- while ((cp2 = src()) !== null)
519
- l += utfx2.calculateCodePoint(cp2);
517
+ var cp3, l = 0;
518
+ while ((cp3 = src()) !== null)
519
+ l += utfx2.calculateCodePoint(cp3);
520
520
  return l;
521
521
  };
522
522
  utfx2.calculateUTF16asUTF8 = function(src) {
523
523
  var n = 0, l = 0;
524
- utfx2.UTF16toUTF8(src, function(cp2) {
524
+ utfx2.UTF16toUTF8(src, function(cp3) {
525
525
  ++n;
526
- l += utfx2.calculateCodePoint(cp2);
526
+ l += utfx2.calculateCodePoint(cp3);
527
527
  });
528
528
  return [n, l];
529
529
  };
@@ -2233,6 +2233,10 @@ var InviteCreateResponseSchema = z2.object({
2233
2233
  invite: InviteSchema,
2234
2234
  accept_url: UrlSchema
2235
2235
  }).strict();
2236
+ var InviteLinkRotateResponseSchema = z2.object({
2237
+ invite: InviteSchema,
2238
+ accept_url: UrlSchema
2239
+ }).strict();
2236
2240
  var InvitesListResponseSchema = z2.object({
2237
2241
  invites: z2.array(InviteSchema),
2238
2242
  total: z2.number().int().nonnegative()
@@ -2545,6 +2549,21 @@ var log = {
2545
2549
  },
2546
2550
  blank: () => {
2547
2551
  process.stdout.write("\n");
2552
+ },
2553
+ // A runnable command rendered cyan+bold — the one thing the eye should catch.
2554
+ cmd: (s) => chalk.cyan.bold(s),
2555
+ // Codex-style next-step block: description line, then the command indented in cyan+bold.
2556
+ next: (description, command) => {
2557
+ process.stdout.write(`
2558
+ ${description}
2559
+ ${chalk.cyan.bold(command)}
2560
+ `);
2561
+ },
2562
+ // Aligned-column row for list/status tables.
2563
+ // col widths: [slug col, state col, rest]. Pass strings pre-padded or let row pad them.
2564
+ row: (cols, widths) => {
2565
+ const padded = cols.map((col, i) => widths[i] !== void 0 ? col.padEnd(widths[i]) : col);
2566
+ process.stdout.write(" " + padded.join(" ") + "\n");
2548
2567
  }
2549
2568
  };
2550
2569
 
@@ -2665,26 +2684,26 @@ import { spawn } from "node:child_process";
2665
2684
  var CONFIG_DIR2 = join4(homedir3(), ".floom");
2666
2685
  var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
2667
2686
  async function tryCommand(cmd, args) {
2668
- return new Promise((resolve3) => {
2687
+ return new Promise((resolve4) => {
2669
2688
  let child;
2670
2689
  try {
2671
2690
  child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
2672
2691
  } catch {
2673
- resolve3(null);
2692
+ resolve4(null);
2674
2693
  return;
2675
2694
  }
2676
2695
  let out = "";
2677
2696
  child.stdout?.on("data", (d) => {
2678
2697
  out += d.toString();
2679
2698
  });
2680
- child.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
2681
- child.on("error", () => resolve3(null));
2699
+ child.on("close", (code) => resolve4(code === 0 ? out.trim() : null));
2700
+ child.on("error", () => resolve4(null));
2682
2701
  const timer = setTimeout(() => {
2683
2702
  try {
2684
2703
  child.kill();
2685
2704
  } catch {
2686
2705
  }
2687
- resolve3(null);
2706
+ resolve4(null);
2688
2707
  }, 800);
2689
2708
  child.on("close", () => clearTimeout(timer));
2690
2709
  });
@@ -2738,7 +2757,7 @@ async function getMachineIdentity() {
2738
2757
  }
2739
2758
 
2740
2759
  // src/version.ts
2741
- var VERSION = "2.0.4";
2760
+ var VERSION = "2.0.6";
2742
2761
 
2743
2762
  // src/api-client.ts
2744
2763
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2908,8 +2927,33 @@ async function api(path, opts = {}) {
2908
2927
  }
2909
2928
 
2910
2929
  // src/commands/login.ts
2930
+ import { spawn as spawn2 } from "node:child_process";
2931
+ import chalk2 from "chalk";
2932
+ function tryOpenBrowser(url) {
2933
+ if (process.env.FLOOM_NO_OPEN === "1") return;
2934
+ const platform2 = process.platform;
2935
+ let cmd;
2936
+ let args;
2937
+ if (platform2 === "darwin") {
2938
+ cmd = "open";
2939
+ args = [url];
2940
+ } else if (platform2 === "win32") {
2941
+ cmd = "cmd";
2942
+ args = ["/c", "start", "", url];
2943
+ } else {
2944
+ cmd = "xdg-open";
2945
+ args = [url];
2946
+ }
2947
+ try {
2948
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2949
+ child.on("error", () => {
2950
+ });
2951
+ child.unref();
2952
+ } catch {
2953
+ }
2954
+ }
2911
2955
  function sleep(ms) {
2912
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2956
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2913
2957
  }
2914
2958
  async function loginCommand() {
2915
2959
  const session = await api("/cli/device/start", {
@@ -2919,10 +2963,11 @@ async function loginCommand() {
2919
2963
  client: `floom-cli/${VERSION}`
2920
2964
  }
2921
2965
  });
2922
- log.heading("CLI login");
2923
- log.info(`Open: ${session.verification_uri}`);
2924
- log.info(`Code: ${session.user_code}`);
2966
+ log.heading("Sign in to Floom");
2967
+ log.info(`Open: ${session.verification_uri}`);
2968
+ log.info(`Code: ${chalk2.bold(session.user_code)}`);
2925
2969
  log.info("Waiting for browser approval. Press Ctrl+C to cancel.");
2970
+ tryOpenBrowser(session.verification_uri);
2926
2971
  const deadline = new Date(session.expires_at).getTime();
2927
2972
  const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
2928
2973
  while (Date.now() < deadline) {
@@ -2940,7 +2985,9 @@ async function loginCommand() {
2940
2985
  email: token.user?.email ?? "unknown@example.com",
2941
2986
  apiUrl: getApiUrl()
2942
2987
  });
2943
- log.ok(`Logged in to ${token.workspace?.name ?? "Floom"}.`);
2988
+ const emailDisplay = token.user?.email ? ` as ${token.user.email}` : "";
2989
+ log.ok(`Logged in to ${token.workspace?.name ?? "Floom"}${emailDisplay}.`);
2990
+ log.next("Next: pull your workspace skills into your AI agents.", "floom pull");
2944
2991
  return;
2945
2992
  }
2946
2993
  } catch (error) {
@@ -2968,9 +3015,10 @@ import { z as z3 } from "zod";
2968
3015
  // src/commands/sync.ts
2969
3016
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
2970
3017
  import { cp, lstat as lstat2, mkdir as mkdir4, readdir as readdir2, readFile as readFile5, rename, rm, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
2971
- import { dirname, join as join5, sep as sep3 } from "node:path";
3018
+ import { dirname, join as join5, resolve as resolve2, sep as sep3 } from "node:path";
2972
3019
  import { createInterface } from "node:readline/promises";
2973
3020
  import { ZodError } from "zod";
3021
+ import chalk3 from "chalk";
2974
3022
 
2975
3023
  // src/lib/signals.ts
2976
3024
  import { rmSync } from "node:fs";
@@ -3094,6 +3142,83 @@ async function fileExists(path) {
3094
3142
  return false;
3095
3143
  }
3096
3144
  }
3145
+ async function dirExists(path) {
3146
+ try {
3147
+ return (await stat3(path)).isDirectory();
3148
+ } catch {
3149
+ return false;
3150
+ }
3151
+ }
3152
+ async function resolvePullDirs(target, _cwd = process.cwd(), homeDir) {
3153
+ const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
3154
+ return uniqueResolvedDirs([primary]);
3155
+ }
3156
+ async function hasStatusVisibleProjectLocalDir(dir) {
3157
+ if (await fileExists(manifestPath(dir))) return true;
3158
+ return await dirExists(dir) && await hasAnySkillSubdir(dir);
3159
+ }
3160
+ async function resolveProjectLocalStatusDirs(target, cwd = process.cwd()) {
3161
+ const projectLocal = presetDir(target, { cwd });
3162
+ if (!await hasStatusVisibleProjectLocalDir(projectLocal)) return [];
3163
+ return uniqueResolvedDirs([projectLocal]);
3164
+ }
3165
+ async function resolveStatusDirs(target, cwd = process.cwd(), homeDir) {
3166
+ const primary = resolveInstallDir({ target, global: true, homeDir }).dir;
3167
+ const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
3168
+ return uniqueResolvedDirs([primary, ...projectLocalDirs]);
3169
+ }
3170
+ async function resolveAutoStatusTargets(cwd = process.cwd(), homeDir) {
3171
+ const detected = new Set(await detectInstalledTargets({ homeDir }));
3172
+ const resolved = [];
3173
+ for (const target of INSTALL_TARGETS) {
3174
+ if (detected.has(target)) {
3175
+ resolved.push({ target, dirs: await resolveStatusDirs(target, cwd, homeDir) });
3176
+ continue;
3177
+ }
3178
+ const projectLocalDirs = await resolveProjectLocalStatusDirs(target, cwd);
3179
+ if (projectLocalDirs.length > 0) resolved.push({ target, dirs: projectLocalDirs });
3180
+ }
3181
+ return resolved;
3182
+ }
3183
+ async function hasAnySkillSubdir(root) {
3184
+ let entries;
3185
+ try {
3186
+ entries = await readdir2(root, { withFileTypes: true });
3187
+ } catch {
3188
+ return false;
3189
+ }
3190
+ for (const entry of entries) {
3191
+ if (!entry.isDirectory()) continue;
3192
+ if (await fileExists(join5(root, entry.name, "SKILL.md"))) return true;
3193
+ }
3194
+ return false;
3195
+ }
3196
+ async function listSkillSubdirs(root) {
3197
+ let entries;
3198
+ try {
3199
+ entries = await readdir2(root, { withFileTypes: true });
3200
+ } catch {
3201
+ return [];
3202
+ }
3203
+ const matches = [];
3204
+ for (const entry of entries) {
3205
+ if (!entry.isDirectory()) continue;
3206
+ const candidate = join5(root, entry.name);
3207
+ if (await fileExists(join5(candidate, "SKILL.md"))) matches.push(candidate);
3208
+ }
3209
+ return matches.sort();
3210
+ }
3211
+ function uniqueResolvedDirs(dirs) {
3212
+ const seen = /* @__PURE__ */ new Set();
3213
+ const unique = [];
3214
+ for (const dir of dirs) {
3215
+ const resolved = resolve2(dir);
3216
+ if (seen.has(resolved)) continue;
3217
+ seen.add(resolved);
3218
+ unique.push(resolved);
3219
+ }
3220
+ return unique;
3221
+ }
3097
3222
  async function liveSkillHash(root, workspaceSlug, skill) {
3098
3223
  const slug = safeSkillSlug(skill.slug);
3099
3224
  const fileHashes = [];
@@ -3251,41 +3376,119 @@ function printNoDetectedTargets() {
3251
3376
  log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
3252
3377
  log.info(` More help: https://floom.dev/docs#agents`);
3253
3378
  }
3254
- async function statusCommand(options) {
3379
+ async function runStatusForDir(target, dir, deps) {
3380
+ const hasFloomManifest = await fileExists(manifestPath(dir));
3381
+ if (!hasFloomManifest) {
3382
+ const localSkillDirs = await listSkillSubdirs(dir);
3383
+ if (localSkillDirs.length > 0) {
3384
+ printRawLocalSkillsHint(target, dir, localSkillDirs);
3385
+ return { ok: true };
3386
+ }
3387
+ }
3388
+ try {
3389
+ const status = await statusLibrary(target, { ...deps, installDir: dir });
3390
+ printFloomManagedStatus(target, dir, status);
3391
+ return { ok: true };
3392
+ } catch (error) {
3393
+ return { ok: false, error };
3394
+ }
3395
+ }
3396
+ function shellSingleQuote(value) {
3397
+ return `'${value.replace(/'/g, "'\\''")}'`;
3398
+ }
3399
+ function printRawLocalSkillsHint(target, dir, localSkillDirs) {
3400
+ log.heading(`Local skills found (${target})`);
3401
+ log.info(`dir ${dir}`);
3402
+ log.info(` Floom does not manage these yet. Push them to your library:`);
3403
+ for (const skillDir of localSkillDirs) {
3404
+ log.info(` - ${skillDir}`);
3405
+ }
3406
+ log.info(` Push a single skill: npx -y @floomhq/floom push ${shellSingleQuote(localSkillDirs[0])}`);
3407
+ log.info(` Push them all: npx -y @floomhq/floom push ${shellSingleQuote(dir)}`);
3408
+ }
3409
+ function stateLabel(state) {
3410
+ switch (state) {
3411
+ case "active":
3412
+ return chalk3.green("up to date");
3413
+ case "stale":
3414
+ return chalk3.yellow("needs pull");
3415
+ case "dirty":
3416
+ return chalk3.yellow("local edits");
3417
+ case "conflict":
3418
+ return chalk3.red("conflict");
3419
+ case "missing":
3420
+ return "not installed";
3421
+ case "unsupported_target":
3422
+ return "other agent";
3423
+ }
3424
+ }
3425
+ function printFloomManagedStatus(target, dir, status) {
3426
+ log.info(`${status.workspaceName} \xB7 ${target} \xB7 ${dir}`);
3427
+ log.blank();
3428
+ const slugWidth = Math.max(...status.skills.map((s) => s.slug.length), 4);
3429
+ const labelWidth = 14;
3430
+ for (const line of status.skills) {
3431
+ const label = stateLabel(line.state);
3432
+ process.stdout.write(` ${line.slug.padEnd(slugWidth)} ${label.padEnd(labelWidth + (label.length - stripAnsiLength(label)))} ${line.version}
3433
+ `);
3434
+ }
3435
+ log.blank();
3436
+ const needsPull = status.skills.filter((s) => s.state === "stale").length;
3437
+ const localEdits = status.skills.filter((s) => s.state === "dirty").length;
3438
+ const conflicts = status.skills.filter((s) => s.state === "conflict").length;
3439
+ const parts = [`${status.skills.length} skill${status.skills.length !== 1 ? "s" : ""}`];
3440
+ if (needsPull > 0) parts.push(`${needsPull} needs pull`);
3441
+ if (localEdits > 0) parts.push(`${localEdits} with local edits`);
3442
+ if (conflicts > 0) parts.push(`${conflicts} conflict${conflicts !== 1 ? "s" : ""}`);
3443
+ log.info(parts.join(" \xB7 "));
3444
+ const showPull = needsPull > 0 || conflicts > 0;
3445
+ const showPush = localEdits > 0 || conflicts > 0;
3446
+ if (showPull || showPush) {
3447
+ log.blank();
3448
+ log.info("Next:");
3449
+ if (showPull) log.info(` ${log.cmd("floom pull")} update skills that are behind`);
3450
+ if (showPush) log.info(` ${log.cmd("floom push <dir>")} publish your local edits`);
3451
+ }
3452
+ }
3453
+ var ANSI_STRIP_RE = /\x1b\[[0-9;]*m/g;
3454
+ function stripAnsiLength(s) {
3455
+ return s.replace(ANSI_STRIP_RE, "").length;
3456
+ }
3457
+ async function statusCommand(options, deps = {}) {
3255
3458
  if (!options.target) {
3256
- const targets = await detectInstalledTargets();
3257
- if (targets.length === 0) {
3459
+ const targetDirs = await resolveAutoStatusTargets();
3460
+ if (targetDirs.length === 0) {
3258
3461
  printNoDetectedTargets();
3259
3462
  process.exitCode = 1;
3260
3463
  return;
3261
3464
  }
3262
- printDetectedTargets(targets);
3263
- const results = [];
3264
- for (const target2 of targets) {
3265
- try {
3266
- results.push({ target: target2, ok: true, status: await statusLibrary(target2) });
3267
- } catch (error) {
3268
- results.push({ target: target2, ok: false, error });
3269
- }
3270
- }
3271
- for (const result of results) {
3272
- if (!result.ok) {
3273
- log.err(`${result.target} ${result.error.message}`);
3274
- continue;
3465
+ printDetectedTargets(targetDirs.map(({ target: target2 }) => target2));
3466
+ let anyFailed2 = false;
3467
+ for (const { target: target2, dirs } of targetDirs) {
3468
+ for (const dir of dirs) {
3469
+ const result = await runStatusForDir(target2, dir, deps);
3470
+ if (!result.ok) {
3471
+ log.err(`${target2} ${dir} ${result.error.message}`);
3472
+ anyFailed2 = true;
3473
+ }
3275
3474
  }
3276
- log.heading(`${result.status.workspaceName} (${result.target})`);
3277
- for (const line of result.status.skills) log.info(`${line.slug} ${line.state} ${line.version}`);
3278
3475
  }
3279
- if (results.some((result) => !result.ok)) process.exitCode = 1;
3476
+ if (anyFailed2) process.exitCode = 1;
3280
3477
  return;
3281
3478
  }
3282
3479
  const target = assertInstallTarget(options.target);
3283
- const status = await statusLibrary(target);
3284
- log.heading(`${status.workspaceName} (${target})`);
3285
- for (const line of status.skills) log.info(`${line.slug} ${line.state} ${line.version}`);
3480
+ let anyFailed = false;
3481
+ for (const dir of await resolveStatusDirs(target)) {
3482
+ const result = await runStatusForDir(target, dir, deps);
3483
+ if (!result.ok) {
3484
+ log.err(`${target} ${dir} ${result.error.message}`);
3485
+ anyFailed = true;
3486
+ }
3487
+ }
3488
+ if (anyFailed) process.exitCode = 1;
3286
3489
  }
3287
3490
  async function statusLibrary(target, deps = {}) {
3288
- const resolved = resolveInstallDir({ target, global: true });
3491
+ const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
3289
3492
  const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "status");
3290
3493
  const manifest = await readManifest(resolved.dir);
3291
3494
  if (!manifest) {
@@ -3323,7 +3526,7 @@ async function statusLibrary(target, deps = {}) {
3323
3526
  }
3324
3527
  return { workspaceName: remote.workspace.name, skills };
3325
3528
  }
3326
- async function pullCommand(options) {
3529
+ async function pullCommand(options, deps = {}) {
3327
3530
  const cleanup = installCancellationHandler();
3328
3531
  try {
3329
3532
  if (!options.target) {
@@ -3338,39 +3541,50 @@ async function pullCommand(options) {
3338
3541
  log.info("Cancelled.");
3339
3542
  return;
3340
3543
  }
3544
+ const dirsByTarget = /* @__PURE__ */ new Map();
3341
3545
  for (const target2 of targets) {
3342
- const resolved2 = resolveInstallDir({ target: target2, global: true });
3343
- cleanup.trackDir(join5(resolved2.dir, ".floom", "tmp"));
3546
+ const dirs2 = await resolvePullDirs(target2);
3547
+ dirsByTarget.set(target2, dirs2);
3548
+ for (const dir of dirs2) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3344
3549
  }
3345
3550
  const results = [];
3346
3551
  for (const target2 of targets) {
3347
3552
  try {
3348
- const result2 = await pullLibrary(target2);
3349
- results.push({ target: target2, ok: true, ...result2 });
3553
+ const dirs2 = dirsByTarget.get(target2) ?? [];
3554
+ let skillCount2 = 0;
3555
+ for (const dir of dirs2) {
3556
+ const result = await pullLibrary(target2, { ...deps, installDir: dir });
3557
+ skillCount2 = result.skillCount;
3558
+ }
3559
+ results.push({ target: target2, ok: true, skillCount: skillCount2, dirs: dirs2 });
3350
3560
  } catch (error) {
3351
3561
  results.push({ target: target2, ok: false, error });
3352
3562
  }
3353
3563
  }
3354
3564
  log.heading("Pull summary:");
3355
- for (const result2 of results) {
3356
- if (result2.ok) log.ok(`${result2.target} ${result2.skillCount} skills ${result2.dir}`);
3357
- else log.err(`${result2.target} ${result2.error.message}`);
3565
+ for (const result of results) {
3566
+ if (result.ok) log.ok(`${result.target} ${result.skillCount} skills ${result.dirs.join(", ")}`);
3567
+ else log.err(`${result.target} ${result.error.message}`);
3358
3568
  }
3359
- if (results.some((result2) => !result2.ok)) process.exitCode = 1;
3569
+ if (results.some((result) => !result.ok)) process.exitCode = 1;
3360
3570
  return;
3361
3571
  }
3362
3572
  const target = assertInstallTarget(options.target);
3363
- const resolved = resolveInstallDir({ target, global: true });
3364
- cleanup.trackDir(join5(resolved.dir, ".floom", "tmp"));
3365
- const result = await pullLibrary(target);
3366
- log.ok(`Pulled ${result.skillCount} skills into ${target} (${result.dir}).`);
3573
+ const dirs = await resolvePullDirs(target);
3574
+ for (const dir of dirs) cleanup.trackDir(join5(dir, ".floom", "tmp"));
3575
+ let skillCount = 0;
3576
+ for (const dir of dirs) {
3577
+ const result = await pullLibrary(target, { ...deps, installDir: dir });
3578
+ skillCount = result.skillCount;
3579
+ }
3580
+ log.ok(`Pulled ${skillCount} skills into ${target} (${dirs.join(", ")}).`);
3367
3581
  log.info(`This syncs ${target} only. For another agent: npx -y @floomhq/floom pull --target <claude|codex|cursor|gemini|opencode>`);
3368
3582
  } finally {
3369
3583
  cleanup.dispose();
3370
3584
  }
3371
3585
  }
3372
3586
  async function pullLibrary(target, deps = {}) {
3373
- const resolved = resolveInstallDir({ target, global: true });
3587
+ const resolved = { dir: deps.installDir ?? resolveInstallDir({ target, global: true }).dir };
3374
3588
  const remote = await (deps.fetchLibrary ?? fetchLibrary)(target, "pull");
3375
3589
  for (const skill of remote.skills) safeSkillSlug(skill.slug);
3376
3590
  const manifest = await readManifest(resolved.dir);
@@ -3630,13 +3844,19 @@ function createMcpServer(deps = {}) {
3630
3844
  return server;
3631
3845
  }
3632
3846
  async function mcpCommand() {
3847
+ if (process.stderr.isTTY || process.stdin.isTTY) {
3848
+ process.stderr.write("Floom MCP server running (stdio).\n");
3849
+ process.stderr.write("This is launched by your AI agent, not run by hand.\n");
3850
+ process.stderr.write("Add it to Claude: claude mcp add floom -- npx -y @floomhq/floom mcp\n");
3851
+ process.stderr.write("Ctrl+C to stop.\n");
3852
+ }
3633
3853
  const server = createMcpServer();
3634
3854
  const transport = new StdioServerTransport();
3635
3855
  await server.connect(transport);
3636
3856
  }
3637
3857
 
3638
3858
  // src/commands/push.ts
3639
- import { basename, join as join6, resolve as resolve2 } from "node:path";
3859
+ import { basename, join as join6, resolve as resolve3 } from "node:path";
3640
3860
  import { readdir as readdir3, readFile as readFile6, stat as stat4 } from "node:fs/promises";
3641
3861
  function parseConcurrency(value) {
3642
3862
  const raw = value ?? 6;
@@ -3701,7 +3921,7 @@ async function runBounded(items, concurrency, worker) {
3701
3921
  async function pushCommand(dir = ".", options = {}, deps = {}) {
3702
3922
  const cleanup = installCancellationHandler();
3703
3923
  try {
3704
- const root = resolve2(dir);
3924
+ const root = resolve3(dir);
3705
3925
  const dirStat = await stat4(root).catch(() => null);
3706
3926
  if (!dirStat || !dirStat.isDirectory()) {
3707
3927
  throw new Error(
@@ -3713,6 +3933,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3713
3933
  if (await hasSkillMarkdown(root)) {
3714
3934
  const result = await pushOneSkill(root, pushApi);
3715
3935
  log.ok(`Pushed ${result.skill.slug} ${result.skill.latest.display}.`);
3936
+ log.next("Next: pull this skill into your AI agents.", "floom pull");
3716
3937
  return;
3717
3938
  }
3718
3939
  const skillDirs = await findImmediateSkillDirs(root);
@@ -3729,7 +3950,7 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3729
3950
  try {
3730
3951
  const result = await pushOneSkill(skillDir, pushApi);
3731
3952
  pushed += 1;
3732
- log.info(`\u2713 ${result.skill.slug} ${result.skill.latest.display}`);
3953
+ log.ok(`${result.skill.slug} ${result.skill.latest.display}`);
3733
3954
  } catch (error) {
3734
3955
  errors.push({ slug, message: error.message });
3735
3956
  }
@@ -3766,19 +3987,196 @@ async function deleteCommand(slug, opts = {}, deps = defaultDeps) {
3766
3987
  async function listCommand() {
3767
3988
  const result = await api("/skills", { authRequired: true });
3768
3989
  if (result.total === 0) {
3769
- log.info("No skills in this workspace.");
3990
+ log.info("No skills in this workspace yet.");
3991
+ log.next("Next: publish your first skill.", "floom push ./path/to/skill-folder");
3770
3992
  return;
3771
3993
  }
3994
+ const slugWidth = Math.max(...result.skills.map((s) => s.slug.length), 4);
3995
+ const verWidth = Math.max(...result.skills.map((s) => s.latest_version.display.length), 3);
3772
3996
  for (const skill of result.skills) {
3773
- log.info(`${skill.slug} ${skill.latest_version.display} ${skill.title}`);
3997
+ log.row([skill.slug, skill.latest_version.display, skill.title], [slugWidth, verWidth, 0]);
3998
+ }
3999
+ }
4000
+
4001
+ // src/commands/sync-command.ts
4002
+ import { cp as cp2, mkdtemp, readdir as readdir4, rm as rm2, stat as stat5 } from "node:fs/promises";
4003
+ import { tmpdir } from "node:os";
4004
+ import { join as join7 } from "node:path";
4005
+ import { createInterface as createInterface2 } from "node:readline/promises";
4006
+ async function hasSkillMarkdown2(dir) {
4007
+ try {
4008
+ const skillMd = await stat5(join7(dir, "SKILL.md"));
4009
+ return skillMd.isFile();
4010
+ } catch {
4011
+ return false;
4012
+ }
4013
+ }
4014
+ async function findImmediateSkillDirs2(root) {
4015
+ let entries;
4016
+ try {
4017
+ entries = await readdir4(root, { withFileTypes: true });
4018
+ } catch (error) {
4019
+ const err = error;
4020
+ if (err.code === "ENOENT") return [];
4021
+ throw error;
4022
+ }
4023
+ const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== ".floom").map((entry) => ({ slug: entry.name, dir: join7(root, entry.name) })).sort((a, b) => a.slug.localeCompare(b.slug));
4024
+ const checks = await Promise.all(dirs.map(async (entry) => await hasSkillMarkdown2(entry.dir) ? entry : null));
4025
+ return checks.filter((entry) => Boolean(entry));
4026
+ }
4027
+ function formatSlugs(slugs) {
4028
+ return slugs.length > 0 ? slugs.join(", ") : "none";
4029
+ }
4030
+ function printPreview(plan) {
4031
+ log.info(
4032
+ `Pull: ${plan.pull.length} skills (${formatSlugs(plan.pull.map((skill) => skill.slug))}). Push: ${plan.push.length} skills (${formatSlugs(plan.push.map((skill) => skill.slug))}). Skip (conflict): ${plan.conflicts.length} skills (${formatSlugs(plan.conflicts.map((skill) => skill.slug))}).`
4033
+ );
4034
+ if (plan.conflicts.length > 0) {
4035
+ const count = plan.conflicts.length;
4036
+ const noun = count === 1 ? "skill changed" : "skills changed";
4037
+ log.err(`${count} ${noun} both locally and on the server: Floom won't guess which wins.`);
4038
+ for (const skill of plan.conflicts) {
4039
+ log.err(` - ${skill.slug} (${skill.version})`);
4040
+ log.err(` Keep the server version: floom pull --target ${plan.target}`);
4041
+ log.err(` Keep your local version: floom push <${skill.slug}-dir>`);
4042
+ }
4043
+ log.err("Your local copy is always backed up to .floom/backups/ first.");
4044
+ log.err("More: https://floom.dev/docs#conflicts");
4045
+ }
4046
+ }
4047
+ async function defaultConfirmProceed() {
4048
+ if (!process.stdin.isTTY) {
4049
+ log.err("Aborted: confirmation is required in non-interactive mode. Re-run with --yes to proceed.");
4050
+ return false;
4051
+ }
4052
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
4053
+ try {
4054
+ const answer = (await rl.question("Proceed? [Y/n] ")).trim().toLowerCase();
4055
+ return answer === "" || answer === "y" || answer === "yes";
4056
+ } finally {
4057
+ rl.close();
4058
+ }
4059
+ }
4060
+ async function resolveTarget(options, deps) {
4061
+ if (options.target) return assertInstallTarget(options.target);
4062
+ const targets = await (deps.detectInstalledTargets ?? detectInstalledTargets)();
4063
+ if (targets.length === 0) {
4064
+ log.err("No AI agent recognized on this machine. Re-run with --target <claude|codex|cursor|gemini|opencode>.");
4065
+ process.exitCode = 1;
4066
+ return null;
4067
+ }
4068
+ if (targets.length > 1) {
4069
+ log.err(`Multiple AI agents detected (${targets.join(", ")}). Re-run with --target <agent>.`);
4070
+ process.exitCode = 1;
4071
+ return null;
4072
+ }
4073
+ return targets[0];
4074
+ }
4075
+ async function createPushSnapshots(pushes) {
4076
+ const root = await mkdtemp(join7(tmpdir(), "floom-sync-push-"));
4077
+ const dirs = [];
4078
+ try {
4079
+ for (const push of pushes) {
4080
+ const dest = join7(root, push.slug);
4081
+ await cp2(push.dir, dest, { recursive: true });
4082
+ dirs.push({ slug: push.slug, dir: dest });
4083
+ }
4084
+ return { root, dirs };
4085
+ } catch (error) {
4086
+ await rm2(root, { recursive: true, force: true });
4087
+ throw error;
4088
+ }
4089
+ }
4090
+ async function buildPlan(options, deps) {
4091
+ const target = await resolveTarget(options, deps);
4092
+ if (!target) return null;
4093
+ const status = await (deps.statusLibrary ?? statusLibrary)(target);
4094
+ const dirtySlugs = new Set(status.skills.filter((skill) => skill.state === "dirty").map((skill) => skill.slug));
4095
+ const installDir = resolveInstallDir({ target, global: true }).dir;
4096
+ const localSkillDirs = await findImmediateSkillDirs2(installDir);
4097
+ return {
4098
+ target,
4099
+ pull: status.skills.filter((skill) => skill.state === "missing" || skill.state === "stale"),
4100
+ push: localSkillDirs.filter((entry) => dirtySlugs.has(entry.slug)),
4101
+ conflicts: status.skills.filter((skill) => skill.state === "conflict")
4102
+ };
4103
+ }
4104
+ async function syncCommand(options = {}, deps = {}) {
4105
+ const plan = await buildPlan(options, deps);
4106
+ if (!plan) return;
4107
+ if (plan.pull.length === 0 && plan.push.length === 0 && plan.conflicts.length === 0) {
4108
+ log.info("Already in sync.");
4109
+ return;
4110
+ }
4111
+ printPreview(plan);
4112
+ if (plan.conflicts.length > 0) {
4113
+ process.exitCode = 1;
4114
+ return;
4115
+ }
4116
+ if (plan.pull.length === 0 && plan.push.length === 0) return;
4117
+ if (options.yes) {
4118
+ log.info("Proceeding (--yes).");
4119
+ } else if (!await (deps.confirmProceed ?? defaultConfirmProceed)()) {
4120
+ log.info("Aborted.");
4121
+ return;
4122
+ }
4123
+ const snapshots = await createPushSnapshots(plan.push);
4124
+ const pushFailures = [];
4125
+ let pushed = 0;
4126
+ let preserveSnapshotsForRecovery = false;
4127
+ try {
4128
+ try {
4129
+ await (deps.pullLibrary ?? pullLibrary)(plan.target);
4130
+ } catch (error) {
4131
+ log.err(`Pull failed: ${error.message}`);
4132
+ process.exitCode = 1;
4133
+ return;
4134
+ }
4135
+ for (const snapshot of snapshots.dirs) {
4136
+ try {
4137
+ await (deps.pushSkill ?? ((dir) => pushCommand(dir)))(snapshot.dir);
4138
+ pushed += 1;
4139
+ } catch (error) {
4140
+ pushFailures.push({
4141
+ slug: snapshot.slug,
4142
+ message: error.message,
4143
+ snapshotDir: snapshot.dir
4144
+ });
4145
+ }
4146
+ }
4147
+ } finally {
4148
+ if (pushFailures.length === 0) {
4149
+ await rm2(snapshots.root, { recursive: true, force: true });
4150
+ } else {
4151
+ preserveSnapshotsForRecovery = true;
4152
+ }
4153
+ }
4154
+ if (pushFailures.length > 0) {
4155
+ log.err(`Push failed for ${pushFailures.length} of ${plan.push.length} skill(s).`);
4156
+ log.err("Your local edits are preserved in two places (nothing is lost):");
4157
+ log.err(` 1. Snapshot taken before pull: ${snapshots.root}`);
4158
+ log.err(` 2. Pull backup of pre-pull dir: ${plan.target} \u2192 .floom/backups/<latest>/`);
4159
+ log.err("");
4160
+ log.err("Failed skills:");
4161
+ for (const failure of pushFailures) {
4162
+ log.err(` - ${failure.slug}: ${failure.message}`);
4163
+ log.err(` snapshot at: ${failure.snapshotDir}`);
4164
+ }
4165
+ log.err("");
4166
+ log.err(`Re-run \`floom sync --target ${plan.target}\` after the network recovers.`);
4167
+ process.exitCode = 1;
4168
+ }
4169
+ void preserveSnapshotsForRecovery;
4170
+ if (pushFailures.length === 0) {
4171
+ log.ok(`Sync complete. Pulled ${plan.pull.length} skills. Pushed ${pushed}/${plan.push.length} skills.`);
3774
4172
  }
3775
4173
  }
3776
4174
 
3777
4175
  // src/commands/rename-machine.ts
3778
4176
  import { readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
3779
- import { join as join7 } from "node:path";
4177
+ import { join as join8 } from "node:path";
3780
4178
  import { homedir as homedir4 } from "node:os";
3781
- var MACHINE_FILE2 = join7(homedir4(), ".floom", "machine.json");
4179
+ var MACHINE_FILE2 = join8(homedir4(), ".floom", "machine.json");
3782
4180
  async function renameMachineCommand(newLabel, _opts) {
3783
4181
  const trimmed = newLabel.trim().slice(0, 80);
3784
4182
  if (!trimmed) {
@@ -3817,8 +4215,8 @@ async function renameMachineCommand(newLabel, _opts) {
3817
4215
  }
3818
4216
 
3819
4217
  // src/commands/add.ts
3820
- import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
3821
- import { join as join8 } from "node:path";
4218
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
4219
+ import { join as join9 } from "node:path";
3822
4220
  var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
3823
4221
  function parseToken(input) {
3824
4222
  const trimmed = input.trim();
@@ -3971,27 +4369,27 @@ async function addCommand(input, opts = {}) {
3971
4369
  safeRemotePath2(file.path);
3972
4370
  }
3973
4371
  const cleanup = installCancellationHandler();
3974
- const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
4372
+ const tempDir = join9(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
3975
4373
  cleanup.trackDir(tempDir);
3976
4374
  try {
3977
4375
  await mkdir5(tempDir, { recursive: true });
3978
4376
  for (const file of shareData.file_contents) {
3979
4377
  const safePath = safeRemotePath2(file.path);
3980
- const dest = join8(tempDir, ...safePath.split("/"));
4378
+ const dest = join9(tempDir, ...safePath.split("/"));
3981
4379
  const destDir = dest.substring(0, dest.lastIndexOf("/"));
3982
4380
  if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
3983
4381
  await writeFile5(dest, bytesForShareFile(file));
3984
4382
  }
3985
- const finalDir = join8(resolved.dir, slug);
3986
- const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
4383
+ const finalDir = join9(resolved.dir, slug);
4384
+ const replacedDir = join9(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
3987
4385
  let movedExisting = false;
3988
4386
  try {
3989
4387
  const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
3990
4388
  await mkdir5(resolved.dir, { recursive: true });
3991
- const { stat: stat5 } = await import("node:fs/promises");
4389
+ const { stat: stat6 } = await import("node:fs/promises");
3992
4390
  let existingDir = false;
3993
4391
  try {
3994
- await stat5(finalDir);
4392
+ await stat6(finalDir);
3995
4393
  existingDir = true;
3996
4394
  } catch {
3997
4395
  }
@@ -4025,7 +4423,7 @@ async function addCommand(input, opts = {}) {
4025
4423
  } finally {
4026
4424
  cleanup.dispose();
4027
4425
  try {
4028
- await rm2(tempDir, { recursive: true, force: true });
4426
+ await rm3(tempDir, { recursive: true, force: true });
4029
4427
  } catch {
4030
4428
  }
4031
4429
  }
@@ -4075,18 +4473,30 @@ async function whoamiCommand() {
4075
4473
  }
4076
4474
  }
4077
4475
  var program = new Command();
4078
- program.name("floom").description("Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose.").version(VERSION);
4079
- program.command("login").description("Log in via browser device flow.").action(loginCommand);
4080
- program.command("logout").description("Clear local Floom auth.").action(logoutCommand);
4081
- program.command("whoami").description("Show local auth state.").action(whoamiCommand);
4082
- program.command("push [dir]").description("Push a skill folder.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
4083
- program.command("delete <slug>").description("Delete a workspace skill.").option("--yes", "Skip confirmation").action((slug, opts) => deleteCommand(slug, opts));
4084
- program.command("pull").description("Pull the whole workspace library.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(pullCommand);
4476
+ program.name("floom").description("Floom: one shared skill library, synced across every AI agent.").version(VERSION).addHelpCommand(false).hook("preAction", () => {
4477
+ });
4478
+ program.action(() => {
4479
+ log.info("Floom: one shared skill library, synced across every AI agent.");
4480
+ log.blank();
4481
+ log.info("New here?");
4482
+ log.info(` 1. ${log.cmd("floom login")} sign in`);
4483
+ log.info(` 2. ${log.cmd("floom pull")} get your team's skills (or ${log.cmd("floom push <dir>")} to publish)`);
4484
+ log.info(` 3. ${log.cmd("floom status")} see what's synced`);
4485
+ log.blank();
4486
+ log.info(`All commands: ${log.cmd("floom --help")}`);
4487
+ });
4488
+ program.command("login").description("Sign in via browser.").action(loginCommand);
4489
+ program.command("logout").description("Sign out and clear local auth.").action(logoutCommand);
4490
+ program.command("whoami").description("Show who you are signed in as.").action(whoamiCommand);
4491
+ program.command("push [dir]").description("Publish a skill folder to your workspace.").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").action(pushCommand);
4492
+ program.command("delete <slug>").description("Delete a skill from your workspace.").option("--yes", "Skip confirmation").action((slug, opts) => deleteCommand(slug, opts));
4493
+ program.command("pull").description("Pull the workspace library into your AI agents.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(pullCommand);
4494
+ program.command("sync").description("Pull remote changes, then push any local edits.").option("--target <target>", "claude | codex | cursor | gemini | opencode").option("--yes", "Skip confirmation").action(syncCommand);
4085
4495
  program.command("list").description("List workspace skills.").action(listCommand);
4086
4496
  program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
4087
4497
  program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
4088
4498
  program.command("add <share-url-or-token>").description("Install a skill from a Floom share link.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action((input, opts) => addCommand(input, opts));
4089
- program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
4499
+ program.command("rename-machine <label>").description('Set the friendly name for this machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
4090
4500
  async function main() {
4091
4501
  try {
4092
4502
  await program.parseAsync(process.argv);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "2.0.5";
1
+ export const VERSION = "2.0.6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://floom.dev",
@@ -41,6 +41,7 @@
41
41
  "zod": "^3.23.8"
42
42
  },
43
43
  "devDependencies": {
44
+ "@floom/shared": "workspace:*",
44
45
  "esbuild": "^0.27.7",
45
46
  "@types/node": "^22.0.0",
46
47
  "@types/prompts": "^2.4.9",