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

View File

@@ -1,7 +1,7 @@
---
id: 7ddb69c7
title: Remove auto-injected MCPs (Playwright, etc) - make user-configured
status: open
status: done
deps: []
created: 2026-01-08T16:17:08.670Z
---

6
package-lock.json generated
View File

@@ -1701,9 +1701,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
"integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,

View File

@@ -4,14 +4,6 @@ import type { Agent } from "@/contexts/session-context";
export interface ActiveProcessesProps {
agents: Agent[];
commands: Array<{
id: string;
name: string;
workingDirectory: string;
currentCommand: string;
isDead: boolean;
exitCode: number | null;
}>;
viewMode: "orchestrator" | "agent";
activeAgentId: string | null;
onSelectAgent: (serverId: string, id: string) => void;
@@ -132,7 +124,6 @@ const styles = StyleSheet.create((theme) => ({
export function ActiveProcesses({
agents,
commands,
viewMode,
activeAgentId,
onSelectAgent,

View File

@@ -34,7 +34,6 @@ export type {
DraftInput,
ProviderModelState,
Agent,
Command,
ExplorerEntry,
ExplorerFile,
ExplorerEntryKind,
@@ -75,14 +74,6 @@ const SESSION_SNAPSHOT_STORAGE_PREFIX = "@paseo:session-snapshot:";
type PersistedSessionSnapshot = {
agents: AgentSnapshotPayload[];
commands: Array<{
id: string;
name: string;
workingDirectory: string;
currentCommand: string;
isDead: boolean;
exitCode: number | null;
}>;
savedAt: string;
};
@@ -97,7 +88,7 @@ async function loadPersistedSessionSnapshot(serverId: string): Promise<Persisted
return null;
}
const parsed = JSON.parse(raw) as PersistedSessionSnapshot;
if (!Array.isArray(parsed?.agents) || !Array.isArray(parsed?.commands)) {
if (!Array.isArray(parsed?.agents)) {
return null;
}
return parsed;
@@ -107,11 +98,10 @@ async function loadPersistedSessionSnapshot(serverId: string): Promise<Persisted
}
}
async function persistSessionSnapshot(serverId: string, snapshot: { agents: AgentSnapshotPayload[]; commands: any[] }) {
async function persistSessionSnapshot(serverId: string, snapshot: { agents: AgentSnapshotPayload[] }) {
try {
const payload: PersistedSessionSnapshot = {
agents: snapshot.agents,
commands: snapshot.commands,
savedAt: new Date().toISOString(),
};
await AsyncStorage.setItem(getSessionSnapshotStorageKey(serverId), JSON.stringify(payload));
@@ -241,7 +231,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents);
const setAgents = useSessionStore((state) => state.setAgents);
const setAgentLastActivity = useSessionStore((state) => state.setAgentLastActivity);
const setCommands = useSessionStore((state) => state.setCommands);
const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions);
const setGitDiffs = useSessionStore((state) => state.setGitDiffs);
const setFileExplorer = useSessionStore((state) => state.setFileExplorer);
@@ -368,13 +357,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
}
setPendingPermissions(serverId, pendingPermissions);
const commandEntries = snapshot.commands ?? [];
setCommands(serverId, (prev) => {
if (prev.size > 0) {
return prev;
}
return new Map(commandEntries.map((command) => [command.id, command]));
});
setHasHydratedAgents(serverId, true);
};
@@ -383,7 +365,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return () => {
isMounted = false;
};
}, [serverId, setAgents, setCommands, setPendingPermissions, setHasHydratedAgents]);
}, [serverId, setAgents, setPendingPermissions, setHasHydratedAgents]);
const updateExplorerState = useCallback(
(agentId: string, updater: (state: any) => any) => {
@@ -632,9 +614,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
sessionStateTimeoutRef.current = null;
}
const { agents: agentsList, commands: commandsList } = message.payload;
const { agents: agentsList } = message.payload;
console.log("[Session] ✅ Received session_state:", agentsList.length, "agents,", commandsList.length, "commands");
console.log("[Session] ✅ Received session_state:", agentsList.length, "agents");
setInitializingAgents(serverId, new Map());
const agents = new Map();
@@ -651,8 +633,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
}
}
const normalizedCommands = commandsList.map((command) => command);
setAgents(serverId, agents);
// Initialize agentLastActivity slice (top-level)
@@ -661,7 +641,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
}
setPendingPermissions(serverId, pendingPermissions);
setCommands(serverId, new Map(normalizedCommands.map((command: any) => [command.id, command])));
setAgentStreamState(serverId, (prev) => {
if (prev.size === 0) {
return prev;
@@ -716,7 +695,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return changed ? next : prev;
});
void persistSessionSnapshot(serverId, { agents: agentsList, commands: normalizedCommands });
void persistSessionSnapshot(serverId, { agents: agentsList });
setHasHydratedAgents(serverId, true);
updateConnectionStatus(serverId, { status: "online", lastOnlineAt: new Date().toISOString(), sessionReady: true });
});
@@ -1271,7 +1250,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
unsubProviderModels();
unsubAgentDeleted();
};
}, [ws, audioPlayer, serverId, setIsPlayingAudio, setMessages, setCurrentAssistantMessage, setAgentStreamState, setAgentStreamingBuffer, clearAgentStreamingBuffer, setInitializingAgents, setAgents, setAgentLastActivity, setCommands, setPendingPermissions, setGitDiffs, setFileExplorer, setProviderModels, setHasHydratedAgents, updateConnectionStatus, getSession, saveDraftInput]);
}, [ws, audioPlayer, serverId, setIsPlayingAudio, setMessages, setCurrentAssistantMessage, setAgentStreamState, setAgentStreamingBuffer, clearAgentStreamingBuffer, setInitializingAgents, setAgents, setAgentLastActivity, setPendingPermissions, setGitDiffs, setFileExplorer, setProviderModels, setHasHydratedAgents, updateConnectionStatus, getSession, saveDraftInput]);
const initializeAgent = useCallback(({ agentId, requestId }: { agentId: string; requestId?: string }) => {
console.log("[Session] initializeAgent called", { agentId, requestId });

View File

@@ -108,15 +108,6 @@ export interface Agent {
parentAgentId?: string | null;
}
export interface Command {
id: string;
name: string;
workingDirectory: string;
currentCommand: string;
isDead: boolean;
exitCode: number | null;
}
export type ExplorerEntryKind = "file" | "directory";
export type ExplorerFileKind = "text" | "image" | "binary";
export type ExplorerEncoding = "utf-8" | "base64" | "none";
@@ -228,9 +219,8 @@ export interface SessionState {
// Initializing agents
initializingAgents: Map<string, boolean>;
// Agents and commands
// Agents
agents: Map<string, Agent>;
commands: Map<string, Command>;
// Permissions
pendingPermissions: Map<string, PendingPermission>;
@@ -301,9 +291,6 @@ interface SessionStoreActions {
// Agent activity timestamps
setAgentLastActivity: (agentId: string, timestamp: Date) => void;
// Commands
setCommands: (serverId: string, commands: Map<string, Command> | ((prev: Map<string, Command>) => Map<string, Command>)) => void;
// Permissions
setPendingPermissions: (serverId: string, perms: Map<string, PendingPermission> | ((prev: Map<string, PendingPermission>) => Map<string, PendingPermission>)) => void;
@@ -379,7 +366,6 @@ function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, aud
agentStreamingBuffer: new Map(),
initializingAgents: new Map(),
agents: new Map(),
commands: new Map(),
pendingPermissions: new Map(),
gitDiffs: new Map(),
fileExplorer: new Map(),
@@ -663,28 +649,6 @@ export const useSessionStore = create<SessionStore>()(
});
},
// Commands
setCommands: (serverId, commands) => {
set((prev) => {
const session = prev.sessions[serverId];
if (!session) {
return prev;
}
const nextCommands = typeof commands === "function" ? commands(session.commands) : commands;
if (session.commands === nextCommands) {
return prev;
}
logSessionStoreUpdate("setCommands", serverId, { count: nextCommands.size });
return {
...prev,
sessions: {
...prev.sessions,
[serverId]: { ...session, commands: nextCommands },
},
};
});
},
// Permissions
setPendingPermissions: (serverId, perms) => {
set((prev) => {

View File

@@ -5,3 +5,4 @@ dist
*.log
.debug.conversations
conversations
.cache

View File

@@ -26,18 +26,18 @@ You are a **voice-controlled** assistant. The user speaks to you via phone and h
**Good example:**
```
User: "List my commands"
You: "You have 3 running. The dev server on port 3000, tests watching for changes, and a Python REPL."
User: "List my agents"
You: "You have two agents. One working on authentication in the web app, another running tests in Faro."
User: "What about finished commands?"
You: "Two finished. The npm install completed successfully and git status exited with code zero."
User: "How's the auth agent doing?"
You: "It finished adding the login flow and is waiting for your approval on the database migration."
```
**Bad example:**
```
User: "List my commands"
You: "You have 5 commands: 1. **dev-server** - Running on port 3000 2. **tests** - Watching for changes..."
User: "List my agents"
You: "You have 2 agents: 1. **auth-agent** - Working on authentication 2. **test-agent** - Running tests..."
```
### Handling STT Errors
@@ -60,7 +60,7 @@ Speech-to-text makes mistakes. Fix them silently using context.
**Examples:**
- User: "Run empty install" → Interpret as "Run npm install"
- User: "Show command to" → If only 2 commands, pick from context; if many, ask which
- User: "Check the agent" → If only one agent, check that one; if multiple, ask which
### Immediate Silence Protocol
@@ -108,51 +108,11 @@ Ask only when the routing decision is truly ambiguous. Otherwise:
After any agent-facing tool call, verbally report the key result in one sentence: who acted, what happened, and whether more work is pending. Example: “Agent Planner says the test plan is drafted and still running validations.” Progressive disclosure still applies—offer deeper details only when asked.
## 4. Special Triggers
### "Show me" → Use present_artifact
When user says **"show me"**, use `present_artifact` to display visual content.
**Keep voice response SHORT. Let the artifact show the data.**
**Prefer command_output or file sources:**
```javascript
// ✅ CORRECT
User: "Show me the git diff"
You: "Here's the diff."
present_artifact({
type: "diff",
source: { type: "command_output", command: "git diff" }
})
// ✅ CORRECT
User: "Show me package.json"
You: "Here's package.json."
present_artifact({
type: "code",
source: { type: "file", path: "/path/to/package.json" }
})
```
**Only use text source for data you already have:**
```javascript
User: "Show me what Planner wrote"
You: [Use the text you already have from agent activity]
You: "Here's Planner's summary."
present_artifact({
type: "markdown",
source: { type: "text", text: plannerSummary }
})
```
## 5. Agent Integrations
## 4. Agent Integrations
### Your Role: Orchestrator
You orchestrate work. Agents execute. Commands run tasks.
You orchestrate work. Agents execute.
**First action when agent work is mentioned: Call `list_agents()`**
@@ -160,7 +120,7 @@ Load the agent list before any agent interaction. Always.
#### Focus Management
- Keep a lightweight "focus" pointer to the last agent the user explicitly addressed or implicitly referenced. Route follow-up utterances there unless the user names another agent or a global command.
- Keep a lightweight "focus" pointer to the last agent the user explicitly addressed or implicitly referenced. Route follow-up utterances there unless the user names another agent.
- Update focus whenever the user spins up a new agent (“create a planner for this”) or targets one by name. Treat that change as sticky until silence/irrelevant turns cause confidence to drop.
- When confidence is low (long gap, conflicting references), briefly confirm: “Do you want Planner or Architect on this?”
- Always narrate hand-offs: “Okay, handing that to Planner.”
@@ -297,7 +257,7 @@ get_agent_activity({ agentId })
send_agent_prompt({ agentId, prompt: "add tests" })
```
## 6. Git & GitHub
## 5. Git & GitHub
### Git Worktree Utilities
@@ -322,7 +282,7 @@ Already authenticated. Use for:
- Managing issues: `gh issue list`
- Checking CI: `gh pr checks`
## 7. Projects & Context
## 6. Projects & Context
### Project Locations
@@ -351,8 +311,8 @@ All projects in `~/dev`:
- Ask: "Kill agent [id]?"
- Wait for "yes"
**Complex coding vs quick commands:**
- Complex or quick → Always delegate to an agent (direct shell commands are disabled)
**Task routing:**
- All coding tasks → Delegate to an agent
- Active agent + related work → Delegate to that agent
- If the user explicitly mentions another agent, switch focus before delegating

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 ?? {};
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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") {

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(),
})
),
}),
});

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();

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;
}