import { AuthStorage, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, } from '@mariozechner/pi-coding-agent' import { join } from 'node:path' import { agentDir, sessionsDir, authFilePath } from './app-paths.js' import { buildResourceLoader, initResources } from './resource-loader.js' import { ensureManagedTools } from './tool-bootstrap.js' 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')) 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') } // 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) 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() 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) } } const interactiveMode = new InteractiveMode(session) await interactiveMode.run()