@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
181 lines • 6.87 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { conditionVersion, stale } from "../../cache/context.js";
import { DropboxApiError, dropboxRpc } from "./client.js";
import { GONE_SUMMARIES, LOST_SUMMARIES, MISS_SUMMARIES } from "./constants.js";
/**
* List a folder's entries, following every continuation cursor.
*
* `/` and `''` both mean the account root.
*/
export async function listFolder(tm, path, opts = {}) {
const apiPath = path === '/' || path === '' ? '' : path;
const recursive = opts.recursive === true;
const pageSize = opts.pageSize ?? 2000;
const limit = opts.limit ?? null;
const out = [];
let resp = (await dropboxRpc(tm, '/files/list_folder', {
path: apiPath,
recursive,
limit: limit === null ? pageSize : Math.min(pageSize, limit),
}));
for (const item of resp.entries)
out.push(item);
while (resp.has_more) {
if (limit !== null && out.length >= limit)
break;
resp = (await dropboxRpc(tm, '/files/list_folder/continue', {
cursor: resp.cursor,
}));
for (const item of resp.entries)
out.push(item);
}
return out;
}
/**
* A cursor at the folder's present state, without listing it.
*
* `/` and `''` both mean the account root. Mirrors Python `latest_cursor`.
*/
export async function latestCursor(tm, path, opts = {}) {
const apiPath = path === '/' || path === '' ? '' : path;
const resp = (await dropboxRpc(tm, '/files/list_folder/get_latest_cursor', {
path: apiPath,
recursive: opts.recursive === true,
}));
return resp.cursor;
}
/**
* Replay changes since `cursor`, following every continuation page.
*/
export async function continueFolder(tm, cursor) {
const out = [];
let resp = (await dropboxRpc(tm, '/files/list_folder/continue', {
cursor,
}));
for (const item of resp.entries)
out.push(item);
while (resp.has_more) {
resp = (await dropboxRpc(tm, '/files/list_folder/continue', {
cursor: resp.cursor,
}));
for (const item of resp.entries)
out.push(item);
}
return { entries: out, cursor: resp.cursor };
}
export async function getMetadata(tm, path) {
return (await dropboxRpc(tm, '/files/get_metadata', { path }));
}
export async function createFolder(tm, path) {
await dropboxRpc(tm, '/files/create_folder_v2', { path, autorename: false });
}
export async function deletePath(tm, path, parentRev = null) {
await dropboxRpc(tm, '/files/delete_v2', parentRev === null ? { path } : { path, parent_rev: parentRev });
}
/**
* An entry's metadata, null when Dropbox has nothing at the path. Only a
* not_found or not_folder 409 is a miss; any other refusal (restricted_content,
* a relay's 5xx naming not_found) names a path that may exist, and throws.
* Mirrors Python's `lookup`.
*/
export async function lookup(tm, path) {
try {
return await getMetadata(tm, path);
}
catch (err) {
if (err instanceof DropboxApiError &&
err.status === 409 &&
MISS_SUMMARIES.some((s) => err.summary.startsWith(s))) {
return null;
}
throw err;
}
}
/**
* The refusal a write sent with a rev met, null for any other failure or a
* write that went plain. Mirrors Python's `refused`.
*/
export async function refused(path, err, cond, rev) {
if (rev === null || rev === '' || !(err instanceof DropboxApiError))
return null;
if (![...LOST_SUMMARIES, ...GONE_SUMMARIES].some((s) => err.summary.startsWith(s)))
return null;
return stale(path, { version: conditionVersion(cond) });
}
/**
* Move one entry with `/files/move_v2`. Answers the moved entry's metadata
* (`.tag` names its kind), or an empty object when the reply carries none.
*/
export async function movePath(tm, from, to) {
const reply = (await dropboxRpc(tm, '/files/move_v2', {
from_path: from,
to_path: to,
autorename: false,
}));
const metadata = reply?.metadata;
return metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata)
? metadata
: {};
}
export async function copyPath(tm, from, to) {
await dropboxRpc(tm, '/files/copy_v2', { from_path: from, to_path: to, autorename: false });
}
export const SEARCH_PAGE = 1000;
// search_v2 + search/continue_v2 serve at most 10,000 matches total; a
// result set that hits the ceiling may be incomplete and must not be used
// to narrow a scan.
export const MAX_SEARCH_MATCHES = 10_000;
/**
* Collect `[path_lower, path_display]` file matches for a search query.
*
* Pages through `/files/search_v2` and `/files/search/continue_v2`,
* deduplicating across pages (the API may repeat results between pages).
* `path: ''` searches the whole account. `truncated` reports whether the
* result hit the API's 10,000-match ceiling (a truncated set is not a
* trustworthy superset).
*/
export async function searchFiles(tm, query, opts = {}) {
const options = {
max_results: SEARCH_PAGE,
file_status: 'active',
filename_only: opts.filenameOnly === true,
};
if (opts.path !== undefined && opts.path !== '')
options.path = opts.path;
let resp = (await dropboxRpc(tm, '/files/search_v2', { query, options }));
const seen = new Set();
const paths = [];
for (;;) {
for (const m of resp.matches ?? []) {
const entry = m.metadata?.metadata;
if (entry?.['.tag'] !== 'file')
continue;
const lower = entry.path_lower ?? '';
const display = entry.path_display ?? lower;
if (lower === '' || seen.has(lower))
continue;
seen.add(lower);
paths.push([lower, display]);
}
if (paths.length >= MAX_SEARCH_MATCHES)
return { paths, truncated: true };
if (resp.has_more !== true || resp.cursor === undefined)
return { paths, truncated: false };
resp = (await dropboxRpc(tm, '/files/search/continue_v2', {
cursor: resp.cursor,
}));
}
}
//# sourceMappingURL=api.js.map