mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
chore: Merge branch 'main'
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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</page_context>'
|
||||
@@ -481,7 +409,6 @@ const promptSections: Record<string, PromptSectionFn> = {
|
||||
'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<string, PromptSectionFn> = {
|
||||
'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 `<AGENT_PROMPT>\n${sections.join('\n\n')}\n</AGENT_PROMPT>`
|
||||
}
|
||||
|
||||
export function getSystemPrompt(): string {
|
||||
return buildSystemPrompt()
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -130,7 +130,6 @@ export async function createHttpServer(config: HttpServerConfig) {
|
||||
createChatRoutes({
|
||||
browser,
|
||||
registry,
|
||||
executionDir,
|
||||
browserosId,
|
||||
rateLimiter,
|
||||
}),
|
||||
|
||||
@@ -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<string> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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<void> {
|
||||
await mkdir(getMemoryDir(), { recursive: true })
|
||||
await mkdir(getSkillsDir(), { recursive: true })
|
||||
await mkdir(getSessionsDir(), { recursive: true })
|
||||
}
|
||||
|
||||
export async function cleanOldSessions(): Promise<void> {
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
this.configureLogDirectory()
|
||||
await ensureBrowserosDir()
|
||||
await cleanOldSessions()
|
||||
await seedSoulTemplate()
|
||||
await seedDefaultSkills()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Confirm with the user before searching:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Create output directory | `evaluate_script` | Create `~/Downloads/compare-<product-slug>/` with a `raw/` subfolder |
|
||||
| Create output directory | `evaluate_script` | Create `compare-<product-slug>/` 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 |
|
||||
|
||||
|
||||
@@ -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-<topic-slug>/`).
|
||||
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-<topic-slug>/` 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`
|
||||
|
||||
@@ -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-<topic-slug>/`.
|
||||
- **Output location:** Where to save files. Default: `extract-<topic-slug>/` 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:
|
||||
```
|
||||
|
||||
@@ -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-<product-slug>/
|
||||
alternatives-<product-slug>/
|
||||
├── raw/ ← per-source research data
|
||||
├── findings.md ← running notes and rankings
|
||||
└── report.html ← final HTML report
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ export type ToolHandler = (
|
||||
) => Promise<void>
|
||||
|
||||
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<
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function withBrowser(
|
||||
args,
|
||||
{
|
||||
browser,
|
||||
directories: { executionDir: process.cwd() },
|
||||
directories: { workingDir: process.cwd() },
|
||||
},
|
||||
signal,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user