@floomhq/floom 1.0.8 → 1.0.9
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 +102 -59
- 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 +1 -1
- package/dist/mcp.js +2 -2
- package/dist/publish.js +1 -1
- package/dist/secrets.js +23 -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,12 @@ 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")}
|
|
102
105
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
106
|
+
${c.dim("Flags: --json")}
|
|
103
107
|
|
|
104
108
|
${c.dim("Advanced")}
|
|
105
109
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
@@ -160,7 +164,7 @@ function parseFlags(argv) {
|
|
|
160
164
|
out.explicitVisibility = true;
|
|
161
165
|
}
|
|
162
166
|
else if (a === "--update") {
|
|
163
|
-
throw new FloomError(
|
|
167
|
+
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
168
|
}
|
|
165
169
|
else if (a === "--share" || a.startsWith("--share=")) {
|
|
166
170
|
const { value, nextIndex } = readFlagValue(argv, i, "--share");
|
|
@@ -195,7 +199,7 @@ function parseFlags(argv) {
|
|
|
195
199
|
throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
|
|
196
200
|
}
|
|
197
201
|
else if (a.startsWith("--")) {
|
|
198
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
|
|
202
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom publish skill.md --type instruction --public`.");
|
|
199
203
|
}
|
|
200
204
|
else
|
|
201
205
|
out.rest.push(a);
|
|
@@ -206,7 +210,7 @@ function parseFlags(argv) {
|
|
|
206
210
|
throw new FloomError("Too many --share recipients.", "Use 200 email addresses or fewer.");
|
|
207
211
|
}
|
|
208
212
|
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.");
|
|
213
|
+
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
214
|
}
|
|
211
215
|
}
|
|
212
216
|
return out;
|
|
@@ -243,22 +247,22 @@ function parseShareFlags(argv) {
|
|
|
243
247
|
i = nextIndex;
|
|
244
248
|
}
|
|
245
249
|
else if (a.startsWith("--")) {
|
|
246
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
250
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
247
251
|
}
|
|
248
252
|
else if (!out.slug) {
|
|
249
253
|
out.slug = a;
|
|
250
254
|
}
|
|
251
255
|
else {
|
|
252
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
|
|
256
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
if (!out.slug)
|
|
256
|
-
throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
|
|
260
|
+
throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
257
261
|
if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
|
|
258
262
|
throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
|
|
259
263
|
}
|
|
260
264
|
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`.");
|
|
265
|
+
throw new FloomError("Missing share action.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
|
|
262
266
|
}
|
|
263
267
|
return out;
|
|
264
268
|
}
|
|
@@ -277,10 +281,10 @@ function parseInitArgs(argv) {
|
|
|
277
281
|
continue;
|
|
278
282
|
}
|
|
279
283
|
if (a.startsWith("--")) {
|
|
280
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md --template brand-voice`.");
|
|
284
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom init skill.md --template brand-voice`.");
|
|
281
285
|
}
|
|
282
286
|
if (file) {
|
|
283
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
|
|
287
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom init skill.md`.");
|
|
284
288
|
}
|
|
285
289
|
file = a;
|
|
286
290
|
}
|
|
@@ -292,10 +296,10 @@ function parseListFlags(argv) {
|
|
|
292
296
|
if (a === "--json")
|
|
293
297
|
out.json = true;
|
|
294
298
|
else if (a.startsWith("--")) {
|
|
295
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
|
|
299
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom list --help` for usage.");
|
|
296
300
|
}
|
|
297
301
|
else {
|
|
298
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
|
|
302
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom list --json`.");
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
305
|
return out;
|
|
@@ -306,11 +310,11 @@ function parseInfoFlags(argv) {
|
|
|
306
310
|
if (a === "--json")
|
|
307
311
|
out.json = true;
|
|
308
312
|
else if (a.startsWith("--"))
|
|
309
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
|
|
313
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
|
|
310
314
|
else if (!out.slug)
|
|
311
315
|
out.slug = a;
|
|
312
316
|
else
|
|
313
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
|
|
317
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
|
|
314
318
|
}
|
|
315
319
|
return out;
|
|
316
320
|
}
|
|
@@ -319,6 +323,7 @@ function parseAddArgs(argv) {
|
|
|
319
323
|
let target;
|
|
320
324
|
let setup = false;
|
|
321
325
|
let force = false;
|
|
326
|
+
let json = false;
|
|
322
327
|
for (let i = 0; i < argv.length; i++) {
|
|
323
328
|
const a = argv[i] ?? "";
|
|
324
329
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -332,22 +337,40 @@ function parseAddArgs(argv) {
|
|
|
332
337
|
else if (a === "--setup") {
|
|
333
338
|
setup = true;
|
|
334
339
|
}
|
|
335
|
-
else if (a === "--
|
|
340
|
+
else if (a === "--json") {
|
|
341
|
+
json = true;
|
|
342
|
+
}
|
|
343
|
+
else if (a === "--force" || a === "--update") {
|
|
336
344
|
force = true;
|
|
337
345
|
}
|
|
338
346
|
else if (a.startsWith("--")) {
|
|
339
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --setup`.");
|
|
347
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
|
|
340
348
|
}
|
|
341
349
|
else if (slug) {
|
|
342
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --setup`.");
|
|
350
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
|
|
343
351
|
}
|
|
344
352
|
else
|
|
345
353
|
slug = a;
|
|
346
354
|
}
|
|
347
355
|
if (!slug) {
|
|
348
|
-
throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --setup`");
|
|
356
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom add <url-or-slug> --setup`");
|
|
357
|
+
}
|
|
358
|
+
if (json && setup) {
|
|
359
|
+
throw new FloomError("Conflicting add flags.", "Use `--json` for machine output or `--setup` for interactive agent setup.");
|
|
360
|
+
}
|
|
361
|
+
return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
|
|
362
|
+
}
|
|
363
|
+
function parseDoctorArgs(argv) {
|
|
364
|
+
const out = { json: false };
|
|
365
|
+
for (const a of argv) {
|
|
366
|
+
if (a === "--json")
|
|
367
|
+
out.json = true;
|
|
368
|
+
else if (a.startsWith("--"))
|
|
369
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
370
|
+
else
|
|
371
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
349
372
|
}
|
|
350
|
-
return
|
|
373
|
+
return out;
|
|
351
374
|
}
|
|
352
375
|
function parseSearchFlags(argv) {
|
|
353
376
|
const out = { json: false };
|
|
@@ -370,7 +393,7 @@ function parseSearchFlags(argv) {
|
|
|
370
393
|
i = nextIndex;
|
|
371
394
|
}
|
|
372
395
|
else if (a.startsWith("--")) {
|
|
373
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
|
|
396
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom search \"support tone\" --type instruction`.");
|
|
374
397
|
}
|
|
375
398
|
else {
|
|
376
399
|
terms.push(a);
|
|
@@ -385,11 +408,11 @@ function parseDeleteFlags(argv) {
|
|
|
385
408
|
if (a === "--yes" || a === "-y")
|
|
386
409
|
out.yes = true;
|
|
387
410
|
else if (a.startsWith("--"))
|
|
388
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
411
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
|
|
389
412
|
else if (!out.slug)
|
|
390
413
|
out.slug = a;
|
|
391
414
|
else
|
|
392
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
|
|
415
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
|
|
393
416
|
}
|
|
394
417
|
return out;
|
|
395
418
|
}
|
|
@@ -423,12 +446,12 @@ function parseSetupFlags(argv) {
|
|
|
423
446
|
i = nextIndex;
|
|
424
447
|
}
|
|
425
448
|
else if (a.startsWith("--")) {
|
|
426
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
|
|
449
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom setup --target codex --dry-run`.");
|
|
427
450
|
}
|
|
428
451
|
else if (!out.file)
|
|
429
452
|
out.file = a;
|
|
430
453
|
else
|
|
431
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
|
|
454
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom setup --target claude --yes`.");
|
|
432
455
|
}
|
|
433
456
|
return out;
|
|
434
457
|
}
|
|
@@ -487,17 +510,21 @@ function parseLibraryCreateFlags(argv) {
|
|
|
487
510
|
else if (a === "--unlisted")
|
|
488
511
|
out.visibility = "unlisted";
|
|
489
512
|
else if (a.startsWith("--")) {
|
|
490
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
513
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
491
514
|
}
|
|
492
515
|
else if (!out.slug)
|
|
493
516
|
out.slug = a;
|
|
494
517
|
else
|
|
495
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom library create <slug> --name <name>`.");
|
|
518
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom library create <slug> --name <name>`.");
|
|
496
519
|
}
|
|
497
520
|
return out;
|
|
498
521
|
}
|
|
499
522
|
async function runLibrary(argv) {
|
|
500
523
|
const [subcommand, ...rest] = argv;
|
|
524
|
+
if (!subcommand || subcommand === "--json") {
|
|
525
|
+
await libraryList(parseListFlags(argv));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
501
528
|
switch (subcommand ?? "list") {
|
|
502
529
|
case "list": {
|
|
503
530
|
const flags = parseListFlags(rest);
|
|
@@ -507,9 +534,9 @@ async function runLibrary(argv) {
|
|
|
507
534
|
case "create": {
|
|
508
535
|
const flags = parseLibraryCreateFlags(rest);
|
|
509
536
|
if (!flags.slug)
|
|
510
|
-
throw new FloomError("Missing library slug.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
537
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
511
538
|
if (!flags.name)
|
|
512
|
-
throw new FloomError("Missing --name.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
539
|
+
throw new FloomError("Missing --name.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
513
540
|
await libraryCreate({
|
|
514
541
|
slug: flags.slug,
|
|
515
542
|
name: flags.name,
|
|
@@ -522,10 +549,10 @@ async function runLibrary(argv) {
|
|
|
522
549
|
const flags = parseFolderTagFlags(rest);
|
|
523
550
|
const [librarySlug, skillSlug] = flags.rest;
|
|
524
551
|
if (!librarySlug || !skillSlug) {
|
|
525
|
-
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
552
|
+
throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
|
|
526
553
|
}
|
|
527
554
|
if (flags.rest.length > 2) {
|
|
528
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
555
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
|
|
529
556
|
}
|
|
530
557
|
await libraryAddSkill({
|
|
531
558
|
librarySlug,
|
|
@@ -539,10 +566,10 @@ async function runLibrary(argv) {
|
|
|
539
566
|
case "rm": {
|
|
540
567
|
const [librarySlug, skillSlug] = rest;
|
|
541
568
|
if (!librarySlug || !skillSlug) {
|
|
542
|
-
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
569
|
+
throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
|
|
543
570
|
}
|
|
544
571
|
if (rest.length > 2) {
|
|
545
|
-
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
|
|
572
|
+
throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
|
|
546
573
|
}
|
|
547
574
|
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
548
575
|
return;
|
|
@@ -550,9 +577,9 @@ async function runLibrary(argv) {
|
|
|
550
577
|
case "subscribe": {
|
|
551
578
|
const slug = rest[0];
|
|
552
579
|
if (!slug)
|
|
553
|
-
throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
|
|
580
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library subscribe superpowers`.");
|
|
554
581
|
if (rest.length > 1) {
|
|
555
|
-
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
|
|
582
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library subscribe superpowers`.");
|
|
556
583
|
}
|
|
557
584
|
await librarySubscribe(slug);
|
|
558
585
|
return;
|
|
@@ -560,9 +587,9 @@ async function runLibrary(argv) {
|
|
|
560
587
|
case "unsubscribe": {
|
|
561
588
|
const slug = rest[0];
|
|
562
589
|
if (!slug)
|
|
563
|
-
throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
|
|
590
|
+
throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
|
|
564
591
|
if (rest.length > 1) {
|
|
565
|
-
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
|
|
592
|
+
throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
|
|
566
593
|
}
|
|
567
594
|
await libraryUnsubscribe(slug);
|
|
568
595
|
return;
|
|
@@ -585,10 +612,10 @@ function parseWatchFlags(argv) {
|
|
|
585
612
|
i = nextIndex;
|
|
586
613
|
}
|
|
587
614
|
else if (a.startsWith("--")) {
|
|
588
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
|
|
615
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
|
|
589
616
|
}
|
|
590
617
|
else {
|
|
591
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
|
|
618
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
|
|
592
619
|
}
|
|
593
620
|
}
|
|
594
621
|
return out;
|
|
@@ -662,10 +689,10 @@ async function main() {
|
|
|
662
689
|
// never block on update-notifier
|
|
663
690
|
}
|
|
664
691
|
}
|
|
665
|
-
// Subcommand --help: any rest arg = --help/-h/help → show top-level
|
|
692
|
+
// Subcommand --help: any rest arg = --help/-h/help → show top-level reference.
|
|
666
693
|
// Subcommands are simple enough that one help screen is fine for Version 1.
|
|
667
694
|
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
668
|
-
|
|
695
|
+
commandUsage();
|
|
669
696
|
return;
|
|
670
697
|
}
|
|
671
698
|
try {
|
|
@@ -679,7 +706,7 @@ async function main() {
|
|
|
679
706
|
commandUsage();
|
|
680
707
|
return;
|
|
681
708
|
case "commands":
|
|
682
|
-
rejectArgs(rest, "Try `floom commands`.");
|
|
709
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom commands`.");
|
|
683
710
|
commandUsage();
|
|
684
711
|
return;
|
|
685
712
|
case "--version":
|
|
@@ -687,16 +714,16 @@ async function main() {
|
|
|
687
714
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
688
715
|
return;
|
|
689
716
|
case "login":
|
|
690
|
-
rejectArgs(rest, "Try `floom login`.");
|
|
717
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom login`.");
|
|
691
718
|
await login();
|
|
692
719
|
return;
|
|
693
720
|
case "logout":
|
|
694
|
-
rejectArgs(rest, "Try `floom logout`.");
|
|
721
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom logout`.");
|
|
695
722
|
await deleteConfig();
|
|
696
723
|
process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
|
|
697
724
|
return;
|
|
698
725
|
case "whoami":
|
|
699
|
-
rejectArgs(rest, "Try `floom whoami`.");
|
|
726
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom whoami`.");
|
|
700
727
|
await whoami();
|
|
701
728
|
return;
|
|
702
729
|
case "init": {
|
|
@@ -708,10 +735,10 @@ async function main() {
|
|
|
708
735
|
const flags = parseFlags(rest);
|
|
709
736
|
const file = flags.rest[0];
|
|
710
737
|
if (!file) {
|
|
711
|
-
throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
|
|
738
|
+
throw new FloomError("Missing file argument.", "Try: `npx -y @floomhq/floom publish skill.md`");
|
|
712
739
|
}
|
|
713
740
|
if (flags.rest.length > 1) {
|
|
714
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
|
|
741
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `npx -y @floomhq/floom publish skill.md`");
|
|
715
742
|
}
|
|
716
743
|
await publish({
|
|
717
744
|
file,
|
|
@@ -725,7 +752,7 @@ async function main() {
|
|
|
725
752
|
return;
|
|
726
753
|
}
|
|
727
754
|
case "scan": {
|
|
728
|
-
const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
|
|
755
|
+
const file = parseSingleFileArg(rest, "Try `npx -y @floomhq/floom scan skill.md`.");
|
|
729
756
|
await scanSkill(file);
|
|
730
757
|
return;
|
|
731
758
|
}
|
|
@@ -754,7 +781,7 @@ async function main() {
|
|
|
754
781
|
case "search": {
|
|
755
782
|
const flags = parseSearchFlags(rest);
|
|
756
783
|
if (!flags.query) {
|
|
757
|
-
throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
|
|
784
|
+
throw new FloomError("Missing search query.", "Try: `npx -y @floomhq/floom search \"support tone\"`.");
|
|
758
785
|
}
|
|
759
786
|
await search({
|
|
760
787
|
query: flags.query,
|
|
@@ -771,6 +798,20 @@ async function main() {
|
|
|
771
798
|
...(flags.target ? { target: flags.target } : {}),
|
|
772
799
|
setup: flags.setup,
|
|
773
800
|
force: flags.force,
|
|
801
|
+
json: flags.json,
|
|
802
|
+
});
|
|
803
|
+
if (flags.setup) {
|
|
804
|
+
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
case "update": {
|
|
809
|
+
const flags = parseAddArgs(rest);
|
|
810
|
+
await install(flags.slug, {
|
|
811
|
+
...(flags.target ? { target: flags.target } : {}),
|
|
812
|
+
setup: flags.setup,
|
|
813
|
+
force: true,
|
|
814
|
+
json: flags.json,
|
|
774
815
|
});
|
|
775
816
|
if (flags.setup) {
|
|
776
817
|
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
@@ -778,7 +819,7 @@ async function main() {
|
|
|
778
819
|
return;
|
|
779
820
|
}
|
|
780
821
|
case "sync":
|
|
781
|
-
rejectArgs(rest, "Try `floom sync`.");
|
|
822
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom sync`.");
|
|
782
823
|
await sync();
|
|
783
824
|
return;
|
|
784
825
|
case "setup":
|
|
@@ -789,7 +830,7 @@ async function main() {
|
|
|
789
830
|
}
|
|
790
831
|
case "agent-prompt":
|
|
791
832
|
case "paste":
|
|
792
|
-
rejectArgs(rest, "Try `floom agent-prompt`.");
|
|
833
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom agent-prompt`.");
|
|
793
834
|
agentPrompt();
|
|
794
835
|
return;
|
|
795
836
|
case "watch": {
|
|
@@ -811,27 +852,29 @@ async function main() {
|
|
|
811
852
|
const flags = parseFolderTagFlags(rest);
|
|
812
853
|
const slug = flags.rest[0];
|
|
813
854
|
if (!slug) {
|
|
814
|
-
throw new FloomError("Missing skill slug.", "Try `floom move support-tone --folder support/tone`.");
|
|
855
|
+
throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
|
|
815
856
|
}
|
|
816
857
|
if (flags.folder === undefined) {
|
|
817
858
|
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
818
859
|
}
|
|
819
860
|
if (flags.rest.length > 1) {
|
|
820
|
-
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
|
|
861
|
+
throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
|
|
821
862
|
}
|
|
822
863
|
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
823
864
|
return;
|
|
824
865
|
}
|
|
825
866
|
case "mcp":
|
|
826
|
-
rejectArgs(rest, "Try `floom mcp`.");
|
|
867
|
+
rejectArgs(rest, "Try `npx -y @floomhq/floom mcp`.");
|
|
827
868
|
printMcpSetup();
|
|
828
869
|
return;
|
|
829
870
|
case "doctor":
|
|
830
|
-
|
|
831
|
-
|
|
871
|
+
{
|
|
872
|
+
const flags = parseDoctorArgs(rest);
|
|
873
|
+
await doctor(flags);
|
|
874
|
+
}
|
|
832
875
|
return;
|
|
833
876
|
default:
|
|
834
|
-
throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
|
|
877
|
+
throw new FloomError(`Unknown command: ${cmd}`, "Run `npx -y @floomhq/floom --help` to see available commands.");
|
|
835
878
|
}
|
|
836
879
|
}
|
|
837
880
|
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
|
@@ -21,7 +21,7 @@ export async function login() {
|
|
|
21
21
|
catch (err) {
|
|
22
22
|
spinner.stop();
|
|
23
23
|
if (err instanceof Error && /timed out/i.test(err.message)) {
|
|
24
|
-
throw new FloomError("No worries — try `floom login` again when ready.");
|
|
24
|
+
throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
|
|
25
25
|
}
|
|
26
26
|
throw err;
|
|
27
27
|
}
|
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 },
|
|
@@ -46,6 +60,12 @@ function pushFinding(findings, seen, label, line, value) {
|
|
|
46
60
|
seen.add(key);
|
|
47
61
|
findings.push({ label, line, preview: redact(value) });
|
|
48
62
|
}
|
|
63
|
+
function isPlaceholderValue(value) {
|
|
64
|
+
if (PLACEHOLDER_RE.test(value))
|
|
65
|
+
return true;
|
|
66
|
+
const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
|
|
67
|
+
return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
|
|
68
|
+
}
|
|
49
69
|
export function detectSecrets(input) {
|
|
50
70
|
const findings = [];
|
|
51
71
|
const seen = new Set();
|
|
@@ -59,14 +79,14 @@ export function detectSecrets(input) {
|
|
|
59
79
|
GENERIC_ASSIGNMENT_RE.lastIndex = 0;
|
|
60
80
|
for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
|
|
61
81
|
const value = match[1] ?? "";
|
|
62
|
-
if (!value ||
|
|
82
|
+
if (!value || isPlaceholderValue(value))
|
|
63
83
|
continue;
|
|
64
84
|
pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
65
85
|
}
|
|
66
86
|
PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
|
|
67
87
|
for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
|
|
68
88
|
const value = match[1] ?? "";
|
|
69
|
-
if (!value)
|
|
89
|
+
if (!value || isPlaceholderValue(value))
|
|
70
90
|
continue;
|
|
71
91
|
pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
72
92
|
}
|
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();
|