@dbx-tools/appkit-mastra 0.6.8 → 0.6.10
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 +49 -2
- package/index.ts +14 -26
- package/lib/index.d.ts +14 -26
- package/lib/index.js +13 -26
- package/lib/src/config.d.ts +21 -0
- package/lib/src/config.js +5 -1
- package/lib/src/databricks-aitools.d.ts +73 -0
- package/lib/src/databricks-aitools.js +152 -0
- package/lib/src/filesystems.d.ts +52 -170
- package/lib/src/filesystems.js +168 -888
- package/lib/src/plugin.js +14 -2
- package/lib/src/remote-skills.d.ts +4 -4
- package/lib/src/remote-skills.js +63 -35
- package/lib/src/workspaces.d.ts +4 -0
- package/lib/src/workspaces.js +37 -13
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -10
- package/src/config.ts +27 -0
- package/src/databricks-aitools.ts +216 -0
- package/src/filesystems.ts +225 -970
- package/src/plugin.ts +14 -1
- package/src/remote-skills.ts +69 -38
- package/src/workspaces.ts +41 -16
package/src/plugin.ts
CHANGED
|
@@ -77,6 +77,7 @@ import type { Pool } from "pg";
|
|
|
77
77
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.ts";
|
|
78
78
|
import { fetchChart } from "./chart.ts";
|
|
79
79
|
import { attributedUserId, MASTRA_CONFIG_SCHEMA, type MastraPluginConfig } from "./config.ts";
|
|
80
|
+
import { provisionDatabricksAITools } from "./databricks-aitools.ts";
|
|
80
81
|
import {
|
|
81
82
|
chartFetchDefaults,
|
|
82
83
|
feedbackWriteDefaults,
|
|
@@ -1011,12 +1012,24 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1011
1012
|
});
|
|
1012
1013
|
}
|
|
1013
1014
|
|
|
1015
|
+
// Fold Databricks AI Tools skills (`databricks aitools`) in as extra local
|
|
1016
|
+
// skill scan paths: reuse the CLI's installed tree when present, else shell
|
|
1017
|
+
// out to the CLI to materialize a curated set.
|
|
1018
|
+
const aiTools = await provisionDatabricksAITools(this.config.databricksAITools);
|
|
1019
|
+
if (aiTools.localSkillPaths.length > 0) {
|
|
1020
|
+
this.logger.info("databricks ai tools provisioned", {
|
|
1021
|
+
source: aiTools.source,
|
|
1022
|
+
localSkillPaths: aiTools.localSkillPaths.length,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const extraSkillPaths = [...provisioned.localSkillPaths, ...aiTools.localSkillPaths];
|
|
1026
|
+
|
|
1014
1027
|
this.built = await buildAgents({
|
|
1015
1028
|
config: this.config,
|
|
1016
1029
|
context: this.context,
|
|
1017
1030
|
memoryBuilder,
|
|
1018
1031
|
log: this.logger,
|
|
1019
|
-
extraSkillPaths
|
|
1032
|
+
extraSkillPaths,
|
|
1020
1033
|
});
|
|
1021
1034
|
|
|
1022
1035
|
// `mastra.server.apiRoutes` is only honored by Mastra's standalone
|
package/src/remote-skills.ts
CHANGED
|
@@ -27,23 +27,24 @@
|
|
|
27
27
|
* discovered by the built-in Assistant-skills mount. Pass `userEmail` (or an
|
|
28
28
|
* explicit `databricksBasePath`) to target `/Users/<email>/.assistant/skills`
|
|
29
29
|
* instead. When no Databricks client is resolvable at startup, the tree is
|
|
30
|
-
* written
|
|
31
|
-
* the current process.
|
|
30
|
+
* written under {@link localFS.tmpFS} and returned as an extra local skill path
|
|
31
|
+
* for the current process.
|
|
32
32
|
*
|
|
33
33
|
* @module
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
37
37
|
import { createRequire } from "node:module";
|
|
38
|
-
import { tmpdir } from "node:os";
|
|
39
38
|
import { join, posix } from "node:path";
|
|
39
|
+
import type { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
40
40
|
import { appkit } from "@dbx-tools/appkit";
|
|
41
41
|
import type { WorkspaceClientLike } from "@dbx-tools/appkit";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
import {
|
|
42
|
+
import { exec } from "@dbx-tools/core";
|
|
43
|
+
import { DatabricksFileSystem } from "@dbx-tools/databricks";
|
|
44
|
+
import { localFS, type LocalFileSystem } from "@dbx-tools/fs";
|
|
45
|
+
import { find } from "@dbx-tools/path";
|
|
46
|
+
import { error, hash, log, net, string } from "@dbx-tools/shared-core";
|
|
47
|
+
import type { FileSystem } from "@dbx-tools/shared-fs";
|
|
47
48
|
|
|
48
49
|
const logger = log.logger("mastra/remote-skills");
|
|
49
50
|
|
|
@@ -64,6 +65,9 @@ const SKILLS_CLI_LAYOUT = [".agents", "skills"] as const;
|
|
|
64
65
|
/** Cap on a direct-fetch download body, matching the `skills` CLI default. */
|
|
65
66
|
const DEFAULT_MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024;
|
|
66
67
|
|
|
68
|
+
/** Stable temp directory holding one rebuilt skill tree per remote source. */
|
|
69
|
+
const LOCAL_SKILLS_DIR = "mastra-local-skills";
|
|
70
|
+
|
|
67
71
|
/* -------------------------------- types -------------------------------- */
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -169,8 +173,8 @@ function toSourceOptions(source: RemoteSkillSource): RemoteSkillSourceOptions {
|
|
|
169
173
|
* Materialize every configured remote skill source at app startup.
|
|
170
174
|
*
|
|
171
175
|
* Writes each resolved `SKILL.md` tree to the Databricks user Assistant skills
|
|
172
|
-
* folder when a writable workspace is available, else
|
|
173
|
-
* returned in {@link ProvisionedRemoteSkills.localSkillPaths}.
|
|
176
|
+
* folder when a writable workspace is available, else under
|
|
177
|
+
* {@link localFS.tmpFS} returned in {@link ProvisionedRemoteSkills.localSkillPaths}.
|
|
174
178
|
*/
|
|
175
179
|
export async function provisionRemoteSkills(
|
|
176
180
|
option: RemoteSkillsOption | undefined,
|
|
@@ -183,12 +187,17 @@ export async function provisionRemoteSkills(
|
|
|
183
187
|
const client = options.client ?? appkit.tryGetExecutionContext()?.client;
|
|
184
188
|
const databricksBasePath = resolveDatabricksBasePath(options, client);
|
|
185
189
|
const destination = databricksBasePath
|
|
186
|
-
? new
|
|
190
|
+
? new DatabricksFileSystem({
|
|
191
|
+
client: client as WorkspaceClient,
|
|
192
|
+
root: databricksBasePath,
|
|
193
|
+
readOnly: false,
|
|
194
|
+
createRoot: true,
|
|
195
|
+
})
|
|
187
196
|
: undefined;
|
|
188
197
|
|
|
189
198
|
const localSkillPaths: string[] = [];
|
|
190
199
|
const skillNames: string[] = [];
|
|
191
|
-
let staging:
|
|
200
|
+
let staging: LocalFileSystem | undefined;
|
|
192
201
|
|
|
193
202
|
try {
|
|
194
203
|
const sources = Array.isArray(options.sources) ? options.sources : [options.sources];
|
|
@@ -196,17 +205,17 @@ export async function provisionRemoteSkills(
|
|
|
196
205
|
const sourceOptions = toSourceOptions(entry);
|
|
197
206
|
const failOnError = sourceOptions.failOnError ?? failDefault;
|
|
198
207
|
try {
|
|
199
|
-
staging ??= await
|
|
200
|
-
const stagedDir = await stageSource(sourceOptions, staging, options);
|
|
208
|
+
staging ??= await initializedScratch("mastra-remote-skills");
|
|
209
|
+
const stagedDir = await stageSource(sourceOptions, staging.root, options);
|
|
201
210
|
const staged = await collectSkillDirs(stagedDir);
|
|
202
211
|
if (staged.length === 0) {
|
|
203
212
|
throw new Error(`no SKILL.md found for source "${sourceOptions.source}"`);
|
|
204
213
|
}
|
|
205
214
|
if (destination && databricksBasePath) {
|
|
206
|
-
await
|
|
215
|
+
await copySkillDirs(destination, staged);
|
|
207
216
|
skillNames.push(...staged.map((dir) => dir.name));
|
|
208
217
|
} else {
|
|
209
|
-
const localDir = await persistLocally(staged);
|
|
218
|
+
const localDir = await persistLocally(sourceOptions.source, staged);
|
|
210
219
|
localSkillPaths.push(localDir);
|
|
211
220
|
skillNames.push(...staged.map((dir) => dir.name));
|
|
212
221
|
}
|
|
@@ -229,7 +238,9 @@ export async function provisionRemoteSkills(
|
|
|
229
238
|
}
|
|
230
239
|
}
|
|
231
240
|
} finally {
|
|
232
|
-
if (staging)
|
|
241
|
+
if (staging) {
|
|
242
|
+
await rm(staging.root, { recursive: true, force: true }).catch(() => undefined);
|
|
243
|
+
}
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
return { localSkillPaths, databricksBasePath, skillNames };
|
|
@@ -250,15 +261,16 @@ function resolveDatabricksBasePath(
|
|
|
250
261
|
}
|
|
251
262
|
|
|
252
263
|
/**
|
|
253
|
-
* Stage one source into a fresh dir under `
|
|
264
|
+
* Stage one source into a fresh dir under `stagingRoot`, preferring the optional
|
|
254
265
|
* `skills` CLI and falling back to a direct fetch.
|
|
255
266
|
*/
|
|
256
267
|
async function stageSource(
|
|
257
268
|
sourceOptions: RemoteSkillSourceOptions,
|
|
258
|
-
|
|
269
|
+
stagingRoot: string,
|
|
259
270
|
options: ProvisionRemoteSkillsOptions,
|
|
260
271
|
): Promise<string> {
|
|
261
|
-
const target =
|
|
272
|
+
const target = join(stagingRoot, `src-${hash.id()}`);
|
|
273
|
+
await mkdir(target, { recursive: true });
|
|
262
274
|
const viaCli = await stageViaSkillsCli(sourceOptions, target);
|
|
263
275
|
if (viaCli) return viaCli;
|
|
264
276
|
return stageViaFetch(sourceOptions, target, options);
|
|
@@ -290,7 +302,7 @@ async function stageViaSkillsCli(
|
|
|
290
302
|
}
|
|
291
303
|
if (string.parseList(sourceOptions.skills).length === 0) args.push("--skill", "*");
|
|
292
304
|
|
|
293
|
-
const result = await spawn(cli.command, args, {
|
|
305
|
+
const result = await exec.spawn(cli.command, args, {
|
|
294
306
|
cwd: target,
|
|
295
307
|
stdout: "capture",
|
|
296
308
|
stderr: "capture",
|
|
@@ -389,28 +401,47 @@ async function collectSkillDirs(root: string): Promise<StagedSkillDir[]> {
|
|
|
389
401
|
return dirs;
|
|
390
402
|
}
|
|
391
403
|
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Copy every staged skill directory into {@link destination}, preserving the
|
|
406
|
+
* `<skill>/<relative>` layout. The destination is just a {@link FileSystem},
|
|
407
|
+
* so the Databricks Assistant tree and the local fallback share one copier.
|
|
408
|
+
*/
|
|
409
|
+
async function copySkillDirs(destination: FileSystem, dirs: StagedSkillDir[]): Promise<void> {
|
|
410
|
+
await destination.init();
|
|
398
411
|
for (const dir of dirs) {
|
|
399
|
-
for (const relative of findFiles("**/*", { cwd: dir.absolutePath, nodir: true })) {
|
|
412
|
+
for (const relative of find.findFiles("**/*", { cwd: dir.absolutePath, nodir: true })) {
|
|
400
413
|
const buffer = await readFile(join(dir.absolutePath, relative));
|
|
401
|
-
const
|
|
402
|
-
await destination.writeFile(
|
|
414
|
+
const skillPath = posix.join(dir.name, relative.split(/[\\/]/).join("/"));
|
|
415
|
+
await destination.writeFile(skillPath, buffer, { overwrite: true });
|
|
403
416
|
}
|
|
404
417
|
}
|
|
405
418
|
}
|
|
406
419
|
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Copy staged skill dirs into a STABLE local tree keyed by the source, and
|
|
422
|
+
* return its root path.
|
|
423
|
+
*
|
|
424
|
+
* A given source resolves to the same content on almost every boot, so each
|
|
425
|
+
* one owns a directory named for its {@link hash.fnvHash} rather than leaving
|
|
426
|
+
* a fresh scratch dir behind per restart. Keying on the source (not the
|
|
427
|
+
* content) is what keeps two different sources from overwriting each other.
|
|
428
|
+
*/
|
|
429
|
+
async function persistLocally(source: string, dirs: StagedSkillDir[]): Promise<string> {
|
|
430
|
+
const stable = await localFS.rebuildFS(`${LOCAL_SKILLS_DIR}/${hash.fnvHash(source)}`, (scratch) =>
|
|
431
|
+
copySkillDirs(scratch, dirs),
|
|
432
|
+
);
|
|
433
|
+
return stable.root;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* A scratch filesystem whose root exists on disk, for the paths that hand a
|
|
438
|
+
* real directory to `node:fs` or a child process rather than going through
|
|
439
|
+
* the {@link FileSystem} API.
|
|
440
|
+
*/
|
|
441
|
+
async function initializedScratch(prefix: string): Promise<LocalFileSystem> {
|
|
442
|
+
const scratch = localFS.scratchFS(prefix);
|
|
443
|
+
await scratch.init();
|
|
444
|
+
return scratch;
|
|
414
445
|
}
|
|
415
446
|
|
|
416
447
|
/** Best-effort existence check. */
|
package/src/workspaces.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
* contributions merge extra filesystems and skill scan roots; built-in
|
|
8
8
|
* Assistant skill trees are toggled with `assistantSkills` (on by default).
|
|
9
9
|
*
|
|
10
|
+
* Databricks mounts use `@dbx-tools/databricks` {@link DatabricksFileSystem}
|
|
11
|
+
* wrapped by {@link filesystems}; missing roots fall back to
|
|
12
|
+
* {@link scratchFilesystem}.
|
|
13
|
+
*
|
|
10
14
|
* @module
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import type { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
18
|
+
import { DatabricksFileSystem } from "@dbx-tools/databricks";
|
|
14
19
|
import { log, string, token } from "@dbx-tools/shared-core";
|
|
15
20
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
16
21
|
import {
|
|
@@ -22,7 +27,7 @@ import {
|
|
|
22
27
|
} from "@mastra/core/workspace";
|
|
23
28
|
|
|
24
29
|
import { MASTRA_SCOPES_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, type User } from "./config.ts";
|
|
25
|
-
import {
|
|
30
|
+
import { scratchFilesystem, filesystems } from "./filesystems.ts";
|
|
26
31
|
|
|
27
32
|
/* ------------------------------ constants ------------------------------ */
|
|
28
33
|
|
|
@@ -175,8 +180,10 @@ function hasWorkspaceFileScope(requestContext: RequestContext | undefined): bool
|
|
|
175
180
|
* set. Returns empty mounts when the OBO user or client is missing.
|
|
176
181
|
* Mastra owns filesystem initialization.
|
|
177
182
|
*/
|
|
178
|
-
function resolveAssistantSkillsMounts(
|
|
179
|
-
|
|
183
|
+
async function resolveAssistantSkillsMounts(
|
|
184
|
+
context: WorkspaceMountContext,
|
|
185
|
+
): Promise<WorkspaceMountContribution> {
|
|
186
|
+
const mounts: Record<string, WorkspaceFilesystem> = {};
|
|
180
187
|
const requestContext = context.requestContext;
|
|
181
188
|
|
|
182
189
|
if (!shouldMountAssistantSkills(requestContext)) {
|
|
@@ -189,7 +196,7 @@ function resolveAssistantSkillsMounts(context: WorkspaceMountContext): Workspace
|
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
const user = requestContext!.get(MASTRA_USER_KEY) as User | undefined;
|
|
192
|
-
const client = user?.executionContext.client;
|
|
199
|
+
const client = user?.executionContext.client as WorkspaceClient | undefined;
|
|
193
200
|
if (!client) {
|
|
194
201
|
logger.debug("assistant-skills:skipped", {
|
|
195
202
|
reason: "missing-obo-client",
|
|
@@ -198,14 +205,14 @@ function resolveAssistantSkillsMounts(context: WorkspaceMountContext): Workspace
|
|
|
198
205
|
return { mounts, skillPaths: [] };
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = databricksFilesystem(
|
|
208
|
+
mounts[ASSISTANT_WORKSPACE_SKILLS_MOUNT] = await databricksFilesystem(
|
|
202
209
|
client,
|
|
203
210
|
ASSISTANT_SHARED_SKILLS_PATH,
|
|
204
211
|
);
|
|
205
212
|
|
|
206
213
|
const email = resolveScopedEmail(requestContext);
|
|
207
214
|
if (email) {
|
|
208
|
-
mounts[ASSISTANT_USER_SKILLS_MOUNT] = databricksFilesystem(
|
|
215
|
+
mounts[ASSISTANT_USER_SKILLS_MOUNT] = await databricksFilesystem(
|
|
209
216
|
client,
|
|
210
217
|
userAssistantSkillsPath(email),
|
|
211
218
|
false,
|
|
@@ -281,17 +288,35 @@ function resolveScopedEmail(requestContext: RequestContext | undefined): string
|
|
|
281
288
|
return email?.trim() || undefined;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
/**
|
|
285
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Wrap a {@link DatabricksFileSystem} as a Mastra filesystem. When the root is
|
|
293
|
+
* missing (and {@link readOnly} so we will not create it), fall back to
|
|
294
|
+
* {@link scratchFilesystem} so skill scans still have a writable local mount.
|
|
295
|
+
*/
|
|
296
|
+
async function databricksFilesystem(
|
|
286
297
|
client: WorkspaceClient,
|
|
287
|
-
|
|
298
|
+
root: string,
|
|
288
299
|
readOnly: boolean = true,
|
|
289
|
-
):
|
|
290
|
-
|
|
300
|
+
): Promise<WorkspaceFilesystem> {
|
|
301
|
+
const fs = new DatabricksFileSystem({
|
|
291
302
|
client,
|
|
292
|
-
|
|
303
|
+
root,
|
|
293
304
|
readOnly,
|
|
305
|
+
createRoot: !readOnly,
|
|
294
306
|
});
|
|
307
|
+
try {
|
|
308
|
+
await fs.init();
|
|
309
|
+
if (await fs.exists(".")) {
|
|
310
|
+
return filesystems(fs, { readOnly });
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
logger.debug("databricks-mount:scratch-fallback", {
|
|
314
|
+
root,
|
|
315
|
+
readOnly,
|
|
316
|
+
error: err instanceof Error ? err.message : String(err),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return scratchFilesystem();
|
|
295
320
|
}
|
|
296
321
|
|
|
297
322
|
/**
|
|
@@ -329,8 +354,8 @@ async function resolveWorkspaceContribution(
|
|
|
329
354
|
/**
|
|
330
355
|
* Dynamic filesystem resolver passed to Mastra {@link Workspace}.
|
|
331
356
|
*
|
|
332
|
-
* Returns a {@link CompositeFilesystem} when any mount resolved; otherwise
|
|
333
|
-
* {@link
|
|
357
|
+
* Returns a {@link CompositeFilesystem} when any mount resolved; otherwise a
|
|
358
|
+
* fresh {@link scratchFilesystem} so Mastra always has a writable local root.
|
|
334
359
|
*/
|
|
335
360
|
async function resolveWorkspaceFilesystem(
|
|
336
361
|
resolvers: WorkspaceMountResolver[],
|
|
@@ -339,10 +364,10 @@ async function resolveWorkspaceFilesystem(
|
|
|
339
364
|
const { mounts } = await resolveWorkspaceContribution(resolvers, context);
|
|
340
365
|
const mountKeys = Object.keys(mounts);
|
|
341
366
|
if (mountKeys.length === 0) {
|
|
342
|
-
logger.debug("filesystem:
|
|
367
|
+
logger.debug("filesystem:scratch", {
|
|
343
368
|
hasRequestContext: Boolean(context.requestContext),
|
|
344
369
|
});
|
|
345
|
-
return
|
|
370
|
+
return scratchFilesystem();
|
|
346
371
|
}
|
|
347
372
|
logger.debug("filesystem:composite", { mountKeys });
|
|
348
373
|
return new CompositeFilesystem({ mounts });
|