@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.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 (231) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +7 -7
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/index.ts +4 -4
  82. package/src/patch/normalize.ts +22 -65
  83. package/src/patch/shared.ts +16 -16
  84. package/src/prompts/system/custom-system-prompt.md +0 -10
  85. package/src/prompts/system/system-prompt.md +69 -89
  86. package/src/prompts/tools/async-result.md +5 -0
  87. package/src/prompts/tools/bash.md +5 -0
  88. package/src/prompts/tools/cancel-job.md +7 -0
  89. package/src/prompts/tools/poll-jobs.md +7 -0
  90. package/src/prompts/tools/task.md +4 -0
  91. package/src/sdk.ts +70 -6
  92. package/src/session/agent-session.ts +40 -6
  93. package/src/session/agent-storage.ts +69 -278
  94. package/src/session/auth-storage.ts +14 -1430
  95. package/src/session/session-manager.ts +69 -5
  96. package/src/session/session-storage.ts +1 -5
  97. package/src/session/streaming-output.ts +637 -76
  98. package/src/slash-commands/builtin-registry.ts +8 -0
  99. package/src/ssh/connection-manager.ts +4 -12
  100. package/src/ssh/sshfs-mount.ts +3 -7
  101. package/src/ssh/utils.ts +8 -0
  102. package/src/system-prompt.ts +24 -90
  103. package/src/task/executor.ts +11 -1
  104. package/src/task/index.ts +258 -13
  105. package/src/task/parallel.ts +32 -0
  106. package/src/task/render.ts +15 -7
  107. package/src/task/types.ts +5 -0
  108. package/src/tools/ask.ts +4 -7
  109. package/src/tools/bash-interactive.ts +4 -5
  110. package/src/tools/bash.ts +125 -41
  111. package/src/tools/cancel-job.ts +93 -0
  112. package/src/tools/fetch.ts +7 -27
  113. package/src/tools/find.ts +3 -3
  114. package/src/tools/gemini-image.ts +15 -14
  115. package/src/tools/grep.ts +3 -3
  116. package/src/tools/index.ts +13 -29
  117. package/src/tools/json-tree.ts +12 -1
  118. package/src/tools/jtd-to-json-schema.ts +10 -74
  119. package/src/tools/jtd-to-typescript.ts +10 -72
  120. package/src/tools/jtd-utils.ts +102 -0
  121. package/src/tools/notebook.ts +4 -9
  122. package/src/tools/output-meta.ts +52 -26
  123. package/src/tools/path-utils.ts +13 -7
  124. package/src/tools/poll-jobs.ts +178 -0
  125. package/src/tools/python.ts +32 -35
  126. package/src/tools/read.ts +61 -82
  127. package/src/tools/render-utils.ts +8 -159
  128. package/src/tools/ssh.ts +7 -20
  129. package/src/tools/submit-result.ts +1 -1
  130. package/src/tools/tool-errors.ts +0 -30
  131. package/src/tools/tool-result.ts +1 -2
  132. package/src/tools/write.ts +8 -10
  133. package/src/tui/code-cell.ts +8 -3
  134. package/src/tui/status-line.ts +4 -4
  135. package/src/tui/types.ts +0 -1
  136. package/src/tui/utils.ts +1 -14
  137. package/src/utils/command-args.ts +76 -0
  138. package/src/utils/file-mentions.ts +15 -19
  139. package/src/utils/frontmatter.ts +5 -10
  140. package/src/utils/shell-snapshot.ts +0 -11
  141. package/src/utils/title-generator.ts +0 -12
  142. package/src/web/scrapers/artifacthub.ts +7 -16
  143. package/src/web/scrapers/arxiv.ts +3 -8
  144. package/src/web/scrapers/aur.ts +8 -22
  145. package/src/web/scrapers/biorxiv.ts +5 -14
  146. package/src/web/scrapers/bluesky.ts +13 -36
  147. package/src/web/scrapers/brew.ts +5 -10
  148. package/src/web/scrapers/cheatsh.ts +2 -12
  149. package/src/web/scrapers/chocolatey.ts +63 -26
  150. package/src/web/scrapers/choosealicense.ts +3 -18
  151. package/src/web/scrapers/cisa-kev.ts +4 -18
  152. package/src/web/scrapers/clojars.ts +6 -33
  153. package/src/web/scrapers/coingecko.ts +25 -33
  154. package/src/web/scrapers/crates-io.ts +7 -26
  155. package/src/web/scrapers/crossref.ts +4 -18
  156. package/src/web/scrapers/devto.ts +11 -41
  157. package/src/web/scrapers/discogs.ts +7 -10
  158. package/src/web/scrapers/discourse.ts +6 -31
  159. package/src/web/scrapers/dockerhub.ts +12 -35
  160. package/src/web/scrapers/fdroid.ts +8 -33
  161. package/src/web/scrapers/firefox-addons.ts +10 -34
  162. package/src/web/scrapers/flathub.ts +7 -24
  163. package/src/web/scrapers/github-gist.ts +2 -12
  164. package/src/web/scrapers/github.ts +9 -47
  165. package/src/web/scrapers/gitlab.ts +130 -185
  166. package/src/web/scrapers/go-pkg.ts +12 -22
  167. package/src/web/scrapers/hackage.ts +88 -43
  168. package/src/web/scrapers/hackernews.ts +25 -45
  169. package/src/web/scrapers/hex.ts +19 -36
  170. package/src/web/scrapers/huggingface.ts +26 -91
  171. package/src/web/scrapers/iacr.ts +3 -8
  172. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  173. package/src/web/scrapers/lemmy.ts +5 -23
  174. package/src/web/scrapers/lobsters.ts +16 -28
  175. package/src/web/scrapers/mastodon.ts +24 -43
  176. package/src/web/scrapers/maven.ts +6 -21
  177. package/src/web/scrapers/mdn.ts +7 -11
  178. package/src/web/scrapers/metacpan.ts +9 -41
  179. package/src/web/scrapers/musicbrainz.ts +4 -28
  180. package/src/web/scrapers/npm.ts +8 -25
  181. package/src/web/scrapers/nuget.ts +14 -37
  182. package/src/web/scrapers/nvd.ts +6 -28
  183. package/src/web/scrapers/ollama.ts +7 -34
  184. package/src/web/scrapers/open-vsx.ts +5 -19
  185. package/src/web/scrapers/opencorporates.ts +30 -14
  186. package/src/web/scrapers/openlibrary.ts +49 -33
  187. package/src/web/scrapers/orcid.ts +4 -18
  188. package/src/web/scrapers/osv.ts +7 -24
  189. package/src/web/scrapers/packagist.ts +9 -24
  190. package/src/web/scrapers/pub-dev.ts +7 -50
  191. package/src/web/scrapers/pubmed.ts +54 -21
  192. package/src/web/scrapers/pypi.ts +8 -26
  193. package/src/web/scrapers/rawg.ts +11 -19
  194. package/src/web/scrapers/readthedocs.ts +4 -9
  195. package/src/web/scrapers/reddit.ts +5 -15
  196. package/src/web/scrapers/repology.ts +8 -20
  197. package/src/web/scrapers/rfc.ts +5 -14
  198. package/src/web/scrapers/rubygems.ts +6 -21
  199. package/src/web/scrapers/searchcode.ts +8 -36
  200. package/src/web/scrapers/sec-edgar.ts +4 -18
  201. package/src/web/scrapers/semantic-scholar.ts +15 -35
  202. package/src/web/scrapers/snapcraft.ts +5 -19
  203. package/src/web/scrapers/sourcegraph.ts +5 -43
  204. package/src/web/scrapers/spdx.ts +4 -18
  205. package/src/web/scrapers/spotify.ts +4 -23
  206. package/src/web/scrapers/stackoverflow.ts +8 -13
  207. package/src/web/scrapers/terraform.ts +9 -37
  208. package/src/web/scrapers/tldr.ts +3 -7
  209. package/src/web/scrapers/twitter.ts +3 -7
  210. package/src/web/scrapers/types.ts +105 -27
  211. package/src/web/scrapers/utils.ts +97 -103
  212. package/src/web/scrapers/vimeo.ts +7 -27
  213. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  214. package/src/web/scrapers/w3c.ts +6 -14
  215. package/src/web/scrapers/wikidata.ts +5 -19
  216. package/src/web/scrapers/wikipedia.ts +2 -12
  217. package/src/web/scrapers/youtube.ts +5 -34
  218. package/src/web/search/index.ts +0 -9
  219. package/src/web/search/providers/anthropic.ts +3 -2
  220. package/src/web/search/providers/brave.ts +3 -18
  221. package/src/web/search/providers/exa.ts +1 -12
  222. package/src/web/search/providers/kimi.ts +5 -44
  223. package/src/web/search/providers/perplexity.ts +1 -12
  224. package/src/web/search/providers/synthetic.ts +3 -26
  225. package/src/web/search/providers/utils.ts +36 -0
  226. package/src/web/search/providers/zai.ts +9 -50
  227. package/src/web/search/types.ts +0 -28
  228. package/src/web/search/utils.ts +17 -0
  229. package/src/tools/output-utils.ts +0 -63
  230. package/src/tools/truncate.ts +0 -385
  231. package/src/web/search/auth.ts +0 -178
@@ -1,10 +1,10 @@
1
1
  import { Database, type Statement } from "bun:sqlite";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { logger } from "@oh-my-pi/pi-utils";
4
+ import { type AuthCredential, AuthCredentialStore, type StoredAuthCredential } from "@oh-my-pi/pi-ai";
5
+ import { isRecord, logger } from "@oh-my-pi/pi-utils";
5
6
  import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
6
7
  import type { RawSettings as Settings } from "../config/settings";
7
- import type { AuthCredential } from "./auth-storage";
8
8
 
9
9
  /** Row shape for settings table queries */
10
10
  type SettingsRow = {
@@ -12,126 +12,28 @@ type SettingsRow = {
12
12
  value: string;
13
13
  };
14
14
 
15
- /** Row shape for auth_credentials table queries */
16
- type AuthRow = {
17
- id: number;
18
- provider: string;
19
- credential_type: string;
20
- data: string;
21
- };
22
-
23
15
  /** Row shape for model_usage table queries */
24
16
  type ModelUsageRow = {
25
17
  model_key: string;
26
18
  last_used_at: number;
27
19
  };
28
20
 
29
- /**
30
- * Auth credential with database row ID for updates/deletes.
31
- * Wraps AuthCredential with storage metadata.
32
- */
33
- export interface StoredAuthCredential {
34
- id: number;
35
- provider: string;
36
- credential: AuthCredential;
37
- }
38
-
39
21
  /** Bump when schema changes require migration */
40
22
  const SCHEMA_VERSION = 4;
41
23
 
42
- /**
43
- * Type guard for plain objects.
44
- * @param value - Value to check
45
- * @returns True if value is a non-null, non-array object
46
- */
47
- function isRecord(value: unknown): value is Record<string, unknown> {
48
- return !!value && typeof value === "object" && !Array.isArray(value);
49
- }
50
-
51
- /**
52
- * Converts credential to DB format, stripping the type discriminant from the data blob.
53
- * @param credential - The credential to serialize
54
- * @returns Object with credentialType and JSON data string, or null for unknown types
55
- */
56
- function serializeCredential(
57
- credential: AuthCredential,
58
- ): { credentialType: AuthCredential["type"]; data: string } | null {
59
- if (credential.type === "api_key") {
60
- return {
61
- credentialType: "api_key",
62
- data: JSON.stringify({ key: credential.key }),
63
- };
64
- }
65
- if (credential.type === "oauth") {
66
- const { type: _type, ...rest } = credential;
67
- return {
68
- credentialType: "oauth",
69
- data: JSON.stringify(rest),
70
- };
71
- }
72
- return null;
73
- }
74
-
75
- /**
76
- * Reconstructs credential from DB row, re-adding the type discriminant.
77
- * @param row - Database row containing credential data
78
- * @returns Reconstructed AuthCredential, or null if parsing fails or type is unknown
79
- */
80
- function deserializeCredential(row: AuthRow): AuthCredential | null {
81
- let parsed: unknown;
82
- try {
83
- parsed = JSON.parse(row.data);
84
- } catch (error) {
85
- logger.warn("AgentStorage failed to parse auth credential", {
86
- provider: row.provider,
87
- id: row.id,
88
- error: String(error),
89
- });
90
- return null;
91
- }
92
- if (!isRecord(parsed)) {
93
- logger.warn("AgentStorage auth credential data invalid", {
94
- provider: row.provider,
95
- id: row.id,
96
- });
97
- return null;
98
- }
99
- if (row.credential_type === "api_key") {
100
- return { type: "api_key", ...(parsed as Record<string, unknown>) } as AuthCredential;
101
- }
102
- if (row.credential_type === "oauth") {
103
- return { type: "oauth", ...(parsed as Record<string, unknown>) } as AuthCredential;
104
- }
105
- logger.warn("AgentStorage unknown credential type", {
106
- provider: row.provider,
107
- id: row.id,
108
- type: row.credential_type,
109
- });
110
- return null;
111
- }
24
+ /** Singleton instances per database path */
25
+ const instances = new Map<string, AgentStorage>();
112
26
 
113
27
  /**
114
- * Unified SQLite storage for agent settings and auth credentials.
28
+ * Unified SQLite storage for agent settings, model usage, and auth credentials.
29
+ * Delegates auth credential operations to AuthCredentialStore from @oh-my-pi/pi-ai.
115
30
  * Uses singleton pattern per database path; access via AgentStorage.open().
116
31
  */
117
32
  export class AgentStorage {
118
33
  #db: Database;
119
- static #instances = new Map<string, AgentStorage>();
34
+ #authStore: AuthCredentialStore;
120
35
 
121
36
  #listSettingsStmt: Statement;
122
- #getCacheStmt: Statement;
123
- #upsertCacheStmt: Statement;
124
- #deleteExpiredCacheStmt: Statement;
125
- #listAuthStmt: Statement;
126
- #listAuthByProviderStmt: Statement;
127
- #listActiveAuthStmt: Statement;
128
- #listActiveAuthByProviderStmt: Statement;
129
- #insertAuthStmt: Statement;
130
- #updateAuthStmt: Statement;
131
- #deleteAuthStmt: Statement;
132
- #deleteAuthByProviderStmt: Statement;
133
- #countAuthStmt: Statement;
134
- #disableAuthStmt: Statement;
135
37
  #upsertModelUsageStmt: Statement;
136
38
  #listModelUsageStmt: Statement;
137
39
  #modelUsageCache: string[] | null = null;
@@ -154,38 +56,10 @@ export class AgentStorage {
154
56
  this.#initializeSchema();
155
57
  this.#hardenPermissions(dbPath);
156
58
 
157
- this.#listSettingsStmt = this.#db.prepare("SELECT key, value FROM settings");
158
-
159
- this.#getCacheStmt = this.#db.prepare("SELECT value FROM cache WHERE key = ? AND expires_at > unixepoch()");
160
- this.#upsertCacheStmt = this.#db.prepare(
161
- "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
162
- );
163
- this.#deleteExpiredCacheStmt = this.#db.prepare("DELETE FROM cache WHERE expires_at <= unixepoch()");
59
+ // Create AuthCredentialStore with our open database
60
+ this.#authStore = new AuthCredentialStore(this.#db);
164
61
 
165
- this.#listAuthStmt = this.#db.prepare(
166
- "SELECT id, provider, credential_type, data FROM auth_credentials ORDER BY id ASC",
167
- );
168
- this.#listAuthByProviderStmt = this.#db.prepare(
169
- "SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? ORDER BY id ASC",
170
- );
171
- this.#listActiveAuthStmt = this.#db.prepare(
172
- "SELECT id, provider, credential_type, data FROM auth_credentials WHERE disabled = 0 ORDER BY id ASC",
173
- );
174
- this.#listActiveAuthByProviderStmt = this.#db.prepare(
175
- "SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? AND disabled = 0 ORDER BY id ASC",
176
- );
177
- this.#insertAuthStmt = this.#db.prepare(
178
- "INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, ?, ?) RETURNING id",
179
- );
180
- this.#updateAuthStmt = this.#db.prepare(
181
- "UPDATE auth_credentials SET credential_type = ?, data = ?, updated_at = unixepoch() WHERE id = ?",
182
- );
183
- this.#deleteAuthStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE id = ?");
184
- this.#deleteAuthByProviderStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
185
- this.#disableAuthStmt = this.#db.prepare(
186
- "UPDATE auth_credentials SET disabled = 1, updated_at = unixepoch() WHERE id = ?",
187
- );
188
- this.#countAuthStmt = this.#db.prepare("SELECT COUNT(*) as count FROM auth_credentials");
62
+ this.#listSettingsStmt = this.#db.prepare("SELECT key, value FROM settings");
189
63
 
190
64
  this.#upsertModelUsageStmt = this.#db.prepare(
191
65
  "INSERT INTO model_usage (model_key, last_used_at) VALUES (?, unixepoch()) ON CONFLICT(model_key) DO UPDATE SET last_used_at = unixepoch()",
@@ -196,8 +70,8 @@ export class AgentStorage {
196
70
  }
197
71
 
198
72
  /**
199
- * Creates tables if missing and migrates legacy single-blob settings to key-value format.
200
- * Handles v1 to v2 schema migration for settings table.
73
+ * Creates tables if missing and migrates legacy settings.
74
+ * AuthCredentialStore handles auth_credentials and cache tables.
201
75
  */
202
76
  #initializeSchema(): void {
203
77
  this.#db.exec(`
@@ -205,24 +79,6 @@ PRAGMA journal_mode=WAL;
205
79
  PRAGMA synchronous=NORMAL;
206
80
  PRAGMA busy_timeout=5000;
207
81
 
208
- CREATE TABLE IF NOT EXISTS auth_credentials (
209
- id INTEGER PRIMARY KEY AUTOINCREMENT,
210
- provider TEXT NOT NULL,
211
- credential_type TEXT NOT NULL,
212
- data TEXT NOT NULL,
213
- disabled INTEGER NOT NULL DEFAULT 0,
214
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
215
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
216
- );
217
- CREATE INDEX IF NOT EXISTS idx_auth_provider ON auth_credentials(provider);
218
-
219
- CREATE TABLE IF NOT EXISTS cache (
220
- key TEXT PRIMARY KEY,
221
- value TEXT NOT NULL,
222
- expires_at INTEGER NOT NULL
223
- );
224
- CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
225
-
226
82
  CREATE TABLE IF NOT EXISTS model_usage (
227
83
  model_key TEXT PRIMARY KEY,
228
84
  last_used_at INTEGER NOT NULL DEFAULT (unixepoch())
@@ -301,11 +157,8 @@ CREATE TABLE settings (
301
157
 
302
158
  #migrateSchema(fromVersion: number): void {
303
159
  if (fromVersion < 4) {
304
- // v3 → v4: Add disabled column to auth_credentials
305
- const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
306
- if (!cols.some(c => c.name === "disabled")) {
307
- this.#db.exec("ALTER TABLE auth_credentials ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0");
308
- }
160
+ // v3 → v4: Add disabled column to auth_credentials (handled by AuthCredentialStore)
161
+ // Nothing to do here - AuthCredentialStore will handle this migration
309
162
  }
310
163
  }
311
164
 
@@ -316,7 +169,7 @@ CREATE TABLE settings (
316
169
  * @returns AgentStorage instance for the given path
317
170
  */
318
171
  static async open(dbPath: string = getAgentDbPath()): Promise<AgentStorage> {
319
- const existing = AgentStorage.#instances.get(dbPath);
172
+ const existing = instances.get(dbPath);
320
173
  if (existing) return existing;
321
174
 
322
175
  const maxRetries = 3;
@@ -326,7 +179,7 @@ CREATE TABLE settings (
326
179
  for (let attempt = 0; attempt < maxRetries; attempt++) {
327
180
  try {
328
181
  const storage = new AgentStorage(dbPath);
329
- AgentStorage.#instances.set(dbPath, storage);
182
+ instances.set(dbPath, storage);
330
183
  return storage;
331
184
  } catch (err) {
332
185
  const isSqliteBusy = err && typeof err === "object" && (err as { code?: string }).code === "SQLITE_BUSY";
@@ -376,40 +229,6 @@ CREATE TABLE settings (
376
229
  });
377
230
  }
378
231
 
379
- /**
380
- * Gets a cached value by key. Returns null if not found or expired.
381
- */
382
- getCache(key: string): string | null {
383
- try {
384
- const row = this.#getCacheStmt.get(key) as { value?: string } | undefined;
385
- return row?.value ?? null;
386
- } catch {
387
- return null;
388
- }
389
- }
390
-
391
- /**
392
- * Sets a cached value with expiry time (unix seconds).
393
- */
394
- setCache(key: string, value: string, expiresAtSec: number): void {
395
- try {
396
- this.#upsertCacheStmt.run(key, value, expiresAtSec);
397
- } catch (error) {
398
- logger.warn("AgentStorage failed to set cache", { key, error: String(error) });
399
- }
400
- }
401
-
402
- /**
403
- * Deletes expired cache entries. Call periodically for cleanup.
404
- */
405
- cleanExpiredCache(): void {
406
- try {
407
- this.#deleteExpiredCacheStmt.run();
408
- } catch {
409
- // Ignore cleanup errors
410
- }
411
- }
412
-
413
232
  /**
414
233
  * Records model usage, updating the last-used timestamp.
415
234
  * @param modelKey - Model key in "provider/modelId" format
@@ -447,8 +266,7 @@ CREATE TABLE settings (
447
266
  * @returns True if at least one credential is stored
448
267
  */
449
268
  hasAuthCredentials(): boolean {
450
- const row = this.#countAuthStmt.get() as { count?: number } | undefined;
451
- return (row?.count ?? 0) > 0;
269
+ return this.#authStore.listAuthCredentials().length > 0;
452
270
  }
453
271
 
454
272
  /**
@@ -459,19 +277,41 @@ CREATE TABLE settings (
459
277
  * @returns Array of stored credentials with their database IDs
460
278
  */
461
279
  listAuthCredentials(provider?: string, includeDisabled = false): StoredAuthCredential[] {
462
- const rows = includeDisabled
463
- ? ((provider
464
- ? (this.#listAuthByProviderStmt.all(provider) as AuthRow[])
465
- : (this.#listAuthStmt.all() as AuthRow[])) ?? [])
466
- : ((provider
467
- ? (this.#listActiveAuthByProviderStmt.all(provider) as AuthRow[])
468
- : (this.#listActiveAuthStmt.all() as AuthRow[])) ?? []);
280
+ // AuthCredentialStore doesn't expose includeDisabled yet, so we filter if needed
281
+ const credentials = this.#authStore.listAuthCredentials(provider);
282
+ if (!includeDisabled) return credentials;
283
+
284
+ // For now, includeDisabled requires direct DB access
285
+ // This is only used internally, so it's acceptable
286
+ const stmt = this.#db.prepare(
287
+ provider
288
+ ? "SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? ORDER BY id ASC"
289
+ : "SELECT id, provider, credential_type, data FROM auth_credentials ORDER BY id ASC",
290
+ );
291
+ const rows = (provider ? stmt.all(provider) : stmt.all()) as Array<{
292
+ id: number;
293
+ provider: string;
294
+ credential_type: string;
295
+ data: string;
296
+ }>;
469
297
 
470
298
  const results: StoredAuthCredential[] = [];
471
299
  for (const row of rows) {
472
- const credential = deserializeCredential(row);
473
- if (!credential) continue;
474
- results.push({ id: row.id, provider: row.provider, credential });
300
+ try {
301
+ const parsed = JSON.parse(row.data);
302
+ if (!parsed || typeof parsed !== "object") continue;
303
+
304
+ let credential: AuthCredential;
305
+ if (row.credential_type === "api_key" && typeof (parsed as { key?: unknown }).key === "string") {
306
+ credential = { type: "api_key", key: (parsed as { key: string }).key };
307
+ } else if (row.credential_type === "oauth") {
308
+ credential = { type: "oauth", ...(parsed as Record<string, unknown>) } as AuthCredential;
309
+ } else {
310
+ continue;
311
+ }
312
+
313
+ results.push({ id: row.id, provider: row.provider, credential });
314
+ } catch {}
475
315
  }
476
316
  return results;
477
317
  }
@@ -484,17 +324,7 @@ CREATE TABLE settings (
484
324
  * @returns Array of newly stored credentials with their database IDs
485
325
  */
486
326
  replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[] {
487
- const replace = this.#db.transaction((providerName: string, items: AuthCredential[]) => {
488
- this.#deleteAuthByProviderStmt.run(providerName);
489
- const inserted: StoredAuthCredential[] = [];
490
- for (const credential of items) {
491
- const record = this.#insertAuthCredential(providerName, credential);
492
- if (record) inserted.push(record);
493
- }
494
- return inserted;
495
- });
496
-
497
- return replace(provider, credentials);
327
+ return this.#authStore.replaceAuthCredentialsForProvider(provider, credentials);
498
328
  }
499
329
 
500
330
  /**
@@ -503,16 +333,7 @@ CREATE TABLE settings (
503
333
  * @param credential - New credential data
504
334
  */
505
335
  updateAuthCredential(id: number, credential: AuthCredential): void {
506
- const serialized = serializeCredential(credential);
507
- if (!serialized) {
508
- logger.warn("AgentStorage updateAuthCredential invalid type", { id, type: credential.type });
509
- return;
510
- }
511
- try {
512
- this.#updateAuthStmt.run(serialized.credentialType, serialized.data, id);
513
- } catch (error) {
514
- logger.warn("AgentStorage updateAuthCredential failed", { id, error: String(error) });
515
- }
336
+ this.#authStore.updateAuthCredential(id, credential);
516
337
  }
517
338
 
518
339
  /**
@@ -520,66 +341,36 @@ CREATE TABLE settings (
520
341
  * @param id - Database row ID of the credential to delete
521
342
  */
522
343
  deleteAuthCredential(id: number): void {
523
- try {
524
- this.#deleteAuthStmt.run(id);
525
- } catch (error) {
526
- logger.warn("AgentStorage deleteAuthCredential failed", { id, error: String(error) });
527
- }
344
+ this.#authStore.deleteAuthCredential(id);
345
+ }
346
+
347
+ /**
348
+ * Deletes all auth credentials for a provider.
349
+ * @param provider - Provider name whose credentials should be deleted
350
+ */
351
+ deleteAuthCredentialsForProvider(provider: string): void {
352
+ this.#authStore.deleteAuthCredentialsForProvider(provider);
528
353
  }
529
354
 
530
355
  /**
531
- * Disables an auth credential by ID (soft-delete).
532
- * Disabled credentials are excluded from normal listing but remain in the database.
533
- * @param id - Database row ID of the credential to disable
356
+ * Gets a cached value by key. Returns null if not found or expired.
534
357
  */
535
- disableAuthCredential(id: number): void {
536
- try {
537
- this.#disableAuthStmt.run(id);
538
- } catch (error) {
539
- logger.warn("AgentStorage disableAuthCredential failed", { id, error: String(error) });
540
- }
358
+ getCache(key: string): string | null {
359
+ return this.#authStore.getCache(key);
541
360
  }
542
361
 
543
362
  /**
544
- * Deletes all auth credentials for a provider.
545
- * @param provider - Provider name whose credentials should be deleted
363
+ * Sets a cached value with expiry time (unix seconds).
546
364
  */
547
- deleteAuthCredentialsForProvider(provider: string): void {
548
- try {
549
- this.#deleteAuthByProviderStmt.run(provider);
550
- } catch (error) {
551
- logger.warn("AgentStorage deleteAuthCredentialsForProvider failed", {
552
- provider,
553
- error: String(error),
554
- });
555
- }
365
+ setCache(key: string, value: string, expiresAtSec: number): void {
366
+ this.#authStore.setCache(key, value, expiresAtSec);
556
367
  }
557
368
 
558
369
  /**
559
- * Inserts a new auth credential for a provider.
560
- * @param provider - Provider name (e.g., "anthropic", "openai")
561
- * @param credential - Credential to insert
562
- * @returns Stored credential with database ID, or null on failure
370
+ * Deletes expired cache entries. Call periodically for cleanup.
563
371
  */
564
- #insertAuthCredential(provider: string, credential: AuthCredential): StoredAuthCredential | null {
565
- const serialized = serializeCredential(credential);
566
- if (!serialized) {
567
- logger.warn("AgentStorage insertAuthCredential invalid type", { provider, type: credential.type });
568
- return null;
569
- }
570
- try {
571
- const row = this.#insertAuthStmt.get(provider, serialized.credentialType, serialized.data) as
572
- | { id?: number }
573
- | undefined;
574
- if (!row?.id) {
575
- logger.warn("AgentStorage insertAuthCredential missing id", { provider });
576
- return null;
577
- }
578
- return { id: row.id, provider, credential };
579
- } catch (error) {
580
- logger.warn("AgentStorage insertAuthCredential failed", { provider, error: String(error) });
581
- return null;
582
- }
372
+ cleanExpiredCache(): void {
373
+ this.#authStore.cleanExpiredCache();
583
374
  }
584
375
 
585
376
  /**