@floomhq/floom 1.0.25 → 1.0.27

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 CHANGED
@@ -4,6 +4,7 @@ Sync AI skills across agents and machines. Publish from your terminal, then add
4
4
 
5
5
  ```bash
6
6
  npm install -g @floomhq/floom
7
+ floom init my-skill.md
7
8
  floom init my-skill
8
9
  floom login
9
10
  floom publish my-skill
@@ -21,9 +22,9 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
21
22
 
22
23
  ## Commands
23
24
 
24
- - `npx -y @floomhq/floom login` — sign in with Google. Use `--provider github` for GitHub. New accounts are created on first login. Token stored at `~/.floom/config.json`.
25
+ - `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
25
26
  - `npx -y @floomhq/floom init [path]` — create a starter skill folder at `<path>/SKILL.md`. Passing an existing-style `file.md` path still creates that Markdown file.
26
- - `npx -y @floomhq/floom publish <path>` — upload a skill folder or Markdown file. Folder packages use `<slug>/SKILL.md` plus optional `references/`, `examples/`, `scripts/`, and `assets/`. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, `--skill-version <label>`, and `--update [slug-or-url]`.
27
+ - `npx -y @floomhq/floom publish <path>` — upload a skill folder or Markdown file. Folder packages use `<slug>/SKILL.md` plus optional `references/`, `examples/`, `scripts/`, and `assets/`. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--skill-version <label>`.
27
28
  - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
28
29
  - `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
29
30
  - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>/SKILL.md` with supporting package files. Optional `--target claude|codex`, `--setup` to connect the agent, and `--force` to replace an existing local copy.
@@ -32,8 +33,8 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
32
33
  - `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
33
34
  - `npx -y @floomhq/floom connect` — alias for setup.
34
35
  - `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
35
- - `npx -y @floomhq/floom sync` — pull your published, saved, and subscribed library skills into Claude or Codex. Optional `--target claude|codex`.
36
- - `npx -y @floomhq/floom watch` — run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
36
+ - `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into Claude or Codex. Optional `--target claude|codex`.
37
+ - `npx -y @floomhq/floom watch` — preview: run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
37
38
  - `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
38
39
  - `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
39
40
  - `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
@@ -74,7 +75,7 @@ version: 0.1.0
74
75
 
75
76
  Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
76
77
 
77
- `floom sync` and `floom watch` pull published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
78
+ `floom sync` and `floom watch` are v1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
78
79
  The manifest records hashes for files Floom previously wrote. Sync writes missing files only.
79
80
  Remote updates, existing untracked files, and locally edited tracked files are skipped as conflicts.
80
81
  Symlinks are never followed. To replace a local skill manually, run `floom add <url-or-slug> --force`.
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { info } from "./info.js";
11
11
  import { deleteSkill } from "./delete.js";
12
12
  import { doctor } from "./doctor.js";
13
13
  import { sync } from "./sync.js";
14
+ import { watchPush } from "./push-watch.js";
14
15
  import { printMcpSetup } from "./mcp.js";
15
16
  import { setupAgent } from "./setup.js";
16
17
  import { search } from "./search.js";
@@ -20,6 +21,7 @@ import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librar
20
21
  import { c, symbols } from "./ui.js";
21
22
  import { printError, FloomError } from "./errors.js";
22
23
  import { CLI_VERSION } from "./version.js";
24
+ import { TARGET_HINT, isAgentTarget } from "./targets.js";
23
25
  const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
24
26
  const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
25
27
  const CLI_COMMAND = "npx -y @floomhq/floom";
@@ -71,9 +73,7 @@ function commandUsage() {
71
73
  ${c.dim("Skills")}
72
74
  ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
73
75
  ${c.dim("Alias: install")}
74
- ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --json")}
75
- ${c.cyan("update")} ${c.dim("<url>")} Refresh or migrate a local skill
76
- ${c.dim("Flags: --target claude|codex (default: claude), --json")}
76
+ ${c.dim(`Flags: --target ${TARGET_HINT} (default: claude), --setup, --force`)}
77
77
  ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
78
78
  ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
79
79
 
@@ -83,7 +83,7 @@ function commandUsage() {
83
83
  ${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
84
84
  ${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
85
85
  ${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
86
- ${c.dim(" --skill-version <label>, --update [slug-or-url]")}
86
+ ${c.dim(" --skill-version <label>")}
87
87
  ${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
88
88
  ${c.dim("Flags: --add <email>, --remove <email>, --list")}
89
89
 
@@ -98,22 +98,19 @@ function commandUsage() {
98
98
  ${c.dim("Agent setup")}
99
99
  ${c.cyan("setup")} Configure Claude Code or Codex instructions
100
100
  ${c.dim("Alias: connect")}
101
- ${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
101
+ ${c.dim(`Flags: --target ${TARGET_HINT}, --yes, --dry-run`)}
102
102
  ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
103
- ${c.dim("Flags: --target claude|codex (default: claude), --json")}
103
+ ${c.dim(`Flags: --target ${TARGET_HINT} (default: claude)`)}
104
104
 
105
105
  ${c.dim("Advanced")}
106
106
  ${c.cyan("library")} Create, browse, and subscribe to libraries
107
107
  ${c.dim("Alias: lib")}
108
108
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
109
109
  ${c.cyan("mcp")} Print optional MCP setup guidance
110
- ${c.cyan("sync")} Pull published, saved, and library skills
111
- ${c.dim("Flags: --target claude|codex (default: claude)")}
112
- ${c.cyan("watch")} Run a polling sync loop
113
- ${c.dim("Flags: --target claude|codex (default: claude), --interval <seconds>")}
114
- ${c.cyan("agent-prompt")} Print the one-line agent instruction
115
- ${c.dim("Alias: paste")}
116
- ${c.dim("Flags: --target claude|codex (default: claude)")}
110
+ ${c.cyan("sync")} Preview pull of published, saved, and library skills
111
+ ${c.dim(`Flags: --target ${TARGET_HINT} (default: claude)`)}
112
+ ${c.cyan("watch")} Preview polling sync loop
113
+ ${c.dim(`Flags: --push, --no-yolo, --target ${TARGET_HINT}`)}
117
114
 
118
115
  ${c.bold("Examples")}
119
116
  ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
@@ -129,120 +126,6 @@ function commandUsage() {
129
126
  `;
130
127
  process.stdout.write(out);
131
128
  }
132
- function subcommandUsage(cmd) {
133
- const key = cmd === "install" ? "add" : cmd === "rm" ? "delete" : cmd === "connect" ? "setup" : cmd === "paste" ? "agent-prompt" : cmd;
134
- const usageByCommand = {
135
- add: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} add`)} ${c.dim("<url-or-slug> [flags]")}
136
-
137
- Install a Floom skill into a local agent skills folder.
138
-
139
- ${c.bold("Flags")}
140
- ${c.cyan("--target")} ${c.dim("claude|codex")} Install for Claude Code or Codex. Default: claude.
141
- ${c.cyan("--setup")} Add Floom guidance to the matching agent instructions file.
142
- ${c.cyan("--force")} Replace the local copy when remote content differs.
143
- ${c.cyan("--json")} Print machine-readable install output.
144
-
145
- ${c.bold("Examples")}
146
- ${c.cyan(`${CLI_COMMAND} add https://floom.dev/s/ffas93ud --setup`)}
147
- ${c.cyan(`${CLI_COMMAND} add ffas93ud --target codex --json`)}
148
- `,
149
- update: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} update`)} ${c.dim("<url-or-slug> [flags]")}
150
-
151
- Refresh or migrate one local skill through the same installer as add.
152
-
153
- ${c.bold("Flags")}
154
- ${c.cyan("--target")} ${c.dim("claude|codex")} Update Claude Code or Codex local skills. Default: claude.
155
- ${c.cyan("--setup")} Also refresh agent setup instructions.
156
- ${c.cyan("--json")} Print machine-readable install output.
157
- `,
158
- doctor: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} doctor`)} ${c.dim("[flags]")}
159
-
160
- Diagnose auth, API reachability, PATH collisions, MCP setup, and local skills folders.
161
-
162
- ${c.bold("Flags")}
163
- ${c.cyan("--target")} ${c.dim("claude|codex")} Check Claude Code or Codex paths. Default: claude.
164
- ${c.cyan("--json")} Print structured checks for scripts.
165
- `,
166
- login: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} login`)} ${c.dim("[flags]")}
167
-
168
- Sign in through browser OAuth.
169
-
170
- ${c.bold("Flags")}
171
- ${c.cyan("--provider")} ${c.dim("google|github")} OAuth provider. Default: google.
172
- `,
173
- publish: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} publish`)} ${c.dim("<path> [flags]")}
174
-
175
- Scan and publish a Markdown skill file or skill folder. Prints a Floom share URL.
176
-
177
- ${c.bold("Flags")}
178
- ${c.cyan("--public")} | ${c.cyan("--private")} | ${c.cyan("--unlisted")} Set visibility. Default: unlisted.
179
- ${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
180
- ${c.cyan("--installs-as")} ${c.dim("claude_skill|memory|rule|codex_instruction|opencode_instruction|cursor_rule|other")}
181
- ${c.cyan("--skill-version")} ${c.dim("<label>")}
182
- ${c.cyan("--update")} ${c.dim("[slug-or-url]")}
183
- `,
184
- init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
185
-
186
- Create a starter skill folder with SKILL.md.
187
-
188
- ${c.bold("Flags")}
189
- ${c.cyan("--template")} ${c.dim(`generic|${INIT_TEMPLATES.filter((t) => t !== "generic").join("|")}`)}
190
- `,
191
- sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
192
-
193
- Pull published, saved, and subscribed library skills.
194
-
195
- ${c.bold("Flags")}
196
- ${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
197
- `,
198
- watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
199
-
200
- Poll sync on an interval.
201
-
202
- ${c.bold("Flags")}
203
- ${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
204
- ${c.cyan("--interval")} ${c.dim("<seconds>")} Poll interval. Minimum: 10.
205
- `,
206
- setup: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} setup`)} ${c.dim("[flags]")}
207
-
208
- Add Floom guidance to CLAUDE.md or AGENTS.md.
209
-
210
- ${c.bold("Flags")}
211
- ${c.cyan("--target")} ${c.dim("claude|codex")} Configure Claude Code or Codex.
212
- ${c.cyan("--file")} ${c.dim("<path>")} Write a specific instructions file.
213
- ${c.cyan("--yes")} Write without prompting.
214
- ${c.cyan("--dry-run")} Preview the change only.
215
- `,
216
- search: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} search`)} ${c.dim("<query> [flags]")}
217
-
218
- Find public skills and libraries.
219
-
220
- ${c.bold("Flags")}
221
- ${c.cyan("--library")} ${c.dim("<slug>")} Limit search to one library.
222
- ${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
223
- ${c.cyan("--json")} Print machine-readable results.
224
- `,
225
- info: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} info`)} ${c.dim("<url-or-slug> [flags]")}
226
-
227
- Show public skill metadata.
228
-
229
- ${c.bold("Flags")}
230
- ${c.cyan("--json")} Print machine-readable metadata.
231
- `,
232
- "agent-prompt": `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} agent-prompt`)} ${c.dim("[flags]")}
233
-
234
- Print the one-line instruction to paste into your agent.
235
-
236
- ${c.bold("Flags")}
237
- ${c.cyan("--target")} ${c.dim("claude|codex")} Print the right local skills path. Default: claude.
238
- `,
239
- };
240
- const body = key ? usageByCommand[key] : undefined;
241
- if (!body)
242
- return false;
243
- process.stdout.write(`${body}\n`);
244
- return true;
245
- }
246
129
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
247
130
  const INSTALL_TARGETS = new Set([
248
131
  "claude_skill",
@@ -263,25 +146,8 @@ function readFlagValue(argv, index, flag) {
263
146
  throw new FloomError(`Missing value for ${flag}.`);
264
147
  return { value, nextIndex: index + 1 };
265
148
  }
266
- function parseLoginArgs(argv) {
267
- let provider = "google";
268
- for (let i = 0; i < argv.length; i += 1) {
269
- const a = argv[i] ?? "";
270
- if (a === "--provider" || a.startsWith("--provider=")) {
271
- const { value, nextIndex } = readFlagValue(argv, i, "--provider");
272
- if (value !== "google" && value !== "github") {
273
- throw new FloomError("Invalid --provider.", "Use google or github.");
274
- }
275
- provider = value;
276
- i = nextIndex;
277
- continue;
278
- }
279
- throw new FloomError(`Unknown flag or argument: ${a}`, `Try \`${CLI_COMMAND} login --provider github\`.`);
280
- }
281
- return { provider };
282
- }
283
149
  function parseFlags(argv) {
284
- const out = { visibility: "unlisted", update: false, rest: [] };
150
+ const out = { update: false, rest: [] };
285
151
  let visibilityFlag = null;
286
152
  for (let i = 0; i < argv.length; i++) {
287
153
  const a = argv[i] ?? "";
@@ -295,15 +161,15 @@ function parseFlags(argv) {
295
161
  }
296
162
  else if (a === "--update" || a.startsWith("--update=")) {
297
163
  out.update = true;
298
- if (a.startsWith("--update=")) {
299
- out.updateSlug = a.slice("--update=".length);
164
+ const inline = a.startsWith("--update=") ? a.slice("--update=".length) : undefined;
165
+ const next = argv[i + 1];
166
+ const value = inline ?? (next && !next.startsWith("--") ? next : undefined);
167
+ if (value) {
168
+ out.updateSlug = value;
169
+ if (!inline)
170
+ i += 1;
300
171
  }
301
172
  else {
302
- const next = argv[i + 1];
303
- if (out.rest.length > 0 && next && !next.startsWith("--")) {
304
- out.updateSlug = next;
305
- i += 1;
306
- }
307
173
  }
308
174
  }
309
175
  else if (a === "--share" || a.startsWith("--share=")) {
@@ -405,6 +271,11 @@ function parseInitArgs(argv) {
405
271
  }
406
272
  return { ...(file ? { file } : {}), ...(template ? { template } : {}) };
407
273
  }
274
+ function parseTargetFlag(value) {
275
+ if (isAgentTarget(value))
276
+ return value;
277
+ throw new FloomError("Invalid --target.", `Use ${TARGET_HINT}.`);
278
+ }
408
279
  function parseListFlags(argv) {
409
280
  const out = { json: false };
410
281
  for (const a of argv) {
@@ -425,10 +296,7 @@ function parseSyncFlags(argv) {
425
296
  const a = argv[i] ?? "";
426
297
  if (a === "--target" || a.startsWith("--target=")) {
427
298
  const { value, nextIndex } = readFlagValue(argv, i, "--target");
428
- if (value !== "claude" && value !== "codex") {
429
- throw new FloomError("Invalid --target.", "Use claude or codex.");
430
- }
431
- out.target = value;
299
+ out.target = parseTargetFlag(value);
432
300
  i = nextIndex;
433
301
  }
434
302
  else if (a.startsWith("--")) {
@@ -459,15 +327,11 @@ function parseAddArgs(argv) {
459
327
  let target;
460
328
  let setup = false;
461
329
  let force = false;
462
- let json = false;
463
330
  for (let i = 0; i < argv.length; i++) {
464
331
  const a = argv[i] ?? "";
465
332
  if (a === "--target" || a.startsWith("--target=")) {
466
333
  const { value, nextIndex } = readFlagValue(argv, i, "--target");
467
- if (value !== "claude" && value !== "codex") {
468
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
469
- }
470
- target = value;
334
+ target = parseTargetFlag(value);
471
335
  i = nextIndex;
472
336
  }
473
337
  else if (a === "--setup") {
@@ -476,9 +340,6 @@ function parseAddArgs(argv) {
476
340
  else if (a === "--force") {
477
341
  force = true;
478
342
  }
479
- else if (a === "--json") {
480
- json = true;
481
- }
482
343
  else if (a.startsWith("--")) {
483
344
  throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} add <url-or-slug> --setup\`.`);
484
345
  }
@@ -491,10 +352,7 @@ function parseAddArgs(argv) {
491
352
  if (!slug) {
492
353
  throw new FloomError("Missing skill slug.", `Try: \`${CLI_COMMAND} add <url-or-slug> --setup\``);
493
354
  }
494
- if (json && setup) {
495
- throw new FloomError("Cannot combine --json with --setup.", "Run the install first, then run `npx -y @floomhq/floom setup --target claude --yes`.");
496
- }
497
- return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
355
+ return target ? { slug, target, setup, force } : { slug, setup, force };
498
356
  }
499
357
  function parseSearchFlags(argv) {
500
358
  const out = { json: false };
@@ -558,10 +416,7 @@ function parseSetupFlags(argv) {
558
416
  out.yes = true;
559
417
  else if (a === "--target" || a.startsWith("--target=")) {
560
418
  const { value, nextIndex } = readFlagValue(argv, i, "--target");
561
- if (value !== "claude" && value !== "codex") {
562
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
563
- }
564
- out.target = value;
419
+ out.target = parseTargetFlag(value);
565
420
  i = nextIndex;
566
421
  }
567
422
  else if (a === "--file" || a.startsWith("--file=")) {
@@ -580,22 +435,16 @@ function parseSetupFlags(argv) {
580
435
  return out;
581
436
  }
582
437
  function parseDoctorFlags(argv) {
583
- const out = { json: false };
438
+ const out = {};
584
439
  for (let i = 0; i < argv.length; i++) {
585
440
  const a = argv[i] ?? "";
586
441
  if (a === "--target" || a.startsWith("--target=")) {
587
442
  const { value, nextIndex } = readFlagValue(argv, i, "--target");
588
- if (value !== "claude" && value !== "codex") {
589
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
590
- }
591
- out.target = value;
443
+ out.target = parseTargetFlag(value);
592
444
  i = nextIndex;
593
445
  }
594
- else if (a === "--json") {
595
- out.json = true;
596
- }
597
446
  else if (a.startsWith("--")) {
598
- throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex --json\`.`);
447
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex\`.`);
599
448
  }
600
449
  else {
601
450
  throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} doctor --target claude\`.`);
@@ -743,7 +592,7 @@ async function runLibrary(argv) {
743
592
  }
744
593
  }
745
594
  function parseWatchFlags(argv) {
746
- const out = { intervalSeconds: 60 };
595
+ const out = { intervalSeconds: 60, push: false, yolo: true, once: false, target: "claude" };
747
596
  for (let i = 0; i < argv.length; i++) {
748
597
  const a = argv[i] ?? "";
749
598
  if (a === "--interval" || a.startsWith("--interval=")) {
@@ -755,50 +604,31 @@ function parseWatchFlags(argv) {
755
604
  out.intervalSeconds = interval;
756
605
  i = nextIndex;
757
606
  }
758
- else if (a === "--target" || a.startsWith("--target=")) {
759
- const { value, nextIndex } = readFlagValue(argv, i, "--target");
760
- if (value !== "claude" && value !== "codex") {
761
- throw new FloomError("Invalid --target.", "Use claude or codex.");
762
- }
763
- out.target = value;
764
- i = nextIndex;
607
+ else if (a === "--push") {
608
+ out.push = true;
765
609
  }
766
- else if (a.startsWith("--")) {
767
- throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
610
+ else if (a === "--no-yolo") {
611
+ out.yolo = false;
768
612
  }
769
- else {
770
- throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
613
+ else if (a === "--once") {
614
+ out.once = true;
771
615
  }
772
- }
773
- return out;
774
- }
775
- function notAvailable(feature) {
776
- throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
777
- }
778
- function parseAgentPromptFlags(argv) {
779
- const out = {};
780
- for (let i = 0; i < argv.length; i++) {
781
- const a = argv[i] ?? "";
782
- if (a === "--target" || a.startsWith("--target=")) {
616
+ else if (a === "--target" || a.startsWith("--target=")) {
783
617
  const { value, nextIndex } = readFlagValue(argv, i, "--target");
784
- if (value !== "claude" && value !== "codex") {
785
- throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
786
- }
787
- out.target = value;
618
+ out.target = parseTargetFlag(value);
788
619
  i = nextIndex;
789
620
  }
790
621
  else if (a.startsWith("--")) {
791
- throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target codex\`.`);
622
+ throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --push --target claude\`.`);
792
623
  }
793
624
  else {
794
- throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target claude\`.`);
625
+ throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --push --target claude\`.`);
795
626
  }
796
627
  }
797
628
  return out;
798
629
  }
799
- function printAgentPrompt(target = "claude") {
800
- const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
801
- process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
630
+ function notAvailable(feature) {
631
+ throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
802
632
  }
803
633
  function parseSingleFileArg(argv, usageHint) {
804
634
  let file;
@@ -824,7 +654,7 @@ function sleep(ms, signal) {
824
654
  }, { once: true });
825
655
  });
826
656
  }
827
- async function watch(intervalSeconds, target) {
657
+ async function watch(intervalSeconds) {
828
658
  const cfg = await readConfig();
829
659
  if (!cfg) {
830
660
  throw new FloomError("Not signed in.", `Run \`${CLI_COMMAND} login\` before \`${CLI_COMMAND} watch\`, or use \`${CLI_COMMAND} add <link>\` without an account.`);
@@ -841,9 +671,9 @@ async function watch(intervalSeconds, target) {
841
671
  };
842
672
  process.on("SIGINT", stop);
843
673
  process.on("SIGTERM", stop);
844
- process.stdout.write(`${symbols.bullet} Watching Floom sync for ${target ?? "claude"} every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
674
+ process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
845
675
  while (!controller.signal.aborted) {
846
- await sync({ spinner: false, quietUnchanged: true, ...(target ? { target } : {}) });
676
+ await sync({ spinner: false, quietUnchanged: true });
847
677
  await sleep(intervalSeconds * 1000, controller.signal);
848
678
  }
849
679
  }
@@ -863,9 +693,10 @@ async function main() {
863
693
  // never block on update-notifier
864
694
  }
865
695
  }
696
+ // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
697
+ // Subcommands are simple enough that one help screen is fine for Version 1.
866
698
  if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
867
- if (!subcommandUsage(cmd))
868
- commandUsage();
699
+ usage();
869
700
  return;
870
701
  }
871
702
  try {
@@ -887,7 +718,8 @@ async function main() {
887
718
  process.stdout.write(`${CLI_VERSION}\n`);
888
719
  return;
889
720
  case "login":
890
- await login(parseLoginArgs(rest).provider);
721
+ rejectArgs(rest, `Try \`${CLI_COMMAND} login\`.`);
722
+ await login();
891
723
  return;
892
724
  case "logout":
893
725
  rejectArgs(rest, `Try \`${CLI_COMMAND} logout\`.`);
@@ -914,8 +746,8 @@ async function main() {
914
746
  }
915
747
  await publish({
916
748
  file,
917
- visibility: flags.visibility,
918
749
  update: flags.update,
750
+ ...(flags.visibility ? { visibility: flags.visibility } : {}),
919
751
  ...(flags.updateSlug ? { updateSlug: flags.updateSlug } : {}),
920
752
  ...(flags.assetType ? { assetType: flags.assetType } : {}),
921
753
  ...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
@@ -970,20 +802,6 @@ async function main() {
970
802
  ...(flags.target ? { target: flags.target } : {}),
971
803
  setup: flags.setup,
972
804
  force: flags.force,
973
- json: flags.json,
974
- });
975
- if (flags.setup) {
976
- await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
977
- }
978
- return;
979
- }
980
- case "update": {
981
- const flags = parseAddArgs(rest);
982
- await install(flags.slug, {
983
- ...(flags.target ? { target: flags.target } : {}),
984
- setup: flags.setup,
985
- force: true,
986
- json: flags.json,
987
805
  });
988
806
  if (flags.setup) {
989
807
  await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
@@ -1001,7 +819,15 @@ async function main() {
1001
819
  }
1002
820
  case "watch": {
1003
821
  const flags = parseWatchFlags(rest);
1004
- await watch(flags.intervalSeconds, flags.target);
822
+ if (flags.push) {
823
+ await watchPush(flags.intervalSeconds, {
824
+ target: flags.target,
825
+ yolo: flags.yolo,
826
+ once: flags.once,
827
+ });
828
+ return;
829
+ }
830
+ await watch(flags.intervalSeconds);
1005
831
  return;
1006
832
  }
1007
833
  case "delete":
@@ -1033,13 +859,6 @@ async function main() {
1033
859
  rejectArgs(rest, `Try \`${CLI_COMMAND} mcp\`.`);
1034
860
  printMcpSetup();
1035
861
  return;
1036
- case "agent-prompt":
1037
- case "paste":
1038
- {
1039
- const flags = parseAgentPromptFlags(rest);
1040
- printAgentPrompt(flags.target);
1041
- }
1042
- return;
1043
862
  case "doctor":
1044
863
  await doctor(parseDoctorFlags(rest));
1045
864
  return;
package/dist/config.js CHANGED
@@ -5,7 +5,6 @@ export const CONFIG_DIR = join(homedir(), ".floom");
5
5
  export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
6
  export const DEFAULT_API_URL = "https://floom.dev";
7
7
  export const DEFAULT_WEB_URL = "https://floom.dev";
8
- const REFRESH_SKEW_SECONDS = 5 * 60;
9
8
  export function getApiUrl() {
10
9
  return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
11
10
  }
@@ -15,7 +14,7 @@ export function resolveApiUrl(cfg) {
15
14
  export function getWebUrl() {
16
15
  return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
17
16
  }
18
- export async function readRawConfig() {
17
+ export async function readConfig() {
19
18
  try {
20
19
  const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
21
20
  const parsed = JSON.parse(buf);
@@ -29,15 +28,6 @@ export async function readRawConfig() {
29
28
  throw e;
30
29
  }
31
30
  }
32
- export async function readConfig() {
33
- const cfg = await readRawConfig();
34
- if (!cfg)
35
- return null;
36
- if (!needsRefresh(cfg))
37
- return cfg;
38
- const refreshed = await refreshConfig(cfg);
39
- return refreshed;
40
- }
41
31
  export async function writeConfig(cfg) {
42
32
  const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
43
33
  const targetDir = dirname(targetPath);
@@ -55,43 +45,3 @@ export async function deleteConfig() {
55
45
  throw e;
56
46
  }
57
47
  }
58
- export function needsRefresh(cfg) {
59
- return !Number.isFinite(cfg.expiresAt) || cfg.expiresAt <= Math.floor(Date.now() / 1000) + REFRESH_SKEW_SECONDS;
60
- }
61
- export async function refreshConfig(cfg) {
62
- const apiUrl = resolveApiUrl(cfg);
63
- let res;
64
- try {
65
- res = await fetch(`${apiUrl}/api/v1/auth/refresh`, {
66
- method: "POST",
67
- headers: { "content-type": "application/json" },
68
- body: JSON.stringify({ refresh_token: cfg.refreshToken }),
69
- });
70
- }
71
- catch {
72
- return null;
73
- }
74
- if (!res.ok)
75
- return null;
76
- let data;
77
- try {
78
- data = (await res.json());
79
- }
80
- catch {
81
- return null;
82
- }
83
- if (!data.access_token || !data.refresh_token)
84
- return null;
85
- const expiresAt = Number.isFinite(data.expires_at)
86
- ? Number(data.expires_at)
87
- : Math.floor(Date.now() / 1000) + (Number.isFinite(data.expires_in) ? Number(data.expires_in) : 3600);
88
- const refreshed = {
89
- apiUrl,
90
- accessToken: data.access_token,
91
- refreshToken: data.refresh_token,
92
- expiresAt,
93
- email: data.email ?? cfg.email ?? null,
94
- };
95
- await writeConfig(refreshed);
96
- return refreshed;
97
- }