@dbx-tools/appkit-mastra 0.9.74 → 0.9.79

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/src/workspaces.ts CHANGED
@@ -24,12 +24,12 @@ import type { WorkspaceClient } from "@databricks/appkit";
24
24
  import { DatabricksFileSystem, workspaceClient } from "@dbx-tools/databricks";
25
25
  import { LocalFileSystem } from "@dbx-tools/fs";
26
26
  import { errorUtils, log, object, stringUtils, token } from "@dbx-tools/shared-core";
27
- import { posixPath } from "@dbx-tools/shared-fs";
27
+ import type { FileSystem } from "@dbx-tools/shared-fs";
28
+ import { LRUCache } from "lru-cache";
28
29
  import type { RequestContext } from "@mastra/core/request-context";
29
30
  import {
30
31
  CompositeFilesystem,
31
32
  Workspace,
32
- type SkillsContext,
33
33
  type SkillsResolver,
34
34
  type WorkspaceFilesystem,
35
35
  type WorkspaceSandbox,
@@ -46,7 +46,12 @@ import {
46
46
  import { filesystems, scratchFilesystem } from "./filesystems.ts";
47
47
  import { MontySandbox } from "./monty-sandbox.ts";
48
48
  import { DatabricksSandbox, type DatabricksWorkspaceSandboxOptions } from "./sandbox.ts";
49
- import { cachedWorkspaceSkillMount, DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS } from "./skill-cache.ts";
49
+ import {
50
+ DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
51
+ workspaceSkillCatalogue,
52
+ type SkillCatalogueResolver,
53
+ type SkillCatalogueSource,
54
+ } from "./skill-cache.ts";
50
55
  import { ASSISTANT_SHARED_SKILLS_PATH, userAssistantSkillsPath } from "./skill-paths.ts";
51
56
 
52
57
  /* ------------------------------ constants ------------------------------ */
@@ -56,7 +61,10 @@ const WORKSPACE_FILE_SCOPES = ["workspace", "workspace.workspace", "all-apis"] a
56
61
 
57
62
  const logger = log.logger("mastra/workspaces");
58
63
  const MAX_SCOPED_WORKSPACE_FILESYSTEMS = 16;
59
- const scopedWorkspaceFilesystems = new Map<string, WorkspaceFilesystem>();
64
+ const scopedWorkspaceFilesystems = new LRUCache<string, WorkspaceFilesystem>({
65
+ max: MAX_SCOPED_WORKSPACE_FILESYSTEMS,
66
+ });
67
+ const workspaceCatalogueResolvers = new WeakMap<Workspace, SkillCatalogueResolver>();
60
68
 
61
69
  /* -------------------------------- types -------------------------------- */
62
70
 
@@ -102,6 +110,8 @@ export interface SkillFolderOptions {
102
110
  /** Mount map plus optional Mastra skill scan roots for one resolver. */
103
111
  export interface WorkspaceMountContribution {
104
112
  mounts: Record<string, WorkspaceFilesystem>;
113
+ /** Ordered source roots included in catalogue discovery. */
114
+ skillSources?: SkillCatalogueSource[];
105
115
  /** Paths within the composite namespace where `SKILL.md` files are scanned. */
106
116
  skillPaths?: string[];
107
117
  /**
@@ -156,16 +166,16 @@ export interface CreateWorkspaceOptions {
156
166
  /** Forwarded to Mastra when skill discovery is enabled. */
157
167
  checkSkillFileMtime?: boolean;
158
168
  /**
159
- * AppKit cache TTL for Databricks skill metadata and content.
160
- * Defaults to five minutes.
169
+ * Refresh TTL for the complete parsed skill catalogue. Auxiliary reads are
170
+ * always fresh. Defaults to five minutes.
161
171
  */
162
172
  workspaceSkillRefreshTtlMs?: number;
163
173
  /** Enable BM25 keyword search over indexed workspace content. */
164
174
  bm25?: boolean;
165
175
  /**
166
- * Command sandbox. Defaults to Databricks Sandbox. Pass `false` to disable
167
- * command execution, or an explicit Mastra sandbox/provider resolver to
168
- * replace Databricks for this workspace.
176
+ * Command sandbox. Defaults to Monty. Pass `"databricks"` or an options
177
+ * object for Databricks Sandbox, `false` to disable command execution, or an
178
+ * explicit Mastra sandbox/provider resolver.
169
179
  */
170
180
  sandbox?: WorkspaceSandboxSelection;
171
181
  /**
@@ -245,12 +255,10 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
245
255
  const resolvers = buildMountResolvers(
246
256
  skillFolders,
247
257
  [...(options.mounts ?? []), ...localSkillMountResolvers(extraSkillPaths)],
248
- options.workspaceSkillRefreshTtlMs,
249
258
  );
250
- const skills =
251
- options.skills ??
252
- (resolvers.length > 0 ? buildWorkspaceSkillsResolver(resolvers) : undefined);
253
- const checkSkillFileMtime = options.checkSkillFileMtime ?? folderNames.length > 0;
259
+ const resolveContribution = contributionResolver(resolvers);
260
+ const skills = options.skills;
261
+ const checkSkillFileMtime = options.checkSkillFileMtime ?? false;
254
262
  const bm25 = options.bm25 !== false;
255
263
  const sandbox = resolveWorkspaceSandbox(options.sandbox, id, name);
256
264
  logger.debug("workspace:create", {
@@ -268,10 +276,10 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
268
276
  sandbox: sandbox ? sandboxName(options.sandbox) : "disabled",
269
277
  });
270
278
 
271
- return new Workspace({
279
+ const workspace = new Workspace({
272
280
  id,
273
281
  name,
274
- filesystem: (context) => resolveWorkspaceFilesystem(resolvers, context),
282
+ filesystem: (context) => resolveWorkspaceFilesystem(resolveContribution, context),
275
283
  ...(skills
276
284
  ? {
277
285
  skills,
@@ -286,6 +294,20 @@ export function createWorkspace(options: CreateWorkspaceOptions = {}): Workspace
286
294
  : {}),
287
295
  bm25,
288
296
  });
297
+ if (!options.skills && resolvers.length > 0) {
298
+ workspaceCatalogueResolvers.set(
299
+ workspace,
300
+ buildSkillCatalogueResolver(resolveContribution, options.workspaceSkillRefreshTtlMs),
301
+ );
302
+ }
303
+ return workspace;
304
+ }
305
+
306
+ /** Return the dbx-tools catalogue resolver attached to one workspace. */
307
+ export function workspaceSkillCatalogueResolver(
308
+ workspace: Workspace | undefined,
309
+ ): SkillCatalogueResolver | undefined {
310
+ return workspace ? workspaceCatalogueResolvers.get(workspace) : undefined;
289
311
  }
290
312
 
291
313
  /**
@@ -316,7 +338,7 @@ function resolveWorkspaceSandbox(
316
338
  workspaceId: string,
317
339
  workspaceName: string,
318
340
  ): WorkspaceSandbox | WorkspaceSandboxResolver | undefined {
319
- const configured = selection ?? "databricks";
341
+ const configured = selection ?? "monty";
320
342
  if (configured === false) return undefined;
321
343
  if (configured === "monty") return new MontySandbox();
322
344
  if (typeof configured === "function" || isSandbox(configured)) return configured;
@@ -370,8 +392,8 @@ function isSandbox(value: unknown): value is WorkspaceSandbox {
370
392
  }
371
393
 
372
394
  function sandboxName(selection: WorkspaceSandboxSelection | undefined): string {
373
- if (selection === undefined || selection === "databricks") return "databricks";
374
- if (selection === "monty") return "monty";
395
+ if (selection === undefined || selection === "monty") return "monty";
396
+ if (selection === "databricks") return "databricks";
375
397
  if (selection === false) return "disabled";
376
398
  if (typeof selection === "function") return "resolver";
377
399
  return isSandbox(selection) ? selection.provider : (selection.provider ?? "databricks");
@@ -397,9 +419,9 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
397
419
  async function resolveSkillFolderMounts(
398
420
  skillFolders: Record<string, SkillFolderOptions>,
399
421
  context: WorkspaceMountContext,
400
- workspaceSkillRefreshTtlMs: number,
401
422
  ): Promise<WorkspaceMountContribution> {
402
423
  const mounts: Record<string, WorkspaceFilesystem> = {};
424
+ const skillSources: SkillCatalogueSource[] = [];
403
425
  const skillPaths: string[] = [];
404
426
  const cacheKeys: string[] = [];
405
427
  const requestContext = context.requestContext;
@@ -423,13 +445,15 @@ async function resolveSkillFolderMounts(
423
445
  client,
424
446
  host,
425
447
  userKey,
426
- workspaceSkillRefreshTtlMs,
427
448
  });
428
449
  if (!resolved) continue;
429
450
  const mount = folder.mount ?? `/${name}`;
430
451
  mounts[mount] = resolved.filesystem;
431
452
  if (resolved.cacheKey) cacheKeys.push(resolved.cacheKey);
432
- if (folder.readable !== false) skillPaths.push(mount);
453
+ if (folder.readable !== false) {
454
+ skillPaths.push(mount);
455
+ skillSources.push({ ...resolved.skillSource, id: object.toStableKey({ mount, source: resolved.skillSource.id }) });
456
+ }
433
457
  }
434
458
 
435
459
  logger.debug("skill-folders:mounted", {
@@ -439,6 +463,7 @@ async function resolveSkillFolderMounts(
439
463
 
440
464
  return {
441
465
  mounts,
466
+ skillSources,
442
467
  skillPaths,
443
468
  ...(cacheKeys.length === Object.keys(mounts).length && cacheKeys.length > 0
444
469
  ? { cacheKey: object.toStableKey(cacheKeys.sort()) }
@@ -458,12 +483,30 @@ async function resolveSkillFolderFilesystem(
458
483
  client: WorkspaceClient | undefined;
459
484
  host: string | undefined;
460
485
  userKey: string | undefined;
461
- workspaceSkillRefreshTtlMs: number;
462
486
  },
463
- ): Promise<{ cacheKey?: string; filesystem: WorkspaceFilesystem } | undefined> {
487
+ ): Promise<
488
+ | {
489
+ cacheKey?: string;
490
+ filesystem: WorkspaceFilesystem;
491
+ skillSource: SkillCatalogueSource;
492
+ }
493
+ | undefined
494
+ > {
464
495
  if (folder.filesystem !== undefined) {
465
496
  const filesystem = await resolveSkillFolderValue(folder.filesystem, context);
466
- return filesystem ? { filesystem } : undefined;
497
+ return filesystem
498
+ ? {
499
+ filesystem,
500
+ skillSource: mastraSkillSource(
501
+ object.toStableKey({
502
+ id: filesystem.id,
503
+ name,
504
+ provider: filesystem.provider,
505
+ }),
506
+ filesystem,
507
+ ),
508
+ }
509
+ : undefined;
467
510
  }
468
511
  // A path mount needs the request's OBO client to reach the workspace.
469
512
  if (folder.path === undefined || !cache.client || !cache.host || !cache.userKey) {
@@ -477,7 +520,6 @@ async function resolveSkillFolderFilesystem(
477
520
  if (!root) return undefined;
478
521
  return databricksFilesystem(cache.client, root, folder.writable !== true, {
479
522
  host: cache.host,
480
- ttlMs: cache.workspaceSkillRefreshTtlMs,
481
523
  userKey: cache.userKey,
482
524
  });
483
525
  }
@@ -515,14 +557,11 @@ function resolveWorkspaceIdentity(options: CreateWorkspaceOptions): {
515
557
  function buildMountResolvers(
516
558
  skillFolders: Record<string, SkillFolderOptions>,
517
559
  mounts: WorkspaceMountResolver[] | undefined,
518
- workspaceSkillRefreshTtlMs = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS,
519
560
  ): WorkspaceMountResolver[] {
520
561
  const resolvers: WorkspaceMountResolver[] = [];
521
562
  const folderCount = Object.keys(skillFolders).length;
522
563
  if (folderCount > 0) {
523
- resolvers.push((context) =>
524
- resolveSkillFolderMounts(skillFolders, context, workspaceSkillRefreshTtlMs),
525
- );
564
+ resolvers.push((context) => resolveSkillFolderMounts(skillFolders, context));
526
565
  }
527
566
  if (mounts?.length) {
528
567
  resolvers.push(...mounts);
@@ -539,21 +578,59 @@ function buildMountResolvers(
539
578
  function localSkillMountResolvers(paths: readonly string[]): WorkspaceMountResolver[] {
540
579
  return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].map((root, index) => {
541
580
  const mount = `/remote-skills/${index}`;
542
- const filesystem = filesystems(
543
- new LocalFileSystem({
544
- root,
545
- readOnly: true,
546
- createRoot: false,
547
- }),
548
- { readOnly: true },
549
- );
581
+ const source = new LocalFileSystem({
582
+ root,
583
+ readOnly: true,
584
+ createRoot: false,
585
+ });
586
+ const filesystem = filesystems(source, { readOnly: true });
550
587
  return () => ({
551
588
  mounts: { [mount]: filesystem },
589
+ skillSources: [sharedSkillSource(object.toStableKey({ mount, root }), source)],
552
590
  skillPaths: [mount],
591
+ cacheKey: object.toStableKey({ mount, root }),
553
592
  });
554
593
  });
555
594
  }
556
595
 
596
+ /** Adapt a portable dbx-tools filesystem to catalogue discovery callbacks. */
597
+ function sharedSkillSource(id: string, filesystem: FileSystem): SkillCatalogueSource {
598
+ return {
599
+ id,
600
+ async list(path) {
601
+ await filesystem.init();
602
+ return (await filesystem.readdir(path)).flatMap((entry) =>
603
+ entry.type === "file" || entry.type === "directory"
604
+ ? [
605
+ {
606
+ name: entry.name,
607
+ type: entry.type,
608
+ ...(entry.size !== undefined ? { size: entry.size } : {}),
609
+ ...(entry.metadata ? { metadata: entry.metadata } : {}),
610
+ },
611
+ ]
612
+ : [],
613
+ );
614
+ },
615
+ async read(path) {
616
+ await filesystem.init();
617
+ return filesystem.readFile(path, { encoding: "utf8" });
618
+ },
619
+ };
620
+ }
621
+
622
+ /** Adapt an arbitrary Mastra filesystem when no portable source is available. */
623
+ function mastraSkillSource(id: string, filesystem: WorkspaceFilesystem): SkillCatalogueSource {
624
+ return {
625
+ id,
626
+ list: (path) => filesystem.readdir(path),
627
+ async read(path) {
628
+ const value = await filesystem.readFile(path, { encoding: "utf8" });
629
+ return typeof value === "string" ? value : value.toString("utf8");
630
+ },
631
+ };
632
+ }
633
+
557
634
  /**
558
635
  * Gate skill-folder mounts on the request's token.
559
636
  *
@@ -579,23 +656,37 @@ async function databricksFilesystem(
579
656
  client: WorkspaceClient,
580
657
  root: string,
581
658
  readOnly: boolean = true,
582
- cache: { host: string; ttlMs: number; userKey: string },
583
- ): Promise<{ cacheKey: string; filesystem: WorkspaceFilesystem } | undefined> {
659
+ cache: { host: string; userKey: string },
660
+ ): Promise<
661
+ {
662
+ cacheKey: string;
663
+ filesystem: WorkspaceFilesystem;
664
+ skillSource: SkillCatalogueSource;
665
+ }
666
+ | undefined
667
+ > {
584
668
  const fs = new DatabricksFileSystem({
585
669
  client: workspaceClient.toLegacyWorkspaceClient(client),
586
670
  root,
587
671
  readOnly,
588
672
  createRoot: !readOnly,
589
673
  });
590
- const cached = cachedWorkspaceSkillMount({
674
+ const cacheKey = object.toStableKey({
591
675
  host: cache.host,
592
- source: fs,
593
- ttlMs: cache.ttlMs,
676
+ readOnly,
677
+ root,
594
678
  userKey: cache.userKey,
595
679
  });
680
+ const filesystem = filesystems(fs, { readOnly });
596
681
  try {
597
- await cached.filesystem.init();
598
- if (await cached.filesystem.exists(".")) return cached;
682
+ await filesystem.init();
683
+ if (await filesystem.exists(".")) {
684
+ return {
685
+ cacheKey,
686
+ filesystem,
687
+ skillSource: sharedSkillSource(cacheKey, fs),
688
+ };
689
+ }
599
690
  } catch (err) {
600
691
  logger.debug("databricks-mount:scratch-fallback", {
601
692
  root,
@@ -615,6 +706,7 @@ async function resolveWorkspaceContribution(
615
706
  context: WorkspaceMountContext,
616
707
  ): Promise<WorkspaceMountContribution> {
617
708
  const mounts: Record<string, WorkspaceFilesystem> = {};
709
+ const skillSources: SkillCatalogueSource[] = [];
618
710
  const skillPaths: string[] = [];
619
711
  const cacheKeys: string[] = [];
620
712
  let cacheable = true;
@@ -634,6 +726,9 @@ async function resolveWorkspaceContribution(
634
726
  if (contribution.skillPaths?.length) {
635
727
  skillPaths.push(...contribution.skillPaths);
636
728
  }
729
+ if (contribution.skillSources?.length) {
730
+ skillSources.push(...contribution.skillSources);
731
+ }
637
732
  }
638
733
 
639
734
  logger.debug("mounts:merged", {
@@ -643,9 +738,10 @@ async function resolveWorkspaceContribution(
643
738
 
644
739
  return {
645
740
  mounts,
741
+ skillSources,
646
742
  skillPaths,
647
743
  ...(cacheable && cacheKeys.length > 0
648
- ? { cacheKey: object.toStableKey(cacheKeys.sort()) }
744
+ ? { cacheKey: object.toStableKey({ mounts: Object.keys(mounts), sources: cacheKeys }) }
649
745
  : {}),
650
746
  };
651
747
  }
@@ -657,10 +753,10 @@ async function resolveWorkspaceContribution(
657
753
  * fresh {@link scratchFilesystem} so Mastra always has a writable local root.
658
754
  */
659
755
  async function resolveWorkspaceFilesystem(
660
- resolvers: WorkspaceMountResolver[],
756
+ resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
661
757
  context: WorkspaceMountContext,
662
758
  ): Promise<WorkspaceFilesystem> {
663
- const { cacheKey, mounts } = await resolveWorkspaceContribution(resolvers, context);
759
+ const { cacheKey, mounts } = await resolveContribution(context);
664
760
  const mountKeys = Object.keys(mounts);
665
761
  if (mountKeys.length === 0) {
666
762
  logger.debug("filesystem:scratch", {
@@ -671,104 +767,46 @@ async function resolveWorkspaceFilesystem(
671
767
  logger.debug("filesystem:composite", { mountKeys });
672
768
  if (!cacheKey) return new CompositeFilesystem({ mounts });
673
769
  const existing = scopedWorkspaceFilesystems.get(cacheKey);
674
- if (existing) {
675
- scopedWorkspaceFilesystems.delete(cacheKey);
676
- scopedWorkspaceFilesystems.set(cacheKey, existing);
677
- return existing;
678
- }
770
+ if (existing) return existing;
679
771
  const filesystem = new CompositeFilesystem({ mounts });
680
772
  scopedWorkspaceFilesystems.set(cacheKey, filesystem);
681
- while (scopedWorkspaceFilesystems.size > MAX_SCOPED_WORKSPACE_FILESYSTEMS) {
682
- const oldest = scopedWorkspaceFilesystems.keys().next().value;
683
- if (oldest === undefined) break;
684
- scopedWorkspaceFilesystems.delete(oldest);
685
- }
686
773
  return filesystem;
687
774
  }
688
775
 
689
- /**
690
- * Build the dynamic {@link SkillsResolver} that collects `skillPaths` from
691
- * every mount resolver on each request.
692
- */
693
- function buildWorkspaceSkillsResolver(
776
+ /** Memoize one complete mount contribution for each request context. */
777
+ function contributionResolver(
694
778
  resolvers: WorkspaceMountResolver[],
695
- ): SkillsResolver {
696
- return async (context: SkillsContext) => {
697
- const contribution = await resolveWorkspaceContribution(resolvers, context);
698
- const skillPaths = await uniqueSkillPaths(contribution);
699
- logger.debug("skills:resolved", {
700
- configuredSkillPaths: contribution.skillPaths,
701
- skillPaths,
702
- });
703
- return skillPaths;
704
- };
705
- }
706
-
707
- /** Expand mounted roots and keep the first concrete skill for each directory name. */
708
- async function uniqueSkillPaths(contribution: WorkspaceMountContribution): Promise<string[]> {
709
- const selected = new Map<string, string>();
710
- const unresolved: string[] = [];
711
- for (const root of contribution.skillPaths ?? []) {
712
- const mount = mountedFilesystem(root, contribution.mounts);
713
- if (!mount) {
714
- unresolved.push(root);
715
- continue;
779
+ ): (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution> {
780
+ const scoped = new WeakMap<RequestContext, Promise<WorkspaceMountContribution>>();
781
+ let unscoped: Promise<WorkspaceMountContribution> | undefined;
782
+ return (context) => {
783
+ if (!context.requestContext) {
784
+ unscoped ??= resolveWorkspaceContribution(resolvers, context);
785
+ return unscoped;
716
786
  }
717
- let candidates: string[];
718
- try {
719
- candidates = await concreteSkillPaths(root, mount.path, mount.filesystem);
720
- } catch (error) {
721
- logger.debug("skills:root-expansion-failed", {
722
- root,
723
- error: errorUtils.errorMessage(error),
724
- });
725
- unresolved.push(root);
726
- continue;
727
- }
728
- if (candidates.length === 0) {
729
- unresolved.push(root);
730
- continue;
731
- }
732
- for (const candidate of candidates) {
733
- const name = posixPath.basename(candidate);
734
- const existing = selected.get(name);
735
- if (existing) {
736
- logger.debug("skills:duplicate-skipped", { name, path: candidate, selected: existing });
737
- } else {
738
- selected.set(name, candidate);
739
- }
740
- }
741
- }
742
- return [...selected.values(), ...unresolved];
743
- }
744
-
745
- /** Find the most-specific mounted filesystem containing one skill path. */
746
- function mountedFilesystem(
747
- skillPath: string,
748
- mounts: Readonly<Record<string, WorkspaceFilesystem>>,
749
- ): { filesystem: WorkspaceFilesystem; path: string } | undefined {
750
- return Object.entries(mounts)
751
- .filter(([mount]) => skillPath === mount || skillPath.startsWith(`${mount}/`))
752
- .sort(([left], [right]) => right.length - left.length)
753
- .map(([path, filesystem]) => ({ filesystem, path }))[0];
787
+ const existing = scoped.get(context.requestContext);
788
+ if (existing) return existing;
789
+ const pending = resolveWorkspaceContribution(resolvers, context);
790
+ scoped.set(context.requestContext, pending);
791
+ return pending;
792
+ };
754
793
  }
755
794
 
756
- /** Return concrete child skill directories from one mounted skill root. */
757
- async function concreteSkillPaths(
758
- root: string,
759
- mount: string,
760
- filesystem: WorkspaceFilesystem,
761
- ): Promise<string[]> {
762
- const relativeRoot = root === mount ? "." : root.slice(mount.length + 1);
763
- if (await filesystem.exists(posixPath.join(relativeRoot, "SKILL.md"))) return [root];
764
- const entries = await filesystem.readdir(relativeRoot);
765
- const candidates: string[] = [];
766
- for (const entry of entries) {
767
- if (entry.type !== "directory") continue;
768
- const relative = posixPath.join(relativeRoot, entry.name);
769
- if (await filesystem.exists(posixPath.join(relative, "SKILL.md"))) {
770
- candidates.push(posixPath.join(root, entry.name));
771
- }
772
- }
773
- return candidates;
795
+ /** Resolve one semantic catalogue owner from a request's ordered skill roots. */
796
+ function buildSkillCatalogueResolver(
797
+ resolveContribution: (context: WorkspaceMountContext) => Promise<WorkspaceMountContribution>,
798
+ ttlMs: number | undefined,
799
+ ): SkillCatalogueResolver {
800
+ return async ({ requestContext }) => {
801
+ const contribution = await resolveContribution({ requestContext });
802
+ const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
803
+ const client = user?.executionContext.client as WorkspaceClient | undefined;
804
+ const host = client ? (await client.config.getHost()).toString() : "local";
805
+ return workspaceSkillCatalogue({
806
+ host,
807
+ sources: contribution.skillSources ?? [],
808
+ ttlMs,
809
+ userKey: user?.id ?? "service",
810
+ });
811
+ };
774
812
  }