@tuongaz/seeflow 0.1.77 → 0.1.81
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 +40 -0
- package/dist/web/assets/{architectureDiagram-3BPJPVTR-D5iHwVvy.js → architectureDiagram-3BPJPVTR-BfABufg7.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-MAYYm7FM.js → blockDiagram-GPEHLZMM-DEsJzjMp.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-7P7yfHg1.js → c4Diagram-AAUBKEIU-BI2bLAFy.js} +1 -1
- package/dist/web/assets/channel-C4GDf2FG.js +1 -0
- package/dist/web/assets/{chart-C68vupBE.js → chart-BbL2c3JQ.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-Bb4cSusI.js → chunk-2J33WTMH-Cgomd5cT.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-DXYpcpTh.js → chunk-4BX2VUAB-D7PhkaW3.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-BxuYKDnf.js → chunk-55IACEB6-CUFJz27b.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-DbWlxAr2.js → chunk-727SXJPM-DuhsbPQs.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-DT8S1q80.js → chunk-AQP2D5EJ-1wXXetBT.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Dc0wDuZz.js → chunk-FMBD7UC4-D6ESudu6.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-CqLLK6H0.js → chunk-ND2GUHAM-Dq8nvkVX.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-CxF7nkDI.js → chunk-QZHKN3VN-2m-ARThf.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-DxP4Rh0o.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DxP4Rh0o.js +1 -0
- package/dist/web/assets/{code-block-DR9fiK_U.js → code-block-DTN7FEQj.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-BflFbtY2.js → cose-bilkent-S5V4N54A-DjzT5K-0.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-BJ5UdyYS.js → dagre-BM42HDAG-BwzsmtSu.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-D0M8fCf7.js → diagram-2AECGRRQ-FYQyXaT_.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-D67gAMS4.js → diagram-5GNKFQAL-DAzyREi8.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-XX62HBG-.js → diagram-KO2AKTUF-DGKrMcjo.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-DCFq3Oac.js → diagram-LMA3HP47-Bff209n5.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-Be392NCN.js → diagram-OG6HWLK6-BuqayNQm.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-DP4eP0as.js → erDiagram-TEJ5UH35-BUjs8hUi.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-Ch1GVJ9R.js → flowDiagram-I6XJVG4X-B_D_x2FB.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-DtvkTizu.js → ganttDiagram-6RSMTGT7-DxfI1q3k.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-YGcuBgb9.js → gitGraphDiagram-PVQCEYII-BIEMbeDK.js} +1 -1
- package/dist/web/assets/index-DRyzDJUb.js +8619 -0
- package/dist/web/assets/{index-DljfurDC.css → index-fl8DS9WO.css} +1 -1
- package/dist/web/assets/{index.es-jrsJPbYZ.js → index.es-CiKqWLXZ.js} +1 -1
- package/dist/web/assets/{infoDiagram-5YYISTIA-wce0BORz.js → infoDiagram-5YYISTIA-DdStjmcL.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-u2MvPgdW.js → ishikawaDiagram-YF4QCWOH-1YlL8XhA.js} +1 -1
- package/dist/web/assets/{journeyDiagram-JHISSGLW-BsOyrTiA.js → journeyDiagram-JHISSGLW-4UmpNnmE.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-ptMERvnN.js → jspdf.es.min-CfPjY_mb.js} +3 -3
- package/dist/web/assets/{kanban-definition-UN3LZRKU-BaraYV9q.js → kanban-definition-UN3LZRKU-BpHur4yM.js} +1 -1
- package/dist/web/assets/{linear-BVqXcDUJ.js → linear-BlKd__8P.js} +1 -1
- package/dist/web/assets/{markdown-DqP0Cywq.js → markdown-B0s014Jt.js} +1 -1
- package/dist/web/assets/{mermaid.core-CakR_vo1.js → mermaid.core-C1P8oPqR.js} +4 -4
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-CO5AsZw3.js → mindmap-definition-RKZ34NQL-CbYEKJD0.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-CiDJY-kx.js → pieDiagram-4H26LBE5-DTM5C3b_.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-BS6oN3s_.js → quadrantDiagram-W4KKPZXB-BTLv_k9D.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-CNbUR_FF.js → requirementDiagram-4Y6WPE33-xrt0GAFn.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-0Esj5uzm.js → sankeyDiagram-5OEKKPKP-CTC7QJzV.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-DR3U38Zi.js → sequenceDiagram-3UESZ5HK-DXxHxv0c.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-C50RQjWe.js → stateDiagram-AJRCARHV-Cq8S_qAw.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BI8aEZqM.js +1 -0
- package/dist/web/assets/{time-C_2J9tFX.js → time-BWlQ2zTb.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-BQXyo2r_.js → timeline-definition-PNZ67QCA-ooIJpgWs.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-DZJ8M3EA.js → vennDiagram-CIIHVFJN-ClAQa-gM.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-B96HtW3i.js → wardley-L42UT6IY-UjncjFg7.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BHkQ79WC.js → wardleyDiagram-YWT4CUSO-BQkPAPTU.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-B_f8koGI.js → xychartDiagram-2RQKCTM6-CO6j1v0J.js} +1 -1
- package/dist/web/index.html +2 -2
- package/examples/component-showcase/seeflow.json +6 -0
- package/examples/ecommerce-platform/seeflow.json +6 -0
- package/examples/order-pipeline/seeflow.json +6 -0
- package/package.json +1 -1
- package/src/api.ts +739 -94
- package/src/cli-e2e.ts +24 -13
- package/src/cli-helpers.ts +26 -0
- package/src/cli-manifest.ts +330 -87
- package/src/cli-ops.ts +56 -2
- package/src/cli.ts +228 -81
- package/src/cors.ts +93 -0
- package/src/jq-filter.ts +253 -0
- package/src/mcp-shim.ts +114 -7
- package/src/mcp-ui.ts +126 -0
- package/src/mcp.ts +258 -97
- package/src/node-files.ts +18 -7
- package/src/operations.ts +68 -32
- package/src/project-scanner.ts +105 -0
- package/src/registry.ts +79 -18
- package/src/route-resolve.ts +41 -0
- package/src/schema.ts +54 -0
- package/src/server.ts +24 -3
- package/src/slugify.ts +16 -0
- package/dist/web/assets/channel-BjsQQK93.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-p3FY5uNC.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-p3FY5uNC.js +0 -1
- package/dist/web/assets/index-Bg3PU4Ev.js +0 -8614
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BbNrmkIR.js +0 -1
- /package/examples/component-showcase/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/chart/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/counter/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/actions/refresh.ts +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/form/spec.json +0 -0
- /package/examples/component-showcase/{style.json → flows/main/style.json} +0 -0
- /package/examples/ecommerce-platform/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-3zFtHg6ENc/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-5F424NWbEu/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-CbwYqb7NfB/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-XwygzfKPZ5/view.html +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-fkptXw7uvs/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-kwBY8YPmYM/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-mPqan8rFYN/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-yKrg9DV5fJ/detail.md +0 -0
- /package/examples/ecommerce-platform/{scripts → flows/main/scripts}/play.ts +0 -0
- /package/examples/ecommerce-platform/{style.json → flows/main/style.json} +0 -0
- /package/examples/order-pipeline/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-GXTKUcE3ye/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-XKIyds0TDg/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-YOYiHJpY0i/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-zUIH7WFnhK/detail.md +0 -0
- /package/examples/order-pipeline/{scripts → flows/main/scripts}/play.ts +0 -0
- /package/examples/order-pipeline/{style.json → flows/main/style.json} +0 -0
package/src/mcp.ts
CHANGED
|
@@ -2,10 +2,19 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import {
|
|
3
3
|
CallToolRequestSchema,
|
|
4
4
|
type CallToolResult,
|
|
5
|
+
ListResourcesRequestSchema,
|
|
5
6
|
ListToolsRequestSchema,
|
|
7
|
+
ReadResourceRequestSchema,
|
|
6
8
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
9
|
import { type ZodTypeAny, z } from 'zod';
|
|
8
10
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
11
|
+
import {
|
|
12
|
+
CANVAS_RESOURCE_MIME,
|
|
13
|
+
CANVAS_RESOURCE_URI,
|
|
14
|
+
type CanvasWidgetState,
|
|
15
|
+
canvasMeta,
|
|
16
|
+
readCanvasHtml,
|
|
17
|
+
} from './mcp-ui.ts';
|
|
9
18
|
import {
|
|
10
19
|
ConnectorPatchBodySchema,
|
|
11
20
|
CreateProjectBodySchema,
|
|
@@ -27,8 +36,48 @@ import type { FlowWatcher } from './watcher.ts';
|
|
|
27
36
|
export interface CreateMcpServerOptions {
|
|
28
37
|
registry: Registry;
|
|
29
38
|
watcher?: FlowWatcher;
|
|
39
|
+
/** Per-process token forwarded to the MCP App iframe via
|
|
40
|
+
* `_meta['openai/widgetState'].backendToken` so cross-origin requests
|
|
41
|
+
* from the sandboxed (`Origin: null`) iframe can carry it as
|
|
42
|
+
* `X-Seeflow-Token`. Same value as the one passed to
|
|
43
|
+
* `createApp({ token })`. Wired into canvas-bearing tool handlers; non-
|
|
44
|
+
* canvas tools ignore it. */
|
|
45
|
+
token?: string;
|
|
46
|
+
/** Reachable loopback URL of the studio HTTP backend (e.g.
|
|
47
|
+
* `http://127.0.0.1:54321`). The MCP App iframe uses it as the REST
|
|
48
|
+
* base URL to load flow data. When unset, canvas-bearing tools omit
|
|
49
|
+
* `_meta` entirely so non-Apps hosts still work (the iframe can't
|
|
50
|
+
* reach a backend without both `httpUrl` and `token`). */
|
|
51
|
+
httpUrl?: string;
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
/** Subset of `CreateMcpServerOptions` that the canvas-bearing tool
|
|
55
|
+
* handlers need at call time. Built once in `createMcpServer` and
|
|
56
|
+
* passed into `buildTools` so the closures inside each handler can
|
|
57
|
+
* attach `_meta` without re-reading the outer options. */
|
|
58
|
+
interface ToolContext {
|
|
59
|
+
registry: Registry;
|
|
60
|
+
token?: string;
|
|
61
|
+
httpUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Distributive Omit so the discriminated union arms stay distinct after
|
|
65
|
+
// stripping the host-only `backendUrl` / `backendToken` keys.
|
|
66
|
+
type DistOmit<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
|
|
67
|
+
type CanvasWidgetStateInput = DistOmit<CanvasWidgetState, 'backendUrl' | 'backendToken'>;
|
|
68
|
+
|
|
69
|
+
/** Build the `_meta` block for a canvas-bearing tool result, when both
|
|
70
|
+
* `httpUrl` and `token` are configured. Returns `undefined` if either
|
|
71
|
+
* is missing (e.g. proxy mode in `mcp-shim.ts`, or tests that bypass
|
|
72
|
+
* the shim) so non-Apps callers still get a plain JSON-only result. */
|
|
73
|
+
const canvasMetaFor = (
|
|
74
|
+
ctx: ToolContext,
|
|
75
|
+
state: CanvasWidgetStateInput,
|
|
76
|
+
): Record<string, unknown> | undefined => {
|
|
77
|
+
if (!ctx.httpUrl || !ctx.token) return undefined;
|
|
78
|
+
return canvasMeta({ ...state, backendUrl: ctx.httpUrl, backendToken: ctx.token });
|
|
79
|
+
};
|
|
80
|
+
|
|
32
81
|
// Tools are pushed into this in-memory list inside `createMcpServer`. Each
|
|
33
82
|
// tool has a tiny one-sentence description, a JSON Schema for its input
|
|
34
83
|
// (built from existing Zod schemas via zod-to-json-schema where reuse is
|
|
@@ -56,9 +105,13 @@ const inputSchemaFromZod = (schema: ZodTypeAny): Record<string, unknown> => {
|
|
|
56
105
|
return rest.type === 'object' ? rest : { type: 'object', ...rest };
|
|
57
106
|
};
|
|
58
107
|
|
|
59
|
-
const okResult = (value: unknown): CallToolResult =>
|
|
60
|
-
|
|
61
|
-
}
|
|
108
|
+
const okResult = (value: unknown, meta?: Record<string, unknown>): CallToolResult => {
|
|
109
|
+
const result: CallToolResult = {
|
|
110
|
+
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
111
|
+
};
|
|
112
|
+
if (meta) result._meta = meta;
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
62
115
|
|
|
63
116
|
// Error payloads (e.g. 'unknown demo', 'Failed to write demo file') still say
|
|
64
117
|
// "demo" so the strings match the REST handlers in api.ts byte-for-byte.
|
|
@@ -69,55 +122,84 @@ const errorResult = (text: string): CallToolResult => ({
|
|
|
69
122
|
content: [{ type: 'text', text }],
|
|
70
123
|
});
|
|
71
124
|
|
|
72
|
-
// Most MCP tools take
|
|
73
|
-
// JSON Schema (rather than a one-off Zod
|
|
74
|
-
// counterpart to share with.
|
|
75
|
-
const
|
|
125
|
+
// Most flow-scoped MCP tools take { project, flow } to address a registered
|
|
126
|
+
// flow. Defined inline as plain JSON Schema (rather than a one-off Zod
|
|
127
|
+
// schema) because there's no REST counterpart to share with.
|
|
128
|
+
const PROJECT_FLOW_PROPERTIES = {
|
|
129
|
+
project: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
minLength: 1,
|
|
132
|
+
description: 'Project slug (e.g. `order-pipeline`) addressing a registered project.',
|
|
133
|
+
},
|
|
134
|
+
flow: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
minLength: 1,
|
|
137
|
+
description: 'Flow slug within that project (e.g. `main` or `retry`).',
|
|
138
|
+
},
|
|
139
|
+
} as const;
|
|
140
|
+
|
|
141
|
+
const FLOW_PROJECT_INPUT_SCHEMA = {
|
|
76
142
|
type: 'object',
|
|
77
|
-
properties: {
|
|
78
|
-
required: ['
|
|
143
|
+
properties: { ...PROJECT_FLOW_PROPERTIES },
|
|
144
|
+
required: ['project', 'flow'],
|
|
79
145
|
additionalProperties: false,
|
|
80
146
|
} as const;
|
|
81
147
|
|
|
82
|
-
const
|
|
148
|
+
const requireProjectFlow = (
|
|
149
|
+
args: unknown,
|
|
150
|
+
): { project: string; flow: string } | { error: string } => {
|
|
83
151
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
|
-
return { error: 'Invalid arguments: expected an object with
|
|
152
|
+
return { error: 'Invalid arguments: expected an object with project + flow' };
|
|
85
153
|
}
|
|
86
|
-
const {
|
|
87
|
-
if (typeof
|
|
88
|
-
return { error: 'Invalid arguments:
|
|
154
|
+
const { project, flow } = args as { project?: unknown; flow?: unknown };
|
|
155
|
+
if (typeof project !== 'string' || project.length === 0) {
|
|
156
|
+
return { error: 'Invalid arguments: project must be a non-empty string' };
|
|
89
157
|
}
|
|
90
|
-
|
|
158
|
+
if (typeof flow !== 'string' || flow.length === 0) {
|
|
159
|
+
return { error: 'Invalid arguments: flow must be a non-empty string' };
|
|
160
|
+
}
|
|
161
|
+
return { project, flow };
|
|
91
162
|
};
|
|
92
163
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
164
|
+
// Project + flow slug pair used by every flow-scoped tool input as a Zod
|
|
165
|
+
// envelope. Each field carries its own description so the JSON Schema
|
|
166
|
+
// `tools/list` surfaces is self-documenting.
|
|
167
|
+
const ProjectFlowSchema = z.object({
|
|
168
|
+
project: z
|
|
169
|
+
.string()
|
|
170
|
+
.min(1)
|
|
171
|
+
.describe('Project slug (e.g. `order-pipeline`) addressing a registered project.'),
|
|
172
|
+
flow: z.string().min(1).describe('Flow slug within that project (e.g. `main` or `retry`).'),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// {project, flow, nodeId} body shape shared by move + reorder + delete inputs.
|
|
176
|
+
const FlowNodeIdBaseSchema = ProjectFlowSchema.extend({
|
|
96
177
|
nodeId: z.string().min(1),
|
|
97
178
|
});
|
|
98
179
|
|
|
99
|
-
// add_node input: {
|
|
100
|
-
// loose here (additionalProperties=true via passthrough) because
|
|
101
|
-
// runs the full validation server-side after the new
|
|
102
|
-
|
|
103
|
-
|
|
180
|
+
// add_node input: { project, flow, node: <node payload> }. The inner `node`
|
|
181
|
+
// object is loose here (additionalProperties=true via passthrough) because
|
|
182
|
+
// ResolvedFlowSchema runs the full validation server-side after the new
|
|
183
|
+
// node is merged in.
|
|
184
|
+
const AddNodeInputSchema = ProjectFlowSchema.extend({
|
|
104
185
|
node: z.record(z.unknown()),
|
|
105
186
|
});
|
|
106
187
|
|
|
107
|
-
// add_bulk input: {
|
|
108
|
-
// per-item shape as add_node / add_connector — ResolvedFlowSchema runs
|
|
109
|
-
// over the whole merged graph server-side after the batch lands. The
|
|
188
|
+
// add_bulk input: { project, flow, nodes?: [...], connectors?: [...] }. Same
|
|
189
|
+
// loose per-item shape as add_node / add_connector — ResolvedFlowSchema runs
|
|
190
|
+
// once over the whole merged graph server-side after the batch lands. The
|
|
110
191
|
// 100-per-kind cap and "at least one non-empty" invariant come from
|
|
111
192
|
// FlowBulkBodyShape + flowBulkNonEmpty (the unrefined object + reusable
|
|
112
193
|
// predicate exported by operations.ts) so the JSON Schema the agent
|
|
113
194
|
// introspects stays a clean object — not an intersection.
|
|
114
195
|
const AddBulkInputSchema = FlowBulkBodyShape.extend({
|
|
115
|
-
|
|
196
|
+
project: ProjectFlowSchema.shape.project,
|
|
197
|
+
flow: ProjectFlowSchema.shape.flow,
|
|
116
198
|
}).refine(flowBulkNonEmpty, { message: FLOW_BULK_NON_EMPTY_MESSAGE });
|
|
117
199
|
|
|
118
200
|
const DeleteNodeInputSchema = FlowNodeIdBaseSchema;
|
|
119
201
|
|
|
120
|
-
// move_node input: {
|
|
202
|
+
// move_node input: { project, flow, nodeId } extended with PositionBodySchema's
|
|
121
203
|
// { x, y } fields so agents see one flat schema.
|
|
122
204
|
const MoveNodeInputSchema = FlowNodeIdBaseSchema.extend({
|
|
123
205
|
x: PositionBodySchema.shape.x,
|
|
@@ -125,8 +207,9 @@ const MoveNodeInputSchema = FlowNodeIdBaseSchema.extend({
|
|
|
125
207
|
});
|
|
126
208
|
|
|
127
209
|
// reorder_node input: each branch of the existing ReorderBodySchema
|
|
128
|
-
// discriminated union extended with
|
|
129
|
-
// on `op` so the emitted JSON Schema is an oneOf the agent
|
|
210
|
+
// discriminated union extended with project/flow/nodeId. Keeps the
|
|
211
|
+
// discriminator on `op` so the emitted JSON Schema is an oneOf the agent
|
|
212
|
+
// can introspect.
|
|
130
213
|
const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
131
214
|
FlowNodeIdBaseSchema.extend({ op: z.literal('forward') }),
|
|
132
215
|
FlowNodeIdBaseSchema.extend({ op: z.literal('backward') }),
|
|
@@ -138,40 +221,40 @@ const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
|
138
221
|
}),
|
|
139
222
|
]);
|
|
140
223
|
|
|
141
|
-
// patch_node input: {
|
|
224
|
+
// patch_node input: { project, flow, nodeId } merged with NodePatchBodySchema's
|
|
142
225
|
// optional fields. .extend() on the strict body schema preserves strict
|
|
143
226
|
// mode, so unknown top-level keys still trip the Zod parse before any disk
|
|
144
227
|
// IO — matching the REST handler's "Invalid node patch body" 400 path.
|
|
145
228
|
const PatchNodeInputSchema = NodePatchBodySchema.extend({
|
|
146
|
-
|
|
229
|
+
project: ProjectFlowSchema.shape.project,
|
|
230
|
+
flow: ProjectFlowSchema.shape.flow,
|
|
147
231
|
nodeId: z.string().min(1),
|
|
148
232
|
});
|
|
149
233
|
|
|
150
|
-
// add_connector input: {
|
|
151
|
-
// `connector` object is loose (additionalProperties=true via
|
|
152
|
-
// ResolvedFlowSchema runs the full validation server-side
|
|
153
|
-
// merged in (post-mutation parse catches dangling
|
|
154
|
-
// kind-discriminator violations).
|
|
155
|
-
const AddConnectorInputSchema =
|
|
156
|
-
flowId: z.string().min(1),
|
|
234
|
+
// add_connector input: { project, flow, connector: <connector payload> }.
|
|
235
|
+
// The inner `connector` object is loose (additionalProperties=true via
|
|
236
|
+
// z.record) because ResolvedFlowSchema runs the full validation server-side
|
|
237
|
+
// after the new connector is merged in (post-mutation parse catches dangling
|
|
238
|
+
// source/target refs and kind-discriminator violations).
|
|
239
|
+
const AddConnectorInputSchema = ProjectFlowSchema.extend({
|
|
157
240
|
connector: z.record(z.unknown()),
|
|
158
241
|
});
|
|
159
242
|
|
|
160
|
-
// patch_connector input: {
|
|
161
|
-
// ConnectorPatchBodySchema. .extend() preserves strict mode so
|
|
162
|
-
// top-level keys trip the Zod parse before any IO — matching the
|
|
163
|
-
// handler's "Invalid connector patch body" 400 path.
|
|
243
|
+
// patch_connector input: { project, flow, connectorId } merged with the
|
|
244
|
+
// strict ConnectorPatchBodySchema. .extend() preserves strict mode so
|
|
245
|
+
// unknown top-level keys trip the Zod parse before any IO — matching the
|
|
246
|
+
// REST handler's "Invalid connector patch body" 400 path.
|
|
164
247
|
const PatchConnectorInputSchema = ConnectorPatchBodySchema.extend({
|
|
165
|
-
|
|
248
|
+
project: ProjectFlowSchema.shape.project,
|
|
249
|
+
flow: ProjectFlowSchema.shape.flow,
|
|
166
250
|
connectorId: z.string().min(1),
|
|
167
251
|
});
|
|
168
252
|
|
|
169
|
-
const DeleteConnectorInputSchema =
|
|
170
|
-
flowId: z.string().min(1),
|
|
253
|
+
const DeleteConnectorInputSchema = ProjectFlowSchema.extend({
|
|
171
254
|
connectorId: z.string().min(1),
|
|
172
255
|
});
|
|
173
256
|
|
|
174
|
-
const buildTools = (ops: Operations): McpTool[] => [
|
|
257
|
+
const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
|
|
175
258
|
{
|
|
176
259
|
name: 'seeflow_list_flows',
|
|
177
260
|
description: 'List every flow registered with the studio.',
|
|
@@ -314,15 +397,25 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
314
397
|
},
|
|
315
398
|
{
|
|
316
399
|
name: 'seeflow_get_flow',
|
|
317
|
-
description: 'Get the full flow definition and on-disk state for a
|
|
318
|
-
inputSchema:
|
|
400
|
+
description: 'Get the full flow definition and on-disk state for a (project, flow) pair.',
|
|
401
|
+
inputSchema: FLOW_PROJECT_INPUT_SCHEMA,
|
|
319
402
|
handler: async (args) => {
|
|
320
|
-
const v =
|
|
403
|
+
const v = requireProjectFlow(args);
|
|
321
404
|
if ('error' in v) return errorResult(v.error);
|
|
322
|
-
const
|
|
405
|
+
const flowSlug = `${v.project}/${v.flow}`;
|
|
406
|
+
const result = await ops.getFlow(flowSlug);
|
|
323
407
|
switch (result.kind) {
|
|
324
|
-
case 'ok':
|
|
325
|
-
|
|
408
|
+
case 'ok': {
|
|
409
|
+
const entry = ctx.registry.resolve(flowSlug);
|
|
410
|
+
const meta = entry
|
|
411
|
+
? canvasMetaFor(ctx, {
|
|
412
|
+
kind: 'navigate',
|
|
413
|
+
projectSlug: entry.projectSlug,
|
|
414
|
+
flowSlug: entry.flowSlug,
|
|
415
|
+
})
|
|
416
|
+
: undefined;
|
|
417
|
+
return okResult(result.data, meta);
|
|
418
|
+
}
|
|
326
419
|
case 'notFound':
|
|
327
420
|
return errorResult('not found');
|
|
328
421
|
case 'fileNotFound':
|
|
@@ -336,14 +429,24 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
336
429
|
"Get a flow's nodes + connectors without inlining per-node file-backed " +
|
|
337
430
|
'content (`detail`, `html`). Cheap topology read — pair with seeflow_get_node ' +
|
|
338
431
|
"when you need a specific node's long-form body.",
|
|
339
|
-
inputSchema:
|
|
432
|
+
inputSchema: FLOW_PROJECT_INPUT_SCHEMA,
|
|
340
433
|
handler: async (args) => {
|
|
341
|
-
const v =
|
|
434
|
+
const v = requireProjectFlow(args);
|
|
342
435
|
if ('error' in v) return errorResult(v.error);
|
|
343
|
-
const
|
|
436
|
+
const flowSlug = `${v.project}/${v.flow}`;
|
|
437
|
+
const result = await ops.getFlowGraph(flowSlug);
|
|
344
438
|
switch (result.kind) {
|
|
345
|
-
case 'ok':
|
|
346
|
-
|
|
439
|
+
case 'ok': {
|
|
440
|
+
const entry = ctx.registry.resolve(flowSlug);
|
|
441
|
+
const meta = entry
|
|
442
|
+
? canvasMetaFor(ctx, {
|
|
443
|
+
kind: 'navigate',
|
|
444
|
+
projectSlug: entry.projectSlug,
|
|
445
|
+
flowSlug: entry.flowSlug,
|
|
446
|
+
})
|
|
447
|
+
: undefined;
|
|
448
|
+
return okResult(result.data, meta);
|
|
449
|
+
}
|
|
347
450
|
case 'notFound':
|
|
348
451
|
return errorResult('not found');
|
|
349
452
|
case 'fileNotFound':
|
|
@@ -365,27 +468,34 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
365
468
|
inputSchema: {
|
|
366
469
|
type: 'object',
|
|
367
470
|
properties: {
|
|
368
|
-
|
|
471
|
+
...PROJECT_FLOW_PROPERTIES,
|
|
369
472
|
nodeId: { type: 'string', minLength: 1 },
|
|
370
473
|
},
|
|
371
|
-
required: ['
|
|
474
|
+
required: ['project', 'flow', 'nodeId'],
|
|
372
475
|
additionalProperties: false,
|
|
373
476
|
},
|
|
374
477
|
handler: async (args) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
const { flowId, nodeId } = args as { flowId?: unknown; nodeId?: unknown };
|
|
379
|
-
if (typeof flowId !== 'string' || flowId.length === 0) {
|
|
380
|
-
return errorResult('Invalid arguments: flowId must be a non-empty string');
|
|
381
|
-
}
|
|
478
|
+
const v = requireProjectFlow(args);
|
|
479
|
+
if ('error' in v) return errorResult(v.error);
|
|
480
|
+
const { nodeId } = (args as { nodeId?: unknown }) ?? {};
|
|
382
481
|
if (typeof nodeId !== 'string' || nodeId.length === 0) {
|
|
383
482
|
return errorResult('Invalid arguments: nodeId must be a non-empty string');
|
|
384
483
|
}
|
|
385
|
-
const
|
|
484
|
+
const flowSlug = `${v.project}/${v.flow}`;
|
|
485
|
+
const result = await ops.getNode(flowSlug, nodeId);
|
|
386
486
|
switch (result.kind) {
|
|
387
|
-
case 'ok':
|
|
388
|
-
|
|
487
|
+
case 'ok': {
|
|
488
|
+
const entry = ctx.registry.resolve(flowSlug);
|
|
489
|
+
const meta = entry
|
|
490
|
+
? canvasMetaFor(ctx, {
|
|
491
|
+
kind: 'navigate',
|
|
492
|
+
projectSlug: entry.projectSlug,
|
|
493
|
+
flowSlug: entry.flowSlug,
|
|
494
|
+
nodeId,
|
|
495
|
+
})
|
|
496
|
+
: undefined;
|
|
497
|
+
return okResult(result.data, meta);
|
|
498
|
+
}
|
|
389
499
|
case 'notFound':
|
|
390
500
|
return errorResult('not found');
|
|
391
501
|
case 'fileNotFound':
|
|
@@ -412,8 +522,18 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
412
522
|
}
|
|
413
523
|
const result = await ops.registerFlow(parsed.data);
|
|
414
524
|
switch (result.kind) {
|
|
415
|
-
case 'ok':
|
|
416
|
-
|
|
525
|
+
case 'ok': {
|
|
526
|
+
const entry = ctx.registry.resolve(result.data.id);
|
|
527
|
+
const meta = entry
|
|
528
|
+
? canvasMetaFor(ctx, {
|
|
529
|
+
kind: 'create',
|
|
530
|
+
projectSlug: entry.projectSlug,
|
|
531
|
+
flowSlug: entry.flowSlug,
|
|
532
|
+
justCreated: true,
|
|
533
|
+
})
|
|
534
|
+
: undefined;
|
|
535
|
+
return okResult(result.data, meta);
|
|
536
|
+
}
|
|
417
537
|
case 'fileNotFound':
|
|
418
538
|
return errorResult(`Flow file not found: ${result.path}`);
|
|
419
539
|
case 'badJson':
|
|
@@ -428,11 +548,11 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
428
548
|
{
|
|
429
549
|
name: 'seeflow_delete_flow',
|
|
430
550
|
description: 'Unregister a flow from the studio (the on-disk file is left untouched).',
|
|
431
|
-
inputSchema:
|
|
551
|
+
inputSchema: FLOW_PROJECT_INPUT_SCHEMA,
|
|
432
552
|
handler: async (args) => {
|
|
433
|
-
const v =
|
|
553
|
+
const v = requireProjectFlow(args);
|
|
434
554
|
if ('error' in v) return errorResult(v.error);
|
|
435
|
-
const result = ops.deleteFlow(v.
|
|
555
|
+
const result = ops.deleteFlow(`${v.project}/${v.flow}`);
|
|
436
556
|
switch (result.kind) {
|
|
437
557
|
case 'ok':
|
|
438
558
|
return okResult({ ok: true });
|
|
@@ -453,8 +573,13 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
453
573
|
}
|
|
454
574
|
const result = await ops.createProject(parsed.data);
|
|
455
575
|
switch (result.kind) {
|
|
456
|
-
case 'ok':
|
|
457
|
-
|
|
576
|
+
case 'ok': {
|
|
577
|
+
const meta = canvasMetaFor(ctx, {
|
|
578
|
+
kind: 'create',
|
|
579
|
+
projectSlug: result.data.slug,
|
|
580
|
+
});
|
|
581
|
+
return okResult(result.data, meta);
|
|
582
|
+
}
|
|
458
583
|
case 'alreadyExists':
|
|
459
584
|
return errorResult(`Project already exists at ${result.path}`);
|
|
460
585
|
case 'scaffoldFailed':
|
|
@@ -472,8 +597,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
472
597
|
if (!parsed.success) {
|
|
473
598
|
return errorResult(`Invalid add_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
474
599
|
}
|
|
475
|
-
const {
|
|
476
|
-
const result = await ops.addNode(
|
|
600
|
+
const { project, flow, node } = parsed.data;
|
|
601
|
+
const result = await ops.addNode(`${project}/${flow}`, node);
|
|
477
602
|
switch (result.kind) {
|
|
478
603
|
case 'ok':
|
|
479
604
|
return okResult({ ok: true, id: result.data.id, node: result.data.node });
|
|
@@ -500,8 +625,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
500
625
|
if (!parsed.success) {
|
|
501
626
|
return errorResult(`Invalid add_bulk arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
502
627
|
}
|
|
503
|
-
const {
|
|
504
|
-
const result = await ops.addBulk(
|
|
628
|
+
const { project, flow, nodes, connectors } = parsed.data;
|
|
629
|
+
const result = await ops.addBulk(`${project}/${flow}`, { nodes, connectors });
|
|
505
630
|
switch (result.kind) {
|
|
506
631
|
case 'ok':
|
|
507
632
|
return okResult({
|
|
@@ -537,8 +662,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
537
662
|
if (!parsed.success) {
|
|
538
663
|
return errorResult(`Invalid delete_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
539
664
|
}
|
|
540
|
-
const {
|
|
541
|
-
const result = await ops.deleteNode(
|
|
665
|
+
const { project, flow, nodeId } = parsed.data;
|
|
666
|
+
const result = await ops.deleteNode(`${project}/${flow}`, nodeId);
|
|
542
667
|
switch (result.kind) {
|
|
543
668
|
case 'ok':
|
|
544
669
|
return okResult({ ok: true });
|
|
@@ -566,8 +691,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
566
691
|
if (!parsed.success) {
|
|
567
692
|
return errorResult(`Invalid move_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
568
693
|
}
|
|
569
|
-
const {
|
|
570
|
-
const result = await ops.moveNode(
|
|
694
|
+
const { project, flow, nodeId, x, y } = parsed.data;
|
|
695
|
+
const result = await ops.moveNode(`${project}/${flow}`, nodeId, { x, y });
|
|
571
696
|
switch (result.kind) {
|
|
572
697
|
case 'ok':
|
|
573
698
|
return okResult({ ok: true, position: result.data.position });
|
|
@@ -596,8 +721,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
596
721
|
if (!parsed.success) {
|
|
597
722
|
return errorResult(`Invalid patch_node arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
598
723
|
}
|
|
599
|
-
const {
|
|
600
|
-
const result = await ops.patchNode(
|
|
724
|
+
const { project, flow, nodeId, ...updates } = parsed.data;
|
|
725
|
+
const result = await ops.patchNode(`${project}/${flow}`, nodeId, updates);
|
|
601
726
|
switch (result.kind) {
|
|
602
727
|
case 'ok':
|
|
603
728
|
return okResult({ ok: true });
|
|
@@ -628,12 +753,12 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
628
753
|
`Invalid reorder_node arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
629
754
|
);
|
|
630
755
|
}
|
|
631
|
-
const {
|
|
756
|
+
const { project, flow, nodeId, ...body } = parsed.data;
|
|
632
757
|
// Delegate the op-specific shape to the existing ReorderBodySchema so
|
|
633
758
|
// reorderNodeImpl receives the same discriminated union the REST route
|
|
634
759
|
// does — keeps a single source of truth for op semantics.
|
|
635
760
|
const reorderBody = ReorderBodySchema.parse(body);
|
|
636
|
-
const result = await ops.reorderNode(
|
|
761
|
+
const result = await ops.reorderNode(`${project}/${flow}`, nodeId, reorderBody);
|
|
637
762
|
switch (result.kind) {
|
|
638
763
|
case 'ok':
|
|
639
764
|
return okResult({ ok: true });
|
|
@@ -664,8 +789,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
664
789
|
`Invalid add_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
665
790
|
);
|
|
666
791
|
}
|
|
667
|
-
const {
|
|
668
|
-
const result = await ops.addConnector(
|
|
792
|
+
const { project, flow, connector } = parsed.data;
|
|
793
|
+
const result = await ops.addConnector(`${project}/${flow}`, connector);
|
|
669
794
|
switch (result.kind) {
|
|
670
795
|
case 'ok':
|
|
671
796
|
return okResult({ ok: true, id: result.data.id });
|
|
@@ -694,8 +819,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
694
819
|
`Invalid patch_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
695
820
|
);
|
|
696
821
|
}
|
|
697
|
-
const {
|
|
698
|
-
const result = await ops.patchConnector(
|
|
822
|
+
const { project, flow, connectorId, ...updates } = parsed.data;
|
|
823
|
+
const result = await ops.patchConnector(`${project}/${flow}`, connectorId, updates);
|
|
699
824
|
switch (result.kind) {
|
|
700
825
|
case 'ok':
|
|
701
826
|
return okResult({ ok: true });
|
|
@@ -725,8 +850,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
725
850
|
`Invalid delete_connector arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
726
851
|
);
|
|
727
852
|
}
|
|
728
|
-
const {
|
|
729
|
-
const result = await ops.deleteConnector(
|
|
853
|
+
const { project, flow, connectorId } = parsed.data;
|
|
854
|
+
const result = await ops.deleteConnector(`${project}/${flow}`, connectorId);
|
|
730
855
|
switch (result.kind) {
|
|
731
856
|
case 'ok':
|
|
732
857
|
return okResult({ ok: true });
|
|
@@ -758,9 +883,16 @@ export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
|
758
883
|
registry: options.registry,
|
|
759
884
|
watcher: options.watcher,
|
|
760
885
|
});
|
|
761
|
-
const tools = buildTools(ops
|
|
886
|
+
const tools = buildTools(ops, {
|
|
887
|
+
registry: options.registry,
|
|
888
|
+
token: options.token,
|
|
889
|
+
httpUrl: options.httpUrl,
|
|
890
|
+
});
|
|
762
891
|
|
|
763
|
-
const server = new Server(
|
|
892
|
+
const server = new Server(
|
|
893
|
+
{ name: 'seeflow', version: '0.1.0' },
|
|
894
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
895
|
+
);
|
|
764
896
|
|
|
765
897
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
766
898
|
tools: tools.map(({ name, description, inputSchema }) => ({
|
|
@@ -781,5 +913,34 @@ export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
|
781
913
|
return tool.handler(request.params.arguments);
|
|
782
914
|
});
|
|
783
915
|
|
|
916
|
+
// MCP Apps resource: a single readable HTML bundle the host iframes when a
|
|
917
|
+
// canvas-bearing tool returns `_meta['openai/outputTemplate'] = CANVAS_RESOURCE_URI`.
|
|
918
|
+
// Listed unconditionally — the bundle is part of the binary, even on hosts
|
|
919
|
+
// that don't speak MCP Apps (they just ignore the resource).
|
|
920
|
+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
|
|
921
|
+
resources: [
|
|
922
|
+
{
|
|
923
|
+
uri: CANVAS_RESOURCE_URI,
|
|
924
|
+
name: 'SeeFlow Canvas',
|
|
925
|
+
mimeType: CANVAS_RESOURCE_MIME,
|
|
926
|
+
},
|
|
927
|
+
],
|
|
928
|
+
}));
|
|
929
|
+
|
|
930
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
931
|
+
if (request.params.uri !== CANVAS_RESOURCE_URI) {
|
|
932
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
contents: [
|
|
936
|
+
{
|
|
937
|
+
uri: CANVAS_RESOURCE_URI,
|
|
938
|
+
mimeType: CANVAS_RESOURCE_MIME,
|
|
939
|
+
text: readCanvasHtml(),
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
};
|
|
943
|
+
});
|
|
944
|
+
|
|
784
945
|
return server;
|
|
785
946
|
}
|
package/src/node-files.ts
CHANGED
|
@@ -3,9 +3,12 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { writeFileAtomic } from './atomic-write.ts';
|
|
4
4
|
|
|
5
5
|
// Spec for fields that the studio externalizes to disk under
|
|
6
|
-
// `<
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// `<repoPath>/<flowDir>/nodes/<id>/<fileName>`, where `flowDir` is
|
|
7
|
+
// `dirname(entry.flowPath)`. For manifest-driven projects this resolves to
|
|
8
|
+
// `flows/<flow-id>/nodes/<id>/`; for legacy single-flow fixtures with
|
|
9
|
+
// `flowPath: 'flow.json'` it collapses to the project root. `nodeTypes` (when
|
|
10
|
+
// present) scopes the spec entry to specific node types; absent means
|
|
11
|
+
// "applies to every node type". Adding a future text field is one line.
|
|
9
12
|
export interface ExternalizedFieldSpec {
|
|
10
13
|
field: string;
|
|
11
14
|
fileName: string;
|
|
@@ -26,6 +29,10 @@ export const externalizedFieldsForNodeType = (
|
|
|
26
29
|
|
|
27
30
|
export type ExternalizedFieldName = (typeof EXTERNALIZED_NODE_FIELDS)[number]['field'];
|
|
28
31
|
|
|
32
|
+
// Flow-relative on-disk path under the flow folder. Returned with forward
|
|
33
|
+
// slashes so it round-trips through HTTP responses (the upload route ships
|
|
34
|
+
// this back as the `path` field, and the watcher's image-ref resolver treats
|
|
35
|
+
// it as relative to the flow folder).
|
|
29
36
|
export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
30
37
|
`nodes/${nodeId}/${fileName}`;
|
|
31
38
|
|
|
@@ -35,14 +42,18 @@ export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
|
35
42
|
// that the file lives under the given node.
|
|
36
43
|
export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
|
|
37
44
|
|
|
38
|
-
export const nodeFileAbsPath = (
|
|
39
|
-
|
|
45
|
+
export const nodeFileAbsPath = (
|
|
46
|
+
repoPath: string,
|
|
47
|
+
flowDir: string,
|
|
48
|
+
nodeId: string,
|
|
49
|
+
fileName: string,
|
|
50
|
+
): string => join(repoPath, flowDir, nodeFileRelPath(nodeId, fileName));
|
|
40
51
|
|
|
41
52
|
export function writeNodeFile(absPath: string, content: string): void {
|
|
42
53
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
43
54
|
writeFileAtomic(absPath, content);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
export function removeNodeDir(repoPath: string, nodeId: string): void {
|
|
47
|
-
rmSync(join(repoPath, 'nodes', nodeId), { recursive: true, force: true });
|
|
57
|
+
export function removeNodeDir(repoPath: string, flowDir: string, nodeId: string): void {
|
|
58
|
+
rmSync(join(repoPath, flowDir, 'nodes', nodeId), { recursive: true, force: true });
|
|
48
59
|
}
|