Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- [2026-07-23] an update re-applies routed configs on the next daemon start, fixes #17
- [2026-07-23] an out-of-date dashboard says restart and refuses changes
- [2026-07-23] claude routing keeps the native login, fixes #15
- [2026-07-22] session reset time on analytics, fixes #9
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@
"post-commit": "bun x @rubriclab/package post-commit"
},
"type": "module",
"version": "0.0.58"
"version": "0.0.59"
}
14 changes: 14 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from 'zod'
import { registerClaudeAccount, registerClaudeApiKeyAccount } from './claude.ts'
import { registerCodexAccount, registerOpenAiApiKeyAccount } from './codex.ts'
import {
healInstalledConfigs,
installClaudeConfig,
installCodexConfig,
installStatus,
Expand Down Expand Up @@ -298,6 +299,19 @@ async function runDaemon(context: ApplicationContext): Promise<void> {
if (await managerAvailable(context.paths.managerSocket)) {
throw new ApplicationError('DAEMON_RUNNING', 'The manager daemon is already running')
}
// ensureDaemon restarts the daemon on a version change, so this runs on the
// first start after every update; a failure must not keep the daemon down.
const healed = await healInstalledConfigs(context.paths).catch(error => {
process.stderr.write(
`[${new Date().toISOString()}] config heal failed: ${errorMessage(error)}\n`
)
return []
})
if (healed.length > 0) {
process.stdout.write(
`[${new Date().toISOString()}] re-applied ${healed.join(' and ')} routing for v${VERSION}\n`
)
}
const manager = new AccountManager({
paths: context.paths,
store: context.store,
Expand Down
35 changes: 35 additions & 0 deletions src/config-install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
healInstalledConfigs,
installClaudeConfig,
installCodexConfig,
installStatus,
Expand Down Expand Up @@ -197,3 +198,37 @@ describe('installClaudeConfig', () => {
expect(settings.env).toBeUndefined()
})
})

describe('healInstalledConfigs', () => {
test('re-applies every routed config, healing what an older version wrote', async () => {
await installCodexConfig(paths())
await writeClaudeSettings({
env: {
ANTHROPIC_AUTH_TOKEN: 'managed-by-tokenmaxx',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8459/anthropic'
}
})
expect(await healInstalledConfigs(paths())).toEqual(['codex', 'claude'])
const settings = await readClaudeSettings()
expect(settings.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
expect(settings.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8459/anthropic')
})

test('never adds routing to an unrouted harness', async () => {
expect(await healInstalledConfigs(paths())).toEqual([])
await expect(readClaudeSettings()).rejects.toThrow()
expect((await installStatus()).codexRouted).toBe(false)
})

test('runs once per version, not on every start', async () => {
await healInstalledConfigs(paths())
await writeClaudeSettings({
env: {
ANTHROPIC_AUTH_TOKEN: 'managed-by-tokenmaxx',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8459/anthropic'
}
})
expect(await healInstalledConfigs(paths())).toEqual([])
expect((await readClaudeSettings()).env?.ANTHROPIC_AUTH_TOKEN).toBe('managed-by-tokenmaxx')
})
})
24 changes: 24 additions & 0 deletions src/config-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import type { ApplicationPaths } from './paths.ts'
import { proxyBaseUrl } from './paths.ts'
import { VERSION } from './version.ts'

const providerName = 'tokenmaxx'
const topBeginMarker = '# >>> tokenmaxx managed (do not edit) >>>'
Expand Down Expand Up @@ -195,3 +196,26 @@ export async function installStatus(): Promise<InstallStatus> {
}
return { claudeRouted, codexRouted, codexStale }
}

// Configs written by an older version stay stale after an update (#17): re-apply
// install for whatever is currently routed, once per version change. Never adds
// routing — a harness the user uninstalled or never installed stays untouched.
export async function healInstalledConfigs(paths: ApplicationPaths): Promise<string[]> {
const stampPath = join(paths.root, 'healed-version')
if ((await readFileOrEmpty(stampPath)).trim() === VERSION) {
return []
}
const { claudeRouted, codexRouted } = await installStatus()
const healed: string[] = []
if (codexRouted) {
await installCodexConfig(paths)
healed.push('codex')
}
if (claudeRouted) {
await installClaudeConfig(paths)
healed.push('claude')
}
await mkdir(paths.root, { recursive: true })
await writeFile(stampPath, `${VERSION}\n`)
return healed
}
Loading