@pyreon/mcp 0.12.10 → 0.12.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/mcp",
3
- "version": "0.12.10",
3
+ "version": "0.12.12",
4
4
  "description": "MCP server for Pyreon — AI-powered framework assistance",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/mcp#readme",
6
6
  "bugs": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@pyreon/compiler": "^0.12.10",
49
+ "@pyreon/compiler": "^0.12.12",
50
50
  "zod": "^4.3.6"
51
51
  },
52
52
  "peerDependencies": {
@@ -698,7 +698,10 @@ theme.remove() // delete from storage`,
698
698
  'i18n/createI18n': {
699
699
  signature:
700
700
  'createI18n(options: { locale: string, messages: Record<string, Record<string, string>>, loader?, fallbackLocale?, pluralRules? }): I18nInstance',
701
- example: `const i18n = createI18n({
701
+ example: `// Full entry — includes JSX components (Trans, I18nProvider, useI18n)
702
+ import { createI18n, useI18n } from '@pyreon/i18n'
703
+
704
+ const i18n = createI18n({
702
705
  locale: 'en',
703
706
  messages: { en: { greeting: 'Hello, {{name}}!' } },
704
707
  loader: (locale, ns) => import(\`./locales/\${locale}/\${ns}.json\`),
@@ -706,9 +709,19 @@ theme.remove() // delete from storage`,
706
709
 
707
710
  const { t, locale } = useI18n()
708
711
  t('greeting', { name: 'World' }) // "Hello, World!"
709
- locale.set('fr') // switch reactively`,
712
+ locale.set('fr') // switch reactively
713
+
714
+ // Backend / non-JSX entry — @pyreon/i18n/core
715
+ // Zero JSX dependencies, transitively only @pyreon/reactivity.
716
+ // Use this on backends, edge workers, non-Pyreon frontends.
717
+ import { createI18n } from '@pyreon/i18n/core'
718
+ const backendI18n = createI18n({ locale: 'en', messages: { en: { hello: 'Hi' } } })
719
+ backendI18n.t('hello')`,
710
720
  notes:
711
- 'Interpolation with {{name}}, pluralization with _one/_other suffixes. Namespace lazy loading. <Trans> component for rich JSX interpolation.',
721
+ 'Interpolation with {{name}}, pluralization with _one/_other suffixes. Namespace lazy loading. <Trans> component for rich JSX interpolation. TWO ENTRY POINTS: `@pyreon/i18n` (full, with JSX components) vs `@pyreon/i18n/core` (framework-agnostic, zero JSX deps — use for backends and non-Pyreon consumers). Both return identical I18nInstance objects.',
722
+ mistakes: `- Using \`@pyreon/i18n\` (the main entry) on a backend without a JSX-aware tsconfig — the bun condition resolves to source which transitively includes the Trans JSX component. Use \`@pyreon/i18n/core\` instead.
723
+ - Reading the README example and importing from \`@pyreon/i18n\` in a non-Pyreon project — that path works for Pyreon UIs but the README now documents \`/core\` as the backend recommendation.
724
+ - Trying to use \`<Trans>\` from \`@pyreon/i18n/core\` — it's intentionally not exported there. Import it from the main \`@pyreon/i18n\` entry instead.`,
712
725
  },
713
726
 
714
727
  // ═══════════════════════════════════════════════════════════════════════════
@@ -735,21 +748,57 @@ await doc.toNotion() // Notion blocks`,
735
748
  // ═══════════════════════════════════════════════════════════════════════════
736
749
 
737
750
  'flow/createFlow': {
738
- signature: 'createFlow(config: { nodes: FlowNode[], edges: FlowEdge[], ... }): FlowInstance',
739
- example: `const flow = createFlow({
751
+ signature:
752
+ 'createFlow<TData = Record<string, unknown>>(config: FlowConfig<TData>): FlowInstance<TData>',
753
+ example: `// Generic over node data shape — typed consumers get strong narrowing
754
+ interface WorkflowData {
755
+ kind: 'trigger' | 'filter' | 'transform' | 'notify'
756
+ label: string
757
+ }
758
+
759
+ const flow = createFlow<WorkflowData>({
740
760
  nodes: [
741
- { id: '1', position: { x: 0, y: 0 }, data: { label: 'Start' } },
742
- { id: '2', position: { x: 200, y: 100 }, data: { label: 'End' } },
761
+ { id: '1', type: 'custom', position: { x: 0, y: 0 }, data: { kind: 'trigger', label: 'Start' } },
762
+ { id: '2', type: 'custom', position: { x: 200, y: 100 }, data: { kind: 'notify', label: 'End' } },
743
763
  ],
744
- edges: [{ id: 'e1', source: '1', target: '2' }],
764
+ edges: [{ id: 'e1', source: '1', target: '2', animated: true }],
745
765
  })
746
766
 
747
- flow.addNode({ id: '3', position: { x: 100, y: 200 }, data: { label: 'New' } })
748
- await flow.layout('layered') // auto-layout via elkjs
767
+ // node.data.kind narrows to the typed union, not unknown
768
+ const trigger = flow.findNodes((n) => n.data.kind === 'trigger')
769
+
770
+ flow.addNode({ id: '3', type: 'custom', position: { x: 100, y: 200 }, data: { kind: 'transform', label: 'New' } })
771
+ await flow.layout('layered', { direction: 'RIGHT', nodeSpacing: 50, layerSpacing: 100 }) // auto-layout via lazy-loaded elkjs
772
+ // LayoutOptions applicability: direction/layerSpacing/edgeRouting apply to layered/tree only;
773
+ // force/stress/radial/box/rectpacking silently ignore them. nodeSpacing applies to all algorithms.
774
+ const json = flow.toJSON(); flow.fromJSON(json) // round-trip serialization
749
775
 
750
- <Flow instance={flow}><Background /><Controls /><MiniMap /></Flow>`,
776
+ // Custom node renderer — every prop except id is a REACTIVE ACCESSOR
777
+ function CustomNode(props: NodeComponentProps<WorkflowData>) {
778
+ return (
779
+ <div
780
+ class={() => (props.selected() ? 'selected' : '')}
781
+ style={() => \`cursor: \${props.dragging() ? 'grabbing' : 'grab'}\`}
782
+ >
783
+ {() => props.data().label}
784
+ </div>
785
+ )
786
+ }
787
+
788
+ <Flow instance={flow} nodeTypes={{ custom: CustomNode }}>
789
+ <Background variant="dots" />
790
+ <Controls />
791
+ <MiniMap />
792
+ </Flow>`,
751
793
  notes:
752
- 'Signal-native nodes/edges. Auto-layout via elkjs (lazy-loaded). Pan/zoom via pointer events + CSS transforms. No D3.',
794
+ "Signal-native nodes/edges. Generic over node data shape: createFlow<TData> returns FlowInstance<TData> so node.data.kind narrows correctly. Defaults to Record<string, unknown> if no generic supplied. NodeComponentProps has THREE reactive accessors — data: () => TData, selected: () => boolean, dragging: () => boolean — read inside reactive scopes so the node patches in place when ANY underlying state changes. Each node mounts EXACTLY ONCE across the lifetime of the graph regardless of how many drags, selection clicks, or updateNode mutations happen. Internally <Flow> uses <For> keyed by node.id plus per-node accessors that read live state from instance.nodes() — so a 60fps drag in a 1000-node graph is O(1) instead of O(N) per frame. Auto-layout via elkjs (lazy-loaded, ~1.4MB chunk only on first .layout() call). Pan/zoom via pointer events + CSS transforms. No D3. JSX components are NOT generic at the call site (<Flow<MyData> /> is invalid JSX) — FlowProps.instance is typed as FlowInstance<any> so typed consumers can pass FlowInstance<MyData> without casting.",
795
+ mistakes: `- Forgetting to declare @pyreon/runtime-dom in consumer app deps — flow's JSX emits _tpl() which needs runtime-dom imports
796
+ - Reading props.data, props.selected, or props.dragging as plain values — they're ALL accessors, call them: props.data().kind, props.selected(), props.dragging()
797
+ - Calling props.data() OUTSIDE a reactive scope — captures the value once at component setup, defeating reactivity. Read it inside JSX expression thunks, effect, or computed: {() => props.data().label}
798
+ - Adding [key: string]: unknown index signature to your node data interface — no longer needed now that createFlow is generic. Just pass createFlow<MyData>(...)
799
+ - Using direction: 'row' on flow's containing layout — Pyreon Element accepts 'inline'|'rows'|'reverseInline'|'reverseRows', not 'row'
800
+ - Setting LayoutOptions.direction (or layerSpacing, or edgeRouting) on a force/stress/radial/box/rectpacking layout and expecting a directional result — these options are namespaced under ELK's layered/tree pipelines and silently ignored by the geometric algorithms. Switch the algorithm to 'layered' or 'tree' if you need a directional layout.
801
+ - Missing the <Flow nodeTypes={{ key: Component }}> registration — node.type strings dispatch to that map`,
753
802
  },
754
803
 
755
804
  // ═══════════════════════════════════════════════════════════════════════════
@@ -758,22 +807,66 @@ await flow.layout('layered') // auto-layout via elkjs
758
807
 
759
808
  'code/createEditor': {
760
809
  signature:
761
- 'createEditor(config: { value?: string, language?: string, theme?: string, minimap?: boolean, ... }): EditorInstance',
810
+ 'createEditor(config: { value?: string, language?: EditorLanguage, theme?: EditorTheme, onChange?: (val: string) => void, minimap?: boolean, lineNumbers?: boolean, ... }): EditorInstance',
762
811
  example: `const editor = createEditor({
763
812
  value: '// hello',
764
813
  language: 'typescript',
765
814
  theme: 'dark',
766
815
  minimap: true,
816
+ onChange: (next) => console.log('user edit:', next),
767
817
  })
768
818
 
769
- editor.value() // reactive Signal<string>
819
+ editor.value() // reactive Signal<string>, read inside JSX/effects
820
+ editor.value.set('new') // write back into CodeMirror
821
+ editor.cursor() // computed { line, col }
822
+ editor.lineCount() // computed
770
823
  editor.goToLine(42)
771
824
  editor.insert('new code')
825
+ editor.setDiagnostics([{ from: 0, to: 5, severity: 'error', message: '...' }])
772
826
 
773
- <CodeEditor instance={editor} />
774
- <DiffEditor original="old" modified="new" />`,
827
+ <CodeEditor instance={editor} style="height: 400px" />
828
+ <DiffEditor original="old" modified="new" language="typescript" />`,
775
829
  notes:
776
- "Built on CodeMirror 6 (~250KB vs Monaco's ~2.5MB). loadLanguage() for lazy grammars. TabbedEditor for multi-file.",
830
+ "Built on CodeMirror 6 (~250KB vs Monaco's ~2.5MB). 19 languages via lazy-loaded grammars (declared as optionalDependencies). Two-way binding: editor.value is a writable Signal — pass onChange for editor → external, set editor.value for external → editor. For external↔editor binding with built-in loop prevention, use the higher-level `bindEditorToSignal({ editor, signal, serialize, parse })` helper instead of hand-rolling the flag pattern. <CodeEditor> auto-mounts and cleans up on unmount.",
831
+ mistakes: `- Forgetting to declare @pyreon/runtime-dom in consumer app deps — <CodeEditor> JSX emits _tpl() which needs runtime-dom imports
832
+ - Hand-rolling the applyingFromExternal/applyingFromEditor flag pattern for two-way binding — use the bindEditorToSignal helper instead, it handles the loop prevention correctly and is tested
833
+ - Calling editor methods before mount — they no-op safely but changes don't persist
834
+ - Setting both vim: true and emacs: true — emacs wins`,
835
+ },
836
+
837
+ 'code/bindEditorToSignal': {
838
+ signature:
839
+ 'bindEditorToSignal<T>(options: { editor: EditorInstance, signal: SignalLike<T>, serialize: (val: T) => string, parse: (text: string) => T | null, onParseError?: (err: Error) => void }): { dispose: () => void }',
840
+ example: `import { bindEditorToSignal, createEditor } from '@pyreon/code'
841
+ import { signal } from '@pyreon/reactivity'
842
+
843
+ interface Doc { name: string; count: number }
844
+ const data = signal<Doc>({ name: 'Alice', count: 1 })
845
+
846
+ const editor = createEditor({
847
+ value: JSON.stringify(data(), null, 2),
848
+ language: 'json',
849
+ })
850
+
851
+ const binding = bindEditorToSignal({
852
+ editor,
853
+ signal: data, // accepts Signal<T> or any SignalLike<T>
854
+ serialize: (val) => JSON.stringify(val, null, 2),
855
+ parse: (text) => {
856
+ try { return JSON.parse(text) } catch { return null }
857
+ },
858
+ onParseError: (err) => console.warn(err.message),
859
+ })
860
+
861
+ // Later, on unmount:
862
+ binding.dispose()`,
863
+ notes:
864
+ "Replaces the recurring loop-prevention flag-pair boilerplate (applyingFromExternal / applyingFromEditor) that consumers had to hand-roll for two-way external↔editor binding. The helper manages both directions, breaks the format-on-input race via internal flags, catches parse errors, and returns a disposable. Accepts any SignalLike<T> (Pyreon Signal, custom store wrapper, etc.). The editor itself ALSO has internal CM↔signal loop guards — this helper adds the SECOND layer for the external↔editor boundary.",
865
+ mistakes: `- Forgetting to call binding.dispose() on unmount — leaks both effects until the editor instance is GC'd
866
+ - Non-deterministic serialize() — if serialize(parse(text)) returns a string structurally different from the input text, the helper dispatches redundant editor writes that fight the user's typing. JSON.stringify with consistent indentation is fine; pretty-printing that varies on every call is not
867
+ - Throwing in parse() without an onParseError handler — the helper catches and silently no-ops if no handler is provided. Pass onParseError to surface parse errors in your UI
868
+ - Returning a non-null value from parse() for malformed input — the helper writes whatever you return, including partial / corrupted state. Return null on parse failure, or throw with an error message
869
+ - Using bindEditorToSignal AND a manual editor.value.set() loop in the same component — defeats the loop prevention. Pick one binding strategy per editor instance`,
777
870
  },
778
871
 
779
872
  // ═══════════════════════════════════════════════════════════════════════════
@@ -869,7 +962,7 @@ console.log(result.totalErrors, result.totalWarnings)
869
962
  // With config file auto-loading + rule overrides
870
963
  lint({ paths: ["."], ruleOverrides: { "pyreon/no-classname": "off" } })`,
871
964
  notes:
872
- 'Programmatic API. 55 rules across 12 categories. Auto-loads .pyreonlintrc.json. Presets: recommended, strict, app, lib. Uses oxc-parser with AST caching.',
965
+ 'Programmatic API. 58 rules across 12 categories. Auto-loads .pyreonlintrc.json. Presets: recommended, strict, app, lib. Uses oxc-parser with AST caching.',
873
966
  },
874
967
 
875
968
  'lint/lintFile': {
@@ -889,12 +982,30 @@ const result = lintFile("app.tsx", source, allRules, config, cache)`,
889
982
  example: `pyreon-lint --preset strict --quiet # CI mode
890
983
  pyreon-lint --fix # auto-fix
891
984
  pyreon-lint --watch src/ # watch mode
892
- pyreon-lint --list # list all 55 rules
985
+ pyreon-lint --list # list all 58 rules
893
986
  pyreon-lint --format json # machine-readable`,
894
987
  notes:
895
988
  "CLI entry. Config: .pyreonlintrc.json, package.json 'pyreonlint' field. Ignore: .pyreonlintignore + .gitignore. Watch: fs.watch recursive with 100ms debounce.",
896
989
  },
897
990
 
991
+ 'lint/no-process-dev-gate': {
992
+ signature: 'rule: pyreon/no-process-dev-gate (architecture, error, auto-fixable)',
993
+ example: `// ❌ Wrong — dead code in real Vite browser bundles
994
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
995
+ if (__DEV__) console.warn('hello')
996
+
997
+ // ✅ Correct — Vite literal-replaces import.meta.env.DEV at build time
998
+ // @ts-ignore — provided by Vite/Rolldown at build time
999
+ const __DEV__ = import.meta.env?.DEV === true
1000
+ if (__DEV__) console.warn('hello')`,
1001
+ notes:
1002
+ "The `typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'` pattern works in vitest (Node, `process` is defined) but is silently dead code in real Vite browser bundles because Vite does NOT polyfill `process` for the client. Every `console.warn` gated on the broken constant never fires for real users in dev mode — unit tests pass while users get nothing. Use `import.meta.env.DEV` instead — Vite/Rolldown literal-replace it at build time, prod tree-shakes the warning to zero bytes, and vitest sets it to `true` automatically. Server-only packages (`zero`, `core/server`, `core/runtime-server`, `vite-plugin`, `cli`, `lint`, `mcp`, `storybook`, `typescript`) and test files are exempt. Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`. The rule has an auto-fix that replaces the broken expression with `import.meta.env?.DEV === true`.",
1003
+ mistakes: `- Copying the \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` pattern from existing codebases — it works in Node but is dead in browser bundles
1004
+ - Trying to test with \`delete globalThis.process\` — vitest's own \`import.meta.env\` depends on \`process\`, so deleting it breaks the FIXED gate too (not because the gate is wrong, but because vitest can't resolve it)
1005
+ - Adding \`process: { env: { ... } }\` polyfills to vite.config.ts as a workaround — fix the source instead
1006
+ - Using the rule for server-only packages — they're correctly exempt because Node always has \`process\``,
1007
+ },
1008
+
898
1009
  // ═══════════════════════════════════════════════════════════════════════════
899
1010
  // @pyreon/ui-core
900
1011
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1145,4 +1256,65 @@ export const Primary: StoryObj<typeof meta> = {
1145
1256
  notes:
1146
1257
  'Storybook renderer for Pyreon components. Re-exports h, Fragment, signal, computed, effect, mount for story convenience.',
1147
1258
  },
1259
+
1260
+ // ═══════════════════════════════════════════════════════════════════════════
1261
+ // @pyreon/document-primitives
1262
+ // ═══════════════════════════════════════════════════════════════════════════
1263
+
1264
+ 'document-primitives/extractDocNode': {
1265
+ signature: 'extractDocNode(templateFn: () => VNode, options?: ExtractOptions): DocNode',
1266
+ example: `import {
1267
+ DocDocument, DocPage, DocHeading, DocText,
1268
+ extractDocNode,
1269
+ } from '@pyreon/document-primitives'
1270
+ import { download } from '@pyreon/document'
1271
+
1272
+ interface Resume { name: string; headline: string }
1273
+
1274
+ function ResumeTemplate(props: { resume: () => Resume }) {
1275
+ return (
1276
+ // title and author accept reactive accessors — extractDocNode
1277
+ // resolves them at extraction time, so each export click reads
1278
+ // the LIVE value from the underlying signal
1279
+ <DocDocument
1280
+ title={() => \`\${props.resume().name} — Resume\`}
1281
+ author={() => props.resume().name}
1282
+ >
1283
+ <DocPage>
1284
+ <DocHeading level="h1">{() => props.resume().name}</DocHeading>
1285
+ <DocText>{() => props.resume().headline}</DocText>
1286
+ </DocPage>
1287
+ </DocDocument>
1288
+ )
1289
+ }
1290
+
1291
+ // One-step extraction. The two-step createDocumentExport(...).getDocNode()
1292
+ // form is still exported for callers that want to pass the helper
1293
+ // object around, but extractDocNode is the recommended form.
1294
+ const tree = extractDocNode(() => <ResumeTemplate resume={store.resume} />)
1295
+ await download(tree, 'resume.pdf')
1296
+ await download(tree, 'resume.docx')
1297
+ await download(tree, 'resume.html')
1298
+ await download(tree, 'resume.md')`,
1299
+ notes:
1300
+ "18 primitives: DocDocument, DocPage, DocSection, DocRow, DocColumn, DocHeading, DocText, DocLink, DocImage, DocTable, DocList, DocListItem, DocCode, DocDivider, DocSpacer, DocButton, DocQuote, DocPageBreak. Same component tree renders in browser AND exports — primitives carry _documentType statics that extractDocumentTree (from @pyreon/connector-document) walks to produce a DocNode for @pyreon/document's render() to consume. DocDocument's title/author/subject accept either a string OR a `() => string` accessor; function values are stored in _documentProps and resolved at extraction time so reactive metadata works without `const initial = get()` workarounds. PR #197 also fixed a latent bug in extractDocumentTree: it now CALLS rocketstyle component functions to read post-attrs _documentProps, where before it only looked at the JSX vnode's props directly — every primitive's metadata was silently dropped during export until that fix landed.",
1301
+ mistakes: `- Calling props.title() at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: <DocDocument title={() => get().name}>
1302
+ - DocRow direction: layout props (direction, gap) go in .attrs() not .theme(). Element accepts 'inline' | 'rows' | 'reverseInline' | 'reverseRows' — 'row' is NOT valid
1303
+ - For text children reactivity, pass a signal accessor and read inside body: <DocText>{() => store.field()}</DocText>
1304
+ - Don't declare runtime-filled fields (tag, _documentProps) in the rocketstyle .attrs<P>() generic — they leak as required JSX props
1305
+ - Using createDocumentExport(...).getDocNode() in new code — prefer extractDocNode(fn) which is one call instead of two. createDocumentExport is kept for backward compat`,
1306
+ },
1307
+
1308
+ 'document-primitives/createDocumentExport': {
1309
+ signature:
1310
+ 'createDocumentExport(templateFn: () => VNode): { getDocNode(): DocNode }',
1311
+ example: `// Two-step form (kept for backward compat). New code should
1312
+ // prefer the one-step extractDocNode helper.
1313
+ import { createDocumentExport } from '@pyreon/document-primitives'
1314
+
1315
+ const helper = createDocumentExport(() => <Resume name="Aisha" />)
1316
+ const tree = helper.getDocNode()`,
1317
+ notes:
1318
+ "Wrapper around extractDocNode. The wrapper-object form is kept for callers that want to pass the helper around (e.g. to wrapper components that take a DocumentExport instance). New code should use extractDocNode(templateFn) which is one call instead of two.",
1319
+ },
1148
1320
  }