@portel/photon-core 2.9.2 → 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.
- package/dist/asset-discovery.d.ts.map +1 -1
- package/dist/asset-discovery.js +16 -1
- package/dist/asset-discovery.js.map +1 -1
- package/dist/base.d.ts +4 -2
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +2 -2
- package/dist/base.js.map +1 -1
- package/dist/dependency-manager.d.ts +12 -0
- package/dist/dependency-manager.d.ts.map +1 -1
- package/dist/dependency-manager.js +56 -0
- package/dist/dependency-manager.js.map +1 -1
- package/dist/schema-extractor.d.ts +4 -2
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +135 -23
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +45 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/asset-discovery.ts +16 -1
- package/src/base.ts +3 -3
- package/src/dependency-manager.ts +58 -0
- package/src/schema-extractor.ts +142 -25
- package/src/types.ts +52 -29
package/src/schema-extractor.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
//
|
|
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,32 +83,32 @@ export class SchemaExtractor {
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
//
|
|
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
|
|
85
|
-
configSchema.description = this.extractDescription(
|
|
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(
|
|
95
|
+
const { properties: configProps, required: configRequired } = this.buildSchemaFromType(paramsType, sourceFile);
|
|
96
|
+
const paramDocs = this.extractParamDocs(jsdocConfig);
|
|
92
97
|
|
|
93
|
-
configSchema.params = Object.keys(
|
|
98
|
+
configSchema.params = Object.keys(configProps).map(name => ({
|
|
94
99
|
name,
|
|
95
|
-
type:
|
|
96
|
-
description: paramDocs.get(name) ||
|
|
97
|
-
required:
|
|
98
|
-
defaultValue:
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
// Fall through — getConfig() is now a normal tool (not hidden)
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const jsdoc = this.getJSDocComment(member, sourceFile);
|
|
@@ -250,11 +255,110 @@ export class SchemaExtractor {
|
|
|
250
255
|
}
|
|
251
256
|
};
|
|
252
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
|
+
|
|
253
352
|
// Visit all nodes in the AST
|
|
254
353
|
const visit = (node: ts.Node) => {
|
|
255
354
|
// Look for class declarations
|
|
256
355
|
if (ts.isClassDeclaration(node)) {
|
|
257
356
|
node.members.forEach((member) => {
|
|
357
|
+
// Detect `protected settings = { ... }` property
|
|
358
|
+
if (ts.isPropertyDeclaration(member)) {
|
|
359
|
+
processSettingsProperty(member, node);
|
|
360
|
+
}
|
|
361
|
+
|
|
258
362
|
// Process all public methods (sync or async)
|
|
259
363
|
// Skip private/protected — only public methods become tools
|
|
260
364
|
if (ts.isMethodDeclaration(member)) {
|
|
@@ -276,11 +380,18 @@ export class SchemaExtractor {
|
|
|
276
380
|
console.error('Failed to parse TypeScript source:', error.message);
|
|
277
381
|
}
|
|
278
382
|
|
|
279
|
-
// Only include configSchema if there's a configure() method
|
|
280
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)
|
|
281
391
|
if (configSchema.hasConfigureMethod) {
|
|
282
392
|
result.configSchema = configSchema;
|
|
283
393
|
}
|
|
394
|
+
|
|
284
395
|
return result;
|
|
285
396
|
}
|
|
286
397
|
|
|
@@ -1230,9 +1341,6 @@ export class SchemaExtractor {
|
|
|
1230
1341
|
if (constraints.pattern !== undefined) {
|
|
1231
1342
|
s.pattern = constraints.pattern;
|
|
1232
1343
|
}
|
|
1233
|
-
if (constraints.format !== undefined) {
|
|
1234
|
-
s.format = constraints.format;
|
|
1235
|
-
}
|
|
1236
1344
|
} else if (s.type === 'array') {
|
|
1237
1345
|
if (constraints.min !== undefined) {
|
|
1238
1346
|
s.minItems = constraints.min;
|
|
@@ -1246,6 +1354,9 @@ export class SchemaExtractor {
|
|
|
1246
1354
|
}
|
|
1247
1355
|
|
|
1248
1356
|
// Apply type-agnostic constraints
|
|
1357
|
+
if (constraints.format !== undefined) {
|
|
1358
|
+
s.format = constraints.format;
|
|
1359
|
+
}
|
|
1249
1360
|
if (constraints.default !== undefined) {
|
|
1250
1361
|
s.default = constraints.default;
|
|
1251
1362
|
}
|
|
@@ -1815,23 +1926,29 @@ export class SchemaExtractor {
|
|
|
1815
1926
|
extractPhotonDependencies(source: string): PhotonDependency[] {
|
|
1816
1927
|
const dependencies: PhotonDependency[] = [];
|
|
1817
1928
|
|
|
1818
|
-
// 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'
|
|
1819
1931
|
// Source ends at: newline, end of comment (*), or @ (next tag)
|
|
1820
|
-
const photonRegex = /@photon\s+(\w+)\s+([^\s*@\n]+)
|
|
1932
|
+
const photonRegex = /@photon\s+(\w+)\s+([^\s*@\n:]+)(?::(\w[\w-]*))?/g;
|
|
1821
1933
|
|
|
1822
1934
|
let match;
|
|
1823
1935
|
while ((match = photonRegex.exec(source)) !== null) {
|
|
1824
|
-
const [, name, rawSource] = match;
|
|
1936
|
+
const [, name, rawSource, instanceName] = match;
|
|
1825
1937
|
const photonSource = rawSource.trim();
|
|
1826
1938
|
|
|
1827
1939
|
// Determine source type
|
|
1828
1940
|
const sourceType = this.classifyPhotonSource(photonSource);
|
|
1829
1941
|
|
|
1830
|
-
|
|
1942
|
+
const dep: PhotonDependency = {
|
|
1831
1943
|
name,
|
|
1832
1944
|
source: photonSource,
|
|
1833
1945
|
sourceType,
|
|
1834
|
-
}
|
|
1946
|
+
};
|
|
1947
|
+
if (instanceName) {
|
|
1948
|
+
dep.instanceName = instanceName;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
dependencies.push(dep);
|
|
1835
1952
|
}
|
|
1836
1953
|
|
|
1837
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
|
-
//
|
|
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 */
|