Merge pull request #410 from ozymandiashh/codex/antigravity-hook-stale-path

Fix Antigravity hook stale CLI paths: install the statusLine hook through a
persistent codeburn binary resolved from PATH and repair stale CodeBurn-owned
hooks on re-install.
This commit is contained in:
iamtoruk 2026-05-31 20:04:11 -07:00
commit b246e822b7
4 changed files with 133 additions and 13 deletions

View file

@ -1,5 +1,14 @@
# Changelog
## Unreleased
### Fixed (CLI)
- **Antigravity hook stale path repair.** `codeburn antigravity-hook install`
now installs the statusLine command through a persistent `codeburn` binary
from PATH and repairs older CodeBurn-owned hooks that pointed at stale local
build artifacts, preventing `agy` from auto-disabling capture after
`MODULE_NOT_FOUND` failures.
## 0.9.11 - 2026-05-27
### Added (CLI)

View file

@ -27,9 +27,12 @@ For Antigravity CLI (`agy`), CodeBurn can also install an opt-in status line
hook with `codeburn antigravity-hook install`. The hook records the CLI's
sanitized `context_window.current_usage` payload while `agy` is still alive,
without prompts or local working-directory paths. It also attempts a best-effort
RPC snapshot for full response metadata. Remove it with
`codeburn antigravity-hook uninstall`; if `--force` replaced an existing
statusLine command, uninstall restores that previous command.
RPC snapshot for full response metadata. The installed command points at a
persistent `codeburn` binary from PATH rather than a local build artifact, and
running `codeburn antigravity-hook install` again repairs older CodeBurn-owned
statusLine commands that used stale absolute paths. Remove it with `codeburn
antigravity-hook uninstall`; if `--force` replaced an existing statusLine
command, uninstall restores that previous command.
## Storage format

View file

@ -1,6 +1,7 @@
import { mkdir, open, readFile, rename, unlink } from 'fs/promises'
import { constants } from 'fs'
import { access, mkdir, open, readFile, rename, unlink } from 'fs/promises'
import { randomBytes } from 'crypto'
import { dirname, join } from 'path'
import { delimiter, dirname, join } from 'path'
import { homedir } from 'os'
import {
@ -18,6 +19,12 @@ type Settings = Record<string, unknown> & {
type StatusLineSettings = NonNullable<Settings['statusLine']>
const PERSISTENT_CLI_REQUIRED_MESSAGE =
'The Antigravity hook needs a persistent codeburn command. Install CodeBurn globally first: npm install -g codeburn'
const DEFAULT_CLI_LOOKUP_PATHS = process.platform === 'win32'
? []
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin']
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
@ -31,10 +38,49 @@ function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
function hookCommand(): string {
const script = process.argv[1] || 'codeburn'
if (script === 'codeburn') return 'codeburn agy-statusline-hook'
return `${shellQuote(process.execPath)} ${shellQuote(script)} agy-statusline-hook`
function isTransientNpxPath(path: string): boolean {
return path.includes('/_npx/') || path.includes('/.npm/_npx/') || path.includes('\\_npx\\')
}
function codeburnExecutableNames(): string[] {
if (process.platform !== 'win32') return ['codeburn']
return ['codeburn.cmd', 'codeburn.exe', 'codeburn.bat', 'codeburn']
}
export function buildAntigravityHookLookupPath(existingPath = process.env.PATH ?? ''): string {
const parts = existingPath.split(delimiter).filter(Boolean)
for (const fallback of DEFAULT_CLI_LOOKUP_PATHS) {
if (!parts.includes(fallback)) parts.push(fallback)
}
return parts.join(delimiter)
}
async function executableExists(path: string): Promise<boolean> {
try {
await access(path, process.platform === 'win32' ? constants.F_OK : constants.F_OK | constants.X_OK)
return true
} catch {
return false
}
}
export async function resolvePersistentCodeburnPathFromPath(lookupPath: string): Promise<string> {
const seen = new Set<string>()
for (const dir of lookupPath.split(delimiter).filter(Boolean)) {
for (const executable of codeburnExecutableNames()) {
const candidate = join(dir, executable)
if (seen.has(candidate)) continue
seen.add(candidate)
if (isTransientNpxPath(candidate)) continue
if (await executableExists(candidate)) return candidate
}
}
throw new Error(PERSISTENT_CLI_REQUIRED_MESSAGE)
}
async function hookCommand(): Promise<string> {
const codeburnPath = await resolvePersistentCodeburnPathFromPath(buildAntigravityHookLookupPath())
return `${shellQuote(codeburnPath)} agy-statusline-hook`
}
function settingsPath(): string {
@ -115,12 +161,15 @@ export async function installAntigravityStatusLineHook(force = false): Promise<'
)
}
if (isCodeBurnHook(existing?.command)) return 'already-installed'
const command = await hookCommand()
if (isCodeBurnHook(existing?.command) && existing?.command === command && existing.type === 'command' && existing.padding === 0) {
return 'already-installed'
}
if (existing && !isCodeBurnHook(existing.command)) await savePreviousStatusLine(existing)
settings.statusLine = {
type: 'command',
command: hookCommand(),
command,
padding: 0,
}
await writeSettings(settings)

View file

@ -1,10 +1,12 @@
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises'
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { delimiter, join } from 'path'
import { describe, expect, it } from 'vitest'
import {
buildAntigravityHookLookupPath,
installAntigravityStatusLineHook,
resolvePersistentCodeburnPathFromPath,
uninstallAntigravityStatusLineHook,
} from '../src/antigravity-statusline.js'
@ -12,10 +14,17 @@ describe('Antigravity CLI statusLine hook installer', () => {
async function withTempSettings(run: (dir: string, settingsPath: string) => Promise<void>) {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-agy-hook-'))
const settingsPath = join(dir, 'settings.json')
const binDir = join(dir, 'bin')
const codeburnPath = join(binDir, process.platform === 'win32' ? 'codeburn.cmd' : 'codeburn')
const oldSettingsPath = process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH']
const oldCacheDir = process.env['CODEBURN_CACHE_DIR']
const oldPath = process.env.PATH
await mkdir(binDir, { recursive: true })
await writeFile(codeburnPath, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n')
await chmod(codeburnPath, 0o755)
process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH'] = settingsPath
process.env['CODEBURN_CACHE_DIR'] = join(dir, 'cache')
process.env.PATH = binDir
try {
await run(dir, settingsPath)
@ -24,10 +33,38 @@ describe('Antigravity CLI statusLine hook installer', () => {
else process.env['CODEBURN_ANTIGRAVITY_SETTINGS_PATH'] = oldSettingsPath
if (oldCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = oldCacheDir
if (oldPath === undefined) delete process.env.PATH
else process.env.PATH = oldPath
await rm(dir, { recursive: true, force: true })
}
}
it('builds a lookup PATH with user paths before fallbacks', () => {
const lookupPath = buildAntigravityHookLookupPath(['/Users/me/.nvm/versions/node/v22.13.0/bin', '/usr/bin'].join(delimiter))
expect(lookupPath.split(delimiter)).toContain('/Users/me/.nvm/versions/node/v22.13.0/bin')
if (process.platform !== 'win32') expect(lookupPath.split(delimiter)).toContain('/opt/homebrew/bin')
})
it('skips transient npx codeburn shims when resolving the hook command', async () => {
await withTempSettings(async (dir) => {
const npxBin = join(dir, '.npm', '_npx', 'abcd', 'node_modules', '.bin')
const persistentBin = join(dir, 'persistent-bin')
const npxCodeburn = join(npxBin, process.platform === 'win32' ? 'codeburn.cmd' : 'codeburn')
const persistentCodeburn = join(persistentBin, process.platform === 'win32' ? 'codeburn.cmd' : 'codeburn')
await mkdir(npxBin, { recursive: true })
await mkdir(persistentBin, { recursive: true })
await writeFile(npxCodeburn, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n')
await writeFile(persistentCodeburn, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n')
await chmod(npxCodeburn, 0o755)
await chmod(persistentCodeburn, 0o755)
const resolved = await resolvePersistentCodeburnPathFromPath([npxBin, persistentBin].join(delimiter))
expect(resolved).toBe(persistentCodeburn)
})
})
it('backs up and restores an existing custom statusLine when forced', async () => {
await withTempSettings(async (dir, settingsPath) => {
const customStatusLine = {
@ -42,6 +79,7 @@ describe('Antigravity CLI statusLine hook installer', () => {
const installed = JSON.parse(await readFile(settingsPath, 'utf-8'))
expect(installed.statusLine.command).toContain('agy-statusline-hook')
expect(installed.statusLine.command).not.toContain('custom-statusline')
const backupPath = join(dir, 'cache', 'antigravity-statusline-previous.json')
const backup = JSON.parse(await readFile(backupPath, 'utf-8'))
@ -64,6 +102,27 @@ describe('Antigravity CLI statusLine hook installer', () => {
padding: 0,
})
expect(settings.statusLine.command).toContain('agy-statusline-hook')
expect(settings.statusLine.command).toContain(join(_dir, 'bin'))
expect(settings.statusLine.command).not.toContain('dist/cli.js')
})
})
it('repairs an existing stale CodeBurn statusLine command without force', async () => {
await withTempSettings(async (dir, settingsPath) => {
await writeFile(settingsPath, JSON.stringify({
statusLine: {
type: 'command',
command: "'/usr/local/bin/node' '/Users/me/codeburn-agy-statusline/dist/cli.js' agy-statusline-hook",
padding: 0,
},
}))
expect(await installAntigravityStatusLineHook(false)).toBe('installed')
const settings = JSON.parse(await readFile(settingsPath, 'utf-8'))
expect(settings.statusLine.command).toContain(join(dir, 'bin'))
expect(settings.statusLine.command).toContain('agy-statusline-hook')
expect(settings.statusLine.command).not.toContain('codeburn-agy-statusline/dist/cli.js')
})
})