mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
feat: refresh agent skills settings UI + seed skills (#478)
* feat: bootstrap 12 default agent skills for new users Seed common browser automation skills (summarize, research, extract data, fill forms, dismiss popups, screenshots, organize tabs, compare prices, save page, monitor changes, read later, manage bookmarks) into ~/.browseros/skills/ on first startup when no user skills exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: populate skill edit dialog with existing content The edit dialog form fields were empty because Radix Dialog's onOpenChange doesn't fire when the open prop changes programmatically. Replace the handleOpenChange wrapper with a useEffect that syncs form state whenever editingSkill changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct tool names in default skill instructions - memory_save → memory_write (actual tool name in memory toolset) - delete_bookmark → remove_bookmark (actual tool name in registry) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move skill content from TS template literals to separate SKILL.md files Replace the monolithic defaults.ts (738-line file with escaped template literals) with individual SKILL.md files per skill. Uses Bun's text import (`with { type: 'text' }`) to inline content at bundle time. Adds md.d.ts for TypeScript module resolution. Much easier to read and edit skill content as plain markdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add build:server:test and start:server:test scripts for local binary testing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: refresh agent skills settings UI * fix: address PR review comments for 0311-skills_ui_refresh * feat: enhance default skills with file persistence, HTML reports, and add find-alternatives Rewrite deep-research, extract-data, compare-prices, manage-bookmarks, and read-later skills to follow a structured phase-based workflow. Key changes: - All research skills now save data incrementally to disk instead of accumulating in memory - Add HTML report generation (light theme) with source links for deep-research, extract-data, and compare-prices - Use hidden windows and parallel tabs (max 10) for multi-source extraction - Simplify read-later to just bookmark + PDF save - Simplify manage-bookmarks to max 3-5 top-level folders with confirmation - Add new find-alternatives skill for product alternative research with 1-5 star ranking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: simplify skills page rendering * fix: clean-up skill * fix: address review feedback for PR #478 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Pencil, Plus, Trash2, Wand2 } from 'lucide-react'
|
||||
import { type FC, useState } from 'react'
|
||||
import { AlertCircle, Pencil, Plus, Trash2, Wand2 } from 'lucide-react'
|
||||
import { type FC, type KeyboardEvent, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
@@ -26,10 +27,21 @@ import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { type SkillDetail, type SkillMeta, useSkills } from './useSkills'
|
||||
|
||||
const loadingSkillCards = [
|
||||
'loading-a',
|
||||
'loading-b',
|
||||
'loading-c',
|
||||
'loading-d',
|
||||
'loading-e',
|
||||
'loading-f',
|
||||
]
|
||||
|
||||
export const SkillsPage: FC = () => {
|
||||
const {
|
||||
skills,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
createSkill,
|
||||
updateSkill,
|
||||
deleteSkill,
|
||||
@@ -40,6 +52,8 @@ export const SkillsPage: FC = () => {
|
||||
const [editingSkill, setEditingSkill] = useState<SkillDetail | null>(null)
|
||||
const [skillToDelete, setSkillToDelete] = useState<SkillMeta | null>(null)
|
||||
|
||||
const enabledCount = skills.filter((skill) => skill.enabled).length
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingSkill(null)
|
||||
setIsDialogOpen(true)
|
||||
@@ -74,23 +88,26 @@ export const SkillsPage: FC = () => {
|
||||
setSkillToDelete(null)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fade-in slide-in-from-bottom-5 animate-in space-y-6 duration-500">
|
||||
<SkillsHeader onCreateClick={handleCreate} />
|
||||
<div className="text-muted-foreground text-sm">Loading skills...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fade-in slide-in-from-bottom-5 animate-in space-y-6 duration-500">
|
||||
<SkillsHeader onCreateClick={handleCreate} />
|
||||
<SkillsHeader
|
||||
skillCount={skills.length}
|
||||
enabledCount={enabledCount}
|
||||
onCreateClick={handleCreate}
|
||||
/>
|
||||
|
||||
{skills.length === 0 ? (
|
||||
{isLoading ? <SkillsLoadingState /> : null}
|
||||
|
||||
{!isLoading && error ? (
|
||||
<SkillsErrorState onRetry={() => void refetch()} />
|
||||
) : null}
|
||||
|
||||
{!isLoading && !error && skills.length === 0 ? (
|
||||
<EmptyState onCreateClick={handleCreate} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
) : null}
|
||||
|
||||
{!isLoading && !error && skills.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{skills.map((skill) => (
|
||||
<SkillCard
|
||||
key={skill.id}
|
||||
@@ -101,7 +118,7 @@ export const SkillsPage: FC = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<SkillDialog
|
||||
open={isDialogOpen}
|
||||
@@ -145,29 +162,85 @@ export const SkillsPage: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const SkillsHeader: FC<{ onCreateClick: () => void }> = ({ onCreateClick }) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">Skills</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Define custom skills to extend your agent's capabilities.
|
||||
</p>
|
||||
const SkillsHeader: FC<{
|
||||
skillCount: number
|
||||
enabledCount: number
|
||||
onCreateClick: () => void
|
||||
}> = ({ skillCount, enabledCount, onCreateClick }) => {
|
||||
const skillLabel = `${skillCount} skill${skillCount === 1 ? '' : 's'}`
|
||||
const enabledLabel = `${enabledCount} enabled`
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">Skills</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Define reusable instructions that extend how your agent responds.
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
{skillLabel} • {enabledLabel}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={onCreateClick} size="sm" className="shrink-0">
|
||||
<Plus className="mr-1.5 size-4" />
|
||||
New Skill
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={onCreateClick} size="sm">
|
||||
<Plus className="mr-1.5 size-4" />
|
||||
New Skill
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const SkillsLoadingState: FC = () => (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{loadingSkillCards.map((cardKey) => (
|
||||
<Card key={cardKey} className="h-full py-0">
|
||||
<CardContent className="space-y-4 p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="size-10 animate-pulse rounded-xl bg-muted" />
|
||||
<div className="h-6 w-11 animate-pulse rounded-full bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-28 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-full animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-4/5 animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
<div className="h-8 w-16 animate-pulse rounded bg-muted" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const SkillsErrorState: FC<{ onRetry: () => void }> = ({ onRetry }) => (
|
||||
<Card className="border-destructive/20 bg-destructive/5 py-0">
|
||||
<CardContent className="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-destructive/10 text-destructive">
|
||||
<AlertCircle className="size-4" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="font-semibold">Couldn't load skills</h2>
|
||||
<p className="text-destructive/80 text-sm">
|
||||
Check that the local agent services are running, then retry.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
const EmptyState: FC<{ onCreateClick: () => void }> = ({ onCreateClick }) => (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Wand2 className="mb-4 size-10 text-muted-foreground" />
|
||||
<Card className="border-dashed py-0">
|
||||
<CardContent className="flex flex-col items-center justify-center py-14 text-center">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]">
|
||||
<Wand2 className="size-5" />
|
||||
</div>
|
||||
<h3 className="mb-1 font-medium text-lg">No skills yet</h3>
|
||||
<p className="mb-4 max-w-sm text-muted-foreground text-sm">
|
||||
Skills are instructions that teach your agent how to handle specific
|
||||
tasks like creating PDFs, processing data, or drafting emails.
|
||||
<p className="mb-5 max-w-sm text-muted-foreground text-sm leading-6">
|
||||
Skills teach your agent how to handle repeatable tasks like research,
|
||||
extraction, and structured workflows.
|
||||
</p>
|
||||
<Button onClick={onCreateClick} size="sm">
|
||||
<Plus className="mr-1.5 size-4" />
|
||||
@@ -183,27 +256,43 @@ const SkillCard: FC<{
|
||||
onDelete: () => void
|
||||
onToggle: (enabled: boolean) => void
|
||||
}> = ({ skill, onEdit, onDelete, onToggle }) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="font-medium text-base">{skill.name}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Card className="h-full py-0 shadow-sm">
|
||||
<CardContent className="flex h-full flex-col p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h2 className="font-semibold text-sm leading-5">{skill.name}</h2>
|
||||
<Switch
|
||||
checked={skill.enabled}
|
||||
onCheckedChange={onToggle}
|
||||
aria-label={`Toggle ${skill.name}`}
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={onEdit}>
|
||||
<Pencil className="size-4" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex-1">
|
||||
<p className="line-clamp-3 text-muted-foreground text-sm leading-5">
|
||||
{skill.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
className="-ml-2 h-7 px-2 text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={onDelete}>
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onDelete}
|
||||
className="size-7 text-muted-foreground hover:bg-transparent hover:text-destructive"
|
||||
aria-label={`Delete ${skill.name}`}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="line-clamp-2 text-muted-foreground text-sm">
|
||||
{skill.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -223,83 +312,142 @@ const SkillDialog: FC<{
|
||||
const [content, setContent] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Reset form when dialog opens
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
setName(editingSkill?.name ?? '')
|
||||
setDescription(editingSkill?.description ?? '')
|
||||
setContent(editingSkill?.content ?? '')
|
||||
}
|
||||
onOpenChange(isOpen)
|
||||
}
|
||||
useEffect(() => {
|
||||
setSaving(false)
|
||||
if (!open) return
|
||||
setName(editingSkill?.name ?? '')
|
||||
setDescription(editingSkill?.description ?? '')
|
||||
setContent(editingSkill?.content ?? '')
|
||||
}, [editingSkill, open])
|
||||
|
||||
const isValid =
|
||||
name.trim().length > 0 &&
|
||||
description.trim().length > 0 &&
|
||||
content.trim().length > 0
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isValid || saving) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await onSave({ name, description, content })
|
||||
await onSave({
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
content,
|
||||
})
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = name.trim() && description.trim() && content.trim()
|
||||
const handleContentKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
void handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-5xl">
|
||||
<DialogHeader className="border-b px-6 py-5">
|
||||
<DialogTitle>
|
||||
{editingSkill ? 'Edit Skill' : 'Create Skill'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingSkill
|
||||
? 'Refine when the agent should use this skill and how it should execute it.'
|
||||
: 'Define a reusable instruction set your agent can apply when a request matches.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-name">Name</Label>
|
||||
<Input
|
||||
id="skill-name"
|
||||
placeholder="e.g., PDF Processing"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
maxLength={100}
|
||||
/>
|
||||
<div className="grid min-h-0 flex-1 overflow-y-auto lg:grid-cols-[280px_minmax(0,1fr)] lg:overflow-hidden">
|
||||
<div className="space-y-5 border-b bg-muted/20 px-6 py-5 lg:border-r lg:border-b-0">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-name">Name</Label>
|
||||
<Input
|
||||
id="skill-name"
|
||||
placeholder="e.g., Read Later"
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
maxLength={100}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs leading-5">
|
||||
Keep it short and recognizable in the skills list.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-description">Description</Label>
|
||||
<Textarea
|
||||
id="skill-description"
|
||||
placeholder="Describe when the agent should use this skill."
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
maxLength={500}
|
||||
className="min-h-28 resize-none bg-background"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs leading-5">
|
||||
This is the trigger summary the agent uses to pick the skill.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-background p-4">
|
||||
<p className="font-medium text-sm">Useful structure</p>
|
||||
<div className="mt-2 space-y-2 text-muted-foreground text-xs leading-5">
|
||||
<p>List the ordered steps the agent should follow.</p>
|
||||
<p>Close with the output or formatting you expect back.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-description">Description</Label>
|
||||
<Input
|
||||
id="skill-description"
|
||||
placeholder="When should the agent use this skill?"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-col px-6 py-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<Label htmlFor="skill-content">Instructions (Markdown)</Label>
|
||||
<Badge variant="outline" className="border-border bg-background">
|
||||
{content.length} characters
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-content">Instructions (Markdown)</Label>
|
||||
<Textarea
|
||||
id="skill-content"
|
||||
placeholder="Write instructions for the agent. Use markdown for formatting."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="min-h-[200px] font-mono text-sm"
|
||||
/>
|
||||
<div className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||
<span className="font-medium text-sm">Markdown editor</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Cmd/Ctrl + Enter to save
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
id="skill-content"
|
||||
placeholder="Write instructions for the agent. Use markdown for structure."
|
||||
value={content}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
onKeyDown={handleContentKeyDown}
|
||||
className="min-h-[320px] flex-1 resize-none overflow-y-auto border-0 bg-transparent p-4 font-mono text-sm leading-6 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || saving}>
|
||||
{saving ? 'Saving...' : editingSkill ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<div className="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Saved locally and available to your agent immediately.
|
||||
</p>
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || saving}>
|
||||
{saving
|
||||
? 'Saving...'
|
||||
: editingSkill
|
||||
? 'Update Skill'
|
||||
: 'Create Skill'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import { fetchDailyRateLimit } from './lib/rate-limiter/fetch-config'
|
||||
import { RateLimiter } from './lib/rate-limiter/rate-limiter'
|
||||
import { Sentry } from './lib/sentry'
|
||||
import { seedSoulTemplate } from './lib/soul'
|
||||
import { seedDefaultSkills } from './skills/seed'
|
||||
import { registry } from './tools/registry'
|
||||
import { VERSION } from './version'
|
||||
|
||||
@@ -132,6 +133,7 @@ export class Application {
|
||||
this.configureLogDirectory()
|
||||
await ensureBrowserosDir()
|
||||
await seedSoulTemplate()
|
||||
await seedDefaultSkills()
|
||||
|
||||
const dbPath = path.join(
|
||||
this.config.executionDir || this.config.resourcesDir,
|
||||
|
||||
126
apps/server/src/skills/defaults/compare-prices/SKILL.md
Normal file
126
apps/server/src/skills/defaults/compare-prices/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: compare-prices
|
||||
description: Search for a product across multiple retailers in parallel, save pricing data to disk, and produce an HTML report with the best deals and direct product links. Use when the user asks to compare prices, find the best deal, or check prices across stores.
|
||||
metadata:
|
||||
display-name: Compare Prices
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Compare Prices
|
||||
|
||||
Search for a product across retailers in parallel using a hidden window, save pricing data incrementally to disk, and deliver a clean HTML comparison report with direct links to every product page.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user asks to compare prices for a product, find the cheapest option, check if a price is good, or shop across multiple stores.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1 — Clarify
|
||||
|
||||
Confirm with the user before searching:
|
||||
|
||||
- **Product name** — exact model, variant, size, or color if applicable
|
||||
- **Retailer preferences** — any stores to include or exclude
|
||||
- **Region / currency** — defaults to user's locale
|
||||
|
||||
### Phase 2 — Set Up & Search
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Create output directory | `evaluate_script` | Create `~/Downloads/compare-<product-slug>/` 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 |
|
||||
|
||||
**Default search targets** (adjust based on product type and user's region):
|
||||
|
||||
| Tab | Target |
|
||||
|-----|--------|
|
||||
| 1 | Google Shopping — `https://www.google.com/search?tbm=shop&q=<product>` |
|
||||
| 2 | Amazon — `https://www.amazon.com/s?k=<product>` |
|
||||
| 3 | Walmart — `https://www.walmart.com/search?q=<product>` |
|
||||
| 4 | Best Buy — `https://www.bestbuy.com/site/searchpage.jsp?st=<product>` |
|
||||
| 5 | Target — `https://www.target.com/s?searchTerm=<product>` |
|
||||
| 6 | eBay — `https://www.ebay.com/sch/i.html?_nkw=<product>` |
|
||||
| 7–10 | Additional retailers relevant to the product category (Newegg for tech, Home Depot for tools, etc.) |
|
||||
|
||||
### Phase 3 — Extract & Save
|
||||
|
||||
For **each tab**, extract pricing data and save immediately:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Navigate | `navigate_page` | Go to the search URL |
|
||||
| Read results | `get_page_content` | Extract the search results page as markdown |
|
||||
| Find best match | `navigate_page` | Click through to the most relevant product listing |
|
||||
| Extract pricing | `get_page_content` | Pull the product page content — price, availability, shipping, seller |
|
||||
| **Save raw data** | `evaluate_script` | Write to `raw/<retailer>.json` with all extracted fields (see format below) |
|
||||
| Close tab | `close_page` | Free the tab after saving |
|
||||
|
||||
**Never hold all retailer data in memory.** Save each retailer's data to its own file immediately after extraction.
|
||||
|
||||
#### Raw Data Format (`raw/<retailer>.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"retailer": "Amazon",
|
||||
"product_name": "Product Title as Listed",
|
||||
"product_url": "https://www.amazon.com/dp/...",
|
||||
"price": 299.99,
|
||||
"original_price": 349.99,
|
||||
"currency": "USD",
|
||||
"shipping": "Free",
|
||||
"availability": "In Stock",
|
||||
"seller": "Amazon.com",
|
||||
"condition": "New",
|
||||
"rating": "4.5/5",
|
||||
"notes": "Prime eligible",
|
||||
"extracted_at": "2025-03-11T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4 — HTML Report
|
||||
|
||||
After all retailers are processed, read the saved `raw/*.json` files and generate a self-contained `report.html`:
|
||||
|
||||
| Requirement | Detail |
|
||||
|-------------|--------|
|
||||
| **Theme** | Light background (`#ffffff`), clean sans-serif typography, generous whitespace |
|
||||
| **Header** | Product name, search date, number of retailers checked |
|
||||
| **Best Deal banner** | Highlighted card at the top showing the lowest total price with a direct link to the product page |
|
||||
| **Comparison table** | All retailers sorted by total price (lowest first) with columns: Retailer, Price, Shipping, Total, Stock, Seller, Rating, Link |
|
||||
| **Product links** | Every retailer name and a "View Deal" button must be a clickable `<a href>` linking to the actual product page URL |
|
||||
| **Price highlights** | Lowest price in green, highest in muted gray. Show discount percentage if original price differs. |
|
||||
| **Self-contained** | All styles in a `<style>` block — no external CSS or JS |
|
||||
| **Responsive** | Readable on desktop and mobile |
|
||||
| **Footer** | "Generated by BrowserOS Compare Prices" with date |
|
||||
|
||||
Use `evaluate_script` to write `report.html` to the output directory.
|
||||
|
||||
### Phase 5 — Open & Notify
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Close hidden window | `close_window` | Clean up the research workspace |
|
||||
| Open report | `new_page` | Open `file://<path>/report.html` in the user's active window |
|
||||
| Notify user | — | Tell the user the comparison is complete, highlight the best deal, and provide the report path |
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Window management | `create_hidden_window`, `close_window` |
|
||||
| Tab management | `new_hidden_page`, `close_page`, `new_page` |
|
||||
| Navigation | `navigate_page` |
|
||||
| Content extraction | `get_page_content` |
|
||||
| Data & file I/O | `evaluate_script` |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Always compare total price** (product + shipping), not just the listed price.
|
||||
- **Note the seller** — marketplace third-party sellers may have different return policies than the retailer itself.
|
||||
- Mention membership discounts (Prime, Walmart+) as a note, not as the default price.
|
||||
- If the product has variants (sizes, colors), ensure every retailer is quoting the same variant.
|
||||
- If a retailer blocks scraping or returns no results, skip it and note the gap in the report.
|
||||
- For used/refurbished listings, separate them from new-condition results.
|
||||
149
apps/server/src/skills/defaults/deep-research/SKILL.md
Normal file
149
apps/server/src/skills/defaults/deep-research/SKILL.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: deep-research
|
||||
description: Research a topic across multiple sources using parallel tabs, save raw content and findings to files, then produce an HTML report and PDF. Use when the user asks to research, investigate, or gather information on a topic.
|
||||
metadata:
|
||||
display-name: Deep Research
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Deep Research
|
||||
|
||||
End-to-end research workflow that searches the web in parallel tabs, persists raw content and notes to disk as it goes (instead of holding everything in memory), synthesizes findings, and delivers a polished HTML report plus PDF.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user asks to research a topic, compare information across sources, investigate something thoroughly, or compile findings from the web.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 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>/`).
|
||||
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`
|
||||
- `ergonomic standing desk features`
|
||||
- `standing desk health benefits studies`
|
||||
3. **Create the output directory.** Use `evaluate_script` to create the target folder structure:
|
||||
```
|
||||
research-<topic-slug>/
|
||||
├── sources/ ← raw page content per source
|
||||
├── findings.md ← running synthesis
|
||||
├── report.html ← final HTML report
|
||||
└── report.pdf ← final PDF report
|
||||
```
|
||||
|
||||
### Phase 2 — Parallel Research & Persistence
|
||||
|
||||
For **each** search query, open a parallel research tab and persist results to disk immediately:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Open tab | `new_hidden_page` | Opens a background tab so research doesn't disrupt the user |
|
||||
| Search | `navigate_page` | Navigate to `https://www.google.com/search?q=<encoded-query>` (or the user's preferred search engine) |
|
||||
| Pick results | `get_page_content` / `get_page_links` | Read the search results page; identify the 2–3 most relevant links |
|
||||
| Visit source | `navigate_page` | Navigate to each selected result |
|
||||
| Extract content | `get_page_content` | Pull the full page text |
|
||||
| **Save raw content** | `evaluate_script` | Write a markdown file to `sources/<n>-<slug>.md` containing the page title, source URL, extraction date, and full text. **Always include the source URL** so every fact is traceable. |
|
||||
| Close tab | `close_page` | Free resources after extraction |
|
||||
|
||||
Repeat across all search angles. Run multiple tabs concurrently where possible.
|
||||
|
||||
#### Source File Format (`sources/<n>-<slug>.md`)
|
||||
|
||||
```markdown
|
||||
# <Page Title>
|
||||
|
||||
- **URL:** <source-url>
|
||||
- **Retrieved:** <date-time>
|
||||
|
||||
---
|
||||
|
||||
<extracted page content>
|
||||
```
|
||||
|
||||
### Phase 3 — Synthesize Findings
|
||||
|
||||
After all sources are saved:
|
||||
|
||||
1. **Read each source file** and extract key facts, data points, expert opinions, and areas of agreement or disagreement.
|
||||
2. **Write `findings.md`** in the output directory using the format below. Every claim must reference the source file and URL it came from.
|
||||
3. Continuously append to `findings.md` as you process each source — do not hold all content in memory.
|
||||
|
||||
#### Findings File Format (`findings.md`)
|
||||
|
||||
```markdown
|
||||
# Research Findings: <Topic>
|
||||
|
||||
**Date:** <current date>
|
||||
**Sources consulted:** <count>
|
||||
**Output directory:** <path>
|
||||
|
||||
## Key Findings
|
||||
|
||||
1. **<Finding title>**
|
||||
<Detail with supporting evidence>
|
||||
_Source: [<source name>](<url>) — sources/<n>-<slug>.md_
|
||||
|
||||
2. **<Finding title>**
|
||||
...
|
||||
|
||||
## Source Summary
|
||||
|
||||
| # | Source | URL | Key Insight | Credibility |
|
||||
|---|--------|-----|-------------|-------------|
|
||||
| 1 | <name> | <url> | <insight> | high / med / low |
|
||||
|
||||
## Agreements & Disagreements
|
||||
|
||||
- **Consensus:** ...
|
||||
- **Conflicting views:** ...
|
||||
|
||||
## Conclusion
|
||||
|
||||
<Synthesis of findings with actionable recommendation>
|
||||
```
|
||||
|
||||
### Phase 4 — HTML Report
|
||||
|
||||
Generate a self-contained `report.html` in the output directory with the following requirements:
|
||||
|
||||
| Requirement | Detail |
|
||||
|-------------|--------|
|
||||
| **Theme** | Light background (`#ffffff`), clean sans-serif typography, generous whitespace |
|
||||
| **Sections** | Title banner, executive summary, key findings (numbered cards), source table, conclusion |
|
||||
| **Source links** | Every finding must hyperlink to its original source URL. The source table must include clickable links. |
|
||||
| **Self-contained** | All styles inline or in a `<style>` block — no external CSS or JS dependencies |
|
||||
| **Responsive** | Readable on both desktop and mobile viewports |
|
||||
| **Footer** | "Generated by BrowserOS Deep Research" with the current date |
|
||||
|
||||
Use `evaluate_script` to write the HTML string to `report.html` in the output directory.
|
||||
|
||||
### Phase 5 — Open, Export & Notify
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Open report | `new_page` | Open `file://<path>/report.html` so the user sees the finished report |
|
||||
| Export PDF | `save_pdf` | Save the currently open report page as `report.pdf` in the same output directory |
|
||||
| Notify user | — | Tell the user research is complete and provide paths to both `report.html` and `report.pdf` |
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Tab management | `new_hidden_page`, `new_page`, `close_page` |
|
||||
| Navigation | `navigate_page` |
|
||||
| Content extraction | `get_page_content`, `get_page_links` |
|
||||
| File I/O & scripting | `evaluate_script` |
|
||||
| Export | `save_pdf` |
|
||||
|
||||
## Tips
|
||||
|
||||
- **4–6 sources** is the sweet spot for balanced coverage. More isn't always better.
|
||||
- **Prioritize recent sources** — check publication dates and prefer current information.
|
||||
- **Note disagreements** between sources rather than hiding them; surface conflicting data.
|
||||
- **Always record the source URL** next to every fact so the report is fully traceable.
|
||||
- For product research, include pricing and availability.
|
||||
- For technical topics, prefer official documentation and peer-reviewed sources.
|
||||
- If a Google search returns unhelpful results, try alternative queries or go directly to known authoritative sites.
|
||||
136
apps/server/src/skills/defaults/extract-data/SKILL.md
Normal file
136
apps/server/src/skills/defaults/extract-data/SKILL.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
name: extract-data
|
||||
description: Extract structured data from web pages — tables, lists, product info, pricing — into clean CSV, JSON, or markdown tables. Parallelizes across hidden tabs for multi-source extraction and saves results to disk incrementally. Use when the user asks to scrape, extract, or pull data from a page.
|
||||
metadata:
|
||||
display-name: Extract Data
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Extract Data
|
||||
|
||||
End-to-end data extraction workflow that pulls structured content from one or many web pages, saves results to disk incrementally (never accumulating everything in memory), and delivers clean output in the user's preferred format.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user asks to extract, scrape, pull, or collect structured data from web pages — tables, product listings, pricing, contact info, search results, leaderboards, or any repeating data pattern.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1 — Clarify & Plan
|
||||
|
||||
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>/`.
|
||||
- **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:
|
||||
```
|
||||
extract-<topic-slug>/
|
||||
├── raw/ ← per-page extracted content
|
||||
├── merged.<format> ← final combined output (csv / json)
|
||||
└── extraction.log ← progress log with source URLs
|
||||
```
|
||||
|
||||
### Phase 2 — Single-Page Extraction
|
||||
|
||||
For a **single page** (or each individual page in a batch):
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Navigate | `navigate_page` | Go to the target URL (skip if already on the page) |
|
||||
| Read content | `get_page_content` | Extract the page as markdown — this captures tables, lists, and text in a structured format |
|
||||
| Identify structure | — | Determine the data pattern: HTML table, repeated cards, key-value pairs, etc. |
|
||||
| Extract data | `evaluate_script` | For complex structures (e.g., product grids, nested cards), run JavaScript to query elements and return a JSON array. For clean markdown tables from `get_page_content`, parse directly. |
|
||||
| **Save immediately** | `evaluate_script` | Write the extracted data to `raw/<n>-<slug>.<format>` with a header comment containing the source URL and timestamp |
|
||||
| Log progress | `evaluate_script` | Append the source URL, row count, and status to `extraction.log` |
|
||||
|
||||
#### Handling Pagination
|
||||
|
||||
If the page has pagination (next buttons, page numbers, infinite scroll):
|
||||
|
||||
1. Extract the current page's data and save to `raw/<n>-page-<p>.<format>`
|
||||
2. Use `click` or `navigate_page` to go to the next page
|
||||
3. Repeat until all pages are processed or a user-specified limit is reached
|
||||
4. Each page's data is saved to its own file immediately — never accumulate across pages in memory
|
||||
|
||||
### Phase 3 — Multi-Source Parallel Extraction
|
||||
|
||||
When extracting from **multiple URLs or sources**, parallelize using a hidden window:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Create workspace | `create_hidden_window` | Open a dedicated hidden window for extraction work — keeps the user's browsing undisturbed |
|
||||
| Open batch of tabs | `new_hidden_page` | Open up to **10 tabs concurrently** within the hidden window, one per source URL |
|
||||
| Extract per tab | `navigate_page` → `get_page_content` → `evaluate_script` | For each tab: navigate, extract content, parse structured data |
|
||||
| Save per tab | `evaluate_script` | Write each tab's results to `raw/<n>-<slug>.<format>` immediately after extraction |
|
||||
| Close tab | `close_page` | Free the tab after its data is saved |
|
||||
| Next batch | — | Once a batch of 10 completes, open the next batch. Continue until all sources are processed. |
|
||||
| Close workspace | `close_window` | Close the hidden window after all extraction is done |
|
||||
|
||||
**Concurrency rule:** Never exceed 10 open tabs at a time. Process in batches of 10, saving and closing before opening the next batch.
|
||||
|
||||
### Phase 4 — Merge & Format
|
||||
|
||||
After all raw files are saved:
|
||||
|
||||
1. **Read each raw file** from `raw/` using `evaluate_script`.
|
||||
2. **Merge into a single output file** (`merged.csv`, `merged.json`, or `merged.md`) with:
|
||||
- Consistent column headers / keys across all sources
|
||||
- A `source_url` column so every row is traceable to its origin
|
||||
- Deduplication if the same record appears in multiple sources
|
||||
3. **Write the merged file** to the output directory.
|
||||
4. For large datasets, provide a summary: total rows, sources processed, any errors.
|
||||
|
||||
#### Output Formats
|
||||
|
||||
| Format | File | Notes |
|
||||
|--------|------|-------|
|
||||
| **CSV** | `merged.csv` | Header row, comma-separated, properly escaped. Include `source_url` as the last column. |
|
||||
| **JSON** | `merged.json` | Array of objects with consistent keys. Each object includes a `source_url` field. |
|
||||
| **Markdown** | `merged.md` | Aligned table with headers. Source URL in the last column. |
|
||||
|
||||
### Phase 5 — HTML Report
|
||||
|
||||
Generate a self-contained `report.html` in the output directory that serves as an index for the entire extraction.
|
||||
|
||||
| Requirement | Detail |
|
||||
|-------------|--------|
|
||||
| **Theme** | Light background (`#ffffff`), clean sans-serif typography, generous whitespace |
|
||||
| **Header** | Title, date, total rows extracted, number of sources processed |
|
||||
| **What was done** | Brief description of the extraction: source URLs, data fields extracted, format used |
|
||||
| **File index** | Table listing every file in the output directory (`raw/*`, `merged.*`, `extraction.log`) with file paths as clickable `file://` links so the user can open them directly |
|
||||
| **Data preview** | First 20 rows of the merged dataset rendered as an HTML table |
|
||||
| **Source list** | All source URLs as clickable hyperlinks with the row count extracted from each |
|
||||
| **Self-contained** | All styles inline or in a `<style>` block — no external dependencies |
|
||||
| **Footer** | "Generated by BrowserOS Extract Data" with the current date |
|
||||
|
||||
Use `evaluate_script` to write the HTML file to the output directory.
|
||||
|
||||
### Phase 6 — Open & Notify
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Open report | `new_page` | Open `file://<path>/report.html` so the user sees the extraction summary |
|
||||
| Notify user | — | Tell the user: extraction is complete, total rows, source count, and paths to `report.html` and `merged.<format>` |
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Window management | `create_hidden_window`, `close_window` |
|
||||
| Tab management | `new_hidden_page`, `close_page`, `new_page` |
|
||||
| Navigation | `navigate_page` |
|
||||
| Content extraction | `get_page_content` |
|
||||
| Data parsing & file I/O | `evaluate_script` |
|
||||
| Interaction | `click` (for pagination) |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Always ask the format first.** CSV, JSON, and Markdown have different strengths — let the user decide.
|
||||
- **Save after every page.** Never hold more than one page's worth of data in memory at a time.
|
||||
- **10 tabs max.** More tabs degrades performance and risks timeouts. Batch in groups of 10.
|
||||
- **Record the source URL** on every row and in every raw file so data is fully traceable.
|
||||
- Clean up extracted data: trim whitespace, normalize currency symbols, remove hidden characters.
|
||||
- For paginated sites, check for a total count or "showing X of Y" to estimate progress.
|
||||
- If a page requires login or blocks extraction, report it to the user rather than retrying silently.
|
||||
49
apps/server/src/skills/defaults/fill-form/SKILL.md
Normal file
49
apps/server/src/skills/defaults/fill-form/SKILL.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: fill-form
|
||||
description: Intelligently fill web forms using provided data — handles text fields, dropdowns, checkboxes, radio buttons, and multi-step forms. Use when the user asks to fill out, complete, or submit a form.
|
||||
metadata:
|
||||
display-name: Fill Form
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Fill Form
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to fill out a form, complete an application, enter data into fields, or submit information on a web page.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Collect the data to fill.** Ask the user for the information if not already provided. Organize it as key-value pairs.
|
||||
|
||||
2. **Take a snapshot** using `take_snapshot` to see the form fields and understand the layout.
|
||||
|
||||
3. **Map data to fields.** Match the user's data keys to form field labels. Handle common variations:
|
||||
- "Name" may map to "Full Name", "Your Name", or separate "First Name" + "Last Name" fields
|
||||
- "Phone" may map to "Phone Number", "Mobile", "Tel"
|
||||
- "Address" may need to split into Street, City, State, Zip
|
||||
|
||||
4. **Fill fields in order.** For each field:
|
||||
- **Text inputs:** Use `fill` with the field selector and value
|
||||
- **Dropdowns/selects:** Use `select_option` with the appropriate value
|
||||
- **Checkboxes:** Use `check` to toggle on/off
|
||||
- **Radio buttons:** Use `click` on the correct option
|
||||
- **Date pickers:** Try `fill` first; if that fails, interact with the date picker UI using `click`
|
||||
- **File uploads:** Use `upload_file` for attachment fields
|
||||
|
||||
5. **Handle multi-step forms.** After filling visible fields:
|
||||
- Look for "Next", "Continue", or "Step 2" buttons
|
||||
- Use `click` to advance
|
||||
- Take a new snapshot to see the next step's fields
|
||||
- Repeat the fill process
|
||||
|
||||
6. **Review before submission.** Take a final `take_snapshot` and present the filled form to the user for confirmation before clicking Submit.
|
||||
|
||||
## Tips
|
||||
|
||||
- Fill fields top-to-bottom, left-to-right to match natural tab order.
|
||||
- For auto-complete fields (like address), type slowly and wait for suggestions to appear, then select.
|
||||
- If a field has validation errors after filling, read the error message and adjust the value.
|
||||
- Never submit payment forms without explicit user confirmation.
|
||||
- For CAPTCHA fields, inform the user they need to complete it manually.
|
||||
189
apps/server/src/skills/defaults/find-alternatives/SKILL.md
Normal file
189
apps/server/src/skills/defaults/find-alternatives/SKILL.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: find-alternatives
|
||||
description: Find alternative products to something the user is looking at or considering. Searches across retailers and review sites, compares options, and delivers a ranked HTML report with ratings, pricing, and direct links. Use when the user asks for alternatives, similar products, or "something like this but..."
|
||||
metadata:
|
||||
display-name: Find Alternatives
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Find Alternatives
|
||||
|
||||
Search for alternative products across retailers and review sites, save research data incrementally to disk, rank the top 5 alternatives on a 1–5 scale, and deliver a clean HTML comparison report with direct product links.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user:
|
||||
|
||||
- Asks for alternatives to a product they're viewing or considering
|
||||
- Says "something like this but cheaper / better / different"
|
||||
- Wants to explore options before buying
|
||||
- Asks "what else is out there" for a product category
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1 — Understand the Product
|
||||
|
||||
1. **Identify the reference product.** Use `get_active_page` and `get_page_content` to understand what the user is currently looking at — product name, brand, price, key features, category.
|
||||
2. **Confirm with 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:
|
||||
```
|
||||
~/Downloads/alternatives-<product-slug>/
|
||||
├── raw/ ← per-source research data
|
||||
├── findings.md ← running notes and rankings
|
||||
└── report.html ← final HTML report
|
||||
```
|
||||
|
||||
### Phase 2 — Research Alternatives
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Open hidden window | `create_hidden_window` | Dedicated research workspace |
|
||||
| Search in parallel | `new_hidden_page` | Open up to **10 tabs** concurrently across search targets |
|
||||
|
||||
**Search targets** (adapt to product category):
|
||||
|
||||
| Tab | Target | Query |
|
||||
|-----|--------|-------|
|
||||
| 1 | Google Shopping | `<product category> alternatives under $<budget>` |
|
||||
| 2 | Google Search | `best <product category> alternatives <year> reddit` |
|
||||
| 3 | Google Search | `<product category> vs comparison <year>` |
|
||||
| 4 | Amazon | `<product category>` filtered to price range |
|
||||
| 5 | Walmart | `<product category>` in price range |
|
||||
| 6 | Best Buy / category retailer | `<product category>` |
|
||||
| 7–10 | Review sites, Reddit threads, or niche retailers relevant to the category |
|
||||
|
||||
For **each tab**:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Navigate | `navigate_page` | Go to the search URL |
|
||||
| Read results | `get_page_content` | Extract search results as markdown |
|
||||
| Visit promising results | `navigate_page` | Click through to individual product pages and review articles |
|
||||
| Extract data | `get_page_content` | Pull product details — name, price, features, ratings, reviews |
|
||||
| **Save immediately** | `evaluate_script` | Write to `raw/<n>-<source-slug>.json` (see format below) |
|
||||
| Close tab | `close_page` | Free the tab after saving |
|
||||
|
||||
#### Raw Data Format (`raw/<n>-<source-slug>.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "Amazon",
|
||||
"source_url": "https://www.amazon.com/...",
|
||||
"products": [
|
||||
{
|
||||
"name": "Product Name",
|
||||
"brand": "Brand",
|
||||
"product_url": "https://...",
|
||||
"price": 149.99,
|
||||
"currency": "USD",
|
||||
"rating": "4.3/5",
|
||||
"review_count": 1250,
|
||||
"key_features": ["feature 1", "feature 2"],
|
||||
"availability": "In Stock",
|
||||
"image_url": "https://..."
|
||||
}
|
||||
],
|
||||
"extracted_at": "2025-03-11T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3 — Rank & Synthesize
|
||||
|
||||
After all sources are saved:
|
||||
|
||||
1. **Read each raw file** from `raw/` using `evaluate_script`.
|
||||
2. **Deduplicate** — the same product may appear across multiple retailers. Group by product, keep the best price.
|
||||
3. **Select the top 5 alternatives** based on:
|
||||
- Price relative to budget
|
||||
- User ratings and review volume
|
||||
- Feature match to the user's criteria
|
||||
- Availability
|
||||
4. **Rate each alternative 1–5** on a composite scale:
|
||||
|
||||
| Rating | Meaning |
|
||||
|--------|---------|
|
||||
| ⭐⭐⭐⭐⭐ 5 | Excellent match — great price, high ratings, strong features |
|
||||
| ⭐⭐⭐⭐ 4 | Very good — minor trade-offs |
|
||||
| ⭐⭐⭐ 3 | Decent — good in some areas, weaker in others |
|
||||
| ⭐⭐ 2 | Fair — notable compromises |
|
||||
| ⭐ 1 | Marginal — only worth considering for a specific reason |
|
||||
|
||||
5. **Write `findings.md`** with the full ranking, reasoning, and source references:
|
||||
|
||||
```markdown
|
||||
# Alternatives for: <Reference Product>
|
||||
|
||||
**Reference price:** $X
|
||||
**Budget range:** $X – $Y
|
||||
**Date:** <current date>
|
||||
|
||||
## Top 5 Alternatives
|
||||
|
||||
### 1. <Product Name> — ⭐⭐⭐⭐⭐ (5/5)
|
||||
- **Price:** $X at <Retailer>
|
||||
- **Why:** <1–2 sentence justification>
|
||||
- **Link:** <product URL>
|
||||
- _Source: raw/<n>-<slug>.json_
|
||||
|
||||
### 2. <Product Name> — ⭐⭐⭐⭐ (4/5)
|
||||
...
|
||||
|
||||
## Comparison vs Reference
|
||||
|
||||
| Feature | Reference | Alt 1 | Alt 2 | Alt 3 | Alt 4 | Alt 5 |
|
||||
|---------|-----------|-------|-------|-------|-------|-------|
|
||||
| Price | $X | $X | $X | $X | $X | $X |
|
||||
| Rating | 4.2/5 | 4.5/5 | 4.3/5 | 4.1/5 | 3.9/5 | 4.0/5 |
|
||||
```
|
||||
|
||||
### Phase 4 — HTML Report
|
||||
|
||||
Generate a self-contained `report.html` in the output directory:
|
||||
|
||||
| Requirement | Detail |
|
||||
|-------------|--------|
|
||||
| **Theme** | Light background (`#ffffff`), clean sans-serif typography, generous whitespace |
|
||||
| **Header** | "Alternatives for: <Product Name>", date, budget range |
|
||||
| **Reference product card** | Show the original product with its price, rating, and link as the baseline |
|
||||
| **Top 5 cards** | Each alternative as a card showing: rank, name, rating (star visualization), price, key features, and a clickable "View Product" link to the actual product page |
|
||||
| **Comparison table** | Side-by-side table with the reference product and all 5 alternatives — price, rating, key features, pros/cons |
|
||||
| **Rating explanation** | Brief note on how the 1–5 rating was determined |
|
||||
| **Product links** | Every product name and "View Product" button must be a clickable `<a href>` to the actual product URL |
|
||||
| **Source references** | Footer section listing all sources consulted with links |
|
||||
| **Self-contained** | All styles in a `<style>` block — no external CSS or JS |
|
||||
| **Responsive** | Readable on desktop and mobile |
|
||||
| **Footer** | "Generated by BrowserOS Find Alternatives" with date |
|
||||
|
||||
Use `evaluate_script` to write the HTML file.
|
||||
|
||||
### Phase 5 — Open & Notify
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Close hidden window | `close_window` | Clean up the research workspace |
|
||||
| Open report | `new_page` | Open `file://<path>/report.html` in the user's active window |
|
||||
| Notify user | — | Summarize the top pick, mention the report path, and highlight any standout findings |
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Page info | `get_active_page` |
|
||||
| Window management | `create_hidden_window`, `close_window` |
|
||||
| Tab management | `new_hidden_page`, `close_page`, `new_page` |
|
||||
| Navigation | `navigate_page` |
|
||||
| Content extraction | `get_page_content` |
|
||||
| Data & file I/O | `evaluate_script` |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Save after every source.** Never accumulate all research data in memory.
|
||||
- **10 tabs max** at a time. Batch if there are more sources.
|
||||
- **Deduplicate across retailers** — the same product on Amazon and Walmart should appear once with the best price noted.
|
||||
- If the reference product is niche, broaden the search to the general category rather than exact alternatives.
|
||||
- Include at least one budget option and one premium option to give the user a range.
|
||||
- If a product has very few reviews (<50), note the low confidence in the rating.
|
||||
35
apps/server/src/skills/defaults/index.ts
Normal file
35
apps/server/src/skills/defaults/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import comparePrices from './compare-prices/SKILL.md' with { type: 'text' }
|
||||
import deepResearch from './deep-research/SKILL.md' with { type: 'text' }
|
||||
import dismissPopups from './dismiss-popups/SKILL.md' with { type: 'text' }
|
||||
import extractData from './extract-data/SKILL.md' with { type: 'text' }
|
||||
import fillForm from './fill-form/SKILL.md' with { type: 'text' }
|
||||
import findAlternatives from './find-alternatives/SKILL.md' with {
|
||||
type: 'text',
|
||||
}
|
||||
import manageBookmarks from './manage-bookmarks/SKILL.md' with { type: 'text' }
|
||||
import monitorPage from './monitor-page/SKILL.md' with { type: 'text' }
|
||||
import organizeTabs from './organize-tabs/SKILL.md' with { type: 'text' }
|
||||
import readLater from './read-later/SKILL.md' with { type: 'text' }
|
||||
import savePage from './save-page/SKILL.md' with { type: 'text' }
|
||||
import screenshotWalkthrough from './screenshot-walkthrough/SKILL.md' with {
|
||||
type: 'text',
|
||||
}
|
||||
import summarizePage from './summarize-page/SKILL.md' with { type: 'text' }
|
||||
|
||||
type DefaultSkill = { id: string; content: string }
|
||||
|
||||
export const DEFAULT_SKILLS: DefaultSkill[] = [
|
||||
{ id: 'summarize-page', content: summarizePage },
|
||||
{ id: 'deep-research', content: deepResearch },
|
||||
{ id: 'extract-data', content: extractData },
|
||||
{ id: 'dismiss-popups', content: dismissPopups },
|
||||
{ id: 'fill-form', content: fillForm },
|
||||
{ id: 'screenshot-walkthrough', content: screenshotWalkthrough },
|
||||
{ id: 'organize-tabs', content: organizeTabs },
|
||||
{ id: 'compare-prices', content: comparePrices },
|
||||
{ id: 'find-alternatives', content: findAlternatives },
|
||||
{ id: 'save-page', content: savePage },
|
||||
{ id: 'monitor-page', content: monitorPage },
|
||||
{ id: 'read-later', content: readLater },
|
||||
{ id: 'manage-bookmarks', content: manageBookmarks },
|
||||
]
|
||||
109
apps/server/src/skills/defaults/manage-bookmarks/SKILL.md
Normal file
109
apps/server/src/skills/defaults/manage-bookmarks/SKILL.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: manage-bookmarks
|
||||
description: Organize bookmarks — find duplicates, categorize by topic, create a clean folder structure, and clean up unused bookmarks. Use when the user asks to organize, clean up, sort, or manage their bookmarks.
|
||||
metadata:
|
||||
display-name: Manage Bookmarks
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Manage Bookmarks
|
||||
|
||||
Analyze the user's bookmark collection, propose a clean top-level folder structure (max 5 folders), execute with confirmation, and deliver a markdown summary of everything that changed.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user asks to organize bookmarks, find duplicates, create bookmark folders, clean up old bookmarks, or restructure their bookmark library.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1 — Analyze
|
||||
|
||||
1. **Retrieve bookmarks** using `get_bookmarks` to get the full bookmark tree.
|
||||
2. **Analyze the collection thoroughly:**
|
||||
- Total bookmarks and existing folders
|
||||
- Duplicates (same URL, possibly different titles)
|
||||
- Group every bookmark by domain and inferred topic
|
||||
- Identify dead or broken patterns (e.g., `localhost`, empty titles)
|
||||
|
||||
3. **Present the analysis to the user.** Use short one-word slug categories:
|
||||
|
||||
```
|
||||
## Bookmark Analysis
|
||||
|
||||
**Total:** 342 bookmarks, 12 folders
|
||||
**Duplicates:** 8
|
||||
|
||||
### Proposed Folders (top-level)
|
||||
- dev — 94 bookmarks (GitHub, Stack Overflow, docs)
|
||||
- work — 67 bookmarks (Notion, Slack, Jira, company domains)
|
||||
- news — 45 bookmarks (HN, Reddit, RSS feeds)
|
||||
- shop — 28 bookmarks (Amazon, product pages)
|
||||
- misc — 108 bookmarks (everything else)
|
||||
|
||||
### Duplicates to Remove
|
||||
- github.com/user/repo × 3 (keep: "User/Repo - GitHub")
|
||||
- notion.so/page × 2 (keep: "Project Notes")
|
||||
```
|
||||
|
||||
**Folder naming rules:**
|
||||
- One-word lowercase slugs: `dev`, `work`, `news`, `shop`, `ref`, `social`, `misc`
|
||||
- **Maximum 3–5 top-level folders.** Fewer is better. Do not over-categorize.
|
||||
- Only suggest subfolders if the user explicitly asks for deeper organization
|
||||
|
||||
4. **Wait for confirmation.** Do not proceed until the user says to go ahead. If they want changes to the plan (rename folders, merge categories, split a group), adjust and re-present.
|
||||
|
||||
### Phase 2 — Organize
|
||||
|
||||
Once the user confirms:
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Create folders | `create_bookmark` | Create each top-level folder from the approved plan |
|
||||
| Move bookmarks | `move_bookmark` | Move each bookmark into its assigned folder |
|
||||
| Remove duplicates | `remove_bookmark` | Remove confirmed duplicates, keeping the one with the better title |
|
||||
|
||||
**Order matters:** Create all folders first, then move bookmarks, then remove duplicates.
|
||||
|
||||
### Phase 3 — Summary
|
||||
|
||||
After all operations complete, present a clean markdown summary:
|
||||
|
||||
```markdown
|
||||
## Bookmark Cleanup Complete
|
||||
|
||||
**Before:** 342 bookmarks, 12 folders
|
||||
**After:** 334 bookmarks, 5 folders
|
||||
|
||||
### Created Folders
|
||||
- dev (94 bookmarks)
|
||||
- work (67 bookmarks)
|
||||
- news (45 bookmarks)
|
||||
- shop (28 bookmarks)
|
||||
- misc (108 bookmarks)
|
||||
|
||||
### Duplicates Removed (8)
|
||||
- github.com/user/repo — removed 2 copies
|
||||
- notion.so/page — removed 1 copy
|
||||
|
||||
### Moved
|
||||
- 287 bookmarks reorganized into new folders
|
||||
- 47 bookmarks already in correct location
|
||||
```
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Read | `get_bookmarks` |
|
||||
| Create | `create_bookmark` |
|
||||
| Move | `move_bookmark` |
|
||||
| Delete | `remove_bookmark` |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Never delete without confirmation.** Always present the plan and wait for the user to say proceed.
|
||||
- **Keep it flat.** 3–5 top-level folders covers most collections. Resist the urge to create deep hierarchies.
|
||||
- When removing duplicates, keep the bookmark with the more descriptive title.
|
||||
- For very large collections (500+), process in batches by category to avoid timeouts.
|
||||
- Some users prefer a flat bookmark bar — ask about their preferred structure before reorganizing.
|
||||
4
apps/server/src/skills/defaults/md.d.ts
vendored
Normal file
4
apps/server/src/skills/defaults/md.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.md' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
62
apps/server/src/skills/defaults/monitor-page/SKILL.md
Normal file
62
apps/server/src/skills/defaults/monitor-page/SKILL.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: monitor-page
|
||||
description: Track changes on a web page by comparing content snapshots over time. Use when the user wants to watch for updates, price drops, stock availability, or content changes.
|
||||
metadata:
|
||||
display-name: Monitor Page
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Monitor Page
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to monitor a page for changes, watch for price drops, track stock availability, detect new content, or be alerted when something changes on a website.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Clarify what to monitor.** Ask the user:
|
||||
- What URL to watch
|
||||
- What specific content to track (price, stock status, text, any change)
|
||||
- How to identify the target content (a specific section, element, or keyword)
|
||||
|
||||
2. **Capture the baseline.** Navigate to the page and extract the current state:
|
||||
- Use `navigate_page` to load the target URL
|
||||
- Use `get_page_content` or `evaluate_script` to extract the specific content to track
|
||||
- Save the baseline to memory using `memory_write` with a descriptive key like `monitor:{url-slug}:baseline`
|
||||
|
||||
3. **Check for changes.** On subsequent checks:
|
||||
- Navigate to the same URL
|
||||
- Extract the same content using the same method
|
||||
- Compare against the saved baseline
|
||||
- Report differences
|
||||
|
||||
4. **Report findings:**
|
||||
|
||||
### If changes detected:
|
||||
```
|
||||
## Page Change Detected
|
||||
|
||||
**URL:** [url]
|
||||
**Checked:** [current date/time]
|
||||
|
||||
### Changes
|
||||
- **Before:** [previous value]
|
||||
- **After:** [current value]
|
||||
```
|
||||
|
||||
### If no changes:
|
||||
```
|
||||
No changes detected on [URL].
|
||||
Last checked: [current date/time]
|
||||
Monitoring: [what you're tracking]
|
||||
```
|
||||
|
||||
5. **Update the baseline** after reporting changes, using `memory_write` to store the new state.
|
||||
|
||||
## Tips
|
||||
|
||||
- For price monitoring, extract just the price element rather than the full page to avoid false positives from ad changes.
|
||||
- Use `evaluate_script` with specific CSS selectors for precise element tracking.
|
||||
- Suggest the user set a reminder to ask you to check again — BrowserOS doesn't yet have scheduled tasks.
|
||||
- For stock availability, look for phrases like "In Stock", "Out of Stock", or "Add to Cart" button presence.
|
||||
65
apps/server/src/skills/defaults/organize-tabs/SKILL.md
Normal file
65
apps/server/src/skills/defaults/organize-tabs/SKILL.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: organize-tabs
|
||||
description: Analyze open tabs, group related ones by topic, close duplicates, and clean up tab clutter. Use when the user asks to organize, clean up, sort, or manage their tabs.
|
||||
metadata:
|
||||
display-name: Organize Tabs
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Organize Tabs
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to organize tabs, clean up tab clutter, group related tabs, close duplicates, or manage their open browser tabs.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **List all open tabs** using `list_pages` to get the full inventory of open pages with their titles and URLs.
|
||||
|
||||
2. **Analyze and categorize.** Group tabs by:
|
||||
- **Domain** — Same website tabs together
|
||||
- **Topic** — Related content across domains (e.g., all "travel planning" tabs)
|
||||
- **Activity** — Shopping, research, social media, work, entertainment
|
||||
|
||||
3. **Identify issues:**
|
||||
- **Duplicates** — Same URL open in multiple tabs
|
||||
- **Dead tabs** — Error pages, "page not found", crashed tabs
|
||||
- **Stale tabs** — Tabs that are likely no longer needed
|
||||
|
||||
4. **Present a plan to the user:**
|
||||
|
||||
```
|
||||
## Tab Analysis
|
||||
|
||||
**Total tabs:** [N]
|
||||
|
||||
### Groups Found
|
||||
- Work: [list of tabs]
|
||||
- Research: [list of tabs]
|
||||
- Shopping: [list of tabs]
|
||||
- Uncategorized: [list of tabs]
|
||||
|
||||
### Issues
|
||||
- Duplicates: [N] tabs (will close extras)
|
||||
- Dead/Error pages: [N] tabs (will close)
|
||||
|
||||
### Proposed Actions
|
||||
1. Group [N] tabs into [M] tab groups
|
||||
2. Close [N] duplicate tabs
|
||||
3. Close [N] dead tabs
|
||||
```
|
||||
|
||||
5. **Execute with user confirmation:**
|
||||
- Use `group_tabs` to create named tab groups for each category
|
||||
- Use `close_page` to close duplicates (keep the first instance)
|
||||
- Use `close_page` to close dead/error tabs
|
||||
|
||||
6. **Offer to bookmark** stale tabs before closing using `create_bookmark`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Always ask before closing tabs — users may have unsaved work.
|
||||
- Keep at least one tab open at all times.
|
||||
- For duplicate detection, compare URLs after removing query parameters and fragments.
|
||||
- If the user has 50+ tabs, prioritize grouping over individual analysis.
|
||||
50
apps/server/src/skills/defaults/read-later/SKILL.md
Normal file
50
apps/server/src/skills/defaults/read-later/SKILL.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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.
|
||||
metadata:
|
||||
display-name: Read Later
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Read Later
|
||||
|
||||
Quick-save the current page: bookmark it into a dedicated "📚 Read Later" folder and download a PDF copy for offline reading.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate when the user asks to save a page for later, read it later, bookmark something to come back to, or keep an offline copy of an article.
|
||||
|
||||
## Workflow
|
||||
|
||||
| Step | Tool | Detail |
|
||||
|------|------|--------|
|
||||
| Get current page | `get_active_page` | Identify the page URL and title |
|
||||
| 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 |
|
||||
| Notify user | — | Tell the user the page has been saved with the bookmark location and PDF file path |
|
||||
|
||||
## Notification Format
|
||||
|
||||
```
|
||||
Saved to 📚 Read Later
|
||||
Title: <page title>
|
||||
URL: <page url>
|
||||
PDF: <download path>
|
||||
```
|
||||
|
||||
## Tool Reference
|
||||
|
||||
| Category | Tools Used |
|
||||
|----------|-----------|
|
||||
| Page info | `get_active_page` |
|
||||
| Bookmarks | `get_bookmarks`, `create_bookmark` |
|
||||
| Export | `save_pdf` |
|
||||
|
||||
## Tips
|
||||
|
||||
- Always check if "📚 Read Later" already exists before creating it — avoid duplicate folders.
|
||||
- If the page title is empty or generic, use the domain + path as the bookmark title.
|
||||
- The PDF captures the page as-is, including the current scroll position and expanded sections.
|
||||
46
apps/server/src/skills/defaults/save-page/SKILL.md
Normal file
46
apps/server/src/skills/defaults/save-page/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: save-page
|
||||
description: Save web pages as PDF files for offline reading, archiving, or sharing. Use when the user asks to save, download, export, or archive a page as PDF.
|
||||
metadata:
|
||||
display-name: Save Page
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Save Page
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to save a page as PDF, download a page for offline reading, archive a webpage, or export page content to a file.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Navigate to the target page** using `navigate_page` if not already there. If the user provides multiple URLs, process them one by one.
|
||||
|
||||
2. **Prepare the page for saving:**
|
||||
- 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:
|
||||
- Pattern: `{domain}-{title-slug}-{date}.pdf`
|
||||
- Example: `nytimes-climate-report-2025-03-11.pdf`
|
||||
- Let the user specify a custom path if they prefer
|
||||
|
||||
4. **For multiple pages**, process each URL sequentially:
|
||||
- Navigate to the page
|
||||
- Save as PDF
|
||||
- Report progress to the user
|
||||
|
||||
5. **Confirm the save:**
|
||||
```
|
||||
Saved: [filename].pdf
|
||||
Source: [URL]
|
||||
Location: [file path]
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- For articles, the PDF will capture the current page state — make sure content is fully loaded.
|
||||
- Some pages have print stylesheets that produce better PDFs — `save_pdf` uses these automatically.
|
||||
- For documentation sites with multiple pages, offer to save each section as a separate PDF.
|
||||
- If saving fails, offer the alternative of using `get_page_content` to save as markdown.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: screenshot-walkthrough
|
||||
description: Capture step-by-step screenshots of a workflow or process for documentation, bug reports, or tutorials. Use when the user asks to document steps, create a walkthrough, or capture a process.
|
||||
metadata:
|
||||
display-name: Screenshot Walkthrough
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Screenshot Walkthrough
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to document a workflow, create a step-by-step guide, capture a process for a bug report, or build visual documentation of a web-based procedure.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Clarify the workflow.** Confirm with the user:
|
||||
- What process to document
|
||||
- Starting URL or page
|
||||
- Where to save the screenshots
|
||||
|
||||
2. **Navigate to the starting point** using `navigate_page`.
|
||||
|
||||
3. **For each step in the workflow:**
|
||||
a. Take a screenshot using `save_screenshot` with a descriptive filename:
|
||||
- Pattern: `step-{number}-{description}.png`
|
||||
- Example: `step-01-login-page.png`, `step-02-enter-credentials.png`
|
||||
b. Note what action to take next
|
||||
c. Perform the action (click, fill, navigate, etc.)
|
||||
d. Wait for the page to settle (new content to load)
|
||||
e. Repeat
|
||||
|
||||
4. **Compile the walkthrough** as a markdown document:
|
||||
|
||||
### Output Format
|
||||
|
||||
```markdown
|
||||
# Walkthrough: [Process Name]
|
||||
|
||||
**Date:** [current date]
|
||||
**URL:** [starting URL]
|
||||
|
||||
## Step 1: [Action Description]
|
||||

|
||||
Navigate to [URL]. You will see [what's on screen].
|
||||
|
||||
## Step 2: [Action Description]
|
||||

|
||||
Click on [element]. [What happens next].
|
||||
```
|
||||
|
||||
5. **Save the walkthrough** using `filesystem_write` alongside the screenshots.
|
||||
|
||||
## Tips
|
||||
|
||||
- Number steps with zero-padded digits (01, 02, ...) for correct file sorting.
|
||||
- Include the browser URL bar in screenshots when the URL is relevant to the step.
|
||||
- For error documentation, capture the error state and any console errors.
|
||||
- If the process involves sensitive data, warn the user before capturing screenshots.
|
||||
46
apps/server/src/skills/defaults/summarize-page/SKILL.md
Normal file
46
apps/server/src/skills/defaults/summarize-page/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: summarize-page
|
||||
description: Extract and summarize the main content of the current web page into structured markdown. Use when the user asks to summarize, digest, or get the gist of a page.
|
||||
metadata:
|
||||
display-name: Summarize Page
|
||||
enabled: "true"
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Summarize Page
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user asks to summarize, digest, condense, or get the key points from the current page or a specific URL.
|
||||
|
||||
## Steps
|
||||
|
||||
1. If the user provided a URL, use `navigate_page` to go there first.
|
||||
2. Use `get_page_content` to extract the full text content of the page.
|
||||
3. Identify the page type (article, documentation, product page, forum thread, etc.) and adapt the summary format accordingly.
|
||||
4. Produce a structured markdown summary:
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
## Summary: [Page Title]
|
||||
|
||||
**Source:** [URL]
|
||||
**Type:** [article/docs/product/forum/etc.]
|
||||
|
||||
### Key Points
|
||||
- [3-5 bullet points capturing the main ideas]
|
||||
|
||||
### Details
|
||||
[2-3 paragraphs expanding on the most important content]
|
||||
|
||||
### Takeaways
|
||||
- [Actionable items or conclusions, if applicable]
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- For long pages, focus on headings, first paragraphs of sections, and any emphasized text.
|
||||
- For product pages, emphasize specs, pricing, and reviews.
|
||||
- For news articles, lead with the who/what/when/where/why.
|
||||
- If the page content is behind a paywall or login, inform the user rather than summarizing partial content.
|
||||
38
apps/server/src/skills/seed.ts
Normal file
38
apps/server/src/skills/seed.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mkdir, readdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { getSkillsDir } from '../lib/browseros-dir'
|
||||
import { logger } from '../lib/logger'
|
||||
import { DEFAULT_SKILLS } from './defaults'
|
||||
|
||||
async function hasExistingSkills(skillsDir: string): Promise<boolean> {
|
||||
try {
|
||||
const entries = await readdir(skillsDir)
|
||||
return entries.some((e) => !e.startsWith('.'))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function seedDefaultSkills(): Promise<void> {
|
||||
const skillsDir = getSkillsDir()
|
||||
if (await hasExistingSkills(skillsDir)) return
|
||||
|
||||
let seeded = 0
|
||||
for (const skill of DEFAULT_SKILLS) {
|
||||
try {
|
||||
const targetDir = join(skillsDir, skill.id)
|
||||
await mkdir(targetDir, { recursive: true })
|
||||
await writeFile(join(targetDir, 'SKILL.md'), skill.content)
|
||||
seeded++
|
||||
} catch (err) {
|
||||
logger.warn('Failed to seed skill', {
|
||||
id: skill.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (seeded > 0) {
|
||||
logger.info(`Seeded ${seeded} default skills`)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
"start:agent": "bun ./scripts/build/controller-ext.ts && bun run --filter @browseros/agent dev",
|
||||
"build": "bun run build:server && bun run build:agent && bun run build:ext",
|
||||
"build:server": "FORCE_COLOR=1 bun scripts/build/server.ts --target=all",
|
||||
"build:server:test": "FORCE_COLOR=1 bun scripts/build/server.ts --target=darwin-arm64 --no-upload",
|
||||
"start:server:test": "bun run build:server:test && set -a && . apps/server/.env.development && set +a && dist/prod/server/.tmp/binaries/browseros-server-darwin-arm64",
|
||||
"build:agent:dev": "FORCE_COLOR=1 bun run --filter @browseros/agent --elide-lines=0 build:dev",
|
||||
"build:agent": "bun run codegen:agent && bun run --filter @browseros/agent build",
|
||||
"build:agent-sdk": "bun run --filter @browseros-ai/agent-sdk build",
|
||||
|
||||
Reference in New Issue
Block a user