@synergenius/flow-weaver 0.10.8 → 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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { TWorkflowAST, TNodeInstanceAST, TNodeTypeAST } from '../../ast/types.js';
5
5
  export interface DescribeOptions {
6
- format?: 'json' | 'text' | 'mermaid' | 'paths';
6
+ format?: 'json' | 'text' | 'mermaid' | 'paths' | 'ascii' | 'ascii-compact';
7
7
  node?: string;
8
8
  workflowName?: string;
9
9
  compile?: boolean;
@@ -63,6 +63,6 @@ export declare function formatTextOutput(ast: TWorkflowAST, output: DescribeOutp
63
63
  /**
64
64
  * Format the describe output based on format option
65
65
  */
66
- export declare function formatDescribeOutput(ast: TWorkflowAST, output: DescribeOutput | FocusedNodeOutput, format: 'json' | 'text' | 'mermaid' | 'paths'): string;
66
+ export declare function formatDescribeOutput(ast: TWorkflowAST, output: DescribeOutput | FocusedNodeOutput, format: 'json' | 'text' | 'mermaid' | 'paths' | 'ascii' | 'ascii-compact'): string;
67
67
  export declare function describeCommand(input: string, options?: DescribeOptions): Promise<void>;
68
68
  //# sourceMappingURL=describe.d.ts.map
@@ -9,6 +9,8 @@ import { validator } from '../../validator.js';
9
9
  import { getNode, getIncomingConnections, getOutgoingConnections } from '../../api/query.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { getErrorMessage } from '../../utils/error-utils.js';
12
+ import { buildDiagramGraph } from '../../diagram/geometry.js';
13
+ import { renderASCII, renderASCIICompact } from '../../diagram/ascii-renderer.js';
12
14
  export function buildNodeInfo(instance, nodeType) {
13
15
  return {
14
16
  id: instance.id,
@@ -329,6 +331,10 @@ export function formatDescribeOutput(ast, output, format) {
329
331
  return generateMermaid(ast);
330
332
  case 'paths':
331
333
  return formatPaths(ast);
334
+ case 'ascii':
335
+ return renderASCII(buildDiagramGraph(ast));
336
+ case 'ascii-compact':
337
+ return renderASCIICompact(buildDiagramGraph(ast));
332
338
  case 'json':
333
339
  default:
334
340
  return JSON.stringify(output, null, 2);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Diagram command — generates SVG or interactive HTML diagrams from workflow files.
2
+ * Diagram command — generates SVG, interactive HTML, or ASCII diagrams from workflow files.
3
3
  */
4
4
  export interface DiagramCommandOptions {
5
5
  theme?: 'dark' | 'light';
@@ -8,7 +8,7 @@ export interface DiagramCommandOptions {
8
8
  showPortLabels?: boolean;
9
9
  workflowName?: string;
10
10
  output?: string;
11
- format?: 'svg' | 'html';
11
+ format?: 'svg' | 'html' | 'ascii' | 'ascii-compact' | 'text';
12
12
  }
13
13
  export declare function diagramCommand(input: string, options?: DiagramCommandOptions): Promise<void>;
14
14
  //# sourceMappingURL=diagram.d.ts.map
@@ -1,12 +1,13 @@
1
1
  /* eslint-disable no-console */
2
2
  /**
3
- * Diagram command — generates SVG or interactive HTML diagrams from workflow files.
3
+ * Diagram command — generates SVG, interactive HTML, or ASCII diagrams from workflow files.
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { fileToSVG, fileToHTML } from '../../diagram/index.js';
7
+ import { fileToSVG, fileToHTML, fileToASCII } from '../../diagram/index.js';
8
8
  import { logger } from '../utils/logger.js';
9
9
  import { getErrorMessage } from '../../utils/error-utils.js';
10
+ const ASCII_FORMATS = new Set(['ascii', 'ascii-compact', 'text']);
10
11
  export async function diagramCommand(input, options = {}) {
11
12
  const { output, format = 'svg', ...diagramOptions } = options;
12
13
  const filePath = path.resolve(input);
@@ -15,10 +16,16 @@ export async function diagramCommand(input, options = {}) {
15
16
  process.exit(1);
16
17
  }
17
18
  try {
18
- const isHtml = format === 'html';
19
- const result = isHtml
20
- ? fileToHTML(filePath, diagramOptions)
21
- : fileToSVG(filePath, diagramOptions);
19
+ let result;
20
+ if (ASCII_FORMATS.has(format)) {
21
+ result = fileToASCII(filePath, { ...diagramOptions, format });
22
+ }
23
+ else if (format === 'html') {
24
+ result = fileToHTML(filePath, diagramOptions);
25
+ }
26
+ else {
27
+ result = fileToSVG(filePath, diagramOptions);
28
+ }
22
29
  if (output) {
23
30
  const outputPath = path.resolve(output);
24
31
  fs.writeFileSync(outputPath, result, 'utf-8');
@@ -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