Skip to main content
Glama
state.ts11.2 kB
import * as fs from "fs/promises"; import * as path from "path"; import * as os from "os"; import type { Notebook, Cell, CodeCell, CodeLanguage, PackageJsonCell, } from "./types.js"; import { randomid } from "./types.js"; import { executeCodeCell, installDependencies, writeCodeCellToDisk, writePackageJsonToDisk, writeTsconfigToDisk, type ExecutionResult, } from "./execution.js"; import { encode, decode, createEmptyNotebook } from "./encoding.js"; import { getTemplate, AVAILABLE_TEMPLATES } from "./templates.generated.js"; /** * Sanitize and validate a file path to prevent path traversal attacks * @param userPath User-provided path * @param allowedDir Base directory that paths must resolve within * @returns Sanitized absolute path * @throws Error if path traversal is detected */ function sanitizePath(userPath: string, allowedDir: string): string { const normalized = path.normalize(userPath); const resolved = path.resolve(allowedDir, normalized); // Ensure resolved path stays within allowed directory if (!resolved.startsWith(allowedDir)) { throw new Error("Path traversal not allowed"); } return resolved; } /** * Notebook state manager * Manages in-memory notebook state and execution */ export class NotebookStateManager { private notebooks: Map<string, Notebook> = new Map(); private notebookDirs: Map<string, string> = new Map(); private tempDir: string; constructor(tempDir?: string) { this.tempDir = tempDir || path.join(os.tmpdir(), "thoughtbox-notebooks"); } /** * Initialize temporary directory */ async init(): Promise<void> { await fs.mkdir(this.tempDir, { recursive: true }); } /** * Create a new notebook */ async createNotebook( title: string, language: CodeLanguage ): Promise<Notebook> { const notebook = createEmptyNotebook(title, language); // Create notebook directory const notebookDir = path.join(this.tempDir, notebook.id); await fs.mkdir(notebookDir, { recursive: true }); await fs.mkdir(path.join(notebookDir, "src"), { recursive: true }); // Write initial files to disk await this.writeNotebookToDisk(notebook, notebookDir); // Store in memory this.notebooks.set(notebook.id, notebook); this.notebookDirs.set(notebook.id, notebookDir); return notebook; } /** * Create a notebook from a template */ async createNotebookFromTemplate( title: string, language: CodeLanguage, templateName: string ): Promise<Notebook> { // Load template content from embedded templates let templateContent: string; try { templateContent = getTemplate(templateName); } catch (error) { const available = AVAILABLE_TEMPLATES.join(', '); throw new Error( `Template "${templateName}" not found. Available templates: ${available}` ); } // Substitute placeholders const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format const populatedContent = templateContent .replace(/\[TOPIC\]/g, title) .replace(/\[DATE\]/g, date) .replace(/\[LANGUAGE\]/g, language); // Decode the populated template const notebook = decode(populatedContent); // Override metadata with provided values notebook.language = language; if (language === "typescript" && !notebook["tsconfig.json"]) { const { buildDefaultTsconfig } = await import("./types.js"); notebook["tsconfig.json"] = buildDefaultTsconfig(); } // Create notebook directory const notebookDir = path.join(this.tempDir, notebook.id); await fs.mkdir(notebookDir, { recursive: true }); await fs.mkdir(path.join(notebookDir, "src"), { recursive: true }); // Write files to disk await this.writeNotebookToDisk(notebook, notebookDir); // Store in memory this.notebooks.set(notebook.id, notebook); this.notebookDirs.set(notebook.id, notebookDir); return notebook; } /** * Load a notebook from path or content */ async loadNotebook( source: { path: string } | { content: string } ): Promise<Notebook> { let notebookContent: string; // Determine source if ("path" in source) { // Sanitize path to prevent directory traversal const safePath = sanitizePath(source.path, process.cwd()); notebookContent = await fs.readFile(safePath, "utf8"); } else { notebookContent = source.content; } // Decode from .src.md format const notebook = decode(notebookContent); // Create notebook directory const notebookDir = path.join(this.tempDir, notebook.id); await fs.mkdir(notebookDir, { recursive: true }); await fs.mkdir(path.join(notebookDir, "src"), { recursive: true }); // Write files to disk await this.writeNotebookToDisk(notebook, notebookDir); // Store in memory this.notebooks.set(notebook.id, notebook); this.notebookDirs.set(notebook.id, notebookDir); return notebook; } /** * Get a notebook by ID */ getNotebook(notebookId: string): Notebook | undefined { return this.notebooks.get(notebookId); } /** * Get all notebooks */ listNotebooks(): Notebook[] { return Array.from(this.notebooks.values()); } /** * Add a cell to a notebook */ async addCell( notebookId: string, cell: Cell, position?: number ): Promise<Notebook> { const notebook = this.notebooks.get(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } const notebookDir = this.notebookDirs.get(notebookId)!; // Assign ID if not present if (!cell.id) { (cell as any).id = randomid(); } // Insert at position or append if (position !== undefined && position >= 0 && position <= notebook.cells.length) { notebook.cells.splice(position, 0, cell); } else { notebook.cells.push(cell); } notebook.updatedAt = Date.now(); // Write to disk if it's a code or package.json cell if (cell.type === "code") { await writeCodeCellToDisk(notebookDir, cell.filename, cell.source); } else if (cell.type === "package.json") { await writePackageJsonToDisk(notebookDir, cell.source); } return notebook; } /** * Update a cell in a notebook */ async updateCell( notebookId: string, cellId: string, updates: Partial<Cell> ): Promise<Notebook> { const notebook = this.notebooks.get(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } const cellIndex = notebook.cells.findIndex((c) => c.id === cellId); if (cellIndex === -1) { throw new Error(`Cell ${cellId} not found`); } const notebookDir = this.notebookDirs.get(notebookId)!; const cell = notebook.cells[cellIndex]; // Update cell properties Object.assign(cell, updates); notebook.updatedAt = Date.now(); // Write to disk if source changed if (cell.type === "code") { const codeCell = cell as CodeCell; if ("source" in updates) { await writeCodeCellToDisk(notebookDir, codeCell.filename, codeCell.source); } } else if (cell.type === "package.json") { const pkgCell = cell as PackageJsonCell; if ("source" in updates) { await writePackageJsonToDisk(notebookDir, pkgCell.source); } } return notebook; } /** * Get a cell from a notebook */ getCell(notebookId: string, cellId: string): Cell | undefined { const notebook = this.notebooks.get(notebookId); if (!notebook) return undefined; return notebook.cells.find((c) => c.id === cellId); } /** * Execute a code cell */ async executeCell( notebookId: string, cellId: string ): Promise<ExecutionResult> { const notebook = this.notebooks.get(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } const cell = notebook.cells.find((c) => c.id === cellId); if (!cell) { throw new Error(`Cell ${cellId} not found`); } if (cell.type !== "code" && cell.type !== "package.json") { throw new Error(`Cell ${cellId} is not executable (type: ${cell.type})`); } const notebookDir = this.notebookDirs.get(notebookId)!; // Check if cell is already running to prevent race conditions if (cell.status === "running") { throw new Error(`Cell ${cellId} is already running`); } // Update cell status cell.status = "running"; cell.output = undefined; cell.error = undefined; let result: ExecutionResult; try { if (cell.type === "package.json") { // Run npm install result = await installDependencies({ cwd: notebookDir }); } else { // Execute code cell result = await executeCodeCell(cell.filename, cell.language, { cwd: notebookDir, }); } // Update cell with results if (result.success) { cell.status = "completed"; cell.output = result.stdout || result.stderr; } else { cell.status = "failed"; cell.error = result.stderr || result.stdout; } } catch (error) { cell.status = "failed"; cell.error = error instanceof Error ? error.message : String(error); result = { stdout: "", stderr: cell.error, exitCode: -1, success: false, }; } notebook.updatedAt = Date.now(); return result; } /** * Export notebook to .src.md format * Always returns content string, optionally writes to path */ async exportNotebook( notebookId: string, path?: string ): Promise<string> { const notebook = this.notebooks.get(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } // Always encode to content string const content = encode(notebook); // If path provided, write to filesystem if (path) { const safePath = sanitizePath(path, process.cwd()); await fs.writeFile(safePath, content, "utf8"); } // Always return content return content; } /** * Delete a notebook */ async deleteNotebook(notebookId: string): Promise<void> { const notebookDir = this.notebookDirs.get(notebookId); if (notebookDir) { await fs.rm(notebookDir, { recursive: true, force: true }); } this.notebooks.delete(notebookId); this.notebookDirs.delete(notebookId); } /** * Write all notebook files to disk */ private async writeNotebookToDisk( notebook: Notebook, notebookDir: string ): Promise<void> { // Write tsconfig if TypeScript if (notebook["tsconfig.json"]) { await writeTsconfigToDisk(notebookDir, notebook["tsconfig.json"]); } // Write cells to disk for (const cell of notebook.cells) { if (cell.type === "code") { await writeCodeCellToDisk(notebookDir, cell.filename, cell.source); } else if (cell.type === "package.json") { await writePackageJsonToDisk(notebookDir, cell.source); } } // Write .src.md file const srcmd = encode(notebook); await fs.writeFile(path.join(notebookDir, "README.md"), srcmd, "utf8"); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/glassBead-tc/Thoughtbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server