@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 +605 -24
- package/dist/index.js.map +4 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
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:
|
|
1911
|
-
const { join:
|
|
1910
|
+
const { readFile: readFile11 } = await import("node:fs/promises");
|
|
1911
|
+
const { join: join15 } = await import("node:path");
|
|
1912
1912
|
try {
|
|
1913
|
-
const raw = await
|
|
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(
|
|
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.
|
|
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
|
|
3411
|
-
import { join as
|
|
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
|
|
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
|
|
3483
|
-
const dest =
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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("
|
|
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
|
|
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(
|
|
3648
|
-
const tmpSkills = await mkdtemp2(
|
|
3649
|
-
const tmpProject = await mkdtemp2(
|
|
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() {
|