mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-13 02:25:04 +00:00
Merge pull request #903 from ozymandiashh/feat/899-proberoots-tier1
feat(doctor): probeRoots for 12 more providers (#899 Tier 1)
This commit is contained in:
commit
9df25fa066
13 changed files with 198 additions and 10 deletions
|
|
@ -8,7 +8,7 @@ import https from 'https'
|
|||
|
||||
import { calculateCost } from '../models.js'
|
||||
import { isSqliteAvailable, isSqliteBusyError, openDatabase } from '../sqlite.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
type AntigravityConversationRoot = {
|
||||
dir: string
|
||||
|
|
@ -1428,6 +1428,13 @@ export function createAntigravityProvider(): Provider {
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [
|
||||
...conversationRoots().map(root => ({ path: root.dir, label: 'conversations' })),
|
||||
{ path: getAntigravityStatusLineEventsPath(), label: 'statusline' },
|
||||
]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverAntigravitySessionSources()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { extractBashCommands } from '../bash-utils.js'
|
|||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import type { ToolCall } from '../types.js'
|
||||
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
import type { ProbeRoot, ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
|
||||
const METADATA_PREFIX_BYTES = 64 * 1024
|
||||
|
||||
|
|
@ -457,6 +457,10 @@ export function createCodeWhaleProvider(overrideDirs?: string | string[]): Provi
|
|||
return mapToolName(rawTool)
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return (configuredDirs ?? defaultSessionDirs()).map(path => ({ path, label: 'sessions' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const seenSessionIds = new Set<string>()
|
||||
const sources: SessionSource[] = []
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { homedir, platform } from 'os'
|
|||
|
||||
import { calculateCost } from '../models.js'
|
||||
import { isSqliteAvailable, getSqliteLoadError, openDatabase, type SqliteDatabase } from '../sqlite.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
/// Crush stores per-project SQLite databases discovered through a JSON registry.
|
||||
/// We only read both. Schema source: charmbracelet/crush
|
||||
|
|
@ -236,6 +236,10 @@ export function createCrushProvider(): Provider {
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: getRegistryPath(), label: 'registry' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!isSqliteAvailable()) return []
|
||||
const registry = await loadRegistry(getRegistryPath())
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
SessionSource,
|
||||
SessionParser,
|
||||
ParsedProviderCall,
|
||||
ProbeRoot,
|
||||
} from './types.js'
|
||||
|
||||
type ConversationSummary = {
|
||||
|
|
@ -513,6 +514,13 @@ export function createCursorAgentProvider(baseDirOverride?: string): Provider {
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [
|
||||
{ path: projectsDir, label: 'projects' },
|
||||
{ path: dbPath, label: 'db' },
|
||||
]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!existsSync(projectsDir)) return []
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { readCachedResults, writeCachedResults } from '../cursor-cache.js'
|
|||
import { isSqliteAvailable, isSqliteBusyError, getSqliteLoadError, openDatabase, blobToText, type SqliteDatabase } from '../sqlite.js'
|
||||
import { estimateTokensFromChars } from '../token-estimate.js'
|
||||
import type { DateRange } from '../types.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
/** Matches cli-date.ts "all" period cap (6 months). */
|
||||
const CURSOR_MAX_LOOKBACK_MONTHS = 6
|
||||
|
|
@ -1045,6 +1045,10 @@ export function createCursorProvider(dbPathOverride?: string): Provider {
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: dbPathOverride ?? getCursorDbPath(), label: 'db' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!isSqliteAvailable()) return []
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SessionSource,
|
||||
SessionParser,
|
||||
ParsedProviderCall,
|
||||
ProbeRoot,
|
||||
} from './types.js'
|
||||
|
||||
const toolNameMap: Record<string, string> = {
|
||||
|
|
@ -391,6 +392,10 @@ export function createDroidProvider(factoryDir?: string): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: sessionsDir, label: 'sessions' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverSessionsInDir(sessionsDir, base)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { calculateCost, getShortModelName } from '../models.js'
|
|||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { isSqliteAvailable, getSqliteLoadError, openDatabase, blobToText, type SqliteDatabase } from '../sqlite.js'
|
||||
import type { ToolCall } from '../types.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
type SessionRow = {
|
||||
id: string
|
||||
|
|
@ -275,6 +275,10 @@ export function createGooseProvider(): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: getDbPath(), label: 'db' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!isSqliteAvailable()) return []
|
||||
const dbPath = getDbPath()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import { isSqliteAvailable, getSqliteLoadError, openDatabase, isSqliteBusyError, type SqliteDatabase } from '../sqlite.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ToolCall } from '../types.js'
|
||||
|
||||
type HermesSessionRow = {
|
||||
|
|
@ -462,6 +462,10 @@ export function createHermesProvider(hermesHomeOverride?: string): Provider {
|
|||
return mapToolName(rawTool)
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: hermesHome, label: 'home' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!isSqliteAvailable()) return []
|
||||
const dbs = await findStateDbs(hermesHome)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { getShortModelName } from '../models.js'
|
||||
import { discoverClineTasksInBaseDirs, createClineParser } from './vscode-cline-parser.js'
|
||||
import type { Provider, SessionSource, SessionParser } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const PROVIDER_NAME = 'ibm-bob'
|
||||
const DISPLAY_NAME = 'IBM Bob'
|
||||
|
|
@ -45,6 +45,10 @@ export function createIBMBobProvider(overrideDir?: string): Provider {
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return (overrideDir ? [overrideDir] : getIBMBobGlobalStorageDirs()).map(path => ({ path, label: 'storage' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const dirs = overrideDir ? [overrideDir] : getIBMBobGlobalStorageDirs()
|
||||
return discoverClineTasksInBaseDirs(dirs, PROVIDER_NAME, DISPLAY_NAME)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { readSessionLines } from '../fs-utils.js'
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
import type { ProbeRoot, ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
|
|
@ -418,6 +418,24 @@ export function createLingTaiTuiProvider(options?: string | LingTaiProviderOptio
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
// Deliberately pre-existence-filter: doctor's job is to show where
|
||||
// discovery looks, so a missing default home reads as "not installed
|
||||
// here" instead of vanishing (getLingTaiHomes drops non-existent
|
||||
// candidates, which is right for discovery and wrong for doctor).
|
||||
const explicit = splitPathList(providerOptions.lingtaiHomeOverride ?? process.env['LINGTAI_HOME'] ?? process.env['LINGTAI_TUI_HOME'])
|
||||
const roots: ProbeRoot[] = explicit.length
|
||||
? explicit.map(path => ({ path, label: 'sessions' }))
|
||||
: [
|
||||
{ path: getDefaultLingTaiHome(providerOptions), label: 'sessions' },
|
||||
{ path: getLingTaiGlobalDir(providerOptions), label: 'registry' },
|
||||
]
|
||||
for (const home of await getLingTaiHomes(providerOptions)) {
|
||||
if (!roots.some(root => root.path === home.path)) roots.push({ path: home.path, label: 'sessions' })
|
||||
}
|
||||
return roots
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverLedgers(await getLingTaiHomes(providerOptions))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { homedir } from 'os'
|
|||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost } from '../models.js'
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
const toolNameMap: Record<string, string> = {
|
||||
read_file: 'Read',
|
||||
|
|
@ -162,6 +162,10 @@ export function createQwenProvider(overrideDir?: string): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: projectsDir, label: 'projects' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const sources: SessionSource[] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { extractBashCommands } from '../bash-utils.js'
|
|||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import { blobToText, getSqliteLoadError, isSqliteAvailable, openDatabase, type SqliteDatabase } from '../sqlite.js'
|
||||
import { estimateTokensFromChars } from '../token-estimate.js'
|
||||
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
import type { ProbeRoot, ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
import { safeNumber } from '../parser.js'
|
||||
|
||||
const WARP_GROUP_CONTAINER = '2BBY89MBSN.dev.warp'
|
||||
|
|
@ -479,6 +479,10 @@ export function createWarpProvider(dbPathOverride?: string): Provider {
|
|||
return rawTool === 'run_command' ? 'Bash' : rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return getDbCandidates(dbPathOverride).map(path => ({ path, label: 'db' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if (!isSqliteAvailable()) return []
|
||||
|
||||
|
|
|
|||
118
tests/provider-probe-roots.test.ts
Normal file
118
tests/provider-probe-roots.test.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { isAbsolute, join } from 'path'
|
||||
|
||||
import { createCodeWhaleProvider } from '../src/providers/codewhale.js'
|
||||
import { createHermesProvider } from '../src/providers/hermes.js'
|
||||
import { createLingTaiTuiProvider } from '../src/providers/lingtai-tui.js'
|
||||
import { createDroidProvider } from '../src/providers/droid.js'
|
||||
import { createCursorProvider } from '../src/providers/cursor.js'
|
||||
import { createCursorAgentProvider } from '../src/providers/cursor-agent.js'
|
||||
import { createGooseProvider } from '../src/providers/goose.js'
|
||||
import { createCrushProvider } from '../src/providers/crush.js'
|
||||
import { createWarpProvider } from '../src/providers/warp.js'
|
||||
import { createAntigravityProvider } from '../src/providers/antigravity.js'
|
||||
import { createQwenProvider } from '../src/providers/qwen.js'
|
||||
import { createIBMBobProvider } from '../src/providers/ibm-bob.js'
|
||||
|
||||
// probeRoots must mirror the exact resolution each provider's discovery uses
|
||||
// (#899 Tier 1). Where a factory takes an override, the assertion is exact:
|
||||
// the same override must come back through probeRoots, proving the two paths
|
||||
// share one resolution. Providers without an override factory get structural
|
||||
// assertions: non-empty, absolute, correctly labeled.
|
||||
|
||||
describe('probeRoots mirrors discovery resolution', () => {
|
||||
it('codewhale reports the configured dirs, or both defaults', async () => {
|
||||
expect(await createCodeWhaleProvider('/tmp/cw-root').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/cw-root', label: 'sessions' },
|
||||
])
|
||||
const defaults = await createCodeWhaleProvider().probeRoots!()
|
||||
expect(defaults).toHaveLength(2)
|
||||
for (const root of defaults) expect(isAbsolute(root.path)).toBe(true)
|
||||
})
|
||||
|
||||
it('hermes reports its resolved home', async () => {
|
||||
expect(await createHermesProvider('/tmp/hermes-home').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/hermes-home', label: 'home' },
|
||||
])
|
||||
})
|
||||
|
||||
it('droid reports the sessions dir under the factory root', async () => {
|
||||
expect(await createDroidProvider('/tmp/factory').probeRoots!()).toEqual([
|
||||
{ path: join('/tmp/factory', 'sessions'), label: 'sessions' },
|
||||
])
|
||||
})
|
||||
|
||||
it('cursor reports the state db path', async () => {
|
||||
expect(await createCursorProvider('/tmp/cursor/state.vscdb').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/cursor/state.vscdb', label: 'db' },
|
||||
])
|
||||
})
|
||||
|
||||
it('cursor-agent reports the projects dir and the attribution db', async () => {
|
||||
expect(await createCursorAgentProvider('/tmp/ca').probeRoots!()).toEqual([
|
||||
{ path: join('/tmp/ca', 'projects'), label: 'projects' },
|
||||
{ path: join('/tmp/ca', 'ai-tracking', 'ai-code-tracking.db'), label: 'db' },
|
||||
])
|
||||
})
|
||||
|
||||
it('warp reports the override db, or both bundle candidates', async () => {
|
||||
expect(await createWarpProvider('/tmp/warp.db').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/warp.db', label: 'db' },
|
||||
])
|
||||
const defaults = await createWarpProvider().probeRoots!()
|
||||
expect(defaults).toHaveLength(2)
|
||||
for (const root of defaults) {
|
||||
expect(isAbsolute(root.path)).toBe(true)
|
||||
expect(root.label).toBe('db')
|
||||
}
|
||||
})
|
||||
|
||||
it('qwen reports the projects dir', async () => {
|
||||
expect(await createQwenProvider('/tmp/qwen-projects').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/qwen-projects', label: 'projects' },
|
||||
])
|
||||
})
|
||||
|
||||
it('ibm-bob reports the storage dirs', async () => {
|
||||
expect(await createIBMBobProvider('/tmp/bob').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/bob', label: 'storage' },
|
||||
])
|
||||
const defaults = await createIBMBobProvider().probeRoots!()
|
||||
expect(defaults.length).toBeGreaterThan(0)
|
||||
for (const root of defaults) expect(root.label).toBe('storage')
|
||||
})
|
||||
|
||||
it('lingtai-tui reports its candidates even when none exist yet', async () => {
|
||||
// getLingTaiHomes drops non-existent candidates (right for discovery);
|
||||
// probeRoots must keep them visible so doctor can show where it looked.
|
||||
const roots = await createLingTaiTuiProvider().probeRoots!()
|
||||
expect(roots.length).toBeGreaterThanOrEqual(2)
|
||||
for (const root of roots) expect(isAbsolute(root.path)).toBe(true)
|
||||
const labels = new Set(roots.map(r => r.label))
|
||||
expect(labels.has('sessions')).toBe(true)
|
||||
expect(labels.has('registry')).toBe(true)
|
||||
})
|
||||
|
||||
it('goose reports its sessions db', async () => {
|
||||
const roots = await createGooseProvider().probeRoots!()
|
||||
expect(roots).toHaveLength(1)
|
||||
expect(isAbsolute(roots[0]!.path)).toBe(true)
|
||||
expect(roots[0]!.label).toBe('db')
|
||||
})
|
||||
|
||||
it('crush reports its registry file', async () => {
|
||||
const roots = await createCrushProvider().probeRoots!()
|
||||
expect(roots).toHaveLength(1)
|
||||
expect(isAbsolute(roots[0]!.path)).toBe(true)
|
||||
expect(roots[0]!.label).toBe('registry')
|
||||
})
|
||||
|
||||
it('antigravity reports its conversation roots and the statusline file', async () => {
|
||||
const roots = await createAntigravityProvider().probeRoots!()
|
||||
expect(roots.length).toBeGreaterThanOrEqual(2)
|
||||
for (const root of roots) expect(isAbsolute(root.path)).toBe(true)
|
||||
const labels = new Set(roots.map(r => r.label))
|
||||
expect(labels.has('conversations')).toBe(true)
|
||||
expect(labels.has('statusline')).toBe(true)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue