@mastra/core
Version:
1,472 lines • 350 kB
JavaScript
const require_rolldown_runtime = require("./rolldown-runtime-uwYp4b74.cjs");
const require_logger = require("./logger-BPclhj7J.cjs");
const require_base = require("./base-B6soWsYg.cjs");
require("./tracing-BUrUJwCM.cjs");
const require_request_context = require("./request-context-ByoZMp-j.cjs");
const require_tool = require("./tool-d85xHVkl.cjs");
const require_errors = require("./errors-Bfv4VhVf.cjs");
const require_workspace_skills = require("./workspace-skills-CjnfyQuP.cjs");
let stream = require("stream");
let crypto = require("crypto");
crypto = require_rolldown_runtime.__toESM(crypto, 1);
let zod_v4 = require("zod/v4");
let os = require("os");
os = require_rolldown_runtime.__toESM(os, 1);
let fs = require("fs");
let module$1 = require("module");
let path = require("path");
path = require_rolldown_runtime.__toESM(path, 1);
let p_map = require("p-map");
p_map = require_rolldown_runtime.__toESM(p_map, 1);
let path_posix = require("path/posix");
path_posix = require_rolldown_runtime.__toESM(path_posix, 1);
let fs_promises = require("fs/promises");
fs_promises = require_rolldown_runtime.__toESM(fs_promises, 1);
let url = require("url");
let child_process = require("child_process");
let string_decoder = require("string_decoder");
let gray_matter = require("gray-matter");
gray_matter = require_rolldown_runtime.__toESM(gray_matter, 1);
let tokenx = require("tokenx");
let ignore = require("ignore");
ignore = require_rolldown_runtime.__toESM(ignore, 1);
//#region src/workspace/lifecycle.ts
/**
* Call a lifecycle method on a provider, preferring the `_`-prefixed wrapper
* (which adds status tracking & race-condition safety) when available,
* falling back to the plain method for interface-only implementations.
*
* @example
* ```typescript
* await callLifecycle(sandbox, 'start'); // calls sandbox._start() ?? sandbox.start()
* await callLifecycle(filesystem, 'init'); // calls filesystem._init() ?? filesystem.init()
* ```
*/
async function callLifecycle(provider, method) {
const wrappedFn = provider[`_${method}`];
if (typeof wrappedFn === "function") await wrappedFn.call(provider);
else {
const plainFn = provider[method];
if (typeof plainFn === "function") await plainFn.call(provider);
}
}
//#endregion
//#region src/workspace/filesystem/composite-filesystem.ts
/**
* CompositeFilesystem - Routes operations to mounted filesystems based on path.
*
* Creates a unified filesystem view by combining multiple filesystems at different
* mount points. Useful for composing local storage, S3, and other backends.
*
* @example
* ```typescript
* const cfs = new CompositeFilesystem({
* mounts: {
* '/local': new LocalFilesystem({ basePath: './data' }),
* '/s3': new S3Filesystem({ bucket: 'my-bucket', ... }),
* }
* });
*
* // readdir('/') returns ['local', 's3']
* // readFile('/local/file.txt') reads from LocalFilesystem
* // readFile('/s3/data.json') reads from S3Filesystem
* ```
*/
/**
* CompositeFilesystem implementation.
*
* Routes file operations to the appropriate underlying filesystem based on path.
* Supports cross-mount operations (copy/move between different filesystems).
*
* The generic parameter preserves the concrete types of mounted filesystems,
* enabling typed access via `mounts.get()`.
*
* @example
* ```typescript
* const cfs = new CompositeFilesystem({
* mounts: {
* '/local': new LocalFilesystem({ basePath: './data' }),
* '/s3': new S3Filesystem({ bucket: 'my-bucket' }),
* },
* });
*
* cfs.mounts.get('/local') // LocalFilesystem
* cfs.mounts.get('/s3') // S3Filesystem
* ```
*/
var CompositeFilesystem = class {
id;
name = "CompositeFilesystem";
provider = "composite";
readOnly;
status = "ready";
_mounts;
constructor(config) {
this.id = `cfs-${Date.now().toString(36)}`;
this._mounts = /* @__PURE__ */ new Map();
for (const [path, fs] of Object.entries(config.mounts)) {
const normalized = this.normalizePath(path);
this._mounts.set(normalized, fs);
}
if (this._mounts.size === 0) throw new Error("CompositeFilesystem requires at least one mount");
this.readOnly = [...this._mounts.values()].every((fs) => fs.readOnly) || void 0;
const mountPaths = [...this._mounts.keys()];
for (const a of mountPaths) for (const b of mountPaths) if (a !== b && b.startsWith(a + "/")) throw new Error(`Nested mount paths are not supported: "${b}" is nested under "${a}"`);
}
/**
* Get all mount paths.
*/
get mountPaths() {
return Array.from(this._mounts.keys());
}
/**
* Get the mounts map.
* Returns a typed map where `get()` preserves the concrete filesystem type per mount path.
*/
get mounts() {
return this._mounts;
}
/**
* Get status and metadata for this composite filesystem.
* Includes info from each mounted filesystem in `metadata.mounts`.
*/
async getInfo() {
const mounts = {};
for (const [mountPath, fs] of this._mounts) mounts[mountPath] = await fs.getInfo?.() ?? null;
return {
id: this.id,
name: this.name,
provider: this.provider,
status: this.status,
readOnly: this.readOnly,
metadata: { mounts }
};
}
/**
* Get the underlying filesystem for a given path.
* Returns undefined if the path doesn't resolve to any mount.
*/
getFilesystemForPath(path) {
return this.resolveMount(path)?.fs;
}
/**
* Get the mount path for a given path.
* Returns undefined if the path doesn't resolve to any mount.
*/
getMountPathForPath(path) {
return this.resolveMount(path)?.mountPath;
}
/**
* Resolve a workspace-relative path to an absolute disk path.
* Strips the mount prefix and delegates to the underlying filesystem.
*/
resolveAbsolutePath(path) {
const r = this.resolveMount(path);
if (!r) return void 0;
return r.fs.resolveAbsolutePath?.(r.fsPath);
}
normalizePath(path) {
if (!path || path === "/" || path === ".") return "/";
let n = path_posix.default.normalize(path);
if (n === ".") return "/";
if (!n.startsWith("/")) n = `/${n}`;
if (n.length > 1 && n.endsWith("/")) n = n.slice(0, -1);
return n;
}
resolveMount(path) {
const normalized = this.normalizePath(path);
let best = null;
for (const [mountPath, fs] of this._mounts) if (normalized === mountPath || normalized.startsWith(mountPath + "/")) {
if (!best || mountPath.length > best.mountPath.length) best = {
mountPath,
fs
};
}
if (!best) return null;
let fsPath = normalized.slice(best.mountPath.length);
if (fsPath === "/") fsPath = "";
else if (fsPath.startsWith("/")) fsPath = fsPath.slice(1);
return {
fs: best.fs,
fsPath,
mountPath: best.mountPath
};
}
getVirtualEntries(path) {
const normalized = this.normalizePath(path);
if (this.resolveMount(normalized)) return null;
const entriesMap = /* @__PURE__ */ new Map();
for (const [mountPath, fs] of this._mounts.entries()) if (normalized === "/" ? mountPath.startsWith("/") : mountPath.startsWith(normalized + "/")) {
const remaining = normalized === "/" ? mountPath.slice(1) : mountPath.slice(normalized.length + 1);
const next = remaining.split("/")[0];
if (next && !entriesMap.has(next)) {
const isDirectMount = remaining === next;
const entry = {
name: next,
type: "directory"
};
if (isDirectMount) entry.mount = {
provider: fs.provider,
icon: fs.icon,
displayName: fs.displayName,
description: fs.description,
status: fs.status,
error: fs.error
};
entriesMap.set(next, entry);
}
}
return entriesMap.size > 0 ? Array.from(entriesMap.values()) : null;
}
isVirtualPath(path) {
const normalized = this.normalizePath(path);
if (normalized === "/" && !this._mounts.has("/")) return true;
for (const mountPath of this._mounts.keys()) if (mountPath.startsWith(normalized + "/")) return true;
return false;
}
/**
* Assert that a filesystem is writable (not read-only).
* @throws {PermissionError} if the filesystem is read-only
*/
assertWritable(fs, path, operation) {
if (fs.readOnly) throw new require_errors.PermissionError(path, `${operation} (filesystem is read-only)`);
}
async init() {
this.status = "initializing";
for (const [mountPath, fs] of this._mounts.entries()) try {
await callLifecycle(fs, "init");
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.warn(`[CompositeFilesystem] Mount "${mountPath}" failed to initialize: ${message}`);
}
this.status = "ready";
}
async destroy() {
this.status = "destroying";
const errors = [];
for (const fs of this._mounts.values()) try {
await callLifecycle(fs, "destroy");
} catch (e) {
errors.push(e instanceof Error ? e : new Error(String(e)));
}
if (errors.length > 0) {
this.status = "error";
throw new AggregateError(errors, "Some filesystems failed to destroy");
}
this.status = "destroyed";
}
async readFile(path, options) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
return r.fs.readFile(r.fsPath, options);
}
async writeFile(path, content, options) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
this.assertWritable(r.fs, path, "writeFile");
return r.fs.writeFile(r.fsPath, content, options);
}
async appendFile(path, content) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
this.assertWritable(r.fs, path, "appendFile");
return r.fs.appendFile(r.fsPath, content);
}
async deleteFile(path, options) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
this.assertWritable(r.fs, path, "deleteFile");
return r.fs.deleteFile(r.fsPath, options);
}
async copyFile(src, dest, options) {
const srcR = this.resolveMount(src);
const destR = this.resolveMount(dest);
if (!srcR) throw new Error(`No mount for source: ${src}`);
if (!destR) throw new Error(`No mount for dest: ${dest}`);
this.assertWritable(destR.fs, dest, "copyFile");
if (srcR.mountPath === destR.mountPath) return srcR.fs.copyFile(srcR.fsPath, destR.fsPath, options);
const content = await srcR.fs.readFile(srcR.fsPath);
await destR.fs.writeFile(destR.fsPath, content, { overwrite: options?.overwrite });
}
async moveFile(src, dest, options) {
const srcR = this.resolveMount(src);
const destR = this.resolveMount(dest);
if (!srcR) throw new Error(`No mount for source: ${src}`);
if (!destR) throw new Error(`No mount for dest: ${dest}`);
this.assertWritable(destR.fs, dest, "moveFile");
this.assertWritable(srcR.fs, src, "moveFile");
if (srcR.mountPath === destR.mountPath) return srcR.fs.moveFile(srcR.fsPath, destR.fsPath, options);
await this.copyFile(src, dest, options);
await srcR.fs.deleteFile(srcR.fsPath);
}
async readdir(path, options) {
const virtual = this.getVirtualEntries(path);
if (virtual) return virtual;
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
return r.fs.readdir(r.fsPath, options);
}
async mkdir(path, options) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
this.assertWritable(r.fs, path, "mkdir");
return r.fs.mkdir(r.fsPath, options);
}
async rmdir(path, options) {
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
this.assertWritable(r.fs, path, "rmdir");
return r.fs.rmdir(r.fsPath, options);
}
async exists(path) {
if (this.isVirtualPath(path)) return true;
const r = this.resolveMount(path);
if (!r) return false;
if (r.fsPath === "") return true;
return r.fs.exists(r.fsPath);
}
async stat(path) {
const normalized = this.normalizePath(path);
if (this.isVirtualPath(path)) {
const parts = normalized.split("/").filter(Boolean);
const now = /* @__PURE__ */ new Date();
return {
name: parts[parts.length - 1] || "",
path: normalized,
type: "directory",
size: 0,
createdAt: now,
modifiedAt: now
};
}
const r = this.resolveMount(path);
if (!r) throw new Error(`No mount for path: ${path}`);
if (r.fsPath === "") {
const parts = normalized.split("/").filter(Boolean);
const now = /* @__PURE__ */ new Date();
return {
name: parts[parts.length - 1] || "",
path: normalized,
type: "directory",
size: 0,
createdAt: now,
modifiedAt: now
};
}
return r.fs.stat(r.fsPath);
}
async isFile(path) {
if (this.isVirtualPath(path)) return false;
const r = this.resolveMount(path);
if (!r) return false;
try {
return (await r.fs.stat(r.fsPath)).type === "file";
} catch {
return false;
}
}
async isDirectory(path) {
if (this.isVirtualPath(path)) return true;
const r = this.resolveMount(path);
if (!r) return false;
if (r.fsPath === "") return true;
try {
return (await r.fs.stat(r.fsPath)).type === "directory";
} catch {
return false;
}
}
/**
* Get instructions describing the mounted filesystems.
* Used by agents to understand available storage locations.
*/
getInstructions(_opts) {
return `Filesystem mount points:\n${Array.from(this._mounts.entries()).map(([mountPath, fs]) => {
return `- ${mountPath}: ${fs.displayName || fs.provider} ${fs.readOnly ? "(read-only)" : "(read-write)"}`;
}).join("\n")}`;
}
};
//#endregion
//#region src/workspace/filesystem/mastra-filesystem.ts
/**
* MastraFilesystem Base Class
*
* Abstract base class for filesystem providers that want automatic logger integration
* and lifecycle management.
*
* Extends MastraBase to receive the Mastra logger when registered with a Mastra instance.
*
* ## Lifecycle Management
*
* The base class provides race-condition-safe lifecycle wrappers:
* - `_init()` - Handles concurrent calls, status management
* - `_destroy()` - Handles concurrent calls and status management
*
* Subclasses override the plain `init()` and `destroy()` methods to provide
* their implementation. Callers use the `_`-prefixed wrappers (or `callLifecycle()`)
* which add status tracking and race-condition safety.
*
* External providers can extend this class to get logger support, or implement
* the WorkspaceFilesystem interface directly if they don't need logging.
*/
/**
* Abstract base class for filesystem providers with logger support and lifecycle management.
*
* Providers that extend this class automatically receive the Mastra logger
* when the filesystem is used with a Mastra instance.
*
* @example
* ```typescript
* class MyCustomFilesystem extends MastraFilesystem {
* readonly id = 'my-fs';
* readonly name = 'MyCustomFilesystem';
* readonly provider = 'custom';
* status: ProviderStatus = 'pending';
*
* constructor() {
* super({ name: 'MyCustomFilesystem' });
* }
*
* // Override init() to provide initialization logic
* async init(): Promise<void> {
* // Your initialization logic here
* }
*
* async readFile(path: string): Promise<string | Buffer> {
* await this.ensureReady();
* this.logger.debug('Reading file', { path });
* // Implementation...
* }
* // ... implement other WorkspaceFilesystem methods
* }
* ```
*/
var MastraFilesystem = class extends require_base.MastraBase {
/** Error message when status is 'error' */
error;
/** Promise for _init() to prevent race conditions from concurrent calls */
_initPromise;
/** Promise for _destroy() to prevent race conditions from concurrent calls */
_destroyPromise;
/** Lifecycle callbacks */
_onInit;
_onDestroy;
constructor(options) {
super({
name: options.name,
component: require_logger.RegisteredLogger.WORKSPACE
});
this._onInit = options.onInit;
this._onDestroy = options.onDestroy;
}
/**
* Initialize the filesystem (wrapper with status management and race-condition safety).
*
* This method is race-condition-safe - concurrent calls will return the same promise.
* Handles status management automatically.
*
* Subclasses override `init()` to provide their initialization logic.
*/
async _init() {
if (this.status === "ready") return;
if (this._destroyPromise) try {
await this._destroyPromise;
} catch {}
if (this._initPromise) return this._initPromise;
this._initPromise = this._executeInit();
try {
await this._initPromise;
} finally {
this._initPromise = void 0;
}
}
/**
* Internal init execution - handles status.
*/
async _executeInit() {
this.status = "initializing";
this.error = void 0;
try {
await this.init();
this.status = "ready";
try {
await this._onInit?.({ filesystem: this });
} catch (error) {
this.logger.warn("onInit callback failed", { error });
}
} catch (error) {
this.status = "error";
this.error = error instanceof Error ? error.message : String(error);
this.logger.error("Failed to initialize filesystem", {
error,
id: this.id
});
throw error;
}
}
/**
* Override this method to implement filesystem initialization logic.
*
* Called by `_init()` after status is set to 'initializing'.
* Status will be set to 'ready' on success, 'error' on failure.
*
* @example
* ```typescript
* async init(): Promise<void> {
* this._client = new StorageClient({ ... });
* await this._client.connect();
* }
* ```
*/
async init() {}
/**
* Ensure the filesystem is ready.
*
* Calls `_init()` if status is not 'ready'. Useful for lazy initialization
* where operations should automatically initialize the filesystem if needed.
*
* @throws {FilesystemNotReadyError} if the filesystem fails to reach 'ready' status
*
* @example
* ```typescript
* async readFile(path: string): Promise<string | Buffer> {
* await this.ensureReady();
* // Now safe to use the filesystem
* }
* ```
*/
async ensureReady() {
if (this.status !== "ready") await this._init();
if (this.status !== "ready") throw new require_errors.FilesystemNotReadyError(this.id);
}
/**
* Destroy the filesystem and clean up all resources (wrapper with status management).
*
* This method is race-condition-safe - concurrent calls will return the same promise.
* Handles status management.
*
* Subclasses override `destroy()` to provide their destroy logic.
*/
async _destroy() {
if (this.status === "destroyed") return;
if (this.status === "pending") {
this.status = "destroyed";
return;
}
if (this._destroyPromise) return this._destroyPromise;
this._destroyPromise = this._executeDestroy();
try {
await this._destroyPromise;
} finally {
this._destroyPromise = void 0;
}
}
/**
* Internal destroy execution - handles status.
*/
async _executeDestroy() {
if (this._initPromise) try {
await this._initPromise;
} catch {}
this.status = "destroying";
try {
await this._onDestroy?.({ filesystem: this });
await this.destroy();
this.status = "destroyed";
} catch (error) {
this.status = "error";
this.logger.error("Failed to destroy filesystem", {
error,
id: this.id
});
throw error;
}
}
/**
* Override this method to implement filesystem destroy logic.
*
* Called by `_destroy()` after status is set to 'destroying'.
* Status will be set to 'destroyed' on success, 'error' on failure.
*/
async destroy() {}
};
//#endregion
//#region src/workspace/utils.ts
/**
* Resolve an instructions override against default instructions.
*
* - `undefined` → return default
* - `string` → return the string as-is
* - `function` → call with { defaultInstructions, requestContext }
*/
function resolveInstructions(override, getDefault, requestContext) {
if (typeof override === "string") return override;
const defaultInstructions = getDefault();
if (override === void 0) return defaultInstructions;
return override({
defaultInstructions,
requestContext
});
}
//#endregion
//#region src/workspace/filesystem/local-filesystem.ts
/**
* Local Filesystem Provider
*
* A filesystem implementation backed by a folder on the local disk.
* This is the default filesystem for development and local agents.
*/
/**
* Local filesystem implementation.
*
* Stores files in a folder on the user's machine.
* This is the recommended filesystem for development and persistent local storage.
*
* @example
* ```typescript
* import { Workspace, LocalFilesystem } from '@mastra/core';
*
* const workspace = new Workspace({
* filesystem: new LocalFilesystem({ basePath: './my-workspace' }),
* });
*
* await workspace.init();
* await workspace.writeFile('hello.txt', 'Hello World!');
* ```
*/
var LocalFilesystem = class extends MastraFilesystem {
id;
name = "LocalFilesystem";
provider = "local";
readOnly;
status = "pending";
_basePath;
_contained;
_allowedPaths;
_instructionsOverride;
/**
* The absolute base path on disk where files are stored.
* Useful for understanding how workspace paths map to disk paths.
*/
get basePath() {
return this._basePath;
}
/**
* Whether file operations are restricted to stay within basePath.
*
* When `true` (default), relative paths resolve against basePath and
* absolute paths are kept as-is. Any resolved path that falls outside
* basePath (and allowedPaths) throws a PermissionError. When `false`,
* no containment check is applied.
*
* **Note:** When used as a CompositeFilesystem mount with `contained: false`,
* the agent can access any path on the host filesystem through this mount.
*/
get contained() {
return this._contained;
}
/**
* Current set of resolved allowed paths.
* These paths are permitted beyond basePath when containment is enabled.
*/
get allowedPaths() {
return this._allowedPaths;
}
/**
* Update allowed paths. Accepts a direct array or an updater callback
* receiving the current paths (React setState pattern).
*
* @example
* ```typescript
* // Set directly
* fs.setAllowedPaths(['../shared-data']);
*
* // Update with callback
* fs.setAllowedPaths(prev => [...prev, '~/.claude/skills']);
* ```
*/
setAllowedPaths(pathsOrUpdater) {
const newPaths = typeof pathsOrUpdater === "function" ? pathsOrUpdater(this._allowedPaths) : pathsOrUpdater;
this._allowedPaths = newPaths.map((p) => require_workspace_skills.resolveToBasePath(this._basePath, p));
}
constructor(options) {
super({
...options,
name: "LocalFilesystem"
});
this.id = options.id ?? this.generateId();
this._basePath = path.resolve(require_workspace_skills.expandTilde(options.basePath));
this._contained = options.contained ?? true;
this.readOnly = options.readOnly;
this._allowedPaths = (options.allowedPaths ?? []).map((p) => require_workspace_skills.resolveToBasePath(this._basePath, p));
this._instructionsOverride = options.instructions;
}
/**
* Return mount config for sandbox integration.
* LocalSandbox uses this to create a symlink from the mount path to basePath.
*/
getMountConfig() {
return {
type: "local",
basePath: this._basePath
};
}
generateId() {
return `local-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
/**
* Check if an absolute path falls within basePath or any allowed path.
*/
_isWithinRoot(absolutePath, root) {
const relative = path.relative(root, absolutePath);
return !relative.startsWith("..") && !path.isAbsolute(relative);
}
_resolvePathForContainment(absolutePath) {
let currentPath = absolutePath;
while (true) {
try {
const realPath = (0, fs.realpathSync)(currentPath);
if (currentPath === absolutePath) return realPath;
const remainder = path.relative(currentPath, absolutePath);
return path.join(realPath, remainder);
} catch (error) {
if (!require_workspace_skills.isEnoentError(error)) return void 0;
}
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) return;
currentPath = parentPath;
}
}
_isWithinAnyRoot(absolutePath) {
const roots = [this._basePath, ...this._allowedPaths];
if (roots.some((root) => this._isWithinRoot(absolutePath, root))) return true;
const resolvedPath = this._resolvePathForContainment(absolutePath);
if (!resolvedPath) return false;
return roots.some((root) => {
const resolvedRoot = this._resolvePathForContainment(root);
return resolvedRoot ? this._isWithinRoot(resolvedPath, resolvedRoot) : false;
});
}
toBuffer(content) {
if (Buffer.isBuffer(content)) return content;
if (content instanceof Uint8Array) return Buffer.from(content);
return Buffer.from(content, "utf-8");
}
resolvePath(inputPath) {
const absolutePath = require_workspace_skills.resolveToBasePath(this._basePath, inputPath);
if (this._contained) {
if (!this._isWithinAnyRoot(absolutePath)) throw new require_errors.PermissionError(inputPath, this._accessOperationHint(inputPath));
}
return absolutePath;
}
/**
* Build the operation string for a containment-violation `PermissionError`.
*
* When the caller passed an absolute path, suggest a concrete relative form
* only when that suffix names an existing entry under the workspace (e.g.
* `/src/app.ts` → `src/app.ts` if `<basePath>/src` exists). Otherwise emit a
* soft hint that doesn't lie about specific paths — agents that mistake `/`
* for the workspace root learn the workspace is sandboxed without us
* inventing a fictitious in-workspace location for `/etc/passwd`.
*/
_accessOperationHint(inputPath) {
if (!path.isAbsolute(inputPath)) return "access";
const stripped = inputPath.replace(/^[/\\]+/, "");
if (!stripped) return "access";
const firstSegment = stripped.split(/[/\\]/, 1)[0];
if (firstSegment && firstSegment !== "." && firstSegment !== "..") try {
if ((0, fs.realpathSync)(path.join(this._basePath, firstSegment))) return `access (path is outside the workspace; use a relative path like "${stripped}")`;
} catch {}
return "access (path is outside the workspace; use a path relative to the workspace root, without a leading \"/\")";
}
/**
* Resolve a workspace-relative path to an absolute disk path.
* Uses the same resolution logic as internal file operations.
* Returns `undefined` if the path violates containment.
*/
resolveAbsolutePath(inputPath) {
try {
return this.resolvePath(inputPath);
} catch {
return;
}
}
toRelativePath(absolutePath) {
return path.relative(this._basePath, absolutePath).replace(/\\/g, "/");
}
assertWritable(operation) {
if (this.readOnly) throw new require_errors.WorkspaceReadOnlyError(operation);
}
/**
* Verify that the resolved path doesn't escape basePath via symlinks.
* Uses realpath to resolve symlinks and check the actual target.
*/
async assertPathContained(absolutePath) {
if (!this._contained) return;
if (this._allowedPaths.some((root) => this._isWithinRoot(absolutePath, root))) return;
let targetReal;
try {
targetReal = await fs_promises.realpath(absolutePath);
} catch (error) {
if (require_workspace_skills.isEnoentError(error)) return;
throw error;
}
const roots = [this._basePath, ...this._allowedPaths];
const rootReals = [];
for (const root of roots) try {
rootReals.push(await fs_promises.realpath(root));
} catch (error) {
if (require_workspace_skills.isEnoentError(error)) continue;
throw error;
}
if (!rootReals.some((rootReal) => targetReal === rootReal || targetReal.startsWith(rootReal + path.sep))) throw new require_errors.PermissionError(absolutePath, "access");
}
async readFile(inputPath, options) {
this.logger.debug("Reading file", {
path: inputPath,
encoding: options?.encoding
});
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
if ((await fs_promises.stat(absolutePath)).isDirectory()) throw new require_errors.IsDirectoryError(inputPath);
if (options?.encoding) return await fs_promises.readFile(absolutePath, { encoding: options.encoding });
return await fs_promises.readFile(absolutePath);
} catch (error) {
if (error instanceof require_errors.IsDirectoryError) throw error;
if (require_workspace_skills.isEnoentError(error)) throw new require_errors.FileNotFoundError(inputPath);
throw error;
}
}
async writeFile(inputPath, content, options) {
const contentSize = Buffer.isBuffer(content) ? content.length : content.length;
this.logger.debug("Writing file", {
path: inputPath,
size: contentSize,
recursive: options?.recursive
});
await this.ensureReady();
this.assertWritable("writeFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
if (options?.recursive === false) {
const dir = path.dirname(absolutePath);
const parentPath = path.dirname(inputPath);
try {
if (!(await fs_promises.stat(dir)).isDirectory()) throw new require_errors.NotDirectoryError(parentPath);
} catch (error) {
if (error instanceof require_errors.NotDirectoryError) throw error;
if (require_workspace_skills.isEnoentError(error)) throw new require_errors.DirectoryNotFoundError(parentPath);
throw error;
}
}
if (options?.recursive !== false) {
const dir = path.dirname(absolutePath);
await fs_promises.mkdir(dir, { recursive: true });
}
if (options?.expectedMtime) try {
const currentStat = await fs_promises.stat(absolutePath);
if (currentStat.mtime.getTime() !== options.expectedMtime.getTime()) throw new require_errors.StaleFileError(inputPath, options.expectedMtime, currentStat.mtime);
} catch (error) {
if (error instanceof require_errors.StaleFileError) throw error;
if (!require_workspace_skills.isEnoentError(error)) throw error;
}
const writeFlag = options?.overwrite === false ? "wx" : "w";
try {
await fs_promises.writeFile(absolutePath, this.toBuffer(content), { flag: writeFlag });
} catch (error) {
if (options?.overwrite === false && require_workspace_skills.isEexistError(error)) throw new require_errors.FileExistsError(inputPath);
throw error;
}
}
async appendFile(inputPath, content) {
const contentSize = Buffer.isBuffer(content) ? content.length : content.length;
this.logger.debug("Appending to file", {
path: inputPath,
size: contentSize
});
await this.ensureReady();
this.assertWritable("appendFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
const dir = path.dirname(absolutePath);
await fs_promises.mkdir(dir, { recursive: true });
await fs_promises.appendFile(absolutePath, this.toBuffer(content));
}
async deleteFile(inputPath, options) {
this.logger.debug("Deleting file", {
path: inputPath,
force: options?.force
});
await this.ensureReady();
this.assertWritable("deleteFile");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
if ((await fs_promises.stat(absolutePath)).isDirectory()) throw new require_errors.IsDirectoryError(inputPath);
await fs_promises.unlink(absolutePath);
} catch (error) {
if (error instanceof require_errors.IsDirectoryError) throw error;
if (require_workspace_skills.isEnoentError(error)) {
if (!options?.force) throw new require_errors.FileNotFoundError(inputPath);
} else throw error;
}
}
async copyFile(src, dest, options) {
this.logger.debug("Copying file", {
src,
dest,
recursive: options?.recursive
});
await this.ensureReady();
this.assertWritable("copyFile");
const srcPath = this.resolvePath(src);
const destPath = this.resolvePath(dest);
await this.assertPathContained(srcPath);
await this.assertPathContained(destPath);
try {
if ((await fs_promises.stat(srcPath)).isDirectory()) {
if (!options?.recursive) throw new require_errors.IsDirectoryError(src);
await this.copyDirectory(srcPath, destPath, options);
} else {
await fs_promises.mkdir(path.dirname(destPath), { recursive: true });
const copyFlags = options?.overwrite === false ? fs.constants.COPYFILE_EXCL : 0;
try {
await fs_promises.copyFile(srcPath, destPath, copyFlags);
} catch (error) {
if (options?.overwrite === false && require_workspace_skills.isEexistError(error)) throw new require_errors.FileExistsError(dest);
throw error;
}
}
} catch (error) {
if (error instanceof require_errors.IsDirectoryError || error instanceof require_errors.FileExistsError) throw error;
if (require_workspace_skills.isEnoentError(error)) throw new require_errors.FileNotFoundError(src);
throw error;
}
}
async copyDirectory(src, dest, options) {
await this.ensureReady();
await fs_promises.mkdir(dest, { recursive: true });
const entries = await fs_promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcEntry = path.join(src, entry.name);
const destEntry = path.join(dest, entry.name);
await this.assertPathContained(srcEntry);
await this.assertPathContained(destEntry);
if (entry.isDirectory()) await this.copyDirectory(srcEntry, destEntry, options);
else {
const copyFlags = options?.overwrite === false ? fs.constants.COPYFILE_EXCL : 0;
try {
await fs_promises.copyFile(srcEntry, destEntry, copyFlags);
} catch (error) {
if (options?.overwrite === false && require_workspace_skills.isEexistError(error)) continue;
throw error;
}
}
}
}
async moveFile(src, dest, options) {
this.logger.debug("Moving file", {
src,
dest,
overwrite: options?.overwrite
});
await this.ensureReady();
this.assertWritable("moveFile");
const srcPath = this.resolvePath(src);
const destPath = this.resolvePath(dest);
await this.assertPathContained(srcPath);
await this.assertPathContained(destPath);
try {
await fs_promises.mkdir(path.dirname(destPath), { recursive: true });
if (options?.overwrite === false) {
await this.copyFile(src, dest, {
...options,
overwrite: false
});
await fs_promises.rm(srcPath, {
recursive: true,
force: true
});
return;
}
try {
await fs_promises.rename(srcPath, destPath);
} catch (error) {
if (error.code !== "EXDEV") throw error;
await this.copyFile(src, dest, options);
await fs_promises.rm(srcPath, {
recursive: true,
force: true
});
}
} catch (error) {
if (error instanceof require_errors.FileExistsError) throw error;
if (require_workspace_skills.isEnoentError(error)) throw new require_errors.FileNotFoundError(src);
throw error;
}
}
async mkdir(inputPath, options) {
this.logger.debug("Creating directory", {
path: inputPath,
recursive: options?.recursive
});
await this.ensureReady();
this.assertWritable("mkdir");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
await fs_promises.mkdir(absolutePath, { recursive: options?.recursive ?? true });
} catch (error) {
if (require_workspace_skills.isEexistError(error)) {
if (!(await fs_promises.stat(absolutePath)).isDirectory()) throw new require_errors.FileExistsError(inputPath);
} else if (require_workspace_skills.isEnoentError(error)) throw new require_errors.DirectoryNotFoundError(path.dirname(inputPath));
else throw error;
}
}
async rmdir(inputPath, options) {
this.logger.debug("Removing directory", {
path: inputPath,
recursive: options?.recursive,
force: options?.force
});
await this.ensureReady();
this.assertWritable("rmdir");
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
if (!(await fs_promises.stat(absolutePath)).isDirectory()) throw new require_errors.NotDirectoryError(inputPath);
if (options?.recursive) await fs_promises.rm(absolutePath, {
recursive: true,
force: options?.force ?? false
});
else {
if ((await fs_promises.readdir(absolutePath)).length > 0) throw new require_errors.DirectoryNotEmptyError(inputPath);
await fs_promises.rmdir(absolutePath);
}
} catch (error) {
if (error instanceof require_errors.NotDirectoryError || error instanceof require_errors.DirectoryNotEmptyError) throw error;
if (require_workspace_skills.isEnoentError(error)) {
if (!options?.force) throw new require_errors.DirectoryNotFoundError(inputPath);
} else throw error;
}
}
async readdir(inputPath, options) {
this.logger.debug("Reading directory", {
path: inputPath,
recursive: options?.recursive
});
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
try {
if (!(await fs_promises.stat(absolutePath)).isDirectory()) throw new require_errors.NotDirectoryError(inputPath);
const entries = await fs_promises.readdir(absolutePath, { withFileTypes: true });
const result = [];
for (const entry of entries) {
const entryPath = path.join(absolutePath, entry.name);
if (options?.extension) {
const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
if (entry.isFile()) {
const ext = path.extname(entry.name);
if (!extensions.some((e) => e === ext || e === ext.slice(1))) continue;
}
}
const isSymlink = entry.isSymbolicLink();
let symlinkTarget;
let resolvedType = "file";
if (isSymlink) try {
symlinkTarget = await fs_promises.readlink(entryPath);
resolvedType = (await fs_promises.stat(entryPath)).isDirectory() ? "directory" : "file";
} catch {
resolvedType = "file";
}
else resolvedType = entry.isDirectory() ? "directory" : "file";
const fileEntry = {
name: entry.name,
type: resolvedType,
isSymlink: isSymlink || void 0,
symlinkTarget
};
if (resolvedType === "file" && !isSymlink) try {
fileEntry.size = (await fs_promises.stat(entryPath)).size;
} catch {}
result.push(fileEntry);
if (options?.recursive && resolvedType === "directory") {
const depth = options.maxDepth ?? 100;
if (depth > 0) {
const subEntries = await this.readdir(this.toRelativePath(entryPath), {
...options,
maxDepth: depth - 1
});
result.push(...subEntries.map((e) => ({
...e,
name: `${entry.name}/${e.name}`
})));
}
}
}
return result;
} catch (error) {
if (error instanceof require_errors.NotDirectoryError) throw error;
if (require_workspace_skills.isEnoentError(error)) throw new require_errors.DirectoryNotFoundError(inputPath);
throw error;
}
}
async exists(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
return require_workspace_skills.fsExists(absolutePath);
}
async stat(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
return {
...await require_workspace_skills.fsStat(absolutePath, inputPath),
path: this.toRelativePath(absolutePath)
};
}
async realpath(inputPath) {
await this.ensureReady();
const absolutePath = this.resolvePath(inputPath);
await this.assertPathContained(absolutePath);
const canonicalPath = await fs_promises.realpath(absolutePath);
return this.toRelativePath(canonicalPath);
}
/**
* Initialize the local filesystem by creating the base directory.
* Status management is handled by the base class.
*/
async init() {
this.logger.debug("Initializing filesystem", { basePath: this._basePath });
await fs_promises.mkdir(this._basePath, { recursive: true });
this.logger.debug("Filesystem initialized", { basePath: this._basePath });
}
/**
* Clean up the local filesystem.
* LocalFilesystem doesn't delete files on destroy by default.
* Status management is handled by the base class.
*/
async destroy() {}
getInfo() {
return {
id: this.id,
name: this.name,
provider: this.provider,
readOnly: this.readOnly,
status: this.status,
error: this.error,
metadata: {
basePath: this.basePath,
contained: this._contained,
...this._allowedPaths.length > 0 && { allowedPaths: [...this._allowedPaths] }
}
};
}
getInstructions(opts) {
return resolveInstructions(this._instructionsOverride, () => this._getDefaultInstructions(), opts?.requestContext);
}
_getDefaultInstructions() {
const parts = [`Local filesystem at "${this.basePath}". Relative paths resolve from this directory.`];
if (this._contained) if (this._allowedPaths.length > 0) parts.push(`File access is restricted to this directory and the following allowed paths: ${this._allowedPaths.join(", ")}.`);
else parts.push("File access is restricted to this directory.");
else parts.push("Containment is disabled, so any path on the host filesystem is accessible.");
return parts.join(" ");
}
};
//#endregion
//#region src/workspace/filesystem/file-read-tracker.ts
/**
* In-memory implementation of FileReadTracker.
*/
var InMemoryFileReadTracker = class {
records = /* @__PURE__ */ new Map();
recordRead(path$8, modifiedAt) {
const normalizedPath = this.normalizePath(path$8);
this.records.set(normalizedPath, {
path: normalizedPath,
readAt: /* @__PURE__ */ new Date(),
modifiedAtRead: modifiedAt
});
}
getReadRecord(path$9) {
return this.records.get(this.normalizePath(path$9));
}
needsReRead(path$10, currentModifiedAt) {
const record = this.getReadRecord(path$10);
if (!record) return {
needsReRead: true,
reason: `File "${path$10}" has not been read. You must read a file before writing to it.`
};
if (currentModifiedAt.getTime() > record.modifiedAtRead.getTime()) return {
needsReRead: true,
reason: `File "${path$10}" was modified since last read (read at: ${record.modifiedAtRead.toISOString()}, current: ${currentModifiedAt.toISOString()}). Please re-read the file to get the latest contents.`
};
return { needsReRead: false };
}
clearReadRecord(path$11) {
this.records.delete(this.normalizePath(path$11));
}
clear() {
this.records.clear();
}
normalizePath(pathStr) {
return path.posix.normalize(pathStr.replace(/\\/g, "/")).replace(/\/$/, "") || "/";
}
};
//#endregion
//#region src/workspace/filesystem/file-write-lock.ts
/**
* In-memory implementation of FileWriteLock using per-path promise queues.
*
* Adapted from mastracode's `withWriteLock` pattern.
*/
var InMemoryFileWriteLock = class {
queues = /* @__PURE__ */ new Map();
timeoutMs;
constructor(opts) {
this.timeoutMs = opts?.timeoutMs ?? 3e4;
}
get size() {
return this.queues.size;
}
withLock(filePath, fn) {
const key = this.normalizePath(filePath);
const currentQueue = this.queues.get(key) ?? Promise.resolve();
let resolve;
let reject;
const resultPromise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const queuePromise = currentQueue.catch(() => {}).then(async () => {
let timeoutId;
try {
const result = await Promise.race([fn(), new Promise((_, rej) => {
timeoutId = setTimeout(() => rej(/* @__PURE__ */ new Error(`write-lock timeout on "${key}" after ${this.timeoutMs}ms`)), this.timeoutMs);
})]);
clearTimeout(timeoutId);
resolve(result);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
});
this.queues.set(key, queuePromise);
queuePromise.finally(() => {
if (this.queues.get(key) === queuePromise) this.queues.delete(key);
});
return resultPromise;
}
normalizePath(pathStr) {
return path.posix.normalize(pathStr.replace(/\\/g, "/").replace(/^\/\/+/, "/")).replace(/\/+$/, "") || "/";
}
};
//#endregion
//#region src/workspace/lsp/language.ts
/**
* Language Detection
*
* Maps file extensions to LSP language identifiers.
* Browser-safe — no Node.js dependencies.
*/
/**
* Maps file extensions (including the dot) to LSP language identifiers.
*/
const LANGUAGE_EXTENSIONS = {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript",
".cjs": "javascript",
".py": "python",
".pyi": "python",
".go": "go",
".rs": "rust",
".c": "c",
".cpp": "cpp",
".cc": "cpp",
".cxx": "cpp",
".h": "c",
".hpp": "cpp",
".java": "java",
".json": "json",
".jsonc": "jsonc",
".yaml": "yaml",
".yml": "yaml",
".md": "markdown",
".html": "html",
".css": "css",
".scss": "scss",
".sass": "sass",
".less": "less"
};
/**
* Get the LSP language ID for a file path based on its extension.
* Returns undefined if the extension is not recognized.
*
* When `customExtensions` is provided, it is checked first, allowing
* custom servers to register new file extensions or override built-in mappings.
*/
function getLanguageId(filePath, customExtensions) {
const dotIndex = filePath.lastIndexOf(".");
if (dotIndex === -1) return void 0;
const ext = filePath.substring(dotIndex);
return customExtensions?.[ext] ?? LANGUAGE_EXTENSIONS[ext];
}
//#endregion
//#region src/workspace/lsp/client.ts
/**
* LSP Client
*
* JSON-RPC client wrapper for communicating with language servers.
* Uses dynamic imports for vscode-jsonrpc and vscode-languageserver-protocol
* to keep them as optional dependencies.
*
* Spawns LSP servers via a SandboxProcessManager, so it works with any
* sandbox backend (local, E2B, etc.) that has a process manager.
*/
/** Cached module references — undefined means not yet checked, null means unavailable */
let jsonrpcModule;
let lspProtocolModule;
/**
* Check if vscode-jsonrpc is available without importing it.
* Synchronous check — safe to call at registration time.
*/
function isLSPAvailable() {
if (jsonrpcModule !== void 0) return jsonrpcModule !== null;
try {
const req = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
req.resolve("vscode-jsonrpc/node");
req.resolve("vscode-languageserver-protocol");
return true;
} catch {
return false;
}
}
/**
* Load vscode-jsonrpc and vscode-languageserver-protocol.
* Returns null if not available. Caches result after first call.
*/
async function loadLSPDeps() {
if (jsonrpcModule !== void 0 && lspProtocolModule !== void 0) {
if (jsonrpcModule === null || lspProtocolModule === null) return null;
return {
...jsonrpcModule,
...lspProtocolModule
};
}
try {
const req = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
const jsonrpc = req("vscode-jsonrpc/node");
const protocol = req("vscode-languageserver-protocol");
jsonrpcModule = {
StreamMessageReader: jsonrpc.StreamMessageReader,
StreamMessageWriter: jsonrpc.StreamMessageWriter,
createMessageConnection: jsonrpc.createMessageConnection
};
lspProtocolModule = {
TextDocumentIdentifier: protocol.TextDocumentIdentifier,
Position: protocol.Position
};
return {
...jsonrpcModule,
...lspProtocolModule
};
} catch {
jsonrpcModule = null;
lspProtocolModule = null;
return null;
}
}
/** Convert a filesystem path to a properly encoded file:// URI. */
function toFileUri(fsPath) {
return (0, url.pathToFileURL)(fsPath).toString();
}
/**
* Normalize a file:// URI to a canonical fs-path-based key for diagnostics
* map storage/lookup. On Windows, different LSP servers emit different
* canonical forms for the same path (e.g. `file:///C:/...` vs
* `file:///c%3A/...`), so we convert back to an OS path and compare those
* instead of comparing URI strings directly.
*/
function diagnosticsKey(uriOrPath) {
let fsPath;
try {
fsPath = uriOrPath.startsWith("file:") ? (0, url.fileURLToPath)(uriOrPath) : uriOrPath;
} catch {
return uriOrPath;
}
const driveMatch = fsPath.match(/^[\\/]?([a-zA-Z]):([\\/].*)$/);
if (driveMatch) return `${driveMatch[1].toLowerCase()}:${driveMatch[2]}`;
return fsPath;
}
async function withTimeout(promise, ms, errorMessage) {
let timer;
return Promise.race([promise, new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(errorMessage)), ms);
})]).finally(() => clearTimeout(timer));
}
/**
* Wraps a JSON-RPC connection to a single LSP server process.
* Uses a SandboxProcessManager to spawn the server process.
*/
var LSPClient = class {
connection = null;
handle = null;
serverDef;
workspaceRoot;
processManager;
diagnostics = /* @__PURE__ */ new Map();
initializationOptions = null;
supportsPullDiagnostics = false;
constructor(serverDef, workspaceRoot, processManager) {
this.serverDef = serverDef;
this.workspaceRoot = workspaceRoot;
this.processManager = processManager;
}
/** Whether the underlying server process is still running. */
get isAlive() {
return this.handle !== null && this.handle.exitCode === void 0;
}
/** Name of the LSP server. */
get serverName() {
return this.serverDef.name;
}
/**
* Initialize the LSP connection — spawns the server and performs the handshake.
*/
async initialize(initTimeout = 1e4) {
const deps = await loadLSPDeps();
if (!deps) throw new Error("LSP dependencies (vscode-jsonrpc) are not available");
const { StreamMessageReader, StreamMessageWriter, createMessageConnection } = deps;
const command = this.serverDef.command(this.workspaceRoot);
if (!command) throw new Error("Failed to resolve LSP server command");
this.handle = await this.processManager.spawn(command, { cwd: this.workspaceRoot });
const initializationOptions = this.serverDef.initialization?.(this.workspaceRoot);
const reader = new StreamMessageReader(this.handle.reader);
const writer = new StreamMessageWriter(this.handle.writer);
const originalWrite = writer.write.bind(writer);
writer.write = (msg) => originalWrite(msg).catch(() => {});
this.connection = createMessageConnection(reader, writer);
this.connection.onError(() => {});
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
this.diagnostics.set(diagnosticsKey(params.uri), params.diagnostics);
});
this.connection.listen();
const initParams = {
processId: process.pid,
rootUri: toFileUri(this.workspaceRoot),
workspaceFolders: [{
name: "workspace",
uri: toFileUri(this.workspaceRoot)
}],
capabilities: {
window: { workDoneProgress: true },
workspace: { configuration: true },
textDocument: {
publis