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

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 (90) hide show
  1. package/CHANGELOG.md +66 -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-session.ts +3 -3
  7. package/src/core/agent-storage.ts +450 -0
  8. package/src/core/auth-storage.ts +102 -183
  9. package/src/core/compaction/branch-summarization.ts +5 -4
  10. package/src/core/compaction/compaction.ts +7 -6
  11. package/src/core/compaction/utils.ts +6 -11
  12. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  13. package/src/core/custom-share.ts +66 -0
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/history-storage.ts +15 -7
  16. package/src/core/prompt-templates.ts +271 -1
  17. package/src/core/sdk.ts +14 -3
  18. package/src/core/settings-manager.ts +100 -34
  19. package/src/core/slash-commands.ts +4 -1
  20. package/src/core/storage-migration.ts +215 -0
  21. package/src/core/system-prompt.ts +130 -290
  22. package/src/core/title-generator.ts +3 -2
  23. package/src/core/tools/ask.ts +2 -2
  24. package/src/core/tools/bash.ts +2 -1
  25. package/src/core/tools/calculator.ts +2 -1
  26. package/src/core/tools/complete.ts +5 -2
  27. package/src/core/tools/edit.ts +2 -1
  28. package/src/core/tools/find.ts +2 -1
  29. package/src/core/tools/gemini-image.ts +2 -1
  30. package/src/core/tools/git.ts +2 -2
  31. package/src/core/tools/grep.ts +2 -1
  32. package/src/core/tools/index.test.ts +0 -28
  33. package/src/core/tools/index.ts +0 -6
  34. package/src/core/tools/lsp/index.ts +2 -1
  35. package/src/core/tools/output.ts +2 -1
  36. package/src/core/tools/read.ts +4 -1
  37. package/src/core/tools/ssh.ts +4 -2
  38. package/src/core/tools/task/agents.ts +56 -30
  39. package/src/core/tools/task/commands.ts +5 -8
  40. package/src/core/tools/task/index.ts +7 -15
  41. package/src/core/tools/web-fetch.ts +2 -1
  42. package/src/core/tools/web-search/auth.ts +106 -16
  43. package/src/core/tools/web-search/index.ts +3 -2
  44. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  45. package/src/core/tools/write.ts +2 -1
  46. package/src/core/voice.ts +3 -1
  47. package/src/discovery/builtin.ts +9 -54
  48. package/src/discovery/claude.ts +16 -69
  49. package/src/discovery/codex.ts +11 -36
  50. package/src/discovery/helpers.ts +52 -1
  51. package/src/main.ts +1 -1
  52. package/src/migrations.ts +20 -20
  53. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  54. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  55. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  56. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  57. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  58. package/src/modes/interactive/interactive-mode.ts +363 -3139
  59. package/src/modes/interactive/theme/theme.ts +5 -5
  60. package/src/modes/interactive/types.ts +189 -0
  61. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  62. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  63. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  64. package/src/prompts/agents/frontmatter.md +7 -0
  65. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  66. package/src/prompts/agents/planner.md +112 -0
  67. package/src/prompts/agents/task.md +15 -0
  68. package/src/prompts/review-request.md +44 -8
  69. package/src/prompts/system/custom-system-prompt.md +80 -0
  70. package/src/prompts/system/file-operations.md +12 -0
  71. package/src/prompts/system/system-prompt.md +237 -0
  72. package/src/prompts/system/title-system.md +2 -0
  73. package/src/prompts/tools/bash.md +1 -1
  74. package/src/prompts/tools/read.md +1 -1
  75. package/src/prompts/tools/task.md +34 -22
  76. package/src/core/tools/rulebook.ts +0 -132
  77. package/src/prompts/architect-plan.md +0 -10
  78. package/src/prompts/implement-with-critic.md +0 -11
  79. package/src/prompts/implement.md +0 -11
  80. package/src/prompts/system-prompt.md +0 -43
  81. package/src/prompts/task.md +0 -14
  82. package/src/prompts/title-system.md +0 -8
  83. /package/src/prompts/{init.md → agents/init.md} +0 -0
  84. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  85. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  86. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  87. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  88. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  89. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  90. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -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);
@@ -5,7 +5,7 @@
5
5
  * .pi is an alias for backwards compatibility.
6
6
  */
7
7
 
8
- import { basename, dirname, isAbsolute, join, resolve } from "path";
8
+ import { dirname, isAbsolute, join, resolve } from "path";
9
9
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
10
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
11
11
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -16,7 +16,7 @@ import { type MCPServer, mcpCapability } from "../capability/mcp";
16
16
  import { type Prompt, promptCapability } from "../capability/prompt";
17
17
  import { type Rule, ruleCapability } from "../capability/rule";
18
18
  import { type Settings, settingsCapability } from "../capability/settings";
19
- import { type Skill, type SkillFrontmatter, skillCapability } from "../capability/skill";
19
+ import { type Skill, skillCapability } from "../capability/skill";
20
20
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
21
21
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
22
22
  import { type CustomTool, toolCapability } from "../capability/tool";
@@ -27,6 +27,7 @@ import {
27
27
  expandEnvVarsDeep,
28
28
  getExtensionNameFromPath,
29
29
  loadFilesFromDir,
30
+ loadSkillsFromDir,
30
31
  parseFrontmatter,
31
32
  parseJSON,
32
33
  SOURCE_PATHS,
@@ -190,64 +191,18 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
190
191
  });
191
192
 
192
193
  // Skills
193
- function loadSkillFromFile(ctx: LoadContext, path: string, level: "user" | "project"): Skill | null {
194
- const content = ctx.fs.readFile(path);
195
- if (!content) return null;
196
-
197
- const { frontmatter, body } = parseFrontmatter(content);
198
- const skillDir = dirname(path);
199
- const parentDirName = basename(skillDir);
200
- const name = (frontmatter.name as string) || parentDirName;
201
-
202
- if (!frontmatter.description) return null;
203
-
204
- return {
205
- name,
206
- path,
207
- content: body,
208
- frontmatter: frontmatter as SkillFrontmatter,
209
- level,
210
- _source: createSourceMeta(PROVIDER_ID, path, level),
211
- };
212
- }
213
-
214
- function loadSkillsRecursive(ctx: LoadContext, dir: string, level: "user" | "project"): LoadResult<Skill> {
215
- const items: Skill[] = [];
216
- const warnings: string[] = [];
217
-
218
- if (!ctx.fs.isDir(dir)) return { items, warnings };
219
-
220
- for (const name of ctx.fs.readDir(dir)) {
221
- if (name.startsWith(".") || name === "node_modules") continue;
222
-
223
- const path = join(dir, name);
224
-
225
- if (ctx.fs.isDir(path)) {
226
- const skillFile = join(path, "SKILL.md");
227
- if (ctx.fs.isFile(skillFile)) {
228
- const skill = loadSkillFromFile(ctx, skillFile, level);
229
- if (skill) items.push(skill);
230
- }
231
-
232
- const sub = loadSkillsRecursive(ctx, path, level);
233
- items.push(...sub.items);
234
- if (sub.warnings) warnings.push(...sub.warnings);
235
- } else if (name === "SKILL.md") {
236
- const skill = loadSkillFromFile(ctx, path, level);
237
- if (skill) items.push(skill);
238
- }
239
- }
240
-
241
- return { items, warnings };
242
- }
243
-
244
194
  function loadSkills(ctx: LoadContext): LoadResult<Skill> {
245
195
  const items: Skill[] = [];
246
196
  const warnings: string[] = [];
247
197
 
248
198
  for (const { dir, level } of getConfigDirs(ctx)) {
249
199
  const skillsDir = join(dir, "skills");
250
- const result = loadSkillsRecursive(ctx, skillsDir, level);
200
+ const result = loadSkillsFromDir(ctx, {
201
+ dir: skillsDir,
202
+ providerId: PROVIDER_ID,
203
+ level,
204
+ requireDescription: true,
205
+ });
251
206
  items.push(...result.items);
252
207
  if (result.warnings) warnings.push(...result.warnings);
253
208
  }
@@ -24,7 +24,7 @@ import {
24
24
  expandEnvVarsDeep,
25
25
  getExtensionNameFromPath,
26
26
  loadFilesFromDir,
27
- parseFrontmatter,
27
+ loadSkillsFromDir,
28
28
  parseJSON,
29
29
  } from "./helpers";
30
30
 
@@ -218,78 +218,25 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
218
218
  const items: Skill[] = [];
219
219
  const warnings: string[] = [];
220
220
 
221
- // User-level: ~/.claude/skills/*/SKILL.md
222
- const userBase = getUserClaude(ctx);
223
- const userSkillsDir = join(userBase, "skills");
224
-
225
- if (ctx.fs.isDir(userSkillsDir)) {
226
- const skillDirs = ctx.fs.readDir(userSkillsDir);
227
-
228
- for (const dirName of skillDirs) {
229
- if (dirName.startsWith(".")) continue;
230
-
231
- const skillDir = join(userSkillsDir, dirName);
232
- if (!ctx.fs.isDir(skillDir)) continue;
233
-
234
- const skillFile = join(skillDir, "SKILL.md");
235
- if (!ctx.fs.isFile(skillFile)) continue;
236
-
237
- const content = ctx.fs.readFile(skillFile);
238
- if (!content) {
239
- warnings.push(`Failed to read ${skillFile}`);
240
- continue;
241
- }
242
-
243
- const { frontmatter, body } = parseFrontmatter(content);
244
- const name = (frontmatter.name as string) || dirName;
245
-
246
- items.push({
247
- name,
248
- path: skillFile,
249
- content: body,
250
- frontmatter,
251
- level: "user",
252
- _source: createSourceMeta(PROVIDER_ID, skillFile, "user"),
253
- });
254
- }
255
- }
221
+ const userSkillsDir = join(getUserClaude(ctx), "skills");
222
+ const userResult = loadSkillsFromDir(ctx, {
223
+ dir: userSkillsDir,
224
+ providerId: PROVIDER_ID,
225
+ level: "user",
226
+ });
227
+ items.push(...userResult.items);
228
+ if (userResult.warnings) warnings.push(...userResult.warnings);
256
229
 
257
- // Project-level: <project>/.claude/skills/*/SKILL.md
258
230
  const projectBase = getProjectClaude(ctx);
259
231
  if (projectBase) {
260
232
  const projectSkillsDir = join(projectBase, "skills");
261
-
262
- if (ctx.fs.isDir(projectSkillsDir)) {
263
- const skillDirs = ctx.fs.readDir(projectSkillsDir);
264
-
265
- for (const dirName of skillDirs) {
266
- if (dirName.startsWith(".")) continue;
267
-
268
- const skillDir = join(projectSkillsDir, dirName);
269
- if (!ctx.fs.isDir(skillDir)) continue;
270
-
271
- const skillFile = join(skillDir, "SKILL.md");
272
- if (!ctx.fs.isFile(skillFile)) continue;
273
-
274
- const content = ctx.fs.readFile(skillFile);
275
- if (!content) {
276
- warnings.push(`Failed to read ${skillFile}`);
277
- continue;
278
- }
279
-
280
- const { frontmatter, body } = parseFrontmatter(content);
281
- const name = (frontmatter.name as string) || dirName;
282
-
283
- items.push({
284
- name,
285
- path: skillFile,
286
- content: body,
287
- frontmatter,
288
- level: "project",
289
- _source: createSourceMeta(PROVIDER_ID, skillFile, "project"),
290
- });
291
- }
292
- }
233
+ const projectResult = loadSkillsFromDir(ctx, {
234
+ dir: projectSkillsDir,
235
+ providerId: PROVIDER_ID,
236
+ level: "project",
237
+ });
238
+ items.push(...projectResult.items);
239
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
293
240
  }
294
241
 
295
242
  return { items, warnings };
@@ -33,6 +33,7 @@ import {
33
33
  discoverExtensionModulePaths,
34
34
  getExtensionNameFromPath,
35
35
  loadFilesFromDir,
36
+ loadSkillsFromDir,
36
37
  parseFrontmatter,
37
38
  SOURCE_PATHS,
38
39
  } from "./helpers";
@@ -209,51 +210,25 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
209
210
  const items: Skill[] = [];
210
211
  const warnings: string[] = [];
211
212
 
212
- // User level: ~/.codex/skills/
213
213
  const userSkillsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
214
- const userResult = loadFilesFromDir(ctx, userSkillsDir, PROVIDER_ID, "user", {
215
- extensions: ["md"],
216
- recursive: true,
217
- transform: (name, content, path, source) => {
218
- const { frontmatter, body } = parseFrontmatter(content);
219
- const skillName = frontmatter.name || name.replace(/\.md$/, "");
220
-
221
- return {
222
- name: String(skillName),
223
- path,
224
- content: body,
225
- frontmatter,
226
- level: "user" as const,
227
- _source: source,
228
- };
229
- },
214
+ const userResult = loadSkillsFromDir(ctx, {
215
+ dir: userSkillsDir,
216
+ providerId: PROVIDER_ID,
217
+ level: "user",
230
218
  });
231
219
  items.push(...userResult.items);
232
- warnings.push(...(userResult.warnings || []));
220
+ if (userResult.warnings) warnings.push(...userResult.warnings);
233
221
 
234
- // Project level: .codex/skills/
235
222
  const codexDir = ctx.fs.walkUp(".codex", { dir: true });
236
223
  if (codexDir) {
237
224
  const projectSkillsDir = join(codexDir, "skills");
238
- const projectResult = loadFilesFromDir(ctx, projectSkillsDir, PROVIDER_ID, "project", {
239
- extensions: ["md"],
240
- recursive: true,
241
- transform: (name, content, path, source) => {
242
- const { frontmatter, body } = parseFrontmatter(content);
243
- const skillName = frontmatter.name || name.replace(/\.md$/, "");
244
-
245
- return {
246
- name: String(skillName),
247
- path,
248
- content: body,
249
- frontmatter,
250
- level: "project" as const,
251
- _source: source,
252
- };
253
- },
225
+ const projectResult = loadSkillsFromDir(ctx, {
226
+ dir: projectSkillsDir,
227
+ providerId: PROVIDER_ID,
228
+ level: "project",
254
229
  });
255
230
  items.push(...projectResult.items);
256
- warnings.push(...(projectResult.warnings || []));
231
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
257
232
  }
258
233
 
259
234
  return { items, warnings };
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { join, resolve } from "path";
6
6
  import { parse as parseYAML } from "yaml";
7
+ import type { Skill, SkillFrontmatter } from "../capability/skill";
7
8
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
8
9
 
9
10
  /**
@@ -126,6 +127,56 @@ export function parseFrontmatter(content: string): {
126
127
  }
127
128
  }
128
129
 
130
+ export function loadSkillsFromDir(
131
+ ctx: LoadContext,
132
+ options: {
133
+ dir: string;
134
+ providerId: string;
135
+ level: "user" | "project";
136
+ requireDescription?: boolean;
137
+ },
138
+ ): LoadResult<Skill> {
139
+ const items: Skill[] = [];
140
+ const warnings: string[] = [];
141
+ const { dir, level, providerId, requireDescription = false } = options;
142
+
143
+ if (!ctx.fs.isDir(dir)) {
144
+ return { items, warnings };
145
+ }
146
+
147
+ for (const name of ctx.fs.readDir(dir)) {
148
+ if (name.startsWith(".") || name === "node_modules") continue;
149
+
150
+ const skillDir = join(dir, name);
151
+ if (!ctx.fs.isDir(skillDir)) continue;
152
+
153
+ const skillFile = join(skillDir, "SKILL.md");
154
+ if (!ctx.fs.isFile(skillFile)) continue;
155
+
156
+ const content = ctx.fs.readFile(skillFile);
157
+ if (!content) {
158
+ warnings.push(`Failed to read ${skillFile}`);
159
+ continue;
160
+ }
161
+
162
+ const { frontmatter, body } = parseFrontmatter(content);
163
+ if (requireDescription && !frontmatter.description) {
164
+ continue;
165
+ }
166
+
167
+ items.push({
168
+ name: (frontmatter.name as string) || name,
169
+ path: skillFile,
170
+ content: body,
171
+ frontmatter: frontmatter as SkillFrontmatter,
172
+ level,
173
+ _source: createSourceMeta(providerId, skillFile, level),
174
+ });
175
+ }
176
+
177
+ return { items, warnings };
178
+ }
179
+
129
180
  /**
130
181
  * Expand environment variables in a string.
131
182
  * Supports ${VAR} and ${VAR:-default} syntax.
@@ -286,7 +337,7 @@ export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): str
286
337
  const discovered: string[] = [];
287
338
 
288
339
  for (const name of ctx.fs.readDir(dir)) {
289
- if (name.startsWith(".")) continue;
340
+ if (name.startsWith(".") || name === "node_modules") continue;
290
341
 
291
342
  const entryPath = join(dir, name);
292
343
 
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