@portel/photon 1.20.1 → 1.21.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.
Files changed (106) hide show
  1. package/README.md +5 -5
  2. package/dist/ag-ui/adapter.d.ts.map +1 -1
  3. package/dist/ag-ui/adapter.js +25 -0
  4. package/dist/ag-ui/adapter.js.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-browse.js +8 -49
  7. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  8. package/dist/auto-ui/beam.d.ts.map +1 -1
  9. package/dist/auto-ui/beam.js +23 -31
  10. package/dist/auto-ui/beam.js.map +1 -1
  11. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  12. package/dist/auto-ui/bridge/index.js +107 -11
  13. package/dist/auto-ui/bridge/index.js.map +1 -1
  14. package/dist/auto-ui/bridge/renderers.d.ts +14 -0
  15. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  16. package/dist/auto-ui/bridge/renderers.js +680 -57
  17. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  18. package/dist/auto-ui/frontend/index.html +3 -3
  19. package/dist/auto-ui/frontend/pure-view.html +19 -19
  20. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.js +29 -0
  22. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  23. package/dist/auto-ui/ui-resolver.d.ts +25 -0
  24. package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
  25. package/dist/auto-ui/ui-resolver.js +95 -0
  26. package/dist/auto-ui/ui-resolver.js.map +1 -0
  27. package/dist/beam-form.bundle.js +7 -7
  28. package/dist/beam-form.bundle.js.map +1 -1
  29. package/dist/beam.bundle.js +905 -185
  30. package/dist/beam.bundle.js.map +4 -4
  31. package/dist/cli/commands/build.d.ts.map +1 -1
  32. package/dist/cli/commands/build.js +9 -5
  33. package/dist/cli/commands/build.js.map +1 -1
  34. package/dist/cli/commands/init.d.ts.map +1 -1
  35. package/dist/cli/commands/init.js +90 -50
  36. package/dist/cli/commands/init.js.map +1 -1
  37. package/dist/cli/commands/publish.d.ts +14 -0
  38. package/dist/cli/commands/publish.d.ts.map +1 -0
  39. package/dist/cli/commands/publish.js +126 -0
  40. package/dist/cli/commands/publish.js.map +1 -0
  41. package/dist/cli/commands/run.d.ts.map +1 -1
  42. package/dist/cli/commands/run.js +2 -0
  43. package/dist/cli/commands/run.js.map +1 -1
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +3 -0
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/context.d.ts +6 -0
  48. package/dist/context.d.ts.map +1 -1
  49. package/dist/context.js +17 -5
  50. package/dist/context.js.map +1 -1
  51. package/dist/daemon/client.d.ts +9 -1
  52. package/dist/daemon/client.d.ts.map +1 -1
  53. package/dist/daemon/client.js +54 -1
  54. package/dist/daemon/client.js.map +1 -1
  55. package/dist/daemon/manager.d.ts +3 -0
  56. package/dist/daemon/manager.d.ts.map +1 -1
  57. package/dist/daemon/manager.js +88 -38
  58. package/dist/daemon/manager.js.map +1 -1
  59. package/dist/daemon/ownership.d.ts +12 -0
  60. package/dist/daemon/ownership.d.ts.map +1 -0
  61. package/dist/daemon/ownership.js +55 -0
  62. package/dist/daemon/ownership.js.map +1 -0
  63. package/dist/daemon/protocol.d.ts +3 -1
  64. package/dist/daemon/protocol.d.ts.map +1 -1
  65. package/dist/daemon/protocol.js +14 -2
  66. package/dist/daemon/protocol.js.map +1 -1
  67. package/dist/daemon/server.js +549 -83
  68. package/dist/daemon/server.js.map +1 -1
  69. package/dist/daemon/session-manager.d.ts +9 -1
  70. package/dist/daemon/session-manager.d.ts.map +1 -1
  71. package/dist/daemon/session-manager.js +54 -1
  72. package/dist/daemon/session-manager.js.map +1 -1
  73. package/dist/daemon/worker-manager.d.ts +12 -0
  74. package/dist/daemon/worker-manager.d.ts.map +1 -1
  75. package/dist/daemon/worker-manager.js +89 -6
  76. package/dist/daemon/worker-manager.js.map +1 -1
  77. package/dist/loader.d.ts +3 -9
  78. package/dist/loader.d.ts.map +1 -1
  79. package/dist/loader.js +168 -113
  80. package/dist/loader.js.map +1 -1
  81. package/dist/photon-cli-runner.d.ts.map +1 -1
  82. package/dist/photon-cli-runner.js +26 -2
  83. package/dist/photon-cli-runner.js.map +1 -1
  84. package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
  85. package/dist/photons/canvas.photon.d.ts +400 -0
  86. package/dist/photons/canvas.photon.d.ts.map +1 -0
  87. package/dist/photons/canvas.photon.js +662 -0
  88. package/dist/photons/canvas.photon.js.map +1 -0
  89. package/dist/photons/canvas.photon.ts +814 -0
  90. package/dist/photons/publish.photon.d.ts +97 -0
  91. package/dist/photons/publish.photon.d.ts.map +1 -0
  92. package/dist/photons/publish.photon.js +569 -0
  93. package/dist/photons/publish.photon.js.map +1 -0
  94. package/dist/photons/publish.photon.ts +683 -0
  95. package/dist/photons/ui/canvas.photon.html +624 -0
  96. package/dist/resource-server.d.ts.map +1 -1
  97. package/dist/resource-server.js +7 -1
  98. package/dist/resource-server.js.map +1 -1
  99. package/dist/shared-utils.d.ts.map +1 -1
  100. package/dist/shared-utils.js +2 -2
  101. package/dist/shared-utils.js.map +1 -1
  102. package/dist/tsx-compiler.d.ts +23 -0
  103. package/dist/tsx-compiler.d.ts.map +1 -0
  104. package/dist/tsx-compiler.js +221 -0
  105. package/dist/tsx-compiler.js.map +1 -0
  106. package/package.json +7 -7
package/dist/loader.js CHANGED
@@ -8,7 +8,7 @@ import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync } from '
8
8
  import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
9
9
  import { createRequire } from 'module';
10
10
  import * as path from 'path';
11
- import { pathToFileURL } from 'url';
11
+ import { fileURLToPath, pathToFileURL } from 'url';
12
12
  import * as crypto from 'crypto';
13
13
  import { startToolSpan } from './telemetry/otel.js';
14
14
  import { spawn } from 'child_process';
@@ -130,6 +130,38 @@ const cliRenderZone = new CLIRenderZone();
130
130
  export function clearRenderZone() {
131
131
  cliRenderZone.clear();
132
132
  }
133
+ /**
134
+ * Inject emit-based convenience helpers on a plain-class instance.
135
+ * Every helper is a thin wrapper around this.emit so generator-yield and
136
+ * imperative-call styles produce identical wire events.
137
+ */
138
+ function injectEmitHelpers(instance) {
139
+ const emit = (data) => instance.emit(data);
140
+ // Mirrors photon-core base-class render(): UI-feedback formats route to their
141
+ // dedicated emit events; all other formats go through the render channel.
142
+ instance.render = (format, value) => {
143
+ if (format === undefined)
144
+ return emit({ emit: 'render:clear' });
145
+ if (format === 'status')
146
+ return emit(typeof value === 'string'
147
+ ? { emit: 'status', message: value }
148
+ : { emit: 'status', ...value });
149
+ if (format === 'progress')
150
+ return emit(typeof value === 'number' ? { emit: 'progress', value } : { emit: 'progress', ...value });
151
+ if (format === 'toast')
152
+ return emit(typeof value === 'string' ? { emit: 'toast', message: value } : { emit: 'toast', ...value });
153
+ emit({ emit: 'render', format, value });
154
+ };
155
+ instance.toast = (message, opts = {}) => emit({ emit: 'toast', message, ...opts });
156
+ instance.log = (message, opts = {}) => emit({ emit: 'log', message, level: opts.level ?? 'info', data: opts.data });
157
+ instance.status = (message) => emit({ emit: 'status', message });
158
+ instance.progress = (value, message) => emit({ emit: 'progress', value, message });
159
+ instance.thinking = (active = true) => emit({ emit: 'thinking', active });
160
+ }
161
+ /** Extra regex checks that force 'emit' capability when helper methods are used. */
162
+ function detectEmitHelperUsage(source) {
163
+ return /this\.(toast|log|status|progress|thinking)\s*\(/.test(source);
164
+ }
133
165
  /**
134
166
  * Render a formatted value in the CLI using @portel/cli's formatOutput.
135
167
  * Uses clear-and-replace semantics — each call overwrites the previous render.
@@ -650,15 +682,12 @@ export class PhotonLoader {
650
682
  // Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
651
683
  // Developers just `import { Array } from '@portel/photon-core'` and use normally
652
684
  this.wireReactiveCollections(instance);
685
+ // Inject format catalog — this.formats
686
+ this.injectFormatCatalog(instance);
653
687
  // Inject @mcp dependencies from source (this.github, this.fs, etc.)
654
688
  if (tsContent) {
655
689
  await this.injectMCPDependencies(instance, tsContent, name);
656
690
  }
657
- // Auto-wrap public methods in @stateful classes to emit events
658
- // All method calls automatically produce events with params, result, timestamp
659
- if (tsContent) {
660
- this.wrapStatefulMethods(instance, tsContent);
661
- }
662
691
  // Inject MCP client factory if available (enables this.mcp() calls)
663
692
  const setMCPFactory = instance.setMCPFactory;
664
693
  if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
@@ -682,6 +711,8 @@ export class PhotonLoader {
682
711
  // Photon base class already has these built-in, so only inject for plain classes
683
712
  if (tsContent && typeof instance.executeTool !== 'function') {
684
713
  const caps = detectCapabilities(tsContent);
714
+ if (detectEmitHelperUsage(tsContent))
715
+ caps.add('emit');
685
716
  if (caps.size > 0) {
686
717
  this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
687
718
  }
@@ -727,15 +758,8 @@ export class PhotonLoader {
727
758
  });
728
759
  }
729
760
  };
730
- // Also inject render() convenience wrapper around emit
731
- instance.render = (format, value) => {
732
- if (format === undefined) {
733
- instance.emit({ emit: 'render:clear' });
734
- }
735
- else {
736
- instance.emit({ emit: 'render', format, value });
737
- }
738
- };
761
+ // Inject convenience helpers (render, toast, log, status, progress, thinking)
762
+ injectEmitHelpers(instance);
739
763
  }
740
764
  if (caps.has('memory')) {
741
765
  // Inject lazy memory provider — capture baseDir from loader context
@@ -1001,8 +1025,13 @@ export class PhotonLoader {
1001
1025
  throw initError;
1002
1026
  }
1003
1027
  }
1028
+ // Auto-wrap public methods in @stateful classes to emit events
1029
+ // Must happen AFTER capability injection so that emit() is available
1030
+ if (tsContent) {
1031
+ this.wrapStatefulMethods(instance, tsContent);
1032
+ }
1004
1033
  // Extract tools, templates, and statics (with schema override support)
1005
- const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath);
1034
+ const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath);
1006
1035
  // ═══ SETTINGS INJECTION ═══
1007
1036
  // If the photon declared `protected settings = { ... }`, inject persistence + proxy
1008
1037
  if (settingsSchema?.hasSettings &&
@@ -1052,6 +1081,8 @@ export class PhotonLoader {
1052
1081
  result.icon = classIcon;
1053
1082
  if (isStateful)
1054
1083
  result.stateful = true;
1084
+ if (extractedAuth)
1085
+ result.auth = extractedAuth;
1055
1086
  // Store class constructor for static method access
1056
1087
  result.classConstructor = MCPClass;
1057
1088
  // Store settings schema for Beam UI
@@ -1123,6 +1154,8 @@ export class PhotonLoader {
1123
1154
  instance._photonResolver = (photonName, instanceName) => {
1124
1155
  return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
1125
1156
  };
1157
+ // Inject format catalog — this.formats
1158
+ this.injectFormatCatalog(instance);
1126
1159
  // Wire reactive collections
1127
1160
  this.wireReactiveCollections(instance);
1128
1161
  // Inject @mcp dependencies
@@ -1152,6 +1185,8 @@ export class PhotonLoader {
1152
1185
  // Detect and inject capabilities for plain classes
1153
1186
  if (tsContent && typeof instance.executeTool !== 'function') {
1154
1187
  const caps = detectCapabilities(tsContent);
1188
+ if (detectEmitHelperUsage(tsContent))
1189
+ caps.add('emit');
1155
1190
  this.injectPathHelpers(instance, tsContent);
1156
1191
  if (caps.has('emit')) {
1157
1192
  instance.emit = (data) => {
@@ -1186,15 +1221,8 @@ export class PhotonLoader {
1186
1221
  });
1187
1222
  }
1188
1223
  };
1189
- // Also inject render() convenience wrapper around emit
1190
- instance.render = (format, value) => {
1191
- if (format === undefined) {
1192
- instance.emit({ emit: 'render:clear' });
1193
- }
1194
- else {
1195
- instance.emit({ emit: 'render', format, value });
1196
- }
1197
- };
1224
+ // Inject convenience helpers (render, toast, log, status, progress, thinking)
1225
+ injectEmitHelpers(instance);
1198
1226
  }
1199
1227
  if (caps.has('memory')) {
1200
1228
  const memoryBaseDir = this.baseDir;
@@ -1296,7 +1324,7 @@ export class PhotonLoader {
1296
1324
  }
1297
1325
  }
1298
1326
  // Extract tools and metadata from embedded source (no disk I/O)
1299
- const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath, tsContent);
1327
+ const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath, tsContent);
1300
1328
  // Settings injection
1301
1329
  if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
1302
1330
  const instanceName = options?.instanceName || 'default';
@@ -1326,6 +1354,8 @@ export class PhotonLoader {
1326
1354
  result.icon = classIcon;
1327
1355
  if (isStateful)
1328
1356
  result.stateful = true;
1357
+ if (extractedAuth)
1358
+ result.auth = extractedAuth;
1329
1359
  result.classConstructor = MCPClass;
1330
1360
  if (settingsSchema?.hasSettings) {
1331
1361
  result.settingsSchema = settingsSchema;
@@ -1341,6 +1371,14 @@ export class PhotonLoader {
1341
1371
  const match = source.match(/\/\*\*([\s\S]*?)\*\/\s*export\s+default\s+class\b/);
1342
1372
  return match ? match[1] : '';
1343
1373
  }
1374
+ extractAuthTag(source) {
1375
+ const docblock = this.extractClassDocblock(source);
1376
+ // Use \b to avoid matching @author, @authorize, etc.
1377
+ const match = docblock.match(/@auth\b(?:\s+(\S+))?/i);
1378
+ if (!match)
1379
+ return undefined;
1380
+ return match[1]?.trim() || 'required';
1381
+ }
1344
1382
  /**
1345
1383
  * Reload a Photon MCP file (for hot reload)
1346
1384
  */
@@ -1567,7 +1605,13 @@ export class PhotonLoader {
1567
1605
  }
1568
1606
  });
1569
1607
  }
1570
- return { tools, templates, statics, settingsSchema: metadata.settingsSchema };
1608
+ return {
1609
+ tools,
1610
+ templates,
1611
+ statics,
1612
+ settingsSchema: metadata.settingsSchema,
1613
+ auth: this.extractAuthTag(source),
1614
+ };
1571
1615
  }
1572
1616
  throw jsonError;
1573
1617
  }
@@ -2241,11 +2285,14 @@ export class PhotonLoader {
2241
2285
  return;
2242
2286
  if (path.dirname(realCurrentPhotonPath) !== path.dirname(resolvedPath))
2243
2287
  return;
2244
- const linkPath = path.join(this.baseDir, path.basename(resolvedPath));
2288
+ // Symlink into the directory where the current photon's symlink lives
2289
+ // (e.g. ~/.photon/), NOT this.baseDir which may be a different workspace.
2290
+ const symlinkDir = path.dirname(currentPhotonPath);
2291
+ const linkPath = path.join(symlinkDir, path.basename(resolvedPath));
2245
2292
  await this.createSymlinkIfMissing(resolvedPath, linkPath);
2246
2293
  const depName = path.basename(resolvedPath).replace(/\.photon\.(ts|js)$/, '');
2247
2294
  const sourceAssetDir = path.join(path.dirname(resolvedPath), depName);
2248
- const targetAssetDir = path.join(this.baseDir, depName);
2295
+ const targetAssetDir = path.join(symlinkDir, depName);
2249
2296
  if (existsSync(sourceAssetDir)) {
2250
2297
  await this.createSymlinkIfMissing(sourceAssetDir, targetAssetDir, 'dir');
2251
2298
  }
@@ -2658,6 +2705,39 @@ Run: photon mcp ${mcpName} --config
2658
2705
  if (mcp.instance._photonConfigError) {
2659
2706
  throw new Error(mcp.instance._photonConfigError);
2660
2707
  }
2708
+ // Enforce @auth at method level — works across ALL transports (CLI, STDIO, HTTP, daemon)
2709
+ const photonAuth = mcp.auth;
2710
+ if (photonAuth === 'required') {
2711
+ const caller = options?.caller;
2712
+ if (!caller || caller.anonymous) {
2713
+ const inputProvider = options?.inputProvider || this.createInputProvider();
2714
+ try {
2715
+ const token = await inputProvider({
2716
+ ask: 'password',
2717
+ message: `${mcp.name} requires authentication. Enter your access token:`,
2718
+ });
2719
+ if (token && typeof token === 'string') {
2720
+ options = {
2721
+ ...options,
2722
+ caller: {
2723
+ id: token.slice(0, 8),
2724
+ name: 'authenticated-user',
2725
+ anonymous: false,
2726
+ claims: { token },
2727
+ },
2728
+ };
2729
+ }
2730
+ else {
2731
+ throw new Error(`Authentication required: ${mcp.name} has @auth required but no credentials were provided`);
2732
+ }
2733
+ }
2734
+ catch (e) {
2735
+ if (e.message?.includes('Authentication required'))
2736
+ throw e;
2737
+ throw new Error(`Authentication required: ${mcp.name} has @auth required. Provide credentials via Authorization header, MCP elicitation, or CLI prompt.`);
2738
+ }
2739
+ }
2740
+ }
2661
2741
  // Intercept auto-generated settings tool
2662
2742
  if (toolName === 'settings' && mcp.instance._settingsBacking) {
2663
2743
  const result = await this.executeSettingsTool(mcp.instance, parameters, {
@@ -3048,7 +3128,7 @@ Run: photon mcp ${mcpName} --config
3048
3128
  : emit.type === 'success'
3049
3129
  ? '✅'
3050
3130
  : 'ℹ';
3051
- this.logger.info(`${icon} ${emit.message}`);
3131
+ process.stdout.write(`${icon} ${emit.message}\n`);
3052
3132
  break;
3053
3133
  case 'thinking':
3054
3134
  if (emit.active) {
@@ -3115,6 +3195,30 @@ Run: photon mcp ${mcpName} --config
3115
3195
  * }
3116
3196
  * ```
3117
3197
  */
3198
+ _formatCatalogCache = null;
3199
+ injectFormatCatalog(instance) {
3200
+ if (instance.formats)
3201
+ return;
3202
+ // Eagerly load once per loader, cached across all photon instances
3203
+ if (!this._formatCatalogCache) {
3204
+ try {
3205
+ const loaderDir = path.dirname(fileURLToPath(import.meta.url));
3206
+ const renderersPath = path.join(loaderDir, 'auto-ui', 'bridge', 'renderers.js');
3207
+ const esmRequire = createRequire(import.meta.url);
3208
+ this._formatCatalogCache = esmRequire(renderersPath).FORMAT_CATALOG;
3209
+ }
3210
+ catch (e) {
3211
+ this.logger.debug('Failed to load format catalog', { error: e?.message });
3212
+ this._formatCatalogCache = {};
3213
+ }
3214
+ }
3215
+ Object.defineProperty(instance, 'formats', {
3216
+ value: this._formatCatalogCache,
3217
+ configurable: true,
3218
+ enumerable: false,
3219
+ writable: false,
3220
+ });
3221
+ }
3118
3222
  wireReactiveCollections(instance) {
3119
3223
  // Get the emit function if available
3120
3224
  const emit = typeof instance.emit === 'function'
@@ -3163,7 +3267,18 @@ Run: photon mcp ${mcpName} --config
3163
3267
  // Get all public method names from the instance
3164
3268
  // Skip runtime-injected methods (emit, render, push, ask) — these are
3165
3269
  // capability methods injected by the loader, not user-defined tools
3166
- const RUNTIME_METHODS = new Set(['emit', 'render', 'channel', 'ask', 'call']);
3270
+ const RUNTIME_METHODS = new Set([
3271
+ 'emit',
3272
+ 'render',
3273
+ 'channel',
3274
+ 'ask',
3275
+ 'call',
3276
+ 'toast',
3277
+ 'log',
3278
+ 'status',
3279
+ 'progress',
3280
+ 'thinking',
3281
+ ]);
3167
3282
  const proto = Object.getPrototypeOf(instance);
3168
3283
  const methodNames = Object.getOwnPropertyNames(proto).filter((name) => {
3169
3284
  // Skip constructor and private/protected methods
@@ -3188,95 +3303,35 @@ Run: photon mcp ${mcpName} --config
3188
3303
  if (typeof original !== 'function')
3189
3304
  continue;
3190
3305
  instance[methodName] = function (...args) {
3191
- // Extract parameter names and map arguments to them
3192
- const paramNames = PhotonLoader.extractParamNames(original);
3193
- const params = Object.fromEntries(paramNames.map((name, i) => [name, args[i]]));
3194
- // Call the original method
3195
3306
  const result = original.apply(this, args);
3196
- // Attach __meta to returned objects for audit trail
3197
- const resultObj = result;
3198
- if (resultObj &&
3199
- typeof resultObj === 'object' &&
3200
- !Array.isArray(resultObj) &&
3201
- !resultObj.__meta) {
3202
- const timestamp = new Date().toISOString();
3203
- Object.defineProperty(result, '__meta', {
3204
- value: {
3205
- createdAt: timestamp,
3206
- createdBy: methodName,
3207
- modifiedAt: null,
3208
- modifiedBy: null,
3209
- modifications: [],
3210
- },
3211
- enumerable: false,
3212
- writable: true,
3213
- configurable: true,
3214
- });
3215
- }
3216
- // Emit event with complete context
3217
- const eventData = {
3218
- method: methodName,
3219
- params,
3220
- result,
3221
- timestamp: new Date().toISOString(),
3222
- };
3223
- if (this.instanceName) {
3224
- eventData.instance = this.instanceName;
3225
- }
3226
- // Detect array mutations for range-based pagination support (Phase 5)
3227
- // If result is an object from this.items, add index and array metadata
3228
- if (result && typeof result === 'object' && Array.isArray(this.items)) {
3229
- const index = this.items.findIndex((item) => item === result);
3230
- if (index !== -1) {
3231
- eventData.index = index;
3232
- eventData.totalCount = this.items.length;
3233
- // Affected range: just this item
3234
- eventData.affectedRange = {
3235
- start: index,
3236
- end: index + 1,
3237
- };
3307
+ const attachMeta = (obj) => {
3308
+ if (obj && typeof obj === 'object' && !Array.isArray(obj) && !obj.__meta) {
3309
+ Object.defineProperty(obj, '__meta', {
3310
+ value: {
3311
+ createdAt: new Date().toISOString(),
3312
+ createdBy: methodName,
3313
+ modifiedAt: null,
3314
+ modifiedBy: null,
3315
+ modifications: [],
3316
+ },
3317
+ enumerable: false,
3318
+ writable: true,
3319
+ configurable: true,
3320
+ });
3238
3321
  }
3322
+ };
3323
+ // For async methods, attach __meta to the resolved value, not the Promise
3324
+ if (result && typeof result.then === 'function') {
3325
+ return result.then((resolved) => {
3326
+ attachMeta(resolved);
3327
+ return resolved;
3328
+ });
3239
3329
  }
3240
- // NOTE: Don't emit here - the real emission happens at executeTool level
3241
- // (line 2362 via outputHandler). This method wrapper is only called during
3242
- // direct instantiation testing, not in actual MCP execution paths where executeTool
3243
- // is the proper routing point.
3244
- //
3245
- // If we emit here too, we get duplicate messages:
3246
- // 1. This wrapper emits directly: emit(eventData)
3247
- // 2. executeTool emits via outputHandler: outputHandler(eventData)
3248
- // Both route to daemon, causing double notifications.
3330
+ attachMeta(result);
3249
3331
  return result;
3250
3332
  };
3251
3333
  }
3252
3334
  }
3253
- /**
3254
- * Extract parameter names from a function by parsing its signature
3255
- *
3256
- * Examples:
3257
- * - (text, priority = 'medium') => ['text', 'priority']
3258
- * - (id) => ['id']
3259
- * - () => []
3260
- */
3261
- static extractParamNames(fn) {
3262
- const fnStr = fn.toString();
3263
- // Match parameters inside parentheses: ( ... )
3264
- const match = fnStr.match(/\(([^)]*)\)/);
3265
- if (!match?.[1]) {
3266
- return [];
3267
- }
3268
- return match[1]
3269
- .split(',')
3270
- .map((param) => {
3271
- const cleaned = param
3272
- .trim()
3273
- .split('=')[0] // Remove default value
3274
- .split(':')[0] // Remove type annotations
3275
- .trim();
3276
- return cleaned;
3277
- })
3278
- .filter((name) => name && name !== 'this');
3279
- }
3280
3335
  /**
3281
3336
  * Extract @mcp dependencies from source and inject them as instance properties
3282
3337
  *