singularity-forge/src/cli.ts

133 lines
5 KiB
TypeScript
Raw Normal View History

2026-03-10 22:28:37 -06:00
import {
AuthStorage,
ModelRegistry,
SettingsManager,
SessionManager,
createAgentSession,
InteractiveMode,
} from '@mariozechner/pi-coding-agent'
import { join } from 'node:path'
2026-03-10 22:28:37 -06:00
import { agentDir, sessionsDir, authFilePath } from './app-paths.js'
import { buildResourceLoader, initResources } from './resource-loader.js'
import { ensureManagedTools } from './tool-bootstrap.js'
2026-03-10 22:28:37 -06:00
import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js'
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
// Provision local managed binaries first so Pi sees them without probing PATH.
ensureManagedTools(join(agentDir, 'bin'))
2026-03-10 22:28:37 -06:00
const authStorage = AuthStorage.create(authFilePath)
loadStoredEnvKeys(authStorage)
await runWizardIfNeeded(authStorage)
const modelRegistry = new ModelRegistry(authStorage)
const settingsManager = SettingsManager.create(agentDir)
// Validate configured model on startup — catches stale settings from prior installs
// (e.g. grok-2 which no longer exists) and fresh installs with no settings.
// Only resets the default when the configured model no longer exists in the registry;
// never overwrites a valid user choice.
const configuredProvider = settingsManager.getDefaultProvider()
const configuredModel = settingsManager.getDefaultModel()
const allModels = modelRegistry.getAll()
const configuredExists = configuredProvider && configuredModel &&
allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel)
if (!configuredModel || !configuredExists) {
// Fallback: pick the best available Anthropic model
const preferred =
allModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
allModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
allModels.find((m) => m.provider === 'anthropic')
if (preferred) {
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id)
}
}
// Default thinking level: off (always reset if not explicitly set)
if (settingsManager.getDefaultThinkingLevel() !== 'off' && !configuredExists) {
settingsManager.setDefaultThinkingLevel('off')
}
2026-03-10 22:28:37 -06:00
// GSD always uses quiet startup — the gsd extension renders its own branded header
if (!settingsManager.getQuietStartup()) {
settingsManager.setQuietStartup(true)
}
// Collapse changelog by default — avoid wall of text on updates
if (!settingsManager.getCollapseChangelog()) {
settingsManager.setCollapseChangelog(true)
}
// Per-directory session storage — same encoding as the upstream SDK so that
// /resume only shows sessions from the current working directory.
const cwd = process.cwd()
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`
const projectSessionsDir = join(sessionsDir, safePath)
const sessionManager = SessionManager.create(cwd, projectSessionsDir)
2026-03-10 22:28:37 -06:00
initResources(agentDir)
const resourceLoader = buildResourceLoader(agentDir)
await resourceLoader.reload()
const { session, extensionsResult } = await createAgentSession({
authStorage,
modelRegistry,
settingsManager,
sessionManager,
resourceLoader,
})
if (extensionsResult.errors.length > 0) {
for (const err of extensionsResult.errors) {
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`)
}
}
// Restore scoped models from settings on startup.
// The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
// but doesn't apply them to the session at startup — so Ctrl+P cycles all models instead of
// just the saved selection until the user re-runs /scoped-models.
const enabledModelPatterns = settingsManager.getEnabledModels()
if (enabledModelPatterns && enabledModelPatterns.length > 0) {
const availableModels = modelRegistry.getAvailable()
const scopedModels: Array<{ model: (typeof availableModels)[number] }> = []
const seen = new Set<string>()
for (const pattern of enabledModelPatterns) {
// Patterns are "provider/modelId" exact strings saved by /scoped-models
const slashIdx = pattern.indexOf('/')
if (slashIdx !== -1) {
const provider = pattern.substring(0, slashIdx)
const modelId = pattern.substring(slashIdx + 1)
const model = availableModels.find((m) => m.provider === provider && m.id === modelId)
if (model) {
const key = `${model.provider}/${model.id}`
if (!seen.has(key)) {
seen.add(key)
scopedModels.push({ model })
}
}
} else {
// Fallback: match by model id alone
const model = availableModels.find((m) => m.id === pattern)
if (model) {
const key = `${model.provider}/${model.id}`
if (!seen.has(key)) {
seen.add(key)
scopedModels.push({ model })
}
}
}
}
// Only apply if we resolved some models and it's a genuine subset
if (scopedModels.length > 0 && scopedModels.length < availableModels.length) {
session.setScopedModels(scopedModels)
}
}
2026-03-10 22:28:37 -06:00
const interactiveMode = new InteractiveMode(session)
await interactiveMode.run()