@floomhq/floom 1.0.64 → 2.0.1

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/doctor.js DELETED
@@ -1,381 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { execFile as execFileCb } from "node:child_process";
3
- import { delimiter } from "node:path";
4
- import { join } from "node:path";
5
- import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
6
- import { promisify } from "node:util";
7
- import { readConfig, refreshConfigIfNeeded, CONFIG_PATH, resolveApiUrl } from "./config.js";
8
- import { floomFetch } from "./lib/api.js";
9
- import { c, symbols } from "./ui.js";
10
- import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
11
- import { targetSkillsDir } from "./targets.js";
12
- const execFile = promisify(execFileCb);
13
- function statusBadge(s) {
14
- if (s === "ok")
15
- return c.green(symbols.ok);
16
- if (s === "warn")
17
- return c.yellow("!");
18
- return c.red(symbols.fail);
19
- }
20
- async function checkAuth() {
21
- const cfg = await readConfig();
22
- if (!cfg) {
23
- return {
24
- name: "Auth",
25
- status: "ok",
26
- detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
27
- };
28
- }
29
- const apiUrl = resolveApiUrl(cfg);
30
- try {
31
- const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
32
- token: cfg.accessToken,
33
- checkOk: false,
34
- });
35
- if (res.status === 401) {
36
- const refreshed = await refreshConfigIfNeeded(cfg, { force: true });
37
- if (refreshed.accessToken !== cfg.accessToken) {
38
- const retry = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
39
- token: refreshed.accessToken,
40
- checkOk: false,
41
- });
42
- if (retry.ok) {
43
- const data = (await retry.json());
44
- return {
45
- name: "Auth",
46
- status: "ok",
47
- detail: data.email ? `Signed in as ${data.email} (refreshed)` : "Signed in (refreshed)",
48
- };
49
- }
50
- }
51
- return {
52
- name: "Auth",
53
- status: "fail",
54
- detail: "Token rejected (401).",
55
- hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
56
- };
57
- }
58
- if (!res.ok) {
59
- return {
60
- name: "Auth",
61
- status: "warn",
62
- detail: `API returned ${res.status}.`,
63
- hint: "Service may be degraded; try again shortly.",
64
- };
65
- }
66
- const data = (await res.json());
67
- return {
68
- name: "Auth",
69
- status: "ok",
70
- detail: data.email ? `Signed in as ${data.email}` : "Signed in",
71
- };
72
- }
73
- catch (err) {
74
- const msg = err instanceof Error ? err.message : String(err);
75
- return {
76
- name: "Auth",
77
- status: "fail",
78
- detail: `Cannot reach ${apiUrl}: ${msg}`,
79
- hint: "Check your network. Try `curl " + apiUrl + "/api/me`.",
80
- };
81
- }
82
- }
83
- async function checkMcp() {
84
- const candidates = [
85
- { name: "Claude Code", path: join(homedir(), ".claude.json") },
86
- { name: "Claude Code (settings)", path: join(homedir(), ".claude", "settings.json") },
87
- { name: "Codex", path: join(homedir(), ".codex", "config.toml") },
88
- { name: "Cursor", path: join(homedir(), ".cursor", "mcp.json") },
89
- { name: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json") },
90
- { name: "Kimi", path: join(homedir(), ".kimi", "mcp.json") },
91
- { name: "Gemini", path: join(homedir(), ".gemini", "settings.json") },
92
- ];
93
- const found = [];
94
- for (const cand of candidates) {
95
- try {
96
- const buf = await readFile(cand.path, "utf8");
97
- // Cheap match: any reference to a Floom MCP server. The actual MCP
98
- // package name is @floomhq/floom-mcp-sync. Detect by package name OR
99
- // by "floom" + "mcp_servers"/"mcpServers" co-occurrence.
100
- const hasFloom = buf.includes("@floomhq/floom-mcp-sync") ||
101
- (buf.includes("\"floom\"") && (buf.includes("mcpServers") || buf.includes("mcp_servers") || buf.includes("\"mcp\""))) ||
102
- (buf.includes("[mcp_servers.floom]"));
103
- if (hasFloom)
104
- found.push({ name: cand.name, configPath: cand.path, hasFloom });
105
- }
106
- catch {
107
- // file missing — fine
108
- }
109
- }
110
- if (found.length === 0) {
111
- return {
112
- name: "MCP",
113
- status: "ok",
114
- detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
115
- };
116
- }
117
- return {
118
- name: "MCP",
119
- status: "ok",
120
- detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
121
- };
122
- }
123
- async function checkTargetDir(target) {
124
- const dir = targetSkillsDir(target);
125
- try {
126
- const s = await stat(dir);
127
- if (!s.isDirectory()) {
128
- return {
129
- name: "Target dir",
130
- status: "fail",
131
- detail: `${dir} exists but is not a directory.`,
132
- };
133
- }
134
- try {
135
- await access(dir, constants.W_OK);
136
- return { name: "Target dir", status: "ok", detail: `${dir} (writable)` };
137
- }
138
- catch {
139
- return {
140
- name: "Target dir",
141
- status: "fail",
142
- detail: `${dir} is not writable.`,
143
- hint: `Try: chmod u+w ${dir}`,
144
- };
145
- }
146
- }
147
- catch (err) {
148
- if (err.code === "ENOENT") {
149
- return {
150
- name: "Target dir",
151
- status: "warn",
152
- detail: `${dir} does not exist yet.`,
153
- hint: `It will be created on first \`npx -y @floomhq/floom add <link> --target ${target}\`.`,
154
- };
155
- }
156
- throw err;
157
- }
158
- }
159
- async function checkLastSync(target) {
160
- const dir = targetSkillsDir(target);
161
- try {
162
- const entries = await readdir(dir);
163
- const skills = entries.filter((e) => e.endsWith(".md") || !e.startsWith("."));
164
- if (skills.length === 0) {
165
- return {
166
- name: "Last sync",
167
- status: "warn",
168
- detail: "No synced skills found yet.",
169
- hint: `Run \`npx -y @floomhq/floom add <link> --target ${target}\` to install your first skill.`,
170
- };
171
- }
172
- // Find most recently modified entry
173
- let newest = { name: "", mtime: 0 };
174
- for (const name of skills) {
175
- try {
176
- const s = await stat(join(dir, name));
177
- if (s.mtimeMs > newest.mtime)
178
- newest = { name, mtime: s.mtimeMs };
179
- }
180
- catch {
181
- // skip
182
- }
183
- }
184
- if (newest.mtime === 0) {
185
- return {
186
- name: "Last sync",
187
- status: "warn",
188
- detail: `${skills.length} entries, no readable mtime.`,
189
- };
190
- }
191
- const ageSec = Math.round((Date.now() - newest.mtime) / 1000);
192
- const human = ageSec < 60
193
- ? `${ageSec}s ago`
194
- : ageSec < 3600
195
- ? `${Math.round(ageSec / 60)}m ago`
196
- : ageSec < 86400
197
- ? `${Math.round(ageSec / 3600)}h ago`
198
- : `${Math.round(ageSec / 86400)}d ago`;
199
- return {
200
- name: "Last sync",
201
- status: "ok",
202
- detail: `${newest.name} — ${human} (${skills.length} total)`,
203
- };
204
- }
205
- catch (err) {
206
- if (err.code === "ENOENT") {
207
- return {
208
- name: "Last sync",
209
- status: "warn",
210
- detail: "Skills dir not created yet.",
211
- };
212
- }
213
- return {
214
- name: "Last sync",
215
- status: "warn",
216
- detail: `Cannot read skills dir: ${err.message}`,
217
- };
218
- }
219
- }
220
- async function checkVersion() {
221
- const apiUrl = resolveApiUrl(await readConfig());
222
- try {
223
- const res = await floomFetch(`${apiUrl}/api/v1/cli-version`, "check CLI version", {
224
- headers: { accept: "application/json" },
225
- checkOk: false,
226
- });
227
- if (!res.ok) {
228
- // Endpoint optional — treat as info-only, not a failure
229
- return {
230
- name: "Version",
231
- status: "ok",
232
- detail: `CLI ${formatVersionLabel(CLI_VERSION)} (server check skipped)`,
233
- };
234
- }
235
- const data = (await res.json());
236
- if (data.min && compareSemverish(CLI_VERSION, data.min) < 0) {
237
- return {
238
- name: "Version",
239
- status: "fail",
240
- detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
241
- hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
242
- };
243
- }
244
- if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
245
- return {
246
- name: "Version",
247
- status: "warn",
248
- detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
249
- hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
250
- };
251
- }
252
- return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
253
- }
254
- catch {
255
- return {
256
- name: "Version",
257
- status: "ok",
258
- detail: `CLI ${formatVersionLabel(CLI_VERSION)} (offline)`,
259
- };
260
- }
261
- }
262
- async function findPathExecutables(name) {
263
- const seen = new Set();
264
- const out = [];
265
- const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
266
- for (const dir of pathDirs) {
267
- const candidate = join(dir, name);
268
- if (seen.has(candidate))
269
- continue;
270
- seen.add(candidate);
271
- try {
272
- await access(candidate, constants.X_OK);
273
- out.push(candidate);
274
- }
275
- catch {
276
- // not present or not executable
277
- }
278
- }
279
- return out;
280
- }
281
- async function safeRealpath(path) {
282
- if (!path)
283
- return null;
284
- try {
285
- return await realpath(path);
286
- }
287
- catch {
288
- return null;
289
- }
290
- }
291
- async function checkCliCommand() {
292
- const bins = await findPathExecutables("floom");
293
- if (bins.length === 0) {
294
- return {
295
- name: "CLI command",
296
- status: "ok",
297
- detail: "`floom` is not globally installed. `npx -y @floomhq/floom ...` is ready.",
298
- };
299
- }
300
- const active = bins[0] ?? "";
301
- const activeReal = await safeRealpath(active);
302
- const currentReal = await safeRealpath(process.argv[1]);
303
- if (activeReal && currentReal && activeReal === currentReal) {
304
- return {
305
- name: "CLI command",
306
- status: "ok",
307
- detail: `floom resolves to ${active}`,
308
- };
309
- }
310
- const activeText = await readExecutableSample(active);
311
- const activeVersion = await readExecutableVersion(active);
312
- const looksLikeOldRuntime = active.includes(".floom/repo") ||
313
- active.includes("/skills-minimal/") ||
314
- activeText.includes("unknown command") ||
315
- activeText.includes("Floom runtime") ||
316
- (activeVersion !== null && compareSemverish(activeVersion, CLI_VERSION) < 0);
317
- return {
318
- name: "CLI command",
319
- status: looksLikeOldRuntime ? "warn" : "ok",
320
- detail: looksLikeOldRuntime
321
- ? `Another floom command is first on PATH: ${active}${activeVersion ? ` (${formatVersionLabel(activeVersion)})` : ""}`
322
- : `floom resolves to ${active}`,
323
- ...(looksLikeOldRuntime
324
- ? {
325
- hint: `If \`floom doctor\` opens the old runtime CLI, remove that file or move it later in PATH, then run \`npm i -g @floomhq/floom\`. Inspect with: command -v floom`,
326
- }
327
- : {}),
328
- };
329
- }
330
- async function readExecutableSample(path) {
331
- try {
332
- return (await readFile(path, "utf8")).slice(0, 4000);
333
- }
334
- catch {
335
- return "";
336
- }
337
- }
338
- async function readExecutableVersion(path) {
339
- try {
340
- const { stdout, stderr } = await execFile(path, ["--version"], { timeout: 1500 });
341
- const text = `${stdout}\n${stderr}`;
342
- return text.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?/)?.[0] ?? null;
343
- }
344
- catch {
345
- return null;
346
- }
347
- }
348
- export async function doctor(opts = {}) {
349
- const target = opts.target ?? "claude";
350
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
351
- const checks = await Promise.all([
352
- checkCliCommand(),
353
- checkAuth(),
354
- checkMcp(),
355
- checkTargetDir(target),
356
- checkLastSync(target),
357
- checkVersion(),
358
- ]);
359
- // Compute column widths for clean table output.
360
- const nameW = Math.max(...checks.map((c) => c.name.length), 6);
361
- for (const check of checks) {
362
- const padded = check.name.padEnd(nameW);
363
- process.stdout.write(` ${statusBadge(check.status)} ${c.bold(padded)} ${check.detail}\n`);
364
- if (check.hint && check.status !== "ok") {
365
- process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
366
- }
367
- }
368
- const anyFail = checks.some((c) => c.status === "fail");
369
- const anyWarn = checks.some((c) => c.status === "warn");
370
- process.stdout.write("\n");
371
- if (anyFail) {
372
- process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
373
- process.exit(1);
374
- }
375
- if (anyWarn) {
376
- process.stdout.write(` ${c.yellow("! All critical checks passed, with warnings.")}\n\n`);
377
- process.exit(0);
378
- }
379
- process.stdout.write(` ${c.green("✓ All checks passed.")} Floom is healthy.\n\n`);
380
- process.stdout.write(` ${c.dim("Config: " + CONFIG_PATH)}\n\n`);
381
- }
package/dist/errors.js DELETED
@@ -1,71 +0,0 @@
1
- import { c, symbols } from "./ui.js";
2
- /**
3
- * FloomError is a friendly, user-facing error. Throw this instead of raw Error
4
- * when you want a clean message (no stack trace, no "Error:" prefix).
5
- */
6
- export class FloomError extends Error {
7
- hint;
8
- constructor(message, hint) {
9
- super(message);
10
- this.name = "FloomError";
11
- if (hint)
12
- this.hint = hint;
13
- }
14
- }
15
- export function friendlyHttp(status, action, detail) {
16
- if (status === 401) {
17
- return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
18
- }
19
- if (status === 403) {
20
- return new FloomError(`You don't have permission to ${action}.`);
21
- }
22
- if (status === 404) {
23
- if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
24
- return new FloomError("Skill not found.", "Check the link or slug, then try again.");
25
- }
26
- return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
27
- }
28
- if (status === 413) {
29
- return new FloomError("That file is too large to publish.");
30
- }
31
- if (status === 429) {
32
- return new FloomError("Floom is rate limiting requests.", "The CLI retries rate-limited requests automatically. Try again in a moment if this persists.");
33
- }
34
- if (status >= 500) {
35
- return new FloomError("Floom is having trouble right now.", "Try again in a moment.");
36
- }
37
- if (detail) {
38
- return new FloomError(detail);
39
- }
40
- return new FloomError(`Request failed (HTTP ${status}) while trying to ${action}.`);
41
- }
42
- export function friendlyNetwork(err) {
43
- const msg = err instanceof Error ? err.message : String(err);
44
- if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|fetch failed/i.test(msg)) {
45
- return new FloomError("Couldn't reach floom (offline?).", "Try again in a moment.");
46
- }
47
- return new FloomError(msg);
48
- }
49
- export function printError(err, opts = {}) {
50
- if (opts.json) {
51
- const error = err instanceof FloomError
52
- ? { error: err.message, hint: err.hint ?? null }
53
- : { error: err instanceof Error ? err.message : String(err), hint: null };
54
- process.stderr.write(`${JSON.stringify(error)}\n`);
55
- return;
56
- }
57
- if (err instanceof FloomError) {
58
- process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
59
- if (err.hint) {
60
- for (const line of err.hint.split("\n")) {
61
- process.stderr.write(` ${c.dim(line)}\n`);
62
- }
63
- process.stderr.write("\n");
64
- }
65
- else
66
- process.stderr.write("\n");
67
- return;
68
- }
69
- const msg = err instanceof Error ? err.message : String(err);
70
- process.stderr.write(`\n${symbols.fail} ${msg}\n\n`);
71
- }
package/dist/feedback.js DELETED
@@ -1,34 +0,0 @@
1
- import ora from "ora";
2
- import { readConfig, resolveApiUrl } from "./config.js";
3
- import { floomFetch } from "./lib/api.js";
4
- import { c, symbols } from "./ui.js";
5
- export async function feedback(opts) {
6
- const cfg = await readConfig();
7
- const apiUrl = resolveApiUrl(cfg);
8
- const payload = {
9
- event_name: "feedback.submitted",
10
- source: "cli",
11
- props: {
12
- kind: opts.kind,
13
- message: opts.message,
14
- ...(opts.target ? { target: opts.target } : {}),
15
- ...(opts.skill ? { skill: opts.skill } : {}),
16
- },
17
- };
18
- const spinner = opts.json ? null : ora({ text: c.dim("Sending feedback..."), color: "yellow" }).start();
19
- try {
20
- await floomFetch(`${apiUrl}/api/v1/events`, "send feedback", {
21
- method: "POST",
22
- ...(cfg?.accessToken ? { token: cfg.accessToken } : {}),
23
- body: payload,
24
- });
25
- }
26
- finally {
27
- spinner?.stop();
28
- }
29
- if (opts.json) {
30
- process.stdout.write(`${JSON.stringify({ ok: true }, null, 2)}\n`);
31
- return;
32
- }
33
- process.stdout.write(`\n${symbols.ok} Feedback sent\n\n`);
34
- }
package/dist/info.js DELETED
@@ -1,78 +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
- const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
8
- function slugFromInput(input) {
9
- const trimmed = input.trim();
10
- try {
11
- const url = new URL(trimmed);
12
- const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
13
- return last.replace(/\.(md|json)$/i, "");
14
- }
15
- catch {
16
- return trimmed.replace(/\.(md|json)$/i, "");
17
- }
18
- }
19
- export async function info(opts) {
20
- const slug = slugFromInput(opts.slug);
21
- if (!slug)
22
- throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
23
- if (!SLUG_RE.test(slug)) {
24
- throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
25
- }
26
- const cfg = await readConfig();
27
- const apiUrl = resolveApiUrl(cfg);
28
- const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
29
- let detail;
30
- try {
31
- detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "load skill metadata", cfg?.accessToken);
32
- }
33
- finally {
34
- spinner?.stop();
35
- }
36
- if (opts.json) {
37
- // Strip body_md from JSON output to keep it summary-shaped, but expose
38
- // the parsed `requires` list so scripted consumers don't need to parse
39
- // YAML themselves.
40
- const { body_md: _body, ...summary } = detail;
41
- const requires = extractRequires(detail.body_md);
42
- process.stdout.write(`${JSON.stringify({ ...summary, requires }, null, 2)}\n`);
43
- return;
44
- }
45
- const title = detail.title ?? c.dim("(untitled)");
46
- process.stdout.write(`\n${symbols.dot} ${c.bold(title)}\n\n`);
47
- process.stdout.write(` ${c.dim("Slug: ")}${detail.slug}\n`);
48
- if (detail.description) {
49
- process.stdout.write(` ${c.dim("About: ")}${detail.description}\n`);
50
- }
51
- process.stdout.write(` ${c.dim("Visibility: ")}${detail.visibility}\n`);
52
- process.stdout.write(` ${c.dim("Type: ")}${formatType(detail.asset_type)}\n`);
53
- const requires = extractRequires(detail.body_md);
54
- if (requires.length > 0) {
55
- process.stdout.write(` ${c.dim("Needs: ")}${formatToolList(requires)}\n`);
56
- }
57
- if (detail.version) {
58
- process.stdout.write(` ${c.dim("Version: ")}${detail.version}\n`);
59
- }
60
- if (detail.installs_as) {
61
- process.stdout.write(` ${c.dim("Installs: ")}${detail.installs_as}\n`);
62
- }
63
- if (detail.original_filename) {
64
- process.stdout.write(` ${c.dim("File: ")}${detail.original_filename}\n`);
65
- }
66
- if (detail.owner_email) {
67
- process.stdout.write(` ${c.dim("Owner: ")}${detail.owner_email}\n`);
68
- }
69
- if (typeof detail.save_count === "number") {
70
- process.stdout.write(` ${c.dim("Saves: ")}${detail.save_count}\n`);
71
- }
72
- if (detail.shared_with_emails?.length) {
73
- process.stdout.write(` ${c.dim("Shared: ")}${detail.shared_with_emails.join(", ")}\n`);
74
- }
75
- process.stdout.write(` ${c.dim("Created: ")}${detail.created_at}\n`);
76
- process.stdout.write(` ${c.dim("Updated: ")}${detail.updated_at}\n`);
77
- process.stdout.write(` ${c.dim("URL: ")}${c.cyan(detail.url.replace(/\.md$/, ""))}\n\n`);
78
- }