@floomhq/floom 1.0.20 → 1.0.22
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 +5 -6
- package/dist/cli.js +44 -9
- package/dist/config.js +51 -1
- package/dist/doctor.js +16 -4
- package/dist/errors.js +1 -1
- package/dist/login.js +6 -5
- package/dist/mcp.js +1 -1
- package/dist/publish.js +36 -10
- package/dist/setup.js +7 -5
- package/dist/sync.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@ Sync AI skills across agents and machines. Publish from your terminal, then add
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @floomhq/floom
|
|
7
|
-
floom init my-skill.md
|
|
8
7
|
floom init my-skill
|
|
9
8
|
floom login
|
|
10
9
|
floom publish my-skill
|
|
@@ -22,9 +21,9 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
22
21
|
|
|
23
22
|
## Commands
|
|
24
23
|
|
|
25
|
-
- `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
|
|
24
|
+
- `npx -y @floomhq/floom login` — sign in with Google. Use `--provider github` for GitHub. New accounts are created on first login. Token stored at `~/.floom/config.json`.
|
|
26
25
|
- `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.
|
|
27
|
-
- `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>`,
|
|
26
|
+
- `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>`, `--skill-version <label>`, and `--update [slug-or-url]`.
|
|
28
27
|
- `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
|
|
29
28
|
- `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
|
|
30
29
|
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>/SKILL.md` with supporting package files. Optional `--target claude|codex`, `--setup` to connect the agent, and `--force` to replace an existing local copy.
|
|
@@ -33,8 +32,8 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
33
32
|
- `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
|
|
34
33
|
- `npx -y @floomhq/floom connect` — alias for setup.
|
|
35
34
|
- `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
|
|
36
|
-
- `npx -y @floomhq/floom sync` —
|
|
37
|
-
- `npx -y @floomhq/floom watch` —
|
|
35
|
+
- `npx -y @floomhq/floom sync` — pull your published, saved, and subscribed library skills into Claude or Codex. Optional `--target claude|codex`.
|
|
36
|
+
- `npx -y @floomhq/floom watch` — run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
|
|
38
37
|
- `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
|
|
39
38
|
- `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
|
|
40
39
|
- `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
|
|
@@ -75,7 +74,7 @@ version: 0.1.0
|
|
|
75
74
|
|
|
76
75
|
Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
|
|
77
76
|
|
|
78
|
-
`floom sync` and `floom watch`
|
|
77
|
+
`floom sync` and `floom watch` pull published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
|
|
79
78
|
The manifest records hashes for files Floom previously wrote. Sync writes missing files only.
|
|
80
79
|
Remote updates, existing untracked files, and locally edited tracked files are skipped as conflicts.
|
|
81
80
|
Symlinks are never followed. To replace a local skill manually, run `floom add <url-or-slug> --force`.
|
package/dist/cli.js
CHANGED
|
@@ -83,7 +83,7 @@ function commandUsage() {
|
|
|
83
83
|
${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
|
|
84
84
|
${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
|
|
85
85
|
${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
|
|
86
|
-
${c.dim(" --skill-version <label
|
|
86
|
+
${c.dim(" --skill-version <label>, --update [slug-or-url]")}
|
|
87
87
|
${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
|
|
88
88
|
${c.dim("Flags: --add <email>, --remove <email>, --list")}
|
|
89
89
|
|
|
@@ -107,9 +107,9 @@ function commandUsage() {
|
|
|
107
107
|
${c.dim("Alias: lib")}
|
|
108
108
|
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
|
|
109
109
|
${c.cyan("mcp")} Print optional MCP setup guidance
|
|
110
|
-
${c.cyan("sync")}
|
|
110
|
+
${c.cyan("sync")} Pull published, saved, and library skills
|
|
111
111
|
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
112
|
-
${c.cyan("watch")}
|
|
112
|
+
${c.cyan("watch")} Run a polling sync loop
|
|
113
113
|
${c.dim("Flags: --target claude|codex (default: claude), --interval <seconds>")}
|
|
114
114
|
${c.cyan("agent-prompt")} Print the one-line agent instruction
|
|
115
115
|
${c.dim("Alias: paste")}
|
|
@@ -162,6 +162,13 @@ Diagnose auth, API reachability, PATH collisions, MCP setup, and local skills fo
|
|
|
162
162
|
${c.bold("Flags")}
|
|
163
163
|
${c.cyan("--target")} ${c.dim("claude|codex")} Check Claude Code or Codex paths. Default: claude.
|
|
164
164
|
${c.cyan("--json")} Print structured checks for scripts.
|
|
165
|
+
`,
|
|
166
|
+
login: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} login`)} ${c.dim("[flags]")}
|
|
167
|
+
|
|
168
|
+
Sign in through browser OAuth.
|
|
169
|
+
|
|
170
|
+
${c.bold("Flags")}
|
|
171
|
+
${c.cyan("--provider")} ${c.dim("google|github")} OAuth provider. Default: google.
|
|
165
172
|
`,
|
|
166
173
|
publish: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} publish`)} ${c.dim("<path> [flags]")}
|
|
167
174
|
|
|
@@ -172,6 +179,7 @@ ${c.bold("Flags")}
|
|
|
172
179
|
${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
|
|
173
180
|
${c.cyan("--installs-as")} ${c.dim("claude_skill|memory|rule|codex_instruction|opencode_instruction|cursor_rule|other")}
|
|
174
181
|
${c.cyan("--skill-version")} ${c.dim("<label>")}
|
|
182
|
+
${c.cyan("--update")} ${c.dim("[slug-or-url]")}
|
|
175
183
|
`,
|
|
176
184
|
init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
|
|
177
185
|
|
|
@@ -182,14 +190,14 @@ ${c.bold("Flags")}
|
|
|
182
190
|
`,
|
|
183
191
|
sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
|
|
184
192
|
|
|
185
|
-
|
|
193
|
+
Pull published, saved, and subscribed library skills.
|
|
186
194
|
|
|
187
195
|
${c.bold("Flags")}
|
|
188
196
|
${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
|
|
189
197
|
`,
|
|
190
198
|
watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
|
|
191
199
|
|
|
192
|
-
Poll sync on an interval.
|
|
200
|
+
Poll sync on an interval.
|
|
193
201
|
|
|
194
202
|
${c.bold("Flags")}
|
|
195
203
|
${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
|
|
@@ -255,6 +263,23 @@ function readFlagValue(argv, index, flag) {
|
|
|
255
263
|
throw new FloomError(`Missing value for ${flag}.`);
|
|
256
264
|
return { value, nextIndex: index + 1 };
|
|
257
265
|
}
|
|
266
|
+
function parseLoginArgs(argv) {
|
|
267
|
+
let provider = "google";
|
|
268
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
269
|
+
const a = argv[i] ?? "";
|
|
270
|
+
if (a === "--provider" || a.startsWith("--provider=")) {
|
|
271
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--provider");
|
|
272
|
+
if (value !== "google" && value !== "github") {
|
|
273
|
+
throw new FloomError("Invalid --provider.", "Use google or github.");
|
|
274
|
+
}
|
|
275
|
+
provider = value;
|
|
276
|
+
i = nextIndex;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw new FloomError(`Unknown flag or argument: ${a}`, `Try \`${CLI_COMMAND} login --provider github\`.`);
|
|
280
|
+
}
|
|
281
|
+
return { provider };
|
|
282
|
+
}
|
|
258
283
|
function parseFlags(argv) {
|
|
259
284
|
const out = { visibility: "unlisted", update: false, rest: [] };
|
|
260
285
|
let visibilityFlag = null;
|
|
@@ -268,8 +293,18 @@ function parseFlags(argv) {
|
|
|
268
293
|
visibilityFlag = nextVisibility;
|
|
269
294
|
out.visibility = nextVisibility;
|
|
270
295
|
}
|
|
271
|
-
else if (a === "--update") {
|
|
272
|
-
|
|
296
|
+
else if (a === "--update" || a.startsWith("--update=")) {
|
|
297
|
+
out.update = true;
|
|
298
|
+
if (a.startsWith("--update=")) {
|
|
299
|
+
out.updateSlug = a.slice("--update=".length);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const next = argv[i + 1];
|
|
303
|
+
if (out.rest.length > 0 && next && !next.startsWith("--")) {
|
|
304
|
+
out.updateSlug = next;
|
|
305
|
+
i += 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
273
308
|
}
|
|
274
309
|
else if (a === "--share" || a.startsWith("--share=")) {
|
|
275
310
|
throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --share\` is planned for a later Floom release.`);
|
|
@@ -852,8 +887,7 @@ async function main() {
|
|
|
852
887
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
853
888
|
return;
|
|
854
889
|
case "login":
|
|
855
|
-
|
|
856
|
-
await login();
|
|
890
|
+
await login(parseLoginArgs(rest).provider);
|
|
857
891
|
return;
|
|
858
892
|
case "logout":
|
|
859
893
|
rejectArgs(rest, `Try \`${CLI_COMMAND} logout\`.`);
|
|
@@ -882,6 +916,7 @@ async function main() {
|
|
|
882
916
|
file,
|
|
883
917
|
visibility: flags.visibility,
|
|
884
918
|
update: flags.update,
|
|
919
|
+
...(flags.updateSlug ? { updateSlug: flags.updateSlug } : {}),
|
|
885
920
|
...(flags.assetType ? { assetType: flags.assetType } : {}),
|
|
886
921
|
...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
|
|
887
922
|
...(flags.version ? { version: flags.version } : {}),
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ export const CONFIG_DIR = join(homedir(), ".floom");
|
|
|
5
5
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
6
6
|
export const DEFAULT_API_URL = "https://floom.dev";
|
|
7
7
|
export const DEFAULT_WEB_URL = "https://floom.dev";
|
|
8
|
+
const REFRESH_SKEW_SECONDS = 5 * 60;
|
|
8
9
|
export function getApiUrl() {
|
|
9
10
|
return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
|
|
10
11
|
}
|
|
@@ -14,7 +15,7 @@ export function resolveApiUrl(cfg) {
|
|
|
14
15
|
export function getWebUrl() {
|
|
15
16
|
return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
|
|
16
17
|
}
|
|
17
|
-
export async function
|
|
18
|
+
export async function readRawConfig() {
|
|
18
19
|
try {
|
|
19
20
|
const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
|
|
20
21
|
const parsed = JSON.parse(buf);
|
|
@@ -28,6 +29,15 @@ export async function readConfig() {
|
|
|
28
29
|
throw e;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
export async function readConfig() {
|
|
33
|
+
const cfg = await readRawConfig();
|
|
34
|
+
if (!cfg)
|
|
35
|
+
return null;
|
|
36
|
+
if (!needsRefresh(cfg))
|
|
37
|
+
return cfg;
|
|
38
|
+
const refreshed = await refreshConfig(cfg);
|
|
39
|
+
return refreshed;
|
|
40
|
+
}
|
|
31
41
|
export async function writeConfig(cfg) {
|
|
32
42
|
const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
|
|
33
43
|
const targetDir = dirname(targetPath);
|
|
@@ -45,3 +55,43 @@ export async function deleteConfig() {
|
|
|
45
55
|
throw e;
|
|
46
56
|
}
|
|
47
57
|
}
|
|
58
|
+
export function needsRefresh(cfg) {
|
|
59
|
+
return !Number.isFinite(cfg.expiresAt) || cfg.expiresAt <= Math.floor(Date.now() / 1000) + REFRESH_SKEW_SECONDS;
|
|
60
|
+
}
|
|
61
|
+
export async function refreshConfig(cfg) {
|
|
62
|
+
const apiUrl = resolveApiUrl(cfg);
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(`${apiUrl}/api/v1/auth/refresh`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ refresh_token: cfg.refreshToken }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok)
|
|
75
|
+
return null;
|
|
76
|
+
let data;
|
|
77
|
+
try {
|
|
78
|
+
data = (await res.json());
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!data.access_token || !data.refresh_token)
|
|
84
|
+
return null;
|
|
85
|
+
const expiresAt = Number.isFinite(data.expires_at)
|
|
86
|
+
? Number(data.expires_at)
|
|
87
|
+
: Math.floor(Date.now() / 1000) + (Number.isFinite(data.expires_in) ? Number(data.expires_in) : 3600);
|
|
88
|
+
const refreshed = {
|
|
89
|
+
apiUrl,
|
|
90
|
+
accessToken: data.access_token,
|
|
91
|
+
refreshToken: data.refresh_token,
|
|
92
|
+
expiresAt,
|
|
93
|
+
email: data.email ?? cfg.email ?? null,
|
|
94
|
+
};
|
|
95
|
+
await writeConfig(refreshed);
|
|
96
|
+
return refreshed;
|
|
97
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import { delimiter } from "node:path";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
7
|
+
import { needsRefresh, readConfig, readRawConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
8
8
|
import { floomFetch } from "./lib/api.js";
|
|
9
9
|
import { c, symbols } from "./ui.js";
|
|
10
10
|
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
@@ -17,14 +17,25 @@ function statusBadge(s) {
|
|
|
17
17
|
return c.red(symbols.fail);
|
|
18
18
|
}
|
|
19
19
|
async function checkAuth() {
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
20
|
+
const raw = await readRawConfig();
|
|
21
|
+
if (!raw) {
|
|
22
22
|
return {
|
|
23
23
|
name: "Auth",
|
|
24
24
|
status: "ok",
|
|
25
25
|
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
const expired = raw.expiresAt <= Math.floor(Date.now() / 1000);
|
|
29
|
+
const expiresSoon = needsRefresh(raw);
|
|
30
|
+
const cfg = await readConfig();
|
|
31
|
+
if (!cfg) {
|
|
32
|
+
return {
|
|
33
|
+
name: "Auth",
|
|
34
|
+
status: "fail",
|
|
35
|
+
detail: expired ? "Token expired and refresh failed." : "Token refresh failed.",
|
|
36
|
+
hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to sign in again.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
28
39
|
const apiUrl = resolveApiUrl(cfg);
|
|
29
40
|
try {
|
|
30
41
|
const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
|
|
@@ -48,10 +59,11 @@ async function checkAuth() {
|
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
const data = (await res.json());
|
|
62
|
+
const suffix = expiresSoon ? " (token refreshed)" : "";
|
|
51
63
|
return {
|
|
52
64
|
name: "Auth",
|
|
53
65
|
status: "ok",
|
|
54
|
-
detail: data.email ? `Signed in as ${data.email}` :
|
|
66
|
+
detail: data.email ? `Signed in as ${data.email}${suffix}` : `Signed in${suffix}`,
|
|
55
67
|
};
|
|
56
68
|
}
|
|
57
69
|
catch (err) {
|
package/dist/errors.js
CHANGED
|
@@ -20,7 +20,7 @@ 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)) {
|
|
23
|
+
if (/fetch|inspect|add|install|show|get|search|list|info|load/i.test(action)) {
|
|
24
24
|
return new FloomError("Skill not found.", "Check the link or slug, then try again.");
|
|
25
25
|
}
|
|
26
26
|
return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
|
package/dist/login.js
CHANGED
|
@@ -8,18 +8,19 @@ import { c, header, symbols } from "./ui.js";
|
|
|
8
8
|
import { FloomError, friendlyHttp, friendlyNetwork } from "./errors.js";
|
|
9
9
|
const DEFAULT_PORT = 7456;
|
|
10
10
|
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
-
export async function login() {
|
|
11
|
+
export async function login(provider = "google") {
|
|
12
12
|
const apiUrl = getApiUrl();
|
|
13
13
|
const port = await pickPort();
|
|
14
|
+
const providerLabel = provider === "github" ? "GitHub" : "Google";
|
|
14
15
|
process.stdout.write(header());
|
|
15
|
-
process.stdout.write(`${symbols.arrow} Opening browser to sign in with
|
|
16
|
+
process.stdout.write(`${symbols.arrow} Opening browser to sign in with ${providerLabel}...\n\n`);
|
|
16
17
|
const spinner = ora({
|
|
17
18
|
text: c.dim("Waiting for sign-in to complete..."),
|
|
18
19
|
color: "yellow",
|
|
19
20
|
}).start();
|
|
20
21
|
let tokens;
|
|
21
22
|
try {
|
|
22
|
-
tokens = await waitForCallback(port);
|
|
23
|
+
tokens = await waitForCallback(port, provider);
|
|
23
24
|
}
|
|
24
25
|
catch (err) {
|
|
25
26
|
spinner.stop();
|
|
@@ -90,7 +91,7 @@ function reserveEphemeralPort() {
|
|
|
90
91
|
});
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
|
-
function waitForCallback(port) {
|
|
94
|
+
function waitForCallback(port, provider) {
|
|
94
95
|
return new Promise((resolve, reject) => {
|
|
95
96
|
const apiUrl = getApiUrl();
|
|
96
97
|
const state = randomBytes(32).toString("base64url");
|
|
@@ -172,7 +173,7 @@ function waitForCallback(port) {
|
|
|
172
173
|
reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
|
|
173
174
|
});
|
|
174
175
|
server.listen(port, "127.0.0.1", () => {
|
|
175
|
-
const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
|
|
176
|
+
const target = `${apiUrl}/auth/cli?port=${port}&provider=${provider}&state=${encodeURIComponent(state)}`;
|
|
176
177
|
open(target).catch((e) => {
|
|
177
178
|
const msg = e instanceof Error ? e.message : String(e);
|
|
178
179
|
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
|
package/dist/mcp.js
CHANGED
|
@@ -4,7 +4,7 @@ export function printMcpSetup() {
|
|
|
4
4
|
- Use Floom skills from the local Floom skills folder when they match the task.
|
|
5
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
6
|
- To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
|
|
7
|
-
- MCP sync is optional
|
|
7
|
+
- MCP sync is optional; use it 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`);
|
|
10
10
|
process.stdout.write(` ${c.bold("Claude Code")}\n`);
|
package/dist/publish.js
CHANGED
|
@@ -19,6 +19,7 @@ const INSTALL_TARGETS = new Set([
|
|
|
19
19
|
"other",
|
|
20
20
|
]);
|
|
21
21
|
const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
22
|
+
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
22
23
|
function parseFrontmatter(input) {
|
|
23
24
|
const trimmed = input.replace(/^/, "");
|
|
24
25
|
if (!trimmed.startsWith("---"))
|
|
@@ -106,6 +107,33 @@ function parseVersion(value, source) {
|
|
|
106
107
|
return value;
|
|
107
108
|
throw new FloomError(`Invalid ${source}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
|
|
108
109
|
}
|
|
110
|
+
function slugFromInput(input) {
|
|
111
|
+
const trimmed = input.trim();
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(trimmed);
|
|
114
|
+
const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
|
|
115
|
+
return last.replace(/\.(md|json)$/i, "");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return trimmed.replace(/\.(md|json)$/i, "");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function inferSlugFromPath(path) {
|
|
122
|
+
return path
|
|
123
|
+
.replace(/[\\/]+$/, "")
|
|
124
|
+
.split(/[\\/]+/)
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.at(-1)
|
|
127
|
+
?.replace(/\.md$/i, "") ?? "";
|
|
128
|
+
}
|
|
129
|
+
function resolveUpdateSlug(opts, meta) {
|
|
130
|
+
const raw = opts.updateSlug ?? meta.slug ?? inferSlugFromPath(opts.file);
|
|
131
|
+
const slug = slugFromInput(raw);
|
|
132
|
+
if (!SLUG_RE.test(slug)) {
|
|
133
|
+
throw new FloomError("Missing or invalid update slug.", "Use `npx -y @floomhq/floom publish <path> --update <slug-or-url>`, or add `slug:` to SKILL.md frontmatter.");
|
|
134
|
+
}
|
|
135
|
+
return slug;
|
|
136
|
+
}
|
|
109
137
|
export async function publish(opts) {
|
|
110
138
|
const skillPackage = await readSkillPackage(opts.file);
|
|
111
139
|
const securityFailures = [
|
|
@@ -136,11 +164,8 @@ export async function publish(opts) {
|
|
|
136
164
|
if (!cfg) {
|
|
137
165
|
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
138
166
|
}
|
|
139
|
-
// Later version: detect already-published when --update is missing.
|
|
140
|
-
// The current API does not return a duplicate-skill code, so we leave
|
|
141
|
-
// the actual dedupe to the server and surface the friendly hint on 409.
|
|
142
167
|
const spinner = ora({
|
|
143
|
-
text: c.dim(
|
|
168
|
+
text: c.dim(`${opts.update ? "Updating" : "Publishing"} ${meta.title ? c.bold(meta.title) : opts.file}...`),
|
|
144
169
|
color: "yellow",
|
|
145
170
|
}).start();
|
|
146
171
|
const apiUrl = resolveApiUrl(cfg);
|
|
@@ -156,12 +181,13 @@ export async function publish(opts) {
|
|
|
156
181
|
original_filename: skillPackage.originalFilename,
|
|
157
182
|
published_via: "cli",
|
|
158
183
|
...(opts.sharedWithEmails ? { shared_with_emails: opts.sharedWithEmails } : {}),
|
|
159
|
-
...(skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
|
|
184
|
+
...(opts.update || skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
|
|
160
185
|
};
|
|
161
186
|
let res;
|
|
162
187
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
const updateSlug = opts.update ? resolveUpdateSlug(opts, meta) : null;
|
|
189
|
+
res = await floomFetch(updateSlug ? `${apiUrl}/api/v1/skills/${encodeURIComponent(updateSlug)}` : `${apiUrl}/api/skills`, updateSlug ? "update your skill" : "publish your skill", {
|
|
190
|
+
method: updateSlug ? "PATCH" : "POST",
|
|
165
191
|
token: cfg.accessToken,
|
|
166
192
|
checkOk: false,
|
|
167
193
|
body,
|
|
@@ -173,11 +199,11 @@ export async function publish(opts) {
|
|
|
173
199
|
}
|
|
174
200
|
if (res.status === 409) {
|
|
175
201
|
spinner.stop();
|
|
176
|
-
throw new FloomError("Skill already published.", "Use `--update
|
|
202
|
+
throw new FloomError("Skill already published.", "Use `--update <slug-or-url>` to publish a new version of an existing skill.");
|
|
177
203
|
}
|
|
178
204
|
if (!res.ok) {
|
|
179
205
|
spinner.stop();
|
|
180
|
-
throw friendlyHttp(res.status, "publish your skill");
|
|
206
|
+
throw friendlyHttp(res.status, opts.update ? "update your skill" : "publish your skill");
|
|
181
207
|
}
|
|
182
208
|
const data = (await res.json());
|
|
183
209
|
// Build a humans-friendly URL: strip the trailing `.md` if the API returned one.
|
|
@@ -188,7 +214,7 @@ export async function publish(opts) {
|
|
|
188
214
|
spinner.stop();
|
|
189
215
|
const versionTag = version ? c.dim(` (${formatVersionLabel(version)})`) : "";
|
|
190
216
|
const titleLabel = data.title ? `"${data.title}"` : opts.file;
|
|
191
|
-
process.stdout.write(`\n${symbols.ok} Published ${c.bold(titleLabel)}${versionTag}\n\n`);
|
|
217
|
+
process.stdout.write(`\n${symbols.ok} ${opts.update ? "Updated" : "Published"} ${c.bold(titleLabel)}${versionTag}\n\n`);
|
|
192
218
|
process.stdout.write(` ${c.cyan(humanUrl)}\n\n`);
|
|
193
219
|
if (data.visibility === "shared" && data.shared_with_emails?.length) {
|
|
194
220
|
process.stdout.write(` ${c.dim(`Shared with ${data.shared_with_emails.join(", ")}`)}\n\n`);
|
package/dist/setup.js
CHANGED
|
@@ -16,14 +16,16 @@ function floomAgentInstructions(target) {
|
|
|
16
16
|
const addCommand = target === "codex"
|
|
17
17
|
? `${CLI_COMMAND} add <slug-or-url> --target codex`
|
|
18
18
|
: `${CLI_COMMAND} add <slug-or-url> --target claude`;
|
|
19
|
+
const localSkillsDir = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
|
|
19
20
|
return `${START_MARKER}
|
|
20
|
-
## Floom
|
|
21
|
+
## Floom Skills
|
|
21
22
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
23
|
+
- Use Floom skills when they fit the task. Search local skills before recreating behavior from scratch.
|
|
24
|
+
- Local skills live in \`${localSkillsDir}\`.
|
|
24
25
|
- Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
|
|
25
|
-
-
|
|
26
|
-
-
|
|
26
|
+
- Search or inspect skills with \`${CLI_COMMAND} search <query>\` and \`${CLI_COMMAND} info <slug-or-url>\`.
|
|
27
|
+
- If Floom MCP is available, use it for compact skill search/status and load only the specific skill needed.
|
|
28
|
+
- Do not publish, sync, or subscribe without user approval.
|
|
27
29
|
${END_MARKER}`;
|
|
28
30
|
}
|
|
29
31
|
async function fileExists(path) {
|
package/dist/sync.js
CHANGED
|
@@ -298,7 +298,7 @@ export async function sync(opts = {}) {
|
|
|
298
298
|
}
|
|
299
299
|
for (const skill of payload.skills)
|
|
300
300
|
validateSyncSkillShape(skill);
|
|
301
|
-
// Version 1
|
|
301
|
+
// Version 1 syncs published, saved, and subscribed library skills.
|
|
302
302
|
const all = dedupeSyncSkills(payload.skills);
|
|
303
303
|
const seen = new Set();
|
|
304
304
|
let unchanged = 0;
|