@synergenius/flow-weaver 0.10.7 → 0.10.9
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/LICENSE +101 -20
- package/dist/cli/commands/describe.d.ts +2 -2
- package/dist/cli/commands/describe.js +6 -0
- package/dist/cli/commands/diagram.d.ts +2 -2
- package/dist/cli/commands/diagram.js +13 -6
- package/dist/cli/flow-weaver.mjs +964 -361
- package/dist/cli/index.js +2 -2
- package/dist/diagram/ascii-renderer.d.ts +13 -0
- package/dist/diagram/ascii-renderer.js +528 -0
- package/dist/diagram/index.d.ts +4 -0
- package/dist/diagram/index.js +26 -0
- package/dist/diagram/types.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/prompts.d.ts +3 -0
- package/dist/mcp/prompts.js +68 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools-diagram.d.ts +2 -1
- package/dist/mcp/tools-diagram.js +50 -14
- package/dist/mcp/tools-query.js +2 -2
- package/dist/parser.d.ts +6 -0
- package/dist/parser.js +59 -25
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -102,7 +102,7 @@ program
|
|
|
102
102
|
program
|
|
103
103
|
.command('describe <input>')
|
|
104
104
|
.description('Output workflow structure in LLM-friendly formats (JSON, text, mermaid)')
|
|
105
|
-
.option('-f, --format <format>', 'Output format: json (default), text, mermaid', 'json')
|
|
105
|
+
.option('-f, --format <format>', 'Output format: json (default), text, mermaid, paths, ascii, ascii-compact', 'json')
|
|
106
106
|
.option('-n, --node <id>', 'Focus on a specific node')
|
|
107
107
|
.option('--compile', 'Also update runtime markers in the source file')
|
|
108
108
|
.option('-w, --workflow-name <name>', 'Specific workflow name to describe')
|
|
@@ -124,7 +124,7 @@ program
|
|
|
124
124
|
.option('-p, --padding <pixels>', 'Canvas padding in pixels')
|
|
125
125
|
.option('--no-port-labels', 'Hide data type labels on ports')
|
|
126
126
|
.option('--workflow-name <name>', 'Specific workflow to render')
|
|
127
|
-
.option('-f, --format <format>', 'Output format: svg (default), html
|
|
127
|
+
.option('-f, --format <format>', 'Output format: svg (default), html, ascii, ascii-compact, text', 'svg')
|
|
128
128
|
.option('-o, --output <file>', 'Write output to file instead of stdout')
|
|
129
129
|
.action(async (input, options) => {
|
|
130
130
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII-based renderers for workflow diagrams.
|
|
3
|
+
*
|
|
4
|
+
* Three formats:
|
|
5
|
+
* - renderASCII: port-level detail with box-drawing, connections drawn between ports
|
|
6
|
+
* - renderASCIICompact: single-row compact node boxes
|
|
7
|
+
* - renderText: structured text listing (most LLM-parseable)
|
|
8
|
+
*/
|
|
9
|
+
import type { DiagramGraph } from './types.js';
|
|
10
|
+
export declare function renderASCII(graph: DiagramGraph): string;
|
|
11
|
+
export declare function renderASCIICompact(graph: DiagramGraph): string;
|
|
12
|
+
export declare function renderText(graph: DiagramGraph): string;
|
|
13
|
+
//# sourceMappingURL=ascii-renderer.d.ts.map
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII-based renderers for workflow diagrams.
|
|
3
|
+
*
|
|
4
|
+
* Three formats:
|
|
5
|
+
* - renderASCII: port-level detail with box-drawing, connections drawn between ports
|
|
6
|
+
* - renderASCIICompact: single-row compact node boxes
|
|
7
|
+
* - renderText: structured text listing (most LLM-parseable)
|
|
8
|
+
*/
|
|
9
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
10
|
+
function groupByLayer(nodes) {
|
|
11
|
+
if (nodes.length === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const sorted = [...nodes].sort((a, b) => a.x - b.x);
|
|
14
|
+
const layers = [];
|
|
15
|
+
let currentX = sorted[0].x;
|
|
16
|
+
let currentLayer = [];
|
|
17
|
+
for (const node of sorted) {
|
|
18
|
+
if (Math.abs(node.x - currentX) > 10) {
|
|
19
|
+
layers.push(currentLayer);
|
|
20
|
+
currentLayer = [];
|
|
21
|
+
currentX = node.x;
|
|
22
|
+
}
|
|
23
|
+
currentLayer.push(node);
|
|
24
|
+
}
|
|
25
|
+
if (currentLayer.length > 0)
|
|
26
|
+
layers.push(currentLayer);
|
|
27
|
+
return layers;
|
|
28
|
+
}
|
|
29
|
+
function buildConnectedPorts(connections) {
|
|
30
|
+
const s = new Set();
|
|
31
|
+
for (const c of connections) {
|
|
32
|
+
s.add(`${c.fromNode}.${c.fromPort}`);
|
|
33
|
+
s.add(`${c.toNode}.${c.toPort}`);
|
|
34
|
+
}
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
function portSymbol(nodeId, port, connected) {
|
|
38
|
+
return connected.has(`${nodeId}.${port.name}`) ? '\u25CF' : '\u25CB';
|
|
39
|
+
}
|
|
40
|
+
// ── Corner characters ────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Pick the right corner box-drawing character.
|
|
43
|
+
* hDir: which side of the corner the horizontal segment extends to.
|
|
44
|
+
* vDir: which side of the corner the vertical segment extends to.
|
|
45
|
+
*/
|
|
46
|
+
function cornerChar(isStep, hDir, vDir) {
|
|
47
|
+
if (hDir === 'right' && vDir === 'down')
|
|
48
|
+
return isStep ? '\u2554' : '\u250C'; // ╔ ┌
|
|
49
|
+
if (hDir === 'left' && vDir === 'down')
|
|
50
|
+
return isStep ? '\u2557' : '\u2510'; // ╗ ┐
|
|
51
|
+
if (hDir === 'right' && vDir === 'up')
|
|
52
|
+
return isStep ? '\u255A' : '\u2514'; // ╚ └
|
|
53
|
+
if (hDir === 'left' && vDir === 'up')
|
|
54
|
+
return isStep ? '\u255D' : '\u2518'; // ╝ ┘
|
|
55
|
+
return '\u253C'; // ┼ fallback
|
|
56
|
+
}
|
|
57
|
+
// ── 2D character grid ────────────────────────────────────────────────────────
|
|
58
|
+
const H_CHARS = new Set('\u2500\u2550'); // ─ ═
|
|
59
|
+
const V_CHARS = new Set('\u2502\u2551'); // │ ║
|
|
60
|
+
class CharGrid {
|
|
61
|
+
cells;
|
|
62
|
+
width;
|
|
63
|
+
height;
|
|
64
|
+
constructor(w, h) {
|
|
65
|
+
this.width = w;
|
|
66
|
+
this.height = h;
|
|
67
|
+
this.cells = new Array(w * h).fill(' ');
|
|
68
|
+
}
|
|
69
|
+
set(x, y, ch) {
|
|
70
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
|
|
71
|
+
return;
|
|
72
|
+
const existing = this.cells[y * this.width + x];
|
|
73
|
+
if (existing !== ' ') {
|
|
74
|
+
// Handle crossings: horizontal meets vertical
|
|
75
|
+
if ((H_CHARS.has(ch) && V_CHARS.has(existing)) || (V_CHARS.has(ch) && H_CHARS.has(existing))) {
|
|
76
|
+
this.cells[y * this.width + x] = '\u253C'; // ┼
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Same direction: keep existing (shared path)
|
|
80
|
+
if ((H_CHARS.has(ch) && H_CHARS.has(existing)) || (V_CHARS.has(ch) && V_CHARS.has(existing))) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.cells[y * this.width + x] = ch;
|
|
85
|
+
}
|
|
86
|
+
get(x, y) {
|
|
87
|
+
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
88
|
+
return this.cells[y * this.width + x];
|
|
89
|
+
}
|
|
90
|
+
return ' ';
|
|
91
|
+
}
|
|
92
|
+
/** Force-write, ignoring collision logic (for boxes and text). */
|
|
93
|
+
forceSet(x, y, ch) {
|
|
94
|
+
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
95
|
+
this.cells[y * this.width + x] = ch;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
writeStr(x, y, s) {
|
|
99
|
+
for (let i = 0; i < s.length; i++) {
|
|
100
|
+
this.forceSet(x + i, y, s[i]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
toLines() {
|
|
104
|
+
const lines = [];
|
|
105
|
+
for (let y = 0; y < this.height; y++) {
|
|
106
|
+
let line = '';
|
|
107
|
+
for (let x = 0; x < this.width; x++) {
|
|
108
|
+
line += this.cells[y * this.width + x];
|
|
109
|
+
}
|
|
110
|
+
lines.push(line.trimEnd());
|
|
111
|
+
}
|
|
112
|
+
while (lines.length > 0 && lines[lines.length - 1] === '')
|
|
113
|
+
lines.pop();
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function mergePortRows(inputs, outputs) {
|
|
118
|
+
const rows = [];
|
|
119
|
+
const maxLen = Math.max(inputs.length, outputs.length);
|
|
120
|
+
for (let i = 0; i < maxLen; i++) {
|
|
121
|
+
rows.push({
|
|
122
|
+
input: i < inputs.length ? inputs[i] : null,
|
|
123
|
+
output: i < outputs.length ? outputs[i] : null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return rows;
|
|
127
|
+
}
|
|
128
|
+
function measureBox(node) {
|
|
129
|
+
const portRows = mergePortRows(node.inputs, node.outputs);
|
|
130
|
+
const maxInputPortLen = node.inputs.length > 0 ? Math.max(...node.inputs.map(p => p.name.length)) : 0;
|
|
131
|
+
const maxOutputPortLen = node.outputs.length > 0 ? Math.max(...node.outputs.map(p => p.name.length)) : 0;
|
|
132
|
+
const portsContentWidth = (maxInputPortLen > 0 ? maxInputPortLen + 2 : 0)
|
|
133
|
+
+ 2
|
|
134
|
+
+ (maxOutputPortLen > 0 ? maxOutputPortLen + 2 : 0);
|
|
135
|
+
const innerWidth = Math.max(node.label.length + 2, portsContentWidth, 10);
|
|
136
|
+
const boxWidth = innerWidth + 2;
|
|
137
|
+
const headerRows = 3;
|
|
138
|
+
const portRowCount = Math.max(portRows.length, 1);
|
|
139
|
+
const boxHeight = headerRows + portRowCount + 1;
|
|
140
|
+
const portRowOffsets = new Map();
|
|
141
|
+
for (let i = 0; i < portRows.length; i++) {
|
|
142
|
+
if (portRows[i].input)
|
|
143
|
+
portRowOffsets.set(portRows[i].input.name, headerRows + i);
|
|
144
|
+
if (portRows[i].output)
|
|
145
|
+
portRowOffsets.set(portRows[i].output.name, headerRows + i);
|
|
146
|
+
}
|
|
147
|
+
return { node, innerWidth, boxWidth, portRows, boxHeight, portRowOffsets };
|
|
148
|
+
}
|
|
149
|
+
function drawBox(grid, box, bx, by, connected) {
|
|
150
|
+
const { innerWidth, portRows, node } = box;
|
|
151
|
+
grid.forceSet(bx, by, '\u250C');
|
|
152
|
+
for (let i = 0; i < innerWidth; i++)
|
|
153
|
+
grid.forceSet(bx + 1 + i, by, '\u2500');
|
|
154
|
+
grid.forceSet(bx + innerWidth + 1, by, '\u2510');
|
|
155
|
+
const labelPad = innerWidth - node.label.length;
|
|
156
|
+
const padL = Math.floor(labelPad / 2);
|
|
157
|
+
grid.forceSet(bx, by + 1, '\u2502');
|
|
158
|
+
grid.writeStr(bx + 1 + padL, by + 1, node.label);
|
|
159
|
+
grid.forceSet(bx + innerWidth + 1, by + 1, '\u2502');
|
|
160
|
+
grid.forceSet(bx, by + 2, '\u251C');
|
|
161
|
+
for (let i = 0; i < innerWidth; i++)
|
|
162
|
+
grid.forceSet(bx + 1 + i, by + 2, '\u2500');
|
|
163
|
+
grid.forceSet(bx + innerWidth + 1, by + 2, '\u2524');
|
|
164
|
+
for (let i = 0; i < Math.max(portRows.length, 1); i++) {
|
|
165
|
+
const ry = by + 3 + i;
|
|
166
|
+
grid.forceSet(bx, ry, '\u2502');
|
|
167
|
+
grid.forceSet(bx + innerWidth + 1, ry, '\u2502');
|
|
168
|
+
if (i < portRows.length) {
|
|
169
|
+
const row = portRows[i];
|
|
170
|
+
if (row.input) {
|
|
171
|
+
grid.writeStr(bx + 1, ry, `${portSymbol(node.id, row.input, connected)} ${row.input.name}`);
|
|
172
|
+
}
|
|
173
|
+
if (row.output) {
|
|
174
|
+
const txt = `${row.output.name} ${portSymbol(node.id, row.output, connected)}`;
|
|
175
|
+
grid.writeStr(bx + innerWidth + 1 - txt.length, ry, txt);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const bottomY = by + box.boxHeight - 1;
|
|
180
|
+
grid.forceSet(bx, bottomY, '\u2514');
|
|
181
|
+
for (let i = 0; i < innerWidth; i++)
|
|
182
|
+
grid.forceSet(bx + 1 + i, bottomY, '\u2500');
|
|
183
|
+
grid.forceSet(bx + innerWidth + 1, bottomY, '\u2518');
|
|
184
|
+
}
|
|
185
|
+
export function renderASCII(graph) {
|
|
186
|
+
const layers = groupByLayer(graph.nodes);
|
|
187
|
+
if (layers.length === 0)
|
|
188
|
+
return `${graph.workflowName}\n(empty workflow)`;
|
|
189
|
+
const layerBoxes = layers.map(layer => layer.map(measureBox));
|
|
190
|
+
const colWidths = layerBoxes.map(boxes => Math.max(...boxes.map(b => b.boxWidth)));
|
|
191
|
+
const wireGap = 14;
|
|
192
|
+
// Column x-offsets
|
|
193
|
+
const colX = [];
|
|
194
|
+
let cx = 1;
|
|
195
|
+
for (let c = 0; c < layerBoxes.length; c++) {
|
|
196
|
+
colX.push(cx);
|
|
197
|
+
cx += colWidths[c] + wireGap;
|
|
198
|
+
}
|
|
199
|
+
const gridWidth = cx + 1;
|
|
200
|
+
// Count spanning connections to reserve highway margin rows
|
|
201
|
+
const nodeColMap = new Map();
|
|
202
|
+
for (let c = 0; c < layerBoxes.length; c++) {
|
|
203
|
+
for (const box of layerBoxes[c])
|
|
204
|
+
nodeColMap.set(box.node.id, c);
|
|
205
|
+
}
|
|
206
|
+
let spanCount = 0;
|
|
207
|
+
for (const conn of graph.connections) {
|
|
208
|
+
const fc = nodeColMap.get(conn.fromNode);
|
|
209
|
+
const tc = nodeColMap.get(conn.toNode);
|
|
210
|
+
if (fc !== undefined && tc !== undefined && Math.abs(tc - fc) > 1)
|
|
211
|
+
spanCount++;
|
|
212
|
+
}
|
|
213
|
+
const highwayMargin = spanCount > 0 ? spanCount + 1 : 0;
|
|
214
|
+
// Position boxes, shifted down to leave room for above-highways
|
|
215
|
+
const nodeGapY = 2;
|
|
216
|
+
const boxPositions = new Map();
|
|
217
|
+
const boxStartY = 3 + highwayMargin;
|
|
218
|
+
let maxGridHeight = 0;
|
|
219
|
+
for (let c = 0; c < layerBoxes.length; c++) {
|
|
220
|
+
let y = boxStartY;
|
|
221
|
+
for (const box of layerBoxes[c]) {
|
|
222
|
+
boxPositions.set(box.node.id, { col: c, bx: colX[c], by: y, box });
|
|
223
|
+
y += box.boxHeight + nodeGapY;
|
|
224
|
+
}
|
|
225
|
+
if (y > maxGridHeight)
|
|
226
|
+
maxGridHeight = y;
|
|
227
|
+
}
|
|
228
|
+
maxGridHeight += highwayMargin + 3;
|
|
229
|
+
const connected = buildConnectedPorts(graph.connections);
|
|
230
|
+
const grid = new CharGrid(gridWidth, maxGridHeight);
|
|
231
|
+
// Title (centered, above the highway margin)
|
|
232
|
+
const titleX = Math.max(0, Math.floor((gridWidth - graph.workflowName.length) / 2));
|
|
233
|
+
grid.writeStr(titleX, 1, graph.workflowName);
|
|
234
|
+
// Draw boxes
|
|
235
|
+
for (const pos of boxPositions.values()) {
|
|
236
|
+
drawBox(grid, pos.box, pos.bx, pos.by, connected);
|
|
237
|
+
}
|
|
238
|
+
// Draw connections
|
|
239
|
+
drawConnections(grid, graph.connections, boxPositions, boxStartY, maxGridHeight);
|
|
240
|
+
// Scope sub-diagrams
|
|
241
|
+
const scopeLines = [];
|
|
242
|
+
for (const node of graph.nodes) {
|
|
243
|
+
if (node.scopeChildren && node.scopeChildren.length > 0) {
|
|
244
|
+
scopeLines.push('');
|
|
245
|
+
scopeLines.push(` Scope [${node.label}]:`);
|
|
246
|
+
if (node.scopeConnections) {
|
|
247
|
+
for (const sc of node.scopeConnections) {
|
|
248
|
+
const arrow = sc.isStepConnection ? '\u2550\u2550\u25B6' : '\u2500\u2500\u25B6';
|
|
249
|
+
scopeLines.push(` ${sc.fromNode}.${sc.fromPort} ${arrow} ${sc.toNode}.${sc.toPort}${sc.isStepConnection ? ' STEP' : ''}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const lines = grid.toLines();
|
|
255
|
+
lines.push(...scopeLines);
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(' \u25CF connected \u25CB not connected \u2550\u2550\u25B6 STEP \u2500\u2500\u25B6 DATA');
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
// ── Connection drawing ───────────────────────────────────────────────────────
|
|
261
|
+
function drawConnections(grid, connections, positions, boxAreaTop, _gridHeight) {
|
|
262
|
+
// Compute global box y-bounds
|
|
263
|
+
let globalMinY = Infinity;
|
|
264
|
+
let globalMaxY = 0;
|
|
265
|
+
for (const pos of positions.values()) {
|
|
266
|
+
globalMinY = Math.min(globalMinY, pos.by);
|
|
267
|
+
globalMaxY = Math.max(globalMaxY, pos.by + pos.box.boxHeight);
|
|
268
|
+
}
|
|
269
|
+
// Sort: adjacent connections first, then spanning. Within each group, sort by source y.
|
|
270
|
+
const sorted = [...connections].sort((a, b) => {
|
|
271
|
+
const fa = positions.get(a.fromNode);
|
|
272
|
+
const ta = positions.get(a.toNode);
|
|
273
|
+
const fb = positions.get(b.fromNode);
|
|
274
|
+
const tb = positions.get(b.toNode);
|
|
275
|
+
if (!fa || !ta || !fb || !tb)
|
|
276
|
+
return 0;
|
|
277
|
+
const spanA = Math.abs(ta.col - fa.col);
|
|
278
|
+
const spanB = Math.abs(tb.col - fb.col);
|
|
279
|
+
if (spanA !== spanB)
|
|
280
|
+
return spanA - spanB;
|
|
281
|
+
const ya = fa.by + (fa.box.portRowOffsets.get(a.fromPort) ?? 0);
|
|
282
|
+
const yb = fb.by + (fb.box.portRowOffsets.get(b.fromPort) ?? 0);
|
|
283
|
+
return ya - yb;
|
|
284
|
+
});
|
|
285
|
+
const usedTracks = new Map();
|
|
286
|
+
let nextAboveHighway = globalMinY - 2;
|
|
287
|
+
let nextBelowHighway = globalMaxY + 1;
|
|
288
|
+
for (const conn of sorted) {
|
|
289
|
+
const fromPos = positions.get(conn.fromNode);
|
|
290
|
+
const toPos = positions.get(conn.toNode);
|
|
291
|
+
if (!fromPos || !toPos)
|
|
292
|
+
continue;
|
|
293
|
+
const fromRowOff = fromPos.box.portRowOffsets.get(conn.fromPort);
|
|
294
|
+
const toRowOff = toPos.box.portRowOffsets.get(conn.toPort);
|
|
295
|
+
if (fromRowOff === undefined || toRowOff === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const y1 = fromPos.by + fromRowOff;
|
|
298
|
+
const y2 = toPos.by + toRowOff;
|
|
299
|
+
const x1 = fromPos.bx + fromPos.box.boxWidth; // first cell after source box right border
|
|
300
|
+
const x2 = toPos.bx - 1; // cell for arrowhead (just before target box left border)
|
|
301
|
+
const isStep = conn.isStepConnection;
|
|
302
|
+
const hCh = isStep ? '\u2550' : '\u2500';
|
|
303
|
+
const vCh = isStep ? '\u2551' : '\u2502';
|
|
304
|
+
const isAdjacent = toPos.col === fromPos.col + 1;
|
|
305
|
+
if (isAdjacent) {
|
|
306
|
+
drawAdjacentRoute(grid, x1, y1, x2, y2, hCh, vCh, isStep, usedTracks, fromPos, toPos);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Spanning: route via highway lane above or below all boxes
|
|
310
|
+
const avgY = (y1 + y2) / 2;
|
|
311
|
+
const midBoxY = (globalMinY + globalMaxY) / 2;
|
|
312
|
+
let highwayY;
|
|
313
|
+
if (avgY <= midBoxY) {
|
|
314
|
+
highwayY = nextAboveHighway;
|
|
315
|
+
nextAboveHighway--;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
highwayY = nextBelowHighway;
|
|
319
|
+
nextBelowHighway++;
|
|
320
|
+
}
|
|
321
|
+
drawSpanningRoute(grid, x1, y1, x2, y2, highwayY, hCh, vCh, isStep, usedTracks, fromPos, toPos);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Route a connection between adjacent columns (1 gap to cross). */
|
|
326
|
+
function drawAdjacentRoute(grid, x1, y1, x2, y2, hCh, vCh, isStep, usedTracks, fromPos, toPos) {
|
|
327
|
+
if (y1 === y2) {
|
|
328
|
+
// Straight horizontal
|
|
329
|
+
for (let x = x1; x < x2; x++)
|
|
330
|
+
grid.set(x, y1, hCh);
|
|
331
|
+
grid.set(x2, y1, '\u25B6');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// L-route: horizontal -> corner -> vertical -> corner -> horizontal
|
|
335
|
+
const gapStart = fromPos.bx + fromPos.box.boxWidth + 1;
|
|
336
|
+
const gapEnd = toPos.bx - 2;
|
|
337
|
+
const midX = findTrack(usedTracks, gapStart, gapEnd, y1, y2);
|
|
338
|
+
markTrack(usedTracks, midX, y1, y2);
|
|
339
|
+
// Source horizontal
|
|
340
|
+
for (let x = x1; x < midX; x++)
|
|
341
|
+
grid.set(x, y1, hCh);
|
|
342
|
+
// Corner at source end
|
|
343
|
+
grid.set(midX, y1, cornerChar(isStep, 'left', y2 > y1 ? 'down' : 'up'));
|
|
344
|
+
// Vertical
|
|
345
|
+
drawVerticalSegment(grid, midX, y1, y2, vCh);
|
|
346
|
+
// Corner at target end
|
|
347
|
+
grid.set(midX, y2, cornerChar(isStep, 'right', y2 > y1 ? 'up' : 'down'));
|
|
348
|
+
// Target horizontal
|
|
349
|
+
for (let x = midX + 1; x < x2; x++)
|
|
350
|
+
grid.set(x, y2, hCh);
|
|
351
|
+
grid.set(x2, y2, '\u25B6');
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Route a connection spanning multiple columns via a highway lane.
|
|
355
|
+
* Shape: horizontal out -> vertical to highway -> horizontal at highway -> vertical to target -> horizontal in
|
|
356
|
+
*/
|
|
357
|
+
function drawSpanningRoute(grid, x1, y1, x2, y2, highwayY, hCh, vCh, isStep, usedTracks, fromPos, toPos) {
|
|
358
|
+
// Find vertical track positions in the source gap and target gap
|
|
359
|
+
const srcGapStart = fromPos.bx + fromPos.box.boxWidth + 1;
|
|
360
|
+
const srcGapEnd = srcGapStart + 10;
|
|
361
|
+
const srcMidX = findTrack(usedTracks, srcGapStart, srcGapEnd, y1, highwayY);
|
|
362
|
+
markTrack(usedTracks, srcMidX, y1, highwayY);
|
|
363
|
+
const tgtGapEnd = toPos.bx - 2;
|
|
364
|
+
const tgtGapStart = tgtGapEnd - 10;
|
|
365
|
+
const tgtMidX = findTrack(usedTracks, tgtGapStart, tgtGapEnd, highwayY, y2);
|
|
366
|
+
markTrack(usedTracks, tgtMidX, highwayY, y2);
|
|
367
|
+
const goingDown = highwayY > y1; // source to highway direction
|
|
368
|
+
const goingUp = y2 < highwayY; // highway to target direction
|
|
369
|
+
// 1. Horizontal from source to srcMidX
|
|
370
|
+
for (let x = x1; x < srcMidX; x++)
|
|
371
|
+
grid.set(x, y1, hCh);
|
|
372
|
+
// 2. Corner: horizontal ends, vertical begins toward highway
|
|
373
|
+
grid.set(srcMidX, y1, cornerChar(isStep, 'left', goingDown ? 'down' : 'up'));
|
|
374
|
+
// 3. Vertical from y1 toward highwayY
|
|
375
|
+
drawVerticalSegment(grid, srcMidX, y1, highwayY, vCh);
|
|
376
|
+
// 4. Corner: vertical ends, horizontal begins at highway level
|
|
377
|
+
grid.set(srcMidX, highwayY, cornerChar(isStep, 'right', goingDown ? 'up' : 'down'));
|
|
378
|
+
// 5. Horizontal at highwayY from srcMidX to tgtMidX
|
|
379
|
+
for (let x = srcMidX + 1; x < tgtMidX; x++)
|
|
380
|
+
grid.set(x, highwayY, hCh);
|
|
381
|
+
// 6. Corner: horizontal ends, vertical begins toward target
|
|
382
|
+
grid.set(tgtMidX, highwayY, cornerChar(isStep, 'left', goingUp ? 'up' : 'down'));
|
|
383
|
+
// 7. Vertical from highwayY toward y2
|
|
384
|
+
drawVerticalSegment(grid, tgtMidX, highwayY, y2, vCh);
|
|
385
|
+
// 8. Corner: vertical ends, horizontal begins to target
|
|
386
|
+
grid.set(tgtMidX, y2, cornerChar(isStep, 'right', goingUp ? 'down' : 'up'));
|
|
387
|
+
// 9. Horizontal from tgtMidX to target
|
|
388
|
+
for (let x = tgtMidX + 1; x < x2; x++)
|
|
389
|
+
grid.set(x, y2, hCh);
|
|
390
|
+
// 10. Arrowhead
|
|
391
|
+
grid.set(x2, y2, '\u25B6');
|
|
392
|
+
}
|
|
393
|
+
/** Draw a vertical segment between two y values (exclusive of endpoints). */
|
|
394
|
+
function drawVerticalSegment(grid, x, y1, y2, vCh) {
|
|
395
|
+
const minY = Math.min(y1, y2);
|
|
396
|
+
const maxY = Math.max(y1, y2);
|
|
397
|
+
for (let y = minY + 1; y < maxY; y++) {
|
|
398
|
+
grid.set(x, y, vCh);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function findTrack(usedTracks, gapStart, gapEnd, y1, y2) {
|
|
402
|
+
const minY = Math.min(y1, y2);
|
|
403
|
+
const maxY = Math.max(y1, y2);
|
|
404
|
+
const actualStart = Math.min(gapStart, gapEnd);
|
|
405
|
+
const actualEnd = Math.max(gapStart, gapEnd);
|
|
406
|
+
const mid = Math.floor((actualStart + actualEnd) / 2);
|
|
407
|
+
for (let offset = 0; offset <= (actualEnd - actualStart); offset++) {
|
|
408
|
+
for (const candidate of [mid + offset, mid - offset]) {
|
|
409
|
+
if (candidate < actualStart || candidate > actualEnd)
|
|
410
|
+
continue;
|
|
411
|
+
const existing = usedTracks.get(candidate);
|
|
412
|
+
if (!existing)
|
|
413
|
+
return candidate;
|
|
414
|
+
let conflict = false;
|
|
415
|
+
for (let y = minY; y <= maxY; y++) {
|
|
416
|
+
if (existing.has(y)) {
|
|
417
|
+
conflict = true;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!conflict)
|
|
422
|
+
return candidate;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return mid;
|
|
426
|
+
}
|
|
427
|
+
function markTrack(usedTracks, x, y1, y2) {
|
|
428
|
+
if (!usedTracks.has(x))
|
|
429
|
+
usedTracks.set(x, new Set());
|
|
430
|
+
const s = usedTracks.get(x);
|
|
431
|
+
const minY = Math.min(y1, y2);
|
|
432
|
+
const maxY = Math.max(y1, y2);
|
|
433
|
+
for (let y = minY; y <= maxY; y++)
|
|
434
|
+
s.add(y);
|
|
435
|
+
}
|
|
436
|
+
// ── renderASCIICompact ───────────────────────────────────────────────────────
|
|
437
|
+
export function renderASCIICompact(graph) {
|
|
438
|
+
const layers = groupByLayer(graph.nodes);
|
|
439
|
+
if (layers.length === 0)
|
|
440
|
+
return `${graph.workflowName}\n(empty workflow)`;
|
|
441
|
+
const outputLines = [];
|
|
442
|
+
outputLines.push(graph.workflowName);
|
|
443
|
+
outputLines.push('');
|
|
444
|
+
const mainChain = [];
|
|
445
|
+
const parallelNodes = [];
|
|
446
|
+
for (const layer of layers) {
|
|
447
|
+
mainChain.push(layer[0]);
|
|
448
|
+
for (let i = 1; i < layer.length; i++)
|
|
449
|
+
parallelNodes.push(layer[i]);
|
|
450
|
+
}
|
|
451
|
+
const topParts = [];
|
|
452
|
+
const midParts = [];
|
|
453
|
+
const botParts = [];
|
|
454
|
+
for (let i = 0; i < mainChain.length; i++) {
|
|
455
|
+
const node = mainChain[i];
|
|
456
|
+
const innerWidth = Math.max(node.label.length + 2, 5);
|
|
457
|
+
const padLeft = Math.floor((innerWidth - node.label.length) / 2);
|
|
458
|
+
const padRight = innerWidth - node.label.length - padLeft;
|
|
459
|
+
topParts.push('\u250C' + '\u2500'.repeat(innerWidth) + '\u2510');
|
|
460
|
+
midParts.push('\u2502' + ' '.repeat(padLeft) + node.label + ' '.repeat(padRight) + '\u2502');
|
|
461
|
+
botParts.push('\u2514' + '\u2500'.repeat(innerWidth) + '\u2518');
|
|
462
|
+
if (i < mainChain.length - 1) {
|
|
463
|
+
topParts.push(' ');
|
|
464
|
+
midParts.push('\u2501\u2501\u2501\u25B6');
|
|
465
|
+
botParts.push(' ');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
outputLines.push(' ' + topParts.join(''));
|
|
469
|
+
outputLines.push(' ' + midParts.join(''));
|
|
470
|
+
outputLines.push(' ' + botParts.join(''));
|
|
471
|
+
if (parallelNodes.length > 0) {
|
|
472
|
+
outputLines.push('');
|
|
473
|
+
outputLines.push(' Parallel: ' + parallelNodes.map(n => n.label).join(', '));
|
|
474
|
+
}
|
|
475
|
+
for (const node of graph.nodes) {
|
|
476
|
+
if (node.scopeChildren && node.scopeChildren.length > 0) {
|
|
477
|
+
outputLines.push('');
|
|
478
|
+
outputLines.push(` Scope [${node.label}]: ` + node.scopeChildren.map(c => c.label).join(' \u2501\u25B6 '));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return outputLines.join('\n');
|
|
482
|
+
}
|
|
483
|
+
// ── renderText ───────────────────────────────────────────────────────────────
|
|
484
|
+
export function renderText(graph) {
|
|
485
|
+
const lines = [];
|
|
486
|
+
const connected = buildConnectedPorts(graph.connections);
|
|
487
|
+
lines.push(graph.workflowName);
|
|
488
|
+
lines.push('\u2550'.repeat(graph.workflowName.length));
|
|
489
|
+
lines.push('');
|
|
490
|
+
lines.push('Nodes:');
|
|
491
|
+
const maxLabelLen = Math.max(...graph.nodes.map(n => n.label.length), 5);
|
|
492
|
+
for (const node of graph.nodes) {
|
|
493
|
+
const label = node.label.padEnd(maxLabelLen);
|
|
494
|
+
const inputPorts = node.inputs.map(p => `${p.name}${portSymbol(node.id, p, connected)}`);
|
|
495
|
+
const outputPorts = node.outputs.map(p => `${p.name}${portSymbol(node.id, p, connected)}`);
|
|
496
|
+
const inputStr = inputPorts.length > 0 ? `[${inputPorts.join(', ')}]` : '';
|
|
497
|
+
const outputStr = outputPorts.length > 0 ? `[${outputPorts.join(', ')}]` : '';
|
|
498
|
+
if (inputStr && outputStr) {
|
|
499
|
+
lines.push(` ${label} ${inputStr} \u2192 ${outputStr}`);
|
|
500
|
+
}
|
|
501
|
+
else if (outputStr) {
|
|
502
|
+
lines.push(` ${label} ${outputStr}`);
|
|
503
|
+
}
|
|
504
|
+
else if (inputStr) {
|
|
505
|
+
lines.push(` ${label} ${inputStr}`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
lines.push(` ${label}`);
|
|
509
|
+
}
|
|
510
|
+
if (node.scopeChildren && node.scopeChildren.length > 0) {
|
|
511
|
+
lines.push(` ${' '.repeat(maxLabelLen)} scope: ${node.scopeChildren.map(c => c.label).join(', ')}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (graph.connections.length > 0) {
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push('Connections:');
|
|
517
|
+
const maxFromLen = Math.max(...graph.connections.map(c => `${c.fromNode}.${c.fromPort}`.length));
|
|
518
|
+
for (const conn of graph.connections) {
|
|
519
|
+
const from = `${conn.fromNode}.${conn.fromPort}`.padEnd(maxFromLen);
|
|
520
|
+
const arrow = conn.isStepConnection ? '\u2501\u2501\u25B6' : '\u2500\u2500\u25B6';
|
|
521
|
+
const to = `${conn.toNode}.${conn.toPort}`;
|
|
522
|
+
const type = conn.isStepConnection ? 'STEP' : '';
|
|
523
|
+
lines.push(` ${from} ${arrow} ${to}${type ? ' ' + type : ''}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return lines.join('\n');
|
|
527
|
+
}
|
|
528
|
+
//# sourceMappingURL=ascii-renderer.js.map
|
package/dist/diagram/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TWorkflowAST } from '../ast/types.js';
|
|
2
2
|
import type { DiagramOptions } from './types.js';
|
|
3
3
|
export type { DiagramOptions } from './types.js';
|
|
4
|
+
export { renderASCII, renderASCIICompact, renderText } from './ascii-renderer.js';
|
|
4
5
|
/**
|
|
5
6
|
* Render a workflow AST to an SVG string.
|
|
6
7
|
*/
|
|
@@ -25,4 +26,7 @@ export declare function sourceToHTML(code: string, options?: DiagramOptions): st
|
|
|
25
26
|
* Parse a workflow file and render the first (or named) workflow to interactive HTML.
|
|
26
27
|
*/
|
|
27
28
|
export declare function fileToHTML(filePath: string, options?: DiagramOptions): string;
|
|
29
|
+
export declare function workflowToASCII(ast: TWorkflowAST, options?: DiagramOptions): string;
|
|
30
|
+
export declare function sourceToASCII(code: string, options?: DiagramOptions): string;
|
|
31
|
+
export declare function fileToASCII(filePath: string, options?: DiagramOptions): string;
|
|
28
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/diagram/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { parser } from '../parser.js';
|
|
|
2
2
|
import { buildDiagramGraph } from './geometry.js';
|
|
3
3
|
import { renderSVG } from './renderer.js';
|
|
4
4
|
import { wrapSVGInHTML } from './html-viewer.js';
|
|
5
|
+
import { renderASCII, renderASCIICompact, renderText } from './ascii-renderer.js';
|
|
6
|
+
export { renderASCII, renderASCIICompact, renderText } from './ascii-renderer.js';
|
|
5
7
|
/**
|
|
6
8
|
* Render a workflow AST to an SVG string.
|
|
7
9
|
*/
|
|
@@ -97,4 +99,28 @@ function pickWorkflow(workflows, options) {
|
|
|
97
99
|
function pickAndRender(workflows, options) {
|
|
98
100
|
return workflowToSVG(pickWorkflow(workflows, options), options);
|
|
99
101
|
}
|
|
102
|
+
// ── ASCII / Text convenience functions ───────────────────────────────────────
|
|
103
|
+
function renderByFormat(graph, format) {
|
|
104
|
+
switch (format) {
|
|
105
|
+
case 'ascii': return renderASCII(graph);
|
|
106
|
+
case 'ascii-compact': return renderASCIICompact(graph);
|
|
107
|
+
case 'text': return renderText(graph);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function workflowToASCII(ast, options = {}) {
|
|
111
|
+
const graph = buildDiagramGraph(ast, options);
|
|
112
|
+
return renderByFormat(graph, options.format ?? 'ascii');
|
|
113
|
+
}
|
|
114
|
+
export function sourceToASCII(code, options = {}) {
|
|
115
|
+
const result = parser.parseFromString(code);
|
|
116
|
+
const ast = pickWorkflow(result.workflows, options);
|
|
117
|
+
const graph = buildDiagramGraph(ast, options);
|
|
118
|
+
return renderByFormat(graph, options.format ?? 'ascii');
|
|
119
|
+
}
|
|
120
|
+
export function fileToASCII(filePath, options = {}) {
|
|
121
|
+
const result = parser.parse(filePath);
|
|
122
|
+
const ast = pickWorkflow(result.workflows, options);
|
|
123
|
+
const graph = buildDiagramGraph(ast, options);
|
|
124
|
+
return renderByFormat(graph, options.format ?? 'ascii');
|
|
125
|
+
}
|
|
100
126
|
//# sourceMappingURL=index.js.map
|
package/dist/diagram/types.d.ts
CHANGED
package/dist/mcp/index.d.ts
CHANGED
|
@@ -8,5 +8,6 @@ export { registerQueryTools } from './tools-query.js';
|
|
|
8
8
|
export { registerTemplateTools } from './tools-template.js';
|
|
9
9
|
export { registerPatternTools } from './tools-pattern.js';
|
|
10
10
|
export { registerResources } from './resources.js';
|
|
11
|
+
export { registerPrompts } from './prompts.js';
|
|
11
12
|
export { startMcpServer, mcpServerCommand } from './server.js';
|
|
12
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/mcp/index.js
CHANGED
|
@@ -7,5 +7,6 @@ export { registerQueryTools } from './tools-query.js';
|
|
|
7
7
|
export { registerTemplateTools } from './tools-template.js';
|
|
8
8
|
export { registerPatternTools } from './tools-pattern.js';
|
|
9
9
|
export { registerResources } from './resources.js';
|
|
10
|
+
export { registerPrompts } from './prompts.js';
|
|
10
11
|
export { startMcpServer, mcpServerCommand } from './server.js';
|
|
11
12
|
//# sourceMappingURL=index.js.map
|