@floomhq/floom 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Floom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @floomhq/floom
2
+
3
+ Publish AI skills from your terminal. Share them with a link. Add other people's skills with one command.
4
+
5
+ ```bash
6
+ npm install -g @floomhq/floom
7
+ floom login
8
+ floom publish my-skill.md
9
+ floom add awesome-skill
10
+ floom list
11
+ ```
12
+
13
+ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the URL can read the raw Markdown — drop it into any AI tool that accepts skills.
14
+
15
+ ## Commands
16
+
17
+ - `floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
18
+ - `floom init [file.md]` — create a starter skill file.
19
+ - `floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--version <label>`.
20
+ - `floom list` — show your published skills. Optional `--json`.
21
+ - `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
22
+ - `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
23
+ - `floom sync` — preview: pull your own Floom-published skills into `~/.claude/skills/`.
24
+ - `floom watch` — preview: run `floom sync` repeatedly. Optional `--interval <seconds>`; minimum `10`.
25
+ - `floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
26
+ - `floom doctor` — diagnose your Floom setup.
27
+ - `floom whoami` — show the signed-in account.
28
+ - `floom logout` — delete local credentials.
29
+
30
+ ## Skill format
31
+
32
+ Optional YAML-ish frontmatter (`title`, `description`, `version`), then freeform Markdown.
33
+
34
+ ```markdown
35
+ ---
36
+ title: Write a LinkedIn post
37
+ description: Yurii-level value density
38
+ version: 0.1.0
39
+ ---
40
+
41
+ # Instructions
42
+ - ...
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
48
+
49
+ `floom sync` and `floom watch` are Version 1 preview commands for your own published skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
50
+ The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
51
+ only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
52
+ conflicts. Symlinks are never followed. To accept the Floom version, move or delete the local file
53
+ and run `floom sync` again.
package/bin/floom.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.js";
package/dist/cli.js ADDED
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ import updateNotifier from "update-notifier";
3
+ import { login } from "./login.js";
4
+ import { publish } from "./publish.js";
5
+ import { whoami } from "./whoami.js";
6
+ import { init } from "./init.js";
7
+ import { deleteConfig } from "./config.js";
8
+ import { list } from "./list.js";
9
+ import { install } from "./install.js";
10
+ import { info } from "./info.js";
11
+ import { deleteSkill } from "./delete.js";
12
+ import { doctor } from "./doctor.js";
13
+ import { sync } from "./sync.js";
14
+ import { c, symbols } from "./ui.js";
15
+ import { printError, FloomError } from "./errors.js";
16
+ const VERSION = "1.0.0";
17
+ const PKG = { name: "@floomhq/floom", version: VERSION };
18
+ const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
19
+ function usage() {
20
+ const out = `
21
+ ${c.coral(" ____ __")}
22
+ ${c.coral(" / __ \\___ / /___ ___ __")}
23
+ ${c.coral(" / /_/ / _ \\/ / __ `/ / / /")}
24
+ ${c.coral(" / _, _/ __/ / /_/ / /_/ /")}
25
+ ${c.coral(" /_/ |_|\\___/_/\\__,_/\\__, /")} ${c.dim(`v${VERSION}`)}
26
+ ${c.coral(" /____/")}
27
+
28
+ ${c.bold("Share AI agent skills with a link.")}
29
+ ${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
30
+
31
+ ${c.bold("Usage")}
32
+ ${c.cyan("floom")} ${c.dim("<command> [options]")}
33
+
34
+ ${c.bold("Start Here")}
35
+ ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
36
+ ${c.dim("Install a public skill. No account needed.")}
37
+
38
+ ${c.cyan("floom publish")} ${c.dim("./support-tone.md --type instruction --public")}
39
+ ${c.dim("Publish Markdown and print a share link.")}
40
+
41
+ ${c.bold("Commands")}
42
+ ${c.dim("Receive")}
43
+ ${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
44
+ ${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
45
+
46
+ ${c.dim("Create")}
47
+ ${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
48
+ ${c.cyan("publish")} ${c.dim("<file.md>")} Upload Markdown and print a URL
49
+
50
+ ${c.dim("Publish Flags")}
51
+ ${c.dim("--public | --private | --unlisted")} ${c.dim("Set link visibility")}
52
+ ${c.dim("--type <kind>")} ${c.dim("knowledge | instruction | workflow | skill")}
53
+ ${c.dim("--installs-as <target>")} ${c.dim("Set install target metadata")}
54
+ ${c.dim("claude_skill | memory | rule | codex_instruction")}
55
+ ${c.dim("--version <label>")} ${c.dim("Attach a human version label")}
56
+
57
+ ${c.dim("Account")}
58
+ ${c.cyan("login")} Sign in with Google
59
+ ${c.cyan("list")} List your published skills ${c.dim("[--json]")}
60
+ ${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
61
+ ${c.cyan("whoami")} Show the signed-in account
62
+ ${c.cyan("logout")} Delete local credentials
63
+
64
+ ${c.dim("System")}
65
+ ${c.cyan("sync")} Preview: pull your own published skills
66
+ ${c.cyan("watch")} Preview: poll your own published skills ${c.dim("[--interval <seconds>, min 10]")}
67
+ ${c.cyan("doctor")} Diagnose auth, API, and local setup
68
+ ${c.cyan("--help")} Show this help
69
+ ${c.cyan("--version")} Show version
70
+
71
+ ${c.bold("Env")}
72
+ ${c.cyan("FLOOM_API_URL")} Override the API host
73
+
74
+ ${c.bold("Links")}
75
+ ${c.dim("Docs")} https://floom.dev
76
+ ${c.dim("Source")} https://github.com/floomhq/floom
77
+ `;
78
+ process.stdout.write(out);
79
+ }
80
+ const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
81
+ const INSTALL_TARGETS = new Set([
82
+ "claude_skill",
83
+ "memory",
84
+ "rule",
85
+ "codex_instruction",
86
+ "opencode_instruction",
87
+ "cursor_rule",
88
+ "other",
89
+ ]);
90
+ const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
91
+ function readFlagValue(argv, index, flag) {
92
+ const current = argv[index] ?? "";
93
+ if (current.startsWith(`${flag}=`))
94
+ return { value: current.slice(flag.length + 1), nextIndex: index };
95
+ const value = argv[index + 1];
96
+ if (!value || value.startsWith("--"))
97
+ throw new FloomError(`Missing value for ${flag}.`);
98
+ return { value, nextIndex: index + 1 };
99
+ }
100
+ function parseFlags(argv) {
101
+ const out = { visibility: "unlisted", update: false, rest: [] };
102
+ for (let i = 0; i < argv.length; i++) {
103
+ const a = argv[i] ?? "";
104
+ if (a === "--public")
105
+ out.visibility = "public";
106
+ else if (a === "--private")
107
+ out.visibility = "private";
108
+ else if (a === "--unlisted")
109
+ out.visibility = "unlisted";
110
+ else if (a === "--update") {
111
+ throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
112
+ }
113
+ else if (a === "--share" || a.startsWith("--share=")) {
114
+ throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --share` is planned for a later Floom release.");
115
+ }
116
+ else if (a === "--type" || a.startsWith("--type=")) {
117
+ const { value, nextIndex } = readFlagValue(argv, i, "--type");
118
+ if (!ASSET_TYPES.has(value)) {
119
+ throw new FloomError(`Invalid --type: ${value}`, "Use one of: knowledge, instruction, workflow, skill.");
120
+ }
121
+ out.assetType = value;
122
+ i = nextIndex;
123
+ }
124
+ else if (a === "--installs-as" || a.startsWith("--installs-as=")) {
125
+ const { value, nextIndex } = readFlagValue(argv, i, "--installs-as");
126
+ if (!INSTALL_TARGETS.has(value)) {
127
+ throw new FloomError(`Invalid --installs-as: ${value}`, "Use one of: claude_skill, memory, rule, codex_instruction, opencode_instruction, cursor_rule, other.");
128
+ }
129
+ out.installsAs = value;
130
+ i = nextIndex;
131
+ }
132
+ else if (a === "--version" || a.startsWith("--version=")) {
133
+ const { value, nextIndex } = readFlagValue(argv, i, "--version");
134
+ if (!VERSION_RE.test(value)) {
135
+ throw new FloomError(`Invalid --version: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
136
+ }
137
+ out.version = value;
138
+ i = nextIndex;
139
+ }
140
+ else
141
+ out.rest.push(a);
142
+ }
143
+ return out;
144
+ }
145
+ function parseListFlags(argv) {
146
+ const out = { json: false };
147
+ for (const a of argv) {
148
+ if (a === "--json")
149
+ out.json = true;
150
+ else if (a.startsWith("--")) {
151
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ function parseInfoFlags(argv) {
157
+ const out = { json: false };
158
+ for (const a of argv) {
159
+ if (a === "--json")
160
+ out.json = true;
161
+ else if (a.startsWith("--"))
162
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
163
+ else if (!out.slug)
164
+ out.slug = a;
165
+ }
166
+ return out;
167
+ }
168
+ function parseDeleteFlags(argv) {
169
+ const out = { yes: false };
170
+ for (const a of argv) {
171
+ if (a === "--yes" || a === "-y")
172
+ out.yes = true;
173
+ else if (a.startsWith("--"))
174
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
175
+ else if (!out.slug)
176
+ out.slug = a;
177
+ }
178
+ return out;
179
+ }
180
+ function parseWatchFlags(argv) {
181
+ const out = { intervalSeconds: 60 };
182
+ for (let i = 0; i < argv.length; i++) {
183
+ const a = argv[i] ?? "";
184
+ if (a === "--interval" || a.startsWith("--interval=")) {
185
+ const { value, nextIndex } = readFlagValue(argv, i, "--interval");
186
+ const interval = Number(value);
187
+ if (!Number.isInteger(interval) || interval < 10) {
188
+ throw new FloomError("Invalid --interval.", "Use an integer number of seconds, minimum 10.");
189
+ }
190
+ out.intervalSeconds = interval;
191
+ i = nextIndex;
192
+ }
193
+ else if (a.startsWith("--")) {
194
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
195
+ }
196
+ else {
197
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+ function notAvailable(feature) {
203
+ throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
204
+ }
205
+ function sleep(ms, signal) {
206
+ if (signal.aborted)
207
+ return Promise.resolve();
208
+ return new Promise((resolve) => {
209
+ const timer = setTimeout(resolve, ms);
210
+ signal.addEventListener("abort", () => {
211
+ clearTimeout(timer);
212
+ resolve();
213
+ }, { once: true });
214
+ });
215
+ }
216
+ async function watch(intervalSeconds) {
217
+ const controller = new AbortController();
218
+ let stopping = false;
219
+ const stop = () => {
220
+ if (stopping)
221
+ return;
222
+ stopping = true;
223
+ controller.abort();
224
+ process.stdout.write(`\n${symbols.bullet} Stopping floom watch\n`);
225
+ process.exit(0);
226
+ };
227
+ process.on("SIGINT", stop);
228
+ process.on("SIGTERM", stop);
229
+ process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
230
+ while (!controller.signal.aborted) {
231
+ await sync({ spinner: false, quietUnchanged: true });
232
+ await sleep(intervalSeconds * 1000, controller.signal);
233
+ }
234
+ }
235
+ async function main() {
236
+ const [, , cmd, ...rest] = process.argv;
237
+ // Update notifier — runs in background, prints at process exit if a newer
238
+ // version is available. Disabled in CI / non-TTY by default.
239
+ if (cmd !== "watch") {
240
+ try {
241
+ updateNotifier({ pkg: PKG, updateCheckInterval: 1000 * 60 * 60 * 24 }).notify({
242
+ defer: true,
243
+ isGlobal: true,
244
+ message: `${symbols.bullet} ${c.bold("floom")} v{latestVersion} available — run \`npm i -g {packageName}\` to update.`,
245
+ });
246
+ }
247
+ catch {
248
+ // never block on update-notifier
249
+ }
250
+ }
251
+ // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
252
+ // Subcommands are simple enough that one help screen is fine for Version 1.
253
+ if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
254
+ usage();
255
+ return;
256
+ }
257
+ try {
258
+ switch (cmd) {
259
+ case undefined:
260
+ case "--help":
261
+ case "-h":
262
+ case "help":
263
+ usage();
264
+ return;
265
+ case "--version":
266
+ case "-v":
267
+ process.stdout.write(`${VERSION}\n`);
268
+ return;
269
+ case "login":
270
+ await login();
271
+ return;
272
+ case "logout":
273
+ await deleteConfig();
274
+ process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
275
+ return;
276
+ case "whoami":
277
+ await whoami();
278
+ return;
279
+ case "init": {
280
+ const file = rest[0];
281
+ await init(file);
282
+ return;
283
+ }
284
+ case "publish": {
285
+ const flags = parseFlags(rest);
286
+ const file = flags.rest[0];
287
+ if (!file) {
288
+ throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
289
+ }
290
+ await publish({
291
+ file,
292
+ visibility: flags.visibility,
293
+ update: flags.update,
294
+ ...(flags.assetType ? { assetType: flags.assetType } : {}),
295
+ ...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
296
+ ...(flags.version ? { version: flags.version } : {}),
297
+ });
298
+ return;
299
+ }
300
+ case "share":
301
+ notAvailable("`floom share`");
302
+ case "list": {
303
+ const flags = parseListFlags(rest);
304
+ await list(flags);
305
+ return;
306
+ }
307
+ case "info":
308
+ {
309
+ const flags = parseInfoFlags(rest);
310
+ await info({ slug: flags.slug ?? "", json: flags.json });
311
+ }
312
+ return;
313
+ case "add":
314
+ case "install": {
315
+ const slug = rest.find((a) => !a.startsWith("--"));
316
+ if (!slug) {
317
+ throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
318
+ }
319
+ await install(slug);
320
+ return;
321
+ }
322
+ case "sync":
323
+ await sync();
324
+ return;
325
+ case "watch": {
326
+ const flags = parseWatchFlags(rest);
327
+ await watch(flags.intervalSeconds);
328
+ return;
329
+ }
330
+ case "delete":
331
+ case "rm": {
332
+ const flags = parseDeleteFlags(rest);
333
+ await deleteSkill({ slug: flags.slug ?? "", yes: flags.yes });
334
+ return;
335
+ }
336
+ case "library":
337
+ case "lib":
338
+ notAvailable("`floom library`");
339
+ case "move":
340
+ notAvailable("`floom move`");
341
+ case "mcp":
342
+ notAvailable("`floom mcp setup`");
343
+ case "doctor":
344
+ await doctor();
345
+ return;
346
+ default:
347
+ throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
348
+ }
349
+ }
350
+ catch (e) {
351
+ printError(e);
352
+ process.exit(1);
353
+ }
354
+ }
355
+ void main();
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname, join } from "node:path";
3
+ import { mkdir, readFile, writeFile, chmod, unlink } from "node:fs/promises";
4
+ export const CONFIG_DIR = join(homedir(), ".floom");
5
+ export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
+ export const DEFAULT_API_URL = "https://floom.dev";
7
+ export const DEFAULT_WEB_URL = "https://floom.dev";
8
+ export function getApiUrl() {
9
+ return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
10
+ }
11
+ export function getWebUrl() {
12
+ return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
13
+ }
14
+ export async function readConfig() {
15
+ try {
16
+ const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
17
+ const parsed = JSON.parse(buf);
18
+ if (!parsed.accessToken || !parsed.refreshToken)
19
+ return null;
20
+ return parsed;
21
+ }
22
+ catch (e) {
23
+ if (e.code === "ENOENT")
24
+ return null;
25
+ throw e;
26
+ }
27
+ }
28
+ export async function writeConfig(cfg) {
29
+ const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
30
+ const targetDir = dirname(targetPath);
31
+ await mkdir(targetDir, { recursive: true, mode: 0o700 });
32
+ await writeFile(targetPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
33
+ // belt-and-suspenders: set perms in case the file already existed
34
+ await chmod(targetPath, 0o600);
35
+ }
36
+ export async function deleteConfig() {
37
+ try {
38
+ await unlink(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH);
39
+ }
40
+ catch (e) {
41
+ if (e.code !== "ENOENT")
42
+ throw e;
43
+ }
44
+ }
package/dist/delete.js ADDED
@@ -0,0 +1,55 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import ora from "ora";
4
+ import { getApiUrl, readConfig } from "./config.js";
5
+ import { deleteRequest } from "./lib/api.js";
6
+ import { c, symbols } from "./ui.js";
7
+ import { FloomError } from "./errors.js";
8
+ function slugFromInput(s) {
9
+ const trimmed = s.trim();
10
+ try {
11
+ const url = new URL(trimmed);
12
+ return (url.pathname.split("/").filter(Boolean).at(-1) ?? "").replace(/\.(md|json)$/i, "");
13
+ }
14
+ catch {
15
+ return trimmed.replace(/\.(md|json)$/i, "");
16
+ }
17
+ }
18
+ async function confirm(question) {
19
+ if (!process.stdin.isTTY) {
20
+ throw new FloomError("Refusing to delete without confirmation in non-interactive mode.", "Pass `--yes` to skip the prompt.");
21
+ }
22
+ const rl = createInterface({ input, output });
23
+ try {
24
+ const answer = (await rl.question(` ${question} (y/N) `)).trim().toLowerCase();
25
+ return answer === "y" || answer === "yes";
26
+ }
27
+ finally {
28
+ rl.close();
29
+ }
30
+ }
31
+ export async function deleteSkill(opts) {
32
+ const slug = slugFromInput(opts.slug);
33
+ if (!slug)
34
+ throw new FloomError("Missing skill slug.", "Try: `floom delete <slug>`");
35
+ const cfg = await readConfig();
36
+ if (!cfg)
37
+ throw new FloomError("Not signed in.", "Run `floom login` first.");
38
+ if (!opts.yes) {
39
+ process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
40
+ const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
41
+ if (!ok) {
42
+ process.stdout.write(`\n${c.dim("Cancelled.")}\n\n`);
43
+ return;
44
+ }
45
+ }
46
+ const apiUrl = cfg.apiUrl ?? getApiUrl();
47
+ const spinner = ora({ text: c.dim(`Deleting ${slug}...`), color: "yellow" }).start();
48
+ try {
49
+ await deleteRequest(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "delete skill", cfg.accessToken);
50
+ }
51
+ finally {
52
+ spinner.stop();
53
+ }
54
+ process.stdout.write(`\n${symbols.ok} Deleted ${c.bold(slug)}\n\n`);
55
+ }