@tuongaz/seeflow 0.1.42 → 0.1.51
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/README.md +14 -0
- package/dist/web/assets/{index-BPUoNIBm.js → index-CFn1Jdmi.js} +17 -17
- package/dist/web/assets/{index-BlkUOp7f.css → index-DSfixlbD.css} +1 -1
- package/dist/web/assets/{index.es-mje3R_63.js → index.es-DQFAA-Eu.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DX3imOs2.js → jspdf.es.min-D7KeFi-m.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/flow.json +8 -8
- package/examples/order-pipeline/.seeflow/flow.json +4 -4
- package/package.json +2 -1
- package/src/api.ts +138 -102
- package/src/cli-e2e.ts +10 -6
- package/src/cli-helpers.ts +79 -0
- package/src/cli-manifest.ts +772 -0
- package/src/cli-ops.ts +18 -0
- package/src/cli.ts +164 -137
- package/src/events.ts +2 -1
- package/src/file-ref.ts +27 -16
- package/src/mcp.ts +104 -35
- package/src/merge.ts +3 -0
- package/src/node-files.ts +5 -2
- package/src/operations.ts +341 -7
- package/src/registry-watcher.ts +86 -0
- package/src/registry.ts +132 -24
- package/src/schema.ts +2 -0
- package/src/server.ts +9 -0
- package/src/watcher.ts +32 -2
package/src/mcp.ts
CHANGED
|
@@ -12,26 +12,11 @@ import {
|
|
|
12
12
|
CreateProjectBodySchema,
|
|
13
13
|
NodePatchBodySchema,
|
|
14
14
|
NodesBulkBodySchema,
|
|
15
|
-
type
|
|
15
|
+
type Operations,
|
|
16
16
|
PositionBodySchema,
|
|
17
17
|
RegisterBodySchema,
|
|
18
18
|
ReorderBodySchema,
|
|
19
|
-
|
|
20
|
-
addConnectorsBulkImpl,
|
|
21
|
-
addNodeImpl,
|
|
22
|
-
addNodesBulkImpl,
|
|
23
|
-
createProjectImpl,
|
|
24
|
-
deleteConnectorImpl,
|
|
25
|
-
deleteFlowImpl,
|
|
26
|
-
deleteNodeImpl,
|
|
27
|
-
getFlowImpl,
|
|
28
|
-
listDemosImpl,
|
|
29
|
-
moveNodeImpl,
|
|
30
|
-
patchConnectorImpl,
|
|
31
|
-
patchNodeImpl,
|
|
32
|
-
registerFlowImpl,
|
|
33
|
-
reorderNodeImpl,
|
|
34
|
-
validateImpl,
|
|
19
|
+
createOperations,
|
|
35
20
|
} from './operations.ts';
|
|
36
21
|
import type { Registry } from './registry.ts';
|
|
37
22
|
import type { FlowWatcher } from './watcher.ts';
|
|
@@ -187,13 +172,25 @@ const DeleteConnectorInputSchema = z.object({
|
|
|
187
172
|
connectorId: z.string().min(1),
|
|
188
173
|
});
|
|
189
174
|
|
|
190
|
-
const buildTools = (
|
|
175
|
+
const buildTools = (ops: Operations): McpTool[] => [
|
|
191
176
|
{
|
|
192
177
|
name: 'seeflow_list_flows',
|
|
193
178
|
description: 'List every flow registered with the studio.',
|
|
194
179
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
195
180
|
handler: async () => {
|
|
196
|
-
const result =
|
|
181
|
+
const result = ops.listFlows();
|
|
182
|
+
return okResult(result.data);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'seeflow_list_flows_summary',
|
|
187
|
+
description:
|
|
188
|
+
'List registered flows as { id, name, description } only. Use this for ' +
|
|
189
|
+
'cheap discovery before drilling into a specific flow with ' +
|
|
190
|
+
'seeflow_get_flow_graph or seeflow_get_node.',
|
|
191
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
192
|
+
handler: async () => {
|
|
193
|
+
const result = ops.listFlowsSummary();
|
|
197
194
|
return okResult(result.data);
|
|
198
195
|
},
|
|
199
196
|
},
|
|
@@ -217,7 +214,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
217
214
|
if (!body || !('flow' in body)) {
|
|
218
215
|
return errorResult('Body must include `flow`');
|
|
219
216
|
}
|
|
220
|
-
const result =
|
|
217
|
+
const result = ops.validate({
|
|
221
218
|
flow: body.flow,
|
|
222
219
|
style: body.style as unknown,
|
|
223
220
|
});
|
|
@@ -231,7 +228,28 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
231
228
|
handler: async (args) => {
|
|
232
229
|
const v = requireFlowId(args);
|
|
233
230
|
if ('error' in v) return errorResult(v.error);
|
|
234
|
-
const result = await
|
|
231
|
+
const result = await ops.getFlow(v.flowId);
|
|
232
|
+
switch (result.kind) {
|
|
233
|
+
case 'ok':
|
|
234
|
+
return okResult(result.data);
|
|
235
|
+
case 'notFound':
|
|
236
|
+
return errorResult('not found');
|
|
237
|
+
case 'fileNotFound':
|
|
238
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'seeflow_get_flow_graph',
|
|
244
|
+
description:
|
|
245
|
+
"Get a flow's nodes + connectors without inlining per-node file-backed " +
|
|
246
|
+
'content (`detail`, `html`). Cheap topology read — pair with seeflow_get_node ' +
|
|
247
|
+
"when you need a specific node's long-form body.",
|
|
248
|
+
inputSchema: FLOW_ID_INPUT_SCHEMA,
|
|
249
|
+
handler: async (args) => {
|
|
250
|
+
const v = requireFlowId(args);
|
|
251
|
+
if ('error' in v) return errorResult(v.error);
|
|
252
|
+
const result = await ops.getFlowGraph(v.flowId);
|
|
235
253
|
switch (result.kind) {
|
|
236
254
|
case 'ok':
|
|
237
255
|
return okResult(result.data);
|
|
@@ -239,6 +257,56 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
239
257
|
return errorResult('not found');
|
|
240
258
|
case 'fileNotFound':
|
|
241
259
|
return errorResult(`Flow file not found: ${result.path}`);
|
|
260
|
+
case 'badJson':
|
|
261
|
+
return errorResult(`Flow file is not valid JSON: ${result.detail}`);
|
|
262
|
+
case 'badSchema':
|
|
263
|
+
return errorResult(
|
|
264
|
+
`Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'seeflow_get_node',
|
|
271
|
+
description:
|
|
272
|
+
'Get a single node from a flow with its file-backed content (detail.md, ' +
|
|
273
|
+
'view.html) inlined. Use after seeflow_get_flow_graph to drill into one node.',
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
flowId: { type: 'string', minLength: 1 },
|
|
278
|
+
nodeId: { type: 'string', minLength: 1 },
|
|
279
|
+
},
|
|
280
|
+
required: ['flowId', 'nodeId'],
|
|
281
|
+
additionalProperties: false,
|
|
282
|
+
},
|
|
283
|
+
handler: async (args) => {
|
|
284
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
285
|
+
return errorResult('Invalid arguments: expected an object with flowId + nodeId');
|
|
286
|
+
}
|
|
287
|
+
const { flowId, nodeId } = args as { flowId?: unknown; nodeId?: unknown };
|
|
288
|
+
if (typeof flowId !== 'string' || flowId.length === 0) {
|
|
289
|
+
return errorResult('Invalid arguments: flowId must be a non-empty string');
|
|
290
|
+
}
|
|
291
|
+
if (typeof nodeId !== 'string' || nodeId.length === 0) {
|
|
292
|
+
return errorResult('Invalid arguments: nodeId must be a non-empty string');
|
|
293
|
+
}
|
|
294
|
+
const result = await ops.getNode(flowId, nodeId);
|
|
295
|
+
switch (result.kind) {
|
|
296
|
+
case 'ok':
|
|
297
|
+
return okResult(result.data);
|
|
298
|
+
case 'notFound':
|
|
299
|
+
return errorResult('not found');
|
|
300
|
+
case 'fileNotFound':
|
|
301
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
302
|
+
case 'unknownNode':
|
|
303
|
+
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
304
|
+
case 'badJson':
|
|
305
|
+
return errorResult(`Flow file is not valid JSON: ${result.detail}`);
|
|
306
|
+
case 'badSchema':
|
|
307
|
+
return errorResult(
|
|
308
|
+
`Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
309
|
+
);
|
|
242
310
|
}
|
|
243
311
|
},
|
|
244
312
|
},
|
|
@@ -251,7 +319,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
251
319
|
if (!parsed.success) {
|
|
252
320
|
return errorResult(`Invalid register body: ${JSON.stringify(parsed.error.issues)}`);
|
|
253
321
|
}
|
|
254
|
-
const result = await
|
|
322
|
+
const result = await ops.registerFlow(parsed.data);
|
|
255
323
|
switch (result.kind) {
|
|
256
324
|
case 'ok':
|
|
257
325
|
return okResult(result.data);
|
|
@@ -275,7 +343,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
275
343
|
handler: async (args) => {
|
|
276
344
|
const v = requireFlowId(args);
|
|
277
345
|
if ('error' in v) return errorResult(v.error);
|
|
278
|
-
const result =
|
|
346
|
+
const result = ops.deleteFlow(v.flowId);
|
|
279
347
|
switch (result.kind) {
|
|
280
348
|
case 'ok':
|
|
281
349
|
return okResult({ ok: true });
|
|
@@ -293,7 +361,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
293
361
|
if (!parsed.success) {
|
|
294
362
|
return errorResult(`Invalid create project body: ${JSON.stringify(parsed.error.issues)}`);
|
|
295
363
|
}
|
|
296
|
-
const result = await
|
|
364
|
+
const result = await ops.createProject(parsed.data);
|
|
297
365
|
switch (result.kind) {
|
|
298
366
|
case 'ok':
|
|
299
367
|
return okResult(result.data);
|
|
@@ -321,7 +389,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
321
389
|
return errorResult(`Invalid add_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
322
390
|
}
|
|
323
391
|
const { flowId, node } = parsed.data;
|
|
324
|
-
const result = await
|
|
392
|
+
const result = await ops.addNode(flowId, node);
|
|
325
393
|
switch (result.kind) {
|
|
326
394
|
case 'ok':
|
|
327
395
|
return okResult({ ok: true, id: result.data.id, node: result.data.node });
|
|
@@ -349,7 +417,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
349
417
|
return errorResult(`Invalid add_nodes arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
350
418
|
}
|
|
351
419
|
const { flowId, nodes } = parsed.data;
|
|
352
|
-
const result = await
|
|
420
|
+
const result = await ops.addNodesBulk(flowId, { nodes });
|
|
353
421
|
switch (result.kind) {
|
|
354
422
|
case 'ok':
|
|
355
423
|
return okResult({ ok: true, nodes: result.data.nodes });
|
|
@@ -380,7 +448,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
380
448
|
return errorResult(`Invalid delete_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
381
449
|
}
|
|
382
450
|
const { flowId, nodeId } = parsed.data;
|
|
383
|
-
const result = await
|
|
451
|
+
const result = await ops.deleteNode(flowId, nodeId);
|
|
384
452
|
switch (result.kind) {
|
|
385
453
|
case 'ok':
|
|
386
454
|
return okResult({ ok: true });
|
|
@@ -409,7 +477,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
409
477
|
return errorResult(`Invalid move_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
410
478
|
}
|
|
411
479
|
const { flowId, nodeId, x, y } = parsed.data;
|
|
412
|
-
const result = await
|
|
480
|
+
const result = await ops.moveNode(flowId, nodeId, { x, y });
|
|
413
481
|
switch (result.kind) {
|
|
414
482
|
case 'ok':
|
|
415
483
|
return okResult({ ok: true, position: result.data.position });
|
|
@@ -439,7 +507,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
439
507
|
return errorResult(`Invalid patch_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
440
508
|
}
|
|
441
509
|
const { flowId, nodeId, ...updates } = parsed.data;
|
|
442
|
-
const result = await
|
|
510
|
+
const result = await ops.patchNode(flowId, nodeId, updates);
|
|
443
511
|
switch (result.kind) {
|
|
444
512
|
case 'ok':
|
|
445
513
|
return okResult({ ok: true });
|
|
@@ -475,7 +543,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
475
543
|
// reorderNodeImpl receives the same discriminated union the REST route
|
|
476
544
|
// does — keeps a single source of truth for op semantics.
|
|
477
545
|
const reorderBody = ReorderBodySchema.parse(body);
|
|
478
|
-
const result = await
|
|
546
|
+
const result = await ops.reorderNode(flowId, nodeId, reorderBody);
|
|
479
547
|
switch (result.kind) {
|
|
480
548
|
case 'ok':
|
|
481
549
|
return okResult({ ok: true });
|
|
@@ -507,7 +575,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
507
575
|
);
|
|
508
576
|
}
|
|
509
577
|
const { flowId, connector } = parsed.data;
|
|
510
|
-
const result = await
|
|
578
|
+
const result = await ops.addConnector(flowId, connector);
|
|
511
579
|
switch (result.kind) {
|
|
512
580
|
case 'ok':
|
|
513
581
|
return okResult({ ok: true, id: result.data.id });
|
|
@@ -537,7 +605,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
537
605
|
);
|
|
538
606
|
}
|
|
539
607
|
const { flowId, connectors } = parsed.data;
|
|
540
|
-
const result = await
|
|
608
|
+
const result = await ops.addConnectorsBulk(flowId, { connectors });
|
|
541
609
|
switch (result.kind) {
|
|
542
610
|
case 'ok':
|
|
543
611
|
return okResult({ ok: true, connectors: result.data.connectors });
|
|
@@ -571,7 +639,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
571
639
|
);
|
|
572
640
|
}
|
|
573
641
|
const { flowId, connectorId, ...updates } = parsed.data;
|
|
574
|
-
const result = await
|
|
642
|
+
const result = await ops.patchConnector(flowId, connectorId, updates);
|
|
575
643
|
switch (result.kind) {
|
|
576
644
|
case 'ok':
|
|
577
645
|
return okResult({ ok: true });
|
|
@@ -602,7 +670,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
602
670
|
);
|
|
603
671
|
}
|
|
604
672
|
const { flowId, connectorId } = parsed.data;
|
|
605
|
-
const result = await
|
|
673
|
+
const result = await ops.deleteConnector(flowId, connectorId);
|
|
606
674
|
switch (result.kind) {
|
|
607
675
|
case 'ok':
|
|
608
676
|
return okResult({ ok: true });
|
|
@@ -630,11 +698,12 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
630
698
|
* mcp-shim.ts) — every request builds its own server in stateless mode.
|
|
631
699
|
*/
|
|
632
700
|
export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
633
|
-
const
|
|
701
|
+
const ops = createOperations({
|
|
634
702
|
registry: options.registry,
|
|
635
703
|
watcher: options.watcher,
|
|
636
704
|
projectBaseDir: options.projectBaseDir,
|
|
637
705
|
});
|
|
706
|
+
const tools = buildTools(ops);
|
|
638
707
|
|
|
639
708
|
const server = new Server({ name: 'seeflow', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
640
709
|
|
package/src/merge.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function mergeFlowAndStyle(flow: Flow, style: Style): ResolvedFlow {
|
|
|
31
31
|
return {
|
|
32
32
|
version: flow.version,
|
|
33
33
|
name: flow.name,
|
|
34
|
+
...(flow.description !== undefined ? { description: flow.description } : {}),
|
|
34
35
|
...(flow.resetAction ? { resetAction: flow.resetAction } : {}),
|
|
35
36
|
nodes: mergedNodes,
|
|
36
37
|
connectors: mergedConnectors,
|
|
@@ -111,6 +112,7 @@ const CONNECTOR_STYLE_KEYS = new Set([
|
|
|
111
112
|
export function splitFlow(resolved: {
|
|
112
113
|
version: number;
|
|
113
114
|
name: string;
|
|
115
|
+
description?: string;
|
|
114
116
|
resetAction?: unknown;
|
|
115
117
|
nodes: Array<Record<string, unknown>>;
|
|
116
118
|
connectors: Array<Record<string, unknown>>;
|
|
@@ -179,6 +181,7 @@ export function splitFlow(resolved: {
|
|
|
179
181
|
nodes: flowNodes,
|
|
180
182
|
connectors: flowConnectors,
|
|
181
183
|
};
|
|
184
|
+
if (resolved.description !== undefined) flow.description = resolved.description;
|
|
182
185
|
if (resolved.resetAction !== undefined) flow.resetAction = resolved.resetAction;
|
|
183
186
|
|
|
184
187
|
const style: Record<string, unknown> = {};
|
package/src/node-files.ts
CHANGED
|
@@ -29,8 +29,11 @@ export type ExternalizedFieldName = (typeof EXTERNALIZED_NODE_FIELDS)[number]['f
|
|
|
29
29
|
export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
30
30
|
`nodes/${nodeId}/${fileName}`;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Node-relative ref: the resolver knows the enclosing node id from the flow.json
|
|
33
|
+
// shape (nodes[i].id), so the on-disk string only needs the filename. Kept as a
|
|
34
|
+
// 2-arg helper so call sites don't change shape and the spec stays explicit
|
|
35
|
+
// that the file lives under the given node.
|
|
36
|
+
export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
|
|
34
37
|
|
|
35
38
|
export const nodeFileAbsPath = (repoPath: string, nodeId: string, fileName: string): string =>
|
|
36
39
|
join(repoPath, '.seeflow', nodeFileRelPath(nodeId, fileName));
|