diff --git a/docs/porting-from-pi-mono.md b/docs/porting-from-pi-mono.md index baceca91f..137df6e6e 100644 --- a/docs/porting-from-pi-mono.md +++ b/docs/porting-from-pi-mono.md @@ -349,7 +349,7 @@ Our fork has architectural decisions that differ from upstream. **Do not port th | Upstream | Our Fork | Notes | | ------------------------------- | ------------------------------------------- | ----------------------------------------------------- | -| `proper-lockfile` + `auth.json` | `agent.db` (bun:sqlite) | Legacy `auth.json` is migrated; do not reintroduce it | +| `proper-lockfile` + `auth.json` | `agent.db` (bun:sqlite) | Credentials stored exclusively in `agent.db` | | Single credential per provider | Multi-credential with round-robin selection | Session affinity and backoff logic preserved | ### Extensions diff --git a/packages/ai/README.md b/packages/ai/README.md index 75f30c38e..589b21720 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -983,7 +983,7 @@ bunx @oh-my-pi/pi-ai login anthropic # login to specific provider bunx @oh-my-pi/pi-ai list # list available providers ``` -Credentials are saved to `auth.json` in the current directory. +Credentials are saved to `agent.db` in the agent directory. ### Programmatic OAuth @@ -1036,7 +1036,7 @@ const credentials = await loginGitHubCopilot({ // Store credentials yourself const auth = { "github-copilot": { type: "oauth", ...credentials } }; -fs.writeFileSync("auth.json", JSON.stringify(auth, null, 2)); +fs.writeFileSync("credentials.json", JSON.stringify(auth, null, 2)); ``` ### Using OAuth Tokens @@ -1048,7 +1048,7 @@ import { getModel, complete, getOAuthApiKey } from "@oh-my-pi/pi-ai"; import * as fs from "node:fs"; // Load your stored credentials -const auth = JSON.parse(fs.readFileSync("auth.json", "utf-8")); +const auth = JSON.parse(fs.readFileSync("credentials.json", "utf-8")); // Get API key (refreshes if expired) const result = await getOAuthApiKey("github-copilot", auth); @@ -1056,7 +1056,7 @@ if (!result) throw new Error("Not logged in"); // Save refreshed credentials auth["github-copilot"] = { type: "oauth", ...result.newCredentials }; -fs.writeFileSync("auth.json", JSON.stringify(auth, null, 2)); +fs.writeFileSync("credentials.json", JSON.stringify(auth, null, 2)); // Use the API key const model = getModel("github-copilot", "gpt-4o"); diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 79ff9011b..981b3b2f1 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -465,10 +465,6 @@ describe("AI Providers Empty Message Tests", () => { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // ========================================================================= - describe("Anthropic OAuth Provider Empty Messages", () => { const llm = getModel("anthropic", "claude-3-5-haiku-20241022"); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 9a82f1b5f..1e332a8c9 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -321,10 +321,6 @@ describe("Tool Results with Images", () => { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // ========================================================================= - describe("Anthropic OAuth Provider (claude-sonnet-4-5)", () => { const model = getModel("anthropic", "claude-sonnet-4-5"); diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index c7a76c4bc..c31224867 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -1,8 +1,8 @@ /** - * Test helper for resolving API keys from ~/.pi/agent/auth.json + * Test helper for resolving API keys from ~/.pi/agent/testauth.db * * Supports both API key and OAuth credentials. - * OAuth tokens are automatically refreshed if expired and saved back to auth.json. + * OAuth tokens are automatically refreshed if expired and saved back to testauth.db. * * E2E tests are disabled by default. Set E2E=1 environment variable to enable. */ @@ -28,7 +28,7 @@ export function e2eApiKey(envVar: string): string | undefined { return Bun.env[envVar]; } -const AUTH_PATH = path.join(os.homedir(), ".pi", "agent", "auth.json"); +const AUTH_PATH = path.join(os.homedir(), ".pi", "agent", "testauth.db"); type ApiKeyCredential = { type: "api_key"; @@ -59,7 +59,7 @@ async function saveAuthStorage(storage: AuthStorage): Promise { } /** - * Resolve API key for a provider from ~/.pi/agent/auth.json + * Resolve API key for a provider from ~/.pi/agent/testauth.db * * For API key credentials, returns the key directly. * For OAuth credentials, returns the access token (refreshing if expired and saving back). @@ -91,7 +91,7 @@ export async function resolveApiKey(provider: string): Promise { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // Tokens are resolved at module level (see oauthTokens above) - // ========================================================================= - describe("Anthropic OAuth Provider (claude-sonnet-4-20250514)", () => { const model = getModel("anthropic", "claude-sonnet-4-20250514"); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index c4a88e292..4c246f50d 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -180,10 +180,6 @@ describe("Token Statistics on Abort", () => { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // ========================================================================= - describe("Anthropic OAuth Provider", () => { const llm = getModel("anthropic", "claude-3-5-haiku-20241022"); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 474279281..038aff69e 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -206,10 +206,6 @@ describe("Tool Call Without Result Tests", () => { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // ========================================================================= - describe("Anthropic OAuth Provider", () => { const model = getModel("anthropic", "claude-3-5-haiku-20241022"); diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index a8eba5380..403330c8a 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -389,10 +389,6 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { ); }); - // ========================================================================= - // OAuth-based providers (credentials from ~/.pi/agent/oauth.json) - // ========================================================================= - describe("Anthropic OAuth Provider Unicode Handling", () => { const llm = getModel("anthropic", "claude-3-5-haiku-20241022"); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1fe14ec56..6df219de8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Added - Added `omp commit` command to generate commit messages and update changelogs with `--push`, `--dry-run`, `--no-changelog`, and model override flags @@ -29,8 +28,7 @@ - Changed Perplexity search context size from 'high' to 'medium' and added search classifier, reasoning effort, and language preference settings - Increased Perplexity default max tokens from 4096 to 8192 for more comprehensive responses - Updated Anthropic and Gemini search providers to support `max_tokens` and `temperature` parameters for finer control over response generation -- Simplified `AuthStorage.create()` to accept direct agent.db path instead of legacy auth.json path with fallback resolution -- Updated `discoverAuthStorage()` to skip JSON-to-SQLite migration step, improving startup performance +- Simplified `AuthStorage.create()` to accept direct agent.db path - Renamed web search types and exports for consistency: `WebSearchProvider` → `SearchProviderId`, `WebSearchResponse` → `SearchResponse`, `WebSearchTool` → `SearchTool`, and related functions - Refactored web search provider system to use centralized provider registry with `getSearchProvider()` and `resolveProviderChain()` for improved provider management - Updated web search system prompt to emphasize comprehensive, detailed answers with concrete data and specific examples over brevity @@ -49,8 +47,7 @@ ### Removed -- Removed legacy auth.json migration system—credentials are now stored exclusively in agent.db -- Removed `getAuthPath()` configuration function—use `getAgentDbPath()` for credential storage location +- Removed legacy auth.json file—credentials are stored exclusively in agent.db ### Fixed @@ -1703,6 +1700,8 @@ - Fixed editor border rendering glitch after canceling slash command autocomplete - Fixed login/logout credential path message to reference agent.db + - Removed legacy auth.json file—credentials are stored exclusively in agent.db + - Removed legacy auth.json file—credentials are stored exclusively in agent.db ## [4.2.0] - 2026-01-10 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 75950b4f8..a97553a26 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -370,7 +370,7 @@ When disabled, neither case triggers automatic compaction (use `/compact` manual - Automatically sets environment variables when the application starts - Only sets variables that aren't already present in `Bun.env` - Supports any environment variable, not just API keys -- Order of precedence: existing env vars > settings.json env vars > auth.json env vars +- Order of precedence: existing env vars > settings.json env vars > agent.db > **Note:** Compaction is lossy. The agent loses full conversation access afterward. Size tasks to avoid context limits when possible. For critical context, ask the agent to write a summary to a file, iterate on it until it covers everything, then start a new session with that file. The full session history is preserved in the JSONL file; use `/tree` to revisit any previous point. diff --git a/packages/coding-agent/docs/config-usage.md b/packages/coding-agent/docs/config-usage.md index ab3e30c12..24c12aeef 100644 --- a/packages/coding-agent/docs/config-usage.md +++ b/packages/coding-agent/docs/config-usage.md @@ -50,7 +50,7 @@ Many modules now use the **capability/discovery system** (`discovery/builtin.ts` | `session/agent-session.ts` | `getAgentDbPath` | `~/.omp/agent/agent.db` | Database path | | `session/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage | | `session/agent-storage.ts` | `getAgentDbPath` | `~/.omp/agent/agent.db` | Settings/auth storage | -| `session/auth-storage.ts` | `getAgentDbPath`, `getAuthPath` | agent.db, auth.json | Auth credential storage | +| `session/auth-storage.ts` | `getAgentDbPath` | agent.db | Auth credential storage | | `session/history-storage.ts` | `getAgentDir` | `~/.omp/agent/` | Command history | | `session/storage-migration.ts` | `getAgentDbPath` | `~/.omp/agent/agent.db` | JSON→SQLite migration | | `modes/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes | @@ -78,12 +78,12 @@ These use helpers to check `.omp`, `.pi`, `.claude`, `.codex`, `.gemini` directo | File | Helper Used | Subpath(s) | Levels | | ---------------------------------------- | ------------------------------------------------------ | --------------------------- | ------------ | | `main.ts` | `findConfigFile` | `SYSTEM.md`, `APPEND_SYSTEM.md` | user+project | -| `sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.yml`, `models.json` | user | +| `sdk.ts` | `getConfigDirPaths` | `models.yml`, `models.json` | user | | `lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project | | `task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project | | `extensibility/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project | | `extensibility/custom-commands/loader.ts`| `getConfigDirs` | `commands/` | user+project | -| `web/search/auth.ts` | `getConfigDirPaths`, `getAgentDbPath` | auth.json, agent.db | user | +| `web/search/auth.ts` | `getConfigDirPaths`, `getAgentDbPath` | agent.db | user | | `web/search/providers/codex.ts` | `getConfigDirPaths`, `getAgentDbPath` | auth config | user | | `web/search/providers/gemini.ts` | `getConfigDirPaths`, `getAgentDbPath` | auth config | user | @@ -110,7 +110,6 @@ These modules use `discovery/builtin.ts` which has its own config directory reso ``` User-level (~/.omp/agent/, ~/.pi/agent/, ~/.claude/, ~/.codex/, ~/.gemini/): ├── agent.db ← SQLite storage (settings, auth) -├── auth.json ← Legacy auth (migrated to agent.db) ├── models.yml ← Model configuration (preferred) ├── models.json ← Model configuration (legacy) ├── config.yml ← Settings (alternative to agent.db) diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index dc0d42f1c..4e7972e8d 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -272,7 +272,7 @@ const { session } = await createAgentSession({ `agentDir` is used for: - Global settings (`config.yml` + `agent.db`) -- Primary auth/models locations (`auth.json`, `models.yml`, `models.json`) + - Primary auth/models locations (`agent.db`, `models.yml`, `models.json`) - Prompt templates (`prompts/`) - Custom TS commands (`commands/`) @@ -328,12 +328,13 @@ API key resolution priority (handled by AuthStorage): 3. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) 4. Fallback resolver (for custom provider keys from `models.yml`) -`discoverAuthStorage` also migrates legacy `auth.json` from user config directories (`.pi`, `.claude`, `.codex`, `.gemini`) into `agent.db`. + + `discoverAuthStorage` opens the `agent.db` SQLite database in the agent directory. ```typescript import { AuthStorage, ModelRegistry, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent"; -// Default: uses agentDir/auth.json → agent.db and agentDir/models.yml (with legacy fallbacks) + // Default: uses agentDir/agent.db and agentDir/models.yml const authStorage = await discoverAuthStorage(); const modelRegistry = discoverModels(authStorage); @@ -347,7 +348,7 @@ const { session } = await createAgentSession({ authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key"); // Custom auth storage location (use create(), constructor is private) -const customAuth = await AuthStorage.create("/my/app/auth.json"); +const customAuth = await AuthStorage.create("/my/app/agent.db"); const customRegistry = new ModelRegistry(customAuth, "/my/app/models.yml"); const { session } = await createAgentSession({ @@ -788,7 +789,7 @@ import { } from "@oh-my-pi/pi-coding-agent"; // Auth and Models -const authStorage = await discoverAuthStorage(); // /auth.json → agent.db (with fallbacks) +const authStorage = await discoverAuthStorage(); // /agent.db const modelRegistry = discoverModels(authStorage); // + /models.yml (or models.json) const allModels = modelRegistry.getAll(); // All models (built-in + custom) const available = modelRegistry.getAvailable(); // Only models with API keys diff --git a/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts b/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts index 83136eff7..122948263 100644 --- a/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts +++ b/packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts @@ -12,7 +12,7 @@ import { SessionManager, } from "@oh-my-pi/pi-coding-agent"; -// Default: discoverAuthStorage() uses ~/.omp/agent/auth.json +// Default: discoverAuthStorage() uses ~/.omp/agent/agent.db // discoverModels() loads built-in + custom models from ~/.omp/agent/models.json const authStorage = await discoverAuthStorage(); const modelRegistry = await discoverModels(authStorage); @@ -25,7 +25,7 @@ await createAgentSession({ console.log("Session with default auth storage and model registry"); // Custom auth storage location -const customAuthStorage = new AuthStorage("/tmp/my-app/auth.json"); +const customAuthStorage = await AuthStorage.create("/tmp/my-app/agent.db"); const customModelRegistry = new ModelRegistry(customAuthStorage, "/tmp/my-app/models.json"); await createAgentSession({ diff --git a/packages/coding-agent/examples/sdk/README.md b/packages/coding-agent/examples/sdk/README.md index a289bdd7e..6a7b75d64 100644 --- a/packages/coding-agent/examples/sdk/README.md +++ b/packages/coding-agent/examples/sdk/README.md @@ -77,7 +77,7 @@ const { session } = await createAgentSession({ }); // Full control -const customAuth = new AuthStorage("/my/app/auth.json"); +const customAuth = await AuthStorage.create("/my/app/agent.db"); customAuth.setRuntimeApiKey("anthropic", Bun.env.MY_KEY!); const customRegistry = new ModelRegistry(customAuth); diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 2f6ff9f2c..359af0606 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -159,6 +159,8 @@ export class ConfigFile implements IConfigFile { } else if (configPath.endsWith(".yaml")) { const jsonPath = `${configPath.slice(0, -5)}.json`; migrateJsonToYml(jsonPath, configPath); + } else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) { + // JSON configs are still supported without migration. } else { throw new Error(`Invalid config file path: ${configPath}`); } diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 232d14415..0bd87cbd2 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -255,6 +255,7 @@ export class ModelRegistry { * Reload models from disk (built-in + custom from models.json). */ refresh(): void { + this.modelsConfigFile.invalidate(); this.customProviderApiKeys.clear(); this.configError = undefined; this.loadModels(); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 48096ae48..a24b692fe 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -23,7 +23,6 @@ import { Settings, settings } from "./config/settings"; import { initializeWithSettings } from "./discovery"; import { exportFromFile } from "./export/html"; import type { ExtensionUIContext } from "./extensibility/extensions/types"; -import { runMigrations, showDeprecationWarnings } from "./migrations"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes"; import { initTheme, stopThemeWatcher } from "./modes/theme/theme"; import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk"; @@ -488,13 +487,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise 0) { - notifs.push({ kind: "warn", message: `Migrated credentials to agent.db: ${migratedProviders.join(", ")}` }); - } - // Create AuthStorage and ModelRegistry upfront const authStorage = await discoverAuthStorage(); const modelRegistry = new ModelRegistry(authStorage); @@ -564,11 +556,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise 0) { - await showDeprecationWarnings(deprecationWarnings); - } - let scopedModels: ScopedModel[] = []; const modelPatterns = parsedArgs.models ?? settings.get("enabledModels"); const modelMatchPreferences = { diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts deleted file mode 100644 index a7c6e726a..000000000 --- a/packages/coding-agent/src/migrations.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * One-time migrations that run on startup. - */ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import chalk from "chalk"; -import { getAgentDbPath, getAgentDir } from "./config"; -import { AgentStorage } from "./session/agent-storage"; -import type { AuthCredential } from "./session/auth-storage"; - -type SessionHeader = { - type: "session"; - cwd: string; -}; - -function isSessionHeader(value: unknown): value is SessionHeader { - if (!value || typeof value !== "object") return false; - const record = value as Record; - return record.type === "session" && typeof record.cwd === "string" && record.cwd.length > 0; -} - -/** - * Migrate legacy oauth.json and settings.json apiKeys to agent.db. - * - * @returns Array of provider names that were migrated - */ -export async function migrateAuthToAgentDb(): Promise { - const agentDir = getAgentDir(); - const oauthPath = path.join(agentDir, "oauth.json"); - const settingsPath = path.join(agentDir, "settings.json"); - const storage = await AgentStorage.open(getAgentDbPath(agentDir)); - - const migrated: Record = {}; - const providers: string[] = []; - - try { - const oauth = await Bun.file(oauthPath).json(); - try { - for (const [provider, cred] of Object.entries(oauth)) { - if (storage.listAuthCredentials(provider).length > 0) { - continue; - } - migrated[provider] = [{ type: "oauth", ...(cred as object) } as AuthCredential]; - providers.push(provider); - } - await fs.promises.rename(oauthPath, `${oauthPath}.migrated`); - } catch (error) { - logger.warn("Failed to migrate oauth.json", { path: oauthPath, error: String(error) }); - } - } catch (err) { - if (!isEnoent(err)) { - logger.warn("Failed to read oauth.json", { path: oauthPath, error: String(err) }); - } - } - - try { - const settings = await Bun.file(settingsPath).json(); - try { - if (settings.apiKeys && typeof settings.apiKeys === "object") { - for (const [provider, key] of Object.entries(settings.apiKeys)) { - if (typeof key !== "string") continue; - if (migrated[provider]) continue; - if (storage.listAuthCredentials(provider).length > 0) continue; - migrated[provider] = [{ type: "api_key", key }]; - providers.push(provider); - } - delete settings.apiKeys; - await Bun.write(settingsPath, JSON.stringify(settings, null, 2)); - } - } catch (error) { - logger.warn("Failed to migrate settings.json apiKeys", { path: settingsPath, error: String(error) }); - } - } catch (err) { - if (!isEnoent(err)) { - logger.warn("Failed to read settings.json", { path: settingsPath, error: String(err) }); - } - } - - for (const [provider, credentials] of Object.entries(migrated)) { - storage.replaceAuthCredentialsForProvider(provider, credentials); - } - - return providers; -} - -/** - * Migrate sessions from ~/.omp/agent/*.jsonl to proper session directories. - * - * Bug in v0.30.0: Sessions were saved to ~/.omp/agent/ instead of - * ~/.omp/agent/sessions//. This migration moves them - * to the correct location based on the cwd in their session header. - * - * See: https://github.com/badlogic/pi-mono/issues/320 - */ -export async function migrateSessionsFromAgentRoot(): Promise { - const agentDir = getAgentDir(); - - // Find all .jsonl files directly in agentDir (not in subdirectories) - let files: string[]; - try { - const entries = await fs.promises.readdir(agentDir); - files = entries.filter(f => f.endsWith(".jsonl")).map(f => path.join(agentDir, f)); - } catch (error) { - logger.warn("Failed to read agent directory for session migration", { path: agentDir, error: String(error) }); - return; - } - - if (files.length === 0) return; - - for (const file of files) { - try { - // Read first line to get session header - let content: string; - try { - content = await Bun.file(file).text(); - } catch (err) { - if (isEnoent(err)) continue; - throw err; - } - const entries: unknown[] = Bun.JSONL.parse(content); - const header = entries[0]; - if (!isSessionHeader(header)) continue; - - const cwd = header.cwd; - - // Compute the correct session directory (same encoding as session-manager.ts) - const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; - const correctDir = path.join(agentDir, "sessions", safePath); - - // Create directory if needed - await fs.promises.mkdir(correctDir, { recursive: true }); - - // Move the file - const fileName = file.split("/").pop() || file.split("\\").pop(); - const newPath = path.join(correctDir, fileName!); - - if (fs.existsSync(newPath)) continue; // Skip if target exists - - await fs.promises.rename(file, newPath); - } catch (error) { - logger.warn("Failed to migrate session file", { path: file, error: String(error) }); - } - } -} - -/** - * Run all migrations. Called once on startup. - * - * @param _cwd - Current working directory (reserved for future project-local migrations) - * @returns Object with migration results - */ -export async function runMigrations(_cwd: string): Promise<{ - migratedAuthProviders: string[]; - deprecationWarnings: string[]; -}> { - // Then: run data migrations - const migratedAuthProviders = await migrateAuthToAgentDb(); - await migrateSessionsFromAgentRoot(); - - return { migratedAuthProviders, deprecationWarnings: [] }; -} - -/** - * Display deprecation warnings to the user in interactive mode. - * - * @param warnings - Array of deprecation warning messages - */ -export async function showDeprecationWarnings(warnings: string[]): Promise { - console.log(chalk.yellow("\n⚠ Deprecation Warnings:")); - for (const warning of warnings) { - console.log(chalk.yellow(` • ${warning}`)); - } - console.log(); -} diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 4cbb59e2f..aac1a07d5 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1,4 +1,3 @@ -import * as path from "node:path"; import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; @@ -6,7 +5,7 @@ import { $env, logger, postmortem } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { loadCapability } from "./capability"; import { type Rule, ruleCapability } from "./capability/rule"; -import { getAgentDir, getConfigDirPaths } from "./config"; +import { getAgentDbPath, getAgentDir } from "./config"; import { ModelRegistry } from "./config/model-registry"; import { formatModelString, parseModelString } from "./config/model-resolver"; import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates"; @@ -229,13 +228,10 @@ function getDefaultAgentDir(): string { * Reads from primary path first, then falls back to legacy paths (.pi, .claude). */ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise { - const primaryPath = path.join(agentDir, "auth.json"); - // Get all auth.json paths (user-level only), excluding the primary - const allPaths = getConfigDirPaths("auth.json", { project: false }); - const fallbackPaths = allPaths.filter(p => p !== primaryPath); - logger.debug("discoverAuthStorage", { agentDir, primaryPath, allPaths, fallbackPaths }); + const dbPath = getAgentDbPath(agentDir); + logger.debug("discoverAuthStorage", { agentDir, dbPath }); - const storage = await AuthStorage.create(primaryPath); + const storage = await AuthStorage.create(dbPath); await storage.reload(); return storage; } diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 6dba84c5e..07129c14e 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -329,6 +329,7 @@ export class AgentSession { private _streamingEditAbortTriggered = false; private _streamingEditCheckedLineCounts = new Map(); private _streamingEditFileCache = new Map(); + private _promptInFlight = false; constructor(config: AgentSessionConfig) { this.agent = config.agent; @@ -693,7 +694,11 @@ export class AgentSession { .map(line => line.slice(1)); if (removedLines.length > 0) { const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd()); - const cachedContent = this._streamingEditFileCache.get(resolvedPath); + let cachedContent = this._streamingEditFileCache.get(resolvedPath); + if (cachedContent === undefined) { + this._ensureFileCache(resolvedPath); + cachedContent = this._streamingEditFileCache.get(resolvedPath); + } if (cachedContent !== undefined) { const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line))); if (missing) { @@ -898,7 +903,7 @@ export class AgentSession { /** Whether agent is currently streaming a response */ get isStreaming(): boolean { - return this.agent.state.isStreaming; + return this.agent.state.isStreaming || this._promptInFlight; } /** Current retry attempt (0 if not retrying) */ @@ -1246,95 +1251,100 @@ export class AgentSession { expandedText: string, options?: Pick, ): Promise { - // Flush any pending bash messages before the new prompt - this._flushPendingBashMessages(); - this._flushPendingPythonMessages(); + this._promptInFlight = true; + try { + // Flush any pending bash messages before the new prompt + this._flushPendingBashMessages(); + this._flushPendingPythonMessages(); - // Reset todo reminder count on new user prompt - this._todoReminderCount = 0; + // Reset todo reminder count on new user prompt + this._todoReminderCount = 0; - // Validate model - if (!this.model) { - throw new Error( - "No model selected.\n\n" + - `Use /login, set an API key environment variable, or create ${getAgentDbPath()}\n\n` + - "Then use /model to select a model.", - ); - } + // Validate model + if (!this.model) { + throw new Error( + "No model selected.\n\n" + + `Use /login, set an API key environment variable, or create ${getAgentDbPath()}\n\n` + + "Then use /model to select a model.", + ); + } - // Validate API key - const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId); - if (!apiKey) { - throw new Error( - `No API key found for ${this.model.provider}.\n\n` + - `Use /login, set an API key environment variable, or create ${getAgentDbPath()}`, - ); - } + // Validate API key + const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId); + if (!apiKey) { + throw new Error( + `No API key found for ${this.model.provider}.\n\n` + + `Use /login, set an API key environment variable, or create ${getAgentDbPath()}`, + ); + } - // Check if we need to compact before sending (catches aborted responses) - const lastAssistant = this._findLastAssistantMessage(); - if (lastAssistant) { - await this._checkCompaction(lastAssistant, false); - } + // Check if we need to compact before sending (catches aborted responses) + const lastAssistant = this._findLastAssistantMessage(); + if (lastAssistant) { + await this._checkCompaction(lastAssistant, false); + } - // Build messages array (custom messages if any, then user message) - const messages: AgentMessage[] = []; - const planReferenceMessage = await this._buildPlanReferenceMessage?.(); - if (planReferenceMessage) { - messages.push(planReferenceMessage); - } - const planModeMessage = await this._buildPlanModeMessage(); - if (planModeMessage) { - messages.push(planModeMessage); - } + // Build messages array (custom messages if any, then user message) + const messages: AgentMessage[] = []; + const planReferenceMessage = await this._buildPlanReferenceMessage?.(); + if (planReferenceMessage) { + messages.push(planReferenceMessage); + } + const planModeMessage = await this._buildPlanModeMessage(); + if (planModeMessage) { + messages.push(planModeMessage); + } - messages.push(message); + messages.push(message); - // Inject any pending "nextTurn" messages as context alongside the user message - for (const msg of this._pendingNextTurnMessages) { - messages.push(msg); - } - this._pendingNextTurnMessages = []; + // Inject any pending "nextTurn" messages as context alongside the user message + for (const msg of this._pendingNextTurnMessages) { + messages.push(msg); + } + this._pendingNextTurnMessages = []; - // Auto-read @filepath mentions - const fileMentions = extractFileMentions(expandedText); - if (fileMentions.length > 0) { - const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd(), { - autoResizeImages: this.settings.get("images.autoResize"), - }); - messages.push(...fileMentionMessages); - } + // Auto-read @filepath mentions + const fileMentions = extractFileMentions(expandedText); + if (fileMentions.length > 0) { + const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd(), { + autoResizeImages: this.settings.get("images.autoResize"), + }); + messages.push(...fileMentionMessages); + } - // Emit before_agent_start extension event - if (this._extensionRunner) { - const result = await this._extensionRunner.emitBeforeAgentStart( - expandedText, - options?.images, - this._baseSystemPrompt, - ); - if (result?.messages) { - for (const msg of result.messages) { - messages.push({ - role: "custom", - customType: msg.customType, - content: msg.content, - display: msg.display, - details: msg.details, - timestamp: Date.now(), - }); + // Emit before_agent_start extension event + if (this._extensionRunner) { + const result = await this._extensionRunner.emitBeforeAgentStart( + expandedText, + options?.images, + this._baseSystemPrompt, + ); + if (result?.messages) { + for (const msg of result.messages) { + messages.push({ + role: "custom", + customType: msg.customType, + content: msg.content, + display: msg.display, + details: msg.details, + timestamp: Date.now(), + }); + } + } + + if (result?.systemPrompt !== undefined) { + this.agent.setSystemPrompt(result.systemPrompt); + } else { + this.agent.setSystemPrompt(this._baseSystemPrompt); } } - if (result?.systemPrompt !== undefined) { - this.agent.setSystemPrompt(result.systemPrompt); - } else { - this.agent.setSystemPrompt(this._baseSystemPrompt); - } + const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined; + await this.agent.prompt(messages, agentPromptOptions); + await this.waitForRetry(); + } finally { + this._promptInFlight = false; } - - const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined; - await this.agent.prompt(messages, agentPromptOptions); - await this.waitForRetry(); } /** diff --git a/packages/coding-agent/src/session/auth-storage.ts b/packages/coding-agent/src/session/auth-storage.ts index db9bb16fe..05e2be311 100644 --- a/packages/coding-agent/src/session/auth-storage.ts +++ b/packages/coding-agent/src/session/auth-storage.ts @@ -137,7 +137,7 @@ class AuthStorageUsageCache implements UsageCache { /** * Credential storage backed by agent.db. - * Reads from SQLite and migrates legacy auth.json paths. + * Reads from SQLite (agent.db). */ export class AuthStorage { private static readonly defaultBackoffMs = 60_000; // Default backoff when no reset time available @@ -159,7 +159,6 @@ export class AuthStorage { private fallbackResolver?: (provider: string) => string | undefined; private constructor( - private dbPath: string, private storage: AgentStorage, options: AuthStorageOptions = {}, ) { @@ -181,7 +180,7 @@ export class AuthStorage { */ static async create(dbPath: string, options: AuthStorageOptions = {}): Promise { const storage = await AgentStorage.open(dbPath); - return new AuthStorage(dbPath, storage, options); + return new AuthStorage(storage, options); } /** @@ -193,7 +192,6 @@ export class AuthStorage { const storage = await AgentStorage.open(dbPath); const instance = Object.create(AuthStorage.prototype) as AuthStorage; - instance.dbPath = dbPath; instance.storage = storage; instance.data = new Map(); instance.runtimeOverrides = new Map(); @@ -252,7 +250,6 @@ export class AuthStorage { return { credentials, runtimeOverrides: Object.keys(runtimeOverrides).length > 0 ? runtimeOverrides : undefined, - dbPath: this.dbPath, }; } @@ -281,7 +278,7 @@ export class AuthStorage { /** * Reload credentials from agent.db. - * Migrates legacy auth.json/settings.json on first load. + * Reloads credentials from the database. */ async reload(): Promise { const records = this.storage.listAuthCredentials(); diff --git a/packages/coding-agent/src/web/search/auth.ts b/packages/coding-agent/src/web/search/auth.ts index 35a8e4be0..d0cd4935c 100644 --- a/packages/coding-agent/src/web/search/auth.ts +++ b/packages/coding-agent/src/web/search/auth.ts @@ -7,12 +7,11 @@ * 3. OAuth credentials in ~/.omp/agent/agent.db (with expiry check) * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback */ -import * as path from "node:path"; import { buildAnthropicHeaders as buildProviderAnthropicHeaders, getEnvApiKey } from "@oh-my-pi/pi-ai"; import { $env, logger } from "@oh-my-pi/pi-utils"; import { getAgentDbPath, getConfigDirPaths } from "../../config"; import { AgentStorage } from "../../session/agent-storage"; -import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "../../session/auth-storage"; +import type { AuthCredential } from "../../session/auth-storage"; import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types"; const DEFAULT_BASE_URL = "https://api.anthropic.com"; @@ -59,26 +58,8 @@ function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthC }; } -function normalizeAuthEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] { - if (!entry) return []; - return Array.isArray(entry) ? entry : [entry]; -} - -async function readLegacyAnthropicOAuthCredentials(configDir: string): Promise { - const authJson = await readJson(path.join(configDir, "auth.json")); - if (!authJson) return []; - const entry = authJson.anthropic as AuthCredentialEntry | undefined; - const credentials = normalizeAuthEntry(entry); - const results: AnthropicOAuthCredential[] = []; - for (const credential of credentials) { - const mapped = toAnthropicOAuthCredential(credential); - if (mapped) results.push(mapped); - } - return results; -} - /** - * Reads Anthropic OAuth credentials from agent.db, migrating from legacy auth.json if needed. + * Reads Anthropic OAuth credentials from agent.db. * @param configDir - Path to the config directory containing agent.db * @returns Array of valid Anthropic OAuth credentials */ @@ -93,10 +74,6 @@ async function readAnthropicOAuthCredentials(configDir: string): Promise { // 2. Provider with api="anthropic-messages" in models.json (check all config dirs) for (const configDir of configDirs) { - const modelsJson = await readJson(path.join(configDir, "models.json")); + const modelsJson = await readJson(`${configDir}/models.json`); if (modelsJson?.providers) { // First pass: look for providers with actual API keys for (const [_name, provider] of Object.entries(modelsJson.providers)) { diff --git a/packages/coding-agent/src/web/search/types.ts b/packages/coding-agent/src/web/search/types.ts index ab04c08da..21f810bd6 100644 --- a/packages/coding-agent/src/web/search/types.ts +++ b/packages/coding-agent/src/web/search/types.ts @@ -88,7 +88,7 @@ export interface ModelsJson { >; } -/** auth.json structure for OAuth credentials */ +/** OAuth credential for Anthropic API access */ export interface AnthropicOAuthCredential { type: "oauth"; access: string; @@ -97,12 +97,6 @@ export interface AnthropicOAuthCredential { expires: number; } -export type AnthropicAuthJsonEntry = AnthropicOAuthCredential | AnthropicOAuthCredential[]; - -export interface AuthJson { - anthropic?: AnthropicAuthJsonEntry; -} - /** Anthropic API response types */ export interface AnthropicSearchResult { type: "web_search_result"; diff --git a/packages/coding-agent/test/agent-session-branching.test.ts b/packages/coding-agent/test/agent-session-branching.test.ts index 9553a2c1d..4de8748d3 100644 --- a/packages/coding-agent/test/agent-session-branching.test.ts +++ b/packages/coding-agent/test/agent-session-branching.test.ts @@ -64,8 +64,8 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("AgentSession branching", () => sessionManager = noSession ? SessionManager.inMemory() : SessionManager.create(tempDir); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); - const modelRegistry = new ModelRegistry(authStorage, tempDir); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); session = new AgentSession({ agent, diff --git a/packages/coding-agent/test/agent-session-compaction.test.ts b/packages/coding-agent/test/agent-session-compaction.test.ts index 79ce6d18a..d1c84d86b 100644 --- a/packages/coding-agent/test/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-compaction.test.ts @@ -68,7 +68,7 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("AgentSession compaction e2e", sessionManager = inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir); const settings = Settings.isolated({ "compaction.keepRecentTokens": 1 }); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); const modelRegistry = new ModelRegistry(authStorage); session = new AgentSession({ diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 4a8b523dc..18ecec5e4 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -89,8 +89,8 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); - const modelRegistry = new ModelRegistry(authStorage, tempDir); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); authStorage.setRuntimeApiKey("anthropic", "test-key"); session = new AgentSession({ @@ -179,8 +179,8 @@ describe("AgentSession concurrent prompt guard", () => { const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); - const modelRegistry = new ModelRegistry(authStorage, tempDir); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); authStorage.setRuntimeApiKey("anthropic", "test-key"); session = new AgentSession({ diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index 078d358c0..2f8e48947 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -104,7 +104,7 @@ describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Compaction hooks", () => { const sessionManager = SessionManager.create(tempDir); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); const modelRegistry = new ModelRegistry(authStorage); hookRunner = new HookRunner(hooks, tempDir, sessionManager, modelRegistry); diff --git a/packages/coding-agent/test/compaction-thinking-model.test.ts b/packages/coding-agent/test/compaction-thinking-model.test.ts index 5f5084bd3..5e92aa840 100644 --- a/packages/coding-agent/test/compaction-thinking-model.test.ts +++ b/packages/coding-agent/test/compaction-thinking-model.test.ts @@ -76,7 +76,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); const modelRegistry = new ModelRegistry(authStorage); session = new AgentSession({ @@ -179,7 +179,7 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic const sessionManager = SessionManager.inMemory(); const settings = Settings.isolated(); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); const modelRegistry = new ModelRegistry(authStorage); session = new AgentSession({ diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index f6616bdb9..86b0cbeb5 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -23,7 +23,7 @@ describe("ExtensionRunner", () => { extensionsDir = path.join(tempDir.path(), ".omp", "extensions"); fs.mkdirSync(extensionsDir, { recursive: true }); sessionManager = SessionManager.inMemory(); - const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db")); modelRegistry = new ModelRegistry(authStorage); }); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index a5900e123..c773294be 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -15,7 +15,7 @@ describe("ModelRegistry", () => { tempDir = path.join(os.tmpdir(), `pi-test-model-registry-${Snowflake.next()}`); fs.mkdirSync(tempDir, { recursive: true }); modelsJsonPath = path.join(tempDir, "models.json"); - authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); }); afterEach(() => { diff --git a/packages/coding-agent/test/streaming-edit-abort.test.ts b/packages/coding-agent/test/streaming-edit-abort.test.ts index d706dc296..3419f774f 100644 --- a/packages/coding-agent/test/streaming-edit-abort.test.ts +++ b/packages/coding-agent/test/streaming-edit-abort.test.ts @@ -93,9 +93,9 @@ async function createSession(tempDir: string, streamFn: Agent["streamFn"], tool: const sessionManager = SessionManager.inMemory(tempDir); const settings = Settings.isolated({ "edit.streamingAbort": true }); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); authStorage.setRuntimeApiKey("anthropic", "test-key"); - const modelRegistry = new ModelRegistry(authStorage, tempDir); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); return new AgentSession({ agent, diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index 3c289abb0..05d12ee65 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -99,8 +99,8 @@ export async function createTestSession(options: TestSessionOptions = {}): Promi const sessionManager = options.inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir); const settings = Settings.isolated(options.settingsOverrides); - const authStorage = await AuthStorage.create(path.join(tempDir, "auth.json")); - const modelRegistry = new ModelRegistry(authStorage, tempDir); + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); const session = new AgentSession({ agent, sessionManager,