@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-storage.ts +450 -0
  7. package/src/core/auth-storage.ts +102 -183
  8. package/src/core/compaction/branch-summarization.ts +5 -4
  9. package/src/core/compaction/compaction.ts +7 -6
  10. package/src/core/compaction/utils.ts +6 -11
  11. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  12. package/src/core/custom-share.ts +66 -0
  13. package/src/core/history-storage.ts +15 -7
  14. package/src/core/prompt-templates.ts +271 -1
  15. package/src/core/sdk.ts +14 -3
  16. package/src/core/settings-manager.ts +100 -34
  17. package/src/core/slash-commands.ts +4 -1
  18. package/src/core/storage-migration.ts +215 -0
  19. package/src/core/system-prompt.ts +87 -289
  20. package/src/core/title-generator.ts +3 -2
  21. package/src/core/tools/ask.ts +2 -2
  22. package/src/core/tools/bash.ts +2 -1
  23. package/src/core/tools/calculator.ts +2 -1
  24. package/src/core/tools/edit.ts +2 -1
  25. package/src/core/tools/find.ts +2 -1
  26. package/src/core/tools/gemini-image.ts +2 -1
  27. package/src/core/tools/git.ts +2 -2
  28. package/src/core/tools/grep.ts +2 -1
  29. package/src/core/tools/index.test.ts +0 -28
  30. package/src/core/tools/index.ts +0 -6
  31. package/src/core/tools/lsp/index.ts +2 -1
  32. package/src/core/tools/output.ts +2 -1
  33. package/src/core/tools/read.ts +4 -1
  34. package/src/core/tools/ssh.ts +4 -2
  35. package/src/core/tools/task/agents.ts +56 -30
  36. package/src/core/tools/task/commands.ts +9 -8
  37. package/src/core/tools/task/index.ts +7 -15
  38. package/src/core/tools/web-fetch.ts +2 -1
  39. package/src/core/tools/web-search/auth.ts +106 -16
  40. package/src/core/tools/web-search/index.ts +3 -2
  41. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  42. package/src/core/tools/write.ts +2 -1
  43. package/src/core/voice.ts +3 -1
  44. package/src/main.ts +1 -1
  45. package/src/migrations.ts +20 -20
  46. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  47. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  48. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  49. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  50. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  51. package/src/modes/interactive/interactive-mode.ts +364 -3143
  52. package/src/modes/interactive/theme/theme.ts +5 -5
  53. package/src/modes/interactive/types.ts +189 -0
  54. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  55. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  56. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  57. package/src/prompts/agents/frontmatter.md +7 -0
  58. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  59. package/src/prompts/{task.md → agents/task.md} +1 -1
  60. package/src/prompts/review-request.md +44 -8
  61. package/src/prompts/system/custom-system-prompt.md +80 -0
  62. package/src/prompts/system/file-operations.md +12 -0
  63. package/src/prompts/system/system-prompt.md +232 -0
  64. package/src/prompts/system/title-system.md +2 -0
  65. package/src/prompts/tools/bash.md +1 -1
  66. package/src/prompts/tools/read.md +1 -1
  67. package/src/prompts/tools/task.md +9 -3
  68. package/src/core/tools/rulebook.ts +0 -132
  69. package/src/prompts/system-prompt.md +0 -43
  70. package/src/prompts/title-system.md +0 -8
  71. /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
  72. /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
  73. /package/src/prompts/{implement.md → agents/implement.md} +0 -0
  74. /package/src/prompts/{init.md → agents/init.md} +0 -0
  75. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  76. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  77. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  78. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  79. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  80. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  81. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -4,19 +4,26 @@
4
4
  * 4-tier auth resolution:
5
5
  * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
6
6
  * 2. Provider with api="anthropic-messages" in ~/.omp/agent/models.json
7
- * 3. OAuth credentials in ~/.omp/agent/auth.json (with expiry check)
7
+ * 3. OAuth credentials in ~/.omp/agent/agent.db (with expiry check)
8
8
  * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
9
9
  */
10
10
 
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { buildBetaHeader, claudeCodeHeaders, claudeCodeVersion } from "@oh-my-pi/pi-ai";
14
- import { getConfigDirPaths } from "../../../config";
15
- import type { AnthropicAuthConfig, AnthropicOAuthCredential, AuthJson, ModelsJson } from "./types";
14
+ import { getAgentDbPath, getConfigDirPaths } from "../../../config";
15
+ import { AgentStorage } from "../../agent-storage";
16
+ import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "../../auth-storage";
17
+ import { migrateJsonStorage } from "../../storage-migration";
18
+ import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
16
19
 
17
20
  const DEFAULT_BASE_URL = "https://api.anthropic.com";
18
21
 
19
- /** Parse a .env file and return key-value pairs */
22
+ /**
23
+ * Parses a .env file and extracts key-value pairs.
24
+ * @param filePath - Path to the .env file
25
+ * @returns Object containing parsed environment variables
26
+ */
20
27
  async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
21
28
  const result: Record<string, string> = {};
22
29
  try {
@@ -47,7 +54,11 @@ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
47
54
  return result;
48
55
  }
49
56
 
50
- /** Get env var from process.env or .env files */
57
+ /**
58
+ * Gets an environment variable from process.env or .env files.
59
+ * @param key - The environment variable name to look up
60
+ * @returns The value if found, undefined otherwise
61
+ */
51
62
  export async function getEnv(key: string): Promise<string | undefined> {
52
63
  if (process.env[key]) return process.env[key];
53
64
 
@@ -60,7 +71,11 @@ export async function getEnv(key: string): Promise<string | undefined> {
60
71
  return undefined;
61
72
  }
62
73
 
63
- /** Read JSON file safely */
74
+ /**
75
+ * Reads and parses a JSON file safely.
76
+ * @param filePath - Path to the JSON file
77
+ * @returns Parsed JSON content, or null if file doesn't exist or parsing fails
78
+ */
64
79
  async function readJson<T>(filePath: string): Promise<T | null> {
65
80
  try {
66
81
  const file = Bun.file(filePath);
@@ -72,22 +87,85 @@ async function readJson<T>(filePath: string): Promise<T | null> {
72
87
  }
73
88
  }
74
89
 
75
- /** Check if a token is an OAuth token (sk-ant-oat* prefix) */
90
+ /**
91
+ * Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
92
+ * @param apiKey - The API key to check
93
+ * @returns True if the token is an OAuth token
94
+ */
76
95
  export function isOAuthToken(apiKey: string): boolean {
77
96
  return apiKey.includes("sk-ant-oat");
78
97
  }
79
98
 
80
- function normalizeAnthropicOAuthCredentials(entry: AuthJson["anthropic"] | undefined): AnthropicOAuthCredential[] {
99
+ /**
100
+ * Converts a generic AuthCredential to AnthropicOAuthCredential if it's a valid OAuth entry.
101
+ * @param credential - The credential to convert
102
+ * @returns The converted OAuth credential, or null if not a valid OAuth type
103
+ */
104
+ function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthCredential | null {
105
+ if (credential.type !== "oauth") return null;
106
+ if (typeof credential.access !== "string" || typeof credential.expires !== "number") return null;
107
+ return {
108
+ type: "oauth",
109
+ access: credential.access,
110
+ refresh: credential.refresh,
111
+ expires: credential.expires,
112
+ };
113
+ }
114
+
115
+ function normalizeAuthEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] {
81
116
  if (!entry) return [];
82
117
  return Array.isArray(entry) ? entry : [entry];
83
118
  }
84
119
 
120
+ async function readLegacyAnthropicOAuthCredentials(configDir: string): Promise<AnthropicOAuthCredential[]> {
121
+ const authJson = await readJson<AuthStorageData>(path.join(configDir, "auth.json"));
122
+ if (!authJson) return [];
123
+ const entry = authJson.anthropic as AuthCredentialEntry | undefined;
124
+ const credentials = normalizeAuthEntry(entry);
125
+ const results: AnthropicOAuthCredential[] = [];
126
+ for (const credential of credentials) {
127
+ const mapped = toAnthropicOAuthCredential(credential);
128
+ if (mapped) results.push(mapped);
129
+ }
130
+ return results;
131
+ }
132
+
85
133
  /**
86
- * Find Anthropic auth config using 4-tier priority:
134
+ * Reads Anthropic OAuth credentials from agent.db, migrating from legacy auth.json if needed.
135
+ * @param configDir - Path to the config directory containing agent.db
136
+ * @returns Array of valid Anthropic OAuth credentials
137
+ */
138
+ async function readAnthropicOAuthCredentials(configDir: string): Promise<AnthropicOAuthCredential[]> {
139
+ await migrateJsonStorage({
140
+ agentDir: configDir,
141
+ settingsPath: path.join(configDir, "settings.json"),
142
+ authPaths: [path.join(configDir, "auth.json")],
143
+ });
144
+
145
+ const storage = AgentStorage.open(getAgentDbPath(configDir));
146
+ const records = storage.listAuthCredentials("anthropic");
147
+ const credentials: AnthropicOAuthCredential[] = [];
148
+ for (const record of records) {
149
+ const mapped = toAnthropicOAuthCredential(record.credential);
150
+ if (mapped) {
151
+ credentials.push(mapped);
152
+ }
153
+ }
154
+
155
+ if (credentials.length === 0) {
156
+ return readLegacyAnthropicOAuthCredentials(configDir);
157
+ }
158
+
159
+ return credentials;
160
+ }
161
+
162
+ /**
163
+ * Finds Anthropic auth config using 4-tier priority:
87
164
  * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
88
165
  * 2. Provider with api="anthropic-messages" in models.json
89
- * 3. OAuth in auth.json (with 5-minute expiry buffer)
166
+ * 3. OAuth in agent.db (with 5-minute expiry buffer)
90
167
  * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
168
+ * @returns The first valid auth configuration found, or null if none available
91
169
  */
92
170
  export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
93
171
  // Get all config directories (user-level only) for fallback support
@@ -131,14 +209,13 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
131
209
  }
132
210
  }
133
211
 
134
- // 3. OAuth credentials in auth.json (with 5-minute expiry buffer, check all config dirs)
212
+ // 3. OAuth credentials in agent.db (with 5-minute expiry buffer, check all config dirs)
135
213
  const expiryBuffer = 5 * 60 * 1000; // 5 minutes
136
214
  const now = Date.now();
137
215
  for (const configDir of configDirs) {
138
- const authJson = await readJson<AuthJson>(path.join(configDir, "auth.json"));
139
- const credentials = normalizeAnthropicOAuthCredentials(authJson?.anthropic);
216
+ const credentials = await readAnthropicOAuthCredentials(configDir);
140
217
  for (const credential of credentials) {
141
- if (credential.type !== "oauth" || !credential.access) continue;
218
+ if (!credential.access) continue;
142
219
  if (credential.expires > now + expiryBuffer) {
143
220
  return {
144
221
  apiKey: credential.access,
@@ -163,6 +240,11 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
163
240
  return null;
164
241
  }
165
242
 
243
+ /**
244
+ * Checks if a base URL points to the official Anthropic API.
245
+ * @param baseUrl - The base URL to check
246
+ * @returns True if the URL is for api.anthropic.com over HTTPS
247
+ */
166
248
  function isAnthropicBaseUrl(baseUrl: string): boolean {
167
249
  try {
168
250
  const url = new URL(baseUrl);
@@ -172,7 +254,11 @@ function isAnthropicBaseUrl(baseUrl: string): boolean {
172
254
  }
173
255
  }
174
256
 
175
- /** Build headers for Anthropic API request */
257
+ /**
258
+ * Builds HTTP headers for Anthropic API requests.
259
+ * @param auth - The authentication configuration
260
+ * @returns Headers object ready for use in fetch requests
261
+ */
176
262
  export function buildAnthropicHeaders(auth: AnthropicAuthConfig): Record<string, string> {
177
263
  const baseBetas = auth.isOAuth
178
264
  ? [
@@ -205,7 +291,11 @@ export function buildAnthropicHeaders(auth: AnthropicAuthConfig): Record<string,
205
291
  return headers;
206
292
  }
207
293
 
208
- /** Build API URL (OAuth requires ?beta=true) */
294
+ /**
295
+ * Builds the full API URL for Anthropic messages endpoint.
296
+ * @param auth - The authentication configuration
297
+ * @returns The complete API URL with beta query parameter
298
+ */
209
299
  export function buildAnthropicUrl(auth: AnthropicAuthConfig): string {
210
300
  const base = `${auth.baseUrl}/v1/messages`;
211
301
  return `${base}?beta=true`;
@@ -17,6 +17,7 @@ import { Type } from "@sinclair/typebox";
17
17
  import type { Theme } from "../../../modes/interactive/theme/theme";
18
18
  import webSearchDescription from "../../../prompts/tools/web-search.md" with { type: "text" };
19
19
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../custom-tools/types";
20
+ import { renderPromptTemplate } from "../../prompt-templates";
20
21
  import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../exa/mcp-client";
21
22
  import { renderExaCall, renderExaResult } from "../exa/render";
22
23
  import type { ExaRenderDetails } from "../exa/types";
@@ -332,7 +333,7 @@ async function executeWebSearch(
332
333
  export const webSearchTool: AgentTool<typeof webSearchSchema> = {
333
334
  name: "web_search",
334
335
  label: "Web Search",
335
- description: webSearchDescription,
336
+ description: renderPromptTemplate(webSearchDescription),
336
337
  parameters: webSearchSchema,
337
338
  execute: async (toolCallId, params) => {
338
339
  return executeWebSearch(toolCallId, params as WebSearchParams);
@@ -343,7 +344,7 @@ export const webSearchTool: AgentTool<typeof webSearchSchema> = {
343
344
  export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRenderDetails> = {
344
345
  name: "web_search",
345
346
  label: "Web Search",
346
- description: webSearchDescription,
347
+ description: renderPromptTemplate(webSearchDescription),
347
348
  parameters: webSearchSchema,
348
349
 
349
350
  async execute(
@@ -22,6 +22,12 @@ const DEFAULT_MAX_TOKENS = 4096;
22
22
  const WEB_SEARCH_TOOL_NAME = "web_search";
23
23
  const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
24
24
 
25
+ /**
26
+ * Applies OAuth-specific tool prefix to search tool name.
27
+ * @param name - The base tool name
28
+ * @param isOAuth - Whether OAuth authentication is being used
29
+ * @returns Tool name with prefix if OAuth, otherwise unchanged
30
+ */
25
31
  const applySearchToolPrefix = (name: string, isOAuth: boolean): string => {
26
32
  return isOAuth ? applyClaudeToolPrefix(name) : name;
27
33
  };
@@ -33,11 +39,21 @@ export interface AnthropicSearchParams {
33
39
  num_results?: number;
34
40
  }
35
41
 
36
- /** Get model from env or use default */
42
+ /**
43
+ * Gets the model to use for web search from environment or default.
44
+ * @returns Model identifier string
45
+ */
37
46
  async function getModel(): Promise<string> {
38
47
  return (await getEnv("ANTHROPIC_SEARCH_MODEL")) ?? DEFAULT_MODEL;
39
48
  }
40
49
 
50
+ /**
51
+ * Builds system instruction blocks for the Anthropic API request.
52
+ * @param auth - Authentication configuration
53
+ * @param model - Model identifier (affects whether Claude Code instruction is included)
54
+ * @param systemPrompt - Optional custom system prompt
55
+ * @returns Array of system blocks for the API request
56
+ */
41
57
  function buildSystemBlocks(
42
58
  auth: AnthropicAuthConfig,
43
59
  model: string,
@@ -53,7 +69,16 @@ function buildSystemBlocks(
53
69
  });
54
70
  }
55
71
 
56
- /** Call Anthropic API with web search */
72
+ /**
73
+ * Calls the Anthropic API with web search tool enabled.
74
+ * @param auth - Authentication configuration (API key or OAuth)
75
+ * @param model - Model identifier to use
76
+ * @param query - Search query from the user
77
+ * @param systemPrompt - Optional custom system prompt
78
+ * @param maxTokens - Maximum tokens for the response
79
+ * @returns Raw API response from Anthropic
80
+ * @throws {WebSearchProviderError} If the API request fails
81
+ */
57
82
  async function callWebSearch(
58
83
  auth: AnthropicAuthConfig,
59
84
  model: string,
@@ -100,7 +125,11 @@ async function callWebSearch(
100
125
  return response.json() as Promise<AnthropicApiResponse>;
101
126
  }
102
127
 
103
- /** Parse page_age string into seconds (e.g., "2 days ago", "3h ago", "1 week ago") */
128
+ /**
129
+ * Parses a human-readable page age string into seconds.
130
+ * @param pageAge - Age string like "2 days ago", "3h ago", "1 week ago"
131
+ * @returns Age in seconds, or undefined if parsing fails
132
+ */
104
133
  function parsePageAge(pageAge: string | null | undefined): number | undefined {
105
134
  if (!pageAge) return undefined;
106
135
 
@@ -132,7 +161,11 @@ function parsePageAge(pageAge: string | null | undefined): number | undefined {
132
161
  return value * (multipliers[unit] ?? 86400);
133
162
  }
134
163
 
135
- /** Parse API response into unified WebSearchResponse */
164
+ /**
165
+ * Parses the Anthropic API response into a unified WebSearchResponse.
166
+ * @param response - Raw API response containing content blocks
167
+ * @returns Normalized response with answer, sources, citations, and usage
168
+ */
136
169
  function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
137
170
  const answerParts: string[] = [];
138
171
  const searchQueries: string[] = [];
@@ -193,12 +226,17 @@ function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
193
226
  };
194
227
  }
195
228
 
196
- /** Execute Anthropic web search */
229
+ /**
230
+ * Executes a web search using Anthropic's Claude with built-in web search tool.
231
+ * @param params - Search parameters including query and optional settings
232
+ * @returns Search response with synthesized answer, sources, and citations
233
+ * @throws {Error} If no Anthropic credentials are configured
234
+ */
197
235
  export async function searchAnthropic(params: AnthropicSearchParams): Promise<WebSearchResponse> {
198
236
  const auth = await findAnthropicAuth();
199
237
  if (!auth) {
200
238
  throw new Error(
201
- "No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth in ~/.omp/agent/auth.json",
239
+ "No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth in ~/.omp/agent/agent.db",
202
240
  );
203
241
  }
204
242
 
@@ -5,6 +5,7 @@ import { Type } from "@sinclair/typebox";
5
5
  import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
6
6
  import writeDescription from "../../prompts/tools/write.md" with { type: "text" };
7
7
  import type { RenderResultOptions } from "../custom-tools/types";
8
+ import { renderPromptTemplate } from "../prompt-templates";
8
9
  import type { ToolSession } from "../sdk";
9
10
  import { untilAborted } from "../utils";
10
11
  import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
@@ -28,7 +29,7 @@ export function createWriteTool(session: ToolSession): AgentTool<typeof writeSch
28
29
  return {
29
30
  name: "write",
30
31
  label: "Write",
31
- description: writeDescription,
32
+ description: renderPromptTemplate(writeDescription),
32
33
  parameters: writeSchema,
33
34
  execute: async (
34
35
  _toolCallId: string,
package/src/core/voice.ts CHANGED
@@ -7,12 +7,14 @@ import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text"
7
7
  import { logger } from "./logger";
8
8
  import type { ModelRegistry } from "./model-registry";
9
9
  import { findSmolModel } from "./model-resolver";
10
+ import { renderPromptTemplate } from "./prompt-templates";
10
11
  import type { VoiceSettings } from "./settings-manager";
11
12
 
12
13
  const DEFAULT_SAMPLE_RATE = 16000;
13
14
  const DEFAULT_CHANNELS = 1;
14
15
  const DEFAULT_BITS = 16;
15
16
  const SUMMARY_MAX_CHARS = 6000;
17
+ const VOICE_SUMMARY_PROMPT = renderPromptTemplate(voiceSummaryPrompt);
16
18
 
17
19
  export interface VoiceRecordingHandle {
18
20
  filePath: string;
@@ -286,7 +288,7 @@ export async function summarizeForVoice(
286
288
  const truncated = text.length > SUMMARY_MAX_CHARS ? `${text.slice(0, SUMMARY_MAX_CHARS)}...` : text;
287
289
  const request = {
288
290
  model: `${model.provider}/${model.id}`,
289
- systemPrompt: voiceSummaryPrompt,
291
+ systemPrompt: VOICE_SUMMARY_PROMPT,
290
292
  userMessage: `<assistant_response>\n${truncated}\n</assistant_response>`,
291
293
  };
292
294
  logger.debug("voice: summary request", request);
package/src/main.ts CHANGED
@@ -76,7 +76,7 @@ async function runInteractiveMode(
76
76
  mode.renderInitialMessages();
77
77
 
78
78
  if (migratedProviders.length > 0) {
79
- mode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
79
+ mode.showWarning(`Migrated credentials to agent.db: ${migratedProviders.join(", ")}`);
80
80
  }
81
81
 
82
82
  if (modelsJsonError) {
package/src/migrations.ts CHANGED
@@ -3,9 +3,11 @@
3
3
  */
4
4
 
5
5
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
6
+ import { join } from "node:path";
7
7
  import chalk from "chalk";
8
- import { getAgentDir, getBinDir } from "./config";
8
+ import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
9
+ import { AgentStorage } from "./core/agent-storage";
10
+ import type { AuthCredential } from "./core/auth-storage";
9
11
 
10
12
  /**
11
13
  * Migrate PI_* environment variables to OMP_* equivalents.
@@ -29,28 +31,27 @@ export function migrateEnvVars(): string[] {
29
31
  }
30
32
 
31
33
  /**
32
- * Migrate legacy oauth.json and settings.json apiKeys to auth.json.
34
+ * Migrate legacy oauth.json and settings.json apiKeys to agent.db.
33
35
  *
34
36
  * @returns Array of provider names that were migrated
35
37
  */
36
- export function migrateAuthToAuthJson(): string[] {
38
+ export function migrateAuthToAgentDb(): string[] {
37
39
  const agentDir = getAgentDir();
38
- const authPath = join(agentDir, "auth.json");
39
40
  const oauthPath = join(agentDir, "oauth.json");
40
41
  const settingsPath = join(agentDir, "settings.json");
42
+ const storage = AgentStorage.open(getAgentDbPath(agentDir));
41
43
 
42
- // Skip if auth.json already exists
43
- if (existsSync(authPath)) return [];
44
-
45
- const migrated: Record<string, unknown> = {};
44
+ const migrated: Record<string, AuthCredential[]> = {};
46
45
  const providers: string[] = [];
47
46
 
48
- // Migrate oauth.json
49
47
  if (existsSync(oauthPath)) {
50
48
  try {
51
49
  const oauth = JSON.parse(readFileSync(oauthPath, "utf-8"));
52
50
  for (const [provider, cred] of Object.entries(oauth)) {
53
- migrated[provider] = { type: "oauth", ...(cred as object) };
51
+ if (storage.listAuthCredentials(provider).length > 0) {
52
+ continue;
53
+ }
54
+ migrated[provider] = [{ type: "oauth", ...(cred as object) } as AuthCredential];
54
55
  providers.push(provider);
55
56
  }
56
57
  renameSync(oauthPath, `${oauthPath}.migrated`);
@@ -59,17 +60,17 @@ export function migrateAuthToAuthJson(): string[] {
59
60
  }
60
61
  }
61
62
 
62
- // Migrate settings.json apiKeys
63
63
  if (existsSync(settingsPath)) {
64
64
  try {
65
65
  const content = readFileSync(settingsPath, "utf-8");
66
66
  const settings = JSON.parse(content);
67
67
  if (settings.apiKeys && typeof settings.apiKeys === "object") {
68
68
  for (const [provider, key] of Object.entries(settings.apiKeys)) {
69
- if (!migrated[provider] && typeof key === "string") {
70
- migrated[provider] = { type: "api_key", key };
71
- providers.push(provider);
72
- }
69
+ if (typeof key !== "string") continue;
70
+ if (migrated[provider]) continue;
71
+ if (storage.listAuthCredentials(provider).length > 0) continue;
72
+ migrated[provider] = [{ type: "api_key", key }];
73
+ providers.push(provider);
73
74
  }
74
75
  delete settings.apiKeys;
75
76
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -79,9 +80,8 @@ export function migrateAuthToAuthJson(): string[] {
79
80
  }
80
81
  }
81
82
 
82
- if (Object.keys(migrated).length > 0) {
83
- mkdirSync(dirname(authPath), { recursive: true });
84
- writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });
83
+ for (const [provider, credentials] of Object.entries(migrated)) {
84
+ storage.replaceAuthCredentialsForProvider(provider, credentials);
85
85
  }
86
86
 
87
87
  return providers;
@@ -201,7 +201,7 @@ export async function runMigrations(_cwd: string): Promise<{
201
201
  const migratedEnvVars = migrateEnvVars();
202
202
 
203
203
  // Then: run data migrations
204
- const migratedAuthProviders = migrateAuthToAuthJson();
204
+ const migratedAuthProviders = migrateAuthToAgentDb();
205
205
  migrateSessionsFromAgentRoot();
206
206
  migrateToolsToBin();
207
207