@tuongaz/seeflow 0.1.91 → 0.1.93

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 (68) hide show
  1. package/dist/web/assets/{architectureDiagram-3BPJPVTR-CLQb7I2I.js → architectureDiagram-3BPJPVTR-vgRdQoho.js} +1 -1
  2. package/dist/web/assets/{blockDiagram-GPEHLZMM-DN0OjtOL.js → blockDiagram-GPEHLZMM-D1bbs0Ix.js} +1 -1
  3. package/dist/web/assets/{c4Diagram-AAUBKEIU-DMVbVbvl.js → c4Diagram-AAUBKEIU-BHmsWf1o.js} +1 -1
  4. package/dist/web/assets/channel-C-YVVNMU.js +1 -0
  5. package/dist/web/assets/{chart-Bx3ReVE3.js → chart-CawZdlOV.js} +1 -1
  6. package/dist/web/assets/{chunk-2J33WTMH-9vQ1xqy3.js → chunk-2J33WTMH-BLQaRMqq.js} +1 -1
  7. package/dist/web/assets/{chunk-4BX2VUAB-DRGxmqVG.js → chunk-4BX2VUAB-DTivSBmA.js} +1 -1
  8. package/dist/web/assets/{chunk-55IACEB6-DlboNFJr.js → chunk-55IACEB6-DZ5Ond8O.js} +1 -1
  9. package/dist/web/assets/{chunk-727SXJPM-2Se8RGwW.js → chunk-727SXJPM-CwKBkYmr.js} +1 -1
  10. package/dist/web/assets/{chunk-AQP2D5EJ-DqZEBh23.js → chunk-AQP2D5EJ-CAiwEMmH.js} +1 -1
  11. package/dist/web/assets/{chunk-FMBD7UC4-BJX_21R2.js → chunk-FMBD7UC4-DCWBhj6w.js} +1 -1
  12. package/dist/web/assets/{chunk-ND2GUHAM-DFBKXknR.js → chunk-ND2GUHAM-vLte473x.js} +1 -1
  13. package/dist/web/assets/{chunk-QZHKN3VN-CHnWLNTw.js → chunk-QZHKN3VN-Bcu6ixss.js} +1 -1
  14. package/dist/web/assets/classDiagram-4FO5ZUOK-BklVRjbL.js +1 -0
  15. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BklVRjbL.js +1 -0
  16. package/dist/web/assets/{code-block-IJZcqBQa.js → code-block-Q3inQ3vw.js} +1 -1
  17. package/dist/web/assets/{cose-bilkent-S5V4N54A-B0LjjLKu.js → cose-bilkent-S5V4N54A-Cb8kZ6Km.js} +1 -1
  18. package/dist/web/assets/{dagre-BM42HDAG-mMg_Ia_X.js → dagre-BM42HDAG-Xws4A-Mi.js} +1 -1
  19. package/dist/web/assets/{diagram-2AECGRRQ-BUy1Taew.js → diagram-2AECGRRQ-S1kziNDg.js} +1 -1
  20. package/dist/web/assets/{diagram-5GNKFQAL-DH_tg4Cb.js → diagram-5GNKFQAL-eoeXtzaC.js} +1 -1
  21. package/dist/web/assets/{diagram-KO2AKTUF-CE1rkBey.js → diagram-KO2AKTUF-DktxY0CQ.js} +1 -1
  22. package/dist/web/assets/{diagram-LMA3HP47-DS7ee5II.js → diagram-LMA3HP47-myBeIjhs.js} +1 -1
  23. package/dist/web/assets/{diagram-OG6HWLK6-DzCf3KBM.js → diagram-OG6HWLK6-CFj88ujv.js} +1 -1
  24. package/dist/web/assets/{erDiagram-TEJ5UH35-Spog-6po.js → erDiagram-TEJ5UH35-WaEyv1iP.js} +1 -1
  25. package/dist/web/assets/{flowDiagram-I6XJVG4X-0VAojPZO.js → flowDiagram-I6XJVG4X-DAZ3T2Zd.js} +1 -1
  26. package/dist/web/assets/{ganttDiagram-6RSMTGT7-DcjKkuMU.js → ganttDiagram-6RSMTGT7-uY09PoQq.js} +1 -1
  27. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-Bq9UIINL.js → gitGraphDiagram-PVQCEYII-BSJHmF4Z.js} +1 -1
  28. package/dist/web/assets/index-5X7OVal6.js +8624 -0
  29. package/dist/web/assets/{index-CWGdrwRY.css → index-I8_SAWCr.css} +1 -1
  30. package/dist/web/assets/{index.es-BUxcZ9iL.js → index.es-BJkNyJb3.js} +1 -1
  31. package/dist/web/assets/{infoDiagram-5YYISTIA-yxIJiF1X.js → infoDiagram-5YYISTIA-BJIRmQdX.js} +1 -1
  32. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-BkcKepSB.js → ishikawaDiagram-YF4QCWOH-Dk3i-Xkk.js} +1 -1
  33. package/dist/web/assets/{journeyDiagram-JHISSGLW-KfXgvDdv.js → journeyDiagram-JHISSGLW-DnJaLB-P.js} +1 -1
  34. package/dist/web/assets/{jspdf.es.min-BPfxcczM.js → jspdf.es.min-C9Avn9P2.js} +3 -3
  35. package/dist/web/assets/{kanban-definition-UN3LZRKU-B2m52Unk.js → kanban-definition-UN3LZRKU-CAvihSvB.js} +1 -1
  36. package/dist/web/assets/{linear-D-pwAWPr.js → linear-QJGLYtiK.js} +1 -1
  37. package/dist/web/assets/{markdown-lr17R9FO.js → markdown-umDyoRvw.js} +1 -1
  38. package/dist/web/assets/{mermaid.core-CRo4rzDL.js → mermaid.core-DjNa-8Hv.js} +4 -4
  39. package/dist/web/assets/{mindmap-definition-RKZ34NQL-DUpVPXgC.js → mindmap-definition-RKZ34NQL-DdGdY0IJ.js} +1 -1
  40. package/dist/web/assets/{pieDiagram-4H26LBE5-CatVLCYi.js → pieDiagram-4H26LBE5-ByidPHli.js} +1 -1
  41. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-TJ5_ZxiK.js → quadrantDiagram-W4KKPZXB-3-CAwQni.js} +1 -1
  42. package/dist/web/assets/{requirementDiagram-4Y6WPE33-B7CTMFGC.js → requirementDiagram-4Y6WPE33-Bc0BnzZd.js} +1 -1
  43. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-C56xsvrm.js → sankeyDiagram-5OEKKPKP-CnxTmkwV.js} +1 -1
  44. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-EPKxvTJ9.js → sequenceDiagram-3UESZ5HK-DrDm1r9r.js} +1 -1
  45. package/dist/web/assets/{stateDiagram-AJRCARHV-Cma2F0T8.js → stateDiagram-AJRCARHV-Dgco9NEU.js} +1 -1
  46. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-F-kPNI2H.js +1 -0
  47. package/dist/web/assets/{time-D6UR1Qac.js → time-Yxy9gOXu.js} +1 -1
  48. package/dist/web/assets/{timeline-definition-PNZ67QCA-iYUvK9JO.js → timeline-definition-PNZ67QCA-CquekUx0.js} +1 -1
  49. package/dist/web/assets/{vennDiagram-CIIHVFJN-CkLxULtS.js → vennDiagram-CIIHVFJN-BGGVq4Mv.js} +1 -1
  50. package/dist/web/assets/{wardley-L42UT6IY-DPcFqZu2.js → wardley-L42UT6IY-fkIRiPsZ.js} +1 -1
  51. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BKqSvb-r.js → wardleyDiagram-YWT4CUSO-ih5pIO2M.js} +1 -1
  52. package/dist/web/assets/{xychartDiagram-2RQKCTM6-0L00Xzr6.js → xychartDiagram-2RQKCTM6-Rwg_Ry62.js} +1 -1
  53. package/dist/web/index.html +2 -2
  54. package/package.json +1 -1
  55. package/src/api.ts +20 -2
  56. package/src/cli-manifest.ts +49 -24
  57. package/src/cli.ts +28 -9
  58. package/src/mcp.ts +14 -2
  59. package/src/merge.ts +0 -1
  60. package/src/node-files.ts +39 -1
  61. package/src/operations.ts +35 -36
  62. package/src/schema-catalog.ts +109 -3
  63. package/src/schema.ts +5 -6
  64. package/dist/web/assets/channel-CilAQwI4.js +0 -1
  65. package/dist/web/assets/classDiagram-4FO5ZUOK-fiT0MjXY.js +0 -1
  66. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-fiT0MjXY.js +0 -1
  67. package/dist/web/assets/index-DFpY3RpV.js +0 -8624
  68. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-DRP_iswg.js +0 -1
package/src/cli.ts CHANGED
@@ -268,11 +268,14 @@ Commands (work without a running studio):
268
268
  connectors:patch <connId> Patch a connector (--project <p> --flow <f>) [--json/--file/--stdin]
269
269
  connectors:delete <connId> Delete a connector (--project <p> --flow <f>)
270
270
  validate Schema-validate a flow.json (--file <file> [--style <file>])
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')
271
+ schema [<category> [<subname>]] [--jq <path>]
272
+ Get the flow.json schema. Run this before designing /
273
+ authoring nodes. No arg → category index with subnames
274
+ inlined; category arg → full JSON Schema(s) + jqHints;
275
+ subname arg one variant + jqHints.dataFields listing
276
+ every data.<field> available. Pair with --jq to slice
277
+ (e.g. 'schema node rectangle --jq
278
+ .schemas.rectangle.properties.data.properties.playAction').
276
279
  ids <type> <count> Print <count> short ids of the given <type>, one per
277
280
  line. <type> is 'node' (-> 'node-...') or 'connector'
278
281
  (-> 'conn-...'). <count> is 1..100. Call once per type
@@ -1100,10 +1103,19 @@ async function runSchema() {
1100
1103
  const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
1101
1104
  const subname = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
1102
1105
  const jqFilter = flagValue('jq');
1103
- const { listSchemaCategories, getSchemaCategory, getCategorySubschema, listCategorySubnames } =
1104
- await import('./schema-catalog.ts');
1106
+ const {
1107
+ listSchemaCategories,
1108
+ getSchemaCategory,
1109
+ getCategorySubschema,
1110
+ listCategorySubnames,
1111
+ buildJqHints,
1112
+ SCHEMA_INDEX_USAGE,
1113
+ } = await import('./schema-catalog.ts');
1105
1114
  if (!category) {
1106
- const base = { categories: listSchemaCategories() };
1115
+ const base = {
1116
+ categories: listSchemaCategories(),
1117
+ usage: SCHEMA_INDEX_USAGE,
1118
+ };
1107
1119
  if (jqFilter !== undefined) {
1108
1120
  printOk({ result: applyJqOrDie(base, jqFilter) });
1109
1121
  }
@@ -1141,6 +1153,7 @@ async function runSchema() {
1141
1153
  subname,
1142
1154
  schemas: single.schemas,
1143
1155
  notes: single.notes,
1156
+ jqHints: buildJqHints(category as string, subname),
1144
1157
  };
1145
1158
  if (jqFilter !== undefined) {
1146
1159
  printOk({ name: category, subname, result: applyJqOrDie(base, jqFilter) });
@@ -1154,7 +1167,13 @@ async function runSchema() {
1154
1167
  process.stderr.write(`${JSON.stringify({ error: message, code: 'notFound', available })}\n`);
1155
1168
  process.exit(3);
1156
1169
  }
1157
- const base = { name: category, schemas: payload.schemas, notes: payload.notes };
1170
+ const base = {
1171
+ name: category,
1172
+ schemas: payload.schemas,
1173
+ notes: payload.notes,
1174
+ subnames: listCategorySubnames(category as string) ?? [],
1175
+ jqHints: buildJqHints(category as string),
1176
+ };
1158
1177
  if (jqFilter !== undefined) {
1159
1178
  printOk({ name: category, result: applyJqOrDie(base, jqFilter) });
1160
1179
  }
package/src/mcp.ts CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  } from './operations.ts';
31
31
  import type { Registry } from './registry.ts';
32
32
  import {
33
+ SCHEMA_INDEX_USAGE,
34
+ buildJqHints,
33
35
  getCategorySubschema,
34
36
  getSchemaCategory,
35
37
  listCategorySubnames,
@@ -329,7 +331,10 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
329
331
  if (subname !== undefined && subname !== null && subname !== '') {
330
332
  return errorResult('Invalid arguments: `subname` requires `name` to be set');
331
333
  }
332
- return okResult({ categories: listSchemaCategories() });
334
+ return okResult({
335
+ categories: listSchemaCategories(),
336
+ usage: SCHEMA_INDEX_USAGE,
337
+ });
333
338
  }
334
339
  if (typeof name !== 'string') {
335
340
  return errorResult('Invalid arguments: `name` must be a string when present');
@@ -345,6 +350,7 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
345
350
  subname,
346
351
  schemas: single.schemas,
347
352
  notes: single.notes,
353
+ jqHints: buildJqHints(name, subname),
348
354
  });
349
355
  }
350
356
  const availableSubs = listCategorySubnames(name);
@@ -363,7 +369,13 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
363
369
  `unknown schema category: ${name} (available: ${schemaCategoryNames().join(', ')})`,
364
370
  );
365
371
  }
366
- return okResult({ name, schemas: payload.schemas, notes: payload.notes });
372
+ return okResult({
373
+ name,
374
+ schemas: payload.schemas,
375
+ notes: payload.notes,
376
+ subnames: listCategorySubnames(name) ?? [],
377
+ jqHints: buildJqHints(name),
378
+ });
367
379
  },
368
380
  },
369
381
  {
package/src/merge.ts CHANGED
@@ -64,7 +64,6 @@ const NODE_STYLE_KEYS = new Set([
64
64
  'borderSize',
65
65
  'borderStyle',
66
66
  'fontSize',
67
- 'textColor',
68
67
  'textAlign',
69
68
  'cornerRadius',
70
69
  'shadow',
package/src/node-files.ts CHANGED
@@ -8,16 +8,54 @@ import { writeFileAtomic } from './atomic-write.ts';
8
8
  // `flows/<flow-id>/nodes/<id>/`; for legacy single-flow fixtures with
9
9
  // `flowPath: 'flow.json'` it collapses to the project root. `nodeTypes` (when
10
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.
11
+ // "applies to every node type".
12
+ //
13
+ // Two flavors of externalization:
14
+ // - `kind: 'ref'` (default) — write the file, replace `data[field]` with
15
+ // `file://<fileName>`. The ref survives splitFlow (the field is in
16
+ // NODE_DATA_FLOW_KEYS in merge.ts). Used by string fields like `detail`
17
+ // and `html`.
18
+ // - `kind: 'sidecar'` — write the file, leave `data[field]` untouched on
19
+ // the in-memory node so the post-mutation parse still sees the original
20
+ // value. splitFlow drops the field from flow.json on write; the resolver
21
+ // inlines it from disk on read. Used by JSON fields like component `spec`.
22
+ //
23
+ // `serialize` turns the in-memory value into file contents. Returning `null`
24
+ // skips the write entirely — used by `spec` to no-op when the caller didn't
25
+ // supply one, instead of writing an empty file that would fail JSON parse
26
+ // on the next read.
12
27
  export interface ExternalizedFieldSpec {
13
28
  field: string;
14
29
  fileName: string;
15
30
  nodeTypes?: readonly string[];
31
+ kind?: 'ref' | 'sidecar';
32
+ serialize?: (value: unknown) => string | null;
16
33
  }
17
34
 
35
+ // Default serializer: strings pass through; non-strings coerce to empty.
36
+ // Keeps the historical detail/html behavior — an absent detail still writes
37
+ // an empty detail.md so the file:// ref points somewhere.
38
+ export const defaultExternalizedSerializer = (value: unknown): string =>
39
+ typeof value === 'string' ? value : '';
40
+
41
+ // JSON serializer for sidecar fields: pretty-print plain objects with a
42
+ // trailing newline. Returns null for anything else so the loop can skip the
43
+ // write rather than emit an invalid sidecar.
44
+ const jsonExternalizedSerializer = (value: unknown): string | null =>
45
+ value !== null && typeof value === 'object' && !Array.isArray(value)
46
+ ? `${JSON.stringify(value, null, 2)}\n`
47
+ : null;
48
+
18
49
  export const EXTERNALIZED_NODE_FIELDS: readonly ExternalizedFieldSpec[] = [
19
50
  { field: 'detail', fileName: 'detail.md' },
20
51
  { field: 'html', fileName: 'view.html', nodeTypes: ['html'] },
52
+ {
53
+ field: 'spec',
54
+ fileName: 'spec.json',
55
+ nodeTypes: ['component'],
56
+ kind: 'sidecar',
57
+ serialize: jsonExternalizedSerializer,
58
+ },
21
59
  ];
22
60
 
23
61
  export const externalizedFieldsForNodeType = (
package/src/operations.ts CHANGED
@@ -16,6 +16,7 @@ import { type LayoutOptions, computeLayout } from './layout.ts';
16
16
  import { mergeFlowAndStyle, splitFlow } from './merge.ts';
17
17
  import {
18
18
  EXTERNALIZED_NODE_FIELDS,
19
+ defaultExternalizedSerializer,
19
20
  externalizedFieldsForNodeType,
20
21
  nodeFileAbsPath,
21
22
  nodeFileRef,
@@ -100,7 +101,6 @@ export const NodePatchBodySchema = z
100
101
  borderWidth: z.number().min(0).max(8).optional(),
101
102
  borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
102
103
  fontSize: z.number().positive().optional(),
103
- textColor: ColorTokenSchema.optional(),
104
104
  textAlign: z.enum(['left', 'center', 'right']).optional(),
105
105
  cornerRadius: z.number().min(0).optional(),
106
106
  shadow: z.number().int().min(0).max(5).optional(),
@@ -164,7 +164,6 @@ const NODE_DATA_PATCH_KEYS = [
164
164
  'borderWidth',
165
165
  'borderStyle',
166
166
  'fontSize',
167
- 'textColor',
168
167
  'textAlign',
169
168
  'cornerRadius',
170
169
  'shadow',
@@ -267,7 +266,6 @@ const NODE_VISUAL_KEYS = new Set([
267
266
  'borderSize',
268
267
  'borderStyle',
269
268
  'fontSize',
270
- 'textColor',
271
269
  'textAlign',
272
270
  'cornerRadius',
273
271
  'shadow',
@@ -1347,12 +1345,16 @@ export async function addNodeImpl(
1347
1345
  ? { ...(newNode.data as Record<string, unknown>) }
1348
1346
  : {};
1349
1347
  const flowDir = dirname(entry.flowPath);
1350
- for (const { field, fileName } of externalizedFieldsForNodeType(newNode.type)) {
1351
- const incoming = data[field];
1352
- const content = typeof incoming === 'string' ? incoming : '';
1353
- data[field] = nodeFileRef(newId, fileName);
1348
+ for (const spec of externalizedFieldsForNodeType(newNode.type)) {
1349
+ const incoming = data[spec.field];
1350
+ const serializer = spec.serialize ?? defaultExternalizedSerializer;
1351
+ const content = serializer(incoming);
1352
+ if (content === null) continue; // serializer opted out (e.g. spec absent)
1353
+ if ((spec.kind ?? 'ref') === 'ref') {
1354
+ data[spec.field] = nodeFileRef(newId, spec.fileName);
1355
+ }
1354
1356
  externalized.push({
1355
- absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, fileName),
1357
+ absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, spec.fileName),
1356
1358
  content,
1357
1359
  });
1358
1360
  }
@@ -1431,12 +1433,16 @@ export async function addFlowBulkImpl(
1431
1433
  const data: Record<string, unknown> = dataIsRecord
1432
1434
  ? { ...(newNode.data as Record<string, unknown>) }
1433
1435
  : {};
1434
- for (const { field, fileName } of externalizedFieldsForNodeType(newNode.type)) {
1435
- const incoming = data[field];
1436
- const content = typeof incoming === 'string' ? incoming : '';
1437
- data[field] = nodeFileRef(newId, fileName);
1436
+ for (const spec of externalizedFieldsForNodeType(newNode.type)) {
1437
+ const incoming = data[spec.field];
1438
+ const serializer = spec.serialize ?? defaultExternalizedSerializer;
1439
+ const content = serializer(incoming);
1440
+ if (content === null) continue; // serializer opted out (e.g. spec absent)
1441
+ if ((spec.kind ?? 'ref') === 'ref') {
1442
+ data[spec.field] = nodeFileRef(newId, spec.fileName);
1443
+ }
1438
1444
  externalized.push({
1439
- absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, fileName),
1445
+ absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, spec.fileName),
1440
1446
  content,
1441
1447
  });
1442
1448
  }
@@ -1637,15 +1643,20 @@ export async function patchNodeImpl(
1637
1643
  ref: string;
1638
1644
  field: string;
1639
1645
  content: string;
1646
+ kind: 'ref' | 'sidecar';
1640
1647
  }> = [];
1641
- for (const { field, fileName } of externalizedFieldsForNodeType(node.type)) {
1642
- const incoming = (updates as Record<string, unknown>)[field];
1648
+ for (const spec of externalizedFieldsForNodeType(node.type)) {
1649
+ const incoming = (updates as Record<string, unknown>)[spec.field];
1643
1650
  if (incoming === undefined) continue;
1651
+ const serializer = spec.serialize ?? defaultExternalizedSerializer;
1652
+ const content = serializer(incoming);
1653
+ if (content === null) continue; // serializer opted out
1644
1654
  externalizedWrites.push({
1645
- absPath: nodeFileAbsPath(entry.repoPath, flowDir, nodeId, fileName),
1646
- ref: nodeFileRef(nodeId, fileName),
1647
- field,
1648
- content: typeof incoming === 'string' ? incoming : '',
1655
+ absPath: nodeFileAbsPath(entry.repoPath, flowDir, nodeId, spec.fileName),
1656
+ ref: nodeFileRef(nodeId, spec.fileName),
1657
+ field: spec.field,
1658
+ content,
1659
+ kind: spec.kind ?? 'ref',
1649
1660
  });
1650
1661
  }
1651
1662
  mergeNodeUpdates(node, updates);
@@ -1664,26 +1675,14 @@ export async function patchNodeImpl(
1664
1675
  message: err instanceof Error ? err.message : String(err),
1665
1676
  };
1666
1677
  }
1667
- data[w.field] = w.ref;
1678
+ // 'ref' fields swap data[field] for a file:// pointer; 'sidecar'
1679
+ // fields (e.g. component spec) leave the in-memory value alone so the
1680
+ // post-mutation parse still sees it — splitFlow drops it from flow.json
1681
+ // on write and the resolver inlines it back from disk on read.
1682
+ if (w.kind === 'ref') data[w.field] = w.ref;
1668
1683
  }
1669
1684
  node.data = data;
1670
1685
  }
1671
- // Component spec sidecar — write the pretty-printed JSON to
1672
- // `<repoPath>/<flowDir>/nodes/<id>/spec.json` so the on-disk source of
1673
- // truth stays in sync. mergeNodeUpdates already put data.spec on the
1674
- // merged tree for the post-mutation ResolvedFlowSchema parse; splitFlow
1675
- // strips it from flow.json so we don't double-store the spec.
1676
- if (node.type === 'component' && updates.spec !== undefined) {
1677
- const specAbs = nodeFileAbsPath(entry.repoPath, flowDir, nodeId, 'spec.json');
1678
- try {
1679
- writeNodeFile(specAbs, `${JSON.stringify(updates.spec, null, 2)}\n`);
1680
- } catch (err) {
1681
- return {
1682
- kind: 'writeFailed',
1683
- message: err instanceof Error ? err.message : String(err),
1684
- };
1685
- }
1686
- }
1687
1686
  return { kind: 'ok' };
1688
1687
  });
1689
1688
  }
@@ -37,6 +37,9 @@ import {
37
37
  export interface SchemaCategory {
38
38
  name: string;
39
39
  description: string;
40
+ // Every drill target valid for `seeflow schema <name> <subname>`. Lets the
41
+ // agent pick a variant without a second round-trip to listCategorySubnames.
42
+ subnames: string[];
40
43
  }
41
44
 
42
45
  export interface SchemaPayload {
@@ -44,13 +47,40 @@ export interface SchemaPayload {
44
47
  notes: string[];
45
48
  }
46
49
 
50
+ // Hint payload attached to every schema response so the agent can drill in
51
+ // further without round-tripping. `examples` are ready-to-paste jq paths;
52
+ // `dataFields` lists the node-variant `data.<field>` keys (single-variant
53
+ // lookups only — undefined for non-node categories or category-level
54
+ // responses).
55
+ export interface JqHints {
56
+ dataFields?: string[];
57
+ examples: string[];
58
+ tip?: string;
59
+ }
60
+
47
61
  // Draft-07 pin matches the widest tool support; the same target string is
48
62
  // used by the MCP `tools/list` JSON Schemas (default in zod-to-json-schema)
49
63
  // so consumers see one consistent dialect across the whole surface.
50
64
  const toJsonSchema = (schema: ZodTypeAny): unknown =>
51
65
  zodToJsonSchema(schema, { $refStrategy: 'none', target: 'jsonSchema7' });
52
66
 
53
- const CATEGORIES: SchemaCategory[] = [
67
+ // Recipe block returned on the schema index (CLI / REST / MCP) so the agent
68
+ // sees the drill + filter pattern in the response itself, not just in
69
+ // `seeflow help schema`.
70
+ export const SCHEMA_INDEX_USAGE = {
71
+ drill: 'seeflow schema <category> [<subname>]',
72
+ filter: 'seeflow schema <category> [<subname>] --jq <jq-path>',
73
+ examples: [
74
+ 'seeflow schema node',
75
+ 'seeflow schema node rectangle',
76
+ 'seeflow schema node rectangle --jq .schemas.rectangle.properties.data.properties.playAction',
77
+ 'seeflow schema action playAction',
78
+ ],
79
+ } as const;
80
+
81
+ // Description metadata. `subnames` are filled in by listSchemaCategories()
82
+ // at call time from PAYLOADS, so the two stay in lockstep automatically.
83
+ const CATEGORY_META: Array<Omit<SchemaCategory, 'subnames'>> = [
54
84
  { name: 'flow', description: 'Top-level flow.json envelope.' },
55
85
  {
56
86
  name: 'node',
@@ -141,7 +171,10 @@ const PAYLOADS: Record<string, SchemaPayload> = {
141
171
  };
142
172
 
143
173
  export function listSchemaCategories(): SchemaCategory[] {
144
- return CATEGORIES.map((c) => ({ ...c }));
174
+ return CATEGORY_META.map((c) => ({
175
+ ...c,
176
+ subnames: Object.keys(PAYLOADS[c.name]?.schemas ?? {}),
177
+ }));
145
178
  }
146
179
 
147
180
  export function getSchemaCategory(name: string): SchemaPayload | null {
@@ -151,7 +184,7 @@ export function getSchemaCategory(name: string): SchemaPayload | null {
151
184
  }
152
185
 
153
186
  export function schemaCategoryNames(): string[] {
154
- return CATEGORIES.map((c) => c.name);
187
+ return CATEGORY_META.map((c) => c.name);
155
188
  }
156
189
 
157
190
  // Drill into one named schema inside a category — e.g. ('node', 'rectangle')
@@ -176,3 +209,76 @@ export function listCategorySubnames(category: string): string[] | null {
176
209
  if (!payload) return null;
177
210
  return Object.keys(payload.schemas);
178
211
  }
212
+
213
+ // Top-level keys under `data.properties` for a single node variant — i.e.
214
+ // the per-shape data fields an author actually sets on a flow.json node
215
+ // (`name`, `icon`, `playAction`, etc.). Returns null when the variant has
216
+ // no `data.properties` wrapper (action / connector / style / componentSpec
217
+ // schemas, plus anything malformed). Pure helper consumed by buildJqHints
218
+ // to surface concrete drill-down paths.
219
+ export function getDataFieldNames(category: string, subname: string): string[] | null {
220
+ const sub = PAYLOADS[category]?.schemas[subname] as
221
+ | { properties?: { data?: { properties?: Record<string, unknown> } } }
222
+ | undefined;
223
+ const dataProps = sub?.properties?.data?.properties;
224
+ if (!dataProps) return null;
225
+ return Object.keys(dataProps);
226
+ }
227
+
228
+ // Build ready-to-paste jq path examples for a schema response. When `subname`
229
+ // is provided, the examples drill into that single variant — including one
230
+ // path per `data.<field>` so the agent can `--jq` straight to (say)
231
+ // `.schemas.rectangle.properties.data.properties.playAction` without first
232
+ // reading the whole envelope. When `subname` is omitted, the hints cover the
233
+ // whole category (iteration, one sample variant, notes). `dataFields` only
234
+ // surfaces on single-variant lookups for shapes that actually carry a
235
+ // `data.properties` wrapper.
236
+ export function buildJqHints(category: string, subname?: string): JqHints | null {
237
+ const payload = PAYLOADS[category];
238
+ if (!payload) return null;
239
+ if (subname) {
240
+ if (payload.schemas[subname] === undefined) return null;
241
+ const dataFields = getDataFieldNames(category, subname);
242
+ const examples = [
243
+ `.schemas.${subname}`,
244
+ `.schemas.${subname}.required`,
245
+ ...(dataFields && dataFields.length > 0
246
+ ? [
247
+ `.schemas.${subname}.properties.data.properties`,
248
+ ...dataFields
249
+ .slice(0, 6)
250
+ .map((f) => `.schemas.${subname}.properties.data.properties.${f}`),
251
+ ]
252
+ : [`.schemas.${subname}.properties`]),
253
+ '.notes',
254
+ '.notes[]',
255
+ ];
256
+ const hint = dataFields
257
+ ? `dataFields lists every \`data.<field>\` available on this variant — point \`--jq\` at any of them with \`.schemas.${subname}.properties.data.properties.<field>\`.`
258
+ : `Use \`--jq\` to pluck a single property — e.g. \`.schemas.${subname}.required\`.`;
259
+ return {
260
+ ...(dataFields ? { dataFields } : {}),
261
+ examples,
262
+ tip: hint,
263
+ };
264
+ }
265
+ const subs = Object.keys(payload.schemas);
266
+ const sample = subs[0];
267
+ if (!sample) return { examples: ['.schemas', '.notes', '.notes[]'] };
268
+ const examples = [
269
+ '.schemas',
270
+ `.schemas.${sample}`,
271
+ `.schemas.${sample}.required`,
272
+ `.schemas.${sample}.properties.data.properties`,
273
+ '.schemas[]',
274
+ '.notes',
275
+ '.notes[]',
276
+ ];
277
+ return {
278
+ examples,
279
+ tip:
280
+ subs.length > 1
281
+ ? `Pass \`seeflow schema ${category} <subname>\` (one of: ${subs.join(', ')}) to drop the other ${subs.length - 1} variant(s) from the payload before \`--jq\`-ing.`
282
+ : `Single-variant category — \`--jq\` paths drill straight into \`.schemas.${sample}\`.`,
283
+ };
284
+ }
package/src/schema.ts CHANGED
@@ -14,25 +14,26 @@ const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
14
14
  // them to actual CSS values (theme-aware, light + dark).
15
15
  export const ColorTokenSchema = z.enum([
16
16
  // `'none'` renders transparent border / fill on nodes (no stroke, no fill).
17
- // Hidden from the text-color and connector-color pickers — invisible text
18
- // and edges aren't useful, and `'default'` already covers "inherit".
17
+ // Hidden from the connector-color picker — invisible edges aren't useful,
18
+ // and `'default'` already covers "inherit".
19
19
  'none',
20
20
  'default',
21
21
  'white',
22
22
  'slate',
23
23
  'gray',
24
24
  'red',
25
- 'rose',
26
25
  'orange',
27
26
  'amber',
27
+ 'yellow',
28
28
  'lime',
29
29
  'green',
30
30
  'teal',
31
31
  'cyan',
32
+ 'sky',
32
33
  'blue',
33
34
  'indigo',
34
35
  'violet',
35
- 'purple',
36
+ 'fuchsia',
36
37
  'pink',
37
38
  ]);
38
39
 
@@ -47,7 +48,6 @@ const NodeVisualBaseShape = {
47
48
  borderSize: z.number().min(0).optional(),
48
49
  borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
49
50
  fontSize: z.number().positive().optional(),
50
- textColor: ColorTokenSchema.optional(),
51
51
  // Horizontal alignment for the node's text content. Defaults to 'center'
52
52
  // at render time when omitted; explicit picks from the toolbar's Align
53
53
  // toggle persist here.
@@ -711,7 +711,6 @@ const NodeStyleSchema = z
711
711
  borderSize: z.number().min(0).optional(),
712
712
  borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
713
713
  fontSize: z.number().positive().optional(),
714
- textColor: ColorTokenSchema.optional(),
715
714
  textAlign: z.enum(['left', 'center', 'right']).optional(),
716
715
  cornerRadius: z.number().min(0).optional(),
717
716
  shadow: z.number().int().min(0).max(5).optional(),
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-CRo4rzDL.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-2Se8RGwW.js";import{a as i}from"./mermaid.core-CRo4rzDL.js";import"./index-DFpY3RpV.js";import"./chunk-FMBD7UC4-BJX_21R2.js";import"./chunk-ND2GUHAM-DFBKXknR.js";import"./chunk-55IACEB6-DlboNFJr.js";import"./chunk-2J33WTMH-9vQ1xqy3.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-2Se8RGwW.js";import{a as i}from"./mermaid.core-CRo4rzDL.js";import"./index-DFpY3RpV.js";import"./chunk-FMBD7UC4-BJX_21R2.js";import"./chunk-ND2GUHAM-DFBKXknR.js";import"./chunk-55IACEB6-DlboNFJr.js";import"./chunk-2J33WTMH-9vQ1xqy3.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};