Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .claude/skills/aeon/references/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ cd apps/dashboard && npm ci && npm run typecheck && npm run lint && npm test &&
cd apps/cli && npm ci && npm run typecheck && npm run lint # needs apps/dashboard deps installed first
cd apps/mcp-server && npm install && npm run build
cd apps/webhook && node --check src/worker.js && npm install && npm run lint && npx wrangler deploy --dry-run --outdir /tmp/w
npm run format:check # from the repo root: biome formatter over all four apps (fix: npm run format)
```

The dashboard runs **both** `typecheck` and `build` on purpose: a past Dependabot bump crashed `next build` while `tsc --noEmit` passed. Don't treat the typecheck as sufficient. The CLI cannot typecheck without the dashboard's `node_modules` - its tsconfig compiles `../dashboard/lib/**/*.ts` and borrows that app's typescript and `@types`.
17 changes: 17 additions & 0 deletions .github/workflows/ci-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ on:
pull_request:
paths:
- 'apps/**'
- 'biome.json'
- '.github/workflows/ci-apps.yml'
push:
branches: [main]
paths:
- 'apps/**'
- 'biome.json'
workflow_dispatch:

permissions:
Expand All @@ -47,6 +49,21 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
format:
name: format - biome
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
# Formatter only (linting stays with each app's eslint). One root biome.json
# covers all four apps, with per-app quote/semicolon overrides that keep each
# app's existing style. Pinned exact so a Biome release can't reformat the
# tree under an unrelated PR. Fix locally with `npm run format`.
- name: Format check (biome ci)
run: npx --yes @biomejs/biome@2.5.15 ci --reporter=github .

dashboard:
name: dashboard — typecheck, test, build
runs-on: ubuntu-24.04
Expand Down
5 changes: 1 addition & 4 deletions apps/cli/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,4 @@
// TypeScript sub-config is used - the CLI is not a React/Next app.
import ts from '../dashboard/node_modules/eslint-config-next/dist/typescript.js'

export default [
{ ignores: ['node_modules/**'] },
...ts,
]
export default [{ ignores: ['node_modules/**'] }, ...ts]
153 changes: 120 additions & 33 deletions apps/cli/src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,72 +41,145 @@ Options:
const CLAUDE_HARNESS = new Set(['claude-code', 'claude'])

export async function authCommand(argv: string[]) {
if (argv.includes('-h') || argv.includes('--help')) { console.log(USAGE); return }
if (argv.includes('-h') || argv.includes('--help')) {
console.log(USAGE)
return
}
requireGh()
if (!isDryRun()) requireInstanceRepo()

let values: { key?: string; provider?: string; 'base-url'?: string; oauth?: boolean; harness?: string; github?: boolean }
let values: {
key?: string
provider?: string
'base-url'?: string
oauth?: boolean
harness?: string
github?: boolean
}
let positionals: string[]
try {
;({ values, positionals } = parseArgs({ args: argv, options: {
key: { type: 'string' }, provider: { type: 'string' },
'base-url': { type: 'string' }, oauth: { type: 'boolean' },
harness: { type: 'string' }, github: { type: 'boolean' },
}, allowPositionals: true }))
} catch (e) { fail(e instanceof Error ? e.message : 'bad arguments') }
;({ values, positionals } = parseArgs({
args: argv,
options: {
key: { type: 'string' },
provider: { type: 'string' },
'base-url': { type: 'string' },
oauth: { type: 'boolean' },
harness: { type: 'string' },
github: { type: 'boolean' },
},
allowPositionals: true,
}))
} catch (e) {
fail(e instanceof Error ? e.message : 'bad arguments')
}

if (values.github) {
if (isDryRun()) return emit({ dryRun: true, method: 'oauth', secret: 'GH_GLOBAL' }, () =>
console.log(c.yellow('dry-run: ') + 'would copy `gh auth token` -> secret GH_GLOBAL'))
if (isDryRun())
return emit({ dryRun: true, method: 'oauth', secret: 'GH_GLOBAL' }, () =>
console.log(c.yellow('dry-run: ') + 'would copy `gh auth token` -> secret GH_GLOBAL'),
)
let result
try { result = captureGithubToken() }
catch (e) { fail(e instanceof Error ? e.message : 'failed to copy GitHub token') }
try {
result = captureGithubToken()
} catch (e) {
fail(e instanceof Error ? e.message : 'failed to copy GitHub token')
}
return emit(result, () => console.log(c.green('✓ ') + `GitHub: copied gh token as ${result.secret}`))
}
// --- grok: X-account OAuth capture or an xAI key (not in HARNESS_AUTH) ---
if (values.harness === 'grok') {
const key = (values.key ?? positionals[0] ?? '').trim()
if (isDryRun()) return emit({ dryRun: true, harness: 'grok', method: key ? 'api-key' : 'oauth', secret: key ? 'XAI_API_KEY' : 'GROK_CREDENTIALS' }, () =>
console.log(c.yellow('dry-run: ') + (key ? 'grok key -> secret XAI_API_KEY' : 'would run `grok login --device-auth` -> secret GROK_CREDENTIALS')))
if (isDryRun())
return emit(
{
dryRun: true,
harness: 'grok',
method: key ? 'api-key' : 'oauth',
secret: key ? 'XAI_API_KEY' : 'GROK_CREDENTIALS',
},
() =>
console.log(
c.yellow('dry-run: ') +
(key
? 'grok key -> secret XAI_API_KEY'
: 'would run `grok login --device-auth` -> secret GROK_CREDENTIALS'),
),
)
let res: { secret: string }
try { res = key ? await storeGrokKey(key) : grokLogin() } catch (e) { fail(e instanceof Error ? e.message : 'grok auth failed') }
try {
res = key ? await storeGrokKey(key) : grokLogin()
} catch (e) {
fail(e instanceof Error ? e.message : 'grok auth failed')
}
return emit({ ok: true, harness: 'grok', method: key ? 'api-key' : 'oauth', secret: res.secret }, () =>
console.log(c.green('✓ ') + `grok: stored as ${res.secret}. Select the harness with \`aeon config set harness grok\`.` +
(key ? '' : '\n The X login rotates its refresh token; set GH_GLOBAL (aeon auth --github) so each run can save the new one.')))
console.log(
c.green('✓ ') +
`grok: stored as ${res.secret}. Select the harness with \`aeon config set harness grok\`.` +
(key
? ''
: '\n The X login rotates its refresh token; set GH_GLOBAL (aeon auth --github) so each run can save the new one.'),
),
)
}

// --- Non-Claude harnesses: native OAuth capture or a provider key ---
// `--harness claude-code` (or `claude`) falls through to the Claude path below.
if (values.harness && !CLAUDE_HARNESS.has(values.harness)) {
const harness = values.harness
const spec = HARNESS_AUTH[harness]
if (!spec) fail(`unknown harness '${harness}'. Native auth is available for: claude-code, ${Object.keys(HARNESS_AUTH).join(', ')}`)
if (!spec)
fail(
`unknown harness '${harness}'. Native auth is available for: claude-code, ${Object.keys(HARNESS_AUTH).join(', ')}`,
)
const key = (values.key ?? positionals[0] ?? '').trim()

// A key was given (or the harness only supports keys) → store it.
if (key || !spec.oauth) {
if (!spec.apiKey) fail(`${harness} has no API-key path — run \`aeon auth --harness ${harness}\` for its login flow`)
if (!spec.apiKey)
fail(`${harness} has no API-key path — run \`aeon auth --harness ${harness}\` for its login flow`)
if (!key) fail(`${harness} takes a provider API key: aeon auth --harness ${harness} --key <…>`)
const target = spec.apiKey.detect ? spec.apiKey.detect(key) : spec.apiKey.secret
if (isDryRun()) return emit({ dryRun: true, harness, method: 'api-key', secret: target }, () =>
console.log(c.yellow('dry-run: ') + `${harness} key → secret ${target}`))
if (isDryRun())
return emit({ dryRun: true, harness, method: 'api-key', secret: target }, () =>
console.log(c.yellow('dry-run: ') + `${harness} key → secret ${target}`),
)
let res: { secret: string }
try { res = setHarnessApiKey(harness, key) } catch (e) { fail(e instanceof Error ? e.message : 'failed to set key') }
try {
res = setHarnessApiKey(harness, key)
} catch (e) {
fail(e instanceof Error ? e.message : 'failed to set key')
}
return emit({ ok: true, harness, method: 'api-key', secret: res.secret }, () =>
console.log(c.green('✓ ') + `${harness}: key stored as ${res.secret}. Select the harness with \`aeon config set harness ${harness}\`.`))
console.log(
c.green('✓ ') +
`${harness}: key stored as ${res.secret}. Select the harness with \`aeon config set harness ${harness}\`.`,
),
)
}

// No key → drive the native OAuth login, then capture the credential.
if (isDryRun()) return emit({ dryRun: true, harness, method: 'oauth', secret: spec.oauth.secret }, () =>
console.log(c.yellow('dry-run: ') + `would run \`${spec.oauth!.cli} ${spec.oauth!.ttyArgs.join(' ')}\` → secret ${spec.oauth!.secret}`))
if (isDryRun())
return emit({ dryRun: true, harness, method: 'oauth', secret: spec.oauth.secret }, () =>
console.log(
c.yellow('dry-run: ') +
`would run \`${spec.oauth!.cli} ${spec.oauth!.ttyArgs.join(' ')}\` → secret ${spec.oauth!.secret}`,
),
)
console.log(c.dim(`Opening ${spec.oauth.cli} login — approve in your browser…`))
let res: { secret: string }
try {
driveTtyLogin(harness)
res = captureHarnessCreds(harness)
} catch (e) { fail(e instanceof Error ? e.message : `${harness} login failed`) }
} catch (e) {
fail(e instanceof Error ? e.message : `${harness} login failed`)
}
return emit({ ok: true, harness, method: 'oauth', secret: res.secret }, () =>
console.log(c.green('✓ ') + `${harness}: login captured as ${res.secret}. Select the harness with \`aeon config set harness ${harness}\`.`))
console.log(
c.green('✓ ') +
`${harness}: login captured as ${res.secret}. Select the harness with \`aeon config set harness ${harness}\`.`,
),
)
}

// --- Claude harness (default), unchanged ---
Expand All @@ -117,11 +190,19 @@ export async function authCommand(argv: string[]) {
// normalizeAuthConfig is pure — it tells us the resolved method/secret without
// touching gh or claude.
let plan
try { plan = normalizeAuthConfig(body) } catch (e) { fail(e instanceof Error ? e.message : 'invalid auth config') }
try {
plan = normalizeAuthConfig(body)
} catch (e) {
fail(e instanceof Error ? e.message : 'invalid auth config')
}
return emit({ dryRun: true, ...plan, key: undefined }, () =>
console.log(c.yellow('dry-run: ') + `method=${plan.method} → secret ${plan.secretName}` +
(plan.baseUrl ? ` + ANTHROPIC_BASE_URL=${plan.baseUrl}` : '') +
(plan.method === 'oauth' && !key ? ' (would run `claude setup-token`)' : '')))
console.log(
c.yellow('dry-run: ') +
`method=${plan.method} → secret ${plan.secretName}` +
(plan.baseUrl ? ` + ANTHROPIC_BASE_URL=${plan.baseUrl}` : '') +
(plan.method === 'oauth' && !key ? ' (would run `claude setup-token`)' : ''),
),
)
}

let result
Expand All @@ -130,6 +211,12 @@ export async function authCommand(argv: string[]) {
} catch (e) {
fail(e instanceof Error ? e.message : 'failed to configure auth')
}
emit(result, () => console.log(c.green('✓ ') + `authenticated (method: ${result.method}` +
(result.secret ? `, secret: ${result.secret}` : '') + ')'))
emit(result, () =>
console.log(
c.green('✓ ') +
`authenticated (method: ${result.method}` +
(result.secret ? `, secret: ${result.secret}` : '') +
')',
),
)
}
23 changes: 18 additions & 5 deletions apps/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { getFileContent } from '../../../dashboard/lib/github.ts'
import {
parseConfig, updateModelInConfig, updateHarnessInConfig, updateGatewayInConfig,
parseConfig,
updateModelInConfig,
updateHarnessInConfig,
updateGatewayInConfig,
} from '../../../dashboard/lib/config.ts'
import { getRepoSlug } from '../../../dashboard/lib/skills.ts'
import { HARNESSES, GATEWAY_PROVIDERS } from '../../../dashboard/lib/types.ts'
Expand All @@ -21,7 +24,10 @@ Options:

export async function configCommand(argv: string[]) {
const sub = argv[0] && !argv[0].startsWith('-') ? argv[0] : 'show'
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) { console.log(USAGE); return }
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) {
console.log(USAGE)
return
}
if (sub === 'show') return show()
if (sub === 'set') return set(argv.slice(1))
fail(`unknown subcommand: ${sub}\n\n${USAGE}`)
Expand Down Expand Up @@ -70,12 +76,19 @@ async function set(args: string[]) {
}
case 'harness': {
if (!HARNESSES.includes(value as Harness)) fail(`harness must be one of: ${HARNESSES.join(', ')}`)
const res = await applyConfig(raw => updateHarnessInConfig(raw, value as Harness), `chore: set harness to ${value}`)
const res = await applyConfig(
raw => updateHarnessInConfig(raw, value as Harness),
`chore: set harness to ${value}`,
)
return reportConfig(res, `set harness → ${value}`)
}
case 'gateway': {
if (!GATEWAY_PROVIDERS.includes(value as GatewayProvider)) fail(`gateway must be one of: ${GATEWAY_PROVIDERS.join(', ')}`)
const res = await applyConfig(raw => updateGatewayInConfig(raw, value as GatewayProvider), `chore: set gateway to ${value}`)
if (!GATEWAY_PROVIDERS.includes(value as GatewayProvider))
fail(`gateway must be one of: ${GATEWAY_PROVIDERS.join(', ')}`)
const res = await applyConfig(
raw => updateGatewayInConfig(raw, value as GatewayProvider),
`chore: set gateway to ${value}`,
)
return reportConfig(res, `set gateway → ${value}`)
}
default:
Expand Down
Loading
Loading