@floomhq/floom 1.0.26 → 1.0.28
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 +6 -5
- package/dist/cli.js +61 -242
- package/dist/config.js +1 -51
- package/dist/doctor.js +12 -40
- package/dist/errors.js +1 -1
- package/dist/install.js +19 -74
- package/dist/login.js +8 -67
- package/dist/mcp.js +5 -2
- package/dist/package.js +177 -81
- package/dist/publish.js +41 -51
- package/dist/push-watch.js +245 -0
- package/dist/setup.js +87 -25
- package/dist/sync-manifest.js +44 -37
- package/dist/sync.js +15 -26
- package/dist/targets.js +45 -12
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -4,10 +4,11 @@ import { delimiter } from "node:path";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import {
|
|
7
|
+
import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
8
8
|
import { floomFetch } from "./lib/api.js";
|
|
9
9
|
import { c, symbols } from "./ui.js";
|
|
10
10
|
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
11
|
+
import { targetSkillsDir } from "./targets.js";
|
|
11
12
|
const execFile = promisify(execFileCb);
|
|
12
13
|
function statusBadge(s) {
|
|
13
14
|
if (s === "ok")
|
|
@@ -17,23 +18,12 @@ function statusBadge(s) {
|
|
|
17
18
|
return c.red(symbols.fail);
|
|
18
19
|
}
|
|
19
20
|
async function checkAuth() {
|
|
20
|
-
const raw = await readRawConfig();
|
|
21
|
-
if (!raw) {
|
|
22
|
-
return {
|
|
23
|
-
name: "Auth",
|
|
24
|
-
status: "ok",
|
|
25
|
-
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const expired = raw.expiresAt <= Math.floor(Date.now() / 1000);
|
|
29
|
-
const expiresSoon = needsRefresh(raw);
|
|
30
21
|
const cfg = await readConfig();
|
|
31
22
|
if (!cfg) {
|
|
32
23
|
return {
|
|
33
24
|
name: "Auth",
|
|
34
|
-
status: "
|
|
35
|
-
detail:
|
|
36
|
-
hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to sign in again.",
|
|
25
|
+
status: "ok",
|
|
26
|
+
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
37
27
|
};
|
|
38
28
|
}
|
|
39
29
|
const apiUrl = resolveApiUrl(cfg);
|
|
@@ -59,11 +49,10 @@ async function checkAuth() {
|
|
|
59
49
|
};
|
|
60
50
|
}
|
|
61
51
|
const data = (await res.json());
|
|
62
|
-
const suffix = expiresSoon ? " (token refreshed)" : "";
|
|
63
52
|
return {
|
|
64
53
|
name: "Auth",
|
|
65
54
|
status: "ok",
|
|
66
|
-
detail: data.email ? `Signed in as ${data.email}
|
|
55
|
+
detail: data.email ? `Signed in as ${data.email}` : "Signed in",
|
|
67
56
|
};
|
|
68
57
|
}
|
|
69
58
|
catch (err) {
|
|
@@ -81,6 +70,9 @@ async function checkMcp() {
|
|
|
81
70
|
{ name: "Claude Code", path: join(homedir(), ".claude.json") },
|
|
82
71
|
{ name: "Claude Code (settings)", path: join(homedir(), ".claude", "settings.json") },
|
|
83
72
|
{ name: "Codex", path: join(homedir(), ".codex", "config.toml") },
|
|
73
|
+
{ name: "Cursor", path: join(homedir(), ".cursor", "mcp.json") },
|
|
74
|
+
{ name: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json") },
|
|
75
|
+
{ name: "Kimi", path: join(homedir(), ".kimi", "mcp.json") },
|
|
84
76
|
{ name: "Gemini", path: join(homedir(), ".gemini", "settings.json") },
|
|
85
77
|
];
|
|
86
78
|
const found = [];
|
|
@@ -91,7 +83,7 @@ async function checkMcp() {
|
|
|
91
83
|
// package name is @floomhq/floom-mcp-sync. Detect by package name OR
|
|
92
84
|
// by "floom" + "mcp_servers"/"mcpServers" co-occurrence.
|
|
93
85
|
const hasFloom = buf.includes("@floomhq/floom-mcp-sync") ||
|
|
94
|
-
(buf.includes("\"floom\"") && (buf.includes("mcpServers") || buf.includes("mcp_servers"))) ||
|
|
86
|
+
(buf.includes("\"floom\"") && (buf.includes("mcpServers") || buf.includes("mcp_servers") || buf.includes("\"mcp\""))) ||
|
|
95
87
|
(buf.includes("[mcp_servers.floom]"));
|
|
96
88
|
if (hasFloom)
|
|
97
89
|
found.push({ name: cand.name, configPath: cand.path, hasFloom });
|
|
@@ -113,13 +105,6 @@ async function checkMcp() {
|
|
|
113
105
|
detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
|
|
114
106
|
};
|
|
115
107
|
}
|
|
116
|
-
function targetSkillsDir(target) {
|
|
117
|
-
if (target === "codex") {
|
|
118
|
-
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
119
|
-
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
120
|
-
}
|
|
121
|
-
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
122
|
-
}
|
|
123
108
|
async function checkTargetDir(target) {
|
|
124
109
|
const dir = targetSkillsDir(target);
|
|
125
110
|
try {
|
|
@@ -347,6 +332,7 @@ async function readExecutableVersion(path) {
|
|
|
347
332
|
}
|
|
348
333
|
export async function doctor(opts = {}) {
|
|
349
334
|
const target = opts.target ?? "claude";
|
|
335
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
350
336
|
const checks = await Promise.all([
|
|
351
337
|
checkCliCommand(),
|
|
352
338
|
checkAuth(),
|
|
@@ -355,22 +341,6 @@ export async function doctor(opts = {}) {
|
|
|
355
341
|
checkLastSync(target),
|
|
356
342
|
checkVersion(),
|
|
357
343
|
]);
|
|
358
|
-
const anyFail = checks.some((check) => check.status === "fail");
|
|
359
|
-
const anyWarn = checks.some((check) => check.status === "warn");
|
|
360
|
-
if (opts.json) {
|
|
361
|
-
process.stdout.write(`${JSON.stringify({
|
|
362
|
-
ok: !anyFail,
|
|
363
|
-
status: anyFail ? "fail" : anyWarn ? "warn" : "ok",
|
|
364
|
-
version: CLI_VERSION,
|
|
365
|
-
target,
|
|
366
|
-
checks,
|
|
367
|
-
configPath: CONFIG_PATH,
|
|
368
|
-
})}\n`);
|
|
369
|
-
if (anyFail)
|
|
370
|
-
process.exit(1);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
374
344
|
// Compute column widths for clean table output.
|
|
375
345
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
376
346
|
for (const check of checks) {
|
|
@@ -380,6 +350,8 @@ export async function doctor(opts = {}) {
|
|
|
380
350
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
381
351
|
}
|
|
382
352
|
}
|
|
353
|
+
const anyFail = checks.some((c) => c.status === "fail");
|
|
354
|
+
const anyWarn = checks.some((c) => c.status === "warn");
|
|
383
355
|
process.stdout.write("\n");
|
|
384
356
|
if (anyFail) {
|
|
385
357
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
package/dist/errors.js
CHANGED
|
@@ -20,7 +20,7 @@ export function friendlyHttp(status, action) {
|
|
|
20
20
|
return new FloomError(`You don't have permission to ${action}.`);
|
|
21
21
|
}
|
|
22
22
|
if (status === 404) {
|
|
23
|
-
if (/fetch|inspect|add|install|show|get|search|list|info
|
|
23
|
+
if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
|
|
24
24
|
return new FloomError("Skill not found.", "Check the link or slug, then try again.");
|
|
25
25
|
}
|
|
26
26
|
return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
|
package/dist/install.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open
|
|
3
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { lstat, mkdir, open } from "node:fs/promises";
|
|
4
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
4
|
import ora from "ora";
|
|
6
5
|
import { readConfig, resolveApiUrl } from "./config.js";
|
|
@@ -9,6 +8,7 @@ import { c, symbols } from "./ui.js";
|
|
|
9
8
|
import { FloomError } from "./errors.js";
|
|
10
9
|
import { normalizeRemotePackageFiles, packageHash, sha256Bytes } from "./package.js";
|
|
11
10
|
import { manifestKey, markSynced, readSyncManifest, withSyncLock, writeSyncManifest } from "./sync-manifest.js";
|
|
11
|
+
import { targetLabel, targetSkillsDir, targetSkillsDirEnv } from "./targets.js";
|
|
12
12
|
const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
13
13
|
const FD_PATH_ROOT = "/proc/self/fd";
|
|
14
14
|
function slugFromInput(input) {
|
|
@@ -22,13 +22,6 @@ function slugFromInput(input) {
|
|
|
22
22
|
return trimmed.replace(/\.(md|json)$/i, "");
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
function skillsDir(target) {
|
|
26
|
-
if (target === "codex") {
|
|
27
|
-
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
28
|
-
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
29
|
-
}
|
|
30
|
-
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
31
|
-
}
|
|
32
25
|
function skillPath(root, slug) {
|
|
33
26
|
return join(root, slug, "SKILL.md");
|
|
34
27
|
}
|
|
@@ -36,7 +29,7 @@ function legacySkillPath(root, slug) {
|
|
|
36
29
|
return join(root, `${slug}.md`);
|
|
37
30
|
}
|
|
38
31
|
function skillsDirHint(target) {
|
|
39
|
-
return target
|
|
32
|
+
return targetSkillsDirEnv(target);
|
|
40
33
|
}
|
|
41
34
|
function setupCommand(target) {
|
|
42
35
|
return `npx -y @floomhq/floom setup --target ${target} --yes`;
|
|
@@ -66,14 +59,12 @@ async function readLocalFile(path) {
|
|
|
66
59
|
throw err;
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
|
-
async function
|
|
62
|
+
async function localPackageHash(root, slug, target, files) {
|
|
70
63
|
const main = await readLocalFile(target);
|
|
71
64
|
if (main === null) {
|
|
72
65
|
const legacy = await readLocalFile(legacySkillPath(root, slug));
|
|
73
|
-
if (legacy !== null &&
|
|
74
|
-
return
|
|
75
|
-
if (legacy !== null)
|
|
76
|
-
return { hash: packageHash(legacy.toString("utf8"), []), source: "legacy" };
|
|
66
|
+
if (legacy !== null && files.length === 0)
|
|
67
|
+
return packageHash(legacy.toString("utf8"), []);
|
|
77
68
|
return null;
|
|
78
69
|
}
|
|
79
70
|
const localFiles = [];
|
|
@@ -83,7 +74,7 @@ async function localPackageState(root, slug, target, files) {
|
|
|
83
74
|
return null;
|
|
84
75
|
localFiles.push({ path: file.path, bytes, sha256: file.sha256 });
|
|
85
76
|
}
|
|
86
|
-
return
|
|
77
|
+
return packageHash(main.toString("utf8"), localFiles);
|
|
87
78
|
}
|
|
88
79
|
async function markInstallSynced(root, slug, files) {
|
|
89
80
|
const manifest = await readSyncManifest();
|
|
@@ -121,33 +112,6 @@ async function writeInstallFile(root, target, body) {
|
|
|
121
112
|
await parent.close();
|
|
122
113
|
}
|
|
123
114
|
}
|
|
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
|
-
}
|
|
151
115
|
async function overwriteInstallFile(root, target, body) {
|
|
152
116
|
const parent = await openSafeParentDirectory(root, target);
|
|
153
117
|
const handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
|
|
@@ -225,14 +189,14 @@ async function assertSafeDirectory(path) {
|
|
|
225
189
|
}
|
|
226
190
|
export async function install(slugInput, opts = {}) {
|
|
227
191
|
const targetAgent = opts.target ?? "claude";
|
|
228
|
-
const root =
|
|
192
|
+
const root = targetSkillsDir(targetAgent);
|
|
229
193
|
const slug = slugFromInput(slugInput);
|
|
230
194
|
if (!SLUG_RE.test(slug)) {
|
|
231
195
|
throw new FloomError(`Invalid skill slug: ${slugInput}`);
|
|
232
196
|
}
|
|
233
197
|
const cfg = await readConfig();
|
|
234
198
|
const apiUrl = resolveApiUrl(cfg);
|
|
235
|
-
const spinner =
|
|
199
|
+
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
236
200
|
let detail;
|
|
237
201
|
try {
|
|
238
202
|
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
@@ -241,7 +205,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
241
205
|
}
|
|
242
206
|
}
|
|
243
207
|
catch (err) {
|
|
244
|
-
spinner
|
|
208
|
+
spinner.stop();
|
|
245
209
|
throw err;
|
|
246
210
|
}
|
|
247
211
|
const target = skillPath(root, slug);
|
|
@@ -267,29 +231,21 @@ export async function install(slugInput, opts = {}) {
|
|
|
267
231
|
}
|
|
268
232
|
throw err;
|
|
269
233
|
}
|
|
270
|
-
|
|
234
|
+
await ensureSafeParentDirectory(root, target);
|
|
235
|
+
const existing = await localPackageHash(root, slug, target, remotePackageFiles);
|
|
271
236
|
const conflictingTarget = await preflightInstallPackage(root, installFiles, opts.force ? { force: true } : {});
|
|
272
237
|
if (conflictingTarget) {
|
|
273
238
|
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("/")}`);
|
|
274
239
|
}
|
|
275
|
-
if (existing
|
|
276
|
-
await removeLegacySkillFile(root, slug);
|
|
240
|
+
if (existing === remoteHash) {
|
|
277
241
|
action = "unchanged";
|
|
278
242
|
}
|
|
279
|
-
else if (
|
|
243
|
+
else if (opts.force) {
|
|
280
244
|
try {
|
|
281
|
-
|
|
282
|
-
await writeInstallFile(root, target, detail.body_md);
|
|
283
|
-
else
|
|
284
|
-
await overwriteInstallFile(root, target, detail.body_md);
|
|
245
|
+
await overwriteInstallFile(root, target, detail.body_md);
|
|
285
246
|
for (const file of remotePackageFiles) {
|
|
286
|
-
|
|
287
|
-
if (existing.source === "legacy")
|
|
288
|
-
await writeInstallFile(root, fileTarget, file.bytes);
|
|
289
|
-
else
|
|
290
|
-
await overwriteInstallFile(root, fileTarget, file.bytes);
|
|
247
|
+
await overwriteInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
291
248
|
}
|
|
292
|
-
await removeLegacySkillFile(root, slug);
|
|
293
249
|
}
|
|
294
250
|
catch (err) {
|
|
295
251
|
const code = err.code;
|
|
@@ -297,7 +253,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
297
253
|
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
298
254
|
throw err;
|
|
299
255
|
}
|
|
300
|
-
action =
|
|
256
|
+
action = "updated";
|
|
301
257
|
}
|
|
302
258
|
else if (existing !== null) {
|
|
303
259
|
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.");
|
|
@@ -308,7 +264,6 @@ export async function install(slugInput, opts = {}) {
|
|
|
308
264
|
for (const file of remotePackageFiles) {
|
|
309
265
|
await writeInstallFile(root, join(dirname(target), file.path), file.bytes);
|
|
310
266
|
}
|
|
311
|
-
await removeLegacySkillFile(root, slug);
|
|
312
267
|
}
|
|
313
268
|
catch (err) {
|
|
314
269
|
const code = err.code;
|
|
@@ -332,17 +287,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
332
287
|
manifestWarning = err instanceof Error ? err.message : String(err);
|
|
333
288
|
}
|
|
334
289
|
});
|
|
335
|
-
spinner
|
|
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
|
-
}
|
|
290
|
+
spinner.stop();
|
|
346
291
|
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
347
292
|
process.stdout.write(` ${c.dim(dirname(target))}\n\n`);
|
|
348
293
|
if (manifestWarning) {
|
|
@@ -350,7 +295,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
350
295
|
}
|
|
351
296
|
process.stdout.write(` ${c.bold("Next")}\n`);
|
|
352
297
|
if (opts.setup) {
|
|
353
|
-
process.stdout.write(` ${c.dim("1.")} Floom is connecting ${targetAgent
|
|
298
|
+
process.stdout.write(` ${c.dim("1.")} Floom is connecting ${targetLabel(targetAgent)} now.\n`);
|
|
354
299
|
process.stdout.write(` ${c.dim("2.")} Tell your agent to use ${c.bold(slug)} when it matches the task.\n\n`);
|
|
355
300
|
}
|
|
356
301
|
else {
|
package/dist/login.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createServer as createNetServer } from "node:net";
|
|
3
|
-
import { randomBytes } from "node:crypto";
|
|
4
3
|
import open from "open";
|
|
5
4
|
import ora from "ora";
|
|
6
5
|
import { getApiUrl, writeConfig } from "./config.js";
|
|
@@ -8,19 +7,18 @@ import { c, header, symbols } from "./ui.js";
|
|
|
8
7
|
import { FloomError, friendlyHttp, friendlyNetwork } from "./errors.js";
|
|
9
8
|
const DEFAULT_PORT = 7456;
|
|
10
9
|
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
-
export async function login(
|
|
10
|
+
export async function login() {
|
|
12
11
|
const apiUrl = getApiUrl();
|
|
13
12
|
const port = await pickPort();
|
|
14
|
-
const providerLabel = provider === "github" ? "GitHub" : "Google";
|
|
15
13
|
process.stdout.write(header());
|
|
16
|
-
process.stdout.write(`${symbols.arrow} Opening browser to sign in with
|
|
14
|
+
process.stdout.write(`${symbols.arrow} Opening browser to sign in with Google...\n\n`);
|
|
17
15
|
const spinner = ora({
|
|
18
16
|
text: c.dim("Waiting for sign-in to complete..."),
|
|
19
17
|
color: "yellow",
|
|
20
18
|
}).start();
|
|
21
19
|
let tokens;
|
|
22
20
|
try {
|
|
23
|
-
tokens = await waitForCallback(port
|
|
21
|
+
tokens = await waitForCallback(port);
|
|
24
22
|
}
|
|
25
23
|
catch (err) {
|
|
26
24
|
spinner.stop();
|
|
@@ -91,10 +89,9 @@ function reserveEphemeralPort() {
|
|
|
91
89
|
});
|
|
92
90
|
});
|
|
93
91
|
}
|
|
94
|
-
function waitForCallback(port
|
|
92
|
+
function waitForCallback(port) {
|
|
95
93
|
return new Promise((resolve, reject) => {
|
|
96
94
|
const apiUrl = getApiUrl();
|
|
97
|
-
const state = randomBytes(32).toString("base64url");
|
|
98
95
|
let settled = false;
|
|
99
96
|
const server = createServer((req, res) => {
|
|
100
97
|
// CORS preflight from the browser bridge page.
|
|
@@ -125,15 +122,6 @@ function waitForCallback(port, provider) {
|
|
|
125
122
|
res.end(localCallbackPage("Missing tokens from OAuth response."));
|
|
126
123
|
return;
|
|
127
124
|
}
|
|
128
|
-
if (data.state !== state) {
|
|
129
|
-
res.writeHead(400, {
|
|
130
|
-
"access-control-allow-origin": origin,
|
|
131
|
-
"access-control-allow-private-network": "true",
|
|
132
|
-
"content-type": "text/html; charset=utf-8",
|
|
133
|
-
});
|
|
134
|
-
res.end(localCallbackPage("Invalid sign-in state."));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
125
|
res.writeHead(200, {
|
|
138
126
|
"access-control-allow-origin": origin,
|
|
139
127
|
"access-control-allow-private-network": "true",
|
|
@@ -151,11 +139,6 @@ function waitForCallback(port, provider) {
|
|
|
151
139
|
});
|
|
152
140
|
return;
|
|
153
141
|
}
|
|
154
|
-
if (req.method === "GET" && req.url?.startsWith("/cli-callback")) {
|
|
155
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
156
|
-
res.end(localCallbackBridgePage());
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
142
|
res.writeHead(404, { "content-type": "text/plain" });
|
|
160
143
|
res.end("Not found");
|
|
161
144
|
});
|
|
@@ -178,12 +161,11 @@ function waitForCallback(port, provider) {
|
|
|
178
161
|
reject(new FloomError(`Local auth server failed on port ${port}.`, `Is port ${port} already in use? (${err.message})`));
|
|
179
162
|
});
|
|
180
163
|
server.listen(port, "127.0.0.1", () => {
|
|
181
|
-
const target = `${apiUrl}/auth/cli?port=${port}
|
|
182
|
-
process.stdout.write(c.dim("If the browser does not open, copy this URL:") +
|
|
183
|
-
`\n${c.cyan(target)}\n\n`);
|
|
164
|
+
const target = `${apiUrl}/auth/cli?port=${port}`;
|
|
184
165
|
open(target).catch((e) => {
|
|
185
166
|
const msg = e instanceof Error ? e.message : String(e);
|
|
186
|
-
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`)
|
|
167
|
+
process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
|
|
168
|
+
c.dim(`Open this URL manually: ${target}\n`));
|
|
187
169
|
});
|
|
188
170
|
});
|
|
189
171
|
});
|
|
@@ -193,7 +175,7 @@ function parseCallbackBody(body, contentType) {
|
|
|
193
175
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
194
176
|
const params = new URLSearchParams(body);
|
|
195
177
|
const parsed = {};
|
|
196
|
-
for (const key of ["access_token", "refresh_token", "expires_in", "token_type"
|
|
178
|
+
for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
|
|
197
179
|
const value = params.get(key);
|
|
198
180
|
if (value)
|
|
199
181
|
parsed[key] = value;
|
|
@@ -202,47 +184,6 @@ function parseCallbackBody(body, contentType) {
|
|
|
202
184
|
}
|
|
203
185
|
return JSON.parse(body);
|
|
204
186
|
}
|
|
205
|
-
function localCallbackBridgePage() {
|
|
206
|
-
return `<!doctype html>
|
|
207
|
-
<html lang="en">
|
|
208
|
-
<head>
|
|
209
|
-
<meta charset="utf-8">
|
|
210
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
211
|
-
<title>Floom CLI sign-in</title>
|
|
212
|
-
</head>
|
|
213
|
-
<body>
|
|
214
|
-
<p>Completing Floom CLI sign-in...</p>
|
|
215
|
-
<script>
|
|
216
|
-
(async function () {
|
|
217
|
-
try {
|
|
218
|
-
var params = new URLSearchParams(window.location.hash.slice(1));
|
|
219
|
-
if (!params.get("access_token") || !params.get("refresh_token")) {
|
|
220
|
-
document.body.textContent = "OAuth response missing tokens.";
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
var body = new URLSearchParams();
|
|
224
|
-
["access_token", "refresh_token", "expires_in", "token_type", "state"].forEach(function (key) {
|
|
225
|
-
var value = params.get(key);
|
|
226
|
-
if (value) body.set(key, value);
|
|
227
|
-
});
|
|
228
|
-
window.history.replaceState(null, "", window.location.pathname);
|
|
229
|
-
var res = await fetch("/cli-callback", {
|
|
230
|
-
method: "POST",
|
|
231
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
232
|
-
body: body.toString()
|
|
233
|
-
});
|
|
234
|
-
var html = await res.text();
|
|
235
|
-
document.open();
|
|
236
|
-
document.write(html);
|
|
237
|
-
document.close();
|
|
238
|
-
} catch (e) {
|
|
239
|
-
document.body.textContent = "Could not complete Floom CLI sign-in. Return to your terminal and run floom login again.";
|
|
240
|
-
}
|
|
241
|
-
})();
|
|
242
|
-
</script>
|
|
243
|
-
</body>
|
|
244
|
-
</html>`;
|
|
245
|
-
}
|
|
246
187
|
function localCallbackPage(message) {
|
|
247
188
|
const safeMessage = escapeHtml(message);
|
|
248
189
|
return `<!doctype html>
|
package/dist/mcp.js
CHANGED
|
@@ -2,15 +2,18 @@ import { c } from "./ui.js";
|
|
|
2
2
|
export function printMcpSetup() {
|
|
3
3
|
const snippet = `## Floom
|
|
4
4
|
- Use Floom skills from the local Floom skills folder when they match the task.
|
|
5
|
-
- To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude
|
|
5
|
+
- To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target <claude|codex|cursor|opencode|kimi>\`.
|
|
6
6
|
- To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
|
|
7
|
-
- MCP sync
|
|
7
|
+
- MCP sync uses the skills directory configured for that agent; set \`FLOOM_SKILLS_DIR\` or the agent-specific env var when registering the server.`;
|
|
8
8
|
process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
|
|
9
9
|
process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
|
|
10
10
|
process.stdout.write(` ${c.bold("Claude Code")}\n`);
|
|
11
11
|
process.stdout.write(` ${c.cyan("claude mcp add floom -- npx -y @floomhq/floom-mcp-sync")}\n\n`);
|
|
12
12
|
process.stdout.write(` ${c.bold("Codex CLI")}\n`);
|
|
13
13
|
process.stdout.write(` ${c.cyan("codex mcp add floom -- npx -y @floomhq/floom-mcp-sync")}\n\n`);
|
|
14
|
+
process.stdout.write(` ${c.bold("Cursor / OpenCode / Kimi")}\n`);
|
|
15
|
+
process.stdout.write(` ${c.cyan("Run the agent's MCP registration flow with FLOOM_SKILLS_DIR set to its skills root.")}\n`);
|
|
16
|
+
process.stdout.write(` ${c.dim("Examples: ~/.cursor/skills-cursor, ~/.config/opencode/skills, ~/.kimi/skills")}\n\n`);
|
|
14
17
|
process.stdout.write(`${c.dim("Full guide:")} ${c.cyan("https://floom.dev/docs/mcp")}\n\n`);
|
|
15
18
|
process.stdout.write(`${c.dim("Recommended agent instruction snippet:")}\n\n`);
|
|
16
19
|
process.stdout.write(`${snippet}\n\n`);
|