@floomhq/floom 1.0.0 → 1.0.2
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 +15 -2
- package/dist/cli.js +244 -14
- package/dist/doctor.js +1 -1
- package/dist/info.js +11 -3
- package/dist/lib/skill-labels.js +140 -0
- package/dist/library.js +26 -1
- package/dist/list.js +6 -3
- package/dist/search.js +54 -0
- package/dist/setup.js +158 -0
- package/dist/sync.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,14 @@ Publish AI skills from your terminal. Share them with a link. Add other people's
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @floomhq/floom
|
|
7
|
+
floom init my-skill.md
|
|
7
8
|
floom login
|
|
8
9
|
floom publish my-skill.md
|
|
10
|
+
floom search review
|
|
9
11
|
floom add awesome-skill
|
|
12
|
+
floom setup --target claude --dry-run
|
|
10
13
|
floom list
|
|
14
|
+
floom library list
|
|
11
15
|
```
|
|
12
16
|
|
|
13
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.
|
|
@@ -20,8 +24,17 @@ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the UR
|
|
|
20
24
|
- `floom list` — show your published skills. Optional `--json`.
|
|
21
25
|
- `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
|
|
22
26
|
- `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
23
|
-
- `floom
|
|
27
|
+
- `floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
|
|
28
|
+
- `floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
|
|
29
|
+
- `floom connect` — alias for `floom setup`.
|
|
30
|
+
- `floom mcp` — print MCP setup commands for supported agent CLIs.
|
|
31
|
+
- `floom sync` — preview: pull your published, saved, and subscribed library skills into `~/.claude/skills/`.
|
|
24
32
|
- `floom watch` — preview: run `floom sync` repeatedly. Optional `--interval <seconds>`; minimum `10`.
|
|
33
|
+
- `floom library list` — list public starter libraries. Optional `--json`.
|
|
34
|
+
- `floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
|
|
35
|
+
- `floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
|
|
36
|
+
- `floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
|
|
37
|
+
- `floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
|
|
25
38
|
- `floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
|
|
26
39
|
- `floom doctor` — diagnose your Floom setup.
|
|
27
40
|
- `floom whoami` — show the signed-in account.
|
|
@@ -46,7 +59,7 @@ version: 0.1.0
|
|
|
46
59
|
|
|
47
60
|
Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
|
|
48
61
|
|
|
49
|
-
`floom sync` and `floom watch` are Version 1 preview commands for
|
|
62
|
+
`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`.
|
|
50
63
|
The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
|
|
51
64
|
only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
|
|
52
65
|
conflicts. Symlinks are never followed. To accept the Floom version, move or delete the local file
|
package/dist/cli.js
CHANGED
|
@@ -11,19 +11,21 @@ import { info } from "./info.js";
|
|
|
11
11
|
import { deleteSkill } from "./delete.js";
|
|
12
12
|
import { doctor } from "./doctor.js";
|
|
13
13
|
import { sync } from "./sync.js";
|
|
14
|
+
import { printMcpSetup } from "./mcp.js";
|
|
15
|
+
import { setupAgent } from "./setup.js";
|
|
16
|
+
import { search } from "./search.js";
|
|
17
|
+
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
14
18
|
import { c, symbols } from "./ui.js";
|
|
15
19
|
import { printError, FloomError } from "./errors.js";
|
|
16
|
-
const VERSION = "1.0.
|
|
20
|
+
const VERSION = "1.0.2";
|
|
17
21
|
const PKG = { name: "@floomhq/floom", version: VERSION };
|
|
18
22
|
const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
|
|
19
23
|
function usage() {
|
|
20
24
|
const out = `
|
|
21
|
-
${c.coral("
|
|
22
|
-
${c.coral("
|
|
23
|
-
${c.coral("
|
|
24
|
-
${c.coral("
|
|
25
|
-
${c.coral(" /_/ |_|\\___/_/\\__,_/\\__, /")} ${c.dim(`v${VERSION}`)}
|
|
26
|
-
${c.coral(" /____/")}
|
|
25
|
+
${c.coral(" __ _")}
|
|
26
|
+
${c.coral(" / _| |___ ___ _ __")} ${c.dim(`v${VERSION}`)}
|
|
27
|
+
${c.coral("| _| / _ \\/ _ \\ ' \\")}
|
|
28
|
+
${c.coral("|_| |_\\___/\\___/_|_|_|")}
|
|
27
29
|
|
|
28
30
|
${c.bold("Share AI agent skills with a link.")}
|
|
29
31
|
${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
|
|
@@ -32,6 +34,9 @@ function usage() {
|
|
|
32
34
|
${c.cyan("floom")} ${c.dim("<command> [options]")}
|
|
33
35
|
|
|
34
36
|
${c.bold("Start Here")}
|
|
37
|
+
${c.cyan("floom init")} ${c.dim("support-tone.md")}
|
|
38
|
+
${c.dim("Create a starter Markdown skill.")}
|
|
39
|
+
|
|
35
40
|
${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
|
|
36
41
|
${c.dim("Install a public skill. No account needed.")}
|
|
37
42
|
|
|
@@ -39,9 +44,10 @@ function usage() {
|
|
|
39
44
|
${c.dim("Publish Markdown and print a share link.")}
|
|
40
45
|
|
|
41
46
|
${c.bold("Commands")}
|
|
42
|
-
|
|
47
|
+
${c.dim("Receive")}
|
|
43
48
|
${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
|
|
44
49
|
${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
|
|
50
|
+
${c.cyan("search")} ${c.dim("<query>")} Search public skills and libraries ${c.dim("[--json]")}
|
|
45
51
|
|
|
46
52
|
${c.dim("Create")}
|
|
47
53
|
${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
|
|
@@ -57,13 +63,18 @@ function usage() {
|
|
|
57
63
|
${c.dim("Account")}
|
|
58
64
|
${c.cyan("login")} Sign in with Google
|
|
59
65
|
${c.cyan("list")} List your published skills ${c.dim("[--json]")}
|
|
66
|
+
${c.cyan("library")} Create, browse, and subscribe to skill libraries
|
|
67
|
+
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
|
|
60
68
|
${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
|
|
61
69
|
${c.cyan("whoami")} Show the signed-in account
|
|
62
70
|
${c.cyan("logout")} Delete local credentials
|
|
63
71
|
|
|
64
72
|
${c.dim("System")}
|
|
65
|
-
${c.cyan("
|
|
66
|
-
${c.cyan("
|
|
73
|
+
${c.cyan("setup")} ${c.dim("[--target claude|codex] [--file path]")} Add Floom guidance to agent instructions
|
|
74
|
+
${c.cyan("connect")} ${c.dim("[--target claude|codex]")} Alias for setup
|
|
75
|
+
${c.cyan("mcp")} Print MCP setup guidance
|
|
76
|
+
${c.cyan("sync")} Preview: pull published, saved, and library skills
|
|
77
|
+
${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
|
|
67
78
|
${c.cyan("doctor")} Diagnose auth, API, and local setup
|
|
68
79
|
${c.cyan("--help")} Show this help
|
|
69
80
|
${c.cyan("--version")} Show version
|
|
@@ -165,6 +176,36 @@ function parseInfoFlags(argv) {
|
|
|
165
176
|
}
|
|
166
177
|
return out;
|
|
167
178
|
}
|
|
179
|
+
function parseSearchFlags(argv) {
|
|
180
|
+
const out = { json: false };
|
|
181
|
+
const terms = [];
|
|
182
|
+
for (let i = 0; i < argv.length; i++) {
|
|
183
|
+
const a = argv[i] ?? "";
|
|
184
|
+
if (a === "--json")
|
|
185
|
+
out.json = true;
|
|
186
|
+
else if (a === "--library" || a.startsWith("--library=")) {
|
|
187
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--library");
|
|
188
|
+
out.library = value;
|
|
189
|
+
i = nextIndex;
|
|
190
|
+
}
|
|
191
|
+
else if (a === "--type" || a.startsWith("--type=")) {
|
|
192
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--type");
|
|
193
|
+
if (!ASSET_TYPES.has(value)) {
|
|
194
|
+
throw new FloomError(`Invalid --type: ${value}`, "Use one of: knowledge, instruction, workflow, skill.");
|
|
195
|
+
}
|
|
196
|
+
out.type = value;
|
|
197
|
+
i = nextIndex;
|
|
198
|
+
}
|
|
199
|
+
else if (a.startsWith("--")) {
|
|
200
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
terms.push(a);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
out.query = terms.join(" ").trim();
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
168
209
|
function parseDeleteFlags(argv) {
|
|
169
210
|
const out = { yes: false };
|
|
170
211
|
for (const a of argv) {
|
|
@@ -177,6 +218,164 @@ function parseDeleteFlags(argv) {
|
|
|
177
218
|
}
|
|
178
219
|
return out;
|
|
179
220
|
}
|
|
221
|
+
function parseSetupFlags(argv) {
|
|
222
|
+
const out = { dryRun: false, yes: false };
|
|
223
|
+
for (let i = 0; i < argv.length; i++) {
|
|
224
|
+
const a = argv[i] ?? "";
|
|
225
|
+
if (a === "--dry-run" || a === "--preview")
|
|
226
|
+
out.dryRun = true;
|
|
227
|
+
else if (a === "--yes" || a === "-y")
|
|
228
|
+
out.yes = true;
|
|
229
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
230
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
231
|
+
if (value !== "claude" && value !== "codex") {
|
|
232
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
233
|
+
}
|
|
234
|
+
out.target = value;
|
|
235
|
+
i = nextIndex;
|
|
236
|
+
}
|
|
237
|
+
else if (a === "--file" || a.startsWith("--file=")) {
|
|
238
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--file");
|
|
239
|
+
out.file = value;
|
|
240
|
+
i = nextIndex;
|
|
241
|
+
}
|
|
242
|
+
else if (a.startsWith("--")) {
|
|
243
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
|
|
244
|
+
}
|
|
245
|
+
else if (!out.file)
|
|
246
|
+
out.file = a;
|
|
247
|
+
else
|
|
248
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function normalizeFolder(value) {
|
|
253
|
+
return value === "root" || value === "/" || value === "." ? null : value;
|
|
254
|
+
}
|
|
255
|
+
function parseFolderTagFlags(argv) {
|
|
256
|
+
const out = { tags: [], rest: [] };
|
|
257
|
+
for (let i = 0; i < argv.length; i++) {
|
|
258
|
+
const a = argv[i] ?? "";
|
|
259
|
+
if (a === "--folder" || a.startsWith("--folder=")) {
|
|
260
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--folder");
|
|
261
|
+
out.folder = normalizeFolder(value);
|
|
262
|
+
i = nextIndex;
|
|
263
|
+
}
|
|
264
|
+
else if (a === "--root") {
|
|
265
|
+
out.folder = null;
|
|
266
|
+
}
|
|
267
|
+
else if (a === "--tag" || a.startsWith("--tag=")) {
|
|
268
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--tag");
|
|
269
|
+
out.tags.push(value);
|
|
270
|
+
i = nextIndex;
|
|
271
|
+
}
|
|
272
|
+
else if (a === "--tags" || a.startsWith("--tags=")) {
|
|
273
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--tags");
|
|
274
|
+
out.tags.push(...value.split(",").map((tag) => tag.trim()).filter(Boolean));
|
|
275
|
+
i = nextIndex;
|
|
276
|
+
}
|
|
277
|
+
else if (a.startsWith("--")) {
|
|
278
|
+
throw new FloomError(`Unknown flag: ${a}`, "Use --folder <path>, --root, --tag <tag>, or --tags a,b.");
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
out.rest.push(a);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
function parseLibraryCreateFlags(argv) {
|
|
287
|
+
const out = { visibility: "unlisted" };
|
|
288
|
+
for (let i = 0; i < argv.length; i++) {
|
|
289
|
+
const a = argv[i] ?? "";
|
|
290
|
+
if (a === "--name" || a.startsWith("--name=")) {
|
|
291
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--name");
|
|
292
|
+
out.name = value;
|
|
293
|
+
i = nextIndex;
|
|
294
|
+
}
|
|
295
|
+
else if (a === "--description" || a.startsWith("--description=")) {
|
|
296
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--description");
|
|
297
|
+
out.description = value;
|
|
298
|
+
i = nextIndex;
|
|
299
|
+
}
|
|
300
|
+
else if (a === "--public")
|
|
301
|
+
out.visibility = "public";
|
|
302
|
+
else if (a === "--private")
|
|
303
|
+
out.visibility = "private";
|
|
304
|
+
else if (a === "--unlisted")
|
|
305
|
+
out.visibility = "unlisted";
|
|
306
|
+
else if (a.startsWith("--")) {
|
|
307
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
308
|
+
}
|
|
309
|
+
else if (!out.slug)
|
|
310
|
+
out.slug = a;
|
|
311
|
+
else
|
|
312
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom library create <slug> --name <name>`.");
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
async function runLibrary(argv) {
|
|
317
|
+
const [subcommand, ...rest] = argv;
|
|
318
|
+
switch (subcommand ?? "list") {
|
|
319
|
+
case "list": {
|
|
320
|
+
const flags = parseListFlags(rest);
|
|
321
|
+
await libraryList(flags);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case "create": {
|
|
325
|
+
const flags = parseLibraryCreateFlags(rest);
|
|
326
|
+
if (!flags.slug)
|
|
327
|
+
throw new FloomError("Missing library slug.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
328
|
+
if (!flags.name)
|
|
329
|
+
throw new FloomError("Missing --name.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
|
|
330
|
+
await libraryCreate({
|
|
331
|
+
slug: flags.slug,
|
|
332
|
+
name: flags.name,
|
|
333
|
+
...(flags.description !== undefined ? { description: flags.description } : {}),
|
|
334
|
+
visibility: flags.visibility,
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
case "add": {
|
|
339
|
+
const flags = parseFolderTagFlags(rest);
|
|
340
|
+
const [librarySlug, skillSlug] = flags.rest;
|
|
341
|
+
if (!librarySlug || !skillSlug) {
|
|
342
|
+
throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
|
|
343
|
+
}
|
|
344
|
+
await libraryAddSkill({
|
|
345
|
+
librarySlug,
|
|
346
|
+
skillSlug,
|
|
347
|
+
...(flags.folder !== undefined ? { folder: flags.folder } : {}),
|
|
348
|
+
tags: flags.tags,
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
case "remove":
|
|
353
|
+
case "rm": {
|
|
354
|
+
const [librarySlug, skillSlug] = rest;
|
|
355
|
+
if (!librarySlug || !skillSlug) {
|
|
356
|
+
throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
|
|
357
|
+
}
|
|
358
|
+
await libraryRemoveSkill(librarySlug, skillSlug);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case "subscribe": {
|
|
362
|
+
const slug = rest[0];
|
|
363
|
+
if (!slug)
|
|
364
|
+
throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
|
|
365
|
+
await librarySubscribe(slug);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
case "unsubscribe": {
|
|
369
|
+
const slug = rest[0];
|
|
370
|
+
if (!slug)
|
|
371
|
+
throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
|
|
372
|
+
await libraryUnsubscribe(slug);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
throw new FloomError(`Unknown library command: ${subcommand}`, "Use: list, create, add, remove, subscribe, unsubscribe.");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
180
379
|
function parseWatchFlags(argv) {
|
|
181
380
|
const out = { intervalSeconds: 60 };
|
|
182
381
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -310,6 +509,19 @@ async function main() {
|
|
|
310
509
|
await info({ slug: flags.slug ?? "", json: flags.json });
|
|
311
510
|
}
|
|
312
511
|
return;
|
|
512
|
+
case "search": {
|
|
513
|
+
const flags = parseSearchFlags(rest);
|
|
514
|
+
if (!flags.query) {
|
|
515
|
+
throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
|
|
516
|
+
}
|
|
517
|
+
await search({
|
|
518
|
+
query: flags.query,
|
|
519
|
+
...(flags.library ? { library: flags.library } : {}),
|
|
520
|
+
...(flags.type ? { type: flags.type } : {}),
|
|
521
|
+
json: flags.json,
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
313
525
|
case "add":
|
|
314
526
|
case "install": {
|
|
315
527
|
const slug = rest.find((a) => !a.startsWith("--"));
|
|
@@ -322,6 +534,12 @@ async function main() {
|
|
|
322
534
|
case "sync":
|
|
323
535
|
await sync();
|
|
324
536
|
return;
|
|
537
|
+
case "setup":
|
|
538
|
+
case "connect": {
|
|
539
|
+
const flags = parseSetupFlags(rest);
|
|
540
|
+
await setupAgent(flags);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
325
543
|
case "watch": {
|
|
326
544
|
const flags = parseWatchFlags(rest);
|
|
327
545
|
await watch(flags.intervalSeconds);
|
|
@@ -335,11 +553,23 @@ async function main() {
|
|
|
335
553
|
}
|
|
336
554
|
case "library":
|
|
337
555
|
case "lib":
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
556
|
+
await runLibrary(rest);
|
|
557
|
+
return;
|
|
558
|
+
case "move": {
|
|
559
|
+
const flags = parseFolderTagFlags(rest);
|
|
560
|
+
const slug = flags.rest[0];
|
|
561
|
+
if (!slug) {
|
|
562
|
+
throw new FloomError("Missing skill slug.", "Try `floom move support-tone --folder support/tone`.");
|
|
563
|
+
}
|
|
564
|
+
if (flags.folder === undefined) {
|
|
565
|
+
throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
|
|
566
|
+
}
|
|
567
|
+
await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
341
570
|
case "mcp":
|
|
342
|
-
|
|
571
|
+
printMcpSetup();
|
|
572
|
+
return;
|
|
343
573
|
case "doctor":
|
|
344
574
|
await doctor();
|
|
345
575
|
return;
|
package/dist/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { stat, readFile, access, readdir, constants } from "node:fs/promises";
|
|
4
4
|
import { getApiUrl, readConfig, CONFIG_PATH } from "./config.js";
|
|
5
5
|
import { c, symbols } from "./ui.js";
|
|
6
|
-
const CLI_VERSION = "1.0.
|
|
6
|
+
const CLI_VERSION = "1.0.2";
|
|
7
7
|
function statusBadge(s) {
|
|
8
8
|
if (s === "ok")
|
|
9
9
|
return c.green(symbols.ok);
|
package/dist/info.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import { getApiUrl, readConfig } from "./config.js";
|
|
3
3
|
import { getJson } from "./lib/api.js";
|
|
4
|
+
import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
|
|
4
5
|
import { c, symbols } from "./ui.js";
|
|
5
6
|
import { FloomError } from "./errors.js";
|
|
6
7
|
function slugFromInput(input) {
|
|
@@ -29,9 +30,12 @@ export async function info(opts) {
|
|
|
29
30
|
spinner?.stop();
|
|
30
31
|
}
|
|
31
32
|
if (opts.json) {
|
|
32
|
-
// Strip body_md from JSON output to keep it summary-shaped
|
|
33
|
+
// Strip body_md from JSON output to keep it summary-shaped, but expose
|
|
34
|
+
// the parsed `requires` list so scripted consumers don't need to parse
|
|
35
|
+
// YAML themselves.
|
|
33
36
|
const { body_md: _body, ...summary } = detail;
|
|
34
|
-
|
|
37
|
+
const requires = extractRequires(detail.body_md);
|
|
38
|
+
process.stdout.write(`${JSON.stringify({ ...summary, requires }, null, 2)}\n`);
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
37
41
|
const title = detail.title ?? c.dim("(untitled)");
|
|
@@ -41,7 +45,11 @@ export async function info(opts) {
|
|
|
41
45
|
process.stdout.write(` ${c.dim("About: ")}${detail.description}\n`);
|
|
42
46
|
}
|
|
43
47
|
process.stdout.write(` ${c.dim("Visibility: ")}${detail.visibility}\n`);
|
|
44
|
-
process.stdout.write(` ${c.dim("Type: ")}${detail.asset_type
|
|
48
|
+
process.stdout.write(` ${c.dim("Type: ")}${formatType(detail.asset_type)}\n`);
|
|
49
|
+
const requires = extractRequires(detail.body_md);
|
|
50
|
+
if (requires.length > 0) {
|
|
51
|
+
process.stdout.write(` ${c.dim("Needs: ")}${formatToolList(requires)}\n`);
|
|
52
|
+
}
|
|
45
53
|
if (detail.version) {
|
|
46
54
|
process.stdout.write(` ${c.dim("Version: ")}${detail.version}\n`);
|
|
47
55
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-side mirror of the web's skill-labels: type label + tool label + a tiny
|
|
3
|
+
* frontmatter parser for `requires:`. No React, no `gray-matter` — keep the
|
|
4
|
+
* CLI dep tree thin.
|
|
5
|
+
*
|
|
6
|
+
* The frontmatter parser intentionally only supports two `requires` shapes:
|
|
7
|
+
* requires: [gmail, linear]
|
|
8
|
+
* requires:
|
|
9
|
+
* - gmail
|
|
10
|
+
* - linear
|
|
11
|
+
*
|
|
12
|
+
* That covers >99% of real skills. If a skill uses a more exotic YAML shape
|
|
13
|
+
* we silently get an empty list — not a crash.
|
|
14
|
+
*/
|
|
15
|
+
const SKILL_TYPE_LABELS = {
|
|
16
|
+
knowledge: "Knowledge",
|
|
17
|
+
instruction: "Instruction",
|
|
18
|
+
workflow: "Workflow",
|
|
19
|
+
skill: "Skill",
|
|
20
|
+
};
|
|
21
|
+
const TOOL_LABELS = {
|
|
22
|
+
gmail: "Gmail",
|
|
23
|
+
"google-mail": "Gmail",
|
|
24
|
+
calendar: "Google Calendar",
|
|
25
|
+
"google-calendar": "Google Calendar",
|
|
26
|
+
drive: "Google Drive",
|
|
27
|
+
"google-drive": "Google Drive",
|
|
28
|
+
docs: "Google Docs",
|
|
29
|
+
"google-docs": "Google Docs",
|
|
30
|
+
sheets: "Google Sheets",
|
|
31
|
+
"google-sheets": "Google Sheets",
|
|
32
|
+
github: "GitHub",
|
|
33
|
+
gitlab: "GitLab",
|
|
34
|
+
linear: "Linear",
|
|
35
|
+
notion: "Notion",
|
|
36
|
+
slack: "Slack",
|
|
37
|
+
intercom: "Intercom",
|
|
38
|
+
zendesk: "Zendesk",
|
|
39
|
+
hubspot: "HubSpot",
|
|
40
|
+
salesforce: "Salesforce",
|
|
41
|
+
stripe: "Stripe",
|
|
42
|
+
airtable: "Airtable",
|
|
43
|
+
jira: "Jira",
|
|
44
|
+
asana: "Asana",
|
|
45
|
+
trello: "Trello",
|
|
46
|
+
zapier: "Zapier",
|
|
47
|
+
composio: "Composio",
|
|
48
|
+
whatsapp: "WhatsApp",
|
|
49
|
+
telegram: "Telegram",
|
|
50
|
+
discord: "Discord",
|
|
51
|
+
twitter: "Twitter",
|
|
52
|
+
x: "X",
|
|
53
|
+
linkedin: "LinkedIn",
|
|
54
|
+
openai: "OpenAI",
|
|
55
|
+
anthropic: "Anthropic",
|
|
56
|
+
gemini: "Gemini",
|
|
57
|
+
claude: "Claude",
|
|
58
|
+
perplexity: "Perplexity",
|
|
59
|
+
supabase: "Supabase",
|
|
60
|
+
vercel: "Vercel",
|
|
61
|
+
resend: "Resend",
|
|
62
|
+
twilio: "Twilio",
|
|
63
|
+
webhook: "Webhook",
|
|
64
|
+
http: "HTTP",
|
|
65
|
+
};
|
|
66
|
+
export function formatType(value) {
|
|
67
|
+
if (!value)
|
|
68
|
+
return "Skill";
|
|
69
|
+
const normalized = value.toLowerCase();
|
|
70
|
+
return SKILL_TYPE_LABELS[normalized] ?? "Skill";
|
|
71
|
+
}
|
|
72
|
+
export function formatTool(slug) {
|
|
73
|
+
const normalized = slug.trim().toLowerCase();
|
|
74
|
+
if (!normalized)
|
|
75
|
+
return slug;
|
|
76
|
+
if (TOOL_LABELS[normalized])
|
|
77
|
+
return TOOL_LABELS[normalized];
|
|
78
|
+
return normalized
|
|
79
|
+
.split(/[-_\s]+/)
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
82
|
+
.join(" ");
|
|
83
|
+
}
|
|
84
|
+
export function formatToolList(tools) {
|
|
85
|
+
return tools.map(formatTool).join(", ");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pull `requires:` out of YAML frontmatter. Returns a deduped lowercase list
|
|
89
|
+
* or [] if absent / malformed.
|
|
90
|
+
*/
|
|
91
|
+
export function extractRequires(body) {
|
|
92
|
+
if (!body)
|
|
93
|
+
return [];
|
|
94
|
+
const trimmed = body.replace(/^/, "");
|
|
95
|
+
if (!trimmed.startsWith("---"))
|
|
96
|
+
return [];
|
|
97
|
+
const end = trimmed.indexOf("\n---", 3);
|
|
98
|
+
if (end === -1)
|
|
99
|
+
return [];
|
|
100
|
+
const headerBlock = trimmed.slice(3, end);
|
|
101
|
+
const lines = headerBlock.split(/\r?\n/);
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const out = [];
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
const raw = lines[i] ?? "";
|
|
106
|
+
const line = raw.trim();
|
|
107
|
+
const inlineMatch = /^requires\s*:\s*\[(.*?)\]\s*$/i.exec(line);
|
|
108
|
+
if (inlineMatch) {
|
|
109
|
+
const inner = inlineMatch[1] ?? "";
|
|
110
|
+
for (const part of inner.split(",")) {
|
|
111
|
+
const value = part.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
112
|
+
if (value && !seen.has(value)) {
|
|
113
|
+
seen.add(value);
|
|
114
|
+
out.push(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
if (/^requires\s*:\s*$/i.test(line)) {
|
|
120
|
+
// Block form. Scan subsequent indented `- foo` lines until indent breaks.
|
|
121
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
122
|
+
const next = lines[j] ?? "";
|
|
123
|
+
const itemMatch = /^\s+-\s+(.+?)\s*$/.exec(next);
|
|
124
|
+
if (!itemMatch) {
|
|
125
|
+
// First non-list line ends the block.
|
|
126
|
+
if (next.trim() === "")
|
|
127
|
+
continue;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const value = (itemMatch[1] ?? "").replace(/^['"]|['"]$/g, "").trim().toLowerCase();
|
|
131
|
+
if (value && !seen.has(value)) {
|
|
132
|
+
seen.add(value);
|
|
133
|
+
out.push(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
package/dist/library.js
CHANGED
|
@@ -44,7 +44,32 @@ export async function libraryCreate(opts) {
|
|
|
44
44
|
...(opts.visibility ? { visibility: opts.visibility } : {}),
|
|
45
45
|
});
|
|
46
46
|
process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
|
|
47
|
-
process.stdout.write(` ${c.dim("
|
|
47
|
+
process.stdout.write(` ${c.dim("API:")} ${apiUrl}/api/v1/libraries/${result.slug}\n`);
|
|
48
|
+
process.stdout.write(` ${c.dim("Sync:")} floom library subscribe ${result.slug}\n\n`);
|
|
49
|
+
}
|
|
50
|
+
export async function libraryAddSkill(opts) {
|
|
51
|
+
const cfg = await readConfig();
|
|
52
|
+
if (!cfg)
|
|
53
|
+
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
54
|
+
const apiUrl = cfg.apiUrl ?? getApiUrl();
|
|
55
|
+
await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
|
|
56
|
+
skill_slug: opts.skillSlug,
|
|
57
|
+
...(opts.folder !== undefined ? { folder: opts.folder } : {}),
|
|
58
|
+
...(opts.tags ? { tags: opts.tags } : {}),
|
|
59
|
+
});
|
|
60
|
+
const folderText = opts.folder ?? c.dim("(root)");
|
|
61
|
+
const tagsText = opts.tags?.length ? opts.tags.join(", ") : c.dim("(none)");
|
|
62
|
+
process.stdout.write(`\n${symbols.ok} Added ${c.cyan(opts.skillSlug)} to ${c.cyan(opts.librarySlug)}\n`);
|
|
63
|
+
process.stdout.write(` ${c.dim("folder:")} ${folderText}\n`);
|
|
64
|
+
process.stdout.write(` ${c.dim("tags:")} ${tagsText}\n\n`);
|
|
65
|
+
}
|
|
66
|
+
export async function libraryRemoveSkill(librarySlug, skillSlug) {
|
|
67
|
+
const cfg = await readConfig();
|
|
68
|
+
if (!cfg)
|
|
69
|
+
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
70
|
+
const apiUrl = cfg.apiUrl ?? getApiUrl();
|
|
71
|
+
await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
|
|
72
|
+
process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
|
|
48
73
|
}
|
|
49
74
|
export async function librarySubscribe(slug) {
|
|
50
75
|
const cfg = await readConfig();
|
package/dist/list.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import { getApiUrl, readConfig } from "./config.js";
|
|
3
3
|
import { getJson } from "./lib/api.js";
|
|
4
|
+
import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
|
|
4
5
|
import { c, symbols } from "./ui.js";
|
|
5
6
|
import { FloomError } from "./errors.js";
|
|
6
7
|
function formatRow(s) {
|
|
7
8
|
const title = s.title ?? c.dim("(untitled)");
|
|
8
9
|
const visibility = c.dim(`[${s.visibility}]`);
|
|
9
|
-
const type = c.dim(`[${s.asset_type
|
|
10
|
+
const type = c.dim(`[${formatType(s.asset_type)}]`);
|
|
10
11
|
const version = s.version ? c.dim(`v${s.version}`) : "";
|
|
11
12
|
const updated = c.dim(formatRelative(s.updated_at));
|
|
12
|
-
|
|
13
|
+
const requires = extractRequires(s.body_md);
|
|
14
|
+
const needs = requires.length > 0 ? c.dim(`needs ${formatToolList(requires)}`) : "";
|
|
15
|
+
return ` ${c.cyan(s.slug.padEnd(14))} ${title} ${type} ${visibility} ${version} ${updated}${needs ? ` ${needs}` : ""}`;
|
|
13
16
|
}
|
|
14
17
|
function formatRelative(iso) {
|
|
15
18
|
const diff = Date.now() - new Date(iso).getTime();
|
|
@@ -38,7 +41,7 @@ export async function list(opts) {
|
|
|
38
41
|
const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
|
|
39
42
|
let published = [];
|
|
40
43
|
try {
|
|
41
|
-
const mine = await getJson(`${apiUrl}/api/me/skills`, "load your skills", cfg.accessToken);
|
|
44
|
+
const mine = await getJson(`${apiUrl}/api/v1/me/skills`, "load your skills", cfg.accessToken);
|
|
42
45
|
published = mine.skills ?? [];
|
|
43
46
|
}
|
|
44
47
|
finally {
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { getApiUrl, readConfig } from "./config.js";
|
|
3
|
+
import { getJson } from "./lib/api.js";
|
|
4
|
+
import { c, symbols } from "./ui.js";
|
|
5
|
+
function formatSkillRow(skill) {
|
|
6
|
+
const title = skill.title ?? c.dim("(untitled)");
|
|
7
|
+
const type = c.dim(`[${skill.asset_type}]`);
|
|
8
|
+
const libs = skill.libraries.length
|
|
9
|
+
? c.dim(` ${skill.libraries.map((lib) => `@${lib.slug}`).join(", ")}`)
|
|
10
|
+
: "";
|
|
11
|
+
return ` ${c.cyan(skill.slug.padEnd(22))} ${title} ${type}${libs}`;
|
|
12
|
+
}
|
|
13
|
+
function formatLibraryRow(library) {
|
|
14
|
+
const count = `${library.skill_count} ${library.skill_count === 1 ? "skill" : "skills"}`;
|
|
15
|
+
return ` ${c.cyan(`@${library.slug}`.padEnd(22))} ${library.name} ${c.dim(count)}`;
|
|
16
|
+
}
|
|
17
|
+
export async function search(opts) {
|
|
18
|
+
const cfg = await readConfig();
|
|
19
|
+
const apiUrl = cfg?.apiUrl ?? getApiUrl();
|
|
20
|
+
const params = new URLSearchParams({ q: opts.query });
|
|
21
|
+
if (opts.library)
|
|
22
|
+
params.set("library", opts.library);
|
|
23
|
+
if (opts.type)
|
|
24
|
+
params.set("type", opts.type);
|
|
25
|
+
const spinner = opts.json ? null : ora({ text: c.dim("Searching Floom..."), color: "yellow" }).start();
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = await getJson(`${apiUrl}/api/v1/search?${params.toString()}`, "search skills");
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
spinner?.stop();
|
|
32
|
+
}
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write(`\n${symbols.dot} ${c.bold("Search results")} ${c.dim(`for "${result.query}"`)}\n\n`);
|
|
38
|
+
if (result.skills.length === 0 && result.libraries.length === 0) {
|
|
39
|
+
process.stdout.write(` ${c.dim("No matching public skills or libraries.")}\n\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (result.skills.length > 0) {
|
|
43
|
+
process.stdout.write(` ${c.dim("Skills")}\n`);
|
|
44
|
+
for (const skill of result.skills)
|
|
45
|
+
process.stdout.write(`${formatSkillRow(skill)}\n`);
|
|
46
|
+
process.stdout.write("\n");
|
|
47
|
+
}
|
|
48
|
+
if (result.libraries.length > 0) {
|
|
49
|
+
process.stdout.write(` ${c.dim("Libraries")}\n`);
|
|
50
|
+
for (const library of result.libraries)
|
|
51
|
+
process.stdout.write(`${formatLibraryRow(library)}\n`);
|
|
52
|
+
process.stdout.write("\n");
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { access, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { FloomError } from "./errors.js";
|
|
7
|
+
import { c, symbols } from "./ui.js";
|
|
8
|
+
const START_MARKER = "<!-- FLOOM SETUP START -->";
|
|
9
|
+
const END_MARKER = "<!-- FLOOM SETUP END -->";
|
|
10
|
+
const TARGETS = {
|
|
11
|
+
claude: { label: "Claude Code", filename: "CLAUDE.md" },
|
|
12
|
+
codex: { label: "Codex", filename: "AGENTS.md" },
|
|
13
|
+
};
|
|
14
|
+
export const FLOOM_AGENT_INSTRUCTIONS = `${START_MARKER}
|
|
15
|
+
## Floom
|
|
16
|
+
|
|
17
|
+
- Before reinventing agent workflows, use Floom to search, list, add, and sync skills.
|
|
18
|
+
- Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
|
|
19
|
+
- Add reusable workflows with \`floom add <slug-or-url>\`.
|
|
20
|
+
- Keep local skills current with \`floom sync\`.
|
|
21
|
+
- For MCP tools, run \`floom mcp\` for setup guidance and add \`@floomhq/floom-mcp-sync\` only after reviewing your local agent config.
|
|
22
|
+
${END_MARKER}`;
|
|
23
|
+
async function fileExists(path) {
|
|
24
|
+
try {
|
|
25
|
+
await access(path);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readIfExists(path) {
|
|
33
|
+
if (!(await fileExists(path)))
|
|
34
|
+
return null;
|
|
35
|
+
const stat = await lstat(path);
|
|
36
|
+
if (stat.isSymbolicLink()) {
|
|
37
|
+
throw new FloomError("Refusing to update an instruction file that is a symbolic link.", `Inspect ${path}, then pass a regular file path.`);
|
|
38
|
+
}
|
|
39
|
+
if (!stat.isFile()) {
|
|
40
|
+
throw new FloomError("Instruction target is not a file.", path);
|
|
41
|
+
}
|
|
42
|
+
return readFile(path, "utf8");
|
|
43
|
+
}
|
|
44
|
+
function parseTargetFromFile(file) {
|
|
45
|
+
const upper = file.toUpperCase();
|
|
46
|
+
if (upper.endsWith("CLAUDE.MD"))
|
|
47
|
+
return "claude";
|
|
48
|
+
if (upper.endsWith("AGENTS.MD"))
|
|
49
|
+
return "codex";
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
async function findUp(filename) {
|
|
53
|
+
let dir = process.cwd();
|
|
54
|
+
const home = resolve(homedir());
|
|
55
|
+
while (true) {
|
|
56
|
+
const candidate = join(dir, filename);
|
|
57
|
+
if (await fileExists(candidate))
|
|
58
|
+
return candidate;
|
|
59
|
+
const parent = dirname(dir);
|
|
60
|
+
if (dir === parent || dir === home)
|
|
61
|
+
return null;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function detectTarget(opts) {
|
|
66
|
+
const agent = opts.target ?? (opts.file ? parseTargetFromFile(opts.file) : undefined);
|
|
67
|
+
if (opts.file) {
|
|
68
|
+
const resolvedAgent = agent ?? "codex";
|
|
69
|
+
return {
|
|
70
|
+
agent: resolvedAgent,
|
|
71
|
+
label: TARGETS[resolvedAgent].label,
|
|
72
|
+
path: resolve(process.cwd(), opts.file),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (agent) {
|
|
76
|
+
const existing = await findUp(TARGETS[agent].filename);
|
|
77
|
+
return {
|
|
78
|
+
agent,
|
|
79
|
+
label: TARGETS[agent].label,
|
|
80
|
+
path: existing ?? resolve(process.cwd(), TARGETS[agent].filename),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const claude = await findUp(TARGETS.claude.filename);
|
|
84
|
+
const codex = await findUp(TARGETS.codex.filename);
|
|
85
|
+
if (claude && codex) {
|
|
86
|
+
throw new FloomError("Found both Claude Code and Codex instruction files.", "Pass `--target claude` or `--target codex`.");
|
|
87
|
+
}
|
|
88
|
+
if (claude)
|
|
89
|
+
return { agent: "claude", label: TARGETS.claude.label, path: claude };
|
|
90
|
+
if (codex)
|
|
91
|
+
return { agent: "codex", label: TARGETS.codex.label, path: codex };
|
|
92
|
+
throw new FloomError("No agent instruction file found.", "Run `floom setup --target claude --yes` or `floom setup --target codex --yes` from the repo root.");
|
|
93
|
+
}
|
|
94
|
+
function renderPreview(target, existing) {
|
|
95
|
+
const action = existing === null ? "create" : "append";
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
`${symbols.bullet} Floom setup preview for ${c.bold(target.label)}`,
|
|
99
|
+
` ${c.dim("Target:")} ${target.path}`,
|
|
100
|
+
` ${c.dim("Action:")} ${action}`,
|
|
101
|
+
"",
|
|
102
|
+
FLOOM_AGENT_INSTRUCTIONS,
|
|
103
|
+
"",
|
|
104
|
+
`${c.dim("MCP setup guidance:")} run ${c.cyan("floom mcp")} to print local agent commands.`,
|
|
105
|
+
"",
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
async function confirmWrite(target, existing) {
|
|
109
|
+
if (!process.stdin.isTTY) {
|
|
110
|
+
throw new FloomError("Refusing to update agent instructions without confirmation in non-interactive mode.", "Pass `--yes` to write, or `--dry-run` to preview.");
|
|
111
|
+
}
|
|
112
|
+
const action = existing === null ? "Create" : "Append to";
|
|
113
|
+
const rl = createInterface({ input, output });
|
|
114
|
+
try {
|
|
115
|
+
const answer = (await rl.question(` ${action} ${target.path} for ${target.label}? ${c.dim("(y/N)")} `)).trim().toLowerCase();
|
|
116
|
+
return answer === "y" || answer === "yes";
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function setupAgent(opts) {
|
|
123
|
+
const target = await detectTarget(opts);
|
|
124
|
+
const existing = await readIfExists(target.path);
|
|
125
|
+
if (existing?.includes(START_MARKER) && existing.includes(END_MARKER)) {
|
|
126
|
+
process.stdout.write(`\n${symbols.ok} Floom instructions already present in ${c.bold(target.path)}\n\n`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (opts.dryRun) {
|
|
130
|
+
process.stdout.write(renderPreview(target, existing));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!opts.yes) {
|
|
134
|
+
process.stdout.write(renderPreview(target, existing));
|
|
135
|
+
const ok = await confirmWrite(target, existing);
|
|
136
|
+
if (!ok) {
|
|
137
|
+
process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await mkdir(dirname(target.path), { recursive: true });
|
|
142
|
+
const next = existing === null
|
|
143
|
+
? `${FLOOM_AGENT_INSTRUCTIONS}\n`
|
|
144
|
+
: `${existing.replace(/\s*$/, "")}\n\n${FLOOM_AGENT_INSTRUCTIONS}\n`;
|
|
145
|
+
if (existing === null) {
|
|
146
|
+
await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
|
|
147
|
+
if (err instanceof Error && "code" in err && err.code === "EEXIST") {
|
|
148
|
+
throw new FloomError("Instruction file appeared while setup was running.", "Re-run `floom setup` so Floom can inspect the current file before writing.");
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
await writeFile(target.path, next, "utf8");
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
|
|
157
|
+
process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("floom mcp")}\n\n`);
|
|
158
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -211,7 +211,7 @@ export async function sync(opts = {}) {
|
|
|
211
211
|
const spinner = opts.spinner === false ? null : ora({ text: c.dim("Syncing skills..."), color: "yellow" }).start();
|
|
212
212
|
let payload;
|
|
213
213
|
try {
|
|
214
|
-
payload = await getJson(`${apiUrl}/api/me/skills`, "load your skills", cfg.accessToken);
|
|
214
|
+
payload = await getJson(`${apiUrl}/api/v1/me/skills`, "load your skills", cfg.accessToken);
|
|
215
215
|
}
|
|
216
216
|
catch (err) {
|
|
217
217
|
spinner?.stop();
|
|
@@ -223,7 +223,7 @@ export async function sync(opts = {}) {
|
|
|
223
223
|
}
|
|
224
224
|
for (const skill of payload.skills)
|
|
225
225
|
validateSyncSkillShape(skill);
|
|
226
|
-
// Version 1 preview syncs
|
|
226
|
+
// Version 1 preview syncs published, saved, and subscribed library skills.
|
|
227
227
|
const all = payload.skills;
|
|
228
228
|
const seen = new Set();
|
|
229
229
|
let unchanged = 0;
|