@portel/photon-core 2.9.1 → 2.9.3

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.
@@ -10,7 +10,7 @@
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, CLIDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset, ConfigSchema, ConfigParam } from './types.js';
13
+ import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, CLIDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset, ConfigSchema, ConfigParam, SettingsSchema, SettingsProperty } from './types.js';
14
14
  import { parseDuration, parseRate } from './utils/duration.js';
15
15
  import { builtinRegistry, type MiddlewareDeclaration } from './middleware.js';
16
16
 
@@ -18,7 +18,9 @@ export interface ExtractedMetadata {
18
18
  tools: ExtractedSchema[];
19
19
  templates: TemplateInfo[];
20
20
  statics: StaticInfo[];
21
- /** Configuration schema from configure() method */
21
+ /** Settings schema from `protected settings = { ... }` property */
22
+ settingsSchema?: SettingsSchema;
23
+ /** @deprecated Configuration schema from configure() method */
22
24
  configSchema?: ConfigSchema;
23
25
  }
24
26
 
@@ -47,7 +49,10 @@ export class SchemaExtractor {
47
49
  const templates: TemplateInfo[] = [];
48
50
  const statics: StaticInfo[] = [];
49
51
 
50
- // Configuration schema tracking
52
+ // Settings schema tracking (new property-based approach)
53
+ let settingsSchema: SettingsSchema | undefined;
54
+
55
+ // Configuration schema tracking (deprecated method-based approach)
51
56
  let configSchema: ConfigSchema = {
52
57
  hasConfigureMethod: false,
53
58
  hasGetConfigMethod: false,
@@ -78,38 +83,42 @@ export class SchemaExtractor {
78
83
  return;
79
84
  }
80
85
 
81
- // Handle configuration convention methods specially
86
+ // Track configure/getConfig for backward compat metadata (deprecated)
87
+ // These are no longer hidden — they become normal visible tools
82
88
  if (methodName === 'configure') {
83
89
  configSchema.hasConfigureMethod = true;
84
- const jsdoc = this.getJSDocComment(member, sourceFile);
85
- configSchema.description = this.extractDescription(jsdoc);
90
+ const jsdocConfig = this.getJSDocComment(member, sourceFile);
91
+ configSchema.description = this.extractDescription(jsdocConfig);
86
92
 
87
- // Extract configure() parameters as config schema
88
93
  const paramsType = this.getFirstParameterType(member, sourceFile);
89
94
  if (paramsType) {
90
- const { properties, required } = this.buildSchemaFromType(paramsType, sourceFile);
91
- const paramDocs = this.extractParamDocs(jsdoc);
95
+ const { properties: configProps, required: configRequired } = this.buildSchemaFromType(paramsType, sourceFile);
96
+ const paramDocs = this.extractParamDocs(jsdocConfig);
92
97
 
93
- configSchema.params = Object.keys(properties).map(name => ({
98
+ configSchema.params = Object.keys(configProps).map(name => ({
94
99
  name,
95
- type: properties[name].type || 'string',
96
- description: paramDocs.get(name) || properties[name].description,
97
- required: required.includes(name),
98
- defaultValue: properties[name].default,
100
+ type: configProps[name].type || 'string',
101
+ description: paramDocs.get(name) || configProps[name].description,
102
+ required: configRequired.includes(name),
103
+ defaultValue: configProps[name].default,
99
104
  }));
100
105
  }
101
- return; // Don't add configure() as a tool
106
+ // Fall through configure() is now a normal tool (not hidden)
102
107
  }
103
108
 
104
109
  if (methodName === 'getConfig') {
105
110
  configSchema.hasGetConfigMethod = true;
106
- return; // Don't add getConfig() as a tool
111
+ // Fall through getConfig() is now a normal tool (not hidden)
107
112
  }
108
113
 
109
114
  const jsdoc = this.getJSDocComment(member, sourceFile);
110
115
 
111
116
  // Skip @internal methods — hidden from LLM and sidebar
112
- if (/@internal\b/.test(jsdoc)) {
117
+ // Exception: daemon-feature methods (@scheduled, @webhook) must still
118
+ // be registered in tools so the runtime can wire up cron jobs/webhooks.
119
+ const isInternal = /@internal\b/.test(jsdoc);
120
+ const hasDaemonFeature = /@scheduled\b/.test(jsdoc) || /@webhook\b/.test(jsdoc) || /@cron\b/.test(jsdoc) || /^scheduled/.test(methodName);
121
+ if (isInternal && !hasDaemonFeature) {
113
122
  return;
114
123
  }
115
124
 
@@ -212,6 +221,7 @@ export class SchemaExtractor {
212
221
  name: methodName,
213
222
  description,
214
223
  inputSchema,
224
+ ...(isInternal ? { internal: true } : {}),
215
225
  ...(outputFormat ? { outputFormat } : {}),
216
226
  ...(layoutHints ? { layoutHints } : {}),
217
227
  ...(buttonLabel ? { buttonLabel } : {}),
@@ -245,11 +255,110 @@ export class SchemaExtractor {
245
255
  }
246
256
  };
247
257
 
258
+ // Helper to extract settings from a `protected settings = { ... }` property
259
+ const processSettingsProperty = (member: ts.PropertyDeclaration, classNode: ts.ClassDeclaration) => {
260
+ const name = member.name.getText(sourceFile);
261
+ if (name !== 'settings') return;
262
+
263
+ // Must be protected
264
+ const isProtected = member.modifiers?.some(
265
+ m => m.kind === ts.SyntaxKind.ProtectedKeyword
266
+ );
267
+ if (!isProtected) return;
268
+
269
+ // Must have an object literal initializer
270
+ if (!member.initializer || !ts.isObjectLiteralExpression(member.initializer)) return;
271
+
272
+ // Get class-level JSDoc for @property descriptions
273
+ const classJsdoc = this.getJSDocComment(classNode as any, sourceFile);
274
+ const propertyDocs = new Map<string, string>();
275
+ const propertyRegex = /@property\s+(\w+)\s+(.*?)(?=\n\s*\*\s*@|\n\s*\*\/|\n\s*\*\s*$)/gs;
276
+ let propMatch: RegExpExecArray | null;
277
+ while ((propMatch = propertyRegex.exec(classJsdoc)) !== null) {
278
+ propertyDocs.set(propMatch[1], propMatch[2].trim());
279
+ }
280
+
281
+ const properties: SettingsProperty[] = [];
282
+
283
+ for (const prop of member.initializer.properties) {
284
+ if (!ts.isPropertyAssignment(prop)) continue;
285
+ const propName = prop.name.getText(sourceFile);
286
+
287
+ // Determine type and default from the initializer
288
+ let type = 'string';
289
+ let defaultValue: any = undefined;
290
+ let required = false;
291
+
292
+ const init = prop.initializer;
293
+
294
+ if (ts.isNumericLiteral(init)) {
295
+ type = 'number';
296
+ defaultValue = Number(init.text);
297
+ } else if (ts.isStringLiteral(init)) {
298
+ type = 'string';
299
+ defaultValue = init.text;
300
+ } else if (init.kind === ts.SyntaxKind.TrueKeyword) {
301
+ type = 'boolean';
302
+ defaultValue = true;
303
+ } else if (init.kind === ts.SyntaxKind.FalseKeyword) {
304
+ type = 'boolean';
305
+ defaultValue = false;
306
+ } else if (init.kind === ts.SyntaxKind.UndefinedKeyword) {
307
+ // `key: undefined` — required, type inferred from as-expression or defaults to string
308
+ required = true;
309
+ } else if (ts.isAsExpression(init)) {
310
+ // `key: undefined as string | undefined` or `key: 5 as number`
311
+ const innerInit = init.expression;
312
+ const isUndefined = innerInit.kind === ts.SyntaxKind.UndefinedKeyword ||
313
+ (ts.isIdentifier(innerInit) && innerInit.text === 'undefined');
314
+ if (isUndefined) {
315
+ required = true;
316
+ } else if (ts.isNumericLiteral(innerInit)) {
317
+ type = 'number';
318
+ defaultValue = Number(innerInit.text);
319
+ } else if (ts.isStringLiteral(innerInit)) {
320
+ type = 'string';
321
+ defaultValue = innerInit.text;
322
+ } else if (innerInit.kind === ts.SyntaxKind.TrueKeyword || innerInit.kind === ts.SyntaxKind.FalseKeyword) {
323
+ type = 'boolean';
324
+ defaultValue = innerInit.kind === ts.SyntaxKind.TrueKeyword;
325
+ }
326
+ // Try to get type from the as-expression type annotation
327
+ const typeText = init.type.getText(sourceFile).replace(/\s*\|\s*undefined/g, '').trim();
328
+ if (typeText === 'number') type = 'number';
329
+ else if (typeText === 'boolean') type = 'boolean';
330
+ else if (typeText === 'string') type = 'string';
331
+ } else if (ts.isArrayLiteralExpression(init)) {
332
+ type = 'array';
333
+ defaultValue = [];
334
+ }
335
+
336
+ properties.push({
337
+ name: propName,
338
+ type,
339
+ description: propertyDocs.get(propName),
340
+ default: defaultValue,
341
+ required,
342
+ });
343
+ }
344
+
345
+ settingsSchema = {
346
+ hasSettings: true,
347
+ properties,
348
+ description: propertyDocs.size > 0 ? 'Photon settings' : undefined,
349
+ };
350
+ };
351
+
248
352
  // Visit all nodes in the AST
249
353
  const visit = (node: ts.Node) => {
250
354
  // Look for class declarations
251
355
  if (ts.isClassDeclaration(node)) {
252
356
  node.members.forEach((member) => {
357
+ // Detect `protected settings = { ... }` property
358
+ if (ts.isPropertyDeclaration(member)) {
359
+ processSettingsProperty(member, node);
360
+ }
361
+
253
362
  // Process all public methods (sync or async)
254
363
  // Skip private/protected — only public methods become tools
255
364
  if (ts.isMethodDeclaration(member)) {
@@ -271,11 +380,18 @@ export class SchemaExtractor {
271
380
  console.error('Failed to parse TypeScript source:', error.message);
272
381
  }
273
382
 
274
- // Only include configSchema if there's a configure() method
275
383
  const result: ExtractedMetadata = { tools, templates, statics };
384
+
385
+ // Include settingsSchema if detected
386
+ if (settingsSchema) {
387
+ result.settingsSchema = settingsSchema;
388
+ }
389
+
390
+ // Include configSchema if there's a configure() method (deprecated)
276
391
  if (configSchema.hasConfigureMethod) {
277
392
  result.configSchema = configSchema;
278
393
  }
394
+
279
395
  return result;
280
396
  }
281
397
 
@@ -1225,9 +1341,6 @@ export class SchemaExtractor {
1225
1341
  if (constraints.pattern !== undefined) {
1226
1342
  s.pattern = constraints.pattern;
1227
1343
  }
1228
- if (constraints.format !== undefined) {
1229
- s.format = constraints.format;
1230
- }
1231
1344
  } else if (s.type === 'array') {
1232
1345
  if (constraints.min !== undefined) {
1233
1346
  s.minItems = constraints.min;
@@ -1241,6 +1354,9 @@ export class SchemaExtractor {
1241
1354
  }
1242
1355
 
1243
1356
  // Apply type-agnostic constraints
1357
+ if (constraints.format !== undefined) {
1358
+ s.format = constraints.format;
1359
+ }
1244
1360
  if (constraints.default !== undefined) {
1245
1361
  s.default = constraints.default;
1246
1362
  }
@@ -1810,23 +1926,29 @@ export class SchemaExtractor {
1810
1926
  extractPhotonDependencies(source: string): PhotonDependency[] {
1811
1927
  const dependencies: PhotonDependency[] = [];
1812
1928
 
1813
- // Match @photon <name> <source> pattern
1929
+ // Match @photon <name> <source> pattern, where source may include :instanceName suffix
1930
+ // e.g., @photon homeTodos todo:home → source='todo', instanceName='home'
1814
1931
  // Source ends at: newline, end of comment (*), or @ (next tag)
1815
- const photonRegex = /@photon\s+(\w+)\s+([^\s*@\n]+)/g;
1932
+ const photonRegex = /@photon\s+(\w+)\s+([^\s*@\n:]+)(?::(\w[\w-]*))?/g;
1816
1933
 
1817
1934
  let match;
1818
1935
  while ((match = photonRegex.exec(source)) !== null) {
1819
- const [, name, rawSource] = match;
1936
+ const [, name, rawSource, instanceName] = match;
1820
1937
  const photonSource = rawSource.trim();
1821
1938
 
1822
1939
  // Determine source type
1823
1940
  const sourceType = this.classifyPhotonSource(photonSource);
1824
1941
 
1825
- dependencies.push({
1942
+ const dep: PhotonDependency = {
1826
1943
  name,
1827
1944
  source: photonSource,
1828
1945
  sourceType,
1829
- });
1946
+ };
1947
+ if (instanceName) {
1948
+ dep.instanceName = instanceName;
1949
+ }
1950
+
1951
+ dependencies.push(dep);
1830
1952
  }
1831
1953
 
1832
1954
  return dependencies;
package/src/types.ts CHANGED
@@ -302,6 +302,8 @@ export interface PhotonDependency {
302
302
  source: string;
303
303
  /** Resolved source type */
304
304
  sourceType: 'marketplace' | 'github' | 'npm' | 'local';
305
+ /** Named instance to load (e.g., 'home' from `@photon homeTodos todo:home`) */
306
+ instanceName?: string;
305
307
  }
306
308
 
307
309
  /**
@@ -487,6 +489,8 @@ export interface PhotonClassExtended extends PhotonClass {
487
489
  assets?: PhotonAssets;
488
490
  /** Names of injected @photon dependencies (for client-side event routing) */
489
491
  injectedPhotons?: string[];
492
+ /** Settings schema if the photon has `protected settings = { ... }` */
493
+ settingsSchema?: SettingsSchema;
490
494
  }
491
495
 
492
496
  /** @deprecated Use PhotonClassExtended instead */
@@ -642,10 +646,56 @@ export interface WorkflowRun {
642
646
  }
643
647
 
644
648
  // ══════════════════════════════════════════════════════════════════════════════
645
- // CONFIGURATION CONVENTION
649
+ // SETTINGS (first-class property-driven configuration)
646
650
  // ══════════════════════════════════════════════════════════════════════════════
647
651
 
648
652
  /**
653
+ * A single property in a photon's settings declaration
654
+ */
655
+ export interface SettingsProperty {
656
+ /** Property name */
657
+ name: string;
658
+ /** JSON Schema type (string, number, boolean, etc.) */
659
+ type: string;
660
+ /** Description from @property JSDoc tag */
661
+ description?: string;
662
+ /** Default value from the object literal (undefined means elicitation required) */
663
+ default?: any;
664
+ /** True if default is undefined — runtime must elicit this value */
665
+ required: boolean;
666
+ }
667
+
668
+ /**
669
+ * Settings schema extracted from a photon's `protected settings = { ... }` property
670
+ *
671
+ * Example:
672
+ * ```typescript
673
+ * /**
674
+ * * @property wipLimit WIP limit for in-progress tasks (1-20)
675
+ * * @property staleTaskDays Days before a task is considered stale
676
+ * *\/
677
+ * protected settings = {
678
+ * wipLimit: 5,
679
+ * staleTaskDays: 7,
680
+ * projectsRoot: undefined as string | undefined,
681
+ * };
682
+ * ```
683
+ */
684
+ export interface SettingsSchema {
685
+ /** Whether a settings property was detected */
686
+ hasSettings: boolean;
687
+ /** Individual settings properties */
688
+ properties: SettingsProperty[];
689
+ /** Description from class-level JSDoc */
690
+ description?: string;
691
+ }
692
+
693
+ // ══════════════════════════════════════════════════════════════════════════════
694
+ // CONFIGURATION CONVENTION (deprecated — use settings property instead)
695
+ // ══════════════════════════════════════════════════════════════════════════════
696
+
697
+ /**
698
+ * @deprecated Use SettingsProperty instead
649
699
  * Configuration parameter extracted from configure() method
650
700
  */
651
701
  export interface ConfigParam {
@@ -662,35 +712,8 @@ export interface ConfigParam {
662
712
  }
663
713
 
664
714
  /**
715
+ * @deprecated Use SettingsSchema instead
665
716
  * Configuration schema extracted from a Photon's configure() method
666
- *
667
- * The configure() method is a by-convention method for photon configuration.
668
- * Similar to how main() makes a photon a UI application, configure() makes
669
- * it a configurable photon.
670
- *
671
- * When present, the framework will:
672
- * 1. Extract parameter schema from the method signature
673
- * 2. Present a configuration UI during install/setup
674
- * 3. Store config at ~/.photon/{photonName}/config.json
675
- * 4. Make config available via getConfig()
676
- *
677
- * Example:
678
- * ```typescript
679
- * export default class MyPhoton extends Photon {
680
- * async configure(params: {
681
- * apiEndpoint: string;
682
- * maxRetries?: number;
683
- * }) {
684
- * // Save config - framework handles storage
685
- * return { success: true };
686
- * }
687
- *
688
- * async getConfig() {
689
- * // Read config - framework handles loading
690
- * return loadPhotonConfig('my-photon');
691
- * }
692
- * }
693
- * ```
694
717
  */
695
718
  export interface ConfigSchema {
696
719
  /** Whether configure() method exists */