@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 +1 -1
- package/dist/cli.js +133 -5
- package/dist/login.js +6 -5
- package/dist/sync.js +12 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|