diff --git a/apps/server/package.json b/apps/server/package.json index eb900df46..2b4f5578d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@browseros/server", - "version": "0.0.73", + "version": "0.0.74", "description": "BrowserOS server", "type": "module", "main": "./src/index.ts", diff --git a/apps/server/src/agent/ai-sdk-agent.ts b/apps/server/src/agent/ai-sdk-agent.ts index dbbf312d1..d2edc5abb 100644 --- a/apps/server/src/agent/ai-sdk-agent.ts +++ b/apps/server/src/agent/ai-sdk-agent.ts @@ -72,7 +72,7 @@ export class AiSdkAgent { const allBrowserTools = buildBrowserToolSet( config.registry, config.browser, - config.resolvedConfig.sessionExecutionDir, + config.resolvedConfig.workingDir, ) const browserTools = config.resolvedConfig.chatMode ? Object.fromEntries( @@ -98,7 +98,7 @@ export class AiSdkAgent { // Add filesystem tools (Pi coding agent) — skip in chat mode (read-only) const filesystemTools = config.resolvedConfig.chatMode ? {} - : buildFilesystemToolSet(config.resolvedConfig.sessionExecutionDir) + : buildFilesystemToolSet(config.resolvedConfig.workingDir) const memoryTools = config.resolvedConfig.chatMode ? {} : buildMemoryToolSet() @@ -143,7 +143,7 @@ export class AiSdkAgent { exclude: excludeSections, isScheduledTask: config.resolvedConfig.isScheduledTask, scheduledTaskWindowId: config.browserContext?.windowId, - workspaceDir: config.resolvedConfig.sessionExecutionDir, + workspaceDir: config.resolvedConfig.workingDir, soulContent, isSoulBootstrap: isBootstrap, chatMode: config.resolvedConfig.chatMode, diff --git a/apps/server/src/agent/tool-adapter.ts b/apps/server/src/agent/tool-adapter.ts index 2921c06f0..17ee79066 100644 --- a/apps/server/src/agent/tool-adapter.ts +++ b/apps/server/src/agent/tool-adapter.ts @@ -38,12 +38,12 @@ function contentToModelOutput( export function buildBrowserToolSet( registry: ToolRegistry, browser: Browser, - executionDir: string, + workingDir: string, ): ToolSet { const toolSet: ToolSet = {} const ctx: ToolContext = { browser, - directories: { executionDir }, + directories: { workingDir }, } for (const def of registry.all()) { diff --git a/apps/server/src/agent/types.ts b/apps/server/src/agent/types.ts index ee86dfa01..a29adcfe3 100644 --- a/apps/server/src/agent/types.ts +++ b/apps/server/src/agent/types.ts @@ -32,7 +32,7 @@ export interface ResolvedAgentConfig { sessionToken?: string contextWindowSize?: number userSystemPrompt?: string - sessionExecutionDir: string + workingDir: string /** Whether the model supports image inputs (vision). Defaults to true. */ supportsImages?: boolean /** Eval mode - enables window management tools. Defaults to false. */ diff --git a/apps/server/src/api/routes/chat.ts b/apps/server/src/api/routes/chat.ts index 64dc98b2e..6708edcad 100644 --- a/apps/server/src/api/routes/chat.ts +++ b/apps/server/src/api/routes/chat.ts @@ -1,4 +1,3 @@ -import { PATHS } from '@browseros/shared/constants/paths' import { zValidator } from '@hono/zod-validator' import { Hono } from 'hono' import { SessionStore } from '../../agent/session-store' @@ -17,21 +16,18 @@ import { ConversationIdParamSchema } from '../utils/validation' interface ChatRouteDeps { browser: Browser registry: ToolRegistry - executionDir?: string browserosId?: string rateLimiter?: RateLimiter } export function createChatRoutes(deps: ChatRouteDeps) { const { browserosId, rateLimiter } = deps - const executionDir = deps.executionDir || PATHS.DEFAULT_EXECUTION_DIR const sessionStore = new SessionStore() const klavisClient = new KlavisClient() const service = new ChatService({ sessionStore, klavisClient, - executionDir, browser: deps.browser, registry: deps.registry, browserosId, diff --git a/apps/server/src/api/server.ts b/apps/server/src/api/server.ts index c21e76266..00ab2f5f0 100644 --- a/apps/server/src/api/server.ts +++ b/apps/server/src/api/server.ts @@ -130,7 +130,6 @@ export async function createHttpServer(config: HttpServerConfig) { createChatRoutes({ browser, registry, - executionDir, browserosId, rateLimiter, }), diff --git a/apps/server/src/api/services/chat-service.ts b/apps/server/src/api/services/chat-service.ts index 0de234e81..c28ca6343 100644 --- a/apps/server/src/api/services/chat-service.ts +++ b/apps/server/src/api/services/chat-service.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { mkdir } from 'node:fs/promises' +import { mkdir, utimes } from 'node:fs/promises' import path from 'node:path' import { createAgentUIStreamResponse, type UIMessage } from 'ai' import { AiSdkAgent } from '../../agent/ai-sdk-agent' @@ -12,6 +12,7 @@ import { formatUserMessage } from '../../agent/format-message' import type { SessionStore } from '../../agent/session-store' import type { ResolvedAgentConfig } from '../../agent/types' import type { Browser } from '../../browser/browser' +import { getSessionsDir } from '../../lib/browseros-dir' import type { KlavisClient } from '../../lib/clients/klavis/klavis-client' import { resolveLLMConfig } from '../../lib/clients/llm/config' import { logger } from '../../lib/logger' @@ -21,7 +22,6 @@ import type { BrowserContext, ChatRequest } from '../types' export interface ChatServiceDeps { sessionStore: SessionStore klavisClient: KlavisClient - executionDir: string browser: Browser registry: ToolRegistry browserosId?: string @@ -38,7 +38,7 @@ export class ChatService { const llmConfig = await resolveLLMConfig(request, this.deps.browserosId) - const sessionExecutionDir = await this.resolveSessionDir(request) + const workingDir = await this.resolveSessionDir(request) const agentConfig: ResolvedAgentConfig = { conversationId: request.conversationId, @@ -54,7 +54,7 @@ export class ChatService { sessionToken: llmConfig.sessionToken, contextWindowSize: request.contextWindowSize, userSystemPrompt: request.userSystemPrompt, - sessionExecutionDir, + workingDir, supportsImages: request.supportsImages, chatMode: request.mode === 'chat', isScheduledTask: request.isScheduledTask, @@ -261,8 +261,12 @@ export class ChatService { private async resolveSessionDir(request: ChatRequest): Promise { const dir = request.userWorkingDir ? request.userWorkingDir - : path.join(this.deps.executionDir, 'sessions', request.conversationId) + : path.join(getSessionsDir(), request.conversationId) await mkdir(dir, { recursive: true }) + if (!request.userWorkingDir) { + const now = new Date() + await utimes(dir, now, now).catch(() => {}) + } return dir } } diff --git a/apps/server/src/api/services/mcp/mcp-server.ts b/apps/server/src/api/services/mcp/mcp-server.ts index cd713ae67..e3daba2df 100644 --- a/apps/server/src/api/services/mcp/mcp-server.ts +++ b/apps/server/src/api/services/mcp/mcp-server.ts @@ -42,7 +42,7 @@ export function createMcpServer(deps: McpServiceDeps): McpServer { registerTools(server, deps.registry, { browser: deps.browser, directories: { - executionDir: deps.executionDir, + workingDir: deps.executionDir, resourcesDir: deps.resourcesDir, }, }) diff --git a/apps/server/src/lib/browseros-dir.ts b/apps/server/src/lib/browseros-dir.ts index 6ef19489a..b325a0536 100644 --- a/apps/server/src/lib/browseros-dir.ts +++ b/apps/server/src/lib/browseros-dir.ts @@ -1,7 +1,8 @@ -import { mkdir } from 'node:fs/promises' +import { mkdir, readdir, rm, stat } from 'node:fs/promises' import { homedir } from 'node:os' import { join } from 'node:path' import { PATHS } from '@browseros/shared/constants/paths' +import { logger } from './logger' export function getBrowserosDir(): string { return join(homedir(), PATHS.BROWSEROS_DIR_NAME) @@ -11,6 +12,10 @@ export function getMemoryDir(): string { return join(getBrowserosDir(), PATHS.MEMORY_DIR_NAME) } +export function getSessionsDir(): string { + return join(getBrowserosDir(), PATHS.SESSIONS_DIR_NAME) +} + export function getSoulPath(): string { return join(getBrowserosDir(), PATHS.SOUL_FILE_NAME) } @@ -26,4 +31,35 @@ export function getSkillsDir(): string { export async function ensureBrowserosDir(): Promise { await mkdir(getMemoryDir(), { recursive: true }) await mkdir(getSkillsDir(), { recursive: true }) + await mkdir(getSessionsDir(), { recursive: true }) +} + +export async function cleanOldSessions(): Promise { + const sessionsDir = getSessionsDir() + let entries: string[] + try { + entries = await readdir(sessionsDir) + } catch { + return + } + + const cutoff = Date.now() - PATHS.SESSION_RETENTION_DAYS * 24 * 60 * 60 * 1000 + let removed = 0 + + for (const entry of entries) { + const entryPath = join(sessionsDir, entry) + try { + const info = await stat(entryPath) + if (info.isDirectory() && info.mtimeMs < cutoff) { + await rm(entryPath, { recursive: true }) + removed++ + } + } catch { + // skip entries that were already removed or inaccessible + } + } + + if (removed > 0) { + logger.info(`Cleaned ${removed} stale session directories`) + } } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 0177f04b6..41a42b788 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -18,7 +18,7 @@ import { ControllerBackend } from './browser/backends/controller' import { Browser } from './browser/browser' import type { ServerConfig } from './config' import { INLINED_ENV } from './env' -import { ensureBrowserosDir } from './lib/browseros-dir' +import { cleanOldSessions, ensureBrowserosDir } from './lib/browseros-dir' import { initializeDb } from './lib/db' import { identity } from './lib/identity' import { logger } from './lib/logger' @@ -132,6 +132,7 @@ export class Application { private async initCoreServices(): Promise { this.configureLogDirectory() await ensureBrowserosDir() + await cleanOldSessions() await seedSoulTemplate() await seedDefaultSkills() diff --git a/apps/server/src/skills/defaults/compare-prices/SKILL.md b/apps/server/src/skills/defaults/compare-prices/SKILL.md index 0b316a0b1..51bc76efa 100644 --- a/apps/server/src/skills/defaults/compare-prices/SKILL.md +++ b/apps/server/src/skills/defaults/compare-prices/SKILL.md @@ -29,7 +29,7 @@ Confirm with the user before searching: | Step | Tool | Detail | |------|------|--------| -| Create output directory | `evaluate_script` | Create `~/Downloads/compare-/` with a `raw/` subfolder | +| Create output directory | `evaluate_script` | Create `compare-/` in your working directory with a `raw/` subfolder | | Open hidden window | `create_hidden_window` | Dedicated workspace — keeps the user's browsing undisturbed | | Open parallel tabs | `new_hidden_page` | Open up to **10 tabs** concurrently, one per retailer/search | diff --git a/apps/server/src/skills/defaults/deep-research/SKILL.md b/apps/server/src/skills/defaults/deep-research/SKILL.md index 8761b2a2f..1f0031dcc 100644 --- a/apps/server/src/skills/defaults/deep-research/SKILL.md +++ b/apps/server/src/skills/defaults/deep-research/SKILL.md @@ -19,7 +19,7 @@ Activate when the user asks to research a topic, compare information across sour ### Phase 1 — Clarify & Plan -1. **Clarify the research question.** If the query is vague, ask the user for specifics: scope, depth, preferred sources, and where to save output (default: `~/Downloads/research-/`). +1. **Clarify the research question.** If the query is vague, ask the user for specifics: scope, depth, preferred sources, and where to save output (default: `research-/` in your working directory). 2. **Plan search queries.** Break the topic into 3–5 search angles. Example for "best standing desks": - `best standing desks 2025 reviews` - `standing desk comparison reddit` diff --git a/apps/server/src/skills/defaults/extract-data/SKILL.md b/apps/server/src/skills/defaults/extract-data/SKILL.md index 75e6878ae..5652314de 100644 --- a/apps/server/src/skills/defaults/extract-data/SKILL.md +++ b/apps/server/src/skills/defaults/extract-data/SKILL.md @@ -22,7 +22,7 @@ Activate when the user asks to extract, scrape, pull, or collect structured data 1. **Clarify the request.** Before extracting, confirm with the user: - **Source(s):** Single page, list of URLs, or search-then-extract? - **Output format:** CSV, JSON, or Markdown table? Default to CSV if not specified. - - **Output location:** Where to save files. Default: `~/Downloads/extract-/`. + - **Output location:** Where to save files. Default: `extract-/` in your working directory. - **What data to extract:** Column names, specific fields, or "everything in the table." 2. **Create the output directory.** Use `evaluate_script` to create the target folder: ``` diff --git a/apps/server/src/skills/defaults/find-alternatives/SKILL.md b/apps/server/src/skills/defaults/find-alternatives/SKILL.md index 35ca23f1e..5800b9b2a 100644 --- a/apps/server/src/skills/defaults/find-alternatives/SKILL.md +++ b/apps/server/src/skills/defaults/find-alternatives/SKILL.md @@ -29,9 +29,9 @@ Activate when the user: - **Price range** — same range, cheaper, or open budget? If unclear, default to ±30% of the reference product's price. - **Key criteria** — what matters most? (e.g., price, quality, brand, specific features) - **Any exclusions** — brands or stores to skip -3. **Create output directory.** Use `evaluate_script` to create: +3. **Create output directory.** Use `evaluate_script` to create in your working directory: ``` - ~/Downloads/alternatives-/ + alternatives-/ ├── raw/ ← per-source research data ├── findings.md ← running notes and rankings └── report.html ← final HTML report diff --git a/apps/server/src/skills/defaults/read-later/SKILL.md b/apps/server/src/skills/defaults/read-later/SKILL.md index 57997c58a..13a942f8d 100644 --- a/apps/server/src/skills/defaults/read-later/SKILL.md +++ b/apps/server/src/skills/defaults/read-later/SKILL.md @@ -1,6 +1,6 @@ --- name: read-later -description: Bookmark the current page to a "Read Later" folder and save a PDF copy for offline reading. Use when the user wants to save a page for later, bookmark it for reading, or keep an offline copy. +description: Bookmark the current page to a "📚 Read Later" folder and save a PDF copy for offline reading. Use when the user wants to save a page for later, bookmark it for reading, or keep an offline copy. metadata: display-name: Read Later enabled: "true" @@ -23,7 +23,7 @@ Activate when the user asks to save a page for later, read it later, bookmark so | Check for folder | `get_bookmarks` | Look for an existing folder named "📚 Read Later" in the bookmark bar | | Create folder (if needed) | `create_bookmark` | If the folder doesn't exist, create "📚 Read Later" in the bookmark bar | | Add bookmark | `create_bookmark` | Save the current page URL and title into the "📚 Read Later" folder | -| Save PDF | `save_pdf` | Download the full page as a PDF to the user's default downloads directory | +| Save PDF | `save_pdf` | Download the full page as a PDF to the working directory | | Notify user | — | Tell the user the page has been saved with the bookmark location and PDF file path | ## Notification Format diff --git a/apps/server/src/skills/defaults/save-page/SKILL.md b/apps/server/src/skills/defaults/save-page/SKILL.md index 501e8bef5..a45d60a3a 100644 --- a/apps/server/src/skills/defaults/save-page/SKILL.md +++ b/apps/server/src/skills/defaults/save-page/SKILL.md @@ -21,7 +21,7 @@ Activate when the user asks to save a page as PDF, download a page for offline r - Dismiss any popups or overlays that would appear in the PDF - Scroll to load any lazy-loaded content if the page uses infinite scroll -3. **Save as PDF** using `save_pdf` with a descriptive filename: +3. **Save as PDF** using `save_pdf` with a descriptive filename in the working directory: - Pattern: `{domain}-{title-slug}-{date}.pdf` - Example: `nytimes-climate-report-2025-03-11.pdf` - Let the user specify a custom path if they prefer diff --git a/apps/server/src/tools/framework.ts b/apps/server/src/tools/framework.ts index b2796d995..580f745a0 100644 --- a/apps/server/src/tools/framework.ts +++ b/apps/server/src/tools/framework.ts @@ -18,7 +18,7 @@ export type ToolHandler = ( ) => Promise export interface ToolDirectories { - executionDir: string + workingDir: string resourcesDir?: string } @@ -27,12 +27,12 @@ export type ToolContext = { directories: ToolDirectories } -export function resolveExecutionPath( +export function resolveWorkingPath( ctx: ToolContext, targetPath: string, cwd?: string, ): string { - return resolve(cwd ?? ctx.directories.executionDir, targetPath) + return resolve(cwd ?? ctx.directories.workingDir, targetPath) } export function defineTool< diff --git a/apps/server/src/tools/page-actions.ts b/apps/server/src/tools/page-actions.ts index 7746c2318..6db1fd6c6 100644 --- a/apps/server/src/tools/page-actions.ts +++ b/apps/server/src/tools/page-actions.ts @@ -1,7 +1,7 @@ import { mkdir, mkdtemp, rename, rm } from 'node:fs/promises' import { join } from 'node:path' import { z } from 'zod' -import { defineTool, resolveExecutionPath } from './framework' +import { defineTool, resolveWorkingPath } from './framework' const pageParam = z.number().describe('Page ID (from list_pages)') const elementParam = z @@ -27,7 +27,7 @@ export const save_pdf = defineTool({ path: z.string(), }), handler: async (args, ctx, response) => { - const resolvedPath = resolveExecutionPath(ctx, args.path, args.cwd) + const resolvedPath = resolveWorkingPath(ctx, args.path, args.cwd) const { data } = await ctx.browser.printToPDF(args.page) await Bun.write(resolvedPath, Buffer.from(data, 'base64')) response.text(`Saved PDF to ${resolvedPath}`) @@ -77,7 +77,7 @@ export const save_screenshot = defineTool({ fullPage: z.boolean(), }), handler: async (args, ctx, response) => { - const resolvedPath = resolveExecutionPath(ctx, args.path, args.cwd) + const resolvedPath = resolveWorkingPath(ctx, args.path, args.cwd) const { data } = await ctx.browser.screenshot(args.page, { format: args.format, quality: args.quality, @@ -120,10 +120,10 @@ export const download_file = defineTool({ destinationPath: z.string(), }), handler: async (args, ctx, response) => { - const resolvedDir = resolveExecutionPath(ctx, args.path, args.cwd) - await mkdir(ctx.directories.executionDir, { recursive: true }) + const resolvedDir = resolveWorkingPath(ctx, args.path, args.cwd) + await mkdir(ctx.directories.workingDir, { recursive: true }) const tempDir = await mkdtemp( - join(ctx.directories.executionDir, 'browseros-dl-'), + join(ctx.directories.workingDir, 'browseros-dl-'), ) try { diff --git a/apps/server/tests/__helpers__/with-browser.ts b/apps/server/tests/__helpers__/with-browser.ts index 665a5fae9..b8556cdd6 100644 --- a/apps/server/tests/__helpers__/with-browser.ts +++ b/apps/server/tests/__helpers__/with-browser.ts @@ -86,7 +86,7 @@ export async function withBrowser( args, { browser, - directories: { executionDir: process.cwd() }, + directories: { workingDir: process.cwd() }, }, signal, ) diff --git a/apps/server/tests/agent/compaction.test.ts b/apps/server/tests/agent/compaction.test.ts index 94ad7b511..cb1f247da 100644 --- a/apps/server/tests/agent/compaction.test.ts +++ b/apps/server/tests/agent/compaction.test.ts @@ -311,7 +311,7 @@ function agentConfig( conversationId: 'test-conversation', provider: LLM_PROVIDERS.OPENROUTER, model: 'moonshotai/kimi-k2.5', - sessionExecutionDir: '/tmp/browseros-tests', + workingDir: '/tmp/browseros-tests', ...overrides, } } diff --git a/bun.lock b/bun.lock index d077da822..bee3a1159 100644 --- a/bun.lock +++ b/bun.lock @@ -144,7 +144,7 @@ }, "apps/server": { "name": "@browseros/server", - "version": "0.0.73", + "version": "0.0.74", "bin": { "browseros-server": "./src/index.ts", }, diff --git a/packages/shared/src/constants/paths.ts b/packages/shared/src/constants/paths.ts index e169ac40f..ac0e428c6 100644 --- a/packages/shared/src/constants/paths.ts +++ b/packages/shared/src/constants/paths.ts @@ -10,10 +10,12 @@ export const PATHS = { DEFAULT_EXECUTION_DIR: process.cwd(), BROWSEROS_DIR_NAME: '.browseros', MEMORY_DIR_NAME: 'memory', + SESSIONS_DIR_NAME: 'sessions', TOOL_OUTPUT_DIR_NAME: 'tool-output', SOUL_FILE_NAME: 'SOUL.md', CORE_MEMORY_FILE_NAME: 'CORE.md', SKILLS_DIR_NAME: 'skills', SOUL_MAX_LINES: 150, MEMORY_RETENTION_DAYS: 30, + SESSION_RETENTION_DAYS: 30, } as const