@floomhq/floom 1.0.11 → 1.0.12
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 +4 -4
- package/dist/cli.js +51 -12
- package/dist/doctor.js +18 -18
- package/dist/install.js +2 -12
- package/dist/login.js +8 -6
- package/dist/secrets.js +3 -0
- package/dist/sync-manifest.js +12 -6
- package/dist/sync.js +15 -17
- package/dist/targets.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
27
27
|
- `npx -y @floomhq/floom publish <file.md>` — scan and upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, `--skill-version <label>`, and `--share <email>`. `--share` sends the normal link by email; no account is needed to add unlisted or public links.
|
|
28
28
|
- `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
|
|
29
29
|
- `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
|
|
30
|
-
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into
|
|
30
|
+
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into the local agent skills folder. Optional `--target claude|codex`, `--setup`, `--force` / `--update`, and `--json`.
|
|
31
31
|
- `npx -y @floomhq/floom update <url-or-slug>` — install the latest remote skill content, replacing the local copy.
|
|
32
32
|
- `npx -y @floomhq/floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
33
33
|
- `npx -y @floomhq/floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
|
|
@@ -35,15 +35,15 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
35
35
|
- `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`, `--file <path>`.
|
|
36
36
|
- `npx -y @floomhq/floom connect` — alias for setup.
|
|
37
37
|
- `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
|
|
38
|
-
- `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into
|
|
39
|
-
- `npx -y @floomhq/floom watch` — preview: run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
|
|
38
|
+
- `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into the local agent skills folder. Optional `--target claude|codex`.
|
|
39
|
+
- `npx -y @floomhq/floom watch` — preview: run sync repeatedly. Optional `--target claude|codex`, `--interval <seconds>`; minimum `10`.
|
|
40
40
|
- `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
|
|
41
41
|
- `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
|
|
42
42
|
- `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
|
|
43
43
|
- `npx -y @floomhq/floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
|
|
44
44
|
- `npx -y @floomhq/floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
|
|
45
45
|
- `npx -y @floomhq/floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
|
|
46
|
-
- `npx -y @floomhq/floom doctor` — diagnose your Floom setup. Optional `--json`.
|
|
46
|
+
- `npx -y @floomhq/floom doctor` — diagnose your Floom setup. Optional `--target claude|codex`, `--json`.
|
|
47
47
|
- `npx -y @floomhq/floom whoami` — show the signed-in account.
|
|
48
48
|
- `npx -y @floomhq/floom logout` — delete local credentials.
|
|
49
49
|
|
package/dist/cli.js
CHANGED
|
@@ -104,7 +104,7 @@ function commandUsage() {
|
|
|
104
104
|
${c.dim("Alias: paste")}
|
|
105
105
|
${c.dim("Flags: --target claude|codex")}
|
|
106
106
|
${c.cyan("doctor")} Troubleshoot auth, API, and local folders
|
|
107
|
-
${c.dim("Flags: --json")}
|
|
107
|
+
${c.dim("Flags: --target claude|codex, --json")}
|
|
108
108
|
|
|
109
109
|
${c.dim("Advanced")}
|
|
110
110
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
@@ -112,7 +112,9 @@ function commandUsage() {
|
|
|
112
112
|
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
|
|
113
113
|
${c.cyan("mcp")} Print optional MCP setup guidance
|
|
114
114
|
${c.cyan("sync")} Preview pull of published, saved, and library skills
|
|
115
|
+
${c.dim("Flags: --target claude|codex")}
|
|
115
116
|
${c.cyan("watch")} Preview polling sync loop
|
|
117
|
+
${c.dim("Flags: --target claude|codex, --interval <seconds>")}
|
|
116
118
|
|
|
117
119
|
${c.bold("Examples")}
|
|
118
120
|
${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --setup")}
|
|
@@ -362,15 +364,27 @@ function parseAddArgs(argv) {
|
|
|
362
364
|
}
|
|
363
365
|
return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
|
|
364
366
|
}
|
|
367
|
+
function parseTargetFlag(value) {
|
|
368
|
+
if (value !== "claude" && value !== "codex") {
|
|
369
|
+
throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
|
|
370
|
+
}
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
365
373
|
function parseDoctorArgs(argv) {
|
|
366
374
|
const out = { json: false };
|
|
367
|
-
for (
|
|
375
|
+
for (let i = 0; i < argv.length; i++) {
|
|
376
|
+
const a = argv[i] ?? "";
|
|
368
377
|
if (a === "--json")
|
|
369
378
|
out.json = true;
|
|
379
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
380
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
381
|
+
out.target = parseTargetFlag(value);
|
|
382
|
+
i = nextIndex;
|
|
383
|
+
}
|
|
370
384
|
else if (a.startsWith("--"))
|
|
371
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
385
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --target codex --json`.");
|
|
372
386
|
else
|
|
373
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
|
|
387
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --target codex --json`.");
|
|
374
388
|
}
|
|
375
389
|
return out;
|
|
376
390
|
}
|
|
@@ -634,11 +648,34 @@ function parseWatchFlags(argv) {
|
|
|
634
648
|
out.intervalSeconds = interval;
|
|
635
649
|
i = nextIndex;
|
|
636
650
|
}
|
|
651
|
+
else if (a === "--target" || a.startsWith("--target=")) {
|
|
652
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
653
|
+
out.target = parseTargetFlag(value);
|
|
654
|
+
i = nextIndex;
|
|
655
|
+
}
|
|
656
|
+
else if (a.startsWith("--")) {
|
|
657
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --target codex --interval 60`.");
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --target codex --interval 60`.");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
function parseSyncFlags(argv) {
|
|
666
|
+
const out = {};
|
|
667
|
+
for (let i = 0; i < argv.length; i++) {
|
|
668
|
+
const a = argv[i] ?? "";
|
|
669
|
+
if (a === "--target" || a.startsWith("--target=")) {
|
|
670
|
+
const { value, nextIndex } = readFlagValue(argv, i, "--target");
|
|
671
|
+
out.target = parseTargetFlag(value);
|
|
672
|
+
i = nextIndex;
|
|
673
|
+
}
|
|
637
674
|
else if (a.startsWith("--")) {
|
|
638
|
-
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom
|
|
675
|
+
throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom sync --target codex`.");
|
|
639
676
|
}
|
|
640
677
|
else {
|
|
641
|
-
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom
|
|
678
|
+
throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom sync --target codex`.");
|
|
642
679
|
}
|
|
643
680
|
}
|
|
644
681
|
return out;
|
|
@@ -674,7 +711,7 @@ function sleep(ms, signal) {
|
|
|
674
711
|
}, { once: true });
|
|
675
712
|
});
|
|
676
713
|
}
|
|
677
|
-
async function watch(intervalSeconds) {
|
|
714
|
+
async function watch(intervalSeconds, target = "claude") {
|
|
678
715
|
const cfg = await readConfig();
|
|
679
716
|
if (!cfg) {
|
|
680
717
|
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` before watch, or use `npx -y @floomhq/floom add <link>` without an account.");
|
|
@@ -691,9 +728,9 @@ async function watch(intervalSeconds) {
|
|
|
691
728
|
};
|
|
692
729
|
process.on("SIGINT", stop);
|
|
693
730
|
process.on("SIGTERM", stop);
|
|
694
|
-
process.stdout.write(`${symbols.bullet} Watching Floom sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
731
|
+
process.stdout.write(`${symbols.bullet} Watching Floom ${target} sync every ${intervalSeconds}s. Press Ctrl-C to stop.\n`);
|
|
695
732
|
while (!controller.signal.aborted) {
|
|
696
|
-
await sync({ spinner: false, quietUnchanged: true });
|
|
733
|
+
await sync({ spinner: false, quietUnchanged: true, target });
|
|
697
734
|
await sleep(intervalSeconds * 1000, controller.signal);
|
|
698
735
|
}
|
|
699
736
|
}
|
|
@@ -843,8 +880,10 @@ async function main() {
|
|
|
843
880
|
return;
|
|
844
881
|
}
|
|
845
882
|
case "sync":
|
|
846
|
-
|
|
847
|
-
|
|
883
|
+
{
|
|
884
|
+
const flags = parseSyncFlags(rest);
|
|
885
|
+
await sync(flags);
|
|
886
|
+
}
|
|
848
887
|
return;
|
|
849
888
|
case "setup":
|
|
850
889
|
case "connect": {
|
|
@@ -860,7 +899,7 @@ async function main() {
|
|
|
860
899
|
}
|
|
861
900
|
case "watch": {
|
|
862
901
|
const flags = parseWatchFlags(rest);
|
|
863
|
-
await watch(flags.intervalSeconds);
|
|
902
|
+
await watch(flags.intervalSeconds, flags.target ?? "claude");
|
|
864
903
|
return;
|
|
865
904
|
}
|
|
866
905
|
case "delete":
|
package/dist/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
|
5
5
|
import { floomFetch } from "./lib/api.js";
|
|
6
6
|
import { c, symbols } from "./ui.js";
|
|
7
7
|
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
8
|
+
import { resolveSkillsDir } from "./targets.js";
|
|
8
9
|
function statusBadge(s) {
|
|
9
10
|
if (s === "ok")
|
|
10
11
|
return c.green(symbols.ok);
|
|
@@ -97,27 +98,24 @@ async function checkMcp() {
|
|
|
97
98
|
detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
async function checkTargetDir() {
|
|
104
|
-
const dir = targetSkillsDir();
|
|
101
|
+
async function checkTargetDir(target) {
|
|
102
|
+
const dir = resolveSkillsDir(target);
|
|
105
103
|
try {
|
|
106
104
|
const s = await stat(dir);
|
|
107
105
|
if (!s.isDirectory()) {
|
|
108
106
|
return {
|
|
109
|
-
name:
|
|
107
|
+
name: `${target} dir`,
|
|
110
108
|
status: "fail",
|
|
111
109
|
detail: `${dir} exists but is not a directory.`,
|
|
112
110
|
};
|
|
113
111
|
}
|
|
114
112
|
try {
|
|
115
113
|
await access(dir, constants.W_OK);
|
|
116
|
-
return { name:
|
|
114
|
+
return { name: `${target} dir`, status: "ok", detail: `${dir} (writable)` };
|
|
117
115
|
}
|
|
118
116
|
catch {
|
|
119
117
|
return {
|
|
120
|
-
name:
|
|
118
|
+
name: `${target} dir`,
|
|
121
119
|
status: "fail",
|
|
122
120
|
detail: `${dir} is not writable.`,
|
|
123
121
|
hint: `Try: chmod u+w ${dir}`,
|
|
@@ -127,7 +125,7 @@ async function checkTargetDir() {
|
|
|
127
125
|
catch (err) {
|
|
128
126
|
if (err.code === "ENOENT") {
|
|
129
127
|
return {
|
|
130
|
-
name:
|
|
128
|
+
name: `${target} dir`,
|
|
131
129
|
status: "warn",
|
|
132
130
|
detail: `${dir} does not exist yet.`,
|
|
133
131
|
hint: "It will be created on first `npx -y @floomhq/floom add`.",
|
|
@@ -136,14 +134,14 @@ async function checkTargetDir() {
|
|
|
136
134
|
throw err;
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
|
-
async function checkLastSync() {
|
|
140
|
-
const dir =
|
|
137
|
+
async function checkLastSync(target) {
|
|
138
|
+
const dir = resolveSkillsDir(target);
|
|
141
139
|
try {
|
|
142
140
|
const entries = await readdir(dir);
|
|
143
141
|
const skills = entries.filter((e) => e.endsWith(".md") || !e.startsWith("."));
|
|
144
142
|
if (skills.length === 0) {
|
|
145
143
|
return {
|
|
146
|
-
name:
|
|
144
|
+
name: `${target} sync`,
|
|
147
145
|
status: "warn",
|
|
148
146
|
detail: "No synced skills found yet.",
|
|
149
147
|
hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
|
|
@@ -163,7 +161,7 @@ async function checkLastSync() {
|
|
|
163
161
|
}
|
|
164
162
|
if (newest.mtime === 0) {
|
|
165
163
|
return {
|
|
166
|
-
name:
|
|
164
|
+
name: `${target} sync`,
|
|
167
165
|
status: "warn",
|
|
168
166
|
detail: `${skills.length} entries, no readable mtime.`,
|
|
169
167
|
};
|
|
@@ -177,7 +175,7 @@ async function checkLastSync() {
|
|
|
177
175
|
? `${Math.round(ageSec / 3600)}h ago`
|
|
178
176
|
: `${Math.round(ageSec / 86400)}d ago`;
|
|
179
177
|
return {
|
|
180
|
-
name:
|
|
178
|
+
name: `${target} sync`,
|
|
181
179
|
status: "ok",
|
|
182
180
|
detail: `${newest.name} — ${human} (${skills.length} total)`,
|
|
183
181
|
};
|
|
@@ -185,13 +183,13 @@ async function checkLastSync() {
|
|
|
185
183
|
catch (err) {
|
|
186
184
|
if (err.code === "ENOENT") {
|
|
187
185
|
return {
|
|
188
|
-
name:
|
|
186
|
+
name: `${target} sync`,
|
|
189
187
|
status: "warn",
|
|
190
188
|
detail: "Skills dir not created yet.",
|
|
191
189
|
};
|
|
192
190
|
}
|
|
193
191
|
return {
|
|
194
|
-
name:
|
|
192
|
+
name: `${target} sync`,
|
|
195
193
|
status: "warn",
|
|
196
194
|
detail: `Cannot read skills dir: ${err.message}`,
|
|
197
195
|
};
|
|
@@ -240,11 +238,12 @@ async function checkVersion() {
|
|
|
240
238
|
}
|
|
241
239
|
}
|
|
242
240
|
export async function doctor(opts = {}) {
|
|
241
|
+
const target = opts.target ?? "claude";
|
|
243
242
|
const checks = await Promise.all([
|
|
244
243
|
checkAuth(),
|
|
245
244
|
checkMcp(),
|
|
246
|
-
checkTargetDir(),
|
|
247
|
-
checkLastSync(),
|
|
245
|
+
checkTargetDir(target),
|
|
246
|
+
checkLastSync(target),
|
|
248
247
|
checkVersion(),
|
|
249
248
|
]);
|
|
250
249
|
const anyFail = checks.some((c) => c.status === "fail");
|
|
@@ -255,6 +254,7 @@ export async function doctor(opts = {}) {
|
|
|
255
254
|
ok: !anyFail,
|
|
256
255
|
status,
|
|
257
256
|
version: CLI_VERSION,
|
|
257
|
+
target,
|
|
258
258
|
config_path: CONFIG_PATH,
|
|
259
259
|
checks,
|
|
260
260
|
}, null, 2)}\n`);
|
package/dist/install.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { lstat, mkdir, open } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import ora from "ora";
|
|
7
6
|
import { readConfig, resolveApiUrl } from "./config.js";
|
|
8
7
|
import { getJson } from "./lib/api.js";
|
|
9
8
|
import { c, symbols } from "./ui.js";
|
|
10
9
|
import { FloomError } from "./errors.js";
|
|
10
|
+
import { resolveSkillsDir, skillsDirHint } from "./targets.js";
|
|
11
11
|
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
12
12
|
const FD_PATH_ROOT = "/proc/self/fd";
|
|
13
13
|
function slugFromInput(input) {
|
|
@@ -21,19 +21,9 @@ function slugFromInput(input) {
|
|
|
21
21
|
return trimmed.replace(/\.(md|json)$/i, "");
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
function skillsDir(target) {
|
|
25
|
-
if (target === "codex") {
|
|
26
|
-
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
27
|
-
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
28
|
-
}
|
|
29
|
-
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
30
|
-
}
|
|
31
24
|
function skillPath(root, slug) {
|
|
32
25
|
return join(root, `${slug}.md`);
|
|
33
26
|
}
|
|
34
|
-
function skillsDirHint(target) {
|
|
35
|
-
return target === "codex" ? "CODEX_SKILLS_DIR" : "CLAUDE_SKILLS_DIR";
|
|
36
|
-
}
|
|
37
27
|
function setupCommand(target) {
|
|
38
28
|
return `npx -y @floomhq/floom setup --target ${target} --yes`;
|
|
39
29
|
}
|
|
@@ -152,7 +142,7 @@ async function assertSafeDirectory(path) {
|
|
|
152
142
|
}
|
|
153
143
|
export async function install(slugInput, opts = {}) {
|
|
154
144
|
const targetAgent = opts.target ?? "claude";
|
|
155
|
-
const root =
|
|
145
|
+
const root = resolveSkillsDir(targetAgent);
|
|
156
146
|
const slug = slugFromInput(slugInput);
|
|
157
147
|
if (!SLUG_RE.test(slug)) {
|
|
158
148
|
throw new FloomError(`Invalid skill slug: ${slugInput}`);
|
package/dist/login.js
CHANGED
|
@@ -60,15 +60,17 @@ export async function login() {
|
|
|
60
60
|
function waitForCallback() {
|
|
61
61
|
return new Promise((resolve, reject) => {
|
|
62
62
|
const apiUrl = getApiUrl();
|
|
63
|
+
const allowedOrigin = new URL(apiUrl).origin;
|
|
63
64
|
const state = randomBytes(24).toString("base64url");
|
|
64
65
|
let settled = false;
|
|
65
66
|
let retriedEphemeralPort = false;
|
|
66
67
|
const server = createServer((req, res) => {
|
|
67
68
|
// CORS preflight from the browser bridge page.
|
|
68
|
-
const origin = req.headers.origin
|
|
69
|
+
const origin = req.headers.origin;
|
|
70
|
+
const corsOrigin = origin === allowedOrigin ? origin : "null";
|
|
69
71
|
if (req.method === "OPTIONS") {
|
|
70
72
|
res.writeHead(204, {
|
|
71
|
-
"access-control-allow-origin":
|
|
73
|
+
"access-control-allow-origin": corsOrigin,
|
|
72
74
|
"access-control-allow-methods": "POST, OPTIONS",
|
|
73
75
|
"access-control-allow-headers": "content-type",
|
|
74
76
|
"access-control-allow-private-network": "true",
|
|
@@ -85,7 +87,7 @@ function waitForCallback() {
|
|
|
85
87
|
const data = parseCallbackBody(body, req.headers["content-type"]);
|
|
86
88
|
if (!data.access_token || !data.refresh_token) {
|
|
87
89
|
res.writeHead(400, {
|
|
88
|
-
"access-control-allow-origin":
|
|
90
|
+
"access-control-allow-origin": corsOrigin,
|
|
89
91
|
"access-control-allow-private-network": "true",
|
|
90
92
|
"content-type": "text/html; charset=utf-8",
|
|
91
93
|
});
|
|
@@ -94,7 +96,7 @@ function waitForCallback() {
|
|
|
94
96
|
}
|
|
95
97
|
if (data.state !== state) {
|
|
96
98
|
res.writeHead(400, {
|
|
97
|
-
"access-control-allow-origin":
|
|
99
|
+
"access-control-allow-origin": corsOrigin,
|
|
98
100
|
"access-control-allow-private-network": "true",
|
|
99
101
|
"content-type": "text/html; charset=utf-8",
|
|
100
102
|
});
|
|
@@ -102,7 +104,7 @@ function waitForCallback() {
|
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
104
106
|
res.writeHead(200, {
|
|
105
|
-
"access-control-allow-origin":
|
|
107
|
+
"access-control-allow-origin": corsOrigin,
|
|
106
108
|
"access-control-allow-private-network": "true",
|
|
107
109
|
"content-type": "text/html; charset=utf-8",
|
|
108
110
|
});
|
|
@@ -112,7 +114,7 @@ function waitForCallback() {
|
|
|
112
114
|
resolve(data);
|
|
113
115
|
}
|
|
114
116
|
catch {
|
|
115
|
-
res.writeHead(400, { "content-type": "text/html; charset=utf-8", "access-control-allow-origin":
|
|
117
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8", "access-control-allow-origin": corsOrigin });
|
|
116
118
|
res.end(localCallbackPage("Invalid OAuth response."));
|
|
117
119
|
}
|
|
118
120
|
});
|
package/dist/secrets.js
CHANGED
|
@@ -38,6 +38,9 @@ const PROMPT_INJECTION_PATTERNS = [
|
|
|
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
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 },
|
|
41
|
+
{ label: "Data exfiltration instruction", regex: /\b(?:send|email|mail|forward|share|transmit|post|upload|exfiltrate|copy)\b[^.\n]{0,160}\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b[^.\n]{0,160}\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
42
|
+
{ label: "Data exfiltration instruction", regex: /\b(?:send|email|mail|forward|share|transmit|post|upload|exfiltrate|copy)\b[^.\n]{0,160}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b[^.\n]{0,160}\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
43
|
+
{ label: "Data exfiltration instruction", regex: /(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?|\bapi keys?|\btokens?|\bsecrets?|\benvironment variables|\.env|\bcredentials)\b[^.\n]{0,160}\b(?:send|email|mail|forward|share|transmit|post|upload|exfiltrate|copy)\b[^.\n]{0,120}\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
|
|
41
44
|
{ 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
45
|
{ 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 },
|
|
43
46
|
{ label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
|
package/dist/sync-manifest.js
CHANGED
|
@@ -3,9 +3,14 @@ import { lstat, mkdir, open, rename } from "node:fs/promises";
|
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { CONFIG_DIR } from "./config.js";
|
|
5
5
|
const MANIFEST_VERSION = 1;
|
|
6
|
-
const MANIFEST_PATH = join(CONFIG_DIR, "sync-manifest.json");
|
|
7
6
|
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8
7
|
const FD_PATH_ROOT = "/proc/self/fd";
|
|
8
|
+
function manifestFilename(scope = "claude") {
|
|
9
|
+
return scope === "claude" ? "sync-manifest.json" : `sync-manifest.${scope}.json`;
|
|
10
|
+
}
|
|
11
|
+
function manifestPath(scope = "claude") {
|
|
12
|
+
return join(CONFIG_DIR, manifestFilename(scope));
|
|
13
|
+
}
|
|
9
14
|
function emptyManifest() {
|
|
10
15
|
return { version: MANIFEST_VERSION, files: {} };
|
|
11
16
|
}
|
|
@@ -21,10 +26,10 @@ function isEntryForKey(key, value) {
|
|
|
21
26
|
SLUG_RE.test(entry.slug) &&
|
|
22
27
|
key.split("/").at(-1) === `${entry.slug}.md`);
|
|
23
28
|
}
|
|
24
|
-
export async function readSyncManifest() {
|
|
29
|
+
export async function readSyncManifest(scope = "claude") {
|
|
25
30
|
try {
|
|
26
31
|
await ensureSyncManifestDir();
|
|
27
|
-
const handle = await open(
|
|
32
|
+
const handle = await open(manifestPath(scope), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
28
33
|
let body;
|
|
29
34
|
try {
|
|
30
35
|
body = await handle.readFile("utf8");
|
|
@@ -51,10 +56,11 @@ export async function readSyncManifest() {
|
|
|
51
56
|
throw err;
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
|
-
export async function writeSyncManifest(manifest) {
|
|
59
|
+
export async function writeSyncManifest(manifest, scope = "claude") {
|
|
55
60
|
await ensureSyncManifestDir();
|
|
56
61
|
const dir = await open(CONFIG_DIR, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
57
|
-
const
|
|
62
|
+
const filename = manifestFilename(scope);
|
|
63
|
+
const tmpBase = `${filename}.${process.pid}.${Date.now()}`;
|
|
58
64
|
const body = JSON.stringify(manifest, null, 2);
|
|
59
65
|
try {
|
|
60
66
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
@@ -66,7 +72,7 @@ export async function writeSyncManifest(manifest) {
|
|
|
66
72
|
await handle.writeFile(body, "utf8");
|
|
67
73
|
await handle.close();
|
|
68
74
|
handle = null;
|
|
69
|
-
await rename(tmpPath, childPath(dir, CONFIG_DIR,
|
|
75
|
+
await rename(tmpPath, childPath(dir, CONFIG_DIR, filename));
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
78
|
catch (err) {
|
package/dist/sync.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { lstat, mkdir, open } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import ora from "ora";
|
|
7
6
|
import { readConfig, resolveApiUrl } from "./config.js";
|
|
@@ -9,13 +8,11 @@ import { getJson } from "./lib/api.js";
|
|
|
9
8
|
import { c, symbols } from "./ui.js";
|
|
10
9
|
import { FloomError } from "./errors.js";
|
|
11
10
|
import { ensureSyncManifestDir, manifestKey, markSynced, readSyncManifest, unmarkSynced, writeSyncManifest } from "./sync-manifest.js";
|
|
11
|
+
import { resolveSkillsDir } from "./targets.js";
|
|
12
12
|
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
13
13
|
const PATH_SEGMENT_RE = /^[a-z0-9._-]{1,128}$/;
|
|
14
14
|
const MANIFEST_SEGMENT_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
15
15
|
const FD_PATH_ROOT = "/proc/self/fd";
|
|
16
|
-
function skillsDir() {
|
|
17
|
-
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
18
|
-
}
|
|
19
16
|
function sha256(input) {
|
|
20
17
|
return createHash("sha256").update(input).digest("hex");
|
|
21
18
|
}
|
|
@@ -56,10 +53,9 @@ function safePathSegments(value, label) {
|
|
|
56
53
|
}
|
|
57
54
|
return segments;
|
|
58
55
|
}
|
|
59
|
-
function skillPath(skill) {
|
|
56
|
+
function skillPath(root, skill) {
|
|
60
57
|
if (!SLUG_RE.test(skill.slug))
|
|
61
58
|
throw new FloomError(`Invalid skill slug: ${skill.slug}`);
|
|
62
|
-
const root = skillsDir();
|
|
63
59
|
const segments = [root];
|
|
64
60
|
segments.push(...safePathSegments(skill.library_slug, "library slug"));
|
|
65
61
|
segments.push(...safePathSegments(skill.folder, "folder"));
|
|
@@ -107,8 +103,8 @@ function targetFromManifestKey(root, key) {
|
|
|
107
103
|
}
|
|
108
104
|
return target;
|
|
109
105
|
}
|
|
110
|
-
async function writeSyncedFile(target, body) {
|
|
111
|
-
const parent = await openSafeParentDirectory(
|
|
106
|
+
async function writeSyncedFile(root, target, body) {
|
|
107
|
+
const parent = await openSafeParentDirectory(root, target, true);
|
|
112
108
|
let handle = null;
|
|
113
109
|
try {
|
|
114
110
|
handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
@@ -206,9 +202,11 @@ export async function sync(opts = {}) {
|
|
|
206
202
|
const cfg = await readConfig();
|
|
207
203
|
if (!cfg)
|
|
208
204
|
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
205
|
+
const targetAgent = opts.target ?? "claude";
|
|
206
|
+
const root = resolveSkillsDir(targetAgent);
|
|
209
207
|
await ensureSyncManifestDir();
|
|
210
208
|
const apiUrl = resolveApiUrl(cfg);
|
|
211
|
-
const spinner = opts.spinner === false ? null : ora({ text: c.dim(
|
|
209
|
+
const spinner = opts.spinner === false ? null : ora({ text: c.dim(`Syncing ${targetAgent} skills...`), color: "yellow" }).start();
|
|
212
210
|
let payload;
|
|
213
211
|
try {
|
|
214
212
|
payload = await getJson(`${apiUrl}/api/v1/me/skills`, "load your skills", cfg.accessToken);
|
|
@@ -217,7 +215,7 @@ export async function sync(opts = {}) {
|
|
|
217
215
|
spinner?.stop();
|
|
218
216
|
throw err;
|
|
219
217
|
}
|
|
220
|
-
await mkdir(
|
|
218
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
221
219
|
if (!Array.isArray(payload.skills)) {
|
|
222
220
|
throw new FloomError("Invalid sync response.");
|
|
223
221
|
}
|
|
@@ -231,8 +229,7 @@ export async function sync(opts = {}) {
|
|
|
231
229
|
let skipped = 0;
|
|
232
230
|
let conflicts = 0;
|
|
233
231
|
const conflictNotes = [];
|
|
234
|
-
const manifest = await readSyncManifest();
|
|
235
|
-
const root = skillsDir();
|
|
232
|
+
const manifest = await readSyncManifest(targetAgent);
|
|
236
233
|
const activeTargetKeys = new Set();
|
|
237
234
|
const pruneBlockedSlugs = new Set();
|
|
238
235
|
let manifestChanged = false;
|
|
@@ -257,7 +254,7 @@ export async function sync(opts = {}) {
|
|
|
257
254
|
}
|
|
258
255
|
let target;
|
|
259
256
|
try {
|
|
260
|
-
target = skillPath(skill);
|
|
257
|
+
target = skillPath(root, skill);
|
|
261
258
|
}
|
|
262
259
|
catch (err) {
|
|
263
260
|
if (err instanceof FloomError) {
|
|
@@ -312,7 +309,7 @@ export async function sync(opts = {}) {
|
|
|
312
309
|
continue;
|
|
313
310
|
}
|
|
314
311
|
try {
|
|
315
|
-
await writeSyncedFile(target, skill.body_md);
|
|
312
|
+
await writeSyncedFile(root, target, skill.body_md);
|
|
316
313
|
}
|
|
317
314
|
catch (err) {
|
|
318
315
|
const code = err.code;
|
|
@@ -327,7 +324,7 @@ export async function sync(opts = {}) {
|
|
|
327
324
|
throw err;
|
|
328
325
|
}
|
|
329
326
|
markSynced(manifest, targetKey, skill.slug, remoteHash);
|
|
330
|
-
await writeSyncManifest(manifest);
|
|
327
|
+
await writeSyncManifest(manifest, targetAgent);
|
|
331
328
|
updated += 1;
|
|
332
329
|
}
|
|
333
330
|
if (payload.full_sync === true) {
|
|
@@ -378,7 +375,7 @@ export async function sync(opts = {}) {
|
|
|
378
375
|
}
|
|
379
376
|
}
|
|
380
377
|
if (manifestChanged)
|
|
381
|
-
await writeSyncManifest(manifest);
|
|
378
|
+
await writeSyncManifest(manifest, targetAgent);
|
|
382
379
|
}
|
|
383
380
|
catch (err) {
|
|
384
381
|
spinner?.stop();
|
|
@@ -394,7 +391,8 @@ export async function sync(opts = {}) {
|
|
|
394
391
|
process.stderr.write(`${symbols.bullet} [floom] skipped local conflict: ${note}\n`);
|
|
395
392
|
}
|
|
396
393
|
if (conflicts > 0) {
|
|
397
|
-
|
|
394
|
+
const targetFlag = targetAgent === "claude" ? "" : ` --target ${targetAgent}`;
|
|
395
|
+
process.stderr.write(` ${c.dim(`Move or delete the local file, then run \`npx -y @floomhq/floom sync${targetFlag}\` again.`)}\n`);
|
|
398
396
|
}
|
|
399
397
|
process.stdout.write(`\n${symbols.ok} [floom] synced ${synced} skills (${unchanged} unchanged, ${updated} updated${conflictNote})${skippedNote}\n\n`);
|
|
400
398
|
}
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function resolveSkillsDir(target) {
|
|
4
|
+
if (target === "codex") {
|
|
5
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
6
|
+
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
7
|
+
}
|
|
8
|
+
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
9
|
+
}
|
|
10
|
+
export function skillsDirHint(target) {
|
|
11
|
+
return target === "codex" ? "CODEX_SKILLS_DIR" : "CLAUDE_SKILLS_DIR";
|
|
12
|
+
}
|