@floomhq/floom 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/cli.js +133 -65
- package/dist/delete.js +2 -2
- package/dist/doctor.js +21 -8
- package/dist/errors.js +2 -2
- package/dist/info.js +2 -2
- package/dist/install.js +20 -8
- package/dist/library.js +7 -7
- package/dist/list.js +2 -2
- package/dist/login.js +14 -3
- package/dist/mcp.js +2 -2
- package/dist/publish.js +1 -1
- package/dist/secrets.js +26 -3
- package/dist/setup.js +8 -8
- package/dist/share.js +2 -2
- package/dist/sync.js +2 -2
- package/dist/whoami.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,12 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
27
27
|
- `npx -y @floomhq/floom publish <file.md>` — scan and upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, `--skill-version <label>`, and `--share <email>`. `--share` sends the normal link by email; no account is needed to add unlisted or public links.
|
|
28
28
|
- `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
|
|
29
29
|
- `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
|
|
30
|
-
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`. Optional `--setup` connects Claude Code
|
|
30
|
+
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`. Optional `--setup` connects Claude Code, `--force` / `--update` replaces an existing local copy, and `--json` prints machine-readable output.
|
|
31
|
+
- `npx -y @floomhq/floom update <url-or-slug>` — install the latest remote skill content, replacing the local copy.
|
|
31
32
|
- `npx -y @floomhq/floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
32
33
|
- `npx -y @floomhq/floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
|
|
33
34
|
- `npx -y @floomhq/floom agent-prompt` — print the sentence to paste into Claude Code or Codex.
|
|
34
|
-
- `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes
|
|
35
|
+
- `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`, `--file <path>`.
|
|
35
36
|
- `npx -y @floomhq/floom connect` — alias for setup.
|
|
36
37
|
- `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
|
|
37
38
|
- `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into `~/.claude/skills/`.
|
|
@@ -42,9 +43,9 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
42
43
|
- `npx -y @floomhq/floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
|
|
43
44
|
- `npx -y @floomhq/floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
|
|
44
45
|
- `npx -y @floomhq/floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
|
|
45
|
-
- `npx -y @floomhq/floom doctor` — diagnose your Floom setup.
|
|
46
|
-
- `floom whoami` — show the signed-in account.
|
|
47
|
-
- `floom logout` — delete local credentials.
|
|
46
|
+
- `npx -y @floomhq/floom doctor` — diagnose your Floom setup. Optional `--json`.
|
|
47
|
+
- `npx -y @floomhq/floom whoami` — show the signed-in account.
|
|
48
|
+
- `npx -y @floomhq/floom logout` — delete local credentials.
|
|
48
49
|
|
|
49
50
|
## Skill format
|
|
50
51
|
|
|
@@ -65,7 +66,7 @@ version: 0.1.0
|
|
|
65
66
|
|
|
66
67
|
Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
|
|
67
68
|
|
|
68
|
-
`floom sync` and `floom watch` are Version 1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
|
|
69
|
+
`npx -y @floomhq/floom sync` and `npx -y @floomhq/floom watch` are Version 1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
|
|
69
70
|
The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
|
|
70
71
|
only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
|
|
71
72
|
conflicts. Symlinks are never followed. To replace a local skill manually, run
|
package/dist/cli.js
CHANGED
|
@@ -63,14 +63,16 @@ function usage() {
|
|
|
63
63
|
}
|
|
64
64
|
function commandUsage() {
|
|
65
65
|
const out = `
|
|
66
|
-
${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [
|
|
67
|
-
${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [
|
|
66
|
+
${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [args]")}
|
|
67
|
+
${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [args]")}
|
|
68
68
|
|
|
69
69
|
${c.bold("Commands")}
|
|
70
70
|
${c.dim("Skills")}
|
|
71
71
|
${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
|
|
72
72
|
${c.dim("Alias: install")}
|
|
73
|
-
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
|
|
73
|
+
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --update, --json")}
|
|
74
|
+
${c.cyan("update")} ${c.dim("<url>")} Install the latest remote skill content, replacing the local copy
|
|
75
|
+
${c.dim("Alias: add --force")}
|
|
74
76
|
${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
|
|
75
77
|
${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
|
|
76
78
|
|
|
@@ -96,10 +98,13 @@ function commandUsage() {
|
|
|
96
98
|
${c.dim("Agent setup")}
|
|
97
99
|
${c.cyan("setup")} Configure Claude Code or Codex instructions
|
|
98
100
|
${c.dim("Alias: connect")}
|
|
99
|
-
${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
|
|
101
|
+
${c.dim("Flags: --target claude|codex, --yes, --dry-run, --file <path>")}
|
|
102
|
+
${c.dim(" From a repo, setup writes that repo's CLAUDE.md/AGENTS.md")}
|
|
100
103
|
${c.cyan("agent-prompt")} Print the sentence to paste into your agent
|
|
101
104
|
${c.dim("Alias: paste")}
|
|
105
|
+
${c.dim("Flags: --target claude|codex")}
|
|
102
106
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
107
|
+
${c.dim("Flags: --json")}
|
|
103
108
|
|
|
104
109
|
${c.dim("Advanced")}
|
|
105
110
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
@@ -126,6 +131,7 @@ function commandUsage() {
|
|
|
126
131
|
}
|
|
127
132
|
const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
|
|
128
133
|
const INIT_TEMPLATES = new Set(["generic", "brand-voice", "pr-review", "sales", "support", "onboarding"]);
|
|
134
|
+
const MAX_SHARE_RECIPIENTS = 25;
|
|
129
135
|
const INSTALL_TARGETS = new Set([
|
|
130
136
|
"claude_skill",
|
|
131
137
|
"memory",
|
|
@@ -160,7 +166,7 @@ function parseFlags(argv) {
|
|
|
160
166
|
out.explicitVisibility = true;
|
|
161
167
|
}
|
|
162
168
|
else if (a === "--update") {
|
|
163
|
-
throw new FloomError(
|
|
169
|
+
throw new FloomError("Publish updates are not available in Floom Version 1.", "Publish as a new skill for now. Use `npx -y @floomhq/floom update <link>` to refresh installed local copies.");
|
|
164
170
|
}
|
|
165
171
|
else if (a === "--share" || a.startsWith("--share=")) {
|
|
166
172
|
const { value, nextIndex } = readFlagValue(argv, i, "--share");
|
|
@@ -195,18 +201,18 @@ function parseFlags(argv) {
|
|
|
195
201
|
throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
|
|
196
202
|
}
|
|
197
203
|
else if (a.startsWith("--")) {
|
|
198
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
|
|
204
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom publish skill.md --type instruction --public`.");
|
|
199
205
|
}
|
|
200
206
|
else
|
|
201
207
|
out.rest.push(a);
|
|
202
208
|
}
|
|
203
209
|
if (out.shareEmails.length > 0) {
|
|
204
210
|
out.shareEmails = dedupeEmails(out.shareEmails);
|
|
205
|
-
if (out.shareEmails.length >
|
|
206
|
-
throw new FloomError("Too many --share recipients.",
|
|
211
|
+
if (out.shareEmails.length > MAX_SHARE_RECIPIENTS) {
|
|
212
|
+
throw new FloomError("Too many --share recipients.", `Use ${MAX_SHARE_RECIPIENTS} email addresses or fewer.`);
|
|
207
213
|
}
|
|
208
214
|
if (out.visibility === "private") {
|
|
209
|
-
throw new FloomError("`--private --share` would email a link recipients cannot open.", "Use `--unlisted --share` for invite emails, or `floom share <slug> --add <email>` for email-gated access after publishing.");
|
|
215
|
+
throw new FloomError("`--private --share` would email a link recipients cannot open.", "Use `--unlisted --share` for invite emails, or `npx -y @floomhq/floom share <slug> --add <email>` for email-gated access after publishing.");
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
return out;
|
|
@@ -243,22 +249,22 @@ function parseShareFlags(argv) {
|
|
|
243
249
|
i = nextIndex;
|
|
244
250
|
}
|
|
245
251
|
else if (a.startsWith("--")) {
|
|
246
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
252
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
247
253
|
}
|
|
248
254
|
else if (!out.slug) {
|
|
249
255
|
out.slug = a;
|
|
250
256
|
}
|
|
251
257
|
else {
|
|
252
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
258
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
if (!out.slug)
|
|
256
|
-
throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
|
|
262
|
+
throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
257
263
|
if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
|
|
258
264
|
throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
|
|
259
265
|
}
|
|
260
266
|
if (!out.list && out.add.length === 0 && out.remove.length === 0) {
|
|
261
|
-
throw new FloomError("Missing share action.", "Try `floom share <slug> --add person@example.com`.");
|
|
267
|
+
throw new FloomError("Missing share action.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
262
268
|
}
|
|
263
269
|
return out;
|
|
264
270
|
}
|
|
@@ -277,10 +283,10 @@ function parseInitArgs(argv) {
|
|
|
277
283
|
continue;
|
|
278
284
|
}
|
|
279
285
|
if (a.startsWith("--")) {
|
|
280
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md --template brand-voice`.");
|
|
286
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom init skill.md --template brand-voice`.");
|
|
281
287
|
}
|
|
282
288
|
if (file) {
|
|
283
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
|
|
289
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom init skill.md`.");
|
|
284
290
|
}
|
|
285
291
|
file = a;
|
|
286
292
|
}
|
|
@@ -292,10 +298,10 @@ function parseListFlags(argv) {
|
|
|
292
298
|
if (a === "--json")
|
|
293
299
|
out.json = true;
|
|
294
300
|
else if (a.startsWith("--")) {
|
|
295
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
301
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom list --help` for usage.");
|
|
296
302
|
}
|
|
297
303
|
else {
|
|
298
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
|
|
304
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom list --json`.");
|
|
299
305
|
}
|
|
300
306
|
}
|
|
301
307
|
return out;
|
|
@@ -306,11 +312,11 @@ function parseInfoFlags(argv) {
|
|
|
306
312
|
if (a === "--json")
|
|
307
313
|
out.json = true;
|
|
308
314
|
else if (a.startsWith("--"))
|
|
309
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
315
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
|
|
310
316
|
else if (!out.slug)
|
|
311
317
|
out.slug = a;
|
|
312
318
|
else
|
|
313
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
|
|
319
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
|
|
314
320
|
}
|
|
315
321
|
return out;
|
|
316
322
|
}
|
|
@@ -319,6 +325,7 @@ function parseAddArgs(argv) {
|
|
|
319
325
|
let target;
|
|
320
326
|
let setup = false;
|
|
321
327
|
let force = false;
|
|
328
|
+
let json = false;
|
|
322
329
|
for (let i = 0; i < argv.length; i++) {
|
|
323
330
|
const a = argv[i] ?? "";
|
|
324
331
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -332,22 +339,61 @@ function parseAddArgs(argv) {
|
|
|
332
339
|
else if (a === "--setup") {
|
|
333
340
|
setup = true;
|
|
334
341
|
}
|
|
335
|
-
else if (a === "--
|
|
342
|
+
else if (a === "--json") {
|
|
343
|
+
json = true;
|
|
344
|
+
}
|
|
345
|
+
else if (a === "--force" || a === "--update") {
|
|
336
346
|
force = true;
|
|
337
347
|
}
|
|
338
348
|
else if (a.startsWith("--")) {
|
|
339
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --setup`.");
|
|
349
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
|
|
340
350
|
}
|
|
341
351
|
else if (slug) {
|
|
342
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --setup`.");
|
|
352
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
|
|
343
353
|
}
|
|
344
354
|
else
|
|
345
355
|
slug = a;
|
|
346
356
|
}
|
|
347
357
|
if (!slug) {
|
|
348
|
-
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --setup`");
|
|
358
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom add <url-or-slug> --setup`");
|
|
359
|
+
}
|
|
360
|
+
if (json && setup) {
|
|
361
|
+
throw new FloomError("Conflicting add flags.", "Use `--json` for machine output or `--setup` for interactive agent setup.");
|
|
362
|
+
}
|
|
363
|
+
return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
|
|
364
|
+
}
|
|
365
|
+
function parseDoctorArgs(argv) {
|
|
366
|
+
const out = { json: false };
|
|
367
|
+
for (const a of argv) {
|
|
368
|
+
if (a === "--json")
|
|
369
|
+
out.json = true;
|
|
370
|
+
else if (a.startsWith("--"))
|
|
371
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
372
|
+
else
|
|
373
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
349
374
|
}
|
|
350
|
-
return
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
function parseAgentPromptArgs(argv) {
|
|
378
|
+
const out = {};
|
|
379
|
+
for (let i = 0; i < argv.length; i++) {
|
|
380
|
+
const a = argv[i] ?? "";
|
|
381
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
382
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
383
|
+
if (value !== "claude" && value !== "codex") {
|
|
384
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
385
|
+
}
|
|
386
|
+
out.target = value;
|
|
387
|
+
i = nextIndex;
|
|
388
|
+
}
|
|
389
|
+
else if (a.startsWith("--")) {
|
|
390
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
351
397
|
}
|
|
352
398
|
function parseSearchFlags(argv) {
|
|
353
399
|
const out = { json: false };
|
|
@@ -370,7 +416,7 @@ function parseSearchFlags(argv) {
|
|
|
370
416
|
i = nextIndex;
|
|
371
417
|
}
|
|
372
418
|
else if (a.startsWith("--")) {
|
|
373
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
|
|
419
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom search \"support tone\" --type instruction`.");
|
|
374
420
|
}
|
|
375
421
|
else {
|
|
376
422
|
terms.push(a);
|
|
@@ -385,11 +431,11 @@ function parseDeleteFlags(argv) {
|
|
|
385
431
|
if (a === "--yes" || a === "-y")
|
|
386
432
|
out.yes = true;
|
|
387
433
|
else if (a.startsWith("--"))
|
|
388
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
434
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
|
|
389
435
|
else if (!out.slug)
|
|
390
436
|
out.slug = a;
|
|
391
437
|
else
|
|
392
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
438
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
|
|
393
439
|
}
|
|
394
440
|
return out;
|
|
395
441
|
}
|
|
@@ -423,12 +469,12 @@ function parseSetupFlags(argv) {
|
|
|
423
469
|
i = nextIndex;
|
|
424
470
|
}
|
|
425
471
|
else if (a.startsWith("--")) {
|
|
426
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
|
|
472
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom setup --target codex --dry-run`.");
|
|
427
473
|
}
|
|
428
474
|
else if (!out.file)
|
|
429
475
|
out.file = a;
|
|
430
476
|
else
|
|
431
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
|
|
477
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom setup --target claude --yes`.");
|
|
432
478
|
}
|
|
433
479
|
return out;
|
|
434
480
|
}
|
|
@@ -487,17 +533,21 @@ function parseLibraryCreateFlags(argv) {
|
|
|
487
533
|
else if (a === "--unlisted")
|
|
488
534
|
out.visibility = "unlisted";
|
|
489
535
|
else if (a.startsWith("--")) {
|
|
490
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
536
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
491
537
|
}
|
|
492
538
|
else if (!out.slug)
|
|
493
539
|
out.slug = a;
|
|
494
540
|
else
|
|
495
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom library create <slug> --name <name>`.");
|
|
541
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom library create <slug> --name <name>`.");
|
|
496
542
|
}
|
|
497
543
|
return out;
|
|
498
544
|
}
|
|
499
545
|
async function runLibrary(argv) {
|
|
500
546
|
const [subcommand, ...rest] = argv;
|
|
547
|
+
if (!subcommand || subcommand === "--json") {
|
|
548
|
+
await libraryList(parseListFlags(argv));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
501
551
|
switch (subcommand ?? "list") {
|
|
502
552
|
case "list": {
|
|
503
553
|
const flags = parseListFlags(rest);
|
|
@@ -507,9 +557,9 @@ async function runLibrary(argv) {
|
|
|
507
557
|
case "create": {
|
|
508
558
|
const flags = parseLibraryCreateFlags(rest);
|
|
509
559
|
if (!flags.slug)
|
|
510
|
-
throw new FloomError("Missing library slug.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
560
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
511
561
|
if (!flags.name)
|
|
512
|
-
throw new FloomError("Missing --name.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
562
|
+
throw new FloomError("Missing --name.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
513
563
|
await libraryCreate({
|
|
514
564
|
slug: flags.slug,
|
|
515
565
|
name: flags.name,
|
|
@@ -522,10 +572,10 @@ async function runLibrary(argv) {
|
|
|
522
572
|
const flags = parseFolderTagFlags(rest);
|
|
523
573
|
const [librarySlug, skillSlug] = flags.rest;
|
|
524
574
|
if (!librarySlug || !skillSlug) {
|
|
525
|
-
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
575
|
+
throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
|
|
526
576
|
}
|
|
527
577
|
if (flags.rest.length > 2) {
|
|
528
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
578
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
|
|
529
579
|
}
|
|
530
580
|
await libraryAddSkill({
|
|
531
581
|
librarySlug,
|
|
@@ -539,10 +589,10 @@ async function runLibrary(argv) {
|
|
|
539
589
|
case "rm": {
|
|
540
590
|
const [librarySlug, skillSlug] = rest;
|
|
541
591
|
if (!librarySlug || !skillSlug) {
|
|
542
|
-
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
592
|
+
throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
|
|
543
593
|
}
|
|
544
594
|
if (rest.length > 2) {
|
|
545
|
-
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
|
|
595
|
+
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
|
|
546
596
|
}
|
|
547
597
|
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
548
598
|
return;
|
|
@@ -550,9 +600,9 @@ async function runLibrary(argv) {
|
|
|
550
600
|
case "subscribe": {
|
|
551
601
|
const slug = rest[0];
|
|
552
602
|
if (!slug)
|
|
553
|
-
throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
|
|
603
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library subscribe superpowers`.");
|
|
554
604
|
if (rest.length > 1) {
|
|
555
|
-
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
|
|
605
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library subscribe superpowers`.");
|
|
556
606
|
}
|
|
557
607
|
await librarySubscribe(slug);
|
|
558
608
|
return;
|
|
@@ -560,9 +610,9 @@ async function runLibrary(argv) {
|
|
|
560
610
|
case "unsubscribe": {
|
|
561
611
|
const slug = rest[0];
|
|
562
612
|
if (!slug)
|
|
563
|
-
throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
|
|
613
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
|
|
564
614
|
if (rest.length > 1) {
|
|
565
|
-
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
|
|
615
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
|
|
566
616
|
}
|
|
567
617
|
await libraryUnsubscribe(slug);
|
|
568
618
|
return;
|
|
@@ -585,10 +635,10 @@ function parseWatchFlags(argv) {
|
|
|
585
635
|
i = nextIndex;
|
|
586
636
|
}
|
|
587
637
|
else if (a.startsWith("--")) {
|
|
588
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
|
|
638
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
|
|
589
639
|
}
|
|
590
640
|
else {
|
|
591
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
|
|
641
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
|
|
592
642
|
}
|
|
593
643
|
}
|
|
594
644
|
return out;
|
|
@@ -609,8 +659,9 @@ function parseSingleFileArg(argv, usageHint) {
|
|
|
609
659
|
throw new FloomError("Missing file argument.", usageHint);
|
|
610
660
|
return file;
|
|
611
661
|
}
|
|
612
|
-
function agentPrompt() {
|
|
613
|
-
|
|
662
|
+
function agentPrompt(target = "claude") {
|
|
663
|
+
const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
|
|
664
|
+
process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
|
|
614
665
|
}
|
|
615
666
|
function sleep(ms, signal) {
|
|
616
667
|
if (signal.aborted)
|
|
@@ -662,10 +713,10 @@ async function main() {
|
|
|
662
713
|
// never block on update-notifier
|
|
663
714
|
}
|
|
664
715
|
}
|
|
665
|
-
// Subcommand --help: any rest arg = --help/-h/help → show top-level
|
|
716
|
+
// Subcommand --help: any rest arg = --help/-h/help → show top-level reference.
|
|
666
717
|
// Subcommands are simple enough that one help screen is fine for Version 1.
|
|
667
718
|
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
668
|
-
|
|
719
|
+
commandUsage();
|
|
669
720
|
return;
|
|
670
721
|
}
|
|
671
722
|
try {
|
|
@@ -679,7 +730,7 @@ async function main() {
|
|
|
679
730
|
commandUsage();
|
|
680
731
|
return;
|
|
681
732
|
case "commands":
|
|
682
|
-
rejectArgs(rest, "Try `floom commands`.");
|
|
733
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom commands`.");
|
|
683
734
|
commandUsage();
|
|
684
735
|
return;
|
|
685
736
|
case "--version":
|
|
@@ -687,16 +738,16 @@ async function main() {
|
|
|
687
738
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
688
739
|
return;
|
|
689
740
|
case "login":
|
|
690
|
-
rejectArgs(rest, "Try `floom login`.");
|
|
741
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom login`.");
|
|
691
742
|
await login();
|
|
692
743
|
return;
|
|
693
744
|
case "logout":
|
|
694
|
-
rejectArgs(rest, "Try `floom logout`.");
|
|
745
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom logout`.");
|
|
695
746
|
await deleteConfig();
|
|
696
747
|
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
697
748
|
return;
|
|
698
749
|
case "whoami":
|
|
699
|
-
rejectArgs(rest, "Try `floom whoami`.");
|
|
750
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom whoami`.");
|
|
700
751
|
await whoami();
|
|
701
752
|
return;
|
|
702
753
|
case "init": {
|
|
@@ -708,10 +759,10 @@ async function main() {
|
|
|
708
759
|
const flags = parseFlags(rest);
|
|
709
760
|
const file = flags.rest[0];
|
|
710
761
|
if (!file) {
|
|
711
|
-
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
762
|
+
throw new FloomError("Missing file argument.", "Try: `npx -y @floomhq/floom publish skill.md`");
|
|
712
763
|
}
|
|
713
764
|
if (flags.rest.length > 1) {
|
|
714
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
|
|
765
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `npx -y @floomhq/floom publish skill.md`");
|
|
715
766
|
}
|
|
716
767
|
await publish({
|
|
717
768
|
file,
|
|
@@ -725,7 +776,7 @@ async function main() {
|
|
|
725
776
|
return;
|
|
726
777
|
}
|
|
727
778
|
case "scan": {
|
|
728
|
-
const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
|
|
779
|
+
const file = parseSingleFileArg(rest, "Try `npx -y @floomhq/floom scan skill.md`.");
|
|
729
780
|
await scanSkill(file);
|
|
730
781
|
return;
|
|
731
782
|
}
|
|
@@ -754,7 +805,7 @@ async function main() {
|
|
|
754
805
|
case "search": {
|
|
755
806
|
const flags = parseSearchFlags(rest);
|
|
756
807
|
if (!flags.query) {
|
|
757
|
-
throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
|
|
808
|
+
throw new FloomError("Missing search query.", "Try: `npx -y @floomhq/floom search \"support tone\"`.");
|
|
758
809
|
}
|
|
759
810
|
await search({
|
|
760
811
|
query: flags.query,
|
|
@@ -771,6 +822,20 @@ async function main() {
|
|
|
771
822
|
...(flags.target ? { target: flags.target } : {}),
|
|
772
823
|
setup: flags.setup,
|
|
773
824
|
force: flags.force,
|
|
825
|
+
json: flags.json,
|
|
826
|
+
});
|
|
827
|
+
if (flags.setup) {
|
|
828
|
+
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
case "update": {
|
|
833
|
+
const flags = parseAddArgs(rest);
|
|
834
|
+
await install(flags.slug, {
|
|
835
|
+
...(flags.target ? { target: flags.target } : {}),
|
|
836
|
+
setup: flags.setup,
|
|
837
|
+
force: true,
|
|
838
|
+
json: flags.json,
|
|
774
839
|
});
|
|
775
840
|
if (flags.setup) {
|
|
776
841
|
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
@@ -778,7 +843,7 @@ async function main() {
|
|
|
778
843
|
return;
|
|
779
844
|
}
|
|
780
845
|
case "sync":
|
|
781
|
-
rejectArgs(rest, "Try `floom sync`.");
|
|
846
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom sync`.");
|
|
782
847
|
await sync();
|
|
783
848
|
return;
|
|
784
849
|
case "setup":
|
|
@@ -788,10 +853,11 @@ async function main() {
|
|
|
788
853
|
return;
|
|
789
854
|
}
|
|
790
855
|
case "agent-prompt":
|
|
791
|
-
case "paste":
|
|
792
|
-
|
|
793
|
-
agentPrompt();
|
|
856
|
+
case "paste": {
|
|
857
|
+
const flags = parseAgentPromptArgs(rest);
|
|
858
|
+
agentPrompt(flags.target ?? "claude");
|
|
794
859
|
return;
|
|
860
|
+
}
|
|
795
861
|
case "watch": {
|
|
796
862
|
const flags = parseWatchFlags(rest);
|
|
797
863
|
await watch(flags.intervalSeconds);
|
|
@@ -811,27 +877,29 @@ async function main() {
|
|
|
811
877
|
const flags = parseFolderTagFlags(rest);
|
|
812
878
|
const slug = flags.rest[0];
|
|
813
879
|
if (!slug) {
|
|
814
|
-
throw new FloomError("Missing skill slug.", "Try `floom move support-tone --folder support/tone`.");
|
|
880
|
+
throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
|
|
815
881
|
}
|
|
816
882
|
if (flags.folder === undefined) {
|
|
817
883
|
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
818
884
|
}
|
|
819
885
|
if (flags.rest.length > 1) {
|
|
820
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
|
|
886
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
|
|
821
887
|
}
|
|
822
888
|
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
823
889
|
return;
|
|
824
890
|
}
|
|
825
891
|
case "mcp":
|
|
826
|
-
rejectArgs(rest, "Try `floom mcp`.");
|
|
892
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom mcp`.");
|
|
827
893
|
printMcpSetup();
|
|
828
894
|
return;
|
|
829
895
|
case "doctor":
|
|
830
|
-
|
|
831
|
-
|
|
896
|
+
{
|
|
897
|
+
const flags = parseDoctorArgs(rest);
|
|
898
|
+
await doctor(flags);
|
|
899
|
+
}
|
|
832
900
|
return;
|
|
833
901
|
default:
|
|
834
|
-
throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
|
|
902
|
+
throw new FloomError(`Unknown command: ${cmd}`, "Run `npx -y @floomhq/floom --help` to see available commands.");
|
|
835
903
|
}
|
|
836
904
|
}
|
|
837
905
|
catch (e) {
|
package/dist/delete.js
CHANGED
|
@@ -31,10 +31,10 @@ async function confirm(question) {
|
|
|
31
31
|
export async function deleteSkill(opts) {
|
|
32
32
|
const slug = slugFromInput(opts.slug);
|
|
33
33
|
if (!slug)
|
|
34
|
-
throw new FloomError("Missing skill slug.", "Try: `floom delete <slug>`");
|
|
34
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom delete <slug>`");
|
|
35
35
|
const cfg = await readConfig();
|
|
36
36
|
if (!cfg)
|
|
37
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
37
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
38
38
|
if (!opts.yes) {
|
|
39
39
|
process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
|
|
40
40
|
const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
|
package/dist/doctor.js
CHANGED
|
@@ -32,7 +32,7 @@ async function checkAuth() {
|
|
|
32
32
|
name: "Auth",
|
|
33
33
|
status: "fail",
|
|
34
34
|
detail: "Token rejected (401).",
|
|
35
|
-
hint: "Run `floom logout && floom login` to refresh.",
|
|
35
|
+
hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
if (!res.ok) {
|
|
@@ -88,7 +88,7 @@ async function checkMcp() {
|
|
|
88
88
|
return {
|
|
89
89
|
name: "MCP",
|
|
90
90
|
status: "ok",
|
|
91
|
-
detail: "Optional MCP not registered. `floom add` still writes local skill files.",
|
|
91
|
+
detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
return {
|
|
@@ -130,7 +130,7 @@ async function checkTargetDir() {
|
|
|
130
130
|
name: "Target dir",
|
|
131
131
|
status: "warn",
|
|
132
132
|
detail: `${dir} does not exist yet.`,
|
|
133
|
-
hint: "It will be created on first `floom add`.",
|
|
133
|
+
hint: "It will be created on first `npx -y @floomhq/floom add`.",
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
throw err;
|
|
@@ -146,7 +146,7 @@ async function checkLastSync() {
|
|
|
146
146
|
name: "Last sync",
|
|
147
147
|
status: "warn",
|
|
148
148
|
detail: "No synced skills found yet.",
|
|
149
|
-
hint: "Run `floom add <link>` to install your first skill.",
|
|
149
|
+
hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
// Find most recently modified entry
|
|
@@ -239,8 +239,7 @@ async function checkVersion() {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
export async function doctor() {
|
|
243
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
|
|
242
|
+
export async function doctor(opts = {}) {
|
|
244
243
|
const checks = await Promise.all([
|
|
245
244
|
checkAuth(),
|
|
246
245
|
checkMcp(),
|
|
@@ -248,6 +247,22 @@ export async function doctor() {
|
|
|
248
247
|
checkLastSync(),
|
|
249
248
|
checkVersion(),
|
|
250
249
|
]);
|
|
250
|
+
const anyFail = checks.some((c) => c.status === "fail");
|
|
251
|
+
const anyWarn = checks.some((c) => c.status === "warn");
|
|
252
|
+
const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
|
|
253
|
+
if (opts.json) {
|
|
254
|
+
process.stdout.write(`${JSON.stringify({
|
|
255
|
+
ok: !anyFail,
|
|
256
|
+
status,
|
|
257
|
+
version: CLI_VERSION,
|
|
258
|
+
config_path: CONFIG_PATH,
|
|
259
|
+
checks,
|
|
260
|
+
}, null, 2)}\n`);
|
|
261
|
+
if (anyFail)
|
|
262
|
+
process.exit(1);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
|
|
251
266
|
// Compute column widths for clean table output.
|
|
252
267
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
253
268
|
for (const check of checks) {
|
|
@@ -257,8 +272,6 @@ export async function doctor() {
|
|
|
257
272
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
261
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
262
275
|
process.stdout.write("\n");
|
|
263
276
|
if (anyFail) {
|
|
264
277
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
package/dist/errors.js
CHANGED
|
@@ -14,7 +14,7 @@ export class FloomError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
export function friendlyHttp(status, action) {
|
|
16
16
|
if (status === 401) {
|
|
17
|
-
return new FloomError("Your token expired.", "Run `floom login` to refresh.");
|
|
17
|
+
return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
|
|
18
18
|
}
|
|
19
19
|
if (status === 403) {
|
|
20
20
|
return new FloomError(`You don't have permission to ${action}.`);
|
|
@@ -23,7 +23,7 @@ export function friendlyHttp(status, action) {
|
|
|
23
23
|
if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
|
|
24
24
|
return new FloomError("Skill not found.", "Check the link or slug, then try again.");
|
|
25
25
|
}
|
|
26
|
-
return new FloomError("Skill not found.", "
|
|
26
|
+
return new FloomError("Skill not found.", "Publish as a new skill for now. Publisher-side version updates are planned for a later release.");
|
|
27
27
|
}
|
|
28
28
|
if (status === 413) {
|
|
29
29
|
return new FloomError("That file is too large to publish.");
|
package/dist/info.js
CHANGED
|
@@ -19,7 +19,7 @@ function slugFromInput(input) {
|
|
|
19
19
|
export async function info(opts) {
|
|
20
20
|
const slug = slugFromInput(opts.slug);
|
|
21
21
|
if (!slug)
|
|
22
|
-
throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
|
|
22
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
|
|
23
23
|
if (!SLUG_RE.test(slug)) {
|
|
24
24
|
throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export async function info(opts) {
|
|
|
28
28
|
const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
|
|
29
29
|
let detail;
|
|
30
30
|
try {
|
|
31
|
-
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "
|
|
31
|
+
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "info skill metadata", cfg?.accessToken);
|
|
32
32
|
}
|
|
33
33
|
finally {
|
|
34
34
|
spinner?.stop();
|
package/dist/install.js
CHANGED
|
@@ -58,7 +58,7 @@ async function localHash(path) {
|
|
|
58
58
|
if (code === "ENOENT")
|
|
59
59
|
return null;
|
|
60
60
|
if (code === "ELOOP")
|
|
61
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
61
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
62
62
|
if (code === "ENOTDIR" || code === "EISDIR") {
|
|
63
63
|
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
64
64
|
}
|
|
@@ -144,7 +144,7 @@ async function ensureSafeParentDirectory(root, target) {
|
|
|
144
144
|
async function assertSafeDirectory(path) {
|
|
145
145
|
const stat = await lstat(path);
|
|
146
146
|
if (stat.isSymbolicLink()) {
|
|
147
|
-
throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
147
|
+
throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
148
148
|
}
|
|
149
149
|
if (!stat.isDirectory()) {
|
|
150
150
|
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
@@ -159,7 +159,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
159
159
|
}
|
|
160
160
|
const cfg = await readConfig();
|
|
161
161
|
const apiUrl = resolveApiUrl(cfg);
|
|
162
|
-
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
162
|
+
const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
163
163
|
let detail;
|
|
164
164
|
try {
|
|
165
165
|
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
@@ -168,7 +168,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
catch (err) {
|
|
171
|
-
spinner
|
|
171
|
+
spinner?.stop();
|
|
172
172
|
throw err;
|
|
173
173
|
}
|
|
174
174
|
try {
|
|
@@ -194,7 +194,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
194
194
|
catch (err) {
|
|
195
195
|
const code = err.code;
|
|
196
196
|
if (code === "ELOOP")
|
|
197
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
197
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
198
198
|
throw err;
|
|
199
199
|
}
|
|
200
200
|
action = "updated";
|
|
@@ -212,16 +212,28 @@ export async function install(slugInput, opts = {}) {
|
|
|
212
212
|
throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
|
|
213
213
|
}
|
|
214
214
|
if (code === "ELOOP") {
|
|
215
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
215
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
216
216
|
}
|
|
217
217
|
if (code === "ENOENT") {
|
|
218
|
-
throw new FloomError("Local path changed during install.", "Run `floom add` again.");
|
|
218
|
+
throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
|
|
219
219
|
}
|
|
220
220
|
throw err;
|
|
221
221
|
}
|
|
222
222
|
action = "installed";
|
|
223
223
|
}
|
|
224
|
-
|
|
224
|
+
const result = {
|
|
225
|
+
slug,
|
|
226
|
+
title: detail.title,
|
|
227
|
+
action,
|
|
228
|
+
target: targetAgent,
|
|
229
|
+
path: target,
|
|
230
|
+
content_hash: remoteHash,
|
|
231
|
+
};
|
|
232
|
+
spinner?.stop();
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
225
237
|
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
226
238
|
process.stdout.write(` ${c.dim(target)}\n\n`);
|
|
227
239
|
process.stdout.write(` ${c.bold("Next")}\n`);
|
package/dist/library.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function libraryList(opts = {}) {
|
|
|
35
35
|
export async function libraryCreate(opts) {
|
|
36
36
|
const cfg = await readConfig();
|
|
37
37
|
if (!cfg)
|
|
38
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
38
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
39
39
|
const apiUrl = resolveApiUrl(cfg);
|
|
40
40
|
const result = await postJson(`${apiUrl}/api/v1/libraries`, "create library", cfg.accessToken, {
|
|
41
41
|
slug: opts.slug,
|
|
@@ -45,12 +45,12 @@ export async function libraryCreate(opts) {
|
|
|
45
45
|
});
|
|
46
46
|
process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
|
|
47
47
|
process.stdout.write(` ${c.dim("API:")} ${apiUrl}/api/v1/libraries/${result.slug}\n`);
|
|
48
|
-
process.stdout.write(` ${c.dim("Sync:")} floom library subscribe ${result.slug}\n\n`);
|
|
48
|
+
process.stdout.write(` ${c.dim("Sync:")} npx -y @floomhq/floom library subscribe ${result.slug}\n\n`);
|
|
49
49
|
}
|
|
50
50
|
export async function libraryAddSkill(opts) {
|
|
51
51
|
const cfg = await readConfig();
|
|
52
52
|
if (!cfg)
|
|
53
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
53
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
54
54
|
const apiUrl = resolveApiUrl(cfg);
|
|
55
55
|
await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
|
|
56
56
|
skill_slug: opts.skillSlug,
|
|
@@ -66,7 +66,7 @@ export async function libraryAddSkill(opts) {
|
|
|
66
66
|
export async function libraryRemoveSkill(librarySlug, skillSlug) {
|
|
67
67
|
const cfg = await readConfig();
|
|
68
68
|
if (!cfg)
|
|
69
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
69
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
70
70
|
const apiUrl = resolveApiUrl(cfg);
|
|
71
71
|
await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
|
|
72
72
|
process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
|
|
@@ -74,7 +74,7 @@ export async function libraryRemoveSkill(librarySlug, skillSlug) {
|
|
|
74
74
|
export async function librarySubscribe(slug) {
|
|
75
75
|
const cfg = await readConfig();
|
|
76
76
|
if (!cfg)
|
|
77
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
77
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
78
78
|
const apiUrl = resolveApiUrl(cfg);
|
|
79
79
|
await postJson(`${apiUrl}/api/v1/me/subscriptions`, "subscribe to library", cfg.accessToken, { library_slug: slug });
|
|
80
80
|
process.stdout.write(`\n${symbols.ok} Subscribed to ${c.cyan(slug)}\n`);
|
|
@@ -83,7 +83,7 @@ export async function librarySubscribe(slug) {
|
|
|
83
83
|
export async function libraryUnsubscribe(slug) {
|
|
84
84
|
const cfg = await readConfig();
|
|
85
85
|
if (!cfg)
|
|
86
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
86
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
87
87
|
const apiUrl = resolveApiUrl(cfg);
|
|
88
88
|
await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, "unsubscribe from library", cfg.accessToken);
|
|
89
89
|
process.stdout.write(`\n${symbols.ok} Unsubscribed from ${c.cyan(slug)}\n\n`);
|
|
@@ -91,7 +91,7 @@ export async function libraryUnsubscribe(slug) {
|
|
|
91
91
|
export async function moveSkill(opts) {
|
|
92
92
|
const cfg = await readConfig();
|
|
93
93
|
if (!cfg)
|
|
94
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
94
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
95
95
|
const apiUrl = resolveApiUrl(cfg);
|
|
96
96
|
await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(opts.slug)}/override`, "set skill override", cfg.accessToken, { folder: opts.folder, tags: opts.tags });
|
|
97
97
|
const folderText = opts.folder ?? c.dim("(root)");
|
package/dist/list.js
CHANGED
|
@@ -36,7 +36,7 @@ function formatRelative(iso) {
|
|
|
36
36
|
export async function list(opts) {
|
|
37
37
|
const cfg = await readConfig();
|
|
38
38
|
if (!cfg) {
|
|
39
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
39
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
40
40
|
}
|
|
41
41
|
const apiUrl = resolveApiUrl(cfg);
|
|
42
42
|
const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
|
|
@@ -54,7 +54,7 @@ export async function list(opts) {
|
|
|
54
54
|
}
|
|
55
55
|
process.stdout.write(`\n${symbols.dot} ${c.bold("Published")} ${c.dim(`(${published.length})`)}\n\n`);
|
|
56
56
|
if (published.length === 0) {
|
|
57
|
-
process.stdout.write(` ${c.dim("Nothing published yet. Try `floom publish skill.md`.")}\n`);
|
|
57
|
+
process.stdout.write(` ${c.dim("Nothing published yet. Try `npx -y @floomhq/floom publish skill.md`.")}\n`);
|
|
58
58
|
}
|
|
59
59
|
else {
|
|
60
60
|
for (const s of published)
|
package/dist/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import open from "open";
|
|
3
4
|
import ora from "ora";
|
|
4
5
|
import { getApiUrl, writeConfig } from "./config.js";
|
|
@@ -21,7 +22,7 @@ export async function login() {
|
|
|
21
22
|
catch (err) {
|
|
22
23
|
spinner.stop();
|
|
23
24
|
if (err instanceof Error && /timed out/i.test(err.message)) {
|
|
24
|
-
throw new FloomError("No worries — try `floom login` again when ready.");
|
|
25
|
+
throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
|
|
25
26
|
}
|
|
26
27
|
throw err;
|
|
27
28
|
}
|
|
@@ -59,6 +60,7 @@ export async function login() {
|
|
|
59
60
|
function waitForCallback() {
|
|
60
61
|
return new Promise((resolve, reject) => {
|
|
61
62
|
const apiUrl = getApiUrl();
|
|
63
|
+
const state = randomBytes(24).toString("base64url");
|
|
62
64
|
let settled = false;
|
|
63
65
|
let retriedEphemeralPort = false;
|
|
64
66
|
const server = createServer((req, res) => {
|
|
@@ -90,6 +92,15 @@ function waitForCallback() {
|
|
|
90
92
|
res.end(localCallbackPage("Missing tokens from OAuth response."));
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
95
|
+
if (data.state !== state) {
|
|
96
|
+
res.writeHead(400, {
|
|
97
|
+
"access-control-allow-origin": origin,
|
|
98
|
+
"access-control-allow-private-network": "true",
|
|
99
|
+
"content-type": "text/html; charset=utf-8",
|
|
100
|
+
});
|
|
101
|
+
res.end(localCallbackPage("Invalid OAuth state."));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
93
104
|
res.writeHead(200, {
|
|
94
105
|
"access-control-allow-origin": origin,
|
|
95
106
|
"access-control-allow-private-network": "true",
|
|
@@ -143,7 +154,7 @@ function waitForCallback() {
|
|
|
143
154
|
return;
|
|
144
155
|
}
|
|
145
156
|
const port = address.port;
|
|
146
|
-
const target = `${apiUrl}/auth/cli?port=${port}`;
|
|
157
|
+
const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
|
|
147
158
|
open(target).catch((e) => {
|
|
148
159
|
const msg = e instanceof Error ? e.message : String(e);
|
|
149
160
|
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
|
|
@@ -157,7 +168,7 @@ function parseCallbackBody(body, contentType) {
|
|
|
157
168
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
158
169
|
const params = new URLSearchParams(body);
|
|
159
170
|
const parsed = {};
|
|
160
|
-
for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
|
|
171
|
+
for (const key of ["access_token", "refresh_token", "expires_in", "token_type", "state"]) {
|
|
161
172
|
const value = params.get(key);
|
|
162
173
|
if (value)
|
|
163
174
|
parsed[key] = value;
|
package/dist/mcp.js
CHANGED
|
@@ -2,8 +2,8 @@ import { c } from "./ui.js";
|
|
|
2
2
|
export function printMcpSetup() {
|
|
3
3
|
const snippet = `## Floom
|
|
4
4
|
- Use Floom skills from the local Floom skills folder when they match the task.
|
|
5
|
-
- To install a shared skill, run \`floom add <slug-or-url> --target claude\` or \`floom add <slug-or-url> --target codex\`.
|
|
6
|
-
- To find reusable behavior, run \`floom search <query>\`.
|
|
5
|
+
- To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
|
|
6
|
+
- To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
|
|
7
7
|
- MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
|
|
8
8
|
process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
|
|
9
9
|
process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
|
package/dist/publish.js
CHANGED
|
@@ -140,7 +140,7 @@ export async function publish(opts) {
|
|
|
140
140
|
const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
|
|
141
141
|
const cfg = await readConfig();
|
|
142
142
|
if (!cfg) {
|
|
143
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
143
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
144
144
|
}
|
|
145
145
|
// Later version: detect already-published when --update is missing.
|
|
146
146
|
// The current API does not return a duplicate-skill code, so we leave
|
package/dist/secrets.js
CHANGED
|
@@ -5,15 +5,29 @@ const SECRET_PATTERNS = [
|
|
|
5
5
|
{ label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{25,}\b/g },
|
|
6
6
|
{ label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g },
|
|
7
7
|
{ label: "GitHub token", regex: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
|
|
8
|
+
{ label: "npm token", regex: /\bnpm_[A-Za-z0-9]{30,}\b/g },
|
|
8
9
|
{ label: "Supabase access token", regex: /\bsbp_[A-Za-z0-9]{30,}\b/g },
|
|
9
10
|
{ label: "Stripe secret key", regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
|
|
10
11
|
{ label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
|
|
11
12
|
{ label: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
13
|
+
{ label: "Database URL with password", regex: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]{8,}@[^\s]+/gi },
|
|
12
14
|
{ label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
|
|
13
15
|
];
|
|
14
16
|
const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
|
|
15
17
|
const PROVIDER_LIKE_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?((?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|sbp_[A-Za-z0-9]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})["']?/gi;
|
|
16
|
-
const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|placeholder|replace|changeme|todo|xxx|
|
|
18
|
+
const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|mock|placeholder|replace|changeme|todo|xxx|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
|
|
19
|
+
const PLACEHOLDER_PHRASE_WORDS = new Set([
|
|
20
|
+
"and",
|
|
21
|
+
"fake",
|
|
22
|
+
"is",
|
|
23
|
+
"key",
|
|
24
|
+
"long",
|
|
25
|
+
"looks",
|
|
26
|
+
"real",
|
|
27
|
+
"secret",
|
|
28
|
+
"that",
|
|
29
|
+
"very",
|
|
30
|
+
]);
|
|
17
31
|
const PROMPT_INJECTION_PATTERNS = [
|
|
18
32
|
{ label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
|
19
33
|
{ label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
|
@@ -23,8 +37,11 @@ const PROMPT_INJECTION_PATTERNS = [
|
|
|
23
37
|
];
|
|
24
38
|
const DATA_EXFILTRATION_PATTERNS = [
|
|
25
39
|
{ label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b(?:[^.\n]{0,120})\b(?:to|into) https?:\/\//gi },
|
|
40
|
+
{ label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b[^.\n]{0,120}\b(?:to|into) https?:\/\//gi },
|
|
26
41
|
{ label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
|
|
42
|
+
{ label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
|
|
27
43
|
{ label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
|
|
44
|
+
{ label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
|
|
28
45
|
];
|
|
29
46
|
function redact(value) {
|
|
30
47
|
if (value.length <= 12)
|
|
@@ -46,6 +63,12 @@ function pushFinding(findings, seen, label, line, value) {
|
|
|
46
63
|
seen.add(key);
|
|
47
64
|
findings.push({ label, line, preview: redact(value) });
|
|
48
65
|
}
|
|
66
|
+
function isPlaceholderValue(value) {
|
|
67
|
+
if (PLACEHOLDER_RE.test(value))
|
|
68
|
+
return true;
|
|
69
|
+
const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
|
|
70
|
+
return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
|
|
71
|
+
}
|
|
49
72
|
export function detectSecrets(input) {
|
|
50
73
|
const findings = [];
|
|
51
74
|
const seen = new Set();
|
|
@@ -59,14 +82,14 @@ export function detectSecrets(input) {
|
|
|
59
82
|
GENERIC_ASSIGNMENT_RE.lastIndex = 0;
|
|
60
83
|
for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
|
|
61
84
|
const value = match[1] ?? "";
|
|
62
|
-
if (!value ||
|
|
85
|
+
if (!value || isPlaceholderValue(value))
|
|
63
86
|
continue;
|
|
64
87
|
pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
65
88
|
}
|
|
66
89
|
PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
|
|
67
90
|
for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
|
|
68
91
|
const value = match[1] ?? "";
|
|
69
|
-
if (!value)
|
|
92
|
+
if (!value || isPlaceholderValue(value))
|
|
70
93
|
continue;
|
|
71
94
|
pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
72
95
|
}
|
package/dist/setup.js
CHANGED
|
@@ -13,16 +13,16 @@ const TARGETS = {
|
|
|
13
13
|
};
|
|
14
14
|
function floomAgentInstructions(target) {
|
|
15
15
|
const addCommand = target === "codex"
|
|
16
|
-
? "floom add <slug-or-url> --target codex"
|
|
17
|
-
: "floom add <slug-or-url> --target claude";
|
|
16
|
+
? "npx -y @floomhq/floom add <slug-or-url> --target codex"
|
|
17
|
+
: "npx -y @floomhq/floom add <slug-or-url> --target claude";
|
|
18
18
|
return `${START_MARKER}
|
|
19
19
|
## Floom
|
|
20
20
|
|
|
21
21
|
- Before recreating agent behavior from scratch, check Floom for reusable skills.
|
|
22
|
-
- Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
|
|
22
|
+
- Search or inspect skills with \`npx -y @floomhq/floom search <query>\`, \`npx -y @floomhq/floom info <slug-or-url>\`, and \`npx -y @floomhq/floom list\`.
|
|
23
23
|
- Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
|
|
24
24
|
- Use installed Markdown skills from the local skills folder when they match the task.
|
|
25
|
-
- \`floom sync\`, \`floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
|
|
25
|
+
- \`npx -y @floomhq/floom sync\`, \`npx -y @floomhq/floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
|
|
26
26
|
${END_MARKER}`;
|
|
27
27
|
}
|
|
28
28
|
async function fileExists(path) {
|
|
@@ -96,7 +96,7 @@ async function detectTarget(opts) {
|
|
|
96
96
|
return { agent: "claude", label: TARGETS.claude.label, path: claude };
|
|
97
97
|
if (codex)
|
|
98
98
|
return { agent: "codex", label: TARGETS.codex.label, path: codex };
|
|
99
|
-
throw new FloomError("No agent instruction file found.", "Run `floom setup --target claude --yes` or `floom setup --target codex --yes` from the repo root.");
|
|
99
|
+
throw new FloomError("No agent instruction file found.", "Run `npx -y @floomhq/floom setup --target claude --yes` or `npx -y @floomhq/floom setup --target codex --yes` from the repo root.");
|
|
100
100
|
}
|
|
101
101
|
function renderPreview(target, existing) {
|
|
102
102
|
const action = existing === null ? "create" : "append";
|
|
@@ -108,7 +108,7 @@ function renderPreview(target, existing) {
|
|
|
108
108
|
"",
|
|
109
109
|
floomAgentInstructions(target.agent),
|
|
110
110
|
"",
|
|
111
|
-
`${c.dim("MCP setup guidance:")} run ${c.cyan("floom mcp")} to print local agent commands.`,
|
|
111
|
+
`${c.dim("MCP setup guidance:")} run ${c.cyan("npx -y @floomhq/floom mcp")} to print local agent commands.`,
|
|
112
112
|
"",
|
|
113
113
|
].join("\n");
|
|
114
114
|
}
|
|
@@ -153,7 +153,7 @@ export async function setupAgent(opts) {
|
|
|
153
153
|
if (existing === null) {
|
|
154
154
|
await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
|
|
155
155
|
if (err instanceof Error && "code" in err && err.code === "EEXIST") {
|
|
156
|
-
throw new FloomError("Instruction file appeared while setup was running.", "Re-run `floom setup` so Floom can inspect the current file before writing.");
|
|
156
|
+
throw new FloomError("Instruction file appeared while setup was running.", "Re-run `npx -y @floomhq/floom setup` so Floom can inspect the current file before writing.");
|
|
157
157
|
}
|
|
158
158
|
throw err;
|
|
159
159
|
});
|
|
@@ -162,5 +162,5 @@ export async function setupAgent(opts) {
|
|
|
162
162
|
await writeFile(target.path, next, "utf8");
|
|
163
163
|
}
|
|
164
164
|
process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
|
|
165
|
-
process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("floom mcp")}\n\n`);
|
|
165
|
+
process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("npx -y @floomhq/floom mcp")}\n\n`);
|
|
166
166
|
}
|
package/dist/share.js
CHANGED
|
@@ -19,11 +19,11 @@ async function readJson(res) {
|
|
|
19
19
|
export async function share(opts) {
|
|
20
20
|
const cfg = await readConfig();
|
|
21
21
|
if (!cfg) {
|
|
22
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
22
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
23
23
|
}
|
|
24
24
|
const slug = slugFromInput(opts.slug);
|
|
25
25
|
if (!slug) {
|
|
26
|
-
throw new FloomError("Missing skill slug.", "Try: `floom share <slug> --list`");
|
|
26
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom share <slug> --list`");
|
|
27
27
|
}
|
|
28
28
|
const apiUrl = resolveApiUrl(cfg);
|
|
29
29
|
const endpoint = `${apiUrl}/api/skills/${encodeURIComponent(slug)}/share`;
|
package/dist/sync.js
CHANGED
|
@@ -205,7 +205,7 @@ function conflictError(message, code) {
|
|
|
205
205
|
export async function sync(opts = {}) {
|
|
206
206
|
const cfg = await readConfig();
|
|
207
207
|
if (!cfg)
|
|
208
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
208
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
209
209
|
await ensureSyncManifestDir();
|
|
210
210
|
const apiUrl = resolveApiUrl(cfg);
|
|
211
211
|
const spinner = opts.spinner === false ? null : ora({ text: c.dim("Syncing skills..."), color: "yellow" }).start();
|
|
@@ -394,7 +394,7 @@ export async function sync(opts = {}) {
|
|
|
394
394
|
process.stderr.write(`${symbols.bullet} [floom] skipped local conflict: ${note}\n`);
|
|
395
395
|
}
|
|
396
396
|
if (conflicts > 0) {
|
|
397
|
-
process.stderr.write(` ${c.dim("Move or delete the local file, then run `floom sync` again.")}\n`);
|
|
397
|
+
process.stderr.write(` ${c.dim("Move or delete the local file, then run `npx -y @floomhq/floom sync` again.")}\n`);
|
|
398
398
|
}
|
|
399
399
|
process.stdout.write(`\n${symbols.ok} [floom] synced ${synced} skills (${unchanged} unchanged, ${updated} updated${conflictNote})${skippedNote}\n\n`);
|
|
400
400
|
}
|
package/dist/whoami.js
CHANGED
|
@@ -6,7 +6,7 @@ import { FloomError, friendlyHttp, friendlyNetwork } from "./errors.js";
|
|
|
6
6
|
export async function whoami() {
|
|
7
7
|
const cfg = await readConfig();
|
|
8
8
|
if (!cfg) {
|
|
9
|
-
throw new FloomError("Not signed in.", "Run `floom login` to sign in.");
|
|
9
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` to sign in.");
|
|
10
10
|
}
|
|
11
11
|
const apiUrl = resolveApiUrl(cfg);
|
|
12
12
|
const spinner = ora({ text: c.dim("Checking session..."), color: "yellow" }).start();
|