mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Add read-only orca linear CLI with trusted launch-prompt pointer (V1) (#5126)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
+40
-2
@@ -17,6 +17,38 @@ export type CommandSpec = {
|
||||
}
|
||||
|
||||
export const GLOBAL_FLAGS = ['help', 'json', 'pairing-code', 'environment']
|
||||
export const BOOLEAN_FLAGS = new Set([
|
||||
'all',
|
||||
'attachments',
|
||||
'children',
|
||||
'comments',
|
||||
'current',
|
||||
'dry-run',
|
||||
'enter',
|
||||
'focus',
|
||||
'force',
|
||||
'full',
|
||||
'help',
|
||||
'inject',
|
||||
'interrupt',
|
||||
'json',
|
||||
'messages',
|
||||
'mobile',
|
||||
'mobile-pairing',
|
||||
'no-pairing',
|
||||
'ready',
|
||||
'relations',
|
||||
'restore-window',
|
||||
'return-preamble',
|
||||
'run-hooks',
|
||||
'show-profile',
|
||||
'staged',
|
||||
'tasks',
|
||||
'text-stdin',
|
||||
'unread',
|
||||
'value-stdin',
|
||||
'wait'
|
||||
])
|
||||
|
||||
export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const commandPath: string[] = []
|
||||
@@ -40,6 +72,10 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
||||
}
|
||||
|
||||
const flag = assignment
|
||||
if (BOOLEAN_FLAGS.has(flag)) {
|
||||
flags.set(flag, true)
|
||||
continue
|
||||
}
|
||||
const hasNext = i + 1 < argv.length
|
||||
const next = argv[i + 1]
|
||||
if (!hasNext || next.startsWith('--')) {
|
||||
@@ -85,7 +121,8 @@ export function supportsBrowserPageFlag(commandPath: string[]): boolean {
|
||||
'computer',
|
||||
'emulator',
|
||||
'note',
|
||||
'diagnostics'
|
||||
'diagnostics',
|
||||
'linear'
|
||||
].includes(commandPath[0])
|
||||
) {
|
||||
return false
|
||||
@@ -123,7 +160,8 @@ export function isCommandGroup(commandPath: string[]): boolean {
|
||||
'emulator',
|
||||
'agent',
|
||||
'environment',
|
||||
'diagnostics'
|
||||
'diagnostics',
|
||||
'linear'
|
||||
].includes(commandPath[0])) ||
|
||||
(commandPath.length === 2 && commandPath[0] === 'agent' && commandPath[1] === 'hooks') ||
|
||||
(commandPath.length === 2 &&
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import { ENVIRONMENT_HANDLERS } from './handlers/environment'
|
||||
import { AGENT_HOOK_HANDLERS } from './handlers/agent-hooks'
|
||||
import { DIAGNOSTICS_HANDLERS } from './handlers/diagnostics'
|
||||
import { EMULATOR_HANDLERS } from './handlers/emulator'
|
||||
import { LINEAR_HANDLERS } from './handlers/linear'
|
||||
|
||||
export type HandlerContext = {
|
||||
flags: Map<string, string | boolean>
|
||||
@@ -53,7 +54,8 @@ function buildHandlers(): Map<string, CommandHandler> {
|
||||
COMPUTER_HANDLERS,
|
||||
AGENT_HOOK_HANDLERS,
|
||||
DIAGNOSTICS_HANDLERS,
|
||||
ENVIRONMENT_HANDLERS
|
||||
ENVIRONMENT_HANDLERS,
|
||||
LINEAR_HANDLERS
|
||||
]
|
||||
for (const group of groups) {
|
||||
for (const [key, handler] of Object.entries(group)) {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const callMock = vi.fn()
|
||||
|
||||
vi.mock('../runtime-client', () => {
|
||||
class RuntimeClient {
|
||||
readonly isRemote: boolean
|
||||
call = callMock
|
||||
getCliStatus = vi.fn()
|
||||
openOrca = vi.fn()
|
||||
|
||||
constructor(
|
||||
_userDataPath?: string,
|
||||
_requestTimeoutMs?: number,
|
||||
remotePairingCode = process.env.ORCA_PAIRING_CODE ?? null,
|
||||
environmentSelector = process.env.ORCA_ENVIRONMENT ?? null
|
||||
) {
|
||||
this.isRemote = Boolean(remotePairingCode || environmentSelector)
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeClientError extends Error {
|
||||
readonly code: string
|
||||
|
||||
constructor(code: string, message: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeRpcFailureError extends RuntimeClientError {
|
||||
readonly response: unknown
|
||||
|
||||
constructor(response: unknown) {
|
||||
super('runtime_error', 'runtime_error')
|
||||
this.response = response
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
RuntimeClient,
|
||||
RuntimeClientError,
|
||||
RuntimeRpcFailureError
|
||||
}
|
||||
})
|
||||
|
||||
import { main } from '../index'
|
||||
import { okFixture, queueFixtures } from '../test-fixtures'
|
||||
|
||||
describe('orca linear CLI handlers', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
callMock.mockReset()
|
||||
process.env = { ...originalEnv }
|
||||
// Why: these tests can run inside an Orca-managed terminal, which exports
|
||||
// real worktree/terminal/pairing env hints; clear them so handler context
|
||||
// assertions stay deterministic.
|
||||
delete process.env.ORCA_WORKTREE_ID
|
||||
delete process.env.ORCA_TERMINAL_HANDLE
|
||||
delete process.env.ORCA_PAIRING_CODE
|
||||
delete process.env.ORCA_ENVIRONMENT
|
||||
process.exitCode = undefined
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('maps --full issue reads to read-only issueContext RPC', async () => {
|
||||
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
||||
|
||||
await main(['linear', 'issue', 'ENG-123', '--full', '--json'], '/tmp/repo')
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith(
|
||||
'linear.issueContext',
|
||||
{
|
||||
input: 'ENG-123',
|
||||
current: false,
|
||||
workspaceId: undefined,
|
||||
include: {
|
||||
comments: true,
|
||||
children: true,
|
||||
attachments: true,
|
||||
relations: true
|
||||
},
|
||||
depth: 2,
|
||||
context: {
|
||||
remote: false,
|
||||
cwd: '/tmp/repo'
|
||||
}
|
||||
},
|
||||
{ timeoutMs: 120_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps global boolean flags before Linear commands from consuming command tokens', async () => {
|
||||
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
||||
|
||||
await main(['--json', 'linear', 'issue', 'ENG-123', '--full'], '/tmp/repo')
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith(
|
||||
'linear.issueContext',
|
||||
expect.objectContaining({
|
||||
input: 'ENG-123',
|
||||
include: expect.objectContaining({
|
||||
comments: true,
|
||||
children: true,
|
||||
attachments: true,
|
||||
relations: true
|
||||
})
|
||||
}),
|
||||
{ timeoutMs: 120_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('passes verified current-context hints without resolving cwd for remote runtimes', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_123'
|
||||
process.env.ORCA_WORKTREE_ID = 'repo::/srv/app'
|
||||
process.env.ORCA_PAIRING_CODE = 'orca://pair?payload=bad'
|
||||
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
||||
|
||||
await main(['linear', 'issue', '--current', '--comments', '--json'], '/client/repo')
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith(
|
||||
'linear.issueContext',
|
||||
expect.objectContaining({
|
||||
input: undefined,
|
||||
current: true,
|
||||
include: expect.objectContaining({ comments: true }),
|
||||
context: {
|
||||
remote: true,
|
||||
worktreeId: 'repo::/srv/app',
|
||||
terminalHandle: 'term_123'
|
||||
}
|
||||
}),
|
||||
{ timeoutMs: undefined }
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects --depth unless children are requested', async () => {
|
||||
await main(['linear', 'issue', 'ENG-123', '--depth', '3'], '/tmp/repo')
|
||||
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(console.error).mock.calls[0][0]).toContain(
|
||||
'--depth requires --children or --full'
|
||||
)
|
||||
expect(process.exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it('maps search to agent search RPC with capped limit', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_search', {
|
||||
issues: [],
|
||||
meta: { query: 'auth', workspaceId: 'all', limit: 50, returned: 0, limitReached: false }
|
||||
})
|
||||
)
|
||||
|
||||
await main(['linear', 'search', 'auth', '--workspace', 'all', '--limit', '500'], '/tmp/repo')
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith('linear.agentSearchIssues', {
|
||||
query: 'auth',
|
||||
limit: 50,
|
||||
workspaceId: 'all'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps boolean flags between Linear and search from consuming the subcommand', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_search', {
|
||||
issues: [],
|
||||
meta: { query: 'auth', workspaceId: undefined, limit: 1, returned: 0, limitReached: false }
|
||||
})
|
||||
)
|
||||
|
||||
await main(['linear', '--json', 'search', 'auth', '--limit', '1'], '/tmp/repo')
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith('linear.agentSearchIssues', {
|
||||
query: 'auth',
|
||||
limit: 1,
|
||||
workspaceId: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function issueResult(): unknown {
|
||||
return {
|
||||
issue: {
|
||||
id: 'issue-id',
|
||||
identifier: 'ENG-123',
|
||||
title: 'Fix auth',
|
||||
url: 'https://linear.app/acme/issue/ENG-123',
|
||||
state: { name: 'Todo' },
|
||||
team: { name: 'Engineering' },
|
||||
labels: []
|
||||
},
|
||||
meta: {
|
||||
requested: {
|
||||
current: false,
|
||||
include: { comments: false, children: false, attachments: false, relations: false },
|
||||
depth: 2
|
||||
},
|
||||
resolved: {
|
||||
id: 'issue-id',
|
||||
identifier: 'ENG-123',
|
||||
workspaceId: 'workspace-1',
|
||||
workspaceName: 'Acme'
|
||||
},
|
||||
partial: false,
|
||||
includeErrors: [],
|
||||
sections: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type {
|
||||
LinearIssueContextResult,
|
||||
LinearIssueInclude,
|
||||
LinearIssueRequest,
|
||||
LinearSearchResult
|
||||
} from '../../shared/linear-agent-access'
|
||||
import {
|
||||
LINEAR_CHILDREN_MAX_DEPTH,
|
||||
clampLinearIssueDepth,
|
||||
clampLinearSearchLimit
|
||||
} from '../../shared/linear-agent-access'
|
||||
import type { CommandHandler } from '../dispatch'
|
||||
import { printResult } from '../format'
|
||||
import {
|
||||
getOptionalNonNegativeIntegerFlag,
|
||||
getOptionalPositiveIntegerFlag,
|
||||
getOptionalStringFlag,
|
||||
getRequiredStringFlag
|
||||
} from '../flags'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
import {
|
||||
formatLinearIssue,
|
||||
formatLinearSearch,
|
||||
printLinearIssueWarnings,
|
||||
printLinearSearchWarnings
|
||||
} from '../linear-format'
|
||||
|
||||
const ISSUE_CONTEXT_TIMEOUT_MS = 120_000
|
||||
|
||||
export const LINEAR_HANDLERS: Record<string, CommandHandler> = {
|
||||
'linear issue': async ({ flags, client, cwd, json }) => {
|
||||
const request = buildIssueRequest(flags, cwd, client.isRemote)
|
||||
const response = await client.call<LinearIssueContextResult>('linear.issueContext', request, {
|
||||
timeoutMs: flags.get('full') === true ? ISSUE_CONTEXT_TIMEOUT_MS : undefined
|
||||
})
|
||||
if (!json) {
|
||||
printLinearIssueWarnings(response.result)
|
||||
}
|
||||
printResult(response, json, formatLinearIssue)
|
||||
},
|
||||
'linear search': async ({ flags, client, json }) => {
|
||||
const limit = clampLinearSearchLimit(getOptionalPositiveIntegerFlag(flags, 'limit'))
|
||||
const response = await client.call<LinearSearchResult>('linear.agentSearchIssues', {
|
||||
query: getRequiredStringFlag(flags, 'query'),
|
||||
limit,
|
||||
workspaceId: getOptionalStringFlag(flags, 'workspace')
|
||||
})
|
||||
if (!json) {
|
||||
printLinearSearchWarnings(response.result)
|
||||
}
|
||||
printResult(response, json, formatLinearSearch)
|
||||
}
|
||||
}
|
||||
|
||||
function buildIssueRequest(
|
||||
flags: Map<string, string | boolean>,
|
||||
cwd: string,
|
||||
remote: boolean
|
||||
): LinearIssueRequest {
|
||||
const full = flags.get('full') === true
|
||||
const includes: Record<LinearIssueInclude, boolean> = {
|
||||
comments: full || flags.get('comments') === true,
|
||||
children: full || flags.get('children') === true,
|
||||
attachments: full || flags.get('attachments') === true,
|
||||
relations: full || flags.get('relations') === true
|
||||
}
|
||||
if (flags.has('depth') && !includes.children) {
|
||||
throw new RuntimeClientError('invalid_argument', '--depth requires --children or --full')
|
||||
}
|
||||
const requestedDepth = getOptionalNonNegativeIntegerFlag(flags, 'depth')
|
||||
if (requestedDepth !== undefined && requestedDepth > LINEAR_CHILDREN_MAX_DEPTH) {
|
||||
throw new RuntimeClientError(
|
||||
'invalid_argument',
|
||||
`--depth must be at most ${LINEAR_CHILDREN_MAX_DEPTH}`
|
||||
)
|
||||
}
|
||||
const workspaceId = getOptionalStringFlag(flags, 'workspace')
|
||||
if (workspaceId === 'all') {
|
||||
throw new RuntimeClientError(
|
||||
'linear_invalid_workspace',
|
||||
'--workspace all is not valid for issue'
|
||||
)
|
||||
}
|
||||
const input = getOptionalStringFlag(flags, 'id')
|
||||
return {
|
||||
input,
|
||||
current: input ? false : flags.get('current') === true,
|
||||
workspaceId,
|
||||
include: includes,
|
||||
depth: clampLinearIssueDepth(requestedDepth),
|
||||
context: {
|
||||
remote,
|
||||
...(remote ? {} : { cwd }),
|
||||
...(process.env.ORCA_WORKTREE_ID ? { worktreeId: process.env.ORCA_WORKTREE_ID } : {}),
|
||||
...(process.env.ORCA_TERMINAL_HANDLE
|
||||
? { terminalHandle: process.env.ORCA_TERMINAL_HANDLE }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,9 @@ Computer Use:
|
||||
computer paste-text Paste text through the native clipboard path
|
||||
computer set-value Set the value of a settable app element
|
||||
|
||||
Linear:
|
||||
linear Read Linear ticket context for agents
|
||||
|
||||
Mobile Emulator (iOS Simulator):
|
||||
emulator list List available/running emulators (Orca-managed + raw serve-sim)
|
||||
emulator attach <device> Attach/start helper and make active for the worktree
|
||||
@@ -356,6 +359,18 @@ export function formatGroupHelp(specs: CommandSpec[], group: string): string {
|
||||
|
||||
function formatCommandFlagHelp(flag: string, commandPath: string[]): string {
|
||||
const command = commandPath.join(' ')
|
||||
if (command === 'linear issue' && flag === 'id') {
|
||||
return '--id <id> Linear issue key, id, or URL'
|
||||
}
|
||||
if (command === 'linear issue' && flag === 'workspace') {
|
||||
return '--workspace <id> Connected Linear workspace id'
|
||||
}
|
||||
if (command === 'linear search' && flag === 'query') {
|
||||
return '--query <text> Text to search across Linear issues'
|
||||
}
|
||||
if (command === 'linear search' && flag === 'workspace') {
|
||||
return '--workspace <id|all> Connected Linear workspace id, or all'
|
||||
}
|
||||
if (flag === 'key' && command === 'computer hotkey') {
|
||||
return '--key <key-combo> Modifier chord with one key, e.g. CmdOrCtrl+A'
|
||||
}
|
||||
@@ -459,5 +474,27 @@ export function formatFlagHelp(flag: string): string {
|
||||
format: '--format <png|jpeg> Screenshot image format'
|
||||
}
|
||||
|
||||
if (flag === 'current') {
|
||||
return '--current Use the current Orca worktree linked Linear issue'
|
||||
}
|
||||
if (flag === 'comments') {
|
||||
return '--comments Include threaded Linear comments'
|
||||
}
|
||||
if (flag === 'children') {
|
||||
return '--children Include recursive child issues'
|
||||
}
|
||||
if (flag === 'depth') {
|
||||
return '--depth <n> Child issue depth for --children/--full'
|
||||
}
|
||||
if (flag === 'attachments') {
|
||||
return '--attachments Include attachment metadata and URLs'
|
||||
}
|
||||
if (flag === 'relations') {
|
||||
return '--relations Include blocking, related, and duplicate links'
|
||||
}
|
||||
if (flag === 'full') {
|
||||
return '--full Include all supported V1 issue context within caps'
|
||||
}
|
||||
|
||||
return helpByFlag[flag] ?? `--${flag}`
|
||||
}
|
||||
|
||||
@@ -143,6 +143,47 @@ describe('orca root help', () => {
|
||||
)
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('progressively discloses Linear commands', async () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['--help'], '/tmp/repo')
|
||||
|
||||
const rootHelp = String(logSpy.mock.calls[0][0])
|
||||
expect(rootHelp).toContain('Linear:')
|
||||
expect(rootHelp).toContain('linear Read Linear ticket context for agents')
|
||||
expect(rootHelp).not.toContain('linear issue')
|
||||
expect(rootHelp).not.toContain('linear search')
|
||||
|
||||
logSpy.mockClear()
|
||||
await main(['linear', '--help'], '/tmp/repo')
|
||||
|
||||
const groupHelp = String(logSpy.mock.calls[0][0])
|
||||
expect(groupHelp).toContain('orca linear')
|
||||
expect(groupHelp).toContain('issue')
|
||||
expect(groupHelp).toContain('search')
|
||||
expect(groupHelp).not.toContain('--comments')
|
||||
expect(groupHelp).not.toContain('--attachments')
|
||||
|
||||
logSpy.mockClear()
|
||||
await main(['linear', 'issue', '--help'], '/tmp/repo')
|
||||
|
||||
const issueHelp = String(logSpy.mock.calls[0][0])
|
||||
expect(issueHelp).toContain('orca linear issue [<id>]')
|
||||
expect(issueHelp).toContain('--comments Include threaded Linear comments')
|
||||
expect(issueHelp).toContain('--attachments Include attachment metadata and URLs')
|
||||
expect(issueHelp).toContain('--workspace <id> Connected Linear workspace id')
|
||||
expect(issueHelp).toContain('--id <id> Linear issue key, id, or URL')
|
||||
|
||||
logSpy.mockClear()
|
||||
await main(['linear', 'search', '--help'], '/tmp/repo')
|
||||
|
||||
const searchHelp = String(logSpy.mock.calls[0][0])
|
||||
expect(searchHelp).toContain('orca linear search <query>')
|
||||
expect(searchHelp).toContain('--workspace <id|all> Connected Linear workspace id, or all')
|
||||
expect(searchHelp).toContain('--query <text> Text to search across Linear issues')
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('orca cli worktree awareness', () => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LinearSearchResult } from '../shared/linear-agent-access'
|
||||
import { printLinearSearchWarnings } from './linear-format'
|
||||
|
||||
describe('linear-format', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('treats older search results without workspaceErrors as non-partial', () => {
|
||||
const result = {
|
||||
issues: [],
|
||||
meta: {
|
||||
query: 'auth',
|
||||
workspaceId: 'all',
|
||||
limit: 20,
|
||||
returned: 0,
|
||||
limitReached: false,
|
||||
partial: false
|
||||
}
|
||||
} as unknown as LinearSearchResult
|
||||
|
||||
printLinearSearchWarnings(result)
|
||||
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import type {
|
||||
LinearIssueContextResult,
|
||||
LinearSearchIssueSummary,
|
||||
LinearSearchResult
|
||||
} from '../shared/linear-agent-access'
|
||||
|
||||
export function formatLinearIssue(result: LinearIssueContextResult): string {
|
||||
const issue = result.issue
|
||||
const lines = [
|
||||
`${issue.identifier} ${issue.title}`,
|
||||
`URL: ${issue.url}`,
|
||||
`State: ${issue.state?.name ?? 'unknown'}`,
|
||||
`Assignee: ${issue.assignee?.displayName ?? 'unassigned'}`,
|
||||
`Project: ${issue.project?.name ?? 'none'}`
|
||||
]
|
||||
if (issue.labels.length > 0) {
|
||||
lines.push(
|
||||
`Labels: ${issue.labels
|
||||
.map((label) => label.name)
|
||||
.filter(Boolean)
|
||||
.join(', ')}`
|
||||
)
|
||||
}
|
||||
const sections = result.meta.sections
|
||||
if (sections.comments) {
|
||||
lines.push(`Comments: ${sections.comments.returned}`)
|
||||
}
|
||||
if (sections.children) {
|
||||
lines.push(`Children: ${sections.children.returned}`)
|
||||
}
|
||||
if (sections.attachments) {
|
||||
lines.push(`Attachments: ${sections.attachments.returned}`)
|
||||
}
|
||||
if (sections.relations) {
|
||||
lines.push(`Relations: ${sections.relations.returned}`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function formatLinearSearch(result: LinearSearchResult): string {
|
||||
if (result.issues.length === 0) {
|
||||
return 'No Linear issues found.'
|
||||
}
|
||||
return result.issues.map(formatSearchRow).join('\n')
|
||||
}
|
||||
|
||||
export function printLinearIssueWarnings(result: LinearIssueContextResult): void {
|
||||
for (const error of result.meta.includeErrors) {
|
||||
console.error(`warning: ${error.include} unavailable: ${error.message}`)
|
||||
}
|
||||
for (const [name, meta] of Object.entries(result.meta.sections)) {
|
||||
if (meta?.capReached) {
|
||||
console.error(`warning: ${name} capped at ${meta.returned}/${meta.cap}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function printLinearSearchWarnings(result: LinearSearchResult): void {
|
||||
if (result.meta.limitReached) {
|
||||
console.error(`warning: showing first ${result.meta.returned} Linear issues`)
|
||||
}
|
||||
for (const error of result.meta.workspaceErrors ?? []) {
|
||||
console.error(
|
||||
`warning: ${error.workspace.name} unavailable for Linear search: ${error.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function formatSearchRow(issue: LinearSearchIssueSummary): string {
|
||||
const state = issue.state?.name ?? 'unknown'
|
||||
const assignee = issue.assignee?.displayName ?? 'unassigned'
|
||||
return `${issue.identifier.padEnd(10)} ${state.padEnd(14)} ${assignee.padEnd(18)} ${issue.title}`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ENVIRONMENT_COMMAND_SPECS } from './environment'
|
||||
import { AGENT_HOOK_COMMAND_SPECS } from './agent-hooks'
|
||||
import { DIAGNOSTICS_COMMAND_SPECS } from './diagnostics'
|
||||
import { EMULATOR_COMMAND_SPECS } from './emulator'
|
||||
import { LINEAR_COMMAND_SPECS } from './linear'
|
||||
|
||||
export const COMMAND_SPECS: CommandSpec[] = [
|
||||
...CORE_COMMAND_SPECS,
|
||||
@@ -22,5 +23,6 @@ export const COMMAND_SPECS: CommandSpec[] = [
|
||||
...AGENT_HOOK_COMMAND_SPECS,
|
||||
...DIAGNOSTICS_COMMAND_SPECS,
|
||||
...ENVIRONMENT_COMMAND_SPECS,
|
||||
...LINEAR_COMMAND_SPECS,
|
||||
...EMULATOR_COMMAND_SPECS
|
||||
]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { CommandSpec } from '../args'
|
||||
import { GLOBAL_FLAGS } from '../args'
|
||||
|
||||
export const LINEAR_COMMAND_SPECS: CommandSpec[] = [
|
||||
{
|
||||
path: ['linear', 'issue'],
|
||||
summary: 'Read Linear issue context for agents',
|
||||
usage:
|
||||
'orca linear issue [<id>] [--current] [--comments] [--children] [--depth <n>] [--attachments] [--relations] [--full] [--workspace <id>] [--json]',
|
||||
allowedFlags: [
|
||||
...GLOBAL_FLAGS,
|
||||
'current',
|
||||
'comments',
|
||||
'children',
|
||||
'depth',
|
||||
'attachments',
|
||||
'relations',
|
||||
'full',
|
||||
'workspace',
|
||||
'id'
|
||||
],
|
||||
positionalArgs: ['id'],
|
||||
examples: [
|
||||
'orca linear issue ENG-123',
|
||||
'orca linear issue --current --comments',
|
||||
'orca linear issue https://linear.app/acme/issue/ENG-123 --full --json'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['linear', 'search'],
|
||||
summary: 'Search connected Linear workspaces',
|
||||
usage: 'orca linear search <query> [--limit <n>] [--workspace <id>|all] [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'limit', 'workspace', 'query'],
|
||||
positionalArgs: ['query'],
|
||||
examples: ['orca linear search "auth bug"', 'orca linear search ENG --workspace all --json']
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user