@portel/photon-core 2.6.1 → 2.8.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/audit.d.ts +150 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +364 -0
- package/dist/audit.js.map +1 -0
- package/dist/base.d.ts +60 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +81 -0
- package/dist/base.js.map +1 -1
- package/dist/class-detection.d.ts +8 -2
- package/dist/class-detection.d.ts.map +1 -1
- package/dist/class-detection.js +41 -11
- package/dist/class-detection.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +100 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +213 -0
- package/dist/memory.js.map +1 -0
- package/dist/schema-extractor.d.ts +13 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +80 -12
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/audit.ts +446 -0
- package/src/base.ts +93 -0
- package/src/class-detection.ts +43 -11
- package/src/index.ts +19 -0
- package/src/memory.ts +241 -0
- package/src/schema-extractor.ts +91 -13
- package/src/types.ts +5 -1
package/src/class-detection.ts
CHANGED
|
@@ -51,41 +51,73 @@ export function hasAsyncMethods(ClassConstructor: new (...args: unknown[]) => un
|
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Check if a class has any public methods (instance or static)
|
|
56
|
+
*/
|
|
57
|
+
export function hasMethods(ClassConstructor: new (...args: unknown[]) => unknown): boolean {
|
|
58
|
+
const prototype = ClassConstructor.prototype;
|
|
59
|
+
for (const key of Object.getOwnPropertyNames(prototype)) {
|
|
60
|
+
if (key === 'constructor') continue;
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
62
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const key of Object.getOwnPropertyNames(ClassConstructor)) {
|
|
68
|
+
if (['length', 'name', 'prototype'].includes(key)) continue;
|
|
69
|
+
const descriptor = Object.getOwnPropertyDescriptor(ClassConstructor, key);
|
|
70
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
/**
|
|
55
79
|
* Find a single Photon class in a module
|
|
56
80
|
*
|
|
57
81
|
* Priority: default export first, then named exports.
|
|
58
|
-
*
|
|
82
|
+
* Default exports are trusted unconditionally — the file is named .photon.ts,
|
|
83
|
+
* so the user's intent is clear. For named exports, async methods are used
|
|
84
|
+
* as a heuristic to distinguish photon classes from helper classes.
|
|
59
85
|
*/
|
|
60
86
|
export function findPhotonClass(module: Record<string, unknown>): (new (...args: unknown[]) => unknown) | null {
|
|
61
|
-
//
|
|
87
|
+
// Default export = the user's photon class. Trust it.
|
|
62
88
|
if (module.default && isClass(module.default)) {
|
|
63
|
-
|
|
64
|
-
return module.default;
|
|
65
|
-
}
|
|
89
|
+
return module.default;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
//
|
|
92
|
+
// Named exports: prefer classes with async methods (likely the photon),
|
|
93
|
+
// but fall back to any class with public methods if none are async
|
|
94
|
+
let fallback: (new (...args: unknown[]) => unknown) | null = null;
|
|
95
|
+
|
|
69
96
|
for (const exportedItem of Object.values(module)) {
|
|
70
|
-
if (isClass(exportedItem)
|
|
71
|
-
|
|
97
|
+
if (isClass(exportedItem)) {
|
|
98
|
+
if (hasAsyncMethods(exportedItem)) {
|
|
99
|
+
return exportedItem;
|
|
100
|
+
}
|
|
101
|
+
if (!fallback && hasMethods(exportedItem)) {
|
|
102
|
+
fallback = exportedItem;
|
|
103
|
+
}
|
|
72
104
|
}
|
|
73
105
|
}
|
|
74
106
|
|
|
75
|
-
return
|
|
107
|
+
return fallback;
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
/**
|
|
79
111
|
* Find all Photon classes in a module
|
|
80
112
|
*
|
|
81
|
-
* Returns every exported class that has
|
|
113
|
+
* Returns every exported class that has methods.
|
|
82
114
|
* Used by NCP which may load multiple classes from one file.
|
|
83
115
|
*/
|
|
84
116
|
export function findPhotonClasses(module: Record<string, unknown>): Array<new (...args: unknown[]) => unknown> {
|
|
85
117
|
const classes: Array<new (...args: unknown[]) => unknown> = [];
|
|
86
118
|
|
|
87
119
|
for (const exportedItem of Object.values(module)) {
|
|
88
|
-
if (isClass(exportedItem) &&
|
|
120
|
+
if (isClass(exportedItem) && hasMethods(exportedItem)) {
|
|
89
121
|
classes.push(exportedItem);
|
|
90
122
|
}
|
|
91
123
|
}
|
package/src/index.ts
CHANGED
|
@@ -365,6 +365,7 @@ export {
|
|
|
365
365
|
// Shared Photon class detection for loaders
|
|
366
366
|
export {
|
|
367
367
|
isClass,
|
|
368
|
+
hasMethods,
|
|
368
369
|
hasAsyncMethods,
|
|
369
370
|
findPhotonClass,
|
|
370
371
|
findPhotonClasses,
|
|
@@ -434,6 +435,24 @@ export {
|
|
|
434
435
|
assertArray,
|
|
435
436
|
} from './validation.js';
|
|
436
437
|
|
|
438
|
+
// ===== EXECUTION AUDIT TRAIL =====
|
|
439
|
+
// Zero-effort execution recording for debugging and observability
|
|
440
|
+
export {
|
|
441
|
+
AuditTrail,
|
|
442
|
+
getAuditTrail,
|
|
443
|
+
setAuditTrail,
|
|
444
|
+
generateExecutionId,
|
|
445
|
+
type ExecutionRecord,
|
|
446
|
+
type AuditQueryOptions,
|
|
447
|
+
} from './audit.js';
|
|
448
|
+
|
|
449
|
+
// ===== SCOPED MEMORY =====
|
|
450
|
+
// Framework-level key-value storage (this.memory on PhotonMCP)
|
|
451
|
+
export {
|
|
452
|
+
MemoryProvider,
|
|
453
|
+
type MemoryScope,
|
|
454
|
+
} from './memory.js';
|
|
455
|
+
|
|
437
456
|
// ===== ASSET DISCOVERY =====
|
|
438
457
|
// Discover UI, prompt, and resource assets from Photon files
|
|
439
458
|
export {
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped Memory System
|
|
3
|
+
*
|
|
4
|
+
* Framework-level key-value storage for photons that eliminates
|
|
5
|
+
* boilerplate file I/O. Available as `this.memory` on PhotonMCP.
|
|
6
|
+
*
|
|
7
|
+
* Three scopes:
|
|
8
|
+
* | Scope | Meaning | Storage |
|
|
9
|
+
* |----------|----------------------------------|-----------------------------------|
|
|
10
|
+
* | photon | Private to this photon (default) | ~/.photon/data/{photonId}/ |
|
|
11
|
+
* | session | Per-user session (Beam sessions) | ~/.photon/sessions/{sessionId}/ |
|
|
12
|
+
* | global | Shared across all photons | ~/.photon/data/_global/ |
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* export default class TodoList extends PhotonMCP {
|
|
17
|
+
* async add({ text }: { text: string }) {
|
|
18
|
+
* const items = await this.memory.get<Task[]>('items') ?? [];
|
|
19
|
+
* items.push({ id: crypto.randomUUID(), text });
|
|
20
|
+
* await this.memory.set('items', items);
|
|
21
|
+
* return items;
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import * as os from 'os';
|
|
30
|
+
|
|
31
|
+
export type MemoryScope = 'photon' | 'session' | 'global';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the base data directory
|
|
35
|
+
*/
|
|
36
|
+
function getDataDir(): string {
|
|
37
|
+
return process.env.PHOTON_DATA_DIR || path.join(os.homedir(), '.photon', 'data');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the sessions directory
|
|
42
|
+
*/
|
|
43
|
+
function getSessionsDir(): string {
|
|
44
|
+
return process.env.PHOTON_SESSIONS_DIR || path.join(os.homedir(), '.photon', 'sessions');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve storage directory for a given scope
|
|
49
|
+
*/
|
|
50
|
+
function resolveDir(photonId: string, scope: MemoryScope, sessionId?: string): string {
|
|
51
|
+
const safeName = photonId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
52
|
+
|
|
53
|
+
switch (scope) {
|
|
54
|
+
case 'photon':
|
|
55
|
+
return path.join(getDataDir(), safeName);
|
|
56
|
+
case 'session':
|
|
57
|
+
if (!sessionId) {
|
|
58
|
+
throw new Error('Session ID required for session-scoped memory. Set via memory.sessionId.');
|
|
59
|
+
}
|
|
60
|
+
const safeSession = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
61
|
+
return path.join(getSessionsDir(), safeSession, safeName);
|
|
62
|
+
case 'global':
|
|
63
|
+
return path.join(getDataDir(), '_global');
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`Unknown memory scope: ${scope}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the file path for a key within a directory
|
|
71
|
+
*/
|
|
72
|
+
function keyPath(dir: string, key: string): string {
|
|
73
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
74
|
+
return path.join(dir, `${safeKey}.json`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scoped Memory Provider
|
|
79
|
+
*
|
|
80
|
+
* Provides key-value storage with automatic JSON serialization.
|
|
81
|
+
* Each key is stored as a separate file for atomic operations.
|
|
82
|
+
*/
|
|
83
|
+
export class MemoryProvider {
|
|
84
|
+
private _photonId: string;
|
|
85
|
+
private _sessionId?: string;
|
|
86
|
+
|
|
87
|
+
constructor(photonId: string, sessionId?: string) {
|
|
88
|
+
this._photonId = photonId;
|
|
89
|
+
this._sessionId = sessionId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Current session ID (can be updated by the runtime)
|
|
94
|
+
*/
|
|
95
|
+
get sessionId(): string | undefined {
|
|
96
|
+
return this._sessionId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
set sessionId(id: string | undefined) {
|
|
100
|
+
this._sessionId = id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get a value from memory
|
|
105
|
+
*
|
|
106
|
+
* @param key The key to retrieve
|
|
107
|
+
* @param scope Storage scope (default: 'photon')
|
|
108
|
+
* @returns The stored value, or null if not found
|
|
109
|
+
*/
|
|
110
|
+
async get<T = any>(key: string, scope: MemoryScope = 'photon'): Promise<T | null> {
|
|
111
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
112
|
+
const filePath = keyPath(dir, key);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (!fs.existsSync(filePath)) return null;
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
117
|
+
return JSON.parse(content) as T;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set a value in memory
|
|
125
|
+
*
|
|
126
|
+
* @param key The key to store
|
|
127
|
+
* @param value The value (must be JSON-serializable)
|
|
128
|
+
* @param scope Storage scope (default: 'photon')
|
|
129
|
+
*/
|
|
130
|
+
async set<T = any>(key: string, value: T, scope: MemoryScope = 'photon'): Promise<void> {
|
|
131
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(dir)) {
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const filePath = keyPath(dir, key);
|
|
138
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Delete a key from memory
|
|
143
|
+
*
|
|
144
|
+
* @param key The key to delete
|
|
145
|
+
* @param scope Storage scope (default: 'photon')
|
|
146
|
+
* @returns true if the key existed and was deleted
|
|
147
|
+
*/
|
|
148
|
+
async delete(key: string, scope: MemoryScope = 'photon'): Promise<boolean> {
|
|
149
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
150
|
+
const filePath = keyPath(dir, key);
|
|
151
|
+
|
|
152
|
+
if (fs.existsSync(filePath)) {
|
|
153
|
+
fs.unlinkSync(filePath);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a key exists in memory
|
|
161
|
+
*
|
|
162
|
+
* @param key The key to check
|
|
163
|
+
* @param scope Storage scope (default: 'photon')
|
|
164
|
+
*/
|
|
165
|
+
async has(key: string, scope: MemoryScope = 'photon'): Promise<boolean> {
|
|
166
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
167
|
+
return fs.existsSync(keyPath(dir, key));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* List all keys in memory for a scope
|
|
172
|
+
*
|
|
173
|
+
* @param scope Storage scope (default: 'photon')
|
|
174
|
+
*/
|
|
175
|
+
async keys(scope: MemoryScope = 'photon'): Promise<string[]> {
|
|
176
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(dir)) return [];
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return fs.readdirSync(dir)
|
|
182
|
+
.filter(f => f.endsWith('.json'))
|
|
183
|
+
.map(f => f.slice(0, -5)); // Remove .json extension
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear all keys in a scope
|
|
191
|
+
*
|
|
192
|
+
* @param scope Storage scope (default: 'photon')
|
|
193
|
+
*/
|
|
194
|
+
async clear(scope: MemoryScope = 'photon'): Promise<void> {
|
|
195
|
+
const dir = resolveDir(this._photonId, scope, this._sessionId);
|
|
196
|
+
|
|
197
|
+
if (fs.existsSync(dir)) {
|
|
198
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
fs.unlinkSync(path.join(dir, file));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get all key-value pairs in a scope
|
|
207
|
+
*
|
|
208
|
+
* @param scope Storage scope (default: 'photon')
|
|
209
|
+
*/
|
|
210
|
+
async getAll<T = any>(scope: MemoryScope = 'photon'): Promise<Record<string, T>> {
|
|
211
|
+
const allKeys = await this.keys(scope);
|
|
212
|
+
const result: Record<string, T> = {};
|
|
213
|
+
|
|
214
|
+
for (const key of allKeys) {
|
|
215
|
+
const value = await this.get<T>(key, scope);
|
|
216
|
+
if (value !== null) {
|
|
217
|
+
result[key] = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Update a value atomically (read-modify-write)
|
|
226
|
+
*
|
|
227
|
+
* @param key The key to update
|
|
228
|
+
* @param updater Function that receives current value and returns new value
|
|
229
|
+
* @param scope Storage scope (default: 'photon')
|
|
230
|
+
*/
|
|
231
|
+
async update<T = any>(
|
|
232
|
+
key: string,
|
|
233
|
+
updater: (current: T | null) => T,
|
|
234
|
+
scope: MemoryScope = 'photon'
|
|
235
|
+
): Promise<T> {
|
|
236
|
+
const current = await this.get<T>(key, scope);
|
|
237
|
+
const updated = updater(current);
|
|
238
|
+
await this.set(key, updated, scope);
|
|
239
|
+
return updated;
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/schema-extractor.ts
CHANGED
|
@@ -109,14 +109,11 @@ export class SchemaExtractor {
|
|
|
109
109
|
// Check if this is an async generator method (has asterisk token)
|
|
110
110
|
const isGenerator = member.asteriskToken !== undefined;
|
|
111
111
|
|
|
112
|
-
// Extract parameter
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const { properties, required } = paramsType
|
|
118
|
-
? this.buildSchemaFromType(paramsType, sourceFile)
|
|
119
|
-
: { properties: {}, required: [] };
|
|
112
|
+
// Extract parameter schema from method signature
|
|
113
|
+
// Supports both patterns:
|
|
114
|
+
// add(item: string) → { item: { type: "string" } }
|
|
115
|
+
// add(params: { item: string }) → { item: { type: "string" } }
|
|
116
|
+
const { properties, required, simpleParams } = this.extractMethodParams(member, sourceFile);
|
|
120
117
|
|
|
121
118
|
// Extract descriptions from JSDoc
|
|
122
119
|
const paramDocs = this.extractParamDocs(jsdoc);
|
|
@@ -178,6 +175,7 @@ export class SchemaExtractor {
|
|
|
178
175
|
const yields = isGenerator ? this.extractYieldsFromJSDoc(jsdoc) : undefined;
|
|
179
176
|
const isStateful = this.hasStatefulTag(jsdoc);
|
|
180
177
|
const autorun = this.hasAutorunTag(jsdoc);
|
|
178
|
+
const isAsync = this.hasAsyncTag(jsdoc);
|
|
181
179
|
|
|
182
180
|
// Daemon features
|
|
183
181
|
const webhook = this.extractWebhook(jsdoc, methodName);
|
|
@@ -199,7 +197,9 @@ export class SchemaExtractor {
|
|
|
199
197
|
...(yields && yields.length > 0 ? { yields } : {}),
|
|
200
198
|
...(isStateful ? { isStateful: true } : {}),
|
|
201
199
|
...(autorun ? { autorun: true } : {}),
|
|
200
|
+
...(isAsync ? { isAsync: true } : {}),
|
|
202
201
|
...(isStaticMethod ? { isStatic: true } : {}),
|
|
202
|
+
...(simpleParams ? { simpleParams: true } : {}),
|
|
203
203
|
// Daemon features
|
|
204
204
|
...(webhook !== undefined ? { webhook } : {}),
|
|
205
205
|
...(scheduled ? { scheduled } : {}),
|
|
@@ -213,10 +213,15 @@ export class SchemaExtractor {
|
|
|
213
213
|
// Look for class declarations
|
|
214
214
|
if (ts.isClassDeclaration(node)) {
|
|
215
215
|
node.members.forEach((member) => {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
// Process all public methods (sync or async)
|
|
217
|
+
// Skip private/protected — only public methods become tools
|
|
218
|
+
if (ts.isMethodDeclaration(member)) {
|
|
219
|
+
const isPrivate = member.modifiers?.some(
|
|
220
|
+
m => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
|
|
221
|
+
);
|
|
222
|
+
if (!isPrivate) {
|
|
223
|
+
processMethod(member);
|
|
224
|
+
}
|
|
220
225
|
}
|
|
221
226
|
});
|
|
222
227
|
}
|
|
@@ -280,6 +285,61 @@ export class SchemaExtractor {
|
|
|
280
285
|
return firstParam.type;
|
|
281
286
|
}
|
|
282
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Extract method parameters into JSON schema properties.
|
|
290
|
+
*
|
|
291
|
+
* Handles two patterns:
|
|
292
|
+
* 1. Object param: add(params: { item: string }) → extracts inner properties
|
|
293
|
+
* 2. Simple params: add(item: string) or add(a: number, b: number) → each param becomes a property
|
|
294
|
+
*/
|
|
295
|
+
private extractMethodParams(method: ts.MethodDeclaration, sourceFile: ts.SourceFile): { properties: Record<string, any>, required: string[], simpleParams?: boolean } {
|
|
296
|
+
if (method.parameters.length === 0) {
|
|
297
|
+
return { properties: {}, required: [] };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const firstParam = method.parameters[0];
|
|
301
|
+
const firstType = firstParam.type;
|
|
302
|
+
|
|
303
|
+
// Pattern 1: Single object param — add(params: { item: string })
|
|
304
|
+
// Unwrap the object type's properties directly
|
|
305
|
+
if (firstType && method.parameters.length === 1) {
|
|
306
|
+
// Direct type literal: { item: string }
|
|
307
|
+
if (ts.isTypeLiteralNode(firstType)) {
|
|
308
|
+
return this.buildSchemaFromType(firstType, sourceFile);
|
|
309
|
+
}
|
|
310
|
+
// Union containing object literal: { item: string } | string
|
|
311
|
+
if (ts.isUnionTypeNode(firstType)) {
|
|
312
|
+
for (const memberType of firstType.types) {
|
|
313
|
+
if (ts.isTypeLiteralNode(memberType)) {
|
|
314
|
+
return this.buildSchemaFromType(memberType, sourceFile);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Pattern 2: Simple typed params — add(item: string) or add(a: number, b: number)
|
|
321
|
+
// Flag as simpleParams so the runtime destructures the params object into individual args
|
|
322
|
+
const properties: Record<string, any> = {};
|
|
323
|
+
const required: string[] = [];
|
|
324
|
+
|
|
325
|
+
for (const param of method.parameters) {
|
|
326
|
+
const paramName = param.name.getText(sourceFile);
|
|
327
|
+
const isOptional = param.questionToken !== undefined || param.initializer !== undefined;
|
|
328
|
+
|
|
329
|
+
if (!isOptional) {
|
|
330
|
+
required.push(paramName);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (param.type) {
|
|
334
|
+
properties[paramName] = this.typeNodeToSchema(param.type, sourceFile);
|
|
335
|
+
} else {
|
|
336
|
+
properties[paramName] = { type: 'string' };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { properties, required, simpleParams: true };
|
|
341
|
+
}
|
|
342
|
+
|
|
283
343
|
/**
|
|
284
344
|
* Build JSON schema from TypeScript type node
|
|
285
345
|
* Extracts: type, optional, readonly
|
|
@@ -657,7 +717,7 @@ export class SchemaExtractor {
|
|
|
657
717
|
*/
|
|
658
718
|
private extractDescription(jsdocContent: string): string {
|
|
659
719
|
// Split by @param to get only the description part (also stop at other @tags)
|
|
660
|
-
const beforeTags = jsdocContent.split(/@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|webhook|cron|scheduled|locked|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
|
|
720
|
+
const beforeTags = jsdocContent.split(/@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
|
|
661
721
|
|
|
662
722
|
// Remove leading * from each line and trim
|
|
663
723
|
const lines = beforeTags
|
|
@@ -1076,6 +1136,14 @@ export class SchemaExtractor {
|
|
|
1076
1136
|
return /@autorun/i.test(jsdocContent);
|
|
1077
1137
|
}
|
|
1078
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Check if JSDoc contains @async tag
|
|
1141
|
+
* Indicates this method runs in background — returns execution ID immediately
|
|
1142
|
+
*/
|
|
1143
|
+
private hasAsyncTag(jsdocContent: string): boolean {
|
|
1144
|
+
return /@async\b/i.test(jsdocContent);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1079
1147
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1080
1148
|
// DAEMON FEATURE EXTRACTION
|
|
1081
1149
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1473,6 +1541,7 @@ export class SchemaExtractor {
|
|
|
1473
1541
|
const params = this.extractConstructorParams(source);
|
|
1474
1542
|
const mcpDeps = this.extractMCPDependencies(source);
|
|
1475
1543
|
const photonDeps = this.extractPhotonDependencies(source);
|
|
1544
|
+
const isStateful = /@stateful\s+true/.test(source);
|
|
1476
1545
|
|
|
1477
1546
|
// Build lookup maps
|
|
1478
1547
|
const mcpMap = new Map(mcpDeps.map(d => [d.name, d]));
|
|
@@ -1507,6 +1576,15 @@ export class SchemaExtractor {
|
|
|
1507
1576
|
};
|
|
1508
1577
|
}
|
|
1509
1578
|
|
|
1579
|
+
// Non-primitive with default on @stateful class → persisted state
|
|
1580
|
+
if (isStateful && param.hasDefault) {
|
|
1581
|
+
return {
|
|
1582
|
+
param,
|
|
1583
|
+
injectionType: 'state' as const,
|
|
1584
|
+
stateKey: param.name,
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1510
1588
|
// Non-primitive without declaration - treat as env var (will likely fail at runtime)
|
|
1511
1589
|
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
1512
1590
|
return {
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface ExtractedSchema {
|
|
|
63
63
|
isStateful?: boolean;
|
|
64
64
|
/** True if this method should auto-execute when selected (idempotent, no required params) */
|
|
65
65
|
autorun?: boolean;
|
|
66
|
+
/** True if this method runs in background — returns execution ID immediately */
|
|
67
|
+
isAsync?: boolean;
|
|
66
68
|
/** True if this is a static method (class-level, no instance needed) */
|
|
67
69
|
isStatic?: boolean;
|
|
68
70
|
|
|
@@ -110,7 +112,7 @@ export interface ConstructorParam {
|
|
|
110
112
|
/**
|
|
111
113
|
* Injection type for constructor parameters
|
|
112
114
|
*/
|
|
113
|
-
export type InjectionType = 'env' | 'mcp' | 'photon';
|
|
115
|
+
export type InjectionType = 'env' | 'mcp' | 'photon' | 'state';
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
118
|
* Resolved injection info for a constructor parameter
|
|
@@ -124,6 +126,8 @@ export interface ResolvedInjection {
|
|
|
124
126
|
photonDependency?: PhotonDependency;
|
|
125
127
|
/** For 'env' - the environment variable name */
|
|
126
128
|
envVarName?: string;
|
|
129
|
+
/** For 'state' - the key name in the persisted snapshot JSON */
|
|
130
|
+
stateKey?: string;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/**
|