@tuongaz/seeflow 0.1.56 → 0.1.61

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/src/cli.ts CHANGED
@@ -157,6 +157,8 @@ if (argv.includes('--version') || argv.includes('-v')) {
157
157
  await runConnectorsDelete();
158
158
  } else if (sub === 'validate') {
159
159
  await runValidate();
160
+ } else if (sub === 'schema') {
161
+ await runSchema();
160
162
  } else if (sub === 'e2e') {
161
163
  await runE2e();
162
164
  } else {
@@ -196,6 +198,8 @@ Commands (work without a running studio):
196
198
  connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
197
199
  connectors:delete <id> <connId> Delete a connector
198
200
  validate Schema-validate a flow.json (--file <file> [--style <file>])
201
+ schema [<category>] Get the flow.json schema. No arg → category index;
202
+ category arg → full JSON Schema(s) for that category
199
203
 
200
204
  Commands (require a running studio):
201
205
  flows:play <id> <n> Trigger a play on node <n>
@@ -251,6 +255,7 @@ async function runHelp() {
251
255
  }
252
256
 
253
257
  async function runStart() {
258
+ mkdirSync(seeflowHome(), { recursive: true });
254
259
  const config = readConfig();
255
260
  const portArg = flagValue('port');
256
261
  // --port wins; otherwise always fall back to the schema default (not the
@@ -800,6 +805,22 @@ async function runValidate() {
800
805
  printOk(body);
801
806
  }
802
807
 
808
+ async function runSchema() {
809
+ const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
810
+ const { listSchemaCategories, getSchemaCategory } = await import('./schema-catalog.ts');
811
+ if (!category) {
812
+ printOk({ categories: listSchemaCategories() });
813
+ }
814
+ const payload = getSchemaCategory(category as string);
815
+ if (!payload) {
816
+ const available = listSchemaCategories().map((c) => c.name);
817
+ const message = `unknown schema category: ${category}`;
818
+ process.stderr.write(`${JSON.stringify({ error: message, code: 'notFound', available })}\n`);
819
+ process.exit(3);
820
+ }
821
+ printOk({ name: category, schemas: payload.schemas, notes: payload.notes });
822
+ }
823
+
803
824
  async function runE2e() {
804
825
  const flowId = requireArg(1, '<flowId>');
805
826
  const skipNodesRaw = flagValue('skip-nodes');
package/src/mcp.ts CHANGED
@@ -20,6 +20,7 @@ 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';
23
24
  import type { FlowWatcher } from './watcher.ts';
24
25
 
25
26
  export interface CreateMcpServerOptions {
@@ -193,6 +194,43 @@ const buildTools = (ops: Operations): McpTool[] => [
193
194
  return okResult(result.data);
194
195
  },
195
196
  },
197
+ {
198
+ name: 'seeflow_schema',
199
+ description:
200
+ 'Get the SeeFlow flow.json schema. Call with no args for a category index; ' +
201
+ "call with `name` for one category's full JSON Schemas. Use this to learn " +
202
+ 'what a node, connector, action, or flow envelope looks like before authoring ' +
203
+ 'writes. Categories: `flow`, `node`, `connector`, `action`, `style`.',
204
+ inputSchema: {
205
+ type: 'object',
206
+ properties: {
207
+ name: {
208
+ type: 'string',
209
+ description: 'Optional category name. Omit for the index.',
210
+ },
211
+ },
212
+ additionalProperties: false,
213
+ },
214
+ handler: async (args) => {
215
+ const name =
216
+ args && typeof args === 'object' && !Array.isArray(args)
217
+ ? (args as { name?: unknown }).name
218
+ : undefined;
219
+ if (name === undefined || name === null || name === '') {
220
+ return okResult({ categories: listSchemaCategories() });
221
+ }
222
+ if (typeof name !== 'string') {
223
+ return errorResult('Invalid arguments: `name` must be a string when present');
224
+ }
225
+ const payload = getSchemaCategory(name);
226
+ if (!payload) {
227
+ return errorResult(
228
+ `unknown schema category: ${name} (available: ${schemaCategoryNames().join(', ')})`,
229
+ );
230
+ }
231
+ return okResult({ name, schemas: payload.schemas, notes: payload.notes });
232
+ },
233
+ },
196
234
  {
197
235
  name: 'validate_seeflow',
198
236
  description:
@@ -0,0 +1,120 @@
1
+ // Single source of truth for runtime schema introspection. The CLI
2
+ // (`seeflow schema`), the MCP tool (`seeflow_schema`), and the REST routes
3
+ // (`GET /api/schema[/:name]`) all delegate here so the agent-facing surface
4
+ // stays in lockstep with the on-disk Zod schemas in schema.ts. Built once at
5
+ // module load — each call returns a fresh shallow copy so callers can't
6
+ // mutate the cached payload.
7
+
8
+ import type { ZodTypeAny } from 'zod';
9
+ import { zodToJsonSchema } from 'zod-to-json-schema';
10
+ import {
11
+ FlowDefaultConnectorSchema,
12
+ FlowEnvelopeSchema,
13
+ FlowEventConnectorSchema,
14
+ FlowHtmlNodeSchema,
15
+ FlowHttpConnectorSchema,
16
+ FlowIconNodeSchema,
17
+ FlowImageNodeSchema,
18
+ FlowPlayNodeSchema,
19
+ FlowQueueConnectorSchema,
20
+ FlowShapeNodeSchema,
21
+ FlowStateNodeSchema,
22
+ PlayActionSchema,
23
+ ResetActionSchema,
24
+ StatusActionSchema,
25
+ StatusReportSchema,
26
+ StyleSchema,
27
+ } from './schema.ts';
28
+
29
+ export interface SchemaCategory {
30
+ name: string;
31
+ description: string;
32
+ }
33
+
34
+ export interface SchemaPayload {
35
+ schemas: Record<string, unknown>;
36
+ notes: string[];
37
+ }
38
+
39
+ // Draft-07 pin matches the widest tool support; the same target string is
40
+ // used by the MCP `tools/list` JSON Schemas (default in zod-to-json-schema)
41
+ // so consumers see one consistent dialect across the whole surface.
42
+ const toJsonSchema = (schema: ZodTypeAny): unknown =>
43
+ zodToJsonSchema(schema, { $refStrategy: 'none', target: 'jsonSchema7' });
44
+
45
+ const CATEGORIES: SchemaCategory[] = [
46
+ { name: 'flow', description: 'Top-level flow.json envelope.' },
47
+ {
48
+ name: 'node',
49
+ description:
50
+ 'All six node variants (playNode, stateNode, shapeNode, imageNode, iconNode, htmlNode).',
51
+ },
52
+ {
53
+ name: 'connector',
54
+ description: 'All four connector kinds (http, event, queue, default).',
55
+ },
56
+ {
57
+ name: 'action',
58
+ description: 'playAction, statusAction, resetAction, statusReport.',
59
+ },
60
+ { name: 'style', description: 'style.json (studio-owned).' },
61
+ ];
62
+
63
+ const PAYLOADS: Record<string, SchemaPayload> = {
64
+ flow: {
65
+ schemas: { flow: toJsonSchema(FlowEnvelopeSchema) },
66
+ notes: ['connectors[].source and connectors[].target must reference an existing nodes[].id.'],
67
+ },
68
+ node: {
69
+ schemas: {
70
+ playNode: toJsonSchema(FlowPlayNodeSchema),
71
+ stateNode: toJsonSchema(FlowStateNodeSchema),
72
+ shapeNode: toJsonSchema(FlowShapeNodeSchema),
73
+ imageNode: toJsonSchema(FlowImageNodeSchema),
74
+ iconNode: toJsonSchema(FlowIconNodeSchema),
75
+ htmlNode: toJsonSchema(FlowHtmlNodeSchema),
76
+ },
77
+ notes: [
78
+ "imageNode.data.path must start with 'nodes/<id>/'.",
79
+ "scriptPath in playAction/statusAction is relative to nodes/<nodeId>/ and may not contain '..' or absolute paths.",
80
+ ],
81
+ },
82
+ connector: {
83
+ schemas: {
84
+ http: toJsonSchema(FlowHttpConnectorSchema),
85
+ event: toJsonSchema(FlowEventConnectorSchema),
86
+ queue: toJsonSchema(FlowQueueConnectorSchema),
87
+ default: toJsonSchema(FlowDefaultConnectorSchema),
88
+ },
89
+ notes: [],
90
+ },
91
+ action: {
92
+ schemas: {
93
+ playAction: toJsonSchema(PlayActionSchema),
94
+ statusAction: toJsonSchema(StatusActionSchema),
95
+ resetAction: toJsonSchema(ResetActionSchema),
96
+ statusReport: toJsonSchema(StatusReportSchema),
97
+ },
98
+ notes: [
99
+ "scriptPath in playAction/statusAction is relative to nodes/<nodeId>/ and may not contain '..' or absolute paths.",
100
+ ],
101
+ },
102
+ style: {
103
+ schemas: { style: toJsonSchema(StyleSchema) },
104
+ notes: [],
105
+ },
106
+ };
107
+
108
+ export function listSchemaCategories(): SchemaCategory[] {
109
+ return CATEGORIES.map((c) => ({ ...c }));
110
+ }
111
+
112
+ export function getSchemaCategory(name: string): SchemaPayload | null {
113
+ const payload = PAYLOADS[name];
114
+ if (!payload) return null;
115
+ return { schemas: { ...payload.schemas }, notes: [...payload.notes] };
116
+ }
117
+
118
+ export function schemaCategoryNames(): string[] {
119
+ return CATEGORIES.map((c) => c.name);
120
+ }
package/src/schema.ts CHANGED
@@ -84,7 +84,7 @@ export const PlayActionSchema = ScriptActionSchema;
84
84
  // invoked from the /reset endpoint. The studio kills every live play and
85
85
  // status script for the demo before running this script, so the running app
86
86
  // sees a clean baseline when wiping its state.
87
- const ResetActionSchema = ScriptActionSchema;
87
+ export const ResetActionSchema = ScriptActionSchema;
88
88
 
89
89
  // Long-running status script. Same spawn shape as ScriptAction (interpreter +
90
90
  // args + scriptPath) but no stdin payload and a much longer max lifetime since
@@ -524,49 +524,61 @@ const FlowNodeBaseShape = {
524
524
  id: z.string().min(1),
525
525
  };
526
526
 
527
+ export const FlowPlayNodeSchema = z
528
+ .object({
529
+ ...FlowNodeBaseShape,
530
+ type: z.literal('playNode'),
531
+ data: FlowPlayNodeDataSchema,
532
+ })
533
+ .strict();
534
+
535
+ export const FlowStateNodeSchema = z
536
+ .object({
537
+ ...FlowNodeBaseShape,
538
+ type: z.literal('stateNode'),
539
+ data: FlowStateNodeDataSchema,
540
+ })
541
+ .strict();
542
+
543
+ export const FlowShapeNodeSchema = z
544
+ .object({
545
+ ...FlowNodeBaseShape,
546
+ type: z.literal('shapeNode'),
547
+ data: FlowShapeNodeDataSchema,
548
+ })
549
+ .strict();
550
+
551
+ export const FlowImageNodeSchema = z
552
+ .object({
553
+ ...FlowNodeBaseShape,
554
+ type: z.literal('imageNode'),
555
+ data: FlowImageNodeDataSchema,
556
+ })
557
+ .strict();
558
+
559
+ export const FlowIconNodeSchema = z
560
+ .object({
561
+ ...FlowNodeBaseShape,
562
+ type: z.literal('iconNode'),
563
+ data: FlowIconNodeDataSchema,
564
+ })
565
+ .strict();
566
+
567
+ export const FlowHtmlNodeSchema = z
568
+ .object({
569
+ ...FlowNodeBaseShape,
570
+ type: z.literal('htmlNode'),
571
+ data: FlowHtmlNodeDataSchema,
572
+ })
573
+ .strict();
574
+
527
575
  const FlowNodeSchema = z.discriminatedUnion('type', [
528
- z
529
- .object({
530
- ...FlowNodeBaseShape,
531
- type: z.literal('playNode'),
532
- data: FlowPlayNodeDataSchema,
533
- })
534
- .strict(),
535
- z
536
- .object({
537
- ...FlowNodeBaseShape,
538
- type: z.literal('stateNode'),
539
- data: FlowStateNodeDataSchema,
540
- })
541
- .strict(),
542
- z
543
- .object({
544
- ...FlowNodeBaseShape,
545
- type: z.literal('shapeNode'),
546
- data: FlowShapeNodeDataSchema,
547
- })
548
- .strict(),
549
- z
550
- .object({
551
- ...FlowNodeBaseShape,
552
- type: z.literal('imageNode'),
553
- data: FlowImageNodeDataSchema,
554
- })
555
- .strict(),
556
- z
557
- .object({
558
- ...FlowNodeBaseShape,
559
- type: z.literal('iconNode'),
560
- data: FlowIconNodeDataSchema,
561
- })
562
- .strict(),
563
- z
564
- .object({
565
- ...FlowNodeBaseShape,
566
- type: z.literal('htmlNode'),
567
- data: FlowHtmlNodeDataSchema,
568
- })
569
- .strict(),
576
+ FlowPlayNodeSchema,
577
+ FlowStateNodeSchema,
578
+ FlowShapeNodeSchema,
579
+ FlowImageNodeSchema,
580
+ FlowIconNodeSchema,
581
+ FlowHtmlNodeSchema,
570
582
  ]);
571
583
 
572
584
  const FlowConnectorBaseShape = {
@@ -576,35 +588,43 @@ const FlowConnectorBaseShape = {
576
588
  label: z.string().optional(),
577
589
  };
578
590
 
591
+ export const FlowHttpConnectorSchema = z
592
+ .object({
593
+ ...FlowConnectorBaseShape,
594
+ kind: z.literal('http'),
595
+ method: HttpMethodSchema.optional(),
596
+ url: z.string().min(1).optional(),
597
+ })
598
+ .strict();
599
+
600
+ export const FlowEventConnectorSchema = z
601
+ .object({
602
+ ...FlowConnectorBaseShape,
603
+ kind: z.literal('event'),
604
+ eventName: z.string().min(1),
605
+ })
606
+ .strict();
607
+
608
+ export const FlowQueueConnectorSchema = z
609
+ .object({
610
+ ...FlowConnectorBaseShape,
611
+ kind: z.literal('queue'),
612
+ queueName: z.string().min(1),
613
+ })
614
+ .strict();
615
+
616
+ export const FlowDefaultConnectorSchema = z
617
+ .object({
618
+ ...FlowConnectorBaseShape,
619
+ kind: z.literal('default'),
620
+ })
621
+ .strict();
622
+
579
623
  const FlowConnectorSchema = z.discriminatedUnion('kind', [
580
- z
581
- .object({
582
- ...FlowConnectorBaseShape,
583
- kind: z.literal('http'),
584
- method: HttpMethodSchema.optional(),
585
- url: z.string().min(1).optional(),
586
- })
587
- .strict(),
588
- z
589
- .object({
590
- ...FlowConnectorBaseShape,
591
- kind: z.literal('event'),
592
- eventName: z.string().min(1),
593
- })
594
- .strict(),
595
- z
596
- .object({
597
- ...FlowConnectorBaseShape,
598
- kind: z.literal('queue'),
599
- queueName: z.string().min(1),
600
- })
601
- .strict(),
602
- z
603
- .object({
604
- ...FlowConnectorBaseShape,
605
- kind: z.literal('default'),
606
- })
607
- .strict(),
624
+ FlowHttpConnectorSchema,
625
+ FlowEventConnectorSchema,
626
+ FlowQueueConnectorSchema,
627
+ FlowDefaultConnectorSchema,
608
628
  ]);
609
629
 
610
630
  export const FlowSchema = z
@@ -641,6 +661,23 @@ export type Flow = z.infer<typeof FlowSchema>;
641
661
  export type FlowNode = z.infer<typeof FlowNodeSchema>;
642
662
  export type FlowConnector = z.infer<typeof FlowConnectorSchema>;
643
663
 
664
+ // Envelope-only flow shape for the `seeflow schema flow` surface. The full
665
+ // FlowSchema validates the whole graph; this companion schema describes the
666
+ // top-level shape without inlining every node + connector variant, so the
667
+ // runtime-introspectable JSON Schema stays compact. Authors drill into
668
+ // `seeflow schema node` / `seeflow schema connector` for the per-variant
669
+ // shapes. Not used for validation — only the catalog reads it.
670
+ export const FlowEnvelopeSchema = z
671
+ .object({
672
+ version: z.literal(2),
673
+ name: z.string().min(1),
674
+ description: z.string().optional(),
675
+ resetAction: ResetActionSchema.optional(),
676
+ nodes: z.array(z.unknown().describe('See `seeflow schema node`')),
677
+ connectors: z.array(z.unknown().describe('See `seeflow schema connector`')),
678
+ })
679
+ .strict();
680
+
644
681
  // =============================================================================
645
682
  // Style schema — keyed map of presentation overrides, side-table by id.
646
683
  // What lives on disk in <project>/.seeflow/style.json (optional file).
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
2
  import { resolve as resolvePath } from 'node:path';
3
3
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
4
4
  import { Hono } from 'hono';
@@ -7,6 +7,7 @@ import { type ProxyFacade, createApi } from './api.ts';
7
7
  import { createDemoRouter } from './demo.ts';
8
8
  import { type EventBus, createEventBus } from './events.ts';
9
9
  import { createMcpServer } from './mcp.ts';
10
+ import { seeflowHome } from './paths.ts';
10
11
  import { type ProcessSpawner, defaultProcessSpawner } from './process-spawner.ts';
11
12
  import { type RegistryWatcher, createRegistryWatcher } from './registry-watcher.ts';
12
13
  import { type Registry, createRegistry } from './registry.ts';
@@ -208,6 +209,7 @@ export interface ServeOptions extends CreateAppOptions {
208
209
  export function serve(options: ServeOptions = {}) {
209
210
  const port = options.port ?? 4321;
210
211
  const hostname = options.hostname ?? '0.0.0.0';
212
+ mkdirSync(seeflowHome(), { recursive: true });
211
213
  const app = createApp(options);
212
214
  return Bun.serve({ port, hostname, fetch: app.fetch });
213
215
  }