mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
* fix(opencode-go): stop before OPENCODE_API_KEY when the credential database is unreadable An unreadable OpenCode credential database read as 'no key', so the Go key resolver fell through to OPENCODE_API_KEY, which OpenCode shares with its Zen provider and can show usage for the wrong key. The database read now reports unreadable separately; with an env key set the resolver stops, and the usage fetch uses a configured cookie or shows a readable error. * fix(opencode-go): treat a denied credential-database listing as unreadable, not missing
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { stat } from 'node:fs/promises'
|
|
import { basename, isAbsolute, join } from 'node:path'
|
|
import { wslGatedReaddir, wslGatedStat } from '../native-chat/wsl-transcript-fs-access'
|
|
import { WslTranscriptFsError } from '../native-chat/wsl-transcript-fs-gate'
|
|
import { resolveOpenCodeDataDirectory } from '../opencode/opencode-data-directory'
|
|
import type { OpenCodeUsageProcessedDatabase } from './types'
|
|
|
|
type OpenCodeDatabaseOverride = {
|
|
isConfigured: boolean
|
|
path: string | null
|
|
}
|
|
|
|
function getOpenCodeDatabaseOverride(dataDirectory: string): OpenCodeDatabaseOverride {
|
|
const raw = process.env.OPENCODE_DB?.trim()
|
|
if (!raw) {
|
|
return { isConfigured: false, path: null }
|
|
}
|
|
if (raw === ':memory:') {
|
|
return { isConfigured: true, path: null }
|
|
}
|
|
return {
|
|
isConfigured: true,
|
|
path: isAbsolute(raw) ? raw : join(dataDirectory, raw)
|
|
}
|
|
}
|
|
|
|
// Why gated: the AI Vault's primary OpenCode source delegates here from inside
|
|
// its discovery fan-out, so a UNC data dir or a UNC OPENCODE_DB on a stalled
|
|
// distro would otherwise hang the whole scan on a raw syscall (STA-4049).
|
|
export async function listOpenCodeDatabases(
|
|
/** Lets a caller report the refusal; an empty list otherwise reads as
|
|
* "OpenCode not used" rather than "we could not look". */
|
|
onRefusal?: (path: string, error: WslTranscriptFsError) => void,
|
|
/** Every other stat/readdir failure, including ENOENT; also read as an empty list. */
|
|
onFsError?: (path: string, error: unknown) => void
|
|
): Promise<string[]> {
|
|
const dataDirectory = resolveOpenCodeDataDirectory()
|
|
const databaseOverride = getOpenCodeDatabaseOverride(dataDirectory)
|
|
if (databaseOverride.isConfigured) {
|
|
if (!databaseOverride.path) {
|
|
return []
|
|
}
|
|
try {
|
|
return (await wslGatedStat(databaseOverride.path, 'scan')).isFile()
|
|
? [databaseOverride.path]
|
|
: []
|
|
} catch (error) {
|
|
reportFailure(databaseOverride.path, error, onRefusal, onFsError)
|
|
return []
|
|
}
|
|
}
|
|
|
|
try {
|
|
const entries = await wslGatedReaddir(dataDirectory, 'scan')
|
|
return entries
|
|
.filter((entry) => entry.isFile() && /^opencode(?:-[A-Za-z0-9_.-]+)?\.db$/.test(entry.name))
|
|
.map((entry) => join(dataDirectory, entry.name))
|
|
.sort()
|
|
} catch (error) {
|
|
reportFailure(dataDirectory, error, onRefusal, onFsError)
|
|
return []
|
|
}
|
|
}
|
|
|
|
function reportFailure(
|
|
path: string,
|
|
error: unknown,
|
|
onRefusal?: (path: string, error: WslTranscriptFsError) => void,
|
|
onFsError?: (path: string, error: unknown) => void
|
|
): void {
|
|
if (error instanceof WslTranscriptFsError) {
|
|
onRefusal?.(path, error)
|
|
} else {
|
|
onFsError?.(path, error)
|
|
}
|
|
}
|
|
|
|
export function compareOpenCodeClaimPriority(left: string, right: string): number {
|
|
// Why: the canonical opencode.db is the live database; it must claim
|
|
// duplicated sessions ahead of stale sibling copies. Remaining ties use
|
|
// path order so ownership is deterministic across rescans.
|
|
const leftRank = basename(left).toLowerCase() === 'opencode.db' ? 0 : 1
|
|
const rightRank = basename(right).toLowerCase() === 'opencode.db' ? 0 : 1
|
|
if (leftRank !== rightRank) {
|
|
return leftRank - rightRank
|
|
}
|
|
return left < right ? -1 : left > right ? 1 : 0
|
|
}
|
|
|
|
export async function getProcessedDatabaseInfo(
|
|
dbPath: string
|
|
): Promise<OpenCodeUsageProcessedDatabase> {
|
|
const dbStat = await stat(dbPath)
|
|
return {
|
|
path: dbPath,
|
|
mtimeMs: dbStat.mtimeMs,
|
|
size: dbStat.size
|
|
}
|
|
}
|