@floomhq/skills 0.2.8 → 0.2.9

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
@@ -1907,10 +1907,10 @@ function parseManifest(raw) {
1907
1907
  };
1908
1908
  }
1909
1909
  async function readManifest(skillDir) {
1910
- const { readFile: readFile9 } = await import("node:fs/promises");
1911
- const { join: join12 } = await import("node:path");
1910
+ const { readFile: readFile11 } = await import("node:fs/promises");
1911
+ const { join: join15 } = await import("node:path");
1912
1912
  try {
1913
- const raw = await readFile9(join12(skillDir, "skill.json"), "utf8");
1913
+ const raw = await readFile11(join15(skillDir, "skill.json"), "utf8");
1914
1914
  let parsed;
1915
1915
  try {
1916
1916
  parsed = JSON.parse(raw);
@@ -2049,12 +2049,13 @@ function presetDir(target, opts) {
2049
2049
  return join(root, ".claude", "skills");
2050
2050
  case "gemini":
2051
2051
  return join(root, ".gemini", "skills");
2052
+ case "kimi":
2053
+ return join(root, ".kimi", "skills");
2052
2054
  case "codex":
2053
2055
  case "cursor":
2054
2056
  case "generic":
2055
2057
  case "all":
2056
2058
  case "opencode":
2057
- case "kimi":
2058
2059
  default:
2059
2060
  return join(root, ".agents", "skills");
2060
2061
  }
@@ -2364,7 +2365,7 @@ var log = {
2364
2365
  err: (msg) => console.error(chalk.red("\u2717 ") + msg),
2365
2366
  step: (msg) => console.log(chalk.dim("\xB7 ") + msg),
2366
2367
  heading: (msg) => console.log("\n" + chalk.bold(msg)),
2367
- kv: (key, value) => console.log(` ${chalk.dim(key.padEnd(16))}${value}`),
2368
+ kv: (key, value) => console.log(` ${chalk.dim(key.padEnd(18))}${value}`),
2368
2369
  blank: () => console.log("")
2369
2370
  };
2370
2371
 
@@ -2448,7 +2449,7 @@ function isLegacyApiUrl(apiUrl) {
2448
2449
  }
2449
2450
 
2450
2451
  // src/version.ts
2451
- var VERSION = "0.2.8";
2452
+ var VERSION = "0.2.9";
2452
2453
 
2453
2454
  // src/api-client.ts
2454
2455
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2968,7 +2969,7 @@ import { join as join8 } from "node:path";
2968
2969
  import { tmpdir } from "node:os";
2969
2970
 
2970
2971
  // src/lib/floom-lock.ts
2971
- import { readFile as readFile7, writeFile as writeFile3, stat as stat3, readdir as readdir2 } from "node:fs/promises";
2972
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as stat3, readdir as readdir2 } from "node:fs/promises";
2972
2973
  import { join as join7, relative as relative2, sep as sep2, posix as posix2 } from "node:path";
2973
2974
  import { createHash as createHash3 } from "node:crypto";
2974
2975
  var EMPTY = { schema_version: "0.1", skills: {} };
@@ -2977,6 +2978,7 @@ async function readLock(projectDir) {
2977
2978
  const raw = await readFile7(join7(projectDir, "floom.lock"), "utf8");
2978
2979
  const parsed = JSON.parse(raw);
2979
2980
  if (parsed.schema_version === "0.1") return parsed;
2981
+ if (parsed.schema_version === "0.2") return parsed;
2980
2982
  const version = typeof parsed.schema_version === "string" ? parsed.schema_version : "missing";
2981
2983
  throw new Error(
2982
2984
  `LOCK_SCHEMA_UNSUPPORTED: floom.lock schema_version ${version} is not supported by this CLI. The existing floom.lock was left unchanged; migrate it or reinstall skills with the current CLI.`
@@ -2987,11 +2989,32 @@ async function readLock(projectDir) {
2987
2989
  }
2988
2990
  }
2989
2991
  async function writeLock(projectDir, lock) {
2992
+ await mkdir3(projectDir, { recursive: true });
2990
2993
  await writeFile3(join7(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
2991
2994
  }
2992
2995
  function setLockEntry(lock, ref, entry) {
2993
2996
  return { ...lock, skills: { ...lock.skills, [ref]: entry } };
2994
2997
  }
2998
+ function upgradeLockToV02(lock, opts = {}) {
2999
+ if (lock.schema_version === "0.2") {
3000
+ return {
3001
+ ...lock,
3002
+ target: opts.target ?? lock.target,
3003
+ scope: opts.scope ?? lock.scope,
3004
+ default_workspace: opts.defaultWorkspace ?? lock.default_workspace,
3005
+ last_sync_at: (/* @__PURE__ */ new Date()).toISOString()
3006
+ };
3007
+ }
3008
+ return {
3009
+ schema_version: "0.2",
3010
+ default_workspace: opts.defaultWorkspace,
3011
+ target: opts.target,
3012
+ scope: opts.scope,
3013
+ last_sync_at: (/* @__PURE__ */ new Date()).toISOString(),
3014
+ instructions: [],
3015
+ skills: lock.skills
3016
+ };
3017
+ }
2995
3018
  async function hashInstalledFolder(folderAbs) {
2996
3019
  const out = [];
2997
3020
  async function walk2(dir) {
@@ -3406,11 +3429,509 @@ async function libraryLeaveCommand(librarySlug) {
3406
3429
  log.ok(`Left workspace ${librarySlug}`);
3407
3430
  }
3408
3431
 
3432
+ // src/lib/config-file.ts
3433
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
3434
+ import { homedir as homedir3 } from "node:os";
3435
+ import { dirname, join as join10 } from "node:path";
3436
+ import { z as z2 } from "zod";
3437
+ var FLOOM_CONFIG_VERSION = "0.1";
3438
+ var TARGETS = ["claude", "codex", "cursor", "kimi", "opencode"];
3439
+ var targetConfigSchema = z2.object({
3440
+ active_workspaces: z2.array(z2.string().min(1)).default([]),
3441
+ instruction_path: z2.string().min(1).optional()
3442
+ });
3443
+ var configSchema = z2.object({
3444
+ version: z2.literal(FLOOM_CONFIG_VERSION).default(FLOOM_CONFIG_VERSION),
3445
+ default_workspace: z2.string().min(1).optional(),
3446
+ targets: z2.record(z2.enum(TARGETS), targetConfigSchema).default({})
3447
+ });
3448
+ function configPath(scope, cwd = process.cwd()) {
3449
+ if (scope === "global") return join10(homedir3(), ".floom", "config.json");
3450
+ return join10(cwd, ".floom", "config.json");
3451
+ }
3452
+ async function readFloomConfig(scope, cwd = process.cwd()) {
3453
+ try {
3454
+ const raw = await readFile8(configPath(scope, cwd), "utf8");
3455
+ return configSchema.parse(JSON.parse(raw));
3456
+ } catch (e) {
3457
+ if (e.code === "ENOENT") return configSchema.parse({});
3458
+ throw e;
3459
+ }
3460
+ }
3461
+ async function writeFloomConfig(scope, config, cwd = process.cwd()) {
3462
+ const path = configPath(scope, cwd);
3463
+ const parsed = configSchema.parse(config);
3464
+ await mkdir6(dirname(path), { recursive: true, mode: 448 });
3465
+ await writeFile4(path, JSON.stringify(parsed, null, 2) + "\n", { mode: 384 });
3466
+ }
3467
+ function normalizeScope(value) {
3468
+ return value === "global" ? "global" : "local";
3469
+ }
3470
+ function assertTarget(value) {
3471
+ if (TARGETS.includes(value)) return value;
3472
+ throw new Error(`Invalid target: ${value}. Expected ${TARGETS.join(", ")}`);
3473
+ }
3474
+ async function setDefaultWorkspace(slug, scope, cwd = process.cwd()) {
3475
+ const config = await readFloomConfig(scope, cwd);
3476
+ const next = { ...config, default_workspace: slug };
3477
+ await writeFloomConfig(scope, next, cwd);
3478
+ return next;
3479
+ }
3480
+ async function setWorkspaceActive(input) {
3481
+ const config = await readFloomConfig(input.scope, input.cwd);
3482
+ const current = config.targets[input.target] ?? { active_workspaces: [] };
3483
+ const set = new Set(current.active_workspaces);
3484
+ if (input.active) set.add(input.workspace);
3485
+ else set.delete(input.workspace);
3486
+ const next = {
3487
+ ...config,
3488
+ targets: {
3489
+ ...config.targets,
3490
+ [input.target]: {
3491
+ ...current,
3492
+ active_workspaces: Array.from(set).sort()
3493
+ }
3494
+ }
3495
+ };
3496
+ await writeFloomConfig(input.scope, next, input.cwd);
3497
+ return next;
3498
+ }
3499
+
3500
+ // src/commands/instruction.ts
3501
+ import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
3502
+ import { basename as basename2, dirname as dirname2, join as join11 } from "node:path";
3503
+ import { createHash as createHash4 } from "node:crypto";
3504
+ var START = "<!-- FLOOM START -->";
3505
+ var END = "<!-- FLOOM END -->";
3506
+ var AGENT_INSTRUCTION_TARGETS = ["claude", "codex", "cursor", "opencode"];
3507
+ function defaultInstructionPath(target) {
3508
+ if (target === "claude") return "CLAUDE.md";
3509
+ if (target === "codex") return "AGENTS.md";
3510
+ if (target === "cursor") return ".cursor/rules/floom.mdc";
3511
+ if (target === "opencode") return ".opencode/instructions/floom.md";
3512
+ throw new Error(`No default instruction path for target ${target}`);
3513
+ }
3514
+ function replaceManagedBlock(existing, blockBody) {
3515
+ const block = `${START}
3516
+ ${blockBody.trim()}
3517
+ ${END}
3518
+ `;
3519
+ const start = existing.indexOf(START);
3520
+ const end = existing.indexOf(END);
3521
+ if (start >= 0 && end > start) {
3522
+ const afterEnd = end + END.length;
3523
+ const prefix = existing.slice(0, start);
3524
+ const suffix = existing.slice(afterEnd).replace(/^\n/, "");
3525
+ return { content: `${prefix}${block}${suffix}`, hadBlock: true };
3526
+ }
3527
+ return { content: existing.trim() ? `${existing.trimEnd()}
3528
+
3529
+ ${block}` : block, hadBlock: false };
3530
+ }
3531
+ function resolveScope(opts) {
3532
+ if (opts.account && opts.workspace) throw new Error("Use either --account or --workspace, not both.");
3533
+ if (opts.account) return "account";
3534
+ if (opts.workspace) return "workspace";
3535
+ throw new Error("Instruction command requires --account or --workspace <slug>.");
3536
+ }
3537
+ function bodySha256(body) {
3538
+ return createHash4("sha256").update(body, "utf8").digest("hex");
3539
+ }
3540
+ async function readUtf8FileStrict(path) {
3541
+ const bytes = await readFile9(path);
3542
+ try {
3543
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3544
+ } catch {
3545
+ throw new Error(`${path} is not valid UTF-8; refusing to rewrite it as a managed instruction file.`);
3546
+ }
3547
+ return bytes.toString("utf8");
3548
+ }
3549
+ function assertAgentInstructionTarget(value) {
3550
+ if (AGENT_INSTRUCTION_TARGETS.includes(value)) return value;
3551
+ throw new Error(`Invalid instruction target: ${value}. Expected ${AGENT_INSTRUCTION_TARGETS.join(", ")}. Kimi uses the Floom guide skill in V0.`);
3552
+ }
3553
+ function assertPublishInstructionTarget(value) {
3554
+ if (value === "default") return "default";
3555
+ return assertAgentInstructionTarget(value);
3556
+ }
3557
+ async function fetchInstruction(input) {
3558
+ return api("/instructions", {
3559
+ authRequired: true,
3560
+ query: input
3561
+ });
3562
+ }
3563
+ function buildInstructionBlock(sections) {
3564
+ return [
3565
+ "# Floom",
3566
+ "",
3567
+ "Use Floom for workspace-approved AI skills and instructions.",
3568
+ "",
3569
+ ...sections.flatMap((section) => [
3570
+ `## ${section.label}`,
3571
+ "",
3572
+ section.body_md.trim(),
3573
+ ""
3574
+ ])
3575
+ ].join("\n");
3576
+ }
3577
+ async function writeManagedInstructionFile(input) {
3578
+ let existing = "";
3579
+ let existed = true;
3580
+ try {
3581
+ existing = await readUtf8FileStrict(input.path);
3582
+ } catch (e) {
3583
+ if (e.code === "ENOENT") existed = false;
3584
+ else throw e;
3585
+ }
3586
+ const next = replaceManagedBlock(existing, input.blockBody);
3587
+ if (existed && !next.hadBlock && !input.apply && !input.force) {
3588
+ log.warn(`Refusing to modify ${input.path} without an existing Floom managed block.`);
3589
+ log.info("Re-run with --apply to append the managed block, or --path <file> for a dedicated file.");
3590
+ process.exit(1);
3591
+ }
3592
+ if (existed && existing === next.content) return { changed: false };
3593
+ let backupPath;
3594
+ if (existed && existing) {
3595
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3596
+ backupPath = join11(".floom", "backups", `${basename2(input.path)}.${stamp}.bak`);
3597
+ await mkdir7(dirname2(backupPath), { recursive: true, mode: 448 });
3598
+ await writeFile5(backupPath, existing, { mode: 384 });
3599
+ }
3600
+ await mkdir7(dirname2(input.path), { recursive: true });
3601
+ await writeFile5(input.path, next.content, "utf8");
3602
+ return { changed: true, backupPath };
3603
+ }
3604
+ async function recordCliActivity(input) {
3605
+ try {
3606
+ await api("/cli/activity", { method: "POST", authRequired: true, body: input });
3607
+ } catch (e) {
3608
+ log.warn(`Activity event not recorded: ${e.message}`);
3609
+ }
3610
+ }
3611
+ async function upsertInstructionLock(input) {
3612
+ const lock = upgradeLockToV02(await readLock(process.cwd()), {
3613
+ target: input.target,
3614
+ scope: input.configScope,
3615
+ defaultWorkspace: input.defaultWorkspace
3616
+ });
3617
+ const instructions = (lock.instructions ?? []).filter(
3618
+ (entry) => !(entry.scope === input.scope && entry.workspace === input.workspace && entry.target === input.target && entry.path === input.path)
3619
+ );
3620
+ instructions.push({
3621
+ scope: input.scope,
3622
+ workspace: input.workspace,
3623
+ target: input.target,
3624
+ version_id: input.version_id,
3625
+ body_sha256: input.body_sha256,
3626
+ pulled_at: (/* @__PURE__ */ new Date()).toISOString(),
3627
+ path: input.path
3628
+ });
3629
+ await writeLock(process.cwd(), { ...lock, instructions, last_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
3630
+ }
3631
+ async function instructionPullCommand(opts = {}) {
3632
+ const target = assertAgentInstructionTarget(opts.target ?? "codex");
3633
+ const configScope = normalizeScope(opts.scope);
3634
+ const scope = resolveScope(opts);
3635
+ const config = await readFloomConfig(configScope);
3636
+ const workspace = opts.workspace ?? (scope === "workspace" ? config.default_workspace : void 0);
3637
+ if (scope === "workspace" && !workspace) throw new Error("Workspace instruction pull requires --workspace <slug> or a configured default workspace.");
3638
+ const path = opts.path ?? defaultInstructionPath(target);
3639
+ const resp = await fetchInstruction({ scope, workspace, target });
3640
+ if (!resp.instruction?.latest) {
3641
+ log.info("No remote instruction found.");
3642
+ return;
3643
+ }
3644
+ const label = scope === "account" ? "Account Instructions" : `Workspace Instructions: ${workspace}`;
3645
+ const writeResult = await writeManagedInstructionFile({
3646
+ path,
3647
+ blockBody: buildInstructionBlock([{ label, body_md: resp.instruction.latest.body_md }]),
3648
+ apply: opts.apply,
3649
+ force: opts.force
3650
+ });
3651
+ if (!writeResult.changed) {
3652
+ log.info(`${path} is already at the latest ${scope} instruction.`);
3653
+ return;
3654
+ }
3655
+ if (writeResult.backupPath) log.info(`Backed up previous instruction file to ${writeResult.backupPath}`);
3656
+ await upsertInstructionLock({
3657
+ scope,
3658
+ workspace,
3659
+ target,
3660
+ path,
3661
+ version_id: resp.instruction.latest.id,
3662
+ body_sha256: resp.instruction.latest.body_sha256,
3663
+ configScope,
3664
+ defaultWorkspace: config.default_workspace
3665
+ });
3666
+ await recordCliActivity({
3667
+ event_type: "instruction.pulled_cli",
3668
+ scope,
3669
+ workspace,
3670
+ target,
3671
+ instruction_id: resp.instruction.id,
3672
+ version_id: resp.instruction.latest.id,
3673
+ path
3674
+ });
3675
+ log.ok(`Pulled ${scope} instruction to ${path}`);
3676
+ }
3677
+ async function instructionPushCommand(file, opts = {}) {
3678
+ const target = assertPublishInstructionTarget(opts.target ?? "default");
3679
+ const scope = resolveScope(opts);
3680
+ if (scope === "workspace") {
3681
+ const workspace = opts.workspace;
3682
+ const info = await api(`/libraries/${workspace}`, { authRequired: true });
3683
+ if (info.role !== "admin" && info.role !== "editor") {
3684
+ throw new Error(`Workspace instruction push requires editor or admin role in ${workspace}.`);
3685
+ }
3686
+ }
3687
+ const body = await readFile9(file, "utf8");
3688
+ const resp = await api("/instructions", {
3689
+ method: "POST",
3690
+ authRequired: true,
3691
+ body: {
3692
+ scope,
3693
+ workspace: opts.workspace,
3694
+ target,
3695
+ body_md: body,
3696
+ changelog: opts.changelog ?? `Published from CLI (${file}, ${bodySha256(body).slice(0, 12)})`
3697
+ }
3698
+ });
3699
+ log.ok(`Published ${scope} instruction ${resp.instruction?.latest?.version_seq ? `v${resp.instruction.latest.version_seq}` : ""}`.trim());
3700
+ }
3701
+
3702
+ // src/commands/workspace-config.ts
3703
+ async function assertWorkspaceExists(slug) {
3704
+ await api(`/libraries/${slug}`, { authRequired: true });
3705
+ }
3706
+ function parseOptions(opts) {
3707
+ return {
3708
+ target: assertTarget(opts.target ?? "codex"),
3709
+ scope: normalizeScope(opts.scope)
3710
+ };
3711
+ }
3712
+ async function defaultWorkspaceCommand(slug, opts = {}) {
3713
+ const scope = normalizeScope(opts.scope);
3714
+ if (!slug) {
3715
+ const config = await readFloomConfig(scope);
3716
+ if (!config.default_workspace) {
3717
+ log.info(`No default workspace configured for ${scope} scope.`);
3718
+ return;
3719
+ }
3720
+ console.log(config.default_workspace);
3721
+ return;
3722
+ }
3723
+ await assertWorkspaceExists(slug);
3724
+ await setDefaultWorkspace(slug, scope);
3725
+ log.ok(`Default workspace (${scope}) set to ${slug}`);
3726
+ }
3727
+ async function workspaceActivateCommand(slug, opts = {}) {
3728
+ const parsed = parseOptions(opts);
3729
+ await assertWorkspaceExists(slug);
3730
+ await setWorkspaceActive({ workspace: slug, target: parsed.target, scope: parsed.scope, active: true });
3731
+ await recordCliActivity({ event_type: "workspace.activate_cli", workspace: slug, target: parsed.target });
3732
+ log.ok(`Activated ${slug} for ${parsed.target} (${parsed.scope})`);
3733
+ }
3734
+ async function workspaceDeactivateCommand(slug, opts = {}) {
3735
+ const parsed = parseOptions(opts);
3736
+ await setWorkspaceActive({ workspace: slug, target: parsed.target, scope: parsed.scope, active: false });
3737
+ await recordCliActivity({ event_type: "workspace.deactivate_cli", workspace: slug, target: parsed.target });
3738
+ log.ok(`Deactivated ${slug} for ${parsed.target} (${parsed.scope})`);
3739
+ }
3740
+ async function workspaceActiveCommand(opts = {}) {
3741
+ const parsed = parseOptions(opts);
3742
+ const config = await readFloomConfig(parsed.scope);
3743
+ const active = config.targets[parsed.target]?.active_workspaces ?? [];
3744
+ if (!active.length) {
3745
+ log.info(`No active workspaces for ${parsed.target} (${parsed.scope}).`);
3746
+ return;
3747
+ }
3748
+ for (const workspace of active) console.log(workspace);
3749
+ }
3750
+
3751
+ // src/commands/sync.ts
3752
+ import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
3753
+ import { dirname as dirname3, join as join12 } from "node:path";
3754
+ var ROUTER_SKILL = [
3755
+ "# Floom Find Skills",
3756
+ "",
3757
+ "Use this router skill instead of loading every Floom skill into context.",
3758
+ "",
3759
+ "When a task may need a reusable skill:",
3760
+ "",
3761
+ "1. Call the Floom MCP `search_skills` tool with a short query.",
3762
+ "2. Review the compact candidate list.",
3763
+ "3. Call `get_skill` only for the selected candidate.",
3764
+ "4. Call `install_skill` only when the skill needs to be available locally.",
3765
+ "",
3766
+ "Do not enumerate the whole workspace library into model context. Keep skill bodies out of context until selected.",
3767
+ ""
3768
+ ].join("\n");
3769
+ async function installRouter(target) {
3770
+ const install = resolveInstallDir({ target });
3771
+ const routerDir = target === "kimi" ? "floom" : "floom-find-skills";
3772
+ const path = join12(install.dir, routerDir, "SKILL.md");
3773
+ await mkdir8(dirname3(path), { recursive: true });
3774
+ await writeFile6(path, ROUTER_SKILL, "utf8");
3775
+ return path;
3776
+ }
3777
+ async function pullInstructions(input) {
3778
+ const path = defaultInstructionPath(input.target);
3779
+ const sections = [];
3780
+ const account = await fetchInstruction({ scope: "account", target: input.target });
3781
+ if (account.instruction?.latest) {
3782
+ sections.push({
3783
+ label: "Account Instructions",
3784
+ scope: "account",
3785
+ instruction_id: account.instruction.id,
3786
+ version_id: account.instruction.latest.id,
3787
+ body_sha256: account.instruction.latest.body_sha256,
3788
+ body_md: account.instruction.latest.body_md
3789
+ });
3790
+ }
3791
+ for (const workspace of input.activeWorkspaces) {
3792
+ const resp = await fetchInstruction({ scope: "workspace", workspace, target: input.target });
3793
+ if (!resp.instruction?.latest) continue;
3794
+ sections.push({
3795
+ label: `Workspace Instructions: ${workspace}`,
3796
+ scope: "workspace",
3797
+ workspace,
3798
+ instruction_id: resp.instruction.id,
3799
+ version_id: resp.instruction.latest.id,
3800
+ body_sha256: resp.instruction.latest.body_sha256,
3801
+ body_md: resp.instruction.latest.body_md
3802
+ });
3803
+ }
3804
+ if (sections.length === 0) {
3805
+ log.info("No remote instructions found.");
3806
+ return;
3807
+ }
3808
+ const writeResult = await writeManagedInstructionFile({
3809
+ path,
3810
+ blockBody: buildInstructionBlock(sections.map((section) => ({ label: section.label, body_md: section.body_md }))),
3811
+ apply: input.apply,
3812
+ force: input.force
3813
+ });
3814
+ if (!writeResult.changed) {
3815
+ log.info(`${path} already contains the latest Floom instruction block.`);
3816
+ return;
3817
+ }
3818
+ if (writeResult.backupPath) log.info(`Backed up previous instruction file to ${writeResult.backupPath}`);
3819
+ for (const section of sections) {
3820
+ await upsertInstructionLock({
3821
+ scope: section.scope,
3822
+ workspace: section.workspace,
3823
+ target: input.target,
3824
+ path,
3825
+ version_id: section.version_id,
3826
+ body_sha256: section.body_sha256,
3827
+ configScope: input.scope,
3828
+ defaultWorkspace: input.defaultWorkspace
3829
+ });
3830
+ await recordCliActivity({
3831
+ event_type: "instruction.pulled_cli",
3832
+ scope: section.scope,
3833
+ workspace: section.workspace,
3834
+ target: input.target,
3835
+ instruction_id: section.instruction_id,
3836
+ version_id: section.version_id,
3837
+ path
3838
+ });
3839
+ }
3840
+ log.ok(`Pulled ${sections.length} instruction section(s) to ${path}`);
3841
+ }
3842
+ async function statusCommand(opts = {}) {
3843
+ const target = assertTarget(opts.target ?? "codex");
3844
+ const scope = normalizeScope(opts.scope);
3845
+ const config = await readFloomConfig(scope);
3846
+ const active = config.targets[target]?.active_workspaces ?? [];
3847
+ log.heading(`Floom status for ${target} (${scope})`);
3848
+ log.kv("Default workspace", config.default_workspace ?? "(none)");
3849
+ log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
3850
+ log.kv("Local pull policy", "router + pinned skills + instructions only");
3851
+ for (const workspace of active) {
3852
+ const pins = await api(`/libraries/${workspace}/pins`, {
3853
+ authRequired: true,
3854
+ query: { target }
3855
+ });
3856
+ log.blank();
3857
+ log.info(`${workspace}: ${pins.pins.length} pinned skill(s) for ${target}`);
3858
+ for (const pin of pins.pins) {
3859
+ log.kv("", `${pin.skill?.slug ?? pin.skill_id}${pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : ""}`);
3860
+ }
3861
+ }
3862
+ }
3863
+ async function pullCommand(opts = {}) {
3864
+ const target = assertTarget(opts.target ?? "codex");
3865
+ const scope = normalizeScope(opts.scope);
3866
+ const config = await readFloomConfig(scope);
3867
+ const active = config.targets[target]?.active_workspaces ?? [];
3868
+ const routerPath = await installRouter(target);
3869
+ log.ok(`Installed Floom router skill to ${routerPath}`);
3870
+ if (target === "kimi") {
3871
+ log.info("Kimi uses the Floom guide/router skill in V0; account and workspace instructions are not merged into Kimi context.");
3872
+ } else {
3873
+ await pullInstructions({
3874
+ target: assertAgentInstructionTarget(target),
3875
+ scope,
3876
+ defaultWorkspace: config.default_workspace,
3877
+ activeWorkspaces: active,
3878
+ apply: opts.apply,
3879
+ force: opts.force
3880
+ });
3881
+ }
3882
+ for (const workspace of active) {
3883
+ const pins = await api(`/libraries/${workspace}/pins`, {
3884
+ authRequired: true,
3885
+ query: { target }
3886
+ });
3887
+ for (const pin of pins.pins) {
3888
+ if (!pin.skill?.slug) continue;
3889
+ await installCommand(`${workspace}/${pin.skill.slug}`, { for: target, force: opts.force });
3890
+ }
3891
+ await recordCliActivity({
3892
+ event_type: "workspace.pulled_cli",
3893
+ workspace,
3894
+ target,
3895
+ resources: pins.pins.map((pin) => ({ type: "skill", id: pin.skill_id, name: pin.skill?.slug ?? pin.skill_id }))
3896
+ });
3897
+ }
3898
+ log.blank();
3899
+ log.ok("Pull complete.");
3900
+ }
3901
+
3902
+ // src/commands/pin.ts
3903
+ async function resolveWorkspace(opts) {
3904
+ if (opts.workspace) return opts.workspace;
3905
+ const config = await readFloomConfig("local");
3906
+ if (config.default_workspace) return config.default_workspace;
3907
+ throw new Error("Pin command requires --workspace <slug> or a configured default workspace.");
3908
+ }
3909
+ async function pinCommand(ref, opts = {}) {
3910
+ const target = assertTarget(opts.target ?? "codex");
3911
+ const workspace = await resolveWorkspace(opts);
3912
+ await api(`/libraries/${workspace}/pins`, {
3913
+ method: "POST",
3914
+ authRequired: true,
3915
+ body: { ref, target }
3916
+ });
3917
+ log.ok(`Pinned ${ref} for ${target} in ${workspace}`);
3918
+ }
3919
+ async function unpinCommand(ref, opts = {}) {
3920
+ const target = assertTarget(opts.target ?? "codex");
3921
+ const workspace = await resolveWorkspace(opts);
3922
+ await api(`/libraries/${workspace}/pins`, {
3923
+ method: "DELETE",
3924
+ authRequired: true,
3925
+ query: { ref, target }
3926
+ });
3927
+ log.ok(`Unpinned ${ref} for ${target} in ${workspace}`);
3928
+ }
3929
+
3409
3930
  // src/commands/mcp.ts
3410
- import { mkdtemp, mkdir as mkdir6, readdir as readdir4, readFile as readFile8, rename as rename3, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
3411
- import { join as join10 } from "node:path";
3931
+ import { mkdtemp, mkdir as mkdir9, readdir as readdir4, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
3932
+ import { join as join13 } from "node:path";
3412
3933
  import { tmpdir as tmpdir3 } from "node:os";
3413
- import { z as z2 } from "zod";
3934
+ import { z as z3 } from "zod";
3414
3935
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3415
3936
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3416
3937
  var API_TIMEOUT_MS = 2e4;
@@ -3479,11 +4000,11 @@ async function installViaApi(token, refText, target) {
3479
4000
  const bundle = await rawGet(dl.download.url);
3480
4001
  if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
3481
4002
  const install = resolveInstallDir({ target });
3482
- await mkdir6(install.dir, { recursive: true });
3483
- const dest = join10(install.dir, parsed.slug);
4003
+ await mkdir9(install.dir, { recursive: true });
4004
+ const dest = join13(install.dir, parsed.slug);
3484
4005
  const exists = await readdir4(dest).then(() => true).catch(() => false);
3485
4006
  if (exists) await rm3(dest, { recursive: true, force: true });
3486
- const temp = await mkdtemp(join10(tmpdir3(), `floom-mcp-${parsed.slug}-`));
4007
+ const temp = await mkdtemp(join13(tmpdir3(), `floom-mcp-${parsed.slug}-`));
3487
4008
  try {
3488
4009
  await extractBundle(bundle, temp);
3489
4010
  await rename3(temp, dest);
@@ -3506,7 +4027,7 @@ async function installViaApi(token, refText, target) {
3506
4027
  return { path: dest, version: dl.version, ref: info.ref ?? ref };
3507
4028
  }
3508
4029
  async function parseSkillBundle(bundle) {
3509
- const tmp = await mkdtemp(join10(tmpdir3(), "floom-mcp-read-"));
4030
+ const tmp = await mkdtemp(join13(tmpdir3(), "floom-mcp-read-"));
3510
4031
  try {
3511
4032
  await extractBundle(bundle, tmp);
3512
4033
  const files = [];
@@ -3514,16 +4035,16 @@ async function parseSkillBundle(bundle) {
3514
4035
  const entries = await readdir4(dir, { withFileTypes: true });
3515
4036
  for (const entry of entries) {
3516
4037
  const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
3517
- const full = join10(dir, entry.name);
4038
+ const full = join13(dir, entry.name);
3518
4039
  if (entry.isDirectory()) await walk2(full, nextRel);
3519
4040
  else files.push(nextRel);
3520
4041
  }
3521
4042
  };
3522
4043
  await walk2(tmp);
3523
4044
  const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
3524
- const skillMd = skillMdPath ? await readFile8(join10(tmp, skillMdPath), "utf8") : "";
4045
+ const skillMd = skillMdPath ? await readFile10(join13(tmp, skillMdPath), "utf8") : "";
3525
4046
  const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
3526
- const skillJson = skillJsonPath ? JSON.parse(await readFile8(join10(tmp, skillJsonPath), "utf8")) : null;
4047
+ const skillJson = skillJsonPath ? JSON.parse(await readFile10(join13(tmp, skillJsonPath), "utf8")) : null;
3527
4048
  return { files, skill_md: skillMd, skill_json: skillJson };
3528
4049
  } finally {
3529
4050
  await rm3(tmp, { recursive: true, force: true });
@@ -3531,13 +4052,13 @@ async function parseSkillBundle(bundle) {
3531
4052
  }
3532
4053
  async function mcpCommand() {
3533
4054
  const server = new McpServer({ name: "floom", version: VERSION });
3534
- server.tool("search_skills", { query: z2.string().min(1), workspace: z2.string().optional(), library: z2.string().optional() }, async ({ query, workspace, library }) => {
4055
+ server.tool("search_skills", { query: z3.string().min(1), workspace: z3.string().optional(), library: z3.string().optional() }, async ({ query, workspace, library }) => {
3535
4056
  const token = await resolveRequiredToken();
3536
4057
  const workspaceSlug = workspace ?? library;
3537
4058
  const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
3538
4059
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
3539
4060
  });
3540
- server.tool("get_skill", { ref: z2.string().min(3) }, async ({ ref }) => {
4061
+ server.tool("get_skill", { ref: z3.string().min(3) }, async ({ ref }) => {
3541
4062
  const token = await resolveOptionalToken();
3542
4063
  const parsed = parseSkillRef(ref);
3543
4064
  if (!parsed) throw new Error("Invalid ref. Expected @owner/slug or workspace/slug");
@@ -3554,7 +4075,55 @@ async function mcpCommand() {
3554
4075
  }
3555
4076
  server.tool("list_workspaces", {}, listWorkspaces);
3556
4077
  server.tool("list_libraries", {}, listWorkspaces);
3557
- server.tool("install_skill", { ref: z2.string().min(3), target: z2.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]) }, async ({ ref, target }) => {
4078
+ server.tool("get_floom_guide", {}, async () => {
4079
+ return {
4080
+ content: [{
4081
+ type: "text",
4082
+ text: JSON.stringify({
4083
+ guide: [
4084
+ "Use Floom to discover approved AI skills without loading every skill into context.",
4085
+ "Call search_skills when you need a skill that is not already installed.",
4086
+ "Call get_skill only after selecting a specific skill candidate.",
4087
+ "Call install_skill only after the user or task requires local installation.",
4088
+ "Use get_instruction for account/workspace guidance visible to this agent."
4089
+ ]
4090
+ })
4091
+ }]
4092
+ };
4093
+ });
4094
+ server.tool("get_instruction", {
4095
+ scope: z3.enum(["account", "workspace"]).default("account"),
4096
+ workspace: z3.string().optional(),
4097
+ target: z3.enum(["default", "claude", "codex", "cursor", "opencode"]).default("default")
4098
+ }, async ({ scope, workspace, target }) => {
4099
+ const token = await resolveRequiredToken();
4100
+ const result = await apiRequest(token, "/instructions", {
4101
+ scope,
4102
+ ...workspace ? { workspace } : {},
4103
+ target
4104
+ });
4105
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
4106
+ });
4107
+ server.tool("list_active_workspaces", {
4108
+ target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode"]).default("codex"),
4109
+ scope: z3.enum(["global", "local"]).default("local")
4110
+ }, async ({ target, scope }) => {
4111
+ const config = await readFloomConfig(normalizeScope(scope));
4112
+ const active = config.targets[assertTarget(target)]?.active_workspaces ?? [];
4113
+ return { content: [{ type: "text", text: JSON.stringify({ target, scope, active_workspaces: active }) }] };
4114
+ });
4115
+ server.tool("list_pinned_skills", {
4116
+ workspace: z3.string().optional(),
4117
+ target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode"]).default("codex")
4118
+ }, async ({ workspace, target }) => {
4119
+ const token = await resolveRequiredToken();
4120
+ const config = await readFloomConfig("local");
4121
+ const workspaceSlug = workspace ?? config.default_workspace;
4122
+ if (!workspaceSlug) throw new Error("workspace is required when no default workspace is configured");
4123
+ const result = await apiRequest(token, `/libraries/${workspaceSlug}/pins`, { target });
4124
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
4125
+ });
4126
+ server.tool("install_skill", { ref: z3.string().min(3), target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]) }, async ({ ref, target }) => {
3558
4127
  const token = await resolveOptionalToken();
3559
4128
  const installed = await installViaApi(token, ref, target);
3560
4129
  return { content: [{ type: "text", text: JSON.stringify(installed) }] };
@@ -3566,7 +4135,7 @@ async function mcpCommand() {
3566
4135
  // src/commands/doctor.ts
3567
4136
  import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm4 } from "node:fs/promises";
3568
4137
  import { tmpdir as tmpdir4 } from "node:os";
3569
- import { join as join11 } from "node:path";
4138
+ import { join as join14 } from "node:path";
3570
4139
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3571
4140
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3572
4141
  function textOf(result) {
@@ -3644,9 +4213,9 @@ async function doctorCommand(opts = {}) {
3644
4213
  emitDoctor(checks, opts.json);
3645
4214
  process.exit(1);
3646
4215
  }
3647
- const tmpHome = await mkdtemp2(join11(tmpdir4(), "floom-doctor-home-"));
3648
- const tmpSkills = await mkdtemp2(join11(tmpdir4(), "floom-doctor-skills-"));
3649
- const tmpProject = await mkdtemp2(join11(tmpdir4(), "floom-doctor-project-"));
4216
+ const tmpHome = await mkdtemp2(join14(tmpdir4(), "floom-doctor-home-"));
4217
+ const tmpSkills = await mkdtemp2(join14(tmpdir4(), "floom-doctor-skills-"));
4218
+ const tmpProject = await mkdtemp2(join14(tmpdir4(), "floom-doctor-project-"));
3650
4219
  const transport = new StdioClientTransport({
3651
4220
  command: process.execPath,
3652
4221
  args: [cliPath, "mcp"],
@@ -3730,16 +4299,28 @@ program.command("outdated").description("Show installed skills with newer versio
3730
4299
  program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
3731
4300
  program.command("list").description("List remote skills.").option("--mine", "Only your own skills (default)").option("--workspace <slug>", "Filter by workspace slug").option("--library <slug>", "Legacy alias for --workspace").option("--folder <uuid>", "Filter by folder id").option("--flat", "Print one ref per line").option("--query <q>", "Filter by query").action(listCommand);
3732
4301
  program.command("info <ref>").description("Show details for a remote skill.").action(infoCommand);
4302
+ program.command("status").description("Show local vs remote Floom workspace state").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((opts) => statusCommand(opts));
4303
+ program.command("pull").description("Pull account/workspace instructions for active workspaces").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").option("--apply", "Append managed block when the target file has no Floom block").option("--force", "Write without the first-apply guard").action((opts) => pullCommand(opts));
4304
+ program.command("pin <ref>").description("Pin a workspace skill for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").action((ref, opts) => pinCommand(ref, opts));
4305
+ program.command("unpin <ref>").description("Unpin a workspace skill for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").action((ref, opts) => unpinCommand(ref, opts));
3733
4306
  program.command("share <ref> <email>").description("Invite someone to a skill by email.").option("--role <role>", "viewer (default) or editor").action((ref, email, opts) => shareCommand(ref, email, opts));
3734
4307
  program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
4308
+ var configCmd = program.command("config").description("Manage local Floom configuration");
4309
+ configCmd.command("default-workspace [slug]").description("Show or set the default workspace").option("--scope <scope>", "global | local", "local").action((slug, opts) => defaultWorkspaceCommand(slug, opts));
3735
4310
  function addWorkspaceCommands(cmd) {
3736
4311
  cmd.command("list").action(libraryListCommand);
3737
4312
  cmd.command("create <slug> <name>").action((slug, name) => libraryCreateCommand(slug, name));
3738
4313
  cmd.command("invite <workspaceSlug> <email>").option("--role <role>", "viewer|editor|admin", "viewer").action((workspaceSlug, email, opts) => libraryInviteCommand(workspaceSlug, email, opts.role));
3739
4314
  cmd.command("leave <workspaceSlug>").action((workspaceSlug) => libraryLeaveCommand(workspaceSlug));
4315
+ cmd.command("activate <workspaceSlug>").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((workspaceSlug, opts) => workspaceActivateCommand(workspaceSlug, opts));
4316
+ cmd.command("deactivate <workspaceSlug>").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((workspaceSlug, opts) => workspaceDeactivateCommand(workspaceSlug, opts));
4317
+ cmd.command("active").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((opts) => workspaceActiveCommand(opts));
3740
4318
  }
3741
4319
  addWorkspaceCommands(program.command("workspace").description("Manage workspaces"));
3742
4320
  addWorkspaceCommands(program.command("library").description("Manage workspaces (legacy alias)"));
4321
+ var instructionCmd = program.command("instruction").description("Manage account and workspace instructions");
4322
+ instructionCmd.command("pull").description("Pull an account or workspace instruction into a managed local block").option("--account", "Pull account instruction").option("--workspace <slug>", "Pull workspace instruction").option("--target <target>", "default | claude | codex | cursor | opencode", "codex").option("--scope <scope>", "global | local", "local").option("--path <path>", "Instruction file path override").option("--apply", "Append managed block when the target file has no Floom block").option("--force", "Write without the first-apply guard").action((opts) => instructionPullCommand(opts));
4323
+ instructionCmd.command("push <file>").description("Publish an account or workspace instruction from a markdown file").option("--account", "Publish account instruction").option("--workspace <slug>", "Publish workspace instruction").option("--target <target>", "default | claude | codex | cursor | opencode", "default").option("--changelog <text>", "Version changelog").action((file, opts) => instructionPushCommand(file, opts));
3743
4324
  program.command("doctor").description("Check local Floom CLI, auth, and fresh-agent MCP installability.").option("--fresh-agent", "Run MCP checks with a clean HOME and temp skills directory").option("--ref <ref>", "Skill ref to install during --fresh-agent, e.g. @depontefede/pdf").option("--target <target>", "Install target for --fresh-agent", "codex").option("--query <query>", "Search query for --fresh-agent", "pdf").option("--json", "Emit machine-readable JSON").action((opts) => doctorCommand(opts));
3744
4325
  program.command("mcp").description("Run local MCP server over stdio.").action(mcpCommand);
3745
4326
  async function main() {