mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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
6
package-lock.json
generated
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
1
packages/server/.gitignore
vendored
1
packages/server/.gitignore
vendored
@@ -5,3 +5,4 @@ dist
|
||||
*.log
|
||||
.debug.conversations
|
||||
conversations
|
||||
.cache
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
packages/server/src/utils/path.ts
Normal file
15
packages/server/src/utils/path.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user