@portel/photon-core 2.22.0 → 2.24.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/dist/base.d.ts +56 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +100 -2
- package/dist/base.js.map +1 -1
- package/dist/bases-registry.d.ts +57 -0
- package/dist/bases-registry.d.ts.map +1 -0
- package/dist/bases-registry.js +127 -0
- package/dist/bases-registry.js.map +1 -0
- package/dist/data-paths.d.ts +31 -18
- package/dist/data-paths.d.ts.map +1 -1
- package/dist/data-paths.js +42 -43
- package/dist/data-paths.js.map +1 -1
- package/dist/description-sanitizer.d.ts +34 -0
- package/dist/description-sanitizer.d.ts.map +1 -0
- package/dist/description-sanitizer.js +80 -0
- package/dist/description-sanitizer.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +109 -1
- package/dist/memory.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +96 -0
- package/dist/middleware.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +9 -2
- package/dist/mixins.js.map +1 -1
- package/dist/path-resolver.d.ts +13 -1
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +23 -1
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-loader-lite.d.ts +17 -2
- package/dist/photon-loader-lite.d.ts.map +1 -1
- package/dist/photon-loader-lite.js +203 -26
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schedule.d.ts +10 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +20 -10
- package/dist/schedule.js.map +1 -1
- package/dist/schema-extractor.d.ts +9 -3
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +149 -17
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +3 -2
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +18 -6
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/base.ts +123 -2
- package/src/bases-registry.ts +141 -0
- package/src/data-paths.ts +43 -49
- package/src/description-sanitizer.ts +102 -0
- package/src/index.ts +20 -1
- package/src/memory.ts +109 -0
- package/src/middleware.ts +98 -0
- package/src/mixins.ts +14 -2
- package/src/path-resolver.ts +26 -1
- package/src/photon-loader-lite.ts +214 -33
- package/src/schedule.ts +26 -10
- package/src/schema-extractor.ts +164 -17
- package/src/stateful.ts +19 -6
- package/src/types.ts +9 -0
package/src/index.ts
CHANGED
|
@@ -162,6 +162,12 @@ export { DependencyManager } from './dependency-manager.js';
|
|
|
162
162
|
|
|
163
163
|
// Schema extraction
|
|
164
164
|
export { SchemaExtractor, detectCapabilities, type PhotonCapability } from './schema-extractor.js';
|
|
165
|
+
export {
|
|
166
|
+
sanitizeDescription,
|
|
167
|
+
MAX_DESCRIPTION_LENGTH,
|
|
168
|
+
type SanitizerWarning,
|
|
169
|
+
type SanitizeResult,
|
|
170
|
+
} from './description-sanitizer.js';
|
|
165
171
|
|
|
166
172
|
// Path resolution (Photon-specific paths)
|
|
167
173
|
export {
|
|
@@ -172,6 +178,7 @@ export {
|
|
|
172
178
|
resolvePhotonPath,
|
|
173
179
|
listPhotonFiles,
|
|
174
180
|
listPhotonFilesWithNamespace,
|
|
181
|
+
listPhotonSourceFiles,
|
|
175
182
|
ensurePhotonDir,
|
|
176
183
|
DEFAULT_PHOTON_DIR,
|
|
177
184
|
type ResolverOptions,
|
|
@@ -494,7 +501,14 @@ export {
|
|
|
494
501
|
|
|
495
502
|
// ===== PHOTON LOADER LITE =====
|
|
496
503
|
// Direct TypeScript API for loading .photon.ts files with full enhancements
|
|
497
|
-
export {
|
|
504
|
+
export {
|
|
505
|
+
photon,
|
|
506
|
+
clearPhotonCache,
|
|
507
|
+
disposePhoton,
|
|
508
|
+
disposeAllPhotons,
|
|
509
|
+
type PhotonOptions,
|
|
510
|
+
type PhotonEvent,
|
|
511
|
+
} from './photon-loader-lite.js';
|
|
498
512
|
|
|
499
513
|
// ===== FILE WATCHING =====
|
|
500
514
|
// Reusable photon file watcher with symlink resolution, debouncing, rename handling
|
|
@@ -592,3 +606,8 @@ export {
|
|
|
592
606
|
// ===== DATA PATHS =====
|
|
593
607
|
// Central data path resolver — single source of truth for all runtime data locations
|
|
594
608
|
export * from './data-paths.js';
|
|
609
|
+
|
|
610
|
+
// ===== BASES REGISTRY =====
|
|
611
|
+
// Daemon-owned registry of every PHOTON_DIR served. Used to discover schedules
|
|
612
|
+
// and other per-base data on daemon startup. See data-paths getBasesRegistryPath.
|
|
613
|
+
export * from './bases-registry.js';
|
package/src/memory.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getLegacyMemoryDir,
|
|
28
28
|
getLegacyGlobalMemoryDir,
|
|
29
29
|
getLegacySessionMemoryDir,
|
|
30
|
+
getDataRoot,
|
|
30
31
|
} from './data-paths.js';
|
|
31
32
|
|
|
32
33
|
export type MemoryScope = 'photon' | 'session' | 'global';
|
|
@@ -216,6 +217,110 @@ export class FileMemoryBackend implements MemoryBackend {
|
|
|
216
217
|
// SCOPE RESOLUTION
|
|
217
218
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
218
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Compatibility shim — one-release migration only.
|
|
222
|
+
*
|
|
223
|
+
* Under docs/internals/PHOTON-DIR-AND-NAMESPACE.md the namespace comes
|
|
224
|
+
* purely from directory position and never changes silently. But installs
|
|
225
|
+
* that ran with the old git-remote-based namespace detection may have data
|
|
226
|
+
* sitting under a stale namespace bucket. On first read, if the canonical
|
|
227
|
+
* dir is empty but exactly one sibling namespace contains this photon's
|
|
228
|
+
* memory, migrate it atomically to the canonical location.
|
|
229
|
+
*
|
|
230
|
+
* Multiple matches → ambiguous; warn to stderr so the user can consolidate
|
|
231
|
+
* manually rather than proceeding with empty memory.
|
|
232
|
+
*
|
|
233
|
+
* @deprecated Remove this helper and its caller one release after the
|
|
234
|
+
* PHOTON_DIR-and-namespace change ships.
|
|
235
|
+
*/
|
|
236
|
+
function findAndMigrateStrandedMemory(
|
|
237
|
+
photonId: string,
|
|
238
|
+
canonicalDir: string,
|
|
239
|
+
baseDir?: string
|
|
240
|
+
): string | null {
|
|
241
|
+
const dataRoot = getDataRoot(baseDir);
|
|
242
|
+
let entries: string[];
|
|
243
|
+
try {
|
|
244
|
+
entries = fsSync.readdirSync(dataRoot);
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const matches: string[] = [];
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
// Skip non-namespace buckets. `_global`, `_sessions`, `.cache`, `tasks`
|
|
252
|
+
// live at the same level but are reserved names.
|
|
253
|
+
if (entry.startsWith('_') || entry.startsWith('.') || entry === 'tasks') continue;
|
|
254
|
+
const candidate = path.join(dataRoot, entry, photonId, 'memory');
|
|
255
|
+
try {
|
|
256
|
+
const stat = fsSync.statSync(candidate);
|
|
257
|
+
if (stat.isDirectory()) {
|
|
258
|
+
// Only count non-empty dirs as real data.
|
|
259
|
+
if (fsSync.readdirSync(candidate).length > 0) matches.push(candidate);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Not a match, continue.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (matches.length === 0) return null;
|
|
267
|
+
if (matches.length > 1) {
|
|
268
|
+
process.stderr.write(
|
|
269
|
+
`[photon] warning: photon '${photonId}' has memory data stranded under multiple namespaces:\n` +
|
|
270
|
+
matches.map((m) => ` - ${m}`).join('\n') +
|
|
271
|
+
`\n Canonical path is ${canonicalDir}.\n` +
|
|
272
|
+
` Move the correct one into the canonical path and delete the others to consolidate.\n`
|
|
273
|
+
);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const stranded = matches[0];
|
|
278
|
+
try {
|
|
279
|
+
fsSync.mkdirSync(path.dirname(canonicalDir), { recursive: true });
|
|
280
|
+
fsSync.renameSync(stranded, canonicalDir);
|
|
281
|
+
// Clean up now-empty parent if it has no siblings.
|
|
282
|
+
try {
|
|
283
|
+
const strandedParent = path.dirname(stranded);
|
|
284
|
+
if (fsSync.readdirSync(strandedParent).length === 0) fsSync.rmdirSync(strandedParent);
|
|
285
|
+
const strandedNs = path.dirname(strandedParent);
|
|
286
|
+
if (fsSync.readdirSync(strandedNs).length === 0) fsSync.rmdirSync(strandedNs);
|
|
287
|
+
} catch {
|
|
288
|
+
// Non-fatal: leave parent dirs if cleanup fails.
|
|
289
|
+
}
|
|
290
|
+
return canonicalDir;
|
|
291
|
+
} catch {
|
|
292
|
+
// Cross-device rename or permission issue — fall back to reading in place.
|
|
293
|
+
return stranded;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Photons whose loader pinned a baseDir get deterministic paths regardless
|
|
299
|
+
* of which process reads them back. When baseDir is missing, getBase()
|
|
300
|
+
* silently falls back to PHOTON_DIR env or ~/.photon, which means the same
|
|
301
|
+
* photon can write to <repo>/.data/ from a CLI process and read from
|
|
302
|
+
* ~/.photon/.data/ from a daemon worker started in a different cwd. We
|
|
303
|
+
* warn once per photon so this drift is noticeable rather than silent.
|
|
304
|
+
*
|
|
305
|
+
* To suppress in test environments where the fallback is intentional, set
|
|
306
|
+
* PHOTON_MEMORY_NO_BASEDIR_WARN=1.
|
|
307
|
+
*/
|
|
308
|
+
const _baseDirFallbackWarned = new Set<string>();
|
|
309
|
+
function warnIfBaseDirMissing(photonId: string, baseDir?: string): void {
|
|
310
|
+
if (baseDir) return;
|
|
311
|
+
if (process.env.PHOTON_MEMORY_NO_BASEDIR_WARN) return;
|
|
312
|
+
if (_baseDirFallbackWarned.has(photonId)) return;
|
|
313
|
+
_baseDirFallbackWarned.add(photonId);
|
|
314
|
+
process.stderr.write(
|
|
315
|
+
`[photon-core] memory.resolveDir for "${photonId}" got no baseDir — ` +
|
|
316
|
+
`falling back to PHOTON_DIR env or ~/.photon. If this photon was ` +
|
|
317
|
+
`loaded with a workingDir, the loader is failing to set ` +
|
|
318
|
+
`instance._baseDir. Symptom: writes from one process land at the ` +
|
|
319
|
+
`loader's baseDir but reads from another process land at the env ` +
|
|
320
|
+
`fallback. See memory-baseDir-resolution-bug for details.\n`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
219
324
|
function resolveDir(
|
|
220
325
|
photonId: string,
|
|
221
326
|
namespace: string,
|
|
@@ -223,12 +328,16 @@ function resolveDir(
|
|
|
223
328
|
sessionId?: string,
|
|
224
329
|
baseDir?: string
|
|
225
330
|
): string {
|
|
331
|
+
warnIfBaseDirMissing(photonId, baseDir);
|
|
226
332
|
switch (scope) {
|
|
227
333
|
case 'photon': {
|
|
228
334
|
const newDir = getPhotonMemoryDir(namespace, photonId, baseDir);
|
|
229
335
|
if (!fsSync.existsSync(newDir)) {
|
|
230
336
|
const legacyDir = getLegacyMemoryDir(photonId, baseDir);
|
|
231
337
|
if (fsSync.existsSync(legacyDir)) return legacyDir;
|
|
338
|
+
// Last resort: data stranded under a different namespace bucket.
|
|
339
|
+
const recovered = findAndMigrateStrandedMemory(photonId, newDir, baseDir);
|
|
340
|
+
if (recovered) return recovered;
|
|
232
341
|
}
|
|
233
342
|
return newDir;
|
|
234
343
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -674,6 +674,102 @@ const bulkheadMiddleware = defineMiddleware<{ maxConcurrent: number }>({
|
|
|
674
674
|
},
|
|
675
675
|
});
|
|
676
676
|
|
|
677
|
+
// --- mask (phase 85) ---
|
|
678
|
+
// Post-execution redaction pass. Rewrites named fields in the result with
|
|
679
|
+
// a masked placeholder before handoff to the transport layer. Defense
|
|
680
|
+
// against oversharing and accidental PII exposure (OWASP MCP #10).
|
|
681
|
+
// Runs AFTER execution, BEFORE __meta attachment (phase ≥ 85).
|
|
682
|
+
|
|
683
|
+
function maskValue(value: unknown, keys: string[], placeholder: string): unknown {
|
|
684
|
+
if (value === null || value === undefined) return value;
|
|
685
|
+
if (Array.isArray(value)) {
|
|
686
|
+
return value.map((v) => maskValue(v, keys, placeholder));
|
|
687
|
+
}
|
|
688
|
+
if (typeof value === 'object') {
|
|
689
|
+
const src = value as Record<string, unknown>;
|
|
690
|
+
const out: Record<string, unknown> = {};
|
|
691
|
+
for (const k of Object.keys(src)) {
|
|
692
|
+
if (keys.includes(k)) {
|
|
693
|
+
out[k] = placeholder;
|
|
694
|
+
} else {
|
|
695
|
+
out[k] = maskValue(src[k], keys, placeholder);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const maskMiddleware = defineMiddleware<{ fields: string[]; placeholder: string }>({
|
|
704
|
+
name: 'mask',
|
|
705
|
+
phase: 85,
|
|
706
|
+
parseShorthand(value: string) {
|
|
707
|
+
const fields = value
|
|
708
|
+
.split(/[,\s]+/)
|
|
709
|
+
.map((s) => s.trim())
|
|
710
|
+
.filter(Boolean);
|
|
711
|
+
return { fields, placeholder: '[REDACTED]' };
|
|
712
|
+
},
|
|
713
|
+
parseConfig(raw) {
|
|
714
|
+
const fields = (raw.fields || '')
|
|
715
|
+
.split(/[,\s]+/)
|
|
716
|
+
.map((s) => s.trim())
|
|
717
|
+
.filter(Boolean);
|
|
718
|
+
return { fields, placeholder: raw.placeholder?.trim() || '[REDACTED]' };
|
|
719
|
+
},
|
|
720
|
+
create(config, _state) {
|
|
721
|
+
return async (ctx, next) => {
|
|
722
|
+
const result = await next();
|
|
723
|
+
if (config.fields.length === 0) return result;
|
|
724
|
+
return maskValue(result, config.fields, config.placeholder);
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// --- maxResponseBytes (phase 88) ---
|
|
730
|
+
// Caps the serialized response size. Hard truncates with a warning marker.
|
|
731
|
+
// Prevents context-window flooding (OWASP MCP #10) — an oversized result
|
|
732
|
+
// is an attack vector on its own even when the data is legitimate.
|
|
733
|
+
// Runs after @mask so the cap applies to the redacted payload.
|
|
734
|
+
|
|
735
|
+
const maxResponseBytesMiddleware = defineMiddleware<{ limit: number }>({
|
|
736
|
+
name: 'maxResponseBytes',
|
|
737
|
+
phase: 88,
|
|
738
|
+
parseShorthand(value: string) {
|
|
739
|
+
return { limit: Math.max(1, parseInt(value.trim(), 10) || 0) };
|
|
740
|
+
},
|
|
741
|
+
parseConfig(raw) {
|
|
742
|
+
const v = raw.limit || raw.bytes || raw.max;
|
|
743
|
+
return { limit: Math.max(1, parseInt(v || '0', 10) || 0) };
|
|
744
|
+
},
|
|
745
|
+
create(config, _state) {
|
|
746
|
+
return async (ctx, next) => {
|
|
747
|
+
const result = await next();
|
|
748
|
+
if (!config.limit || config.limit <= 0) return result;
|
|
749
|
+
let serialized: string;
|
|
750
|
+
try {
|
|
751
|
+
serialized = typeof result === 'string' ? result : JSON.stringify(result);
|
|
752
|
+
} catch {
|
|
753
|
+
return result; // unserializable → leave untouched
|
|
754
|
+
}
|
|
755
|
+
const byteLen = Buffer.byteLength(serialized, 'utf8');
|
|
756
|
+
if (byteLen <= config.limit) return result;
|
|
757
|
+
const truncated = serialized.slice(
|
|
758
|
+
0,
|
|
759
|
+
Math.max(0, config.limit - 32)
|
|
760
|
+
);
|
|
761
|
+
return {
|
|
762
|
+
truncated: true,
|
|
763
|
+
reason: 'maxResponseBytes',
|
|
764
|
+
limit: config.limit,
|
|
765
|
+
originalBytes: byteLen,
|
|
766
|
+
tool: `${ctx.photon}.${ctx.tool}`,
|
|
767
|
+
preview: truncated,
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
|
|
677
773
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
678
774
|
// GLOBAL BUILT-IN REGISTRY
|
|
679
775
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -691,6 +787,8 @@ builtinRegistry.register(queuedMiddleware);
|
|
|
691
787
|
builtinRegistry.register(lockedMiddleware);
|
|
692
788
|
builtinRegistry.register(timeoutMiddleware);
|
|
693
789
|
builtinRegistry.register(retryableMiddleware);
|
|
790
|
+
builtinRegistry.register(maskMiddleware);
|
|
791
|
+
builtinRegistry.register(maxResponseBytesMiddleware);
|
|
694
792
|
|
|
695
793
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
696
794
|
// PIPELINE ASSEMBLY
|
package/src/mixins.ts
CHANGED
|
@@ -58,6 +58,13 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
58
58
|
*/
|
|
59
59
|
_photonName?: string;
|
|
60
60
|
_photonNamespace?: string;
|
|
61
|
+
/**
|
|
62
|
+
* PHOTON_DIR this instance was loaded from - set by runtime loader.
|
|
63
|
+
* Pinned so memory and other .data/-rooted state resolve to the same
|
|
64
|
+
* root regardless of which process reads back later.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
_baseDir?: string;
|
|
61
68
|
|
|
62
69
|
/**
|
|
63
70
|
* Session ID for session-scoped memory - set by runtime
|
|
@@ -113,7 +120,12 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
113
120
|
.replace(/([A-Z])/g, '-$1')
|
|
114
121
|
.toLowerCase()
|
|
115
122
|
.replace(/^-/, '');
|
|
116
|
-
this._memory = new MemoryProvider(
|
|
123
|
+
this._memory = new MemoryProvider(
|
|
124
|
+
name,
|
|
125
|
+
this._sessionId,
|
|
126
|
+
this._photonNamespace,
|
|
127
|
+
this._baseDir
|
|
128
|
+
);
|
|
117
129
|
}
|
|
118
130
|
return this._memory;
|
|
119
131
|
}
|
|
@@ -128,7 +140,7 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
128
140
|
.replace(/([A-Z])/g, '-$1')
|
|
129
141
|
.toLowerCase()
|
|
130
142
|
.replace(/^-/, '');
|
|
131
|
-
this._schedule = new ScheduleProvider(name);
|
|
143
|
+
this._schedule = new ScheduleProvider(name, this._baseDir);
|
|
132
144
|
}
|
|
133
145
|
return this._schedule;
|
|
134
146
|
}
|
package/src/path-resolver.ts
CHANGED
|
@@ -18,7 +18,10 @@ import * as fsSync from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import * as os from 'os';
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
import { DEFAULT_PHOTON_DIR } from './data-paths.js';
|
|
22
|
+
// Re-exported so existing `import { DEFAULT_PHOTON_DIR } from '@portel/photon-core'`
|
|
23
|
+
// callers keep working. One canonical definition lives in data-paths.ts.
|
|
24
|
+
export { DEFAULT_PHOTON_DIR };
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Expand tilde (~) to user's home directory
|
|
@@ -53,6 +56,28 @@ const SKIP_DIRS = new Set([
|
|
|
53
56
|
'node_modules', 'marketplace', 'photons', 'templates',
|
|
54
57
|
]);
|
|
55
58
|
|
|
59
|
+
/**
|
|
60
|
+
* List photon source file names in a directory. Returns just filenames
|
|
61
|
+
* (not full paths) so callers can choose to either join with the dir
|
|
62
|
+
* or operate on the basename directly. Missing/unreadable dirs return [].
|
|
63
|
+
*
|
|
64
|
+
* Replaces scattered `readdirSync(dir).filter(f => f.endsWith('.photon.ts'))`
|
|
65
|
+
* expressions so the default extension list stays consistent.
|
|
66
|
+
*/
|
|
67
|
+
export function listPhotonSourceFiles(
|
|
68
|
+
dir: string,
|
|
69
|
+
options?: { extensions?: string[] },
|
|
70
|
+
): string[] {
|
|
71
|
+
const extensions = options?.extensions ?? defaultOptions.extensions;
|
|
72
|
+
try {
|
|
73
|
+
return fsSync
|
|
74
|
+
.readdirSync(dir)
|
|
75
|
+
.filter((f) => extensions.some((ext) => f.endsWith(ext)));
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/**
|
|
57
82
|
* Resolve a file path from name.
|
|
58
83
|
*
|