@portel/photon 1.20.0 → 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 (149) 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/routes/api-config.d.ts.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.js +161 -20
  10. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  11. package/dist/auto-ui/beam.d.ts.map +1 -1
  12. package/dist/auto-ui/beam.js +24 -31
  13. package/dist/auto-ui/beam.js.map +1 -1
  14. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  15. package/dist/auto-ui/bridge/index.js +107 -11
  16. package/dist/auto-ui/bridge/index.js.map +1 -1
  17. package/dist/auto-ui/bridge/renderers.d.ts +14 -0
  18. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  19. package/dist/auto-ui/bridge/renderers.js +692 -61
  20. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  21. package/dist/auto-ui/frontend/index.html +3 -3
  22. package/dist/auto-ui/frontend/pure-view.html +19 -19
  23. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  24. package/dist/auto-ui/streamable-http-transport.js +144 -28
  25. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  26. package/dist/auto-ui/ui-resolver.d.ts +25 -0
  27. package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
  28. package/dist/auto-ui/ui-resolver.js +95 -0
  29. package/dist/auto-ui/ui-resolver.js.map +1 -0
  30. package/dist/beam-form.bundle.js +26 -189
  31. package/dist/beam-form.bundle.js.map +4 -4
  32. package/dist/beam.bundle.js +1646 -494
  33. package/dist/beam.bundle.js.map +4 -4
  34. package/dist/cli/commands/beam.d.ts.map +1 -1
  35. package/dist/cli/commands/beam.js +47 -30
  36. package/dist/cli/commands/beam.js.map +1 -1
  37. package/dist/cli/commands/build.d.ts.map +1 -1
  38. package/dist/cli/commands/build.js +36 -7
  39. package/dist/cli/commands/build.js.map +1 -1
  40. package/dist/cli/commands/daemon.d.ts.map +1 -1
  41. package/dist/cli/commands/daemon.js +12 -6
  42. package/dist/cli/commands/daemon.js.map +1 -1
  43. package/dist/cli/commands/init.d.ts.map +1 -1
  44. package/dist/cli/commands/init.js +90 -50
  45. package/dist/cli/commands/init.js.map +1 -1
  46. package/dist/cli/commands/mcp.d.ts.map +1 -1
  47. package/dist/cli/commands/mcp.js +18 -6
  48. package/dist/cli/commands/mcp.js.map +1 -1
  49. package/dist/cli/commands/publish.d.ts +14 -0
  50. package/dist/cli/commands/publish.d.ts.map +1 -0
  51. package/dist/cli/commands/publish.js +126 -0
  52. package/dist/cli/commands/publish.js.map +1 -0
  53. package/dist/cli/commands/run.d.ts.map +1 -1
  54. package/dist/cli/commands/run.js +2 -0
  55. package/dist/cli/commands/run.js.map +1 -1
  56. package/dist/cli/commands/serve.d.ts.map +1 -1
  57. package/dist/cli/commands/serve.js +14 -2
  58. package/dist/cli/commands/serve.js.map +1 -1
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +3 -0
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli-alias.d.ts.map +1 -1
  63. package/dist/cli-alias.js +2 -3
  64. package/dist/cli-alias.js.map +1 -1
  65. package/dist/context-store.d.ts +4 -4
  66. package/dist/context-store.d.ts.map +1 -1
  67. package/dist/context-store.js +18 -15
  68. package/dist/context-store.js.map +1 -1
  69. package/dist/context.d.ts +31 -2
  70. package/dist/context.d.ts.map +1 -1
  71. package/dist/context.js +86 -9
  72. package/dist/context.js.map +1 -1
  73. package/dist/daemon/client.d.ts +9 -1
  74. package/dist/daemon/client.d.ts.map +1 -1
  75. package/dist/daemon/client.js +58 -2
  76. package/dist/daemon/client.js.map +1 -1
  77. package/dist/daemon/manager.d.ts +5 -0
  78. package/dist/daemon/manager.d.ts.map +1 -1
  79. package/dist/daemon/manager.js +116 -34
  80. package/dist/daemon/manager.js.map +1 -1
  81. package/dist/daemon/ownership.d.ts +12 -0
  82. package/dist/daemon/ownership.d.ts.map +1 -0
  83. package/dist/daemon/ownership.js +55 -0
  84. package/dist/daemon/ownership.js.map +1 -0
  85. package/dist/daemon/protocol.d.ts +3 -1
  86. package/dist/daemon/protocol.d.ts.map +1 -1
  87. package/dist/daemon/protocol.js +14 -2
  88. package/dist/daemon/protocol.js.map +1 -1
  89. package/dist/daemon/server.js +587 -77
  90. package/dist/daemon/server.js.map +1 -1
  91. package/dist/daemon/session-manager.d.ts +9 -1
  92. package/dist/daemon/session-manager.d.ts.map +1 -1
  93. package/dist/daemon/session-manager.js +54 -1
  94. package/dist/daemon/session-manager.js.map +1 -1
  95. package/dist/daemon/worker-host.js +7 -0
  96. package/dist/daemon/worker-host.js.map +1 -1
  97. package/dist/daemon/worker-manager.d.ts +12 -0
  98. package/dist/daemon/worker-manager.d.ts.map +1 -1
  99. package/dist/daemon/worker-manager.js +147 -16
  100. package/dist/daemon/worker-manager.js.map +1 -1
  101. package/dist/daemon/worker-protocol.d.ts +3 -0
  102. package/dist/daemon/worker-protocol.d.ts.map +1 -1
  103. package/dist/deploy/cloudflare.d.ts.map +1 -1
  104. package/dist/deploy/cloudflare.js +2 -4
  105. package/dist/deploy/cloudflare.js.map +1 -1
  106. package/dist/loader.d.ts +10 -9
  107. package/dist/loader.d.ts.map +1 -1
  108. package/dist/loader.js +224 -115
  109. package/dist/loader.js.map +1 -1
  110. package/dist/marketplace-manager.d.ts +1 -1
  111. package/dist/marketplace-manager.d.ts.map +1 -1
  112. package/dist/marketplace-manager.js +5 -4
  113. package/dist/marketplace-manager.js.map +1 -1
  114. package/dist/photon-cli-runner.d.ts.map +1 -1
  115. package/dist/photon-cli-runner.js +66 -23
  116. package/dist/photon-cli-runner.js.map +1 -1
  117. package/dist/photon-doc-extractor.d.ts.map +1 -1
  118. package/dist/photon-doc-extractor.js +59 -15
  119. package/dist/photon-doc-extractor.js.map +1 -1
  120. package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
  121. package/dist/photons/canvas.photon.d.ts +400 -0
  122. package/dist/photons/canvas.photon.d.ts.map +1 -0
  123. package/dist/photons/canvas.photon.js +662 -0
  124. package/dist/photons/canvas.photon.js.map +1 -0
  125. package/dist/photons/canvas.photon.ts +814 -0
  126. package/dist/photons/publish.photon.d.ts +97 -0
  127. package/dist/photons/publish.photon.d.ts.map +1 -0
  128. package/dist/photons/publish.photon.js +569 -0
  129. package/dist/photons/publish.photon.js.map +1 -0
  130. package/dist/photons/publish.photon.ts +683 -0
  131. package/dist/photons/ui/canvas.photon.html +624 -0
  132. package/dist/resource-server.d.ts.map +1 -1
  133. package/dist/resource-server.js +7 -1
  134. package/dist/resource-server.js.map +1 -1
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +14 -16
  137. package/dist/server.js.map +1 -1
  138. package/dist/shared-utils.d.ts +4 -0
  139. package/dist/shared-utils.d.ts.map +1 -1
  140. package/dist/shared-utils.js +24 -2
  141. package/dist/shared-utils.js.map +1 -1
  142. package/dist/template-manager.d.ts.map +1 -1
  143. package/dist/template-manager.js +56 -234
  144. package/dist/template-manager.js.map +1 -1
  145. package/dist/tsx-compiler.d.ts +23 -0
  146. package/dist/tsx-compiler.d.ts.map +1 -0
  147. package/dist/tsx-compiler.js +221 -0
  148. package/dist/tsx-compiler.js.map +1 -0
  149. 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.
@@ -225,6 +257,12 @@ export class PhotonLoader {
225
257
  * without file I/O when running as a standalone binary.
226
258
  */
227
259
  preloadedDependencies;
260
+ /**
261
+ * Optional progress callback — invoked during long-running init phases
262
+ * (dependency install, compilation, onInitialize). Used by worker-host
263
+ * to send keepalive signals so the spawn timeout resets.
264
+ */
265
+ onProgress;
228
266
  constructor(verbose = false, logger, baseDir) {
229
267
  this.dependencyManager = new DependencyManager();
230
268
  this.verbose = verbose;
@@ -332,7 +370,7 @@ export class PhotonLoader {
332
370
  * Directory where MCP-specific dependencies are cached
333
371
  */
334
372
  getDependencyCacheDir(cacheKey) {
335
- return path.join(getCacheDir(), 'dependencies', cacheKey);
373
+ return path.join(getCacheDir(process.env.PHOTON_DIR), 'dependencies', cacheKey);
336
374
  }
337
375
  getBuildCacheDir(cacheKey) {
338
376
  return path.join(this.getDependencyCacheDir(cacheKey), '.build');
@@ -425,6 +463,7 @@ export class PhotonLoader {
425
463
  }
426
464
  let nodeModules = null;
427
465
  if (dependencies.length > 0) {
466
+ this.onProgress?.('installing dependencies');
428
467
  nodeModules = await this.dependencyManager.ensureDependencies(cacheKey, dependencies);
429
468
  if (nodeModules) {
430
469
  this.log(`📦 Dependencies ready for ${mcpName}`, { nodeModules });
@@ -572,6 +611,7 @@ export class PhotonLoader {
572
611
  }
573
612
  const importModule = async () => {
574
613
  if (tsContent) {
614
+ this.onProgress?.('compiling typescript');
575
615
  const cachedJsPath = await this.compileTypeScript(absolutePath, cacheKey, tsContent);
576
616
  const cachedJsUrl = pathToFileURL(cachedJsPath).href;
577
617
  return await import(`${cachedJsUrl}?t=${Date.now()}`);
@@ -642,15 +682,12 @@ export class PhotonLoader {
642
682
  // Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
643
683
  // Developers just `import { Array } from '@portel/photon-core'` and use normally
644
684
  this.wireReactiveCollections(instance);
685
+ // Inject format catalog — this.formats
686
+ this.injectFormatCatalog(instance);
645
687
  // Inject @mcp dependencies from source (this.github, this.fs, etc.)
646
688
  if (tsContent) {
647
689
  await this.injectMCPDependencies(instance, tsContent, name);
648
690
  }
649
- // Auto-wrap public methods in @stateful classes to emit events
650
- // All method calls automatically produce events with params, result, timestamp
651
- if (tsContent) {
652
- this.wrapStatefulMethods(instance, tsContent);
653
- }
654
691
  // Inject MCP client factory if available (enables this.mcp() calls)
655
692
  const setMCPFactory = instance.setMCPFactory;
656
693
  if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
@@ -674,6 +711,8 @@ export class PhotonLoader {
674
711
  // Photon base class already has these built-in, so only inject for plain classes
675
712
  if (tsContent && typeof instance.executeTool !== 'function') {
676
713
  const caps = detectCapabilities(tsContent);
714
+ if (detectEmitHelperUsage(tsContent))
715
+ caps.add('emit');
677
716
  if (caps.size > 0) {
678
717
  this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
679
718
  }
@@ -719,15 +758,8 @@ export class PhotonLoader {
719
758
  });
720
759
  }
721
760
  };
722
- // Also inject render() convenience wrapper around emit
723
- instance.render = (format, value) => {
724
- if (format === undefined) {
725
- instance.emit({ emit: 'render:clear' });
726
- }
727
- else {
728
- instance.emit({ emit: 'render', format, value });
729
- }
730
- };
761
+ // Inject convenience helpers (render, toast, log, status, progress, thinking)
762
+ injectEmitHelpers(instance);
731
763
  }
732
764
  if (caps.has('memory')) {
733
765
  // Inject lazy memory provider — capture baseDir from loader context
@@ -978,6 +1010,7 @@ export class PhotonLoader {
978
1010
  // instance after loadFile returns, then onInitialize is called manually.
979
1011
  const onInitialize = instance.onInitialize;
980
1012
  if (typeof onInitialize === 'function' && !options?.skipInitialize) {
1013
+ this.onProgress?.('running onInitialize');
981
1014
  try {
982
1015
  await onInitialize.call(instance);
983
1016
  }
@@ -992,8 +1025,13 @@ export class PhotonLoader {
992
1025
  throw initError;
993
1026
  }
994
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
+ }
995
1033
  // Extract tools, templates, and statics (with schema override support)
996
- const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath);
1034
+ const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath);
997
1035
  // ═══ SETTINGS INJECTION ═══
998
1036
  // If the photon declared `protected settings = { ... }`, inject persistence + proxy
999
1037
  if (settingsSchema?.hasSettings &&
@@ -1043,6 +1081,8 @@ export class PhotonLoader {
1043
1081
  result.icon = classIcon;
1044
1082
  if (isStateful)
1045
1083
  result.stateful = true;
1084
+ if (extractedAuth)
1085
+ result.auth = extractedAuth;
1046
1086
  // Store class constructor for static method access
1047
1087
  result.classConstructor = MCPClass;
1048
1088
  // Store settings schema for Beam UI
@@ -1114,6 +1154,8 @@ export class PhotonLoader {
1114
1154
  instance._photonResolver = (photonName, instanceName) => {
1115
1155
  return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
1116
1156
  };
1157
+ // Inject format catalog — this.formats
1158
+ this.injectFormatCatalog(instance);
1117
1159
  // Wire reactive collections
1118
1160
  this.wireReactiveCollections(instance);
1119
1161
  // Inject @mcp dependencies
@@ -1143,6 +1185,8 @@ export class PhotonLoader {
1143
1185
  // Detect and inject capabilities for plain classes
1144
1186
  if (tsContent && typeof instance.executeTool !== 'function') {
1145
1187
  const caps = detectCapabilities(tsContent);
1188
+ if (detectEmitHelperUsage(tsContent))
1189
+ caps.add('emit');
1146
1190
  this.injectPathHelpers(instance, tsContent);
1147
1191
  if (caps.has('emit')) {
1148
1192
  instance.emit = (data) => {
@@ -1177,15 +1221,8 @@ export class PhotonLoader {
1177
1221
  });
1178
1222
  }
1179
1223
  };
1180
- // Also inject render() convenience wrapper around emit
1181
- instance.render = (format, value) => {
1182
- if (format === undefined) {
1183
- instance.emit({ emit: 'render:clear' });
1184
- }
1185
- else {
1186
- instance.emit({ emit: 'render', format, value });
1187
- }
1188
- };
1224
+ // Inject convenience helpers (render, toast, log, status, progress, thinking)
1225
+ injectEmitHelpers(instance);
1189
1226
  }
1190
1227
  if (caps.has('memory')) {
1191
1228
  const memoryBaseDir = this.baseDir;
@@ -1276,6 +1313,7 @@ export class PhotonLoader {
1276
1313
  // Call lifecycle hook
1277
1314
  const onInitialize = instance.onInitialize;
1278
1315
  if (typeof onInitialize === 'function' && !options?.skipInitialize) {
1316
+ this.onProgress?.('running onInitialize');
1279
1317
  try {
1280
1318
  await onInitialize.call(instance);
1281
1319
  }
@@ -1286,7 +1324,7 @@ export class PhotonLoader {
1286
1324
  }
1287
1325
  }
1288
1326
  // Extract tools and metadata from embedded source (no disk I/O)
1289
- 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);
1290
1328
  // Settings injection
1291
1329
  if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
1292
1330
  const instanceName = options?.instanceName || 'default';
@@ -1316,6 +1354,8 @@ export class PhotonLoader {
1316
1354
  result.icon = classIcon;
1317
1355
  if (isStateful)
1318
1356
  result.stateful = true;
1357
+ if (extractedAuth)
1358
+ result.auth = extractedAuth;
1319
1359
  result.classConstructor = MCPClass;
1320
1360
  if (settingsSchema?.hasSettings) {
1321
1361
  result.settingsSchema = settingsSchema;
@@ -1331,6 +1371,14 @@ export class PhotonLoader {
1331
1371
  const match = source.match(/\/\*\*([\s\S]*?)\*\/\s*export\s+default\s+class\b/);
1332
1372
  return match ? match[1] : '';
1333
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
+ }
1334
1382
  /**
1335
1383
  * Reload a Photon MCP file (for hot reload)
1336
1384
  */
@@ -1557,7 +1605,13 @@ export class PhotonLoader {
1557
1605
  }
1558
1606
  });
1559
1607
  }
1560
- 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
+ };
1561
1615
  }
1562
1616
  throw jsonError;
1563
1617
  }
@@ -2231,11 +2285,14 @@ export class PhotonLoader {
2231
2285
  return;
2232
2286
  if (path.dirname(realCurrentPhotonPath) !== path.dirname(resolvedPath))
2233
2287
  return;
2234
- 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));
2235
2292
  await this.createSymlinkIfMissing(resolvedPath, linkPath);
2236
2293
  const depName = path.basename(resolvedPath).replace(/\.photon\.(ts|js)$/, '');
2237
2294
  const sourceAssetDir = path.join(path.dirname(resolvedPath), depName);
2238
- const targetAssetDir = path.join(this.baseDir, depName);
2295
+ const targetAssetDir = path.join(symlinkDir, depName);
2239
2296
  if (existsSync(sourceAssetDir)) {
2240
2297
  await this.createSymlinkIfMissing(sourceAssetDir, targetAssetDir, 'dir');
2241
2298
  }
@@ -2648,6 +2705,39 @@ Run: photon mcp ${mcpName} --config
2648
2705
  if (mcp.instance._photonConfigError) {
2649
2706
  throw new Error(mcp.instance._photonConfigError);
2650
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
+ }
2651
2741
  // Intercept auto-generated settings tool
2652
2742
  if (toolName === 'settings' && mcp.instance._settingsBacking) {
2653
2743
  const result = await this.executeSettingsTool(mcp.instance, parameters, {
@@ -2659,7 +2749,10 @@ Run: photon mcp ${mcpName} --config
2659
2749
  return result;
2660
2750
  }
2661
2751
  // Get tool metadata for functional tags
2662
- const toolMeta = mcp.tools.find((t) => t.name === toolName) || {};
2752
+ const rawToolMeta = mcp.tools.find((t) => t.name === toolName) || {};
2753
+ const normalized = this.normalizeNestedParamsTool(rawToolMeta, parameters);
2754
+ const toolMeta = normalized.toolMeta;
2755
+ parameters = normalized.parameters;
2663
2756
  const hasFunctionalTags = toolMeta.middleware?.length > 0;
2664
2757
  // Validate required parameters before execution
2665
2758
  const requiredParams = toolMeta.inputSchema?.required || [];
@@ -2909,6 +3002,47 @@ Run: photon mcp ${mcpName} --config
2909
3002
  span.end();
2910
3003
  }
2911
3004
  }
3005
+ normalizeNestedParamsTool(toolMeta, parameters) {
3006
+ const schema = toolMeta?.inputSchema;
3007
+ if (!schema?.properties || typeof schema.properties !== 'object') {
3008
+ return { toolMeta, parameters };
3009
+ }
3010
+ const propNames = Object.keys(schema.properties);
3011
+ if (propNames.length !== 1 || propNames[0] !== 'params') {
3012
+ return { toolMeta, parameters };
3013
+ }
3014
+ const nested = schema.properties.params;
3015
+ if (!nested ||
3016
+ nested.type !== 'object' ||
3017
+ !nested.properties ||
3018
+ typeof nested.properties !== 'object') {
3019
+ return { toolMeta, parameters };
3020
+ }
3021
+ const normalizedToolMeta = {
3022
+ ...toolMeta,
3023
+ inputSchema: {
3024
+ type: 'object',
3025
+ properties: nested.properties,
3026
+ ...(Array.isArray(nested.required) ? { required: nested.required } : {}),
3027
+ },
3028
+ // Named object params should stay as a single object argument at runtime.
3029
+ simpleParams: false,
3030
+ };
3031
+ if (parameters &&
3032
+ typeof parameters === 'object' &&
3033
+ 'params' in parameters &&
3034
+ parameters.params &&
3035
+ typeof parameters.params === 'object') {
3036
+ return {
3037
+ toolMeta: normalizedToolMeta,
3038
+ parameters: parameters.params,
3039
+ };
3040
+ }
3041
+ return {
3042
+ toolMeta: normalizedToolMeta,
3043
+ parameters,
3044
+ };
3045
+ }
2912
3046
  /**
2913
3047
  * Create an input provider for generator ask yields
2914
3048
  * Supports the new ask/emit pattern from photon-core 1.2.0
@@ -2994,7 +3128,7 @@ Run: photon mcp ${mcpName} --config
2994
3128
  : emit.type === 'success'
2995
3129
  ? '✅'
2996
3130
  : 'ℹ';
2997
- this.logger.info(`${icon} ${emit.message}`);
3131
+ process.stdout.write(`${icon} ${emit.message}\n`);
2998
3132
  break;
2999
3133
  case 'thinking':
3000
3134
  if (emit.active) {
@@ -3061,6 +3195,30 @@ Run: photon mcp ${mcpName} --config
3061
3195
  * }
3062
3196
  * ```
3063
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
+ }
3064
3222
  wireReactiveCollections(instance) {
3065
3223
  // Get the emit function if available
3066
3224
  const emit = typeof instance.emit === 'function'
@@ -3109,7 +3267,18 @@ Run: photon mcp ${mcpName} --config
3109
3267
  // Get all public method names from the instance
3110
3268
  // Skip runtime-injected methods (emit, render, push, ask) — these are
3111
3269
  // capability methods injected by the loader, not user-defined tools
3112
- 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
+ ]);
3113
3282
  const proto = Object.getPrototypeOf(instance);
3114
3283
  const methodNames = Object.getOwnPropertyNames(proto).filter((name) => {
3115
3284
  // Skip constructor and private/protected methods
@@ -3134,95 +3303,35 @@ Run: photon mcp ${mcpName} --config
3134
3303
  if (typeof original !== 'function')
3135
3304
  continue;
3136
3305
  instance[methodName] = function (...args) {
3137
- // Extract parameter names and map arguments to them
3138
- const paramNames = PhotonLoader.extractParamNames(original);
3139
- const params = Object.fromEntries(paramNames.map((name, i) => [name, args[i]]));
3140
- // Call the original method
3141
3306
  const result = original.apply(this, args);
3142
- // Attach __meta to returned objects for audit trail
3143
- const resultObj = result;
3144
- if (resultObj &&
3145
- typeof resultObj === 'object' &&
3146
- !Array.isArray(resultObj) &&
3147
- !resultObj.__meta) {
3148
- const timestamp = new Date().toISOString();
3149
- Object.defineProperty(result, '__meta', {
3150
- value: {
3151
- createdAt: timestamp,
3152
- createdBy: methodName,
3153
- modifiedAt: null,
3154
- modifiedBy: null,
3155
- modifications: [],
3156
- },
3157
- enumerable: false,
3158
- writable: true,
3159
- configurable: true,
3160
- });
3161
- }
3162
- // Emit event with complete context
3163
- const eventData = {
3164
- method: methodName,
3165
- params,
3166
- result,
3167
- timestamp: new Date().toISOString(),
3168
- };
3169
- if (this.instanceName) {
3170
- eventData.instance = this.instanceName;
3171
- }
3172
- // Detect array mutations for range-based pagination support (Phase 5)
3173
- // If result is an object from this.items, add index and array metadata
3174
- if (result && typeof result === 'object' && Array.isArray(this.items)) {
3175
- const index = this.items.findIndex((item) => item === result);
3176
- if (index !== -1) {
3177
- eventData.index = index;
3178
- eventData.totalCount = this.items.length;
3179
- // Affected range: just this item
3180
- eventData.affectedRange = {
3181
- start: index,
3182
- end: index + 1,
3183
- };
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
+ });
3184
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
+ });
3185
3329
  }
3186
- // NOTE: Don't emit here - the real emission happens at executeTool level
3187
- // (line 2362 via outputHandler). This method wrapper is only called during
3188
- // direct instantiation testing, not in actual MCP execution paths where executeTool
3189
- // is the proper routing point.
3190
- //
3191
- // If we emit here too, we get duplicate messages:
3192
- // 1. This wrapper emits directly: emit(eventData)
3193
- // 2. executeTool emits via outputHandler: outputHandler(eventData)
3194
- // Both route to daemon, causing double notifications.
3330
+ attachMeta(result);
3195
3331
  return result;
3196
3332
  };
3197
3333
  }
3198
3334
  }
3199
- /**
3200
- * Extract parameter names from a function by parsing its signature
3201
- *
3202
- * Examples:
3203
- * - (text, priority = 'medium') => ['text', 'priority']
3204
- * - (id) => ['id']
3205
- * - () => []
3206
- */
3207
- static extractParamNames(fn) {
3208
- const fnStr = fn.toString();
3209
- // Match parameters inside parentheses: ( ... )
3210
- const match = fnStr.match(/\(([^)]*)\)/);
3211
- if (!match?.[1]) {
3212
- return [];
3213
- }
3214
- return match[1]
3215
- .split(',')
3216
- .map((param) => {
3217
- const cleaned = param
3218
- .trim()
3219
- .split('=')[0] // Remove default value
3220
- .split(':')[0] // Remove type annotations
3221
- .trim();
3222
- return cleaned;
3223
- })
3224
- .filter((name) => name && name !== 'this');
3225
- }
3226
3335
  /**
3227
3336
  * Extract @mcp dependencies from source and inject them as instance properties
3228
3337
  *