@portel/photon 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/dist/a2ui/mapper.d.ts +40 -0
- package/dist/a2ui/mapper.d.ts.map +1 -0
- package/dist/a2ui/mapper.js +286 -0
- package/dist/a2ui/mapper.js.map +1 -0
- package/dist/a2ui/types.d.ts +129 -0
- package/dist/a2ui/types.d.ts.map +1 -0
- package/dist/a2ui/types.js +20 -0
- package/dist/a2ui/types.js.map +1 -0
- package/dist/ag-ui/adapter.d.ts +9 -1
- package/dist/ag-ui/adapter.d.ts.map +1 -1
- package/dist/ag-ui/adapter.js +33 -16
- package/dist/ag-ui/adapter.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-daemon.d.ts +18 -0
- package/dist/auto-ui/beam/routes/api-daemon.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-daemon.js +118 -0
- package/dist/auto-ui/beam/routes/api-daemon.js.map +1 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +34 -34
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +371 -0
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +38 -1
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +19 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +757 -107
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +2 -0
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +92 -3
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/host.d.ts.map +1 -1
- package/dist/cli/commands/host.js +9 -1
- package/dist/cli/commands/host.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +7 -3
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/maker.d.ts +8 -0
- package/dist/cli/commands/maker.d.ts.map +1 -1
- package/dist/cli/commands/maker.js +113 -46
- package/dist/cli/commands/maker.js.map +1 -1
- package/dist/cli/commands/marketplace.d.ts.map +1 -1
- package/dist/cli/commands/marketplace.js +7 -1
- package/dist/cli/commands/marketplace.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +10 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +215 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +33 -15
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli/commands/ps.d.ts +16 -0
- package/dist/cli/commands/ps.d.ts.map +1 -0
- package/dist/cli/commands/ps.js +267 -0
- package/dist/cli/commands/ps.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +7 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +14 -4
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +9 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/context-store.d.ts +4 -4
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +20 -17
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +5 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +68 -14
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts +60 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +76 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/execution-history-sqlite.d.ts +50 -0
- package/dist/daemon/execution-history-sqlite.d.ts.map +1 -0
- package/dist/daemon/execution-history-sqlite.js +165 -0
- package/dist/daemon/execution-history-sqlite.js.map +1 -0
- package/dist/daemon/execution-history.d.ts +78 -0
- package/dist/daemon/execution-history.d.ts.map +1 -0
- package/dist/daemon/execution-history.js +246 -0
- package/dist/daemon/execution-history.js.map +1 -0
- package/dist/daemon/hot-reload-state.d.ts +27 -0
- package/dist/daemon/hot-reload-state.d.ts.map +1 -0
- package/dist/daemon/hot-reload-state.js +48 -0
- package/dist/daemon/hot-reload-state.js.map +1 -0
- package/dist/daemon/protocol.d.ts +5 -1
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +13 -0
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/registry-keys.d.ts +88 -0
- package/dist/daemon/registry-keys.d.ts.map +1 -0
- package/dist/daemon/registry-keys.js +91 -0
- package/dist/daemon/registry-keys.js.map +1 -0
- package/dist/daemon/server.js +1521 -186
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-resolver.d.ts +28 -0
- package/dist/daemon/session-resolver.d.ts.map +1 -0
- package/dist/daemon/session-resolver.js +41 -0
- package/dist/daemon/session-resolver.js.map +1 -0
- package/dist/data-migration.js +20 -9
- package/dist/data-migration.js.map +1 -1
- package/dist/loader.d.ts +22 -8
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +214 -94
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +9 -5
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/namespace-migration.d.ts.map +1 -1
- package/dist/namespace-migration.js +28 -23
- package/dist/namespace-migration.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +57 -8
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/serv/auth/auth-store.d.ts +155 -0
- package/dist/serv/auth/auth-store.d.ts.map +1 -0
- package/dist/serv/auth/auth-store.js +240 -0
- package/dist/serv/auth/auth-store.js.map +1 -0
- package/dist/serv/auth/endpoints.d.ts +113 -0
- package/dist/serv/auth/endpoints.d.ts.map +1 -0
- package/dist/serv/auth/endpoints.js +1005 -0
- package/dist/serv/auth/endpoints.js.map +1 -0
- package/dist/serv/auth/http-adapter.d.ts +60 -0
- package/dist/serv/auth/http-adapter.d.ts.map +1 -0
- package/dist/serv/auth/http-adapter.js +235 -0
- package/dist/serv/auth/http-adapter.js.map +1 -0
- package/dist/serv/auth/jwt.d.ts +92 -6
- package/dist/serv/auth/jwt.d.ts.map +1 -1
- package/dist/serv/auth/jwt.js +226 -24
- package/dist/serv/auth/jwt.js.map +1 -1
- package/dist/serv/auth/oauth-sqlite-stores.d.ts +48 -0
- package/dist/serv/auth/oauth-sqlite-stores.d.ts.map +1 -0
- package/dist/serv/auth/oauth-sqlite-stores.js +212 -0
- package/dist/serv/auth/oauth-sqlite-stores.js.map +1 -0
- package/dist/serv/auth/sqlite-stores.d.ts +85 -0
- package/dist/serv/auth/sqlite-stores.d.ts.map +1 -0
- package/dist/serv/auth/sqlite-stores.js +446 -0
- package/dist/serv/auth/sqlite-stores.js.map +1 -0
- package/dist/serv/auth/well-known.d.ts +54 -1
- package/dist/serv/auth/well-known.d.ts.map +1 -1
- package/dist/serv/auth/well-known.js +166 -17
- package/dist/serv/auth/well-known.js.map +1 -1
- package/dist/serv/index.d.ts +45 -2
- package/dist/serv/index.d.ts.map +1 -1
- package/dist/serv/index.js +65 -1
- package/dist/serv/index.js.map +1 -1
- package/dist/serv/types/index.d.ts +80 -0
- package/dist/serv/types/index.d.ts.map +1 -1
- package/dist/serv/types/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +61 -6
- package/dist/server.js.map +1 -1
- package/dist/shared/announce-context.d.ts +51 -0
- package/dist/shared/announce-context.d.ts.map +1 -0
- package/dist/shared/announce-context.js +73 -0
- package/dist/shared/announce-context.js.map +1 -0
- package/dist/shared/audit-sqlite.d.ts +63 -0
- package/dist/shared/audit-sqlite.d.ts.map +1 -0
- package/dist/shared/audit-sqlite.js +187 -0
- package/dist/shared/audit-sqlite.js.map +1 -0
- package/dist/shared/audit.d.ts +25 -3
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/audit.js +97 -3
- package/dist/shared/audit.js.map +1 -1
- package/dist/shared/error-handler.d.ts +10 -1
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +17 -2
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/security.d.ts +12 -0
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +80 -0
- package/dist/shared/security.js.map +1 -1
- package/dist/shared/sqlite-runtime.d.ts +46 -0
- package/dist/shared/sqlite-runtime.d.ts.map +1 -0
- package/dist/shared/sqlite-runtime.js +110 -0
- package/dist/shared/sqlite-runtime.js.map +1 -0
- package/dist/tasks/store.d.ts +1 -1
- package/dist/tasks/store.d.ts.map +1 -1
- package/dist/tasks/store.js +29 -15
- package/dist/tasks/store.js.map +1 -1
- package/dist/telemetry/metrics.d.ts +26 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +31 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +3 -3
- package/dist/test-runner.js.map +1 -1
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +7 -14
- package/dist/version-checker.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +103 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -2
- package/templates/photon.template.ts +7 -13
package/dist/loader.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Loads a single .photon.ts file and extracts tools
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
|
-
import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync } from 'fs';
|
|
7
|
+
import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync, statSync, } from 'fs';
|
|
8
8
|
import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import * as path from 'path';
|
|
@@ -13,7 +13,7 @@ import * as crypto from 'crypto';
|
|
|
13
13
|
import { startToolSpan } from './telemetry/otel.js';
|
|
14
14
|
import { recordToolCall, recordCircuitStateChange, recordRateLimitRejection, recordBulkheadRejection, } from './telemetry/metrics.js';
|
|
15
15
|
import { runWithRequestContext } from './telemetry/context.js';
|
|
16
|
-
import { spawn } from 'child_process';
|
|
16
|
+
import { spawn, execSync } from 'child_process';
|
|
17
17
|
import { SchemaExtractor, DependencyManager,
|
|
18
18
|
// Generator utilities (ask/emit pattern from 1.2.0)
|
|
19
19
|
isAsyncGenerator, executeGenerator,
|
|
@@ -38,9 +38,9 @@ executionContext,
|
|
|
38
38
|
// Lock helper
|
|
39
39
|
withLock as withLockHelper,
|
|
40
40
|
// Middleware system
|
|
41
|
-
builtinRegistry, MiddlewareRegistry, buildMiddlewareChain,
|
|
41
|
+
builtinRegistry, MiddlewareRegistry, buildMiddlewareChain, getCacheDir, } from '@portel/photon-core';
|
|
42
42
|
import { getDefaultContext } from './context.js';
|
|
43
|
-
import
|
|
43
|
+
import { getInstanceStatePath } from './context-store.js';
|
|
44
44
|
import { MarketplaceManager } from './marketplace-manager.js';
|
|
45
45
|
import { PHOTON_VERSION, getResolvedPhotonCoreVersion } from './version.js';
|
|
46
46
|
// Timeout for external fetch requests (marketplace, GitHub)
|
|
@@ -686,10 +686,75 @@ export class PhotonLoader {
|
|
|
686
686
|
// Set photon name and namespace for event source identification and data paths
|
|
687
687
|
instance._photonName = name;
|
|
688
688
|
instance._photonNamespace = this.resolveNamespace(absolutePath);
|
|
689
|
+
// Pin baseDir so this.memory (and any other .data/-rooted state)
|
|
690
|
+
// resolves to the same root no matter which process reads back.
|
|
691
|
+
// Without this, MemoryProvider falls through to getDefaultContext()
|
|
692
|
+
// and drifts between cwd-derived locations across daemon restarts.
|
|
693
|
+
instance._baseDir = this.baseDir;
|
|
689
694
|
// Inject instance name for named instances (runtime concept, not code)
|
|
690
695
|
instance.instanceName = options?.instanceName ?? '';
|
|
691
696
|
// Inject file path for storage()/assets() resolution
|
|
692
697
|
instance._photonFilePath = absolutePath;
|
|
698
|
+
// Inject this.shell — execSync wrapper with cwd defaulted to the
|
|
699
|
+
// photon's own folder. Shelling out from a photon method should run
|
|
700
|
+
// from where the photon lives, not the daemon's cwd, so that
|
|
701
|
+
// `photon cli peer method` inherits the same marketplace context the
|
|
702
|
+
// user would see when running the command from that folder directly.
|
|
703
|
+
// User-defined shell() on the class wins (no clobber).
|
|
704
|
+
if (typeof instance.shell !== 'function') {
|
|
705
|
+
const photonDir = path.dirname(absolutePath);
|
|
706
|
+
const loaderLogger = this.logger;
|
|
707
|
+
instance.shell = (cmd, timeoutMs = 30000) => {
|
|
708
|
+
try {
|
|
709
|
+
return execSync(cmd, {
|
|
710
|
+
cwd: photonDir,
|
|
711
|
+
timeout: timeoutMs,
|
|
712
|
+
encoding: 'utf-8',
|
|
713
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
// execSync throws on non-zero exit or timeout. Preserve any
|
|
718
|
+
// partial stdout for callers that parse incremental output;
|
|
719
|
+
// surface stderr through the loader logger so the failure is
|
|
720
|
+
// not silent.
|
|
721
|
+
const e = err;
|
|
722
|
+
const stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString('utf-8');
|
|
723
|
+
if (stderr) {
|
|
724
|
+
loaderLogger.warn(`this.shell error in ${name}`, {
|
|
725
|
+
cmd,
|
|
726
|
+
stderr: stderr.slice(0, 500),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const stdout = typeof e.stdout === 'string' ? e.stdout : e.stdout?.toString('utf-8');
|
|
730
|
+
return stdout ?? '';
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
// Stat-gate baseline for CLI-direct dispatch. Daemon-routed calls
|
|
735
|
+
// have their own equivalent at src/daemon/server.ts; this ensures
|
|
736
|
+
// `photon cli foo bar` run immediately after a `sed -i` sees the
|
|
737
|
+
// new code on the first call.
|
|
738
|
+
try {
|
|
739
|
+
const s = statSync(absolutePath);
|
|
740
|
+
instance._photonSourceStat = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino };
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// No stat — the gate stays a no-op for this instance.
|
|
744
|
+
}
|
|
745
|
+
instance._photonReloader = async () => {
|
|
746
|
+
try {
|
|
747
|
+
await this.reloadFile(absolutePath);
|
|
748
|
+
const s = statSync(absolutePath);
|
|
749
|
+
instance._photonSourceStat = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino };
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
this.logger.debug('Stat-gate reload failed', {
|
|
753
|
+
path: absolutePath,
|
|
754
|
+
error: getErrorMessage(err),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
};
|
|
693
758
|
// Inject dynamic photon resolver for this.photon.use()
|
|
694
759
|
instance._photonResolver = (photonName, instanceName) => {
|
|
695
760
|
return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
|
|
@@ -754,9 +819,12 @@ export class PhotonLoader {
|
|
|
754
819
|
this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
|
|
755
820
|
}
|
|
756
821
|
this.injectPathHelpers(instance, tsContent);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
822
|
+
// Always-inject emit + its helpers (render/toast/log/status/progress/
|
|
823
|
+
// thinking). The injection is a pure closure — zero cost if unused.
|
|
824
|
+
// Dropping the capability-detection gate means typed-access patterns
|
|
825
|
+
// like `(this as any).emit(...)` work even if the photon-core regex
|
|
826
|
+
// misses them. User-defined emit on the class wins (no clobber).
|
|
827
|
+
if (!instance.emit) {
|
|
760
828
|
instance.emit = (data) => {
|
|
761
829
|
const store = executionContext.getStore();
|
|
762
830
|
const emitData = instance._photonName && typeof data === 'object' && data !== null
|
|
@@ -811,8 +879,12 @@ export class PhotonLoader {
|
|
|
811
879
|
configurable: true,
|
|
812
880
|
});
|
|
813
881
|
}
|
|
814
|
-
|
|
815
|
-
|
|
882
|
+
// Inject call() for cross-photon communication. Always-inject (no
|
|
883
|
+
// capability-detection gate) because the underlying _callHandler
|
|
884
|
+
// is ALWAYS wired above. Gating on a regex that matches literal
|
|
885
|
+
// `this.call(` misses typed-access patterns like
|
|
886
|
+
// `(this as any).call(...)` and fails silently at runtime.
|
|
887
|
+
if (!instance.call) {
|
|
816
888
|
instance.call = async (target, params = {}, options) => {
|
|
817
889
|
const dotIndex = target.indexOf('.');
|
|
818
890
|
if (dotIndex === -1) {
|
|
@@ -826,7 +898,8 @@ export class PhotonLoader {
|
|
|
826
898
|
return instance._callHandler(photonName, methodName, params, options?.instance);
|
|
827
899
|
};
|
|
828
900
|
}
|
|
829
|
-
|
|
901
|
+
// Always-inject mcp(). Pure closure. User-defined mcp wins.
|
|
902
|
+
if (!instance.mcp) {
|
|
830
903
|
// Inject mcp() accessor for external MCP server access
|
|
831
904
|
const mcpClients = new Map();
|
|
832
905
|
instance.mcp = (mcpName) => {
|
|
@@ -842,9 +915,10 @@ export class PhotonLoader {
|
|
|
842
915
|
return client;
|
|
843
916
|
};
|
|
844
917
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
918
|
+
// Always-inject caller getter. The prototype check preserves any
|
|
919
|
+
// user-defined getter on the class (e.g. Photon base class has its
|
|
920
|
+
// own) so we don't clobber.
|
|
921
|
+
if (!Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'caller')) {
|
|
848
922
|
Object.defineProperty(instance, 'caller', {
|
|
849
923
|
get() {
|
|
850
924
|
const store = executionContext.getStore();
|
|
@@ -853,8 +927,8 @@ export class PhotonLoader {
|
|
|
853
927
|
configurable: true,
|
|
854
928
|
});
|
|
855
929
|
}
|
|
856
|
-
|
|
857
|
-
|
|
930
|
+
// Always-inject withLock. Pure closure. User-defined wins.
|
|
931
|
+
if (!instance.withLock) {
|
|
858
932
|
instance.withLock = async (lockName, fn, timeout) => {
|
|
859
933
|
return withLockHelper(lockName, fn, timeout);
|
|
860
934
|
};
|
|
@@ -884,8 +958,7 @@ export class PhotonLoader {
|
|
|
884
958
|
if (caps.has('instanceMeta')) {
|
|
885
959
|
// Inject instance metadata (file-stat-based timestamps)
|
|
886
960
|
const instanceName = options?.instanceName || 'default';
|
|
887
|
-
const
|
|
888
|
-
const stateFile = path.join(stateDir, `${instanceName}.json`);
|
|
961
|
+
const stateFile = getInstanceStatePath(name, instanceName, this.baseDir);
|
|
889
962
|
try {
|
|
890
963
|
const stat = await fs.stat(stateFile);
|
|
891
964
|
instance.instanceMeta = {
|
|
@@ -903,37 +976,43 @@ export class PhotonLoader {
|
|
|
903
976
|
};
|
|
904
977
|
}
|
|
905
978
|
}
|
|
906
|
-
|
|
907
|
-
|
|
979
|
+
// Always-inject allInstances. The async generator is lazy — directory
|
|
980
|
+
// reads only happen when the caller iterates. User-defined wins.
|
|
981
|
+
if (!instance.allInstances) {
|
|
982
|
+
// Inject cross-instance iterator. Walks the Option B layout —
|
|
983
|
+
// `.data/{photon}/state/{instance}/state.json` — one subdirectory
|
|
984
|
+
// per instance. getInstanceStatePath keeps the path source truth
|
|
985
|
+
// in one place.
|
|
908
986
|
const photonName = name;
|
|
909
987
|
const loaderBaseDir = this.baseDir;
|
|
910
988
|
instance.allInstances = async function* () {
|
|
911
|
-
|
|
989
|
+
// Derive the parent state dir from a known instance path.
|
|
990
|
+
const stateDir = path.dirname(path.dirname(getInstanceStatePath(photonName, 'default', loaderBaseDir)));
|
|
991
|
+
let subdirs;
|
|
912
992
|
try {
|
|
913
|
-
|
|
914
|
-
for (const file of files.filter((f) => f.endsWith('.json'))) {
|
|
915
|
-
const instName = file.replace('.json', '');
|
|
916
|
-
const statePath = path.join(stateDir, file);
|
|
917
|
-
try {
|
|
918
|
-
const snapshot = await readJSON(statePath);
|
|
919
|
-
const stat = await fs.stat(statePath);
|
|
920
|
-
yield {
|
|
921
|
-
name: instName,
|
|
922
|
-
meta: {
|
|
923
|
-
name: instName,
|
|
924
|
-
createdAt: stat.birthtime.toISOString(),
|
|
925
|
-
updatedAt: stat.mtime.toISOString(),
|
|
926
|
-
},
|
|
927
|
-
state: snapshot,
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
catch {
|
|
931
|
-
// Skip corrupted state files
|
|
932
|
-
}
|
|
933
|
-
}
|
|
993
|
+
subdirs = await fs.readdir(stateDir);
|
|
934
994
|
}
|
|
935
995
|
catch {
|
|
936
|
-
//
|
|
996
|
+
return; // state dir doesn't exist yet — no instances
|
|
997
|
+
}
|
|
998
|
+
for (const instName of subdirs) {
|
|
999
|
+
const statePath = getInstanceStatePath(photonName, instName, loaderBaseDir);
|
|
1000
|
+
try {
|
|
1001
|
+
const snapshot = await readJSON(statePath);
|
|
1002
|
+
const stat = await fs.stat(statePath);
|
|
1003
|
+
yield {
|
|
1004
|
+
name: instName,
|
|
1005
|
+
meta: {
|
|
1006
|
+
name: instName,
|
|
1007
|
+
createdAt: stat.birthtime.toISOString(),
|
|
1008
|
+
updatedAt: stat.mtime.toISOString(),
|
|
1009
|
+
},
|
|
1010
|
+
state: snapshot,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
// Not a state subdir, or a corrupted / legacy file — skip.
|
|
1015
|
+
}
|
|
937
1016
|
}
|
|
938
1017
|
};
|
|
939
1018
|
}
|
|
@@ -1042,26 +1121,11 @@ export class PhotonLoader {
|
|
|
1042
1121
|
if (tsContent) {
|
|
1043
1122
|
await this.checkCLIDependencies(tsContent, name);
|
|
1044
1123
|
}
|
|
1045
|
-
// Call lifecycle hook if present with error handling
|
|
1046
|
-
// skipInitialize is used during hot-reload: state is transferred from
|
|
1047
|
-
// instance after loadFile returns, then onInitialize is called
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
this.onProgress?.('running onInitialize');
|
|
1051
|
-
try {
|
|
1052
|
-
await onInitialize.call(instance);
|
|
1053
|
-
}
|
|
1054
|
-
catch (error) {
|
|
1055
|
-
const initError = new Error(`Initialization failed for ${name}: ${getErrorMessage(error)}\n` +
|
|
1056
|
-
`\nThe onInitialize() lifecycle hook threw an error.\n` +
|
|
1057
|
-
`Check your constructor configuration and initialization logic.`);
|
|
1058
|
-
initError.name = 'PhotonInitializationError';
|
|
1059
|
-
if (error instanceof Error && error.stack) {
|
|
1060
|
-
initError.stack = error.stack;
|
|
1061
|
-
}
|
|
1062
|
-
throw initError;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1124
|
+
// Call lifecycle hook if present with error handling.
|
|
1125
|
+
// skipInitialize is used during hot-reload: state is transferred from
|
|
1126
|
+
// the old instance after loadFile returns, then onInitialize is called
|
|
1127
|
+
// manually.
|
|
1128
|
+
await this.invokeInitialize(instance, name, options);
|
|
1065
1129
|
// Auto-wrap public methods in @stateful classes to emit events
|
|
1066
1130
|
// Must happen AFTER capability injection so that emit() is available
|
|
1067
1131
|
if (tsContent) {
|
|
@@ -1180,13 +1244,46 @@ export class PhotonLoader {
|
|
|
1180
1244
|
}
|
|
1181
1245
|
instance._photonName = name;
|
|
1182
1246
|
instance._photonNamespace = this.resolveNamespace(absolutePath);
|
|
1247
|
+
// Same pin as the file-load path above — see that comment for why
|
|
1248
|
+
// this.memory drifts without it.
|
|
1249
|
+
instance._baseDir = this.baseDir;
|
|
1183
1250
|
instance.instanceName = options?.instanceName ?? '';
|
|
1184
1251
|
// Inject file path for storage()/assets() resolution.
|
|
1185
|
-
// For preloaded modules (compiled binaries), remap to
|
|
1186
|
-
//
|
|
1252
|
+
// For preloaded modules (compiled binaries), remap to the resolved
|
|
1253
|
+
// PHOTON_DIR (this.baseDir) so storage() resolves under the deployment
|
|
1254
|
+
// directory instead of the build machine's source tree. Respects Option B:
|
|
1255
|
+
// storage follows wherever the photon is deployed, not always ~/.photon.
|
|
1187
1256
|
// Assets still use absolutePath (resolved from embedded source paths).
|
|
1188
|
-
const storagePath = path.join(
|
|
1257
|
+
const storagePath = path.join(this.baseDir, path.basename(absolutePath));
|
|
1189
1258
|
instance._photonFilePath = storagePath;
|
|
1259
|
+
// Inject this.shell — see the primary load path for rationale.
|
|
1260
|
+
// User-defined shell() wins; we don't clobber.
|
|
1261
|
+
if (typeof instance.shell !== 'function') {
|
|
1262
|
+
const photonDir = path.dirname(absolutePath);
|
|
1263
|
+
const loaderLogger = this.logger;
|
|
1264
|
+
instance.shell = (cmd, timeoutMs = 30000) => {
|
|
1265
|
+
try {
|
|
1266
|
+
return execSync(cmd, {
|
|
1267
|
+
cwd: photonDir,
|
|
1268
|
+
timeout: timeoutMs,
|
|
1269
|
+
encoding: 'utf-8',
|
|
1270
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
catch (err) {
|
|
1274
|
+
const e = err;
|
|
1275
|
+
const stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString('utf-8');
|
|
1276
|
+
if (stderr) {
|
|
1277
|
+
loaderLogger.warn(`this.shell error in ${name}`, {
|
|
1278
|
+
cmd,
|
|
1279
|
+
stderr: stderr.slice(0, 500),
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
const stdout = typeof e.stdout === 'string' ? e.stdout : e.stdout?.toString('utf-8');
|
|
1283
|
+
return stdout ?? '';
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1190
1287
|
// Inject dynamic photon resolver for this.photon.use()
|
|
1191
1288
|
instance._photonResolver = (photonName, instanceName) => {
|
|
1192
1289
|
return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
|
|
@@ -1225,7 +1322,8 @@ export class PhotonLoader {
|
|
|
1225
1322
|
if (detectEmitHelperUsage(tsContent))
|
|
1226
1323
|
caps.add('emit');
|
|
1227
1324
|
this.injectPathHelpers(instance, tsContent);
|
|
1228
|
-
|
|
1325
|
+
// Always-inject emit (see primary path). User-defined wins.
|
|
1326
|
+
if (!instance.emit) {
|
|
1229
1327
|
instance.emit = (data) => {
|
|
1230
1328
|
const store = executionContext.getStore();
|
|
1231
1329
|
const emitData = instance._photonName && typeof data === 'object' && data !== null
|
|
@@ -1273,7 +1371,8 @@ export class PhotonLoader {
|
|
|
1273
1371
|
configurable: true,
|
|
1274
1372
|
});
|
|
1275
1373
|
}
|
|
1276
|
-
|
|
1374
|
+
// Always-inject call() (see the primary load path for rationale).
|
|
1375
|
+
if (!instance.call) {
|
|
1277
1376
|
instance.call = async (target, params = {}, opts) => {
|
|
1278
1377
|
const dotIndex = target.indexOf('.');
|
|
1279
1378
|
if (dotIndex === -1) {
|
|
@@ -1348,18 +1447,7 @@ export class PhotonLoader {
|
|
|
1348
1447
|
await this.checkCLIDependencies(tsContent, name);
|
|
1349
1448
|
}
|
|
1350
1449
|
// Call lifecycle hook
|
|
1351
|
-
|
|
1352
|
-
if (typeof onInitialize === 'function' && !options?.skipInitialize) {
|
|
1353
|
-
this.onProgress?.('running onInitialize');
|
|
1354
|
-
try {
|
|
1355
|
-
await onInitialize.call(instance);
|
|
1356
|
-
}
|
|
1357
|
-
catch (error) {
|
|
1358
|
-
const initError = new Error(`Initialization failed for ${name}: ${getErrorMessage(error)}`);
|
|
1359
|
-
initError.name = 'PhotonInitializationError';
|
|
1360
|
-
throw initError;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1450
|
+
await this.invokeInitialize(instance, name, options);
|
|
1363
1451
|
// Extract tools and metadata from embedded source (no disk I/O)
|
|
1364
1452
|
const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath, tsContent);
|
|
1365
1453
|
// Settings injection
|
|
@@ -1419,7 +1507,7 @@ export class PhotonLoader {
|
|
|
1419
1507
|
/**
|
|
1420
1508
|
* Reload a Photon MCP file (for hot reload)
|
|
1421
1509
|
*/
|
|
1422
|
-
async reloadFile(filePath) {
|
|
1510
|
+
async reloadFile(filePath, options) {
|
|
1423
1511
|
// Invalidate the cache for this file
|
|
1424
1512
|
const absolutePath = path.resolve(filePath);
|
|
1425
1513
|
if (absolutePath.endsWith('.ts')) {
|
|
@@ -1445,7 +1533,7 @@ export class PhotonLoader {
|
|
|
1445
1533
|
}
|
|
1446
1534
|
}
|
|
1447
1535
|
}
|
|
1448
|
-
return this.loadFile(filePath);
|
|
1536
|
+
return this.loadFile(filePath, options);
|
|
1449
1537
|
}
|
|
1450
1538
|
/**
|
|
1451
1539
|
* Compile TypeScript file to JavaScript and cache it
|
|
@@ -1677,10 +1765,16 @@ export class PhotonLoader {
|
|
|
1677
1765
|
// SETTINGS — property-driven configuration with auto-persistence
|
|
1678
1766
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1679
1767
|
/**
|
|
1680
|
-
* Get the settings persistence path for a photon instance
|
|
1768
|
+
* Get the settings persistence path for a photon instance.
|
|
1769
|
+
* Co-located with state under Option B: the settings JSON lives next to
|
|
1770
|
+
* state.json inside the per-instance directory.
|
|
1681
1771
|
*/
|
|
1682
1772
|
getSettingsPath(photonName, instanceName) {
|
|
1683
|
-
|
|
1773
|
+
// getInstanceStatePath returns `.../state/{instance}/state.json`; swap
|
|
1774
|
+
// the filename so settings land at `.../state/{instance}/settings.json`.
|
|
1775
|
+
// Keeps the source of truth for the layout in context-store.
|
|
1776
|
+
const statePath = getInstanceStatePath(photonName, instanceName, this.baseDir);
|
|
1777
|
+
return path.join(path.dirname(statePath), 'settings.json');
|
|
1684
1778
|
}
|
|
1685
1779
|
/**
|
|
1686
1780
|
* Load persisted settings from disk
|
|
@@ -3668,23 +3762,49 @@ Run: photon mcp ${mcpName} --config
|
|
|
3668
3762
|
});
|
|
3669
3763
|
}
|
|
3670
3764
|
/**
|
|
3671
|
-
* Resolve the namespace for a photon based on its
|
|
3672
|
-
*
|
|
3765
|
+
* Resolve the namespace for a photon based purely on its directory
|
|
3766
|
+
* position relative to baseDir. See docs/internals/PHOTON-DIR-AND-NAMESPACE.md.
|
|
3767
|
+
*
|
|
3768
|
+
* {baseDir}/foo.photon.ts → '' (flat at root)
|
|
3769
|
+
* {baseDir}/alice/foo.photon.ts → 'alice'
|
|
3770
|
+
* {baseDir}/org/team/foo.photon.ts → 'org/team'
|
|
3673
3771
|
*
|
|
3674
|
-
*
|
|
3675
|
-
*
|
|
3676
|
-
* ~/.photon/acme/todo.photon.ts → 'acme'
|
|
3677
|
-
* ~/.photon/todo.photon.ts → detected from git or 'local'
|
|
3772
|
+
* The runtime never consults git state. PHOTON_DIR is the outer boundary;
|
|
3773
|
+
* the file's position within it is the only namespace signal.
|
|
3678
3774
|
*/
|
|
3679
3775
|
resolveNamespace(absolutePath) {
|
|
3680
3776
|
const rel = path.relative(this.baseDir, absolutePath);
|
|
3681
3777
|
const parts = rel.split(path.sep);
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3778
|
+
if (parts.length < 2)
|
|
3779
|
+
return '';
|
|
3780
|
+
return parts.slice(0, -1).join(path.sep);
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Invoke the `onInitialize` lifecycle hook on a loaded photon instance,
|
|
3784
|
+
* honoring the `skipInitialize` option and wrapping any thrown error in
|
|
3785
|
+
* a PhotonInitializationError so the caller can surface hook failures
|
|
3786
|
+
* distinctly from runtime errors.
|
|
3787
|
+
*/
|
|
3788
|
+
async invokeInitialize(instance, name, options, ctx) {
|
|
3789
|
+
const hook = instance?.onInitialize;
|
|
3790
|
+
if (typeof hook !== 'function' || options?.skipInitialize)
|
|
3791
|
+
return;
|
|
3792
|
+
this.onProgress?.('running onInitialize');
|
|
3793
|
+
try {
|
|
3794
|
+
await hook.call(instance, ctx);
|
|
3795
|
+
}
|
|
3796
|
+
catch (error) {
|
|
3797
|
+
const message = ctx
|
|
3798
|
+
? `Initialization failed for ${name}: ${getErrorMessage(error)}`
|
|
3799
|
+
: `Initialization failed for ${name}: ${getErrorMessage(error)}\n` +
|
|
3800
|
+
`\nThe onInitialize() lifecycle hook threw an error.\n` +
|
|
3801
|
+
`Check your constructor configuration and initialization logic.`;
|
|
3802
|
+
const initError = new Error(message);
|
|
3803
|
+
initError.name = 'PhotonInitializationError';
|
|
3804
|
+
if (error instanceof Error && error.stack)
|
|
3805
|
+
initError.stack = error.stack;
|
|
3806
|
+
throw initError;
|
|
3685
3807
|
}
|
|
3686
|
-
// Flat file at root — detect from git remote or default to 'local'
|
|
3687
|
-
return detectNamespace(this.baseDir);
|
|
3688
3808
|
}
|
|
3689
3809
|
/**
|
|
3690
3810
|
* Discover and extract assets from a Photon file
|