@floomhq/floom 1.0.63 → 2.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/dist/lib/api.js DELETED
@@ -1,142 +0,0 @@
1
- import { friendlyHttp, friendlyNetwork, FloomError } from "../errors.js";
2
- /**
3
- * Shared HTTP helpers for CLI commands. Mirrors the mcp-sync/lib/api.ts
4
- * patterns but throws FloomError instances so the CLI's printError gives
5
- * users a clean message instead of a stack trace.
6
- */
7
- export const DEFAULT_TIMEOUT_MS = 60_000;
8
- export async function floomFetch(url, action, opts = {}) {
9
- const headers = { ...(opts.headers ?? {}) };
10
- if (opts.token)
11
- headers.authorization = `Bearer ${opts.token}`;
12
- if (opts.body !== undefined)
13
- headers["content-type"] = "application/json";
14
- const attempts = Math.max(1, 1 + rateLimitRetries(opts.rateLimitRetries));
15
- for (let attempt = 0; attempt < attempts; attempt += 1) {
16
- const controller = new AbortController();
17
- const timer = setTimeout(() => controller.abort(), requestTimeoutMs(opts.timeoutMs));
18
- let res;
19
- try {
20
- res = await fetch(url, {
21
- method: opts.method ?? "GET",
22
- headers,
23
- signal: controller.signal,
24
- ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
25
- });
26
- }
27
- catch (err) {
28
- if (err.name === "AbortError") {
29
- throw new FloomError(`Request timed out while trying to ${action}.`, "Try again in a moment.");
30
- }
31
- throw friendlyNetwork(err);
32
- }
33
- finally {
34
- clearTimeout(timer);
35
- }
36
- if (res.status === 429 && attempt < attempts - 1) {
37
- await drainResponse(res);
38
- await sleep(rateLimitDelayMs(res, attempt));
39
- continue;
40
- }
41
- if (opts.checkOk !== false && !res.ok) {
42
- throw friendlyHttp(res.status, action, await responseErrorDetail(res));
43
- }
44
- return res;
45
- }
46
- throw new FloomError(`Request failed (HTTP 429) while trying to ${action}.`);
47
- }
48
- function requestTimeoutMs(override) {
49
- if (override !== undefined)
50
- return override;
51
- const fromEnv = Number(process.env.FLOOM_REQUEST_TIMEOUT_MS);
52
- if (Number.isFinite(fromEnv) && fromEnv > 0)
53
- return fromEnv;
54
- return DEFAULT_TIMEOUT_MS;
55
- }
56
- function rateLimitRetries(override) {
57
- if (override !== undefined)
58
- return Math.max(0, Math.floor(override));
59
- const fromEnv = Number(process.env.FLOOM_RATE_LIMIT_RETRIES);
60
- if (Number.isFinite(fromEnv) && fromEnv >= 0)
61
- return Math.floor(fromEnv);
62
- return 8;
63
- }
64
- function rateLimitDelayMs(res, attempt) {
65
- const retryAfter = res.headers.get("retry-after");
66
- if (retryAfter) {
67
- const seconds = Number(retryAfter);
68
- if (Number.isFinite(seconds) && seconds >= 0) {
69
- return capRateLimitDelay(seconds * 1000);
70
- }
71
- const dateMs = Date.parse(retryAfter);
72
- if (Number.isFinite(dateMs)) {
73
- return capRateLimitDelay(Math.max(0, dateMs - Date.now()));
74
- }
75
- }
76
- const resetSeconds = Number(res.headers.get("x-ratelimit-reset"));
77
- if (Number.isFinite(resetSeconds) && resetSeconds > 0) {
78
- return capRateLimitDelay(Math.max(0, (resetSeconds * 1000) - Date.now()));
79
- }
80
- const base = Number(process.env.FLOOM_RATE_LIMIT_RETRY_MS);
81
- const fallback = Number.isFinite(base) && base >= 0 ? base : 1_000;
82
- return capRateLimitDelay(fallback * (attempt + 1));
83
- }
84
- function capRateLimitDelay(ms) {
85
- const fromEnv = Number(process.env.FLOOM_RATE_LIMIT_MAX_WAIT_MS);
86
- const max = Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 65_000;
87
- return Math.min(Math.max(0, ms), max);
88
- }
89
- async function drainResponse(res) {
90
- try {
91
- await res.arrayBuffer();
92
- }
93
- catch {
94
- // Ignore bodies from rate-limit responses; the retry decision is header-based.
95
- }
96
- }
97
- async function responseErrorDetail(res) {
98
- try {
99
- const text = await res.text();
100
- if (!text.trim())
101
- return null;
102
- try {
103
- const json = JSON.parse(text);
104
- const detail = typeof json.error === "string"
105
- ? json.error
106
- : typeof json.message === "string"
107
- ? json.message
108
- : null;
109
- const hint = typeof json.hint === "string" ? json.hint : null;
110
- return [detail, hint].filter(Boolean).join("\n") || null;
111
- }
112
- catch {
113
- return text.trim().slice(0, 400);
114
- }
115
- }
116
- catch {
117
- return null;
118
- }
119
- }
120
- function sleep(ms) {
121
- return new Promise((resolve) => setTimeout(resolve, ms));
122
- }
123
- export async function getJson(url, action, token) {
124
- const res = await floomFetch(url, action, token ? { token } : {});
125
- return (await res.json());
126
- }
127
- export async function getText(url, action, token) {
128
- const res = await floomFetch(url, action, token ? { token } : {});
129
- return res.text();
130
- }
131
- export async function postJson(url, action, token, body) {
132
- const res = await floomFetch(url, action, { method: "POST", token, body });
133
- return (await res.json());
134
- }
135
- export async function deleteRequest(url, action, token) {
136
- const res = await floomFetch(url, action, { method: "DELETE", token });
137
- return (await res.json());
138
- }
139
- export async function putJson(url, action, token, body) {
140
- const res = await floomFetch(url, action, { method: "PUT", token, body });
141
- return (await res.json());
142
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * CLI-side mirror of the web's skill-labels: type label + tool label + a tiny
3
- * frontmatter parser for `requires:`. No React, no `gray-matter` — keep the
4
- * CLI dep tree thin.
5
- *
6
- * The frontmatter parser intentionally only supports two `requires` shapes:
7
- * requires: [gmail, linear]
8
- * requires:
9
- * - gmail
10
- * - linear
11
- *
12
- * That covers >99% of real skills. If a skill uses a more exotic YAML shape
13
- * we silently get an empty list — not a crash.
14
- */
15
- const SKILL_TYPE_LABELS = {
16
- knowledge: "Knowledge",
17
- instruction: "Instruction",
18
- workflow: "Workflow",
19
- skill: "Skill",
20
- };
21
- const TOOL_LABELS = {
22
- gmail: "Gmail",
23
- "google-mail": "Gmail",
24
- calendar: "Google Calendar",
25
- "google-calendar": "Google Calendar",
26
- drive: "Google Drive",
27
- "google-drive": "Google Drive",
28
- docs: "Google Docs",
29
- "google-docs": "Google Docs",
30
- sheets: "Google Sheets",
31
- "google-sheets": "Google Sheets",
32
- github: "GitHub",
33
- gitlab: "GitLab",
34
- linear: "Linear",
35
- notion: "Notion",
36
- slack: "Slack",
37
- intercom: "Intercom",
38
- zendesk: "Zendesk",
39
- hubspot: "HubSpot",
40
- salesforce: "Salesforce",
41
- stripe: "Stripe",
42
- airtable: "Airtable",
43
- jira: "Jira",
44
- asana: "Asana",
45
- trello: "Trello",
46
- zapier: "Zapier",
47
- composio: "Composio",
48
- whatsapp: "WhatsApp",
49
- telegram: "Telegram",
50
- discord: "Discord",
51
- twitter: "Twitter",
52
- x: "X",
53
- linkedin: "LinkedIn",
54
- openai: "OpenAI",
55
- anthropic: "Anthropic",
56
- gemini: "Gemini",
57
- claude: "Claude",
58
- perplexity: "Perplexity",
59
- supabase: "Supabase",
60
- vercel: "Vercel",
61
- resend: "Resend",
62
- twilio: "Twilio",
63
- webhook: "Webhook",
64
- http: "HTTP",
65
- };
66
- export function formatType(value) {
67
- if (!value)
68
- return "Skill";
69
- const normalized = value.toLowerCase();
70
- return SKILL_TYPE_LABELS[normalized] ?? "Skill";
71
- }
72
- export function formatTool(slug) {
73
- const normalized = slug.trim().toLowerCase();
74
- if (!normalized)
75
- return slug;
76
- if (TOOL_LABELS[normalized])
77
- return TOOL_LABELS[normalized];
78
- return normalized
79
- .split(/[-_\s]+/)
80
- .filter(Boolean)
81
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
82
- .join(" ");
83
- }
84
- export function formatToolList(tools) {
85
- return tools.map(formatTool).join(", ");
86
- }
87
- /**
88
- * Pull `requires:` out of YAML frontmatter. Returns a deduped lowercase list
89
- * or [] if absent / malformed.
90
- */
91
- export function extractRequires(body) {
92
- if (!body)
93
- return [];
94
- const trimmed = body.replace(/^/, "");
95
- if (!trimmed.startsWith("---"))
96
- return [];
97
- const end = trimmed.indexOf("\n---", 3);
98
- if (end === -1)
99
- return [];
100
- const headerBlock = trimmed.slice(3, end);
101
- const lines = headerBlock.split(/\r?\n/);
102
- const seen = new Set();
103
- const out = [];
104
- for (let i = 0; i < lines.length; i++) {
105
- const raw = lines[i] ?? "";
106
- const line = raw.trim();
107
- const inlineMatch = /^requires\s*:\s*\[(.*?)\]\s*$/i.exec(line);
108
- if (inlineMatch) {
109
- const inner = inlineMatch[1] ?? "";
110
- for (const part of inner.split(",")) {
111
- const value = part.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
112
- if (value && !seen.has(value)) {
113
- seen.add(value);
114
- out.push(value);
115
- }
116
- }
117
- return out;
118
- }
119
- if (/^requires\s*:\s*$/i.test(line)) {
120
- // Block form. Scan subsequent indented `- foo` lines until indent breaks.
121
- for (let j = i + 1; j < lines.length; j++) {
122
- const next = lines[j] ?? "";
123
- const itemMatch = /^\s+-\s+(.+?)\s*$/.exec(next);
124
- if (!itemMatch) {
125
- // First non-list line ends the block.
126
- if (next.trim() === "")
127
- continue;
128
- break;
129
- }
130
- const value = (itemMatch[1] ?? "").replace(/^['"]|['"]$/g, "").trim().toLowerCase();
131
- if (value && !seen.has(value)) {
132
- seen.add(value);
133
- out.push(value);
134
- }
135
- }
136
- return out;
137
- }
138
- }
139
- return out;
140
- }
package/dist/library.js DELETED
@@ -1,102 +0,0 @@
1
- import ora from "ora";
2
- import { readConfig, resolveApiUrl } from "./config.js";
3
- import { deleteRequest, getJson, postJson, putJson } from "./lib/api.js";
4
- import { c, symbols } from "./ui.js";
5
- import { FloomError } from "./errors.js";
6
- function formatLibraryRow(lib) {
7
- const name = lib.name ?? c.dim("(unnamed)");
8
- const vis = c.dim(`[${lib.visibility}]`);
9
- return ` ${c.cyan(lib.slug.padEnd(22))} ${name} ${vis}`;
10
- }
11
- export async function libraryList(opts = {}) {
12
- const cfg = await readConfig();
13
- const apiUrl = resolveApiUrl(cfg);
14
- const spinner = opts.json ? null : ora({ text: c.dim("Loading libraries..."), color: "yellow" }).start();
15
- let result;
16
- try {
17
- result = await getJson(`${apiUrl}/api/v1/libraries`, "load libraries", cfg?.accessToken);
18
- }
19
- finally {
20
- spinner?.stop();
21
- }
22
- if (opts.json) {
23
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
24
- return;
25
- }
26
- process.stdout.write(`\n${symbols.dot} ${c.bold("Public libraries")} ${c.dim(`(${result.libraries.length})`)}\n\n`);
27
- if (result.libraries.length === 0) {
28
- process.stdout.write(` ${c.dim("No public libraries yet.")}\n\n`);
29
- return;
30
- }
31
- for (const lib of result.libraries)
32
- process.stdout.write(`${formatLibraryRow(lib)}\n`);
33
- process.stdout.write("\n");
34
- }
35
- export async function libraryCreate(opts) {
36
- const cfg = await readConfig();
37
- if (!cfg)
38
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
39
- const apiUrl = resolveApiUrl(cfg);
40
- const result = await postJson(`${apiUrl}/api/v1/libraries`, "create library", cfg.accessToken, {
41
- slug: opts.slug,
42
- name: opts.name,
43
- ...(opts.description !== undefined ? { description: opts.description } : {}),
44
- ...(opts.visibility ? { visibility: opts.visibility } : {}),
45
- });
46
- process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
47
- process.stdout.write(` ${c.dim("API:")} ${apiUrl}/api/v1/libraries/${result.slug}\n`);
48
- process.stdout.write(` ${c.dim("Sync:")} floom library subscribe ${result.slug}\n\n`);
49
- }
50
- export async function libraryAddSkill(opts) {
51
- const cfg = await readConfig();
52
- if (!cfg)
53
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
54
- const apiUrl = resolveApiUrl(cfg);
55
- await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
56
- skill_slug: opts.skillSlug,
57
- ...(opts.folder !== undefined ? { folder: opts.folder } : {}),
58
- ...(opts.tags ? { tags: opts.tags } : {}),
59
- });
60
- const folderText = opts.folder ?? c.dim("(root)");
61
- const tagsText = opts.tags?.length ? opts.tags.join(", ") : c.dim("(none)");
62
- process.stdout.write(`\n${symbols.ok} Added ${c.cyan(opts.skillSlug)} to ${c.cyan(opts.librarySlug)}\n`);
63
- process.stdout.write(` ${c.dim("folder:")} ${folderText}\n`);
64
- process.stdout.write(` ${c.dim("tags:")} ${tagsText}\n\n`);
65
- }
66
- export async function libraryRemoveSkill(librarySlug, skillSlug) {
67
- const cfg = await readConfig();
68
- if (!cfg)
69
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
70
- const apiUrl = resolveApiUrl(cfg);
71
- await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
72
- process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
73
- }
74
- export async function librarySubscribe(slug) {
75
- const cfg = await readConfig();
76
- if (!cfg)
77
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
78
- const apiUrl = resolveApiUrl(cfg);
79
- await postJson(`${apiUrl}/api/v1/me/subscriptions`, "subscribe to library", cfg.accessToken, { library_slug: slug });
80
- process.stdout.write(`\n${symbols.ok} Subscribed to ${c.cyan(slug)}\n`);
81
- process.stdout.write(` ${c.dim("Run `floom sync --target claude` or `floom sync --target codex` to pull this library locally.")}\n\n`);
82
- }
83
- export async function libraryUnsubscribe(slug) {
84
- const cfg = await readConfig();
85
- if (!cfg)
86
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
87
- const apiUrl = resolveApiUrl(cfg);
88
- await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, "unsubscribe from library", cfg.accessToken);
89
- process.stdout.write(`\n${symbols.ok} Unsubscribed from ${c.cyan(slug)}\n\n`);
90
- }
91
- export async function moveSkill(opts) {
92
- const cfg = await readConfig();
93
- if (!cfg)
94
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
95
- const apiUrl = resolveApiUrl(cfg);
96
- await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(opts.slug)}/override`, "set skill override", cfg.accessToken, { folder: opts.folder, tags: opts.tags });
97
- const folderText = opts.folder ?? c.dim("(root)");
98
- const tagsText = opts.tags.length ? opts.tags.join(", ") : c.dim("(none)");
99
- process.stdout.write(`\n${symbols.ok} Moved ${c.cyan(opts.slug)}\n`);
100
- process.stdout.write(` ${c.dim("folder:")} ${folderText}\n`);
101
- process.stdout.write(` ${c.dim("tags:")} ${tagsText}\n\n`);
102
- }
package/dist/list.js DELETED
@@ -1,79 +0,0 @@
1
- import ora from "ora";
2
- import { readConfig, resolveApiUrl } from "./config.js";
3
- import { getJson } from "./lib/api.js";
4
- import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
5
- import { c, symbols } from "./ui.js";
6
- import { FloomError } from "./errors.js";
7
- import { formatVersionLabel } from "./version.js";
8
- function formatRow(s) {
9
- const title = s.title ?? c.dim("(untitled)");
10
- const visibility = c.dim(`[${s.visibility}]`);
11
- const type = c.dim(`[${formatType(s.asset_type)}]`);
12
- const version = s.version ? c.dim(formatVersionLabel(s.version)) : "";
13
- const updated = c.dim(formatRelative(s.updated_at));
14
- const requires = extractRequires(s.body_md);
15
- const needs = requires.length > 0 ? c.dim(`needs ${formatToolList(requires)}`) : "";
16
- return ` ${c.cyan(s.slug.padEnd(14))} ${title} ${type} ${visibility} ${version} ${updated}${needs ? ` ${needs}` : ""}`;
17
- }
18
- function formatRelative(iso) {
19
- const diff = Date.now() - new Date(iso).getTime();
20
- if (Number.isNaN(diff))
21
- return iso;
22
- const sec = Math.floor(diff / 1000);
23
- if (sec < 60)
24
- return `${sec}s ago`;
25
- const min = Math.floor(sec / 60);
26
- if (min < 60)
27
- return `${min}m ago`;
28
- const hr = Math.floor(min / 60);
29
- if (hr < 24)
30
- return `${hr}h ago`;
31
- const day = Math.floor(hr / 24);
32
- if (day < 30)
33
- return `${day}d ago`;
34
- return new Date(iso).toISOString().slice(0, 10);
35
- }
36
- export async function list(opts) {
37
- const cfg = await readConfig();
38
- if (!cfg) {
39
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
40
- }
41
- const apiUrl = resolveApiUrl(cfg);
42
- const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
43
- let published = [];
44
- try {
45
- let cursor;
46
- const seenCursors = new Set();
47
- for (let page = 0; page < 1000; page += 1) {
48
- const url = new URL(`${apiUrl}/api/v1/me/skills`);
49
- url.searchParams.set("limit", "100");
50
- url.searchParams.set("scope", "owned");
51
- if (cursor)
52
- url.searchParams.set("cursor", cursor);
53
- const mine = await getJson(url.toString(), "load your skills", cfg.accessToken);
54
- published.push(...(mine.skills ?? []));
55
- if (!mine.next_cursor)
56
- break;
57
- if (seenCursors.has(mine.next_cursor))
58
- throw new FloomError("Invalid skills response.");
59
- seenCursors.add(mine.next_cursor);
60
- cursor = mine.next_cursor;
61
- }
62
- }
63
- finally {
64
- spinner?.stop();
65
- }
66
- if (opts.json) {
67
- process.stdout.write(`${JSON.stringify({ published }, null, 2)}\n`);
68
- return;
69
- }
70
- process.stdout.write(`\n${symbols.dot} ${c.bold("Skills")} ${c.dim(`(${published.length})`)}\n\n`);
71
- if (published.length === 0) {
72
- process.stdout.write(` ${c.dim("Nothing published yet. Try `npx -y @floomhq/floom publish my-skill`.")}\n`);
73
- }
74
- else {
75
- for (const s of published)
76
- process.stdout.write(`${formatRow(s)}\n`);
77
- }
78
- process.stdout.write("\n");
79
- }