@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.
- package/dist/cli/commands/mcp-setup.d.ts +45 -0
- package/dist/cli/commands/mcp-setup.js +374 -0
- package/dist/cli/flow-weaver.mjs +664 -375
- package/dist/cli/index.js +30 -13
- package/dist/mcp/editor-connection.d.ts +11 -9
- package/dist/mcp/editor-connection.js +29 -12
- package/dist/mcp/resources.d.ts +3 -3
- package/dist/mcp/resources.js +5 -5
- package/dist/mcp/server.js +3 -5
- package/dist/mcp/tools-editor.js +22 -22
- package/dist/mcp/types.d.ts +3 -3
- package/package.json +1 -1
|
@@ -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
|