refactor: remove executeCommand in favor of standard run/stream path

This commit is contained in:
Mohamed Boudra
2026-02-18 10:57:21 +07:00
parent 803e31e61c
commit 64b36d0df7
10 changed files with 120 additions and 330 deletions
@@ -31,7 +31,6 @@ import type {
PaseoWorktreeArchiveResponse, PaseoWorktreeArchiveResponse,
ProjectIconResponse, ProjectIconResponse,
ListCommandsResponse, ListCommandsResponse,
ExecuteCommandResponse,
ListProviderModelsResponseMessage, ListProviderModelsResponseMessage,
ListAvailableProvidersResponse, ListAvailableProvidersResponse,
SpeechModelsListResponse, SpeechModelsListResponse,
@@ -208,7 +207,6 @@ type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
type SpeechModelsListPayload = SpeechModelsListResponse["payload"]; type SpeechModelsListPayload = SpeechModelsListResponse["payload"];
type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"]; type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"];
type ListCommandsPayload = ListCommandsResponse["payload"]; type ListCommandsPayload = ListCommandsResponse["payload"];
type ExecuteCommandPayload = ExecuteCommandResponse["payload"];
type ListCommandsDraftConfig = Pick< type ListCommandsDraftConfig = Pick<
AgentSessionConfig, AgentSessionConfig,
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" "provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
@@ -2209,25 +2207,6 @@ export class DaemonClient {
}); });
} }
async executeCommand(
agentId: string,
commandName: string,
args?: string,
requestId?: string
): Promise<ExecuteCommandPayload> {
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "execute_command_request",
agentId,
commandName,
args,
},
responseType: "execute_command_response",
timeout: 30000,
});
}
// ============================================================================ // ============================================================================
// Permissions // Permissions
// ============================================================================ // ============================================================================
@@ -281,15 +281,6 @@ export type AgentSlashCommand = {
argumentHint: string; argumentHint: string;
}; };
/**
* Result from executing a slash command.
*/
export type AgentCommandResult = {
text: string;
timeline: AgentTimelineItem[];
usage?: AgentUsage;
};
export type ListPersistedAgentsOptions = { export type ListPersistedAgentsOptions = {
limit?: number; limit?: number;
}; };
@@ -353,13 +344,6 @@ export interface AgentSession {
* Commands are provider-specific - Claude supports skills and built-in commands. * Commands are provider-specific - Claude supports skills and built-in commands.
*/ */
listCommands?(): Promise<AgentSlashCommand[]>; listCommands?(): Promise<AgentSlashCommand[]>;
/**
* Execute a slash command by name.
* The command name should NOT include the leading "/" - it will be added automatically.
* @param commandName The command name (e.g., "help", "context")
* @param args Optional arguments to pass to the command
*/
executeCommand?(commandName: string, args?: string): Promise<AgentCommandResult>;
/** /**
* Update the model used for subsequent turns (if supported by provider). * Update the model used for subsequent turns (if supported by provider).
*/ */
@@ -3,7 +3,6 @@
* *
* Tests the ability to: * Tests the ability to:
* 1. List available slash commands from a ClaudeAgentSession * 1. List available slash commands from a ClaudeAgentSession
* 2. Execute slash commands through ClaudeAgentSession
* *
* These tests verify that the agent abstraction layer properly exposes * These tests verify that the agent abstraction layer properly exposes
* the Claude Agent SDK's command capabilities. * the Claude Agent SDK's command capabilities.
@@ -91,77 +90,4 @@ const hasClaudeCredentials =
}, 30000); }, 30000);
}); });
describe("executeCommand()", () => {
it("should execute a command and return a result", async () => {
session = await client.createSession(testConfig);
// The session should have an executeCommand method
expect(typeof session.executeCommand).toBe("function");
// First get available commands to use one that exists
const commands = await session.listCommands!();
expect(commands.length).toBeGreaterThan(0);
// Execute one of the available commands
const firstCommand = commands[0];
const result = await session.executeCommand!(firstCommand.name);
// Should return a result object
expect(result).toBeDefined();
expect(typeof result.text).toBe("string");
// Note: Some commands may return empty text (e.g., plugin descriptions)
await session.close();
}, 60000);
it("should return non-empty output for /context command", async () => {
session = await client.createSession(testConfig);
// Execute /context - a local command that returns token usage info
const result = await session.executeCommand!("context");
expect(result).toBeDefined();
expect(typeof result.text).toBe("string");
// /context MUST return actual content - this is the key test
// If this fails, user messages from SDK are not being captured
expect(result.text.length).toBeGreaterThan(0);
expect(result.text).toContain("Context");
await session.close();
}, 60000);
it("should include timeline items in result", async () => {
session = await client.createSession(testConfig);
// First get available commands to use one that exists
const commands = await session.listCommands!();
expect(commands.length).toBeGreaterThan(0);
const firstCommand = commands[0];
const result = await session.executeCommand!(firstCommand.name);
// Result should include timeline items from the command execution
expect(Array.isArray(result.timeline)).toBe(true);
// Timeline may or may not have items depending on command execution
// Just verify the structure is correct
await session.close();
}, 60000);
it("should throw for non-existent command", async () => {
session = await client.createSession(testConfig);
// Non-existent commands should still work (they get sent as prompts)
// but the response should indicate the command doesn't exist
// This is how the SDK works - invalid commands become regular prompts
const result = await session.executeCommand!("nonexistent_command_xyz");
// The result should exist (command is treated as a prompt)
expect(result).toBeDefined();
await session.close();
}, 60000);
});
}); });
@@ -33,7 +33,6 @@ import {
import type { import type {
AgentCapabilityFlags, AgentCapabilityFlags,
AgentClient, AgentClient,
AgentCommandResult,
AgentMetadata, AgentMetadata,
AgentMode, AgentMode,
AgentModelDefinition, AgentModelDefinition,
@@ -1015,45 +1014,6 @@ class ClaudeAgentSession implements AgentSession {
})); }));
} }
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> {
const commandPrompt = args ? `/${commandName} ${args}` : `/${commandName}`;
// Commands return their output in a user message with <local-command-stdout> tags,
// NOT as an assistant message. We need to extract that specifically.
const events = this.stream(commandPrompt);
const timeline: AgentTimelineItem[] = [];
let commandOutput = "";
let usage: AgentUsage | undefined;
for await (const event of events) {
if (event.type === "timeline") {
timeline.push(event.item);
// Check for command output in user messages
if (event.item.type === "user_message") {
const text = event.item.text;
const match = text.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
if (match) {
commandOutput = match[1].trim();
}
}
// Also capture assistant messages (some commands may produce both)
if (event.item.type === "assistant_message" && !commandOutput) {
commandOutput = event.item.text;
}
} else if (event.type === "turn_completed") {
usage = event.usage;
} else if (event.type === "turn_failed") {
throw new Error(event.error);
}
}
return {
text: commandOutput,
timeline,
usage,
};
}
private async primeSelectableModelIds(query: Query): Promise<void> { private async primeSelectableModelIds(query: Query): Promise<void> {
try { try {
const models = await query.supportedModels(); const models = await query.supportedModels();
@@ -516,7 +516,7 @@ describe("Codex app-server provider (integration)", () => {
}, 120000); }, 120000);
test.runIf(isCodexInstalled())( test.runIf(isCodexInstalled())(
"listCommands includes custom prompts and executeCommand matches run('/prompts:*') expansion", "listCommands includes custom prompts and run('/prompts:*') expands them",
async () => { async () => {
const cleanup = useTempCodexSessionDir(); const cleanup = useTempCodexSessionDir();
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
@@ -553,9 +553,6 @@ describe("Codex app-server provider (integration)", () => {
const executeArgs = "NAME=world extra_value"; const executeArgs = "NAME=world extra_value";
const expectedExpanded = `${token}::name=world::pos1=extra_value::dollar=$`; const expectedExpanded = `${token}::name=world::pos1=extra_value::dollar=$`;
const executeResult = await session.executeCommand?.("prompts:test", executeArgs);
expect(executeResult?.text).toContain(expectedExpanded);
const rawSlashInput = "/prompts:test NAME=world extra_value"; const rawSlashInput = "/prompts:test NAME=world extra_value";
const runResult = await session.run(rawSlashInput); const runResult = await session.run(rawSlashInput);
expect(runResult.finalText).toContain(expectedExpanded); expect(runResult.finalText).toContain(expectedExpanded);
@@ -571,6 +568,63 @@ describe("Codex app-server provider (integration)", () => {
120000 120000
); );
test.runIf(isCodexInstalled())(
"slash prompt run streams live turn events (turn_started/turn_completed)",
async () => {
const cleanup = useTempCodexSessionDir();
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
const promptsDir = path.join(codexHome, "prompts");
const promptPath = path.join(promptsDir, "stream-test.md");
const cwd = tmpCwd("codex-cmd-stream-");
const token = `PASEO_STREAM_TOKEN_${Date.now()}`;
mkdirSync(promptsDir, { recursive: true });
writeFileSync(
promptPath,
[
"---",
"description: Stream Test Prompt",
"---",
`Reply with exactly: ${token}`,
].join("\n"),
"utf8"
);
try {
const client = new CodexAppServerAgentClient(logger);
const session = await client.createSession({
provider: "codex",
cwd,
modeId: "auto",
model: CODEX_TEST_MODEL,
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
});
try {
const events = session.stream("/prompts:stream-test");
const seenTypes = new Set<string>();
const assistantChunks: string[] = [];
for await (const event of events) {
seenTypes.add(event.type);
if (event.type === "timeline" && event.item.type === "assistant_message") {
assistantChunks.push(event.item.text);
}
}
expect(seenTypes.has("turn_started")).toBe(true);
expect(seenTypes.has("turn_completed")).toBe(true);
expect(assistantChunks.join("")).toContain(token);
} finally {
await session.close();
}
} finally {
cleanup();
rmSync(cwd, { recursive: true, force: true });
rmSync(promptPath, { force: true });
}
},
120000
);
test.runIf(isCodexInstalled())("command approval flow requests permission and runs command", async () => { test.runIf(isCodexInstalled())("command approval flow requests permission and runs command", async () => {
const cleanup = useTempCodexSessionDir(); const cleanup = useTempCodexSessionDir();
const cwd = tmpCwd("codex-cmd-approval-"); const cwd = tmpCwd("codex-cmd-approval-");
@@ -1,7 +1,6 @@
import type { import type {
AgentCapabilityFlags, AgentCapabilityFlags,
AgentClient, AgentClient,
AgentCommandResult,
AgentMode, AgentMode,
AgentModelDefinition, AgentModelDefinition,
McpServerConfig, McpServerConfig,
@@ -2034,17 +2033,48 @@ class CodexAppServerAgentSession implements AgentSession {
} }
} }
private async buildCommandPromptInput(
commandName: string,
args?: string
): Promise<AgentPromptInput> {
if (commandName.startsWith("prompts:")) {
const promptName = commandName.slice("prompts:".length);
const codexHome = resolveCodexHomeDir();
const promptPath = path.join(codexHome, "prompts", `${promptName}.md`);
const raw = await fs.readFile(promptPath, "utf8");
const parsed = parseFrontMatter(raw);
return expandCodexCustomPrompt(parsed.body, args);
}
if (!this.connected) {
await this.connect();
} else {
await this.loadSkills();
}
const skill = this.cachedSkills.find((entry) => entry.name === commandName);
if (skill) {
const input = [
{ type: "skill", name: skill.name, path: skill.path },
] as unknown as AgentPromptContentBlock[];
if (args && args.trim().length > 0) {
input.push({ type: "text", text: args.trim() });
} else {
input.push({ type: "text", text: `$${skill.name}` });
}
return input;
}
return args ? `$${commandName} ${args}` : `$${commandName}`;
}
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> { async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
const slashCommand = await this.resolveSlashCommandInvocation(prompt); const slashCommand = await this.resolveSlashCommandInvocation(prompt);
if (slashCommand) { if (slashCommand) {
const result = await this.executeCommand(slashCommand.commandName, slashCommand.args); const commandInput = await this.buildCommandPromptInput(
const info = await this.getRuntimeInfo(); slashCommand.commandName,
return { slashCommand.args
sessionId: info.sessionId ?? "", );
finalText: result.text, return this.runInternal(commandInput, options);
usage: result.usage,
timeline: result.timeline,
};
} }
return this.runInternal(prompt, options); return this.runInternal(prompt, options);
} }
@@ -2086,15 +2116,11 @@ class CodexAppServerAgentSession implements AgentSession {
): AsyncGenerator<AgentStreamEvent> { ): AsyncGenerator<AgentStreamEvent> {
const slashCommand = await this.resolveSlashCommandInvocation(prompt); const slashCommand = await this.resolveSlashCommandInvocation(prompt);
if (slashCommand) { if (slashCommand) {
const result = await this.executeCommand(slashCommand.commandName, slashCommand.args); const commandInput = await this.buildCommandPromptInput(
for (const item of result.timeline) { slashCommand.commandName,
yield { type: "timeline", provider: CODEX_PROVIDER, item }; slashCommand.args
} );
yield { yield* this.streamInternal(commandInput, options);
type: "turn_completed",
provider: CODEX_PROVIDER,
usage: result.usage,
};
return; return;
} }
yield* this.streamInternal(prompt, options); yield* this.streamInternal(prompt, options);
@@ -2404,42 +2430,6 @@ class CodexAppServerAgentSession implements AgentSession {
); );
} }
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> {
if (commandName.startsWith("prompts:")) {
const promptName = commandName.slice("prompts:".length);
const codexHome = resolveCodexHomeDir();
const promptPath = path.join(codexHome, "prompts", `${promptName}.md`);
const raw = await fs.readFile(promptPath, "utf8");
const parsed = parseFrontMatter(raw);
const expanded = expandCodexCustomPrompt(parsed.body, args);
const result = await this.runInternal(expanded);
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
}
if (!this.connected) {
await this.connect();
} else {
await this.loadSkills();
}
const skill = this.cachedSkills.find((entry) => entry.name === commandName);
if (skill) {
const input = [
{ type: "skill", name: skill.name, path: skill.path },
] as unknown as AgentPromptContentBlock[];
if (args && args.trim().length > 0) {
input.push({ type: "text", text: args.trim() });
} else {
input.push({ type: "text", text: `$${skill.name}` });
}
const result = await this.runInternal(input);
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
}
const skillPrompt = args ? `$${commandName} ${args}` : `$${commandName}`;
const result = await this.runInternal(skillPrompt);
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
}
private async ensureThread(): Promise<void> { private async ensureThread(): Promise<void> {
if (!this.client) return; if (!this.client) return;
if (this.currentThreadId) return; if (this.currentThreadId) return;
@@ -77,7 +77,7 @@ describe("codex agent commands E2E", () => {
rmSync(codexHome, { recursive: true, force: true }); rmSync(codexHome, { recursive: true, force: true });
}, 30_000); }, 30_000);
test("executes a custom prompt command (prompts:*)", async () => { test("executes a custom prompt via normal sendMessage path (prompts:*)", async () => {
const codexHome = process.env.CODEX_HOME ?? path.join(process.env.HOME ?? "/tmp", ".codex"); const codexHome = process.env.CODEX_HOME ?? path.join(process.env.HOME ?? "/tmp", ".codex");
const promptsDir = path.join(codexHome, "prompts"); const promptsDir = path.join(codexHome, "prompts");
@@ -95,18 +95,19 @@ describe("codex agent commands E2E", () => {
title: "Codex Prompt Execute Test Agent", title: "Codex Prompt Execute Test Agent",
}); });
const result = await ctx.client.executeCommand( await ctx.client.sendMessage(
agent.id, agent.id,
"prompts:paseo-test-sayok", "/prompts:paseo-test-sayok NAME=world"
"NAME=world"
); );
expect(result.error).toBeNull(); const state = await ctx.client.waitForFinish(agent.id, 30_000);
expect(result.result?.text).toContain("PASEO_OK");
expect(state.status).toBe("idle");
expect(state.lastMessage).toContain("PASEO_OK");
rmSync(promptPath, { force: true }); rmSync(promptPath, { force: true });
}, 30_000); }, 30_000);
test("sendMessage routes known slash commands through executeCommand", async () => { test("sendMessage routes known slash commands through standard run/stream", async () => {
const prevCodexHome = process.env.CODEX_HOME; const prevCodexHome = process.env.CODEX_HOME;
const codexHome = tmpDir("codex-home-send-command-"); const codexHome = tmpDir("codex-home-send-command-");
const promptsDir = path.join(codexHome, "prompts"); const promptsDir = path.join(codexHome, "prompts");
-82
View File
@@ -1527,15 +1527,6 @@ export class Session {
await this.handleListCommandsRequest(msg); await this.handleListCommandsRequest(msg);
break; break;
case "execute_command_request":
await this.handleExecuteCommandRequest(
msg.agentId,
msg.commandName,
msg.args,
msg.requestId
);
break;
case "register_push_token": case "register_push_token":
this.handleRegisterPushToken(msg.token); this.handleRegisterPushToken(msg.token);
break; break;
@@ -3553,79 +3544,6 @@ export class Session {
} }
} }
/**
* Handle execute command request for an agent
*/
private async handleExecuteCommandRequest(
agentId: string,
commandName: string,
args: string | undefined,
requestId: string
): Promise<void> {
this.sessionLogger.debug(
{ agentId, commandName },
`Handling execute command request for agent ${agentId}`
);
try {
const agents = this.agentManager.listAgents();
const agent = agents.find((a) => a.id === agentId);
if (!agent) {
this.emit({
type: "execute_command_response",
payload: {
agentId,
result: null,
error: `Agent not found: ${agentId}`,
requestId,
},
});
return;
}
const session = agent.session;
if (!session || !session.executeCommand) {
this.emit({
type: "execute_command_response",
payload: {
agentId,
result: null,
error: `Agent does not support executing commands`,
requestId,
},
});
return;
}
const result = await session.executeCommand(commandName, args);
this.emit({
type: "execute_command_response",
payload: {
agentId,
result,
error: null,
requestId,
},
});
} catch (error: any) {
this.sessionLogger.error(
{ err: error, agentId, commandName },
"Failed to execute command"
);
this.emit({
type: "execute_command_response",
payload: {
agentId,
result: null,
error: error.message,
requestId,
},
});
}
}
/** /**
* Handle agent permission response from user * Handle agent permission response from user
*/ */
@@ -16,7 +16,6 @@ import type {
AgentSessionConfig, AgentSessionConfig,
AgentStreamEvent, AgentStreamEvent,
AgentSlashCommand, AgentSlashCommand,
AgentCommandResult,
AgentUsage, AgentUsage,
ListModelsOptions, ListModelsOptions,
} from "../agent/agent-sdk-types.js"; } from "../agent/agent-sdk-types.js";
@@ -251,7 +250,7 @@ class FakeAgentSession implements AgentSession {
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> { async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
const slashCommand = await this.resolveSlashCommandInput(prompt); const slashCommand = await this.resolveSlashCommandInput(prompt);
if (slashCommand) { if (slashCommand) {
const result = await this.executeCommand( const result = await this.runSlashCommand(
slashCommand.commandName, slashCommand.commandName,
slashCommand.args slashCommand.args
); );
@@ -290,7 +289,7 @@ class FakeAgentSession implements AgentSession {
await this.appendHistoryEvent(turnStarted); await this.appendHistoryEvent(turnStarted);
yield turnStarted; yield turnStarted;
const result = await this.executeCommand( const result = await this.runSlashCommand(
slashCommand.commandName, slashCommand.commandName,
slashCommand.args slashCommand.args
); );
@@ -578,7 +577,14 @@ class FakeAgentSession implements AgentSession {
]; ];
} }
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> { private async runSlashCommand(
commandName: string,
args?: string
): Promise<{
text: string;
timeline: AgentRunResult["timeline"];
usage: AgentUsage;
}> {
const fullName = commandName.trim(); const fullName = commandName.trim();
if (this.providerName === "codex" && fullName.startsWith("prompts:")) { if (this.providerName === "codex" && fullName.startsWith("prompts:")) {
const promptId = fullName.slice("prompts:".length); const promptId = fullName.slice("prompts:".length);
-28
View File
@@ -941,14 +941,6 @@ export const ListCommandsRequestSchema = z.object({
requestId: z.string(), requestId: z.string(),
}); });
export const ExecuteCommandRequestSchema = z.object({
type: z.literal("execute_command_request"),
agentId: z.string(),
commandName: z.string(),
args: z.string().optional(),
requestId: z.string(),
});
export const RegisterPushTokenMessageSchema = z.object({ export const RegisterPushTokenMessageSchema = z.object({
type: z.literal("register_push_token"), type: z.literal("register_push_token"),
token: z.string(), token: z.string(),
@@ -1082,7 +1074,6 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
ClientHeartbeatMessageSchema, ClientHeartbeatMessageSchema,
PingMessageSchema, PingMessageSchema,
ListCommandsRequestSchema, ListCommandsRequestSchema,
ExecuteCommandRequestSchema,
RegisterPushTokenMessageSchema, RegisterPushTokenMessageSchema,
ListTerminalsRequestSchema, ListTerminalsRequestSchema,
SubscribeTerminalsRequestSchema, SubscribeTerminalsRequestSchema,
@@ -1868,22 +1859,6 @@ export const ListCommandsResponseSchema = z.object({
}), }),
}); });
const AgentCommandResultSchema = z.object({
text: z.string(),
timeline: z.array(AgentTimelineItemPayloadSchema),
usage: AgentUsageSchema.optional(),
});
export const ExecuteCommandResponseSchema = z.object({
type: z.literal("execute_command_response"),
payload: z.object({
agentId: z.string(),
result: AgentCommandResultSchema.nullable(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
// ============================================================================ // ============================================================================
// Terminal Outbound Messages // Terminal Outbound Messages
// ============================================================================ // ============================================================================
@@ -2050,7 +2025,6 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
SpeechModelsListResponseSchema, SpeechModelsListResponseSchema,
SpeechModelsDownloadResponseSchema, SpeechModelsDownloadResponseSchema,
ListCommandsResponseSchema, ListCommandsResponseSchema,
ExecuteCommandResponseSchema,
ListTerminalsResponseSchema, ListTerminalsResponseSchema,
TerminalsChangedSchema, TerminalsChangedSchema,
CreateTerminalResponseSchema, CreateTerminalResponseSchema,
@@ -2191,8 +2165,6 @@ export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessa
export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema>; export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema>;
export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>; export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>;
export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>; export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>;
export type ExecuteCommandRequest = z.infer<typeof ExecuteCommandRequestSchema>;
export type ExecuteCommandResponse = z.infer<typeof ExecuteCommandResponseSchema>;
export type RegisterPushTokenMessage = z.infer<typeof RegisterPushTokenMessageSchema>; export type RegisterPushTokenMessage = z.infer<typeof RegisterPushTokenMessageSchema>;
// Terminal message types // Terminal message types