@floomhq/floom 1.0.6 → 1.0.7

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/README.md CHANGED
@@ -3,28 +3,29 @@
3
3
  Publish AI skills from your terminal. Share them with a link. Add other people's skills with one command.
4
4
 
5
5
  ```bash
6
- npm install -g @floomhq/floom
7
- floom init my-skill.md
8
- floom login
9
- floom publish my-skill.md
10
- floom share my-skill --add teammate@example.com
11
- floom search review
12
- floom add awesome-skill
13
- floom setup --target claude --dry-run
14
- floom list
15
- floom library list
6
+ npx -y @floomhq/floom init my-skill.md
7
+ npx -y @floomhq/floom login
8
+ npx -y @floomhq/floom publish my-skill.md
9
+ npx -y @floomhq/floom share my-skill --add teammate@example.com
10
+ npx -y @floomhq/floom search review
11
+ npx -y @floomhq/floom add awesome-skill --setup
12
+ npx -y @floomhq/floom setup --target claude --dry-run
13
+ npx -y @floomhq/floom list
14
+ npx -y @floomhq/floom library list
16
15
  ```
17
16
 
18
17
  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.
19
18
 
19
+ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose `floom-skills` to avoid colliding with any older local Floom runtime CLI.
20
+
20
21
  ## Commands
21
22
 
22
- - `floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
23
+ - `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
23
24
  - `floom init [file.md]` — create a starter skill file.
24
- - `floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--version <label>`.
25
+ - `npx -y @floomhq/floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--skill-version <label>`.
25
26
  - `floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
26
27
  - `floom list` — show your published skills. Optional `--json`.
27
- - `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
28
+ - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`. Optional `--setup` connects Claude Code and `--force` replaces an existing local copy.
28
29
  - `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
29
30
  - `floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
30
31
  - `floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
@@ -38,7 +39,7 @@ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the UR
38
39
  - `floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
39
40
  - `floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
40
41
  - `floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
41
- - `floom doctor` — diagnose your Floom setup.
42
+ - `npx -y @floomhq/floom doctor` — diagnose your Floom setup.
42
43
  - `floom whoami` — show the signed-in account.
43
44
  - `floom logout` — delete local credentials.
44
45
 
@@ -64,5 +65,5 @@ Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
64
65
  `floom sync` and `floom watch` are Version 1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
65
66
  The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
66
67
  only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
67
- conflicts. Symlinks are never followed. To accept the Floom version, move or delete the local file
68
- and run `floom sync` again.
68
+ conflicts. Symlinks are never followed. To replace a local skill manually, run
69
+ `npx -y @floomhq/floom add <url-or-slug> --force`.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { login } from "./login.js";
4
4
  import { publish } from "./publish.js";
5
5
  import { whoami } from "./whoami.js";
6
6
  import { init } from "./init.js";
7
- import { deleteConfig } from "./config.js";
7
+ import { deleteConfig, readConfig } from "./config.js";
8
8
  import { list } from "./list.js";
9
9
  import { install } from "./install.js";
10
10
  import { info } from "./info.js";
@@ -63,13 +63,14 @@ function usage() {
63
63
  }
64
64
  function commandUsage() {
65
65
  const out = `
66
- ${c.bold("Usage:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
66
+ ${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [flags]")}
67
+ ${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [flags]")}
67
68
 
68
69
  ${c.bold("Commands")}
69
70
  ${c.dim("Skills")}
70
71
  ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
71
72
  ${c.dim("Alias: install")}
72
- ${c.dim("Flags: --target claude|codex (default: claude), --setup")}
73
+ ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
73
74
  ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
74
75
  ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
75
76
 
@@ -105,16 +106,16 @@ function commandUsage() {
105
106
  ${c.cyan("watch")} Preview polling sync loop
106
107
 
107
108
  ${c.bold("Examples")}
108
- ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
109
- ${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
110
- ${c.cyan("floom setup")} ${c.dim("--target claude --yes")}
109
+ ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
110
+ ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
111
+ ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
111
112
 
112
113
  ${c.bold("Help")}
113
- ${c.cyan("floom commands")} Show this reference
114
+ ${c.cyan("npx -y @floomhq/floom commands")} Show this reference
114
115
  ${c.cyan("--help")} Show this reference
115
116
  ${c.cyan("--version")} Show CLI version
116
117
 
117
- ${c.dim("Run")} ${c.cyan("floom")} ${c.dim("with no command for the guided start screen.")}
118
+ ${c.dim("Run")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("with no command for the guided start screen.")}
118
119
  `;
119
120
  process.stdout.write(out);
120
121
  }
@@ -173,15 +174,17 @@ function parseFlags(argv) {
173
174
  out.installsAs = value;
174
175
  i = nextIndex;
175
176
  }
176
- else if (a === "--skill-version" || a.startsWith("--skill-version=") || a === "--version" || a.startsWith("--version=")) {
177
- const flagName = a.startsWith("--skill-version") ? "--skill-version" : "--version";
178
- const { value, nextIndex } = readFlagValue(argv, i, flagName);
177
+ else if (a === "--skill-version" || a.startsWith("--skill-version=")) {
178
+ const { value, nextIndex } = readFlagValue(argv, i, "--skill-version");
179
179
  if (!VERSION_RE.test(value)) {
180
- throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
180
+ throw new FloomError(`Invalid --skill-version: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
181
181
  }
182
182
  out.version = value;
183
183
  i = nextIndex;
184
184
  }
185
+ else if (a === "--version" || a.startsWith("--version=")) {
186
+ throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
187
+ }
185
188
  else if (a.startsWith("--")) {
186
189
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
187
190
  }
@@ -272,6 +275,7 @@ function parseAddArgs(argv) {
272
275
  let slug;
273
276
  let target;
274
277
  let setup = false;
278
+ let force = false;
275
279
  for (let i = 0; i < argv.length; i++) {
276
280
  const a = argv[i] ?? "";
277
281
  if (a === "--target" || a.startsWith("--target=")) {
@@ -285,6 +289,9 @@ function parseAddArgs(argv) {
285
289
  else if (a === "--setup") {
286
290
  setup = true;
287
291
  }
292
+ else if (a === "--force") {
293
+ force = true;
294
+ }
288
295
  else if (a.startsWith("--")) {
289
296
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --setup`.");
290
297
  }
@@ -297,7 +304,7 @@ function parseAddArgs(argv) {
297
304
  if (!slug) {
298
305
  throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --setup`");
299
306
  }
300
- return target ? { slug, target, setup } : { slug, setup };
307
+ return target ? { slug, target, setup, force } : { slug, setup, force };
301
308
  }
302
309
  function parseSearchFlags(argv) {
303
310
  const out = { json: false };
@@ -571,6 +578,10 @@ function sleep(ms, signal) {
571
578
  });
572
579
  }
573
580
  async function watch(intervalSeconds) {
581
+ const cfg = await readConfig();
582
+ if (!cfg) {
583
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` before watch, or use `npx -y @floomhq/floom add <link>` without an account.");
584
+ }
574
585
  const controller = new AbortController();
575
586
  let stopping = false;
576
587
  const stop = () => {
@@ -712,6 +723,7 @@ async function main() {
712
723
  await install(flags.slug, {
713
724
  ...(flags.target ? { target: flags.target } : {}),
714
725
  setup: flags.setup,
726
+ force: flags.force,
715
727
  });
716
728
  if (flags.setup) {
717
729
  await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
@@ -771,7 +783,7 @@ async function main() {
771
783
  }
772
784
  }
773
785
  catch (e) {
774
- printError(e);
786
+ printError(e, { json: rest.includes("--json") });
775
787
  process.exit(1);
776
788
  }
777
789
  }
package/dist/errors.js CHANGED
@@ -20,6 +20,9 @@ export function friendlyHttp(status, action) {
20
20
  return new FloomError(`You don't have permission to ${action}.`);
21
21
  }
22
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
+ }
23
26
  return new FloomError("Skill not found.", "Run `floom publish` without `--update` to create a new one.");
24
27
  }
25
28
  if (status === 413) {
@@ -37,7 +40,14 @@ export function friendlyNetwork(err) {
37
40
  }
38
41
  return new FloomError(msg);
39
42
  }
40
- export function printError(err) {
43
+ export function printError(err, opts = {}) {
44
+ if (opts.json) {
45
+ const error = err instanceof FloomError
46
+ ? { error: err.message, hint: err.hint ?? null }
47
+ : { error: err instanceof Error ? err.message : String(err), hint: null };
48
+ process.stderr.write(`${JSON.stringify(error)}\n`);
49
+ return;
50
+ }
41
51
  if (err instanceof FloomError) {
42
52
  process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
43
53
  if (err.hint) {
package/dist/install.js CHANGED
@@ -77,6 +77,18 @@ async function writeInstallFile(root, target, body) {
77
77
  await parent.close();
78
78
  }
79
79
  }
80
+ async function overwriteInstallFile(target, body) {
81
+ const handle = await open(target, constants.O_WRONLY | constants.O_TRUNC | constants.O_NOFOLLOW);
82
+ try {
83
+ const stat = await handle.stat();
84
+ if (!stat.isFile())
85
+ throw new FloomError("Local path is blocked by an existing file or directory.");
86
+ await writeAll(handle, body);
87
+ }
88
+ finally {
89
+ await handle.close();
90
+ }
91
+ }
80
92
  async function openSafeParentDirectory(root, target) {
81
93
  await ensureSafeParentDirectory(root, target);
82
94
  return open(dirname(target), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
@@ -175,8 +187,20 @@ export async function install(slugInput, opts = {}) {
175
187
  if (existing === remoteHash) {
176
188
  action = "unchanged";
177
189
  }
190
+ else if (existing !== null && opts.force) {
191
+ try {
192
+ await overwriteInstallFile(target, detail.body_md);
193
+ }
194
+ catch (err) {
195
+ const code = err.code;
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.");
198
+ throw err;
199
+ }
200
+ action = "updated";
201
+ }
178
202
  else if (existing !== null) {
179
- throw new FloomError("Local skill already exists with different content.", "Move or delete the local file, then run `floom add` again.");
203
+ 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.");
180
204
  }
181
205
  else {
182
206
  try {
@@ -185,7 +209,7 @@ export async function install(slugInput, opts = {}) {
185
209
  catch (err) {
186
210
  const code = err.code;
187
211
  if (code === "EEXIST") {
188
- throw new FloomError("Local skill already exists with different content.", "Move or delete the local file, then run `floom add` again.");
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.");
189
213
  }
190
214
  if (code === "ELOOP") {
191
215
  throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
package/dist/login.js CHANGED
@@ -8,7 +8,6 @@ const DEFAULT_PORT = 7456;
8
8
  const TIMEOUT_MS = 5 * 60 * 1000;
9
9
  export async function login() {
10
10
  const apiUrl = getApiUrl();
11
- const port = await pickPort();
12
11
  process.stdout.write(header());
13
12
  process.stdout.write(`${symbols.arrow} Opening browser to sign in with Google...\n\n`);
14
13
  const spinner = ora({
@@ -17,7 +16,7 @@ export async function login() {
17
16
  }).start();
18
17
  let tokens;
19
18
  try {
20
- tokens = await waitForCallback(port);
19
+ tokens = await waitForCallback();
21
20
  }
22
21
  catch (err) {
23
22
  spinner.stop();
@@ -57,16 +56,11 @@ export async function login() {
57
56
  process.stdout.write(`${symbols.ok} Signed in as ${c.bold(me.email ?? me.id)}\n`);
58
57
  process.stdout.write(` ${c.dim("Your token is saved at ~/.floom/config.json")}\n\n`);
59
58
  }
60
- /** Reserve a free port. Defaults to 7456 (must be in Supabase uri_allow_list). */
61
- async function pickPort() {
62
- // Supabase uri_allow_list whitelists 7456 explicitly. If it's busy, we fail
63
- // loudly rather than silently using a port that won't be allowed.
64
- return DEFAULT_PORT;
65
- }
66
- function waitForCallback(port) {
59
+ function waitForCallback() {
67
60
  return new Promise((resolve, reject) => {
68
61
  const apiUrl = getApiUrl();
69
62
  let settled = false;
63
+ let retriedEphemeralPort = false;
70
64
  const server = createServer((req, res) => {
71
65
  // CORS preflight from the browser bridge page.
72
66
  const origin = req.headers.origin ?? "*";
@@ -128,13 +122,27 @@ function waitForCallback(port) {
128
122
  server.close();
129
123
  }
130
124
  server.on("error", (err) => {
125
+ const code = err.code;
126
+ if (!settled && !retriedEphemeralPort && code === "EADDRINUSE") {
127
+ retriedEphemeralPort = true;
128
+ server.listen(0, "127.0.0.1");
129
+ return;
130
+ }
131
131
  if (settled)
132
132
  return;
133
133
  settled = true;
134
134
  clearTimeout(timer);
135
- reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
135
+ reject(new FloomError("Local auth server failed.", err.message));
136
136
  });
137
- server.listen(port, "127.0.0.1", () => {
137
+ server.listen(DEFAULT_PORT, "127.0.0.1", () => {
138
+ const address = server.address();
139
+ if (!address || typeof address === "string") {
140
+ settled = true;
141
+ cleanup();
142
+ reject(new FloomError("Could not reserve a local sign-in port."));
143
+ return;
144
+ }
145
+ const port = address.port;
138
146
  const target = `${apiUrl}/auth/cli?port=${port}`;
139
147
  open(target).catch((e) => {
140
148
  const msg = e instanceof Error ? e.message : String(e);
package/dist/publish.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
3
4
  import ora from "ora";
4
5
  import clipboard from "clipboardy";
5
6
  import { getWebUrl, readConfig, resolveApiUrl } from "./config.js";
@@ -34,21 +35,27 @@ function parseFrontmatter(input) {
34
35
  const headerBlock = trimmed.slice(3, end).trim();
35
36
  const rest = trimmed.slice(end + 4).replace(/^\r?\n/, "");
36
37
  const meta = {};
37
- const lines = headerBlock.split(/\r?\n/);
38
- for (let i = 0; i < lines.length; i++) {
39
- const rawLine = lines[i] ?? "";
40
- const line = rawLine.trim();
41
- if (!line || line.startsWith("#"))
38
+ let parsed;
39
+ try {
40
+ parsed = headerBlock ? parseYaml(headerBlock) : {};
41
+ }
42
+ catch (err) {
43
+ return {
44
+ meta,
45
+ body: rest,
46
+ error: {
47
+ message: err instanceof Error ? err.message.replace(/\n.*/s, "") : "Invalid YAML.",
48
+ line: yamlErrorLine(err),
49
+ },
50
+ };
51
+ }
52
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
53
+ return { meta, body: rest };
54
+ for (const [rawKey, rawValue] of Object.entries(parsed)) {
55
+ const key = rawKey.trim().toLowerCase();
56
+ const value = frontmatterScalar(rawValue);
57
+ if (value === undefined)
42
58
  continue;
43
- const colon = line.indexOf(":");
44
- if (colon === -1) {
45
- return { meta, body: rest, error: { message: `Couldn't parse line: \`${rawLine}\``, line: i + 2 } };
46
- }
47
- const key = line.slice(0, colon).trim().toLowerCase();
48
- let value = line.slice(colon + 1).trim();
49
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
50
- value = value.slice(1, -1);
51
- }
52
59
  if (key === "title"
53
60
  || key === "description"
54
61
  || key === "version"
@@ -67,6 +74,18 @@ function parseFrontmatter(input) {
67
74
  }
68
75
  return { meta, body: rest };
69
76
  }
77
+ function yamlErrorLine(err) {
78
+ const linePos = err.linePos;
79
+ const line = linePos?.[0]?.line;
80
+ return typeof line === "number" && Number.isFinite(line) ? line + 1 : 2;
81
+ }
82
+ function frontmatterScalar(value) {
83
+ if (typeof value === "string")
84
+ return value;
85
+ if (typeof value === "number" || typeof value === "boolean")
86
+ return String(value);
87
+ return undefined;
88
+ }
70
89
  function parseAssetType(value, source) {
71
90
  if (!value)
72
91
  return undefined;
@@ -193,8 +212,8 @@ export async function publish(opts) {
193
212
  process.stdout.write(` ${c.dim("Share it anywhere.")}\n\n`);
194
213
  }
195
214
  process.stdout.write(` ${c.bold("Next")}\n`);
196
- process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --target claude`)}\n`);
215
+ process.stdout.write(` ${c.dim("1.")} Test locally: ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
197
216
  process.stdout.write(` ${c.dim("2.")} Send the link.\n`);
198
- process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --target claude`)}\n`);
217
+ process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
199
218
  process.stdout.write(` ${c.dim("4.")} Agent reads the installed Markdown from the local skills folder.\n\n`);
200
219
  }
package/dist/scan.js CHANGED
@@ -17,6 +17,9 @@ export async function scanSkill(file) {
17
17
  throw new FloomError(`That's a directory, not a file: ${file}`);
18
18
  throw new FloomError(`Couldn't read ${file}: ${err.message}`);
19
19
  }
20
+ if (!raw.trim()) {
21
+ throw new FloomError(`File is empty: ${file}`, "Add skill instructions before scanning or publishing.");
22
+ }
20
23
  const findings = detectSkillSecurityFindings(raw);
21
24
  if (findings.length > 0) {
22
25
  throw new FloomError("Security scan failed.", `${formatSecurityFindings(findings)}\nRemove secrets, prompt-injection text, or data-exfiltration instructions before publishing.`);
package/dist/secrets.js CHANGED
@@ -12,7 +12,8 @@ const SECRET_PATTERNS = [
12
12
  { label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
13
13
  ];
14
14
  const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
15
- const PLACEHOLDER_RE = /^(?:your|example|placeholder|replace|changeme|todo|xxx|test|demo|dummy|redacted)/i;
15
+ 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;
16
17
  const PROMPT_INJECTION_PATTERNS = [
17
18
  { label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
18
19
  { label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
@@ -62,6 +63,13 @@ export function detectSecrets(input) {
62
63
  continue;
63
64
  pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
64
65
  }
66
+ PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
67
+ for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
68
+ const value = match[1] ?? "";
69
+ if (!value)
70
+ continue;
71
+ pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
72
+ }
65
73
  return findings.sort((a, b) => a.line - b.line || a.label.localeCompare(b.label));
66
74
  }
67
75
  function detectPatternFindings(input, patterns, category) {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Publish AI skills from your terminal. Share with a link.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "floom": "bin/floom.js"
8
+ "floom-skills": "bin/floom.js"
9
9
  },
10
10
  "files": [
11
11
  "bin",
@@ -30,7 +30,8 @@
30
30
  "open": "10.1.0",
31
31
  "ora": "8.1.1",
32
32
  "picocolors": "1.1.1",
33
- "update-notifier": "7.3.1"
33
+ "update-notifier": "7.3.1",
34
+ "yaml": "2.8.4"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "22.10.5",