@tuongaz/seeflow 0.1.26 → 0.1.27
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/web/assets/{index-BMaMEi2a.js → index-CdNWAi1U.js} +4 -4
- package/dist/web/assets/{index.es-M1iBDKG6.js → index.es-CPyvUCV3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-xZpq8bcn.js → jspdf.es.min-Dkq0NSxE.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +14 -77
- package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
- package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
- package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
- package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/style.json +85 -0
- package/examples/order-pipeline/.seeflow/architecture.json +93 -0
- package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
- package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
- package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
- package/examples/order-pipeline/.seeflow/style.json +42 -0
- package/package.json +1 -1
- package/src/api.ts +118 -118
- package/src/cli.ts +13 -13
- package/src/demo.ts +6 -6
- package/src/diagram.ts +4 -4
- package/src/events.ts +14 -14
- package/src/file-ref.ts +79 -0
- package/src/mcp.ts +117 -89
- package/src/merge.ts +190 -0
- package/src/operations.ts +415 -416
- package/src/proxy.ts +31 -31
- package/src/registry.ts +32 -20
- package/src/schema.ts +252 -8
- package/src/sdk-template.ts +2 -2
- package/src/sdk-writer.ts +2 -2
- package/src/server.ts +2 -2
- package/src/status-runner.ts +34 -38
- package/src/watcher.ts +165 -114
- package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/mcp.ts
CHANGED
|
@@ -18,22 +18,23 @@ import {
|
|
|
18
18
|
addNodeImpl,
|
|
19
19
|
createProjectImpl,
|
|
20
20
|
deleteConnectorImpl,
|
|
21
|
-
|
|
21
|
+
deleteFlowImpl,
|
|
22
22
|
deleteNodeImpl,
|
|
23
|
-
|
|
23
|
+
getFlowImpl,
|
|
24
24
|
listDemosImpl,
|
|
25
25
|
moveNodeImpl,
|
|
26
26
|
patchConnectorImpl,
|
|
27
27
|
patchNodeImpl,
|
|
28
|
-
|
|
28
|
+
registerFlowImpl,
|
|
29
29
|
reorderNodeImpl,
|
|
30
|
+
validateImpl,
|
|
30
31
|
} from './operations.ts';
|
|
31
32
|
import type { Registry } from './registry.ts';
|
|
32
|
-
import type {
|
|
33
|
+
import type { FlowWatcher } from './watcher.ts';
|
|
33
34
|
|
|
34
35
|
export interface CreateMcpServerOptions {
|
|
35
36
|
registry: Registry;
|
|
36
|
-
watcher?:
|
|
37
|
+
watcher?: FlowWatcher;
|
|
37
38
|
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
38
39
|
projectBaseDir?: string;
|
|
39
40
|
}
|
|
@@ -69,44 +70,44 @@ const errorResult = (text: string): CallToolResult => ({
|
|
|
69
70
|
content: [{ type: 'text', text }],
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
// Most MCP tools take a single
|
|
73
|
+
// Most MCP tools take a single flowId argument. Defined inline as plain
|
|
73
74
|
// JSON Schema (rather than a one-off Zod schema) because there's no REST
|
|
74
75
|
// counterpart to share with.
|
|
75
76
|
const DEMO_ID_INPUT_SCHEMA = {
|
|
76
77
|
type: 'object',
|
|
77
|
-
properties: {
|
|
78
|
-
required: ['
|
|
78
|
+
properties: { flowId: { type: 'string', minLength: 1 } },
|
|
79
|
+
required: ['flowId'],
|
|
79
80
|
additionalProperties: false,
|
|
80
81
|
} as const;
|
|
81
82
|
|
|
82
|
-
const requireDemoId = (args: unknown): {
|
|
83
|
+
const requireDemoId = (args: unknown): { flowId: string } | { error: string } => {
|
|
83
84
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
|
-
return { error: 'Invalid arguments: expected an object with
|
|
85
|
+
return { error: 'Invalid arguments: expected an object with flowId' };
|
|
85
86
|
}
|
|
86
|
-
const {
|
|
87
|
-
if (typeof
|
|
88
|
-
return { error: 'Invalid arguments:
|
|
87
|
+
const { flowId } = args as { flowId?: unknown };
|
|
88
|
+
if (typeof flowId !== 'string' || flowId.length === 0) {
|
|
89
|
+
return { error: 'Invalid arguments: flowId must be a non-empty string' };
|
|
89
90
|
}
|
|
90
|
-
return {
|
|
91
|
+
return { flowId };
|
|
91
92
|
};
|
|
92
93
|
|
|
93
|
-
// {
|
|
94
|
+
// {flowId, nodeId} body shape shared by move + reorder + delete inputs.
|
|
94
95
|
const DemoNodeIdBaseSchema = z.object({
|
|
95
|
-
|
|
96
|
+
flowId: z.string().min(1),
|
|
96
97
|
nodeId: z.string().min(1),
|
|
97
98
|
});
|
|
98
99
|
|
|
99
|
-
// add_node input: {
|
|
100
|
-
// loose here (additionalProperties=true via passthrough) because
|
|
100
|
+
// add_node input: { flowId, node: <node payload> }. The inner `node` object is
|
|
101
|
+
// loose here (additionalProperties=true via passthrough) because FlowSchema
|
|
101
102
|
// runs the full validation server-side after the new node is merged in.
|
|
102
103
|
const AddNodeInputSchema = z.object({
|
|
103
|
-
|
|
104
|
+
flowId: z.string().min(1),
|
|
104
105
|
node: z.record(z.unknown()),
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
const DeleteNodeInputSchema = DemoNodeIdBaseSchema;
|
|
108
109
|
|
|
109
|
-
// move_node input: {
|
|
110
|
+
// move_node input: { flowId, nodeId } extended with PositionBodySchema's
|
|
110
111
|
// { x, y } fields so agents see one flat schema.
|
|
111
112
|
const MoveNodeInputSchema = DemoNodeIdBaseSchema.extend({
|
|
112
113
|
x: PositionBodySchema.shape.x,
|
|
@@ -114,7 +115,7 @@ const MoveNodeInputSchema = DemoNodeIdBaseSchema.extend({
|
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
// reorder_node input: each branch of the existing ReorderBodySchema
|
|
117
|
-
// discriminated union extended with
|
|
118
|
+
// discriminated union extended with flowId/nodeId. Keeps the discriminator
|
|
118
119
|
// on `op` so the emitted JSON Schema is an oneOf the agent can introspect.
|
|
119
120
|
const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
120
121
|
DemoNodeIdBaseSchema.extend({ op: z.literal('forward') }),
|
|
@@ -127,42 +128,42 @@ const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
|
127
128
|
}),
|
|
128
129
|
]);
|
|
129
130
|
|
|
130
|
-
// patch_node input: {
|
|
131
|
+
// patch_node input: { flowId, nodeId } merged with NodePatchBodySchema's
|
|
131
132
|
// optional fields. .extend() on the strict body schema preserves strict
|
|
132
133
|
// mode, so unknown top-level keys still trip the Zod parse before any disk
|
|
133
134
|
// IO — matching the REST handler's "Invalid node patch body" 400 path.
|
|
134
135
|
const PatchNodeInputSchema = NodePatchBodySchema.extend({
|
|
135
|
-
|
|
136
|
+
flowId: z.string().min(1),
|
|
136
137
|
nodeId: z.string().min(1),
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
// add_connector input: {
|
|
140
|
+
// add_connector input: { flowId, connector: <connector payload> }. The inner
|
|
140
141
|
// `connector` object is loose (additionalProperties=true via z.record) because
|
|
141
|
-
//
|
|
142
|
+
// FlowSchema runs the full validation server-side after the new connector is
|
|
142
143
|
// merged in (post-mutation parse catches dangling source/target refs and
|
|
143
144
|
// kind-discriminator violations).
|
|
144
145
|
const AddConnectorInputSchema = z.object({
|
|
145
|
-
|
|
146
|
+
flowId: z.string().min(1),
|
|
146
147
|
connector: z.record(z.unknown()),
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
// patch_connector input: {
|
|
150
|
+
// patch_connector input: { flowId, connectorId } merged with the strict
|
|
150
151
|
// ConnectorPatchBodySchema. .extend() preserves strict mode so unknown
|
|
151
152
|
// top-level keys trip the Zod parse before any IO — matching the REST
|
|
152
153
|
// handler's "Invalid connector patch body" 400 path.
|
|
153
154
|
const PatchConnectorInputSchema = ConnectorPatchBodySchema.extend({
|
|
154
|
-
|
|
155
|
+
flowId: z.string().min(1),
|
|
155
156
|
connectorId: z.string().min(1),
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
const DeleteConnectorInputSchema = z.object({
|
|
159
|
-
|
|
160
|
+
flowId: z.string().min(1),
|
|
160
161
|
connectorId: z.string().min(1),
|
|
161
162
|
});
|
|
162
163
|
|
|
163
164
|
const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
164
165
|
{
|
|
165
|
-
name: '
|
|
166
|
+
name: 'seeflow_list_flows',
|
|
166
167
|
description: 'List every demo registered with the studio.',
|
|
167
168
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
168
169
|
handler: async () => {
|
|
@@ -171,25 +172,52 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
171
172
|
},
|
|
172
173
|
},
|
|
173
174
|
{
|
|
174
|
-
name: '
|
|
175
|
-
description:
|
|
175
|
+
name: 'validate_seeflow',
|
|
176
|
+
description:
|
|
177
|
+
'Validate an architecture.json (and optional style.json) against the ' +
|
|
178
|
+
'SeeFlow schemas. Stateless: no flow id, no file:// resolution, no ' +
|
|
179
|
+
'registry side-effects. Returns { ok: true } or { ok: false, issues }.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
architecture: { type: 'object' },
|
|
184
|
+
style: { type: 'object' },
|
|
185
|
+
},
|
|
186
|
+
required: ['architecture'],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
},
|
|
189
|
+
handler: async (args) => {
|
|
190
|
+
const body = args as Record<string, unknown> | undefined;
|
|
191
|
+
if (!body || !('architecture' in body)) {
|
|
192
|
+
return errorResult('Body must include `architecture`');
|
|
193
|
+
}
|
|
194
|
+
const result = validateImpl({
|
|
195
|
+
architecture: body.architecture,
|
|
196
|
+
style: body.style as unknown,
|
|
197
|
+
});
|
|
198
|
+
return okResult(result);
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'seeflow_get_flow',
|
|
203
|
+
description: 'Get the full demo definition and on-disk state for a flowId.',
|
|
176
204
|
inputSchema: DEMO_ID_INPUT_SCHEMA,
|
|
177
205
|
handler: async (args) => {
|
|
178
206
|
const v = requireDemoId(args);
|
|
179
207
|
if ('error' in v) return errorResult(v.error);
|
|
180
|
-
const result = await
|
|
208
|
+
const result = await getFlowImpl(deps, v.flowId);
|
|
181
209
|
switch (result.kind) {
|
|
182
210
|
case 'ok':
|
|
183
211
|
return okResult(result.data);
|
|
184
212
|
case 'notFound':
|
|
185
213
|
return errorResult('not found');
|
|
186
214
|
case 'fileNotFound':
|
|
187
|
-
return errorResult(`
|
|
215
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
188
216
|
}
|
|
189
217
|
},
|
|
190
218
|
},
|
|
191
219
|
{
|
|
192
|
-
name: '
|
|
220
|
+
name: 'seeflow_register_flow',
|
|
193
221
|
description: 'Register an existing demo file on disk with the studio.',
|
|
194
222
|
inputSchema: inputSchemaFromZod(RegisterBodySchema),
|
|
195
223
|
handler: async (args) => {
|
|
@@ -197,17 +225,17 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
197
225
|
if (!parsed.success) {
|
|
198
226
|
return errorResult(`Invalid register body: ${JSON.stringify(parsed.error.issues)}`);
|
|
199
227
|
}
|
|
200
|
-
const result = await
|
|
228
|
+
const result = await registerFlowImpl(deps, parsed.data);
|
|
201
229
|
switch (result.kind) {
|
|
202
230
|
case 'ok':
|
|
203
231
|
return okResult(result.data);
|
|
204
232
|
case 'fileNotFound':
|
|
205
|
-
return errorResult(`
|
|
233
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
206
234
|
case 'badJson':
|
|
207
|
-
return errorResult(`
|
|
235
|
+
return errorResult(`Flow file is not valid JSON: ${result.detail}`);
|
|
208
236
|
case 'badSchema':
|
|
209
237
|
return errorResult(
|
|
210
|
-
`
|
|
238
|
+
`Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
211
239
|
);
|
|
212
240
|
case 'sdkWriteFailed':
|
|
213
241
|
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
@@ -215,13 +243,13 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
215
243
|
},
|
|
216
244
|
},
|
|
217
245
|
{
|
|
218
|
-
name: '
|
|
246
|
+
name: 'seeflow_delete_flow',
|
|
219
247
|
description: 'Unregister a demo from the studio (the on-disk file is left untouched).',
|
|
220
248
|
inputSchema: DEMO_ID_INPUT_SCHEMA,
|
|
221
249
|
handler: async (args) => {
|
|
222
250
|
const v = requireDemoId(args);
|
|
223
251
|
if ('error' in v) return errorResult(v.error);
|
|
224
|
-
const result =
|
|
252
|
+
const result = deleteFlowImpl(deps, v.flowId);
|
|
225
253
|
switch (result.kind) {
|
|
226
254
|
case 'ok':
|
|
227
255
|
return okResult({ ok: true });
|
|
@@ -265,19 +293,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
265
293
|
if (!parsed.success) {
|
|
266
294
|
return errorResult(`Invalid add_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
267
295
|
}
|
|
268
|
-
const {
|
|
269
|
-
const result = await addNodeImpl(deps,
|
|
296
|
+
const { flowId, node } = parsed.data;
|
|
297
|
+
const result = await addNodeImpl(deps, flowId, node);
|
|
270
298
|
switch (result.kind) {
|
|
271
299
|
case 'ok':
|
|
272
300
|
return okResult({ ok: true, id: result.data.id, node: result.data.node });
|
|
273
|
-
case '
|
|
301
|
+
case 'flowNotFound':
|
|
274
302
|
return errorResult('unknown demo');
|
|
275
303
|
case 'fileNotFound':
|
|
276
|
-
return errorResult(`
|
|
304
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
277
305
|
case 'badJson':
|
|
278
|
-
return errorResult(`
|
|
306
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
279
307
|
case 'badSchema':
|
|
280
|
-
return errorResult(`
|
|
308
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
281
309
|
case 'writeFailed':
|
|
282
310
|
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
283
311
|
}
|
|
@@ -292,19 +320,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
292
320
|
if (!parsed.success) {
|
|
293
321
|
return errorResult(`Invalid delete_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
294
322
|
}
|
|
295
|
-
const {
|
|
296
|
-
const result = await deleteNodeImpl(deps,
|
|
323
|
+
const { flowId, nodeId } = parsed.data;
|
|
324
|
+
const result = await deleteNodeImpl(deps, flowId, nodeId);
|
|
297
325
|
switch (result.kind) {
|
|
298
326
|
case 'ok':
|
|
299
327
|
return okResult({ ok: true });
|
|
300
|
-
case '
|
|
328
|
+
case 'flowNotFound':
|
|
301
329
|
return errorResult('unknown demo');
|
|
302
330
|
case 'fileNotFound':
|
|
303
|
-
return errorResult(`
|
|
331
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
304
332
|
case 'badJson':
|
|
305
|
-
return errorResult(`
|
|
333
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
306
334
|
case 'badSchema':
|
|
307
|
-
return errorResult(`
|
|
335
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
308
336
|
case 'unknownNode':
|
|
309
337
|
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
310
338
|
case 'writeFailed':
|
|
@@ -321,19 +349,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
321
349
|
if (!parsed.success) {
|
|
322
350
|
return errorResult(`Invalid move_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
323
351
|
}
|
|
324
|
-
const {
|
|
325
|
-
const result = await moveNodeImpl(deps,
|
|
352
|
+
const { flowId, nodeId, x, y } = parsed.data;
|
|
353
|
+
const result = await moveNodeImpl(deps, flowId, nodeId, { x, y });
|
|
326
354
|
switch (result.kind) {
|
|
327
355
|
case 'ok':
|
|
328
356
|
return okResult({ ok: true, position: result.data.position });
|
|
329
|
-
case '
|
|
357
|
+
case 'flowNotFound':
|
|
330
358
|
return errorResult('unknown demo');
|
|
331
359
|
case 'fileNotFound':
|
|
332
|
-
return errorResult(`
|
|
360
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
333
361
|
case 'badJson':
|
|
334
|
-
return errorResult(`
|
|
362
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
335
363
|
case 'badSchema':
|
|
336
|
-
return errorResult(`
|
|
364
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
337
365
|
case 'unknownNode':
|
|
338
366
|
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
339
367
|
case 'writeFailed':
|
|
@@ -351,19 +379,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
351
379
|
if (!parsed.success) {
|
|
352
380
|
return errorResult(`Invalid patch_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
353
381
|
}
|
|
354
|
-
const {
|
|
355
|
-
const result = await patchNodeImpl(deps,
|
|
382
|
+
const { flowId, nodeId, ...updates } = parsed.data;
|
|
383
|
+
const result = await patchNodeImpl(deps, flowId, nodeId, updates);
|
|
356
384
|
switch (result.kind) {
|
|
357
385
|
case 'ok':
|
|
358
386
|
return okResult({ ok: true });
|
|
359
|
-
case '
|
|
387
|
+
case 'flowNotFound':
|
|
360
388
|
return errorResult('unknown demo');
|
|
361
389
|
case 'fileNotFound':
|
|
362
|
-
return errorResult(`
|
|
390
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
363
391
|
case 'badJson':
|
|
364
|
-
return errorResult(`
|
|
392
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
365
393
|
case 'badSchema':
|
|
366
|
-
return errorResult(`
|
|
394
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
367
395
|
case 'unknownNode':
|
|
368
396
|
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
369
397
|
case 'writeFailed':
|
|
@@ -383,23 +411,23 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
383
411
|
`Invalid reorder_node arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
384
412
|
);
|
|
385
413
|
}
|
|
386
|
-
const {
|
|
414
|
+
const { flowId, nodeId, ...body } = parsed.data;
|
|
387
415
|
// Delegate the op-specific shape to the existing ReorderBodySchema so
|
|
388
416
|
// reorderNodeImpl receives the same discriminated union the REST route
|
|
389
417
|
// does — keeps a single source of truth for op semantics.
|
|
390
418
|
const reorderBody = ReorderBodySchema.parse(body);
|
|
391
|
-
const result = await reorderNodeImpl(deps,
|
|
419
|
+
const result = await reorderNodeImpl(deps, flowId, nodeId, reorderBody);
|
|
392
420
|
switch (result.kind) {
|
|
393
421
|
case 'ok':
|
|
394
422
|
return okResult({ ok: true });
|
|
395
|
-
case '
|
|
423
|
+
case 'flowNotFound':
|
|
396
424
|
return errorResult('unknown demo');
|
|
397
425
|
case 'fileNotFound':
|
|
398
|
-
return errorResult(`
|
|
426
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
399
427
|
case 'badJson':
|
|
400
|
-
return errorResult(`
|
|
428
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
401
429
|
case 'badSchema':
|
|
402
|
-
return errorResult(`
|
|
430
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
403
431
|
case 'unknownNode':
|
|
404
432
|
return errorResult(`Unknown nodeId: ${nodeId}`);
|
|
405
433
|
case 'writeFailed':
|
|
@@ -419,19 +447,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
419
447
|
`Invalid add_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
420
448
|
);
|
|
421
449
|
}
|
|
422
|
-
const {
|
|
423
|
-
const result = await addConnectorImpl(deps,
|
|
450
|
+
const { flowId, connector } = parsed.data;
|
|
451
|
+
const result = await addConnectorImpl(deps, flowId, connector);
|
|
424
452
|
switch (result.kind) {
|
|
425
453
|
case 'ok':
|
|
426
454
|
return okResult({ ok: true, id: result.data.id });
|
|
427
|
-
case '
|
|
455
|
+
case 'flowNotFound':
|
|
428
456
|
return errorResult('unknown demo');
|
|
429
457
|
case 'fileNotFound':
|
|
430
|
-
return errorResult(`
|
|
458
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
431
459
|
case 'badJson':
|
|
432
|
-
return errorResult(`
|
|
460
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
433
461
|
case 'badSchema':
|
|
434
|
-
return errorResult(`
|
|
462
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
435
463
|
case 'writeFailed':
|
|
436
464
|
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
437
465
|
}
|
|
@@ -449,19 +477,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
449
477
|
`Invalid patch_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
450
478
|
);
|
|
451
479
|
}
|
|
452
|
-
const {
|
|
453
|
-
const result = await patchConnectorImpl(deps,
|
|
480
|
+
const { flowId, connectorId, ...updates } = parsed.data;
|
|
481
|
+
const result = await patchConnectorImpl(deps, flowId, connectorId, updates);
|
|
454
482
|
switch (result.kind) {
|
|
455
483
|
case 'ok':
|
|
456
484
|
return okResult({ ok: true });
|
|
457
|
-
case '
|
|
485
|
+
case 'flowNotFound':
|
|
458
486
|
return errorResult('unknown demo');
|
|
459
487
|
case 'fileNotFound':
|
|
460
|
-
return errorResult(`
|
|
488
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
461
489
|
case 'badJson':
|
|
462
|
-
return errorResult(`
|
|
490
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
463
491
|
case 'badSchema':
|
|
464
|
-
return errorResult(`
|
|
492
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
465
493
|
case 'unknownConnector':
|
|
466
494
|
return errorResult(`Unknown connectorId: ${connectorId}`);
|
|
467
495
|
case 'writeFailed':
|
|
@@ -480,19 +508,19 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
480
508
|
`Invalid delete_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
481
509
|
);
|
|
482
510
|
}
|
|
483
|
-
const {
|
|
484
|
-
const result = await deleteConnectorImpl(deps,
|
|
511
|
+
const { flowId, connectorId } = parsed.data;
|
|
512
|
+
const result = await deleteConnectorImpl(deps, flowId, connectorId);
|
|
485
513
|
switch (result.kind) {
|
|
486
514
|
case 'ok':
|
|
487
515
|
return okResult({ ok: true });
|
|
488
|
-
case '
|
|
516
|
+
case 'flowNotFound':
|
|
489
517
|
return errorResult('unknown demo');
|
|
490
518
|
case 'fileNotFound':
|
|
491
|
-
return errorResult(`
|
|
519
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
492
520
|
case 'badJson':
|
|
493
|
-
return errorResult(`
|
|
521
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
494
522
|
case 'badSchema':
|
|
495
|
-
return errorResult(`
|
|
523
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
496
524
|
case 'unknownConnector':
|
|
497
525
|
return errorResult(`Unknown connectorId: ${connectorId}`);
|
|
498
526
|
case 'writeFailed':
|
package/src/merge.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Architecture, Flow, Style } from './schema.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge architecture.json (semantic data) and the optional style.json
|
|
5
|
+
* (presentation overrides) into the merged Flow shape consumed by the API,
|
|
6
|
+
* the canvas, and the rest of the studio.
|
|
7
|
+
*
|
|
8
|
+
* Style entries with no matching architecture id are silently dropped — the
|
|
9
|
+
* write path strips dangling entries after delete, but a stale file on disk
|
|
10
|
+
* shouldn't break the read path.
|
|
11
|
+
*/
|
|
12
|
+
export function mergeArchitectureAndStyle(arch: Architecture, style: Style): Flow {
|
|
13
|
+
const nodeStyles = style.nodes ?? {};
|
|
14
|
+
const connectorStyles = style.connectors ?? {};
|
|
15
|
+
|
|
16
|
+
const mergedNodes = arch.nodes.map((node) => {
|
|
17
|
+
const s = nodeStyles[node.id] ?? {};
|
|
18
|
+
const { position, ...visual } = s;
|
|
19
|
+
return {
|
|
20
|
+
...node,
|
|
21
|
+
position: position ?? { x: 0, y: 0 },
|
|
22
|
+
data: { ...node.data, ...visual },
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const mergedConnectors = arch.connectors.map((conn) => {
|
|
27
|
+
const s = connectorStyles[conn.id] ?? {};
|
|
28
|
+
return { ...conn, ...s };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
version: arch.version,
|
|
33
|
+
name: arch.name,
|
|
34
|
+
...(arch.resetAction ? { resetAction: arch.resetAction } : {}),
|
|
35
|
+
nodes: mergedNodes,
|
|
36
|
+
connectors: mergedConnectors,
|
|
37
|
+
} as Flow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fields that live in a node's `data` block on architecture.json. Every other
|
|
41
|
+
// data field is visual and routes to style.json.
|
|
42
|
+
const NODE_DATA_ARCH_KEYS = new Set([
|
|
43
|
+
'name',
|
|
44
|
+
'kind',
|
|
45
|
+
'stateSource',
|
|
46
|
+
'handlerModule',
|
|
47
|
+
'icon',
|
|
48
|
+
'description',
|
|
49
|
+
'detail',
|
|
50
|
+
'playAction',
|
|
51
|
+
'statusAction',
|
|
52
|
+
'shape',
|
|
53
|
+
'path',
|
|
54
|
+
'alt',
|
|
55
|
+
'htmlPath',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const NODE_STYLE_KEYS = new Set([
|
|
59
|
+
'width',
|
|
60
|
+
'height',
|
|
61
|
+
'borderColor',
|
|
62
|
+
'backgroundColor',
|
|
63
|
+
'borderSize',
|
|
64
|
+
'borderStyle',
|
|
65
|
+
'fontSize',
|
|
66
|
+
'textColor',
|
|
67
|
+
'cornerRadius',
|
|
68
|
+
'locked',
|
|
69
|
+
'borderWidth',
|
|
70
|
+
'color',
|
|
71
|
+
'strokeWidth',
|
|
72
|
+
'autoSize',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const CONNECTOR_ARCH_KEYS = new Set([
|
|
76
|
+
'id',
|
|
77
|
+
'source',
|
|
78
|
+
'target',
|
|
79
|
+
'kind',
|
|
80
|
+
'label',
|
|
81
|
+
'method',
|
|
82
|
+
'url',
|
|
83
|
+
'eventName',
|
|
84
|
+
'queueName',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const CONNECTOR_STYLE_KEYS = new Set([
|
|
88
|
+
'sourceHandle',
|
|
89
|
+
'targetHandle',
|
|
90
|
+
'sourceHandleAutoPicked',
|
|
91
|
+
'targetHandleAutoPicked',
|
|
92
|
+
'sourcePin',
|
|
93
|
+
'targetPin',
|
|
94
|
+
'style',
|
|
95
|
+
'color',
|
|
96
|
+
'direction',
|
|
97
|
+
'borderSize',
|
|
98
|
+
'path',
|
|
99
|
+
'fontSize',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Split a merged Flow back into (architecture, style) for atomic write. The
|
|
104
|
+
* inverse of mergeArchitectureAndStyle: position and every visual field on
|
|
105
|
+
* each node moves to `style.nodes[id]`; handles, pins, and visual fields on
|
|
106
|
+
* each connector move to `style.connectors[id]`. Architecture keeps every
|
|
107
|
+
* semantic data field — the routing tables above are the source of truth.
|
|
108
|
+
*
|
|
109
|
+
* Style entries that end up empty are omitted from the output so the file
|
|
110
|
+
* stays compact (matches the design's "delete style.json when {}" rule).
|
|
111
|
+
*/
|
|
112
|
+
export function splitFlow(flow: {
|
|
113
|
+
version: number;
|
|
114
|
+
name: string;
|
|
115
|
+
resetAction?: unknown;
|
|
116
|
+
nodes: Array<Record<string, unknown>>;
|
|
117
|
+
connectors: Array<Record<string, unknown>>;
|
|
118
|
+
}): { architecture: Record<string, unknown>; style: Record<string, unknown> } {
|
|
119
|
+
const archNodes: Array<Record<string, unknown>> = [];
|
|
120
|
+
const styleNodes: Record<string, Record<string, unknown>> = {};
|
|
121
|
+
|
|
122
|
+
for (const node of flow.nodes) {
|
|
123
|
+
const id = node.id as string;
|
|
124
|
+
const archNode: Record<string, unknown> = { id, type: node.type };
|
|
125
|
+
const styleEntry: Record<string, unknown> = {};
|
|
126
|
+
|
|
127
|
+
if (node.position && typeof node.position === 'object') {
|
|
128
|
+
styleEntry.position = node.position;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = (node.data ?? {}) as Record<string, unknown>;
|
|
132
|
+
const archData: Record<string, unknown> = {};
|
|
133
|
+
for (const [k, v] of Object.entries(data)) {
|
|
134
|
+
if (v === undefined) continue;
|
|
135
|
+
if (NODE_DATA_ARCH_KEYS.has(k)) {
|
|
136
|
+
archData[k] = v;
|
|
137
|
+
} else if (NODE_STYLE_KEYS.has(k)) {
|
|
138
|
+
styleEntry[k] = v;
|
|
139
|
+
} else {
|
|
140
|
+
// Unknown forward-compat key — keep on architecture side so the
|
|
141
|
+
// schema's strict() will catch typos but extension is possible by
|
|
142
|
+
// updating the routing tables here.
|
|
143
|
+
archData[k] = v;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
archNode.data = archData;
|
|
147
|
+
archNodes.push(archNode);
|
|
148
|
+
|
|
149
|
+
if (Object.keys(styleEntry).length > 0) {
|
|
150
|
+
styleNodes[id] = styleEntry;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const archConnectors: Array<Record<string, unknown>> = [];
|
|
155
|
+
const styleConnectors: Record<string, Record<string, unknown>> = {};
|
|
156
|
+
|
|
157
|
+
for (const conn of flow.connectors) {
|
|
158
|
+
const id = conn.id as string;
|
|
159
|
+
const archConn: Record<string, unknown> = {};
|
|
160
|
+
const styleEntry: Record<string, unknown> = {};
|
|
161
|
+
for (const [k, v] of Object.entries(conn)) {
|
|
162
|
+
if (v === undefined) continue;
|
|
163
|
+
if (CONNECTOR_ARCH_KEYS.has(k)) {
|
|
164
|
+
archConn[k] = v;
|
|
165
|
+
} else if (CONNECTOR_STYLE_KEYS.has(k)) {
|
|
166
|
+
styleEntry[k] = v;
|
|
167
|
+
} else {
|
|
168
|
+
archConn[k] = v;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
archConnectors.push(archConn);
|
|
172
|
+
if (Object.keys(styleEntry).length > 0) {
|
|
173
|
+
styleConnectors[id] = styleEntry;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const architecture: Record<string, unknown> = {
|
|
178
|
+
version: flow.version,
|
|
179
|
+
name: flow.name,
|
|
180
|
+
nodes: archNodes,
|
|
181
|
+
connectors: archConnectors,
|
|
182
|
+
};
|
|
183
|
+
if (flow.resetAction !== undefined) architecture.resetAction = flow.resetAction;
|
|
184
|
+
|
|
185
|
+
const style: Record<string, unknown> = {};
|
|
186
|
+
if (Object.keys(styleNodes).length > 0) style.nodes = styleNodes;
|
|
187
|
+
if (Object.keys(styleConnectors).length > 0) style.connectors = styleConnectors;
|
|
188
|
+
|
|
189
|
+
return { architecture, style };
|
|
190
|
+
}
|