@floomhq/floom 1.0.9 → 1.0.11

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/bin/floom.js CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -102,6 +102,7 @@ function commandUsage() {
102
102
  ${c.dim(" From a repo, setup writes that repo's CLAUDE.md/AGENTS.md")}
103
103
  ${c.cyan("agent-prompt")} Print the sentence to paste into your agent
104
104
  ${c.dim("Alias: paste")}
105
+ ${c.dim("Flags: --target claude|codex")}
105
106
  ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
106
107
  ${c.dim("Flags: --json")}
107
108
 
@@ -130,6 +131,7 @@ function commandUsage() {
130
131
  }
131
132
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
132
133
  const INIT_TEMPLATES = new Set(["generic", "brand-voice", "pr-review", "sales", "support", "onboarding"]);
134
+ const MAX_SHARE_RECIPIENTS = 25;
133
135
  const INSTALL_TARGETS = new Set([
134
136
  "claude_skill",
135
137
  "memory",
@@ -206,8 +208,8 @@ function parseFlags(argv) {
206
208
  }
207
209
  if (out.shareEmails.length > 0) {
208
210
  out.shareEmails = dedupeEmails(out.shareEmails);
209
- if (out.shareEmails.length > 200) {
210
- throw new FloomError("Too many --share recipients.", "Use 200 email addresses or fewer.");
211
+ if (out.shareEmails.length > MAX_SHARE_RECIPIENTS) {
212
+ throw new FloomError("Too many --share recipients.", `Use ${MAX_SHARE_RECIPIENTS} email addresses or fewer.`);
211
213
  }
212
214
  if (out.visibility === "private") {
213
215
  throw new FloomError("`--private --share` would email a link recipients cannot open.", "Use `--unlisted --share` for invite emails, or `npx -y @floomhq/floom share <slug> --add <email>` for email-gated access after publishing.");
@@ -372,6 +374,27 @@ function parseDoctorArgs(argv) {
372
374
  }
373
375
  return out;
374
376
  }
377
+ function parseAgentPromptArgs(argv) {
378
+ const out = {};
379
+ for (let i = 0; i < argv.length; i++) {
380
+ const a = argv[i] ?? "";
381
+ if (a === "--target" || a.startsWith("--target=")) {
382
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
383
+ if (value !== "claude" && value !== "codex") {
384
+ throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
385
+ }
386
+ out.target = value;
387
+ i = nextIndex;
388
+ }
389
+ else if (a.startsWith("--")) {
390
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
391
+ }
392
+ else {
393
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
394
+ }
395
+ }
396
+ return out;
397
+ }
375
398
  function parseSearchFlags(argv) {
376
399
  const out = { json: false };
377
400
  const terms = [];
@@ -636,8 +659,9 @@ function parseSingleFileArg(argv, usageHint) {
636
659
  throw new FloomError("Missing file argument.", usageHint);
637
660
  return file;
638
661
  }
639
- function agentPrompt() {
640
- process.stdout.write("Use my installed Floom skills when they fit the task. Search ~/.claude/skills first.\n");
662
+ function agentPrompt(target = "claude") {
663
+ const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
664
+ process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
641
665
  }
642
666
  function sleep(ms, signal) {
643
667
  if (signal.aborted)
@@ -829,10 +853,11 @@ async function main() {
829
853
  return;
830
854
  }
831
855
  case "agent-prompt":
832
- case "paste":
833
- rejectArgs(rest, "Try `npx -y @floomhq/floom agent-prompt`.");
834
- agentPrompt();
856
+ case "paste": {
857
+ const flags = parseAgentPromptArgs(rest);
858
+ agentPrompt(flags.target ?? "claude");
835
859
  return;
860
+ }
836
861
  case "watch": {
837
862
  const flags = parseWatchFlags(rest);
838
863
  await watch(flags.intervalSeconds);
package/dist/login.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:http";
2
+ import { randomBytes } from "node:crypto";
2
3
  import open from "open";
3
4
  import ora from "ora";
4
5
  import { getApiUrl, writeConfig } from "./config.js";
@@ -59,6 +60,7 @@ export async function login() {
59
60
  function waitForCallback() {
60
61
  return new Promise((resolve, reject) => {
61
62
  const apiUrl = getApiUrl();
63
+ const state = randomBytes(24).toString("base64url");
62
64
  let settled = false;
63
65
  let retriedEphemeralPort = false;
64
66
  const server = createServer((req, res) => {
@@ -90,6 +92,15 @@ function waitForCallback() {
90
92
  res.end(localCallbackPage("Missing tokens from OAuth response."));
91
93
  return;
92
94
  }
95
+ if (data.state !== state) {
96
+ res.writeHead(400, {
97
+ "access-control-allow-origin": origin,
98
+ "access-control-allow-private-network": "true",
99
+ "content-type": "text/html; charset=utf-8",
100
+ });
101
+ res.end(localCallbackPage("Invalid OAuth state."));
102
+ return;
103
+ }
93
104
  res.writeHead(200, {
94
105
  "access-control-allow-origin": origin,
95
106
  "access-control-allow-private-network": "true",
@@ -143,7 +154,7 @@ function waitForCallback() {
143
154
  return;
144
155
  }
145
156
  const port = address.port;
146
- const target = `${apiUrl}/auth/cli?port=${port}`;
157
+ const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
147
158
  open(target).catch((e) => {
148
159
  const msg = e instanceof Error ? e.message : String(e);
149
160
  process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
@@ -157,7 +168,7 @@ function parseCallbackBody(body, contentType) {
157
168
  if (type.includes("application/x-www-form-urlencoded")) {
158
169
  const params = new URLSearchParams(body);
159
170
  const parsed = {};
160
- for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
171
+ for (const key of ["access_token", "refresh_token", "expires_in", "token_type", "state"]) {
161
172
  const value = params.get(key);
162
173
  if (value)
163
174
  parsed[key] = value;
package/dist/secrets.js CHANGED
@@ -37,8 +37,11 @@ const PROMPT_INJECTION_PATTERNS = [
37
37
  ];
38
38
  const DATA_EXFILTRATION_PATTERNS = [
39
39
  { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b(?:[^.\n]{0,120})\b(?:to|into) https?:\/\//gi },
40
+ { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b[^.\n]{0,120}\b(?:to|into) https?:\/\//gi },
40
41
  { label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
42
+ { label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
41
43
  { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
44
+ { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
42
45
  ];
43
46
  function redact(value) {
44
47
  if (value.length <= 12)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Publish AI skills from your terminal. Share with a link.",
5
5
  "license": "MIT",
6
6
  "type": "module",