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:
1 parent
e763e2f472
commit
ccfd3bb6fc
52 files changed
+1322
-8788
No files matched your search
@@ -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.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
+7
-1
@@ -1,5 +1,11 @@
|
||||
# Grok API Configuration
|
||||
GROK_API_KEY=your_grok_api_key_here
|
||||
|
||||
# Optional: Custom Grok API Base URL
|
||||
# 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
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+94
-115
@@ -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: Message[],
|
||||
tools: ToolDefinition[],
|
||||
): AsyncGenerator<StreamDelta, void, unknown> {
|
||||
const payload: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages,
|
||||
tools: tools || [],
|
||||
tool_choice: tools && tools.length > 0 ? "auto" : undefined,
|
||||
temperature: 0.7,
|
||||
max_tokens: this.defaultMaxTokens,
|
||||
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(
|
||||
requestPayload
|
||||
)) as any;
|
||||
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) {
|
||||
yield chunk;
|
||||
const choice = chunk.choices?.[0];
|
||||
if (!choice) continue;
|
||||
|
||||
const delta: StreamDelta = { finishReason: choice.finish_reason };
|
||||
|
||||
if (choice.delta?.content) {
|
||||
delta.content = choice.delta.content;
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Grok API error: ${error.message}`);
|
||||
|
||||
if ((choice.delta as Record<string, unknown>)?.reasoning) {
|
||||
delta.reasoning = (choice.delta as Record<string, unknown>).reasoning as string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async search(
|
||||
query: string,
|
||||
searchParameters?: SearchParameters
|
||||
): Promise<GrokResponse> {
|
||||
const searchMessage: GrokMessage = {
|
||||
role: "user",
|
||||
content: query,
|
||||
};
|
||||
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 },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const searchOptions: SearchOptions = {
|
||||
search_parameters: searchParameters || { mode: "on" },
|
||||
};
|
||||
yield delta;
|
||||
}
|
||||
}
|
||||
|
||||
return this.chat([searchMessage], [], undefined, searchOptions);
|
||||
async searchWeb(query: string): Promise<string> {
|
||||
return this.search(query, "web_search");
|
||||
}
|
||||
|
||||
async searchX(query: string): Promise<string> {
|
||||
return this.search(query, "x_search");
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+18
-330
@@ -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: {
|
||||
query: {
|
||||
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",
|
||||
description: "The search query",
|
||||
},
|
||||
},
|
||||
required: ["id", "content", "status", "priority"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["todos"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_todo_list",
|
||||
description: "Update existing todos in the todo list",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
updates: {
|
||||
type: "array",
|
||||
description: "Array of todo updates",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "ID of the todo item to update",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
enum: ["pending", "in_progress", "completed"],
|
||||
description: "New status for the todo item",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "New content for the todo item",
|
||||
},
|
||||
priority: {
|
||||
type: "string",
|
||||
enum: ["high", "medium", "low"],
|
||||
description: "New priority for the todo item",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["updates"],
|
||||
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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
+98
-402
@@ -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,
|
||||
});
|
||||
for await (const chunk of agent.processMessage(prompt)) {
|
||||
switch (chunk.type) {
|
||||
case "content":
|
||||
if (chunk.content) process.stdout.write(chunk.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,
|
||||
},
|
||||
}));
|
||||
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,
|
||||
});
|
||||
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
|
||||
);
|
||||
await runHeadless(options.prompt, config.apiKey, config.baseURL, config.model, config.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);
|
||||
}
|
||||
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
|
||||
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}`,
|
||||
);
|
||||
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"
|
||||
console.log(
|
||||
` ${m.description} | ${formatContext(m.contextWindow)} context | $${m.inputPrice}/$${m.outputPrice} per 1M tokens`,
|
||||
);
|
||||
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);
|
||||
}
|
||||
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`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> = {};
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
+31
-64
@@ -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;
|
||||
}
|
||||
|
||||
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}`);
|
||||
getCwd(): string {
|
||||
return this.cwd;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
+48
-20
@@ -2,33 +2,61 @@ export interface ToolResult {
|
||||
success: boolean;
|
||||
output?: string;
|
||||
error?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
export interface Message {
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string;
|
||||
tool_calls?: ToolCall[];
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
execute: (...args: any[]) => Promise<ToolResult>;
|
||||
parameters: {
|
||||
type: "object";
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface EditorCommand {
|
||||
command: 'view' | 'str_replace' | 'create' | 'insert' | 'undo_edit';
|
||||
path?: string;
|
||||
old_str?: string;
|
||||
new_str?: string;
|
||||
export interface ChatEntry {
|
||||
type: "user" | "assistant" | "tool_call" | "tool_result" | "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 ConfirmationState {
|
||||
skipThisSession: boolean;
|
||||
pendingOperation: boolean;
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
contextWindow: number;
|
||||
inputPrice: number;
|
||||
outputPrice: number;
|
||||
reasoning: boolean;
|
||||
description: string;
|
||||
}
|
||||
+374
-133
@@ -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 [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);
|
||||
|
||||
const confirmationService = ConfirmationService.getInstance();
|
||||
|
||||
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') {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.trim()) {
|
||||
setIsProcessing(true);
|
||||
const result = await agent.processCommand(input.trim());
|
||||
setHistory(prev => [...prev, { command: input.trim(), result }]);
|
||||
setInput('');
|
||||
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 (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>
|
||||
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],
|
||||
);
|
||||
} 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 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 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} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color="blue">$ </Text>
|
||||
<Text>
|
||||
{input}
|
||||
{!isProcessing && <Text color="white">█</Text>}
|
||||
</Text>
|
||||
{isProcessing && <Text color="yellow"> (processing...)</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
{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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+4
-4
@@ -5,17 +5,17 @@
|
||||
"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"]
|
||||
|
||||
Reference in new issue
Block a user