@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.
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
package/dist/version.js CHANGED
@@ -1,25 +1 @@
1
- import pkg from "../package.json" with { type: "json" };
2
- export const CLI_VERSION = pkg.version;
3
- function numericParts(value) {
4
- const normalized = value.trim().replace(/^v/i, "");
5
- const numeric = normalized.match(/^\d+(?:\.\d+)*/)?.[0] ?? "0";
6
- return numeric.split(".").map((part) => Number.parseInt(part, 10));
7
- }
8
- export function compareSemverish(a, b) {
9
- const left = numericParts(a);
10
- const right = numericParts(b);
11
- const max = Math.max(left.length, right.length);
12
- for (let i = 0; i < max; i++) {
13
- const l = left[i] ?? 0;
14
- const r = right[i] ?? 0;
15
- if (l > r)
16
- return 1;
17
- if (l < r)
18
- return -1;
19
- }
20
- return 0;
21
- }
22
- export function formatVersionLabel(value) {
23
- const trimmed = value.trim();
24
- return trimmed.match(/^v/i) ? trimmed.replace(/^v/i, "v") : `v${trimmed}`;
25
- }
1
+ export const VERSION = "2.0.1";
package/package.json CHANGED
@@ -1,58 +1,50 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.64",
4
- "description": "Sync AI skills across agents and machines.",
3
+ "version": "2.0.1",
4
+ "description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
5
5
  "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "floom": "bin/floom.js",
9
- "floom-skills": "bin/floom.js"
6
+ "homepage": "https://floom.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/floomhq/skills-mvp.git"
10
10
  },
11
+ "types": "./dist/index.d.ts",
11
12
  "files": [
12
- "bin",
13
- "dist",
14
- "README.md",
15
- "LICENSE"
13
+ "dist"
16
14
  ],
17
- "engines": {
18
- "node": ">=20"
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "bin": {
20
+ "floom": "dist/index.js"
19
21
  },
20
22
  "scripts": {
21
- "build": "tsc",
22
- "dev": "tsc --watch",
23
- "typecheck": "tsc --noEmit",
24
- "test": "npm run build && node --test test/*.mjs",
25
- "pack:check": "npm pack --dry-run",
26
- "prepack": "npm run build",
27
- "prepublishOnly": "npm run build"
23
+ "build": "tsc -p tsconfig.typecheck.json --noEmit && node scripts/build-bundle.mjs",
24
+ "dev": "tsx watch src/index.ts",
25
+ "lint": "tsc -p tsconfig.typecheck.json --noEmit --pretty false",
26
+ "pack:check": "node scripts/verify-package.mjs",
27
+ "pack:smoke": "pnpm run build && node scripts/verify-package.mjs --install-smoke",
28
+ "prepack": "pnpm run build",
29
+ "prepublishOnly": "pnpm run build && pnpm run pack:check",
30
+ "start": "node dist/index.js",
31
+ "test": "tsx --tsconfig tsconfig.typecheck.json --test \"src/**/*.test.ts\"",
32
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
28
33
  },
29
34
  "dependencies": {
30
- "clipboardy": "4.0.0",
31
- "open": "10.1.0",
32
- "ora": "8.1.1",
33
- "picocolors": "1.1.1",
34
- "update-notifier": "7.3.1",
35
- "yaml": "2.8.4"
35
+ "@modelcontextprotocol/sdk": "^1.12.2",
36
+ "chalk": "^5.3.0",
37
+ "commander": "^12.1.0",
38
+ "ora": "^8.1.0",
39
+ "prompts": "^2.4.2",
40
+ "tar": "^7.4.3",
41
+ "zod": "^3.23.8"
36
42
  },
37
43
  "devDependencies": {
38
- "@types/node": "22.10.5",
39
- "@types/update-notifier": "6.0.8",
40
- "typescript": "5.7.3"
41
- },
42
- "publishConfig": {
43
- "access": "public"
44
- },
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/floomhq/floom.git",
48
- "directory": "cli"
49
- },
50
- "keywords": [
51
- "ai",
52
- "skills",
53
- "claude",
54
- "markdown",
55
- "cli",
56
- "floom"
57
- ]
44
+ "esbuild": "^0.27.7",
45
+ "@types/node": "^22.0.0",
46
+ "@types/prompts": "^2.4.9",
47
+ "tsx": "^4.19.0",
48
+ "typescript": "^5.6.0"
49
+ }
58
50
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,90 +0,0 @@
1
- # @floomhq/floom
2
-
3
- Sync AI skills across agents and machines. Publish from your terminal, then add or sync skills with one command.
4
-
5
- ```bash
6
- npm install -g @floomhq/floom
7
- floom init my-skill.md
8
- floom init my-skill
9
- floom login
10
- floom publish my-skill
11
- floom share my-skill --add teammate@example.com
12
- floom search review
13
- floom add awesome-skill --setup
14
- floom setup --target claude --dry-run
15
- floom daemon install --target all
16
- floom status
17
- floom list
18
- floom library list
19
- ```
20
-
21
- 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.
22
-
23
- The package is designed for `npx -y @floomhq/floom ...`. Global installs expose `floom`; `floom doctor` reports older local Floom runtime CLIs if they shadow the new command.
24
-
25
- ## Commands
26
-
27
- - `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
28
- - `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.
29
- - `npx -y @floomhq/floom scan <path>` — check a skill file or package for high-confidence secrets, prompt-injection text, exfiltration instructions, and unsupported package layout before publishing.
30
- - `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>`.
31
- - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
32
- - `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
33
- - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into the selected agent skills root with supporting package files. Optional `--target claude|codex|cursor|opencode|kimi`, `--setup` to connect the agent, and `--force` to replace an existing local copy.
34
- - `npx -y @floomhq/floom info <url-or-slug>` — show skill metadata. Optional `--json`.
35
- - `npx -y @floomhq/floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
36
- - `npx -y @floomhq/floom setup` — add Floom usage guidance to the selected harness instructions file. Optional `--target claude|codex|cursor|opencode|kimi`, `--dry-run`, `--yes`.
37
- - `npx -y @floomhq/floom connect` — alias for setup.
38
- - `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
39
- - `npx -y @floomhq/floom sync` — pull your published, saved, and subscribed library skills into one target. Optional `--target claude|codex|cursor|opencode|kimi`.
40
- - `npx -y @floomhq/floom watch` — run one target sync loop. Optional `--target claude|codex|cursor|opencode|kimi`, `--push`, `--no-yolo`, `--interval <seconds>`; minimum `10`.
41
- - `npx -y @floomhq/floom daemon install --target all` — install always-on sync for Claude, Codex, Cursor, OpenCode, and Kimi.
42
- - `npx -y @floomhq/floom daemon status --json` — show the latest daemon cycle and per-target result.
43
- - `npx -y @floomhq/floom status --json` — reconcile cloud, cache, native projection, and daemon counts.
44
- - `npx -y @floomhq/floom feedback --kind <kind> --message <text>` — send bounded agent feedback to Floom.
45
- - `npx -y @floomhq/floom audit skills --json` — report malformed, duplicate, and suspicious owned skills.
46
- - `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
47
- - `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
48
- - `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
49
- - `npx -y @floomhq/floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
50
- - `npx -y @floomhq/floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
51
- - `npx -y @floomhq/floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
52
- - `npx -y @floomhq/floom doctor` — diagnose your Floom setup.
53
- - `npx -y @floomhq/floom whoami` — show the signed-in account.
54
- - `npx -y @floomhq/floom logout` — delete local credentials.
55
-
56
- ## Skill format
57
-
58
- A skill package is a folder:
59
-
60
- ```text
61
- my-skill/
62
- SKILL.md
63
- references/
64
- examples/
65
- scripts/
66
- assets/
67
- ```
68
-
69
- `SKILL.md` contains optional YAML frontmatter (`title`, `description`, `type`, `version`), then freeform Markdown. Supporting files are packaged only from `references/`, `examples/`, `scripts/`, and `assets/`.
70
-
71
- ```markdown
72
- ---
73
- title: Write a LinkedIn post
74
- description: Yurii-level value density
75
- version: 0.1.0
76
- ---
77
-
78
- # Instructions
79
- - ...
80
- ```
81
-
82
- ## Configuration
83
-
84
- Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
85
-
86
- `floom sync`, `floom watch`, and `floom daemon` handle published, saved, and subscribed library skills. Direct `floom sync --target <agent>` writes to that target's native skill root for compatibility. The always-on daemon writes pulled cloud packages into `~/.floom/skill-cache/<target>/` and keeps native harness roots as the user-editable projection/push surface.
87
-
88
- Sync manifests are target-scoped. Native projections are tracked under `~/.floom/native-sync-manifests/`; daemon cache roots keep their own `.floom-cli-sync-manifest.json`. The manifests record hashes for files Floom previously wrote. Sync writes missing files and applies cloud updates when the local file still matches the last Floom-tracked hash.
89
- Existing untracked files and locally edited tracked files are preserved as conflicts.
90
- Symlinks are never followed. To replace a local skill manually, run `floom add <url-or-slug> --force`.
package/bin/floom.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import "../dist/cli.js";
package/dist/audit.js DELETED
@@ -1,236 +0,0 @@
1
- import ora from "ora";
2
- import { readFile } from "node:fs/promises";
3
- import { readConfig, resolveApiUrl } from "./config.js";
4
- import { getJson, postJson } from "./lib/api.js";
5
- import { FloomError } from "./errors.js";
6
- import { c, symbols } from "./ui.js";
7
- const FIXTURE_RE = /\b(?:cli lifecycle audit fixture|launch gate|temp skill|test fixture|audit fixture)\b/i;
8
- const ARCHIVE_CHUNK_SIZE = 1000;
9
- function contentHash(skill) {
10
- return skill.content_sha256 ?? skill.content_hash ?? undefined;
11
- }
12
- function normalized(value) {
13
- return (value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
14
- }
15
- function riskFor(score, reasons) {
16
- if (score >= 90 || reasons.includes("security_scan_failure"))
17
- return "critical";
18
- if (score >= 70)
19
- return "high";
20
- if (score >= 40)
21
- return "medium";
22
- return "low";
23
- }
24
- function scoreSkill(skill, duplicateGroup) {
25
- const reasons = [];
26
- let score = 0;
27
- const title = normalized(skill.title);
28
- const description = normalized(skill.description);
29
- const body = skill.body_md ?? "";
30
- const slug = skill.slug;
31
- if (!title) {
32
- score += 20;
33
- reasons.push("blank_title");
34
- }
35
- if (!description) {
36
- score += 15;
37
- reasons.push("blank_description");
38
- }
39
- if (duplicateGroup) {
40
- score += 35;
41
- reasons.push("duplicate_content_hash");
42
- }
43
- if (FIXTURE_RE.test(`${skill.title ?? ""} ${skill.description ?? ""} ${slug}`)) {
44
- score += 25;
45
- reasons.push("launch_or_test_fixture");
46
- }
47
- if (title && title === normalized(slug)) {
48
- score += 20;
49
- reasons.push("slug_only_title");
50
- }
51
- if (body.trim().length > 0 && body.trim().length < 80) {
52
- score += 35;
53
- reasons.push("near_empty_body");
54
- }
55
- if (!/^---\s*\n/.test(body.trimStart())) {
56
- score += 30;
57
- reasons.push("missing_yaml_frontmatter");
58
- }
59
- if (/\b(?:api[_-]?key|secret|token)\b/i.test(body) && /\b(?:sk-|AIza|BEGIN PRIVATE KEY)\b/.test(body)) {
60
- score += 50;
61
- reasons.push("possible_secret");
62
- }
63
- if (score === 0)
64
- return null;
65
- const risk = riskFor(score, reasons);
66
- const hash = contentHash(skill);
67
- return {
68
- slug,
69
- title: skill.title?.trim() || "",
70
- risk,
71
- score,
72
- reasons,
73
- ...(hash ? { content_hash: hash } : {}),
74
- ...(duplicateGroup ? { duplicate_group: duplicateGroup } : {}),
75
- recommended_action: archiveRecommended(reasons) ? "archive" : "review",
76
- safe: !reasons.includes("possible_secret"),
77
- };
78
- }
79
- function archiveRecommended(reasons) {
80
- if (reasons.includes("possible_secret"))
81
- return true;
82
- if (reasons.includes("launch_or_test_fixture"))
83
- return true;
84
- if (reasons.includes("blank_title") && reasons.includes("duplicate_content_hash"))
85
- return true;
86
- if (reasons.includes("near_empty_body") && reasons.includes("duplicate_content_hash"))
87
- return true;
88
- return false;
89
- }
90
- async function loadOwnedSkills() {
91
- const cfg = await readConfig();
92
- if (!cfg)
93
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
94
- const apiUrl = resolveApiUrl(cfg);
95
- const skills = [];
96
- let cursor;
97
- const seenCursors = new Set();
98
- for (let page = 0; page < 1000; page += 1) {
99
- const url = new URL(`${apiUrl}/api/v1/me/skills`);
100
- url.searchParams.set("limit", "100");
101
- url.searchParams.set("scope", "owned");
102
- if (cursor)
103
- url.searchParams.set("cursor", cursor);
104
- const mine = await getJson(url.toString(), "audit your skills", cfg.accessToken);
105
- skills.push(...(mine.skills ?? []));
106
- if (!mine.next_cursor)
107
- break;
108
- if (seenCursors.has(mine.next_cursor))
109
- throw new FloomError("Invalid skills response.");
110
- seenCursors.add(mine.next_cursor);
111
- cursor = mine.next_cursor;
112
- }
113
- return skills;
114
- }
115
- function archiveSlugsFromPlan(raw) {
116
- const value = raw && typeof raw === "object" ? raw : {};
117
- const plan = value.archive_plan;
118
- if (!Array.isArray(plan))
119
- throw new FloomError("Invalid archive plan.", "Pass a JSON file from `floom audit skills --fix-plan`.");
120
- const slugs = plan.map((item) => {
121
- if (typeof item === "string")
122
- return item;
123
- if (item && typeof item === "object" && typeof item.slug === "string") {
124
- return item.slug;
125
- }
126
- return "";
127
- }).filter(Boolean);
128
- if (slugs.length === 0)
129
- throw new FloomError("Archive plan has no slugs.");
130
- return [...new Set(slugs)];
131
- }
132
- async function applyArchivePlan(planPath, yes, json) {
133
- const cfg = await readConfig();
134
- if (!cfg)
135
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
136
- const raw = JSON.parse(await readFile(planPath, "utf8"));
137
- const slugs = archiveSlugsFromPlan(raw);
138
- const chunks = [];
139
- for (let i = 0; i < slugs.length; i += ARCHIVE_CHUNK_SIZE)
140
- chunks.push(slugs.slice(i, i + ARCHIVE_CHUNK_SIZE));
141
- const responses = [];
142
- for (const chunk of chunks) {
143
- responses.push(await postJson(`${resolveApiUrl(cfg)}/api/v1/me/skills/archive`, yes ? "archive skills" : "preview skill archive", cfg.accessToken, { slugs: chunk, dry_run: !yes }));
144
- }
145
- const payload = {
146
- dry_run: responses.every((response) => response.dry_run),
147
- requested: responses.reduce((sum, response) => sum + response.requested, 0),
148
- matched: responses.flatMap((response) => response.matched),
149
- missing: responses.flatMap((response) => response.missing),
150
- ...(yes ? { archived: responses.flatMap((response) => response.archived ?? []) } : {}),
151
- };
152
- if (json) {
153
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
154
- return;
155
- }
156
- const action = payload.dry_run ? "Archive preview" : "Archive applied";
157
- process.stdout.write(`\n${symbols.ok} ${action}: ${payload.matched.length}/${payload.requested} matched\n`);
158
- if (payload.archived)
159
- process.stdout.write(` archived: ${payload.archived.length}\n`);
160
- if (payload.missing.length > 0)
161
- process.stdout.write(` missing or not owned: ${payload.missing.join(", ")}\n`);
162
- if (payload.dry_run)
163
- process.stdout.write(` ${c.dim("Re-run with --yes to archive matched skills.")}\n`);
164
- process.stdout.write("\n");
165
- }
166
- function duplicateGroups(skills) {
167
- const byHash = new Map();
168
- for (const skill of skills) {
169
- const hash = contentHash(skill);
170
- if (!hash)
171
- continue;
172
- const group = byHash.get(hash) ?? [];
173
- group.push(skill);
174
- byHash.set(hash, group);
175
- }
176
- const out = new Map();
177
- let i = 1;
178
- for (const [hash, group] of byHash.entries()) {
179
- if (group.length < 2)
180
- continue;
181
- const id = `dup_${i}`;
182
- i += 1;
183
- for (const skill of group)
184
- out.set(`${hash}\0${skill.slug}`, id);
185
- }
186
- return out;
187
- }
188
- function summarize(skills, findings) {
189
- return {
190
- total: skills.length,
191
- findings: findings.length,
192
- critical: findings.filter((f) => f.risk === "critical").length,
193
- high: findings.filter((f) => f.risk === "high").length,
194
- medium: findings.filter((f) => f.risk === "medium").length,
195
- low: findings.filter((f) => f.risk === "low").length,
196
- blank_titles: skills.filter((s) => !normalized(s.title)).length,
197
- blank_descriptions: skills.filter((s) => !normalized(s.description)).length,
198
- duplicate_hash_groups: new Set(findings.map((f) => f.duplicate_group).filter(Boolean)).size,
199
- };
200
- }
201
- export async function auditSkills(opts) {
202
- if (opts.archivePlan) {
203
- await applyArchivePlan(opts.archivePlan, opts.yes, opts.json);
204
- return;
205
- }
206
- const spinner = opts.json || opts.fixPlan ? null : ora({ text: c.dim("Auditing skills..."), color: "yellow" }).start();
207
- let skills;
208
- try {
209
- skills = await loadOwnedSkills();
210
- }
211
- finally {
212
- spinner?.stop();
213
- }
214
- const duplicates = duplicateGroups(skills);
215
- const findings = skills
216
- .map((skill) => scoreSkill(skill, contentHash(skill) ? duplicates.get(`${contentHash(skill)}\0${skill.slug}`) : undefined))
217
- .filter((finding) => finding !== null)
218
- .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
219
- const summary = summarize(skills, findings);
220
- const archivePlan = findings
221
- .filter((finding) => finding.recommended_action === "archive")
222
- .map((finding) => ({ slug: finding.slug, reasons: finding.reasons, score: finding.score }));
223
- if (opts.json || opts.fixPlan) {
224
- process.stdout.write(`${JSON.stringify({ summary, findings, ...(opts.fixPlan ? { archive_plan: archivePlan } : {}) }, null, 2)}\n`);
225
- return;
226
- }
227
- process.stdout.write(`\n${symbols.dot} ${c.bold("Skill audit")} ${c.dim(`(${summary.total} owned skills)`)}\n\n`);
228
- process.stdout.write(` findings: ${summary.findings} critical: ${summary.critical} high: ${summary.high} medium: ${summary.medium} low: ${summary.low}\n`);
229
- process.stdout.write(` blank titles: ${summary.blank_titles} blank descriptions: ${summary.blank_descriptions} duplicate groups: ${summary.duplicate_hash_groups}\n\n`);
230
- for (const finding of findings.slice(0, 25)) {
231
- process.stdout.write(` ${c.cyan(finding.slug.padEnd(14))} ${finding.risk.padEnd(8)} ${String(finding.score).padStart(3)} ${finding.reasons.join(", ")}\n`);
232
- }
233
- if (findings.length > 25)
234
- process.stdout.write(` ${c.dim(`... ${findings.length - 25} more. Run with --json for full output.`)}\n`);
235
- process.stdout.write("\n");
236
- }