@floomhq/floom 1.0.3 → 1.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/README.md +2 -0
- package/dist/cli.js +248 -70
- 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/README.md
CHANGED
|
@@ -7,6 +7,7 @@ npm install -g @floomhq/floom
|
|
|
7
7
|
floom init my-skill.md
|
|
8
8
|
floom login
|
|
9
9
|
floom publish my-skill.md
|
|
10
|
+
floom share my-skill --add teammate@example.com
|
|
10
11
|
floom search review
|
|
11
12
|
floom add awesome-skill
|
|
12
13
|
floom setup --target claude --dry-run
|
|
@@ -21,6 +22,7 @@ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the UR
|
|
|
21
22
|
- `floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
|
|
22
23
|
- `floom init [file.md]` — create a starter skill file.
|
|
23
24
|
- `floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--version <label>`.
|
|
25
|
+
- `floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
|
|
24
26
|
- `floom list` — show your published skills. Optional `--json`.
|
|
25
27
|
- `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
|
|
26
28
|
- `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
package/dist/cli.js
CHANGED
|
@@ -14,79 +14,104 @@ 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";
|
|
18
|
+
import { share } from "./share.js";
|
|
17
19
|
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
18
20
|
import { c, symbols } from "./ui.js";
|
|
19
21
|
import { printError, FloomError } from "./errors.js";
|
|
20
|
-
|
|
21
|
-
const PKG = { name: "@floomhq/floom", version:
|
|
22
|
+
import { CLI_VERSION } from "./version.js";
|
|
23
|
+
const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
|
|
22
24
|
const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
|
|
23
25
|
function usage() {
|
|
24
26
|
const out = `
|
|
25
|
-
${c.coral("
|
|
26
|
-
${c.coral(" /
|
|
27
|
-
${c.coral("
|
|
28
|
-
${c.coral("
|
|
27
|
+
${c.coral(" ________")}
|
|
28
|
+
${c.coral(" / ____/ /___ ____ ____ ___")} ${c.dim(`v${CLI_VERSION}`)}
|
|
29
|
+
${c.coral(" / /_ / / __ \\/ __ \\/ __ `__ \\")}
|
|
30
|
+
${c.coral(" / __/ / / /_/ / /_/ / / / / / /")}
|
|
31
|
+
${c.coral(" /_/ /_/\\____/\\____/_/ /_/ /_/")}
|
|
29
32
|
|
|
30
33
|
${c.bold("Share AI agent skills with a link.")}
|
|
31
34
|
${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
|
|
32
35
|
|
|
33
|
-
${c.bold("
|
|
34
|
-
${c.
|
|
36
|
+
${c.bold("Do this next")}
|
|
37
|
+
${c.dim("1. Add a skill someone sent you")}
|
|
38
|
+
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --target claude")}
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
${c.dim("2. Publish your own skill")}
|
|
41
|
+
${c.cyan("npx -y @floomhq/floom init")} ${c.dim("support-tone.md")}
|
|
42
|
+
${c.dim("# edit support-tone.md")}
|
|
43
|
+
${c.cyan("npx -y @floomhq/floom scan")} ${c.dim("support-tone.md")}
|
|
44
|
+
${c.cyan("npx -y @floomhq/floom login")}
|
|
45
|
+
${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
39
46
|
|
|
40
|
-
${c.dim("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
47
|
+
${c.dim("3. Tell your agent where skills land")}
|
|
48
|
+
${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
|
|
49
|
+
${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target codex --yes")}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
${c.
|
|
51
|
+
${c.bold("Safety")}
|
|
52
|
+
${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
|
|
47
53
|
|
|
48
|
-
${c.bold("
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
${c.bold("More")}
|
|
55
|
+
${c.cyan("floom commands")} ${c.dim("Full command list")}
|
|
56
|
+
${c.cyan("floom doctor")} ${c.dim("Check auth, API, and local folders")}
|
|
57
|
+
${c.dim("Docs")} https://floom.dev
|
|
58
|
+
`;
|
|
59
|
+
process.stdout.write(out);
|
|
60
|
+
}
|
|
61
|
+
function commandUsage() {
|
|
62
|
+
const out = `
|
|
63
|
+
${c.bold("Usage:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
${c.cyan("
|
|
65
|
+
${c.bold("Commands")}
|
|
66
|
+
${c.dim("Skills")}
|
|
67
|
+
${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
|
|
68
|
+
${c.dim("Alias: install")}
|
|
69
|
+
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
70
|
+
${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
|
|
71
|
+
${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
|
|
57
72
|
|
|
58
|
-
${c.dim("
|
|
59
|
-
${c.
|
|
60
|
-
${c.
|
|
61
|
-
${c.
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
${c.dim("Publishing")}
|
|
74
|
+
${c.cyan("init")} ${c.dim("[path]")} Create a skill scaffold
|
|
75
|
+
${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
|
|
76
|
+
${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
|
|
77
|
+
${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
|
|
78
|
+
${c.dim(" --skill-version <label>")}
|
|
79
|
+
${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
|
|
80
|
+
${c.dim("Flags: --add <email>, --remove <email>, --list")}
|
|
64
81
|
|
|
65
82
|
${c.dim("Account")}
|
|
66
|
-
${c.cyan("login")}
|
|
67
|
-
${c.cyan("list")}
|
|
68
|
-
${c.cyan("
|
|
83
|
+
${c.cyan("login")} Authenticate
|
|
84
|
+
${c.cyan("list")} Your published skills
|
|
85
|
+
${c.cyan("delete")} ${c.dim("<url>")} Delete one of your skills
|
|
86
|
+
${c.dim("Alias: rm")}
|
|
87
|
+
${c.cyan("whoami")} Show the signed-in account
|
|
88
|
+
${c.cyan("logout")} Switch accounts or remove local credentials
|
|
89
|
+
|
|
90
|
+
${c.dim("Agent setup")}
|
|
91
|
+
${c.cyan("setup")} Configure Claude Code or Codex instructions
|
|
92
|
+
${c.dim("Alias: connect")}
|
|
93
|
+
${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
|
|
94
|
+
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
95
|
+
|
|
96
|
+
${c.dim("Advanced")}
|
|
97
|
+
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
98
|
+
${c.dim("Alias: lib")}
|
|
69
99
|
${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("
|
|
100
|
+
${c.cyan("mcp")} Print optional MCP setup guidance
|
|
101
|
+
${c.cyan("sync")} Preview pull of published, saved, and library skills
|
|
102
|
+
${c.cyan("watch")} Preview polling sync loop
|
|
73
103
|
|
|
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
|
|
104
|
+
${c.bold("Examples")}
|
|
105
|
+
${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
|
|
106
|
+
${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
107
|
+
${c.cyan("floom setup")} ${c.dim("--target claude --yes")}
|
|
83
108
|
|
|
84
|
-
${c.bold("
|
|
85
|
-
${c.cyan("
|
|
109
|
+
${c.bold("Help")}
|
|
110
|
+
${c.cyan("floom commands")} Show this reference
|
|
111
|
+
${c.cyan("--help")} Show this reference
|
|
112
|
+
${c.cyan("--version")} Show CLI version
|
|
86
113
|
|
|
87
|
-
${c.
|
|
88
|
-
${c.dim("Docs")} https://floom.dev
|
|
89
|
-
${c.dim("Source")} https://github.com/floomhq/floom
|
|
114
|
+
${c.dim("Run")} ${c.cyan("floom")} ${c.dim("with no command for the guided start screen.")}
|
|
90
115
|
`;
|
|
91
116
|
process.stdout.write(out);
|
|
92
117
|
}
|
|
@@ -112,14 +137,17 @@ function readFlagValue(argv, index, flag) {
|
|
|
112
137
|
}
|
|
113
138
|
function parseFlags(argv) {
|
|
114
139
|
const out = { visibility: "unlisted", update: false, rest: [] };
|
|
140
|
+
let visibilityFlag = null;
|
|
115
141
|
for (let i = 0; i < argv.length; i++) {
|
|
116
142
|
const a = argv[i] ?? "";
|
|
117
|
-
if (a === "--public")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
if (a === "--public" || a === "--private" || a === "--unlisted") {
|
|
144
|
+
const nextVisibility = a.slice(2);
|
|
145
|
+
if (visibilityFlag && visibilityFlag !== nextVisibility) {
|
|
146
|
+
throw new FloomError("Conflicting visibility flags.", "Use only one of: --public, --private, or --unlisted.");
|
|
147
|
+
}
|
|
148
|
+
visibilityFlag = nextVisibility;
|
|
149
|
+
out.visibility = nextVisibility;
|
|
150
|
+
}
|
|
123
151
|
else if (a === "--update") {
|
|
124
152
|
throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
|
|
125
153
|
}
|
|
@@ -142,19 +170,73 @@ function parseFlags(argv) {
|
|
|
142
170
|
out.installsAs = value;
|
|
143
171
|
i = nextIndex;
|
|
144
172
|
}
|
|
145
|
-
else if (a === "--version" || a.startsWith("--version=")) {
|
|
146
|
-
const
|
|
173
|
+
else if (a === "--skill-version" || a.startsWith("--skill-version=") || a === "--version" || a.startsWith("--version=")) {
|
|
174
|
+
const flagName = a.startsWith("--skill-version") ? "--skill-version" : "--version";
|
|
175
|
+
const { value, nextIndex } = readFlagValue(argv, i, flagName);
|
|
147
176
|
if (!VERSION_RE.test(value)) {
|
|
148
|
-
throw new FloomError(`Invalid
|
|
177
|
+
throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
149
178
|
}
|
|
150
179
|
out.version = value;
|
|
151
180
|
i = nextIndex;
|
|
152
181
|
}
|
|
182
|
+
else if (a.startsWith("--")) {
|
|
183
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
|
|
184
|
+
}
|
|
153
185
|
else
|
|
154
186
|
out.rest.push(a);
|
|
155
187
|
}
|
|
156
188
|
return out;
|
|
157
189
|
}
|
|
190
|
+
function parseShareFlags(argv) {
|
|
191
|
+
const out = { list: false, add: [], remove: [] };
|
|
192
|
+
for (let i = 0; i < argv.length; i++) {
|
|
193
|
+
const a = argv[i] ?? "";
|
|
194
|
+
if (a === "--list") {
|
|
195
|
+
out.list = true;
|
|
196
|
+
}
|
|
197
|
+
else if (a === "--add" || a.startsWith("--add=")) {
|
|
198
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--add");
|
|
199
|
+
out.add.push(value);
|
|
200
|
+
i = nextIndex;
|
|
201
|
+
}
|
|
202
|
+
else if (a === "--remove" || a.startsWith("--remove=")) {
|
|
203
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--remove");
|
|
204
|
+
out.remove.push(value);
|
|
205
|
+
i = nextIndex;
|
|
206
|
+
}
|
|
207
|
+
else if (a.startsWith("--")) {
|
|
208
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
209
|
+
}
|
|
210
|
+
else if (!out.slug) {
|
|
211
|
+
out.slug = a;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!out.slug)
|
|
218
|
+
throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
|
|
219
|
+
if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
|
|
220
|
+
throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
|
|
221
|
+
}
|
|
222
|
+
if (!out.list && out.add.length === 0 && out.remove.length === 0) {
|
|
223
|
+
throw new FloomError("Missing share action.", "Try `floom share <slug> --add person@example.com`.");
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
function parseInitArgs(argv) {
|
|
228
|
+
let file;
|
|
229
|
+
for (const a of argv) {
|
|
230
|
+
if (a.startsWith("--")) {
|
|
231
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md`.");
|
|
232
|
+
}
|
|
233
|
+
if (file) {
|
|
234
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
|
|
235
|
+
}
|
|
236
|
+
file = a;
|
|
237
|
+
}
|
|
238
|
+
return file ? { file } : {};
|
|
239
|
+
}
|
|
158
240
|
function parseListFlags(argv) {
|
|
159
241
|
const out = { json: false };
|
|
160
242
|
for (const a of argv) {
|
|
@@ -163,6 +245,9 @@ function parseListFlags(argv) {
|
|
|
163
245
|
else if (a.startsWith("--")) {
|
|
164
246
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
165
247
|
}
|
|
248
|
+
else {
|
|
249
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
|
|
250
|
+
}
|
|
166
251
|
}
|
|
167
252
|
return out;
|
|
168
253
|
}
|
|
@@ -175,9 +260,38 @@ function parseInfoFlags(argv) {
|
|
|
175
260
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
176
261
|
else if (!out.slug)
|
|
177
262
|
out.slug = a;
|
|
263
|
+
else
|
|
264
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
|
|
178
265
|
}
|
|
179
266
|
return out;
|
|
180
267
|
}
|
|
268
|
+
function parseAddArgs(argv) {
|
|
269
|
+
let slug;
|
|
270
|
+
let target;
|
|
271
|
+
for (let i = 0; i < argv.length; i++) {
|
|
272
|
+
const a = argv[i] ?? "";
|
|
273
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
274
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
275
|
+
if (value !== "claude" && value !== "codex") {
|
|
276
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
277
|
+
}
|
|
278
|
+
target = value;
|
|
279
|
+
i = nextIndex;
|
|
280
|
+
}
|
|
281
|
+
else if (a.startsWith("--")) {
|
|
282
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
|
|
283
|
+
}
|
|
284
|
+
else if (slug) {
|
|
285
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
slug = a;
|
|
289
|
+
}
|
|
290
|
+
if (!slug) {
|
|
291
|
+
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --target claude`");
|
|
292
|
+
}
|
|
293
|
+
return target ? { slug, target } : { slug };
|
|
294
|
+
}
|
|
181
295
|
function parseSearchFlags(argv) {
|
|
182
296
|
const out = { json: false };
|
|
183
297
|
const terms = [];
|
|
@@ -217,9 +331,19 @@ function parseDeleteFlags(argv) {
|
|
|
217
331
|
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
218
332
|
else if (!out.slug)
|
|
219
333
|
out.slug = a;
|
|
334
|
+
else
|
|
335
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
220
336
|
}
|
|
221
337
|
return out;
|
|
222
338
|
}
|
|
339
|
+
function rejectArgs(argv, usageHint) {
|
|
340
|
+
const arg = argv[0];
|
|
341
|
+
if (!arg)
|
|
342
|
+
return;
|
|
343
|
+
if (arg.startsWith("--"))
|
|
344
|
+
throw new FloomError(`Unknown flag: ${arg}`, usageHint);
|
|
345
|
+
throw new FloomError(`Unexpected argument: ${arg}`, usageHint);
|
|
346
|
+
}
|
|
223
347
|
function parseSetupFlags(argv) {
|
|
224
348
|
const out = { dryRun: false, yes: false };
|
|
225
349
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -343,6 +467,9 @@ async function runLibrary(argv) {
|
|
|
343
467
|
if (!librarySlug || !skillSlug) {
|
|
344
468
|
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
345
469
|
}
|
|
470
|
+
if (flags.rest.length > 2) {
|
|
471
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
472
|
+
}
|
|
346
473
|
await libraryAddSkill({
|
|
347
474
|
librarySlug,
|
|
348
475
|
skillSlug,
|
|
@@ -357,6 +484,9 @@ async function runLibrary(argv) {
|
|
|
357
484
|
if (!librarySlug || !skillSlug) {
|
|
358
485
|
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
359
486
|
}
|
|
487
|
+
if (rest.length > 2) {
|
|
488
|
+
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
|
|
489
|
+
}
|
|
360
490
|
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
361
491
|
return;
|
|
362
492
|
}
|
|
@@ -364,6 +494,9 @@ async function runLibrary(argv) {
|
|
|
364
494
|
const slug = rest[0];
|
|
365
495
|
if (!slug)
|
|
366
496
|
throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
|
|
497
|
+
if (rest.length > 1) {
|
|
498
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
|
|
499
|
+
}
|
|
367
500
|
await librarySubscribe(slug);
|
|
368
501
|
return;
|
|
369
502
|
}
|
|
@@ -371,6 +504,9 @@ async function runLibrary(argv) {
|
|
|
371
504
|
const slug = rest[0];
|
|
372
505
|
if (!slug)
|
|
373
506
|
throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
|
|
507
|
+
if (rest.length > 1) {
|
|
508
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
|
|
509
|
+
}
|
|
374
510
|
await libraryUnsubscribe(slug);
|
|
375
511
|
return;
|
|
376
512
|
}
|
|
@@ -403,6 +539,19 @@ function parseWatchFlags(argv) {
|
|
|
403
539
|
function notAvailable(feature) {
|
|
404
540
|
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
405
541
|
}
|
|
542
|
+
function parseSingleFileArg(argv, usageHint) {
|
|
543
|
+
let file;
|
|
544
|
+
for (const a of argv) {
|
|
545
|
+
if (a.startsWith("--"))
|
|
546
|
+
throw new FloomError(`Unknown flag: ${a}`, usageHint);
|
|
547
|
+
if (file)
|
|
548
|
+
throw new FloomError(`Unexpected argument: ${a}`, usageHint);
|
|
549
|
+
file = a;
|
|
550
|
+
}
|
|
551
|
+
if (!file)
|
|
552
|
+
throw new FloomError("Missing file argument.", usageHint);
|
|
553
|
+
return file;
|
|
554
|
+
}
|
|
406
555
|
function sleep(ms, signal) {
|
|
407
556
|
if (signal.aborted)
|
|
408
557
|
return Promise.resolve();
|
|
@@ -458,28 +607,37 @@ async function main() {
|
|
|
458
607
|
try {
|
|
459
608
|
switch (cmd) {
|
|
460
609
|
case undefined:
|
|
610
|
+
usage();
|
|
611
|
+
return;
|
|
461
612
|
case "--help":
|
|
462
613
|
case "-h":
|
|
463
614
|
case "help":
|
|
464
|
-
|
|
615
|
+
commandUsage();
|
|
616
|
+
return;
|
|
617
|
+
case "commands":
|
|
618
|
+
rejectArgs(rest, "Try `floom commands`.");
|
|
619
|
+
commandUsage();
|
|
465
620
|
return;
|
|
466
621
|
case "--version":
|
|
467
622
|
case "-v":
|
|
468
|
-
process.stdout.write(`${
|
|
623
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
469
624
|
return;
|
|
470
625
|
case "login":
|
|
626
|
+
rejectArgs(rest, "Try `floom login`.");
|
|
471
627
|
await login();
|
|
472
628
|
return;
|
|
473
629
|
case "logout":
|
|
630
|
+
rejectArgs(rest, "Try `floom logout`.");
|
|
474
631
|
await deleteConfig();
|
|
475
632
|
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
476
633
|
return;
|
|
477
634
|
case "whoami":
|
|
635
|
+
rejectArgs(rest, "Try `floom whoami`.");
|
|
478
636
|
await whoami();
|
|
479
637
|
return;
|
|
480
638
|
case "init": {
|
|
481
|
-
const
|
|
482
|
-
await init(file);
|
|
639
|
+
const flags = parseInitArgs(rest);
|
|
640
|
+
await init(flags.file);
|
|
483
641
|
return;
|
|
484
642
|
}
|
|
485
643
|
case "publish": {
|
|
@@ -488,6 +646,9 @@ async function main() {
|
|
|
488
646
|
if (!file) {
|
|
489
647
|
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
490
648
|
}
|
|
649
|
+
if (flags.rest.length > 1) {
|
|
650
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
|
|
651
|
+
}
|
|
491
652
|
await publish({
|
|
492
653
|
file,
|
|
493
654
|
visibility: flags.visibility,
|
|
@@ -498,8 +659,22 @@ async function main() {
|
|
|
498
659
|
});
|
|
499
660
|
return;
|
|
500
661
|
}
|
|
662
|
+
case "scan": {
|
|
663
|
+
const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
|
|
664
|
+
await scanSkill(file);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
501
667
|
case "share":
|
|
502
|
-
|
|
668
|
+
{
|
|
669
|
+
const flags = parseShareFlags(rest);
|
|
670
|
+
if (flags.list) {
|
|
671
|
+
await share({ slug: flags.slug ?? "", kind: "list" });
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
await share({ slug: flags.slug ?? "", kind: "patch", add: flags.add, remove: flags.remove });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
503
678
|
case "list": {
|
|
504
679
|
const flags = parseListFlags(rest);
|
|
505
680
|
await list(flags);
|
|
@@ -526,14 +701,12 @@ async function main() {
|
|
|
526
701
|
}
|
|
527
702
|
case "add":
|
|
528
703
|
case "install": {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
|
|
532
|
-
}
|
|
533
|
-
await install(slug);
|
|
704
|
+
const flags = parseAddArgs(rest);
|
|
705
|
+
await install(flags.slug, flags.target ? { target: flags.target } : {});
|
|
534
706
|
return;
|
|
535
707
|
}
|
|
536
708
|
case "sync":
|
|
709
|
+
rejectArgs(rest, "Try `floom sync`.");
|
|
537
710
|
await sync();
|
|
538
711
|
return;
|
|
539
712
|
case "setup":
|
|
@@ -566,13 +739,18 @@ async function main() {
|
|
|
566
739
|
if (flags.folder === undefined) {
|
|
567
740
|
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
568
741
|
}
|
|
742
|
+
if (flags.rest.length > 1) {
|
|
743
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
|
|
744
|
+
}
|
|
569
745
|
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
570
746
|
return;
|
|
571
747
|
}
|
|
572
748
|
case "mcp":
|
|
749
|
+
rejectArgs(rest, "Try `floom mcp`.");
|
|
573
750
|
printMcpSetup();
|
|
574
751
|
return;
|
|
575
752
|
case "doctor":
|
|
753
|
+
rejectArgs(rest, "Try `floom doctor`.");
|
|
576
754
|
await doctor();
|
|
577
755
|
return;
|
|
578
756
|
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;
|