@tuongaz/seeflow 0.1.57 → 0.1.63
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 +3 -3
- package/dist/web/assets/{index-CPlccVLi.js → index-DAP_yx-l.js} +354 -354
- package/dist/web/assets/{index.es-CYTTDW0Q.js → index.es-2bA-nRVD.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DOaPC0dc.js → jspdf.es.min-C7u0-VKd.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/{.seeflow/flow.json → flow.json} +3 -25
- package/examples/ecommerce-platform/{.seeflow/scripts → scripts}/play.ts +1 -1
- package/examples/order-pipeline/{.seeflow/flow.json → flow.json} +1 -10
- package/package.json +1 -1
- package/src/api.ts +83 -55
- package/src/cli-helpers.ts +6 -5
- package/src/cli-manifest.ts +129 -15
- package/src/cli.ts +106 -13
- package/src/diagram.ts +0 -1
- package/src/file-ref.ts +16 -15
- package/src/mcp.ts +96 -16
- package/src/merge.ts +0 -1
- package/src/node-files.ts +5 -5
- package/src/operations.ts +40 -101
- package/src/paths.ts +16 -0
- package/src/proxy.ts +13 -13
- package/src/schema-catalog.ts +114 -0
- package/src/schema.ts +110 -133
- package/src/server.ts +3 -5
- package/src/short-id.ts +24 -0
- package/src/status-runner.ts +3 -3
- package/src/watcher.ts +15 -27
- package/src/sdk-template.ts +0 -37
- package/src/sdk-writer.ts +0 -37
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-3zFtHg6ENc/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-5F424NWbEu/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-CbwYqb7NfB/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-XwygzfKPZ5/view.html +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-fkptXw7uvs/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-kwBY8YPmYM/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-mPqan8rFYN/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-yKrg9DV5fJ/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/style.json → style.json} +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-GXTKUcE3ye/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-XKIyds0TDg/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-YOYiHJpY0i/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-zUIH7WFnhK/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/scripts → scripts}/play.ts +0 -0
- /package/examples/order-pipeline/{.seeflow/style.json → style.json} +0 -0
package/src/cli-manifest.ts
CHANGED
|
@@ -134,7 +134,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
134
134
|
synopsis: 'seeflow register [--path <dir>] [--flow <file>]',
|
|
135
135
|
description:
|
|
136
136
|
'Register a demo repo with the studio. Reads <repoPath>/<flow> (defaulting ' +
|
|
137
|
-
'to
|
|
137
|
+
'to ./flow.json), validates the schema, and writes an entry to ' +
|
|
138
138
|
'~/.seeflow/registry.json. Alias of flows:register.',
|
|
139
139
|
category: 'flows',
|
|
140
140
|
args: [],
|
|
@@ -143,12 +143,12 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
143
143
|
{
|
|
144
144
|
name: 'flow',
|
|
145
145
|
valuePlaceholder: '<file>',
|
|
146
|
-
description: 'Path to flow.json relative to repo root (default:
|
|
146
|
+
description: 'Path to flow.json relative to repo root (default: flow.json)',
|
|
147
147
|
},
|
|
148
148
|
],
|
|
149
149
|
outputs: {
|
|
150
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
151
|
-
errorKinds: ['fileNotFound', 'badJson', 'badSchema'
|
|
150
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
151
|
+
errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
|
|
152
152
|
},
|
|
153
153
|
requiresStudio: false,
|
|
154
154
|
examples: ['seeflow register', 'seeflow register --path ./my-app'],
|
|
@@ -164,13 +164,13 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
164
164
|
{
|
|
165
165
|
name: 'flow',
|
|
166
166
|
valuePlaceholder: '<file>',
|
|
167
|
-
description: 'Path to flow.json relative to repo root (default:
|
|
167
|
+
description: 'Path to flow.json relative to repo root (default: flow.json)',
|
|
168
168
|
},
|
|
169
169
|
],
|
|
170
170
|
body: { schemaRef: 'RegisterBody' },
|
|
171
171
|
outputs: {
|
|
172
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
173
|
-
errorKinds: ['fileNotFound', 'badJson', 'badSchema'
|
|
172
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
173
|
+
errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
|
|
174
174
|
},
|
|
175
175
|
requiresStudio: false,
|
|
176
176
|
examples: ['seeflow flows:register --path ./my-app'],
|
|
@@ -303,22 +303,36 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
303
303
|
// ---- project -----------------------------------------------------------
|
|
304
304
|
{
|
|
305
305
|
name: 'projects:create',
|
|
306
|
-
synopsis: 'seeflow projects:create --name <name>',
|
|
306
|
+
synopsis: 'seeflow projects:create --path <dir> --name <name> [--description <text>]',
|
|
307
307
|
description:
|
|
308
|
-
'Scaffold a new project
|
|
309
|
-
'
|
|
308
|
+
'Scaffold a new project at <path> with an empty flow.json and register it. ' +
|
|
309
|
+
'Errors if <path>/flow.json already exists — use flows:register for ' +
|
|
310
|
+
'an existing project.',
|
|
310
311
|
category: 'project',
|
|
311
312
|
args: [],
|
|
312
313
|
flags: [
|
|
314
|
+
{
|
|
315
|
+
name: 'path',
|
|
316
|
+
valuePlaceholder: '<dir>',
|
|
317
|
+
description: 'Project folder (created if it does not exist)',
|
|
318
|
+
required: true,
|
|
319
|
+
},
|
|
313
320
|
{ name: 'name', valuePlaceholder: '<name>', description: 'Project name', required: true },
|
|
321
|
+
{
|
|
322
|
+
name: 'description',
|
|
323
|
+
valuePlaceholder: '<text>',
|
|
324
|
+
description: 'Optional human description, written into flow.json',
|
|
325
|
+
},
|
|
314
326
|
],
|
|
315
327
|
body: { schemaRef: 'CreateProjectBody' },
|
|
316
328
|
outputs: {
|
|
317
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
318
|
-
errorKinds: ['scaffoldFailed'],
|
|
329
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
330
|
+
errorKinds: ['alreadyExists', 'scaffoldFailed'],
|
|
319
331
|
},
|
|
320
332
|
requiresStudio: false,
|
|
321
|
-
examples: [
|
|
333
|
+
examples: [
|
|
334
|
+
'seeflow projects:create --path ./checkout --name "Checkout" --description "Cart + payments flow"',
|
|
335
|
+
],
|
|
322
336
|
},
|
|
323
337
|
// ---- nodes -------------------------------------------------------------
|
|
324
338
|
{
|
|
@@ -331,7 +345,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
331
345
|
body: {
|
|
332
346
|
example: {
|
|
333
347
|
type: 'stateNode',
|
|
334
|
-
data: { name: 'hello',
|
|
348
|
+
data: { name: 'hello', stateSource: { kind: 'request' } },
|
|
335
349
|
},
|
|
336
350
|
},
|
|
337
351
|
outputs: {
|
|
@@ -514,7 +528,64 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
514
528
|
],
|
|
515
529
|
outputs: { okExample: { ok: true } },
|
|
516
530
|
requiresStudio: false,
|
|
517
|
-
examples: ['seeflow validate --file
|
|
531
|
+
examples: ['seeflow validate --file flow.json'],
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'ids',
|
|
535
|
+
synopsis: 'seeflow ids <type> <count>',
|
|
536
|
+
description:
|
|
537
|
+
'Generate <count> canonical short ids of the given <type>, one per line ' +
|
|
538
|
+
'on stdout. <type> must be `node` (emits `node-<10 base62 chars>`) or ' +
|
|
539
|
+
'`connector` (emits `conn-<10 base62 chars>`). <count> must be an ' +
|
|
540
|
+
'integer in [1, 100]. Uses the same alphabet, length, and rejection-' +
|
|
541
|
+
'sampling logic as the canvas / server / upload regex, so skill-minted ' +
|
|
542
|
+
'ids match every other id producer in the studio. Pure compute — no ' +
|
|
543
|
+
'studio required. Call once per type (i.e. one call for nodes, one for ' +
|
|
544
|
+
'connectors) when seeding a flow.json.',
|
|
545
|
+
category: 'meta',
|
|
546
|
+
args: [
|
|
547
|
+
{
|
|
548
|
+
name: 'type',
|
|
549
|
+
required: true,
|
|
550
|
+
description: "Id kind to generate: 'node' (→ `node-…`) or 'connector' (→ `conn-…`)",
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'count',
|
|
554
|
+
required: true,
|
|
555
|
+
description: 'How many ids to print (integer, 1..100)',
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
flags: [],
|
|
559
|
+
outputKind: 'text',
|
|
560
|
+
outputs: {},
|
|
561
|
+
requiresStudio: false,
|
|
562
|
+
examples: ['seeflow ids node 10', 'seeflow ids connector 5'],
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: 'schema',
|
|
566
|
+
synopsis: 'seeflow schema [<category>]',
|
|
567
|
+
description:
|
|
568
|
+
'Introspect the SeeFlow flow.json / style.json schemas at runtime. Call ' +
|
|
569
|
+
'without arguments to list the five categories (flow, node, connector, ' +
|
|
570
|
+
'action, style); call with a category name to get its full JSON Schema(s) ' +
|
|
571
|
+
'(Draft-07) plus a `notes` array of cross-field invariants the schema ' +
|
|
572
|
+
"can't express. Use this before authoring any flow.json write — never " +
|
|
573
|
+
'memorise field shapes.',
|
|
574
|
+
category: 'meta',
|
|
575
|
+
args: [
|
|
576
|
+
{
|
|
577
|
+
name: 'category',
|
|
578
|
+
required: false,
|
|
579
|
+
description: 'One of: flow, node, connector, action, style',
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
flags: [],
|
|
583
|
+
outputs: {
|
|
584
|
+
okExample: { categories: [{ name: 'flow', description: 'Top-level flow.json envelope.' }] },
|
|
585
|
+
errorKinds: ['notFound'],
|
|
586
|
+
},
|
|
587
|
+
requiresStudio: false,
|
|
588
|
+
examples: ['seeflow schema', 'seeflow schema node', 'seeflow schema connector'],
|
|
518
589
|
},
|
|
519
590
|
// ---- live --------------------------------------------------------------
|
|
520
591
|
{
|
|
@@ -536,6 +607,49 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
536
607
|
requiresStudio: true,
|
|
537
608
|
examples: ['seeflow e2e abc12345'],
|
|
538
609
|
},
|
|
610
|
+
{
|
|
611
|
+
name: 'emit',
|
|
612
|
+
synopsis:
|
|
613
|
+
'seeflow emit <flowId> <nodeId> <status> [--run-id <id>] [--payload <json>] [--studio-url <url>]',
|
|
614
|
+
description:
|
|
615
|
+
'Broadcast a node-status event to the studio (status: running | done | error). ' +
|
|
616
|
+
'User apps shell out to this command instead of importing an in-repo helper; ' +
|
|
617
|
+
"the studio re-broadcasts the event on the flow's SSE stream.",
|
|
618
|
+
category: 'live',
|
|
619
|
+
args: [
|
|
620
|
+
{ name: 'flowId', required: true, description: 'Flow id or slug' },
|
|
621
|
+
{ name: 'nodeId', required: true, description: 'Node id in the flow' },
|
|
622
|
+
{ name: 'status', required: true, description: 'Status: running | done | error' },
|
|
623
|
+
],
|
|
624
|
+
flags: [
|
|
625
|
+
{
|
|
626
|
+
name: 'run-id',
|
|
627
|
+
valuePlaceholder: '<id>',
|
|
628
|
+
description: 'Correlate events emitted within a single run',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'payload',
|
|
632
|
+
valuePlaceholder: '<json>',
|
|
633
|
+
description: 'JSON string merged into the event payload',
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: 'studio-url',
|
|
637
|
+
valuePlaceholder: '<url>',
|
|
638
|
+
description: 'Override studio base URL (skips auto-start; targets the URL as-is)',
|
|
639
|
+
},
|
|
640
|
+
{ name: 'no-start', description: 'Fail if the studio is not already running' },
|
|
641
|
+
],
|
|
642
|
+
outputs: {
|
|
643
|
+
okExample: { ok: true },
|
|
644
|
+
errorKinds: [],
|
|
645
|
+
},
|
|
646
|
+
requiresStudio: true,
|
|
647
|
+
examples: [
|
|
648
|
+
'seeflow emit abc12345 api-charge done',
|
|
649
|
+
'seeflow emit abc12345 api-charge error --payload \'{"code":402}\'',
|
|
650
|
+
'seeflow emit abc12345 api-charge running --run-id run-9b3 --studio-url http://localhost:4321',
|
|
651
|
+
],
|
|
652
|
+
},
|
|
539
653
|
];
|
|
540
654
|
|
|
541
655
|
function resolveSchemaRef(ref: string): unknown {
|
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
NodePatchBodySchema,
|
|
13
13
|
ReorderBodySchema,
|
|
14
14
|
} from './operations.ts';
|
|
15
|
-
import { seeflowHome } from './paths.ts';
|
|
15
|
+
import { PROJECT_FLOW_FILENAME, seeflowHome } from './paths.ts';
|
|
16
16
|
import { defaultProcessSpawner } from './process-spawner.ts';
|
|
17
17
|
import { type Registry, createRegistry } from './registry.ts';
|
|
18
18
|
import {
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
} from './runtime.ts';
|
|
29
29
|
import { FlowSchema } from './schema.ts';
|
|
30
30
|
import { serve } from './server.ts';
|
|
31
|
+
import { MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
31
32
|
import { createStatusRunner } from './status-runner.ts';
|
|
32
33
|
|
|
33
|
-
const DEFAULT_FLOW_PATH =
|
|
34
|
+
const DEFAULT_FLOW_PATH = PROJECT_FLOW_FILENAME;
|
|
34
35
|
const HEALTH_TIMEOUT_MS = 10_000;
|
|
35
36
|
const HEALTH_POLL_INTERVAL_MS = 150;
|
|
36
37
|
const STOP_TIMEOUT_MS = 5_000;
|
|
@@ -157,8 +158,14 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
157
158
|
await runConnectorsDelete();
|
|
158
159
|
} else if (sub === 'validate') {
|
|
159
160
|
await runValidate();
|
|
161
|
+
} else if (sub === 'schema') {
|
|
162
|
+
await runSchema();
|
|
163
|
+
} else if (sub === 'ids') {
|
|
164
|
+
await runIds();
|
|
160
165
|
} else if (sub === 'e2e') {
|
|
161
166
|
await runE2e();
|
|
167
|
+
} else if (sub === 'emit') {
|
|
168
|
+
await runEmit();
|
|
162
169
|
} else {
|
|
163
170
|
console.error(`Unknown subcommand: ${sub}`);
|
|
164
171
|
printHelp();
|
|
@@ -178,7 +185,7 @@ Commands (work without a running studio):
|
|
|
178
185
|
stop Stop a background studio instance
|
|
179
186
|
register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
|
|
180
187
|
flows:register Register a demo repo
|
|
181
|
-
projects:create Create a new project (--name <name>)
|
|
188
|
+
projects:create Create a new project (--path <dir> --name <name> [--description <text>])
|
|
182
189
|
flows:list List registered flows
|
|
183
190
|
flows:summary List registered flows (id + name + description only)
|
|
184
191
|
flows:get <id> Get flow details
|
|
@@ -196,9 +203,18 @@ Commands (work without a running studio):
|
|
|
196
203
|
connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
|
|
197
204
|
connectors:delete <id> <connId> Delete a connector
|
|
198
205
|
validate Schema-validate a flow.json (--file <file> [--style <file>])
|
|
206
|
+
schema [<category>] Get the flow.json schema. No arg → category index;
|
|
207
|
+
category arg → full JSON Schema(s) for that category
|
|
208
|
+
ids <type> <count> Print <count> short ids of the given <type>, one per
|
|
209
|
+
line. <type> is 'node' (-> 'node-...') or 'connector'
|
|
210
|
+
(-> 'conn-...'). <count> is 1..100. Call once per type
|
|
211
|
+
when seeding a flow.json (e.g. 'ids node 10', then
|
|
212
|
+
'ids connector 12').
|
|
199
213
|
|
|
200
214
|
Commands (require a running studio):
|
|
201
215
|
flows:play <id> <n> Trigger a play on node <n>
|
|
216
|
+
emit <id> <n> <st> Broadcast a status event for node <n> (st: running|done|error)
|
|
217
|
+
[--run-id <id>] [--payload <json>] [--studio-url <url>]
|
|
202
218
|
e2e <id> End-to-end validate a registered flow (--skip-nodes a,b)
|
|
203
219
|
|
|
204
220
|
Meta:
|
|
@@ -223,14 +239,14 @@ Options (start):
|
|
|
223
239
|
Options (register):
|
|
224
240
|
--path <dir> Path to repo root (default: current directory)
|
|
225
241
|
--flow <file> Path to flow JSON, relative to repo root
|
|
226
|
-
(default:
|
|
242
|
+
(default: flow.json)
|
|
227
243
|
|
|
228
244
|
Examples:
|
|
229
245
|
npx -y @tuongaz/seeflow@latest
|
|
230
246
|
npx -y @tuongaz/seeflow@latest --port 8080
|
|
231
247
|
npx -y @tuongaz/seeflow@latest start --foreground
|
|
232
248
|
npx -y @tuongaz/seeflow@latest register --path ./my-app
|
|
233
|
-
npx -y @tuongaz/seeflow@latest projects:create --name "Checkout"
|
|
249
|
+
npx -y @tuongaz/seeflow@latest projects:create --path ./checkout --name "Checkout"
|
|
234
250
|
npx -y @tuongaz/seeflow@latest flows:list
|
|
235
251
|
npx -y @tuongaz/seeflow@latest stop
|
|
236
252
|
`.trim(),
|
|
@@ -251,6 +267,7 @@ async function runHelp() {
|
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
async function runStart() {
|
|
270
|
+
mkdirSync(seeflowHome(), { recursive: true });
|
|
254
271
|
const config = readConfig();
|
|
255
272
|
const portArg = flagValue('port');
|
|
256
273
|
// --port wins; otherwise always fall back to the schema default (not the
|
|
@@ -316,7 +333,7 @@ async function seedExamples(registry: Registry) {
|
|
|
316
333
|
|
|
317
334
|
async function seedExample(registry: Registry, exampleName: string) {
|
|
318
335
|
const destDir = join(seeflowHome(), exampleName);
|
|
319
|
-
const flowPath =
|
|
336
|
+
const flowPath = PROJECT_FLOW_FILENAME;
|
|
320
337
|
|
|
321
338
|
// Always sync from source so that schema changes and example updates are
|
|
322
339
|
// reflected on every startup, even when the dest directory already exists.
|
|
@@ -410,6 +427,25 @@ function reportDaemonFailure(logPath: string | undefined) {
|
|
|
410
427
|
console.error(tail || '(log is empty — daemon exited before writing anything)');
|
|
411
428
|
}
|
|
412
429
|
|
|
430
|
+
async function runIds() {
|
|
431
|
+
const typeArg = argv[1];
|
|
432
|
+
if (!typeArg || typeArg.startsWith('--')) {
|
|
433
|
+
printError("Missing required positional argument: type (expected 'node' or 'connector')");
|
|
434
|
+
}
|
|
435
|
+
if (!isIdType(typeArg)) {
|
|
436
|
+
printError(`Invalid type: ${typeArg} (expected 'node' or 'connector')`);
|
|
437
|
+
}
|
|
438
|
+
const rawCount = argv[2];
|
|
439
|
+
if (!rawCount || rawCount.startsWith('--')) {
|
|
440
|
+
printError(`Missing required positional argument: count (integer 1..${MAX_ID_COUNT})`);
|
|
441
|
+
}
|
|
442
|
+
const count = Number.parseInt(rawCount as string, 10);
|
|
443
|
+
if (!Number.isFinite(count) || count < 1 || count > MAX_ID_COUNT) {
|
|
444
|
+
printError(`Invalid count: ${rawCount} (expected an integer in [1, ${MAX_ID_COUNT}])`);
|
|
445
|
+
}
|
|
446
|
+
for (const id of generateIds(typeArg, count)) process.stdout.write(`${id}\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
413
449
|
async function printVersion() {
|
|
414
450
|
const pkgPath = join(import.meta.dir, '../package.json');
|
|
415
451
|
const pkg = (await Bun.file(pkgPath).json()) as { version?: string };
|
|
@@ -524,12 +560,6 @@ async function runRegister() {
|
|
|
524
560
|
} else {
|
|
525
561
|
console.log(`Registered "${parsed.data.name}" (slug: ${body.slug})`);
|
|
526
562
|
}
|
|
527
|
-
|
|
528
|
-
if (body.sdk?.outcome === 'written') {
|
|
529
|
-
console.log(`Wrote ${body.sdk.filePath} (event-bound state node detected)`);
|
|
530
|
-
} else if (body.sdk?.outcome === 'present') {
|
|
531
|
-
console.log(`SDK helper already present at ${body.sdk.filePath} (skipped)`);
|
|
532
|
-
}
|
|
533
563
|
}
|
|
534
564
|
|
|
535
565
|
async function ensureStudioRunning(url: string, port: number, noStart: boolean) {
|
|
@@ -581,10 +611,18 @@ async function waitForHealth(url: string, timeoutMs: number): Promise<boolean> {
|
|
|
581
611
|
// ---- HTTP-passthrough subcommands ----------------------------------------
|
|
582
612
|
|
|
583
613
|
async function runProjectsCreate() {
|
|
614
|
+
const rawPath = flagValue('path');
|
|
615
|
+
if (!rawPath) printError('Missing required flag: --path');
|
|
584
616
|
const name = flagValue('name');
|
|
585
617
|
if (!name) printError('Missing required flag: --name');
|
|
618
|
+
const description = flagValue('description');
|
|
619
|
+
|
|
586
620
|
const ops = createCliOperations();
|
|
587
|
-
const result = await ops.createProject({
|
|
621
|
+
const result = await ops.createProject({
|
|
622
|
+
path: resolve(rawPath as string),
|
|
623
|
+
name: name as string,
|
|
624
|
+
...(description !== undefined ? { description } : {}),
|
|
625
|
+
});
|
|
588
626
|
printOutcome(result);
|
|
589
627
|
}
|
|
590
628
|
|
|
@@ -646,6 +684,45 @@ async function runFlowsPlay() {
|
|
|
646
684
|
printOk(out);
|
|
647
685
|
}
|
|
648
686
|
|
|
687
|
+
async function runEmit() {
|
|
688
|
+
const flowId = requireArg(1, '<flowId>');
|
|
689
|
+
const nodeId = requireArg(2, '<nodeId>');
|
|
690
|
+
const status = requireArg(3, '<status>');
|
|
691
|
+
if (status !== 'running' && status !== 'done' && status !== 'error') {
|
|
692
|
+
printError(`Invalid status: ${status} (expected running | done | error)`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const runId = flagValue('run-id');
|
|
696
|
+
const rawPayload = flagValue('payload');
|
|
697
|
+
let payload: unknown;
|
|
698
|
+
if (rawPayload !== undefined) {
|
|
699
|
+
try {
|
|
700
|
+
payload = JSON.parse(rawPayload);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
703
|
+
printError(`--payload must be valid JSON: ${message}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Explicit --studio-url targets a specific instance; skip auto-start since
|
|
708
|
+
// the caller is asserting where the studio lives.
|
|
709
|
+
const studioUrlFlag = flagValue('studio-url');
|
|
710
|
+
let url: string;
|
|
711
|
+
if (studioUrlFlag) {
|
|
712
|
+
url = studioUrlFlag.replace(/\/+$/, '');
|
|
713
|
+
} else {
|
|
714
|
+
({ url } = await studioUrlOrDie(hasFlag('no-start')));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const body: Record<string, unknown> = { flowId, nodeId, status };
|
|
718
|
+
if (runId !== undefined) body.runId = runId;
|
|
719
|
+
if (payload !== undefined) body.payload = payload;
|
|
720
|
+
|
|
721
|
+
const res = await postJson(`${url}/api/emit`, body);
|
|
722
|
+
const out = (await handleResponse(res)) as object;
|
|
723
|
+
printOk(out);
|
|
724
|
+
}
|
|
725
|
+
|
|
649
726
|
async function runNodesAdd() {
|
|
650
727
|
const flowId = requireArg(1, '<flowId>');
|
|
651
728
|
const body = await bodyFromFlags();
|
|
@@ -800,6 +877,22 @@ async function runValidate() {
|
|
|
800
877
|
printOk(body);
|
|
801
878
|
}
|
|
802
879
|
|
|
880
|
+
async function runSchema() {
|
|
881
|
+
const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
|
|
882
|
+
const { listSchemaCategories, getSchemaCategory } = await import('./schema-catalog.ts');
|
|
883
|
+
if (!category) {
|
|
884
|
+
printOk({ categories: listSchemaCategories() });
|
|
885
|
+
}
|
|
886
|
+
const payload = getSchemaCategory(category as string);
|
|
887
|
+
if (!payload) {
|
|
888
|
+
const available = listSchemaCategories().map((c) => c.name);
|
|
889
|
+
const message = `unknown schema category: ${category}`;
|
|
890
|
+
process.stderr.write(`${JSON.stringify({ error: message, code: 'notFound', available })}\n`);
|
|
891
|
+
process.exit(3);
|
|
892
|
+
}
|
|
893
|
+
printOk({ name: category, schemas: payload.schemas, notes: payload.notes });
|
|
894
|
+
}
|
|
895
|
+
|
|
803
896
|
async function runE2e() {
|
|
804
897
|
const flowId = requireArg(1, '<flowId>');
|
|
805
898
|
const skipNodesRaw = flagValue('skip-nodes');
|
package/src/diagram.ts
CHANGED
package/src/file-ref.ts
CHANGED
|
@@ -19,24 +19,25 @@ const looksLikeFlowNode = (obj: Record<string, unknown>): obj is { id: string; d
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve every `file://<relative-path>` string in `raw` by reading the file
|
|
22
|
-
* under `<
|
|
22
|
+
* under `<projectRoot>/nodes/<nodeId>/` (node-relative) and substituting its
|
|
23
23
|
* UTF-8 content. Strings outside any enclosing flow node are treated as
|
|
24
24
|
* invalid — every supported file:// ref currently lives inside `node.data`.
|
|
25
25
|
*
|
|
26
|
-
* Returns the mutated tree plus the sorted, de-duplicated list of
|
|
27
|
-
* paths that resolved cleanly (the watcher tracks these for
|
|
28
|
-
* external contract uses `nodes/<id>/<file>` even though
|
|
26
|
+
* Returns the mutated tree plus the sorted, de-duplicated list of project-
|
|
27
|
+
* root-relative paths that resolved cleanly (the watcher tracks these for
|
|
28
|
+
* live reload, so the external contract uses `nodes/<id>/<file>` even though
|
|
29
|
+
* the source string is short).
|
|
29
30
|
*/
|
|
30
31
|
export function resolveFileRefs(
|
|
31
32
|
raw: unknown,
|
|
32
|
-
|
|
33
|
+
projectRoot: string,
|
|
33
34
|
): { resolved: unknown; refs: string[] } {
|
|
34
35
|
const refs = new Set<string>();
|
|
35
|
-
let
|
|
36
|
+
let projectRealRoot: string;
|
|
36
37
|
try {
|
|
37
|
-
|
|
38
|
+
projectRealRoot = existsSync(projectRoot) ? realpathSync(projectRoot) : projectRoot;
|
|
38
39
|
} catch {
|
|
39
|
-
|
|
40
|
+
projectRealRoot = projectRoot;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const resolveString = (s: string, nodeId: string | null): string => {
|
|
@@ -45,26 +46,26 @@ export function resolveFileRefs(
|
|
|
45
46
|
if (!isCleanRelativePath(relPath)) return invalidMarker(relPath);
|
|
46
47
|
if (nodeId === null) return invalidMarker(relPath);
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
-
const abs = join(
|
|
50
|
-
if (!existsSync(abs)) return missingMarker(
|
|
49
|
+
const projectRelPath = `nodes/${nodeId}/${relPath}`;
|
|
50
|
+
const abs = join(projectRoot, projectRelPath);
|
|
51
|
+
if (!existsSync(abs)) return missingMarker(projectRelPath);
|
|
51
52
|
|
|
52
53
|
// Symlink-escape defense: resolve realpath and confirm it stays inside root.
|
|
53
54
|
let realAbs: string;
|
|
54
55
|
try {
|
|
55
56
|
realAbs = realpathSync(abs);
|
|
56
57
|
} catch {
|
|
57
|
-
return missingMarker(
|
|
58
|
+
return missingMarker(projectRelPath);
|
|
58
59
|
}
|
|
59
|
-
const rel = relative(
|
|
60
|
+
const rel = relative(projectRealRoot, realAbs);
|
|
60
61
|
if (rel.startsWith('..') || isAbsolute(rel)) return invalidMarker(relPath);
|
|
61
62
|
|
|
62
63
|
try {
|
|
63
64
|
const content = readFileSync(realAbs, 'utf8');
|
|
64
|
-
refs.add(
|
|
65
|
+
refs.add(projectRelPath);
|
|
65
66
|
return content;
|
|
66
67
|
} catch {
|
|
67
|
-
return missingMarker(
|
|
68
|
+
return missingMarker(projectRelPath);
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
|
package/src/mcp.ts
CHANGED
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
flowBulkNonEmpty,
|
|
21
21
|
} from './operations.ts';
|
|
22
22
|
import type { Registry } from './registry.ts';
|
|
23
|
+
import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
|
|
24
|
+
import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
23
25
|
import type { FlowWatcher } from './watcher.ts';
|
|
24
26
|
|
|
25
27
|
export interface CreateMcpServerOptions {
|
|
26
28
|
registry: Registry;
|
|
27
29
|
watcher?: FlowWatcher;
|
|
28
|
-
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
29
|
-
projectBaseDir?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Tools are pushed into this in-memory list inside `createMcpServer`. Each
|
|
@@ -193,6 +193,94 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
193
193
|
return okResult(result.data);
|
|
194
194
|
},
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'seeflow_schema',
|
|
198
|
+
description:
|
|
199
|
+
'Get the SeeFlow flow.json schema. Call with no args for a category index; ' +
|
|
200
|
+
"call with `name` for one category's full JSON Schemas. Use this to learn " +
|
|
201
|
+
'what a node, connector, action, or flow envelope looks like before authoring ' +
|
|
202
|
+
'writes. Categories: `flow`, `node`, `connector`, `action`, `style`.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
name: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Optional category name. Omit for the index.',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
},
|
|
213
|
+
handler: async (args) => {
|
|
214
|
+
const name =
|
|
215
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
216
|
+
? (args as { name?: unknown }).name
|
|
217
|
+
: undefined;
|
|
218
|
+
if (name === undefined || name === null || name === '') {
|
|
219
|
+
return okResult({ categories: listSchemaCategories() });
|
|
220
|
+
}
|
|
221
|
+
if (typeof name !== 'string') {
|
|
222
|
+
return errorResult('Invalid arguments: `name` must be a string when present');
|
|
223
|
+
}
|
|
224
|
+
const payload = getSchemaCategory(name);
|
|
225
|
+
if (!payload) {
|
|
226
|
+
return errorResult(
|
|
227
|
+
`unknown schema category: ${name} (available: ${schemaCategoryNames().join(', ')})`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return okResult({ name, schemas: payload.schemas, notes: payload.notes });
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'seeflow_ids',
|
|
235
|
+
description:
|
|
236
|
+
'Batch-mint canonical short ids. `type` is `node` (emits `node-<10 base62 chars>`) ' +
|
|
237
|
+
'or `connector` (emits `conn-<10 base62 chars>`); `count` is an integer in ' +
|
|
238
|
+
'[1, 100]. Pure compute — no flow side effects, no studio state read. Use ' +
|
|
239
|
+
'before authoring a flow.json so minted ids match every other id producer ' +
|
|
240
|
+
'in the studio (canvas, server, upload regex). Call once per type when ' +
|
|
241
|
+
'seeding a flow (one call for nodes, one for connectors).',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
type: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
enum: [...ID_TYPES],
|
|
248
|
+
description: "Id kind: 'node' (→ `node-…`) or 'connector' (→ `conn-…`)",
|
|
249
|
+
},
|
|
250
|
+
count: {
|
|
251
|
+
type: 'integer',
|
|
252
|
+
minimum: 1,
|
|
253
|
+
maximum: MAX_ID_COUNT,
|
|
254
|
+
description: `How many ids to mint (1..${MAX_ID_COUNT})`,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ['type', 'count'],
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
},
|
|
260
|
+
handler: async (args) => {
|
|
261
|
+
const body =
|
|
262
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
263
|
+
? (args as { type?: unknown; count?: unknown })
|
|
264
|
+
: {};
|
|
265
|
+
const { type, count } = body;
|
|
266
|
+
if (!isIdType(type)) {
|
|
267
|
+
return errorResult(
|
|
268
|
+
`invalid type: ${String(type)} (expected one of: ${ID_TYPES.join(', ')})`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
typeof count !== 'number' ||
|
|
273
|
+
!Number.isInteger(count) ||
|
|
274
|
+
count < 1 ||
|
|
275
|
+
count > MAX_ID_COUNT
|
|
276
|
+
) {
|
|
277
|
+
return errorResult(
|
|
278
|
+
`invalid count: ${String(count)} (expected an integer in [1, ${MAX_ID_COUNT}])`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return okResult({ ids: generateIds(type, count) });
|
|
282
|
+
},
|
|
283
|
+
},
|
|
196
284
|
{
|
|
197
285
|
name: 'validate_seeflow',
|
|
198
286
|
description:
|
|
@@ -330,8 +418,6 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
330
418
|
return errorResult(
|
|
331
419
|
`Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
332
420
|
);
|
|
333
|
-
case 'sdkWriteFailed':
|
|
334
|
-
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
335
421
|
}
|
|
336
422
|
},
|
|
337
423
|
},
|
|
@@ -353,7 +439,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
353
439
|
},
|
|
354
440
|
{
|
|
355
441
|
name: 'seeflow_create_project',
|
|
356
|
-
description:
|
|
442
|
+
description:
|
|
443
|
+
'Scaffold a new SeeFlow project at the given path. Errors if a project already exists there.',
|
|
357
444
|
inputSchema: inputSchemaFromZod(CreateProjectBodySchema),
|
|
358
445
|
handler: async (args) => {
|
|
359
446
|
const parsed = CreateProjectBodySchema.safeParse(args);
|
|
@@ -364,23 +451,17 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
364
451
|
switch (result.kind) {
|
|
365
452
|
case 'ok':
|
|
366
453
|
return okResult(result.data);
|
|
367
|
-
case '
|
|
368
|
-
return errorResult(`
|
|
369
|
-
case 'badSchema':
|
|
370
|
-
return errorResult(
|
|
371
|
-
`Existing demo file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
372
|
-
);
|
|
454
|
+
case 'alreadyExists':
|
|
455
|
+
return errorResult(`Project already exists at ${result.path}`);
|
|
373
456
|
case 'scaffoldFailed':
|
|
374
457
|
return errorResult(`Failed to scaffold project: ${result.message}`);
|
|
375
|
-
case 'sdkWriteFailed':
|
|
376
|
-
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
377
458
|
}
|
|
378
459
|
},
|
|
379
460
|
},
|
|
380
461
|
{
|
|
381
462
|
name: 'seeflow_add_node',
|
|
382
463
|
description:
|
|
383
|
-
'Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on htmlNode) are auto-externalized to <project
|
|
464
|
+
'Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on htmlNode) are auto-externalized to <project>/nodes/<id>/ and stored as file:// refs in flow.json; reads inline the resolved content transparently.',
|
|
384
465
|
inputSchema: inputSchemaFromZod(AddNodeInputSchema),
|
|
385
466
|
handler: async (args) => {
|
|
386
467
|
const parsed = AddNodeInputSchema.safeParse(args);
|
|
@@ -504,7 +585,7 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
504
585
|
{
|
|
505
586
|
name: 'seeflow_patch_node',
|
|
506
587
|
description:
|
|
507
|
-
'Update fields on an existing node (position, name, description, detail, icon, colors, border, font, shape, dimensions, autoSize, plus iconNode-only color/strokeWidth/alt). Setting detail (every node) or html (htmlNode) writes the content to <project
|
|
588
|
+
'Update fields on an existing node (position, name, description, detail, icon, colors, border, font, shape, dimensions, autoSize, plus iconNode-only color/strokeWidth/alt). Setting detail (every node) or html (htmlNode) writes the content to <project>/nodes/<id>/{detail.md|view.html}; the file:// ref on the node persists. Empty-string detail empties the file but keeps the ref.',
|
|
508
589
|
inputSchema: inputSchemaFromZod(PatchNodeInputSchema),
|
|
509
590
|
handler: async (args) => {
|
|
510
591
|
const parsed = PatchNodeInputSchema.safeParse(args);
|
|
@@ -672,7 +753,6 @@ export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
|
672
753
|
const ops = createOperations({
|
|
673
754
|
registry: options.registry,
|
|
674
755
|
watcher: options.watcher,
|
|
675
|
-
projectBaseDir: options.projectBaseDir,
|
|
676
756
|
});
|
|
677
757
|
const tools = buildTools(ops);
|
|
678
758
|
|