@floomhq/floom 1.0.13 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import updateNotifier from "update-notifier";
3
3
  import { login } from "./login.js";
4
4
  import { publish } from "./publish.js";
5
5
  import { whoami } from "./whoami.js";
6
- import { init } from "./init.js";
6
+ import { init, INIT_TEMPLATES } from "./init.js";
7
7
  import { deleteConfig, readConfig } from "./config.js";
8
8
  import { list } from "./list.js";
9
9
  import { install } from "./install.js";
@@ -22,6 +22,7 @@ import { printError, FloomError } from "./errors.js";
22
22
  import { CLI_VERSION } from "./version.js";
23
23
  const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
24
24
  const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
25
+ const CLI_COMMAND = "npx -y @floomhq/floom";
25
26
  function usage() {
26
27
  const out = `
27
28
  ${c.blue(" ________ ")}
@@ -30,22 +31,22 @@ function usage() {
30
31
  ${c.blue("/ __/ / / /_/ / /_/ / / / / / / ")}
31
32
  ${c.blue("/_/ /_/\\____/\\____/_/ /_/ /_/ ")}
32
33
 
33
- ${c.bold("Floom lets you share AI workflows with anyone.")}
34
+ ${c.bold("Floom syncs your AI skills across agents and machines.")}
34
35
  ${c.dim("A skill is reusable knowledge, instructions, or a workflow for your AI agent.")}
35
36
  ${c.dim("Examples: brand voice, PR review checklist, sales research workflow.")}
36
37
 
37
- ${c.bold("You installed Floom. Copy one recipe:")}
38
+ ${c.bold("Start with one command:")}
38
39
 
39
40
  ${c.bold("1. I received a Floom link")}
40
41
  ${c.dim("Replace <skill-link> with the full Floom URL someone sent you:")}
41
42
  ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("<skill-link> --setup")}
42
43
  ${c.dim('Then tell Claude Code: "Use my Floom skills when they fit this task."')}
43
44
 
44
- ${c.bold("2. I want to make a share link")}
45
- ${c.cyan("npx -y @floomhq/floom init")} ${c.dim("my-skill.md --template brand-voice")}
46
- ${c.dim("Write what your agent needs to know or do in my-skill.md.")}
45
+ ${c.bold("2. I want my own synced skill library")}
46
+ ${c.cyan("npx -y @floomhq/floom init")} ${c.dim("my-skill")}
47
+ ${c.dim("Write what your agent needs to know or do in my-skill/SKILL.md.")}
47
48
  ${c.cyan("npx -y @floomhq/floom login")}
48
- ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("my-skill.md --public")}
49
+ ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("my-skill --public")}
49
50
  ${c.dim("Floom scans it, prints a link, and copies the link when possible.")}
50
51
 
51
52
  ${c.bold("Good to know")}
@@ -55,8 +56,7 @@ function usage() {
55
56
 
56
57
  ${c.bold("Stuck?")}
57
58
  ${c.cyan("npx -y @floomhq/floom doctor")} ${c.dim("Find the problem")}
58
- ${c.cyan("npx -y @floomhq/floom scan my-skill.md")} ${c.dim("Check a file before publishing")}
59
- ${c.cyan("npx -y @floomhq/floom init my-skill.md --template support")} ${c.dim("Start from an example")}
59
+ ${c.cyan("npx -y @floomhq/floom scan my-skill")} ${c.dim("Check a skill before publishing")}
60
60
  ${c.cyan("npx -y @floomhq/floom commands")} ${c.dim("See every command")}
61
61
  ${c.dim("Step-by-step guide")} https://floom.dev/docs/getting-started
62
62
  `;
@@ -64,16 +64,14 @@ function usage() {
64
64
  }
65
65
  function commandUsage() {
66
66
  const out = `
67
- ${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [args]")}
68
- ${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [args]")}
67
+ ${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [flags]")}
68
+ ${c.dim("After global install:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
69
69
 
70
70
  ${c.bold("Commands")}
71
71
  ${c.dim("Skills")}
72
72
  ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
73
73
  ${c.dim("Alias: install")}
74
- ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --update, --json")}
75
- ${c.cyan("update")} ${c.dim("<url>")} Install the latest remote skill content, replacing the local copy
76
- ${c.dim("Alias: add --force")}
74
+ ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
77
75
  ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
78
76
  ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
79
77
 
@@ -83,8 +81,7 @@ function commandUsage() {
83
81
  ${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
84
82
  ${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
85
83
  ${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
86
- ${c.dim(" --skill-version <label>, --share <email>")}
87
- ${c.dim(" --share emails the normal link; no account is needed to add it")}
84
+ ${c.dim(" --skill-version <label>")}
88
85
  ${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
89
86
  ${c.dim("Flags: --add <email>, --remove <email>, --list")}
90
87
 
@@ -99,13 +96,9 @@ function commandUsage() {
99
96
  ${c.dim("Agent setup")}
100
97
  ${c.cyan("setup")} Configure Claude Code or Codex instructions
101
98
  ${c.dim("Alias: connect")}
102
- ${c.dim("Flags: --target claude|codex, --yes, --dry-run, --file <path>")}
103
- ${c.dim(" From a repo, setup writes that repo's CLAUDE.md/AGENTS.md")}
104
- ${c.cyan("agent-prompt")} Print the sentence to paste into your agent
105
- ${c.dim("Alias: paste")}
106
- ${c.dim("Flags: --target claude|codex")}
99
+ ${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
107
100
  ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
108
- ${c.dim("Flags: --target claude|codex, --json")}
101
+ ${c.dim("Flags: --target claude|codex (default: claude)")}
109
102
 
110
103
  ${c.dim("Advanced")}
111
104
  ${c.cyan("library")} Create, browse, and subscribe to libraries
@@ -113,14 +106,12 @@ function commandUsage() {
113
106
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
114
107
  ${c.cyan("mcp")} Print optional MCP setup guidance
115
108
  ${c.cyan("sync")} Preview pull of published, saved, and library skills
116
- ${c.dim("Flags: --target claude|codex")}
109
+ ${c.dim("Flags: --target claude|codex (default: claude)")}
117
110
  ${c.cyan("watch")} Preview polling sync loop
118
- ${c.dim("Flags: --target claude|codex, --interval <seconds>")}
119
111
 
120
112
  ${c.bold("Examples")}
121
113
  ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
122
- ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
123
- ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --share teammate@example.com")}
114
+ ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone --type instruction --public")}
124
115
  ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
125
116
 
126
117
  ${c.bold("Help")}
@@ -133,8 +124,6 @@ function commandUsage() {
133
124
  process.stdout.write(out);
134
125
  }
135
126
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
136
- const INIT_TEMPLATES = new Set(["generic", "brand-voice", "pr-review", "sales", "support", "onboarding"]);
137
- const MAX_SHARE_RECIPIENTS = 25;
138
127
  const INSTALL_TARGETS = new Set([
139
128
  "claude_skill",
140
129
  "memory",
@@ -155,7 +144,7 @@ function readFlagValue(argv, index, flag) {
155
144
  return { value, nextIndex: index + 1 };
156
145
  }
157
146
  function parseFlags(argv) {
158
- const out = { visibility: "unlisted", update: false, shareEmails: [], explicitVisibility: false, rest: [] };
147
+ const out = { visibility: "unlisted", update: false, rest: [] };
159
148
  let visibilityFlag = null;
160
149
  for (let i = 0; i < argv.length; i++) {
161
150
  const a = argv[i] ?? "";
@@ -166,15 +155,12 @@ function parseFlags(argv) {
166
155
  }
167
156
  visibilityFlag = nextVisibility;
168
157
  out.visibility = nextVisibility;
169
- out.explicitVisibility = true;
170
158
  }
171
159
  else if (a === "--update") {
172
- 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.");
160
+ throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --update\` is planned for a later Floom release.`);
173
161
  }
174
162
  else if (a === "--share" || a.startsWith("--share=")) {
175
- const { value, nextIndex } = readFlagValue(argv, i, "--share");
176
- out.shareEmails.push(...parseEmailList(value, "--share"));
177
- i = nextIndex;
163
+ throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --share\` is planned for a later Floom release.`);
178
164
  }
179
165
  else if (a === "--type" || a.startsWith("--type=")) {
180
166
  const { value, nextIndex } = readFlagValue(argv, i, "--type");
@@ -204,36 +190,13 @@ function parseFlags(argv) {
204
190
  throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
205
191
  }
206
192
  else if (a.startsWith("--")) {
207
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom publish skill.md --type instruction --public`.");
193
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} publish my-skill --type instruction --public\`.`);
208
194
  }
209
195
  else
210
196
  out.rest.push(a);
211
197
  }
212
- if (out.shareEmails.length > 0) {
213
- out.shareEmails = dedupeEmails(out.shareEmails);
214
- if (out.shareEmails.length > MAX_SHARE_RECIPIENTS) {
215
- throw new FloomError("Too many --share recipients.", `Use ${MAX_SHARE_RECIPIENTS} email addresses or fewer.`);
216
- }
217
- if (out.visibility === "private") {
218
- 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.");
219
- }
220
- }
221
198
  return out;
222
199
  }
223
- function parseEmailList(value, source) {
224
- const emails = value.split(",").map((email) => email.trim().toLowerCase()).filter(Boolean);
225
- if (emails.length === 0)
226
- throw new FloomError(`Missing value for ${source}.`, `Try \`${source} teammate@example.com\`.`);
227
- for (const email of emails) {
228
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
229
- throw new FloomError(`Invalid email for ${source}: ${email}`, "Use an address like teammate@example.com.");
230
- }
231
- }
232
- return emails;
233
- }
234
- function dedupeEmails(emails) {
235
- return [...new Set(emails)];
236
- }
237
200
  function parseShareFlags(argv) {
238
201
  const out = { list: false, add: [], remove: [] };
239
202
  for (let i = 0; i < argv.length; i++) {
@@ -252,48 +215,48 @@ function parseShareFlags(argv) {
252
215
  i = nextIndex;
253
216
  }
254
217
  else if (a.startsWith("--")) {
255
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
218
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} share <slug> --add person@example.com\`.`);
256
219
  }
257
220
  else if (!out.slug) {
258
221
  out.slug = a;
259
222
  }
260
223
  else {
261
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
224
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} share <slug> --add person@example.com\`.`);
262
225
  }
263
226
  }
264
227
  if (!out.slug)
265
- throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
228
+ throw new FloomError("Missing skill slug.", `Try \`${CLI_COMMAND} share <slug> --add person@example.com\`.`);
266
229
  if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
267
230
  throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
268
231
  }
269
232
  if (!out.list && out.add.length === 0 && out.remove.length === 0) {
270
- throw new FloomError("Missing share action.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
233
+ throw new FloomError("Missing share action.", `Try \`${CLI_COMMAND} share <slug> --add person@example.com\`.`);
271
234
  }
272
235
  return out;
273
236
  }
274
237
  function parseInitArgs(argv) {
275
238
  let file;
276
- let template = "generic";
239
+ let template;
277
240
  for (let i = 0; i < argv.length; i++) {
278
241
  const a = argv[i] ?? "";
279
242
  if (a === "--template" || a.startsWith("--template=")) {
280
243
  const { value, nextIndex } = readFlagValue(argv, i, "--template");
281
- if (!INIT_TEMPLATES.has(value)) {
282
- throw new FloomError(`Invalid --template: ${value}`, "Use one of: generic, brand-voice, pr-review, sales, support, onboarding.");
244
+ if (!INIT_TEMPLATES.includes(value)) {
245
+ throw new FloomError(`Invalid --template: ${value}`, `Use one of: ${INIT_TEMPLATES.join(", ")}.`);
283
246
  }
284
247
  template = value;
285
248
  i = nextIndex;
286
249
  continue;
287
250
  }
288
251
  if (a.startsWith("--")) {
289
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom init skill.md --template brand-voice`.");
252
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} init my-skill\`.`);
290
253
  }
291
254
  if (file) {
292
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom init skill.md`.");
255
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} init my-skill\`.`);
293
256
  }
294
257
  file = a;
295
258
  }
296
- return file ? { file, template } : { template };
259
+ return { ...(file ? { file } : {}), ...(template ? { template } : {}) };
297
260
  }
298
261
  function parseListFlags(argv) {
299
262
  const out = { json: false };
@@ -301,10 +264,31 @@ function parseListFlags(argv) {
301
264
  if (a === "--json")
302
265
  out.json = true;
303
266
  else if (a.startsWith("--")) {
304
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom list --help` for usage.");
267
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} list --help\` for usage.`);
268
+ }
269
+ else {
270
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} list --json\`.`);
271
+ }
272
+ }
273
+ return out;
274
+ }
275
+ function parseSyncFlags(argv) {
276
+ const out = {};
277
+ for (let i = 0; i < argv.length; i++) {
278
+ const a = argv[i] ?? "";
279
+ if (a === "--target" || a.startsWith("--target=")) {
280
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
281
+ if (value !== "claude" && value !== "codex") {
282
+ throw new FloomError("Invalid --target.", "Use claude or codex.");
283
+ }
284
+ out.target = value;
285
+ i = nextIndex;
286
+ }
287
+ else if (a.startsWith("--")) {
288
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} sync --target claude\`.`);
305
289
  }
306
290
  else {
307
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom list --json`.");
291
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} sync --target claude\`.`);
308
292
  }
309
293
  }
310
294
  return out;
@@ -315,11 +299,11 @@ function parseInfoFlags(argv) {
315
299
  if (a === "--json")
316
300
  out.json = true;
317
301
  else if (a.startsWith("--"))
318
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
302
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} info <slug> --json\`.`);
319
303
  else if (!out.slug)
320
304
  out.slug = a;
321
305
  else
322
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
306
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} info <slug> --json\`.`);
323
307
  }
324
308
  return out;
325
309
  }
@@ -328,7 +312,6 @@ function parseAddArgs(argv) {
328
312
  let target;
329
313
  let setup = false;
330
314
  let force = false;
331
- let json = false;
332
315
  for (let i = 0; i < argv.length; i++) {
333
316
  const a = argv[i] ?? "";
334
317
  if (a === "--target" || a.startsWith("--target=")) {
@@ -342,73 +325,22 @@ function parseAddArgs(argv) {
342
325
  else if (a === "--setup") {
343
326
  setup = true;
344
327
  }
345
- else if (a === "--json") {
346
- json = true;
347
- }
348
- else if (a === "--force" || a === "--update") {
328
+ else if (a === "--force") {
349
329
  force = true;
350
330
  }
351
331
  else if (a.startsWith("--")) {
352
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
332
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} add <url-or-slug> --setup\`.`);
353
333
  }
354
334
  else if (slug) {
355
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
335
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} add <url-or-slug> --setup\`.`);
356
336
  }
357
337
  else
358
338
  slug = a;
359
339
  }
360
340
  if (!slug) {
361
- throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom add <url-or-slug> --setup`");
362
- }
363
- if (json && setup) {
364
- throw new FloomError("Conflicting add flags.", "Use `--json` for machine output or `--setup` for interactive agent setup.");
365
- }
366
- return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
367
- }
368
- function parseTargetFlag(value) {
369
- if (value !== "claude" && value !== "codex") {
370
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
371
- }
372
- return value;
373
- }
374
- function parseDoctorArgs(argv) {
375
- const out = { json: false };
376
- for (let i = 0; i < argv.length; i++) {
377
- const a = argv[i] ?? "";
378
- if (a === "--json")
379
- out.json = true;
380
- else if (a === "--target" || a.startsWith("--target=")) {
381
- const { value, nextIndex } = readFlagValue(argv, i, "--target");
382
- out.target = parseTargetFlag(value);
383
- i = nextIndex;
384
- }
385
- else if (a.startsWith("--"))
386
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --target codex --json`.");
387
- else
388
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --target codex --json`.");
389
- }
390
- return out;
391
- }
392
- function parseAgentPromptArgs(argv) {
393
- const out = {};
394
- for (let i = 0; i < argv.length; i++) {
395
- const a = argv[i] ?? "";
396
- if (a === "--target" || a.startsWith("--target=")) {
397
- const { value, nextIndex } = readFlagValue(argv, i, "--target");
398
- if (value !== "claude" && value !== "codex") {
399
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
400
- }
401
- out.target = value;
402
- i = nextIndex;
403
- }
404
- else if (a.startsWith("--")) {
405
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
406
- }
407
- else {
408
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
409
- }
341
+ throw new FloomError("Missing skill slug.", `Try: \`${CLI_COMMAND} add <url-or-slug> --setup\``);
410
342
  }
411
- return out;
343
+ return target ? { slug, target, setup, force } : { slug, setup, force };
412
344
  }
413
345
  function parseSearchFlags(argv) {
414
346
  const out = { json: false };
@@ -431,7 +363,7 @@ function parseSearchFlags(argv) {
431
363
  i = nextIndex;
432
364
  }
433
365
  else if (a.startsWith("--")) {
434
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom search \"support tone\" --type instruction`.");
366
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} search "support tone" --type instruction\`.`);
435
367
  }
436
368
  else {
437
369
  terms.push(a);
@@ -446,11 +378,11 @@ function parseDeleteFlags(argv) {
446
378
  if (a === "--yes" || a === "-y")
447
379
  out.yes = true;
448
380
  else if (a.startsWith("--"))
449
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
381
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} delete <slug> --yes\`.`);
450
382
  else if (!out.slug)
451
383
  out.slug = a;
452
384
  else
453
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
385
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} delete <slug> --yes\`.`);
454
386
  }
455
387
  return out;
456
388
  }
@@ -484,12 +416,33 @@ function parseSetupFlags(argv) {
484
416
  i = nextIndex;
485
417
  }
486
418
  else if (a.startsWith("--")) {
487
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom setup --target codex --dry-run`.");
419
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} setup --target codex --dry-run\`.`);
488
420
  }
489
421
  else if (!out.file)
490
422
  out.file = a;
491
423
  else
492
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom setup --target claude --yes`.");
424
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} setup --target claude --yes\`.`);
425
+ }
426
+ return out;
427
+ }
428
+ function parseDoctorFlags(argv) {
429
+ const out = {};
430
+ for (let i = 0; i < argv.length; i++) {
431
+ const a = argv[i] ?? "";
432
+ if (a === "--target" || a.startsWith("--target=")) {
433
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
434
+ if (value !== "claude" && value !== "codex") {
435
+ throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
436
+ }
437
+ out.target = value;
438
+ i = nextIndex;
439
+ }
440
+ else if (a.startsWith("--")) {
441
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex\`.`);
442
+ }
443
+ else {
444
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} doctor --target claude\`.`);
445
+ }
493
446
  }
494
447
  return out;
495
448
  }
@@ -548,21 +501,17 @@ function parseLibraryCreateFlags(argv) {
548
501
  else if (a === "--unlisted")
549
502
  out.visibility = "unlisted";
550
503
  else if (a.startsWith("--")) {
551
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
504
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} library create team-onboarding --name "Team onboarding"\`.`);
552
505
  }
553
506
  else if (!out.slug)
554
507
  out.slug = a;
555
508
  else
556
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom library create <slug> --name <name>`.");
509
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} library create <slug> --name <name>\`.`);
557
510
  }
558
511
  return out;
559
512
  }
560
513
  async function runLibrary(argv) {
561
514
  const [subcommand, ...rest] = argv;
562
- if (!subcommand || subcommand === "--json") {
563
- await libraryList(parseListFlags(argv));
564
- return;
565
- }
566
515
  switch (subcommand ?? "list") {
567
516
  case "list": {
568
517
  const flags = parseListFlags(rest);
@@ -572,9 +521,9 @@ async function runLibrary(argv) {
572
521
  case "create": {
573
522
  const flags = parseLibraryCreateFlags(rest);
574
523
  if (!flags.slug)
575
- throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
524
+ throw new FloomError("Missing library slug.", `Try \`${CLI_COMMAND} library create team-onboarding --name "Team onboarding"\`.`);
576
525
  if (!flags.name)
577
- throw new FloomError("Missing --name.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
526
+ throw new FloomError("Missing --name.", `Try \`${CLI_COMMAND} library create team-onboarding --name "Team onboarding"\`.`);
578
527
  await libraryCreate({
579
528
  slug: flags.slug,
580
529
  name: flags.name,
@@ -587,10 +536,10 @@ async function runLibrary(argv) {
587
536
  const flags = parseFolderTagFlags(rest);
588
537
  const [librarySlug, skillSlug] = flags.rest;
589
538
  if (!librarySlug || !skillSlug) {
590
- throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
539
+ throw new FloomError("Missing library or skill slug.", `Try \`${CLI_COMMAND} library add team-onboarding support-tone --folder support\`.`);
591
540
  }
592
541
  if (flags.rest.length > 2) {
593
- throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
542
+ throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, `Try \`${CLI_COMMAND} library add team-onboarding support-tone --folder support\`.`);
594
543
  }
595
544
  await libraryAddSkill({
596
545
  librarySlug,
@@ -604,10 +553,10 @@ async function runLibrary(argv) {
604
553
  case "rm": {
605
554
  const [librarySlug, skillSlug] = rest;
606
555
  if (!librarySlug || !skillSlug) {
607
- throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
556
+ throw new FloomError("Missing library or skill slug.", `Try \`${CLI_COMMAND} library remove team-onboarding support-tone\`.`);
608
557
  }
609
558
  if (rest.length > 2) {
610
- throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
559
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, `Try \`${CLI_COMMAND} library remove team-onboarding support-tone\`.`);
611
560
  }
612
561
  await libraryRemoveSkill(librarySlug, skillSlug);
613
562
  return;
@@ -615,9 +564,9 @@ async function runLibrary(argv) {
615
564
  case "subscribe": {
616
565
  const slug = rest[0];
617
566
  if (!slug)
618
- throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library subscribe superpowers`.");
567
+ throw new FloomError("Missing library slug.", `Try \`${CLI_COMMAND} library subscribe superpowers\`.`);
619
568
  if (rest.length > 1) {
620
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library subscribe superpowers`.");
569
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, `Try \`${CLI_COMMAND} library subscribe superpowers\`.`);
621
570
  }
622
571
  await librarySubscribe(slug);
623
572
  return;
@@ -625,9 +574,9 @@ async function runLibrary(argv) {
625
574
  case "unsubscribe": {
626
575
  const slug = rest[0];
627
576
  if (!slug)
628
- throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
577
+ throw new FloomError("Missing library slug.", `Try \`${CLI_COMMAND} library unsubscribe superpowers\`.`);
629
578
  if (rest.length > 1) {
630
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
579
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, `Try \`${CLI_COMMAND} library unsubscribe superpowers\`.`);
631
580
  }
632
581
  await libraryUnsubscribe(slug);
633
582
  return;
@@ -649,34 +598,11 @@ function parseWatchFlags(argv) {
649
598
  out.intervalSeconds = interval;
650
599
  i = nextIndex;
651
600
  }
652
- else if (a === "--target" || a.startsWith("--target=")) {
653
- const { value, nextIndex } = readFlagValue(argv, i, "--target");
654
- out.target = parseTargetFlag(value);
655
- i = nextIndex;
656
- }
657
- else if (a.startsWith("--")) {
658
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --target codex --interval 60`.");
659
- }
660
- else {
661
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --target codex --interval 60`.");
662
- }
663
- }
664
- return out;
665
- }
666
- function parseSyncFlags(argv) {
667
- const out = {};
668
- for (let i = 0; i < argv.length; i++) {
669
- const a = argv[i] ?? "";
670
- if (a === "--target" || a.startsWith("--target=")) {
671
- const { value, nextIndex } = readFlagValue(argv, i, "--target");
672
- out.target = parseTargetFlag(value);
673
- i = nextIndex;
674
- }
675
601
  else if (a.startsWith("--")) {
676
- throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom sync --target codex`.");
602
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
677
603
  }
678
604
  else {
679
- throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom sync --target codex`.");
605
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
680
606
  }
681
607
  }
682
608
  return out;
@@ -697,10 +623,6 @@ function parseSingleFileArg(argv, usageHint) {
697
623
  throw new FloomError("Missing file argument.", usageHint);
698
624
  return file;
699
625
  }
700
- function agentPrompt(target = "claude") {
701
- const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
702
- process.stdout.write(`Before recreating behavior from scratch, search ${folder} for matching Floom skills. If none fit, search Floom with \`npx -y @floomhq/floom search <query>\`, show useful matches, and ask before installing anything.\n`);
703
- }
704
626
  function sleep(ms, signal) {
705
627
  if (signal.aborted)
706
628
  return Promise.resolve();
@@ -712,10 +634,10 @@ function sleep(ms, signal) {
712
634
  }, { once: true });
713
635
  });
714
636
  }
715
- async function watch(intervalSeconds, target = "claude") {
637
+ async function watch(intervalSeconds) {
716
638
  const cfg = await readConfig();
717
639
  if (!cfg) {
718
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` before watch, or use `npx -y @floomhq/floom add <link>` without an account.");
640
+ throw new FloomError("Not signed in.", `Run \`${CLI_COMMAND} login\` before \`${CLI_COMMAND} watch\`, or use \`${CLI_COMMAND} add <link>\` without an account.`);
719
641
  }
720
642
  const controller = new AbortController();
721
643
  let stopping = false;
@@ -729,9 +651,9 @@ async function watch(intervalSeconds, target = "claude") {
729
651
  };
730
652
  process.on("SIGINT", stop);
731
653
  process.on("SIGTERM", stop);
732
- process.stdout.write(`${symbols.bullet} Watching Floom ${target} sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
654
+ process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
733
655
  while (!controller.signal.aborted) {
734
- await sync({ spinner: false, quietUnchanged: true, target });
656
+ await sync({ spinner: false, quietUnchanged: true });
735
657
  await sleep(intervalSeconds * 1000, controller.signal);
736
658
  }
737
659
  }
@@ -751,10 +673,10 @@ async function main() {
751
673
  // never block on update-notifier
752
674
  }
753
675
  }
754
- // Subcommand --help: any rest arg = --help/-h/help → show top-level reference.
676
+ // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
755
677
  // Subcommands are simple enough that one help screen is fine for Version 1.
756
678
  if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
757
- commandUsage();
679
+ usage();
758
680
  return;
759
681
  }
760
682
  try {
@@ -768,7 +690,7 @@ async function main() {
768
690
  commandUsage();
769
691
  return;
770
692
  case "commands":
771
- rejectArgs(rest, "Try `npx -y @floomhq/floom commands`.");
693
+ rejectArgs(rest, `Try \`${CLI_COMMAND} commands\`.`);
772
694
  commandUsage();
773
695
  return;
774
696
  case "--version":
@@ -776,31 +698,31 @@ async function main() {
776
698
  process.stdout.write(`${CLI_VERSION}\n`);
777
699
  return;
778
700
  case "login":
779
- rejectArgs(rest, "Try `npx -y @floomhq/floom login`.");
701
+ rejectArgs(rest, `Try \`${CLI_COMMAND} login\`.`);
780
702
  await login();
781
703
  return;
782
704
  case "logout":
783
- rejectArgs(rest, "Try `npx -y @floomhq/floom logout`.");
705
+ rejectArgs(rest, `Try \`${CLI_COMMAND} logout\`.`);
784
706
  await deleteConfig();
785
707
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
786
708
  return;
787
709
  case "whoami":
788
- rejectArgs(rest, "Try `npx -y @floomhq/floom whoami`.");
710
+ rejectArgs(rest, `Try \`${CLI_COMMAND} whoami\`.`);
789
711
  await whoami();
790
712
  return;
791
713
  case "init": {
792
714
  const flags = parseInitArgs(rest);
793
- await init(flags.file, flags.template);
715
+ await init(flags.file, flags.template ? { template: flags.template } : {});
794
716
  return;
795
717
  }
796
718
  case "publish": {
797
719
  const flags = parseFlags(rest);
798
720
  const file = flags.rest[0];
799
721
  if (!file) {
800
- throw new FloomError("Missing file argument.", "Try: `npx -y @floomhq/floom publish skill.md`");
722
+ throw new FloomError("Missing file argument.", `Try: \`${CLI_COMMAND} publish my-skill\``);
801
723
  }
802
724
  if (flags.rest.length > 1) {
803
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `npx -y @floomhq/floom publish skill.md`");
725
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, `Try: \`${CLI_COMMAND} publish my-skill\``);
804
726
  }
805
727
  await publish({
806
728
  file,
@@ -809,12 +731,11 @@ async function main() {
809
731
  ...(flags.assetType ? { assetType: flags.assetType } : {}),
810
732
  ...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
811
733
  ...(flags.version ? { version: flags.version } : {}),
812
- ...(flags.shareEmails.length > 0 ? { sharedWithEmails: flags.shareEmails } : {}),
813
734
  });
814
735
  return;
815
736
  }
816
737
  case "scan": {
817
- const file = parseSingleFileArg(rest, "Try `npx -y @floomhq/floom scan skill.md`.");
738
+ const file = parseSingleFileArg(rest, `Try \`${CLI_COMMAND} scan my-skill\`.`);
818
739
  await scanSkill(file);
819
740
  return;
820
741
  }
@@ -843,7 +764,7 @@ async function main() {
843
764
  case "search": {
844
765
  const flags = parseSearchFlags(rest);
845
766
  if (!flags.query) {
846
- throw new FloomError("Missing search query.", "Try: `npx -y @floomhq/floom search \"support tone\"`.");
767
+ throw new FloomError("Missing search query.", `Try: \`${CLI_COMMAND} search "support tone"\`.`);
847
768
  }
848
769
  await search({
849
770
  query: flags.query,
@@ -860,20 +781,6 @@ async function main() {
860
781
  ...(flags.target ? { target: flags.target } : {}),
861
782
  setup: flags.setup,
862
783
  force: flags.force,
863
- json: flags.json,
864
- });
865
- if (flags.setup) {
866
- await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
867
- }
868
- return;
869
- }
870
- case "update": {
871
- const flags = parseAddArgs(rest);
872
- await install(flags.slug, {
873
- ...(flags.target ? { target: flags.target } : {}),
874
- setup: flags.setup,
875
- force: true,
876
- json: flags.json,
877
784
  });
878
785
  if (flags.setup) {
879
786
  await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
@@ -881,10 +788,7 @@ async function main() {
881
788
  return;
882
789
  }
883
790
  case "sync":
884
- {
885
- const flags = parseSyncFlags(rest);
886
- await sync(flags);
887
- }
791
+ await sync(parseSyncFlags(rest));
888
792
  return;
889
793
  case "setup":
890
794
  case "connect": {
@@ -892,15 +796,9 @@ async function main() {
892
796
  await setupAgent(flags);
893
797
  return;
894
798
  }
895
- case "agent-prompt":
896
- case "paste": {
897
- const flags = parseAgentPromptArgs(rest);
898
- agentPrompt(flags.target ?? "claude");
899
- return;
900
- }
901
799
  case "watch": {
902
800
  const flags = parseWatchFlags(rest);
903
- await watch(flags.intervalSeconds, flags.target ?? "claude");
801
+ await watch(flags.intervalSeconds);
904
802
  return;
905
803
  }
906
804
  case "delete":
@@ -917,29 +815,26 @@ async function main() {
917
815
  const flags = parseFolderTagFlags(rest);
918
816
  const slug = flags.rest[0];
919
817
  if (!slug) {
920
- throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
818
+ throw new FloomError("Missing skill slug.", `Try \`${CLI_COMMAND} move support-tone --folder support/tone\`.`);
921
819
  }
922
820
  if (flags.folder === undefined) {
923
821
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
924
822
  }
925
823
  if (flags.rest.length > 1) {
926
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
824
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, `Try \`${CLI_COMMAND} move support-tone --folder support/tone\`.`);
927
825
  }
928
826
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
929
827
  return;
930
828
  }
931
829
  case "mcp":
932
- rejectArgs(rest, "Try `npx -y @floomhq/floom mcp`.");
830
+ rejectArgs(rest, `Try \`${CLI_COMMAND} mcp\`.`);
933
831
  printMcpSetup();
934
832
  return;
935
833
  case "doctor":
936
- {
937
- const flags = parseDoctorArgs(rest);
938
- await doctor(flags);
939
- }
834
+ await doctor(parseDoctorFlags(rest));
940
835
  return;
941
836
  default:
942
- throw new FloomError(`Unknown command: ${cmd}`, "Run `npx -y @floomhq/floom --help` to see available commands.");
837
+ throw new FloomError(`Unknown command: ${cmd}`, `Run \`${CLI_COMMAND} --help\` to see available commands.`);
943
838
  }
944
839
  }
945
840
  catch (e) {