@portel/photon-core 2.10.1 → 2.12.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.
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Runtime Scheduling System
3
+ *
4
+ * Programmatic scheduling for photons — create, pause, resume, and cancel
5
+ * scheduled tasks at runtime. Available as `this.schedule` on Photon.
6
+ *
7
+ * Complements static `@scheduled`/`@cron` JSDoc tags with dynamic scheduling.
8
+ * Schedules persist to disk; the daemon reads and executes them.
9
+ *
10
+ * Storage: ~/.photon/schedules/{photonId}/{taskId}.json
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * export default class Cleanup extends Photon {
15
+ * async setup() {
16
+ * await this.schedule.create({
17
+ * name: 'nightly-cleanup',
18
+ * schedule: '0 0 * * *',
19
+ * method: 'purge',
20
+ * params: { olderThan: 30 },
21
+ * });
22
+ * }
23
+ *
24
+ * async purge({ olderThan }: { olderThan: number }) {
25
+ * // ... cleanup logic
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ import * as fs from 'fs/promises';
31
+ import * as path from 'path';
32
+ import * as os from 'os';
33
+ import { randomUUID } from 'crypto';
34
+ // ── Cron Shorthands ────────────────────────────────────────────────────
35
+ const CRON_SHORTHANDS = {
36
+ '@yearly': '0 0 1 1 *',
37
+ '@annually': '0 0 1 1 *',
38
+ '@monthly': '0 0 1 * *',
39
+ '@weekly': '0 0 * * 0',
40
+ '@daily': '0 0 * * *',
41
+ '@midnight': '0 0 * * *',
42
+ '@hourly': '0 * * * *',
43
+ };
44
+ /**
45
+ * Resolve cron shorthands and validate basic format.
46
+ * Returns the resolved 5-field cron expression.
47
+ */
48
+ function resolveCron(schedule) {
49
+ const trimmed = schedule.trim();
50
+ // Check shorthands
51
+ const shorthand = CRON_SHORTHANDS[trimmed.toLowerCase()];
52
+ if (shorthand)
53
+ return shorthand;
54
+ // Validate 5-field cron format
55
+ const fields = trimmed.split(/\s+/);
56
+ if (fields.length !== 5) {
57
+ throw new Error(`Invalid cron expression: '${schedule}'. Expected 5 fields (minute hour day month weekday) or a shorthand (@hourly, @daily, @weekly, @monthly, @yearly).`);
58
+ }
59
+ return trimmed;
60
+ }
61
+ // ── Storage Helpers ────────────────────────────────────────────────────
62
+ function getSchedulesDir() {
63
+ return process.env.PHOTON_SCHEDULES_DIR || path.join(os.homedir(), '.photon', 'schedules');
64
+ }
65
+ function photonScheduleDir(photonId) {
66
+ const safeName = photonId.replace(/[^a-zA-Z0-9_-]/g, '_');
67
+ return path.join(getSchedulesDir(), safeName);
68
+ }
69
+ function taskPath(photonId, taskId) {
70
+ return path.join(photonScheduleDir(photonId), `${taskId}.json`);
71
+ }
72
+ async function ensureDir(dir) {
73
+ try {
74
+ await fs.mkdir(dir, { recursive: true });
75
+ }
76
+ catch (err) {
77
+ if (err.code !== 'EEXIST')
78
+ throw err;
79
+ }
80
+ }
81
+ // ── Schedule Provider ──────────────────────────────────────────────────
82
+ /**
83
+ * Runtime Schedule Provider
84
+ *
85
+ * Provides CRUD operations for scheduled tasks.
86
+ * Tasks are persisted as JSON files that the daemon watches and executes.
87
+ */
88
+ export class ScheduleProvider {
89
+ _photonId;
90
+ constructor(photonId) {
91
+ this._photonId = photonId;
92
+ }
93
+ /**
94
+ * Create a new scheduled task
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * await this.schedule.create({
99
+ * name: 'daily-report',
100
+ * schedule: '0 9 * * *',
101
+ * method: 'generate',
102
+ * params: { format: 'pdf' },
103
+ * });
104
+ * ```
105
+ */
106
+ async create(options) {
107
+ const cron = resolveCron(options.schedule);
108
+ // Check for duplicate name
109
+ const existing = await this.getByName(options.name);
110
+ if (existing) {
111
+ throw new Error(`Schedule '${options.name}' already exists (id: ${existing.id}). Use update() to modify it.`);
112
+ }
113
+ const task = {
114
+ id: randomUUID(),
115
+ name: options.name,
116
+ description: options.description,
117
+ cron,
118
+ method: options.method,
119
+ params: options.params || {},
120
+ fireOnce: options.fireOnce ?? false,
121
+ maxExecutions: options.maxExecutions ?? 0,
122
+ status: 'active',
123
+ createdAt: new Date().toISOString(),
124
+ executionCount: 0,
125
+ photonId: this._photonId,
126
+ };
127
+ await this._save(task);
128
+ return task;
129
+ }
130
+ /**
131
+ * Get a scheduled task by ID
132
+ */
133
+ async get(taskId) {
134
+ try {
135
+ const content = await fs.readFile(taskPath(this._photonId, taskId), 'utf-8');
136
+ return JSON.parse(content);
137
+ }
138
+ catch (err) {
139
+ if (err.code === 'ENOENT')
140
+ return null;
141
+ throw err;
142
+ }
143
+ }
144
+ /**
145
+ * Get a scheduled task by name
146
+ */
147
+ async getByName(name) {
148
+ const tasks = await this.list();
149
+ return tasks.find(t => t.name === name) || null;
150
+ }
151
+ /**
152
+ * List all scheduled tasks, optionally filtered by status
153
+ */
154
+ async list(status) {
155
+ const dir = photonScheduleDir(this._photonId);
156
+ let files;
157
+ try {
158
+ files = await fs.readdir(dir);
159
+ }
160
+ catch (err) {
161
+ if (err.code === 'ENOENT')
162
+ return [];
163
+ throw err;
164
+ }
165
+ const tasks = [];
166
+ for (const file of files) {
167
+ if (!file.endsWith('.json'))
168
+ continue;
169
+ try {
170
+ const content = await fs.readFile(path.join(dir, file), 'utf-8');
171
+ const task = JSON.parse(content);
172
+ if (!status || task.status === status) {
173
+ tasks.push(task);
174
+ }
175
+ }
176
+ catch {
177
+ // Skip corrupt files
178
+ }
179
+ }
180
+ return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
181
+ }
182
+ /**
183
+ * Update an existing scheduled task
184
+ */
185
+ async update(taskId, updates) {
186
+ const task = await this.get(taskId);
187
+ if (!task) {
188
+ throw new Error(`Schedule not found: ${taskId}`);
189
+ }
190
+ if (updates.schedule !== undefined) {
191
+ task.cron = resolveCron(updates.schedule);
192
+ }
193
+ if (updates.method !== undefined)
194
+ task.method = updates.method;
195
+ if (updates.params !== undefined)
196
+ task.params = updates.params;
197
+ if (updates.description !== undefined)
198
+ task.description = updates.description;
199
+ if (updates.fireOnce !== undefined)
200
+ task.fireOnce = updates.fireOnce;
201
+ if (updates.maxExecutions !== undefined)
202
+ task.maxExecutions = updates.maxExecutions;
203
+ await this._save(task);
204
+ return task;
205
+ }
206
+ /**
207
+ * Pause a scheduled task (stops execution until resumed)
208
+ */
209
+ async pause(taskId) {
210
+ const task = await this.get(taskId);
211
+ if (!task)
212
+ throw new Error(`Schedule not found: ${taskId}`);
213
+ if (task.status !== 'active') {
214
+ throw new Error(`Cannot pause task with status '${task.status}'. Only active tasks can be paused.`);
215
+ }
216
+ task.status = 'paused';
217
+ await this._save(task);
218
+ return task;
219
+ }
220
+ /**
221
+ * Resume a paused scheduled task
222
+ */
223
+ async resume(taskId) {
224
+ const task = await this.get(taskId);
225
+ if (!task)
226
+ throw new Error(`Schedule not found: ${taskId}`);
227
+ if (task.status !== 'paused') {
228
+ throw new Error(`Cannot resume task with status '${task.status}'. Only paused tasks can be resumed.`);
229
+ }
230
+ task.status = 'active';
231
+ await this._save(task);
232
+ return task;
233
+ }
234
+ /**
235
+ * Cancel (delete) a scheduled task
236
+ */
237
+ async cancel(taskId) {
238
+ try {
239
+ await fs.unlink(taskPath(this._photonId, taskId));
240
+ return true;
241
+ }
242
+ catch (err) {
243
+ if (err.code === 'ENOENT')
244
+ return false;
245
+ throw err;
246
+ }
247
+ }
248
+ /**
249
+ * Cancel a scheduled task by name
250
+ */
251
+ async cancelByName(name) {
252
+ const task = await this.getByName(name);
253
+ if (!task)
254
+ return false;
255
+ return this.cancel(task.id);
256
+ }
257
+ /**
258
+ * Check if a schedule with the given name exists
259
+ */
260
+ async has(name) {
261
+ const task = await this.getByName(name);
262
+ return task !== null;
263
+ }
264
+ /**
265
+ * Cancel all scheduled tasks for this photon
266
+ */
267
+ async cancelAll() {
268
+ const tasks = await this.list();
269
+ let count = 0;
270
+ for (const task of tasks) {
271
+ if (await this.cancel(task.id))
272
+ count++;
273
+ }
274
+ return count;
275
+ }
276
+ /** @internal */
277
+ async _save(task) {
278
+ const dir = photonScheduleDir(this._photonId);
279
+ await ensureDir(dir);
280
+ await fs.writeFile(taskPath(this._photonId, task.id), JSON.stringify(task, null, 2));
281
+ }
282
+ }
283
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAqEpC,0EAA0E;AAE1E,MAAM,eAAe,GAA2B;IAC9C,SAAS,EAAI,WAAW;IACxB,WAAW,EAAE,WAAW;IACxB,UAAU,EAAG,WAAW;IACxB,SAAS,EAAI,WAAW;IACxB,QAAQ,EAAK,WAAW;IACxB,WAAW,EAAE,WAAW;IACxB,SAAS,EAAI,WAAW;CACzB,CAAC;AAEF;;;GAGG;AACH,SAAS,WAAW,CAAC,QAAgB;IACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEhC,mBAAmB;IACnB,MAAM,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,+BAA+B;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,6BAA6B,QAAQ,oHAAoH,CAC1J,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,0EAA0E;AAE1E,SAAS,eAAe;IACtB,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AAC7F,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB;IACzC,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,QAAQ,CAAC,QAAgB,EAAE,MAAc;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;AAClE,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IACvC,CAAC;AACH,CAAC;AAED,0EAA0E;AAE1E;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IACnB,SAAS,CAAS;IAE1B,YAAY,QAAgB;QAC1B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,MAAM,CAAC,OAA8B;QACzC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAE3C,2BAA2B;QAC3B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,aAAa,OAAO,CAAC,IAAI,yBAAyB,QAAQ,CAAC,EAAE,+BAA+B,CAAC,CAAC;QAChH,CAAC;QAED,MAAM,IAAI,GAAkB;YAC1B,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,IAAI;YACJ,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;YAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK;YACnC,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,CAAC;YACzC,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,cAAc,EAAE,CAAC;YACjB,QAAQ,EAAE,IAAI,CAAC,SAAS;SACzB,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,GAAG,CAAC,MAAc;QACtB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACvC,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,SAAS,CAAC,IAAY;QAC1B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC;IAClD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI,CAAC,MAAuB;QAChC,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,KAAe,CAAC;QACpB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAC;YACrC,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,MAAM,KAAK,GAAoB,EAAE,CAAC;QAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YACtC,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;gBACjE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;gBAClD,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;oBACtC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,qBAAqB;YACvB,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACtE,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,OAA8B;QACzD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QACnD,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/D,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/D,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAC9E,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;YAAE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACrE,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS;YAAE,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QAEpF,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc;QACxB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,CAAC,MAAM,qCAAqC,CAAC,CAAC;QACtG,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,MAAc;QACzB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,mCAAmC,IAAI,CAAC,MAAM,sCAAsC,CAAC,CAAC;QACxG,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,MAAc;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACxC,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,GAAG,CAAC,IAAY;QACpB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxC,OAAO,IAAI,KAAK,IAAI,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,SAAS;QACb,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,KAAK,EAAE,CAAC;QAC1C,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gBAAgB;IACR,KAAK,CAAC,KAAK,CAAC,IAAmB;QACrC,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACrB,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC;CACF"}
@@ -57,6 +57,11 @@ export declare class SchemaExtractor {
57
57
  * Extracts: type, optional, readonly
58
58
  */
59
59
  private buildSchemaFromType;
60
+ /**
61
+ * Extract JSDoc description from a property signature.
62
+ * Handles both /** comment * / style and // comment style.
63
+ */
64
+ private getPropertyJsDoc;
60
65
  /**
61
66
  * Convert TypeScript type node to JSON schema
62
67
  */
@@ -160,6 +165,52 @@ export declare class SchemaExtractor {
160
165
  * Indicates this method runs in background — returns execution ID immediately
161
166
  */
162
167
  private hasAsyncTag;
168
+ /**
169
+ * Extract @title tag value (human-readable display name)
170
+ * Example: @title Create New Task
171
+ */
172
+ private extractTitle;
173
+ /**
174
+ * Check if JSDoc contains method-level @readOnly tag (NOT param-level {@readOnly})
175
+ * Method-level: indicates tool has no side effects (MCP readOnlyHint)
176
+ * Param-level: {@readOnly} inside @param — different regex, no conflict
177
+ */
178
+ private hasReadOnlyHint;
179
+ /**
180
+ * Check if JSDoc contains @destructive tag
181
+ * Indicates tool performs destructive operations requiring confirmation
182
+ */
183
+ private hasDestructiveHint;
184
+ /**
185
+ * Check if JSDoc contains @idempotent tag
186
+ * Indicates tool is safe to retry — multiple calls produce same effect
187
+ */
188
+ private hasIdempotentHint;
189
+ /**
190
+ * Extract open/closed world hint from @openWorld or @closedWorld tags
191
+ * @openWorld → true (tool interacts with external systems)
192
+ * @closedWorld → false (tool operates only on local data)
193
+ * Returns undefined if neither tag present
194
+ */
195
+ private extractOpenWorldHint;
196
+ /**
197
+ * Extract @audience tag value
198
+ * @audience user → ['user']
199
+ * @audience assistant → ['assistant']
200
+ * @audience user assistant → ['user', 'assistant']
201
+ */
202
+ private extractAudience;
203
+ /**
204
+ * Extract @priority tag value (content importance 0.0-1.0)
205
+ */
206
+ private extractContentPriority;
207
+ /**
208
+ * Infer output schema from TypeScript return type annotation.
209
+ * Unwraps Promise<T> and converts T to JSON Schema.
210
+ * Property descriptions come from JSDoc on the type/interface properties.
211
+ * Only produces a schema for object return types (not primitives/arrays).
212
+ */
213
+ private inferOutputSchemaFromReturnType;
163
214
  /**
164
215
  * Extract notification subscriptions from @notify-on tag
165
216
  * Specifies which event types this photon is interested in
@@ -294,6 +345,15 @@ export declare class SchemaExtractor {
294
345
  * Note: Does NOT match @icon inside layout hints like {@icon fieldname}
295
346
  */
296
347
  private extractIcon;
348
+ /**
349
+ * Extract icon image entries from @icon (file path) and @icons tags
350
+ * @icon ./icons/calc.png → [{ path: './icons/calc.png' }]
351
+ * @icon ./icons/calc.svg → [{ path: './icons/calc.svg' }]
352
+ * @icons ./icons/calc-48.png 48x48 → [{ path: '...', sizes: '48x48' }]
353
+ * @icons ./icons/calc-dark.svg dark → [{ path: '...', theme: 'dark' }]
354
+ * @icons ./icons/calc-96.png 96x96 dark → [{ path: '...', sizes: '96x96', theme: 'dark' }]
355
+ */
356
+ private extractIconImages;
297
357
  /**
298
358
  * Extract MIME type from @mimeType tag
299
359
  * Example: @mimeType text/markdown
@@ -1 +1 @@
1
- {"version":3,"file":"schema-extractor.d.ts","sourceRoot":"","sources":["../src/schema-extractor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema-extractor.d.ts","sourceRoot":"","sources":["../src/schema-extractor.ts"],"names":[],"mappings":"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"}
@@ -191,10 +191,20 @@ export class SchemaExtractor {
191
191
  const layoutHints = this.extractLayoutHints(jsdoc);
192
192
  const buttonLabel = this.extractButtonLabel(jsdoc);
193
193
  const icon = this.extractIcon(jsdoc);
194
+ const iconImages = this.extractIconImages(jsdoc);
194
195
  const yields = isGenerator ? this.extractYieldsFromJSDoc(jsdoc) : undefined;
195
196
  const isStateful = this.hasStatefulTag(jsdoc);
196
197
  const autorun = this.hasAutorunTag(jsdoc);
197
198
  const isAsync = this.hasAsyncTag(jsdoc);
199
+ // MCP standard annotations
200
+ const title = this.extractTitle(jsdoc);
201
+ const readOnlyHint = this.hasReadOnlyHint(jsdoc);
202
+ const destructiveHint = this.hasDestructiveHint(jsdoc);
203
+ const idempotentHint = this.hasIdempotentHint(jsdoc);
204
+ const openWorldHint = this.extractOpenWorldHint(jsdoc);
205
+ const audience = this.extractAudience(jsdoc);
206
+ const contentPriority = this.extractContentPriority(jsdoc);
207
+ const outputSchema = this.inferOutputSchemaFromReturnType(member, sourceFile);
198
208
  // Daemon features
199
209
  const webhook = this.extractWebhook(jsdoc, methodName);
200
210
  const scheduled = this.extractScheduled(jsdoc, methodName);
@@ -236,6 +246,7 @@ export class SchemaExtractor {
236
246
  ...(layoutHints ? { layoutHints } : {}),
237
247
  ...(buttonLabel ? { buttonLabel } : {}),
238
248
  ...(icon ? { icon } : {}),
249
+ ...(iconImages ? { iconImages } : {}),
239
250
  ...(isGenerator ? { isGenerator: true } : {}),
240
251
  ...(yields && yields.length > 0 ? { yields } : {}),
241
252
  ...(isStateful ? { isStateful: true } : {}),
@@ -261,6 +272,15 @@ export class SchemaExtractor {
261
272
  ...(deprecated !== undefined ? { deprecated } : {}),
262
273
  // Unified middleware declarations (new — runtime uses this)
263
274
  ...(middleware.length > 0 ? { middleware } : {}),
275
+ // MCP standard annotations
276
+ ...(title ? { title } : {}),
277
+ ...(readOnlyHint ? { readOnlyHint: true } : {}),
278
+ ...(destructiveHint ? { destructiveHint: true } : {}),
279
+ ...(idempotentHint ? { idempotentHint: true } : {}),
280
+ ...(openWorldHint !== undefined ? { openWorldHint } : {}),
281
+ ...(audience ? { audience } : {}),
282
+ ...(contentPriority !== undefined ? { contentPriority } : {}),
283
+ ...(outputSchema ? { outputSchema } : {}),
264
284
  // Event emission (for @stateful classes)
265
285
  ...emitsEventData,
266
286
  });
@@ -543,6 +563,11 @@ export class SchemaExtractor {
543
563
  else {
544
564
  properties[propName] = { type: 'object' };
545
565
  }
566
+ // Extract JSDoc description from property (e.g., /** Task ID */ id: string)
567
+ const jsDocComment = this.getPropertyJsDoc(member, sourceFile);
568
+ if (jsDocComment) {
569
+ properties[propName].description = jsDocComment;
570
+ }
546
571
  // Add readonly from TypeScript (JSDoc can override)
547
572
  if (isReadonly) {
548
573
  properties[propName]._tsReadOnly = true;
@@ -552,6 +577,29 @@ export class SchemaExtractor {
552
577
  }
553
578
  return { properties, required };
554
579
  }
580
+ /**
581
+ * Extract JSDoc description from a property signature.
582
+ * Handles both /** comment * / style and // comment style.
583
+ */
584
+ getPropertyJsDoc(member, sourceFile) {
585
+ // Check for JSDoc comments attached to the node
586
+ const jsDocNodes = member.jsDoc;
587
+ if (jsDocNodes && jsDocNodes.length > 0) {
588
+ const comment = jsDocNodes[0].comment;
589
+ if (typeof comment === 'string')
590
+ return comment.trim();
591
+ }
592
+ // Fallback: look for leading comment in source text
593
+ const fullText = sourceFile.getFullText();
594
+ const start = member.getFullStart();
595
+ const leading = fullText.substring(start, member.getStart(sourceFile)).trim();
596
+ // Match /** ... */ or /* ... */
597
+ const blockMatch = leading.match(/\/\*\*?\s*([\s\S]*?)\s*\*\//);
598
+ if (blockMatch) {
599
+ return blockMatch[1].replace(/\s*\*\s*/g, ' ').trim();
600
+ }
601
+ return undefined;
602
+ }
555
603
  /**
556
604
  * Convert TypeScript type node to JSON schema
557
605
  */
@@ -665,6 +713,12 @@ export class SchemaExtractor {
665
713
  resolved = node.type;
666
714
  return;
667
715
  }
716
+ // Also resolve interface declarations → synthesize a TypeLiteral
717
+ if (ts.isInterfaceDeclaration(node) && node.name.text === typeName) {
718
+ // Create a synthetic TypeLiteralNode from interface members
719
+ resolved = ts.factory.createTypeLiteralNode(node.members.filter(ts.isPropertySignature));
720
+ return;
721
+ }
668
722
  ts.forEachChild(node, visit);
669
723
  };
670
724
  visit(sourceFile);
@@ -1529,6 +1583,114 @@ export class SchemaExtractor {
1529
1583
  hasAsyncTag(jsdocContent) {
1530
1584
  return /@async\b/i.test(jsdocContent);
1531
1585
  }
1586
+ /**
1587
+ * Extract @title tag value (human-readable display name)
1588
+ * Example: @title Create New Task
1589
+ */
1590
+ extractTitle(jsdocContent) {
1591
+ const match = jsdocContent.match(/@title\s+(.+?)(?:\n|\s*\*\/|\s*\*\s*@)/s);
1592
+ if (match) {
1593
+ return match[1].replace(/\s*\*\s*/g, ' ').trim();
1594
+ }
1595
+ return undefined;
1596
+ }
1597
+ /**
1598
+ * Check if JSDoc contains method-level @readOnly tag (NOT param-level {@readOnly})
1599
+ * Method-level: indicates tool has no side effects (MCP readOnlyHint)
1600
+ * Param-level: {@readOnly} inside @param — different regex, no conflict
1601
+ */
1602
+ hasReadOnlyHint(jsdocContent) {
1603
+ // Match @readOnly that is NOT inside curly braces (param-level uses {@readOnly})
1604
+ // Look for @readOnly at start of line (after * ) or start of JSDoc
1605
+ return /(?:^|\n)\s*\*?\s*@readOnly\b/m.test(jsdocContent);
1606
+ }
1607
+ /**
1608
+ * Check if JSDoc contains @destructive tag
1609
+ * Indicates tool performs destructive operations requiring confirmation
1610
+ */
1611
+ hasDestructiveHint(jsdocContent) {
1612
+ return /@destructive\b/i.test(jsdocContent);
1613
+ }
1614
+ /**
1615
+ * Check if JSDoc contains @idempotent tag
1616
+ * Indicates tool is safe to retry — multiple calls produce same effect
1617
+ */
1618
+ hasIdempotentHint(jsdocContent) {
1619
+ return /@idempotent\b/i.test(jsdocContent);
1620
+ }
1621
+ /**
1622
+ * Extract open/closed world hint from @openWorld or @closedWorld tags
1623
+ * @openWorld → true (tool interacts with external systems)
1624
+ * @closedWorld → false (tool operates only on local data)
1625
+ * Returns undefined if neither tag present
1626
+ */
1627
+ extractOpenWorldHint(jsdocContent) {
1628
+ if (/@openWorld\b/i.test(jsdocContent))
1629
+ return true;
1630
+ if (/@closedWorld\b/i.test(jsdocContent))
1631
+ return false;
1632
+ return undefined;
1633
+ }
1634
+ /**
1635
+ * Extract @audience tag value
1636
+ * @audience user → ['user']
1637
+ * @audience assistant → ['assistant']
1638
+ * @audience user assistant → ['user', 'assistant']
1639
+ */
1640
+ extractAudience(jsdocContent) {
1641
+ const match = jsdocContent.match(/@audience\s+([\w\s]+?)(?:\n|\s*\*\/|\s*\*\s*@)/);
1642
+ if (match) {
1643
+ const values = match[1].trim().split(/\s+/);
1644
+ const valid = values.filter(v => v === 'user' || v === 'assistant');
1645
+ return valid.length > 0 ? valid : undefined;
1646
+ }
1647
+ return undefined;
1648
+ }
1649
+ /**
1650
+ * Extract @priority tag value (content importance 0.0-1.0)
1651
+ */
1652
+ extractContentPriority(jsdocContent) {
1653
+ const match = jsdocContent.match(/@priority\s+([\d.]+)/);
1654
+ if (match) {
1655
+ const value = parseFloat(match[1]);
1656
+ if (!isNaN(value) && value >= 0 && value <= 1) {
1657
+ return value;
1658
+ }
1659
+ }
1660
+ return undefined;
1661
+ }
1662
+ /**
1663
+ * Infer output schema from TypeScript return type annotation.
1664
+ * Unwraps Promise<T> and converts T to JSON Schema.
1665
+ * Property descriptions come from JSDoc on the type/interface properties.
1666
+ * Only produces a schema for object return types (not primitives/arrays).
1667
+ */
1668
+ inferOutputSchemaFromReturnType(method, sourceFile) {
1669
+ const returnType = method.type;
1670
+ if (!returnType)
1671
+ return undefined;
1672
+ // Unwrap Promise<T> → T
1673
+ let innerType = returnType;
1674
+ if (ts.isTypeReferenceNode(returnType)) {
1675
+ const typeName = returnType.typeName.getText(sourceFile);
1676
+ if (typeName === 'Promise' && returnType.typeArguments?.length) {
1677
+ innerType = returnType.typeArguments[0];
1678
+ }
1679
+ }
1680
+ // Convert to JSON Schema
1681
+ const schema = this.typeNodeToSchema(innerType, sourceFile);
1682
+ // Only produce outputSchema for object types with properties
1683
+ if (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length > 0) {
1684
+ const result = {
1685
+ type: 'object',
1686
+ properties: schema.properties,
1687
+ };
1688
+ if (schema.required?.length)
1689
+ result.required = schema.required;
1690
+ return result;
1691
+ }
1692
+ return undefined;
1693
+ }
1532
1694
  /**
1533
1695
  * Extract notification subscriptions from @notify-on tag
1534
1696
  * Specifies which event types this photon is interested in
@@ -1840,7 +2002,7 @@ export class SchemaExtractor {
1840
2002
  return subtype ? `chart:${subtype}` : 'chart';
1841
2003
  }
1842
2004
  // Match visualization formats
1843
- if (['metric', 'gauge', 'timeline', 'dashboard', 'cart'].includes(format)) {
2005
+ if (['metric', 'gauge', 'timeline', 'dashboard', 'cart', 'qr'].includes(format)) {
1844
2006
  return format;
1845
2007
  }
1846
2008
  // Match container formats
@@ -1898,10 +2060,47 @@ export class SchemaExtractor {
1898
2060
  const withoutLayoutHints = jsdocContent.replace(/\{[^}]+\}/g, '');
1899
2061
  const iconMatch = withoutLayoutHints.match(/@icon\s+([^\s@*,]+)/i);
1900
2062
  if (iconMatch) {
1901
- return iconMatch[1].trim();
2063
+ const value = iconMatch[1].trim();
2064
+ // If it looks like a file path, don't return as emoji icon
2065
+ if (value.startsWith('./') || value.startsWith('../') || /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(value)) {
2066
+ return undefined;
2067
+ }
2068
+ return value;
1902
2069
  }
1903
2070
  return undefined;
1904
2071
  }
2072
+ /**
2073
+ * Extract icon image entries from @icon (file path) and @icons tags
2074
+ * @icon ./icons/calc.png → [{ path: './icons/calc.png' }]
2075
+ * @icon ./icons/calc.svg → [{ path: './icons/calc.svg' }]
2076
+ * @icons ./icons/calc-48.png 48x48 → [{ path: '...', sizes: '48x48' }]
2077
+ * @icons ./icons/calc-dark.svg dark → [{ path: '...', theme: 'dark' }]
2078
+ * @icons ./icons/calc-96.png 96x96 dark → [{ path: '...', sizes: '96x96', theme: 'dark' }]
2079
+ */
2080
+ extractIconImages(jsdocContent) {
2081
+ const images = [];
2082
+ // Check if @icon value is a file path
2083
+ const withoutLayoutHints = jsdocContent.replace(/\{[^}]+\}/g, '');
2084
+ const iconMatch = withoutLayoutHints.match(/@icon\s+([^\s@*,]+)/i);
2085
+ if (iconMatch) {
2086
+ const value = iconMatch[1].trim();
2087
+ if (value.startsWith('./') || value.startsWith('../') || /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(value)) {
2088
+ images.push({ path: value });
2089
+ }
2090
+ }
2091
+ // Extract @icons entries (can have multiple)
2092
+ const iconsRegex = /@icons\s+([^\s@*,]+)(?:\s+(\d+x\d+))?(?:\s+(light|dark))?/gi;
2093
+ let match;
2094
+ while ((match = iconsRegex.exec(jsdocContent)) !== null) {
2095
+ const entry = { path: match[1].trim() };
2096
+ if (match[2])
2097
+ entry.sizes = match[2];
2098
+ if (match[3])
2099
+ entry.theme = match[3];
2100
+ images.push(entry);
2101
+ }
2102
+ return images.length > 0 ? images : undefined;
2103
+ }
1905
2104
  /**
1906
2105
  * Extract MIME type from @mimeType tag
1907
2106
  * Example: @mimeType text/markdown