refactor: remove terminal MCP, Playwright, and commands infrastructure

- Remove terminal/tmux MCP from voice LLM - it now only uses agent-control MCP
- Remove Playwright MCP auto-injection from Claude and Codex agents
- Remove commands state from session, store, and UI
- Remove present_artifact tool and guidelines from voice prompt
- Update agent-prompt.md examples to focus on agents instead of commands
- Remove @huggingface/transformers dependency and test scripts
- Add expandTilde utility to utils/path.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Mohamed Boudra
2026-01-09 13:01:26 +07:00
parent 666a0f2fa6
commit 1a2498e9e7
15 changed files with 54 additions and 501 deletions
+5 -205
View File
@@ -1,209 +1,9 @@
import { tool, experimental_createMCPClient, type ToolSet } from "ai";
import { z } from "zod";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { createTerminalMcpServer } from "../terminal-mcp/index.js";
type McpClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
import type { ToolSet } from "ai";
/**
* Singleton MCP clients
* Get all tools for voice LLM
* @param agentTools - Agent control tools from MCP
*/
let terminalMcpClient: McpClient | null = null;
let playwrightMcpClient: McpClient | null = null;
/**
* Get or create Terminal MCP client (singleton)
*/
async function getTerminalMcpClient(): Promise<McpClient> {
if (terminalMcpClient) {
return terminalMcpClient;
}
// Create Terminal MCP server
const server = await createTerminalMcpServer({ sessionName: "__paseo" });
// Create linked transport pair
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// Connect server to its transport
await server.connect(serverTransport);
// Create client connected to the other side
terminalMcpClient = await experimental_createMCPClient({
transport: clientTransport,
});
console.log("Terminal MCP client initialized");
return terminalMcpClient;
}
/**
* Get or create Playwright MCP client (singleton)
*/
async function getPlaywrightMcpClient(): Promise<McpClient> {
if (playwrightMcpClient) {
return playwrightMcpClient;
}
const transport = new StdioClientTransport({
command: "npx",
args: ["@playwright/mcp", "--image-responses", "omit"],
});
playwrightMcpClient = await experimental_createMCPClient({
transport,
});
console.log("Playwright MCP client initialized");
return playwrightMcpClient;
}
/**
* Cache for merged MCP tools
*/
let mcpToolsCache: ToolSet | null = null;
let mcpToolsPromise: Promise<ToolSet> | null = null;
async function getMcpTools(): Promise<ToolSet> {
if (mcpToolsCache) {
return mcpToolsCache;
}
if (mcpToolsPromise) {
return mcpToolsPromise;
}
mcpToolsPromise = (async () => {
const [terminalClient, playwrightClient] = await Promise.all([
getTerminalMcpClient(),
getPlaywrightMcpClient().catch((error) => {
console.error("Failed to initialize Playwright MCP:", error);
return null;
}),
]);
const terminalTools = (await terminalClient.tools()) as ToolSet;
const playwrightTools: ToolSet = playwrightClient
? ((await playwrightClient.tools()) as ToolSet)
: {};
const mergedTools: ToolSet = {
...terminalTools,
...playwrightTools,
};
mcpToolsCache = mergedTools;
console.log(`Loaded ${Object.keys(mergedTools).length} MCP tools`);
return mergedTools;
})();
return mcpToolsPromise;
}
/**
* Manual tools that aren't MCP-based
*/
const manualTools: ToolSet = {
present_artifact: tool({
description:
"Present an artifact (plan, diff, screenshot, etc.) to the user for review. Use this when you need to show information that's hard to convey via TTS, such as markdown plans, code diffs, or visual content",
inputSchema: z.object({
type: z
.enum(["markdown", "diff", "image", "code"])
.describe("Type of artifact to present."),
title: z
.string()
.describe(
"Title for the artifact (e.g., 'Implementation Plan', 'Refactoring Strategy', '/path/to/project/package.json')."
),
source: z.union([
z.object({
type: z.literal("file"),
path: z.string(),
}),
z.object({
type: z.literal("command_output"),
command: z.string(),
}),
z.object({
type: z.literal("text"),
text: z.string(),
}),
]),
}),
execute: async () => {
// Artifact will be broadcast by orchestrator via onToolCall callback
// We just return a simple acknowledgment here
return {
success: true,
message: "Artifact presented to user.",
};
},
}),
};
/**
* Get all tools (MCP + manual) for LLM
* @param terminalTools - Optional custom terminal tools (per-session). If not provided, uses global singleton.
* @param agentTools - Optional agent tools (per-session).
*/
export async function getAllTools(
terminalTools?: ToolSet,
agentTools?: ToolSet
): Promise<ToolSet> {
if (terminalTools) {
// Use provided terminal tools (per-session) and merge with Playwright and agent tools
const playwrightClient = await getPlaywrightMcpClient().catch((error) => {
console.error("Failed to initialize Playwright MCP:", error);
return null;
});
const playwrightTools: ToolSet = playwrightClient
? ((await playwrightClient.tools()) as ToolSet)
: {};
const combinedTools: ToolSet = {
...terminalTools,
...playwrightTools,
...(agentTools ?? {}),
...manualTools,
};
return combinedTools;
}
// Fallback to global singleton tools
const mcpTools = await getMcpTools();
const combinedMcpTools: ToolSet = {
...mcpTools,
...(agentTools ?? {}),
...manualTools,
};
return combinedMcpTools;
}
/**
* Cleanup function to close MCP clients
*/
export async function closeMcpClients() {
if (terminalMcpClient) {
await terminalMcpClient.close();
terminalMcpClient = null;
console.log("Terminal MCP client closed");
}
if (playwrightMcpClient) {
await playwrightMcpClient.close();
playwrightMcpClient = null;
console.log("Playwright MCP client closed");
}
mcpToolsCache = null;
mcpToolsPromise = null;
export function getAllTools(agentTools?: ToolSet): ToolSet {
return agentTools ?? {};
}
@@ -1,6 +1,6 @@
import { fetchProviderModelCatalog } from "./model-catalog.js";
import type { AgentProvider } from "./agent-sdk-types.js";
import { expandTilde } from "../terminal-mcp/tmux.js";
import { expandTilde } from "../../utils/path.js";
type ResolveAgentModelOptions = {
provider: AgentProvider;
@@ -28,8 +28,7 @@ Conversation with agents:
Agent selection guidance:
- Codex: methodical and slower; great for deep debugging, tracing code paths, refactoring, complex features, and design discussions.
- Claude: fast; strong at tool use (e.g., Playwright MCP, web search), agentic control, and managing other agents; may jump to conclusions—ask it to verify.
- For debugging with UI/Playwright: Claude can drive Playwright MCP and logging; Codex can audit code and propose fixes.
- Claude: fast; strong at tool use, agentic control, and managing other agents; may jump to conclusions—ask it to verify.
Clarifying ambiguous requests:
- Research first to understand the current state.
@@ -743,11 +743,6 @@ class ClaudeAgentSession implements AgentSession {
url: agentControlUrl,
...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}),
},
playwright: {
type: "stdio",
command: "npx",
args: ["@playwright/mcp", "--headless", "--isolated"],
},
};
if (this.config.mcpServers) {
@@ -2517,12 +2517,6 @@ function buildCodexMcpConfig(
};
}
// Add playwright MCP server (same as Claude provider)
mcpServers["playwright"] = {
command: "npx",
args: ["@playwright/mcp", "--headless", "--isolated"],
};
// Merge MCP servers from extra.codex.mcp_servers (legacy location)
const extraCodex = config.extra?.codex as Record<string, unknown> | undefined;
if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") {
-10
View File
@@ -684,16 +684,6 @@ export const SessionStateMessageSchema = z.object({
type: z.literal("session_state"),
payload: z.object({
agents: z.array(AgentSnapshotPayloadSchema),
commands: z.array(
z.object({
id: z.string(),
name: z.string(),
workingDirectory: z.string(),
currentCommand: z.string(),
isDead: z.boolean(),
exitCode: z.number().nullable(),
})
),
}),
});
+5 -140
View File
@@ -36,8 +36,6 @@ import {
} from "./persistence-hooks.js";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { fetchProviderModelCatalog } from "./agent/model-catalog.js";
import { AgentManager } from "./agent/agent-manager.js";
import type { ManagedAgent } from "./agent/agent-manager.js";
@@ -52,7 +50,6 @@ import type {
AgentPersistenceHandle,
} from "./agent/agent-sdk-types.js";
import { AgentRegistry, type StoredAgentRecord } from "./agent/agent-registry.js";
import { expandTilde } from "./terminal-mcp/tmux.js";
import {
listDirectoryEntries,
readExplorerFile,
@@ -63,13 +60,13 @@ import {
generateAgentTitle,
isTitleGeneratorInitialized,
} from "../services/agent-title-generator.js";
import type { TerminalManager } from "./terminal-mcp/terminal-manager.js";
import {
createWorktree,
detectRepoInfo,
slugify,
validateBranchSlug,
} from "../utils/worktree.js";
import { expandTilde } from "../utils/path.js";
type AgentMcpClientConfig = {
agentMcpUrl: string;
@@ -228,13 +225,6 @@ export class Session {
private readonly sttManager: STTManager;
// Per-session MCP client and tools
private terminalMcpClient: Awaited<
ReturnType<typeof experimental_createMCPClient>
> | null = null;
private terminalTools: ToolSet | null = null;
private terminalManager: TerminalManager | null = null;
private terminalInitPromise: Promise<void> | null = null;
private terminalInitError: Error | null = null;
private agentMcpClient: Awaited<
ReturnType<typeof experimental_createMCPClient>
> | null = null;
@@ -279,23 +269,7 @@ export class Session {
this.ttsManager = new TTSManager(this.conversationId);
this.sttManager = new STTManager(this.conversationId);
// Initialize terminal + agent MCP clients asynchronously, but keep promise handles to avoid orphaned rejections
this.terminalInitPromise = this.initializeTerminalMcp().catch((error) => {
const normalizedError =
error instanceof Error
? error
: new Error(
typeof error === "string"
? error
: "Unknown terminal initialization error"
);
this.terminalInitError = normalizedError;
console.error(
`[Session ${this.clientId}] Terminal MCP init failed:`,
normalizedError
);
});
// Initialize agent MCP client asynchronously
void this.initializeAgentMcp();
this.subscribeToAgentEvents();
@@ -439,50 +413,6 @@ export class Session {
});
}
/**
* Initialize Terminal MCP client for this session
*/
private async initializeTerminalMcp(): Promise<void> {
try {
// Create Terminal Manager directly
const { TerminalManager } = await import(
"./terminal-mcp/terminal-manager.js"
);
this.terminalManager = new TerminalManager(this.conversationId);
await this.terminalManager.initialize();
// Create Terminal MCP server with conversation-specific session
const server = await createTerminalMcpServer({
sessionName: this.conversationId,
});
// Create linked transport pair
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// Connect server to its transport
await server.connect(serverTransport);
// Create client connected to the other side
this.terminalMcpClient = await experimental_createMCPClient({
transport: clientTransport,
});
// Get tools from the client
this.terminalTools = (await this.terminalMcpClient.tools()) as ToolSet;
console.log(
`[Session ${this.clientId}] Terminal MCP initialized with session ${this.conversationId}`
);
} catch (error) {
console.error(
`[Session ${this.clientId}] Failed to initialize Terminal MCP:`,
error
);
throw error;
}
}
/**
* Initialize Agent MCP client for this session
*/
@@ -2299,38 +2229,16 @@ export class Session {
const agents = [...liveAgents, ...persistedAgents];
// Get live commands from terminal manager
let commands: any[] = [];
if (this.terminalInitPromise) {
await this.terminalInitPromise;
}
if (this.terminalInitError) {
console.error(
`[Session ${this.clientId}] Skipping command listing due to terminal init failure:`,
this.terminalInitError
);
} else if (this.terminalManager) {
try {
commands = await this.terminalManager.listCommands();
} catch (error) {
console.error(
`[Session ${this.clientId}] Failed to list commands:`,
error
);
}
}
// Emit session state
this.emit({
type: "session_state",
payload: {
agents,
commands,
},
});
console.log(
`[Session ${this.clientId}] Sent session state: ${agents.length} agents, ${commands.length} commands`
`[Session ${this.clientId}] Sent session state: ${agents.length} agents`
);
} catch (error) {
console.error(
@@ -2714,21 +2622,7 @@ export class Session {
apiKey: process.env.OPENROUTER_API_KEY,
});
// Wait for terminal MCP to initialize if needed
if (!this.terminalTools) {
console.log(
`[Session ${this.clientId}] Waiting for terminal MCP initialization...`
);
// Wait up to 5 seconds for initialization
const startTime = Date.now();
while (!this.terminalTools && Date.now() - startTime < 5000) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (!this.terminalTools) {
throw new Error("Terminal MCP failed to initialize");
}
}
// Wait for agent MCP to initialize if needed
if (!this.agentTools) {
console.log(
`[Session ${this.clientId}] Waiting for agent MCP initialization...`
@@ -2744,7 +2638,7 @@ export class Session {
}
}
const allTools = await getAllTools(this.terminalTools, this.agentTools ?? undefined);
const allTools = getAllTools(this.agentTools ?? undefined);
const result = await streamText({
model: openrouter("anthropic/claude-haiku-4.5"),
@@ -3239,36 +3133,7 @@ export class Session {
this.ttsManager.cleanup();
this.sttManager.cleanup();
// Kill tmux session for this conversation
try {
console.log(
`[Session ${this.clientId}] Killing tmux session ${this.conversationId}`
);
await execAsync(`tmux kill-session -t ${this.conversationId}`);
console.log(
`[Session ${this.clientId}] Tmux session ${this.conversationId} killed`
);
} catch (error) {
// Session might not exist or already be killed - that's okay
console.log(
`[Session ${this.clientId}] Tmux session cleanup (session may not exist):`,
error instanceof Error ? error.message : String(error)
);
}
// Close MCP clients
if (this.terminalMcpClient) {
try {
await this.terminalMcpClient.close();
console.log(`[Session ${this.clientId}] Terminal MCP client closed`);
} catch (error) {
console.error(
`[Session ${this.clientId}] Failed to close Terminal MCP client:`,
error
);
}
}
if (this.agentMcpClient) {
try {
await this.agentMcpClient.close();
+15
View File
@@ -0,0 +1,15 @@
import os from "os";
/**
* Expand tilde in path to home directory
*/
export 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;
}