refactor: standardize pending permission keying and user message hydration
This commit is contained in:
@@ -342,19 +342,13 @@ export function AgentStreamView({
|
||||
ListHeaderComponent={
|
||||
pendingPermissionItems.length > 0 ? (
|
||||
<View style={stylesheet.permissionsContainer}>
|
||||
{pendingPermissionItems.map((permission, index) => {
|
||||
const permissionKey = permission.request.id
|
||||
? `${permission.agentId}:${permission.request.id}`
|
||||
: `${permission.agentId}:permission-${permission.request.name ?? permission.request.title ?? index}`;
|
||||
|
||||
return (
|
||||
<PermissionRequestCard
|
||||
key={permissionKey}
|
||||
permission={permission}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pendingPermissionItems.map((permission) => (
|
||||
<PermissionRequestCard
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -26,6 +26,17 @@ import type {
|
||||
import { ScrollView } from "react-native";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
||||
const fallbackId =
|
||||
request.id ||
|
||||
(typeof request.metadata?.id === "string" ? request.metadata.id : undefined) ||
|
||||
request.name ||
|
||||
request.title ||
|
||||
`${request.kind}:${JSON.stringify(request.input ?? request.metadata ?? request.raw ?? {})}`;
|
||||
|
||||
return `${agentId}:${fallbackId}`;
|
||||
};
|
||||
|
||||
export type MessageEntry =
|
||||
| {
|
||||
type: "user";
|
||||
@@ -286,7 +297,8 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
||||
const agent = normalizeAgentSnapshot(snapshot);
|
||||
nextAgents.set(agent.id, agent);
|
||||
for (const request of agent.pendingPermissions) {
|
||||
nextPermissions.set(request.id, { agentId: agent.id, request });
|
||||
const key = derivePendingPermissionKey(agent.id, request);
|
||||
nextPermissions.set(key, { key, agentId: agent.id, request });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +336,8 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
||||
}
|
||||
}
|
||||
for (const request of agent.pendingPermissions) {
|
||||
next.set(request.id, { agentId: agent.id, request });
|
||||
const key = derivePendingPermissionKey(agent.id, request);
|
||||
next.set(key, { key, agentId: agent.id, request });
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -413,7 +426,8 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
||||
|
||||
setPendingPermissions((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(request.id, { agentId, request });
|
||||
const key = derivePendingPermissionKey(agentId, request);
|
||||
next.set(key, { key, agentId, request });
|
||||
return next;
|
||||
});
|
||||
});
|
||||
@@ -427,7 +441,15 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
||||
|
||||
setPendingPermissions((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(requestId);
|
||||
const derivedKey = `${agentId}:${requestId}`;
|
||||
if (!next.delete(derivedKey)) {
|
||||
for (const [key, pending] of next.entries()) {
|
||||
if (pending.agentId === agentId && pending.request.id === requestId) {
|
||||
next.delete(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface SelectedToolCall {
|
||||
* Uses actual ACP types instead of any
|
||||
*/
|
||||
export interface PendingPermission {
|
||||
key: string;
|
||||
agentId: string;
|
||||
request: AgentPermissionRequest;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { hydrateStreamState, type StreamItem, type ToolCallItem } from "./stream";
|
||||
import type { AgentStreamEventPayload } from "@server/server/messages";
|
||||
|
||||
type HarnessUpdate = { event: AgentStreamEventPayload; timestamp: Date };
|
||||
|
||||
const HARNESS_CALL_IDS = {
|
||||
command: "harness-command",
|
||||
edit: "harness-edit",
|
||||
read: "harness-read",
|
||||
};
|
||||
|
||||
const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "user_message",
|
||||
text: "Create a README snippet, show me the diff, and run ls.",
|
||||
messageId: "msg-live-user",
|
||||
},
|
||||
},
|
||||
timestamp: new Date("2025-02-01T10:00:00Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
input: {
|
||||
file_path: "README.md",
|
||||
patch: "*** Begin Patch\n*** Update File: README.md\n@@\n-Old line\n+New line\n*** End Patch",
|
||||
},
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:01Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_json",
|
||||
json: {
|
||||
changes: [
|
||||
{
|
||||
file_path: "README.md",
|
||||
previous_content: "Old line\n",
|
||||
content: "New line\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
output: {
|
||||
changes: [
|
||||
{
|
||||
file_path: "README.md",
|
||||
previous_content: "Old line\n",
|
||||
content: "New line\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:02Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
input: { file_path: "README.md" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:03Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "# README\nNew line\n",
|
||||
},
|
||||
],
|
||||
output: { content: "# README\nNew line\n" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:04Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
kind: "execute",
|
||||
input: { command: "ls" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:05Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_json",
|
||||
json: {
|
||||
result: {
|
||||
command: "ls",
|
||||
output: "README.md\npackages\n",
|
||||
},
|
||||
metadata: { exit_code: 0, cwd: "/tmp/harness" },
|
||||
},
|
||||
},
|
||||
],
|
||||
output: {
|
||||
result: {
|
||||
command: "ls",
|
||||
output: "README.md\npackages\n",
|
||||
},
|
||||
metadata: { exit_code: 0, cwd: "/tmp/harness" },
|
||||
},
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:06Z"),
|
||||
},
|
||||
];
|
||||
|
||||
// Hydration snapshot recorded after refreshing the chat – this is the broken state we need to codify.
|
||||
const STREAM_HARNESS_HYDRATED: HarnessUpdate[] = [
|
||||
{
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "user_message",
|
||||
text: "Create a README snippet, show me the diff, and run ls.",
|
||||
messageId: "msg-live-user",
|
||||
},
|
||||
},
|
||||
timestamp: new Date("2025-02-01T10:05:00Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:01Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:02Z"),
|
||||
},
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
kind: "execute",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:03Z"),
|
||||
},
|
||||
];
|
||||
|
||||
describe("stream harness captures hydrated regression", () => {
|
||||
it("records tool payloads during the live run", () => {
|
||||
const liveState = hydrateStreamState(STREAM_HARNESS_LIVE);
|
||||
const snapshots = extractHarnessSnapshots(liveState);
|
||||
|
||||
expect(snapshots.edit?.payload.data.parsedEdits?.[0]?.diffLines.length).toBeGreaterThan(0);
|
||||
expect(snapshots.read?.payload.data.parsedReads?.[0]?.content).toContain("New line");
|
||||
expect(snapshots.command?.payload.data.parsedCommand?.output).toContain("README.md");
|
||||
});
|
||||
|
||||
it("should hydrate tool payloads after a refresh", () => {
|
||||
const hydratedState = hydrateStreamState(STREAM_HARNESS_HYDRATED);
|
||||
const snapshots = extractHarnessSnapshots(hydratedState);
|
||||
|
||||
expect(snapshots.edit?.payload.data.parsedEdits?.[0]?.diffLines.length).toBeGreaterThan(0);
|
||||
expect(snapshots.read?.payload.data.parsedReads?.[0]?.content).toContain("New line");
|
||||
expect(snapshots.command?.payload.data.parsedCommand?.output).toContain("README.md");
|
||||
});
|
||||
});
|
||||
|
||||
function buildToolStartEvent({
|
||||
callId,
|
||||
server,
|
||||
tool,
|
||||
input,
|
||||
kind,
|
||||
status = "pending",
|
||||
}: {
|
||||
callId: string;
|
||||
server: string;
|
||||
tool: string;
|
||||
input?: Record<string, unknown>;
|
||||
kind?: string;
|
||||
status?: string;
|
||||
}): AgentStreamEventPayload {
|
||||
return {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server,
|
||||
tool,
|
||||
status,
|
||||
callId,
|
||||
displayName: tool,
|
||||
kind,
|
||||
raw: input ? { type: "mcp_tool_use", id: callId, server, name: tool, input } : undefined,
|
||||
input,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildToolResultEvent({
|
||||
callId,
|
||||
server,
|
||||
tool,
|
||||
rawContent,
|
||||
output,
|
||||
}: {
|
||||
callId: string;
|
||||
server: string;
|
||||
tool: string;
|
||||
rawContent: Array<Record<string, unknown>>;
|
||||
output?: Record<string, unknown>;
|
||||
}): AgentStreamEventPayload {
|
||||
return {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server,
|
||||
tool,
|
||||
callId,
|
||||
displayName: tool,
|
||||
raw: {
|
||||
type: "mcp_tool_result",
|
||||
tool_use_id: callId,
|
||||
server,
|
||||
tool_name: tool,
|
||||
content: rawContent,
|
||||
},
|
||||
output,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractHarnessSnapshots(state: StreamItem[]): Record<keyof typeof HARNESS_CALL_IDS, ToolCallItem | undefined> {
|
||||
const lookup = Object.values(HARNESS_CALL_IDS).reduce<Record<string, ToolCallItem | undefined>>(
|
||||
(acc, id) => {
|
||||
acc[id] = findToolByCallId(state, id);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
command: lookup[HARNESS_CALL_IDS.command],
|
||||
edit: lookup[HARNESS_CALL_IDS.edit],
|
||||
read: lookup[HARNESS_CALL_IDS.read],
|
||||
};
|
||||
}
|
||||
|
||||
function findToolByCallId(state: StreamItem[], callId: string): ToolCallItem | undefined {
|
||||
return state.find(
|
||||
(item): item is ToolCallItem =>
|
||||
item.kind === "tool_call" &&
|
||||
item.payload.source === "agent" &&
|
||||
item.payload.data.callId === callId
|
||||
);
|
||||
}
|
||||
+42
-220
@@ -72,228 +72,41 @@ If user says any of these, **STOP ALL OUTPUT IMMEDIATELY**:
|
||||
|
||||
**Response: Complete silence. No acknowledgment. Wait for user to address you again.**
|
||||
|
||||
## 2. Tool Execution Pattern
|
||||
## 2. Delegation Pattern
|
||||
|
||||
### Core Rule: Always Call the Actual Tool
|
||||
### Core Rule: Always Work Through Coding Agents
|
||||
|
||||
**NEVER just describe what you would do. ALWAYS call the tool function.**
|
||||
Direct command-line tools (`execute_command`, `send_text_to_command`, `kill_command`, etc.) are disabled. Every change to files, git, builds, or tests must go through a coding agent. Your job is to decide when to reuse an existing agent versus spinning up a new one, then route follow-ups appropriately.
|
||||
|
||||
### Safe Operations (Execute Immediately)
|
||||
|
||||
These only READ information. Execute without asking:
|
||||
These orchestration tools only read or summarize state:
|
||||
|
||||
- `list_commands()` - List all commands (running and finished)
|
||||
- `capture_command()` - Read command output
|
||||
- Checking git status, viewing files, reading logs
|
||||
- `list_agents()` – discover who exists before delegating
|
||||
- `get_agent_activity()` – pull the curated activity/readout for a specific agent
|
||||
- `wait_for_agent()` – block until an agent requests permission or completes the current run
|
||||
|
||||
**Pattern:**
|
||||
Call them without asking when context requires it. Never fabricate their output.
|
||||
|
||||
```
|
||||
User: "List my commands"
|
||||
You: [CALL list_commands() - don't just say you will]
|
||||
You: "You have dev server running and tests passed 10 minutes ago."
|
||||
```
|
||||
### Delegated Operations (Announce + Execute)
|
||||
|
||||
### Destructive Operations (Announce + Execute)
|
||||
- `create_coding_agent()` – Ask for confirmation unless the user already issued a clear imperative (“spin up a new planner”), then acknowledge and create immediately.
|
||||
- `send_agent_prompt()` – Route the user’s request to the focused agent. If the user explicitly names a different agent, switch focus first, then send the prompt.
|
||||
- `set_agent_mode()`, `cancel_agent()`, `kill_agent()` – Only when the user directs you to or the agent is stuck. Confirm destructive actions.
|
||||
|
||||
These modify state. For clear requests: announce briefly, execute, report.
|
||||
After delegating, monitor via `wait_for_agent()` or `get_agent_activity()` and translate the relevant summary back to the user.
|
||||
|
||||
- `execute_command()` - Runs a command
|
||||
- `send_text_to_command()` - Sends input to running process
|
||||
- `kill_command()` - Terminates a process
|
||||
### When to Ask vs Act
|
||||
|
||||
**Pattern:**
|
||||
Ask only when the routing decision is truly ambiguous. Otherwise:
|
||||
|
||||
```
|
||||
User: "Run the tests"
|
||||
You: "Running npm test."
|
||||
[CALL execute_command()]
|
||||
You: "All 47 tests passed."
|
||||
```
|
||||
|
||||
**After user says "yes" to your announcement:**
|
||||
Don't repeat yourself. Just execute and report results.
|
||||
|
||||
```
|
||||
User: "Install dependencies"
|
||||
You: "Running npm install."
|
||||
User: "Yes"
|
||||
You: [Execute immediately]
|
||||
You: "Installed 243 packages in 12 seconds."
|
||||
```
|
||||
|
||||
### When to Ask vs Execute
|
||||
|
||||
**Only ask when truly ambiguous:**
|
||||
|
||||
- Multiple projects exist and user didn't specify directory
|
||||
- Command has genuinely ambiguous parameters
|
||||
- Unclear which running command to interact with
|
||||
|
||||
**Use context to avoid asking:**
|
||||
|
||||
- If user just ran a command, that's "the command" they're referring to
|
||||
- If project name has STT error → fix silently
|
||||
- Default to most recent or relevant command when clear from context
|
||||
- Default to the most recently addressed agent.
|
||||
- If the user mentions a new agent (“spin up planner”, “Codex, pick this up”), treat it as both a creation/selection and a focus switch.
|
||||
- Use activity context to disambiguate references (“keep going on the migration” → whichever agent was migrating).
|
||||
|
||||
### Tool Results Reporting
|
||||
|
||||
**After ANY tool execution, verbally report the key result.**
|
||||
|
||||
Keep it conversational and brief (1-2 sentences max). Use progressive disclosure - user will ask for details if needed.
|
||||
|
||||
```
|
||||
User: "Run the tests"
|
||||
You: "Running npm test."
|
||||
[Execute]
|
||||
You: "47 tests passed."
|
||||
|
||||
User: "How long?"
|
||||
You: "About 8 seconds."
|
||||
```
|
||||
|
||||
**Why this is critical:**
|
||||
|
||||
- Voice users can't see command output - they depend on your summary
|
||||
- User may be on phone away from laptop - verbal feedback is essential
|
||||
- Never leave the user hanging
|
||||
|
||||
## 3. Command Execution System
|
||||
|
||||
### Core Concept
|
||||
|
||||
Every command you run creates a **command ID** (like `@123`) that you can reference later. Commands stay available for inspection even after they finish.
|
||||
|
||||
### Available Tools
|
||||
|
||||
**Primary operations:**
|
||||
|
||||
- `execute_command(command, directory, maxWait?)` - Run a shell command
|
||||
- `list_commands()` - List all commands (running and finished)
|
||||
- `capture_command(commandId, lines?)` - Get command output
|
||||
- `send_text_to_command(commandId, text, pressEnter?, return_output?)` - Send input to running command
|
||||
- `send_keys_to_command(commandId, keys, repeat?, return_output?)` - Send special keys (Ctrl-C, arrows, etc.)
|
||||
- `kill_command(commandId)` - Terminate command and cleanup
|
||||
|
||||
### Command Execution
|
||||
|
||||
**All commands use bash -c wrapper automatically**, so you can use:
|
||||
- Pipes: `ls | grep foo`
|
||||
- Operators: `cd src && npm test`
|
||||
- Redirects: `echo "test" > file.txt`
|
||||
- Any bash syntax: semicolons, subshells, variables, etc.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```javascript
|
||||
// Simple command
|
||||
execute_command(
|
||||
command="npm test",
|
||||
directory="~/dev/voice-dev"
|
||||
)
|
||||
→ Returns: { commandId: "@123", output: "...", exitCode: 0, isDead: true }
|
||||
|
||||
// Complex bash command
|
||||
execute_command(
|
||||
command="cd packages/web && npm run build && echo 'Done'",
|
||||
directory="~/dev/voice-dev"
|
||||
)
|
||||
|
||||
// Interactive command (REPL, server, etc.)
|
||||
execute_command(
|
||||
command="python3",
|
||||
directory="~/dev"
|
||||
)
|
||||
→ Returns: { commandId: "@124", output: ">>>", exitCode: null, isDead: false }
|
||||
```
|
||||
|
||||
### Understanding Command States
|
||||
|
||||
**isDead: false** - Command is still running
|
||||
- Interactive processes (REPL, dev server, watching tests)
|
||||
- Long-running operations
|
||||
- Can send input via `send_text_to_command` or `send_keys_to_command`
|
||||
|
||||
**isDead: true** - Command has finished
|
||||
- One-shot commands that completed (ls, git status, npm test)
|
||||
- Has an exit code (0 = success, non-zero = error)
|
||||
- Output is fully captured and available
|
||||
- Can still read output via `capture_command`
|
||||
- Will appear in `list_commands` until you `kill_command` it
|
||||
|
||||
### Interactive Command Pattern
|
||||
|
||||
For REPLs, servers, or any interactive process:
|
||||
|
||||
```javascript
|
||||
// 1. Start interactive command
|
||||
execute_command("python3", "~/dev", maxWait=5000)
|
||||
// Wait for stability (>>> prompt appears)
|
||||
// Returns: { commandId: "@125", output: "Python 3.11...\n>>>", isDead: false }
|
||||
|
||||
// 2. Send input
|
||||
send_text_to_command("@125", "print('hello')", pressEnter=true, return_output={maxWait: 2000})
|
||||
// Returns: { output: "hello\n>>>" }
|
||||
|
||||
// 3. More input
|
||||
send_text_to_command("@125", "x = 5 + 3", pressEnter=true, return_output={maxWait: 2000})
|
||||
|
||||
// 4. Exit
|
||||
send_text_to_command("@125", "exit()", pressEnter=true)
|
||||
// Command exits, isDead becomes true, exit code captured
|
||||
|
||||
// 5. Cleanup
|
||||
kill_command("@125")
|
||||
```
|
||||
|
||||
### Command Lifecycle
|
||||
|
||||
1. **Execute**: `execute_command()` creates window, runs command, waits for completion or stability
|
||||
2. **Running**: If interactive, `isDead=false`, can send input
|
||||
3. **Finished**: If command exits, `isDead=true`, exit code available
|
||||
4. **Inspectable**: Finished commands remain visible in `list_commands`
|
||||
5. **Cleanup**: Use `kill_command()` to remove from list
|
||||
|
||||
### Special Keys
|
||||
|
||||
Use `send_keys_to_command` for control sequences:
|
||||
|
||||
- `C-c` - Ctrl+C (interrupt/cancel)
|
||||
- `C-d` - Ctrl+D (EOF)
|
||||
- `BTab` - Shift+Tab
|
||||
- `Enter`, `Escape`, `Tab`, `Space`
|
||||
- `Up`, `Down`, `Left`, `Right`
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// Interrupt running command
|
||||
send_keys_to_command("@126", "C-c")
|
||||
|
||||
// Navigate in TUI
|
||||
send_keys_to_command("@127", "Down", repeat=3)
|
||||
```
|
||||
|
||||
### maxWait Parameter
|
||||
|
||||
Controls how long to wait for command completion or output stability:
|
||||
|
||||
- **One-shot commands**: Tool returns when command exits (even if quick)
|
||||
- **Interactive commands**: Tool returns when output stabilizes for 1 second
|
||||
- **Default**: 120000ms (2 minutes)
|
||||
|
||||
**Usage:**
|
||||
|
||||
```javascript
|
||||
// Quick command
|
||||
execute_command("ls", "~/dev", maxWait=5000)
|
||||
|
||||
// Slow build
|
||||
execute_command("npm run build", "~/project", maxWait=300000)
|
||||
|
||||
// Interactive (returns when prompt appears)
|
||||
execute_command("python3", "~/dev", maxWait=10000)
|
||||
```
|
||||
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
|
||||
|
||||
@@ -326,12 +139,12 @@ present_artifact({
|
||||
**Only use text source for data you already have:**
|
||||
|
||||
```javascript
|
||||
User: "Show me that command output"
|
||||
You: [Capture command via capture_command]
|
||||
You: "Here's the output."
|
||||
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: capturedOutput }
|
||||
source: { type: "text", text: plannerSummary }
|
||||
})
|
||||
```
|
||||
|
||||
@@ -345,14 +158,22 @@ You orchestrate work. Agents execute. Commands run tasks.
|
||||
|
||||
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.
|
||||
- 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.”
|
||||
- Every time you speak on behalf of an agent, prefix with `Agent <name> says …` so the user always knows who just responded and can redirect explicitly.
|
||||
|
||||
**Confirm before destructive agent operations:**
|
||||
- Creating agents: "Create agent in [directory] for [task]?"
|
||||
- Creating agents: "Create agent in [directory] for [task]?" (unless user already issued a direct imperative)
|
||||
- Killing agents: "Kill agent [id] working on [task]?"
|
||||
|
||||
**Delegate vs execute:**
|
||||
- Complex coding → Agent with initialPrompt + mode
|
||||
- Quick commands → Execute directly
|
||||
- Active agent context → Send prompt to that agent
|
||||
- Everything touching code, git, or shell runs through agents
|
||||
- Keep lightweight questions or summaries in-orchestrator when no action is needed
|
||||
- Active agent context → Send prompt to that agent (respect focus)
|
||||
|
||||
### Available Agents (Source of Truth)
|
||||
|
||||
@@ -370,7 +191,7 @@ We only have two coding agents. Do not call tools to discover them—treat this
|
||||
|
||||
### Creating Agents
|
||||
|
||||
**Creation requires confirmation. Always ask first.**
|
||||
**Confirm creation only when intent is unclear.** If the user gives a direct imperative (“spin up a new planner agent in voice-dev”), acknowledge and create immediately; otherwise, ask.
|
||||
|
||||
```javascript
|
||||
// Claude Code with planning
|
||||
@@ -523,26 +344,27 @@ All projects in `~/dev`:
|
||||
**Agent work mentioned?**
|
||||
1. Call `list_agents()` first
|
||||
2. Reuse existing agent if task relates to its work
|
||||
3. Confirm before creating new agent
|
||||
3. Only confirm new-agent creation when the request is ambiguous. Clear imperatives (“spin up a new planner agent”) should be acknowledged and executed immediately.
|
||||
|
||||
**Creating/killing agents?**
|
||||
- Ask: "Create agent in [dir] for [task]?"
|
||||
- Ask: "Create agent in [dir] for [task]?" when intent isn’t explicit
|
||||
- Ask: "Kill agent [id]?"
|
||||
- Wait for "yes"
|
||||
|
||||
**Complex coding vs quick commands:**
|
||||
- Complex → Agent with initialPrompt + mode
|
||||
- Quick command → Execute directly
|
||||
- Complex or quick → Always delegate to an agent (direct shell commands are disabled)
|
||||
- Active agent + related work → Delegate to that agent
|
||||
- If the user explicitly mentions another agent, switch focus before delegating
|
||||
|
||||
**Context tracking:**
|
||||
- Track active agents and their directories
|
||||
- Use conversation context to resolve ambiguity
|
||||
- Fix STT errors silently
|
||||
- Maintain a recency-based focus pointer and narrate any focus change out loud
|
||||
|
||||
### Core Reminders
|
||||
|
||||
- Call actual tools, never just describe
|
||||
- 1-3 sentences max per response
|
||||
- Always report command results verbally
|
||||
- Always report agent/tool results verbally (preface with "Agent X says …" when relaying)
|
||||
- Default to action when context is clear
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "node:path";
|
||||
import { ClaudeAgentClient, convertClaudeHistoryEntry } from "./claude-agent.js";
|
||||
import { hydrateStreamState, type StreamItem, type ToolCallItem, type AgentToolCallData } from "../../../../../app/src/types/stream.js";
|
||||
import type { AgentStreamEventPayload } from "../../messages.js";
|
||||
import type { AgentSessionConfig, AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
|
||||
import type { AgentProvider, AgentSessionConfig, AgentStreamEvent, AgentTimelineItem } from "../agent-sdk-types.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "claude-agent-e2e-"));
|
||||
@@ -376,13 +376,117 @@ describe("ClaudeAgentClient (SDK integration)", () => {
|
||||
hydratedSnapshots.map((entry) => [entry.key, entry.data])
|
||||
);
|
||||
|
||||
assertHydratedReplica(commandTool!, hydratedMap, (data) => rawContainsText(data.raw, cwd));
|
||||
assertHydratedReplica(editTool!, hydratedMap, (data) =>
|
||||
rawContainsText(data.raw, "hydrate-proof.txt")
|
||||
assertHydratedReplica(
|
||||
commandTool!,
|
||||
hydratedMap,
|
||||
(data) => (data.parsedCommand?.output ?? "").includes(cwd),
|
||||
({ live, hydrated }) => {
|
||||
expect((live.parsedCommand?.command ?? "").toLowerCase()).toContain("pwd");
|
||||
expect(live.parsedCommand?.output ?? "").toContain(cwd);
|
||||
expect((hydrated.parsedCommand?.command ?? "").toLowerCase()).toContain("pwd");
|
||||
expect(hydrated.parsedCommand?.output ?? "").toContain(cwd);
|
||||
}
|
||||
);
|
||||
assertHydratedReplica(readTool!, hydratedMap, (data) =>
|
||||
rawContainsText(data.raw, "HYDRATION_PROOF_LINE_TWO")
|
||||
assertHydratedReplica(
|
||||
editTool!,
|
||||
hydratedMap,
|
||||
(data) =>
|
||||
Array.isArray(data.parsedEdits) &&
|
||||
data.parsedEdits.some(
|
||||
(entry) =>
|
||||
(entry.filePath ?? "").includes("hydrate-proof.txt") &&
|
||||
entry.diffLines.some((line) => line.content.includes("HYDRATION_PROOF_LINE_TWO"))
|
||||
),
|
||||
({ live, hydrated }) => {
|
||||
const liveDiff = JSON.stringify(live.parsedEdits ?? []);
|
||||
const hydratedDiff = JSON.stringify(hydrated.parsedEdits ?? []);
|
||||
expect(liveDiff).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||
expect(liveDiff).toContain("HYDRATION_PROOF_LINE_TWO");
|
||||
expect(hydratedDiff).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||
expect(hydratedDiff).toContain("HYDRATION_PROOF_LINE_TWO");
|
||||
}
|
||||
);
|
||||
assertHydratedReplica(
|
||||
readTool!,
|
||||
hydratedMap,
|
||||
(data) =>
|
||||
Array.isArray(data.parsedReads) &&
|
||||
data.parsedReads.some(
|
||||
(entry) =>
|
||||
(entry.filePath ?? "").includes("hydrate-proof.txt") &&
|
||||
entry.content.includes("HYDRATION_PROOF_LINE_TWO")
|
||||
),
|
||||
({ live, hydrated }) => {
|
||||
const liveReads = JSON.stringify(live.parsedReads ?? []);
|
||||
const hydratedReads = JSON.stringify(hydrated.parsedReads ?? []);
|
||||
expect(liveReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||
expect(hydratedReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||
expect(liveReads).toContain("HYDRATION_PROOF_LINE_TWO");
|
||||
expect(hydratedReads).toContain("HYDRATION_PROOF_LINE_TWO");
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
cleanupClaudeHistory(cwd);
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000
|
||||
);
|
||||
|
||||
test(
|
||||
"hydrates user messages from persisted history",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const client = new ClaudeAgentClient();
|
||||
const config: AgentSessionConfig = {
|
||||
provider: "claude",
|
||||
cwd,
|
||||
extra: { claude: { maxThinkingTokens: 1024 } },
|
||||
};
|
||||
|
||||
const promptMarker = `HYDRATED_USER_${Date.now().toString(36)}`;
|
||||
const prompt = `Reply with the exact text ${promptMarker} and then stop.`;
|
||||
const liveTimelineUpdates: StreamHydrationUpdate[] = [
|
||||
buildUserMessageUpdate("claude", prompt, "msg-claude-hydrated-user"),
|
||||
];
|
||||
|
||||
try {
|
||||
const session = await client.createSession(config);
|
||||
const events = session.stream(prompt);
|
||||
try {
|
||||
for await (const event of events) {
|
||||
recordTimelineUpdate(liveTimelineUpdates, event);
|
||||
if (event.type === "turn_completed" || event.type === "turn_failed") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
|
||||
const handle = session.describePersistence();
|
||||
expect(handle).toBeTruthy();
|
||||
const sessionId = handle?.sessionId ?? handle?.nativeHandle;
|
||||
expect(typeof sessionId).toBe("string");
|
||||
|
||||
const historyPaths = getClaudeHistoryPaths(cwd, sessionId!);
|
||||
expect(await waitForHistoryFile(historyPaths)).toBe(true);
|
||||
|
||||
const resumed = await client.resumeSession(handle!, { cwd });
|
||||
const hydrationUpdates: StreamHydrationUpdate[] = [];
|
||||
try {
|
||||
for await (const event of resumed.streamHistory()) {
|
||||
recordTimelineUpdate(hydrationUpdates, event);
|
||||
}
|
||||
} finally {
|
||||
await resumed.close();
|
||||
}
|
||||
|
||||
const liveState = hydrateStreamState(liveTimelineUpdates);
|
||||
const hydratedState = hydrateStreamState(hydrationUpdates);
|
||||
|
||||
expect(stateIncludesUserMessage(liveState, promptMarker)).toBe(true);
|
||||
expect(stateIncludesUserMessage(hydratedState, promptMarker)).toBe(true);
|
||||
} finally {
|
||||
cleanupClaudeHistory(cwd);
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
@@ -466,6 +570,31 @@ function recordTimelineUpdate(target: StreamHydrationUpdate[], event: AgentStrea
|
||||
});
|
||||
}
|
||||
|
||||
function buildUserMessageUpdate(
|
||||
provider: AgentProvider,
|
||||
text: string,
|
||||
messageId: string
|
||||
): StreamHydrationUpdate {
|
||||
return {
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider,
|
||||
item: {
|
||||
type: "user_message",
|
||||
text,
|
||||
messageId,
|
||||
},
|
||||
},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
function stateIncludesUserMessage(state: StreamItem[], marker: string): boolean {
|
||||
return state.some(
|
||||
(item) => item.kind === "user_message" && item.text.toLowerCase().includes(marker.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function extractAgentToolSnapshots(state: StreamItem[]): ToolSnapshot[] {
|
||||
return state
|
||||
.filter(
|
||||
@@ -490,7 +619,8 @@ function buildToolSnapshotKey(data: AgentToolCallData, fallbackId: string): stri
|
||||
function assertHydratedReplica(
|
||||
liveSnapshot: ToolSnapshot,
|
||||
hydratedMap: Map<string, AgentToolCallData>,
|
||||
predicate: (data: AgentToolCallData) => boolean
|
||||
predicate: (data: AgentToolCallData) => boolean,
|
||||
extraAssertions?: (ctx: { live: AgentToolCallData; hydrated: AgentToolCallData }) => void
|
||||
) {
|
||||
expect(predicate(liveSnapshot.data)).toBe(true);
|
||||
const hydrated = hydratedMap.get(liveSnapshot.key);
|
||||
@@ -500,6 +630,9 @@ function assertHydratedReplica(
|
||||
expect(hydrated?.tool).toBe(liveSnapshot.data.tool);
|
||||
expect(hydrated?.displayName).toBe(liveSnapshot.data.displayName);
|
||||
expect(predicate(hydrated!)).toBe(true);
|
||||
if (hydrated && extraAssertions) {
|
||||
extraAssertions({ live: liveSnapshot.data, hydrated });
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeClaudeProjectName(cwd: string): string {
|
||||
|
||||
@@ -1118,29 +1118,33 @@ export function convertClaudeHistoryEntry(
|
||||
const content = message.content as string | ClaudeContentChunk[];
|
||||
const normalizedBlocks = normalizeHistoryBlocks(content);
|
||||
const hasToolBlock = normalizedBlocks?.some((block) => hasToolLikeBlock(block)) ?? false;
|
||||
const timeline: AgentTimelineItem[] = [];
|
||||
|
||||
if (entry.type === "user") {
|
||||
const text = extractUserMessageText(content);
|
||||
if (text) {
|
||||
timeline.push({
|
||||
type: "user_message",
|
||||
text,
|
||||
raw: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasToolBlock && normalizedBlocks) {
|
||||
return mapBlocks(Array.isArray(content) ? content : normalizedBlocks);
|
||||
const mapped = mapBlocks(Array.isArray(content) ? content : normalizedBlocks);
|
||||
if (entry.type === "user") {
|
||||
const toolItems = mapped.filter((item) => item.type === "tool_call");
|
||||
return timeline.length ? [...timeline, ...toolItems] : toolItems;
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
if (entry.type === "assistant" && content) {
|
||||
return mapBlocks(content);
|
||||
}
|
||||
|
||||
if (entry.type === "user") {
|
||||
const text = extractUserMessageText(content);
|
||||
if (text) {
|
||||
return [
|
||||
{
|
||||
type: "user_message",
|
||||
text,
|
||||
raw: message,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
return timeline;
|
||||
}
|
||||
|
||||
class Pushable<T> implements AsyncIterable<T> {
|
||||
|
||||
@@ -4,7 +4,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { CodexAgentClient } from "./codex-agent.js";
|
||||
import type { AgentSessionConfig, AgentStreamEvent, AgentPermissionRequest } from "../agent-sdk-types.js";
|
||||
import { hydrateStreamState, type StreamItem } from "../../../../../app/src/types/stream.js";
|
||||
import type { AgentProvider, AgentSessionConfig, AgentStreamEvent, AgentPermissionRequest } from "../agent-sdk-types.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(os.tmpdir(), "codex-agent-e2e-"));
|
||||
@@ -324,4 +325,97 @@ describe("CodexAgentClient (SDK integration)", () => {
|
||||
},
|
||||
30_000
|
||||
);
|
||||
|
||||
test(
|
||||
"hydrates user messages from persisted history",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const restoreSessionDir = useTempCodexSessionDir();
|
||||
const client = new CodexAgentClient();
|
||||
const config: AgentSessionConfig = { provider: "codex", cwd };
|
||||
let session: Awaited<ReturnType<typeof client.createSession>> | null = null;
|
||||
let resumed: Awaited<ReturnType<typeof client.resumeSession>> | null = null;
|
||||
|
||||
const promptMarker = `CODEX_USER_${Date.now().toString(36)}`;
|
||||
const prompt = `Reply only with ${promptMarker} and stop.`;
|
||||
const liveTimelineUpdates: StreamHydrationUpdate[] = [
|
||||
buildUserMessageUpdate("codex", prompt, "msg-codex-hydrated-user"),
|
||||
];
|
||||
|
||||
try {
|
||||
session = await client.createSession(config);
|
||||
const events = session.stream(prompt);
|
||||
for await (const event of events) {
|
||||
recordTimelineUpdate(liveTimelineUpdates, event);
|
||||
if (event.type === "turn_completed" || event.type === "turn_failed") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const handle = session.describePersistence();
|
||||
expect(handle).toBeTruthy();
|
||||
|
||||
await session.close();
|
||||
session = null;
|
||||
|
||||
resumed = await client.resumeSession(handle!);
|
||||
const hydrationUpdates: StreamHydrationUpdate[] = [];
|
||||
for await (const event of resumed.streamHistory()) {
|
||||
recordTimelineUpdate(hydrationUpdates, event);
|
||||
}
|
||||
|
||||
const liveState = hydrateStreamState(liveTimelineUpdates);
|
||||
const hydratedState = hydrateStreamState(hydrationUpdates);
|
||||
|
||||
expect(stateIncludesUserMessage(liveState, promptMarker)).toBe(true);
|
||||
expect(stateIncludesUserMessage(hydratedState, promptMarker)).toBe(true);
|
||||
} finally {
|
||||
await session?.close();
|
||||
await resumed?.close();
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
restoreSessionDir();
|
||||
}
|
||||
},
|
||||
180_000
|
||||
);
|
||||
});
|
||||
|
||||
type StreamHydrationUpdate = {
|
||||
event: Extract<AgentStreamEvent, { type: "timeline" }>;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
function recordTimelineUpdate(target: StreamHydrationUpdate[], event: AgentStreamEvent) {
|
||||
if (event.type !== "timeline") {
|
||||
return;
|
||||
}
|
||||
target.push({
|
||||
event: event as StreamHydrationUpdate["event"],
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
function buildUserMessageUpdate(
|
||||
provider: AgentProvider,
|
||||
text: string,
|
||||
messageId: string
|
||||
): StreamHydrationUpdate {
|
||||
return {
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider,
|
||||
item: {
|
||||
type: "user_message",
|
||||
text,
|
||||
messageId,
|
||||
},
|
||||
},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
function stateIncludesUserMessage(state: StreamItem[], marker: string): boolean {
|
||||
return state.some(
|
||||
(item) => item.kind === "user_message" && item.text.toLowerCase().includes(marker.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -912,12 +912,13 @@ function handleRolloutResponseItem(
|
||||
|
||||
switch (payload.type) {
|
||||
case "message": {
|
||||
if (payload.role !== "assistant") {
|
||||
return;
|
||||
}
|
||||
const text = extractMessageText(payload.content);
|
||||
if (text) {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "assistant_message", text, raw: payload } });
|
||||
if (payload.role === "assistant") {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "assistant_message", text, raw: payload } });
|
||||
} else if (payload.role === "user") {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "user_message", text, raw: payload } });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1203,8 +1204,17 @@ function extractMessageText(content: unknown): string {
|
||||
}
|
||||
const parts: string[] = [];
|
||||
for (const block of content) {
|
||||
if (block && typeof block === "object" && (block as any).type === "output_text" && typeof (block as any).text === "string") {
|
||||
parts.push((block as any).text);
|
||||
if (!block || typeof block !== "object") {
|
||||
continue;
|
||||
}
|
||||
const text = typeof (block as any).text === "string" ? (block as any).text : undefined;
|
||||
if (text && text.trim()) {
|
||||
parts.push(text.trim());
|
||||
continue;
|
||||
}
|
||||
const message = typeof (block as any).message === "string" ? (block as any).message : undefined;
|
||||
if (message && message.trim()) {
|
||||
parts.push(message.trim());
|
||||
}
|
||||
}
|
||||
return parts.join("\n").trim();
|
||||
|
||||
@@ -26,50 +26,50 @@ export async function createTerminalMcpServer(
|
||||
|
||||
// COMMAND-BASED TOOLS
|
||||
|
||||
// Tool: execute_command
|
||||
server.registerTool(
|
||||
"execute_command",
|
||||
{
|
||||
title: "Execute Command",
|
||||
description:
|
||||
"Execute a shell command in a specified directory. Commands are wrapped in bash -c, so you can use pipes, operators, redirects, and all bash features. The command runs until completion or until output stabilizes (for interactive processes). Returns command ID for follow-up interactions, output, exit code (if finished), and whether the process is still running. Windows remain after command exits for inspection.",
|
||||
inputSchema: {
|
||||
command: z
|
||||
.string()
|
||||
.describe(
|
||||
"The command to execute. Can include pipes (|), operators (&&, ||, ;), redirects (>, >>), and any bash syntax. Examples: 'npm test', 'ls | grep foo', 'cd src && npm run build'"
|
||||
),
|
||||
directory: z
|
||||
.string()
|
||||
.describe(
|
||||
"Absolute path to the working directory. Can use ~ for home directory."
|
||||
),
|
||||
maxWait: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Maximum milliseconds to wait for command completion or output stability (default: 120000 = 2 minutes). For interactive commands, returns when output stabilizes. For one-shot commands, returns when command exits."
|
||||
),
|
||||
},
|
||||
outputSchema: {
|
||||
commandId: z.string(),
|
||||
output: z.string(),
|
||||
exitCode: z.number().nullable(),
|
||||
isDead: z.boolean(),
|
||||
},
|
||||
},
|
||||
async ({ command, directory, maxWait }) => {
|
||||
const result = await terminalManager.executeCommand(
|
||||
command,
|
||||
directory,
|
||||
maxWait
|
||||
);
|
||||
return {
|
||||
content: [],
|
||||
structuredContent: result,
|
||||
};
|
||||
}
|
||||
);
|
||||
// Tool: execute_command (disabled – commands must be run through coding agents)
|
||||
// server.registerTool(
|
||||
// "execute_command",
|
||||
// {
|
||||
// title: "Execute Command",
|
||||
// description:
|
||||
// "Execute a shell command in a specified directory. Commands are wrapped in bash -c, so you can use pipes, operators, redirects, and all bash features. The command runs until completion or until output stabilizes (for interactive processes). Returns command ID for follow-up interactions, output, exit code (if finished), and whether the process is still running. Windows remain after command exits for inspection.",
|
||||
// inputSchema: {
|
||||
// command: z
|
||||
// .string()
|
||||
// .describe(
|
||||
// "The command to execute. Can include pipes (|), operators (&&, ||, ;), redirects (>, >>), and any bash syntax. Examples: 'npm test', 'ls | grep foo', 'cd src && npm run build'"
|
||||
// ),
|
||||
// directory: z
|
||||
// .string()
|
||||
// .describe(
|
||||
// "Absolute path to the working directory. Can use ~ for home directory."
|
||||
// ),
|
||||
// maxWait: z
|
||||
// .number()
|
||||
// .optional()
|
||||
// .describe(
|
||||
// "Maximum milliseconds to wait for command completion or output stability (default: 120000 = 2 minutes). For interactive commands, returns when output stabilizes. For one-shot commands, returns when command exits."
|
||||
// ),
|
||||
// },
|
||||
// outputSchema: {
|
||||
// commandId: z.string(),
|
||||
// output: z.string(),
|
||||
// exitCode: z.number().nullable(),
|
||||
// isDead: z.boolean(),
|
||||
// },
|
||||
// },
|
||||
// async ({ command, directory, maxWait }) => {
|
||||
// const result = await terminalManager.executeCommand(
|
||||
// command,
|
||||
// directory,
|
||||
// maxWait
|
||||
// );
|
||||
// return {
|
||||
// content: [],
|
||||
// structuredContent: result,
|
||||
// };
|
||||
// }
|
||||
// );
|
||||
|
||||
// Tool: list_commands
|
||||
server.registerTool(
|
||||
|
||||
Reference in New Issue
Block a user