@floomhq/floom 1.0.1 → 1.0.3
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 +7 -0
- package/dist/cli.js +103 -12
- package/dist/doctor.js +1 -1
- package/dist/list.js +1 -1
- package/dist/mcp.js +5 -4
- package/dist/search.js +54 -0
- package/dist/setup.js +158 -0
- package/dist/sync.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,12 @@ 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
|
|
11
14
|
floom library list
|
|
12
15
|
```
|
|
@@ -21,6 +24,10 @@ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the UR
|
|
|
21
24
|
- `floom list` — show your published skills. Optional `--json`.
|
|
22
25
|
- `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
|
|
23
26
|
- `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
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.
|
|
24
31
|
- `floom sync` — preview: pull your published, saved, and subscribed library skills into `~/.claude/skills/`.
|
|
25
32
|
- `floom watch` — preview: run `floom sync` repeatedly. Optional `--interval <seconds>`; minimum `10`.
|
|
26
33
|
- `floom library list` — list public starter libraries. Optional `--json`.
|
package/dist/cli.js
CHANGED
|
@@ -11,20 +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";
|
|
14
17
|
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
15
18
|
import { c, symbols } from "./ui.js";
|
|
16
19
|
import { printError, FloomError } from "./errors.js";
|
|
17
|
-
const VERSION = "1.0.
|
|
20
|
+
const VERSION = "1.0.3";
|
|
18
21
|
const PKG = { name: "@floomhq/floom", version: VERSION };
|
|
19
22
|
const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
|
|
20
23
|
function usage() {
|
|
21
24
|
const out = `
|
|
22
|
-
${c.coral("
|
|
23
|
-
${c.coral("
|
|
24
|
-
${c.coral("
|
|
25
|
-
${c.coral("
|
|
26
|
-
${c.coral(" /_/ |_|\\___/_/\\__,_/\\__, /")} ${c.dim(`v${VERSION}`)}
|
|
27
|
-
${c.coral(" /____/")}
|
|
25
|
+
${c.coral(" __ _")}
|
|
26
|
+
${c.coral(" / _| |___ ___ _ __")} ${c.dim(`v${VERSION}`)}
|
|
27
|
+
${c.coral("| _| / _ \\/ _ \\ ' \\")}
|
|
28
|
+
${c.coral("|_| |_\\___/\\___/_|_|_|")}
|
|
28
29
|
|
|
29
30
|
${c.bold("Share AI agent skills with a link.")}
|
|
30
31
|
${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
|
|
@@ -33,16 +34,22 @@ function usage() {
|
|
|
33
34
|
${c.cyan("floom")} ${c.dim("<command> [options]")}
|
|
34
35
|
|
|
35
36
|
${c.bold("Start Here")}
|
|
37
|
+
${c.dim("Try a shared skill, no account needed:")}
|
|
36
38
|
${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
|
|
37
|
-
${c.dim("Install a public skill. No account needed.")}
|
|
38
39
|
|
|
39
|
-
${c.
|
|
40
|
-
|
|
40
|
+
${c.dim("Publish your own skill:")}
|
|
41
|
+
${c.cyan("floom init")} ${c.dim("support-tone.md")}
|
|
42
|
+
${c.cyan("floom login")}
|
|
43
|
+
${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
|
|
44
|
+
|
|
45
|
+
${c.dim("Tell your agent where Floom installs skills:")}
|
|
46
|
+
${c.cyan("floom setup")} ${c.dim("--target claude --dry-run")}
|
|
41
47
|
|
|
42
48
|
${c.bold("Commands")}
|
|
43
|
-
|
|
49
|
+
${c.dim("Receive")}
|
|
44
50
|
${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
|
|
45
51
|
${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
|
|
52
|
+
${c.cyan("search")} ${c.dim("<query>")} Search public skills and libraries ${c.dim("[--json]")}
|
|
46
53
|
|
|
47
54
|
${c.dim("Create")}
|
|
48
55
|
${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
|
|
@@ -65,6 +72,9 @@ function usage() {
|
|
|
65
72
|
${c.cyan("logout")} Delete local credentials
|
|
66
73
|
|
|
67
74
|
${c.dim("System")}
|
|
75
|
+
${c.cyan("setup")} ${c.dim("[--target claude|codex] [--file path]")} Add Floom guidance to agent instructions
|
|
76
|
+
${c.cyan("connect")} ${c.dim("[--target claude|codex]")} Alias for setup
|
|
77
|
+
${c.cyan("mcp")} Print MCP setup guidance
|
|
68
78
|
${c.cyan("sync")} Preview: pull published, saved, and library skills
|
|
69
79
|
${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
|
|
70
80
|
${c.cyan("doctor")} Diagnose auth, API, and local setup
|
|
@@ -168,6 +178,36 @@ function parseInfoFlags(argv) {
|
|
|
168
178
|
}
|
|
169
179
|
return out;
|
|
170
180
|
}
|
|
181
|
+
function parseSearchFlags(argv) {
|
|
182
|
+
const out = { json: false };
|
|
183
|
+
const terms = [];
|
|
184
|
+
for (let i = 0; i < argv.length; i++) {
|
|
185
|
+
const a = argv[i] ?? "";
|
|
186
|
+
if (a === "--json")
|
|
187
|
+
out.json = true;
|
|
188
|
+
else if (a === "--library" || a.startsWith("--library=")) {
|
|
189
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--library");
|
|
190
|
+
out.library = value;
|
|
191
|
+
i = nextIndex;
|
|
192
|
+
}
|
|
193
|
+
else if (a === "--type" || a.startsWith("--type=")) {
|
|
194
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--type");
|
|
195
|
+
if (!ASSET_TYPES.has(value)) {
|
|
196
|
+
throw new FloomError(`Invalid --type: ${value}`, "Use one of: knowledge, instruction, workflow, skill.");
|
|
197
|
+
}
|
|
198
|
+
out.type = value;
|
|
199
|
+
i = nextIndex;
|
|
200
|
+
}
|
|
201
|
+
else if (a.startsWith("--")) {
|
|
202
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
terms.push(a);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
out.query = terms.join(" ").trim();
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
171
211
|
function parseDeleteFlags(argv) {
|
|
172
212
|
const out = { yes: false };
|
|
173
213
|
for (const a of argv) {
|
|
@@ -180,6 +220,37 @@ function parseDeleteFlags(argv) {
|
|
|
180
220
|
}
|
|
181
221
|
return out;
|
|
182
222
|
}
|
|
223
|
+
function parseSetupFlags(argv) {
|
|
224
|
+
const out = { dryRun: false, yes: false };
|
|
225
|
+
for (let i = 0; i < argv.length; i++) {
|
|
226
|
+
const a = argv[i] ?? "";
|
|
227
|
+
if (a === "--dry-run" || a === "--preview")
|
|
228
|
+
out.dryRun = true;
|
|
229
|
+
else if (a === "--yes" || a === "-y")
|
|
230
|
+
out.yes = true;
|
|
231
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
232
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
233
|
+
if (value !== "claude" && value !== "codex") {
|
|
234
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
235
|
+
}
|
|
236
|
+
out.target = value;
|
|
237
|
+
i = nextIndex;
|
|
238
|
+
}
|
|
239
|
+
else if (a === "--file" || a.startsWith("--file=")) {
|
|
240
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--file");
|
|
241
|
+
out.file = value;
|
|
242
|
+
i = nextIndex;
|
|
243
|
+
}
|
|
244
|
+
else if (a.startsWith("--")) {
|
|
245
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
|
|
246
|
+
}
|
|
247
|
+
else if (!out.file)
|
|
248
|
+
out.file = a;
|
|
249
|
+
else
|
|
250
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
183
254
|
function normalizeFolder(value) {
|
|
184
255
|
return value === "root" || value === "/" || value === "." ? null : value;
|
|
185
256
|
}
|
|
@@ -440,6 +511,19 @@ async function main() {
|
|
|
440
511
|
await info({ slug: flags.slug ?? "", json: flags.json });
|
|
441
512
|
}
|
|
442
513
|
return;
|
|
514
|
+
case "search": {
|
|
515
|
+
const flags = parseSearchFlags(rest);
|
|
516
|
+
if (!flags.query) {
|
|
517
|
+
throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
|
|
518
|
+
}
|
|
519
|
+
await search({
|
|
520
|
+
query: flags.query,
|
|
521
|
+
...(flags.library ? { library: flags.library } : {}),
|
|
522
|
+
...(flags.type ? { type: flags.type } : {}),
|
|
523
|
+
json: flags.json,
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
443
527
|
case "add":
|
|
444
528
|
case "install": {
|
|
445
529
|
const slug = rest.find((a) => !a.startsWith("--"));
|
|
@@ -452,6 +536,12 @@ async function main() {
|
|
|
452
536
|
case "sync":
|
|
453
537
|
await sync();
|
|
454
538
|
return;
|
|
539
|
+
case "setup":
|
|
540
|
+
case "connect": {
|
|
541
|
+
const flags = parseSetupFlags(rest);
|
|
542
|
+
await setupAgent(flags);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
455
545
|
case "watch": {
|
|
456
546
|
const flags = parseWatchFlags(rest);
|
|
457
547
|
await watch(flags.intervalSeconds);
|
|
@@ -480,7 +570,8 @@ async function main() {
|
|
|
480
570
|
return;
|
|
481
571
|
}
|
|
482
572
|
case "mcp":
|
|
483
|
-
|
|
573
|
+
printMcpSetup();
|
|
574
|
+
return;
|
|
484
575
|
case "doctor":
|
|
485
576
|
await doctor();
|
|
486
577
|
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/list.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function list(opts) {
|
|
|
41
41
|
const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
|
|
42
42
|
let published = [];
|
|
43
43
|
try {
|
|
44
|
-
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);
|
|
45
45
|
published = mine.skills ?? [];
|
|
46
46
|
}
|
|
47
47
|
finally {
|
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { c } from "./ui.js";
|
|
2
2
|
export function printMcpSetup() {
|
|
3
3
|
const snippet = `## Floom
|
|
4
|
-
-
|
|
5
|
-
- To install a skill
|
|
6
|
-
- To
|
|
4
|
+
- Use Floom skills from ~/.claude/skills/ when they match the task.
|
|
5
|
+
- To install a shared skill, run \`floom add <slug-or-url>\`.
|
|
6
|
+
- To find reusable behavior, run \`floom search <query>\`.
|
|
7
|
+
- MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
|
|
7
8
|
process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
|
|
8
9
|
process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
|
|
9
10
|
process.stdout.write(` ${c.bold("Claude Code")}\n`);
|
|
@@ -17,6 +18,6 @@ export function printMcpSetup() {
|
|
|
17
18
|
process.stdout.write(` ${c.bold("OpenCode")}\n`);
|
|
18
19
|
process.stdout.write(` ${c.dim("Edit ~/.config/opencode/opencode.json - see guide.")}\n\n`);
|
|
19
20
|
process.stdout.write(`${c.dim("Full guide:")} ${c.cyan("https://floom.dev/docs/mcp")}\n\n`);
|
|
20
|
-
process.stdout.write(`${c.dim("Recommended
|
|
21
|
+
process.stdout.write(`${c.dim("Recommended agent instruction snippet:")}\n\n`);
|
|
21
22
|
process.stdout.write(`${snippet}\n\n`);
|
|
22
23
|
}
|
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 recreating agent behavior from scratch, check Floom for reusable skills.
|
|
18
|
+
- Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
|
|
19
|
+
- Add shared skills with \`floom add <slug-or-url>\`; public and unlisted links do not require a Floom account.
|
|
20
|
+
- Use installed Markdown skills from the local skills folder when they match the task.
|
|
21
|
+
- \`floom sync\`, \`floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
|
|
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();
|