@portel/photon-core 1.3.0 → 1.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.
- package/dist/generator.d.ts +236 -2
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +30 -0
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +39 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +213 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/photon-config.d.ts +86 -0
- package/dist/photon-config.d.ts.map +1 -0
- package/dist/photon-config.js +156 -0
- package/dist/photon-config.js.map +1 -0
- package/dist/schema-extractor.d.ts +99 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +311 -5
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +238 -0
- package/dist/stateful.d.ts.map +1 -0
- package/dist/stateful.js +469 -0
- package/dist/stateful.js.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -2
- package/src/generator.ts +270 -2
- package/src/index.ts +73 -1
- package/src/mcp-client.ts +254 -0
- package/src/photon-config.ts +201 -0
- package/src/schema-extractor.ts +353 -6
- package/src/stateful.ts +659 -0
- package/src/types.ts +289 -0
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 } from './types.js';
|
|
13
|
+
import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset } from './types.js';
|
|
14
14
|
|
|
15
15
|
export interface ExtractedMetadata {
|
|
16
16
|
tools: ExtractedSchema[];
|
|
@@ -67,13 +67,13 @@ export class SchemaExtractor {
|
|
|
67
67
|
const isGenerator = member.asteriskToken !== undefined;
|
|
68
68
|
|
|
69
69
|
// Extract parameter type information
|
|
70
|
+
// Extract parameter type (may be undefined for no-arg methods)
|
|
70
71
|
const paramsType = this.getFirstParameterType(member, sourceFile);
|
|
71
|
-
if (!paramsType) {
|
|
72
|
-
return; // Skip methods without proper params
|
|
73
|
-
}
|
|
74
72
|
|
|
75
|
-
// Build schema from TypeScript type
|
|
76
|
-
const { properties, required } =
|
|
73
|
+
// Build schema from TypeScript type (empty for no-arg methods)
|
|
74
|
+
const { properties, required } = paramsType
|
|
75
|
+
? this.buildSchemaFromType(paramsType, sourceFile)
|
|
76
|
+
: { properties: {}, required: [] };
|
|
77
77
|
|
|
78
78
|
// Extract descriptions from JSDoc
|
|
79
79
|
const paramDocs = this.extractParamDocs(jsdoc);
|
|
@@ -130,6 +130,7 @@ export class SchemaExtractor {
|
|
|
130
130
|
else {
|
|
131
131
|
const outputFormat = this.extractFormat(jsdoc);
|
|
132
132
|
const yields = isGenerator ? this.extractYieldsFromJSDoc(jsdoc) : undefined;
|
|
133
|
+
const isStateful = this.hasStatefulTag(jsdoc);
|
|
133
134
|
|
|
134
135
|
tools.push({
|
|
135
136
|
name: methodName,
|
|
@@ -138,6 +139,7 @@ export class SchemaExtractor {
|
|
|
138
139
|
...(outputFormat ? { outputFormat } : {}),
|
|
139
140
|
...(isGenerator ? { isGenerator: true } : {}),
|
|
140
141
|
...(yields && yields.length > 0 ? { yields } : {}),
|
|
142
|
+
...(isStateful ? { isStateful: true } : {}),
|
|
141
143
|
});
|
|
142
144
|
}
|
|
143
145
|
};
|
|
@@ -450,6 +452,15 @@ export class SchemaExtractor {
|
|
|
450
452
|
return null;
|
|
451
453
|
}
|
|
452
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Check if a type is a primitive (string, number, boolean)
|
|
457
|
+
* Primitives are injected from environment variables
|
|
458
|
+
*/
|
|
459
|
+
private isPrimitiveType(type: string): boolean {
|
|
460
|
+
const normalizedType = type.trim().toLowerCase();
|
|
461
|
+
return ['string', 'number', 'boolean'].includes(normalizedType);
|
|
462
|
+
}
|
|
463
|
+
|
|
453
464
|
/**
|
|
454
465
|
* Extract constructor parameters for config injection
|
|
455
466
|
*/
|
|
@@ -486,6 +497,7 @@ export class SchemaExtractor {
|
|
|
486
497
|
isOptional,
|
|
487
498
|
hasDefault,
|
|
488
499
|
defaultValue,
|
|
500
|
+
isPrimitive: this.isPrimitiveType(type),
|
|
489
501
|
});
|
|
490
502
|
}
|
|
491
503
|
});
|
|
@@ -808,6 +820,14 @@ export class SchemaExtractor {
|
|
|
808
820
|
return /@Static/i.test(jsdocContent);
|
|
809
821
|
}
|
|
810
822
|
|
|
823
|
+
/**
|
|
824
|
+
* Check if JSDoc contains @stateful tag
|
|
825
|
+
* Indicates this method is a stateful workflow that supports checkpoint/resume
|
|
826
|
+
*/
|
|
827
|
+
private hasStatefulTag(jsdocContent: string): boolean {
|
|
828
|
+
return /@stateful/i.test(jsdocContent);
|
|
829
|
+
}
|
|
830
|
+
|
|
811
831
|
/**
|
|
812
832
|
* Extract URI pattern from @Static tag
|
|
813
833
|
* Example: @Static github://repos/{owner}/{repo}/readme
|
|
@@ -946,4 +966,331 @@ export class SchemaExtractor {
|
|
|
946
966
|
// Default: GitHub shorthand (owner/repo)
|
|
947
967
|
return 'github';
|
|
948
968
|
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Extract Photon dependencies from source code
|
|
972
|
+
* Parses @photon tags in file-level or class-level JSDoc comments
|
|
973
|
+
*
|
|
974
|
+
* Format: @photon <name> <source>
|
|
975
|
+
*
|
|
976
|
+
* Source formats:
|
|
977
|
+
* - Marketplace: rss-feed (simple name from marketplace)
|
|
978
|
+
* - GitHub: portel-dev/photons/rss-feed
|
|
979
|
+
* - npm package: npm:@portel/rss-feed-photon
|
|
980
|
+
* - Local path: ./my-photon.photon.ts
|
|
981
|
+
*
|
|
982
|
+
* Example:
|
|
983
|
+
* ```
|
|
984
|
+
* /**
|
|
985
|
+
* * @photon rssFeed rss-feed
|
|
986
|
+
* * @photon custom ./my-photon.photon.ts
|
|
987
|
+
* *\/
|
|
988
|
+
* ```
|
|
989
|
+
*/
|
|
990
|
+
extractPhotonDependencies(source: string): PhotonDependency[] {
|
|
991
|
+
const dependencies: PhotonDependency[] = [];
|
|
992
|
+
|
|
993
|
+
// Match @photon <name> <source> pattern
|
|
994
|
+
// Source ends at: newline, end of comment (*), or @ (next tag)
|
|
995
|
+
const photonRegex = /@photon\s+(\w+)\s+([^\s*@\n]+)/g;
|
|
996
|
+
|
|
997
|
+
let match;
|
|
998
|
+
while ((match = photonRegex.exec(source)) !== null) {
|
|
999
|
+
const [, name, rawSource] = match;
|
|
1000
|
+
const photonSource = rawSource.trim();
|
|
1001
|
+
|
|
1002
|
+
// Determine source type
|
|
1003
|
+
const sourceType = this.classifyPhotonSource(photonSource);
|
|
1004
|
+
|
|
1005
|
+
dependencies.push({
|
|
1006
|
+
name,
|
|
1007
|
+
source: photonSource,
|
|
1008
|
+
sourceType,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return dependencies;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Classify Photon source type based on format
|
|
1017
|
+
*/
|
|
1018
|
+
private classifyPhotonSource(source: string): 'marketplace' | 'github' | 'npm' | 'local' {
|
|
1019
|
+
// npm package: npm:@scope/package or npm:package
|
|
1020
|
+
if (source.startsWith('npm:')) {
|
|
1021
|
+
return 'npm';
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Local path (relative or absolute, or ends with .photon.ts)
|
|
1025
|
+
if (source.startsWith('./') || source.startsWith('../') ||
|
|
1026
|
+
source.startsWith('/') || source.startsWith('~') ||
|
|
1027
|
+
/^[A-Za-z]:[\\/]/.test(source) ||
|
|
1028
|
+
source.endsWith('.photon.ts')) {
|
|
1029
|
+
return 'local';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// GitHub: has at least 2 slashes (owner/repo/photon) or 1 slash (owner/repo)
|
|
1033
|
+
if ((source.match(/\//g) || []).length >= 1) {
|
|
1034
|
+
return 'github';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Default: Marketplace (simple name like "rss-feed")
|
|
1038
|
+
return 'marketplace';
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Resolve all injections for a Photon class
|
|
1043
|
+
* Determines how each constructor parameter should be injected:
|
|
1044
|
+
* - Primitives (string, number, boolean) → env var
|
|
1045
|
+
* - Non-primitives matching @mcp → MCP client
|
|
1046
|
+
* - Non-primitives matching @photon → Photon instance
|
|
1047
|
+
*
|
|
1048
|
+
* @param source The Photon source code
|
|
1049
|
+
* @param mcpName The MCP name (for env var prefixing)
|
|
1050
|
+
*/
|
|
1051
|
+
resolveInjections(source: string, mcpName: string): ResolvedInjection[] {
|
|
1052
|
+
const params = this.extractConstructorParams(source);
|
|
1053
|
+
const mcpDeps = this.extractMCPDependencies(source);
|
|
1054
|
+
const photonDeps = this.extractPhotonDependencies(source);
|
|
1055
|
+
|
|
1056
|
+
// Build lookup maps
|
|
1057
|
+
const mcpMap = new Map(mcpDeps.map(d => [d.name, d]));
|
|
1058
|
+
const photonMap = new Map(photonDeps.map(d => [d.name, d]));
|
|
1059
|
+
|
|
1060
|
+
return params.map(param => {
|
|
1061
|
+
// Primitives → env var
|
|
1062
|
+
if (param.isPrimitive) {
|
|
1063
|
+
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
1064
|
+
return {
|
|
1065
|
+
param,
|
|
1066
|
+
injectionType: 'env' as const,
|
|
1067
|
+
envVarName,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Check if matches an @mcp declaration
|
|
1072
|
+
if (mcpMap.has(param.name)) {
|
|
1073
|
+
return {
|
|
1074
|
+
param,
|
|
1075
|
+
injectionType: 'mcp' as const,
|
|
1076
|
+
mcpDependency: mcpMap.get(param.name),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Check if matches an @photon declaration
|
|
1081
|
+
if (photonMap.has(param.name)) {
|
|
1082
|
+
return {
|
|
1083
|
+
param,
|
|
1084
|
+
injectionType: 'photon' as const,
|
|
1085
|
+
photonDependency: photonMap.get(param.name),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Non-primitive without declaration - treat as env var (will likely fail at runtime)
|
|
1090
|
+
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
1091
|
+
return {
|
|
1092
|
+
param,
|
|
1093
|
+
injectionType: 'env' as const,
|
|
1094
|
+
envVarName,
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Convert MCP name and parameter name to environment variable name
|
|
1101
|
+
* Example: (filesystem, workdir) → FILESYSTEM_WORKDIR
|
|
1102
|
+
*/
|
|
1103
|
+
private toEnvVarName(mcpName: string, paramName: string): string {
|
|
1104
|
+
const mcpPrefix = mcpName.toUpperCase().replace(/-/g, '_');
|
|
1105
|
+
const paramSuffix = paramName
|
|
1106
|
+
.replace(/([A-Z])/g, '_$1')
|
|
1107
|
+
.toUpperCase()
|
|
1108
|
+
.replace(/^_/, '');
|
|
1109
|
+
return `${mcpPrefix}_${paramSuffix}`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1113
|
+
// ASSET EXTRACTION - @ui, @prompt, @resource annotations
|
|
1114
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Extract all assets from Photon source code
|
|
1118
|
+
* Parses @ui, @prompt, @resource annotations from class-level JSDoc
|
|
1119
|
+
*
|
|
1120
|
+
* Format:
|
|
1121
|
+
* - @ui <id> <path> - UI templates for MCP Apps
|
|
1122
|
+
* - @prompt <id> <path> - Static MCP prompts
|
|
1123
|
+
* - @resource <id> <path> - Static MCP resources
|
|
1124
|
+
*
|
|
1125
|
+
* Example:
|
|
1126
|
+
* ```
|
|
1127
|
+
* /**
|
|
1128
|
+
* * @ui preferences ./ui/preferences.html
|
|
1129
|
+
* * @prompt system ./prompts/system.md
|
|
1130
|
+
* * @resource config ./resources/config.json
|
|
1131
|
+
* *\/
|
|
1132
|
+
* export default class MyPhoton extends PhotonMCP { ... }
|
|
1133
|
+
* ```
|
|
1134
|
+
*/
|
|
1135
|
+
extractAssets(source: string, assetFolder?: string): PhotonAssets {
|
|
1136
|
+
const ui = this.extractUIAssets(source);
|
|
1137
|
+
const prompts = this.extractPromptAssets(source);
|
|
1138
|
+
const resources = this.extractResourceAssets(source);
|
|
1139
|
+
|
|
1140
|
+
// Also extract method-level @ui annotations (links UI to specific tool)
|
|
1141
|
+
this.extractMethodUILinks(source, ui);
|
|
1142
|
+
|
|
1143
|
+
return {
|
|
1144
|
+
ui,
|
|
1145
|
+
prompts,
|
|
1146
|
+
resources,
|
|
1147
|
+
assetFolder,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Extract UI assets from @ui annotations
|
|
1153
|
+
* Format: @ui <id> <path>
|
|
1154
|
+
* Path must start with ./ or / to distinguish from method-level @ui references
|
|
1155
|
+
*/
|
|
1156
|
+
private extractUIAssets(source: string): UIAsset[] {
|
|
1157
|
+
const assets: UIAsset[] = [];
|
|
1158
|
+
// Path must start with ./ or / to be a declaration (not a reference)
|
|
1159
|
+
const uiRegex = /@ui\s+(\w[\w-]*)\s+(\.\/[^\s*]+|\/[^\s*]+)/g;
|
|
1160
|
+
|
|
1161
|
+
let match;
|
|
1162
|
+
while ((match = uiRegex.exec(source)) !== null) {
|
|
1163
|
+
const [, id, path] = match;
|
|
1164
|
+
assets.push({
|
|
1165
|
+
id,
|
|
1166
|
+
path,
|
|
1167
|
+
mimeType: this.getMimeTypeFromPath(path),
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return assets;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Extract prompt assets from @prompt annotations
|
|
1176
|
+
* Format: @prompt <id> <path>
|
|
1177
|
+
* Path must start with ./ or / to be a valid declaration
|
|
1178
|
+
*/
|
|
1179
|
+
private extractPromptAssets(source: string): PromptAsset[] {
|
|
1180
|
+
const assets: PromptAsset[] = [];
|
|
1181
|
+
const promptRegex = /@prompt\s+(\w[\w-]*)\s+(\.\/[^\s*]+|\/[^\s*]+)/g;
|
|
1182
|
+
|
|
1183
|
+
let match;
|
|
1184
|
+
while ((match = promptRegex.exec(source)) !== null) {
|
|
1185
|
+
const [, id, path] = match;
|
|
1186
|
+
assets.push({
|
|
1187
|
+
id,
|
|
1188
|
+
path,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return assets;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Extract resource assets from @resource annotations
|
|
1197
|
+
* Format: @resource <id> <path>
|
|
1198
|
+
* Path must start with ./ or / to be a valid declaration
|
|
1199
|
+
*/
|
|
1200
|
+
private extractResourceAssets(source: string): ResourceAsset[] {
|
|
1201
|
+
const assets: ResourceAsset[] = [];
|
|
1202
|
+
const resourceRegex = /@resource\s+(\w[\w-]*)\s+(\.\/[^\s*]+|\/[^\s*]+)/g;
|
|
1203
|
+
|
|
1204
|
+
let match;
|
|
1205
|
+
while ((match = resourceRegex.exec(source)) !== null) {
|
|
1206
|
+
const [, id, path] = match;
|
|
1207
|
+
assets.push({
|
|
1208
|
+
id,
|
|
1209
|
+
path,
|
|
1210
|
+
mimeType: this.getMimeTypeFromPath(path),
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return assets;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Extract method-level @ui annotations that link UI to tools
|
|
1219
|
+
* Format: @ui <id> on a method's JSDoc
|
|
1220
|
+
*/
|
|
1221
|
+
private extractMethodUILinks(source: string, uiAssets: UIAsset[]): void {
|
|
1222
|
+
try {
|
|
1223
|
+
const sourceFile = ts.createSourceFile(
|
|
1224
|
+
'temp.ts',
|
|
1225
|
+
source,
|
|
1226
|
+
ts.ScriptTarget.Latest,
|
|
1227
|
+
true
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
const visit = (node: ts.Node) => {
|
|
1231
|
+
if (ts.isClassDeclaration(node)) {
|
|
1232
|
+
node.members.forEach((member) => {
|
|
1233
|
+
if (ts.isMethodDeclaration(member) &&
|
|
1234
|
+
member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
1235
|
+
const jsdoc = this.getJSDocComment(member, sourceFile);
|
|
1236
|
+
const methodName = member.name.getText(sourceFile);
|
|
1237
|
+
|
|
1238
|
+
// Check for @ui <id> in method JSDoc
|
|
1239
|
+
const uiMatch = jsdoc.match(/@ui\s+(\S+)/);
|
|
1240
|
+
if (uiMatch) {
|
|
1241
|
+
const uiId = uiMatch[1];
|
|
1242
|
+
// Link UI asset to this method
|
|
1243
|
+
const asset = uiAssets.find(a => a.id === uiId);
|
|
1244
|
+
if (asset) {
|
|
1245
|
+
asset.linkedTool = methodName;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
ts.forEachChild(node, visit);
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
visit(sourceFile);
|
|
1255
|
+
} catch (error: any) {
|
|
1256
|
+
// Silently fail - UI links are optional
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Get MIME type from file extension
|
|
1262
|
+
*/
|
|
1263
|
+
private getMimeTypeFromPath(path: string): string {
|
|
1264
|
+
const ext = path.toLowerCase().split('.').pop() || '';
|
|
1265
|
+
const mimeTypes: Record<string, string> = {
|
|
1266
|
+
// Web
|
|
1267
|
+
'html': 'text/html',
|
|
1268
|
+
'htm': 'text/html',
|
|
1269
|
+
'css': 'text/css',
|
|
1270
|
+
'js': 'application/javascript',
|
|
1271
|
+
'mjs': 'application/javascript',
|
|
1272
|
+
'jsx': 'text/jsx',
|
|
1273
|
+
'ts': 'text/typescript',
|
|
1274
|
+
'tsx': 'text/tsx',
|
|
1275
|
+
// Data
|
|
1276
|
+
'json': 'application/json',
|
|
1277
|
+
'yaml': 'application/yaml',
|
|
1278
|
+
'yml': 'application/yaml',
|
|
1279
|
+
'xml': 'application/xml',
|
|
1280
|
+
'csv': 'text/csv',
|
|
1281
|
+
// Documents
|
|
1282
|
+
'md': 'text/markdown',
|
|
1283
|
+
'txt': 'text/plain',
|
|
1284
|
+
'pdf': 'application/pdf',
|
|
1285
|
+
// Images
|
|
1286
|
+
'png': 'image/png',
|
|
1287
|
+
'jpg': 'image/jpeg',
|
|
1288
|
+
'jpeg': 'image/jpeg',
|
|
1289
|
+
'gif': 'image/gif',
|
|
1290
|
+
'svg': 'image/svg+xml',
|
|
1291
|
+
'webp': 'image/webp',
|
|
1292
|
+
'ico': 'image/x-icon',
|
|
1293
|
+
};
|
|
1294
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
1295
|
+
}
|
|
949
1296
|
}
|