From 91cca23d234f30dac767fd28d2620e7782d6f23a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 5 Jan 2026 02:41:08 +0100 Subject: [PATCH] Add migration for commands->prompts, warn about deprecated hooks/tools dirs - Auto-migrate commands/ to prompts/ on startup - Warn if hooks/ or tools/ directories contain custom extensions - Show deprecation warnings in interactive mode with keypress to continue - Update CHANGELOG and docs with full migration guide --- AGENTS.md | 2 +- README.md | 5 + packages/coding-agent/CHANGELOG.md | 191 +++++++++++++++++ packages/coding-agent/README.md | 258 +++++++++++++++++++---- packages/coding-agent/docs/extensions.md | 6 +- packages/coding-agent/src/main.ts | 11 +- packages/coding-agent/src/migrations.ts | 118 ++++++++++- todo.md | 6 - 8 files changed, 540 insertions(+), 57 deletions(-) delete mode 100644 todo.md diff --git a/AGENTS.md b/AGENTS.md index b71720cb..2befbcf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t - Always ask before removing functionality or code that appears to be intentional ## Commands -- After code changes: `npm run check` (get full output, no tail) +- After code changes (not documentation changes): `npm run check` (get full output, no tail) - NEVER run: `npm run dev`, `npm run build`, `npm test` - Only run specific tests if user instructs: `npm test -- test/specific.test.ts` - NEVER commit unless user asks diff --git a/README.md b/README.md index 9a9d5e38..ab7f0a37 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,11 @@ cd packages/coding-agent && npx tsx src/cli.ts cd packages/pods && npx tsx src/cli.ts ``` +To run tests that don't require an LLM endpoint: +```bash +./test.sh +``` + ### Versioning (Lockstep) **All packages MUST always have the same version number.** Use these commands to bump versions: diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3674fc82..81e0295a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,197 @@ ## [Unreleased] +This release unifies hooks and custom tools into a single "extensions" system and renames "slash commands" to "prompt templates". ([#454](https://github.com/badlogic/pi-mono/issues/454)) + +**Before migrating, read:** +- [docs/extensions.md](docs/extensions.md) - Full API reference +- [README.md](README.md) - Extensions section with examples +- [examples/extensions/](examples/extensions/) - Working examples + +### Extensions Migration + +Hooks and custom tools are now unified as **extensions**. Both were TypeScript modules exporting a factory function that receives an API object. Now there's one concept, one discovery location, one CLI flag, one settings.json entry. + +**No automatic file migration.** You must manually: +1. Move files from `hooks/` and `tools/` directories to `extensions/` +2. Move files from `commands/` to `prompts/` +3. Update imports and type names in your extension code +4. Update `settings.json` if you have explicit hook and custom tool paths configured + +**Directory changes:** +``` +# Before +~/.pi/agent/hooks/*.ts → ~/.pi/agent/extensions/*.ts +~/.pi/agent/tools/*.ts → ~/.pi/agent/extensions/*.ts +.pi/hooks/*.ts → .pi/extensions/*.ts +.pi/tools/*.ts → .pi/extensions/*.ts +``` + +**Extension discovery rules** (in `extensions/` directories): +1. **Direct files:** `extensions/*.ts` or `*.js` → loaded directly +2. **Subdirectory with index:** `extensions/myext/index.ts` → loaded as single extension +3. **Subdirectory with package.json:** `extensions/myext/package.json` with `"pi"` field → loads declared paths + +```json +// extensions/my-package/package.json +{ + "name": "my-extension-package", + "dependencies": { "lodash": "^4.0.0" }, + "pi": { + "extensions": ["./src/main.ts", "./src/tools.ts"] + } +} +``` + +No recursion beyond one level. Complex packages must use the `package.json` manifest. Dependencies in `package.json` are resolved via jiti. + +**Type renames:** +- `HookAPI` → `ExtensionAPI` +- `HookContext` → `ExtensionContext` +- `HookCommandContext` → `ExtensionCommandContext` +- `HookUIContext` → `ExtensionUIContext` +- `CustomToolAPI` → `ExtensionAPI` (merged) +- `CustomToolContext` → `ExtensionContext` (merged) +- `CustomToolUIContext` → `ExtensionUIContext` +- `CustomTool` → `ToolDefinition` +- `CustomToolFactory` → `ExtensionFactory` +- `HookMessage` → `CustomMessage` + +**Import changes:** +```typescript +// Before (hook) +import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent"; +export default function (pi: HookAPI) { ... } + +// Before (custom tool) +import type { CustomToolFactory } from "@mariozechner/pi-coding-agent"; +const factory: CustomToolFactory = (pi) => ({ name: "my_tool", ... }); +export default factory; + +// After (both are now extensions) +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +export default function (pi: ExtensionAPI) { + pi.on("tool_call", async (event, ctx) => { ... }); + pi.registerTool({ name: "my_tool", ... }); +} +``` + +**Custom tools now have full context access.** Tools registered via `pi.registerTool()` now receive the same `ctx` object that event handlers receive. Previously, custom tools had limited context. Now all extension code shares the same capabilities: + +- `pi.registerTool()` - Register tools the LLM can call +- `pi.registerCommand()` - Register commands like `/mycommand` +- `pi.registerShortcut()` - Register keyboard shortcuts (shown in `/hotkeys`) +- `pi.registerFlag()` - Register CLI flags (shown in `--help`) +- `pi.registerMessageRenderer()` - Custom TUI rendering for message types +- `pi.on()` - Subscribe to lifecycle events (tool_call, session_start, etc.) +- `pi.sendMessage()` - Inject messages into the conversation +- `pi.appendEntry()` - Persist custom data in session (survives restart/branch) +- `pi.exec()` - Run shell commands +- `pi.getActiveTools()` / `pi.setActiveTools()` - Dynamic tool enable/disable +- `pi.getAllTools()` - List all available tools +- `pi.events` - Event bus for cross-extension communication +- `ctx.ui.confirm()` / `select()` / `input()` - User prompts +- `ctx.ui.notify()` - Toast notifications +- `ctx.ui.setStatus()` - Persistent status in footer (multiple extensions can set their own) +- `ctx.ui.setWidget()` - Widget display above editor +- `ctx.ui.setTitle()` - Set terminal window title +- `ctx.ui.custom()` - Full TUI component with keyboard handling +- `ctx.ui.editor()` - Multi-line text editor with external editor support +- `ctx.sessionManager` - Read session entries, get branch history + +**Settings changes:** +```json +// Before +{ + "hooks": ["./my-hook.ts"], + "customTools": ["./my-tool.ts"] +} + +// After +{ + "extensions": ["./my-extension.ts"] +} +``` + +**CLI changes:** +```bash +# Before +pi --hook ./safety.ts --tool ./todo.ts + +# After +pi --extension ./safety.ts -e ./todo.ts +``` + +### Prompt Templates Migration + +"Slash commands" (markdown files defining reusable prompts invoked via `/name`) are renamed to "prompt templates" to avoid confusion with extension-registered commands. + +**Automatic migration:** The `commands/` directory is automatically renamed to `prompts/` on startup (if `prompts/` doesn't exist). Works for both regular directories and symlinks. + +**Directory changes:** +``` +~/.pi/agent/commands/*.md → ~/.pi/agent/prompts/*.md +.pi/commands/*.md → .pi/prompts/*.md +``` + +**SDK type renames:** +- `FileSlashCommand` → `PromptTemplate` +- `LoadSlashCommandsOptions` → `LoadPromptTemplatesOptions` + +**SDK function renames:** +- `discoverSlashCommands()` → `discoverPromptTemplates()` +- `loadSlashCommands()` → `loadPromptTemplates()` +- `expandSlashCommand()` → `expandPromptTemplate()` +- `getCommandsDir()` → `getPromptsDir()` + +**SDK option renames:** +- `CreateAgentSessionOptions.slashCommands` → `.promptTemplates` +- `AgentSession.fileCommands` → `.promptTemplates` +- `PromptOptions.expandSlashCommands` → `.expandPromptTemplates` + +### SDK Migration + +**Discovery functions:** +- `discoverAndLoadHooks()` → `discoverAndLoadExtensions()` +- `discoverAndLoadCustomTools()` → merged into `discoverAndLoadExtensions()` +- `loadHooks()` → `loadExtensions()` +- `loadCustomTools()` → merged into `loadExtensions()` + +**Runner and wrapper:** +- `HookRunner` → `ExtensionRunner` +- `wrapToolsWithHooks()` → `wrapToolsWithExtensions()` +- `wrapToolWithHook()` → `wrapToolWithExtensions()` + +**CreateAgentSessionOptions:** +- `.hooks` → `.extensions` +- `.customTools` → merged into `.extensions` +- `.slashCommands` → `.promptTemplates` + +**AgentSession:** +- `.hookRunner` → `.extensionRunner` +- `.fileCommands` → `.promptTemplates` +- `.sendHookMessage()` → `.sendCustomMessage()` + +### Session Migration + +**Automatic.** Session version bumped from 2 to 3. Existing sessions are migrated on first load: +- Message role `"hookMessage"` → `"custom"` + +### Breaking Changes + +- **Settings:** `hooks` and `customTools` arrays replaced with single `extensions` array +- **CLI:** `--hook` and `--tool` flags replaced with `--extension` / `-e` +- **Directories:** `hooks/`, `tools/` → `extensions/`; `commands/` → `prompts/` +- **Types:** See type renames above +- **SDK:** See SDK migration above + +### Changed + +- Extensions can have their own `package.json` with dependencies (resolved via jiti) +- Documentation: `docs/hooks.md` and `docs/custom-tools.md` merged into `docs/extensions.md` +- Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/` +- README: Extensions section expanded with custom tools, commands, events, state persistence, shortcuts, flags, and UI examples + ## [0.34.2] - 2026-01-04 ## [0.34.1] - 2026-01-04 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index a5c7f86c..23007862 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -373,10 +373,9 @@ The output becomes part of your next prompt, formatted as: ``` Ran `ls -la` -``` + ``` -``` Run multiple commands before prompting; all outputs are included together. @@ -806,72 +805,248 @@ cd /path/to/brave-search && npm install ### Extensions -Extensions are TypeScript modules that extend pi's behavior. They can subscribe to lifecycle events, register custom tools, add commands, and more. +Extensions are TypeScript modules that extend pi's behavior. **Use cases:** -- **Register custom tools** (callable by the LLM, with custom UI and rendering) -- **Intercept events** (block commands, modify context/results, customize compaction) -- **Persist state** (store custom data in session, reconstruct on reload/branch) -- **External integrations** (file watchers, webhooks, git checkpointing) +- **Custom tools** - Register tools callable by the LLM with custom UI and rendering +- **Custom commands** - Add `/commands` for users (e.g., `/deploy`, `/stats`) +- **Event interception** - Block tool calls, modify results, customize compaction +- **State persistence** - Store data in session, reconstruct on reload/branch +- **External integrations** - File watchers, webhooks, git checkpointing +- **Custom UI** - Full TUI control from tools, commands, or event handlers -**Extension locations:** +**Locations:** - Global: `~/.pi/agent/extensions/*.ts` or `~/.pi/agent/extensions/*/index.ts` - Project: `.pi/extensions/*.ts` or `.pi/extensions/*/index.ts` - CLI: `--extension ` or `-e ` -**Quick example:** +**Dependencies:** Extensions can have their own dependencies. Place a `package.json` next to the extension (or in a parent directory), run `npm install`, and imports are resolved via [jiti](https://github.com/unjs/jiti). See [examples/extensions/with-deps/](examples/extensions/with-deps/). + +#### Custom Tools + +Tools are functions the LLM can call. They appear in the system prompt and can have custom rendering. ```typescript import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; +import { Text } from "@mariozechner/pi-tui"; export default function (pi: ExtensionAPI) { - // Subscribe to events - pi.on("tool_call", async (event, ctx) => { - if (event.toolName === "bash" && /sudo/.test(event.input.command as string)) { - const ok = await ctx.ui.confirm("Allow sudo?", event.input.command as string); - if (!ok) return { block: true, reason: "Blocked by user" }; - } - }); - - // Register a custom tool pi.registerTool({ - name: "greet", - label: "Greeting", - description: "Generate a greeting", + name: "deploy", + label: "Deploy", + description: "Deploy the application to production", parameters: Type.Object({ - name: Type.String({ description: "Name to greet" }), + environment: Type.String({ description: "Target environment" }), }), + async execute(toolCallId, params, onUpdate, ctx, signal) { + // Show progress via onUpdate + onUpdate({ status: "Deploying..." }); + + // Ask user for confirmation + const ok = await ctx.ui.confirm("Deploy?", `Deploy to ${params.environment}?`); + if (!ok) { + return { content: [{ type: "text", text: "Cancelled" }], details: { cancelled: true } }; + } + + // Run shell commands + const result = await ctx.exec("./deploy.sh", [params.environment], { signal }); + return { - content: [{ type: "text", text: `Hello, ${params.name}!` }], - details: {}, + content: [{ type: "text", text: result.stdout }], + details: { environment: params.environment, exitCode: result.exitCode }, }; }, - }); - // Register a command - pi.registerCommand("hello", { - description: "Say hello", - handler: async (args, ctx) => { - ctx.ui.notify(`Hello ${args || "world"}!`, "info"); + // Custom TUI rendering (optional) + renderCall(args, theme) { + return new Text(theme.bold("deploy ") + theme.fg("accent", args.environment), 0, 0); + }, + renderResult(result, options, theme) { + const ok = result.details?.exitCode === 0; + return new Text(ok ? theme.fg("success", "✓ Deployed") : theme.fg("error", "✗ Failed"), 0, 0); }, }); } ``` -**Features:** -- Event handlers: `pi.on("tool_call", ...)`, `pi.on("session_start", ...)`, etc. -- Custom tools: `pi.registerTool({ name, execute, renderResult, ... })` -- Commands: `pi.registerCommand("name", { handler })` -- Keyboard shortcuts: `pi.registerShortcut("ctrl+x", { handler })` -- CLI flags: `pi.registerFlag("--my-flag", { ... })` -- UI access: `ctx.ui.confirm()`, `ctx.ui.select()`, `ctx.ui.input()` -- Shell execution: `pi.exec("git", ["status"])` -- Message injection: `pi.sendMessage({ content, ... }, { triggerTurn: true })` +#### Custom Commands -> See [Extensions Documentation](docs/extensions.md) for full API reference. pi can help you create extensions. +Commands are user-invoked via `/name`. They can show custom UI, modify state, or trigger agent turns. +```typescript +export default function (pi: ExtensionAPI) { + pi.registerCommand("stats", { + description: "Show session statistics", + handler: async (args, ctx) => { + // Simple notification + ctx.ui.notify(`${ctx.sessionManager.getEntries().length} entries`, "info"); + }, + }); + + pi.registerCommand("todos", { + description: "Interactive todo viewer", + handler: async (args, ctx) => { + // Full custom UI with keyboard handling + await ctx.ui.custom((tui, theme, done) => { + return { + render(width) { + return [ + theme.bold("Todos"), + "- [ ] Item 1", + "- [x] Item 2", + "", + theme.fg("dim", "Press Escape to close"), + ]; + }, + handleInput(data) { + if (matchesKey(data, "escape")) done(); + }, + }; + }); + }, + }); +} +``` + +#### Event Interception + +Subscribe to lifecycle events to block, modify, or observe agent behavior. + +```typescript +export default function (pi: ExtensionAPI) { + // Block dangerous commands + pi.on("tool_call", async (event, ctx) => { + if (event.toolName === "bash" && /rm -rf/.test(event.input.command as string)) { + const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?"); + if (!ok) return { block: true, reason: "Blocked by user" }; + } + }); + + // Modify tool results + pi.on("tool_result", async (event, ctx) => { + if (event.toolName === "read") { + // Redact secrets from file contents + return { modifiedResult: event.result.replace(/API_KEY=\w+/g, "API_KEY=***") }; + } + }); + + // Custom compaction + pi.on("session_before_compact", async (event, ctx) => { + return { customSummary: "My custom summary of the conversation so far..." }; + }); + + // Git checkpoint on each turn + pi.on("turn_end", async (event, ctx) => { + await ctx.exec("git", ["stash", "push", "-m", `pi-checkpoint-${Date.now()}`]); + }); +} +``` + +#### State Persistence + +Store state in session entries that survive reload and work correctly with branching. + +```typescript +export default function (pi: ExtensionAPI) { + let counter = 0; + + // Reconstruct state from session history + const reconstruct = (ctx) => { + counter = 0; + for (const entry of ctx.sessionManager.getBranch()) { + if (entry.type === "custom" && entry.customType === "my_counter") { + counter = entry.data.value; + } + } + }; + + pi.on("session_start", async (e, ctx) => reconstruct(ctx)); + pi.on("session_branch", async (e, ctx) => reconstruct(ctx)); + pi.on("session_tree", async (e, ctx) => reconstruct(ctx)); + + pi.registerCommand("increment", { + handler: async (args, ctx) => { + counter++; + ctx.appendEntry("my_counter", { value: counter }); // Persisted in session + ctx.ui.notify(`Counter: ${counter}`, "info"); + }, + }); +} +``` + +#### Keyboard Shortcuts + +Register custom keyboard shortcuts (shown in `/hotkeys`): + +```typescript +export default function (pi: ExtensionAPI) { + pi.registerShortcut("ctrl+shift+d", { + description: "Deploy to production", + handler: async (ctx) => { + ctx.ui.notify("Deploying...", "info"); + await ctx.exec("./deploy.sh", []); + }, + }); +} +``` + +#### CLI Flags + +Register custom CLI flags (parsed automatically, shown in `--help`): + +```typescript +export default function (pi: ExtensionAPI) { + pi.registerFlag("--dry-run", { + description: "Run without making changes", + type: "boolean", + }); + + pi.on("tool_call", async (event, ctx) => { + if (pi.getFlag("dry-run") && event.toolName === "write") { + return { block: true, reason: "Dry run mode" }; + } + }); +} +``` + +#### Custom UI + +Extensions have full TUI access via `ctx.ui`: + +```typescript +// Simple prompts +const confirmed = await ctx.ui.confirm("Title", "Are you sure?"); +const choice = await ctx.ui.select("Pick one", ["Option A", "Option B"]); +const text = await ctx.ui.input("Enter value"); + +// Notifications +ctx.ui.notify("Done!", "success"); // success, info, warning, error + +// Status line (persistent in footer, multiple extensions can set their own) +ctx.ui.setStatus("my-ext", "Processing..."); +ctx.ui.setStatus("my-ext", null); // Clear + +// Widgets (above editor) +ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); + +// Full custom component with keyboard handling +await ctx.ui.custom((tui, theme, done) => ({ + render(width) { + return [ + theme.bold("My Component"), + theme.fg("dim", "Press Escape to close"), + ]; + }, + handleInput(data) { + if (matchesKey(data, "escape")) done(); + }, +})); +``` + +> See [docs/extensions.md](docs/extensions.md) for full API reference. +> See [docs/tui.md](docs/tui.md) for TUI components and custom rendering. > See [examples/extensions/](examples/extensions/) for working examples. --- @@ -1053,7 +1228,7 @@ pi --export session.jsonl # Auto-generated filename pi --export session.jsonl output.html # Custom filename ``` -Works with both session files and streaming event logs from `--mode json`. +Works with session files. --- @@ -1120,3 +1295,4 @@ MIT - [@mariozechner/pi-ai](https://www.npmjs.com/package/@mariozechner/pi-ai): Core LLM toolkit - [@mariozechner/pi-agent](https://www.npmjs.com/package/@mariozechner/pi-agent): Agent framework +- [@mariozechner/pi-tui](https://www.npmjs.com/package/@mariozechner/pi-tui): Terminal UI components diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index d67b68f7..5ae6a63b 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -15,13 +15,15 @@ Extensions are TypeScript modules that extend pi's behavior. They can subscribe **Example use cases:** - Permission gates (confirm before `rm -rf`, `sudo`, etc.) -- Git checkpointing (stash at each turn, restore on `/branch`) +- Git checkpointing (stash at each turn, restore on branch) - Path protection (block writes to `.env`, `node_modules/`) +- Custom compaction (summarize conversation your way) - Interactive tools (questions, wizards, custom dialogs) - Stateful tools (todo lists, connection pools) - External integrations (file watchers, webhooks, CI triggers) +- Games while you wait (see `snake.ts` example) -See [examples/extensions/](../examples/extensions/) and [examples/hooks/](../examples/hooks/) for working implementations. +See [examples/extensions/](../examples/extensions/) for working implementations. ## Quick Start diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 5dbe1d82..d740b7fa 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -27,7 +27,7 @@ import { SettingsManager } from "./core/settings-manager.js"; import { resolvePromptInput } from "./core/system-prompt.js"; import { printTimings, time } from "./core/timings.js"; import { allTools } from "./core/tools/index.js"; -import { runMigrations } from "./migrations.js"; +import { runMigrations, showDeprecationWarnings } from "./migrations.js"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js"; import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js"; @@ -282,8 +282,8 @@ function buildSessionOptions( export async function main(args: string[]) { time("start"); - // Run migrations - const { migratedAuthProviders: migratedProviders } = runMigrations(); + // Run migrations (pass cwd for project-local migrations) + const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd()); // Create AuthStorage and ModelRegistry upfront const authStorage = discoverAuthStorage(); @@ -366,6 +366,11 @@ export async function main(args: string[]) { initTheme(settingsManager.getTheme(), isInteractive); time("initTheme"); + // Show deprecation warnings in interactive mode + if (isInteractive && deprecationWarnings.length > 0) { + await showDeprecationWarnings(deprecationWarnings); + } + let scopedModels: ScopedModel[] = []; const modelPatterns = parsed.models ?? settingsManager.getEnabledModels(); if (modelPatterns && modelPatterns.length > 0) { diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index f2bda674..f5b058ac 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -2,9 +2,14 @@ * One-time migrations that run on startup. */ +import chalk from "chalk"; import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs"; import { dirname, join } from "path"; -import { getAgentDir } from "./config.js"; +import { CONFIG_DIR_NAME, getAgentDir } from "./config.js"; + +const MIGRATION_GUIDE_URL = + "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration"; +const EXTENSIONS_DOC_URL = "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md"; /** * Migrate legacy oauth.json and settings.json apiKeys to auth.json. @@ -123,13 +128,118 @@ export function migrateSessionsFromAgentRoot(): void { } } +/** + * Migrate commands/ to prompts/ if needed. + * Works for both regular directories and symlinks. + */ +function migrateCommandsToPrompts(baseDir: string, label: string): boolean { + const commandsDir = join(baseDir, "commands"); + const promptsDir = join(baseDir, "prompts"); + + if (existsSync(commandsDir) && !existsSync(promptsDir)) { + try { + renameSync(commandsDir, promptsDir); + console.log(chalk.green(`Migrated ${label} commands/ → prompts/`)); + return true; + } catch (err) { + console.log( + chalk.yellow( + `Warning: Could not migrate ${label} commands/ to prompts/: ${err instanceof Error ? err.message : err}`, + ), + ); + } + } + return false; +} + +/** + * Check for deprecated hooks/ and tools/ directories. + * Note: tools/ may contain fd/rg binaries extracted by pi, so only warn if it has other files. + */ +function checkDeprecatedExtensionDirs(baseDir: string, label: string): string[] { + const hooksDir = join(baseDir, "hooks"); + const toolsDir = join(baseDir, "tools"); + const warnings: string[] = []; + + if (existsSync(hooksDir)) { + warnings.push(`${label} hooks/ directory found. Hooks have been renamed to extensions.`); + } + + if (existsSync(toolsDir)) { + // Check if tools/ contains anything other than fd/rg (which are auto-extracted binaries) + try { + const entries = readdirSync(toolsDir); + const customTools = entries.filter((e) => e !== "fd" && e !== "rg"); + if (customTools.length > 0) { + warnings.push( + `${label} tools/ directory contains custom tools. Custom tools have been merged into extensions.`, + ); + } + } catch { + // Ignore read errors + } + } + + return warnings; +} + +/** + * Run extension system migrations (commands→prompts) and collect warnings about deprecated directories. + */ +function migrateExtensionSystem(cwd: string): string[] { + const agentDir = getAgentDir(); + const projectDir = join(cwd, CONFIG_DIR_NAME); + + // Migrate commands/ to prompts/ + migrateCommandsToPrompts(agentDir, "Global"); + migrateCommandsToPrompts(projectDir, "Project"); + + // Check for deprecated directories + const warnings = [ + ...checkDeprecatedExtensionDirs(agentDir, "Global"), + ...checkDeprecatedExtensionDirs(projectDir, "Project"), + ]; + + return warnings; +} + +/** + * Print deprecation warnings and wait for keypress. + */ +export async function showDeprecationWarnings(warnings: string[]): Promise { + if (warnings.length === 0) return; + + for (const warning of warnings) { + console.log(chalk.yellow(`Warning: ${warning}`)); + } + console.log(chalk.yellow(`\nMove your extensions to the extensions/ directory.`)); + console.log(chalk.yellow(`Migration guide: ${MIGRATION_GUIDE_URL}`)); + console.log(chalk.yellow(`Documentation: ${EXTENSIONS_DOC_URL}`)); + console.log(chalk.dim(`\nPress any key to continue...`)); + + await new Promise((resolve) => { + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.once("data", () => { + process.stdin.setRawMode?.(false); + process.stdin.pause(); + resolve(); + }); + }); + console.log(); +} + /** * Run all migrations. Called once on startup. * - * @returns Object with migration results + * @returns Object with migration results and deprecation warnings */ -export function runMigrations(): { migratedAuthProviders: string[] } { +export function runMigrations(cwd: string = process.cwd()): { + migratedAuthProviders: string[]; + deprecationWarnings: string[]; +} { const migratedAuthProviders = migrateAuthToAuthJson(); migrateSessionsFromAgentRoot(); - return { migratedAuthProviders }; + const deprecationWarnings = migrateExtensionSystem(cwd); + return { migratedAuthProviders, deprecationWarnings }; } diff --git a/todo.md b/todo.md deleted file mode 100644 index 3ae3823f..00000000 --- a/todo.md +++ /dev/null @@ -1,6 +0,0 @@ -# Export HTML TODOs - -- [x] "Ctrl+T toggle thinking · Ctrl+O toggle tools" font size is not the same as all the other font sizes -- [ ] System prompt doesn't show included AGENTS.md, skills. See `packages/coding-agent/src/core/system-prompt.ts`. Can only be done if we export live from a session, not via `--export` CLI flag -- [x] "Available Tools" has no newline after it -- [x] `read` tool has too much vertical spacing between tool call header and tool result, and also with tool bottom border (was white-space: pre-wrap on .tool-output preserving template literal whitespace)