@composer-app/mcp 0.0.1-beta.2 → 0.0.1-beta.3

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
@@ -4,16 +4,824 @@ import {
4
4
  logError,
5
5
  startMcpHttpServer,
6
6
  startMcpServer
7
- } from "./chunk-SZ67UYAY.js";
7
+ } from "./chunk-VVYEIOFH.js";
8
8
 
9
- // src/cli.ts
9
+ // src/setup.ts
10
+ import * as fs from "fs/promises";
11
+ import * as path from "path";
12
+ import * as os from "os";
10
13
  import { execFile } from "child_process";
11
14
  import { promisify } from "util";
12
- import fs from "fs/promises";
13
- import path from "path";
14
- import os from "os";
15
15
  import { fileURLToPath } from "url";
16
- var exec = promisify(execFile);
16
+
17
+ // src/setup-ui.ts
18
+ import * as readline from "readline";
19
+ var ESC = "\x1B[";
20
+ var seq = {
21
+ RESET: `${ESC}0m`,
22
+ BOLD: `${ESC}1m`,
23
+ DIM: `${ESC}2m`,
24
+ ORANGE: `${ESC}38;5;208m`,
25
+ // 256-color orange ≈ #E16900
26
+ GRAY: `${ESC}90m`,
27
+ HIDE: `${ESC}?25l`,
28
+ SHOW: `${ESC}?25h`,
29
+ CLEAR_RIGHT: `${ESC}K`,
30
+ CLEAR_DOWN: `${ESC}0J`
31
+ };
32
+ var write = (s) => {
33
+ process.stdout.write(s);
34
+ };
35
+ var up = (n) => n > 0 ? `${ESC}${n}A` : "";
36
+ var paint = (s, code) => `${code}${s}${seq.RESET}`;
37
+ var orange = (s) => paint(s, seq.ORANGE);
38
+ var bold = (s) => paint(s, seq.BOLD);
39
+ var dim = (s) => paint(s, seq.DIM);
40
+ var gray = (s) => paint(s, seq.GRAY);
41
+ var BANNER = [
42
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
43
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
44
+ "\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
45
+ "\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
46
+ "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551",
47
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
48
+ ];
49
+ function printIntro() {
50
+ write("\n");
51
+ for (const line of BANNER) write(` ${orange(line)}
52
+ `);
53
+ write("\n");
54
+ write(
55
+ ` ${bold("Realtime collaborative markdown")} for you and your AI agents.
56
+ `
57
+ );
58
+ write(` Same doc, live \u2014 comments, suggestions, edits. No copy-paste.
59
+ `);
60
+ write("\n");
61
+ }
62
+ function section(title, opts = {}) {
63
+ if (opts.first) {
64
+ if (opts.rule !== false) {
65
+ const width = Math.max(10, (process.stdout.columns ?? 72) - 4);
66
+ write(` ${gray("\u2500".repeat(width))}
67
+
68
+
69
+ `);
70
+ }
71
+ } else {
72
+ write("\n\n");
73
+ }
74
+ write(`${orange("\u25B8")} ${bold(title)}
75
+ `);
76
+ write("\n");
77
+ }
78
+ function note(lines) {
79
+ for (const line of lines) write(` ${dim(line)}
80
+ `);
81
+ write("\n");
82
+ }
83
+ function success(msg) {
84
+ write(` ${orange("\u2713")} ${msg}
85
+ `);
86
+ }
87
+ function failure(msg) {
88
+ write(` ${paint("\u2717", `${ESC}31m`)} ${msg}
89
+ `);
90
+ }
91
+ function skipped(msg) {
92
+ write(` ${gray("\xB7")} ${dim(msg)}
93
+ `);
94
+ }
95
+ function isInteractive() {
96
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
97
+ }
98
+ async function withRawStdin(handle) {
99
+ return new Promise((resolve) => {
100
+ const stdin = process.stdin;
101
+ readline.emitKeypressEvents(stdin);
102
+ stdin.setRawMode(true);
103
+ stdin.resume();
104
+ write(seq.HIDE);
105
+ const cleanup = () => {
106
+ stdin.removeListener("keypress", onKey);
107
+ stdin.setRawMode(false);
108
+ stdin.pause();
109
+ write(seq.SHOW);
110
+ };
111
+ const onKey = (_str, key) => {
112
+ if (key?.ctrl && key.name === "c") {
113
+ cleanup();
114
+ write("\n");
115
+ process.exit(130);
116
+ }
117
+ if (handle(key) === "done") {
118
+ cleanup();
119
+ resolve();
120
+ }
121
+ };
122
+ stdin.on("keypress", onKey);
123
+ });
124
+ }
125
+ var POINTER = "\u276F";
126
+ var CHECKED = "\u25C9";
127
+ var UNCHECKED = "\u25EF";
128
+ function clamp(n, lo, hi) {
129
+ return Math.max(lo, Math.min(hi, n));
130
+ }
131
+ function clearWidget(height) {
132
+ write(up(height));
133
+ write(seq.CLEAR_DOWN);
134
+ }
135
+ async function select(question, options, opts = {}) {
136
+ if (options.length === 0) return { kind: "skip" };
137
+ const defaultIndex = opts.defaultIndex ?? 0;
138
+ const canGoBack = opts.canGoBack ?? false;
139
+ const height = 1 + 1 + options.length + 1 + 1;
140
+ let cursor = clamp(defaultIndex, 0, options.length - 1);
141
+ const helpText = canGoBack ? "\u2191/\u2193 navigate \xB7 enter confirm \xB7 b back \xB7 q skip" : "\u2191/\u2193 navigate \xB7 enter confirm \xB7 q skip";
142
+ const render = () => {
143
+ let out = "";
144
+ out += ` ${bold(question)}${seq.CLEAR_RIGHT}
145
+ `;
146
+ out += `${seq.CLEAR_RIGHT}
147
+ `;
148
+ for (let i = 0; i < options.length; i++) {
149
+ const opt = options[i];
150
+ const active = i === cursor;
151
+ const pointer = active ? orange(POINTER) : " ";
152
+ const label = active ? bold(opt.label) : opt.label;
153
+ const hint = opt.hint ? dim(` ${opt.hint}`) : "";
154
+ out += ` ${pointer} ${label}${hint}${seq.CLEAR_RIGHT}
155
+ `;
156
+ }
157
+ out += `${seq.CLEAR_RIGHT}
158
+ `;
159
+ out += ` ${gray(helpText)}${seq.CLEAR_RIGHT}
160
+ `;
161
+ return out;
162
+ };
163
+ write(render());
164
+ let outcome = { kind: "skip" };
165
+ await withRawStdin((key) => {
166
+ if (key.name === "up" || key.name === "k") {
167
+ cursor = (cursor - 1 + options.length) % options.length;
168
+ } else if (key.name === "down" || key.name === "j") {
169
+ cursor = (cursor + 1) % options.length;
170
+ } else if (key.name === "return") {
171
+ outcome = { kind: "ok", value: options[cursor].value };
172
+ return "done";
173
+ } else if (canGoBack && key.name === "b") {
174
+ outcome = { kind: "back" };
175
+ return "done";
176
+ } else if (key.name === "q" || key.name === "escape") {
177
+ outcome = { kind: "skip" };
178
+ return "done";
179
+ } else {
180
+ return "continue";
181
+ }
182
+ write(up(height) + render());
183
+ return "continue";
184
+ });
185
+ clearWidget(height);
186
+ return outcome;
187
+ }
188
+ async function multiselect(question, options, opts = {}) {
189
+ if (options.length === 0) return { kind: "ok", value: [] };
190
+ const canGoBack = opts.canGoBack ?? false;
191
+ const checks = (opts.defaultSelected ?? options.map(() => true)).slice();
192
+ let cursor = 0;
193
+ const height = 1 + 1 + options.length + 1 + 1;
194
+ const helpText = canGoBack ? "\u2191/\u2193 navigate \xB7 space toggle \xB7 a all \xB7 enter confirm \xB7 b back \xB7 q skip" : "\u2191/\u2193 navigate \xB7 space toggle \xB7 a all \xB7 enter confirm \xB7 q skip";
195
+ const render = () => {
196
+ let out = "";
197
+ out += ` ${bold(question)}${seq.CLEAR_RIGHT}
198
+ `;
199
+ out += `${seq.CLEAR_RIGHT}
200
+ `;
201
+ for (let i = 0; i < options.length; i++) {
202
+ const opt = options[i];
203
+ const active = i === cursor;
204
+ const pointer = active ? orange(POINTER) : " ";
205
+ const box = checks[i] ? orange(CHECKED) : gray(UNCHECKED);
206
+ const label = active ? bold(opt.label) : opt.label;
207
+ const hint = opt.hint ? dim(` ${opt.hint}`) : "";
208
+ out += ` ${pointer} ${box} ${label}${hint}${seq.CLEAR_RIGHT}
209
+ `;
210
+ }
211
+ out += `${seq.CLEAR_RIGHT}
212
+ `;
213
+ out += ` ${gray(helpText)}${seq.CLEAR_RIGHT}
214
+ `;
215
+ return out;
216
+ };
217
+ write(render());
218
+ let outcome = { kind: "skip" };
219
+ await withRawStdin((key) => {
220
+ if (key.name === "up" || key.name === "k") {
221
+ cursor = (cursor - 1 + options.length) % options.length;
222
+ } else if (key.name === "down" || key.name === "j") {
223
+ cursor = (cursor + 1) % options.length;
224
+ } else if (key.name === "space") {
225
+ checks[cursor] = !checks[cursor];
226
+ } else if (key.name === "a") {
227
+ const allOn = checks.every((c) => c);
228
+ for (let i = 0; i < checks.length; i++) checks[i] = !allOn;
229
+ } else if (key.name === "return") {
230
+ outcome = {
231
+ kind: "ok",
232
+ value: options.filter((_, i) => checks[i]).map((o) => o.value)
233
+ };
234
+ return "done";
235
+ } else if (canGoBack && key.name === "b") {
236
+ outcome = { kind: "back" };
237
+ return "done";
238
+ } else if (key.name === "q" || key.name === "escape") {
239
+ outcome = { kind: "skip" };
240
+ return "done";
241
+ } else {
242
+ return "continue";
243
+ }
244
+ write(up(height) + render());
245
+ return "continue";
246
+ });
247
+ clearWidget(height);
248
+ return outcome;
249
+ }
250
+ async function confirm(question, opts = {}) {
251
+ const defaultYes = opts.defaultYes ?? true;
252
+ const r = await select(
253
+ question,
254
+ [
255
+ { value: true, label: "Yes" },
256
+ { value: false, label: "No" }
257
+ ],
258
+ { defaultIndex: defaultYes ? 0 : 1, canGoBack: opts.canGoBack }
259
+ );
260
+ return r;
261
+ }
262
+
263
+ // src/setup.ts
264
+ var execP = promisify(execFile);
265
+ var MCP_NAME = "composer-mcp";
266
+ var MCP_COMMAND = "npx";
267
+ var MCP_ARGS = ["-y", "@composer-app/mcp@latest", "mcp"];
268
+ var HINT_MARKER = "`composer-mcp` tools";
269
+ var HINT_BLOCK = `
270
+ ## Composer
271
+
272
+ - When generating markdown the user may want to review or iterate on (plans, writing, skills, docs), offer to send it to Composer via the \`composer-mcp\` tools without being asked.
273
+ `;
274
+ var HOME = os.homedir();
275
+ var HARNESSES = [
276
+ {
277
+ id: "claude-code",
278
+ name: "Claude Code",
279
+ configDir: path.join(HOME, ".claude"),
280
+ cli: "claude",
281
+ userRuleFile: path.join(HOME, ".claude", "CLAUDE.md"),
282
+ async registerMcp(env) {
283
+ const args = ["mcp", "add", "--scope", "user", MCP_NAME];
284
+ for (const [k, v] of Object.entries(env)) args.push("-e", `${k}=${v}`);
285
+ args.push("--", MCP_COMMAND, ...MCP_ARGS);
286
+ await execP("claude", args);
287
+ }
288
+ },
289
+ {
290
+ id: "codex",
291
+ name: "OpenAI Codex",
292
+ configDir: path.join(HOME, ".codex"),
293
+ cli: "codex",
294
+ userRuleFile: path.join(HOME, ".codex", "AGENTS.md"),
295
+ async registerMcp(env) {
296
+ await appendCodexToml(env);
297
+ }
298
+ },
299
+ {
300
+ id: "cursor",
301
+ name: "Cursor",
302
+ configDir: path.join(HOME, ".cursor"),
303
+ userRuleFile: null,
304
+ async registerMcp(env) {
305
+ await writeMcpJsonConfig(path.join(HOME, ".cursor", "mcp.json"), env);
306
+ }
307
+ },
308
+ {
309
+ id: "gemini",
310
+ name: "Gemini CLI",
311
+ configDir: path.join(HOME, ".gemini"),
312
+ cli: "gemini",
313
+ userRuleFile: path.join(HOME, ".gemini", "GEMINI.md"),
314
+ async registerMcp(env) {
315
+ await writeMcpJsonConfig(
316
+ path.join(HOME, ".gemini", "settings.json"),
317
+ env
318
+ );
319
+ }
320
+ },
321
+ {
322
+ id: "windsurf",
323
+ name: "Windsurf",
324
+ configDir: path.join(HOME, ".codeium", "windsurf"),
325
+ userRuleFile: null,
326
+ async registerMcp(env) {
327
+ await writeMcpJsonConfig(
328
+ path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
329
+ env
330
+ );
331
+ }
332
+ }
333
+ ];
334
+ async function pathExists(p) {
335
+ try {
336
+ await fs.access(p);
337
+ return true;
338
+ } catch {
339
+ return false;
340
+ }
341
+ }
342
+ async function commandAvailable(name) {
343
+ try {
344
+ if (process.platform === "win32") {
345
+ await execP("where", [name]);
346
+ } else {
347
+ await execP("sh", ["-c", `command -v ${name}`]);
348
+ }
349
+ return true;
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+ async function detectHarnesses() {
355
+ const results = await Promise.all(
356
+ HARNESSES.map(async (h) => {
357
+ if (await pathExists(h.configDir)) return h;
358
+ if (h.cli && await commandAvailable(h.cli)) return h;
359
+ return null;
360
+ })
361
+ );
362
+ return results.filter((h) => h !== null);
363
+ }
364
+ async function writeMcpJsonConfig(filePath, env) {
365
+ let config = {};
366
+ try {
367
+ const raw = await fs.readFile(filePath, "utf8");
368
+ const parsed = JSON.parse(raw);
369
+ if (parsed && typeof parsed === "object") {
370
+ config = parsed;
371
+ }
372
+ } catch (err) {
373
+ if (err.code !== "ENOENT") throw err;
374
+ }
375
+ const servers = config.mcpServers ?? {};
376
+ servers[MCP_NAME] = { command: MCP_COMMAND, args: MCP_ARGS, env };
377
+ config.mcpServers = servers;
378
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
379
+ await fs.writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
380
+ }
381
+ async function appendCodexToml(env) {
382
+ const filePath = path.join(HOME, ".codex", "config.toml");
383
+ let existing = "";
384
+ try {
385
+ existing = await fs.readFile(filePath, "utf8");
386
+ } catch (err) {
387
+ if (err.code !== "ENOENT") throw err;
388
+ }
389
+ const header = `[mcp_servers.${MCP_NAME}]`;
390
+ if (existing.includes(header)) return;
391
+ const argsList = MCP_ARGS.map((a) => `"${tomlEscape(a)}"`).join(", ");
392
+ const envPairs = Object.entries(env).map(([k, v]) => `"${tomlEscape(k)}" = "${tomlEscape(v)}"`).join(", ");
393
+ const block = `
394
+ ${header}
395
+ command = "${MCP_COMMAND}"
396
+ args = [${argsList}]
397
+ env = { ${envPairs} }
398
+ `;
399
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
400
+ const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
401
+ await fs.writeFile(filePath, existing + sep + block, "utf8");
402
+ }
403
+ function tomlEscape(s) {
404
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
405
+ }
406
+ async function appendHint(filePath) {
407
+ let existing = "";
408
+ let existed = true;
409
+ try {
410
+ existing = await fs.readFile(filePath, "utf8");
411
+ } catch (err) {
412
+ if (err.code !== "ENOENT") throw err;
413
+ existed = false;
414
+ }
415
+ if (existing.includes(HINT_MARKER)) return "already-present";
416
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
417
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "" : "\n";
418
+ await fs.writeFile(filePath, existing + sep + HINT_BLOCK, "utf8");
419
+ return existed ? "appended" : "created";
420
+ }
421
+ async function addClaudePermission() {
422
+ const filePath = path.join(HOME, ".claude", "settings.json");
423
+ let settings = {};
424
+ try {
425
+ const raw = await fs.readFile(filePath, "utf8");
426
+ const parsed = JSON.parse(raw);
427
+ if (parsed && typeof parsed === "object") {
428
+ settings = parsed;
429
+ }
430
+ } catch (err) {
431
+ if (err.code !== "ENOENT") throw err;
432
+ }
433
+ const permissions = settings.permissions ?? {};
434
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
435
+ const entry = `mcp__${MCP_NAME}`;
436
+ if (allow.includes(entry)) return "already-present";
437
+ allow.push(entry);
438
+ permissions.allow = allow;
439
+ settings.permissions = permissions;
440
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
441
+ await fs.writeFile(filePath, JSON.stringify(settings, null, 2) + "\n", "utf8");
442
+ return "added";
443
+ }
444
+ async function copyClaudeSkill() {
445
+ const skillDir = path.join(HOME, ".claude", "skills", "composer");
446
+ const skillPath = path.join(skillDir, "SKILL.md");
447
+ await fs.mkdir(skillDir, { recursive: true });
448
+ const pkgRoot = fileURLToPath(new URL("..", import.meta.url));
449
+ const skillSource = path.join(pkgRoot, "skill", "SKILL.md");
450
+ const skillContent = await fs.readFile(skillSource, "utf8");
451
+ await fs.writeFile(skillPath, skillContent);
452
+ return skillPath;
453
+ }
454
+ function shouldInstallSkill(plan) {
455
+ return plan.registrations.some((h) => h.id === "claude-code");
456
+ }
457
+ function emptyPlan() {
458
+ return {
459
+ registrations: [],
460
+ hintFiles: [],
461
+ addPermissions: false
462
+ };
463
+ }
464
+ function planHasWork(plan) {
465
+ return plan.registrations.length > 0 || plan.hintFiles.length > 0 || plan.addPermissions || shouldInstallSkill(plan);
466
+ }
467
+ var registerStep = async (state, canGoBack) => {
468
+ section("Register the MCP server", { first: true, rule: state.rule });
469
+ note([
470
+ "Gives your agent(s) access to Composer's tools \u2014 create docs, leave",
471
+ "comments, post suggestions, watch for @mentions."
472
+ ]);
473
+ const choices = state.detected.map((h) => ({
474
+ value: h.id,
475
+ label: h.name,
476
+ hint: "detected"
477
+ }));
478
+ const previouslyPicked = new Set(state.plan.registrations.map((h) => h.id));
479
+ const r = await multiselect(
480
+ "Which harness(es) should Composer register with?",
481
+ choices,
482
+ {
483
+ canGoBack,
484
+ defaultSelected: state.detected.map(
485
+ (h) => previouslyPicked.size === 0 ? true : previouslyPicked.has(h.id)
486
+ )
487
+ }
488
+ );
489
+ if (r.kind === "back") return "back";
490
+ if (r.kind === "skip") {
491
+ state.plan.registrations = [];
492
+ skipped("no harnesses registered");
493
+ return "prompted";
494
+ }
495
+ state.plan.registrations = state.detected.filter(
496
+ (h) => r.value.includes(h.id)
497
+ );
498
+ if (state.plan.registrations.length === 0) {
499
+ skipped("no harnesses registered");
500
+ } else {
501
+ success(
502
+ `planned: ${state.plan.registrations.map((h) => h.name).join(", ")}`
503
+ );
504
+ }
505
+ return "prompted";
506
+ };
507
+ var scopeStep = async (state, canGoBack) => {
508
+ section("Add the 'offer Composer' rule");
509
+ note([
510
+ "Adds a line to your agent's rules file (CLAUDE.md, AGENTS.md, etc.)",
511
+ "so it proactively offers to send generated markdown \u2014 plans, writing,",
512
+ "docs \u2014 to Composer instead of waiting for you to ask."
513
+ ]);
514
+ const r = await select(
515
+ "Where should the rule go?",
516
+ [
517
+ { value: "user", label: "User-level", hint: "applies to all your projects" },
518
+ { value: "project", label: "Project-level", hint: process.cwd() }
519
+ ],
520
+ { canGoBack, defaultIndex: state.scope === "project" ? 1 : 0 }
521
+ );
522
+ if (r.kind === "back") return "back";
523
+ if (r.kind === "skip") {
524
+ state.scope = null;
525
+ state.plan.hintFiles = [];
526
+ skipped("rule file unchanged");
527
+ return "prompted";
528
+ }
529
+ state.scope = r.value;
530
+ state.plan.hintFiles = [];
531
+ return "prompted";
532
+ };
533
+ var userTargetsStep = async (state, canGoBack) => {
534
+ if (state.scope !== "user") return "skipped";
535
+ const targets = state.detected.filter((h) => h.userRuleFile);
536
+ if (targets.length === 0) {
537
+ skipped(
538
+ "none of the detected harnesses have a user-level rule-file convention"
539
+ );
540
+ return "skipped";
541
+ }
542
+ const choices = targets.map((h) => ({
543
+ value: h.userRuleFile,
544
+ label: h.userRuleFile.replace(HOME, "~"),
545
+ hint: h.name
546
+ }));
547
+ const r = await multiselect("Which rule file(s) should I update?", choices, {
548
+ canGoBack,
549
+ defaultSelected: choices.map(() => true)
550
+ });
551
+ if (r.kind === "back") return "back";
552
+ if (r.kind === "skip") {
553
+ state.plan.hintFiles = [];
554
+ skipped("no rule files updated");
555
+ return "prompted";
556
+ }
557
+ state.plan.hintFiles = r.value;
558
+ if (r.value.length > 0) {
559
+ success(
560
+ `planned: ${r.value.map((f) => f.replace(HOME, "~")).join(", ")}`
561
+ );
562
+ } else {
563
+ skipped("no rule files updated");
564
+ }
565
+ return "prompted";
566
+ };
567
+ var projectTargetStep = async (state, canGoBack) => {
568
+ if (state.scope !== "project") return "skipped";
569
+ const cwd = process.cwd();
570
+ const candidates = [
571
+ "AGENTS.md",
572
+ "CLAUDE.md",
573
+ "GEMINI.md",
574
+ ".cursorrules",
575
+ ".windsurfrules"
576
+ ];
577
+ const existing = [];
578
+ for (const name of candidates) {
579
+ if (await pathExists(path.join(cwd, name))) existing.push(name);
580
+ }
581
+ let target = null;
582
+ if (existing.length === 1) {
583
+ target = path.join(cwd, existing[0]);
584
+ } else if (existing.length > 1) {
585
+ const r = await select(
586
+ "Multiple rule files found \u2014 which should I update?",
587
+ existing.map((name) => ({ value: name, label: name })),
588
+ { canGoBack }
589
+ );
590
+ if (r.kind === "back") return "back";
591
+ if (r.kind === "skip") {
592
+ state.plan.hintFiles = [];
593
+ skipped("no rule file updated");
594
+ return "prompted";
595
+ }
596
+ target = path.join(cwd, r.value);
597
+ } else {
598
+ const r = await confirm(
599
+ `No rule file found in ${cwd}. Create ${bold("AGENTS.md")}?`,
600
+ { canGoBack }
601
+ );
602
+ if (r.kind === "back") return "back";
603
+ if (r.kind === "skip" || r.value === false) {
604
+ state.plan.hintFiles = [];
605
+ skipped("no rule file created");
606
+ return "prompted";
607
+ }
608
+ target = path.join(cwd, "AGENTS.md");
609
+ }
610
+ state.plan.hintFiles = target ? [target] : [];
611
+ if (target) success(`planned: ${target}`);
612
+ return "prompted";
613
+ };
614
+ var COMPOSER_TOOLS = [
615
+ ["composer_create_room", "create a new collaborative doc"],
616
+ ["composer_join_room", "join an existing doc by share URL"],
617
+ ["composer_attach_room", "refresh an attached doc's snapshot"],
618
+ ["composer_next_event", "watch for @mentions and replies"],
619
+ ["composer_get_section", "read a section's markdown"],
620
+ ["composer_get_full_doc", "read the entire doc as markdown"],
621
+ ["composer_add_comment", "post a comment on selected text"],
622
+ ["composer_reply_comment", "reply to a comment thread"],
623
+ ["composer_add_suggestion", "propose a text replacement"],
624
+ ["composer_reply_suggestion", "reply to a suggestion thread"],
625
+ ["composer_resolve_thread", "mark a thread resolved"]
626
+ ];
627
+ function printComposerTools() {
628
+ const w = (s) => process.stdout.write(s);
629
+ w("\n");
630
+ w(` ${dim("These tools will be pre-approved:")}
631
+
632
+ `);
633
+ for (const [name, desc] of COMPOSER_TOOLS) {
634
+ w(` ${orange("\u2022")} ${bold(name)} ${dim("\u2014 " + desc)}
635
+ `);
636
+ }
637
+ w("\n");
638
+ }
639
+ var permissionsStep = async (state, canGoBack) => {
640
+ if (!state.plan.registrations.some((h) => h.id === "claude-code"))
641
+ return "skipped";
642
+ section("Allow Composer permissions");
643
+ note([
644
+ "Skips Claude Code's per-tool permission prompts. Revoke anytime from",
645
+ `${gray("~/.claude/settings.json")}.`
646
+ ]);
647
+ printComposerTools();
648
+ const r = await confirm("Allow all Composer permissions?", {
649
+ canGoBack,
650
+ defaultYes: state.plan.addPermissions
651
+ });
652
+ if (r.kind === "back") return "back";
653
+ if (r.kind === "skip") {
654
+ state.plan.addPermissions = false;
655
+ skipped("permissions unchanged");
656
+ return "prompted";
657
+ }
658
+ state.plan.addPermissions = r.value;
659
+ if (r.value) success("planned: allow all Composer tools");
660
+ else skipped("permissions unchanged");
661
+ return "prompted";
662
+ };
663
+ function renderPlan(plan) {
664
+ const lines = [];
665
+ if (plan.registrations.length > 0) {
666
+ lines.push(` ${bold("Register MCP server with:")}`);
667
+ for (const h of plan.registrations) {
668
+ lines.push(` ${orange("\u2022")} ${h.name}`);
669
+ }
670
+ }
671
+ if (plan.hintFiles.length > 0) {
672
+ lines.push(` ${bold("Append Composer hint to:")}`);
673
+ for (const f of plan.hintFiles) {
674
+ lines.push(` ${orange("\u2022")} ${f.replace(HOME, "~")}`);
675
+ }
676
+ }
677
+ if (plan.addPermissions) {
678
+ lines.push(` ${bold("Pre-approve permissions:")}`);
679
+ lines.push(
680
+ ` ${orange("\u2022")} mcp__${MCP_NAME} in ~/.claude/settings.json`
681
+ );
682
+ }
683
+ if (shouldInstallSkill(plan)) {
684
+ lines.push(` ${bold("Install Claude skill:")}`);
685
+ lines.push(` ${orange("\u2022")} ~/.claude/skills/composer/SKILL.md`);
686
+ }
687
+ return lines.join("\n");
688
+ }
689
+ async function executePlan(plan, env) {
690
+ for (const h of plan.registrations) {
691
+ try {
692
+ await h.registerMcp(env);
693
+ success(`registered with ${h.name}`);
694
+ } catch (err) {
695
+ failure(`${h.name}: ${err.message}`);
696
+ }
697
+ }
698
+ for (const file of plan.hintFiles) {
699
+ try {
700
+ const result = await appendHint(file);
701
+ success(`${file.replace(HOME, "~")} ${dim(`(${result})`)}`);
702
+ } catch (err) {
703
+ failure(`${file}: ${err.message}`);
704
+ }
705
+ }
706
+ if (plan.addPermissions) {
707
+ try {
708
+ const result = await addClaudePermission();
709
+ success(`permissions.allow mcp__${MCP_NAME} ${dim(`(${result})`)}`);
710
+ } catch (err) {
711
+ failure(`settings.json: ${err.message}`);
712
+ }
713
+ }
714
+ if (shouldInstallSkill(plan)) {
715
+ try {
716
+ const skillPath = await copyClaudeSkill();
717
+ success(`skill installed ${dim(`(${skillPath.replace(HOME, "~")})`)}`);
718
+ } catch (err) {
719
+ failure(`skill copy: ${err.message}`);
720
+ }
721
+ }
722
+ }
723
+ async function runSetup(opts) {
724
+ const env = {
725
+ COMPOSER_SERVER_HOST: process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app",
726
+ COMPOSER_APP_BASE: process.env.COMPOSER_APP_BASE ?? "https://usecomposer.app"
727
+ };
728
+ if (!isInteractive() && !opts.yes) {
729
+ console.log(
730
+ "composer-mcp setup needs an interactive terminal, or `--yes` to accept defaults."
731
+ );
732
+ return;
733
+ }
734
+ printIntro();
735
+ if (opts.dryRun) {
736
+ note([
737
+ `${bold(orange("DRY RUN"))} \u2014 no files will be written, no commands will run.`
738
+ ]);
739
+ }
740
+ const detected = opts.all ? HARNESSES : await detectHarnesses();
741
+ if (detected.length === 0) {
742
+ console.log(
743
+ " No supported AI harness detected. Install Claude Code, Codex, Cursor,\n Gemini CLI, or Windsurf first, then re-run setup. Or pass --all to\n configure an undetected harness manually.\n"
744
+ );
745
+ return;
746
+ }
747
+ if (opts.all) {
748
+ note([`All harnesses: ${detected.map((h) => bold(h.name)).join(", ")}`]);
749
+ } else {
750
+ note([`Detected: ${detected.map((h) => bold(h.name)).join(", ")}`]);
751
+ }
752
+ const state = {
753
+ detected,
754
+ scope: null,
755
+ rule: opts.rule,
756
+ plan: {
757
+ ...emptyPlan(),
758
+ // Seed permissions to `true` so the prompt defaults to Yes and
759
+ // `--yes` mode picks it up for free. Gated on actual Claude
760
+ // registration at apply time.
761
+ addPermissions: true
762
+ }
763
+ };
764
+ if (opts.yes) {
765
+ state.scope = "user";
766
+ state.plan.registrations = detected;
767
+ state.plan.hintFiles = detected.map((h) => h.userRuleFile).filter((f) => Boolean(f));
768
+ } else {
769
+ const steps = [
770
+ registerStep,
771
+ scopeStep,
772
+ userTargetsStep,
773
+ projectTargetStep,
774
+ permissionsStep
775
+ ];
776
+ let i = 0;
777
+ const history = [];
778
+ while (i < steps.length) {
779
+ const canGoBack = history.length > 0;
780
+ const outcome = await steps[i](state, canGoBack);
781
+ if (outcome === "back") {
782
+ if (history.length === 0) continue;
783
+ i = history.pop();
784
+ continue;
785
+ }
786
+ if (outcome === "prompted") history.push(i);
787
+ i++;
788
+ }
789
+ }
790
+ section("Review");
791
+ if (!planHasWork(state.plan)) {
792
+ note(["Nothing to do \u2014 you skipped every step."]);
793
+ console.log();
794
+ return;
795
+ }
796
+ console.log(renderPlan(state.plan));
797
+ console.log();
798
+ if (opts.dryRun) {
799
+ console.log(
800
+ ` ${orange("done")} \u2014 ${bold("dry run complete")}, nothing was written.`
801
+ );
802
+ console.log();
803
+ return;
804
+ }
805
+ if (!opts.yes) {
806
+ const go = await confirm("Apply these changes?", { defaultYes: true });
807
+ if (go.kind !== "ok" || go.value !== true) {
808
+ console.log();
809
+ console.log(` ${gray("cancelled")} \u2014 nothing was written.`);
810
+ console.log();
811
+ return;
812
+ }
813
+ }
814
+ await loadOrCreateIdentity(path.join(HOME, ".composer-mcp"));
815
+ section("Applying");
816
+ await executePlan(state.plan, env);
817
+ console.log();
818
+ console.log(
819
+ ` ${orange("done")} \u2014 restart your agent, then paste a share prompt from any Composer doc.`
820
+ );
821
+ console.log();
822
+ }
823
+
824
+ // src/cli.ts
17
825
  var DEFAULT_HTTP_PORT = 3456;
18
826
  function resolveHttpPort() {
19
827
  const argv = process.argv.slice(3);
@@ -38,54 +846,24 @@ async function main() {
38
846
  const cmd = process.argv[2];
39
847
  if (cmd === "mcp") return startMcpServer();
40
848
  if (cmd === "http") return startMcpHttpServer({ port: resolveHttpPort() });
41
- if (cmd === "setup") return setup();
849
+ if (cmd === "setup") {
850
+ const argv = process.argv.slice(3);
851
+ const dryRun = argv.some((a) => a === "--dry-run" || a === "-n");
852
+ const yes = argv.some((a) => a === "--yes" || a === "-y");
853
+ const noRule = argv.some((a) => a === "--no-rule");
854
+ const all = argv.some((a) => a === "--all" || a === "-a");
855
+ return runSetup({ dryRun, yes, rule: !noRule, all });
856
+ }
42
857
  console.log(`composer-mcp
43
858
  setup Register the MCP server with your agent
859
+ flags: --yes / -y accept defaults everywhere, no prompts
860
+ --dry-run / -n show what would happen without writing anything
861
+ --all / -a show every known harness, skip detection
862
+ --no-rule skip the horizontal rule before the first step
44
863
  mcp Run as an MCP server over stdio (invoked by the host CLI)
45
864
  http Run as an MCP server over HTTP (for local dev + HMR)
46
865
  flags: --port N (or COMPOSER_MCP_PORT env; default ${DEFAULT_HTTP_PORT})`);
47
866
  }
48
- async function setup() {
49
- const dir = path.join(os.homedir(), ".composer-mcp");
50
- await loadOrCreateIdentity(dir);
51
- const serverHost = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
52
- const appBase = process.env.COMPOSER_APP_BASE ?? "https://usecomposer.app";
53
- try {
54
- await exec("claude", [
55
- "mcp",
56
- "add",
57
- "--scope",
58
- "user",
59
- "composer-mcp",
60
- "-e",
61
- `COMPOSER_SERVER_HOST=${serverHost}`,
62
- "-e",
63
- `COMPOSER_APP_BASE=${appBase}`,
64
- "--",
65
- "npx",
66
- "-y",
67
- "@composer-app/mcp@latest",
68
- "mcp"
69
- ]);
70
- console.log("\u2713 Registered composer-mcp with agent");
71
- } catch (e) {
72
- console.error(
73
- "\u2717 Could not register with agent:",
74
- e.message
75
- );
76
- process.exit(1);
77
- }
78
- const skillDir = path.join(os.homedir(), ".claude", "skills", "composer");
79
- await fs.mkdir(skillDir, { recursive: true });
80
- const pkgRoot = fileURLToPath(new URL("..", import.meta.url));
81
- const skillSource = path.join(pkgRoot, "skill", "SKILL.md");
82
- const skillContent = await fs.readFile(skillSource, "utf8");
83
- await fs.writeFile(path.join(skillDir, "SKILL.md"), skillContent);
84
- console.log(`\u2713 Wrote skill to ${skillDir}/SKILL.md`);
85
- console.log(
86
- "\nRestart your agent, then paste a share prompt from any Composer doc."
87
- );
88
- }
89
867
  main().catch((e) => {
90
868
  logError("cli main() rejected", e);
91
869
  console.error(e);