diff --git a/apps/skills/app-navigation/SKILL.md b/apps/skills/app-navigation/SKILL.md new file mode 100644 index 00000000..0cb6e05e --- /dev/null +++ b/apps/skills/app-navigation/SKILL.md @@ -0,0 +1,91 @@ +--- +name: app-navigation +description: >- + Navigate the app UI - open notes, switch views, filter and search the knowledge base, and manage saved views. Use when the user wants to open notes or change the UI view. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "App Navigation" + author: rowboatlabs + tags: "navigation, ui, knowledge-base" +--- + +# App Navigation Skill + +You have access to the **app-navigation** tool which lets you control the Rowboat UI directly — opening notes, switching views, filtering the knowledge base, and creating saved views. + +## Actions + +### open-note +Open a specific knowledge file in the editor pane. + +**When to use:** When the user asks to see, open, or view a specific note (e.g., "open John's note", "show me the Acme project page"). + +**Parameters:** +- `path`: Full workspace-relative path (e.g., `knowledge/People/John Smith.md`) + +**Tips:** +- Use `workspace-grep` first to find the exact path if you're unsure of the filename. +- Always pass the full `knowledge/...` path, not just the filename. + +### open-view +Switch the UI to the graph or bases view. + +**When to use:** When the user asks to see the knowledge graph, view all notes, or open the bases/table view. + +**Parameters:** +- `view`: `\"graph\"` or `\"bases\"` + +### update-base-view +Change filters, columns, sort order, or search in the bases (table) view. + +**When to use:** When the user asks to find, filter, sort, or search notes. Examples: "show me all active customers", "filter by topic=hiring", "sort by name", "search for pricing". + +**Parameters:** +- `filters`: Object with `set`, `add`, `remove`, or `clear` — each takes an array of `{ category, value }` pairs. + - `set`: Replace ALL current filters with these. + - `add`: Append filters without removing existing ones. + - `remove`: Remove specific filters. + - `clear: true`: Remove all filters. +- `columns`: Object with `set`, `add`, or `remove` — each takes an array of column names (frontmatter keys). +- `sort`: `{ field, dir }` where dir is `\"asc\"` or `\"desc\"`. +- `search`: Free-text search string. + +**Tips:** +- If unsure what categories/values are available, call `get-base-state` first. +- For "show me X", prefer `filters.set` to start fresh rather than `filters.add`. +- Categories come from frontmatter keys (e.g., relationship, status, topic, type). +- **CRITICAL: Do NOT pass `columns` unless the user explicitly asks to show/hide specific columns.** Omit the `columns` parameter entirely when only filtering, sorting, or searching. Passing `columns` will override the user's current column layout and can make the view appear empty. + +### get-base-state +Retrieve information about what's in the knowledge base — available filter categories, values, and note count. + +**When to use:** When you need to know what properties exist before filtering, or when the user asks "what can I filter by?", "how many notes are there?", etc. + +**Parameters:** +- `base_name` (optional): Name of a saved base to inspect. + +### create-base +Save the current view configuration as a named base. + +**When to use:** When the user asks to save a filtered view, create a saved search, or says "save this as [name]". + +**Parameters:** +- `name`: Human-readable name for the base. + +## Workflow Example + +1. User: "Show me all people who are customers" +2. First, check what properties are available: + `app-navigation({ action: \"get-base-state\" })` +3. Apply filters based on the available properties: + `app-navigation({ action: \"update-base-view\", filters: { set: [{ category: \"relationship\", value: \"customer\" }] } })` +4. If the user wants to save it: + `app-navigation({ action: \"create-base\", name: \"Customers\" })` + +## Important Notes +- The `update-base-view` action will automatically navigate to the bases view if the user isn't already there. +- `open-note` validates that the file exists before navigating. +- Filter categories and values come from frontmatter in knowledge files. +- **Never send `columns` or `sort` with `update-base-view` unless the user specifically asks to change them.** Only pass the parameters you intend to change — omitted parameters are left untouched. diff --git a/apps/skills/background-agents/SKILL.md b/apps/skills/background-agents/SKILL.md new file mode 100644 index 00000000..20855351 --- /dev/null +++ b/apps/skills/background-agents/SKILL.md @@ -0,0 +1,564 @@ +--- +name: background-agents +description: >- + Creating, editing, and scheduling background agents. Configure schedules and build multi-agent workflows. Use when the user wants to create, inspect, or schedule background agents. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Background Agents" + author: rowboatlabs + tags: "agents, automation, scheduling" +--- + +# Background Agents + +Load this skill whenever a user wants to inspect, create, edit, or schedule background agents inside the Rowboat workspace. + +## Core Concepts + +**IMPORTANT**: In the CLI, there are NO separate "workflow" files. Everything is an agent. + +- **All definitions live in `agents/*.md`** - Markdown files with YAML frontmatter +- Agents configure a model, tools (in frontmatter), and instructions (in the body) +- Tools can be: builtin (like `executeCommand`), MCP integrations, or **other agents** +- **"Workflows" are just agents that orchestrate other agents** by having them as tools +- **Background agents run on schedules** defined in `~/.rowboat/config/agent-schedule.json` + +## How multi-agent workflows work + +1. **Create an orchestrator agent** that has other agents in its `tools` +2. **Schedule the orchestrator** in agent-schedule.json (see Scheduling section below) +3. The orchestrator calls other agents as tools when needed +4. Data flows through tool call parameters and responses + +## Scheduling Background Agents + +Background agents run automatically based on schedules defined in `~/.rowboat/config/agent-schedule.json`. + +### Schedule Configuration File + +```json" + ` +{ + "agents": { + "agent_name": { + "schedule": { ... }, + "enabled": true + } + } +} +``` + +### Schedule Types + +**IMPORTANT: All times are in local time** (the timezone of the machine running Rowboat). + +**1. Cron Schedule** - Runs at exact times defined by cron expression +```json" + ` +{ + "schedule": { + "type": "cron", + "expression": "0 8 * * *" + }, + "enabled": true +} +``` + +Common cron expressions: +- `*/5 * * * *` - Every 5 minutes +- `0 8 * * *` - Every day at 8am +- `0 9 * * 1` - Every Monday at 9am +- `0 0 1 * *` - First day of every month at midnight + +**2. Window Schedule** - Runs once during a time window +```json" + ` +{ + "schedule": { + "type": "window", + "cron": "0 0 * * *", + "startTime": "08:00", + "endTime": "10:00" + }, + "enabled": true +} +``` + +The agent will run once at a random time within the window. Use this when you want flexibility (e.g., "sometime in the morning" rather than "exactly at 8am"). + +**3. Once Schedule** - Runs exactly once at a specific time +```json" + ` +{ + "schedule": { + "type": "once", + "runAt": "2024-02-05T10:30:00" + }, + "enabled": true +} +``` + +Use this for one-time tasks like migrations or setup scripts. The `runAt` is in local time (no Z suffix). + +### Starting Message + +You can specify a `startingMessage` that gets sent to the agent when it starts. If not provided, defaults to `\"go\"`. + +```json" + ` +{ + "schedule": { "type": "cron", "expression": "0 8 * * *" }, + "enabled": true, + "startingMessage": "Please summarize my emails from the last 24 hours" +} +``` + +### Description + +You can add a `description` field to describe what the agent does. This is displayed in the UI. + +```json" + ` +{ + "schedule": { "type": "cron", "expression": "0 8 * * *" }, + "enabled": true, + "description": "Summarizes emails and calendar events every morning" +} +``` + +### Complete Schedule Example + +```json" + ` +{ + "agents": { + "daily_digest": { + "schedule": { + "type": "cron", + "expression": "0 8 * * *" + }, + "enabled": true, + "description": "Daily email and calendar summary", + "startingMessage": "Summarize my emails and calendar for today" + }, + "morning_briefing": { + "schedule": { + "type": "window", + "cron": "0 0 * * *", + "startTime": "07:00", + "endTime": "09:00" + }, + "enabled": true, + "description": "Morning news and updates briefing" + }, + "one_time_setup": { + "schedule": { + "type": "once", + "runAt": "2024-12-01T12:00:00" + }, + "enabled": true, + "description": "One-time data migration task" + } + } +} +``` + +### Schedule State (Read-Only) + +**IMPORTANT: Do NOT modify `agent-schedule-state.json`** - it is managed automatically by the background runner. + +The runner automatically tracks execution state in `~/.rowboat/config/agent-schedule-state.json`: +- `status`: scheduled, running, finished, failed, triggered (for once-schedules) +- `lastRunAt`: When the agent last ran +- `nextRunAt`: When the agent will run next +- `lastError`: Error message if the last run failed +- `runCount`: Total number of runs + +When you add an agent to `agent-schedule.json`, the runner will automatically create and manage its state entry. You only need to edit `agent-schedule.json`. + +## Agent File Format + +Agent files are **Markdown files with YAML frontmatter**. The frontmatter contains configuration (model, tools), and the body contains the instructions. + +### Basic Structure +```markdown" + ` +--- +model: gpt-5.1 +tools: + tool_key: + type: builtin + name: tool_name +--- +# Instructions + +Your detailed instructions go here in Markdown format. +``` + +### Frontmatter Fields +- `model`: (OPTIONAL) Model to use (e.g., 'gpt-5.1', 'claude-sonnet-4-5') +- `provider`: (OPTIONAL) Provider alias from models.json +- `tools`: (OPTIONAL) Object containing tool definitions + +### Instructions (Body) +The Markdown body after the frontmatter contains the agent's instructions. Use standard Markdown formatting. + +### Naming Rules +- Agent filename determines the agent name (without .md extension) +- Example: `summariser_agent.md` creates an agent named "summariser_agent" +- Use lowercase with underscores for multi-word names +- No spaces or special characters in names +- **The agent name in agent-schedule.json must match the filename** (without .md) + +### Agent Format Example +```markdown" + ` +--- +model: gpt-5.1 +tools: + search: + type: mcp + name: firecrawl_search + description: Search the web + mcpServerName: firecrawl + inputSchema: + type: object + properties: + query: + type: string + description: Search query + required: + - query +--- +# Web Search Agent + +You are a web search agent. When asked a question: + +1. Use the search tool to find relevant information +2. Summarize the results clearly +3. Cite your sources + +Be concise and accurate. +``` + +## Tool Types & Schemas + +Tools in agents must follow one of three types. Each has specific required fields. + +### 1. Builtin Tools +Internal Rowboat tools (executeCommand, file operations, MCP queries, etc.) + +**YAML Schema:** +```yaml" + ` +tool_key: + type: builtin + name: tool_name +``` + +**Required fields:** +- `type`: Must be "builtin" +- `name`: Builtin tool name (e.g., "executeCommand", "workspace-readFile") + +**Example:** +```yaml" + ` +bash: + type: builtin + name: executeCommand +``` + +**Available builtin tools:** +- `executeCommand` - Execute shell commands +- `workspace-readFile`, `workspace-writeFile`, `workspace-remove` - File operations +- `workspace-readdir`, `workspace-exists`, `workspace-stat` - Directory operations +- `workspace-mkdir`, `workspace-rename`, `workspace-copy` - File/directory management +- `analyzeAgent` - Analyze agent structure +- `addMcpServer`, `listMcpServers`, `listMcpTools` - MCP management +- `loadSkill` - Load skill guidance + +### 2. MCP Tools +Tools from external MCP servers (APIs, databases, web scraping, etc.) + +**YAML Schema:** +```yaml" + ` +tool_key: + type: mcp + name: tool_name_from_server + description: What the tool does + mcpServerName: server_name_from_config + inputSchema: + type: object + properties: + param: + type: string + description: Parameter description + required: + - param +``` + +**Required fields:** +- `type`: Must be "mcp" +- `name`: Exact tool name from MCP server +- `description`: What the tool does (helps agent understand when to use it) +- `mcpServerName`: Server name from config/mcp.json +- `inputSchema`: Full JSON Schema object for tool parameters + +**Example:** +```yaml" + ` +search: + type: mcp + name: firecrawl_search + description: Search the web + mcpServerName: firecrawl + inputSchema: + type: object + properties: + query: + type: string + description: Search query + required: + - query +``` + +**Important:** +- Use `listMcpTools` to get the exact inputSchema from the server +- Copy the schema exactly—don't modify property types or structure +- Only include `required` array if parameters are mandatory + +### 3. Agent Tools (for chaining agents) +Reference other agents as tools to build multi-agent workflows + +**YAML Schema:** +```yaml" + ` +tool_key: + type: agent + name: target_agent_name +``` + +**Required fields:** +- `type`: Must be "agent" +- `name`: Name of the target agent (must exist in agents/ directory) + +**Example:** +```yaml" + ` +summariser: + type: agent + name: summariser_agent +``` + +**How it works:** +- Use `type: agent` to call other agents as tools +- The target agent will be invoked with the parameters you pass +- Results are returned as tool output +- This is how you build multi-agent workflows +- The referenced agent file must exist (e.g., `agents/summariser_agent.md`) + +## Complete Multi-Agent Workflow Example + +**Email digest workflow** - This is all done through agents calling other agents: + +**1. Task-specific agent** (`agents/email_reader.md`): +```markdown" + ` +--- +model: gpt-5.1 +tools: + read_file: + type: builtin + name: workspace-readFile + list_dir: + type: builtin + name: workspace-readdir +--- +# Email Reader Agent + +Read emails from the gmail_sync folder and extract key information. +Look for unread or recent emails and summarize the sender, subject, and key points. +Don't ask for human input. +``` + +**2. Agent that delegates to other agents** (`agents/daily_summary.md`): +```markdown" + ` +--- +model: gpt-5.1 +tools: + email_reader: + type: agent + name: email_reader + write_file: + type: builtin + name: workspace-writeFile +--- +# Daily Summary Agent + +1. Use the email_reader tool to get email summaries +2. Create a consolidated daily digest +3. Save the digest to ~/Desktop/daily_digest.md + +Don't ask for human input. +``` + +Note: The output path (`~/Desktop/daily_digest.md`) is hardcoded in the instructions. When creating agents that output files, always ask the user where they want files saved and include the full path in the agent instructions. + +**3. Orchestrator agent** (`agents/morning_briefing.md`): +```markdown" + ` +--- +model: gpt-5.1 +tools: + daily_summary: + type: agent + name: daily_summary + search: + type: mcp + name: search + mcpServerName: exa + description: Search the web for news + inputSchema: + type: object + properties: + query: + type: string + description: Search query +--- +# Morning Briefing Workflow + +Create a morning briefing: + +1. Get email digest using daily_summary +2. Search for relevant news using the search tool +3. Compile a comprehensive morning briefing + +Execute these steps in sequence. Don't ask for human input. +``` + +**4. Schedule the workflow** in `~/.rowboat/config/agent-schedule.json`: +```json" + ` +{ + "agents": { + "morning_briefing": { + "schedule": { + "type": "cron", + "expression": "0 7 * * *" + }, + "enabled": true, + "startingMessage": "Create my morning briefing for today" + } + } +} +``` + +This schedules the morning briefing workflow to run every day at 7am local time. + +## Naming and organization rules +- **All agents live in `agents/*.md`** - Markdown files with YAML frontmatter +- Agent filename (without .md) becomes the agent name +- When referencing an agent as a tool, use its filename without extension +- When scheduling an agent, use its filename without extension in agent-schedule.json +- Use relative paths (no \${BASE_DIR} prefixes) when giving examples to users + +## Best practices for background agents +1. **Single responsibility**: Each agent should do one specific thing well +2. **Clear delegation**: Agent instructions should explicitly say when to call other agents +3. **Autonomous operation**: Add "Don't ask for human input" for background agents +4. **Data passing**: Make it clear what data to extract and pass between agents +5. **Tool naming**: Use descriptive tool keys (e.g., "summariser", "fetch_data", "analyze") +6. **Orchestration**: Create a top-level agent that coordinates the workflow +7. **Scheduling**: Use appropriate schedule types - cron for recurring, window for flexible timing, once for one-time tasks +8. **Error handling**: Background agents should handle errors gracefully since there's no human to intervene +9. **Avoid executeCommand**: Do NOT attach `executeCommand` to background agents as it poses security risks when running unattended. Instead, use the specific builtin tools needed (`workspace-readFile`, `workspace-writeFile`, etc.) or MCP tools for external integrations +10. **File output paths**: When creating an agent that outputs files, ASK the user where the file should be stored (default to Desktop: `~/Desktop`). Then hardcode the full output path in the agent's instructions so it knows exactly where to write files. Example instruction: "Save the output to /Users/username/Desktop/daily_report.md" + +## Validation & Best Practices + +### CRITICAL: Schema Compliance +- Agent files MUST be valid Markdown with YAML frontmatter +- Agent filename (without .md) becomes the agent name +- Tools in frontmatter MUST have valid `type` ("builtin", "mcp", or "agent") +- MCP tools MUST have all required fields: name, description, mcpServerName, inputSchema +- Agent tools MUST reference existing agent files +- Invalid agents will fail to load and prevent workflow execution + +### File Creation/Update Process +1. When creating an agent, use `workspace-writeFile` with valid Markdown + YAML frontmatter +2. When updating an agent, read it first with `workspace-readFile`, modify, then use `workspace-writeFile` +3. Validate YAML syntax in frontmatter before writing—malformed YAML breaks the agent +4. **Quote strings containing colons** (e.g., `description: \"Default: 8\"` not `description: Default: 8`) +5. Test agent loading after creation/update by using `analyzeAgent` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing frontmatter delimiters:** +```markdown" + ` +model: gpt-5.1 +# My Agent +Instructions here +``` + +❌ **WRONG - Invalid YAML indentation:** +```markdown" + ` +--- +tools: +bash: + type: builtin +--- +``` +(bash should be indented under tools) + +❌ **WRONG - Invalid tool type:** +```yaml" + ` +tools: + tool1: + type: custom + name: something +``` +(type must be builtin, mcp, or agent) + +❌ **WRONG - Unquoted strings containing colons:** +```yaml" + ` +tools: + search: + description: Number of results (default: 8) +``` +(Strings with colons must be quoted: `description: \"Number of results (default: 8)\"`) + +❌ **WRONG - MCP tool missing required fields:** +```yaml" + ` +tools: + search: + type: mcp + name: firecrawl_search +``` +(Missing: description, mcpServerName, inputSchema) + +✅ **CORRECT - Minimal valid agent** (`agents/simple_agent.md`): +```markdown" + ` +--- +model: gpt-5.1 +--- +# Simple Agent + +Do simple tasks as instructed. +``` + +✅ **CORRECT - Agent with MCP tool** (`agents/search_agent.md`): +```markdown" + ` +--- +model: gpt-5.1 +tools: + search: + type: mcp + name: firecrawl_search + description: Search the web + mcpServerName: firecrawl + inputSchema: + type: object + properties: + query: + type: string +--- +# Search Agent + +Use the search tool to find information on the web. +``` + +## Capabilities checklist +1. Explore `agents/` directory to understand existing agents before editing +2. Read existing agents with `workspace-readFile` before making changes +3. Validate YAML frontmatter syntax before creating/updating agents +4. Use `analyzeAgent` to verify agent structure after creation/update +5. When creating multi-agent workflows, create an orchestrator agent +6. Add other agents as tools with `type: agent` for chaining +7. Use `listMcpServers` and `listMcpTools` when adding MCP integrations +8. Configure schedules in `~/.rowboat/config/agent-schedule.json` (ONLY edit this file, NOT the state file) +9. Confirm work done and outline next steps once changes are complete diff --git a/apps/skills/browser-control/SKILL.md b/apps/skills/browser-control/SKILL.md new file mode 100644 index 00000000..e38e8f10 --- /dev/null +++ b/apps/skills/browser-control/SKILL.md @@ -0,0 +1,113 @@ +--- +name: browser-control +description: >- + Control Rowboat's embedded browser pane — open sites, inspect the live page, switch tabs, and interact with indexed page elements. Use when the user wants to open a website in-app, search the web in the browser pane, click something on a page, fill a form, or interact with a live webpage inside Rowboat. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: Browser Control +--- + +# Browser Control Skill + +You have access to the **browser-control** tool, which controls Rowboat's embedded browser pane directly. + +Use this skill when the user asks you to open a website, browse in-app, search the web in the browser pane, click something on a page, fill a form, or otherwise interact with a live webpage inside Rowboat. + +## Core Workflow + +1. Start with `browser-control({ action: "open" })` if the browser pane may not already be open. +2. Use `browser-control({ action: "read-page" })` to inspect the current page. +3. The tool returns: + - `snapshotId` + - page `url` and `title` + - visible page text + - interactable elements with numbered `index` values +4. Prefer acting on those numbered indices with `click` / `type` / `press`. +5. After each action, read the returned page snapshot before deciding the next step. + +## Actions + +### open +Open the browser pane and ensure an active tab exists. + +### get-state +Return the current browser tabs and active tab id. + +### new-tab +Open a new browser tab. + +Parameters: +- `target` (optional): URL or plain-language search query + +### switch-tab +Switch to a tab by `tabId`. + +### close-tab +Close a tab by `tabId`. + +### navigate +Navigate the active tab. + +Parameters: +- `target`: URL or plain-language search query + +Plain-language targets are converted into a search automatically. + +### back / forward / reload +Standard browser navigation controls. + +### read-page +Read the current page and return a compact snapshot. + +Parameters: +- `maxElements` (optional) +- `maxTextLength` (optional) + +### click +Click an element. + +Prefer: +- `index`: element index from `read-page` + +Optional: +- `snapshotId`: include it when acting on a recent snapshot +- `selector`: fallback only when no usable index exists + +### type +Type into an input, textarea, or contenteditable element. + +Parameters: +- `text`: text to enter +- plus the same target fields as `click` + +### press +Send a key press such as `Enter`, `Tab`, `Escape`, or arrow keys. + +Parameters: +- `key` +- optional target fields if you need to focus a specific element first + +### scroll +Scroll the current page. + +Parameters: +- `direction`: `"up"` or `"down"` (optional; defaults down) +- `amount`: pixel distance (optional) + +### wait +Wait for the page to settle, useful after async UI changes. + +Parameters: +- `ms`: milliseconds to wait (optional) + +## Important Rules + +- Prefer `read-page` before interacting. +- Prefer element `index` over CSS selectors. +- If the tool says the snapshot is stale, call `read-page` again. +- After navigation, clicking, typing, pressing, or scrolling, use the returned page snapshot instead of assuming the page state. +- Use Rowboat's browser for live interaction. Use web search tools for research where a live session is unnecessary. +- Do not wrap browser URLs or browser pages in ```filepath blocks. Filepath cards are only for real files on disk, not web pages or browser tabs. +- If you mention a page the browser opened, use plain text for the URL/title instead of trying to create a clickable file card. diff --git a/apps/skills/builtin-tools/SKILL.md b/apps/skills/builtin-tools/SKILL.md new file mode 100644 index 00000000..2b0b4f47 --- /dev/null +++ b/apps/skills/builtin-tools/SKILL.md @@ -0,0 +1,237 @@ +--- +name: builtin-tools +description: >- + Understanding and using builtin tools like executeCommand for bash/shell in agent definitions. Use when creating or modifying agents that need builtin tool access. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Builtin Tools Reference" + author: rowboatlabs + tags: "tools, agents, reference" +--- + +# Builtin Tools Reference + +Load this skill when creating or modifying agents that need access to Rowboat's builtin tools (shell execution, file operations, etc.). + +## Available Builtin Tools + +Agents can use builtin tools by declaring them in the YAML frontmatter \`tools\` section with \`type: builtin\` and the appropriate \`name\`. + +### executeCommand +**The most powerful and versatile builtin tool** - Execute any bash/shell command and get the output. + +**Security note:** Commands are filtered through \`.rowboat/config/security.json\`. Populate this file with allowed command names (array or dictionary entries). Any command not present is blocked and returns exit code 126 so the agent knows it violated the policy. + +**Agent tool declaration (YAML frontmatter):** +\`\`\`yaml +tools: + bash: + type: builtin + name: executeCommand +\`\`\` + +**What it can do:** +- Run package managers (npm, pip, apt, brew, cargo, go get, etc.) +- Git operations (clone, commit, push, pull, status, diff, log, etc.) +- System operations (ps, top, df, du, find, grep, kill, etc.) +- Build and compilation (make, cargo build, go build, npm run build, etc.) +- Network operations (curl, wget, ping, ssh, netstat, etc.) +- Text processing (awk, sed, grep, jq, yq, cut, sort, uniq, etc.) +- Database operations (psql, mysql, mongo, redis-cli, etc.) +- Container operations (docker, kubectl, podman, etc.) +- Testing and debugging (pytest, jest, cargo test, etc.) +- File operations (cat, head, tail, wc, diff, patch, etc.) +- Any CLI tool or script execution + +**Agent instruction examples:** +- "Use the bash tool to run git commands for version control operations" +- "Execute curl commands using the bash tool to fetch data from APIs" +- "Use bash to run 'npm install' and 'npm test' commands" +- "Run Python scripts using the bash tool with 'python script.py'" +- "Use bash to execute 'docker ps' and inspect container status" +- "Run database queries using 'psql' or 'mysql' commands via bash" +- "Use bash to execute system monitoring commands like 'top' or 'ps aux'" + +**Pro tips for agent instructions:** +- Commands can be chained with && for sequential execution +- Use pipes (|) to combine Unix tools (e.g., "cat file.txt | grep pattern | wc -l") +- Redirect output with > or >> when needed +- Full bash shell features are available (variables, loops, conditionals, etc.) +- Tools like jq, yq, awk, sed can parse and transform data + +**Example agent with executeCommand** (\`agents/arxiv-feed-reader.md\`): +\`\`\`markdown +--- +model: gpt-5.1 +tools: + bash: + type: builtin + name: executeCommand +--- +# arXiv Feed Reader + +Extract latest papers from the arXiv feed and summarize them. + +Use curl to fetch the RSS feed, then parse it with yq and jq: + +\\\`\\\`\\\`bash +curl -s https://rss.arxiv.org/rss/cs.AI | yq -p=xml -o=json | jq -r '.rss.channel.item[] | select(.title | test("agent"; "i")) | "\\(.title)\\n\\(.link)\\n\\(.description)\\n"' +\\\`\\\`\\\` + +This will give you papers containing 'agent' in the title. +\`\`\` + +**Another example - System monitoring agent** (\`agents/system-monitor.md\`): +\`\`\`markdown +--- +model: gpt-5.1 +tools: + bash: + type: builtin + name: executeCommand +--- +# System Monitor + +Monitor system resources using bash commands: +- Use 'df -h' for disk usage +- Use 'free -h' for memory +- Use 'top -bn1' for processes +- Use 'ps aux' for process list + +Parse the output and report any issues. +\`\`\` + +**Another example - Git automation agent** (\`agents/git-helper.md\`): +\`\`\`markdown +--- +model: gpt-5.1 +tools: + bash: + type: builtin + name: executeCommand +--- +# Git Helper + +Help with git operations. Use commands like: +- 'git status' - Check working tree status +- 'git log --oneline -10' - View recent commits +- 'git diff' - See changes +- 'git branch -a' - List branches + +Can also run 'git add', 'git commit', 'git push' when instructed. +\`\`\` + +## Agent-to-Agent Calling + +Agents can call other agents as tools to create complex multi-step workflows. This is the core mechanism for building multi-agent systems in the CLI. + +**Tool declaration (YAML frontmatter):** +\`\`\`yaml +tools: + summariser: + type: agent + name: summariser_agent +\`\`\` + +**When to use:** +- Breaking complex tasks into specialized sub-agents +- Creating reusable agent components +- Orchestrating multi-step workflows +- Delegating specialized tasks (e.g., summarization, data processing, audio generation) + +**How it works:** +- The agent calls the tool like any other tool +- The target agent receives the input and processes it +- Results are returned as tool output +- The calling agent can then continue processing or delegate further + +**Example - Agent that delegates to a summarizer** (\`agents/paper_analyzer.md\`): +\`\`\`markdown +--- +model: gpt-5.1 +tools: + summariser: + type: agent + name: summariser_agent +--- +# Paper Analyzer + +Pick 2 interesting papers and summarise each using the summariser tool. +Pass the paper URL to the summariser. Don't ask for human input. +\`\`\` + +**Tips for agent chaining:** +- Make instructions explicit about when to call other agents +- Pass clear, structured data between agents +- Add "Don't ask for human input" for autonomous workflows +- Keep each agent focused on a single responsibility + +## Additional Builtin Tools + +While \`executeCommand\` is the most versatile, other builtin tools exist for specific Rowboat operations (file management, agent inspection, etc.). These are primarily used by the Rowboat copilot itself and are not typically needed in user agents. If you need file operations, consider using bash commands like \`cat\`, \`echo\`, \`tee\`, etc. through \`executeCommand\`. + +### Copilot-Specific Builtin Tools + +The Rowboat copilot has access to special builtin tools that regular agents don't typically use. These tools help the copilot assist users with workspace management and MCP integration: + +#### File & Directory Operations +- \`workspace-readdir\` - List directory contents (supports recursive exploration) +- \`workspace-readFile\` - Read file contents +- \`workspace-writeFile\` - Create or update file contents +- \`workspace-edit\` - Make precise edits by replacing specific text (safer than full rewrites) +- \`workspace-remove\` - Remove files or directories +- \`workspace-exists\` - Check if a file or directory exists +- \`workspace-stat\` - Get file/directory statistics +- \`workspace-mkdir\` - Create directories +- \`workspace-rename\` - Rename or move files/directories +- \`workspace-copy\` - Copy files +- \`workspace-getRoot\` - Get workspace root directory path +- \`workspace-glob\` - Find files matching a glob pattern (e.g., "**/*.ts", "agents/*.md") +- \`workspace-grep\` - Search file contents using regex, returns matching files and lines + +#### Agent Operations +- \`analyzeAgent\` - Read and analyze an agent file structure +- \`loadSkill\` - Load a Rowboat skill definition into context + +#### MCP Operations +- \`addMcpServer\` - Add or update an MCP server configuration (with validation) +- \`listMcpServers\` - List all available MCP servers +- \`listMcpTools\` - List all available tools from a specific MCP server +- \`executeMcpTool\` - **Execute a specific MCP tool on behalf of the user** + +#### Using executeMcpTool as Copilot + +The \`executeMcpTool\` builtin allows the copilot to directly execute MCP tools without creating an agent. Load the "mcp-integration" skill for complete guidance on discovering and executing MCP tools, including workflows, schema matching, and examples. + +**When to use executeMcpTool vs creating an agent:** +- Use \`executeMcpTool\` for immediate, one-time tasks +- Create an agent when the user needs repeated use or autonomous operation +- Create an agent for complex multi-step workflows involving multiple tools + +## Best Practices + +1. **Give agents clear examples** in their instructions showing exact bash commands to run +2. **Explain output parsing** - show how to use jq, yq, grep, awk to extract data +3. **Chain commands efficiently** - use && for sequences, | for pipes +4. **Handle errors** - remind agents to check exit codes and stderr +5. **Be specific** - provide example commands rather than generic descriptions +6. **Security** - remind agents to validate inputs and avoid dangerous operations + +## When to Use Builtin Tools vs MCP Tools vs Agent Tools + +- **Use builtin executeCommand** when you need: CLI tools, system operations, data processing, git operations, any shell command +- **Use MCP tools** when you need: Web scraping (firecrawl), text-to-speech (elevenlabs), specialized APIs, external service integrations +- **Use agent tools (\`type: agent\`)** when you need: Complex multi-step logic, task delegation, specialized processing that benefits from LLM reasoning + +Many tasks can be accomplished with just \`executeCommand\` and common Unix tools - it's incredibly powerful! + +## Key Insight: Multi-Agent Workflows + +In the CLI, multi-agent workflows are built by: +1. Creating specialized agents as Markdown files in the \`agents/\` directory +2. Creating an orchestrator agent that has other agents in its \`tools\` (YAML frontmatter) +3. Running the orchestrator with \`rowboatx --agent orchestrator_name\` + +There are no separate "workflow" files - everything is an agent defined in Markdown! diff --git a/apps/x/packages/core/src/application/assistant/skills/composio-integration/skill.ts b/apps/skills/composio-integration/SKILL.md similarity index 53% rename from apps/x/packages/core/src/application/assistant/skills/composio-integration/skill.ts rename to apps/skills/composio-integration/SKILL.md index 795daeeb..08cabbb9 100644 --- a/apps/x/packages/core/src/application/assistant/skills/composio-integration/skill.ts +++ b/apps/skills/composio-integration/SKILL.md @@ -1,4 +1,14 @@ -export const skill = String.raw` +--- +name: composio-integration +description: >- + Discover, connect, and execute Composio tools for any third-party service — email, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, calendar, and more. Use when the user asks to interact with any third-party service. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: Composio Integration +--- + # Composio Integration **Load this skill** when the user asks to interact with ANY third-party service — email, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, calendar, etc. This skill provides the complete workflow for discovering, connecting, and executing Composio tools. @@ -16,34 +26,34 @@ export const skill = String.raw` | Service | Slug | |---------|------| -| Gmail | \`gmail\` | -| Google Calendar | \`googlecalendar\` | -| Google Sheets | \`googlesheets\` | -| Google Docs | \`googledocs\` | -| Google Drive | \`googledrive\` | -| Slack | \`slack\` | -| GitHub | \`github\` | -| Notion | \`notion\` | -| Linear | \`linear\` | -| Jira | \`jira\` | -| Asana | \`asana\` | -| Trello | \`trello\` | -| HubSpot | \`hubspot\` | -| Salesforce | \`salesforce\` | -| LinkedIn | \`linkedin\` | -| X (Twitter) | \`twitter\` | -| Reddit | \`reddit\` | -| Dropbox | \`dropbox\` | -| OneDrive | \`onedrive\` | -| Microsoft Outlook | \`microsoft_outlook\` | -| Microsoft Teams | \`microsoft_teams\` | -| Calendly | \`calendly\` | -| Cal.com | \`cal\` | -| Intercom | \`intercom\` | -| Zendesk | \`zendesk\` | -| Airtable | \`airtable\` | +| Gmail | `gmail` | +| Google Calendar | `googlecalendar` | +| Google Sheets | `googlesheets` | +| Google Docs | `googledocs` | +| Google Drive | `googledrive` | +| Slack | `slack` | +| GitHub | `github` | +| Notion | `notion` | +| Linear | `linear` | +| Jira | `jira` | +| Asana | `asana` | +| Trello | `trello` | +| HubSpot | `hubspot` | +| Salesforce | `salesforce` | +| LinkedIn | `linkedin` | +| X (Twitter) | `twitter` | +| Reddit | `reddit` | +| Dropbox | `dropbox` | +| OneDrive | `onedrive` | +| Microsoft Outlook | `microsoft_outlook` | +| Microsoft Teams | `microsoft_teams` | +| Calendly | `calendly` | +| Cal.com | `cal` | +| Intercom | `intercom` | +| Zendesk | `zendesk` | +| Airtable | `airtable` | -**IMPORTANT:** Always use these exact slugs. Do NOT guess — e.g., Google Sheets is \`googlesheets\` (no underscore), not \`google_sheets\`. +**IMPORTANT:** Always use these exact slugs. Do NOT guess — e.g., Google Sheets is `googlesheets` (no underscore), not `google_sheets`. ## Critical: Check First, Connect Second @@ -52,10 +62,10 @@ export const skill = String.raw` **Flow:** 1. Check if the service is in the "Currently connected" list (in the system prompt above) 2. If **connected** → go directly to step 4 -3. If **NOT connected** → call \`composio-connect-toolkit\` once, wait for user to authenticate, then continue -4. Call \`composio-search-tools\` with SHORT keyword queries -5. Read the \`inputSchema\` from results — note \`required\` fields -6. Call \`composio-execute-tool\` with slug, toolkit, and all required arguments +3. If **NOT connected** → call `composio-connect-toolkit` once, wait for user to authenticate, then continue +4. Call `composio-search-tools` with SHORT keyword queries +5. Read the `inputSchema` from results — note `required` fields +6. Call `composio-execute-tool` with slug, toolkit, and all required arguments **NEVER call composio-connect-toolkit for a service that's already connected.** This creates duplicate connect cards in the UI. @@ -74,42 +84,42 @@ If the first search returns 0 results, try a different short query (e.g., "issue ## Passing Arguments -**ALWAYS include the \`arguments\` field** when calling \`composio-execute-tool\`, even if the tool has no required parameters. +**ALWAYS include the `arguments` field** when calling `composio-execute-tool`, even if the tool has no required parameters. -- Read the \`inputSchema\` from search results carefully -- Extract user-provided values into the correct fields (e.g., "rowboatlabs/rowboat" → \`owner: "rowboatlabs", repo: "rowboat"\`) -- For tools with empty \`properties: {}\`, pass \`arguments: {}\` +- Read the `inputSchema` from search results carefully +- Extract user-provided values into the correct fields (e.g., "rowboatlabs/rowboat" → `owner: "rowboatlabs", repo: "rowboat"`) +- For tools with empty `properties: {}`, pass `arguments: {}` - For tools with required fields, pass all of them ### Example: GitHub Issues User says: "Get me the open issues on rowboatlabs/rowboat" -1. \`composio-search-tools({ query: "list issues", toolkitSlug: "github" })\` - → finds \`GITHUB_ISSUES_LIST_FOR_REPO\` with required: ["owner", "repo"] -2. \`composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })\` +1. `composio-search-tools({ query: "list issues", toolkitSlug: "github" })` + → finds `GITHUB_ISSUES_LIST_FOR_REPO` with required: ["owner", "repo"] +2. `composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })` ### Example: Gmail Fetch User says: "What's my latest email?" -1. \`composio-search-tools({ query: "fetch emails", toolkitSlug: "gmail" })\` - → finds \`GMAIL_FETCH_EMAILS\` -2. \`composio-execute-tool({ toolSlug: "GMAIL_FETCH_EMAILS", toolkitSlug: "gmail", arguments: { user_id: "me", max_results: 5 } })\` +1. `composio-search-tools({ query: "fetch emails", toolkitSlug: "gmail" })` + → finds `GMAIL_FETCH_EMAILS` +2. `composio-execute-tool({ toolSlug: "GMAIL_FETCH_EMAILS", toolkitSlug: "gmail", arguments: { user_id: "me", max_results: 5 } })` ### Example: LinkedIn Profile (no-arg tool) User says: "Get my LinkedIn profile" -1. \`composio-search-tools({ query: "get profile", toolkitSlug: "linkedin" })\` - → finds \`LINKEDIN_GET_MY_INFO\` with properties: {} -2. \`composio-execute-tool({ toolSlug: "LINKEDIN_GET_MY_INFO", toolkitSlug: "linkedin", arguments: {} })\` +1. `composio-search-tools({ query: "get profile", toolkitSlug: "linkedin" })` + → finds `LINKEDIN_GET_MY_INFO` with properties: {} +2. `composio-execute-tool({ toolSlug: "LINKEDIN_GET_MY_INFO", toolkitSlug: "linkedin", arguments: {} })` ## Error Recovery - **If a tool call fails** (missing fields, 500 error): Fix the arguments and retry IMMEDIATELY. Do NOT stop and narrate the error to the user. - **If search returns 0 results**: Try a different short query. If still 0, the tool may not exist for that service. -- **If a tool requires connection**: Call \`composio-connect-toolkit\` once, then retry after connection. +- **If a tool requires connection**: Call `composio-connect-toolkit` once, then retry after connection. ## Multi-Part Requests @@ -122,6 +132,3 @@ When the user says "connect X and then do Y" — complete BOTH parts in one turn - **Read-only actions** (fetch, list, get, search): Execute without asking - **Mutating actions** (send email, create issue, post, delete): Show the user what you're about to do and confirm before executing - **Connecting a toolkit**: Always safe — just do it when needed -`; - -export default skill; diff --git a/apps/skills/create-presentations/SKILL.md b/apps/skills/create-presentations/SKILL.md new file mode 100644 index 00000000..1ee3f10e --- /dev/null +++ b/apps/skills/create-presentations/SKILL.md @@ -0,0 +1,2752 @@ +--- +name: create-presentations +description: >- + Create PDF presentations and slide decks from natural language requests using knowledge base context. Use when the user wants to create a presentation, pitch deck, or slide deck. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Create Presentations" + author: rowboatlabs + tags: "presentations, pdf, slides" +--- + +# PDF Presentation Skill + +## Theme Selection + +If the user specifies a visual theme, colors, or brand guidelines, use those. If they do NOT specify a theme, **do not ask** — pick the best fit based on the topic and audience: + +- **Dark Professional** — Deep navy/charcoal backgrounds, indigo (#6366f1) and violet (#8b5cf6) accents, white text. Best for: tech, SaaS, keynotes, engineering. +- **Light Editorial** — White/warm cream backgrounds, amber (#f59e0b) and stone accents, dark text with serif headings. Best for: reports, proposals, thought leadership, research. +- **Bold Vibrant** — Mixed dark and light slides, emerald (#10b981) and rose (#f43e5c) accents, high contrast. Best for: pitch decks, marketing, creative, fundraising. + +Note the theme used at the end of delivery so the user can request a swap if they prefer a different look. + +## Visual Consistency Rules + +Every presentation must have a unified color theme applied across ALL slides. Do not mix unrelated color palettes between slides. + +1. **Define a theme palette upfront** — Pick one primary color, one accent color, and one neutral base (dark or light). Use these consistently across every slide. +2. **Backgrounds** — Use at most 2-3 background variations (e.g. dark base, light base, and primary color). Alternate them for rhythm but keep them from the same palette. +3. **Accent color** — Use the same accent color for all highlights: overlines, bullets, icons, chart fills, timeline dots, CTA buttons, divider lines. +4. **Typography colors** — Headings, body text, and muted text should use the same tones on every slide. Don't switch between warm and cool grays mid-deck. +5. **Charts and data** — Use shades/tints of the primary and accent colors for chart fills. Never introduce one-off colors that don't appear elsewhere in the deck. +6. **Consistent fonts** — Pick one heading font and one body font. Use them on every slide. Don't mix different heading fonts across slides. + +## Critical: One Theme Per Deck + +The example layouts in this document each use different colors and styles for showcase purposes only. When building an actual presentation, pick ONE theme and apply it consistently to EVERY slide. Borrow layout structures and patterns from the examples, but replace all colors, fonts, and backgrounds with your chosen theme's palette. Never copy the example colors verbatim — adapt them to the unified theme. + +### Visual Consistency Rules + +Every presentation must have a unified color theme applied across ALL slides. This is the #1 most important design rule. A deck where every slide looks like it belongs together is always better than a deck with individually beautiful but visually inconsistent slides. + +#### Background Strategy (STRICT) +Pick ONE dominant background tone and use it for 80%+ of slides. Add subtle variation within that tone — never alternate between dark and light backgrounds. + +##### For dark themes: + +Deep base (e.g. #0f172a) — use for title, section dividers, closing (primary background) +Medium base (e.g. #1e293b or #111827) — use for content slides, charts, tables (secondary background) +Accent pop (e.g. #6366f1) — use for 1-2 key stat or quote slides only (rare emphasis) +NEVER use white or light backgrounds in a dark-themed deck. Data tables, team grids, and other content that "feels light" should still use the dark palette with adjusted contrast. + +##### For light themes: + +Light base (e.g. #fafaf9 or #ffffff) — use for most content slides (primary background) +Warm tint (e.g. #fefce8 or #f8fafc) — use for alternation and visual rhythm (secondary background) +Accent pop (e.g. the theme's primary color) — use for 1-2 key stat or quote slides only (rare emphasis) +NEVER use dark/navy backgrounds in a light-themed deck. + +Never alternate between dark and light backgrounds. This creates a jarring strobe effect and breaks visual cohesion. The audience's eyes have to constantly readjust. Instead, create rhythm through subtle shade variation within the same tone family. +Never use more than 3 background color values across the entire deck. + +#### Color & Typography Rules + +Define a theme palette upfront — Pick one primary color, one accent color, and one neutral base (dark or light). Use these consistently across every slide. Write these as CSS variables and reference them everywhere. +Accent color — Use the SAME accent color for ALL highlights across the entire deck: overlines, bullets, icons, chart fills, timeline dots, CTA buttons, divider lines. Do not use different accent colors on different slides. +Typography colors — Headings, body text, and muted text should use the same tones on every slide. Don't switch between warm and cool grays mid-deck. +Charts and data — Use shades/tints of the primary and accent colors for chart fills. Never introduce one-off colors that don't appear elsewhere in the deck. +Consistent fonts — Pick one heading font and one body font. Use them on every slide. Don't mix different heading fonts across slides. + +#### Title Slide Rules + +Title text must span the FULL slide width. Never place a decorative element beside the title that competes for horizontal space. +Title slides should use a single-column, vertically-stacked layout: overline → title → subtitle → optional tags/pills. No side-by-side elements on title slides. +If a decorative visual is needed, place it BEHIND the text (as a CSS background, gradient, or pseudo-element), never beside it. +Title font-size must not exceed 64px. For titles longer than 5 words, use 48px max. + +## Content Planning (Do This Before Building) + +Before writing any HTML, plan the narrative arc: + +1. **Hook** — What's the opening statement or question that grabs attention? +2. **Core argument** — What's the one thing the audience should remember? +3. **Supporting evidence** — What data, examples, or frameworks back it up? +4. **Call to action** — What should the audience do next? + +Map each point to a slide layout from the Available Layout Types below. For a typical presentation, generate **8-15 slides**: title + agenda (optional) + 6-10 content slides + closing. Don't pad with filler — every slide should earn its place. Use layout variety — never use the same layout for consecutive slides. + +## Workflow + +1. Use workspace-readFile to check knowledge/ for relevant context about the company, product, team, etc. +2. Ensure Playwright is installed: \`npm install playwright && npx playwright install chromium\` +3. Use workspace-getRoot to get the workspace root path. +4. Plan the narrative arc and slide outline (see Content Planning above). +5. Use workspace-writeFile to create the HTML file at tmp/presentation.html (workspace-relative) with slides (1280x720px each). +6. **Perform the Post-Generation Validation (see below). Fix any issues before proceeding.** +7. Use workspace-writeFile to create the conversion script at tmp/convert.js (workspace-relative) — see Playwright Export section. +8. Run it: \`node /tmp/convert.js\` +9. Tell the user: "Your presentation is ready at ~/Desktop/presentation.pdf" and note the theme used. + +**Critical**: Never show HTML code to the user. Never ask the user to run commands, install packages, or make technical decisions. The entire pipeline from content to PDF must be invisible to the user. + +Use workspace-writeFile and workspace-readFile for ALL file operations. Do NOT use executeCommand to write or read files. + +## Post-Generation Validation (REQUIRED) + +After generating the slide HTML, perform ALL of these checks before converting to PDF: + +1. **Title overflow check**: For every slide, verify that the title text at its set font-size fits within the slide width (1280px) minus padding (120px total). If \`title_chars × 0.6 × font_size > 1160\`, reduce font-size. Use these max sizes: + - Short titles (1-3 words): 72px max + - Medium titles (4-6 words): 56px max + - Long titles (7+ words): 44px max + Apply \`word-wrap: break-word\` and \`overflow-wrap: break-word\` to all title elements. Never use \`white-space: nowrap\` on titles. + +2. **Content bounds check**: Verify no element extends beyond the 1280x720 slide boundary. Look for: long titles, bullet lists with 6+ items, wide tables, long labels on charts, text that wraps more lines than the available height allows. + +3. **Broken visuals check**: Confirm no \`\` tags reference external URLs. All visuals must be CSS, SVG, or emoji only. Never use external images — they will fail in PDF rendering. Use CSS shapes, gradients, SVG, or emoji for all visual elements. + +4. **Font loading check**: Verify the Google Fonts \`\` tag includes ALL font families used in the CSS. Missing fonts cause fallback rendering and broken typography. + +5. **Theme consistency check**: Confirm all slides use the same palette — no rogue colors in charts, backgrounds, or text that don't belong to the chosen theme. + +6. **Fix before proceeding**: If any check fails, fix the HTML before PDF conversion. Do not proceed with known issues. + +## PDF Export Rules + +These rules prevent rendering issues in PDF. Violating them causes overlapping rectangles and broken layouts. + +1. **No layered elements** — Never create separate elements for backgrounds or shadows. Style content elements directly. +2. **No box-shadow** — Use borders instead: \`border: 1px solid #e5e7eb\` +3. **Bullets via CSS only** — Use \`li::before\` pseudo-elements, not separate DOM elements. +4. **Content must fit** — Slides are 1280x720px with 60px padding. Safe content area is 1160x600px. Use \`overflow: hidden\`. +5. **No footers or headers** — Never add fixed/absolute-positioned footer or header elements to slides. They overlap with content in PDF rendering. If you need a slide number or title, include it as part of the normal content flow. +6. **No external images** — All visuals must be CSS, SVG, or emoji. External image URLs will render as broken white boxes in PDF. + +## Required CSS + +\`\`\`css +@page { size: 1280px 720px; margin: 0; } +html { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } +.slide { + width: 1280px; + height: 720px; + padding: 60px; + overflow: hidden; + page-break-after: always; + page-break-inside: avoid; +} +.slide:last-child { page-break-after: auto; } +\`\`\` + +## Playwright Export + +\`\`\`javascript +// save as tmp/convert.js via workspace-writeFile +const { chromium } = require('playwright'); +const path = require('path'); + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + // Replace with the actual absolute path from workspace-getRoot + await page.goto('file:///tmp/presentation.html', { waitUntil: 'networkidle' }); + await page.pdf({ + path: path.join(process.env.HOME, 'Desktop', 'presentation.pdf'), + width: '1280px', + height: '720px', + printBackground: true, + }); + await browser.close(); + console.log('Done: ~/Desktop/presentation.pdf'); +})(); +\`\`\` + +Replace \`\` with the actual absolute path returned by workspace-getRoot. + +## Available Layout Types (35 Templates) + +Use these as reference when building presentations. Pick the appropriate layout for each slide based on the content type. Mix and match for visual variety. + +### Title & Structure Slides +1. **Title Slide (Dark Gradient)** — Hero opening with gradient text and atmospheric glow +2. **Title Slide (Light Editorial)** — Clean, warm serif typography with editorial feel +3. **Section Divider** — Chapter break with oversized background number +4. **Agenda / Table of Contents** — Serif title with numbered items and descriptions +5. **Full-Bleed Cinematic** — Atmospheric background with grid texture, orbs, and bottom-aligned content + +### Content Slides +6. **Big Statement / Quote** — Full-color background with bold quote or key takeaway +7. **Big Stat Number** — Single dramatic metric with context text +8. **Bullet List (Split Panel)** — Dark sidebar title + light content area with icon bullets +9. **Numbered List** — Ordered steps in numbered cards +10. **Two Columns** — Side-by-side content cards +11. **Three Columns with Icons** — Feature cards with icon accents +12. **Image + Text** — Visual panel left, content + CTA right +13. **Image Gallery (2x2)** — Grid of captioned visual cards using CSS gradient backgrounds + +### Chart & Data Slides +14. **Bar Chart (Vertical)** — Vertical bars with gradient fills and labels +15. **Horizontal Bar Chart** — Ranked bars for lists with long labels +16. **Stacked Bar Chart** — Segmented bars showing composition/breakdown +17. **Combo Chart (Bar + Line)** — SVG bars for volume + line for growth rate +18. **Donut Chart** — CSS conic-gradient donut with legend +19. **Line Chart (SVG)** — SVG polyline with area fill and data labels +20. **KPI Dashboard** — Color-coded metric cards with change indicators +21. **Data Table** — Styled rows with colored header and status badges +22. **Feature Matrix** — Checkmark comparison table (features x tiers) + +### Diagram Slides +23. **Horizontal Timeline** — Connected milestone dots on a horizontal axis +24. **Vertical Timeline** — Left-rail progression of milestones +25. **Process Flow** — Step cards connected with arrows +26. **Funnel Diagram** — Tapered width bars showing conversion stages +27. **Pyramid Diagram** — Tiered hierarchy showing levels/priorities +28. **Cycle Diagram** — Flywheel/feedback loop with circular node arrangement +29. **Venn Diagram** — Three translucent overlapping circles +30. **2x2 Matrix** — Four color-coded quadrants with axis labels + +### Comparison Slides +31. **Comparison / Vs** — Split layout with contrasting colors for A vs B +32. **Pros & Cons** — Checkmarks vs. warnings in two columns +33. **Pricing Table** — Tiered cards with featured highlight + +### People & Closing Slides +34. **Team Grid** — Avatar circles with role descriptions +35. **Thank You / CTA** — Atmospheric closing with contact details + +### Layout Selection Heuristic + +For each slide, identify the content type and pick the matching layout: + +| Content Type | Best Layouts | +|---|---| +| Opening / hook | Title Slide, Full-Bleed Cinematic | +| Agenda / overview | Agenda/TOC | +| Key metric or stat | Big Stat Number, KPI Dashboard | +| List of points | Bullet List, Numbered List | +| Features or pillars | Three Columns, Two Columns | +| Trend over time | Line Chart, Horizontal Timeline | +| Composition / breakdown | Donut Chart, Stacked Bar, Pie | +| Ranking | Horizontal Bar Chart | +| Comparison | Vs Slide, Pros & Cons | +| Process or steps | Process Flow, Vertical Timeline | +| Hierarchy | Pyramid Diagram | +| Feedback loop | Cycle Diagram | +| Overlap / intersection | Venn Diagram | +| Prioritization | 2x2 Matrix | +| Data details | Data Table, Feature Matrix | +| Pricing | Pricing Table | +| Emotional / cinematic | Big Statement, Full-Bleed Cinematic | +| Team intro | Team Grid | +| Closing | Thank You / CTA | + +Never use the same layout for consecutive slides. Alternate between dark and light backgrounds for rhythm. + +### Design Guidelines + +- Use Google Fonts loaded via \`\` tag. Recommended pairings: + - **Primary pair**: Outfit (headings) + DM Sans (body) — works for most decks + - **Editorial pair**: Playfair Display (headings) + DM Sans (body) — for reports/proposals + - **Accent fonts**: Space Mono (overlines, labels), Crimson Pro (quotes) +- Dark slides: use subtle radial gradients for atmosphere, semi-transparent overlays for depth +- Light slides: use warm neutrals, clean borders, and ample whitespace +- Charts: use CSS (conic-gradient for donuts, inline styles for bar heights) or inline SVG for line/combo charts +- Typography hierarchy: monospace overlines for labels -> sans-serif for headings -> serif for editorial/quotes +- Cards: use \`border-radius: 12-16px\`, subtle borders (\`rgba(255,255,255,0.08)\` on dark), no box-shadow (PDF rule) +- All visuals must be CSS, SVG, or emoji — no external images + +### HTML Template Examples + + + + + + +Slide Deck Templates — The Future of AI Coworkers + + + + + + + + +
+ +
Dark Gradient Title
+
Hero opening slide with gradient text and atmospheric glow
+
+
+
Keynote 2026
+

The Future of
AI Coworkers

+
How intelligent agents are transforming collaboration, creativity, and the way teams build together.
+
+
+
+ + +
+ +
Light Editorial Title
+
Clean, warm title slide with serif typography and an editorial feel
+
+
+
Industry Report 2026
+

Working Alongside AI

+
A comprehensive look at how AI coworkers are augmenting human potential across every industry, from startups to the Fortune 500.
+
+
+
+ + +
+ +
Chapter Break
+
Dramatic section separator with oversized background number
+
+
+
01
+
+
Section One
+

The Rise of Intelligent Collaboration

+
+
+
+
+
+ + +
+ +
Big Statement Slide
+
Full-color background with a bold quote or key takeaway
+
+
+
AI coworkers don't replace human creativity — they amplify it, handling the routine so teams can focus on the extraordinary.
+
— Annual Workplace Intelligence Report, 2026
+
+
+
+ + +
+ +
Split Panel with Bullets
+
Dark sidebar with title, light content area with icon-accented bullets
+
+
+
+
+

Key Benefits of AI Coworkers

+
+
+
+
+
+

10x Faster Research

+

AI agents synthesize thousands of documents in seconds, surfacing insights that would take humans weeks.

+
+
+
+
🎯
+
+

Proactive Task Management

+

Intelligent assistants anticipate next steps, draft follow-ups, and keep projects on track automatically.

+
+
+
+
🤝
+
+

Always-On Collaboration

+

AI coworkers bridge time zones, summarize meetings, and ensure no team member is ever out of the loop.

+
+
+
+
📈
+
+

Continuous Learning

+

Each interaction makes the AI smarter — building a compounding knowledge base for your entire organization.

+
+
+
+
+
+
+ + +
+ +
Warm Two-Column Layout
+
Side-by-side content cards on a warm yellow background
+
+
+
+

Two Modes of AI Collaboration

+
Framework
+
+
+
+

🧠 Thinking Partner

+

AI coworkers serve as brainstorming partners that challenge assumptions, offer alternative perspectives, and help teams explore ideas they wouldn't have considered alone. They bring pattern recognition across vast datasets to creative problem-solving sessions.

+
+
+

⚙️ Execution Engine

+

From drafting reports to analyzing data pipelines, AI coworkers handle the heavy lifting of execution. They turn rough outlines into polished deliverables, automate repetitive workflows, and free humans to focus on strategy and relationship building.

+
+
+
+
+
+ + +
+ +
Dark Three-Column Feature Cards
+
Glassmorphic cards with icon accents on a dark background
+
+
+

Core Capabilities

+
+
+
🔍
+

Deep Research

+

Analyze millions of data points across your organization's knowledge base to surface critical insights and connections.

+
+
+
✍️
+

Content Creation

+

Draft, edit, and refine documents, presentations, and communications tailored to your brand voice and standards.

+
+
+
🔗
+

Workflow Orchestration

+

Connect tools, automate handoffs, and ensure seamless execution across your entire tech stack and team.

+
+
+
+
+
+ + +
+ +
Vertical Bar Chart
+
Clean data visualization with gradient bars on white
+
+
+

Productivity Gains by Department

+
Average hours saved per week after AI coworker deployment
+
+
+
+
18h
+
+
Engineering
+
+
+
+
15h
+
+
Marketing
+
+
+
+
22h
+
+
Sales
+
+
+
+
13h
+
+
Design
+
+
+
+
20h
+
+
Operations
+
+
+
+
16h
+
+
Finance
+
+
+
+
14h
+
+
HR
+
+
+
+
+
+ + +
+ +
Donut Chart with Legend
+
Dark split layout with donut visualization and data legend
+
+
+
+

How Teams Use AI Coworkers

+
Survey of 5,000+ professionals on their primary use cases for AI collaboration in the workplace.
+
+
Research & Analysis — 42%
+
Content Drafting — 26%
+
Code & Engineering — 17%
+
Meeting Summaries — 15%
+
+
+
+
+
+
5K+
+
respondents
+
+
+
+
+
+ + +
+ +
Trend Line Chart
+
Light green theme with SVG line chart showing growth trajectory
+
+
+

AI Coworker Adoption Rate

+
Percentage of Fortune 500 companies with deployed AI agents, 2022–2026
+ + + + + + + + + + 0% + 25% + 50% + 75% + 100% + + + + + + + + + + + + + + + + + + + 2021 + 2022 + 2023 + 2024 + 2025 + 2026 + + 10% + 19% + 35% + 60% + 84% + 99% + +
+
+
+ + +
+ +
Evolution Timeline
+
Dark purple with connected milestone dots and descriptions
+
+
+

The Evolution of AI Coworkers

+
+
+
2020
+
+
Basic Chatbots
+
Simple Q&A bots handling repetitive customer queries
+
+
+
2022
+
+
LLM Assistants
+
General-purpose AI for writing, analysis, and coding tasks
+
+
+
2024
+
+
AI Agents
+
Autonomous agents that plan, execute, and iterate on complex workflows
+
+
+
2026
+
+
AI Coworkers
+
Persistent, context-aware teammates with memory and deep integrations
+
+
+
+
+
+ + +
+ +
Light Vertical Timeline
+
Clean white layout with a vertical progression of milestones
+
+
+
Roadmap
+
+
+
Q1 2026
+

Launch AI Knowledge Graph

+

Persistent memory layer that maps relationships across all work data — emails, meetings, docs.

+
+
+
Q2 2026
+

Multi-Agent Orchestration

+

Deploy specialized agents that collaborate — research agent, writing agent, code agent — working in concert.

+
+
+
Q3 2026
+

Proactive Insights Engine

+

AI coworker surfaces insights before you ask — flagging risks, opportunities, and action items automatically.

+
+
+
Q4 2026
+

Full Workflow Autonomy

+

End-to-end autonomous task completion with human-in-the-loop oversight for critical decisions.

+
+
+
+
+
+ + +
+ +
Step-by-Step Process
+
Ocean blue gradient with connected process steps and arrows
+
+
+

How AI Coworkers Learn Your Workflow

+
+
+
01
+

Connect

+

Integrate with your tools — email, calendar, Slack, docs

+
+
+
+
02
+

Observe

+

AI maps your workflows, relationships, and patterns

+
+
+
+
03
+

Assist

+

Proactively suggests actions and drafts deliverables

+
+
+
+
04
+

Evolve

+

Gets smarter with every interaction, compounding value

+
+
+
+
+
+ + +
+ +
Metrics Dashboard
+
Dark zinc theme with color-coded metric cards
+
+
+

Impact Metrics — Q4 2026

+
+
+
Tasks Automated
+
12.4K
+
34% vs Q3
+
+
+
Hours Saved / Week
+
847
+
22% vs Q3
+
+
+
Team Satisfaction
+
94%
+
8pts vs Q3
+
+
+
ROI Multiple
+
11.2x
+
2.1x vs Q3
+
+
+
+
+
+ + +
+ +
Side-by-Side Comparison
+
Split layout with contrasting colors for before/after or A vs B
+
+
+
+

Traditional Workflow

+
    +
  • Manual research across scattered sources
  • +
  • Hours spent formatting reports and decks
  • +
  • Context lost between meetings and tools
  • +
  • Repetitive tasks drain creative energy
  • +
  • Knowledge silos across the org
  • +
+
+
VS
+
+

With AI Coworkers

+
    +
  • Instant synthesis from all data sources
  • +
  • Auto-generated first drafts in seconds
  • +
  • Persistent memory across every interaction
  • +
  • Automation frees focus for high-impact work
  • +
  • Shared intelligence for the entire team
  • +
+
+
+
+
+ + +
+ +
Tiered Pricing
+
Dark theme with featured tier highlight
+
+
+

Choose Your AI Coworker Plan

+
+
+
Starter
+
$29/mo
+
For individuals getting started
+
    +
  • 1 AI coworker agent
  • +
  • 5 tool integrations
  • +
  • 10K messages / month
  • +
  • 7-day memory window
  • +
+
+ +
+
Enterprise
+
Custom
+
For large organizations
+
    +
  • Unlimited agents
  • +
  • Custom model training
  • +
  • SSO & compliance
  • +
  • Dedicated support
  • +
  • On-premise option
  • +
+
+
+
+
+
+ + +
+ +
Team Members
+
Light layout with avatar circles and role descriptions
+
+
+

Meet Your AI Team

+
+
+
🔬
+

Research Agent

+
Deep Analysis
+
Scans thousands of sources to deliver synthesized insights in seconds
+
+
+
✏️
+

Writing Agent

+
Content Creation
+
Drafts, edits, and polishes documents in your brand voice
+
+
+
💻
+

Code Agent

+
Engineering
+
Writes, reviews, and debugs code across your entire stack
+
+
+
📊
+

Data Agent

+
Analytics
+
Transforms raw data into dashboards and actionable reports
+
+
+
+
+
+ + +
+ +
Visual Storytelling Split
+
Left visual panel with decorative elements, right content with CTA
+
+
+
+
+
+
🤖
+
+
+

Your AI Coworker Remembers Everything

+

Unlike session-based tools that forget after every chat, AI coworkers build persistent knowledge graphs from your emails, meetings, and documents — compounding intelligence over time.

+ See It In Action → +
+
+
+
+ + +
+ +
Conversion Funnel
+
Dark cosmic theme with tapered funnel stages
+
+
+
+

AI Coworker Adoption Funnel

+
From first touch to full deployment — how organizations onboard their AI teammates.
+
+
+
+ Discovery & Demo10,000 +
+
+ Free Trial6,200 +
+
+ Active Usage3,800 +
+
+ Paid Conversion2,100 +
+
+ Enterprise Deploy940 +
+
+
+
+
+ + +
+ +
Thank You & CTA
+
Atmospheric closing slide with contact details and next steps
+
+
+
🚀
+

Thank You

+
The future of work isn't about replacing humans — it's about giving every person an incredible AI teammate. Let's build it together.
+
+
📧 hello@aico.ai
+
🌐 aico.ai
+
🐦 @aico_ai
+
+
+
+
+ + +
+ +
Hero Metric
+
Single dramatic number with context — ideal for impact statements
+
+
+
+
Global AI Coworker Impact
+
4.2M
+
hours saved per day
+
Across 12,000+ companies worldwide, AI coworkers are giving teams back the equivalent of 525,000 full workdays — every single day.
+
+
+
+
+ + +
+ +
Segmented Horizontal Bars
+
Dark indigo theme with color-coded segments showing composition
+
+
+

AI Task Distribution by Department

+
Breakdown of AI coworker usage across task categories
+
+
Research
+
Drafting
+
Automation
+
Analysis
+
+
+
+
Sales
+
+
+
+
+
+
+
+
+
Marketing
+
+
+
+
+
+
+
+
+
Engineering
+
+
+
+
+
+
+
+
+
Finance
+
+
+
+
+
+
+
+
+
HR
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
Ranked Horizontal Bars
+
Warm amber theme — great for ranked lists with long labels
+
+
+

Top AI Coworker Use Cases

+
Ranked by weekly active usage across 5,000+ teams
+
+
+
Meeting summaries
+
92%
+
+
+
Email drafting
+
84%
+
+
+
Code review
+
76%
+
+
+
Data analysis
+
71%
+
+
+
Research synthesis
+
65%
+
+
+
Report generation
+
58%
+
+
+
+
+
+ + +
+ +
Styled Data Table
+
Clean white table with colored header and status badges
+
+
+

AI Coworker Platform Comparison

+
Feature and performance benchmarks across leading platforms
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlatformResponse TimeMemoryIntegrationsStatus
AiCo Pro0.8s avgPersistent140+Leader
WorkBot AI1.2s avgSession only85+Growing
TeamMind1.5s avg7-day window60+Growing
AssistIQ2.1s avgSession only35+Emerging
CoPilotX0.9s avg30-day window110+Leader
+
+
+
+ + +
+ +
Bar + Line Overlay
+
Dark theme SVG with bars for volume and line for growth rate
+
+
+

AI Coworker Revenue & Growth

+
Quarterly revenue ($M) with year-over-year growth rate
+
+
Revenue ($M)
+
YoY Growth %
+
+ + + + + + + + + + + + + + + + $12M + $19M + $31M + $48M + $72M + $105M + + + + + + + + + + 58% + 63% + 68% + 55% + 50% + 46% + + Q1 '24 + Q2 '24 + Q3 '24 + Q4 '24 + Q1 '25 + Q2 '25 + +
+
+
+ + +
+ +
Strategy Hierarchy
+
Magenta gradient with tiered pyramid showing priorities
+
+
+
+

AI Coworker Maturity Model

+
Organizations progress through five levels of AI integration, each building on the last.
+
+
+
+
Autonomy
+
Self-directed workflows
+
+
+
Proactive Insights
+
AI surfaces opportunities
+
+
+
Contextual Assistance
+
Persistent memory + deep integrations
+
+
+
Task Automation
+
Repetitive work handled by AI
+
+
+
Basic Chat
+
Simple Q&A and information retrieval
+
+
+
+
+
+ + +
+ +
Flywheel / Feedback Loop
+
Light green with circular node arrangement and center label
+
+
+

The AI Coworker Flywheel

+
+ +
+
📥
+

Ingest

+

Connects to emails, docs, meetings, and tools

+
+ +
+
🧠
+

Learn

+

Maps patterns, preferences, and relationships

+
+ +
+
+

Act

+

Automates tasks and generates deliverables

+
+ +
+
📈
+

Improve

+

Feedback refines accuracy and relevance

+
+ +
+
+
+
+ +
+
🔄
+
Compounding
Intelligence
+
+
+
+
+
+ + +
+ +
Overlapping Concepts
+
Dark slate with three translucent overlapping circles
+
+
+
+

The AI Coworker Sweet Spot

+
The most impactful AI coworkers sit at the intersection of three capabilities — understanding context, taking action, and learning continuously.
+
+
+
+ Context
Awareness
+
+
+ Autonomous
Action
+
+
+ Continuous
Learning
+
+
+
⭐ AI
Coworker
+
+
+
+
+
+ + +
+ +
Strategic Quadrant
+
Light layout with four color-coded quadrants and axis labels
+
+
+

AI Coworker Task Prioritization Matrix

+
+
+

🚀 Automate Now

+

High frequency, low complexity tasks like scheduling, data entry, meeting notes, and status updates.

+
+
+

🤝 Augment & Assist

+

High frequency, high complexity tasks like code review, research synthesis, and report drafting.

+
+
+

📋 Batch & Template

+

Low frequency, low complexity tasks like onboarding docs, expense reports, and form filling.

+
+
+

🧠 Strategic Co-Pilot

+

Low frequency, high complexity tasks like strategy planning, crisis response, and deal negotiation.

+
+
+
+ ← Low complexity + High complexity → +
+
+
+
+ + +
+ +
2×2 Visual Grid
+
Dark zinc with gradient-captioned cards — uses CSS backgrounds instead of images
+
+ +
+
+ + +
+ +
Ordered Steps
+
Ocean teal with numbered cards — simpler than full process flow
+
+
+
+

5 Rules for AI Coworker Success

+
The principles that separate teams who thrive with AI from those who struggle.
+
+
+
+
01
+
+

Start with High-Volume Tasks

+

Deploy AI where repetition is highest — email, scheduling, summaries.

+
+
+
+
02
+
+

Give Context Generously

+

The more your AI knows about your work, the better it performs.

+
+
+
+
03
+
+

Trust But Verify

+

Review AI outputs initially, then gradually increase autonomy.

+
+
+
+
04
+
+

Build Feedback Loops

+

Correct mistakes — each correction makes the AI permanently smarter.

+
+
+
+
05
+
+

Expand Gradually

+

Once one workflow succeeds, replicate the pattern across the team.

+
+
+
+
+
+
+ + +
+ +
Advantages vs. Considerations
+
Light purple with check/warning icons — honest framing of tradeoffs
+
+
+

AI Coworkers: Benefits & Considerations

+
+
+
✓ Advantages
+
    +
  • Instant access to organizational knowledge
  • +
  • 24/7 availability across time zones
  • +
  • Consistent quality on repetitive tasks
  • +
  • Scales without proportional cost increase
  • +
  • Learns and improves over time
  • +
+
+
+
⚠ Considerations
+
    +
  • Requires initial setup and training period
  • +
  • Data privacy policies must be established
  • +
  • Change management for team adoption
  • +
  • Best for structured, repeatable workflows
  • +
  • Human oversight still needed for critical decisions
  • +
+
+
+
+
+
+ + +
+ +
Checkmark Comparison Table
+
Dark theme with features × tiers showing capability coverage
+
+
+

Feature Availability by Plan

+
What's included at each tier of AI coworker deployment
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureStarterTeamEnterprise
Chat-based assistant
Persistent memory
Knowledge graph
Multi-agent orchestration
Custom model training
SSO & compliance
API access
+
+
+
+ + +
+ +
Table of Contents
+
Clean white with serif title and numbered agenda items
+
+
+
+
Presentation Outline
+

Today's Agenda

+
+
+
+
01
+
+

The Rise of AI Coworkers

+

Market landscape and driving forces

+
+
+
+
02
+
+

Core Capabilities

+

What makes an AI coworker different from a chatbot

+
+
+
+
03
+
+

Impact & Metrics

+

Real-world results from early adopters

+
+
+
+
04
+
+

Implementation Roadmap

+

How to get started in 90 days

+
+
+
+
05
+
+

Q&A and Next Steps

+

Open discussion and action items

+
+
+
+
+
+
+ + +
+ +
Atmospheric Background Slide
+
Immersive dark slide with grid texture, orbs, and bottom-aligned content
+
+
+
+
+
+
+
+
+
A New Era Begins
+

Every Knowledge Worker Deserves an AI Teammate

+

We're building toward a world where AI handles the busywork and humans do what they do best — think creatively, build relationships, and make decisions that matter.

+
+
+
+
+ + + diff --git a/apps/skills/deletion-guardrails/SKILL.md b/apps/skills/deletion-guardrails/SKILL.md new file mode 100644 index 00000000..3782701d --- /dev/null +++ b/apps/skills/deletion-guardrails/SKILL.md @@ -0,0 +1,33 @@ +--- +name: deletion-guardrails +description: >- + Following the confirmation process before removing workflows or agents and their dependencies. Use when a user asks to delete agents or workflows. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Deletion Guardrails" + author: rowboatlabs + tags: "safety, deletion, guardrails" +--- + +# Deletion Guardrails + +Load this skill when a user asks to delete agents or workflows so you follow the required confirmation steps. + +## Workflow deletion protocol +1. Read the workflow file to identify every agent it references. +2. Report those agents to the user and ask whether they should be deleted too. +3. Wait for explicit confirmation before deleting anything. +4. Only remove the workflow and/or agents the user authorizes. + +## Agent deletion protocol +1. Inspect the agent file to discover which workflows reference it. +2. List those workflows to the user and ask whether they should be updated or deleted. +3. Pause for confirmation before modifying workflows or removing the agent. +4. Perform only the deletions the user approves. + +## Safety checklist +- Never delete cascaded resources automatically. +- Keep a clear audit trail in your responses describing what was removed. +- If the user’s instructions are ambiguous, ask clarifying questions before taking action. diff --git a/apps/skills/doc-collab/SKILL.md b/apps/skills/doc-collab/SKILL.md new file mode 100644 index 00000000..2d399e37 --- /dev/null +++ b/apps/skills/doc-collab/SKILL.md @@ -0,0 +1,300 @@ +--- +name: doc-collab +description: >- + Collaborate on documents - create, edit, and refine notes and documents in the knowledge base. Use when the user wants to work on a document, edit notes, or write content. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Document Collaboration" + author: rowboatlabs + tags: "documents, editing, knowledge-base" +--- + +# Document Collaboration Skill + +You are an expert document assistant helping the user create, edit, and refine documents in their knowledge base. + +## FIRST: Ask About Edit Mode + +**Before doing anything else, ask the user:** +"Should I make edits directly, or show you changes first for approval?" + +- **Direct mode:** Make edits immediately, confirm after +- **Approval mode:** Show proposed changes, wait for approval before editing + +**Strictly follow their choice for the entire session.** Don't switch modes without asking. + +## CRITICAL: Re-read Before Every Response + +**Before every response, you MUST use workspace-readFile to re-read the current document.** The user may have edited the file manually outside of this conversation. Always work with the latest version of the file, never rely on a cached or previous version. + +## Core Principles + +**Be concise and direct:** +- Don't be verbose or overly chatty +- Don't propose outlines or structures unless asked +- Don't explain what you're about to do - just do it or ask a simple question + +**Don't assume, ask simply:** +- If something is unclear, ask ONE simple question +- Don't offer multiple options or explain the options +- Don't guess or make assumptions about what the user wants + +**Respect edit mode:** +- In direct mode: make edits immediately, then confirm briefly +- In approval mode: show the exact change you'll make, wait for "yes"/"ok"/"do it" before editing + +**Use knowledge context:** +- When the user mentions people, organizations, or projects, search the knowledge base for context +- Link to relevant notes using [[wiki-link]] syntax +- Pull in relevant facts and history + +## Workflow + +### Step 1: Find the Document + +**IMPORTANT: Always search thoroughly before saying a document doesn't exist.** + +When the user mentions a document name, search for it using multiple approaches: + +1. **Search by name pattern** (handles partial matches, different cases): +\`\`\` +workspace-glob({ pattern: "knowledge/**/*[name]*", path: "knowledge/" }) +\`\`\` + +2. **Search by content** (finds docs that mention the topic): +\`\`\` +workspace-grep({ pattern: "[name]", path: "knowledge/" }) +\`\`\` + +3. **Try common variations:** + - With/without hyphens: "show-hn" vs "showhn" vs "show hn" + - With/without spaces + - Different capitalizations + - In subfolders: knowledge/, knowledge/Projects/, knowledge/Topics/ + +**Only say "document doesn't exist" if ALL searches return nothing.** + +**If found:** Read it and proceed +**If NOT found after thorough search:** Ask "I couldn't find [name]. Shall I create it?" + +**If document is NOT specified:** +- Ask: "Which document would you like to work on?" + +**Creating new documents:** +1. Ask simply: "Shall I create [filename]?" (don't ask about location - default to \`knowledge/\` root) +2. Create it with just a title - don't pre-populate with structure or outlines +3. Ask: "What would you like in this?" + +\`\`\` +workspace-createFile({ + path: "knowledge/[Document Name].md", + content: "# [Document Title]\n\n" +}) +\`\`\` + +**WRONG approach:** +- "Should this be in Projects/ or Topics/?" - don't ask, just use root +- "Here's a proposed outline..." - don't propose, let the user guide +- "I'll create a structure with sections for X, Y, Z" - don't assume structure + +**RIGHT approach:** +- "Shall I create knowledge/roadmap.md?" +- *creates file with just the title* +- "Created. What would you like in this?" + +### Step 2: Understand the Request + +**IMPORTANT: Never make unsolicited edits.** If the user hasn't specified what they want to do with the document, ask them: "What would you like to change?" Do NOT proactively improve, restructure, or suggest edits unless the user has explicitly asked for changes. + +**Types of requests:** + +1. **Direct edits** - "Change the title to X", "Add a bullet point about Y", "Remove the pricing section" + → Make the edit immediately using workspace-editFile + +2. **Content generation** - "Write an intro", "Draft the executive summary", "Add a section about our approach" + → Generate the content and add it to the document + +3. **Review/feedback** - "What do you think?", "Is this clear?", "Any suggestions?" + → Read the document and provide thoughtful feedback + +4. **Research-backed additions** - "Add context about [Person]", "Include what we discussed with [Company]" + → Search knowledge base first, then add relevant context + +5. **No clear request** - User just says "let's work on X" with no specific ask + → Read the document, then ask: "What would you like to change?" + +### Step 3: Execute Changes + +**For edits, use workspace-editFile:** +\`\`\` +workspace-editFile({ + path: "knowledge/[path].md", + old_string: "[exact text to replace]", + new_string: "[new text]" +}) +\`\`\` + +**For additions at the end:** +\`\`\` +workspace-editFile({ + path: "knowledge/[path].md", + old_string: "[last line or section]", + new_string: "[last line or section]\n\n[new content]" +}) +\`\`\` + +**For new sections:** +Find the right place in the document structure and insert the new section. + +### Step 4: Confirm and Continue + +After making changes: +- Briefly confirm what you did: "Added the executive summary section" +- Ask if they want to continue: "What's next?" or "Anything else to adjust?" +- Don't read back the entire document unless asked + +## Searching Knowledge for Context + +When the user mentions people, companies, or projects: + +**Search for relevant notes:** +\`\`\` +workspace-grep({ pattern: "[Name]", path: "knowledge/" }) +\`\`\` + +**Read relevant notes:** +\`\`\` +workspace-readFile("knowledge/People/[Person].md") +workspace-readFile("knowledge/Organizations/[Company].md") +workspace-readFile("knowledge/Projects/[Project].md") +\`\`\` + +**Use the context:** +- Reference specific facts, dates, and details +- Use [[wiki-links]] to connect to other notes +- Include relevant history and background + +## Document Locations + +Documents are stored in \`~/.rowboat/knowledge/\` with subfolders: +- \`People/\` - Notes about individuals +- \`Organizations/\` - Notes about companies, teams +- \`Projects/\` - Project documentation +- \`Topics/\` - Subject matter notes +- Root level for general documents + +## Rich Blocks + +Notes support rich block types beyond standard Markdown. Blocks are fenced code blocks with a language identifier and a JSON body. Use these when the user asks for visual content like charts, tables, images, or embeds. + +### Image Block +Displays an image with optional alt text and caption. +\`\`\`image +{"src": "https://example.com/photo.png", "alt": "Description", "caption": "Optional caption"} +\`\`\` +- \`src\` (required): URL or relative path to the image +- \`alt\` (optional): Alt text +- \`caption\` (optional): Caption displayed below the image + +### Embed Block +Embeds external content (YouTube videos, Figma designs, or generic links). +\`\`\`embed +{"provider": "youtube", "url": "https://www.youtube.com/watch?v=VIDEO_ID", "caption": "Video title"} +\`\`\` +- \`provider\` (required): \`"youtube"\`, \`"figma"\`, or \`"generic"\` +- \`url\` (required): Full URL to the content +- \`caption\` (optional): Caption displayed below the embed +- YouTube and Figma render as iframes; generic shows a link card + +### Chart Block +Renders a chart from inline data. +\`\`\`chart +{"chart": "bar", "title": "Q1 Revenue", "data": [{"month": "Jan", "revenue": 50000}, {"month": "Feb", "revenue": 62000}], "x": "month", "y": "revenue"} +\`\`\` +- \`chart\` (required): \`"line"\`, \`"bar"\`, or \`"pie"\` +- \`title\` (optional): Chart title +- \`data\` (optional): Array of objects with the data points +- \`source\` (optional): Relative path to a JSON file containing the data array (alternative to inline data) +- \`x\` (required): Key name for the x-axis / label field +- \`y\` (required): Key name for the y-axis / value field + +### Table Block +Renders a styled table from structured data. +\`\`\`table +{"title": "Team", "columns": ["name", "role"], "data": [{"name": "Alice", "role": "Eng"}, {"name": "Bob", "role": "Design"}]} +\`\`\` +- \`columns\` (required): Array of column names (determines display order) +- \`data\` (required): Array of row objects +- \`title\` (optional): Table title + +### Block Guidelines +- The JSON must be valid and on a single line (no pretty-printing) +- Insert blocks using \`workspace-editFile\` just like any other content +- When the user asks for a chart, table, or embed — use blocks rather than plain Markdown tables or image links +- When editing a note that already contains blocks, preserve them unless the user asks to change them + +## Best Practices + +**Writing style:** +- Match the user's tone and style in the document +- Be concise but complete +- Use markdown formatting (headers, bullets, bold, etc.) + +**Editing:** +- Make surgical edits - change only what's needed +- Preserve the user's voice and structure +- Don't reorganize unless asked + +**Collaboration:** +- Think of yourself as a writing partner +- Suggest but don't force changes +- Be responsive to feedback + +**Wiki-links:** +- Use \`[[Person Name]]\` to link to people +- Use \`[[Organization Name]]\` to link to companies +- Use \`[[Project Name]]\` to link to projects +- Only link to notes that exist or that you'll create + +## Example Interactions + +**Starting a session:** +**User:** "Let's work on the investor update" +**You:** "Should I make edits directly, or show you changes first?" +**User:** "directly is fine" +**You:** *Search for it, read it* +"Found knowledge/Investor Update Q1.md. What would you like to change?" + +**Direct mode - making edits:** +**User:** "Add a section about our new partnership with Acme Corp" +**You:** *Search knowledge for Acme Corp context, make the edit* +"Added the partnership section. Anything else?" + +**Approval mode - showing changes first:** +**User:** "Add a section about Acme Corp" +**You:** "I'll add this after the Overview section: +\`\`\` +## Partnership with Acme Corp +[content based on knowledge...] +\`\`\` +Ok to add?" +**User:** "yes" +**You:** *Makes the edit* +"Done. What's next?" + +**Creating a new doc:** +**User:** "Create a doc for the roadmap" +**You:** "Shall I create knowledge/roadmap.md?" +**User:** "yes" +**You:** *Creates file with just title* +"Created. What would you like in this?" + +**WRONG examples - don't do this:** +- "Nice, new doc time! Quick clarifier: should this be standalone or in Projects/?" ❌ +- "Here's a proposed outline for the doc..." ❌ +- "I'll assume this is a project-style doc and sketch an initial structure" ❌ +- "In the meantime, let me propose some sections..." ❌ +- Switching from approval mode to direct mode without asking ❌ +- In approval mode: making edits without showing the change first ❌ diff --git a/apps/skills/draft-emails/SKILL.md b/apps/skills/draft-emails/SKILL.md new file mode 100644 index 00000000..787c52d3 --- /dev/null +++ b/apps/skills/draft-emails/SKILL.md @@ -0,0 +1,261 @@ +--- +name: draft-emails +description: >- + Process incoming emails and create draft responses using calendar and knowledge base for context. Use when the user wants to reply to emails or draft email responses. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.1.0" + title: "Draft Emails" + author: rowboatlabs + tags: "email, productivity" +--- + +# Email Draft Skill + +You are helping the user draft email responses. Use their calendar and knowledge base for context. + +## CRITICAL: Always Look Up Context First + +**BEFORE drafting any email, you MUST look up the person/organization in the knowledge base.** + +**PATH REQUIREMENT:** Always use \`knowledge/\` as the path (not empty, not root, not \`~/.rowboat\`). +- **WRONG:** \`path: ""\` or \`path: "."\` +- **CORRECT:** \`path: "knowledge/"\` + +When the user says "draft an email to Monica" or mentions ANY person, organization, project, or topic: + +1. **STOP** - Do not draft anything yet +2. **SEARCH** - Look them up in the knowledge base (path MUST be \`knowledge/\`): + \`\`\` + workspace-grep({ pattern: "Monica", path: "knowledge/" }) + \`\`\` +3. **READ** - Read their note to understand who they are: + \`\`\` + workspace-readFile("knowledge/People/Monica Smith.md") + \`\`\` +4. **UNDERSTAND** - Extract their role, organization, relationship history, past interactions, open items +5. **THEN DRAFT** - Only now draft the email, using this context + +**DO NOT** skip this step. **DO NOT** provide generic templates. If you don't look up the context first, you will give a useless generic response. + +## Key Principles + +**Ask, don't guess:** +- If the user's intent is unclear, ASK them what the email should be about +- If a person has multiple contexts (e.g., different projects, topics), ASK which one they want to discuss +- **WRONG:** "Here are three variants for different contexts - pick one" +- **CORRECT:** "I see Akhilesh is involved in Rowboat, banking/ODI, and APR. Which topic would you like to discuss in this email?" + +**Be decisive, not generic:** +- Once you know the context, draft ONE email - no multiple versions or options +- Do NOT provide generic templates - every draft should be personalized based on knowledge base context +- Infer the right tone, content, and approach from the context you gather +- Do NOT hedge with "here are a few options" or "you could say X or Y" - either ask for clarification OR make a decision and draft ONE email + +## State Management + +All state is stored in \`pre-built/email-draft/\`: + +- \`state.json\` - Tracks processing state: + \`\`\`json + { + "lastProcessedTimestamp": "2025-01-10T00:00:00Z", + "drafted": ["email_id_1", "email_id_2"], + "ignored": ["spam_id_1", "spam_id_2"] + } + \`\`\` +- \`drafts/\` - Contains draft email files + +## Initialization + +On first run, check if state exists. If not, create it: + +1. Check if \`pre-built/email-draft/state.json\` exists +2. If not, create \`pre-built/email-draft/\` and \`pre-built/email-draft/drafts/\` +3. Initialize \`state.json\` with empty arrays and a timestamp of "1970-01-01T00:00:00Z" + +## Processing Flow + +### Step 1: Load State + +Read \`pre-built/email-draft/state.json\` to get: +- \`lastProcessedTimestamp\` - Only process emails newer than this +- \`drafted\` - List of email IDs already drafted (skip these) +- \`ignored\` - List of email IDs marked as ignored (skip these) + +### Step 2: Scan for New Emails + +List emails in \`gmail_sync/\` folder. + +For each email file: +1. Extract the email ID from filename (e.g., \`19048cf9c0317981.md\` -> \`19048cf9c0317981\`) +2. Skip if ID is in \`drafted\` or \`ignored\` lists +3. Read the email content + +### Step 3: Parse Email + +Each email file contains: +\`\`\`markdown +# Subject Line + +**Thread ID:** +**Message Count:** + +--- + +### From: Name +**Date:** + + +\`\`\` + +Extract: +- Thread ID (this is the email ID) +- From (sender name and email) +- Date +- Subject (from the # heading) +- Body content +- Message count (to understand if it's a thread) + +### Step 4: Classify Email + +Determine the email type and action: + +**IGNORE these (add to \`ignored\` list):** +- Newsletters (unsubscribe links, "View in browser", bulk sender indicators) +- Marketing emails (promotional language, no-reply senders) +- Automated notifications (GitHub, Jira, Slack, shipping updates) +- Spam or cold outreach that's clearly irrelevant +- Emails where you (the user) are the sender and it's outbound with no reply + +**DRAFT response for:** +- Meeting requests or scheduling emails +- Personal emails from known contacts +- Business inquiries that seem legitimate +- Follow-ups on existing conversations +- Emails requesting information or action + +### Step 5: Gather Context + +Before drafting, gather relevant context. **Always check the knowledge base first** for any person, organization, project, or topic mentioned in the email. + +**Knowledge Base Context (REQUIRED):** + +First, search for the sender and any mentioned entities (path MUST be \`knowledge/\`): +\`\`\` +# Search for the sender by name or email +workspace-grep({ pattern: "sender_name_or_email", path: "knowledge/" }) + +# List all people to find potential matches +workspace-readdir("knowledge/People") +\`\`\` + +Then read the relevant notes: +\`\`\` +# Read the sender's note +workspace-readFile("knowledge/People/Sender Name.md") + +# Read their organization's note +workspace-readFile("knowledge/Organizations/Company Name.md") +\`\`\` + +Extract from these notes: +- Their role, title, and organization +- History of past interactions and meetings +- Commitments made (by them or to them) +- Open items and pending actions +- Relationship context and rapport + +Use this context to provide informed, personalized responses that demonstrate you remember past interactions. + +**Calendar Context** (for scheduling emails): +- Read calendar events from \`calendar_sync/\` folder +- Look for events in the relevant time period +- Check for conflicts, availability + +### Step 6: Create Draft + +For emails that need a response, create a draft file in \`pre-built/email-draft/drafts/\`: + +**Filename:** \`{email_id}_draft.md\` + +**Content format:** +\`\`\`markdown +# Draft Response + +**Original Email ID:** {email_id} +**Original Subject:** {subject} +**From:** {sender} +**Date Processed:** {current_date} + +--- + +## Context Used + +- Calendar: {relevant calendar info or "N/A"} +- Memory: {relevant notes or "N/A"} + +--- + +## Draft Response + +Subject: Re: {original_subject} + +{draft email body} + +--- + +## Notes + +{any notes about why this response was crafted this way} +\`\`\` + +**Drafting Guidelines:** +- Draft ONE email - do not offer multiple versions or options unless explicitly asked +- Be concise and professional +- For scheduling: propose specific times based on calendar availability +- For inquiries: answer directly or indicate what info is needed +- Reference any relevant context from memory naturally - show you remember past interactions +- Match the tone of the incoming email +- If it's a thread with multiple messages, read the full context +- Do NOT use generic templates or placeholder language - personalize based on knowledge base +- If you're unsure about the user's intent, ask a clarifying question first + +### Step 7: Update State + +After processing each email: +1. Add the email ID to either \`drafted\` or \`ignored\` list +2. Update \`lastProcessedTimestamp\` to the current time +3. Write updated state to \`pre-built/email-draft/state.json\` + +## Output + +After processing all new emails, provide a summary: + +\`\`\` +## Processing Summary + +**Emails Scanned:** X +**Drafts Created:** Y +**Ignored:** Z + +### Drafts Created: +- {email_id}: {subject} - {brief reason} + +### Ignored: +- {email_id}: {subject} - {reason for ignoring} +\`\`\` + +## Error Handling + +- If an email file is malformed, log it and continue +- If calendar/notes folders don't exist, proceed without that context +- Always save state after each email to avoid reprocessing on failure + +## Important Notes + +- Never actually send emails - only create drafts +- The user will review and send drafts manually +- Be conservative with ignore - when in doubt, create a draft +- For ambiguous emails, create a draft with a note explaining the ambiguity diff --git a/apps/skills/mcp-integration/SKILL.md b/apps/skills/mcp-integration/SKILL.md new file mode 100644 index 00000000..b75628dc --- /dev/null +++ b/apps/skills/mcp-integration/SKILL.md @@ -0,0 +1,443 @@ +--- +name: mcp-integration +description: >- + Discovering, executing, and integrating MCP tools. Use when checking external capabilities, adding MCP servers, or executing MCP tools on behalf of users. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "MCP Integration Guidance" + author: rowboatlabs + tags: "mcp, integrations, tools" +--- + +# MCP Integration Guidance + +**Load this skill proactively** when a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, time/date, etc.). This skill provides complete guidance on discovering and executing MCP tools. + +## CRITICAL: Always Check MCP Tools First + +**IMPORTANT**: When a user asks for ANY task that might require external capabilities (web search, API calls, data fetching, etc.), ALWAYS: + +1. **First check**: Call \`listMcpServers\` to see what's available +2. **Then list tools**: Call \`listMcpTools\` on relevant servers +3. **Execute if possible**: Use \`executeMcpTool\` if a tool matches the need +4. **Only then decline**: If no MCP tool can help, explain what's not possible + +**DO NOT** immediately say "I can't do that" or "I don't have internet access" without checking MCP tools first! + +### Common User Requests and MCP Tools + +| User Request | Check For | Likely Tool | +|--------------|-----------|-------------| +| "Search the web/internet" | firecrawl, composio, fetch | \`firecrawl_search\`, \`COMPOSIO_SEARCH_WEB\` | +| "Scrape this website" | firecrawl | \`firecrawl_scrape\` | +| "Read/write files" | filesystem | \`read_file\`, \`write_file\` | +| "Get current time/date" | time | \`get_current_time\` | +| "Make HTTP request" | fetch | \`fetch\`, \`post\` | +| "GitHub operations" | github | \`create_issue\`, \`search_repos\` | +| "Generate audio/speech" | elevenLabs | \`text_to_speech\` | +| "Tweet/social media" | twitter, composio | Various social tools | + +## Key concepts +- MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`. +- Agents reference MCP tools through the \`"tools"\` block by specifying \`type\`, \`name\`, \`description\`, \`mcpServerName\`, and a full \`inputSchema\`. +- Tool schemas can include optional property descriptions; only include \`"required"\` when parameters are mandatory. + +## CRITICAL: Adding MCP Servers + +**ALWAYS use the \`addMcpServer\` builtin tool** to add or update MCP server configurations. This tool validates the configuration before saving and prevents startup errors. + +**NEVER manually create or edit \`config/mcp.json\`** using \`workspace-writeFile\` for MCP servers—this bypasses validation and will cause errors. + +### MCP Server Configuration Schema + +There are TWO types of MCP servers: + +#### 1. STDIO (Command-based) Servers +For servers that run as local processes (Node.js, Python, etc.): + +**Required fields:** +- \`command\`: string (e.g., "npx", "node", "python", "uvx") + +**Optional fields:** +- \`args\`: array of strings (command arguments) +- \`env\`: object with string key-value pairs (environment variables) +- \`type\`: "stdio" (optional, inferred from presence of \`command\`) + +**Schema:** +\`\`\`json +{ + "type": "stdio", + "command": "string (REQUIRED)", + "args": ["string", "..."], + "env": { + "KEY": "value" + } +} +\`\`\` + +**Valid STDIO examples:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/data"] +} +\`\`\` + +\`\`\`json +{ + "command": "python", + "args": ["-m", "mcp_server_git"], + "env": { + "GIT_REPO_PATH": "/path/to/repo" + } +} +\`\`\` + +\`\`\`json +{ + "command": "uvx", + "args": ["mcp-server-fetch"] +} +\`\`\` + +#### 2. HTTP/SSE Servers +For servers that expose HTTP or Server-Sent Events endpoints: + +**Required fields:** +- \`url\`: string (complete URL including protocol and path) + +**Optional fields:** +- \`headers\`: object with string key-value pairs (HTTP headers) +- \`type\`: "http" (optional, inferred from presence of \`url\`) + +**Schema:** +\`\`\`json +{ + "type": "http", + "url": "string (REQUIRED)", + "headers": { + "Authorization": "Bearer token", + "Custom-Header": "value" + } +} +\`\`\` + +**Valid HTTP examples:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +\`\`\`json +{ + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer sk-1234567890" + } +} +\`\`\` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing required field:** +\`\`\`json +{ + "args": ["some-arg"] +} +\`\`\` +Error: Missing \`command\` for stdio OR \`url\` for http + +❌ **WRONG - Empty object:** +\`\`\`json +{} +\`\`\` +Error: Must have either \`command\` (stdio) or \`url\` (http) + +❌ **WRONG - Mixed types:** +\`\`\`json +{ + "command": "npx", + "url": "http://localhost:3000" +} +\`\`\` +Error: Cannot have both \`command\` and \`url\` + +✅ **CORRECT - Minimal stdio:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-time"] +} +\`\`\` + +✅ **CORRECT - Minimal http:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +### Using addMcpServer Tool + +**Example 1: Add stdio server** +\`\`\`json +{ + "serverName": "filesystem", + "serverType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/data"] +} +\`\`\` + +**Example 2: Add HTTP server** +\`\`\`json +{ + "serverName": "custom-api", + "serverType": "http", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token123" + } +} +\`\`\` + +**Example 3: Add Python MCP server** +\`\`\`json +{ + "serverName": "github", + "serverType": "stdio", + "command": "python", + "args": ["-m", "mcp_server_github"], + "env": { + "GITHUB_TOKEN": "ghp_xxxxx" + } +} +\`\`\` + +## Operator actions +1. Use \`listMcpServers\` to enumerate configured servers. +2. Use \`addMcpServer\` to add or update MCP server configurations (with validation). +3. Use \`listMcpTools\` for a server to understand the available operations and schemas. +4. Use \`executeMcpTool\` to run MCP tools directly on behalf of the user. +5. Explain which MCP tools match the user's needs before editing agent definitions. +6. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. + +## Executing MCP Tools Directly (Copilot) + +As the copilot, you can execute MCP tools directly on behalf of the user using the \`executeMcpTool\` builtin. This allows you to use MCP tools without creating an agent. + +### When to Execute MCP Tools Directly +- User asks you to perform a task that an MCP tool can handle (web search, file operations, API calls, etc.) +- User wants immediate results from an MCP tool without setting up an agent +- You need to test or demonstrate an MCP tool's functionality +- You're helping the user accomplish a one-time task + +### Workflow for Executing MCP Tools +1. **Discover available servers**: Use \`listMcpServers\` to see what MCP servers are configured +2. **List tools from a server**: Use \`listMcpTools\` with the server name to see available tools and their schemas +3. **CAREFULLY EXAMINE THE SCHEMA**: Look at the \`inputSchema\` to understand exactly what parameters are required +4. **Execute the tool**: Use \`executeMcpTool\` with the server name, tool name, and required arguments (matching the schema exactly) +5. **Return results**: Present the results to the user in a helpful format + +### CRITICAL: Schema Matching + +**ALWAYS** examine the \`inputSchema\` from \`listMcpTools\` before calling \`executeMcpTool\`. + +The schema tells you: +- What parameters are required (check the \`"required"\` array) +- What type each parameter should be (string, number, boolean, object, array) +- Parameter descriptions and examples + +**Example schema from listMcpTools:** +\`\`\`json +{ + "name": "COMPOSIO_SEARCH_WEB", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query" + }, + "limit": { + "type": "number", + "description": "Number of results" + } + }, + "required": ["query"] + } +} +\`\`\` + +**Correct executeMcpTool call:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB", + "arguments": { + "query": "elon musk latest news" + } +} +\`\`\` + +**WRONG - Missing arguments:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB" +} +\`\`\` + +**WRONG - Wrong parameter name:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB", + "arguments": { + "search": "elon musk" // Wrong! Should be "query" + } +} +\`\`\` + +### Example: Using Firecrawl to Search the Web + +**Step 1: List servers** +\`\`\`json +// Call: listMcpServers +// Response: { "servers": [{"name": "firecrawl", "type": "stdio", ...}] } +\`\`\` + +**Step 2: List tools** +\`\`\`json +// Call: listMcpTools with serverName: "firecrawl" +// Response: { "tools": [{"name": "firecrawl_search", "description": "Search the web", "inputSchema": {...}}] } +\`\`\` + +**Step 3: Execute the tool** +\`\`\`json +{ + "serverName": "firecrawl", + "toolName": "firecrawl_search", + "arguments": { + "query": "latest AI news", + "limit": 5 + } +} +\`\`\` + +### Example: Using Filesystem Tool + +**Execute a filesystem read operation:** +\`\`\`json +{ + "serverName": "filesystem", + "toolName": "read_file", + "arguments": { + "path": "/path/to/file.txt" + } +} +\`\`\` + +### Tips for Executing MCP Tools +- Always check the \`inputSchema\` from \`listMcpTools\` to know what arguments are required +- Match argument types exactly (string, number, boolean, object, array) +- Provide helpful context to the user about what the tool is doing +- Handle errors gracefully and suggest alternatives if a tool fails +- For complex tasks, consider creating an agent instead of one-off tool calls + +### Discovery Pattern (Recommended) + +When a user asks for something that might be accomplished with an MCP tool: + +1. **Identify the need**: "You want to search the web? Let me check what MCP tools are available..." +2. **List servers**: Call \`listMcpServers\` +3. **Check for relevant tools**: If you find a relevant server (e.g., "firecrawl" for web search), call \`listMcpTools\` +4. **Execute the tool**: Once you find the right tool and understand its schema, call \`executeMcpTool\` +5. **Present results**: Format and explain the results to the user + +### Common MCP Servers and Their Tools + +Based on typical configurations, you might find: +- **firecrawl**: Web scraping, search, crawling (\`firecrawl_search\`, \`firecrawl_scrape\`, \`firecrawl_crawl\`) +- **filesystem**: File operations (\`read_file\`, \`write_file\`, \`list_directory\`) +- **github**: GitHub operations (\`create_issue\`, \`create_pr\`, \`search_repositories\`) +- **fetch**: HTTP requests (\`fetch\`, \`post\`) +- **time**: Time/date operations (\`get_current_time\`, \`convert_timezone\`) + +Always use \`listMcpServers\` and \`listMcpTools\` to discover what's actually available rather than assuming. + +## Adding MCP Tools to Agents + +Once an MCP server is configured, add its tools to agent definitions (Markdown files with YAML frontmatter): + +### MCP Tool Format in Agent (YAML frontmatter) +\`\`\`yaml +tools: + descriptive_key: + type: mcp + name: actual_tool_name_from_server + description: What the tool does + mcpServerName: server_name_from_config + inputSchema: + type: object + properties: + param1: + type: string + description: What param1 means + required: + - param1 +\`\`\` + +### Tool Schema Rules +- Use \`listMcpTools\` to get the exact \`inputSchema\` from the server +- Copy the schema exactly as provided by the MCP server +- Only include \`required\` array if parameters are truly mandatory +- Add descriptions to help the agent understand parameter usage + +### Example snippets to reference +- Firecrawl search (required param): +\`\`\`yaml +tools: + search: + type: mcp + name: firecrawl_search + description: Search the web + mcpServerName: firecrawl + inputSchema: + type: object + properties: + query: + type: string + description: Search query + limit: + type: number + description: Number of results + required: + - query +\`\`\` + +- ElevenLabs text-to-speech (no required array): +\`\`\`yaml +tools: + text_to_speech: + type: mcp + name: text_to_speech + description: Generate audio from text + mcpServerName: elevenLabs + inputSchema: + type: object + properties: + text: + type: string +\`\`\` + + +## Safety reminders +- ALWAYS use \`addMcpServer\` to configure MCP servers—never manually edit config files +- Only recommend MCP tools that are actually configured (use \`listMcpServers\` first) +- Clarify any missing details (required parameters, server names) before modifying files +- Test server connection with \`listMcpTools\` after adding a new server +- Invalid MCP configs prevent agents from starting—validation is critical diff --git a/apps/skills/meeting-prep/SKILL.md b/apps/skills/meeting-prep/SKILL.md new file mode 100644 index 00000000..6809e6d4 --- /dev/null +++ b/apps/skills/meeting-prep/SKILL.md @@ -0,0 +1,174 @@ +--- +name: meeting-prep +description: >- + Prepare for meetings by gathering context about attendees from the knowledge base. Use when the user wants to prep for a meeting or create a meeting brief. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Meeting Prep" + author: rowboatlabs + tags: "meetings, calendar, preparation" +--- + +# Meeting Prep Skill + +You are helping the user prepare for meetings by gathering context from their knowledge base and calendar. + +## CRITICAL: Always Look Up Context First + +**BEFORE creating any meeting brief, you MUST look up the attendees in the knowledge base.** + +**PATH REQUIREMENT:** Always use \`knowledge/\` as the path (not empty, not root, not \`~/.rowboat\`). +- **WRONG:** \`path: ""\` or \`path: "."\` +- **CORRECT:** \`path: "knowledge/"\` + +When the user asks to prep for a meeting or mentions attendees: + +1. **STOP** - Do not create a generic brief +2. **SEARCH** - Look up each attendee in the knowledge base: + \`\`\` + workspace-grep({ pattern: "Attendee Name", path: "knowledge/" }) + \`\`\` +3. **READ** - Read their notes to understand who they are: + \`\`\` + workspace-readFile("knowledge/People/Attendee Name.md") + workspace-readFile("knowledge/Organizations/Their Company.md") + \`\`\` +4. **UNDERSTAND** - Extract their role, organization, relationship history, past interactions, open items +5. **THEN BRIEF** - Only now create the meeting brief, using this context + +**DO NOT** skip this step. **DO NOT** provide generic briefs. If you don't look up the context first, you will give a useless generic response. + +## Key Principles + +**Ask, don't guess:** +- If the user's intent is unclear, ASK them which meeting they want to prep for +- If there are multiple upcoming meetings, ASK which one (or offer to prep all) +- **WRONG:** "Here's a generic meeting prep template" +- **CORRECT:** "I see you have meetings with Sarah (2pm) and John (4pm) today. Which one would you like me to prep?" + +**Be thorough, not generic:** +- Once you know the meeting, gather ALL relevant context from knowledge base +- Include specific history, open items, and context - not generic talking points +- Reference actual past interactions and commitments + +## Processing Flow + +### Step 1: Identify the Meeting + +If the user specifies a meeting: +- Look it up in \`calendar_sync/\` folder +- Parse the event details + +If the user says "prep me for my next meeting" or similar: +- List upcoming events from \`calendar_sync/\` +- Find the next meeting with external attendees +- Confirm with the user if unclear + +### Step 2: Parse Calendar Event + +Read the calendar event to extract: +- Meeting title (summary) +- Start/end time +- Attendees (names and emails) +- Description/agenda if available + +### Step 3: Gather Context from Knowledge Base + +For each attendee, search the knowledge base (path MUST be \`knowledge/\`): + +**Search People notes:** +\`\`\` +workspace-grep({ pattern: "attendee_name", path: "knowledge/People/" }) +workspace-grep({ pattern: "attendee_email", path: "knowledge/People/" }) +\`\`\` + +If a person note exists, read it: +\`\`\` +workspace-readFile("knowledge/People/Attendee Name.md") +\`\`\` + +Extract: +- Their role/title +- Company/organization +- Key facts about them +- Previous interactions +- Open items + +**Search Organization notes:** +\`\`\` +workspace-grep({ pattern: "company_name", path: "knowledge/Organizations/" }) +\`\`\` + +**Search Projects:** +\`\`\` +workspace-grep({ pattern: "attendee_name", path: "knowledge/Projects/" }) +workspace-grep({ pattern: "company_name", path: "knowledge/Projects/" }) +\`\`\` + +### Step 4: Create Meeting Brief + +Create a brief with this format: + +\`\`\`markdown +📋 +Meeting Brief: {Attendee Name} +{Time} today · {Company} + +About {First Name} +{Role at company}. {Key background - 1-2 sentences}. {What they care about or focus on}. + +Your History +- {Date}: {Brief description of interaction/outcome} +- {Date}: {Brief description} +- {Date}: {Brief description} + +Open Items +- {Action item} (they asked {date}) +- {Action item} + +Suggested Talking Points +- {Concrete suggestion based on history} +- {Reference relevant entities with [[wiki-links]]} +\`\`\` + +**Example:** +\`\`\`markdown +📋 +Meeting Brief: Sarah Chen +2:00 PM today · Horizon Ventures + +About Sarah +Partner at Horizon Ventures. Led investments in WorkOS and Segment. Very focused on unit economics. + +Your History +- Jan 15: Partner meeting — positive reception +- Jan 12: Sent updated deck with cohort analysis +- Jan 8: First pitch — she loved the 125% NRR + +Open Items +- Send updated financial model (she asked Jan 15) +- Discuss term sheet timeline + +Suggested Talking Points +- Address her question about CAC by channel +- Mention [[TechFlow]] expansion closed ($120K ARR) +\`\`\` + +**Briefing Guidelines:** +- Use \`[[Name]]\` wiki-link syntax for cross-references to people, projects, orgs +- Keep "About" section concise - 2-3 sentences max +- History should be reverse chronological (most recent first) +- Limit to 3-5 most relevant history items +- Open items should be actionable and specific +- Talking points should be concrete, not generic +- If no notes exist for a person, mention that and offer to create one + +## Important Notes + +- Only prep for meetings with external attendees +- Skip internal calendar blocks (DND, Focus Time, Lunch, etc.) +- For meetings with multiple attendees, create sections for each key person +- Prioritize recent interactions (last 30 days) in the history section +- If an attendee has no notes, suggest what you'd want to capture about them diff --git a/apps/skills/organize-files/SKILL.md b/apps/skills/organize-files/SKILL.md new file mode 100644 index 00000000..39017460 --- /dev/null +++ b/apps/skills/organize-files/SKILL.md @@ -0,0 +1,189 @@ +--- +name: organize-files +description: >- + Find, organize, and tidy up files on the user's machine. Move files to folders, clean up Desktop/Downloads, locate specific files. Use when the user wants to find or organize files. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Organize Files" + author: rowboatlabs + tags: "files, organization, cleanup" +--- + +# Organize Files Skill + +You are helping the user organize, tidy up, and find files on their local machine. + +## Core Capabilities + +1. **Find files** - Locate files by name, type, or content +2. **Organize files** - Move files into logical folders +3. **Tidy up** - Clean up cluttered directories (Desktop, Downloads, etc.) +4. **Create structure** - Set up folder hierarchies for projects + +## Key Principles + +**Always preview before acting:** +- Show the user what files will be affected BEFORE moving/deleting +- List the proposed changes and ask for confirmation +- **WRONG:** Immediately run \`mv\` commands without showing what will move +- **CORRECT:** "I found 23 screenshots on your Desktop. Here's the plan: [list]. Should I proceed?" + +**Be conservative with destructive operations:** +- Never delete files without explicit confirmation +- Prefer moving to a "to-review" folder over deleting +- When in doubt, ask + +**Handle paths safely:** +- Always quote paths to handle spaces: \`"$HOME/My Documents"\` +- Expand ~ to $HOME in commands +- Use absolute paths when possible + +## Finding Files + +**By name pattern:** +\`\`\`bash +# Find all PDFs in Downloads +find ~/Downloads -name "*.pdf" -type f + +# Find files containing "AI" in the name +find ~/Downloads -iname "*AI*" -type f + +# Find screenshots (common naming patterns) +find ~/Desktop -name "Screenshot*" -o -name "Screen Shot*" +\`\`\` + +**By type:** +\`\`\`bash +# Images +find ~/Desktop -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -o -name "*.webp" \) + +# Documents +find ~/Desktop -type f \( -name "*.pdf" -o -name "*.doc" -o -name "*.docx" -o -name "*.txt" \) + +# Videos +find ~/Desktop -type f \( -name "*.mp4" -o -name "*.mov" -o -name "*.avi" -o -name "*.mkv" \) +\`\`\` + +**By date:** +\`\`\`bash +# Files modified in last 7 days +find ~/Downloads -type f -mtime -7 + +# Files older than 30 days +find ~/Downloads -type f -mtime +30 +\`\`\` + +**By content (for text/PDF):** +\`\`\`bash +# Search inside files for text +grep -r "search term" ~/Documents --include="*.txt" --include="*.md" + +# For PDFs, use pdfgrep if available, or list and let user check +find ~/Downloads -name "*.pdf" -exec basename {} \; +\`\`\` + +**Extracting content from documents:** +When users want to read or summarize a document's contents (PDF, Excel, CSV, Word .docx), use the \`parseFile\` builtin tool. It extracts text from binary formats so you can answer questions about them. +- Accepts absolute paths (e.g., \`~/Downloads/report.pdf\`) or workspace-relative paths — no need to copy files first. +- Supported formats: \`.pdf\`, \`.xlsx\`, \`.xls\`, \`.csv\`, \`.docx\` + +For scanned PDFs, images with text, complex layouts, or presentations where local parsing falls short, use the \`LLMParse\` builtin tool instead. It sends the file to the configured LLM as a multimodal attachment and returns well-structured markdown. +- Supports everything \`parseFile\` does plus images (\`.png\`, \`.jpg\`, \`.gif\`, \`.webp\`, \`.svg\`, \`.bmp\`, \`.tiff\`), PowerPoint (\`.pptx\`), HTML, and plain text. +- Also accepts an optional \`prompt\` parameter for custom extraction instructions. + +## Organizing Files + +**Create destination folder:** +\`\`\`bash +mkdir -p ~/Desktop/Screenshots +mkdir -p ~/Downloads/PDFs +mkdir -p ~/Documents/Projects/ProjectName +\`\`\` + +**Move files:** +\`\`\`bash +# Move specific file +mv ~/Desktop/Screenshot\ 2024-01-15.png ~/Desktop/Screenshots/ + +# Move all matching files (after confirmation!) +find ~/Desktop -name "Screenshot*" -exec mv {} ~/Desktop/Screenshots/ \; + +# Safer: move with verbose output +mv -v ~/Desktop/Screenshot*.png ~/Desktop/Screenshots/ +\`\`\` + +**Batch organization pattern:** +\`\`\`bash +# Create folders by file type +mkdir -p ~/Desktop/{Screenshots,Documents,Images,Videos,Other} + +# Move by type (show user the plan first!) +find ~/Desktop -maxdepth 1 -name "*.png" -exec mv -v {} ~/Desktop/Images/ \; +find ~/Desktop -maxdepth 1 -name "*.pdf" -exec mv -v {} ~/Desktop/Documents/ \; +\`\`\` + +## Common Organization Tasks + +### Screenshots on Desktop +1. List screenshots: \`find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \) -type f\` +2. Count them: add \`| wc -l\` +3. Create folder: \`mkdir -p ~/Desktop/Screenshots\` +4. Show plan and get confirmation +5. Move: \`find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \) -exec mv -v {} ~/Desktop/Screenshots/ \;\` + +### Clean up Downloads +1. Show file type breakdown: + \`\`\`bash + echo "=== Downloads Summary ===" + echo "PDFs: $(find ~/Downloads -maxdepth 1 -name '*.pdf' | wc -l)" + echo "Images: $(find ~/Downloads -maxdepth 1 \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) | wc -l)" + echo "DMGs: $(find ~/Downloads -maxdepth 1 -name '*.dmg' | wc -l)" + echo "ZIPs: $(find ~/Downloads -maxdepth 1 -name '*.zip' | wc -l)" + \`\`\` +2. Propose organization structure +3. Get confirmation +4. Execute moves + +### Find a specific file +1. Ask clarifying questions if needed (file type, approximate name, when downloaded) +2. Search with appropriate find command +3. Show matches with full paths +4. Offer to open the containing folder: \`open ~/Downloads\` (macOS) + +## Output Format + +When presenting a plan: +\`\`\` +📁 Organization Plan: Desktop Cleanup + +Found 47 files to organize: +- 23 screenshots → ~/Desktop/Screenshots/ +- 12 PDFs → ~/Desktop/Documents/ +- 8 images → ~/Desktop/Images/ +- 4 other files (leaving in place) + +Should I proceed with this organization? +\`\`\` + +When reporting results: +\`\`\` +✅ Organization Complete + +Moved 43 files: +- 23 screenshots to Screenshots/ +- 12 PDFs to Documents/ +- 8 images to Images/ + +4 files left in place (mixed types - review manually) +\`\`\` + +## Safety Rules + +1. **Never delete without explicit permission** - even "cleanup" means organize, not delete +2. **Don't touch system folders** - /System, /Library, /Applications, etc. +3. **Don't touch hidden files** - files starting with . unless explicitly asked +4. **Limit depth** - use \`-maxdepth 1\` unless user wants recursive organization +5. **Show before doing** - always preview the operation first +6. **Preserve originals when uncertain** - copy instead of move if unsure diff --git a/apps/skills/slack/SKILL.md b/apps/skills/slack/SKILL.md new file mode 100644 index 00000000..1a57ea7a --- /dev/null +++ b/apps/skills/slack/SKILL.md @@ -0,0 +1,133 @@ +--- +name: slack +description: >- + Send Slack messages, view channel history, search conversations, find users, and manage team communication. Use when the user wants to interact with Slack. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Slack Integration" + author: rowboatlabs + tags: "slack, messaging, communication" +--- + +# Slack Integration Skill (agent-slack CLI) + +You interact with Slack by running **agent-slack** commands through \`executeCommand\`. + +--- + +## 1. Check Connection + +Before any Slack operation, read \`~/.rowboat/config/slack.json\`. If \`enabled\` is \`false\` or the \`workspaces\` array is empty, simply tell the user: "Slack is not enabled. You can enable it in the Connectors settings." Do not attempt any agent-slack commands. + +If enabled, use the workspace URLs from the config for all commands. + +--- + +## 2. Core Commands + +### Messages + +| Action | Command | +|--------|---------| +| List recent messages | \`agent-slack message list "#channel-name" --limit 25\` | +| List thread replies | \`agent-slack message list "#channel" --thread-ts 1234567890.123456\` | +| Get a single message | \`agent-slack message get "https://team.slack.com/archives/C.../p..."\` | +| Send a message | \`agent-slack message send "#channel-name" "Hello team!"\` | +| Reply in thread | \`agent-slack message send "#channel-name" "Reply text" --thread-ts 1234567890.123456\` | +| Edit a message | \`agent-slack message edit "#channel-name" --ts 1234567890.123456 "Updated text"\` | +| Delete a message | \`agent-slack message delete "#channel-name" --ts 1234567890.123456\` | + +**Targets** can be: +- A full Slack URL: \`https://team.slack.com/archives/C01234567/p1234567890123456\` +- A channel name: \`"#general"\` or \`"general"\` +- A channel ID: \`C01234567\` + +### Reactions + +\`\`\` +agent-slack message react add "" --ts +agent-slack message react remove "" --ts +\`\`\` + +### Search + +\`\`\` +agent-slack search messages "query text" --limit 20 +agent-slack search messages "query" --channel "#channel-name" --user "@username" +agent-slack search messages "query" --after 2025-01-01 --before 2025-02-01 +agent-slack search files "query" --limit 10 +\`\`\` + +### Channels + +\`\`\` +agent-slack channel new --name "project-x" --workspace https://team.slack.com +agent-slack channel new --name "secret-project" --private +agent-slack channel invite --channel "#project-x" --users "@alice,@bob" +\`\`\` + +### Users + +\`\`\` +agent-slack user list --limit 200 +agent-slack user get "@username" +agent-slack user get U01234567 +\`\`\` + +### Canvases + +\`\`\` +agent-slack canvas get "https://team.slack.com/docs/F01234567" +agent-slack canvas get F01234567 --workspace https://team.slack.com +\`\`\` + +--- + +## 3. Multi-Workspace + +**Important:** The user has chosen which workspaces to use. Before your first Slack operation, read \`~/.rowboat/config/slack.json\` to see the selected workspaces. Only interact with workspaces listed in that config — ignore any other authenticated workspaces. + +If the selected workspace list contains multiple entries, use \`--workspace \` to disambiguate: + +\`\`\` +agent-slack message list "#general" --workspace https://team.slack.com +\`\`\` + +If only one workspace is selected, always use \`--workspace\` with its URL to avoid ambiguity with other authenticated workspaces. + +--- + +## 4. Token Budget Control + +Use \`--limit\` to control how many messages/results are returned. Use \`--max-body-chars\` or \`--max-content-chars\` to truncate long message bodies: + +\`\`\` +agent-slack message list "#channel" --limit 10 +agent-slack search messages "query" --limit 5 --max-content-chars 2000 +\`\`\` + +--- + +## 5. Discovering More Commands + +For any command you're unsure about: + +\`\`\` +agent-slack --help +agent-slack message --help +agent-slack search --help +agent-slack channel --help +\`\`\` + +--- + +## Best Practices + +- **Always show drafts before sending** — Never send Slack messages without user confirmation +- **Summarize, don't dump** — When showing channel history, summarize the key points rather than pasting everything +- **Prefer Slack URLs** — When referring to messages, use Slack URLs over raw channel names when available +- **Use --limit** — Always set reasonable limits to keep output concise and token-efficient +- **Resolve user IDs** — Messages contain raw user IDs like \`U078AHJP341\`. Resolve them to real names before presenting to the user. Batch all lookups into a single \`executeCommand\` call using \`;\` separators, e.g. \`agent-slack user get U078AHJP341 --workspace ... ; agent-slack user get U090UEZCEQ0 --workspace ...\` +- **Cross-reference with knowledge base** — Check if mentioned people have notes in the knowledge base diff --git a/apps/skills/tracks/SKILL.md b/apps/skills/tracks/SKILL.md new file mode 100644 index 00000000..c6ccf23d --- /dev/null +++ b/apps/skills/tracks/SKILL.md @@ -0,0 +1,474 @@ +--- +name: tracks +description: >- + Create and manage track blocks — YAML-fenced, auto-updating content blocks embedded in notes. Use when the user wants to track, monitor, watch, or keep an eye on something in a note, asks for recurring/auto-refreshing content ("every morning...", "show current...", "pin live X here"), or presses Cmd+K and requests auto-updating content at the cursor. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: Tracks +--- + +# Tracks Skill + +You are helping the user create and manage **track blocks** — YAML-fenced, auto-updating content blocks embedded in notes. Load this skill whenever the user wants to track, monitor, watch, or keep an eye on something in a note, asks for recurring/auto-refreshing content ("every morning...", "show current...", "pin live X here"), or presses Cmd+K and requests auto-updating content at the cursor. + +## First: Just Do It — Do Not Ask About Edit Mode + +Track creation and editing are **action-first**. When the user asks to track, monitor, watch, or pin auto-updating content, you proceed directly — read the file, construct the block, `workspace-edit` it in. Do not ask "Should I make edits directly, or show you changes first for approval?" — that prompt belongs to generic document editing, not to tracks. + +- If another skill or an earlier turn already asked about edit mode and is waiting, treat the user's track request as implicit "direct mode" and proceed. +- You may still ask **one** short clarifying question when genuinely ambiguous (e.g. which note to add it to). Not about permission to edit. +- The Suggested Topics flow below is the one first-turn-confirmation exception — leave it intact. + +## What Is a Track Block + +A track block is a scheduled, agent-run block embedded directly inside a markdown note. Each block has: +- A YAML-fenced `track` block that defines the instruction, schedule, and metadata. +- A sibling "target region" — an HTML-comment-fenced area where the generated output lives. The runner rewrites the target region on each scheduled run. + +**Concrete example** (a track that shows the current time in Chicago every hour): + +```track +trackId: chicago-time +instruction: | + Show the current time in Chicago, IL in 12-hour format. +active: true +schedule: + type: cron + expression: "0 * * * *" +``` + + + + +Good use cases: +- Weather / air quality for a location +- News digests or headlines +- Stock or crypto prices +- Sports scores +- Service status pages +- Personal dashboards (today's calendar, steps, focus stats) +- Any recurring summary that decays fast + +## Anatomy + +Each track has two parts that live next to each other in the note: + +1. The `track` code fence — contains the YAML config. The fence language tag is literally `track`. +2. The target-comment region — `` and `` with optional content between. The ID must match the `trackId` in the YAML. + +The target region is **sibling**, not nested. It must **never** live inside the ```track fence. + +## Canonical Schema + +Below is the authoritative schema for a track block (generated at runtime from the TypeScript source — never out of date). Use it to validate every field name, type, and constraint before writing YAML: + +```yaml +{{TRACK_BLOCK_SCHEMA}} +``` + +**Runtime-managed fields — never write these yourself:** `lastRunAt`, `lastRunId`, `lastRunSummary`. + +## Choosing a trackId + +- Kebab-case, short, descriptive: `chicago-time`, `sfo-weather`, `hn-top5`, `btc-usd`. +- **Must be unique within the note file.** Before inserting, read the file and check: + - All existing `trackId:` lines in ```track blocks + - All existing `` comments +- If you need disambiguation, add scope: `btc-price-usd`, `weather-home`, `news-ai-2`. +- Don't reuse an old ID even if the previous block was deleted — pick a fresh one. + +## Writing a Good Instruction + +### The Frame: This Is a Personal Knowledge Tracker + +Track output lives in a personal knowledge base the user scans frequently. Aim for data-forward, scannable output — the answer to "what's current / what changed?" in the fewest words that carry real information. Not prose. Not decoration. + +### Core Rules + +- **Specific and actionable.** State exactly what to fetch or compute. +- **Single-focus.** One block = one purpose. Split "weather + news + stocks" into three blocks, don't bundle. +- **Imperative voice, 1-3 sentences.** +- **Specify output shape.** Describe it concretely: "one line: `°F, `", "3-column markdown table", "bulleted digest of 5 items". + +### Self-Sufficiency (critical) + +The instruction runs later, in a background scheduler, with **no chat context and no memory of this conversation**. It must stand alone. + +**Never use phrases that depend on prior conversation or prior runs:** +- "as before", "same style as before", "like last time" +- "keep the format we discussed", "matching the previous output" +- "continue from where you left off" (without stating the state) + +If you want consistent style across runs, **describe the style inline** (e.g. "a 3-column markdown table with headers `Location`, `Local Time`, `Offset`"; "a one-line status: HH:MM, conditions, temp"). The track agent only sees your instruction — not this chat, not what you produced last time. + +### Output Patterns — Match the Data + +Pick a shape that fits what the user is tracking. Five common patterns — the first four are plain markdown; the fifth is a rich rendered block: + +**1. Single metric / status line.** +- Good: "Fetch USD/INR. Return one line: `USD/INR: (as of )`." +- Bad: "Give me a nice update about the dollar rate." + +**2. Compact table.** +- Good: "Show current local time for India, Chicago, Indianapolis as a 3-column markdown table: `Location | Local Time | Offset vs India`. One row per location, no prose." +- Bad: "Show a polished, table-first world clock with a pleasant layout." + +**3. Rolling digest.** +- Good: "Summarize the top 5 HN front-page stories as bullets: `- (<points> pts, <comments> comments)`. No commentary." +- Bad: "Give me the top HN stories with thoughtful takeaways." + +**4. Status / threshold watch.** +- Good: "Check https://status.example.com. Return one line: `✓ All systems operational` or `⚠ <component>: <status>`. If degraded, add one bullet per affected component." +- Bad: "Keep an eye on the status page and tell me how it looks." + +**5. Rich block render — when the data has a natural visual form.** + +The track agent can emit *rich blocks* — special fenced blocks the editor renders as styled UI (charts, calendars, embedded iframes, etc.). When the data fits one of these shapes, instruct the agent explicitly so it doesn't fall back to plain markdown: + +- `table` — multi-row data, scoreboards, leaderboards. *"Render as a `table` block with columns Rank, Title, Points, Comments."* +- `chart` — time series, breakdowns, share-of-total. *"Render as a `chart` block (line, bar, or pie) with x=date, y=rate."* +- `mermaid` — flowcharts, sequence/relationship diagrams, gantt charts. *"Render as a `mermaid` diagram."* +- `calendar` — upcoming events / agenda. *"Render as a `calendar` block."* +- `email` — single email thread digest (subject, from, summary, latest body, optional draft). *"Render the most important unanswered thread as an `email` block."* +- `image` — single image with caption. *"Render as an `image` block."* +- `embed` — YouTube or Figma. *"Render as an `embed` block."* +- `iframe` — live dashboards, status pages, anything that benefits from being live not snapshotted. *"Render as an `iframe` block pointing to <url>."* +- `transcript` — long meeting transcripts (collapsible). *"Render as a `transcript` block."* +- `prompt` — a "next step" Copilot card the user can click to start a chat. *"End with a `prompt` block labeled '<short label>' that runs '<longer prompt to send to Copilot>'."* + +You **do not** need to write the block body yourself — describe the desired output in the instruction and the track agent will format it (it knows each block's exact schema). Avoid `track` and `task` block types — those are user-authored input, not agent output. + +- Good: "Show today's calendar events. Render as a `calendar` block with `showJoinButton: true`." +- Good: "Plot USD/INR over the last 7 days as a `chart` block — line chart, x=date, y=rate." +- Bad: "Show today's calendar." (vague — agent may produce a markdown bullet list when the user wants the rich block) + +### Anti-Patterns + +- **Decorative adjectives** describing the output: "polished", "clean", "beautiful", "pleasant", "nicely formatted" — they tell the agent nothing concrete. +- **References to past state** without a mechanism to access it ("as before", "same as last time"). +- **Bundling multiple purposes** into one instruction — split into separate track blocks. +- **Open-ended prose requests** ("tell me about X", "give me thoughts on X"). +- **Output-shape words without a concrete shape** ("dashboard-like", "report-style"). + +## YAML String Style (critical — read before writing any `instruction` or `eventMatchCriteria`) + +The two free-form fields — `instruction` and `eventMatchCriteria` — are where YAML parsing usually breaks. The runner re-emits the full YAML block every time it writes `lastRunAt`, `lastRunSummary`, etc., and the YAML library may re-flow long plain (unquoted) strings onto multiple lines. Once that happens, any `:` **followed by a space** inside the value silently corrupts the block: YAML interprets the `:` as a new key/value separator and the instruction gets truncated. + +Real failure seen in the wild — an instruction containing the phrase `"polished UI style as before: clean, compact..."` was written as a plain scalar, got re-emitted across multiple lines on the next run, and the `as before:` became a phantom key. The block parsed as garbage after that. + +### The rule: always use a safe scalar style + +**Default to the literal block scalar (`|`) for `instruction` and `eventMatchCriteria`, every time.** It is the only style that is robust across the full range of punctuation these fields typically contain, and it is safe even if the content later grows to multiple lines. + +### Preferred: literal block scalar (`|`) + +```yaml +instruction: | + Show current local time for India, Chicago, and Indianapolis as a + 3-column markdown table: Location | Local Time | Offset vs India. + One row per location, 24-hour time (HH:MM), no extra prose. + Note: when a location is in DST, reflect that in the offset column. +eventMatchCriteria: | + Emails from the finance team about Q3 budget or OKRs. +``` + +- `|` preserves line breaks verbatim. Colons, `#`, quotes, leading `-`, percent signs — all literal. No escaping needed. +- **Indent every content line by 2 spaces** relative to the key (`instruction:`). Use spaces, never tabs. +- Leave a real newline after `|` — content starts on the next line, not the same line. +- Default chomping (no modifier) is fine. Do **not** add `-` or `+` unless you know you need them. +- A `|` block is terminated by a line indented less than the content — typically the next sibling key (`active:`, `schedule:`). + +### Acceptable alternative: double-quoted on a single line + +Fine for short single-sentence fields with no newline needs: + +```yaml +instruction: "Show the current time in Chicago, IL in 12-hour format." +eventMatchCriteria: "Emails about Q3 planning, OKRs, or roadmap decisions." +``` + +- Escape `"` as `\"` and backslash as `\\`. +- Prefer `|` the moment the string needs two sentences or a newline. + +### Single-quoted on a single line (only if double-quoted would require heavy escaping) + +```yaml +instruction: 'He said "hi" at 9:00.' +``` + +- A literal single quote is escaped by doubling it: `'it''s fine'`. +- No other escape sequences work. + +### Do NOT use plain (unquoted) scalars for these two fields + +Even if the current value looks safe, a future edit (by you or the user) may introduce a `:` or `#`, and a future re-emit may fold the line. The `|` style is safe under **all** future edits — plain scalars are not. + +### Editing an existing track + +If you `workspace-edit` an existing track's `instruction` or `eventMatchCriteria` and find it is still a plain scalar, **upgrade it to `|`** in the same edit. Don't leave a plain scalar behind that the next run will corrupt. + +### Never-hand-write fields + +`lastRunAt`, `lastRunId`, `lastRunSummary` are owned by the runner. Don't touch them — don't even try to style them. If your `workspace-edit`'s `oldString` happens to include these lines, copy them byte-for-byte into `newString` unchanged. + +## Schedules + +Schedule is an **optional** discriminated union. Three types: + +### `cron` — recurring at exact times + +```yaml +schedule: + type: cron + expression: "0 * * * *" +``` + +Fires at the exact cron time. Use when the user wants precise timing ("at 9am daily", "every hour on the hour"). + +### `window` — recurring within a time-of-day range + +```yaml +schedule: + type: window + cron: "0 0 * * 1-5" + startTime: "09:00" + endTime: "17:00" +``` + +Fires **at most once per cron occurrence**, but only if the current time is within `startTime`–`endTime` (24-hour HH:MM, local). Use when the user wants "sometime in the morning" or "once per weekday during work hours" — flexible timing with bounds. + +### `once` — one-shot at a future time + +```yaml +schedule: + type: once + runAt: "2026-04-14T09:00:00" +``` + +Fires once at `runAt` and never again. Local time, no `Z` suffix. + +### Cron cookbook + +- `"*/15 * * * *"` — every 15 minutes +- `"0 * * * *"` — every hour on the hour +- `"0 8 * * *"` — daily at 8am +- `"0 9 * * 1-5"` — weekdays at 9am +- `"0 0 * * 0"` — Sundays at midnight +- `"0 0 1 * *"` — first of month at midnight + +**Omit `schedule` entirely for a manual-only track** — the user triggers it via the Play button in the UI. + +## Event Triggers (third trigger type) + +In addition to manual and scheduled, a track can be triggered by **events** — incoming signals from the user's data sources (currently: gmail emails). Set `eventMatchCriteria` to a description of what kinds of events should consider this track for an update: + +```track +trackId: q3-planning-emails +instruction: | + Maintain a running summary of decisions and open questions about Q3 + planning, drawn from emails on the topic. +active: true +eventMatchCriteria: | + Emails about Q3 planning, roadmap decisions, or quarterly OKRs. +``` + +How it works: +1. When a new event arrives (e.g. an email syncs), a fast LLM classifier checks `eventMatchCriteria` against the event content. +2. If it might match, the track-run agent receives both the event payload and the existing track content, and decides whether to actually update. +3. If the event isn't truly relevant on closer inspection, the agent skips the update — no fabricated content. + +When to suggest event triggers: +- The user wants to **maintain a living summary** of a topic ("keep notes on everything related to project X"). +- The content depends on **incoming signals** rather than periodic refresh ("update this whenever a relevant email arrives"). +- Mention to the user: scheduled (cron) is for time-driven updates; event is for signal-driven updates. They can be combined — a track can have both a `schedule` and `eventMatchCriteria` (it'll run on schedule AND on relevant events). + +Writing good `eventMatchCriteria`: +- Be descriptive but not overly narrow — Pass 1 routing is liberal by design. +- Examples: `"Emails from John about the migration project"`, `"Calendar events related to customer interviews"`, `"Meeting notes that mention pricing changes"`. + +Tracks **without** `eventMatchCriteria` opt out of events entirely — they'll only run on schedule or manually. + +## Insertion Workflow + +**Reminder:** once you have enough to act, act. Do not pause to ask about edit mode. + +### Cmd+K with cursor context + +When the user invokes Cmd+K, the context includes an attachment mention like: +> User has attached the following files: +> - notes.md (text/markdown) at knowledge/notes.md (line 42) + +Workflow: +1. Extract the `path` and `line N` from the attachment. +2. `workspace-readFile({ path })` — always re-read fresh. +3. Check existing `trackId`s in the file to guarantee uniqueness. +4. Locate the line. Pick a **unique 2-3 line anchor** around line N (a full heading, a distinctive sentence). Avoid blank lines and generic text. +5. Construct the full track block (YAML + target pair). +6. `workspace-edit({ path, oldString: <anchor>, newString: <anchor with block spliced at line N> })`. + +### Sidebar chat with a specific note + +1. If a file is mentioned/attached, read it. +2. If ambiguous, ask one question: "Which note should I add the track to?" +3. **Default placement: append** to the end of the file. Find the last non-empty line as the anchor. `newString` = that line + `\n\n` + track block + target pair. +4. If the user specified a section ("under the Weather heading"), anchor on that heading. + +### No note context at all + +Ask one question: "Which note should this track live in?" Don't create a new note unless the user asks. + +### Suggested Topics exploration flow + +Sometimes the user arrives from the Suggested Topics panel and gives you a prompt like: +- "I am exploring a suggested topic card from the Suggested Topics panel." +- a title, category, description, and target folder such as `knowledge/Topics/` or `knowledge/People/` + +In that flow: +1. On the first turn, **do not create or modify anything yet**. Briefly explain the tracking note you can set up and ask for confirmation. +2. If the user clearly confirms ("yes", "set it up", "do it"), treat that as explicit permission to proceed. +3. Before creating a new note, search the target folder for an existing matching note and update it if one already exists. +4. If no matching note exists and the prompt gave you a target folder, create the new note there without bouncing back to ask "which note should this live in?". +5. Use the card title as the default note title / filename unless a small normalization is clearly needed. +6. Keep the surrounding note scaffolding minimal but useful. The track block should be the core of the note. +7. If the target folder is one of the structured knowledge folders (`knowledge/People/`, `knowledge/Organizations/`, `knowledge/Projects/`, `knowledge/Topics/`), mirror the local note style by quickly checking a nearby note or config before writing if needed. + +## The Exact Text to Insert + +Write it verbatim like this (including the blank line between fence and target): + +```track +trackId: <id> +instruction: | + <instruction, indented 2 spaces, may span multiple lines> +active: true +schedule: + type: cron + expression: "0 * * * *" +``` + +<!--track-target:<id>--> +<!--/track-target:<id>--> + +**Rules:** +- One blank line between the closing ``` fence and the `<!--track-target:ID-->`. +- Target pair is **empty on creation**. The runner fills it on the first run. +- **Always use the literal block scalar (`|`)** for `instruction` and `eventMatchCriteria`, indented 2 spaces. Never a plain (unquoted) scalar — see the YAML String Style section above for why. +- **Always quote cron expressions** in YAML — they contain spaces and `*`. +- Use 2-space YAML indent. No tabs. +- Top-level markdown only — never inside a code fence, blockquote, or table. + +## After Insertion + +- Confirm in one line: "Added `chicago-time` track, refreshing hourly." +- **Then offer to run it once now** (see "Running a Track" below) — especially valuable for newly created blocks where the target region is otherwise empty until the next scheduled or event-triggered run. +- **Do not** write anything into the `<!--track-target:...-->` region yourself — use the `run-track-block` tool to delegate to the track agent. + +## Running a Track (the `run-track-block` tool) + +The `run-track-block` tool manually triggers a track run right now. Equivalent to the user clicking the Play button — but you can pass extra `context` to bias what the track agent does on this single run (without modifying the block's `instruction`). + +### When to proactively offer to run + +These are upsells — ask first, don't run silently. + +- **Just created a new track block.** Before declaring done, offer: + > "Want me to run it once now to seed the initial content?" + + This is **especially valuable for event-triggered tracks** (with `eventMatchCriteria`) — otherwise the target region stays empty until the next matching event arrives. + + For tracks that pull from existing local data (synced emails, calendar, meeting notes), suggest a **backfill** with explicit context (see below). + +- **Just edited an existing track.** Offer: + > "Want me to run it now to see the updated output?" + +- **Explicit user request.** "run the X track", "test it", "refresh that block" → call the tool directly. + +### Using the `context` parameter (the powerful case) + +The `context` parameter is extra guidance for the track agent on this run only. It's the difference between a stock refresh and a smart backfill. + +**Examples:** + +- New track: "Track emails about Q3 planning" → after creating it, run with: + > context: "Initial backfill — scan `gmail_sync/` for emails from the last 90 days that match this track's topic (Q3 planning, OKRs, roadmap), and synthesize the initial summary." + +- New track: "Summarize this week's customer calls" → run with: + > context: "Backfill from this week's meeting notes in `granola_sync/` and `fireflies_sync/`." + +- Manual refresh after the user mentions a recent change: + > context: "Focus on changes from the last 7 days only." + +- Plain refresh (user says "run it now"): **omit `context` entirely**. Don't invent context — it can mislead the agent. + +### What to do with the result + +The tool returns `{ success, runId, action, summary, contentAfter, error }`: + +- **`action: 'replace'`** → the track was updated. Confirm with one line, optionally citing the first line of `contentAfter`: + > "Done — track now shows: 72°F, partly cloudy in Chicago." + +- **`action: 'no_update'`** → the agent decided nothing needed to change. Tell the user briefly; `summary` may explain why. + +- **`error` set** → surface it concisely. If the error is `'Already running'` (concurrency guard), let the user know the track is mid-run and to retry shortly. + +### Don'ts + +- **Don't auto-run** after every edit — ask first. +- **Don't pass `context`** for a plain refresh — only when there's specific extra guidance to give. +- **Don't use `run-track-block` to manually write content** — that's `update-track-content`'s job (and even that should be rare; the track agent handles content via this tool). +- **Don't `run-track-block` repeatedly** in a single turn — one run per user-facing action. + +## Proactive Suggestions + +When the user signals interest in recurring or time-decaying info, **offer a track block** instead of a one-off answer. Signals: +- "I want to track / monitor / watch / keep an eye on / follow X" +- "Can you check on X every morning / hourly / weekly?" +- The user just asked a one-off question whose answer decays (weather, score, price, status, news). +- The user is building a time-sensitive page (weekly dashboard, morning briefing). + +Suggestion style — one line, concrete: +> "I can turn this into a track block that refreshes hourly — want that?" + +Don't upsell aggressively. If the user clearly wants a one-off answer, give them one. + +## Don'ts + +- **Don't reuse** an existing `trackId` in the same file. +- **Don't add `schedule`** if the user explicitly wants a manual-only track. +- **Don't write** `lastRunAt`, `lastRunId`, or `lastRunSummary` — runtime-managed. +- **Don't nest** the `<!--track-target:ID-->` region inside the ```track fence. +- **Don't touch** content between `<!--track-target:ID-->` and `<!--/track-target:ID-->` — that's generated content. +- **Don't schedule** with `"* * * * *"` (every minute) unless the user explicitly asks. +- **Don't add a `Z` suffix** on `runAt` — local time only. +- **Don't use `workspace-writeFile`** to rewrite the whole file — always `workspace-edit` with a unique anchor. + +## Editing or Removing an Existing Track + +**Change schedule or instruction:** read the file, `workspace-edit` the YAML body. Anchor on the unique `trackId: <id>` line plus a few surrounding lines. + +**Pause without deleting:** flip `active: false`. + +**Remove entirely:** `workspace-edit` with `oldString` = the full ```track block **plus** the target pair (so generated content also disappears), `newString` = empty. + +## Quick Reference + +Minimal template: + +```track +trackId: <kebab-id> +instruction: | + <what to produce — always use `|`, indented 2 spaces> +active: true +schedule: + type: cron + expression: "0 * * * *" +``` + +<!--track-target:<kebab-id>--> +<!--/track-target:<kebab-id>--> + +Top cron expressions: `"0 * * * *"` (hourly), `"0 8 * * *"` (daily 8am), `"0 9 * * 1-5"` (weekdays 9am), `"*/15 * * * *"` (every 15m). + +YAML style reminder: `instruction` and `eventMatchCriteria` are **always** `|` block scalars. Never plain. Never leave a plain scalar in place when editing. diff --git a/apps/skills/web-search/SKILL.md b/apps/skills/web-search/SKILL.md new file mode 100644 index 00000000..e8bcefba --- /dev/null +++ b/apps/skills/web-search/SKILL.md @@ -0,0 +1,61 @@ +--- +name: web-search +description: >- + Searching the web or researching a topic. Guidance on when to use web-search vs research-search and how many searches to do. Use when the user wants to search the internet. +license: MIT +compatibility: Designed for Rowboat desktop app +metadata: + version: "1.0.0" + title: "Web Search" + author: rowboatlabs + tags: "search, web, research" +--- + +# Web Search Skill + +You have access to two search tools for finding information on the internet. Choose the right one based on the user's intent. + +## Tools + +### web-search (Brave Search) +Quick, general-purpose web search. Returns titles, URLs, and short descriptions. + +**Best for:** +- Quick lookups for things that change ("current price of Bitcoin", "weather in SF") +- Current events and breaking news +- Finding a specific website or page +- Simple questions with direct answers +- Checking a fact or date + +### research-search (Exa Search) +Deep, research-oriented search. Returns full article text, highlights, and metadata (author, published date). + +**Best for:** +- Exploring a topic in depth ("what are the latest advances in CRISPR") +- Finding articles, blog posts, papers, and quality sources +- Discovering companies, people, or organizations +- Research where you need rich context, not just links +- When the user says "research", "find articles about", "look into", "deep dive" + +**Category filter:** Use the category parameter when the user's intent clearly maps to one: company, research paper, news, tweet, personal site, financial report, people. + +## How Many Searches to Do + +**CRITICAL: Always start with exactly ONE search call.** Pick the single best tool (\`web-search\` or \`research-search\`) and make one request. Wait for the result before deciding if more searches are needed. + +**NEVER call multiple search tools simultaneously.** No parallel web-search + research-search. No firing off two web-searches at once. Always sequential: one search at a time. + +Only make a follow-up search if: +- The first search returned truly uninformative or irrelevant results +- The query has clearly distinct sub-topics that the first search couldn't cover (e.g., "compare X and Y" after getting results for X only) +- The user explicitly asks you to dig deeper + +One good search is almost always enough. Default to one and stop. + +## Choosing Between the Two + +If both tools are attached, prefer: +- \`web-search\` when the user wants a quick answer or specific link +- \`research-search\` when the user wants to learn, explore, or gather sources + +If only one is attached, use whichever is available. diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs index 178cb7e1..1048800a 100644 --- a/apps/x/apps/main/forge.config.cjs +++ b/apps/x/apps/main/forge.config.cjs @@ -11,6 +11,9 @@ module.exports = { icon: './icons/icon', // .icns extension added automatically appBundleId: 'com.rowboat.app', appCategoryType: 'public.app-category.productivity', + // Bundles <repo>/apps/skills/ into Resources/skills/ in the packaged app. + // Read at runtime via process.resourcesPath in main.ts (resolveSkillsDir). + extraResource: [path.join(__dirname, '../../../skills')], extendInfo: { NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)', }, diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 61c130df..fbbfab63 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -23,7 +23,6 @@ const execAsync = promisify(exec); import { RunEvent } from '@x/shared/dist/runs.js'; import { ServiceEvent } from '@x/shared/dist/service-events.js'; import container from '@x/core/dist/di/container.js'; -import type { ISkillsRepo } from '@x/core/dist/skills/repo.js'; import type { ISkillResolver } from '@x/core/dist/skills/resolver.js'; import { listOnboardingModels } from '@x/core/dist/models/models-dev.js'; import { testModelConnection } from '@x/core/dist/models/models.js'; @@ -824,7 +823,7 @@ export function setupIpcHandlers() { return { success: false, error: err instanceof Error ? err.message : String(err) }; } }, - // Skills handlers + // Skills handlers (read-only) 'skills:list': async () => { const resolver = container.resolve<ISkillResolver>('skillResolver'); const skills = await resolver.getCatalog(); @@ -834,20 +833,6 @@ export function setupIpcHandlers() { const resolver = container.resolve<ISkillResolver>('skillResolver'); return await resolver.resolve(args.id); }, - 'skills:getOfficial': async (_event, args) => { - const resolver = container.resolve<ISkillResolver>('skillResolver'); - return await resolver.getOfficial(args.id); - }, - 'skills:saveOverride': async (_event, args) => { - const repo = container.resolve<ISkillsRepo>('skillsRepo'); - await repo.saveOverride(args.skillId, args.meta, args.content); - return { success: true as const }; - }, - 'skills:deleteOverride': async (_event, args) => { - const repo = container.resolve<ISkillsRepo>('skillsRepo'); - await repo.deleteOverride(args.skillId); - return { success: true as const }; - }, // Billing handler 'billing:getInfo': async () => { return await getBillingInfo(); diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 382f4ed7..ef4bd835 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -22,7 +22,6 @@ import { init as initEmailLabeling } from "@x/core/dist/knowledge/label_emails.j import { init as initNoteTagging } from "@x/core/dist/knowledge/tag_notes.js"; import { init as initInlineTasks } from "@x/core/dist/knowledge/inline_tasks.js"; import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js"; -import { init as initSkillSync } from "@x/core/dist/skills/sync.js"; import { init as initAgentNotes } from "@x/core/dist/knowledge/agent_notes.js"; import { init as initTrackScheduler } from "@x/core/dist/knowledge/track/scheduler.js"; import { init as initTrackEventProcessor } from "@x/core/dist/knowledge/track/events.js"; @@ -33,7 +32,7 @@ import started from "electron-squirrel-startup"; import { execSync, exec, execFileSync } from "node:child_process"; import { promisify } from "node:util"; import { init as initChromeSync } from "@x/core/dist/knowledge/chrome-extension/server/server.js"; -import { registerBrowserControlService } from "@x/core/dist/di/container.js"; +import { registerBrowserControlService, registerSkillsDir } from "@x/core/dist/di/container.js"; import { browserViewManager, BROWSER_PARTITION } from "./browser/view.js"; import { setupBrowserEventForwarding } from "./browser/ipc.js"; import { ElectronBrowserControlService } from "./browser/control-service.js"; @@ -43,6 +42,17 @@ const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +function resolveSkillsDir(): string { + if (app.isPackaged) { + // forge.config.cjs ships apps/skills/ as extraResource → Resources/skills/ + return path.join(process.resourcesPath, "skills"); + } + // Dev: walk up to repo root from this file's bundled location. + // main runs from apps/x/apps/main/.package/dist/main.cjs, so 5 levels up + // lands at <repo>/apps/x; one more lands at <repo>/apps; skills is its sibling. + return path.resolve(__dirname, "..", "..", "..", "..", "..", "skills"); +} + // run this as early in the main process as possible if (started) app.quit(); @@ -233,6 +243,10 @@ app.whenReady().then(async () => { registerBrowserControlService(new ElectronBrowserControlService()); + // Skills ship with the app. Register the source directory before any + // consumer (resolver, IPC handlers, copilot instructions) resolves. + registerSkillsDir(resolveSkillsDir()); + setupIpcHandlers(); setupBrowserEventForwarding(); @@ -287,9 +301,6 @@ app.whenReady().then(async () => { // start background agent runner (scheduled agents) initAgentRunner(); - // start skill sync service - initSkillSync(); - // start agent notes learning service initAgentNotes(); diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 55e86f85..e78c5259 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -1511,10 +1511,7 @@ export function SettingsDialog({ children, initialTab }: SettingsDialogProps) { const [saving, setSaving] = useState(false) const [error, setError] = useState<string | null>(null) const [rowboatConnected, setRowboatConnected] = useState(false) - const [skillUpdateCount, setSkillUpdateCount] = useState(0) - const [skillsExpanded, setSkillsExpanded] = useState(false) - // Check if user is signed in to Rowboat useEffect(() => { if (!open) return window.ipc.invoke('oauth:getState', null).then((result) => { @@ -1525,16 +1522,6 @@ export function SettingsDialog({ children, initialTab }: SettingsDialogProps) { }) }, [open]) - useEffect(() => { - if (!open) return - window.ipc.invoke('skills:list', null).then((result) => { - const count = result.skills.filter((s: { hasUpdate?: boolean }) => s.hasUpdate).length - setSkillUpdateCount(count) - }).catch(() => { - setSkillUpdateCount(0) - }) - }, [open]) - useEffect(() => { if (initialTab && open) { setActiveTab(initialTab) @@ -1623,14 +1610,7 @@ export function SettingsDialog({ children, initialTab }: SettingsDialogProps) { return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild>{children}</DialogTrigger> - <DialogContent - className={cn( - "p-0 gap-0 overflow-hidden transition-all duration-200", - skillsExpanded - ? "max-w-[1200px]! w-[1200px] h-[80vh]" - : "max-w-[900px]! w-[900px] h-[600px]" - )} - > + <DialogContent className="p-0 gap-0 overflow-hidden max-w-[900px]! w-[900px] h-[600px]"> <div className="flex h-full overflow-hidden"> {/* Sidebar */} <div className="w-48 border-r bg-muted/30 p-2 flex flex-col"> @@ -1651,11 +1631,6 @@ export function SettingsDialog({ children, initialTab }: SettingsDialogProps) { > <tab.icon className="size-4" /> <span className="flex-1">{tab.label}</span> - {tab.id === "skills" && skillUpdateCount > 0 && ( - <span className="flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-amber-500 text-white text-[10px] font-medium"> - {skillUpdateCount} - </span> - )} </button> ))} </nav> @@ -1688,7 +1663,7 @@ export function SettingsDialog({ children, initialTab }: SettingsDialogProps) { ) : activeTab === "appearance" ? ( <AppearanceSettings /> ) : activeTab === "skills" ? ( - <SkillsSettings dialogOpen={open} onExpandRequest={setSkillsExpanded} /> + <SkillsSettings dialogOpen={open} /> ) : activeTab === "tools" ? ( <ToolsLibrarySettings dialogOpen={open} rowboatConnected={rowboatConnected} /> ) : loading ? ( diff --git a/apps/x/apps/renderer/src/components/settings/skills-settings.tsx b/apps/x/apps/renderer/src/components/settings/skills-settings.tsx index 89545dc1..a280a3a5 100644 --- a/apps/x/apps/renderer/src/components/settings/skills-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/skills-settings.tsx @@ -1,110 +1,22 @@ "use client" import * as React from "react" -import { useState, useEffect, useCallback, useMemo } from "react" -import { ArrowLeft, RotateCcw, Save, Pencil, ArrowUpCircle, X } from "lucide-react" +import { useState, useEffect, useCallback } from "react" +import { ArrowLeft } from "lucide-react" import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { toast } from "sonner" -import type { ResolvedSkill, SkillOverride } from "@x/shared/dist/skill.js" - -type ViewMode = "view" | "edit" | "compare" +import type { ResolvedSkill } from "@x/shared/dist/skill.js" interface SkillsSettingsProps { dialogOpen: boolean - onExpandRequest?: (expanded: boolean) => void } -// ── Simple line-based diff ────────────────────────────────────────────── -type DiffLine = { type: "same" | "add" | "del"; text: string } - -function computeDiff(oldText: string, newText: string): DiffLine[] { - const oldLines = oldText.split("\n") - const newLines = newText.split("\n") - - // Simple LCS-based diff - const m = oldLines.length - const n = newLines.length - const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)) - - for (let i = 1; i <= m; i++) { - for (let j = 1; j <= n; j++) { - if (oldLines[i - 1] === newLines[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1 - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) - } - } - } - - // Backtrack to build diff - const result: DiffLine[] = [] - let i = m, j = n - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { - result.push({ type: "same", text: oldLines[i - 1] }) - i--; j-- - } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { - result.push({ type: "add", text: newLines[j - 1] }) - j-- - } else { - result.push({ type: "del", text: oldLines[i - 1] }) - i-- - } - } - - return result.reverse() -} - -function DiffView({ oldText, newText }: { oldText: string; newText: string }) { - const lines = useMemo(() => computeDiff(oldText, newText), [oldText, newText]) - const stats = useMemo(() => { - const added = lines.filter((l) => l.type === "add").length - const removed = lines.filter((l) => l.type === "del").length - return { added, removed } - }, [lines]) - - return ( - <div className="flex flex-col h-full overflow-hidden"> - <div className="flex items-center gap-3 mb-2 text-xs text-muted-foreground shrink-0"> - <span className="text-emerald-600 dark:text-emerald-400 font-medium">+{stats.added} added</span> - <span className="text-red-600 dark:text-red-400 font-medium">-{stats.removed} removed</span> - </div> - <div className="flex-1 overflow-y-auto rounded-md border bg-muted/20"> - <pre className="text-xs font-mono p-0 m-0"> - {lines.map((line, i) => ( - <div - key={i} - className={cn( - "px-3 py-0.5 border-l-2", - line.type === "add" && "bg-emerald-500/10 border-l-emerald-500 text-emerald-800 dark:text-emerald-300", - line.type === "del" && "bg-red-500/10 border-l-red-500 text-red-800 dark:text-red-300 line-through opacity-70", - line.type === "same" && "border-l-transparent text-muted-foreground" - )} - > - <span className="inline-block w-6 text-right mr-3 opacity-40 select-none text-[10px]"> - {line.type === "add" ? "+" : line.type === "del" ? "-" : " "} - </span> - {line.text || " "} - </div> - ))} - </pre> - </div> - </div> - ) -} - -// ── Main component ────────────────────────────────────────────────────── - -export function SkillsSettings({ dialogOpen, onExpandRequest }: SkillsSettingsProps) { +export function SkillsSettings({ dialogOpen }: SkillsSettingsProps) { const [skills, setSkills] = useState<ResolvedSkill[]>([]) const [loading, setLoading] = useState(true) const [selectedSkill, setSelectedSkill] = useState<string | null>(null) - const [editContent, setEditContent] = useState("") - const [officialContent, setOfficialContent] = useState("") - const [viewMode, setViewMode] = useState<ViewMode>("view") - const [saving, setSaving] = useState(false) + const [skillContent, setSkillContent] = useState("") const loadSkills = useCallback(async () => { try { @@ -120,23 +32,15 @@ export function SkillsSettings({ dialogOpen, onExpandRequest }: SkillsSettingsPr }, []) useEffect(() => { - if (dialogOpen) { - loadSkills() - } + if (dialogOpen) loadSkills() }, [dialogOpen, loadSkills]) - // Notify parent to expand/shrink when entering/leaving compare mode - useEffect(() => { - onExpandRequest?.(viewMode === "compare") - }, [viewMode, onExpandRequest]) - const handleSelectSkill = useCallback(async (skillId: string) => { try { const skill = await window.ipc.invoke("skills:get", { id: skillId }) if (skill) { setSelectedSkill(skillId) - setEditContent(skill.content) - setViewMode("view") + setSkillContent(skill.content) } } catch (err) { console.error("Failed to load skill:", err) @@ -144,285 +48,70 @@ export function SkillsSettings({ dialogOpen, onExpandRequest }: SkillsSettingsPr } }, []) - const handleCustomize = useCallback(async () => { - if (!selectedSkill) return - setViewMode("edit") - }, [selectedSkill]) - - const handleSave = useCallback(async () => { - if (!selectedSkill) return - const skill = skills.find((s) => s.id === selectedSkill) - if (!skill) return - - try { - setSaving(true) - const meta: SkillOverride = { - base_skill_id: selectedSkill, - base_version: skill.version, - } - await window.ipc.invoke("skills:saveOverride", { - skillId: selectedSkill, - meta, - content: editContent, - }) - toast.success("Skill customization saved") - setViewMode("view") - await loadSkills() - const updated = await window.ipc.invoke("skills:get", { id: selectedSkill }) - if (updated) { - setEditContent(updated.content) - } - } catch (err) { - console.error("Failed to save skill override:", err) - toast.error("Failed to save") - } finally { - setSaving(false) - } - }, [selectedSkill, editContent, skills, loadSkills]) - - const handleReset = useCallback(async () => { - if (!selectedSkill) return - - try { - await window.ipc.invoke("skills:deleteOverride", { skillId: selectedSkill }) - toast.success("Skill reset to official version") - await loadSkills() - const official = await window.ipc.invoke("skills:get", { id: selectedSkill }) - if (official) { - setEditContent(official.content) - } - setViewMode("view") - } catch (err) { - console.error("Failed to reset skill:", err) - toast.error("Failed to reset") - } - }, [selectedSkill, loadSkills]) - - const handleCompareUpdate = useCallback(async () => { - if (!selectedSkill) return - try { - const official = await window.ipc.invoke("skills:getOfficial", { id: selectedSkill }) - if (official) { - setOfficialContent(official.content) - setViewMode("compare") - } - } catch (err) { - console.error("Failed to load official skill:", err) - toast.error("Failed to load official version") - } - }, [selectedSkill]) - - const handleAcceptUpdate = useCallback(async () => { - if (!selectedSkill) return - - try { - await window.ipc.invoke("skills:deleteOverride", { skillId: selectedSkill }) - toast.success("Updated to latest official version") - await loadSkills() - const updated = await window.ipc.invoke("skills:get", { id: selectedSkill }) - if (updated) { - setEditContent(updated.content) - } - setViewMode("view") - } catch (err) { - console.error("Failed to accept update:", err) - toast.error("Failed to accept update") - } - }, [selectedSkill, loadSkills]) - - const handleAcceptAndRecustomize = useCallback(async () => { - if (!selectedSkill) return - const skill = skills.find((s) => s.id === selectedSkill) - if (!skill) return - - try { - const meta: SkillOverride = { - base_skill_id: selectedSkill, - base_version: skill.version, - } - await window.ipc.invoke("skills:saveOverride", { - skillId: selectedSkill, - meta, - content: editContent, - }) - toast.success("Base version updated — your customizations are preserved") - await loadSkills() - setViewMode("edit") - } catch (err) { - console.error("Failed to update base version:", err) - toast.error("Failed to update") - } - }, [selectedSkill, editContent, skills, loadSkills]) - - const handleBack = useCallback(() => { - setSelectedSkill(null) - setViewMode("view") - }, []) - - const selectedSkillData = skills.find((s) => s.id === selectedSkill) + const selectedSkillData = selectedSkill + ? skills.find((s) => s.id === selectedSkill) + : null if (loading) { return ( - <div className="h-full flex items-center justify-center text-muted-foreground text-sm"> + <div className="h-full flex items-center justify-center text-sm text-muted-foreground"> Loading skills... </div> ) } - // ── Compare view — unified diff ──────────────────────────────────── - if (selectedSkill && selectedSkillData && viewMode === "compare") { - return ( - <div className="h-full flex flex-col overflow-hidden"> - {/* Header */} - <div className="flex items-center gap-2 pb-3 border-b mb-3 shrink-0"> - <Button variant="ghost" size="sm" onClick={() => setViewMode("view")} className="h-7 w-7 p-0"> - <X className="size-4" /> - </Button> - <div className="flex-1 min-w-0"> - <span className="font-medium text-sm">Review Update: {selectedSkillData.title}</span> - <p className="text-xs text-muted-foreground mt-0.5"> - Changes from v{selectedSkillData.baseVersion} to v{selectedSkillData.version} - </p> - </div> - </div> - - {/* Diff */} - <div className="flex-1 min-h-0 overflow-hidden"> - <DiffView oldText={editContent} newText={officialContent} /> - </div> - - {/* Action buttons */} - <div className="flex items-center gap-2 pt-3 border-t mt-3 shrink-0"> - <Button variant="default" size="sm" onClick={handleAcceptUpdate} className="text-xs gap-1.5"> - <ArrowUpCircle className="size-3.5" /> - Accept Update - </Button> - <Button variant="outline" size="sm" onClick={handleAcceptAndRecustomize} className="text-xs gap-1.5"> - <Pencil className="size-3.5" /> - Keep Mine, Dismiss - </Button> - <Button variant="ghost" size="sm" onClick={() => setViewMode("view")} className="text-xs"> - Cancel - </Button> - </div> - </div> - ) - } - - // ── Skill detail / editor view ───────────────────────────────────── if (selectedSkill && selectedSkillData) { return ( - <div className="h-full flex flex-col overflow-hidden"> - {/* Header */} - <div className="flex items-center gap-2 pb-3 border-b mb-3 shrink-0"> - <Button variant="ghost" size="sm" onClick={handleBack} className="h-7 w-7 p-0"> - <ArrowLeft className="size-4" /> + <div className="flex flex-col h-full overflow-hidden gap-3"> + <div className="flex items-center gap-2 shrink-0"> + <Button + variant="ghost" + size="sm" + onClick={() => setSelectedSkill(null)} + className="h-8 px-2" + > + <ArrowLeft className="h-4 w-4 mr-1" /> + Back </Button> <div className="flex-1 min-w-0"> - <div className="flex items-center gap-2"> - <span className="font-medium text-sm truncate">{selectedSkillData.title}</span> - <SourceBadge source={selectedSkillData.source} baseVersion={selectedSkillData.baseVersion} /> - </div> - </div> - <div className="flex items-center gap-1.5"> - {selectedSkillData.source === "override" && viewMode !== "edit" && ( - <Button variant="ghost" size="sm" onClick={handleReset} className="h-7 text-xs gap-1"> - <RotateCcw className="size-3" /> - Reset - </Button> - )} - {viewMode !== "edit" ? ( - <Button variant="ghost" size="sm" onClick={handleCustomize} className="h-7 text-xs gap-1"> - <Pencil className="size-3" /> - {selectedSkillData.source === "override" ? "Edit" : "Customize"} - </Button> - ) : ( - <Button variant="default" size="sm" onClick={handleSave} disabled={saving} className="h-7 text-xs gap-1"> - <Save className="size-3" /> - {saving ? "Saving..." : "Save"} - </Button> - )} + <div className="font-medium text-sm truncate">{selectedSkillData.title}</div> + <div className="text-xs text-muted-foreground truncate">{selectedSkillData.summary}</div> </div> </div> - - {/* Update banner */} - {selectedSkillData.hasUpdate && viewMode !== "edit" && ( - <button - onClick={handleCompareUpdate} - className="flex items-center gap-3 px-3 py-2.5 mb-3 rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-950/30 hover:bg-amber-100 dark:hover:bg-amber-950/50 transition-colors shrink-0" - > - <ArrowUpCircle className="size-4 text-amber-600 dark:text-amber-400 shrink-0" /> - <div className="flex-1 text-left"> - <span className="text-xs font-medium text-amber-800 dark:text-amber-300"> - Official update available (v{selectedSkillData.version}) - </span> - <p className="text-[11px] text-amber-600 dark:text-amber-400 mt-0.5"> - Your version is based on v{selectedSkillData.baseVersion}. Click to review changes. - </p> - </div> - <span className="text-xs font-medium text-amber-700 dark:text-amber-300 shrink-0"> - Review → - </span> - </button> - )} - - {/* Content */} - <div className="flex-1 min-h-0 overflow-hidden"> - {viewMode === "edit" ? ( - <textarea - value={editContent} - onChange={(e) => setEditContent(e.target.value)} - className="w-full h-full resize-none bg-muted/50 rounded-md p-3 font-mono text-xs border-0 focus:outline-none focus:ring-1 focus:ring-ring" - spellCheck={false} - /> - ) : ( - <div className="h-full overflow-y-auto"> - <pre className="whitespace-pre-wrap text-xs font-mono text-muted-foreground p-3 bg-muted/30 rounded-md"> - {editContent} - </pre> - </div> - )} + <div className="flex-1 overflow-y-auto rounded-md border bg-muted/20 p-3"> + <pre className="text-xs font-mono whitespace-pre-wrap">{skillContent}</pre> </div> </div> ) } - // ── Skills list view ─────────────────────────────────────────────── return ( - <div className="h-full overflow-y-auto space-y-1"> - {skills.map((skill) => ( - <button - key={skill.id} - onClick={() => handleSelectSkill(skill.id)} - className={cn( - "w-full text-left px-3 py-2.5 rounded-md transition-colors", - "hover:bg-muted/50 border border-transparent hover:border-border", - skill.hasUpdate && "border-amber-200 dark:border-amber-800/50 bg-amber-50/50 dark:bg-amber-950/10" - )} - > - <div className="flex items-center gap-2 mb-0.5"> - <span className="text-sm font-medium truncate">{skill.title}</span> - <SourceBadge source={skill.source} baseVersion={skill.baseVersion} /> - {skill.hasUpdate && ( - <Badge className="bg-amber-500 hover:bg-amber-500 text-white text-[10px] px-1.5 py-0"> - Update - </Badge> - )} + <div className="flex flex-col h-full overflow-hidden"> + <p className="text-xs text-muted-foreground shrink-0 mb-3"> + Skills are read-only guidance bundled with the app. Updates ship with new app releases. + </p> + <div className="flex-1 overflow-y-auto -mx-1 px-1 space-y-1"> + {skills.length === 0 ? ( + <div className="text-sm text-muted-foreground text-center py-8"> + No skills available. </div> - <p className="text-xs text-muted-foreground line-clamp-1">{skill.summary}</p> - </button> - ))} + ) : ( + skills.map((skill) => ( + <button + key={skill.id} + onClick={() => handleSelectSkill(skill.id)} + className={cn( + "w-full text-left p-3 rounded-md border bg-card hover:bg-accent transition-colors", + )} + > + <div className="font-medium text-sm">{skill.title}</div> + <div className="text-xs text-muted-foreground mt-0.5 line-clamp-2"> + {skill.summary} + </div> + </button> + )) + )} + </div> </div> ) } - -function SourceBadge({ source, baseVersion }: { source: string; baseVersion?: string }) { - if (source === "override") { - return ( - <Badge variant="secondary" className="text-[10px] px-1.5 py-0"> - Customized{baseVersion ? ` from v${baseVersion}` : ""} - </Badge> - ) - } - return null -} diff --git a/apps/x/packages/core/src/application/assistant/skills/browser-control/skill.ts b/apps/x/packages/core/src/application/assistant/skills/browser-control/skill.ts deleted file mode 100644 index f1c06f0c..00000000 --- a/apps/x/packages/core/src/application/assistant/skills/browser-control/skill.ts +++ /dev/null @@ -1,106 +0,0 @@ -export const skill = String.raw` -# Browser Control Skill - -You have access to the **browser-control** tool, which controls Rowboat's embedded browser pane directly. - -Use this skill when the user asks you to open a website, browse in-app, search the web in the browser pane, click something on a page, fill a form, or otherwise interact with a live webpage inside Rowboat. - -## Core Workflow - -1. Start with ` + "`browser-control({ action: \"open\" })`" + ` if the browser pane may not already be open. -2. Use ` + "`browser-control({ action: \"read-page\" })`" + ` to inspect the current page. -3. The tool returns: - - ` + "`snapshotId`" + ` - - page ` + "`url`" + ` and ` + "`title`" + ` - - visible page text - - interactable elements with numbered ` + "`index`" + ` values -4. Prefer acting on those numbered indices with ` + "`click`" + ` / ` + "`type`" + ` / ` + "`press`" + `. -5. After each action, read the returned page snapshot before deciding the next step. - -## Actions - -### open -Open the browser pane and ensure an active tab exists. - -### get-state -Return the current browser tabs and active tab id. - -### new-tab -Open a new browser tab. - -Parameters: -- ` + "`target`" + ` (optional): URL or plain-language search query - -### switch-tab -Switch to a tab by ` + "`tabId`" + `. - -### close-tab -Close a tab by ` + "`tabId`" + `. - -### navigate -Navigate the active tab. - -Parameters: -- ` + "`target`" + `: URL or plain-language search query - -Plain-language targets are converted into a search automatically. - -### back / forward / reload -Standard browser navigation controls. - -### read-page -Read the current page and return a compact snapshot. - -Parameters: -- ` + "`maxElements`" + ` (optional) -- ` + "`maxTextLength`" + ` (optional) - -### click -Click an element. - -Prefer: -- ` + "`index`" + `: element index from ` + "`read-page`" + ` - -Optional: -- ` + "`snapshotId`" + `: include it when acting on a recent snapshot -- ` + "`selector`" + `: fallback only when no usable index exists - -### type -Type into an input, textarea, or contenteditable element. - -Parameters: -- ` + "`text`" + `: text to enter -- plus the same target fields as ` + "`click`" + ` - -### press -Send a key press such as ` + "`Enter`" + `, ` + "`Tab`" + `, ` + "`Escape`" + `, or arrow keys. - -Parameters: -- ` + "`key`" + ` -- optional target fields if you need to focus a specific element first - -### scroll -Scroll the current page. - -Parameters: -- ` + "`direction`" + `: ` + "`\"up\"`" + ` or ` + "`\"down\"`" + ` (optional; defaults down) -- ` + "`amount`" + `: pixel distance (optional) - -### wait -Wait for the page to settle, useful after async UI changes. - -Parameters: -- ` + "`ms`" + `: milliseconds to wait (optional) - -## Important Rules - -- Prefer ` + "`read-page`" + ` before interacting. -- Prefer element ` + "`index`" + ` over CSS selectors. -- If the tool says the snapshot is stale, call ` + "`read-page`" + ` again. -- After navigation, clicking, typing, pressing, or scrolling, use the returned page snapshot instead of assuming the page state. -- Use Rowboat's browser for live interaction. Use web search tools for research where a live session is unnecessary. -- Do not wrap browser URLs or browser pages in ` + "```filepath" + ` blocks. Filepath cards are only for real files on disk, not web pages or browser tabs. -- If you mention a page the browser opened, use plain text for the URL/title instead of trying to create a clickable file card. -`; - -export default skill; diff --git a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts b/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts deleted file mode 100644 index ff345acf..00000000 --- a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { z } from 'zod'; -import { stringify as stringifyYaml } from 'yaml'; -import { TrackBlockSchema } from '@x/shared/dist/track-block.js'; - -const schemaYaml = stringifyYaml(z.toJSONSchema(TrackBlockSchema)).trimEnd(); - -const richBlockMenu = `**5. Rich block render — when the data has a natural visual form.** - -The track agent can emit *rich blocks* — special fenced blocks the editor renders as styled UI (charts, calendars, embedded iframes, etc.). When the data fits one of these shapes, instruct the agent explicitly so it doesn't fall back to plain markdown: - -- \`table\` — multi-row data, scoreboards, leaderboards. *"Render as a \`table\` block with columns Rank, Title, Points, Comments."* -- \`chart\` — time series, breakdowns, share-of-total. *"Render as a \`chart\` block (line, bar, or pie) with x=date, y=rate."* -- \`mermaid\` — flowcharts, sequence/relationship diagrams, gantt charts. *"Render as a \`mermaid\` diagram."* -- \`calendar\` — upcoming events / agenda. *"Render as a \`calendar\` block."* -- \`email\` — single email thread digest (subject, from, summary, latest body, optional draft). *"Render the most important unanswered thread as an \`email\` block."* -- \`image\` — single image with caption. *"Render as an \`image\` block."* -- \`embed\` — YouTube or Figma. *"Render as an \`embed\` block."* -- \`iframe\` — live dashboards, status pages, anything that benefits from being live not snapshotted. *"Render as an \`iframe\` block pointing to <url>."* -- \`transcript\` — long meeting transcripts (collapsible). *"Render as a \`transcript\` block."* -- \`prompt\` — a "next step" Copilot card the user can click to start a chat. *"End with a \`prompt\` block labeled '<short label>' that runs '<longer prompt to send to Copilot>'."* - -You **do not** need to write the block body yourself — describe the desired output in the instruction and the track agent will format it (it knows each block's exact schema). Avoid \`track\` and \`task\` block types — those are user-authored input, not agent output. - -- Good: "Show today's calendar events. Render as a \`calendar\` block with \`showJoinButton: true\`." -- Good: "Plot USD/INR over the last 7 days as a \`chart\` block — line chart, x=date, y=rate." -- Bad: "Show today's calendar." (vague — agent may produce a markdown bullet list when the user wants the rich block)`; - -export const skill = String.raw` -# Tracks Skill - -You are helping the user create and manage **track blocks** — YAML-fenced, auto-updating content blocks embedded in notes. Load this skill whenever the user wants to track, monitor, watch, or keep an eye on something in a note, asks for recurring/auto-refreshing content ("every morning...", "show current...", "pin live X here"), or presses Cmd+K and requests auto-updating content at the cursor. - -## First: Just Do It — Do Not Ask About Edit Mode - -Track creation and editing are **action-first**. When the user asks to track, monitor, watch, or pin auto-updating content, you proceed directly — read the file, construct the block, ` + "`" + `workspace-edit` + "`" + ` it in. Do not ask "Should I make edits directly, or show you changes first for approval?" — that prompt belongs to generic document editing, not to tracks. - -- If another skill or an earlier turn already asked about edit mode and is waiting, treat the user's track request as implicit "direct mode" and proceed. -- You may still ask **one** short clarifying question when genuinely ambiguous (e.g. which note to add it to). Not about permission to edit. -- The Suggested Topics flow below is the one first-turn-confirmation exception — leave it intact. - -## What Is a Track Block - -A track block is a scheduled, agent-run block embedded directly inside a markdown note. Each block has: -- A YAML-fenced ` + "`" + `track` + "`" + ` block that defines the instruction, schedule, and metadata. -- A sibling "target region" — an HTML-comment-fenced area where the generated output lives. The runner rewrites the target region on each scheduled run. - -**Concrete example** (a track that shows the current time in Chicago every hour): - -` + "```" + `track -trackId: chicago-time -instruction: | - Show the current time in Chicago, IL in 12-hour format. -active: true -schedule: - type: cron - expression: "0 * * * *" -` + "```" + ` - -<!--track-target:chicago-time--> -<!--/track-target:chicago-time--> - -Good use cases: -- Weather / air quality for a location -- News digests or headlines -- Stock or crypto prices -- Sports scores -- Service status pages -- Personal dashboards (today's calendar, steps, focus stats) -- Any recurring summary that decays fast - -## Anatomy - -Each track has two parts that live next to each other in the note: - -1. The ` + "`" + `track` + "`" + ` code fence — contains the YAML config. The fence language tag is literally ` + "`" + `track` + "`" + `. -2. The target-comment region — ` + "`" + `<!--track-target:ID-->` + "`" + ` and ` + "`" + `<!--/track-target:ID-->` + "`" + ` with optional content between. The ID must match the ` + "`" + `trackId` + "`" + ` in the YAML. - -The target region is **sibling**, not nested. It must **never** live inside the ` + "`" + "```" + `track` + "`" + ` fence. - -## Canonical Schema - -Below is the authoritative schema for a track block (generated at build time from the TypeScript source — never out of date). Use it to validate every field name, type, and constraint before writing YAML: - -` + "```" + `yaml -${schemaYaml} -` + "```" + ` - -**Runtime-managed fields — never write these yourself:** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `. - -## Choosing a trackId - -- Kebab-case, short, descriptive: ` + "`" + `chicago-time` + "`" + `, ` + "`" + `sfo-weather` + "`" + `, ` + "`" + `hn-top5` + "`" + `, ` + "`" + `btc-usd` + "`" + `. -- **Must be unique within the note file.** Before inserting, read the file and check: - - All existing ` + "`" + `trackId:` + "`" + ` lines in ` + "`" + "```" + `track` + "`" + ` blocks - - All existing ` + "`" + `<!--track-target:...-->` + "`" + ` comments -- If you need disambiguation, add scope: ` + "`" + `btc-price-usd` + "`" + `, ` + "`" + `weather-home` + "`" + `, ` + "`" + `news-ai-2` + "`" + `. -- Don't reuse an old ID even if the previous block was deleted — pick a fresh one. - -## Writing a Good Instruction - -### The Frame: This Is a Personal Knowledge Tracker - -Track output lives in a personal knowledge base the user scans frequently. Aim for data-forward, scannable output — the answer to "what's current / what changed?" in the fewest words that carry real information. Not prose. Not decoration. - -### Core Rules - -- **Specific and actionable.** State exactly what to fetch or compute. -- **Single-focus.** One block = one purpose. Split "weather + news + stocks" into three blocks, don't bundle. -- **Imperative voice, 1-3 sentences.** -- **Specify output shape.** Describe it concretely: "one line: ` + "`" + `<temp>°F, <conditions>` + "`" + `", "3-column markdown table", "bulleted digest of 5 items". - -### Self-Sufficiency (critical) - -The instruction runs later, in a background scheduler, with **no chat context and no memory of this conversation**. It must stand alone. - -**Never use phrases that depend on prior conversation or prior runs:** -- "as before", "same style as before", "like last time" -- "keep the format we discussed", "matching the previous output" -- "continue from where you left off" (without stating the state) - -If you want consistent style across runs, **describe the style inline** (e.g. "a 3-column markdown table with headers ` + "`" + `Location` + "`" + `, ` + "`" + `Local Time` + "`" + `, ` + "`" + `Offset` + "`" + `"; "a one-line status: HH:MM, conditions, temp"). The track agent only sees your instruction — not this chat, not what you produced last time. - -### Output Patterns — Match the Data - -Pick a shape that fits what the user is tracking. Five common patterns — the first four are plain markdown; the fifth is a rich rendered block: - -**1. Single metric / status line.** -- Good: "Fetch USD/INR. Return one line: ` + "`" + `USD/INR: <rate> (as of <HH:MM IST>)` + "`" + `." -- Bad: "Give me a nice update about the dollar rate." - -**2. Compact table.** -- Good: "Show current local time for India, Chicago, Indianapolis as a 3-column markdown table: ` + "`" + `Location | Local Time | Offset vs India` + "`" + `. One row per location, no prose." -- Bad: "Show a polished, table-first world clock with a pleasant layout." - -**3. Rolling digest.** -- Good: "Summarize the top 5 HN front-page stories as bullets: ` + "`" + `- <title> (<points> pts, <comments> comments)` + "`" + `. No commentary." -- Bad: "Give me the top HN stories with thoughtful takeaways." - -**4. Status / threshold watch.** -- Good: "Check https://status.example.com. Return one line: ` + "`" + `✓ All systems operational` + "`" + ` or ` + "`" + `⚠ <component>: <status>` + "`" + `. If degraded, add one bullet per affected component." -- Bad: "Keep an eye on the status page and tell me how it looks." - -${richBlockMenu} - -### Anti-Patterns - -- **Decorative adjectives** describing the output: "polished", "clean", "beautiful", "pleasant", "nicely formatted" — they tell the agent nothing concrete. -- **References to past state** without a mechanism to access it ("as before", "same as last time"). -- **Bundling multiple purposes** into one instruction — split into separate track blocks. -- **Open-ended prose requests** ("tell me about X", "give me thoughts on X"). -- **Output-shape words without a concrete shape** ("dashboard-like", "report-style"). - -## YAML String Style (critical — read before writing any ` + "`" + `instruction` + "`" + ` or ` + "`" + `eventMatchCriteria` + "`" + `) - -The two free-form fields — ` + "`" + `instruction` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + ` — are where YAML parsing usually breaks. The runner re-emits the full YAML block every time it writes ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `, etc., and the YAML library may re-flow long plain (unquoted) strings onto multiple lines. Once that happens, any ` + "`" + `:` + "`" + ` **followed by a space** inside the value silently corrupts the block: YAML interprets the ` + "`" + `:` + "`" + ` as a new key/value separator and the instruction gets truncated. - -Real failure seen in the wild — an instruction containing the phrase ` + "`" + `"polished UI style as before: clean, compact..."` + "`" + ` was written as a plain scalar, got re-emitted across multiple lines on the next run, and the ` + "`" + `as before:` + "`" + ` became a phantom key. The block parsed as garbage after that. - -### The rule: always use a safe scalar style - -**Default to the literal block scalar (` + "`" + `|` + "`" + `) for ` + "`" + `instruction` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + `, every time.** It is the only style that is robust across the full range of punctuation these fields typically contain, and it is safe even if the content later grows to multiple lines. - -### Preferred: literal block scalar (` + "`" + `|` + "`" + `) - -` + "```" + `yaml -instruction: | - Show current local time for India, Chicago, and Indianapolis as a - 3-column markdown table: Location | Local Time | Offset vs India. - One row per location, 24-hour time (HH:MM), no extra prose. - Note: when a location is in DST, reflect that in the offset column. -eventMatchCriteria: | - Emails from the finance team about Q3 budget or OKRs. -` + "```" + ` - -- ` + "`" + `|` + "`" + ` preserves line breaks verbatim. Colons, ` + "`" + `#` + "`" + `, quotes, leading ` + "`" + `-` + "`" + `, percent signs — all literal. No escaping needed. -- **Indent every content line by 2 spaces** relative to the key (` + "`" + `instruction:` + "`" + `). Use spaces, never tabs. -- Leave a real newline after ` + "`" + `|` + "`" + ` — content starts on the next line, not the same line. -- Default chomping (no modifier) is fine. Do **not** add ` + "`" + `-` + "`" + ` or ` + "`" + `+` + "`" + ` unless you know you need them. -- A ` + "`" + `|` + "`" + ` block is terminated by a line indented less than the content — typically the next sibling key (` + "`" + `active:` + "`" + `, ` + "`" + `schedule:` + "`" + `). - -### Acceptable alternative: double-quoted on a single line - -Fine for short single-sentence fields with no newline needs: - -` + "```" + `yaml -instruction: "Show the current time in Chicago, IL in 12-hour format." -eventMatchCriteria: "Emails about Q3 planning, OKRs, or roadmap decisions." -` + "```" + ` - -- Escape ` + "`" + `"` + "`" + ` as ` + "`" + `\"` + "`" + ` and backslash as ` + "`" + `\\` + "`" + `. -- Prefer ` + "`" + `|` + "`" + ` the moment the string needs two sentences or a newline. - -### Single-quoted on a single line (only if double-quoted would require heavy escaping) - -` + "```" + `yaml -instruction: 'He said "hi" at 9:00.' -` + "```" + ` - -- A literal single quote is escaped by doubling it: ` + "`" + `'it''s fine'` + "`" + `. -- No other escape sequences work. - -### Do NOT use plain (unquoted) scalars for these two fields - -Even if the current value looks safe, a future edit (by you or the user) may introduce a ` + "`" + `:` + "`" + ` or ` + "`" + `#` + "`" + `, and a future re-emit may fold the line. The ` + "`" + `|` + "`" + ` style is safe under **all** future edits — plain scalars are not. - -### Editing an existing track - -If you ` + "`" + `workspace-edit` + "`" + ` an existing track's ` + "`" + `instruction` + "`" + ` or ` + "`" + `eventMatchCriteria` + "`" + ` and find it is still a plain scalar, **upgrade it to ` + "`" + `|` + "`" + `** in the same edit. Don't leave a plain scalar behind that the next run will corrupt. - -### Never-hand-write fields - -` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + ` are owned by the runner. Don't touch them — don't even try to style them. If your ` + "`" + `workspace-edit` + "`" + `'s ` + "`" + `oldString` + "`" + ` happens to include these lines, copy them byte-for-byte into ` + "`" + `newString` + "`" + ` unchanged. - -## Schedules - -Schedule is an **optional** discriminated union. Three types: - -### ` + "`" + `cron` + "`" + ` — recurring at exact times - -` + "```" + `yaml -schedule: - type: cron - expression: "0 * * * *" -` + "```" + ` - -Fires at the exact cron time. Use when the user wants precise timing ("at 9am daily", "every hour on the hour"). - -### ` + "`" + `window` + "`" + ` — recurring within a time-of-day range - -` + "```" + `yaml -schedule: - type: window - cron: "0 0 * * 1-5" - startTime: "09:00" - endTime: "17:00" -` + "```" + ` - -Fires **at most once per cron occurrence**, but only if the current time is within ` + "`" + `startTime` + "`" + `–` + "`" + `endTime` + "`" + ` (24-hour HH:MM, local). Use when the user wants "sometime in the morning" or "once per weekday during work hours" — flexible timing with bounds. - -### ` + "`" + `once` + "`" + ` — one-shot at a future time - -` + "```" + `yaml -schedule: - type: once - runAt: "2026-04-14T09:00:00" -` + "```" + ` - -Fires once at ` + "`" + `runAt` + "`" + ` and never again. Local time, no ` + "`" + `Z` + "`" + ` suffix. - -### Cron cookbook - -- ` + "`" + `"*/15 * * * *"` + "`" + ` — every 15 minutes -- ` + "`" + `"0 * * * *"` + "`" + ` — every hour on the hour -- ` + "`" + `"0 8 * * *"` + "`" + ` — daily at 8am -- ` + "`" + `"0 9 * * 1-5"` + "`" + ` — weekdays at 9am -- ` + "`" + `"0 0 * * 0"` + "`" + ` — Sundays at midnight -- ` + "`" + `"0 0 1 * *"` + "`" + ` — first of month at midnight - -**Omit ` + "`" + `schedule` + "`" + ` entirely for a manual-only track** — the user triggers it via the Play button in the UI. - -## Event Triggers (third trigger type) - -In addition to manual and scheduled, a track can be triggered by **events** — incoming signals from the user's data sources (currently: gmail emails). Set ` + "`" + `eventMatchCriteria` + "`" + ` to a description of what kinds of events should consider this track for an update: - -` + "```" + `track -trackId: q3-planning-emails -instruction: | - Maintain a running summary of decisions and open questions about Q3 - planning, drawn from emails on the topic. -active: true -eventMatchCriteria: | - Emails about Q3 planning, roadmap decisions, or quarterly OKRs. -` + "```" + ` - -How it works: -1. When a new event arrives (e.g. an email syncs), a fast LLM classifier checks ` + "`" + `eventMatchCriteria` + "`" + ` against the event content. -2. If it might match, the track-run agent receives both the event payload and the existing track content, and decides whether to actually update. -3. If the event isn't truly relevant on closer inspection, the agent skips the update — no fabricated content. - -When to suggest event triggers: -- The user wants to **maintain a living summary** of a topic ("keep notes on everything related to project X"). -- The content depends on **incoming signals** rather than periodic refresh ("update this whenever a relevant email arrives"). -- Mention to the user: scheduled (cron) is for time-driven updates; event is for signal-driven updates. They can be combined — a track can have both a ` + "`" + `schedule` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + ` (it'll run on schedule AND on relevant events). - -Writing good ` + "`" + `eventMatchCriteria` + "`" + `: -- Be descriptive but not overly narrow — Pass 1 routing is liberal by design. -- Examples: ` + "`" + `"Emails from John about the migration project"` + "`" + `, ` + "`" + `"Calendar events related to customer interviews"` + "`" + `, ` + "`" + `"Meeting notes that mention pricing changes"` + "`" + `. - -Tracks **without** ` + "`" + `eventMatchCriteria` + "`" + ` opt out of events entirely — they'll only run on schedule or manually. - -## Insertion Workflow - -**Reminder:** once you have enough to act, act. Do not pause to ask about edit mode. - -### Cmd+K with cursor context - -When the user invokes Cmd+K, the context includes an attachment mention like: -> User has attached the following files: -> - notes.md (text/markdown) at knowledge/notes.md (line 42) - -Workflow: -1. Extract the ` + "`" + `path` + "`" + ` and ` + "`" + `line N` + "`" + ` from the attachment. -2. ` + "`" + `workspace-readFile({ path })` + "`" + ` — always re-read fresh. -3. Check existing ` + "`" + `trackId` + "`" + `s in the file to guarantee uniqueness. -4. Locate the line. Pick a **unique 2-3 line anchor** around line N (a full heading, a distinctive sentence). Avoid blank lines and generic text. -5. Construct the full track block (YAML + target pair). -6. ` + "`" + `workspace-edit({ path, oldString: <anchor>, newString: <anchor with block spliced at line N> })` + "`" + `. - -### Sidebar chat with a specific note - -1. If a file is mentioned/attached, read it. -2. If ambiguous, ask one question: "Which note should I add the track to?" -3. **Default placement: append** to the end of the file. Find the last non-empty line as the anchor. ` + "`" + `newString` + "`" + ` = that line + ` + "`" + `\n\n` + "`" + ` + track block + target pair. -4. If the user specified a section ("under the Weather heading"), anchor on that heading. - -### No note context at all - -Ask one question: "Which note should this track live in?" Don't create a new note unless the user asks. - -### Suggested Topics exploration flow - -Sometimes the user arrives from the Suggested Topics panel and gives you a prompt like: -- "I am exploring a suggested topic card from the Suggested Topics panel." -- a title, category, description, and target folder such as ` + "`" + `knowledge/Topics/` + "`" + ` or ` + "`" + `knowledge/People/` + "`" + ` - -In that flow: -1. On the first turn, **do not create or modify anything yet**. Briefly explain the tracking note you can set up and ask for confirmation. -2. If the user clearly confirms ("yes", "set it up", "do it"), treat that as explicit permission to proceed. -3. Before creating a new note, search the target folder for an existing matching note and update it if one already exists. -4. If no matching note exists and the prompt gave you a target folder, create the new note there without bouncing back to ask "which note should this live in?". -5. Use the card title as the default note title / filename unless a small normalization is clearly needed. -6. Keep the surrounding note scaffolding minimal but useful. The track block should be the core of the note. -7. If the target folder is one of the structured knowledge folders (` + "`" + `knowledge/People/` + "`" + `, ` + "`" + `knowledge/Organizations/` + "`" + `, ` + "`" + `knowledge/Projects/` + "`" + `, ` + "`" + `knowledge/Topics/` + "`" + `), mirror the local note style by quickly checking a nearby note or config before writing if needed. - -## The Exact Text to Insert - -Write it verbatim like this (including the blank line between fence and target): - -` + "```" + `track -trackId: <id> -instruction: | - <instruction, indented 2 spaces, may span multiple lines> -active: true -schedule: - type: cron - expression: "0 * * * *" -` + "```" + ` - -<!--track-target:<id>--> -<!--/track-target:<id>--> - -**Rules:** -- One blank line between the closing ` + "`" + "```" + `" + " fence and the ` + "`" + `<!--track-target:ID-->` + "`" + `. -- Target pair is **empty on creation**. The runner fills it on the first run. -- **Always use the literal block scalar (` + "`" + `|` + "`" + `)** for ` + "`" + `instruction` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + `, indented 2 spaces. Never a plain (unquoted) scalar — see the YAML String Style section above for why. -- **Always quote cron expressions** in YAML — they contain spaces and ` + "`" + `*` + "`" + `. -- Use 2-space YAML indent. No tabs. -- Top-level markdown only — never inside a code fence, blockquote, or table. - -## After Insertion - -- Confirm in one line: "Added ` + "`" + `chicago-time` + "`" + ` track, refreshing hourly." -- **Then offer to run it once now** (see "Running a Track" below) — especially valuable for newly created blocks where the target region is otherwise empty until the next scheduled or event-triggered run. -- **Do not** write anything into the ` + "`" + `<!--track-target:...-->` + "`" + ` region yourself — use the ` + "`" + `run-track-block` + "`" + ` tool to delegate to the track agent. - -## Running a Track (the ` + "`" + `run-track-block` + "`" + ` tool) - -The ` + "`" + `run-track-block` + "`" + ` tool manually triggers a track run right now. Equivalent to the user clicking the Play button — but you can pass extra ` + "`" + `context` + "`" + ` to bias what the track agent does on this single run (without modifying the block's ` + "`" + `instruction` + "`" + `). - -### When to proactively offer to run - -These are upsells — ask first, don't run silently. - -- **Just created a new track block.** Before declaring done, offer: - > "Want me to run it once now to seed the initial content?" - - This is **especially valuable for event-triggered tracks** (with ` + "`" + `eventMatchCriteria` + "`" + `) — otherwise the target region stays empty until the next matching event arrives. - - For tracks that pull from existing local data (synced emails, calendar, meeting notes), suggest a **backfill** with explicit context (see below). - -- **Just edited an existing track.** Offer: - > "Want me to run it now to see the updated output?" - -- **Explicit user request.** "run the X track", "test it", "refresh that block" → call the tool directly. - -### Using the ` + "`" + `context` + "`" + ` parameter (the powerful case) - -The ` + "`" + `context` + "`" + ` parameter is extra guidance for the track agent on this run only. It's the difference between a stock refresh and a smart backfill. - -**Examples:** - -- New track: "Track emails about Q3 planning" → after creating it, run with: - > context: "Initial backfill — scan ` + "`" + `gmail_sync/` + "`" + ` for emails from the last 90 days that match this track's topic (Q3 planning, OKRs, roadmap), and synthesize the initial summary." - -- New track: "Summarize this week's customer calls" → run with: - > context: "Backfill from this week's meeting notes in ` + "`" + `granola_sync/` + "`" + ` and ` + "`" + `fireflies_sync/` + "`" + `." - -- Manual refresh after the user mentions a recent change: - > context: "Focus on changes from the last 7 days only." - -- Plain refresh (user says "run it now"): **omit ` + "`" + `context` + "`" + ` entirely**. Don't invent context — it can mislead the agent. - -### What to do with the result - -The tool returns ` + "`" + `{ success, runId, action, summary, contentAfter, error }` + "`" + `: - -- **` + "`" + `action: 'replace'` + "`" + `** → the track was updated. Confirm with one line, optionally citing the first line of ` + "`" + `contentAfter` + "`" + `: - > "Done — track now shows: 72°F, partly cloudy in Chicago." - -- **` + "`" + `action: 'no_update'` + "`" + `** → the agent decided nothing needed to change. Tell the user briefly; ` + "`" + `summary` + "`" + ` may explain why. - -- **` + "`" + `error` + "`" + ` set** → surface it concisely. If the error is ` + "`" + `'Already running'` + "`" + ` (concurrency guard), let the user know the track is mid-run and to retry shortly. - -### Don'ts - -- **Don't auto-run** after every edit — ask first. -- **Don't pass ` + "`" + `context` + "`" + `** for a plain refresh — only when there's specific extra guidance to give. -- **Don't use ` + "`" + `run-track-block` + "`" + ` to manually write content** — that's ` + "`" + `update-track-content` + "`" + `'s job (and even that should be rare; the track agent handles content via this tool). -- **Don't ` + "`" + `run-track-block` + "`" + ` repeatedly** in a single turn — one run per user-facing action. - -## Proactive Suggestions - -When the user signals interest in recurring or time-decaying info, **offer a track block** instead of a one-off answer. Signals: -- "I want to track / monitor / watch / keep an eye on / follow X" -- "Can you check on X every morning / hourly / weekly?" -- The user just asked a one-off question whose answer decays (weather, score, price, status, news). -- The user is building a time-sensitive page (weekly dashboard, morning briefing). - -Suggestion style — one line, concrete: -> "I can turn this into a track block that refreshes hourly — want that?" - -Don't upsell aggressively. If the user clearly wants a one-off answer, give them one. - -## Don'ts - -- **Don't reuse** an existing ` + "`" + `trackId` + "`" + ` in the same file. -- **Don't add ` + "`" + `schedule` + "`" + `** if the user explicitly wants a manual-only track. -- **Don't write** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, or ` + "`" + `lastRunSummary` + "`" + ` — runtime-managed. -- **Don't nest** the ` + "`" + `<!--track-target:ID-->` + "`" + ` region inside the ` + "`" + "```" + `track` + "`" + ` fence. -- **Don't touch** content between ` + "`" + `<!--track-target:ID-->` + "`" + ` and ` + "`" + `<!--/track-target:ID-->` + "`" + ` — that's generated content. -- **Don't schedule** with ` + "`" + `"* * * * *"` + "`" + ` (every minute) unless the user explicitly asks. -- **Don't add a ` + "`" + `Z` + "`" + ` suffix** on ` + "`" + `runAt` + "`" + ` — local time only. -- **Don't use ` + "`" + `workspace-writeFile` + "`" + `** to rewrite the whole file — always ` + "`" + `workspace-edit` + "`" + ` with a unique anchor. - -## Editing or Removing an Existing Track - -**Change schedule or instruction:** read the file, ` + "`" + `workspace-edit` + "`" + ` the YAML body. Anchor on the unique ` + "`" + `trackId: <id>` + "`" + ` line plus a few surrounding lines. - -**Pause without deleting:** flip ` + "`" + `active: false` + "`" + `. - -**Remove entirely:** ` + "`" + `workspace-edit` + "`" + ` with ` + "`" + `oldString` + "`" + ` = the full ` + "`" + "```" + `track` + "`" + ` block **plus** the target pair (so generated content also disappears), ` + "`" + `newString` + "`" + ` = empty. - -## Quick Reference - -Minimal template: - -` + "```" + `track -trackId: <kebab-id> -instruction: | - <what to produce — always use ` + "`" + `|` + "`" + `, indented 2 spaces> -active: true -schedule: - type: cron - expression: "0 * * * *" -` + "```" + ` - -<!--track-target:<kebab-id>--> -<!--/track-target:<kebab-id>--> - -Top cron expressions: ` + "`" + `"0 * * * *"` + "`" + ` (hourly), ` + "`" + `"0 8 * * *"` + "`" + ` (daily 8am), ` + "`" + `"0 9 * * 1-5"` + "`" + ` (weekdays 9am), ` + "`" + `"*/15 * * * *"` + "`" + ` (every 15m). - -YAML style reminder: ` + "`" + `instruction` + "`" + ` and ` + "`" + `eventMatchCriteria` + "`" + ` are **always** ` + "`" + `|` + "`" + ` block scalars. Never plain. Never leave a plain scalar in place when editing. -`; - -export default skill; diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 7bad538a..7c296426 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -87,7 +87,6 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = { return { success: true, skillName: resolved.id, - source: resolved.source, content: resolved.content, }; }, diff --git a/apps/x/packages/core/src/config/config.ts b/apps/x/packages/core/src/config/config.ts index f3fdb208..3d320172 100644 --- a/apps/x/packages/core/src/config/config.ts +++ b/apps/x/packages/core/src/config/config.ts @@ -33,8 +33,6 @@ function ensureDirs() { ensure(path.join(WorkDir, "agents")); ensure(path.join(WorkDir, "config")); ensure(path.join(WorkDir, "knowledge")); - ensure(path.join(WorkDir, "skills", "overrides")); - ensure(path.join(WorkDir, "skills", "official")); } function ensureDefaultConfigs() { diff --git a/apps/x/packages/core/src/di/container.ts b/apps/x/packages/core/src/di/container.ts index 810f4884..08c40134 100644 --- a/apps/x/packages/core/src/di/container.ts +++ b/apps/x/packages/core/src/di/container.ts @@ -15,7 +15,6 @@ import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js"; import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js"; import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js"; -import { FSSkillsRepo, ISkillsRepo } from "../skills/repo.js"; import { FSOfficialSkillsRepo, IOfficialSkillsRepo } from "../skills/official-repo.js"; import { SkillResolver, ISkillResolver } from "../skills/resolver.js"; import type { IBrowserControlService } from "../application/browser-control/service.js"; @@ -43,7 +42,6 @@ container.register({ agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(), agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(), slackConfigRepo: asClass<ISlackConfigRepo>(FSSlackConfigRepo).singleton(), - skillsRepo: asClass<ISkillsRepo>(FSSkillsRepo).singleton(), officialSkillsRepo: asClass<IOfficialSkillsRepo>(FSOfficialSkillsRepo).singleton(), skillResolver: asClass<ISkillResolver>(SkillResolver).singleton(), }); @@ -55,3 +53,9 @@ export function registerBrowserControlService(service: IBrowserControlService): browserControlService: asValue(service), }); } + +export function registerSkillsDir(skillsDir: string): void { + container.register({ + skillsDir: asValue(skillsDir), + }); +} diff --git a/apps/x/packages/core/src/skills/official-repo.ts b/apps/x/packages/core/src/skills/official-repo.ts index 22731493..73c4acf1 100644 --- a/apps/x/packages/core/src/skills/official-repo.ts +++ b/apps/x/packages/core/src/skills/official-repo.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { WorkDir } from "../config/config.js"; import { parseSkillMd } from "./skill-md-parser.js"; import type { SkillDefinition } from "./types.js"; @@ -10,7 +9,11 @@ export interface IOfficialSkillsRepo { } export class FSOfficialSkillsRepo implements IOfficialSkillsRepo { - private readonly officialDir = path.join(WorkDir, "skills", "official"); + private readonly officialDir: string; + + constructor({ skillsDir }: { skillsDir: string }) { + this.officialDir = skillsDir; + } async listOfficial(): Promise<SkillDefinition[]> { const result: SkillDefinition[] = []; diff --git a/apps/x/packages/core/src/skills/placeholders.ts b/apps/x/packages/core/src/skills/placeholders.ts new file mode 100644 index 00000000..f485f7c5 --- /dev/null +++ b/apps/x/packages/core/src/skills/placeholders.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { stringify as stringifyYaml } from "yaml"; +import { TrackBlockSchema } from "@x/shared/dist/track-block.js"; + +// Lazily computed so we don't pay the cost unless a skill actually uses the placeholder. +const renderers: Record<string, () => string> = { + TRACK_BLOCK_SCHEMA: () => stringifyYaml(z.toJSONSchema(TrackBlockSchema)).trimEnd(), +}; + +const PLACEHOLDER = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g; + +export function substitutePlaceholders(content: string): string { + return content.replace(PLACEHOLDER, (match, key) => { + const renderer = renderers[key]; + if (!renderer) return match; + try { + return renderer(); + } catch (err) { + console.error(`[skills] placeholder ${key} failed:`, err); + return match; + } + }); +} diff --git a/apps/x/packages/core/src/skills/repo.ts b/apps/x/packages/core/src/skills/repo.ts deleted file mode 100644 index 139072f7..00000000 --- a/apps/x/packages/core/src/skills/repo.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { WorkDir } from "../config/config.js"; -import fs from "fs/promises"; -import path from "path"; -import { parse, stringify } from "yaml"; -import { SkillOverride, SkillOverrideEntry } from "@x/shared/dist/skill.js"; - -export interface ISkillsRepo { - listOverrides(): Promise<SkillOverrideEntry[]>; - getOverride(skillId: string): Promise<SkillOverrideEntry | null>; - saveOverride(skillId: string, meta: SkillOverride, content: string): Promise<void>; - deleteOverride(skillId: string): Promise<void>; -} - -export class FSSkillsRepo implements ISkillsRepo { - private readonly overridesDir = path.join(WorkDir, "skills", "overrides"); - - async listOverrides(): Promise<SkillOverrideEntry[]> { - const result: SkillOverrideEntry[] = []; - let files: string[]; - try { - files = await fs.readdir(this.overridesDir); - } catch { - return result; - } - - for (const file of files) { - if (!file.endsWith(".md")) continue; - try { - const entry = await this.parseOverrideMd(path.join(this.overridesDir, file)); - result.push(entry); - } catch (error) { - console.error(`Error parsing skill override ${file}: ${error instanceof Error ? error.message : String(error)}`); - } - } - return result; - } - - async getOverride(skillId: string): Promise<SkillOverrideEntry | null> { - const filePath = path.join(this.overridesDir, `${skillId}.md`); - try { - await fs.access(filePath); - return await this.parseOverrideMd(filePath); - } catch { - return null; - } - } - - async saveOverride(skillId: string, meta: SkillOverride, content: string): Promise<void> { - await fs.mkdir(this.overridesDir, { recursive: true }); - const frontmatter = stringify(meta); - const fileContent = `---\n${frontmatter}---\n${content}`; - await fs.writeFile(path.join(this.overridesDir, `${skillId}.md`), fileContent); - } - - async deleteOverride(skillId: string): Promise<void> { - const filePath = path.join(this.overridesDir, `${skillId}.md`); - try { - await fs.unlink(filePath); - } catch { - // File doesn't exist, nothing to delete - } - } - - private async parseOverrideMd(filePath: string): Promise<SkillOverrideEntry> { - const raw = await fs.readFile(filePath, "utf8"); - const skillId = path.basename(filePath, ".md"); - - if (!raw.startsWith("---")) { - throw new Error(`Skill override ${skillId} missing frontmatter`); - } - - const end = raw.indexOf("\n---", 3); - if (end === -1) { - throw new Error(`Skill override ${skillId} has malformed frontmatter`); - } - - const fm = raw.slice(3, end).trim(); - const body = raw.slice(end + 4).trim(); - const meta = SkillOverride.parse(parse(fm)); - - return { - skillId, - meta, - content: body, - }; - } -} diff --git a/apps/x/packages/core/src/skills/resolver.ts b/apps/x/packages/core/src/skills/resolver.ts index 4a59eb90..27920071 100644 --- a/apps/x/packages/core/src/skills/resolver.ts +++ b/apps/x/packages/core/src/skills/resolver.ts @@ -1,114 +1,37 @@ import { ResolvedSkill } from "@x/shared/dist/skill.js"; import { IOfficialSkillsRepo } from "./official-repo.js"; -import { ISkillsRepo } from "./repo.js"; +import { substitutePlaceholders } from "./placeholders.js"; export interface ISkillResolver { getCatalog(): Promise<ResolvedSkill[]>; resolve(id: string): Promise<ResolvedSkill | null>; - getOfficial(id: string): Promise<ResolvedSkill | null>; - generateCatalogMarkdown(): Promise<string>; } export class SkillResolver implements ISkillResolver { private readonly officialSkillsRepo: IOfficialSkillsRepo; - private readonly skillsRepo: ISkillsRepo; - constructor({ officialSkillsRepo, skillsRepo }: { officialSkillsRepo: IOfficialSkillsRepo; skillsRepo: ISkillsRepo }) { + constructor({ officialSkillsRepo }: { officialSkillsRepo: IOfficialSkillsRepo }) { this.officialSkillsRepo = officialSkillsRepo; - this.skillsRepo = skillsRepo; } async getCatalog(): Promise<ResolvedSkill[]> { const officials = await this.officialSkillsRepo.listOfficial(); - const overrides = await this.skillsRepo.listOverrides(); - const overrideMap = new Map(overrides.map((o) => [o.skillId, o])); - - const results: ResolvedSkill[] = []; - - for (const official of officials) { - const override = overrideMap.get(official.id); - if (override) { - results.push({ - id: official.id, - title: override.meta.title ?? official.title, - summary: override.meta.summary ?? official.summary, - version: official.version, - source: "override", - content: override.content, - hasUpdate: override.meta.base_version !== official.version, - baseVersion: override.meta.base_version, - }); - } else { - results.push({ - id: official.id, - title: official.title, - summary: official.summary, - version: official.version, - source: "official", - content: official.content, - }); - } - } - - return results; + return officials.map((official) => ({ + id: official.id, + title: official.title, + summary: official.summary, + content: substitutePlaceholders(official.content), + })); } async resolve(id: string): Promise<ResolvedSkill | null> { const official = await this.officialSkillsRepo.getOfficial(id); if (!official) return null; - - const override = await this.skillsRepo.getOverride(id); - if (override) { - return { - id: official.id, - title: override.meta.title ?? official.title, - summary: override.meta.summary ?? official.summary, - version: official.version, - source: "override", - content: override.content, - hasUpdate: override.meta.base_version !== official.version, - baseVersion: override.meta.base_version, - }; - } - return { id: official.id, title: official.title, summary: official.summary, - version: official.version, - source: "official", - content: official.content, + content: substitutePlaceholders(official.content), }; } - - async getOfficial(id: string): Promise<ResolvedSkill | null> { - const official = await this.officialSkillsRepo.getOfficial(id); - if (!official) return null; - - return { - id: official.id, - title: official.title, - summary: official.summary, - version: official.version, - source: "official", - content: official.content, - }; - } - - async generateCatalogMarkdown(): Promise<string> { - const catalog = await this.getCatalog(); - const sections = catalog.map((skill) => [ - `## ${skill.title}`, - `- **Skill file:** \`${skill.id}\``, - `- **Use it for:** ${skill.summary}`, - ].join("\n")); - - return [ - "# Rowboat Skill Catalog", - "", - "Use this catalog to see which specialized skills you can load. Each entry lists the skill id plus a short description of when it helps.", - "", - sections.join("\n\n"), - ].join("\n"); - } } diff --git a/apps/x/packages/core/src/skills/sync.ts b/apps/x/packages/core/src/skills/sync.ts deleted file mode 100644 index dd3ba102..00000000 --- a/apps/x/packages/core/src/skills/sync.ts +++ /dev/null @@ -1,168 +0,0 @@ -import https from "node:https"; -import http from "node:http"; -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import path from "node:path"; -import { execSync } from "node:child_process"; -import { pipeline } from "node:stream/promises"; -import { WorkDir } from "../config/config.js"; - -const SYNC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour -const REPO_OWNER = "rowboatlabs"; -const REPO_NAME = "skills"; -const BRANCH = "main"; -const TARBALL_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/tarball/${BRANCH}`; - -const officialDir = path.join(WorkDir, "skills", "official"); -const syncStateFile = path.join(WorkDir, "skills", "last-sync.json"); - -interface SyncState { - timestamp: string; - etag: string | null; -} - -function log(msg: string) { - console.log(`[SkillSync] ${msg}`); -} - -async function readSyncState(): Promise<SyncState | null> { - try { - const raw = await fsp.readFile(syncStateFile, "utf-8"); - return JSON.parse(raw); - } catch { - return null; - } -} - -async function writeSyncState(state: SyncState): Promise<void> { - await fsp.writeFile(syncStateFile, JSON.stringify(state, null, 2)); -} - -/** - * Download and extract the GitHub tarball to the official skills directory. - * Returns true if new skills were downloaded, false if 304 (not modified). - */ -async function syncFromGitHub(): Promise<boolean> { - const state = await readSyncState(); - - return new Promise((resolve, reject) => { - const headers: Record<string, string> = { - "User-Agent": "Rowboat-SkillSync/1.0", - Accept: "application/vnd.github+json", - }; - if (state?.etag) { - headers["If-None-Match"] = state.etag; - } - - const makeRequest = (url: string) => { - const mod = url.startsWith("https") ? https : http; - mod.get(url, { headers }, (res) => { - // Handle redirects (GitHub returns 302 for tarball) - if (res.statusCode === 301 || res.statusCode === 302) { - const location = res.headers.location; - if (location) { - makeRequest(location); - return; - } - } - - if (res.statusCode === 304) { - log("Skills up to date (304 Not Modified)"); - resolve(false); - return; - } - - if (res.statusCode !== 200) { - reject(new Error(`GitHub API returned ${res.statusCode}`)); - return; - } - - const newEtag = res.headers.etag ?? null; - const tmpDir = path.join(WorkDir, "skills", ".sync-tmp"); - - // Clean tmp dir - fs.rmSync(tmpDir, { recursive: true, force: true }); - fs.mkdirSync(tmpDir, { recursive: true }); - - const tarPath = path.join(tmpDir, "download.tar.gz"); - const writeStream = fs.createWriteStream(tarPath); - - pipeline(res, writeStream) - .then(async () => { - // Extract tarball - const extractDir = path.join(tmpDir, "extracted"); - fs.mkdirSync(extractDir, { recursive: true }); - execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`, { stdio: "pipe" }); - - // GitHub tarballs have a top-level directory like owner-repo-hash/ - const entries = await fsp.readdir(extractDir); - const topDir = entries[0]; - if (!topDir) { - throw new Error("Extracted tarball is empty"); - } - - const sourceDir = path.join(extractDir, topDir); - - // Atomic swap: rename old -> .old, new -> official, delete .old - const oldDir = path.join(WorkDir, "skills", ".official-old"); - fs.rmSync(oldDir, { recursive: true, force: true }); - - const officialExists = fs.existsSync(officialDir); - if (officialExists) { - await fsp.rename(officialDir, oldDir); - } - await fsp.rename(sourceDir, officialDir); - if (officialExists) { - fs.rmSync(oldDir, { recursive: true, force: true }); - } - - // Cleanup tmp - fs.rmSync(tmpDir, { recursive: true, force: true }); - - // Update sync state - await writeSyncState({ - timestamp: new Date().toISOString(), - etag: newEtag, - }); - - log("Skills synced from GitHub successfully"); - resolve(true); - }) - .catch(reject); - }).on("error", reject); - }; - - makeRequest(TARBALL_URL); - }); -} - -async function runSync(): Promise<void> { - // Ensure official dir exists - await fsp.mkdir(officialDir, { recursive: true }); - - // Try syncing from GitHub - try { - await syncFromGitHub(); - } catch (error) { - log(`Sync failed (will use cached skills): ${error instanceof Error ? error.message : String(error)}`); - } -} - -export async function init(): Promise<void> { - log("Starting skill sync service..."); - - // Initial sync - await runSync(); - - // Periodic sync - const loop = async () => { - while (true) { - await new Promise((resolve) => setTimeout(resolve, SYNC_INTERVAL_MS)); - log("Running periodic sync..."); - await runSync(); - } - }; - loop().catch((error) => { - log(`Sync loop error: ${error instanceof Error ? error.message : String(error)}`); - }); -} diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index d795a95f..2d394d8b 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -8,7 +8,7 @@ import { AgentScheduleState } from './agent-schedule-state.js'; import { ServiceEvent } from './service-events.js'; import { TrackEvent } from './track-block.js'; import { UserMessageContent } from './message.js'; -import { ResolvedSkill, SkillOverride } from './skill.js'; +import { ResolvedSkill } from './skill.js'; import { RowboatApiConfig } from './rowboat-account.js'; import { ZListToolkitsResponse } from './composio.js'; import { BrowserStateSchema } from './browser-control.js'; @@ -735,30 +735,6 @@ const ipcSchemas = { }), res: ResolvedSkill.nullable(), }, - 'skills:getOfficial': { - req: z.object({ - id: z.string(), - }), - res: ResolvedSkill.nullable(), - }, - 'skills:saveOverride': { - req: z.object({ - skillId: z.string(), - meta: SkillOverride, - content: z.string(), - }), - res: z.object({ - success: z.literal(true), - }), - }, - 'skills:deleteOverride': { - req: z.object({ - skillId: z.string(), - }), - res: z.object({ - success: z.literal(true), - }), - }, 'billing:getInfo': { req: z.null(), res: z.object({ diff --git a/apps/x/packages/shared/src/skill.ts b/apps/x/packages/shared/src/skill.ts index 4d9e5b83..bf7d448f 100644 --- a/apps/x/packages/shared/src/skill.ts +++ b/apps/x/packages/shared/src/skill.ts @@ -1,8 +1,7 @@ import { z } from 'zod'; // SKILL.md frontmatter schema (Agent Skills spec compliant) -// Top-level: name, description, license, compatibility, allowed-tools, metadata -// Custom Rowboat fields go under metadata +// https://agentskills.io/specification export const SkillFrontmatter = z.object({ name: z.string().max(64), description: z.string().max(1024), @@ -19,43 +18,12 @@ export const SkillFrontmatter = z.object({ export type SkillFrontmatter = z.infer<typeof SkillFrontmatter>; -// Official skill metadata (bundled with app) -export const OfficialSkillMeta = z.object({ - id: z.string(), - title: z.string(), - summary: z.string(), - version: z.string(), - source: z.literal("official"), -}); - -// User override metadata (stored on disk as YAML frontmatter) -export const SkillOverride = z.object({ - base_skill_id: z.string(), - base_version: z.string(), - title: z.string().optional(), - summary: z.string().optional(), -}); - -// Parsed override entry (metadata + content) -export const SkillOverrideEntry = z.object({ - skillId: z.string(), - meta: SkillOverride, - content: z.string(), -}); - -// Resolved skill seen by the agent (source-agnostic) +// Skill seen by the agent and the renderer (read-only). export const ResolvedSkill = z.object({ id: z.string(), title: z.string(), summary: z.string(), - version: z.string(), - source: z.enum(["official", "override", "installed"]), content: z.string(), - hasUpdate: z.boolean().optional(), - baseVersion: z.string().optional(), }); -export type OfficialSkillMeta = z.infer<typeof OfficialSkillMeta>; -export type SkillOverride = z.infer<typeof SkillOverride>; -export type SkillOverrideEntry = z.infer<typeof SkillOverrideEntry>; export type ResolvedSkill = z.infer<typeof ResolvedSkill>;