@floomhq/skills 0.2.7 → 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 +614 -31
- package/dist/index.js.map +4 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
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;
|
|
@@ -2876,7 +2877,8 @@ import { readFile as readFile6 } from "node:fs/promises";
|
|
|
2876
2877
|
import { join as join6 } from "node:path";
|
|
2877
2878
|
async function publishCommand(opts = {}) {
|
|
2878
2879
|
const auth = await readAuth();
|
|
2879
|
-
|
|
2880
|
+
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
2881
|
+
if (!auth && !envToken && !opts.dryRun) {
|
|
2880
2882
|
log.err("Not logged in. Run: floom login");
|
|
2881
2883
|
process.exit(1);
|
|
2882
2884
|
}
|
|
@@ -2896,7 +2898,9 @@ async function publishCommand(opts = {}) {
|
|
|
2896
2898
|
const manifest = m.manifest;
|
|
2897
2899
|
const skillMd = await readFile6(join6(dir, "SKILL.md"), "utf8");
|
|
2898
2900
|
const { meta } = parseSkillFrontmatter(skillMd);
|
|
2899
|
-
const
|
|
2901
|
+
const me = !opts.dryRun && !auth && envToken ? await api("/me", { authRequired: true }) : null;
|
|
2902
|
+
const handle = auth?.handle ?? me?.user.handle;
|
|
2903
|
+
const refRoot = opts.workspace ?? opts.library ?? handle ?? "<your-handle>";
|
|
2900
2904
|
log.heading(`${opts.dryRun ? "Dry-running" : "Publishing"} ${refRoot}/${manifest.name}@${manifest.version}`);
|
|
2901
2905
|
log.step("Packing bundle...");
|
|
2902
2906
|
const bundle = await collectBundle(dir);
|
|
@@ -2909,8 +2913,7 @@ async function publishCommand(opts = {}) {
|
|
|
2909
2913
|
log.ok("Dry run passed. No upload was requested.");
|
|
2910
2914
|
return;
|
|
2911
2915
|
}
|
|
2912
|
-
|
|
2913
|
-
if (!activeAuth) {
|
|
2916
|
+
if (!handle) {
|
|
2914
2917
|
log.err("Not logged in. Run: floom login");
|
|
2915
2918
|
process.exit(1);
|
|
2916
2919
|
}
|
|
@@ -2937,7 +2940,7 @@ async function publishCommand(opts = {}) {
|
|
|
2937
2940
|
});
|
|
2938
2941
|
} catch (e) {
|
|
2939
2942
|
if (e instanceof FloomError && e.code === "VERSION_ALREADY_EXISTS") {
|
|
2940
|
-
log.err(`Version ${manifest.version} already exists for @${
|
|
2943
|
+
log.err(`Version ${manifest.version} already exists for @${handle}/${manifest.name}.`);
|
|
2941
2944
|
log.info("Bump the version in skill.json (e.g. 0.1.1) and try again.");
|
|
2942
2945
|
process.exit(1);
|
|
2943
2946
|
}
|
|
@@ -2955,7 +2958,7 @@ async function publishCommand(opts = {}) {
|
|
|
2955
2958
|
log.ok(`Published ${complete.ref}`);
|
|
2956
2959
|
log.blank();
|
|
2957
2960
|
log.info("View:");
|
|
2958
|
-
log.kv("", `${
|
|
2961
|
+
log.kv("", `${(auth?.apiUrl ?? process.env.FLOOM_API_URL ?? "https://skills.floom.dev/api/v1").replace("/api/v1", "")}/@${handle}/${manifest.name}`);
|
|
2959
2962
|
log.info("Install:");
|
|
2960
2963
|
log.kv("", complete.install_command);
|
|
2961
2964
|
}
|
|
@@ -2966,7 +2969,7 @@ import { join as join8 } from "node:path";
|
|
|
2966
2969
|
import { tmpdir } from "node:os";
|
|
2967
2970
|
|
|
2968
2971
|
// src/lib/floom-lock.ts
|
|
2969
|
-
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";
|
|
2970
2973
|
import { join as join7, relative as relative2, sep as sep2, posix as posix2 } from "node:path";
|
|
2971
2974
|
import { createHash as createHash3 } from "node:crypto";
|
|
2972
2975
|
var EMPTY = { schema_version: "0.1", skills: {} };
|
|
@@ -2975,6 +2978,7 @@ async function readLock(projectDir) {
|
|
|
2975
2978
|
const raw = await readFile7(join7(projectDir, "floom.lock"), "utf8");
|
|
2976
2979
|
const parsed = JSON.parse(raw);
|
|
2977
2980
|
if (parsed.schema_version === "0.1") return parsed;
|
|
2981
|
+
if (parsed.schema_version === "0.2") return parsed;
|
|
2978
2982
|
const version = typeof parsed.schema_version === "string" ? parsed.schema_version : "missing";
|
|
2979
2983
|
throw new Error(
|
|
2980
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.`
|
|
@@ -2985,11 +2989,32 @@ async function readLock(projectDir) {
|
|
|
2985
2989
|
}
|
|
2986
2990
|
}
|
|
2987
2991
|
async function writeLock(projectDir, lock) {
|
|
2992
|
+
await mkdir3(projectDir, { recursive: true });
|
|
2988
2993
|
await writeFile3(join7(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
2989
2994
|
}
|
|
2990
2995
|
function setLockEntry(lock, ref, entry) {
|
|
2991
2996
|
return { ...lock, skills: { ...lock.skills, [ref]: entry } };
|
|
2992
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
|
+
}
|
|
2993
3018
|
async function hashInstalledFolder(folderAbs) {
|
|
2994
3019
|
const out = [];
|
|
2995
3020
|
async function walk2(dir) {
|
|
@@ -3358,7 +3383,7 @@ async function shareCommand(refStr, email, opts = {}) {
|
|
|
3358
3383
|
body: { email, role }
|
|
3359
3384
|
});
|
|
3360
3385
|
log.ok(`Shared ${refStr} with ${r.grant.email} as ${r.grant.role}.`);
|
|
3361
|
-
log.
|
|
3386
|
+
if (r.email_status) log.kv("email", r.email_status);
|
|
3362
3387
|
}
|
|
3363
3388
|
async function unshareCommand(refStr, email) {
|
|
3364
3389
|
const ref = parseSkillRef(refStr);
|
|
@@ -3404,11 +3429,509 @@ async function libraryLeaveCommand(librarySlug) {
|
|
|
3404
3429
|
log.ok(`Left workspace ${librarySlug}`);
|
|
3405
3430
|
}
|
|
3406
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
|
+
|
|
3407
3930
|
// src/commands/mcp.ts
|
|
3408
|
-
import { mkdtemp, mkdir as
|
|
3409
|
-
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";
|
|
3410
3933
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
3411
|
-
import { z as
|
|
3934
|
+
import { z as z3 } from "zod";
|
|
3412
3935
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3413
3936
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3414
3937
|
var API_TIMEOUT_MS = 2e4;
|
|
@@ -3477,11 +4000,11 @@ async function installViaApi(token, refText, target) {
|
|
|
3477
4000
|
const bundle = await rawGet(dl.download.url);
|
|
3478
4001
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
3479
4002
|
const install = resolveInstallDir({ target });
|
|
3480
|
-
await
|
|
3481
|
-
const dest =
|
|
4003
|
+
await mkdir9(install.dir, { recursive: true });
|
|
4004
|
+
const dest = join13(install.dir, parsed.slug);
|
|
3482
4005
|
const exists = await readdir4(dest).then(() => true).catch(() => false);
|
|
3483
4006
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
3484
|
-
const temp = await mkdtemp(
|
|
4007
|
+
const temp = await mkdtemp(join13(tmpdir3(), `floom-mcp-${parsed.slug}-`));
|
|
3485
4008
|
try {
|
|
3486
4009
|
await extractBundle(bundle, temp);
|
|
3487
4010
|
await rename3(temp, dest);
|
|
@@ -3504,7 +4027,7 @@ async function installViaApi(token, refText, target) {
|
|
|
3504
4027
|
return { path: dest, version: dl.version, ref: info.ref ?? ref };
|
|
3505
4028
|
}
|
|
3506
4029
|
async function parseSkillBundle(bundle) {
|
|
3507
|
-
const tmp = await mkdtemp(
|
|
4030
|
+
const tmp = await mkdtemp(join13(tmpdir3(), "floom-mcp-read-"));
|
|
3508
4031
|
try {
|
|
3509
4032
|
await extractBundle(bundle, tmp);
|
|
3510
4033
|
const files = [];
|
|
@@ -3512,16 +4035,16 @@ async function parseSkillBundle(bundle) {
|
|
|
3512
4035
|
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3513
4036
|
for (const entry of entries) {
|
|
3514
4037
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
3515
|
-
const full =
|
|
4038
|
+
const full = join13(dir, entry.name);
|
|
3516
4039
|
if (entry.isDirectory()) await walk2(full, nextRel);
|
|
3517
4040
|
else files.push(nextRel);
|
|
3518
4041
|
}
|
|
3519
4042
|
};
|
|
3520
4043
|
await walk2(tmp);
|
|
3521
4044
|
const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
|
|
3522
|
-
const skillMd = skillMdPath ? await
|
|
4045
|
+
const skillMd = skillMdPath ? await readFile10(join13(tmp, skillMdPath), "utf8") : "";
|
|
3523
4046
|
const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
|
|
3524
|
-
const skillJson = skillJsonPath ? JSON.parse(await
|
|
4047
|
+
const skillJson = skillJsonPath ? JSON.parse(await readFile10(join13(tmp, skillJsonPath), "utf8")) : null;
|
|
3525
4048
|
return { files, skill_md: skillMd, skill_json: skillJson };
|
|
3526
4049
|
} finally {
|
|
3527
4050
|
await rm3(tmp, { recursive: true, force: true });
|
|
@@ -3529,13 +4052,13 @@ async function parseSkillBundle(bundle) {
|
|
|
3529
4052
|
}
|
|
3530
4053
|
async function mcpCommand() {
|
|
3531
4054
|
const server = new McpServer({ name: "floom", version: VERSION });
|
|
3532
|
-
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 }) => {
|
|
3533
4056
|
const token = await resolveRequiredToken();
|
|
3534
4057
|
const workspaceSlug = workspace ?? library;
|
|
3535
4058
|
const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
|
|
3536
4059
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3537
4060
|
});
|
|
3538
|
-
server.tool("get_skill", { ref:
|
|
4061
|
+
server.tool("get_skill", { ref: z3.string().min(3) }, async ({ ref }) => {
|
|
3539
4062
|
const token = await resolveOptionalToken();
|
|
3540
4063
|
const parsed = parseSkillRef(ref);
|
|
3541
4064
|
if (!parsed) throw new Error("Invalid ref. Expected @owner/slug or workspace/slug");
|
|
@@ -3552,7 +4075,55 @@ async function mcpCommand() {
|
|
|
3552
4075
|
}
|
|
3553
4076
|
server.tool("list_workspaces", {}, listWorkspaces);
|
|
3554
4077
|
server.tool("list_libraries", {}, listWorkspaces);
|
|
3555
|
-
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 }) => {
|
|
3556
4127
|
const token = await resolveOptionalToken();
|
|
3557
4128
|
const installed = await installViaApi(token, ref, target);
|
|
3558
4129
|
return { content: [{ type: "text", text: JSON.stringify(installed) }] };
|
|
@@ -3564,7 +4135,7 @@ async function mcpCommand() {
|
|
|
3564
4135
|
// src/commands/doctor.ts
|
|
3565
4136
|
import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm4 } from "node:fs/promises";
|
|
3566
4137
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
3567
|
-
import { join as
|
|
4138
|
+
import { join as join14 } from "node:path";
|
|
3568
4139
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3569
4140
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3570
4141
|
function textOf(result) {
|
|
@@ -3642,9 +4213,9 @@ async function doctorCommand(opts = {}) {
|
|
|
3642
4213
|
emitDoctor(checks, opts.json);
|
|
3643
4214
|
process.exit(1);
|
|
3644
4215
|
}
|
|
3645
|
-
const tmpHome = await mkdtemp2(
|
|
3646
|
-
const tmpSkills = await mkdtemp2(
|
|
3647
|
-
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-"));
|
|
3648
4219
|
const transport = new StdioClientTransport({
|
|
3649
4220
|
command: process.execPath,
|
|
3650
4221
|
args: [cliPath, "mcp"],
|
|
@@ -3728,16 +4299,28 @@ program.command("outdated").description("Show installed skills with newer versio
|
|
|
3728
4299
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
3729
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);
|
|
3730
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));
|
|
3731
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));
|
|
3732
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));
|
|
3733
4310
|
function addWorkspaceCommands(cmd) {
|
|
3734
4311
|
cmd.command("list").action(libraryListCommand);
|
|
3735
4312
|
cmd.command("create <slug> <name>").action((slug, name) => libraryCreateCommand(slug, name));
|
|
3736
4313
|
cmd.command("invite <workspaceSlug> <email>").option("--role <role>", "viewer|editor|admin", "viewer").action((workspaceSlug, email, opts) => libraryInviteCommand(workspaceSlug, email, opts.role));
|
|
3737
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));
|
|
3738
4318
|
}
|
|
3739
4319
|
addWorkspaceCommands(program.command("workspace").description("Manage workspaces"));
|
|
3740
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));
|
|
3741
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));
|
|
3742
4325
|
program.command("mcp").description("Run local MCP server over stdio.").action(mcpCommand);
|
|
3743
4326
|
async function main() {
|