From f1c84d385862ffda614b661c874126522b9957bb Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:50:17 -0700 Subject: [PATCH] refactor(cli): split oversized command modules (#9775) --- resources/skills/current-manifest.json | 4 +- resources/skills/release-mapping.json | 13 +++ resources/skills/snapshot-registry.json | 32 ++++++ src/cli/specs/linear-mcp.ts | 96 ++++++++++++++++ src/cli/specs/linear.ts | 93 +-------------- src/main/ssh/ssh-remote-cli-args.ts | 117 +++++++++++++++++++ src/main/ssh/ssh-remote-orca-cli.ts | 147 ++++-------------------- 7 files changed, 285 insertions(+), 217 deletions(-) create mode 100644 src/cli/specs/linear-mcp.ts create mode 100644 src/main/ssh/ssh-remote-cli-args.ts diff --git a/resources/skills/current-manifest.json b/resources/skills/current-manifest.json index f1b1e2cec..2662ae48a 100644 --- a/resources/skills/current-manifest.json +++ b/resources/skills/current-manifest.json @@ -22,7 +22,7 @@ { "name": "linear-tickets", "sourcePath": "skills/linear-tickets", - "releaseRevision": 5, + "releaseRevision": 6, "packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6", "gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c", "files": [ @@ -94,7 +94,7 @@ { "name": "orca-linear", "sourcePath": "skills/orca-linear", - "releaseRevision": 3, + "releaseRevision": 4, "packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d", "gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23", "files": [ diff --git a/resources/skills/release-mapping.json b/resources/skills/release-mapping.json index 2eea665f9..b83af3545 100644 --- a/resources/skills/release-mapping.json +++ b/resources/skills/release-mapping.json @@ -548,6 +548,19 @@ "orca-per-workspace-env": 2, "orchestration": 25 } + }, + { + "appVersion": "1.4.149-rc.1.issue7936", + "skills": { + "computer-use": 5, + "linear-tickets": 5, + "orca-cli": 35, + "orca-emulator": 4, + "orca-emulator-android": 2, + "orca-linear": 3, + "orca-per-workspace-env": 2, + "orchestration": 25 + } } ] } diff --git a/resources/skills/snapshot-registry.json b/resources/skills/snapshot-registry.json index 77be83dee..eb0d14051 100644 --- a/resources/skills/snapshot-registry.json +++ b/resources/skills/snapshot-registry.json @@ -1198,6 +1198,22 @@ }, { "releaseRevision": 5, + "packageDigest": "30f836d836df611c0c974ac0ee48d817fbe6711a49465d139f0981cbd75f828e", + "gitTreeSha": "8e862333bdeb9970e11dcd6634e493ecc6ff6698", + "files": [ + { + "path": "SKILL.md", + "size": 11514, + "executable": false, + "classification": "text", + "exactSha256": "922c1740ffe8be685ed35e6f51c7baa81f246e1738c8ecff224fca2b64a415b9", + "textNormalizedSha256": "922c1740ffe8be685ed35e6f51c7baa81f246e1738c8ecff224fca2b64a415b9", + "identitySha256": "922c1740ffe8be685ed35e6f51c7baa81f246e1738c8ecff224fca2b64a415b9" + } + ] + }, + { + "releaseRevision": 6, "packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6", "gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c", "files": [ @@ -1248,6 +1264,22 @@ }, { "releaseRevision": 3, + "packageDigest": "2fcccfe7c3166ca0b78a50ccdfd7acad4c6e6eeffa14e48ed1edee53c796e1f0", + "gitTreeSha": "761d4427e9f330607664da5d6ff55400be8c690b", + "files": [ + { + "path": "SKILL.md", + "size": 11238, + "executable": false, + "classification": "text", + "exactSha256": "04ef6cb0377bc7c2dd38dc713b4ef5196ee6851dc90e9fdcddbc2347faa678e6", + "textNormalizedSha256": "04ef6cb0377bc7c2dd38dc713b4ef5196ee6851dc90e9fdcddbc2347faa678e6", + "identitySha256": "04ef6cb0377bc7c2dd38dc713b4ef5196ee6851dc90e9fdcddbc2347faa678e6" + } + ] + }, + { + "releaseRevision": 4, "packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d", "gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23", "files": [ diff --git a/src/cli/specs/linear-mcp.ts b/src/cli/specs/linear-mcp.ts new file mode 100644 index 000000000..39528f864 --- /dev/null +++ b/src/cli/specs/linear-mcp.ts @@ -0,0 +1,96 @@ +import type { CommandSpec } from '../args' +import { GLOBAL_FLAGS } from '../args' + +export const LINEAR_MCP_COMMAND_SPECS: CommandSpec[] = [ + { + path: ['linear', 'save-issue'], + summary: 'Create or update a Linear issue', + usage: + 'orca linear save-issue [] [--current] [--team ] [--title ] [--description <text> | --body-file <path|->] [--state <state>] [--assignee me|<user>|null] [--priority none|low|medium|high|urgent] [--estimate <number>|null] [--due-date <yyyy-mm-dd>|null] [--label <label>...] [--project <project>|null] [--parent-id <issue>|null] [--write-id <uuid>] [--workspace <id>] [--json]', + allowedFlags: [ + ...GLOBAL_FLAGS, + 'current', + 'team', + 'title', + 'description', + 'body', + 'body-file', + 'state', + 'assignee', + 'priority', + 'estimate', + 'due-date', + 'label', + 'project', + 'parent-id', + 'write-id', + 'workspace', + 'id' + ], + positionalArgs: ['id'], + examples: [ + 'orca linear save-issue --team ENG --title "Fix auth" --priority high --json', + 'orca linear save-issue ENG-123 --title "Fix OAuth callback" --assignee me --json', + 'orca linear save-issue --current --project null --due-date null --json' + ], + notes: [ + 'Without <id> or --current, creates an issue and requires --team and --title.', + 'Labels replace the complete label set, matching Linear MCP save_issue semantics.', + 'Use the literal null to clear assignee, estimate, due date, project, or parent.' + ] + }, + { + path: ['linear', 'list-issues'], + summary: 'List Linear issues with MCP-compatible filters', + usage: + 'orca linear list-issues [--team <team>] [--cycle <cycle>] [--label <label>] [--limit <n>] [--query <text>] [--state <state>] [--cursor <cursor>] [--order-by createdAt|updatedAt] [--project <project>] [--release <release>] [--assignee <user|me|null>] [--delegate <user|me|null>] [--parent-id <issue|null>] [--priority <0-4>] [--created-at <datetime|duration>] [--updated-at <datetime|duration>] [--include-archived] [--workspace <id>|all] [--json]', + allowedFlags: [ + ...GLOBAL_FLAGS, + 'team', + 'cycle', + 'label', + 'limit', + 'query', + 'state', + 'cursor', + 'order-by', + 'project', + 'release', + 'assignee', + 'delegate', + 'parent-id', + 'priority', + 'created-at', + 'updated-at', + 'include-archived', + 'workspace' + ], + examples: [ + 'orca linear list-issues --team ENG --state started --assignee me --json', + 'orca linear list-issues --query auth --updated-at -P7D --limit 100 --json', + 'orca linear list-issues --cursor <cursor> --workspace <id> --json' + ] + }, + { + path: ['linear', 'relation', 'add'], + summary: 'Add a Linear issue relation', + usage: + 'orca linear relation add [<id>] [--current] --related <issue> --type blocks|blocked-by|related|duplicate-of [--workspace <id>] [--json]', + allowedFlags: [...GLOBAL_FLAGS, 'current', 'related', 'type', 'workspace', 'id'], + positionalArgs: ['id'], + examples: [ + 'orca linear relation add ENG-1 --related ENG-2 --type blocks --json', + 'orca linear relation add --current --related ENG-2 --type blocked-by --json' + ] + }, + { + path: ['linear', 'relation', 'remove'], + aliases: [['linear', 'relation', 'rm']], + summary: 'Remove a Linear issue relation', + usage: + 'orca linear relation remove [<id>] [--current] --related <issue> --type blocks|blocked-by|related|duplicate-of [--workspace <id>] [--json]', + allowedFlags: [...GLOBAL_FLAGS, 'current', 'related', 'type', 'workspace', 'id'], + positionalArgs: ['id'], + examples: ['orca linear relation remove ENG-1 --related ENG-2 --type related --json'] + } +] diff --git a/src/cli/specs/linear.ts b/src/cli/specs/linear.ts index 575927455..6b60db03f 100644 --- a/src/cli/specs/linear.ts +++ b/src/cli/specs/linear.ts @@ -1,98 +1,9 @@ import type { CommandSpec } from '../args' import { GLOBAL_FLAGS } from '../args' +import { LINEAR_MCP_COMMAND_SPECS } from './linear-mcp' export const LINEAR_COMMAND_SPECS: CommandSpec[] = [ - { - path: ['linear', 'save-issue'], - summary: 'Create or update a Linear issue', - usage: - 'orca linear save-issue [<id>] [--current] [--team <key|id>] [--title <title>] [--description <text> | --body-file <path|->] [--state <state>] [--assignee me|<user>|null] [--priority none|low|medium|high|urgent] [--estimate <number>|null] [--due-date <yyyy-mm-dd>|null] [--label <label>...] [--project <project>|null] [--parent-id <issue>|null] [--write-id <uuid>] [--workspace <id>] [--json]', - allowedFlags: [ - ...GLOBAL_FLAGS, - 'current', - 'team', - 'title', - 'description', - 'body', - 'body-file', - 'state', - 'assignee', - 'priority', - 'estimate', - 'due-date', - 'label', - 'project', - 'parent-id', - 'write-id', - 'workspace', - 'id' - ], - positionalArgs: ['id'], - examples: [ - 'orca linear save-issue --team ENG --title "Fix auth" --priority high --json', - 'orca linear save-issue ENG-123 --title "Fix OAuth callback" --assignee me --json', - 'orca linear save-issue --current --project null --due-date null --json' - ], - notes: [ - 'Without <id> or --current, creates an issue and requires --team and --title.', - 'Labels replace the complete label set, matching Linear MCP save_issue semantics.', - 'Use the literal null to clear assignee, estimate, due date, project, or parent.' - ] - }, - { - path: ['linear', 'list-issues'], - summary: 'List Linear issues with MCP-compatible filters', - usage: - 'orca linear list-issues [--team <team>] [--cycle <cycle>] [--label <label>] [--limit <n>] [--query <text>] [--state <state>] [--cursor <cursor>] [--order-by createdAt|updatedAt] [--project <project>] [--release <release>] [--assignee <user|me|null>] [--delegate <user|me|null>] [--parent-id <issue|null>] [--priority <0-4>] [--created-at <datetime|duration>] [--updated-at <datetime|duration>] [--include-archived] [--workspace <id>|all] [--json]', - allowedFlags: [ - ...GLOBAL_FLAGS, - 'team', - 'cycle', - 'label', - 'limit', - 'query', - 'state', - 'cursor', - 'order-by', - 'project', - 'release', - 'assignee', - 'delegate', - 'parent-id', - 'priority', - 'created-at', - 'updated-at', - 'include-archived', - 'workspace' - ], - examples: [ - 'orca linear list-issues --team ENG --state started --assignee me --json', - 'orca linear list-issues --query auth --updated-at -P7D --limit 100 --json', - 'orca linear list-issues --cursor <cursor> --workspace <id> --json' - ] - }, - { - path: ['linear', 'relation', 'add'], - summary: 'Add a Linear issue relation', - usage: - 'orca linear relation add [<id>] [--current] --related <issue> --type blocks|blocked-by|related|duplicate-of [--workspace <id>] [--json]', - allowedFlags: [...GLOBAL_FLAGS, 'current', 'related', 'type', 'workspace', 'id'], - positionalArgs: ['id'], - examples: [ - 'orca linear relation add ENG-1 --related ENG-2 --type blocks --json', - 'orca linear relation add --current --related ENG-2 --type blocked-by --json' - ] - }, - { - path: ['linear', 'relation', 'remove'], - aliases: [['linear', 'relation', 'rm']], - summary: 'Remove a Linear issue relation', - usage: - 'orca linear relation remove [<id>] [--current] --related <issue> --type blocks|blocked-by|related|duplicate-of [--workspace <id>] [--json]', - allowedFlags: [...GLOBAL_FLAGS, 'current', 'related', 'type', 'workspace', 'id'], - positionalArgs: ['id'], - examples: ['orca linear relation remove ENG-1 --related ENG-2 --type related --json'] - }, + ...LINEAR_MCP_COMMAND_SPECS, { path: ['linear', 'issue'], summary: 'Read Linear issue context for agents', diff --git a/src/main/ssh/ssh-remote-cli-args.ts b/src/main/ssh/ssh-remote-cli-args.ts new file mode 100644 index 000000000..579326e67 --- /dev/null +++ b/src/main/ssh/ssh-remote-cli-args.ts @@ -0,0 +1,117 @@ +import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error' + +const REMOTE_BOOLEAN_FLAGS = new Set([ + 'all', + 'attachments', + 'children', + 'comments', + 'current', + 'full', + 'help', + 'inject', + 'include-archived', + 'json', + 'me', + 'relations', + 'parent-current', + 'unread', + 'wait' +]) +const REPEATED_FLAG_SEPARATOR = '\u0000' +const REPEATABLE_REMOTE_STRING_FLAGS = new Set(['label']) + +export function parseRemoteCliArgs(argv: string[]): ParsedRemoteCli { + const commandPath: string[] = [] + const flags = new Map<string, string | boolean>() + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i] + if (!token.startsWith('--')) { + commandPath.push(token) + continue + } + const assignment = token.slice(2) + // Why: the SSH relay-backed shim should accept values beginning with `--` via `--flag=value`. + const equalsIndex = assignment.indexOf('=') + if (equalsIndex !== -1) { + setRemoteFlag(flags, assignment.slice(0, equalsIndex), assignment.slice(equalsIndex + 1)) + continue + } + + const flag = assignment + const next = argv[i + 1] + if (!isRemoteBooleanFlag(flag, commandPath) && next && !next.startsWith('--')) { + setRemoteFlag(flags, flag, next) + i += 1 + } else { + setRemoteFlag(flags, flag, true) + } + } + return { commandPath, flags } +} + +export function resolveRemoteCliHandle( + flags: Map<string, string | boolean>, + env: Record<string, string>, + flagName: string +): string { + return optionalRemoteCliString(flags, flagName) ?? env.ORCA_TERMINAL_HANDLE ?? 'unknown' +} + +export function requiredRemoteCliString( + flags: Map<string, string | boolean>, + name: string +): string { + const value = optionalRemoteCliString(flags, name) + if (!value) { + throw new Error(`Missing --${name}`) + } + return value +} + +export function optionalRemoteCliString( + flags: Map<string, string | boolean>, + name: string +): string | undefined { + const value = flags.get(name) + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +export function optionalRemoteCliNumber( + flags: Map<string, string | boolean>, + name: string +): number | undefined { + const value = optionalRemoteCliString(flags, name) + if (value === undefined) { + return undefined + } + const parsed = Number(value) + if (!Number.isFinite(parsed)) { + throw new RemoteCliArgumentError('invalid_argument', `Invalid numeric value for --${name}`) + } + return parsed +} + +function isRemoteBooleanFlag(flag: string, commandPath: string[]): boolean { + // Why: Android launch already uses --activity <name>; only Linear issue reads use it as a boolean. + return ( + REMOTE_BOOLEAN_FLAGS.has(flag) || + (flag === 'activity' && commandPath[0] === 'linear' && commandPath[1] === 'issue') + ) +} + +function setRemoteFlag( + flags: Map<string, string | boolean>, + name: string, + value: string | boolean +): void { + const previous = flags.get(name) + if ( + typeof previous === 'string' && + typeof value === 'string' && + REPEATABLE_REMOTE_STRING_FLAGS.has(name) + ) { + flags.set(name, `${previous}${REPEATED_FLAG_SEPARATOR}${value}`) + return + } + flags.set(name, value) +} diff --git a/src/main/ssh/ssh-remote-orca-cli.ts b/src/main/ssh/ssh-remote-orca-cli.ts index 15094b0e8..14f5448d3 100644 --- a/src/main/ssh/ssh-remote-orca-cli.ts +++ b/src/main/ssh/ssh-remote-orca-cli.ts @@ -11,6 +11,13 @@ import { type RemoteOrcaCliResult } from './ssh-remote-cli-host-passthrough' import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error' +import { + optionalRemoteCliNumber, + optionalRemoteCliString, + parseRemoteCliArgs, + requiredRemoteCliString, + resolveRemoteCliHandle +} from './ssh-remote-cli-args' import { getRemoteLinearHelp, tryDispatchRemoteLinearCli } from './ssh-remote-linear-cli' import { getRemoteOrchestrationPayload, @@ -32,26 +39,6 @@ const HOST_INTERACTIVE_COMMANDS: Record<string, string> = { 'orca agent-teams-tmux is a tmux pane shim for the Orca host machine and cannot run through the SSH relay bridge.' } -const REMOTE_BOOLEAN_FLAGS = new Set([ - 'all', - 'attachments', - 'children', - 'comments', - 'current', - 'full', - 'help', - 'inject', - 'include-archived', - 'json', - 'me', - 'relations', - 'parent-current', - 'unread', - 'wait' -]) -const REPEATED_FLAG_SEPARATOR = '\u0000' -const REPEATABLE_REMOTE_STRING_FLAGS = new Set(['label']) - export async function runRemoteOrcaCli( runtime: OrcaRuntimeService, request: RemoteOrcaCliRequest, @@ -175,19 +162,19 @@ async function dispatchRemoteCli( } case 'terminal list': return await call(dispatcher, 'terminal.list', { - worktree: optionalString(parsed.flags, 'worktree'), - limit: optionalNumber(parsed.flags, 'limit') + worktree: optionalRemoteCliString(parsed.flags, 'worktree'), + limit: optionalRemoteCliNumber(parsed.flags, 'limit') }) case 'orchestration send': { - const type = optionalString(parsed.flags, 'type') + const type = optionalRemoteCliString(parsed.flags, 'type') return await call(dispatcher, 'orchestration.send', { from: resolveRemoteOrchestrationSender(parsed.flags, env, type), - to: requiredString(parsed.flags, 'to'), - subject: requiredString(parsed.flags, 'subject'), - body: optionalString(parsed.flags, 'body'), + to: requiredRemoteCliString(parsed.flags, 'to'), + subject: requiredRemoteCliString(parsed.flags, 'subject'), + body: optionalRemoteCliString(parsed.flags, 'body'), type, - priority: optionalString(parsed.flags, 'priority'), - threadId: optionalString(parsed.flags, 'thread-id'), + priority: optionalRemoteCliString(parsed.flags, 'priority'), + threadId: optionalRemoteCliString(parsed.flags, 'thread-id'), payload: getRemoteOrchestrationPayload(parsed.flags), // Why: the legacy in-process bridge must preserve the same pane // authority as the full host CLI passthrough. @@ -196,24 +183,24 @@ async function dispatchRemoteCli( } case 'orchestration check': return await call(dispatcher, 'orchestration.check', { - terminal: resolveHandle(parsed.flags, env, 'terminal'), + terminal: resolveRemoteCliHandle(parsed.flags, env, 'terminal'), unread: parsed.flags.has('unread') ? true : undefined, all: parsed.flags.has('all') ? true : undefined, - types: optionalString(parsed.flags, 'types'), + types: optionalRemoteCliString(parsed.flags, 'types'), inject: parsed.flags.has('inject') ? true : undefined, wait: parsed.flags.has('wait') ? true : undefined, - timeoutMs: optionalNumber(parsed.flags, 'timeout-ms') + timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms') }) case 'orchestration reply': return await call(dispatcher, 'orchestration.reply', { - id: requiredString(parsed.flags, 'id'), - body: requiredString(parsed.flags, 'body'), - from: resolveHandle(parsed.flags, env, 'from') + id: requiredRemoteCliString(parsed.flags, 'id'), + body: requiredRemoteCliString(parsed.flags, 'body'), + from: resolveRemoteCliHandle(parsed.flags, env, 'from') }) case 'orchestration inbox': return await call(dispatcher, 'orchestration.inbox', { - limit: optionalNumber(parsed.flags, 'limit'), - terminal: optionalString(parsed.flags, 'terminal') + limit: optionalRemoteCliNumber(parsed.flags, 'limit'), + terminal: optionalRemoteCliString(parsed.flags, 'terminal') }) default: // Why: only reachable when the full host CLI could not be launched; @@ -238,94 +225,6 @@ async function call( }) } -function parseRemoteCliArgs(argv: string[]): ParsedRemoteCli { - const commandPath: string[] = [] - const flags = new Map<string, string | boolean>() - for (let i = 0; i < argv.length; i += 1) { - const token = argv[i] - if (!token.startsWith('--')) { - commandPath.push(token) - continue - } - const assignment = token.slice(2) - // Why: the SSH relay-backed shim should accept the same `--flag=value` - // form as the local CLI, including values that themselves start with `--`. - const equalsIndex = assignment.indexOf('=') - if (equalsIndex !== -1) { - setRemoteFlag(flags, assignment.slice(0, equalsIndex), assignment.slice(equalsIndex + 1)) - continue - } - - const flag = assignment - const next = argv[i + 1] - if (!isRemoteBooleanFlag(flag, commandPath) && next && !next.startsWith('--')) { - setRemoteFlag(flags, flag, next) - i += 1 - } else { - setRemoteFlag(flags, flag, true) - } - } - return { commandPath, flags } -} - -function isRemoteBooleanFlag(flag: string, commandPath: string[]): boolean { - // Why: Android launch already uses --activity <name>; only Linear issue reads use it as a boolean. - return ( - REMOTE_BOOLEAN_FLAGS.has(flag) || - (flag === 'activity' && commandPath[0] === 'linear' && commandPath[1] === 'issue') - ) -} - -function setRemoteFlag( - flags: Map<string, string | boolean>, - name: string, - value: string | boolean -): void { - const previous = flags.get(name) - if ( - typeof previous === 'string' && - typeof value === 'string' && - REPEATABLE_REMOTE_STRING_FLAGS.has(name) - ) { - flags.set(name, `${previous}${REPEATED_FLAG_SEPARATOR}${value}`) - return - } - flags.set(name, value) -} - -function resolveHandle( - flags: Map<string, string | boolean>, - env: Record<string, string>, - flagName: string -): string { - return optionalString(flags, flagName) ?? env.ORCA_TERMINAL_HANDLE ?? 'unknown' -} - -function requiredString(flags: Map<string, string | boolean>, name: string): string { - const value = optionalString(flags, name) - if (!value) { - throw new Error(`Missing --${name}`) - } - return value -} - -function optionalString(flags: Map<string, string | boolean>, name: string): string | undefined { - const value = flags.get(name) - return typeof value === 'string' && value.length > 0 ? value : undefined -} - -function optionalNumber(flags: Map<string, string | boolean>, name: string): number | undefined { - const value = optionalString(flags, name) - if (value === undefined) { - return undefined - } - const parsed = Number(value) - if (!Number.isFinite(parsed)) { - throw new RemoteCliArgumentError('invalid_argument', `Invalid numeric value for --${name}`) - } - return parsed -} - function buildLocalError(message: string, code = 'runtime_error'): RpcResponse { return { id: 'remote-cli-local',