diff --git a/apps/agent/components/sidebar/SettingsSidebar.tsx b/apps/agent/components/sidebar/SettingsSidebar.tsx index 07855dee1..1bad5d41a 100644 --- a/apps/agent/components/sidebar/SettingsSidebar.tsx +++ b/apps/agent/components/sidebar/SettingsSidebar.tsx @@ -62,7 +62,7 @@ const primarySettingsSections: NavSection[] = [ items: [ { name: 'BrowserOS AI', to: '/settings/ai', icon: Bot }, { - name: 'Chat & Hub Provider', + name: 'Chat & Council Provider', to: '/settings/chat', icon: MessageSquare, }, diff --git a/apps/agent/entrypoints/background/scheduledJobRuns.ts b/apps/agent/entrypoints/background/scheduledJobRuns.ts index 98102fc50..0257048bb 100644 --- a/apps/agent/entrypoints/background/scheduledJobRuns.ts +++ b/apps/agent/entrypoints/background/scheduledJobRuns.ts @@ -109,25 +109,6 @@ export const scheduledJobRuns = async () => { throw new Error(`Job not found: ${jobId}`) } - const backgroundWindow = await chrome.windows.create({ - url: 'chrome://newtab', - focused: false, - state: 'minimized', - type: 'normal', - }) - - // FIXME: Race condition - the controller-ext extension sends a window_created - // WebSocket message to register window ownership, but our HTTP request may arrive - // at the server before that registration completes. This delay is a temporary fix. - // Proper solution: ControllerBridge should wait/poll for window ownership registration. - await new Promise((resolve) => setTimeout(resolve, 1000)) - - const backgroundTab = backgroundWindow?.tabs?.[0] - - if (!backgroundWindow || !backgroundTab) { - throw new Error('Failed to create background window') - } - const jobRun = await createJobRun(jobId, 'running') const abortController = new AbortController() runAbortControllers.set(jobRun.id, abortController) @@ -135,8 +116,6 @@ export const scheduledJobRuns = async () => { try { const response = await getChatServerResponse({ message: job.query, - activeTab: backgroundTab, - windowId: backgroundWindow.id, signal: abortController.signal, }) @@ -163,13 +142,6 @@ export const scheduledJobRuns = async () => { }) } finally { runAbortControllers.delete(jobRun.id) - if (backgroundWindow.id) { - try { - await chrome.windows.remove(backgroundWindow.id) - } catch { - // Window may already be closed - } - } await updateJobLastRunAt(jobId) } } diff --git a/apps/server/src/agent/ai-sdk-agent.ts b/apps/server/src/agent/ai-sdk-agent.ts index dbbf312d1..1ea86cb0b 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() @@ -118,9 +118,7 @@ export class AiSdkAgent { } // Build system prompt with optional section exclusions - // Tool definitions are already injected by the AI SDK via tool schemas, - // so skip the redundant tool-reference section. - const excludeSections: string[] = ['tool-reference'] + const excludeSections: string[] = [] if (config.resolvedConfig.isScheduledTask) { excludeSections.push('tab-grouping') } @@ -143,7 +141,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/prompt.ts b/apps/server/src/agent/prompt.ts index 23893c1a0..08b7b6612 100644 --- a/apps/server/src/agent/prompt.ts +++ b/apps/server/src/agent/prompt.ts @@ -128,82 +128,6 @@ function getErrorRecovery(): string { ---` } -// ----------------------------------------------------------------------------- -// section: cdp-tool-reference -// Skipped by ToolLoopAgent — the AI SDK already injects tool schemas into the -// LLM call. Kept for MCP prompt serving where clients lack tool definitions. -// ----------------------------------------------------------------------------- - -function getCdpToolReference(): string { - return `# Tool Reference - -## Page Management -- \`get_active_page\` - Get the currently active (focused) page -- \`list_pages\` - Get all open pages with IDs, titles, tab IDs, and URLs -- \`new_page(url, hidden?, background?, windowId?)\` - Open a new page. Use hidden for background processing, background to avoid activating. -- \`close_page(page)\` - Close a page by its page ID -- \`navigate_page(page, action, url?)\` - Navigate: action is "url", "back", "forward", or "reload" -- \`wait_for(page, text?, selector?, timeout?)\` - Wait for text or CSS selector to appear - -## Content Capture -- \`take_snapshot(page)\` - Get interactive elements with IDs (e.g. [47]). **Always take before interacting.** -- \`take_enhanced_snapshot(page)\` - Detailed accessibility tree with structural context -- \`get_page_content(page, selector?, viewportOnly?, includeLinks?, includeImages?)\` - Extract page as clean markdown with headers, links, lists, tables. **Prefer for data extraction.** -- \`take_screenshot(page, format?, quality?, fullPage?)\` - Capture page image -- \`evaluate_script(page, expression)\` - Run JavaScript in page context - -## Input & Interaction -- \`click(page, element)\` - Click element by ID from snapshot -- \`click_at(page, x, y)\` - Click at specific coordinates -- \`hover(page, element)\` - Hover over element -- \`focus(page, element)\` - Focus an element (scrolls into view first) -- \`clear(page, element)\` - Clear text from input or textarea -- \`fill(page, element, text, clear?)\` - Type into input/textarea (clears first by default) -- \`check(page, element)\` - Check a checkbox or radio button (no-op if already checked) -- \`uncheck(page, element)\` - Uncheck a checkbox (no-op if already unchecked) -- \`upload_file(page, element, files)\` - Set file(s) on a file input (absolute paths) -- \`select_option(page, element, value)\` - Select dropdown option by value or text -- \`press_key(page, key)\` - Press key or combo (e.g., "Enter", "Control+A", "ArrowDown") -- \`drag(page, sourceElement, targetElement?, targetX?, targetY?)\` - Drag element to another element or coordinates -- \`scroll(page, direction?, amount?, element?)\` - Scroll page or element (up/down/left/right) -- \`handle_dialog(page, accept, promptText?)\` - Handle browser dialogs (alert, confirm, prompt) - -## Page Actions -- \`save_pdf(page, path, cwd?)\` - Save page as PDF to disk -- \`download_file(page, element, path, cwd?)\` - Click element to trigger download, save to directory - -## Window Management -- \`list_windows\` - Get all browser windows -- \`create_window(hidden?)\` - Create a new browser window -- \`close_window(windowId)\` - Close a browser window -- \`activate_window(windowId)\` - Activate (focus) a browser window - -## Tab Groups -- \`list_tab_groups\` - Get all tab groups with IDs, titles, colors, and page IDs -- \`group_tabs(pageIds, title?, groupId?)\` - Create group or add pages to existing group (groupId is a string) -- \`update_tab_group(groupId, title?, color?, collapsed?)\` - Update group properties -- \`ungroup_tabs(pageIds)\` - Remove pages from their groups -- \`close_tab_group(groupId)\` - Close a tab group and all its tabs - -**Colors**: grey, blue, red, yellow, green, pink, purple, cyan, orange - -## Bookmarks -- \`get_bookmarks\` - Get all bookmarks -- \`create_bookmark(title, url?, parentId?)\` - Create bookmark or folder (omit url for folder) -- \`update_bookmark(id, title?, url?)\` - Edit bookmark -- \`remove_bookmark(id)\` - Delete bookmark or folder (recursive) -- \`move_bookmark(id, parentId?, index?)\` - Move bookmark or folder -- \`search_bookmarks(query)\` - Search bookmarks by title or URL - -## History -- \`search_history(query, maxResults?)\` - Search browser history -- \`get_recent_history(maxResults?)\` - Get recent history items -- \`delete_history_url(url)\` - Delete a specific URL from history -- \`delete_history_range(startTime, endTime)\` - Delete history within a time range (epoch ms) - ----` -} - // ----------------------------------------------------------------------------- // section: external-integrations // ----------------------------------------------------------------------------- @@ -428,11 +352,15 @@ function getPageContext( '\n\n**CRITICAL RULES:**\n1. **Do NOT call `get_active_page` or `list_pages` to find your starting page.** Use the **page ID from the Browser Context** directly.' if (options?.isScheduledTask) { - const windowLine = options.scheduledTaskWindowId - ? `When creating new pages with \`new_page\`, always pass \`windowId: ${options.scheduledTaskWindowId}\`.` - : 'When creating new pages with `new_page`, pass the `windowId` from the Browser Context.' - prompt += `\n2. ${windowLine}` - prompt += '\n3. Complete the task end-to-end and report results.' + const windowRef = options.scheduledTaskWindowId + ? `\`windowId: ${options.scheduledTaskWindowId}\`` + : 'the `windowId` from the Browser Context' + prompt += `\n2. **Always pass ${windowRef}** when calling \`new_page\` or \`new_hidden_page\`. Never omit the \`windowId\` parameter.` + prompt += + '\n3. **Do NOT close your dedicated hidden window** (via `close_window`). It is managed by the system and will be cleaned up automatically.' + prompt += + '\n4. **Do NOT create new windows** (via `create_window` or `create_hidden_window`). Use your existing hidden window for all pages.' + prompt += '\n5. Complete the task end-to-end and report results.' } prompt += '\n' @@ -481,7 +409,6 @@ const promptSections: Record = { 'observe-act-verify': getObserveActVerify, 'handle-obstacles': getHandleObstacles, 'error-recovery': getErrorRecovery, - 'tool-reference': getCdpToolReference, 'external-integrations': getExternalIntegrations, style: getStyle, nudges: getNudges, @@ -495,8 +422,6 @@ const promptSections: Record = { 'security-reminder': getSecurityReminder, } -export const PROMPT_SECTION_KEYS = Object.keys(promptSections) - interface BuildSystemPromptOptions { userSystemPrompt?: string exclude?: string[] @@ -523,7 +448,3 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string { return `\n${sections.join('\n\n')}\n` } - -export function getSystemPrompt(): string { - return buildSystemPrompt() -} 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/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