/** * Filesystem Service - Real filesystem operations * * Node.js port of the Python FilesystemService. */ import { FileInfo, FileInfoResponse, FileType, DirectoryListing, MtimeResponse, ReadFileResponse, NebulaCell, NotebookCellsResponse, SaveNotebookResult } from './types'; export interface AgentPermissionSnapshot { agent_created: boolean; agent_permitted: boolean; has_history: boolean; can_agent_modify: boolean; reason: string; } export declare class FilesystemService { private writeLocks; private defaultRoot; constructor(defaultRoot?: string); /** * Get the server root directory. */ getRootDirectory(): string; /** * Set the server root directory and persist it. */ setRootDirectory(rootDirectory: string, options?: { persist?: boolean; }): string; private expandRootDirectory; private saveRootDirectory; /** * Normalize and expand path */ normalizePath(filePath: string): string; /** * Serialize write operations per notebook to avoid interleaving writes. */ private withWriteLock; /** * Atomically write a file (write temp, fsync, rename, fsync dir). * Prevents partial/corrupt files on interruption. */ private atomicWriteFileSync; private writeJsonAtomicSync; /** * Async variant of atomicWriteFileSync (write temp, fsync, rename, fsync dir). * Used on hot notebook-save paths so large writes never block the event loop. * * `durable: false` skips both fsyncs while keeping the tmp+rename atomicity. * Use it for auxiliary files (history/session/journal/lastsave): on network * filesystems (GPFS) each fsync can take seconds, and a save doing ~10 of * them was observed at 20-27s. A crash can lose the last few seconds of an * auxiliary file, but never corrupt it — the rename is still atomic. The * notebook file itself stays fully durable. */ private atomicWriteFile; private writeJsonAtomic; /** * Format file size for display */ formatSize(size: number): string; /** * Determine file type from extension */ getFileType(extension: string): FileType; /** * Get file info for a path */ private getFileInfo; /** Async, non-blocking variant of getFileInfo (one `fsp.stat`). Used by * listDirectory so a folder's entries stat in parallel off the event loop. */ private getFileInfoAsync; /** * Convert FileInfo to FileInfoResponse for API */ private toFileInfoResponse; /** * Get directory modification time (lightweight check for changes) */ getDirectoryMtime(dirPath: string): MtimeResponse; /** * Get file modification time */ getFileMtime(filePath: string): Promise; /** * List contents of a directory */ listDirectory(dirPath: string): Promise; /** * Read a file's contents */ readFile(filePath: string): Promise; /** * Write content to a file */ writeFile(filePath: string, content: unknown, fileType?: 'text' | 'notebook'): boolean; /** * Create a new file or directory */ createFile(filePath: string, isDirectory?: boolean): FileInfo & { is_directory: boolean; }; /** * Get the history file path for a notebook */ private getHistoryPath; /** * Last-Nebula-save record for text-format notebooks: the canonical cell * state (id/type/content) as of the last save THROUGH Nebula. At history * load, divergence between this record and the file means the notebook was * edited externally (vim, git checkout, ...) — the difference is * reconciled into history as a synthesized external-edit operation so * undo/redo stays infinite across external edits. */ private getLastSavePath; private writeTextNotebookLastSave; /** * Get the session state file path for a notebook */ private getSessionPath; /** * Get the journal file path for a notebook (used for crash-safe commits) */ private getJournalPath; private readJournal; private hasPendingCommit; private getNebulaPaths; /** * Delete notebook-related metadata files (history, session) */ private deleteNotebookMetadata; /** * Delete a file or directory */ deleteFile(filePath: string): boolean; /** * Rename notebook-related metadata files */ private renameNotebookMetadata; /** * Rename/move a file or directory */ renameFile(oldPath: string, newPath: string): FileInfo; /** * Duplicate notebook-related metadata files */ private duplicateNotebookMetadata; /** * Recursively copy a directory */ private copyDirectoryRecursive; /** * Duplicate a file or directory with _copy suffix */ duplicateFile(filePath: string): FileInfoResponse; /** * Upload a file to a directory */ /** * `onConflict` says what an existing file of the same name means: * - 'rename' (default): keep both — store under `name_1.ext`. Right for * browser drag-drop, where a gesture must never clobber. Callers MUST * surface the returned path: the stored name is not the requested one. * - 'overwrite': replace in place (cp semantics). * - 'fail': refuse loudly. For API/CLI callers who named an exact * destination — silently storing elsewhere while reporting success sent * an agent's edits to `train_…_1.py` nobody was reading (lab report). */ uploadFile(destDir: string, tempFilePath: string, originalName: string, onConflict?: 'rename' | 'overwrite' | 'fail'): Promise; /** * Convert Jupyter source to string */ private sourceToString; /** * Convert string to Jupyter source format (array of lines) */ private stringToSource; /** * Convert Jupyter outputs to Nebula format */ private convertOutputs; /** * Convert Nebula outputs back to Jupyter format */ private convertOutputsToJupyter; /** * Read a notebook and convert to internal cell format (async; hot path for * notebook load — large .ipynb reads must not block the event loop) */ getNotebookCells(notebookPath: string): Promise; /** * Sync variant of getNotebookCells. Kept for cold callers that need a * synchronous read (headless cache warm-up, initial-history bootstrap). */ getNotebookCellsSync(notebookPath: string): NotebookCellsResponse; /** * Convert raw notebook file content to the internal cell format (CPU only). */ private buildNotebookCellsResponse; /** * Read a notebook and convert to internal cell format, resolving default kernel if needed */ getNotebookCellsWithKernel(notebookPath: string): Promise; /** * Save cells to a notebook file */ saveNotebookCells(notebookPath: string, cells: NebulaCell[], kernelName?: string, notebookMetadata?: Record): Promise; /** * Save notebook cells and history in a single crash-safe commit. * Uses a small journal + atomic writes to avoid partial files. */ saveNotebookBundle(notebookPath: string, cells: NebulaCell[], kernelName?: string, history?: unknown[], session?: Record, notebookMetadata?: Record): Promise; /** * Save cells to a text-format notebook (.py percent / .qmd). * Outputs and execution counts are never serialized — by format design. */ private saveTextNotebookCells; /** * Get notebook-level metadata */ getNotebookMetadata(notebookPath: string): Record; /** * Async variant of getNotebookMetadata. Nebula-written notebooks put * `metadata` first, so the head-bytes scan avoids reading and parsing the * whole file; externally written notebooks fall back to a full async read. */ getNotebookMetadataAsync(notebookPath: string): Promise>; /** * Update notebook-level metadata without modifying cells */ updateNotebookMetadata(notebookPath: string, metadataUpdates: Record): Promise<{ success: boolean; changed?: boolean; mtime?: number; error?: string; }>; private stripOutputsForHistorySnapshot; private buildInitialHistory; /** * Set agent permission and ensure newly-permitted notebooks are immediately editable. */ setAgentPermission(notebookPath: string, permitted: boolean): Promise<{ success: boolean; error?: string; status?: AgentPermissionSnapshot; mtime?: number; }>; /** * Derive agent permission status from persisted notebook metadata. */ getAgentPermissionStatus(notebookPath: string): AgentPermissionSnapshot; /** * Async variant of getAgentPermissionStatus for hot request paths (it runs * on every agent write operation). Reads metadata via the head-bytes fast * path and checks history without parsing it, so the per-operation gate * never blocks the event loop on a slow network filesystem. */ getAgentPermissionStatusAsync(notebookPath: string): Promise; private deriveAgentPermission; /** * Check if a notebook is permitted for agent modifications */ isAgentPermitted(notebookPath: string): boolean; /** * Check if a notebook has history tracking enabled */ hasHistory(notebookPath: string): boolean; /** * Async variant of hasHistory that only parses small files: history journals * grow to hundreds of KB, and this check runs on every agent operation — a * file that large necessarily has entries, so its size alone answers. */ hasHistoryAsync(notebookPath: string): Promise; /** * Save operation history for a notebook */ saveHistory(notebookPath: string, history: unknown[]): Promise; /** * Load operation history for a notebook (async; history files can be many MB) */ loadHistory(notebookPath: string): Promise; /** * Sync variant of loadHistory. Kept for cold callers that need a * synchronous read (headless undo/redo state bootstrap). */ loadHistorySync(notebookPath: string): unknown[]; /** * Compare the file's current cells against the last-Nebula-save record. * On divergence, append ONE synthesized batch operation describing the * external edit (updateContent / insertCell / deleteCell, source * 'external') — both undo systems replay these, so a single undo step * reverts the external edit and redo reapplies it. Falls back to a plain * snapshot entry if synthesis fails (undo then bottoms out there instead * of corrupting). */ private reconcileExternalTextEdits; private synthesizeExternalEditOps; /** * Save session state for a notebook */ saveSession(notebookPath: string, session: Record): Promise; /** * Load session state for a notebook */ loadSession(notebookPath: string): Record; } export declare const fsService: FilesystemService;