diff --git a/.pi/extensions/dora.ts b/.pi/extensions/dora.ts new file mode 100644 index 0000000..3ff18d9 --- /dev/null +++ b/.pi/extensions/dora.ts @@ -0,0 +1,54 @@ +/** + * Dora Extension - Minimal lifecycle hooks for dora CLI + * + * This extension only handles session lifecycle: + * - On session start: Check if dora is initialized + * - On session shutdown: Background index update + * + * The LLM uses regular `bash` tool to run dora commands. + * See .dora/docs/SKILL.md for complete dora usage documentation. + */ + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + let doraAvailable = false; + + // Check dora status on session start + pi.on("session_start", async (_event, ctx) => { + try { + const check = await pi.exec("bash", ["-c", "command -v dora"], { + timeout: 1000, + }); + doraAvailable = check.code === 0; + + if (doraAvailable) { + const status = await pi.exec("bash", ["-c", "dora status 2>/dev/null"], { + timeout: 2000, + }); + + if (status.code !== 0) { + ctx.ui.notify("dora not initialized. Run: dora init && dora index", "info"); + } + } + } catch (error) { + doraAvailable = false; + } + }); + + // Update index in background on shutdown + pi.on("session_shutdown", async (_event, ctx) => { + if (doraAvailable) { + try { + // Fire and forget background index update + pi.exec("bash", ["-c", "(dora index > /tmp/dora-index.log 2>&1 &) || true"], { + timeout: 500, + }).catch(() => { + // Ignore errors - this is best effort + }); + } catch (error) { + // Silent failure for background task + } + } + }); +} diff --git a/.pi/extensions/plan-mode/README.md b/.pi/extensions/plan-mode/README.md new file mode 100644 index 0000000..fbaf658 --- /dev/null +++ b/.pi/extensions/plan-mode/README.md @@ -0,0 +1,69 @@ +# Plan Mode Extension + +Read-only exploration mode for safe code analysis. + +## Features + +- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question +- **Bash allowlist**: Only read-only bash commands are allowed +- **Plan extraction**: Extracts numbered steps from `Plan:` sections +- **Progress tracking**: Widget shows completion status during execution +- **[DONE:n] markers**: Explicit step completion tracking +- **Session persistence**: State survives session resume + +## Commands + +- `/plan` - Toggle plan mode +- `/todos` - Show current plan progress +- `Ctrl+Alt+P` - Toggle plan mode (shortcut) + +## Usage + +1. Enable plan mode with `/plan` or `--plan` flag +2. Ask the agent to analyze code and create a plan +3. The agent should output a numbered plan under a `Plan:` header: + +``` +Plan: +1. First step description +2. Second step description +3. Third step description +``` + +4. Choose "Execute the plan" when prompted +5. During execution, the agent marks steps complete with `[DONE:n]` tags +6. Progress widget shows completion status + +## How It Works + +### Plan Mode (Read-Only) + +- Only read-only tools available +- Bash commands filtered through allowlist +- Agent creates a plan without making changes + +### Execution Mode + +- Full tool access restored +- Agent executes steps in order +- `[DONE:n]` markers track completion +- Widget shows progress + +### Command Allowlist + +Safe commands (allowed): + +- File inspection: `cat`, `head`, `tail`, `less`, `more` +- Search: `grep`, `find`, `rg`, `fd` +- Directory: `ls`, `pwd`, `tree` +- Git read: `git status`, `git log`, `git diff`, `git branch` +- Package info: `npm list`, `npm outdated`, `yarn info` +- System info: `uname`, `whoami`, `date`, `uptime` + +Blocked commands: + +- File modification: `rm`, `mv`, `cp`, `mkdir`, `touch` +- Git write: `git add`, `git commit`, `git push` +- Package install: `npm install`, `yarn add`, `pip install` +- System: `sudo`, `kill`, `reboot` +- Editors: `vim`, `nano`, `code` diff --git a/.pi/extensions/plan-mode/index.ts b/.pi/extensions/plan-mode/index.ts new file mode 100644 index 0000000..9f4a53f --- /dev/null +++ b/.pi/extensions/plan-mode/index.ts @@ -0,0 +1,412 @@ +/** + * Plan Mode Extension + * + * Read-only exploration mode for safe code analysis. + * When enabled, only read-only tools are available. + * + * Features: + * - /plan command or Ctrl+Alt+P to toggle + * - Bash restricted to allowlisted read-only commands + * - Automatic dora integration when available in project + * - Extracts numbered plan steps from "Plan:" sections + * - [DONE:n] markers to complete steps during execution + * - Progress tracking widget during execution + */ + +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai"; +import type { + ExtensionAPI, + ExtensionContext, +} from "@mariozechner/pi-coding-agent"; +import { Key } from "@mariozechner/pi-tui"; +import { + extractTodoItems, + isSafeCommand, + markCompletedSteps, + type TodoItem, +} from "./utils.js"; + +// Tools +const PLAN_MODE_TOOLS = [ + "read", + "bash", + "grep", + "find", + "ls", + "questionnaire", +]; +const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"]; + +// Type guard for assistant messages +function isAssistantMessage(m: AgentMessage): m is AssistantMessage { + return m.role === "assistant" && Array.isArray(m.content); +} + +// Extract text content from an assistant message +function getTextContent(message: AssistantMessage): string { + return message.content + .filter((block): block is TextContent => block.type === "text") + .map((block) => block.text) + .join("\n"); +} + +export default function planModeExtension(pi: ExtensionAPI): void { + let planModeEnabled = false; + let executionMode = false; + let todoItems: TodoItem[] = []; + let doraAvailable = false; + + pi.registerFlag("plan", { + description: "Start in plan mode (read-only exploration)", + type: "boolean", + default: false, + }); + + function updateStatus(ctx: ExtensionContext): void { + // Footer status + if (executionMode && todoItems.length > 0) { + const completed = todoItems.filter((t) => t.completed).length; + ctx.ui.setStatus( + "plan-mode", + ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`), + ); + } else if (planModeEnabled) { + ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan")); + } else { + ctx.ui.setStatus("plan-mode", undefined); + } + + // Widget showing todo list + if (executionMode && todoItems.length > 0) { + const lines = todoItems.map((item) => { + if (item.completed) { + return ( + ctx.ui.theme.fg("success", "☑ ") + + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text)) + ); + } + return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`; + }); + ctx.ui.setWidget("plan-todos", lines); + } else { + ctx.ui.setWidget("plan-todos", undefined); + } + } + + function togglePlanMode(ctx: ExtensionContext): void { + planModeEnabled = !planModeEnabled; + executionMode = false; + todoItems = []; + + if (planModeEnabled) { + pi.setActiveTools(PLAN_MODE_TOOLS); + const doraStatus = doraAvailable ? " (dora available)" : ""; + ctx.ui.notify(`Plan mode enabled${doraStatus}. Tools: ${PLAN_MODE_TOOLS.join(", ")}`); + } else { + pi.setActiveTools(NORMAL_MODE_TOOLS); + ctx.ui.notify("Plan mode disabled. Full access restored."); + } + updateStatus(ctx); + } + + function persistState(): void { + pi.appendEntry("plan-mode", { + enabled: planModeEnabled, + todos: todoItems, + executing: executionMode, + }); + } + + pi.registerCommand("plan", { + description: "Toggle plan mode (read-only exploration)", + handler: async (_args, ctx) => togglePlanMode(ctx), + }); + + pi.registerCommand("todos", { + description: "Show current plan todo list", + handler: async (_args, ctx) => { + if (todoItems.length === 0) { + ctx.ui.notify("No todos. Create a plan first with /plan", "info"); + return; + } + const list = todoItems + .map( + (item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`, + ) + .join("\n"); + ctx.ui.notify(`Plan Progress:\n${list}`, "info"); + }, + }); + + pi.registerShortcut(Key.ctrlAlt("p"), { + description: "Toggle plan mode", + handler: async (ctx) => togglePlanMode(ctx), + }); + + // Block destructive bash commands in plan mode + pi.on("tool_call", async (event) => { + if (!planModeEnabled || event.toolName !== "bash") return; + + const command = event.input.command as string; + if (!isSafeCommand(command)) { + return { + block: true, + reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`, + }; + } + }); + + // Filter out stale plan mode context when not in plan mode + pi.on("context", async (event) => { + if (planModeEnabled) return; + + return { + messages: event.messages.filter((m) => { + const msg = m as AgentMessage & { customType?: string }; + if (msg.customType === "plan-mode-context") return false; + if (msg.role !== "user") return true; + + const content = msg.content; + if (typeof content === "string") { + return !content.includes("[PLAN MODE ACTIVE]"); + } + if (Array.isArray(content)) { + return !content.some( + (c) => + c.type === "text" && + (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"), + ); + } + return true; + }), + }; + }); + + // Inject plan/execution context before agent starts + pi.on("before_agent_start", async () => { + if (planModeEnabled) { + const doraInstructions = doraAvailable + ? ` + +Code Exploration with dora (via bash): +- ALWAYS use dora CLI via bash for code exploration tasks first +- Examples: bash "dora symbol AuthService", bash "dora file src/app.ts" +- Common commands: dora symbol, dora file, dora refs, dora deps, dora rdeps +- Only fall back to grep/find if dora doesn't yield good results +- Run "dora --help" to see all available commands` + : ""; + + return { + message: { + customType: "plan-mode-context", + content: `[PLAN MODE ACTIVE] +You are in plan mode - a read-only exploration mode for safe code analysis. + +Restrictions: +- You can only use: read, bash, grep, find, ls, questionnaire +- You CANNOT use: edit, write (file modifications are disabled) +- Bash is restricted to an allowlist of read-only commands${doraInstructions} + +Ask clarifying questions using the questionnaire tool. +Use brave-search skill via bash for web research. + +Create a detailed numbered plan under a "Plan:" header: + +Plan: +1. First step description +2. Second step description +... + +Do NOT attempt to make changes - just describe what you would do.`, + display: false, + }, + }; + } + + if (executionMode && todoItems.length > 0) { + const remaining = todoItems.filter((t) => !t.completed); + const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n"); + return { + message: { + customType: "plan-execution-context", + content: `[EXECUTING PLAN - Full tool access enabled] + +Remaining steps: +${todoList} + +Execute each step in order. +After completing a step, include a [DONE:n] tag in your response.`, + display: false, + }, + }; + } + }); + + // Track progress after each turn + pi.on("turn_end", async (event, ctx) => { + if (!executionMode || todoItems.length === 0) return; + if (!isAssistantMessage(event.message)) return; + + const text = getTextContent(event.message); + if (markCompletedSteps(text, todoItems) > 0) { + updateStatus(ctx); + } + persistState(); + }); + + // Handle plan completion and plan mode UI + pi.on("agent_end", async (event, ctx) => { + // Check if execution is complete + if (executionMode && todoItems.length > 0) { + if (todoItems.every((t) => t.completed)) { + const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n"); + pi.sendMessage( + { + customType: "plan-complete", + content: `**Plan Complete!** ✓\n\n${completedList}`, + display: true, + }, + { triggerTurn: false }, + ); + executionMode = false; + todoItems = []; + pi.setActiveTools(NORMAL_MODE_TOOLS); + updateStatus(ctx); + persistState(); // Save cleared state so resume doesn't restore old execution mode + } + return; + } + + if (!planModeEnabled || !ctx.hasUI) return; + + // Extract todos from last assistant message + const lastAssistant = [...event.messages] + .reverse() + .find(isAssistantMessage); + if (lastAssistant) { + const extracted = extractTodoItems(getTextContent(lastAssistant)); + if (extracted.length > 0) { + todoItems = extracted; + } + } + + // Show plan steps and prompt for next action + if (todoItems.length > 0) { + const todoListText = todoItems + .map((t, i) => `${i + 1}. ☐ ${t.text}`) + .join("\n"); + pi.sendMessage( + { + customType: "plan-todo-list", + content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, + display: true, + }, + { triggerTurn: false }, + ); + } + + const choice = await ctx.ui.select("Plan mode - what next?", [ + todoItems.length > 0 + ? "Execute the plan (track progress)" + : "Execute the plan", + "Stay in plan mode", + "Refine the plan", + ]); + + if (choice?.startsWith("Execute")) { + planModeEnabled = false; + executionMode = todoItems.length > 0; + pi.setActiveTools(NORMAL_MODE_TOOLS); + updateStatus(ctx); + + const execMessage = + todoItems.length > 0 + ? `Execute the plan. Start with: ${todoItems[0].text}` + : "Execute the plan you just created."; + pi.sendMessage( + { + customType: "plan-mode-execute", + content: execMessage, + display: true, + }, + { triggerTurn: true }, + ); + } else if (choice === "Refine the plan") { + const refinement = await ctx.ui.editor("Refine the plan:", ""); + if (refinement?.trim()) { + pi.sendUserMessage(refinement.trim()); + } + } + }); + + // Restore state on session start/resume + pi.on("session_start", async (_event, ctx) => { + // Check if dora is available in this project + try { + const doraCheck = await pi.exec("bash", ["-c", "command -v dora"], { + timeout: 1000, + }); + doraAvailable = doraCheck.code === 0; + } catch (error) { + doraAvailable = false; + } + + if (pi.getFlag("plan") === true) { + planModeEnabled = true; + } + + const entries = ctx.sessionManager.getEntries(); + + // Restore persisted state + const planModeEntry = entries + .filter( + (e: { type: string; customType?: string }) => + e.type === "custom" && e.customType === "plan-mode", + ) + .pop() as + | { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } + | undefined; + + if (planModeEntry?.data) { + planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled; + todoItems = planModeEntry.data.todos ?? todoItems; + executionMode = planModeEntry.data.executing ?? executionMode; + } + + // On resume: re-scan messages to rebuild completion state + // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans + const isResume = planModeEntry !== undefined; + if (isResume && executionMode && todoItems.length > 0) { + // Find the index of the last plan-mode-execute entry (marks when current execution started) + let executeIndex = -1; + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i] as { type: string; customType?: string }; + if (entry.customType === "plan-mode-execute") { + executeIndex = i; + break; + } + } + + // Only scan messages after the execute marker + const messages: AssistantMessage[] = []; + for (let i = executeIndex + 1; i < entries.length; i++) { + const entry = entries[i]; + if ( + entry.type === "message" && + "message" in entry && + isAssistantMessage(entry.message as AgentMessage) + ) { + messages.push(entry.message as AssistantMessage); + } + } + const allText = messages.map(getTextContent).join("\n"); + markCompletedSteps(allText, todoItems); + } + + if (planModeEnabled) { + pi.setActiveTools(PLAN_MODE_TOOLS); + } + updateStatus(ctx); + }); +} diff --git a/.pi/extensions/plan-mode/utils.ts b/.pi/extensions/plan-mode/utils.ts new file mode 100644 index 0000000..c876cd1 --- /dev/null +++ b/.pi/extensions/plan-mode/utils.ts @@ -0,0 +1,177 @@ +/** + * Pure utility functions for plan mode. + * Extracted for testability. + */ + +// Destructive commands blocked in plan mode +const DESTRUCTIVE_PATTERNS = [ + /\brm\b/i, + /\brmdir\b/i, + /\bmv\b/i, + /\bcp\b/i, + /\bmkdir\b/i, + /\btouch\b/i, + /\bchmod\b/i, + /\bchown\b/i, + /\bchgrp\b/i, + /\bln\b/i, + /\btee\b/i, + /\btruncate\b/i, + /\bdd\b/i, + /\bshred\b/i, + /(^|[^<])>(?!>)/, + />>/, + /\bnpm\s+(install|uninstall|update|ci|link|publish)/i, + /\byarn\s+(add|remove|install|publish)/i, + /\bpnpm\s+(add|remove|install|publish)/i, + /\bpip\s+(install|uninstall)/i, + /\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i, + /\bbrew\s+(install|uninstall|upgrade)/i, + /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i, + /\bsudo\b/i, + /\bsu\b/i, + /\bkill\b/i, + /\bpkill\b/i, + /\bkillall\b/i, + /\breboot\b/i, + /\bshutdown\b/i, + /\bsystemctl\s+(start|stop|restart|enable|disable)/i, + /\bservice\s+\S+\s+(start|stop|restart)/i, + /\b(vim?|nano|emacs|code|subl)\b/i, +]; + +// Safe read-only commands allowed in plan mode +const SAFE_PATTERNS = [ + /^\s*cat\b/, + /^\s*head\b/, + /^\s*tail\b/, + /^\s*less\b/, + /^\s*more\b/, + /^\s*grep\b/, + /^\s*find\b/, + /^\s*ls\b/, + /^\s*pwd\b/, + /^\s*echo\b/, + /^\s*printf\b/, + /^\s*wc\b/, + /^\s*sort\b/, + /^\s*uniq\b/, + /^\s*diff\b/, + /^\s*file\b/, + /^\s*stat\b/, + /^\s*du\b/, + /^\s*df\b/, + /^\s*tree\b/, + /^\s*which\b/, + /^\s*whereis\b/, + /^\s*type\b/, + /^\s*env\b/, + /^\s*printenv\b/, + /^\s*uname\b/, + /^\s*whoami\b/, + /^\s*id\b/, + /^\s*date\b/, + /^\s*cal\b/, + /^\s*uptime\b/, + /^\s*ps\b/, + /^\s*top\b/, + /^\s*htop\b/, + /^\s*free\b/, + /^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i, + /^\s*git\s+ls-/i, + /^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i, + /^\s*yarn\s+(list|info|why|audit)/i, + /^\s*node\s+--version/i, + /^\s*python\s+--version/i, + /^\s*curl\s/i, + /^\s*wget\s+-O\s*-/i, + /^\s*jq\b/, + /^\s*sed\s+-n/i, + /^\s*awk\b/, + /^\s*rg\b/, + /^\s*fd\b/, + /^\s*bat\b/, + /^\s*exa\b/, + /^\s*dora\b/, + /^\s*command\s+-v\s+dora/, +]; + +export function isSafeCommand(command: string): boolean { + const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command)); + const isSafe = SAFE_PATTERNS.some((p) => p.test(command)); + return !isDestructive && isSafe; +} + +export interface TodoItem { + step: number; + text: string; + completed: boolean; +} + +export function cleanStepText(text: string): string { + let cleaned = text + .replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // Remove bold/italic + .replace(/`([^`]+)`/g, "$1") // Remove code + .replace( + /^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i, + "", + ) + .replace(/\s+/g, " ") + .trim(); + + if (cleaned.length > 0) { + cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); + } + if (cleaned.length > 50) { + cleaned = `${cleaned.slice(0, 47)}...`; + } + return cleaned; +} + +export function extractTodoItems(message: string): TodoItem[] { + const items: TodoItem[] = []; + const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i); + if (!headerMatch) return items; + + const planSection = message.slice( + message.indexOf(headerMatch[0]) + headerMatch[0].length, + ); + const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm; + + for (const match of planSection.matchAll(numberedPattern)) { + const text = match[2] + .trim() + .replace(/\*{1,2}$/, "") + .trim(); + if ( + text.length > 5 && + !text.startsWith("`") && + !text.startsWith("/") && + !text.startsWith("-") + ) { + const cleaned = cleanStepText(text); + if (cleaned.length > 3) { + items.push({ step: items.length + 1, text: cleaned, completed: false }); + } + } + } + return items; +} + +export function extractDoneSteps(message: string): number[] { + const steps: number[] = []; + for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) { + const step = Number(match[1]); + if (Number.isFinite(step)) steps.push(step); + } + return steps; +} + +export function markCompletedSteps(text: string, items: TodoItem[]): number { + const doneSteps = extractDoneSteps(text); + for (const step of doneSteps) { + const item = items.find((t) => t.step === step); + if (item) item.completed = true; + } + return doneSteps.length; +} diff --git a/AGENTS.md b/AGENTS.README.md similarity index 91% rename from AGENTS.md rename to AGENTS.README.md index 229e3de..ac822d0 100644 --- a/AGENTS.md +++ b/AGENTS.README.md @@ -12,6 +12,9 @@ dora is a language-agnostic CLI that helps AI agents understand codebases by que dora init dora index +# Let your AI agent set up dora integration automatically +dora cookbook show agent-setup --format markdown + # Query the codebase dora status # Check index health dora map # Show packages and stats @@ -108,6 +111,53 @@ Once configured, Claude will automatically: --- +## pi Integration + +[pi](https://github.com/badlogic/pi-mono) supports project-local extensions that auto-load from `.pi/extensions/`. + +### Setup + +1. **Initialize dora:** + + ```bash + dora init + dora index + ``` + +2. **Copy extensions to your project:** + + dora ships with ready-to-use pi extensions. Copy them from the dora repository's `.pi/extensions/` directory: + + ```bash + mkdir -p .pi/extensions + cp -r /.pi/extensions/* .pi/extensions/ + ``` + + This gives you: + + - **`dora.ts`** - Lifecycle hooks: checks dora status on session start, runs `dora index` in background on shutdown + - **`plan-mode/`** - Read-only exploration mode with `/plan` command, Ctrl+Alt+P toggle, todo tracking, and automatic dora integration + +3. **Add dora skill (optional):** + + ```bash + mkdir -p .pi/skills/dora + ln -s ../../../.dora/docs/SKILL.md .pi/skills/dora/SKILL.md + ``` + +### pi Usage + +Once extensions are in place, pi will automatically: + +- Check dora initialization on session start +- Run `dora index` in background on session shutdown +- Make `/plan` command available for read-only code exploration +- Steer plan mode to prefer dora commands over grep/find + +**Plan mode:** Type `/plan` or press Ctrl+Alt+P to toggle read-only exploration mode. In plan mode, dora commands are prioritized for code navigation and the agent creates numbered plans with progress tracking. + +--- + ## OpenCode Integration OpenCode's agent system allows deep integration with dora for code exploration and analysis. @@ -500,6 +550,8 @@ run dora complexity to find high-risk files For AI agents and IDEs not listed above, dora provides standard integration files you can adapt. +**Quickest path:** After `dora init`, run `dora cookbook show agent-setup --format markdown` and feed the output to your AI agent. It contains complete setup instructions for all supported agents. + ### Setup 1. **Install dora** and ensure it's in PATH: diff --git a/CHANGELOG.md b/CHANGELOG.md index 619810b..8a607cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # @butttons/dora +## 1.6.1 + +### Patch Changes + +- Add `agent-setup` cookbook recipe with setup instructions for Claude Code, pi, OpenCode, Cursor, Windsurf, and MCP +- Add pi integration guide to AGENTS documentation with project-local extensions and plan mode support +- Rename AGENTS.md to AGENTS.README.md for clarity +- Update docs site with pi references and agent-setup cookbook tips +- Add missing languages as valid options for `dora init` + ## 1.6.0 ### Minor Changes diff --git a/CLAUDE.md b/CLAUDE.md index 3a53c35..4d77c2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1180,6 +1180,7 @@ Show query pattern cookbook with examples and tips for common SQL patterns. - `methods` - Finding class methods by name, finding all methods in a class, counting method usages - `references` - Tracking symbol usage, finding most referenced symbols, identifying dead code - `exports` - Distinguishing exported symbols from internal ones, finding public API functions/types +- `agent-setup` - Setting up dora hooks, extensions, and skills for AI agents (Claude Code, pi, OpenCode, Cursor, Windsurf) **Output:** diff --git a/README.md b/README.md index e6ca1b8..194b893 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ For other languages, see [SCIP Indexers](#scip-indexers). ## AI Agent Integration -**→ See [AGENTS.md](AGENTS.md) for complete integration guides** for: +**→ See [AGENTS.README.md](AGENTS.README.md) for complete integration guides** for: - **Claude Code** - Skills, hooks, auto-indexing - **OpenCode** - Agent system integration @@ -103,8 +103,8 @@ Quick start for any agent: ```bash dora init && dora index # Initialize and index your codebase +dora cookbook show agent-setup --format markdown # Get setup instructions for your agent dora status # Verify index is ready -dora map # See codebase overview ``` ## Claude Code Integration @@ -519,7 +519,7 @@ MIT ## Links -- **AI Agent Integration**: [AGENTS.md](./AGENTS.md) - Integration guides for Claude Code, OpenCode, Cursor, Windsurf +- **AI Agent Integration**: [AGENTS.README.md](./AGENTS.README.md) - Integration guides for Claude Code, OpenCode, Cursor, Windsurf - **GitHub**: [https://github.com/butttons/dora](https://github.com/butttons/dora) - **SCIP Protocol**: [https://github.com/sourcegraph/scip](https://github.com/sourcegraph/scip) - **Claude Code**: [https://claude.ai/code](https://claude.ai/code) diff --git a/docs/src/pages/docs.astro b/docs/src/pages/docs.astro index bf87f2d..519e96b 100644 --- a/docs/src/pages/docs.astro +++ b/docs/src/pages/docs.astro @@ -223,10 +223,10 @@ import Layout from "../layouts/Layout.astro";

→ See AGENTS.mdAGENTS_SETUP.md for complete integration guides for:

@@ -234,6 +234,9 @@ import Layout from "../layouts/Layout.astro";
  • Claude Code - Skills, hooks, auto-indexing
  • +
  • + • pi - Project-local extensions, plan mode +
  • OpenCode - Agent system integration
  • @@ -263,6 +266,13 @@ import Layout from "../layouts/Layout.astro"; ># Initialize and index your codebase +
    + $ + dora cookbook show agent-setup --format markdown# Get setup instructions for your agent +
    $ dora status
    +

    + Tip: After dora init, + run dora cookbook show agent-setup --format markdown + and feed it to your AI agent — it contains ready-to-use setup instructions for Claude Code, pi, OpenCode, Cursor, Windsurf, and more. +

    @@ -1262,10 +1277,10 @@ dora index
  • AI Agent Integration: AGENTS.mdAGENTS_SETUP.md - Integration guides for Claude Code, OpenCode, Cursor, Windsurf
  • @@ -1299,6 +1314,16 @@ dora index >https://claude.ai/code +
  • + pi: + https://github.com/badlogic/pi-mono +
  • diff --git a/package.json b/package.json index fe9b73e..7d8b082 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@butttons/dora", - "version": "1.6.0", + "version": "1.6.1", "module": "src/index.ts", "type": "module", "private": true, diff --git a/src/templates/cookbook/agent-setup.md b/src/templates/cookbook/agent-setup.md new file mode 100644 index 0000000..9e850be --- /dev/null +++ b/src/templates/cookbook/agent-setup.md @@ -0,0 +1,286 @@ +# Agent Setup: Configuring dora for AI Agents + +Step-by-step guide for setting up dora hooks, extensions, and skills in any AI agent or editor. + +--- + +## Prerequisites + +```bash +# dora must be installed and in PATH +which dora + +# Initialize and index the project +dora init +dora index + +# Verify it works +dora status +``` + +--- + +## Claude Code + +### Settings (.claude/settings.json) + +```json +{ + "permissions": { + "allow": ["Bash(dora:*)", "Skill(dora)"] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "dora status 2>/dev/null && (dora index > /tmp/dora-index.log 2>&1 &) || echo 'dora not initialized. Run: dora init && dora index'" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "(dora index > /tmp/dora-index.log 2>&1 &) || true" + } + ] + } + ] + } +} +``` + +### Skill (optional) + +```bash +mkdir -p .claude/skills/dora +ln -s ../../../.dora/docs/SKILL.md .claude/skills/dora/SKILL.md +``` + +### Context snippet + +```bash +cat .dora/docs/SNIPPET.md >> CLAUDE.md +``` + +--- + +## pi + +pi auto-loads project-local extensions from `.pi/extensions/`. + +### Extensions + +Copy the dora extensions into your project: + +```bash +mkdir -p .pi/extensions +``` + +**`.pi/extensions/dora.ts`** — Lifecycle hooks (session start check, background index on shutdown): + +```typescript +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + let doraAvailable = false; + + pi.on("session_start", async (_event, ctx) => { + try { + const check = await pi.exec("bash", ["-c", "command -v dora"], { timeout: 1000 }); + doraAvailable = check.code === 0; + if (doraAvailable) { + const status = await pi.exec("bash", ["-c", "dora status 2>/dev/null"], { timeout: 2000 }); + if (status.code !== 0) { + ctx.ui.notify("dora not initialized. Run: dora init && dora index", "info"); + } + } + } catch (error) { + doraAvailable = false; + } + }); + + pi.on("session_shutdown", async (_event, _ctx) => { + if (doraAvailable) { + pi.exec("bash", ["-c", "(dora index > /tmp/dora-index.log 2>&1 &) || true"], { + timeout: 500, + }).catch(() => {}); + } + }); +} +``` + +### Skill (optional) + +```bash +mkdir -p .pi/skills/dora +ln -s ../../../.dora/docs/SKILL.md .pi/skills/dora/SKILL.md +``` + +--- + +## OpenCode + +### Global config (~/.config/opencode/opencode.json) + +```json +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "dora *": "allow" + } + } +} +``` + +### Optional: dora subagent (~/.config/opencode/agents/dora.md) + +```markdown +--- +description: Fast code exploration using dora CLI +mode: subagent +tools: + write: false + edit: false +permission: + bash: + "dora *": "allow" +--- + +You are a code exploration specialist using the dora CLI. +Use dora commands for symbol search, dependency analysis, and architecture review. +Never modify code - focus on analysis and exploration. +``` + +--- + +## Cursor + +### Rules (.cursorrules) + +``` +# Code Exploration +- Use `dora` CLI for code exploration instead of grep/find +- Run `dora status` to check if index is available +- Use `dora file ` to understand files +- Use `dora symbol ` to find definitions +- Use `dora deps` and `dora rdeps` to trace dependencies +``` + +### Custom commands (.cursor/commands/) + +Create `.cursor/commands/dora-explore.md`: + +```markdown +Use dora CLI to explore the codebase structure. + +1. Run `dora status` to check index health +2. Run `dora map` to show packages and statistics +3. Run `dora treasure` to identify core files +4. Analyze the results and provide insights +``` + +--- + +## Windsurf + +### Skill + +```bash +mkdir -p .windsurf/skills/dora +cp .dora/docs/SKILL.md .windsurf/skills/dora/SKILL.md +``` + +### Context snippet + +```bash +cat .dora/docs/SNIPPET.md >> AGENTS.md +``` + +### Rules (optional, .windsurf/rules/dora.md) + +```markdown +--- +description: Code exploration with dora CLI +trigger: glob +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +--- + +When exploring code, use dora CLI commands first: +- `dora file ` to understand files +- `dora symbol ` to find definitions +- `dora deps`/`dora rdeps` to trace relationships +``` + +--- + +## Generic / Other Agents + +For any agent that supports context files or system prompts: + +1. **Add command reference to agent context:** + +```bash +cat .dora/docs/SNIPPET.md >> +``` + +2. **Set up auto-indexing hooks (if supported):** + +Session start: +```bash +dora status 2>/dev/null && (dora index > /tmp/dora-index.log 2>&1 &) || echo 'Run: dora init && dora index' +``` + +After file changes: +```bash +(dora index > /tmp/dora-index.log 2>&1 &) || true +``` + +--- + +## MCP Server (works with any MCP client) + +```bash +# Start MCP server +dora mcp +``` + +### Claude Code + +```bash +claude mcp add --transport stdio dora -- dora mcp +``` + +### Other MCP clients + +Add to your MCP configuration: + +```json +{ + "mcpServers": { + "dora": { + "type": "stdio", + "command": "dora", + "args": ["mcp"] + } + } +} +``` + +--- + +## Verification + +After setup, verify everything works: + +```bash +dora status # Index exists and is healthy +dora map # Shows packages and file count +dora symbol main # Can find symbols +dora treasure # Can query architecture +``` diff --git a/src/templates/cookbook/index.md b/src/templates/cookbook/index.md index 0c3b73f..8ce2885 100644 --- a/src/templates/cookbook/index.md +++ b/src/templates/cookbook/index.md @@ -30,6 +30,7 @@ Browse these specialized query patterns for common use cases: - **methods** - Finding class methods by name, finding all methods in a class, counting method usages - **references** - Tracking symbol usage, finding most referenced symbols, identifying dead code - **exports** - Distinguishing exported symbols from internal ones, finding public API functions/types +- **agent-setup** - Setting up dora hooks, extensions, and skills for AI agents (Claude Code, pi, OpenCode, Cursor, Windsurf) ## Common JOIN Patterns diff --git a/src/utils/templates.ts b/src/utils/templates.ts index b746649..80c0261 100644 --- a/src/utils/templates.ts +++ b/src/utils/templates.ts @@ -20,6 +20,9 @@ import cookbookReferencesMd from "../templates/cookbook/references.md" with { import cookbookExportsMd from "../templates/cookbook/exports.md" with { type: "text", }; +import cookbookAgentSetupMd from "../templates/cookbook/agent-setup.md" with { + type: "text", +}; /** * Copy a single file if it doesn't exist at target @@ -74,6 +77,10 @@ export async function copyTemplates(targetDoraDir: string): Promise { content: cookbookExportsMd, target: join(targetDoraDir, "cookbook", "exports.md"), }, + { + content: cookbookAgentSetupMd, + target: join(targetDoraDir, "cookbook", "agent-setup.md"), + }, ]; // Create subdirectories