@floomhq/floom 1.0.7 → 1.0.9

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/delete.js CHANGED
@@ -31,10 +31,10 @@ async function confirm(question) {
31
31
  export async function deleteSkill(opts) {
32
32
  const slug = slugFromInput(opts.slug);
33
33
  if (!slug)
34
- throw new FloomError("Missing skill slug.", "Try: `floom delete <slug>`");
34
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom delete <slug>`");
35
35
  const cfg = await readConfig();
36
36
  if (!cfg)
37
- throw new FloomError("Not signed in.", "Run `floom login` first.");
37
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
38
38
  if (!opts.yes) {
39
39
  process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
40
40
  const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
package/dist/doctor.js CHANGED
@@ -32,7 +32,7 @@ async function checkAuth() {
32
32
  name: "Auth",
33
33
  status: "fail",
34
34
  detail: "Token rejected (401).",
35
- hint: "Run `floom logout && floom login` to refresh.",
35
+ hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
36
36
  };
37
37
  }
38
38
  if (!res.ok) {
@@ -88,7 +88,7 @@ async function checkMcp() {
88
88
  return {
89
89
  name: "MCP",
90
90
  status: "ok",
91
- detail: "Optional MCP not registered. `floom add` still writes local skill files.",
91
+ detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
92
92
  };
93
93
  }
94
94
  return {
@@ -130,7 +130,7 @@ async function checkTargetDir() {
130
130
  name: "Target dir",
131
131
  status: "warn",
132
132
  detail: `${dir} does not exist yet.`,
133
- hint: "It will be created on first `floom add`.",
133
+ hint: "It will be created on first `npx -y @floomhq/floom add`.",
134
134
  };
135
135
  }
136
136
  throw err;
@@ -146,7 +146,7 @@ async function checkLastSync() {
146
146
  name: "Last sync",
147
147
  status: "warn",
148
148
  detail: "No synced skills found yet.",
149
- hint: "Run `floom add <link>` to install your first skill.",
149
+ hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
150
150
  };
151
151
  }
152
152
  // Find most recently modified entry
@@ -239,8 +239,7 @@ async function checkVersion() {
239
239
  };
240
240
  }
241
241
  }
242
- export async function doctor() {
243
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
242
+ export async function doctor(opts = {}) {
244
243
  const checks = await Promise.all([
245
244
  checkAuth(),
246
245
  checkMcp(),
@@ -248,6 +247,22 @@ export async function doctor() {
248
247
  checkLastSync(),
249
248
  checkVersion(),
250
249
  ]);
250
+ const anyFail = checks.some((c) => c.status === "fail");
251
+ const anyWarn = checks.some((c) => c.status === "warn");
252
+ const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
253
+ if (opts.json) {
254
+ process.stdout.write(`${JSON.stringify({
255
+ ok: !anyFail,
256
+ status,
257
+ version: CLI_VERSION,
258
+ config_path: CONFIG_PATH,
259
+ checks,
260
+ }, null, 2)}\n`);
261
+ if (anyFail)
262
+ process.exit(1);
263
+ return;
264
+ }
265
+ process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
251
266
  // Compute column widths for clean table output.
252
267
  const nameW = Math.max(...checks.map((c) => c.name.length), 6);
253
268
  for (const check of checks) {
@@ -257,8 +272,6 @@ export async function doctor() {
257
272
  process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
258
273
  }
259
274
  }
260
- const anyFail = checks.some((c) => c.status === "fail");
261
- const anyWarn = checks.some((c) => c.status === "warn");
262
275
  process.stdout.write("\n");
263
276
  if (anyFail) {
264
277
  process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
package/dist/errors.js CHANGED
@@ -14,7 +14,7 @@ export class FloomError extends Error {
14
14
  }
15
15
  export function friendlyHttp(status, action) {
16
16
  if (status === 401) {
17
- return new FloomError("Your token expired.", "Run `floom login` to refresh.");
17
+ return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
18
18
  }
19
19
  if (status === 403) {
20
20
  return new FloomError(`You don't have permission to ${action}.`);
@@ -23,7 +23,7 @@ export function friendlyHttp(status, action) {
23
23
  if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
24
24
  return new FloomError("Skill not found.", "Check the link or slug, then try again.");
25
25
  }
26
- return new FloomError("Skill not found.", "Run `floom publish` without `--update` to create a new one.");
26
+ return new FloomError("Skill not found.", "Publish as a new skill for now. Publisher-side version updates are planned for a later release.");
27
27
  }
28
28
  if (status === 413) {
29
29
  return new FloomError("That file is too large to publish.");
package/dist/info.js CHANGED
@@ -19,7 +19,7 @@ function slugFromInput(input) {
19
19
  export async function info(opts) {
20
20
  const slug = slugFromInput(opts.slug);
21
21
  if (!slug)
22
- throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
22
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
23
23
  if (!SLUG_RE.test(slug)) {
24
24
  throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
25
25
  }
@@ -28,7 +28,7 @@ export async function info(opts) {
28
28
  const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
29
29
  let detail;
30
30
  try {
31
- detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "load skill metadata", cfg?.accessToken);
31
+ detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "info skill metadata", cfg?.accessToken);
32
32
  }
33
33
  finally {
34
34
  spinner?.stop();
package/dist/init.js CHANGED
@@ -4,7 +4,8 @@ import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  import { c, symbols } from "./ui.js";
6
6
  import { FloomError } from "./errors.js";
7
- const TEMPLATE = `---
7
+ const TEMPLATES = {
8
+ generic: `---
8
9
  title:
9
10
  description:
10
11
  version: 1.0
@@ -26,10 +27,156 @@ version: 1.0
26
27
 
27
28
 
28
29
  # Examples
29
- `;
30
- export async function init(filename) {
30
+ `,
31
+ "brand-voice": `---
32
+ title: Brand voice
33
+ description: Help an agent write in our company voice.
34
+ type: knowledge
35
+ installs_as: memory
36
+ version: 1.0
37
+ ---
38
+
39
+ # Brand Voice
40
+
41
+ ## Use when
42
+ - Writing customer-facing copy
43
+ - Rewriting drafts to match our tone
44
+ - Reviewing messaging before it ships
45
+
46
+ ## Voice rules
47
+ - Sound clear, direct, and useful.
48
+ - Prefer concrete nouns and short sentences.
49
+ - Avoid hype, filler, and generic AI language.
50
+
51
+ ## Words we use
52
+ - Replace this list with approved terms.
53
+
54
+ ## Words we avoid
55
+ - Replace this list with banned or overused terms.
56
+
57
+ ## Examples
58
+ - Before:
59
+ - After:
60
+ `,
61
+ "pr-review": `---
62
+ title: PR review
63
+ description: Review code changes with risk-first feedback.
64
+ type: workflow
65
+ installs_as: claude_skill
66
+ version: 1.0
67
+ ---
68
+
69
+ # PR Review
70
+
71
+ ## Use when
72
+ - Reviewing a pull request or diff
73
+ - Checking a change before merge
74
+
75
+ ## Review order
76
+ 1. Correctness and regressions
77
+ 2. Security and data safety
78
+ 3. Tests and missing edge cases
79
+ 4. Maintainability
80
+
81
+ ## Output
82
+ - Lead with findings.
83
+ - Include file paths and line references.
84
+ - Keep style comments out unless they affect behavior.
85
+ - If no issues are found, say that clearly and name any test gaps.
86
+ `,
87
+ sales: `---
88
+ title: Sales research
89
+ description: Prepare concise account research and outreach context.
90
+ type: workflow
91
+ installs_as: memory
92
+ version: 1.0
93
+ ---
94
+
95
+ # Sales Research
96
+
97
+ ## Use when
98
+ - Preparing for a prospect call
99
+ - Writing a relevant outbound message
100
+ - Summarizing account context for the team
101
+
102
+ ## Gather
103
+ - Company
104
+ - Buyer persona
105
+ - Recent trigger
106
+ - Likely pain
107
+ - Existing tools or workflow
108
+
109
+ ## Output
110
+ - 5 bullet account summary
111
+ - 3 likely pain points
112
+ - 2 tailored opener angles
113
+ - 1 clear next action
114
+ `,
115
+ support: `---
116
+ title: Support tone
117
+ description: Answer support tickets with a clear and calm company voice.
118
+ type: instruction
119
+ installs_as: memory
120
+ version: 1.0
121
+ ---
122
+
123
+ # Support Tone
124
+
125
+ ## Use when
126
+ - Replying to customer support messages
127
+ - Summarizing customer issues
128
+ - Drafting escalation notes
129
+
130
+ ## Rules
131
+ - Acknowledge the issue in plain language.
132
+ - Give the next concrete step.
133
+ - Do not over-apologize.
134
+ - Do not invent product behavior.
135
+ - Escalate when data, billing, or account access is involved.
136
+
137
+ ## Reply shape
138
+ 1. Short acknowledgement
139
+ 2. Direct answer or next step
140
+ 3. What happens next
141
+ `,
142
+ onboarding: `---
143
+ title: Team onboarding
144
+ description: Help a new teammate understand how this team works.
145
+ type: knowledge
146
+ installs_as: memory
147
+ version: 1.0
148
+ ---
149
+
150
+ # Team Onboarding
151
+
152
+ ## Use when
153
+ - A new teammate asks how work gets done
154
+ - An agent needs company or team context
155
+ - Creating first-week task plans
156
+
157
+ ## Team context
158
+ - Mission:
159
+ - Customers:
160
+ - Current priorities:
161
+ - Tools:
162
+
163
+ ## How we work
164
+ - Decision rules:
165
+ - Review process:
166
+ - Communication norms:
167
+ - Definition of done:
168
+
169
+ ## First-week checklist
170
+ - Read:
171
+ - Set up:
172
+ - Ask:
173
+ - Ship:
174
+ `,
175
+ };
176
+ export async function init(filename, template = "generic") {
31
177
  const target = filename ?? "skill.md";
32
178
  const filePath = resolve(process.cwd(), target);
179
+ const body = TEMPLATES[template];
33
180
  const exists = await fileExists(filePath);
34
181
  if (exists) {
35
182
  if (!process.stdin.isTTY) {
@@ -44,7 +191,7 @@ export async function init(filename) {
44
191
  }
45
192
  }
46
193
  try {
47
- await writeFile(filePath, TEMPLATE, "utf8");
194
+ await writeFile(filePath, body, "utf8");
48
195
  }
49
196
  catch (err) {
50
197
  const code = err.code;
@@ -57,10 +204,11 @@ export async function init(filename) {
57
204
  throw new FloomError(`Couldn't create ${target}: ${err.message}`);
58
205
  }
59
206
  process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
207
+ process.stdout.write(` ${c.dim(`Template: ${template}`)}\n`);
60
208
  process.stdout.write(`\n ${c.bold("Next")}\n`);
61
209
  process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
62
- process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`floom scan ${shellQuote(target)}`)}\n`);
63
- process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`floom publish ${shellQuote(target)} --type instruction --public`)}\n\n`);
210
+ process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`npx -y @floomhq/floom scan ${shellQuote(target)}`)}\n`);
211
+ process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`npx -y @floomhq/floom publish ${shellQuote(target)} --public`)}\n\n`);
64
212
  }
65
213
  function shellQuote(value) {
66
214
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
package/dist/install.js CHANGED
@@ -58,7 +58,7 @@ async function localHash(path) {
58
58
  if (code === "ENOENT")
59
59
  return null;
60
60
  if (code === "ELOOP")
61
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
61
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
62
62
  if (code === "ENOTDIR" || code === "EISDIR") {
63
63
  throw new FloomError("Local path is blocked by an existing file or directory.");
64
64
  }
@@ -144,7 +144,7 @@ async function ensureSafeParentDirectory(root, target) {
144
144
  async function assertSafeDirectory(path) {
145
145
  const stat = await lstat(path);
146
146
  if (stat.isSymbolicLink()) {
147
- throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `floom add` again.");
147
+ throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
148
148
  }
149
149
  if (!stat.isDirectory()) {
150
150
  throw new FloomError("Local path is blocked by an existing file or directory.");
@@ -159,7 +159,7 @@ export async function install(slugInput, opts = {}) {
159
159
  }
160
160
  const cfg = await readConfig();
161
161
  const apiUrl = resolveApiUrl(cfg);
162
- const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
162
+ const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
163
163
  let detail;
164
164
  try {
165
165
  detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
@@ -168,7 +168,7 @@ export async function install(slugInput, opts = {}) {
168
168
  }
169
169
  }
170
170
  catch (err) {
171
- spinner.stop();
171
+ spinner?.stop();
172
172
  throw err;
173
173
  }
174
174
  try {
@@ -194,7 +194,7 @@ export async function install(slugInput, opts = {}) {
194
194
  catch (err) {
195
195
  const code = err.code;
196
196
  if (code === "ELOOP")
197
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
197
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
198
198
  throw err;
199
199
  }
200
200
  action = "updated";
@@ -212,16 +212,28 @@ export async function install(slugInput, opts = {}) {
212
212
  throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
213
213
  }
214
214
  if (code === "ELOOP") {
215
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
215
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
216
216
  }
217
217
  if (code === "ENOENT") {
218
- throw new FloomError("Local path changed during install.", "Run `floom add` again.");
218
+ throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
219
219
  }
220
220
  throw err;
221
221
  }
222
222
  action = "installed";
223
223
  }
224
- spinner.stop();
224
+ const result = {
225
+ slug,
226
+ title: detail.title,
227
+ action,
228
+ target: targetAgent,
229
+ path: target,
230
+ content_hash: remoteHash,
231
+ };
232
+ spinner?.stop();
233
+ if (opts.json) {
234
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
235
+ return;
236
+ }
225
237
  process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
226
238
  process.stdout.write(` ${c.dim(target)}\n\n`);
227
239
  process.stdout.write(` ${c.bold("Next")}\n`);
package/dist/library.js CHANGED
@@ -35,7 +35,7 @@ export async function libraryList(opts = {}) {
35
35
  export async function libraryCreate(opts) {
36
36
  const cfg = await readConfig();
37
37
  if (!cfg)
38
- throw new FloomError("Not signed in.", "Run `floom login` first.");
38
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
39
39
  const apiUrl = resolveApiUrl(cfg);
40
40
  const result = await postJson(`${apiUrl}/api/v1/libraries`, "create library", cfg.accessToken, {
41
41
  slug: opts.slug,
@@ -45,12 +45,12 @@ export async function libraryCreate(opts) {
45
45
  });
46
46
  process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
47
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`);
48
+ process.stdout.write(` ${c.dim("Sync:")} npx -y @floomhq/floom library subscribe ${result.slug}\n\n`);
49
49
  }
50
50
  export async function libraryAddSkill(opts) {
51
51
  const cfg = await readConfig();
52
52
  if (!cfg)
53
- throw new FloomError("Not signed in.", "Run `floom login` first.");
53
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
54
54
  const apiUrl = resolveApiUrl(cfg);
55
55
  await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
56
56
  skill_slug: opts.skillSlug,
@@ -66,7 +66,7 @@ export async function libraryAddSkill(opts) {
66
66
  export async function libraryRemoveSkill(librarySlug, skillSlug) {
67
67
  const cfg = await readConfig();
68
68
  if (!cfg)
69
- throw new FloomError("Not signed in.", "Run `floom login` first.");
69
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
70
70
  const apiUrl = resolveApiUrl(cfg);
71
71
  await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
72
72
  process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
@@ -74,7 +74,7 @@ export async function libraryRemoveSkill(librarySlug, skillSlug) {
74
74
  export async function librarySubscribe(slug) {
75
75
  const cfg = await readConfig();
76
76
  if (!cfg)
77
- throw new FloomError("Not signed in.", "Run `floom login` first.");
77
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
78
78
  const apiUrl = resolveApiUrl(cfg);
79
79
  await postJson(`${apiUrl}/api/v1/me/subscriptions`, "subscribe to library", cfg.accessToken, { library_slug: slug });
80
80
  process.stdout.write(`\n${symbols.ok} Subscribed to ${c.cyan(slug)}\n`);
@@ -83,7 +83,7 @@ export async function librarySubscribe(slug) {
83
83
  export async function libraryUnsubscribe(slug) {
84
84
  const cfg = await readConfig();
85
85
  if (!cfg)
86
- throw new FloomError("Not signed in.", "Run `floom login` first.");
86
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
87
87
  const apiUrl = resolveApiUrl(cfg);
88
88
  await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, "unsubscribe from library", cfg.accessToken);
89
89
  process.stdout.write(`\n${symbols.ok} Unsubscribed from ${c.cyan(slug)}\n\n`);
@@ -91,7 +91,7 @@ export async function libraryUnsubscribe(slug) {
91
91
  export async function moveSkill(opts) {
92
92
  const cfg = await readConfig();
93
93
  if (!cfg)
94
- throw new FloomError("Not signed in.", "Run `floom login` first.");
94
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
95
95
  const apiUrl = resolveApiUrl(cfg);
96
96
  await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(opts.slug)}/override`, "set skill override", cfg.accessToken, { folder: opts.folder, tags: opts.tags });
97
97
  const folderText = opts.folder ?? c.dim("(root)");
package/dist/list.js CHANGED
@@ -36,7 +36,7 @@ function formatRelative(iso) {
36
36
  export async function list(opts) {
37
37
  const cfg = await readConfig();
38
38
  if (!cfg) {
39
- throw new FloomError("Not signed in.", "Run `floom login` first.");
39
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
40
40
  }
41
41
  const apiUrl = resolveApiUrl(cfg);
42
42
  const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
@@ -54,7 +54,7 @@ export async function list(opts) {
54
54
  }
55
55
  process.stdout.write(`\n${symbols.dot} ${c.bold("Published")} ${c.dim(`(${published.length})`)}\n\n`);
56
56
  if (published.length === 0) {
57
- process.stdout.write(` ${c.dim("Nothing published yet. Try `floom publish skill.md`.")}\n`);
57
+ process.stdout.write(` ${c.dim("Nothing published yet. Try `npx -y @floomhq/floom publish skill.md`.")}\n`);
58
58
  }
59
59
  else {
60
60
  for (const s of published)
package/dist/login.js CHANGED
@@ -21,7 +21,7 @@ export async function login() {
21
21
  catch (err) {
22
22
  spinner.stop();
23
23
  if (err instanceof Error && /timed out/i.test(err.message)) {
24
- throw new FloomError("No worries — try `floom login` again when ready.");
24
+ throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
25
25
  }
26
26
  throw err;
27
27
  }
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import { c } from "./ui.js";
2
2
  export function printMcpSetup() {
3
3
  const snippet = `## Floom
4
4
  - Use Floom skills from the local Floom skills folder when they match the task.
5
- - To install a shared skill, run \`floom add <slug-or-url> --target claude\` or \`floom add <slug-or-url> --target codex\`.
6
- - To find reusable behavior, run \`floom search <query>\`.
5
+ - To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
6
+ - To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
7
7
  - MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
8
8
  process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
9
9
  process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
package/dist/publish.js CHANGED
@@ -140,7 +140,7 @@ export async function publish(opts) {
140
140
  const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
141
141
  const cfg = await readConfig();
142
142
  if (!cfg) {
143
- throw new FloomError("Not signed in.", "Run `floom login` first.");
143
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
144
144
  }
145
145
  // Later version: detect already-published when --update is missing.
146
146
  // The current API does not return a duplicate-skill code, so we leave
@@ -192,10 +192,15 @@ export async function publish(opts) {
192
192
  spinner.stop();
193
193
  const versionTag = version ? c.dim(` (${formatVersionLabel(version)})`) : "";
194
194
  const titleLabel = data.title ? `"${data.title}"` : opts.file;
195
+ const invitedEmails = opts.sharedWithEmails ?? [];
195
196
  process.stdout.write(`\n${symbols.ok} Published ${c.bold(titleLabel)}${versionTag}\n\n`);
197
+ process.stdout.write(` ${c.bold("Send this to someone:")}\n`);
196
198
  process.stdout.write(` ${c.cyan(humanUrl)}\n\n`);
197
- if (data.visibility === "shared" && data.shared_with_emails?.length) {
198
- process.stdout.write(` ${c.dim(`Shared with ${data.shared_with_emails.join(", ")}`)}\n\n`);
199
+ process.stdout.write(` ${c.bold("They run:")}\n`);
200
+ process.stdout.write(` ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n\n`);
201
+ if (invitedEmails.length) {
202
+ process.stdout.write(` ${c.bold("Email invite:")}\n`);
203
+ process.stdout.write(` ${c.dim(invitedEmails.join(", "))}\n\n`);
199
204
  }
200
205
  let copied = false;
201
206
  try {
@@ -206,14 +211,11 @@ export async function publish(opts) {
206
211
  copied = false;
207
212
  }
208
213
  if (copied) {
209
- process.stdout.write(` ${c.dim("Copied to clipboard. Share it anywhere.")}\n\n`);
214
+ process.stdout.write(` ${c.dim("Copied link to clipboard.")}\n\n`);
210
215
  }
211
216
  else {
212
217
  process.stdout.write(` ${c.dim("Share it anywhere.")}\n\n`);
213
218
  }
214
- process.stdout.write(` ${c.bold("Next")}\n`);
215
- process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
216
- process.stdout.write(` ${c.dim("2.")} Send the link.\n`);
217
- process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
218
- process.stdout.write(` ${c.dim("4.")} Agent reads the installed Markdown from the local skills folder.\n\n`);
219
+ process.stdout.write(` ${c.bold("Agent prompt:")}\n`);
220
+ process.stdout.write(` ${c.cyan("npx -y @floomhq/floom agent-prompt")}\n\n`);
219
221
  }
package/dist/secrets.js CHANGED
@@ -5,15 +5,29 @@ const SECRET_PATTERNS = [
5
5
  { label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{25,}\b/g },
6
6
  { label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g },
7
7
  { label: "GitHub token", regex: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
8
+ { label: "npm token", regex: /\bnpm_[A-Za-z0-9]{30,}\b/g },
8
9
  { label: "Supabase access token", regex: /\bsbp_[A-Za-z0-9]{30,}\b/g },
9
10
  { label: "Stripe secret key", regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
10
11
  { label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
11
12
  { label: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
13
+ { label: "Database URL with password", regex: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]{8,}@[^\s]+/gi },
12
14
  { label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
13
15
  ];
14
16
  const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
15
17
  const PROVIDER_LIKE_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?((?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|sbp_[A-Za-z0-9]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})["']?/gi;
16
- const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|placeholder|replace|changeme|todo|xxx|test|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
18
+ const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|mock|placeholder|replace|changeme|todo|xxx|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
19
+ const PLACEHOLDER_PHRASE_WORDS = new Set([
20
+ "and",
21
+ "fake",
22
+ "is",
23
+ "key",
24
+ "long",
25
+ "looks",
26
+ "real",
27
+ "secret",
28
+ "that",
29
+ "very",
30
+ ]);
17
31
  const PROMPT_INJECTION_PATTERNS = [
18
32
  { label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
19
33
  { label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
@@ -46,6 +60,12 @@ function pushFinding(findings, seen, label, line, value) {
46
60
  seen.add(key);
47
61
  findings.push({ label, line, preview: redact(value) });
48
62
  }
63
+ function isPlaceholderValue(value) {
64
+ if (PLACEHOLDER_RE.test(value))
65
+ return true;
66
+ const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
67
+ return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
68
+ }
49
69
  export function detectSecrets(input) {
50
70
  const findings = [];
51
71
  const seen = new Set();
@@ -59,14 +79,14 @@ export function detectSecrets(input) {
59
79
  GENERIC_ASSIGNMENT_RE.lastIndex = 0;
60
80
  for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
61
81
  const value = match[1] ?? "";
62
- if (!value || PLACEHOLDER_RE.test(value))
82
+ if (!value || isPlaceholderValue(value))
63
83
  continue;
64
84
  pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
65
85
  }
66
86
  PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
67
87
  for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
68
88
  const value = match[1] ?? "";
69
- if (!value)
89
+ if (!value || isPlaceholderValue(value))
70
90
  continue;
71
91
  pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
72
92
  }
package/dist/setup.js CHANGED
@@ -13,16 +13,16 @@ const TARGETS = {
13
13
  };
14
14
  function floomAgentInstructions(target) {
15
15
  const addCommand = target === "codex"
16
- ? "floom add <slug-or-url> --target codex"
17
- : "floom add <slug-or-url> --target claude";
16
+ ? "npx -y @floomhq/floom add <slug-or-url> --target codex"
17
+ : "npx -y @floomhq/floom add <slug-or-url> --target claude";
18
18
  return `${START_MARKER}
19
19
  ## Floom
20
20
 
21
21
  - Before recreating agent behavior from scratch, check Floom for reusable skills.
22
- - Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
22
+ - Search or inspect skills with \`npx -y @floomhq/floom search <query>\`, \`npx -y @floomhq/floom info <slug-or-url>\`, and \`npx -y @floomhq/floom list\`.
23
23
  - Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
24
24
  - Use installed Markdown skills from the local skills folder when they match the task.
25
- - \`floom sync\`, \`floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
25
+ - \`npx -y @floomhq/floom sync\`, \`npx -y @floomhq/floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
26
26
  ${END_MARKER}`;
27
27
  }
28
28
  async function fileExists(path) {
@@ -96,7 +96,7 @@ async function detectTarget(opts) {
96
96
  return { agent: "claude", label: TARGETS.claude.label, path: claude };
97
97
  if (codex)
98
98
  return { agent: "codex", label: TARGETS.codex.label, path: codex };
99
- throw new FloomError("No agent instruction file found.", "Run `floom setup --target claude --yes` or `floom setup --target codex --yes` from the repo root.");
99
+ throw new FloomError("No agent instruction file found.", "Run `npx -y @floomhq/floom setup --target claude --yes` or `npx -y @floomhq/floom setup --target codex --yes` from the repo root.");
100
100
  }
101
101
  function renderPreview(target, existing) {
102
102
  const action = existing === null ? "create" : "append";
@@ -108,7 +108,7 @@ function renderPreview(target, existing) {
108
108
  "",
109
109
  floomAgentInstructions(target.agent),
110
110
  "",
111
- `${c.dim("MCP setup guidance:")} run ${c.cyan("floom mcp")} to print local agent commands.`,
111
+ `${c.dim("MCP setup guidance:")} run ${c.cyan("npx -y @floomhq/floom mcp")} to print local agent commands.`,
112
112
  "",
113
113
  ].join("\n");
114
114
  }
@@ -153,7 +153,7 @@ export async function setupAgent(opts) {
153
153
  if (existing === null) {
154
154
  await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
155
155
  if (err instanceof Error && "code" in err && err.code === "EEXIST") {
156
- throw new FloomError("Instruction file appeared while setup was running.", "Re-run `floom setup` so Floom can inspect the current file before writing.");
156
+ throw new FloomError("Instruction file appeared while setup was running.", "Re-run `npx -y @floomhq/floom setup` so Floom can inspect the current file before writing.");
157
157
  }
158
158
  throw err;
159
159
  });
@@ -162,5 +162,5 @@ export async function setupAgent(opts) {
162
162
  await writeFile(target.path, next, "utf8");
163
163
  }
164
164
  process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
165
- process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("floom mcp")}\n\n`);
165
+ process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("npx -y @floomhq/floom mcp")}\n\n`);
166
166
  }