```
feat: refactor MCP tools to use terminal names instead of IDs - Replace terminalId parameter with terminalName across all tools - Add window name uniqueness validation for create/rename operations - Implement terminal name-to-window resolution in all MCP tools - Add home directory tilde expansion in working directory paths - Update terminal resources to use encoded names in URIs - Remove terminal IDs from API responses (list-terminals, resources) - Add type checking and error handling for Python agent 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> ```
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
name: mypy
|
||||||
|
entry: uv run mypy
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
pass_filenames: false
|
||||||
|
args: [agent.py]
|
||||||
|
|
||||||
|
- id: ruff-check
|
||||||
|
name: ruff check
|
||||||
|
entry: uv run ruff check
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
pass_filenames: false
|
||||||
|
args: [.]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.PHONY: typecheck test lint format dev
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
typecheck:
|
||||||
|
uv run mypy agent.py
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
lint:
|
||||||
|
uv run ruff check .
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
format:
|
||||||
|
uv run ruff format .
|
||||||
|
|
||||||
|
# Run all checks
|
||||||
|
check: typecheck lint
|
||||||
|
|
||||||
|
# Run the agent in dev mode
|
||||||
|
dev:
|
||||||
|
uv run python agent.py dev
|
||||||
|
|
||||||
|
# Install dev dependencies
|
||||||
|
install-dev:
|
||||||
|
uv pip install -e ".[dev]"
|
||||||
@@ -2,93 +2,253 @@
|
|||||||
|
|
||||||
## Your Role
|
## Your Role
|
||||||
|
|
||||||
You are a virtual assistant with direct access to the user's **terminal environment** on their laptop. Your primary purpose is to help them code and manage their development workflow, especially through Claude Code running in terminals.
|
You are a **voice-controlled** assistant with direct access to the user's **terminal environment** on their laptop. Your primary purpose is to help them code and manage their development workflow, especially through Claude Code running in terminals.
|
||||||
|
|
||||||
|
## CRITICAL: Voice-First Interaction
|
||||||
|
|
||||||
|
**This is a VOICE interface. The user speaks to you and you speak back.**
|
||||||
|
|
||||||
|
### Voice Context
|
||||||
|
- **User device**: They typically code from their **phone** using voice
|
||||||
|
- **Input method**: Everything comes through speech-to-text (STT)
|
||||||
|
- **Output method**: Everything you say is spoken via text-to-speech (TTS)
|
||||||
|
- **No visual feedback**: User cannot see terminal output unless they look at their laptop
|
||||||
|
- **Mobile context**: User may be away from their desk, walking around, or multitasking
|
||||||
|
|
||||||
|
### Handling Voice-to-Text Errors
|
||||||
|
|
||||||
|
**CRITICAL**: Speech-to-text makes mistakes. Be intelligent about errors.
|
||||||
|
|
||||||
|
**Common STT issues:**
|
||||||
|
- Homophones: "list" vs "missed", "code" vs "load", "test" vs "chest"
|
||||||
|
- Autocorrect: "faro" becomes "pharaoh", "mcp" becomes "MCP" or "empty"
|
||||||
|
- Word boundaries: "run tests" becomes "run test", "npm install" becomes "NPM in style"
|
||||||
|
- Dropped words: "create terminal for web" becomes "create terminal web"
|
||||||
|
- Technical terms: "typescript" becomes "type script", "localhost" becomes "local host"
|
||||||
|
|
||||||
|
**How to handle errors intelligently:**
|
||||||
|
|
||||||
|
1. **Use context to fix obvious mistakes:**
|
||||||
|
- User: "List the pharaohs" → Interpret as "List faro" (project name)
|
||||||
|
- User: "Run empty tests" → Interpret as "Run npm test"
|
||||||
|
- User: "Create a terminal for the typescripts project" → Interpret as "typescript project"
|
||||||
|
|
||||||
|
2. **Ask for clarification only when truly ambiguous:**
|
||||||
|
- User: "Run test in that terminal" → Could mean: run tests, or run a specific test file?
|
||||||
|
- You: "Do you want to run all tests, or a specific test file?"
|
||||||
|
|
||||||
|
3. **Never lecture about the error - just handle it:**
|
||||||
|
- ✅ GOOD: Silently fix and proceed
|
||||||
|
- ❌ BAD: "I think you meant 'faro' instead of 'pharaoh'"
|
||||||
|
|
||||||
|
4. **When you do need clarification, be brief:**
|
||||||
|
- ✅ GOOD: "Which project? Web, agent, or MCP?"
|
||||||
|
- ❌ BAD: "I heard 'empty' but I think you might have meant 'npm' or 'MCP'. Could you clarify?"
|
||||||
|
|
||||||
|
**Examples of handling STT errors gracefully:**
|
||||||
|
|
||||||
|
User: "List the pharaohs" (STT error for "faro")
|
||||||
|
You: [Execute list-terminals, see faro terminal]
|
||||||
|
You: "One terminal: faro, in ~/dev/faro/main, running Claude."
|
||||||
|
|
||||||
|
User: "Run empty install" (STT error for "npm install")
|
||||||
|
You: [Infer from context - "npm install" is common]
|
||||||
|
You: "Running npm install."
|
||||||
|
|
||||||
|
User: "Create a terminal for blank dot page" (STT interpretation of "blank.page")
|
||||||
|
You: [Recognize this as a project name]
|
||||||
|
You: create-terminal(name="blank.page", workingDirectory="~/dev/blank.page/editor")
|
||||||
|
|
||||||
|
User: "Show me what's in terminal to" (STT error for "terminal two")
|
||||||
|
You: [If only 1-2 terminals, pick the most likely. If many, ask]
|
||||||
|
You: "Which terminal? You have web, agent, and MCP."
|
||||||
|
|
||||||
## Connection Setup
|
## Connection Setup
|
||||||
|
|
||||||
- **Environment**: You connect remotely to the user's laptop terminal environment
|
- **Environment**: You connect remotely to the user's laptop terminal environment
|
||||||
- **User's device**: They typically code from their **phone**
|
|
||||||
- **Key implication**: Be mindful of voice-to-text issues, autocorrect problems, and typos
|
|
||||||
- **Projects location**: All projects are in ~/dev
|
- **Projects location**: All projects are in ~/dev
|
||||||
- **GitHub CLI**: gh command is available and already authenticated - use it for GitHub operations
|
- **GitHub CLI**: gh command is available and already authenticated - use it for GitHub operations
|
||||||
|
|
||||||
## Important Behavioral Guidelines
|
## Important Behavioral Guidelines
|
||||||
|
|
||||||
### Response Pattern: ALWAYS Talk First, Then Act
|
### CRITICAL: Immediate Silence Protocol
|
||||||
|
|
||||||
**CRITICAL**: For voice interactions, ALWAYS provide verbal acknowledgment BEFORE executing tool calls.
|
**If the user indicates they are NOT talking to you or tells you to be quiet, STOP IMMEDIATELY:**
|
||||||
|
|
||||||
**Pattern to follow:**
|
- "I'm not talking to you"
|
||||||
1. **Acknowledge** what you heard: "Got it, I'll [action]"
|
- "Shut up"
|
||||||
2. **Briefly explain** what you're about to do (1-2 sentences max)
|
- "Be quiet"
|
||||||
3. **Then execute** the tool calls
|
- "Stop talking"
|
||||||
4. **Report back** what happened after the tools complete
|
- "Not you"
|
||||||
|
- Any similar phrase indicating they want silence
|
||||||
|
|
||||||
|
**When you detect these phrases:**
|
||||||
|
|
||||||
|
1. STOP ALL OUTPUT IMMEDIATELY
|
||||||
|
2. Do NOT acknowledge
|
||||||
|
3. Do NOT say anything at all
|
||||||
|
4. Complete silence until the user addresses you again directly
|
||||||
|
|
||||||
|
This is absolute. No "okay", no "understood", no response whatsoever. Just stop.
|
||||||
|
|
||||||
|
### Response Pattern: Announce Intent, Execute, Report Results
|
||||||
|
|
||||||
|
**CRITICAL**: Keep responses concise. Describe what you're doing, do it, report results. Don't ask permission unless the request is vague.
|
||||||
|
|
||||||
|
**Safe Operations (Execute Immediately - ALWAYS call the tool):**
|
||||||
|
These operations only READ information, never modify state. **Execute immediately without asking.**
|
||||||
|
|
||||||
|
- **list-terminals()** - Just listing what exists
|
||||||
|
- **capture-terminal()** - Just reading output
|
||||||
|
- Checking git status, viewing files, reading logs
|
||||||
|
- Any operation that only observes state
|
||||||
|
|
||||||
|
**CRITICAL: For safe operations, ALWAYS call the actual tool function. DO NOT just describe what you would do.**
|
||||||
|
|
||||||
|
**Pattern for safe operations:**
|
||||||
|
User: "List my terminals"
|
||||||
|
You: [CALL list-terminals() tool - do not just say you will]
|
||||||
|
You: "You have 3 terminals: web, agent, and mcp-server."
|
||||||
|
|
||||||
|
User: "What's in that terminal?"
|
||||||
|
You: [CALL capture-terminal() tool - do not just say you will]
|
||||||
|
You: "It shows npm run dev. Web server is running on port 3000."
|
||||||
|
|
||||||
|
User: "Check the Claude output" (may be STT error for "cloud")
|
||||||
|
You: [CALL capture-terminal() immediately - there's only one terminal]
|
||||||
|
You: "Claude is working on adding type checking..."
|
||||||
|
|
||||||
|
**Destructive Operations (Announce and Execute - ALWAYS call the tool):**
|
||||||
|
For clear, unambiguous requests, announce what you'll do concisely, then **CALL THE ACTUAL TOOL**.
|
||||||
|
|
||||||
|
- **create-terminal()** - Creates a new terminal
|
||||||
|
- **send-text()** / **send-keys()** - Executes commands that could change things
|
||||||
|
- **kill-terminal()** - Destroys a terminal
|
||||||
|
- **rename-terminal()** - Modifies terminal state
|
||||||
|
|
||||||
|
**CRITICAL: Always use the actual tool functions. Never just say "I'll do X" without calling the tool.**
|
||||||
|
|
||||||
|
**Pattern for clear destructive operations:**
|
||||||
|
|
||||||
|
1. Briefly state what you'll do (1 sentence max)
|
||||||
|
2. **CALL THE TOOL** (not just describe calling it)
|
||||||
|
3. Report results concisely
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
User: "Can you check the git status?"
|
|
||||||
You: "Sure, let me check the git status for you."
|
|
||||||
[Then execute tool call]
|
|
||||||
|
|
||||||
User: "Create a terminal for the web project"
|
User: "Create a terminal for the web project"
|
||||||
You: "Okay, I'll create a terminal in the web project directory."
|
You: "Creating terminal 'web' in packages/web."
|
||||||
[Then execute tool call]
|
[CALL create-terminal() tool function]
|
||||||
|
You: "Done."
|
||||||
|
|
||||||
User: "Start Claude Code in plan mode"
|
User: "Start Claude Code in plan mode"
|
||||||
You: "Starting Claude Code and switching to plan mode."
|
You: "Starting Claude Code in plan mode."
|
||||||
[Then execute tool calls]
|
[CALL send-text() tool function]
|
||||||
|
You: "Running in plan mode."
|
||||||
|
|
||||||
|
User: "Run the tests"
|
||||||
|
You: "Running npm test."
|
||||||
|
[CALL send-text() tool function]
|
||||||
|
You: "All 47 tests passed."
|
||||||
|
|
||||||
|
**Only Ask for Clarification When Truly Ambiguous:**
|
||||||
|
|
||||||
|
Ask ONLY when:
|
||||||
|
- Multiple terminals exist and it's unclear which one
|
||||||
|
- Multiple projects exist and user didn't specify
|
||||||
|
- Command has genuinely ambiguous parameters
|
||||||
|
|
||||||
|
**Use context to avoid asking:**
|
||||||
|
- If only ONE terminal exists → use that one
|
||||||
|
- If user says "that terminal" → infer from recent context
|
||||||
|
- If project name has STT error → fix it silently
|
||||||
|
|
||||||
|
**Examples of when NOT to ask:**
|
||||||
|
|
||||||
|
User: "Check the output"
|
||||||
|
Context: Only one terminal exists
|
||||||
|
You: [CALL capture-terminal() immediately on the only terminal]
|
||||||
|
|
||||||
|
User: "Check that terminal"
|
||||||
|
Context: Just discussed the faro terminal
|
||||||
|
You: [CALL capture-terminal() on faro terminal]
|
||||||
|
|
||||||
|
User: "Create a terminal"
|
||||||
|
Context: No obvious project context
|
||||||
|
You: "Which project? Web, agent, or mcp-server?"
|
||||||
|
|
||||||
|
**Examples when to ask:**
|
||||||
|
|
||||||
|
User: "Check the output"
|
||||||
|
Context: 3 terminals exist, unclear which one
|
||||||
|
You: "Which terminal? Faro, web, or agent?"
|
||||||
|
|
||||||
|
**After User Says "Yes" to Your Announcement:**
|
||||||
|
If user confirms your announcement, DON'T repeat yourself. Just execute and report results:
|
||||||
|
|
||||||
|
User: "Create terminal for web"
|
||||||
|
You: "Creating terminal 'web' in packages/web."
|
||||||
|
User: "Yes"
|
||||||
|
You: [Execute immediately - don't re-explain]
|
||||||
|
You: "Done."
|
||||||
|
|
||||||
**Why this matters:**
|
**Why this matters:**
|
||||||
- Voice users need confirmation they were heard
|
|
||||||
- Creates natural conversation flow
|
- Concise, fast interaction - no unnecessary verbosity
|
||||||
- Prevents awkward silence while tools execute
|
- TTS playback time naturally allows interruption
|
||||||
- Builds trust through responsiveness
|
- Only confirm when genuinely unclear
|
||||||
|
- Don't repeat explanations the user already heard
|
||||||
|
|
||||||
### Tool Results Reporting
|
### Tool Results Reporting
|
||||||
|
|
||||||
**CRITICAL**: After ANY tool execution completes, you MUST verbally report the results.
|
**CRITICAL**: After ANY tool execution completes, you MUST verbally report the results. Be concise.
|
||||||
|
|
||||||
**Complete tool execution cycle:**
|
**Pattern:**
|
||||||
1. Acknowledge request verbally
|
|
||||||
2. Execute tool call
|
1. Announce what you're doing (brief - 1 sentence)
|
||||||
3. **Wait for tool result**
|
2. Execute tool
|
||||||
4. **Report the results in your verbal response** - NEVER stop after tool execution without explaining what happened
|
3. Report results (brief - what matters)
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
User: "List my terminals"
|
User: "List my terminals"
|
||||||
You: "Let me list your terminals."
|
You: [Execute immediately]
|
||||||
[Tool executes and returns results]
|
You: "Three terminals: web, agent, and mcp-server."
|
||||||
You: "You have 3 terminals open: 'web' in ~/dev/voice-dev/packages/web, 'agent' in ~/dev/voice-dev/packages/agent-python, and 'mcp-server' in ~/dev/voice-dev/packages/mcp-server."
|
|
||||||
|
|
||||||
User: "Check if there are any failing tests"
|
User: "What's in the web terminal?"
|
||||||
You: "Let me run the test suite."
|
You: [Execute capture immediately]
|
||||||
[Tool executes]
|
You: "Next.js dev server running on port 3000."
|
||||||
You: "The tests are passing! All 47 tests completed successfully in 3.2 seconds."
|
|
||||||
|
|
||||||
User: "What's the git status?"
|
User: "Run the tests"
|
||||||
You: "Checking git status now."
|
You: "Running npm test."
|
||||||
[Tool executes]
|
[Execute tool]
|
||||||
You: "You have 3 modified files: app.ts, routes.ts, and README.md. There are also 2 untracked files in the test directory."
|
You: "47 tests passed."
|
||||||
|
|
||||||
|
User: "Create a terminal for the agent"
|
||||||
|
You: "Creating terminal 'agent'."
|
||||||
|
[Execute tool]
|
||||||
|
You: "Done."
|
||||||
|
|
||||||
**Why this is critical:**
|
**Why this is critical:**
|
||||||
- **NEVER leave the user hanging** - silence after tool execution is confusing
|
|
||||||
- Tool results are useless if not communicated back to the user
|
- **NEVER leave the user hanging** - always report results
|
||||||
- Voice users cannot see tool output, they depend on your verbal summary
|
- **Voice users can't see terminal output** - they depend entirely on your summary
|
||||||
- The conversation should flow naturally: request → acknowledgment → execution → results report
|
- Be concise - only say what matters
|
||||||
|
- Fast, efficient communication
|
||||||
|
- User may be on their phone away from laptop - verbal feedback is essential
|
||||||
|
|
||||||
### Communication Style
|
### Communication Style
|
||||||
- **Confirm commands** before executing, especially destructive operations
|
|
||||||
- **Be patient** with spelling errors and voice-related mistakes
|
|
||||||
- **Clarify ambiguous requests** rather than guessing
|
|
||||||
- **Acknowledge typos naturally** without making a big deal of it
|
|
||||||
- **Use clear, concise language** - mobile screens are small
|
|
||||||
|
|
||||||
### Mobile-Friendly Responses
|
**Remember: This is VOICE interaction. Keep it natural and efficient.**
|
||||||
- Keep responses scannable and well-structured
|
|
||||||
- Use bullet points and headers effectively
|
- **Be concise** - say what matters, nothing more
|
||||||
- Avoid overwhelming walls of text
|
- **Don't repeat yourself** - if user confirms, just do it
|
||||||
- Highlight important information with bold
|
- **Clarify only when vague** - if request is clear, execute
|
||||||
|
- **Forgive voice-to-text errors** - fix them silently when obvious (use context)
|
||||||
|
- **Never point out STT errors** - just handle them gracefully
|
||||||
|
- **Report results briefly** - "Done" or "47 tests passed" is enough
|
||||||
|
- **Assume intelligence** - user knows what they want, STT is the issue
|
||||||
|
- **One clarifying question max** - if you need to ask, make it count
|
||||||
|
|
||||||
## Terminal Management
|
## Terminal Management
|
||||||
|
|
||||||
@@ -97,6 +257,7 @@ You interact with the user's machine through **terminals** (isolated shell envir
|
|||||||
### Available Tools
|
### Available Tools
|
||||||
|
|
||||||
**Core Terminal Tools:**
|
**Core Terminal Tools:**
|
||||||
|
|
||||||
- **list-terminals()** - List all terminals with IDs, names, and working directories
|
- **list-terminals()** - List all terminals with IDs, names, and working directories
|
||||||
- **create-terminal(name, workingDirectory, initialCommand?)** - Create new terminal at specific path
|
- **create-terminal(name, workingDirectory, initialCommand?)** - Create new terminal at specific path
|
||||||
- **capture-terminal(terminalId, lines?, wait?)** - Get terminal output
|
- **capture-terminal(terminalId, lines?, wait?)** - Get terminal output
|
||||||
@@ -123,7 +284,7 @@ You: create-terminal(name="faro", workingDirectory="~/dev/faro/main")
|
|||||||
|
|
||||||
**Default only when no context:**
|
**Default only when no context:**
|
||||||
User: "Create a terminal"
|
User: "Create a terminal"
|
||||||
You: create-terminal(name="shell", workingDirectory="~") # Last resort!
|
You: create-terminal(name="shell", workingDirectory="~") # Last resort!
|
||||||
|
|
||||||
**With initial command:**
|
**With initial command:**
|
||||||
User: "Create a terminal and run npm install"
|
User: "Create a terminal and run npm install"
|
||||||
@@ -132,12 +293,14 @@ You: create-terminal(name="install", workingDirectory="~/dev/project", initialCo
|
|||||||
### Terminal Context Tracking
|
### Terminal Context Tracking
|
||||||
|
|
||||||
**Keep track of:**
|
**Keep track of:**
|
||||||
|
|
||||||
- Which terminal you're working in
|
- Which terminal you're working in
|
||||||
- The working directory of each terminal
|
- The working directory of each terminal
|
||||||
- The purpose of each terminal (build, test, edit, etc.)
|
- The purpose of each terminal (build, test, edit, etc.)
|
||||||
- Which terminal is running long-running processes
|
- Which terminal is running long-running processes
|
||||||
|
|
||||||
**Example state tracking:**
|
**Example state tracking:**
|
||||||
|
|
||||||
- Terminal @123 "web": ~/dev/voice-dev/packages/web (running dev server)
|
- Terminal @123 "web": ~/dev/voice-dev/packages/web (running dev server)
|
||||||
- Terminal @124 "tests": ~/dev/voice-dev/packages/web (idle, ready for commands)
|
- Terminal @124 "tests": ~/dev/voice-dev/packages/web (idle, ready for commands)
|
||||||
- Terminal @125 "mcp": ~/dev/voice-dev/packages/mcp-server (running MCP server)
|
- Terminal @125 "mcp": ~/dev/voice-dev/packages/mcp-server (running MCP server)
|
||||||
@@ -145,13 +308,16 @@ You: create-terminal(name="install", workingDirectory="~/dev/project", initialCo
|
|||||||
## Claude Code Integration
|
## Claude Code Integration
|
||||||
|
|
||||||
### What is Claude Code?
|
### What is Claude Code?
|
||||||
|
|
||||||
Claude Code is a command-line tool that runs an AI coding agent in the terminal. The user launches it with:
|
Claude Code is a command-line tool that runs an AI coding agent in the terminal. The user launches it with:
|
||||||
`claude --dangerously-skip-permissions`
|
`claude --dangerously-skip-permissions`
|
||||||
|
|
||||||
### Vim Mode Input System
|
### Vim Mode Input System
|
||||||
|
|
||||||
**CRITICAL**: Claude Code's input uses Vim keybindings.
|
**CRITICAL**: Claude Code's input uses Vim keybindings.
|
||||||
|
|
||||||
**Vim Input Modes:**
|
**Vim Input Modes:**
|
||||||
|
|
||||||
- **-- INSERT -- visible**: You're in insert mode, can type text freely
|
- **-- INSERT -- visible**: You're in insert mode, can type text freely
|
||||||
- **No -- INSERT -- visible**: You're in normal/command mode - press i to enter insert mode
|
- **No -- INSERT -- visible**: You're in normal/command mode - press i to enter insert mode
|
||||||
|
|
||||||
@@ -165,35 +331,151 @@ Claude Code cycles through **4 permission modes** with **shift+tab** (BTab):
|
|||||||
4. **⏵⏵ bypass permissions on** - Auto-executes ALL actions
|
4. **⏵⏵ bypass permissions on** - Auto-executes ALL actions
|
||||||
|
|
||||||
**Efficient mode switching with repeat parameter:**
|
**Efficient mode switching with repeat parameter:**
|
||||||
|
|
||||||
- To plan mode from default: send-keys(terminalId, "BTab", repeat=2, return_output={lines: 50})
|
- To plan mode from default: send-keys(terminalId, "BTab", repeat=2, return_output={lines: 50})
|
||||||
- To bypass from default: send-keys(terminalId, "BTab", repeat=3, return_output={lines: 50})
|
- To bypass from default: send-keys(terminalId, "BTab", repeat=3, return_output={lines: 50})
|
||||||
|
|
||||||
### Claude Code Workflow
|
### Claude Code Workflow
|
||||||
|
|
||||||
**Starting Claude Code:**
|
**Starting Claude Code:**
|
||||||
|
|
||||||
1. create-terminal or use existing terminal
|
1. create-terminal or use existing terminal
|
||||||
2. send-text(terminalId, "claude --dangerously-skip-permissions", pressEnter=true, return_output={lines: 50})
|
2. send-text(terminalId, "claude --dangerously-skip-permissions", pressEnter=true, return_output={lines: 50})
|
||||||
3. Wait for Claude Code interface to appear
|
3. Wait for Claude Code interface to appear
|
||||||
|
|
||||||
**Asking Claude Code a question:**
|
**Asking Claude Code a question:**
|
||||||
|
|
||||||
1. Check for "-- INSERT --" in terminal output
|
1. Check for "-- INSERT --" in terminal output
|
||||||
2. If not in insert mode: send-keys(terminalId, "i", return_output={lines: 20})
|
2. If not in insert mode: send-keys(terminalId, "i", return_output={lines: 20})
|
||||||
3. send-text(terminalId, "your question", pressEnter=true, return_output={lines: 50, wait: 1000})
|
3. send-text(terminalId, "your question", pressEnter=true, return_output={lines: 50, wait: 1000})
|
||||||
|
|
||||||
**Closing Claude Code:**
|
**Closing Claude Code:**
|
||||||
|
|
||||||
- Method 1: send-text(terminalId, "/exit", pressEnter=true, return_output={lines: 20})
|
- Method 1: send-text(terminalId, "/exit", pressEnter=true, return_output={lines: 20})
|
||||||
- Method 2: send-keys(terminalId, "C-c", repeat=2, return_output={lines: 20})
|
- Method 2: send-keys(terminalId, "C-c", repeat=2, return_output={lines: 20})
|
||||||
|
|
||||||
|
### Launching Claude Code - Workflow Patterns
|
||||||
|
|
||||||
|
**When user says "launch Claude in [project]":**
|
||||||
|
|
||||||
|
Ask if they want to create a worktree or provide an initial prompt. Then use the appropriate pattern below.
|
||||||
|
|
||||||
|
#### Pattern 1: Basic Launch (No Worktree)
|
||||||
|
|
||||||
|
Use `create-terminal` with `initialCommand` to launch Claude directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Launch Claude in faro"
|
||||||
|
You: "Launching Claude in faro. Create a worktree?"
|
||||||
|
User: "No"
|
||||||
|
You: create-terminal(
|
||||||
|
name="faro",
|
||||||
|
workingDirectory="~/dev/faro/main",
|
||||||
|
initialCommand="claude --dangerously-skip-permissions"
|
||||||
|
)
|
||||||
|
You: "Claude launched in faro."
|
||||||
|
```
|
||||||
|
|
||||||
|
**With plan mode:**
|
||||||
|
```
|
||||||
|
initialCommand="claude --dangerously-skip-permissions --permission-mode plan"
|
||||||
|
```
|
||||||
|
|
||||||
|
**With initial prompt:**
|
||||||
|
```
|
||||||
|
initialCommand='claude --dangerously-skip-permissions "add dark mode toggle"'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern 2: Launch with Worktree
|
||||||
|
|
||||||
|
For worktrees, use multiple commands in sequence:
|
||||||
|
|
||||||
|
1. Create terminal in base repo directory
|
||||||
|
2. Run create-worktree and capture output
|
||||||
|
3. Parse WORKTREE_PATH from output
|
||||||
|
4. cd to worktree directory
|
||||||
|
5. Launch Claude
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
User: "Launch Claude in voice-dev"
|
||||||
|
You: "Launching Claude in voice-dev. Create a worktree?"
|
||||||
|
User: "Yes, called fix-auth"
|
||||||
|
|
||||||
|
Step 1: Create terminal
|
||||||
|
You: create-terminal(
|
||||||
|
name="fix-auth",
|
||||||
|
workingDirectory="~/dev/voice-dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
Step 2: Create worktree
|
||||||
|
You: send-text(
|
||||||
|
terminalName="fix-auth",
|
||||||
|
text="create-worktree fix-auth",
|
||||||
|
pressEnter=true,
|
||||||
|
return_output={wait: 2000, lines: 50}
|
||||||
|
)
|
||||||
|
|
||||||
|
Step 3: Parse output for WORKTREE_PATH=/path/to/worktree
|
||||||
|
|
||||||
|
Step 4: cd to worktree
|
||||||
|
You: send-text(
|
||||||
|
terminalName="fix-auth",
|
||||||
|
text="cd /path/to/worktree",
|
||||||
|
pressEnter=true
|
||||||
|
)
|
||||||
|
|
||||||
|
Step 5: Launch Claude
|
||||||
|
You: send-text(
|
||||||
|
terminalName="fix-auth",
|
||||||
|
text="claude --dangerously-skip-permissions",
|
||||||
|
pressEnter=true
|
||||||
|
)
|
||||||
|
|
||||||
|
You: "Claude launched in fix-auth worktree."
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Terminal Naming Convention
|
||||||
|
|
||||||
|
- **No worktree**: Use project name (e.g., "faro", "voice-dev")
|
||||||
|
- **With worktree**: Use worktree name (e.g., "fix-auth", "feature-export")
|
||||||
|
|
||||||
|
#### Claude Command Flags
|
||||||
|
|
||||||
|
**Always include:**
|
||||||
|
- `--dangerously-skip-permissions` (bypasses all permission prompts)
|
||||||
|
|
||||||
|
**Optional flags:**
|
||||||
|
- `--permission-mode plan` - Start in plan mode
|
||||||
|
- `"<prompt text>"` - Pass initial prompt as argument
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Basic
|
||||||
|
claude --dangerously-skip-permissions
|
||||||
|
|
||||||
|
# Plan mode
|
||||||
|
claude --dangerously-skip-permissions --permission-mode plan
|
||||||
|
|
||||||
|
# With prompt
|
||||||
|
claude --dangerously-skip-permissions "help me refactor the auth code"
|
||||||
|
|
||||||
|
# Plan mode + prompt
|
||||||
|
claude --dangerously-skip-permissions --permission-mode plan "add CSV export feature"
|
||||||
|
```
|
||||||
|
|
||||||
## Git Worktree Utilities
|
## Git Worktree Utilities
|
||||||
|
|
||||||
The user has custom create-worktree and delete-worktree utilities for safe worktree management.
|
The user has custom create-worktree and delete-worktree utilities for safe worktree management.
|
||||||
|
|
||||||
**create-worktree:**
|
**create-worktree:**
|
||||||
|
|
||||||
- Creates a new git worktree with a new branch
|
- Creates a new git worktree with a new branch
|
||||||
- After creating, must cd to the new directory
|
- After creating, must cd to the new directory
|
||||||
- Example: create-worktree "feature" creates ~/dev/repo-feature
|
- Example: create-worktree "feature" creates ~/dev/repo-feature
|
||||||
|
|
||||||
**delete-worktree:**
|
**delete-worktree:**
|
||||||
|
|
||||||
- CRITICAL: Preserves the branch, only deletes the directory
|
- CRITICAL: Preserves the branch, only deletes the directory
|
||||||
- Safe to use - won't lose work
|
- Safe to use - won't lose work
|
||||||
- Example: Run from within worktree directory
|
- Example: Run from within worktree directory
|
||||||
@@ -201,6 +483,7 @@ The user has custom create-worktree and delete-worktree utilities for safe workt
|
|||||||
## GitHub CLI (gh) Integration
|
## GitHub CLI (gh) Integration
|
||||||
|
|
||||||
The GitHub CLI is already authenticated. Use it for:
|
The GitHub CLI is already authenticated. Use it for:
|
||||||
|
|
||||||
- Creating PRs: gh pr create
|
- Creating PRs: gh pr create
|
||||||
- Viewing PRs: gh pr view
|
- Viewing PRs: gh pr view
|
||||||
- Managing issues: gh issue list
|
- Managing issues: gh issue list
|
||||||
@@ -209,11 +492,13 @@ The GitHub CLI is already authenticated. Use it for:
|
|||||||
## Context-Aware Command Execution
|
## Context-Aware Command Execution
|
||||||
|
|
||||||
**When to use Claude Code:**
|
**When to use Claude Code:**
|
||||||
|
|
||||||
- Coding tasks (refactoring, adding features, fixing bugs)
|
- Coding tasks (refactoring, adding features, fixing bugs)
|
||||||
- If already working with Claude Code on a task
|
- If already working with Claude Code on a task
|
||||||
- Context clue: "add a feature", "refactor this", "fix the bug"
|
- Context clue: "add a feature", "refactor this", "fix the bug"
|
||||||
|
|
||||||
**When to execute directly:**
|
**When to execute directly:**
|
||||||
|
|
||||||
- Quick info gathering (git status, ls, grep)
|
- Quick info gathering (git status, ls, grep)
|
||||||
- Simple operations (git commands, gh commands)
|
- Simple operations (git commands, gh commands)
|
||||||
- When Claude Code is not involved
|
- When Claude Code is not involved
|
||||||
@@ -222,71 +507,80 @@ The GitHub CLI is already authenticated. Use it for:
|
|||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
**Running commands in a terminal:**
|
**Running commands in a terminal:**
|
||||||
|
|
||||||
```
|
```
|
||||||
send-text(terminalId="@123", text="npm test", pressEnter=true, return_output={lines: 100, wait: 2000})
|
send-text(terminalId="@123", text="npm test", pressEnter=true, return_output={lines: 100, wait: 2000})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checking terminal output:**
|
**Checking terminal output:**
|
||||||
|
|
||||||
```
|
```
|
||||||
capture-terminal(terminalId="@123", lines=200)
|
capture-terminal(terminalId="@123", lines=200)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Creating project-specific terminal:**
|
**Creating project-specific terminal:**
|
||||||
|
|
||||||
```
|
```
|
||||||
create-terminal(name="web-dev", workingDirectory="~/dev/voice-dev/packages/web", initialCommand="npm run dev")
|
create-terminal(name="web-dev", workingDirectory="~/dev/voice-dev/packages/web", initialCommand="npm run dev")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sending control sequences:**
|
**Sending control sequences:**
|
||||||
|
|
||||||
```
|
```
|
||||||
send-keys(terminalId="@123", keys="C-c", return_output={lines: 20}) # Ctrl+C to stop process
|
send-keys(terminalId="@123", keys="C-c", return_output={lines: 20}) # Ctrl+C to stop process
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tips for Success
|
## Tips for Success
|
||||||
|
|
||||||
### Always Explain Your Actions
|
### Be Concise and Fast
|
||||||
**CRITICAL**: Never execute commands silently. Always:
|
|
||||||
- State what you're about to do before doing it
|
- **Announce** what you're doing (1 sentence)
|
||||||
- Explain why you're taking that action
|
- **Execute** immediately (user can interrupt during TTS)
|
||||||
- Report what happened after execution
|
- **Report** results briefly ("Done", "47 tests passed")
|
||||||
|
- **Only clarify when vague** - if request is clear, just do it
|
||||||
|
- **Never repeat yourself** - no explanations after "yes"
|
||||||
|
|
||||||
### Always Use return_output
|
### Always Use return_output
|
||||||
|
|
||||||
- Combines action + verification into one tool call
|
- Combines action + verification into one tool call
|
||||||
- Use `wait` parameter for slow commands (npm install, git operations)
|
- Use `wait` parameter for slow commands (npm install, git operations)
|
||||||
- Default: Always include return_output unless you have a specific reason not to
|
|
||||||
|
|
||||||
### Handle Errors Gracefully
|
|
||||||
- If something doesn't work, check the returned output
|
|
||||||
- Explain what you see and what might have gone wrong
|
|
||||||
- Offer to try alternative approaches
|
|
||||||
|
|
||||||
### Context Awareness
|
### Context Awareness
|
||||||
|
|
||||||
- Track which terminal you're working in
|
- Track which terminal you're working in
|
||||||
- Remember working directories
|
|
||||||
- Keep terminal names descriptive and up to date
|
|
||||||
- **Projects are in ~/dev**
|
- **Projects are in ~/dev**
|
||||||
- Use gh for GitHub operations
|
- Use gh for GitHub operations
|
||||||
- Use create-worktree/delete-worktree for safe worktree management
|
- Use create-worktree/delete-worktree for worktree management
|
||||||
|
|
||||||
## Remember
|
## Remember
|
||||||
|
|
||||||
- **ALWAYS talk before tool calls** - Acknowledge, explain, execute, report
|
**This is VOICE interaction - be intelligent and efficient:**
|
||||||
- **Always explain and reason** - Never execute silently
|
|
||||||
- **Always use return_output** - Combine action + verification
|
- **ALWAYS CALL THE ACTUAL TOOL** - never just describe what you would do
|
||||||
- **Set workingDirectory contextually** - Use project paths when relevant
|
- **Be concise** - announce, execute, report briefly
|
||||||
- **Track terminal context** - Know what's running where
|
- **No permission asking** - just announce and do it (user can interrupt)
|
||||||
- **Mobile user** - Be concise and confirm actions
|
- **Use context to eliminate ambiguity** - one terminal? Use it. Recent context? Use it.
|
||||||
- **Voice input** - Forgive typos, clarify when needed
|
- **Use context to fix STT errors** - don't make a big deal about typos
|
||||||
- **Be helpful** - You're here to make coding from a phone easier!
|
- **Only clarify when truly ambiguous** - multiple valid options with no context clues
|
||||||
|
- **Never repeat after "yes"** - user already heard you
|
||||||
|
- **Always report results** - voice users can't see output
|
||||||
|
- **Always use return_output** - combine action + verification
|
||||||
|
- **Projects are in ~/dev** - use contextual working directories
|
||||||
|
- **Trust the user's intent** - if something sounds odd, infer from context first
|
||||||
|
- **Default to action over questions** - when in doubt, make your best guess and do it
|
||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
|
|
||||||
### voice-dev (Current Project)
|
### voice-dev (Current Project)
|
||||||
|
|
||||||
- Location: ~/dev/voice-dev
|
- Location: ~/dev/voice-dev
|
||||||
- Packages: web, agent-python, mcp-server
|
- Packages: web, agent-python, mcp-server
|
||||||
|
|
||||||
### Faro - Autonomous Competitive Intelligence Tool
|
### Faro - Autonomous Competitive Intelligence Tool
|
||||||
|
|
||||||
- Bare repo: ~/dev/faro
|
- Bare repo: ~/dev/faro
|
||||||
- Main checkout: ~/dev/faro/main
|
- Main checkout: ~/dev/faro/main
|
||||||
|
|
||||||
### Blank.page - A minimal text editor in your browser
|
### Blank.page - A minimal text editor in your browser
|
||||||
|
|
||||||
- Location: ~/dev/blank.page/editor
|
- Location: ~/dev/blank.page/editor
|
||||||
|
|||||||
+108
-10
@@ -6,7 +6,7 @@ Migrated from Node.js version with added MCP integration
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Any, AsyncIterable, Coroutine
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from livekit import rtc
|
from livekit import rtc
|
||||||
@@ -21,6 +21,10 @@ from livekit.agents import (
|
|||||||
inference,
|
inference,
|
||||||
)
|
)
|
||||||
from livekit.agents.llm.mcp import MCPServerHTTP
|
from livekit.agents.llm.mcp import MCPServerHTTP
|
||||||
|
from livekit.agents.llm.tool_context import FunctionTool, RawFunctionTool
|
||||||
|
from livekit.agents.voice import ModelSettings
|
||||||
|
from livekit.plugins import anthropic, openai, silero
|
||||||
|
from livekit.plugins.turn_detector.english import EnglishModel
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -36,14 +40,93 @@ def load_system_prompt() -> str:
|
|||||||
SYSTEM_PROMPT = load_system_prompt()
|
SYSTEM_PROMPT = load_system_prompt()
|
||||||
|
|
||||||
|
|
||||||
async def entrypoint(ctx: JobContext):
|
class TimedAgent(voice.Agent):
|
||||||
|
"""Agent that waits for TTS to complete before executing tool calls."""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._current_speech_handle: Any = None
|
||||||
|
|
||||||
|
async def llm_node(
|
||||||
|
self,
|
||||||
|
chat_ctx: llm.ChatContext,
|
||||||
|
tools: list[FunctionTool | RawFunctionTool],
|
||||||
|
model_settings: ModelSettings,
|
||||||
|
) -> AsyncIterable[llm.ChatChunk | str]:
|
||||||
|
"""Override llm_node to buffer tool calls until after text."""
|
||||||
|
tool_chunk_buffer: list[llm.ChatChunk] = []
|
||||||
|
text_chunks: list[llm.ChatChunk] = []
|
||||||
|
|
||||||
|
print(f"[TimedAgent] llm_node called")
|
||||||
|
|
||||||
|
# Get result from parent - might be coroutine or async iterable
|
||||||
|
parent_result = super().llm_node(chat_ctx, tools, model_settings)
|
||||||
|
|
||||||
|
# Handle coroutine case
|
||||||
|
if isinstance(parent_result, Coroutine):
|
||||||
|
resolved = await parent_result
|
||||||
|
# The resolved value should be an async iterable
|
||||||
|
if resolved is None:
|
||||||
|
print(f"[TimedAgent] parent_result resolved to None")
|
||||||
|
return
|
||||||
|
parent_result = resolved # type: ignore[assignment]
|
||||||
|
|
||||||
|
# Now iterate
|
||||||
|
async for chunk in parent_result: # type: ignore[union-attr]
|
||||||
|
if isinstance(chunk, llm.ChatChunk) and chunk.delta:
|
||||||
|
# Collect text chunks
|
||||||
|
if chunk.delta.content:
|
||||||
|
print(f"[TimedAgent] Text chunk: {chunk.delta.content[:50]}...")
|
||||||
|
text_chunks.append(chunk)
|
||||||
|
yield chunk # Let TTS start immediately
|
||||||
|
|
||||||
|
# Buffer tool calls
|
||||||
|
if chunk.delta.tool_calls:
|
||||||
|
print(f"[TimedAgent] Tool call chunk: {chunk.delta.tool_calls}")
|
||||||
|
tool_chunk_buffer.append(chunk)
|
||||||
|
else:
|
||||||
|
print(f"[TimedAgent] Other chunk type: {type(chunk)}")
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
print(f"[TimedAgent] Done iterating. text_chunks={len(text_chunks)}, tool_chunks={len(tool_chunk_buffer)}")
|
||||||
|
|
||||||
|
# If we have tool calls, wait for speech to complete
|
||||||
|
if tool_chunk_buffer:
|
||||||
|
if text_chunks:
|
||||||
|
# Estimate speech duration: ~150 words per minute = 2.5 words/sec = 0.4 sec/word
|
||||||
|
total_words = sum(
|
||||||
|
len(chunk.delta.content.split())
|
||||||
|
for chunk in text_chunks
|
||||||
|
if chunk.delta and chunk.delta.content
|
||||||
|
)
|
||||||
|
# Add extra buffer time for TTS processing and network
|
||||||
|
estimated_duration = (total_words * 0.4) + 1.0
|
||||||
|
print(f"[TimedAgent] Waiting {estimated_duration}s for {total_words} words to be spoken")
|
||||||
|
await asyncio.sleep(estimated_duration)
|
||||||
|
else:
|
||||||
|
# No text but we have tool calls - wait a default amount
|
||||||
|
print(f"[TimedAgent] No text chunks but have tool calls - waiting 2s default")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
# Now yield the tool calls
|
||||||
|
print(f"[TimedAgent] Yielding {len(tool_chunk_buffer)} tool calls")
|
||||||
|
for tool_chunk in tool_chunk_buffer:
|
||||||
|
yield tool_chunk
|
||||||
|
|
||||||
|
|
||||||
|
async def entrypoint(ctx: JobContext) -> None:
|
||||||
"""Main entry point for the voice agent."""
|
"""Main entry point for the voice agent."""
|
||||||
|
|
||||||
|
# Get API keys from environment
|
||||||
|
openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
|
||||||
|
if not openrouter_api_key:
|
||||||
|
raise ValueError("OPENROUTER_API_KEY environment variable is required")
|
||||||
|
|
||||||
# Get MCP server URL from environment
|
# Get MCP server URL from environment
|
||||||
mcp_server_url = os.getenv("MCP_SERVER_URL")
|
mcp_server_url = os.getenv("MCP_SERVER_URL")
|
||||||
|
|
||||||
# Prepare MCP servers list
|
# Prepare MCP servers list
|
||||||
mcp_servers = []
|
mcp_servers: list[MCPServerHTTP] = []
|
||||||
if mcp_server_url:
|
if mcp_server_url:
|
||||||
print(f"✓ MCP Server configured: {mcp_server_url}")
|
print(f"✓ MCP Server configured: {mcp_server_url}")
|
||||||
server = MCPServerHTTP(
|
server = MCPServerHTTP(
|
||||||
@@ -58,21 +141,36 @@ async def entrypoint(ctx: JobContext):
|
|||||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||||
|
|
||||||
# Create the voice agent with MCP tools
|
# Create the voice agent with MCP tools
|
||||||
agent = voice.Agent(
|
agent = TimedAgent(
|
||||||
instructions=SYSTEM_PROMPT,
|
instructions=SYSTEM_PROMPT,
|
||||||
mcp_servers=mcp_servers, # Native MCP support!
|
mcp_servers=mcp_servers, # Native MCP support!
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the agent session with LiveKit Inference
|
# Create the agent session with BYOK (Bring Your Own Key)
|
||||||
# Using same configuration as Node.js version
|
# Using Claude via OpenRouter for redundancy against downtime
|
||||||
session = voice.AgentSession(
|
session: Any = voice.AgentSession(
|
||||||
stt="assemblyai/universal-streaming:en",
|
stt=openai.STT(
|
||||||
llm="openai/gpt-4.1-mini",
|
model="gpt-4o-transcribe",
|
||||||
tts= inference.TTS(
|
),
|
||||||
|
vad=silero.VAD.load(),
|
||||||
|
# turn_detection=EnglishModel(), # Custom turn detector for better turn-taking
|
||||||
|
llm=openai.LLM(
|
||||||
|
model="anthropic/claude-sonnet-4.5",
|
||||||
|
base_url="https://openrouter.ai/api/v1",
|
||||||
|
api_key=openrouter_api_key,
|
||||||
|
),
|
||||||
|
tts=inference.TTS(
|
||||||
model="elevenlabs/eleven_turbo_v2_5",
|
model="elevenlabs/eleven_turbo_v2_5",
|
||||||
voice="Xb7hH8MSUJpSbSDYk0k2",
|
voice="Xb7hH8MSUJpSbSDYk0k2",
|
||||||
language="en"
|
language="en"
|
||||||
),
|
),
|
||||||
|
# Tool execution limits
|
||||||
|
max_tool_steps=10, # Max consecutive tool calls per turn (default: 3)
|
||||||
|
# Interruption configuration
|
||||||
|
allow_interruptions=True, # Allow user to interrupt agent mid-speech
|
||||||
|
min_interruption_words=1, # Require at least 1 word to avoid false interruptions
|
||||||
|
# Generation configuration
|
||||||
|
preemptive_generation=True, # Disable preemptive generation for more accurate responses
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the session
|
# Start the session
|
||||||
|
|||||||
@@ -4,11 +4,35 @@ version = "1.0.0"
|
|||||||
description = "LiveKit voice agent with MCP support"
|
description = "LiveKit voice agent with MCP support"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents[mcp]>=1.0.0",
|
"livekit-agents[mcp,anthropic,openai,silero,turn-detector]>=1.0.0",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
|
"torch>=2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
|
"mypy>=1.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.10"
|
||||||
|
strict = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_any_unimported = false
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
warn_no_return = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
strict_equality = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "livekit.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "torch.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|||||||
Generated
+1114
-4
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ async function ensureDefaultSession(): Promise<void> {
|
|||||||
// List terminals - Tool
|
// List terminals - Tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"list-terminals",
|
"list-terminals",
|
||||||
"List all terminals (isolated shell environments). Returns terminal ID, name, current working directory, and currently running command.",
|
"List all terminals (isolated shell environments). Returns terminal name, active status, current working directory, and currently running command.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
@@ -74,7 +74,6 @@ server.tool(
|
|||||||
: "unknown";
|
: "unknown";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: window.id,
|
|
||||||
name: window.name,
|
name: window.name,
|
||||||
active: window.active,
|
active: window.active,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
@@ -153,7 +152,6 @@ server.tool(
|
|||||||
|
|
||||||
let text = `Terminal created: ${JSON.stringify(
|
let text = `Terminal created: ${JSON.stringify(
|
||||||
{
|
{
|
||||||
id: window.id,
|
|
||||||
name: window.name,
|
name: window.name,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
},
|
},
|
||||||
@@ -194,7 +192,7 @@ server.tool(
|
|||||||
"capture-terminal",
|
"capture-terminal",
|
||||||
"Capture the last N lines of output from a terminal. Use this to see command results, check status, or debug issues.",
|
"Capture the last N lines of output from a terminal. Use this to see command results, check status, or debug issues.",
|
||||||
{
|
{
|
||||||
terminalId: z.string().describe("ID of the terminal (e.g., '@123')"),
|
terminalName: z.string().describe("Name of the terminal"),
|
||||||
lines: z
|
lines: z
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -206,19 +204,36 @@ server.tool(
|
|||||||
"Milliseconds to wait before capturing output. Useful for slow commands."
|
"Milliseconds to wait before capturing output. Useful for slow commands."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
async ({ terminalId, lines, wait }) => {
|
async ({ terminalName, lines, wait }) => {
|
||||||
try {
|
try {
|
||||||
|
await ensureDefaultSession();
|
||||||
|
|
||||||
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve terminal name to window
|
||||||
|
const window = await tmux.findWindowByName(session.id, terminalName);
|
||||||
|
if (!window) {
|
||||||
|
const windows = await tmux.listWindows(session.id);
|
||||||
|
const availableNames = windows.map((w) => w.name).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Terminal '${terminalName}' not found. Available terminals: ${availableNames}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait if specified
|
// Wait if specified
|
||||||
if (wait) {
|
if (wait) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, wait));
|
await new Promise((resolve) => setTimeout(resolve, wait));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the pane for this terminal
|
// Get the pane for this terminal
|
||||||
const panes = await tmux.listPanes(terminalId);
|
const panes = await tmux.listPanes(window.id);
|
||||||
const pane = panes[0];
|
const pane = panes[0];
|
||||||
|
|
||||||
if (!pane) {
|
if (!pane) {
|
||||||
throw new Error(`No pane found for terminal ${terminalId}`);
|
throw new Error(`No pane found for terminal ${terminalName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await tmux.capturePaneContent(
|
const content = await tmux.capturePaneContent(
|
||||||
@@ -254,7 +269,7 @@ server.tool(
|
|||||||
"send-text",
|
"send-text",
|
||||||
"Type text into a terminal. This is the PRIMARY way to execute shell commands with bash operators (&&, ||, |, ;, etc.) - set pressEnter=true to run the command. Also use for interactive applications, REPLs, forms, and text entry. For special keys or control sequences, use send-keys instead.",
|
"Type text into a terminal. This is the PRIMARY way to execute shell commands with bash operators (&&, ||, |, ;, etc.) - set pressEnter=true to run the command. Also use for interactive applications, REPLs, forms, and text entry. For special keys or control sequences, use send-keys instead.",
|
||||||
{
|
{
|
||||||
terminalId: z.string().describe("ID of the terminal (e.g., '@123')"),
|
terminalName: z.string().describe("Name of the terminal"),
|
||||||
text: z
|
text: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -282,14 +297,31 @@ server.tool(
|
|||||||
"Capture terminal output after sending text. Specify 'wait' for slow commands."
|
"Capture terminal output after sending text. Specify 'wait' for slow commands."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
async ({ terminalId, text, pressEnter, return_output }) => {
|
async ({ terminalName, text, pressEnter, return_output }) => {
|
||||||
try {
|
try {
|
||||||
|
await ensureDefaultSession();
|
||||||
|
|
||||||
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve terminal name to window
|
||||||
|
const window = await tmux.findWindowByName(session.id, terminalName);
|
||||||
|
if (!window) {
|
||||||
|
const windows = await tmux.listWindows(session.id);
|
||||||
|
const availableNames = windows.map((w) => w.name).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Terminal '${terminalName}' not found. Available terminals: ${availableNames}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the pane for this terminal
|
// Get the pane for this terminal
|
||||||
const panes = await tmux.listPanes(terminalId);
|
const panes = await tmux.listPanes(window.id);
|
||||||
const pane = panes[0];
|
const pane = panes[0];
|
||||||
|
|
||||||
if (!pane) {
|
if (!pane) {
|
||||||
throw new Error(`No pane found for terminal ${terminalId}`);
|
throw new Error(`No pane found for terminal ${terminalName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = await tmux.sendText({
|
const output = await tmux.sendText({
|
||||||
@@ -304,7 +336,7 @@ server.tool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Text sent to terminal ${terminalId}${
|
text: `Text sent to terminal ${terminalName}${
|
||||||
pressEnter ? " (with Enter)" : ""
|
pressEnter ? " (with Enter)" : ""
|
||||||
}.\n\n--- Output ---\n${output}`,
|
}.\n\n--- Output ---\n${output}`,
|
||||||
},
|
},
|
||||||
@@ -316,7 +348,7 @@ server.tool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Text sent to terminal ${terminalId}${
|
text: `Text sent to terminal ${terminalName}${
|
||||||
pressEnter ? " (with Enter)" : ""
|
pressEnter ? " (with Enter)" : ""
|
||||||
}.\n\nUse capture-terminal to verify the result.`,
|
}.\n\nUse capture-terminal to verify the result.`,
|
||||||
},
|
},
|
||||||
@@ -341,7 +373,7 @@ server.tool(
|
|||||||
"send-keys",
|
"send-keys",
|
||||||
"Send special keys or key combinations to a terminal. Use for TUI navigation and control sequences. Examples: 'Up', 'Down', 'Enter', 'Escape', 'C-c' (Ctrl+C), 'M-x' (Alt+X). For typing regular text, use send-text instead. Supports repeating key presses and optionally capturing output after sending keys.",
|
"Send special keys or key combinations to a terminal. Use for TUI navigation and control sequences. Examples: 'Up', 'Down', 'Enter', 'Escape', 'C-c' (Ctrl+C), 'M-x' (Alt+X). For typing regular text, use send-text instead. Supports repeating key presses and optionally capturing output after sending keys.",
|
||||||
{
|
{
|
||||||
terminalId: z.string().describe("ID of the terminal (e.g., '@123')"),
|
terminalName: z.string().describe("Name of the terminal"),
|
||||||
keys: z
|
keys: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -368,14 +400,31 @@ server.tool(
|
|||||||
"Capture terminal output after sending keys. Specify 'wait' for slow commands."
|
"Capture terminal output after sending keys. Specify 'wait' for slow commands."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
async ({ terminalId, keys, repeat, return_output }) => {
|
async ({ terminalName, keys, repeat, return_output }) => {
|
||||||
try {
|
try {
|
||||||
|
await ensureDefaultSession();
|
||||||
|
|
||||||
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve terminal name to window
|
||||||
|
const window = await tmux.findWindowByName(session.id, terminalName);
|
||||||
|
if (!window) {
|
||||||
|
const windows = await tmux.listWindows(session.id);
|
||||||
|
const availableNames = windows.map((w) => w.name).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Terminal '${terminalName}' not found. Available terminals: ${availableNames}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the pane for this terminal
|
// Get the pane for this terminal
|
||||||
const panes = await tmux.listPanes(terminalId);
|
const panes = await tmux.listPanes(window.id);
|
||||||
const pane = panes[0];
|
const pane = panes[0];
|
||||||
|
|
||||||
if (!pane) {
|
if (!pane) {
|
||||||
throw new Error(`No pane found for terminal ${terminalId}`);
|
throw new Error(`No pane found for terminal ${terminalName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = await tmux.sendKeys({
|
const output = await tmux.sendKeys({
|
||||||
@@ -390,7 +439,7 @@ server.tool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Keys '${keys}' sent to terminal ${terminalId}${
|
text: `Keys '${keys}' sent to terminal ${terminalName}${
|
||||||
repeat && repeat > 1 ? ` (repeated ${repeat} times)` : ""
|
repeat && repeat > 1 ? ` (repeated ${repeat} times)` : ""
|
||||||
}.\n\n--- Output ---\n${output}`,
|
}.\n\n--- Output ---\n${output}`,
|
||||||
},
|
},
|
||||||
@@ -402,7 +451,7 @@ server.tool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Keys '${keys}' sent to terminal ${terminalId}${
|
text: `Keys '${keys}' sent to terminal ${terminalName}${
|
||||||
repeat && repeat > 1 ? ` (repeated ${repeat} times)` : ""
|
repeat && repeat > 1 ? ` (repeated ${repeat} times)` : ""
|
||||||
}.\n\nUse capture-terminal to verify the result.`,
|
}.\n\nUse capture-terminal to verify the result.`,
|
||||||
},
|
},
|
||||||
@@ -427,17 +476,24 @@ server.tool(
|
|||||||
"rename-terminal",
|
"rename-terminal",
|
||||||
"Rename a terminal to a more descriptive name",
|
"Rename a terminal to a more descriptive name",
|
||||||
{
|
{
|
||||||
terminalId: z.string().describe("ID of the terminal (e.g., '@123')"),
|
terminalName: z.string().describe("Current name of the terminal"),
|
||||||
name: z.string().describe("New name for the terminal"),
|
newName: z.string().describe("New name for the terminal"),
|
||||||
},
|
},
|
||||||
async ({ terminalId, name }) => {
|
async ({ terminalName, newName }) => {
|
||||||
try {
|
try {
|
||||||
await tmux.renameWindow(terminalId, name);
|
await ensureDefaultSession();
|
||||||
|
|
||||||
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tmux.renameWindow(session.id, terminalName, newName);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Terminal ${terminalId} renamed to "${name}"`,
|
text: `Terminal "${terminalName}" renamed to "${newName}"`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -460,16 +516,33 @@ server.tool(
|
|||||||
"kill-terminal",
|
"kill-terminal",
|
||||||
"Close a terminal and end its shell session",
|
"Close a terminal and end its shell session",
|
||||||
{
|
{
|
||||||
terminalId: z.string().describe("ID of the terminal (e.g., '@123')"),
|
terminalName: z.string().describe("Name of the terminal"),
|
||||||
},
|
},
|
||||||
async ({ terminalId }) => {
|
async ({ terminalName }) => {
|
||||||
try {
|
try {
|
||||||
await tmux.killWindow(terminalId);
|
await ensureDefaultSession();
|
||||||
|
|
||||||
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve terminal name to window
|
||||||
|
const window = await tmux.findWindowByName(session.id, terminalName);
|
||||||
|
if (!window) {
|
||||||
|
const windows = await tmux.listWindows(session.id);
|
||||||
|
const availableNames = windows.map((w) => w.name).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Terminal '${terminalName}' not found. Available terminals: ${availableNames}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tmux.killWindow(window.id);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Terminal ${terminalId} has been closed`,
|
text: `Terminal "${terminalName}" has been closed`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -511,7 +584,6 @@ server.resource("Terminals", "tmux://terminals", async () => {
|
|||||||
: "unknown";
|
: "unknown";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: window.id,
|
|
||||||
name: window.name,
|
name: window.name,
|
||||||
active: window.active,
|
active: window.active,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
@@ -543,7 +615,7 @@ server.resource("Terminals", "tmux://terminals", async () => {
|
|||||||
// Expose terminal content as a resource
|
// Expose terminal content as a resource
|
||||||
server.resource(
|
server.resource(
|
||||||
"Terminal Content",
|
"Terminal Content",
|
||||||
new ResourceTemplate("tmux://terminal/{terminalId}", {
|
new ResourceTemplate("tmux://terminal/{terminalName}", {
|
||||||
list: async () => {
|
list: async () => {
|
||||||
try {
|
try {
|
||||||
await ensureDefaultSession();
|
await ensureDefaultSession();
|
||||||
@@ -557,7 +629,7 @@ server.resource(
|
|||||||
|
|
||||||
const terminalResources = windows.map((window) => ({
|
const terminalResources = windows.map((window) => ({
|
||||||
name: `Terminal: ${window.name} ${window.active ? "(active)" : ""}`,
|
name: `Terminal: ${window.name} ${window.active ? "(active)" : ""}`,
|
||||||
uri: `tmux://terminal/${window.id}`,
|
uri: `tmux://terminal/${encodeURIComponent(window.name)}`,
|
||||||
description: `Content from terminal ${window.name}`,
|
description: `Content from terminal ${window.name}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -574,19 +646,35 @@ server.resource(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (uri, { terminalId }) => {
|
async (uri, { terminalName }) => {
|
||||||
try {
|
try {
|
||||||
// Ensure terminalId is a string
|
await ensureDefaultSession();
|
||||||
const terminalIdStr = Array.isArray(terminalId)
|
|
||||||
? terminalId[0]
|
const session = await tmux.findSessionByName(DEFAULT_SESSION);
|
||||||
: terminalId;
|
if (!session) {
|
||||||
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure terminalName is a string
|
||||||
|
const terminalNameStr = Array.isArray(terminalName)
|
||||||
|
? terminalName[0]
|
||||||
|
: terminalName;
|
||||||
|
|
||||||
|
// Decode URI component in case terminal name has special characters
|
||||||
|
const decodedTerminalName = decodeURIComponent(terminalNameStr);
|
||||||
|
|
||||||
|
// Resolve terminal name to window
|
||||||
|
const window = await tmux.findWindowByName(session.id, decodedTerminalName);
|
||||||
|
if (!window) {
|
||||||
|
throw new Error(`Terminal '${decodedTerminalName}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the pane for this terminal
|
// Get the pane for this terminal
|
||||||
const panes = await tmux.listPanes(terminalIdStr);
|
const panes = await tmux.listPanes(window.id);
|
||||||
const pane = panes[0];
|
const pane = panes[0];
|
||||||
|
|
||||||
if (!pane) {
|
if (!pane) {
|
||||||
throw new Error(`No pane found for terminal ${terminalIdStr}`);
|
throw new Error(`No pane found for terminal ${decodedTerminalName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await tmux.capturePaneContent(pane.id, 200, false);
|
const content = await tmux.capturePaneContent(pane.id, 200, false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { exec as execCallback } from "child_process";
|
import { exec as execCallback } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
const exec = promisify(execCallback);
|
const exec = promisify(execCallback);
|
||||||
|
|
||||||
@@ -111,6 +112,32 @@ export async function findSessionByName(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a window by name in a session
|
||||||
|
*/
|
||||||
|
export async function findWindowByName(
|
||||||
|
sessionId: string,
|
||||||
|
name: string
|
||||||
|
): Promise<TmuxWindow | null> {
|
||||||
|
try {
|
||||||
|
const windows = await listWindows(sessionId);
|
||||||
|
return windows.find((window) => window.name === name) || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a window name is unique in a session
|
||||||
|
*/
|
||||||
|
export async function isWindowNameUnique(
|
||||||
|
sessionId: string,
|
||||||
|
name: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const window = await findWindowByName(sessionId, name);
|
||||||
|
return window === null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List windows in a session
|
* List windows in a session
|
||||||
*/
|
*/
|
||||||
@@ -237,6 +264,20 @@ export async function createSession(name: string): Promise<TmuxSession | null> {
|
|||||||
return findSessionByName(name);
|
return findSessionByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand tilde in path to home directory
|
||||||
|
*/
|
||||||
|
function expandTilde(path: string): string {
|
||||||
|
if (path.startsWith('~/')) {
|
||||||
|
const homeDir = process.env.HOME || os.homedir();
|
||||||
|
return path.replace('~', homeDir);
|
||||||
|
}
|
||||||
|
if (path === '~') {
|
||||||
|
return process.env.HOME || os.homedir();
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new window in a session with optional working directory and initial command
|
* Create a new window in a session with optional working directory and initial command
|
||||||
*/
|
*/
|
||||||
@@ -248,10 +289,20 @@ export async function createWindow(
|
|||||||
command?: string;
|
command?: string;
|
||||||
}
|
}
|
||||||
): Promise<(TmuxWindow & { paneId: string; output?: string }) | null> {
|
): Promise<(TmuxWindow & { paneId: string; output?: string }) | null> {
|
||||||
|
// Validate name uniqueness
|
||||||
|
const isUnique = await isWindowNameUnique(sessionId, name);
|
||||||
|
if (!isUnique) {
|
||||||
|
throw new Error(
|
||||||
|
`Terminal with name '${name}' already exists. Please choose a unique name.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Build new-window command with optional working directory
|
// Build new-window command with optional working directory
|
||||||
let newWindowCmd = `new-window -t '${sessionId}' -n '${name}'`;
|
let newWindowCmd = `new-window -t '${sessionId}' -n '${name}'`;
|
||||||
if (options?.workingDirectory) {
|
if (options?.workingDirectory) {
|
||||||
newWindowCmd += ` -c '${options.workingDirectory}'`;
|
// Expand tilde to home directory before passing to tmux
|
||||||
|
const expandedPath = expandTilde(options.workingDirectory);
|
||||||
|
newWindowCmd += ` -c '${expandedPath}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeTmux(newWindowCmd);
|
await executeTmux(newWindowCmd);
|
||||||
@@ -313,13 +364,35 @@ export async function killPane(paneId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename a tmux window by ID
|
* Rename a tmux window by name or ID
|
||||||
*/
|
*/
|
||||||
export async function renameWindow(
|
export async function renameWindow(
|
||||||
windowId: string,
|
sessionId: string,
|
||||||
name: string
|
windowNameOrId: string,
|
||||||
|
newName: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await executeTmux(`rename-window -t '${windowId}' '${name}'`);
|
// Validate new name is unique
|
||||||
|
const isUnique = await isWindowNameUnique(sessionId, newName);
|
||||||
|
if (!isUnique) {
|
||||||
|
throw new Error(
|
||||||
|
`Terminal with name '${newName}' already exists. Please choose a unique name.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if windowNameOrId is a window ID (starts with @) or a name
|
||||||
|
let windowId: string;
|
||||||
|
if (windowNameOrId.startsWith("@")) {
|
||||||
|
windowId = windowNameOrId;
|
||||||
|
} else {
|
||||||
|
// Resolve name to ID
|
||||||
|
const window = await findWindowByName(sessionId, windowNameOrId);
|
||||||
|
if (!window) {
|
||||||
|
throw new Error(`Terminal '${windowNameOrId}' not found.`);
|
||||||
|
}
|
||||||
|
windowId = window.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeTmux(`rename-window -t '${windowId}' '${newName}'`);
|
||||||
// Disable automatic renaming to preserve the manual name
|
// Disable automatic renaming to preserve the manual name
|
||||||
await executeTmux(`set-window-option -t '${windowId}' automatic-rename off`);
|
await executeTmux(`set-window-option -t '${windowId}' automatic-rename off`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user