diff --git a/.cursor/.gitignore b/.cursor/.gitignore new file mode 100644 index 0000000..8bf7cc2 --- /dev/null +++ b/.cursor/.gitignore @@ -0,0 +1 @@ +plans/ diff --git a/.cursor/rules/development-workflow.mdc b/.cursor/rules/development-workflow.mdc index 15e7143..03fc6f9 100644 --- a/.cursor/rules/development-workflow.mdc +++ b/.cursor/rules/development-workflow.mdc @@ -1,6 +1,6 @@ --- -description: Development workflow, scripts, and common tasks -globs: +description: Development workflow and common commands +globs: "**/*" alwaysApply: true --- @@ -8,19 +8,16 @@ alwaysApply: true ## Prerequisites -- Node.js 18+ (required) -- Bun 1.0+ (preferred, npm as fallback) +- Bun 1.0+ (required) ## Common Commands ```bash # Install dependencies -bun install # preferred -npm install # fallback +bun install -# Development (runs from source) -bun run dev # uses Bun runtime -bun run dev:node # uses tsx (Node.js + TypeScript) +# Development (runs from source with Bun) +bun run dev # Build (compile TypeScript to dist/) bun run build @@ -32,19 +29,17 @@ bun run typecheck bun run lint # Run the built CLI -bun run start # Node.js -bun run start:bun # Bun runtime +bun run start ``` ## Environment Variables | Variable | Required | Description | -|----------|----------|-------------| -| `GROK_API_KEY` | Yes | API key for Grok / OpenAI-compatible provider | +|----|----|----| +| `GROK_API_KEY` | Yes | API key for xAI Grok | | `GROK_BASE_URL` | No | Custom API endpoint (default: `https://api.x.ai/v1`) | -| `GROK_MODEL` | No | Model override (default: `grok-code-fast-1`) | -| `GROK_MAX_TOKENS` | No | Max tokens per response (default: 1536) | -| `MORPH_API_KEY` | No | Enables Morph Fast Apply editing | +| `GROK_MODEL` | No | Model override (default: `grok-4-1-fast`) | +| `GROK_MAX_TOKENS` | No | Max tokens per response (default: 16384) | Copy `.env.example` to `.env` and fill in your values. @@ -58,5 +53,6 @@ Copy `.env.example` to `.env` and fill in your values. ## Project Structure Conventions - Source code lives in `src/`, compiled output in `dist/` (gitignored). -- No test framework is configured yet — if adding tests, prefer Vitest for ESM compatibility. - The CLI entry point is `src/index.ts` which uses Commander.js for argument parsing. +- UI is built with OpenTUI React (`@opentui/react`). +- Only tool is bash — all file operations happen through shell commands. diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc index cc5da0d..9f1b669 100644 --- a/.cursor/rules/project-overview.mdc +++ b/.cursor/rules/project-overview.mdc @@ -1,50 +1,63 @@ --- -description: Project overview and architecture for Grok CLI -globs: +description: Project overview and architecture reference +globs: "**/*" alwaysApply: true --- # Grok CLI — Project Overview -Grok CLI (`@vibe-kit/grok-cli`) is an open-source conversational AI CLI tool powered by Grok that brings AI assistance directly into the terminal. +Grok CLI (`@vibe-kit/grok-cli`) is an open-source AI coding agent powered by Grok that brings AI assistance directly into the terminal. ## Tech Stack - **Language**: TypeScript (ES2022, ESNext modules) -- **Runtime**: Node.js 18+ (Bun preferred) -- **UI Framework**: React with [Ink](https://github.com/vadimdemedes/ink) for terminal rendering +- **Runtime**: Bun (primary) +- **UI Framework**: React with [OpenTUI](https://github.com/anomalyco/opentui) for terminal rendering - **Build**: `tsc` (TypeScript compiler) -- **Package Manager**: Bun (primary), npm (fallback) -- **Linting**: ESLint with `@typescript-eslint` -- **API Client**: OpenAI SDK (used for OpenAI-compatible APIs including Grok) +- **Package Manager**: Bun +- **API Client**: OpenAI SDK (for OpenAI-compatible xAI API) +- **Tools**: Bash-only (all file operations via shell commands) +- **Search**: X Search API + Web Search via xAI Responses API ## Architecture ``` src/ -├── index.ts # CLI entry point (Commander.js) -├── agent/ # GrokAgent — orchestrates client, tools, MCP -├── grok/ # Grok API client and tool definitions -├── mcp/ # Model Context Protocol integration -├── commands/ # CLI subcommands (e.g. mcp) -├── tools/ # Built-in tools (bash, text-editor, search, etc.) -├── ui/ # Ink-based terminal UI -│ ├── app.tsx -│ ├── components/ # Chat interface, diff renderer, inputs, etc. -│ └── utils/ # Colors, markdown rendering, code colorizer -├── hooks/ # React hooks for input handling -├── utils/ # Settings, token counting, custom instructions -└── types/ # Shared TypeScript type definitions +├── index.ts # CLI entry point (Commander.js + OpenTUI) +├── agent/ +│ └── agent.ts # Agent — orchestrates client, bash tool, search +├── grok/ +│ ├── client.ts # Grok API client (Chat Completions + Responses API) +│ ├── models.ts # Model definitions and metadata +│ └── tools.ts # Tool schemas (bash, search_web, search_x) +├── tools/ +│ └── bash.ts # Bash command execution +├── ui/ +│ └── app.tsx # OpenTUI React terminal UI +├── utils/ +│ ├── settings.ts # User and project settings +│ └── instructions.ts # Custom instructions loader (.grok/GROK.md) +└── types/ + └── index.ts # Shared TypeScript types ``` ## Key Patterns -- **Agent loop**: `GrokAgent` runs an iterative tool-call loop — the LLM responds, tools execute, results feed back until no more tool calls remain or the round limit is hit. -- **Streaming**: `processUserMessageStream` is an async generator that yields chunks for real-time UI updates. -- **Settings hierarchy**: User-level (`~/.grok/user-settings.json`) → Project-level (`.grok/settings.json`) → System defaults. -- **MCP**: External tools are integrated via Model Context Protocol servers (stdio, HTTP, SSE transports). +- **Agent loop**: `Agent.processMessage()` is an async generator that yields `StreamChunk` objects — the LLM responds, tools execute, results feed back until no more tool calls remain. +- **Bash-only tools**: The agent uses bash for everything (file editing, searching, git, builds, etc.). +- **X Search & Web Search**: Integrated via the xAI Responses API for real-time information. +- **Settings hierarchy**: Environment variables → User-level (`~/.grok/user-settings.json`) → Project-level (`.grok/settings.json`). - **ESM only**: The project uses `"type": "module"` — all imports use `.js` extensions for compiled output. +## Latest Grok Models + +- grok-4-0709 (flagship reasoning) +- grok-4.20-beta-0309 (multi-agent, 2M context) +- grok-4-fast (fast reasoning, 2M context) +- grok-4-1-fast (latest fast, 2M context) +- grok-code-fast-1 (code optimized) +- grok-3, grok-3-mini + ## CI/CD - **typecheck.yml**: Runs `tsc --noEmit` on push/PR to `main`/`develop`. diff --git a/.cursor/rules/tools-and-agent.mdc b/.cursor/rules/tools-and-agent.mdc index 3818f42..81a02c0 100644 --- a/.cursor/rules/tools-and-agent.mdc +++ b/.cursor/rules/tools-and-agent.mdc @@ -6,27 +6,25 @@ alwaysApply: false # Agent & Tools System -## GrokAgent +## Agent -`GrokAgent` (in `src/agent/grok-agent.ts`) is the core orchestrator. It extends `EventEmitter` and manages: +`Agent` (in `src/agent/agent.ts`) is the core orchestrator. It manages: - The Grok API client (`GrokClient`) -- Built-in tools (text editor, bash, search, todo, confirmation) -- Optional Morph Fast Apply editor -- MCP server integration +- Bash tool for all shell operations +- Web search and X search via the Responses API - Chat history and message accumulation - Abort/cancellation support ### Agent Loop -The agent uses an iterative tool-call pattern: +The agent uses an iterative tool-call pattern via `processMessage` (async generator): -1. Send messages to the LLM. +1. Send messages to the LLM via Chat Completions API (streaming). 2. If the response contains `tool_calls`, execute each tool. 3. Append tool results to the message history. 4. Repeat until no tool calls remain or `maxToolRounds` is reached. - -Both `processUserMessage` (non-streaming) and `processUserMessageStream` (async generator) implement this loop. +5. Yield `StreamChunk` objects for real-time UI updates. ## Tool Interface @@ -37,7 +35,6 @@ interface ToolResult { success: boolean; output?: string; error?: string; - data?: any; } ``` @@ -47,23 +44,19 @@ Tools should never throw — wrap errors in `{ success: false, error: "..." }`. | Tool | File | Purpose | |------|------|---------| -| `TextEditorTool` | `src/tools/text-editor.ts` | View, create, and str_replace files | -| `MorphEditorTool` | `src/tools/morph-editor.ts` | Fast Apply editing (requires MORPH_API_KEY) | -| `BashTool` | `src/tools/bash.ts` | Execute shell commands | -| `SearchTool` | `src/tools/search.ts` | Unified text/file search (uses ripgrep) | -| `TodoTool` | `src/tools/todo-tool.ts` | Create/update visual todo lists | -| `ConfirmationTool` | `src/tools/confirmation-tool.ts` | User confirmation for operations | +| `BashTool` | `src/tools/bash.ts` | Execute any shell command | +| `search_web` | via `GrokClient.searchWeb()` | Web search using Responses API | +| `search_x` | via `GrokClient.searchX()` | X/Twitter search using Responses API | + +## Grok Client + +`GrokClient` (in `src/grok/client.ts`) handles two API endpoints: + +- **Chat Completions** (`/v1/chat/completions`): Main agent loop with streaming and tool calling +- **Responses API** (`/v1/responses`): X Search and Web Search with server-side tools ## Adding a New Tool -1. Create a new file in `src/tools/` implementing the tool class. -2. Export it from `src/tools/index.ts`. -3. Register the tool schema in `src/grok/tools.ts` (add to `GROK_TOOLS` array). -4. Add the execution case in `GrokAgent.executeTool()`. -5. Update the system prompt in the `GrokAgent` constructor to document the tool. - -## MCP Integration - -- MCP tools are prefixed with `mcp__` and dispatched through `executeMCPTool`. -- MCP config is loaded from `.grok/settings.json` under `mcpServers`. -- Supported transports: stdio, HTTP, SSE (defined in `src/mcp/transports.ts`). +1. Add the tool schema to `src/grok/tools.ts` (in the `TOOLS` array). +2. Add the execution case in `Agent.executeTool()`. +3. Update the system prompt in `Agent` to document the tool. diff --git a/.env.example b/.env.example index c471f4c..3b2d6c6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,11 @@ # Grok API Configuration GROK_API_KEY=your_grok_api_key_here -# Optional: Custom Grok API Base URL -# GROK_BASE_URL=https://api.x.ai/v1 \ No newline at end of file +# Optional: Custom API base URL (default: https://api.x.ai/v1) +# GROK_BASE_URL=https://api.x.ai/v1 + +# Optional: Default model (default: grok-4-1-fast) +# GROK_MODEL=grok-4-1-fast + +# Optional: Max tokens per response (default: 16384) +# GROK_MAX_TOKENS=16384 diff --git a/.github/workflows/contributor-check.yml b/.github/workflows/contributor-check.yml new file mode 100644 index 0000000..5e84e1c --- /dev/null +++ b/.github/workflows/contributor-check.yml @@ -0,0 +1,184 @@ +name: Contributor Trust Check + +on: + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + check: + name: Scan PR author + runs-on: ubuntu-24.04 + concurrency: + group: brin-check-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + - name: Query Brin API + id: brin + run: | + AUTHOR="${{ github.event.pull_request.user.login }}" + RESPONSE=$(curl -sf "https://api.brin.sh/contributor/${AUTHOR}?details=true&mode=full" || echo '{}') + echo "response<> "$GITHUB_OUTPUT" + echo "$RESPONSE" >> "$GITHUB_OUTPUT" + echo "BRIN_EOF" >> "$GITHUB_OUTPUT" + + - name: Apply label and comment + uses: actions/github-script@v7 + env: + BRIN_RESPONSE: ${{ steps.brin.outputs.response }} + with: + script: | + const marker = ""; + const pr = context.payload.pull_request; + let data; + try { + data = JSON.parse(process.env.BRIN_RESPONSE); + } catch { + core.warning("Failed to parse Brin API response"); + return; + } + + if (!data.score && data.score !== 0) { + core.warning("Brin API returned no score"); + return; + } + + const score = data.score; + const verdict = data.verdict ?? "unknown"; + const confidence = data.confidence ?? "unknown"; + const isSafe = verdict === "safe"; + + const labels = [ + { name: "contributor:verified", color: "0969da", description: "Contributor passed trust analysis." }, + { name: "contributor:flagged", color: "e16f24", description: "Contributor flagged for review by trust analysis." }, + ]; + + for (const label of labels) { + try { + const { data: existing } = await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + }); + if (existing.color !== label.color || (existing.description ?? "") !== label.description) { + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description, + }); + } + } catch (error) { + if (error.status !== 404) throw error; + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description, + }); + } + } + + const nextLabel = isSafe ? "contributor:verified" : "contributor:flagged"; + const labelNames = labels.map((l) => l.name); + + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const preserved = currentLabels + .map((l) => l.name) + .filter((name) => !labelNames.includes(name)); + + await github.rest.issues.setLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [...preserved, nextLabel], + }); + + core.info(`PR #${pr.number}: ${verdict} -> ${nextLabel}`); + + if (isSafe) { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100, + }); + const existing = comments.find((c) => c.body?.includes(marker)); + if (existing) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + }); + } + return; + } + + const sub = data.sub_scores ?? {}; + const fmt = (v) => (v != null ? String(Math.round(v)) : "\u2014"); + + const verdictEmoji = { + caution: "\u26A0\uFE0F", + suspicious: "\u26D4", + dangerous: "\uD83D\uDEA8", + }[verdict] ?? "\u2753"; + + let body = `${marker}\n`; + body += `### ${verdictEmoji} Contributor Trust Check \u2014 Review Recommended\n\n`; + body += `This contributor's profile shows patterns that may warrant additional review. `; + body += `This is based on their GitHub activity, not the contents of this PR.\n\n`; + body += `**[${data.name}](https://github.com/${data.name})** \u00b7 Score: **${score}**/100\n\n`; + + if (data.threats && data.threats.length > 0) { + body += `#### Why was this flagged?\n\n`; + body += `| Signal | Severity | Detail |\n|--------|----------|--------|\n`; + for (const t of data.threats) { + body += `| ${t.type} | ${t.severity} | ${t.detail} |\n`; + } + body += `\n`; + } + + body += `
\nDimension breakdown\n\n`; + body += `| Dimension | Score | What it measures |\n|-----------|-------|------------------|\n`; + body += `| Identity | ${fmt(sub.identity)} | Account age, contribution history, GPG keys, org memberships |\n`; + body += `| Behavior | ${fmt(sub.behavior)} | PR patterns, unsolicited contribution ratio, activity cadence |\n`; + body += `| Content | ${fmt(sub.content)} | PR body substance, issue linkage, contribution quality |\n`; + body += `| Graph | ${fmt(sub.graph)} | Cross-repo trust, co-contributor relationships |\n\n`; + body += `
\n\n`; + + body += `Analyzed by [Brin](https://brin.sh) \u00b7 [Full profile](${data.url}?details=true)\n`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100, + }); + + const existingComment = comments.find((c) => c.body?.includes(marker)); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body, + }); + } diff --git a/bun.lock b/bun.lock index 771053b..e285eb8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,94 +1,42 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "@vibe-kit/grok-cli", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", - "axios": "^1.7.0", - "cfonts": "^3.3.0", - "chalk": "^5.3.0", + "@ai-sdk/xai": "^3.0.67", + "@opentui/core": "^0.1.88", + "@opentui/react": "^0.1.88", + "ai": "^6.0.116", "commander": "^12.0.0", + "diff": "^8.0.3", "dotenv": "^16.4.0", - "enquirer": "^2.4.1", - "fs-extra": "^11.2.0", - "ink": "^4.4.1", - "marked": "^15.0.12", - "marked-terminal": "^7.3.0", - "openai": "^5.10.1", - "react": "^18.3.1", - "ripgrep-node": "^1.0.0", - "tiktoken": "^1.0.21", + "react": "^19.1.0", + "zod": "^4.3.6", }, "devDependencies": { - "@types/fs-extra": "^11.0.2", - "@types/node": "^20.8.0", - "@types/react": "^18.3.3", + "@types/diff": "^8.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.1.0", "@typescript-eslint/eslint-plugin": "^8.37.0", "@typescript-eslint/parser": "^8.37.0", "eslint": "^9.31.0", - "tsx": "^4.0.0", - "typescript": "^5.3.3", + "typescript": "^5.8.0", }, }, }, "packages": { - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], - "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.67", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], + "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], @@ -116,7 +64,61 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.18.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], + + "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], + + "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], + + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], + + "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], + + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], + + "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], + + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], + + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], + + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], + + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], + + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], + + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], + + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], + + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], + + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], + + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], + + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], + + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], + + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], + + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], + + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], + + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], + + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], + + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], + + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], + + "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], + + "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -124,21 +126,37 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentui/core": ["@opentui/core@0.1.88", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.88", "@opentui/core-darwin-x64": "0.1.88", "@opentui/core-linux-arm64": "0.1.88", "@opentui/core-linux-x64": "0.1.88", "@opentui/core-win32-arm64": "0.1.88", "@opentui/core-win32-x64": "0.1.88", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-eaDVZfAzZraddOIkgWSHMVkyaY0O20foYnPWKPQx1TY4t7G1oatIoan2zkytx67epW+4BZQ9vGib+61/uNM1MA=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.88", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oGRexWwZFeQJymOK5ORrLrwJUbPHMYaFa0EcLnlhvPnymm1xyMcRKm39ez0WSIdtiCCi/PmMHX95CfyyJB5VMA=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.88", "", { "os": "darwin", "cpu": "x64" }, "sha512-ddnruYpXt7gXsAqZoQzNrHtZ50niYQfESVT3rhE5qgsz7zoWBdKe/RxLKcb6zQmHMZML6SjSh0NrMG86lsH4dQ=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.88", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfcU/Sw8re3aWWb9cQ4OXmVNp/pchu6lgDRqvfy0EKTpzd7CNIu6a0xm+rcUKiPO7BrTrwtumT5/jZWWgCdHlg=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.88", "", { "os": "linux", "cpu": "x64" }, "sha512-nyfilOYLu6XWRlPl1R0Y6WzdL+jVdIFnwShBWcZL+QC5HiJnQc6LKy5yX8uv0fVbY5xs1wBvlHVeUj1UwFQyFQ=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.88", "", { "os": "win32", "cpu": "arm64" }, "sha512-jv/dQwcku7YZ4lNnYjivVvjPwTfDfzGfcplUqHxmirnv1Q1pZL1qS5wH1PV6RhAKN779vHTvnYMD4OgHWzqVaA=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.88", "", { "os": "win32", "cpu": "x64" }, "sha512-saGvsQqwL8H7B0VBCQ+szMCKh9WIfTebOR8cwPa2+DR+1FnrEG2I4kiikoj4hfYfRMX18A0A11vQxSh3vvy8Ig=="], + + "@opentui/react": ["@opentui/react@0.1.88", "", { "dependencies": { "@opentui/core": "0.1.88", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-EfEUBlGgCx9LqpbB852L+m7F19NcR8yeUww+C8NytIQeVKJbFIhSLu3r3wjBEGNoYiRk1DtAwWBpG5YaVvkXXw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], + "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], - "@types/node": ["@types/node@20.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g=="], - - "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], - - "@types/react": ["@types/react@18.3.24", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.43.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="], @@ -160,138 +178,76 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - - "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], - - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - - "axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], + + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="], + + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="], + + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="], + + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "cfonts": ["cfonts@3.3.1", "", { "dependencies": { "supports-color": "^8", "window-size": "^1" }, "bin": "bin/index.js" }, "sha512-ZGEmN3W9mViWEDjsuPo4nK4h39sfh6YtoneFYp9WLPI/rw8BaSSrfQC6jkrGW3JMvV3ZnExJB/AEqXc/nHYxkw=="], - - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], - - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - - "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - - "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], - - "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], - - "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], - - "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": "bin/esbuild" }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": "bin/eslint.js" }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="], @@ -310,15 +266,13 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -332,9 +286,9 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -342,161 +296,71 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - - "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ink": ["ink@4.4.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^6.0.0", "auto-bind": "^5.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "indent-string": "^5.0.0", "is-ci": "^3.0.1", "is-lower-case": "^2.0.2", "is-upper-case": "^2.0.2", "lodash": "^4.17.21", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^6.0.0", "stack-utils": "^2.0.6", "string-width": "^5.1.2", "type-fest": "^0.12.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0", "ws": "^8.12.0", "yoga-wasm-web": "~0.3.3" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-accessor-descriptor": ["is-accessor-descriptor@1.0.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA=="], - - "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], - - "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], - - "is-data-descriptor": ["is-data-descriptor@1.0.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw=="], - - "is-descriptor": ["is-descriptor@1.0.3", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-lower-case": ["is-lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ=="], - - "is-number": ["is-number@3.0.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-upper-case": ["is-upper-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], + + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "marked": ["marked@15.0.12", "", { "bin": "bin/marked.js" }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - - "marked-terminal": ["marked-terminal@7.3.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "ansi-regex": "^6.1.0", "chalk": "^5.4.1", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "node-emoji": "^2.2.0", "supports-hyperlinks": "^3.1.0" }, "peerDependencies": { "marked": ">=1 <16" } }, "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "node-emoji": ["node-emoji@2.2.0", "", { "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", "emojilib": "^2.4.0", "skin-tone": "^2.0.0" } }, "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "openai": ["openai@5.20.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "bin": "bin/cli" }, "sha512-UndCB0R5V3iB9I98NyF69zNP6YfwU4+Fjk0eW4HhooTm+Awlpm/MGjJTwJsyNV/qkH1NJi0GG+9odwukGTqExQ=="], + "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -504,244 +368,170 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + + "planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], + "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "react-reconciler": ["react-reconciler@0.29.2", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "ripgrep-node": ["ripgrep-node@1.0.0", "", {}, "sha512-r/DBVFwrgHLpPvUZquwvRLE+UFLwB6NaeQVAetS3zOlUdyH3i20QBMzBMr7VU9702j0OxuJJ88WxRnAnjrD5DQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.4", "", {}, "sha512-3MY16e0ocMHL7N1ufpdObURGyX+lCo0T/A+y6VCwosLdH1HSda4QZl1Sdt/O+2qWp48WFi26XEp5rF0LoaL0Dg=="], - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], - - "slice-ansi": ["slice-ansi@6.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA=="], - - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], - "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@0.12.0", "", {}, "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], - - "window-size": ["window-size@1.1.1", "", { "dependencies": { "define-property": "^1.0.0", "is-number": "^3.0.0" }, "bin": "cli.js" }, "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], - "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], - "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], - - "@alcalzone/ansi-tokenize/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@opentui/core/diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], - - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], - "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], - "marked-terminal/ansi-escapes": ["ansi-escapes@7.1.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g=="], - - "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], - - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - - "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "to-regex-range/is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cli-table3/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], } } diff --git a/hello.py b/hello.py new file mode 100644 index 0000000..2f39e4f --- /dev/null +++ b/hello.py @@ -0,0 +1,5 @@ +def hello_world(): + print("Hello, World!") + +if __name__ == "__main__": + hello_world() diff --git a/package.json b/package.json index 8b516a2..cd64de9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.34", - "description": "An open-source AI agent that brings the power of Grok directly into your terminal.", + "version": "1.0.0", + "description": "An open-source AI coding agent powered by Grok, built with Bun and OpenTUI.", "type": "module", "main": "dist/index.js", "exports": { @@ -14,51 +14,42 @@ "grok": "dist/index.js" }, "scripts": { - "build": "tsc", - "build:bun": "bun run tsc", "dev": "bun run src/index.ts", - "dev:node": "tsx src/index.ts", - "start": "node dist/index.js", - "start:bun": "bun run dist/index.js", + "build": "tsc", + "start": "bun run dist/index.js", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "typecheck": "tsc --noEmit", - "install:bun": "bun install" + "typecheck": "tsc --noEmit" }, "keywords": [ "cli", "agent", - "text-editor", "grok", - "ai" + "ai", + "coding", + "terminal", + "opentui" ], - "author": "Your Name", + "author": "Vibe Kit", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", - "axios": "^1.7.0", - "cfonts": "^3.3.0", - "chalk": "^5.3.0", + "@ai-sdk/xai": "^3.0.67", + "@opentui/core": "^0.1.88", + "@opentui/react": "^0.1.88", + "ai": "^6.0.116", "commander": "^12.0.0", + "diff": "^8.0.3", "dotenv": "^16.4.0", - "enquirer": "^2.4.1", - "fs-extra": "^11.2.0", - "ink": "^4.4.1", - "marked": "^15.0.12", - "marked-terminal": "^7.3.0", - "openai": "^5.10.1", - "react": "^18.3.1", - "ripgrep-node": "^1.0.0", - "tiktoken": "^1.0.21" + "react": "^19.1.0", + "zod": "^4.3.6" }, "devDependencies": { - "@types/fs-extra": "^11.0.2", - "@types/node": "^20.8.0", - "@types/react": "^18.3.3", + "@types/diff": "^8.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.1.0", "@typescript-eslint/eslint-plugin": "^8.37.0", "@typescript-eslint/parser": "^8.37.0", "eslint": "^9.31.0", - "tsx": "^4.0.0", - "typescript": "^5.3.3" + "typescript": "^5.8.0" }, "engines": { "node": ">=18.0.0" diff --git a/src/agent/agent.ts b/src/agent/agent.ts new file mode 100644 index 0000000..675378b --- /dev/null +++ b/src/agent/agent.ts @@ -0,0 +1,455 @@ +import { streamText, stepCountIs, type ModelMessage } from "ai"; +import { createProvider, generateTitle as genTitle, type XaiProvider } from "../grok/client.js"; +import { createTools } from "../grok/tools.js"; +import { BashTool } from "../tools/bash.js"; +import { loadCustomInstructions } from "../utils/instructions.js"; +import type { + AgentMode, + Plan, + StreamChunk, + SubagentStatus, + TaskRequest, + ToolCall, + ToolResult, +} from "../types/index.js"; + +const MAX_TOOL_ROUNDS = 400; + +const ENVIRONMENT = `ENVIRONMENT: +You are running inside a terminal (CLI). Your text output is rendered in a plain terminal — not a browser, not a rich text editor. +- Use plain text only. No markdown tables, no HTML, no images, no colored text. +- Use simple markers like dashes (-) or asterisks (*) for lists. +- Use indentation and blank lines for structure. +- Keep lines under 100 characters when possible. +- Use backticks for inline code and triple backticks for code blocks — these are rendered. +- Never use unicode box-drawing, fancy borders, or ASCII art in your responses.`; + +const MODE_PROMPTS: Record = { + agent: `You are Grok CLI in Agent mode — a powerful AI coding agent. You execute tasks directly using tools. + +${ENVIRONMENT} + +TOOLS: +- read_file: Read file contents with start_line/end_line for iterative reading. Use for examining code. +- write_file: Create new files or overwrite existing ones with full content. +- edit_file: Replace a unique string in a file with new content. The old_string must be unique — include enough context lines. +- bash: Execute shell commands — searching (grep/rg/find), git, builds, tests, package managers, etc. +- task: Delegate a focused task to a sub-agent. Use general for multi-step execution and explore for fast read-only research. +- search_web: Search the web for current information, documentation, APIs, tutorials, etc. +- search_x: Search X/Twitter for real-time posts, discussions, opinions, and trends. + +WORKFLOW: +1. Understand the request +2. Decide whether a sub-agent should handle the first investigation pass +3. Use read_file and bash to explore the codebase directly when the task is small or tightly scoped +4. Use edit_file for targeted changes, write_file for new files or full rewrites +5. Verify changes by reading modified files +6. Run tests or builds with bash to confirm correctness +7. Use search_web or search_x when you need up-to-date information + +DEFAULT DELEGATION POLICY: +- Prefer the task tool by default for code review, code quality analysis, architecture research, root-cause investigation, bug triage, or any request that likely needs reading multiple files before acting. +- Use the explore sub-agent for read-only investigation, reviews, research, and "how does this work?" tasks. +- Use the general sub-agent for delegated work that may need editing files, running commands, or producing a concrete implementation. +- Do not wait for the user to explicitly ask for a sub-agent when delegation would clearly help. +- Skip delegation only when the task is trivial, single-file, or you already have the exact answer. + +EXAMPLES: +- "review this change" -> delegate to explore first +- "research how auth works" -> delegate to explore first +- "investigate why this test fails" -> delegate to explore first, then continue with findings +- "refactor this module" -> delegate a focused part to general when helpful + +IMPORTANT: +- Prefer edit_file for surgical changes to existing files — it shows a clean diff. +- Use write_file only for new files or when most of the file is changing. +- Use read_file instead of cat/head/tail for reading files. + +Be direct. Execute, don't just describe. Show results, not plans.`, + + plan: `You are Grok CLI in Plan mode — you analyze and plan but DO NOT execute changes. + +${ENVIRONMENT} + +TOOLS: +- read_file: Read file contents for analysis. +- bash: ONLY for searching (find, grep, ls) — NEVER modify files. +- generate_plan: ALWAYS use this to present your plan. Creates an interactive UI with steps and questions. + +BEHAVIOR: +- Explore the codebase first using read_file and bash to understand the current state +- ALWAYS call generate_plan to present your plan — never just describe it in text +- Include clear, ordered steps with affected file paths +- Include questions when you need user input on approach, trade-offs, or preferences +- Use "select" questions for single-choice decisions, "multiselect" for picking multiple options, and "text" for free-form input +- Highlight potential risks, edge cases, and dependencies in the plan summary +- NEVER create, modify, or delete files — only read and analyze`, + + ask: `You are Grok CLI in Ask mode — you answer questions clearly and thoroughly. + +${ENVIRONMENT} + +TOOLS: +- read_file: Read file contents for context. +- bash: ONLY for searching (find, grep, ls) — NEVER modify. + +BEHAVIOR: +- Answer the user's question directly and thoroughly +- Use tools to gather context when needed +- Provide code examples when helpful +- NEVER create, modify, or delete files +- Focus on explanation, not execution`, +}; + +function buildSystemPrompt(cwd: string, mode: AgentMode, planContext?: string | null): string { + const custom = loadCustomInstructions(); + const customSection = custom + ? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n` + : ""; + + const planSection = planContext + ? `\n\nAPPROVED PLAN:\nThe following plan has been approved by the user. Execute it now.\n${planContext}\n` + : ""; + + return `${MODE_PROMPTS[mode]}${customSection}${planSection} + +Current working directory: ${cwd}`; +} + +function buildSubagentPrompt(request: TaskRequest, cwd: string): string { + const mode: AgentMode = request.agent === "explore" ? "ask" : "agent"; + const role = + request.agent === "explore" + ? "You are the Explore sub-agent. You are read-only and focus on fast codebase research." + : "You are the General sub-agent. You can investigate, edit files, and run commands to complete delegated work."; + + const rules = + request.agent === "explore" + ? [ + "Do not create, modify, or delete files.", + "Prefer `read_file` and search commands over broad shell exploration.", + "Return concise findings for the parent agent.", + ] + : [ + "Work only on the delegated task below.", + "Use tools directly instead of narrating your intent.", + "Return a concise summary for the parent agent with key outcomes and any open risks.", + ]; + + return [ + role, + "", + "You are helping a parent agent. Do not address the end user directly.", + "Focus tightly on the delegated scope and summarize what matters back to the parent agent.", + "", + ...rules, + "", + `Delegated task: ${request.description}`, + "", + buildSystemPrompt(cwd, mode), + ].join("\n"); +} + +export class Agent { + private provider: XaiProvider; + private bash: BashTool; + private messages: ModelMessage[] = []; + private abortController: AbortController | null = null; + private maxToolRounds: number; + private mode: AgentMode = "agent"; + private modelId: string; + private maxTokens: number; + private planContext: string | null = null; + private subagentStatusListeners = new Set<(status: SubagentStatus | null) => void>(); + + constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) { + this.provider = createProvider(apiKey, baseURL); + this.bash = new BashTool(); + this.modelId = model || "grok-4-1-fast"; + this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS; + const envMax = Number(process.env.GROK_MAX_TOKENS); + this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384; + } + + getModel(): string { + return this.modelId; + } + + setModel(model: string): void { + this.modelId = model; + } + + getMode(): AgentMode { + return this.mode; + } + + setMode(mode: AgentMode): void { + if (mode !== this.mode) { + this.mode = mode; + this.messages = []; + } + } + + setPlanContext(ctx: string | null): void { + this.planContext = ctx; + } + + getCwd(): string { + return this.bash.getCwd(); + } + + async generateTitle(userMessage: string): Promise { + return genTitle(this.provider, userMessage); + } + + abort(): void { + this.abortController?.abort(); + } + + clearHistory(): void { + this.messages = []; + } + + onSubagentStatus(listener: (status: SubagentStatus | null) => void): () => void { + this.subagentStatusListeners.add(listener); + return () => { + this.subagentStatusListeners.delete(listener); + }; + } + + private emitSubagentStatus(status: SubagentStatus | null): void { + for (const listener of this.subagentStatusListeners) { + listener(status); + } + } + + private async runTask(request: TaskRequest): Promise { + const childMode: AgentMode = request.agent === "explore" ? "ask" : "agent"; + const childBash = new BashTool(this.bash.getCwd()); + const childTools = createTools(childBash, this.provider, childMode); + const initialDetail = request.agent === "explore" ? "Scanning the codebase" : "Planning delegated work"; + let assistantText = ""; + let lastActivity = initialDetail; + + this.emitSubagentStatus({ + agent: request.agent, + description: request.description, + detail: initialDetail, + }); + + try { + const result = streamText({ + model: this.provider(request.agent === "explore" ? "grok-4-1-fast" : this.modelId), + system: buildSubagentPrompt(request, childBash.getCwd()), + messages: [{ role: "user", content: request.prompt }], + tools: childTools, + stopWhen: stepCountIs(Math.min(this.maxToolRounds, request.agent === "explore" ? 60 : 120)), + maxRetries: 0, + abortSignal: this.abortController?.signal, + temperature: request.agent === "explore" ? 0.2 : 0.5, + maxOutputTokens: Math.min(this.maxTokens, 8_192), + }); + + for await (const part of result.fullStream) { + if (part.type === "text-delta") { + assistantText += part.text; + continue; + } + + if (part.type === "tool-call") { + lastActivity = formatSubagentActivity(part.toolName, part.input); + this.emitSubagentStatus({ + agent: request.agent, + description: request.description, + detail: lastActivity, + }); + } + } + + await result.response; + + const output = assistantText.trim() || `Task completed. Last action: ${lastActivity}`; + return { + success: true, + output, + task: { + agent: request.agent, + description: request.description, + summary: firstLine(output), + activity: lastActivity, + }, + }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + const output = `Task failed: ${msg}`; + return { + success: false, + output, + task: { + agent: request.agent, + description: request.description, + summary: output, + activity: lastActivity, + }, + }; + } finally { + this.emitSubagentStatus(null); + } + } + + async *processMessage(userMessage: string): AsyncGenerator { + this.abortController = new AbortController(); + this.emitSubagentStatus(null); + + this.messages.push({ role: "user", content: userMessage }); + + let assistantText = ""; + let streamOk = false; + + try { + const tools = createTools(this.bash, this.provider, this.mode, { + runTask: (request) => this.runTask(request), + }); + const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext); + this.planContext = null; + + const result = streamText({ + model: this.provider(this.modelId), + system, + messages: this.messages, + tools, + stopWhen: stepCountIs(this.maxToolRounds), + maxRetries: 0, + abortSignal: this.abortController.signal, + temperature: 0.7, + maxOutputTokens: this.maxTokens, + }); + + for await (const part of result.fullStream) { + if (this.abortController.signal.aborted) { + yield { type: "content", content: "\n\n[Cancelled]" }; + break; + } + + switch (part.type) { + case "text-delta": + assistantText += part.text; + yield { type: "content", content: part.text }; + break; + + case "reasoning-delta": + yield { type: "reasoning", content: part.text }; + break; + + case "tool-call": { + const tc = toToolCall(part); + yield { type: "tool_calls", toolCalls: [tc] }; + break; + } + + case "tool-result": { + const tc: ToolCall = { + id: part.toolCallId, + type: "function", + function: { name: part.toolName, arguments: JSON.stringify(part.input ?? {}) }, + }; + const tr = toToolResult(part.output); + yield { type: "tool_result", toolCall: tc, toolResult: tr }; + break; + } + + case "error": + yield { type: "error", content: String(part.error) }; + break; + } + } + + try { + const response = await result.response; + this.messages.push(...response.messages); + streamOk = true; + } catch { + // response promise can fail after stream errors — fall back to manual message + } + + if (!streamOk && assistantText.trim()) { + this.messages.push({ role: "assistant", content: assistantText }); + } + + yield { type: "done" }; + } catch (err: unknown) { + if (this.abortController.signal.aborted) { + yield { type: "content", content: "\n\n[Cancelled]" }; + } else { + const msg = err instanceof Error ? err.message : String(err); + yield { type: "error", content: `Error: ${msg}` }; + } + if (assistantText.trim()) { + this.messages.push({ role: "assistant", content: assistantText }); + } + yield { type: "done" }; + } finally { + this.abortController = null; + } + } +} + +function toToolCall(part: { toolCallId: string; toolName: string; args?: unknown; input?: unknown }): ToolCall { + return { + id: part.toolCallId, + type: "function", + function: { + name: part.toolName, + arguments: JSON.stringify(part.input ?? part.args ?? {}), + }, + }; +} + +function toToolResult(output: unknown): ToolResult { + if (output && typeof output === "object" && "success" in output) { + const r = output as { + success: boolean; + output?: string; + diff?: ToolResult["diff"]; + plan?: Plan; + task?: ToolResult["task"]; + }; + return { + success: r.success, + output: r.output, + error: r.success ? undefined : r.output, + diff: r.diff, + plan: r.plan, + task: r.task, + }; + } + return { success: true, output: String(output) }; +} + +function formatSubagentActivity(toolName: string, args?: unknown): string { + const parsed = parseToolArgs(args); + if (toolName === "read_file") return `Read ${parsed.path || "file"}`; + if (toolName === "write_file") return `Write ${parsed.path || "file"}`; + if (toolName === "edit_file") return `Edit ${parsed.path || "file"}`; + if (toolName === "search_web") return `Web search "${truncate(parsed.query || "", 50)}"`; + if (toolName === "search_x") return `X search "${truncate(parsed.query || "", 50)}"`; + if (toolName === "bash") return truncate(parsed.command || "Run command", 70); + return truncate(`${toolName}`, 70); +} + +function parseToolArgs(args: unknown): Record { + if (!args || typeof args !== "object") return {}; + const result: Record = {}; + for (const [key, value] of Object.entries(args)) { + result[key] = typeof value === "string" ? value : JSON.stringify(value); + } + return result; +} + +function firstLine(text: string): string { + return text.trim().split("\n").find(Boolean)?.trim() || "Task completed."; +} + +function truncate(text: string, max: number): string { + return text.length <= max ? text : `${text.slice(0, max - 1)}…`; +} diff --git a/src/agent/grok-agent.ts b/src/agent/grok-agent.ts deleted file mode 100644 index 716cc1e..0000000 --- a/src/agent/grok-agent.ts +++ /dev/null @@ -1,780 +0,0 @@ -import { GrokClient, GrokMessage, GrokToolCall } from "../grok/client.js"; -import { - GROK_TOOLS, - addMCPToolsToGrokTools, - getAllGrokTools, - getMCPManager, - initializeMCPServers, -} from "../grok/tools.js"; -import { loadMCPConfig } from "../mcp/config.js"; -import { - TextEditorTool, - MorphEditorTool, - BashTool, - TodoTool, - ConfirmationTool, - SearchTool, -} from "../tools/index.js"; -import { ToolResult } from "../types/index.js"; -import { EventEmitter } from "events"; -import { createTokenCounter, TokenCounter } from "../utils/token-counter.js"; -import { loadCustomInstructions } from "../utils/custom-instructions.js"; -import { getSettingsManager } from "../utils/settings-manager.js"; - -export interface ChatEntry { - type: "user" | "assistant" | "tool_result" | "tool_call"; - content: string; - timestamp: Date; - toolCalls?: GrokToolCall[]; - toolCall?: GrokToolCall; - toolResult?: { success: boolean; output?: string; error?: string }; - isStreaming?: boolean; -} - -export interface StreamingChunk { - type: "content" | "tool_calls" | "tool_result" | "done" | "token_count"; - content?: string; - toolCalls?: GrokToolCall[]; - toolCall?: GrokToolCall; - toolResult?: ToolResult; - tokenCount?: number; -} - -export class GrokAgent extends EventEmitter { - private grokClient: GrokClient; - private textEditor: TextEditorTool; - private morphEditor: MorphEditorTool | null; - private bash: BashTool; - private todoTool: TodoTool; - private confirmationTool: ConfirmationTool; - private search: SearchTool; - private chatHistory: ChatEntry[] = []; - private messages: GrokMessage[] = []; - private tokenCounter: TokenCounter; - private abortController: AbortController | null = null; - private mcpInitialized: boolean = false; - private maxToolRounds: number; - - constructor( - apiKey: string, - baseURL?: string, - model?: string, - maxToolRounds?: number - ) { - super(); - const manager = getSettingsManager(); - const savedModel = manager.getCurrentModel(); - const modelToUse = model || savedModel || "grok-code-fast-1"; - this.maxToolRounds = maxToolRounds || 400; - this.grokClient = new GrokClient(apiKey, modelToUse, baseURL); - this.textEditor = new TextEditorTool(); - this.morphEditor = process.env.MORPH_API_KEY ? new MorphEditorTool() : null; - this.bash = new BashTool(); - this.todoTool = new TodoTool(); - this.confirmationTool = new ConfirmationTool(); - this.search = new SearchTool(); - this.tokenCounter = createTokenCounter(modelToUse); - - // Initialize MCP servers if configured - this.initializeMCP(); - - // Load custom instructions - const customInstructions = loadCustomInstructions(); - const customInstructionsSection = customInstructions - ? `\n\nCUSTOM INSTRUCTIONS:\n${customInstructions}\n\nThe above custom instructions should be followed alongside the standard instructions below.` - : ""; - - // Initialize with system message - this.messages.push({ - role: "system", - content: `You are Grok CLI, an AI assistant that helps with file editing, coding tasks, and system operations.${customInstructionsSection} - -You have access to these tools: -- view_file: View file contents or directory listings -- create_file: Create new files with content (ONLY use this for files that don't exist yet) -- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)${ - this.morphEditor - ? "\n- edit_file: High-speed file editing with Morph Fast Apply (4,500+ tokens/sec with 98% accuracy)" - : "" - } -- bash: Execute bash commands (use for searching, file discovery, navigation, and system operations) -- search: Unified search tool for finding text content or files (similar to Cursor's search functionality) -- create_todo_list: Create a visual todo list for planning and tracking tasks -- update_todo_list: Update existing todos in your todo list - -REAL-TIME INFORMATION: -You have access to real-time web search and X (Twitter) data. When users ask for current information, latest news, or recent events, you automatically have access to up-to-date information from the web and social media. - -IMPORTANT TOOL USAGE RULES: -- NEVER use create_file on files that already exist - this will overwrite them completely -- ALWAYS use str_replace_editor to modify existing files, even for small changes -- Before editing a file, use view_file to see its current contents -- Use create_file ONLY when creating entirely new files that don't exist - -SEARCHING AND EXPLORATION: -- Use search for fast, powerful text search across files or finding files by name (unified search tool) -- Examples: search for text content like "import.*react", search for files like "component.tsx" -- Use bash with commands like 'find', 'grep', 'rg', 'ls' for complex file operations and navigation -- view_file is best for reading specific files you already know exist - -When a user asks you to edit, update, modify, or change an existing file: -1. First use view_file to see the current contents -2. Then use str_replace_editor to make the specific changes -3. Never use create_file for existing files - -When a user asks you to create a new file that doesn't exist: -1. Use create_file with the full content - -TASK PLANNING WITH TODO LISTS: -- For complex requests with multiple steps, ALWAYS create a todo list first to plan your approach -- Use create_todo_list to break down tasks into manageable items with priorities -- Mark tasks as 'in_progress' when you start working on them (only one at a time) -- Mark tasks as 'completed' immediately when finished -- Use update_todo_list to track your progress throughout the task -- Todo lists provide visual feedback with colors: ✅ Green (completed), 🔄 Cyan (in progress), ⏳ Yellow (pending) -- Always create todos with priorities: 'high' (🔴), 'medium' (🟡), 'low' (🟢) - -USER CONFIRMATION SYSTEM: -File operations (create_file, str_replace_editor) and bash commands will automatically request user confirmation before execution. The confirmation system will show users the actual content or command before they decide. Users can choose to approve individual operations or approve all operations of that type for the session. - -If a user rejects an operation, the tool will return an error and you should not proceed with that specific operation. - -Be helpful, direct, and efficient. Always explain what you're doing and show the results. - -IMPORTANT RESPONSE GUIDELINES: -- After using tools, do NOT respond with pleasantries like "Thanks for..." or "Great!" -- Only provide necessary explanations or next steps if relevant to the task -- Keep responses concise and focused on the actual work being done -- If a tool execution completes the user's request, you can remain silent or give a brief confirmation - -Current working directory: ${process.cwd()}`, - }); - } - - private async initializeMCP(): Promise { - // Initialize MCP in the background without blocking - Promise.resolve().then(async () => { - try { - const config = loadMCPConfig(); - if (config.servers.length > 0) { - await initializeMCPServers(); - } - } catch (error) { - console.warn("MCP initialization failed:", error); - } finally { - this.mcpInitialized = true; - } - }); - } - - private isGrokModel(): boolean { - const currentModel = this.grokClient.getCurrentModel(); - return currentModel.toLowerCase().includes("grok"); - } - - // Heuristic: enable web search only when likely needed - private shouldUseSearchFor(message: string): boolean { - const q = message.toLowerCase(); - const keywords = [ - "today", - "latest", - "news", - "trending", - "breaking", - "current", - "now", - "recent", - "x.com", - "twitter", - "tweet", - "what happened", - "as of", - "update on", - "release notes", - "changelog", - "price", - ]; - if (keywords.some((k) => q.includes(k))) return true; - // crude date pattern (e.g., 2024/2025) may imply recency - if (/(20\d{2})/.test(q)) return true; - return false; - } - - async processUserMessage(message: string): Promise { - // Add user message to conversation - const userEntry: ChatEntry = { - type: "user", - content: message, - timestamp: new Date(), - }; - this.chatHistory.push(userEntry); - this.messages.push({ role: "user", content: message }); - - const newEntries: ChatEntry[] = [userEntry]; - const maxToolRounds = this.maxToolRounds; // Prevent infinite loops - let toolRounds = 0; - - try { - const tools = await getAllGrokTools(); - let currentResponse = await this.grokClient.chat( - this.messages, - tools, - undefined, - this.isGrokModel() && this.shouldUseSearchFor(message) - ? { search_parameters: { mode: "auto" } } - : { search_parameters: { mode: "off" } } - ); - - // Agent loop - continue until no more tool calls or max rounds reached - while (toolRounds < maxToolRounds) { - const assistantMessage = currentResponse.choices[0]?.message; - - if (!assistantMessage) { - throw new Error("No response from Grok"); - } - - // Handle tool calls - if ( - assistantMessage.tool_calls && - assistantMessage.tool_calls.length > 0 - ) { - toolRounds++; - - // Add assistant message with tool calls - const assistantEntry: ChatEntry = { - type: "assistant", - content: assistantMessage.content || "Using tools to help you...", - timestamp: new Date(), - toolCalls: assistantMessage.tool_calls, - }; - this.chatHistory.push(assistantEntry); - newEntries.push(assistantEntry); - - // Add assistant message to conversation - this.messages.push({ - role: "assistant", - content: assistantMessage.content || "", - tool_calls: assistantMessage.tool_calls, - } as any); - - // Create initial tool call entries to show tools are being executed - assistantMessage.tool_calls.forEach((toolCall) => { - const toolCallEntry: ChatEntry = { - type: "tool_call", - content: "Executing...", - timestamp: new Date(), - toolCall: toolCall, - }; - this.chatHistory.push(toolCallEntry); - newEntries.push(toolCallEntry); - }); - - // Execute tool calls and update the entries - for (const toolCall of assistantMessage.tool_calls) { - const result = await this.executeTool(toolCall); - - // Update the existing tool_call entry with the result - const entryIndex = this.chatHistory.findIndex( - (entry) => - entry.type === "tool_call" && entry.toolCall?.id === toolCall.id - ); - - if (entryIndex !== -1) { - const updatedEntry: ChatEntry = { - ...this.chatHistory[entryIndex], - type: "tool_result", - content: result.success - ? result.output || "Success" - : result.error || "Error occurred", - toolResult: result, - }; - this.chatHistory[entryIndex] = updatedEntry; - - // Also update in newEntries for return value - const newEntryIndex = newEntries.findIndex( - (entry) => - entry.type === "tool_call" && - entry.toolCall?.id === toolCall.id - ); - if (newEntryIndex !== -1) { - newEntries[newEntryIndex] = updatedEntry; - } - } - - // Add tool result to messages with proper format (needed for AI context) - this.messages.push({ - role: "tool", - content: result.success - ? result.output || "Success" - : result.error || "Error", - tool_call_id: toolCall.id, - }); - } - - // Get next response - this might contain more tool calls - currentResponse = await this.grokClient.chat( - this.messages, - tools, - undefined, - this.isGrokModel() && this.shouldUseSearchFor(message) - ? { search_parameters: { mode: "auto" } } - : { search_parameters: { mode: "off" } } - ); - } else { - // No more tool calls, add final response - const finalEntry: ChatEntry = { - type: "assistant", - content: - assistantMessage.content || - "I understand, but I don't have a specific response.", - timestamp: new Date(), - }; - this.chatHistory.push(finalEntry); - this.messages.push({ - role: "assistant", - content: assistantMessage.content || "", - }); - newEntries.push(finalEntry); - break; // Exit the loop - } - } - - if (toolRounds >= maxToolRounds) { - const warningEntry: ChatEntry = { - type: "assistant", - content: - "Maximum tool execution rounds reached. Stopping to prevent infinite loops.", - timestamp: new Date(), - }; - this.chatHistory.push(warningEntry); - newEntries.push(warningEntry); - } - - return newEntries; - } catch (error: any) { - const errorEntry: ChatEntry = { - type: "assistant", - content: `Sorry, I encountered an error: ${error.message}`, - timestamp: new Date(), - }; - this.chatHistory.push(errorEntry); - return [userEntry, errorEntry]; - } - } - - private messageReducer(previous: any, item: any): any { - const reduce = (acc: any, delta: any) => { - acc = { ...acc }; - for (const [key, value] of Object.entries(delta)) { - if (acc[key] === undefined || acc[key] === null) { - acc[key] = value; - // Clean up index properties from tool calls - if (Array.isArray(acc[key])) { - for (const arr of acc[key]) { - delete arr.index; - } - } - } else if (typeof acc[key] === "string" && typeof value === "string") { - (acc[key] as string) += value; - } else if (Array.isArray(acc[key]) && Array.isArray(value)) { - const accArray = acc[key] as any[]; - for (let i = 0; i < value.length; i++) { - if (!accArray[i]) accArray[i] = {}; - accArray[i] = reduce(accArray[i], value[i]); - } - } else if (typeof acc[key] === "object" && typeof value === "object") { - acc[key] = reduce(acc[key], value); - } - } - return acc; - }; - - return reduce(previous, item.choices[0]?.delta || {}); - } - - async *processUserMessageStream( - message: string - ): AsyncGenerator { - // Create new abort controller for this request - this.abortController = new AbortController(); - - // Add user message to conversation - const userEntry: ChatEntry = { - type: "user", - content: message, - timestamp: new Date(), - }; - this.chatHistory.push(userEntry); - this.messages.push({ role: "user", content: message }); - - // Calculate input tokens - let inputTokens = this.tokenCounter.countMessageTokens( - this.messages as any - ); - yield { - type: "token_count", - tokenCount: inputTokens, - }; - - const maxToolRounds = this.maxToolRounds; // Prevent infinite loops - let toolRounds = 0; - let totalOutputTokens = 0; - let lastTokenUpdate = 0; - - try { - // Agent loop - continue until no more tool calls or max rounds reached - while (toolRounds < maxToolRounds) { - // Check if operation was cancelled - if (this.abortController?.signal.aborted) { - yield { - type: "content", - content: "\n\n[Operation cancelled by user]", - }; - yield { type: "done" }; - return; - } - - // Stream response and accumulate - const tools = await getAllGrokTools(); - const stream = this.grokClient.chatStream( - this.messages, - tools, - undefined, - this.isGrokModel() && this.shouldUseSearchFor(message) - ? { search_parameters: { mode: "auto" } } - : { search_parameters: { mode: "off" } } - ); - let accumulatedMessage: any = {}; - let accumulatedContent = ""; - let toolCallsYielded = false; - - for await (const chunk of stream) { - // Check for cancellation in the streaming loop - if (this.abortController?.signal.aborted) { - yield { - type: "content", - content: "\n\n[Operation cancelled by user]", - }; - yield { type: "done" }; - return; - } - - if (!chunk.choices?.[0]) continue; - - // Accumulate the message using reducer - accumulatedMessage = this.messageReducer(accumulatedMessage, chunk); - - // Check for tool calls - yield when we have complete tool calls with function names - if (!toolCallsYielded && accumulatedMessage.tool_calls?.length > 0) { - // Check if we have at least one complete tool call with a function name - const hasCompleteTool = accumulatedMessage.tool_calls.some( - (tc: any) => tc.function?.name - ); - if (hasCompleteTool) { - yield { - type: "tool_calls", - toolCalls: accumulatedMessage.tool_calls, - }; - toolCallsYielded = true; - } - } - - // Stream content as it comes - if (chunk.choices[0].delta?.content) { - accumulatedContent += chunk.choices[0].delta.content; - - // Update token count in real-time including accumulated content and any tool calls - const currentOutputTokens = - this.tokenCounter.estimateStreamingTokens(accumulatedContent) + - (accumulatedMessage.tool_calls - ? this.tokenCounter.countTokens( - JSON.stringify(accumulatedMessage.tool_calls) - ) - : 0); - totalOutputTokens = currentOutputTokens; - - yield { - type: "content", - content: chunk.choices[0].delta.content, - }; - - // Emit token count update - const now = Date.now(); - if (now - lastTokenUpdate > 250) { - lastTokenUpdate = now; - yield { - type: "token_count", - tokenCount: inputTokens + totalOutputTokens, - }; - } - } - } - - // Add assistant entry to history - const assistantEntry: ChatEntry = { - type: "assistant", - content: accumulatedMessage.content || "Using tools to help you...", - timestamp: new Date(), - toolCalls: accumulatedMessage.tool_calls || undefined, - }; - this.chatHistory.push(assistantEntry); - - // Add accumulated message to conversation - this.messages.push({ - role: "assistant", - content: accumulatedMessage.content || "", - tool_calls: accumulatedMessage.tool_calls, - } as any); - - // Handle tool calls if present - if (accumulatedMessage.tool_calls?.length > 0) { - toolRounds++; - - // Only yield tool_calls if we haven't already yielded them during streaming - if (!toolCallsYielded) { - yield { - type: "tool_calls", - toolCalls: accumulatedMessage.tool_calls, - }; - } - - // Execute tools - for (const toolCall of accumulatedMessage.tool_calls) { - // Check for cancellation before executing each tool - if (this.abortController?.signal.aborted) { - yield { - type: "content", - content: "\n\n[Operation cancelled by user]", - }; - yield { type: "done" }; - return; - } - - const result = await this.executeTool(toolCall); - - const toolResultEntry: ChatEntry = { - type: "tool_result", - content: result.success - ? result.output || "Success" - : result.error || "Error occurred", - timestamp: new Date(), - toolCall: toolCall, - toolResult: result, - }; - this.chatHistory.push(toolResultEntry); - - yield { - type: "tool_result", - toolCall, - toolResult: result, - }; - - // Add tool result with proper format (needed for AI context) - this.messages.push({ - role: "tool", - content: result.success - ? result.output || "Success" - : result.error || "Error", - tool_call_id: toolCall.id, - }); - } - - // Update token count after processing all tool calls to include tool results - inputTokens = this.tokenCounter.countMessageTokens( - this.messages as any - ); - // Final token update after tools processed - yield { - type: "token_count", - tokenCount: inputTokens + totalOutputTokens, - }; - - // Continue the loop to get the next response (which might have more tool calls) - } else { - // No tool calls, we're done - break; - } - } - - if (toolRounds >= maxToolRounds) { - yield { - type: "content", - content: - "\n\nMaximum tool execution rounds reached. Stopping to prevent infinite loops.", - }; - } - - yield { type: "done" }; - } catch (error: any) { - // Check if this was a cancellation - if (this.abortController?.signal.aborted) { - yield { - type: "content", - content: "\n\n[Operation cancelled by user]", - }; - yield { type: "done" }; - return; - } - - const errorEntry: ChatEntry = { - type: "assistant", - content: `Sorry, I encountered an error: ${error.message}`, - timestamp: new Date(), - }; - this.chatHistory.push(errorEntry); - yield { - type: "content", - content: errorEntry.content, - }; - yield { type: "done" }; - } finally { - // Clean up abort controller - this.abortController = null; - } - } - - private async executeTool(toolCall: GrokToolCall): Promise { - try { - const args = JSON.parse(toolCall.function.arguments); - - switch (toolCall.function.name) { - case "view_file": - const range: [number, number] | undefined = - args.start_line && args.end_line - ? [args.start_line, args.end_line] - : undefined; - return await this.textEditor.view(args.path, range); - - case "create_file": - return await this.textEditor.create(args.path, args.content); - - case "str_replace_editor": - return await this.textEditor.strReplace( - args.path, - args.old_str, - args.new_str, - args.replace_all - ); - - case "edit_file": - if (!this.morphEditor) { - return { - success: false, - error: - "Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.", - }; - } - return await this.morphEditor.editFile( - args.target_file, - args.instructions, - args.code_edit - ); - - case "bash": - return await this.bash.execute(args.command); - - case "create_todo_list": - return await this.todoTool.createTodoList(args.todos); - - case "update_todo_list": - return await this.todoTool.updateTodoList(args.updates); - - case "search": - return await this.search.search(args.query, { - searchType: args.search_type, - includePattern: args.include_pattern, - excludePattern: args.exclude_pattern, - caseSensitive: args.case_sensitive, - wholeWord: args.whole_word, - regex: args.regex, - maxResults: args.max_results, - fileTypes: args.file_types, - includeHidden: args.include_hidden, - }); - - default: - // Check if this is an MCP tool - if (toolCall.function.name.startsWith("mcp__")) { - return await this.executeMCPTool(toolCall); - } - - return { - success: false, - error: `Unknown tool: ${toolCall.function.name}`, - }; - } - } catch (error: any) { - return { - success: false, - error: `Tool execution error: ${error.message}`, - }; - } - } - - private async executeMCPTool(toolCall: GrokToolCall): Promise { - try { - const args = JSON.parse(toolCall.function.arguments); - const mcpManager = getMCPManager(); - - const result = await mcpManager.callTool(toolCall.function.name, args); - - if (result.isError) { - return { - success: false, - error: (result.content[0] as any)?.text || "MCP tool error", - }; - } - - // Extract content from result - const output = result.content - .map((item) => { - if (item.type === "text") { - return item.text; - } else if (item.type === "resource") { - return `Resource: ${item.resource?.uri || "Unknown"}`; - } - return String(item); - }) - .join("\n"); - - return { - success: true, - output: output || "Success", - }; - } catch (error: any) { - return { - success: false, - error: `MCP tool execution error: ${error.message}`, - }; - } - } - - getChatHistory(): ChatEntry[] { - return [...this.chatHistory]; - } - - getCurrentDirectory(): string { - return this.bash.getCurrentDirectory(); - } - - async executeBashCommand(command: string): Promise { - return await this.bash.execute(command); - } - - getCurrentModel(): string { - return this.grokClient.getCurrentModel(); - } - - setModel(model: string): void { - this.grokClient.setModel(model); - // Update token counter for new model - this.tokenCounter.dispose(); - this.tokenCounter = createTokenCounter(model); - } - - abortCurrentOperation(): void { - if (this.abortController) { - this.abortController.abort(); - } - } -} diff --git a/src/agent/index.ts b/src/agent/index.ts deleted file mode 100644 index 8b20d61..0000000 --- a/src/agent/index.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { TextEditorTool, BashTool } from '../tools/index.js'; -import { ToolResult, AgentState } from '../types/index.js'; - -export class Agent { - private textEditor: TextEditorTool; - private bash: BashTool; - private state: AgentState; - - constructor() { - this.textEditor = new TextEditorTool(); - this.bash = new BashTool(); - this.state = { - currentDirectory: process.cwd(), - editHistory: [], - tools: [] - }; - } - - async processCommand(input: string): Promise { - const trimmedInput = input.trim(); - - if (trimmedInput.startsWith('view ')) { - const args = this.parseViewCommand(trimmedInput); - return this.textEditor.view(args.path, args.range); - } - - if (trimmedInput.startsWith('str_replace ')) { - const args = this.parseStrReplaceCommand(trimmedInput); - if (!args) { - return { success: false, error: 'Invalid str_replace command format' }; - } - return this.textEditor.strReplace(args.path, args.oldStr, args.newStr); - } - - if (trimmedInput.startsWith('create ')) { - const args = this.parseCreateCommand(trimmedInput); - if (!args) { - return { success: false, error: 'Invalid create command format' }; - } - return this.textEditor.create(args.path, args.content); - } - - if (trimmedInput.startsWith('insert ')) { - const args = this.parseInsertCommand(trimmedInput); - if (!args) { - return { success: false, error: 'Invalid insert command format' }; - } - return this.textEditor.insert(args.path, args.line, args.content); - } - - if (trimmedInput === 'undo_edit') { - return this.textEditor.undoEdit(); - } - - if (trimmedInput.startsWith('bash ') || trimmedInput.startsWith('$ ')) { - const command = trimmedInput.startsWith('bash ') - ? trimmedInput.substring(5) - : trimmedInput.substring(2); - return this.bash.execute(command); - } - - if (trimmedInput === 'pwd') { - return { - success: true, - output: this.bash.getCurrentDirectory() - }; - } - - if (trimmedInput === 'history') { - const history = this.textEditor.getEditHistory(); - return { - success: true, - output: history.length > 0 - ? JSON.stringify(history, null, 2) - : 'No edit history' - }; - } - - if (trimmedInput === 'help') { - return this.getHelp(); - } - - return this.bash.execute(trimmedInput); - } - - private parseViewCommand(input: string): { path: string; range?: [number, number] } { - const parts = input.split(' '); - const path = parts[1]; - - if (parts.length > 2) { - const rangePart = parts[2]; - if (rangePart.includes('-')) { - const [start, end] = rangePart.split('-').map(Number); - return { path, range: [start, end] }; - } - } - - return { path }; - } - - private parseStrReplaceCommand(input: string): { path: string; oldStr: string; newStr: string } | null { - const match = input.match(/str_replace\s+(\S+)\s+"([^"]+)"\s+"([^"]*)"/); - if (!match) return null; - - return { - path: match[1], - oldStr: match[2], - newStr: match[3] - }; - } - - private parseCreateCommand(input: string): { path: string; content: string } | null { - const match = input.match(/create\s+(\S+)\s+"([^"]*)"/); - if (!match) return null; - - return { - path: match[1], - content: match[2] - }; - } - - private parseInsertCommand(input: string): { path: string; line: number; content: string } | null { - const match = input.match(/insert\s+(\S+)\s+(\d+)\s+"([^"]*)"/); - if (!match) return null; - - return { - path: match[1], - line: parseInt(match[2]), - content: match[3] - }; - } - - private getHelp(): ToolResult { - return { - success: true, - output: `Available commands: - view [start-end] - View file contents or directory - str_replace "old" "new" - Replace text in file - create "content" - Create new file with content - insert "text" - Insert text at specific line - undo_edit - Undo last edit operation - bash - Execute bash command - $ - Execute bash command (shorthand) - pwd - Show current directory - history - Show edit history - help - Show this help message` - }; - } - - getCurrentState(): AgentState { - return { - ...this.state, - currentDirectory: this.bash.getCurrentDirectory(), - editHistory: this.textEditor.getEditHistory() - }; - } -} \ No newline at end of file diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts deleted file mode 100644 index ffb4438..0000000 --- a/src/commands/mcp.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { Command } from 'commander'; -import { addMCPServer, removeMCPServer, loadMCPConfig, PREDEFINED_SERVERS } from '../mcp/config.js'; -import { getMCPManager } from '../grok/tools.js'; -import { MCPServerConfig } from '../mcp/client.js'; -import chalk from 'chalk'; - -export function createMCPCommand(): Command { - const mcpCommand = new Command('mcp'); - mcpCommand.description('Manage MCP (Model Context Protocol) servers'); - - // Add server command - mcpCommand - .command('add ') - .description('Add an MCP server') - .option('-t, --transport ', 'Transport type (stdio, http, sse, streamable_http)', 'stdio') - .option('-c, --command ', 'Command to run the server (for stdio transport)') - .option('-a, --args [args...]', 'Arguments for the server command (for stdio transport)', []) - .option('-u, --url ', 'URL for HTTP/SSE transport') - .option('-h, --headers [headers...]', 'HTTP headers (key=value format)', []) - .option('-e, --env [env...]', 'Environment variables (key=value format)', []) - .action(async (name: string, options) => { - try { - // Check if it's a predefined server - if (PREDEFINED_SERVERS[name]) { - const config = PREDEFINED_SERVERS[name]; - addMCPServer(config); - console.log(chalk.green(`✓ Added predefined MCP server: ${name}`)); - - // Try to connect immediately - const manager = getMCPManager(); - await manager.addServer(config); - console.log(chalk.green(`✓ Connected to MCP server: ${name}`)); - - const tools = manager.getTools().filter(t => t.serverName === name); - console.log(chalk.blue(` Available tools: ${tools.length}`)); - - return; - } - - // Custom server - const transportType = options.transport.toLowerCase(); - - if (transportType === 'stdio') { - if (!options.command) { - console.error(chalk.red('Error: --command is required for stdio transport')); - process.exit(1); - } - } else if (transportType === 'http' || transportType === 'sse' || transportType === 'streamable_http') { - if (!options.url) { - console.error(chalk.red(`Error: --url is required for ${transportType} transport`)); - process.exit(1); - } - } else { - console.error(chalk.red('Error: Transport type must be stdio, http, sse, or streamable_http')); - process.exit(1); - } - - // Parse environment variables - const env: Record = {}; - for (const envVar of options.env || []) { - const [key, value] = envVar.split('=', 2); - if (key && value) { - env[key] = value; - } - } - - // Parse headers - const headers: Record = {}; - for (const header of options.headers || []) { - const [key, value] = header.split('=', 2); - if (key && value) { - headers[key] = value; - } - } - - const config = { - name, - transport: { - type: transportType as 'stdio' | 'http' | 'sse' | 'streamable_http', - command: options.command, - args: options.args || [], - url: options.url, - env, - headers: Object.keys(headers).length > 0 ? headers : undefined - } - }; - - addMCPServer(config); - console.log(chalk.green(`✓ Added MCP server: ${name}`)); - - // Try to connect immediately - const manager = getMCPManager(); - await manager.addServer(config); - console.log(chalk.green(`✓ Connected to MCP server: ${name}`)); - - const tools = manager.getTools().filter(t => t.serverName === name); - console.log(chalk.blue(` Available tools: ${tools.length}`)); - - } catch (error: any) { - console.error(chalk.red(`Error adding MCP server: ${error.message}`)); - process.exit(1); - } - }); - - // Add server from JSON command - mcpCommand - .command('add-json ') - .description('Add an MCP server from JSON configuration') - .action(async (name: string, jsonConfig: string) => { - try { - let config; - try { - config = JSON.parse(jsonConfig); - } catch (error) { - console.error(chalk.red('Error: Invalid JSON configuration')); - process.exit(1); - } - - const serverConfig: MCPServerConfig = { - name, - transport: { - type: 'stdio', // default - command: config.command, - args: config.args || [], - env: config.env || {}, - url: config.url, - headers: config.headers - } - }; - - // Override transport type if specified - if (config.transport) { - if (typeof config.transport === 'string') { - serverConfig.transport.type = config.transport as 'stdio' | 'http' | 'sse'; - } else if (typeof config.transport === 'object') { - serverConfig.transport = { ...serverConfig.transport, ...config.transport }; - } - } - - addMCPServer(serverConfig); - console.log(chalk.green(`✓ Added MCP server: ${name}`)); - - // Try to connect immediately - const manager = getMCPManager(); - await manager.addServer(serverConfig); - console.log(chalk.green(`✓ Connected to MCP server: ${name}`)); - - const tools = manager.getTools().filter(t => t.serverName === name); - console.log(chalk.blue(` Available tools: ${tools.length}`)); - - } catch (error: any) { - console.error(chalk.red(`Error adding MCP server: ${error.message}`)); - process.exit(1); - } - }); - - // Remove server command - mcpCommand - .command('remove ') - .description('Remove an MCP server') - .action(async (name: string) => { - try { - const manager = getMCPManager(); - await manager.removeServer(name); - removeMCPServer(name); - console.log(chalk.green(`✓ Removed MCP server: ${name}`)); - } catch (error: any) { - console.error(chalk.red(`Error removing MCP server: ${error.message}`)); - process.exit(1); - } - }); - - // List servers command - mcpCommand - .command('list') - .description('List configured MCP servers') - .action(() => { - const config = loadMCPConfig(); - const manager = getMCPManager(); - - if (config.servers.length === 0) { - console.log(chalk.yellow('No MCP servers configured')); - return; - } - - console.log(chalk.bold('Configured MCP servers:')); - console.log(); - - for (const server of config.servers) { - const isConnected = manager.getServers().includes(server.name); - const status = isConnected - ? chalk.green('✓ Connected') - : chalk.red('✗ Disconnected'); - - console.log(`${chalk.bold(server.name)}: ${status}`); - - // Display transport information - if (server.transport) { - console.log(` Transport: ${server.transport.type}`); - if (server.transport.type === 'stdio') { - console.log(` Command: ${server.transport.command} ${(server.transport.args || []).join(' ')}`); - } else if (server.transport.type === 'http' || server.transport.type === 'sse') { - console.log(` URL: ${server.transport.url}`); - } - } else if (server.command) { - // Legacy format - console.log(` Command: ${server.command} ${(server.args || []).join(' ')}`); - } - - if (isConnected) { - const transportType = manager.getTransportType(server.name); - if (transportType) { - console.log(` Active Transport: ${transportType}`); - } - - const tools = manager.getTools().filter(t => t.serverName === server.name); - console.log(` Tools: ${tools.length}`); - if (tools.length > 0) { - tools.forEach(tool => { - const displayName = tool.name.replace(`mcp__${server.name}__`, ''); - console.log(` - ${displayName}: ${tool.description}`); - }); - } - } - - console.log(); - } - }); - - // Test server command - mcpCommand - .command('test ') - .description('Test connection to an MCP server') - .action(async (name: string) => { - try { - const config = loadMCPConfig(); - const serverConfig = config.servers.find(s => s.name === name); - - if (!serverConfig) { - console.error(chalk.red(`Server ${name} not found`)); - process.exit(1); - } - - console.log(chalk.blue(`Testing connection to ${name}...`)); - - const manager = getMCPManager(); - await manager.addServer(serverConfig); - - const tools = manager.getTools().filter(t => t.serverName === name); - console.log(chalk.green(`✓ Successfully connected to ${name}`)); - console.log(chalk.blue(` Available tools: ${tools.length}`)); - - if (tools.length > 0) { - console.log(' Tools:'); - tools.forEach(tool => { - const displayName = tool.name.replace(`mcp__${name}__`, ''); - console.log(` - ${displayName}: ${tool.description}`); - }); - } - - } catch (error: any) { - console.error(chalk.red(`✗ Failed to connect to ${name}: ${error.message}`)); - process.exit(1); - } - }); - - return mcpCommand; -} \ No newline at end of file diff --git a/src/grok/client.ts b/src/grok/client.ts index bf10180..2d4c87b 100644 --- a/src/grok/client.ts +++ b/src/grok/client.ts @@ -1,153 +1,39 @@ -import OpenAI from "openai"; -import type { ChatCompletionMessageParam } from "openai/resources/chat"; +import { createXai } from "@ai-sdk/xai"; +import { generateText } from "ai"; -export type GrokMessage = ChatCompletionMessageParam; +export type XaiProvider = ReturnType; -export interface GrokTool { - type: "function"; - function: { - name: string; - description: string; - parameters: { - type: "object"; - properties: Record; - required: string[]; - }; - }; +export function createProvider(apiKey: string, baseURL?: string): XaiProvider { + return createXai({ + apiKey, + baseURL: baseURL || process.env.GROK_BASE_URL || "https://api.x.ai/v1", + }); } -export interface GrokToolCall { - id: string; - type: "function"; - function: { - name: string; - arguments: string; - }; -} - -export interface SearchParameters { - mode?: "auto" | "on" | "off"; - // sources removed - let API use default sources to avoid format issues -} - -export interface SearchOptions { - search_parameters?: SearchParameters; -} - -export interface GrokResponse { - choices: Array<{ - message: { - role: string; - content: string | null; - tool_calls?: GrokToolCall[]; - }; - finish_reason: string; - }>; -} - -export class GrokClient { - private client: OpenAI; - private currentModel: string = "grok-code-fast-1"; - private defaultMaxTokens: number; - - constructor(apiKey: string, model?: string, baseURL?: string) { - this.client = new OpenAI({ - apiKey, - baseURL: baseURL || process.env.GROK_BASE_URL || "https://api.x.ai/v1", - timeout: 360000, +export async function generateTitle( + provider: XaiProvider, + userMessage: string, +): Promise { + try { + const { text } = await generateText({ + model: provider("grok-3-mini-fast"), + temperature: 0.5, + maxOutputTokens: 60, + system: [ + "You are a title generator. Output ONLY a short title. Nothing else.", + "Rules:", + "- Single line, ≤50 characters", + "- Use the same language as the user message", + "- Focus on the main topic or intent", + "- Keep technical terms, filenames, numbers exact", + "- Remove filler words (the, this, my, a, an)", + "- Never use tools or explain anything", + "- If the message is a greeting, output something like 'Quick chat'", + ].join("\n"), + prompt: userMessage, }); - const envMax = Number(process.env.GROK_MAX_TOKENS); - this.defaultMaxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 1536; - if (model) { - this.currentModel = model; - } - } - - setModel(model: string): void { - this.currentModel = model; - } - - getCurrentModel(): string { - return this.currentModel; - } - - async chat( - messages: GrokMessage[], - tools?: GrokTool[], - model?: string, - searchOptions?: SearchOptions - ): Promise { - try { - const requestPayload: any = { - model: model || this.currentModel, - messages, - tools: tools || [], - tool_choice: tools && tools.length > 0 ? "auto" : undefined, - temperature: 0.7, - max_tokens: this.defaultMaxTokens, - }; - - // Add search parameters if specified - if (searchOptions?.search_parameters) { - requestPayload.search_parameters = searchOptions.search_parameters; - } - - const response = - await this.client.chat.completions.create(requestPayload); - - return response as GrokResponse; - } catch (error: any) { - throw new Error(`Grok API error: ${error.message}`); - } - } - - async *chatStream( - messages: GrokMessage[], - tools?: GrokTool[], - model?: string, - searchOptions?: SearchOptions - ): AsyncGenerator { - try { - const requestPayload: any = { - model: model || this.currentModel, - messages, - tools: tools || [], - tool_choice: tools && tools.length > 0 ? "auto" : undefined, - temperature: 0.7, - max_tokens: this.defaultMaxTokens, - stream: true, - }; - - // Add search parameters if specified - if (searchOptions?.search_parameters) { - requestPayload.search_parameters = searchOptions.search_parameters; - } - - const stream = (await this.client.chat.completions.create( - requestPayload - )) as any; - - for await (const chunk of stream) { - yield chunk; - } - } catch (error: any) { - throw new Error(`Grok API error: ${error.message}`); - } - } - - async search( - query: string, - searchParameters?: SearchParameters - ): Promise { - const searchMessage: GrokMessage = { - role: "user", - content: query, - }; - - const searchOptions: SearchOptions = { - search_parameters: searchParameters || { mode: "on" }, - }; - - return this.chat([searchMessage], [], undefined, searchOptions); + return text?.trim().replace(/^["']|["']$/g, "") || "New session"; + } catch { + return "New session"; } } diff --git a/src/grok/models.ts b/src/grok/models.ts new file mode 100644 index 0000000..4fcd4f5 --- /dev/null +++ b/src/grok/models.ts @@ -0,0 +1,92 @@ +import type { ModelInfo } from "../types/index.js"; + +export const MODELS: ModelInfo[] = [ + { + id: "grok-4-0709", + name: "Grok 4", + contextWindow: 256_000, + inputPrice: 3.0, + outputPrice: 15.0, + reasoning: true, + description: "Flagship reasoning model", + }, + { + id: "grok-4.20-beta-0309", + name: "Grok 4.20 Beta", + contextWindow: 2_000_000, + inputPrice: 2.0, + outputPrice: 6.0, + reasoning: true, + description: "Multi-agent reasoning beta with 2M context", + }, + { + id: "grok-4-fast", + name: "Grok 4 Fast", + contextWindow: 2_000_000, + inputPrice: 0.2, + outputPrice: 0.5, + reasoning: true, + description: "Fast reasoning with 2M context", + }, + { + id: "grok-4-1-fast", + name: "Grok 4.1 Fast", + contextWindow: 2_000_000, + inputPrice: 0.2, + outputPrice: 0.5, + reasoning: true, + description: "Latest fast model with 2M context", + }, + { + id: "grok-code-fast-1", + name: "Grok Code Fast", + contextWindow: 256_000, + inputPrice: 0.2, + outputPrice: 1.5, + reasoning: false, + description: "Optimized for code generation", + }, + { + id: "grok-3", + name: "Grok 3", + contextWindow: 131_000, + inputPrice: 3.0, + outputPrice: 15.0, + reasoning: false, + description: "Grok 3 flagship", + }, + { + id: "grok-3-mini", + name: "Grok 3 Mini", + contextWindow: 131_000, + inputPrice: 0.3, + outputPrice: 0.5, + reasoning: false, + description: "Budget-friendly compact model", + }, +]; + +export const MODEL_GROUPS: { category: string; models: string[] }[] = [ + { + category: "Grok 4", + models: ["grok-4-0709", "grok-4.20-beta-0309", "grok-4-fast", "grok-4-1-fast"], + }, + { + category: "Grok Code", + models: ["grok-code-fast-1"], + }, + { + category: "Grok 3", + models: ["grok-3", "grok-3-mini"], + }, +]; + +export const DEFAULT_MODEL = "grok-4-1-fast"; + +export function getModelInfo(modelId: string): ModelInfo | undefined { + return MODELS.find((m) => m.id === modelId); +} + +export function getModelIds(): string[] { + return MODELS.map((m) => m.id); +} \ No newline at end of file diff --git a/src/grok/tools.ts b/src/grok/tools.ts index a050bca..d4daaca 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -1,375 +1,236 @@ -import { GrokTool } from "./client.js"; -import { MCPManager, MCPTool } from "../mcp/client.js"; -import { loadMCPConfig } from "../mcp/config.js"; +import { tool, generateText } from "ai"; +import { z } from "zod"; +import type { BashTool } from "../tools/bash.js"; +import { readFile, writeFile, editFile } from "../tools/file.js"; +import type { XaiProvider } from "./client.js"; +import type { AgentMode, TaskRequest, ToolResult } from "../types/index.js"; -const BASE_GROK_TOOLS: GrokTool[] = [ - { - type: "function", - function: { - name: "view_file", - description: "View contents of a file or list directory contents", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to file or directory to view", - }, - start_line: { - type: "number", - description: - "Starting line number for partial file view (optional)", - }, - end_line: { - type: "number", - description: "Ending line number for partial file view (optional)", - }, - }, - required: ["path"], - }, - }, - }, - { - type: "function", - function: { - name: "create_file", - description: "Create a new file with specified content", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "Path where the file should be created", - }, - content: { - type: "string", - description: "Content to write to the file", - }, - }, - required: ["path", "content"], - }, - }, - }, - { - type: "function", - function: { - name: "str_replace_editor", - description: "Replace specific text in a file. Use this for single line edits only", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the file to edit", - }, - old_str: { - type: "string", - description: - "Text to replace (must match exactly, or will use fuzzy matching for multi-line strings)", - }, - new_str: { - type: "string", - description: "Text to replace with", - }, - replace_all: { - type: "boolean", - description: - "Replace all occurrences (default: false, only replaces first occurrence)", - }, - }, - required: ["path", "old_str", "new_str"], - }, - }, - }, +const SEARCH_MODEL = "grok-3-mini-fast"; - { - type: "function", - function: { - name: "bash", - description: "Execute a bash command", - parameters: { - type: "object", - properties: { - command: { - type: "string", - description: "The bash command to execute", - }, - }, - required: ["command"], - }, - }, - }, - { - type: "function", - function: { - name: "search", +interface CreateToolsOptions { + runTask?: (request: TaskRequest) => Promise; +} + +export function createTools( + bash: BashTool, + provider: XaiProvider, + mode: AgentMode = "agent", + options: CreateToolsOptions = {}, +) { + const cwd = () => bash.getCwd(); + + const base = { + bash: tool({ description: - "Unified search tool for finding text content or files (similar to Cursor's search)", - parameters: { - type: "object", - properties: { - query: { - type: "string", - description: "Text to search for or file name/path pattern", - }, - search_type: { - type: "string", - enum: ["text", "files", "both"], - description: - "Type of search: 'text' for content search, 'files' for file names, 'both' for both (default: 'both')", - }, - include_pattern: { - type: "string", - description: - "Glob pattern for files to include (e.g. '*.ts', '*.js')", - }, - exclude_pattern: { - type: "string", - description: - "Glob pattern for files to exclude (e.g. '*.log', 'node_modules')", - }, - case_sensitive: { - type: "boolean", - description: - "Whether search should be case sensitive (default: false)", - }, - whole_word: { - type: "boolean", - description: "Whether to match whole words only (default: false)", - }, - regex: { - type: "boolean", - description: "Whether query is a regex pattern (default: false)", - }, - max_results: { - type: "number", - description: "Maximum number of results to return (default: 50)", - }, - file_types: { - type: "array", - items: { type: "string" }, - description: "File types to search (e.g. ['js', 'ts', 'py'])", - }, - include_hidden: { - type: "boolean", - description: "Whether to include hidden files (default: false)", - }, - }, - required: ["query"], + "Execute a bash command. Use for searching (grep, rg, find), git, build tools, package managers, running tests, and any other shell command. For file read/write/edit, prefer the dedicated file tools instead.", + inputSchema: z.object({ + command: z.string().describe("The bash command to execute"), + timeout: z + .number() + .optional() + .describe( + "Timeout in milliseconds (default: 30000). Use higher values for long-running commands.", + ), + }), + execute: async ({ command, timeout }) => { + const result = await bash.execute(command, timeout); + return { + success: result.success, + output: result.success + ? result.output || "Command executed successfully (no output)" + : result.error || "Command failed", + }; }, - }, - }, - { - type: "function", - function: { - name: "create_todo_list", - description: "Create a new todo list for planning and tracking tasks", - parameters: { - type: "object", - properties: { - todos: { - type: "array", - description: "Array of todo items", - items: { - type: "object", - properties: { - id: { - type: "string", - description: "Unique identifier for the todo item", - }, - content: { - type: "string", - description: "Description of the todo item", - }, - status: { - type: "string", - enum: ["pending", "in_progress", "completed"], - description: "Current status of the todo item", - }, - priority: { - type: "string", - enum: ["high", "medium", "low"], - description: "Priority level of the todo item", - }, - }, - required: ["id", "content", "status", "priority"], - }, - }, - }, - required: ["todos"], - }, - }, - }, - { - type: "function", - function: { - name: "update_todo_list", - description: "Update existing todos in the todo list", - parameters: { - type: "object", - properties: { - updates: { - type: "array", - description: "Array of todo updates", - items: { - type: "object", - properties: { - id: { - type: "string", - description: "ID of the todo item to update", - }, - status: { - type: "string", - enum: ["pending", "in_progress", "completed"], - description: "New status for the todo item", - }, - content: { - type: "string", - description: "New content for the todo item", - }, - priority: { - type: "string", - enum: ["high", "medium", "low"], - description: "New priority for the todo item", - }, - }, - required: ["id"], - }, - }, - }, - required: ["updates"], - }, - }, - }, -]; + }), -// Morph Fast Apply tool (conditional) -const MORPH_EDIT_TOOL: GrokTool = { - type: "function", - function: { - name: "edit_file", - description: "Use this tool to make an edit to an existing file.\n\nThis will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.\nWhen writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines.\n\nFor example:\n\n// ... existing code ...\nFIRST_EDIT\n// ... existing code ...\nSECOND_EDIT\n// ... existing code ...\nTHIRD_EDIT\n// ... existing code ...\n\nYou should still bias towards repeating as few lines of the original file as possible to convey the change.\nBut, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.\nDO NOT omit spans of pre-existing code (or comments) without using the // ... existing code ... comment to indicate its absence. If you omit the existing code comment, the model may inadvertently delete these lines.\nIf you plan on deleting a section, you must provide context before and after to delete it. If the initial code is ```code \\n Block 1 \\n Block 2 \\n Block 3 \\n code```, and you want to remove Block 2, you would output ```// ... existing code ... \\n Block 1 \\n Block 3 \\n // ... existing code ...```.\nMake sure it is clear what the edit should be, and where it should be applied.\nMake edits to a file in a single edit_file call instead of multiple edit_file calls to the same file. The apply model can handle many distinct edits at once.", - parameters: { - type: "object", - properties: { - target_file: { - type: "string", - description: "The target file to modify." - }, - instructions: { - type: "string", - description: "A single sentence instruction describing what you are going to do for the sketched edit. This is used to assist the less intelligent model in applying the edit. Use the first person to describe what you are going to do. Use it to disambiguate uncertainty in the edit." - }, - code_edit: { - type: "string", - description: "Specify ONLY the precise lines of code that you wish to edit. NEVER specify or write out unchanged code. Instead, represent all unchanged code using the comment of the language you're editing in - example: // ... existing code ..." + read_file: tool({ + description: + "Read the contents of a file. Returns numbered lines with a header showing the range and total line count. Use start_line/end_line to read specific sections of large files iteratively.", + inputSchema: z.object({ + path: z.string().describe("File path (relative to cwd or absolute)"), + start_line: z + .number() + .optional() + .describe("First line to read (1-indexed, default: 1)"), + end_line: z + .number() + .optional() + .describe("Last line to read (inclusive, default: end of file)"), + }), + execute: async ({ path, start_line, end_line }) => { + return readFile(path, cwd(), start_line, end_line); + }, + }), + + search_web: tool({ + description: + "Search the web for current information, documentation, APIs, tutorials, news, or any real-time data. Returns summarized results with sources.", + inputSchema: z.object({ + query: z.string().describe("The search query"), + }), + execute: async ({ query }) => { + try { + const { text } = await generateText({ + model: provider(SEARCH_MODEL), + maxOutputTokens: 4096, + providerOptions: { + xai: { + searchParameters: { + mode: "on", + returnCitations: true, + sources: [{ type: "web" }], + }, + }, + }, + prompt: query, + }); + return { success: true, output: text }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, output: `Web search failed: ${msg}` }; } }, - required: ["target_file", "instructions", "code_edit"] + }), + + search_x: tool({ + description: + "Search X (Twitter) for real-time posts, discussions, opinions, and trends. Returns relevant posts with authors and engagement data.", + inputSchema: z.object({ + query: z.string().describe("The search query"), + }), + execute: async ({ query }) => { + try { + const { text } = await generateText({ + model: provider(SEARCH_MODEL), + maxOutputTokens: 4096, + providerOptions: { + xai: { + searchParameters: { + mode: "on", + returnCitations: true, + sources: [{ type: "x" }], + }, + }, + }, + prompt: query, + }); + return { success: true, output: text }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, output: `X search failed: ${msg}` }; + } + }, + }), + + }; + + const tools: Record = { ...base }; + + if (mode === "agent") { + tools.write_file = tool({ + description: + "Create or overwrite a file with the given content. Use for creating new files or completely rewriting existing ones. Returns a diff of the changes.", + inputSchema: z.object({ + path: z.string().describe("File path (relative to cwd or absolute)"), + content: z.string().describe("The full file content to write"), + }), + execute: async ({ path, content }) => { + return writeFile(path, content, cwd()); + }, + }); + + tools.edit_file = tool({ + description: + "Edit a file by replacing a unique string with new content. The old_string must appear exactly once in the file. Include enough surrounding context lines in old_string to make it unique. Returns a diff of the changes.", + inputSchema: z.object({ + path: z.string().describe("File path (relative to cwd or absolute)"), + old_string: z.string().describe("The exact text to find (must be unique in the file)"), + new_string: z.string().describe("The replacement text"), + }), + execute: async ({ path, old_string, new_string }) => { + return editFile(path, old_string, new_string, cwd()); + }, + }); + + if (options.runTask) { + tools.task = tool({ + description: + "Delegate a focused task to a sub-agent. Prefer this proactively for review, research, investigation, and code quality work instead of waiting for the user to request a sub-agent. Use `general` for multi-step execution and `explore` for fast read-only investigation. Provide a short description plus a detailed prompt for the child agent.", + inputSchema: z.object({ + agent: z + .enum(["general", "explore"]) + .default("general") + .describe("Which sub-agent to use"), + description: z + .string() + .describe("A short label for the delegated task, such as 'Deep code quality analysis'"), + prompt: z + .string() + .describe("Detailed instructions for the sub-agent to complete"), + }), + execute: async ({ agent, description, prompt }) => { + return options.runTask!({ agent, description, prompt }); + }, + }); } } -}; -// Function to build tools array conditionally -function buildGrokTools(): GrokTool[] { - const tools = [...BASE_GROK_TOOLS]; - - // Add Morph Fast Apply tool if API key is available - if (process.env.MORPH_API_KEY) { - tools.splice(3, 0, MORPH_EDIT_TOOL); // Insert after str_replace_editor - } - + if (mode !== "plan") return tools; + + tools.generate_plan = tool({ + description: + "Generate an interactive implementation plan with steps and optional questions for the user. The plan is displayed in a structured UI where the user can review steps and answer questions. Always use this tool when creating plans.", + inputSchema: z.object({ + title: z.string().describe("Plan title"), + summary: z.string().describe("Brief summary of what the plan accomplishes"), + steps: z + .array( + z.object({ + title: z.string().describe("Step title"), + description: z + .string() + .describe("Detailed description of what this step involves"), + filePaths: z + .array(z.string()) + .optional() + .describe("Files affected by this step"), + }), + ) + .describe("Ordered list of implementation steps"), + questions: z + .array( + z.object({ + id: z.string().describe("Unique question identifier"), + question: z.string().describe("The question to ask the user"), + header: z + .string() + .optional() + .describe("Single-word tab label (e.g. 'Format', 'Storage', 'Testing')"), + type: z + .enum(["select", "multiselect", "text"]) + .describe("Question type: select (pick one), multiselect (pick many), or text (free-form)"), + options: z + .array( + z.object({ + id: z.string().describe("Option identifier"), + label: z.string().describe("Option display text"), + }), + ) + .optional() + .describe("Options for select/multiselect questions"), + }), + ) + .optional() + .describe("Questions for the user to answer before proceeding"), + }), + execute: async ({ title, summary, steps, questions }) => { + return { + success: true, + output: `Plan "${title}" generated with ${steps.length} steps`, + plan: { title, summary, steps, questions }, + }; + }, + }); + return tools; } - -// Export dynamic tools array -export const GROK_TOOLS: GrokTool[] = buildGrokTools(); - -// Global MCP manager instance -let mcpManager: MCPManager | null = null; - -export function getMCPManager(): MCPManager { - if (!mcpManager) { - mcpManager = new MCPManager(); - } - return mcpManager; -} - -export async function initializeMCPServers(): Promise { - const manager = getMCPManager(); - const config = loadMCPConfig(); - - // Store original stderr.write - const originalStderrWrite = process.stderr.write; - - // Temporarily suppress stderr to hide verbose MCP connection logs - process.stderr.write = function(chunk: any, encoding?: any, callback?: any): boolean { - // Filter out mcp-remote verbose logs - const chunkStr = chunk.toString(); - if (chunkStr.includes('[') && ( - chunkStr.includes('Using existing client port') || - chunkStr.includes('Connecting to remote server') || - chunkStr.includes('Using transport strategy') || - chunkStr.includes('Connected to remote server') || - chunkStr.includes('Local STDIO server running') || - chunkStr.includes('Proxy established successfully') || - chunkStr.includes('Local→Remote') || - chunkStr.includes('Remote→Local') - )) { - // Suppress these verbose logs - if (callback) callback(); - return true; - } - - // Allow other stderr output - return originalStderrWrite.call(this, chunk, encoding, callback); - }; - - try { - for (const serverConfig of config.servers) { - try { - await manager.addServer(serverConfig); - } catch (error) { - console.warn(`Failed to initialize MCP server ${serverConfig.name}:`, error); - } - } - } finally { - // Restore original stderr.write - process.stderr.write = originalStderrWrite; - } -} - -export function convertMCPToolToGrokTool(mcpTool: MCPTool): GrokTool { - return { - type: "function", - function: { - name: mcpTool.name, - description: mcpTool.description, - parameters: mcpTool.inputSchema || { - type: "object", - properties: {}, - required: [] - } - } - }; -} - -export function addMCPToolsToGrokTools(baseTools: GrokTool[]): GrokTool[] { - if (!mcpManager) { - return baseTools; - } - - const mcpTools = mcpManager.getTools(); - const grokMCPTools = mcpTools.map(convertMCPToolToGrokTool); - - return [...baseTools, ...grokMCPTools]; -} - -export async function getAllGrokTools(): Promise { - const manager = getMCPManager(); - // Try to initialize servers if not already done, but don't block - manager.ensureServersInitialized().catch(() => { - // Ignore initialization errors to avoid blocking - }); - return addMCPToolsToGrokTools(GROK_TOOLS); -} diff --git a/src/hooks/use-enhanced-input.ts b/src/hooks/use-enhanced-input.ts deleted file mode 100644 index 0184039..0000000 --- a/src/hooks/use-enhanced-input.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { useState, useCallback, useRef } from "react"; -import { - deleteCharBefore, - deleteCharAfter, - deleteWordBefore, - deleteWordAfter, - insertText, - moveToLineStart, - moveToLineEnd, - moveToPreviousWord, - moveToNextWord, -} from "../utils/text-utils.js"; -import { useInputHistory } from "./use-input-history.js"; - -export interface Key { - name?: string; - ctrl?: boolean; - meta?: boolean; - shift?: boolean; - paste?: boolean; - sequence?: string; - upArrow?: boolean; - downArrow?: boolean; - leftArrow?: boolean; - rightArrow?: boolean; - return?: boolean; - escape?: boolean; - tab?: boolean; - backspace?: boolean; - delete?: boolean; -} - -export interface EnhancedInputHook { - input: string; - cursorPosition: number; - isMultiline: boolean; - setInput: (text: string) => void; - setCursorPosition: (position: number) => void; - clearInput: () => void; - insertAtCursor: (text: string) => void; - resetHistory: () => void; - handleInput: (inputChar: string, key: Key) => void; -} - -interface UseEnhancedInputProps { - onSubmit?: (text: string) => void; - onEscape?: () => void; - onSpecialKey?: (key: Key) => boolean; // Return true to prevent default handling - disabled?: boolean; - multiline?: boolean; -} - -export function useEnhancedInput({ - onSubmit, - onEscape, - onSpecialKey, - disabled = false, - multiline = false, -}: UseEnhancedInputProps = {}): EnhancedInputHook { - const [input, setInputState] = useState(""); - const [cursorPosition, setCursorPositionState] = useState(0); - const isMultilineRef = useRef(multiline); - - const { - addToHistory, - navigateHistory, - resetHistory, - setOriginalInput, - isNavigatingHistory, - } = useInputHistory(); - - const setInput = useCallback((text: string) => { - setInputState(text); - setCursorPositionState(Math.min(text.length, cursorPosition)); - if (!isNavigatingHistory()) { - setOriginalInput(text); - } - }, [cursorPosition, isNavigatingHistory, setOriginalInput]); - - const setCursorPosition = useCallback((position: number) => { - setCursorPositionState(Math.max(0, Math.min(input.length, position))); - }, [input.length]); - - const clearInput = useCallback(() => { - setInputState(""); - setCursorPositionState(0); - setOriginalInput(""); - }, [setOriginalInput]); - - const insertAtCursor = useCallback((text: string) => { - const result = insertText(input, cursorPosition, text); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - }, [input, cursorPosition, setOriginalInput]); - - const handleSubmit = useCallback(() => { - if (input.trim()) { - addToHistory(input); - onSubmit?.(input); - clearInput(); - } - }, [input, addToHistory, onSubmit, clearInput]); - - const handleInput = useCallback((inputChar: string, key: Key) => { - if (disabled) return; - - // Handle Ctrl+C - check multiple ways it could be detected - if ((key.ctrl && inputChar === "c") || inputChar === "\x03") { - setInputState(""); - setCursorPositionState(0); - setOriginalInput(""); - return; - } - - // Allow special key handler to override default behavior - if (onSpecialKey?.(key)) { - return; - } - - // Handle Escape - if (key.escape) { - onEscape?.(); - return; - } - - // Handle Enter/Return - if (key.return) { - if (multiline && key.shift) { - // Shift+Enter in multiline mode inserts newline - const result = insertText(input, cursorPosition, "\n"); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } else { - handleSubmit(); - } - return; - } - - // Handle history navigation - if ((key.upArrow || key.name === 'up') && !key.ctrl && !key.meta) { - const historyInput = navigateHistory("up"); - if (historyInput !== null) { - setInputState(historyInput); - setCursorPositionState(historyInput.length); - } - return; - } - - if ((key.downArrow || key.name === 'down') && !key.ctrl && !key.meta) { - const historyInput = navigateHistory("down"); - if (historyInput !== null) { - setInputState(historyInput); - setCursorPositionState(historyInput.length); - } - return; - } - - // Handle cursor movement - ignore meta flag for arrows as it's unreliable in terminals - // Only do word movement if ctrl is pressed AND no arrow escape sequence is in inputChar - if ((key.leftArrow || key.name === 'left') && key.ctrl && !inputChar.includes('[')) { - const newPos = moveToPreviousWord(input, cursorPosition); - setCursorPositionState(newPos); - return; - } - - if ((key.rightArrow || key.name === 'right') && key.ctrl && !inputChar.includes('[')) { - const newPos = moveToNextWord(input, cursorPosition); - setCursorPositionState(newPos); - return; - } - - // Handle regular cursor movement - single character (ignore meta flag) - if (key.leftArrow || key.name === 'left') { - const newPos = Math.max(0, cursorPosition - 1); - setCursorPositionState(newPos); - return; - } - - if (key.rightArrow || key.name === 'right') { - const newPos = Math.min(input.length, cursorPosition + 1); - setCursorPositionState(newPos); - return; - } - - // Handle Home/End keys or Ctrl+A/E - if ((key.ctrl && inputChar === "a") || key.name === "home") { - setCursorPositionState(0); // Simple start of input - return; - } - - if ((key.ctrl && inputChar === "e") || key.name === "end") { - setCursorPositionState(input.length); // Simple end of input - return; - } - - // Handle deletion - check multiple ways backspace might be detected - // Backspace can be detected in different ways depending on terminal - // In some terminals, backspace shows up as delete:true with empty inputChar - const isBackspace = key.backspace || - key.name === 'backspace' || - inputChar === '\b' || - inputChar === '\x7f' || - (key.delete && inputChar === '' && !key.shift); - - if (isBackspace) { - if (key.ctrl || key.meta) { - // Ctrl/Cmd + Backspace: Delete word before cursor - const result = deleteWordBefore(input, cursorPosition); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } else { - // Regular backspace - const result = deleteCharBefore(input, cursorPosition); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } - return; - } - - // Handle forward delete (Del key) - but not if it was already handled as backspace above - if ((key.delete && inputChar !== '') || (key.ctrl && inputChar === "d")) { - if (key.ctrl || key.meta) { - // Ctrl/Cmd + Delete: Delete word after cursor - const result = deleteWordAfter(input, cursorPosition); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } else { - // Regular delete - const result = deleteCharAfter(input, cursorPosition); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } - return; - } - - // Handle Ctrl+K: Delete from cursor to end of line - if (key.ctrl && inputChar === "k") { - const lineEnd = moveToLineEnd(input, cursorPosition); - const newText = input.slice(0, cursorPosition) + input.slice(lineEnd); - setInputState(newText); - setOriginalInput(newText); - return; - } - - // Handle Ctrl+U: Delete from cursor to start of line - if (key.ctrl && inputChar === "u") { - const lineStart = moveToLineStart(input, cursorPosition); - const newText = input.slice(0, lineStart) + input.slice(cursorPosition); - setInputState(newText); - setCursorPositionState(lineStart); - setOriginalInput(newText); - return; - } - - // Handle Ctrl+W: Delete word before cursor - if (key.ctrl && inputChar === "w") { - const result = deleteWordBefore(input, cursorPosition); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - return; - } - - // Handle Ctrl+X: Clear entire input - if (key.ctrl && inputChar === "x") { - setInputState(""); - setCursorPositionState(0); - setOriginalInput(""); - return; - } - - // Handle regular character input - if (inputChar && !key.ctrl && !key.meta) { - const result = insertText(input, cursorPosition, inputChar); - setInputState(result.text); - setCursorPositionState(result.position); - setOriginalInput(result.text); - } - }, [disabled, onSpecialKey, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput]); - - return { - input, - cursorPosition, - isMultiline: isMultilineRef.current, - setInput, - setCursorPosition, - clearInput, - insertAtCursor, - resetHistory, - handleInput, - }; -} \ No newline at end of file diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts deleted file mode 100644 index 6feaf5f..0000000 --- a/src/hooks/use-input-handler.ts +++ /dev/null @@ -1,752 +0,0 @@ -import { useState, useMemo, useEffect } from "react"; -import { useInput } from "ink"; -import { GrokAgent, ChatEntry } from "../agent/grok-agent.js"; -import { ConfirmationService } from "../utils/confirmation-service.js"; -import { useEnhancedInput, Key } from "./use-enhanced-input.js"; - -import { filterCommandSuggestions } from "../ui/components/command-suggestions.js"; -import { loadModelConfig, updateCurrentModel } from "../utils/model-config.js"; - -interface UseInputHandlerProps { - agent: GrokAgent; - chatHistory: ChatEntry[]; - setChatHistory: React.Dispatch>; - setIsProcessing: (processing: boolean) => void; - setIsStreaming: (streaming: boolean) => void; - setTokenCount: (count: number) => void; - setProcessingTime: (time: number) => void; - processingStartTime: React.MutableRefObject; - isProcessing: boolean; - isStreaming: boolean; - isConfirmationActive?: boolean; -} - -interface CommandSuggestion { - command: string; - description: string; -} - -interface ModelOption { - model: string; -} - -export function useInputHandler({ - agent, - chatHistory, - setChatHistory, - setIsProcessing, - setIsStreaming, - setTokenCount, - setProcessingTime, - processingStartTime, - isProcessing, - isStreaming, - isConfirmationActive = false, -}: UseInputHandlerProps) { - const [showCommandSuggestions, setShowCommandSuggestions] = useState(false); - const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); - const [showModelSelection, setShowModelSelection] = useState(false); - const [selectedModelIndex, setSelectedModelIndex] = useState(0); - const [autoEditEnabled, setAutoEditEnabled] = useState(() => { - const confirmationService = ConfirmationService.getInstance(); - const sessionFlags = confirmationService.getSessionFlags(); - return sessionFlags.allOperations; - }); - - const handleSpecialKey = (key: Key): boolean => { - // Don't handle input if confirmation dialog is active - if (isConfirmationActive) { - return true; // Prevent default handling - } - - // Handle shift+tab to toggle auto-edit mode - if (key.shift && key.tab) { - const newAutoEditState = !autoEditEnabled; - setAutoEditEnabled(newAutoEditState); - - const confirmationService = ConfirmationService.getInstance(); - if (newAutoEditState) { - // Enable auto-edit: set all operations to be accepted - confirmationService.setSessionFlag("allOperations", true); - } else { - // Disable auto-edit: reset session flags - confirmationService.resetSession(); - } - return true; // Handled - } - - // Handle escape key for closing menus - if (key.escape) { - if (showCommandSuggestions) { - setShowCommandSuggestions(false); - setSelectedCommandIndex(0); - return true; - } - if (showModelSelection) { - setShowModelSelection(false); - setSelectedModelIndex(0); - return true; - } - if (isProcessing || isStreaming) { - agent.abortCurrentOperation(); - setIsProcessing(false); - setIsStreaming(false); - setTokenCount(0); - setProcessingTime(0); - processingStartTime.current = 0; - return true; - } - return false; // Let default escape handling work - } - - // Handle command suggestions navigation - if (showCommandSuggestions) { - const filteredSuggestions = filterCommandSuggestions( - commandSuggestions, - input - ); - - if (filteredSuggestions.length === 0) { - setShowCommandSuggestions(false); - setSelectedCommandIndex(0); - return false; // Continue processing - } else { - if (key.upArrow) { - setSelectedCommandIndex((prev) => - prev === 0 ? filteredSuggestions.length - 1 : prev - 1 - ); - return true; - } - if (key.downArrow) { - setSelectedCommandIndex( - (prev) => (prev + 1) % filteredSuggestions.length - ); - return true; - } - if (key.tab || key.return) { - const safeIndex = Math.min( - selectedCommandIndex, - filteredSuggestions.length - 1 - ); - const selectedCommand = filteredSuggestions[safeIndex]; - const newInput = selectedCommand.command + " "; - setInput(newInput); - setCursorPosition(newInput.length); - setShowCommandSuggestions(false); - setSelectedCommandIndex(0); - return true; - } - } - } - - // Handle model selection navigation - if (showModelSelection) { - if (key.upArrow) { - setSelectedModelIndex((prev) => - prev === 0 ? availableModels.length - 1 : prev - 1 - ); - return true; - } - if (key.downArrow) { - setSelectedModelIndex((prev) => (prev + 1) % availableModels.length); - return true; - } - if (key.tab || key.return) { - const selectedModel = availableModels[selectedModelIndex]; - agent.setModel(selectedModel.model); - updateCurrentModel(selectedModel.model); - const confirmEntry: ChatEntry = { - type: "assistant", - content: `✓ Switched to model: ${selectedModel.model}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, confirmEntry]); - setShowModelSelection(false); - setSelectedModelIndex(0); - return true; - } - } - - return false; // Let default handling proceed - }; - - const handleInputSubmit = async (userInput: string) => { - if (userInput === "exit" || userInput === "quit") { - process.exit(0); - return; - } - - if (userInput.trim()) { - const directCommandResult = await handleDirectCommand(userInput); - if (!directCommandResult) { - await processUserMessage(userInput); - } - } - }; - - const handleInputChange = (newInput: string) => { - // Update command suggestions based on input - if (newInput.startsWith("/")) { - setShowCommandSuggestions(true); - setSelectedCommandIndex(0); - } else { - setShowCommandSuggestions(false); - setSelectedCommandIndex(0); - } - }; - - const { - input, - cursorPosition, - setInput, - setCursorPosition, - clearInput, - resetHistory, - handleInput, - } = useEnhancedInput({ - onSubmit: handleInputSubmit, - onSpecialKey: handleSpecialKey, - disabled: isConfirmationActive, - }); - - // Hook up the actual input handling - useInput((inputChar: string, key: Key) => { - handleInput(inputChar, key); - }); - - // Update command suggestions when input changes - useEffect(() => { - handleInputChange(input); - }, [input]); - - const commandSuggestions: CommandSuggestion[] = [ - { command: "/help", description: "Show help information" }, - { command: "/clear", description: "Clear chat history" }, - { command: "/models", description: "Switch Grok Model" }, - { command: "/commit-and-push", description: "AI commit & push to remote" }, - { command: "/exit", description: "Exit the application" }, - ]; - - // Load models from configuration with fallback to defaults - const availableModels: ModelOption[] = useMemo(() => { - return loadModelConfig(); // Return directly, interface already matches - }, []); - - const handleDirectCommand = async (input: string): Promise => { - const trimmedInput = input.trim(); - - if (trimmedInput === "/clear") { - // Reset chat history - setChatHistory([]); - - // Reset processing states - setIsProcessing(false); - setIsStreaming(false); - setTokenCount(0); - setProcessingTime(0); - processingStartTime.current = 0; - - // Reset confirmation service session flags - const confirmationService = ConfirmationService.getInstance(); - confirmationService.resetSession(); - - clearInput(); - resetHistory(); - return true; - } - - if (trimmedInput === "/help") { - const helpEntry: ChatEntry = { - type: "assistant", - content: `Grok CLI Help: - -Built-in Commands: - /clear - Clear chat history - /help - Show this help - /models - Switch between available models - /exit - Exit application - exit, quit - Exit application - -Git Commands: - /commit-and-push - AI-generated commit + push to remote - -Enhanced Input Features: - ↑/↓ Arrow - Navigate command history - Ctrl+C - Clear input (press twice to exit) - Ctrl+←/→ - Move by word - Ctrl+A/E - Move to line start/end - Ctrl+W - Delete word before cursor - Ctrl+K - Delete to end of line - Ctrl+U - Delete to start of line - Shift+Tab - Toggle auto-edit mode (bypass confirmations) - -Direct Commands (executed immediately): - ls [path] - List directory contents - pwd - Show current directory - cd - Change directory - cat - View file contents - mkdir - Create directory - touch - Create empty file - -Model Configuration: - Edit ~/.grok/models.json to add custom models (Claude, GPT, Gemini, etc.) - -For complex operations, just describe what you want in natural language. -Examples: - "edit package.json and add a new script" - "create a new React component called Header" - "show me all TypeScript files in this project"`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, helpEntry]); - clearInput(); - return true; - } - - if (trimmedInput === "/exit") { - process.exit(0); - return true; - } - - if (trimmedInput === "/models") { - setShowModelSelection(true); - setSelectedModelIndex(0); - clearInput(); - return true; - } - - if (trimmedInput.startsWith("/models ")) { - const modelArg = trimmedInput.split(" ")[1]; - const modelNames = availableModels.map((m) => m.model); - - if (modelNames.includes(modelArg)) { - agent.setModel(modelArg); - updateCurrentModel(modelArg); // Update project current model - const confirmEntry: ChatEntry = { - type: "assistant", - content: `✓ Switched to model: ${modelArg}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, confirmEntry]); - } else { - const errorEntry: ChatEntry = { - type: "assistant", - content: `Invalid model: ${modelArg} - -Available models: ${modelNames.join(", ")}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, errorEntry]); - } - - clearInput(); - return true; - } - - - if (trimmedInput === "/commit-and-push") { - const userEntry: ChatEntry = { - type: "user", - content: "/commit-and-push", - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, userEntry]); - - setIsProcessing(true); - setIsStreaming(true); - - try { - // First check if there are any changes at all - const initialStatusResult = await agent.executeBashCommand( - "git status --porcelain" - ); - - if ( - !initialStatusResult.success || - !initialStatusResult.output?.trim() - ) { - const noChangesEntry: ChatEntry = { - type: "assistant", - content: "No changes to commit. Working directory is clean.", - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, noChangesEntry]); - setIsProcessing(false); - setIsStreaming(false); - setInput(""); - return true; - } - - // Add all changes - const addResult = await agent.executeBashCommand("git add ."); - - if (!addResult.success) { - const addErrorEntry: ChatEntry = { - type: "assistant", - content: `Failed to stage changes: ${ - addResult.error || "Unknown error" - }`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, addErrorEntry]); - setIsProcessing(false); - setIsStreaming(false); - setInput(""); - return true; - } - - // Show that changes were staged - const addEntry: ChatEntry = { - type: "tool_result", - content: "Changes staged successfully", - timestamp: new Date(), - toolCall: { - id: `git_add_${Date.now()}`, - type: "function", - function: { - name: "bash", - arguments: JSON.stringify({ command: "git add ." }), - }, - }, - toolResult: addResult, - }; - setChatHistory((prev) => [...prev, addEntry]); - - // Get staged changes for commit message generation - const diffResult = await agent.executeBashCommand("git diff --cached"); - - // Generate commit message using AI - const commitPrompt = `Generate a concise, professional git commit message for these changes: - -Git Status: -${initialStatusResult.output} - -Git Diff (staged changes): -${diffResult.output || "No staged changes shown"} - -Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters. -Respond with ONLY the commit message, no additional text.`; - - let commitMessage = ""; - let streamingEntry: ChatEntry | null = null; - - for await (const chunk of agent.processUserMessageStream( - commitPrompt - )) { - if (chunk.type === "content" && chunk.content) { - if (!streamingEntry) { - const newEntry = { - type: "assistant" as const, - content: `Generating commit message...\n\n${chunk.content}`, - timestamp: new Date(), - isStreaming: true, - }; - setChatHistory((prev) => [...prev, newEntry]); - streamingEntry = newEntry; - commitMessage = chunk.content; - } else { - commitMessage += chunk.content; - setChatHistory((prev) => - prev.map((entry, idx) => - idx === prev.length - 1 && entry.isStreaming - ? { - ...entry, - content: `Generating commit message...\n\n${commitMessage}`, - } - : entry - ) - ); - } - } else if (chunk.type === "done") { - if (streamingEntry) { - setChatHistory((prev) => - prev.map((entry) => - entry.isStreaming - ? { - ...entry, - content: `Generated commit message: "${commitMessage.trim()}"`, - isStreaming: false, - } - : entry - ) - ); - } - break; - } - } - - // Execute the commit - const cleanCommitMessage = commitMessage - .trim() - .replace(/^["']|["']$/g, ""); - const commitCommand = `git commit -m "${cleanCommitMessage}"`; - const commitResult = await agent.executeBashCommand(commitCommand); - - const commitEntry: ChatEntry = { - type: "tool_result", - content: commitResult.success - ? commitResult.output || "Commit successful" - : commitResult.error || "Commit failed", - timestamp: new Date(), - toolCall: { - id: `git_commit_${Date.now()}`, - type: "function", - function: { - name: "bash", - arguments: JSON.stringify({ command: commitCommand }), - }, - }, - toolResult: commitResult, - }; - setChatHistory((prev) => [...prev, commitEntry]); - - // If commit was successful, push to remote - if (commitResult.success) { - // First try regular push, if it fails try with upstream setup - let pushResult = await agent.executeBashCommand("git push"); - let pushCommand = "git push"; - - if ( - !pushResult.success && - pushResult.error?.includes("no upstream branch") - ) { - pushCommand = "git push -u origin HEAD"; - pushResult = await agent.executeBashCommand(pushCommand); - } - - const pushEntry: ChatEntry = { - type: "tool_result", - content: pushResult.success - ? pushResult.output || "Push successful" - : pushResult.error || "Push failed", - timestamp: new Date(), - toolCall: { - id: `git_push_${Date.now()}`, - type: "function", - function: { - name: "bash", - arguments: JSON.stringify({ command: pushCommand }), - }, - }, - toolResult: pushResult, - }; - setChatHistory((prev) => [...prev, pushEntry]); - } - } catch (error: any) { - const errorEntry: ChatEntry = { - type: "assistant", - content: `Error during commit and push: ${error.message}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, errorEntry]); - } - - setIsProcessing(false); - setIsStreaming(false); - clearInput(); - return true; - } - - const directBashCommands = [ - "ls", - "pwd", - "cd", - "cat", - "mkdir", - "touch", - "echo", - "grep", - "find", - "cp", - "mv", - "rm", - ]; - const firstWord = trimmedInput.split(" ")[0]; - - if (directBashCommands.includes(firstWord)) { - const userEntry: ChatEntry = { - type: "user", - content: trimmedInput, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, userEntry]); - - try { - const result = await agent.executeBashCommand(trimmedInput); - - const commandEntry: ChatEntry = { - type: "tool_result", - content: result.success - ? result.output || "Command completed" - : result.error || "Command failed", - timestamp: new Date(), - toolCall: { - id: `bash_${Date.now()}`, - type: "function", - function: { - name: "bash", - arguments: JSON.stringify({ command: trimmedInput }), - }, - }, - toolResult: result, - }; - setChatHistory((prev) => [...prev, commandEntry]); - } catch (error: any) { - const errorEntry: ChatEntry = { - type: "assistant", - content: `Error executing command: ${error.message}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, errorEntry]); - } - - clearInput(); - return true; - } - - return false; - }; - - const processUserMessage = async (userInput: string) => { - const userEntry: ChatEntry = { - type: "user", - content: userInput, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, userEntry]); - - setIsProcessing(true); - clearInput(); - - try { - setIsStreaming(true); - let streamingEntry: ChatEntry | null = null; - - for await (const chunk of agent.processUserMessageStream(userInput)) { - switch (chunk.type) { - case "content": - if (chunk.content) { - if (!streamingEntry) { - const newStreamingEntry = { - type: "assistant" as const, - content: chunk.content, - timestamp: new Date(), - isStreaming: true, - }; - setChatHistory((prev) => [...prev, newStreamingEntry]); - streamingEntry = newStreamingEntry; - } else { - setChatHistory((prev) => - prev.map((entry, idx) => - idx === prev.length - 1 && entry.isStreaming - ? { ...entry, content: entry.content + chunk.content } - : entry - ) - ); - } - } - break; - - case "token_count": - if (chunk.tokenCount !== undefined) { - setTokenCount(chunk.tokenCount); - } - break; - - case "tool_calls": - if (chunk.toolCalls) { - // Stop streaming for the current assistant message - setChatHistory((prev) => - prev.map((entry) => - entry.isStreaming - ? { - ...entry, - isStreaming: false, - toolCalls: chunk.toolCalls, - } - : entry - ) - ); - streamingEntry = null; - - // Add individual tool call entries to show tools are being executed - chunk.toolCalls.forEach((toolCall) => { - const toolCallEntry: ChatEntry = { - type: "tool_call", - content: "Executing...", - timestamp: new Date(), - toolCall: toolCall, - }; - setChatHistory((prev) => [...prev, toolCallEntry]); - }); - } - break; - - case "tool_result": - if (chunk.toolCall && chunk.toolResult) { - setChatHistory((prev) => - prev.map((entry) => { - if (entry.isStreaming) { - return { ...entry, isStreaming: false }; - } - // Update the existing tool_call entry with the result - if ( - entry.type === "tool_call" && - entry.toolCall?.id === chunk.toolCall?.id - ) { - return { - ...entry, - type: "tool_result", - content: chunk.toolResult.success - ? chunk.toolResult.output || "Success" - : chunk.toolResult.error || "Error occurred", - toolResult: chunk.toolResult, - }; - } - return entry; - }) - ); - streamingEntry = null; - } - break; - - case "done": - if (streamingEntry) { - setChatHistory((prev) => - prev.map((entry) => - entry.isStreaming ? { ...entry, isStreaming: false } : entry - ) - ); - } - setIsStreaming(false); - break; - } - } - } catch (error: any) { - const errorEntry: ChatEntry = { - type: "assistant", - content: `Error: ${error.message}`, - timestamp: new Date(), - }; - setChatHistory((prev) => [...prev, errorEntry]); - setIsStreaming(false); - } - - setIsProcessing(false); - processingStartTime.current = 0; - }; - - - return { - input, - cursorPosition, - showCommandSuggestions, - selectedCommandIndex, - showModelSelection, - selectedModelIndex, - commandSuggestions, - availableModels, - agent, - autoEditEnabled, - }; -} diff --git a/src/hooks/use-input-history.ts b/src/hooks/use-input-history.ts deleted file mode 100644 index 41900cd..0000000 --- a/src/hooks/use-input-history.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useState, useCallback } from "react"; - -export interface InputHistoryHook { - addToHistory: (input: string) => void; - navigateHistory: (direction: "up" | "down") => string | null; - getCurrentHistoryIndex: () => number; - resetHistory: () => void; - isNavigatingHistory: () => boolean; - setOriginalInput: (input: string) => void; -} - -export function useInputHistory(): InputHistoryHook { - const [history, setHistory] = useState([]); - const [currentIndex, setCurrentIndex] = useState(-1); - const [originalInput, setOriginalInput] = useState(""); - - const addToHistory = useCallback((input: string) => { - if (input.trim() && !history.includes(input.trim())) { - setHistory(prev => [...prev, input.trim()]); - } - setCurrentIndex(-1); - setOriginalInput(""); - }, [history]); - - const navigateHistory = useCallback((direction: "up" | "down"): string | null => { - if (history.length === 0) return null; - - let newIndex: number; - - if (direction === "up") { - if (currentIndex === -1) { - newIndex = history.length - 1; - } else { - newIndex = Math.max(0, currentIndex - 1); - } - } else { - if (currentIndex === -1) { - return null; - } else if (currentIndex === history.length - 1) { - newIndex = -1; - return originalInput; - } else { - newIndex = Math.min(history.length - 1, currentIndex + 1); - } - } - - setCurrentIndex(newIndex); - return newIndex === -1 ? originalInput : history[newIndex]; - }, [history, currentIndex, originalInput]); - - const getCurrentHistoryIndex = useCallback(() => currentIndex, [currentIndex]); - - const resetHistory = useCallback(() => { - setHistory([]); - setCurrentIndex(-1); - setOriginalInput(""); - }, []); - - const isNavigatingHistory = useCallback(() => currentIndex !== -1, [currentIndex]); - - const setOriginalInputCallback = useCallback((input: string) => { - if (currentIndex === -1) { - setOriginalInput(input); - } - }, [currentIndex]); - - return { - addToHistory, - navigateHistory, - getCurrentHistoryIndex, - resetHistory, - isNavigatingHistory, - setOriginalInput: setOriginalInputCallback, - }; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 8d3dc11..5c34c33 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,462 +1,160 @@ -#!/usr/bin/env node -import React from "react"; -import { render } from "ink"; +#!/usr/bin/env bun import { program } from "commander"; import * as dotenv from "dotenv"; -import { GrokAgent } from "./agent/grok-agent.js"; -import ChatInterface from "./ui/components/chat-interface.js"; -import { getSettingsManager } from "./utils/settings-manager.js"; -import { ConfirmationService } from "./utils/confirmation-service.js"; -import { createMCPCommand } from "./commands/mcp.js"; -import type { ChatCompletionMessageParam } from "openai/resources/chat"; +import { createCliRenderer } from "@opentui/core"; +import { createRoot } from "@opentui/react"; +import { createElement } from "react"; +import { Agent } from "./agent/agent.js"; +import { App } from "./ui/app.js"; +import { getApiKey, getBaseURL, getCurrentModel, saveUserSettings } from "./utils/settings.js"; +import { MODELS } from "./grok/models.js"; -// Load environment variables dotenv.config(); -// Disable default SIGINT handling to let Ink handle Ctrl+C -// We'll handle exit through the input system instead - process.on("SIGTERM", () => { - // Restore terminal to normal mode before exit - if (process.stdin.isTTY && process.stdin.setRawMode) { - try { - process.stdin.setRawMode(false); - } catch (e) { - // Ignore errors when setting raw mode - } - } - console.log("\nGracefully shutting down..."); process.exit(0); }); -// Handle uncaught exceptions to prevent hanging -process.on("uncaughtException", (error) => { - console.error("Uncaught exception:", error); +process.on("uncaughtException", (err) => { + console.error("Fatal:", err.message); process.exit(1); }); -process.on("unhandledRejection", (reason, promise) => { - console.error("Unhandled rejection at:", promise, "reason:", reason); +process.on("unhandledRejection", (reason) => { + console.error("Unhandled rejection:", reason); process.exit(1); }); -// Ensure user settings are initialized -function ensureUserSettingsDirectory(): void { - try { - const manager = getSettingsManager(); - // This will create default settings if they don't exist - manager.loadUserSettings(); - } catch (error) { - // Silently ignore errors during setup - } -} - -// Load API key from user settings if not in environment -function loadApiKey(): string | undefined { - const manager = getSettingsManager(); - return manager.getApiKey(); -} - -// Load base URL from user settings if not in environment -function loadBaseURL(): string { - const manager = getSettingsManager(); - return manager.getBaseURL(); -} - -// Save command line settings to user settings file -async function saveCommandLineSettings( - apiKey?: string, - baseURL?: string -): Promise { - try { - const manager = getSettingsManager(); - - // Update with command line values - if (apiKey) { - manager.updateUserSetting("apiKey", apiKey); - console.log("✅ API key saved to ~/.grok/user-settings.json"); - } - if (baseURL) { - manager.updateUserSetting("baseURL", baseURL); - console.log("✅ Base URL saved to ~/.grok/user-settings.json"); - } - } catch (error) { - console.warn( - "⚠️ Could not save settings to file:", - error instanceof Error ? error.message : "Unknown error" - ); - } -} - -// Load model from user settings if not in environment -function loadModel(): string | undefined { - // First check environment variables - let model = process.env.GROK_MODEL; - - if (!model) { - // Use the unified model loading from settings manager - try { - const manager = getSettingsManager(); - model = manager.getCurrentModel(); - } catch (error) { - // Ignore errors, model will remain undefined - } - } - - return model; -} - -// Handle commit-and-push command in headless mode -async function handleCommitAndPushHeadless( +async function startInteractive( apiKey: string, - baseURL?: string, - model?: string, - maxToolRounds?: number -): Promise { - try { - const agent = new GrokAgent(apiKey, baseURL, model, maxToolRounds); + baseURL: string, + model: string, + maxToolRounds: number, + initialMessage?: string, +) { + const agent = new Agent(apiKey, baseURL, model, maxToolRounds); - // Configure confirmation service for headless mode (auto-approve all operations) - const confirmationService = ConfirmationService.getInstance(); - confirmationService.setSessionFlag("allOperations", true); + const renderer = await createCliRenderer({ + exitOnCtrlC: false, + }); - console.log("🤖 Processing commit and push...\n"); - console.log("> /commit-and-push\n"); + const onExit = () => { renderer.destroy(); process.exit(0); }; - // First check if there are any changes at all - const initialStatusResult = await agent.executeBashCommand( - "git status --porcelain" - ); - - if (!initialStatusResult.success || !initialStatusResult.output?.trim()) { - console.log("❌ No changes to commit. Working directory is clean."); - process.exit(1); - } - - console.log("✅ git status: Changes detected"); - - // Add all changes - const addResult = await agent.executeBashCommand("git add ."); - - if (!addResult.success) { - console.log( - `❌ git add: ${addResult.error || "Failed to stage changes"}` - ); - process.exit(1); - } - - console.log("✅ git add: Changes staged"); - - // Get staged changes for commit message generation - const diffResult = await agent.executeBashCommand("git diff --cached"); - - // Generate commit message using AI - const commitPrompt = `Generate a concise, professional git commit message for these changes: - -Git Status: -${initialStatusResult.output} - -Git Diff (staged changes): -${diffResult.output || "No staged changes shown"} - -Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters. -Respond with ONLY the commit message, no additional text.`; - - console.log("🤖 Generating commit message..."); - - const commitMessageEntries = await agent.processUserMessage(commitPrompt); - let commitMessage = ""; - - // Extract the commit message from the AI response - for (const entry of commitMessageEntries) { - if (entry.type === "assistant" && entry.content.trim()) { - commitMessage = entry.content.trim(); - break; - } - } - - if (!commitMessage) { - console.log("❌ Failed to generate commit message"); - process.exit(1); - } - - // Clean the commit message - const cleanCommitMessage = commitMessage.replace(/^["']|["']$/g, ""); - console.log(`✅ Generated commit message: "${cleanCommitMessage}"`); - - // Execute the commit - const commitCommand = `git commit -m "${cleanCommitMessage}"`; - const commitResult = await agent.executeBashCommand(commitCommand); - - if (commitResult.success) { - console.log( - `✅ git commit: ${ - commitResult.output?.split("\n")[0] || "Commit successful" - }` - ); - - // If commit was successful, push to remote - // First try regular push, if it fails try with upstream setup - let pushResult = await agent.executeBashCommand("git push"); - - if ( - !pushResult.success && - pushResult.error?.includes("no upstream branch") - ) { - console.log("🔄 Setting upstream and pushing..."); - pushResult = await agent.executeBashCommand("git push -u origin HEAD"); - } - - if (pushResult.success) { - console.log( - `✅ git push: ${ - pushResult.output?.split("\n")[0] || "Push successful" - }` - ); - } else { - console.log(`❌ git push: ${pushResult.error || "Push failed"}`); - process.exit(1); - } - } else { - console.log(`❌ git commit: ${commitResult.error || "Commit failed"}`); - process.exit(1); - } - } catch (error: any) { - console.error("❌ Error during commit and push:", error.message); - process.exit(1); - } + createRoot(renderer).render(createElement(App, { agent, initialMessage, onExit })); } -// Headless mode processing function -async function processPromptHeadless( +async function runHeadless( prompt: string, apiKey: string, - baseURL?: string, - model?: string, - maxToolRounds?: number -): Promise { - try { - const agent = new GrokAgent(apiKey, baseURL, model, maxToolRounds); + baseURL: string, + model: string, + maxToolRounds: number, +) { + const agent = new Agent(apiKey, baseURL, model, maxToolRounds); - // Configure confirmation service for headless mode (auto-approve all operations) - const confirmationService = ConfirmationService.getInstance(); - confirmationService.setSessionFlag("allOperations", true); + process.stdout.write(`\x1b[36m⏳ Processing...\x1b[0m\n`); - // Process the user message - const chatEntries = await agent.processUserMessage(prompt); - - // Convert chat entries to OpenAI compatible message objects - const messages: ChatCompletionMessageParam[] = []; - - for (const entry of chatEntries) { - switch (entry.type) { - case "user": - messages.push({ - role: "user", - content: entry.content, - }); - break; - - case "assistant": - const assistantMessage: ChatCompletionMessageParam = { - role: "assistant", - content: entry.content, - }; - - // Add tool calls if present - if (entry.toolCalls && entry.toolCalls.length > 0) { - assistantMessage.tool_calls = entry.toolCalls.map((toolCall) => ({ - id: toolCall.id, - type: "function", - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, - })); + for await (const chunk of agent.processMessage(prompt)) { + switch (chunk.type) { + case "content": + if (chunk.content) process.stdout.write(chunk.content); + break; + case "tool_calls": + if (chunk.toolCalls) { + for (const tc of chunk.toolCalls) { + process.stderr.write(`\x1b[33m⚙ ${tc.function.name}\x1b[0m\n`); } - - messages.push(assistantMessage); - break; - - case "tool_result": - if (entry.toolCall) { - messages.push({ - role: "tool", - tool_call_id: entry.toolCall.id, - content: entry.content, - }); - } - break; - } + } + break; + case "tool_result": + if (chunk.toolResult) { + const icon = chunk.toolResult.success ? "✓" : "✗"; + const color = chunk.toolResult.success ? "\x1b[32m" : "\x1b[31m"; + process.stderr.write(`${color}${icon} ${chunk.toolCall?.function.name || "tool"}\x1b[0m\n`); + } + break; + case "error": + process.stderr.write(`\x1b[31m${chunk.content}\x1b[0m\n`); + break; + case "done": + process.stdout.write("\n"); + break; } + } +} - // Output each message as a separate JSON object - for (const message of messages) { - console.log(JSON.stringify(message)); - } - } catch (error: any) { - // Output error in OpenAI compatible format - console.log( - JSON.stringify({ - role: "assistant", - content: `Error: ${error.message}`, - }) +function resolveConfig(options: Record) { + const apiKey = options.apiKey || getApiKey(); + const baseURL = options.baseUrl || getBaseURL(); + const model = options.model || getCurrentModel(); + const maxToolRounds = parseInt(options.maxToolRounds || "400") || 400; + + if (!apiKey) { + console.error( + "Error: API key required. Set GROK_API_KEY env var, use --api-key, or save to ~/.grok/user-settings.json", ); process.exit(1); } + + if (options.apiKey) saveUserSettings({ apiKey: options.apiKey }); + if (options.baseUrl) saveUserSettings({ baseURL: options.baseUrl }); + + return { apiKey, baseURL, model, maxToolRounds }; } program .name("grok") - .description( - "A conversational AI CLI tool powered by Grok with text editor capabilities" - ) - .version("1.0.1") - .argument("[message...]", "Initial message to send to Grok") - .option("-d, --directory ", "set working directory", process.cwd()) - .option("-k, --api-key ", "Grok API key (or set GROK_API_KEY env var)") - .option( - "-u, --base-url ", - "Grok API base URL (or set GROK_BASE_URL env var)" - ) - .option( - "-m, --model ", - "AI model to use (e.g., grok-code-fast-1, grok-4-latest) (or set GROK_MODEL env var)" - ) - .option( - "-p, --prompt ", - "process a single prompt and exit (headless mode)" - ) - .option( - "--max-tool-rounds ", - "maximum number of tool execution rounds (default: 400)", - "400" - ) - .action(async (message, options) => { + .description("AI coding agent powered by Grok — built with Bun and OpenTUI") + .version("1.0.0") + .argument("[message...]", "Initial message to send") + .option("-k, --api-key ", "Grok API key") + .option("-u, --base-url ", "API base URL") + .option("-m, --model ", "Model to use") + .option("-d, --directory ", "Working directory", process.cwd()) + .option("-p, --prompt ", "Run a single prompt headlessly") + .option("--max-tool-rounds ", "Max tool execution rounds", "400") + .action(async (message: string[], options) => { if (options.directory) { try { process.chdir(options.directory); - } catch (error: any) { - console.error( - `Error changing directory to ${options.directory}:`, - error.message - ); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`Cannot change to directory ${options.directory}: ${msg}`); process.exit(1); } } - try { - // Get API key from options, environment, or user settings - const apiKey = options.apiKey || loadApiKey(); - const baseURL = options.baseUrl || loadBaseURL(); - const model = options.model || loadModel(); - const maxToolRounds = parseInt(options.maxToolRounds) || 400; + const config = resolveConfig(options); - if (!apiKey) { - console.error( - "❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or set \"apiKey\" field in ~/.grok/user-settings.json" - ); - process.exit(1); - } - - // Save API key and base URL to user settings if provided via command line - if (options.apiKey || options.baseUrl) { - await saveCommandLineSettings(options.apiKey, options.baseUrl); - } - - // Headless mode: process prompt and exit - if (options.prompt) { - await processPromptHeadless( - options.prompt, - apiKey, - baseURL, - model, - maxToolRounds - ); - return; - } - - // Interactive mode: launch UI - const agent = new GrokAgent(apiKey, baseURL, model, maxToolRounds); - console.log("🤖 Starting Grok CLI Conversational Assistant...\n"); - - ensureUserSettingsDirectory(); - - // Support variadic positional arguments for multi-word initial message - const initialMessage = Array.isArray(message) - ? message.join(" ") - : message; - - render(React.createElement(ChatInterface, { agent, initialMessage })); - } catch (error: any) { - console.error("❌ Error initializing Grok CLI:", error.message); - process.exit(1); + if (options.prompt) { + await runHeadless(options.prompt, config.apiKey, config.baseURL, config.model, config.maxToolRounds); + return; } + + const initialMessage = message.length > 0 ? message.join(" ") : undefined; + await startInteractive(config.apiKey, config.baseURL, config.model, config.maxToolRounds, initialMessage); }); -// Git subcommand -const gitCommand = program - .command("git") - .description("Git operations with AI assistance"); - -gitCommand - .command("commit-and-push") - .description("Generate AI commit message and push to remote") - .option("-d, --directory ", "set working directory", process.cwd()) - .option("-k, --api-key ", "Grok API key (or set GROK_API_KEY env var)") - .option( - "-u, --base-url ", - "Grok API base URL (or set GROK_BASE_URL env var)" - ) - .option( - "-m, --model ", - "AI model to use (e.g., grok-code-fast-1, grok-4-latest) (or set GROK_MODEL env var)" - ) - .option( - "--max-tool-rounds ", - "maximum number of tool execution rounds (default: 400)", - "400" - ) - .action(async (options) => { - if (options.directory) { - try { - process.chdir(options.directory); - } catch (error: any) { - console.error( - `Error changing directory to ${options.directory}:`, - error.message - ); - process.exit(1); - } - } - - try { - // Get API key from options, environment, or user settings - const apiKey = options.apiKey || loadApiKey(); - const baseURL = options.baseUrl || loadBaseURL(); - const model = options.model || loadModel(); - const maxToolRounds = parseInt(options.maxToolRounds) || 400; - - if (!apiKey) { - console.error( - "❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or save to ~/.grok/user-settings.json" - ); - process.exit(1); - } - - // Save API key and base URL to user settings if provided via command line - if (options.apiKey || options.baseUrl) { - await saveCommandLineSettings(options.apiKey, options.baseUrl); - } - - await handleCommitAndPushHeadless(apiKey, baseURL, model, maxToolRounds); - } catch (error: any) { - console.error("❌ Error during git commit-and-push:", error.message); - process.exit(1); +program + .command("models") + .description("List available Grok models") + .action(() => { + console.log("\nAvailable Grok Models:\n"); + for (const m of MODELS) { + const reasoning = m.reasoning ? " (reasoning)" : ""; + console.log( + ` \x1b[36m${m.id}\x1b[0m — ${m.name}${reasoning}`, + ); + console.log( + ` ${m.description} | ${formatContext(m.contextWindow)} context | $${m.inputPrice}/$${m.outputPrice} per 1M tokens`, + ); } + console.log(); }); -// MCP command -program.addCommand(createMCPCommand()); - program.parse(); + +function formatContext(tokens: number): string { + if (tokens >= 1_000_000) return `${tokens / 1_000_000}M`; + return `${tokens / 1_000}K`; +} diff --git a/src/mcp/client.ts b/src/mcp/client.ts deleted file mode 100644 index 598f367..0000000 --- a/src/mcp/client.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { EventEmitter } from "events"; -import { createTransport, MCPTransport, TransportType, TransportConfig } from "./transports.js"; - -export interface MCPServerConfig { - name: string; - transport: TransportConfig; - // Legacy support for stdio-only configs - command?: string; - args?: string[]; - env?: Record; -} - -export interface MCPTool { - name: string; - description: string; - inputSchema: any; - serverName: string; -} - -export class MCPManager extends EventEmitter { - private clients: Map = new Map(); - private transports: Map = new Map(); - private tools: Map = new Map(); - - async addServer(config: MCPServerConfig): Promise { - try { - // Handle legacy stdio-only configuration - let transportConfig = config.transport; - if (!transportConfig && config.command) { - transportConfig = { - type: 'stdio', - command: config.command, - args: config.args, - env: config.env - }; - } - - if (!transportConfig) { - throw new Error('Transport configuration is required'); - } - - // Create transport - const transport = createTransport(transportConfig); - this.transports.set(config.name, transport); - - // Create client - const client = new Client( - { - name: "grok-cli", - version: "1.0.0" - }, - { - capabilities: { - tools: {} - } - } - ); - - this.clients.set(config.name, client); - - // Connect - const sdkTransport = await transport.connect(); - await client.connect(sdkTransport); - - // List available tools - const toolsResult = await client.listTools(); - - // Register tools - for (const tool of toolsResult.tools) { - const mcpTool: MCPTool = { - name: `mcp__${config.name}__${tool.name}`, - description: tool.description || `Tool from ${config.name} server`, - inputSchema: tool.inputSchema, - serverName: config.name - }; - this.tools.set(mcpTool.name, mcpTool); - } - - this.emit('serverAdded', config.name, toolsResult.tools.length); - } catch (error) { - this.emit('serverError', config.name, error); - throw error; - } - } - - async removeServer(serverName: string): Promise { - // Remove tools - for (const [toolName, tool] of this.tools.entries()) { - if (tool.serverName === serverName) { - this.tools.delete(toolName); - } - } - - // Disconnect client - const client = this.clients.get(serverName); - if (client) { - await client.close(); - this.clients.delete(serverName); - } - - // Close transport - const transport = this.transports.get(serverName); - if (transport) { - await transport.disconnect(); - this.transports.delete(serverName); - } - - this.emit('serverRemoved', serverName); - } - - async callTool(toolName: string, arguments_: any): Promise { - const tool = this.tools.get(toolName); - if (!tool) { - throw new Error(`Tool ${toolName} not found`); - } - - const client = this.clients.get(tool.serverName); - if (!client) { - throw new Error(`Server ${tool.serverName} not connected`); - } - - // Extract the original tool name (remove mcp__servername__ prefix) - const originalToolName = toolName.replace(`mcp__${tool.serverName}__`, ''); - - return await client.callTool({ - name: originalToolName, - arguments: arguments_ - }); - } - - getTools(): MCPTool[] { - return Array.from(this.tools.values()); - } - - getServers(): string[] { - return Array.from(this.clients.keys()); - } - - async shutdown(): Promise { - const serverNames = Array.from(this.clients.keys()); - await Promise.all(serverNames.map(name => this.removeServer(name))); - } - - getTransportType(serverName: string): TransportType | undefined { - const transport = this.transports.get(serverName); - return transport?.getType(); - } - - async ensureServersInitialized(): Promise { - if (this.clients.size > 0) { - return; // Already initialized - } - - const { loadMCPConfig } = await import('../mcp/config'); - const config = loadMCPConfig(); - - // Initialize servers in parallel to avoid blocking - const initPromises = config.servers.map(async (serverConfig) => { - try { - await this.addServer(serverConfig); - } catch (error) { - console.warn(`Failed to initialize MCP server ${serverConfig.name}:`, error); - } - }); - - await Promise.all(initPromises); - } -} \ No newline at end of file diff --git a/src/mcp/config.ts b/src/mcp/config.ts deleted file mode 100644 index 3e319a8..0000000 --- a/src/mcp/config.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getSettingsManager } from "../utils/settings-manager.js"; -import { MCPServerConfig } from "./client.js"; - -export interface MCPConfig { - servers: MCPServerConfig[]; -} - -/** - * Load MCP configuration from project settings - */ -export function loadMCPConfig(): MCPConfig { - const manager = getSettingsManager(); - const projectSettings = manager.loadProjectSettings(); - const servers = projectSettings.mcpServers ? Object.values(projectSettings.mcpServers) : []; - return { servers }; -} - -export function saveMCPConfig(config: MCPConfig): void { - const manager = getSettingsManager(); - const mcpServers: Record = {}; - - // Convert servers array to object keyed by name - for (const server of config.servers) { - mcpServers[server.name] = server; - } - - manager.updateProjectSetting('mcpServers', mcpServers); -} - -export function addMCPServer(config: MCPServerConfig): void { - const manager = getSettingsManager(); - const projectSettings = manager.loadProjectSettings(); - const mcpServers = projectSettings.mcpServers || {}; - - mcpServers[config.name] = config; - manager.updateProjectSetting('mcpServers', mcpServers); -} - -export function removeMCPServer(serverName: string): void { - const manager = getSettingsManager(); - const projectSettings = manager.loadProjectSettings(); - const mcpServers = projectSettings.mcpServers; - - if (mcpServers) { - delete mcpServers[serverName]; - manager.updateProjectSetting('mcpServers', mcpServers); - } -} - -export function getMCPServer(serverName: string): MCPServerConfig | undefined { - const manager = getSettingsManager(); - const projectSettings = manager.loadProjectSettings(); - return projectSettings.mcpServers?.[serverName]; -} - -// Predefined server configurations -export const PREDEFINED_SERVERS: Record = {}; diff --git a/src/mcp/transports.ts b/src/mcp/transports.ts deleted file mode 100644 index 1b482e2..0000000 --- a/src/mcp/transports.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { ChildProcess, spawn } from "child_process"; -import { EventEmitter } from "events"; -import axios, { AxiosInstance } from "axios"; - -export type TransportType = 'stdio' | 'http' | 'sse' | 'streamable_http'; - -export interface TransportConfig { - type: TransportType; - command?: string; - args?: string[]; - env?: Record; - url?: string; - headers?: Record; -} - -export interface MCPTransport { - connect(): Promise; - disconnect(): Promise; - getType(): TransportType; -} - -export class StdioTransport implements MCPTransport { - private transport?: StdioClientTransport; - private process?: ChildProcess; - - constructor(private config: TransportConfig) { - if (!config.command) { - throw new Error('Command is required for stdio transport'); - } - } - - async connect(): Promise { - // Create transport with environment variables to suppress verbose output - const env = { - ...process.env, - ...this.config.env, - // Try to suppress verbose output from mcp-remote - MCP_REMOTE_QUIET: '1', - MCP_REMOTE_SILENT: '1', - DEBUG: '', - NODE_ENV: 'production' - }; - - this.transport = new StdioClientTransport({ - command: this.config.command!, - args: this.config.args || [], - env - }); - - return this.transport; - } - - async disconnect(): Promise { - if (this.transport) { - await this.transport.close(); - this.transport = undefined; - } - - if (this.process) { - this.process.kill(); - this.process = undefined; - } - } - - getType(): TransportType { - return 'stdio'; - } -} - -export class HttpTransport extends EventEmitter implements MCPTransport { - private client?: AxiosInstance; - private connected = false; - - constructor(private config: TransportConfig) { - super(); - if (!config.url) { - throw new Error('URL is required for HTTP transport'); - } - } - - async connect(): Promise { - this.client = axios.create({ - baseURL: this.config.url, - headers: { - 'Content-Type': 'application/json', - ...this.config.headers - } - }); - - // Test connection - try { - await this.client.get('/health'); - this.connected = true; - } catch (error) { - // If health endpoint doesn't exist, try a basic request - this.connected = true; - } - - return new HttpClientTransport(this.client); - } - - async disconnect(): Promise { - this.connected = false; - this.client = undefined; - } - - getType(): TransportType { - return 'http'; - } -} - -export class SSETransport extends EventEmitter implements MCPTransport { - private connected = false; - - constructor(private config: TransportConfig) { - super(); - if (!config.url) { - throw new Error('URL is required for SSE transport'); - } - } - - async connect(): Promise { - return new Promise((resolve, reject) => { - try { - // For Node.js environment, we'll use a simple HTTP-based approach - // In a real implementation, you'd use a proper SSE library like 'eventsource' - this.connected = true; - resolve(new SSEClientTransport(this.config.url!)); - } catch (error) { - reject(error); - } - }); - } - - async disconnect(): Promise { - this.connected = false; - } - - getType(): TransportType { - return 'sse'; - } -} - -// Custom HTTP Transport implementation -class HttpClientTransport extends EventEmitter implements Transport { - constructor(private client: AxiosInstance) { - super(); - } - - async start(): Promise { - // HTTP transport is connection-less, so we're always "started" - } - - async close(): Promise { - // Nothing to close for HTTP transport - } - - async send(message: any): Promise { - try { - const response = await this.client.post('/rpc', message); - return response.data; - } catch (error) { - throw new Error(`HTTP transport error: ${error}`); - } - } -} - -// Custom SSE Transport implementation -class SSEClientTransport extends EventEmitter implements Transport { - constructor(private url: string) { - super(); - } - - async start(): Promise { - // SSE transport is event-driven, so we're always "started" - } - - async close(): Promise { - // Nothing to close for basic SSE transport - } - - async send(message: any): Promise { - // For bidirectional communication over SSE, we typically use HTTP POST - // for sending messages and SSE for receiving - try { - const response = await axios.post(this.url.replace('/sse', '/rpc'), message, { - headers: { 'Content-Type': 'application/json' } - }); - return response.data; - } catch (error) { - throw new Error(`SSE transport error: ${error}`); - } - } -} - -export class StreamableHttpTransport extends EventEmitter implements MCPTransport { - private connected = false; - - constructor(private config: TransportConfig) { - super(); - if (!config.url) { - throw new Error('URL is required for streamable_http transport'); - } - } - - async connect(): Promise { - return new Promise((resolve, reject) => { - try { - this.connected = true; - resolve(new StreamableHttpClientTransport(this.config.url!, this.config.headers)); - } catch (error) { - reject(error); - } - }); - } - - async disconnect(): Promise { - this.connected = false; - } - - getType(): TransportType { - return 'streamable_http'; - } -} - -// Custom Streamable HTTP Transport implementation for GitHub Copilot MCP -class StreamableHttpClientTransport extends EventEmitter implements Transport { - constructor(private url: string, private headers?: Record) { - super(); - } - - async start(): Promise { - // Streamable HTTP transport is connection-less, so we're always "started" - } - - async close(): Promise { - // Nothing to close for streamable HTTP transport - } - - async send(message: any): Promise { - console.log('StreamableHttpTransport: SSE endpoints require persistent connections, not suitable for MCP request-response pattern'); - console.log('StreamableHttpTransport: Message that would be sent:', JSON.stringify(message)); - - // For now, return a mock response to indicate the transport type is not compatible - // with the MCP protocol's request-response pattern - throw new Error('StreamableHttpTransport: SSE endpoints are not compatible with MCP request-response pattern. GitHub Copilot MCP may require a different integration approach.'); - } -} - -export function createTransport(config: TransportConfig): MCPTransport { - switch (config.type) { - case 'stdio': - return new StdioTransport(config); - case 'http': - return new HttpTransport(config); - case 'sse': - return new SSETransport(config); - case 'streamable_http': - return new StreamableHttpTransport(config); - default: - throw new Error(`Unsupported transport type: ${config.type}`); - } -} \ No newline at end of file diff --git a/src/tools/bash.ts b/src/tools/bash.ts index 5ff180a..5331cb7 100644 --- a/src/tools/bash.ts +++ b/src/tools/bash.ts @@ -1,86 +1,63 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { ToolResult } from '../types/index.js'; -import { ConfirmationService } from '../utils/confirmation-service.js'; +import { exec } from "child_process"; +import { stat } from "fs/promises"; +import path from "path"; +import { promisify } from "util"; +import type { ToolResult } from "../types/index.js"; const execAsync = promisify(exec); export class BashTool { - private currentDirectory: string = process.cwd(); - private confirmationService = ConfirmationService.getInstance(); + private cwd: string; + constructor(initialCwd = process.cwd()) { + this.cwd = initialCwd; + } - async execute(command: string, timeout: number = 30000): Promise { + async execute(command: string, timeout = 30_000): Promise { try { - // Check if user has already accepted bash commands for this session - const sessionFlags = this.confirmationService.getSessionFlags(); - if (!sessionFlags.bashCommands && !sessionFlags.allOperations) { - // Request confirmation showing the command - const confirmationResult = await this.confirmationService.requestConfirmation({ - operation: 'Run bash command', - filename: command, - showVSCodeOpen: false, - content: `Command: ${command}\nWorking directory: ${this.currentDirectory}` - }, 'bash'); - - if (!confirmationResult.confirmed) { - return { - success: false, - error: confirmationResult.feedback || 'Command execution cancelled by user' - }; - } - } - - if (command.startsWith('cd ')) { - const newDir = command.substring(3).trim(); + if (command.startsWith("cd ")) { + const dir = command.substring(3).trim().replace(/^["']|["']$/g, ""); try { - process.chdir(newDir); - this.currentDirectory = process.cwd(); - return { - success: true, - output: `Changed directory to: ${this.currentDirectory}` - }; - } catch (error: any) { - return { - success: false, - error: `Cannot change directory: ${error.message}` - }; + const nextCwd = path.resolve(this.cwd, dir); + const info = await stat(nextCwd); + if (!info.isDirectory()) { + return { success: false, error: `Cannot change directory: ${nextCwd} is not a directory` }; + } + this.cwd = nextCwd; + return { success: true, output: `Changed directory to: ${this.cwd}` }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, error: `Cannot change directory: ${msg}` }; } } const { stdout, stderr } = await execAsync(command, { - cwd: this.currentDirectory, + cwd: this.cwd, timeout, - maxBuffer: 1024 * 1024 + maxBuffer: 10 * 1024 * 1024, + env: { ...process.env, FORCE_COLOR: "0" }, }); - const output = stdout + (stderr ? `\nSTDERR: ${stderr}` : ''); - + const output = stdout + (stderr ? `\nSTDERR: ${stderr}` : ""); return { success: true, - output: output.trim() || 'Command executed successfully (no output)' - }; - } catch (error: any) { - return { - success: false, - error: `Command failed: ${error.message}` + output: output.trim() || "Command executed successfully (no output)", }; + } catch (err: unknown) { + if (err && typeof err === "object" && "stdout" in err) { + const execErr = err as { stdout?: string; stderr?: string; message: string }; + const output = + (execErr.stdout || "") + (execErr.stderr ? `\nSTDERR: ${execErr.stderr}` : ""); + if (output.trim()) { + return { success: false, error: output.trim() }; + } + } + const msg = err instanceof Error ? err.message : String(err); + return { success: false, error: `Command failed: ${msg}` }; } } - getCurrentDirectory(): string { - return this.currentDirectory; + getCwd(): string { + return this.cwd; } - - async listFiles(directory: string = '.'): Promise { - return this.execute(`ls -la ${directory}`); - } - - async findFiles(pattern: string, directory: string = '.'): Promise { - return this.execute(`find ${directory} -name "${pattern}" -type f`); - } - - async grep(pattern: string, files: string = '.'): Promise { - return this.execute(`grep -r "${pattern}" ${files}`); - } -} \ No newline at end of file +} diff --git a/src/tools/confirmation-tool.ts b/src/tools/confirmation-tool.ts deleted file mode 100644 index 8039a35..0000000 --- a/src/tools/confirmation-tool.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ToolResult } from '../types/index.js'; -import { ConfirmationService, ConfirmationOptions } from '../utils/confirmation-service.js'; - -export interface ConfirmationRequest { - operation: string; - filename: string; - description?: string; - showVSCodeOpen?: boolean; - autoAccept?: boolean; -} - -export class ConfirmationTool { - private confirmationService: ConfirmationService; - - constructor() { - this.confirmationService = ConfirmationService.getInstance(); - } - - async requestConfirmation(request: ConfirmationRequest): Promise { - try { - // If autoAccept is true, skip the confirmation dialog - if (request.autoAccept) { - return { - success: true, - output: `Auto-accepted: ${request.operation}(${request.filename})${request.description ? ` - ${request.description}` : ''}` - }; - } - - const options: ConfirmationOptions = { - operation: request.operation, - filename: request.filename, - showVSCodeOpen: request.showVSCodeOpen || false - }; - - // Determine operation type based on operation name - const operationType = request.operation.toLowerCase().includes('bash') ? 'bash' : 'file'; - const result = await this.confirmationService.requestConfirmation(options, operationType); - - if (result.confirmed) { - return { - success: true, - output: `User confirmed: ${request.operation}(${request.filename})${request.description ? ` - ${request.description}` : ''}${result.dontAskAgain ? ' (Don\'t ask again enabled)' : ''}` - }; - } else { - return { - success: false, - error: result.feedback || `User rejected: ${request.operation}(${request.filename})` - }; - } - } catch (error: any) { - return { - success: false, - error: `Confirmation error: ${error.message}` - }; - } - } - - async checkSessionAcceptance(): Promise { - try { - const sessionFlags = this.confirmationService.getSessionFlags(); - // Return structured data without JSON output to avoid displaying raw JSON - return { - success: true, - data: { - fileOperationsAccepted: sessionFlags.fileOperations, - bashCommandsAccepted: sessionFlags.bashCommands, - allOperationsAccepted: sessionFlags.allOperations, - hasAnyAcceptance: sessionFlags.fileOperations || sessionFlags.bashCommands || sessionFlags.allOperations - } - }; - } catch (error: any) { - return { - success: false, - error: `Error checking session acceptance: ${error.message}` - }; - } - } - - resetSession(): void { - this.confirmationService.resetSession(); - } - - isPending(): boolean { - return this.confirmationService.isPending(); - } -} \ No newline at end of file diff --git a/src/tools/file.ts b/src/tools/file.ts new file mode 100644 index 0000000..a7bec3e --- /dev/null +++ b/src/tools/file.ts @@ -0,0 +1,119 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; +import { dirname, resolve, isAbsolute } from "path"; +import { createTwoFilesPatch } from "diff"; + +export interface FileDiff { + filePath: string; + additions: number; + removals: number; + patch: string; + isNew: boolean; +} + +export interface FileResult { + success: boolean; + output: string; + diff?: FileDiff; +} + +function resolvePath(filePath: string, cwd: string): string { + return isAbsolute(filePath) ? filePath : resolve(cwd, filePath); +} + +function computeDiff(filePath: string, before: string, after: string): FileDiff { + const patch = createTwoFilesPatch(filePath, filePath, before, after, "", "", { + context: 3, + }); + + let additions = 0; + let removals = 0; + for (const line of patch.split("\n")) { + if (line.startsWith("+") && !line.startsWith("+++")) additions++; + if (line.startsWith("-") && !line.startsWith("---")) removals++; + } + + return { filePath, additions, removals, patch, isNew: before === "" }; +} + +export function readFile(filePath: string, cwd: string, startLine?: number, endLine?: number): FileResult { + try { + const full = resolvePath(filePath, cwd); + if (!existsSync(full)) { + return { success: false, output: `File not found: ${filePath}` }; + } + const content = readFileSync(full, "utf-8"); + const lines = content.split("\n"); + const totalLines = lines.length; + + const start = Math.max(0, (startLine ?? 1) - 1); + const end = Math.min(totalLines, endLine ?? totalLines); + const slice = lines.slice(start, end); + + const numbered = slice.map((line, i) => `${start + i + 1} | ${line}`).join("\n"); + const header = `[${filePath}: lines ${start + 1}-${end} of ${totalLines}]`; + return { success: true, output: `${header}\n${numbered}` }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, output: `Failed to read file: ${msg}` }; + } +} + +export function writeFile(filePath: string, content: string, cwd: string): FileResult { + try { + const full = resolvePath(filePath, cwd); + const before = existsSync(full) ? readFileSync(full, "utf-8") : ""; + const dir = dirname(full); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(full, content, "utf-8"); + + const diff = computeDiff(filePath, before, content); + const verb = before === "" ? "Created" : "Updated"; + return { + success: true, + output: `${verb} ${filePath} (+${diff.additions} -${diff.removals})`, + diff, + }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, output: `Failed to write file: ${msg}` }; + } +} + +export function editFile( + filePath: string, + oldString: string, + newString: string, + cwd: string, +): FileResult { + try { + const full = resolvePath(filePath, cwd); + if (!existsSync(full)) { + return { success: false, output: `File not found: ${filePath}` }; + } + const before = readFileSync(full, "utf-8"); + const count = before.split(oldString).length - 1; + + if (count === 0) { + return { success: false, output: `old_string not found in ${filePath}` }; + } + if (count > 1) { + return { + success: false, + output: `old_string is not unique in ${filePath} (${count} occurrences). Include more surrounding context to make it unique.`, + }; + } + + const after = before.replace(oldString, newString); + writeFileSync(full, after, "utf-8"); + + const diff = computeDiff(filePath, before, after); + return { + success: true, + output: `Edited ${filePath} (+${diff.additions} -${diff.removals})`, + diff, + }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { success: false, output: `Failed to edit file: ${msg}` }; + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts deleted file mode 100644 index 5401ef6..0000000 --- a/src/tools/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { BashTool } from "./bash.js"; -export { TextEditorTool } from "./text-editor.js"; -export { MorphEditorTool } from "./morph-editor.js"; -export { TodoTool } from "./todo-tool.js"; -export { ConfirmationTool } from "./confirmation-tool.js"; -export { SearchTool } from "./search.js"; diff --git a/src/tools/morph-editor.ts b/src/tools/morph-editor.ts deleted file mode 100644 index 561a1b2..0000000 --- a/src/tools/morph-editor.ts +++ /dev/null @@ -1,393 +0,0 @@ -import fs from "fs-extra"; -import * as path from "path"; -import axios from "axios"; -import { ToolResult } from "../types/index.js"; -import { ConfirmationService } from "../utils/confirmation-service.js"; - -export class MorphEditorTool { - private confirmationService = ConfirmationService.getInstance(); - private morphApiKey: string; - private morphBaseUrl: string = "https://api.morphllm.com/v1"; - - constructor(apiKey?: string) { - this.morphApiKey = apiKey || process.env.MORPH_API_KEY || ""; - if (!this.morphApiKey) { - console.warn("MORPH_API_KEY not found. Morph editor functionality will be limited."); - } - } - - /** - * Use this tool to make an edit to an existing file. - * - * This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write. - * When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines. - * - * For example: - * - * // ... existing code ... - * FIRST_EDIT - * // ... existing code ... - * SECOND_EDIT - * // ... existing code ... - * THIRD_EDIT - * // ... existing code ... - * - * You should still bias towards repeating as few lines of the original file as possible to convey the change. - * But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity. - * DO NOT omit spans of pre-existing code (or comments) without using the // ... existing code ... comment to indicate its absence. If you omit the existing code comment, the model may inadvertently delete these lines. - * If you plan on deleting a section, you must provide context before and after to delete it. If the initial code is ```code \n Block 1 \n Block 2 \n Block 3 \n code```, and you want to remove Block 2, you would output ```// ... existing code ... \n Block 1 \n Block 3 \n // ... existing code ...```. - * Make sure it is clear what the edit should be, and where it should be applied. - * Make edits to a file in a single edit_file call instead of multiple edit_file calls to the same file. The apply model can handle many distinct edits at once. - */ - async editFile( - targetFile: string, - instructions: string, - codeEdit: string - ): Promise { - try { - const resolvedPath = path.resolve(targetFile); - - if (!(await fs.pathExists(resolvedPath))) { - return { - success: false, - error: `File not found: ${targetFile}`, - }; - } - - if (!this.morphApiKey) { - return { - success: false, - error: "MORPH_API_KEY not configured. Please set your Morph API key.", - }; - } - - // Read the initial code - const initialCode = await fs.readFile(resolvedPath, "utf-8"); - - // Check user confirmation before proceeding - const sessionFlags = this.confirmationService.getSessionFlags(); - if (!sessionFlags.fileOperations && !sessionFlags.allOperations) { - const confirmationResult = await this.confirmationService.requestConfirmation( - { - operation: "Edit file with Morph Fast Apply", - filename: targetFile, - showVSCodeOpen: false, - content: `Instructions: ${instructions}\n\nEdit:\n${codeEdit}`, - }, - "file" - ); - - if (!confirmationResult.confirmed) { - return { - success: false, - error: confirmationResult.feedback || "File edit cancelled by user", - }; - } - } - - // Call Morph Fast Apply API - const mergedCode = await this.callMorphApply(instructions, initialCode, codeEdit); - - // Write the merged code back to file - await fs.writeFile(resolvedPath, mergedCode, "utf-8"); - - // Generate diff for display - const oldLines = initialCode.split("\n"); - const newLines = mergedCode.split("\n"); - const diff = this.generateDiff(oldLines, newLines, targetFile); - - return { - success: true, - output: diff, - }; - } catch (error: any) { - return { - success: false, - error: `Error editing ${targetFile} with Morph: ${error.message}`, - }; - } - } - - private async callMorphApply( - instructions: string, - initialCode: string, - editSnippet: string - ): Promise { - try { - const response = await axios.post(`${this.morphBaseUrl}/chat/completions`, { - model: "morph-v3-large", - messages: [ - { - role: "user", - content: `${instructions}\n${initialCode}\n${editSnippet}`, - }, - ], - }, { - headers: { - "Authorization": `Bearer ${this.morphApiKey}`, - "Content-Type": "application/json", - }, - }); - - if (!response.data.choices || !response.data.choices[0] || !response.data.choices[0].message) { - throw new Error("Invalid response format from Morph API"); - } - - return response.data.choices[0].message.content; - } catch (error: any) { - if (error.response) { - throw new Error(`Morph API error (${error.response.status}): ${error.response.data}`); - } - throw error; - } - } - - private generateDiff( - oldLines: string[], - newLines: string[], - filePath: string - ): string { - const CONTEXT_LINES = 3; - - const changes: Array<{ - oldStart: number; - oldEnd: number; - newStart: number; - newEnd: number; - }> = []; - - let i = 0, j = 0; - - while (i < oldLines.length || j < newLines.length) { - while (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) { - i++; - j++; - } - - if (i < oldLines.length || j < newLines.length) { - const changeStart = { old: i, new: j }; - - let oldEnd = i; - let newEnd = j; - - while (oldEnd < oldLines.length || newEnd < newLines.length) { - let matchFound = false; - let matchLength = 0; - - for (let k = 0; k < Math.min(2, oldLines.length - oldEnd, newLines.length - newEnd); k++) { - if (oldEnd + k < oldLines.length && - newEnd + k < newLines.length && - oldLines[oldEnd + k] === newLines[newEnd + k]) { - matchLength++; - } else { - break; - } - } - - if (matchLength >= 2 || (oldEnd >= oldLines.length && newEnd >= newLines.length)) { - matchFound = true; - } - - if (matchFound) { - break; - } - - if (oldEnd < oldLines.length) oldEnd++; - if (newEnd < newLines.length) newEnd++; - } - - changes.push({ - oldStart: changeStart.old, - oldEnd: oldEnd, - newStart: changeStart.new, - newEnd: newEnd - }); - - i = oldEnd; - j = newEnd; - } - } - - const hunks: Array<{ - oldStart: number; - oldCount: number; - newStart: number; - newCount: number; - lines: Array<{ type: '+' | '-' | ' '; content: string }>; - }> = []; - - let accumulatedOffset = 0; - - for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) { - const change = changes[changeIdx]; - - let contextStart = Math.max(0, change.oldStart - CONTEXT_LINES); - let contextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES); - - if (hunks.length > 0) { - const lastHunk = hunks[hunks.length - 1]; - const lastHunkEnd = lastHunk.oldStart + lastHunk.oldCount; - - if (lastHunkEnd >= contextStart) { - const oldHunkEnd = lastHunk.oldStart + lastHunk.oldCount; - const newContextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES); - - for (let idx = oldHunkEnd; idx < change.oldStart; idx++) { - lastHunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - for (let idx = change.oldStart; idx < change.oldEnd; idx++) { - lastHunk.lines.push({ type: '-', content: oldLines[idx] }); - } - for (let idx = change.newStart; idx < change.newEnd; idx++) { - lastHunk.lines.push({ type: '+', content: newLines[idx] }); - } - - for (let idx = change.oldEnd; idx < newContextEnd && idx < oldLines.length; idx++) { - lastHunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - lastHunk.oldCount = newContextEnd - lastHunk.oldStart; - lastHunk.newCount = lastHunk.oldCount + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart); - - continue; - } - } - - const hunk: typeof hunks[0] = { - oldStart: contextStart + 1, - oldCount: contextEnd - contextStart, - newStart: contextStart + 1 + accumulatedOffset, - newCount: contextEnd - contextStart + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart), - lines: [] - }; - - for (let idx = contextStart; idx < change.oldStart; idx++) { - hunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - for (let idx = change.oldStart; idx < change.oldEnd; idx++) { - hunk.lines.push({ type: '-', content: oldLines[idx] }); - } - - for (let idx = change.newStart; idx < change.newEnd; idx++) { - hunk.lines.push({ type: '+', content: newLines[idx] }); - } - - for (let idx = change.oldEnd; idx < contextEnd && idx < oldLines.length; idx++) { - hunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - hunks.push(hunk); - - accumulatedOffset += (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart); - } - - let addedLines = 0; - let removedLines = 0; - - for (const hunk of hunks) { - for (const line of hunk.lines) { - if (line.type === '+') addedLines++; - if (line.type === '-') removedLines++; - } - } - - let summary = `Updated ${filePath} with Morph Fast Apply`; - if (addedLines > 0 && removedLines > 0) { - summary += ` - ${addedLines} addition${ - addedLines !== 1 ? "s" : "" - } and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`; - } else if (addedLines > 0) { - summary += ` - ${addedLines} addition${addedLines !== 1 ? "s" : ""}`; - } else if (removedLines > 0) { - summary += ` - ${removedLines} removal${ - removedLines !== 1 ? "s" : "" - }`; - } else if (changes.length === 0) { - return `No changes applied to ${filePath}`; - } - - let diff = summary + "\n"; - diff += `--- a/${filePath}\n`; - diff += `+++ b/${filePath}\n`; - - for (const hunk of hunks) { - diff += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`; - - for (const line of hunk.lines) { - diff += `${line.type}${line.content}\n`; - } - } - - return diff.trim(); - } - - async view( - filePath: string, - viewRange?: [number, number] - ): Promise { - try { - const resolvedPath = path.resolve(filePath); - - if (await fs.pathExists(resolvedPath)) { - const stats = await fs.stat(resolvedPath); - - if (stats.isDirectory()) { - const files = await fs.readdir(resolvedPath); - return { - success: true, - output: `Directory contents of ${filePath}:\n${files.join("\n")}`, - }; - } - - const content = await fs.readFile(resolvedPath, "utf-8"); - const lines = content.split("\n"); - - if (viewRange) { - const [start, end] = viewRange; - const selectedLines = lines.slice(start - 1, end); - const numberedLines = selectedLines - .map((line, idx) => `${start + idx}: ${line}`) - .join("\n"); - - return { - success: true, - output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`, - }; - } - - const totalLines = lines.length; - const displayLines = totalLines > 10 ? lines.slice(0, 10) : lines; - const numberedLines = displayLines - .map((line, idx) => `${idx + 1}: ${line}`) - .join("\n"); - const additionalLinesMessage = - totalLines > 10 ? `\n... +${totalLines - 10} lines` : ""; - - return { - success: true, - output: `Contents of ${filePath}:\n${numberedLines}${additionalLinesMessage}`, - }; - } else { - return { - success: false, - error: `File or directory not found: ${filePath}`, - }; - } - } catch (error: any) { - return { - success: false, - error: `Error viewing ${filePath}: ${error.message}`, - }; - } - } - - setApiKey(apiKey: string): void { - this.morphApiKey = apiKey; - } - - getApiKey(): string { - return this.morphApiKey; - } -} diff --git a/src/tools/search.ts b/src/tools/search.ts deleted file mode 100644 index 3303fbf..0000000 --- a/src/tools/search.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { spawn } from "child_process"; -import { ToolResult } from "../types/index.js"; -import { ConfirmationService } from "../utils/confirmation-service.js"; -import fs from "fs-extra"; -import * as path from "path"; - -export interface SearchResult { - file: string; - line: number; - column: number; - text: string; - match: string; -} - -export interface FileSearchResult { - path: string; - name: string; - score: number; -} - -export interface UnifiedSearchResult { - type: "text" | "file"; - file: string; - line?: number; - column?: number; - text?: string; - match?: string; - score?: number; -} - -export class SearchTool { - private confirmationService = ConfirmationService.getInstance(); - private currentDirectory: string = process.cwd(); - - /** - * Unified search method that can search for text content or find files - */ - async search( - query: string, - options: { - searchType?: "text" | "files" | "both"; - includePattern?: string; - excludePattern?: string; - caseSensitive?: boolean; - wholeWord?: boolean; - regex?: boolean; - maxResults?: number; - fileTypes?: string[]; - excludeFiles?: string[]; - includeHidden?: boolean; - } = {} - ): Promise { - try { - const searchType = options.searchType || "both"; - const results: UnifiedSearchResult[] = []; - - // Search for text content if requested - if (searchType === "text" || searchType === "both") { - const textResults = await this.executeRipgrep(query, options); - results.push( - ...textResults.map((r) => ({ - type: "text" as const, - file: r.file, - line: r.line, - column: r.column, - text: r.text, - match: r.match, - })) - ); - } - - // Search for files if requested - if (searchType === "files" || searchType === "both") { - const fileResults = await this.findFilesByPattern(query, options); - results.push( - ...fileResults.map((r) => ({ - type: "file" as const, - file: r.path, - score: r.score, - })) - ); - } - - if (results.length === 0) { - return { - success: true, - output: `No results found for "${query}"`, - }; - } - - const formattedOutput = this.formatUnifiedResults( - results, - query, - searchType - ); - - return { - success: true, - output: formattedOutput, - }; - } catch (error: any) { - return { - success: false, - error: `Search error: ${error.message}`, - }; - } - } - - /** - * Execute ripgrep command with specified options - */ - private async executeRipgrep( - query: string, - options: { - includePattern?: string; - excludePattern?: string; - caseSensitive?: boolean; - wholeWord?: boolean; - regex?: boolean; - maxResults?: number; - fileTypes?: string[]; - excludeFiles?: string[]; - } - ): Promise { - return new Promise((resolve, reject) => { - const args = [ - "--json", - "--with-filename", - "--line-number", - "--column", - "--no-heading", - "--color=never", - ]; - - // Add case sensitivity - if (!options.caseSensitive) { - args.push("--ignore-case"); - } - - // Add whole word matching - if (options.wholeWord) { - args.push("--word-regexp"); - } - - // Add regex mode - if (!options.regex) { - args.push("--fixed-strings"); - } - - // Add max results limit - if (options.maxResults) { - args.push("--max-count", options.maxResults.toString()); - } - - // Add file type filters - if (options.fileTypes) { - options.fileTypes.forEach((type) => { - args.push("--type", type); - }); - } - - // Add include pattern - if (options.includePattern) { - args.push("--glob", options.includePattern); - } - - // Add exclude pattern - if (options.excludePattern) { - args.push("--glob", `!${options.excludePattern}`); - } - - // Add exclude files - if (options.excludeFiles) { - options.excludeFiles.forEach((file) => { - args.push("--glob", `!${file}`); - }); - } - - // Respect gitignore and common ignore patterns - args.push( - "--no-require-git", - "--follow", - "--glob", - "!.git/**", - "--glob", - "!node_modules/**", - "--glob", - "!.DS_Store", - "--glob", - "!*.log" - ); - - // Add query and search directory - args.push(query, this.currentDirectory); - - const rg = spawn("rg", args); - let output = ""; - let errorOutput = ""; - - rg.stdout.on("data", (data) => { - output += data.toString(); - }); - - rg.stderr.on("data", (data) => { - errorOutput += data.toString(); - }); - - rg.on("close", (code) => { - if (code === 0 || code === 1) { - // 0 = found, 1 = not found - const results = this.parseRipgrepOutput(output); - resolve(results); - } else { - reject(new Error(`Ripgrep failed with code ${code}: ${errorOutput}`)); - } - }); - - rg.on("error", (error) => { - reject(error); - }); - }); - } - - /** - * Parse ripgrep JSON output into SearchResult objects - */ - private parseRipgrepOutput(output: string): SearchResult[] { - const results: SearchResult[] = []; - const lines = output - .trim() - .split("\n") - .filter((line) => line.length > 0); - - for (const line of lines) { - try { - const parsed = JSON.parse(line); - if (parsed.type === "match") { - const data = parsed.data; - results.push({ - file: data.path.text, - line: data.line_number, - column: data.submatches[0]?.start || 0, - text: data.lines.text.trim(), - match: data.submatches[0]?.match?.text || "", - }); - } - } catch (e) { - // Skip invalid JSON lines - continue; - } - } - - return results; - } - - /** - * Find files by pattern using a simple file walking approach - */ - private async findFilesByPattern( - pattern: string, - options: { - maxResults?: number; - includeHidden?: boolean; - excludePattern?: string; - } - ): Promise { - const files: FileSearchResult[] = []; - const maxResults = options.maxResults || 50; - const searchPattern = pattern.toLowerCase(); - - const walkDir = async (dir: string, depth: number = 0): Promise => { - if (depth > 10 || files.length >= maxResults) return; // Prevent infinite recursion and limit results - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - if (files.length >= maxResults) break; - - const fullPath = path.join(dir, entry.name); - const relativePath = path.relative(this.currentDirectory, fullPath); - - // Skip hidden files unless explicitly included - if (!options.includeHidden && entry.name.startsWith(".")) { - continue; - } - - // Skip common directories - if ( - entry.isDirectory() && - [ - "node_modules", - ".git", - ".svn", - ".hg", - "dist", - "build", - ".next", - ".cache", - ].includes(entry.name) - ) { - continue; - } - - // Apply exclude pattern - if ( - options.excludePattern && - relativePath.includes(options.excludePattern) - ) { - continue; - } - - if (entry.isFile()) { - const score = this.calculateFileScore( - entry.name, - relativePath, - searchPattern - ); - if (score > 0) { - files.push({ - path: relativePath, - name: entry.name, - score, - }); - } - } else if (entry.isDirectory()) { - await walkDir(fullPath, depth + 1); - } - } - } catch (error) { - // Skip directories we can't read - } - }; - - await walkDir(this.currentDirectory); - - // Sort by score (descending) and return top results - return files.sort((a, b) => b.score - a.score).slice(0, maxResults); - } - - /** - * Calculate fuzzy match score for file names - */ - private calculateFileScore( - fileName: string, - filePath: string, - pattern: string - ): number { - const lowerFileName = fileName.toLowerCase(); - const lowerFilePath = filePath.toLowerCase(); - - // Exact matches get highest score - if (lowerFileName === pattern) return 100; - if (lowerFileName.includes(pattern)) return 80; - - // Path matches get medium score - if (lowerFilePath.includes(pattern)) return 60; - - // Fuzzy matching - check if all characters of pattern exist in order - let patternIndex = 0; - for ( - let i = 0; - i < lowerFileName.length && patternIndex < pattern.length; - i++ - ) { - if (lowerFileName[i] === pattern[patternIndex]) { - patternIndex++; - } - } - - if (patternIndex === pattern.length) { - // All characters found in order - score based on how close they are - return Math.max(10, 40 - (fileName.length - pattern.length)); - } - - return 0; - } - - /** - * Format unified search results for display - */ - private formatUnifiedResults( - results: UnifiedSearchResult[], - query: string, - searchType: string - ): string { - if (results.length === 0) { - return `No results found for "${query}"`; - } - - let output = `Search results for "${query}":\n`; - - // Separate text and file results - const textResults = results.filter((r) => r.type === "text"); - const fileResults = results.filter((r) => r.type === "file"); - - // Show all unique files (from both text matches and file matches) - const allFiles = new Set(); - - // Add files from text results - textResults.forEach((result) => { - allFiles.add(result.file); - }); - - // Add files from file search results - fileResults.forEach((result) => { - allFiles.add(result.file); - }); - - const fileList = Array.from(allFiles); - const displayLimit = 8; - - // Show files in compact format - fileList.slice(0, displayLimit).forEach((file) => { - // Count matches in this file for text results - const matchCount = textResults.filter((r) => r.file === file).length; - const matchIndicator = matchCount > 0 ? ` (${matchCount} matches)` : ""; - output += ` ${file}${matchIndicator}\n`; - }); - - // Show "+X more" if there are additional results - if (fileList.length > displayLimit) { - const remaining = fileList.length - displayLimit; - output += ` ... +${remaining} more\n`; - } - - return output.trim(); - } - - /** - * Update current working directory - */ - setCurrentDirectory(directory: string): void { - this.currentDirectory = directory; - } - - /** - * Get current working directory - */ - getCurrentDirectory(): string { - return this.currentDirectory; - } -} diff --git a/src/tools/text-editor.ts b/src/tools/text-editor.ts deleted file mode 100644 index 02e101f..0000000 --- a/src/tools/text-editor.ts +++ /dev/null @@ -1,704 +0,0 @@ -import fs from "fs-extra"; -import * as path from "path"; -import { writeFile as writeFilePromise } from "fs/promises"; -import { ToolResult, EditorCommand } from "../types/index.js"; -import { ConfirmationService } from "../utils/confirmation-service.js"; - -export class TextEditorTool { - private editHistory: EditorCommand[] = []; - private confirmationService = ConfirmationService.getInstance(); - - async view( - filePath: string, - viewRange?: [number, number] - ): Promise { - try { - const resolvedPath = path.resolve(filePath); - - if (await fs.pathExists(resolvedPath)) { - const stats = await fs.stat(resolvedPath); - - if (stats.isDirectory()) { - const files = await fs.readdir(resolvedPath); - return { - success: true, - output: `Directory contents of ${filePath}:\n${files.join("\n")}`, - }; - } - - const content = await fs.readFile(resolvedPath, "utf-8"); - const lines = content.split("\n"); - - if (viewRange) { - const [start, end] = viewRange; - const selectedLines = lines.slice(start - 1, end); - const numberedLines = selectedLines - .map((line, idx) => `${start + idx}: ${line}`) - .join("\n"); - - return { - success: true, - output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`, - }; - } - - const totalLines = lines.length; - const displayLines = totalLines > 10 ? lines.slice(0, 10) : lines; - const numberedLines = displayLines - .map((line, idx) => `${idx + 1}: ${line}`) - .join("\n"); - const additionalLinesMessage = - totalLines > 10 ? `\n... +${totalLines - 10} lines` : ""; - - return { - success: true, - output: `Contents of ${filePath}:\n${numberedLines}${additionalLinesMessage}`, - }; - } else { - return { - success: false, - error: `File or directory not found: ${filePath}`, - }; - } - } catch (error: any) { - return { - success: false, - error: `Error viewing ${filePath}: ${error.message}`, - }; - } - } - - async strReplace( - filePath: string, - oldStr: string, - newStr: string, - replaceAll: boolean = false - ): Promise { - try { - const resolvedPath = path.resolve(filePath); - - if (!(await fs.pathExists(resolvedPath))) { - return { - success: false, - error: `File not found: ${filePath}`, - }; - } - - const content = await fs.readFile(resolvedPath, "utf-8"); - - if (!content.includes(oldStr)) { - if (oldStr.includes('\n')) { - const fuzzyResult = this.findFuzzyMatch(content, oldStr); - if (fuzzyResult) { - oldStr = fuzzyResult; - } else { - return { - success: false, - error: `String not found in file. For multi-line replacements, consider using line-based editing.`, - }; - } - } else { - return { - success: false, - error: `String not found in file: "${oldStr}"`, - }; - } - } - - const occurrences = (content.match(new RegExp(oldStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; - - const sessionFlags = this.confirmationService.getSessionFlags(); - if (!sessionFlags.fileOperations && !sessionFlags.allOperations) { - const previewContent = replaceAll - ? content.split(oldStr).join(newStr) - : content.replace(oldStr, newStr); - const oldLines = content.split("\n"); - const newLines = previewContent.split("\n"); - const diffContent = this.generateDiff(oldLines, newLines, filePath); - - const confirmationResult = - await this.confirmationService.requestConfirmation( - { - operation: `Edit file${replaceAll && occurrences > 1 ? ` (${occurrences} occurrences)` : ''}`, - filename: filePath, - showVSCodeOpen: false, - content: diffContent, - }, - "file" - ); - - if (!confirmationResult.confirmed) { - return { - success: false, - error: confirmationResult.feedback || "File edit cancelled by user", - }; - } - } - - const newContent = replaceAll - ? content.split(oldStr).join(newStr) - : content.replace(oldStr, newStr); - await writeFilePromise(resolvedPath, newContent, "utf-8"); - - this.editHistory.push({ - command: "str_replace", - path: filePath, - old_str: oldStr, - new_str: newStr, - }); - - const oldLines = content.split("\n"); - const newLines = newContent.split("\n"); - const diff = this.generateDiff(oldLines, newLines, filePath); - - return { - success: true, - output: diff, - }; - } catch (error: any) { - return { - success: false, - error: `Error replacing text in ${filePath}: ${error.message}`, - }; - } - } - - async create(filePath: string, content: string): Promise { - try { - const resolvedPath = path.resolve(filePath); - - // Check if user has already accepted file operations for this session - const sessionFlags = this.confirmationService.getSessionFlags(); - if (!sessionFlags.fileOperations && !sessionFlags.allOperations) { - // Create a diff-style preview for file creation - const contentLines = content.split("\n"); - const diffContent = [ - `Created ${filePath}`, - `--- /dev/null`, - `+++ b/${filePath}`, - `@@ -0,0 +1,${contentLines.length} @@`, - ...contentLines.map((line) => `+${line}`), - ].join("\n"); - - const confirmationResult = - await this.confirmationService.requestConfirmation( - { - operation: "Write", - filename: filePath, - showVSCodeOpen: false, - content: diffContent, - }, - "file" - ); - - if (!confirmationResult.confirmed) { - return { - success: false, - error: - confirmationResult.feedback || "File creation cancelled by user", - }; - } - } - - const dir = path.dirname(resolvedPath); - await fs.ensureDir(dir); - await writeFilePromise(resolvedPath, content, "utf-8"); - - this.editHistory.push({ - command: "create", - path: filePath, - content, - }); - - // Generate diff output using the same method as str_replace - const oldLines: string[] = []; // Empty for new files - const newLines = content.split("\n"); - const diff = this.generateDiff(oldLines, newLines, filePath); - - return { - success: true, - output: diff, - }; - } catch (error: any) { - return { - success: false, - error: `Error creating ${filePath}: ${error.message}`, - }; - } - } - - async replaceLines( - filePath: string, - startLine: number, - endLine: number, - newContent: string - ): Promise { - try { - const resolvedPath = path.resolve(filePath); - - if (!(await fs.pathExists(resolvedPath))) { - return { - success: false, - error: `File not found: ${filePath}`, - }; - } - - const fileContent = await fs.readFile(resolvedPath, "utf-8"); - const lines = fileContent.split("\n"); - - if (startLine < 1 || startLine > lines.length) { - return { - success: false, - error: `Invalid start line: ${startLine}. File has ${lines.length} lines.`, - }; - } - - if (endLine < startLine || endLine > lines.length) { - return { - success: false, - error: `Invalid end line: ${endLine}. Must be between ${startLine} and ${lines.length}.`, - }; - } - - const sessionFlags = this.confirmationService.getSessionFlags(); - if (!sessionFlags.fileOperations && !sessionFlags.allOperations) { - const newLines = [...lines]; - const replacementLines = newContent.split("\n"); - newLines.splice(startLine - 1, endLine - startLine + 1, ...replacementLines); - - const diffContent = this.generateDiff(lines, newLines, filePath); - - const confirmationResult = - await this.confirmationService.requestConfirmation( - { - operation: `Replace lines ${startLine}-${endLine}`, - filename: filePath, - showVSCodeOpen: false, - content: diffContent, - }, - "file" - ); - - if (!confirmationResult.confirmed) { - return { - success: false, - error: confirmationResult.feedback || "Line replacement cancelled by user", - }; - } - } - - const replacementLines = newContent.split("\n"); - lines.splice(startLine - 1, endLine - startLine + 1, ...replacementLines); - const newFileContent = lines.join("\n"); - - await writeFilePromise(resolvedPath, newFileContent, "utf-8"); - - this.editHistory.push({ - command: "str_replace", - path: filePath, - old_str: `lines ${startLine}-${endLine}`, - new_str: newContent, - }); - - const oldLines = fileContent.split("\n"); - const diff = this.generateDiff(oldLines, lines, filePath); - - return { - success: true, - output: diff, - }; - } catch (error: any) { - return { - success: false, - error: `Error replacing lines in ${filePath}: ${error.message}`, - }; - } - } - - async insert( - filePath: string, - insertLine: number, - content: string - ): Promise { - try { - const resolvedPath = path.resolve(filePath); - - if (!(await fs.pathExists(resolvedPath))) { - return { - success: false, - error: `File not found: ${filePath}`, - }; - } - - const fileContent = await fs.readFile(resolvedPath, "utf-8"); - const lines = fileContent.split("\n"); - - lines.splice(insertLine - 1, 0, content); - const newContent = lines.join("\n"); - - await writeFilePromise(resolvedPath, newContent, "utf-8"); - - this.editHistory.push({ - command: "insert", - path: filePath, - insert_line: insertLine, - content, - }); - - return { - success: true, - output: `Successfully inserted content at line ${insertLine} in ${filePath}`, - }; - } catch (error: any) { - return { - success: false, - error: `Error inserting content in ${filePath}: ${error.message}`, - }; - } - } - - async undoEdit(): Promise { - if (this.editHistory.length === 0) { - return { - success: false, - error: "No edits to undo", - }; - } - - const lastEdit = this.editHistory.pop()!; - - try { - switch (lastEdit.command) { - case "str_replace": - if (lastEdit.path && lastEdit.old_str && lastEdit.new_str) { - const content = await fs.readFile(lastEdit.path, "utf-8"); - const revertedContent = content.replace( - lastEdit.new_str, - lastEdit.old_str - ); - await writeFilePromise(lastEdit.path, revertedContent, "utf-8"); - } - break; - - case "create": - if (lastEdit.path) { - await fs.remove(lastEdit.path); - } - break; - - case "insert": - if (lastEdit.path && lastEdit.insert_line) { - const content = await fs.readFile(lastEdit.path, "utf-8"); - const lines = content.split("\n"); - lines.splice(lastEdit.insert_line - 1, 1); - await writeFilePromise(lastEdit.path, lines.join("\n"), "utf-8"); - } - break; - } - - return { - success: true, - output: `Successfully undid ${lastEdit.command} operation`, - }; - } catch (error: any) { - return { - success: false, - error: `Error undoing edit: ${error.message}`, - }; - } - } - - private findFuzzyMatch(content: string, searchStr: string): string | null { - const functionMatch = searchStr.match(/function\s+(\w+)/); - if (!functionMatch) return null; - - const functionName = functionMatch[1]; - const contentLines = content.split('\n'); - - let functionStart = -1; - for (let i = 0; i < contentLines.length; i++) { - if (contentLines[i].includes(`function ${functionName}`) && contentLines[i].includes('{')) { - functionStart = i; - break; - } - } - - if (functionStart === -1) return null; - - let braceCount = 0; - let functionEnd = functionStart; - - for (let i = functionStart; i < contentLines.length; i++) { - const line = contentLines[i]; - for (const char of line) { - if (char === '{') braceCount++; - if (char === '}') braceCount--; - } - - if (braceCount === 0 && i > functionStart) { - functionEnd = i; - break; - } - } - - const actualFunction = contentLines.slice(functionStart, functionEnd + 1).join('\n'); - - const searchNormalized = this.normalizeForComparison(searchStr); - const actualNormalized = this.normalizeForComparison(actualFunction); - - if (this.isSimilarStructure(searchNormalized, actualNormalized)) { - return actualFunction; - } - - return null; - } - - private normalizeForComparison(str: string): string { - return str - .replace(/["'`]/g, '"') - .replace(/\s+/g, ' ') - .replace(/{\s+/g, '{ ') - .replace(/\s+}/g, ' }') - .replace(/;\s*/g, ';') - .trim(); - } - - private isSimilarStructure(search: string, actual: string): boolean { - const extractTokens = (str: string) => { - const tokens = str.match(/\b(function|console\.log|return|if|else|for|while)\b/g) || []; - return tokens; - }; - - const searchTokens = extractTokens(search); - const actualTokens = extractTokens(actual); - - if (searchTokens.length !== actualTokens.length) return false; - - for (let i = 0; i < searchTokens.length; i++) { - if (searchTokens[i] !== actualTokens[i]) return false; - } - - return true; - } - - /** - * Compute Longest Common Subsequence using dynamic programming - * Returns array of indices in oldLines that are part of LCS - */ - private computeLCS(oldLines: string[], newLines: string[]): number[][] { - const m = oldLines.length; - const n = newLines.length; - const dp: number[][] = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0)); - - // Build LCS length table - 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]); - } - } - } - - return dp; - } - - /** - * Extract changes from LCS table - * Returns array of change regions - */ - private extractChanges( - oldLines: string[], - newLines: string[], - lcs: number[][] - ): Array<{ oldStart: number; oldEnd: number; newStart: number; newEnd: number }> { - const changes: Array<{ - oldStart: number; - oldEnd: number; - newStart: number; - newEnd: number; - }> = []; - - let i = oldLines.length; - let j = newLines.length; - let oldEnd = i; - let newEnd = j; - let inChange = false; - - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { - // Lines match - if we were in a change, close it - if (inChange) { - changes.unshift({ - oldStart: i, - oldEnd: oldEnd, - newStart: j, - newEnd: newEnd - }); - inChange = false; - } - i--; - j--; - } else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) { - // Insertion in new file - if (!inChange) { - oldEnd = i; - newEnd = j; - inChange = true; - } - j--; - } else if (i > 0) { - // Deletion from old file - if (!inChange) { - oldEnd = i; - newEnd = j; - inChange = true; - } - i--; - } - } - - // Close any remaining change - if (inChange) { - changes.unshift({ - oldStart: 0, - oldEnd: oldEnd, - newStart: 0, - newEnd: newEnd - }); - } - - return changes; - } - - private generateDiff( - oldLines: string[], - newLines: string[], - filePath: string - ): string { - const CONTEXT_LINES = 3; - - // Use LCS-based diff algorithm to find actual changes - const lcs = this.computeLCS(oldLines, newLines); - const changes = this.extractChanges(oldLines, newLines, lcs); - - const hunks: Array<{ - oldStart: number; - oldCount: number; - newStart: number; - newCount: number; - lines: Array<{ type: '+' | '-' | ' '; content: string }>; - }> = []; - - let accumulatedOffset = 0; - - for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) { - const change = changes[changeIdx]; - - let contextStart = Math.max(0, change.oldStart - CONTEXT_LINES); - let contextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES); - - if (hunks.length > 0) { - const lastHunk = hunks[hunks.length - 1]; - const lastHunkEnd = lastHunk.oldStart + lastHunk.oldCount; - - if (lastHunkEnd >= contextStart) { - const oldHunkEnd = lastHunk.oldStart + lastHunk.oldCount; - const newContextEnd = Math.min(oldLines.length, change.oldEnd + CONTEXT_LINES); - - for (let idx = oldHunkEnd; idx < change.oldStart; idx++) { - lastHunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - for (let idx = change.oldStart; idx < change.oldEnd; idx++) { - lastHunk.lines.push({ type: '-', content: oldLines[idx] }); - } - for (let idx = change.newStart; idx < change.newEnd; idx++) { - lastHunk.lines.push({ type: '+', content: newLines[idx] }); - } - - for (let idx = change.oldEnd; idx < newContextEnd && idx < oldLines.length; idx++) { - lastHunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - lastHunk.oldCount = newContextEnd - lastHunk.oldStart; - lastHunk.newCount = lastHunk.oldCount + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart); - - continue; - } - } - - const hunk: typeof hunks[0] = { - oldStart: contextStart + 1, - oldCount: contextEnd - contextStart, - newStart: contextStart + 1 + accumulatedOffset, - newCount: contextEnd - contextStart + (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart), - lines: [] - }; - - for (let idx = contextStart; idx < change.oldStart; idx++) { - hunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - for (let idx = change.oldStart; idx < change.oldEnd; idx++) { - hunk.lines.push({ type: '-', content: oldLines[idx] }); - } - - for (let idx = change.newStart; idx < change.newEnd; idx++) { - hunk.lines.push({ type: '+', content: newLines[idx] }); - } - - for (let idx = change.oldEnd; idx < contextEnd && idx < oldLines.length; idx++) { - hunk.lines.push({ type: ' ', content: oldLines[idx] }); - } - - hunks.push(hunk); - - accumulatedOffset += (change.newEnd - change.newStart) - (change.oldEnd - change.oldStart); - } - - let addedLines = 0; - let removedLines = 0; - - for (const hunk of hunks) { - for (const line of hunk.lines) { - if (line.type === '+') addedLines++; - if (line.type === '-') removedLines++; - } - } - - let summary = `Updated ${filePath}`; - if (addedLines > 0 && removedLines > 0) { - summary += ` with ${addedLines} addition${ - addedLines !== 1 ? "s" : "" - } and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`; - } else if (addedLines > 0) { - summary += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""}`; - } else if (removedLines > 0) { - summary += ` with ${removedLines} removal${ - removedLines !== 1 ? "s" : "" - }`; - } else if (changes.length === 0) { - return `No changes in ${filePath}`; - } - - let diff = summary + "\n"; - diff += `--- a/${filePath}\n`; - diff += `+++ b/${filePath}\n`; - - for (const hunk of hunks) { - diff += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`; - - for (const line of hunk.lines) { - diff += `${line.type}${line.content}\n`; - } - } - - return diff.trim(); - } - - getEditHistory(): EditorCommand[] { - return [...this.editHistory]; - } -} diff --git a/src/tools/todo-tool.ts b/src/tools/todo-tool.ts deleted file mode 100644 index 4895630..0000000 --- a/src/tools/todo-tool.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { ToolResult } from '../types/index.js'; - -interface TodoItem { - id: string; - content: string; - status: 'pending' | 'in_progress' | 'completed'; - priority: 'high' | 'medium' | 'low'; -} - -export class TodoTool { - private todos: TodoItem[] = []; - - formatTodoList(): string { - if (this.todos.length === 0) { - return 'No todos created yet'; - } - - const getCheckbox = (status: string): string => { - switch (status) { - case 'completed': - return '●'; - case 'in_progress': - return '◐'; - case 'pending': - return '○'; - default: - return '○'; - } - }; - - const getStatusColor = (status: string): string => { - switch (status) { - case 'completed': - return '\x1b[32m'; // Green - case 'in_progress': - return '\x1b[36m'; // Cyan - case 'pending': - return '\x1b[37m'; // White/default - default: - return '\x1b[0m'; // Reset - } - }; - - const reset = '\x1b[0m'; - let output = ''; - - this.todos.forEach((todo, index) => { - const checkbox = getCheckbox(todo.status); - const statusColor = getStatusColor(todo.status); - const strikethrough = todo.status === 'completed' ? '\x1b[9m' : ''; - const indent = index === 0 ? '' : ' '; - - output += `${indent}${statusColor}${strikethrough}${checkbox} ${todo.content}${reset}\n`; - }); - - return output; - } - - async createTodoList(todos: TodoItem[]): Promise { - try { - // Validate todos - for (const todo of todos) { - if (!todo.id || !todo.content || !todo.status || !todo.priority) { - return { - success: false, - error: 'Each todo must have id, content, status, and priority fields' - }; - } - - if (!['pending', 'in_progress', 'completed'].includes(todo.status)) { - return { - success: false, - error: `Invalid status: ${todo.status}. Must be pending, in_progress, or completed` - }; - } - - if (!['high', 'medium', 'low'].includes(todo.priority)) { - return { - success: false, - error: `Invalid priority: ${todo.priority}. Must be high, medium, or low` - }; - } - } - - this.todos = todos; - - return { - success: true, - output: this.formatTodoList() - }; - } catch (error) { - return { - success: false, - error: `Error creating todo list: ${error instanceof Error ? error.message : String(error)}` - }; - } - } - - async updateTodoList(updates: { id: string; status?: string; content?: string; priority?: string }[]): Promise { - try { - const updatedIds: string[] = []; - - for (const update of updates) { - const todoIndex = this.todos.findIndex(t => t.id === update.id); - - if (todoIndex === -1) { - return { - success: false, - error: `Todo with id ${update.id} not found` - }; - } - - const todo = this.todos[todoIndex]; - - if (update.status && !['pending', 'in_progress', 'completed'].includes(update.status)) { - return { - success: false, - error: `Invalid status: ${update.status}. Must be pending, in_progress, or completed` - }; - } - - if (update.priority && !['high', 'medium', 'low'].includes(update.priority)) { - return { - success: false, - error: `Invalid priority: ${update.priority}. Must be high, medium, or low` - }; - } - - if (update.status) todo.status = update.status as any; - if (update.content) todo.content = update.content; - if (update.priority) todo.priority = update.priority as any; - - updatedIds.push(update.id); - } - - return { - success: true, - output: this.formatTodoList() - }; - } catch (error) { - return { - success: false, - error: `Error updating todo list: ${error instanceof Error ? error.message : String(error)}` - }; - } - } - - async viewTodoList(): Promise { - return { - success: true, - output: this.formatTodoList() - }; - } -} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index f565e25..2309f39 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,34 +1,108 @@ +export interface FileDiff { + filePath: string; + additions: number; + removals: number; + patch: string; + isNew: boolean; +} + +export interface PlanStep { + title: string; + description: string; + filePaths?: string[]; +} + +export interface PlanQuestion { + id: string; + question: string; + header?: string; + type: "select" | "multiselect" | "text"; + options?: { id: string; label: string }[]; +} + +export interface Plan { + title: string; + summary: string; + steps: PlanStep[]; + questions?: PlanQuestion[]; +} + +export interface TaskRequest { + agent: "general" | "explore"; + description: string; + prompt: string; +} + +export interface TaskRun { + agent: "general" | "explore"; + description: string; + summary: string; + activity?: string; +} + +export interface SubagentStatus { + agent: "general" | "explore"; + description: string; + detail: string; +} + export interface ToolResult { success: boolean; output?: string; error?: string; - data?: any; + diff?: FileDiff; + plan?: Plan; + task?: TaskRun; } -export interface Tool { - name: string; - description: string; - execute: (...args: any[]) => Promise; +export interface ToolCall { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; } -export interface EditorCommand { - command: 'view' | 'str_replace' | 'create' | 'insert' | 'undo_edit'; - path?: string; - old_str?: string; - new_str?: string; +export interface ChatEntry { + type: "user" | "assistant" | "tool_call" | "tool_result"; + content: string; + timestamp: Date; + modeColor?: string; + toolCalls?: ToolCall[]; + toolCall?: ToolCall; + toolResult?: ToolResult; +} + +export interface StreamChunk { + type: "content" | "tool_calls" | "tool_result" | "done" | "error" | "reasoning"; content?: string; - insert_line?: number; - view_range?: [number, number]; - replace_all?: boolean; + toolCalls?: ToolCall[]; + toolCall?: ToolCall; + toolResult?: ToolResult; } -export interface AgentState { - currentDirectory: string; - editHistory: EditorCommand[]; - tools: Tool[]; +export interface ModelInfo { + id: string; + name: string; + contextWindow: number; + inputPrice: number; + outputPrice: number; + reasoning: boolean; + description: string; } -export interface ConfirmationState { - skipThisSession: boolean; - pendingOperation: boolean; -} \ No newline at end of file +export interface Skill { + id: string; + title: string; + description: string; + executable?: boolean; +} + +export type AgentMode = "agent" | "plan" | "ask"; + +export const MODES: { id: AgentMode; label: string; color: string }[] = [ + { id: "agent", label: "Agent", color: "#5c9cf5" }, + { id: "plan", label: "Plan", color: "#e5c07b" }, + { id: "ask", label: "Ask", color: "#22c55e" }, +]; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index e595a5c..1b7acd2 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -1,162 +1,1107 @@ -import React, { useState, useEffect } from 'react'; -import { Box, Text, useInput } from 'ink'; -import { Agent } from '../agent/index.js'; -import { ToolResult } from '../types/index.js'; -import { ConfirmationService, ConfirmationOptions } from '../utils/confirmation-service.js'; -import ConfirmationDialog from './components/confirmation-dialog.js'; -import chalk from 'chalk'; +import os from "os"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { useKeyboard, useTerminalDimensions } from "@opentui/react"; +import type { ScrollBoxRenderable, TextareaRenderable, KeyBinding } from "@opentui/core"; +import { decodePasteBytes, PasteEvent } from "@opentui/core"; +import type { Agent } from "../agent/agent.js"; +import type { + ChatEntry, + ToolCall, + AgentMode, + ModelInfo, + FileDiff, + Plan, + PlanQuestion, + SubagentStatus, +} from "../types/index.js"; +import { MODES } from "../types/index.js"; +import { getModelInfo, MODELS } from "../grok/models.js"; +import { saveProjectSettings } from "../utils/settings.js"; +import { dark, type Theme } from "./theme.js"; +import { Markdown } from "./markdown.js"; +import { PlanView, PlanQuestionsPanel, formatPlanAnswers, initialPlanQuestionsState, type PlanAnswers, type PlanQuestionsState } from "./plan.js"; -interface Props { - agent: Agent; -} +const STAR_PALETTE = ["#777777", "#666666", "#4a4a4a", "#333333", "#222222"]; +const LOADING_SPINNER_FRAMES = ["⬒", "⬔", "⬓", "⬕"]; -export default function App({ agent }: Props) { - const [input, setInput] = useState(''); - const [history, setHistory] = useState>([]); - const [isProcessing, setIsProcessing] = useState(false); - const [confirmationOptions, setConfirmationOptions] = useState(null); - // Removed useApp().exit - using process.exit(0) instead for better terminal handling - - const confirmationService = ConfirmationService.getInstance(); +type Star = { col: number; ch: string }; +type Row = { stars: Star[]; grok?: number }; + +const HERO_ROWS: Row[] = [ + { stars: [{ col: 0, ch: "·" }, { col: 13, ch: "*" }, { col: 21, ch: "·" }, { col: 34, ch: "·" }] }, + { stars: [{ col: 3, ch: "*" }, { col: 11, ch: "·" }, { col: 17, ch: "·" }, { col: 25, ch: "*" }] }, + { stars: [{ col: 6, ch: "·" }, { col: 12, ch: "·" }, { col: 15, ch: "·" }, { col: 18, ch: "·" }, { col: 24, ch: "·" }] }, + { stars: [{ col: 2, ch: "·" }, { col: 10, ch: "·" }, { col: 19, ch: "·" }, { col: 27, ch: "·" }], grok: 13 }, + { stars: [{ col: 6, ch: "·" }, { col: 12, ch: "·" }, { col: 15, ch: "·" }, { col: 18, ch: "·" }, { col: 24, ch: "·" }] }, + { stars: [{ col: 3, ch: "·" }, { col: 11, ch: "*" }, { col: 17, ch: "·" }, { col: 25, ch: "·" }] }, + { stars: [{ col: 0, ch: "*" }, { col: 13, ch: "·" }, { col: 21, ch: "*" }, { col: 34, ch: "·" }] }, +]; + +function HeroLogo({ t }: { t: Theme }) { + const [tick, setTick] = useState(0); + const starIdx = useRef(0); useEffect(() => { - const handleConfirmationRequest = (options: ConfirmationOptions) => { - setConfirmationOptions(options); - }; - - confirmationService.on('confirmation-requested', handleConfirmationRequest); - - return () => { - confirmationService.off('confirmation-requested', handleConfirmationRequest); - }; - }, [confirmationService]); - - // Reset confirmation service session on app start - useEffect(() => { - confirmationService.resetSession(); + const id = setInterval(() => setTick(n => n + 1), 900); + return () => clearInterval(id); }, []); - useInput(async (inputChar: string, key: any) => { - // If confirmation dialog is open, don't handle normal input - if (confirmationOptions) { - return; - } - if (key.ctrl && inputChar === 'c') { - process.exit(0); - return; - } + starIdx.current = 0; + const nextColor = () => { + const i = starIdx.current++; + return STAR_PALETTE[(i * 7 + tick * 3 + i * tick) % STAR_PALETTE.length]; + }; - if (key.return) { - if (input.trim() === 'exit' || input.trim() === 'quit') { - process.exit(0); + return ( + + {HERO_ROWS.map((row, r) => { + const els: React.ReactNode[] = []; + let cursor = 0; + + for (const star of row.stars) { + if (row.grok !== undefined && cursor <= row.grok && star.col > row.grok) { + els.push(" ".repeat(row.grok - cursor)); + els.push({"Grok"}); + cursor = row.grok + 4; + } + const gap = star.col - cursor; + if (gap > 0) els.push(" ".repeat(gap)); + els.push({star.ch}); + cursor = star.col + 1; + } + + if (row.grok !== undefined && cursor <= row.grok) { + els.push(" ".repeat(row.grok - cursor)); + els.push({"Grok"}); + cursor = row.grok + 4; + } + + els.push(" ".repeat(Math.max(0, 35 - cursor))); + return {els}; + })} + + ); +} + +const SPLIT = { + topLeft: "", bottomLeft: "", vertical: "┃", topRight: "", + bottomRight: "", horizontal: " ", bottomT: "", topT: "", + cross: "", leftT: "", rightT: "", +}; +const SPLIT_END = { ...SPLIT, bottomLeft: "╹" }; +const EMPTY = { + topLeft: "", bottomLeft: "", vertical: "", topRight: "", + bottomRight: "", horizontal: " ", bottomT: "", topT: "", + cross: "", leftT: "", rightT: "", +}; +const LINE = { + topLeft: "━", bottomLeft: "━", vertical: "", topRight: "━", + bottomRight: "━", horizontal: "━", bottomT: "━", topT: "━", + cross: "━", leftT: "━", rightT: "━", +}; + +interface SlashMenuItem { + id: string; + label: string; + description: string; +} + +const SLASH_MENU_ITEMS: SlashMenuItem[] = [ + { id: "exit", label: "exit", description: "Quit the CLI" }, + { id: "help", label: "help", description: "Show available commands" }, + { id: "mcps", label: "mcps", description: "Manage MCP servers" }, + { id: "models", label: "models", description: "Select a model" }, + { id: "new", label: "new session", description: "Start a new session" }, + { id: "review", label: "review", description: "Review recent changes" }, + { id: "skills", label: "skills", description: "Manage skills" }, +]; + +interface AppProps { agent: Agent; initialMessage?: string; onExit?: () => void } + +export function App({ agent, initialMessage, onExit }: AppProps) { + const t = dark; + const [messages, setMessages] = useState([]); + const [streamContent, setStreamContent] = useState(""); + const [streamReasoning, setStreamReasoning] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + const [model, setModel] = useState(agent.getModel()); + const [mode, setModeState] = useState(agent.getMode()); + const [showModelPicker, setShowModelPicker] = useState(false); + const [modelPickerIndex, setModelPickerIndex] = useState(0); + const [modelSearchQuery, setModelSearchQuery] = useState(""); + const [activeToolCalls, setActiveToolCalls] = useState([]); + const [sessionTitle, setSessionTitle] = useState(null); + const [showSlashMenu, setShowSlashMenu] = useState(false); + const [slashMenuIndex, setSlashMenuIndex] = useState(0); + const [slashSearchQuery, setSlashSearchQuery] = useState(""); + const [pasteBlocks, setPasteBlocks] = useState<{ id: number; content: string; lines: number; isImage?: boolean }[]>([]); + const [activePlan, setActivePlan] = useState(null); + const [activeSubagent, setActiveSubagent] = useState(null); + const [pqs, setPqs] = useState(initialPlanQuestionsState()); + const imageCounterRef = useRef(0); + const pasteCounterRef = useRef(0); + const inputRef = useRef(null); + const scrollRef = useRef(null); + const { width, height } = useTerminalDimensions(); + const processedInitial = useRef(false); + const contentAccRef = useRef(""); + const startTimeRef = useRef(0); + const isProcessingRef = useRef(false); + + const setMode = useCallback((m: AgentMode) => { + if (m === "agent" && mode === "plan" && activePlan) { + const planText = [`# ${activePlan.title}`, activePlan.summary, "", + ...activePlan.steps.map((s, i) => `${i + 1}. ${s.title}: ${s.description}${s.filePaths?.length ? ` (${s.filePaths.join(", ")})` : ""}`), + ].join("\n"); + agent.setPlanContext(planText); + } + agent.setMode(m); + setModeState(m); + }, [agent, mode, activePlan]); + const cycleMode = useCallback(() => { + const idx = MODES.findIndex((m) => m.id === mode); + setMode(MODES[(idx + 1) % MODES.length].id); + }, [mode, setMode]); + + const modeInfo = MODES.find((m) => m.id === mode)!; + const modelInfo = getModelInfo(model); + const flatModels = MODELS.map((m) => m.id); + const filteredModels = modelSearchQuery + ? MODELS.filter((m) => + m.name.toLowerCase().includes(modelSearchQuery.toLowerCase()) || + m.id.toLowerCase().includes(modelSearchQuery.toLowerCase()) + ) + : MODELS; + const filteredModelIds = filteredModels.map((m) => m.id); + const filteredSlashItems = slashSearchQuery + ? SLASH_MENU_ITEMS.filter((item) => + item.label.toLowerCase().includes(slashSearchQuery.toLowerCase()) || + item.description.toLowerCase().includes(slashSearchQuery.toLowerCase()) + ) + : SLASH_MENU_ITEMS; + + const scrollToBottom = useCallback(() => { + try { scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999); } catch { /* */ } + }, []); + + const processMessage = useCallback(async (text: string) => { + if (!text.trim() || isProcessingRef.current) return; + isProcessingRef.current = true; + setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); setActiveSubagent(null); contentAccRef.current = ""; + startTimeRef.current = Date.now(); + if (!sessionTitle) agent.generateTitle(text.trim()).then(setSessionTitle).catch(() => {}); + setMessages((prev) => [...prev, { type: "user", content: text.trim(), timestamp: new Date(), modeColor: modeInfo.color }]); + setTimeout(scrollToBottom, 50); + try { + for await (const chunk of agent.processMessage(text.trim())) { + switch (chunk.type) { + case "content": + contentAccRef.current += chunk.content || ""; + setStreamContent(sanitizeContent(contentAccRef.current)); + setTimeout(scrollToBottom, 10); + break; + case "reasoning": + setStreamReasoning((p) => p + (chunk.content || "")); + break; + case "tool_calls": + if (chunk.toolCalls) { + const cleaned = sanitizeContent(contentAccRef.current); + if (cleaned) { + setMessages((p) => [...p, { type: "assistant", content: cleaned, timestamp: new Date(), modeColor: modeInfo.color }]); + } + contentAccRef.current = ""; setStreamContent(""); + setActiveToolCalls(chunk.toolCalls); + } + break; + case "tool_result": + if (chunk.toolCall && chunk.toolResult) { + setMessages((p) => [...p, { + type: "tool_result", + content: chunk.toolResult!.success ? (chunk.toolResult!.output || "Success") : (chunk.toolResult!.error || "Error"), + timestamp: new Date(), modeColor: modeInfo.color, toolCall: chunk.toolCall, toolResult: chunk.toolResult, + }]); + if (chunk.toolResult.plan?.questions?.length) { + setActivePlan(chunk.toolResult.plan); + setPqs(initialPlanQuestionsState()); + } + setActiveToolCalls([]); setTimeout(scrollToBottom, 10); + } + break; + case "error": + contentAccRef.current += `\n${chunk.content || "Unknown error"}`; + setStreamContent(contentAccRef.current); + break; + case "done": break; + } + } + } catch { contentAccRef.current += "\nAn unexpected error occurred."; setStreamContent(contentAccRef.current); } + const finalContent = sanitizeContent(contentAccRef.current); + if (finalContent) { + setMessages((p) => [...p, { type: "assistant", content: finalContent, timestamp: new Date(), modeColor: modeInfo.color }]); + } + + contentAccRef.current = ""; setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); setActiveSubagent(null); + isProcessingRef.current = false; + setIsProcessing(false); setTimeout(scrollToBottom, 50); + }, [agent, scrollToBottom, modeInfo, model]); + + useEffect(() => { if (initialMessage && !processedInitial.current) { processedInitial.current = true; processMessage(initialMessage); } }, [initialMessage, processMessage]); + useEffect(() => agent.onSubagentStatus(setActiveSubagent), [agent]); + + const handleCommand = useCallback((cmd: string): boolean => { + const c = cmd.trim().toLowerCase(); + if (c === "/clear") { agent.clearHistory(); setMessages([]); setStreamContent(""); setSessionTitle(null); setPasteBlocks([]); setActivePlan(null); setPqs(initialPlanQuestionsState()); imageCounterRef.current = 0; return true; } + if (c === "/model" || c === "/models") { setShowModelPicker(true); setModelPickerIndex(0); setModelSearchQuery(""); return true; } + if (c === "/quit" || c === "/exit" || c === "/q") { onExit ? onExit() : process.exit(0); } + return false; + }, [agent, model, flatModels]); + + const handleSlashMenuSelect = useCallback((item: SlashMenuItem) => { + setShowSlashMenu(false); + inputRef.current?.clear(); + switch (item.id) { + case "new": + agent.clearHistory(); setMessages([]); setStreamContent(""); setSessionTitle(null); setPasteBlocks([]); setActivePlan(null); setPqs(initialPlanQuestionsState()); + break; + case "models": + setShowModelPicker(true); setModelPickerIndex(0); setModelSearchQuery(""); + break; + case "exit": + onExit ? onExit() : process.exit(0); + break; + case "help": + setMessages((p) => [...p, { type: "assistant", content: SLASH_MENU_ITEMS.map((i) => `/${i.label} — ${i.description}`).join("\n"), timestamp: new Date() }]); + break; + case "skills": + setMessages((p) => [...p, { type: "assistant", content: "Skills management coming soon.", timestamp: new Date() }]); + break; + case "mcps": + setMessages((p) => [...p, { type: "assistant", content: "MCP server management coming soon.", timestamp: new Date() }]); + break; + case "review": + setMessages((p) => [...p, { type: "assistant", content: "Review feature coming soon.", timestamp: new Date() }]); + break; + } + }, [agent, flatModels, model]); + + const showPlanPanel = !!(activePlan?.questions?.length); + const planQuestions = activePlan?.questions ?? []; + const isSinglePlan = planQuestions.length === 1 && planQuestions[0]?.type !== "multiselect"; + const planTabCount = isSinglePlan ? 1 : planQuestions.length + 1; + const isPlanConfirmTab = !isSinglePlan && pqs.tab === planQuestions.length; + + const dismissPlan = useCallback(() => { + setActivePlan(null); + setPqs(initialPlanQuestionsState()); + }, []); + + const submitPlanAnswers = useCallback(() => { + if (!activePlan?.questions?.length) return; + const text = formatPlanAnswers(activePlan.questions, pqs.answers); + setActivePlan(null); + setPqs(initialPlanQuestionsState()); + processMessage(text); + }, [activePlan, pqs.answers, processMessage]); + + const handlePlanSelect = useCallback((q: PlanQuestion, idx: number, options: { id: string; label: string }[], showCustom: boolean) => { + const isCustom = showCustom && idx === options.length; + if (isCustom) { + if (q.type === "multiselect") { + const customVal = pqs.customInputs[q.id] ?? ""; + if (customVal) { + const existing = (pqs.answers[q.id] as string[] | undefined) ?? []; + if (existing.includes(customVal)) { + setPqs((s) => ({ ...s, answers: { ...s.answers, [q.id]: existing.filter((x) => x !== customVal) } })); + } else { + setPqs((s) => ({ ...s, editing: true })); + } + } else { + setPqs((s) => ({ ...s, editing: true })); + } + } else { + setPqs((s) => ({ ...s, editing: true })); + } + return; + } + const opt = options[idx]; + if (!opt) return; + + if (q.type === "multiselect") { + setPqs((s) => { + const existing = (s.answers[q.id] as string[] | undefined) ?? []; + const next = existing.includes(opt.id) ? existing.filter((x) => x !== opt.id) : [...existing, opt.id]; + return { ...s, answers: { ...s.answers, [q.id]: next } }; + }); + } else { + setPqs((s) => ({ ...s, answers: { ...s.answers, [q.id]: opt.id } })); + if (isSinglePlan) { submitPlanAnswers(); return; } + setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 })); + } + }, [pqs, isSinglePlan, submitPlanAnswers]); + + useKeyboard((key) => { + if (showPlanPanel) { + const q = planQuestions[pqs.tab]; + + // Escape always dismisses + if (key.name === "escape") { dismissPlan(); return; } + + // When editing custom text input + if (pqs.editing && !isPlanConfirmTab) { + if (key.name === "return") { + const qId = q?.id; + if (qId) { + const text = (pqs.customInputs[qId] ?? "").trim(); + if (text) { + if (q.type === "multiselect") { + const existing = (pqs.answers[qId] as string[] | undefined) ?? []; + const next = existing.includes(text) ? existing : [...existing, text]; + setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: next } })); + } else if (q.type === "text") { + setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: text } })); + if (isSinglePlan) { submitPlanAnswers(); return; } + setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 })); + } else { + setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: text } })); + if (isSinglePlan) { submitPlanAnswers(); return; } + setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 })); + } + } else { + setPqs((s) => ({ ...s, editing: false })); + } + } + return; + } + if (key.name === "backspace") { + const qId = q?.id; + if (qId) setPqs((s) => ({ ...s, customInputs: { ...s.customInputs, [qId]: (s.customInputs[qId] ?? "").slice(0, -1) } })); + return; + } + if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) { + const qId = q?.id; + if (qId) setPqs((s) => ({ ...s, customInputs: { ...s.customInputs, [qId]: (s.customInputs[qId] ?? "") + key.sequence } })); + return; + } return; } - if (input.trim()) { - setIsProcessing(true); - const result = await agent.processCommand(input.trim()); - setHistory(prev => [...prev, { command: input.trim(), result }]); - setInput(''); - setIsProcessing(false); + // Tab / left / right — switch between question tabs + if (key.name === "tab") { + const dir = key.shift ? -1 : 1; + setPqs((s) => ({ ...s, tab: (s.tab + dir + planTabCount) % planTabCount, selected: 0 })); + return; + } + if (key.name === "left" || key.name === "h") { + setPqs((s) => ({ ...s, tab: (s.tab - 1 + planTabCount) % planTabCount, selected: 0 })); + return; + } + if (key.name === "right" || key.name === "l") { + setPqs((s) => ({ ...s, tab: (s.tab + 1) % planTabCount, selected: 0 })); + return; + } + + // Confirm tab + if (isPlanConfirmTab) { + if (key.name === "return") { submitPlanAnswers(); return; } + return; + } + + if (!q) return; + + // Text-only question (no options) + if (q.type === "text") { + setPqs((s) => ({ ...s, editing: true })); + return; + } + + // Up/down — navigate options + const options = q.options ?? []; + const showCustom = true; + const totalItems = options.length + 1; + + if (key.name === "up" || key.name === "k") { + setPqs((s) => ({ ...s, selected: (s.selected - 1 + totalItems) % totalItems })); + return; + } + if (key.name === "down" || key.name === "j") { + setPqs((s) => ({ ...s, selected: (s.selected + 1) % totalItems })); + return; + } + + // Number keys 1-9 for quick selection + const digit = Number(key.name); + if (!Number.isNaN(digit) && digit >= 1 && digit <= Math.min(totalItems, 9)) { + const idx = digit - 1; + setPqs((s) => ({ ...s, selected: idx })); + handlePlanSelect(q, idx, options, showCustom); + return; + } + + // Enter — select current option + if (key.name === "return") { + handlePlanSelect(q, pqs.selected, options, showCustom); + return; + } + + return; + } + if (showSlashMenu) { + if (key.name === "escape") { setShowSlashMenu(false); setSlashSearchQuery(""); inputRef.current?.clear(); return; } + if (key.name === "up") { setSlashMenuIndex((i) => Math.max(0, i - 1)); return; } + if (key.name === "down") { setSlashMenuIndex((i) => Math.min(filteredSlashItems.length - 1, i + 1)); return; } + if (key.name === "return") { + const item = filteredSlashItems[slashMenuIndex]; + if (item) handleSlashMenuSelect(item); + setSlashSearchQuery(""); + return; + } + if (key.name === "backspace") { + setSlashSearchQuery((q) => q.slice(0, -1)); + setSlashMenuIndex(0); + return; + } + if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) { + setSlashSearchQuery((q) => q + key.sequence); + setSlashMenuIndex(0); + return; } return; } - - if (key.backspace || key.delete) { - setInput(prev => prev.slice(0, -1)); + if (showModelPicker) { + if (key.name === "escape") { setShowModelPicker(false); setModelSearchQuery(""); return; } + if (key.name === "up") { setModelPickerIndex((i) => Math.max(0, i - 1)); return; } + if (key.name === "down") { setModelPickerIndex((i) => Math.min(filteredModelIds.length - 1, i + 1)); return; } + if (key.name === "return") { + const sel = filteredModelIds[modelPickerIndex]; + if (sel) { agent.setModel(sel); setModel(sel); saveProjectSettings({ model: sel }); } + setShowModelPicker(false); setModelSearchQuery(""); return; + } + if (key.name === "backspace") { + setModelSearchQuery((q) => q.slice(0, -1)); + setModelPickerIndex(0); + return; + } + if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) { + setModelSearchQuery((q) => q + key.sequence); + setModelPickerIndex(0); + return; + } return; } - - if (inputChar && !key.ctrl && !key.meta) { - setInput(prev => prev + inputChar); + if (isProcessing && key.name === "escape") { agent.abort(); return; } + if (key.sequence === "/" && !isProcessing) { + const text = inputRef.current?.plainText || ""; + if (!text.trim()) { setShowSlashMenu(true); setSlashMenuIndex(0); setSlashSearchQuery(""); return; } } + if (key.name === "c" && key.ctrl) { + const text = inputRef.current?.plainText || ""; + if (text.trim()) { inputRef.current?.clear(); setPasteBlocks([]); } else { onExit ? onExit() : process.exit(0); } + return; + } + if (key.name === "tab" && !isProcessing) { cycleMode(); return; } }); - const renderResult = (result: ToolResult) => { - if (result.success) { - return ( - - ✓ Success - {result.output && ( - - {result.output} - - )} - - ); - } else { - return ( - - ✗ Error - {result.error && ( - - {result.error} - - )} - - ); + const handlePaste = useCallback((event: PasteEvent) => { + const text = decodePasteBytes(event.bytes); + const trimmed = text.trim(); + const imageExts = /\.(png|jpe?g|gif|webp|svg|bmp|ico|tiff?)$/i; + if (imageExts.test(trimmed) && !trimmed.includes("\n")) { + event.preventDefault(); + const id = ++pasteCounterRef.current; + const imgNum = ++imageCounterRef.current; + setPasteBlocks((prev) => [...prev, { id, content: trimmed, lines: 1, isImage: true }]); + inputRef.current?.insertText(`[Image ${imgNum}]`); + return; } - }; + const lineCount = text.split("\n").length; + if (lineCount < 2) return; + event.preventDefault(); + const id = ++pasteCounterRef.current; + setPasteBlocks((prev) => [...prev, { id, content: text, lines: lineCount }]); + inputRef.current?.insertText(`[Pasted ~${lineCount} lines]`); + }, []); - const handleConfirmation = (dontAskAgain?: boolean) => { - confirmationService.confirmOperation(true, dontAskAgain); - setConfirmationOptions(null); - }; + const handleSubmit = useCallback(() => { + const raw = inputRef.current?.plainText || ""; + if (!raw.trim() && pasteBlocks.length === 0) return; + inputRef.current?.clear(); + let message = raw; + const blocks = [...pasteBlocks]; + let imgIdx = 0; + setPasteBlocks([]); + for (const block of blocks) { + if (block.isImage) { + imgIdx++; + message = message.replace(`[Image ${imgIdx}]`, block.content); + } else { + message = message.replace(`[Pasted ~${block.lines} lines]`, block.content); + } + } + if (!message.trim()) return; + if (handleCommand(message)) return; + processMessage(message); + }, [handleCommand, processMessage, pasteBlocks]); - const handleRejection = (feedback?: string) => { - confirmationService.rejectOperation(feedback); - setConfirmationOptions(null); - }; - - if (confirmationOptions) { - return ( - - ); - } + const hasMessages = messages.length > 0 || streamContent || isProcessing; return ( - - - - 🔧 Grok CLI - Text Editor Agent - - - - - - Available commands: view, str_replace, create, insert, undo_edit, bash, help - - - Type 'help' for detailed usage, 'exit' or Ctrl+C to quit - - - - - {history.slice(-10).map((entry, index) => ( - - - $ - {entry.command} - - {renderResult(entry.result)} - - ))} - - - - $ - - {input} - {!isProcessing && █} - - {isProcessing && (processing...)} - - + + {hasMessages ? ( + + {/* Session header — ┃ left-border panel like OpenCode's Header */} + + {/* Scrollable messages */} + + {messages.map((msg, i) => ( + + ))} + {/* Active tool calls — pending inline */} + {activeToolCalls.map((tc, i) => ( + tc.function.name === "task" + ? + : {toolLabel(tc)} + ))} + {activeSubagent && } + {/* Streaming assistant content */} + {streamContent && ( + + + + )} + {/* Waiting indicator */} + {isProcessing && !streamContent && activeToolCalls.length === 0 && ( + + )} + {/* Plan questions panel — inline, OpenCode-style */} + {showPlanPanel && ( + + )} + + {/* Prompt */} + + + + + ) : ( + /* ── Home ───────────────────────────────────────── */ + <> + + + + + + + + + + + + + + {agent.getCwd().replace(os.homedir(), "~")} + + {"v1.0.0"} + + + )} + {showSlashMenu && } + {showModelPicker && } + ); -} \ No newline at end of file +} + +/* ── Session Header ──────────────────────────────────────────── */ + +function SessionHeader({ t, modeInfo, sessionTitle }: { + t: Theme; modeInfo: typeof MODES[number]; sessionTitle: string | null; +}) { + return ( + + + + {modeInfo.label} + {sessionTitle ? {": "}{sessionTitle} : null} + + + + ); +} + +/* ── Prompt Box ──────────────────────────────────────────────── */ + +const TEXTAREA_KEYBINDINGS: KeyBinding[] = [ + { name: "return", action: "submit" }, + { name: "return", shift: true, action: "newline" }, +]; + +function PromptBox({ t, inputRef, isProcessing, showModelPicker, showSlashMenu, showPlanQuestions, onSubmit, onPaste, pasteBlocks, modeInfo, model, modelInfo, placeholder }: { + t: Theme; inputRef: React.RefObject; + isProcessing: boolean; showModelPicker: boolean; showSlashMenu: boolean; showPlanQuestions: boolean; onSubmit: () => void; + onPaste: (event: PasteEvent) => void; pasteBlocks: { id: number; content: string; lines: number }[]; + modeInfo: typeof MODES[number]; model: string; + modelInfo: ReturnType; placeholder?: string; +}) { + return ( + + + +