@floomhq/skills 0.2.8 → 0.2.10
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 +709 -52
- 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);
|
|
@@ -2014,6 +2014,19 @@ var scrypt = promisify(scryptCb);
|
|
|
2014
2014
|
// ../shared/src/install-targets.ts
|
|
2015
2015
|
import { homedir } from "node:os";
|
|
2016
2016
|
import { join } from "node:path";
|
|
2017
|
+
var INSTALL_TARGETS = [
|
|
2018
|
+
"generic",
|
|
2019
|
+
"all",
|
|
2020
|
+
"claude",
|
|
2021
|
+
"codex",
|
|
2022
|
+
"cursor",
|
|
2023
|
+
"gemini",
|
|
2024
|
+
"opencode",
|
|
2025
|
+
"kimi"
|
|
2026
|
+
];
|
|
2027
|
+
function isInstallTarget(value) {
|
|
2028
|
+
return !!value && INSTALL_TARGETS.includes(value);
|
|
2029
|
+
}
|
|
2017
2030
|
var COMPATIBLE_AGENTS = {
|
|
2018
2031
|
generic: ["Claude Code", "Codex CLI", "Cursor", "Gemini CLI", "OpenCode", "Kimi CLI"],
|
|
2019
2032
|
all: ["Claude Code", "Codex CLI", "Cursor", "Gemini CLI", "OpenCode", "Kimi CLI"],
|
|
@@ -2049,35 +2062,36 @@ function presetDir(target, opts) {
|
|
|
2049
2062
|
return join(root, ".claude", "skills");
|
|
2050
2063
|
case "gemini":
|
|
2051
2064
|
return join(root, ".gemini", "skills");
|
|
2065
|
+
case "kimi":
|
|
2066
|
+
return join(root, ".kimi", "skills");
|
|
2052
2067
|
case "codex":
|
|
2053
2068
|
case "cursor":
|
|
2054
2069
|
case "generic":
|
|
2055
2070
|
case "all":
|
|
2056
2071
|
case "opencode":
|
|
2057
|
-
case "kimi":
|
|
2058
2072
|
default:
|
|
2059
2073
|
return join(root, ".agents", "skills");
|
|
2060
2074
|
}
|
|
2061
2075
|
}
|
|
2062
2076
|
function resolveInstallDir(args) {
|
|
2077
|
+
const target = args.target ?? "generic";
|
|
2063
2078
|
if (args.to) {
|
|
2064
2079
|
return {
|
|
2065
|
-
target
|
|
2080
|
+
target,
|
|
2066
2081
|
dir: args.to,
|
|
2067
2082
|
origin: "explicit",
|
|
2068
|
-
compatibleAgents: COMPATIBLE_AGENTS[
|
|
2083
|
+
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2069
2084
|
};
|
|
2070
2085
|
}
|
|
2071
|
-
const targetEnvDir = envDirForTarget(
|
|
2086
|
+
const targetEnvDir = envDirForTarget(target);
|
|
2072
2087
|
if (targetEnvDir) {
|
|
2073
2088
|
return {
|
|
2074
|
-
target
|
|
2089
|
+
target,
|
|
2075
2090
|
dir: targetEnvDir,
|
|
2076
2091
|
origin: "env",
|
|
2077
|
-
compatibleAgents: COMPATIBLE_AGENTS[
|
|
2092
|
+
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2078
2093
|
};
|
|
2079
2094
|
}
|
|
2080
|
-
const target = args.target ?? "generic";
|
|
2081
2095
|
return {
|
|
2082
2096
|
target,
|
|
2083
2097
|
dir: presetDir(target, { global: args.global, cwd: args.cwd }),
|
|
@@ -2364,7 +2378,7 @@ var log = {
|
|
|
2364
2378
|
err: (msg) => console.error(chalk.red("\u2717 ") + msg),
|
|
2365
2379
|
step: (msg) => console.log(chalk.dim("\xB7 ") + msg),
|
|
2366
2380
|
heading: (msg) => console.log("\n" + chalk.bold(msg)),
|
|
2367
|
-
kv: (key, value) => console.log(` ${chalk.dim(key.padEnd(
|
|
2381
|
+
kv: (key, value) => console.log(` ${chalk.dim(key.padEnd(18))}${value}`),
|
|
2368
2382
|
blank: () => console.log("")
|
|
2369
2383
|
};
|
|
2370
2384
|
|
|
@@ -2380,7 +2394,8 @@ var CONFIG_DIR = join3(homedir2(), ".floom");
|
|
|
2380
2394
|
var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
|
|
2381
2395
|
var DEFAULT_APP_URL = "https://skills.floom.dev";
|
|
2382
2396
|
var DEFAULT_API_URL = "https://skills.floom.dev/api/v1";
|
|
2383
|
-
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app"]);
|
|
2397
|
+
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app", "skills.wasm.floom.dev"]);
|
|
2398
|
+
var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["skills.floom.dev", "skills.wasm.floom.dev", "localhost", "127.0.0.1", "::1"]);
|
|
2384
2399
|
async function ensureDir() {
|
|
2385
2400
|
await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
2386
2401
|
}
|
|
@@ -2394,12 +2409,15 @@ async function readRawAuth() {
|
|
|
2394
2409
|
return JSON.parse(raw);
|
|
2395
2410
|
} catch (e) {
|
|
2396
2411
|
if (e.code === "ENOENT") return null;
|
|
2412
|
+
if (e instanceof SyntaxError) {
|
|
2413
|
+
throw new FloomError("AUTH_REQUIRED", "Invalid ~/.floom/auth.json. Run: floom login to refresh local auth.");
|
|
2414
|
+
}
|
|
2397
2415
|
throw e;
|
|
2398
2416
|
}
|
|
2399
2417
|
}
|
|
2400
2418
|
async function writeAuth(state) {
|
|
2401
2419
|
await ensureDir();
|
|
2402
|
-
await writeFile(AUTH_FILE, JSON.stringify({ ...state, apiUrl:
|
|
2420
|
+
await writeFile(AUTH_FILE, JSON.stringify({ ...state, apiUrl: trustedApiUrlOrDefault(state.apiUrl) }, null, 2), { mode: 384 });
|
|
2403
2421
|
try {
|
|
2404
2422
|
await chmod(AUTH_FILE, 384);
|
|
2405
2423
|
} catch {
|
|
@@ -2422,14 +2440,16 @@ function getAppUrl() {
|
|
|
2422
2440
|
}
|
|
2423
2441
|
function getApiBaseUrls(preferred) {
|
|
2424
2442
|
const explicitApiUrl = process.env.FLOOM_API_URL?.trim();
|
|
2425
|
-
if (explicitApiUrl) return [
|
|
2426
|
-
const primary =
|
|
2443
|
+
if (explicitApiUrl) return [trustedApiUrlOrDefault(explicitApiUrl)];
|
|
2444
|
+
const primary = trustedApiUrlOrDefault(preferred ?? getApiUrl());
|
|
2427
2445
|
const bases = [primary];
|
|
2428
2446
|
if (preferred && !process.env.FLOOM_APP_URL) bases.push(DEFAULT_API_URL);
|
|
2429
2447
|
return Array.from(new Set(bases));
|
|
2430
2448
|
}
|
|
2431
2449
|
function normalizeApiUrl(apiUrl) {
|
|
2432
|
-
|
|
2450
|
+
if (typeof apiUrl !== "string") return DEFAULT_API_URL;
|
|
2451
|
+
const trimmed = apiUrl.trim().replace(/\/$/, "");
|
|
2452
|
+
if (!trimmed) return DEFAULT_API_URL;
|
|
2433
2453
|
try {
|
|
2434
2454
|
const url = new URL(trimmed);
|
|
2435
2455
|
if (LEGACY_API_HOSTS.has(url.hostname)) return DEFAULT_API_URL;
|
|
@@ -2438,6 +2458,22 @@ function normalizeApiUrl(apiUrl) {
|
|
|
2438
2458
|
}
|
|
2439
2459
|
return trimmed;
|
|
2440
2460
|
}
|
|
2461
|
+
function allowsCustomApiUrl() {
|
|
2462
|
+
const raw = process.env.FLOOM_ALLOW_CUSTOM_API_URL?.trim().toLowerCase();
|
|
2463
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
2464
|
+
}
|
|
2465
|
+
function isTrustedApiUrl(apiUrl) {
|
|
2466
|
+
try {
|
|
2467
|
+
const url = new URL(apiUrl);
|
|
2468
|
+
return TRUSTED_API_HOSTS.has(url.hostname) || allowsCustomApiUrl();
|
|
2469
|
+
} catch {
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
function trustedApiUrlOrDefault(apiUrl) {
|
|
2474
|
+
const normalized = normalizeApiUrl(apiUrl);
|
|
2475
|
+
return isTrustedApiUrl(normalized) ? normalized : DEFAULT_API_URL;
|
|
2476
|
+
}
|
|
2441
2477
|
function isLegacyApiUrl(apiUrl) {
|
|
2442
2478
|
if (!apiUrl) return false;
|
|
2443
2479
|
try {
|
|
@@ -2448,7 +2484,7 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2448
2484
|
}
|
|
2449
2485
|
|
|
2450
2486
|
// src/version.ts
|
|
2451
|
-
var VERSION = "0.2.
|
|
2487
|
+
var VERSION = "0.2.10";
|
|
2452
2488
|
|
|
2453
2489
|
// src/api-client.ts
|
|
2454
2490
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2490,7 +2526,8 @@ async function api(path, opts = {}) {
|
|
|
2490
2526
|
"User-Agent": `floom-cli/${VERSION}`,
|
|
2491
2527
|
"x-floom-cli-version": VERSION
|
|
2492
2528
|
};
|
|
2493
|
-
|
|
2529
|
+
const requestToken = opts.tokenOverride ?? token;
|
|
2530
|
+
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
|
|
2494
2531
|
let res;
|
|
2495
2532
|
try {
|
|
2496
2533
|
res = await fetchWithTimeout(url.toString(), {
|
|
@@ -2587,8 +2624,9 @@ async function loginCommand() {
|
|
|
2587
2624
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2588
2625
|
while (Date.now() < deadline) {
|
|
2589
2626
|
await new Promise((r) => setTimeout(r, interval));
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2627
|
+
const poll = await api(`/cli/sessions/${session.session_id}`, {
|
|
2628
|
+
tokenOverride: session.device_code
|
|
2629
|
+
});
|
|
2592
2630
|
if (poll.status === "approved" && poll.token && poll.handle && poll.email) {
|
|
2593
2631
|
await writeAuth({
|
|
2594
2632
|
token: poll.token,
|
|
@@ -2957,7 +2995,8 @@ async function publishCommand(opts = {}) {
|
|
|
2957
2995
|
log.ok(`Published ${complete.ref}`);
|
|
2958
2996
|
log.blank();
|
|
2959
2997
|
log.info("View:");
|
|
2960
|
-
|
|
2998
|
+
const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
|
|
2999
|
+
log.kv("", `${displayApiUrl.replace("/api/v1", "")}/@${handle}/${manifest.name}`);
|
|
2961
3000
|
log.info("Install:");
|
|
2962
3001
|
log.kv("", complete.install_command);
|
|
2963
3002
|
}
|
|
@@ -2968,7 +3007,7 @@ import { join as join8 } from "node:path";
|
|
|
2968
3007
|
import { tmpdir } from "node:os";
|
|
2969
3008
|
|
|
2970
3009
|
// src/lib/floom-lock.ts
|
|
2971
|
-
import { readFile as readFile7, writeFile as writeFile3, stat as stat3, readdir as readdir2 } from "node:fs/promises";
|
|
3010
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as stat3, readdir as readdir2 } from "node:fs/promises";
|
|
2972
3011
|
import { join as join7, relative as relative2, sep as sep2, posix as posix2 } from "node:path";
|
|
2973
3012
|
import { createHash as createHash3 } from "node:crypto";
|
|
2974
3013
|
var EMPTY = { schema_version: "0.1", skills: {} };
|
|
@@ -2977,6 +3016,7 @@ async function readLock(projectDir) {
|
|
|
2977
3016
|
const raw = await readFile7(join7(projectDir, "floom.lock"), "utf8");
|
|
2978
3017
|
const parsed = JSON.parse(raw);
|
|
2979
3018
|
if (parsed.schema_version === "0.1") return parsed;
|
|
3019
|
+
if (parsed.schema_version === "0.2") return parsed;
|
|
2980
3020
|
const version = typeof parsed.schema_version === "string" ? parsed.schema_version : "missing";
|
|
2981
3021
|
throw new Error(
|
|
2982
3022
|
`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 +3027,32 @@ async function readLock(projectDir) {
|
|
|
2987
3027
|
}
|
|
2988
3028
|
}
|
|
2989
3029
|
async function writeLock(projectDir, lock) {
|
|
3030
|
+
await mkdir3(projectDir, { recursive: true });
|
|
2990
3031
|
await writeFile3(join7(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
2991
3032
|
}
|
|
2992
3033
|
function setLockEntry(lock, ref, entry) {
|
|
2993
3034
|
return { ...lock, skills: { ...lock.skills, [ref]: entry } };
|
|
2994
3035
|
}
|
|
3036
|
+
function upgradeLockToV02(lock, opts = {}) {
|
|
3037
|
+
if (lock.schema_version === "0.2") {
|
|
3038
|
+
return {
|
|
3039
|
+
...lock,
|
|
3040
|
+
target: opts.target ?? lock.target,
|
|
3041
|
+
scope: opts.scope ?? lock.scope,
|
|
3042
|
+
default_workspace: opts.defaultWorkspace ?? lock.default_workspace,
|
|
3043
|
+
last_sync_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
return {
|
|
3047
|
+
schema_version: "0.2",
|
|
3048
|
+
default_workspace: opts.defaultWorkspace,
|
|
3049
|
+
target: opts.target,
|
|
3050
|
+
scope: opts.scope,
|
|
3051
|
+
last_sync_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3052
|
+
instructions: [],
|
|
3053
|
+
skills: lock.skills
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
2995
3056
|
async function hashInstalledFolder(folderAbs) {
|
|
2996
3057
|
const out = [];
|
|
2997
3058
|
async function walk2(dir) {
|
|
@@ -3046,6 +3107,12 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3046
3107
|
log.info("Expected: @owner/slug, workspace-slug/slug, or with @version suffix");
|
|
3047
3108
|
process.exit(1);
|
|
3048
3109
|
}
|
|
3110
|
+
if (opts.for && !isInstallTarget(opts.for)) {
|
|
3111
|
+
log.err(`Invalid install target: ${opts.for}`);
|
|
3112
|
+
log.info("Expected: claude | codex | cursor | gemini | opencode | kimi | all");
|
|
3113
|
+
process.exit(1);
|
|
3114
|
+
}
|
|
3115
|
+
const installTarget = isInstallTarget(opts.for) ? opts.for : "generic";
|
|
3049
3116
|
let info;
|
|
3050
3117
|
try {
|
|
3051
3118
|
info = await api(`/skills/${ref.owner}/${ref.slug}`);
|
|
@@ -3070,7 +3137,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3070
3137
|
process.exit(1);
|
|
3071
3138
|
}
|
|
3072
3139
|
const target = resolveInstallDir({
|
|
3073
|
-
target:
|
|
3140
|
+
target: installTarget,
|
|
3074
3141
|
to: opts.to,
|
|
3075
3142
|
global: opts.global
|
|
3076
3143
|
});
|
|
@@ -3131,7 +3198,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3131
3198
|
bundle_sha256: dl.bundle_sha256,
|
|
3132
3199
|
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3133
3200
|
path: destFolder.replace(projectDir + "/", ""),
|
|
3134
|
-
preset:
|
|
3201
|
+
preset: installTarget
|
|
3135
3202
|
});
|
|
3136
3203
|
await writeLock(projectDir, next);
|
|
3137
3204
|
log.blank();
|
|
@@ -3406,14 +3473,521 @@ async function libraryLeaveCommand(librarySlug) {
|
|
|
3406
3473
|
log.ok(`Left workspace ${librarySlug}`);
|
|
3407
3474
|
}
|
|
3408
3475
|
|
|
3476
|
+
// src/lib/config-file.ts
|
|
3477
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
3478
|
+
import { homedir as homedir3 } from "node:os";
|
|
3479
|
+
import { dirname, join as join10 } from "node:path";
|
|
3480
|
+
import { z as z2 } from "zod";
|
|
3481
|
+
var FLOOM_CONFIG_VERSION = "0.1";
|
|
3482
|
+
var TARGETS = ["claude", "codex", "cursor", "kimi", "opencode"];
|
|
3483
|
+
var targetConfigSchema = z2.object({
|
|
3484
|
+
active_workspaces: z2.array(z2.string().min(1)).default([]),
|
|
3485
|
+
instruction_path: z2.string().min(1).optional()
|
|
3486
|
+
});
|
|
3487
|
+
var configSchema = z2.object({
|
|
3488
|
+
version: z2.literal(FLOOM_CONFIG_VERSION).default(FLOOM_CONFIG_VERSION),
|
|
3489
|
+
default_workspace: z2.string().min(1).optional(),
|
|
3490
|
+
targets: z2.record(z2.enum(TARGETS), targetConfigSchema).default({})
|
|
3491
|
+
});
|
|
3492
|
+
function configPath(scope, cwd = process.cwd()) {
|
|
3493
|
+
if (scope === "global") return join10(homedir3(), ".floom", "config.json");
|
|
3494
|
+
return join10(cwd, ".floom", "config.json");
|
|
3495
|
+
}
|
|
3496
|
+
async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
3497
|
+
try {
|
|
3498
|
+
const raw = await readFile8(configPath(scope, cwd), "utf8");
|
|
3499
|
+
return configSchema.parse(JSON.parse(raw));
|
|
3500
|
+
} catch (e) {
|
|
3501
|
+
if (e.code === "ENOENT") return configSchema.parse({});
|
|
3502
|
+
throw e;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
async function writeFloomConfig(scope, config, cwd = process.cwd()) {
|
|
3506
|
+
const path = configPath(scope, cwd);
|
|
3507
|
+
const parsed = configSchema.parse(config);
|
|
3508
|
+
await mkdir6(dirname(path), { recursive: true, mode: 448 });
|
|
3509
|
+
await writeFile4(path, JSON.stringify(parsed, null, 2) + "\n", { mode: 384 });
|
|
3510
|
+
}
|
|
3511
|
+
function normalizeScope(value) {
|
|
3512
|
+
return value === "global" ? "global" : "local";
|
|
3513
|
+
}
|
|
3514
|
+
function assertTarget(value) {
|
|
3515
|
+
if (TARGETS.includes(value)) return value;
|
|
3516
|
+
throw new Error(`Invalid target: ${value}. Expected ${TARGETS.join(", ")}`);
|
|
3517
|
+
}
|
|
3518
|
+
async function setDefaultWorkspace(slug, scope, cwd = process.cwd()) {
|
|
3519
|
+
const config = await readFloomConfig(scope, cwd);
|
|
3520
|
+
const next = { ...config, default_workspace: slug };
|
|
3521
|
+
await writeFloomConfig(scope, next, cwd);
|
|
3522
|
+
return next;
|
|
3523
|
+
}
|
|
3524
|
+
async function setWorkspaceActive(input) {
|
|
3525
|
+
const config = await readFloomConfig(input.scope, input.cwd);
|
|
3526
|
+
const current = config.targets[input.target] ?? { active_workspaces: [] };
|
|
3527
|
+
const set = new Set(current.active_workspaces);
|
|
3528
|
+
if (input.active) set.add(input.workspace);
|
|
3529
|
+
else set.delete(input.workspace);
|
|
3530
|
+
const next = {
|
|
3531
|
+
...config,
|
|
3532
|
+
targets: {
|
|
3533
|
+
...config.targets,
|
|
3534
|
+
[input.target]: {
|
|
3535
|
+
...current,
|
|
3536
|
+
active_workspaces: Array.from(set).sort()
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
await writeFloomConfig(input.scope, next, input.cwd);
|
|
3541
|
+
return next;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/commands/instruction.ts
|
|
3545
|
+
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
|
|
3546
|
+
import { basename as basename2, dirname as dirname2, join as join11 } from "node:path";
|
|
3547
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3548
|
+
var START = "<!-- FLOOM START -->";
|
|
3549
|
+
var END = "<!-- FLOOM END -->";
|
|
3550
|
+
var AGENT_INSTRUCTION_TARGETS = ["claude", "codex", "cursor", "opencode"];
|
|
3551
|
+
function defaultInstructionPath(target) {
|
|
3552
|
+
if (target === "claude") return "CLAUDE.md";
|
|
3553
|
+
if (target === "codex") return "AGENTS.md";
|
|
3554
|
+
if (target === "cursor") return ".cursor/rules/floom.mdc";
|
|
3555
|
+
if (target === "opencode") return ".opencode/instructions/floom.md";
|
|
3556
|
+
throw new Error(`No default instruction path for target ${target}`);
|
|
3557
|
+
}
|
|
3558
|
+
function replaceManagedBlock(existing, blockBody) {
|
|
3559
|
+
const block = `${START}
|
|
3560
|
+
${blockBody.trim()}
|
|
3561
|
+
${END}
|
|
3562
|
+
`;
|
|
3563
|
+
const start = existing.indexOf(START);
|
|
3564
|
+
const end = existing.indexOf(END);
|
|
3565
|
+
if (start >= 0 && end > start) {
|
|
3566
|
+
const afterEnd = end + END.length;
|
|
3567
|
+
const prefix = existing.slice(0, start);
|
|
3568
|
+
const suffix = existing.slice(afterEnd).replace(/^\n/, "");
|
|
3569
|
+
return { content: `${prefix}${block}${suffix}`, hadBlock: true };
|
|
3570
|
+
}
|
|
3571
|
+
return { content: existing.trim() ? `${existing.trimEnd()}
|
|
3572
|
+
|
|
3573
|
+
${block}` : block, hadBlock: false };
|
|
3574
|
+
}
|
|
3575
|
+
function resolveScope(opts) {
|
|
3576
|
+
if (opts.account && opts.workspace) throw new Error("Use either --account or --workspace, not both.");
|
|
3577
|
+
if (opts.account) return "account";
|
|
3578
|
+
if (opts.workspace) return "workspace";
|
|
3579
|
+
throw new Error("Instruction command requires --account or --workspace <slug>.");
|
|
3580
|
+
}
|
|
3581
|
+
function bodySha256(body) {
|
|
3582
|
+
return createHash4("sha256").update(body, "utf8").digest("hex");
|
|
3583
|
+
}
|
|
3584
|
+
async function readUtf8FileStrict(path) {
|
|
3585
|
+
const bytes = await readFile9(path);
|
|
3586
|
+
try {
|
|
3587
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3588
|
+
} catch {
|
|
3589
|
+
throw new Error(`${path} is not valid UTF-8; refusing to rewrite it as a managed instruction file.`);
|
|
3590
|
+
}
|
|
3591
|
+
return bytes.toString("utf8");
|
|
3592
|
+
}
|
|
3593
|
+
function assertAgentInstructionTarget(value) {
|
|
3594
|
+
if (AGENT_INSTRUCTION_TARGETS.includes(value)) return value;
|
|
3595
|
+
throw new Error(`Invalid instruction target: ${value}. Expected ${AGENT_INSTRUCTION_TARGETS.join(", ")}. Kimi uses the Floom guide skill in V0.`);
|
|
3596
|
+
}
|
|
3597
|
+
function assertPublishInstructionTarget(value) {
|
|
3598
|
+
if (value === "default") return "default";
|
|
3599
|
+
return assertAgentInstructionTarget(value);
|
|
3600
|
+
}
|
|
3601
|
+
async function fetchInstruction(input) {
|
|
3602
|
+
return api("/instructions", {
|
|
3603
|
+
authRequired: true,
|
|
3604
|
+
query: input
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
function buildInstructionBlock(sections) {
|
|
3608
|
+
return [
|
|
3609
|
+
"# Floom",
|
|
3610
|
+
"",
|
|
3611
|
+
"Use Floom for workspace-approved AI skills and instructions.",
|
|
3612
|
+
"",
|
|
3613
|
+
...sections.flatMap((section) => [
|
|
3614
|
+
`## ${section.label}`,
|
|
3615
|
+
"",
|
|
3616
|
+
section.body_md.trim(),
|
|
3617
|
+
""
|
|
3618
|
+
])
|
|
3619
|
+
].join("\n");
|
|
3620
|
+
}
|
|
3621
|
+
async function writeManagedInstructionFile(input) {
|
|
3622
|
+
let existing = "";
|
|
3623
|
+
let existed = true;
|
|
3624
|
+
try {
|
|
3625
|
+
existing = await readUtf8FileStrict(input.path);
|
|
3626
|
+
} catch (e) {
|
|
3627
|
+
if (e.code === "ENOENT") existed = false;
|
|
3628
|
+
else throw e;
|
|
3629
|
+
}
|
|
3630
|
+
const next = replaceManagedBlock(existing, input.blockBody);
|
|
3631
|
+
if (existed && !next.hadBlock && !input.apply && !input.force) {
|
|
3632
|
+
log.warn(`Refusing to modify ${input.path} without an existing Floom managed block.`);
|
|
3633
|
+
log.info("Re-run with --apply to append the managed block, or --path <file> for a dedicated file.");
|
|
3634
|
+
process.exit(1);
|
|
3635
|
+
}
|
|
3636
|
+
if (existed && existing === next.content) return { changed: false };
|
|
3637
|
+
let backupPath;
|
|
3638
|
+
if (existed && existing) {
|
|
3639
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3640
|
+
backupPath = join11(".floom", "backups", `${basename2(input.path)}.${stamp}.bak`);
|
|
3641
|
+
await mkdir7(dirname2(backupPath), { recursive: true, mode: 448 });
|
|
3642
|
+
await writeFile5(backupPath, existing, { mode: 384 });
|
|
3643
|
+
}
|
|
3644
|
+
await mkdir7(dirname2(input.path), { recursive: true });
|
|
3645
|
+
await writeFile5(input.path, next.content, "utf8");
|
|
3646
|
+
return { changed: true, backupPath };
|
|
3647
|
+
}
|
|
3648
|
+
async function recordCliActivity(input) {
|
|
3649
|
+
try {
|
|
3650
|
+
await api("/cli/activity", { method: "POST", authRequired: true, body: input });
|
|
3651
|
+
} catch (e) {
|
|
3652
|
+
log.warn(`Activity event not recorded: ${e.message}`);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
async function upsertInstructionLock(input) {
|
|
3656
|
+
const lock = upgradeLockToV02(await readLock(process.cwd()), {
|
|
3657
|
+
target: input.target,
|
|
3658
|
+
scope: input.configScope,
|
|
3659
|
+
defaultWorkspace: input.defaultWorkspace
|
|
3660
|
+
});
|
|
3661
|
+
const instructions = (lock.instructions ?? []).filter(
|
|
3662
|
+
(entry) => !(entry.scope === input.scope && entry.workspace === input.workspace && entry.target === input.target && entry.path === input.path)
|
|
3663
|
+
);
|
|
3664
|
+
instructions.push({
|
|
3665
|
+
scope: input.scope,
|
|
3666
|
+
workspace: input.workspace,
|
|
3667
|
+
target: input.target,
|
|
3668
|
+
version_id: input.version_id,
|
|
3669
|
+
body_sha256: input.body_sha256,
|
|
3670
|
+
pulled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3671
|
+
path: input.path
|
|
3672
|
+
});
|
|
3673
|
+
await writeLock(process.cwd(), { ...lock, instructions, last_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3674
|
+
}
|
|
3675
|
+
async function instructionPullCommand(opts = {}) {
|
|
3676
|
+
const target = assertAgentInstructionTarget(opts.target ?? "codex");
|
|
3677
|
+
const configScope = normalizeScope(opts.scope);
|
|
3678
|
+
const scope = resolveScope(opts);
|
|
3679
|
+
const config = await readFloomConfig(configScope);
|
|
3680
|
+
const workspace = opts.workspace ?? (scope === "workspace" ? config.default_workspace : void 0);
|
|
3681
|
+
if (scope === "workspace" && !workspace) throw new Error("Workspace instruction pull requires --workspace <slug> or a configured default workspace.");
|
|
3682
|
+
const path = opts.path ?? defaultInstructionPath(target);
|
|
3683
|
+
const resp = await fetchInstruction({ scope, workspace, target });
|
|
3684
|
+
if (!resp.instruction?.latest) {
|
|
3685
|
+
log.info("No remote instruction found.");
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const label = scope === "account" ? "Account Instructions" : `Workspace Instructions: ${workspace}`;
|
|
3689
|
+
const writeResult = await writeManagedInstructionFile({
|
|
3690
|
+
path,
|
|
3691
|
+
blockBody: buildInstructionBlock([{ label, body_md: resp.instruction.latest.body_md }]),
|
|
3692
|
+
apply: opts.apply,
|
|
3693
|
+
force: opts.force
|
|
3694
|
+
});
|
|
3695
|
+
if (!writeResult.changed) {
|
|
3696
|
+
log.info(`${path} is already at the latest ${scope} instruction.`);
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
if (writeResult.backupPath) log.info(`Backed up previous instruction file to ${writeResult.backupPath}`);
|
|
3700
|
+
await upsertInstructionLock({
|
|
3701
|
+
scope,
|
|
3702
|
+
workspace,
|
|
3703
|
+
target,
|
|
3704
|
+
path,
|
|
3705
|
+
version_id: resp.instruction.latest.id,
|
|
3706
|
+
body_sha256: resp.instruction.latest.body_sha256,
|
|
3707
|
+
configScope,
|
|
3708
|
+
defaultWorkspace: config.default_workspace
|
|
3709
|
+
});
|
|
3710
|
+
await recordCliActivity({
|
|
3711
|
+
event_type: "instruction.pulled_cli",
|
|
3712
|
+
scope,
|
|
3713
|
+
workspace,
|
|
3714
|
+
target,
|
|
3715
|
+
instruction_id: resp.instruction.id,
|
|
3716
|
+
version_id: resp.instruction.latest.id,
|
|
3717
|
+
path
|
|
3718
|
+
});
|
|
3719
|
+
log.ok(`Pulled ${scope} instruction to ${path}`);
|
|
3720
|
+
}
|
|
3721
|
+
async function instructionPushCommand(file, opts = {}) {
|
|
3722
|
+
const target = assertPublishInstructionTarget(opts.target ?? "default");
|
|
3723
|
+
const scope = resolveScope(opts);
|
|
3724
|
+
if (scope === "workspace") {
|
|
3725
|
+
const workspace = opts.workspace;
|
|
3726
|
+
const info = await api(`/libraries/${workspace}`, { authRequired: true });
|
|
3727
|
+
if (info.role !== "admin" && info.role !== "editor") {
|
|
3728
|
+
throw new Error(`Workspace instruction push requires editor or admin role in ${workspace}.`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
const body = await readFile9(file, "utf8");
|
|
3732
|
+
const resp = await api("/instructions", {
|
|
3733
|
+
method: "POST",
|
|
3734
|
+
authRequired: true,
|
|
3735
|
+
body: {
|
|
3736
|
+
scope,
|
|
3737
|
+
workspace: opts.workspace,
|
|
3738
|
+
target,
|
|
3739
|
+
body_md: body,
|
|
3740
|
+
changelog: opts.changelog ?? `Published from CLI (${file}, ${bodySha256(body).slice(0, 12)})`
|
|
3741
|
+
}
|
|
3742
|
+
});
|
|
3743
|
+
log.ok(`Published ${scope} instruction ${resp.instruction?.latest?.version_seq ? `v${resp.instruction.latest.version_seq}` : ""}`.trim());
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// src/commands/workspace-config.ts
|
|
3747
|
+
async function assertWorkspaceExists(slug) {
|
|
3748
|
+
await api(`/libraries/${slug}`, { authRequired: true });
|
|
3749
|
+
}
|
|
3750
|
+
function parseOptions(opts) {
|
|
3751
|
+
return {
|
|
3752
|
+
target: assertTarget(opts.target ?? "codex"),
|
|
3753
|
+
scope: normalizeScope(opts.scope)
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
async function defaultWorkspaceCommand(slug, opts = {}) {
|
|
3757
|
+
const scope = normalizeScope(opts.scope);
|
|
3758
|
+
if (!slug) {
|
|
3759
|
+
const config = await readFloomConfig(scope);
|
|
3760
|
+
if (!config.default_workspace) {
|
|
3761
|
+
log.info(`No default workspace configured for ${scope} scope.`);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
console.log(config.default_workspace);
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
await assertWorkspaceExists(slug);
|
|
3768
|
+
await setDefaultWorkspace(slug, scope);
|
|
3769
|
+
log.ok(`Default workspace (${scope}) set to ${slug}`);
|
|
3770
|
+
}
|
|
3771
|
+
async function workspaceActivateCommand(slug, opts = {}) {
|
|
3772
|
+
const parsed = parseOptions(opts);
|
|
3773
|
+
await assertWorkspaceExists(slug);
|
|
3774
|
+
await setWorkspaceActive({ workspace: slug, target: parsed.target, scope: parsed.scope, active: true });
|
|
3775
|
+
await recordCliActivity({ event_type: "workspace.activate_cli", workspace: slug, target: parsed.target });
|
|
3776
|
+
log.ok(`Activated ${slug} for ${parsed.target} (${parsed.scope})`);
|
|
3777
|
+
}
|
|
3778
|
+
async function workspaceDeactivateCommand(slug, opts = {}) {
|
|
3779
|
+
const parsed = parseOptions(opts);
|
|
3780
|
+
await setWorkspaceActive({ workspace: slug, target: parsed.target, scope: parsed.scope, active: false });
|
|
3781
|
+
await recordCliActivity({ event_type: "workspace.deactivate_cli", workspace: slug, target: parsed.target });
|
|
3782
|
+
log.ok(`Deactivated ${slug} for ${parsed.target} (${parsed.scope})`);
|
|
3783
|
+
}
|
|
3784
|
+
async function workspaceActiveCommand(opts = {}) {
|
|
3785
|
+
const parsed = parseOptions(opts);
|
|
3786
|
+
const config = await readFloomConfig(parsed.scope);
|
|
3787
|
+
const active = config.targets[parsed.target]?.active_workspaces ?? [];
|
|
3788
|
+
if (!active.length) {
|
|
3789
|
+
log.info(`No active workspaces for ${parsed.target} (${parsed.scope}).`);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
for (const workspace of active) console.log(workspace);
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
// src/commands/sync.ts
|
|
3796
|
+
import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
|
|
3797
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
3798
|
+
var ROUTER_SKILL = [
|
|
3799
|
+
"# Floom Find Skills",
|
|
3800
|
+
"",
|
|
3801
|
+
"Use this router skill instead of loading every Floom skill into context.",
|
|
3802
|
+
"",
|
|
3803
|
+
"When a task may need a reusable skill:",
|
|
3804
|
+
"",
|
|
3805
|
+
"1. Call the Floom MCP `search_skills` tool with a short query.",
|
|
3806
|
+
"2. Review the compact candidate list.",
|
|
3807
|
+
"3. Call `get_skill` only for the selected candidate.",
|
|
3808
|
+
"4. Call `install_skill` only when the skill needs to be available locally.",
|
|
3809
|
+
"",
|
|
3810
|
+
"Do not enumerate the whole workspace library into model context. Keep skill bodies out of context until selected.",
|
|
3811
|
+
""
|
|
3812
|
+
].join("\n");
|
|
3813
|
+
async function installRouter(target) {
|
|
3814
|
+
const install = resolveInstallDir({ target });
|
|
3815
|
+
const routerDir = target === "kimi" ? "floom" : "floom-find-skills";
|
|
3816
|
+
const path = join12(install.dir, routerDir, "SKILL.md");
|
|
3817
|
+
await mkdir8(dirname3(path), { recursive: true });
|
|
3818
|
+
await writeFile6(path, ROUTER_SKILL, "utf8");
|
|
3819
|
+
return path;
|
|
3820
|
+
}
|
|
3821
|
+
async function pullInstructions(input) {
|
|
3822
|
+
const path = defaultInstructionPath(input.target);
|
|
3823
|
+
const sections = [];
|
|
3824
|
+
const account = await fetchInstruction({ scope: "account", target: input.target });
|
|
3825
|
+
if (account.instruction?.latest) {
|
|
3826
|
+
sections.push({
|
|
3827
|
+
label: "Account Instructions",
|
|
3828
|
+
scope: "account",
|
|
3829
|
+
instruction_id: account.instruction.id,
|
|
3830
|
+
version_id: account.instruction.latest.id,
|
|
3831
|
+
body_sha256: account.instruction.latest.body_sha256,
|
|
3832
|
+
body_md: account.instruction.latest.body_md
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
for (const workspace of input.activeWorkspaces) {
|
|
3836
|
+
const resp = await fetchInstruction({ scope: "workspace", workspace, target: input.target });
|
|
3837
|
+
if (!resp.instruction?.latest) continue;
|
|
3838
|
+
sections.push({
|
|
3839
|
+
label: `Workspace Instructions: ${workspace}`,
|
|
3840
|
+
scope: "workspace",
|
|
3841
|
+
workspace,
|
|
3842
|
+
instruction_id: resp.instruction.id,
|
|
3843
|
+
version_id: resp.instruction.latest.id,
|
|
3844
|
+
body_sha256: resp.instruction.latest.body_sha256,
|
|
3845
|
+
body_md: resp.instruction.latest.body_md
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3848
|
+
if (sections.length === 0) {
|
|
3849
|
+
log.info("No remote instructions found.");
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
const writeResult = await writeManagedInstructionFile({
|
|
3853
|
+
path,
|
|
3854
|
+
blockBody: buildInstructionBlock(sections.map((section) => ({ label: section.label, body_md: section.body_md }))),
|
|
3855
|
+
apply: input.apply,
|
|
3856
|
+
force: input.force
|
|
3857
|
+
});
|
|
3858
|
+
if (!writeResult.changed) {
|
|
3859
|
+
log.info(`${path} already contains the latest Floom instruction block.`);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (writeResult.backupPath) log.info(`Backed up previous instruction file to ${writeResult.backupPath}`);
|
|
3863
|
+
for (const section of sections) {
|
|
3864
|
+
await upsertInstructionLock({
|
|
3865
|
+
scope: section.scope,
|
|
3866
|
+
workspace: section.workspace,
|
|
3867
|
+
target: input.target,
|
|
3868
|
+
path,
|
|
3869
|
+
version_id: section.version_id,
|
|
3870
|
+
body_sha256: section.body_sha256,
|
|
3871
|
+
configScope: input.scope,
|
|
3872
|
+
defaultWorkspace: input.defaultWorkspace
|
|
3873
|
+
});
|
|
3874
|
+
await recordCliActivity({
|
|
3875
|
+
event_type: "instruction.pulled_cli",
|
|
3876
|
+
scope: section.scope,
|
|
3877
|
+
workspace: section.workspace,
|
|
3878
|
+
target: input.target,
|
|
3879
|
+
instruction_id: section.instruction_id,
|
|
3880
|
+
version_id: section.version_id,
|
|
3881
|
+
path
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
log.ok(`Pulled ${sections.length} instruction section(s) to ${path}`);
|
|
3885
|
+
}
|
|
3886
|
+
async function statusCommand(opts = {}) {
|
|
3887
|
+
const target = assertTarget(opts.target ?? "codex");
|
|
3888
|
+
const scope = normalizeScope(opts.scope);
|
|
3889
|
+
const config = await readFloomConfig(scope);
|
|
3890
|
+
const active = config.targets[target]?.active_workspaces ?? [];
|
|
3891
|
+
log.heading(`Floom status for ${target} (${scope})`);
|
|
3892
|
+
log.kv("Default workspace", config.default_workspace ?? "(none)");
|
|
3893
|
+
log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
|
|
3894
|
+
log.kv("Local pull policy", "router + pinned skills + instructions only");
|
|
3895
|
+
for (const workspace of active) {
|
|
3896
|
+
const pins = await api(`/libraries/${workspace}/pins`, {
|
|
3897
|
+
authRequired: true,
|
|
3898
|
+
query: { target }
|
|
3899
|
+
});
|
|
3900
|
+
log.blank();
|
|
3901
|
+
log.info(`${workspace}: ${pins.pins.length} pinned skill(s) for ${target}`);
|
|
3902
|
+
for (const pin of pins.pins) {
|
|
3903
|
+
log.kv("", `${pin.skill?.slug ?? pin.skill_id}${pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : ""}`);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
async function pullCommand(opts = {}) {
|
|
3908
|
+
const target = assertTarget(opts.target ?? "codex");
|
|
3909
|
+
const scope = normalizeScope(opts.scope);
|
|
3910
|
+
const config = await readFloomConfig(scope);
|
|
3911
|
+
const active = config.targets[target]?.active_workspaces ?? [];
|
|
3912
|
+
const routerPath = await installRouter(target);
|
|
3913
|
+
log.ok(`Installed Floom router skill to ${routerPath}`);
|
|
3914
|
+
if (target === "kimi") {
|
|
3915
|
+
log.info("Kimi uses the Floom guide/router skill in V0; account and workspace instructions are not merged into Kimi context.");
|
|
3916
|
+
} else {
|
|
3917
|
+
await pullInstructions({
|
|
3918
|
+
target: assertAgentInstructionTarget(target),
|
|
3919
|
+
scope,
|
|
3920
|
+
defaultWorkspace: config.default_workspace,
|
|
3921
|
+
activeWorkspaces: active,
|
|
3922
|
+
apply: opts.apply,
|
|
3923
|
+
force: opts.force
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
for (const workspace of active) {
|
|
3927
|
+
const pins = await api(`/libraries/${workspace}/pins`, {
|
|
3928
|
+
authRequired: true,
|
|
3929
|
+
query: { target }
|
|
3930
|
+
});
|
|
3931
|
+
for (const pin of pins.pins) {
|
|
3932
|
+
if (!pin.skill?.slug) continue;
|
|
3933
|
+
await installCommand(`${workspace}/${pin.skill.slug}`, { for: target, force: opts.force });
|
|
3934
|
+
}
|
|
3935
|
+
await recordCliActivity({
|
|
3936
|
+
event_type: "workspace.pulled_cli",
|
|
3937
|
+
workspace,
|
|
3938
|
+
target,
|
|
3939
|
+
resources: pins.pins.map((pin) => ({ type: "skill", id: pin.skill_id, name: pin.skill?.slug ?? pin.skill_id }))
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
log.blank();
|
|
3943
|
+
log.ok("Pull complete.");
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
// src/commands/pin.ts
|
|
3947
|
+
async function resolveWorkspace(opts) {
|
|
3948
|
+
if (opts.workspace) return opts.workspace;
|
|
3949
|
+
const config = await readFloomConfig("local");
|
|
3950
|
+
if (config.default_workspace) return config.default_workspace;
|
|
3951
|
+
throw new Error("Pin command requires --workspace <slug> or a configured default workspace.");
|
|
3952
|
+
}
|
|
3953
|
+
async function pinCommand(ref, opts = {}) {
|
|
3954
|
+
const target = assertTarget(opts.target ?? "codex");
|
|
3955
|
+
const workspace = await resolveWorkspace(opts);
|
|
3956
|
+
await api(`/libraries/${workspace}/pins`, {
|
|
3957
|
+
method: "POST",
|
|
3958
|
+
authRequired: true,
|
|
3959
|
+
body: { ref, target }
|
|
3960
|
+
});
|
|
3961
|
+
log.ok(`Pinned ${ref} for ${target} in ${workspace}`);
|
|
3962
|
+
}
|
|
3963
|
+
async function unpinCommand(ref, opts = {}) {
|
|
3964
|
+
const target = assertTarget(opts.target ?? "codex");
|
|
3965
|
+
const workspace = await resolveWorkspace(opts);
|
|
3966
|
+
await api(`/libraries/${workspace}/pins`, {
|
|
3967
|
+
method: "DELETE",
|
|
3968
|
+
authRequired: true,
|
|
3969
|
+
query: { ref, target }
|
|
3970
|
+
});
|
|
3971
|
+
log.ok(`Unpinned ${ref} for ${target} in ${workspace}`);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3409
3974
|
// src/commands/mcp.ts
|
|
3410
|
-
import { mkdtemp, mkdir as
|
|
3411
|
-
import { join as
|
|
3975
|
+
import { mkdtemp, mkdir as mkdir9, readdir as readdir4, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
3976
|
+
import { join as join13 } from "node:path";
|
|
3412
3977
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
3413
|
-
import { z as
|
|
3978
|
+
import { z as z3 } from "zod";
|
|
3414
3979
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3415
3980
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3416
3981
|
var API_TIMEOUT_MS = 2e4;
|
|
3982
|
+
function semverGte2(a, b) {
|
|
3983
|
+
const pa = a.split(".").map(Number);
|
|
3984
|
+
const pb = b.split(".").map(Number);
|
|
3985
|
+
for (let i = 0; i < 3; i++) {
|
|
3986
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
3987
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
3988
|
+
}
|
|
3989
|
+
return true;
|
|
3990
|
+
}
|
|
3417
3991
|
async function fetchWithTimeout2(url, init = {}) {
|
|
3418
3992
|
const controller = new AbortController();
|
|
3419
3993
|
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
@@ -3471,19 +4045,26 @@ async function apiRequest(token, path, query) {
|
|
|
3471
4045
|
}
|
|
3472
4046
|
throw lastError ?? new Error("API request failed");
|
|
3473
4047
|
}
|
|
3474
|
-
async function installViaApi(token, refText, target) {
|
|
4048
|
+
async function installViaApi(token, refText, target, options = {}) {
|
|
3475
4049
|
const parsed = parseSkillRef(refText);
|
|
3476
4050
|
if (!parsed) throw new Error(`Invalid ref: ${refText}`);
|
|
4051
|
+
if (!isInstallTarget(target)) throw new Error(`Invalid install target: ${target}`);
|
|
3477
4052
|
const info = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}`);
|
|
4053
|
+
if (info.min_floom_version && !semverGte2(VERSION, info.min_floom_version)) {
|
|
4054
|
+
throw new Error(`This skill requires Floom CLI >= ${info.min_floom_version} (you have ${VERSION}).`);
|
|
4055
|
+
}
|
|
3478
4056
|
const dl = await apiRequest(token, `/skills/${parsed.owner}/${parsed.slug}/download`, parsed.version ? { version: parsed.version } : void 0);
|
|
3479
4057
|
const bundle = await rawGet(dl.download.url);
|
|
3480
4058
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
3481
4059
|
const install = resolveInstallDir({ target });
|
|
3482
|
-
await
|
|
3483
|
-
const dest =
|
|
4060
|
+
await mkdir9(install.dir, { recursive: true });
|
|
4061
|
+
const dest = join13(install.dir, parsed.slug);
|
|
3484
4062
|
const exists = await readdir4(dest).then(() => true).catch(() => false);
|
|
4063
|
+
if (exists && !options.force) {
|
|
4064
|
+
throw new Error(`Folder already exists at ${dest}. Call install_skill with force=true to overwrite after reviewing local changes.`);
|
|
4065
|
+
}
|
|
3485
4066
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
3486
|
-
const temp = await mkdtemp(
|
|
4067
|
+
const temp = await mkdtemp(join13(tmpdir3(), `floom-mcp-${parsed.slug}-`));
|
|
3487
4068
|
try {
|
|
3488
4069
|
await extractBundle(bundle, temp);
|
|
3489
4070
|
await rename3(temp, dest);
|
|
@@ -3503,10 +4084,10 @@ async function installViaApi(token, refText, target) {
|
|
|
3503
4084
|
preset: target
|
|
3504
4085
|
});
|
|
3505
4086
|
await writeLock(process.cwd(), next);
|
|
3506
|
-
return { path: dest, version: dl.version, ref: info.ref ?? ref };
|
|
4087
|
+
return { path: dest, version: dl.version, ref: info.ref ?? ref, has_scripts: !!dl.has_scripts };
|
|
3507
4088
|
}
|
|
3508
4089
|
async function parseSkillBundle(bundle) {
|
|
3509
|
-
const tmp = await mkdtemp(
|
|
4090
|
+
const tmp = await mkdtemp(join13(tmpdir3(), "floom-mcp-read-"));
|
|
3510
4091
|
try {
|
|
3511
4092
|
await extractBundle(bundle, tmp);
|
|
3512
4093
|
const files = [];
|
|
@@ -3514,16 +4095,16 @@ async function parseSkillBundle(bundle) {
|
|
|
3514
4095
|
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3515
4096
|
for (const entry of entries) {
|
|
3516
4097
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
3517
|
-
const full =
|
|
4098
|
+
const full = join13(dir, entry.name);
|
|
3518
4099
|
if (entry.isDirectory()) await walk2(full, nextRel);
|
|
3519
4100
|
else files.push(nextRel);
|
|
3520
4101
|
}
|
|
3521
4102
|
};
|
|
3522
4103
|
await walk2(tmp);
|
|
3523
4104
|
const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
|
|
3524
|
-
const skillMd = skillMdPath ? await
|
|
4105
|
+
const skillMd = skillMdPath ? await readFile10(join13(tmp, skillMdPath), "utf8") : "";
|
|
3525
4106
|
const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
|
|
3526
|
-
const skillJson = skillJsonPath ? JSON.parse(await
|
|
4107
|
+
const skillJson = skillJsonPath ? JSON.parse(await readFile10(join13(tmp, skillJsonPath), "utf8")) : null;
|
|
3527
4108
|
return { files, skill_md: skillMd, skill_json: skillJson };
|
|
3528
4109
|
} finally {
|
|
3529
4110
|
await rm3(tmp, { recursive: true, force: true });
|
|
@@ -3531,13 +4112,13 @@ async function parseSkillBundle(bundle) {
|
|
|
3531
4112
|
}
|
|
3532
4113
|
async function mcpCommand() {
|
|
3533
4114
|
const server = new McpServer({ name: "floom", version: VERSION });
|
|
3534
|
-
server.tool("search_skills", { query:
|
|
4115
|
+
server.tool("search_skills", { query: z3.string().min(1), workspace: z3.string().optional(), library: z3.string().optional() }, async ({ query, workspace, library }) => {
|
|
3535
4116
|
const token = await resolveRequiredToken();
|
|
3536
4117
|
const workspaceSlug = workspace ?? library;
|
|
3537
4118
|
const result = await apiRequest(token, "/skills", { q: query, ...workspaceSlug ? { library: workspaceSlug } : {} });
|
|
3538
4119
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3539
4120
|
});
|
|
3540
|
-
server.tool("get_skill", { ref:
|
|
4121
|
+
server.tool("get_skill", { ref: z3.string().min(3) }, async ({ ref }) => {
|
|
3541
4122
|
const token = await resolveOptionalToken();
|
|
3542
4123
|
const parsed = parseSkillRef(ref);
|
|
3543
4124
|
if (!parsed) throw new Error("Invalid ref. Expected @owner/slug or workspace/slug");
|
|
@@ -3554,9 +4135,61 @@ async function mcpCommand() {
|
|
|
3554
4135
|
}
|
|
3555
4136
|
server.tool("list_workspaces", {}, listWorkspaces);
|
|
3556
4137
|
server.tool("list_libraries", {}, listWorkspaces);
|
|
3557
|
-
server.tool("
|
|
4138
|
+
server.tool("get_floom_guide", {}, async () => {
|
|
4139
|
+
return {
|
|
4140
|
+
content: [{
|
|
4141
|
+
type: "text",
|
|
4142
|
+
text: JSON.stringify({
|
|
4143
|
+
guide: [
|
|
4144
|
+
"Use Floom to discover approved AI skills without loading every skill into context.",
|
|
4145
|
+
"Call search_skills when you need a skill that is not already installed.",
|
|
4146
|
+
"Call get_skill only after selecting a specific skill candidate.",
|
|
4147
|
+
"Call install_skill only after the user or task requires local installation.",
|
|
4148
|
+
"Use get_instruction for account/workspace guidance visible to this agent."
|
|
4149
|
+
]
|
|
4150
|
+
})
|
|
4151
|
+
}]
|
|
4152
|
+
};
|
|
4153
|
+
});
|
|
4154
|
+
server.tool("get_instruction", {
|
|
4155
|
+
scope: z3.enum(["account", "workspace"]).default("account"),
|
|
4156
|
+
workspace: z3.string().optional(),
|
|
4157
|
+
target: z3.enum(["default", "claude", "codex", "cursor", "opencode"]).default("default")
|
|
4158
|
+
}, async ({ scope, workspace, target }) => {
|
|
4159
|
+
const token = await resolveRequiredToken();
|
|
4160
|
+
const result = await apiRequest(token, "/instructions", {
|
|
4161
|
+
scope,
|
|
4162
|
+
...workspace ? { workspace } : {},
|
|
4163
|
+
target
|
|
4164
|
+
});
|
|
4165
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4166
|
+
});
|
|
4167
|
+
server.tool("list_active_workspaces", {
|
|
4168
|
+
target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode"]).default("codex"),
|
|
4169
|
+
scope: z3.enum(["global", "local"]).default("local")
|
|
4170
|
+
}, async ({ target, scope }) => {
|
|
4171
|
+
const config = await readFloomConfig(normalizeScope(scope));
|
|
4172
|
+
const active = config.targets[assertTarget(target)]?.active_workspaces ?? [];
|
|
4173
|
+
return { content: [{ type: "text", text: JSON.stringify({ target, scope, active_workspaces: active }) }] };
|
|
4174
|
+
});
|
|
4175
|
+
server.tool("list_pinned_skills", {
|
|
4176
|
+
workspace: z3.string().optional(),
|
|
4177
|
+
target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode"]).default("codex")
|
|
4178
|
+
}, async ({ workspace, target }) => {
|
|
4179
|
+
const token = await resolveRequiredToken();
|
|
4180
|
+
const config = await readFloomConfig("local");
|
|
4181
|
+
const workspaceSlug = workspace ?? config.default_workspace;
|
|
4182
|
+
if (!workspaceSlug) throw new Error("workspace is required when no default workspace is configured");
|
|
4183
|
+
const result = await apiRequest(token, `/libraries/${workspaceSlug}/pins`, { target });
|
|
4184
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4185
|
+
});
|
|
4186
|
+
server.tool("install_skill", {
|
|
4187
|
+
ref: z3.string().min(3),
|
|
4188
|
+
target: z3.enum(["claude", "codex", "cursor", "kimi", "opencode", "gemini"]),
|
|
4189
|
+
force: z3.boolean().optional().default(false)
|
|
4190
|
+
}, async ({ ref, target, force }) => {
|
|
3558
4191
|
const token = await resolveOptionalToken();
|
|
3559
|
-
const installed = await installViaApi(token, ref, target);
|
|
4192
|
+
const installed = await installViaApi(token, ref, target, { force });
|
|
3560
4193
|
return { content: [{ type: "text", text: JSON.stringify(installed) }] };
|
|
3561
4194
|
});
|
|
3562
4195
|
const transport = new StdioServerTransport();
|
|
@@ -3566,7 +4199,7 @@ async function mcpCommand() {
|
|
|
3566
4199
|
// src/commands/doctor.ts
|
|
3567
4200
|
import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm4 } from "node:fs/promises";
|
|
3568
4201
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
3569
|
-
import { join as
|
|
4202
|
+
import { join as join14 } from "node:path";
|
|
3570
4203
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3571
4204
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3572
4205
|
function textOf(result) {
|
|
@@ -3597,6 +4230,15 @@ function warn(name, detail) {
|
|
|
3597
4230
|
function fail(name, detail) {
|
|
3598
4231
|
return { name, ok: false, detail };
|
|
3599
4232
|
}
|
|
4233
|
+
function apiUrlCheck(rawApiUrl, label = "api_url") {
|
|
4234
|
+
const normalized = normalizeApiUrl(rawApiUrl);
|
|
4235
|
+
const trusted = trustedApiUrlOrDefault(rawApiUrl);
|
|
4236
|
+
if (isLegacyApiUrl(rawApiUrl)) return warn(label, `legacy URL ${rawApiUrl}; using ${DEFAULT_API_URL}`);
|
|
4237
|
+
if (normalized !== trusted) {
|
|
4238
|
+
return warn(label, `ignored untrusted API URL ${normalized}; set FLOOM_ALLOW_CUSTOM_API_URL=1 only for a trusted self-hosted Floom API`);
|
|
4239
|
+
}
|
|
4240
|
+
return pass(label, trusted);
|
|
4241
|
+
}
|
|
3600
4242
|
async function validateCurrentToken(token) {
|
|
3601
4243
|
if (!token) return warn("fresh_agent_auth", "missing token; authenticated MCP calls skipped");
|
|
3602
4244
|
try {
|
|
@@ -3626,7 +4268,7 @@ async function doctorCommand(opts = {}) {
|
|
|
3626
4268
|
const checks2 = [
|
|
3627
4269
|
pass("cli_version", VERSION),
|
|
3628
4270
|
authCheck2,
|
|
3629
|
-
process.env.FLOOM_API_URL ?
|
|
4271
|
+
process.env.FLOOM_API_URL ? apiUrlCheck(process.env.FLOOM_API_URL) : isLegacyApiUrl(rawAuth?.apiUrl) ? warn("auth_api_url", `legacy URL in ~/.floom/auth.json; using ${DEFAULT_API_URL}`) : apiUrlCheck(auth2?.apiUrl ?? DEFAULT_API_URL)
|
|
3630
4272
|
];
|
|
3631
4273
|
emitDoctor(checks2, opts.json);
|
|
3632
4274
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
@@ -3644,9 +4286,9 @@ async function doctorCommand(opts = {}) {
|
|
|
3644
4286
|
emitDoctor(checks, opts.json);
|
|
3645
4287
|
process.exit(1);
|
|
3646
4288
|
}
|
|
3647
|
-
const tmpHome = await mkdtemp2(
|
|
3648
|
-
const tmpSkills = await mkdtemp2(
|
|
3649
|
-
const tmpProject = await mkdtemp2(
|
|
4289
|
+
const tmpHome = await mkdtemp2(join14(tmpdir4(), "floom-doctor-home-"));
|
|
4290
|
+
const tmpSkills = await mkdtemp2(join14(tmpdir4(), "floom-doctor-skills-"));
|
|
4291
|
+
const tmpProject = await mkdtemp2(join14(tmpdir4(), "floom-doctor-project-"));
|
|
3650
4292
|
const transport = new StdioClientTransport({
|
|
3651
4293
|
command: process.execPath,
|
|
3652
4294
|
args: [cliPath, "mcp"],
|
|
@@ -3656,7 +4298,7 @@ async function doctorCommand(opts = {}) {
|
|
|
3656
4298
|
HOME: tmpHome,
|
|
3657
4299
|
FLOOM_SKILLS_DIR: tmpSkills,
|
|
3658
4300
|
...hasValidToken && token ? { FLOOM_API_TOKEN: token } : {},
|
|
3659
|
-
...process.env.FLOOM_API_URL ? { FLOOM_API_URL:
|
|
4301
|
+
...process.env.FLOOM_API_URL ? { FLOOM_API_URL: trustedApiUrlOrDefault(process.env.FLOOM_API_URL) } : auth?.apiUrl ? { FLOOM_API_URL: trustedApiUrlOrDefault(auth.apiUrl) } : {}
|
|
3660
4302
|
},
|
|
3661
4303
|
stderr: "pipe"
|
|
3662
4304
|
});
|
|
@@ -3682,12 +4324,15 @@ async function doctorCommand(opts = {}) {
|
|
|
3682
4324
|
} else {
|
|
3683
4325
|
checks.push(warn("mcp_authenticated_tools", "skipped list_workspaces/search_skills because no valid token is available"));
|
|
3684
4326
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4327
|
+
if (opts.ref) {
|
|
4328
|
+
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
4329
|
+
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
4330
|
+
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
4331
|
+
const entries = await readdir5(tmpSkills);
|
|
4332
|
+
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
4333
|
+
} else {
|
|
4334
|
+
checks.push(warn("mcp_public_skill", "skipped; pass --ref <public-skill-ref> to verify get_skill/install_skill against a known public skill"));
|
|
4335
|
+
}
|
|
3691
4336
|
} catch (e) {
|
|
3692
4337
|
checks.push(fail("fresh_agent_mcp", e.message));
|
|
3693
4338
|
} finally {
|
|
@@ -3730,16 +4375,28 @@ program.command("outdated").description("Show installed skills with newer versio
|
|
|
3730
4375
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
3731
4376
|
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
4377
|
program.command("info <ref>").description("Show details for a remote skill.").action(infoCommand);
|
|
4378
|
+
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));
|
|
4379
|
+
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));
|
|
4380
|
+
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));
|
|
4381
|
+
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
4382
|
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
4383
|
program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
|
|
4384
|
+
var configCmd = program.command("config").description("Manage local Floom configuration");
|
|
4385
|
+
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
4386
|
function addWorkspaceCommands(cmd) {
|
|
3736
4387
|
cmd.command("list").action(libraryListCommand);
|
|
3737
4388
|
cmd.command("create <slug> <name>").action((slug, name) => libraryCreateCommand(slug, name));
|
|
3738
4389
|
cmd.command("invite <workspaceSlug> <email>").option("--role <role>", "viewer|editor|admin", "viewer").action((workspaceSlug, email, opts) => libraryInviteCommand(workspaceSlug, email, opts.role));
|
|
3739
4390
|
cmd.command("leave <workspaceSlug>").action((workspaceSlug) => libraryLeaveCommand(workspaceSlug));
|
|
4391
|
+
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));
|
|
4392
|
+
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));
|
|
4393
|
+
cmd.command("active").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((opts) => workspaceActiveCommand(opts));
|
|
3740
4394
|
}
|
|
3741
4395
|
addWorkspaceCommands(program.command("workspace").description("Manage workspaces"));
|
|
3742
4396
|
addWorkspaceCommands(program.command("library").description("Manage workspaces (legacy alias)"));
|
|
4397
|
+
var instructionCmd = program.command("instruction").description("Manage account and workspace instructions");
|
|
4398
|
+
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));
|
|
4399
|
+
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
4400
|
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
4401
|
program.command("mcp").description("Run local MCP server over stdio.").action(mcpCommand);
|
|
3745
4402
|
async function main() {
|