@portel/photon 1.34.1 → 1.35.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 (111) hide show
  1. package/README.md +1 -0
  2. package/dist/a2a/card-generator.js +1 -1
  3. package/dist/auto-ui/beam/external-mcp.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.js +93 -16
  6. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  8. package/dist/auto-ui/beam/routes/api-config.js +43 -15
  9. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  10. package/dist/auto-ui/beam/routes/api-marketplace.js +4 -4
  11. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  12. package/dist/auto-ui/beam/subscription.d.ts.map +1 -1
  13. package/dist/auto-ui/beam/subscription.js +4 -1
  14. package/dist/auto-ui/beam/subscription.js.map +1 -1
  15. package/dist/auto-ui/beam.d.ts +2 -0
  16. package/dist/auto-ui/beam.d.ts.map +1 -1
  17. package/dist/auto-ui/beam.js +73 -3
  18. package/dist/auto-ui/beam.js.map +1 -1
  19. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  20. package/dist/auto-ui/bridge/renderers.js +44 -1
  21. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  22. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  23. package/dist/auto-ui/streamable-http-transport.js +5 -3
  24. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  25. package/dist/beam.bundle.js +81 -83
  26. package/dist/beam.bundle.js.map +3 -3
  27. package/dist/cli/commands/info.d.ts.map +1 -1
  28. package/dist/cli/commands/info.js +3 -1
  29. package/dist/cli/commands/info.js.map +1 -1
  30. package/dist/cli/commands/ps.d.ts +2 -4
  31. package/dist/cli/commands/ps.d.ts.map +1 -1
  32. package/dist/cli/commands/ps.js +5 -11
  33. package/dist/cli/commands/ps.js.map +1 -1
  34. package/dist/cli-alias.js +1 -0
  35. package/dist/cli-alias.js.map +1 -1
  36. package/dist/daemon/client.d.ts.map +1 -1
  37. package/dist/daemon/client.js +31 -19
  38. package/dist/daemon/client.js.map +1 -1
  39. package/dist/daemon/manager.d.ts.map +1 -1
  40. package/dist/daemon/manager.js +9 -0
  41. package/dist/daemon/manager.js.map +1 -1
  42. package/dist/daemon/registry-keys.d.ts +6 -0
  43. package/dist/daemon/registry-keys.d.ts.map +1 -1
  44. package/dist/daemon/registry-keys.js +9 -0
  45. package/dist/daemon/registry-keys.js.map +1 -1
  46. package/dist/daemon/server.js +158 -32
  47. package/dist/daemon/server.js.map +1 -1
  48. package/dist/deploy/cloudflare.js.map +1 -1
  49. package/dist/formats/format-registry.d.ts +32 -0
  50. package/dist/formats/format-registry.d.ts.map +1 -0
  51. package/dist/formats/format-registry.js +67 -0
  52. package/dist/formats/format-registry.js.map +1 -0
  53. package/dist/loader.d.ts +6 -0
  54. package/dist/loader.d.ts.map +1 -1
  55. package/dist/loader.js +121 -30
  56. package/dist/loader.js.map +1 -1
  57. package/dist/marketplace-manager.js +1 -1
  58. package/dist/marketplace-manager.js.map +1 -1
  59. package/dist/photon-cli-runner.d.ts.map +1 -1
  60. package/dist/photon-cli-runner.js +1 -0
  61. package/dist/photon-cli-runner.js.map +1 -1
  62. package/dist/serv/auth/http-adapter.d.ts.map +1 -1
  63. package/dist/serv/auth/http-adapter.js +7 -2
  64. package/dist/serv/auth/http-adapter.js.map +1 -1
  65. package/dist/serv/middleware/tenant.d.ts.map +1 -1
  66. package/dist/serv/middleware/tenant.js +1 -0
  67. package/dist/serv/middleware/tenant.js.map +1 -1
  68. package/dist/server.d.ts +10 -0
  69. package/dist/server.d.ts.map +1 -1
  70. package/dist/server.js +173 -155
  71. package/dist/server.js.map +1 -1
  72. package/dist/settings-persistence.d.ts +10 -0
  73. package/dist/settings-persistence.d.ts.map +1 -1
  74. package/dist/settings-persistence.js +25 -4
  75. package/dist/settings-persistence.js.map +1 -1
  76. package/dist/shared/audit.d.ts.map +1 -1
  77. package/dist/shared/audit.js +2 -1
  78. package/dist/shared/audit.js.map +1 -1
  79. package/dist/shared/identity.d.ts +50 -0
  80. package/dist/shared/identity.d.ts.map +1 -0
  81. package/dist/shared/identity.js +61 -0
  82. package/dist/shared/identity.js.map +1 -0
  83. package/dist/shared/memory-sqlite.d.ts.map +1 -1
  84. package/dist/shared/memory-sqlite.js +1 -0
  85. package/dist/shared/memory-sqlite.js.map +1 -1
  86. package/dist/shared/security.d.ts.map +1 -1
  87. package/dist/shared/security.js +7 -1
  88. package/dist/shared/security.js.map +1 -1
  89. package/dist/shared-utils.d.ts.map +1 -1
  90. package/dist/shared-utils.js +2 -2
  91. package/dist/shared-utils.js.map +1 -1
  92. package/dist/tasks/store.d.ts.map +1 -1
  93. package/dist/tasks/store.js +2 -1
  94. package/dist/tasks/store.js.map +1 -1
  95. package/dist/telemetry/context.d.ts.map +1 -1
  96. package/dist/telemetry/context.js +8 -2
  97. package/dist/telemetry/context.js.map +1 -1
  98. package/dist/types/server-types.d.ts +12 -0
  99. package/dist/types/server-types.d.ts.map +1 -1
  100. package/dist/version-notify.d.ts.map +1 -1
  101. package/dist/version-notify.js +2 -1
  102. package/dist/version-notify.js.map +1 -1
  103. package/package.json +4 -3
  104. package/templates/cloudflare/worker.ts.template +182 -4
  105. package/dist/photons/canvas/ui/canvas.photon.html +0 -1493
  106. package/dist/photons/canvas.photon.d.ts +0 -400
  107. package/dist/photons/canvas.photon.d.ts.map +0 -1
  108. package/dist/photons/canvas.photon.js +0 -662
  109. package/dist/photons/canvas.photon.js.map +0 -1
  110. package/dist/photons/canvas.photon.ts +0 -814
  111. package/dist/photons/ui/canvas.photon.html +0 -624
package/dist/loader.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import * as fs from 'fs/promises';
7
7
  import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync, statSync, } from 'fs';
8
8
  import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
9
+ import { qualifyChannel, circuitKey, preloadedCacheKey, instanceCacheKey, } from './shared/identity.js';
9
10
  import { parseCfBindings } from './cf-bindings-parser.js';
10
11
  import { CFLocalRuntime, mergeBindings } from './runtime/cf-local.js';
11
12
  import { scanCfUsage } from './cf-usage-scanner.js';
@@ -43,7 +44,7 @@ executionContext,
43
44
  // Lock helper
44
45
  withLock as withLockHelper,
45
46
  // Middleware system
46
- builtinRegistry, MiddlewareRegistry, buildMiddlewareChain, getCacheDir, } from '@portel/photon-core';
47
+ builtinRegistry, MiddlewareRegistry, buildMiddlewareChain, getCacheDir, getPhotonDataDir, } from '@portel/photon-core';
47
48
  import { getDefaultContext } from './context.js';
48
49
  import { getInstanceStatePath } from './context-store.js';
49
50
  import { ContextRegistry, RepeatDetector, assembleSampleParams, } from './sample-augmenter.js';
@@ -235,10 +236,14 @@ function injectEmitHelpers(instance) {
235
236
  instance.log = (message, opts = {}) => emit({ emit: 'log', message, level: opts.level ?? 'info', data: opts.data });
236
237
  }
237
238
  if (!('status' in instance)) {
238
- instance.status = (message) => emit({ emit: 'status', message });
239
+ instance.status = (message, value) => emit(typeof message === 'string'
240
+ ? { emit: 'status', message, ...(value !== undefined && { value }) }
241
+ : { emit: 'status', ...message });
239
242
  }
240
243
  if (!('progress' in instance)) {
241
- instance.progress = (value, message) => emit({ emit: 'progress', value, message });
244
+ instance.progress = (value, message, meta) => emit(typeof value === 'number'
245
+ ? { emit: 'progress', value, ...(message && { message }), ...(meta && { meta }) }
246
+ : { emit: 'progress', ...value });
242
247
  }
243
248
  if (!('thinking' in instance)) {
244
249
  instance.thinking = (active = true) => emit({ emit: 'thinking', active });
@@ -389,6 +394,28 @@ function injectResourceNotifier(instance, notifier) {
389
394
  }
390
395
  };
391
396
  }
397
+ /**
398
+ * Formats @portel/cli's formatOutput can render directly. Anything else
399
+ * (gauge, stack, chart:*, ...) would hit formatDataWithHint's switch with
400
+ * no matching case and silently drop the data — fall back to auto-detect
401
+ * instead so the value always stays visible.
402
+ */
403
+ const CLI_BASE_FORMATS = new Set([
404
+ 'primitive',
405
+ 'list',
406
+ 'table',
407
+ 'tree',
408
+ 'card',
409
+ 'tabs',
410
+ 'accordion',
411
+ 'none',
412
+ 'json',
413
+ 'markdown',
414
+ 'yaml',
415
+ 'xml',
416
+ 'html',
417
+ 'code',
418
+ ]);
392
419
  /**
393
420
  * Render a formatted value in the CLI using @portel/cli's formatOutput.
394
421
  * Uses clear-and-replace semantics — each call overwrites the previous render.
@@ -406,9 +433,10 @@ function renderCLIFormat(format, value) {
406
433
  }
407
434
  return;
408
435
  }
436
+ const safeFormat = CLI_BASE_FORMATS.has(format) || format.startsWith('code:') ? format : undefined;
409
437
  // Use the eagerly-imported formatter — synchronous, so line counting works
410
438
  cliRenderZone.render(() => {
411
- cliFormatOutput(value, format);
439
+ cliFormatOutput(value, safeFormat);
412
440
  });
413
441
  }
414
442
  catch {
@@ -1234,7 +1262,7 @@ export class PhotonLoader {
1234
1262
  if (caps.size > 0) {
1235
1263
  this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
1236
1264
  }
1237
- this.injectPathHelpers(instance, tsContent);
1265
+ this.injectPathHelpers(instance);
1238
1266
  // Always-inject emit + its helpers (render/toast/log/status/progress/
1239
1267
  // thinking). The injection is a pure closure — zero cost if unused.
1240
1268
  // Dropping the capability-detection gate means typed-access patterns
@@ -1254,7 +1282,7 @@ export class PhotonLoader {
1254
1282
  // Also publish to channel broker if channel is specified
1255
1283
  if (data && typeof data.channel === 'string') {
1256
1284
  // Auto-prefix channel with photon name if not already namespaced
1257
- const channel = data.channel.includes(':') ? data.channel : `${name}:${data.channel}`;
1285
+ const channel = qualifyChannel(name, data.channel);
1258
1286
  import('@portel/photon-core')
1259
1287
  .then(({ getBroker }) => {
1260
1288
  const broker = getBroker();
@@ -1862,10 +1890,7 @@ export class PhotonLoader {
1862
1890
  }
1863
1891
  // Detect and inject capabilities for plain classes
1864
1892
  if (tsContent && typeof instance.executeTool !== 'function') {
1865
- const caps = detectCapabilities(tsContent);
1866
- if (detectEmitHelperUsage(tsContent))
1867
- caps.add('emit');
1868
- this.injectPathHelpers(instance, tsContent);
1893
+ this.injectPathHelpers(instance);
1869
1894
  // Always-inject emit (see primary path). User-defined wins.
1870
1895
  if (!instance.emit) {
1871
1896
  instance.emit = (data) => {
@@ -1878,7 +1903,7 @@ export class PhotonLoader {
1878
1903
  if (handler)
1879
1904
  handler(emitData);
1880
1905
  if (data && typeof data.channel === 'string') {
1881
- const channel = data.channel.includes(':') ? data.channel : `${name}:${data.channel}`;
1906
+ const channel = qualifyChannel(name, data.channel);
1882
1907
  import('@portel/photon-core')
1883
1908
  .then(({ getBroker }) => {
1884
1909
  getBroker()
@@ -2129,12 +2154,28 @@ export class PhotonLoader {
2129
2154
  return explicitScopes;
2130
2155
  return [`${tool.name}:${tool.readOnlyHint ? 'read' : 'write'}`];
2131
2156
  }
2157
+ /**
2158
+ * Drop circuit-breaker state for every instance/tool of a photon.
2159
+ * Called on hot reload so the reloaded code starts with closed circuits
2160
+ * and long-running daemons don't accumulate entries for retired versions.
2161
+ */
2162
+ clearCircuitState(photonName) {
2163
+ const prefix = `${photonName}:`;
2164
+ for (const key of this.circuitHealthTracker.keys()) {
2165
+ if (key.startsWith(prefix))
2166
+ this.circuitHealthTracker.delete(key);
2167
+ }
2168
+ }
2132
2169
  /**
2133
2170
  * Reload a Photon MCP file (for hot reload)
2134
2171
  */
2135
2172
  async reloadFile(filePath, options) {
2136
2173
  // Invalidate the cache for this file
2137
2174
  const absolutePath = path.resolve(filePath);
2175
+ this.clearCircuitState(path
2176
+ .basename(absolutePath)
2177
+ .replace(/\.photon\.(ts|js|mjs)$/, '')
2178
+ .replace(/\.(ts|js|mjs)$/, ''));
2138
2179
  if (absolutePath.endsWith('.ts')) {
2139
2180
  // Clear the compiled cache
2140
2181
  const tsContent = await readText(absolutePath);
@@ -2807,9 +2848,7 @@ export class PhotonLoader {
2807
2848
  this.log(` ⚠️ Skipping stub dependency: ${dep.name} (module failed to load)`);
2808
2849
  return undefined;
2809
2850
  }
2810
- const cacheKey = dep.instanceName
2811
- ? `preloaded:${dep.name}::${dep.instanceName}`
2812
- : `preloaded:${dep.name}`;
2851
+ const cacheKey = preloadedCacheKey(dep.name, dep.instanceName);
2813
2852
  if (this.loadedPhotons.has(cacheKey)) {
2814
2853
  return this.loadedPhotons.get(cacheKey).instance;
2815
2854
  }
@@ -2823,7 +2862,7 @@ export class PhotonLoader {
2823
2862
  const resolvedPath = await this.resolvePhotonPath(dep, currentPhotonPath);
2824
2863
  await this.materializeSiblingDependencySymlink(dep, currentPhotonPath, resolvedPath);
2825
2864
  // Cache key includes instance name to allow multiple instances of the same photon
2826
- const cacheKey = dep.instanceName ? `${resolvedPath}::${dep.instanceName}` : resolvedPath;
2865
+ const cacheKey = instanceCacheKey(resolvedPath, dep.instanceName);
2827
2866
  // Check cache
2828
2867
  if (this.loadedPhotons.has(cacheKey)) {
2829
2868
  return this.loadedPhotons.get(cacheKey).instance;
@@ -3368,7 +3407,7 @@ Run: photon mcp ${mcpName} --config
3368
3407
  handlerOverrides.set('circuitBreaker', (config, _state) => {
3369
3408
  const tracker = this.circuitHealthTracker;
3370
3409
  return async (ctx, next) => {
3371
- const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
3410
+ const key = circuitKey(ctx.photon, ctx.instance, ctx.tool);
3372
3411
  let circuit = tracker.get(key);
3373
3412
  if (!circuit) {
3374
3413
  circuit = { failures: 0, state: 'closed', openedAt: 0 };
@@ -3399,7 +3438,7 @@ Run: photon mcp ${mcpName} --config
3399
3438
  channel: `${ctx.photon}:circuits`,
3400
3439
  event: 'state-change',
3401
3440
  data: {
3402
- key: `${ctx.photon}:${ctx.instance}:${ctx.tool}`,
3441
+ key: circuitKey(ctx.photon, ctx.instance, ctx.tool),
3403
3442
  from,
3404
3443
  to,
3405
3444
  timestamp: Date.now(),
@@ -4030,6 +4069,7 @@ Run: photon mcp ${mcpName} --config
4030
4069
  */
4031
4070
  createInputProvider() {
4032
4071
  return async (ask) => {
4072
+ this.progressRenderer.done();
4033
4073
  switch (ask.ask) {
4034
4074
  case 'text':
4035
4075
  case 'password':
@@ -4037,9 +4077,52 @@ Run: photon mcp ${mcpName} --config
4037
4077
  case 'confirm':
4038
4078
  return await elicitConfirm(ask.message);
4039
4079
  case 'select': {
4040
- const options = (ask.options || []).map((o) => typeof o === 'string' ? o : o.label);
4041
- const result = await elicitPrompt(`${ask.message}\nOptions: ${options.join(', ')}`);
4042
- return result;
4080
+ const options = (ask.options || []).map((o) => {
4081
+ if (typeof o === 'string')
4082
+ return { value: o, label: o };
4083
+ return { value: o.value ?? o.label, label: o.label };
4084
+ });
4085
+ // Print choices clearly as a numbered list
4086
+ let msg = `${ask.message}\n`;
4087
+ options.forEach((opt, idx) => {
4088
+ msg += ` [${idx + 1}] ${opt.label}\n`;
4089
+ });
4090
+ msg += ask.multi
4091
+ ? `Select option(s) (comma-separated numbers or names): `
4092
+ : `Select an option (number or name): `;
4093
+ const result = await elicitPrompt(msg);
4094
+ if (!result)
4095
+ return ask.multi ? [] : null;
4096
+ // Parse and match the user selection
4097
+ if (ask.multi) {
4098
+ const parts = result.split(',').map((s) => s.trim().toLowerCase());
4099
+ const selected = [];
4100
+ for (const part of parts) {
4101
+ const idx = parseInt(part, 10) - 1;
4102
+ if (idx >= 0 && idx < options.length) {
4103
+ selected.push(options[idx].value);
4104
+ }
4105
+ else {
4106
+ const match = options.find((o) => o.label.toLowerCase() === part ||
4107
+ o.value.toString().toLowerCase() === part ||
4108
+ o.label.toLowerCase().includes(part));
4109
+ if (match)
4110
+ selected.push(match.value);
4111
+ }
4112
+ }
4113
+ return selected;
4114
+ }
4115
+ else {
4116
+ const part = result.trim().toLowerCase();
4117
+ const idx = parseInt(part, 10) - 1;
4118
+ if (idx >= 0 && idx < options.length) {
4119
+ return options[idx].value;
4120
+ }
4121
+ const match = options.find((o) => o.label.toLowerCase() === part ||
4122
+ o.value.toString().toLowerCase() === part ||
4123
+ o.label.toLowerCase().includes(part));
4124
+ return match ? match.value : result;
4125
+ }
4043
4126
  }
4044
4127
  case 'number': {
4045
4128
  const result = await elicitPrompt(ask.message, ask.default?.toString());
@@ -4483,24 +4566,32 @@ Run: photon mcp ${mcpName} --config
4483
4566
  /**
4484
4567
  * Inject Photon path helpers for plain classes that use them without extending Photon.
4485
4568
  */
4486
- injectPathHelpers(instance, source) {
4487
- if (!source) {
4488
- return;
4489
- }
4490
- if (/this\.storage\s*\(/.test(source) && typeof instance.storage !== 'function') {
4569
+ // Always inject; never gate on source regexes. Regex detection misses
4570
+ // TS-cast call shapes like `(this as any).storage(...)` and silently
4571
+ // leaves the helper undefined — the same blind spot that caused the
4572
+ // memory-injection data-loss bug. Every helper is a lazy closure (zero
4573
+ // cost unless called) and user-defined methods always win.
4574
+ injectPathHelpers(instance) {
4575
+ if (typeof instance.storage !== 'function') {
4576
+ // Mirror Photon.storage() in photon-core base.ts exactly: documented
4577
+ // contract is the data dir (getPhotonDataDir), never next to source.
4578
+ // The previous source-adjacent resolution was copied from assets()
4579
+ // (where source-adjacent is correct) and contradicted every doc.
4580
+ const storageBaseDir = this.baseDir;
4491
4581
  instance.storage = (subpath) => {
4492
4582
  if (!instance._photonFilePath || typeof instance._photonFilePath !== 'string') {
4493
4583
  throw new Error('storage() requires _photonFilePath to be set by the runtime loader. ' +
4494
4584
  'Ensure this photon is loaded through the standard runtime.');
4495
4585
  }
4496
- const dir = path.dirname(instance._photonFilePath);
4497
- const name = path.basename(instance._photonFilePath).replace(/\.photon\.(ts|js)$/, '');
4498
- const target = path.join(dir, name, subpath);
4586
+ const name = (typeof instance._photonName === 'string' && instance._photonName) ||
4587
+ path.basename(instance._photonFilePath).replace(/\.photon\.(ts|js)$/, '');
4588
+ const ns = (typeof instance._photonNamespace === 'string' && instance._photonNamespace) || 'local';
4589
+ const target = path.join(getPhotonDataDir(ns, name, storageBaseDir), subpath);
4499
4590
  mkdirSync(target, { recursive: true });
4500
4591
  return target;
4501
4592
  };
4502
4593
  }
4503
- if (/this\.assets\s*\(/.test(source) && typeof instance.assets !== 'function') {
4594
+ if (typeof instance.assets !== 'function') {
4504
4595
  instance.assets = (subpath, options) => {
4505
4596
  const normalized = typeof options === 'boolean' ? { load: options } : (options ?? {});
4506
4597
  if (!instance._photonFilePath || typeof instance._photonFilePath !== 'string') {
@@ -4523,7 +4614,7 @@ Run: photon mcp ${mcpName} --config
4523
4614
  return readFileSync(assetPath, normalized.encoding ?? 'utf-8');
4524
4615
  };
4525
4616
  }
4526
- if (/this\.assetUrl\s*\(/.test(source) && typeof instance.assetUrl !== 'function') {
4617
+ if (typeof instance.assetUrl !== 'function') {
4527
4618
  instance.assetUrl = (subpath) => {
4528
4619
  const name = (typeof instance._photonName === 'string' && instance._photonName
4529
4620
  ? instance._photonName