@floomhq/floom 1.0.14 → 1.0.17

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