@floomhq/floom 1.0.26 → 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 +6 -5
- package/dist/cli.js +61 -242
- package/dist/config.js +1 -51
- package/dist/doctor.js +11 -39
- package/dist/errors.js +1 -1
- package/dist/install.js +19 -74
- package/dist/login.js +8 -67
- package/dist/mcp.js +5 -2
- package/dist/package.js +177 -81
- package/dist/publish.js +41 -51
- package/dist/push-watch.js +245 -0
- package/dist/setup.js +87 -25
- package/dist/sync-manifest.js +44 -37
- package/dist/sync.js +15 -26
- package/dist/targets.js +45 -12
- package/package.json +1 -1
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.
|
|
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
|
|
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`
|
|
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(
|
|
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
|
|
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(
|
|
101
|
+
${c.dim(`Flags: --target ${TARGET_HINT}, --yes, --dry-run`)}
|
|
102
102
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
103
|
-
${c.dim(
|
|
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")}
|
|
111
|
-
${c.dim(
|
|
112
|
-
${c.cyan("watch")}
|
|
113
|
-
${c.dim(
|
|
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 = {
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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 === "--
|
|
759
|
-
|
|
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
|
|
767
|
-
|
|
610
|
+
else if (a === "--no-yolo") {
|
|
611
|
+
out.yolo = false;
|
|
768
612
|
}
|
|
769
|
-
else {
|
|
770
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
|
800
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|