@tuongaz/seeflow 0.1.82 → 0.1.84

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.
Files changed (65) hide show
  1. package/dist/web/assets/{architectureDiagram-3BPJPVTR-C6jG13JR.js → architectureDiagram-3BPJPVTR-I46aqv2T.js} +1 -1
  2. package/dist/web/assets/{blockDiagram-GPEHLZMM-BCwHFzKE.js → blockDiagram-GPEHLZMM-C08px5xi.js} +1 -1
  3. package/dist/web/assets/{c4Diagram-AAUBKEIU-C5oIgyEj.js → c4Diagram-AAUBKEIU-iyIMetcW.js} +1 -1
  4. package/dist/web/assets/channel-X0ALvlnb.js +1 -0
  5. package/dist/web/assets/{chart-CUib8j1x.js → chart-BGNzA0s8.js} +1 -1
  6. package/dist/web/assets/{chunk-2J33WTMH-pmw8bFhw.js → chunk-2J33WTMH-CQFLt-Og.js} +1 -1
  7. package/dist/web/assets/{chunk-4BX2VUAB-CMvX_m4h.js → chunk-4BX2VUAB-M_HU7fhX.js} +1 -1
  8. package/dist/web/assets/{chunk-55IACEB6-D44BQXvr.js → chunk-55IACEB6-CZ6tcHFC.js} +1 -1
  9. package/dist/web/assets/{chunk-727SXJPM-DFEMoCvZ.js → chunk-727SXJPM-ZXC_1s3s.js} +1 -1
  10. package/dist/web/assets/{chunk-AQP2D5EJ-DDIMOnRk.js → chunk-AQP2D5EJ-D1nyTSxR.js} +1 -1
  11. package/dist/web/assets/{chunk-FMBD7UC4-LXLjZhT_.js → chunk-FMBD7UC4-D8IemyXd.js} +1 -1
  12. package/dist/web/assets/{chunk-ND2GUHAM-C8IKIBCh.js → chunk-ND2GUHAM-aT_PIEvu.js} +1 -1
  13. package/dist/web/assets/{chunk-QZHKN3VN-B0u1h0_j.js → chunk-QZHKN3VN-BKY6KT8K.js} +1 -1
  14. package/dist/web/assets/classDiagram-4FO5ZUOK-mwGbzVrI.js +1 -0
  15. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-mwGbzVrI.js +1 -0
  16. package/dist/web/assets/{code-block-Djnu184B.js → code-block-CMPt7Auf.js} +1 -1
  17. package/dist/web/assets/{cose-bilkent-S5V4N54A-BUNO7CMC.js → cose-bilkent-S5V4N54A-Cr3EeHXE.js} +1 -1
  18. package/dist/web/assets/{dagre-BM42HDAG-CdGFe41U.js → dagre-BM42HDAG-BfJiRz9t.js} +1 -1
  19. package/dist/web/assets/{diagram-2AECGRRQ-V2Y9lItI.js → diagram-2AECGRRQ-CZ1Rti0o.js} +1 -1
  20. package/dist/web/assets/{diagram-5GNKFQAL-CQ68SweE.js → diagram-5GNKFQAL-COe7jS3C.js} +1 -1
  21. package/dist/web/assets/{diagram-KO2AKTUF-DSbX7Of7.js → diagram-KO2AKTUF-lmIy3eJx.js} +1 -1
  22. package/dist/web/assets/{diagram-LMA3HP47-Cn2lxMxE.js → diagram-LMA3HP47-Dv9UOOZz.js} +1 -1
  23. package/dist/web/assets/{diagram-OG6HWLK6-DxpRae3_.js → diagram-OG6HWLK6-C8HoI5ul.js} +1 -1
  24. package/dist/web/assets/{erDiagram-TEJ5UH35-CcNV8XGY.js → erDiagram-TEJ5UH35-CvqQcwx_.js} +1 -1
  25. package/dist/web/assets/{flowDiagram-I6XJVG4X-FzlTb7Kj.js → flowDiagram-I6XJVG4X-B0f8FFr8.js} +1 -1
  26. package/dist/web/assets/{ganttDiagram-6RSMTGT7-CsZsdpk8.js → ganttDiagram-6RSMTGT7-C0FgETUZ.js} +1 -1
  27. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-cwaeRxXC.js → gitGraphDiagram-PVQCEYII-BSUklsqu.js} +1 -1
  28. package/dist/web/assets/{index-AGsIlqaG.js → index-Cnz1Cpa9.js} +5 -5
  29. package/dist/web/assets/{index.es-BLBZrQQg.js → index.es-CcTdlE2B.js} +1 -1
  30. package/dist/web/assets/{infoDiagram-5YYISTIA-BdoV8cwC.js → infoDiagram-5YYISTIA-qq3OPeKx.js} +1 -1
  31. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-DgF0aARl.js → ishikawaDiagram-YF4QCWOH-BxoH_vWZ.js} +1 -1
  32. package/dist/web/assets/{journeyDiagram-JHISSGLW-CrR2wfa8.js → journeyDiagram-JHISSGLW-ClnMLnWd.js} +1 -1
  33. package/dist/web/assets/{jspdf.es.min-Dl965zni.js → jspdf.es.min-C17Fkx-5.js} +3 -3
  34. package/dist/web/assets/{kanban-definition-UN3LZRKU-DsHEJCCs.js → kanban-definition-UN3LZRKU-BiBGcvgP.js} +1 -1
  35. package/dist/web/assets/{linear-BN0LZNw-.js → linear-D--UMRYt.js} +1 -1
  36. package/dist/web/assets/{markdown-NiIdJguA.js → markdown-D-hxiBfP.js} +1 -1
  37. package/dist/web/assets/{mermaid.core-tY2raeE_.js → mermaid.core-ClLm1p66.js} +4 -4
  38. package/dist/web/assets/{mindmap-definition-RKZ34NQL-DxwAweLP.js → mindmap-definition-RKZ34NQL-CLSXYvAW.js} +1 -1
  39. package/dist/web/assets/{pieDiagram-4H26LBE5-Bg9fR9kw.js → pieDiagram-4H26LBE5-CjvKFGOn.js} +1 -1
  40. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-BK1dS-rz.js → quadrantDiagram-W4KKPZXB-BUYqMtGH.js} +1 -1
  41. package/dist/web/assets/{requirementDiagram-4Y6WPE33-DQYhYuTB.js → requirementDiagram-4Y6WPE33-DNCHy1eI.js} +1 -1
  42. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-CcR27WzM.js → sankeyDiagram-5OEKKPKP-zoL2eKAM.js} +1 -1
  43. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-CgfAvO57.js → sequenceDiagram-3UESZ5HK-DePRNn1e.js} +1 -1
  44. package/dist/web/assets/{stateDiagram-AJRCARHV-DdU1AbEe.js → stateDiagram-AJRCARHV-uLinqFcr.js} +1 -1
  45. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-hyQj-KY3.js +1 -0
  46. package/dist/web/assets/{time-BfPOB-bQ.js → time-C9xT3sm_.js} +1 -1
  47. package/dist/web/assets/{timeline-definition-PNZ67QCA-CYQkkhD-.js → timeline-definition-PNZ67QCA-BnQVT-CY.js} +1 -1
  48. package/dist/web/assets/{vennDiagram-CIIHVFJN-CcmBbhR6.js → vennDiagram-CIIHVFJN-BUyqbvsU.js} +1 -1
  49. package/dist/web/assets/{wardley-L42UT6IY-CK4ciiRm.js → wardley-L42UT6IY-zz-5TvX0.js} +1 -1
  50. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-Do0eKBGx.js → wardleyDiagram-YWT4CUSO-DVnYIARL.js} +1 -1
  51. package/dist/web/assets/{xychartDiagram-2RQKCTM6-CSeQg6Oy.js → xychartDiagram-2RQKCTM6-BMxn2lA_.js} +1 -1
  52. package/dist/web/index.html +1 -1
  53. package/package.json +1 -1
  54. package/src/api.ts +44 -18
  55. package/src/cli-helpers.ts +3 -0
  56. package/src/cli-manifest.ts +81 -18
  57. package/src/cli.ts +121 -8
  58. package/src/mcp.ts +60 -11
  59. package/src/operations.ts +77 -2
  60. package/src/project-scanner.ts +18 -0
  61. package/src/schema-catalog.ts +23 -0
  62. package/dist/web/assets/channel-gvIm48Rt.js +0 -1
  63. package/dist/web/assets/classDiagram-4FO5ZUOK-eMNtcBZw.js +0 -1
  64. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-eMNtcBZw.js +0 -1
  65. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-CNXxJnrL.js +0 -1
@@ -133,9 +133,10 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
133
133
  name: 'register',
134
134
  synopsis: 'seeflow register [--path <dir>] [--flow <file>]',
135
135
  description:
136
- 'Register a demo repo with the studio. Reads <repoPath>/<flow> (defaulting ' +
137
- 'to ./flow.json), validates the schema, and writes an entry to ' +
138
- '~/.seeflow/registry.json. Alias of flows:register.',
136
+ 'Register a demo repo with the studio. Manifest-aware: when <repoPath>/seeflow.json ' +
137
+ 'exists, scans every declared flow under flows/<id>/flow.json and upserts one entry ' +
138
+ 'per flow. Otherwise reads <repoPath>/<flow> (defaulting to ./flow.json) as a ' +
139
+ 'single-flow project. Writes to ~/.seeflow/registry.json. Alias of flows:register.',
139
140
  category: 'flows',
140
141
  args: [],
141
142
  flags: [
@@ -143,20 +144,25 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
143
144
  {
144
145
  name: 'flow',
145
146
  valuePlaceholder: '<file>',
146
- description: 'Path to flow.json relative to repo root (default: flow.json)',
147
+ description:
148
+ 'Path to flow.json relative to repo root (default: flow.json). Ignored when seeflow.json exists.',
147
149
  },
148
150
  ],
149
151
  outputs: {
150
152
  okExample: { id: 'abc12345', slug: 'checkout' },
151
- errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
153
+ errorKinds: ['fileNotFound', 'badJson', 'badSchema', 'manifestInvalid'],
152
154
  },
153
155
  requiresStudio: false,
154
- examples: ['seeflow register', 'seeflow register --path ./my-app'],
156
+ examples: [
157
+ 'seeflow register',
158
+ 'seeflow register --path ./my-app',
159
+ 'seeflow register --path ./checkout # manifest-aware re-scan',
160
+ ],
155
161
  },
156
162
  {
157
163
  name: 'flows:register',
158
164
  synopsis: 'seeflow flows:register [--path <dir>] [--flow <file>]',
159
- description: 'Register a demo repo. Identical behaviour to `register`.',
165
+ description: 'Register a demo repo. Identical behaviour to `register` (manifest-aware).',
160
166
  category: 'flows',
161
167
  args: [],
162
168
  flags: [
@@ -164,27 +170,40 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
164
170
  {
165
171
  name: 'flow',
166
172
  valuePlaceholder: '<file>',
167
- description: 'Path to flow.json relative to repo root (default: flow.json)',
173
+ description:
174
+ 'Path to flow.json relative to repo root (default: flow.json). Ignored when seeflow.json exists.',
168
175
  },
169
176
  ],
170
177
  body: { schemaRef: 'RegisterBody' },
171
178
  outputs: {
172
179
  okExample: { id: 'abc12345', slug: 'checkout' },
173
- errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
180
+ errorKinds: ['fileNotFound', 'badJson', 'badSchema', 'manifestInvalid'],
174
181
  },
175
182
  requiresStudio: false,
176
183
  examples: ['seeflow flows:register --path ./my-app'],
177
184
  },
178
185
  {
179
186
  name: 'flows:list',
180
- synopsis: 'seeflow flows:list',
181
- description: 'List every registered flow with id, slug, name, repoPath, and valid flag.',
187
+ synopsis: 'seeflow flows:list [--project <p>]',
188
+ description:
189
+ 'List every registered flow with id, slug, name, repoPath, and valid flag. With ' +
190
+ '--project <p>, filters to one project and returns { projectSlug, flows: [{ flowSlug, ' +
191
+ 'name, icon?, isDefault, valid }] }.',
182
192
  category: 'flows',
183
193
  args: [],
184
- flags: [],
185
- outputs: { okExample: { flows: [{ id: 'abc12345', slug: 'checkout', name: 'Checkout' }] } },
194
+ flags: [
195
+ {
196
+ name: 'project',
197
+ valuePlaceholder: '<p>',
198
+ description: 'Project slug — filters output to flows under this project',
199
+ },
200
+ ],
201
+ outputs: {
202
+ okExample: { flows: [{ id: 'abc12345', slug: 'checkout', name: 'Checkout' }] },
203
+ errorKinds: ['projectNotFound'],
204
+ },
186
205
  requiresStudio: false,
187
- examples: ['seeflow flows:list'],
206
+ examples: ['seeflow flows:list', 'seeflow flows:list --project order-pipeline'],
188
207
  },
189
208
  {
190
209
  name: 'flows:summary',
@@ -488,6 +507,33 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
488
507
  'seeflow projects:create --path ./checkout --name "Checkout" --description "Cart + payments flow"',
489
508
  ],
490
509
  },
510
+ {
511
+ name: 'projects:list',
512
+ synopsis: 'seeflow projects:list',
513
+ description:
514
+ 'List every registered project: groups the registry by projectSlug and reads each ' +
515
+ 'seeflow.json for the human-readable name + defaultFlow. Falls back to the ' +
516
+ "projectSlug and the registry's isDefault entry when a manifest is missing or " +
517
+ 'malformed, so a partially-broken project still surfaces.',
518
+ category: 'project',
519
+ args: [],
520
+ flags: [],
521
+ outputs: {
522
+ okExample: {
523
+ projects: [
524
+ {
525
+ projectSlug: 'checkout',
526
+ name: 'Checkout',
527
+ defaultFlow: 'main',
528
+ flowCount: 2,
529
+ repoPath: '/abs/path/to/.seeflow/checkout',
530
+ },
531
+ ],
532
+ },
533
+ },
534
+ requiresStudio: false,
535
+ examples: ['seeflow projects:list'],
536
+ },
491
537
  // ---- nodes -------------------------------------------------------------
492
538
  {
493
539
  name: 'nodes:add',
@@ -823,7 +869,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
823
869
  },
824
870
  {
825
871
  name: 'schema',
826
- synopsis: 'seeflow schema [<category>] [--jq <filter>]',
872
+ synopsis: 'seeflow schema [<category> [<subname>]] [--jq <filter>]',
827
873
  description:
828
874
  'Introspect the SeeFlow flow.json / style.json / spec.json schemas at ' +
829
875
  'runtime. Call without arguments to list the six categories (flow, node, ' +
@@ -831,9 +877,13 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
831
877
  'get its full JSON Schema(s) (Draft-07) plus a `notes` array of cross-' +
832
878
  "field invariants the schema can't express. The `node` payload includes " +
833
879
  "all 13 flat variants (including type:'component', whose `spec` field " +
834
- 'lives in a sidecar — drill into `componentSpec` for that shape). Use ' +
835
- 'this before authoring any flow.json / spec.json write never memorise ' +
836
- 'field shapes.\n\n' +
880
+ 'lives in a sidecar — drill into `componentSpec` for that shape).\n\n' +
881
+ 'Pass a third positional `subname` to get just one named schema within ' +
882
+ 'the category — e.g. `seeflow schema node component`, `seeflow schema ' +
883
+ 'node rectangle`, `seeflow schema action playAction`. The category-' +
884
+ 'level `notes` ride along unchanged because the cross-variant invariants ' +
885
+ 'still apply when looking at one variant. Use this before authoring any ' +
886
+ 'flow.json / spec.json write — never memorise field shapes.\n\n' +
837
887
  'Pass --jq <filter> to extract a slice of the response with a jq path ' +
838
888
  'expression. Supported subset: identity (`.`), field access ' +
839
889
  '(`.foo.bar`), bracket access (`.["foo"]`, `.[3]`, negative indices ' +
@@ -849,6 +899,16 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
849
899
  required: false,
850
900
  description: 'One of: flow, node, connector, action, componentSpec, style',
851
901
  },
902
+ {
903
+ name: 'subname',
904
+ required: false,
905
+ description:
906
+ 'Optional named schema within the category — e.g. for `node`: ' +
907
+ 'rectangle, ellipse, sticky, text, database, server, user, queue, ' +
908
+ 'cloud, image, html, icon, component. For `action`: playAction, ' +
909
+ 'statusAction, resetAction, statusReport, componentAction. For ' +
910
+ '`componentSpec`: componentSpec, componentSpecElement.',
911
+ },
852
912
  ],
853
913
  flags: [
854
914
  {
@@ -868,6 +928,9 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
868
928
  examples: [
869
929
  'seeflow schema',
870
930
  'seeflow schema node',
931
+ 'seeflow schema node component',
932
+ 'seeflow schema node rectangle',
933
+ 'seeflow schema action playAction',
871
934
  'seeflow schema connector',
872
935
  'seeflow schema componentSpec',
873
936
  'seeflow schema node --jq .schemas.rectangle',
package/src/cli.ts CHANGED
@@ -178,6 +178,8 @@ if (argv.includes('--version') || argv.includes('-v')) {
178
178
  await runRegister();
179
179
  } else if (sub === 'projects:create') {
180
180
  await runProjectsCreate();
181
+ } else if (sub === 'projects:list') {
182
+ await runProjectsList();
181
183
  } else if (sub === 'flows:list') {
182
184
  await runFlowsList();
183
185
  } else if (sub === 'flows:summary') {
@@ -243,10 +245,11 @@ Usage:
243
245
  Commands (work without a running studio):
244
246
  start Start the SeeFlow Studio server (default port 4321) — default when no command is given
245
247
  stop Stop a background studio instance
246
- register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
247
- flows:register Register a demo repo
248
+ register Register a demo repo. Manifest-aware: when <repoPath>/seeflow.json exists, re-scans every declared flow; otherwise reads <repoPath>/<flow> (defaults to flow.json) as a single-flow project (alias of flows:register)
249
+ flows:register Register a demo repo (manifest-aware — same behaviour as register)
248
250
  projects:create Scaffold a new project (writes <path>/seeflow.json + <path>/flows/main/flow.json) — (--path <dir> --name <name> [--description <text>])
249
- flows:list List registered flows
251
+ projects:list List every registered project with projectSlug, name, defaultFlow, flowCount
252
+ flows:list List registered flows. With --project <p>, filters to one project (returns flowSlug, name, icon?, isDefault per flow)
250
253
  flows:summary List registered flows (id + name + description only)
251
254
  flows:get Get flow details (--project <p> --flow <f>)
252
255
  flows:graph List nodes + connectors without inlined file content (--project <p> --flow <f>)
@@ -265,8 +268,11 @@ Commands (work without a running studio):
265
268
  connectors:patch <connId> Patch a connector (--project <p> --flow <f>) [--json/--file/--stdin]
266
269
  connectors:delete <connId> Delete a connector (--project <p> --flow <f>)
267
270
  validate Schema-validate a flow.json (--file <file> [--style <file>])
268
- schema [<category>] Get the flow.json schema. No arg → category index;
269
- category arg full JSON Schema(s) for that category
271
+ schema [<category> [<subname>]]
272
+ Get the flow.json schema. No arg category index;
273
+ category arg → full JSON Schema(s) for that category;
274
+ subname arg → just that named schema (e.g.
275
+ 'schema node component', 'schema node rectangle')
270
276
  ids <type> <count> Print <count> short ids of the given <type>, one per
271
277
  line. <type> is 'node' (-> 'node-...') or 'connector'
272
278
  (-> 'conn-...'). <count> is 1..100. Call once per type
@@ -591,12 +597,24 @@ function isEsrch(err: unknown): boolean {
591
597
 
592
598
  async function runRegister() {
593
599
  const repoPath = resolve(flagValue('path') ?? '.');
594
- const demoPathArg = flagValue('flow') ?? DEFAULT_FLOW_PATH;
595
600
 
601
+ // Manifest-aware path: if <repoPath>/seeflow.json is on disk, scan and
602
+ // upsert every declared flow in one shot.
603
+ if (existsSync(join(repoPath, 'seeflow.json'))) {
604
+ await runRegisterManifest(repoPath);
605
+ return;
606
+ }
607
+
608
+ // Legacy single-flow path: pre-manifest projects (and skill tests that
609
+ // exercise registerFlow directly) still pass a bare flow.json at the
610
+ // root. Read it, schema-validate, upsert one entry.
611
+ const demoPathArg = flagValue('flow') ?? DEFAULT_FLOW_PATH;
596
612
  const fullPath = isAbsolute(demoPathArg) ? demoPathArg : join(repoPath, demoPathArg);
597
613
  if (!existsSync(fullPath)) {
598
614
  console.error(`No demo file at ${fullPath}`);
599
- console.error(`Create ${DEFAULT_FLOW_PATH} in your repo, or pass --flow <path>.`);
615
+ console.error(
616
+ `Create ${DEFAULT_FLOW_PATH} in your repo, or pass --flow <path>. For manifest-driven projects, place a seeflow.json at the repo root.`,
617
+ );
600
618
  process.exit(1);
601
619
  }
602
620
 
@@ -640,6 +658,48 @@ async function runRegister() {
640
658
  }
641
659
  }
642
660
 
661
+ async function runRegisterManifest(repoPath: string) {
662
+ const outcome = registerProject({ repoPath });
663
+ if (outcome.kind !== 'ok') {
664
+ switch (outcome.kind) {
665
+ case 'manifest-invalid':
666
+ console.error(`${join(repoPath, 'seeflow.json')} is invalid: ${outcome.message}`);
667
+ process.exit(2);
668
+ break;
669
+ case 'manifest-missing':
670
+ // Defensive — runRegister gated on existsSync, so this should not fire.
671
+ console.error(`No seeflow.json at ${repoPath}`);
672
+ process.exit(3);
673
+ break;
674
+ case 'legacy-root-flow':
675
+ console.error(
676
+ `${repoPath} has a legacy root flow.json but no seeflow.json. Migrate it into the new flows/<id>/ layout before re-registering.`,
677
+ );
678
+ process.exit(3);
679
+ break;
680
+ case 'flow-json-missing':
681
+ console.error(
682
+ `Manifest declares flow "${outcome.flowId}" but ${outcome.flowPath} is missing.`,
683
+ );
684
+ process.exit(3);
685
+ break;
686
+ }
687
+ process.exit(1);
688
+ }
689
+
690
+ const pid = readPid();
691
+ const live = pid !== undefined && isPidAlive(pid);
692
+ const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
693
+ const baseUrl = live ? (overrideUrl ?? studioUrl(readConfig())) : null;
694
+
695
+ for (const entry of outcome.entries) {
696
+ const tail = baseUrl
697
+ ? ` → ${baseUrl}/projects/${outcome.projectSlug}/flows/${entry.flowSlug}`
698
+ : ` (slug: ${entry.slug})`;
699
+ console.log(`Registered "${entry.name}"${tail}`);
700
+ }
701
+ }
702
+
643
703
  async function ensureStudioRunning(url: string, port: number, noStart: boolean) {
644
704
  if (await healthOk(url)) return;
645
705
 
@@ -706,10 +766,23 @@ async function runProjectsCreate() {
706
766
 
707
767
  async function runFlowsList() {
708
768
  const ops = createCliOperations();
769
+ const project = flagValue('project');
770
+ if (project) {
771
+ const result = ops.listFlowsByProject(project);
772
+ if (result.kind !== 'ok') printOutcome(result);
773
+ printOk({ projectSlug: result.data.projectSlug, flows: result.data.flows });
774
+ return;
775
+ }
709
776
  const result = ops.listFlows();
710
777
  printOk({ flows: result.data });
711
778
  }
712
779
 
780
+ function runProjectsList() {
781
+ const ops = createCliOperations();
782
+ const result = ops.listProjects();
783
+ printOk({ projects: result.data });
784
+ }
785
+
713
786
  async function runFlowsSummary() {
714
787
  const ops = createCliOperations();
715
788
  const result = ops.listFlowsSummary();
@@ -1025,8 +1098,10 @@ async function runValidate() {
1025
1098
 
1026
1099
  async function runSchema() {
1027
1100
  const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
1101
+ const subname = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
1028
1102
  const jqFilter = flagValue('jq');
1029
- const { listSchemaCategories, getSchemaCategory } = await import('./schema-catalog.ts');
1103
+ const { listSchemaCategories, getSchemaCategory, getCategorySubschema, listCategorySubnames } =
1104
+ await import('./schema-catalog.ts');
1030
1105
  if (!category) {
1031
1106
  const base = { categories: listSchemaCategories() };
1032
1107
  if (jqFilter !== undefined) {
@@ -1034,6 +1109,44 @@ async function runSchema() {
1034
1109
  }
1035
1110
  printOk(base);
1036
1111
  }
1112
+ // Drill into a single named schema within the category — e.g.
1113
+ // `seeflow schema node component`. Notes ride along unchanged because the
1114
+ // cross-variant invariants (image path prefix, scriptPath rooting, etc.)
1115
+ // are still relevant when you're looking at one variant.
1116
+ if (subname) {
1117
+ const single = getCategorySubschema(category as string, subname);
1118
+ if (!single) {
1119
+ const availableSubs = listCategorySubnames(category as string);
1120
+ if (availableSubs === null) {
1121
+ const available = listSchemaCategories().map((c) => c.name);
1122
+ const message = `unknown schema category: ${category}`;
1123
+ process.stderr.write(
1124
+ `${JSON.stringify({ error: message, code: 'notFound', available })}\n`,
1125
+ );
1126
+ process.exit(3);
1127
+ }
1128
+ const message = `unknown schema subname: ${subname}`;
1129
+ process.stderr.write(
1130
+ `${JSON.stringify({
1131
+ error: message,
1132
+ code: 'notFound',
1133
+ category,
1134
+ available: availableSubs,
1135
+ })}\n`,
1136
+ );
1137
+ process.exit(3);
1138
+ }
1139
+ const base = {
1140
+ name: category,
1141
+ subname,
1142
+ schemas: single.schemas,
1143
+ notes: single.notes,
1144
+ };
1145
+ if (jqFilter !== undefined) {
1146
+ printOk({ name: category, subname, result: applyJqOrDie(base, jqFilter) });
1147
+ }
1148
+ printOk(base);
1149
+ }
1037
1150
  const payload = getSchemaCategory(category as string);
1038
1151
  if (!payload) {
1039
1152
  const available = listSchemaCategories().map((c) => c.name);
package/src/mcp.ts CHANGED
@@ -29,7 +29,13 @@ import {
29
29
  flowBulkNonEmpty,
30
30
  } from './operations.ts';
31
31
  import type { Registry } from './registry.ts';
32
- import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
32
+ import {
33
+ getCategorySubschema,
34
+ getSchemaCategory,
35
+ listCategorySubnames,
36
+ listSchemaCategories,
37
+ schemaCategoryNames,
38
+ } from './schema-catalog.ts';
33
39
  import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
34
40
  import type { FlowWatcher } from './watcher.ts';
35
41
 
@@ -280,13 +286,18 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
280
286
  name: 'seeflow_schema',
281
287
  description:
282
288
  'Get the SeeFlow flow.json / spec.json schemas. Call with no args for a ' +
283
- "category index; call with `name` for one category's full JSON Schemas. " +
284
- 'Use this to learn what a node, connector, action, component spec, or ' +
285
- 'flow envelope looks like before authoring writes. Categories: `flow`, ' +
286
- '`node` (13 flat variants rectangle/ellipse/sticky/text/database/server/' +
287
- 'user/queue/cloud/image/html/icon/component), `connector`, `action` ' +
288
- '(playAction/statusAction/resetAction/statusReport/componentAction), ' +
289
- "`componentSpec` (sidecar shape for type:'component' nodes), `style`.",
289
+ "category index; call with `name` for one category's full JSON Schemas; " +
290
+ 'call with `name` + `subname` for just one named schema within that ' +
291
+ "category (e.g. name='node', subname='component' just the component " +
292
+ "node variant; name='node', subname='rectangle' just the rectangle " +
293
+ "node variant; name='action', subname='playAction' → just the playAction " +
294
+ 'shape). Use this to learn what a node, connector, action, component ' +
295
+ 'spec, or flow envelope looks like before authoring writes. Categories: ' +
296
+ '`flow`, `node` (13 flat variants — rectangle/ellipse/sticky/text/' +
297
+ 'database/server/user/queue/cloud/image/html/icon/component), ' +
298
+ '`connector`, `action` (playAction/statusAction/resetAction/statusReport/' +
299
+ "componentAction), `componentSpec` (sidecar shape for type:'component' " +
300
+ 'nodes), `style`.',
290
301
  inputSchema: {
291
302
  type: 'object',
292
303
  properties: {
@@ -294,20 +305,58 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
294
305
  type: 'string',
295
306
  description: 'Optional category name. Omit for the index.',
296
307
  },
308
+ subname: {
309
+ type: 'string',
310
+ description:
311
+ 'Optional named schema within the category (requires `name`). For ' +
312
+ "name='node': rectangle, ellipse, sticky, text, database, server, " +
313
+ 'user, queue, cloud, image, html, icon, component. For ' +
314
+ "name='action': playAction, statusAction, resetAction, statusReport, " +
315
+ "componentAction. For name='componentSpec': componentSpec, " +
316
+ 'componentSpecElement.',
317
+ },
297
318
  },
298
319
  additionalProperties: false,
299
320
  },
300
321
  handler: async (args) => {
301
- const name =
322
+ const argObj =
302
323
  args && typeof args === 'object' && !Array.isArray(args)
303
- ? (args as { name?: unknown }).name
304
- : undefined;
324
+ ? (args as { name?: unknown; subname?: unknown })
325
+ : {};
326
+ const name = argObj.name;
327
+ const subname = argObj.subname;
305
328
  if (name === undefined || name === null || name === '') {
329
+ if (subname !== undefined && subname !== null && subname !== '') {
330
+ return errorResult('Invalid arguments: `subname` requires `name` to be set');
331
+ }
306
332
  return okResult({ categories: listSchemaCategories() });
307
333
  }
308
334
  if (typeof name !== 'string') {
309
335
  return errorResult('Invalid arguments: `name` must be a string when present');
310
336
  }
337
+ if (subname !== undefined && subname !== null && subname !== '') {
338
+ if (typeof subname !== 'string') {
339
+ return errorResult('Invalid arguments: `subname` must be a string when present');
340
+ }
341
+ const single = getCategorySubschema(name, subname);
342
+ if (single) {
343
+ return okResult({
344
+ name,
345
+ subname,
346
+ schemas: single.schemas,
347
+ notes: single.notes,
348
+ });
349
+ }
350
+ const availableSubs = listCategorySubnames(name);
351
+ if (availableSubs === null) {
352
+ return errorResult(
353
+ `unknown schema category: ${name} (available: ${schemaCategoryNames().join(', ')})`,
354
+ );
355
+ }
356
+ return errorResult(
357
+ `unknown schema subname: ${subname} in category ${name} (available: ${availableSubs.join(', ')})`,
358
+ );
359
+ }
311
360
  const payload = getSchemaCategory(name);
312
361
  if (!payload) {
313
362
  return errorResult(
package/src/operations.ts CHANGED
@@ -22,8 +22,8 @@ import {
22
22
  removeNodeDir,
23
23
  writeNodeFile,
24
24
  } from './node-files.ts';
25
- import { scanProject } from './project-scanner.ts';
26
- import { type Registry, slugify } from './registry.ts';
25
+ import { readProjectManifest, scanProject } from './project-scanner.ts';
26
+ import { type FlowEntry, type Registry, slugify } from './registry.ts';
27
27
  import {
28
28
  ColorTokenSchema,
29
29
  ComponentSpecSchema,
@@ -404,6 +404,31 @@ export interface CreateProjectSuccess {
404
404
 
405
405
  export type ListFlowsOutcome = { kind: 'ok'; data: FlowListItem[] };
406
406
 
407
+ export interface ProjectListItem {
408
+ projectSlug: string;
409
+ name: string;
410
+ description?: string;
411
+ defaultFlow: string;
412
+ flowCount: number;
413
+ repoPath: string;
414
+ }
415
+
416
+ export type ListProjectsOutcome = { kind: 'ok'; data: ProjectListItem[] };
417
+
418
+ export interface ProjectFlowListItem {
419
+ id: string;
420
+ slug: string;
421
+ flowSlug: string;
422
+ name: string;
423
+ icon?: string;
424
+ isDefault: boolean;
425
+ valid: boolean;
426
+ }
427
+
428
+ export type ListFlowsByProjectOutcome =
429
+ | { kind: 'ok'; data: { projectSlug: string; flows: ProjectFlowListItem[] } }
430
+ | { kind: 'projectNotFound'; projectSlug: string };
431
+
407
432
  // Minimal projection for agent/CLI discovery — `description` and `name` come
408
433
  // from the live watcher snapshot when available so author edits to flow.json
409
434
  // surface immediately; fall back to the registry value at startup before
@@ -951,6 +976,52 @@ export function listDemosImpl(deps: OperationsDeps): ListFlowsOutcome {
951
976
  return { kind: 'ok', data };
952
977
  }
953
978
 
979
+ export function listProjectsImpl(deps: OperationsDeps): ListProjectsOutcome {
980
+ const grouped = new Map<string, FlowEntry[]>();
981
+ for (const entry of deps.registry.list()) {
982
+ const existing = grouped.get(entry.projectSlug);
983
+ if (existing) existing.push(entry);
984
+ else grouped.set(entry.projectSlug, [entry]);
985
+ }
986
+ const data: ProjectListItem[] = [];
987
+ for (const [projectSlug, entries] of grouped) {
988
+ const head = entries[0];
989
+ if (!head) continue;
990
+ const manifest = readProjectManifest(head.repoPath);
991
+ const defaultEntry = entries.find((e) => e.isDefault) ?? head;
992
+ data.push({
993
+ projectSlug,
994
+ name: manifest?.name ?? projectSlug,
995
+ ...(manifest?.description !== undefined ? { description: manifest.description } : {}),
996
+ defaultFlow: manifest?.defaultFlow ?? defaultEntry.flowSlug,
997
+ flowCount: entries.length,
998
+ repoPath: head.repoPath,
999
+ });
1000
+ }
1001
+ return { kind: 'ok', data };
1002
+ }
1003
+
1004
+ export function listFlowsByProjectImpl(
1005
+ deps: OperationsDeps,
1006
+ projectSlug: string,
1007
+ ): ListFlowsByProjectOutcome {
1008
+ const entries = deps.registry.list().filter((e) => e.projectSlug === projectSlug);
1009
+ if (entries.length === 0) return { kind: 'projectNotFound', projectSlug };
1010
+ const flows: ProjectFlowListItem[] = entries.map((e) => {
1011
+ const fileExists = existsSync(resolveFilePath(e.repoPath, e.flowPath));
1012
+ return {
1013
+ id: e.id,
1014
+ slug: e.slug,
1015
+ flowSlug: e.flowSlug,
1016
+ name: e.name,
1017
+ ...(e.icon !== undefined ? { icon: e.icon } : {}),
1018
+ isDefault: e.isDefault,
1019
+ valid: e.valid && fileExists,
1020
+ };
1021
+ });
1022
+ return { kind: 'ok', data: { projectSlug, flows } };
1023
+ }
1024
+
954
1025
  export function listFlowsSummaryImpl(deps: OperationsDeps): ListFlowsSummaryOutcome {
955
1026
  const { registry, watcher } = deps;
956
1027
  const data = registry.list().map((e) => {
@@ -1899,6 +1970,8 @@ export async function applyLayoutImpl(
1899
1970
  export interface Operations {
1900
1971
  listFlows(): ReturnType<typeof listDemosImpl>;
1901
1972
  listFlowsSummary(): ReturnType<typeof listFlowsSummaryImpl>;
1973
+ listProjects(): ReturnType<typeof listProjectsImpl>;
1974
+ listFlowsByProject(projectSlug: string): ReturnType<typeof listFlowsByProjectImpl>;
1902
1975
  getFlow(id: string): ReturnType<typeof getFlowImpl>;
1903
1976
  getFlowGraph(id: string): ReturnType<typeof getFlowGraphImpl>;
1904
1977
  getNode(flowId: string, nodeId: string): ReturnType<typeof getNodeImpl>;
@@ -1946,6 +2019,8 @@ export function createOperations(deps: OperationsDeps): Operations {
1946
2019
  return {
1947
2020
  listFlows: () => listDemosImpl(deps),
1948
2021
  listFlowsSummary: () => listFlowsSummaryImpl(deps),
2022
+ listProjects: () => listProjectsImpl(deps),
2023
+ listFlowsByProject: (projectSlug) => listFlowsByProjectImpl(deps, projectSlug),
1949
2024
  getFlow: (id) => getFlowImpl(deps, id),
1950
2025
  getFlowGraph: (id) => getFlowGraphImpl(deps, id),
1951
2026
  getNode: (flowId, nodeId) => getNodeImpl(deps, flowId, nodeId),
@@ -47,6 +47,24 @@ export type ScanResult =
47
47
  const MANIFEST_FILENAME = 'seeflow.json';
48
48
  const LEGACY_FLOW_FILENAME = 'flow.json';
49
49
 
50
+ /**
51
+ * Best-effort manifest read for listing routes / CLI listing verbs.
52
+ * Returns `null` when the manifest is missing or malformed — callers fall
53
+ * back to derived defaults (projectSlug, isDefault entry) so one broken
54
+ * project does not collapse the whole listing.
55
+ */
56
+ export function readProjectManifest(repoPath: string): SeeflowManifest | null {
57
+ const manifestPath = join(repoPath, MANIFEST_FILENAME);
58
+ if (!existsSync(manifestPath)) return null;
59
+ try {
60
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
61
+ const parsed = SeeflowManifestSchema.safeParse(raw);
62
+ return parsed.success ? parsed.data : null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
50
68
  export function scanProject(repoPath: string): ScanResult {
51
69
  const manifestPath = join(repoPath, MANIFEST_FILENAME);
52
70
  if (!existsSync(manifestPath)) {
@@ -151,3 +151,26 @@ export function getSchemaCategory(name: string): SchemaPayload | null {
151
151
  export function schemaCategoryNames(): string[] {
152
152
  return CATEGORIES.map((c) => c.name);
153
153
  }
154
+
155
+ // Drill into one named schema inside a category — e.g. ('node', 'rectangle')
156
+ // returns just the rectangle variant. The category-level notes ride along
157
+ // unchanged because they describe cross-variant invariants the caller still
158
+ // needs (image path prefix, scriptPath rooting, etc.). Returns null if either
159
+ // the category or the subname is unknown; callers use listCategorySubnames
160
+ // to build a helpful "available" list in that case.
161
+ export function getCategorySubschema(category: string, subname: string): SchemaPayload | null {
162
+ const payload = PAYLOADS[category];
163
+ if (!payload) return null;
164
+ const schema = payload.schemas[subname];
165
+ if (schema === undefined) return null;
166
+ return { schemas: { [subname]: schema }, notes: [...payload.notes] };
167
+ }
168
+
169
+ // Returns the subname keys (rectangle, ellipse, …) for a known category, or
170
+ // null if the category itself is unknown. Used to surface "available" lists
171
+ // on lookup failures so the error message is self-correcting.
172
+ export function listCategorySubnames(category: string): string[] | null {
173
+ const payload = PAYLOADS[category];
174
+ if (!payload) return null;
175
+ return Object.keys(payload.schemas);
176
+ }
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-tY2raeE_.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as e,C as t}from"./chunk-727SXJPM-DFEMoCvZ.js";import{a as i}from"./mermaid.core-tY2raeE_.js";import"./index-AGsIlqaG.js";import"./chunk-FMBD7UC4-LXLjZhT_.js";import"./chunk-ND2GUHAM-C8IKIBCh.js";import"./chunk-55IACEB6-D44BQXvr.js";import"./chunk-2J33WTMH-pmw8bFhw.js";import"./purify.es-CLGrRn1w.js";import"./step-CWvwoXpJ.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as e,C as t}from"./chunk-727SXJPM-DFEMoCvZ.js";import{a as i}from"./mermaid.core-tY2raeE_.js";import"./index-AGsIlqaG.js";import"./chunk-FMBD7UC4-LXLjZhT_.js";import"./chunk-ND2GUHAM-C8IKIBCh.js";import"./chunk-55IACEB6-D44BQXvr.js";import"./chunk-2J33WTMH-pmw8bFhw.js";import"./purify.es-CLGrRn1w.js";import"./step-CWvwoXpJ.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{b as r,a as e,s as a,S as s}from"./chunk-AQP2D5EJ-DDIMOnRk.js";import{a as i}from"./mermaid.core-tY2raeE_.js";import"./index-AGsIlqaG.js";import"./chunk-55IACEB6-D44BQXvr.js";import"./chunk-2J33WTMH-pmw8bFhw.js";import"./purify.es-CLGrRn1w.js";import"./step-CWvwoXpJ.js";var n={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{n as diagram};