@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 +17 -16
- package/dist/cli.js +26 -14
- package/dist/errors.js +11 -1
- package/dist/install.js +26 -2
- package/dist/login.js +19 -11
- package/dist/publish.js +35 -16
- package/dist/scan.js +3 -0
- package/dist/secrets.js +9 -1
- package/package.json +4 -3
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
|
-
|
|
7
|
-
floom
|
|
8
|
-
floom
|
|
9
|
-
floom
|
|
10
|
-
|
|
11
|
-
floom
|
|
12
|
-
floom
|
|
13
|
-
|
|
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
|
|
68
|
-
|
|
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")}
|
|
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=")
|
|
177
|
-
const
|
|
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
|
|
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.", "
|
|
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.", "
|
|
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(
|
|
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
|
-
|
|
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(
|
|
135
|
+
reject(new FloomError("Local auth server failed.", err.message));
|
|
136
136
|
});
|
|
137
|
-
server.listen(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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} --
|
|
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} --
|
|
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
|
|
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.
|
|
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",
|