@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 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
- return target ? { slug, target, setup, force } : { slug, setup, force };
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 top-level usage.
677
- // Subcommands are simple enough that one help screen is fine for Version 1.
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
- usage();
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 localPackageHash(root, slug, target, files) {
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 && files.length === 0)
74
- return packageHash(legacy.toString("utf8"), []);
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.stop();
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 ensureSafeParentDirectory(root, target);
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
- await overwriteInstallFile(root, target, detail.body_md);
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
- await overwriteInstallFile(root, join(dirname(target), file.path), file.bytes);
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.stop();
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 },
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",