@portel/photon-core 2.2.0 → 2.4.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 (75) hide show
  1. package/dist/asset-discovery.d.ts +25 -0
  2. package/dist/asset-discovery.d.ts.map +1 -0
  3. package/dist/asset-discovery.js +144 -0
  4. package/dist/asset-discovery.js.map +1 -0
  5. package/dist/base.d.ts +1 -1
  6. package/dist/base.d.ts.map +1 -1
  7. package/dist/base.js +12 -6
  8. package/dist/base.js.map +1 -1
  9. package/dist/class-detection.d.ts +32 -0
  10. package/dist/class-detection.d.ts.map +1 -0
  11. package/dist/class-detection.js +86 -0
  12. package/dist/class-detection.js.map +1 -0
  13. package/dist/compiler.d.ts +22 -0
  14. package/dist/compiler.d.ts.map +1 -0
  15. package/dist/compiler.js +48 -0
  16. package/dist/compiler.js.map +1 -0
  17. package/dist/config.d.ts +63 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +117 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/design-system/tokens.d.ts +66 -0
  22. package/dist/design-system/tokens.d.ts.map +1 -1
  23. package/dist/design-system/tokens.js +324 -44
  24. package/dist/design-system/tokens.js.map +1 -1
  25. package/dist/env-utils.d.ts +61 -0
  26. package/dist/env-utils.d.ts.map +1 -0
  27. package/dist/env-utils.js +171 -0
  28. package/dist/env-utils.js.map +1 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +27 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/mcp-apps.d.ts +130 -0
  34. package/dist/mcp-apps.d.ts.map +1 -0
  35. package/dist/mcp-apps.js +87 -0
  36. package/dist/mcp-apps.js.map +1 -0
  37. package/dist/mime-types.d.ts +13 -0
  38. package/dist/mime-types.d.ts.map +1 -0
  39. package/dist/mime-types.js +47 -0
  40. package/dist/mime-types.js.map +1 -0
  41. package/dist/rendering/index.d.ts +49 -0
  42. package/dist/rendering/index.d.ts.map +1 -1
  43. package/dist/rendering/index.js +153 -0
  44. package/dist/rendering/index.js.map +1 -1
  45. package/dist/schema-extractor.d.ts +18 -1
  46. package/dist/schema-extractor.d.ts.map +1 -1
  47. package/dist/schema-extractor.js +81 -11
  48. package/dist/schema-extractor.js.map +1 -1
  49. package/dist/types.d.ts +75 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js.map +1 -1
  52. package/dist/validation.d.ts +51 -0
  53. package/dist/validation.d.ts.map +1 -0
  54. package/dist/validation.js +249 -0
  55. package/dist/validation.js.map +1 -0
  56. package/dist/version-check.d.ts +22 -0
  57. package/dist/version-check.d.ts.map +1 -0
  58. package/dist/version-check.js +91 -0
  59. package/dist/version-check.js.map +1 -0
  60. package/package.json +12 -2
  61. package/src/asset-discovery.ts +160 -0
  62. package/src/base.ts +12 -5
  63. package/src/class-detection.ts +94 -0
  64. package/src/compiler.ts +57 -0
  65. package/src/config.ts +134 -0
  66. package/src/design-system/tokens.ts +381 -57
  67. package/src/env-utils.ts +216 -0
  68. package/src/index.ts +106 -0
  69. package/src/mcp-apps.ts +204 -0
  70. package/src/mime-types.ts +49 -0
  71. package/src/rendering/index.ts +197 -0
  72. package/src/schema-extractor.ts +95 -12
  73. package/src/types.ts +82 -0
  74. package/src/validation.ts +363 -0
  75. package/src/version-check.ts +92 -0
@@ -19,6 +19,8 @@ export * from './field-renderers.js';
19
19
  export * from './template-engine.js';
20
20
  export * from '../design-system/index.js';
21
21
 
22
+ import { analyzeFields, type FieldMapping } from './field-analyzer.js';
23
+ import { selectLayout, type LayoutType } from './layout-selector.js';
22
24
  import { generateFieldAnalyzerJS } from './field-analyzer.js';
23
25
  import { generateLayoutSelectorJS } from './layout-selector.js';
24
26
  import { generateComponentsJS, generateComponentCSS } from './components.js';
@@ -61,3 +63,198 @@ export function generateSmartRenderingCSS(): string {
61
63
  generateTemplateEngineCSS(),
62
64
  ].join('\n');
63
65
  }
66
+
67
+ // ===== Smart Rendering Utilities =====
68
+ // Shared by NCP, Lumina, and other runtimes
69
+
70
+ /**
71
+ * Check if data would benefit from rich HTML rendering
72
+ * (arrays of objects, nested structures, etc.)
73
+ */
74
+ export function shouldUseRichRendering(data: any): boolean {
75
+ if (!data) return false;
76
+
77
+ // Arrays of objects benefit from list/grid rendering
78
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
79
+ return true;
80
+ }
81
+
82
+ // Nested objects benefit from tree/card rendering
83
+ if (typeof data === 'object' && !Array.isArray(data)) {
84
+ const values = Object.values(data);
85
+ if (values.some(v => typeof v === 'object' && v !== null)) {
86
+ return true;
87
+ }
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Generate an HTML content block for MCP tool responses.
95
+ * Analyses data, selects layout, and embeds CSS/JS for rich rendering.
96
+ */
97
+ export function generateHTMLContent(
98
+ data: any,
99
+ options: {
100
+ title?: string;
101
+ format?: LayoutType;
102
+ standalone?: boolean;
103
+ theme?: 'light' | 'dark';
104
+ } = {}
105
+ ): string {
106
+ const { title, format, standalone = false, theme } = options;
107
+
108
+ let fieldMapping: FieldMapping | undefined;
109
+ if (data && typeof data === 'object') {
110
+ fieldMapping = analyzeFields(data);
111
+ }
112
+
113
+ const layout = format || selectLayout(data);
114
+
115
+ const renderScript = `
116
+ <script>
117
+ ${generateSmartRenderingJS()}
118
+
119
+ document.addEventListener('DOMContentLoaded', function() {
120
+ const container = document.getElementById('smart-render-container');
121
+ const data = ${JSON.stringify(data)};
122
+ const layout = '${layout}';
123
+ const fieldMapping = ${JSON.stringify(fieldMapping || {})};
124
+
125
+ if (window.TemplateEngine && window.TemplateEngine.render) {
126
+ container.innerHTML = window.TemplateEngine.render(data, {
127
+ layout,
128
+ fieldMapping,
129
+ title: ${JSON.stringify(title || '')}
130
+ });
131
+ } else {
132
+ container.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
133
+ }
134
+ });
135
+ </script>
136
+ `;
137
+
138
+ const themeVars = theme ? `
139
+ :root {
140
+ --bg-primary: ${theme === 'dark' ? '#0a0a0a' : '#ffffff'};
141
+ --text-primary: ${theme === 'dark' ? '#f5f5f5' : '#1a1a1a'};
142
+ }
143
+ ` : '';
144
+
145
+ const styles = `
146
+ <style>
147
+ ${generateSmartRenderingCSS()}
148
+ ${themeVars}
149
+ </style>
150
+ `;
151
+
152
+ const content = `
153
+ ${styles}
154
+ <div id="smart-render-container" class="smart-render">
155
+ ${title ? `<h2 class="smart-render-title">${title}</h2>` : ''}
156
+ <div class="loading">Loading...</div>
157
+ </div>
158
+ ${renderScript}
159
+ `;
160
+
161
+ if (standalone) {
162
+ return `<!DOCTYPE html>
163
+ <html${theme ? ` data-theme="${theme}"` : ''}>
164
+ <head>
165
+ <meta charset="UTF-8">
166
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
167
+ <title>${title || 'Result'}</title>
168
+ </head>
169
+ <body>
170
+ ${content}
171
+ </body>
172
+ </html>`;
173
+ }
174
+
175
+ return content;
176
+ }
177
+
178
+ /**
179
+ * Create an MCP content block with HTML type
180
+ */
181
+ export function createHTMLContentBlock(
182
+ data: any,
183
+ options: {
184
+ title?: string;
185
+ format?: LayoutType;
186
+ } = {}
187
+ ): { type: 'text'; text: string; mimeType?: string } {
188
+ return {
189
+ type: 'text',
190
+ text: generateHTMLContent(data, { ...options, standalone: false }),
191
+ mimeType: 'text/html'
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Generate an HTML fragment for embedding, returning separate html/css/js parts
197
+ */
198
+ export function generateSmartRenderFragment(
199
+ data: any,
200
+ options: { format?: LayoutType } = {}
201
+ ): { html: string; css: string; js: string } {
202
+ const { format } = options;
203
+
204
+ let fieldMapping: FieldMapping | undefined;
205
+ if (data && typeof data === 'object') {
206
+ fieldMapping = analyzeFields(data);
207
+ }
208
+
209
+ const layout = format || selectLayout(data);
210
+
211
+ const html = `<div id="smart-render-${Date.now()}" class="smart-render-container"></div>`;
212
+ const css = generateSmartRenderingCSS();
213
+ const js = `
214
+ ${generateSmartRenderingJS()}
215
+ (function() {
216
+ const containers = document.querySelectorAll('.smart-render-container');
217
+ const container = containers[containers.length - 1];
218
+ const data = ${JSON.stringify(data)};
219
+ if (window.TemplateEngine && window.TemplateEngine.render) {
220
+ container.innerHTML = window.TemplateEngine.render(data, {
221
+ layout: '${layout}',
222
+ fieldMapping: ${JSON.stringify(fieldMapping || {})}
223
+ });
224
+ }
225
+ })();
226
+ `;
227
+
228
+ return { html, css, js };
229
+ }
230
+
231
+ /**
232
+ * Generate MCP content blocks with smart rendering.
233
+ * Returns JSON text block + optional HTML block for rich clients.
234
+ */
235
+ export function generateMCPSmartContent(
236
+ data: any,
237
+ options: { includeHtml?: boolean; format?: LayoutType } = {}
238
+ ): Array<{ type: string; text?: string; mimeType?: string }> {
239
+ const { includeHtml = true, format } = options;
240
+
241
+ const content: Array<{ type: string; text?: string; mimeType?: string }> = [];
242
+
243
+ // Always include JSON for compatibility
244
+ content.push({
245
+ type: 'text',
246
+ text: JSON.stringify(data, null, 2)
247
+ });
248
+
249
+ // Optionally include HTML for rich rendering
250
+ if (includeHtml && shouldUseRichRendering(data)) {
251
+ const { html, css, js } = generateSmartRenderFragment(data, { format });
252
+ content.push({
253
+ type: 'text',
254
+ text: `<style>${css}</style>${html}<script>${js}</script>`,
255
+ mimeType: 'text/html'
256
+ });
257
+ }
258
+
259
+ return content;
260
+ }
@@ -10,12 +10,14 @@
10
10
 
11
11
  import * as fs from 'fs/promises';
12
12
  import * as ts from 'typescript';
13
- import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset } from './types.js';
13
+ import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, CLIDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset, ConfigSchema, ConfigParam } from './types.js';
14
14
 
15
15
  export interface ExtractedMetadata {
16
16
  tools: ExtractedSchema[];
17
17
  templates: TemplateInfo[];
18
18
  statics: StaticInfo[];
19
+ /** Configuration schema from configure() method */
20
+ configSchema?: ConfigSchema;
19
21
  }
20
22
 
21
23
  /**
@@ -43,6 +45,13 @@ export class SchemaExtractor {
43
45
  const templates: TemplateInfo[] = [];
44
46
  const statics: StaticInfo[] = [];
45
47
 
48
+ // Configuration schema tracking
49
+ let configSchema: ConfigSchema = {
50
+ hasConfigureMethod: false,
51
+ hasGetConfigMethod: false,
52
+ params: [],
53
+ };
54
+
46
55
  try {
47
56
  // If source doesn't contain a class declaration, wrap it in one
48
57
  let sourceToParse = source;
@@ -67,6 +76,34 @@ export class SchemaExtractor {
67
76
  return;
68
77
  }
69
78
 
79
+ // Handle configuration convention methods specially
80
+ if (methodName === 'configure') {
81
+ configSchema.hasConfigureMethod = true;
82
+ const jsdoc = this.getJSDocComment(member, sourceFile);
83
+ configSchema.description = this.extractDescription(jsdoc);
84
+
85
+ // Extract configure() parameters as config schema
86
+ const paramsType = this.getFirstParameterType(member, sourceFile);
87
+ if (paramsType) {
88
+ const { properties, required } = this.buildSchemaFromType(paramsType, sourceFile);
89
+ const paramDocs = this.extractParamDocs(jsdoc);
90
+
91
+ configSchema.params = Object.keys(properties).map(name => ({
92
+ name,
93
+ type: properties[name].type || 'string',
94
+ description: paramDocs.get(name) || properties[name].description,
95
+ required: required.includes(name),
96
+ defaultValue: properties[name].default,
97
+ }));
98
+ }
99
+ return; // Don't add configure() as a tool
100
+ }
101
+
102
+ if (methodName === 'getConfig') {
103
+ configSchema.hasGetConfigMethod = true;
104
+ return; // Don't add getConfig() as a tool
105
+ }
106
+
70
107
  const jsdoc = this.getJSDocComment(member, sourceFile);
71
108
 
72
109
  // Check if this is an async generator method (has asterisk token)
@@ -147,6 +184,9 @@ export class SchemaExtractor {
147
184
  const scheduled = this.extractScheduled(jsdoc, methodName);
148
185
  const locked = this.extractLocked(jsdoc, methodName);
149
186
 
187
+ // Check for static keyword on the method
188
+ const isStaticMethod = member.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword) || false;
189
+
150
190
  tools.push({
151
191
  name: methodName,
152
192
  description,
@@ -159,6 +199,7 @@ export class SchemaExtractor {
159
199
  ...(yields && yields.length > 0 ? { yields } : {}),
160
200
  ...(isStateful ? { isStateful: true } : {}),
161
201
  ...(autorun ? { autorun: true } : {}),
202
+ ...(isStaticMethod ? { isStatic: true } : {}),
162
203
  // Daemon features
163
204
  ...(webhook !== undefined ? { webhook } : {}),
164
205
  ...(scheduled ? { scheduled } : {}),
@@ -188,7 +229,12 @@ export class SchemaExtractor {
188
229
  console.error('Failed to parse TypeScript source:', error.message);
189
230
  }
190
231
 
191
- return { tools, templates, statics };
232
+ // Only include configSchema if there's a configure() method
233
+ const result: ExtractedMetadata = { tools, templates, statics };
234
+ if (configSchema.hasConfigureMethod) {
235
+ result.configSchema = configSchema;
236
+ }
237
+ return result;
192
238
  }
193
239
 
194
240
  /**
@@ -610,21 +656,24 @@ export class SchemaExtractor {
610
656
  * Extract main description from JSDoc comment
611
657
  */
612
658
  private extractDescription(jsdocContent: string): string {
613
- // Split by @param to get only the description part
614
- const beforeParams = jsdocContent.split(/@param/)[0];
659
+ // Split by @param to get only the description part (also stop at other @tags)
660
+ const beforeTags = jsdocContent.split(/@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|webhook|cron|scheduled|locked|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
615
661
 
616
662
  // Remove leading * from each line and trim
617
- const lines = beforeParams
663
+ const lines = beforeTags
618
664
  .split('\n')
619
665
  .map((line) => line.trim().replace(/^\*\s?/, ''))
620
- .filter((line) => line && !line.startsWith('@')); // Exclude @tags and empty lines
666
+ .filter((line) => line !== ''); // Keep non-empty lines
667
+
668
+ // Take lines up to the first markdown heading (## sections are extended docs)
669
+ const descLines: string[] = [];
670
+ for (const line of lines) {
671
+ if (line.startsWith('#')) break;
672
+ descLines.push(line);
673
+ }
621
674
 
622
- // Take only the last meaningful line (the actual method description)
623
- // This filters out file headers
624
- const meaningfulLines = lines.filter(line => line.length > 5); // Filter out short lines
625
- const description = meaningfulLines.length > 0
626
- ? meaningfulLines[meaningfulLines.length - 1]
627
- : lines.join(' ');
675
+ // Join all description lines into a single string
676
+ const description = descLines.join(' ');
628
677
 
629
678
  // Clean up multiple spaces
630
679
  return description.replace(/\s+/g, ' ').trim() || 'No description';
@@ -1376,6 +1425,40 @@ export class SchemaExtractor {
1376
1425
  return 'marketplace';
1377
1426
  }
1378
1427
 
1428
+ /**
1429
+ * Extract CLI dependencies from source code
1430
+ * Parses @cli tags in file-level or class-level JSDoc comments
1431
+ *
1432
+ * Format: @cli <name> - <install_url>
1433
+ *
1434
+ * Example:
1435
+ * ```
1436
+ * /**
1437
+ * * @cli git - https://git-scm.com/downloads
1438
+ * * @cli ffmpeg - https://ffmpeg.org/download.html
1439
+ * *\/
1440
+ * ```
1441
+ */
1442
+ extractCLIDependencies(source: string): CLIDependency[] {
1443
+ const dependencies: CLIDependency[] = [];
1444
+
1445
+ // Match @cli <name> or @cli <name> - <url> pattern
1446
+ // The URL is optional
1447
+ const cliRegex = /@cli\s+(\w[\w-]*)\s*(?:-\s*([^\s*@\n]+))?/g;
1448
+
1449
+ let match;
1450
+ while ((match = cliRegex.exec(source)) !== null) {
1451
+ const [, name, installUrl] = match;
1452
+
1453
+ dependencies.push({
1454
+ name: name.trim(),
1455
+ installUrl: installUrl?.trim(),
1456
+ });
1457
+ }
1458
+
1459
+ return dependencies;
1460
+ }
1461
+
1379
1462
  /**
1380
1463
  * Resolve all injections for a Photon class
1381
1464
  * Determines how each constructor parameter should be injected:
package/src/types.ts CHANGED
@@ -63,6 +63,8 @@ export interface ExtractedSchema {
63
63
  isStateful?: boolean;
64
64
  /** True if this method should auto-execute when selected (idempotent, no required params) */
65
65
  autorun?: boolean;
66
+ /** True if this is a static method (class-level, no instance needed) */
67
+ isStatic?: boolean;
66
68
 
67
69
  // ═══ DAEMON FEATURES ═══
68
70
 
@@ -192,6 +194,24 @@ export interface PhotonDependency {
192
194
  sourceType: 'marketplace' | 'github' | 'npm' | 'local';
193
195
  }
194
196
 
197
+ /**
198
+ * CLI Dependency - System command-line tool required by a Photon
199
+ *
200
+ * Declared via @cli annotation:
201
+ * ```
202
+ * /**
203
+ * * @cli git - https://git-scm.com/downloads
204
+ * * @cli ffmpeg - https://ffmpeg.org/download.html
205
+ * *\/
206
+ * ```
207
+ */
208
+ export interface CLIDependency {
209
+ /** CLI command name (e.g., 'git', 'ffmpeg') */
210
+ name: string;
211
+ /** Install URL or instructions */
212
+ installUrl?: string;
213
+ }
214
+
195
215
  // ════════════════════════════════════════════════════════════════════════════
196
216
  // PHOTON ASSETS - Static files referenced via @ui, @prompt, @resource
197
217
  // ════════════════════════════════════════════════════════════════════════════
@@ -501,3 +521,65 @@ export interface WorkflowRun {
501
521
  ts: number;
502
522
  };
503
523
  }
524
+
525
+ // ══════════════════════════════════════════════════════════════════════════════
526
+ // CONFIGURATION CONVENTION
527
+ // ══════════════════════════════════════════════════════════════════════════════
528
+
529
+ /**
530
+ * Configuration parameter extracted from configure() method
531
+ */
532
+ export interface ConfigParam {
533
+ /** Parameter name */
534
+ name: string;
535
+ /** Parameter type (string, number, boolean, etc.) */
536
+ type: string;
537
+ /** Description from JSDoc */
538
+ description?: string;
539
+ /** Whether the parameter is required */
540
+ required: boolean;
541
+ /** Default value if any */
542
+ defaultValue?: any;
543
+ }
544
+
545
+ /**
546
+ * Configuration schema extracted from a Photon's configure() method
547
+ *
548
+ * The configure() method is a by-convention method for photon configuration.
549
+ * Similar to how main() makes a photon a UI application, configure() makes
550
+ * it a configurable photon.
551
+ *
552
+ * When present, the framework will:
553
+ * 1. Extract parameter schema from the method signature
554
+ * 2. Present a configuration UI during install/setup
555
+ * 3. Store config at ~/.photon/{photonName}/config.json
556
+ * 4. Make config available via getConfig()
557
+ *
558
+ * Example:
559
+ * ```typescript
560
+ * export default class MyPhoton extends PhotonMCP {
561
+ * async configure(params: {
562
+ * apiEndpoint: string;
563
+ * maxRetries?: number;
564
+ * }) {
565
+ * // Save config - framework handles storage
566
+ * return { success: true };
567
+ * }
568
+ *
569
+ * async getConfig() {
570
+ * // Read config - framework handles loading
571
+ * return loadPhotonConfig('my-photon');
572
+ * }
573
+ * }
574
+ * ```
575
+ */
576
+ export interface ConfigSchema {
577
+ /** Whether configure() method exists */
578
+ hasConfigureMethod: boolean;
579
+ /** Whether getConfig() method exists */
580
+ hasGetConfigMethod: boolean;
581
+ /** Configuration parameters from configure() signature */
582
+ params: ConfigParam[];
583
+ /** Description from configure() JSDoc */
584
+ description?: string;
585
+ }