@floomhq/floom 1.0.3 → 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 -69
- 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 +2 -8
- 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 +16 -8
- 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,79 +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
|
-
|
|
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.dim("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
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")}
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
${c.
|
|
50
|
+
${c.bold("Safety")}
|
|
51
|
+
${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
|
|
47
52
|
|
|
48
|
-
${c.bold("
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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]")}
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
${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
|
|
57
71
|
|
|
58
|
-
${c.dim("
|
|
59
|
-
${c.
|
|
60
|
-
${c.
|
|
61
|
-
${c.
|
|
62
|
-
|
|
63
|
-
|
|
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>")}
|
|
64
78
|
|
|
65
79
|
${c.dim("Account")}
|
|
66
|
-
${c.cyan("login")}
|
|
67
|
-
${c.cyan("list")}
|
|
68
|
-
${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")}
|
|
69
96
|
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
|
|
70
|
-
${c.cyan("
|
|
71
|
-
${c.cyan("
|
|
72
|
-
${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
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
${c.cyan("sync")} Preview: pull published, saved, and library skills
|
|
79
|
-
${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
|
|
80
|
-
${c.cyan("doctor")} Diagnose auth, API, and local setup
|
|
81
|
-
${c.cyan("--help")} Show this help
|
|
82
|
-
${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")}
|
|
83
105
|
|
|
84
|
-
${c.bold("
|
|
85
|
-
${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
|
|
86
110
|
|
|
87
|
-
${c.
|
|
88
|
-
${c.dim("Docs")} https://floom.dev
|
|
89
|
-
${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.")}
|
|
90
112
|
`;
|
|
91
113
|
process.stdout.write(out);
|
|
92
114
|
}
|
|
@@ -112,14 +134,17 @@ function readFlagValue(argv, index, flag) {
|
|
|
112
134
|
}
|
|
113
135
|
function parseFlags(argv) {
|
|
114
136
|
const out = { visibility: "unlisted", update: false, rest: [] };
|
|
137
|
+
let visibilityFlag = null;
|
|
115
138
|
for (let i = 0; i < argv.length; i++) {
|
|
116
139
|
const a = argv[i] ?? "";
|
|
117
|
-
if (a === "--public")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
|
123
148
|
else if (a === "--update") {
|
|
124
149
|
throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
|
|
125
150
|
}
|
|
@@ -142,19 +167,36 @@ function parseFlags(argv) {
|
|
|
142
167
|
out.installsAs = value;
|
|
143
168
|
i = nextIndex;
|
|
144
169
|
}
|
|
145
|
-
else if (a === "--version" || a.startsWith("--version=")) {
|
|
146
|
-
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);
|
|
147
173
|
if (!VERSION_RE.test(value)) {
|
|
148
|
-
throw new FloomError(`Invalid
|
|
174
|
+
throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
149
175
|
}
|
|
150
176
|
out.version = value;
|
|
151
177
|
i = nextIndex;
|
|
152
178
|
}
|
|
179
|
+
else if (a.startsWith("--")) {
|
|
180
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
|
|
181
|
+
}
|
|
153
182
|
else
|
|
154
183
|
out.rest.push(a);
|
|
155
184
|
}
|
|
156
185
|
return out;
|
|
157
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
|
+
}
|
|
158
200
|
function parseListFlags(argv) {
|
|
159
201
|
const out = { json: false };
|
|
160
202
|
for (const a of argv) {
|
|
@@ -163,6 +205,9 @@ function parseListFlags(argv) {
|
|
|
163
205
|
else if (a.startsWith("--")) {
|
|
164
206
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
165
207
|
}
|
|
208
|
+
else {
|
|
209
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
|
|
210
|
+
}
|
|
166
211
|
}
|
|
167
212
|
return out;
|
|
168
213
|
}
|
|
@@ -175,9 +220,38 @@ function parseInfoFlags(argv) {
|
|
|
175
220
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
176
221
|
else if (!out.slug)
|
|
177
222
|
out.slug = a;
|
|
223
|
+
else
|
|
224
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
|
|
178
225
|
}
|
|
179
226
|
return out;
|
|
180
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
|
+
}
|
|
181
255
|
function parseSearchFlags(argv) {
|
|
182
256
|
const out = { json: false };
|
|
183
257
|
const terms = [];
|
|
@@ -217,9 +291,19 @@ function parseDeleteFlags(argv) {
|
|
|
217
291
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
218
292
|
else if (!out.slug)
|
|
219
293
|
out.slug = a;
|
|
294
|
+
else
|
|
295
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
220
296
|
}
|
|
221
297
|
return out;
|
|
222
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
|
+
}
|
|
223
307
|
function parseSetupFlags(argv) {
|
|
224
308
|
const out = { dryRun: false, yes: false };
|
|
225
309
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -343,6 +427,9 @@ async function runLibrary(argv) {
|
|
|
343
427
|
if (!librarySlug || !skillSlug) {
|
|
344
428
|
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
345
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
|
+
}
|
|
346
433
|
await libraryAddSkill({
|
|
347
434
|
librarySlug,
|
|
348
435
|
skillSlug,
|
|
@@ -357,6 +444,9 @@ async function runLibrary(argv) {
|
|
|
357
444
|
if (!librarySlug || !skillSlug) {
|
|
358
445
|
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
359
446
|
}
|
|
447
|
+
if (rest.length > 2) {
|
|
448
|
+
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
|
|
449
|
+
}
|
|
360
450
|
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
361
451
|
return;
|
|
362
452
|
}
|
|
@@ -364,6 +454,9 @@ async function runLibrary(argv) {
|
|
|
364
454
|
const slug = rest[0];
|
|
365
455
|
if (!slug)
|
|
366
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
|
+
}
|
|
367
460
|
await librarySubscribe(slug);
|
|
368
461
|
return;
|
|
369
462
|
}
|
|
@@ -371,6 +464,9 @@ async function runLibrary(argv) {
|
|
|
371
464
|
const slug = rest[0];
|
|
372
465
|
if (!slug)
|
|
373
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
|
+
}
|
|
374
470
|
await libraryUnsubscribe(slug);
|
|
375
471
|
return;
|
|
376
472
|
}
|
|
@@ -403,6 +499,19 @@ function parseWatchFlags(argv) {
|
|
|
403
499
|
function notAvailable(feature) {
|
|
404
500
|
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
405
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
|
+
}
|
|
406
515
|
function sleep(ms, signal) {
|
|
407
516
|
if (signal.aborted)
|
|
408
517
|
return Promise.resolve();
|
|
@@ -458,28 +567,37 @@ async function main() {
|
|
|
458
567
|
try {
|
|
459
568
|
switch (cmd) {
|
|
460
569
|
case undefined:
|
|
570
|
+
usage();
|
|
571
|
+
return;
|
|
461
572
|
case "--help":
|
|
462
573
|
case "-h":
|
|
463
574
|
case "help":
|
|
464
|
-
|
|
575
|
+
commandUsage();
|
|
576
|
+
return;
|
|
577
|
+
case "commands":
|
|
578
|
+
rejectArgs(rest, "Try `floom commands`.");
|
|
579
|
+
commandUsage();
|
|
465
580
|
return;
|
|
466
581
|
case "--version":
|
|
467
582
|
case "-v":
|
|
468
|
-
process.stdout.write(`${
|
|
583
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
469
584
|
return;
|
|
470
585
|
case "login":
|
|
586
|
+
rejectArgs(rest, "Try `floom login`.");
|
|
471
587
|
await login();
|
|
472
588
|
return;
|
|
473
589
|
case "logout":
|
|
590
|
+
rejectArgs(rest, "Try `floom logout`.");
|
|
474
591
|
await deleteConfig();
|
|
475
592
|
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
476
593
|
return;
|
|
477
594
|
case "whoami":
|
|
595
|
+
rejectArgs(rest, "Try `floom whoami`.");
|
|
478
596
|
await whoami();
|
|
479
597
|
return;
|
|
480
598
|
case "init": {
|
|
481
|
-
const
|
|
482
|
-
await init(file);
|
|
599
|
+
const flags = parseInitArgs(rest);
|
|
600
|
+
await init(flags.file);
|
|
483
601
|
return;
|
|
484
602
|
}
|
|
485
603
|
case "publish": {
|
|
@@ -488,6 +606,9 @@ async function main() {
|
|
|
488
606
|
if (!file) {
|
|
489
607
|
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
490
608
|
}
|
|
609
|
+
if (flags.rest.length > 1) {
|
|
610
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
|
|
611
|
+
}
|
|
491
612
|
await publish({
|
|
492
613
|
file,
|
|
493
614
|
visibility: flags.visibility,
|
|
@@ -498,6 +619,11 @@ async function main() {
|
|
|
498
619
|
});
|
|
499
620
|
return;
|
|
500
621
|
}
|
|
622
|
+
case "scan": {
|
|
623
|
+
const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
|
|
624
|
+
await scanSkill(file);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
501
627
|
case "share":
|
|
502
628
|
notAvailable("`floom share`");
|
|
503
629
|
case "list": {
|
|
@@ -526,14 +652,12 @@ async function main() {
|
|
|
526
652
|
}
|
|
527
653
|
case "add":
|
|
528
654
|
case "install": {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
|
|
532
|
-
}
|
|
533
|
-
await install(slug);
|
|
655
|
+
const flags = parseAddArgs(rest);
|
|
656
|
+
await install(flags.slug, flags.target ? { target: flags.target } : {});
|
|
534
657
|
return;
|
|
535
658
|
}
|
|
536
659
|
case "sync":
|
|
660
|
+
rejectArgs(rest, "Try `floom sync`.");
|
|
537
661
|
await sync();
|
|
538
662
|
return;
|
|
539
663
|
case "setup":
|
|
@@ -566,13 +690,18 @@ async function main() {
|
|
|
566
690
|
if (flags.folder === undefined) {
|
|
567
691
|
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
568
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
|
+
}
|
|
569
696
|
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
570
697
|
return;
|
|
571
698
|
}
|
|
572
699
|
case "mcp":
|
|
700
|
+
rejectArgs(rest, "Try `floom mcp`.");
|
|
573
701
|
printMcpSetup();
|
|
574
702
|
return;
|
|
575
703
|
case "doctor":
|
|
704
|
+
rejectArgs(rest, "Try `floom doctor`.");
|
|
576
705
|
await doctor();
|
|
577
706
|
return;
|
|
578
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 {
|