@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.
- package/README.md +123 -0
- package/dist/auto-ui.d.ts +103 -0
- package/dist/auto-ui.d.ts.map +1 -0
- package/dist/auto-ui.js +275 -0
- package/dist/auto-ui.js.map +1 -0
- package/dist/base.d.ts +9 -2
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +23 -10
- package/dist/base.js.map +1 -1
- package/dist/cli-ui-renderer.d.ts +31 -0
- package/dist/cli-ui-renderer.d.ts.map +1 -0
- package/dist/cli-ui-renderer.js +224 -0
- package/dist/cli-ui-renderer.js.map +1 -0
- package/dist/dependency-manager.d.ts.map +1 -1
- package/dist/dependency-manager.js +0 -1
- package/dist/dependency-manager.js.map +1 -1
- package/dist/design-system/index.d.ts +21 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +27 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +149 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +413 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/design-system/transaction-ui.d.ts +70 -0
- package/dist/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/design-system/transaction-ui.js +982 -0
- package/dist/design-system/transaction-ui.js.map +1 -0
- package/dist/generator.d.ts +58 -8
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +9 -4
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -44
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +395 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +304 -0
- package/dist/io.js.map +1 -0
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +2 -1
- package/dist/path-resolver.js.map +1 -1
- package/dist/rendering/components.d.ts +29 -0
- package/dist/rendering/components.d.ts.map +1 -0
- package/dist/rendering/components.js +773 -0
- package/dist/rendering/components.js.map +1 -0
- package/dist/rendering/field-analyzer.d.ts +48 -0
- package/dist/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/rendering/field-analyzer.js +270 -0
- package/dist/rendering/field-analyzer.js.map +1 -0
- package/dist/rendering/field-renderers.d.ts +64 -0
- package/dist/rendering/field-renderers.d.ts.map +1 -0
- package/dist/rendering/field-renderers.js +317 -0
- package/dist/rendering/field-renderers.js.map +1 -0
- package/dist/rendering/index.d.ts +28 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +60 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/layout-selector.d.ts +48 -0
- package/dist/rendering/layout-selector.d.ts.map +1 -0
- package/dist/rendering/layout-selector.js +347 -0
- package/dist/rendering/layout-selector.js.map +1 -0
- package/dist/rendering/template-engine.d.ts +41 -0
- package/dist/rendering/template-engine.d.ts.map +1 -0
- package/dist/rendering/template-engine.js +236 -0
- package/dist/rendering/template-engine.js.map +1 -0
- package/dist/schema-extractor.d.ts +30 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +205 -12
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +63 -0
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +222 -0
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ucp/ap2/handlers.d.ts +242 -0
- package/dist/ucp/ap2/handlers.d.ts.map +1 -0
- package/dist/ucp/ap2/handlers.js +482 -0
- package/dist/ucp/ap2/handlers.js.map +1 -0
- package/dist/ucp/ap2/mandates.d.ts +95 -0
- package/dist/ucp/ap2/mandates.d.ts.map +1 -0
- package/dist/ucp/ap2/mandates.js +234 -0
- package/dist/ucp/ap2/mandates.js.map +1 -0
- package/dist/ucp/ap2/types.d.ts +305 -0
- package/dist/ucp/ap2/types.d.ts.map +1 -0
- package/dist/ucp/ap2/types.js +8 -0
- package/dist/ucp/ap2/types.js.map +1 -0
- package/dist/ucp/capabilities/checkout.d.ts +118 -0
- package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
- package/dist/ucp/capabilities/checkout.js +344 -0
- package/dist/ucp/capabilities/checkout.js.map +1 -0
- package/dist/ucp/capabilities/identity.d.ts +130 -0
- package/dist/ucp/capabilities/identity.d.ts.map +1 -0
- package/dist/ucp/capabilities/identity.js +290 -0
- package/dist/ucp/capabilities/identity.js.map +1 -0
- package/dist/ucp/capabilities/order.d.ts +142 -0
- package/dist/ucp/capabilities/order.d.ts.map +1 -0
- package/dist/ucp/capabilities/order.js +383 -0
- package/dist/ucp/capabilities/order.js.map +1 -0
- package/dist/ucp/index.d.ts +18 -0
- package/dist/ucp/index.d.ts.map +1 -0
- package/dist/ucp/index.js +19 -0
- package/dist/ucp/index.js.map +1 -0
- package/dist/ucp/manifest.d.ts +62 -0
- package/dist/ucp/manifest.d.ts.map +1 -0
- package/dist/ucp/manifest.js +180 -0
- package/dist/ucp/manifest.js.map +1 -0
- package/dist/ucp/types.d.ts +327 -0
- package/dist/ucp/types.d.ts.map +1 -0
- package/dist/ucp/types.js +8 -0
- package/dist/ucp/types.js.map +1 -0
- package/package.json +3 -4
- package/src/auto-ui.ts +413 -0
- package/src/base.ts +22 -9
- package/src/cli-ui-renderer.ts +264 -0
- package/src/dependency-manager.ts +0 -1
- package/src/design-system/index.ts +30 -0
- package/src/design-system/tokens.ts +451 -0
- package/src/design-system/transaction-ui.ts +1038 -0
- package/src/generator.ts +68 -8
- package/src/index.ts +159 -101
- package/src/io.ts +493 -0
- package/src/path-resolver.ts +2 -1
- package/src/rendering/components.ts +785 -0
- package/src/rendering/field-analyzer.ts +299 -0
- package/src/rendering/field-renderers.ts +356 -0
- package/src/rendering/index.ts +63 -0
- package/src/rendering/layout-selector.ts +390 -0
- package/src/rendering/template-engine.ts +254 -0
- package/src/schema-extractor.ts +225 -12
- package/src/stateful.ts +301 -0
- package/src/types.ts +10 -1
- package/src/ucp/ap2/handlers.ts +779 -0
- package/src/ucp/ap2/mandates.ts +354 -0
- package/src/ucp/ap2/types.ts +441 -0
- package/src/ucp/capabilities/checkout.ts +497 -0
- package/src/ucp/capabilities/identity.ts +425 -0
- package/src/ucp/capabilities/order.ts +549 -0
- package/src/ucp/index.ts +27 -0
- package/src/ucp/manifest.ts +257 -0
- package/src/ucp/types.ts +454 -0
- package/dist/cli-formatter.d.ts +0 -92
- package/dist/cli-formatter.d.ts.map +0 -1
- package/dist/cli-formatter.js +0 -486
- package/dist/cli-formatter.js.map +0 -1
- package/dist/elicit.d.ts +0 -93
- package/dist/elicit.d.ts.map +0 -1
- package/dist/elicit.js +0 -373
- package/dist/elicit.js.map +0 -1
- package/dist/mcp-client.d.ts +0 -218
- package/dist/mcp-client.d.ts.map +0 -1
- package/dist/mcp-client.js +0 -424
- package/dist/mcp-client.js.map +0 -1
- package/dist/mcp-sdk-transport.d.ts +0 -88
- package/dist/mcp-sdk-transport.d.ts.map +0 -1
- package/dist/mcp-sdk-transport.js +0 -360
- package/dist/mcp-sdk-transport.js.map +0 -1
- package/dist/photon-config.d.ts +0 -86
- package/dist/photon-config.d.ts.map +0 -1
- package/dist/photon-config.js +0 -156
- package/dist/photon-config.js.map +0 -1
- package/src/cli-formatter.ts +0 -579
- package/src/elicit.ts +0 -438
- package/src/mcp-client.ts +0 -561
- package/src/mcp-sdk-transport.ts +0 -449
- package/src/photon-config.ts +0 -201
package/src/schema-extractor.ts
CHANGED
|
@@ -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
|
|
581
|
-
|
|
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
|
-
|
|
849
|
-
|
|
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
|
-
|
|
855
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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 {
|