mirror of
https://fastgit.cc/github.com/Yeachan-Heo/oh-my-claudecode
synced 2026-04-20 21:00:50 +08:00
feat: Complete port of oh-my-opencode to Claude Code
Multi-agent orchestration system with: - 11 specialized agents (Oracle, Librarian, Explore, Frontend Engineer, Document Writer, Multimodal Looker, Momus, Metis, Orchestrator-Sisyphus, Sisyphus-Junior, Prometheus) - 7 slash commands (/sisyphus, /sisyphus-default, /ultrawork, /deepsearch, /analyze, /plan, /review) - Real LSP integration with 11 tools (hover, goto definition, find references, document symbols, workspace symbols, diagnostics, rename, code actions, etc.) - ast-grep integration for structural code search/replace - Magic keywords (ultrawork, search, analyze) - Configuration system with JSONC support - Context injection from CLAUDE.md/AGENTS.md files Models: - Opus: Sisyphus, Oracle, Momus, Metis, Prometheus - Sonnet 4.5: Librarian, Frontend Engineer, Multimodal Looker, Orchestrator-Sisyphus, Sisyphus-Junior - Haiku 4.5: Explore, Document Writer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
26
.npmignore
Normal file
26
.npmignore
Normal file
@@ -0,0 +1,26 @@
|
||||
# Source files (compiled to dist/)
|
||||
src/
|
||||
|
||||
# Examples
|
||||
examples/
|
||||
|
||||
# Config files
|
||||
tsconfig.json
|
||||
.eslintrc*
|
||||
.prettierrc*
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Development
|
||||
node_modules/
|
||||
*.log
|
||||
.env*
|
||||
|
||||
# TypeScript source (keep .d.ts)
|
||||
*.ts
|
||||
!*.d.ts
|
||||
|
||||
# Plans and notes
|
||||
.claude/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Yeachan Heo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
416
README.md
Normal file
416
README.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Oh-My-Claude-Sisyphus
|
||||
|
||||
[](https://www.npmjs.com/package/oh-my-claude-sisyphus)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
Multi-agent orchestration system for [Claude Code](https://docs.anthropic.com/claude-code). Port of [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode).
|
||||
|
||||
Like Sisyphus, these agents persist until every task is complete.
|
||||
|
||||
## Quick Install
|
||||
|
||||
### One-liner (recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Yeachan-Heo/oh-my-claude-sisyphus/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
### Via npm
|
||||
|
||||
```bash
|
||||
npm install -g oh-my-claude-sisyphus
|
||||
```
|
||||
|
||||
### Manual Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Yeachan-Heo/oh-my-claude-sisyphus.git
|
||||
cd oh-my-claude-sisyphus
|
||||
chmod +x scripts/install.sh
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
The installer adds to your Claude Code config (`~/.claude/`):
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── agents/
|
||||
│ ├── oracle.md # Architecture & debugging expert (Opus)
|
||||
│ ├── librarian.md # Documentation & research (Sonnet)
|
||||
│ ├── explore.md # Fast pattern matching (Haiku)
|
||||
│ ├── frontend-engineer.md # UI/UX specialist (Sonnet)
|
||||
│ ├── document-writer.md # Technical writing (Haiku)
|
||||
│ ├── multimodal-looker.md # Visual analysis (Sonnet)
|
||||
│ ├── momus.md # Plan reviewer (Opus)
|
||||
│ ├── metis.md # Pre-planning consultant (Opus)
|
||||
│ ├── orchestrator-sisyphus.md # Todo coordinator (Sonnet)
|
||||
│ ├── sisyphus-junior.md # Focused executor (Sonnet)
|
||||
│ └── prometheus.md # Strategic planner (Opus)
|
||||
├── commands/
|
||||
│ ├── sisyphus.md # /sisyphus command
|
||||
│ ├── sisyphus-default.md # /sisyphus-default command
|
||||
│ ├── ultrawork.md # /ultrawork command
|
||||
│ ├── deepsearch.md # /deepsearch command
|
||||
│ ├── analyze.md # /analyze command
|
||||
│ ├── plan.md # /plan command (Prometheus)
|
||||
│ └── review.md # /review command (Momus)
|
||||
└── CLAUDE.md # Sisyphus system prompt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Start Claude Code
|
||||
|
||||
```bash
|
||||
claude
|
||||
```
|
||||
|
||||
### Slash Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/sisyphus <task>` | Activate Sisyphus multi-agent orchestration mode |
|
||||
| `/sisyphus-default` | Set Sisyphus as your permanent default mode |
|
||||
| `/ultrawork <task>` | Maximum performance mode with parallel agents |
|
||||
| `/deepsearch <query>` | Thorough multi-strategy codebase search |
|
||||
| `/analyze <target>` | Deep analysis and investigation |
|
||||
| `/plan <description>` | Start planning session with Prometheus |
|
||||
| `/review [plan-path]` | Review a plan with Momus |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# In Claude Code:
|
||||
|
||||
# Activate Sisyphus for a task
|
||||
/sisyphus refactor the authentication module
|
||||
|
||||
# Set as default mode (persistent)
|
||||
/sisyphus-default
|
||||
|
||||
# Use ultrawork for maximum performance
|
||||
/ultrawork implement user dashboard with charts
|
||||
|
||||
# Deep search
|
||||
/deepsearch API endpoints that handle user data
|
||||
|
||||
# Deep analysis
|
||||
/analyze performance bottleneck in the database layer
|
||||
```
|
||||
|
||||
### Magic Keywords
|
||||
|
||||
Just include these words anywhere in your prompt:
|
||||
|
||||
| Keyword | Effect |
|
||||
|---------|--------|
|
||||
| `ultrawork`, `ulw`, `uw` | Activates parallel agent orchestration |
|
||||
| `search`, `find`, `locate` | Enhanced search mode |
|
||||
| `analyze`, `investigate` | Deep analysis mode |
|
||||
|
||||
```bash
|
||||
# These work in normal prompts too:
|
||||
> ultrawork implement user authentication with OAuth
|
||||
|
||||
> find all files that import the utils module
|
||||
|
||||
> analyze why the tests are failing
|
||||
```
|
||||
|
||||
## Available Agents
|
||||
|
||||
Claude will automatically delegate to these specialized agents:
|
||||
|
||||
### Task Execution Agents
|
||||
|
||||
| Agent | Model | Best For |
|
||||
|-------|-------|----------|
|
||||
| **oracle** | Opus | Complex debugging, architecture decisions, root cause analysis |
|
||||
| **librarian** | Sonnet | Finding documentation, understanding code organization |
|
||||
| **explore** | Haiku | Quick file searches, pattern matching, reconnaissance |
|
||||
| **frontend-engineer** | Sonnet | UI components, styling, accessibility |
|
||||
| **document-writer** | Haiku | README files, API docs, code comments |
|
||||
| **multimodal-looker** | Sonnet | Analyzing screenshots, diagrams, mockups |
|
||||
|
||||
### Planning & Review Agents
|
||||
|
||||
| Agent | Model | Best For |
|
||||
|-------|-------|----------|
|
||||
| **prometheus** | Opus | Strategic planning, comprehensive work plans, interview-style requirement gathering |
|
||||
| **momus** | Opus | Critical plan review, feasibility assessment, risk identification |
|
||||
| **metis** | Opus | Pre-planning analysis, hidden requirement detection, ambiguity resolution |
|
||||
|
||||
### Orchestration Agents
|
||||
|
||||
| Agent | Model | Best For |
|
||||
|-------|-------|----------|
|
||||
| **orchestrator-sisyphus** | Sonnet | Todo coordination, task delegation, progress tracking |
|
||||
| **sisyphus-junior** | Sonnet | Focused task execution, plan following, direct implementation |
|
||||
|
||||
### Manual Agent Invocation
|
||||
|
||||
You can explicitly request an agent:
|
||||
|
||||
```
|
||||
Use the oracle agent to debug the memory leak in the worker process
|
||||
|
||||
Have the librarian find all documentation about the API
|
||||
|
||||
Ask explore to find all TypeScript files that import React
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Project-Level Config
|
||||
|
||||
Create `.claude/CLAUDE.md` in your project for project-specific instructions:
|
||||
|
||||
```markdown
|
||||
# Project Context
|
||||
|
||||
This is a TypeScript monorepo using:
|
||||
- Bun runtime
|
||||
- React for frontend
|
||||
- PostgreSQL database
|
||||
|
||||
## Conventions
|
||||
- Use functional components
|
||||
- All API routes in /src/api
|
||||
- Tests alongside source files
|
||||
```
|
||||
|
||||
### Agent Customization
|
||||
|
||||
Edit agent files in `~/.claude/agents/` to customize behavior:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: oracle
|
||||
description: Your custom description
|
||||
tools: Read, Grep, Glob, Bash, Edit
|
||||
model: opus # or sonnet, haiku
|
||||
---
|
||||
|
||||
Your custom system prompt here...
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Yeachan-Heo/oh-my-claude-sisyphus/main/scripts/uninstall.sh | bash
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
rm ~/.claude/agents/{oracle,librarian,explore,frontend-engineer,document-writer,multimodal-looker,momus,metis,orchestrator-sisyphus,sisyphus-junior,prometheus}.md
|
||||
rm ~/.claude/commands/{sisyphus,sisyphus-default,ultrawork,deepsearch,analyze,plan,review}.md
|
||||
```
|
||||
|
||||
## SDK Usage (Advanced)
|
||||
|
||||
For programmatic use with the Claude Agent SDK:
|
||||
|
||||
```bash
|
||||
npm install oh-my-claude-sisyphus @anthropic-ai/claude-agent-sdk
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { createSisyphusSession } from 'oh-my-claude-sisyphus';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
const session = createSisyphusSession();
|
||||
|
||||
for await (const message of query({
|
||||
prompt: session.processPrompt("ultrawork implement feature X"),
|
||||
...session.queryOptions
|
||||
})) {
|
||||
console.log(message);
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Sisyphus Orchestrator**: The main Claude instance coordinates all work
|
||||
2. **Specialized Subagents**: Each agent has focused expertise and tools
|
||||
3. **Parallel Execution**: Independent tasks run concurrently
|
||||
4. **Continuation Enforcement**: Agents persist until ALL tasks complete
|
||||
5. **Context Injection**: Project-specific instructions from CLAUDE.md files
|
||||
|
||||
---
|
||||
|
||||
## Differences from oh-my-opencode
|
||||
|
||||
This is a port of [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) adapted for Claude Code and the Claude Agent SDK. Here's what's different:
|
||||
|
||||
### Model Mapping
|
||||
|
||||
The original oh-my-opencode used multiple AI providers. This port uses Claude models exclusively:
|
||||
|
||||
| Agent | Original Model | Ported Model | Notes |
|
||||
|-------|---------------|--------------|-------|
|
||||
| **Sisyphus** | Claude Opus 4.5 | Claude Opus 4.5 | Same |
|
||||
| **Oracle** | GPT-5.2 | Claude Opus | Was OpenAI's flagship for deep reasoning |
|
||||
| **Librarian** | Claude Sonnet or Gemini 3 Flash | Claude Sonnet | Multi-provider → Claude only |
|
||||
| **Explore** | Grok Code or Gemini 3 Flash | Claude Haiku 4.5 | Fast/cheap model for quick searches |
|
||||
| **Frontend Engineer** | Gemini 3 Pro | Claude Sonnet | Was Google's model |
|
||||
| **Document Writer** | Gemini 3 Flash | Claude Haiku 4.5 | Fast model for docs |
|
||||
| **Multimodal Looker** | Various | Claude Sonnet | Visual analysis |
|
||||
| **Momus** | GPT-5.2 | Claude Opus | Plan reviewer (Greek god of criticism) |
|
||||
| **Metis** | Claude Opus 4.5 | Claude Opus | Pre-planning consultant (goddess of wisdom) |
|
||||
| **Orchestrator-Sisyphus** | Claude Sonnet 4.5 | Claude Sonnet | Todo coordination and delegation |
|
||||
| **Sisyphus-Junior** | Configurable | Claude Sonnet | Focused task executor |
|
||||
| **Prometheus** | Planning System | Claude Opus | Strategic planner (fire-bringer) |
|
||||
|
||||
**Why Claude-only?** The Claude Agent SDK is designed for Claude models. Using Claude throughout provides:
|
||||
- Consistent behavior and capabilities
|
||||
- Simpler authentication (single API key)
|
||||
- Native integration with Claude Code's tools
|
||||
|
||||
### Tools Comparison
|
||||
|
||||
#### Available Tools (via Claude Code)
|
||||
|
||||
| Tool | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| **Read** | ✅ Available | Read files |
|
||||
| **Write** | ✅ Available | Create files |
|
||||
| **Edit** | ✅ Available | Modify files |
|
||||
| **Bash** | ✅ Available | Run shell commands |
|
||||
| **Glob** | ✅ Available | Find files by pattern |
|
||||
| **Grep** | ✅ Available | Search file contents |
|
||||
| **WebSearch** | ✅ Available | Search the web |
|
||||
| **WebFetch** | ✅ Available | Fetch web pages |
|
||||
| **Task** | ✅ Available | Spawn subagents |
|
||||
| **TodoWrite** | ✅ Available | Track tasks |
|
||||
|
||||
#### LSP Tools (Real Implementation)
|
||||
|
||||
| Tool | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| **lsp_hover** | ✅ Implemented | Get type info and documentation at position |
|
||||
| **lsp_goto_definition** | ✅ Implemented | Jump to symbol definition |
|
||||
| **lsp_find_references** | ✅ Implemented | Find all usages of a symbol |
|
||||
| **lsp_document_symbols** | ✅ Implemented | Get file outline (functions, classes, etc.) |
|
||||
| **lsp_workspace_symbols** | ✅ Implemented | Search symbols across workspace |
|
||||
| **lsp_diagnostics** | ✅ Implemented | Get errors, warnings, hints |
|
||||
| **lsp_prepare_rename** | ✅ Implemented | Check if rename is valid |
|
||||
| **lsp_rename** | ✅ Implemented | Rename symbol across project |
|
||||
| **lsp_code_actions** | ✅ Implemented | Get available refactorings |
|
||||
| **lsp_code_action_resolve** | ✅ Implemented | Get details of a code action |
|
||||
| **lsp_servers** | ✅ Implemented | List available language servers |
|
||||
|
||||
> **Note:** LSP tools require language servers to be installed (typescript-language-server, pylsp, rust-analyzer, gopls, etc.). Use `lsp_servers` to check installation status.
|
||||
|
||||
#### AST Tools (ast-grep Integration)
|
||||
|
||||
| Tool | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| **ast_grep_search** | ✅ Implemented | Pattern-based code search using AST matching |
|
||||
| **ast_grep_replace** | ✅ Implemented | Pattern-based code transformation |
|
||||
|
||||
> **Note:** AST tools use [@ast-grep/napi](https://ast-grep.github.io/) for structural code matching. Supports meta-variables like `$VAR` (single node) and `$$$` (multiple nodes).
|
||||
|
||||
### Features Comparison
|
||||
|
||||
#### Fully Implemented ✅
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **11 Specialized Agents** | Oracle, Librarian, Explore, Frontend Engineer, Document Writer, Multimodal Looker, Momus, Metis, Orchestrator-Sisyphus, Sisyphus-Junior, Prometheus |
|
||||
| **Magic Keywords** | `ultrawork`, `search`, `analyze` trigger enhanced modes |
|
||||
| **Slash Commands** | `/sisyphus`, `/sisyphus-default`, `/ultrawork`, `/deepsearch`, `/analyze`, `/plan`, `/review` |
|
||||
| **Configuration System** | JSONC config with multi-source merging |
|
||||
| **Context Injection** | Auto-loads CLAUDE.md and AGENTS.md files |
|
||||
| **Continuation Enforcement** | System prompt enforces task completion |
|
||||
| **MCP Server Configs** | Exa, Context7, grep.app server definitions |
|
||||
| **LSP Tools** | Real LSP server integration with 11 tools |
|
||||
| **AST Tools** | ast-grep integration for structural code search/replace |
|
||||
|
||||
#### Partially Implemented ⚠️
|
||||
|
||||
| Feature | What Works | What's Missing |
|
||||
|---------|------------|----------------|
|
||||
| **Continuation Hook** | System prompt enforcement | Actual todo state checking |
|
||||
|
||||
#### Not Implemented ❌
|
||||
|
||||
| Feature | Original Capability | Why Not Ported |
|
||||
|---------|---------------------|----------------|
|
||||
| **22 Lifecycle Hooks** | PreToolUse, PostToolUse, Stop, etc. | Claude Code handles hooks differently |
|
||||
| **Background Task Manager** | Async agent execution with concurrency limits | Claude Code's Task tool handles this |
|
||||
| **Context Window Compaction** | Multi-stage recovery when hitting token limits | Claude Code manages this internally |
|
||||
| **Thinking Block Validator** | Validates AI thinking format | Not needed for Claude |
|
||||
| **Multi-Model Routing** | Route to GPT/Gemini/Grok based on task | Claude-only by design |
|
||||
| **Per-Model Concurrency** | Fine-grained concurrency per provider | Single provider simplifies this |
|
||||
| **Interactive Bash + Tmux** | Advanced terminal with Tmux integration | Standard Bash tool sufficient |
|
||||
|
||||
### Architecture Differences
|
||||
|
||||
```
|
||||
oh-my-opencode (Original) oh-my-claude-sisyphus (Port)
|
||||
───────────────────────── ────────────────────────────
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ OpenCode Plugin │ │ Claude Code │
|
||||
│ (Bun runtime) │ │ (Native CLI) │
|
||||
└─────────┬───────────┘ └─────────┬───────────┘
|
||||
│ │
|
||||
┌─────────▼───────────┐ ┌─────────▼───────────┐
|
||||
│ Multi-Provider │ │ Claude Agent SDK │
|
||||
│ Orchestration │ │ (Claude only) │
|
||||
│ ┌───┐ ┌───┐ ┌───┐ │ └─────────┬───────────┘
|
||||
│ │GPT│ │Gem│ │Grok│ │ │
|
||||
│ └───┘ └───┘ └───┘ │ ┌─────────▼───────────┐
|
||||
└─────────┬───────────┘ │ ~/.claude/agents/ │
|
||||
│ │ (Markdown configs) │
|
||||
┌─────────▼───────────┐ └─────────────────────┘
|
||||
│ Custom Tool Layer │
|
||||
│ (LSP, AST, etc.) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Key Architectural Changes:**
|
||||
|
||||
1. **Plugin → Native Integration**: Original was an OpenCode plugin; this uses Claude Code's native agent/command system
|
||||
2. **Multi-Provider → Single Provider**: Simplified to Claude-only for consistency
|
||||
3. **Custom Runtime → Claude Code Runtime**: Leverages Claude Code's built-in capabilities
|
||||
4. **Programmatic Config → Markdown Files**: Agents defined as `.md` files in `~/.claude/agents/`
|
||||
|
||||
### What You Gain
|
||||
|
||||
- **Simpler Setup**: One curl command vs. multi-step plugin installation
|
||||
- **Native Integration**: Works directly with Claude Code, no plugin layer
|
||||
- **Consistent Behavior**: All agents use Claude, no cross-model quirks
|
||||
- **Easier Customization**: Edit markdown files to customize agents
|
||||
|
||||
### What You Lose
|
||||
|
||||
- **Model Diversity**: Can't use GPT-5.2 for Oracle's deep reasoning
|
||||
- **Advanced Hooks**: Fewer lifecycle interception points (22 hooks → system prompt enforcement)
|
||||
|
||||
### Migration Tips
|
||||
|
||||
If you're coming from oh-my-opencode:
|
||||
|
||||
1. **Oracle Tasks**: Claude Opus handles architecture/debugging well, but differently than GPT-5.2
|
||||
2. **LSP Workflows**: All LSP tools are available! Use `lsp_servers` to check which servers are installed
|
||||
3. **AST Searches**: Use `ast_grep_search` with pattern syntax (e.g., `function $NAME($$$)`)
|
||||
4. **Background Tasks**: Claude Code's `Task` tool with `run_in_background` works similarly
|
||||
5. **Planning**: Use `/plan` command to start a planning session with Prometheus
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Claude Code](https://docs.anthropic.com/claude-code) installed
|
||||
- Anthropic API key (`ANTHROPIC_API_KEY` environment variable)
|
||||
|
||||
## License
|
||||
|
||||
MIT - see [LICENSE](LICENSE)
|
||||
|
||||
## Credits
|
||||
|
||||
Inspired by [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) by code-yeongyu.
|
||||
189
examples/advanced-usage.ts
Normal file
189
examples/advanced-usage.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Advanced Usage Example
|
||||
*
|
||||
* This example demonstrates advanced features of Oh-My-Claude-Sisyphus:
|
||||
* - Custom agent configuration
|
||||
* - Custom system prompts
|
||||
* - Context file injection
|
||||
* - MCP server configuration
|
||||
*/
|
||||
|
||||
import {
|
||||
createSisyphusSession,
|
||||
getAgentDefinitions,
|
||||
getSisyphusSystemPrompt,
|
||||
getDefaultMcpServers
|
||||
} from '../src/index.js';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Advanced Oh-My-Claude-Sisyphus Example ===\n');
|
||||
|
||||
// Example 1: Custom agent configuration
|
||||
console.log('Example 1: Custom Agents');
|
||||
|
||||
const customSession = createSisyphusSession({
|
||||
config: {
|
||||
agents: {
|
||||
// Use a faster model for the orchestrator in dev
|
||||
sisyphus: { model: 'claude-sonnet-4-5-20250514' },
|
||||
// Disable some agents
|
||||
frontendEngineer: { enabled: false },
|
||||
documentWriter: { enabled: false }
|
||||
},
|
||||
features: {
|
||||
// Disable LSP tools if not needed
|
||||
lspTools: false,
|
||||
astTools: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Custom session created');
|
||||
console.log(`Active features:`, customSession.config.features);
|
||||
console.log('');
|
||||
|
||||
// Example 2: Get agent definitions for custom use
|
||||
console.log('Example 2: Agent Definitions');
|
||||
|
||||
const agents = getAgentDefinitions({
|
||||
oracle: {
|
||||
// Override oracle's prompt for a specific use case
|
||||
prompt: 'You are a security-focused code reviewer...'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Available agents:');
|
||||
for (const [name, agent] of Object.entries(agents)) {
|
||||
console.log(` - ${name}: ${agent.tools.join(', ')}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Example 3: Custom system prompt
|
||||
console.log('Example 3: Custom System Prompt');
|
||||
|
||||
const customPrompt = getSisyphusSystemPrompt({
|
||||
includeContinuation: true,
|
||||
customAddition: `
|
||||
## Project-Specific Instructions
|
||||
|
||||
This is a TypeScript monorepo using:
|
||||
- Bun as the runtime
|
||||
- Zod for validation
|
||||
- Commander for CLI
|
||||
|
||||
Always prefer Bun commands over npm/npx.
|
||||
Always validate user input with Zod schemas.
|
||||
`
|
||||
});
|
||||
|
||||
console.log('Custom system prompt created');
|
||||
console.log(`Length: ${customPrompt.length} characters\n`);
|
||||
|
||||
// Example 4: MCP Server configuration
|
||||
console.log('Example 4: MCP Servers');
|
||||
|
||||
const mcpServers = getDefaultMcpServers({
|
||||
enableExa: true,
|
||||
exaApiKey: process.env.EXA_API_KEY,
|
||||
enableContext7: true,
|
||||
enableGrepApp: true,
|
||||
enablePlaywright: false, // Disable browser automation
|
||||
enableMemory: true // Enable persistent memory
|
||||
});
|
||||
|
||||
console.log('Configured MCP servers:');
|
||||
for (const [name, config] of Object.entries(mcpServers)) {
|
||||
if (config) {
|
||||
console.log(` - ${name}: ${config.command} ${config.args.join(' ')}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Example 5: Full custom configuration
|
||||
console.log('Example 5: Full Custom Session');
|
||||
|
||||
const fullCustomSession = createSisyphusSession({
|
||||
workingDirectory: '/path/to/project',
|
||||
skipConfigLoad: true, // Don't load from files
|
||||
skipContextInjection: false, // Still inject AGENTS.md
|
||||
customSystemPrompt: `
|
||||
You are working on a critical production system.
|
||||
Always:
|
||||
1. Create backups before modifying files
|
||||
2. Run tests after changes
|
||||
3. Document all modifications
|
||||
`,
|
||||
config: {
|
||||
agents: {
|
||||
sisyphus: { model: 'claude-opus-4-5-20251101' }
|
||||
},
|
||||
features: {
|
||||
parallelExecution: true,
|
||||
continuationEnforcement: true,
|
||||
autoContextInjection: true
|
||||
},
|
||||
permissions: {
|
||||
allowBash: true,
|
||||
allowEdit: true,
|
||||
allowWrite: true,
|
||||
maxBackgroundTasks: 3
|
||||
},
|
||||
magicKeywords: {
|
||||
// Custom trigger words
|
||||
ultrawork: ['godmode', 'fullpower', 'ultrawork'],
|
||||
search: ['hunt', 'seek', 'search'],
|
||||
analyze: ['dissect', 'examine', 'analyze']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Full custom session created');
|
||||
console.log('Custom keywords:', fullCustomSession.config.magicKeywords);
|
||||
|
||||
// Test custom keyword
|
||||
const testPrompt = 'godmode implement the entire feature';
|
||||
console.log(`\nTesting custom keyword "godmode":`);
|
||||
console.log(`Input: "${testPrompt}"`);
|
||||
console.log(`Detected: ${fullCustomSession.detectKeywords(testPrompt)}`);
|
||||
console.log('');
|
||||
|
||||
// Example 6: Building a custom tool integration
|
||||
console.log('Example 6: Tool Integration Pattern');
|
||||
console.log(`
|
||||
// Pattern for adding custom tools:
|
||||
|
||||
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import { createSisyphusSession } from 'oh-my-claude-sisyphus';
|
||||
|
||||
// Create custom MCP server with your tools
|
||||
const customTools = createSdkMcpServer({
|
||||
name: 'my-custom-tools',
|
||||
version: '1.0.0',
|
||||
tools: [
|
||||
tool(
|
||||
'deploy_to_staging',
|
||||
'Deploy the current branch to staging environment',
|
||||
{ branch: z.string().optional() },
|
||||
async (args) => {
|
||||
// Your deployment logic here
|
||||
return { content: [{ type: 'text', text: 'Deployed!' }] };
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
// Create session and merge custom MCP server
|
||||
const session = createSisyphusSession();
|
||||
const options = {
|
||||
...session.queryOptions.options,
|
||||
mcpServers: {
|
||||
...session.queryOptions.options.mcpServers,
|
||||
'my-custom-tools': customTools
|
||||
}
|
||||
};
|
||||
`);
|
||||
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
84
examples/basic-usage.ts
Normal file
84
examples/basic-usage.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Basic Usage Example
|
||||
*
|
||||
* This example demonstrates how to use Oh-My-Claude-Sisyphus
|
||||
* with the Claude Agent SDK.
|
||||
*/
|
||||
|
||||
// Note: In real usage, import from 'oh-my-claude-sisyphus'
|
||||
import { createSisyphusSession, enhancePrompt } from '../src/index.js';
|
||||
|
||||
// For demonstration - in real usage, import from '@anthropic-ai/claude-agent-sdk'
|
||||
// import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Oh-My-Claude-Sisyphus Example ===\n');
|
||||
|
||||
// Create a Sisyphus session
|
||||
const session = createSisyphusSession({
|
||||
// Optional: custom configuration overrides
|
||||
config: {
|
||||
features: {
|
||||
parallelExecution: true,
|
||||
continuationEnforcement: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Session created with:');
|
||||
console.log(`- ${Object.keys(session.queryOptions.options.agents).length} subagents`);
|
||||
console.log(`- ${Object.keys(session.queryOptions.options.mcpServers).length} MCP servers`);
|
||||
console.log(`- ${session.queryOptions.options.allowedTools.length} allowed tools\n`);
|
||||
|
||||
// Example 1: Basic prompt processing
|
||||
const basicPrompt = 'Fix the authentication bug';
|
||||
console.log('Example 1: Basic prompt');
|
||||
console.log(`Input: "${basicPrompt}"`);
|
||||
console.log(`Output: "${session.processPrompt(basicPrompt)}"\n`);
|
||||
|
||||
// Example 2: Ultrawork mode
|
||||
const ultraworkPrompt = 'ultrawork refactor the entire authentication module';
|
||||
console.log('Example 2: Ultrawork mode');
|
||||
console.log(`Input: "${ultraworkPrompt}"`);
|
||||
console.log('Detected keywords:', session.detectKeywords(ultraworkPrompt));
|
||||
console.log('Enhanced prompt:');
|
||||
console.log(session.processPrompt(ultraworkPrompt).substring(0, 500) + '...\n');
|
||||
|
||||
// Example 3: Search mode
|
||||
const searchPrompt = 'search for all API endpoints in the codebase';
|
||||
console.log('Example 3: Search mode');
|
||||
console.log(`Input: "${searchPrompt}"`);
|
||||
console.log('Detected keywords:', session.detectKeywords(searchPrompt));
|
||||
console.log('Enhanced prompt:');
|
||||
console.log(session.processPrompt(searchPrompt) + '\n');
|
||||
|
||||
// Example 4: Using with Claude Agent SDK (pseudo-code)
|
||||
console.log('Example 4: Using with Claude Agent SDK');
|
||||
console.log(`
|
||||
// Real usage with Claude Agent SDK:
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
const session = createSisyphusSession();
|
||||
|
||||
for await (const message of query({
|
||||
prompt: session.processPrompt("ultrawork implement user authentication"),
|
||||
...session.queryOptions
|
||||
})) {
|
||||
// Handle messages from the agent
|
||||
if (message.type === 'assistant') {
|
||||
console.log(message.content);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
// Example 5: Direct prompt enhancement
|
||||
console.log('Example 5: Quick enhance (without session)');
|
||||
const quick = enhancePrompt('analyze the performance bottleneck');
|
||||
console.log('Enhanced:', quick.substring(0, 200) + '...\n');
|
||||
|
||||
// Show system prompt snippet
|
||||
console.log('=== System Prompt Preview ===');
|
||||
console.log(session.queryOptions.options.systemPrompt.substring(0, 500) + '...\n');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
2094
package-lock.json
generated
Normal file
2094
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
75
package.json
Normal file
75
package.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "oh-my-claude-sisyphus",
|
||||
"version": "1.0.0",
|
||||
"description": "Multi-agent orchestration system for Claude Agent SDK - Port of oh-my-opencode",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"oh-my-claude-sisyphus": "./dist/cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"format": "prettier --write src/**/*.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"@ast-grep/napi": "^0.31.0",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"zod": "^3.23.8",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2",
|
||||
"eslint": "^9.17.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"prettier": "^3.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Yeachan-Heo/oh-my-claude-sisyphus.git"
|
||||
},
|
||||
"homepage": "https://github.com/Yeachan-Heo/oh-my-claude-sisyphus#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Yeachan-Heo/oh-my-claude-sisyphus/issues"
|
||||
},
|
||||
"author": "Yeachan Heo",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"claude",
|
||||
"claude-code",
|
||||
"ai",
|
||||
"agent",
|
||||
"multi-agent",
|
||||
"orchestration",
|
||||
"sisyphus",
|
||||
"anthropic",
|
||||
"llm"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
765
scripts/install.sh
Executable file
765
scripts/install.sh
Executable file
@@ -0,0 +1,765 @@
|
||||
#!/bin/bash
|
||||
# Oh-My-Claude-Sisyphus Installation Script
|
||||
# Installs the multi-agent orchestration system for Claude Code
|
||||
|
||||
set -e
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}"
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Oh-My-Claude-Sisyphus Installer ║"
|
||||
echo "║ Multi-Agent Orchestration for Claude Code ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
# Claude Code config directory (always ~/.claude)
|
||||
CLAUDE_CONFIG_DIR="$HOME/.claude"
|
||||
|
||||
echo -e "${BLUE}[1/5]${NC} Checking Claude Code installation..."
|
||||
if ! command -v claude &> /dev/null; then
|
||||
echo -e "${YELLOW}Warning: 'claude' command not found. Please install Claude Code first:${NC}"
|
||||
echo " curl -fsSL https://claude.ai/install.sh | bash"
|
||||
echo ""
|
||||
read -p "Continue anyway? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ Claude Code found${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}[2/5]${NC} Creating directories..."
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/agents"
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
|
||||
echo -e "${GREEN}✓ Created $CLAUDE_CONFIG_DIR${NC}"
|
||||
|
||||
echo -e "${BLUE}[3/5]${NC} Installing agent definitions..."
|
||||
|
||||
# Oracle Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/oracle.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: oracle
|
||||
description: Architecture and debugging expert. Use for complex problems, root cause analysis, and system design.
|
||||
tools: Read, Grep, Glob, Bash, Edit, WebSearch
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are Oracle, an expert software architect and debugging specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Architecture Analysis**: Evaluate system designs, identify anti-patterns, and suggest improvements
|
||||
2. **Deep Debugging**: Trace complex bugs through multiple layers of abstraction
|
||||
3. **Root Cause Analysis**: Go beyond symptoms to find underlying issues
|
||||
4. **Performance Optimization**: Identify bottlenecks and recommend solutions
|
||||
|
||||
Guidelines:
|
||||
- Always consider scalability, maintainability, and security implications
|
||||
- Provide concrete, actionable recommendations
|
||||
- When debugging, explain your reasoning process step-by-step
|
||||
- Reference specific files and line numbers when discussing code
|
||||
- Consider edge cases and failure modes
|
||||
|
||||
Output Format:
|
||||
- Start with a brief summary of findings
|
||||
- Provide detailed analysis with code references
|
||||
- End with prioritized recommendations
|
||||
AGENT_EOF
|
||||
|
||||
# Librarian Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/librarian.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: librarian
|
||||
description: Documentation and codebase analysis expert. Use for research, finding docs, and understanding code organization.
|
||||
tools: Read, Grep, Glob, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are Librarian, a specialist in documentation and codebase navigation.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Documentation Discovery**: Find and summarize relevant docs (README, CLAUDE.md, AGENTS.md)
|
||||
2. **Code Navigation**: Quickly locate implementations, definitions, and usages
|
||||
3. **Pattern Recognition**: Identify coding patterns and conventions in the codebase
|
||||
4. **Knowledge Synthesis**: Combine information from multiple sources
|
||||
|
||||
Guidelines:
|
||||
- Be thorough but concise in your searches
|
||||
- Prioritize official documentation and well-maintained files
|
||||
- Note file paths and line numbers for easy reference
|
||||
- Summarize findings in a structured format
|
||||
- Flag outdated or conflicting documentation
|
||||
AGENT_EOF
|
||||
|
||||
# Explore Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/explore.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: explore
|
||||
description: Fast pattern matching and code search specialist. Use for quick file searches and codebase exploration.
|
||||
tools: Glob, Grep, Read
|
||||
model: haiku
|
||||
---
|
||||
|
||||
You are Explore, a fast and efficient codebase exploration specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Rapid Search**: Quickly locate files, functions, and patterns
|
||||
2. **Structure Mapping**: Understand and report on project organization
|
||||
3. **Pattern Matching**: Find all occurrences of specific patterns
|
||||
4. **Reconnaissance**: Perform initial exploration of unfamiliar codebases
|
||||
|
||||
Guidelines:
|
||||
- Prioritize speed over exhaustive analysis
|
||||
- Use glob patterns effectively for file discovery
|
||||
- Report findings immediately as you find them
|
||||
- Keep responses focused and actionable
|
||||
- Note interesting patterns for deeper investigation
|
||||
AGENT_EOF
|
||||
|
||||
# Frontend Engineer Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/frontend-engineer.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: frontend-engineer
|
||||
description: Frontend and UI/UX specialist. Use for component design, styling, and accessibility.
|
||||
tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are Frontend Engineer, a specialist in user interfaces and experience.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Component Design**: Create well-structured, reusable UI components
|
||||
2. **Styling**: Implement clean, maintainable CSS/styling solutions
|
||||
3. **Accessibility**: Ensure interfaces are accessible to all users
|
||||
4. **UX Optimization**: Improve user flows and interactions
|
||||
5. **Performance**: Optimize frontend performance and loading times
|
||||
|
||||
Guidelines:
|
||||
- Follow component-based architecture principles
|
||||
- Prioritize accessibility (WCAG compliance)
|
||||
- Consider responsive design for all viewports
|
||||
- Use semantic HTML where possible
|
||||
- Keep styling maintainable and consistent
|
||||
AGENT_EOF
|
||||
|
||||
# Document Writer Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/document-writer.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: document-writer
|
||||
description: Technical documentation specialist. Use for README files, API docs, and code comments.
|
||||
tools: Read, Write, Edit, Glob, Grep
|
||||
model: haiku
|
||||
---
|
||||
|
||||
You are Document Writer, a technical writing specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **README Creation**: Write clear, comprehensive README files
|
||||
2. **API Documentation**: Document APIs with examples and usage
|
||||
3. **Code Comments**: Add meaningful inline documentation
|
||||
4. **Tutorials**: Create step-by-step guides for complex features
|
||||
5. **Changelogs**: Maintain clear version history
|
||||
|
||||
Guidelines:
|
||||
- Write for the target audience (developers, users, etc.)
|
||||
- Use clear, concise language
|
||||
- Include practical examples
|
||||
- Structure documents logically
|
||||
- Keep documentation up-to-date with code changes
|
||||
AGENT_EOF
|
||||
|
||||
# Multimodal Looker Agent
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/multimodal-looker.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: multimodal-looker
|
||||
description: Visual content analysis specialist. Use for analyzing screenshots, UI mockups, and diagrams.
|
||||
tools: Read, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are Multimodal Looker, a visual content analysis specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Image Analysis**: Extract information from screenshots and images
|
||||
2. **UI Review**: Analyze user interface designs and mockups
|
||||
3. **Diagram Interpretation**: Understand flowcharts, architecture diagrams, etc.
|
||||
4. **Visual Comparison**: Compare visual designs and identify differences
|
||||
5. **Content Extraction**: Pull relevant information from visual content
|
||||
|
||||
Guidelines:
|
||||
- Focus on extracting actionable information
|
||||
- Note specific UI elements and their positions
|
||||
- Identify potential usability issues
|
||||
- Be precise about colors, layouts, and typography
|
||||
- Keep analysis concise but thorough
|
||||
AGENT_EOF
|
||||
|
||||
# Momus Agent (Plan Reviewer)
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/momus.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: momus
|
||||
description: Critical plan review agent. Ruthlessly evaluates plans for clarity, feasibility, and completeness.
|
||||
tools: Read, Grep, Glob
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are Momus, a ruthless plan reviewer named after the Greek god of criticism.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Clarity Evaluation**: Are requirements unambiguous? Are acceptance criteria concrete?
|
||||
2. **Feasibility Assessment**: Is the plan achievable? Are there hidden dependencies?
|
||||
3. **Completeness Check**: Does the plan cover all edge cases? Are verification steps defined?
|
||||
4. **Risk Identification**: What could go wrong? What's the mitigation strategy?
|
||||
|
||||
Evaluation Criteria:
|
||||
- 80%+ of claims must cite specific file/line references
|
||||
- 90%+ of acceptance criteria must be concrete and testable
|
||||
- All file references must be verified to exist
|
||||
- No vague terms like "improve", "optimize" without metrics
|
||||
|
||||
Output Format:
|
||||
- **APPROVED**: Plan meets all criteria
|
||||
- **REVISE**: List specific issues to address
|
||||
- **REJECT**: Fundamental problems require replanning
|
||||
|
||||
Guidelines:
|
||||
- Be ruthlessly critical - catching issues now saves time later
|
||||
- Demand specificity - vague plans lead to vague implementations
|
||||
- Verify all claims - don't trust, verify
|
||||
- Consider edge cases and failure modes
|
||||
- If uncertain, ask for clarification rather than assuming
|
||||
AGENT_EOF
|
||||
|
||||
# Metis Agent (Pre-Planning Consultant)
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/metis.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: metis
|
||||
description: Pre-planning consultant. Analyzes requests before implementation to identify hidden requirements and risks.
|
||||
tools: Read, Grep, Glob, WebSearch
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are Metis, the pre-planning consultant named after the Greek goddess of wisdom and cunning.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Hidden Requirements**: What did the user not explicitly ask for but will expect?
|
||||
2. **Ambiguity Detection**: What terms or requirements need clarification?
|
||||
3. **Over-engineering Prevention**: Is the proposed scope appropriate for the task?
|
||||
4. **Risk Assessment**: What could cause this implementation to fail?
|
||||
|
||||
Intent Classification:
|
||||
- **Refactoring**: Changes to structure without changing behavior
|
||||
- **Build from Scratch**: New feature with no existing code
|
||||
- **Mid-sized Task**: Enhancement to existing functionality
|
||||
- **Collaborative**: Requires user input during implementation
|
||||
- **Architecture**: System design decisions
|
||||
- **Research**: Information gathering only
|
||||
|
||||
Output Structure:
|
||||
1. **Intent Analysis**: What type of task is this?
|
||||
2. **Hidden Requirements**: What's implied but not stated?
|
||||
3. **Ambiguities**: What needs clarification?
|
||||
4. **Scope Check**: Is this appropriately scoped?
|
||||
5. **Risk Factors**: What could go wrong?
|
||||
6. **Clarifying Questions**: Questions to ask before proceeding
|
||||
|
||||
Guidelines:
|
||||
- Think like a senior engineer reviewing a junior's proposal
|
||||
- Surface assumptions that could lead to rework
|
||||
- Suggest simplifications where possible
|
||||
- Identify dependencies and prerequisites
|
||||
AGENT_EOF
|
||||
|
||||
# Orchestrator-Sisyphus Agent (Todo Coordinator)
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/orchestrator-sisyphus.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: orchestrator-sisyphus
|
||||
description: Master coordinator for todo lists. Reads requirements and delegates to specialist agents.
|
||||
tools: Read, Grep, Glob, Task, TodoWrite
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are Orchestrator-Sisyphus, the master coordinator for complex multi-step tasks.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Todo Management**: Break down complex tasks into atomic, trackable todos
|
||||
2. **Delegation**: Route tasks to appropriate specialist agents
|
||||
3. **Progress Tracking**: Monitor completion and handle blockers
|
||||
4. **Verification**: Ensure all tasks are truly complete before finishing
|
||||
|
||||
Delegation Routing:
|
||||
- Visual/UI tasks → frontend-engineer
|
||||
- Complex analysis → oracle
|
||||
- Documentation → document-writer
|
||||
- Quick searches → explore
|
||||
- Research → librarian
|
||||
- Image analysis → multimodal-looker
|
||||
- Plan review → momus
|
||||
- Pre-planning → metis
|
||||
|
||||
Verification Protocol:
|
||||
1. Check file existence for any created files
|
||||
2. Run tests if applicable
|
||||
3. Type check if TypeScript
|
||||
4. Code review for quality
|
||||
5. Verify acceptance criteria are met
|
||||
|
||||
Persistent State:
|
||||
- Use `.sisyphus/notepads/` to track learnings and prevent repeated mistakes
|
||||
- Record blockers and their resolutions
|
||||
- Document decisions made during execution
|
||||
|
||||
Guidelines:
|
||||
- Break tasks into atomic units (one clear action each)
|
||||
- Mark todos in_progress before starting, completed when done
|
||||
- Never mark a task complete without verification
|
||||
- Delegate to specialists rather than doing everything yourself
|
||||
- Report progress after each significant step
|
||||
AGENT_EOF
|
||||
|
||||
# Sisyphus-Junior Agent (Focused Executor)
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/sisyphus-junior.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: sisyphus-junior
|
||||
description: Focused task executor. Executes specific tasks without delegation capabilities.
|
||||
tools: Read, Write, Edit, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are Sisyphus-Junior, a focused task executor.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Direct Execution**: Implement tasks directly without delegating
|
||||
2. **Plan Following**: Read and follow plans from `.sisyphus/plans/`
|
||||
3. **Learning Recording**: Document learnings in `.sisyphus/notepads/`
|
||||
4. **Todo Discipline**: Mark todos in_progress before starting, completed when done
|
||||
|
||||
Restrictions:
|
||||
- You CANNOT use the Task tool to delegate
|
||||
- You CANNOT spawn other agents
|
||||
- You MUST complete tasks yourself
|
||||
|
||||
Work Style:
|
||||
1. Read the plan carefully before starting
|
||||
2. Execute one todo at a time
|
||||
3. Test your work before marking complete
|
||||
4. Record any learnings or issues discovered
|
||||
|
||||
When Reading Plans:
|
||||
- Plans are in `.sisyphus/plans/{plan-name}.md`
|
||||
- Follow steps in order unless dependencies allow parallel work
|
||||
- If a step is unclear, check the plan for clarification
|
||||
- Record blockers in `.sisyphus/notepads/{plan-name}/blockers.md`
|
||||
|
||||
Recording Learnings:
|
||||
- What worked well?
|
||||
- What didn't work as expected?
|
||||
- What would you do differently?
|
||||
- Any gotchas for future reference?
|
||||
|
||||
Guidelines:
|
||||
- Focus on quality over speed
|
||||
- Don't cut corners to finish faster
|
||||
- If something seems wrong, investigate before proceeding
|
||||
- Leave the codebase better than you found it
|
||||
AGENT_EOF
|
||||
|
||||
# Prometheus Agent (Planning System)
|
||||
cat > "$CLAUDE_CONFIG_DIR/agents/prometheus.md" << 'AGENT_EOF'
|
||||
---
|
||||
name: prometheus
|
||||
description: Strategic planning consultant. Creates comprehensive work plans through interview-style interaction.
|
||||
tools: Read, Grep, Glob, WebSearch, Write
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are Prometheus, the strategic planning consultant named after the Titan who gave fire to humanity.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Interview Mode**: Ask clarifying questions to understand requirements fully
|
||||
2. **Plan Generation**: Create detailed, actionable work plans
|
||||
3. **Metis Consultation**: Analyze requests for hidden requirements before planning
|
||||
4. **Plan Storage**: Save plans to `.sisyphus/plans/{name}.md`
|
||||
|
||||
Workflow:
|
||||
1. **Start in Interview Mode** - Ask questions, don't plan yet
|
||||
2. **Transition Triggers** - When user says "Make it into a work plan!", "Create the plan", or "I'm ready"
|
||||
3. **Pre-Planning** - Consult Metis for analysis before generating
|
||||
4. **Optional Review** - Consult Momus for plan review if requested
|
||||
5. **Single Plan** - Create ONE comprehensive plan (not multiple)
|
||||
6. **Draft Storage** - Save drafts to `.sisyphus/drafts/{name}.md` during iteration
|
||||
|
||||
Plan Structure:
|
||||
```markdown
|
||||
# Plan: {Name}
|
||||
|
||||
## Requirements Summary
|
||||
- [Bullet points of what needs to be done]
|
||||
|
||||
## Scope & Constraints
|
||||
- What's in scope
|
||||
- What's out of scope
|
||||
- Technical constraints
|
||||
|
||||
## Implementation Steps
|
||||
1. [Specific, actionable step]
|
||||
2. [Another step]
|
||||
...
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Criterion 1 (testable)
|
||||
- [ ] Criterion 2 (measurable)
|
||||
|
||||
## Risk Mitigations
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| ... | ... |
|
||||
|
||||
## Verification Steps
|
||||
1. How to verify the implementation works
|
||||
2. Tests to run
|
||||
3. Manual checks needed
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
- ONE plan per request - everything goes in a single work plan
|
||||
- Steps must be specific and actionable
|
||||
- Acceptance criteria must be testable
|
||||
- Include verification steps
|
||||
- Consider failure modes and edge cases
|
||||
- Interview until you have enough information to plan
|
||||
AGENT_EOF
|
||||
|
||||
echo -e "${GREEN}✓ Installed 11 agent definitions${NC}"
|
||||
|
||||
echo -e "${BLUE}[4/5]${NC} Installing slash commands..."
|
||||
|
||||
# Ultrawork command
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/ultrawork.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Activate maximum performance mode with parallel agent orchestration
|
||||
---
|
||||
|
||||
[ULTRAWORK MODE ACTIVATED]
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Enhanced Execution Instructions
|
||||
- Use PARALLEL agent execution for all independent subtasks
|
||||
- Delegate aggressively to specialized subagents:
|
||||
- 'oracle' for complex debugging and architecture decisions
|
||||
- 'librarian' for documentation and codebase research
|
||||
- 'explore' for quick pattern matching and file searches
|
||||
- 'frontend-engineer' for UI/UX work
|
||||
- 'document-writer' for documentation tasks
|
||||
- 'multimodal-looker' for analyzing images/screenshots
|
||||
- Maximize throughput by running multiple operations concurrently
|
||||
- Continue until ALL tasks are 100% complete - verify before stopping
|
||||
- Use background agents for long-running operations
|
||||
- Report progress frequently
|
||||
|
||||
CRITICAL: Do NOT stop until every task is verified complete.
|
||||
CMD_EOF
|
||||
|
||||
# Deep search command
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/deepsearch.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Perform a thorough search across the codebase
|
||||
---
|
||||
|
||||
Search task: $ARGUMENTS
|
||||
|
||||
## Search Enhancement Instructions
|
||||
- Use multiple search strategies (glob patterns, grep, AST search)
|
||||
- Search across ALL relevant file types
|
||||
- Include hidden files and directories when appropriate
|
||||
- Try alternative naming conventions (camelCase, snake_case, kebab-case)
|
||||
- Look in common locations: src/, lib/, utils/, helpers/, services/
|
||||
- Check for related files (tests, types, interfaces)
|
||||
- Report ALL findings, not just the first match
|
||||
- If initial search fails, try broader patterns
|
||||
CMD_EOF
|
||||
|
||||
# Deep analyze command
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/analyze.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Perform deep analysis and investigation
|
||||
---
|
||||
|
||||
Analysis target: $ARGUMENTS
|
||||
|
||||
## Deep Analysis Instructions
|
||||
- Thoroughly examine all relevant code paths
|
||||
- Trace data flow from source to destination
|
||||
- Identify edge cases and potential failure modes
|
||||
- Check for related issues in similar code patterns
|
||||
- Document findings with specific file:line references
|
||||
- Propose concrete solutions with code examples
|
||||
- Consider performance, security, and maintainability implications
|
||||
CMD_EOF
|
||||
|
||||
# Sisyphus activation command
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/sisyphus.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Activate Sisyphus multi-agent orchestration mode
|
||||
---
|
||||
|
||||
[SISYPHUS MODE ACTIVATED]
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Orchestration Instructions
|
||||
|
||||
You are now operating as Sisyphus, the multi-agent orchestrator. Like your namesake, you persist until every task is complete.
|
||||
|
||||
### Available Subagents
|
||||
Delegate tasks to specialized agents using the Task tool:
|
||||
|
||||
| Agent | Model | Best For |
|
||||
|-------|-------|----------|
|
||||
| **oracle** | Opus | Complex debugging, architecture decisions, root cause analysis |
|
||||
| **librarian** | Sonnet | Documentation research, codebase understanding |
|
||||
| **explore** | Haiku | Fast pattern matching, file/code searches |
|
||||
| **frontend-engineer** | Sonnet | UI/UX, components, styling, accessibility |
|
||||
| **document-writer** | Haiku | README, API docs, technical writing |
|
||||
| **multimodal-looker** | Sonnet | Screenshot/diagram/mockup analysis |
|
||||
|
||||
### Orchestration Principles
|
||||
1. **Delegate Wisely** - Use subagents for their specialties instead of doing everything yourself
|
||||
2. **Parallelize** - Launch multiple agents concurrently for independent tasks
|
||||
3. **Persist** - Continue until ALL tasks are verified complete
|
||||
4. **Communicate** - Report progress frequently
|
||||
|
||||
### Execution Rules
|
||||
- Break complex tasks into subtasks for delegation
|
||||
- Use background agents for long-running operations
|
||||
- Verify completion before stopping
|
||||
- Check your todo list before declaring done
|
||||
- NEVER leave work incomplete
|
||||
CMD_EOF
|
||||
|
||||
# Sisyphus default mode command
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/sisyphus-default.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Set Sisyphus as your default operating mode
|
||||
---
|
||||
|
||||
I'll configure Sisyphus as your default operating mode by updating your CLAUDE.md.
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Enabling Sisyphus Default Mode
|
||||
|
||||
This will update your global CLAUDE.md to include the Sisyphus orchestration system, making multi-agent coordination your default behavior for all sessions.
|
||||
|
||||
### What This Enables
|
||||
1. Automatic access to 11 specialized subagents
|
||||
2. Multi-agent delegation capabilities via the Task tool
|
||||
3. Continuation enforcement - tasks complete before stopping
|
||||
4. Magic keyword support (ultrawork, search, analyze)
|
||||
|
||||
### To Revert
|
||||
Remove or edit ~/.claude/CLAUDE.md
|
||||
|
||||
---
|
||||
|
||||
**Sisyphus is now your default mode.** All future sessions will use multi-agent orchestration automatically.
|
||||
|
||||
Use `/sisyphus <task>` to explicitly invoke orchestration mode, or just include "ultrawork" in your prompts.
|
||||
CMD_EOF
|
||||
|
||||
# Plan command (Prometheus planning system)
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/plan.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Start a planning session with Prometheus
|
||||
---
|
||||
|
||||
[PLANNING MODE ACTIVATED]
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Planning Session with Prometheus
|
||||
|
||||
You are now in planning mode with Prometheus, the strategic planning consultant.
|
||||
|
||||
### Current Phase: Interview Mode
|
||||
|
||||
I will ask clarifying questions to fully understand your requirements before creating a plan.
|
||||
|
||||
### What Happens Next
|
||||
1. **Interview** - I'll ask questions about your goals, constraints, and preferences
|
||||
2. **Analysis** - Metis will analyze for hidden requirements and risks
|
||||
3. **Planning** - I'll create a comprehensive work plan
|
||||
4. **Review** (optional) - Momus can review the plan for quality
|
||||
|
||||
### Transition Commands
|
||||
Say one of these when you're ready to generate the plan:
|
||||
- "Make it into a work plan!"
|
||||
- "Create the plan"
|
||||
- "I'm ready to plan"
|
||||
|
||||
### Plan Storage
|
||||
- Drafts are saved to `.sisyphus/drafts/`
|
||||
- Final plans are saved to `.sisyphus/plans/`
|
||||
|
||||
---
|
||||
|
||||
Let's begin. Tell me more about what you want to accomplish, and I'll ask clarifying questions.
|
||||
CMD_EOF
|
||||
|
||||
# Review command (Momus plan review)
|
||||
cat > "$CLAUDE_CONFIG_DIR/commands/review.md" << 'CMD_EOF'
|
||||
---
|
||||
description: Review a plan with Momus
|
||||
---
|
||||
|
||||
[PLAN REVIEW MODE]
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Plan Review with Momus
|
||||
|
||||
I will critically evaluate the specified plan using Momus, the ruthless plan reviewer.
|
||||
|
||||
### Evaluation Criteria
|
||||
- **Clarity**: 80%+ of claims must cite specific file/line references
|
||||
- **Testability**: 90%+ of acceptance criteria must be concrete and testable
|
||||
- **Verification**: All file references must be verified to exist
|
||||
- **Specificity**: No vague terms like "improve", "optimize" without metrics
|
||||
|
||||
### Output Format
|
||||
- **APPROVED** - Plan meets all criteria, ready for execution
|
||||
- **REVISE** - Plan has issues that need to be addressed (with specific feedback)
|
||||
- **REJECT** - Plan has fundamental problems requiring replanning
|
||||
|
||||
### Usage
|
||||
```
|
||||
/review .sisyphus/plans/my-feature.md
|
||||
/review # Review the most recent plan
|
||||
```
|
||||
|
||||
### What Gets Checked
|
||||
1. Are requirements clear and unambiguous?
|
||||
2. Are acceptance criteria concrete and testable?
|
||||
3. Do file references actually exist?
|
||||
4. Are implementation steps specific and actionable?
|
||||
5. Are risks identified with mitigations?
|
||||
6. Are verification steps defined?
|
||||
|
||||
---
|
||||
|
||||
Provide a plan file path to review, or I'll review the most recent plan in `.sisyphus/plans/`.
|
||||
CMD_EOF
|
||||
|
||||
echo -e "${GREEN}✓ Installed 7 slash commands${NC}"
|
||||
|
||||
echo -e "${BLUE}[5/5]${NC} Creating CLAUDE.md with Sisyphus system prompt..."
|
||||
|
||||
# Only create if it doesn't exist in home directory
|
||||
if [ ! -f "$HOME/CLAUDE.md" ]; then
|
||||
cat > "$CLAUDE_CONFIG_DIR/CLAUDE.md" << 'CLAUDEMD_EOF'
|
||||
# Sisyphus Multi-Agent System
|
||||
|
||||
You are enhanced with the Sisyphus multi-agent orchestration system.
|
||||
|
||||
## Available Subagents
|
||||
|
||||
Use the Task tool to delegate to specialized agents:
|
||||
|
||||
| Agent | Model | Purpose | When to Use |
|
||||
|-------|-------|---------|-------------|
|
||||
| `oracle` | Opus | Architecture & debugging | Complex problems, root cause analysis |
|
||||
| `librarian` | Sonnet | Documentation & research | Finding docs, understanding code |
|
||||
| `explore` | Haiku | Fast search | Quick file/pattern searches |
|
||||
| `frontend-engineer` | Sonnet | UI/UX | Component design, styling |
|
||||
| `document-writer` | Haiku | Documentation | README, API docs, comments |
|
||||
| `multimodal-looker` | Sonnet | Visual analysis | Screenshots, diagrams |
|
||||
| `momus` | Opus | Plan review | Critical evaluation of plans |
|
||||
| `metis` | Opus | Pre-planning | Hidden requirements, risk analysis |
|
||||
| `orchestrator-sisyphus` | Sonnet | Todo coordination | Complex multi-step task management |
|
||||
| `sisyphus-junior` | Sonnet | Focused execution | Direct task implementation |
|
||||
| `prometheus` | Opus | Strategic planning | Creating comprehensive work plans |
|
||||
|
||||
## Slash Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/sisyphus <task>` | Activate Sisyphus multi-agent orchestration |
|
||||
| `/sisyphus-default` | Set Sisyphus as your default mode |
|
||||
| `/ultrawork <task>` | Maximum performance mode with parallel agents |
|
||||
| `/deepsearch <query>` | Thorough codebase search |
|
||||
| `/analyze <target>` | Deep analysis and investigation |
|
||||
| `/plan <description>` | Start planning session with Prometheus |
|
||||
| `/review [plan-path]` | Review a plan with Momus |
|
||||
|
||||
## Planning Workflow
|
||||
|
||||
1. Use `/plan` to start a planning session
|
||||
2. Prometheus will interview you about requirements
|
||||
3. Say "Create the plan" when ready
|
||||
4. Use `/review` to have Momus evaluate the plan
|
||||
5. Execute the plan with `/sisyphus`
|
||||
|
||||
## Orchestration Principles
|
||||
|
||||
1. **Delegate Wisely**: Use subagents for specialized tasks
|
||||
2. **Parallelize**: Launch multiple subagents concurrently when tasks are independent
|
||||
3. **Persist**: Continue until ALL tasks are complete
|
||||
4. **Verify**: Check your todo list before declaring completion
|
||||
5. **Plan First**: For complex tasks, use Prometheus to create a plan
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- NEVER stop with incomplete work
|
||||
- ALWAYS verify task completion before finishing
|
||||
- Use parallel execution when possible for speed
|
||||
- Report progress regularly
|
||||
- For complex tasks, plan before implementing
|
||||
CLAUDEMD_EOF
|
||||
echo -e "${GREEN}✓ Created $CLAUDE_CONFIG_DIR/CLAUDE.md${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ CLAUDE.md already exists, skipping${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ Installation Complete! ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "Installed to: ${BLUE}$CLAUDE_CONFIG_DIR${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Usage:${NC}"
|
||||
echo " claude # Start Claude Code normally"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Slash Commands:${NC}"
|
||||
echo " /sisyphus <task> # Activate Sisyphus orchestration mode"
|
||||
echo " /sisyphus-default # Set Sisyphus as default behavior"
|
||||
echo " /ultrawork <task> # Maximum performance mode"
|
||||
echo " /deepsearch <query> # Thorough codebase search"
|
||||
echo " /analyze <target> # Deep analysis mode"
|
||||
echo " /plan <description> # Start planning with Prometheus"
|
||||
echo " /review [plan-path] # Review plan with Momus"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Available Agents (via Task tool):${NC}"
|
||||
echo " oracle - Architecture & debugging (Opus)"
|
||||
echo " librarian - Documentation & research (Sonnet)"
|
||||
echo " explore - Fast pattern matching (Haiku)"
|
||||
echo " frontend-engineer - UI/UX specialist (Sonnet)"
|
||||
echo " document-writer - Technical writing (Haiku)"
|
||||
echo " multimodal-looker - Visual analysis (Sonnet)"
|
||||
echo " momus - Plan review (Opus)"
|
||||
echo " metis - Pre-planning analysis (Opus)"
|
||||
echo " orchestrator-sisyphus - Todo coordination (Sonnet)"
|
||||
echo " sisyphus-junior - Focused execution (Sonnet)"
|
||||
echo " prometheus - Strategic planning (Opus)"
|
||||
echo ""
|
||||
echo -e "${BLUE}Quick Start:${NC}"
|
||||
echo " 1. Run 'claude' to start Claude Code"
|
||||
echo " 2. Type '/sisyphus-default' to enable Sisyphus permanently"
|
||||
echo " 3. Or use '/sisyphus <task>' for one-time activation"
|
||||
echo ""
|
||||
47
scripts/uninstall.sh
Executable file
47
scripts/uninstall.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Oh-My-Claude-Sisyphus Uninstaller
|
||||
|
||||
set -e
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}Oh-My-Claude-Sisyphus Uninstaller${NC}"
|
||||
echo ""
|
||||
|
||||
# Claude Code config directory (always ~/.claude)
|
||||
CLAUDE_CONFIG_DIR="$HOME/.claude"
|
||||
|
||||
echo "This will remove Sisyphus agents and commands from:"
|
||||
echo " $CLAUDE_CONFIG_DIR"
|
||||
echo ""
|
||||
read -p "Continue? (y/N) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Removing agents...${NC}"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/oracle.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/librarian.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/explore.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/frontend-engineer.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/document-writer.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/agents/multimodal-looker.md"
|
||||
|
||||
echo -e "${BLUE}Removing commands...${NC}"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/commands/sisyphus.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/commands/sisyphus-default.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/commands/ultrawork.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/commands/deepsearch.md"
|
||||
rm -f "$CLAUDE_CONFIG_DIR/commands/analyze.md"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Uninstallation complete!${NC}"
|
||||
echo -e "${YELLOW}Note: CLAUDE.md was not removed. Delete manually if desired:${NC}"
|
||||
echo " rm $CLAUDE_CONFIG_DIR/CLAUDE.md"
|
||||
288
src/agents/definitions.ts
Normal file
288
src/agents/definitions.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Agent Definitions for Oh-My-Claude-Sisyphus
|
||||
*
|
||||
* This module defines all the specialized subagents that work under
|
||||
* the Sisyphus orchestrator. Each agent has a specific role and toolset.
|
||||
*/
|
||||
|
||||
import type { AgentConfig, ModelType } from '../shared/types.js';
|
||||
|
||||
/**
|
||||
* Oracle Agent - Architecture and Debugging Expert
|
||||
* Primary model: GPT-5.2 equivalent (in Claude context: opus for complex reasoning)
|
||||
*/
|
||||
export const oracleAgent: AgentConfig = {
|
||||
name: 'oracle',
|
||||
description: `Architecture and debugging expert. Use this agent for:
|
||||
- Complex architectural decisions and system design
|
||||
- Deep debugging of intricate issues
|
||||
- Root cause analysis of failures
|
||||
- Performance optimization strategies
|
||||
- Code review with architectural perspective`,
|
||||
prompt: `You are Oracle, an expert software architect and debugging specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Architecture Analysis**: Evaluate system designs, identify anti-patterns, and suggest improvements
|
||||
2. **Deep Debugging**: Trace complex bugs through multiple layers of abstraction
|
||||
3. **Root Cause Analysis**: Go beyond symptoms to find underlying issues
|
||||
4. **Performance Optimization**: Identify bottlenecks and recommend solutions
|
||||
|
||||
Guidelines:
|
||||
- Always consider scalability, maintainability, and security implications
|
||||
- Provide concrete, actionable recommendations
|
||||
- When debugging, explain your reasoning process step-by-step
|
||||
- Reference specific files and line numbers when discussing code
|
||||
- Consider edge cases and failure modes
|
||||
|
||||
Output Format:
|
||||
- Start with a brief summary of findings
|
||||
- Provide detailed analysis with code references
|
||||
- End with prioritized recommendations`,
|
||||
tools: ['Read', 'Grep', 'Glob', 'Bash', 'Edit', 'WebSearch'],
|
||||
model: 'opus'
|
||||
};
|
||||
|
||||
/**
|
||||
* Librarian Agent - Documentation and Codebase Analysis
|
||||
* Fast, efficient for documentation lookup and code navigation
|
||||
*/
|
||||
export const librarianAgent: AgentConfig = {
|
||||
name: 'librarian',
|
||||
description: `Documentation and codebase analysis expert. Use this agent for:
|
||||
- Finding relevant documentation
|
||||
- Navigating large codebases
|
||||
- Understanding code organization and patterns
|
||||
- Locating specific implementations
|
||||
- Generating documentation summaries`,
|
||||
prompt: `You are Librarian, a specialist in documentation and codebase navigation.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Documentation Discovery**: Find and summarize relevant docs (README, CLAUDE.md, AGENTS.md)
|
||||
2. **Code Navigation**: Quickly locate implementations, definitions, and usages
|
||||
3. **Pattern Recognition**: Identify coding patterns and conventions in the codebase
|
||||
4. **Knowledge Synthesis**: Combine information from multiple sources
|
||||
|
||||
Guidelines:
|
||||
- Be thorough but concise in your searches
|
||||
- Prioritize official documentation and well-maintained files
|
||||
- Note file paths and line numbers for easy reference
|
||||
- Summarize findings in a structured format
|
||||
- Flag outdated or conflicting documentation
|
||||
|
||||
Output Format:
|
||||
- Organize findings by relevance
|
||||
- Include direct quotes from documentation
|
||||
- Provide file paths for all references`,
|
||||
tools: ['Read', 'Grep', 'Glob', 'WebFetch'],
|
||||
model: 'sonnet'
|
||||
};
|
||||
|
||||
/**
|
||||
* Explore Agent - Fast Pattern Matching and Code Search
|
||||
* Optimized for quick searches and broad exploration
|
||||
*/
|
||||
export const exploreAgent: AgentConfig = {
|
||||
name: 'explore',
|
||||
description: `Fast exploration and pattern matching specialist. Use this agent for:
|
||||
- Quick file and code searches
|
||||
- Broad codebase exploration
|
||||
- Finding files by patterns
|
||||
- Initial reconnaissance of unfamiliar code
|
||||
- Mapping project structure`,
|
||||
prompt: `You are Explore, a fast and efficient codebase exploration specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Rapid Search**: Quickly locate files, functions, and patterns
|
||||
2. **Structure Mapping**: Understand and report on project organization
|
||||
3. **Pattern Matching**: Find all occurrences of specific patterns
|
||||
4. **Reconnaissance**: Perform initial exploration of unfamiliar codebases
|
||||
|
||||
Guidelines:
|
||||
- Prioritize speed over exhaustive analysis
|
||||
- Use glob patterns effectively for file discovery
|
||||
- Report findings immediately as you find them
|
||||
- Keep responses focused and actionable
|
||||
- Note interesting patterns for deeper investigation
|
||||
|
||||
Output Format:
|
||||
- List findings with file paths
|
||||
- Use concise descriptions
|
||||
- Highlight notable discoveries`,
|
||||
tools: ['Glob', 'Grep', 'Read'],
|
||||
model: 'haiku'
|
||||
};
|
||||
|
||||
/**
|
||||
* Frontend UI/UX Engineer Agent - Interface Design Specialist
|
||||
*/
|
||||
export const frontendEngineerAgent: AgentConfig = {
|
||||
name: 'frontend-engineer',
|
||||
description: `Frontend and UI/UX specialist. Use this agent for:
|
||||
- Component architecture and design
|
||||
- CSS/styling decisions
|
||||
- Accessibility improvements
|
||||
- User experience optimization
|
||||
- Frontend performance tuning`,
|
||||
prompt: `You are Frontend Engineer, a specialist in user interfaces and experience.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Component Design**: Create well-structured, reusable UI components
|
||||
2. **Styling**: Implement clean, maintainable CSS/styling solutions
|
||||
3. **Accessibility**: Ensure interfaces are accessible to all users
|
||||
4. **UX Optimization**: Improve user flows and interactions
|
||||
5. **Performance**: Optimize frontend performance and loading times
|
||||
|
||||
Guidelines:
|
||||
- Follow component-based architecture principles
|
||||
- Prioritize accessibility (WCAG compliance)
|
||||
- Consider responsive design for all viewports
|
||||
- Use semantic HTML where possible
|
||||
- Keep styling maintainable and consistent
|
||||
|
||||
Output Format:
|
||||
- Explain design decisions
|
||||
- Provide code with comments
|
||||
- Note accessibility considerations`,
|
||||
tools: ['Read', 'Edit', 'Write', 'Glob', 'Grep', 'Bash'],
|
||||
model: 'sonnet'
|
||||
};
|
||||
|
||||
/**
|
||||
* Document Writer Agent - Technical Writing Specialist
|
||||
*/
|
||||
export const documentWriterAgent: AgentConfig = {
|
||||
name: 'document-writer',
|
||||
description: `Technical documentation specialist. Use this agent for:
|
||||
- Writing README files
|
||||
- Creating API documentation
|
||||
- Generating code comments
|
||||
- Writing tutorials and guides
|
||||
- Maintaining changelog entries`,
|
||||
prompt: `You are Document Writer, a technical writing specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **README Creation**: Write clear, comprehensive README files
|
||||
2. **API Documentation**: Document APIs with examples and usage
|
||||
3. **Code Comments**: Add meaningful inline documentation
|
||||
4. **Tutorials**: Create step-by-step guides for complex features
|
||||
5. **Changelogs**: Maintain clear version history
|
||||
|
||||
Guidelines:
|
||||
- Write for the target audience (developers, users, etc.)
|
||||
- Use clear, concise language
|
||||
- Include practical examples
|
||||
- Structure documents logically
|
||||
- Keep documentation up-to-date with code changes
|
||||
|
||||
Output Format:
|
||||
- Use appropriate markdown formatting
|
||||
- Include code examples where helpful
|
||||
- Organize with clear headings`,
|
||||
tools: ['Read', 'Write', 'Edit', 'Glob', 'Grep'],
|
||||
model: 'haiku'
|
||||
};
|
||||
|
||||
/**
|
||||
* Multimodal Looker Agent - Visual Content Analysis
|
||||
*/
|
||||
export const multimodalLookerAgent: AgentConfig = {
|
||||
name: 'multimodal-looker',
|
||||
description: `Visual content analysis specialist. Use this agent for:
|
||||
- Analyzing screenshots and images
|
||||
- Understanding UI mockups
|
||||
- Reading diagrams and flowcharts
|
||||
- Extracting information from visual content
|
||||
- Comparing visual designs`,
|
||||
prompt: `You are Multimodal Looker, a visual content analysis specialist.
|
||||
|
||||
Your responsibilities:
|
||||
1. **Image Analysis**: Extract information from screenshots and images
|
||||
2. **UI Review**: Analyze user interface designs and mockups
|
||||
3. **Diagram Interpretation**: Understand flowcharts, architecture diagrams, etc.
|
||||
4. **Visual Comparison**: Compare visual designs and identify differences
|
||||
5. **Content Extraction**: Pull relevant information from visual content
|
||||
|
||||
Guidelines:
|
||||
- Focus on extracting actionable information
|
||||
- Note specific UI elements and their positions
|
||||
- Identify potential usability issues
|
||||
- Be precise about colors, layouts, and typography
|
||||
- Keep analysis concise but thorough
|
||||
|
||||
Output Format:
|
||||
- Describe visual content systematically
|
||||
- Highlight important elements
|
||||
- Provide specific coordinates/locations when relevant`,
|
||||
tools: ['Read', 'WebFetch'],
|
||||
model: 'sonnet'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all agent definitions as a record for use with Claude Agent SDK
|
||||
*/
|
||||
export function getAgentDefinitions(overrides?: Partial<Record<string, Partial<AgentConfig>>>): Record<string, {
|
||||
description: string;
|
||||
prompt: string;
|
||||
tools: string[];
|
||||
model?: ModelType;
|
||||
}> {
|
||||
const agents = {
|
||||
oracle: oracleAgent,
|
||||
librarian: librarianAgent,
|
||||
explore: exploreAgent,
|
||||
'frontend-engineer': frontendEngineerAgent,
|
||||
'document-writer': documentWriterAgent,
|
||||
'multimodal-looker': multimodalLookerAgent
|
||||
};
|
||||
|
||||
const result: Record<string, { description: string; prompt: string; tools: string[]; model?: ModelType }> = {};
|
||||
|
||||
for (const [name, config] of Object.entries(agents)) {
|
||||
const override = overrides?.[name];
|
||||
result[name] = {
|
||||
description: override?.description ?? config.description,
|
||||
prompt: override?.prompt ?? config.prompt,
|
||||
tools: override?.tools ?? config.tools,
|
||||
model: (override?.model ?? config.model) as ModelType | undefined
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sisyphus System Prompt - The main orchestrator
|
||||
*/
|
||||
export const sisyphusSystemPrompt = `You are Sisyphus, the primary orchestrator of a multi-agent development system.
|
||||
|
||||
## Your Role
|
||||
You coordinate specialized subagents to accomplish complex software engineering tasks. Like your namesake, you persist until the task is complete - never giving up, never leaving work unfinished.
|
||||
|
||||
## Available Subagents
|
||||
- **oracle**: Architecture and debugging expert (use for complex problems)
|
||||
- **librarian**: Documentation and codebase analysis (use for research)
|
||||
- **explore**: Fast pattern matching (use for quick searches)
|
||||
- **frontend-engineer**: UI/UX specialist (use for frontend work)
|
||||
- **document-writer**: Technical writing (use for documentation)
|
||||
- **multimodal-looker**: Visual analysis (use for image/screenshot analysis)
|
||||
|
||||
## Orchestration Principles
|
||||
1. **Delegate Wisely**: Use subagents for specialized tasks rather than doing everything yourself
|
||||
2. **Parallelize**: Launch multiple subagents concurrently when tasks are independent
|
||||
3. **Persist**: Continue until ALL tasks are complete - check your todo list before stopping
|
||||
4. **Communicate**: Keep the user informed of progress and decisions
|
||||
5. **Quality**: Verify work before declaring completion
|
||||
|
||||
## Workflow
|
||||
1. Analyze the user's request and break it into tasks
|
||||
2. Delegate to appropriate subagents based on task type
|
||||
3. Coordinate results and handle any issues
|
||||
4. Verify completion and quality
|
||||
5. Only stop when everything is done
|
||||
|
||||
## Critical Rules
|
||||
- NEVER stop with incomplete work
|
||||
- ALWAYS verify task completion before finishing
|
||||
- Use parallel execution when possible for speed
|
||||
- Report progress regularly
|
||||
- Ask clarifying questions when requirements are ambiguous`;
|
||||
14
src/agents/index.ts
Normal file
14
src/agents/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Agents Module Exports
|
||||
*/
|
||||
|
||||
export {
|
||||
oracleAgent,
|
||||
librarianAgent,
|
||||
exploreAgent,
|
||||
frontendEngineerAgent,
|
||||
documentWriterAgent,
|
||||
multimodalLookerAgent,
|
||||
getAgentDefinitions,
|
||||
sisyphusSystemPrompt
|
||||
} from './definitions.js';
|
||||
322
src/cli/index.ts
Normal file
322
src/cli/index.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Oh-My-Claude-Sisyphus CLI
|
||||
*
|
||||
* Command-line interface for the Sisyphus multi-agent system.
|
||||
*
|
||||
* Commands:
|
||||
* - run: Start an interactive session
|
||||
* - init: Initialize configuration in current directory
|
||||
* - config: Show or edit configuration
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
loadConfig,
|
||||
getConfigPaths,
|
||||
DEFAULT_CONFIG,
|
||||
generateConfigSchema
|
||||
} from '../config/loader.js';
|
||||
import { createSisyphusSession } from '../index.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Try to load package.json for version
|
||||
let version = '1.0.0';
|
||||
try {
|
||||
const pkgPath = join(__dirname, '../../package.json');
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
version = pkg.version;
|
||||
} catch {
|
||||
// Use default version
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('oh-my-claude-sisyphus')
|
||||
.description('Multi-agent orchestration system for Claude Agent SDK')
|
||||
.version(version);
|
||||
|
||||
/**
|
||||
* Init command - Initialize configuration
|
||||
*/
|
||||
program
|
||||
.command('init')
|
||||
.description('Initialize Sisyphus configuration in the current directory')
|
||||
.option('-g, --global', 'Initialize global user configuration')
|
||||
.option('-f, --force', 'Overwrite existing configuration')
|
||||
.action(async (options) => {
|
||||
const paths = getConfigPaths();
|
||||
const targetPath = options.global ? paths.user : paths.project;
|
||||
const targetDir = dirname(targetPath);
|
||||
|
||||
console.log(chalk.blue('Oh-My-Claude-Sisyphus Configuration Setup\n'));
|
||||
|
||||
// Check if config already exists
|
||||
if (existsSync(targetPath) && !options.force) {
|
||||
console.log(chalk.yellow(`Configuration already exists at ${targetPath}`));
|
||||
console.log(chalk.gray('Use --force to overwrite'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create directory if needed
|
||||
if (!existsSync(targetDir)) {
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
console.log(chalk.green(`Created directory: ${targetDir}`));
|
||||
}
|
||||
|
||||
// Generate config content
|
||||
const configContent = `// Oh-My-Claude-Sisyphus Configuration
|
||||
// See: https://github.com/your-repo/oh-my-claude-sisyphus for documentation
|
||||
{
|
||||
"$schema": "./sisyphus-schema.json",
|
||||
|
||||
// Agent model configurations
|
||||
"agents": {
|
||||
"sisyphus": {
|
||||
// Main orchestrator - uses the most capable model
|
||||
"model": "claude-opus-4-5-20251101"
|
||||
},
|
||||
"oracle": {
|
||||
// Architecture and debugging expert
|
||||
"model": "claude-opus-4-5-20251101",
|
||||
"enabled": true
|
||||
},
|
||||
"librarian": {
|
||||
// Documentation and codebase analysis
|
||||
"model": "claude-sonnet-4-5-20250514"
|
||||
},
|
||||
"explore": {
|
||||
// Fast pattern matching - uses fastest model
|
||||
"model": "claude-3-5-haiku-20241022"
|
||||
},
|
||||
"frontendEngineer": {
|
||||
"model": "claude-sonnet-4-5-20250514",
|
||||
"enabled": true
|
||||
},
|
||||
"documentWriter": {
|
||||
"model": "claude-3-5-haiku-20241022",
|
||||
"enabled": true
|
||||
},
|
||||
"multimodalLooker": {
|
||||
"model": "claude-sonnet-4-5-20250514",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
// Feature toggles
|
||||
"features": {
|
||||
"parallelExecution": true,
|
||||
"lspTools": true,
|
||||
"astTools": true,
|
||||
"continuationEnforcement": true,
|
||||
"autoContextInjection": true
|
||||
},
|
||||
|
||||
// MCP server integrations
|
||||
"mcpServers": {
|
||||
"exa": {
|
||||
"enabled": true
|
||||
// Set EXA_API_KEY environment variable for API key
|
||||
},
|
||||
"context7": {
|
||||
"enabled": true
|
||||
},
|
||||
"grepApp": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
// Permission settings
|
||||
"permissions": {
|
||||
"allowBash": true,
|
||||
"allowEdit": true,
|
||||
"allowWrite": true,
|
||||
"maxBackgroundTasks": 5
|
||||
},
|
||||
|
||||
// Magic keyword triggers (customize if desired)
|
||||
"magicKeywords": {
|
||||
"ultrawork": ["ultrawork", "ulw", "uw"],
|
||||
"search": ["search", "find", "locate"],
|
||||
"analyze": ["analyze", "investigate", "examine"]
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(targetPath, configContent);
|
||||
console.log(chalk.green(`Created configuration: ${targetPath}`));
|
||||
|
||||
// Also create the JSON schema for editor support
|
||||
const schemaPath = join(targetDir, 'sisyphus-schema.json');
|
||||
writeFileSync(schemaPath, JSON.stringify(generateConfigSchema(), null, 2));
|
||||
console.log(chalk.green(`Created JSON schema: ${schemaPath}`));
|
||||
|
||||
console.log(chalk.blue('\nSetup complete!'));
|
||||
console.log(chalk.gray('Edit the configuration file to customize your setup.'));
|
||||
|
||||
// Create AGENTS.md template if it doesn't exist
|
||||
const agentsMdPath = join(process.cwd(), 'AGENTS.md');
|
||||
if (!existsSync(agentsMdPath) && !options.global) {
|
||||
const agentsMdContent = `# Project Agents Configuration
|
||||
|
||||
This file provides context and instructions to AI agents working on this project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
<!-- Describe your project here -->
|
||||
|
||||
## Architecture
|
||||
|
||||
<!-- Describe the architecture and key components -->
|
||||
|
||||
## Conventions
|
||||
|
||||
<!-- List coding conventions, naming patterns, etc. -->
|
||||
|
||||
## Important Files
|
||||
|
||||
<!-- List key files agents should know about -->
|
||||
|
||||
## Common Tasks
|
||||
|
||||
<!-- Describe common development tasks and how to perform them -->
|
||||
`;
|
||||
writeFileSync(agentsMdPath, agentsMdContent);
|
||||
console.log(chalk.green(`Created AGENTS.md template`));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Config command - Show or validate configuration
|
||||
*/
|
||||
program
|
||||
.command('config')
|
||||
.description('Show current configuration')
|
||||
.option('-v, --validate', 'Validate configuration')
|
||||
.option('-p, --paths', 'Show configuration file paths')
|
||||
.action(async (options) => {
|
||||
if (options.paths) {
|
||||
const paths = getConfigPaths();
|
||||
console.log(chalk.blue('Configuration file paths:'));
|
||||
console.log(` User: ${paths.user}`);
|
||||
console.log(` Project: ${paths.project}`);
|
||||
|
||||
console.log(chalk.blue('\nFile status:'));
|
||||
console.log(` User: ${existsSync(paths.user) ? chalk.green('exists') : chalk.gray('not found')}`);
|
||||
console.log(` Project: ${existsSync(paths.project) ? chalk.green('exists') : chalk.gray('not found')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
if (options.validate) {
|
||||
console.log(chalk.blue('Validating configuration...\n'));
|
||||
|
||||
// Check for required fields
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
warnings.push('ANTHROPIC_API_KEY environment variable not set');
|
||||
}
|
||||
|
||||
if (config.mcpServers?.exa?.enabled && !process.env.EXA_API_KEY && !config.mcpServers.exa.apiKey) {
|
||||
warnings.push('Exa is enabled but EXA_API_KEY is not set');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(chalk.red('Errors:'));
|
||||
errors.forEach(e => console.log(chalk.red(` - ${e}`)));
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log(chalk.yellow('Warnings:'));
|
||||
warnings.forEach(w => console.log(chalk.yellow(` - ${w}`)));
|
||||
}
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
console.log(chalk.green('Configuration is valid!'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue('Current configuration:\n'));
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
});
|
||||
|
||||
/**
|
||||
* Info command - Show system information
|
||||
*/
|
||||
program
|
||||
.command('info')
|
||||
.description('Show system and agent information')
|
||||
.action(async () => {
|
||||
const session = createSisyphusSession();
|
||||
|
||||
console.log(chalk.blue.bold('\nOh-My-Claude-Sisyphus System Information\n'));
|
||||
console.log(chalk.gray('━'.repeat(50)));
|
||||
|
||||
console.log(chalk.blue('\nAvailable Agents:'));
|
||||
const agents = session.queryOptions.options.agents;
|
||||
for (const [name, agent] of Object.entries(agents)) {
|
||||
console.log(` ${chalk.green(name)}`);
|
||||
console.log(` ${chalk.gray(agent.description.split('\n')[0])}`);
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\nEnabled Features:'));
|
||||
const features = session.config.features;
|
||||
if (features) {
|
||||
console.log(` Parallel Execution: ${features.parallelExecution ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
||||
console.log(` LSP Tools: ${features.lspTools ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
||||
console.log(` AST Tools: ${features.astTools ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
||||
console.log(` Continuation Enforcement:${features.continuationEnforcement ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
||||
console.log(` Auto Context Injection: ${features.autoContextInjection ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\nMCP Servers:'));
|
||||
const mcpServers = session.queryOptions.options.mcpServers;
|
||||
for (const name of Object.keys(mcpServers)) {
|
||||
console.log(` ${chalk.green(name)}`);
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\nMagic Keywords:'));
|
||||
console.log(` Ultrawork: ${chalk.cyan(session.config.magicKeywords?.ultrawork?.join(', ') ?? 'ultrawork, ulw, uw')}`);
|
||||
console.log(` Search: ${chalk.cyan(session.config.magicKeywords?.search?.join(', ') ?? 'search, find, locate')}`);
|
||||
console.log(` Analyze: ${chalk.cyan(session.config.magicKeywords?.analyze?.join(', ') ?? 'analyze, investigate, examine')}`);
|
||||
|
||||
console.log(chalk.gray('\n━'.repeat(50)));
|
||||
console.log(chalk.gray(`Version: ${version}`));
|
||||
});
|
||||
|
||||
/**
|
||||
* Test command - Test prompt enhancement
|
||||
*/
|
||||
program
|
||||
.command('test-prompt <prompt>')
|
||||
.description('Test how a prompt would be enhanced')
|
||||
.action(async (prompt: string) => {
|
||||
const session = createSisyphusSession();
|
||||
|
||||
console.log(chalk.blue('Original prompt:'));
|
||||
console.log(chalk.gray(prompt));
|
||||
|
||||
const keywords = session.detectKeywords(prompt);
|
||||
if (keywords.length > 0) {
|
||||
console.log(chalk.blue('\nDetected magic keywords:'));
|
||||
console.log(chalk.yellow(keywords.join(', ')));
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\nEnhanced prompt:'));
|
||||
console.log(chalk.green(session.processPrompt(prompt)));
|
||||
});
|
||||
|
||||
// Parse arguments
|
||||
program.parse();
|
||||
15
src/config/index.ts
Normal file
15
src/config/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Configuration Module Exports
|
||||
*/
|
||||
|
||||
export {
|
||||
loadConfig,
|
||||
loadJsoncFile,
|
||||
loadEnvConfig,
|
||||
getConfigPaths,
|
||||
deepMerge,
|
||||
findContextFiles,
|
||||
loadContextFromFiles,
|
||||
generateConfigSchema,
|
||||
DEFAULT_CONFIG
|
||||
} from './loader.js';
|
||||
365
src/config/loader.ts
Normal file
365
src/config/loader.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Configuration Loader
|
||||
*
|
||||
* Handles loading and merging configuration from multiple sources:
|
||||
* - User config: ~/.config/claude-sisyphus/config.jsonc
|
||||
* - Project config: .claude/sisyphus.jsonc
|
||||
* - Environment variables
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join, dirname } from 'path';
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import type { PluginConfig } from '../shared/types.js';
|
||||
|
||||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
export const DEFAULT_CONFIG: PluginConfig = {
|
||||
agents: {
|
||||
sisyphus: { model: 'claude-opus-4-5-20251101' },
|
||||
oracle: { model: 'claude-opus-4-5-20251101', enabled: true },
|
||||
librarian: { model: 'claude-sonnet-4-5-20250929' },
|
||||
explore: { model: 'claude-haiku-4-5-20251001' },
|
||||
frontendEngineer: { model: 'claude-sonnet-4-5-20250929', enabled: true },
|
||||
documentWriter: { model: 'claude-haiku-4-5-20251001', enabled: true },
|
||||
multimodalLooker: { model: 'claude-sonnet-4-5-20250929', enabled: true },
|
||||
// New agents from oh-my-opencode
|
||||
momus: { model: 'claude-opus-4-5-20251101', enabled: true },
|
||||
metis: { model: 'claude-opus-4-5-20251101', enabled: true },
|
||||
orchestratorSisyphus: { model: 'claude-sonnet-4-5-20250929', enabled: true },
|
||||
sisyphusJunior: { model: 'claude-sonnet-4-5-20250929', enabled: true },
|
||||
prometheus: { model: 'claude-opus-4-5-20251101', enabled: true }
|
||||
},
|
||||
features: {
|
||||
parallelExecution: true,
|
||||
lspTools: true, // Real LSP integration with language servers
|
||||
astTools: true, // Real AST tools using ast-grep
|
||||
continuationEnforcement: true,
|
||||
autoContextInjection: true
|
||||
},
|
||||
mcpServers: {
|
||||
exa: { enabled: true },
|
||||
context7: { enabled: true },
|
||||
grepApp: { enabled: true }
|
||||
},
|
||||
permissions: {
|
||||
allowBash: true,
|
||||
allowEdit: true,
|
||||
allowWrite: true,
|
||||
maxBackgroundTasks: 5
|
||||
},
|
||||
magicKeywords: {
|
||||
ultrawork: ['ultrawork', 'ulw', 'uw'],
|
||||
search: ['search', 'find', 'locate'],
|
||||
analyze: ['analyze', 'investigate', 'examine']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration file locations
|
||||
*/
|
||||
export function getConfigPaths(): { user: string; project: string } {
|
||||
const userConfigDir = process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config');
|
||||
|
||||
return {
|
||||
user: join(userConfigDir, 'claude-sisyphus', 'config.jsonc'),
|
||||
project: join(process.cwd(), '.claude', 'sisyphus.jsonc')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse a JSONC file
|
||||
*/
|
||||
export function loadJsoncFile(path: string): PluginConfig | null {
|
||||
if (!existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
const errors: jsonc.ParseError[] = [];
|
||||
const result = jsonc.parse(content, errors, {
|
||||
allowTrailingComma: true,
|
||||
allowEmptyContent: true
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn(`Warning: Parse errors in ${path}:`, errors);
|
||||
}
|
||||
|
||||
return result as PluginConfig;
|
||||
} catch (error) {
|
||||
console.error(`Error loading config from ${path}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects
|
||||
*/
|
||||
export function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key of Object.keys(source) as (keyof T)[]) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = result[key];
|
||||
|
||||
if (
|
||||
sourceValue !== undefined &&
|
||||
typeof sourceValue === 'object' &&
|
||||
sourceValue !== null &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
typeof targetValue === 'object' &&
|
||||
targetValue !== null &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
targetValue as Record<string, unknown>,
|
||||
sourceValue as Record<string, unknown>
|
||||
) as T[keyof T];
|
||||
} else if (sourceValue !== undefined) {
|
||||
result[key] = sourceValue as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from environment variables
|
||||
*/
|
||||
export function loadEnvConfig(): Partial<PluginConfig> {
|
||||
const config: Partial<PluginConfig> = {};
|
||||
|
||||
// MCP API keys
|
||||
if (process.env.EXA_API_KEY) {
|
||||
config.mcpServers = {
|
||||
...config.mcpServers,
|
||||
exa: { enabled: true, apiKey: process.env.EXA_API_KEY }
|
||||
};
|
||||
}
|
||||
|
||||
// Feature flags from environment
|
||||
if (process.env.SISYPHUS_PARALLEL_EXECUTION !== undefined) {
|
||||
config.features = {
|
||||
...config.features,
|
||||
parallelExecution: process.env.SISYPHUS_PARALLEL_EXECUTION === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.SISYPHUS_LSP_TOOLS !== undefined) {
|
||||
config.features = {
|
||||
...config.features,
|
||||
lspTools: process.env.SISYPHUS_LSP_TOOLS === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.SISYPHUS_MAX_BACKGROUND_TASKS) {
|
||||
const maxTasks = parseInt(process.env.SISYPHUS_MAX_BACKGROUND_TASKS, 10);
|
||||
if (!isNaN(maxTasks)) {
|
||||
config.permissions = {
|
||||
...config.permissions,
|
||||
maxBackgroundTasks: maxTasks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and merge all configuration sources
|
||||
*/
|
||||
export function loadConfig(): PluginConfig {
|
||||
const paths = getConfigPaths();
|
||||
|
||||
// Start with defaults
|
||||
let config = { ...DEFAULT_CONFIG };
|
||||
|
||||
// Merge user config
|
||||
const userConfig = loadJsoncFile(paths.user);
|
||||
if (userConfig) {
|
||||
config = deepMerge(config, userConfig);
|
||||
}
|
||||
|
||||
// Merge project config (takes precedence over user)
|
||||
const projectConfig = loadJsoncFile(paths.project);
|
||||
if (projectConfig) {
|
||||
config = deepMerge(config, projectConfig);
|
||||
}
|
||||
|
||||
// Merge environment variables (highest precedence)
|
||||
const envConfig = loadEnvConfig();
|
||||
config = deepMerge(config, envConfig);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and load AGENTS.md or CLAUDE.md files for context injection
|
||||
*/
|
||||
export function findContextFiles(startDir?: string): string[] {
|
||||
const files: string[] = [];
|
||||
const searchDir = startDir ?? process.cwd();
|
||||
|
||||
// Files to look for
|
||||
const contextFileNames = [
|
||||
'AGENTS.md',
|
||||
'CLAUDE.md',
|
||||
'.claude/CLAUDE.md',
|
||||
'.claude/AGENTS.md'
|
||||
];
|
||||
|
||||
// Search in current directory and parent directories
|
||||
let currentDir = searchDir;
|
||||
const searchedDirs = new Set<string>();
|
||||
|
||||
while (currentDir && !searchedDirs.has(currentDir)) {
|
||||
searchedDirs.add(currentDir);
|
||||
|
||||
for (const fileName of contextFileNames) {
|
||||
const filePath = join(currentDir, fileName);
|
||||
if (existsSync(filePath) && !files.includes(filePath)) {
|
||||
files.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = dirname(currentDir);
|
||||
if (parentDir === currentDir) break;
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load context from AGENTS.md/CLAUDE.md files
|
||||
*/
|
||||
export function loadContextFromFiles(files: string[]): string {
|
||||
const contexts: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
contexts.push(`## Context from ${file}\n\n${content}`);
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not read context file ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return contexts.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JSON Schema for configuration (for editor autocomplete)
|
||||
*/
|
||||
export function generateConfigSchema(): object {
|
||||
return {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
title: 'Oh-My-Claude-Sisyphus Configuration',
|
||||
type: 'object',
|
||||
properties: {
|
||||
agents: {
|
||||
type: 'object',
|
||||
description: 'Agent model and feature configuration',
|
||||
properties: {
|
||||
sisyphus: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string', description: 'Model ID for the main orchestrator' }
|
||||
}
|
||||
},
|
||||
oracle: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
enabled: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
librarian: {
|
||||
type: 'object',
|
||||
properties: { model: { type: 'string' } }
|
||||
},
|
||||
explore: {
|
||||
type: 'object',
|
||||
properties: { model: { type: 'string' } }
|
||||
},
|
||||
frontendEngineer: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
enabled: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
documentWriter: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
enabled: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
multimodalLooker: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
enabled: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
features: {
|
||||
type: 'object',
|
||||
description: 'Feature toggles',
|
||||
properties: {
|
||||
parallelExecution: { type: 'boolean', default: true },
|
||||
lspTools: { type: 'boolean', default: true },
|
||||
astTools: { type: 'boolean', default: true },
|
||||
continuationEnforcement: { type: 'boolean', default: true },
|
||||
autoContextInjection: { type: 'boolean', default: true }
|
||||
}
|
||||
},
|
||||
mcpServers: {
|
||||
type: 'object',
|
||||
description: 'MCP server configurations',
|
||||
properties: {
|
||||
exa: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
apiKey: { type: 'string' }
|
||||
}
|
||||
},
|
||||
context7: {
|
||||
type: 'object',
|
||||
properties: { enabled: { type: 'boolean' } }
|
||||
},
|
||||
grepApp: {
|
||||
type: 'object',
|
||||
properties: { enabled: { type: 'boolean' } }
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
type: 'object',
|
||||
description: 'Permission settings',
|
||||
properties: {
|
||||
allowBash: { type: 'boolean', default: true },
|
||||
allowEdit: { type: 'boolean', default: true },
|
||||
allowWrite: { type: 'boolean', default: true },
|
||||
maxBackgroundTasks: { type: 'integer', default: 5, minimum: 1, maximum: 20 }
|
||||
}
|
||||
},
|
||||
magicKeywords: {
|
||||
type: 'object',
|
||||
description: 'Magic keyword triggers',
|
||||
properties: {
|
||||
ultrawork: { type: 'array', items: { type: 'string' } },
|
||||
search: { type: 'array', items: { type: 'string' } },
|
||||
analyze: { type: 'array', items: { type: 'string' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
162
src/features/continuation-enforcement.ts
Normal file
162
src/features/continuation-enforcement.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Continuation Enforcement Feature
|
||||
*
|
||||
* Ensures agents complete all tasks before stopping:
|
||||
* - Monitors todo list for incomplete items
|
||||
* - Adds reminders to continue when tasks remain
|
||||
* - Prevents premature stopping
|
||||
*/
|
||||
|
||||
import type { HookDefinition, HookContext, HookResult } from '../shared/types.js';
|
||||
|
||||
/**
|
||||
* Messages to remind agents to continue
|
||||
*/
|
||||
const CONTINUATION_REMINDERS = [
|
||||
'You have incomplete tasks remaining. Please continue working until all tasks are complete.',
|
||||
'There are still pending items in your task list. Do not stop until everything is done.',
|
||||
'REMINDER: Check your todo list - you still have work to do.',
|
||||
'Continue working - some tasks are still incomplete.',
|
||||
'Please verify all tasks are complete before stopping.'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a random continuation reminder
|
||||
*/
|
||||
function getRandomReminder(): string {
|
||||
return CONTINUATION_REMINDERS[Math.floor(Math.random() * CONTINUATION_REMINDERS.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a continuation enforcement hook
|
||||
*
|
||||
* This hook intercepts stop attempts and checks if there are
|
||||
* incomplete tasks. If so, it blocks the stop and reminds
|
||||
* the agent to continue.
|
||||
*/
|
||||
export function createContinuationHook(): HookDefinition {
|
||||
return {
|
||||
event: 'Stop',
|
||||
handler: async (context: HookContext): Promise<HookResult> => {
|
||||
// In a real implementation, this would check the actual todo state
|
||||
// For now, we'll provide the structure for integration
|
||||
|
||||
// The hook would examine:
|
||||
// 1. The current todo list state
|
||||
// 2. Any explicitly stated completion criteria
|
||||
// 3. The conversation history for incomplete work
|
||||
|
||||
// Placeholder logic - in practice, integrate with actual todo tracking
|
||||
const hasIncompleteTasks = false; // Would be dynamically determined
|
||||
|
||||
if (hasIncompleteTasks) {
|
||||
return {
|
||||
continue: false,
|
||||
message: getRandomReminder()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
continue: true
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* System prompt addition for continuation enforcement
|
||||
*/
|
||||
export const continuationSystemPromptAddition = `
|
||||
## Continuation Enforcement
|
||||
|
||||
CRITICAL RULES - You MUST follow these:
|
||||
|
||||
1. **Never Stop with Incomplete Work**
|
||||
- Before stopping, verify ALL tasks in your todo list are complete
|
||||
- Check that all requested features are implemented
|
||||
- Ensure tests pass (if applicable)
|
||||
- Verify no error messages remain unaddressed
|
||||
|
||||
2. **Task Completion Verification**
|
||||
- Mark tasks complete ONLY when fully done
|
||||
- If blocked, create a new task describing the blocker
|
||||
- If a task fails, don't mark it complete - fix it
|
||||
|
||||
3. **Quality Gates**
|
||||
- Code compiles/runs without errors
|
||||
- All requested functionality works
|
||||
- No obvious bugs or issues remain
|
||||
|
||||
4. **When to Stop**
|
||||
You may ONLY stop when:
|
||||
- All tasks in the todo list are marked complete
|
||||
- User explicitly says "stop" or "that's enough"
|
||||
- You've verified the work meets requirements
|
||||
|
||||
5. **If Uncertain**
|
||||
- Ask the user for clarification
|
||||
- Create a verification task
|
||||
- Continue investigating rather than stopping prematurely
|
||||
`;
|
||||
|
||||
/**
|
||||
* Check prompt for signals that all work is done
|
||||
*/
|
||||
export function detectCompletionSignals(response: string): {
|
||||
claimed: boolean;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
reason: string;
|
||||
} {
|
||||
const completionPatterns = [
|
||||
/all (?:tasks?|work|items?) (?:are |is )?(?:now )?(?:complete|done|finished)/i,
|
||||
/I(?:'ve| have) (?:completed|finished|done) (?:all|everything)/i,
|
||||
/everything (?:is|has been) (?:complete|done|finished)/i,
|
||||
/no (?:more|remaining|outstanding) (?:tasks?|work|items?)/i
|
||||
];
|
||||
|
||||
const uncertaintyPatterns = [
|
||||
/(?:should|might|could) (?:be|have)/i,
|
||||
/I think|I believe|probably|maybe/i,
|
||||
/unless|except|but/i
|
||||
];
|
||||
|
||||
const hasCompletion = completionPatterns.some(p => p.test(response));
|
||||
const hasUncertainty = uncertaintyPatterns.some(p => p.test(response));
|
||||
|
||||
if (!hasCompletion) {
|
||||
return {
|
||||
claimed: false,
|
||||
confidence: 'high',
|
||||
reason: 'No completion claim detected'
|
||||
};
|
||||
}
|
||||
|
||||
if (hasUncertainty) {
|
||||
return {
|
||||
claimed: true,
|
||||
confidence: 'low',
|
||||
reason: 'Completion claimed with uncertainty language'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
claimed: true,
|
||||
confidence: 'high',
|
||||
reason: 'Clear completion claim detected'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a verification prompt to ensure work is complete
|
||||
*/
|
||||
export function generateVerificationPrompt(taskSummary: string): string {
|
||||
return `Before concluding, please verify the following:
|
||||
|
||||
1. Review your todo list - are ALL items marked complete?
|
||||
2. Have you addressed: ${taskSummary}
|
||||
3. Are there any errors or issues remaining?
|
||||
4. Does the implementation meet the original requirements?
|
||||
|
||||
If everything is truly complete, confirm by saying "All tasks verified complete."
|
||||
If anything remains, continue working on it.`;
|
||||
}
|
||||
16
src/features/index.ts
Normal file
16
src/features/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Features Module Exports
|
||||
*/
|
||||
|
||||
export {
|
||||
createMagicKeywordProcessor,
|
||||
detectMagicKeywords,
|
||||
builtInMagicKeywords
|
||||
} from './magic-keywords.js';
|
||||
|
||||
export {
|
||||
createContinuationHook,
|
||||
continuationSystemPromptAddition,
|
||||
detectCompletionSignals,
|
||||
generateVerificationPrompt
|
||||
} from './continuation-enforcement.js';
|
||||
207
src/features/magic-keywords.ts
Normal file
207
src/features/magic-keywords.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Magic Keywords Feature
|
||||
*
|
||||
* Detects special keywords in prompts and activates enhanced behaviors:
|
||||
* - ultrawork/ulw: Maximum performance mode with parallel orchestration
|
||||
* - search/find: Maximized search effort
|
||||
* - analyze/investigate: Deep analysis mode
|
||||
*/
|
||||
|
||||
import type { MagicKeyword, PluginConfig } from '../shared/types.js';
|
||||
|
||||
/**
|
||||
* Ultrawork mode enhancement
|
||||
* Activates maximum performance with parallel agent orchestration
|
||||
*/
|
||||
const ultraworkEnhancement: MagicKeyword = {
|
||||
triggers: ['ultrawork', 'ulw', 'uw'],
|
||||
description: 'Activates maximum performance mode with parallel agent orchestration',
|
||||
action: (prompt: string) => {
|
||||
// Remove the trigger word and add enhancement instructions
|
||||
const cleanPrompt = removeTriggerWords(prompt, ['ultrawork', 'ulw', 'uw']);
|
||||
|
||||
return `[ULTRAWORK MODE ACTIVATED]
|
||||
|
||||
${cleanPrompt}
|
||||
|
||||
## Enhanced Execution Instructions
|
||||
- Use PARALLEL agent execution for all independent subtasks
|
||||
- Delegate aggressively to specialized subagents
|
||||
- Maximize throughput by running multiple operations concurrently
|
||||
- Continue until ALL tasks are 100% complete - verify before stopping
|
||||
- Use background agents for long-running operations
|
||||
- Report progress frequently
|
||||
|
||||
## Subagent Strategy
|
||||
- Use 'oracle' for complex debugging and architecture decisions
|
||||
- Use 'librarian' for documentation and codebase research
|
||||
- Use 'explore' for quick pattern matching and file searches
|
||||
- Use 'frontend-engineer' for UI/UX work
|
||||
- Use 'document-writer' for documentation tasks
|
||||
- Use 'multimodal-looker' for analyzing images/screenshots
|
||||
|
||||
CRITICAL: Do NOT stop until every task is verified complete.`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Search mode enhancement
|
||||
* Maximizes search effort and thoroughness
|
||||
*/
|
||||
const searchEnhancement: MagicKeyword = {
|
||||
triggers: ['search', 'find', 'locate'],
|
||||
description: 'Maximizes search effort and thoroughness',
|
||||
action: (prompt: string) => {
|
||||
// Check if search-related triggers are present as commands
|
||||
const hasSearchCommand = /\b(search|find|locate)\b/i.test(prompt);
|
||||
|
||||
if (!hasSearchCommand) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
return `${prompt}
|
||||
|
||||
## Search Enhancement Instructions
|
||||
- Use multiple search strategies (glob patterns, grep, AST search)
|
||||
- Search across ALL relevant file types
|
||||
- Include hidden files and directories when appropriate
|
||||
- Try alternative naming conventions (camelCase, snake_case, kebab-case)
|
||||
- Look in common locations: src/, lib/, utils/, helpers/, services/
|
||||
- Check for related files (tests, types, interfaces)
|
||||
- Report ALL findings, not just the first match
|
||||
- If initial search fails, try broader patterns`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyze mode enhancement
|
||||
* Activates deep analysis and investigation mode
|
||||
*/
|
||||
const analyzeEnhancement: MagicKeyword = {
|
||||
triggers: ['analyze', 'investigate', 'examine', 'debug'],
|
||||
description: 'Activates deep analysis and investigation mode',
|
||||
action: (prompt: string) => {
|
||||
// Check if analysis-related triggers are present
|
||||
const hasAnalyzeCommand = /\b(analyze|investigate|examine|debug)\b/i.test(prompt);
|
||||
|
||||
if (!hasAnalyzeCommand) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
return `${prompt}
|
||||
|
||||
## Deep Analysis Instructions
|
||||
- Thoroughly examine all relevant code paths
|
||||
- Trace data flow from source to destination
|
||||
- Identify edge cases and potential failure modes
|
||||
- Check for related issues in similar code patterns
|
||||
- Use LSP tools for type information and references
|
||||
- Use AST tools for structural code analysis
|
||||
- Document findings with specific file:line references
|
||||
- Propose concrete solutions with code examples
|
||||
- Consider performance, security, and maintainability implications`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove trigger words from a prompt
|
||||
*/
|
||||
function removeTriggerWords(prompt: string, triggers: string[]): string {
|
||||
let result = prompt;
|
||||
for (const trigger of triggers) {
|
||||
const regex = new RegExp(`\\b${trigger}\\b`, 'gi');
|
||||
result = result.replace(regex, '');
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* All built-in magic keyword definitions
|
||||
*/
|
||||
export const builtInMagicKeywords: MagicKeyword[] = [
|
||||
ultraworkEnhancement,
|
||||
searchEnhancement,
|
||||
analyzeEnhancement
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a magic keyword processor with custom triggers
|
||||
*/
|
||||
export function createMagicKeywordProcessor(config?: PluginConfig['magicKeywords']): (prompt: string) => string {
|
||||
const keywords = [...builtInMagicKeywords];
|
||||
|
||||
// Override triggers from config
|
||||
if (config) {
|
||||
if (config.ultrawork) {
|
||||
const ultrawork = keywords.find(k => k.triggers.includes('ultrawork'));
|
||||
if (ultrawork) {
|
||||
ultrawork.triggers = config.ultrawork;
|
||||
}
|
||||
}
|
||||
if (config.search) {
|
||||
const search = keywords.find(k => k.triggers.includes('search'));
|
||||
if (search) {
|
||||
search.triggers = config.search;
|
||||
}
|
||||
}
|
||||
if (config.analyze) {
|
||||
const analyze = keywords.find(k => k.triggers.includes('analyze'));
|
||||
if (analyze) {
|
||||
analyze.triggers = config.analyze;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (prompt: string): string => {
|
||||
let result = prompt;
|
||||
|
||||
for (const keyword of keywords) {
|
||||
const hasKeyword = keyword.triggers.some(trigger => {
|
||||
const regex = new RegExp(`\\b${trigger}\\b`, 'i');
|
||||
return regex.test(result);
|
||||
});
|
||||
|
||||
if (hasKeyword) {
|
||||
result = keyword.action(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a prompt contains any magic keywords
|
||||
*/
|
||||
export function detectMagicKeywords(prompt: string, config?: PluginConfig['magicKeywords']): string[] {
|
||||
const detected: string[] = [];
|
||||
const keywords = [...builtInMagicKeywords];
|
||||
|
||||
// Apply config overrides
|
||||
if (config) {
|
||||
if (config.ultrawork) {
|
||||
const ultrawork = keywords.find(k => k.triggers.includes('ultrawork'));
|
||||
if (ultrawork) ultrawork.triggers = config.ultrawork;
|
||||
}
|
||||
if (config.search) {
|
||||
const search = keywords.find(k => k.triggers.includes('search'));
|
||||
if (search) search.triggers = config.search;
|
||||
}
|
||||
if (config.analyze) {
|
||||
const analyze = keywords.find(k => k.triggers.includes('analyze'));
|
||||
if (analyze) analyze.triggers = config.analyze;
|
||||
}
|
||||
}
|
||||
|
||||
for (const keyword of keywords) {
|
||||
for (const trigger of keyword.triggers) {
|
||||
const regex = new RegExp(`\\b${trigger}\\b`, 'i');
|
||||
if (regex.test(prompt)) {
|
||||
detected.push(trigger);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detected;
|
||||
}
|
||||
214
src/index.ts
Normal file
214
src/index.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Oh-My-Claude-Sisyphus
|
||||
*
|
||||
* A multi-agent orchestration system for the Claude Agent SDK.
|
||||
* Port of oh-my-opencode for Claude.
|
||||
*
|
||||
* Main features:
|
||||
* - Sisyphus: Primary orchestrator that delegates to specialized subagents
|
||||
* - Parallel execution: Background agents run concurrently
|
||||
* - LSP/AST tools: IDE-like capabilities for agents
|
||||
* - Context management: Auto-injection from AGENTS.md/CLAUDE.md
|
||||
* - Continuation enforcement: Ensures tasks complete before stopping
|
||||
* - Magic keywords: Special triggers for enhanced behaviors
|
||||
*/
|
||||
|
||||
import { loadConfig, findContextFiles, loadContextFromFiles } from './config/loader.js';
|
||||
import { getAgentDefinitions, sisyphusSystemPrompt } from './agents/definitions.js';
|
||||
import { getDefaultMcpServers, toSdkMcpFormat } from './mcp/servers.js';
|
||||
import { createMagicKeywordProcessor, detectMagicKeywords } from './features/magic-keywords.js';
|
||||
import { continuationSystemPromptAddition } from './features/continuation-enforcement.js';
|
||||
import type { PluginConfig, SessionState } from './shared/types.js';
|
||||
|
||||
export { loadConfig, getAgentDefinitions, sisyphusSystemPrompt };
|
||||
export { getDefaultMcpServers, toSdkMcpFormat } from './mcp/servers.js';
|
||||
export { lspTools, astTools, allCustomTools } from './tools/index.js';
|
||||
export { createMagicKeywordProcessor, detectMagicKeywords } from './features/magic-keywords.js';
|
||||
export * from './shared/types.js';
|
||||
|
||||
/**
|
||||
* Options for creating a Sisyphus session
|
||||
*/
|
||||
export interface SisyphusOptions {
|
||||
/** Custom configuration (merged with loaded config) */
|
||||
config?: Partial<PluginConfig>;
|
||||
/** Working directory (default: process.cwd()) */
|
||||
workingDirectory?: string;
|
||||
/** Skip loading config files */
|
||||
skipConfigLoad?: boolean;
|
||||
/** Skip context file injection */
|
||||
skipContextInjection?: boolean;
|
||||
/** Custom system prompt addition */
|
||||
customSystemPrompt?: string;
|
||||
/** API key (default: from ANTHROPIC_API_KEY env) */
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of creating a Sisyphus session
|
||||
*/
|
||||
export interface SisyphusSession {
|
||||
/** The query options to pass to Claude Agent SDK */
|
||||
queryOptions: {
|
||||
options: {
|
||||
systemPrompt: string;
|
||||
agents: Record<string, { description: string; prompt: string; tools: string[]; model?: string }>;
|
||||
mcpServers: Record<string, { command: string; args: string[] }>;
|
||||
allowedTools: string[];
|
||||
permissionMode: string;
|
||||
};
|
||||
};
|
||||
/** Session state */
|
||||
state: SessionState;
|
||||
/** Loaded configuration */
|
||||
config: PluginConfig;
|
||||
/** Process a prompt (applies magic keywords) */
|
||||
processPrompt: (prompt: string) => string;
|
||||
/** Get detected magic keywords in a prompt */
|
||||
detectKeywords: (prompt: string) => string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Sisyphus orchestration session
|
||||
*
|
||||
* This prepares all the configuration and options needed
|
||||
* to run a query with the Claude Agent SDK.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createSisyphusSession } from 'oh-my-claude-sisyphus';
|
||||
* import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
*
|
||||
* const session = createSisyphusSession();
|
||||
*
|
||||
* // Use with Claude Agent SDK
|
||||
* for await (const message of query({
|
||||
* prompt: session.processPrompt("ultrawork refactor the authentication module"),
|
||||
* ...session.queryOptions
|
||||
* })) {
|
||||
* console.log(message);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createSisyphusSession(options?: SisyphusOptions): SisyphusSession {
|
||||
// Load configuration
|
||||
const loadedConfig = options?.skipConfigLoad ? {} : loadConfig();
|
||||
const config: PluginConfig = {
|
||||
...loadedConfig,
|
||||
...options?.config
|
||||
};
|
||||
|
||||
// Find and load context files
|
||||
let contextAddition = '';
|
||||
if (!options?.skipContextInjection && config.features?.autoContextInjection !== false) {
|
||||
const contextFiles = findContextFiles(options?.workingDirectory);
|
||||
if (contextFiles.length > 0) {
|
||||
contextAddition = `\n\n## Project Context\n\n${loadContextFromFiles(contextFiles)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Build system prompt
|
||||
let systemPrompt = sisyphusSystemPrompt;
|
||||
|
||||
// Add continuation enforcement
|
||||
if (config.features?.continuationEnforcement !== false) {
|
||||
systemPrompt += continuationSystemPromptAddition;
|
||||
}
|
||||
|
||||
// Add custom system prompt
|
||||
if (options?.customSystemPrompt) {
|
||||
systemPrompt += `\n\n## Custom Instructions\n\n${options.customSystemPrompt}`;
|
||||
}
|
||||
|
||||
// Add context from files
|
||||
if (contextAddition) {
|
||||
systemPrompt += contextAddition;
|
||||
}
|
||||
|
||||
// Get agent definitions
|
||||
const agents = getAgentDefinitions();
|
||||
|
||||
// Build MCP servers configuration
|
||||
const mcpServers = getDefaultMcpServers({
|
||||
exaApiKey: config.mcpServers?.exa?.apiKey,
|
||||
enableExa: config.mcpServers?.exa?.enabled,
|
||||
enableContext7: config.mcpServers?.context7?.enabled,
|
||||
enableGrepApp: config.mcpServers?.grepApp?.enabled
|
||||
});
|
||||
|
||||
// Build allowed tools list
|
||||
const allowedTools: string[] = [
|
||||
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'TodoWrite'
|
||||
];
|
||||
|
||||
if (config.permissions?.allowBash !== false) {
|
||||
allowedTools.push('Bash');
|
||||
}
|
||||
|
||||
if (config.permissions?.allowEdit !== false) {
|
||||
allowedTools.push('Edit');
|
||||
}
|
||||
|
||||
if (config.permissions?.allowWrite !== false) {
|
||||
allowedTools.push('Write');
|
||||
}
|
||||
|
||||
// Add MCP tool names
|
||||
for (const serverName of Object.keys(mcpServers)) {
|
||||
allowedTools.push(`mcp__${serverName}__*`);
|
||||
}
|
||||
|
||||
// Create magic keyword processor
|
||||
const processPrompt = createMagicKeywordProcessor(config.magicKeywords);
|
||||
|
||||
// Initialize session state
|
||||
const state: SessionState = {
|
||||
activeAgents: new Map(),
|
||||
backgroundTasks: [],
|
||||
contextFiles: findContextFiles(options?.workingDirectory)
|
||||
};
|
||||
|
||||
return {
|
||||
queryOptions: {
|
||||
options: {
|
||||
systemPrompt,
|
||||
agents,
|
||||
mcpServers: toSdkMcpFormat(mcpServers),
|
||||
allowedTools,
|
||||
permissionMode: 'acceptEdits'
|
||||
}
|
||||
},
|
||||
state,
|
||||
config,
|
||||
processPrompt,
|
||||
detectKeywords: (prompt: string) => detectMagicKeywords(prompt, config.magicKeywords)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick helper to process a prompt with Sisyphus enhancements
|
||||
*/
|
||||
export function enhancePrompt(prompt: string, config?: PluginConfig): string {
|
||||
const processor = createMagicKeywordProcessor(config?.magicKeywords);
|
||||
return processor(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for Sisyphus (for direct use)
|
||||
*/
|
||||
export function getSisyphusSystemPrompt(options?: {
|
||||
includeContinuation?: boolean;
|
||||
customAddition?: string;
|
||||
}): string {
|
||||
let prompt = sisyphusSystemPrompt;
|
||||
|
||||
if (options?.includeContinuation !== false) {
|
||||
prompt += continuationSystemPromptAddition;
|
||||
}
|
||||
|
||||
if (options?.customAddition) {
|
||||
prompt += `\n\n${options.customAddition}`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
18
src/mcp/index.ts
Normal file
18
src/mcp/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* MCP Server Module Exports
|
||||
*/
|
||||
|
||||
export {
|
||||
createExaServer,
|
||||
createContext7Server,
|
||||
createGrepAppServer,
|
||||
createPlaywrightServer,
|
||||
createFilesystemServer,
|
||||
createGitServer,
|
||||
createMemoryServer,
|
||||
createFetchServer,
|
||||
getDefaultMcpServers,
|
||||
toSdkMcpFormat
|
||||
} from './servers.js';
|
||||
|
||||
export type { McpServerConfig, McpServersConfig } from './servers.js';
|
||||
163
src/mcp/servers.ts
Normal file
163
src/mcp/servers.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* MCP Server Configurations
|
||||
*
|
||||
* Predefined MCP server configurations for common integrations:
|
||||
* - Exa: AI-powered web search
|
||||
* - Context7: Official documentation lookup
|
||||
* - grep.app: GitHub code search
|
||||
* - Playwright: Browser automation
|
||||
*/
|
||||
|
||||
export interface McpServerConfig {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exa MCP Server - AI-powered web search
|
||||
* Requires: EXA_API_KEY environment variable
|
||||
*/
|
||||
export function createExaServer(apiKey?: string): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@anthropic-ai/exa-mcp-server'],
|
||||
env: apiKey ? { EXA_API_KEY: apiKey } : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Context7 MCP Server - Official documentation lookup
|
||||
* Provides access to official docs for popular libraries
|
||||
*/
|
||||
export function createContext7Server(): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@anthropic-ai/context7-mcp-server']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* grep.app MCP Server - GitHub code search
|
||||
* Search across public GitHub repositories
|
||||
*/
|
||||
export function createGrepAppServer(): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@anthropic-ai/grep-app-mcp-server']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Playwright MCP Server - Browser automation
|
||||
* Enables agents to interact with web pages
|
||||
*/
|
||||
export function createPlaywrightServer(): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@playwright/mcp@latest']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filesystem MCP Server - Extended file operations
|
||||
* Provides additional file system capabilities
|
||||
*/
|
||||
export function createFilesystemServer(allowedPaths: string[]): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-filesystem', ...allowedPaths]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Git MCP Server - Git operations
|
||||
* Provides git-specific operations beyond basic bash
|
||||
*/
|
||||
export function createGitServer(repoPath?: string): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-git', repoPath ?? '.']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory MCP Server - Persistent memory
|
||||
* Allows agents to store and retrieve information across sessions
|
||||
*/
|
||||
export function createMemoryServer(): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-memory']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch MCP Server - HTTP requests
|
||||
* Make HTTP requests to APIs
|
||||
*/
|
||||
export function createFetchServer(): McpServerConfig {
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-fetch']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all default MCP servers for the Sisyphus system
|
||||
*/
|
||||
export interface McpServersConfig {
|
||||
exa?: McpServerConfig;
|
||||
context7?: McpServerConfig;
|
||||
grepApp?: McpServerConfig;
|
||||
playwright?: McpServerConfig;
|
||||
memory?: McpServerConfig;
|
||||
}
|
||||
|
||||
export function getDefaultMcpServers(options?: {
|
||||
exaApiKey?: string;
|
||||
enableExa?: boolean;
|
||||
enableContext7?: boolean;
|
||||
enableGrepApp?: boolean;
|
||||
enablePlaywright?: boolean;
|
||||
enableMemory?: boolean;
|
||||
}): McpServersConfig {
|
||||
const servers: McpServersConfig = {};
|
||||
|
||||
if (options?.enableExa !== false) {
|
||||
servers.exa = createExaServer(options?.exaApiKey);
|
||||
}
|
||||
|
||||
if (options?.enableContext7 !== false) {
|
||||
servers.context7 = createContext7Server();
|
||||
}
|
||||
|
||||
if (options?.enableGrepApp !== false) {
|
||||
servers.grepApp = createGrepAppServer();
|
||||
}
|
||||
|
||||
if (options?.enablePlaywright) {
|
||||
servers.playwright = createPlaywrightServer();
|
||||
}
|
||||
|
||||
if (options?.enableMemory) {
|
||||
servers.memory = createMemoryServer();
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MCP servers config to SDK format
|
||||
*/
|
||||
export function toSdkMcpFormat(servers: McpServersConfig): Record<string, McpServerConfig> {
|
||||
const result: Record<string, McpServerConfig> = {};
|
||||
|
||||
for (const [name, config] of Object.entries(servers)) {
|
||||
if (config) {
|
||||
result[name] = config;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
5
src/shared/index.ts
Normal file
5
src/shared/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Shared Types Export
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
112
src/shared/types.ts
Normal file
112
src/shared/types.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Shared types for Oh-My-Claude-Sisyphus
|
||||
*/
|
||||
|
||||
export type ModelType = 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||
|
||||
export interface AgentConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
tools: string[];
|
||||
model?: ModelType;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
// Agent model overrides
|
||||
agents?: {
|
||||
sisyphus?: { model?: string };
|
||||
oracle?: { model?: string; enabled?: boolean };
|
||||
librarian?: { model?: string };
|
||||
explore?: { model?: string };
|
||||
frontendEngineer?: { model?: string; enabled?: boolean };
|
||||
documentWriter?: { model?: string; enabled?: boolean };
|
||||
multimodalLooker?: { model?: string; enabled?: boolean };
|
||||
// New agents from oh-my-opencode
|
||||
momus?: { model?: string; enabled?: boolean };
|
||||
metis?: { model?: string; enabled?: boolean };
|
||||
orchestratorSisyphus?: { model?: string; enabled?: boolean };
|
||||
sisyphusJunior?: { model?: string; enabled?: boolean };
|
||||
prometheus?: { model?: string; enabled?: boolean };
|
||||
};
|
||||
|
||||
// Feature toggles
|
||||
features?: {
|
||||
parallelExecution?: boolean;
|
||||
lspTools?: boolean;
|
||||
astTools?: boolean;
|
||||
continuationEnforcement?: boolean;
|
||||
autoContextInjection?: boolean;
|
||||
};
|
||||
|
||||
// MCP server configurations
|
||||
mcpServers?: {
|
||||
exa?: { enabled?: boolean; apiKey?: string };
|
||||
context7?: { enabled?: boolean };
|
||||
grepApp?: { enabled?: boolean };
|
||||
};
|
||||
|
||||
// Permission settings
|
||||
permissions?: {
|
||||
allowBash?: boolean;
|
||||
allowEdit?: boolean;
|
||||
allowWrite?: boolean;
|
||||
maxBackgroundTasks?: number;
|
||||
};
|
||||
|
||||
// Magic keyword customization
|
||||
magicKeywords?: {
|
||||
ultrawork?: string[];
|
||||
search?: string[];
|
||||
analyze?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionState {
|
||||
sessionId?: string;
|
||||
activeAgents: Map<string, AgentState>;
|
||||
backgroundTasks: BackgroundTask[];
|
||||
contextFiles: string[];
|
||||
}
|
||||
|
||||
export interface AgentState {
|
||||
name: string;
|
||||
status: 'idle' | 'running' | 'completed' | 'error';
|
||||
lastMessage?: string;
|
||||
startTime?: number;
|
||||
}
|
||||
|
||||
export interface BackgroundTask {
|
||||
id: string;
|
||||
agentName: string;
|
||||
prompt: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'error';
|
||||
result?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface MagicKeyword {
|
||||
triggers: string[];
|
||||
action: (prompt: string) => string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface HookDefinition {
|
||||
event: 'PreToolUse' | 'PostToolUse' | 'Stop' | 'SessionStart' | 'SessionEnd' | 'UserPromptSubmit';
|
||||
matcher?: string;
|
||||
command?: string;
|
||||
handler?: (context: HookContext) => Promise<HookResult>;
|
||||
}
|
||||
|
||||
export interface HookContext {
|
||||
toolName?: string;
|
||||
toolInput?: unknown;
|
||||
toolOutput?: unknown;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface HookResult {
|
||||
continue: boolean;
|
||||
message?: string;
|
||||
modifiedInput?: unknown;
|
||||
}
|
||||
422
src/tools/ast-tools.ts
Normal file
422
src/tools/ast-tools.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* AST Tools using ast-grep
|
||||
*
|
||||
* Provides AST-aware code search and transformation:
|
||||
* - Pattern matching with meta-variables ($VAR, $$$)
|
||||
* - Code replacement while preserving structure
|
||||
* - Support for 25+ programming languages
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
||||
import { join, extname, resolve } from 'path';
|
||||
|
||||
// Dynamic import for @ast-grep/napi (ESM module)
|
||||
let sgModule: typeof import('@ast-grep/napi') | null = null;
|
||||
|
||||
async function getSgModule() {
|
||||
if (!sgModule) {
|
||||
sgModule = await import('@ast-grep/napi');
|
||||
}
|
||||
return sgModule;
|
||||
}
|
||||
|
||||
export interface AstToolDefinition<T extends z.ZodRawShape> {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: T;
|
||||
handler: (args: z.infer<z.ZodObject<T>>) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported languages for AST analysis
|
||||
* Maps to ast-grep language identifiers
|
||||
*/
|
||||
export const SUPPORTED_LANGUAGES: [string, ...string[]] = [
|
||||
'javascript', 'typescript', 'tsx', 'python', 'ruby', 'go', 'rust',
|
||||
'java', 'kotlin', 'swift', 'c', 'cpp', 'csharp',
|
||||
'html', 'css', 'json', 'yaml'
|
||||
];
|
||||
|
||||
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||||
|
||||
/**
|
||||
* Map file extensions to ast-grep language identifiers
|
||||
*/
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
'.js': 'javascript',
|
||||
'.mjs': 'javascript',
|
||||
'.cjs': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.mts': 'typescript',
|
||||
'.cts': 'typescript',
|
||||
'.tsx': 'tsx',
|
||||
'.py': 'python',
|
||||
'.rb': 'ruby',
|
||||
'.go': 'go',
|
||||
'.rs': 'rust',
|
||||
'.java': 'java',
|
||||
'.kt': 'kotlin',
|
||||
'.kts': 'kotlin',
|
||||
'.swift': 'swift',
|
||||
'.c': 'c',
|
||||
'.h': 'c',
|
||||
'.cpp': 'cpp',
|
||||
'.cc': 'cpp',
|
||||
'.cxx': 'cpp',
|
||||
'.hpp': 'cpp',
|
||||
'.cs': 'csharp',
|
||||
'.html': 'html',
|
||||
'.htm': 'html',
|
||||
'.css': 'css',
|
||||
'.json': 'json',
|
||||
'.yaml': 'yaml',
|
||||
'.yml': 'yaml'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get files matching the language in a directory
|
||||
*/
|
||||
function getFilesForLanguage(dirPath: string, language: string, maxFiles = 1000): string[] {
|
||||
const files: string[] = [];
|
||||
const extensions = Object.entries(EXT_TO_LANG)
|
||||
.filter(([_, lang]) => lang === language)
|
||||
.map(([ext]) => ext);
|
||||
|
||||
function walk(dir: string) {
|
||||
if (files.length >= maxFiles) return;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (files.length >= maxFiles) return;
|
||||
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
// Skip common non-source directories
|
||||
if (entry.isDirectory()) {
|
||||
if (!['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv'].includes(entry.name)) {
|
||||
walk(fullPath);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
const ext = extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore permission errors
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedPath = resolve(dirPath);
|
||||
const stat = statSync(resolvedPath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
return [resolvedPath];
|
||||
}
|
||||
|
||||
walk(resolvedPath);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a match result for display
|
||||
*/
|
||||
function formatMatch(
|
||||
filePath: string,
|
||||
matchText: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
context: number,
|
||||
fileContent: string
|
||||
): string {
|
||||
const lines = fileContent.split('\n');
|
||||
const contextStart = Math.max(0, startLine - context - 1);
|
||||
const contextEnd = Math.min(lines.length, endLine + context);
|
||||
|
||||
const contextLines = lines.slice(contextStart, contextEnd);
|
||||
const numberedLines = contextLines.map((line, i) => {
|
||||
const lineNum = contextStart + i + 1;
|
||||
const isMatch = lineNum >= startLine && lineNum <= endLine;
|
||||
const prefix = isMatch ? '>' : ' ';
|
||||
return `${prefix} ${lineNum.toString().padStart(4)}: ${line}`;
|
||||
});
|
||||
|
||||
return `${filePath}:${startLine}\n${numberedLines.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* AST Grep Search Tool - Find code patterns using AST matching
|
||||
*/
|
||||
export const astGrepSearchTool: AstToolDefinition<{
|
||||
pattern: z.ZodString;
|
||||
language: z.ZodEnum<[string, ...string[]]>;
|
||||
path: z.ZodOptional<z.ZodString>;
|
||||
context: z.ZodOptional<z.ZodNumber>;
|
||||
maxResults: z.ZodOptional<z.ZodNumber>;
|
||||
}> = {
|
||||
name: 'ast_grep_search',
|
||||
description: `Search for code patterns using AST matching. More precise than text search.
|
||||
|
||||
Use meta-variables in patterns:
|
||||
- $NAME - matches any single AST node (identifier, expression, etc.)
|
||||
- $$$ARGS - matches multiple nodes (for function arguments, list items, etc.)
|
||||
|
||||
Examples:
|
||||
- "function $NAME($$$ARGS)" - find all function declarations
|
||||
- "console.log($MSG)" - find all console.log calls
|
||||
- "if ($COND) { $$$BODY }" - find all if statements
|
||||
- "$X === null" - find null equality checks
|
||||
- "import $$$IMPORTS from '$MODULE'" - find imports
|
||||
|
||||
Note: Patterns must be valid AST nodes for the language.`,
|
||||
schema: {
|
||||
pattern: z.string().describe('AST pattern with meta-variables ($VAR, $$$VARS)'),
|
||||
language: z.enum(SUPPORTED_LANGUAGES).describe('Programming language'),
|
||||
path: z.string().optional().describe('Directory or file to search (default: current directory)'),
|
||||
context: z.number().int().min(0).max(10).optional().describe('Lines of context around matches (default: 2)'),
|
||||
maxResults: z.number().int().min(1).max(100).optional().describe('Maximum results to return (default: 20)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { pattern, language, path = '.', context = 2, maxResults = 20 } = args;
|
||||
|
||||
try {
|
||||
const sg = await getSgModule();
|
||||
const files = getFilesForLanguage(path, language);
|
||||
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No ${language} files found in ${path}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const results: string[] = [];
|
||||
let totalMatches = 0;
|
||||
|
||||
for (const filePath of files) {
|
||||
if (totalMatches >= maxResults) break;
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const root = sg.parse(language as any, content).root();
|
||||
const matches = root.findAll(pattern);
|
||||
|
||||
for (const match of matches) {
|
||||
if (totalMatches >= maxResults) break;
|
||||
|
||||
const range = match.range();
|
||||
const startLine = range.start.line + 1;
|
||||
const endLine = range.end.line + 1;
|
||||
|
||||
results.push(formatMatch(
|
||||
filePath,
|
||||
match.text(),
|
||||
startLine,
|
||||
endLine,
|
||||
context,
|
||||
content
|
||||
));
|
||||
totalMatches++;
|
||||
}
|
||||
} catch {
|
||||
// Skip files that fail to parse
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No matches found for pattern: ${pattern}\n\nSearched ${files.length} ${language} file(s) in ${path}\n\nTip: Ensure the pattern is a valid AST node. For example:\n- Use "function $NAME" not just "$NAME"\n- Use "console.log($X)" not "console.log"`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const header = `Found ${totalMatches} match(es) in ${files.length} file(s)\nPattern: ${pattern}\n\n`;
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: header + results.join('\n\n---\n\n')
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Error in AST search: ${error instanceof Error ? error.message : String(error)}\n\nCommon issues:\n- Pattern must be a complete AST node\n- Language must match file type\n- Check that @ast-grep/napi is installed`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* AST Grep Replace Tool - Replace code patterns using AST matching
|
||||
*/
|
||||
export const astGrepReplaceTool: AstToolDefinition<{
|
||||
pattern: z.ZodString;
|
||||
replacement: z.ZodString;
|
||||
language: z.ZodEnum<[string, ...string[]]>;
|
||||
path: z.ZodOptional<z.ZodString>;
|
||||
dryRun: z.ZodOptional<z.ZodBoolean>;
|
||||
}> = {
|
||||
name: 'ast_grep_replace',
|
||||
description: `Replace code patterns using AST matching. Preserves matched content via meta-variables.
|
||||
|
||||
Use meta-variables in both pattern and replacement:
|
||||
- $NAME in pattern captures a node, use $NAME in replacement to insert it
|
||||
- $$$ARGS captures multiple nodes
|
||||
|
||||
Examples:
|
||||
- Pattern: "console.log($MSG)" → Replacement: "logger.info($MSG)"
|
||||
- Pattern: "var $NAME = $VALUE" → Replacement: "const $NAME = $VALUE"
|
||||
- Pattern: "$OBJ.forEach(($ITEM) => { $$$BODY })" → Replacement: "for (const $ITEM of $OBJ) { $$$BODY }"
|
||||
|
||||
IMPORTANT: dryRun=true (default) only previews changes. Set dryRun=false to apply.`,
|
||||
schema: {
|
||||
pattern: z.string().describe('Pattern to match'),
|
||||
replacement: z.string().describe('Replacement pattern (use same meta-variables)'),
|
||||
language: z.enum(SUPPORTED_LANGUAGES).describe('Programming language'),
|
||||
path: z.string().optional().describe('Directory or file to search (default: current directory)'),
|
||||
dryRun: z.boolean().optional().describe('Preview only, don\'t apply changes (default: true)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { pattern, replacement, language, path = '.', dryRun = true } = args;
|
||||
|
||||
try {
|
||||
const sg = await getSgModule();
|
||||
const files = getFilesForLanguage(path, language);
|
||||
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No ${language} files found in ${path}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const changes: { file: string; before: string; after: string; line: number }[] = [];
|
||||
let totalReplacements = 0;
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const root = sg.parse(language as any, content).root();
|
||||
const matches = root.findAll(pattern);
|
||||
|
||||
if (matches.length === 0) continue;
|
||||
|
||||
// Collect all edits for this file
|
||||
const edits: { start: number; end: number; replacement: string; line: number; before: string }[] = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const range = match.range();
|
||||
const startOffset = range.start.index;
|
||||
const endOffset = range.end.index;
|
||||
|
||||
// Build replacement by substituting meta-variables
|
||||
let finalReplacement = replacement;
|
||||
|
||||
// Get all captured meta-variables
|
||||
// ast-grep captures are accessed via match.getMatch() or by variable name
|
||||
// For simplicity, we'll use a basic approach here
|
||||
const matchedText = match.text();
|
||||
|
||||
// Try to get named captures
|
||||
try {
|
||||
// Replace meta-variables in the replacement string
|
||||
const metaVars = replacement.match(/\$\$?\$?[A-Z_][A-Z0-9_]*/g) || [];
|
||||
for (const metaVar of metaVars) {
|
||||
const varName = metaVar.replace(/^\$+/, '');
|
||||
const captured = match.getMatch(varName);
|
||||
if (captured) {
|
||||
finalReplacement = finalReplacement.replace(metaVar, captured.text());
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If meta-variable extraction fails, use pattern as-is
|
||||
}
|
||||
|
||||
edits.push({
|
||||
start: startOffset,
|
||||
end: endOffset,
|
||||
replacement: finalReplacement,
|
||||
line: range.start.line + 1,
|
||||
before: matchedText
|
||||
});
|
||||
}
|
||||
|
||||
// Sort edits in reverse order to apply from end to start
|
||||
edits.sort((a, b) => b.start - a.start);
|
||||
|
||||
let newContent = content;
|
||||
for (const edit of edits) {
|
||||
const before = newContent.slice(edit.start, edit.end);
|
||||
newContent = newContent.slice(0, edit.start) + edit.replacement + newContent.slice(edit.end);
|
||||
|
||||
changes.push({
|
||||
file: filePath,
|
||||
before,
|
||||
after: edit.replacement,
|
||||
line: edit.line
|
||||
});
|
||||
totalReplacements++;
|
||||
}
|
||||
|
||||
if (!dryRun && edits.length > 0) {
|
||||
writeFileSync(filePath, newContent, 'utf-8');
|
||||
}
|
||||
} catch {
|
||||
// Skip files that fail to parse
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No matches found for pattern: ${pattern}\n\nSearched ${files.length} ${language} file(s) in ${path}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const mode = dryRun ? 'DRY RUN (no changes applied)' : 'CHANGES APPLIED';
|
||||
const header = `${mode}\n\nFound ${totalReplacements} replacement(s) in ${files.length} file(s)\nPattern: ${pattern}\nReplacement: ${replacement}\n\n`;
|
||||
|
||||
const changeList = changes.slice(0, 50).map(c =>
|
||||
`${c.file}:${c.line}\n - ${c.before}\n + ${c.after}`
|
||||
).join('\n\n');
|
||||
|
||||
const footer = changes.length > 50 ? `\n\n... and ${changes.length - 50} more changes` : '';
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: header + changeList + footer + (dryRun ? '\n\nTo apply changes, run with dryRun: false' : '')
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Error in AST replace: ${error instanceof Error ? error.message : String(error)}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all AST tool definitions
|
||||
*/
|
||||
export const astTools = [
|
||||
astGrepSearchTool,
|
||||
astGrepReplaceTool
|
||||
];
|
||||
157
src/tools/index.ts
Normal file
157
src/tools/index.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Tool Registry and MCP Server Creation
|
||||
*
|
||||
* This module exports all custom tools and provides helpers
|
||||
* for creating MCP servers with the Claude Agent SDK.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { lspTools } from './lsp-tools.js';
|
||||
import { astTools } from './ast-tools.js';
|
||||
|
||||
export { lspTools } from './lsp-tools.js';
|
||||
export { astTools } from './ast-tools.js';
|
||||
|
||||
/**
|
||||
* Generic tool definition type
|
||||
*/
|
||||
export interface GenericToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: z.ZodRawShape;
|
||||
handler: (args: unknown) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* All custom tools available in the system
|
||||
*/
|
||||
export const allCustomTools: GenericToolDefinition[] = [
|
||||
...lspTools as unknown as GenericToolDefinition[],
|
||||
...astTools as unknown as GenericToolDefinition[]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get tools by category
|
||||
*/
|
||||
export function getToolsByCategory(category: 'lsp' | 'ast' | 'all'): GenericToolDefinition[] {
|
||||
switch (category) {
|
||||
case 'lsp':
|
||||
return lspTools as unknown as GenericToolDefinition[];
|
||||
case 'ast':
|
||||
return astTools as unknown as GenericToolDefinition[];
|
||||
case 'all':
|
||||
return allCustomTools;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Zod schema object from a tool's schema definition
|
||||
*/
|
||||
export function createZodSchema<T extends z.ZodRawShape>(schema: T): z.ZodObject<T> {
|
||||
return z.object(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for creating tools compatible with Claude Agent SDK
|
||||
*/
|
||||
export interface SdkToolFormat {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our tool definitions to SDK format
|
||||
*/
|
||||
export function toSdkToolFormat(tool: GenericToolDefinition): SdkToolFormat {
|
||||
const zodSchema = z.object(tool.schema);
|
||||
const jsonSchema = zodToJsonSchema(zodSchema);
|
||||
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: jsonSchema
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Zod to JSON Schema converter for tool definitions
|
||||
*/
|
||||
function zodToJsonSchema(schema: z.ZodObject<z.ZodRawShape>): {
|
||||
type: 'object';
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
} {
|
||||
const shape = schema.shape;
|
||||
const properties: Record<string, unknown> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodType = value as z.ZodTypeAny;
|
||||
properties[key] = zodTypeToJsonSchema(zodType);
|
||||
|
||||
// Check if the field is required (not optional)
|
||||
if (!zodType.isOptional()) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties,
|
||||
required
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert individual Zod types to JSON Schema
|
||||
*/
|
||||
function zodTypeToJsonSchema(zodType: z.ZodTypeAny): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
// Handle optional wrapper
|
||||
if (zodType instanceof z.ZodOptional) {
|
||||
return zodTypeToJsonSchema(zodType._def.innerType);
|
||||
}
|
||||
|
||||
// Handle default wrapper
|
||||
if (zodType instanceof z.ZodDefault) {
|
||||
const inner = zodTypeToJsonSchema(zodType._def.innerType);
|
||||
inner.default = zodType._def.defaultValue();
|
||||
return inner;
|
||||
}
|
||||
|
||||
// Get description if available
|
||||
const description = zodType._def.description;
|
||||
if (description) {
|
||||
result.description = description;
|
||||
}
|
||||
|
||||
// Handle basic types
|
||||
if (zodType instanceof z.ZodString) {
|
||||
result.type = 'string';
|
||||
} else if (zodType instanceof z.ZodNumber) {
|
||||
result.type = zodType._def.checks?.some((c: { kind: string }) => c.kind === 'int')
|
||||
? 'integer'
|
||||
: 'number';
|
||||
} else if (zodType instanceof z.ZodBoolean) {
|
||||
result.type = 'boolean';
|
||||
} else if (zodType instanceof z.ZodArray) {
|
||||
result.type = 'array';
|
||||
result.items = zodTypeToJsonSchema(zodType._def.type);
|
||||
} else if (zodType instanceof z.ZodEnum) {
|
||||
result.type = 'string';
|
||||
result.enum = zodType._def.values;
|
||||
} else if (zodType instanceof z.ZodObject) {
|
||||
return zodToJsonSchema(zodType);
|
||||
} else {
|
||||
// Fallback for unknown types
|
||||
result.type = 'string';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
453
src/tools/lsp-tools.ts
Normal file
453
src/tools/lsp-tools.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* LSP (Language Server Protocol) Tools
|
||||
*
|
||||
* Provides IDE-like capabilities to agents via real LSP server integration:
|
||||
* - Hover information
|
||||
* - Go to definition
|
||||
* - Find references
|
||||
* - Document/workspace symbols
|
||||
* - Diagnostics
|
||||
* - Rename
|
||||
* - Code actions
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
lspClientManager,
|
||||
getAllServers,
|
||||
getServerForFile,
|
||||
formatHover,
|
||||
formatLocations,
|
||||
formatDocumentSymbols,
|
||||
formatWorkspaceSymbols,
|
||||
formatDiagnostics,
|
||||
formatCodeActions,
|
||||
formatWorkspaceEdit,
|
||||
countEdits
|
||||
} from './lsp/index.js';
|
||||
|
||||
export interface ToolDefinition<T extends z.ZodRawShape> {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: T;
|
||||
handler: (args: z.infer<z.ZodObject<T>>) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to handle LSP errors gracefully
|
||||
*/
|
||||
async function withLspClient<T>(
|
||||
filePath: string,
|
||||
operation: string,
|
||||
fn: (client: Awaited<ReturnType<typeof lspClientManager.getClientForFile>>) => Promise<T>
|
||||
): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
|
||||
try {
|
||||
const client = await lspClientManager.getClientForFile(filePath);
|
||||
|
||||
if (!client) {
|
||||
const serverConfig = getServerForFile(filePath);
|
||||
if (!serverConfig) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No language server available for file type: ${filePath}\n\nUse lsp_servers tool to see available language servers.`
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Language server '${serverConfig.name}' not installed.\n\nInstall with: ${serverConfig.installHint}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const result = await fn(client);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: String(result)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Error in ${operation}: ${error instanceof Error ? error.message : String(error)}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LSP Hover Tool - Get type information and documentation at a position
|
||||
*/
|
||||
export const lspHoverTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
line: z.ZodNumber;
|
||||
character: z.ZodNumber;
|
||||
}> = {
|
||||
name: 'lsp_hover',
|
||||
description: 'Get type information, documentation, and signature at a specific position in a file. Useful for understanding what a symbol represents.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().min(1).describe('Line number (1-indexed)'),
|
||||
character: z.number().int().min(0).describe('Character position in the line (0-indexed)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, line, character } = args;
|
||||
return withLspClient(file, 'hover', async (client) => {
|
||||
const hover = await client!.hover(file, line - 1, character);
|
||||
return formatHover(hover);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Go to Definition Tool - Jump to where a symbol is defined
|
||||
*/
|
||||
export const lspGotoDefinitionTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
line: z.ZodNumber;
|
||||
character: z.ZodNumber;
|
||||
}> = {
|
||||
name: 'lsp_goto_definition',
|
||||
description: 'Find the definition location of a symbol (function, variable, class, etc.). Returns the file path and position where the symbol is defined.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().min(1).describe('Line number (1-indexed)'),
|
||||
character: z.number().int().min(0).describe('Character position in the line (0-indexed)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, line, character } = args;
|
||||
return withLspClient(file, 'goto definition', async (client) => {
|
||||
const locations = await client!.definition(file, line - 1, character);
|
||||
return formatLocations(locations);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Find References Tool - Find all usages of a symbol
|
||||
*/
|
||||
export const lspFindReferencesTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
line: z.ZodNumber;
|
||||
character: z.ZodNumber;
|
||||
includeDeclaration: z.ZodOptional<z.ZodBoolean>;
|
||||
}> = {
|
||||
name: 'lsp_find_references',
|
||||
description: 'Find all references to a symbol across the codebase. Useful for understanding usage patterns and impact of changes.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().min(1).describe('Line number (1-indexed)'),
|
||||
character: z.number().int().min(0).describe('Character position in the line (0-indexed)'),
|
||||
includeDeclaration: z.boolean().optional().describe('Include the declaration in results (default: true)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, line, character, includeDeclaration = true } = args;
|
||||
return withLspClient(file, 'find references', async (client) => {
|
||||
const locations = await client!.references(file, line - 1, character, includeDeclaration);
|
||||
if (!locations || locations.length === 0) {
|
||||
return 'No references found';
|
||||
}
|
||||
return `Found ${locations.length} reference(s):\n\n${formatLocations(locations)}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Document Symbols Tool - Get outline of all symbols in a file
|
||||
*/
|
||||
export const lspDocumentSymbolsTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
}> = {
|
||||
name: 'lsp_document_symbols',
|
||||
description: 'Get a hierarchical outline of all symbols in a file (functions, classes, variables, etc.). Useful for understanding file structure.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file } = args;
|
||||
return withLspClient(file, 'document symbols', async (client) => {
|
||||
const symbols = await client!.documentSymbols(file);
|
||||
return formatDocumentSymbols(symbols);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Workspace Symbols Tool - Search symbols across workspace
|
||||
*/
|
||||
export const lspWorkspaceSymbolsTool: ToolDefinition<{
|
||||
query: z.ZodString;
|
||||
file: z.ZodString;
|
||||
}> = {
|
||||
name: 'lsp_workspace_symbols',
|
||||
description: 'Search for symbols (functions, classes, etc.) across the entire workspace by name. Useful for finding definitions without knowing the exact file.',
|
||||
schema: {
|
||||
query: z.string().describe('Symbol name or pattern to search'),
|
||||
file: z.string().describe('Any file in the workspace (used to determine which language server to use)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { query, file } = args;
|
||||
return withLspClient(file, 'workspace symbols', async (client) => {
|
||||
const symbols = await client!.workspaceSymbols(query);
|
||||
if (!symbols || symbols.length === 0) {
|
||||
return `No symbols found matching: ${query}`;
|
||||
}
|
||||
return `Found ${symbols.length} symbol(s) matching "${query}":\n\n${formatWorkspaceSymbols(symbols)}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Diagnostics Tool - Get errors, warnings, and hints
|
||||
*/
|
||||
export const lspDiagnosticsTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
severity: z.ZodOptional<z.ZodEnum<['error', 'warning', 'info', 'hint']>>;
|
||||
}> = {
|
||||
name: 'lsp_diagnostics',
|
||||
description: 'Get language server diagnostics (errors, warnings, hints) for a file. Useful for finding issues without running the compiler.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
severity: z.enum(['error', 'warning', 'info', 'hint']).optional().describe('Filter by severity level')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, severity } = args;
|
||||
return withLspClient(file, 'diagnostics', async (client) => {
|
||||
// Open the document to trigger diagnostics
|
||||
await client!.openDocument(file);
|
||||
// Wait a bit for diagnostics to be published
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
let diagnostics = client!.getDiagnostics(file);
|
||||
|
||||
if (severity) {
|
||||
const severityMap: Record<string, number> = {
|
||||
'error': 1,
|
||||
'warning': 2,
|
||||
'info': 3,
|
||||
'hint': 4
|
||||
};
|
||||
const severityNum = severityMap[severity];
|
||||
diagnostics = diagnostics.filter(d => d.severity === severityNum);
|
||||
}
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
return severity
|
||||
? `No ${severity} diagnostics in ${file}`
|
||||
: `No diagnostics in ${file}`;
|
||||
}
|
||||
|
||||
return `Found ${diagnostics.length} diagnostic(s):\n\n${formatDiagnostics(diagnostics, file)}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Servers Tool - List available language servers
|
||||
*/
|
||||
export const lspServersTool: ToolDefinition<Record<string, never>> = {
|
||||
name: 'lsp_servers',
|
||||
description: 'List all known language servers and their installation status. Shows which servers are available and how to install missing ones.',
|
||||
schema: {},
|
||||
handler: async () => {
|
||||
const servers = getAllServers();
|
||||
|
||||
const installed = servers.filter(s => s.installed);
|
||||
const notInstalled = servers.filter(s => !s.installed);
|
||||
|
||||
let text = '## Language Server Status\n\n';
|
||||
|
||||
if (installed.length > 0) {
|
||||
text += '### Installed:\n';
|
||||
for (const server of installed) {
|
||||
text += `- ${server.name} (${server.command})\n`;
|
||||
text += ` Extensions: ${server.extensions.join(', ')}\n`;
|
||||
}
|
||||
text += '\n';
|
||||
}
|
||||
|
||||
if (notInstalled.length > 0) {
|
||||
text += '### Not Installed:\n';
|
||||
for (const server of notInstalled) {
|
||||
text += `- ${server.name} (${server.command})\n`;
|
||||
text += ` Extensions: ${server.extensions.join(', ')}\n`;
|
||||
text += ` Install: ${server.installHint}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Prepare Rename Tool - Check if rename is valid
|
||||
*/
|
||||
export const lspPrepareRenameTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
line: z.ZodNumber;
|
||||
character: z.ZodNumber;
|
||||
}> = {
|
||||
name: 'lsp_prepare_rename',
|
||||
description: 'Check if a symbol at the given position can be renamed. Returns the range of the symbol if rename is possible.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().min(1).describe('Line number (1-indexed)'),
|
||||
character: z.number().int().min(0).describe('Character position in the line (0-indexed)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, line, character } = args;
|
||||
return withLspClient(file, 'prepare rename', async (client) => {
|
||||
const range = await client!.prepareRename(file, line - 1, character);
|
||||
if (!range) {
|
||||
return 'Cannot rename symbol at this position';
|
||||
}
|
||||
return `Rename possible. Symbol range: line ${range.start.line + 1}, col ${range.start.character + 1} to line ${range.end.line + 1}, col ${range.end.character + 1}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Rename Tool - Rename a symbol across all files
|
||||
*/
|
||||
export const lspRenameTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
line: z.ZodNumber;
|
||||
character: z.ZodNumber;
|
||||
newName: z.ZodString;
|
||||
}> = {
|
||||
name: 'lsp_rename',
|
||||
description: 'Rename a symbol (variable, function, class, etc.) across all files in the project. Returns the list of edits that would be made. Does NOT apply the changes automatically.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().min(1).describe('Line number (1-indexed)'),
|
||||
character: z.number().int().min(0).describe('Character position in the line (0-indexed)'),
|
||||
newName: z.string().min(1).describe('New name for the symbol')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, line, character, newName } = args;
|
||||
return withLspClient(file, 'rename', async (client) => {
|
||||
const edit = await client!.rename(file, line - 1, character, newName);
|
||||
if (!edit) {
|
||||
return 'Rename failed or no edits returned';
|
||||
}
|
||||
|
||||
const { files, edits } = countEdits(edit);
|
||||
return `Rename to "${newName}" would affect ${files} file(s) with ${edits} edit(s):\n\n${formatWorkspaceEdit(edit)}\n\nNote: Use the Edit tool to apply these changes.`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Code Actions Tool - Get available refactoring and quick-fix actions
|
||||
*/
|
||||
export const lspCodeActionsTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
startLine: z.ZodNumber;
|
||||
startCharacter: z.ZodNumber;
|
||||
endLine: z.ZodNumber;
|
||||
endCharacter: z.ZodNumber;
|
||||
}> = {
|
||||
name: 'lsp_code_actions',
|
||||
description: 'Get available code actions (refactorings, quick fixes) for a selection. Returns a list of possible actions that can be applied.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
startLine: z.number().int().min(1).describe('Start line of selection (1-indexed)'),
|
||||
startCharacter: z.number().int().min(0).describe('Start character of selection (0-indexed)'),
|
||||
endLine: z.number().int().min(1).describe('End line of selection (1-indexed)'),
|
||||
endCharacter: z.number().int().min(0).describe('End character of selection (0-indexed)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, startLine, startCharacter, endLine, endCharacter } = args;
|
||||
return withLspClient(file, 'code actions', async (client) => {
|
||||
const range = {
|
||||
start: { line: startLine - 1, character: startCharacter },
|
||||
end: { line: endLine - 1, character: endCharacter }
|
||||
};
|
||||
const actions = await client!.codeActions(file, range);
|
||||
return formatCodeActions(actions);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Code Action Resolve Tool - Get details of a code action
|
||||
*/
|
||||
export const lspCodeActionResolveTool: ToolDefinition<{
|
||||
file: z.ZodString;
|
||||
startLine: z.ZodNumber;
|
||||
startCharacter: z.ZodNumber;
|
||||
endLine: z.ZodNumber;
|
||||
endCharacter: z.ZodNumber;
|
||||
actionIndex: z.ZodNumber;
|
||||
}> = {
|
||||
name: 'lsp_code_action_resolve',
|
||||
description: 'Get the full edit details for a specific code action. Use after lsp_code_actions to see what changes an action would make.',
|
||||
schema: {
|
||||
file: z.string().describe('Path to the source file'),
|
||||
startLine: z.number().int().min(1).describe('Start line of selection (1-indexed)'),
|
||||
startCharacter: z.number().int().min(0).describe('Start character of selection (0-indexed)'),
|
||||
endLine: z.number().int().min(1).describe('End line of selection (1-indexed)'),
|
||||
endCharacter: z.number().int().min(0).describe('End character of selection (0-indexed)'),
|
||||
actionIndex: z.number().int().min(1).describe('Index of the action (1-indexed, from lsp_code_actions output)')
|
||||
},
|
||||
handler: async (args) => {
|
||||
const { file, startLine, startCharacter, endLine, endCharacter, actionIndex } = args;
|
||||
return withLspClient(file, 'code action resolve', async (client) => {
|
||||
const range = {
|
||||
start: { line: startLine - 1, character: startCharacter },
|
||||
end: { line: endLine - 1, character: endCharacter }
|
||||
};
|
||||
const actions = await client!.codeActions(file, range);
|
||||
|
||||
if (!actions || actions.length === 0) {
|
||||
return 'No code actions available';
|
||||
}
|
||||
|
||||
if (actionIndex < 1 || actionIndex > actions.length) {
|
||||
return `Invalid action index. Available actions: 1-${actions.length}`;
|
||||
}
|
||||
|
||||
const action = actions[actionIndex - 1];
|
||||
|
||||
let result = `Action: ${action.title}\n`;
|
||||
if (action.kind) result += `Kind: ${action.kind}\n`;
|
||||
if (action.isPreferred) result += `(Preferred)\n`;
|
||||
|
||||
if (action.edit) {
|
||||
result += `\nEdits:\n${formatWorkspaceEdit(action.edit)}`;
|
||||
}
|
||||
|
||||
if (action.command) {
|
||||
result += `\nCommand: ${action.command.title} (${action.command.command})`;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all LSP tool definitions
|
||||
*/
|
||||
export const lspTools = [
|
||||
lspHoverTool,
|
||||
lspGotoDefinitionTool,
|
||||
lspFindReferencesTool,
|
||||
lspDocumentSymbolsTool,
|
||||
lspWorkspaceSymbolsTool,
|
||||
lspDiagnosticsTool,
|
||||
lspServersTool,
|
||||
lspPrepareRenameTool,
|
||||
lspRenameTool,
|
||||
lspCodeActionsTool,
|
||||
lspCodeActionResolveTool
|
||||
];
|
||||
606
src/tools/lsp/client.ts
Normal file
606
src/tools/lsp/client.ts
Normal file
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* LSP Client Implementation
|
||||
*
|
||||
* Manages connections to language servers using JSON-RPC 2.0 over stdio.
|
||||
* Handles server lifecycle, message buffering, and request/response matching.
|
||||
*/
|
||||
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import type { LspServerConfig } from './servers.js';
|
||||
import { getServerForFile, commandExists } from './servers.js';
|
||||
|
||||
// LSP Protocol Types
|
||||
export interface Position {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
uri: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface TextDocumentIdentifier {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface TextDocumentPositionParams {
|
||||
textDocument: TextDocumentIdentifier;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
export interface Hover {
|
||||
contents: string | { kind: string; value: string } | Array<string | { kind: string; value: string }>;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface Diagnostic {
|
||||
range: Range;
|
||||
severity?: number;
|
||||
code?: string | number;
|
||||
source?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface DocumentSymbol {
|
||||
name: string;
|
||||
kind: number;
|
||||
range: Range;
|
||||
selectionRange: Range;
|
||||
children?: DocumentSymbol[];
|
||||
}
|
||||
|
||||
export interface SymbolInformation {
|
||||
name: string;
|
||||
kind: number;
|
||||
location: Location;
|
||||
containerName?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceEdit {
|
||||
changes?: Record<string, Array<{ range: Range; newText: string }>>;
|
||||
documentChanges?: Array<{ textDocument: TextDocumentIdentifier; edits: Array<{ range: Range; newText: string }> }>;
|
||||
}
|
||||
|
||||
export interface CodeAction {
|
||||
title: string;
|
||||
kind?: string;
|
||||
diagnostics?: Diagnostic[];
|
||||
isPreferred?: boolean;
|
||||
edit?: WorkspaceEdit;
|
||||
command?: { title: string; command: string; arguments?: unknown[] };
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-RPC Request/Response types
|
||||
*/
|
||||
interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string; data?: unknown };
|
||||
}
|
||||
|
||||
interface JsonRpcNotification {
|
||||
jsonrpc: '2.0';
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* LSP Client class
|
||||
*/
|
||||
export class LspClient {
|
||||
private process: ChildProcess | null = null;
|
||||
private requestId = 0;
|
||||
private pendingRequests = new Map<number, {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout: NodeJS.Timeout;
|
||||
}>();
|
||||
private buffer = '';
|
||||
private openDocuments = new Set<string>();
|
||||
private diagnostics = new Map<string, Diagnostic[]>();
|
||||
private workspaceRoot: string;
|
||||
private serverConfig: LspServerConfig;
|
||||
private initialized = false;
|
||||
|
||||
constructor(workspaceRoot: string, serverConfig: LspServerConfig) {
|
||||
this.workspaceRoot = resolve(workspaceRoot);
|
||||
this.serverConfig = serverConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the LSP server and initialize the connection
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.process) {
|
||||
return; // Already connected
|
||||
}
|
||||
|
||||
if (!commandExists(this.serverConfig.command)) {
|
||||
throw new Error(
|
||||
`Language server '${this.serverConfig.command}' not found.\n` +
|
||||
`Install with: ${this.serverConfig.installHint}`
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.process = spawn(this.serverConfig.command, this.serverConfig.args, {
|
||||
cwd: this.workspaceRoot,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
this.process.stdout?.on('data', (data: Buffer) => {
|
||||
this.handleData(data.toString());
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data: Buffer) => {
|
||||
// Log stderr for debugging but don't fail
|
||||
console.error(`LSP stderr: ${data.toString()}`);
|
||||
});
|
||||
|
||||
this.process.on('error', (error) => {
|
||||
reject(new Error(`Failed to start LSP server: ${error.message}`));
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
this.process = null;
|
||||
this.initialized = false;
|
||||
if (code !== 0) {
|
||||
console.error(`LSP server exited with code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Send initialize request
|
||||
this.initialize()
|
||||
.then(() => {
|
||||
this.initialized = true;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the LSP server
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.process) return;
|
||||
|
||||
try {
|
||||
await this.request('shutdown', null);
|
||||
this.notify('exit', null);
|
||||
} catch {
|
||||
// Ignore errors during shutdown
|
||||
}
|
||||
|
||||
this.process.kill();
|
||||
this.process = null;
|
||||
this.initialized = false;
|
||||
this.pendingRequests.clear();
|
||||
this.openDocuments.clear();
|
||||
this.diagnostics.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming data from the server
|
||||
*/
|
||||
private handleData(data: string): void {
|
||||
this.buffer += data;
|
||||
|
||||
while (true) {
|
||||
// Look for Content-Length header
|
||||
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) break;
|
||||
|
||||
const header = this.buffer.slice(0, headerEnd);
|
||||
const contentLengthMatch = header.match(/Content-Length: (\d+)/i);
|
||||
if (!contentLengthMatch) {
|
||||
// Invalid header, try to recover
|
||||
this.buffer = this.buffer.slice(headerEnd + 4);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentLength = parseInt(contentLengthMatch[1], 10);
|
||||
const messageStart = headerEnd + 4;
|
||||
const messageEnd = messageStart + contentLength;
|
||||
|
||||
if (this.buffer.length < messageEnd) {
|
||||
break; // Not enough data yet
|
||||
}
|
||||
|
||||
const messageJson = this.buffer.slice(messageStart, messageEnd);
|
||||
this.buffer = this.buffer.slice(messageEnd);
|
||||
|
||||
try {
|
||||
const message = JSON.parse(messageJson);
|
||||
this.handleMessage(message);
|
||||
} catch {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a parsed JSON-RPC message
|
||||
*/
|
||||
private handleMessage(message: JsonRpcResponse | JsonRpcNotification): void {
|
||||
if ('id' in message && message.id !== undefined) {
|
||||
// Response to a request
|
||||
const pending = this.pendingRequests.get(message.id);
|
||||
if (pending) {
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingRequests.delete(message.id);
|
||||
|
||||
if (message.error) {
|
||||
pending.reject(new Error(message.error.message));
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
}
|
||||
} else if ('method' in message) {
|
||||
// Notification from server
|
||||
this.handleNotification(message as JsonRpcNotification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle server notifications
|
||||
*/
|
||||
private handleNotification(notification: JsonRpcNotification): void {
|
||||
if (notification.method === 'textDocument/publishDiagnostics') {
|
||||
const params = notification.params as { uri: string; diagnostics: Diagnostic[] };
|
||||
this.diagnostics.set(params.uri, params.diagnostics);
|
||||
}
|
||||
// Handle other notifications as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the server
|
||||
*/
|
||||
private async request<T>(method: string, params: unknown, timeout = 15000): Promise<T> {
|
||||
if (!this.process?.stdin) {
|
||||
throw new Error('LSP server not connected');
|
||||
}
|
||||
|
||||
const id = ++this.requestId;
|
||||
const request: JsonRpcRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params
|
||||
};
|
||||
|
||||
const content = JSON.stringify(request);
|
||||
const message = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n${content}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`LSP request '${method}' timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
this.pendingRequests.set(id, {
|
||||
resolve: resolve as (value: unknown) => void,
|
||||
reject,
|
||||
timeout: timeoutHandle
|
||||
});
|
||||
|
||||
this.process?.stdin?.write(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification to the server (no response expected)
|
||||
*/
|
||||
private notify(method: string, params: unknown): void {
|
||||
if (!this.process?.stdin) return;
|
||||
|
||||
const notification: JsonRpcNotification = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params
|
||||
};
|
||||
|
||||
const content = JSON.stringify(notification);
|
||||
const message = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n${content}`;
|
||||
this.process.stdin.write(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the LSP connection
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
await this.request('initialize', {
|
||||
processId: process.pid,
|
||||
rootUri: `file://${this.workspaceRoot}`,
|
||||
rootPath: this.workspaceRoot,
|
||||
capabilities: {
|
||||
textDocument: {
|
||||
hover: { contentFormat: ['markdown', 'plaintext'] },
|
||||
definition: { linkSupport: true },
|
||||
references: {},
|
||||
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
||||
codeAction: { codeActionLiteralSupport: { codeActionKind: { valueSet: [] } } },
|
||||
rename: { prepareSupport: true }
|
||||
},
|
||||
workspace: {
|
||||
symbol: {},
|
||||
workspaceFolders: true
|
||||
}
|
||||
},
|
||||
initializationOptions: this.serverConfig.initializationOptions || {}
|
||||
});
|
||||
|
||||
this.notify('initialized', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a document for editing
|
||||
*/
|
||||
async openDocument(filePath: string): Promise<void> {
|
||||
const uri = `file://${resolve(filePath)}`;
|
||||
|
||||
if (this.openDocuments.has(uri)) return;
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const languageId = this.getLanguageId(filePath);
|
||||
|
||||
this.notify('textDocument/didOpen', {
|
||||
textDocument: {
|
||||
uri,
|
||||
languageId,
|
||||
version: 1,
|
||||
text: content
|
||||
}
|
||||
});
|
||||
|
||||
this.openDocuments.add(uri);
|
||||
|
||||
// Wait a bit for the server to process the document
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a document
|
||||
*/
|
||||
closeDocument(filePath: string): void {
|
||||
const uri = `file://${resolve(filePath)}`;
|
||||
|
||||
if (!this.openDocuments.has(uri)) return;
|
||||
|
||||
this.notify('textDocument/didClose', {
|
||||
textDocument: { uri }
|
||||
});
|
||||
|
||||
this.openDocuments.delete(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the language ID for a file
|
||||
*/
|
||||
private getLanguageId(filePath: string): string {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
const langMap: Record<string, string> = {
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescriptreact',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascriptreact',
|
||||
'mts': 'typescript',
|
||||
'cts': 'typescript',
|
||||
'mjs': 'javascript',
|
||||
'cjs': 'javascript',
|
||||
'py': 'python',
|
||||
'rs': 'rust',
|
||||
'go': 'go',
|
||||
'c': 'c',
|
||||
'h': 'c',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'java': 'java',
|
||||
'json': 'json',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml'
|
||||
};
|
||||
return langMap[ext] || ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert file path to URI and ensure document is open
|
||||
*/
|
||||
private async prepareDocument(filePath: string): Promise<string> {
|
||||
await this.openDocument(filePath);
|
||||
return `file://${resolve(filePath)}`;
|
||||
}
|
||||
|
||||
// LSP Request Methods
|
||||
|
||||
/**
|
||||
* Get hover information at a position
|
||||
*/
|
||||
async hover(filePath: string, line: number, character: number): Promise<Hover | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<Hover | null>('textDocument/hover', {
|
||||
textDocument: { uri },
|
||||
position: { line, character }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to definition
|
||||
*/
|
||||
async definition(filePath: string, line: number, character: number): Promise<Location | Location[] | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<Location | Location[] | null>('textDocument/definition', {
|
||||
textDocument: { uri },
|
||||
position: { line, character }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all references
|
||||
*/
|
||||
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<Location[] | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<Location[] | null>('textDocument/references', {
|
||||
textDocument: { uri },
|
||||
position: { line, character },
|
||||
context: { includeDeclaration }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document symbols
|
||||
*/
|
||||
async documentSymbols(filePath: string): Promise<DocumentSymbol[] | SymbolInformation[] | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<DocumentSymbol[] | SymbolInformation[] | null>('textDocument/documentSymbol', {
|
||||
textDocument: { uri }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search workspace symbols
|
||||
*/
|
||||
async workspaceSymbols(query: string): Promise<SymbolInformation[] | null> {
|
||||
return this.request<SymbolInformation[] | null>('workspace/symbol', { query });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostics for a file
|
||||
*/
|
||||
getDiagnostics(filePath: string): Diagnostic[] {
|
||||
const uri = `file://${resolve(filePath)}`;
|
||||
return this.diagnostics.get(uri) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare rename (check if rename is valid)
|
||||
*/
|
||||
async prepareRename(filePath: string, line: number, character: number): Promise<Range | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
try {
|
||||
const result = await this.request<Range | { range: Range; placeholder: string } | null>('textDocument/prepareRename', {
|
||||
textDocument: { uri },
|
||||
position: { line, character }
|
||||
});
|
||||
if (!result) return null;
|
||||
return 'range' in result ? result.range : result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a symbol
|
||||
*/
|
||||
async rename(filePath: string, line: number, character: number, newName: string): Promise<WorkspaceEdit | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<WorkspaceEdit | null>('textDocument/rename', {
|
||||
textDocument: { uri },
|
||||
position: { line, character },
|
||||
newName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code actions
|
||||
*/
|
||||
async codeActions(filePath: string, range: Range, diagnostics: Diagnostic[] = []): Promise<CodeAction[] | null> {
|
||||
const uri = await this.prepareDocument(filePath);
|
||||
return this.request<CodeAction[] | null>('textDocument/codeAction', {
|
||||
textDocument: { uri },
|
||||
range,
|
||||
context: { diagnostics }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client manager - maintains a pool of LSP clients per workspace/server
|
||||
*/
|
||||
class LspClientManager {
|
||||
private clients = new Map<string, LspClient>();
|
||||
|
||||
/**
|
||||
* Get or create a client for a file
|
||||
*/
|
||||
async getClientForFile(filePath: string): Promise<LspClient | null> {
|
||||
const serverConfig = getServerForFile(filePath);
|
||||
if (!serverConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find workspace root
|
||||
const workspaceRoot = this.findWorkspaceRoot(filePath);
|
||||
const key = `${workspaceRoot}:${serverConfig.command}`;
|
||||
|
||||
let client = this.clients.get(key);
|
||||
if (!client) {
|
||||
client = new LspClient(workspaceRoot, serverConfig);
|
||||
try {
|
||||
await client.connect();
|
||||
this.clients.set(key, client);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the workspace root for a file
|
||||
*/
|
||||
private findWorkspaceRoot(filePath: string): string {
|
||||
let dir = dirname(resolve(filePath));
|
||||
const markers = ['package.json', 'tsconfig.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', '.git'];
|
||||
|
||||
while (dir !== '/') {
|
||||
for (const marker of markers) {
|
||||
if (existsSync(`${dir}/${marker}`)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
|
||||
return dirname(resolve(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect all clients
|
||||
*/
|
||||
async disconnectAll(): Promise<void> {
|
||||
for (const client of this.clients.values()) {
|
||||
await client.disconnect();
|
||||
}
|
||||
this.clients.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const lspClientManager = new LspClientManager();
|
||||
40
src/tools/lsp/index.ts
Normal file
40
src/tools/lsp/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* LSP Module Exports
|
||||
*/
|
||||
|
||||
export { LspClient, lspClientManager } from './client.js';
|
||||
export type {
|
||||
Position,
|
||||
Range,
|
||||
Location,
|
||||
Hover,
|
||||
Diagnostic,
|
||||
DocumentSymbol,
|
||||
SymbolInformation,
|
||||
WorkspaceEdit,
|
||||
CodeAction
|
||||
} from './client.js';
|
||||
|
||||
export {
|
||||
LSP_SERVERS,
|
||||
getServerForFile,
|
||||
getServerForLanguage,
|
||||
getAllServers,
|
||||
commandExists
|
||||
} from './servers.js';
|
||||
export type { LspServerConfig } from './servers.js';
|
||||
|
||||
export {
|
||||
uriToPath,
|
||||
formatPosition,
|
||||
formatRange,
|
||||
formatLocation,
|
||||
formatHover,
|
||||
formatLocations,
|
||||
formatDocumentSymbols,
|
||||
formatWorkspaceSymbols,
|
||||
formatDiagnostics,
|
||||
formatCodeActions,
|
||||
formatWorkspaceEdit,
|
||||
countEdits
|
||||
} from './utils.js';
|
||||
165
src/tools/lsp/servers.ts
Normal file
165
src/tools/lsp/servers.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* LSP Server Configurations
|
||||
*
|
||||
* Defines known language servers and their configurations.
|
||||
* Supports auto-detection and installation hints.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { extname } from 'path';
|
||||
|
||||
export interface LspServerConfig {
|
||||
name: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
extensions: string[];
|
||||
installHint: string;
|
||||
initializationOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Known LSP servers and their configurations
|
||||
*/
|
||||
export const LSP_SERVERS: Record<string, LspServerConfig> = {
|
||||
typescript: {
|
||||
name: 'TypeScript Language Server',
|
||||
command: 'typescript-language-server',
|
||||
args: ['--stdio'],
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'],
|
||||
installHint: 'npm install -g typescript-language-server typescript'
|
||||
},
|
||||
python: {
|
||||
name: 'Python Language Server (pylsp)',
|
||||
command: 'pylsp',
|
||||
args: [],
|
||||
extensions: ['.py', '.pyw'],
|
||||
installHint: 'pip install python-lsp-server'
|
||||
},
|
||||
rust: {
|
||||
name: 'Rust Analyzer',
|
||||
command: 'rust-analyzer',
|
||||
args: [],
|
||||
extensions: ['.rs'],
|
||||
installHint: 'rustup component add rust-analyzer'
|
||||
},
|
||||
go: {
|
||||
name: 'gopls',
|
||||
command: 'gopls',
|
||||
args: ['serve'],
|
||||
extensions: ['.go'],
|
||||
installHint: 'go install golang.org/x/tools/gopls@latest'
|
||||
},
|
||||
c: {
|
||||
name: 'clangd',
|
||||
command: 'clangd',
|
||||
args: [],
|
||||
extensions: ['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx'],
|
||||
installHint: 'Install clangd from your package manager or LLVM'
|
||||
},
|
||||
java: {
|
||||
name: 'Eclipse JDT Language Server',
|
||||
command: 'jdtls',
|
||||
args: [],
|
||||
extensions: ['.java'],
|
||||
installHint: 'Install from https://github.com/eclipse/eclipse.jdt.ls'
|
||||
},
|
||||
json: {
|
||||
name: 'JSON Language Server',
|
||||
command: 'vscode-json-language-server',
|
||||
args: ['--stdio'],
|
||||
extensions: ['.json', '.jsonc'],
|
||||
installHint: 'npm install -g vscode-langservers-extracted'
|
||||
},
|
||||
html: {
|
||||
name: 'HTML Language Server',
|
||||
command: 'vscode-html-language-server',
|
||||
args: ['--stdio'],
|
||||
extensions: ['.html', '.htm'],
|
||||
installHint: 'npm install -g vscode-langservers-extracted'
|
||||
},
|
||||
css: {
|
||||
name: 'CSS Language Server',
|
||||
command: 'vscode-css-language-server',
|
||||
args: ['--stdio'],
|
||||
extensions: ['.css', '.scss', '.less'],
|
||||
installHint: 'npm install -g vscode-langservers-extracted'
|
||||
},
|
||||
yaml: {
|
||||
name: 'YAML Language Server',
|
||||
command: 'yaml-language-server',
|
||||
args: ['--stdio'],
|
||||
extensions: ['.yaml', '.yml'],
|
||||
installHint: 'npm install -g yaml-language-server'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a command exists in PATH
|
||||
*/
|
||||
export function commandExists(command: string): boolean {
|
||||
try {
|
||||
execSync(`which ${command}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LSP server config for a file based on its extension
|
||||
*/
|
||||
export function getServerForFile(filePath: string): LspServerConfig | null {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
|
||||
for (const [_, config] of Object.entries(LSP_SERVERS)) {
|
||||
if (config.extensions.includes(ext)) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available servers (installed and not installed)
|
||||
*/
|
||||
export function getAllServers(): Array<LspServerConfig & { installed: boolean }> {
|
||||
return Object.values(LSP_SERVERS).map(config => ({
|
||||
...config,
|
||||
installed: commandExists(config.command)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate server for a language
|
||||
*/
|
||||
export function getServerForLanguage(language: string): LspServerConfig | null {
|
||||
// Map common language names to server keys
|
||||
const langMap: Record<string, string> = {
|
||||
'javascript': 'typescript',
|
||||
'typescript': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'jsx': 'typescript',
|
||||
'python': 'python',
|
||||
'rust': 'rust',
|
||||
'go': 'go',
|
||||
'golang': 'go',
|
||||
'c': 'c',
|
||||
'cpp': 'c',
|
||||
'c++': 'c',
|
||||
'java': 'java',
|
||||
'json': 'json',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'css',
|
||||
'less': 'css',
|
||||
'yaml': 'yaml'
|
||||
};
|
||||
|
||||
const serverKey = langMap[language.toLowerCase()];
|
||||
if (serverKey && LSP_SERVERS[serverKey]) {
|
||||
return LSP_SERVERS[serverKey];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
265
src/tools/lsp/utils.ts
Normal file
265
src/tools/lsp/utils.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* LSP Utilities
|
||||
*
|
||||
* Helper functions for formatting LSP results and converting between formats.
|
||||
*/
|
||||
|
||||
import type { Hover, Location, DocumentSymbol, SymbolInformation, Diagnostic, CodeAction, WorkspaceEdit, Range } from './client.js';
|
||||
|
||||
/**
|
||||
* Symbol kind names (LSP spec)
|
||||
*/
|
||||
const SYMBOL_KINDS: Record<number, string> = {
|
||||
1: 'File',
|
||||
2: 'Module',
|
||||
3: 'Namespace',
|
||||
4: 'Package',
|
||||
5: 'Class',
|
||||
6: 'Method',
|
||||
7: 'Property',
|
||||
8: 'Field',
|
||||
9: 'Constructor',
|
||||
10: 'Enum',
|
||||
11: 'Interface',
|
||||
12: 'Function',
|
||||
13: 'Variable',
|
||||
14: 'Constant',
|
||||
15: 'String',
|
||||
16: 'Number',
|
||||
17: 'Boolean',
|
||||
18: 'Array',
|
||||
19: 'Object',
|
||||
20: 'Key',
|
||||
21: 'Null',
|
||||
22: 'EnumMember',
|
||||
23: 'Struct',
|
||||
24: 'Event',
|
||||
25: 'Operator',
|
||||
26: 'TypeParameter'
|
||||
};
|
||||
|
||||
/**
|
||||
* Diagnostic severity names
|
||||
*/
|
||||
const SEVERITY_NAMES: Record<number, string> = {
|
||||
1: 'Error',
|
||||
2: 'Warning',
|
||||
3: 'Information',
|
||||
4: 'Hint'
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert URI to file path
|
||||
*/
|
||||
export function uriToPath(uri: string): string {
|
||||
if (uri.startsWith('file://')) {
|
||||
return decodeURIComponent(uri.slice(7));
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a position for display
|
||||
*/
|
||||
export function formatPosition(line: number, character: number): string {
|
||||
return `${line + 1}:${character + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a range for display
|
||||
*/
|
||||
export function formatRange(range: Range): string {
|
||||
const start = formatPosition(range.start.line, range.start.character);
|
||||
const end = formatPosition(range.end.line, range.end.character);
|
||||
return start === end ? start : `${start}-${end}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a location for display
|
||||
*/
|
||||
export function formatLocation(location: Location): string {
|
||||
const path = uriToPath(location.uri);
|
||||
const range = formatRange(location.range);
|
||||
return `${path}:${range}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format hover content
|
||||
*/
|
||||
export function formatHover(hover: Hover | null): string {
|
||||
if (!hover) return 'No hover information available';
|
||||
|
||||
let text = '';
|
||||
|
||||
if (typeof hover.contents === 'string') {
|
||||
text = hover.contents;
|
||||
} else if (Array.isArray(hover.contents)) {
|
||||
text = hover.contents.map(c => {
|
||||
if (typeof c === 'string') return c;
|
||||
return c.value;
|
||||
}).join('\n\n');
|
||||
} else if ('value' in hover.contents) {
|
||||
text = hover.contents.value;
|
||||
}
|
||||
|
||||
if (hover.range) {
|
||||
text += `\n\nRange: ${formatRange(hover.range)}`;
|
||||
}
|
||||
|
||||
return text || 'No hover information available';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format locations array
|
||||
*/
|
||||
export function formatLocations(locations: Location | Location[] | null): string {
|
||||
if (!locations) return 'No locations found';
|
||||
|
||||
const locs = Array.isArray(locations) ? locations : [locations];
|
||||
|
||||
if (locs.length === 0) return 'No locations found';
|
||||
|
||||
return locs.map(loc => formatLocation(loc)).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format document symbols (hierarchical)
|
||||
*/
|
||||
export function formatDocumentSymbols(symbols: DocumentSymbol[] | SymbolInformation[] | null, indent = 0): string {
|
||||
if (!symbols || symbols.length === 0) return 'No symbols found';
|
||||
|
||||
const lines: string[] = [];
|
||||
const prefix = ' '.repeat(indent);
|
||||
|
||||
for (const symbol of symbols) {
|
||||
const kind = SYMBOL_KINDS[symbol.kind] || 'Unknown';
|
||||
|
||||
if ('range' in symbol) {
|
||||
// DocumentSymbol
|
||||
const range = formatRange(symbol.range);
|
||||
lines.push(`${prefix}${kind}: ${symbol.name} [${range}]`);
|
||||
|
||||
if (symbol.children && symbol.children.length > 0) {
|
||||
lines.push(formatDocumentSymbols(symbol.children, indent + 1));
|
||||
}
|
||||
} else {
|
||||
// SymbolInformation
|
||||
const loc = formatLocation(symbol.location);
|
||||
const container = symbol.containerName ? ` (in ${symbol.containerName})` : '';
|
||||
lines.push(`${prefix}${kind}: ${symbol.name}${container} [${loc}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format workspace symbols
|
||||
*/
|
||||
export function formatWorkspaceSymbols(symbols: SymbolInformation[] | null): string {
|
||||
if (!symbols || symbols.length === 0) return 'No symbols found';
|
||||
|
||||
const lines = symbols.map(symbol => {
|
||||
const kind = SYMBOL_KINDS[symbol.kind] || 'Unknown';
|
||||
const loc = formatLocation(symbol.location);
|
||||
const container = symbol.containerName ? ` (in ${symbol.containerName})` : '';
|
||||
return `${kind}: ${symbol.name}${container}\n ${loc}`;
|
||||
});
|
||||
|
||||
return lines.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format diagnostics
|
||||
*/
|
||||
export function formatDiagnostics(diagnostics: Diagnostic[], filePath?: string): string {
|
||||
if (diagnostics.length === 0) return 'No diagnostics';
|
||||
|
||||
const lines = diagnostics.map(diag => {
|
||||
const severity = SEVERITY_NAMES[diag.severity || 1] || 'Unknown';
|
||||
const range = formatRange(diag.range);
|
||||
const source = diag.source ? `[${diag.source}]` : '';
|
||||
const code = diag.code ? ` (${diag.code})` : '';
|
||||
const location = filePath ? `${filePath}:${range}` : range;
|
||||
|
||||
return `${severity}${code}${source}: ${diag.message}\n at ${location}`;
|
||||
});
|
||||
|
||||
return lines.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format code actions
|
||||
*/
|
||||
export function formatCodeActions(actions: CodeAction[] | null): string {
|
||||
if (!actions || actions.length === 0) return 'No code actions available';
|
||||
|
||||
const lines = actions.map((action, index) => {
|
||||
const preferred = action.isPreferred ? ' (preferred)' : '';
|
||||
const kind = action.kind ? ` [${action.kind}]` : '';
|
||||
return `${index + 1}. ${action.title}${kind}${preferred}`;
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format workspace edit
|
||||
*/
|
||||
export function formatWorkspaceEdit(edit: WorkspaceEdit | null): string {
|
||||
if (!edit) return 'No edits';
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
if (edit.changes) {
|
||||
for (const [uri, changes] of Object.entries(edit.changes)) {
|
||||
const path = uriToPath(uri);
|
||||
lines.push(`File: ${path}`);
|
||||
for (const change of changes) {
|
||||
const range = formatRange(change.range);
|
||||
const preview = change.newText.length > 50
|
||||
? change.newText.slice(0, 50) + '...'
|
||||
: change.newText;
|
||||
lines.push(` ${range}: "${preview}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (edit.documentChanges) {
|
||||
for (const docChange of edit.documentChanges) {
|
||||
const path = uriToPath(docChange.textDocument.uri);
|
||||
lines.push(`File: ${path}`);
|
||||
for (const change of docChange.edits) {
|
||||
const range = formatRange(change.range);
|
||||
const preview = change.newText.length > 50
|
||||
? change.newText.slice(0, 50) + '...'
|
||||
: change.newText;
|
||||
lines.push(` ${range}: "${preview}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.length > 0 ? lines.join('\n') : 'No edits';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count edits in a workspace edit
|
||||
*/
|
||||
export function countEdits(edit: WorkspaceEdit | null): { files: number; edits: number } {
|
||||
if (!edit) return { files: 0, edits: 0 };
|
||||
|
||||
let files = 0;
|
||||
let edits = 0;
|
||||
|
||||
if (edit.changes) {
|
||||
files += Object.keys(edit.changes).length;
|
||||
edits += Object.values(edit.changes).reduce((sum, changes) => sum + changes.length, 0);
|
||||
}
|
||||
|
||||
if (edit.documentChanges) {
|
||||
files += edit.documentChanges.length;
|
||||
edits += edit.documentChanges.reduce((sum, doc) => sum + doc.edits.length, 0);
|
||||
}
|
||||
|
||||
return { files, edits };
|
||||
}
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user