@portel/photon-core 1.4.0 → 2.1.0

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 (169) hide show
  1. package/README.md +123 -0
  2. package/dist/auto-ui.d.ts +103 -0
  3. package/dist/auto-ui.d.ts.map +1 -0
  4. package/dist/auto-ui.js +275 -0
  5. package/dist/auto-ui.js.map +1 -0
  6. package/dist/base.d.ts +9 -2
  7. package/dist/base.d.ts.map +1 -1
  8. package/dist/base.js +23 -10
  9. package/dist/base.js.map +1 -1
  10. package/dist/cli-ui-renderer.d.ts +31 -0
  11. package/dist/cli-ui-renderer.d.ts.map +1 -0
  12. package/dist/cli-ui-renderer.js +224 -0
  13. package/dist/cli-ui-renderer.js.map +1 -0
  14. package/dist/dependency-manager.d.ts.map +1 -1
  15. package/dist/dependency-manager.js +0 -1
  16. package/dist/dependency-manager.js.map +1 -1
  17. package/dist/design-system/index.d.ts +21 -0
  18. package/dist/design-system/index.d.ts.map +1 -0
  19. package/dist/design-system/index.js +27 -0
  20. package/dist/design-system/index.js.map +1 -0
  21. package/dist/design-system/tokens.d.ts +149 -0
  22. package/dist/design-system/tokens.d.ts.map +1 -0
  23. package/dist/design-system/tokens.js +413 -0
  24. package/dist/design-system/tokens.js.map +1 -0
  25. package/dist/design-system/transaction-ui.d.ts +70 -0
  26. package/dist/design-system/transaction-ui.d.ts.map +1 -0
  27. package/dist/design-system/transaction-ui.js +982 -0
  28. package/dist/design-system/transaction-ui.js.map +1 -0
  29. package/dist/generator.d.ts +58 -8
  30. package/dist/generator.d.ts.map +1 -1
  31. package/dist/generator.js +9 -4
  32. package/dist/generator.js.map +1 -1
  33. package/dist/index.d.ts +10 -7
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +48 -44
  36. package/dist/index.js.map +1 -1
  37. package/dist/io.d.ts +395 -0
  38. package/dist/io.d.ts.map +1 -0
  39. package/dist/io.js +304 -0
  40. package/dist/io.js.map +1 -0
  41. package/dist/path-resolver.d.ts.map +1 -1
  42. package/dist/path-resolver.js +2 -1
  43. package/dist/path-resolver.js.map +1 -1
  44. package/dist/rendering/components.d.ts +29 -0
  45. package/dist/rendering/components.d.ts.map +1 -0
  46. package/dist/rendering/components.js +773 -0
  47. package/dist/rendering/components.js.map +1 -0
  48. package/dist/rendering/field-analyzer.d.ts +48 -0
  49. package/dist/rendering/field-analyzer.d.ts.map +1 -0
  50. package/dist/rendering/field-analyzer.js +270 -0
  51. package/dist/rendering/field-analyzer.js.map +1 -0
  52. package/dist/rendering/field-renderers.d.ts +64 -0
  53. package/dist/rendering/field-renderers.d.ts.map +1 -0
  54. package/dist/rendering/field-renderers.js +317 -0
  55. package/dist/rendering/field-renderers.js.map +1 -0
  56. package/dist/rendering/index.d.ts +28 -0
  57. package/dist/rendering/index.d.ts.map +1 -0
  58. package/dist/rendering/index.js +60 -0
  59. package/dist/rendering/index.js.map +1 -0
  60. package/dist/rendering/layout-selector.d.ts +48 -0
  61. package/dist/rendering/layout-selector.d.ts.map +1 -0
  62. package/dist/rendering/layout-selector.js +347 -0
  63. package/dist/rendering/layout-selector.js.map +1 -0
  64. package/dist/rendering/template-engine.d.ts +41 -0
  65. package/dist/rendering/template-engine.d.ts.map +1 -0
  66. package/dist/rendering/template-engine.js +236 -0
  67. package/dist/rendering/template-engine.js.map +1 -0
  68. package/dist/schema-extractor.d.ts +30 -0
  69. package/dist/schema-extractor.d.ts.map +1 -1
  70. package/dist/schema-extractor.js +205 -12
  71. package/dist/schema-extractor.js.map +1 -1
  72. package/dist/stateful.d.ts +63 -0
  73. package/dist/stateful.d.ts.map +1 -1
  74. package/dist/stateful.js +222 -0
  75. package/dist/stateful.js.map +1 -1
  76. package/dist/types.d.ts +9 -1
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/ucp/ap2/handlers.d.ts +242 -0
  80. package/dist/ucp/ap2/handlers.d.ts.map +1 -0
  81. package/dist/ucp/ap2/handlers.js +482 -0
  82. package/dist/ucp/ap2/handlers.js.map +1 -0
  83. package/dist/ucp/ap2/mandates.d.ts +95 -0
  84. package/dist/ucp/ap2/mandates.d.ts.map +1 -0
  85. package/dist/ucp/ap2/mandates.js +234 -0
  86. package/dist/ucp/ap2/mandates.js.map +1 -0
  87. package/dist/ucp/ap2/types.d.ts +305 -0
  88. package/dist/ucp/ap2/types.d.ts.map +1 -0
  89. package/dist/ucp/ap2/types.js +8 -0
  90. package/dist/ucp/ap2/types.js.map +1 -0
  91. package/dist/ucp/capabilities/checkout.d.ts +118 -0
  92. package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
  93. package/dist/ucp/capabilities/checkout.js +344 -0
  94. package/dist/ucp/capabilities/checkout.js.map +1 -0
  95. package/dist/ucp/capabilities/identity.d.ts +130 -0
  96. package/dist/ucp/capabilities/identity.d.ts.map +1 -0
  97. package/dist/ucp/capabilities/identity.js +290 -0
  98. package/dist/ucp/capabilities/identity.js.map +1 -0
  99. package/dist/ucp/capabilities/order.d.ts +142 -0
  100. package/dist/ucp/capabilities/order.d.ts.map +1 -0
  101. package/dist/ucp/capabilities/order.js +383 -0
  102. package/dist/ucp/capabilities/order.js.map +1 -0
  103. package/dist/ucp/index.d.ts +18 -0
  104. package/dist/ucp/index.d.ts.map +1 -0
  105. package/dist/ucp/index.js +19 -0
  106. package/dist/ucp/index.js.map +1 -0
  107. package/dist/ucp/manifest.d.ts +62 -0
  108. package/dist/ucp/manifest.d.ts.map +1 -0
  109. package/dist/ucp/manifest.js +180 -0
  110. package/dist/ucp/manifest.js.map +1 -0
  111. package/dist/ucp/types.d.ts +327 -0
  112. package/dist/ucp/types.d.ts.map +1 -0
  113. package/dist/ucp/types.js +8 -0
  114. package/dist/ucp/types.js.map +1 -0
  115. package/package.json +3 -4
  116. package/src/auto-ui.ts +413 -0
  117. package/src/base.ts +22 -9
  118. package/src/cli-ui-renderer.ts +264 -0
  119. package/src/dependency-manager.ts +0 -1
  120. package/src/design-system/index.ts +30 -0
  121. package/src/design-system/tokens.ts +451 -0
  122. package/src/design-system/transaction-ui.ts +1038 -0
  123. package/src/generator.ts +68 -8
  124. package/src/index.ts +159 -101
  125. package/src/io.ts +493 -0
  126. package/src/path-resolver.ts +2 -1
  127. package/src/rendering/components.ts +785 -0
  128. package/src/rendering/field-analyzer.ts +299 -0
  129. package/src/rendering/field-renderers.ts +356 -0
  130. package/src/rendering/index.ts +63 -0
  131. package/src/rendering/layout-selector.ts +390 -0
  132. package/src/rendering/template-engine.ts +254 -0
  133. package/src/schema-extractor.ts +225 -12
  134. package/src/stateful.ts +301 -0
  135. package/src/types.ts +10 -1
  136. package/src/ucp/ap2/handlers.ts +779 -0
  137. package/src/ucp/ap2/mandates.ts +354 -0
  138. package/src/ucp/ap2/types.ts +441 -0
  139. package/src/ucp/capabilities/checkout.ts +497 -0
  140. package/src/ucp/capabilities/identity.ts +425 -0
  141. package/src/ucp/capabilities/order.ts +549 -0
  142. package/src/ucp/index.ts +27 -0
  143. package/src/ucp/manifest.ts +257 -0
  144. package/src/ucp/types.ts +454 -0
  145. package/dist/cli-formatter.d.ts +0 -92
  146. package/dist/cli-formatter.d.ts.map +0 -1
  147. package/dist/cli-formatter.js +0 -486
  148. package/dist/cli-formatter.js.map +0 -1
  149. package/dist/elicit.d.ts +0 -93
  150. package/dist/elicit.d.ts.map +0 -1
  151. package/dist/elicit.js +0 -373
  152. package/dist/elicit.js.map +0 -1
  153. package/dist/mcp-client.d.ts +0 -218
  154. package/dist/mcp-client.d.ts.map +0 -1
  155. package/dist/mcp-client.js +0 -424
  156. package/dist/mcp-client.js.map +0 -1
  157. package/dist/mcp-sdk-transport.d.ts +0 -88
  158. package/dist/mcp-sdk-transport.d.ts.map +0 -1
  159. package/dist/mcp-sdk-transport.js +0 -360
  160. package/dist/mcp-sdk-transport.js.map +0 -1
  161. package/dist/photon-config.d.ts +0 -86
  162. package/dist/photon-config.d.ts.map +0 -1
  163. package/dist/photon-config.js +0 -156
  164. package/dist/photon-config.js.map +0 -1
  165. package/src/cli-formatter.ts +0 -579
  166. package/src/elicit.ts +0 -438
  167. package/src/mcp-client.ts +0 -561
  168. package/src/mcp-sdk-transport.ts +0 -449
  169. package/src/photon-config.ts +0 -201
@@ -61,6 +61,12 @@ export class SchemaExtractor {
61
61
  // Helper to process a method declaration
62
62
  const processMethod = (member: ts.MethodDeclaration) => {
63
63
  const methodName = member.name.getText(sourceFile);
64
+
65
+ // Skip private methods (prefixed with _)
66
+ if (methodName.startsWith('_')) {
67
+ return;
68
+ }
69
+
64
70
  const jsdoc = this.getJSDocComment(member, sourceFile);
65
71
 
66
72
  // Check if this is an async generator method (has asterisk token)
@@ -129,17 +135,25 @@ export class SchemaExtractor {
129
135
  // Otherwise, it's a regular tool
130
136
  else {
131
137
  const outputFormat = this.extractFormat(jsdoc);
138
+ const layoutHints = this.extractLayoutHints(jsdoc);
139
+ const buttonLabel = this.extractButtonLabel(jsdoc);
140
+ const icon = this.extractIcon(jsdoc);
132
141
  const yields = isGenerator ? this.extractYieldsFromJSDoc(jsdoc) : undefined;
133
142
  const isStateful = this.hasStatefulTag(jsdoc);
143
+ const autorun = this.hasAutorunTag(jsdoc);
134
144
 
135
145
  tools.push({
136
146
  name: methodName,
137
147
  description,
138
148
  inputSchema,
139
149
  ...(outputFormat ? { outputFormat } : {}),
150
+ ...(layoutHints ? { layoutHints } : {}),
151
+ ...(buttonLabel ? { buttonLabel } : {}),
152
+ ...(icon ? { icon } : {}),
140
153
  ...(isGenerator ? { isGenerator: true } : {}),
141
154
  ...(yields && yields.length > 0 ? { yields } : {}),
142
155
  ...(isStateful ? { isStateful: true } : {}),
156
+ ...(autorun ? { autorun: true } : {}),
143
157
  });
144
158
  }
145
159
  };
@@ -219,6 +233,19 @@ export class SchemaExtractor {
219
233
  const properties: Record<string, any> = {};
220
234
  const required: string[] = [];
221
235
 
236
+ // Handle union types (e.g., { ip: string } | string)
237
+ // Extract properties from the first object type member
238
+ if (ts.isUnionTypeNode(typeNode)) {
239
+ for (const memberType of typeNode.types) {
240
+ if (ts.isTypeLiteralNode(memberType)) {
241
+ // Found an object type in the union, extract its properties
242
+ return this.buildSchemaFromType(memberType, sourceFile);
243
+ }
244
+ }
245
+ // No object type found in union, return empty
246
+ return { properties, required };
247
+ }
248
+
222
249
  // Handle type literal (object type)
223
250
  if (ts.isTypeLiteralNode(typeNode)) {
224
251
  typeNode.members.forEach((member) => {
@@ -570,6 +597,54 @@ export class SchemaExtractor {
570
597
  * Extract parameter descriptions from JSDoc @param tags
571
598
  * Also removes constraint tags from descriptions
572
599
  */
600
+ /**
601
+ * Remove {@example ...} tags that may contain nested braces/brackets (JSON)
602
+ */
603
+ private removeExampleTags(text: string): string {
604
+ let result = text;
605
+ let searchStart = 0;
606
+
607
+ while (true) {
608
+ const exampleStart = result.indexOf('{@example ', searchStart);
609
+ if (exampleStart === -1) break;
610
+
611
+ const contentStart = exampleStart + '{@example '.length;
612
+ let braceDepth = 0;
613
+ let bracketDepth = 0;
614
+ let i = contentStart;
615
+ let inString = false;
616
+
617
+ while (i < result.length) {
618
+ const ch = result[i];
619
+ const prevCh = i > 0 ? result[i - 1] : '';
620
+
621
+ if (ch === '"' && prevCh !== '\\') {
622
+ inString = !inString;
623
+ } else if (!inString) {
624
+ if (ch === '{') braceDepth++;
625
+ else if (ch === '[') bracketDepth++;
626
+ else if (ch === ']') bracketDepth--;
627
+ else if (ch === '}') {
628
+ if (braceDepth === 0 && bracketDepth === 0) {
629
+ // Found the closing brace of the {@example} tag
630
+ result = result.substring(0, exampleStart) + result.substring(i + 1);
631
+ break;
632
+ }
633
+ braceDepth--;
634
+ }
635
+ }
636
+ i++;
637
+ }
638
+
639
+ // Safety: if we didn't find closing brace, move past this tag
640
+ if (i >= result.length) {
641
+ searchStart = exampleStart + 1;
642
+ }
643
+ }
644
+
645
+ return result;
646
+ }
647
+
573
648
  private extractParamDocs(jsdocContent: string): Map<string, string> {
574
649
  const paramDocs = new Map<string, string>();
575
650
  const paramRegex = /@param\s+(\w+)\s+(.+)/g;
@@ -577,19 +652,25 @@ export class SchemaExtractor {
577
652
  let match;
578
653
  while ((match = paramRegex.exec(jsdocContent)) !== null) {
579
654
  const [, paramName, description] = match;
580
- // Remove all constraint tags from description
581
- const cleanDesc = description
655
+ // Remove {@example} tags first (handles nested braces/brackets)
656
+ let cleanDesc = this.removeExampleTags(description);
657
+ // Remove other constraint tags from description
658
+ cleanDesc = cleanDesc
582
659
  .replace(/\{@min\s+[^}]+\}/g, '')
583
660
  .replace(/\{@max\s+[^}]+\}/g, '')
584
661
  .replace(/\{@pattern\s+[^}]+\}/g, '')
585
662
  .replace(/\{@format\s+[^}]+\}/g, '')
663
+ .replace(/\{@choice\s+[^}]+\}/g, '')
664
+ .replace(/\{@field\s+[^}]+\}/g, '')
586
665
  .replace(/\{@default\s+[^}]+\}/g, '')
587
666
  .replace(/\{@unique(?:Items)?\s*\}/g, '')
588
- .replace(/\{@example\s+[^}]+\}/g, '')
589
667
  .replace(/\{@multipleOf\s+[^}]+\}/g, '')
590
668
  .replace(/\{@deprecated(?:\s+[^}]+)?\}/g, '')
591
669
  .replace(/\{@readOnly\s*\}/g, '')
592
670
  .replace(/\{@writeOnly\s*\}/g, '')
671
+ .replace(/\{@label\s+[^}]+\}/g, '')
672
+ .replace(/\{@placeholder\s+[^}]+\}/g, '')
673
+ .replace(/\{@hint\s+[^}]+\}/g, '')
593
674
  .replace(/\s+/g, ' ') // Collapse multiple spaces
594
675
  .trim();
595
676
  paramDocs.set(paramName, cleanDesc);
@@ -636,6 +717,19 @@ export class SchemaExtractor {
636
717
  paramConstraints.format = formatMatch[1].trim();
637
718
  }
638
719
 
720
+ // Extract {@choice value1,value2,...} - converts to enum in schema
721
+ const choiceMatch = description.match(/\{@choice\s+([^}]+)\}/);
722
+ if (choiceMatch) {
723
+ const choices = choiceMatch[1].split(',').map((c: string) => c.trim());
724
+ paramConstraints.enum = choices;
725
+ }
726
+
727
+ // Extract {@field type} - hints for UI form rendering
728
+ const fieldMatch = description.match(/\{@field\s+([a-z]+)\}/);
729
+ if (fieldMatch) {
730
+ paramConstraints.field = fieldMatch[1].trim();
731
+ }
732
+
639
733
  // Extract {@default value} - use lookahead to match until tag-closing }
640
734
  const defaultMatch = description.match(/\{@default\s+(.+?)\}(?=\s|$|{@)/);
641
735
  if (defaultMatch) {
@@ -702,6 +796,29 @@ export class SchemaExtractor {
702
796
  }
703
797
  }
704
798
 
799
+ // Extract {@label displayName} - custom label for form fields
800
+ const labelMatch = description.match(/\{@label\s+([^}]+)\}/);
801
+ if (labelMatch) {
802
+ paramConstraints.label = labelMatch[1].trim();
803
+ }
804
+
805
+ // Extract {@placeholder text} - placeholder text for input fields
806
+ const placeholderMatch = description.match(/\{@placeholder\s+([^}]+)\}/);
807
+ if (placeholderMatch) {
808
+ paramConstraints.placeholder = placeholderMatch[1].trim();
809
+ }
810
+
811
+ // Extract {@hint text} - help text shown below/beside the field
812
+ const hintMatch = description.match(/\{@hint\s+([^}]+)\}/);
813
+ if (hintMatch) {
814
+ paramConstraints.hint = hintMatch[1].trim();
815
+ }
816
+
817
+ // Extract {@hidden} - hide field from UI forms (for programmatic use only)
818
+ if (description.match(/\{@hidden\s*\}/)) {
819
+ paramConstraints.hidden = true;
820
+ }
821
+
705
822
  if (Object.keys(paramConstraints).length > 0) {
706
823
  constraints.set(paramName, paramConstraints);
707
824
  }
@@ -776,6 +893,26 @@ export class SchemaExtractor {
776
893
  if (constraints.deprecated !== undefined) {
777
894
  s.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
778
895
  }
896
+ // Apply enum from @choice tag (overrides TypeScript-derived enum if present)
897
+ if (constraints.enum !== undefined && !s.enum) {
898
+ s.enum = constraints.enum;
899
+ }
900
+ // Apply field hint for UI rendering
901
+ if (constraints.field !== undefined) {
902
+ s.field = constraints.field;
903
+ }
904
+ // Apply custom label for form fields
905
+ if (constraints.label !== undefined) {
906
+ s.title = constraints.label; // JSON Schema uses 'title' for display label
907
+ }
908
+ // Apply placeholder for input fields
909
+ if (constraints.placeholder !== undefined) {
910
+ s.placeholder = constraints.placeholder;
911
+ }
912
+ // Apply hint text for form fields
913
+ if (constraints.hint !== undefined) {
914
+ s.hint = constraints.hint;
915
+ }
779
916
 
780
917
  // readOnly and writeOnly are mutually exclusive
781
918
  // JSDoc takes precedence over TypeScript
@@ -787,6 +924,11 @@ export class SchemaExtractor {
787
924
  s.writeOnly = true;
788
925
  delete s.readOnly; // Clear readOnly if writeOnly is set
789
926
  }
927
+
928
+ // Apply hidden flag for UI forms
929
+ if (constraints.hidden === true) {
930
+ s.hidden = true;
931
+ }
790
932
  };
791
933
 
792
934
  // Apply to anyOf schemas or direct schema
@@ -828,6 +970,14 @@ export class SchemaExtractor {
828
970
  return /@stateful/i.test(jsdocContent);
829
971
  }
830
972
 
973
+ /**
974
+ * Check if JSDoc contains @autorun tag
975
+ * Indicates this method should auto-execute when selected (idempotent, no required params)
976
+ */
977
+ private hasAutorunTag(jsdocContent: string): boolean {
978
+ return /@autorun/i.test(jsdocContent);
979
+ }
980
+
831
981
  /**
832
982
  * Extract URI pattern from @Static tag
833
983
  * Example: @Static github://repos/{owner}/{repo}/readme
@@ -839,29 +989,92 @@ export class SchemaExtractor {
839
989
 
840
990
  /**
841
991
  * Extract format hint from @format tag
992
+ * Supports nested syntax: @format list {@title name, @subtitle email}
842
993
  * Example: @format table
843
994
  * Example: @format json
844
995
  * Example: @format code:typescript
996
+ * Example: @format list {@title name, @subtitle email, @style inset}
845
997
  */
846
998
  private extractFormat(jsdocContent: string): OutputFormat | undefined {
999
+ // Match format with optional nested hints: @format list {...}
1000
+ // The nested hints are extracted separately by extractLayoutHints
1001
+ const formatMatch = jsdocContent.match(/@format\s+(\w+)(?::(\w+))?/i);
1002
+ if (!formatMatch) return undefined;
1003
+
1004
+ const format = formatMatch[1].toLowerCase();
1005
+ const subtype = formatMatch[2];
1006
+
847
1007
  // Match structural formats
848
- const structuralMatch = jsdocContent.match(/@format\s+(primitive|table|tree|list|none)/i);
849
- if (structuralMatch) {
850
- return structuralMatch[1].toLowerCase() as OutputFormat;
1008
+ if (['primitive', 'table', 'tree', 'list', 'none', 'card', 'grid', 'chips', 'kv'].includes(format)) {
1009
+ return format as OutputFormat;
851
1010
  }
852
1011
 
853
1012
  // Match content formats
854
- const contentMatch = jsdocContent.match(/@format\s+(json|markdown|yaml|xml|html)/i);
855
- if (contentMatch) {
856
- return contentMatch[1].toLowerCase() as OutputFormat;
1013
+ if (['json', 'markdown', 'yaml', 'xml', 'html', 'mermaid'].includes(format)) {
1014
+ return format as OutputFormat;
857
1015
  }
858
1016
 
859
1017
  // Match code format (with optional language)
860
- const codeMatch = jsdocContent.match(/@format\s+code(?::(\w+))?/i);
861
- if (codeMatch) {
862
- return codeMatch[1] ? `code:${codeMatch[1]}` as OutputFormat : 'code';
1018
+ if (format === 'code') {
1019
+ return subtype ? `code:${subtype}` as OutputFormat : 'code';
1020
+ }
1021
+
1022
+ return undefined;
1023
+ }
1024
+
1025
+ /**
1026
+ * Extract layout hints from nested @format syntax
1027
+ * Example: @format list {@title name, @subtitle email, @icon avatar, @style inset}
1028
+ * Returns: { title: 'name', subtitle: 'email', icon: 'avatar', style: 'inset' }
1029
+ */
1030
+ private extractLayoutHints(jsdocContent: string): Record<string, string> | undefined {
1031
+ // Match @format TYPE {hints}
1032
+ const match = jsdocContent.match(/@format\s+\w+(?::\w+)?\s*\{([^}]+)\}/i);
1033
+ if (!match) return undefined;
1034
+
1035
+ const hintsString = match[1];
1036
+ const hints: Record<string, string> = {};
1037
+
1038
+ // Parse comma-separated hints: @title name, @subtitle email:link
1039
+ const parts = hintsString.split(',').map(s => s.trim());
1040
+
1041
+ for (const part of parts) {
1042
+ // Match @key value or @key value:renderer
1043
+ const hintMatch = part.match(/@(\w+)\s+([^\s,]+(?:\s+[^\s@,][^\s,]*)*)/);
1044
+ if (hintMatch) {
1045
+ const [, key, value] = hintMatch;
1046
+ hints[key.toLowerCase()] = value.trim();
1047
+ }
1048
+ }
1049
+
1050
+ return Object.keys(hints).length > 0 ? hints : undefined;
1051
+ }
1052
+
1053
+ /**
1054
+ * Extract button label from @returns {@label} tag
1055
+ * Example: @returns {@label Calculate Sum} The result
1056
+ * Example: @returns {@label Run Query}
1057
+ */
1058
+ private extractButtonLabel(jsdocContent: string): string | undefined {
1059
+ // Look for {@label ...} in @returns tag
1060
+ const returnsMatch = jsdocContent.match(/@returns?\s+.*?\{@label\s+([^}]+)\}/i);
1061
+ if (returnsMatch) {
1062
+ return returnsMatch[1].trim();
863
1063
  }
1064
+ return undefined;
1065
+ }
864
1066
 
1067
+ /**
1068
+ * Extract icon from @icon tag
1069
+ * Example: @icon calculator
1070
+ * Example: @icon 🧮
1071
+ * Example: @icon mdi:calculator
1072
+ */
1073
+ private extractIcon(jsdocContent: string): string | undefined {
1074
+ const iconMatch = jsdocContent.match(/@icon\s+([^\s@*]+)/i);
1075
+ if (iconMatch) {
1076
+ return iconMatch[1].trim();
1077
+ }
865
1078
  return undefined;
866
1079
  }
867
1080
 
package/src/stateful.ts CHANGED
@@ -56,6 +56,7 @@ import * as path from 'path';
56
56
  import * as os from 'os';
57
57
  import { createReadStream } from 'fs';
58
58
  import { createInterface } from 'readline';
59
+ import { executionContext } from '@portel/cli';
59
60
  import type {
60
61
  StateLogEntry,
61
62
  StateLogStart,
@@ -641,6 +642,306 @@ export async function cleanupRuns(maxAgeMs: number, runsDir?: string): Promise<n
641
642
  return deleted;
642
643
  }
643
644
 
645
+ // ══════════════════════════════════════════════════════════════════════════════
646
+ // IMPLICIT STATEFUL EXECUTOR - Auto-detect checkpoint usage
647
+ // ══════════════════════════════════════════════════════════════════════════════
648
+
649
+ /**
650
+ * Configuration for maybeStatefulExecute
651
+ */
652
+ export interface MaybeStatefulConfig {
653
+ /** Photon name */
654
+ photon: string;
655
+ /** Tool name being executed */
656
+ tool: string;
657
+ /** Input parameters */
658
+ params: Record<string, any>;
659
+ /** Input provider for ask yields */
660
+ inputProvider: InputProvider;
661
+ /** Output handler for emit yields */
662
+ outputHandler?: OutputHandler;
663
+ /** Runs directory (defaults to ~/.photon/runs) */
664
+ runsDir?: string;
665
+ /** Existing run ID to resume (if set, forces stateful mode) */
666
+ resumeRunId?: string;
667
+ }
668
+
669
+ /**
670
+ * Result of maybe-stateful execution
671
+ */
672
+ export interface MaybeStatefulResult<T> {
673
+ /** Final result (if completed) */
674
+ result?: T;
675
+ /** Error message (if failed) */
676
+ error?: string;
677
+ /** Run ID (only if stateful - checkpoint was yielded) */
678
+ runId?: string;
679
+ /** Was this a stateful workflow? */
680
+ isStateful: boolean;
681
+ /** Was this resumed from a previous run? */
682
+ resumed: boolean;
683
+ /** Step number resumed from (if resumed) */
684
+ resumedFromStep?: number;
685
+ /** Total checkpoints completed */
686
+ checkpointsCompleted: number;
687
+ /** Final status */
688
+ status: WorkflowStatus;
689
+ }
690
+
691
+ /**
692
+ * Buffered event from early execution (before stateful mode detected)
693
+ */
694
+ interface BufferedEvent {
695
+ type: 'emit' | 'ask' | 'answer';
696
+ data: any;
697
+ }
698
+
699
+ /**
700
+ * Execute a generator with implicit checkpoint detection
701
+ *
702
+ * - If the generator yields checkpoint, automatically becomes stateful (JSONL persistence)
703
+ * - If no checkpoint, runs as ephemeral (no persistence overhead)
704
+ * - Seamlessly handles resume if runId is provided
705
+ *
706
+ * @example
707
+ * const result = await maybeStatefulExecute(
708
+ * () => myPhoton.myMethod(params),
709
+ * {
710
+ * photon: 'my-photon',
711
+ * tool: 'myMethod',
712
+ * params,
713
+ * inputProvider: cliInputProvider
714
+ * }
715
+ * );
716
+ *
717
+ * if (result.isStateful) {
718
+ * console.log(`Run ID: ${result.runId}`);
719
+ * }
720
+ */
721
+ export async function maybeStatefulExecute<T>(
722
+ generatorFn: () => AsyncGenerator<StatefulYield, T, any> | Promise<T>,
723
+ config: MaybeStatefulConfig
724
+ ): Promise<MaybeStatefulResult<T>> {
725
+ return executionContext.run({ outputHandler: config.outputHandler }, async () => {
726
+ // If resuming, use stateful executor directly
727
+ if (config.resumeRunId) {
728
+ // Get resume state to find step number
729
+ const resumeState = await parseResumeState(config.resumeRunId, config.runsDir);
730
+
731
+ // Validate run exists
732
+ if (!resumeState) {
733
+ return {
734
+ error: `Run not found: ${config.resumeRunId}`,
735
+ isStateful: false,
736
+ resumed: false,
737
+ checkpointsCompleted: 0,
738
+ status: 'failed',
739
+ };
740
+ }
741
+
742
+ // Check if already completed
743
+ if (resumeState.isComplete) {
744
+ return {
745
+ result: resumeState.result,
746
+ error: resumeState.error,
747
+ runId: config.resumeRunId,
748
+ isStateful: true,
749
+ resumed: true,
750
+ resumedFromStep: resumeState.lastCheckpoint?.state?.step as number | undefined,
751
+ checkpointsCompleted: resumeState.entries.filter(e => e.t === 'checkpoint').length,
752
+ status: resumeState.error ? 'failed' : 'completed',
753
+ };
754
+ }
755
+
756
+ const resumedFromStep = resumeState.lastCheckpoint?.state?.step as number | undefined;
757
+
758
+ // Count existing checkpoints
759
+ const existingCheckpoints = resumeState.entries.filter(e => e.t === 'checkpoint').length;
760
+
761
+ const statefulResult = await executeStatefulGenerator<T>(
762
+ generatorFn as () => AsyncGenerator<StatefulYield, T, any>,
763
+ {
764
+ runId: config.resumeRunId,
765
+ runsDir: config.runsDir,
766
+ photon: config.photon,
767
+ tool: config.tool,
768
+ params: config.params,
769
+ inputProvider: config.inputProvider,
770
+ outputHandler: config.outputHandler,
771
+ resume: true,
772
+ }
773
+ );
774
+
775
+ // Count total checkpoints after execution
776
+ const finalState = await parseResumeState(config.resumeRunId, config.runsDir);
777
+ const totalCheckpoints = finalState?.entries.filter(e => e.t === 'checkpoint').length || existingCheckpoints;
778
+
779
+ return {
780
+ result: statefulResult.result,
781
+ error: statefulResult.error,
782
+ runId: statefulResult.runId,
783
+ isStateful: true,
784
+ resumed: statefulResult.resumed,
785
+ resumedFromStep,
786
+ checkpointsCompleted: totalCheckpoints,
787
+ status: statefulResult.status,
788
+ };
789
+ }
790
+
791
+ // Call the function
792
+ const maybeGenerator = generatorFn();
793
+
794
+ // Handle non-generator functions (regular async methods)
795
+ if (!isAsyncGenerator(maybeGenerator)) {
796
+ try {
797
+ const finalValue = await maybeGenerator;
798
+ return {
799
+ result: finalValue,
800
+ isStateful: false,
801
+ resumed: false,
802
+ checkpointsCompleted: 0,
803
+ status: 'completed',
804
+ };
805
+ } catch (error: any) {
806
+ return {
807
+ error: error.message,
808
+ isStateful: false,
809
+ resumed: false,
810
+ checkpointsCompleted: 0,
811
+ status: 'failed',
812
+ };
813
+ }
814
+ }
815
+
816
+ // It's a generator - execute with checkpoint detection
817
+ const generator = maybeGenerator;
818
+ const bufferedEvents: BufferedEvent[] = [];
819
+ let isStateful = false;
820
+ let runId: string | undefined;
821
+ let log: StateLog | undefined;
822
+ let checkpointIndex = 0;
823
+ let askIndex = 0;
824
+
825
+ /**
826
+ * Initialize stateful mode on first checkpoint
827
+ */
828
+ async function initStateful(): Promise<void> {
829
+ if (isStateful) return;
830
+
831
+ isStateful = true;
832
+ runId = generateRunId();
833
+ log = new StateLog(runId, config.runsDir);
834
+ await log.init();
835
+
836
+ // Write start entry
837
+ await log.writeStart(config.tool, config.params);
838
+
839
+ // Replay buffered events to log
840
+ for (const event of bufferedEvents) {
841
+ if (event.type === 'emit') {
842
+ const emit = event.data as EmitYield;
843
+ await log.writeEmit(emit.emit, (emit as any).message, emit);
844
+ } else if (event.type === 'ask') {
845
+ const { id, ask, message } = event.data;
846
+ await log.writeAsk(id, ask, message);
847
+ } else if (event.type === 'answer') {
848
+ const { id, value } = event.data;
849
+ await log.writeAnswer(id, value);
850
+ }
851
+ }
852
+ bufferedEvents.length = 0; // Clear buffer
853
+ }
854
+
855
+ try {
856
+ let result = await generator.next();
857
+
858
+ while (!result.done) {
859
+ const yielded = result.value;
860
+
861
+ if (isCheckpointYield(yielded)) {
862
+ // First checkpoint triggers stateful mode
863
+ await initStateful();
864
+
865
+ const cpId = yielded.id || `cp_${checkpointIndex++}`;
866
+ await log!.writeCheckpoint(cpId, yielded.state);
867
+
868
+ // Continue with the state
869
+ result = await generator.next(yielded.state);
870
+ } else if (isAskYield(yielded as PhotonYield)) {
871
+ const askYield = yielded as AskYield;
872
+ const askId = askYield.id || `ask_${askIndex++}`;
873
+
874
+ // Buffer or log ask
875
+ if (isStateful) {
876
+ await log!.writeAsk(askId, askYield.ask, askYield.message);
877
+ } else {
878
+ bufferedEvents.push({ type: 'ask', data: { id: askId, ask: askYield.ask, message: askYield.message } });
879
+ }
880
+
881
+ // Get input
882
+ const input = await config.inputProvider(askYield);
883
+
884
+ // Buffer or log answer
885
+ if (isStateful) {
886
+ await log!.writeAnswer(askId, input);
887
+ } else {
888
+ bufferedEvents.push({ type: 'answer', data: { id: askId, value: input } });
889
+ }
890
+
891
+ result = await generator.next(input);
892
+ } else if (isEmitYield(yielded as PhotonYield)) {
893
+ const emitYield = yielded as EmitYield;
894
+
895
+ // Buffer or log emit
896
+ if (isStateful) {
897
+ await log!.writeEmit(emitYield.emit, (emitYield as any).message, emitYield);
898
+ } else {
899
+ bufferedEvents.push({ type: 'emit', data: emitYield });
900
+ }
901
+
902
+ // Call output handler
903
+ if (config.outputHandler) {
904
+ await config.outputHandler(emitYield);
905
+ }
906
+
907
+ result = await generator.next();
908
+ } else {
909
+ // Unknown yield, skip
910
+ result = await generator.next();
911
+ }
912
+ }
913
+
914
+ // Write return entry if stateful
915
+ if (isStateful && log) {
916
+ await log.writeReturn(result.value);
917
+ }
918
+
919
+ return {
920
+ result: result.value,
921
+ runId,
922
+ isStateful,
923
+ resumed: false,
924
+ checkpointsCompleted: checkpointIndex,
925
+ status: 'completed',
926
+ };
927
+ } catch (error: any) {
928
+ // Write error entry if stateful
929
+ if (isStateful && log) {
930
+ await log.writeError(error.message, error.stack);
931
+ }
932
+
933
+ return {
934
+ error: error.message,
935
+ runId,
936
+ isStateful,
937
+ resumed: false,
938
+ checkpointsCompleted: checkpointIndex,
939
+ status: 'failed',
940
+ };
941
+ }
942
+ });
943
+ }
944
+
644
945
  // ══════════════════════════════════════════════════════════════════════════════
645
946
  // EXPORTS
646
947
  // ══════════════════════════════════════════════════════════════════════════════
package/src/types.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  */
10
10
  export type OutputFormat =
11
11
  | 'primitive' | 'table' | 'tree' | 'list' | 'none'
12
- | 'json' | 'markdown' | 'yaml' | 'xml' | 'html'
12
+ | 'json' | 'markdown' | 'yaml' | 'xml' | 'html' | 'mermaid'
13
+ | 'card' | 'grid' | 'chips' | 'kv' | 'tabs' | 'accordion'
13
14
  | `code` | `code:${string}`;
14
15
 
15
16
  export interface PhotonTool {
@@ -48,12 +49,20 @@ export interface ExtractedSchema {
48
49
  required?: string[];
49
50
  };
50
51
  outputFormat?: OutputFormat;
52
+ /** Layout hints from nested @format syntax: @format list {@title name, @subtitle email} */
53
+ layoutHints?: Record<string, string>;
54
+ /** Custom button label from @returns {@label} tag */
55
+ buttonLabel?: string;
56
+ /** Icon from @icon tag (emoji or icon name) */
57
+ icon?: string;
51
58
  /** True if this method is an async generator (uses yield for prompts) */
52
59
  isGenerator?: boolean;
53
60
  /** Yield information for generator methods (used by REST APIs) */
54
61
  yields?: YieldInfo[];
55
62
  /** True if this is a stateful workflow (supports checkpoint/resume) */
56
63
  isStateful?: boolean;
64
+ /** True if this method should auto-execute when selected (idempotent, no required params) */
65
+ autorun?: boolean;
57
66
  }
58
67
 
59
68
  export interface PhotonMCPClass {