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:
Yeachan-Heo
2026-01-09 03:46:13 +00:00
commit cd98f12fac
31 changed files with 7750 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.DS_Store

26
.npmignore Normal file
View 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
View 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
View File

@@ -0,0 +1,416 @@
# Oh-My-Claude-Sisyphus
[![npm version](https://badge.fury.io/js/oh-my-claude-sisyphus.svg)](https://www.npmjs.com/package/oh-my-claude-sisyphus)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
View 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
View 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

File diff suppressed because it is too large Load Diff

75
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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' } }
}
}
}
};
}

View 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
View 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';

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
/**
* Shared Types Export
*/
export * from './types.js';

112
src/shared/types.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}