diff options
Diffstat (limited to 'pi/agent/extensions/ask-mode')
| -rw-r--r-- | pi/agent/extensions/ask-mode/README.md | 84 | ||||
| -rw-r--r-- | pi/agent/extensions/ask-mode/index.ts | 183 | ||||
| -rw-r--r-- | pi/agent/extensions/ask-mode/utils.ts | 94 |
3 files changed, 0 insertions, 361 deletions
diff --git a/pi/agent/extensions/ask-mode/README.md b/pi/agent/extensions/ask-mode/README.md deleted file mode 100644 index 2c0d17c..0000000 --- a/pi/agent/extensions/ask-mode/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Ask Mode - -Exploration-only mode for Pi. - -This extension adds a session-scoped `/ask` mode that turns Pi into a read-only -investigation assistant. It is meant for understanding a codebase, debugging, -reading logs, or answering questions without making changes. - -## What It Does - -- `/ask` enters ask mode -- `/ask <prompt>` enters ask mode and immediately sends the prompt -- `/ask-exit` leaves ask mode -- `/ask-status` shows whether ask mode is active -- limits tools to `read`, `bash`, `grep`, `find`, and `ls` -- blocks unsafe bash commands even though `bash` stays enabled -- injects per-turn instructions telling the model to inspect and explain, not implement - -## Usage Flows - -### Flow 1: Enter ask mode first, then explore - -```text -/ask -``` - -Then ask questions naturally: - -```text -Why does VM2 fail to reach readiness on the first create attempt? -``` - -### Flow 2: Enter ask mode and ask immediately - -```text -/ask Compare the fresh-subagent extension behavior with what the README claims. -``` - -### Flow 3: Leave ask mode - -```text -/ask-exit -``` - -That restores the previously active tool set. - -### Flow 4: Check whether you are still in ask mode - -```text -/ask-status -``` - -## Safety Model - -Ask mode is meant for exploration only. - -- `edit` and `write` are removed from the active tool set -- custom tools outside the ask-mode allowlist are blocked -- `bash` remains available, but only for safe read-only commands - -Examples of the kind of bash commands ask mode allows: - -- `rg foo src` -- `git diff` -- `ls -la` -- `sed -n '1,120p' file` -- `curl http://host/...` - -Examples it blocks: - -- `rm` -- `touch` -- `mkdir` -- `git commit` -- `npm install` -- `sudo ...` -- shell redirection that writes files - -## Notes And Limits - -- This is session-scoped and restores on resume if the session was left in ask mode. -- It is intended for investigation, not planning or implementation. -- If you ask for a change while ask mode is active, Pi should explain what would - need to change instead of making the change. diff --git a/pi/agent/extensions/ask-mode/index.ts b/pi/agent/extensions/ask-mode/index.ts deleted file mode 100644 index 4f19815..0000000 --- a/pi/agent/extensions/ask-mode/index.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { TextContent } from "@mariozechner/pi-ai"; -import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { isSafeAskModeCommand } from "./utils.js"; - -const ASK_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"]; -const STATE_TYPE = "ask-mode"; -const CONTEXT_TYPE = "ask-mode-context"; - -interface AskModeState { - enabled: boolean; - normalTools: string[]; -} - -function hasAskModeMarker(message: AgentMessage): boolean { - const customMessage = message as AgentMessage & { customType?: string }; - if (customMessage.customType === CONTEXT_TYPE) return true; - - if (message.role !== "user") return false; - if (typeof message.content === "string") return message.content.includes("[ASK MODE ACTIVE]"); - if (!Array.isArray(message.content)) return false; - - return message.content.some( - (block) => block.type === "text" && (block as TextContent).text?.includes("[ASK MODE ACTIVE]"), - ); -} - -export default function askModeExtension(pi: ExtensionAPI): void { - let askModeEnabled = false; - let normalTools: string[] = []; - - function persistState(): void { - pi.appendEntry<AskModeState>(STATE_TYPE, { - enabled: askModeEnabled, - normalTools, - }); - } - - function updateStatus(ctx: ExtensionContext): void { - if (!askModeEnabled) { - ctx.ui.setStatus("ask-mode", undefined); - ctx.ui.setWidget("ask-mode", undefined); - return; - } - - ctx.ui.setStatus("ask-mode", ctx.ui.theme.fg("warning", "⏸ ask")); - ctx.ui.setWidget("ask-mode", [ - ctx.ui.theme.fg("warning", "Ask mode"), - "Exploration only", - "Files are read-only", - "Bash is restricted to safe read-only commands", - ]); - } - - function enterAskMode(ctx: ExtensionContext): void { - if (askModeEnabled) { - updateStatus(ctx); - return; - } - - normalTools = pi.getActiveTools(); - askModeEnabled = true; - pi.setActiveTools(ASK_MODE_TOOLS); - ctx.ui.notify(`Ask mode enabled. Tools: ${ASK_MODE_TOOLS.join(", ")}`, "info"); - updateStatus(ctx); - persistState(); - } - - function exitAskMode(ctx: ExtensionContext): void { - if (!askModeEnabled) { - ctx.ui.notify("Ask mode is not active.", "info"); - updateStatus(ctx); - return; - } - - askModeEnabled = false; - pi.setActiveTools(normalTools.length > 0 ? normalTools : ["read", "bash", "edit", "write"]); - ctx.ui.notify("Ask mode disabled. Previous tools restored.", "info"); - updateStatus(ctx); - persistState(); - } - - pi.registerCommand("ask", { - description: "Enter ask mode for exploration-only work. Optional prompt sends a question immediately.", - handler: async (args, ctx) => { - const prompt = args.trim(); - enterAskMode(ctx); - if (prompt) { - pi.sendUserMessage(prompt); - if (!ctx.hasUI) { - await ctx.waitForIdle(); - } - } - }, - }); - - pi.registerCommand("ask-exit", { - description: "Leave ask mode and restore the previous tool set", - handler: async (_args, ctx) => exitAskMode(ctx), - }); - - pi.registerCommand("ask-status", { - description: "Show whether ask mode is active", - handler: async (_args, ctx) => { - const message = askModeEnabled - ? `Ask mode active. Tools: ${ASK_MODE_TOOLS.join(", ")}` - : "Ask mode is not active."; - if (!ctx.hasUI) { - process.stdout.write(`${message}\n`); - return; - } - ctx.ui.notify(message, "info"); - }, - }); - - pi.on("tool_call", async (event) => { - if (!askModeEnabled) return; - - if (!ASK_MODE_TOOLS.includes(event.toolName)) { - return { - block: true, - reason: `Ask mode: tool "${event.toolName}" is disabled. Use /ask-exit before modifying files or using other tools.`, - }; - } - - if (event.toolName === "bash") { - const command = String(event.input.command ?? ""); - if (!isSafeAskModeCommand(command)) { - return { - block: true, - reason: `Ask mode: bash command blocked (not recognized as safe read-only exploration).\nCommand: ${command}`, - }; - } - } - }); - - pi.on("context", async (event) => { - if (askModeEnabled) return; - return { - messages: event.messages.filter((message) => !hasAskModeMarker(message as AgentMessage)), - }; - }); - - pi.on("before_agent_start", async () => { - if (!askModeEnabled) return; - - return { - message: { - customType: CONTEXT_TYPE, - content: `[ASK MODE ACTIVE] -You are in ask mode: exploration only. - -Rules: -- Do not modify files. -- Do not use edit or write tools. -- Use read, grep, find, ls, and only safe read-only bash commands. -- Inspect, explain, compare, summarize, and answer questions. -- If a requested action would require a file change, say so explicitly instead of doing it. - -Focus on observation and analysis, not implementation.`, - display: false, - }, - }; - }); - - pi.on("session_start", async (_event, ctx) => { - const entries = ctx.sessionManager.getEntries(); - const latestState = entries - .filter((entry: { type: string; customType?: string }) => entry.type === "custom" && entry.customType === STATE_TYPE) - .pop() as { data?: AskModeState } | undefined; - - if (latestState?.data) { - askModeEnabled = latestState.data.enabled ?? askModeEnabled; - normalTools = latestState.data.normalTools ?? normalTools; - } - - if (askModeEnabled) { - pi.setActiveTools(ASK_MODE_TOOLS); - } - - updateStatus(ctx); - }); -} diff --git a/pi/agent/extensions/ask-mode/utils.ts b/pi/agent/extensions/ask-mode/utils.ts deleted file mode 100644 index db8c889..0000000 --- a/pi/agent/extensions/ask-mode/utils.ts +++ /dev/null @@ -1,94 +0,0 @@ -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, -]; - -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/, -]; - -export function isSafeAskModeCommand(command: string): boolean { - const isDestructive = DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command)); - const isSafe = SAFE_PATTERNS.some((pattern) => pattern.test(command)); - return !isDestructive && isSafe; -} |
