@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
@@ -2,6 +2,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
3
3
  import { type Static, Type } from "@sinclair/typebox";
4
4
  import gitDescription from "../../prompts/tools/git.md" with { type: "text" };
5
+ import { renderPromptTemplate } from "../prompt-templates";
5
6
  import type { ToolSession } from "./index";
6
7
 
7
8
  const gitSchema = Type.Object({
@@ -200,7 +201,7 @@ export function createGitTool(session: ToolSession): AgentTool<typeof gitSchema,
200
201
  return {
201
202
  name: "git",
202
203
  label: "Git",
203
- description: gitDescription,
204
+ description: renderPromptTemplate(gitDescription),
204
205
  parameters: gitSchema,
205
206
  execute: async (_toolCallId, params: Static<typeof gitSchema>, _signal?: AbortSignal) => {
206
207
  if (params.operation === "commit" && !params.message) {
@@ -224,7 +225,6 @@ export function createGitTool(session: ToolSession): AgentTool<typeof gitSchema,
224
225
  export const gitTool = createGitTool({
225
226
  cwd: process.cwd(),
226
227
  hasUI: false,
227
- rulebookRules: [],
228
228
  getSessionFile: () => null,
229
229
  getSessionSpawns: () => null,
230
230
  })!;
@@ -8,6 +8,7 @@ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/t
8
8
  import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
9
9
  import { ensureTool } from "../../utils/tools-manager";
10
10
  import type { RenderResultOptions } from "../custom-tools/types";
11
+ import { renderPromptTemplate } from "../prompt-templates";
11
12
  import { ScopeSignal, untilAborted } from "../utils";
12
13
  import type { ToolSession } from "./index";
13
14
  import { resolveToCwd } from "./path-utils";
@@ -96,7 +97,7 @@ export function createGrepTool(session: ToolSession, options?: GrepToolOptions):
96
97
  return {
97
98
  name: "grep",
98
99
  label: "Grep",
99
- description: grepDescription,
100
+ description: renderPromptTemplate(grepDescription),
100
101
  parameters: grepSchema,
101
102
  execute: async (
102
103
  _toolCallId: string,
@@ -5,7 +5,6 @@ function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
5
5
  return {
6
6
  cwd: "/tmp/test",
7
7
  hasUI: false,
8
- rulebookRules: [],
9
8
  getSessionFile: () => null,
10
9
  getSessionSpawns: () => "*",
11
10
  ...overrides,
@@ -75,32 +74,6 @@ describe("createTools", () => {
75
74
  expect(names).toContain("ask");
76
75
  });
77
76
 
78
- it("excludes rulebook tool when no rules provided", async () => {
79
- const session = createTestSession({ rulebookRules: [] });
80
- const tools = await createTools(session);
81
- const names = tools.map((t) => t.name);
82
-
83
- expect(names).not.toContain("rulebook");
84
- });
85
-
86
- it("includes rulebook tool when rules provided", async () => {
87
- const session = createTestSession({
88
- rulebookRules: [
89
- {
90
- path: "/test/rule.md",
91
- name: "Test Rule",
92
- content: "Test content",
93
- description: "A test rule",
94
- _source: { provider: "test", providerName: "Test", path: "/test", level: "project" },
95
- },
96
- ],
97
- });
98
- const tools = await createTools(session);
99
- const names = tools.map((t) => t.name);
100
-
101
- expect(names).toContain("rulebook");
102
- });
103
-
104
77
  it("excludes git tool when disabled in settings", async () => {
105
78
  const session = createTestSession({
106
79
  settings: {
@@ -174,7 +147,6 @@ describe("createTools", () => {
174
147
  "notebook",
175
148
  "output",
176
149
  "read",
177
- "rulebook",
178
150
  "task",
179
151
  "web_fetch",
180
152
  "web_search",
@@ -26,7 +26,6 @@ export { createNotebookTool, type NotebookToolDetails } from "./notebook";
26
26
  export { createOutputTool, type OutputToolDetails } from "./output";
27
27
  export { createReadTool, type ReadToolDetails } from "./read";
28
28
  export { reportFindingTool, type SubmitReviewDetails } from "./review";
29
- export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
30
29
  export { createSshTool, type SSHToolDetails } from "./ssh";
31
30
  export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
32
31
  export {
@@ -62,7 +61,6 @@ export {
62
61
  export { createWriteTool, type WriteToolDetails } from "./write";
63
62
 
64
63
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
65
- import type { Rule } from "../../capability/rule";
66
64
  import type { EventBus } from "../event-bus";
67
65
  import type { BashInterceptorRule } from "../settings-manager";
68
66
  import { createAskTool } from "./ask";
@@ -79,7 +77,6 @@ import { createNotebookTool } from "./notebook";
79
77
  import { createOutputTool } from "./output";
80
78
  import { createReadTool } from "./read";
81
79
  import { reportFindingTool } from "./review";
82
- import { createRulebookTool } from "./rulebook";
83
80
  import { createSshTool } from "./ssh";
84
81
  import { createTaskTool } from "./task/index";
85
82
  import { createWebFetchTool } from "./web-fetch";
@@ -95,8 +92,6 @@ export interface ToolSession {
95
92
  cwd: string;
96
93
  /** Whether UI is available */
97
94
  hasUI: boolean;
98
- /** Rulebook rules */
99
- rulebookRules: Rule[];
100
95
  /** Event bus for tool/extension communication */
101
96
  eventBus?: EventBus;
102
97
  /** Output schema for structured completion (subagents) */
@@ -141,7 +136,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
141
136
  notebook: createNotebookTool,
142
137
  output: createOutputTool,
143
138
  read: createReadTool,
144
- rulebook: createRulebookTool,
145
139
  task: createTaskTool,
146
140
  web_fetch: createWebFetchTool,
147
141
  web_search: createWebSearchTool,
@@ -6,6 +6,7 @@ import type { BunFile } from "bun";
6
6
  import { type Theme, theme } from "../../../modes/interactive/theme/theme";
7
7
  import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
8
8
  import { logger } from "../../logger";
9
+ import { renderPromptTemplate } from "../../prompt-templates";
9
10
  import { once, untilAborted } from "../../utils";
10
11
  import type { ToolSession } from "../index";
11
12
  import { resolveToCwd } from "../path-utils";
@@ -737,7 +738,7 @@ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema,
737
738
  return {
738
739
  name: "lsp",
739
740
  label: "LSP",
740
- description: lspDescription,
741
+ description: renderPromptTemplate(lspDescription),
741
742
  parameters: lspSchema,
742
743
  renderCall,
743
744
  renderResult,
@@ -14,6 +14,7 @@ import { Type } from "@sinclair/typebox";
14
14
  import type { Theme } from "../../modes/interactive/theme/theme";
15
15
  import outputDescription from "../../prompts/tools/output.md" with { type: "text" };
16
16
  import type { RenderResultOptions } from "../custom-tools/types";
17
+ import { renderPromptTemplate } from "../prompt-templates";
17
18
  import type { ToolSession } from "./index";
18
19
  import {
19
20
  formatCount,
@@ -236,7 +237,7 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
236
237
  return {
237
238
  name: "output",
238
239
  label: "Output",
239
- description: outputDescription,
240
+ description: renderPromptTemplate(outputDescription),
240
241
  parameters: outputSchema,
241
242
  execute: async (
242
243
  _toolCallId: string,
@@ -12,6 +12,7 @@ import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
12
12
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
13
13
  import { ensureTool } from "../../utils/tools-manager";
14
14
  import type { RenderResultOptions } from "../custom-tools/types";
15
+ import { renderPromptTemplate } from "../prompt-templates";
15
16
  import type { ToolSession } from "../sdk";
16
17
  import { ScopeSignal, untilAborted } from "../utils";
17
18
  import { createLsTool } from "./ls";
@@ -429,7 +430,9 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
429
430
  return {
430
431
  name: "read",
431
432
  label: "Read",
432
- description: readDescription.replace("{{DEFAULT_MAX_LINES}}", String(DEFAULT_MAX_LINES)),
433
+ description: renderPromptTemplate(readDescription, {
434
+ DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
435
+ }),
433
436
  parameters: readSchema,
434
437
  execute: async (
435
438
  toolCallId: string,
@@ -8,6 +8,7 @@ import { loadSync } from "../../discovery/index";
8
8
  import type { Theme } from "../../modes/interactive/theme/theme";
9
9
  import sshDescriptionBase from "../../prompts/tools/ssh.md" with { type: "text" };
10
10
  import type { RenderResultOptions } from "../custom-tools/types";
11
+ import { renderPromptTemplate } from "../prompt-templates";
11
12
  import type { SSHHostInfo } from "../ssh/connection-manager";
12
13
  import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
13
14
  import { executeSSH } from "../ssh/ssh-executor";
@@ -54,11 +55,12 @@ function formatHostEntry(host: SSHHost): string {
54
55
  }
55
56
 
56
57
  function formatDescription(hosts: SSHHost[]): string {
58
+ const baseDescription = renderPromptTemplate(sshDescriptionBase);
57
59
  if (hosts.length === 0) {
58
- return sshDescriptionBase;
60
+ return baseDescription;
59
61
  }
60
62
  const hostList = hosts.map(formatHostEntry).join("\n");
61
- return `${sshDescriptionBase}\n\nAvailable hosts:\n${hostList}`;
63
+ return `${baseDescription}\n\nAvailable hosts:\n${hostList}`;
62
64
  }
63
65
 
64
66
  function quoteRemotePath(value: string): string {
@@ -4,47 +4,73 @@
4
4
  * Agents are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
6
 
7
+ import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" };
7
8
  // Embed agent markdown files at build time
8
- import exploreMd from "../../../prompts/explore.md" with { type: "text" };
9
- import planMd from "../../../prompts/plan.md" with { type: "text" };
10
- import reviewerMd from "../../../prompts/reviewer.md" with { type: "text" };
11
- import taskMd from "../../../prompts/task.md" with { type: "text" };
9
+ import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" };
10
+ import planMd from "../../../prompts/agents/plan.md" with { type: "text" };
11
+ import reviewerMd from "../../../prompts/agents/reviewer.md" with { type: "text" };
12
+ import taskMd from "../../../prompts/agents/task.md" with { type: "text" };
13
+ import { renderPromptTemplate } from "../../prompt-templates";
12
14
  import type { AgentDefinition, AgentSource } from "./types";
13
15
 
14
- const EMBEDDED_AGENTS: { name: string; content: string }[] = [
15
- { name: "explore.md", content: exploreMd },
16
- { name: "plan.md", content: planMd },
17
- { name: "reviewer.md", content: reviewerMd },
16
+ interface AgentFrontmatter {
17
+ name: string;
18
+ description: string;
19
+ spawns?: string;
20
+ model?: string;
21
+ }
22
+
23
+ interface EmbeddedAgentDef {
24
+ fileName: string;
25
+ frontmatter?: AgentFrontmatter;
26
+ template: string;
27
+ }
28
+
29
+ function buildAgentContent(def: EmbeddedAgentDef): string {
30
+ const body = renderPromptTemplate(def.template);
31
+ if (!def.frontmatter) return body;
32
+ return renderPromptTemplate(agentFrontmatterTemplate, { ...def.frontmatter, body });
33
+ }
34
+
35
+ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
36
+ { fileName: "explore.md", template: exploreMd },
37
+ { fileName: "plan.md", template: planMd },
38
+ { fileName: "reviewer.md", template: reviewerMd },
18
39
  {
19
- name: "task.md",
20
- content: `---
21
- name: task
22
- description: General-purpose subagent with full capabilities for delegated multi-step tasks
23
- spawns: explore
24
- model: default
25
- ---
26
- ${taskMd}`,
40
+ fileName: "task.md",
41
+ frontmatter: {
42
+ name: "task",
43
+ description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
44
+ spawns: "explore",
45
+ model: "default",
46
+ },
47
+ template: taskMd,
27
48
  },
28
49
  {
29
- name: "quick_task.md",
30
- content: `---
31
- name: quick_task
32
- description: Quick task for fast execution
33
- model: pi/smol
34
- ---
35
- ${taskMd}`,
50
+ fileName: "quick_task.md",
51
+ frontmatter: {
52
+ name: "quick_task",
53
+ description: "Quick task for fast execution",
54
+ model: "pi/smol",
55
+ },
56
+ template: taskMd,
36
57
  },
37
58
  {
38
- name: "deep_task.md",
39
- content: `---
40
- name: deep_task
41
- description: Deep task for comprehensive reasoning
42
- model: pi/slow
43
- ---
44
- ${taskMd}`,
59
+ fileName: "deep_task.md",
60
+ frontmatter: {
61
+ name: "deep_task",
62
+ description: "Deep task for comprehensive reasoning",
63
+ model: "pi/slow",
64
+ },
65
+ template: taskMd,
45
66
  },
46
67
  ];
47
68
 
69
+ const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS.map((def) => ({
70
+ name: def.fileName,
71
+ content: buildAgentContent(def),
72
+ }));
73
+
48
74
  /**
49
75
  * Parse YAML frontmatter from markdown content.
50
76
  */
@@ -9,16 +9,13 @@ import { type SlashCommand, slashCommandCapability } from "../../../capability/s
9
9
  import { loadSync } from "../../../discovery";
10
10
 
11
11
  // Embed command markdown files at build time
12
- import architectPlanMd from "../../../prompts/architect-plan.md" with { type: "text" };
13
- import implementMd from "../../../prompts/implement.md" with { type: "text" };
14
- import implementWithCriticMd from "../../../prompts/implement-with-critic.md" with { type: "text" };
15
- import initMd from "../../../prompts/init.md" with { type: "text" };
12
+ import initMd from "../../../prompts/agents/init.md" with { type: "text" };
13
+ import plannerMd from "../../../prompts/agents/planner.md" with { type: "text" };
14
+ import { renderPromptTemplate } from "../../prompt-templates";
16
15
 
17
16
  const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
18
- { name: "architect-plan.md", content: architectPlanMd },
19
- { name: "implement-with-critic.md", content: implementWithCriticMd },
20
- { name: "implement.md", content: implementMd },
21
- { name: "init.md", content: initMd },
17
+ { name: "init.md", content: renderPromptTemplate(initMd) },
18
+ { name: "planner.md", content: renderPromptTemplate(plannerMd) },
22
19
  ];
23
20
 
24
21
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
@@ -17,6 +17,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
17
  import type { Usage } from "@oh-my-pi/pi-ai";
18
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
19
19
  import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
20
+ import { renderPromptTemplate } from "../../prompt-templates";
20
21
  import { formatDuration } from "../render-utils";
21
22
  import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
22
23
  import { discoverAgents, getAgent } from "./discovery";
@@ -95,21 +96,12 @@ export { taskSchema } from "./types";
95
96
  async function buildDescription(cwd: string): Promise<string> {
96
97
  const { agents } = await discoverAgents(cwd);
97
98
 
98
- // Build agents list
99
- const agentLines: string[] = [];
100
- for (const agent of agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
101
- const tools = agent.tools?.join(", ") || "All tools";
102
- agentLines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
103
- }
104
- if (agents.length > MAX_AGENTS_IN_DESCRIPTION) {
105
- agentLines.push(` ...and ${agents.length - MAX_AGENTS_IN_DESCRIPTION} more agents`);
106
- }
107
-
108
- // Fill template placeholders
109
- return taskDescriptionTemplate
110
- .replace("{{AGENTS_LIST}}", agentLines.join("\n"))
111
- .replace("{{MAX_PARALLEL_TASKS}}", String(MAX_PARALLEL_TASKS))
112
- .replace("{{MAX_CONCURRENCY}}", String(MAX_CONCURRENCY));
99
+ return renderPromptTemplate(taskDescriptionTemplate, {
100
+ agents: agents.slice(0, MAX_AGENTS_IN_DESCRIPTION),
101
+ moreAgents: agents.length > MAX_AGENTS_IN_DESCRIPTION ? agents.length - MAX_AGENTS_IN_DESCRIPTION : 0,
102
+ MAX_PARALLEL_TASKS,
103
+ MAX_CONCURRENCY,
104
+ });
113
105
  }
114
106
 
115
107
  /**
@@ -10,6 +10,7 @@ import { type Theme, theme } from "../../modes/interactive/theme/theme";
10
10
  import webFetchDescription from "../../prompts/tools/web-fetch.md" with { type: "text" };
11
11
  import { ensureTool } from "../../utils/tools-manager";
12
12
  import type { RenderResultOptions } from "../custom-tools/types";
13
+ import { renderPromptTemplate } from "../prompt-templates";
13
14
  import type { ToolSession } from "./index";
14
15
  import { specialHandlers } from "./web-scrapers/index";
15
16
  import type { RenderResult } from "./web-scrapers/types";
@@ -836,7 +837,7 @@ export function createWebFetchTool(_session: ToolSession): AgentTool<typeof webF
836
837
  return {
837
838
  name: "web_fetch",
838
839
  label: "Web Fetch",
839
- description: webFetchDescription,
840
+ description: renderPromptTemplate(webFetchDescription),
840
841
  parameters: webFetchSchema,
841
842
  execute: async (
842
843
  _toolCallId: string,
@@ -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(