@floomhq/floom 1.0.21 → 1.0.23
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 +4 -5
- package/dist/cli.js +19 -7
- package/dist/config.js +51 -1
- package/dist/doctor.js +16 -4
- package/dist/errors.js +1 -1
- package/dist/login.js +3 -2
- 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
|
|
@@ -24,7 +23,7 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
24
23
|
|
|
25
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")}
|
|
@@ -179,6 +179,7 @@ ${c.bold("Flags")}
|
|
|
179
179
|
${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
|
|
180
180
|
${c.cyan("--installs-as")} ${c.dim("claude_skill|memory|rule|codex_instruction|opencode_instruction|cursor_rule|other")}
|
|
181
181
|
${c.cyan("--skill-version")} ${c.dim("<label>")}
|
|
182
|
+
${c.cyan("--update")} ${c.dim("[slug-or-url]")}
|
|
182
183
|
`,
|
|
183
184
|
init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
|
|
184
185
|
|
|
@@ -189,14 +190,14 @@ ${c.bold("Flags")}
|
|
|
189
190
|
`,
|
|
190
191
|
sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
Pull published, saved, and subscribed library skills.
|
|
193
194
|
|
|
194
195
|
${c.bold("Flags")}
|
|
195
196
|
${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
|
|
196
197
|
`,
|
|
197
198
|
watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
|
|
198
199
|
|
|
199
|
-
Poll sync on an interval.
|
|
200
|
+
Poll sync on an interval.
|
|
200
201
|
|
|
201
202
|
${c.bold("Flags")}
|
|
202
203
|
${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
|
|
@@ -292,8 +293,18 @@ function parseFlags(argv) {
|
|
|
292
293
|
visibilityFlag = nextVisibility;
|
|
293
294
|
out.visibility = nextVisibility;
|
|
294
295
|
}
|
|
295
|
-
else if (a === "--update") {
|
|
296
|
-
|
|
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
|
+
}
|
|
297
308
|
}
|
|
298
309
|
else if (a === "--share" || a.startsWith("--share=")) {
|
|
299
310
|
throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --share\` is planned for a later Floom release.`);
|
|
@@ -905,6 +916,7 @@ async function main() {
|
|
|
905
916
|
file,
|
|
906
917
|
visibility: flags.visibility,
|
|
907
918
|
update: flags.update,
|
|
919
|
+
...(flags.updateSlug ? { updateSlug: flags.updateSlug } : {}),
|
|
908
920
|
...(flags.assetType ? { assetType: flags.assetType } : {}),
|
|
909
921
|
...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
|
|
910
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
|
@@ -174,10 +174,11 @@ function waitForCallback(port, provider) {
|
|
|
174
174
|
});
|
|
175
175
|
server.listen(port, "127.0.0.1", () => {
|
|
176
176
|
const target = `${apiUrl}/auth/cli?port=${port}&provider=${provider}&state=${encodeURIComponent(state)}`;
|
|
177
|
+
process.stdout.write(c.dim("If the browser does not open, copy this URL:") +
|
|
178
|
+
`\n${c.cyan(target)}\n\n`);
|
|
177
179
|
open(target).catch((e) => {
|
|
178
180
|
const msg = e instanceof Error ? e.message : String(e);
|
|
179
|
-
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`)
|
|
180
|
-
c.dim(`Open this URL manually: ${target}\n`));
|
|
181
|
+
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`));
|
|
181
182
|
});
|
|
182
183
|
});
|
|
183
184
|
});
|
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;
|