@synergenius/flow-weaver 0.10.9 → 0.10.10

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * mcp-setup command — detect AI coding tools and configure the Flow Weaver MCP server.
3
+ */
4
+ export type ToolId = 'claude' | 'cursor' | 'vscode' | 'windsurf' | 'codex' | 'openclaw';
5
+ export interface McpSetupDeps {
6
+ execCommand: (cmd: string) => Promise<{
7
+ stdout: string;
8
+ exitCode: number;
9
+ }>;
10
+ fileExists: (filePath: string) => Promise<boolean>;
11
+ readFile: (filePath: string) => Promise<string | null>;
12
+ writeFile: (filePath: string, content: string) => Promise<void>;
13
+ mkdir: (dirPath: string) => Promise<void>;
14
+ cwd: () => string;
15
+ homedir: () => string;
16
+ log: (msg: string) => void;
17
+ }
18
+ export interface ToolDefinition {
19
+ id: ToolId;
20
+ displayName: string;
21
+ detect: (deps: McpSetupDeps) => Promise<boolean>;
22
+ isConfigured: (deps: McpSetupDeps) => Promise<boolean>;
23
+ configure: (deps: McpSetupDeps) => Promise<string>;
24
+ }
25
+ export interface McpSetupOptions {
26
+ tool?: string[];
27
+ all?: boolean;
28
+ list?: boolean;
29
+ }
30
+ interface DetectedTool {
31
+ id: ToolId;
32
+ displayName: string;
33
+ detected: boolean;
34
+ configured: boolean;
35
+ }
36
+ export declare function defaultDeps(): McpSetupDeps;
37
+ export declare function mergeJsonConfig(deps: McpSetupDeps, filePath: string, rootKey: string): Promise<{
38
+ action: 'created' | 'added' | 'already-configured';
39
+ detail: string;
40
+ }>;
41
+ export declare const TOOL_REGISTRY: ToolDefinition[];
42
+ export declare function detectTools(deps: McpSetupDeps): Promise<DetectedTool[]>;
43
+ export declare function mcpSetupCommand(options: McpSetupOptions, deps?: McpSetupDeps): Promise<void>;
44
+ export {};
45
+ //# sourceMappingURL=mcp-setup.d.ts.map
@@ -0,0 +1,374 @@
1
+ /**
2
+ * mcp-setup command — detect AI coding tools and configure the Flow Weaver MCP server.
3
+ */
4
+ import { execSync } from 'child_process';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import confirm from '@inquirer/confirm';
9
+ import { ExitPromptError } from '@inquirer/core';
10
+ import { isNonInteractive } from './init.js';
11
+ // ── Constants ────────────────────────────────────────────────────────────────
12
+ const MCP_COMMAND = 'npx';
13
+ const MCP_ARGS = ['@synergenius/flow-weaver@latest', 'mcp-server', '--stdio'];
14
+ const MCP_ENTRY = { command: MCP_COMMAND, args: [...MCP_ARGS] };
15
+ // ── Deps ─────────────────────────────────────────────────────────────────────
16
+ export function defaultDeps() {
17
+ return {
18
+ execCommand: async (cmd) => {
19
+ try {
20
+ const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
21
+ return { stdout: stdout.trim(), exitCode: 0 };
22
+ }
23
+ catch {
24
+ return { stdout: '', exitCode: 1 };
25
+ }
26
+ },
27
+ fileExists: async (filePath) => {
28
+ try {
29
+ await fs.promises.access(filePath);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ },
36
+ readFile: async (filePath) => {
37
+ try {
38
+ return await fs.promises.readFile(filePath, 'utf8');
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ },
44
+ writeFile: async (filePath, content) => {
45
+ await fs.promises.writeFile(filePath, content, 'utf8');
46
+ },
47
+ mkdir: async (dirPath) => {
48
+ await fs.promises.mkdir(dirPath, { recursive: true });
49
+ },
50
+ cwd: () => process.cwd(),
51
+ homedir: () => os.homedir(),
52
+ log: (msg) => process.stdout.write(msg + '\n'),
53
+ };
54
+ }
55
+ // ── Helpers ──────────────────────────────────────────────────────────────────
56
+ function whichCmd(binary) {
57
+ return process.platform === 'win32' ? `where ${binary}` : `which ${binary}`;
58
+ }
59
+ async function binaryExists(binary, deps) {
60
+ const result = await deps.execCommand(whichCmd(binary));
61
+ return result.exitCode === 0;
62
+ }
63
+ export async function mergeJsonConfig(deps, filePath, rootKey) {
64
+ const existing = await deps.readFile(filePath);
65
+ if (existing === null) {
66
+ // File doesn't exist: create it
67
+ const dir = path.dirname(filePath);
68
+ await deps.mkdir(dir);
69
+ const config = { [rootKey]: { 'flow-weaver': MCP_ENTRY } };
70
+ await deps.writeFile(filePath, JSON.stringify(config, null, 2) + '\n');
71
+ return { action: 'created', detail: `created ${filePath}` };
72
+ }
73
+ // File exists: parse and merge
74
+ let config;
75
+ try {
76
+ config = JSON.parse(existing);
77
+ }
78
+ catch {
79
+ throw new Error(`invalid JSON in ${filePath}`);
80
+ }
81
+ if (!config[rootKey] || typeof config[rootKey] !== 'object') {
82
+ config[rootKey] = {};
83
+ }
84
+ const servers = config[rootKey];
85
+ if (servers['flow-weaver']) {
86
+ return { action: 'already-configured', detail: `already in ${filePath}` };
87
+ }
88
+ servers['flow-weaver'] = MCP_ENTRY;
89
+ await deps.writeFile(filePath, JSON.stringify(config, null, 2) + '\n');
90
+ return { action: 'added', detail: `added to ${filePath}` };
91
+ }
92
+ // ── Tool Registry ────────────────────────────────────────────────────────────
93
+ export const TOOL_REGISTRY = [
94
+ // Claude Code
95
+ {
96
+ id: 'claude',
97
+ displayName: 'Claude Code',
98
+ detect: (deps) => binaryExists('claude', deps),
99
+ isConfigured: async (deps) => {
100
+ const result = await deps.execCommand('claude mcp list');
101
+ return result.exitCode === 0 && result.stdout.includes('flow-weaver');
102
+ },
103
+ configure: async (deps) => {
104
+ const cmd = `claude mcp add --scope project flow-weaver -- ${MCP_COMMAND} ${MCP_ARGS.join(' ')}`;
105
+ const result = await deps.execCommand(cmd);
106
+ if (result.exitCode !== 0) {
107
+ throw new Error('claude mcp add failed');
108
+ }
109
+ return 'registered via claude mcp add';
110
+ },
111
+ },
112
+ // Cursor
113
+ {
114
+ id: 'cursor',
115
+ displayName: 'Cursor',
116
+ detect: async (deps) => {
117
+ const dirExists = await deps.fileExists(path.join(deps.cwd(), '.cursor'));
118
+ if (dirExists)
119
+ return true;
120
+ return binaryExists('cursor', deps);
121
+ },
122
+ isConfigured: async (deps) => {
123
+ const filePath = path.join(deps.cwd(), '.cursor', 'mcp.json');
124
+ const content = await deps.readFile(filePath);
125
+ if (!content)
126
+ return false;
127
+ try {
128
+ const config = JSON.parse(content);
129
+ return !!config?.mcpServers?.['flow-weaver'];
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ },
135
+ configure: async (deps) => {
136
+ const filePath = path.join(deps.cwd(), '.cursor', 'mcp.json');
137
+ const result = await mergeJsonConfig(deps, filePath, 'mcpServers');
138
+ return result.detail;
139
+ },
140
+ },
141
+ // VS Code Copilot
142
+ {
143
+ id: 'vscode',
144
+ displayName: 'VS Code Copilot',
145
+ detect: (deps) => binaryExists('code', deps),
146
+ isConfigured: async (deps) => {
147
+ const filePath = path.join(deps.cwd(), '.vscode', 'mcp.json');
148
+ const content = await deps.readFile(filePath);
149
+ if (!content)
150
+ return false;
151
+ try {
152
+ const config = JSON.parse(content);
153
+ return !!config?.servers?.['flow-weaver'];
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ },
159
+ configure: async (deps) => {
160
+ const filePath = path.join(deps.cwd(), '.vscode', 'mcp.json');
161
+ const result = await mergeJsonConfig(deps, filePath, 'servers');
162
+ return result.detail;
163
+ },
164
+ },
165
+ // Windsurf
166
+ {
167
+ id: 'windsurf',
168
+ displayName: 'Windsurf',
169
+ detect: async (deps) => {
170
+ const configDir = path.join(deps.homedir(), '.codeium', 'windsurf');
171
+ const dirExists = await deps.fileExists(configDir);
172
+ if (dirExists)
173
+ return true;
174
+ return binaryExists('windsurf', deps);
175
+ },
176
+ isConfigured: async (deps) => {
177
+ const filePath = path.join(deps.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
178
+ const content = await deps.readFile(filePath);
179
+ if (!content)
180
+ return false;
181
+ try {
182
+ const config = JSON.parse(content);
183
+ return !!config?.mcpServers?.['flow-weaver'];
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ },
189
+ configure: async (deps) => {
190
+ const filePath = path.join(deps.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
191
+ const result = await mergeJsonConfig(deps, filePath, 'mcpServers');
192
+ return result.detail;
193
+ },
194
+ },
195
+ // Codex (OpenAI)
196
+ {
197
+ id: 'codex',
198
+ displayName: 'Codex',
199
+ detect: (deps) => binaryExists('codex', deps),
200
+ isConfigured: async (deps) => {
201
+ const result = await deps.execCommand('codex mcp list');
202
+ return result.exitCode === 0 && result.stdout.includes('flow-weaver');
203
+ },
204
+ configure: async (deps) => {
205
+ const cmd = `codex mcp add flow-weaver -- ${MCP_COMMAND} ${MCP_ARGS.join(' ')}`;
206
+ const result = await deps.execCommand(cmd);
207
+ if (result.exitCode !== 0) {
208
+ throw new Error('codex mcp add failed');
209
+ }
210
+ return 'registered via codex mcp add';
211
+ },
212
+ },
213
+ // OpenClaw
214
+ {
215
+ id: 'openclaw',
216
+ displayName: 'OpenClaw',
217
+ detect: async (deps) => {
218
+ return deps.fileExists(path.join(deps.cwd(), 'openclaw.json'));
219
+ },
220
+ isConfigured: async (deps) => {
221
+ const filePath = path.join(deps.cwd(), 'openclaw.json');
222
+ const content = await deps.readFile(filePath);
223
+ if (!content)
224
+ return false;
225
+ try {
226
+ const config = JSON.parse(content);
227
+ return !!config?.mcpServers?.['flow-weaver'];
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ },
233
+ configure: async (deps) => {
234
+ const filePath = path.join(deps.cwd(), 'openclaw.json');
235
+ const result = await mergeJsonConfig(deps, filePath, 'mcpServers');
236
+ return result.detail;
237
+ },
238
+ },
239
+ ];
240
+ // ── Detection ────────────────────────────────────────────────────────────────
241
+ export async function detectTools(deps) {
242
+ const results = await Promise.all(TOOL_REGISTRY.map(async (tool) => {
243
+ const detected = await tool.detect(deps);
244
+ const configured = detected ? await tool.isConfigured(deps) : false;
245
+ return { id: tool.id, displayName: tool.displayName, detected, configured };
246
+ }));
247
+ return results;
248
+ }
249
+ // ── Configuration ────────────────────────────────────────────────────────────
250
+ async function configureTool(tool, deps) {
251
+ try {
252
+ const already = await tool.isConfigured(deps);
253
+ if (already) {
254
+ return { id: tool.id, displayName: tool.displayName, action: 'already-configured', detail: 'already configured' };
255
+ }
256
+ const detail = await tool.configure(deps);
257
+ return { id: tool.id, displayName: tool.displayName, action: 'configured', detail };
258
+ }
259
+ catch (err) {
260
+ const msg = err instanceof Error ? err.message : String(err);
261
+ return { id: tool.id, displayName: tool.displayName, action: 'failed', detail: msg };
262
+ }
263
+ }
264
+ // ── Command ──────────────────────────────────────────────────────────────────
265
+ export async function mcpSetupCommand(options, deps) {
266
+ const d = deps ?? defaultDeps();
267
+ // Step 1: detect all tools
268
+ const detected = await detectTools(d);
269
+ // Step 2: --list mode
270
+ if (options.list) {
271
+ d.log('');
272
+ for (const t of detected) {
273
+ const status = t.detected
274
+ ? (t.configured ? 'detected, configured' : 'detected')
275
+ : 'not found';
276
+ const icon = t.detected ? (t.configured ? '●' : '○') : '·';
277
+ d.log(` ${icon} ${t.displayName.padEnd(18)} ${status}`);
278
+ }
279
+ d.log('');
280
+ return;
281
+ }
282
+ // Step 3: determine which tools to configure
283
+ let toolIds;
284
+ if (options.tool && options.tool.length > 0) {
285
+ // Validate tool names
286
+ const valid = new Set(TOOL_REGISTRY.map((t) => t.id));
287
+ for (const name of options.tool) {
288
+ if (!valid.has(name)) {
289
+ d.log(`Unknown tool: "${name}". Valid tools: ${[...valid].join(', ')}`);
290
+ return;
291
+ }
292
+ }
293
+ toolIds = options.tool;
294
+ }
295
+ else if (options.all) {
296
+ toolIds = detected.filter((t) => t.detected).map((t) => t.id);
297
+ }
298
+ else if (isNonInteractive()) {
299
+ // Non-TTY: configure all detected tools
300
+ toolIds = detected.filter((t) => t.detected).map((t) => t.id);
301
+ }
302
+ else {
303
+ // Interactive: show detection results, then confirm each
304
+ const detectedTools = detected.filter((t) => t.detected);
305
+ if (detectedTools.length === 0) {
306
+ d.log('No AI coding tools detected. You can specify tools manually with --tool.');
307
+ return;
308
+ }
309
+ d.log('');
310
+ d.log('Detected tools:');
311
+ for (const t of detected) {
312
+ const icon = t.detected ? '✓' : '✗';
313
+ d.log(` ${icon} ${t.displayName}`);
314
+ }
315
+ d.log('');
316
+ toolIds = [];
317
+ try {
318
+ for (const t of detectedTools) {
319
+ if (t.configured) {
320
+ d.log(` ${t.displayName}: already configured, skipping`);
321
+ continue;
322
+ }
323
+ const yes = await confirm({
324
+ message: `Configure ${t.displayName}?`,
325
+ default: true,
326
+ });
327
+ if (yes)
328
+ toolIds.push(t.id);
329
+ }
330
+ }
331
+ catch (err) {
332
+ if (err instanceof ExitPromptError)
333
+ return;
334
+ throw err;
335
+ }
336
+ d.log('');
337
+ }
338
+ if (toolIds.length === 0) {
339
+ const anyDetected = detected.some((t) => t.detected);
340
+ if (!anyDetected) {
341
+ d.log('No AI coding tools detected. You can specify tools manually with --tool.');
342
+ }
343
+ else {
344
+ d.log('No tools selected.');
345
+ }
346
+ return;
347
+ }
348
+ // Step 4: configure selected tools
349
+ const toolMap = new Map(TOOL_REGISTRY.map((t) => [t.id, t]));
350
+ const results = [];
351
+ for (const id of toolIds) {
352
+ const tool = toolMap.get(id);
353
+ const result = await configureTool(tool, d);
354
+ results.push(result);
355
+ const icon = result.action === 'configured' ? '✓'
356
+ : result.action === 'already-configured' ? '●'
357
+ : '✗';
358
+ d.log(`${icon} ${result.displayName}: ${result.detail}`);
359
+ }
360
+ // Summary
361
+ const configured = results.filter((r) => r.action === 'configured').length;
362
+ const alreadyDone = results.filter((r) => r.action === 'already-configured').length;
363
+ const failed = results.filter((r) => r.action === 'failed').length;
364
+ const parts = [];
365
+ if (configured > 0)
366
+ parts.push(`${configured} configured`);
367
+ if (alreadyDone > 0)
368
+ parts.push(`${alreadyDone} already configured`);
369
+ if (failed > 0)
370
+ parts.push(`${failed} failed`);
371
+ d.log('');
372
+ d.log(`Done. ${parts.join(', ')}.`);
373
+ }
374
+ //# sourceMappingURL=mcp-setup.js.map