@portel/photon-core 2.6.1 → 2.7.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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- 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 +5 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +10 -1
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +2 -0
- 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/index.ts +18 -0
- package/src/memory.ts +241 -0
- package/src/schema-extractor.ts +11 -1
- package/src/types.ts +2 -0
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
|
@@ -178,6 +178,7 @@ export class SchemaExtractor {
|
|
|
178
178
|
const yields = isGenerator ? this.extractYieldsFromJSDoc(jsdoc) : undefined;
|
|
179
179
|
const isStateful = this.hasStatefulTag(jsdoc);
|
|
180
180
|
const autorun = this.hasAutorunTag(jsdoc);
|
|
181
|
+
const isAsync = this.hasAsyncTag(jsdoc);
|
|
181
182
|
|
|
182
183
|
// Daemon features
|
|
183
184
|
const webhook = this.extractWebhook(jsdoc, methodName);
|
|
@@ -199,6 +200,7 @@ export class SchemaExtractor {
|
|
|
199
200
|
...(yields && yields.length > 0 ? { yields } : {}),
|
|
200
201
|
...(isStateful ? { isStateful: true } : {}),
|
|
201
202
|
...(autorun ? { autorun: true } : {}),
|
|
203
|
+
...(isAsync ? { isAsync: true } : {}),
|
|
202
204
|
...(isStaticMethod ? { isStatic: true } : {}),
|
|
203
205
|
// Daemon features
|
|
204
206
|
...(webhook !== undefined ? { webhook } : {}),
|
|
@@ -657,7 +659,7 @@ export class SchemaExtractor {
|
|
|
657
659
|
*/
|
|
658
660
|
private extractDescription(jsdocContent: string): string {
|
|
659
661
|
// 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];
|
|
662
|
+
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
663
|
|
|
662
664
|
// Remove leading * from each line and trim
|
|
663
665
|
const lines = beforeTags
|
|
@@ -1076,6 +1078,14 @@ export class SchemaExtractor {
|
|
|
1076
1078
|
return /@autorun/i.test(jsdocContent);
|
|
1077
1079
|
}
|
|
1078
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* Check if JSDoc contains @async tag
|
|
1083
|
+
* Indicates this method runs in background — returns execution ID immediately
|
|
1084
|
+
*/
|
|
1085
|
+
private hasAsyncTag(jsdocContent: string): boolean {
|
|
1086
|
+
return /@async\b/i.test(jsdocContent);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1079
1089
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1080
1090
|
// DAEMON FEATURE EXTRACTION
|
|
1081
1091
|
// ═══════════════════════════════════════════════════════════════════════════════
|
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
|
|