@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/chunk-VVYEIOFH.js +5095 -0
- package/dist/cli.js +826 -48
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/skill/.claude/settings.local.json +8 -0
- package/skill/SKILL.md +16 -1
- package/dist/chunk-SZ67UYAY.js +0 -1363
package/dist/cli.js
CHANGED
|
@@ -4,16 +4,824 @@ import {
|
|
|
4
4
|
logError,
|
|
5
5
|
startMcpHttpServer,
|
|
6
6
|
startMcpServer
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-VVYEIOFH.js";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
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
|
-
|
|
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")
|
|
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);
|