@oh-my-pi/pi-coding-agent 4.0.1 → 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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +111 -184
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/history-storage.ts +174 -0
- package/src/core/index.ts +1 -0
- package/src/core/keybindings.ts +3 -0
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +87 -289
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +9 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/components/custom-editor.ts +7 -0
- package/src/modes/interactive/components/history-search.ts +158 -0
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +370 -3115
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/{task.md → agents/task.md} +1 -1
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +232 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +9 -3
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
- /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
- /package/src/prompts/{implement.md → agents/implement.md} +0 -0
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /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/
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
139
|
-
const credentials = normalizeAnthropicOAuthCredentials(authJson?.anthropic);
|
|
216
|
+
const credentials = await readAnthropicOAuthCredentials(configDir);
|
|
140
217
|
for (const credential of credentials) {
|
|
141
|
-
if (
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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/
|
|
239
|
+
"No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth in ~/.omp/agent/agent.db",
|
|
202
240
|
);
|
|
203
241
|
}
|
|
204
242
|
|
package/src/core/tools/write.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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 =
|
|
204
|
+
const migratedAuthProviders = migrateAuthToAgentDb();
|
|
205
205
|
migrateSessionsFromAgentRoot();
|
|
206
206
|
migrateToolsToBin();
|
|
207
207
|
|
|
@@ -29,6 +29,7 @@ export class CustomEditor extends Editor {
|
|
|
29
29
|
public onCtrlP?: () => void;
|
|
30
30
|
public onShiftCtrlP?: () => void;
|
|
31
31
|
public onCtrlL?: () => void;
|
|
32
|
+
public onCtrlR?: () => void;
|
|
32
33
|
public onCtrlO?: () => void;
|
|
33
34
|
public onCtrlT?: () => void;
|
|
34
35
|
public onCtrlG?: () => void;
|
|
@@ -113,6 +114,12 @@ export class CustomEditor extends Editor {
|
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Intercept Ctrl+R for history search
|
|
118
|
+
if (matchesKey(data, "ctrl+r") && this.onCtrlR) {
|
|
119
|
+
this.onCtrlR();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
// Intercept Ctrl+O for tool output expansion
|
|
117
124
|
if (isCtrlO(data) && this.onCtrlO) {
|
|
118
125
|
this.onCtrlO();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Container,
|
|
4
|
+
Input,
|
|
5
|
+
isArrowDown,
|
|
6
|
+
isArrowUp,
|
|
7
|
+
isEnter,
|
|
8
|
+
isEscape,
|
|
9
|
+
Spacer,
|
|
10
|
+
Text,
|
|
11
|
+
truncateToWidth,
|
|
12
|
+
visibleWidth,
|
|
13
|
+
} from "@oh-my-pi/pi-tui";
|
|
14
|
+
import type { HistoryEntry, HistoryStorage } from "../../../core/history-storage";
|
|
15
|
+
import { theme } from "../theme/theme";
|
|
16
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
17
|
+
|
|
18
|
+
class HistoryResultsList implements Component {
|
|
19
|
+
private results: HistoryEntry[] = [];
|
|
20
|
+
private selectedIndex = 0;
|
|
21
|
+
private maxVisible = 10;
|
|
22
|
+
|
|
23
|
+
setResults(results: HistoryEntry[], selectedIndex: number): void {
|
|
24
|
+
this.results = results;
|
|
25
|
+
this.selectedIndex = selectedIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSelectedIndex(selectedIndex: number): void {
|
|
29
|
+
this.selectedIndex = selectedIndex;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {
|
|
33
|
+
// No cached state to invalidate currently
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(width: number): string[] {
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
|
|
39
|
+
if (this.results.length === 0) {
|
|
40
|
+
lines.push(theme.fg("muted", " No matching history"));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startIndex = Math.max(
|
|
45
|
+
0,
|
|
46
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.results.length - this.maxVisible),
|
|
47
|
+
);
|
|
48
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.results.length);
|
|
49
|
+
|
|
50
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
51
|
+
const entry = this.results[i];
|
|
52
|
+
const isSelected = i === this.selectedIndex;
|
|
53
|
+
|
|
54
|
+
const cursorSymbol = `${theme.nav.cursor} `;
|
|
55
|
+
const cursorWidth = visibleWidth(cursorSymbol);
|
|
56
|
+
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : " ".repeat(cursorWidth);
|
|
57
|
+
const maxWidth = width - cursorWidth;
|
|
58
|
+
|
|
59
|
+
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|
|
60
|
+
const truncated = truncateToWidth(normalized, maxWidth, theme.format.ellipsis);
|
|
61
|
+
lines.push(cursor + (isSelected ? theme.bold(truncated) : truncated));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (startIndex > 0 || endIndex < this.results.length) {
|
|
65
|
+
const scrollText = ` (${this.selectedIndex + 1}/${this.results.length})`;
|
|
66
|
+
lines.push(theme.fg("muted", truncateToWidth(scrollText, width, "")));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class HistorySearchComponent extends Container {
|
|
74
|
+
private historyStorage: HistoryStorage;
|
|
75
|
+
private searchInput: Input;
|
|
76
|
+
private results: HistoryEntry[] = [];
|
|
77
|
+
private selectedIndex = 0;
|
|
78
|
+
private resultsList: HistoryResultsList;
|
|
79
|
+
private onSelect: (prompt: string) => void;
|
|
80
|
+
private onCancel: () => void;
|
|
81
|
+
private resultLimit = 100;
|
|
82
|
+
|
|
83
|
+
constructor(historyStorage: HistoryStorage, onSelect: (prompt: string) => void, onCancel: () => void) {
|
|
84
|
+
super();
|
|
85
|
+
this.historyStorage = historyStorage;
|
|
86
|
+
this.onSelect = onSelect;
|
|
87
|
+
this.onCancel = onCancel;
|
|
88
|
+
|
|
89
|
+
this.searchInput = new Input();
|
|
90
|
+
this.searchInput.onSubmit = () => {
|
|
91
|
+
const selected = this.results[this.selectedIndex];
|
|
92
|
+
if (selected) {
|
|
93
|
+
this.onSelect(selected.prompt);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.searchInput.onEscape = () => {
|
|
97
|
+
this.onCancel();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.resultsList = new HistoryResultsList();
|
|
101
|
+
|
|
102
|
+
this.addChild(new Spacer(1));
|
|
103
|
+
this.addChild(new Text(theme.bold("Search History (Ctrl+R)"), 1, 0));
|
|
104
|
+
this.addChild(new Spacer(1));
|
|
105
|
+
this.addChild(new DynamicBorder());
|
|
106
|
+
this.addChild(new Spacer(1));
|
|
107
|
+
this.addChild(this.searchInput);
|
|
108
|
+
this.addChild(new Spacer(1));
|
|
109
|
+
this.addChild(this.resultsList);
|
|
110
|
+
this.addChild(new Spacer(1));
|
|
111
|
+
this.addChild(new Text(theme.fg("muted", "up/down navigate enter select esc cancel"), 1, 0));
|
|
112
|
+
this.addChild(new Spacer(1));
|
|
113
|
+
this.addChild(new DynamicBorder());
|
|
114
|
+
|
|
115
|
+
this.updateResults();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleInput(keyData: string): void {
|
|
119
|
+
if (isArrowUp(keyData)) {
|
|
120
|
+
if (this.results.length === 0) return;
|
|
121
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
122
|
+
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isArrowDown(keyData)) {
|
|
127
|
+
if (this.results.length === 0) return;
|
|
128
|
+
this.selectedIndex = Math.min(this.results.length - 1, this.selectedIndex + 1);
|
|
129
|
+
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isEnter(keyData)) {
|
|
134
|
+
const selected = this.results[this.selectedIndex];
|
|
135
|
+
if (selected) {
|
|
136
|
+
this.onSelect(selected.prompt);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isEscape(keyData)) {
|
|
142
|
+
this.onCancel();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.searchInput.handleInput(keyData);
|
|
147
|
+
this.updateResults();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private updateResults(): void {
|
|
151
|
+
const query = this.searchInput.getValue().trim();
|
|
152
|
+
this.results = query
|
|
153
|
+
? this.historyStorage.search(query, this.resultLimit)
|
|
154
|
+
: this.historyStorage.getRecent(this.resultLimit);
|
|
155
|
+
this.selectedIndex = 0;
|
|
156
|
+
this.resultsList.setResults(this.results, this.selectedIndex);
|
|
157
|
+
}
|
|
158
|
+
}
|