@portel/photon-core 2.15.0 → 2.17.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 +66 -0
- package/dist/asset-discovery.d.ts.map +1 -1
- package/dist/asset-discovery.js +22 -1
- package/dist/asset-discovery.js.map +1 -1
- package/dist/base.d.ts +116 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +143 -5
- package/dist/base.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +7 -0
- package/dist/mixins.js.map +1 -1
- package/dist/schema-extractor.d.ts +8 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +44 -7
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +2 -1
- 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 +21 -1
- package/src/base.ts +156 -8
- package/src/index.ts +1 -0
- package/src/mixins.ts +9 -1
- package/src/schema-extractor.ts +55 -8
- package/src/types.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portel/photon-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Core library for parsing, loading, and managing .photon.ts files - runtime-agnostic foundation for building custom Photon runtimes",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/src/asset-discovery.ts
CHANGED
|
@@ -110,12 +110,32 @@ export async function autoDiscoverAssets(
|
|
|
110
110
|
assets: PhotonAssets,
|
|
111
111
|
): Promise<void> {
|
|
112
112
|
// Auto-discover UI files
|
|
113
|
+
// .photon.html files (declarative mode) take priority over .html files with the same base name.
|
|
113
114
|
const uiDir = path.join(assetFolder, 'ui');
|
|
114
115
|
if (await fileExists(uiDir)) {
|
|
115
116
|
try {
|
|
116
117
|
const files = await fs.readdir(uiDir);
|
|
118
|
+
// Collect .photon.html files first so they take priority
|
|
119
|
+
const photonHtmlFiles = new Set<string>();
|
|
117
120
|
for (const file of files) {
|
|
118
|
-
|
|
121
|
+
if (file.endsWith('.photon.html')) {
|
|
122
|
+
photonHtmlFiles.add(file.replace(/\.photon\.html$/, ''));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
// Determine the asset ID:
|
|
127
|
+
// - foo.photon.html → id "foo"
|
|
128
|
+
// - foo.html → id "foo" (but skipped if foo.photon.html exists)
|
|
129
|
+
let id: string;
|
|
130
|
+
if (file.endsWith('.photon.html')) {
|
|
131
|
+
id = file.replace(/\.photon\.html$/, '');
|
|
132
|
+
} else {
|
|
133
|
+
id = path.basename(file, path.extname(file));
|
|
134
|
+
// Skip .html files when a .photon.html with the same base name exists
|
|
135
|
+
if (file.endsWith('.html') && photonHtmlFiles.has(id)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
119
139
|
if (!assets.ui.find((u) => u.id === id)) {
|
|
120
140
|
assets.ui.push({
|
|
121
141
|
id,
|
package/src/base.ts
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { MCPClient, MCPClientFactory, createMCPProxy } from '@portel/mcp';
|
|
43
|
-
import { executionContext } from '@portel/cli';
|
|
43
|
+
import { executionContext, type CallerInfo } from '@portel/cli';
|
|
44
44
|
import { getBroker } from './channels/index.js';
|
|
45
45
|
import { withLock as withLockHelper } from './decorators.js';
|
|
46
46
|
import { MemoryProvider } from './memory.js';
|
|
@@ -95,6 +95,28 @@ export class Photon {
|
|
|
95
95
|
*/
|
|
96
96
|
_sessionId?: string;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Authenticated caller identity
|
|
100
|
+
*
|
|
101
|
+
* Populated from MCP OAuth when `@auth` is enabled on the photon.
|
|
102
|
+
* Returns the identity of whoever is calling the current method —
|
|
103
|
+
* human (via social login) or agent (via API key).
|
|
104
|
+
*
|
|
105
|
+
* Returns an anonymous caller if no auth token was provided.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // In a method:
|
|
110
|
+
* const userId = this.caller.id; // stable user ID from JWT
|
|
111
|
+
* const name = this.caller.name; // display name
|
|
112
|
+
* const isAnon = this.caller.anonymous; // true if no auth
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
get caller(): CallerInfo {
|
|
116
|
+
const store = executionContext.getStore();
|
|
117
|
+
return store?.caller ?? { id: 'anonymous', anonymous: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
/**
|
|
99
121
|
* Scoped key-value storage for photon data
|
|
100
122
|
*
|
|
@@ -236,6 +258,33 @@ export class Photon {
|
|
|
236
258
|
return path.join(dir, name, 'assets', subpath);
|
|
237
259
|
}
|
|
238
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Get a URL path for an asset served by Beam
|
|
263
|
+
*
|
|
264
|
+
* Returns a relative URL like `/api/assets/my-photon/images/logo.png`
|
|
265
|
+
* that Beam serves from the photon's assets directory. Use this in
|
|
266
|
+
* HTML, markdown, or slides where browser-accessible URLs are needed.
|
|
267
|
+
*
|
|
268
|
+
* @param subpath Path within the assets folder
|
|
269
|
+
* @returns URL path (relative to Beam host)
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* const logoUrl = this.assetUrl('images/logo.png');
|
|
274
|
+
* // → '/api/assets/my-photon/images/logo.png'
|
|
275
|
+
*
|
|
276
|
+
* return ``; // works in markdown/slides
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
protected assetUrl(subpath: string): string {
|
|
280
|
+
const name = this._photonName || this.constructor.name
|
|
281
|
+
.replace(/MCP$/, '')
|
|
282
|
+
.replace(/([A-Z])/g, '-$1')
|
|
283
|
+
.toLowerCase()
|
|
284
|
+
.replace(/^-/, '');
|
|
285
|
+
return `/api/assets/${encodeURIComponent(name)}/${subpath}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
239
288
|
/**
|
|
240
289
|
* Dynamic photon access
|
|
241
290
|
*
|
|
@@ -457,16 +506,13 @@ export class Photon {
|
|
|
457
506
|
]);
|
|
458
507
|
|
|
459
508
|
// Get all property names from prototype chain
|
|
509
|
+
// Use getOwnPropertyDescriptor to avoid triggering getters (which may call storage())
|
|
460
510
|
let current = prototype;
|
|
461
511
|
while (current && current !== Photon.prototype) {
|
|
462
512
|
Object.getOwnPropertyNames(current).forEach((name) => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
!conventionMethods.has(name) &&
|
|
467
|
-
typeof (prototype as any)[name] === 'function' &&
|
|
468
|
-
!methods.includes(name)
|
|
469
|
-
) {
|
|
513
|
+
if (name.startsWith('_') || conventionMethods.has(name) || methods.includes(name)) return;
|
|
514
|
+
const desc = Object.getOwnPropertyDescriptor(current, name);
|
|
515
|
+
if (desc && typeof desc.value === 'function') {
|
|
470
516
|
methods.push(name);
|
|
471
517
|
}
|
|
472
518
|
});
|
|
@@ -618,4 +664,106 @@ export class Photon {
|
|
|
618
664
|
): Promise<T> {
|
|
619
665
|
return withLockHelper(lockName, fn, timeout);
|
|
620
666
|
}
|
|
667
|
+
|
|
668
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
669
|
+
// IDENTITY-AWARE LOCK MANAGEMENT
|
|
670
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Identity-aware lock handler - injected by runtime
|
|
674
|
+
* @internal
|
|
675
|
+
*/
|
|
676
|
+
_lockHandler?: {
|
|
677
|
+
assign(lockName: string, holder: string, timeout?: number): Promise<boolean>;
|
|
678
|
+
transfer(lockName: string, fromHolder: string, toHolder: string, timeout?: number): Promise<boolean>;
|
|
679
|
+
release(lockName: string, holder: string): Promise<boolean>;
|
|
680
|
+
query(lockName: string): Promise<{ holder: string | null; acquiredAt?: number; expiresAt?: number }>;
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Assign a lock to a specific caller (identity-aware)
|
|
685
|
+
*
|
|
686
|
+
* Unlike `withLock` which auto-acquires/releases around a function,
|
|
687
|
+
* this explicitly assigns a lock to a caller ID. The lock persists
|
|
688
|
+
* until transferred or released.
|
|
689
|
+
*
|
|
690
|
+
* @param lockName Name of the lock
|
|
691
|
+
* @param callerId Caller ID to assign the lock to
|
|
692
|
+
* @param timeout Lock timeout in ms (default 30000, auto-extended on transfer)
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* ```typescript
|
|
696
|
+
* // Assign "turn" lock to first player
|
|
697
|
+
* await this.acquireLock('turn', this.caller.id);
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
protected async acquireLock(lockName: string, callerId: string, timeout?: number): Promise<boolean> {
|
|
701
|
+
if (!this._lockHandler) {
|
|
702
|
+
console.warn(`[photon] acquireLock('${lockName}'): no lock handler configured`);
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
return this._lockHandler.assign(lockName, callerId, timeout);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Transfer a lock from the current holder to another caller
|
|
710
|
+
*
|
|
711
|
+
* Only succeeds if `fromCallerId` is the current holder.
|
|
712
|
+
*
|
|
713
|
+
* @param lockName Name of the lock
|
|
714
|
+
* @param toCallerId Caller ID to transfer the lock to
|
|
715
|
+
* @param fromCallerId Current holder (defaults to this.caller.id)
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* // After a chess move, transfer turn to opponent
|
|
720
|
+
* await this.transferLock('turn', opponentId);
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
protected async transferLock(lockName: string, toCallerId: string, fromCallerId?: string): Promise<boolean> {
|
|
724
|
+
if (!this._lockHandler) {
|
|
725
|
+
console.warn(`[photon] transferLock('${lockName}'): no lock handler configured`);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
return this._lockHandler.transfer(lockName, fromCallerId ?? this.caller.id, toCallerId);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Release a lock (make the method open to anyone)
|
|
733
|
+
*
|
|
734
|
+
* @param lockName Name of the lock
|
|
735
|
+
* @param callerId Holder to release from (defaults to this.caller.id)
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```typescript
|
|
739
|
+
* // Presenter releases navigation control to audience
|
|
740
|
+
* await this.releaseLock('navigation');
|
|
741
|
+
* ```
|
|
742
|
+
*/
|
|
743
|
+
protected async releaseLock(lockName: string, callerId?: string): Promise<boolean> {
|
|
744
|
+
if (!this._lockHandler) {
|
|
745
|
+
console.warn(`[photon] releaseLock('${lockName}'): no lock handler configured`);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
return this._lockHandler.release(lockName, callerId ?? this.caller.id);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Query who holds a specific lock
|
|
753
|
+
*
|
|
754
|
+
* @param lockName Name of the lock
|
|
755
|
+
* @returns Lock holder info, or null holder if unlocked
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* const lock = await this.getLock('turn');
|
|
760
|
+
* if (lock.holder === this.caller.id) { ... }
|
|
761
|
+
* ```
|
|
762
|
+
*/
|
|
763
|
+
protected async getLock(lockName: string): Promise<{ holder: string | null; acquiredAt?: number; expiresAt?: number }> {
|
|
764
|
+
if (!this._lockHandler) {
|
|
765
|
+
return { holder: null };
|
|
766
|
+
}
|
|
767
|
+
return this._lockHandler.query(lockName);
|
|
768
|
+
}
|
|
621
769
|
}
|
package/src/index.ts
CHANGED
package/src/mixins.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { MCPClient, MCPClientFactory, createMCPProxy } from '@portel/mcp';
|
|
26
|
-
import { executionContext } from '@portel/cli';
|
|
26
|
+
import { executionContext, type CallerInfo } from '@portel/cli';
|
|
27
27
|
import { getBroker } from './channels/index.js';
|
|
28
28
|
import { MemoryProvider } from './memory.js';
|
|
29
29
|
import { ScheduleProvider } from './schedule.js';
|
|
@@ -94,6 +94,14 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
94
94
|
*/
|
|
95
95
|
private _mcpClients: Map<string, MCPClient & Record<string, (params?: any) => Promise<any>>> = new Map();
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Authenticated caller identity from MCP OAuth
|
|
99
|
+
*/
|
|
100
|
+
get caller(): CallerInfo {
|
|
101
|
+
const store = executionContext.getStore();
|
|
102
|
+
return store?.caller ?? { id: 'anonymous', anonymous: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
/**
|
|
98
106
|
* Scoped key-value storage for photon data
|
|
99
107
|
*/
|
package/src/schema-extractor.ts
CHANGED
|
@@ -24,6 +24,13 @@ export interface ExtractedMetadata {
|
|
|
24
24
|
configSchema?: ConfigSchema;
|
|
25
25
|
/** Notification subscription from @notify-on tag */
|
|
26
26
|
notificationSubscriptions?: NotificationSubscription;
|
|
27
|
+
/**
|
|
28
|
+
* MCP OAuth auth requirement (from @auth tag)
|
|
29
|
+
* - 'required': all methods require authenticated caller
|
|
30
|
+
* - 'optional': caller populated if token present, anonymous allowed
|
|
31
|
+
* - string URL: OIDC provider URL (implies required)
|
|
32
|
+
*/
|
|
33
|
+
auth?: 'required' | 'optional' | string;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/**
|
|
@@ -64,6 +71,9 @@ export class SchemaExtractor {
|
|
|
64
71
|
// Notification subscriptions tracking (from @notify-on tag)
|
|
65
72
|
let notificationSubscriptions: NotificationSubscription | undefined;
|
|
66
73
|
|
|
74
|
+
// MCP OAuth auth requirement (from @auth tag)
|
|
75
|
+
let auth: 'required' | 'optional' | string | undefined;
|
|
76
|
+
|
|
67
77
|
try {
|
|
68
78
|
// If source doesn't contain a class declaration, wrap it in one
|
|
69
79
|
let sourceToParse = source;
|
|
@@ -448,6 +458,12 @@ export class SchemaExtractor {
|
|
|
448
458
|
const classJsdoc = this.getJSDocComment(node as any, sourceFile);
|
|
449
459
|
const isStatefulClass = /@stateful\b/i.test(classJsdoc);
|
|
450
460
|
|
|
461
|
+
// Extract @auth tag for MCP OAuth requirement
|
|
462
|
+
const authMatch = classJsdoc.match(/@auth(?:\s+(\S+))?/i);
|
|
463
|
+
if (authMatch) {
|
|
464
|
+
auth = authMatch[1]?.trim() || 'required';
|
|
465
|
+
}
|
|
466
|
+
|
|
451
467
|
// Extract notification subscriptions from @notify-on tag
|
|
452
468
|
notificationSubscriptions = this.extractNotifyOn(classJsdoc);
|
|
453
469
|
|
|
@@ -495,6 +511,11 @@ export class SchemaExtractor {
|
|
|
495
511
|
result.notificationSubscriptions = notificationSubscriptions;
|
|
496
512
|
}
|
|
497
513
|
|
|
514
|
+
// Include auth requirement if detected
|
|
515
|
+
if (auth) {
|
|
516
|
+
result.auth = auth;
|
|
517
|
+
}
|
|
518
|
+
|
|
498
519
|
return result;
|
|
499
520
|
}
|
|
500
521
|
|
|
@@ -1220,7 +1241,7 @@ export class SchemaExtractor {
|
|
|
1220
1241
|
private extractDescription(jsdocContent: string): string {
|
|
1221
1242
|
// Split by @tags that appear at start of a JSDoc line (after optional * prefix)
|
|
1222
1243
|
// This avoids matching @tag references inline in description text
|
|
1223
|
-
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|fallback|logged|circuitBreaker|cached|timeout|retryable|throttled|debounced|queued|validate|use|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
|
|
1244
|
+
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|fallback|logged|circuitBreaker|cached|timeout|retryable|throttled|debounced|queued|validate|use|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility|auth)\b/)[0];
|
|
1224
1245
|
|
|
1225
1246
|
// Remove leading * from each line and trim
|
|
1226
1247
|
const lines = beforeTags
|
|
@@ -1387,11 +1408,23 @@ export class SchemaExtractor {
|
|
|
1387
1408
|
if (formatMatch) {
|
|
1388
1409
|
const format = formatMatch[1].trim();
|
|
1389
1410
|
// Validate format is in whitelist (JSON Schema + custom formats)
|
|
1390
|
-
const validFormats = [
|
|
1391
|
-
|
|
1411
|
+
const validFormats = [
|
|
1412
|
+
// JSON Schema standard formats
|
|
1413
|
+
'email', 'date', 'date-time', 'time', 'duration', 'uri', 'uri-reference', 'uuid', 'ipv4', 'ipv6', 'hostname',
|
|
1414
|
+
// Photon display formats
|
|
1415
|
+
'json', 'table', 'date-range', 'datetime-range',
|
|
1416
|
+
// Enhanced input formats
|
|
1417
|
+
'password', 'secret', 'url', 'phone', 'tel', 'search', 'color', 'textarea', 'slider',
|
|
1418
|
+
'tags', 'rating', 'radio', 'segmented', 'code', 'markdown',
|
|
1419
|
+
// File formats
|
|
1420
|
+
'path', 'file', 'directory',
|
|
1421
|
+
];
|
|
1422
|
+
// Allow subtypes like code:typescript, code:python
|
|
1423
|
+
const formatBase = format.split(':')[0];
|
|
1424
|
+
if (!validFormats.includes(format) && !validFormats.includes(formatBase)) {
|
|
1392
1425
|
console.warn(
|
|
1393
1426
|
`Invalid @format value: "${format}". ` +
|
|
1394
|
-
`Valid formats: ${validFormats.join(', ')}. Format not applied.`
|
|
1427
|
+
`Valid formats: ${validFormats.join(', ')} (with optional :subtype). Format not applied.`
|
|
1395
1428
|
);
|
|
1396
1429
|
} else {
|
|
1397
1430
|
paramConstraints.format = format;
|
|
@@ -1619,7 +1652,13 @@ export class SchemaExtractor {
|
|
|
1619
1652
|
`Pattern is ignored when specific values are defined via enum or @choice.`
|
|
1620
1653
|
);
|
|
1621
1654
|
}
|
|
1622
|
-
// Skip enum types for most constraints (but still apply deprecated, examples,
|
|
1655
|
+
// Skip enum types for most constraints (but still apply format, deprecated, examples, default)
|
|
1656
|
+
if (constraints.format !== undefined) {
|
|
1657
|
+
s.format = constraints.format;
|
|
1658
|
+
}
|
|
1659
|
+
if (constraints.default !== undefined) {
|
|
1660
|
+
s.default = constraints.default;
|
|
1661
|
+
}
|
|
1623
1662
|
if (constraints.examples !== undefined) {
|
|
1624
1663
|
s.examples = constraints.examples;
|
|
1625
1664
|
}
|
|
@@ -2285,7 +2324,7 @@ export class SchemaExtractor {
|
|
|
2285
2324
|
}
|
|
2286
2325
|
|
|
2287
2326
|
// Match content formats
|
|
2288
|
-
if (['json', 'markdown', 'yaml', 'xml', 'html', 'mermaid'].includes(format)) {
|
|
2327
|
+
if (['json', 'markdown', 'yaml', 'xml', 'html', 'mermaid', 'embed'].includes(format)) {
|
|
2289
2328
|
return format as OutputFormat;
|
|
2290
2329
|
}
|
|
2291
2330
|
|
|
@@ -2300,7 +2339,14 @@ export class SchemaExtractor {
|
|
|
2300
2339
|
}
|
|
2301
2340
|
|
|
2302
2341
|
// Match visualization formats
|
|
2303
|
-
if (['metric', 'gauge', 'timeline', 'dashboard', 'cart', 'qr'
|
|
2342
|
+
if (['metric', 'gauge', 'progress', 'badge', 'timeline', 'dashboard', 'cart', 'qr', 'slides',
|
|
2343
|
+
'steps', 'stepper', 'log', 'image', 'hero', 'banner', 'quote', 'profile', 'heatmap',
|
|
2344
|
+
'kanban', 'calendar', 'map', 'cron', 'comparison', 'invoice', 'receipt', 'network', 'graph'].includes(format)) {
|
|
2345
|
+
return format as OutputFormat;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// Match layout/design formats
|
|
2349
|
+
if (['stat-group', 'statgroup', 'feature-grid', 'features', 'carousel', 'gallery', 'masonry'].includes(format)) {
|
|
2304
2350
|
return format as OutputFormat;
|
|
2305
2351
|
}
|
|
2306
2352
|
|
|
@@ -2920,7 +2966,7 @@ export class SchemaExtractor {
|
|
|
2920
2966
|
/**
|
|
2921
2967
|
* Capability types that can be auto-detected from source code
|
|
2922
2968
|
*/
|
|
2923
|
-
export type PhotonCapability = 'emit' | 'memory' | 'call' | 'mcp' | 'lock' | 'instanceMeta' | 'allInstances';
|
|
2969
|
+
export type PhotonCapability = 'emit' | 'memory' | 'call' | 'mcp' | 'lock' | 'instanceMeta' | 'allInstances' | 'caller';
|
|
2924
2970
|
|
|
2925
2971
|
/**
|
|
2926
2972
|
* Detect capabilities used by a Photon from its source code.
|
|
@@ -2941,5 +2987,6 @@ export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
|
2941
2987
|
if (/this\.withLock\s*\(/.test(source)) caps.add('lock');
|
|
2942
2988
|
if (/this\.instanceMeta\b/.test(source)) caps.add('instanceMeta');
|
|
2943
2989
|
if (/this\.allInstances\s*\(/.test(source)) caps.add('allInstances');
|
|
2990
|
+
if (/this\.caller\b/.test(source)) caps.add('caller');
|
|
2944
2991
|
return caps;
|
|
2945
2992
|
}
|
package/src/types.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* - Structural: primitive, table, tree, list, none, card, grid, chips, kv
|
|
8
8
|
* - Content: json, markdown, yaml, xml, html, mermaid, code, code:<lang>
|
|
9
9
|
* - Visualization: chart, chart:<type>, metric, gauge, timeline, dashboard, cart
|
|
10
|
+
* - Content: json, markdown, yaml, xml, html, mermaid, code, code:<lang>, slides
|
|
10
11
|
* - Container: panels, tabs, accordion, stack, columns
|
|
11
12
|
*/
|
|
12
13
|
export type OutputFormat =
|
|
13
14
|
| 'primitive' | 'table' | 'tree' | 'list' | 'none'
|
|
14
|
-
| 'json' | 'markdown' | 'yaml' | 'xml' | 'html' | 'mermaid'
|
|
15
|
+
| 'json' | 'markdown' | 'yaml' | 'xml' | 'html' | 'mermaid' | 'slides'
|
|
15
16
|
| 'card' | 'grid' | 'chips' | 'kv' | 'qr'
|
|
16
17
|
| 'chart' | `chart:${string}` | 'metric' | 'gauge' | 'timeline' | 'dashboard' | 'cart'
|
|
17
18
|
| 'panels' | 'tabs' | 'accordion' | 'stack' | 'columns'
|