@floomhq/floom 1.0.25 → 1.0.27

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/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 { needsRefresh, readConfig, readRawConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
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: "fail",
35
- detail: expired ? "Token expired and refresh failed." : "Token refresh failed.",
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}${suffix}` : `Signed in${suffix}`,
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 = [];
@@ -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|load/i.test(action)) {
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, readdir, rmdir, unlink } from "node:fs/promises";
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 === "codex" ? "CODEX_SKILLS_DIR" : "CLAUDE_SKILLS_DIR";
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 localPackageState(root, slug, target, files) {
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 && legacy.length === 0)
74
- return null;
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 { hash: packageHash(main.toString("utf8"), localFiles), source: "native" };
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 = skillsDir(targetAgent);
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 = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
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?.stop();
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
- const existing = await localPackageState(root, slug, target, remotePackageFiles);
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?.hash === remoteHash && existing.source === "native") {
276
- await removeLegacySkillFile(root, slug);
240
+ if (existing === remoteHash) {
277
241
  action = "unchanged";
278
242
  }
279
- else if (existing !== null && (opts.force || (existing.source === "legacy" && existing.hash === remoteHash))) {
243
+ else if (opts.force) {
280
244
  try {
281
- if (existing.source === "legacy")
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
- 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);
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 = existing.source === "legacy" ? "installed" : "updated";
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?.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
- }
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 === "claude" ? "Claude Code" : "Codex"} now.\n`);
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(provider = "google") {
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 ${providerLabel}...\n\n`);
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, provider);
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, provider) {
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}&provider=${provider}&state=${encodeURIComponent(state)}`;
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", "state"]) {
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\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
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 is optional; use it while the Floom MCP server is configured and running.`;
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`);