Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
adc7455
feat(usage): add Kiro subscription credits
guanbear Aug 12, 2026
1bc0fa1
fix(usage): distinguish compact Kiro label
guanbear Aug 12, 2026
04eb691
fix(usage): use compact Kiro Q label
guanbear Aug 12, 2026
bba016d
fix(usage): avoid inventing Kiro reset time
guanbear Aug 14, 2026
10affdc
fix(usage): follow split shared UI types
guanbear Aug 14, 2026
136ae26
fix(usage): keep non-date Kiro reset descriptions
guanbear Aug 14, 2026
3fcd012
fix(usage): launch Kiro .cmd shims through cmd.exe
guanbear Aug 14, 2026
9996044
fix(usage): align provider search with current lint gates
guanbear Aug 15, 2026
2118b79
test(usage): mock the Kiro fetcher in the Antigravity service test
guanbear Aug 24, 2026
e685ce3
fix(usage): thread the reset-countdown clock into the Kiro reset label
guanbear Aug 31, 2026
651b4a6
fix(usage): resolve Kiro review findings
guanbear Sep 2, 2026
b28b50e
fix(usage): complete Kiro rebase integration
guanbear Sep 28, 2026
1fcf401
Merge remote-tracking branch 'upstream/main' into codex/pr-14001-repair
guanbear Sep 28, 2026
2a1ab9d
fix(usage): narrow Kiro CLI errors without assertions
guanbear Sep 28, 2026
0149ead
Merge remote-tracking branch 'upstream/main' into codex/pr-14001-repair
guanbear Sep 29, 2026
bf31aa9
Merge Orca main into Kiro usage PR and resolve provider conflicts
w-felipe360 Oct 8, 2026
2ef26e2
Merge branch 'stablyai:main' into fix/kiro-pr-14001-main-conflicts
w-felipe360 Oct 8, 2026
1d8d237
fix(usage): detect configured Kiro CLI and use shared process runner
w-felipe360 Oct 8, 2026
92add40
Merge commit 'e3639ef8d69209b8278b41c0e722811960ea7ec0' into fix/kiro…
w-felipe360 Oct 8, 2026
b850900
fix(usage): accept Kiro reset descriptions without ISO dates
w-felipe360 Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/main/ipc/preflight-agent-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,54 @@ describe('preflight', () => {
})

afterEach(() => {
vi.unstubAllEnvs()
Object.defineProperty(process, 'platform', {
configurable: true,
value: originalPlatform
})
})

it('detects Kiro through its configured local command outside PATH', async () => {
const command = '/custom/bin/kiro-cli'
vi.stubEnv('KIRO_CLI_PATH', command)
execFileAsyncMock.mockImplementation(async (program) => {
if (program !== command) {
throw new Error('not found')
}
return { stdout: 'kiro-cli 1.0', stderr: '' }
})

await expect(detectInstalledAgents()).resolves.toContain('kiro')
expect(execFileAsyncMock).toHaveBeenCalledWith(command, ['--version'], expect.anything())
})

it('does not detect Kiro when its configured command fails, even if PATH has another copy', async () => {
const command = '/custom/bin/broken-kiro-cli'
vi.stubEnv('KIRO_CLI_PATH', command)
execFileAsyncMock.mockImplementation(async (program, args) => {
if (program === 'which' && args[0] === 'kiro-cli') {
return { stdout: '/usr/bin/kiro-cli\n', stderr: '' }
}
throw new Error('not found')
})

await expect(detectInstalledAgents()).resolves.not.toContain('kiro')
})

it('does not apply the local Kiro command to WSL detection', async () => {
vi.stubEnv('KIRO_CLI_PATH', 'C:\\custom\\kiro-cli.cmd')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
runWslProcessMock.mockResolvedValue({
code: 0,
stdout: '',
stderr: '',
timedOut: false
})

await expect(detectInstalledAgents({ wslDistro: 'Ubuntu' })).resolves.not.toContain('kiro')
expect(execFileAsyncMock).not.toHaveBeenCalled()
})

it('only reports agents when which/where resolves to a real executable path', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
Expand Down
15 changes: 12 additions & 3 deletions src/main/preflight/agent-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { invalidateWslGuestEnvironment } from '../wsl/wsl-guest-environment'
import { prunePreflightWslCache } from '../preflight-wsl-cache'
import { detectAgentCommandsOnHost } from './agent-command-detection'
import { resolveKiroCommand } from '../rate-limits/kiro-usage-fetcher'
export { detectAgentCommandsOnHost } from './agent-command-detection'

export type PreflightStatus = {
Expand Down Expand Up @@ -145,15 +146,23 @@ async function detectCommandRuntime(
}

export async function detectInstalledAgents(context?: PreflightRuntimeContext): Promise<string[]> {
const wslTarget = getPreflightWslTarget(context)
const commands = getTuiAgentDetectionProbeCommands(
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
getPreflightWslTarget(context) ? 'wsl' : process.platform
wslTarget ? 'wsl' : process.platform
)
return resolveDetectedTuiAgentIds(
const detected = resolveDetectedTuiAgentIds(
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
await detectAgentCommandsOnHost(commands, { context }),
getPreflightWslTarget(context) ? 'wsl' : process.platform
wslTarget ? 'wsl' : process.platform
)
if (!wslTarget && process.env.KIRO_CLI_PATH?.trim()) {
const kiro = await findRunnableLocalCommand(resolveKiroCommand())
return kiro.status === 'available'
? [...new Set([...detected, 'kiro'])]
: detected.filter((id) => id !== 'kiro')
}
return detected
}

export async function detectInstalledAgentsWithShellPathHydration(
Expand Down
137 changes: 137 additions & 0 deletions src/main/rate-limits/kiro-usage-fetcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, it, vi } from 'vitest'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runProcess } from '../../shared/child-process/run-process'
import { fetchKiroRateLimits, parseKiroUsageOutput, resolveKiroCommand } from './kiro-usage-fetcher'

vi.mock('../../shared/child-process/run-process', () => ({ runProcess: vi.fn() }))

const KIRO_USAGE_OUTPUT =
'Estimated Usage | resets on 2026-09-01 | KIRO PRO\nCredits (10 of 100 covered in plan)\n10%'

describe('Kiro usage fetcher', () => {
it('uses the shared CLI resolver for version-manager installs', () => {
const homePath = mkdtempSync(join(tmpdir(), 'orca-kiro-command-'))
const command = join(homePath, '.volta', 'bin', 'kiro-cli')
mkdirSync(join(homePath, '.volta', 'bin'), { recursive: true })
writeFileSync(command, '#!/bin/sh\n')
chmodSync(command, 0o755)
vi.stubEnv('KIRO_CLI_PATH', '')
try {
expect(resolveKiroCommand({ platform: 'darwin', pathEnv: '', homePath })).toBe(command)
} finally {
vi.unstubAllEnvs()
rmSync(homePath, { recursive: true, force: true })
}
})

it('parses the official CLI usage command output', () => {
const result = parseKiroUsageOutput(`
\u001b[1mEstimated Usage | resets on 2026-09-01 | KIRO PRO+\u001b[0m
Credits (1233.74 of 2000 covered in plan)
\u001b[32m████ 61%\u001b[0m
`)

expect(result).toMatchObject({ provider: 'kiro', status: 'ok', planType: 'KIRO PRO+' })
expect(result.monthly?.usedPercent).toBeCloseTo(61.687)
expect(result.monthly).toMatchObject({
resetsAt: null,
resetDescription: '2026-09-01'
})
})

it('accepts a Kiro reset description without an ISO date', () => {
const output =
'Estimated Usage | resets on 01/01 | KIRO PRO\nCredits (10 of 100 covered in plan)\n10%'

expect(parseKiroUsageOutput(output)).toMatchObject({
status: 'ok',
planType: 'KIRO PRO',
monthly: { usedPercent: 10, resetsAt: null, resetDescription: '01/01' }
})
})

it('runs the local non-model usage command', async () => {
const runner = vi.fn().mockResolvedValue({
stdout: KIRO_USAGE_OUTPUT,
stderr: ''
})

const result = await fetchKiroRateLimits({ command: '/tmp/kiro-cli', runner })

expect(runner).toHaveBeenCalledWith(
'/tmp/kiro-cli',
['chat', '/usage', '--no-interactive', '--wrap', 'never'],
undefined
)
expect(result.status).toBe('ok')
})

it('passes configured .cmd shims to the shared process runner', async () => {
vi.mocked(runProcess).mockResolvedValueOnce({
code: 0,
signal: null,
stdout: KIRO_USAGE_OUTPUT,
stderr: '',
timedOut: false
})

const result = await fetchKiroRateLimits({ command: 'C:\\Users\\me\\bin\\kiro-cli.cmd' })

expect(runProcess).toHaveBeenCalledWith(
expect.objectContaining({
program: 'C:\\Users\\me\\bin\\kiro-cli.cmd',
args: ['chat', '/usage', '--no-interactive', '--wrap', 'never'],
timeoutMs: 20_000,
maxOutputBytes: 1024 * 1024,
killOnOutputLimit: true
})
)
expect(result.status).toBe('ok')
})

it('rejects partial usage output from a failed command', async () => {
vi.mocked(runProcess).mockResolvedValueOnce({
code: 1,
signal: null,
stdout: KIRO_USAGE_OUTPUT,
stderr: '',
timedOut: false
})

await expect(fetchKiroRateLimits({ command: '/tmp/kiro-cli' })).resolves.toMatchObject({
status: 'error',
usageMetadata: { failureKind: 'usage-unavailable' }
})
})

it('fails closed on unexpected output', () => {
expect(parseKiroUsageOutput('Please sign in first')).toMatchObject({
provider: 'kiro',
status: 'error',
usageMetadata: { failureKind: 'parse' }
})
})

it('reports a missing CLI without exposing process details', async () => {
const error = Object.assign(new Error('spawn /secret/path ENOENT'), { code: 'ENOENT' })
const result = await fetchKiroRateLimits({ runner: vi.fn().mockRejectedValue(error) })

expect(result).toMatchObject({ status: 'unavailable', error: 'Kiro CLI is not installed' })
expect(JSON.stringify(result)).not.toContain('/secret/path')
})

it('distinguishes a failed usage command from a missing CLI', async () => {
const result = await fetchKiroRateLimits({
runner: vi.fn().mockRejectedValue(new Error('command timed out'))
})

expect(result).toMatchObject({
status: 'error',
error: 'Kiro usage command failed',
usageMetadata: { failureKind: 'usage-unavailable' }
})
expect(JSON.stringify(result)).not.toContain('command timed out')
})
})
127 changes: 127 additions & 0 deletions src/main/rate-limits/kiro-usage-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences'
import { resolveCliCommand, withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution'
import { runProcess } from '../../shared/child-process/run-process'
import type { ProviderRateLimits, RateLimitWindow } from '../../shared/rate-limit-types'

const CLI_TIMEOUT_MS = 20_000
const MONTHLY_WINDOW_MINUTES = 43_200

type CommandResult = { stdout: string; stderr: string }
type CommandRunner = (
command: string,
args: string[],
signal?: AbortSignal
) => Promise<CommandResult>

function result(
status: ProviderRateLimits['status'],
error: string | null,
monthly: RateLimitWindow | null = null,
planType: string | null = null,
failureKind?: 'cli-unavailable' | 'parse' | 'usage-unavailable'
): ProviderRateLimits {
return {
provider: 'kiro',
session: null,
weekly: null,
monthly,
planType,
updatedAt: Date.now(),
error,
status,
usageMetadata: {
source: 'cli',
...(failureKind ? { failureKind } : {})
}
}
}

export function resolveKiroCommand(
options: {
pathEnv?: string | null
platform?: NodeJS.Platform
homePath?: string
} = {}
): string {
const configured = process.env.KIRO_CLI_PATH?.trim()
if (configured) {
return configured
}
return resolveCliCommand('kiro-cli', options)
}

const runCommand: CommandRunner = async (command, args, signal) => {
const result = await runProcess({
program: command,
args,
env: withCliRuntimeOnPath(command, { ...process.env }),
timeoutMs: CLI_TIMEOUT_MS,
maxOutputBytes: 1024 * 1024,
killOnOutputLimit: true,
signal
})
if (result.code !== 0 || result.timedOut || result.signal || result.outputTruncated) {
throw new Error('Kiro usage command failed')
}
return { stdout: result.stdout, stderr: result.stderr }
}

export function parseKiroUsageOutput(output: string): ProviderRateLimits {
const readable = stripAnsiEscapeSequences(output).replace(/\r/g, '')
const header = readable.match(/Estimated Usage\s*\|\s*resets on\s+([^|\n]+?)\s*\|\s*([^\n]+)/i)
const resetDescription = header?.[1].trim()
const credits = readable.match(/Credits\s*\(([\d,.]+)\s+of\s+([\d,.]+)\s+covered in plan\)/i)
const percent = readable.match(/(?:^|\s)(\d+(?:\.\d+)?)%\s*$/m)
const used = credits ? Number(credits[1].replaceAll(',', '')) : Number.NaN
const limit = credits ? Number(credits[2].replaceAll(',', '')) : Number.NaN
const usedPercent =
Number.isFinite(used) && Number.isFinite(limit) && limit > 0
? Math.min(100, Math.max(0, (used / limit) * 100))
: percent
? Math.min(100, Math.max(0, Number(percent[1])))
: null
if (!header || !resetDescription || usedPercent === null || !Number.isFinite(usedPercent)) {
return result('error', 'Could not parse Kiro usage output', null, null, 'parse')
}
return result(
'ok',
null,
{
usedPercent,
windowMinutes: MONTHLY_WINDOW_MINUTES,
// Kiro supplies no exact reset time or zone, so keep its text as display metadata.
resetsAt: null,
resetDescription
},
header[2].trim()
)
}

export async function fetchKiroRateLimits(
options: { signal?: AbortSignal; runner?: CommandRunner; command?: string } = {}
): Promise<ProviderRateLimits> {
try {
const { stdout, stderr } = await (options.runner ?? runCommand)(
options.command ?? resolveKiroCommand(),
['chat', '/usage', '--no-interactive', '--wrap', 'never'],
options.signal
)
return parseKiroUsageOutput(`${stdout}\n${stderr}`)
} catch (error) {
const code =
error !== null &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string'
? error.code
: null
const unavailable = code === 'ENOENT'
return result(
unavailable ? 'unavailable' : 'error',
unavailable ? 'Kiro CLI is not installed' : 'Kiro usage command failed',
null,
null,
unavailable ? 'cli-unavailable' : 'usage-unavailable'
)
}
}
3 changes: 3 additions & 0 deletions src/main/rate-limits/rate-limit-service-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { readGrokAuthSession } from './grok-auth'
import { fetchCursorRateLimits } from './cursor-fetcher'
import { readCursorAuthSession } from './cursor-auth'
import { fetchOpenCodeGoUsage } from './opencode-go-usage-source-selection'
import { fetchKiroRateLimits } from './kiro-usage-fetcher'
import { fetchZcodeRateLimits } from './zcode-usage-fetcher'
import { fetchAntigravityRateLimits } from './antigravity-usage-fetcher'
import { hasMiniMaxSessionCookie } from '../minimax/minimax-cookie-store'
Expand Down Expand Up @@ -94,6 +95,7 @@ export function mockFreshBackgroundProviderFetches(): void {
vi.mocked(fetchMiniMaxRateLimits).mockImplementation(async () => okProvider('minimax', 0))
vi.mocked(fetchGrokRateLimits).mockImplementation(async () => unavailableProvider('grok'))
vi.mocked(fetchCursorRateLimits).mockImplementation(async () => unavailableProvider('cursor'))
vi.mocked(fetchKiroRateLimits).mockImplementation(async () => unavailableProvider('kiro'))
vi.mocked(fetchZcodeRateLimits).mockImplementation(async () => unavailableProvider('zcode'))
vi.mocked(fetchAntigravityRateLimits).mockImplementation(async () =>
unavailableProvider('antigravity')
Expand All @@ -116,6 +118,7 @@ export function resetRateLimitProviderMocks(): void {
status: 'unavailable'
})
vi.mocked(fetchCursorRateLimits).mockResolvedValue(unavailableProvider('cursor'))
vi.mocked(fetchKiroRateLimits).mockResolvedValue(unavailableProvider('kiro'))
vi.mocked(fetchZcodeRateLimits).mockResolvedValue(unavailableProvider('zcode'))
vi.mocked(fetchAntigravityRateLimits).mockResolvedValue(unavailableProvider('antigravity'))
vi.mocked(hasMiniMaxSessionCookie).mockReturnValue(false)
Expand Down
4 changes: 4 additions & 0 deletions src/main/rate-limits/service-account-target-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./kiro-usage-fetcher', () => ({
fetchKiroRateLimits: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))
Expand Down
Loading
Loading