@portel/photon-core 2.14.0 → 2.15.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/README.md +123 -3
- package/dist/base.d.ts +101 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +111 -0
- package/dist/base.js.map +1 -1
- package/dist/generator.d.ts +16 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +19 -1
- package/dist/mixins.js.map +1 -1
- package/dist/path-resolver.d.ts +36 -3
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +122 -17
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-loader-lite.js +1 -1
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +42 -3
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/base.ts +152 -0
- package/src/generator.ts +20 -1
- package/src/index.ts +5 -0
- package/src/mixins.ts +19 -1
- package/src/path-resolver.ts +141 -18
- package/src/photon-loader-lite.ts +1 -1
- package/src/schema-extractor.ts +42 -3
- package/src/types.ts +3 -1
package/src/generator.ts
CHANGED
|
@@ -730,6 +730,23 @@ export interface EmitQR {
|
|
|
730
730
|
value: string;
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Render emit — sends a formatted intermediate result to the client.
|
|
735
|
+
* The value is rendered using the same format pipeline as @format return values.
|
|
736
|
+
* Custom formats are resolved from assets/formats/<name>.html.
|
|
737
|
+
*/
|
|
738
|
+
export interface EmitRender {
|
|
739
|
+
emit: 'render';
|
|
740
|
+
/** Format type (table, qr, chart:bar, dashboard, or custom name) */
|
|
741
|
+
format: string;
|
|
742
|
+
/** Data to render — same shape as a return value with that @format */
|
|
743
|
+
value: any;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export interface EmitRenderClear {
|
|
747
|
+
emit: 'render:clear';
|
|
748
|
+
}
|
|
749
|
+
|
|
733
750
|
export type EmitYield =
|
|
734
751
|
| EmitStatus
|
|
735
752
|
| EmitProgress
|
|
@@ -739,7 +756,9 @@ export type EmitYield =
|
|
|
739
756
|
| EmitThinking
|
|
740
757
|
| EmitArtifact
|
|
741
758
|
| EmitUI
|
|
742
|
-
| EmitQR
|
|
759
|
+
| EmitQR
|
|
760
|
+
| EmitRender
|
|
761
|
+
| EmitRenderClear;
|
|
743
762
|
|
|
744
763
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
745
764
|
// COMBINED TYPES
|
package/src/index.ts
CHANGED
|
@@ -166,12 +166,15 @@ export { SchemaExtractor, detectCapabilities, type PhotonCapability } from './sc
|
|
|
166
166
|
export {
|
|
167
167
|
resolvePath,
|
|
168
168
|
listFiles,
|
|
169
|
+
listFilesWithNamespace,
|
|
169
170
|
ensureDir,
|
|
170
171
|
resolvePhotonPath,
|
|
171
172
|
listPhotonFiles,
|
|
173
|
+
listPhotonFilesWithNamespace,
|
|
172
174
|
ensurePhotonDir,
|
|
173
175
|
DEFAULT_PHOTON_DIR,
|
|
174
176
|
type ResolverOptions,
|
|
177
|
+
type ListedPhoton,
|
|
175
178
|
} from './path-resolver.js';
|
|
176
179
|
|
|
177
180
|
// Types
|
|
@@ -239,6 +242,8 @@ export {
|
|
|
239
242
|
type EmitArtifact,
|
|
240
243
|
type EmitUI,
|
|
241
244
|
type EmitQR,
|
|
245
|
+
type EmitRender,
|
|
246
|
+
type EmitRenderClear,
|
|
242
247
|
|
|
243
248
|
// Checkpoint yield type
|
|
244
249
|
type CheckpointYield,
|
package/src/mixins.ts
CHANGED
|
@@ -124,6 +124,18 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
124
124
|
return this._schedule;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Render a formatted value as an intermediate result.
|
|
129
|
+
* Each call replaces the previous render. Call with no args to clear.
|
|
130
|
+
*/
|
|
131
|
+
protected render(format?: string, value?: any): void {
|
|
132
|
+
if (format === undefined) {
|
|
133
|
+
this.emit({ emit: 'render:clear' });
|
|
134
|
+
} else {
|
|
135
|
+
this.emit({ emit: 'render', format, value });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
/**
|
|
128
140
|
* Emit an event/progress update
|
|
129
141
|
*/
|
|
@@ -139,9 +151,15 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
if (data && typeof data.channel === 'string') {
|
|
154
|
+
// Auto-prefix channel with photon name if not already namespaced
|
|
155
|
+
const channel = data.channel.includes(':')
|
|
156
|
+
? data.channel
|
|
157
|
+
: this._photonName
|
|
158
|
+
? `${this._photonName}:${data.channel}`
|
|
159
|
+
: data.channel;
|
|
142
160
|
const broker = getBroker();
|
|
143
161
|
broker.publish({
|
|
144
|
-
channel
|
|
162
|
+
channel,
|
|
145
163
|
event: data.event || 'message',
|
|
146
164
|
data: data.data !== undefined ? data.data : data,
|
|
147
165
|
timestamp: Date.now(),
|
package/src/path-resolver.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generic path resolution utilities used by photon, lumina, ncp.
|
|
5
5
|
* Configurable file extensions and default directories.
|
|
6
|
+
*
|
|
7
|
+
* Supports namespace-based directory structure:
|
|
8
|
+
* ~/.photon/
|
|
9
|
+
* portel-dev/ ← namespace (marketplace author)
|
|
10
|
+
* whatsapp.photon.ts
|
|
11
|
+
* local/ ← implicit namespace for user-created photons
|
|
12
|
+
* todo.photon.ts
|
|
13
|
+
* legacy.photon.ts ← flat files (pre-migration, still supported)
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
import * as fs from 'fs/promises';
|
|
@@ -37,9 +45,17 @@ const defaultOptions: Required<ResolverOptions> = {
|
|
|
37
45
|
defaultDir: DEFAULT_PHOTON_DIR,
|
|
38
46
|
};
|
|
39
47
|
|
|
48
|
+
/** Directories to skip when scanning for namespace subdirectories */
|
|
49
|
+
const SKIP_DIRS = new Set([
|
|
50
|
+
'state', 'context', 'env', '.cache', '.config',
|
|
51
|
+
'node_modules', 'marketplace', 'photons', 'templates',
|
|
52
|
+
]);
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
|
-
* Resolve a file path from name
|
|
42
|
-
*
|
|
55
|
+
* Resolve a file path from name.
|
|
56
|
+
*
|
|
57
|
+
* Supports namespace-qualified names: 'namespace:photonName'
|
|
58
|
+
* For unqualified names, searches flat files first, then namespace subdirectories.
|
|
43
59
|
*/
|
|
44
60
|
export async function resolvePath(
|
|
45
61
|
name: string,
|
|
@@ -59,66 +75,172 @@ export async function resolvePath(
|
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
|
|
78
|
+
// Parse namespace:name format
|
|
79
|
+
const colonIndex = name.indexOf(':');
|
|
80
|
+
let namespace: string | undefined;
|
|
81
|
+
let photonName: string;
|
|
82
|
+
if (colonIndex !== -1) {
|
|
83
|
+
namespace = name.slice(0, colonIndex);
|
|
84
|
+
photonName = name.slice(colonIndex + 1);
|
|
85
|
+
} else {
|
|
86
|
+
photonName = name;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
// Remove extension if provided (match any configured extension)
|
|
63
|
-
let basename =
|
|
90
|
+
let basename = photonName;
|
|
64
91
|
for (const ext of opts.extensions) {
|
|
65
|
-
if (
|
|
66
|
-
basename =
|
|
92
|
+
if (photonName.endsWith(ext)) {
|
|
93
|
+
basename = photonName.slice(0, -ext.length);
|
|
67
94
|
break;
|
|
68
95
|
}
|
|
69
96
|
}
|
|
70
97
|
|
|
71
|
-
//
|
|
98
|
+
// If namespace is specified, search only that namespace directory
|
|
99
|
+
if (namespace) {
|
|
100
|
+
for (const ext of opts.extensions) {
|
|
101
|
+
const filePath = path.join(dir, namespace, `${basename}${ext}`);
|
|
102
|
+
try {
|
|
103
|
+
await fs.access(filePath);
|
|
104
|
+
return filePath;
|
|
105
|
+
} catch {
|
|
106
|
+
// Continue
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Unqualified name: search flat files first (backward compat)
|
|
72
113
|
for (const ext of opts.extensions) {
|
|
73
114
|
const filePath = path.join(dir, `${basename}${ext}`);
|
|
74
115
|
try {
|
|
75
116
|
await fs.access(filePath);
|
|
76
117
|
return filePath;
|
|
77
118
|
} catch {
|
|
78
|
-
// Continue
|
|
119
|
+
// Continue
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Then search namespace subdirectories (one level deep)
|
|
124
|
+
try {
|
|
125
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const ext of opts.extensions) {
|
|
131
|
+
const filePath = path.join(dir, entry.name, `${basename}${ext}`);
|
|
132
|
+
try {
|
|
133
|
+
await fs.access(filePath);
|
|
134
|
+
return filePath;
|
|
135
|
+
} catch {
|
|
136
|
+
// Continue
|
|
137
|
+
}
|
|
138
|
+
}
|
|
79
139
|
}
|
|
140
|
+
} catch {
|
|
141
|
+
// dir doesn't exist
|
|
80
142
|
}
|
|
81
143
|
|
|
82
|
-
// Not found
|
|
83
144
|
return null;
|
|
84
145
|
}
|
|
85
146
|
|
|
86
147
|
/**
|
|
87
|
-
*
|
|
148
|
+
* Result from listing files, including namespace information.
|
|
149
|
+
*/
|
|
150
|
+
export interface ListedPhoton {
|
|
151
|
+
/** Short name (e.g., 'whatsapp') */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Namespace (e.g., 'portel-dev') or empty string for flat/root-level files */
|
|
154
|
+
namespace: string;
|
|
155
|
+
/** Qualified name (e.g., 'portel-dev:whatsapp' or 'whatsapp' for flat) */
|
|
156
|
+
qualifiedName: string;
|
|
157
|
+
/** Full absolute path to the file */
|
|
158
|
+
filePath: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* List all matching files in a directory.
|
|
163
|
+
*
|
|
164
|
+
* Scans both flat files (backward compat) and namespace subdirectories.
|
|
165
|
+
* Returns short names for backward compatibility.
|
|
88
166
|
*/
|
|
89
167
|
export async function listFiles(
|
|
90
168
|
workingDir?: string,
|
|
91
169
|
options?: ResolverOptions
|
|
92
170
|
): Promise<string[]> {
|
|
171
|
+
const listed = await listFilesWithNamespace(workingDir, options);
|
|
172
|
+
return listed.map((l) => l.name).sort();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* List all matching files with full namespace metadata.
|
|
177
|
+
*
|
|
178
|
+
* Scans flat files at the root level and one level of namespace subdirectories.
|
|
179
|
+
*/
|
|
180
|
+
export async function listFilesWithNamespace(
|
|
181
|
+
workingDir?: string,
|
|
182
|
+
options?: ResolverOptions
|
|
183
|
+
): Promise<ListedPhoton[]> {
|
|
93
184
|
const opts = { ...defaultOptions, ...options };
|
|
94
185
|
const dir = expandTilde(workingDir || opts.defaultDir);
|
|
186
|
+
const results: ListedPhoton[] = [];
|
|
95
187
|
|
|
96
188
|
try {
|
|
97
|
-
// Ensure directory exists
|
|
98
189
|
await fs.mkdir(dir, { recursive: true });
|
|
99
|
-
|
|
100
190
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
101
|
-
const files: string[] = [];
|
|
102
191
|
|
|
192
|
+
// Scan flat files at root level (backward compat / pre-migration)
|
|
103
193
|
for (const entry of entries) {
|
|
104
|
-
// Include both regular files and symlinks
|
|
105
194
|
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
106
|
-
// Check if file matches any extension
|
|
107
195
|
for (const ext of opts.extensions) {
|
|
108
196
|
if (entry.name.endsWith(ext)) {
|
|
109
|
-
// Remove extension for display
|
|
110
197
|
const name = entry.name.slice(0, -ext.length);
|
|
111
|
-
|
|
198
|
+
results.push({
|
|
199
|
+
name,
|
|
200
|
+
namespace: '',
|
|
201
|
+
qualifiedName: name,
|
|
202
|
+
filePath: path.join(dir, entry.name),
|
|
203
|
+
});
|
|
112
204
|
break;
|
|
113
205
|
}
|
|
114
206
|
}
|
|
115
207
|
}
|
|
116
208
|
}
|
|
117
209
|
|
|
118
|
-
|
|
210
|
+
// Scan namespace subdirectories (one level deep)
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const nsDir = path.join(dir, entry.name);
|
|
217
|
+
try {
|
|
218
|
+
const nsEntries = await fs.readdir(nsDir, { withFileTypes: true });
|
|
219
|
+
for (const nsEntry of nsEntries) {
|
|
220
|
+
if (nsEntry.isFile() || nsEntry.isSymbolicLink()) {
|
|
221
|
+
for (const ext of opts.extensions) {
|
|
222
|
+
if (nsEntry.name.endsWith(ext)) {
|
|
223
|
+
const name = nsEntry.name.slice(0, -ext.length);
|
|
224
|
+
results.push({
|
|
225
|
+
name,
|
|
226
|
+
namespace: entry.name,
|
|
227
|
+
qualifiedName: `${entry.name}:${name}`,
|
|
228
|
+
filePath: path.join(nsDir, nsEntry.name),
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// Namespace dir unreadable, skip
|
|
237
|
+
}
|
|
238
|
+
}
|
|
119
239
|
} catch {
|
|
120
|
-
|
|
240
|
+
// Root dir doesn't exist
|
|
121
241
|
}
|
|
242
|
+
|
|
243
|
+
return results;
|
|
122
244
|
}
|
|
123
245
|
|
|
124
246
|
/**
|
|
@@ -132,4 +254,5 @@ export async function ensureDir(dir?: string): Promise<void> {
|
|
|
132
254
|
// Convenience aliases for photon-specific usage
|
|
133
255
|
export const resolvePhotonPath = resolvePath;
|
|
134
256
|
export const listPhotonFiles = listFiles;
|
|
257
|
+
export const listPhotonFilesWithNamespace = listFilesWithNamespace;
|
|
135
258
|
export const ensurePhotonDir = ensureDir;
|
|
@@ -368,7 +368,7 @@ function wrapStatefulMethods(
|
|
|
368
368
|
|
|
369
369
|
// Skip framework-injected methods from withPhotonCapabilities
|
|
370
370
|
const frameworkMethods = new Set([
|
|
371
|
-
'emit', 'call', 'mcp', 'setMCPFactory', 'onInitialize', 'onShutdown',
|
|
371
|
+
'emit', 'render', 'call', 'mcp', 'setMCPFactory', 'onInitialize', 'onShutdown',
|
|
372
372
|
]);
|
|
373
373
|
|
|
374
374
|
// Walk the prototype chain to find all public methods
|
package/src/schema-extractor.ts
CHANGED
|
@@ -1329,6 +1329,7 @@ export class SchemaExtractor {
|
|
|
1329
1329
|
.replace(/\{@pattern\s+[^}]+\}/g, '')
|
|
1330
1330
|
.replace(/\{@format\s+[^}]+\}/g, '')
|
|
1331
1331
|
.replace(/\{@choice\s+[^}]+\}/g, '')
|
|
1332
|
+
.replace(/\{@choice-from\s+[^}]+\}/g, '')
|
|
1332
1333
|
.replace(/\{@field\s+[^}]+\}/g, '')
|
|
1333
1334
|
.replace(/\{@default\s+[^}]+\}/g, '')
|
|
1334
1335
|
.replace(/\{@unique(?:Items)?\s*\}/g, '')
|
|
@@ -1404,6 +1405,12 @@ export class SchemaExtractor {
|
|
|
1404
1405
|
paramConstraints.enum = choices;
|
|
1405
1406
|
}
|
|
1406
1407
|
|
|
1408
|
+
// Extract {@choice-from toolName} or {@choice-from toolName.field}
|
|
1409
|
+
const choiceFromMatch = description.match(/\{@choice-from\s+([^}]+)\}/);
|
|
1410
|
+
if (choiceFromMatch) {
|
|
1411
|
+
paramConstraints.choiceFrom = choiceFromMatch[1].trim();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1407
1414
|
// Extract {@field type} - hints for UI form rendering
|
|
1408
1415
|
const fieldMatch = description.match(/\{@field\s+([a-z]+)\}/);
|
|
1409
1416
|
if (fieldMatch) {
|
|
@@ -1526,7 +1533,7 @@ export class SchemaExtractor {
|
|
|
1526
1533
|
}
|
|
1527
1534
|
|
|
1528
1535
|
// Validate no unknown {@...} tags (typos in constraint names)
|
|
1529
|
-
const allKnownTags = ['min', 'max', 'pattern', 'format', 'choice', 'field', 'default', 'unique', 'uniqueItems',
|
|
1536
|
+
const allKnownTags = ['min', 'max', 'pattern', 'format', 'choice', 'choice-from', 'field', 'default', 'unique', 'uniqueItems',
|
|
1530
1537
|
'example', 'multipleOf', 'deprecated', 'readOnly', 'writeOnly', 'label', 'placeholder',
|
|
1531
1538
|
'hint', 'hidden', 'accept', 'minItems', 'maxItems'];
|
|
1532
1539
|
const unknownTagRegex = /\{@([\w-]+)\s*(?:\s+[^}]*)?\}/g;
|
|
@@ -1715,6 +1722,10 @@ export class SchemaExtractor {
|
|
|
1715
1722
|
s.enum = constraints.enum;
|
|
1716
1723
|
}
|
|
1717
1724
|
}
|
|
1725
|
+
// Apply dynamic choice provider (x-choiceFrom extension)
|
|
1726
|
+
if (constraints.choiceFrom !== undefined) {
|
|
1727
|
+
s['x-choiceFrom'] = constraints.choiceFrom;
|
|
1728
|
+
}
|
|
1718
1729
|
// Apply field hint for UI rendering
|
|
1719
1730
|
if (constraints.field !== undefined) {
|
|
1720
1731
|
s.field = constraints.field;
|
|
@@ -2652,7 +2663,7 @@ export class SchemaExtractor {
|
|
|
2652
2663
|
};
|
|
2653
2664
|
}
|
|
2654
2665
|
|
|
2655
|
-
// Check if matches an @photon declaration
|
|
2666
|
+
// Check if matches an @photon declaration (exact match)
|
|
2656
2667
|
if (photonMap.has(param.name)) {
|
|
2657
2668
|
return {
|
|
2658
2669
|
param,
|
|
@@ -2661,6 +2672,25 @@ export class SchemaExtractor {
|
|
|
2661
2672
|
};
|
|
2662
2673
|
}
|
|
2663
2674
|
|
|
2675
|
+
// Instance-aware DI: if paramName ends with a photon dep name (case-insensitive),
|
|
2676
|
+
// the prefix becomes the instance name.
|
|
2677
|
+
// e.g., personalWhatsapp + @photon whatsapp → instance "personal" of whatsapp
|
|
2678
|
+
// workWhatsapp + @photon whatsapp → instance "work" of whatsapp
|
|
2679
|
+
for (const [depName, dep] of photonMap) {
|
|
2680
|
+
const lowerParam = param.name.toLowerCase();
|
|
2681
|
+
const lowerDep = depName.toLowerCase();
|
|
2682
|
+
if (lowerParam.endsWith(lowerDep) && lowerParam.length > lowerDep.length) {
|
|
2683
|
+
const prefix = param.name.slice(0, param.name.length - depName.length);
|
|
2684
|
+
// Ensure the prefix is a valid instance name (lowercase the first char)
|
|
2685
|
+
const instanceName = prefix.charAt(0).toLowerCase() + prefix.slice(1);
|
|
2686
|
+
return {
|
|
2687
|
+
param,
|
|
2688
|
+
injectionType: 'photon' as const,
|
|
2689
|
+
photonDependency: { ...dep, instanceName: instanceName || undefined },
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2664
2694
|
// Non-primitive with default on @stateful class → persisted state
|
|
2665
2695
|
if (isStateful && param.hasDefault) {
|
|
2666
2696
|
return {
|
|
@@ -2826,7 +2856,15 @@ export class SchemaExtractor {
|
|
|
2826
2856
|
// Link UI asset to this method
|
|
2827
2857
|
const asset = uiAssets.find(a => a.id === uiId);
|
|
2828
2858
|
if (asset) {
|
|
2829
|
-
|
|
2859
|
+
// First method wins as primary (used for app detection)
|
|
2860
|
+
if (!asset.linkedTool) {
|
|
2861
|
+
asset.linkedTool = methodName;
|
|
2862
|
+
}
|
|
2863
|
+
// Track all methods that reference this UI
|
|
2864
|
+
if (!asset.linkedTools) asset.linkedTools = [];
|
|
2865
|
+
if (!asset.linkedTools.includes(methodName)) {
|
|
2866
|
+
asset.linkedTools.push(methodName);
|
|
2867
|
+
}
|
|
2830
2868
|
}
|
|
2831
2869
|
}
|
|
2832
2870
|
}
|
|
@@ -2896,6 +2934,7 @@ export type PhotonCapability = 'emit' | 'memory' | 'call' | 'mcp' | 'lock' | 'in
|
|
|
2896
2934
|
export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
2897
2935
|
const caps = new Set<PhotonCapability>();
|
|
2898
2936
|
if (/this\.emit\s*\(/.test(source)) caps.add('emit');
|
|
2937
|
+
if (/this\.render\s*\(/.test(source)) caps.add('emit'); // render() needs emit injection
|
|
2899
2938
|
if (/this\.memory\b/.test(source)) caps.add('memory');
|
|
2900
2939
|
if (/this\.call\s*\(/.test(source)) caps.add('call');
|
|
2901
2940
|
if (/this\.mcp\s*\(/.test(source)) caps.add('mcp');
|
package/src/types.ts
CHANGED
|
@@ -432,8 +432,10 @@ export interface UIAsset {
|
|
|
432
432
|
resolvedPath?: string;
|
|
433
433
|
/** MIME type (detected from extension) */
|
|
434
434
|
mimeType?: string;
|
|
435
|
-
/**
|
|
435
|
+
/** Primary tool this UI is linked to (first method with @ui annotation — used for app detection) */
|
|
436
436
|
linkedTool?: string;
|
|
437
|
+
/** All tools that reference this UI asset (multiple methods can share one template) */
|
|
438
|
+
linkedTools?: string[];
|
|
437
439
|
/** MCP resource URI (set by loader, e.g., 'ui://photon-name/main-ui') */
|
|
438
440
|
uri?: string;
|
|
439
441
|
}
|