diff --git a/apps/cli/src/agent/json-event-emitter.ts b/apps/cli/src/agent/json-event-emitter.ts index 6b6c02a88b6..f46c39be506 100644 --- a/apps/cli/src/agent/json-event-emitter.ts +++ b/apps/cli/src/agent/json-event-emitter.ts @@ -156,7 +156,7 @@ export class JsonEventEmitter { emitControl(event: { subtype: "ack" | "done" | "error" requestId?: string - command?: string + command?: JsonEvent["command"] taskId?: string content?: string success?: boolean diff --git a/apps/cli/src/commands/cli/stdin-stream.ts b/apps/cli/src/commands/cli/stdin-stream.ts index 03e003d6f88..2845265dd05 100644 --- a/apps/cli/src/commands/cli/stdin-stream.ts +++ b/apps/cli/src/commands/cli/stdin-stream.ts @@ -1,7 +1,12 @@ import { createInterface } from "readline" import { randomUUID } from "crypto" -import type { RooCodeSettings } from "@roo-code/types" +import { + rooCliCommandNames, + type RooCliCommandName, + type RooCliInputCommand, + type RooCliStartCommand, +} from "@roo-code/types" import { isRecord } from "@/lib/utils/guards.js" @@ -12,20 +17,15 @@ import type { JsonEventEmitter } from "@/agent/json-event-emitter.js" // Types // --------------------------------------------------------------------------- -export type StdinStreamCommandName = "start" | "message" | "cancel" | "ping" | "shutdown" +export type StdinStreamCommandName = RooCliCommandName -export type StdinStreamCommand = - | { command: "start"; requestId: string; prompt: string; configuration?: RooCodeSettings } - | { command: "message"; requestId: string; prompt: string } - | { command: "cancel"; requestId: string } - | { command: "ping"; requestId: string } - | { command: "shutdown"; requestId: string } +export type StdinStreamCommand = RooCliInputCommand // --------------------------------------------------------------------------- // Parsing // --------------------------------------------------------------------------- -export const VALID_STDIN_COMMANDS = new Set(["start", "message", "cancel", "ping", "shutdown"]) +export const VALID_STDIN_COMMANDS = new Set(rooCliCommandNames) export function parseStdinStreamCommand(line: string, lineNumber: number): StdinStreamCommand { let parsed: unknown @@ -67,7 +67,12 @@ export function parseStdinStreamCommand(line: string, lineNumber: number): Stdin } if (command === "start" && isRecord(parsed.configuration)) { - return { command, requestId, prompt: promptRaw, configuration: parsed.configuration as RooCodeSettings } + return { + command, + requestId, + prompt: promptRaw, + configuration: parsed.configuration as RooCliStartCommand["configuration"], + } } return { command, requestId, prompt: promptRaw } diff --git a/apps/cli/src/types/json-events.ts b/apps/cli/src/types/json-events.ts index 048a303a0f1..73eb1b71506 100644 --- a/apps/cli/src/types/json-events.ts +++ b/apps/cli/src/types/json-events.ts @@ -1,3 +1,15 @@ +import { + rooCliOutputFormats, + type RooCliCost, + type RooCliEventType, + type RooCliFinalOutput, + type RooCliOutputFormat, + type RooCliQueueItem, + type RooCliStreamEvent, + type RooCliToolResult, + type RooCliToolUse, +} from "@roo-code/types" + /** * JSON Event Types for Structured CLI Output * @@ -14,9 +26,9 @@ /** * Output format options for the CLI. */ -export const OUTPUT_FORMATS = ["text", "json", "stream-json"] as const +export const OUTPUT_FORMATS = rooCliOutputFormats -export type OutputFormat = (typeof OUTPUT_FORMATS)[number] +export type OutputFormat = RooCliOutputFormat export function isValidOutputFormat(format: string): format is OutputFormat { return (OUTPUT_FORMATS as readonly string[]).includes(format) @@ -25,66 +37,24 @@ export function isValidOutputFormat(format: string): format is OutputFormat { /** * Event type discriminators for JSON output. */ -export type JsonEventType = - | "system" // System messages (init, ready, shutdown) - | "control" // Transport/control protocol events - | "queue" // Message queue telemetry from extension state - | "assistant" // Assistant text messages - | "user" // User messages (echoed input) - | "tool_use" // Tool invocations (file ops, commands, browser, MCP) - | "tool_result" // Results from tool execution - | "thinking" // Reasoning/thinking content - | "error" // Errors - | "result" // Final task result +export type JsonEventType = RooCliEventType -export interface JsonEventQueueItem { - /** Queue item id generated by MessageQueueService */ - id: string - /** Queued text prompt preview */ - text?: string - /** Number of attached images in the queued message */ - imageCount?: number - /** Queue insertion/update timestamp (ms epoch) */ - timestamp?: number -} +export type JsonEventQueueItem = RooCliQueueItem /** * Tool use information for tool_use events. */ -export interface JsonEventToolUse { - /** Tool name (e.g., "read_file", "write_to_file", "execute_command") */ - name: string - /** Tool input parameters */ - input?: Record -} +export type JsonEventToolUse = RooCliToolUse /** * Tool result information for tool_result events. */ -export interface JsonEventToolResult { - /** Tool name that produced this result */ - name: string - /** Tool output (for successful execution) */ - output?: string - /** Error message (for failed execution) */ - error?: string -} +export type JsonEventToolResult = RooCliToolResult /** * Cost and token usage information. */ -export interface JsonEventCost { - /** Total cost in USD */ - totalCost?: number - /** Input tokens used */ - inputTokens?: number - /** Output tokens generated */ - outputTokens?: number - /** Cache write tokens */ - cacheWrites?: number - /** Cache read tokens */ - cacheReads?: number -} +export type JsonEventCost = RooCliCost /** * Base JSON event structure. @@ -94,7 +64,7 @@ export interface JsonEventCost { * - Each delta includes `id` for easy correlation * - Final message has `done: true` */ -export interface JsonEvent { +export type JsonEvent = RooCliStreamEvent & { /** Event type discriminator */ type: JsonEventType /** Protocol schema version (included on system.init) */ @@ -137,7 +107,7 @@ export interface JsonEvent { * Final JSON output for "json" mode (single object at end). * Contains the result and accumulated messages. */ -export interface JsonFinalOutput { +export type JsonFinalOutput = RooCliFinalOutput & { /** Final result type */ type: "result" /** Whether the task succeeded */ diff --git a/packages/types/src/__tests__/cli.test.ts b/packages/types/src/__tests__/cli.test.ts new file mode 100644 index 00000000000..483e633b4c9 --- /dev/null +++ b/packages/types/src/__tests__/cli.test.ts @@ -0,0 +1,80 @@ +import { + rooCliControlEventSchema, + rooCliFinalOutputSchema, + rooCliInputCommandSchema, + rooCliStreamEventSchema, +} from "../cli.js" + +describe("CLI types", () => { + describe("rooCliInputCommandSchema", () => { + it("validates a start command", () => { + const result = rooCliInputCommandSchema.safeParse({ + command: "start", + requestId: "req-1", + prompt: "hello", + configuration: {}, + }) + + expect(result.success).toBe(true) + }) + + it("rejects a message command without prompt", () => { + const result = rooCliInputCommandSchema.safeParse({ + command: "message", + requestId: "req-2", + }) + + expect(result.success).toBe(false) + }) + }) + + describe("rooCliControlEventSchema", () => { + it("validates a control done event", () => { + const result = rooCliControlEventSchema.safeParse({ + type: "control", + subtype: "done", + requestId: "req-3", + command: "start", + success: true, + code: "task_completed", + }) + + expect(result.success).toBe(true) + }) + + it("rejects control event without requestId", () => { + const result = rooCliControlEventSchema.safeParse({ + type: "control", + subtype: "ack", + }) + + expect(result.success).toBe(false) + }) + }) + + describe("rooCliStreamEventSchema", () => { + it("accepts passthrough fields for forward compatibility", () => { + const result = rooCliStreamEventSchema.safeParse({ + type: "assistant", + id: 42, + content: "partial", + customField: "future", + }) + + expect(result.success).toBe(true) + }) + }) + + describe("rooCliFinalOutputSchema", () => { + it("validates final json output shape", () => { + const result = rooCliFinalOutputSchema.safeParse({ + type: "result", + success: true, + content: "done", + events: [], + }) + + expect(result.success).toBe(true) + }) + }) +}) diff --git a/packages/types/src/cli.ts b/packages/types/src/cli.ts new file mode 100644 index 00000000000..738db4a103e --- /dev/null +++ b/packages/types/src/cli.ts @@ -0,0 +1,173 @@ +import { z } from "zod" + +import { rooCodeSettingsSchema } from "./global-settings.js" + +/** + * Roo CLI stdin commands + */ + +export const rooCliCommandNames = ["start", "message", "cancel", "ping", "shutdown"] as const + +export const rooCliCommandNameSchema = z.enum(rooCliCommandNames) + +export type RooCliCommandName = z.infer + +export const rooCliCommandBaseSchema = z.object({ + command: rooCliCommandNameSchema, + requestId: z.string().min(1), +}) + +export type RooCliCommandBase = z.infer + +export const rooCliStartCommandSchema = rooCliCommandBaseSchema.extend({ + command: z.literal("start"), + prompt: z.string(), + configuration: rooCodeSettingsSchema.optional(), +}) + +export type RooCliStartCommand = z.infer + +export const rooCliMessageCommandSchema = rooCliCommandBaseSchema.extend({ + command: z.literal("message"), + prompt: z.string(), +}) + +export type RooCliMessageCommand = z.infer + +export const rooCliCancelCommandSchema = rooCliCommandBaseSchema.extend({ + command: z.literal("cancel"), +}) + +export type RooCliCancelCommand = z.infer + +export const rooCliPingCommandSchema = rooCliCommandBaseSchema.extend({ + command: z.literal("ping"), +}) + +export type RooCliPingCommand = z.infer + +export const rooCliShutdownCommandSchema = rooCliCommandBaseSchema.extend({ + command: z.literal("shutdown"), +}) + +export type RooCliShutdownCommand = z.infer + +export const rooCliInputCommandSchema = z.discriminatedUnion("command", [ + rooCliStartCommandSchema, + rooCliMessageCommandSchema, + rooCliCancelCommandSchema, + rooCliPingCommandSchema, + rooCliShutdownCommandSchema, +]) + +export type RooCliInputCommand = z.infer + +/** + * Roo CLI stream-json output + */ + +export const rooCliOutputFormats = ["text", "json", "stream-json"] as const + +export const rooCliOutputFormatSchema = z.enum(rooCliOutputFormats) + +export type RooCliOutputFormat = z.infer + +export const rooCliEventTypes = [ + "system", + "control", + "queue", + "assistant", + "user", + "tool_use", + "tool_result", + "thinking", + "error", + "result", +] as const + +export const rooCliEventTypeSchema = z.enum(rooCliEventTypes) + +export type RooCliEventType = z.infer + +export const rooCliControlSubtypes = ["ack", "done", "error"] as const + +export const rooCliControlSubtypeSchema = z.enum(rooCliControlSubtypes) + +export type RooCliControlSubtype = z.infer + +export const rooCliQueueItemSchema = z.object({ + id: z.string().min(1), + text: z.string().optional(), + imageCount: z.number().optional(), + timestamp: z.number().optional(), +}) + +export type RooCliQueueItem = z.infer + +export const rooCliToolUseSchema = z.object({ + name: z.string(), + input: z.record(z.unknown()).optional(), +}) + +export type RooCliToolUse = z.infer + +export const rooCliToolResultSchema = z.object({ + name: z.string(), + output: z.string().optional(), + error: z.string().optional(), +}) + +export type RooCliToolResult = z.infer + +export const rooCliCostSchema = z.object({ + totalCost: z.number().optional(), + inputTokens: z.number().optional(), + outputTokens: z.number().optional(), + cacheWrites: z.number().optional(), + cacheReads: z.number().optional(), +}) + +export type RooCliCost = z.infer + +export const rooCliStreamEventSchema = z + .object({ + type: rooCliEventTypeSchema.optional(), + subtype: z.string().optional(), + requestId: z.string().optional(), + command: rooCliCommandNameSchema.optional(), + taskId: z.string().optional(), + code: z.string().optional(), + content: z.string().optional(), + success: z.boolean().optional(), + id: z.number().optional(), + done: z.boolean().optional(), + queueDepth: z.number().optional(), + queue: z.array(rooCliQueueItemSchema).optional(), + schemaVersion: z.number().optional(), + protocol: z.string().optional(), + capabilities: z.array(z.string()).optional(), + tool_use: rooCliToolUseSchema.optional(), + tool_result: rooCliToolResultSchema.optional(), + cost: rooCliCostSchema.optional(), + }) + .passthrough() + +export type RooCliStreamEvent = z.infer + +export const rooCliControlEventSchema = rooCliStreamEventSchema.extend({ + type: z.literal("control"), + subtype: rooCliControlSubtypeSchema, + requestId: z.string().min(1), +}) + +export type RooCliControlEvent = z.infer + +export const rooCliFinalOutputSchema = z.object({ + type: z.literal("result"), + success: z.boolean(), + content: z.string().optional(), + cost: rooCliCostSchema.optional(), + events: z.array(rooCliStreamEventSchema), +}) + +export type RooCliFinalOutput = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 278e727243c..cd5804aecb7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,4 +1,5 @@ export * from "./api.js" +export * from "./cli.js" export * from "./cloud.js" export * from "./codebase-index.js" export * from "./context-management.js"