@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.
- 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 +141 -24
- 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 +148 -26
- 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,38 +83,42 @@ 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);
|
|
110
115
|
|
|
111
116
|
// Skip @internal methods — hidden from LLM and sidebar
|
|
112
|
-
|
|
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]+)
|
|
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
|
-
|
|
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
|
-
//
|
|
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 */
|