diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index 63f0ba43..089eee85 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -2,6 +2,10 @@ import { createFreshSandbox } from "@/lib/sandbox/createFreshSandbox"; import { handleAgentRequest } from "@/lib/agent/handleAgentRequest"; import { AGENT_DATA_DIR } from "@/lib/agent/constants"; +/** + * + * @param req + */ export async function POST(req: Request) { return handleAgentRequest(req, () => createFreshSandbox(AGENT_DATA_DIR)); } diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 6aa0cb41..3d8167e4 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -2,8 +2,10 @@ import { createSnapshotSandbox } from "@/lib/sandbox/createSnapshotSandbox"; import { handleAgentRequest } from "@/lib/agent/handleAgentRequest"; import { AGENT_DATA_DIR } from "@/lib/agent/constants"; +/** + * + * @param req + */ export async function POST(req: Request) { - return handleAgentRequest(req, (bearerToken) => - createSnapshotSandbox(bearerToken, AGENT_DATA_DIR), - ); + return handleAgentRequest(req, bearerToken => createSnapshotSandbox(bearerToken, AGENT_DATA_DIR)); } diff --git a/app/api/fs/route.ts b/app/api/fs/route.ts index 0a27632b..836f84ee 100644 --- a/app/api/fs/route.ts +++ b/app/api/fs/route.ts @@ -8,10 +8,12 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "../agent/_agent-data"); // Recursively read all files in a directory -async function readAllFiles( - dir: string, - baseDir: string -): Promise> { +/** + * + * @param dir + * @param baseDir + */ +async function readAllFiles(dir: string, baseDir: string): Promise> { const result: Record = {}; const entries = await readdir(dir, { withFileTypes: true }); @@ -85,6 +87,9 @@ async function readAllFiles( return result; } +/** + * + */ export async function GET() { const files = await readAllFiles(AGENT_DATA_DIR, AGENT_DATA_DIR); diff --git a/app/components/lite-terminal/LiteTerminal.ts b/app/components/lite-terminal/LiteTerminal.ts index 9e9194b0..8281a08c 100644 --- a/app/components/lite-terminal/LiteTerminal.ts +++ b/app/components/lite-terminal/LiteTerminal.ts @@ -39,6 +39,10 @@ export class LiteTerminal { private dirtyLines: Set = new Set(); private lastCursorLine = -1; + /** + * + * @param options + */ constructor(options: LiteTerminalOptions = {}) { this._options = { cursorBlink: true, @@ -88,6 +92,8 @@ export class LiteTerminal { /** * Open terminal in a container element + * + * @param container */ open(container: HTMLElement): void { this.container = container; @@ -134,6 +140,8 @@ export class LiteTerminal { /** * Write data to the terminal + * + * @param data */ write(data: string): void { this.pendingWrites.push(data); @@ -142,6 +150,8 @@ export class LiteTerminal { /** * Write data followed by newline + * + * @param data */ writeln(data: string): void { this.write(data + "\n"); @@ -164,6 +174,8 @@ export class LiteTerminal { /** * Register callback for input data + * + * @param callback */ onData(callback: DataCallback): void { this.inputHandler.onData(callback); @@ -231,6 +243,8 @@ export class LiteTerminal { /** * Process a single parse result + * + * @param result */ private processParseResult(result: ParseResult): void { switch (result.type) { @@ -258,6 +272,8 @@ export class LiteTerminal { /** * Write text to the current position + * + * @param text */ private writeText(text: string): void { for (const char of text) { @@ -271,6 +287,8 @@ export class LiteTerminal { /** * Write a single character at current position + * + * @param char */ private writeChar(char: string): void { const line = this.lines[this.currentLine]; @@ -338,9 +356,7 @@ export class LiteTerminal { } else { // Different style const after = seg.text.slice(1); - const newSegments: StyledSegment[] = [ - { text: char, style: { ...this.currentStyle } }, - ]; + const newSegments: StyledSegment[] = [{ text: char, style: { ...this.currentStyle } }]; if (after) { newSegments.push({ text: after, style: seg.style }); } @@ -389,11 +405,12 @@ export class LiteTerminal { /** * Handle cursor movement commands + * + * @param cursor + * @param cursor.action + * @param cursor.count */ - private handleCursor(cursor: { - action: "left" | "right" | "home"; - count?: number; - }): void { + private handleCursor(cursor: { action: "left" | "right" | "home"; count?: number }): void { const count = cursor.count || 1; switch (cursor.action) { @@ -413,6 +430,8 @@ export class LiteTerminal { /** * Handle clear commands + * + * @param type */ private handleClear(type: "line" | "screen" | "scrollback"): void { switch (type) { @@ -426,10 +445,7 @@ export class LiteTerminal { const segLen = line[segmentIndex].text.length; if (pos + segLen > this.currentCol) { // Truncate this segment - line[segmentIndex].text = line[segmentIndex].text.slice( - 0, - this.currentCol - pos - ); + line[segmentIndex].text = line[segmentIndex].text.slice(0, this.currentCol - pos); segmentIndex++; break; } @@ -456,6 +472,9 @@ export class LiteTerminal { /** * Compare two styles for equality + * + * @param a + * @param b */ private stylesEqual(a: TextStyle, b: TextStyle): boolean { return ( @@ -471,13 +490,18 @@ export class LiteTerminal { /** * Render the terminal content to DOM with inline cursor * Uses incremental updates when possible for better iOS performance + * + * @param forceFullRender */ private render(forceFullRender = false): void { if (!this.outputElement || !this.cursorElement) return; // Full render if forced or if structure changed significantly - if (forceFullRender || this.lineElements.length === 0 || - this.lines.length !== this.lineElements.length) { + if ( + forceFullRender || + this.lineElements.length === 0 || + this.lines.length !== this.lineElements.length + ) { this.fullRender(); return; } @@ -529,6 +553,8 @@ export class LiteTerminal { /** * Re-render a single line + * + * @param lineIndex */ private renderLine(lineIndex: number): void { const lineEl = this.lineElements[lineIndex]; @@ -538,6 +564,9 @@ export class LiteTerminal { /** * Render the content of a single line into a line element + * + * @param lineIndex + * @param lineEl */ private renderLineContent(lineIndex: number, lineEl: HTMLElement): void { if (!this.cursorElement) return; @@ -602,8 +631,14 @@ export class LiteTerminal { /** * Create a styled element - span, anchor, or text node + * + * @param text + * @param style */ - private createStyledSpan(text: string, style: TextStyle): HTMLSpanElement | HTMLAnchorElement | Text | DocumentFragment { + private createStyledSpan( + text: string, + style: TextStyle, + ): HTMLSpanElement | HTMLAnchorElement | Text | DocumentFragment { const classes = this.getStyleClasses(style); const inlineStyle = this.getInlineStyle(style); @@ -640,11 +675,15 @@ export class LiteTerminal { /** * Create text content with clickable URL links (for plain URLs without OSC 8) + * + * @param text + * @param classes + * @param inlineStyle */ private createTextWithLinks( text: string, classes: string, - inlineStyle: string + inlineStyle: string, ): DocumentFragment { const fragment = document.createDocumentFragment(); let lastIndex = 0; @@ -686,8 +725,16 @@ export class LiteTerminal { /** * Create a styled element (span or text node) - helper for createTextWithLinks + * + * @param text + * @param classes + * @param inlineStyle */ - private createStyledElement(text: string, classes: string, inlineStyle: string): HTMLSpanElement | Text { + private createStyledElement( + text: string, + classes: string, + inlineStyle: string, + ): HTMLSpanElement | Text { if (!classes && !inlineStyle) { return document.createTextNode(text); } @@ -706,8 +753,9 @@ export class LiteTerminal { const charWidth = this.measureCharWidth(); const computedStyle = getComputedStyle(this.outputElement); - const lineHeight = parseFloat(computedStyle.lineHeight) || - (this._options.fontSize! * (this._options.lineHeight || 1.2)); + const lineHeight = + parseFloat(computedStyle.lineHeight) || + this._options.fontSize! * (this._options.lineHeight || 1.2); this.cursorElement.style.width = `${charWidth}px`; this.cursorElement.style.height = `${lineHeight}px`; @@ -715,13 +763,28 @@ export class LiteTerminal { // Allowlist of valid color class names (prevents XSS via class injection) private static readonly VALID_COLOR_CLASSES = new Set([ - "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", - "brightBlack", "brightRed", "brightGreen", "brightYellow", - "brightBlue", "brightMagenta", "brightCyan", "brightWhite", + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "brightBlack", + "brightRed", + "brightGreen", + "brightYellow", + "brightBlue", + "brightMagenta", + "brightCyan", + "brightWhite", ]); /** * Get CSS classes for a text style + * + * @param style */ private getStyleClasses(style: TextStyle): string { const classes: string[] = []; @@ -744,6 +807,8 @@ export class LiteTerminal { /** * Get inline style for RGB colors + * + * @param style */ private getInlineStyle(style: TextStyle): string { // Only allow properly formatted rgb() values (XSS protection) @@ -753,8 +818,6 @@ export class LiteTerminal { return ""; } - - /** * Scroll to bottom of terminal (uses window scroll) */ @@ -803,22 +866,13 @@ export class LiteTerminal { const theme = this._options.theme || {}; - this.container.style.setProperty( - "background-color", - theme.background || "#000" - ); + this.container.style.setProperty("background-color", theme.background || "#000"); this.container.style.setProperty("color", theme.foreground || "#e0e0e0"); // Set CSS custom properties for colors this.container.style.setProperty("--term-cyan", theme.cyan || "#0AC5B3"); - this.container.style.setProperty( - "--term-brightCyan", - theme.brightCyan || "#3DD9C8" - ); - this.container.style.setProperty( - "--term-brightBlack", - theme.brightBlack || "#666" - ); + this.container.style.setProperty("--term-brightCyan", theme.brightCyan || "#3DD9C8"); + this.container.style.setProperty("--term-brightBlack", theme.brightBlack || "#666"); // Cursor color if (this.cursorElement) { diff --git a/app/components/lite-terminal/ansi-parser.ts b/app/components/lite-terminal/ansi-parser.ts index 1d8e38f0..dc3ff05e 100644 --- a/app/components/lite-terminal/ansi-parser.ts +++ b/app/components/lite-terminal/ansi-parser.ts @@ -13,7 +13,7 @@ export interface ParseResult { // SGR (Select Graphic Rendition) parameter handlers const SGR_HANDLERS: Record void> = { - 0: (s) => { + 0: s => { // Reset all attributes delete s.bold; delete s.dim; @@ -21,85 +21,88 @@ const SGR_HANDLERS: Record void> = { delete s.underline; delete s.color; }, - 1: (s) => { + 1: s => { s.bold = true; }, - 2: (s) => { + 2: s => { s.dim = true; }, - 3: (s) => { + 3: s => { s.italic = true; }, - 4: (s) => { + 4: s => { s.underline = true; }, - 22: (s) => { + 22: s => { delete s.bold; delete s.dim; }, - 23: (s) => { + 23: s => { delete s.italic; }, - 24: (s) => { + 24: s => { delete s.underline; }, // Standard colors (foreground) - 30: (s) => { + 30: s => { s.color = "black"; }, - 31: (s) => { + 31: s => { s.color = "red"; }, - 32: (s) => { + 32: s => { s.color = "green"; }, - 33: (s) => { + 33: s => { s.color = "yellow"; }, - 34: (s) => { + 34: s => { s.color = "blue"; }, - 35: (s) => { + 35: s => { s.color = "magenta"; }, - 36: (s) => { + 36: s => { s.color = "cyan"; }, - 37: (s) => { + 37: s => { s.color = "white"; }, - 39: (s) => { + 39: s => { delete s.color; }, // Default foreground // Bright colors - 90: (s) => { + 90: s => { s.color = "brightBlack"; }, - 91: (s) => { + 91: s => { s.color = "brightRed"; }, - 92: (s) => { + 92: s => { s.color = "brightGreen"; }, - 93: (s) => { + 93: s => { s.color = "brightYellow"; }, - 94: (s) => { + 94: s => { s.color = "brightBlue"; }, - 95: (s) => { + 95: s => { s.color = "brightMagenta"; }, - 96: (s) => { + 96: s => { s.color = "brightCyan"; }, - 97: (s) => { + 97: s => { s.color = "brightWhite"; }, }; /** * Parse SGR (Select Graphic Rendition) parameters and update style + * + * @param params + * @param style */ function parseSGR(params: string, style: TextStyle): Partial { const parts = params ? params.split(";").map(Number) : [0]; @@ -181,6 +184,8 @@ export class AnsiParser { /** * Parse text with ANSI escape codes * Returns an array of parse results + * + * @param text */ parse(text: string): ParseResult[] { const results: ParseResult[] = []; @@ -210,10 +215,7 @@ export class AnsiParser { if (nextChar === "[") { // Find the end of the CSI sequence (letter A-Z, a-z, or @) let j = i + 2; - while ( - j < this.buffer.length && - !/[A-Za-z@~]/.test(this.buffer[j]) - ) { + while (j < this.buffer.length && !/[A-Za-z@~]/.test(this.buffer[j])) { j++; } @@ -330,6 +332,9 @@ export class AnsiParser { /** * Handle CSI (Control Sequence Introducer) sequences + * + * @param params + * @param cmd */ private handleCSI(params: string, cmd: string): ParseResult | null { switch (cmd) { @@ -387,6 +392,8 @@ export class AnsiParser { /** * Handle OSC (Operating System Command) sequences + * + * @param content */ private handleOSC(content: string): ParseResult | null { // OSC 8 - Hyperlinks: 8;;URL or 8;params;URL diff --git a/app/components/lite-terminal/index.ts b/app/components/lite-terminal/index.ts index 742c9c87..76b5aeb8 100644 --- a/app/components/lite-terminal/index.ts +++ b/app/components/lite-terminal/index.ts @@ -1,7 +1,2 @@ export { LiteTerminal } from "./LiteTerminal"; -export type { - LiteTerminalOptions, - ThemeConfig, - TextStyle, - DataCallback, -} from "./types"; +export type { LiteTerminalOptions, ThemeConfig, TextStyle, DataCallback } from "./types"; diff --git a/app/components/lite-terminal/input-handler.ts b/app/components/lite-terminal/input-handler.ts index d1a50689..f2718b57 100644 --- a/app/components/lite-terminal/input-handler.ts +++ b/app/components/lite-terminal/input-handler.ts @@ -17,6 +17,8 @@ export class InputHandler { /** * Attach input handling to an element + * + * @param container */ attach(container: HTMLElement): void { this.container = container; @@ -85,6 +87,8 @@ export class InputHandler { /** * Register a callback for input data + * + * @param callback */ onData(callback: DataCallback): void { this.callbacks.push(callback); @@ -92,6 +96,8 @@ export class InputHandler { /** * Emit data to all registered callbacks + * + * @param data */ private emit(data: string): void { for (const cb of this.callbacks) { @@ -177,6 +183,9 @@ export class InputHandler { this.container?.classList.remove("focused"); }; + /** + * + */ private scrollCursorIntoView(): void { if (!this.container) return; diff --git a/app/components/terminal-parts/agent-command.ts b/app/components/terminal-parts/agent-command.ts index b829b2c3..7a006660 100644 --- a/app/components/terminal-parts/agent-command.ts +++ b/app/components/terminal-parts/agent-command.ts @@ -13,10 +13,20 @@ type TerminalWriter = { }; // Format text for terminal: normalize newlines and convert tabs to spaces +/** + * + * @param text + */ function formatForTerminal(text: string): string { return text.replace(/\t/g, " ").replace(/\r?\n/g, "\r\n"); } +/** + * + * @param term + * @param getAccessToken + * @param agentEndpoint + */ export function createAgentCommand( term: TerminalWriter, getAccessToken: () => Promise, @@ -25,12 +35,13 @@ export function createAgentCommand( const agentMessages: UIMessage[] = []; let messageIdCounter = 0; - const agentCmd = defineCommand("agent", async (args) => { + const agentCmd = defineCommand("agent", async args => { const prompt = args.join(" "); if (!prompt) { return { stdout: "", - stderr: "Usage: agent \nExample: agent how do I use custom commands?\n\nThis is a multi-turn chat. Use 'agent reset' to clear history.\n", + stderr: + "Usage: agent \nExample: agent how do I use custom commands?\n\nThis is a multi-turn chat. Use 'agent reset' to clear history.\n", exitCode: 1, }; } @@ -157,7 +168,7 @@ export function createAgentCommand( if (displayResult && displayResult.trim()) { const resultLines = displayResult.split("\n").filter((l: string) => l.trim()); const linesToShow = resultLines.slice(0, MAX_TOOL_OUTPUT_LINES); - let output = linesToShow.map((line) => `\x1b[2m${line}\x1b[0m`).join("\n"); + let output = linesToShow.map(line => `\x1b[2m${line}\x1b[0m`).join("\n"); if (resultLines.length > MAX_TOOL_OUTPUT_LINES) { output += `\n\x1b[2m... (${resultLines.length - MAX_TOOL_OUTPUT_LINES} more lines)\x1b[0m`; } @@ -245,7 +256,8 @@ export function createAgentCommand( else if (data.type === "tool-output-available" && data.toolCallId) { const existing = toolCallsMap.get(data.toolCallId); const result = data.output; - const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2); + const resultStr = + typeof result === "string" ? result : JSON.stringify(result, null, 2); const tc = { toolName: existing?.toolName || "tool", @@ -266,13 +278,11 @@ export function createAgentCommand( // Start streaming thinking in dim italic isStreaming = true; term.write("\x1b[2m\x1b[3m"); // dim + italic - } - else if (data.type === "reasoning-delta" && data.delta) { + } else if (data.type === "reasoning-delta" && data.delta) { // Stream thinking tokens as they arrive term.write(formatForTerminal(data.delta)); resetThinkingTimer(); // Keep resetting while actively streaming - } - else if (data.type === "reasoning-end") { + } else if (data.type === "reasoning-end") { // End thinking block if (isStreaming) { term.write("\x1b[0m\r\n"); // reset styling + newline @@ -283,19 +293,15 @@ export function createAgentCommand( else if (data.type === "error") { const errorMsg = data.error || data.message || "Unknown error"; term.write(`\x1b[31mError: ${formatForTerminal(String(errorMsg))}\x1b[0m\r\n`); - } - else if (data.type === "tool-input-error") { + } else if (data.type === "tool-input-error") { const errorMsg = data.error || "Tool input error"; term.write(`\x1b[31m[Tool Error] ${formatForTerminal(String(errorMsg))}\x1b[0m\r\n`); - } - else if (data.type === "tool-output-error") { + } else if (data.type === "tool-output-error") { const errorMsg = data.error || "Tool execution error"; term.write(`\x1b[31m[Tool Error] ${formatForTerminal(String(errorMsg))}\x1b[0m\r\n`); - } - else if (data.type === "tool-output-denied") { + } else if (data.type === "tool-output-denied") { term.write(`\x1b[33m[Tool Denied]\x1b[0m\r\n`); - } - else if (data.type === "abort") { + } else if (data.type === "abort") { term.write(`\x1b[33m[Aborted]\x1b[0m\r\n`); } } catch (e) { diff --git a/app/components/terminal-parts/commands.ts b/app/components/terminal-parts/commands.ts index 82b62dd3..423cc2fe 100644 --- a/app/components/terminal-parts/commands.ts +++ b/app/components/terminal-parts/commands.ts @@ -1,6 +1,9 @@ import { defineCommand } from "just-bash/browser"; import { getTerminalData } from "../TerminalData"; +/** + * + */ export function createStaticCommands() { const aboutCmd = defineCommand("about", async () => ({ stdout: getTerminalData("cmd-about"), diff --git a/app/components/terminal-parts/input-handler.ts b/app/components/terminal-parts/input-handler.ts index b3e97bb7..762b5a7b 100644 --- a/app/components/terminal-parts/input-handler.ts +++ b/app/components/terminal-parts/input-handler.ts @@ -10,8 +10,12 @@ type Terminal = { onData: (callback: (data: string) => void) => void; }; - // Find the start of the previous word +/** + * + * @param str + * @param pos + */ function findPrevWordBoundary(str: string, pos: number): number { if (pos <= 0) return 0; let i = pos - 1; @@ -23,6 +27,11 @@ function findPrevWordBoundary(str: string, pos: number): number { } // Find the end of the next word +/** + * + * @param str + * @param pos + */ function findNextWordBoundary(str: string, pos: number): number { const len = str.length; if (pos >= len) return len; @@ -35,7 +44,15 @@ function findNextWordBoundary(str: string, pos: number): number { } // Extract the word being typed for completion -function getCompletionContext(cmd: string, cursorPos: number): { prefix: string; wordStart: number } { +/** + * + * @param cmd + * @param cursorPos + */ +function getCompletionContext( + cmd: string, + cursorPos: number, +): { prefix: string; wordStart: number } { let wordStart = cursorPos; // Walk back to find the start of the current word while (wordStart > 0 && cmd[wordStart - 1] !== " ") { @@ -47,10 +64,13 @@ function getCompletionContext(cmd: string, cursorPos: number): { prefix: string; }; } +/** + * + * @param term + * @param bash + */ export function createInputHandler(term: Terminal, bash: Bash) { - const history: string[] = JSON.parse( - sessionStorage.getItem(HISTORY_KEY) || "[]" - ); + const history: string[] = JSON.parse(sessionStorage.getItem(HISTORY_KEY) || "[]"); let cmd = ""; let cursorPos = 0; let historyIndex = history.length; @@ -201,14 +221,12 @@ export function createInputHandler(term: Terminal, bash: Bash) { const lsResult = await bash.exec("ls -1"); candidates = lsResult.stdout .split("\n") - .map((f) => f.trim()) - .filter((f) => f.length > 0); + .map(f => f.trim()) + .filter(f => f.length > 0); } // Find matching candidates - const matches = candidates.filter((c) => - c.toLowerCase().startsWith(prefix.toLowerCase()) - ); + const matches = candidates.filter(c => c.toLowerCase().startsWith(prefix.toLowerCase())); if (matches.length === 0) { // No matches - do nothing @@ -264,19 +282,14 @@ export function createInputHandler(term: Terminal, bash: Bash) { history.push(trimmed); historyIndex = history.length; - sessionStorage.setItem( - HISTORY_KEY, - JSON.stringify(history.slice(-MAX_HISTORY)) - ); + sessionStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(-MAX_HISTORY))); if (trimmed === "clear") { term.write("\x1b[2J\x1b[3J\x1b[H"); } else { const result = await bash.exec(trimmed); if (result.stdout) - term.write( - formatMarkdown(colorizeUrls(result.stdout)).replace(/\n/g, "\r\n") - ); + term.write(formatMarkdown(colorizeUrls(result.stdout)).replace(/\n/g, "\r\n")); if (result.stderr) term.write(result.stderr.replace(/\n/g, "\r\n")); } diff --git a/app/components/terminal-parts/markdown.ts b/app/components/terminal-parts/markdown.ts index cdd13236..5d0ef06f 100644 --- a/app/components/terminal-parts/markdown.ts +++ b/app/components/terminal-parts/markdown.ts @@ -10,6 +10,8 @@ const RESET = "\x1b[0m"; /** * Apply terminal formatting to markdown-style text. * Preserves the actual characters but wraps them in ANSI escape sequences. + * + * @param text */ export function formatMarkdown(text: string): string { let result = text; @@ -36,7 +38,7 @@ export function formatMarkdown(text: string): string { }); // Inline code: `code` (single line only, no nested backticks) - result = result.replace(/`([^`\n]+)`/g, (match) => { + result = result.replace(/`([^`\n]+)`/g, match => { return `${CYAN}${match}${RESET}`; }); diff --git a/app/components/terminal-parts/welcome.ts b/app/components/terminal-parts/welcome.ts index f9882ab1..12f9e4c3 100644 --- a/app/components/terminal-parts/welcome.ts +++ b/app/components/terminal-parts/welcome.ts @@ -6,6 +6,10 @@ type Terminal = { cols: number; }; +/** + * + * @param term + */ export function showWelcome(term: Terminal) { term.writeln(""); @@ -20,7 +24,9 @@ export function showWelcome(term: Terminal) { } term.writeln(""); - term.writeln("\x1b[2mA sandboxed bash interpreter for AI agents. Pure TypeScript with in-memory filesystem. From \x1b]8;;https://vercel.com\x07\x1b[4m\x1b[36mVercel Labs\x1b[0m\x1b[2m\x1b]8;;\x07.\x1b[0m"); + term.writeln( + "\x1b[2mA sandboxed bash interpreter for AI agents. Pure TypeScript with in-memory filesystem. From \x1b]8;;https://vercel.com\x07\x1b[4m\x1b[36mVercel Labs\x1b[0m\x1b[2m\x1b]8;;\x07.\x1b[0m", + ); term.writeln(""); term.writeln(" \x1b[1m\x1b[36mnpm install just-bash\x1b[0m"); term.writeln(""); @@ -29,11 +35,11 @@ export function showWelcome(term: Terminal) { term.writeln("\x1b[2m const { stdout } = await bash.exec(\n 'echo hello');\x1b[0m"); term.writeln(""); term.writeln( - "\x1b[2mCommands:\x1b[0m \x1b[36mabout\x1b[0m, \x1b[36minstall\x1b[0m, \x1b[36mgithub\x1b[0m, \x1b[36magent\x1b[0m, \x1b[36mhelp\x1b[0m" + "\x1b[2mCommands:\x1b[0m \x1b[36mabout\x1b[0m, \x1b[36minstall\x1b[0m, \x1b[36mgithub\x1b[0m, \x1b[36magent\x1b[0m, \x1b[36mhelp\x1b[0m", ); term.writeln( - "\x1b[2mTry:\x1b[0m \x1b[36mls\x1b[0m | \x1b[36mhead\x1b[0m, \x1b[36mgrep\x1b[0m bash README.md, \x1b[36mtree\x1b[0m, \x1b[36mcat\x1b[0m package.json | \x1b[36mjq\x1b[0m .version" + "\x1b[2mTry:\x1b[0m \x1b[36mls\x1b[0m | \x1b[36mhead\x1b[0m, \x1b[36mgrep\x1b[0m bash README.md, \x1b[36mtree\x1b[0m, \x1b[36mcat\x1b[0m package.json | \x1b[36mjq\x1b[0m .version", ); term.writeln(""); term.write("$ "); -} \ No newline at end of file +} diff --git a/app/hooks/useSetupSandbox.ts b/app/hooks/useSetupSandbox.ts index af498b72..8b53e2ea 100644 --- a/app/hooks/useSetupSandbox.ts +++ b/app/hooks/useSetupSandbox.ts @@ -3,6 +3,9 @@ import { usePrivy } from "@privy-io/react-auth"; import { getSandboxes } from "@/lib/recoup-api/getSandboxes"; import { setupSandbox } from "@/lib/recoup-api/setupSandbox"; +/** + * + */ export function useSetupSandbox() { const { authenticated, getAccessToken } = usePrivy(); const hasRun = useRef(false); diff --git a/app/md/[[...path]]/route.ts b/app/md/[[...path]]/route.ts index abc3068a..74f9e183 100644 --- a/app/md/[[...path]]/route.ts +++ b/app/md/[[...path]]/route.ts @@ -8,33 +8,42 @@ import { const FILES: Record = { "README.md": FILE_README, - "LICENSE": FILE_LICENSE, + LICENSE: FILE_LICENSE, "package.json": FILE_PACKAGE_JSON, "AGENTS.md": FILE_AGENTS_MD, "wtf-is-this.md": FILE_WTF_IS_THIS, }; +/** + * + */ export function generateStaticParams() { return [ { path: [] }, // /md -> README.md - ...Object.keys(FILES).map((file) => ({ path: [file] })), + ...Object.keys(FILES).map(file => ({ path: [file] })), ]; } -export async function GET( - _request: Request, - { params }: { params: Promise<{ path?: string[] }> } -) { +/** + * + * @param _request + * @param root0 + * @param root0.params + */ +export async function GET(_request: Request, { params }: { params: Promise<{ path?: string[] }> }) { let { path } = await params; const filePath = path?.join("/").replace(/^md\//, "") || "README.md"; const content = FILES[filePath]; if (!content) { - return new Response(`File not found: ${filePath}\n\nAvailable files:\n${Object.keys(FILES).join("\n")}`, { - status: 404, - headers: { "Content-Type": "text/plain" }, - }); + return new Response( + `File not found: ${filePath}\n\nAvailable files:\n${Object.keys(FILES).join("\n")}`, + { + status: 404, + headers: { "Content-Type": "text/plain" }, + }, + ); } const contentType = filePath.endsWith(".json") diff --git a/lib/agent/createAgentResponse.ts b/lib/agent/createAgentResponse.ts index f5e0e9a3..8cf090eb 100644 --- a/lib/agent/createAgentResponse.ts +++ b/lib/agent/createAgentResponse.ts @@ -5,6 +5,12 @@ import { after } from "next/server"; import { SANDBOX_CWD, SYSTEM_INSTRUCTIONS, TOOL_PROMPT } from "./constants"; import { saveSnapshot } from "@/lib/sandbox/saveSnapshot"; +/** + * + * @param sandbox + * @param messages + * @param bearerToken + */ export async function createAgentResponse( sandbox: Sandbox, messages: unknown[], diff --git a/lib/agent/handleAgentRequest.ts b/lib/agent/handleAgentRequest.ts index ea19cbe6..d63fd45e 100644 --- a/lib/agent/handleAgentRequest.ts +++ b/lib/agent/handleAgentRequest.ts @@ -3,6 +3,11 @@ import { createAgentResponse } from "./createAgentResponse"; type CreateSandbox = (bearerToken: string) => Promise; +/** + * + * @param req + * @param createSandbox + */ export async function handleAgentRequest( req: Request, createSandbox: CreateSandbox, @@ -15,9 +20,7 @@ export async function handleAgentRequest( const bearerToken = authHeader.slice("Bearer ".length); const { messages } = await req.json(); - const lastUserMessage = messages - .filter((m: { role: string }) => m.role === "user") - .pop(); + const lastUserMessage = messages.filter((m: { role: string }) => m.role === "user").pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); const sandbox = await createSandbox(bearerToken); diff --git a/lib/recoup-api/createSandbox.ts b/lib/recoup-api/createSandbox.ts index 56ec3b35..6c480563 100644 --- a/lib/recoup-api/createSandbox.ts +++ b/lib/recoup-api/createSandbox.ts @@ -1,8 +1,10 @@ import { RECOUP_API_URL } from "@/lib/consts"; -export async function createSandbox( - bearerToken: string, -): Promise { +/** + * + * @param bearerToken + */ +export async function createSandbox(bearerToken: string): Promise { try { const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { method: "POST", diff --git a/lib/recoup-api/getSandboxes.ts b/lib/recoup-api/getSandboxes.ts index fe7569c7..131e3e7e 100644 --- a/lib/recoup-api/getSandboxes.ts +++ b/lib/recoup-api/getSandboxes.ts @@ -1,5 +1,9 @@ import { RECOUP_API_URL } from "@/lib/consts"; +/** + * + * @param bearerToken + */ export async function getSandboxes(bearerToken: string) { const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { headers: { Authorization: `Bearer ${bearerToken}` }, diff --git a/lib/recoup-api/setupSandbox.ts b/lib/recoup-api/setupSandbox.ts index 7f193eda..85e79474 100644 --- a/lib/recoup-api/setupSandbox.ts +++ b/lib/recoup-api/setupSandbox.ts @@ -1,5 +1,9 @@ import { RECOUP_API_URL } from "@/lib/consts"; +/** + * + * @param bearerToken + */ export function setupSandbox(bearerToken: string) { fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, { method: "POST", diff --git a/lib/recoup-api/updateAccountSnapshot.ts b/lib/recoup-api/updateAccountSnapshot.ts index b2bb7626..04280baa 100644 --- a/lib/recoup-api/updateAccountSnapshot.ts +++ b/lib/recoup-api/updateAccountSnapshot.ts @@ -1,5 +1,10 @@ import { RECOUP_API_URL } from "@/lib/consts"; +/** + * + * @param bearerToken + * @param snapshotId + */ export async function updateAccountSnapshot( bearerToken: string, snapshotId: string, diff --git a/lib/sandbox/createFreshSandbox.ts b/lib/sandbox/createFreshSandbox.ts index 8026447c..d92e3381 100644 --- a/lib/sandbox/createFreshSandbox.ts +++ b/lib/sandbox/createFreshSandbox.ts @@ -1,6 +1,10 @@ import { Sandbox } from "@vercel/sandbox"; import { readSourceFiles } from "./readSourceFiles"; +/** + * + * @param agentDataDir + */ export async function createFreshSandbox(agentDataDir: string): Promise { const sandbox = await Sandbox.create(); diff --git a/lib/sandbox/createSnapshotSandbox.ts b/lib/sandbox/createSnapshotSandbox.ts index 935b753e..c4550895 100644 --- a/lib/sandbox/createSnapshotSandbox.ts +++ b/lib/sandbox/createSnapshotSandbox.ts @@ -2,6 +2,11 @@ import { Sandbox } from "@vercel/sandbox"; import { createSandbox } from "@/lib/recoup-api/createSandbox"; import { createFreshSandbox } from "./createFreshSandbox"; +/** + * + * @param bearerToken + * @param agentDataDir + */ export async function createSnapshotSandbox( bearerToken: string, agentDataDir: string, diff --git a/lib/sandbox/readSourceFiles.ts b/lib/sandbox/readSourceFiles.ts index 3d0e5afc..30652efe 100644 --- a/lib/sandbox/readSourceFiles.ts +++ b/lib/sandbox/readSourceFiles.ts @@ -2,6 +2,11 @@ import { readdirSync, readFileSync } from "fs"; import { join, relative } from "path"; import { SANDBOX_CWD } from "@/lib/agent/constants"; +/** + * + * @param dir + * @param baseDir + */ export function readSourceFiles( dir: string, baseDir?: string, diff --git a/lib/sandbox/saveSnapshot.ts b/lib/sandbox/saveSnapshot.ts index 68eab9c1..8af9cadc 100644 --- a/lib/sandbox/saveSnapshot.ts +++ b/lib/sandbox/saveSnapshot.ts @@ -1,10 +1,12 @@ import { Sandbox } from "@vercel/sandbox"; import { updateAccountSnapshot } from "@/lib/recoup-api/updateAccountSnapshot"; -export async function saveSnapshot( - sandbox: Sandbox, - bearerToken: string, -): Promise { +/** + * + * @param sandbox + * @param bearerToken + */ +export async function saveSnapshot(sandbox: Sandbox, bearerToken: string): Promise { try { const result = await sandbox.snapshot(); await updateAccountSnapshot(bearerToken, result.snapshotId); diff --git a/next.config.ts b/next.config.ts index b7df6f70..28371bab 100644 --- a/next.config.ts +++ b/next.config.ts @@ -33,7 +33,7 @@ const nextConfig: NextConfig = { }, ], }; - } + }, }; export default nextConfig;