@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/README.md +53 -42
- package/index.ts +8 -3
- package/lib/index.d.ts +8 -3
- package/lib/index.js +6 -3
- package/lib/src/agents.js +38 -5
- package/lib/src/config.d.ts +7 -7
- package/lib/src/config.js +4 -4
- package/lib/src/genie.js +9 -15
- package/lib/src/mlflow.js +5 -5
- package/lib/src/monty-native.d.ts +40 -0
- package/lib/src/monty-native.js +100 -0
- package/lib/src/monty-sandbox.js +7 -3
- package/lib/src/remote-skills.js +26 -2
- package/lib/src/skill-cache.d.ts +80 -56
- package/lib/src/skill-cache.js +215 -158
- package/lib/src/skill-search.d.ts +30 -0
- package/lib/src/skill-search.js +171 -0
- package/lib/src/workspaces.d.ts +10 -5
- package/lib/src/workspaces.js +132 -117
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -15
- package/src/agents.ts +39 -6
- package/src/config.ts +10 -10
- package/src/genie.ts +8 -13
- package/src/mlflow.ts +4 -4
- package/src/monty-native.ts +149 -0
- package/src/monty-sandbox.ts +7 -2
- package/src/remote-skills.ts +32 -1
- package/src/skill-cache.ts +278 -243
- package/src/skill-search.ts +203 -0
- package/src/workspaces.ts +177 -139
package/src/skill-cache.ts
CHANGED
|
@@ -1,312 +1,347 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* User-scoped workspace skill catalogues cached as one semantic record.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
7
|
import { CacheManager } from "@databricks/appkit";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
8
|
+
import { errorUtils, log, object } from "@dbx-tools/shared-core";
|
|
9
|
+
import { posixPath } from "@dbx-tools/shared-fs";
|
|
10
|
+
import type { RequestContext } from "@mastra/core/request-context";
|
|
11
|
+
import { LRUCache } from "lru-cache";
|
|
12
|
+
import { parse as parseYaml } from "yaml";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
const CACHE_NAMESPACE = "mastra:workspace-skill-catalogue";
|
|
16
|
+
const CATALOGUE_VERSION = 1;
|
|
17
|
+
const MAX_SCOPED_CATALOGUES = 256;
|
|
18
|
+
const logger = log.logger("mastra/skill-cache");
|
|
19
|
+
|
|
20
|
+
const SkillCatalogueEntrySchema = z
|
|
21
|
+
.object({
|
|
22
|
+
name: z.string().describe("Unique skill name from SKILL.md frontmatter."),
|
|
23
|
+
description: z.string().describe("Skill description from SKILL.md frontmatter."),
|
|
24
|
+
instructions: z.string().describe("Markdown instructions below SKILL.md frontmatter."),
|
|
25
|
+
sourceId: z.string().describe("Identity of the mounted source that owns this skill."),
|
|
26
|
+
sourcePath: z.string().describe("Path to SKILL.md relative to its source root."),
|
|
27
|
+
signature: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Stable metadata signature used to retain unchanged instructions."),
|
|
31
|
+
})
|
|
32
|
+
.describe("One parsed skill in a materialized workspace catalogue.");
|
|
33
|
+
|
|
34
|
+
const SkillCatalogueSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
version: z.literal(CATALOGUE_VERSION).describe("Persisted catalogue schema version."),
|
|
37
|
+
generatedAt: z.string().datetime().describe("Time the catalogue was last refreshed."),
|
|
38
|
+
roots: z.array(z.string()).describe("Ordered source identities included in the catalogue."),
|
|
39
|
+
skills: z
|
|
40
|
+
.array(SkillCatalogueEntrySchema)
|
|
41
|
+
.describe("Parsed skills after ordered duplicate-name precedence is applied."),
|
|
42
|
+
})
|
|
43
|
+
.describe("One user-scoped materialized workspace skill catalogue.");
|
|
44
|
+
|
|
45
|
+
/** Parsed skill retained in a materialized workspace catalogue. */
|
|
46
|
+
export type SkillCatalogueEntry = z.infer<typeof SkillCatalogueEntrySchema>;
|
|
47
|
+
|
|
48
|
+
/** Versioned persisted skill catalogue. */
|
|
49
|
+
export type SkillCatalogue = z.infer<typeof SkillCatalogueSchema>;
|
|
50
|
+
|
|
51
|
+
/** One ordered filesystem root contributing skills to a catalogue. */
|
|
52
|
+
export interface SkillCatalogueSource {
|
|
53
|
+
/** Stable source identity including its mount precedence. */
|
|
54
|
+
id: string;
|
|
55
|
+
/** List one source-relative directory with provider metadata when available. */
|
|
56
|
+
list(path: string): Promise<SkillCatalogueFileEntry[]>;
|
|
57
|
+
/** Read one source-relative file fresh from its owning filesystem. */
|
|
58
|
+
read(path: string): Promise<string>;
|
|
59
|
+
}
|
|
27
60
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
/** Minimal directory metadata used to compare one catalogue refresh. */
|
|
62
|
+
export interface SkillCatalogueFileEntry {
|
|
63
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
64
|
+
name: string;
|
|
65
|
+
size?: number;
|
|
66
|
+
type: "directory" | "file";
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cache: AppKitCachedFileSystem;
|
|
69
|
+
/** Options for one user-scoped workspace skill catalogue. */
|
|
70
|
+
export interface WorkspaceSkillCatalogueOptions {
|
|
37
71
|
host: string;
|
|
38
|
-
|
|
72
|
+
sources: readonly SkillCatalogueSource[];
|
|
73
|
+
ttlMs?: number;
|
|
39
74
|
userKey: string;
|
|
40
75
|
}
|
|
41
76
|
|
|
42
|
-
|
|
77
|
+
/** Request state used to resolve one user-scoped catalogue owner. */
|
|
78
|
+
export interface SkillCatalogueResolveContext {
|
|
79
|
+
abortSignal?: AbortSignal;
|
|
80
|
+
requestContext?: RequestContext;
|
|
81
|
+
}
|
|
43
82
|
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
83
|
+
/** Resolve one request's complete skill catalogue. */
|
|
84
|
+
export type SkillCatalogueResolver = (
|
|
85
|
+
context: SkillCatalogueResolveContext,
|
|
86
|
+
) => Promise<WorkspaceSkillCatalogue>;
|
|
46
87
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ttlMs?: number;
|
|
52
|
-
userKey: string;
|
|
88
|
+
interface ParsedSkill {
|
|
89
|
+
description: string;
|
|
90
|
+
instructions: string;
|
|
91
|
+
name: string;
|
|
53
92
|
}
|
|
54
93
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
filesystem: MastraFileSystemAdapter;
|
|
59
|
-
}
|
|
94
|
+
const scopedCatalogues = new LRUCache<string, WorkspaceSkillCatalogue>({
|
|
95
|
+
max: MAX_SCOPED_CATALOGUES,
|
|
96
|
+
});
|
|
60
97
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
export class
|
|
66
|
-
readonly
|
|
67
|
-
readonly
|
|
68
|
-
readonly
|
|
69
|
-
readonly root: string;
|
|
98
|
+
/** Default lifetime for one complete workspace skill catalogue. */
|
|
99
|
+
export const DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
100
|
+
|
|
101
|
+
/** Materialized catalogue with one AppKit cache record and fresh auxiliary reads. */
|
|
102
|
+
export class WorkspaceSkillCatalogue {
|
|
103
|
+
readonly cacheKey: string;
|
|
104
|
+
readonly host: string;
|
|
105
|
+
readonly userKey: string;
|
|
70
106
|
|
|
71
|
-
private
|
|
72
|
-
private readonly
|
|
73
|
-
private readonly
|
|
74
|
-
private readonly userKey: string;
|
|
107
|
+
private lastValid?: SkillCatalogue;
|
|
108
|
+
private readonly ttlMs: number;
|
|
109
|
+
private sources: readonly SkillCatalogueSource[];
|
|
75
110
|
|
|
76
|
-
constructor(options:
|
|
111
|
+
constructor(private readonly options: WorkspaceSkillCatalogueOptions) {
|
|
77
112
|
this.host = options.host;
|
|
78
|
-
this.source = options.source;
|
|
79
|
-
this.root = options.source.root;
|
|
80
|
-
this.readOnly = options.source.readOnly;
|
|
81
|
-
this.ttlSec = cacheTtlSec(options.ttlMs);
|
|
82
113
|
this.userKey = options.userKey;
|
|
83
|
-
this.
|
|
114
|
+
this.sources = options.sources;
|
|
115
|
+
this.ttlMs = positiveTtl(options.ttlMs);
|
|
116
|
+
this.cacheKey = object.toStableKey({
|
|
117
|
+
host: options.host,
|
|
118
|
+
roots: options.sources.map(({ id }) => id),
|
|
119
|
+
userKey: options.userKey,
|
|
120
|
+
version: CATALOGUE_VERSION,
|
|
121
|
+
});
|
|
84
122
|
}
|
|
85
123
|
|
|
86
|
-
/** Rebind
|
|
87
|
-
bind(
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
/** Rebind refreshes and lazy reads to the current request's filesystem clients. */
|
|
125
|
+
bind(sources: readonly SkillCatalogueSource[]): void {
|
|
126
|
+
const identities = sources.map(({ id }) => id);
|
|
127
|
+
if (object.toStableKey(identities) !== object.toStableKey(this.sources.map(({ id }) => id))) {
|
|
128
|
+
throw new Error("Cannot rebind a skill catalogue to a different ordered root set");
|
|
90
129
|
}
|
|
91
|
-
this.
|
|
130
|
+
this.sources = sources;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
/** Return the cached catalogue, refreshing all roots once on cache miss. */
|
|
134
|
+
async get(signal?: AbortSignal): Promise<SkillCatalogue> {
|
|
135
|
+
const cache = CacheManager.getInstanceSync();
|
|
136
|
+
try {
|
|
137
|
+
const catalogue = SkillCatalogueSchema.parse(
|
|
138
|
+
await cache.getOrExecute(
|
|
139
|
+
[CACHE_NAMESPACE, CATALOGUE_VERSION, this.options.host, this.rootKey()],
|
|
140
|
+
() => this.refresh(),
|
|
141
|
+
this.options.userKey,
|
|
142
|
+
{
|
|
143
|
+
ttl: Math.max(1, Math.ceil(this.ttlMs / 1000)),
|
|
144
|
+
callerSignal: signal,
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
this.lastValid = catalogue;
|
|
149
|
+
return catalogue;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!this.lastValid) throw error;
|
|
152
|
+
logger.warn("skill catalogue refresh failed; retaining last valid catalogue", {
|
|
153
|
+
error: errorUtils.errorMessage(error),
|
|
154
|
+
roots: this.sources.map(({ id }) => id),
|
|
155
|
+
});
|
|
156
|
+
return this.lastValid;
|
|
157
|
+
}
|
|
105
158
|
}
|
|
106
159
|
|
|
107
|
-
|
|
108
|
-
async
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
): Promise<string>;
|
|
112
|
-
async readFile(inputPath: string, options?: ReadFileOptions): Promise<string | Uint8Array> {
|
|
113
|
-
const value = await this.cached("readFile", inputPath, undefined, async (source) => ({
|
|
114
|
-
base64: Buffer.from(await source.readFile(inputPath)).toString("base64"),
|
|
115
|
-
}));
|
|
116
|
-
const bytes = Buffer.from(value.base64, "base64");
|
|
117
|
-
return options?.encoding ? bytes.toString(options.encoding as BufferEncoding) : bytes;
|
|
160
|
+
/** Expire this complete catalogue without touching unrelated cache entries. */
|
|
161
|
+
async invalidate(): Promise<void> {
|
|
162
|
+
const cache = CacheManager.getInstanceSync();
|
|
163
|
+
await cache.delete(this.persistentKey(cache));
|
|
118
164
|
}
|
|
119
165
|
|
|
120
|
-
|
|
166
|
+
/** Read one selected skill's auxiliary file directly from its current source. */
|
|
167
|
+
async readSkillFile(
|
|
168
|
+
skillName: string,
|
|
121
169
|
inputPath: string,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await this.mutate((source) => source.copyFile(sourcePath, destinationPath, options));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async moveFile(
|
|
145
|
-
sourcePath: string,
|
|
146
|
-
destinationPath: string,
|
|
147
|
-
options?: CopyOptions,
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
await this.mutate((source) => source.moveFile(sourcePath, destinationPath, options));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async mkdir(inputPath: string, options?: MakeDirectoryOptions): Promise<void> {
|
|
153
|
-
await this.mutate((source) => source.mkdir(inputPath, options));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async rmdir(inputPath: string, options?: RemoveOptions): Promise<void> {
|
|
157
|
-
await this.mutate((source) => source.rmdir(inputPath, options));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async readdir(inputPath: string, options?: ListOptions): Promise<FileEntry[]> {
|
|
161
|
-
return this.cached("readdir", inputPath, options, (source) =>
|
|
162
|
-
source.readdir(inputPath, options),
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async exists(inputPath: string): Promise<boolean> {
|
|
167
|
-
return this.cached("exists", inputPath, undefined, (source) => source.exists(inputPath));
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
): Promise<string> {
|
|
172
|
+
signal?.throwIfAborted();
|
|
173
|
+
const catalogue = await this.get(signal);
|
|
174
|
+
const skill = catalogue.skills.find(({ name }) => name === skillName);
|
|
175
|
+
if (!skill) throw new Error(`Unknown skill: ${skillName}`);
|
|
176
|
+
const normalized = posixPath.normalize(inputPath);
|
|
177
|
+
if (
|
|
178
|
+
!normalized.ok ||
|
|
179
|
+
normalized.path === "/" ||
|
|
180
|
+
inputPath.split("/").some((segment) => segment === "..")
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(`Invalid skill file path: ${inputPath}`);
|
|
183
|
+
}
|
|
184
|
+
const source = this.sources.find(({ id }) => id === skill.sourceId);
|
|
185
|
+
if (!source) throw new Error(`Skill source is no longer mounted: ${skill.sourceId}`);
|
|
186
|
+
const parent = skill.sourcePath.split("/").slice(0, -1).join("/");
|
|
187
|
+
const path = [parent, normalized.path.replace(/^\/+/, "")].filter(Boolean).join("/");
|
|
188
|
+
return source.read(path);
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
async
|
|
171
|
-
const
|
|
172
|
-
|
|
191
|
+
private async refresh(): Promise<SkillCatalogue> {
|
|
192
|
+
const previous = new Map(
|
|
193
|
+
this.lastValid?.skills.map((skill) => [`${skill.sourceId}\0${skill.sourcePath}`, skill]) ?? [],
|
|
173
194
|
);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
const selected = new Map<string, SkillCatalogueEntry>();
|
|
196
|
+
for (const source of this.sources) {
|
|
197
|
+
for (const candidate of await discoverSkillFiles(source)) {
|
|
198
|
+
const prior = previous.get(`${source.id}\0${candidate.path}`);
|
|
199
|
+
const signature = fileSignature(candidate.entry);
|
|
200
|
+
let parsed: ParsedSkill | undefined;
|
|
201
|
+
if (signature && prior?.signature === signature) {
|
|
202
|
+
parsed = prior;
|
|
203
|
+
} else {
|
|
204
|
+
parsed = parseSkill(
|
|
205
|
+
await source.read(candidate.path),
|
|
206
|
+
candidate.path,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!parsed || selected.has(parsed.name)) continue;
|
|
210
|
+
selected.set(parsed.name, {
|
|
211
|
+
...parsed,
|
|
212
|
+
sourceId: source.id,
|
|
213
|
+
sourcePath: candidate.path,
|
|
214
|
+
...(signature ? { signature } : {}),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return SkillCatalogueSchema.parse({
|
|
219
|
+
version: CATALOGUE_VERSION,
|
|
220
|
+
generatedAt: new Date().toISOString(),
|
|
221
|
+
roots: this.sources.map(({ id }) => id),
|
|
222
|
+
skills: [...selected.values()],
|
|
182
223
|
});
|
|
183
224
|
}
|
|
184
225
|
|
|
185
|
-
private
|
|
186
|
-
|
|
187
|
-
inputPath: string,
|
|
188
|
-
options: unknown,
|
|
189
|
-
load: (source: FileSystem) => Promise<T>,
|
|
190
|
-
): Promise<T> {
|
|
191
|
-
const source = this.source;
|
|
192
|
-
const cache = await CacheManager.getInstance();
|
|
193
|
-
const generation = await this.generation(cache);
|
|
194
|
-
return cache.getOrExecute(
|
|
195
|
-
[
|
|
196
|
-
CACHE_NAMESPACE,
|
|
197
|
-
this.host,
|
|
198
|
-
this.root,
|
|
199
|
-
generation,
|
|
200
|
-
operation,
|
|
201
|
-
source.resolvePath(inputPath),
|
|
202
|
-
object.toStableKey(options ?? {}),
|
|
203
|
-
],
|
|
204
|
-
() => load(source),
|
|
205
|
-
this.userKey,
|
|
206
|
-
{ ttl: this.ttlSec },
|
|
207
|
-
);
|
|
226
|
+
private rootKey(): string {
|
|
227
|
+
return object.toStableKey(this.sources.map(({ id }) => id));
|
|
208
228
|
}
|
|
209
229
|
|
|
210
|
-
private
|
|
211
|
-
return cache.
|
|
212
|
-
[CACHE_NAMESPACE,
|
|
213
|
-
|
|
214
|
-
this.userKey,
|
|
215
|
-
{ ttl: this.ttlSec * 2 },
|
|
230
|
+
private persistentKey(cache: CacheManager): string {
|
|
231
|
+
return cache.generateKey(
|
|
232
|
+
[CACHE_NAMESPACE, CATALOGUE_VERSION, this.options.host, this.rootKey()],
|
|
233
|
+
this.options.userKey,
|
|
216
234
|
);
|
|
217
235
|
}
|
|
218
|
-
|
|
219
|
-
private generationKey(cache: CacheManager): string {
|
|
220
|
-
return cache.generateKey([CACHE_NAMESPACE, "generation", this.host, this.root], this.userKey);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private async mutate(run: (source: FileSystem) => Promise<void>): Promise<void> {
|
|
224
|
-
const source = this.source;
|
|
225
|
-
await run(source);
|
|
226
|
-
await this.invalidate();
|
|
227
|
-
}
|
|
228
236
|
}
|
|
229
237
|
|
|
230
|
-
/** Return one stable
|
|
231
|
-
export function
|
|
232
|
-
options:
|
|
233
|
-
):
|
|
234
|
-
const
|
|
238
|
+
/** Return one stable catalogue owner for a user, host, and ordered root set. */
|
|
239
|
+
export function workspaceSkillCatalogue(
|
|
240
|
+
options: WorkspaceSkillCatalogueOptions,
|
|
241
|
+
): WorkspaceSkillCatalogue {
|
|
242
|
+
const key = object.toStableKey({
|
|
235
243
|
host: options.host,
|
|
236
|
-
|
|
237
|
-
root: options.source.root,
|
|
244
|
+
roots: options.sources.map(({ id }) => id),
|
|
238
245
|
userKey: options.userKey,
|
|
239
246
|
});
|
|
240
|
-
const existing =
|
|
247
|
+
const existing = scopedCatalogues.get(key);
|
|
241
248
|
if (existing) {
|
|
242
|
-
existing.
|
|
243
|
-
|
|
244
|
-
scopedMounts.set(cacheKey, existing);
|
|
245
|
-
return { cacheKey, filesystem: existing.adapter };
|
|
249
|
+
existing.bind(options.sources);
|
|
250
|
+
return existing;
|
|
246
251
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
adapter: filesystems(cache, {
|
|
251
|
-
id: cache.id,
|
|
252
|
-
readOnly: options.source.readOnly,
|
|
253
|
-
}),
|
|
254
|
-
cache,
|
|
255
|
-
host: options.host,
|
|
256
|
-
root: options.source.root,
|
|
257
|
-
userKey: options.userKey,
|
|
258
|
-
};
|
|
259
|
-
scopedMounts.set(cacheKey, entry);
|
|
260
|
-
trimScopedMounts();
|
|
261
|
-
return { cacheKey, filesystem: entry.adapter };
|
|
252
|
+
const catalogue = new WorkspaceSkillCatalogue(options);
|
|
253
|
+
scopedCatalogues.set(key, catalogue);
|
|
254
|
+
return catalogue;
|
|
262
255
|
}
|
|
263
256
|
|
|
264
|
-
/** Invalidate
|
|
257
|
+
/** Invalidate matching complete catalogues and retain their stable owners. */
|
|
265
258
|
export async function clearWorkspaceSkillCache(options: {
|
|
266
259
|
host: string;
|
|
267
|
-
|
|
268
|
-
userKey: string;
|
|
260
|
+
userKey?: string;
|
|
269
261
|
}): Promise<void> {
|
|
270
|
-
const matches = [...
|
|
271
|
-
(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
(options.root === undefined || entry.root === options.root),
|
|
262
|
+
const matches = [...scopedCatalogues.values()].filter(
|
|
263
|
+
(catalogue) =>
|
|
264
|
+
catalogue.host === options.host &&
|
|
265
|
+
(options.userKey === undefined || catalogue.userKey === options.userKey),
|
|
275
266
|
);
|
|
276
|
-
await Promise.all(matches.map((
|
|
267
|
+
await Promise.all(matches.map((catalogue) => catalogue.invalidate()));
|
|
277
268
|
}
|
|
278
269
|
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
270
|
+
async function discoverSkillFiles(
|
|
271
|
+
source: SkillCatalogueSource,
|
|
272
|
+
): Promise<Array<{ entry: SkillCatalogueFileEntry; path: string }>> {
|
|
273
|
+
let rootEntries: SkillCatalogueFileEntry[];
|
|
274
|
+
try {
|
|
275
|
+
rootEntries = await source.list(".");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (errorUtils.errorContext(error).notAccessible) return [];
|
|
278
|
+
throw error;
|
|
282
279
|
}
|
|
283
|
-
|
|
280
|
+
const candidates: Array<{ entry: SkillCatalogueFileEntry; path: string }> = [];
|
|
281
|
+
const rootSkill = rootEntries.find(
|
|
282
|
+
({ name, type }) => name.toLowerCase() === "skill.md" && type === "file",
|
|
283
|
+
);
|
|
284
|
+
if (rootSkill) candidates.push({ entry: rootSkill, path: rootSkill.name });
|
|
285
|
+
await Promise.all(
|
|
286
|
+
rootEntries
|
|
287
|
+
.filter(({ type }) => type === "directory")
|
|
288
|
+
.map(async (directory) => {
|
|
289
|
+
const entries = await source.list(directory.name);
|
|
290
|
+
const skill = entries.find(
|
|
291
|
+
({ name, type }) => name.toLowerCase() === "skill.md" && type === "file",
|
|
292
|
+
);
|
|
293
|
+
if (skill) candidates.push({ entry: skill, path: posixPath.join(directory.name, skill.name) });
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
return candidates.sort((left, right) => left.path.localeCompare(right.path));
|
|
284
297
|
}
|
|
285
298
|
|
|
286
|
-
function
|
|
287
|
-
const
|
|
299
|
+
function parseSkill(source: string, path: string): ParsedSkill | undefined {
|
|
300
|
+
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(source);
|
|
301
|
+
if (!match) {
|
|
302
|
+
logger.warn("skill skipped (missing YAML frontmatter)", { path });
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
const metadata = parseYaml(match[1]!) as unknown;
|
|
306
|
+
if (!object.isRecord(metadata)) {
|
|
307
|
+
logger.warn("skill skipped (invalid YAML frontmatter)", { path });
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const parsed = z
|
|
311
|
+
.object({
|
|
312
|
+
name: z.string().trim().min(1),
|
|
313
|
+
description: z.string().trim().min(1),
|
|
314
|
+
})
|
|
315
|
+
.safeParse(metadata);
|
|
316
|
+
if (!parsed.success) {
|
|
317
|
+
logger.warn("skill skipped (invalid metadata)", {
|
|
318
|
+
path,
|
|
319
|
+
error: parsed.error.message,
|
|
320
|
+
});
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
288
323
|
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
...(modifiedAt ? { modifiedAt: modifiedAt.getTime() } : {}),
|
|
324
|
+
name: parsed.data.name,
|
|
325
|
+
description: parsed.data.description,
|
|
326
|
+
instructions: match[2]!.trim(),
|
|
293
327
|
};
|
|
294
328
|
}
|
|
295
329
|
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
...(modifiedAt !== undefined ? { modifiedAt: new Date(modifiedAt) } : {}),
|
|
330
|
+
function fileSignature(entry: SkillCatalogueFileEntry): string | undefined {
|
|
331
|
+
const metadata = object.isRecord(entry.metadata) ? entry.metadata : {};
|
|
332
|
+
const values = {
|
|
333
|
+
modifiedAt: metadata.modifiedAt,
|
|
334
|
+
objectId: metadata.objectId,
|
|
335
|
+
size: entry.size,
|
|
303
336
|
};
|
|
337
|
+
return Object.values(values).some((value) => value !== undefined)
|
|
338
|
+
? object.toStableKey(values)
|
|
339
|
+
: undefined;
|
|
304
340
|
}
|
|
305
341
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (oldest === undefined) return;
|
|
310
|
-
scopedMounts.delete(oldest);
|
|
342
|
+
function positiveTtl(value = DEFAULT_WORKSPACE_SKILL_CACHE_TTL_MS): number {
|
|
343
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
344
|
+
throw new RangeError("Workspace skill catalogue ttlMs must be a positive finite number");
|
|
311
345
|
}
|
|
346
|
+
return value;
|
|
312
347
|
}
|