@floomhq/floom 1.0.17 → 1.0.19
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/dist/cli.js +84 -14
- package/dist/doctor.js +16 -3
- package/dist/install.js +63 -15
- package/dist/login.js +13 -2
- package/dist/secrets.js +1 -1
- package/dist/targets.js +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -71,7 +71,9 @@ function commandUsage() {
|
|
|
71
71
|
${c.dim("Skills")}
|
|
72
72
|
${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
|
|
73
73
|
${c.dim("Alias: install")}
|
|
74
|
-
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
|
|
74
|
+
${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --json")}
|
|
75
|
+
${c.cyan("update")} ${c.dim("<url>")} Refresh or migrate a local skill
|
|
76
|
+
${c.dim("Flags: --target claude|codex (default: claude), --json")}
|
|
75
77
|
${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
|
|
76
78
|
${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
|
|
77
79
|
|
|
@@ -98,7 +100,7 @@ function commandUsage() {
|
|
|
98
100
|
${c.dim("Alias: connect")}
|
|
99
101
|
${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
|
|
100
102
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
101
|
-
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
103
|
+
${c.dim("Flags: --target claude|codex (default: claude), --json")}
|
|
102
104
|
|
|
103
105
|
${c.dim("Advanced")}
|
|
104
106
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
@@ -108,6 +110,10 @@ function commandUsage() {
|
|
|
108
110
|
${c.cyan("sync")} Preview pull of published, saved, and library skills
|
|
109
111
|
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
110
112
|
${c.cyan("watch")} Preview polling sync loop
|
|
113
|
+
${c.dim("Flags: --target claude|codex (default: claude), --interval <seconds>")}
|
|
114
|
+
${c.cyan("agent-prompt")} Print the one-line agent instruction
|
|
115
|
+
${c.dim("Alias: paste")}
|
|
116
|
+
${c.dim("Flags: --target claude|codex (default: claude)")}
|
|
111
117
|
|
|
112
118
|
${c.bold("Examples")}
|
|
113
119
|
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
|
|
@@ -312,6 +318,7 @@ function parseAddArgs(argv) {
|
|
|
312
318
|
let target;
|
|
313
319
|
let setup = false;
|
|
314
320
|
let force = false;
|
|
321
|
+
let json = false;
|
|
315
322
|
for (let i = 0; i < argv.length; i++) {
|
|
316
323
|
const a = argv[i] ?? "";
|
|
317
324
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -328,6 +335,9 @@ function parseAddArgs(argv) {
|
|
|
328
335
|
else if (a === "--force") {
|
|
329
336
|
force = true;
|
|
330
337
|
}
|
|
338
|
+
else if (a === "--json") {
|
|
339
|
+
json = true;
|
|
340
|
+
}
|
|
331
341
|
else if (a.startsWith("--")) {
|
|
332
342
|
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} add <url-or-slug> --setup\`.`);
|
|
333
343
|
}
|
|
@@ -340,7 +350,10 @@ function parseAddArgs(argv) {
|
|
|
340
350
|
if (!slug) {
|
|
341
351
|
throw new FloomError("Missing skill slug.", `Try: \`${CLI_COMMAND} add <url-or-slug> --setup\``);
|
|
342
352
|
}
|
|
343
|
-
|
|
353
|
+
if (json && setup) {
|
|
354
|
+
throw new FloomError("Cannot combine --json with --setup.", "Run the install first, then run `npx -y @floomhq/floom setup --target claude --yes`.");
|
|
355
|
+
}
|
|
356
|
+
return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
|
|
344
357
|
}
|
|
345
358
|
function parseSearchFlags(argv) {
|
|
346
359
|
const out = { json: false };
|
|
@@ -426,7 +439,7 @@ function parseSetupFlags(argv) {
|
|
|
426
439
|
return out;
|
|
427
440
|
}
|
|
428
441
|
function parseDoctorFlags(argv) {
|
|
429
|
-
const out = {};
|
|
442
|
+
const out = { json: false };
|
|
430
443
|
for (let i = 0; i < argv.length; i++) {
|
|
431
444
|
const a = argv[i] ?? "";
|
|
432
445
|
if (a === "--target" || a.startsWith("--target=")) {
|
|
@@ -437,8 +450,11 @@ function parseDoctorFlags(argv) {
|
|
|
437
450
|
out.target = value;
|
|
438
451
|
i = nextIndex;
|
|
439
452
|
}
|
|
453
|
+
else if (a === "--json") {
|
|
454
|
+
out.json = true;
|
|
455
|
+
}
|
|
440
456
|
else if (a.startsWith("--")) {
|
|
441
|
-
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex\`.`);
|
|
457
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} doctor --target codex --json\`.`);
|
|
442
458
|
}
|
|
443
459
|
else {
|
|
444
460
|
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} doctor --target claude\`.`);
|
|
@@ -598,11 +614,19 @@ function parseWatchFlags(argv) {
|
|
|
598
614
|
out.intervalSeconds = interval;
|
|
599
615
|
i = nextIndex;
|
|
600
616
|
}
|
|
617
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
618
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
619
|
+
if (value !== "claude" && value !== "codex") {
|
|
620
|
+
throw new FloomError("Invalid --target.", "Use claude or codex.");
|
|
621
|
+
}
|
|
622
|
+
out.target = value;
|
|
623
|
+
i = nextIndex;
|
|
624
|
+
}
|
|
601
625
|
else if (a.startsWith("--")) {
|
|
602
|
-
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
|
|
626
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
|
|
603
627
|
}
|
|
604
628
|
else {
|
|
605
|
-
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --interval 60\`.`);
|
|
629
|
+
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} watch --target claude --interval 60\`.`);
|
|
606
630
|
}
|
|
607
631
|
}
|
|
608
632
|
return out;
|
|
@@ -610,6 +634,31 @@ function parseWatchFlags(argv) {
|
|
|
610
634
|
function notAvailable(feature) {
|
|
611
635
|
throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
|
|
612
636
|
}
|
|
637
|
+
function parseAgentPromptFlags(argv) {
|
|
638
|
+
const out = {};
|
|
639
|
+
for (let i = 0; i < argv.length; i++) {
|
|
640
|
+
const a = argv[i] ?? "";
|
|
641
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
642
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
643
|
+
if (value !== "claude" && value !== "codex") {
|
|
644
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
645
|
+
}
|
|
646
|
+
out.target = value;
|
|
647
|
+
i = nextIndex;
|
|
648
|
+
}
|
|
649
|
+
else if (a.startsWith("--")) {
|
|
650
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target codex\`.`);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} agent-prompt --target claude\`.`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
function printAgentPrompt(target = "claude") {
|
|
659
|
+
const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
|
|
660
|
+
process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
|
|
661
|
+
}
|
|
613
662
|
function parseSingleFileArg(argv, usageHint) {
|
|
614
663
|
let file;
|
|
615
664
|
for (const a of argv) {
|
|
@@ -634,7 +683,7 @@ function sleep(ms, signal) {
|
|
|
634
683
|
}, { once: true });
|
|
635
684
|
});
|
|
636
685
|
}
|
|
637
|
-
async function watch(intervalSeconds) {
|
|
686
|
+
async function watch(intervalSeconds, target) {
|
|
638
687
|
const cfg = await readConfig();
|
|
639
688
|
if (!cfg) {
|
|
640
689
|
throw new FloomError("Not signed in.", `Run \`${CLI_COMMAND} login\` before \`${CLI_COMMAND} watch\`, or use \`${CLI_COMMAND} add <link>\` without an account.`);
|
|
@@ -651,9 +700,9 @@ async function watch(intervalSeconds) {
|
|
|
651
700
|
};
|
|
652
701
|
process.on("SIGINT", stop);
|
|
653
702
|
process.on("SIGTERM", stop);
|
|
654
|
-
process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
703
|
+
process.stdout.write(`${symbols.bullet} Watching Floom sync for ${target ?? "claude"} every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
655
704
|
while (!controller.signal.aborted) {
|
|
656
|
-
await sync({ spinner: false, quietUnchanged: true });
|
|
705
|
+
await sync({ spinner: false, quietUnchanged: true, ...(target ? { target } : {}) });
|
|
657
706
|
await sleep(intervalSeconds * 1000, controller.signal);
|
|
658
707
|
}
|
|
659
708
|
}
|
|
@@ -673,10 +722,10 @@ async function main() {
|
|
|
673
722
|
// never block on update-notifier
|
|
674
723
|
}
|
|
675
724
|
}
|
|
676
|
-
// Subcommand --help: any rest arg = --help/-h/help → show
|
|
677
|
-
// Subcommands are simple enough that one
|
|
725
|
+
// Subcommand --help: any rest arg = --help/-h/help → show the command reference.
|
|
726
|
+
// Subcommands are simple enough that one reference screen is fine for Version 1.
|
|
678
727
|
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
679
|
-
|
|
728
|
+
commandUsage();
|
|
680
729
|
return;
|
|
681
730
|
}
|
|
682
731
|
try {
|
|
@@ -781,6 +830,20 @@ async function main() {
|
|
|
781
830
|
...(flags.target ? { target: flags.target } : {}),
|
|
782
831
|
setup: flags.setup,
|
|
783
832
|
force: flags.force,
|
|
833
|
+
json: flags.json,
|
|
834
|
+
});
|
|
835
|
+
if (flags.setup) {
|
|
836
|
+
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
837
|
+
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
case "update": {
|
|
841
|
+
const flags = parseAddArgs(rest);
|
|
842
|
+
await install(flags.slug, {
|
|
843
|
+
...(flags.target ? { target: flags.target } : {}),
|
|
844
|
+
setup: flags.setup,
|
|
845
|
+
force: true,
|
|
846
|
+
json: flags.json,
|
|
784
847
|
});
|
|
785
848
|
if (flags.setup) {
|
|
786
849
|
await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
|
|
@@ -798,7 +861,7 @@ async function main() {
|
|
|
798
861
|
}
|
|
799
862
|
case "watch": {
|
|
800
863
|
const flags = parseWatchFlags(rest);
|
|
801
|
-
await watch(flags.intervalSeconds);
|
|
864
|
+
await watch(flags.intervalSeconds, flags.target);
|
|
802
865
|
return;
|
|
803
866
|
}
|
|
804
867
|
case "delete":
|
|
@@ -830,6 +893,13 @@ async function main() {
|
|
|
830
893
|
rejectArgs(rest, `Try \`${CLI_COMMAND} mcp\`.`);
|
|
831
894
|
printMcpSetup();
|
|
832
895
|
return;
|
|
896
|
+
case "agent-prompt":
|
|
897
|
+
case "paste":
|
|
898
|
+
{
|
|
899
|
+
const flags = parseAgentPromptFlags(rest);
|
|
900
|
+
printAgentPrompt(flags.target);
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
833
903
|
case "doctor":
|
|
834
904
|
await doctor(parseDoctorFlags(rest));
|
|
835
905
|
return;
|
package/dist/doctor.js
CHANGED
|
@@ -335,7 +335,6 @@ async function readExecutableVersion(path) {
|
|
|
335
335
|
}
|
|
336
336
|
export async function doctor(opts = {}) {
|
|
337
337
|
const target = opts.target ?? "claude";
|
|
338
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
339
338
|
const checks = await Promise.all([
|
|
340
339
|
checkCliCommand(),
|
|
341
340
|
checkAuth(),
|
|
@@ -344,6 +343,22 @@ export async function doctor(opts = {}) {
|
|
|
344
343
|
checkLastSync(target),
|
|
345
344
|
checkVersion(),
|
|
346
345
|
]);
|
|
346
|
+
const anyFail = checks.some((check) => check.status === "fail");
|
|
347
|
+
const anyWarn = checks.some((check) => check.status === "warn");
|
|
348
|
+
if (opts.json) {
|
|
349
|
+
process.stdout.write(`${JSON.stringify({
|
|
350
|
+
ok: !anyFail,
|
|
351
|
+
status: anyFail ? "fail" : anyWarn ? "warn" : "ok",
|
|
352
|
+
version: CLI_VERSION,
|
|
353
|
+
target,
|
|
354
|
+
checks,
|
|
355
|
+
configPath: CONFIG_PATH,
|
|
356
|
+
})}\n`);
|
|
357
|
+
if (anyFail)
|
|
358
|
+
process.exit(1);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
347
362
|
// Compute column widths for clean table output.
|
|
348
363
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
349
364
|
for (const check of checks) {
|
|
@@ -353,8 +368,6 @@ export async function doctor(opts = {}) {
|
|
|
353
368
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
354
369
|
}
|
|
355
370
|
}
|
|
356
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
357
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
358
371
|
process.stdout.write("\n");
|
|
359
372
|
if (anyFail) {
|
|
360
373
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
package/dist/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readdir, rmdir, unlink } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ora from "ora";
|
|
@@ -66,12 +66,14 @@ async function readLocalFile(path) {
|
|
|
66
66
|
throw err;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
async function
|
|
69
|
+
async function localPackageState(root, slug, target, files) {
|
|
70
70
|
const main = await readLocalFile(target);
|
|
71
71
|
if (main === null) {
|
|
72
72
|
const legacy = await readLocalFile(legacySkillPath(root, slug));
|
|
73
|
-
if (legacy !== null &&
|
|
74
|
-
return
|
|
73
|
+
if (legacy !== null && legacy.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
if (legacy !== null)
|
|
76
|
+
return { hash: packageHash(legacy.toString("utf8"), []), source: "legacy" };
|
|
75
77
|
return null;
|
|
76
78
|
}
|
|
77
79
|
const localFiles = [];
|
|
@@ -81,7 +83,7 @@ async function localPackageHash(root, slug, target, files) {
|
|
|
81
83
|
return null;
|
|
82
84
|
localFiles.push({ path: file.path, bytes, sha256: file.sha256 });
|
|
83
85
|
}
|
|
84
|
-
return packageHash(main.toString("utf8"), localFiles);
|
|
86
|
+
return { hash: packageHash(main.toString("utf8"), localFiles), source: "native" };
|
|
85
87
|
}
|
|
86
88
|
async function markInstallSynced(root, slug, files) {
|
|
87
89
|
const manifest = await readSyncManifest();
|
|
@@ -119,6 +121,33 @@ async function writeInstallFile(root, target, body) {
|
|
|
119
121
|
await parent.close();
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
async function removeLegacySkillFile(root, slug) {
|
|
125
|
+
try {
|
|
126
|
+
await unlink(legacySkillPath(root, slug));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const code = err.code;
|
|
130
|
+
if (code === "ENOENT")
|
|
131
|
+
return;
|
|
132
|
+
if (code === "EISDIR" || code === "ELOOP" || code === "EPERM")
|
|
133
|
+
return;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function removeEmptyNativeSkillDir(root, slug) {
|
|
138
|
+
try {
|
|
139
|
+
const dir = dirname(skillPath(root, slug));
|
|
140
|
+
const entries = await readdir(dir);
|
|
141
|
+
if (entries.length === 0)
|
|
142
|
+
await rmdir(dir);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const code = err.code;
|
|
146
|
+
if (code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST")
|
|
147
|
+
return;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
122
151
|
async function overwriteInstallFile(root, target, body) {
|
|
123
152
|
const parent = await openSafeParentDirectory(root, target);
|
|
124
153
|
const handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
|
|
@@ -203,7 +232,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
203
232
|
}
|
|
204
233
|
const cfg = await readConfig();
|
|
205
234
|
const apiUrl = resolveApiUrl(cfg);
|
|
206
|
-
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
235
|
+
const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
207
236
|
let detail;
|
|
208
237
|
try {
|
|
209
238
|
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
@@ -212,7 +241,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
212
241
|
}
|
|
213
242
|
}
|
|
214
243
|
catch (err) {
|
|
215
|
-
spinner
|
|
244
|
+
spinner?.stop();
|
|
216
245
|
throw err;
|
|
217
246
|
}
|
|
218
247
|
const target = skillPath(root, slug);
|
|
@@ -238,21 +267,29 @@ export async function install(slugInput, opts = {}) {
|
|
|
238
267
|
}
|
|
239
268
|
throw err;
|
|
240
269
|
}
|
|
241
|
-
await
|
|
242
|
-
const existing = await localPackageHash(root, slug, target, remotePackageFiles);
|
|
270
|
+
const existing = await localPackageState(root, slug, target, remotePackageFiles);
|
|
243
271
|
const conflictingTarget = await preflightInstallPackage(root, installFiles, opts.force ? { force: true } : {});
|
|
244
272
|
if (conflictingTarget) {
|
|
245
273
|
throw new FloomError("Local skill already exists with different content.", `Run \`npx -y @floomhq/floom add <link> --force\` to replace it, or move the local file first: ${relative(root, conflictingTarget).split(sep).join("/")}`);
|
|
246
274
|
}
|
|
247
|
-
if (existing === remoteHash) {
|
|
275
|
+
if (existing?.hash === remoteHash && existing.source === "native") {
|
|
276
|
+
await removeLegacySkillFile(root, slug);
|
|
248
277
|
action = "unchanged";
|
|
249
278
|
}
|
|
250
|
-
else if (existing !== null && opts.force) {
|
|
279
|
+
else if (existing !== null && (opts.force || (existing.source === "legacy" && existing.hash === remoteHash))) {
|
|
251
280
|
try {
|
|
252
|
-
|
|
281
|
+
if (existing.source === "legacy")
|
|
282
|
+
await writeInstallFile(root, target, detail.body_md);
|
|
283
|
+
else
|
|
284
|
+
await overwriteInstallFile(root, target, detail.body_md);
|
|
253
285
|
for (const file of remotePackageFiles) {
|
|
254
|
-
|
|
286
|
+
const fileTarget = join(dirname(target), file.path);
|
|
287
|
+
if (existing.source === "legacy")
|
|
288
|
+
await writeInstallFile(root, fileTarget, file.bytes);
|
|
289
|
+
else
|
|
290
|
+
await overwriteInstallFile(root, fileTarget, file.bytes);
|
|
255
291
|
}
|
|
292
|
+
await removeLegacySkillFile(root, slug);
|
|
256
293
|
}
|
|
257
294
|
catch (err) {
|
|
258
295
|
const code = err.code;
|
|
@@ -260,7 +297,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
260
297
|
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
261
298
|
throw err;
|
|
262
299
|
}
|
|
263
|
-
action = "updated";
|
|
300
|
+
action = existing.source === "legacy" ? "installed" : "updated";
|
|
264
301
|
}
|
|
265
302
|
else if (existing !== null) {
|
|
266
303
|
throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
|
|
@@ -271,6 +308,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
271
308
|
for (const file of remotePackageFiles) {
|
|
272
309
|
await writeInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
273
310
|
}
|
|
311
|
+
await removeLegacySkillFile(root, slug);
|
|
274
312
|
}
|
|
275
313
|
catch (err) {
|
|
276
314
|
const code = err.code;
|
|
@@ -294,7 +332,17 @@ export async function install(slugInput, opts = {}) {
|
|
|
294
332
|
manifestWarning = err instanceof Error ? err.message : String(err);
|
|
295
333
|
}
|
|
296
334
|
});
|
|
297
|
-
spinner
|
|
335
|
+
spinner?.stop();
|
|
336
|
+
if (opts.json) {
|
|
337
|
+
process.stdout.write(`${JSON.stringify({
|
|
338
|
+
slug,
|
|
339
|
+
action,
|
|
340
|
+
target: targetAgent,
|
|
341
|
+
path: target,
|
|
342
|
+
setup: Boolean(opts.setup),
|
|
343
|
+
})}\n`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
298
346
|
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
299
347
|
process.stdout.write(` ${c.dim(dirname(target))}\n\n`);
|
|
300
348
|
if (manifestWarning) {
|
package/dist/login.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createServer as createNetServer } from "node:net";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
import open from "open";
|
|
4
5
|
import ora from "ora";
|
|
5
6
|
import { getApiUrl, writeConfig } from "./config.js";
|
|
@@ -92,6 +93,7 @@ function reserveEphemeralPort() {
|
|
|
92
93
|
function waitForCallback(port) {
|
|
93
94
|
return new Promise((resolve, reject) => {
|
|
94
95
|
const apiUrl = getApiUrl();
|
|
96
|
+
const state = randomBytes(32).toString("base64url");
|
|
95
97
|
let settled = false;
|
|
96
98
|
const server = createServer((req, res) => {
|
|
97
99
|
// CORS preflight from the browser bridge page.
|
|
@@ -122,6 +124,15 @@ function waitForCallback(port) {
|
|
|
122
124
|
res.end(localCallbackPage("Missing tokens from OAuth response."));
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
127
|
+
if (data.state !== state) {
|
|
128
|
+
res.writeHead(400, {
|
|
129
|
+
"access-control-allow-origin": origin,
|
|
130
|
+
"access-control-allow-private-network": "true",
|
|
131
|
+
"content-type": "text/html; charset=utf-8",
|
|
132
|
+
});
|
|
133
|
+
res.end(localCallbackPage("Invalid sign-in state."));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
125
136
|
res.writeHead(200, {
|
|
126
137
|
"access-control-allow-origin": origin,
|
|
127
138
|
"access-control-allow-private-network": "true",
|
|
@@ -161,7 +172,7 @@ function waitForCallback(port) {
|
|
|
161
172
|
reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
|
|
162
173
|
});
|
|
163
174
|
server.listen(port, "127.0.0.1", () => {
|
|
164
|
-
const target = `${apiUrl}/auth/cli?port=${port}`;
|
|
175
|
+
const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
|
|
165
176
|
open(target).catch((e) => {
|
|
166
177
|
const msg = e instanceof Error ? e.message : String(e);
|
|
167
178
|
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
|
|
@@ -175,7 +186,7 @@ function parseCallbackBody(body, contentType) {
|
|
|
175
186
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
176
187
|
const params = new URLSearchParams(body);
|
|
177
188
|
const parsed = {};
|
|
178
|
-
for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
|
|
189
|
+
for (const key of ["access_token", "refresh_token", "expires_in", "token_type", "state"]) {
|
|
179
190
|
const value = params.get(key);
|
|
180
191
|
if (value)
|
|
181
192
|
parsed[key] = value;
|
package/dist/secrets.js
CHANGED
|
@@ -13,7 +13,7 @@ const SECRET_PATTERNS = [
|
|
|
13
13
|
];
|
|
14
14
|
const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
|
|
15
15
|
const PROVIDER_LIKE_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?((?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|sbp_[A-Za-z0-9]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})["']?/gi;
|
|
16
|
-
const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|placeholder|replace|changeme|todo|xxx|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
|
|
16
|
+
const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|placeholder|replace|changeme|todo|xxx|test|demo|dummy|fake|mock|staging|dev|local|redacted)(?:$|[_./+=-])/i;
|
|
17
17
|
const PROMPT_INJECTION_PATTERNS = [
|
|
18
18
|
{ label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
|
19
19
|
{ label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function resolveSkillsDir(target) {
|
|
4
|
+
if (process.env.FLOOM_SKILLS_DIR)
|
|
5
|
+
return process.env.FLOOM_SKILLS_DIR;
|
|
6
|
+
if (target === "codex") {
|
|
7
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
8
|
+
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
9
|
+
}
|
|
10
|
+
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
11
|
+
}
|
|
12
|
+
export function skillsDirHint(target) {
|
|
13
|
+
if (process.env.FLOOM_SKILLS_DIR)
|
|
14
|
+
return "FLOOM_SKILLS_DIR";
|
|
15
|
+
return target === "codex" ? "CODEX_SKILLS_DIR" : "CLAUDE_SKILLS_DIR";
|
|
16
|
+
}
|