feat: complete architecture rewrite with Bun, OpenTUI, bash-only tools, and X Search API

Major rewrite of the Grok CLI agent:

- Runtime: Bun-first (removed Node.js/tsx fallbacks)
- UI: Replaced Ink with OpenTUI (@opentui/react) terminal framework
- Tools: Simplified to bash-only (removed text-editor, search, morph, todo, confirmation tools)
- Search: Integrated X Search API and Web Search via xAI Responses API
- Models: Updated to all latest Grok models (grok-4, grok-4.20-beta, grok-4-1-fast, grok-4-fast, grok-3-mini)
- Agent: Streamlined agent loop with async generator streaming
- Client: Dual API support (Chat Completions for tool calling, Responses API for search)
- Removed MCP integration, confirmation service, token counter, Morph editor
- Updated tsconfig for OpenTUI React JSX
- Clean 10-file architecture in src/

Co-authored-by: Ismail Pelaseyed <homanp@users.noreply.github.com>
This commit is contained in:
Cursor AgentandIsmail Pelaseyed committed 2026-03-17 23:06:55 +00:00
1 parent e763e2f472
commit ccfd3bb6fc
52 files changed
+1349 -8815

No files matched your search

+13 -17
View File
@@ -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.
+38 -25
View File
@@ -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`.
+20 -27
View File
@@ -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.
+8 -2
View File
@@ -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
# 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
+157 -434
View File
@@ -1,93 +1,30 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@vibe-kit/grok-cli",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.0",
"axios": "^1.7.0",
"cfonts": "^3.3.0",
"@opentui/core": "^0.1.88",
"@opentui/react": "^0.1.88",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"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",
},
"devDependencies": {
"@types/fs-extra": "^11.0.2",
"@types/node": "^20.8.0",
"@types/react": "^18.3.3",
"@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=="],
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
"@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=="],
@@ -115,7 +52,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=="],
@@ -123,21 +114,31 @@
"@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=="],
"@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=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@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=="],
@@ -159,7 +160,9 @@
"@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=="],
"@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=="],
@@ -167,130 +170,62 @@
"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=="],
"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.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"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=="],
@@ -309,15 +244,11 @@
"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=="],
@@ -331,9 +262,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=="],
@@ -341,93 +272,37 @@
"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=="],
@@ -437,63 +312,29 @@
"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=="],
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"openai": ["openai@5.20.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "bin": "bin/cli" }, "sha512-UndCB0R5V3iB9I98NyF69zNP6YfwU4+Fjk0eW4HhooTm+Awlpm/MGjJTwJsyNV/qkH1NJi0GG+9odwukGTqExQ=="],
@@ -503,256 +344,138 @@
"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=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"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=="],
"espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"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=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"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=="],
}
}
+16 -28
View File
@@ -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,39 @@
"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",
"@opentui/core": "^0.1.88",
"@opentui/react": "^0.1.88",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"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"
},
"devDependencies": {
"@types/fs-extra": "^11.0.2",
"@types/node": "^20.8.0",
"@types/react": "^18.3.3",
"@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"
+221
View File
@@ -0,0 +1,221 @@
import { GrokClient } from "../grok/client.js";
import { TOOLS } from "../grok/tools.js";
import { BashTool } from "../tools/bash.js";
import { loadCustomInstructions } from "../utils/instructions.js";
import type { ChatEntry, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js";
const MAX_TOOL_ROUNDS = 400;
function buildSystemPrompt(cwd: string): string {
const custom = loadCustomInstructions();
const customSection = custom
? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above custom instructions alongside the standard instructions.\n`
: "";
return `You are Grok CLI, a powerful AI coding agent running in the terminal. You operate directly through bash commands.${customSection}
TOOLS:
- bash: Execute any shell command. Use this for EVERYTHING — viewing files (cat, bat, less), editing files (sed, awk, tee, or write with heredocs), searching code (grep, rg, find), git operations, running builds, installing packages, etc.
- search_web: Search the internet for real-time information, documentation, APIs, current events.
- search_x: Search X (Twitter) for posts, discussions, trends, community sentiment.
WORKFLOW:
1. Understand what the user wants
2. Use bash to explore, read files, and understand the codebase
3. Make changes using bash commands (sed, tee, heredocs, etc.)
4. Verify your changes by reading the modified files
5. Run any relevant tests or builds to confirm correctness
FILE EDITING PATTERNS:
- For small edits: sed -i 's/old/new/g' file
- For creating files: cat > file << 'EOF' ... EOF
- For appending: cat >> file << 'EOF' ... EOF
- For complex edits: Use a combination of sed, awk, or write the entire file with cat/tee
- Always verify edits by reading the file after modification
RESPONSE STYLE:
- Be direct and concise
- Show what you're doing, not what you plan to do
- After tool use, give brief confirmation or note issues
- Use markdown formatting in responses
Current working directory: ${cwd}`;
}
export class Agent {
private client: GrokClient;
private bash: BashTool;
private messages: Message[] = [];
private history: ChatEntry[] = [];
private abortController: AbortController | null = null;
private maxToolRounds: number;
constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) {
this.client = new GrokClient(apiKey, model, baseURL);
this.bash = new BashTool();
this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS;
this.messages.push({
role: "system",
content: buildSystemPrompt(process.cwd()),
});
}
getModel(): string {
return this.client.getModel();
}
setModel(model: string): void {
this.client.setModel(model);
}
getHistory(): ChatEntry[] {
return [...this.history];
}
getCwd(): string {
return this.bash.getCwd();
}
abort(): void {
this.abortController?.abort();
}
clearHistory(): void {
const systemMsg = this.messages[0];
this.messages = [systemMsg];
this.history = [];
}
async *processMessage(userMessage: string): AsyncGenerator<StreamChunk, void, unknown> {
this.abortController = new AbortController();
const userEntry: ChatEntry = {
type: "user",
content: userMessage,
timestamp: new Date(),
};
this.history.push(userEntry);
this.messages.push({ role: "user", content: userMessage });
let toolRounds = 0;
try {
while (toolRounds < this.maxToolRounds) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
}
let accContent = "";
let accToolCalls: ToolCall[] | undefined;
let accReasoning = "";
for await (const delta of this.client.chatStream(this.messages, TOOLS)) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
}
if (delta.content) {
accContent += delta.content;
yield { type: "content", content: delta.content };
}
if (delta.reasoning) {
accReasoning += delta.reasoning;
yield { type: "reasoning", content: delta.reasoning };
}
if (delta.toolCalls) {
accToolCalls = delta.toolCalls;
yield { type: "tool_calls", toolCalls: delta.toolCalls };
}
}
this.messages.push({
role: "assistant",
content: accContent || "",
tool_calls: accToolCalls,
});
this.history.push({
type: "assistant",
content: accContent || "",
timestamp: new Date(),
toolCalls: accToolCalls,
});
if (!accToolCalls || accToolCalls.length === 0) {
break;
}
toolRounds++;
for (const tc of accToolCalls) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
}
const result = await this.executeTool(tc);
yield { type: "tool_result", toolCall: tc, toolResult: result };
this.history.push({
type: "tool_result",
content: result.success ? (result.output || "Success") : (result.error || "Error"),
timestamp: new Date(),
toolCall: tc,
toolResult: result,
});
this.messages.push({
role: "tool",
content: result.success ? (result.output || "Success") : (result.error || "Error"),
tool_call_id: tc.id,
});
}
}
if (toolRounds >= this.maxToolRounds) {
yield { type: "content", content: "\n\nMax tool rounds reached." };
}
yield { type: "done" };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
yield { type: "error", content: `Error: ${msg}` };
yield { type: "done" };
} finally {
this.abortController = null;
}
}
private async executeTool(tc: ToolCall): Promise<ToolResult> {
try {
const args = JSON.parse(tc.function.arguments);
switch (tc.function.name) {
case "bash":
return await this.bash.execute(args.command, args.timeout);
case "search_web":
const webResult = await this.client.searchWeb(args.query);
return { success: true, output: webResult };
case "search_x":
const xResult = await this.client.searchX(args.query);
return { success: true, output: xResult };
default:
return { success: false, error: `Unknown tool: ${tc.function.name}` };
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { success: false, error: `Tool error: ${msg}` };
}
}
}
-780
View File
@@ -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<void> {
// 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<ChatEntry[]> {
// 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<StreamingChunk, void, unknown> {
// 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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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();
}
}
}
-157
View File
@@ -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<ToolResult> {
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 <path> [start-end] - View file contents or directory
str_replace <path> "old" "new" - Replace text in file
create <path> "content" - Create new file with content
insert <path> <line> "text" - Insert text at specific line
undo_edit - Undo last edit operation
bash <command> - Execute bash command
$ <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()
};
}
}
-268
View File
@@ -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 <name>')
.description('Add an MCP server')
.option('-t, --transport <type>', 'Transport type (stdio, http, sse, streamable_http)', 'stdio')
.option('-c, --command <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>', '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<string, string> = {};
for (const envVar of options.env || []) {
const [key, value] = envVar.split('=', 2);
if (key && value) {
env[key] = value;
}
}
// Parse headers
const headers: Record<string, string> = {};
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 <name> <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 <name>')
.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 <name>')
.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;
}
+102 -123
View File
@@ -1,153 +1,132 @@
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat";
import type { ToolDefinition, ToolCall, Message } from "../types/index.js";
import { DEFAULT_MODEL } from "./models.js";
export type GrokMessage = ChatCompletionMessageParam;
export interface GrokTool {
type: "function";
function: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, any>;
required: string[];
};
};
}
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 interface StreamDelta {
content?: string;
toolCalls?: ToolCall[];
reasoning?: string;
finishReason?: string | null;
}
export class GrokClient {
private client: OpenAI;
private currentModel: string = "grok-code-fast-1";
private defaultMaxTokens: number;
private model: string;
private maxTokens: 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,
timeout: 360_000,
});
this.model = model || DEFAULT_MODEL;
const envMax = Number(process.env.GROK_MAX_TOKENS);
this.defaultMaxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 1536;
if (model) {
this.currentModel = model;
}
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
}
getModel(): string {
return this.model;
}
setModel(model: string): void {
this.currentModel = model;
}
getCurrentModel(): string {
return this.currentModel;
}
async chat(
messages: GrokMessage[],
tools?: GrokTool[],
model?: string,
searchOptions?: SearchOptions
): Promise<GrokResponse> {
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}`);
}
this.model = model;
}
async *chatStream(
messages: GrokMessage[],
tools?: GrokTool[],
model?: string,
searchOptions?: SearchOptions
): AsyncGenerator<any, void, unknown> {
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,
};
messages: Message[],
tools: ToolDefinition[],
): AsyncGenerator<StreamDelta, void, unknown> {
const payload: Record<string, unknown> = {
model: this.model,
messages,
temperature: 0.7,
max_tokens: this.maxTokens,
stream: true,
};
// Add search parameters if specified
if (searchOptions?.search_parameters) {
requestPayload.search_parameters = searchOptions.search_parameters;
if (tools.length > 0) {
payload.tools = tools;
payload.tool_choice = "auto";
}
const stream = (await this.client.chat.completions.create(
payload as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,
)) as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>;
const accToolCalls: Record<number, { id: string; name: string; arguments: string }> = {};
for await (const chunk of stream) {
const choice = chunk.choices?.[0];
if (!choice) continue;
const delta: StreamDelta = { finishReason: choice.finish_reason };
if (choice.delta?.content) {
delta.content = choice.delta.content;
}
const stream = (await this.client.chat.completions.create(
requestPayload
)) as any;
for await (const chunk of stream) {
yield chunk;
if ((choice.delta as Record<string, unknown>)?.reasoning) {
delta.reasoning = (choice.delta as Record<string, unknown>).reasoning as string;
}
} catch (error: any) {
throw new Error(`Grok API error: ${error.message}`);
if (choice.delta?.tool_calls) {
for (const tc of choice.delta.tool_calls) {
const idx = tc.index;
if (!accToolCalls[idx]) {
accToolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
}
if (tc.id) accToolCalls[idx].id = tc.id;
if (tc.function?.name) accToolCalls[idx].name += tc.function.name;
if (tc.function?.arguments) accToolCalls[idx].arguments += tc.function.arguments;
}
}
if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
const calls = Object.values(accToolCalls);
if (calls.length > 0) {
delta.toolCalls = calls.map((c) => ({
id: c.id,
type: "function" as const,
function: { name: c.name, arguments: c.arguments },
}));
}
}
yield delta;
}
}
async search(
query: string,
searchParameters?: SearchParameters
): Promise<GrokResponse> {
const searchMessage: GrokMessage = {
role: "user",
content: query,
};
async searchWeb(query: string): Promise<string> {
return this.search(query, "web_search");
}
const searchOptions: SearchOptions = {
search_parameters: searchParameters || { mode: "on" },
};
async searchX(query: string): Promise<string> {
return this.search(query, "x_search");
}
return this.chat([searchMessage], [], undefined, searchOptions);
private async search(query: string, toolType: "web_search" | "x_search"): Promise<string> {
try {
const response = await this.client.responses.create({
model: this.model,
input: [{ role: "user", content: query }],
tools: [{ type: toolType } as unknown as OpenAI.Responses.Tool],
});
const parts: string[] = [];
for (const item of response.output) {
if (item.type === "message") {
for (const content of item.content) {
if (content.type === "output_text") {
parts.push(content.text);
}
}
}
}
return parts.join("\n") || "No results found.";
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return `Search failed: ${msg}`;
}
}
}
+77
View File
@@ -0,0 +1,77 @@
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 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);
}
+19 -331
View File
@@ -1,92 +1,12 @@
import { GrokTool } from "./client.js";
import { MCPManager, MCPTool } from "../mcp/client.js";
import { loadMCPConfig } from "../mcp/config.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"],
},
},
},
import type { ToolDefinition } from "../types/index.js";
export const TOOLS: ToolDefinition[] = [
{
type: "function",
function: {
name: "bash",
description: "Execute a bash command",
description:
"Execute a bash command. Use this for ALL operations: viewing files (cat, less), editing files (sed, tee), searching (grep, rg, find), git, build tools, package managers, and any other shell command. Commands run in the project working directory.",
parameters: {
type: "object",
properties: {
@@ -94,6 +14,11 @@ const BASE_GROK_TOOLS: GrokTool[] = [
type: "string",
description: "The bash command to execute",
},
timeout: {
type: "number",
description:
"Timeout in milliseconds (default: 30000). Use higher values for long-running commands.",
},
},
required: ["command"],
},
@@ -102,57 +27,15 @@ const BASE_GROK_TOOLS: GrokTool[] = [
{
type: "function",
function: {
name: "search",
name: "search_web",
description:
"Unified search tool for finding text content or files (similar to Cursor's search)",
"Search the web for current, real-time information. Returns results from across the internet. Use when the user asks about current events, documentation, APIs, packages, or anything requiring up-to-date information.",
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)",
description: "The search query",
},
},
required: ["query"],
@@ -162,214 +45,19 @@ const BASE_GROK_TOOLS: GrokTool[] = [
{
type: "function",
function: {
name: "create_todo_list",
description: "Create a new todo list for planning and tracking tasks",
name: "search_x",
description:
"Search X (formerly Twitter) for posts, discussions, trends, and real-time conversations. Use when the user asks about social media discussions, trending topics, or community sentiment.",
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"],
},
query: {
type: "string",
description: "The search query",
},
},
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"],
required: ["query"],
},
},
},
];
// 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 ..."
}
},
required: ["target_file", "instructions", "code_edit"]
}
}
};
// 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
}
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<void> {
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<GrokTool[]> {
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);
}
-298
View File
@@ -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,
};
}
-752
View File
@@ -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<React.SetStateAction<ChatEntry[]>>;
setIsProcessing: (processing: boolean) => void;
setIsStreaming: (streaming: boolean) => void;
setTokenCount: (count: number) => void;
setProcessingTime: (time: number) => void;
processingStartTime: React.MutableRefObject<number>;
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<boolean> => {
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 <path> - Change directory
cat <file> - View file contents
mkdir <dir> - Create directory
touch <file>- 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,
};
}
-75
View File
@@ -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<string[]>([]);
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,
};
}
+109 -413
View File
@@ -1,462 +1,158 @@
#!/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<void> {
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<void> {
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: true,
});
console.log("🤖 Processing commit and push...\n");
console.log("> /commit-and-push\n");
// 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 }));
}
// Headless mode processing function
async function processPromptHeadless(
async function runHeadless(
prompt: string,
apiKey: string,
baseURL?: string,
model?: string,
maxToolRounds?: number
): Promise<void> {
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<string, string | undefined>) {
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 <dir>", "set working directory", process.cwd())
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
.option(
"-u, --base-url <url>",
"Grok API base URL (or set GROK_BASE_URL env var)"
)
.option(
"-m, --model <model>",
"AI model to use (e.g., grok-code-fast-1, grok-4-latest) (or set GROK_MODEL env var)"
)
.option(
"-p, --prompt <prompt>",
"process a single prompt and exit (headless mode)"
)
.option(
"--max-tool-rounds <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 <key>", "Grok API key")
.option("-u, --base-url <url>", "API base URL")
.option("-m, --model <model>", "Model to use")
.option("-d, --directory <dir>", "Working directory", process.cwd())
.option("-p, --prompt <prompt>", "Run a single prompt headlessly")
.option("--max-tool-rounds <n>", "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 <dir>", "set working directory", process.cwd())
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
.option(
"-u, --base-url <url>",
"Grok API base URL (or set GROK_BASE_URL env var)"
)
.option(
"-m, --model <model>",
"AI model to use (e.g., grok-code-fast-1, grok-4-latest) (or set GROK_MODEL env var)"
)
.option(
"--max-tool-rounds <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`;
}
-170
View File
@@ -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<string, string>;
}
export interface MCPTool {
name: string;
description: string;
inputSchema: any;
serverName: string;
}
export class MCPManager extends EventEmitter {
private clients: Map<string, Client> = new Map();
private transports: Map<string, MCPTransport> = new Map();
private tools: Map<string, MCPTool> = new Map();
async addServer(config: MCPServerConfig): Promise<void> {
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<void> {
// 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<CallToolResult> {
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<void> {
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<void> {
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);
}
}
-57
View File
@@ -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<string, MCPServerConfig> = {};
// 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<string, MCPServerConfig> = {};
-265
View File
@@ -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<string, string>;
url?: string;
headers?: Record<string, string>;
}
export interface MCPTransport {
connect(): Promise<Transport>;
disconnect(): Promise<void>;
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<Transport> {
// 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<void> {
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<Transport> {
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<void> {
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<Transport> {
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<void> {
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<void> {
// HTTP transport is connection-less, so we're always "started"
}
async close(): Promise<void> {
// Nothing to close for HTTP transport
}
async send(message: any): Promise<any> {
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<void> {
// SSE transport is event-driven, so we're always "started"
}
async close(): Promise<void> {
// Nothing to close for basic SSE transport
}
async send(message: any): Promise<any> {
// 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<Transport> {
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<void> {
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<string, string>) {
super();
}
async start(): Promise<void> {
// Streamable HTTP transport is connection-less, so we're always "started"
}
async close(): Promise<void> {
// Nothing to close for streamable HTTP transport
}
async send(message: any): Promise<any> {
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}`);
}
}
+32 -65
View File
@@ -1,86 +1,53 @@
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 { 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 = process.cwd();
async execute(command: string, timeout: number = 30000): Promise<ToolResult> {
async execute(command: string, timeout = 30_000): Promise<ToolResult> {
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}`
};
process.chdir(dir);
this.cwd = process.cwd();
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<ToolResult> {
return this.execute(`ls -la ${directory}`);
}
async findFiles(pattern: string, directory: string = '.'): Promise<ToolResult> {
return this.execute(`find ${directory} -name "${pattern}" -type f`);
}
async grep(pattern: string, files: string = '.'): Promise<ToolResult> {
return this.execute(`grep -r "${pattern}" ${files}`);
}
}
}
-86
View File
@@ -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<ToolResult> {
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<ToolResult> {
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();
}
}
-6
View File
@@ -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";
-393
View File
@@ -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<ToolResult> {
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<string> {
try {
const response = await axios.post(`${this.morphBaseUrl}/chat/completions`, {
model: "morph-v3-large",
messages: [
{
role: "user",
content: `<instruction>${instructions}</instruction>\n<code>${initialCode}</code>\n<update>${editSnippet}</update>`,
},
],
}, {
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<ToolResult> {
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;
}
}
-443
View File
@@ -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<ToolResult> {
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<SearchResult[]> {
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<FileSearchResult[]> {
const files: FileSearchResult[] = [];
const maxResults = options.maxResults || 50;
const searchPattern = pattern.toLowerCase();
const walkDir = async (dir: string, depth: number = 0): Promise<void> => {
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<string>();
// 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;
}
}
-704
View File
@@ -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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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];
}
}
-154
View File
@@ -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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
return {
success: true,
output: this.formatTodoList()
};
}
}
+50 -22
View File
@@ -2,33 +2,61 @@ export interface ToolResult {
success: boolean;
output?: string;
error?: string;
data?: any;
}
export interface Tool {
name: string;
description: string;
execute: (...args: any[]) => Promise<ToolResult>;
export interface Message {
role: "system" | "user" | "assistant" | "tool";
content: string;
tool_calls?: ToolCall[];
tool_call_id?: string;
}
export interface EditorCommand {
command: 'view' | 'str_replace' | 'create' | 'insert' | 'undo_edit';
path?: string;
old_str?: string;
new_str?: string;
export interface ToolCall {
id: string;
type: "function";
function: {
name: string;
arguments: string;
};
}
export interface ToolDefinition {
type: "function";
function: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, unknown>;
required: string[];
};
};
}
export interface ChatEntry {
type: "user" | "assistant" | "tool_call" | "tool_result" | "search_result";
content: string;
timestamp: Date;
toolCalls?: ToolCall[];
toolCall?: ToolCall;
toolResult?: ToolResult;
isStreaming?: boolean;
}
export interface StreamChunk {
type: "content" | "tool_calls" | "tool_result" | "search_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;
}
+376 -135
View File
@@ -1,162 +1,403 @@
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 { useState, useEffect, useCallback, useRef } from "react";
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
import type { InputRenderable, ScrollBoxRenderable } from "@opentui/core";
import type { Agent } from "../agent/agent.js";
import type { ChatEntry, ToolCall, StreamChunk } from "../types/index.js";
import { getModelInfo } from "../grok/models.js";
import { getAvailableModels, saveProjectSettings } from "../utils/settings.js";
interface Props {
interface AppProps {
agent: Agent;
initialMessage?: string;
}
export default function App({ agent }: Props) {
const [input, setInput] = useState('');
const [history, setHistory] = useState<Array<{ command: string; result: ToolResult }>>([]);
export function App({ agent, initialMessage }: AppProps) {
const [messages, setMessages] = useState<ChatEntry[]>([]);
const [streamContent, setStreamContent] = useState("");
const [streamReasoning, setStreamReasoning] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [confirmationOptions, setConfirmationOptions] = useState<ConfirmationOptions | null>(null);
// Removed useApp().exit - using process.exit(0) instead for better terminal handling
const confirmationService = ConfirmationService.getInstance();
const [inputValue, setInputValue] = useState("");
const [model, setModel] = useState(agent.getModel());
const [showModelPicker, setShowModelPicker] = useState(false);
const [modelPickerIndex, setModelPickerIndex] = useState(0);
const [showHelp, setShowHelp] = useState(false);
const [activeToolCalls, setActiveToolCalls] = useState<ToolCall[]>([]);
const inputRef = useRef<InputRenderable>(null);
const scrollRef = useRef<ScrollBoxRenderable>(null);
const { width, height } = useTerminalDimensions();
const processedInitial = useRef(false);
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 scrollToBottom = useCallback(() => {
try {
scrollRef.current?.scrollTo({ y: "end" } as any);
} catch {
// ignore scroll errors
}
}, []);
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;
}
const processMessage = useCallback(
async (text: string) => {
if (!text.trim() || isProcessing) return;
if (key.return) {
if (input.trim() === 'exit' || input.trim() === 'quit') {
setIsProcessing(true);
setStreamContent("");
setStreamReasoning("");
setActiveToolCalls([]);
setMessages((prev) => [
...prev,
{ type: "user", content: text.trim(), timestamp: new Date() },
]);
setTimeout(scrollToBottom, 50);
try {
for await (const chunk of agent.processMessage(text.trim())) {
switch (chunk.type) {
case "content":
setStreamContent((prev) => prev + (chunk.content || ""));
setTimeout(scrollToBottom, 10);
break;
case "reasoning":
setStreamReasoning((prev) => prev + (chunk.content || ""));
break;
case "tool_calls":
if (chunk.toolCalls) {
setActiveToolCalls(chunk.toolCalls);
}
break;
case "tool_result":
if (chunk.toolCall && chunk.toolResult) {
setMessages((prev) => [
...prev,
{
type: "tool_result",
content: chunk.toolResult!.success
? chunk.toolResult!.output || "Success"
: chunk.toolResult!.error || "Error",
timestamp: new Date(),
toolCall: chunk.toolCall,
toolResult: chunk.toolResult,
},
]);
setActiveToolCalls([]);
setTimeout(scrollToBottom, 10);
}
break;
case "error":
setStreamContent((prev) => prev + `\n${chunk.content || "Unknown error"}`);
break;
case "done":
break;
}
}
} catch {
setStreamContent((prev) => prev + "\nAn unexpected error occurred.");
}
const finalContent = (() => {
let c = "";
setStreamContent((prev) => {
c = prev;
return prev;
});
return c;
})();
if (finalContent.trim()) {
setMessages((prev) => [
...prev,
{ type: "assistant", content: finalContent, timestamp: new Date() },
]);
}
setStreamContent("");
setStreamReasoning("");
setActiveToolCalls([]);
setIsProcessing(false);
setTimeout(scrollToBottom, 50);
},
[agent, isProcessing, scrollToBottom],
);
useEffect(() => {
if (initialMessage && !processedInitial.current) {
processedInitial.current = true;
processMessage(initialMessage);
}
}, [initialMessage, processMessage]);
const handleCommand = useCallback(
(cmd: string): boolean => {
const c = cmd.trim().toLowerCase();
if (c === "/help" || c === "/?") {
setShowHelp((v) => !v);
return true;
}
if (c === "/clear") {
agent.clearHistory();
setMessages([]);
setStreamContent("");
return true;
}
if (c === "/model" || c === "/models") {
setShowModelPicker(true);
setModelPickerIndex(0);
return true;
}
if (c === "/quit" || c === "/exit" || c === "/q") {
process.exit(0);
}
return false;
},
[agent],
);
useKeyboard((key) => {
if (showModelPicker) {
const models = getAvailableModels();
if (key.name === "escape") {
setShowModelPicker(false);
return;
}
if (input.trim()) {
setIsProcessing(true);
const result = await agent.processCommand(input.trim());
setHistory(prev => [...prev, { command: input.trim(), result }]);
setInput('');
setIsProcessing(false);
if (key.name === "up") {
setModelPickerIndex((i) => Math.max(0, i - 1));
return;
}
if (key.name === "down") {
setModelPickerIndex((i) => Math.min(models.length - 1, i + 1));
return;
}
if (key.name === "return") {
const selected = models[modelPickerIndex];
if (selected) {
agent.setModel(selected);
setModel(selected);
saveProjectSettings({ model: selected });
}
setShowModelPicker(false);
return;
}
return;
}
if (key.backspace || key.delete) {
setInput(prev => prev.slice(0, -1));
if (showHelp && key.name === "escape") {
setShowHelp(false);
return;
}
if (inputChar && !key.ctrl && !key.meta) {
setInput(prev => prev + inputChar);
if (isProcessing && key.name === "escape") {
agent.abort();
return;
}
});
const renderResult = (result: ToolResult) => {
if (result.success) {
return (
<Box flexDirection="column" marginBottom={1}>
<Text color="green">✓ Success</Text>
{result.output && (
<Box marginLeft={2}>
<Text>{result.output}</Text>
</Box>
)}
</Box>
);
} else {
return (
<Box flexDirection="column" marginBottom={1}>
<Text color="red">✗ Error</Text>
{result.error && (
<Box marginLeft={2}>
<Text color="red">{result.error}</Text>
</Box>
)}
</Box>
);
}
};
const handleSubmit = useCallback(
() => {
const value = inputRef.current?.value || "";
if (!value.trim()) return;
if (inputRef.current) inputRef.current.value = "";
if (handleCommand(value)) return;
processMessage(value);
},
[handleCommand, processMessage],
);
const handleConfirmation = (dontAskAgain?: boolean) => {
confirmationService.confirmOperation(true, dontAskAgain);
setConfirmationOptions(null);
};
const handleRejection = (feedback?: string) => {
confirmationService.rejectOperation(feedback);
setConfirmationOptions(null);
};
if (confirmationOptions) {
return (
<ConfirmationDialog
operation={confirmationOptions.operation}
filename={confirmationOptions.filename}
showVSCodeOpen={confirmationOptions.showVSCodeOpen}
onConfirm={handleConfirmation}
onReject={handleRejection}
/>
);
if (showModelPicker) {
return <ModelPicker currentModel={model} selectedIndex={modelPickerIndex} />;
}
return (
<Box flexDirection="column" padding={1}>
<Box marginBottom={1}>
<Text bold color="cyan">
🔧 Grok CLI - Text Editor Agent
</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text dimColor>
Available commands: view, str_replace, create, insert, undo_edit, bash, help
</Text>
<Text dimColor>
Type 'help' for detailed usage, 'exit' or Ctrl+C to quit
</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
{history.slice(-10).map((entry, index) => (
<Box key={index} flexDirection="column" marginBottom={1}>
<Box>
<Text color="blue">$ </Text>
<Text>{entry.command}</Text>
</Box>
{renderResult(entry.result)}
</Box>
))}
</Box>
<Box>
<Text color="blue">$ </Text>
<Text>
{input}
{!isProcessing && <Text color="white">█</Text>}
</Text>
{isProcessing && <Text color="yellow"> (processing...)</Text>}
</Box>
</Box>
<box flexDirection="column" width="100%" height="100%">
<StatusBar model={model} cwd={agent.getCwd()} isProcessing={isProcessing} />
<scrollbox
ref={scrollRef}
flexGrow={1}
flexShrink={1}
>
<box flexDirection="column" gap={1} padding={1}>
{showHelp && <HelpPanel />}
{messages.map((msg, i) => (
<MessageView key={i} entry={msg} />
))}
{activeToolCalls.length > 0 && (
<box flexDirection="column">
{activeToolCalls.map((tc, i) => (
<text key={i} fg="#facc15">
{`⚙ ${tc.function.name}(${truncate(getToolArgs(tc), 120)})`}
</text>
))}
</box>
)}
{streamReasoning && (
<box>
<text fg="#8b5cf6">{`💭 ${truncate(streamReasoning, 200)}`}</text>
</box>
)}
{streamContent && (
<box>
<text fg="#e2e8f0">{streamContent}</text>
</box>
)}
{isProcessing && !streamContent && activeToolCalls.length === 0 && (
<text fg="#a78bfa">{"⏳ Thinking..."}</text>
)}
</box>
</scrollbox>
<box
border
borderStyle="rounded"
borderColor={isProcessing ? "#6b7280" : "#3b82f6"}
padding={0}
>
<input
ref={inputRef}
focused={!isProcessing && !showModelPicker}
placeholder={isProcessing ? "Processing... (Esc to cancel)" : "Type a message... (/help for commands)"}
onSubmit={handleSubmit as any}
/>
</box>
</box>
);
}
}
function StatusBar({ model, cwd, isProcessing }: { model: string; cwd: string; isProcessing: boolean }) {
const info = getModelInfo(model);
const modelName = info?.name || model;
return (
<box
flexDirection="row"
justifyContent="space-between"
backgroundColor="#1e293b"
padding={0}
>
<text fg="#60a5fa">
<b>{" GROK CLI "}</b>
</text>
<text fg="#94a3b8">{` ${cwd} `}</text>
<text fg={isProcessing ? "#facc15" : "#34d399"}>
{` ${modelName} ${isProcessing ? "⏳" : "●"} `}
</text>
</box>
);
}
function MessageView({ entry }: { entry: ChatEntry }) {
switch (entry.type) {
case "user":
return (
<box flexDirection="column">
<text fg="#60a5fa"><b>{"❯ You"}</b></text>
<text content={entry.content} />
</box>
);
case "assistant":
return (
<box flexDirection="column">
<text fg="#34d399"><b>{"✦ Grok"}</b></text>
<text fg="#e2e8f0">{entry.content}</text>
</box>
);
case "tool_result": {
const name = entry.toolCall?.function.name || "tool";
const success = entry.toolResult?.success ?? true;
const icon = success ? "✓" : "✗";
const color = success ? "#34d399" : "#ef4444";
const output = truncate(entry.content, 500);
return (
<box flexDirection="column">
<text fg={color}>{`${icon} ${name}`}</text>
{output && (
<box>
<text fg="#9ca3af">{output}</text>
</box>
)}
</box>
);
}
default:
return <text fg="#6b7280">{entry.content}</text>;
}
}
function HelpPanel() {
return (
<box
border
borderStyle="rounded"
borderColor="#6366f1"
flexDirection="column"
padding={1}
title=" Help "
titleAlignment="center"
>
<text fg="#e2e8f0"><b>{"Commands"}</b></text>
<text fg="#94a3b8">{"/model — Switch model"}</text>
<text fg="#94a3b8">{"/clear — Clear chat history"}</text>
<text fg="#94a3b8">{"/help — Toggle this help"}</text>
<text fg="#94a3b8">{"/quit — Exit"}</text>
<br />
<text fg="#e2e8f0"><b>{"Shortcuts"}</b></text>
<text fg="#94a3b8">{"Esc — Cancel current operation"}</text>
<text fg="#94a3b8">{"Enter — Send message"}</text>
</box>
);
}
function ModelPicker({ currentModel, selectedIndex }: { currentModel: string; selectedIndex: number }) {
const models = getAvailableModels();
return (
<box flexDirection="column" width="100%" height="100%">
<box
border
borderStyle="rounded"
borderColor="#6366f1"
flexDirection="column"
padding={1}
title=" Select Model "
titleAlignment="center"
flexGrow={1}
>
<text fg="#94a3b8">{"Use ↑/↓ to select, Enter to confirm, Esc to cancel\n"}</text>
{models.map((m, i) => {
const isSelected = i === selectedIndex;
const isCurrent = m === currentModel;
const info = getModelInfo(m);
const prefix = isSelected ? "❯ " : " ";
const suffix = isCurrent ? " (current)" : "";
const desc = info ? ` — ${info.description}` : "";
return (
<text
key={i}
fg={isSelected ? "#60a5fa" : isCurrent ? "#34d399" : "#e2e8f0"}
>
{`${prefix}${m}${suffix}${desc}`}
</text>
);
})}
</box>
</box>
);
}
function truncate(s: string, max: number): string {
if (s.length <= max) return s;
return s.slice(0, max) + "…";
}
function getToolArgs(tc: ToolCall): string {
try {
const args = JSON.parse(tc.function.arguments);
if (tc.function.name === "bash") return args.command || "";
return args.query || JSON.stringify(args);
} catch {
return tc.function.arguments;
}
}
-109
View File
@@ -1,109 +0,0 @@
import React, { useState } from "react";
import { Box, Text, useInput, useApp } from "ink";
import { GrokAgent } from "../../agent/grok-agent.js";
import { getSettingsManager } from "../../utils/settings-manager.js";
interface ApiKeyInputProps {
onApiKeySet: (agent: GrokAgent) => void;
}
export default function ApiKeyInput({ onApiKeySet }: ApiKeyInputProps) {
const [input, setInput] = useState("");
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const { exit } = useApp();
useInput((inputChar, key) => {
if (isSubmitting) return;
if (key.ctrl && inputChar === "c") {
exit();
return;
}
if (key.return) {
handleSubmit();
return;
}
if (key.backspace || key.delete) {
setInput((prev) => prev.slice(0, -1));
setError("");
return;
}
if (inputChar && !key.ctrl && !key.meta) {
setInput((prev) => prev + inputChar);
setError("");
}
});
const handleSubmit = async () => {
if (!input.trim()) {
setError("API key cannot be empty");
return;
}
setIsSubmitting(true);
try {
const apiKey = input.trim();
const agent = new GrokAgent(apiKey);
// Set environment variable for current process
process.env.GROK_API_KEY = apiKey;
// Save to user settings
try {
const manager = getSettingsManager();
manager.updateUserSetting('apiKey', apiKey);
console.log(`\n✅ API key saved to ~/.grok/user-settings.json`);
} catch (error) {
console.log('\n⚠️ Could not save API key to settings file');
console.log('API key set for current session only');
}
onApiKeySet(agent);
} catch (error: any) {
setError("Invalid API key format");
setIsSubmitting(false);
}
};
const displayText = input.length > 0 ?
(isSubmitting ? "*".repeat(input.length) : "*".repeat(input.length) + "█") :
(isSubmitting ? " " : "█");
return (
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text color="yellow">🔑 Grok API Key Required</Text>
<Box marginBottom={1}>
<Text color="gray">Please enter your Grok API key to continue:</Text>
</Box>
<Box borderStyle="round" borderColor="blue" paddingX={1} marginBottom={1}>
<Text color="gray">❯ </Text>
<Text>{displayText}</Text>
</Box>
{error ? (
<Box marginBottom={1}>
<Text color="red">❌ {error}</Text>
</Box>
) : null}
<Box flexDirection="column" marginTop={1}>
<Text color="gray" dimColor>• Press Enter to submit</Text>
<Text color="gray" dimColor>• Press Ctrl+C to exit</Text>
<Text color="gray" dimColor>Note: API key will be saved to ~/.grok/user-settings.json</Text>
</Box>
{isSubmitting ? (
<Box marginTop={1}>
<Text color="yellow">🔄 Validating API key...</Text>
</Box>
) : null}
</Box>
);
}
-233
View File
@@ -1,233 +0,0 @@
import React from "react";
import { Box, Text } from "ink";
import { ChatEntry } from "../../agent/grok-agent.js";
import { DiffRenderer } from "./diff-renderer.js";
import { MarkdownRenderer } from "../utils/markdown-renderer.js";
interface ChatHistoryProps {
entries: ChatEntry[];
isConfirmationActive?: boolean;
}
// Memoized ChatEntry component to prevent unnecessary re-renders
const MemoizedChatEntry = React.memo(
({ entry, index }: { entry: ChatEntry; index: number }) => {
const renderDiff = (diffContent: string, filename?: string) => {
return (
<DiffRenderer
diffContent={diffContent}
filename={filename}
terminalWidth={80}
/>
);
};
const renderFileContent = (content: string) => {
const lines = content.split("\n");
// Calculate minimum indentation like DiffRenderer does
let baseIndentation = Infinity;
for (const line of lines) {
if (line.trim() === "") continue;
const firstCharIndex = line.search(/\S/);
const currentIndent = firstCharIndex === -1 ? 0 : firstCharIndex;
baseIndentation = Math.min(baseIndentation, currentIndent);
}
if (!isFinite(baseIndentation)) {
baseIndentation = 0;
}
return lines.map((line, index) => {
const displayContent = line.substring(baseIndentation);
return (
<Text key={index} color="gray">
{displayContent}
</Text>
);
});
};
switch (entry.type) {
case "user":
return (
<Box key={index} flexDirection="column" marginTop={1}>
<Box>
<Text color="gray">
{">"} {entry.content}
</Text>
</Box>
</Box>
);
case "assistant":
return (
<Box key={index} flexDirection="column" marginTop={1}>
<Box flexDirection="row" alignItems="flex-start">
<Text color="white">⏺ </Text>
<Box flexDirection="column" flexGrow={1}>
{entry.toolCalls ? (
// If there are tool calls, just show plain text
<Text color="white">{entry.content.trim()}</Text>
) : (
// If no tool calls, render as markdown
<MarkdownRenderer content={entry.content.trim()} />
)}
{entry.isStreaming && <Text color="cyan">█</Text>}
</Box>
</Box>
</Box>
);
case "tool_call":
case "tool_result":
const getToolActionName = (toolName: string) => {
// Handle MCP tools with mcp__servername__toolname format
if (toolName.startsWith("mcp__")) {
const parts = toolName.split("__");
if (parts.length >= 3) {
const serverName = parts[1];
const actualToolName = parts.slice(2).join("__");
return `${serverName.charAt(0).toUpperCase() + serverName.slice(1)}(${actualToolName.replace(/_/g, " ")})`;
}
}
switch (toolName) {
case "view_file":
return "Read";
case "str_replace_editor":
return "Update";
case "create_file":
return "Create";
case "bash":
return "Bash";
case "search":
return "Search";
case "create_todo_list":
return "Created Todo";
case "update_todo_list":
return "Updated Todo";
default:
return "Tool";
}
};
const toolName = entry.toolCall?.function?.name || "unknown";
const actionName = getToolActionName(toolName);
const getFilePath = (toolCall: any) => {
if (toolCall?.function?.arguments) {
try {
const args = JSON.parse(toolCall.function.arguments);
if (toolCall.function.name === "search") {
return args.query;
}
return args.path || args.file_path || args.command || "";
} catch {
return "";
}
}
return "";
};
const filePath = getFilePath(entry.toolCall);
const isExecuting = entry.type === "tool_call" || !entry.toolResult;
// Format JSON content for better readability
const formatToolContent = (content: string, toolName: string) => {
if (toolName.startsWith("mcp__")) {
try {
// Try to parse as JSON and format it
const parsed = JSON.parse(content);
if (Array.isArray(parsed)) {
// For arrays, show a summary instead of full JSON
return `Found ${parsed.length} items`;
} else if (typeof parsed === 'object') {
// For objects, show a formatted version
return JSON.stringify(parsed, null, 2);
}
} catch {
// If not JSON, return as is
return content;
}
}
return content;
};
const shouldShowDiff =
entry.toolCall?.function?.name === "str_replace_editor" &&
entry.toolResult?.success &&
entry.content.includes("Updated") &&
entry.content.includes("---") &&
entry.content.includes("+++");
const shouldShowFileContent =
(entry.toolCall?.function?.name === "view_file" ||
entry.toolCall?.function?.name === "create_file") &&
entry.toolResult?.success &&
!shouldShowDiff;
return (
<Box key={index} flexDirection="column" marginTop={1}>
<Box>
<Text color="magenta">⏺</Text>
<Text color="white">
{" "}
{filePath ? `${actionName}(${filePath})` : actionName}
</Text>
</Box>
<Box marginLeft={2} flexDirection="column">
{isExecuting ? (
<Text color="cyan">⎿ Executing...</Text>
) : shouldShowFileContent ? (
<Box flexDirection="column">
<Text color="gray">⎿ File contents:</Text>
<Box marginLeft={2} flexDirection="column">
{renderFileContent(entry.content)}
</Box>
</Box>
) : shouldShowDiff ? (
// For diff results, show only the summary line, not the raw content
<Text color="gray">⎿ {entry.content.split("\n")[0]}</Text>
) : (
<Text color="gray">⎿ {formatToolContent(entry.content, toolName)}</Text>
)}
</Box>
{shouldShowDiff && !isExecuting && (
<Box marginLeft={4} flexDirection="column">
{renderDiff(entry.content, filePath)}
</Box>
)}
</Box>
);
default:
return null;
}
}
);
MemoizedChatEntry.displayName = "MemoizedChatEntry";
export function ChatHistory({
entries,
isConfirmationActive = false,
}: ChatHistoryProps) {
// Filter out tool_call entries with "Executing..." when confirmation is active
const filteredEntries = isConfirmationActive
? entries.filter(
(entry) =>
!(entry.type === "tool_call" && entry.content === "Executing...")
)
: entries;
return (
<Box flexDirection="column">
{filteredEntries.slice(-20).map((entry, index) => (
<MemoizedChatEntry
key={`${entry.timestamp.getTime()}-${index}`}
entry={entry}
index={index}
/>
))}
</Box>
);
}
-132
View File
@@ -1,132 +0,0 @@
import React from "react";
import { Box, Text } from "ink";
interface ChatInputProps {
input: string;
cursorPosition: number;
isProcessing: boolean;
isStreaming: boolean;
}
export function ChatInput({
input,
cursorPosition,
isProcessing,
isStreaming,
}: ChatInputProps) {
const beforeCursor = input.slice(0, cursorPosition);
const afterCursor = input.slice(cursorPosition);
// Handle multiline input display
const lines = input.split("\n");
const isMultiline = lines.length > 1;
// Calculate cursor position across lines
let currentLineIndex = 0;
let currentCharIndex = 0;
let totalChars = 0;
for (let i = 0; i < lines.length; i++) {
if (totalChars + lines[i].length >= cursorPosition) {
currentLineIndex = i;
currentCharIndex = cursorPosition - totalChars;
break;
}
totalChars += lines[i].length + 1; // +1 for newline
}
const showCursor = !isProcessing && !isStreaming;
const borderColor = isProcessing || isStreaming ? "yellow" : "blue";
const promptColor = "cyan";
// Display placeholder when input is empty
const placeholderText = "Ask me anything...";
const isPlaceholder = !input;
if (isMultiline) {
return (
<Box
borderStyle="round"
borderColor={borderColor}
paddingY={0}
marginTop={1}
>
{lines.map((line, index) => {
const isCurrentLine = index === currentLineIndex;
const promptChar = index === 0 ? "❯" : "│";
if (isCurrentLine) {
const beforeCursorInLine = line.slice(0, currentCharIndex);
const cursorChar =
line.slice(currentCharIndex, currentCharIndex + 1) || " ";
const afterCursorInLine = line.slice(currentCharIndex + 1);
return (
<Box key={index}>
<Text color={promptColor}>{promptChar} </Text>
<Text>
{beforeCursorInLine}
{showCursor && (
<Text backgroundColor="white" color="black">
{cursorChar}
</Text>
)}
{!showCursor && cursorChar !== " " && cursorChar}
{afterCursorInLine}
</Text>
</Box>
);
} else {
return (
<Box key={index}>
<Text color={promptColor}>{promptChar} </Text>
<Text>{line}</Text>
</Box>
);
}
})}
</Box>
);
}
// Single line input box
const cursorChar = input.slice(cursorPosition, cursorPosition + 1) || " ";
const afterCursorText = input.slice(cursorPosition + 1);
return (
<Box
borderStyle="round"
borderColor={borderColor}
paddingX={1}
paddingY={0}
marginTop={1}
>
<Box>
<Text color={promptColor}>❯ </Text>
{isPlaceholder ? (
<>
<Text color="gray" dimColor>
{placeholderText}
</Text>
{showCursor && (
<Text backgroundColor="white" color="black">
{" "}
</Text>
)}
</>
) : (
<Text>
{beforeCursor}
{showCursor && (
<Text backgroundColor="white" color="black">
{cursorChar}
</Text>
)}
{!showCursor && cursorChar !== " " && cursorChar}
{afterCursorText}
</Text>
)}
</Box>
</Box>
);
}
-415
View File
@@ -1,415 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { Box, Text } from "ink";
import { GrokAgent, ChatEntry } from "../../agent/grok-agent.js";
import { useInputHandler } from "../../hooks/use-input-handler.js";
import { LoadingSpinner } from "./loading-spinner.js";
import { CommandSuggestions } from "./command-suggestions.js";
import { ModelSelection } from "./model-selection.js";
import { ChatHistory } from "./chat-history.js";
import { ChatInput } from "./chat-input.js";
import { MCPStatus } from "./mcp-status.js";
import ConfirmationDialog from "./confirmation-dialog.js";
import {
ConfirmationService,
ConfirmationOptions,
} from "../../utils/confirmation-service.js";
import ApiKeyInput from "./api-key-input.js";
import cfonts from "cfonts";
interface ChatInterfaceProps {
agent?: GrokAgent;
initialMessage?: string;
}
// Main chat component that handles input when agent is available
function ChatInterfaceWithAgent({
agent,
initialMessage,
}: {
agent: GrokAgent;
initialMessage?: string;
}) {
const [chatHistory, setChatHistory] = useState<ChatEntry[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [processingTime, setProcessingTime] = useState(0);
const [tokenCount, setTokenCount] = useState(0);
const [isStreaming, setIsStreaming] = useState(false);
const [confirmationOptions, setConfirmationOptions] =
useState<ConfirmationOptions | null>(null);
const scrollRef = useRef<any>();
const processingStartTime = useRef<number>(0);
const confirmationService = ConfirmationService.getInstance();
const {
input,
cursorPosition,
showCommandSuggestions,
selectedCommandIndex,
showModelSelection,
selectedModelIndex,
commandSuggestions,
availableModels,
autoEditEnabled,
} = useInputHandler({
agent,
chatHistory,
setChatHistory,
setIsProcessing,
setIsStreaming,
setTokenCount,
setProcessingTime,
processingStartTime,
isProcessing,
isStreaming,
isConfirmationActive: !!confirmationOptions,
});
useEffect(() => {
// Only clear console on non-Windows platforms or if not PowerShell
// Windows PowerShell can have issues with console.clear() causing flickering
const isWindows = process.platform === "win32";
const isPowerShell =
process.env.ComSpec?.toLowerCase().includes("powershell") ||
process.env.PSModulePath !== undefined;
if (!isWindows || !isPowerShell) {
console.clear();
}
// Add top padding
console.log(" ");
// Generate logo with margin to match Ink paddingX={2}
const logoOutput = cfonts.render("GROK", {
font: "3d",
align: "left",
colors: ["magenta", "gray"],
space: true,
maxLength: "0",
gradient: ["magenta", "cyan"],
independentGradient: false,
transitionGradient: true,
env: "node",
});
// Add horizontal margin (2 spaces) to match Ink paddingX={2}
const logoLines = (logoOutput as any).string.split("\n");
logoLines.forEach((line: string) => {
if (line.trim()) {
console.log(" " + line); // Add 2 spaces for horizontal margin
} else {
console.log(line); // Keep empty lines as-is
}
});
console.log(" "); // Spacing after logo
setChatHistory([]);
}, []);
// Process initial message if provided (streaming for faster feedback)
useEffect(() => {
if (initialMessage && agent) {
const userEntry: ChatEntry = {
type: "user",
content: initialMessage,
timestamp: new Date(),
};
setChatHistory([userEntry]);
const processInitialMessage = async () => {
setIsProcessing(true);
setIsStreaming(true);
try {
let streamingEntry: ChatEntry | null = null;
for await (const chunk of agent.processUserMessageStream(initialMessage)) {
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 };
}
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;
};
processInitialMessage();
}
}, [initialMessage, agent]);
useEffect(() => {
const handleConfirmationRequest = (options: ConfirmationOptions) => {
setConfirmationOptions(options);
};
confirmationService.on("confirmation-requested", handleConfirmationRequest);
return () => {
confirmationService.off(
"confirmation-requested",
handleConfirmationRequest
);
};
}, [confirmationService]);
useEffect(() => {
if (!isProcessing && !isStreaming) {
setProcessingTime(0);
return;
}
if (processingStartTime.current === 0) {
processingStartTime.current = Date.now();
}
const interval = setInterval(() => {
setProcessingTime(
Math.floor((Date.now() - processingStartTime.current) / 1000)
);
}, 1000);
return () => clearInterval(interval);
}, [isProcessing, isStreaming]);
const handleConfirmation = (dontAskAgain?: boolean) => {
confirmationService.confirmOperation(true, dontAskAgain);
setConfirmationOptions(null);
};
const handleRejection = (feedback?: string) => {
confirmationService.rejectOperation(feedback);
setConfirmationOptions(null);
// Reset processing states when operation is cancelled
setIsProcessing(false);
setIsStreaming(false);
setTokenCount(0);
setProcessingTime(0);
processingStartTime.current = 0;
};
return (
<Box flexDirection="column" paddingX={2}>
{/* Show tips only when no chat history and no confirmation dialog */}
{chatHistory.length === 0 && !confirmationOptions && (
<Box flexDirection="column" marginBottom={2}>
<Text color="cyan" bold>
Tips for getting started:
</Text>
<Box marginTop={1} flexDirection="column">
<Text color="gray">
1. Ask questions, edit files, or run commands.
</Text>
<Text color="gray">2. Be specific for the best results.</Text>
<Text color="gray">
3. Create GROK.md files to customize your interactions with Grok.
</Text>
<Text color="gray">
4. Press Shift+Tab to toggle auto-edit mode.
</Text>
<Text color="gray">5. /help for more information.</Text>
</Box>
</Box>
)}
<Box flexDirection="column" marginBottom={1}>
<Text color="gray">
Type your request in natural language. Ctrl+C to clear, 'exit' to
quit.
</Text>
</Box>
<Box flexDirection="column" ref={scrollRef}>
<ChatHistory
entries={chatHistory}
isConfirmationActive={!!confirmationOptions}
/>
</Box>
{/* Show confirmation dialog if one is pending */}
{confirmationOptions && (
<ConfirmationDialog
operation={confirmationOptions.operation}
filename={confirmationOptions.filename}
showVSCodeOpen={confirmationOptions.showVSCodeOpen}
content={confirmationOptions.content}
onConfirm={handleConfirmation}
onReject={handleRejection}
/>
)}
{!confirmationOptions && (
<>
<LoadingSpinner
isActive={isProcessing || isStreaming}
processingTime={processingTime}
tokenCount={tokenCount}
/>
<ChatInput
input={input}
cursorPosition={cursorPosition}
isProcessing={isProcessing}
isStreaming={isStreaming}
/>
<Box flexDirection="row" marginTop={1}>
<Box marginRight={2}>
<Text color="cyan">
{autoEditEnabled ? "▶" : "⏸"} auto-edit:{" "}
{autoEditEnabled ? "on" : "off"}
</Text>
<Text color="gray" dimColor>
{" "}
(shift + tab)
</Text>
</Box>
<Box marginRight={2}>
<Text color="yellow">≋ {agent.getCurrentModel()}</Text>
</Box>
<MCPStatus />
</Box>
<CommandSuggestions
suggestions={commandSuggestions}
input={input}
selectedIndex={selectedCommandIndex}
isVisible={showCommandSuggestions}
/>
<ModelSelection
models={availableModels}
selectedIndex={selectedModelIndex}
isVisible={showModelSelection}
currentModel={agent.getCurrentModel()}
/>
</>
)}
</Box>
);
}
// Main component that handles API key input or chat interface
export default function ChatInterface({
agent,
initialMessage,
}: ChatInterfaceProps) {
const [currentAgent, setCurrentAgent] = useState<GrokAgent | null>(
agent || null
);
const handleApiKeySet = (newAgent: GrokAgent) => {
setCurrentAgent(newAgent);
};
if (!currentAgent) {
return <ApiKeyInput onApiKeySet={handleApiKeySet} />;
}
return (
<ChatInterfaceWithAgent
agent={currentAgent}
initialMessage={initialMessage}
/>
);
}
-63
View File
@@ -1,63 +0,0 @@
import React, { useMemo } from "react";
import { Box, Text } from "ink";
interface CommandSuggestion {
command: string;
description: string;
}
interface CommandSuggestionsProps {
suggestions: CommandSuggestion[];
input: string;
selectedIndex: number;
isVisible: boolean;
}
export const MAX_SUGGESTIONS = 8;
export function filterCommandSuggestions<T extends { command: string }>(
suggestions: T[],
input: string
): T[] {
const lowerInput = input.toLowerCase();
return suggestions
.filter((s) => s.command.toLowerCase().startsWith(lowerInput))
.slice(0, MAX_SUGGESTIONS);
}
export function CommandSuggestions({
suggestions,
input,
selectedIndex,
isVisible,
}: CommandSuggestionsProps) {
if (!isVisible) return null;
const filteredSuggestions = useMemo(
() => filterCommandSuggestions(suggestions, input),
[suggestions, input]
);
return (
<Box marginTop={1} flexDirection="column">
{filteredSuggestions.map((suggestion, index) => (
<Box key={index} paddingLeft={1}>
<Text
color={index === selectedIndex ? "black" : "white"}
backgroundColor={index === selectedIndex ? "cyan" : undefined}
>
{suggestion.command}
</Text>
<Box marginLeft={1}>
<Text color="gray">{suggestion.description}</Text>
</Box>
</Box>
))}
<Box marginTop={1}>
<Text color="gray" dimColor>
↑↓ navigate • Enter/Tab select • Esc cancel
</Text>
</Box>
</Box>
);
}
-173
View File
@@ -1,173 +0,0 @@
import React, { useState } from "react";
import { Box, Text, useInput } from "ink";
import { DiffRenderer } from "./diff-renderer.js";
interface ConfirmationDialogProps {
operation: string;
filename: string;
onConfirm: (dontAskAgain?: boolean) => void;
onReject: (feedback?: string) => void;
showVSCodeOpen?: boolean;
content?: string; // Optional content to show (file content or command)
}
export default function ConfirmationDialog({
operation,
filename,
onConfirm,
onReject,
showVSCodeOpen = false,
content,
}: ConfirmationDialogProps) {
const [selectedOption, setSelectedOption] = useState(0);
const [feedbackMode, setFeedbackMode] = useState(false);
const [feedback, setFeedback] = useState("");
const options = [
"Yes",
"Yes, and don't ask again this session",
"No",
"No, with feedback",
];
useInput((input, key) => {
if (feedbackMode) {
if (key.return) {
onReject(feedback.trim());
return;
}
if (key.backspace || key.delete) {
setFeedback((prev) => prev.slice(0, -1));
return;
}
if (input && !key.ctrl && !key.meta) {
setFeedback((prev) => prev + input);
}
return;
}
if (key.upArrow || (key.shift && key.tab)) {
setSelectedOption((prev) => (prev > 0 ? prev - 1 : options.length - 1));
return;
}
if (key.downArrow || key.tab) {
setSelectedOption((prev) => (prev + 1) % options.length);
return;
}
if (key.return) {
if (selectedOption === 0) {
onConfirm(false);
} else if (selectedOption === 1) {
onConfirm(true);
} else if (selectedOption === 2) {
onReject("Operation cancelled by user");
} else {
setFeedbackMode(true);
}
return;
}
if (key.escape) {
if (feedbackMode) {
setFeedbackMode(false);
setFeedback("");
} else {
// Cancel the confirmation when escape is pressed from main confirmation
onReject("Operation cancelled by user (pressed Escape)");
}
return;
}
});
if (feedbackMode) {
return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" marginBottom={1}>
<Text color="gray">
Type your feedback and press Enter, or press Escape to go back.
</Text>
</Box>
<Box
borderStyle="round"
borderColor="yellow"
paddingX={1}
marginTop={1}
>
<Text color="gray">❯ </Text>
<Text>
{feedback}
<Text color="white">█</Text>
</Text>
</Box>
</Box>
);
}
return (
<Box flexDirection="column">
{/* Tool use header - styled like chat history */}
<Box marginTop={1}>
<Box>
<Text color="magenta">⏺</Text>
<Text color="white">
{" "}
{operation}({filename})
</Text>
</Box>
</Box>
<Box marginLeft={2} flexDirection="column">
<Text color="gray">⎿ Requesting user confirmation</Text>
{showVSCodeOpen && (
<Box marginTop={1}>
<Text color="gray">⎿ Opened changes in Visual Studio Code ⧉</Text>
</Box>
)}
{/* Show content preview if provided */}
{content && (
<>
<Text color="gray">⎿ {content.split('\n')[0]}</Text>
<Box marginLeft={4} flexDirection="column">
<DiffRenderer
diffContent={content}
filename={filename}
terminalWidth={80}
/>
</Box>
</>
)}
</Box>
{/* Confirmation options */}
<Box flexDirection="column" marginTop={1}>
<Box marginBottom={1}>
<Text>Do you want to proceed with this operation?</Text>
</Box>
<Box flexDirection="column">
{options.map((option, index) => (
<Box key={index} paddingLeft={1}>
<Text
color={selectedOption === index ? "black" : "white"}
backgroundColor={selectedOption === index ? "cyan" : undefined}
>
{index + 1}. {option}
</Text>
</Box>
))}
</Box>
<Box marginTop={1}>
<Text color="gray" dimColor>
↑↓ navigate • Enter select • Esc cancel
</Text>
</Box>
</Box>
</Box>
);
}
-280
View File
@@ -1,280 +0,0 @@
/**
* Professional diff renderer component
*/
import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../utils/colors.js';
import crypto from 'crypto';
import { colorizeCode } from '../utils/code-colorizer.js';
import { MaxSizedBox } from '../shared/max-sized-box.js';
interface DiffLine {
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
oldLine?: number;
newLine?: number;
content: string;
}
function parseDiffWithLineNumbers(diffContent: string): DiffLine[] {
const lines = diffContent.split('\n');
const result: DiffLine[] = [];
let currentOldLine = 0;
let currentNewLine = 0;
let inHunk = false;
const hunkHeaderRegex = /^@@ -(\d+),?\d* \+(\d+),?\d* @@/;
for (const line of lines) {
const hunkMatch = line.match(hunkHeaderRegex);
if (hunkMatch) {
currentOldLine = parseInt(hunkMatch[1], 10);
currentNewLine = parseInt(hunkMatch[2], 10);
inHunk = true;
result.push({ type: 'hunk', content: line });
// We need to adjust the starting point because the first line number applies to the *first* actual line change/context,
// but we increment *before* pushing that line. So decrement here.
currentOldLine--;
currentNewLine--;
continue;
}
if (!inHunk) {
// Skip standard Git header lines more robustly
if (
line.startsWith('--- ') ||
line.startsWith('+++ ') ||
line.startsWith('diff --git') ||
line.startsWith('index ') ||
line.startsWith('similarity index') ||
line.startsWith('rename from') ||
line.startsWith('rename to') ||
line.startsWith('new file mode') ||
line.startsWith('deleted file mode')
)
continue;
// If it's not a hunk or header, skip (or handle as 'other' if needed)
continue;
}
if (line.startsWith('+')) {
currentNewLine++; // Increment before pushing
result.push({
type: 'add',
newLine: currentNewLine,
content: line.substring(1),
});
} else if (line.startsWith('-')) {
currentOldLine++; // Increment before pushing
result.push({
type: 'del',
oldLine: currentOldLine,
content: line.substring(1),
});
} else if (line.startsWith(' ')) {
currentOldLine++; // Increment before pushing
currentNewLine++;
result.push({
type: 'context',
oldLine: currentOldLine,
newLine: currentNewLine,
content: line.substring(1),
});
} else if (line.startsWith('\\')) {
// Handle "\ No newline at end of file"
result.push({ type: 'other', content: line });
}
}
return result;
}
interface DiffRendererProps {
diffContent: string;
filename?: string;
tabWidth?: number;
availableTerminalHeight?: number;
terminalWidth?: number;
}
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
export const DiffRenderer = ({
diffContent,
filename,
tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight,
terminalWidth = 80,
}: DiffRendererProps): React.ReactElement => {
if (!diffContent || typeof diffContent !== 'string') {
return <Text color={Colors.AccentYellow}>No diff content.</Text>;
}
// Strip the first summary line (e.g. "Updated file.txt with 1 addition and 2 removals")
const lines = diffContent.split('\n');
const firstLine = lines[0];
let actualDiffContent = diffContent;
if (firstLine && (firstLine.startsWith('Updated ') || firstLine.startsWith('Created '))) {
actualDiffContent = lines.slice(1).join('\n');
}
const parsedLines = parseDiffWithLineNumbers(actualDiffContent);
if (parsedLines.length === 0) {
return <Text dimColor>No changes detected.</Text>;
}
// Always render as diff format to show line numbers and + signs
const renderedOutput = renderDiffContent(
parsedLines,
filename,
tabWidth,
availableTerminalHeight,
terminalWidth,
);
return <>{renderedOutput}</>;
};
const renderDiffContent = (
parsedLines: DiffLine[],
filename: string | undefined,
tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight: number | undefined,
terminalWidth: number,
) => {
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map((line) => ({
...line,
content: line.content.replace(/\t/g, ' '.repeat(tabWidth)),
}));
// Filter out non-displayable lines (hunks, potentially 'other') using the normalized list
const displayableLines = normalizedLines.filter(
(l) => l.type !== 'hunk' && l.type !== 'other',
);
if (displayableLines.length === 0) {
return <Text dimColor>No changes detected.</Text>;
}
// Calculate the minimum indentation across all displayable lines
let baseIndentation = Infinity; // Start high to find the minimum
for (const line of displayableLines) {
// Only consider lines with actual content for indentation calculation
if (line.content.trim() === '') continue;
const firstCharIndex = line.content.search(/\S/); // Find index of first non-whitespace char
const currentIndent = firstCharIndex === -1 ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found
baseIndentation = Math.min(baseIndentation, currentIndent);
}
// If baseIndentation remained Infinity (e.g., no displayable lines with content), default to 0
if (!isFinite(baseIndentation)) {
baseIndentation = 0;
}
const key = filename
? `diff-box-${filename}`
: `diff-box-${crypto.createHash('sha1').update(JSON.stringify(parsedLines)).digest('hex')}`;
let lastLineNumber: number | null = null;
const MAX_CONTEXT_LINES_WITHOUT_GAP = 5;
return (
<MaxSizedBox
maxHeight={availableTerminalHeight}
maxWidth={terminalWidth}
key={key}
>
{displayableLines.reduce<React.ReactNode[]>((acc, line, index) => {
// Determine the relevant line number for gap calculation based on type
let relevantLineNumberForGapCalc: number | null = null;
if (line.type === 'add' || line.type === 'context') {
relevantLineNumberForGapCalc = line.newLine ?? null;
} else if (line.type === 'del') {
// For deletions, the gap is typically in relation to the original file's line numbering
relevantLineNumberForGapCalc = line.oldLine ?? null;
}
if (
lastLineNumber !== null &&
relevantLineNumberForGapCalc !== null &&
relevantLineNumberForGapCalc >
lastLineNumber + MAX_CONTEXT_LINES_WITHOUT_GAP + 1
) {
acc.push(
<Box key={`gap-${index}`}>
<Text wrap="truncate">{'═'.repeat(terminalWidth)}</Text>
</Box>,
);
}
const lineKey = `diff-line-${index}`;
let gutterNumStr = '';
let backgroundColor: string | undefined = undefined;
let prefixSymbol = ' ';
let dim = false;
switch (line.type) {
case 'add':
gutterNumStr = (line.newLine ?? '').toString();
backgroundColor = '#86efac'; // Light green for additions
prefixSymbol = '+';
lastLineNumber = line.newLine ?? null;
break;
case 'del':
gutterNumStr = (line.oldLine ?? '').toString();
backgroundColor = 'redBright'; // Light red for deletions
prefixSymbol = '-';
// For deletions, update lastLineNumber based on oldLine if it's advancing.
// This helps manage gaps correctly if there are multiple consecutive deletions
// or if a deletion is followed by a context line far away in the original file.
if (line.oldLine !== undefined) {
lastLineNumber = line.oldLine;
}
break;
case 'context':
gutterNumStr = (line.newLine ?? '').toString();
dim = true;
prefixSymbol = ' ';
lastLineNumber = line.newLine ?? null;
break;
default:
return acc;
}
const displayContent = line.content.substring(baseIndentation);
acc.push(
<Box key={lineKey} flexDirection="row">
<Text color={Colors.Gray} dimColor={dim}>{gutterNumStr.padEnd(4)}</Text>
<Text color={backgroundColor ? '#000000' : undefined} backgroundColor={backgroundColor} dimColor={!backgroundColor && dim}>{prefixSymbol} </Text>
<Text color={backgroundColor ? '#000000' : undefined} backgroundColor={backgroundColor} dimColor={!backgroundColor && dim} wrap="wrap">
{displayContent}
</Text>
</Box>,
);
return acc;
}, [])}
</MaxSizedBox>
);
};
const getLanguageFromExtension = (extension: string): string | null => {
const languageMap: { [key: string]: string } = {
js: 'javascript',
ts: 'typescript',
py: 'python',
json: 'json',
css: 'css',
html: 'html',
sh: 'bash',
md: 'markdown',
yaml: 'yaml',
yml: 'yaml',
txt: 'plaintext',
java: 'java',
c: 'c',
cpp: 'cpp',
rb: 'ruby',
};
return languageMap[extension] || null; // Return null if extension not found
};
-76
View File
@@ -1,76 +0,0 @@
import React, { useState, useEffect } from "react";
import { Box, Text } from "ink";
import { formatTokenCount } from "../../utils/token-counter.js";
interface LoadingSpinnerProps {
isActive: boolean;
processingTime: number;
tokenCount: number;
}
const loadingTexts = [
"Thinking...",
"Computing...",
"Analyzing...",
"Processing...",
"Calculating...",
"Interfacing...",
"Optimizing...",
"Synthesizing...",
"Decrypting...",
"Calibrating...",
"Bootstrapping...",
"Synchronizing...",
"Compiling...",
"Downloading...",
];
export function LoadingSpinner({
isActive,
processingTime,
tokenCount,
}: LoadingSpinnerProps) {
const [spinnerFrame, setSpinnerFrame] = useState(0);
const [loadingTextIndex, setLoadingTextIndex] = useState(0);
useEffect(() => {
if (!isActive) return;
const spinnerFrames = ["/", "-", "\\", "|"];
// Reduced frequency: 500ms instead of 250ms to reduce flickering on Windows
const interval = setInterval(() => {
setSpinnerFrame((prev) => (prev + 1) % spinnerFrames.length);
}, 500);
return () => clearInterval(interval);
}, [isActive]);
useEffect(() => {
if (!isActive) return;
setLoadingTextIndex(Math.floor(Math.random() * loadingTexts.length));
// Increased interval: 4s instead of 2s to reduce state changes
const interval = setInterval(() => {
setLoadingTextIndex(Math.floor(Math.random() * loadingTexts.length));
}, 4000);
return () => clearInterval(interval);
}, [isActive]);
if (!isActive) return null;
const spinnerFrames = ["/", "-", "\\", "|"];
return (
<Box marginTop={1}>
<Text color="cyan">
{spinnerFrames[spinnerFrame]} {loadingTexts[loadingTextIndex]}{" "}
</Text>
<Text color="gray">
({processingTime}s · ↑ {formatTokenCount(tokenCount)} tokens · esc to
interrupt)
</Text>
</Box>
);
}
-49
View File
@@ -1,49 +0,0 @@
import React, { useState, useEffect } from "react";
import { Box, Text } from "ink";
import { getMCPManager } from "../../grok/tools.js";
import { MCPTool } from "../../mcp/client.js";
interface MCPStatusProps {}
export function MCPStatus({}: MCPStatusProps) {
const [connectedServers, setConnectedServers] = useState<string[]>([]);
const [availableTools, setAvailableTools] = useState<MCPTool[]>([]);
useEffect(() => {
const updateStatus = () => {
try {
const manager = getMCPManager();
const servers = manager.getServers();
const tools = manager.getTools();
setConnectedServers(servers);
setAvailableTools(tools);
} catch (error) {
// MCP manager not initialized yet
setConnectedServers([]);
setAvailableTools([]);
}
};
// Initial update with a small delay to allow MCP initialization
const initialTimer = setTimeout(updateStatus, 2000);
// Set up polling to check for status changes
const interval = setInterval(updateStatus, 2000);
return () => {
clearTimeout(initialTimer);
clearInterval(interval);
};
}, []);
if (connectedServers.length === 0) {
return null;
}
return (
<Box marginLeft={1}>
<Text color="green">⚒ mcps: {connectedServers.length} </Text>
</Box>
);
}
-45
View File
@@ -1,45 +0,0 @@
import React from "react";
import { Box, Text } from "ink";
interface ModelOption {
model: string;
}
interface ModelSelectionProps {
models: ModelOption[];
selectedIndex: number;
isVisible: boolean;
currentModel: string;
}
export function ModelSelection({
models,
selectedIndex,
isVisible,
currentModel,
}: ModelSelectionProps) {
if (!isVisible) return null;
return (
<Box marginTop={1} flexDirection="column">
<Box marginBottom={1}>
<Text color="cyan">Select Grok Model (current: {currentModel}):</Text>
</Box>
{models.map((modelOption, index) => (
<Box key={index} paddingLeft={1}>
<Text
color={index === selectedIndex ? "black" : "white"}
backgroundColor={index === selectedIndex ? "cyan" : undefined}
>
{modelOption.model}
</Text>
</Box>
))}
<Box marginTop={1}>
<Text color="gray" dimColor>
↑↓ navigate • Enter/Tab select • Esc cancel
</Text>
</Box>
</Box>
);
}
-24
View File
@@ -1,24 +0,0 @@
import React from 'react';
import { Box } from 'ink';
interface MaxSizedBoxProps {
maxHeight?: number;
maxWidth?: number;
children: React.ReactNode;
}
export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
maxHeight,
maxWidth,
children,
...props
}) => {
return (
<Box
flexDirection="column"
{...props}
>
{children}
</Box>
);
};
-20
View File
@@ -1,20 +0,0 @@
import React from 'react';
import { Text, Box } from 'ink';
export const colorizeCode = (
content: string,
language: string | null,
availableTerminalHeight?: number,
terminalWidth?: number
): React.ReactNode => {
// Simple plain text rendering - could be enhanced with syntax highlighting later
return (
<Box flexDirection="column">
{content.split('\n').map((line, index) => (
<Text key={index} wrap="wrap">
{line}
</Text>
))}
</Box>
);
};
-14
View File
@@ -1,14 +0,0 @@
/**
* Color constants for the CLI interface
*/
export const Colors = {
AccentYellow: 'yellow',
Gray: 'gray',
Red: 'red',
Green: 'green',
Blue: 'blue',
Cyan: 'cyan',
Magenta: 'magenta',
White: 'white',
Black: 'black'
} as const;
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
import { Text } from 'ink';
import { marked } from 'marked';
import TerminalRenderer from 'marked-terminal';
// Configure marked to use the terminal renderer with default settings
marked.setOptions({
renderer: new (TerminalRenderer as any)()
});
export function MarkdownRenderer({ content }: { content: string }) {
try {
// Use marked.parse for synchronous parsing
const result = marked.parse(content);
// Handle both sync and async results
const rendered = typeof result === 'string' ? result : content;
return <Text>{rendered}</Text>;
} catch (error) {
// Fallback to plain text if markdown parsing fails
console.error('Markdown rendering error:', error);
return <Text>{content}</Text>;
}
}
-149
View File
@@ -1,149 +0,0 @@
import { exec } from "child_process";
import { promisify } from "util";
import { EventEmitter } from "events";
const execAsync = promisify(exec);
export interface ConfirmationOptions {
operation: string;
filename: string;
showVSCodeOpen?: boolean;
content?: string; // Content to show in confirmation dialog
}
export interface ConfirmationResult {
confirmed: boolean;
dontAskAgain?: boolean;
feedback?: string;
}
export class ConfirmationService extends EventEmitter {
private static instance: ConfirmationService;
private skipConfirmationThisSession = false;
private pendingConfirmation: Promise<ConfirmationResult> | null = null;
private resolveConfirmation: ((result: ConfirmationResult) => void) | null =
null;
// Session flags for different operation types
private sessionFlags = {
fileOperations: false,
bashCommands: false,
allOperations: false,
};
static getInstance(): ConfirmationService {
if (!ConfirmationService.instance) {
ConfirmationService.instance = new ConfirmationService();
}
return ConfirmationService.instance;
}
constructor() {
super();
}
async requestConfirmation(
options: ConfirmationOptions,
operationType: "file" | "bash" = "file"
): Promise<ConfirmationResult> {
// Check session flags
if (
this.sessionFlags.allOperations ||
(operationType === "file" && this.sessionFlags.fileOperations) ||
(operationType === "bash" && this.sessionFlags.bashCommands)
) {
return { confirmed: true };
}
// If VS Code should be opened, try to open it
if (options.showVSCodeOpen) {
try {
await this.openInVSCode(options.filename);
} catch (error) {
// If VS Code opening fails, continue without it
options.showVSCodeOpen = false;
}
}
// Create a promise that will be resolved by the UI component
this.pendingConfirmation = new Promise<ConfirmationResult>((resolve) => {
this.resolveConfirmation = resolve;
});
// Emit custom event that the UI can listen to (using setImmediate to ensure the UI updates)
setImmediate(() => {
this.emit("confirmation-requested", options);
});
const result = await this.pendingConfirmation;
if (result.dontAskAgain) {
// Set the appropriate session flag based on operation type
if (operationType === "file") {
this.sessionFlags.fileOperations = true;
} else if (operationType === "bash") {
this.sessionFlags.bashCommands = true;
}
// Could also set allOperations for global skip
}
return result;
}
confirmOperation(confirmed: boolean, dontAskAgain?: boolean): void {
if (this.resolveConfirmation) {
this.resolveConfirmation({ confirmed, dontAskAgain });
this.resolveConfirmation = null;
this.pendingConfirmation = null;
}
}
rejectOperation(feedback?: string): void {
if (this.resolveConfirmation) {
this.resolveConfirmation({ confirmed: false, feedback });
this.resolveConfirmation = null;
this.pendingConfirmation = null;
}
}
private async openInVSCode(filename: string): Promise<void> {
// Try different VS Code commands
const commands = ["code", "code-insiders", "codium"];
for (const cmd of commands) {
try {
await execAsync(`which ${cmd}`);
await execAsync(`${cmd} "${filename}"`);
return;
} catch (error) {
// Continue to next command
continue;
}
}
throw new Error("VS Code not found");
}
isPending(): boolean {
return this.pendingConfirmation !== null;
}
resetSession(): void {
this.sessionFlags = {
fileOperations: false,
bashCommands: false,
allOperations: false,
};
}
getSessionFlags() {
return { ...this.sessionFlags };
}
setSessionFlag(
flagType: "fileOperations" | "bashCommands" | "allOperations",
value: boolean
) {
this.sessionFlags[flagType] = value;
}
}
-26
View File
@@ -1,26 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export function loadCustomInstructions(workingDirectory: string = process.cwd()): string | null {
try {
let instructionsPath = path.join(workingDirectory, '.grok', 'GROK.md');
if (fs.existsSync(instructionsPath)) {
const customInstructions = fs.readFileSync(instructionsPath, 'utf-8');
return customInstructions.trim();
}
instructionsPath = path.join(os.homedir(), '.grok', 'GROK.md');
if (fs.existsSync(instructionsPath)) {
const customInstructions = fs.readFileSync(instructionsPath, 'utf-8');
return customInstructions.trim();
}
return null;
} catch (error) {
console.warn('Failed to load custom instructions:', error);
return null;
}
}
+22
View File
@@ -0,0 +1,22 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
export function loadCustomInstructions(): string | null {
const candidates = [
path.join(process.cwd(), ".grok", "GROK.md"),
path.join(os.homedir(), ".grok", "GROK.md"),
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) {
return fs.readFileSync(p, "utf-8").trim();
}
} catch {
continue;
}
}
return null;
}
-56
View File
@@ -1,56 +0,0 @@
import { getSettingsManager, UserSettings, ProjectSettings } from './settings-manager.js';
export interface ModelOption {
model: string;
}
export type ModelConfig = string;
// Re-export interfaces for backward compatibility
export { UserSettings, ProjectSettings };
/**
* Get the effective current model
* Priority: project current model > user default model > system default
*/
export function getCurrentModel(): string {
const manager = getSettingsManager();
return manager.getCurrentModel();
}
/**
* Load model configuration
* Priority: user-settings.json models > default hardcoded
*/
export function loadModelConfig(): ModelOption[] {
const manager = getSettingsManager();
const models = manager.getAvailableModels();
return models.map(model => ({
model: model.trim()
}));
}
/**
* Get default models list
*/
export function getDefaultModels(): string[] {
const manager = getSettingsManager();
return manager.getAvailableModels();
}
/**
* Update the current model in project settings
*/
export function updateCurrentModel(modelName: string): void {
const manager = getSettingsManager();
manager.setCurrentModel(modelName);
}
/**
* Update the user's default model preference
*/
export function updateDefaultModel(modelName: string): void {
const manager = getSettingsManager();
manager.updateUserSetting('defaultModel', modelName);
}
-378
View File
@@ -1,378 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
/**
* Current settings version - increment this when adding new models or changing settings structure
* This triggers automatic migration for existing users
*/
const SETTINGS_VERSION = 2;
/**
* User-level settings stored in ~/.grok/user-settings.json
* These are global settings that apply across all projects
*/
export interface UserSettings {
apiKey?: string; // Grok API key
baseURL?: string; // API base URL
defaultModel?: string; // User's preferred default model
models?: string[]; // Available models list
settingsVersion?: number; // Version for migration tracking
}
/**
* Project-level settings stored in .grok/settings.json
* These are project-specific settings
*/
export interface ProjectSettings {
model?: string; // Current model for this project
mcpServers?: Record<string, any>; // MCP server configurations
}
/**
* Default values for user settings
*/
const DEFAULT_USER_SETTINGS: Partial<UserSettings> = {
baseURL: "https://api.x.ai/v1",
defaultModel: "grok-code-fast-1",
models: [
// Grok 4.1 Fast models (2M context, latest - November 2025)
"grok-4-1-fast-reasoning",
"grok-4-1-fast-non-reasoning",
// Grok 4 Fast models (2M context)
"grok-4-fast-reasoning",
"grok-4-fast-non-reasoning",
// Grok 4 flagship (256K context)
"grok-4",
"grok-4-latest",
// Grok Code (optimized for coding, 256K context)
"grok-code-fast-1",
// Grok 3 models (131K context)
"grok-3",
"grok-3-latest",
"grok-3-fast",
"grok-3-mini",
"grok-3-mini-fast",
],
};
/**
* Default values for project settings
*/
const DEFAULT_PROJECT_SETTINGS: Partial<ProjectSettings> = {
model: "grok-code-fast-1",
};
/**
* Unified settings manager that handles both user-level and project-level settings
*/
export class SettingsManager {
private static instance: SettingsManager;
private userSettingsPath: string;
private projectSettingsPath: string;
private constructor() {
// User settings path: ~/.grok/user-settings.json
this.userSettingsPath = path.join(
os.homedir(),
".grok",
"user-settings.json"
);
// Project settings path: .grok/settings.json (in current working directory)
this.projectSettingsPath = path.join(
process.cwd(),
".grok",
"settings.json"
);
}
/**
* Get singleton instance
*/
public static getInstance(): SettingsManager {
if (!SettingsManager.instance) {
SettingsManager.instance = new SettingsManager();
}
return SettingsManager.instance;
}
/**
* Ensure directory exists for a given file path
*/
private ensureDirectoryExists(filePath: string): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
}
/**
* Load user settings from ~/.grok/user-settings.json
*/
public loadUserSettings(): UserSettings {
try {
if (!fs.existsSync(this.userSettingsPath)) {
// Create default user settings if file doesn't exist
const newSettings = { ...DEFAULT_USER_SETTINGS, settingsVersion: SETTINGS_VERSION };
this.saveUserSettings(newSettings);
return newSettings;
}
const content = fs.readFileSync(this.userSettingsPath, "utf-8");
const settings = JSON.parse(content);
// Check if migration is needed
const currentVersion = settings.settingsVersion || 1;
if (currentVersion < SETTINGS_VERSION) {
const migratedSettings = this.migrateSettings(settings, currentVersion);
this.saveUserSettings(migratedSettings);
return migratedSettings;
}
// Merge with defaults to ensure all required fields exist
return { ...DEFAULT_USER_SETTINGS, ...settings };
} catch (error) {
console.warn(
"Failed to load user settings:",
error instanceof Error ? error.message : "Unknown error"
);
return { ...DEFAULT_USER_SETTINGS };
}
}
/**
* Migrate settings from an older version to the current version
*/
private migrateSettings(settings: UserSettings, fromVersion: number): UserSettings {
let migrated = { ...settings };
// Migration from version 1 to 2: Add new Grok 4.1 and Grok 4 Fast models
if (fromVersion < 2) {
const defaultModels = DEFAULT_USER_SETTINGS.models || [];
const existingModels = new Set(migrated.models || []);
// Add any new models that don't exist in user's current list
const newModels = defaultModels.filter(model => !existingModels.has(model));
// Prepend new models to the list (newest models first)
migrated.models = [...newModels, ...(migrated.models || [])];
}
// Add future migrations here:
// if (fromVersion < 3) { ... }
migrated.settingsVersion = SETTINGS_VERSION;
return migrated;
}
/**
* Save user settings to ~/.grok/user-settings.json
*/
public saveUserSettings(settings: Partial<UserSettings>): void {
try {
this.ensureDirectoryExists(this.userSettingsPath);
// Read existing settings directly to avoid recursion
let existingSettings: UserSettings = { ...DEFAULT_USER_SETTINGS };
if (fs.existsSync(this.userSettingsPath)) {
try {
const content = fs.readFileSync(this.userSettingsPath, "utf-8");
const parsed = JSON.parse(content);
existingSettings = { ...DEFAULT_USER_SETTINGS, ...parsed };
} catch (error) {
// If file is corrupted, use defaults
console.warn("Corrupted user settings file, using defaults");
}
}
const mergedSettings = { ...existingSettings, ...settings };
fs.writeFileSync(
this.userSettingsPath,
JSON.stringify(mergedSettings, null, 2),
{ mode: 0o600 } // Secure permissions for API key
);
} catch (error) {
console.error(
"Failed to save user settings:",
error instanceof Error ? error.message : "Unknown error"
);
throw error;
}
}
/**
* Update a specific user setting
*/
public updateUserSetting<K extends keyof UserSettings>(
key: K,
value: UserSettings[K]
): void {
const settings = { [key]: value } as Partial<UserSettings>;
this.saveUserSettings(settings);
}
/**
* Get a specific user setting
*/
public getUserSetting<K extends keyof UserSettings>(key: K): UserSettings[K] {
const settings = this.loadUserSettings();
return settings[key];
}
/**
* Load project settings from .grok/settings.json
*/
public loadProjectSettings(): ProjectSettings {
try {
if (!fs.existsSync(this.projectSettingsPath)) {
// Create default project settings if file doesn't exist
this.saveProjectSettings(DEFAULT_PROJECT_SETTINGS);
return { ...DEFAULT_PROJECT_SETTINGS };
}
const content = fs.readFileSync(this.projectSettingsPath, "utf-8");
const settings = JSON.parse(content);
// Merge with defaults
return { ...DEFAULT_PROJECT_SETTINGS, ...settings };
} catch (error) {
console.warn(
"Failed to load project settings:",
error instanceof Error ? error.message : "Unknown error"
);
return { ...DEFAULT_PROJECT_SETTINGS };
}
}
/**
* Save project settings to .grok/settings.json
*/
public saveProjectSettings(settings: Partial<ProjectSettings>): void {
try {
this.ensureDirectoryExists(this.projectSettingsPath);
// Read existing settings directly to avoid recursion
let existingSettings: ProjectSettings = { ...DEFAULT_PROJECT_SETTINGS };
if (fs.existsSync(this.projectSettingsPath)) {
try {
const content = fs.readFileSync(this.projectSettingsPath, "utf-8");
const parsed = JSON.parse(content);
existingSettings = { ...DEFAULT_PROJECT_SETTINGS, ...parsed };
} catch (error) {
// If file is corrupted, use defaults
console.warn("Corrupted project settings file, using defaults");
}
}
const mergedSettings = { ...existingSettings, ...settings };
fs.writeFileSync(
this.projectSettingsPath,
JSON.stringify(mergedSettings, null, 2)
);
} catch (error) {
console.error(
"Failed to save project settings:",
error instanceof Error ? error.message : "Unknown error"
);
throw error;
}
}
/**
* Update a specific project setting
*/
public updateProjectSetting<K extends keyof ProjectSettings>(
key: K,
value: ProjectSettings[K]
): void {
const settings = { [key]: value } as Partial<ProjectSettings>;
this.saveProjectSettings(settings);
}
/**
* Get a specific project setting
*/
public getProjectSetting<K extends keyof ProjectSettings>(
key: K
): ProjectSettings[K] {
const settings = this.loadProjectSettings();
return settings[key];
}
/**
* Get the current model with proper fallback logic:
* 1. Project-specific model setting
* 2. User's default model
* 3. System default
*/
public getCurrentModel(): string {
const projectModel = this.getProjectSetting("model");
if (projectModel) {
return projectModel;
}
const userDefaultModel = this.getUserSetting("defaultModel");
if (userDefaultModel) {
return userDefaultModel;
}
return DEFAULT_PROJECT_SETTINGS.model || "grok-code-fast-1";
}
/**
* Set the current model for the project
*/
public setCurrentModel(model: string): void {
this.updateProjectSetting("model", model);
}
/**
* Get available models list from user settings
*/
public getAvailableModels(): string[] {
const models = this.getUserSetting("models");
return models || DEFAULT_USER_SETTINGS.models || [];
}
/**
* Get API key from user settings or environment
*/
public getApiKey(): string | undefined {
// First check environment variable
const envApiKey = process.env.GROK_API_KEY;
if (envApiKey) {
return envApiKey;
}
// Then check user settings
return this.getUserSetting("apiKey");
}
/**
* Get base URL from user settings or environment
*/
public getBaseURL(): string {
// First check environment variable
const envBaseURL = process.env.GROK_BASE_URL;
if (envBaseURL) {
return envBaseURL;
}
// Then check user settings
const userBaseURL = this.getUserSetting("baseURL");
return (
userBaseURL || DEFAULT_USER_SETTINGS.baseURL || "https://api.x.ai/v1"
);
}
}
/**
* Convenience function to get the singleton instance
*/
export function getSettingsManager(): SettingsManager {
return SettingsManager.getInstance();
}
+84 -3
View File
@@ -1,4 +1,85 @@
// This file is kept for potential future convenience functions
// All settings management should use getSettingsManager() from './settings-manager.js'
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { DEFAULT_MODEL, getModelIds } from "../grok/models.js";
export { getSettingsManager, UserSettings, ProjectSettings } from './settings-manager.js';
export interface UserSettings {
apiKey?: string;
baseURL?: string;
defaultModel?: string;
models?: string[];
}
export interface ProjectSettings {
model?: string;
}
const USER_DIR = path.join(os.homedir(), ".grok");
const USER_SETTINGS_PATH = path.join(USER_DIR, "user-settings.json");
function ensureDir(dir: string): void {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
}
function readJson<T>(filePath: string): T | null {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
} catch {
return null;
}
}
function writeJson(filePath: string, data: unknown): void {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
}
export function loadUserSettings(): UserSettings {
const defaults: UserSettings = {
baseURL: "https://api.x.ai/v1",
defaultModel: DEFAULT_MODEL,
models: getModelIds(),
};
const saved = readJson<UserSettings>(USER_SETTINGS_PATH);
return { ...defaults, ...saved };
}
export function saveUserSettings(partial: Partial<UserSettings>): void {
const current = loadUserSettings();
writeJson(USER_SETTINGS_PATH, { ...current, ...partial });
}
export function loadProjectSettings(): ProjectSettings {
const projectPath = path.join(process.cwd(), ".grok", "settings.json");
return readJson<ProjectSettings>(projectPath) || {};
}
export function saveProjectSettings(partial: Partial<ProjectSettings>): void {
const projectPath = path.join(process.cwd(), ".grok", "settings.json");
const current = loadProjectSettings();
writeJson(projectPath, { ...current, ...partial });
}
export function getApiKey(): string | undefined {
return process.env.GROK_API_KEY || loadUserSettings().apiKey;
}
export function getBaseURL(): string {
return process.env.GROK_BASE_URL || loadUserSettings().baseURL || "https://api.x.ai/v1";
}
export function getCurrentModel(): string {
if (process.env.GROK_MODEL) return process.env.GROK_MODEL;
const project = loadProjectSettings();
if (project.model) return project.model;
const user = loadUserSettings();
return user.defaultModel || DEFAULT_MODEL;
}
export function getAvailableModels(): string[] {
const user = loadUserSettings();
return user.models || getModelIds();
}
-221
View File
@@ -1,221 +0,0 @@
/**
* Text manipulation utilities for terminal input handling
* Inspired by Gemini CLI's text processing capabilities
*/
export interface TextPosition {
index: number;
line: number;
column: number;
}
export interface TextSelection {
start: number;
end: number;
}
/**
* Check if a character is a word boundary
*/
export function isWordBoundary(char: string | undefined): boolean {
if (!char) return true;
return /\s/.test(char) || /[^\w]/.test(char);
}
/**
* Find the start of the current word at the given position
*/
export function findWordStart(text: string, position: number): number {
if (position <= 0) return 0;
let pos = position - 1;
while (pos > 0 && !isWordBoundary(text[pos])) {
pos--;
}
// If we stopped at a word boundary, move forward to the actual word start
if (pos > 0 && isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Find the end of the current word at the given position
*/
export function findWordEnd(text: string, position: number): number {
if (position >= text.length) return text.length;
let pos = position;
while (pos < text.length && !isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Move cursor to the previous word boundary
*/
export function moveToPreviousWord(text: string, position: number): number {
if (position <= 0) return 0;
let pos = position - 1;
// Skip whitespace
while (pos > 0 && isWordBoundary(text[pos])) {
pos--;
}
// Find start of the word
while (pos > 0 && !isWordBoundary(text[pos - 1])) {
pos--;
}
return pos;
}
/**
* Move cursor to the next word boundary
*/
export function moveToNextWord(text: string, position: number): number {
if (position >= text.length) return text.length;
let pos = position;
// Skip current word
while (pos < text.length && !isWordBoundary(text[pos])) {
pos++;
}
// Skip whitespace
while (pos < text.length && isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Delete the word before the cursor
*/
export function deleteWordBefore(text: string, position: number): { text: string; position: number } {
const wordStart = moveToPreviousWord(text, position);
const newText = text.slice(0, wordStart) + text.slice(position);
return {
text: newText,
position: wordStart,
};
}
/**
* Delete the word after the cursor
*/
export function deleteWordAfter(text: string, position: number): { text: string; position: number } {
const wordEnd = moveToNextWord(text, position);
const newText = text.slice(0, position) + text.slice(wordEnd);
return {
text: newText,
position,
};
}
/**
* Get the current line and column from text position
*/
export function getTextPosition(text: string, index: number): TextPosition {
const lines = text.slice(0, index).split('\n');
return {
index,
line: lines.length - 1,
column: lines[lines.length - 1].length,
};
}
/**
* Move to the beginning of the current line
*/
export function moveToLineStart(text: string, position: number): number {
const beforeCursor = text.slice(0, position);
const lastNewlineIndex = beforeCursor.lastIndexOf('\n');
return lastNewlineIndex === -1 ? 0 : lastNewlineIndex + 1;
}
/**
* Move to the end of the current line
*/
export function moveToLineEnd(text: string, position: number): number {
const afterCursor = text.slice(position);
const nextNewlineIndex = afterCursor.indexOf('\n');
return nextNewlineIndex === -1 ? text.length : position + nextNewlineIndex;
}
/**
* Handle proper Unicode-aware character deletion
*/
export function deleteCharBefore(text: string, position: number): { text: string; position: number } {
if (position <= 0) {
return { text, position };
}
// Handle surrogate pairs and combining characters
let deleteCount = 1;
const charBefore = text.charAt(position - 1);
// Check for high surrogate (first part of surrogate pair)
if (position >= 2) {
const charBeforeBefore = text.charAt(position - 2);
if (charBeforeBefore >= '\uD800' && charBeforeBefore <= '\uDBFF' &&
charBefore >= '\uDC00' && charBefore <= '\uDFFF') {
deleteCount = 2;
}
}
const newText = text.slice(0, position - deleteCount) + text.slice(position);
return {
text: newText,
position: position - deleteCount,
};
}
/**
* Handle proper Unicode-aware character deletion forward
*/
export function deleteCharAfter(text: string, position: number): { text: string; position: number } {
if (position >= text.length) {
return { text, position };
}
// Handle surrogate pairs and combining characters
let deleteCount = 1;
const charAfter = text.charAt(position);
// Check for high surrogate (first part of surrogate pair)
if (position + 1 < text.length) {
const charAfterAfter = text.charAt(position + 1);
if (charAfter >= '\uD800' && charAfter <= '\uDBFF' &&
charAfterAfter >= '\uDC00' && charAfterAfter <= '\uDFFF') {
deleteCount = 2;
}
}
const newText = text.slice(0, position) + text.slice(position + deleteCount);
return {
text: newText,
position,
};
}
/**
* Insert text at the given position with proper Unicode handling
*/
export function insertText(text: string, position: number, insert: string): { text: string; position: number } {
const newText = text.slice(0, position) + insert + text.slice(position);
return {
text: newText,
position: position + insert.length,
};
}
-91
View File
@@ -1,91 +0,0 @@
import { get_encoding, encoding_for_model, Tiktoken } from 'tiktoken';
export class TokenCounter {
private encoder: Tiktoken;
constructor(model: string = 'gpt-4') {
try {
// Try to get encoding for specific model
this.encoder = encoding_for_model(model as any);
} catch {
// Fallback to cl100k_base (used by GPT-4 and most modern models)
this.encoder = get_encoding('cl100k_base');
}
}
/**
* Count tokens in a string
*/
countTokens(text: string): number {
if (!text) return 0;
return this.encoder.encode(text).length;
}
/**
* Count tokens in messages array (for chat completions)
*/
countMessageTokens(messages: Array<{ role: string; content: string | null; [key: string]: any }>): number {
let totalTokens = 0;
for (const message of messages) {
// Every message follows <|start|>{role/name}\n{content}<|end|\>\n
totalTokens += 3; // Base tokens per message
if (message.content && typeof message.content === 'string') {
totalTokens += this.countTokens(message.content);
}
if (message.role) {
totalTokens += this.countTokens(message.role);
}
// Add extra tokens for tool calls if present
if (message.tool_calls) {
totalTokens += this.countTokens(JSON.stringify(message.tool_calls));
}
}
totalTokens += 3; // Every reply is primed with <|start|>assistant<|message|>
return totalTokens;
}
/**
* Estimate tokens for streaming content
* This is an approximation since we don't have the full response yet
*/
estimateStreamingTokens(accumulatedContent: string): number {
return this.countTokens(accumulatedContent);
}
/**
* Clean up resources
*/
dispose(): void {
this.encoder.free();
}
}
/**
* Format token count for display (e.g., 1.2k for 1200)
*/
export function formatTokenCount(count: number): string {
if (count <= 999) {
return count.toString();
}
if (count < 1_000_000) {
const k = count / 1000;
return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`;
}
const m = count / 1_000_000;
return m % 1 === 0 ? `${m}m` : `${m.toFixed(1)}m`;
}
/**
* Create a token counter instance
*/
export function createTokenCounter(model?: string): TokenCounter {
return new TokenCounter(model);
}
+5 -5
View File
@@ -5,18 +5,18 @@
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true,
"jsx": "react",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"noImplicitAny": false
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
}