@floomhq/floom 1.0.2 → 1.0.4
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/cli.js +198 -67
- package/dist/config.js +3 -0
- package/dist/delete.js +2 -2
- package/dist/doctor.js +18 -15
- package/dist/errors.js +6 -2
- package/dist/info.js +6 -2
- package/dist/init.js +23 -4
- package/dist/install.js +43 -12
- package/dist/lib/api.js +13 -5
- package/dist/library.js +8 -8
- package/dist/list.js +4 -3
- package/dist/mcp.js +5 -10
- package/dist/publish.js +27 -16
- package/dist/scan.js +26 -0
- package/dist/search.js +2 -2
- package/dist/secrets.js +105 -0
- package/dist/setup.js +19 -11
- package/dist/share.js +2 -2
- package/dist/sync.js +2 -2
- package/dist/version.js +25 -0
- package/dist/whoami.js +9 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,77 +14,101 @@ import { sync } from "./sync.js";
|
|
|
14
14
|
import { printMcpSetup } from "./mcp.js";
|
|
15
15
|
import { setupAgent } from "./setup.js";
|
|
16
16
|
import { search } from "./search.js";
|
|
17
|
+
import { scanSkill } from "./scan.js";
|
|
17
18
|
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
18
19
|
import { c, symbols } from "./ui.js";
|
|
19
20
|
import { printError, FloomError } from "./errors.js";
|
|
20
|
-
|
|
21
|
-
const PKG = { name: "@floomhq/floom", version:
|
|
21
|
+
import { CLI_VERSION } from "./version.js";
|
|
22
|
+
const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
|
|
22
23
|
const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
|
|
23
24
|
function usage() {
|
|
24
25
|
const out = `
|
|
25
|
-
${c.coral("
|
|
26
|
-
${c.coral(" /
|
|
27
|
-
${c.coral("
|
|
28
|
-
${c.coral("
|
|
26
|
+
${c.coral(" ________")}
|
|
27
|
+
${c.coral(" / ____/ /___ ____ ____ ___")} ${c.dim(`v${CLI_VERSION}`)}
|
|
28
|
+
${c.coral(" / /_ / / __ \\/ __ \\/ __ `__ \\")}
|
|
29
|
+
${c.coral(" / __/ / / /_/ / /_/ / / / / / /")}
|
|
30
|
+
${c.coral(" /_/ /_/\\____/\\____/_/ /_/ /_/")}
|
|
29
31
|
|
|
30
32
|
${c.bold("Share AI agent skills with a link.")}
|
|
31
33
|
${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
|
|
32
34
|
|
|
33
|
-
${c.bold("
|
|
34
|
-
${c.
|
|
35
|
+
${c.bold("Do this next")}
|
|
36
|
+
${c.dim("1. Add a skill someone sent you")}
|
|
37
|
+
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --target claude")}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
${c.dim("
|
|
39
|
+
${c.dim("2. Publish your own skill")}
|
|
40
|
+
${c.cyan("npx -y @floomhq/floom init")} ${c.dim("support-tone.md")}
|
|
41
|
+
${c.dim("# edit support-tone.md")}
|
|
42
|
+
${c.cyan("npx -y @floomhq/floom scan")} ${c.dim("support-tone.md")}
|
|
43
|
+
${c.cyan("npx -y @floomhq/floom login")}
|
|
44
|
+
${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
39
45
|
|
|
40
|
-
${c.
|
|
41
|
-
${c.
|
|
46
|
+
${c.dim("3. Tell your agent where skills land")}
|
|
47
|
+
${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
|
|
48
|
+
${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target codex --yes")}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
${c.bold("Safety")}
|
|
51
|
+
${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
|
|
45
52
|
|
|
46
|
-
${c.bold("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
${c.bold("More")}
|
|
54
|
+
${c.cyan("floom commands")} ${c.dim("Full command list")}
|
|
55
|
+
${c.cyan("floom doctor")} ${c.dim("Check auth, API, and local folders")}
|
|
56
|
+
${c.dim("Docs")} https://floom.dev
|
|
57
|
+
`;
|
|
58
|
+
process.stdout.write(out);
|
|
59
|
+
}
|
|
60
|
+
function commandUsage() {
|
|
61
|
+
const out = `
|
|
62
|
+
${c.bold("Usage:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
${c.cyan("
|
|
64
|
+
${c.bold("Commands")}
|
|
65
|
+
${c.dim("Skills")}
|
|
66
|
+
${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
|
|
67
|
+
${c.dim("Alias: install")}
|
|
68
|
+
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
69
|
+
${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
|
|
70
|
+
${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
|
|
55
71
|
|
|
56
|
-
${c.dim("
|
|
57
|
-
${c.
|
|
58
|
-
${c.
|
|
59
|
-
${c.
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
${c.dim("Publishing")}
|
|
73
|
+
${c.cyan("init")} ${c.dim("[path]")} Create a skill scaffold
|
|
74
|
+
${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
|
|
75
|
+
${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
|
|
76
|
+
${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
|
|
77
|
+
${c.dim(" --skill-version <label>")}
|
|
62
78
|
|
|
63
79
|
${c.dim("Account")}
|
|
64
|
-
${c.cyan("login")}
|
|
65
|
-
${c.cyan("list")}
|
|
66
|
-
${c.cyan("
|
|
80
|
+
${c.cyan("login")} Authenticate
|
|
81
|
+
${c.cyan("list")} Your published skills
|
|
82
|
+
${c.cyan("delete")} ${c.dim("<url>")} Delete one of your skills
|
|
83
|
+
${c.dim("Alias: rm")}
|
|
84
|
+
${c.cyan("whoami")} Show the signed-in account
|
|
85
|
+
${c.cyan("logout")} Switch accounts or remove local credentials
|
|
86
|
+
|
|
87
|
+
${c.dim("Agent setup")}
|
|
88
|
+
${c.cyan("setup")} Configure Claude Code or Codex instructions
|
|
89
|
+
${c.dim("Alias: connect")}
|
|
90
|
+
${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
|
|
91
|
+
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
92
|
+
|
|
93
|
+
${c.dim("Advanced")}
|
|
94
|
+
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
95
|
+
${c.dim("Alias: lib")}
|
|
67
96
|
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
|
|
68
|
-
${c.cyan("
|
|
69
|
-
${c.cyan("
|
|
70
|
-
${c.cyan("
|
|
97
|
+
${c.cyan("mcp")} Print optional MCP setup guidance
|
|
98
|
+
${c.cyan("sync")} Preview pull of published, saved, and library skills
|
|
99
|
+
${c.cyan("watch")} Preview polling sync loop
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
${c.cyan("sync")} Preview: pull published, saved, and library skills
|
|
77
|
-
${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
|
|
78
|
-
${c.cyan("doctor")} Diagnose auth, API, and local setup
|
|
79
|
-
${c.cyan("--help")} Show this help
|
|
80
|
-
${c.cyan("--version")} Show version
|
|
101
|
+
${c.bold("Examples")}
|
|
102
|
+
${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
|
|
103
|
+
${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
104
|
+
${c.cyan("floom setup")} ${c.dim("--target claude --yes")}
|
|
81
105
|
|
|
82
|
-
${c.bold("
|
|
83
|
-
${c.cyan("
|
|
106
|
+
${c.bold("Help")}
|
|
107
|
+
${c.cyan("floom commands")} Show this reference
|
|
108
|
+
${c.cyan("--help")} Show this reference
|
|
109
|
+
${c.cyan("--version")} Show CLI version
|
|
84
110
|
|
|
85
|
-
${c.
|
|
86
|
-
${c.dim("Docs")} https://floom.dev
|
|
87
|
-
${c.dim("Source")} https://github.com/floomhq/floom
|
|
111
|
+
${c.dim("Run")} ${c.cyan("floom")} ${c.dim("with no command for the guided start screen.")}
|
|
88
112
|
`;
|
|
89
113
|
process.stdout.write(out);
|
|
90
114
|
}
|
|
@@ -110,14 +134,17 @@ function readFlagValue(argv, index, flag) {
|
|
|
110
134
|
}
|
|
111
135
|
function parseFlags(argv) {
|
|
112
136
|
const out = { visibility: "unlisted", update: false, rest: [] };
|
|
137
|
+
let visibilityFlag = null;
|
|
113
138
|
for (let i = 0; i < argv.length; i++) {
|
|
114
139
|
const a = argv[i] ?? "";
|
|
115
|
-
if (a === "--public")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
if (a === "--public" || a === "--private" || a === "--unlisted") {
|
|
141
|
+
const nextVisibility = a.slice(2);
|
|
142
|
+
if (visibilityFlag && visibilityFlag !== nextVisibility) {
|
|
143
|
+
throw new FloomError("Conflicting visibility flags.", "Use only one of: --public, --private, or --unlisted.");
|
|
144
|
+
}
|
|
145
|
+
visibilityFlag = nextVisibility;
|
|
146
|
+
out.visibility = nextVisibility;
|
|
147
|
+
}
|
|
121
148
|
else if (a === "--update") {
|
|
122
149
|
throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
|
|
123
150
|
}
|
|
@@ -140,19 +167,36 @@ function parseFlags(argv) {
|
|
|
140
167
|
out.installsAs = value;
|
|
141
168
|
i = nextIndex;
|
|
142
169
|
}
|
|
143
|
-
else if (a === "--version" || a.startsWith("--version=")) {
|
|
144
|
-
const
|
|
170
|
+
else if (a === "--skill-version" || a.startsWith("--skill-version=") || a === "--version" || a.startsWith("--version=")) {
|
|
171
|
+
const flagName = a.startsWith("--skill-version") ? "--skill-version" : "--version";
|
|
172
|
+
const { value, nextIndex } = readFlagValue(argv, i, flagName);
|
|
145
173
|
if (!VERSION_RE.test(value)) {
|
|
146
|
-
throw new FloomError(`Invalid
|
|
174
|
+
throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
147
175
|
}
|
|
148
176
|
out.version = value;
|
|
149
177
|
i = nextIndex;
|
|
150
178
|
}
|
|
179
|
+
else if (a.startsWith("--")) {
|
|
180
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
|
|
181
|
+
}
|
|
151
182
|
else
|
|
152
183
|
out.rest.push(a);
|
|
153
184
|
}
|
|
154
185
|
return out;
|
|
155
186
|
}
|
|
187
|
+
function parseInitArgs(argv) {
|
|
188
|
+
let file;
|
|
189
|
+
for (const a of argv) {
|
|
190
|
+
if (a.startsWith("--")) {
|
|
191
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md`.");
|
|
192
|
+
}
|
|
193
|
+
if (file) {
|
|
194
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
|
|
195
|
+
}
|
|
196
|
+
file = a;
|
|
197
|
+
}
|
|
198
|
+
return file ? { file } : {};
|
|
199
|
+
}
|
|
156
200
|
function parseListFlags(argv) {
|
|
157
201
|
const out = { json: false };
|
|
158
202
|
for (const a of argv) {
|
|
@@ -161,6 +205,9 @@ function parseListFlags(argv) {
|
|
|
161
205
|
else if (a.startsWith("--")) {
|
|
162
206
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
163
207
|
}
|
|
208
|
+
else {
|
|
209
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
|
|
210
|
+
}
|
|
164
211
|
}
|
|
165
212
|
return out;
|
|
166
213
|
}
|
|
@@ -173,9 +220,38 @@ function parseInfoFlags(argv) {
|
|
|
173
220
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
174
221
|
else if (!out.slug)
|
|
175
222
|
out.slug = a;
|
|
223
|
+
else
|
|
224
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
|
|
176
225
|
}
|
|
177
226
|
return out;
|
|
178
227
|
}
|
|
228
|
+
function parseAddArgs(argv) {
|
|
229
|
+
let slug;
|
|
230
|
+
let target;
|
|
231
|
+
for (let i = 0; i < argv.length; i++) {
|
|
232
|
+
const a = argv[i] ?? "";
|
|
233
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
234
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
235
|
+
if (value !== "claude" && value !== "codex") {
|
|
236
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
237
|
+
}
|
|
238
|
+
target = value;
|
|
239
|
+
i = nextIndex;
|
|
240
|
+
}
|
|
241
|
+
else if (a.startsWith("--")) {
|
|
242
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
|
|
243
|
+
}
|
|
244
|
+
else if (slug) {
|
|
245
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
slug = a;
|
|
249
|
+
}
|
|
250
|
+
if (!slug) {
|
|
251
|
+
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --target claude`");
|
|
252
|
+
}
|
|
253
|
+
return target ? { slug, target } : { slug };
|
|
254
|
+
}
|
|
179
255
|
function parseSearchFlags(argv) {
|
|
180
256
|
const out = { json: false };
|
|
181
257
|
const terms = [];
|
|
@@ -215,9 +291,19 @@ function parseDeleteFlags(argv) {
|
|
|
215
291
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
216
292
|
else if (!out.slug)
|
|
217
293
|
out.slug = a;
|
|
294
|
+
else
|
|
295
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
218
296
|
}
|
|
219
297
|
return out;
|
|
220
298
|
}
|
|
299
|
+
function rejectArgs(argv, usageHint) {
|
|
300
|
+
const arg = argv[0];
|
|
301
|
+
if (!arg)
|
|
302
|
+
return;
|
|
303
|
+
if (arg.startsWith("--"))
|
|
304
|
+
throw new FloomError(`Unknown flag: ${arg}`, usageHint);
|
|
305
|
+
throw new FloomError(`Unexpected argument: ${arg}`, usageHint);
|
|
306
|
+
}
|
|
221
307
|
function parseSetupFlags(argv) {
|
|
222
308
|
const out = { dryRun: false, yes: false };
|
|
223
309
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -341,6 +427,9 @@ async function runLibrary(argv) {
|
|
|
341
427
|
if (!librarySlug || !skillSlug) {
|
|
342
428
|
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
343
429
|
}
|
|
430
|
+
if (flags.rest.length > 2) {
|
|
431
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
432
|
+
}
|
|
344
433
|
await libraryAddSkill({
|
|
345
434
|
librarySlug,
|
|
346
435
|
skillSlug,
|
|
@@ -355,6 +444,9 @@ async function runLibrary(argv) {
|
|
|
355
444
|
if (!librarySlug || !skillSlug) {
|
|
356
445
|
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
357
446
|
}
|
|
447
|
+
if (rest.length > 2) {
|
|
448
|
+
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
|
|
449
|
+
}
|
|
358
450
|
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
359
451
|
return;
|
|
360
452
|
}
|
|
@@ -362,6 +454,9 @@ async function runLibrary(argv) {
|
|
|
362
454
|
const slug = rest[0];
|
|
363
455
|
if (!slug)
|
|
364
456
|
throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
|
|
457
|
+
if (rest.length > 1) {
|
|
458
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
|
|
459
|
+
}
|
|
365
460
|
await librarySubscribe(slug);
|
|
366
461
|
return;
|
|
367
462
|
}
|
|
@@ -369,6 +464,9 @@ async function runLibrary(argv) {
|
|
|
369
464
|
const slug = rest[0];
|
|
370
465
|
if (!slug)
|
|
371
466
|
throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
|
|
467
|
+
if (rest.length > 1) {
|
|
468
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
|
|
469
|
+
}
|
|
372
470
|
await libraryUnsubscribe(slug);
|
|
373
471
|
return;
|
|
374
472
|
}
|
|
@@ -401,6 +499,19 @@ function parseWatchFlags(argv) {
|
|
|
401
499
|
function notAvailable(feature) {
|
|
402
500
|
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
403
501
|
}
|
|
502
|
+
function parseSingleFileArg(argv, usageHint) {
|
|
503
|
+
let file;
|
|
504
|
+
for (const a of argv) {
|
|
505
|
+
if (a.startsWith("--"))
|
|
506
|
+
throw new FloomError(`Unknown flag: ${a}`, usageHint);
|
|
507
|
+
if (file)
|
|
508
|
+
throw new FloomError(`Unexpected argument: ${a}`, usageHint);
|
|
509
|
+
file = a;
|
|
510
|
+
}
|
|
511
|
+
if (!file)
|
|
512
|
+
throw new FloomError("Missing file argument.", usageHint);
|
|
513
|
+
return file;
|
|
514
|
+
}
|
|
404
515
|
function sleep(ms, signal) {
|
|
405
516
|
if (signal.aborted)
|
|
406
517
|
return Promise.resolve();
|
|
@@ -456,28 +567,37 @@ async function main() {
|
|
|
456
567
|
try {
|
|
457
568
|
switch (cmd) {
|
|
458
569
|
case undefined:
|
|
570
|
+
usage();
|
|
571
|
+
return;
|
|
459
572
|
case "--help":
|
|
460
573
|
case "-h":
|
|
461
574
|
case "help":
|
|
462
|
-
|
|
575
|
+
commandUsage();
|
|
576
|
+
return;
|
|
577
|
+
case "commands":
|
|
578
|
+
rejectArgs(rest, "Try `floom commands`.");
|
|
579
|
+
commandUsage();
|
|
463
580
|
return;
|
|
464
581
|
case "--version":
|
|
465
582
|
case "-v":
|
|
466
|
-
process.stdout.write(`${
|
|
583
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
467
584
|
return;
|
|
468
585
|
case "login":
|
|
586
|
+
rejectArgs(rest, "Try `floom login`.");
|
|
469
587
|
await login();
|
|
470
588
|
return;
|
|
471
589
|
case "logout":
|
|
590
|
+
rejectArgs(rest, "Try `floom logout`.");
|
|
472
591
|
await deleteConfig();
|
|
473
592
|
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
474
593
|
return;
|
|
475
594
|
case "whoami":
|
|
595
|
+
rejectArgs(rest, "Try `floom whoami`.");
|
|
476
596
|
await whoami();
|
|
477
597
|
return;
|
|
478
598
|
case "init": {
|
|
479
|
-
const
|
|
480
|
-
await init(file);
|
|
599
|
+
const flags = parseInitArgs(rest);
|
|
600
|
+
await init(flags.file);
|
|
481
601
|
return;
|
|
482
602
|
}
|
|
483
603
|
case "publish": {
|
|
@@ -486,6 +606,9 @@ async function main() {
|
|
|
486
606
|
if (!file) {
|
|
487
607
|
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
488
608
|
}
|
|
609
|
+
if (flags.rest.length > 1) {
|
|
610
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
|
|
611
|
+
}
|
|
489
612
|
await publish({
|
|
490
613
|
file,
|
|
491
614
|
visibility: flags.visibility,
|
|
@@ -496,6 +619,11 @@ async function main() {
|
|
|
496
619
|
});
|
|
497
620
|
return;
|
|
498
621
|
}
|
|
622
|
+
case "scan": {
|
|
623
|
+
const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
|
|
624
|
+
await scanSkill(file);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
499
627
|
case "share":
|
|
500
628
|
notAvailable("`floom share`");
|
|
501
629
|
case "list": {
|
|
@@ -524,14 +652,12 @@ async function main() {
|
|
|
524
652
|
}
|
|
525
653
|
case "add":
|
|
526
654
|
case "install": {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
|
|
530
|
-
}
|
|
531
|
-
await install(slug);
|
|
655
|
+
const flags = parseAddArgs(rest);
|
|
656
|
+
await install(flags.slug, flags.target ? { target: flags.target } : {});
|
|
532
657
|
return;
|
|
533
658
|
}
|
|
534
659
|
case "sync":
|
|
660
|
+
rejectArgs(rest, "Try `floom sync`.");
|
|
535
661
|
await sync();
|
|
536
662
|
return;
|
|
537
663
|
case "setup":
|
|
@@ -564,13 +690,18 @@ async function main() {
|
|
|
564
690
|
if (flags.folder === undefined) {
|
|
565
691
|
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
566
692
|
}
|
|
693
|
+
if (flags.rest.length > 1) {
|
|
694
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
|
|
695
|
+
}
|
|
567
696
|
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
568
697
|
return;
|
|
569
698
|
}
|
|
570
699
|
case "mcp":
|
|
700
|
+
rejectArgs(rest, "Try `floom mcp`.");
|
|
571
701
|
printMcpSetup();
|
|
572
702
|
return;
|
|
573
703
|
case "doctor":
|
|
704
|
+
rejectArgs(rest, "Try `floom doctor`.");
|
|
574
705
|
await doctor();
|
|
575
706
|
return;
|
|
576
707
|
default:
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,9 @@ export const DEFAULT_WEB_URL = "https://floom.dev";
|
|
|
8
8
|
export function getApiUrl() {
|
|
9
9
|
return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
|
|
10
10
|
}
|
|
11
|
+
export function resolveApiUrl(cfg) {
|
|
12
|
+
return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? cfg?.apiUrl?.replace(/\/$/, "") ?? DEFAULT_API_URL;
|
|
13
|
+
}
|
|
11
14
|
export function getWebUrl() {
|
|
12
15
|
return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
|
|
13
16
|
}
|
package/dist/delete.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import ora from "ora";
|
|
4
|
-
import {
|
|
4
|
+
import { readConfig, resolveApiUrl } from "./config.js";
|
|
5
5
|
import { deleteRequest } from "./lib/api.js";
|
|
6
6
|
import { c, symbols } from "./ui.js";
|
|
7
7
|
import { FloomError } from "./errors.js";
|
|
@@ -43,7 +43,7 @@ export async function deleteSkill(opts) {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
const apiUrl = cfg
|
|
46
|
+
const apiUrl = resolveApiUrl(cfg);
|
|
47
47
|
const spinner = ora({ text: c.dim(`Deleting ${slug}...`), color: "yellow" }).start();
|
|
48
48
|
try {
|
|
49
49
|
await deleteRequest(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "delete skill", cfg.accessToken);
|
package/dist/doctor.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { stat, readFile, access, readdir, constants } from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
5
|
+
import { floomFetch } from "./lib/api.js";
|
|
5
6
|
import { c, symbols } from "./ui.js";
|
|
6
|
-
|
|
7
|
+
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
7
8
|
function statusBadge(s) {
|
|
8
9
|
if (s === "ok")
|
|
9
10
|
return c.green(symbols.ok);
|
|
@@ -20,10 +21,11 @@ async function checkAuth() {
|
|
|
20
21
|
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
|
-
const apiUrl = cfg
|
|
24
|
+
const apiUrl = resolveApiUrl(cfg);
|
|
24
25
|
try {
|
|
25
|
-
const res = await
|
|
26
|
-
|
|
26
|
+
const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
|
|
27
|
+
token: cfg.accessToken,
|
|
28
|
+
checkOk: false,
|
|
27
29
|
});
|
|
28
30
|
if (res.status === 401) {
|
|
29
31
|
return {
|
|
@@ -196,48 +198,49 @@ async function checkLastSync() {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
async function checkVersion() {
|
|
199
|
-
const apiUrl = (await readConfig())
|
|
201
|
+
const apiUrl = resolveApiUrl(await readConfig());
|
|
200
202
|
try {
|
|
201
|
-
const res = await
|
|
203
|
+
const res = await floomFetch(`${apiUrl}/api/v1/cli-version`, "check CLI version", {
|
|
202
204
|
headers: { accept: "application/json" },
|
|
205
|
+
checkOk: false,
|
|
203
206
|
});
|
|
204
207
|
if (!res.ok) {
|
|
205
208
|
// Endpoint optional — treat as info-only, not a failure
|
|
206
209
|
return {
|
|
207
210
|
name: "Version",
|
|
208
211
|
status: "ok",
|
|
209
|
-
detail: `CLI
|
|
212
|
+
detail: `CLI ${formatVersionLabel(CLI_VERSION)} (server check skipped)`,
|
|
210
213
|
};
|
|
211
214
|
}
|
|
212
215
|
const data = (await res.json());
|
|
213
|
-
if (data.min && CLI_VERSION
|
|
216
|
+
if (data.min && compareSemverish(CLI_VERSION, data.min) < 0) {
|
|
214
217
|
return {
|
|
215
218
|
name: "Version",
|
|
216
219
|
status: "fail",
|
|
217
|
-
detail: `CLI
|
|
220
|
+
detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
|
|
218
221
|
hint: "Run `npm i -g @floomhq/floom` to upgrade.",
|
|
219
222
|
};
|
|
220
223
|
}
|
|
221
|
-
if (data.latest && CLI_VERSION
|
|
224
|
+
if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
|
|
222
225
|
return {
|
|
223
226
|
name: "Version",
|
|
224
227
|
status: "warn",
|
|
225
|
-
detail: `CLI
|
|
228
|
+
detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
|
|
226
229
|
hint: "Run `npm i -g @floomhq/floom` to upgrade.",
|
|
227
230
|
};
|
|
228
231
|
}
|
|
229
|
-
return { name: "Version", status: "ok", detail: `CLI
|
|
232
|
+
return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
|
|
230
233
|
}
|
|
231
234
|
catch {
|
|
232
235
|
return {
|
|
233
236
|
name: "Version",
|
|
234
237
|
status: "ok",
|
|
235
|
-
detail: `CLI
|
|
238
|
+
detail: `CLI ${formatVersionLabel(CLI_VERSION)} (offline)`,
|
|
236
239
|
};
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
242
|
export async function doctor() {
|
|
240
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(
|
|
243
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
|
|
241
244
|
const checks = await Promise.all([
|
|
242
245
|
checkAuth(),
|
|
243
246
|
checkMcp(),
|
package/dist/errors.js
CHANGED
|
@@ -40,8 +40,12 @@ export function friendlyNetwork(err) {
|
|
|
40
40
|
export function printError(err) {
|
|
41
41
|
if (err instanceof FloomError) {
|
|
42
42
|
process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
|
|
43
|
-
if (err.hint)
|
|
44
|
-
|
|
43
|
+
if (err.hint) {
|
|
44
|
+
for (const line of err.hint.split("\n")) {
|
|
45
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
46
|
+
}
|
|
47
|
+
process.stderr.write("\n");
|
|
48
|
+
}
|
|
45
49
|
else
|
|
46
50
|
process.stderr.write("\n");
|
|
47
51
|
return;
|
package/dist/info.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
|
-
import {
|
|
2
|
+
import { readConfig, resolveApiUrl } from "./config.js";
|
|
3
3
|
import { getJson } from "./lib/api.js";
|
|
4
4
|
import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
|
|
5
5
|
import { c, symbols } from "./ui.js";
|
|
6
6
|
import { FloomError } from "./errors.js";
|
|
7
|
+
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7
8
|
function slugFromInput(input) {
|
|
8
9
|
const trimmed = input.trim();
|
|
9
10
|
try {
|
|
@@ -19,8 +20,11 @@ export async function info(opts) {
|
|
|
19
20
|
const slug = slugFromInput(opts.slug);
|
|
20
21
|
if (!slug)
|
|
21
22
|
throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
|
|
23
|
+
if (!SLUG_RE.test(slug)) {
|
|
24
|
+
throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
|
|
25
|
+
}
|
|
22
26
|
const cfg = await readConfig();
|
|
23
|
-
const apiUrl = cfg
|
|
27
|
+
const apiUrl = resolveApiUrl(cfg);
|
|
24
28
|
const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
|
|
25
29
|
let detail;
|
|
26
30
|
try {
|
package/dist/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFile, access } from "node:fs/promises";
|
|
2
|
-
import { resolve, basename } from "node:path";
|
|
2
|
+
import { dirname, resolve, basename } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { c, symbols } from "./ui.js";
|
|
@@ -43,10 +43,29 @@ export async function init(filename) {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
await writeFile(filePath, TEMPLATE, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const code = err.code;
|
|
51
|
+
if (code === "ENOENT") {
|
|
52
|
+
throw new FloomError(`Directory not found: ${dirname(target)}`, "Create the directory first, or choose a filename in the current directory.");
|
|
53
|
+
}
|
|
54
|
+
if (code === "EISDIR") {
|
|
55
|
+
throw new FloomError(`That's a directory, not a file: ${target}`);
|
|
56
|
+
}
|
|
57
|
+
throw new FloomError(`Couldn't create ${target}: ${err.message}`);
|
|
58
|
+
}
|
|
47
59
|
process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
|
|
48
|
-
process.stdout.write(
|
|
49
|
-
process.stdout.write(` ${c.
|
|
60
|
+
process.stdout.write(`\n ${c.bold("Next")}\n`);
|
|
61
|
+
process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
|
|
62
|
+
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`floom scan ${shellQuote(target)}`)}\n`);
|
|
63
|
+
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`floom publish ${shellQuote(target)} --type instruction --public`)}\n\n`);
|
|
64
|
+
}
|
|
65
|
+
function shellQuote(value) {
|
|
66
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
|
67
|
+
return value;
|
|
68
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
50
69
|
}
|
|
51
70
|
async function fileExists(p) {
|
|
52
71
|
try {
|