@floomhq/floom 2.0.3 → 2.0.5
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 +351 -24
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2477,6 +2477,46 @@ var ContractDecisionChecklistItemSchema = z2.object({
|
|
|
2477
2477
|
title: z2.string().min(1),
|
|
2478
2478
|
locked: z2.literal(true)
|
|
2479
2479
|
}).strict();
|
|
2480
|
+
var AGENT_TARGETS = ["claude", "codex", "cursor", "gemini", "opencode"];
|
|
2481
|
+
var AgentTargetSyncSchema = z2.object({
|
|
2482
|
+
last_pulled_at: IsoDateTimeSchema.nullable()
|
|
2483
|
+
}).strict();
|
|
2484
|
+
var AgentMachineSchema = z2.object({
|
|
2485
|
+
machine_id: z2.string().uuid(),
|
|
2486
|
+
machine_label: z2.string().min(1),
|
|
2487
|
+
last_seen_at: IsoDateTimeSchema,
|
|
2488
|
+
target_sync: z2.record(z2.enum(AGENT_TARGETS), AgentTargetSyncSchema)
|
|
2489
|
+
}).strict();
|
|
2490
|
+
var AgentsListResponseSchema = z2.object({
|
|
2491
|
+
machines: z2.array(AgentMachineSchema),
|
|
2492
|
+
total: z2.number().int().nonnegative()
|
|
2493
|
+
}).strict();
|
|
2494
|
+
var ActivityEventSchema = z2.object({
|
|
2495
|
+
id: IdSchema,
|
|
2496
|
+
event_name: z2.enum(["signup", "workspace_create", "push", "pull", "share_create", "skill_delete"]),
|
|
2497
|
+
source: z2.enum(["web", "cli", "api"]),
|
|
2498
|
+
target: z2.enum(AGENT_TARGETS).nullable(),
|
|
2499
|
+
skill_slug: z2.string().nullable(),
|
|
2500
|
+
created_at: IsoDateTimeSchema
|
|
2501
|
+
}).strict();
|
|
2502
|
+
var ActivityFeedResponseSchema = z2.object({
|
|
2503
|
+
events: z2.array(ActivityEventSchema),
|
|
2504
|
+
total: z2.number().int().nonnegative()
|
|
2505
|
+
}).strict();
|
|
2506
|
+
var SkillSyncTargetEntrySchema = z2.object({
|
|
2507
|
+
target: TargetSchema,
|
|
2508
|
+
last_pulled_at: IsoDateTimeSchema
|
|
2509
|
+
}).strict();
|
|
2510
|
+
var SkillSyncMachineSchema = z2.object({
|
|
2511
|
+
machine_id: IdSchema,
|
|
2512
|
+
machine_label: z2.string().min(1),
|
|
2513
|
+
targets: z2.array(SkillSyncTargetEntrySchema)
|
|
2514
|
+
}).strict();
|
|
2515
|
+
var SkillSyncStateResponseSchema = z2.object({
|
|
2516
|
+
machines: z2.array(SkillSyncMachineSchema),
|
|
2517
|
+
total_machines: z2.number().int().nonnegative(),
|
|
2518
|
+
total_targets_synced: z2.number().int().nonnegative()
|
|
2519
|
+
}).strict();
|
|
2480
2520
|
|
|
2481
2521
|
// src/lib/output.ts
|
|
2482
2522
|
import chalk from "chalk";
|
|
@@ -2563,14 +2603,20 @@ async function readRawAuth() {
|
|
|
2563
2603
|
const code = e.code;
|
|
2564
2604
|
if (code === "ENOENT") return null;
|
|
2565
2605
|
if (code === "EACCES" || code === "EPERM") {
|
|
2566
|
-
throw new Error(
|
|
2606
|
+
throw new Error(
|
|
2607
|
+
"Cannot read your Floom auth file (permission denied).\n Fix: chmod 600 ~/.floom/auth.json\n Or re-authenticate: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2608
|
+
);
|
|
2567
2609
|
}
|
|
2568
|
-
throw new Error(
|
|
2610
|
+
throw new Error(
|
|
2611
|
+
"Cannot read your Floom auth file.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2612
|
+
);
|
|
2569
2613
|
}
|
|
2570
2614
|
try {
|
|
2571
2615
|
return JSON.parse(raw);
|
|
2572
2616
|
} catch {
|
|
2573
|
-
throw new Error(
|
|
2617
|
+
throw new Error(
|
|
2618
|
+
"Your Floom auth file is corrupted.\n Run: npx -y @floomhq/floom login to generate a fresh one.\n More help: https://floom.dev/docs#troubleshooting"
|
|
2619
|
+
);
|
|
2574
2620
|
}
|
|
2575
2621
|
}
|
|
2576
2622
|
async function writeAuth(state) {
|
|
@@ -2692,7 +2738,7 @@ async function getMachineIdentity() {
|
|
|
2692
2738
|
}
|
|
2693
2739
|
|
|
2694
2740
|
// src/version.ts
|
|
2695
|
-
var VERSION = "2.0.
|
|
2741
|
+
var VERSION = "2.0.4";
|
|
2696
2742
|
|
|
2697
2743
|
// src/api-client.ts
|
|
2698
2744
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
@@ -2739,11 +2785,54 @@ async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
|
|
|
2739
2785
|
return truncated ? `${text}
|
|
2740
2786
|
[truncated]` : text;
|
|
2741
2787
|
}
|
|
2788
|
+
var HELP = "https://floom.dev/docs#troubleshooting";
|
|
2789
|
+
function friendlyApiErrorMessage(code, raw, status) {
|
|
2790
|
+
if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
|
|
2791
|
+
return `Not signed in.
|
|
2792
|
+
Run: npx -y @floomhq/floom login
|
|
2793
|
+
More help: ${HELP}`;
|
|
2794
|
+
}
|
|
2795
|
+
if (code === "TOKEN_EXPIRED") {
|
|
2796
|
+
return `Your session has expired.
|
|
2797
|
+
Run: npx -y @floomhq/floom login
|
|
2798
|
+
More help: ${HELP}`;
|
|
2799
|
+
}
|
|
2800
|
+
if (code === "SKILL_ACCESS_DENIED" || status === 403) {
|
|
2801
|
+
return `Access denied. This skill belongs to another workspace or you don't have permission to modify it.
|
|
2802
|
+
If you want your own copy, push it with a different slug. More help: ${HELP}`;
|
|
2803
|
+
}
|
|
2804
|
+
if (code === "SKILL_NOT_FOUND" || status === 404) {
|
|
2805
|
+
return `Skill not found. It may have been deleted or the slug is wrong.
|
|
2806
|
+
Run: npx -y @floomhq/floom list
|
|
2807
|
+
More help: ${HELP}`;
|
|
2808
|
+
}
|
|
2809
|
+
if (code === "RATE_LIMITED" || status === 429) {
|
|
2810
|
+
return `Too many requests. Wait a moment and try again.
|
|
2811
|
+
More help: ${HELP}`;
|
|
2812
|
+
}
|
|
2813
|
+
if (code === "CLI_VERSION_TOO_OLD") {
|
|
2814
|
+
return `Your CLI is out of date.
|
|
2815
|
+
Run: npx -y @floomhq/floom@latest <command>
|
|
2816
|
+
More help: ${HELP}`;
|
|
2817
|
+
}
|
|
2818
|
+
if (code === "FILE_TOO_LARGE" || code === "BUNDLE_TOO_LARGE") {
|
|
2819
|
+
return `${raw}
|
|
2820
|
+
Reduce the size of your skill files before pushing. More help: ${HELP}`;
|
|
2821
|
+
}
|
|
2822
|
+
if (status && status >= 500) {
|
|
2823
|
+
return `Floom service error (${status}). Please try again in a moment.
|
|
2824
|
+
More help: ${HELP}`;
|
|
2825
|
+
}
|
|
2826
|
+
return raw;
|
|
2827
|
+
}
|
|
2742
2828
|
async function api(path, opts = {}) {
|
|
2743
2829
|
const auth = await readAuth();
|
|
2744
2830
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
2745
2831
|
if (opts.authRequired && !token) {
|
|
2746
|
-
throw new FloomError(
|
|
2832
|
+
throw new FloomError(
|
|
2833
|
+
"AUTH_REQUIRED",
|
|
2834
|
+
"Not signed in.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2835
|
+
);
|
|
2747
2836
|
}
|
|
2748
2837
|
let lastError = null;
|
|
2749
2838
|
const bases = getApiBaseUrls(auth?.apiUrl);
|
|
@@ -2776,7 +2865,9 @@ async function api(path, opts = {}) {
|
|
|
2776
2865
|
} catch (e) {
|
|
2777
2866
|
lastError = new FloomError(
|
|
2778
2867
|
"INTERNAL_ERROR",
|
|
2779
|
-
`
|
|
2868
|
+
`Cannot reach Floom (${base}): ${e.message}
|
|
2869
|
+
Check your internet connection or firewall. If on a corporate network, you may need to set a proxy: npm config set proxy http://...
|
|
2870
|
+
More help: https://floom.dev/docs#troubleshooting`,
|
|
2780
2871
|
{ apiUrl: base }
|
|
2781
2872
|
);
|
|
2782
2873
|
continue;
|
|
@@ -2796,17 +2887,19 @@ async function api(path, opts = {}) {
|
|
|
2796
2887
|
}
|
|
2797
2888
|
const envelopeError = json?.error;
|
|
2798
2889
|
if (envelopeError && typeof envelopeError === "object" && envelopeError.code) {
|
|
2890
|
+
const rawMsg2 = typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code);
|
|
2799
2891
|
throw new FloomError(
|
|
2800
2892
|
envelopeError.code,
|
|
2801
|
-
|
|
2893
|
+
friendlyApiErrorMessage(envelopeError.code, rawMsg2, res.status),
|
|
2802
2894
|
{ status: res.status, requestId: envelopeError.request_id, apiUrl: base }
|
|
2803
2895
|
);
|
|
2804
2896
|
}
|
|
2805
2897
|
if (res.ok) return json;
|
|
2806
2898
|
const err = envelopeError ?? {};
|
|
2899
|
+
const rawMsg = err.message ?? `HTTP ${res.status} ${res.statusText}`;
|
|
2807
2900
|
lastError = new FloomError(
|
|
2808
2901
|
err.code ?? "INTERNAL_ERROR",
|
|
2809
|
-
err.
|
|
2902
|
+
friendlyApiErrorMessage(err.code ?? "INTERNAL_ERROR", rawMsg, res.status),
|
|
2810
2903
|
{ status: res.status, requestId: err.request_id, apiUrl: base }
|
|
2811
2904
|
);
|
|
2812
2905
|
if (res.status !== 404 || json?.error) break;
|
|
@@ -2862,7 +2955,9 @@ async function loginCommand() {
|
|
|
2862
2955
|
if (remaining <= 0) break;
|
|
2863
2956
|
await sleep(Math.min(interval, remaining));
|
|
2864
2957
|
}
|
|
2865
|
-
throw new Error(
|
|
2958
|
+
throw new Error(
|
|
2959
|
+
"Login timed out.\n The browser approval window expired. Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
|
|
2960
|
+
);
|
|
2866
2961
|
}
|
|
2867
2962
|
|
|
2868
2963
|
// src/commands/mcp.ts
|
|
@@ -2936,7 +3031,9 @@ async function readManifest(root) {
|
|
|
2936
3031
|
if (err.code === "ENOENT") return null;
|
|
2937
3032
|
if (error instanceof SyntaxError || error instanceof ZodError) {
|
|
2938
3033
|
throw new Error(
|
|
2939
|
-
`Corrupted manifest at ${manifestPath(root)}. Remove that file or run
|
|
3034
|
+
`Corrupted manifest at ${manifestPath(root)}. Remove that file or run floom pull after backing up local edits.
|
|
3035
|
+
Run: rm ${manifestPath(root)} && npx -y @floomhq/floom pull
|
|
3036
|
+
More help: https://floom.dev/docs#troubleshooting`
|
|
2940
3037
|
);
|
|
2941
3038
|
}
|
|
2942
3039
|
throw error;
|
|
@@ -3148,8 +3245,11 @@ function printDetectedTargets(targets) {
|
|
|
3148
3245
|
for (const target of targets) log.info(`- ${target}`);
|
|
3149
3246
|
}
|
|
3150
3247
|
function printNoDetectedTargets() {
|
|
3151
|
-
log.
|
|
3152
|
-
log.info(`
|
|
3248
|
+
log.err(`No AI agent recognized on this machine.`);
|
|
3249
|
+
log.info(` Floom looks for: ${INSTALL_TARGETS.join(", ")}.`);
|
|
3250
|
+
log.info(` Make sure one of these is installed, then re-run.`);
|
|
3251
|
+
log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
|
|
3252
|
+
log.info(` More help: https://floom.dev/docs#agents`);
|
|
3153
3253
|
}
|
|
3154
3254
|
async function statusCommand(options) {
|
|
3155
3255
|
if (!options.target) {
|
|
@@ -3563,11 +3663,17 @@ async function findImmediateSkillDirs(root) {
|
|
|
3563
3663
|
async function pushOneSkill(root, pushApi) {
|
|
3564
3664
|
const dirStat = await stat4(root).catch(() => null);
|
|
3565
3665
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3566
|
-
throw new Error(
|
|
3666
|
+
throw new Error(
|
|
3667
|
+
`Folder not found: ${root}
|
|
3668
|
+
Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
|
|
3669
|
+
);
|
|
3567
3670
|
}
|
|
3568
3671
|
const bundle = await collectBundle(root);
|
|
3569
3672
|
const skillMd = bundle.files.find((file) => file.relPath === "SKILL.md");
|
|
3570
|
-
if (!skillMd) throw new Error(
|
|
3673
|
+
if (!skillMd) throw new Error(
|
|
3674
|
+
`Missing SKILL.md in ${root}.
|
|
3675
|
+
Every skill folder needs a SKILL.md at its root. See: https://floom.dev/docs#concept`
|
|
3676
|
+
);
|
|
3571
3677
|
const files = await Promise.all(bundle.files.map(async (file) => ({
|
|
3572
3678
|
path: file.relPath,
|
|
3573
3679
|
content_base64: (await readFile6(file.absPath)).toString("base64")
|
|
@@ -3598,7 +3704,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3598
3704
|
const root = resolve2(dir);
|
|
3599
3705
|
const dirStat = await stat4(root).catch(() => null);
|
|
3600
3706
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
3601
|
-
throw new Error(
|
|
3707
|
+
throw new Error(
|
|
3708
|
+
`Folder not found: ${dir}
|
|
3709
|
+
Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
|
|
3710
|
+
);
|
|
3602
3711
|
}
|
|
3603
3712
|
const pushApi = deps.pushApi ?? api;
|
|
3604
3713
|
if (await hasSkillMarkdown(root)) {
|
|
@@ -3607,7 +3716,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
|
3607
3716
|
return;
|
|
3608
3717
|
}
|
|
3609
3718
|
const skillDirs = await findImmediateSkillDirs(root);
|
|
3610
|
-
if (skillDirs.length === 0) throw new Error(
|
|
3719
|
+
if (skillDirs.length === 0) throw new Error(
|
|
3720
|
+
`No skills found in ${dir}.
|
|
3721
|
+
Neither the folder itself nor any immediate subfolder contains a SKILL.md. See: https://floom.dev/docs#concept`
|
|
3722
|
+
);
|
|
3611
3723
|
const concurrency = parseConcurrency(options.concurrency);
|
|
3612
3724
|
const startedAt = Date.now();
|
|
3613
3725
|
const errors = [];
|
|
@@ -3704,6 +3816,221 @@ async function renameMachineCommand(newLabel, _opts) {
|
|
|
3704
3816
|
}
|
|
3705
3817
|
}
|
|
3706
3818
|
|
|
3819
|
+
// src/commands/add.ts
|
|
3820
|
+
import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
3821
|
+
import { join as join8 } from "node:path";
|
|
3822
|
+
var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
|
|
3823
|
+
function parseToken(input) {
|
|
3824
|
+
const trimmed = input.trim();
|
|
3825
|
+
if (TOKEN_RE.test(trimmed)) return trimmed;
|
|
3826
|
+
try {
|
|
3827
|
+
const url = new URL(trimmed);
|
|
3828
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
3829
|
+
if (parts.length >= 2 && parts[parts.length - 2] === "s") {
|
|
3830
|
+
const candidate = parts[parts.length - 1];
|
|
3831
|
+
if (candidate && TOKEN_RE.test(candidate)) return candidate;
|
|
3832
|
+
}
|
|
3833
|
+
const last = parts[parts.length - 1];
|
|
3834
|
+
if (last && TOKEN_RE.test(last)) return last;
|
|
3835
|
+
} catch {
|
|
3836
|
+
}
|
|
3837
|
+
return null;
|
|
3838
|
+
}
|
|
3839
|
+
var SAFE_SLUG_RE2 = /^(?=.{1,80}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3840
|
+
var MAX_DECODED_FILE_BYTES2 = 2 * 1024 * 1024;
|
|
3841
|
+
function safeSkillSlug2(slug) {
|
|
3842
|
+
if (!slug) throw new Error("Remote skill slug is empty.");
|
|
3843
|
+
if (slug.length > 80) throw new Error(`Remote skill slug "${slug.slice(0, 80)}\u2026" exceeds 80 characters.`);
|
|
3844
|
+
if (!SAFE_SLUG_RE2.test(slug)) throw new Error(`Unsafe remote skill slug: ${slug}`);
|
|
3845
|
+
return slug;
|
|
3846
|
+
}
|
|
3847
|
+
function safeRemotePath2(path) {
|
|
3848
|
+
const normalized = path.replace(/\\/g, "/");
|
|
3849
|
+
if (!normalized || /[\x00-\x1f\x7f]/.test(normalized) || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
3850
|
+
throw new Error(`Unsafe remote file path: ${path}`);
|
|
3851
|
+
}
|
|
3852
|
+
return normalized;
|
|
3853
|
+
}
|
|
3854
|
+
function bytesForShareFile(file) {
|
|
3855
|
+
if (file.mode === "text") {
|
|
3856
|
+
const text = file.content_text ?? "";
|
|
3857
|
+
if (Buffer.byteLength(text, "utf8") > MAX_DECODED_FILE_BYTES2) {
|
|
3858
|
+
throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
|
|
3859
|
+
}
|
|
3860
|
+
return Buffer.from(text, "utf8");
|
|
3861
|
+
}
|
|
3862
|
+
const url = file.binary?.download_url ?? null;
|
|
3863
|
+
if (!url?.startsWith("data:")) return Buffer.alloc(0);
|
|
3864
|
+
const [, encoded] = url.split(",", 2);
|
|
3865
|
+
const base64 = encoded ?? "";
|
|
3866
|
+
const bytes = Buffer.from(base64, "base64");
|
|
3867
|
+
if (bytes.byteLength > MAX_DECODED_FILE_BYTES2) {
|
|
3868
|
+
throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
|
|
3869
|
+
}
|
|
3870
|
+
return bytes;
|
|
3871
|
+
}
|
|
3872
|
+
function derivedSlug(title) {
|
|
3873
|
+
return title.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 80).replace(/^-+|-+$/g, "") || "shared-skill";
|
|
3874
|
+
}
|
|
3875
|
+
async function fetchPublicShare(token) {
|
|
3876
|
+
const apiBase = getAppUrl().replace(/\/$/, "");
|
|
3877
|
+
const url = `${apiBase}/api/v1/share/${encodeURIComponent(token)}`;
|
|
3878
|
+
const controller = new AbortController();
|
|
3879
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
3880
|
+
let res;
|
|
3881
|
+
try {
|
|
3882
|
+
res = await fetch(url, {
|
|
3883
|
+
headers: { "User-Agent": "floom-cli/add" },
|
|
3884
|
+
redirect: "manual",
|
|
3885
|
+
signal: controller.signal
|
|
3886
|
+
});
|
|
3887
|
+
} catch (e) {
|
|
3888
|
+
if (e.name === "AbortError") {
|
|
3889
|
+
throw new Error("Request timed out. Check your internet connection and try again.");
|
|
3890
|
+
}
|
|
3891
|
+
throw new Error(`Cannot reach Floom: ${e.message}`);
|
|
3892
|
+
} finally {
|
|
3893
|
+
clearTimeout(timer);
|
|
3894
|
+
}
|
|
3895
|
+
if (res.status === 404) {
|
|
3896
|
+
throw new FloomError(
|
|
3897
|
+
"SKILL_NOT_FOUND",
|
|
3898
|
+
"Share link not found or revoked. Ask the sender for a fresh link."
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
if (res.status === 429) {
|
|
3902
|
+
throw new FloomError(
|
|
3903
|
+
"RATE_LIMITED",
|
|
3904
|
+
"Too many requests. Try again in a minute."
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
if (!res.ok) {
|
|
3908
|
+
throw new Error(`Floom API returned HTTP ${res.status}. Try again in a moment.`);
|
|
3909
|
+
}
|
|
3910
|
+
let json;
|
|
3911
|
+
try {
|
|
3912
|
+
json = await res.json();
|
|
3913
|
+
} catch {
|
|
3914
|
+
throw new Error("Unexpected response from Floom API. Try again in a moment.");
|
|
3915
|
+
}
|
|
3916
|
+
if (json && typeof json === "object" && "error" in json) {
|
|
3917
|
+
const errObj = json.error;
|
|
3918
|
+
if (errObj) throw new Error(errObj.message ?? "API error");
|
|
3919
|
+
}
|
|
3920
|
+
const candidate = json;
|
|
3921
|
+
if (!candidate || typeof candidate !== "object" || !candidate.skill || !Array.isArray(candidate.file_contents)) {
|
|
3922
|
+
throw new Error("Unexpected response shape from Floom API.");
|
|
3923
|
+
}
|
|
3924
|
+
return candidate;
|
|
3925
|
+
}
|
|
3926
|
+
async function addCommand(input, opts = {}) {
|
|
3927
|
+
const token = parseToken(input);
|
|
3928
|
+
if (!token) {
|
|
3929
|
+
log.err(
|
|
3930
|
+
"That doesn't look like a Floom share link. Should look like: https://floom.dev/s/fls_xxx"
|
|
3931
|
+
);
|
|
3932
|
+
process.exitCode = 1;
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
let target;
|
|
3936
|
+
if (opts.target) {
|
|
3937
|
+
try {
|
|
3938
|
+
target = assertInstallTarget(opts.target);
|
|
3939
|
+
} catch {
|
|
3940
|
+
log.err(`Unknown target "${opts.target}". Use one of: claude, codex, cursor, gemini, opencode`);
|
|
3941
|
+
process.exitCode = 1;
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
} else {
|
|
3945
|
+
const detected = await detectInstalledTargets();
|
|
3946
|
+
if (detected.length === 0) {
|
|
3947
|
+
log.err("No AI agent recognized on this machine.");
|
|
3948
|
+
log.info(" Floom looks for: claude, codex, cursor, gemini, opencode.");
|
|
3949
|
+
log.info(" Or pick one explicitly: npx -y @floomhq/floom add <share-url> --target claude");
|
|
3950
|
+
process.exitCode = 1;
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
target = detected[0];
|
|
3954
|
+
}
|
|
3955
|
+
const resolved = resolveInstallDir({ target, global: true });
|
|
3956
|
+
log.step(`Fetching share\u2026`);
|
|
3957
|
+
let shareData;
|
|
3958
|
+
try {
|
|
3959
|
+
shareData = await fetchPublicShare(token);
|
|
3960
|
+
} catch (e) {
|
|
3961
|
+
if (e instanceof FloomError) {
|
|
3962
|
+
log.err(e.message);
|
|
3963
|
+
} else {
|
|
3964
|
+
log.err(e.message ?? "Unknown error");
|
|
3965
|
+
}
|
|
3966
|
+
process.exitCode = 1;
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
const slug = safeSkillSlug2(derivedSlug(shareData.skill.title));
|
|
3970
|
+
for (const file of shareData.file_contents) {
|
|
3971
|
+
safeRemotePath2(file.path);
|
|
3972
|
+
}
|
|
3973
|
+
const cleanup = installCancellationHandler();
|
|
3974
|
+
const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
|
|
3975
|
+
cleanup.trackDir(tempDir);
|
|
3976
|
+
try {
|
|
3977
|
+
await mkdir5(tempDir, { recursive: true });
|
|
3978
|
+
for (const file of shareData.file_contents) {
|
|
3979
|
+
const safePath = safeRemotePath2(file.path);
|
|
3980
|
+
const dest = join8(tempDir, ...safePath.split("/"));
|
|
3981
|
+
const destDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
3982
|
+
if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
|
|
3983
|
+
await writeFile5(dest, bytesForShareFile(file));
|
|
3984
|
+
}
|
|
3985
|
+
const finalDir = join8(resolved.dir, slug);
|
|
3986
|
+
const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
|
|
3987
|
+
let movedExisting = false;
|
|
3988
|
+
try {
|
|
3989
|
+
const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
|
|
3990
|
+
await mkdir5(resolved.dir, { recursive: true });
|
|
3991
|
+
const { stat: stat5 } = await import("node:fs/promises");
|
|
3992
|
+
let existingDir = false;
|
|
3993
|
+
try {
|
|
3994
|
+
await stat5(finalDir);
|
|
3995
|
+
existingDir = true;
|
|
3996
|
+
} catch {
|
|
3997
|
+
}
|
|
3998
|
+
if (existingDir) {
|
|
3999
|
+
await rename2(finalDir, replacedDir);
|
|
4000
|
+
movedExisting = true;
|
|
4001
|
+
}
|
|
4002
|
+
try {
|
|
4003
|
+
await rename2(tempDir, finalDir);
|
|
4004
|
+
} catch (renameErr) {
|
|
4005
|
+
if (movedExisting) {
|
|
4006
|
+
try {
|
|
4007
|
+
await rename2(replacedDir, finalDir);
|
|
4008
|
+
} catch {
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
throw new Error(`Failed to install ${slug}: ${renameErr.message}`);
|
|
4012
|
+
}
|
|
4013
|
+
if (movedExisting) await rmFs(replacedDir, { recursive: true, force: true });
|
|
4014
|
+
} catch (e) {
|
|
4015
|
+
throw e;
|
|
4016
|
+
}
|
|
4017
|
+
log.blank();
|
|
4018
|
+
log.ok(`Installed ${shareData.skill.title} ${shareData.skill.version.display} from ${shareData.skill.owner.name}`);
|
|
4019
|
+
log.info(` \u2192 ${finalDir}`);
|
|
4020
|
+
log.blank();
|
|
4021
|
+
log.info(`Next: in your agent, the skill is now loaded under the slug "${slug}".`);
|
|
4022
|
+
} catch (e) {
|
|
4023
|
+
log.err(e.message ?? "Unknown error");
|
|
4024
|
+
process.exitCode = 1;
|
|
4025
|
+
} finally {
|
|
4026
|
+
cleanup.dispose();
|
|
4027
|
+
try {
|
|
4028
|
+
await rm2(tempDir, { recursive: true, force: true });
|
|
4029
|
+
} catch {
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3707
4034
|
// src/index.ts
|
|
3708
4035
|
async function logoutCommand() {
|
|
3709
4036
|
const auth = await readAuth();
|
|
@@ -3720,7 +4047,9 @@ async function logoutCommand() {
|
|
|
3720
4047
|
async function whoamiCommand() {
|
|
3721
4048
|
const auth = await readAuth();
|
|
3722
4049
|
if (!auth) {
|
|
3723
|
-
log.
|
|
4050
|
+
log.err("Not signed in.");
|
|
4051
|
+
log.info(" Run: npx -y @floomhq/floom login");
|
|
4052
|
+
log.info(" More help: https://floom.dev/docs#troubleshooting");
|
|
3724
4053
|
process.exitCode = 1;
|
|
3725
4054
|
return;
|
|
3726
4055
|
}
|
|
@@ -3739,12 +4068,9 @@ async function whoamiCommand() {
|
|
|
3739
4068
|
}
|
|
3740
4069
|
log.kv("api url", auth.apiUrl);
|
|
3741
4070
|
} catch (error) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
log.err(error.message);
|
|
3746
|
-
}
|
|
3747
|
-
log.warn("Local auth file exists but the server rejected this session. Run: npx -y @floomhq/floom login");
|
|
4071
|
+
log.err(error.message);
|
|
4072
|
+
log.warn("Session invalid. Run: npx -y @floomhq/floom login");
|
|
4073
|
+
log.info(" More help: https://floom.dev/docs#troubleshooting");
|
|
3748
4074
|
process.exitCode = 1;
|
|
3749
4075
|
}
|
|
3750
4076
|
}
|
|
@@ -3759,13 +4085,14 @@ program.command("pull").description("Pull the whole workspace library.").option(
|
|
|
3759
4085
|
program.command("list").description("List workspace skills.").action(listCommand);
|
|
3760
4086
|
program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
|
|
3761
4087
|
program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
|
|
4088
|
+
program.command("add <share-url-or-token>").description("Install a skill from a Floom share link.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action((input, opts) => addCommand(input, opts));
|
|
3762
4089
|
program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
|
|
3763
4090
|
async function main() {
|
|
3764
4091
|
try {
|
|
3765
4092
|
await program.parseAsync(process.argv);
|
|
3766
4093
|
} catch (e) {
|
|
3767
4094
|
if (e instanceof FloomError) {
|
|
3768
|
-
log.err(
|
|
4095
|
+
log.err(e.message);
|
|
3769
4096
|
process.exit(1);
|
|
3770
4097
|
}
|
|
3771
4098
|
log.err(e.message ?? "Unknown error");
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "2.0.
|
|
1
|
+
export const VERSION = "2.0.5";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/floom",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://floom.dev",
|