mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93895392e6 | |||
| 473070e78b | |||
| 12268a777f | |||
| 351c10d814 | |||
| 9de3843605 | |||
| 1d5473e111 | |||
| b6adcf159e | |||
| b1da4a28e6 | |||
| 95abb6fa6e | |||
| a9970cf346 | |||
| 13060a20f9 | |||
| adf603e944 | |||
| af486133a5 | |||
| a97cd47ced | |||
| 68518a2bdb | |||
| fd61db3e12 | |||
| e49066a119 | |||
| efaff7f44f | |||
| d3c970b607 | |||
| 23254fee64 | |||
| fe072ad2e1 |
@@ -0,0 +1,32 @@
|
||||
name: Build and Deploy Docs to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install Dependencies
|
||||
working-directory: ./www
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
working-directory: ./www
|
||||
run: bun run build
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: www/out
|
||||
branch: gh-pages
|
||||
@@ -6,7 +6,6 @@ aidocs/
|
||||
*.log
|
||||
/kit
|
||||
.idea
|
||||
test/
|
||||
build/
|
||||
dist/
|
||||
contribute/output/
|
||||
|
||||
@@ -18,9 +18,10 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
|
||||
## Features
|
||||
|
||||
- **Multi-Provider LLM Support**: Anthropic, OpenAI, Google Gemini, Ollama, Azure OpenAI, AWS Bedrock, OpenRouter, and more
|
||||
- **Built-in Core Tools**: bash, read, write, edit, grep, find, ls - no MCP overhead
|
||||
- **Built-in Core Tools**: bash, read, write, edit, grep, find, ls, spawn_subagent - no MCP overhead
|
||||
- **MCP Integration**: Connect external MCP servers for expanded capabilities
|
||||
- **Extension System**: Write custom tools, commands, widgets, and UI modifications in Go
|
||||
- **Theming**: 22 built-in color themes (KITT, Catppuccin, Dracula, Nord, etc.) with runtime switching and custom theme files
|
||||
- **Interactive TUI**: Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
|
||||
- **Session Management**: Tree-based conversation history with branching support
|
||||
- **Non-Interactive Mode**: Script-friendly positional args with JSON output
|
||||
@@ -29,10 +30,14 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in
|
||||
|
||||
## Installation
|
||||
|
||||
### Using npm (recommended)
|
||||
### Using npm / bun / pnpm
|
||||
|
||||
```bash
|
||||
npm install -g @mark3labs/kit
|
||||
# or
|
||||
bun install -g @mark3labs/kit
|
||||
# or
|
||||
pnpm install -g @mark3labs/kit
|
||||
```
|
||||
|
||||
### Using Go
|
||||
@@ -103,8 +108,8 @@ Kit looks for configuration in the following locations (in order of priority):
|
||||
|
||||
1. CLI flags
|
||||
2. Environment variables (with `KIT_` prefix)
|
||||
3. `./.kit.yml` (project-local)
|
||||
4. `~/.kit.yml` (global)
|
||||
3. `./.kit.yml` / `./.kit.yaml` / `./.kit.json` (project-local)
|
||||
4. `~/.kit.yml` / `~/.kit.yaml` / `~/.kit.json` (global)
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
@@ -179,6 +184,7 @@ mcpServers:
|
||||
--top-p Nucleus sampling 0.0-1.0 (default: 0.95)
|
||||
--top-k Limit top K tokens (default: 40)
|
||||
--stop-sequences Custom stop sequences (comma-separated)
|
||||
--thinking-level Extended thinking level: off, minimal, low, medium, high (default: off)
|
||||
|
||||
# System
|
||||
--config Config file path (default: ~/.kit.yml)
|
||||
@@ -190,28 +196,61 @@ mcpServers:
|
||||
|
||||
```bash
|
||||
# Authentication (for OAuth-enabled providers)
|
||||
kit auth login # Start OAuth flow
|
||||
kit auth logout # Remove credentials
|
||||
kit auth status # Check authentication status
|
||||
kit auth login [provider] # Start OAuth flow (e.g., anthropic)
|
||||
kit auth logout [provider] # Remove credentials for provider
|
||||
kit auth status # Check authentication status
|
||||
|
||||
# Model database
|
||||
kit models # List available models
|
||||
kit models --all # Show all providers (not just Fantasy-compatible)
|
||||
kit update-models # Update local model database from models.dev
|
||||
kit models [provider] # List available models (optionally filter by provider)
|
||||
kit models --all # Show all providers (not just Fantasy-compatible)
|
||||
kit update-models [source] # Update model database (from models.dev, URL, file, or 'embedded')
|
||||
|
||||
# Extension management
|
||||
kit extensions list # List discovered extensions
|
||||
kit extensions validate # Validate extension files
|
||||
kit extensions init # Generate example extension template
|
||||
kit extensions list # List discovered extensions
|
||||
kit extensions validate # Validate extension files
|
||||
kit extensions init # Generate example extension template
|
||||
kit install <git-url> # Install extensions from git repositories
|
||||
kit install -l <git-url> # Install to project-local .kit/git/ directory
|
||||
kit install -u <git-url> # Update an already-installed package
|
||||
kit install --uninstall <pkg> # Remove an installed package
|
||||
|
||||
# Skills
|
||||
kit skill # Install the Kit extensions skill via skills.sh
|
||||
|
||||
# ACP server
|
||||
kit acp # Start as ACP agent (stdio JSON-RPC)
|
||||
kit acp --debug # With debug logging to stderr
|
||||
kit acp # Start as ACP agent (stdio JSON-RPC)
|
||||
kit acp --debug # With debug logging to stderr
|
||||
```
|
||||
|
||||
## Themes
|
||||
|
||||
Kit ships with 22 built-in color themes that control all UI elements. Switch at runtime:
|
||||
|
||||
```
|
||||
/theme dracula
|
||||
/theme catppuccin
|
||||
/theme tokyonight
|
||||
```
|
||||
|
||||
### Custom themes
|
||||
|
||||
Drop a `.yml` file in `~/.config/kit/themes/` (user) or `.kit/themes/` (project):
|
||||
|
||||
```yaml
|
||||
# ~/.config/kit/themes/my-theme.yml
|
||||
primary:
|
||||
light: "#8839ef"
|
||||
dark: "#cba6f7"
|
||||
success:
|
||||
light: "#40a02b"
|
||||
dark: "#a6e3a1"
|
||||
```
|
||||
|
||||
Built-in themes: `kitt`, `catppuccin`, `dracula`, `tokyonight`, `nord`, `gruvbox`, `monokai`, `solarized`, `github`, `one-dark`, `rose-pine`, `ayu`, `material`, `everforest`, `kanagawa`, `amoled`, `synthwave`, `vesper`, `flexoki`, `matrix`, `vercel`, `zenburn`
|
||||
|
||||
## Extension System
|
||||
|
||||
Extensions are Go source files that run via Yaegi interpreter. They can add custom tools, slash commands, widgets, keyboard shortcuts, and intercept lifecycle events.
|
||||
Extensions are Go source files that run via Yaegi interpreter. They can add custom tools, slash commands, widgets, keyboard shortcuts, themes, and intercept lifecycle events.
|
||||
|
||||
### Minimal Extension
|
||||
|
||||
@@ -239,37 +278,69 @@ kit -e examples/extensions/minimal.go
|
||||
|
||||
### Extension Capabilities
|
||||
|
||||
**Lifecycle Events**: OnSessionStart, OnSessionShutdown, OnAgentStart, OnAgentEnd, OnToolCall, OnToolResult, OnInput, OnMessageStart, OnMessageUpdate, OnMessageEnd, OnModelChange, OnContextPrepare, OnBeforeFork, OnBeforeSessionSwitch, OnBeforeCompact
|
||||
**Lifecycle Events**: OnSessionStart, OnSessionShutdown, OnBeforeAgentStart, OnAgentStart, OnAgentEnd, OnToolCall, OnToolExecutionStart, OnToolExecutionEnd, OnToolResult, OnInput, OnMessageStart, OnMessageUpdate, OnMessageEnd, OnModelChange, OnContextPrepare, OnBeforeFork, OnBeforeSessionSwitch, OnBeforeCompact
|
||||
|
||||
**Custom Components**:
|
||||
- **Tools**: Add new tools the LLM can invoke
|
||||
- **Commands**: Register slash commands (e.g., `/mycommand`)
|
||||
- **Options**: Register configurable extension options
|
||||
- **Widgets**: Persistent status displays above/below input
|
||||
- **Headers/Footers**: Persistent content above/below the conversation
|
||||
- **Status Bar**: Custom status bar entries
|
||||
- **Shortcuts**: Global keyboard shortcuts
|
||||
- **Overlays**: Modal dialogs with markdown content
|
||||
- **Tool Renderers**: Customize how tool calls display
|
||||
- **Message Renderers**: Custom rendering for assistant messages
|
||||
- **Editor Interceptors**: Handle key events and wrap rendering
|
||||
- **Interactive Prompts**: Select, confirm, input, and multi-select dialogs
|
||||
- **Subagents**: Spawn in-process child Kit instances
|
||||
- **LLM Completion**: Direct model calls via `Complete()`
|
||||
- **Themes**: Register and switch color themes via `RegisterTheme`, `SetTheme`, `ListThemes`
|
||||
- **Custom Events**: Inter-extension communication via `EmitCustomEvent`
|
||||
|
||||
### Extension Examples
|
||||
|
||||
See the `examples/extensions/` directory:
|
||||
|
||||
- `minimal.go` - Clean UI with custom footer
|
||||
- `notify.go` - Desktop notifications
|
||||
- `widget-status.go` - Persistent status widgets
|
||||
- `custom-editor-demo.go` - Vim-like modal editor
|
||||
- `prompt-demo.go` - Interactive prompts (select/confirm/input)
|
||||
- `tool-logger.go` - Log all tool calls
|
||||
- `overlay-demo.go` - Modal dialogs
|
||||
- `plan-mode.go` - Read-only planning mode
|
||||
- `subagent-widget.go` - Multi-agent orchestration
|
||||
- `auto-commit.go` - Auto-commit on shutdown
|
||||
- `bookmark.go` - Bookmark conversations
|
||||
- `branded-output.go` - Branded output rendering
|
||||
- `compact-notify.go` - Notification on compaction
|
||||
- `confirm-destructive.go` - Confirm destructive operations
|
||||
- `context-inject.go` - Inject context into conversations
|
||||
- `custom-editor-demo.go` - Vim-like modal editor
|
||||
- `dev-reload.go` - Development live-reload
|
||||
- `header-footer-demo.go` - Custom headers and footers
|
||||
- `inline-bash.go` - Inline bash execution
|
||||
- `interactive-shell.go` - Interactive shell integration
|
||||
- `kit-kit.go` - Kit-in-Kit (sub-agent spawning)
|
||||
- `lsp-diagnostics.go` - LSP diagnostic integration
|
||||
- `notify.go` - Desktop notifications
|
||||
- `overlay-demo.go` - Modal dialogs
|
||||
- `permission-gate.go` - Permission gating for tools
|
||||
- `pirate.go` - Pirate-themed personality
|
||||
- `plan-mode.go` - Read-only planning mode
|
||||
- `project-rules.go` - Project-specific rules
|
||||
- `prompt-demo.go` - Interactive prompts (select/confirm/input)
|
||||
- `protected-paths.go` - Path protection for sensitive files
|
||||
- `subagent-widget.go` - Multi-agent orchestration with status widget
|
||||
- `subagent-test.go` - Subagent testing utilities
|
||||
- `summarize.go` - Conversation summarization
|
||||
- `tool-logger.go` - Log all tool calls
|
||||
- `neon-theme.go` - Custom theme registration and switching
|
||||
- `tool-renderer-demo.go` - Custom tool call rendering
|
||||
- `widget-status.go` - Persistent status widgets
|
||||
|
||||
### Loading Extensions
|
||||
|
||||
**Auto-discovery** (loads automatically):
|
||||
- `./.kit/extensions/*.go` (project-local)
|
||||
- `~/.config/kit/extensions/*.go` (global)
|
||||
- `~/.config/kit/extensions/*.go` (global single files)
|
||||
- `~/.config/kit/extensions/*/main.go` (global subdirectory extensions)
|
||||
- `.kit/extensions/*.go` (project-local single files)
|
||||
- `.kit/extensions/*/main.go` (project-local subdirectory extensions)
|
||||
- `~/.local/share/kit/git/` (global git-installed packages)
|
||||
- `.kit/git/` (project-local git-installed packages)
|
||||
|
||||
**Explicit loading**:
|
||||
```bash
|
||||
@@ -282,13 +353,50 @@ kit -e ext1.go -e ext2.go # Multiple extensions
|
||||
kit --no-extensions
|
||||
```
|
||||
|
||||
### Testing Extensions
|
||||
|
||||
Kit provides a testing package to help you write unit tests for your extensions:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
func TestMyExtension(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Emit events and verify behavior
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the extension printed something
|
||||
test.AssertPrinted(t, harness, "session started")
|
||||
}
|
||||
```
|
||||
|
||||
**Available assertions:**
|
||||
- `AssertBlocked()`, `AssertNotBlocked()` — Verify tool blocking
|
||||
- `AssertWidgetSet()`, `AssertWidgetText()` — Verify widget content
|
||||
- `AssertPrinted()`, `AssertPrintedContains()` — Verify output
|
||||
- `AssertToolRegistered()`, `AssertCommandRegistered()` — Verify registration
|
||||
|
||||
See `examples/extensions/tool-logger_test.go` for a complete example with 14 test cases covering tool calls, input handling, and session lifecycle.
|
||||
|
||||
## Session Management
|
||||
|
||||
Kit uses a tree-based session model that supports branching and forking conversations.
|
||||
|
||||
### Session Locations
|
||||
|
||||
- Default: `~/.local/share/kit/sessions/<cwd-hash>/<uuid>.jsonl`
|
||||
- Default: `~/.kit/sessions/<cwd-path>/<timestamp>_<id>.jsonl`
|
||||
- Path separators in the working directory are replaced with `--` (e.g., `/home/user/project` becomes `home--user--project`)
|
||||
- Each line is a session entry (messages, tool calls, extension data)
|
||||
- Supports branching from any message to explore alternate paths
|
||||
|
||||
@@ -355,6 +463,19 @@ host, err := kit.New(ctx, &kit.Options{
|
||||
MaxSteps: 10,
|
||||
Streaming: true,
|
||||
Quiet: true,
|
||||
|
||||
// Session options
|
||||
SessionPath: "./session.jsonl", // Open specific session
|
||||
Continue: true, // Resume most recent session
|
||||
NoSession: true, // Ephemeral mode
|
||||
|
||||
// Tool options
|
||||
ExtraTools: []kit.Tool{...}, // Additional tools alongside defaults
|
||||
|
||||
// Compaction
|
||||
AutoCompact: true, // Auto-compact near context limit
|
||||
|
||||
Debug: true, // Debug logging
|
||||
})
|
||||
```
|
||||
|
||||
@@ -384,14 +505,29 @@ response, err := host.PromptWithCallbacks(
|
||||
### Session Management
|
||||
|
||||
```go
|
||||
// Multi-turn conversations retain context automatically
|
||||
host.Prompt(ctx, "My name is Alice")
|
||||
response, _ := host.Prompt(ctx, "What's my name?")
|
||||
|
||||
host.SaveSession("./session.json")
|
||||
host.LoadSession("./session.json")
|
||||
// Sessions are persisted automatically to JSONL files.
|
||||
// Access session info:
|
||||
path := host.GetSessionPath()
|
||||
id := host.GetSessionID()
|
||||
|
||||
// Clear conversation history
|
||||
host.ClearSession()
|
||||
```
|
||||
|
||||
Session persistence is configured via `Options`:
|
||||
|
||||
```go
|
||||
host, _ := kit.New(ctx, &kit.Options{
|
||||
SessionPath: "./my-session.jsonl", // Open specific session
|
||||
Continue: true, // Resume most recent session
|
||||
NoSession: true, // Ephemeral mode
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Subagent Pattern
|
||||
@@ -413,12 +549,25 @@ Parse the JSON output:
|
||||
{
|
||||
"response": "Final assistant response text",
|
||||
"model": "anthropic/claude-haiku-3-5-20241022",
|
||||
"stop_reason": "end_turn",
|
||||
"session_id": "a1b2c3d4e5f6",
|
||||
"usage": {
|
||||
"input_tokens": 1024,
|
||||
"output_tokens": 512,
|
||||
"total_tokens": 1536
|
||||
"total_tokens": 1536,
|
||||
"cache_read_tokens": 0,
|
||||
"cache_creation_tokens": 0
|
||||
},
|
||||
"messages": [...]
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"parts": [
|
||||
{"type": "text", "data": "..."},
|
||||
{"type": "tool_call", "data": {"name": "...", "args": "..."}},
|
||||
{"type": "tool_result", "data": {"name": "...", "result": "..."}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -468,19 +617,27 @@ go fmt ./...
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
cmd/kit/ - CLI entry point
|
||||
cmd/ - CLI command implementations
|
||||
pkg/kit/ - Go SDK
|
||||
internal/agent/ - Agent loop and tool execution
|
||||
internal/ui/ - Bubble Tea TUI components
|
||||
cmd/kit/ - CLI entry point (main.go)
|
||||
cmd/ - CLI command implementations (root, auth, models, etc.)
|
||||
pkg/kit/ - Go SDK for embedding Kit
|
||||
internal/app/ - Application orchestrator (agent loop, message store, queue)
|
||||
internal/agent/ - Agent execution and tool dispatch
|
||||
internal/auth/ - OAuth authentication and credential storage
|
||||
internal/acpserver/ - ACP (Agent Client Protocol) server
|
||||
internal/clipboard/ - Cross-platform clipboard operations
|
||||
internal/compaction/ - Conversation compaction and summarization
|
||||
internal/config/ - Configuration management
|
||||
internal/core/ - Built-in tools (bash, read, write, edit, grep, find, ls)
|
||||
internal/extensions/ - Yaegi extension system
|
||||
internal/core/ - Built-in tools
|
||||
internal/tools/ - MCP tool integration
|
||||
internal/config/ - Configuration management
|
||||
internal/acpserver/ - ACP (Agent Client Protocol) server
|
||||
internal/session/ - Session persistence
|
||||
internal/models/ - Provider and model management
|
||||
internal/kitsetup/ - Initial setup wizard
|
||||
internal/message/ - Message content types and structured content blocks
|
||||
internal/models/ - Provider and model management
|
||||
internal/session/ - Session persistence (tree-based JSONL)
|
||||
internal/skills/ - Skill loading and system prompt composition
|
||||
internal/tools/ - MCP tool integration
|
||||
internal/ui/ - Bubble Tea TUI components
|
||||
examples/extensions/ - Example extension files
|
||||
npm/ - NPM package wrapper for distribution
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
@@ -509,18 +666,23 @@ google/gemini-2.0-flash-exp
|
||||
### Model Aliases
|
||||
|
||||
```bash
|
||||
claude-opus-latest → claude-opus-4-20250514
|
||||
claude-sonnet-latest → claude-sonnet-4-5-20250929
|
||||
claude-3-5-haiku-latest → claude-3-5-haiku-20241022
|
||||
claude-opus-latest → claude-opus-4-20250514
|
||||
claude-sonnet-latest → claude-sonnet-4-5-20250929
|
||||
claude-4-opus-latest → claude-opus-4-20250514
|
||||
claude-4-sonnet-latest → claude-sonnet-4-5-20250929
|
||||
claude-3-7-sonnet-latest → claude-3-7-sonnet-20250219
|
||||
claude-3-5-sonnet-latest → claude-3-5-sonnet-20241022
|
||||
claude-3-5-haiku-latest → claude-3-5-haiku-20241022
|
||||
claude-3-opus-latest → claude-3-opus-20240229
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
Contributions are welcome! Please see the [contribution guide](contribute/contribute.md) for guidelines.
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
[MIT](LICENSE)
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
+74
-17
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -141,24 +142,58 @@ func LoadConfigWithEnvSubstitution(configPath string) error {
|
||||
return kit.LoadConfigWithEnvSubstitution(configPath)
|
||||
}
|
||||
|
||||
func configToUiTheme(theme config.Theme) ui.Theme {
|
||||
// adaptiveOrDefault converts a config.AdaptiveColor to a resolved color.Color,
|
||||
// falling back to fallback when both Light and Dark are empty.
|
||||
func adaptiveOrDefault(ac config.AdaptiveColor, fallback color.Color) color.Color {
|
||||
if ac.Light == "" && ac.Dark == "" {
|
||||
return fallback
|
||||
}
|
||||
return ui.AdaptiveColor(ac.Light, ac.Dark)
|
||||
}
|
||||
|
||||
func configToUiTheme(cfg config.Theme) ui.Theme {
|
||||
def := ui.DefaultTheme()
|
||||
return ui.Theme{
|
||||
Primary: ui.AdaptiveColor(theme.Primary.Light, theme.Primary.Dark),
|
||||
Secondary: ui.AdaptiveColor(theme.Secondary.Light, theme.Secondary.Dark),
|
||||
Success: ui.AdaptiveColor(theme.Success.Light, theme.Success.Dark),
|
||||
Warning: ui.AdaptiveColor(theme.Warning.Light, theme.Warning.Dark),
|
||||
Error: ui.AdaptiveColor(theme.Error.Light, theme.Error.Dark),
|
||||
Info: ui.AdaptiveColor(theme.Info.Light, theme.Info.Dark),
|
||||
Text: ui.AdaptiveColor(theme.Text.Light, theme.Text.Dark),
|
||||
Muted: ui.AdaptiveColor(theme.Muted.Light, theme.Muted.Dark),
|
||||
VeryMuted: ui.AdaptiveColor(theme.VeryMuted.Light, theme.VeryMuted.Dark),
|
||||
Background: ui.AdaptiveColor(theme.Background.Light, theme.Background.Dark),
|
||||
Border: ui.AdaptiveColor(theme.Border.Light, theme.Border.Dark),
|
||||
MutedBorder: ui.AdaptiveColor(theme.MutedBorder.Light, theme.MutedBorder.Dark),
|
||||
System: ui.AdaptiveColor(theme.System.Light, theme.System.Dark),
|
||||
Tool: ui.AdaptiveColor(theme.Tool.Light, theme.Tool.Dark),
|
||||
Accent: ui.AdaptiveColor(theme.Accent.Light, theme.Accent.Dark),
|
||||
Highlight: ui.AdaptiveColor(theme.Highlight.Light, theme.Highlight.Dark),
|
||||
Primary: adaptiveOrDefault(cfg.Primary, def.Primary),
|
||||
Secondary: adaptiveOrDefault(cfg.Secondary, def.Secondary),
|
||||
Success: adaptiveOrDefault(cfg.Success, def.Success),
|
||||
Warning: adaptiveOrDefault(cfg.Warning, def.Warning),
|
||||
Error: adaptiveOrDefault(cfg.Error, def.Error),
|
||||
Info: adaptiveOrDefault(cfg.Info, def.Info),
|
||||
Text: adaptiveOrDefault(cfg.Text, def.Text),
|
||||
Muted: adaptiveOrDefault(cfg.Muted, def.Muted),
|
||||
VeryMuted: adaptiveOrDefault(cfg.VeryMuted, def.VeryMuted),
|
||||
Background: adaptiveOrDefault(cfg.Background, def.Background),
|
||||
Border: adaptiveOrDefault(cfg.Border, def.Border),
|
||||
MutedBorder: adaptiveOrDefault(cfg.MutedBorder, def.MutedBorder),
|
||||
System: adaptiveOrDefault(cfg.System, def.System),
|
||||
Tool: adaptiveOrDefault(cfg.Tool, def.Tool),
|
||||
Accent: adaptiveOrDefault(cfg.Accent, def.Accent),
|
||||
Highlight: adaptiveOrDefault(cfg.Highlight, def.Highlight),
|
||||
|
||||
DiffInsertBg: adaptiveOrDefault(cfg.DiffInsertBg, def.DiffInsertBg),
|
||||
DiffDeleteBg: adaptiveOrDefault(cfg.DiffDeleteBg, def.DiffDeleteBg),
|
||||
DiffEqualBg: adaptiveOrDefault(cfg.DiffEqualBg, def.DiffEqualBg),
|
||||
DiffMissingBg: adaptiveOrDefault(cfg.DiffMissingBg, def.DiffMissingBg),
|
||||
|
||||
CodeBg: adaptiveOrDefault(cfg.CodeBg, def.CodeBg),
|
||||
GutterBg: adaptiveOrDefault(cfg.GutterBg, def.GutterBg),
|
||||
WriteBg: adaptiveOrDefault(cfg.WriteBg, def.WriteBg),
|
||||
|
||||
Markdown: ui.MarkdownThemeColors{
|
||||
Text: adaptiveOrDefault(cfg.Markdown.Text, def.Markdown.Text),
|
||||
Muted: adaptiveOrDefault(cfg.Markdown.Muted, def.Markdown.Muted),
|
||||
Heading: adaptiveOrDefault(cfg.Markdown.Heading, def.Markdown.Heading),
|
||||
Emph: adaptiveOrDefault(cfg.Markdown.Emph, def.Markdown.Emph),
|
||||
Strong: adaptiveOrDefault(cfg.Markdown.Strong, def.Markdown.Strong),
|
||||
Link: adaptiveOrDefault(cfg.Markdown.Link, def.Markdown.Link),
|
||||
Code: adaptiveOrDefault(cfg.Markdown.Code, def.Markdown.Code),
|
||||
Error: adaptiveOrDefault(cfg.Markdown.Error, def.Markdown.Error),
|
||||
Keyword: adaptiveOrDefault(cfg.Markdown.Keyword, def.Markdown.Keyword),
|
||||
String: adaptiveOrDefault(cfg.Markdown.String, def.Markdown.String),
|
||||
Number: adaptiveOrDefault(cfg.Markdown.Number, def.Markdown.Number),
|
||||
Comment: adaptiveOrDefault(cfg.Markdown.Comment, def.Markdown.Comment),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,6 +936,28 @@ func runNormalMode(ctx context.Context) error {
|
||||
SetActiveTools: func(names []string) {
|
||||
kitInstance.SetExtensionActiveTools(names)
|
||||
},
|
||||
RegisterTheme: func(name string, config extensions.ThemeColorConfig) {
|
||||
tc := func(c extensions.ThemeColor) [2]string { return [2]string{c.Light, c.Dark} }
|
||||
ui.RegisterThemeFromConfig(name,
|
||||
tc(config.Primary), tc(config.Secondary),
|
||||
tc(config.Success), tc(config.Warning),
|
||||
tc(config.Error), tc(config.Info),
|
||||
tc(config.Text), tc(config.Muted),
|
||||
tc(config.VeryMuted), tc(config.Background),
|
||||
tc(config.Border), tc(config.MutedBorder),
|
||||
tc(config.System), tc(config.Tool),
|
||||
tc(config.Accent), tc(config.Highlight),
|
||||
tc(config.MdHeading), tc(config.MdLink),
|
||||
tc(config.MdKeyword), tc(config.MdString),
|
||||
tc(config.MdNumber), tc(config.MdComment),
|
||||
)
|
||||
},
|
||||
SetTheme: func(name string) error {
|
||||
return ui.ApplyTheme(name)
|
||||
},
|
||||
ListThemes: func() []string {
|
||||
return ui.ListThemes()
|
||||
},
|
||||
ShowOverlay: func(config extensions.OverlayConfig) extensions.OverlayResult {
|
||||
ch := make(chan app.OverlayResponse, 1)
|
||||
appInstance.SendOverlayRequest(app.OverlayRequestEvent{
|
||||
|
||||
@@ -83,6 +83,12 @@ kit install github.com/mark3labs/kit/examples/extensions --local
|
||||
|-----------|-------------|---------|
|
||||
| `kit-telegram/` | Telegram relay for remote monitoring & control | `RegisterCommand`, `OnAgentStart/End`, `SetStatus`, `SendMessage` |
|
||||
|
||||
### Themes
|
||||
|
||||
| Extension | Description | Key API |
|
||||
|-----------|-------------|---------|
|
||||
| `neon-theme.go` | Register and switch custom themes | `RegisterTheme`, `SetTheme` |
|
||||
|
||||
### Rendering
|
||||
|
||||
| Extension | Description | Key API |
|
||||
|
||||
@@ -23,8 +23,7 @@ import (
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionShutdown(func(_ ext.SessionShutdownEvent, ctx ext.Context) {
|
||||
// Check for staged changes.
|
||||
diff, err := exec.Command("git", "diff", "--cached", "--quiet").CombinedOutput()
|
||||
_ = diff
|
||||
err := exec.Command("git", "diff", "--cached", "--quiet").Run()
|
||||
if err == nil {
|
||||
return // exit code 0 means no staged changes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Extension Test Template
|
||||
//
|
||||
// This is a template for writing tests for your Kit extension.
|
||||
// Copy this file to your extension directory, rename it to something like
|
||||
// "my-ext_test.go", and customize it for your extension.
|
||||
//
|
||||
// Run tests with: go test -v
|
||||
//
|
||||
// IMPORTANT: This file should be in the same directory as your extension
|
||||
// and use package main, NOT package test.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
)
|
||||
|
||||
// Test that your extension loads without errors
|
||||
func TestExtension_Loads(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
ext := harness.LoadFile("my-ext.go") // Change to your extension filename
|
||||
|
||||
// Verify the extension was loaded
|
||||
if ext == nil {
|
||||
t.Fatal("extension should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test your event handlers are registered
|
||||
func TestExtension_EventHandlers(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Uncomment the handlers your extension uses:
|
||||
// test.AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
// test.AssertHasHandlers(t, harness, extensions.Input)
|
||||
// test.AssertHasHandlers(t, harness, extensions.SessionStart)
|
||||
// test.AssertHasHandlers(t, harness, extensions.AgentEnd)
|
||||
}
|
||||
|
||||
// Test tool registration
|
||||
func TestExtension_Tools(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test that your tools are registered
|
||||
// test.AssertToolRegistered(t, harness, "my_tool")
|
||||
|
||||
// Or test all registered tools
|
||||
tools := harness.RegisteredTools()
|
||||
t.Logf("Registered %d tools", len(tools))
|
||||
for _, tool := range tools {
|
||||
t.Logf(" - %s: %s", tool.Name, tool.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// Test command registration
|
||||
func TestExtension_Commands(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test that your commands are registered
|
||||
// test.AssertCommandRegistered(t, harness, "mycommand")
|
||||
|
||||
// Or test all registered commands
|
||||
cmds := harness.RegisteredCommands()
|
||||
t.Logf("Registered %d commands", len(cmds))
|
||||
for _, cmd := range cmds {
|
||||
t.Logf(" - %s: %s", cmd.Name, cmd.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// Test session start behavior
|
||||
func TestExtension_SessionStart(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Emit session start event
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{
|
||||
SessionID: "test-session",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify expected behavior:
|
||||
// - Did it print something?
|
||||
// test.AssertPrinted(t, harness, "expected output")
|
||||
|
||||
// - Did it set a widget?
|
||||
// test.AssertWidgetSet(t, harness, "my-widget")
|
||||
// test.AssertWidgetText(t, harness, "my-widget", "expected text")
|
||||
|
||||
// - Did it set the header/footer?
|
||||
// test.AssertHeaderSet(t, harness)
|
||||
// test.AssertFooterSet(t, harness)
|
||||
|
||||
// - Did it set a status?
|
||||
// test.AssertStatusSet(t, harness, "myext:status")
|
||||
}
|
||||
|
||||
// Test tool call handling
|
||||
func TestExtension_ToolCall(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test a specific tool call
|
||||
result, err := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "some_tool",
|
||||
Input: `{"key": "value"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// If your extension blocks certain tools:
|
||||
// test.AssertNotBlocked(t, result)
|
||||
// OR
|
||||
// test.AssertBlocked(t, result, "expected reason")
|
||||
|
||||
// Suppress unused variable warning (remove this when using result)
|
||||
_ = result
|
||||
|
||||
// Check for print output
|
||||
// test.AssertPrinted(t, harness, "expected message")
|
||||
}
|
||||
|
||||
// Test input handling
|
||||
func TestExtension_InputHandling(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test input that should be handled
|
||||
result, err := harness.Emit(extensions.InputEvent{
|
||||
Text: "test input",
|
||||
Source: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// If your extension handles/transforms input:
|
||||
// test.AssertInputHandled(t, result, "handled")
|
||||
// OR
|
||||
// test.AssertInputTransformed(t, result, "transformed text")
|
||||
|
||||
// Suppress unused variable warning (remove this when using result)
|
||||
_ = result
|
||||
}
|
||||
|
||||
// Test with configured prompt results
|
||||
func TestExtension_WithPrompts(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Configure what prompts should return
|
||||
harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
|
||||
Value: "option1",
|
||||
Index: 0,
|
||||
Cancelled: false,
|
||||
})
|
||||
|
||||
// Now when your extension calls ctx.PromptSelect(), it gets the configured result
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
// Verify behavior based on the selected options
|
||||
}
|
||||
@@ -23,14 +23,14 @@ import (
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
type RelayConfig struct {
|
||||
Version int `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BotToken string `json:"botToken"`
|
||||
BotID int64 `json:"botId"`
|
||||
BotUsername string `json:"botUsername"`
|
||||
ChatID int64 `json:"chatId"`
|
||||
AllowedUserIDs []int64 `json:"allowedUserIds"`
|
||||
LastValidatedAt string `json:"lastValidatedAt"`
|
||||
Version int `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BotToken string `json:"botToken"`
|
||||
BotID int64 `json:"botId"`
|
||||
BotUsername string `json:"botUsername"`
|
||||
ChatID int64 `json:"chatId"`
|
||||
AllowedUserIDs []int64 `json:"allowedUserIds"`
|
||||
LastValidatedAt string `json:"lastValidatedAt"`
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
@@ -53,17 +53,17 @@ type TelegramChatMember struct {
|
||||
}
|
||||
|
||||
type TelegramMessage struct {
|
||||
MessageID int `json:"message_id"`
|
||||
Date int64 `json:"date"`
|
||||
Text string `json:"text"`
|
||||
Caption string `json:"caption"`
|
||||
From TelegramUser `json:"from"`
|
||||
Chat TelegramChat `json:"chat"`
|
||||
EditDate int64 `json:"edit_date"`
|
||||
MessageID int `json:"message_id"`
|
||||
Date int64 `json:"date"`
|
||||
Text string `json:"text"`
|
||||
Caption string `json:"caption"`
|
||||
From TelegramUser `json:"from"`
|
||||
Chat TelegramChat `json:"chat"`
|
||||
EditDate int64 `json:"edit_date"`
|
||||
}
|
||||
|
||||
type TelegramUpdate struct {
|
||||
UpdateID int64 `json:"update_id"`
|
||||
UpdateID int64 `json:"update_id"`
|
||||
Message *TelegramMessage `json:"message"`
|
||||
EditedMessage *TelegramMessage `json:"edited_message"`
|
||||
}
|
||||
@@ -91,13 +91,13 @@ type RenderAction struct {
|
||||
}
|
||||
|
||||
type ActiveRunState struct {
|
||||
ID int
|
||||
StartedAt time.Time
|
||||
StepCount int
|
||||
ProgressMessageID int
|
||||
LastRenderedText string
|
||||
Actions []RenderAction
|
||||
LastAssistantText string
|
||||
ID int
|
||||
StartedAt time.Time
|
||||
StepCount int
|
||||
ProgressMessageID int
|
||||
LastRenderedText string
|
||||
Actions []RenderAction
|
||||
LastAssistantText string
|
||||
LastAssistantError bool
|
||||
}
|
||||
|
||||
@@ -137,16 +137,16 @@ var (
|
||||
config *RelayConfig
|
||||
|
||||
// Relay connection
|
||||
pollLoopActive bool
|
||||
pollGeneration int
|
||||
pollStopCh chan struct{}
|
||||
lastAPISuccessAt time.Time
|
||||
retryActive bool
|
||||
retryAttempt int
|
||||
retryLogPath string
|
||||
currentOffset int64
|
||||
offsetInitialized bool
|
||||
isConnecting bool
|
||||
pollLoopActive bool
|
||||
pollGeneration int
|
||||
pollStopCh chan struct{}
|
||||
lastAPISuccessAt time.Time
|
||||
retryActive bool
|
||||
retryAttempt int
|
||||
retryLogPath string
|
||||
currentOffset int64
|
||||
offsetInitialized bool
|
||||
isConnecting bool
|
||||
|
||||
// Spinner
|
||||
spinnerIndex int
|
||||
@@ -169,8 +169,8 @@ var (
|
||||
pendingTest *PendingTest
|
||||
|
||||
// Latest context for background goroutines
|
||||
latestCtx ext.Context
|
||||
latestCtxSet bool
|
||||
latestCtx ext.Context
|
||||
latestCtxSet bool
|
||||
|
||||
// Debug mode
|
||||
debugMode bool
|
||||
@@ -255,7 +255,7 @@ func createRetryLogPath() string {
|
||||
return filepath.Join(failureLogDir(), stamp+".log")
|
||||
}
|
||||
|
||||
func appendFailureLog(path string, entry map[string]interface{}) {
|
||||
func appendFailureLog(path string, entry map[string]any) {
|
||||
dir := filepath.Dir(path)
|
||||
os.MkdirAll(dir, 0755)
|
||||
data, _ := json.Marshal(entry)
|
||||
@@ -271,7 +271,7 @@ func appendFailureLog(path string, entry map[string]interface{}) {
|
||||
// Telegram Bot API client
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
func telegramRequest(token string, method string, body map[string]interface{}, timeoutSec int) (json.RawMessage, error) {
|
||||
func telegramRequest(token string, method string, body map[string]any, timeoutSec int) (json.RawMessage, error) {
|
||||
url := fmt.Sprintf("%s/bot%s/%s", telegramAPIBase, token, method)
|
||||
payload, _ := json.Marshal(body)
|
||||
client := &http.Client{Timeout: time.Duration(timeoutSec) * time.Second}
|
||||
@@ -295,7 +295,7 @@ func telegramRequest(token string, method string, body map[string]interface{}, t
|
||||
}
|
||||
|
||||
func tgGetMe(token string) (*TelegramUser, error) {
|
||||
result, err := telegramRequest(token, "getMe", map[string]interface{}{}, 15)
|
||||
result, err := telegramRequest(token, "getMe", map[string]any{}, 15)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func tgGetMe(token string) (*TelegramUser, error) {
|
||||
}
|
||||
|
||||
func tgGetChat(token string, chatID int64) (*TelegramChat, error) {
|
||||
result, err := telegramRequest(token, "getChat", map[string]interface{}{
|
||||
result, err := telegramRequest(token, "getChat", map[string]any{
|
||||
"chat_id": chatID,
|
||||
}, 15)
|
||||
if err != nil {
|
||||
@@ -321,7 +321,7 @@ func tgGetChat(token string, chatID int64) (*TelegramChat, error) {
|
||||
}
|
||||
|
||||
func tgGetChatMember(token string, chatID int64, userID int64) (*TelegramChatMember, error) {
|
||||
result, err := telegramRequest(token, "getChatMember", map[string]interface{}{
|
||||
result, err := telegramRequest(token, "getChatMember", map[string]any{
|
||||
"chat_id": chatID,
|
||||
"user_id": userID,
|
||||
}, 15)
|
||||
@@ -336,7 +336,7 @@ func tgGetChatMember(token string, chatID int64, userID int64) (*TelegramChatMem
|
||||
}
|
||||
|
||||
func tgGetUpdates(token string, offset int64, hasOffset bool, timeoutSeconds int, clientTimeoutSec int) ([]TelegramUpdate, error) {
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"timeout": timeoutSeconds,
|
||||
"allowed_updates": []string{"message", "edited_message"},
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func tgGetUpdates(token string, offset int64, hasOffset bool, timeoutSeconds int
|
||||
}
|
||||
|
||||
func tgSendMessage(token string, chatID int64, text string) (*TelegramMessage, error) {
|
||||
result, err := telegramRequest(token, "sendMessage", map[string]interface{}{
|
||||
result, err := telegramRequest(token, "sendMessage", map[string]any{
|
||||
"chat_id": chatID,
|
||||
"text": text,
|
||||
"disable_web_page_preview": true,
|
||||
@@ -371,9 +371,9 @@ func tgSendMessage(token string, chatID int64, text string) (*TelegramMessage, e
|
||||
}
|
||||
|
||||
func tgEditMessageText(token string, chatID int64, messageID int, text string) (*TelegramMessage, error) {
|
||||
result, err := telegramRequest(token, "editMessageText", map[string]interface{}{
|
||||
result, err := telegramRequest(token, "editMessageText", map[string]any{
|
||||
"chat_id": chatID,
|
||||
"message_id": messageID,
|
||||
"message_id": messageID,
|
||||
"text": text,
|
||||
"disable_web_page_preview": true,
|
||||
}, 30)
|
||||
@@ -480,7 +480,7 @@ func handleAPIFailure(err error, operation string) {
|
||||
attempt := retryAttempt
|
||||
mu.Unlock()
|
||||
|
||||
appendFailureLog(logPath, map[string]interface{}{
|
||||
appendFailureLog(logPath, map[string]any{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"operation": operation,
|
||||
"attempt": attempt,
|
||||
@@ -880,10 +880,10 @@ func formatElapsed(d time.Duration) string {
|
||||
}
|
||||
|
||||
func summarizeToolAction(toolName string, inputJSON string) string {
|
||||
var args map[string]interface{}
|
||||
var args map[string]any
|
||||
json.Unmarshal([]byte(inputJSON), &args)
|
||||
if args == nil {
|
||||
args = make(map[string]interface{})
|
||||
args = make(map[string]any)
|
||||
}
|
||||
getStr := func(key string, fallback string) string {
|
||||
if v, ok := args[key]; ok {
|
||||
@@ -919,10 +919,10 @@ func summarizeToolResult(toolName string, inputJSON string, isError bool) string
|
||||
if isError {
|
||||
return "failed " + summarizeToolAction(toolName, inputJSON)
|
||||
}
|
||||
var args map[string]interface{}
|
||||
var args map[string]any
|
||||
json.Unmarshal([]byte(inputJSON), &args)
|
||||
if args == nil {
|
||||
args = make(map[string]interface{})
|
||||
args = make(map[string]any)
|
||||
}
|
||||
getStr := func(key string, fallback string) string {
|
||||
if v, ok := args[key]; ok {
|
||||
@@ -1718,7 +1718,7 @@ func runConnectFlow(ctx ext.Context) {
|
||||
Enabled: enableNow,
|
||||
BotToken: token,
|
||||
BotID: me.ID,
|
||||
BotUsername: me.Username,
|
||||
BotUsername: me.Username,
|
||||
ChatID: resolved.chatID,
|
||||
AllowedUserIDs: resolved.allowedUserIDs,
|
||||
LastValidatedAt: time.Now().Format(time.RFC3339),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
// Init registers a "neon" theme and a /neon slash command to apply it.
|
||||
// Demonstrates how extensions can create and set themes programmatically.
|
||||
//
|
||||
// Usage: kit -e examples/extensions/neon-theme.go
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
// Register a cyberpunk neon theme at startup.
|
||||
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
|
||||
Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
|
||||
Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
|
||||
Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
|
||||
Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
|
||||
Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
|
||||
MdKeyword: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
MdString: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
MdComment: ext.ThemeColor{Light: "#888888", Dark: "#555555"},
|
||||
})
|
||||
|
||||
ctx.PrintInfo("Neon theme registered! Use /theme neon to activate.")
|
||||
})
|
||||
|
||||
// Also register a /neon slash command as a shortcut.
|
||||
api.RegisterCommand(ext.CommandDef{
|
||||
Name: "neon",
|
||||
Description: "Switch to the neon cyberpunk theme",
|
||||
Execute: func(args string, ctx ext.Context) (string, error) {
|
||||
if err := ctx.SetTheme("neon"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Neon theme activated!", nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
)
|
||||
|
||||
// Test that the tool-logger extension loads and registers handlers
|
||||
func TestToolLogger_Loads(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
ext := harness.LoadFile("tool-logger.go")
|
||||
|
||||
if ext == nil {
|
||||
t.Fatal("extension should not be nil")
|
||||
}
|
||||
|
||||
// Verify all expected handlers are registered
|
||||
test.AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
test.AssertHasHandlers(t, harness, extensions.ToolResult)
|
||||
test.AssertHasHandlers(t, harness, extensions.SessionStart)
|
||||
test.AssertHasHandlers(t, harness, extensions.SessionShutdown)
|
||||
test.AssertHasHandlers(t, harness, extensions.Input)
|
||||
}
|
||||
|
||||
// Test that tool calls are logged (handlers run without errors)
|
||||
func TestToolLogger_ToolCall(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
// Emit a tool call event
|
||||
result, err := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "Read",
|
||||
ToolCallID: "call-123",
|
||||
Input: `{"file": "test.txt"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Tool logger should not block any tools
|
||||
test.AssertNotBlocked(t, result)
|
||||
}
|
||||
|
||||
// Test that tool results are processed
|
||||
func TestToolLogger_ToolResult(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
content := "Hello, World!"
|
||||
result, err := harness.Emit(extensions.ToolResultEvent{
|
||||
ToolName: "Read",
|
||||
Content: content,
|
||||
IsError: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Tool logger should not modify results
|
||||
if result != nil {
|
||||
t.Error("expected nil result (no modification)")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that error tool results are handled
|
||||
func TestToolLogger_ToolResultError(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
result, err := harness.Emit(extensions.ToolResultEvent{
|
||||
ToolName: "Bash",
|
||||
Content: "command not found",
|
||||
IsError: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Error("expected nil result (no modification)")
|
||||
}
|
||||
}
|
||||
|
||||
// Test session start handler
|
||||
func TestToolLogger_SessionStart(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{
|
||||
SessionID: "test-session-123",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Handler should run without errors (logs to file)
|
||||
// Since file logging happens outside our mock, we just verify no errors
|
||||
}
|
||||
|
||||
// Test session shutdown handler
|
||||
func TestToolLogger_SessionShutdown(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionShutdownEvent{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the !time command
|
||||
func TestToolLogger_TimeCommand(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
result, err := harness.Emit(extensions.InputEvent{
|
||||
Text: "!time",
|
||||
Source: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
test.AssertInputHandled(t, result, "handled")
|
||||
|
||||
// Verify PrintInfo was called with a time message
|
||||
infos := harness.Context().GetPrintInfos()
|
||||
found := false
|
||||
for _, info := range infos {
|
||||
if strings.Contains(info, "Current time:") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected PrintInfo with 'Current time:', got: %v", infos)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the !status command
|
||||
func TestToolLogger_StatusCommand(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
result, err := harness.Emit(extensions.InputEvent{
|
||||
Text: "!status",
|
||||
Source: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
test.AssertInputHandled(t, result, "handled")
|
||||
|
||||
// Verify PrintBlock was called
|
||||
blocks := harness.Context().PrintBlocks
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("expected 1 PrintBlock call, got %d", len(blocks))
|
||||
}
|
||||
|
||||
block := blocks[0]
|
||||
if block.Subtitle != "tool-logger extension" {
|
||||
t.Errorf("expected subtitle 'tool-logger extension', got %q", block.Subtitle)
|
||||
}
|
||||
if block.BorderColor != "#a6e3a1" {
|
||||
t.Errorf("expected border color '#a6e3a1', got %q", block.BorderColor)
|
||||
}
|
||||
if !strings.Contains(block.Text, "Session active") {
|
||||
t.Errorf("expected text to contain 'Session active', got %q", block.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that unknown commands are not handled
|
||||
func TestToolLogger_UnknownCommand(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
result, err := harness.Emit(extensions.InputEvent{
|
||||
Text: "!unknown",
|
||||
Source: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result for unknown command, got %v", result)
|
||||
}
|
||||
|
||||
// Verify no info/block prints for unknown commands
|
||||
if len(harness.Context().GetPrintInfos()) != 0 {
|
||||
t.Error("expected no PrintInfo calls for unknown command")
|
||||
}
|
||||
if len(harness.Context().PrintBlocks) != 0 {
|
||||
t.Error("expected no PrintBlock calls for unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
// Test regular text input (not a command)
|
||||
func TestToolLogger_RegularInput(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
result, err := harness.Emit(extensions.InputEvent{
|
||||
Text: "This is a normal message",
|
||||
Source: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result for regular input, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// Test complete session flow
|
||||
func TestToolLogger_FullSession(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
// Simulate a full session
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Several tool calls
|
||||
tools := []string{"Read", "Glob", "Grep", "Bash"}
|
||||
for _, tool := range tools {
|
||||
_, err := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: tool,
|
||||
Input: "{}",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error for tool %s: %v", tool, err)
|
||||
}
|
||||
|
||||
_, err = harness.Emit(extensions.ToolResultEvent{
|
||||
ToolName: tool,
|
||||
Content: "result",
|
||||
IsError: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error for tool result %s: %v", tool, err)
|
||||
}
|
||||
}
|
||||
|
||||
// User issues a command
|
||||
_, err = harness.Emit(extensions.InputEvent{Text: "!time", Source: "cli"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
_, err = harness.Emit(extensions.SessionShutdownEvent{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the !time command was handled
|
||||
if len(harness.Context().GetPrintInfos()) != 1 {
|
||||
t.Errorf("expected 1 PrintInfo call, got %d", len(harness.Context().GetPrintInfos()))
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the extension handles file write errors gracefully
|
||||
func TestToolLogger_FileError(t *testing.T) {
|
||||
// This test verifies the extension doesn't panic when file operations fail
|
||||
// Since we can't easily mock os.OpenFile, we rely on the extension code
|
||||
// properly checking for errors (which it does)
|
||||
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
// Just verify the handlers run without panicking
|
||||
_, err := harness.Emit(extensions.ToolCallEvent{ToolName: "Read", Input: "{}"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
_, err = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test concurrent tool calls (race condition check)
|
||||
func TestToolLogger_ConcurrentToolCalls(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
// Run multiple tool calls concurrently
|
||||
done := make(chan bool, 10)
|
||||
for i := range 10 {
|
||||
go func(index int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
toolName := "Tool" + string(rune('0'+index))
|
||||
_, err := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: toolName,
|
||||
Input: "{}",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("error in goroutine %d: %v", index, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for range 10 {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// Test the actual log file is created and written to
|
||||
func TestToolLogger_LogFile(t *testing.T) {
|
||||
logFile := "/tmp/kit-tool-log.txt"
|
||||
|
||||
// Clean up before test
|
||||
_ = os.Remove(logFile)
|
||||
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("tool-logger.go")
|
||||
|
||||
// Emit events
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
_, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "Read", Input: "{}"})
|
||||
_, _ = harness.Emit(extensions.ToolResultEvent{ToolName: "Read", Content: "data", IsError: false})
|
||||
|
||||
// Note: Since the extension writes to a real file and the test harness
|
||||
// mocks the context, the file writes actually happen. Let's verify.
|
||||
|
||||
// Give it a moment for file operations
|
||||
if _, err := os.Stat(logFile); err == nil {
|
||||
// File exists - read and verify content
|
||||
content, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
t.Logf("Could not read log file: %v", err)
|
||||
} else {
|
||||
contentStr := string(content)
|
||||
if !strings.Contains(contentStr, "SESSION_START") {
|
||||
t.Error("log file should contain SESSION_START")
|
||||
}
|
||||
if !strings.Contains(contentStr, "CALL tool=Read") {
|
||||
t.Error("log file should contain CALL tool=Read")
|
||||
}
|
||||
if !strings.Contains(contentStr, "RESULT tool=Read") {
|
||||
t.Error("log file should contain RESULT tool=Read")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Log("Note: Log file not created - this is expected since the extension writes directly to disk")
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func Init(api ext.API) {
|
||||
DisplayName: "File",
|
||||
BorderColor: "#89b4fa", // Catppuccin blue
|
||||
RenderHeader: func(toolArgs string, width int) string {
|
||||
var args map[string]interface{}
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(toolArgs), &args); err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func Init(api ext.API) {
|
||||
Background: "#1e1e2e", // Dark background
|
||||
BorderColor: "#a6e3a1", // Catppuccin green
|
||||
RenderHeader: func(toolArgs string, width int) string {
|
||||
var args map[string]interface{}
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(toolArgs), &args); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ func NewOAuthClient() *OAuthClient {
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratePKCE generates a cryptographically secure PKCE verifier and challenge pair
|
||||
// generatePKCE generates a cryptographically secure PKCE verifier and challenge pair
|
||||
// for the OAuth 2.0 PKCE flow. The verifier is a random 32-byte string encoded as
|
||||
// base64url, and the challenge is the SHA256 hash of the verifier, also base64url encoded.
|
||||
// Returns the verifier (to be stored securely), challenge (to be sent with auth request),
|
||||
// and any error encountered during generation.
|
||||
func GeneratePKCE() (verifier, challenge string, err error) {
|
||||
func generatePKCE() (verifier, challenge string, err error) {
|
||||
// Generate 32 bytes of random data
|
||||
verifierBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(verifierBytes); err != nil {
|
||||
@@ -76,7 +76,7 @@ func GeneratePKCE() (verifier, challenge string, err error) {
|
||||
// and PKCE challenge. Returns an AuthData structure containing the URL for user
|
||||
// authentication and the PKCE verifier for the subsequent code exchange.
|
||||
func (c *OAuthClient) GetAuthorizationURL() (*AuthData, error) {
|
||||
verifier, challenge, err := GeneratePKCE()
|
||||
verifier, challenge, err := generatePKCE()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate PKCE: %w", err)
|
||||
}
|
||||
|
||||
@@ -71,5 +71,5 @@ func DetectMediaType(data []byte) string {
|
||||
// ErrNoImage is returned when the clipboard does not contain image data.
|
||||
var ErrNoImage = fmt.Errorf("no image data on clipboard")
|
||||
|
||||
// ErrNoClipboardTool is returned when no suitable clipboard tool is found.
|
||||
var ErrNoClipboardTool = fmt.Errorf("no clipboard tool available (install xclip, wl-paste, or use macOS)")
|
||||
// errNoClipboardTool is returned when no suitable clipboard tool is found.
|
||||
var errNoClipboardTool = fmt.Errorf("no clipboard tool available (install xclip, wl-paste, or use macOS)")
|
||||
|
||||
@@ -7,9 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// ReadImage reads image data from the system clipboard on macOS.
|
||||
// It uses osascript to check if the clipboard contains an image and then
|
||||
// reads the data using a temporary approach. If the clipboard contains
|
||||
// an image, it writes it to stdout as PNG data.
|
||||
// It uses osascript to check if the clipboard contains an image via
|
||||
// NSPasteboard and writes it to stdout as PNG data.
|
||||
func ReadImage() (*ImageData, error) {
|
||||
// Use osascript to write clipboard image to stdout via a pipe.
|
||||
// The script checks if the clipboard has a «class PNGf» item.
|
||||
|
||||
@@ -41,7 +41,7 @@ func ReadImage() (*ImageData, error) {
|
||||
return nil, ErrNoImage
|
||||
}
|
||||
|
||||
return nil, ErrNoClipboardTool
|
||||
return nil, errNoClipboardTool
|
||||
}
|
||||
|
||||
// readWithXclip reads image data using xclip.
|
||||
|
||||
@@ -5,5 +5,5 @@ package clipboard
|
||||
// ReadImage reads image data from the system clipboard on Windows.
|
||||
// Windows clipboard image support is not yet implemented.
|
||||
func ReadImage() (*ImageData, error) {
|
||||
return nil, ErrNoClipboardTool
|
||||
return nil, errNoClipboardTool
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// Token estimation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// EstimateTokens provides a rough token count (~4 chars per token).
|
||||
func EstimateTokens(text string) int {
|
||||
// estimateTokens provides a rough token count (~4 chars per token).
|
||||
func estimateTokens(text string) int {
|
||||
return len(text) / 4
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func estimateSingleMessageTokens(msg fantasy.Message) int {
|
||||
total := 0
|
||||
for _, part := range msg.Content {
|
||||
if tp, ok := part.(fantasy.TextPart); ok {
|
||||
total += EstimateTokens(tp.Text)
|
||||
total += estimateTokens(tp.Text)
|
||||
}
|
||||
}
|
||||
return total
|
||||
|
||||
@@ -36,9 +36,9 @@ func TestEstimateTokens(t *testing.T) {
|
||||
{"hello world", 2}, // 11 / 4 = 2
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := EstimateTokens(tt.text)
|
||||
got := estimateTokens(tt.text)
|
||||
if got != tt.want {
|
||||
t.Errorf("EstimateTokens(%q) = %d, want %d", tt.text, got, tt.want)
|
||||
t.Errorf("estimateTokens(%q) = %d, want %d", tt.text, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-37
@@ -105,42 +105,56 @@ type AdaptiveColor struct {
|
||||
Dark string `json:"dark,omitempty" yaml:"dark,omitempty"`
|
||||
}
|
||||
|
||||
// MarkdownThemeConfig defines color overrides for markdown rendering and
|
||||
// syntax highlighting.
|
||||
type MarkdownThemeConfig struct {
|
||||
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
||||
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
||||
Heading AdaptiveColor `json:"heading,omitzero" yaml:"heading,omitempty"`
|
||||
Emph AdaptiveColor `json:"emph,omitzero" yaml:"emph,omitempty"`
|
||||
Strong AdaptiveColor `json:"strong,omitzero" yaml:"strong,omitempty"`
|
||||
Link AdaptiveColor `json:"link,omitzero" yaml:"link,omitempty"`
|
||||
Code AdaptiveColor `json:"code,omitzero" yaml:"code,omitempty"`
|
||||
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
||||
Keyword AdaptiveColor `json:"keyword,omitzero" yaml:"keyword,omitempty"`
|
||||
String AdaptiveColor `json:"string,omitzero" yaml:"string,omitempty"`
|
||||
Number AdaptiveColor `json:"number,omitzero" yaml:"number,omitempty"`
|
||||
Comment AdaptiveColor `json:"comment,omitzero" yaml:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Theme defines the color scheme for the application UI with adaptive colors
|
||||
// that support both light and dark modes.
|
||||
type Theme struct {
|
||||
Primary AdaptiveColor `json:"primary" yaml:"primary"`
|
||||
Secondary AdaptiveColor `json:"secondary" yaml:"secondary"`
|
||||
Success AdaptiveColor `json:"success" yaml:"success"`
|
||||
Warning AdaptiveColor `json:"warning" yaml:"warning"`
|
||||
Error AdaptiveColor `json:"error" yaml:"error"`
|
||||
Info AdaptiveColor `json:"info" yaml:"info"`
|
||||
Text AdaptiveColor `json:"text" yaml:"text"`
|
||||
Muted AdaptiveColor `json:"muted" yaml:"muted"`
|
||||
VeryMuted AdaptiveColor `json:"very-muted" yaml:"very-muted"`
|
||||
Background AdaptiveColor `json:"background" yaml:"background"`
|
||||
Border AdaptiveColor `json:"border" yaml:"border"`
|
||||
MutedBorder AdaptiveColor `json:"muted-border" yaml:"muted-border"`
|
||||
System AdaptiveColor `json:"system" yaml:"system"`
|
||||
Tool AdaptiveColor `json:"tool" yaml:"tool"`
|
||||
Accent AdaptiveColor `json:"accent" yaml:"accent"`
|
||||
Highlight AdaptiveColor `json:"highlight" yaml:"highlight"`
|
||||
}
|
||||
Primary AdaptiveColor `json:"primary,omitzero" yaml:"primary,omitempty"`
|
||||
Secondary AdaptiveColor `json:"secondary,omitzero" yaml:"secondary,omitempty"`
|
||||
Success AdaptiveColor `json:"success,omitzero" yaml:"success,omitempty"`
|
||||
Warning AdaptiveColor `json:"warning,omitzero" yaml:"warning,omitempty"`
|
||||
Error AdaptiveColor `json:"error,omitzero" yaml:"error,omitempty"`
|
||||
Info AdaptiveColor `json:"info,omitzero" yaml:"info,omitempty"`
|
||||
Text AdaptiveColor `json:"text,omitzero" yaml:"text,omitempty"`
|
||||
Muted AdaptiveColor `json:"muted,omitzero" yaml:"muted,omitempty"`
|
||||
VeryMuted AdaptiveColor `json:"very-muted,omitzero" yaml:"very-muted,omitempty"`
|
||||
Background AdaptiveColor `json:"background,omitzero" yaml:"background,omitempty"`
|
||||
Border AdaptiveColor `json:"border,omitzero" yaml:"border,omitempty"`
|
||||
MutedBorder AdaptiveColor `json:"muted-border,omitzero" yaml:"muted-border,omitempty"`
|
||||
System AdaptiveColor `json:"system,omitzero" yaml:"system,omitempty"`
|
||||
Tool AdaptiveColor `json:"tool,omitzero" yaml:"tool,omitempty"`
|
||||
Accent AdaptiveColor `json:"accent,omitzero" yaml:"accent,omitempty"`
|
||||
Highlight AdaptiveColor `json:"highlight,omitzero" yaml:"highlight,omitempty"`
|
||||
|
||||
// MarkdownTheme defines the color scheme for markdown rendering with syntax
|
||||
// highlighting support and adaptive colors for light and dark modes.
|
||||
type MarkdownTheme struct {
|
||||
Text AdaptiveColor `json:"text" yaml:"text"`
|
||||
Muted AdaptiveColor `json:"muted" yaml:"muted"`
|
||||
Heading AdaptiveColor `json:"heading" yaml:"heading"`
|
||||
Emph AdaptiveColor `json:"emph" yaml:"emph"`
|
||||
Strong AdaptiveColor `json:"strong" yaml:"strong"`
|
||||
Link AdaptiveColor `json:"link" yaml:"link"`
|
||||
Code AdaptiveColor `json:"code" yaml:"code"`
|
||||
Error AdaptiveColor `json:"error" yaml:"error"`
|
||||
Keyword AdaptiveColor `json:"keyword" yaml:"keyword"`
|
||||
String AdaptiveColor `json:"string" yaml:"string"`
|
||||
Number AdaptiveColor `json:"number" yaml:"number"`
|
||||
Comment AdaptiveColor `json:"comment" yaml:"comment"`
|
||||
// Diff block backgrounds
|
||||
DiffInsertBg AdaptiveColor `json:"diff-insert-bg,omitzero" yaml:"diff-insert-bg,omitempty"`
|
||||
DiffDeleteBg AdaptiveColor `json:"diff-delete-bg,omitzero" yaml:"diff-delete-bg,omitempty"`
|
||||
DiffEqualBg AdaptiveColor `json:"diff-equal-bg,omitzero" yaml:"diff-equal-bg,omitempty"`
|
||||
DiffMissingBg AdaptiveColor `json:"diff-missing-bg,omitzero" yaml:"diff-missing-bg,omitempty"`
|
||||
|
||||
// Code/output block backgrounds
|
||||
CodeBg AdaptiveColor `json:"code-bg,omitzero" yaml:"code-bg,omitempty"`
|
||||
GutterBg AdaptiveColor `json:"gutter-bg,omitzero" yaml:"gutter-bg,omitempty"`
|
||||
WriteBg AdaptiveColor `json:"write-bg,omitzero" yaml:"write-bg,omitempty"`
|
||||
|
||||
// Markdown rendering and syntax highlighting
|
||||
Markdown MarkdownThemeConfig `json:"markdown,omitzero" yaml:"markdown,omitempty"`
|
||||
}
|
||||
|
||||
// Config represents the complete application configuration including MCP servers,
|
||||
@@ -157,7 +171,6 @@ type Config struct {
|
||||
ProviderURL string `json:"provider-url,omitempty" yaml:"provider-url,omitempty"`
|
||||
Stream *bool `json:"stream,omitempty" yaml:"stream,omitempty"`
|
||||
Theme any `json:"theme" yaml:"theme"`
|
||||
MarkdownTheme any `json:"markdown-theme" yaml:"markdown-theme"`
|
||||
// Model generation parameters
|
||||
MaxTokens int `json:"max-tokens,omitempty" yaml:"max-tokens,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
||||
@@ -373,11 +386,10 @@ func FilepathOr[T any](key string, value *T) error {
|
||||
fmt.Fprintf(os.Stderr, "%q", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if filepath.Ext(absPath) == ".json" {
|
||||
switch filepath.Ext(absPath) {
|
||||
case ".json":
|
||||
return json.Unmarshal(b, value)
|
||||
}
|
||||
|
||||
if filepath.Ext(absPath) == ".yaml" {
|
||||
case ".yaml", ".yml":
|
||||
return yaml.Unmarshal(b, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +485,36 @@ type Context struct {
|
||||
// ctx.RenderMessage("build-status", "All 42 tests passed.")
|
||||
RenderMessage func(rendererName string, content string)
|
||||
|
||||
// RegisterTheme adds a named theme to the runtime theme registry.
|
||||
// If a theme with the same name already exists it is replaced.
|
||||
// The theme becomes available via /theme and ctx.SetTheme().
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
// Primary: ext.ThemeColor{Dark: "#FF00FF"},
|
||||
// Secondary: ext.ThemeColor{Dark: "#00FFFF"},
|
||||
// Success: ext.ThemeColor{Dark: "#00FF00"},
|
||||
// Warning: ext.ThemeColor{Dark: "#FFFF00"},
|
||||
// Error: ext.ThemeColor{Dark: "#FF0000"},
|
||||
// Info: ext.ThemeColor{Dark: "#00FFFF"},
|
||||
// Text: ext.ThemeColor{Dark: "#FFFFFF"},
|
||||
// Background: ext.ThemeColor{Dark: "#000000"},
|
||||
// })
|
||||
RegisterTheme func(name string, config ThemeColorConfig)
|
||||
|
||||
// SetTheme switches the active color theme by name. The name must
|
||||
// match a built-in theme, a user/project theme file, or a theme
|
||||
// registered via RegisterTheme. Returns an error if not found.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// err := ctx.SetTheme("neon")
|
||||
SetTheme func(name string) error
|
||||
|
||||
// ListThemes returns the names of all available themes.
|
||||
ListThemes func() []string
|
||||
|
||||
// ReloadExtensions hot-reloads all extensions from disk. Existing
|
||||
// extensions receive a SessionShutdown event, then new code is loaded
|
||||
// and receives a SessionStart event. Event handlers, commands,
|
||||
@@ -1723,3 +1753,44 @@ type BeforeCompactResult struct {
|
||||
}
|
||||
|
||||
func (BeforeCompactResult) isResult() {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme types (exposed to Yaegi — concrete structs, string hex colors)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ThemeColor is an adaptive color pair with light and dark hex values.
|
||||
// Either field may be empty to inherit from the default theme.
|
||||
type ThemeColor struct {
|
||||
Light string
|
||||
Dark string
|
||||
}
|
||||
|
||||
// ThemeColorConfig defines a complete color theme that extensions can register
|
||||
// programmatically via ctx.RegisterTheme(). Uses plain hex strings (not
|
||||
// color.Color) so the type is safe to pass across the Yaegi boundary.
|
||||
type ThemeColorConfig struct {
|
||||
Primary ThemeColor
|
||||
Secondary ThemeColor
|
||||
Success ThemeColor
|
||||
Warning ThemeColor
|
||||
Error ThemeColor
|
||||
Info ThemeColor
|
||||
Text ThemeColor
|
||||
Muted ThemeColor
|
||||
VeryMuted ThemeColor
|
||||
Background ThemeColor
|
||||
Border ThemeColor
|
||||
MutedBorder ThemeColor
|
||||
System ThemeColor
|
||||
Tool ThemeColor
|
||||
Accent ThemeColor
|
||||
Highlight ThemeColor
|
||||
|
||||
// Markdown/syntax highlighting overrides.
|
||||
MdHeading ThemeColor
|
||||
MdLink ThemeColor
|
||||
MdKeyword ThemeColor
|
||||
MdString ThemeColor
|
||||
MdNumber ThemeColor
|
||||
MdComment ThemeColor
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ func Symbols() interp.Exports {
|
||||
"SubagentHandle": reflect.ValueOf((*SubagentHandle)(nil)),
|
||||
"SubagentEvent": reflect.ValueOf((*SubagentEvent)(nil)),
|
||||
|
||||
// Theme types
|
||||
"ThemeColor": reflect.ValueOf((*ThemeColor)(nil)),
|
||||
"ThemeColorConfig": reflect.ValueOf((*ThemeColorConfig)(nil)),
|
||||
|
||||
// Event structs
|
||||
"ToolCallEvent": reflect.ValueOf((*ToolCallEvent)(nil)),
|
||||
"ToolCallResult": reflect.ValueOf((*ToolCallResult)(nil)),
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
# Testing Kit Extensions
|
||||
|
||||
The `github.com/mark3labs/kit/internal/extensions/test` package provides utilities for testing Kit extensions using standard Go testing patterns.
|
||||
|
||||
## Overview
|
||||
|
||||
Extension tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance. This allows you to:
|
||||
|
||||
- Test event handlers without running the full Kit TUI
|
||||
- Verify that your extension registers tools/commands correctly
|
||||
- Assert that context methods (Print, SetWidget, etc.) are called as expected
|
||||
- Test blocking and non-blocking event handling
|
||||
|
||||
## Installation
|
||||
|
||||
The test package is part of the Kit codebase. Import it in your extension tests:
|
||||
|
||||
```go
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Testing an Extension File
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
func TestMyExtension(t *testing.T) {
|
||||
// Create a test harness
|
||||
harness := test.New(t)
|
||||
|
||||
// Load your extension
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Emit events and verify behavior
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the extension printed something
|
||||
test.AssertPrinted(t, harness, "session started")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Inline Extension Code
|
||||
|
||||
For quick tests, you can load extension source directly:
|
||||
|
||||
```go
|
||||
func TestToolBlocking(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
|
||||
if tc.ToolName == "dangerous" {
|
||||
return &ext.ToolCallResult{Block: true, Reason: "not allowed"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
harness := test.New(t)
|
||||
harness.LoadString(src, "test-ext.go")
|
||||
|
||||
// Test the tool is blocked
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "dangerous",
|
||||
Input: "{}",
|
||||
})
|
||||
|
||||
test.AssertBlocked(t, result, "not allowed")
|
||||
}
|
||||
```
|
||||
|
||||
## Common Testing Patterns
|
||||
|
||||
### Testing Tool Registration
|
||||
|
||||
```go
|
||||
func TestToolRegistration(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Verify the tool was registered
|
||||
test.AssertToolRegistered(t, harness, "my_tool")
|
||||
|
||||
// Or inspect tools directly
|
||||
tools := harness.RegisteredTools()
|
||||
for _, tool := range tools {
|
||||
if tool.Name == "my_tool" {
|
||||
t.Logf("Tool description: %s", tool.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Command Registration
|
||||
|
||||
```go
|
||||
func TestCommandRegistration(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
test.AssertCommandRegistered(t, harness, "mycommand")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Widgets
|
||||
|
||||
```go
|
||||
func TestWidgetBehavior(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Trigger the event that creates the widget
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
// Verify the widget was set
|
||||
test.AssertWidgetSet(t, harness, "my-widget")
|
||||
|
||||
// Verify specific widget content
|
||||
test.AssertWidgetText(t, harness, "my-widget", "Expected Text")
|
||||
|
||||
// Or verify partial content
|
||||
test.AssertWidgetTextContains(t, harness, "my-widget", "partial")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Input Handling
|
||||
|
||||
```go
|
||||
func TestInputHandling(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test that the extension handles certain input
|
||||
result, _ := harness.Emit(extensions.InputEvent{
|
||||
Text: "secret password",
|
||||
Source: "cli",
|
||||
})
|
||||
|
||||
test.AssertInputHandled(t, result, "handled")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Print Functions
|
||||
|
||||
```go
|
||||
func TestPrintOutput(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "test",
|
||||
Input: "{}",
|
||||
})
|
||||
|
||||
// Assert exact match
|
||||
test.AssertPrinted(t, harness, "exact output")
|
||||
|
||||
// Or partial match
|
||||
test.AssertPrintedContains(t, harness, "partial")
|
||||
|
||||
// Assert info/error messages
|
||||
test.AssertPrintInfo(t, harness, "info message")
|
||||
test.AssertPrintError(t, harness, "error message")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Status Bar
|
||||
|
||||
```go
|
||||
func TestStatusBar(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.AgentEndEvent{})
|
||||
|
||||
test.AssertStatusSet(t, harness, "myext:status")
|
||||
test.AssertStatusText(t, harness, "myext:status", "Ready")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Prompt Results
|
||||
|
||||
Configure the mock context to return specific prompt results:
|
||||
|
||||
```go
|
||||
func TestWithPrompts(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Configure prompt results before emitting events
|
||||
harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
|
||||
Value: "option1",
|
||||
Index: 0,
|
||||
Cancelled: false,
|
||||
})
|
||||
|
||||
// Now when your extension calls ctx.PromptSelect(), it will get this result
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
}
|
||||
```
|
||||
|
||||
## Available Assertions
|
||||
|
||||
The test package provides these assertion helpers:
|
||||
|
||||
**Event Results:**
|
||||
- `AssertNotBlocked(t, result)` - Verify tool was not blocked
|
||||
- `AssertBlocked(t, result, reason)` - Verify tool was blocked with reason
|
||||
- `AssertInputHandled(t, result, action)` - Verify input was handled
|
||||
- `AssertInputTransformed(t, result, text)` - Verify input transformation
|
||||
|
||||
**Context Interactions:**
|
||||
- `AssertPrinted(t, harness, text)` - Verify exact print output
|
||||
- `AssertPrintedContains(t, harness, substring)` - Verify partial print output
|
||||
- `AssertPrintInfo(t, harness, text)` - Verify PrintInfo was called
|
||||
- `AssertPrintError(t, harness, text)` - Verify PrintError was called
|
||||
- `AssertWidgetSet(t, harness, id)` - Verify widget was set
|
||||
- `AssertWidgetNotSet(t, harness, id)` - Verify widget was not set
|
||||
- `AssertWidgetText(t, harness, id, text)` - Verify widget content
|
||||
- `AssertWidgetTextContains(t, harness, id, substring)` - Verify widget contains text
|
||||
- `AssertHeaderSet(t, harness)` - Verify header was set
|
||||
- `AssertFooterSet(t, harness)` - Verify footer was set
|
||||
- `AssertStatusSet(t, harness, key)` - Verify status was set
|
||||
- `AssertStatusText(t, harness, key, text)` - Verify status text
|
||||
|
||||
**Registration:**
|
||||
- `AssertToolRegistered(t, harness, name)` - Verify tool registration
|
||||
- `AssertCommandRegistered(t, harness, name)` - Verify command registration
|
||||
- `AssertHasHandlers(t, harness, eventType)` - Verify handlers exist
|
||||
- `AssertNoHandlers(t, harness, eventType)` - Verify no handlers
|
||||
|
||||
**Messaging:**
|
||||
- `AssertMessageSent(t, harness, text)` - Verify SendMessage was called
|
||||
- `AssertCancelAndSend(t, harness, text)` - Verify CancelAndSend was called
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Accessing the Mock Context
|
||||
|
||||
For custom assertions, access the mock context directly:
|
||||
|
||||
```go
|
||||
func TestCustomAssertion(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
// Get all recorded prints
|
||||
prints := harness.Context().GetPrints()
|
||||
|
||||
// Check widget directly
|
||||
widget, ok := harness.Context().GetWidget("my-widget")
|
||||
if ok && widget.Style.BorderColor == "#ff0000" {
|
||||
t.Log("Widget has red border")
|
||||
}
|
||||
|
||||
// Check options
|
||||
optionValue := harness.Context().GetOption("my-option")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Multiple Extensions
|
||||
|
||||
Each harness is isolated:
|
||||
|
||||
```go
|
||||
func TestExtensionIsolation(t *testing.T) {
|
||||
// These run in completely separate interpreters
|
||||
harness1 := test.New(t)
|
||||
harness1.LoadFile("ext1.go")
|
||||
|
||||
harness2 := test.New(t)
|
||||
harness2.LoadFile("ext2.go")
|
||||
|
||||
// Events to one don't affect the other
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Result Extraction
|
||||
|
||||
When you need to inspect result details:
|
||||
|
||||
```go
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{...})
|
||||
tcr := test.GetToolCallResult(result)
|
||||
if tcr != nil {
|
||||
t.Logf("Block: %v, Reason: %s", tcr.Block, tcr.Reason)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test one behavior per test** - Keep tests focused and readable
|
||||
2. **Use inline source for simple tests** - LoadString is great for isolated tests
|
||||
3. **Use LoadFile for integration tests** - Tests the actual extension file
|
||||
4. **Assert on context calls** - Verify your extension interacts with the context correctly
|
||||
5. **Test both positive and negative cases** - Verify tools are blocked AND allowed appropriately
|
||||
6. **Test all event handlers** - Make sure all registered handlers work correctly
|
||||
|
||||
## Limitations
|
||||
|
||||
The test harness has these limitations:
|
||||
|
||||
1. **No TUI rendering** - Widgets are recorded but not rendered visually
|
||||
2. **Prompts return configured values** - You must pre-configure prompt results in tests
|
||||
3. **Subagents don't spawn real processes** - SpawnSubagent returns nil/empty results
|
||||
4. **LLM completions are mocked** - Complete returns empty responses
|
||||
5. **Some context methods are no-ops** - Exit, SetActiveTools, etc. don't have side effects
|
||||
|
||||
These limitations are intentional - the test harness focuses on testing extension logic, not the full Kit runtime.
|
||||
|
||||
## Example: Complete Extension Test
|
||||
|
||||
Here's a complete example testing a realistic extension:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
// Test that the extension properly blocks dangerous tools
|
||||
func TestSafetyExtension_BlocksDangerousTools(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("safety-ext.go")
|
||||
|
||||
// Verify it handles tool calls
|
||||
test.AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
|
||||
// Test allowed tool
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{ToolName: "read", Input: "{}"})
|
||||
test.AssertNotBlocked(t, result)
|
||||
|
||||
// Test blocked tool
|
||||
result, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "rm", Input: "{}"})
|
||||
test.AssertBlocked(t, result, "safety block")
|
||||
test.AssertPrintError(t, harness, "Tool rm is blocked")
|
||||
}
|
||||
|
||||
// Test that the extension shows status on agent completion
|
||||
func TestSafetyExtension_ShowsStatus(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("safety-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.AgentEndEvent{})
|
||||
|
||||
test.AssertWidgetSet(t, harness, "safety-widget")
|
||||
test.AssertWidgetTextContains(t, harness, "safety-widget", "Safe")
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,297 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
// AssertNotBlocked fails the test if the tool call result indicates the tool was blocked.
|
||||
func AssertNotBlocked(t *testing.T, result extensions.Result) {
|
||||
t.Helper()
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
if tcr, ok := result.(extensions.ToolCallResult); ok {
|
||||
if tcr.Block {
|
||||
t.Errorf("expected tool to not be blocked, but it was blocked with reason: %q", tcr.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AssertBlocked fails the test if the tool call result does not indicate the tool was blocked.
|
||||
func AssertBlocked(t *testing.T, result extensions.Result, expectedReason string) {
|
||||
t.Helper()
|
||||
if result == nil {
|
||||
t.Error("expected tool to be blocked, but result was nil")
|
||||
return
|
||||
}
|
||||
tcr, ok := result.(extensions.ToolCallResult)
|
||||
if !ok {
|
||||
t.Errorf("expected ToolCallResult, got %T", result)
|
||||
return
|
||||
}
|
||||
if !tcr.Block {
|
||||
t.Error("expected tool to be blocked, but it was not blocked")
|
||||
return
|
||||
}
|
||||
if expectedReason != "" && tcr.Reason != expectedReason {
|
||||
t.Errorf("expected block reason %q, got %q", expectedReason, tcr.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertInputHandled fails the test if the input result does not indicate the input was handled.
|
||||
func AssertInputHandled(t *testing.T, result extensions.Result, expectedAction string) {
|
||||
t.Helper()
|
||||
if result == nil {
|
||||
t.Error("expected input to be handled, but result was nil")
|
||||
return
|
||||
}
|
||||
ir, ok := result.(extensions.InputResult)
|
||||
if !ok {
|
||||
t.Errorf("expected InputResult, got %T", result)
|
||||
return
|
||||
}
|
||||
if ir.Action != expectedAction {
|
||||
t.Errorf("expected action %q, got %q", expectedAction, ir.Action)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertInputTransformed fails the test if the input was not transformed to the expected text.
|
||||
func AssertInputTransformed(t *testing.T, result extensions.Result, expectedText string) {
|
||||
t.Helper()
|
||||
if result == nil {
|
||||
t.Errorf("expected input to be transformed to %q, but result was nil", expectedText)
|
||||
return
|
||||
}
|
||||
ir, ok := result.(extensions.InputResult)
|
||||
if !ok {
|
||||
t.Errorf("expected InputResult, got %T", result)
|
||||
return
|
||||
}
|
||||
if ir.Action != "transform" {
|
||||
t.Errorf("expected action 'transform', got %q", ir.Action)
|
||||
}
|
||||
if ir.Text != expectedText {
|
||||
t.Errorf("expected transformed text %q, got %q", expectedText, ir.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertPrinted fails the test if the expected text was not printed.
|
||||
func AssertPrinted(t *testing.T, harness *Harness, expected string) {
|
||||
t.Helper()
|
||||
prints := harness.Context().GetPrints()
|
||||
if slices.Contains(prints, expected) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected text %q to be printed, but it was not found in prints: %v", expected, prints)
|
||||
}
|
||||
|
||||
// AssertPrintedContains fails the test if no printed text contains the expected substring.
|
||||
func AssertPrintedContains(t *testing.T, harness *Harness, substring string) {
|
||||
t.Helper()
|
||||
prints := harness.Context().GetPrints()
|
||||
for _, p := range prints {
|
||||
if strings.Contains(p, substring) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("expected printed text to contain %q, but it was not found in prints: %v", substring, prints)
|
||||
}
|
||||
|
||||
// AssertPrintInfo fails the test if the expected info message was not printed.
|
||||
func AssertPrintInfo(t *testing.T, harness *Harness, expected string) {
|
||||
t.Helper()
|
||||
infos := harness.Context().GetPrintInfos()
|
||||
if slices.Contains(infos, expected) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected info message %q, but it was not found in PrintInfos: %v", expected, infos)
|
||||
}
|
||||
|
||||
// AssertPrintError fails the test if the expected error message was not printed.
|
||||
func AssertPrintError(t *testing.T, harness *Harness, expected string) {
|
||||
t.Helper()
|
||||
errors := harness.Context().GetPrintErrors()
|
||||
if slices.Contains(errors, expected) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected error message %q, but it was not found in PrintErrors: %v", expected, errors)
|
||||
}
|
||||
|
||||
// AssertWidgetSet fails the test if the widget with the given ID was not set.
|
||||
func AssertWidgetSet(t *testing.T, harness *Harness, id string) {
|
||||
t.Helper()
|
||||
if !harness.Context().HasWidget(id) {
|
||||
t.Errorf("expected widget %q to be set, but it was not", id)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertWidgetNotSet fails the test if the widget with the given ID was set.
|
||||
func AssertWidgetNotSet(t *testing.T, harness *Harness, id string) {
|
||||
t.Helper()
|
||||
if harness.Context().HasWidget(id) {
|
||||
t.Errorf("expected widget %q to not be set, but it was", id)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertWidgetText fails the test if the widget with the given ID does not have the expected text.
|
||||
func AssertWidgetText(t *testing.T, harness *Harness, id string, expected string) {
|
||||
t.Helper()
|
||||
widget, ok := harness.Context().GetWidget(id)
|
||||
if !ok {
|
||||
t.Errorf("expected widget %q to be set, but it was not", id)
|
||||
return
|
||||
}
|
||||
if widget.Content.Text != expected {
|
||||
t.Errorf("expected widget %q to have text %q, got %q", id, expected, widget.Content.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertWidgetTextContains fails the test if the widget text does not contain the expected substring.
|
||||
func AssertWidgetTextContains(t *testing.T, harness *Harness, id string, substring string) {
|
||||
t.Helper()
|
||||
widget, ok := harness.Context().GetWidget(id)
|
||||
if !ok {
|
||||
t.Errorf("expected widget %q to be set, but it was not", id)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(widget.Content.Text, substring) {
|
||||
t.Errorf("expected widget %q text to contain %q, but got %q", id, substring, widget.Content.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertHeaderSet fails the test if no header was set.
|
||||
func AssertHeaderSet(t *testing.T, harness *Harness) {
|
||||
t.Helper()
|
||||
if harness.Context().GetHeader() == nil {
|
||||
t.Error("expected header to be set, but it was not")
|
||||
}
|
||||
}
|
||||
|
||||
// AssertFooterSet fails the test if no footer was set.
|
||||
func AssertFooterSet(t *testing.T, harness *Harness) {
|
||||
t.Helper()
|
||||
if harness.Context().GetFooter() == nil {
|
||||
t.Error("expected footer to be set, but it was not")
|
||||
}
|
||||
}
|
||||
|
||||
// AssertStatusSet fails the test if the status with the given key was not set.
|
||||
func AssertStatusSet(t *testing.T, harness *Harness, key string) {
|
||||
t.Helper()
|
||||
_, ok := harness.Context().GetStatus(key)
|
||||
if !ok {
|
||||
t.Errorf("expected status %q to be set, but it was not", key)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertStatusText fails the test if the status with the given key does not have the expected text.
|
||||
func AssertStatusText(t *testing.T, harness *Harness, key string, expected string) {
|
||||
t.Helper()
|
||||
status, ok := harness.Context().GetStatus(key)
|
||||
if !ok {
|
||||
t.Errorf("expected status %q to be set, but it was not", key)
|
||||
return
|
||||
}
|
||||
if status.Text != expected {
|
||||
t.Errorf("expected status %q to have text %q, got %q", key, expected, status.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertHasHandlers fails the test if no handlers are registered for the given event type.
|
||||
func AssertHasHandlers(t *testing.T, harness *Harness, eventType extensions.EventType) {
|
||||
t.Helper()
|
||||
if !harness.HasHandlers(eventType) {
|
||||
t.Errorf("expected handlers for event type %q, but none were registered", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertNoHandlers fails the test if any handlers are registered for the given event type.
|
||||
func AssertNoHandlers(t *testing.T, harness *Harness, eventType extensions.EventType) {
|
||||
t.Helper()
|
||||
if harness.HasHandlers(eventType) {
|
||||
t.Errorf("expected no handlers for event type %q, but some were registered", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertToolRegistered fails the test if the tool with the given name was not registered.
|
||||
func AssertToolRegistered(t *testing.T, harness *Harness, toolName string) {
|
||||
t.Helper()
|
||||
tools := harness.RegisteredTools()
|
||||
for _, tool := range tools {
|
||||
if tool.Name == toolName {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("expected tool %q to be registered, but it was not found in %v", toolName, tools)
|
||||
}
|
||||
|
||||
// AssertCommandRegistered fails the test if the command with the given name was not registered.
|
||||
func AssertCommandRegistered(t *testing.T, harness *Harness, cmdName string) {
|
||||
t.Helper()
|
||||
cmds := harness.RegisteredCommands()
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Name == cmdName {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("expected command %q to be registered, but it was not found in %v", cmdName, cmds)
|
||||
}
|
||||
|
||||
// AssertMessageSent fails the test if the expected message was not sent.
|
||||
func AssertMessageSent(t *testing.T, harness *Harness, expected string) {
|
||||
t.Helper()
|
||||
ctx := harness.Context()
|
||||
if slices.Contains(ctx.Messages, expected) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected message %q to be sent, but it was not found in messages: %v", expected, ctx.Messages)
|
||||
}
|
||||
|
||||
// AssertCancelAndSend fails the test if the expected text was not sent via CancelAndSend.
|
||||
func AssertCancelAndSend(t *testing.T, harness *Harness, expected string) {
|
||||
t.Helper()
|
||||
ctx := harness.Context()
|
||||
if slices.Contains(ctx.CancelSends, expected) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected CancelAndSend with %q, but it was not found: %v", expected, ctx.CancelSends)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// GetToolCallResult extracts a ToolCallResult from a Result, or nil if not applicable.
|
||||
func GetToolCallResult(result extensions.Result) *extensions.ToolCallResult {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
if tcr, ok := result.(extensions.ToolCallResult); ok {
|
||||
return &tcr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInputResult extracts an InputResult from a Result, or nil if not applicable.
|
||||
func GetInputResult(result extensions.Result) *extensions.InputResult {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
if ir, ok := result.(extensions.InputResult); ok {
|
||||
return &ir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetToolResultResult extracts a ToolResultResult from a Result, or nil if not applicable.
|
||||
func GetToolResultResult(result extensions.Result) *extensions.ToolResultResult {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
if trr, ok := result.(extensions.ToolResultResult); ok {
|
||||
return &trr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Package test provides utilities for testing Kit extensions.
|
||||
//
|
||||
// This package allows extension authors to write standard Go tests that load
|
||||
// and exercise their extensions in a controlled environment. Extensions are
|
||||
// loaded into a Yaegi interpreter with all Kit API symbols available.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
// "github.com/mark3labs/kit/internal/extensions/test"
|
||||
// )
|
||||
//
|
||||
// func TestMyExtension(t *testing.T) {
|
||||
// // Create a test harness
|
||||
// harness := test.New(t)
|
||||
//
|
||||
// // Load your extension file
|
||||
// ext := harness.LoadFile("my-ext.go")
|
||||
//
|
||||
// // Emit events and check results
|
||||
// result := harness.Emit(test.ToolCallEvent{
|
||||
// ToolName: "my_tool",
|
||||
// Input: `{"key": "value"}`,
|
||||
// })
|
||||
//
|
||||
// // Use assertion helpers
|
||||
// test.AssertNotBlocked(t, result)
|
||||
// test.AssertPrinted(t, harness, "expected output")
|
||||
// }
|
||||
//
|
||||
// The harness provides a mock Context that records all interactions,
|
||||
// allowing you to verify that your extension called SetWidget, Print, etc.
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
"github.com/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
"github.com/traefik/yaegi/stdlib/unrestricted"
|
||||
)
|
||||
|
||||
// Harness provides a testing environment for Kit extensions.
|
||||
// It loads extensions into an isolated Yaegi interpreter and provides
|
||||
// methods to emit events and verify extension behavior.
|
||||
type Harness struct {
|
||||
t *testing.T
|
||||
runner *extensions.Runner
|
||||
context *MockContext
|
||||
extPath string
|
||||
}
|
||||
|
||||
// New creates a new test harness for the given test.
|
||||
// The harness must be used within a single test function.
|
||||
func New(t *testing.T) *Harness {
|
||||
return &Harness{
|
||||
t: t,
|
||||
context: NewMockContext(),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFile loads an extension from a file path.
|
||||
// The extension is evaluated in a fresh Yaegi interpreter with all
|
||||
// Kit API symbols available. The Init function is called automatically.
|
||||
//
|
||||
// Returns the loaded extension or fails the test on error.
|
||||
func (h *Harness) LoadFile(path string) *extensions.LoadedExtension {
|
||||
h.t.Helper()
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
h.t.Fatalf("extension file not found: %s: %v", path, err)
|
||||
}
|
||||
|
||||
// Read extension source
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
h.t.Fatalf("failed to read extension file: %v", err)
|
||||
}
|
||||
|
||||
return h.loadSource(string(src), path)
|
||||
}
|
||||
|
||||
// LoadString loads an extension from a source string.
|
||||
// Useful for inline extension tests. The path is used for error reporting.
|
||||
func (h *Harness) LoadString(src string, path string) *extensions.LoadedExtension {
|
||||
h.t.Helper()
|
||||
return h.loadSource(src, path)
|
||||
}
|
||||
|
||||
// loadSource is the internal implementation that loads extension source
|
||||
// into a Yaegi interpreter.
|
||||
func (h *Harness) loadSource(src string, path string) *extensions.LoadedExtension {
|
||||
h.t.Helper()
|
||||
|
||||
// Create a fresh interpreter
|
||||
i := interp.New(interp.Options{})
|
||||
|
||||
// Expose Go stdlib
|
||||
if err := i.Use(stdlib.Symbols); err != nil {
|
||||
h.t.Fatalf("failed to load stdlib symbols: %v", err)
|
||||
}
|
||||
if err := i.Use(unrestricted.Symbols); err != nil {
|
||||
h.t.Fatalf("failed to load unrestricted symbols: %v", err)
|
||||
}
|
||||
|
||||
// Expose Kit extension API symbols
|
||||
if err := i.Use(extensions.Symbols()); err != nil {
|
||||
h.t.Fatalf("failed to load extension symbols: %v", err)
|
||||
}
|
||||
|
||||
// Evaluate the extension source
|
||||
if _, err := i.Eval(src); err != nil {
|
||||
h.t.Fatalf("failed to evaluate extension source: %v", err)
|
||||
}
|
||||
|
||||
// Extract the Init function
|
||||
initVal, err := i.Eval("Init")
|
||||
if err != nil {
|
||||
h.t.Fatalf("extension has no Init function: %v", err)
|
||||
}
|
||||
|
||||
initFn, ok := initVal.Interface().(func(extensions.API))
|
||||
if !ok {
|
||||
h.t.Fatalf("Init has wrong signature (want func(ext.API), got %T)", initVal.Interface())
|
||||
}
|
||||
|
||||
// Create the extension struct
|
||||
ext := &extensions.LoadedExtension{
|
||||
Path: path,
|
||||
Handlers: make(map[extensions.EventType][]extensions.HandlerFunc),
|
||||
}
|
||||
|
||||
// Create the API object using the test helper
|
||||
api := extensions.NewTestAPI(ext)
|
||||
|
||||
// Call Init to register handlers
|
||||
initFn(api)
|
||||
|
||||
// Create runner with the loaded extension
|
||||
h.runner = extensions.NewRunner([]extensions.LoadedExtension{*ext})
|
||||
h.extPath = path
|
||||
|
||||
// Wire the mock context
|
||||
h.runner.SetContext(h.context.ToContext())
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
// Emit sends an event to the loaded extension(s) and returns the result.
|
||||
// Events are dispatched in order and blocking results stop propagation.
|
||||
func (h *Harness) Emit(event extensions.Event) (extensions.Result, error) {
|
||||
h.t.Helper()
|
||||
|
||||
if h.runner == nil {
|
||||
h.t.Fatal("no extension loaded, call LoadFile() or LoadString() first")
|
||||
}
|
||||
|
||||
return h.runner.Emit(event)
|
||||
}
|
||||
|
||||
// EmitJSON is a convenience method for emitting a ToolCallEvent with JSON input.
|
||||
func (h *Harness) EmitJSON(toolName string, input string) (*extensions.ToolCallResult, error) {
|
||||
h.t.Helper()
|
||||
|
||||
result, err := h.Emit(extensions.ToolCallEvent{
|
||||
ToolName: toolName,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tcr, ok := result.(extensions.ToolCallResult)
|
||||
if !ok {
|
||||
h.t.Fatalf("expected ToolCallResult, got %T", result)
|
||||
}
|
||||
|
||||
return &tcr, nil
|
||||
}
|
||||
|
||||
// Context returns the mock context for inspection.
|
||||
// Use this to verify Print calls, widget settings, etc.
|
||||
func (h *Harness) Context() *MockContext {
|
||||
return h.context
|
||||
}
|
||||
|
||||
// Runner returns the underlying runner for advanced use cases.
|
||||
func (h *Harness) Runner() *extensions.Runner {
|
||||
return h.runner
|
||||
}
|
||||
|
||||
// HasHandlers reports whether any handlers are registered for the given event type.
|
||||
func (h *Harness) HasHandlers(eventType extensions.EventType) bool {
|
||||
if h.runner == nil {
|
||||
return false
|
||||
}
|
||||
return h.runner.HasHandlers(eventType)
|
||||
}
|
||||
|
||||
// RegisteredTools returns all tools registered by the extension.
|
||||
func (h *Harness) RegisteredTools() []extensions.ToolDef {
|
||||
if h.runner == nil {
|
||||
return nil
|
||||
}
|
||||
return h.runner.RegisteredTools()
|
||||
}
|
||||
|
||||
// RegisteredCommands returns all commands registered by the extension.
|
||||
func (h *Harness) RegisteredCommands() []extensions.CommandDef {
|
||||
if h.runner == nil {
|
||||
return nil
|
||||
}
|
||||
return h.runner.RegisteredCommands()
|
||||
}
|
||||
|
||||
// MustLoad is like LoadFile but fails the test immediately on error.
|
||||
// It returns the harness for chaining.
|
||||
func (h *Harness) MustLoad(path string) *Harness {
|
||||
h.t.Helper()
|
||||
h.LoadFile(path)
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
// Test harness with a simple extension
|
||||
func TestHarness_LoadString(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.Print("session started")
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "test-ext.go")
|
||||
|
||||
// Emit session start event
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the extension printed something
|
||||
prints := harness.Context().GetPrints()
|
||||
if len(prints) != 1 || prints[0] != "session started" {
|
||||
t.Errorf("expected ['session started'], got %v", prints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_ToolCallBlocking(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
|
||||
if tc.ToolName == "banned" {
|
||||
return &ext.ToolCallResult{Block: true, Reason: "tool is banned"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "blocker.go")
|
||||
|
||||
// Test blocked tool
|
||||
result, err := harness.Emit(extensions.ToolCallEvent{ToolName: "banned", Input: "{}"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertBlocked(t, result, "tool is banned")
|
||||
|
||||
// Test allowed tool
|
||||
result2, err := harness.Emit(extensions.ToolCallEvent{ToolName: "allowed", Input: "{}"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result2 != nil {
|
||||
t.Errorf("expected nil result for allowed tool, got %v", result2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_ToolRegistration(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.RegisterTool(ext.ToolDef{
|
||||
Name: "my_tool",
|
||||
Description: "does stuff",
|
||||
Parameters: "{}",
|
||||
Execute: func(input string) (string, error) {
|
||||
return "result: " + input, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "tool-ext.go")
|
||||
|
||||
tools := harness.RegisteredTools()
|
||||
if len(tools) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(tools))
|
||||
}
|
||||
|
||||
if tools[0].Name != "my_tool" {
|
||||
t.Errorf("expected tool name 'my_tool', got %q", tools[0].Name)
|
||||
}
|
||||
|
||||
AssertToolRegistered(t, harness, "my_tool")
|
||||
}
|
||||
|
||||
func TestHarness_CommandRegistration(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.RegisterCommand(ext.CommandDef{
|
||||
Name: "hello",
|
||||
Description: "says hello",
|
||||
Execute: func(args string, ctx ext.Context) (string, error) {
|
||||
ctx.Print("Hello, " + args)
|
||||
return "greeting sent", nil
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "cmd-ext.go")
|
||||
|
||||
cmds := harness.RegisteredCommands()
|
||||
if len(cmds) != 1 {
|
||||
t.Fatalf("expected 1 command, got %d", len(cmds))
|
||||
}
|
||||
|
||||
if cmds[0].Name != "hello" {
|
||||
t.Errorf("expected command name 'hello', got %q", cmds[0].Name)
|
||||
}
|
||||
|
||||
AssertCommandRegistered(t, harness, "hello")
|
||||
}
|
||||
|
||||
func TestHarness_WidgetSetting(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetWidget(ext.WidgetConfig{
|
||||
ID: "my-widget",
|
||||
Placement: ext.WidgetAbove,
|
||||
Content: ext.WidgetContent{Text: "Hello, World!"},
|
||||
Style: ext.WidgetStyle{BorderColor: "#ff0000"},
|
||||
})
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "widget-ext.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertWidgetSet(t, harness, "my-widget")
|
||||
AssertWidgetText(t, harness, "my-widget", "Hello, World!")
|
||||
|
||||
// Also verify directly
|
||||
widget, ok := harness.Context().GetWidget("my-widget")
|
||||
if !ok {
|
||||
t.Error("expected widget 'my-widget' to exist")
|
||||
}
|
||||
if widget.Style.BorderColor != "#ff0000" {
|
||||
t.Errorf("expected border color '#ff0000', got %q", widget.Style.BorderColor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_FooterSetting(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetFooter(ext.HeaderFooterConfig{
|
||||
Content: ext.WidgetContent{Text: "Status: OK"},
|
||||
Style: ext.WidgetStyle{BorderColor: "#00ff00"},
|
||||
})
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "footer-ext.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertFooterSet(t, harness)
|
||||
|
||||
footer := harness.Context().GetFooter()
|
||||
if footer == nil {
|
||||
t.Fatal("expected footer to be set")
|
||||
}
|
||||
if footer.Content.Text != "Status: OK" {
|
||||
t.Errorf("expected footer text 'Status: OK', got %q", footer.Content.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_PrintInfoAndError(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.PrintInfo("Information message")
|
||||
ctx.PrintError("Error message")
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "print-ext.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertPrintInfo(t, harness, "Information message")
|
||||
AssertPrintError(t, harness, "Error message")
|
||||
}
|
||||
|
||||
func TestHarness_EmitJSON(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
|
||||
if tc.ToolName == "test_tool" {
|
||||
return &ext.ToolCallResult{Block: true, Reason: "blocked"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "json-ext.go")
|
||||
|
||||
result, err := harness.EmitJSON("test_tool", `{"key": "value"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if !result.Block {
|
||||
t.Error("expected Block=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_HasHandlers(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(_ ext.ToolCallEvent, _ ext.Context) *ext.ToolCallResult {
|
||||
return nil
|
||||
})
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, _ ext.Context) {
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "handlers-ext.go")
|
||||
|
||||
AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
AssertHasHandlers(t, harness, extensions.SessionStart)
|
||||
AssertNoHandlers(t, harness, extensions.AgentEnd)
|
||||
}
|
||||
|
||||
func TestHarness_MultipleExtensions(t *testing.T) {
|
||||
ext1 := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.Print("extension 1")
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
ext2 := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.Print("extension 2")
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
// Load first extension
|
||||
harness1 := New(t)
|
||||
harness1.LoadString(ext1, "ext1.go")
|
||||
|
||||
// Load second extension
|
||||
harness2 := New(t)
|
||||
harness2.LoadString(ext2, "ext2.go")
|
||||
|
||||
// Verify they are isolated
|
||||
_, _ = harness1.Emit(extensions.SessionStartEvent{SessionID: "test1"})
|
||||
_, _ = harness2.Emit(extensions.SessionStartEvent{SessionID: "test2"})
|
||||
|
||||
prints1 := harness1.Context().GetPrints()
|
||||
prints2 := harness2.Context().GetPrints()
|
||||
|
||||
if len(prints1) != 1 || prints1[0] != "extension 1" {
|
||||
t.Errorf("ext1 prints: expected ['extension 1'], got %v", prints1)
|
||||
}
|
||||
|
||||
if len(prints2) != 1 || prints2[0] != "extension 2" {
|
||||
t.Errorf("ext2 prints: expected ['extension 2'], got %v", prints2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_InputHandling(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import (
|
||||
"kit/ext"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnInput(func(ie ext.InputEvent, ctx ext.Context) *ext.InputResult {
|
||||
if strings.Contains(ie.Text, "secret") {
|
||||
return &ext.InputResult{Action: "handled"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "input-ext.go")
|
||||
|
||||
// Test handled input
|
||||
result, err := harness.Emit(extensions.InputEvent{Text: "my secret password", Source: "cli"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertInputHandled(t, result, "handled")
|
||||
|
||||
// Test unhandled input
|
||||
result2, err := harness.Emit(extensions.InputEvent{Text: "normal input", Source: "cli"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result2 != nil {
|
||||
t.Errorf("expected nil result for unhandled input, got %v", result2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHarness_StatusSetting(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetStatus("myext:status", "Ready", 50)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "status-ext.go")
|
||||
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
AssertStatusSet(t, harness, "myext:status")
|
||||
AssertStatusText(t, harness, "myext:status", "Ready")
|
||||
}
|
||||
|
||||
func TestHarness_LoadFile_NotFound(t *testing.T) {
|
||||
// Test that loading a nonexistent file fails the test
|
||||
// We create a mock testing.T to capture the failure
|
||||
mockT := &testing.T{}
|
||||
harness := New(mockT)
|
||||
|
||||
// Just verify the harness was created successfully
|
||||
_ = harness.Context().GetPrints()
|
||||
|
||||
// The actual behavior (Fatalf on missing file) is tested implicitly
|
||||
// whenever LoadFile is used in other tests
|
||||
}
|
||||
|
||||
// MockContext tests
|
||||
func TestMockContext_Prompts(t *testing.T) {
|
||||
ctx := NewMockContext()
|
||||
|
||||
// Configure results
|
||||
ctx.SetPromptSelectResult(extensions.PromptSelectResult{Value: "option1", Index: 0, Cancelled: false})
|
||||
ctx.SetPromptConfirmResult(extensions.PromptConfirmResult{Value: true, Cancelled: false})
|
||||
ctx.SetPromptInputResult(extensions.PromptInputResult{Value: "input text", Cancelled: false})
|
||||
|
||||
extCtx := ctx.ToContext()
|
||||
|
||||
// Test prompts return configured results
|
||||
selectResult := extCtx.PromptSelect(extensions.PromptSelectConfig{Message: "test", Options: []string{"a", "b"}})
|
||||
if selectResult.Value != "option1" {
|
||||
t.Errorf("expected 'option1', got %q", selectResult.Value)
|
||||
}
|
||||
|
||||
confirmResult := extCtx.PromptConfirm(extensions.PromptConfirmConfig{Message: "test"})
|
||||
if !confirmResult.Value {
|
||||
t.Error("expected true")
|
||||
}
|
||||
|
||||
inputResult := extCtx.PromptInput(extensions.PromptInputConfig{Message: "test"})
|
||||
if inputResult.Value != "input text" {
|
||||
t.Errorf("expected 'input text', got %q", inputResult.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockContext_Options(t *testing.T) {
|
||||
ctx := NewMockContext()
|
||||
extCtx := ctx.ToContext()
|
||||
|
||||
// Initially empty
|
||||
if extCtx.GetOption("key") != "" {
|
||||
t.Error("expected empty option")
|
||||
}
|
||||
|
||||
// Set option
|
||||
extCtx.SetOption("key", "value")
|
||||
if extCtx.GetOption("key") != "value" {
|
||||
t.Errorf("expected 'value', got %q", extCtx.GetOption("key"))
|
||||
}
|
||||
}
|
||||
|
||||
// Assertion helper tests
|
||||
func TestAssertPrintedContains(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.Print("This is a long message with some content")
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "print-ext.go")
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
AssertPrintedContains(t, harness, "long message")
|
||||
AssertPrintedContains(t, harness, "some content")
|
||||
}
|
||||
|
||||
func TestAssertWidgetTextContains(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetWidget(ext.WidgetConfig{
|
||||
ID: "status",
|
||||
Content: ext.WidgetContent{Text: "Build: passing, Tests: 42/42"},
|
||||
})
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "widget-ext.go")
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
AssertWidgetTextContains(t, harness, "status", "Build: passing")
|
||||
AssertWidgetTextContains(t, harness, "status", "42/42")
|
||||
}
|
||||
|
||||
// Test that shows how to test a realistic extension pattern
|
||||
func TestExample_RealisticExtension(t *testing.T) {
|
||||
// This is an example of a realistic extension that:
|
||||
// 1. Blocks dangerous tools
|
||||
// 2. Shows a status widget
|
||||
// 3. Logs tool calls
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
var blockedTools = []string{"rm", "del", "remove"}
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
|
||||
// Check if tool is blocked
|
||||
for _, blocked := range blockedTools {
|
||||
if tc.ToolName == blocked {
|
||||
ctx.PrintError("Tool " + tc.ToolName + " is blocked for safety")
|
||||
return &ext.ToolCallResult{Block: true, Reason: "safety block"}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the tool call
|
||||
ctx.SetStatus("tool-logger:last", tc.ToolName, 10)
|
||||
return nil
|
||||
})
|
||||
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetWidget(ext.WidgetConfig{
|
||||
ID: "safety-status",
|
||||
Content: ext.WidgetContent{Text: "Safety: Active"},
|
||||
Style: ext.WidgetStyle{BorderColor: "#00ff00"},
|
||||
})
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
harness := New(t)
|
||||
harness.LoadString(src, "safety-ext.go")
|
||||
|
||||
// Verify handlers are registered
|
||||
AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
AssertHasHandlers(t, harness, extensions.SessionStart)
|
||||
|
||||
// Test session start
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify widget was set
|
||||
AssertWidgetSet(t, harness, "safety-status")
|
||||
AssertWidgetText(t, harness, "safety-status", "Safety: Active")
|
||||
|
||||
// Test allowed tool
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{ToolName: "read", Input: "{}"})
|
||||
AssertNotBlocked(t, result)
|
||||
|
||||
// Verify status was updated
|
||||
AssertStatusSet(t, harness, "tool-logger:last")
|
||||
AssertStatusText(t, harness, "tool-logger:last", "read")
|
||||
|
||||
// Test blocked tool
|
||||
result2, _ := harness.Emit(extensions.ToolCallEvent{ToolName: "rm", Input: `{"file": "test.txt"}`})
|
||||
AssertBlocked(t, result2, "safety block")
|
||||
AssertPrintError(t, harness, "Tool rm is blocked for safety")
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
// MockContext records all interactions with the extension context.
|
||||
// It provides a Context object that captures Print calls, widget settings,
|
||||
// and other context operations for verification in tests.
|
||||
type MockContext struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Recorded calls
|
||||
Prints []string
|
||||
PrintInfos []string
|
||||
PrintErrors []string
|
||||
PrintBlocks []extensions.PrintBlockOpts
|
||||
Messages []string
|
||||
CancelSends []string
|
||||
|
||||
// Widget state
|
||||
Widgets map[string]extensions.WidgetConfig
|
||||
RemovedIDs []string
|
||||
Header *extensions.HeaderFooterConfig
|
||||
Footer *extensions.HeaderFooterConfig
|
||||
HeaderRemoved bool
|
||||
FooterRemoved bool
|
||||
|
||||
// Context properties
|
||||
SessionID string
|
||||
CWD string
|
||||
Model string
|
||||
Interactive bool
|
||||
|
||||
// UI visibility
|
||||
UIVisibility *extensions.UIVisibility
|
||||
|
||||
// Status entries
|
||||
StatusEntries map[string]extensions.StatusBarEntry
|
||||
RemovedStatus []string
|
||||
|
||||
// Editor
|
||||
EditorConfig *extensions.EditorConfig
|
||||
EditorReset bool
|
||||
EditorTexts []string
|
||||
|
||||
// Options
|
||||
Options map[string]string
|
||||
|
||||
// Prompt results (configurable for testing)
|
||||
PromptSelectResult extensions.PromptSelectResult
|
||||
PromptConfirmResult extensions.PromptConfirmResult
|
||||
PromptInputResult extensions.PromptInputResult
|
||||
PromptMultiSelectResult extensions.PromptMultiSelectResult
|
||||
|
||||
// Overlay
|
||||
Overlays []extensions.OverlayConfig
|
||||
}
|
||||
|
||||
// StatusBarEntry represents a recorded status bar entry
|
||||
type StatusBarEntry struct {
|
||||
Key string
|
||||
Text string
|
||||
Priority int
|
||||
}
|
||||
|
||||
// NewMockContext creates a new mock context with default values.
|
||||
func NewMockContext() *MockContext {
|
||||
return &MockContext{
|
||||
Prints: make([]string, 0),
|
||||
PrintInfos: make([]string, 0),
|
||||
PrintErrors: make([]string, 0),
|
||||
PrintBlocks: make([]extensions.PrintBlockOpts, 0),
|
||||
Messages: make([]string, 0),
|
||||
CancelSends: make([]string, 0),
|
||||
Widgets: make(map[string]extensions.WidgetConfig),
|
||||
RemovedIDs: make([]string, 0),
|
||||
StatusEntries: make(map[string]extensions.StatusBarEntry),
|
||||
RemovedStatus: make([]string, 0),
|
||||
EditorTexts: make([]string, 0),
|
||||
Options: make(map[string]string),
|
||||
Overlays: make([]extensions.OverlayConfig, 0),
|
||||
Interactive: true,
|
||||
SessionID: "test-session",
|
||||
CWD: "/test",
|
||||
Model: "test-model",
|
||||
}
|
||||
}
|
||||
|
||||
// ToContext returns a extensions.Context wired to record all interactions.
|
||||
func (m *MockContext) ToContext() extensions.Context {
|
||||
return extensions.Context{
|
||||
SessionID: m.SessionID,
|
||||
CWD: m.CWD,
|
||||
Model: m.Model,
|
||||
Interactive: m.Interactive,
|
||||
Print: m.recordPrint,
|
||||
PrintInfo: m.recordPrintInfo,
|
||||
PrintError: m.recordPrintError,
|
||||
PrintBlock: m.recordPrintBlock,
|
||||
SendMessage: m.recordSendMessage,
|
||||
CancelAndSend: m.recordCancelAndSend,
|
||||
SetWidget: m.recordSetWidget,
|
||||
RemoveWidget: m.recordRemoveWidget,
|
||||
SetHeader: m.recordSetHeader,
|
||||
RemoveHeader: m.recordRemoveHeader,
|
||||
SetFooter: m.recordSetFooter,
|
||||
RemoveFooter: m.recordRemoveFooter,
|
||||
PromptSelect: m.recordPromptSelect,
|
||||
PromptConfirm: m.recordPromptConfirm,
|
||||
PromptInput: m.recordPromptInput,
|
||||
PromptMultiSelect: m.recordPromptMultiSelect,
|
||||
SetEditor: m.recordSetEditor,
|
||||
ResetEditor: m.recordResetEditor,
|
||||
SetEditorText: m.recordSetEditorText,
|
||||
SetUIVisibility: m.recordUIVisibility,
|
||||
GetContextStats: m.getContextStats,
|
||||
GetMessages: m.getMessages,
|
||||
GetSessionPath: m.getSessionPath,
|
||||
AppendEntry: m.appendEntry,
|
||||
GetEntries: m.getEntries,
|
||||
SetStatus: m.recordSetStatus,
|
||||
RemoveStatus: m.recordRemoveStatus,
|
||||
GetOption: m.getOption,
|
||||
SetOption: m.setOption,
|
||||
SetModel: m.setModel,
|
||||
GetAllTools: m.getAllTools,
|
||||
SetActiveTools: m.setActiveTools,
|
||||
Exit: m.exit,
|
||||
Complete: m.complete,
|
||||
SuspendTUI: m.suspendTUI,
|
||||
RenderMessage: m.renderMessage,
|
||||
RegisterTheme: m.registerTheme,
|
||||
SetTheme: m.setTheme,
|
||||
ListThemes: m.listThemes,
|
||||
ReloadExtensions: m.reloadExtensions,
|
||||
SpawnSubagent: m.spawnSubagent,
|
||||
ShowOverlay: m.showOverlay,
|
||||
}
|
||||
}
|
||||
|
||||
// Record methods
|
||||
|
||||
func (m *MockContext) recordPrint(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Prints = append(m.Prints, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPrintInfo(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PrintInfos = append(m.PrintInfos, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPrintError(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PrintErrors = append(m.PrintErrors, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPrintBlock(opts extensions.PrintBlockOpts) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PrintBlocks = append(m.PrintBlocks, opts)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSendMessage(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Messages = append(m.Messages, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordCancelAndSend(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.CancelSends = append(m.CancelSends, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetWidget(config extensions.WidgetConfig) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Widgets[config.ID] = config
|
||||
}
|
||||
|
||||
func (m *MockContext) recordRemoveWidget(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.Widgets, id)
|
||||
m.RemovedIDs = append(m.RemovedIDs, id)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetHeader(config extensions.HeaderFooterConfig) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Header = &config
|
||||
}
|
||||
|
||||
func (m *MockContext) recordRemoveHeader() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Header = nil
|
||||
m.HeaderRemoved = true
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetFooter(config extensions.HeaderFooterConfig) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Footer = &config
|
||||
}
|
||||
|
||||
func (m *MockContext) recordRemoveFooter() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Footer = nil
|
||||
m.FooterRemoved = true
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetStatus(key string, text string, priority int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.StatusEntries[key] = extensions.StatusBarEntry{
|
||||
Key: key,
|
||||
Text: text,
|
||||
Priority: priority,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockContext) recordRemoveStatus(key string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.StatusEntries, key)
|
||||
m.RemovedStatus = append(m.RemovedStatus, key)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetEditor(config extensions.EditorConfig) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.EditorConfig = &config
|
||||
}
|
||||
|
||||
func (m *MockContext) recordResetEditor() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.EditorReset = true
|
||||
m.EditorConfig = nil
|
||||
}
|
||||
|
||||
func (m *MockContext) recordSetEditorText(text string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.EditorTexts = append(m.EditorTexts, text)
|
||||
}
|
||||
|
||||
func (m *MockContext) recordUIVisibility(vis extensions.UIVisibility) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.UIVisibility = &vis
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPromptSelect(config extensions.PromptSelectConfig) extensions.PromptSelectResult {
|
||||
// Return the configured result (tests can set this)
|
||||
return m.PromptSelectResult
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPromptConfirm(config extensions.PromptConfirmConfig) extensions.PromptConfirmResult {
|
||||
return m.PromptConfirmResult
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPromptInput(config extensions.PromptInputConfig) extensions.PromptInputResult {
|
||||
return m.PromptInputResult
|
||||
}
|
||||
|
||||
func (m *MockContext) recordPromptMultiSelect(config extensions.PromptMultiSelectConfig) extensions.PromptMultiSelectResult {
|
||||
return m.PromptMultiSelectResult
|
||||
}
|
||||
|
||||
func (m *MockContext) showOverlay(config extensions.OverlayConfig) extensions.OverlayResult {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Overlays = append(m.Overlays, config)
|
||||
return extensions.OverlayResult{Cancelled: true} // Default to cancelled for tests
|
||||
}
|
||||
|
||||
// Stub methods that do nothing or return defaults
|
||||
|
||||
func (m *MockContext) getContextStats() extensions.ContextStats {
|
||||
return extensions.ContextStats{
|
||||
EstimatedTokens: 1000,
|
||||
ContextLimit: 200000,
|
||||
UsagePercent: 0.5,
|
||||
MessageCount: 10,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockContext) getMessages() []extensions.SessionMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) getSessionPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MockContext) appendEntry(entryType string, data string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockContext) getEntries(entryType string) []extensions.ExtensionEntry {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) getOption(name string) string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.Options[name]
|
||||
}
|
||||
|
||||
func (m *MockContext) setOption(name string, value string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Options[name] = value
|
||||
}
|
||||
|
||||
func (m *MockContext) setModel(modelString string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) getAllTools() []extensions.ToolInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) setActiveTools(names []string) {}
|
||||
|
||||
func (m *MockContext) exit() {}
|
||||
|
||||
func (m *MockContext) complete(req extensions.CompleteRequest) (extensions.CompleteResponse, error) {
|
||||
return extensions.CompleteResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *MockContext) suspendTUI(callback func()) error {
|
||||
callback()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) renderMessage(rendererName string, content string) {}
|
||||
|
||||
func (m *MockContext) registerTheme(name string, config extensions.ThemeColorConfig) {}
|
||||
|
||||
func (m *MockContext) setTheme(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) listThemes() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) reloadExtensions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockContext) spawnSubagent(config extensions.SubagentConfig) (*extensions.SubagentHandle, *extensions.SubagentResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Accessor methods for verification
|
||||
|
||||
// GetPrints returns all recorded Print calls.
|
||||
func (m *MockContext) GetPrints() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make([]string, len(m.Prints))
|
||||
copy(result, m.Prints)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPrintInfos returns all recorded PrintInfo calls.
|
||||
func (m *MockContext) GetPrintInfos() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make([]string, len(m.PrintInfos))
|
||||
copy(result, m.PrintInfos)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPrintErrors returns all recorded PrintError calls.
|
||||
func (m *MockContext) GetPrintErrors() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make([]string, len(m.PrintErrors))
|
||||
copy(result, m.PrintErrors)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetWidget returns a recorded widget by ID.
|
||||
func (m *MockContext) GetWidget(id string) (extensions.WidgetConfig, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
w, ok := m.Widgets[id]
|
||||
return w, ok
|
||||
}
|
||||
|
||||
// HasWidget reports whether a widget with the given ID was set.
|
||||
func (m *MockContext) HasWidget(id string) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
_, ok := m.Widgets[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetHeader returns the recorded header configuration.
|
||||
func (m *MockContext) GetHeader() *extensions.HeaderFooterConfig {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.Header
|
||||
}
|
||||
|
||||
// GetFooter returns the recorded footer configuration.
|
||||
func (m *MockContext) GetFooter() *extensions.HeaderFooterConfig {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.Footer
|
||||
}
|
||||
|
||||
// GetStatus returns a recorded status entry by key.
|
||||
func (m *MockContext) GetStatus(key string) (extensions.StatusBarEntry, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
s, ok := m.StatusEntries[key]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// SetPromptSelectResult configures the result returned by PromptSelect.
|
||||
func (m *MockContext) SetPromptSelectResult(result extensions.PromptSelectResult) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PromptSelectResult = result
|
||||
}
|
||||
|
||||
// SetPromptConfirmResult configures the result returned by PromptConfirm.
|
||||
func (m *MockContext) SetPromptConfirmResult(result extensions.PromptConfirmResult) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PromptConfirmResult = result
|
||||
}
|
||||
|
||||
// SetPromptInputResult configures the result returned by PromptInput.
|
||||
func (m *MockContext) SetPromptInputResult(result extensions.PromptInputResult) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PromptInputResult = result
|
||||
}
|
||||
|
||||
// SetPromptMultiSelectResult configures the result returned by PromptMultiSelect.
|
||||
func (m *MockContext) SetPromptMultiSelectResult(result extensions.PromptMultiSelectResult) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.PromptMultiSelectResult = result
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package extensions
|
||||
|
||||
// NewTestAPI creates an API object wired for testing.
|
||||
// This is used by the test harness to load extensions and verify behavior.
|
||||
// The registration functions wire handlers directly to the provided extension.
|
||||
func NewTestAPI(ext *LoadedExtension) API {
|
||||
reg := func(event EventType, fn HandlerFunc) {
|
||||
ext.Handlers[event] = append(ext.Handlers[event], fn)
|
||||
}
|
||||
|
||||
return API{
|
||||
onToolCall: func(h func(ToolCallEvent, Context) *ToolCallResult) {
|
||||
reg(ToolCall, func(e Event, c Context) Result {
|
||||
r := h(e.(ToolCallEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onToolExecStart: func(h func(ToolExecutionStartEvent, Context)) {
|
||||
reg(ToolExecutionStart, func(e Event, c Context) Result {
|
||||
h(e.(ToolExecutionStartEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onToolExecEnd: func(h func(ToolExecutionEndEvent, Context)) {
|
||||
reg(ToolExecutionEnd, func(e Event, c Context) Result {
|
||||
h(e.(ToolExecutionEndEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onToolResult: func(h func(ToolResultEvent, Context) *ToolResultResult) {
|
||||
reg(ToolResult, func(e Event, c Context) Result {
|
||||
r := h(e.(ToolResultEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onInput: func(h func(InputEvent, Context) *InputResult) {
|
||||
reg(Input, func(e Event, c Context) Result {
|
||||
r := h(e.(InputEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onBeforeAgentStart: func(h func(BeforeAgentStartEvent, Context) *BeforeAgentStartResult) {
|
||||
reg(BeforeAgentStart, func(e Event, c Context) Result {
|
||||
r := h(e.(BeforeAgentStartEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onAgentStart: func(h func(AgentStartEvent, Context)) {
|
||||
reg(AgentStart, func(e Event, c Context) Result {
|
||||
h(e.(AgentStartEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onAgentEnd: func(h func(AgentEndEvent, Context)) {
|
||||
reg(AgentEnd, func(e Event, c Context) Result {
|
||||
h(e.(AgentEndEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onMessageStart: func(h func(MessageStartEvent, Context)) {
|
||||
reg(MessageStart, func(e Event, c Context) Result {
|
||||
h(e.(MessageStartEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onMessageUpdate: func(h func(MessageUpdateEvent, Context)) {
|
||||
reg(MessageUpdate, func(e Event, c Context) Result {
|
||||
h(e.(MessageUpdateEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onMessageEnd: func(h func(MessageEndEvent, Context)) {
|
||||
reg(MessageEnd, func(e Event, c Context) Result {
|
||||
h(e.(MessageEndEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onSessionStart: func(h func(SessionStartEvent, Context)) {
|
||||
reg(SessionStart, func(e Event, c Context) Result {
|
||||
h(e.(SessionStartEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onSessionShutdown: func(h func(SessionShutdownEvent, Context)) {
|
||||
reg(SessionShutdown, func(e Event, c Context) Result {
|
||||
h(e.(SessionShutdownEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onModelChange: func(h func(ModelChangeEvent, Context)) {
|
||||
reg(ModelChange, func(e Event, c Context) Result {
|
||||
h(e.(ModelChangeEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onContextPrepare: func(h func(ContextPrepareEvent, Context) *ContextPrepareResult) {
|
||||
reg(ContextPrepare, func(e Event, c Context) Result {
|
||||
r := h(e.(ContextPrepareEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onBeforeFork: func(h func(BeforeForkEvent, Context) *BeforeForkResult) {
|
||||
reg(BeforeFork, func(e Event, c Context) Result {
|
||||
r := h(e.(BeforeForkEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onBeforeSessionSwitch: func(h func(BeforeSessionSwitchEvent, Context) *BeforeSessionSwitchResult) {
|
||||
reg(BeforeSessionSwitch, func(e Event, c Context) Result {
|
||||
r := h(e.(BeforeSessionSwitchEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onBeforeCompact: func(h func(BeforeCompactEvent, Context) *BeforeCompactResult) {
|
||||
reg(BeforeCompact, func(e Event, c Context) Result {
|
||||
r := h(e.(BeforeCompactEvent), c)
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return *r
|
||||
})
|
||||
},
|
||||
registerToolFn: func(tool ToolDef) {
|
||||
ext.Tools = append(ext.Tools, tool)
|
||||
},
|
||||
registerCmdFn: func(cmd CommandDef) {
|
||||
ext.Commands = append(ext.Commands, cmd)
|
||||
},
|
||||
registerToolRendererFn: func(config ToolRenderConfig) {
|
||||
ext.ToolRenderers = append(ext.ToolRenderers, config)
|
||||
},
|
||||
onCustomEvent: func(name string, handler func(string)) {
|
||||
if ext.CustomEventHandlers == nil {
|
||||
ext.CustomEventHandlers = make(map[string][]func(string))
|
||||
}
|
||||
ext.CustomEventHandlers[name] = append(ext.CustomEventHandlers[name], handler)
|
||||
},
|
||||
registerOption: func(opt OptionDef) {
|
||||
ext.Options = append(ext.Options, opt)
|
||||
},
|
||||
registerShortcutFn: func(def ShortcutDef, handler func(Context)) {
|
||||
ext.Shortcuts = append(ext.Shortcuts, ShortcutEntry{Def: def, Handler: handler})
|
||||
},
|
||||
registerMessageRendererFn: func(config MessageRendererConfig) {
|
||||
ext.MessageRenderers = append(ext.MessageRenderers, config)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -166,28 +166,3 @@ func (p *ProviderPool) Close() {
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// Stats returns current pool statistics.
|
||||
func (p *ProviderPool) Stats() PoolStats {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
stats := PoolStats{
|
||||
TotalProviders: len(p.providers),
|
||||
}
|
||||
for _, pp := range p.providers {
|
||||
if pp.refs > 0 {
|
||||
stats.ActiveProviders++
|
||||
} else {
|
||||
stats.IdleProviders++
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// PoolStats contains provider pool statistics.
|
||||
type PoolStats struct {
|
||||
TotalProviders int
|
||||
ActiveProviders int
|
||||
IdleProviders int
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func resolveModelAlias(provider, modelName string) string {
|
||||
}
|
||||
|
||||
if resolved, exists := aliasMap[modelName]; exists {
|
||||
if _, err := registry.ValidateModel(provider, resolved); err == nil {
|
||||
if registry.LookupModel(provider, resolved) != nil {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,8 @@ func ThinkingLevels() []ThinkingLevel {
|
||||
return []ThinkingLevel{ThinkingOff, ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh}
|
||||
}
|
||||
|
||||
// ThinkingBudgetTokens returns the token budget for a thinking level, or 0 for "off".
|
||||
func ThinkingBudgetTokens(level ThinkingLevel) int64 {
|
||||
// thinkingBudgetTokens returns the token budget for a thinking level, or 0 for "off".
|
||||
func thinkingBudgetTokens(level ThinkingLevel) int64 {
|
||||
switch level {
|
||||
case ThinkingMinimal:
|
||||
return 1024
|
||||
@@ -162,16 +162,6 @@ func ParseModelString(modelString string) (provider, model string, err error) {
|
||||
return "", "", fmt.Errorf("invalid model format %q: expected provider/model (e.g. anthropic/claude-sonnet-4-5)", modelString)
|
||||
}
|
||||
|
||||
// Legacy colon-separated format
|
||||
if strings.Contains(modelString, ":") {
|
||||
parts := strings.SplitN(modelString, ":", 2)
|
||||
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
|
||||
fmt.Fprintf(os.Stderr, "Warning: model format %q uses deprecated colon separator. Use %s/%s instead.\n",
|
||||
modelString, parts[0], parts[1])
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("invalid model format %q: expected provider/model (e.g. anthropic/claude-sonnet-4-5)", modelString)
|
||||
}
|
||||
|
||||
@@ -489,7 +479,7 @@ func buildAnthropicProviderOptions(config *ProviderConfig, modelName string) fan
|
||||
return nil
|
||||
}
|
||||
|
||||
budget := ThinkingBudgetTokens(config.ThinkingLevel)
|
||||
budget := thinkingBudgetTokens(config.ThinkingLevel)
|
||||
if budget == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -147,24 +147,8 @@ func (r *ModelsRegistry) LookupModel(provider, modelID string) *ModelInfo {
|
||||
return &modelInfo
|
||||
}
|
||||
|
||||
// ValidateModel validates if a model exists and returns detailed information.
|
||||
// Deprecated: Use LookupModel instead — it returns nil for unknown models
|
||||
// rather than an error, letting the provider API be the authority.
|
||||
func (r *ModelsRegistry) ValidateModel(provider, modelID string) (*ModelInfo, error) {
|
||||
if info := r.LookupModel(provider, modelID); info != nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unsupported provider: %s", provider)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("model %s not found for provider %s", modelID, providerInfo.ID)
|
||||
}
|
||||
|
||||
// GetRequiredEnvVars returns the required environment variables for a provider.
|
||||
func (r *ModelsRegistry) GetRequiredEnvVars(provider string) ([]string, error) {
|
||||
// getRequiredEnvVars returns the required environment variables for a provider.
|
||||
func (r *ModelsRegistry) getRequiredEnvVars(provider string) ([]string, error) {
|
||||
providerInfo, exists := r.providers[provider]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unsupported provider: %s", provider)
|
||||
@@ -194,7 +178,7 @@ func (r *ModelsRegistry) ValidateEnvironment(provider string, apiKey string) err
|
||||
}
|
||||
}
|
||||
|
||||
envVars, err := r.GetRequiredEnvVars(provider)
|
||||
envVars, err := r.getRequiredEnvVars(provider)
|
||||
if err != nil {
|
||||
// Unknown provider — nothing to validate
|
||||
return nil
|
||||
|
||||
@@ -144,17 +144,6 @@ func NewMessageEntry(parentID string, msg message.Message) (*MessageEntry, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewMessageEntryFromRaw creates a MessageEntry with pre-marshaled parts.
|
||||
func NewMessageEntryFromRaw(parentID, role string, parts json.RawMessage, model, provider string) *MessageEntry {
|
||||
return &MessageEntry{
|
||||
Entry: NewEntry(EntryTypeMessage, parentID),
|
||||
Role: role,
|
||||
Parts: parts,
|
||||
Model: model,
|
||||
Provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
// NewModelChangeEntry creates a ModelChangeEntry.
|
||||
func NewModelChangeEntry(parentID, provider, modelID string) *ModelChangeEntry {
|
||||
return &ModelChangeEntry{
|
||||
|
||||
@@ -253,27 +253,3 @@ func extractTextPreview(partsJSON json.RawMessage) string {
|
||||
func DeleteSession(path string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// ListChildSessions returns all sessions that have the given session ID as
|
||||
// their parent. This is useful for finding subagent sessions spawned from
|
||||
// a parent session. Results are sorted by creation time (newest first).
|
||||
func ListChildSessions(parentID string) ([]SessionInfo, error) {
|
||||
if parentID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
allSessions, err := ListAllSessions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var children []SessionInfo
|
||||
for _, s := range allSessions {
|
||||
if s.ParentSessionID == parentID {
|
||||
children = append(children, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Already sorted by modification time from ListAllSessions
|
||||
return children, nil
|
||||
}
|
||||
|
||||
+2
-9
@@ -36,7 +36,7 @@ func NewCLI(debug bool, compact bool) (*CLI, error) {
|
||||
if compact {
|
||||
cli.renderer = NewCompactRenderer(cli.width, debug)
|
||||
} else {
|
||||
cli.renderer = NewMessageRenderer(cli.width, debug)
|
||||
cli.renderer = newMessageRenderer(cli.width, debug)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
@@ -108,13 +108,6 @@ func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayToolCallMessage is a no-op retained for backward compatibility. Tool
|
||||
// calls are now rendered as part of the unified tool block in DisplayToolMessage,
|
||||
// which combines the invocation header with the execution result.
|
||||
func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) {
|
||||
// No-op: unified tool blocks are rendered in DisplayToolMessage.
|
||||
}
|
||||
|
||||
// DisplayToolMessage renders and displays the complete result of a tool execution,
|
||||
// including the tool name, arguments, and result. The isError parameter determines
|
||||
// whether the result should be displayed as an error or success message.
|
||||
@@ -141,7 +134,7 @@ func (c *CLI) DisplayInfo(message string) {
|
||||
func (c *CLI) DisplayExtensionBlock(text, borderColor, subtitle string) {
|
||||
theme := GetTheme()
|
||||
|
||||
var borderClr = lipgloss.Color("#89b4fa")
|
||||
borderClr := theme.Info
|
||||
if borderColor != "" {
|
||||
borderClr = lipgloss.Color(borderColor)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,24 @@ var SlashCommands = []SlashCommand{
|
||||
return matches
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "/theme",
|
||||
Description: "Switch color theme (e.g. /theme catppuccin)",
|
||||
Category: "System",
|
||||
Complete: func(prefix string) []string {
|
||||
names := ListThemes()
|
||||
if prefix == "" {
|
||||
return names
|
||||
}
|
||||
var matches []string
|
||||
for _, n := range names {
|
||||
if strings.HasPrefix(n, strings.ToLower(prefix)) {
|
||||
matches = append(matches, n)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "/quit",
|
||||
Description: "Exit the application",
|
||||
|
||||
@@ -39,9 +39,26 @@ func SetTheme(theme Theme) {
|
||||
currentTheme = theme
|
||||
}
|
||||
|
||||
// MarkdownThemeColors defines colors for markdown rendering and syntax highlighting.
|
||||
type MarkdownThemeColors struct {
|
||||
Text color.Color
|
||||
Muted color.Color
|
||||
Heading color.Color
|
||||
Emph color.Color
|
||||
Strong color.Color
|
||||
Link color.Color
|
||||
Code color.Color
|
||||
Error color.Color
|
||||
Keyword color.Color
|
||||
String color.Color
|
||||
Number color.Color
|
||||
Comment color.Color
|
||||
}
|
||||
|
||||
// Theme defines a comprehensive color scheme for the application's UI, supporting
|
||||
// both light and dark terminal modes through adaptive colors. It includes semantic
|
||||
// colors for different message types and UI elements, based on the Catppuccin color palette.
|
||||
// both light and dark terminal modes through adaptive colors. Inspired by the
|
||||
// Knight Rider KITT aesthetic — scanner reds, amber dashboard glows, and dark
|
||||
// cockpit tones.
|
||||
type Theme struct {
|
||||
Primary color.Color
|
||||
Secondary color.Color
|
||||
@@ -70,40 +87,60 @@ type Theme struct {
|
||||
CodeBg color.Color // Background for code blocks (Read tool)
|
||||
GutterBg color.Color // Line-number gutter background
|
||||
WriteBg color.Color // Green-tinted bg for Write tool content
|
||||
|
||||
// Markdown rendering and syntax highlighting colors
|
||||
Markdown MarkdownThemeColors
|
||||
}
|
||||
|
||||
// DefaultTheme creates and returns the default KIT theme based on the Catppuccin
|
||||
// Mocha (dark) and Latte (light) color palettes. This theme provides a cohesive,
|
||||
// pleasant visual experience with carefully selected colors for different UI elements.
|
||||
// DefaultTheme creates and returns the default KIT theme inspired by the
|
||||
// Knight Rider KITT aesthetic — scanner reds, amber dashboard glows, and a
|
||||
// dark cockpit. No blues or bright greens; everything stays in the warm
|
||||
// red/amber/gray family of KITT's instrument panel.
|
||||
func DefaultTheme() Theme {
|
||||
return Theme{
|
||||
Primary: AdaptiveColor("#8839ef", "#cba6f7"), // Latte/Mocha Mauve
|
||||
Secondary: AdaptiveColor("#04a5e5", "#89dceb"), // Latte/Mocha Sky
|
||||
Success: AdaptiveColor("#40a02b", "#a6e3a1"), // Latte/Mocha Green
|
||||
Warning: AdaptiveColor("#df8e1d", "#f9e2af"), // Latte/Mocha Yellow
|
||||
Error: AdaptiveColor("#d20f39", "#f38ba8"), // Latte/Mocha Red
|
||||
Info: AdaptiveColor("#1e66f5", "#89b4fa"), // Latte/Mocha Blue
|
||||
Text: AdaptiveColor("#4c4f69", "#cdd6f4"), // Latte/Mocha Text
|
||||
Muted: AdaptiveColor("#6c6f85", "#a6adc8"), // Latte/Mocha Subtext 0
|
||||
VeryMuted: AdaptiveColor("#9ca0b0", "#6c7086"), // Latte/Mocha Overlay 0
|
||||
Background: AdaptiveColor("#eff1f5", "#1e1e2e"), // Latte/Mocha Base
|
||||
Border: AdaptiveColor("#acb0be", "#585b70"), // Latte/Mocha Surface 2
|
||||
MutedBorder: AdaptiveColor("#ccd0da", "#313244"), // Latte/Mocha Surface 0
|
||||
System: AdaptiveColor("#179299", "#94e2d5"), // Latte/Mocha Teal
|
||||
Tool: AdaptiveColor("#fe640b", "#fab387"), // Latte/Mocha Peach
|
||||
Accent: AdaptiveColor("#ea76cb", "#f5c2e7"), // Latte/Mocha Pink
|
||||
Highlight: AdaptiveColor("#e6e9ef", "#181825"), // Latte Mantle / Mocha Mantle
|
||||
Primary: AdaptiveColor("#CC1100", "#FF2200"), // KITT scanner red
|
||||
Secondary: AdaptiveColor("#CC6600", "#FF8800"), // Amber dashboard glow
|
||||
Success: AdaptiveColor("#998800", "#CCAA00"), // Warm gold — system OK
|
||||
Warning: AdaptiveColor("#CC8800", "#FFB800"), // Amber caution light
|
||||
Error: AdaptiveColor("#CC0000", "#FF3333"), // Alert red
|
||||
Info: AdaptiveColor("#BB6600", "#DD8833"), // Warm amber readout
|
||||
Text: AdaptiveColor("#1A1A1A", "#E0E0E0"), // Console text
|
||||
Muted: AdaptiveColor("#707070", "#808080"), // Dimmed readout
|
||||
VeryMuted: AdaptiveColor("#A0A0A0", "#505050"), // Inactive element
|
||||
Background: AdaptiveColor("#F0F0F0", "#0D0D0D"), // Cockpit interior
|
||||
Border: AdaptiveColor("#B0B0B0", "#3A3A3A"), // Panel edge
|
||||
MutedBorder: AdaptiveColor("#D0D0D0", "#222222"), // Subtle divider
|
||||
System: AdaptiveColor("#CC6600", "#FF8800"), // Amber system status
|
||||
Tool: AdaptiveColor("#CC6600", "#FF8800"), // Amber instrument
|
||||
Accent: AdaptiveColor("#DD2222", "#FF4444"), // Secondary scanner glow
|
||||
Highlight: AdaptiveColor("#FFF0F0", "#1A1010"), // Red-tinted mantle
|
||||
|
||||
// Diff backgrounds — subtle tinted variants of the base palette
|
||||
DiffInsertBg: AdaptiveColor("#d5f0d5", "#1a3a2a"), // Green tint
|
||||
DiffDeleteBg: AdaptiveColor("#f5d5d5", "#3a1a2a"), // Red tint
|
||||
DiffEqualBg: AdaptiveColor("#eceef3", "#232336"), // Neutral
|
||||
DiffMissingBg: AdaptiveColor("#e4e6eb", "#1a1a2e"), // Darker neutral
|
||||
// Diff backgrounds
|
||||
DiffInsertBg: AdaptiveColor("#F0E8D0", "#2A2410"), // Warm amber tint (added)
|
||||
DiffDeleteBg: AdaptiveColor("#F5D5D5", "#2E1A1A"), // Red tint (removed)
|
||||
DiffEqualBg: AdaptiveColor("#E8E8E8", "#161616"), // Neutral
|
||||
DiffMissingBg: AdaptiveColor("#E0E0E0", "#111111"), // Darker neutral
|
||||
|
||||
// Code & output backgrounds
|
||||
CodeBg: AdaptiveColor("#eceef3", "#232336"), // Matches DiffEqualBg
|
||||
GutterBg: AdaptiveColor("#e4e6eb", "#1a1a2e"), // Slightly darker
|
||||
WriteBg: AdaptiveColor("#d5f0d5", "#1a3a2a"), // Matches DiffInsertBg (green tint)
|
||||
CodeBg: AdaptiveColor("#E8E8E8", "#161616"), // Matches DiffEqualBg
|
||||
GutterBg: AdaptiveColor("#E0E0E0", "#111111"), // Slightly darker
|
||||
WriteBg: AdaptiveColor("#F0E8D0", "#2A2410"), // Warm amber tint
|
||||
|
||||
// Markdown & syntax highlighting — all warm tones
|
||||
Markdown: MarkdownThemeColors{
|
||||
Text: AdaptiveColor("#1A1A1A", "#E0E0E0"), // Console text
|
||||
Muted: AdaptiveColor("#707070", "#808080"), // Dimmed readout
|
||||
Heading: AdaptiveColor("#CC1100", "#FF4444"), // Scanner red accent
|
||||
Emph: AdaptiveColor("#CC8800", "#FFB800"), // Amber emphasis
|
||||
Strong: AdaptiveColor("#1A1A1A", "#E0E0E0"), // Bright text
|
||||
Link: AdaptiveColor("#CC4400", "#FF7744"), // Warm orange link
|
||||
Code: AdaptiveColor("#333333", "#CCCCCC"), // Inline code
|
||||
Error: AdaptiveColor("#CC0000", "#FF3333"), // Alert red
|
||||
Keyword: AdaptiveColor("#CC3300", "#FF6644"), // Orange-red keyword
|
||||
String: AdaptiveColor("#BB7700", "#DDAA33"), // Amber string
|
||||
Number: AdaptiveColor("#CC8800", "#FFB800"), // Amber number
|
||||
Comment: AdaptiveColor("#909090", "#606060"), // Dark gray comment
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ func CreateUsageTracker(modelString, providerAPIKey string) *UsageTracker {
|
||||
}
|
||||
|
||||
registry := models.GetGlobalRegistry()
|
||||
modelInfo, err := registry.ValidateModel(provider, model)
|
||||
if err != nil {
|
||||
modelInfo := registry.LookupModel(provider, model)
|
||||
if modelInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func SetupCLI(opts *CLISetupOptions) (*CLI, error) {
|
||||
// Skip usage tracking for ollama as it's not in models.dev
|
||||
if provider != "ollama" {
|
||||
registry := models.GetGlobalRegistry()
|
||||
if modelInfo, err := registry.ValidateModel(provider, model); err == nil {
|
||||
if modelInfo := registry.LookupModel(provider, model); modelInfo != nil {
|
||||
// Check if OAuth credentials are being used for Anthropic models
|
||||
isOAuth := false
|
||||
if provider == "anthropic" {
|
||||
|
||||
+20
-16
@@ -96,11 +96,12 @@ func NewInputComponent(width int, title string, appCtrl AppController) *InputCom
|
||||
key.WithHelp("ctrl+j", "insert newline"),
|
||||
)
|
||||
|
||||
// Style the textarea to match huh theme
|
||||
// Style the textarea using theme colors.
|
||||
theme := GetTheme()
|
||||
styles := ta.Styles()
|
||||
styles.Focused.Base = lipgloss.NewStyle()
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(theme.VeryMuted)
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(theme.Text)
|
||||
styles.Focused.Prompt = lipgloss.NewStyle()
|
||||
styles.Focused.CursorLine = lipgloss.NewStyle()
|
||||
ta.SetStyles(styles)
|
||||
@@ -376,9 +377,11 @@ func (s *InputComponent) handleSubmit(value string) tea.Cmd {
|
||||
func (s *InputComponent) View() tea.View {
|
||||
containerStyle := lipgloss.NewStyle()
|
||||
|
||||
theme := GetTheme()
|
||||
|
||||
// PaddingLeft(3) aligns with message content: border(1) + paddingLeft(2).
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")).
|
||||
Foreground(theme.Text).
|
||||
MarginBottom(1).
|
||||
PaddingLeft(3)
|
||||
|
||||
@@ -388,7 +391,7 @@ func (s *InputComponent) View() tea.View {
|
||||
BorderRight(false).
|
||||
BorderTop(false).
|
||||
BorderBottom(false).
|
||||
BorderForeground(lipgloss.Color("39")).
|
||||
BorderForeground(theme.Primary).
|
||||
PaddingLeft(2). // match message block paddingLeft
|
||||
Width(s.width - 1) // full width minus left border
|
||||
|
||||
@@ -405,7 +408,7 @@ func (s *InputComponent) View() tea.View {
|
||||
// Show image attachment indicator when images are pending.
|
||||
if len(s.pendingImages) > 0 {
|
||||
imgStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("39")).
|
||||
Foreground(theme.Secondary).
|
||||
PaddingLeft(3)
|
||||
|
||||
label := fmt.Sprintf("[%d image(s) attached] ctrl+u to clear", len(s.pendingImages))
|
||||
@@ -415,7 +418,7 @@ func (s *InputComponent) View() tea.View {
|
||||
|
||||
if !s.hideHint {
|
||||
helpStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Foreground(theme.VeryMuted).
|
||||
MarginTop(1).
|
||||
PaddingLeft(3)
|
||||
|
||||
@@ -440,10 +443,11 @@ func (s *InputComponent) View() tea.View {
|
||||
|
||||
// renderPopup renders the autocomplete popup for slash command suggestions.
|
||||
func (s *InputComponent) renderPopup() string {
|
||||
theme := GetTheme()
|
||||
popupWidth := max(s.width-4, 20)
|
||||
popupStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("236")).
|
||||
BorderForeground(theme.MutedBorder).
|
||||
Padding(1, 2).
|
||||
Width(popupWidth).
|
||||
MarginLeft(0)
|
||||
@@ -466,16 +470,16 @@ func (s *InputComponent) renderPopup() string {
|
||||
|
||||
var indicator string
|
||||
if i == s.selected {
|
||||
indicator = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Render("> ")
|
||||
indicator = lipgloss.NewStyle().Foreground(theme.Primary).Render("> ")
|
||||
} else {
|
||||
indicator = " "
|
||||
}
|
||||
|
||||
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
|
||||
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
||||
nameStyle := lipgloss.NewStyle().Foreground(theme.Secondary).Bold(true)
|
||||
descStyle := lipgloss.NewStyle().Foreground(theme.Muted)
|
||||
if i == s.selected {
|
||||
nameStyle = nameStyle.Foreground(lipgloss.Color("87"))
|
||||
descStyle = descStyle.Foreground(lipgloss.Color("250"))
|
||||
nameStyle = nameStyle.Foreground(theme.Primary)
|
||||
descStyle = descStyle.Foreground(theme.Text)
|
||||
}
|
||||
|
||||
if s.fileMode {
|
||||
@@ -530,10 +534,10 @@ func (s *InputComponent) renderPopup() string {
|
||||
}
|
||||
|
||||
if startIdx > 0 {
|
||||
items = append([]string{lipgloss.NewStyle().Foreground(lipgloss.Color("238")).Render(" ↑ more above")}, items...)
|
||||
items = append([]string{lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(" ↑ more above")}, items...)
|
||||
}
|
||||
if endIdx < len(s.filtered) {
|
||||
items = append(items, lipgloss.NewStyle().Foreground(lipgloss.Color("238")).Render(" ↓ more below"))
|
||||
items = append(items, lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(" ↓ more below"))
|
||||
}
|
||||
|
||||
content := strings.Join(items, "\n")
|
||||
@@ -547,7 +551,7 @@ func (s *InputComponent) renderPopup() string {
|
||||
} else {
|
||||
footerText = "↑↓ tab ↵ esc"
|
||||
}
|
||||
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("238")).Italic(true).
|
||||
footer := lipgloss.NewStyle().Foreground(theme.VeryMuted).Italic(true).
|
||||
Render(footerText)
|
||||
|
||||
return popupStyle.Render(content + "\n\n" + footer)
|
||||
|
||||
@@ -156,10 +156,10 @@ type MessageRenderer struct {
|
||||
getToolRenderer func(toolName string) *ToolRendererData
|
||||
}
|
||||
|
||||
// NewMessageRenderer creates and initializes a new MessageRenderer with the specified
|
||||
// newMessageRenderer creates and initializes a new MessageRenderer with the specified
|
||||
// terminal width and debug mode setting. The width parameter determines line wrapping
|
||||
// and layout calculations.
|
||||
func NewMessageRenderer(width int, debug bool) *MessageRenderer {
|
||||
func newMessageRenderer(width int, debug bool) *MessageRenderer {
|
||||
return &MessageRenderer{
|
||||
width: width,
|
||||
debug: debug,
|
||||
|
||||
+55
-6
@@ -372,11 +372,9 @@ type AppModel struct {
|
||||
appCtrl AppController
|
||||
|
||||
// input is the child input component (slash commands + autocomplete).
|
||||
// Placeholder until InputComponent is implemented in TAS-15.
|
||||
input inputComponentIface
|
||||
|
||||
// stream is the child streaming display component (spinner + streaming text).
|
||||
// Placeholder until StreamComponent is implemented in TAS-16.
|
||||
stream streamComponentIface
|
||||
|
||||
// renderer renders completed messages for tea.Println output. It is either
|
||||
@@ -593,7 +591,7 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
cr.getToolRenderer = opts.GetToolRenderer
|
||||
rdr = cr
|
||||
} else {
|
||||
mr := NewMessageRenderer(width, false)
|
||||
mr := newMessageRenderer(width, false)
|
||||
mr.getToolRenderer = opts.GetToolRenderer
|
||||
rdr = mr
|
||||
}
|
||||
@@ -1061,6 +1059,12 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
cmds = append(cmds, m.drainScrollback())
|
||||
return m, tea.Batch(cmds...)
|
||||
case "/theme":
|
||||
if cmd := m.handleThemeCommand(strings.TrimSpace(args)); cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
cmds = append(cmds, m.drainScrollback())
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1547,8 +1551,9 @@ func (m *AppModel) renderStream() string {
|
||||
|
||||
// Show canceling warning if set.
|
||||
if m.canceling {
|
||||
theme := GetTheme()
|
||||
warning := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("214")).
|
||||
Foreground(theme.Warning).
|
||||
Bold(true).
|
||||
Render(" ⚠ Press ESC again to cancel")
|
||||
return lipgloss.JoinVertical(lipgloss.Left,
|
||||
@@ -1858,6 +1863,8 @@ func (m *AppModel) handleSlashCommand(sc *SlashCommand) tea.Cmd {
|
||||
m.printResetUsage()
|
||||
case "/model":
|
||||
return m.handleModelCommand("")
|
||||
case "/theme":
|
||||
return m.handleThemeCommand("")
|
||||
case "/thinking":
|
||||
return m.handleThinkingCommand("")
|
||||
case "/compact":
|
||||
@@ -1901,8 +1908,8 @@ func (m *AppModel) printSystemMessage(text string) {
|
||||
func (m *AppModel) printExtensionBlock(evt app.ExtensionPrintEvent) {
|
||||
theme := GetTheme()
|
||||
|
||||
// Resolve border color: use the extension's hex value, fall back to theme accent.
|
||||
var borderClr = lipgloss.Color("#89b4fa") // default blue
|
||||
// Resolve border color: use the extension's hex value, fall back to theme info.
|
||||
borderClr := theme.Info
|
||||
if evt.BorderColor != "" {
|
||||
borderClr = lipgloss.Color(evt.BorderColor)
|
||||
}
|
||||
@@ -2412,6 +2419,48 @@ func (m *AppModel) handleModelCommand(args string) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Theme command handler
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// handleThemeCommand switches the active color theme. With no arguments it
|
||||
// lists available themes and highlights the active one. With a name argument
|
||||
// (e.g. "/theme catppuccin") it switches immediately.
|
||||
func (m *AppModel) handleThemeCommand(args string) tea.Cmd {
|
||||
if args == "" {
|
||||
// List available themes.
|
||||
names := ListThemes()
|
||||
active := ActiveThemeName()
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, "Available themes:")
|
||||
for _, name := range names {
|
||||
if name == active {
|
||||
lines = append(lines, fmt.Sprintf(" * %s (active)", name))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf(" %s", name))
|
||||
}
|
||||
}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, fmt.Sprintf("User themes: %s", userThemesDir()))
|
||||
if pdir := projectThemesDir(); pdir != "" {
|
||||
lines = append(lines, fmt.Sprintf("Project themes: %s", pdir))
|
||||
} else {
|
||||
lines = append(lines, "Project themes: .kit/themes/ (not found)")
|
||||
}
|
||||
m.printSystemMessage(strings.Join(lines, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ApplyTheme(args); err != nil {
|
||||
m.printSystemMessage(fmt.Sprintf("Theme error: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
m.printSystemMessage(fmt.Sprintf("Switched to theme: %s", args))
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Thinking command handler
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
@@ -116,7 +116,7 @@ func newTestAppModel(ctrl AppController) (*AppModel, *stubStreamComponent, *stub
|
||||
appCtrl: ctrl,
|
||||
stream: stream,
|
||||
input: input,
|
||||
renderer: NewMessageRenderer(80, false),
|
||||
renderer: newMessageRenderer(80, false),
|
||||
compactMode: false,
|
||||
modelName: "test-model",
|
||||
width: 80,
|
||||
|
||||
@@ -242,7 +242,7 @@ func (o *overlayDialog) Render() string {
|
||||
innerContent := strings.Join(parts, "\n")
|
||||
|
||||
// Resolve border color.
|
||||
borderClr := lipgloss.Color("#89b4fa") // default blue
|
||||
borderClr := theme.Info
|
||||
if o.borderColor != "" {
|
||||
borderClr = lipgloss.Color(o.borderColor)
|
||||
}
|
||||
|
||||
@@ -49,11 +49,12 @@ func NewSlashCommandInput(width int, title string) *SlashCommandInput {
|
||||
key.WithHelp("ctrl+j", "insert newline"),
|
||||
)
|
||||
|
||||
// Style the textarea to match huh theme
|
||||
// Style the textarea using theme colors.
|
||||
theme := GetTheme()
|
||||
styles := ta.Styles()
|
||||
styles.Focused.Base = lipgloss.NewStyle()
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(theme.VeryMuted)
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(theme.Text)
|
||||
styles.Focused.Prompt = lipgloss.NewStyle()
|
||||
styles.Focused.CursorLine = lipgloss.NewStyle()
|
||||
ta.SetStyles(styles)
|
||||
@@ -178,9 +179,11 @@ func (s *SlashCommandInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (s *SlashCommandInput) View() tea.View {
|
||||
containerStyle := lipgloss.NewStyle()
|
||||
|
||||
theme := GetTheme()
|
||||
|
||||
// PaddingLeft(3) aligns with message content: border(1) + paddingLeft(2).
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")).
|
||||
Foreground(theme.Text).
|
||||
MarginBottom(1).
|
||||
PaddingLeft(3)
|
||||
|
||||
@@ -191,7 +194,7 @@ func (s *SlashCommandInput) View() tea.View {
|
||||
BorderRight(false).
|
||||
BorderTop(false).
|
||||
BorderBottom(false).
|
||||
BorderForeground(lipgloss.Color("39")).
|
||||
BorderForeground(theme.Primary).
|
||||
PaddingLeft(2). // match message block paddingLeft
|
||||
Width(s.width - 1) // full width minus left border
|
||||
|
||||
@@ -223,7 +226,7 @@ func (s *SlashCommandInput) View() tea.View {
|
||||
// Add help text at bottom (unless hidden by extension).
|
||||
if !s.hideHint {
|
||||
helpStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Foreground(theme.VeryMuted).
|
||||
MarginTop(1).
|
||||
PaddingLeft(3)
|
||||
|
||||
@@ -240,10 +243,12 @@ func (s *SlashCommandInput) View() tea.View {
|
||||
|
||||
// renderPopup renders the autocomplete popup
|
||||
func (s *SlashCommandInput) renderPopup() string {
|
||||
theme := GetTheme()
|
||||
|
||||
// Popup styling
|
||||
popupStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("236")).
|
||||
BorderForeground(theme.MutedBorder).
|
||||
Padding(1, 2).
|
||||
Width(s.width - 4). // Account for container padding
|
||||
MarginLeft(0) // No extra margin needed due to container padding
|
||||
@@ -268,7 +273,7 @@ func (s *SlashCommandInput) renderPopup() string {
|
||||
var indicator string
|
||||
if i == s.selected {
|
||||
indicator = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("39")).
|
||||
Foreground(theme.Primary).
|
||||
Render("> ")
|
||||
} else {
|
||||
indicator = " "
|
||||
@@ -276,16 +281,16 @@ func (s *SlashCommandInput) renderPopup() string {
|
||||
|
||||
// Format item
|
||||
nameStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("39")).
|
||||
Foreground(theme.Secondary).
|
||||
Bold(true)
|
||||
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("243"))
|
||||
Foreground(theme.Muted)
|
||||
|
||||
// Highlight selected item
|
||||
if i == s.selected {
|
||||
nameStyle = nameStyle.Foreground(lipgloss.Color("87"))
|
||||
descStyle = descStyle.Foreground(lipgloss.Color("250"))
|
||||
nameStyle = nameStyle.Foreground(theme.Primary)
|
||||
descStyle = descStyle.Foreground(theme.Text)
|
||||
}
|
||||
|
||||
// Format with proper spacing
|
||||
@@ -305,11 +310,11 @@ func (s *SlashCommandInput) renderPopup() string {
|
||||
|
||||
// Add scroll indicators if needed
|
||||
if startIdx > 0 {
|
||||
scrollUpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("238"))
|
||||
scrollUpStyle := lipgloss.NewStyle().Foreground(theme.VeryMuted)
|
||||
items = append([]string{scrollUpStyle.Render(" ↑ more above")}, items...)
|
||||
}
|
||||
if endIdx < len(s.filtered) {
|
||||
scrollDownStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("238"))
|
||||
scrollDownStyle := lipgloss.NewStyle().Foreground(theme.VeryMuted)
|
||||
items = append(items, scrollDownStyle.Render(" ↓ more below"))
|
||||
}
|
||||
// Join items
|
||||
@@ -317,7 +322,7 @@ func (s *SlashCommandInput) renderPopup() string {
|
||||
|
||||
// Add footer hint
|
||||
footerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("238")).
|
||||
Foreground(theme.VeryMuted).
|
||||
Italic(true)
|
||||
footer := footerStyle.Render("↑↓ navigate • tab complete • ↵ select • esc dismiss")
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ func NewStreamComponent(compactMode bool, width int, modelName string) *StreamCo
|
||||
spinnerFrames: knightRiderFrames(),
|
||||
compactMode: compactMode,
|
||||
modelName: modelName,
|
||||
messageRenderer: NewMessageRenderer(width, false),
|
||||
messageRenderer: newMessageRenderer(width, false),
|
||||
compactRenderer: NewCompactRenderer(width, false),
|
||||
width: width,
|
||||
}
|
||||
|
||||
+83
-124
@@ -1,11 +1,12 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/glamour/ansi"
|
||||
"github.com/mark3labs/kit/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// uintPtr returns a pointer to u. Used by ansi.StyleConfig fields.
|
||||
@@ -20,6 +21,18 @@ func BaseStyle() lipgloss.Style {
|
||||
return lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
// colorHex converts a color.Color to a hex string suitable for ansi.StyleConfig.
|
||||
func colorHex(c color.Color) string {
|
||||
r, g, b, _ := c.RGBA()
|
||||
return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
|
||||
}
|
||||
|
||||
// colorHexPtr returns a pointer to the hex string of a color.Color.
|
||||
func colorHexPtr(c color.Color) *string {
|
||||
s := colorHex(c)
|
||||
return &s
|
||||
}
|
||||
|
||||
// GetMarkdownRenderer creates and returns a configured glamour.TermRenderer for
|
||||
// rendering markdown content with syntax highlighting and proper formatting. The
|
||||
// renderer is customized with our theme colors and adapted to the specified width.
|
||||
@@ -31,169 +44,119 @@ func GetMarkdownRenderer(width int) *glamour.TermRenderer {
|
||||
return r
|
||||
}
|
||||
|
||||
// colorScheme holds resolved color values for markdown rendering.
|
||||
type colorScheme struct {
|
||||
text string
|
||||
muted string
|
||||
heading string
|
||||
emph string
|
||||
strong string
|
||||
link string
|
||||
code string
|
||||
err string
|
||||
keyword string
|
||||
str string
|
||||
number string
|
||||
comment string
|
||||
}
|
||||
|
||||
// resolveColorScheme determines the color palette based on user config and background.
|
||||
func resolveColorScheme() colorScheme {
|
||||
var mdTheme config.MarkdownTheme
|
||||
err := config.FilepathOr("markdown-theme", &mdTheme)
|
||||
fromConfig := err == nil && viper.InConfig("markdown-theme")
|
||||
|
||||
if fromConfig && IsDarkBackground() {
|
||||
return colorScheme{
|
||||
text: mdTheme.Text.Light, muted: mdTheme.Muted.Light,
|
||||
heading: mdTheme.Heading.Light, emph: mdTheme.Emph.Light,
|
||||
strong: mdTheme.Strong.Light, link: mdTheme.Link.Light,
|
||||
code: mdTheme.Code.Light, err: mdTheme.Error.Light,
|
||||
keyword: mdTheme.Keyword.Light, str: mdTheme.String.Light,
|
||||
number: mdTheme.Number.Light, comment: mdTheme.Comment.Light,
|
||||
}
|
||||
}
|
||||
if fromConfig {
|
||||
return colorScheme{
|
||||
text: mdTheme.Text.Dark, muted: mdTheme.Muted.Dark,
|
||||
heading: mdTheme.Heading.Dark, emph: mdTheme.Emph.Dark,
|
||||
strong: mdTheme.Strong.Dark, link: mdTheme.Link.Dark,
|
||||
code: mdTheme.Code.Dark, err: mdTheme.Error.Dark,
|
||||
keyword: mdTheme.Keyword.Dark, str: mdTheme.String.Dark,
|
||||
number: mdTheme.Number.Dark, comment: mdTheme.Comment.Dark,
|
||||
}
|
||||
}
|
||||
if IsDarkBackground() {
|
||||
return colorScheme{
|
||||
text: "#F9FAFB", muted: "#9CA3AF",
|
||||
heading: "#22D3EE", emph: "#FDE047",
|
||||
strong: "#F9FAFB", link: "#60A5FA",
|
||||
code: "#D1D5DB", err: "#F87171",
|
||||
keyword: "#C084FC", str: "#34D399",
|
||||
number: "#FBBF24", comment: "#9CA3AF",
|
||||
}
|
||||
}
|
||||
return colorScheme{
|
||||
text: "#1F2937", muted: "#6B7280",
|
||||
heading: "#0891B2", emph: "#D97706",
|
||||
strong: "#1F2937", link: "#2563EB",
|
||||
code: "#374151", err: "#DC2626",
|
||||
keyword: "#7C3AED", str: "#059669",
|
||||
number: "#D97706", comment: "#6B7280",
|
||||
}
|
||||
}
|
||||
|
||||
// generateMarkdownStyleConfig creates an ansi.StyleConfig for markdown rendering.
|
||||
// generateMarkdownStyleConfig creates an ansi.StyleConfig from the active theme.
|
||||
func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
cs := resolveColorScheme()
|
||||
md := GetTheme().Markdown
|
||||
text := colorHexPtr(md.Text)
|
||||
muted := colorHexPtr(md.Muted)
|
||||
heading := colorHexPtr(md.Heading)
|
||||
emph := colorHexPtr(md.Emph)
|
||||
strong := colorHexPtr(md.Strong)
|
||||
link := colorHexPtr(md.Link)
|
||||
code := colorHexPtr(md.Code)
|
||||
errClr := colorHexPtr(md.Error)
|
||||
keyword := colorHexPtr(md.Keyword)
|
||||
str := colorHexPtr(md.String)
|
||||
number := colorHexPtr(md.Number)
|
||||
comment := colorHexPtr(md.Comment)
|
||||
|
||||
return ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "",
|
||||
BlockSuffix: "",
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
Margin: uintPtr(0), // Remove margin to prevent spacing
|
||||
Margin: uintPtr(0),
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: &cs.muted,
|
||||
Color: muted,
|
||||
Italic: new(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
Indent: uintPtr(1),
|
||||
},
|
||||
List: ansi.StyleList{
|
||||
LevelIndent: 0, // Remove list indentation
|
||||
LevelIndent: 0,
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
Bold: new(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: new(true),
|
||||
Color: &cs.muted,
|
||||
Color: muted,
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: &cs.emph,
|
||||
Color: emph,
|
||||
Italic: new(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: new(true),
|
||||
Color: &cs.strong,
|
||||
Color: strong,
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: &cs.muted,
|
||||
Color: muted,
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
StylePrimitive: ansi.StylePrimitive{},
|
||||
@@ -201,25 +164,25 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: &cs.link,
|
||||
Color: link,
|
||||
Underline: new(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: &cs.link,
|
||||
Color: link,
|
||||
Bold: new(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: &cs.link,
|
||||
Color: link,
|
||||
Underline: new(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: &cs.link,
|
||||
Color: link,
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: &cs.code,
|
||||
Color: code,
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
},
|
||||
@@ -228,50 +191,46 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "",
|
||||
Color: &cs.code,
|
||||
Color: code,
|
||||
},
|
||||
Margin: uintPtr(0), // Remove margin
|
||||
Margin: uintPtr(0),
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
Text: ansi.StylePrimitive{Color: &cs.text},
|
||||
Error: ansi.StylePrimitive{Color: &cs.err},
|
||||
Comment: ansi.StylePrimitive{Color: &cs.comment},
|
||||
CommentPreproc: ansi.StylePrimitive{Color: &cs.keyword},
|
||||
Keyword: ansi.StylePrimitive{Color: &cs.keyword},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
Color: &cs.keyword,
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
Color: &cs.keyword,
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{Color: &cs.keyword},
|
||||
Operator: ansi.StylePrimitive{Color: &cs.text},
|
||||
Punctuation: ansi.StylePrimitive{Color: &cs.text},
|
||||
Name: ansi.StylePrimitive{Color: &cs.text},
|
||||
NameBuiltin: ansi.StylePrimitive{Color: &cs.text},
|
||||
NameTag: ansi.StylePrimitive{Color: &cs.keyword},
|
||||
NameAttribute: ansi.StylePrimitive{Color: &cs.text},
|
||||
NameClass: ansi.StylePrimitive{Color: &cs.keyword},
|
||||
NameConstant: ansi.StylePrimitive{Color: &cs.text},
|
||||
NameDecorator: ansi.StylePrimitive{Color: &cs.text},
|
||||
NameFunction: ansi.StylePrimitive{Color: &cs.text},
|
||||
LiteralNumber: ansi.StylePrimitive{Color: &cs.number},
|
||||
LiteralString: ansi.StylePrimitive{Color: &cs.str},
|
||||
Text: ansi.StylePrimitive{Color: text},
|
||||
Error: ansi.StylePrimitive{Color: errClr},
|
||||
Comment: ansi.StylePrimitive{Color: comment},
|
||||
CommentPreproc: ansi.StylePrimitive{Color: keyword},
|
||||
Keyword: ansi.StylePrimitive{Color: keyword},
|
||||
KeywordReserved: ansi.StylePrimitive{Color: keyword},
|
||||
KeywordNamespace: ansi.StylePrimitive{Color: keyword},
|
||||
KeywordType: ansi.StylePrimitive{Color: keyword},
|
||||
Operator: ansi.StylePrimitive{Color: text},
|
||||
Punctuation: ansi.StylePrimitive{Color: text},
|
||||
Name: ansi.StylePrimitive{Color: text},
|
||||
NameBuiltin: ansi.StylePrimitive{Color: text},
|
||||
NameTag: ansi.StylePrimitive{Color: keyword},
|
||||
NameAttribute: ansi.StylePrimitive{Color: text},
|
||||
NameClass: ansi.StylePrimitive{Color: keyword},
|
||||
NameConstant: ansi.StylePrimitive{Color: text},
|
||||
NameDecorator: ansi.StylePrimitive{Color: text},
|
||||
NameFunction: ansi.StylePrimitive{Color: text},
|
||||
LiteralNumber: ansi.StylePrimitive{Color: number},
|
||||
LiteralString: ansi.StylePrimitive{Color: str},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
Color: &cs.keyword,
|
||||
Color: keyword,
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{Color: &cs.err},
|
||||
GenericDeleted: ansi.StylePrimitive{Color: errClr},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
Color: &cs.emph,
|
||||
Color: emph,
|
||||
Italic: new(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{Color: &cs.str},
|
||||
GenericInserted: ansi.StylePrimitive{Color: str},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
Color: &cs.strong,
|
||||
Color: strong,
|
||||
Bold: new(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
Color: &cs.heading,
|
||||
Color: heading,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -288,14 +247,14 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: &cs.link,
|
||||
Color: link,
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
Paragraph: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: &cs.text,
|
||||
Color: text,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,637 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ThemeEntry is a named, loadable theme — either built-in or discovered from disk.
|
||||
type ThemeEntry struct {
|
||||
Name string // Display name (filename stem or preset name)
|
||||
Source string // "builtin" or absolute file path
|
||||
theme Theme // Resolved theme (lazy-loaded for file-based)
|
||||
loaded bool
|
||||
}
|
||||
|
||||
// Theme returns the resolved ui.Theme, loading from disk on first access.
|
||||
func (e *ThemeEntry) Theme() (Theme, error) {
|
||||
if e.loaded {
|
||||
return e.theme, nil
|
||||
}
|
||||
if e.Source == "builtin" {
|
||||
// Already set at registration time.
|
||||
return e.theme, nil
|
||||
}
|
||||
t, err := loadThemeFile(e.Source)
|
||||
if err != nil {
|
||||
return Theme{}, fmt.Errorf("loading theme %q: %w", e.Name, err)
|
||||
}
|
||||
e.theme = t
|
||||
e.loaded = true
|
||||
return e.theme, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Built-in presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// builtinThemes returns the set of themes shipped with Kit.
|
||||
// makeTheme builds a full Theme from a compact palette spec. Fields left as
|
||||
// zero color.Color inherit from the KITT default theme, keeping the preset
|
||||
// definitions focused on what differs.
|
||||
type presetColors struct {
|
||||
primary, secondary, success, warning, error_, info [2]string // [light, dark]
|
||||
text, muted, veryMuted, background, border, mutedBorder [2]string
|
||||
system, tool, accent, highlight [2]string
|
||||
mdKeyword, mdString, mdNumber, mdComment, mdHeading, mdLink [2]string
|
||||
}
|
||||
|
||||
func makeTheme(p presetColors) Theme {
|
||||
ac := func(pair [2]string) color.Color { return AdaptiveColor(pair[0], pair[1]) }
|
||||
def := DefaultTheme()
|
||||
acOr := func(pair [2]string, fb color.Color) color.Color {
|
||||
if pair[0] == "" && pair[1] == "" {
|
||||
return fb
|
||||
}
|
||||
return ac(pair)
|
||||
}
|
||||
t := Theme{
|
||||
Primary: ac(p.primary),
|
||||
Secondary: acOr(p.secondary, ac(p.primary)),
|
||||
Success: ac(p.success),
|
||||
Warning: ac(p.warning),
|
||||
Error: ac(p.error_),
|
||||
Info: ac(p.info),
|
||||
Text: ac(p.text),
|
||||
Muted: acOr(p.muted, def.Muted),
|
||||
VeryMuted: acOr(p.veryMuted, def.VeryMuted),
|
||||
Background: ac(p.background),
|
||||
Border: acOr(p.border, def.Border),
|
||||
MutedBorder: acOr(p.mutedBorder, def.MutedBorder),
|
||||
System: acOr(p.system, ac(p.info)),
|
||||
Tool: acOr(p.tool, ac(p.warning)),
|
||||
Accent: acOr(p.accent, ac(p.primary)),
|
||||
Highlight: acOr(p.highlight, def.Highlight),
|
||||
}
|
||||
// Derive diff/code backgrounds from the base background.
|
||||
t.DiffInsertBg = def.DiffInsertBg
|
||||
t.DiffDeleteBg = def.DiffDeleteBg
|
||||
t.DiffEqualBg = def.DiffEqualBg
|
||||
t.DiffMissingBg = def.DiffMissingBg
|
||||
t.CodeBg = def.CodeBg
|
||||
t.GutterBg = def.GutterBg
|
||||
t.WriteBg = def.WriteBg
|
||||
// Markdown colors.
|
||||
t.Markdown = MarkdownThemeColors{
|
||||
Text: t.Text,
|
||||
Muted: t.Muted,
|
||||
Heading: acOr(p.mdHeading, t.Primary),
|
||||
Emph: t.Warning,
|
||||
Strong: t.Text,
|
||||
Link: acOr(p.mdLink, t.Info),
|
||||
Code: t.Muted,
|
||||
Error: t.Error,
|
||||
Keyword: acOr(p.mdKeyword, t.Primary),
|
||||
String: acOr(p.mdString, t.Success),
|
||||
Number: acOr(p.mdNumber, t.Warning),
|
||||
Comment: acOr(p.mdComment, t.VeryMuted),
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// builtinThemes returns the set of themes shipped with Kit.
|
||||
// Inspired by the OpenCode theme collection.
|
||||
func builtinThemes() map[string]Theme {
|
||||
return map[string]Theme{
|
||||
"kitt": DefaultTheme(),
|
||||
|
||||
"catppuccin": makeTheme(presetColors{
|
||||
primary: [2]string{"#8839ef", "#cba6f7"}, secondary: [2]string{"#04a5e5", "#89dceb"},
|
||||
success: [2]string{"#40a02b", "#a6e3a1"}, warning: [2]string{"#df8e1d", "#f9e2af"},
|
||||
error_: [2]string{"#d20f39", "#f38ba8"}, info: [2]string{"#1e66f5", "#89b4fa"},
|
||||
text: [2]string{"#4c4f69", "#cdd6f4"}, muted: [2]string{"#6c6f85", "#a6adc8"},
|
||||
veryMuted: [2]string{"#9ca0b0", "#6c7086"}, background: [2]string{"#eff1f5", "#1e1e2e"},
|
||||
border: [2]string{"#acb0be", "#585b70"}, mutedBorder: [2]string{"#ccd0da", "#313244"},
|
||||
system: [2]string{"#179299", "#94e2d5"}, tool: [2]string{"#fe640b", "#fab387"},
|
||||
accent: [2]string{"#ea76cb", "#f5c2e7"}, highlight: [2]string{"#e6e9ef", "#181825"},
|
||||
mdKeyword: [2]string{"#8839ef", "#cba6f7"}, mdString: [2]string{"#40a02b", "#a6e3a1"},
|
||||
mdNumber: [2]string{"#fe640b", "#fab387"}, mdComment: [2]string{"#9ca0b0", "#6c7086"},
|
||||
}),
|
||||
|
||||
"dracula": makeTheme(presetColors{
|
||||
primary: [2]string{"#7c6bf5", "#bd93f9"}, secondary: [2]string{"#d16090", "#ff79c6"},
|
||||
success: [2]string{"#2fbf71", "#50fa7b"}, warning: [2]string{"#f7a14d", "#ffb86c"},
|
||||
error_: [2]string{"#d9536f", "#ff5555"}, info: [2]string{"#1d7fc5", "#8be9fd"},
|
||||
text: [2]string{"#1f1f2f", "#f8f8f2"}, background: [2]string{"#f8f8f2", "#1d1e28"},
|
||||
accent: [2]string{"#d16090", "#ff79c6"},
|
||||
mdKeyword: [2]string{"#7c6bf5", "#bd93f9"}, mdString: [2]string{"#2fbf71", "#50fa7b"},
|
||||
mdComment: [2]string{"#6272a4", "#6272a4"},
|
||||
}),
|
||||
|
||||
"tokyonight": makeTheme(presetColors{
|
||||
primary: [2]string{"#2e7de9", "#7aa2f7"}, secondary: [2]string{"#b15c00", "#ff9e64"},
|
||||
success: [2]string{"#587539", "#9ece6a"}, warning: [2]string{"#8c6c3e", "#e0af68"},
|
||||
error_: [2]string{"#c94060", "#f7768e"}, info: [2]string{"#007197", "#7dcfff"},
|
||||
text: [2]string{"#273153", "#c0caf5"}, background: [2]string{"#e1e2e7", "#1a1b26"},
|
||||
mdKeyword: [2]string{"#2e7de9", "#7aa2f7"}, mdString: [2]string{"#587539", "#9ece6a"},
|
||||
mdComment: [2]string{"#848cb5", "#565f89"},
|
||||
}),
|
||||
|
||||
"nord": makeTheme(presetColors{
|
||||
primary: [2]string{"#5e81ac", "#88c0d0"}, secondary: [2]string{"#bf616a", "#d57780"},
|
||||
success: [2]string{"#8fbcbb", "#a3be8c"}, warning: [2]string{"#d08770", "#d08770"},
|
||||
error_: [2]string{"#bf616a", "#bf616a"}, info: [2]string{"#81a1c1", "#81a1c1"},
|
||||
text: [2]string{"#2e3440", "#e5e9f0"}, background: [2]string{"#eceff4", "#2e3440"},
|
||||
mdKeyword: [2]string{"#5e81ac", "#81a1c1"}, mdString: [2]string{"#8fbcbb", "#a3be8c"},
|
||||
mdComment: [2]string{"#616e88", "#616e88"},
|
||||
}),
|
||||
|
||||
"gruvbox": makeTheme(presetColors{
|
||||
primary: [2]string{"#076678", "#83a598"}, secondary: [2]string{"#9d0006", "#fb4934"},
|
||||
success: [2]string{"#79740e", "#b8bb26"}, warning: [2]string{"#b57614", "#fabd2f"},
|
||||
error_: [2]string{"#9d0006", "#fb4934"}, info: [2]string{"#8f3f71", "#d3869b"},
|
||||
text: [2]string{"#3c3836", "#ebdbb2"}, background: [2]string{"#fbf1c7", "#282828"},
|
||||
mdKeyword: [2]string{"#9d0006", "#fb4934"}, mdString: [2]string{"#79740e", "#b8bb26"},
|
||||
mdComment: [2]string{"#928374", "#928374"},
|
||||
}),
|
||||
|
||||
"monokai": makeTheme(presetColors{
|
||||
primary: [2]string{"#bf7bff", "#ae81ff"}, secondary: [2]string{"#d9487c", "#f92672"},
|
||||
success: [2]string{"#4fb54b", "#a6e22e"}, warning: [2]string{"#f1a948", "#fd971f"},
|
||||
error_: [2]string{"#e54b4b", "#f92672"}, info: [2]string{"#2d9ad7", "#66d9ef"},
|
||||
text: [2]string{"#292318", "#f8f8f2"}, background: [2]string{"#fdf8ec", "#272822"},
|
||||
mdKeyword: [2]string{"#d9487c", "#f92672"}, mdString: [2]string{"#4fb54b", "#a6e22e"},
|
||||
mdComment: [2]string{"#888888", "#75715e"},
|
||||
}),
|
||||
|
||||
"solarized": makeTheme(presetColors{
|
||||
primary: [2]string{"#268bd2", "#6c71c4"}, secondary: [2]string{"#d33682", "#d33682"},
|
||||
success: [2]string{"#859900", "#859900"}, warning: [2]string{"#b58900", "#b58900"},
|
||||
error_: [2]string{"#dc322f", "#dc322f"}, info: [2]string{"#2aa198", "#2aa198"},
|
||||
text: [2]string{"#586e75", "#93a1a1"}, background: [2]string{"#fdf6e3", "#002b36"},
|
||||
mdKeyword: [2]string{"#268bd2", "#6c71c4"}, mdString: [2]string{"#859900", "#859900"},
|
||||
mdComment: [2]string{"#93a1a1", "#586e75"},
|
||||
}),
|
||||
|
||||
"github": makeTheme(presetColors{
|
||||
primary: [2]string{"#0969da", "#58a6ff"}, secondary: [2]string{"#1b7c83", "#39c5cf"},
|
||||
success: [2]string{"#1a7f37", "#3fb950"}, warning: [2]string{"#9a6700", "#e3b341"},
|
||||
error_: [2]string{"#cf222e", "#f85149"}, info: [2]string{"#bc4c00", "#d29922"},
|
||||
text: [2]string{"#24292f", "#c9d1d9"}, background: [2]string{"#ffffff", "#0d1117"},
|
||||
mdKeyword: [2]string{"#0969da", "#58a6ff"}, mdString: [2]string{"#1a7f37", "#3fb950"},
|
||||
mdComment: [2]string{"#6e7781", "#8b949e"},
|
||||
}),
|
||||
|
||||
"one-dark": makeTheme(presetColors{
|
||||
primary: [2]string{"#4078f2", "#61afef"}, secondary: [2]string{"#0184bc", "#56b6c2"},
|
||||
success: [2]string{"#50a14f", "#98c379"}, warning: [2]string{"#c18401", "#e5c07b"},
|
||||
error_: [2]string{"#e45649", "#e06c75"}, info: [2]string{"#986801", "#d19a66"},
|
||||
text: [2]string{"#383a42", "#abb2bf"}, background: [2]string{"#fafafa", "#282c34"},
|
||||
mdKeyword: [2]string{"#a626a4", "#c678dd"}, mdString: [2]string{"#50a14f", "#98c379"},
|
||||
mdComment: [2]string{"#a0a1a7", "#5c6370"},
|
||||
}),
|
||||
|
||||
"rose-pine": makeTheme(presetColors{
|
||||
primary: [2]string{"#31748f", "#9ccfd8"}, secondary: [2]string{"#d7827e", "#ebbcba"},
|
||||
success: [2]string{"#286983", "#31748f"}, warning: [2]string{"#ea9d34", "#f6c177"},
|
||||
error_: [2]string{"#b4637a", "#eb6f92"}, info: [2]string{"#56949f", "#9ccfd8"},
|
||||
text: [2]string{"#575279", "#e0def4"}, background: [2]string{"#faf4ed", "#191724"},
|
||||
mdKeyword: [2]string{"#31748f", "#9ccfd8"}, mdString: [2]string{"#ea9d34", "#f6c177"},
|
||||
mdComment: [2]string{"#9893a5", "#6e6a86"},
|
||||
}),
|
||||
|
||||
"ayu": makeTheme(presetColors{
|
||||
primary: [2]string{"#4aa8c8", "#3fb7e3"}, secondary: [2]string{"#ef7d71", "#f2856f"},
|
||||
success: [2]string{"#5fb978", "#78d05c"}, warning: [2]string{"#ea9f41", "#e4a75c"},
|
||||
error_: [2]string{"#e6656a", "#f58572"}, info: [2]string{"#2f9bce", "#66c6f1"},
|
||||
text: [2]string{"#4f5964", "#d6dae0"}, background: [2]string{"#fdfaf4", "#0f1419"},
|
||||
mdKeyword: [2]string{"#4aa8c8", "#3fb7e3"}, mdString: [2]string{"#5fb978", "#78d05c"},
|
||||
mdComment: [2]string{"#abb0b6", "#5c6773"},
|
||||
}),
|
||||
|
||||
"material": makeTheme(presetColors{
|
||||
primary: [2]string{"#6182b8", "#82aaff"}, secondary: [2]string{"#39adb5", "#89ddff"},
|
||||
success: [2]string{"#91b859", "#c3e88d"}, warning: [2]string{"#ffb300", "#ffcb6b"},
|
||||
error_: [2]string{"#e53935", "#f07178"}, info: [2]string{"#f4511e", "#ffcb6b"},
|
||||
text: [2]string{"#263238", "#eeffff"}, background: [2]string{"#fafafa", "#263238"},
|
||||
mdKeyword: [2]string{"#6182b8", "#82aaff"}, mdString: [2]string{"#91b859", "#c3e88d"},
|
||||
mdComment: [2]string{"#aabfc5", "#546e7a"},
|
||||
}),
|
||||
|
||||
"everforest": makeTheme(presetColors{
|
||||
primary: [2]string{"#8da101", "#a7c080"}, secondary: [2]string{"#df69ba", "#d699b6"},
|
||||
success: [2]string{"#8da101", "#a7c080"}, warning: [2]string{"#f57d26", "#e69875"},
|
||||
error_: [2]string{"#f85552", "#e67e80"}, info: [2]string{"#35a77c", "#83c092"},
|
||||
text: [2]string{"#5c6a72", "#d3c6aa"}, background: [2]string{"#fdf6e3", "#2d353b"},
|
||||
mdKeyword: [2]string{"#8da101", "#a7c080"}, mdString: [2]string{"#35a77c", "#83c092"},
|
||||
mdComment: [2]string{"#939b84", "#859289"},
|
||||
}),
|
||||
|
||||
"kanagawa": makeTheme(presetColors{
|
||||
primary: [2]string{"#2D4F67", "#7E9CD8"}, secondary: [2]string{"#D27E99", "#D27E99"},
|
||||
success: [2]string{"#98BB6C", "#98BB6C"}, warning: [2]string{"#D7A657", "#D7A657"},
|
||||
error_: [2]string{"#E82424", "#E82424"}, info: [2]string{"#76946A", "#76946A"},
|
||||
text: [2]string{"#54433A", "#DCD7BA"}, background: [2]string{"#F2E9DE", "#1F1F28"},
|
||||
mdKeyword: [2]string{"#2D4F67", "#7E9CD8"}, mdString: [2]string{"#98BB6C", "#98BB6C"},
|
||||
mdComment: [2]string{"#A09D98", "#727169"},
|
||||
}),
|
||||
|
||||
"amoled": makeTheme(presetColors{
|
||||
primary: [2]string{"#6200ff", "#b388ff"}, secondary: [2]string{"#ff0080", "#ff4081"},
|
||||
success: [2]string{"#00e676", "#00ff88"}, warning: [2]string{"#ffab00", "#ffea00"},
|
||||
error_: [2]string{"#ff1744", "#ff1744"}, info: [2]string{"#00b0ff", "#18ffff"},
|
||||
text: [2]string{"#0a0a0a", "#ffffff"}, background: [2]string{"#f0f0f0", "#000000"},
|
||||
mdKeyword: [2]string{"#6200ff", "#b388ff"}, mdString: [2]string{"#00e676", "#00ff88"},
|
||||
mdComment: [2]string{"#757575", "#424242"},
|
||||
}),
|
||||
|
||||
"synthwave": makeTheme(presetColors{
|
||||
primary: [2]string{"#00bcd4", "#36f9f6"}, secondary: [2]string{"#9c27b0", "#b084eb"},
|
||||
success: [2]string{"#4caf50", "#72f1b8"}, warning: [2]string{"#ff9800", "#fede5d"},
|
||||
error_: [2]string{"#f44336", "#fe4450"}, info: [2]string{"#ff5722", "#ff8b39"},
|
||||
text: [2]string{"#262335", "#ffffff"}, background: [2]string{"#fafafa", "#262335"},
|
||||
mdKeyword: [2]string{"#9c27b0", "#b084eb"}, mdString: [2]string{"#4caf50", "#72f1b8"},
|
||||
mdComment: [2]string{"#848bbd", "#848bbd"},
|
||||
}),
|
||||
|
||||
"vesper": makeTheme(presetColors{
|
||||
primary: [2]string{"#FFC799", "#FFC799"}, secondary: [2]string{"#B30000", "#FF8080"},
|
||||
success: [2]string{"#99FFE4", "#99FFE4"}, warning: [2]string{"#FFC799", "#FFC799"},
|
||||
error_: [2]string{"#FF8080", "#FF8080"}, info: [2]string{"#FFC799", "#FFC799"},
|
||||
text: [2]string{"#1a1a1a", "#FFF"}, background: [2]string{"#F0F0F0", "#101010"},
|
||||
mdKeyword: [2]string{"#FFC799", "#FFC799"}, mdString: [2]string{"#99FFE4", "#99FFE4"},
|
||||
mdComment: [2]string{"#7a7a7a", "#505050"},
|
||||
}),
|
||||
|
||||
"flexoki": makeTheme(presetColors{
|
||||
primary: [2]string{"#205EA6", "#DA702C"}, secondary: [2]string{"#BC5215", "#8B7EC8"},
|
||||
success: [2]string{"#66800B", "#879A39"}, warning: [2]string{"#BC5215", "#DA702C"},
|
||||
error_: [2]string{"#AF3029", "#D14D41"}, info: [2]string{"#24837B", "#3AA99F"},
|
||||
text: [2]string{"#100F0F", "#CECDC3"}, background: [2]string{"#FFFCF0", "#100F0F"},
|
||||
mdKeyword: [2]string{"#205EA6", "#DA702C"}, mdString: [2]string{"#66800B", "#879A39"},
|
||||
mdComment: [2]string{"#878580", "#878580"},
|
||||
}),
|
||||
|
||||
"matrix": makeTheme(presetColors{
|
||||
primary: [2]string{"#1cc24b", "#2eff6a"}, secondary: [2]string{"#c770ff", "#c770ff"},
|
||||
success: [2]string{"#1cc24b", "#62ff94"}, warning: [2]string{"#e6ff57", "#e6ff57"},
|
||||
error_: [2]string{"#ff4b4b", "#ff4b4b"}, info: [2]string{"#30b3ff", "#30b3ff"},
|
||||
text: [2]string{"#203022", "#62ff94"}, background: [2]string{"#eef3ea", "#0a0e0a"},
|
||||
mdKeyword: [2]string{"#1cc24b", "#2eff6a"}, mdString: [2]string{"#1cc24b", "#62ff94"},
|
||||
mdComment: [2]string{"#5a7a5e", "#3a5a3e"},
|
||||
}),
|
||||
|
||||
"vercel": makeTheme(presetColors{
|
||||
primary: [2]string{"#0070F3", "#0070F3"}, secondary: [2]string{"#8E4EC6", "#8E4EC6"},
|
||||
success: [2]string{"#388E3C", "#46A758"}, warning: [2]string{"#FF9500", "#FFB224"},
|
||||
error_: [2]string{"#DC3545", "#E5484D"}, info: [2]string{"#0070F3", "#52A8FF"},
|
||||
text: [2]string{"#171717", "#EDEDED"}, background: [2]string{"#FFFFFF", "#000000"},
|
||||
mdKeyword: [2]string{"#0070F3", "#0070F3"}, mdString: [2]string{"#388E3C", "#46A758"},
|
||||
mdComment: [2]string{"#6B6B6B", "#666666"},
|
||||
}),
|
||||
|
||||
"zenburn": makeTheme(presetColors{
|
||||
primary: [2]string{"#5f7f8f", "#8cd0d3"}, secondary: [2]string{"#5f8f8f", "#93e0e3"},
|
||||
success: [2]string{"#5f8f5f", "#7f9f7f"}, warning: [2]string{"#8f8f5f", "#f0dfaf"},
|
||||
error_: [2]string{"#8f5f5f", "#cc9393"}, info: [2]string{"#8f7f5f", "#dfaf8f"},
|
||||
text: [2]string{"#3f3f3f", "#dcdccc"}, background: [2]string{"#ffffef", "#3f3f3f"},
|
||||
mdKeyword: [2]string{"#5f7f8f", "#8cd0d3"}, mdString: [2]string{"#5f8f5f", "#cc9393"},
|
||||
mdComment: [2]string{"#7f7f7f", "#7f9f7f"},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme registry (global)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var themeRegistry []ThemeEntry
|
||||
|
||||
// initThemeRegistry populates the registry from built-ins, user themes, and
|
||||
// project-local themes. Later sources override earlier ones with the same name:
|
||||
// 1. Built-in presets
|
||||
// 2. User themes (~/.config/kit/themes/)
|
||||
// 3. Project-local (.kit/themes/ in the working directory)
|
||||
func initThemeRegistry() {
|
||||
themeRegistry = nil
|
||||
|
||||
// 1. Built-in presets.
|
||||
for name, t := range builtinThemes() {
|
||||
themeRegistry = append(themeRegistry, ThemeEntry{
|
||||
Name: name,
|
||||
Source: "builtin",
|
||||
theme: t,
|
||||
loaded: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. User themes from ~/.config/kit/themes/
|
||||
scanThemesDir(userThemesDir())
|
||||
|
||||
// 3. Project-local themes from .kit/themes/
|
||||
scanThemesDir(projectThemesDir())
|
||||
|
||||
sortRegistry()
|
||||
}
|
||||
|
||||
// scanThemesDir adds all .yml/.yaml/.json theme files from dir to the registry.
|
||||
// Files override any existing entry with the same stem name.
|
||||
func scanThemesDir(dir string) {
|
||||
if dir == "" {
|
||||
return
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(entry.Name()))
|
||||
if ext != ".yml" && ext != ".yaml" && ext != ".json" {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
||||
removeFromRegistry(name)
|
||||
themeRegistry = append(themeRegistry, ThemeEntry{
|
||||
Name: name,
|
||||
Source: filepath.Join(dir, entry.Name()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sortRegistry() {
|
||||
sort.Slice(themeRegistry, func(i, j int) bool {
|
||||
return themeRegistry[i].Name < themeRegistry[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
func removeFromRegistry(name string) {
|
||||
for i := range themeRegistry {
|
||||
if themeRegistry[i].Name == name {
|
||||
themeRegistry = append(themeRegistry[:i], themeRegistry[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// userThemesDir returns ~/.config/kit/themes, creating it if needed.
|
||||
func userThemesDir() string {
|
||||
cfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
dir := filepath.Join(cfgDir, "kit", "themes")
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// projectThemesDir returns .kit/themes/ relative to the working directory.
|
||||
// Returns "" if the directory doesn't exist (does NOT create it).
|
||||
func projectThemesDir() string {
|
||||
dir := filepath.Join(".kit", "themes")
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return ""
|
||||
}
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return dir
|
||||
}
|
||||
return abs
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListThemes returns the names of all available themes (built-in + user).
|
||||
func ListThemes() []string {
|
||||
if themeRegistry == nil {
|
||||
initThemeRegistry()
|
||||
}
|
||||
names := make([]string, len(themeRegistry))
|
||||
for i := range themeRegistry {
|
||||
names[i] = themeRegistry[i].Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// LoadThemeByName looks up a theme by name, loads it if needed, and returns it.
|
||||
func LoadThemeByName(name string) (Theme, error) {
|
||||
if themeRegistry == nil {
|
||||
initThemeRegistry()
|
||||
}
|
||||
for i := range themeRegistry {
|
||||
if themeRegistry[i].Name == name {
|
||||
return themeRegistry[i].Theme()
|
||||
}
|
||||
}
|
||||
return Theme{}, fmt.Errorf("theme %q not found", name)
|
||||
}
|
||||
|
||||
// ApplyTheme loads a theme by name and sets it as the active global theme.
|
||||
func ApplyTheme(name string) error {
|
||||
t, err := LoadThemeByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SetTheme(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshThemeRegistry re-scans the themes directory. Call after the user
|
||||
// drops a new file into ~/.config/kit/themes/.
|
||||
func RefreshThemeRegistry() {
|
||||
initThemeRegistry()
|
||||
}
|
||||
|
||||
// RegisterThemeFromConfig adds a theme to the runtime registry from an
|
||||
// extension's ThemeColorConfig (string hex pairs). Replaces any existing
|
||||
// entry with the same name. The theme is immediately available via
|
||||
// ListThemes, LoadThemeByName, and ApplyTheme.
|
||||
func RegisterThemeFromConfig(name string, primary, secondary, success, warning, error_, info, text, muted, veryMuted, background, border, mutedBorder, system, tool, accent, highlight, mdHeading, mdLink, mdKeyword, mdString, mdNumber, mdComment [2]string) {
|
||||
if themeRegistry == nil {
|
||||
initThemeRegistry()
|
||||
}
|
||||
t := makeTheme(presetColors{
|
||||
primary: primary, secondary: secondary,
|
||||
success: success, warning: warning,
|
||||
error_: error_, info: info,
|
||||
text: text, muted: muted,
|
||||
veryMuted: veryMuted, background: background,
|
||||
border: border, mutedBorder: mutedBorder,
|
||||
system: system, tool: tool,
|
||||
accent: accent, highlight: highlight,
|
||||
mdHeading: mdHeading, mdLink: mdLink,
|
||||
mdKeyword: mdKeyword, mdString: mdString,
|
||||
mdNumber: mdNumber, mdComment: mdComment,
|
||||
})
|
||||
removeFromRegistry(name)
|
||||
themeRegistry = append(themeRegistry, ThemeEntry{
|
||||
Name: name,
|
||||
Source: "extension",
|
||||
theme: t,
|
||||
loaded: true,
|
||||
})
|
||||
sortRegistry()
|
||||
}
|
||||
|
||||
// ActiveThemeName returns the name of the currently active theme by comparing
|
||||
// against known entries. Returns "custom" if no match is found.
|
||||
func ActiveThemeName() string {
|
||||
if themeRegistry == nil {
|
||||
initThemeRegistry()
|
||||
}
|
||||
current := GetTheme()
|
||||
for _, e := range themeRegistry {
|
||||
if !e.loaded {
|
||||
continue
|
||||
}
|
||||
if e.theme.Primary == current.Primary &&
|
||||
e.theme.Secondary == current.Secondary &&
|
||||
e.theme.Error == current.Error &&
|
||||
e.theme.Text == current.Text {
|
||||
return e.Name
|
||||
}
|
||||
}
|
||||
return "custom"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// themeFileConfig mirrors config.Theme for unmarshaling theme files.
|
||||
// Uses the same adaptive color structure.
|
||||
type themeFileConfig struct {
|
||||
Primary adaptiveColorPair `json:"primary,omitzero" yaml:"primary,omitempty"`
|
||||
Secondary adaptiveColorPair `json:"secondary,omitzero" yaml:"secondary,omitempty"`
|
||||
Success adaptiveColorPair `json:"success,omitzero" yaml:"success,omitempty"`
|
||||
Warning adaptiveColorPair `json:"warning,omitzero" yaml:"warning,omitempty"`
|
||||
Error adaptiveColorPair `json:"error,omitzero" yaml:"error,omitempty"`
|
||||
Info adaptiveColorPair `json:"info,omitzero" yaml:"info,omitempty"`
|
||||
Text adaptiveColorPair `json:"text,omitzero" yaml:"text,omitempty"`
|
||||
Muted adaptiveColorPair `json:"muted,omitzero" yaml:"muted,omitempty"`
|
||||
VeryMuted adaptiveColorPair `json:"very-muted,omitzero" yaml:"very-muted,omitempty"`
|
||||
Background adaptiveColorPair `json:"background,omitzero" yaml:"background,omitempty"`
|
||||
Border adaptiveColorPair `json:"border,omitzero" yaml:"border,omitempty"`
|
||||
MutedBorder adaptiveColorPair `json:"muted-border,omitzero" yaml:"muted-border,omitempty"`
|
||||
System adaptiveColorPair `json:"system,omitzero" yaml:"system,omitempty"`
|
||||
Tool adaptiveColorPair `json:"tool,omitzero" yaml:"tool,omitempty"`
|
||||
Accent adaptiveColorPair `json:"accent,omitzero" yaml:"accent,omitempty"`
|
||||
Highlight adaptiveColorPair `json:"highlight,omitzero" yaml:"highlight,omitempty"`
|
||||
|
||||
DiffInsertBg adaptiveColorPair `json:"diff-insert-bg,omitzero" yaml:"diff-insert-bg,omitempty"`
|
||||
DiffDeleteBg adaptiveColorPair `json:"diff-delete-bg,omitzero" yaml:"diff-delete-bg,omitempty"`
|
||||
DiffEqualBg adaptiveColorPair `json:"diff-equal-bg,omitzero" yaml:"diff-equal-bg,omitempty"`
|
||||
DiffMissingBg adaptiveColorPair `json:"diff-missing-bg,omitzero" yaml:"diff-missing-bg,omitempty"`
|
||||
CodeBg adaptiveColorPair `json:"code-bg,omitzero" yaml:"code-bg,omitempty"`
|
||||
GutterBg adaptiveColorPair `json:"gutter-bg,omitzero" yaml:"gutter-bg,omitempty"`
|
||||
WriteBg adaptiveColorPair `json:"write-bg,omitzero" yaml:"write-bg,omitempty"`
|
||||
|
||||
Markdown struct {
|
||||
Text adaptiveColorPair `json:"text,omitzero" yaml:"text,omitempty"`
|
||||
Muted adaptiveColorPair `json:"muted,omitzero" yaml:"muted,omitempty"`
|
||||
Heading adaptiveColorPair `json:"heading,omitzero" yaml:"heading,omitempty"`
|
||||
Emph adaptiveColorPair `json:"emph,omitzero" yaml:"emph,omitempty"`
|
||||
Strong adaptiveColorPair `json:"strong,omitzero" yaml:"strong,omitempty"`
|
||||
Link adaptiveColorPair `json:"link,omitzero" yaml:"link,omitempty"`
|
||||
Code adaptiveColorPair `json:"code,omitzero" yaml:"code,omitempty"`
|
||||
Error adaptiveColorPair `json:"error,omitzero" yaml:"error,omitempty"`
|
||||
Keyword adaptiveColorPair `json:"keyword,omitzero" yaml:"keyword,omitempty"`
|
||||
String adaptiveColorPair `json:"string,omitzero" yaml:"string,omitempty"`
|
||||
Number adaptiveColorPair `json:"number,omitzero" yaml:"number,omitempty"`
|
||||
Comment adaptiveColorPair `json:"comment,omitzero" yaml:"comment,omitempty"`
|
||||
} `json:"markdown,omitzero" yaml:"markdown,omitempty"`
|
||||
}
|
||||
|
||||
type adaptiveColorPair struct {
|
||||
Light string `json:"light,omitempty" yaml:"light,omitempty"`
|
||||
Dark string `json:"dark,omitempty" yaml:"dark,omitempty"`
|
||||
}
|
||||
|
||||
// resolve converts an adaptiveColorPair to a resolved color.Color,
|
||||
// falling back to fallback when both Light and Dark are empty.
|
||||
func (a adaptiveColorPair) resolve(fallback color.Color) color.Color {
|
||||
if a.Light == "" && a.Dark == "" {
|
||||
return fallback
|
||||
}
|
||||
return AdaptiveColor(a.Light, a.Dark)
|
||||
}
|
||||
|
||||
func loadThemeFile(path string) (Theme, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Theme{}, err
|
||||
}
|
||||
|
||||
var cfg themeFileConfig
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".json":
|
||||
err = json.Unmarshal(data, &cfg)
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(data, &cfg)
|
||||
default:
|
||||
return Theme{}, fmt.Errorf("unsupported theme file format: %s", ext)
|
||||
}
|
||||
if err != nil {
|
||||
return Theme{}, err
|
||||
}
|
||||
|
||||
return fileConfigToTheme(cfg), nil
|
||||
}
|
||||
|
||||
func fileConfigToTheme(cfg themeFileConfig) Theme {
|
||||
def := DefaultTheme()
|
||||
return Theme{
|
||||
Primary: cfg.Primary.resolve(def.Primary),
|
||||
Secondary: cfg.Secondary.resolve(def.Secondary),
|
||||
Success: cfg.Success.resolve(def.Success),
|
||||
Warning: cfg.Warning.resolve(def.Warning),
|
||||
Error: cfg.Error.resolve(def.Error),
|
||||
Info: cfg.Info.resolve(def.Info),
|
||||
Text: cfg.Text.resolve(def.Text),
|
||||
Muted: cfg.Muted.resolve(def.Muted),
|
||||
VeryMuted: cfg.VeryMuted.resolve(def.VeryMuted),
|
||||
Background: cfg.Background.resolve(def.Background),
|
||||
Border: cfg.Border.resolve(def.Border),
|
||||
MutedBorder: cfg.MutedBorder.resolve(def.MutedBorder),
|
||||
System: cfg.System.resolve(def.System),
|
||||
Tool: cfg.Tool.resolve(def.Tool),
|
||||
Accent: cfg.Accent.resolve(def.Accent),
|
||||
Highlight: cfg.Highlight.resolve(def.Highlight),
|
||||
|
||||
DiffInsertBg: cfg.DiffInsertBg.resolve(def.DiffInsertBg),
|
||||
DiffDeleteBg: cfg.DiffDeleteBg.resolve(def.DiffDeleteBg),
|
||||
DiffEqualBg: cfg.DiffEqualBg.resolve(def.DiffEqualBg),
|
||||
DiffMissingBg: cfg.DiffMissingBg.resolve(def.DiffMissingBg),
|
||||
CodeBg: cfg.CodeBg.resolve(def.CodeBg),
|
||||
GutterBg: cfg.GutterBg.resolve(def.GutterBg),
|
||||
WriteBg: cfg.WriteBg.resolve(def.WriteBg),
|
||||
|
||||
Markdown: MarkdownThemeColors{
|
||||
Text: cfg.Markdown.Text.resolve(def.Markdown.Text),
|
||||
Muted: cfg.Markdown.Muted.resolve(def.Markdown.Muted),
|
||||
Heading: cfg.Markdown.Heading.resolve(def.Markdown.Heading),
|
||||
Emph: cfg.Markdown.Emph.resolve(def.Markdown.Emph),
|
||||
Strong: cfg.Markdown.Strong.resolve(def.Markdown.Strong),
|
||||
Link: cfg.Markdown.Link.resolve(def.Markdown.Link),
|
||||
Code: cfg.Markdown.Code.resolve(def.Markdown.Code),
|
||||
Error: cfg.Markdown.Error.resolve(def.Markdown.Error),
|
||||
Keyword: cfg.Markdown.Keyword.resolve(def.Markdown.Keyword),
|
||||
String: cfg.Markdown.String.resolve(def.Markdown.String),
|
||||
Number: cfg.Markdown.Number.resolve(def.Markdown.Number),
|
||||
Comment: cfg.Markdown.Comment.resolve(def.Markdown.Comment),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,12 @@ func NewToolApprovalInput(toolName, toolArgs string, width int) *ToolApprovalInp
|
||||
ta.SetHeight(4) // Default to 3 lines like huh
|
||||
ta.Focus()
|
||||
|
||||
// Style the textarea to match huh theme
|
||||
// Style the textarea using theme colors.
|
||||
theme := GetTheme()
|
||||
styles := ta.Styles()
|
||||
styles.Focused.Base = lipgloss.NewStyle()
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(theme.VeryMuted)
|
||||
styles.Focused.Text = lipgloss.NewStyle().Foreground(theme.Text)
|
||||
styles.Focused.Prompt = lipgloss.NewStyle()
|
||||
styles.Focused.CursorLine = lipgloss.NewStyle()
|
||||
ta.SetStyles(styles)
|
||||
@@ -87,9 +88,11 @@ func (t *ToolApprovalInput) View() tea.View {
|
||||
}
|
||||
containerStyle := lipgloss.NewStyle()
|
||||
|
||||
theme := GetTheme()
|
||||
|
||||
// PaddingLeft(3) aligns with message content: border(1) + paddingLeft(2).
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")).
|
||||
Foreground(theme.Text).
|
||||
MarginBottom(1).
|
||||
PaddingLeft(3)
|
||||
|
||||
@@ -100,19 +103,19 @@ func (t *ToolApprovalInput) View() tea.View {
|
||||
BorderRight(false).
|
||||
BorderTop(false).
|
||||
BorderBottom(false).
|
||||
BorderForeground(lipgloss.Color("39")).
|
||||
BorderForeground(theme.Primary).
|
||||
PaddingLeft(2). // match message block paddingLeft
|
||||
Width(t.width - 1) // full width minus left border
|
||||
|
||||
// Style for the currently selected/highlighted option
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("42")). // Bright green
|
||||
Foreground(theme.Success).
|
||||
Bold(true).
|
||||
Underline(true)
|
||||
|
||||
// Style for the unselected/unhighlighted option
|
||||
unselectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")) // Dark gray
|
||||
Foreground(theme.VeryMuted)
|
||||
|
||||
// Build the view
|
||||
var view strings.Builder
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Guide for creating Kit extensions. Use when the user asks to build,
|
||||
|
||||
# Kit Extensions Development Guide
|
||||
|
||||
Kit extensions are single-file Go programs interpreted at runtime by Yaegi. They hook into Kit's lifecycle, register custom tools and slash commands, display widgets, intercept editor input, render tool output, and more.
|
||||
Kit extensions are single-file Go programs interpreted at runtime by Yaegi. They hook into Kit's lifecycle, register custom tools and slash commands, display widgets, intercept editor input, render tool output, register and switch color themes, and more.
|
||||
|
||||
Extensions can be distributed via git repositories using `kit install`. Repos can contain single extensions or collections of multiple extensions.
|
||||
|
||||
@@ -416,6 +416,17 @@ result := ctx.PromptInput(ext.PromptInputConfig{
|
||||
if !result.Cancelled {
|
||||
// result.Value string
|
||||
}
|
||||
|
||||
// Multi-select (toggle with spacebar, confirm with enter)
|
||||
result := ctx.PromptMultiSelect(ext.PromptMultiSelectConfig{
|
||||
Message: "Select extensions to install:",
|
||||
Options: []string{"git", "todo", "weather"},
|
||||
DefaultSelected: []int{0, 1, 2}, // pre-selected indices; nil = all selected
|
||||
})
|
||||
if !result.Cancelled {
|
||||
// result.Values []string — selected option texts
|
||||
// result.Indices []int — selected option indices
|
||||
}
|
||||
```
|
||||
|
||||
### Overlay Dialogs
|
||||
@@ -531,6 +542,64 @@ ctx.SuspendTUI(func() {
|
||||
})
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
Register, switch, and list color themes at runtime:
|
||||
|
||||
```go
|
||||
// Register a custom theme (empty fields inherit from default).
|
||||
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
|
||||
Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
|
||||
Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
|
||||
Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
|
||||
Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
|
||||
Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
|
||||
MdKeyword: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
MdString: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
MdComment: ext.ThemeColor{Light: "#888888", Dark: "#555555"},
|
||||
})
|
||||
|
||||
// Switch to a theme by name (built-in, file-based, or extension-registered).
|
||||
err := ctx.SetTheme("neon")
|
||||
|
||||
// List all available theme names.
|
||||
names := ctx.ListThemes() // []string
|
||||
```
|
||||
|
||||
**ThemeColorConfig fields:**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `Primary` | Main brand/accent color |
|
||||
| `Secondary` | Secondary accent |
|
||||
| `Success` | Success states |
|
||||
| `Warning` | Warning states |
|
||||
| `Error` | Error/critical states |
|
||||
| `Info` | Informational states |
|
||||
| `Text` | Primary text |
|
||||
| `Muted` | Dimmed text |
|
||||
| `VeryMuted` | Very dimmed text |
|
||||
| `Background` | Base background |
|
||||
| `Border` | Panel borders |
|
||||
| `MutedBorder` | Subtle dividers |
|
||||
| `System` | System messages |
|
||||
| `Tool` | Tool-related elements |
|
||||
| `Accent` | Secondary highlight |
|
||||
| `Highlight` | Highlighted regions |
|
||||
| `MdHeading` | Markdown headings |
|
||||
| `MdLink` | Markdown links |
|
||||
| `MdKeyword` | Syntax: keywords |
|
||||
| `MdString` | Syntax: strings |
|
||||
| `MdNumber` | Syntax: numbers |
|
||||
| `MdComment` | Syntax: comments |
|
||||
|
||||
Each field is an `ext.ThemeColor` with `Light` and `Dark` hex strings. Kit ships 22 built-in themes: `kitt`, `catppuccin`, `dracula`, `tokyonight`, `nord`, `gruvbox`, `monokai`, `solarized`, `github`, `one-dark`, `rose-pine`, `ayu`, `material`, `everforest`, `kanagawa`, `amoled`, `synthwave`, `vesper`, `flexoki`, `matrix`, `vercel`, `zenburn`.
|
||||
|
||||
Users can also drop `.yml`/`.yaml`/`.json` theme files in `~/.config/kit/themes/` (global) or `.kit/themes/` (project-local). Extension-registered themes take highest precedence.
|
||||
|
||||
### Application Control
|
||||
|
||||
```go
|
||||
@@ -738,20 +807,164 @@ api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern: Custom Theme with Slash Command
|
||||
|
||||
Register a theme and provide a slash command shortcut to activate it:
|
||||
|
||||
```go
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
|
||||
Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
|
||||
Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
|
||||
Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
|
||||
Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
|
||||
Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
|
||||
})
|
||||
})
|
||||
|
||||
api.RegisterCommand(ext.CommandDef{
|
||||
Name: "neon",
|
||||
Description: "Switch to the neon cyberpunk theme",
|
||||
Execute: func(args string, ctx ext.Context) (string, error) {
|
||||
if err := ctx.SetTheme("neon"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Neon theme activated!", nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern: Spawning Kit as a Sub-Agent
|
||||
|
||||
Extensions can spawn Kit as a subprocess for delegation:
|
||||
Use `ctx.SpawnSubagent` to spawn an isolated child Kit instance. The subagent runs as a subprocess with `--json --no-extensions` flags, ensuring isolation.
|
||||
|
||||
**Blocking mode** — waits for completion:
|
||||
|
||||
```go
|
||||
_, result, err := ctx.SpawnSubagent(ext.SubagentConfig{
|
||||
Prompt: "Analyze the test files and summarize coverage",
|
||||
Model: "anthropic/claude-haiku-3-5-20241022", // empty = parent's model
|
||||
SystemPrompt: "You are a test analysis expert.",
|
||||
Timeout: 2 * time.Minute, // 0 = 5 minute default
|
||||
Blocking: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.PrintError("spawn failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
if result.Error != nil {
|
||||
ctx.PrintError("subagent failed: " + result.Error.Error())
|
||||
return
|
||||
}
|
||||
ctx.PrintInfo("Result:\n" + result.Response)
|
||||
// result.Elapsed, result.ExitCode, result.SessionID
|
||||
// result.Usage.InputTokens, result.Usage.OutputTokens (if available)
|
||||
```
|
||||
|
||||
**Background mode** — returns immediately with a handle:
|
||||
|
||||
```go
|
||||
handle, _, err := ctx.SpawnSubagent(ext.SubagentConfig{
|
||||
Prompt: "Write unit tests for UserService",
|
||||
OnOutput: func(chunk string) {
|
||||
// Live stderr streaming (progress, tool calls, etc.)
|
||||
},
|
||||
OnEvent: func(event ext.SubagentEvent) {
|
||||
// Real-time events: "text", "reasoning", "tool_call",
|
||||
// "tool_result", "tool_execution_start", "tool_execution_end",
|
||||
// "turn_start", "turn_end"
|
||||
// event.Type, event.Content, event.ToolName, event.ToolArgs, etc.
|
||||
},
|
||||
OnComplete: func(result ext.SubagentResult) {
|
||||
ctx.SendMessage("Subagent finished:\n" + result.Response)
|
||||
},
|
||||
})
|
||||
// handle.Kill() — terminate the subagent
|
||||
// handle.Wait() — block until completion, returns SubagentResult
|
||||
// <-handle.Done() — channel that closes on completion
|
||||
```
|
||||
|
||||
**SubagentConfig fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `Prompt` | string | Task instruction (required) |
|
||||
| `Model` | string | Override model ("provider/model"), empty = parent's |
|
||||
| `SystemPrompt` | string | Custom system prompt, empty = default |
|
||||
| `Timeout` | time.Duration | Execution limit, 0 = 5 minutes |
|
||||
| `Blocking` | bool | Wait for completion vs return handle |
|
||||
| `NoSession` | bool | Don't persist subagent session file |
|
||||
| `ParentSessionID` | string | Link to parent session (optional) |
|
||||
| `OnOutput` | func(string) | Stderr streaming callback |
|
||||
| `OnEvent` | func(SubagentEvent) | Real-time event callback |
|
||||
| `OnComplete` | func(SubagentResult) | Completion callback |
|
||||
|
||||
**SubagentResult fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `Response` | string | Final text response |
|
||||
| `Error` | error | Non-nil on failure |
|
||||
| `ExitCode` | int | Process exit code (0 = success) |
|
||||
| `Elapsed` | time.Duration | Total execution time |
|
||||
| `Usage` | *SubagentUsage | Token usage (InputTokens, OutputTokens) |
|
||||
| `SessionID` | string | Subagent's session ID (if persisted) |
|
||||
|
||||
You can also spawn Kit as a raw subprocess for simpler cases:
|
||||
|
||||
```bash
|
||||
kit --quiet --no-session --no-extensions --system-prompt "You are a reviewer" --model anthropic/claude-sonnet-4-20250514 "Review this code"
|
||||
```
|
||||
|
||||
Key flags: `--quiet` (stdout only, no TUI), `--no-session` (ephemeral), `--no-extensions` (prevent recursion), `--system-prompt` (string or file path).
|
||||
|
||||
---
|
||||
|
||||
## Testing Extensions
|
||||
|
||||
Kit provides a testing package to help you write unit tests for your extensions:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
func TestMyExtension(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Test event handlers
|
||||
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify behavior with assertions
|
||||
test.AssertPrinted(t, harness, "session started")
|
||||
test.AssertWidgetSet(t, harness, "my-widget")
|
||||
|
||||
// Test tool blocking
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{ToolName: "dangerous"})
|
||||
test.AssertBlocked(t, result, "not allowed")
|
||||
}
|
||||
```
|
||||
|
||||
**Key testing patterns:**
|
||||
- Load extensions with `LoadFile()` or `LoadString()` for inline code
|
||||
- Emit events with `Emit()` to trigger handlers
|
||||
- Verify with 25+ assertion helpers: `AssertWidgetSet()`, `AssertToolRegistered()`, `AssertPrintInfo()`, etc.
|
||||
- Mock prompts by setting results on `harness.Context().SetPromptSelectResult()`
|
||||
- Test multiple scenarios per extension with isolated harness instances
|
||||
|
||||
See `examples/extensions/tool-logger_test.go` for a complete example with 14 test cases.
|
||||
|
||||
### CLI Testing Commands
|
||||
|
||||
```bash
|
||||
# Validate syntax of all discovered extensions
|
||||
kit extensions validate
|
||||
@@ -1003,4 +1216,6 @@ func applyMode(ctx ext.Context, active bool, tools []string) {
|
||||
- [`internal/extensions/runner.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/runner.go) — Event dispatch and state management
|
||||
- [`internal/extensions/loader.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/loader.go) — Yaegi interpreter setup
|
||||
- [`internal/extensions/symbols.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/symbols.go) — All types exported to extensions
|
||||
- [`internal/extensions/test/`](https://github.com/mark3labs/kit/tree/main/internal/extensions/test) — Testing package with harness, mocks, and assertions
|
||||
- [`examples/extensions/tool-logger_test.go`](https://github.com/mark3labs/kit/blob/main/examples/extensions/tool-logger_test.go) — Complete test example
|
||||
- [`examples/extensions/`](https://github.com/mark3labs/kit/tree/main/examples/extensions) — 25+ working example extensions
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
out/
|
||||
dist/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,3 @@
|
||||
// Bootstraps the Tome documentation shell.
|
||||
// Configure your site in tome.config.js instead.
|
||||
import "@tomehq/theme/entry";
|
||||
+918
@@ -0,0 +1,918 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "www",
|
||||
"devDependencies": {
|
||||
"@tomehq/cli": "^0.5.0",
|
||||
"@tomehq/theme": "^0.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.7.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA=="],
|
||||
|
||||
"@apidevtools/openapi-schemas": ["@apidevtools/openapi-schemas@2.1.0", "", {}, "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="],
|
||||
|
||||
"@apidevtools/swagger-methods": ["@apidevtools/swagger-methods@3.0.2", "", {}, "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="],
|
||||
|
||||
"@apidevtools/swagger-parser": ["@apidevtools/swagger-parser@10.1.1", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.2", "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", "@jsdevtools/ono": "^7.1.3", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "call-me-maybe": "^1.0.2" }, "peerDependencies": { "openapi-types": ">=7" } }, "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA=="],
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.3", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7" } }, "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA=="],
|
||||
|
||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
|
||||
|
||||
"@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
|
||||
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="],
|
||||
|
||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.1", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "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.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="],
|
||||
|
||||
"@shikijs/twoslash": ["@shikijs/twoslash@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/types": "1.29.2", "twoslash": "^0.2.12" } }, "sha512-2S04ppAEa477tiaLfGEn1QJWbZUmbk8UoPbAEw4PifsrxkBXtAtOflIZJNtuCwz8ptc/TPxy7CO7gW4Uoi6o/g=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@tomehq/cli": ["@tomehq/cli@0.5.0", "", { "dependencies": { "@tomehq/core": "0.5.0", "@vitejs/plugin-react": "^4.0.0", "commander": "^12.0.0", "picocolors": "^1.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "vite": "^6.0.0" }, "bin": { "tome": "dist/cli.js" } }, "sha512-fZkEsE5RregWDOBFFMkMPodu30hDp/wK3xIsuknUGABcwGHWu3A+QmY0vvf9HuoGHPjz7mc7bM0dmJB1CO7G/g=="],
|
||||
|
||||
"@tomehq/components": ["@tomehq/components@0.5.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-JKEpbGmmas4vms5jVn3t/xphjM+7xaZLMbFhVvze25fOiwNOjBRJ8hOr68aO5vAVDu1oXSC2GOV8LogY4I3sfA=="],
|
||||
|
||||
"@tomehq/core": ["@tomehq/core@0.5.0", "", { "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", "@mdx-js/rollup": "^3.0.0", "@modelcontextprotocol/sdk": "^1.12.0", "@shikijs/twoslash": "^1.22.0", "estree-util-visit": "^2.0.0", "glob": "^11.0.0", "gray-matter": "^4.0.3", "hast-util-to-html": "^9.0.0", "isomorphic-dompurify": "^3.3.0", "openapi-types": "^12.1.3", "rehype-autolink-headings": "^7.1.0", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "shiki": "^1.22.0", "unified": "^11.0.0", "unist-util-visit": "^5.1.0", "yaml": "^2.8.2", "zod": "^3.23.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0" } }, "sha512-+TRZBnlUzDV89SyrSmrVQFfZDZ76yj6jHaShU/jxsPfXc1o8LqvBbFbRpWYJf8/yCDH3FUee1ZnZHtcq3K3R1w=="],
|
||||
|
||||
"@tomehq/theme": ["@tomehq/theme@0.5.0", "", { "dependencies": { "@tomehq/components": "0.5.0", "@tomehq/core": "0.5.0" }, "peerDependencies": { "@docsearch/css": "^3.0.0", "@docsearch/react": "^3.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@docsearch/css", "@docsearch/react"] }, "sha512-YRGJj4Igc2reoP/H6kASrY4wtaWbt3xiwrS5BBpBn9+EO2G0cZb1uLyYxf4lx7yEBy5NXa6jKwrfC6+zWvWQ6A=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="],
|
||||
|
||||
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"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@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
|
||||
|
||||
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
|
||||
|
||||
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.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-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "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-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="],
|
||||
|
||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||
|
||||
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||
|
||||
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isomorphic-dompurify": ["isomorphic-dompurify@3.5.1", "", { "dependencies": { "dompurify": "^3.3.3", "jsdom": "^29.0.0" } }, "sha512-LlUCZikq/W6CmSq1UWcFLgS4nhkNBwimmVyNQtdByDffT0yVjKYef9lincZ7/ohBlknPkX1C97BBmjeNuFFR4Q=="],
|
||||
|
||||
"jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"jsdom": ["jsdom@29.0.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@asamuzakjp/dom-selector": "^7.0.2", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.3", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
|
||||
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
|
||||
|
||||
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
|
||||
|
||||
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
|
||||
|
||||
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
|
||||
|
||||
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||
|
||||
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
|
||||
|
||||
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
|
||||
|
||||
"regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="],
|
||||
|
||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||
|
||||
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
|
||||
|
||||
"rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="],
|
||||
|
||||
"rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
|
||||
|
||||
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tldts": ["tldts@7.0.26", "", { "dependencies": { "tldts-core": "^7.0.26" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.0.26", "", {}, "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
|
||||
|
||||
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"twoslash": ["twoslash@0.2.12", "", { "dependencies": { "@typescript/vfs": "^1.6.0", "twoslash-protocol": "0.2.12" }, "peerDependencies": { "typescript": "*" } }, "sha512-tEHPASMqi7kqwfJbkk7hc/4EhlrKCSLcur+TcvYki3vhIfaRMXnXjaYFgXpoZRbT6GdprD4tGuVBEmTpUgLBsw=="],
|
||||
|
||||
"twoslash-protocol": ["twoslash-protocol@0.2.12", "", {}, "sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg=="],
|
||||
|
||||
"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.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||
|
||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"@apidevtools/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kit Documentation</title>
|
||||
<link rel="icon" href="/logo.jpg" type="image/jpeg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Source+Code+Pro:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="./styles/custom.css" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { -webkit-font-smoothing: antialiased; }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="tome-root"></div>
|
||||
<script type="module" src="./.tome/entry.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "www",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tome dev",
|
||||
"build": "tome build",
|
||||
"deploy": "tome deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tomehq/cli": "^0.5.0",
|
||||
"@tomehq/theme": "^0.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: JSON Output
|
||||
description: Machine-readable JSON output for scripting and automation.
|
||||
---
|
||||
|
||||
# JSON Output
|
||||
|
||||
Use the `--json` flag to get structured output for scripting and automation:
|
||||
|
||||
```bash
|
||||
kit "Explain main.go" --json --quiet --no-session
|
||||
```
|
||||
|
||||
## Response format
|
||||
|
||||
```json
|
||||
{
|
||||
"response": "Final assistant response text",
|
||||
"model": "anthropic/claude-haiku-3-5-20241022",
|
||||
"stop_reason": "end_turn",
|
||||
"session_id": "a1b2c3d4e5f6",
|
||||
"usage": {
|
||||
"input_tokens": 1024,
|
||||
"output_tokens": 512,
|
||||
"total_tokens": 1536,
|
||||
"cache_read_tokens": 0,
|
||||
"cache_creation_tokens": 0
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"parts": [
|
||||
{"type": "text", "data": "..."},
|
||||
{"type": "tool_call", "data": {"name": "...", "args": "..."}},
|
||||
{"type": "tool_result", "data": {"name": "...", "result": "..."}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Fields
|
||||
|
||||
### Top-level
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `response` | string | The final assistant response text |
|
||||
| `model` | string | The model that was used |
|
||||
| `stop_reason` | string | Why the model stopped (e.g., `end_turn`) |
|
||||
| `session_id` | string | Session identifier (omitted in `--no-session` mode) |
|
||||
| `usage` | object | Token usage statistics |
|
||||
| `messages` | array | Full conversation history |
|
||||
|
||||
### Usage
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `input_tokens` | int | Tokens sent to the model |
|
||||
| `output_tokens` | int | Tokens generated by the model |
|
||||
| `total_tokens` | int | Sum of input and output tokens |
|
||||
| `cache_read_tokens` | int | Tokens read from prompt cache |
|
||||
| `cache_creation_tokens` | int | Tokens written to prompt cache |
|
||||
|
||||
### Message parts
|
||||
|
||||
Each message contains a `parts` array with typed entries:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `text` | Assistant text content |
|
||||
| `tool_call` | Tool invocation with name and args |
|
||||
| `tool_result` | Tool execution result |
|
||||
| `reasoning` | Extended thinking content |
|
||||
| `finish` | End-of-turn marker |
|
||||
|
||||
## Parsing in scripts
|
||||
|
||||
### bash + jq
|
||||
|
||||
```bash
|
||||
result=$(kit "Count files" --json --quiet --no-session)
|
||||
response=$(echo "$result" | jq -r '.response')
|
||||
tokens=$(echo "$result" | jq '.usage.total_tokens')
|
||||
```
|
||||
|
||||
### Go SDK
|
||||
|
||||
For Go programs, use the SDK's `PromptResult` method instead of parsing JSON:
|
||||
|
||||
```go
|
||||
result, err := host.PromptResult(ctx, "Count files")
|
||||
fmt.Println(result.Response)
|
||||
fmt.Println(result.Usage.TotalTokens)
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Subagents
|
||||
description: Multi-agent orchestration with Kit subagents.
|
||||
---
|
||||
|
||||
# Subagents
|
||||
|
||||
Kit supports multi-agent orchestration through both subprocess spawning and in-process subagents.
|
||||
|
||||
## Subprocess pattern
|
||||
|
||||
Spawn Kit as a subprocess for isolated agent execution:
|
||||
|
||||
```bash
|
||||
kit "Analyze codebase" \
|
||||
--json \
|
||||
--no-session \
|
||||
--no-extensions \
|
||||
--quiet \
|
||||
--model anthropic/claude-haiku-3-5-20241022
|
||||
```
|
||||
|
||||
Key flags for subprocess usage:
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|---------|
|
||||
| `--quiet` | Stdout only, no TUI |
|
||||
| `--no-session` | Ephemeral, no persistence |
|
||||
| `--no-extensions` | Prevent recursive extension loading |
|
||||
| `--json` | Machine-readable output |
|
||||
| `--system-prompt` | Custom system prompt (string or file path) |
|
||||
|
||||
Positional arguments are the prompt. `@file` arguments attach file content as context.
|
||||
|
||||
## Built-in spawn_subagent tool
|
||||
|
||||
Kit includes a built-in `spawn_subagent` tool that the LLM can use to delegate tasks to independent child agents:
|
||||
|
||||
```
|
||||
spawn_subagent(
|
||||
task: "Analyze the test files and summarize coverage",
|
||||
model: "anthropic/claude-haiku-3-5-20241022", // optional
|
||||
system_prompt: "You are a test analysis expert.", // optional
|
||||
timeout_seconds: 300 // optional, max 1800
|
||||
)
|
||||
```
|
||||
|
||||
Subagents run as separate in-process Kit instances with full tool access (except spawning further subagents, to prevent infinite recursion). They can run in parallel.
|
||||
|
||||
## Extension subagents
|
||||
|
||||
Extensions can spawn subagents programmatically:
|
||||
|
||||
```go
|
||||
result := ctx.SpawnSubagent(ext.SubagentConfig{
|
||||
Task: "Review this code for security issues",
|
||||
Model: "anthropic/claude-sonnet-4-5-20250929",
|
||||
SystemPrompt: "You are a security auditor.",
|
||||
})
|
||||
```
|
||||
|
||||
## Go SDK subagents
|
||||
|
||||
The SDK provides in-process subagent spawning:
|
||||
|
||||
```go
|
||||
result, err := host.Subagent(ctx, kit.SubagentConfig{
|
||||
Task: "Summarize the changes in this PR",
|
||||
Model: "anthropic/claude-haiku-3-5-20241022",
|
||||
SystemPrompt: "You are a code reviewer.",
|
||||
Timeout: 5 * time.Minute,
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Testing with tmux
|
||||
description: Test Kit's TUI non-interactively using tmux.
|
||||
---
|
||||
|
||||
# Testing with tmux
|
||||
|
||||
Kit's interactive TUI can be tested non-interactively using tmux. This is useful for automated testing, CI pipelines, and extension development.
|
||||
|
||||
## Basic pattern
|
||||
|
||||
```bash
|
||||
# Start Kit in a detached tmux session
|
||||
tmux new-session -d -s kittest -x 120 -y 40 \
|
||||
"output/kit -e ext.go --no-session 2>kit_stderr.log"
|
||||
|
||||
# Wait for startup
|
||||
sleep 3
|
||||
|
||||
# Capture the current screen
|
||||
tmux capture-pane -t kittest -p
|
||||
|
||||
# Send input
|
||||
tmux send-keys -t kittest '/command' Enter
|
||||
|
||||
# Wait for response
|
||||
sleep 2
|
||||
|
||||
# Capture updated screen
|
||||
tmux capture-pane -t kittest -p
|
||||
|
||||
# Cleanup
|
||||
tmux kill-session -t kittest
|
||||
```
|
||||
|
||||
## Testing extensions
|
||||
|
||||
When testing extensions, the pattern is:
|
||||
|
||||
1. Build Kit with your changes
|
||||
2. Start Kit in tmux with the extension loaded
|
||||
3. Send slash commands or prompts
|
||||
4. Capture and verify the screen output
|
||||
5. Check stderr logs for errors
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
go build -o output/kit ./cmd/kit
|
||||
|
||||
# Start with extension
|
||||
tmux new-session -d -s kittest -x 120 -y 40 \
|
||||
"output/kit -e examples/extensions/widget-status.go --no-session 2>kit_stderr.log"
|
||||
|
||||
sleep 3
|
||||
|
||||
# Verify widget appears in screen
|
||||
tmux capture-pane -t kittest -p | grep "Status"
|
||||
|
||||
# Send a slash command
|
||||
tmux send-keys -t kittest '/stats' Enter
|
||||
sleep 1
|
||||
tmux capture-pane -t kittest -p
|
||||
|
||||
# Cleanup
|
||||
tmux kill-session -t kittest
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `-x` and `-y` to set consistent terminal dimensions
|
||||
- Redirect stderr to a log file (`2>kit.log`) for debugging
|
||||
- Use `--no-session` to avoid creating session files during tests
|
||||
- Add sufficient `sleep` between commands for the TUI to render
|
||||
- Use `grep` on captured pane output to verify specific content
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Commands
|
||||
description: Complete reference for all Kit CLI subcommands.
|
||||
---
|
||||
|
||||
# Commands
|
||||
|
||||
## Authentication
|
||||
|
||||
For OAuth-enabled providers like Anthropic.
|
||||
|
||||
```bash
|
||||
kit auth login [provider] # Start OAuth flow (e.g., anthropic)
|
||||
kit auth logout [provider] # Remove credentials for provider
|
||||
kit auth status # Check authentication status
|
||||
```
|
||||
|
||||
## Model database
|
||||
|
||||
Manage the local model database that maps provider names to API configurations.
|
||||
|
||||
```bash
|
||||
kit models [provider] # List available models (optionally filter by provider)
|
||||
kit models --all # Show all providers (not just Fantasy-compatible)
|
||||
kit update-models [source] # Update model database
|
||||
```
|
||||
|
||||
The `update-models` command accepts an optional source argument:
|
||||
- *(none)* — update from [models.dev](https://models.dev)
|
||||
- A URL — fetch from a custom endpoint
|
||||
- A file path — load from a local file
|
||||
- `embedded` — reset to the bundled database
|
||||
|
||||
## Extension management
|
||||
|
||||
```bash
|
||||
kit extensions list # List discovered extensions
|
||||
kit extensions validate # Validate extension files
|
||||
kit extensions init # Generate example extension template
|
||||
```
|
||||
|
||||
### Installing extensions from git
|
||||
|
||||
```bash
|
||||
kit install <git-url> # Install extensions from git repositories
|
||||
kit install -l <git-url> # Install to project-local .kit/git/ directory
|
||||
kit install -u <git-url> # Update an already-installed package
|
||||
kit install --uninstall <pkg> # Remove an installed package
|
||||
kit install --all # Install all extensions without prompting
|
||||
```
|
||||
|
||||
## Skills
|
||||
|
||||
```bash
|
||||
kit skill # Install the Kit extensions skill via skills.sh
|
||||
```
|
||||
|
||||
## Interactive slash commands
|
||||
|
||||
These commands are available inside the Kit TUI during an interactive session:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/help` | Show available commands |
|
||||
| `/tools` | List available MCP tools |
|
||||
| `/servers` | Show connected MCP servers |
|
||||
| `/model [name]` | Switch model or open model selector |
|
||||
| `/theme [name]` | Switch color theme or list available themes |
|
||||
| `/thinking [level]` | Set thinking level (off, minimal, low, medium, high) |
|
||||
| `/compact [focus]` | Summarize older messages to free context |
|
||||
| `/clear` | Clear conversation |
|
||||
| `/clear-queue` | Clear queued messages |
|
||||
| `/usage` | Show token usage |
|
||||
| `/reset-usage` | Reset usage statistics |
|
||||
| `/tree` | Navigate session tree |
|
||||
| `/fork` | Branch from an earlier message |
|
||||
| `/new` | Start a new session |
|
||||
| `/name` | Set session display name |
|
||||
| `/session` | Show session info |
|
||||
| `/quit` | Exit Kit |
|
||||
|
||||
## ACP server
|
||||
|
||||
Run Kit as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server. ACP-compatible clients communicate with Kit over JSON-RPC 2.0 on stdin/stdout.
|
||||
|
||||
```bash
|
||||
kit acp # Start as ACP agent
|
||||
kit acp --debug # With debug logging to stderr
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Global Flags
|
||||
description: Complete reference for all Kit CLI flags.
|
||||
---
|
||||
|
||||
# Global Flags
|
||||
|
||||
All flags can be passed to the root `kit` command.
|
||||
|
||||
## Model and provider
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--model` | `-m` | `anthropic/claude-sonnet-4-5-20250929` | Model to use (provider/model format) |
|
||||
| `--provider-api-key` | — | — | API key for the provider |
|
||||
| `--provider-url` | — | — | Base URL for provider API |
|
||||
| `--tls-skip-verify` | — | `false` | Skip TLS certificate verification |
|
||||
|
||||
## Session management
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--session` | `-s` | — | Open specific JSONL session file |
|
||||
| `--continue` | `-c` | `false` | Resume most recent session for current directory |
|
||||
| `--resume` | `-r` | `false` | Interactive session picker |
|
||||
| `--no-session` | — | `false` | Ephemeral mode, no persistence |
|
||||
|
||||
## Behavior
|
||||
|
||||
These flags control Kit's behavior. When a prompt is passed as a positional argument, Kit runs in non-interactive mode.
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--quiet` | — | `false` | Suppress all output (non-interactive only) |
|
||||
| `--json` | — | `false` | Output response as JSON (non-interactive only) |
|
||||
| `--no-exit` | — | `false` | Enter interactive mode after prompt completes |
|
||||
| `--max-steps` | — | `0` | Maximum agent steps (0 for unlimited) |
|
||||
| `--stream` | — | `true` | Enable streaming output |
|
||||
| `--compact` | — | `false` | Enable compact output mode |
|
||||
| `--auto-compact` | — | `false` | Auto-compact conversation near context limit |
|
||||
|
||||
## Extensions
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--extension` | `-e` | — | Load additional extension file(s) (repeatable) |
|
||||
| `--no-extensions` | — | `false` | Disable all extensions |
|
||||
|
||||
## Generation parameters
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--max-tokens` | — | `4096` | Maximum tokens in response |
|
||||
| `--temperature` | — | `0.7` | Randomness 0.0–1.0 |
|
||||
| `--top-p` | — | `0.95` | Nucleus sampling 0.0–1.0 |
|
||||
| `--top-k` | — | `40` | Limit top K tokens |
|
||||
| `--stop-sequences` | — | — | Custom stop sequences (comma-separated) |
|
||||
| `--thinking-level` | — | `off` | Extended thinking level: off, minimal, low, medium, high |
|
||||
|
||||
## System
|
||||
|
||||
| Flag | Short | Default | Description |
|
||||
|------|-------|---------|-------------|
|
||||
| `--config` | — | `~/.kit.yml` | Config file path |
|
||||
| `--system-prompt` | — | — | System prompt text or file path |
|
||||
| `--debug` | — | `false` | Enable debug logging |
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Configuration
|
||||
description: Configure Kit using config files, environment variables, and CLI flags.
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
Kit looks for configuration in the following locations, in order of priority:
|
||||
|
||||
1. CLI flags
|
||||
2. Environment variables (with `KIT_` prefix)
|
||||
3. `./.kit.yml` / `./.kit.yaml` / `./.kit.json` (project-local)
|
||||
4. `~/.kit.yml` / `~/.kit.yaml` / `~/.kit.json` (global)
|
||||
|
||||
## Basic configuration
|
||||
|
||||
Create `~/.kit.yml`:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-sonnet-4-5-20250929
|
||||
max-tokens: 4096
|
||||
temperature: 0.7
|
||||
stream: true
|
||||
```
|
||||
|
||||
## All configuration keys
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `model` | string | `anthropic/claude-sonnet-4-5-20250929` | Model to use (provider/model format) |
|
||||
| `max-tokens` | int | `4096` | Maximum tokens in response |
|
||||
| `temperature` | float | `0.7` | Randomness 0.0–1.0 |
|
||||
| `top-p` | float | `0.95` | Nucleus sampling 0.0–1.0 |
|
||||
| `top-k` | int | `40` | Limit top K tokens |
|
||||
| `stream` | bool | `true` | Enable streaming output |
|
||||
| `debug` | bool | `false` | Enable debug logging |
|
||||
| `compact` | bool | `false` | Enable compact output mode |
|
||||
| `system-prompt` | string | — | System prompt text or file path |
|
||||
| `max-steps` | int | `0` | Maximum agent steps (0 = unlimited) |
|
||||
| `thinking-level` | string | `off` | Extended thinking: off, minimal, low, medium, high |
|
||||
| `provider-api-key` | string | — | API key for the provider |
|
||||
| `provider-url` | string | — | Base URL for provider API |
|
||||
| `tls-skip-verify` | bool | `false` | Skip TLS certificate verification |
|
||||
| `stop-sequences` | list | — | Custom stop sequences |
|
||||
| `theme` | object or string | — | UI theme ([inline overrides or file path](/themes)) |
|
||||
|
||||
## Environment variables
|
||||
|
||||
Any configuration key can be set via environment variable with the `KIT_` prefix. Hyphens become underscores:
|
||||
|
||||
```bash
|
||||
export KIT_MODEL="openai/gpt-4o"
|
||||
export KIT_MAX_TOKENS="8192"
|
||||
export KIT_TEMPERATURE="0.5"
|
||||
```
|
||||
|
||||
Provider API keys use their own environment variables:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export GOOGLE_API_KEY="..."
|
||||
```
|
||||
|
||||
## MCP server configuration
|
||||
|
||||
Add external MCP servers to your `.kit.yml`:
|
||||
|
||||
```yaml
|
||||
mcpServers:
|
||||
filesystem:
|
||||
type: local
|
||||
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed"]
|
||||
environment:
|
||||
LOG_LEVEL: "info"
|
||||
allowedTools: ["read_file", "write_file"]
|
||||
excludedTools: ["delete_file"]
|
||||
|
||||
search:
|
||||
type: remote
|
||||
url: "https://mcp.example.com/search"
|
||||
```
|
||||
|
||||
### MCP server fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | string | `local` (stdio) or `remote` (streamable HTTP) |
|
||||
| `command` | list | Command and args for local servers |
|
||||
| `environment` | map | Environment variables for the server process |
|
||||
| `url` | string | URL for remote servers |
|
||||
| `allowedTools` | list | Whitelist of tool names to expose |
|
||||
| `excludedTools` | list | Blacklist of tool names to hide |
|
||||
|
||||
A legacy format with `transport`, `args`, `env`, and `headers` fields is also supported.
|
||||
|
||||
## Theme configuration
|
||||
|
||||
Set theme colors inline or reference an external file:
|
||||
|
||||
```yaml
|
||||
# Inline partial overrides (unspecified fields inherit from default)
|
||||
theme:
|
||||
primary:
|
||||
light: "#8839ef"
|
||||
dark: "#cba6f7"
|
||||
error:
|
||||
dark: "#FF0000"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Reference external theme file
|
||||
theme: "./themes/my-custom-theme.yml"
|
||||
```
|
||||
|
||||
See [Themes](/themes) for the full theme file format, built-in themes, and the extension theme API.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Development
|
||||
description: Build, test, and contribute to Kit.
|
||||
---
|
||||
|
||||
# Development
|
||||
|
||||
## Build and test
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o output/kit ./cmd/kit
|
||||
|
||||
# Run all tests
|
||||
go test -race ./...
|
||||
|
||||
# Run a specific test
|
||||
go test -race ./cmd -run TestScriptExecution
|
||||
|
||||
# Lint
|
||||
go vet ./...
|
||||
|
||||
# Format
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
cmd/kit/ - CLI entry point (main.go)
|
||||
cmd/ - CLI command implementations (root, auth, models, etc.)
|
||||
pkg/kit/ - Go SDK for embedding Kit
|
||||
internal/app/ - Application orchestrator (agent loop, message store, queue)
|
||||
internal/agent/ - Agent execution and tool dispatch
|
||||
internal/auth/ - OAuth authentication and credential storage
|
||||
internal/acpserver/ - ACP (Agent Client Protocol) server
|
||||
internal/clipboard/ - Cross-platform clipboard operations
|
||||
internal/compaction/ - Conversation compaction and summarization
|
||||
internal/config/ - Configuration management
|
||||
internal/core/ - Built-in tools (bash, read, write, edit, grep, find, ls)
|
||||
internal/extensions/ - Yaegi extension system
|
||||
internal/kitsetup/ - Initial setup wizard
|
||||
internal/message/ - Message content types and structured content blocks
|
||||
internal/models/ - Provider and model management
|
||||
internal/session/ - Session persistence (tree-based JSONL)
|
||||
internal/skills/ - Skill loading and system prompt composition
|
||||
internal/tools/ - MCP tool integration
|
||||
internal/ui/ - Bubble Tea TUI components
|
||||
examples/extensions/ - Example extension files
|
||||
npm/ - NPM package wrapper for distribution
|
||||
```
|
||||
|
||||
## Architecture overview
|
||||
|
||||
Kit is built around a few key architectural patterns:
|
||||
|
||||
### Multi-provider LLM support
|
||||
|
||||
The `llm.Provider` interface abstracts different LLM providers. Each provider implements message formatting, tool calling, and streaming for its specific API.
|
||||
|
||||
### MCP client-server model
|
||||
|
||||
External tools are integrated via the Model Context Protocol (MCP). Kit acts as an MCP client, connecting to MCP servers configured in `.kit.yml`.
|
||||
|
||||
### Extension system
|
||||
|
||||
Extensions are Go source files interpreted at runtime by Yaegi. The `internal/extensions/` package manages loading, symbol export, and lifecycle dispatch. See the [Extension System](/extensions/overview) docs for details.
|
||||
|
||||
### TUI architecture
|
||||
|
||||
The interactive terminal UI is built with [Bubble Tea v2](https://github.com/charmbracelet/bubbletea), using a parent-child model where `AppModel` manages child components (`InputComponent`, `StreamComponent`, etc.).
|
||||
|
||||
### Decoupling pattern
|
||||
|
||||
`cmd/root.go` contains converter functions (e.g., `widgetProviderForUI()`) that bridge `internal/extensions/` types to `internal/ui/` types. The UI never imports the extensions package directly.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see the [contribution guide](https://github.com/mark3labs/kit/blob/master/contribute/contribute.md) for guidelines.
|
||||
|
||||
## Community
|
||||
|
||||
- [Discord](https://discord.gg/RqSS2NQVsY)
|
||||
- [GitHub Issues](https://github.com/mark3labs/kit/issues)
|
||||
@@ -0,0 +1,283 @@
|
||||
---
|
||||
title: Capabilities
|
||||
description: All extension capabilities — lifecycle events, tools, commands, widgets, and more.
|
||||
---
|
||||
|
||||
# Extension Capabilities
|
||||
|
||||
## Lifecycle events
|
||||
|
||||
Extensions can hook into 18 lifecycle events:
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `OnSessionStart` | Session initialized |
|
||||
| `OnSessionShutdown` | Session ending |
|
||||
| `OnBeforeAgentStart` | Before the agent loop begins |
|
||||
| `OnAgentStart` | Agent loop started |
|
||||
| `OnAgentEnd` | Agent loop completed |
|
||||
| `OnToolCall` | Tool call requested by the model |
|
||||
| `OnToolExecutionStart` | Tool execution beginning |
|
||||
| `OnToolExecutionEnd` | Tool execution completed |
|
||||
| `OnToolResult` | Tool result returned |
|
||||
| `OnInput` | User input received |
|
||||
| `OnMessageStart` | Assistant message started |
|
||||
| `OnMessageUpdate` | Streaming text chunk received |
|
||||
| `OnMessageEnd` | Assistant message completed |
|
||||
| `OnModelChange` | Model switched |
|
||||
| `OnContextPrepare` | Context being assembled for the model |
|
||||
| `OnBeforeFork` | Before forking a conversation branch |
|
||||
| `OnBeforeSessionSwitch` | Before switching sessions |
|
||||
| `OnBeforeCompact` | Before conversation compaction |
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
api.OnToolCall(func(event ext.ToolCallEvent, ctx ext.Context) {
|
||||
ctx.PrintInfo("Calling tool: " + event.Name)
|
||||
})
|
||||
|
||||
api.OnAgentEnd(func(_ ext.AgentEndEvent, ctx ext.Context) {
|
||||
ctx.PrintInfo("Agent finished")
|
||||
})
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
Register custom tools that the LLM can invoke:
|
||||
|
||||
```go
|
||||
api.RegisterTool(ext.ToolDef{
|
||||
Name: "weather",
|
||||
Description: "Get current weather for a location",
|
||||
Parameters: map[string]ext.ParameterDef{
|
||||
"city": {Type: "string", Description: "City name", Required: true},
|
||||
},
|
||||
Handler: func(ctx ext.Context, params map[string]any) (string, error) {
|
||||
city := params["city"].(string)
|
||||
return "Sunny, 72°F in " + city, nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
Register slash commands that users can invoke directly:
|
||||
|
||||
```go
|
||||
api.RegisterCommand(ext.CommandDef{
|
||||
Name: "stats",
|
||||
Description: "Show context statistics",
|
||||
Handler: func(ctx ext.Context, args string) {
|
||||
stats := ctx.GetContextStats()
|
||||
ctx.PrintInfo(fmt.Sprintf("Tokens: %d", stats.TotalTokens))
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Widgets
|
||||
|
||||
Add persistent status displays above or below the input area:
|
||||
|
||||
```go
|
||||
ctx.SetWidget(ext.WidgetConfig{
|
||||
ID: "token-count",
|
||||
Position: "bottom",
|
||||
Content: ext.WidgetContent{Text: "Tokens: 1,234"},
|
||||
})
|
||||
|
||||
// Update later
|
||||
ctx.SetWidget(ext.WidgetConfig{
|
||||
ID: "token-count",
|
||||
Position: "bottom",
|
||||
Content: ext.WidgetContent{Text: "Tokens: 2,456"},
|
||||
})
|
||||
|
||||
// Remove
|
||||
ctx.RemoveWidget("token-count")
|
||||
```
|
||||
|
||||
## Headers and footers
|
||||
|
||||
Persistent content above and below the conversation:
|
||||
|
||||
```go
|
||||
ctx.SetHeader(ext.HeaderFooterConfig{
|
||||
Content: ext.WidgetContent{Text: "Project: my-app | Branch: main"},
|
||||
})
|
||||
|
||||
ctx.SetFooter(ext.HeaderFooterConfig{
|
||||
Content: ext.WidgetContent{Text: "Plan Mode (read-only)"},
|
||||
})
|
||||
```
|
||||
|
||||
## Status bar
|
||||
|
||||
Custom status bar entries:
|
||||
|
||||
```go
|
||||
ctx.SetStatus("mode", "Planning")
|
||||
ctx.RemoveStatus("mode")
|
||||
```
|
||||
|
||||
## Shortcuts
|
||||
|
||||
Global keyboard shortcuts:
|
||||
|
||||
```go
|
||||
api.RegisterShortcut(ext.ShortcutDef{
|
||||
Key: "ctrl+t",
|
||||
Description: "Toggle plan mode",
|
||||
}, func(ctx ext.Context) {
|
||||
// handle shortcut
|
||||
})
|
||||
```
|
||||
|
||||
## Overlays
|
||||
|
||||
Modal dialogs with markdown content:
|
||||
|
||||
```go
|
||||
ctx.ShowOverlay(ext.OverlayConfig{
|
||||
Title: "Help",
|
||||
Content: "# Keyboard Shortcuts\n\n- **ctrl+t** — Toggle plan mode\n- **ctrl+s** — Save session",
|
||||
})
|
||||
```
|
||||
|
||||
## Tool renderers
|
||||
|
||||
Customize how specific tool calls are displayed in the TUI:
|
||||
|
||||
```go
|
||||
api.RegisterToolRenderer(ext.ToolRenderConfig{
|
||||
ToolName: "bash",
|
||||
Render: func(name, args, result string, isError bool) string {
|
||||
return "$ " + args + "\n" + result
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Message renderers
|
||||
|
||||
Custom rendering for assistant messages:
|
||||
|
||||
```go
|
||||
api.RegisterMessageRenderer(ext.MessageRendererConfig{
|
||||
Name: "custom",
|
||||
Render: func(content string) string {
|
||||
return ">> " + content
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Editor interceptors
|
||||
|
||||
Handle key events and wrap the editor's rendering:
|
||||
|
||||
```go
|
||||
ctx.SetEditor(ext.EditorConfig{
|
||||
HandleKey: func(key, text string) ext.EditorKeyAction {
|
||||
if key == "escape" {
|
||||
return ext.EditorKeyAction{Handled: true}
|
||||
}
|
||||
return ext.EditorKeyAction{Handled: false}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Interactive prompts
|
||||
|
||||
Select, confirm, input, and multi-select dialogs:
|
||||
|
||||
```go
|
||||
// Single select
|
||||
response := ctx.PromptSelect(ext.PromptSelectConfig{
|
||||
Title: "Choose a model",
|
||||
Options: []string{"claude-sonnet", "gpt-4o", "llama3"},
|
||||
})
|
||||
|
||||
// Confirm
|
||||
confirmed := ctx.PromptConfirm(ext.PromptConfirmConfig{
|
||||
Title: "Delete this file?",
|
||||
})
|
||||
|
||||
// Text input
|
||||
name := ctx.PromptInput(ext.PromptInputConfig{
|
||||
Title: "Enter project name",
|
||||
Placeholder: "my-project",
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Register configurable extension options:
|
||||
|
||||
```go
|
||||
api.RegisterOption(ext.OptionDef{
|
||||
Name: "auto-commit",
|
||||
Description: "Automatically commit on shutdown",
|
||||
DefaultValue: "false",
|
||||
})
|
||||
```
|
||||
|
||||
## Subagents
|
||||
|
||||
Spawn in-process child Kit instances:
|
||||
|
||||
```go
|
||||
result := ctx.SpawnSubagent(ext.SubagentConfig{
|
||||
Task: "Analyze the test files and summarize coverage",
|
||||
Model: "anthropic/claude-haiku-3-5-20241022",
|
||||
SystemPrompt: "You are a test analysis expert.",
|
||||
})
|
||||
```
|
||||
|
||||
## LLM completion
|
||||
|
||||
Make direct model calls without going through the agent loop:
|
||||
|
||||
```go
|
||||
response := ctx.Complete(ext.CompleteRequest{
|
||||
Prompt: "Summarize this in one sentence: " + content,
|
||||
})
|
||||
```
|
||||
|
||||
## Themes
|
||||
|
||||
Register and switch color themes at runtime:
|
||||
|
||||
```go
|
||||
// Register a custom theme
|
||||
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
|
||||
Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
|
||||
Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
|
||||
Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
|
||||
Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
|
||||
Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
|
||||
})
|
||||
|
||||
// Switch to it
|
||||
ctx.SetTheme("neon")
|
||||
|
||||
// List all available themes
|
||||
names := ctx.ListThemes()
|
||||
```
|
||||
|
||||
See [Themes](/themes) for the full theme file format, built-in themes, and color reference.
|
||||
|
||||
## Custom events
|
||||
|
||||
Inter-extension communication:
|
||||
|
||||
```go
|
||||
// Emit
|
||||
ctx.EmitCustomEvent("my-extension:data-ready", payload)
|
||||
|
||||
// Listen
|
||||
api.OnCustomEvent("my-extension:data-ready", func(data any, ctx ext.Context) {
|
||||
// handle event
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Examples
|
||||
description: Catalog of example extensions included with Kit.
|
||||
---
|
||||
|
||||
# Extension Examples
|
||||
|
||||
Kit ships with a rich set of example extensions in the `examples/extensions/` directory. These serve as both documentation and starting points for your own extensions.
|
||||
|
||||
## UI and display
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`minimal.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/minimal.go) | Clean UI with custom footer |
|
||||
| [`branded-output.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/branded-output.go) | Branded output rendering |
|
||||
| [`header-footer-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/header-footer-demo.go) | Custom headers and footers |
|
||||
| [`widget-status.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/widget-status.go) | Persistent status widgets |
|
||||
| [`overlay-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/overlay-demo.go) | Modal dialogs |
|
||||
| [`tool-renderer-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-renderer-demo.go) | Custom tool call rendering |
|
||||
| [`custom-editor-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/custom-editor-demo.go) | Vim-like modal editor |
|
||||
| [`pirate.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/pirate.go) | Pirate-themed personality |
|
||||
|
||||
## Workflow and automation
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`auto-commit.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/auto-commit.go) | Auto-commit changes on shutdown |
|
||||
| [`plan-mode.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/plan-mode.go) | Read-only planning mode |
|
||||
| [`permission-gate.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/permission-gate.go) | Permission gating for destructive tools |
|
||||
| [`confirm-destructive.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/confirm-destructive.go) | Confirm destructive operations |
|
||||
| [`protected-paths.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/protected-paths.go) | Path protection for sensitive files |
|
||||
| [`project-rules.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/project-rules.go) | Project-specific rules injection |
|
||||
| [`compact-notify.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/compact-notify.go) | Notification on conversation compaction |
|
||||
|
||||
## Interactive features
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`prompt-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/prompt-demo.go) | Interactive prompts (select/confirm/input) |
|
||||
| [`bookmark.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/bookmark.go) | Bookmark conversations |
|
||||
| [`inline-bash.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/inline-bash.go) | Inline bash execution |
|
||||
| [`interactive-shell.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/interactive-shell.go) | Interactive shell integration |
|
||||
| [`notify.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/notify.go) | Desktop notifications |
|
||||
|
||||
## Agent and context
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`tool-logger.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-logger.go) | Log all tool calls |
|
||||
| [`context-inject.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/context-inject.go) | Inject context into conversations |
|
||||
| [`summarize.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/summarize.go) | Conversation summarization |
|
||||
| [`lsp-diagnostics.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/lsp-diagnostics.go) | LSP diagnostic integration |
|
||||
|
||||
## Multi-agent
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`kit-kit.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/kit-kit.go) | Kit-in-Kit sub-agent spawning |
|
||||
| [`subagent-widget.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/subagent-widget.go) | Multi-agent orchestration with status widget |
|
||||
| [`subagent-test.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/subagent-test.go) | Subagent testing utilities |
|
||||
|
||||
## Development
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| [`dev-reload.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/dev-reload.go) | Development live-reload |
|
||||
| [`tool-logger_test.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-logger_test.go) | Example extension tests (see [Testing](/extensions/testing)) |
|
||||
| [`extension_test_template.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/extension_test_template.go) | Copy-and-paste test template for your extensions |
|
||||
|
||||
## Subdirectory extensions
|
||||
|
||||
| Directory | Description |
|
||||
|-----------|-------------|
|
||||
| [`kit-kit-agents/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/kit-kit-agents) | Multi-agent orchestration example |
|
||||
| [`kit-telegram/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/kit-telegram) | Telegram bot integration |
|
||||
| [`status-tools/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/status-tools) | Status bar tool examples |
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Loading Extensions
|
||||
description: How Kit discovers and loads extensions.
|
||||
---
|
||||
|
||||
# Loading Extensions
|
||||
|
||||
## Auto-discovery
|
||||
|
||||
Kit automatically discovers and loads extensions from these paths, in order:
|
||||
|
||||
| Path | Scope |
|
||||
|------|-------|
|
||||
| `~/.config/kit/extensions/*.go` | Global single files |
|
||||
| `~/.config/kit/extensions/*/main.go` | Global subdirectory extensions |
|
||||
| `.kit/extensions/*.go` | Project-local single files |
|
||||
| `.kit/extensions/*/main.go` | Project-local subdirectory extensions |
|
||||
| `~/.local/share/kit/git/` | Global git-installed packages |
|
||||
| `.kit/git/` | Project-local git-installed packages |
|
||||
|
||||
## Explicit loading
|
||||
|
||||
Load extensions by path using the `-e` flag:
|
||||
|
||||
```bash
|
||||
kit -e path/to/extension.go
|
||||
```
|
||||
|
||||
Load multiple extensions:
|
||||
|
||||
```bash
|
||||
kit -e ext1.go -e ext2.go
|
||||
```
|
||||
|
||||
## Disabling extensions
|
||||
|
||||
Disable all auto-discovered extensions:
|
||||
|
||||
```bash
|
||||
kit --no-extensions
|
||||
```
|
||||
|
||||
You can combine `--no-extensions` with `-e` to load only specific extensions:
|
||||
|
||||
```bash
|
||||
kit --no-extensions -e my-extension.go
|
||||
```
|
||||
|
||||
## Installing from git
|
||||
|
||||
Install extensions from git repositories using `kit install`:
|
||||
|
||||
```bash
|
||||
# Install globally (to ~/.local/share/kit/git/)
|
||||
kit install https://github.com/user/my-kit-extension.git
|
||||
|
||||
# Install project-locally (to .kit/git/)
|
||||
kit install -l https://github.com/user/my-kit-extension.git
|
||||
|
||||
# Update an installed package
|
||||
kit install -u https://github.com/user/my-kit-extension.git
|
||||
|
||||
# Remove
|
||||
kit install --uninstall my-kit-extension
|
||||
```
|
||||
|
||||
## Extension structure
|
||||
|
||||
### Single-file extensions
|
||||
|
||||
A single `.go` file with an `Init` function:
|
||||
|
||||
```go
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
// register handlers, tools, commands, etc.
|
||||
}
|
||||
```
|
||||
|
||||
The `//go:build ignore` directive prevents the Go toolchain from trying to compile the file as part of a normal build.
|
||||
|
||||
### Subdirectory extensions
|
||||
|
||||
For more complex extensions, create a directory with a `main.go` entry point:
|
||||
|
||||
```
|
||||
.kit/extensions/my-extension/
|
||||
├── main.go # Must contain Init(api ext.API)
|
||||
├── helpers.go # Additional source files
|
||||
└── config.go
|
||||
```
|
||||
|
||||
### Package-level state
|
||||
|
||||
Yaegi supports package-level variables captured in closures. This is the standard way to maintain state across event callbacks:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
var callCount int
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(_ ext.ToolCallEvent, ctx ext.Context) {
|
||||
callCount++
|
||||
ctx.SetFooter(ext.HeaderFooterConfig{
|
||||
Content: ext.WidgetContent{
|
||||
Text: fmt.Sprintf("Tools called: %d", callCount),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Extension System
|
||||
description: Overview of Kit's Go-based extension system.
|
||||
---
|
||||
|
||||
# Extension System
|
||||
|
||||
Extensions are Go source files interpreted at runtime via [Yaegi](https://github.com/traefik/yaegi). They can add custom tools, slash commands, widgets, keyboard shortcuts, and intercept lifecycle events — all without recompiling Kit.
|
||||
|
||||
## Minimal extension
|
||||
|
||||
```go
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.SetFooter(ext.HeaderFooterConfig{
|
||||
Content: ext.WidgetContent{Text: "Custom Footer"},
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
kit -e examples/extensions/minimal.go
|
||||
```
|
||||
|
||||
## How extensions work
|
||||
|
||||
1. Kit discovers extension files from [auto-discovery paths](/extensions/loading) or explicit `-e` flags
|
||||
2. Each `.go` file is loaded into a Yaegi interpreter with access to the `kit/ext` package
|
||||
3. Kit calls the `Init(api ext.API)` function in each extension
|
||||
4. The extension registers callbacks, tools, commands, and UI components via the `api` and `ctx` objects
|
||||
|
||||
## Key concepts
|
||||
|
||||
### The `API` object
|
||||
|
||||
Passed to `Init()`, the `API` object is used to register lifecycle event handlers and static components:
|
||||
|
||||
- **Lifecycle handlers** — `api.OnSessionStart(...)`, `api.OnToolCall(...)`, etc.
|
||||
- **Tools** — `api.RegisterTool(ext.ToolDef{...})`
|
||||
- **Commands** — `api.RegisterCommand(ext.CommandDef{...})`
|
||||
- **Shortcuts** — `api.RegisterShortcut(ext.ShortcutDef{...}, handler)`
|
||||
- **Tool renderers** — `api.RegisterToolRenderer(ext.ToolRenderConfig{...})`
|
||||
- **Message renderers** — `api.RegisterMessageRenderer(ext.MessageRendererConfig{...})`
|
||||
- **Options** — `api.RegisterOption(ext.OptionDef{...})`
|
||||
|
||||
### The `Context` object
|
||||
|
||||
Passed to event handlers, the `Context` object provides runtime access to Kit's state and UI:
|
||||
|
||||
- **Output** — `ctx.Print(...)`, `ctx.PrintInfo(...)`, `ctx.PrintError(...)`
|
||||
- **UI components** — `ctx.SetWidget(...)`, `ctx.SetHeader(...)`, `ctx.SetFooter(...)`, `ctx.SetStatus(...)`
|
||||
- **Editor** — `ctx.SetEditor(...)`, `ctx.ResetEditor()`
|
||||
- **Prompts** — `ctx.PromptSelect(...)`, `ctx.PromptConfirm(...)`, `ctx.PromptInput(...)`
|
||||
- **Overlays** — `ctx.ShowOverlay(...)`
|
||||
- **Messages** — `ctx.SendMessage(...)`, `ctx.GetMessages()`
|
||||
- **Model** — `ctx.SetModel(...)`, `ctx.GetAvailableModels()`
|
||||
- **Tools** — `ctx.GetAllTools()`, `ctx.SetActiveTools(...)`
|
||||
- **Context stats** — `ctx.GetContextStats()`
|
||||
- **Session data** — `ctx.AppendEntry(...)`, `ctx.GetEntries(...)`
|
||||
- **Subagents** — `ctx.SpawnSubagent(...)`
|
||||
- **LLM completion** — `ctx.Complete(...)`
|
||||
- **Custom events** — `ctx.EmitCustomEvent(...)`
|
||||
|
||||
See [Capabilities](/extensions/capabilities) for full details on each component type, and [Testing](/extensions/testing) for writing tests for your extensions.
|
||||
@@ -0,0 +1,448 @@
|
||||
---
|
||||
title: Testing Extensions
|
||||
description: Write unit tests for your Kit extensions using the test package.
|
||||
---
|
||||
|
||||
# Testing Extensions
|
||||
|
||||
Kit provides a testing package (`github.com/mark3labs/kit/internal/extensions/test`) that enables you to write unit tests for your extensions. Tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance, allowing you to verify behavior without running the full Kit TUI.
|
||||
|
||||
## Overview
|
||||
|
||||
Extension tests allow you to:
|
||||
|
||||
- Test event handlers without running the interactive TUI
|
||||
- Verify tool/command registration
|
||||
- Assert that context methods (Print, SetWidget, etc.) are called correctly
|
||||
- Test blocking and non-blocking event handling
|
||||
- Simulate user input and tool calls
|
||||
- Verify widget, header, footer, and status bar updates
|
||||
|
||||
## Installation
|
||||
|
||||
The test package is part of the Kit codebase. Import it in your extension tests:
|
||||
|
||||
```go
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Testing an Extension File
|
||||
|
||||
Create a test file alongside your extension (e.g., `my-ext_test.go`):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/mark3labs/kit/internal/extensions/test"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
)
|
||||
|
||||
func TestMyExtension(t *testing.T) {
|
||||
// Create a test harness
|
||||
harness := test.New(t)
|
||||
|
||||
// Load your extension
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Emit events and check results
|
||||
result, err := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "my_tool",
|
||||
Input: `{"key": "value"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Use assertion helpers
|
||||
test.AssertNotBlocked(t, result)
|
||||
test.AssertPrinted(t, harness, "expected output")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Inline Extension Code
|
||||
|
||||
For quick tests or edge cases, you can load extension source directly:
|
||||
|
||||
```go
|
||||
func TestToolBlocking(t *testing.T) {
|
||||
src := `package main
|
||||
|
||||
import "kit/ext"
|
||||
|
||||
func Init(api ext.API) {
|
||||
api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
|
||||
if tc.ToolName == "dangerous" {
|
||||
return &ext.ToolCallResult{Block: true, Reason: "not allowed"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
harness := test.New(t)
|
||||
harness.LoadString(src, "test-ext.go")
|
||||
|
||||
// Test the tool is blocked
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{
|
||||
ToolName: "dangerous",
|
||||
Input: "{}",
|
||||
})
|
||||
|
||||
test.AssertBlocked(t, result, "not allowed")
|
||||
}
|
||||
```
|
||||
|
||||
## Common Testing Patterns
|
||||
|
||||
### Testing Handler Registration
|
||||
|
||||
Verify your extension registers the expected handlers:
|
||||
|
||||
```go
|
||||
func TestHandlers(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
test.AssertHasHandlers(t, harness, extensions.ToolCall)
|
||||
test.AssertHasHandlers(t, harness, extensions.SessionStart)
|
||||
test.AssertNoHandlers(t, harness, extensions.AgentEnd) // Verify no unexpected handlers
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Tool Registration
|
||||
|
||||
```go
|
||||
func TestTools(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Verify a specific tool is registered
|
||||
test.AssertToolRegistered(t, harness, "my_tool")
|
||||
|
||||
// Or inspect all tools
|
||||
tools := harness.RegisteredTools()
|
||||
for _, tool := range tools {
|
||||
t.Logf("Tool: %s - %s", tool.Name, tool.Description)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Commands
|
||||
|
||||
```go
|
||||
func TestCommands(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
test.AssertCommandRegistered(t, harness, "mycommand")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Widgets
|
||||
|
||||
```go
|
||||
func TestWidgets(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Trigger event that creates the widget
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
// Verify widget was set
|
||||
test.AssertWidgetSet(t, harness, "my-widget")
|
||||
test.AssertWidgetText(t, harness, "my-widget", "Expected Text")
|
||||
test.AssertWidgetTextContains(t, harness, "my-widget", "partial")
|
||||
|
||||
// Check widget properties directly
|
||||
widget, ok := harness.Context().GetWidget("my-widget")
|
||||
if ok {
|
||||
t.Logf("Border color: %s", widget.Style.BorderColor)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Input Handling
|
||||
|
||||
```go
|
||||
func TestInput(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
result, _ := harness.Emit(extensions.InputEvent{
|
||||
Text: "!mycommand",
|
||||
Source: "cli",
|
||||
})
|
||||
|
||||
test.AssertInputHandled(t, result, "handled")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Headers and Footers
|
||||
|
||||
```go
|
||||
func TestHeaderFooter(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
|
||||
test.AssertHeaderSet(t, harness)
|
||||
test.AssertFooterSet(t, harness)
|
||||
|
||||
// Inspect content
|
||||
header := harness.Context().GetHeader()
|
||||
if header != nil {
|
||||
t.Logf("Header text: %s", header.Content.Text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Status Bar
|
||||
|
||||
```go
|
||||
func TestStatus(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.AgentEndEvent{})
|
||||
|
||||
test.AssertStatusSet(t, harness, "myext:status")
|
||||
test.AssertStatusText(t, harness, "myext:status", "Ready")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Print Output
|
||||
|
||||
```go
|
||||
func TestOutput(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
_, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "test"})
|
||||
|
||||
// Exact match
|
||||
test.AssertPrinted(t, harness, "exact output")
|
||||
|
||||
// Partial match
|
||||
test.AssertPrintedContains(t, harness, "partial")
|
||||
|
||||
// Styled output
|
||||
test.AssertPrintInfo(t, harness, "info message")
|
||||
test.AssertPrintError(t, harness, "error message")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with Prompts
|
||||
|
||||
Configure mock prompt results for testing interactive behavior:
|
||||
|
||||
```go
|
||||
func TestWithPrompts(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Configure what prompts should return
|
||||
harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
|
||||
Value: "option1",
|
||||
Index: 0,
|
||||
Cancelled: false,
|
||||
})
|
||||
|
||||
harness.Context().SetPromptConfirmResult(extensions.PromptConfirmResult{
|
||||
Value: true,
|
||||
Cancelled: false,
|
||||
})
|
||||
|
||||
// Now when your extension calls ctx.PromptSelect(), it gets this result
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Complete Session Flow
|
||||
|
||||
```go
|
||||
func TestFullSession(t *testing.T) {
|
||||
harness := test.New(t)
|
||||
harness.LoadFile("my-ext.go")
|
||||
|
||||
// Simulate a complete session
|
||||
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
||||
_, _ = harness.Emit(extensions.BeforeAgentStartEvent{})
|
||||
_, _ = harness.Emit(extensions.AgentStartEvent{})
|
||||
|
||||
// Multiple tool calls
|
||||
tools := []string{"Read", "Grep", "Bash"}
|
||||
for _, tool := range tools {
|
||||
_, _ = harness.Emit(extensions.ToolCallEvent{ToolName: tool})
|
||||
_, _ = harness.Emit(extensions.ToolResultEvent{ToolName: tool})
|
||||
}
|
||||
|
||||
_, _ = harness.Emit(extensions.AgentEndEvent{})
|
||||
_, _ = harness.Emit(extensions.SessionShutdownEvent{})
|
||||
|
||||
// Verify final state
|
||||
test.AssertWidgetTextContains(t, harness, "status", "Complete")
|
||||
}
|
||||
```
|
||||
|
||||
## Available Assertions
|
||||
|
||||
The test package provides these assertion helpers:
|
||||
|
||||
### Event Results
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `AssertNotBlocked(t, result)` | Verify tool was not blocked |
|
||||
| `AssertBlocked(t, result, reason)` | Verify tool was blocked with reason |
|
||||
| `AssertInputHandled(t, result, action)` | Verify input was handled |
|
||||
| `AssertInputTransformed(t, result, text)` | Verify input was transformed |
|
||||
|
||||
### Context Interactions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `AssertPrinted(t, harness, text)` | Verify exact print output |
|
||||
| `AssertPrintedContains(t, harness, substring)` | Verify partial print output |
|
||||
| `AssertPrintInfo(t, harness, text)` | Verify PrintInfo was called |
|
||||
| `AssertPrintError(t, harness, text)` | Verify PrintError was called |
|
||||
| `AssertWidgetSet(t, harness, id)` | Verify widget was set |
|
||||
| `AssertWidgetNotSet(t, harness, id)` | Verify widget was not set |
|
||||
| `AssertWidgetText(t, harness, id, text)` | Verify widget content |
|
||||
| `AssertWidgetTextContains(t, harness, id, substring)` | Verify widget contains text |
|
||||
| `AssertHeaderSet(t, harness)` | Verify header was set |
|
||||
| `AssertFooterSet(t, harness)` | Verify footer was set |
|
||||
| `AssertStatusSet(t, harness, key)` | Verify status was set |
|
||||
| `AssertStatusText(t, harness, key, text)` | Verify status text |
|
||||
|
||||
### Registration
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `AssertToolRegistered(t, harness, name)` | Verify tool registration |
|
||||
| `AssertCommandRegistered(t, harness, name)` | Verify command registration |
|
||||
| `AssertHasHandlers(t, harness, eventType)` | Verify handlers exist |
|
||||
| `AssertNoHandlers(t, harness, eventType)` | Verify no handlers |
|
||||
|
||||
### Messaging
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `AssertMessageSent(t, harness, text)` | Verify SendMessage was called |
|
||||
| `AssertCancelAndSend(t, harness, text)` | Verify CancelAndSend was called |
|
||||
|
||||
## Helper Functions
|
||||
|
||||
For custom assertions, extract result details:
|
||||
|
||||
```go
|
||||
result, _ := harness.Emit(extensions.ToolCallEvent{...})
|
||||
tcr := test.GetToolCallResult(result)
|
||||
if tcr != nil {
|
||||
t.Logf("Block: %v, Reason: %s", tcr.Block, tcr.Reason)
|
||||
}
|
||||
|
||||
ir := test.GetInputResult(result)
|
||||
trr := test.GetToolResultResult(result)
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Accessing the Mock Context
|
||||
|
||||
For custom verification:
|
||||
|
||||
```go
|
||||
ctx := harness.Context()
|
||||
|
||||
// Get all recorded prints
|
||||
prints := ctx.GetPrints()
|
||||
|
||||
// Check options
|
||||
value := ctx.GetOption("my-option")
|
||||
|
||||
// Verify widget properties
|
||||
widget, ok := ctx.GetWidget("my-widget")
|
||||
if ok && widget.Style.BorderColor == "#ff0000" {
|
||||
t.Log("Widget has red border")
|
||||
}
|
||||
|
||||
// Check status entries
|
||||
status, ok := ctx.GetStatus("myext:status")
|
||||
```
|
||||
|
||||
### Testing Multiple Extensions
|
||||
|
||||
Each harness is isolated:
|
||||
|
||||
```go
|
||||
harness1 := test.New(t)
|
||||
harness1.LoadFile("ext1.go")
|
||||
|
||||
harness2 := test.New(t)
|
||||
harness2.LoadFile("ext2.go")
|
||||
|
||||
// Events to one don't affect the other
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
Run all tests in your extension directory:
|
||||
|
||||
```bash
|
||||
cd examples/extensions
|
||||
go test -v
|
||||
```
|
||||
|
||||
Run with race detector:
|
||||
|
||||
```bash
|
||||
go test -race -v
|
||||
```
|
||||
|
||||
Run a specific test:
|
||||
|
||||
```bash
|
||||
go test -v -run TestMyExtension
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test one behavior per test** — Keep tests focused and readable
|
||||
2. **Use inline source for edge cases** — `LoadString()` is great for testing specific scenarios
|
||||
3. **Use `LoadFile()` for integration tests** — Tests the actual extension file
|
||||
4. **Assert on context calls** — Verify your extension interacts with the context correctly
|
||||
5. **Test both positive and negative cases** — Verify tools are blocked AND allowed appropriately
|
||||
6. **Test all event handlers** — Make sure all registered handlers work correctly
|
||||
7. **Use descriptive test names** — `TestExtension_BlocksDangerousTools` is clearer than `Test1`
|
||||
|
||||
## Limitations
|
||||
|
||||
The test harness has these intentional limitations:
|
||||
|
||||
- **No TUI rendering** — Widgets are recorded but not rendered visually
|
||||
- **Prompts return configured values** — Pre-configure prompt results in tests
|
||||
- **Subagents don't spawn real processes** — `SpawnSubagent()` returns nil/empty results
|
||||
- **LLM completions are mocked** — `Complete()` returns empty responses
|
||||
- **Some context methods are no-ops** — `Exit()`, `SetActiveTools()`, etc. don't have side effects
|
||||
|
||||
These limitations focus testing on extension logic rather than the full Kit runtime.
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `examples/extensions/tool-logger_test.go` for a complete example with 14 tests covering:
|
||||
|
||||
- Handler registration
|
||||
- Tool call and result handling
|
||||
- Session lifecycle events
|
||||
- Input commands (`!time`, `!status`)
|
||||
- Unknown command handling
|
||||
- Concurrent operations (race condition check)
|
||||
- Real file logging verification
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Kit
|
||||
description: Kit is a powerful, extensible AI coding agent CLI with multi-provider support, built-in tools, and a rich extension system.
|
||||
toc: false
|
||||
---
|
||||
|
||||
<div style="text-align: center; margin: 2rem 0;">
|
||||
<img src="/logo.jpg" alt="KIT" style="max-width: 400px; width: 100%; margin: 0 auto; display: block;" />
|
||||
</div>
|
||||
|
||||
A powerful, extensible AI coding agent CLI with multi-provider support, built-in tools, and a rich extension system.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Provider LLM Support** — Anthropic, OpenAI, Google Gemini, Ollama, Azure OpenAI, AWS Bedrock, OpenRouter, and more
|
||||
- **Built-in Core Tools** — bash, read, write, edit, grep, find, ls, spawn_subagent with no MCP overhead
|
||||
- **MCP Integration** — Connect external MCP servers for expanded capabilities
|
||||
- **Extension System** — Write custom tools, commands, widgets, and UI modifications in Go
|
||||
- **Interactive TUI** — Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
|
||||
- **Session Management** — Tree-based conversation history with branching support
|
||||
- **Non-Interactive Mode** — Script-friendly positional args with JSON output
|
||||
- **ACP Server** — Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio
|
||||
- **Go SDK** — Embed Kit in your own applications
|
||||
|
||||
## Quick links
|
||||
|
||||
| Resource | Description |
|
||||
|----------|-------------|
|
||||
| [Installation](/installation) | Get Kit up and running |
|
||||
| [Quick Start](/quick-start) | Your first Kit session |
|
||||
| [Configuration](/configuration) | Customize Kit for your workflow |
|
||||
| [Extensions](/extensions/overview) | Build custom tools and UI components |
|
||||
| [Go SDK](/sdk/overview) | Embed Kit in your applications |
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Install Kit using npm, bun, pnpm, Go, or build from source.
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
## Using npm / bun / pnpm
|
||||
|
||||
```bash
|
||||
npm install -g @mark3labs/kit
|
||||
```
|
||||
|
||||
```bash
|
||||
bun install -g @mark3labs/kit
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm install -g @mark3labs/kit
|
||||
```
|
||||
|
||||
## Using Go
|
||||
|
||||
```bash
|
||||
go install github.com/mark3labs/kit/cmd/kit@latest
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mark3labs/kit.git
|
||||
cd kit
|
||||
go build -o kit ./cmd/kit
|
||||
```
|
||||
|
||||
## Verifying the installation
|
||||
|
||||
After installing, verify Kit is available:
|
||||
|
||||
```bash
|
||||
kit --help
|
||||
```
|
||||
|
||||
## Setting up a provider
|
||||
|
||||
Kit needs at least one LLM provider configured. Set an API key for your preferred provider:
|
||||
|
||||
```bash
|
||||
# Anthropic (default provider)
|
||||
export ANTHROPIC_API_KEY="sk-..."
|
||||
|
||||
# OpenAI
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
|
||||
# Google Gemini
|
||||
export GOOGLE_API_KEY="..."
|
||||
```
|
||||
|
||||
For OAuth-enabled providers like Anthropic, you can also authenticate interactively:
|
||||
|
||||
```bash
|
||||
kit auth login anthropic
|
||||
```
|
||||
|
||||
See [Providers](/providers) for the full list of supported providers and their configuration.
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Providers
|
||||
description: Supported LLM providers and model configuration.
|
||||
---
|
||||
|
||||
# Providers
|
||||
|
||||
Kit supports a wide range of LLM providers through a unified `provider/model` string format.
|
||||
|
||||
## Supported providers
|
||||
|
||||
| Provider | Prefix | Description |
|
||||
|----------|--------|-------------|
|
||||
| **Anthropic** | `anthropic/` | Claude models (native, prompt caching, OAuth) |
|
||||
| **OpenAI** | `openai/` | GPT models |
|
||||
| **Google** | `google/` or `gemini/` | Gemini models |
|
||||
| **Ollama** | `ollama/` | Local models |
|
||||
| **Azure OpenAI** | `azure/` | Azure-hosted OpenAI |
|
||||
| **AWS Bedrock** | `bedrock/` | Bedrock models |
|
||||
| **Google Vertex** | `google-vertex-anthropic/` | Claude on Vertex AI |
|
||||
| **OpenRouter** | `openrouter/` | Multi-provider router |
|
||||
| **Vercel AI** | `vercel/` | Vercel AI SDK models |
|
||||
| **Auto-routed** | any | Any provider from the models.dev database |
|
||||
|
||||
## Model string format
|
||||
|
||||
```bash
|
||||
provider/model # Standard format
|
||||
anthropic/claude-sonnet-4-5-20250929
|
||||
openai/gpt-4o
|
||||
ollama/llama3
|
||||
google/gemini-2.0-flash-exp
|
||||
```
|
||||
|
||||
## Model aliases
|
||||
|
||||
Kit provides aliases for commonly used models:
|
||||
|
||||
```bash
|
||||
claude-opus-latest → claude-opus-4-20250514
|
||||
claude-sonnet-latest → claude-sonnet-4-5-20250929
|
||||
claude-4-opus-latest → claude-opus-4-20250514
|
||||
claude-4-sonnet-latest → claude-sonnet-4-5-20250929
|
||||
claude-3-7-sonnet-latest → claude-3-7-sonnet-20250219
|
||||
claude-3-5-sonnet-latest → claude-3-5-sonnet-20241022
|
||||
claude-3-5-haiku-latest → claude-3-5-haiku-20241022
|
||||
claude-3-opus-latest → claude-3-opus-20240229
|
||||
```
|
||||
|
||||
## Specifying a model
|
||||
|
||||
Via CLI flag:
|
||||
|
||||
```bash
|
||||
kit --model openai/gpt-4o
|
||||
kit -m ollama/llama3
|
||||
```
|
||||
|
||||
Via config file:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-sonnet-4-5-20250929
|
||||
```
|
||||
|
||||
Via environment variable:
|
||||
|
||||
```bash
|
||||
export KIT_MODEL="google/gemini-2.0-flash-exp"
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### API keys
|
||||
|
||||
Set the appropriate environment variable for your provider:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export GOOGLE_API_KEY="..."
|
||||
```
|
||||
|
||||
Or pass it directly:
|
||||
|
||||
```bash
|
||||
kit --provider-api-key "sk-..." --model openai/gpt-4o
|
||||
```
|
||||
|
||||
### OAuth
|
||||
|
||||
For providers that support OAuth (e.g., Anthropic):
|
||||
|
||||
```bash
|
||||
kit auth login anthropic # Start OAuth flow
|
||||
kit auth status # Check authentication status
|
||||
kit auth logout anthropic # Remove credentials
|
||||
```
|
||||
|
||||
### Custom provider URL
|
||||
|
||||
For self-hosted or proxy endpoints:
|
||||
|
||||
```bash
|
||||
kit --provider-url "https://my-proxy.example.com/v1" --model openai/gpt-4o
|
||||
```
|
||||
|
||||
## Model database
|
||||
|
||||
Kit ships with a local model database that maps provider names to API configurations. You can manage it with:
|
||||
|
||||
```bash
|
||||
kit models # List available models
|
||||
kit models openai # Filter by provider
|
||||
kit models --all # Show all providers
|
||||
kit update-models # Update from models.dev
|
||||
kit update-models embedded # Reset to bundled database
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Quick Start
|
||||
description: Get up and running with Kit in minutes.
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
## Basic usage
|
||||
|
||||
Start an interactive session:
|
||||
|
||||
```bash
|
||||
kit
|
||||
```
|
||||
|
||||
Run a one-off prompt:
|
||||
|
||||
```bash
|
||||
kit "List files in src/"
|
||||
```
|
||||
|
||||
Attach files as context using the `@` prefix:
|
||||
|
||||
```bash
|
||||
kit @main.go @test.go "Review these files"
|
||||
```
|
||||
|
||||
Use a specific model:
|
||||
|
||||
```bash
|
||||
kit --model anthropic/claude-sonnet-4-5-20250929
|
||||
```
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
Kit can run as a non-interactive tool for scripting and automation.
|
||||
|
||||
Get JSON output:
|
||||
|
||||
```bash
|
||||
kit "Explain main.go" --json
|
||||
```
|
||||
|
||||
Quiet mode (final response only, no TUI):
|
||||
|
||||
```bash
|
||||
kit "Run tests" --quiet
|
||||
```
|
||||
|
||||
Ephemeral mode (no session file created):
|
||||
|
||||
```bash
|
||||
kit "Quick question" --no-session
|
||||
```
|
||||
|
||||
## Resuming sessions
|
||||
|
||||
Continue the most recent session for the current directory:
|
||||
|
||||
```bash
|
||||
kit --continue
|
||||
# or
|
||||
kit -c
|
||||
```
|
||||
|
||||
Pick from previous sessions interactively:
|
||||
|
||||
```bash
|
||||
kit --resume
|
||||
# or
|
||||
kit -r
|
||||
```
|
||||
|
||||
## ACP server mode
|
||||
|
||||
Kit can run as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server, enabling ACP-compatible clients (such as [OpenCode](https://github.com/sst/opencode)) to drive Kit as a remote coding agent over stdio:
|
||||
|
||||
```bash
|
||||
# Start Kit as an ACP server (JSON-RPC 2.0 on stdin/stdout)
|
||||
kit acp
|
||||
|
||||
# With debug logging to stderr
|
||||
kit acp --debug
|
||||
```
|
||||
|
||||
The ACP server exposes Kit's full capabilities — LLM execution, tool calls (bash, read, write, edit, grep, etc.), and session persistence — over the standard ACP protocol.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Callbacks
|
||||
description: Monitor tool calls and streaming output with the Kit Go SDK.
|
||||
---
|
||||
|
||||
# Callbacks
|
||||
|
||||
## PromptWithCallbacks
|
||||
|
||||
The `PromptWithCallbacks` method provides real-time visibility into tool calls and streaming output:
|
||||
|
||||
```go
|
||||
response, err := host.PromptWithCallbacks(
|
||||
ctx,
|
||||
"List files in current directory",
|
||||
func(name, args string) {
|
||||
// Called when the model invokes a tool
|
||||
fmt.Println("Calling tool:", name)
|
||||
},
|
||||
func(name, args, result string, isError bool) {
|
||||
// Called when a tool returns its result
|
||||
if isError {
|
||||
fmt.Println("Tool failed:", name)
|
||||
}
|
||||
},
|
||||
func(chunk string) {
|
||||
// Called for each streaming text chunk
|
||||
fmt.Print(chunk)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Callback signatures
|
||||
|
||||
| Callback | Signature | When |
|
||||
|----------|-----------|------|
|
||||
| `onToolCall` | `func(name, args string)` | Model requests a tool call |
|
||||
| `onToolResult` | `func(name, args, result string, isError bool)` | Tool execution completes |
|
||||
| `onStreaming` | `func(chunk string)` | Streaming text chunk received |
|
||||
|
||||
Any callback can be `nil` if you don't need it:
|
||||
|
||||
```go
|
||||
// Only care about streaming output
|
||||
response, err := host.PromptWithCallbacks(ctx, "Hello", nil, nil, func(chunk string) {
|
||||
fmt.Print(chunk)
|
||||
})
|
||||
```
|
||||
|
||||
## Event-based monitoring
|
||||
|
||||
For more granular control, use the event subscription API:
|
||||
|
||||
```go
|
||||
// Subscribe returns an unsubscribe function
|
||||
unsub := host.OnToolCall(func(event kit.ToolCallEvent) {
|
||||
fmt.Printf("Tool: %s, Args: %s\n", event.Name, event.Args)
|
||||
})
|
||||
defer unsub()
|
||||
|
||||
unsub2 := host.OnToolResult(func(event kit.ToolResultEvent) {
|
||||
fmt.Printf("Result: %s (error: %v)\n", event.Name, event.IsError)
|
||||
})
|
||||
defer unsub2()
|
||||
|
||||
unsub3 := host.OnStreaming(func(event kit.MessageUpdateEvent) {
|
||||
fmt.Print(event.Chunk)
|
||||
})
|
||||
defer unsub3()
|
||||
|
||||
unsub4 := host.OnResponse(func(event kit.ResponseEvent) {
|
||||
fmt.Println("Final response received")
|
||||
})
|
||||
defer unsub4()
|
||||
|
||||
unsub5 := host.OnTurnStart(func(event kit.TurnStartEvent) {
|
||||
fmt.Println("Turn started")
|
||||
})
|
||||
defer unsub5()
|
||||
|
||||
unsub6 := host.OnTurnEnd(func(event kit.TurnEndEvent) {
|
||||
fmt.Println("Turn ended")
|
||||
})
|
||||
defer unsub6()
|
||||
```
|
||||
|
||||
## Hook system
|
||||
|
||||
Hooks allow you to intercept and modify behavior. Unlike events, hooks can modify or cancel operations:
|
||||
|
||||
```go
|
||||
// Intercept tool calls before execution
|
||||
host.OnBeforeToolCall(0, func(ctx context.Context, name string, args string) (string, error) {
|
||||
if name == "bash" {
|
||||
log.Println("Bash command:", args)
|
||||
}
|
||||
return args, nil // return modified args or error to cancel
|
||||
})
|
||||
|
||||
// Process results after tool execution
|
||||
host.OnAfterToolResult(0, func(ctx context.Context, name string, result string) (string, error) {
|
||||
return result, nil
|
||||
})
|
||||
|
||||
// Before/after each agent turn
|
||||
host.OnBeforeTurn(0, func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
host.OnAfterTurn(0, func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
The first argument is a priority (lower = runs first).
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: SDK Options
|
||||
description: Configuration options for the Kit Go SDK.
|
||||
---
|
||||
|
||||
# SDK Options
|
||||
|
||||
Pass an `Options` struct to `kit.New()` to configure the Kit instance.
|
||||
|
||||
## Full options reference
|
||||
|
||||
```go
|
||||
host, err := kit.New(ctx, &kit.Options{
|
||||
// Model
|
||||
Model: "ollama/llama3",
|
||||
SystemPrompt: "You are a helpful bot",
|
||||
ConfigFile: "/path/to/config.yml",
|
||||
|
||||
// Behavior
|
||||
MaxSteps: 10,
|
||||
Streaming: true,
|
||||
Quiet: true,
|
||||
Debug: true,
|
||||
|
||||
// Session
|
||||
SessionPath: "./session.jsonl",
|
||||
SessionDir: "/custom/sessions/",
|
||||
Continue: true,
|
||||
NoSession: true,
|
||||
|
||||
// Tools
|
||||
Tools: []kit.Tool{...}, // Replace default tool set entirely
|
||||
ExtraTools: []kit.Tool{...}, // Add tools alongside defaults
|
||||
|
||||
// Compaction
|
||||
AutoCompact: true,
|
||||
|
||||
// Skills
|
||||
Skills: []string{"/path/to/skill.md"},
|
||||
SkillsDir: "/path/to/skills/",
|
||||
})
|
||||
```
|
||||
|
||||
## Options fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `Model` | `string` | config default | Model string (provider/model format) |
|
||||
| `SystemPrompt` | `string` | — | System prompt text or file path |
|
||||
| `ConfigFile` | `string` | `~/.kit.yml` | Path to config file |
|
||||
| `MaxSteps` | `int` | `0` | Max agent steps (0 = unlimited) |
|
||||
| `Streaming` | `bool` | `true` | Enable streaming output |
|
||||
| `Quiet` | `bool` | `false` | Suppress output |
|
||||
| `Debug` | `bool` | `false` | Enable debug logging |
|
||||
| `SessionPath` | `string` | — | Open a specific session file |
|
||||
| `SessionDir` | `string` | — | Base directory for session discovery |
|
||||
| `Continue` | `bool` | `false` | Resume most recent session |
|
||||
| `NoSession` | `bool` | `false` | Ephemeral mode (no persistence) |
|
||||
| `Tools` | `[]Tool` | — | Replace the entire default tool set |
|
||||
| `ExtraTools` | `[]Tool` | — | Additional tools alongside core/MCP/extension tools |
|
||||
| `AutoCompact` | `bool` | `false` | Auto-compact when near context limit |
|
||||
| `CompactionOptions` | `*CompactionOptions` | — | Configuration for auto-compaction |
|
||||
| `Skills` | `[]string` | — | Explicit skill files/dirs to load |
|
||||
| `SkillsDir` | `string` | — | Override default skills directory |
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Go SDK
|
||||
description: Embed Kit in your Go applications.
|
||||
---
|
||||
|
||||
# Go SDK
|
||||
|
||||
The `pkg/kit` package lets you embed Kit as a library in your Go applications.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/mark3labs/kit/pkg/kit
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
kit "github.com/mark3labs/kit/pkg/kit"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create Kit instance with default configuration
|
||||
host, err := kit.New(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer host.Close()
|
||||
|
||||
// Send a prompt
|
||||
response, err := host.Prompt(ctx, "What is 2+2?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
println(response)
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-turn conversations
|
||||
|
||||
Conversations retain context automatically across calls:
|
||||
|
||||
```go
|
||||
host.Prompt(ctx, "My name is Alice")
|
||||
response, _ := host.Prompt(ctx, "What's my name?")
|
||||
// response: "Your name is Alice"
|
||||
```
|
||||
|
||||
## Additional prompt methods
|
||||
|
||||
The SDK provides several prompt variants:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `Prompt(ctx, message)` | Simple prompt, returns response string |
|
||||
| `PromptWithCallbacks(ctx, message, ...)` | With tool call and streaming callbacks |
|
||||
| `PromptWithOptions(ctx, message, opts)` | With per-call options |
|
||||
| `PromptResult(ctx, message)` | Returns full `TurnResult` with usage stats |
|
||||
| `PromptResultWithFiles(ctx, message, files)` | Multimodal with file attachments |
|
||||
| `Steer(ctx, instruction)` | System-level steering without user message |
|
||||
| `FollowUp(ctx, text)` | Continue without new user input |
|
||||
|
||||
## Event system
|
||||
|
||||
Subscribe to events for monitoring:
|
||||
|
||||
```go
|
||||
unsubscribe := host.OnToolCall(func(event kit.ToolCallEvent) {
|
||||
fmt.Println("Tool called:", event.Name)
|
||||
})
|
||||
defer unsubscribe()
|
||||
|
||||
host.OnToolResult(func(event kit.ToolResultEvent) {
|
||||
fmt.Println("Tool result:", event.Name)
|
||||
})
|
||||
|
||||
host.OnStreaming(func(event kit.MessageUpdateEvent) {
|
||||
fmt.Print(event.Chunk)
|
||||
})
|
||||
```
|
||||
|
||||
## Model management
|
||||
|
||||
Switch models at runtime:
|
||||
|
||||
```go
|
||||
host.SetModel(ctx, "openai/gpt-4o")
|
||||
info := host.GetModelInfo()
|
||||
models := host.GetAvailableModels()
|
||||
```
|
||||
|
||||
## Context and compaction
|
||||
|
||||
Monitor and manage context usage:
|
||||
|
||||
```go
|
||||
tokens := host.EstimateContextTokens()
|
||||
stats := host.GetContextStats()
|
||||
|
||||
if host.ShouldCompact() {
|
||||
result, err := host.Compact(ctx, nil, "")
|
||||
}
|
||||
```
|
||||
|
||||
See [Options](/sdk/options), [Callbacks](/sdk/callbacks), and [Sessions](/sdk/sessions) for more details.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: SDK Sessions
|
||||
description: Session management in the Kit Go SDK.
|
||||
---
|
||||
|
||||
# SDK Sessions
|
||||
|
||||
## Automatic persistence
|
||||
|
||||
By default, Kit automatically persists sessions to JSONL files. Multi-turn conversations retain context across calls:
|
||||
|
||||
```go
|
||||
host.Prompt(ctx, "My name is Alice")
|
||||
response, _ := host.Prompt(ctx, "What's my name?")
|
||||
// response: "Your name is Alice"
|
||||
```
|
||||
|
||||
## Accessing session info
|
||||
|
||||
```go
|
||||
// Get the current session file path
|
||||
path := host.GetSessionPath()
|
||||
|
||||
// Get the session ID
|
||||
id := host.GetSessionID()
|
||||
|
||||
// Get the current model string
|
||||
model := host.GetModelString()
|
||||
```
|
||||
|
||||
## Configuring sessions via Options
|
||||
|
||||
Session behavior is configured at initialization:
|
||||
|
||||
```go
|
||||
// Open a specific session file
|
||||
host, _ := kit.New(ctx, &kit.Options{
|
||||
SessionPath: "./my-session.jsonl",
|
||||
})
|
||||
|
||||
// Resume the most recent session for the current directory
|
||||
host, _ := kit.New(ctx, &kit.Options{
|
||||
Continue: true,
|
||||
})
|
||||
|
||||
// Ephemeral mode (no file persistence)
|
||||
host, _ := kit.New(ctx, &kit.Options{
|
||||
NoSession: true,
|
||||
})
|
||||
|
||||
// Custom session directory
|
||||
host, _ := kit.New(ctx, &kit.Options{
|
||||
SessionDir: "/custom/sessions/",
|
||||
})
|
||||
```
|
||||
|
||||
## Clearing history
|
||||
|
||||
Clear the in-memory conversation history (does not delete the session file):
|
||||
|
||||
```go
|
||||
host.ClearSession()
|
||||
```
|
||||
|
||||
## Tree-based sessions
|
||||
|
||||
Kit's session model is tree-based, supporting branching. You can branch from any entry to explore alternate conversation paths:
|
||||
|
||||
```go
|
||||
// Access the tree session manager
|
||||
ts := host.GetTreeSession()
|
||||
|
||||
// Branch from a specific entry
|
||||
err := host.Branch("entry-id-123")
|
||||
```
|
||||
|
||||
## Listing and managing sessions
|
||||
|
||||
Package-level functions for session discovery:
|
||||
|
||||
```go
|
||||
// List sessions for a specific directory
|
||||
sessions := kit.ListSessions("/home/user/project")
|
||||
|
||||
// List all sessions across all directories
|
||||
all := kit.ListAllSessions()
|
||||
|
||||
// Delete a session file
|
||||
kit.DeleteSession("/path/to/session.jsonl")
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Session Management
|
||||
description: How Kit persists and manages conversation sessions.
|
||||
---
|
||||
|
||||
# Session Management
|
||||
|
||||
Kit uses a tree-based session model that supports branching and forking conversations.
|
||||
|
||||
## Session storage
|
||||
|
||||
Sessions are stored as JSONL (JSON Lines) files:
|
||||
|
||||
```
|
||||
~/.kit/sessions/<cwd-path>/<timestamp>_<id>.jsonl
|
||||
```
|
||||
|
||||
Path separators in the working directory are replaced with `--`. For example, `/home/user/project` becomes `home--user--project`.
|
||||
|
||||
Each line in the session file is a JSON entry representing a message, tool call, model change, or extension data. The tree structure allows branching from any message to explore alternate paths.
|
||||
|
||||
## Resuming sessions
|
||||
|
||||
### Continue most recent
|
||||
|
||||
Resume the most recent session for the current directory:
|
||||
|
||||
```bash
|
||||
kit --continue
|
||||
kit -c
|
||||
```
|
||||
|
||||
### Interactive picker
|
||||
|
||||
Choose from previous sessions interactively:
|
||||
|
||||
```bash
|
||||
kit --resume
|
||||
kit -r
|
||||
```
|
||||
|
||||
### Open a specific session
|
||||
|
||||
```bash
|
||||
kit --session path/to/session.jsonl
|
||||
kit -s path/to/session.jsonl
|
||||
```
|
||||
|
||||
## Ephemeral mode
|
||||
|
||||
Run without creating a session file:
|
||||
|
||||
```bash
|
||||
kit --no-session
|
||||
```
|
||||
|
||||
This is useful for one-off prompts, scripting, and subagent patterns where persistence isn't needed.
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: Themes
|
||||
description: Customize Kit's appearance with built-in themes, custom theme files, and the extension theme API.
|
||||
---
|
||||
|
||||
# Themes
|
||||
|
||||
Kit ships with 22 built-in color themes and supports custom themes via YAML/JSON files or the extension API. Themes control all UI colors: input borders, popups, system messages, markdown rendering, syntax highlighting, and diff displays.
|
||||
|
||||
## Quick start
|
||||
|
||||
Switch themes at runtime with the `/theme` command:
|
||||
|
||||
```
|
||||
/theme dracula
|
||||
/theme catppuccin
|
||||
/theme kitt
|
||||
```
|
||||
|
||||
Run `/theme` with no arguments to list all available themes.
|
||||
|
||||
## Built-in themes
|
||||
|
||||
| Theme | Style |
|
||||
|-------|-------|
|
||||
| `kitt` | KITT-inspired reds and ambers (default) |
|
||||
| `catppuccin` | Soothing pastels (Mocha/Latte) |
|
||||
| `dracula` | Purple and cyan dark theme |
|
||||
| `tokyonight` | Cool blues with warm accents |
|
||||
| `nord` | Arctic, north-bluish palette |
|
||||
| `gruvbox` | Retro groove colors |
|
||||
| `monokai` | Classic syntax theme |
|
||||
| `solarized` | Precision colors for machines and people |
|
||||
| `github` | GitHub's light and dark palettes |
|
||||
| `one-dark` | Atom One Dark |
|
||||
| `rose-pine` | Soho vibes with muted tones |
|
||||
| `ayu` | Simple with bright colors |
|
||||
| `material` | Material Design palette |
|
||||
| `everforest` | Green-focused comfortable theme |
|
||||
| `kanagawa` | Dark theme inspired by Katsushika Hokusai |
|
||||
| `amoled` | Pure black background, vivid accents |
|
||||
| `synthwave` | Retro neon glows |
|
||||
| `vesper` | Warm minimalist dark theme |
|
||||
| `flexoki` | Inky reading palette |
|
||||
| `matrix` | Green-on-black terminal aesthetic |
|
||||
| `vercel` | Clean monochrome with blue accents |
|
||||
| `zenburn` | Low-contrast, warm dark theme |
|
||||
|
||||
All themes support both light and dark terminal modes via adaptive colors.
|
||||
|
||||
## Custom theme files
|
||||
|
||||
Create a `.yml`, `.yaml`, or `.json` file with color definitions. Kit discovers themes from two directories:
|
||||
|
||||
| Location | Scope | Precedence |
|
||||
|----------|-------|------------|
|
||||
| `~/.config/kit/themes/` | User (global) | Overrides built-ins |
|
||||
| `.kit/themes/` | Project-local | Overrides user and built-ins |
|
||||
|
||||
### Theme file format
|
||||
|
||||
A theme file defines adaptive color pairs with `light` and `dark` hex values. Any field left empty inherits from the default KITT theme.
|
||||
|
||||
```yaml
|
||||
# ~/.config/kit/themes/my-theme.yml
|
||||
|
||||
# Core semantic colors
|
||||
primary:
|
||||
light: "#8839ef"
|
||||
dark: "#cba6f7"
|
||||
secondary:
|
||||
light: "#04a5e5"
|
||||
dark: "#89dceb"
|
||||
success:
|
||||
light: "#40a02b"
|
||||
dark: "#a6e3a1"
|
||||
warning:
|
||||
light: "#df8e1d"
|
||||
dark: "#f9e2af"
|
||||
error:
|
||||
light: "#d20f39"
|
||||
dark: "#f38ba8"
|
||||
info:
|
||||
light: "#1e66f5"
|
||||
dark: "#89b4fa"
|
||||
|
||||
# Text and chrome
|
||||
text:
|
||||
light: "#4c4f69"
|
||||
dark: "#cdd6f4"
|
||||
muted:
|
||||
light: "#6c6f85"
|
||||
dark: "#a6adc8"
|
||||
very-muted:
|
||||
light: "#9ca0b0"
|
||||
dark: "#6c7086"
|
||||
background:
|
||||
light: "#eff1f5"
|
||||
dark: "#1e1e2e"
|
||||
border:
|
||||
light: "#acb0be"
|
||||
dark: "#585b70"
|
||||
muted-border:
|
||||
light: "#ccd0da"
|
||||
dark: "#313244"
|
||||
|
||||
# Semantic roles
|
||||
system:
|
||||
light: "#179299"
|
||||
dark: "#94e2d5"
|
||||
tool:
|
||||
light: "#fe640b"
|
||||
dark: "#fab387"
|
||||
accent:
|
||||
light: "#ea76cb"
|
||||
dark: "#f5c2e7"
|
||||
highlight:
|
||||
light: "#e6e9ef"
|
||||
dark: "#181825"
|
||||
|
||||
# Diff backgrounds
|
||||
diff-insert-bg:
|
||||
light: "#d5f0d5"
|
||||
dark: "#1a3a2a"
|
||||
diff-delete-bg:
|
||||
light: "#f5d5d5"
|
||||
dark: "#3a1a2a"
|
||||
diff-equal-bg:
|
||||
light: "#eceef3"
|
||||
dark: "#232336"
|
||||
diff-missing-bg:
|
||||
light: "#e4e6eb"
|
||||
dark: "#1a1a2e"
|
||||
|
||||
# Code block backgrounds
|
||||
code-bg:
|
||||
light: "#eceef3"
|
||||
dark: "#232336"
|
||||
gutter-bg:
|
||||
light: "#e4e6eb"
|
||||
dark: "#1a1a2e"
|
||||
write-bg:
|
||||
light: "#d5f0d5"
|
||||
dark: "#1a3a2a"
|
||||
|
||||
# Markdown and syntax highlighting
|
||||
markdown:
|
||||
heading:
|
||||
light: "#1e66f5"
|
||||
dark: "#89b4fa"
|
||||
link:
|
||||
light: "#1e66f5"
|
||||
dark: "#89b4fa"
|
||||
keyword:
|
||||
light: "#8839ef"
|
||||
dark: "#cba6f7"
|
||||
string:
|
||||
light: "#40a02b"
|
||||
dark: "#a6e3a1"
|
||||
number:
|
||||
light: "#fe640b"
|
||||
dark: "#fab387"
|
||||
comment:
|
||||
light: "#9ca0b0"
|
||||
dark: "#6c7086"
|
||||
```
|
||||
|
||||
### Partial themes
|
||||
|
||||
You only need to define the colors you want to change. Unspecified fields fall back to the default theme:
|
||||
|
||||
```yaml
|
||||
# Just override the primary and accent colors
|
||||
primary:
|
||||
dark: "#FF00FF"
|
||||
accent:
|
||||
dark: "#00FFFF"
|
||||
```
|
||||
|
||||
### Distributing themes
|
||||
|
||||
- **Personal**: Drop a file in `~/.config/kit/themes/`
|
||||
- **Team/project**: Drop a file in `.kit/themes/` and commit it to version control
|
||||
- **Override built-in**: Name your file the same as a built-in (e.g., `dracula.yml`) and it takes precedence
|
||||
|
||||
## Config file theme
|
||||
|
||||
You can also set theme colors directly in `.kit.yml`:
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
primary:
|
||||
light: "#8839ef"
|
||||
dark: "#cba6f7"
|
||||
error:
|
||||
dark: "#FF0000"
|
||||
```
|
||||
|
||||
Or reference an external theme file:
|
||||
|
||||
```yaml
|
||||
theme: "./themes/my-custom-theme.yml"
|
||||
```
|
||||
|
||||
## Extension theme API
|
||||
|
||||
Extensions can register and switch themes programmatically at runtime.
|
||||
|
||||
### Registering a theme
|
||||
|
||||
```go
|
||||
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
|
||||
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
|
||||
Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
|
||||
Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
|
||||
Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
|
||||
Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
|
||||
Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
|
||||
Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
|
||||
MdKeyword: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
|
||||
MdString: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
|
||||
MdComment: ext.ThemeColor{Light: "#888888", Dark: "#555555"},
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Switching themes
|
||||
|
||||
```go
|
||||
err := ctx.SetTheme("dracula")
|
||||
```
|
||||
|
||||
### Listing available themes
|
||||
|
||||
```go
|
||||
names := ctx.ListThemes()
|
||||
```
|
||||
|
||||
### ThemeColorConfig fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `Primary` | Main brand/accent color |
|
||||
| `Secondary` | Secondary accent |
|
||||
| `Success` | Success states |
|
||||
| `Warning` | Warning states |
|
||||
| `Error` | Error/critical states |
|
||||
| `Info` | Informational states |
|
||||
| `Text` | Primary text |
|
||||
| `Muted` | Dimmed text |
|
||||
| `VeryMuted` | Very dimmed text |
|
||||
| `Background` | Base background |
|
||||
| `Border` | Panel borders |
|
||||
| `MutedBorder` | Subtle dividers |
|
||||
| `System` | System messages |
|
||||
| `Tool` | Tool-related elements |
|
||||
| `Accent` | Secondary highlight |
|
||||
| `Highlight` | Highlighted regions |
|
||||
| `MdHeading` | Markdown headings |
|
||||
| `MdLink` | Markdown links |
|
||||
| `MdKeyword` | Syntax: keywords |
|
||||
| `MdString` | Syntax: strings |
|
||||
| `MdNumber` | Syntax: numbers |
|
||||
| `MdComment` | Syntax: comments |
|
||||
|
||||
Each field is an `ext.ThemeColor` with `Light` and `Dark` hex strings. Empty fields inherit from the default theme.
|
||||
|
||||
## Precedence order
|
||||
|
||||
When multiple sources define the same theme name, later sources win:
|
||||
|
||||
1. Built-in presets (lowest)
|
||||
2. User themes (`~/.config/kit/themes/`)
|
||||
3. Project-local themes (`.kit/themes/`)
|
||||
4. Extension-registered themes (highest)
|
||||
|
||||
Theme changes via `/theme` or `ctx.SetTheme()` take effect immediately on all UI elements, including previously rendered messages.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 329 KiB |
@@ -0,0 +1,30 @@
|
||||
:root,
|
||||
:root[data-theme="dark"],
|
||||
:root[data-theme="light"],
|
||||
html,
|
||||
html[data-theme="dark"],
|
||||
#tome-root,
|
||||
#tome-root *,
|
||||
[data-theme],
|
||||
body,
|
||||
div {
|
||||
--bg: #08080a !important;
|
||||
--sf: #0e0e12 !important;
|
||||
--sfH: #141418 !important;
|
||||
--bd: #1a1a22 !important;
|
||||
--tx: #e8e0e0 !important;
|
||||
--tx2: #8a8090 !important;
|
||||
--txM: #6a6070 !important;
|
||||
--ac: #e03030 !important;
|
||||
--acD: rgba(224, 48, 48, 0.12) !important;
|
||||
--acT: #ff4444 !important;
|
||||
--cdBg: #0a0a0e !important;
|
||||
--cdTx: #c8a0a0 !important;
|
||||
--sbBg: #0a0a0d !important;
|
||||
--hdBg: rgba(8, 8, 10, 0.92) !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #e8e0e0 !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/** @type {import('@tomehq/core').TomeConfig} */
|
||||
export default {
|
||||
name: "Kit",
|
||||
logo: "/logo.jpg",
|
||||
favicon: "/logo.jpg",
|
||||
baseUrl: "https://go-kit.dev",
|
||||
theme: {
|
||||
preset: "cipher",
|
||||
accent: "#e03030",
|
||||
mode: "dark",
|
||||
fonts: {
|
||||
heading: "Space Grotesk",
|
||||
body: "Space Grotesk",
|
||||
code: "Source Code Pro",
|
||||
},
|
||||
},
|
||||
navigation: [
|
||||
{
|
||||
group: "Getting Started",
|
||||
pages: ["index", "installation", "quick-start"],
|
||||
},
|
||||
{
|
||||
group: "Configuration",
|
||||
pages: ["configuration", "providers", "themes"],
|
||||
},
|
||||
{
|
||||
group: "CLI Reference",
|
||||
pages: ["cli/flags", "cli/commands"],
|
||||
},
|
||||
{
|
||||
group: "Extensions",
|
||||
pages: [
|
||||
"extensions/overview",
|
||||
"extensions/capabilities",
|
||||
"extensions/examples",
|
||||
"extensions/loading",
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Sessions",
|
||||
pages: ["sessions"],
|
||||
},
|
||||
{
|
||||
group: "Go SDK",
|
||||
pages: ["sdk/overview", "sdk/options", "sdk/callbacks", "sdk/sessions"],
|
||||
},
|
||||
{
|
||||
group: "Advanced",
|
||||
pages: ["advanced/subagents", "advanced/json-output", "advanced/testing"],
|
||||
},
|
||||
{
|
||||
group: "Development",
|
||||
pages: ["development"],
|
||||
},
|
||||
],
|
||||
socialLinks: [
|
||||
{ platform: "github", url: "https://github.com/mark3labs/kit" },
|
||||
{ platform: "discord", url: "https://discord.gg/RqSS2NQVsY" },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user