@floomhq/floom 1.0.19 → 1.0.21

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
@@ -22,7 +22,7 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
22
22
 
23
23
  ## Commands
24
24
 
25
- - `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
25
+ - `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`.
26
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.
27
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>`.
28
28
  - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
package/dist/cli.js CHANGED
@@ -129,6 +129,119 @@ function commandUsage() {
129
129
  `;
130
130
  process.stdout.write(out);
131
131
  }
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
+ `,
183
+ init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
184
+
185
+ Create a starter skill folder with SKILL.md.
186
+
187
+ ${c.bold("Flags")}
188
+ ${c.cyan("--template")} ${c.dim(`generic|${INIT_TEMPLATES.filter((t) => t !== "generic").join("|")}`)}
189
+ `,
190
+ sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
191
+
192
+ Preview pull of published, saved, and subscribed library skills.
193
+
194
+ ${c.bold("Flags")}
195
+ ${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
196
+ `,
197
+ watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
198
+
199
+ Poll sync on an interval. Preview behavior.
200
+
201
+ ${c.bold("Flags")}
202
+ ${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
203
+ ${c.cyan("--interval")} ${c.dim("<seconds>")} Poll interval. Minimum: 10.
204
+ `,
205
+ setup: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} setup`)} ${c.dim("[flags]")}
206
+
207
+ Add Floom guidance to CLAUDE.md or AGENTS.md.
208
+
209
+ ${c.bold("Flags")}
210
+ ${c.cyan("--target")} ${c.dim("claude|codex")} Configure Claude Code or Codex.
211
+ ${c.cyan("--file")} ${c.dim("<path>")} Write a specific instructions file.
212
+ ${c.cyan("--yes")} Write without prompting.
213
+ ${c.cyan("--dry-run")} Preview the change only.
214
+ `,
215
+ search: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} search`)} ${c.dim("<query> [flags]")}
216
+
217
+ Find public skills and libraries.
218
+
219
+ ${c.bold("Flags")}
220
+ ${c.cyan("--library")} ${c.dim("<slug>")} Limit search to one library.
221
+ ${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
222
+ ${c.cyan("--json")} Print machine-readable results.
223
+ `,
224
+ info: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} info`)} ${c.dim("<url-or-slug> [flags]")}
225
+
226
+ Show public skill metadata.
227
+
228
+ ${c.bold("Flags")}
229
+ ${c.cyan("--json")} Print machine-readable metadata.
230
+ `,
231
+ "agent-prompt": `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} agent-prompt`)} ${c.dim("[flags]")}
232
+
233
+ Print the one-line instruction to paste into your agent.
234
+
235
+ ${c.bold("Flags")}
236
+ ${c.cyan("--target")} ${c.dim("claude|codex")} Print the right local skills path. Default: claude.
237
+ `,
238
+ };
239
+ const body = key ? usageByCommand[key] : undefined;
240
+ if (!body)
241
+ return false;
242
+ process.stdout.write(`${body}\n`);
243
+ return true;
244
+ }
132
245
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
133
246
  const INSTALL_TARGETS = new Set([
134
247
  "claude_skill",
@@ -149,6 +262,23 @@ function readFlagValue(argv, index, flag) {
149
262
  throw new FloomError(`Missing value for ${flag}.`);
150
263
  return { value, nextIndex: index + 1 };
151
264
  }
265
+ function parseLoginArgs(argv) {
266
+ let provider = "google";
267
+ for (let i = 0; i < argv.length; i += 1) {
268
+ const a = argv[i] ?? "";
269
+ if (a === "--provider" || a.startsWith("--provider=")) {
270
+ const { value, nextIndex } = readFlagValue(argv, i, "--provider");
271
+ if (value !== "google" && value !== "github") {
272
+ throw new FloomError("Invalid --provider.", "Use google or github.");
273
+ }
274
+ provider = value;
275
+ i = nextIndex;
276
+ continue;
277
+ }
278
+ throw new FloomError(`Unknown flag or argument: ${a}`, `Try \`${CLI_COMMAND} login --provider github\`.`);
279
+ }
280
+ return { provider };
281
+ }
152
282
  function parseFlags(argv) {
153
283
  const out = { visibility: "unlisted", update: false, rest: [] };
154
284
  let visibilityFlag = null;
@@ -722,10 +852,9 @@ async function main() {
722
852
  // never block on update-notifier
723
853
  }
724
854
  }
725
- // Subcommand --help: any rest arg = --help/-h/help → show the command reference.
726
- // Subcommands are simple enough that one reference screen is fine for Version 1.
727
855
  if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
728
- commandUsage();
856
+ if (!subcommandUsage(cmd))
857
+ commandUsage();
729
858
  return;
730
859
  }
731
860
  try {
@@ -747,8 +876,7 @@ async function main() {
747
876
  process.stdout.write(`${CLI_VERSION}\n`);
748
877
  return;
749
878
  case "login":
750
- rejectArgs(rest, `Try \`${CLI_COMMAND} login\`.`);
751
- await login();
879
+ await login(parseLoginArgs(rest).provider);
752
880
  return;
753
881
  case "logout":
754
882
  rejectArgs(rest, `Try \`${CLI_COMMAND} logout\`.`);
package/dist/login.js CHANGED
@@ -8,18 +8,19 @@ import { c, header, symbols } from "./ui.js";
8
8
  import { FloomError, friendlyHttp, friendlyNetwork } from "./errors.js";
9
9
  const DEFAULT_PORT = 7456;
10
10
  const TIMEOUT_MS = 5 * 60 * 1000;
11
- export async function login() {
11
+ export async function login(provider = "google") {
12
12
  const apiUrl = getApiUrl();
13
13
  const port = await pickPort();
14
+ const providerLabel = provider === "github" ? "GitHub" : "Google";
14
15
  process.stdout.write(header());
15
- process.stdout.write(`${symbols.arrow} Opening browser to sign in with Google...\n\n`);
16
+ process.stdout.write(`${symbols.arrow} Opening browser to sign in with ${providerLabel}...\n\n`);
16
17
  const spinner = ora({
17
18
  text: c.dim("Waiting for sign-in to complete..."),
18
19
  color: "yellow",
19
20
  }).start();
20
21
  let tokens;
21
22
  try {
22
- tokens = await waitForCallback(port);
23
+ tokens = await waitForCallback(port, provider);
23
24
  }
24
25
  catch (err) {
25
26
  spinner.stop();
@@ -90,7 +91,7 @@ function reserveEphemeralPort() {
90
91
  });
91
92
  });
92
93
  }
93
- function waitForCallback(port) {
94
+ function waitForCallback(port, provider) {
94
95
  return new Promise((resolve, reject) => {
95
96
  const apiUrl = getApiUrl();
96
97
  const state = randomBytes(32).toString("base64url");
@@ -172,7 +173,7 @@ function waitForCallback(port) {
172
173
  reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
173
174
  });
174
175
  server.listen(port, "127.0.0.1", () => {
175
- const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
176
+ const target = `${apiUrl}/auth/cli?port=${port}&provider=${provider}&state=${encodeURIComponent(state)}`;
176
177
  open(target).catch((e) => {
177
178
  const msg = e instanceof Error ? e.message : String(e);
178
179
  process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
package/dist/sync.js CHANGED
@@ -79,6 +79,17 @@ function skillPath(skill, targetAgent) {
79
79
  function syncKey(skill) {
80
80
  return `${skill.library_slug ?? ""}\0${skill.folder ?? ""}\0${skill.slug}`;
81
81
  }
82
+ function hasStructuredPath(skill) {
83
+ return Boolean(skill.library_slug || skill.folder);
84
+ }
85
+ function dedupeSyncSkills(skills) {
86
+ const structuredSlugs = new Set();
87
+ for (const skill of skills) {
88
+ if (hasStructuredPath(skill))
89
+ structuredSlugs.add(skill.slug);
90
+ }
91
+ return skills.filter((skill) => !structuredSlugs.has(skill.slug) || hasStructuredPath(skill));
92
+ }
82
93
  function validateSyncSkillShape(skill) {
83
94
  if (!skill || typeof skill !== "object")
84
95
  throw new FloomError("Invalid sync response.");
@@ -288,7 +299,7 @@ export async function sync(opts = {}) {
288
299
  for (const skill of payload.skills)
289
300
  validateSyncSkillShape(skill);
290
301
  // Version 1 preview syncs published, saved, and subscribed library skills.
291
- const all = payload.skills;
302
+ const all = dedupeSyncSkills(payload.skills);
292
303
  const seen = new Set();
293
304
  let unchanged = 0;
294
305
  let updated = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",