@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.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 +87 -0
- package/README.md +44 -3
- package/docs/extensions.md +29 -4
- package/docs/sdk.md +3 -3
- package/package.json +5 -5
- package/src/cli/args.ts +8 -0
- package/src/config.ts +5 -15
- package/src/core/agent-session.ts +193 -47
- package/src/core/auth-storage.ts +16 -3
- package/src/core/bash-executor.ts +79 -14
- package/src/core/custom-commands/types.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/export-html/index.ts +33 -1
- package/src/core/export-html/template.css +99 -0
- package/src/core/export-html/template.generated.ts +1 -1
- package/src/core/export-html/template.js +133 -8
- package/src/core/extensions/index.ts +22 -4
- package/src/core/extensions/loader.ts +152 -214
- package/src/core/extensions/runner.ts +139 -79
- package/src/core/extensions/types.ts +143 -19
- package/src/core/extensions/wrapper.ts +5 -8
- package/src/core/hooks/types.ts +1 -1
- package/src/core/index.ts +2 -1
- package/src/core/keybindings.ts +4 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +35 -26
- package/src/core/sdk.ts +96 -76
- package/src/core/settings-manager.ts +45 -14
- package/src/core/system-prompt.ts +5 -15
- package/src/core/tools/bash.ts +115 -54
- package/src/core/tools/find.ts +86 -7
- package/src/core/tools/grep.ts +27 -6
- package/src/core/tools/index.ts +15 -6
- package/src/core/tools/ls.ts +49 -18
- package/src/core/tools/render-utils.ts +2 -1
- package/src/core/tools/task/worker.ts +35 -12
- package/src/core/tools/web-search/auth.ts +37 -32
- package/src/core/tools/web-search/providers/anthropic.ts +35 -22
- package/src/index.ts +101 -9
- package/src/main.ts +60 -20
- package/src/migrations.ts +47 -2
- package/src/modes/index.ts +2 -2
- package/src/modes/interactive/components/assistant-message.ts +25 -7
- package/src/modes/interactive/components/bash-execution.ts +5 -0
- package/src/modes/interactive/components/branch-summary-message.ts +5 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
- package/src/modes/interactive/components/countdown-timer.ts +38 -0
- package/src/modes/interactive/components/custom-editor.ts +8 -0
- package/src/modes/interactive/components/custom-message.ts +5 -0
- package/src/modes/interactive/components/footer.ts +2 -5
- package/src/modes/interactive/components/hook-input.ts +29 -20
- package/src/modes/interactive/components/hook-selector.ts +52 -38
- package/src/modes/interactive/components/index.ts +39 -0
- package/src/modes/interactive/components/login-dialog.ts +160 -0
- package/src/modes/interactive/components/model-selector.ts +10 -2
- package/src/modes/interactive/components/session-selector.ts +5 -1
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line/segments.ts +3 -3
- package/src/modes/interactive/components/tool-execution.ts +9 -16
- package/src/modes/interactive/components/tree-selector.ts +1 -6
- package/src/modes/interactive/interactive-mode.ts +466 -215
- package/src/modes/interactive/theme/theme.ts +50 -2
- package/src/modes/print-mode.ts +78 -31
- package/src/modes/rpc/rpc-mode.ts +186 -78
- package/src/modes/rpc/rpc-types.ts +10 -3
- package/src/prompts/system-prompt.md +36 -28
- package/src/utils/clipboard.ts +90 -50
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/tools-manager.ts +2 -2
package/src/core/tools/find.ts
CHANGED
|
@@ -44,6 +44,22 @@ export interface FindToolDetails {
|
|
|
44
44
|
error?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Pluggable operations for the find tool.
|
|
49
|
+
* Override these to delegate file search to remote systems (e.g., SSH).
|
|
50
|
+
*/
|
|
51
|
+
export interface FindOperations {
|
|
52
|
+
/** Check if path exists */
|
|
53
|
+
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
|
54
|
+
/** Find files matching glob pattern. Returns relative paths. */
|
|
55
|
+
glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FindToolOptions {
|
|
59
|
+
/** Custom operations for find. Default: local filesystem + fd */
|
|
60
|
+
operations?: FindOperations;
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
async function captureCommandOutput(
|
|
48
64
|
command: string,
|
|
49
65
|
args: string[],
|
|
@@ -91,7 +107,9 @@ async function captureCommandOutput(
|
|
|
91
107
|
return { stdout, stderr, exitCode, aborted: scope.aborted };
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
export function createFindTool(session: ToolSession): AgentTool<typeof findSchema> {
|
|
110
|
+
export function createFindTool(session: ToolSession, options?: FindToolOptions): AgentTool<typeof findSchema> {
|
|
111
|
+
const customOps = options?.operations;
|
|
112
|
+
|
|
95
113
|
return {
|
|
96
114
|
name: "find",
|
|
97
115
|
label: "Find",
|
|
@@ -117,12 +135,6 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
117
135
|
signal?: AbortSignal,
|
|
118
136
|
) => {
|
|
119
137
|
return untilAborted(signal, async () => {
|
|
120
|
-
// Ensure fd is available
|
|
121
|
-
const fdPath = await ensureTool("fd", true);
|
|
122
|
-
if (!fdPath) {
|
|
123
|
-
throw new Error("fd is not available and could not be downloaded");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
138
|
const searchPath = resolveToCwd(searchDir || ".", session.cwd);
|
|
127
139
|
const scopePath = (() => {
|
|
128
140
|
const relative = path.relative(session.cwd, searchPath).replace(/\\/g, "/");
|
|
@@ -133,6 +145,73 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
133
145
|
const includeHidden = hidden ?? false;
|
|
134
146
|
const shouldSortByMtime = sortByMtime ?? false;
|
|
135
147
|
|
|
148
|
+
// If custom operations provided with glob, use that instead of fd
|
|
149
|
+
if (customOps?.glob) {
|
|
150
|
+
if (!(await customOps.exists(searchPath))) {
|
|
151
|
+
throw new Error(`Path not found: ${searchPath}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const results = await customOps.glob(pattern, searchPath, {
|
|
155
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
156
|
+
limit: effectiveLimit,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (results.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
162
|
+
details: { scopePath, fileCount: 0, files: [], truncated: false },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Relativize paths
|
|
167
|
+
const relativized = results.map((p) => {
|
|
168
|
+
if (p.startsWith(searchPath)) {
|
|
169
|
+
return p.slice(searchPath.length + 1);
|
|
170
|
+
}
|
|
171
|
+
return path.relative(searchPath, p);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
175
|
+
const rawOutput = relativized.join("\n");
|
|
176
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
177
|
+
|
|
178
|
+
let resultOutput = truncation.content;
|
|
179
|
+
const details: FindToolDetails = {
|
|
180
|
+
scopePath,
|
|
181
|
+
fileCount: relativized.length,
|
|
182
|
+
files: relativized,
|
|
183
|
+
truncated: resultLimitReached || truncation.truncated,
|
|
184
|
+
};
|
|
185
|
+
const notices: string[] = [];
|
|
186
|
+
|
|
187
|
+
if (resultLimitReached) {
|
|
188
|
+
notices.push(
|
|
189
|
+
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
190
|
+
);
|
|
191
|
+
details.resultLimitReached = effectiveLimit;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (truncation.truncated) {
|
|
195
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
196
|
+
details.truncation = truncation;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (notices.length > 0) {
|
|
200
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: resultOutput }],
|
|
205
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Default: use fd
|
|
210
|
+
const fdPath = await ensureTool("fd", true);
|
|
211
|
+
if (!fdPath) {
|
|
212
|
+
throw new Error("fd is not available and could not be downloaded");
|
|
213
|
+
}
|
|
214
|
+
|
|
136
215
|
// Build fd arguments
|
|
137
216
|
// When pattern contains path separators (e.g. "reports/**"), use --full-path
|
|
138
217
|
// so fd matches against the full path, not just the filename.
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -70,7 +70,29 @@ export interface GrepToolDetails {
|
|
|
70
70
|
error?: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Pluggable operations for the grep tool.
|
|
75
|
+
* Override these to delegate search to remote systems (e.g., SSH).
|
|
76
|
+
*/
|
|
77
|
+
export interface GrepOperations {
|
|
78
|
+
/** Check if path is a directory. Throws if path doesn't exist. */
|
|
79
|
+
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
|
80
|
+
/** Read file contents for context lines */
|
|
81
|
+
readFile: (absolutePath: string) => Promise<string> | string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defaultGrepOperations: GrepOperations = {
|
|
85
|
+
isDirectory: async (p) => (await Bun.file(p).stat()).isDirectory(),
|
|
86
|
+
readFile: (p) => Bun.file(p).text(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export interface GrepToolOptions {
|
|
90
|
+
/** Custom operations for grep. Default: local filesystem + ripgrep */
|
|
91
|
+
operations?: GrepOperations;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createGrepTool(session: ToolSession, options?: GrepToolOptions): AgentTool<typeof grepSchema> {
|
|
95
|
+
const ops = options?.operations ?? defaultGrepOperations;
|
|
74
96
|
return {
|
|
75
97
|
name: "grep",
|
|
76
98
|
label: "Grep",
|
|
@@ -120,14 +142,13 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
120
142
|
const relative = nodePath.relative(session.cwd, searchPath).replace(/\\/g, "/");
|
|
121
143
|
return relative.length === 0 ? "." : relative;
|
|
122
144
|
})();
|
|
123
|
-
|
|
145
|
+
|
|
146
|
+
let isDirectory: boolean;
|
|
124
147
|
try {
|
|
125
|
-
|
|
148
|
+
isDirectory = await ops.isDirectory(searchPath);
|
|
126
149
|
} catch {
|
|
127
150
|
throw new Error(`Path not found: ${searchPath}`);
|
|
128
151
|
}
|
|
129
|
-
|
|
130
|
-
const isDirectory = searchStat.isDirectory();
|
|
131
152
|
const contextValue = context && context > 0 ? context : 0;
|
|
132
153
|
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
133
154
|
const effectiveOutputMode = outputMode ?? "content";
|
|
@@ -150,7 +171,7 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
150
171
|
if (!linesPromise) {
|
|
151
172
|
linesPromise = (async () => {
|
|
152
173
|
try {
|
|
153
|
-
const content = await
|
|
174
|
+
const content = await ops.readFile(filePath);
|
|
154
175
|
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
155
176
|
} catch {
|
|
156
177
|
return [];
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
export { type AskToolDetails, askTool, createAskTool } from "./ask";
|
|
2
|
-
export { type BashToolDetails, createBashTool } from "./bash";
|
|
2
|
+
export { type BashOperations, type BashToolDetails, createBashTool } from "./bash";
|
|
3
3
|
export { type CalculatorToolDetails, createCalculatorTool } from "./calculator";
|
|
4
4
|
export { createCompleteTool } from "./complete";
|
|
5
|
-
export { createEditTool } from "./edit";
|
|
5
|
+
export { createEditTool, type EditToolDetails } from "./edit";
|
|
6
6
|
// Exa MCP tools (22 tools)
|
|
7
7
|
export { exaTools } from "./exa/index";
|
|
8
8
|
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
|
|
9
|
-
export { createFindTool, type FindToolDetails } from "./find";
|
|
9
|
+
export { createFindTool, type FindOperations, type FindToolDetails, type FindToolOptions } from "./find";
|
|
10
10
|
export { setPreferredImageProvider } from "./gemini-image";
|
|
11
11
|
export { createGitTool, type GitToolDetails, gitTool } from "./git";
|
|
12
|
-
export { createGrepTool, type GrepToolDetails } from "./grep";
|
|
13
|
-
export { createLsTool, type LsToolDetails } from "./ls";
|
|
12
|
+
export { createGrepTool, type GrepOperations, type GrepToolDetails, type GrepToolOptions } from "./grep";
|
|
13
|
+
export { createLsTool, type LsOperations, type LsToolDetails, type LsToolOptions } from "./ls";
|
|
14
14
|
export {
|
|
15
15
|
createLspTool,
|
|
16
16
|
type FileDiagnosticsResult,
|
|
@@ -29,7 +29,16 @@ export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
|
29
29
|
export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
|
|
30
30
|
export { createSshTool, type SSHToolDetails } from "./ssh";
|
|
31
31
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
32
|
-
export
|
|
32
|
+
export {
|
|
33
|
+
DEFAULT_MAX_BYTES,
|
|
34
|
+
DEFAULT_MAX_LINES,
|
|
35
|
+
formatSize,
|
|
36
|
+
type TruncationOptions,
|
|
37
|
+
type TruncationResult,
|
|
38
|
+
truncateHead,
|
|
39
|
+
truncateLine,
|
|
40
|
+
truncateTail,
|
|
41
|
+
} from "./truncate";
|
|
33
42
|
export { createWebFetchTool, type WebFetchToolDetails } from "./web-fetch";
|
|
34
43
|
export {
|
|
35
44
|
companyWebSearchTools,
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -28,6 +28,22 @@ const lsSchema = Type.Object({
|
|
|
28
28
|
|
|
29
29
|
const DEFAULT_LIMIT = 500;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Pluggable operations for the ls tool.
|
|
33
|
+
* Override these to delegate directory listing to remote systems (e.g., SSH).
|
|
34
|
+
*/
|
|
35
|
+
export interface LsOperations {
|
|
36
|
+
/** Check if path exists and return stats. Returns undefined if not found. */
|
|
37
|
+
stat: (absolutePath: string) => Promise<{ isDirectory: () => boolean; mtimeMs: number } | undefined>;
|
|
38
|
+
/** Read directory entries (names only) */
|
|
39
|
+
readdir: (absolutePath: string) => Promise<string[]>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LsToolOptions {
|
|
43
|
+
/** Custom operations for directory listing. Default: local filesystem via Bun */
|
|
44
|
+
operations?: LsOperations;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
export interface LsToolDetails {
|
|
32
48
|
entries?: string[];
|
|
33
49
|
dirCount?: number;
|
|
@@ -37,7 +53,24 @@ export interface LsToolDetails {
|
|
|
37
53
|
entryLimitReached?: number;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
/** Default operations using Bun APIs */
|
|
57
|
+
const defaultLsOperations: LsOperations = {
|
|
58
|
+
async stat(absolutePath: string) {
|
|
59
|
+
try {
|
|
60
|
+
const s = await Bun.file(absolutePath).stat();
|
|
61
|
+
return { isDirectory: () => s.isDirectory(), mtimeMs: s.mtimeMs };
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
async readdir(absolutePath: string) {
|
|
67
|
+
return Array.fromAsync(new Bun.Glob("*").scan({ cwd: absolutePath, dot: true, onlyFiles: false }));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function createLsTool(session: ToolSession, options?: LsToolOptions): AgentTool<typeof lsSchema> {
|
|
72
|
+
const ops = options?.operations ?? defaultLsOperations;
|
|
73
|
+
|
|
41
74
|
return {
|
|
42
75
|
name: "ls",
|
|
43
76
|
label: "Ls",
|
|
@@ -53,10 +86,8 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
53
86
|
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
54
87
|
|
|
55
88
|
// Check if path exists and is a directory
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
dirStat = await Bun.file(dirPath).stat();
|
|
59
|
-
} catch {
|
|
89
|
+
const dirStat = await ops.stat(dirPath);
|
|
90
|
+
if (!dirStat) {
|
|
60
91
|
throw new Error(`Path not found: ${dirPath}`);
|
|
61
92
|
}
|
|
62
93
|
|
|
@@ -67,7 +98,7 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
67
98
|
// Read directory entries
|
|
68
99
|
let entries: string[];
|
|
69
100
|
try {
|
|
70
|
-
entries = await
|
|
101
|
+
entries = await ops.readdir(dirPath);
|
|
71
102
|
} catch (error) {
|
|
72
103
|
const message = error instanceof Error ? error.message : String(error);
|
|
73
104
|
throw new Error(`Cannot read directory: ${message}`);
|
|
@@ -93,22 +124,22 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
93
124
|
let suffix = "";
|
|
94
125
|
let age = "";
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (entryStat.isDirectory()) {
|
|
99
|
-
suffix = "/";
|
|
100
|
-
dirCount += 1;
|
|
101
|
-
} else {
|
|
102
|
-
fileCount += 1;
|
|
103
|
-
}
|
|
104
|
-
// Calculate age from mtime
|
|
105
|
-
const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
|
|
106
|
-
age = formatAge(ageSeconds);
|
|
107
|
-
} catch {
|
|
127
|
+
const entryStat = await ops.stat(fullPath);
|
|
128
|
+
if (!entryStat) {
|
|
108
129
|
// Skip entries we can't stat
|
|
109
130
|
continue;
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
if (entryStat.isDirectory()) {
|
|
134
|
+
suffix = "/";
|
|
135
|
+
dirCount += 1;
|
|
136
|
+
} else {
|
|
137
|
+
fileCount += 1;
|
|
138
|
+
}
|
|
139
|
+
// Calculate age from mtime
|
|
140
|
+
const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
|
|
141
|
+
age = formatAge(ageSeconds);
|
|
142
|
+
|
|
112
143
|
// Format: "name/ (2d ago)" or "name (just now)"
|
|
113
144
|
const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
|
|
114
145
|
results.push(line);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* tool renderers to ensure a unified TUI experience.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
@@ -490,7 +491,7 @@ export function truncateDiffByHunk(
|
|
|
490
491
|
// =============================================================================
|
|
491
492
|
|
|
492
493
|
export function shortenPath(filePath: string, homeDir?: string): string {
|
|
493
|
-
const home = homeDir ??
|
|
494
|
+
const home = homeDir ?? homedir();
|
|
494
495
|
if (home && filePath.startsWith(home)) {
|
|
495
496
|
return `~${filePath.slice(home.length)}`;
|
|
496
497
|
}
|
|
@@ -197,20 +197,43 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
197
197
|
// Note: Does not support --extension CLI flag or extension CLI flags
|
|
198
198
|
const extensionRunner = session.extensionRunner;
|
|
199
199
|
if (extensionRunner) {
|
|
200
|
-
extensionRunner.initialize(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
extensionRunner.initialize(
|
|
201
|
+
// ExtensionActions
|
|
202
|
+
{
|
|
203
|
+
sendMessage: (message, options) => {
|
|
204
|
+
session.sendCustomMessage(message, options).catch((e) => {
|
|
205
|
+
console.error(`Extension sendMessage failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
sendUserMessage: (content, options) => {
|
|
209
|
+
session.sendUserMessage(content, options).catch((e) => {
|
|
210
|
+
console.error(`Extension sendUserMessage failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
appendEntry: (customType, data) => {
|
|
214
|
+
session.sessionManager.appendCustomEntry(customType, data);
|
|
215
|
+
},
|
|
216
|
+
getActiveTools: () => session.getActiveToolNames(),
|
|
217
|
+
getAllTools: () => session.getAllToolNames(),
|
|
218
|
+
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
219
|
+
setModel: async (model) => {
|
|
220
|
+
const key = await session.modelRegistry.getApiKey(model);
|
|
221
|
+
if (!key) return false;
|
|
222
|
+
await session.setModel(model);
|
|
223
|
+
return true;
|
|
224
|
+
},
|
|
225
|
+
getThinkingLevel: () => session.thinkingLevel,
|
|
226
|
+
setThinkingLevel: (level) => session.setThinkingLevel(level),
|
|
206
227
|
},
|
|
207
|
-
|
|
208
|
-
|
|
228
|
+
// ExtensionContextActions
|
|
229
|
+
{
|
|
230
|
+
getModel: () => session.model,
|
|
231
|
+
isIdle: () => !session.isStreaming,
|
|
232
|
+
abort: () => session.abort(),
|
|
233
|
+
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
234
|
+
shutdown: () => {},
|
|
209
235
|
},
|
|
210
|
-
|
|
211
|
-
getAllToolsHandler: () => session.getAllToolNames(),
|
|
212
|
-
setActiveToolsHandler: (toolNamesList: string[]) => session.setActiveToolsByName(toolNamesList),
|
|
213
|
-
});
|
|
236
|
+
);
|
|
214
237
|
extensionRunner.onError((err) => {
|
|
215
238
|
console.error(`Extension error (${err.extensionPath}): ${err.error}`);
|
|
216
239
|
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { buildBetaHeader, claudeCodeHeaders, claudeCodeVersion } from "@oh-my-pi/pi-ai";
|
|
13
14
|
import { getConfigDirPaths } from "../../../config";
|
|
14
15
|
import type { AnthropicAuthConfig, AnthropicOAuthCredential, AuthJson, ModelsJson } from "./types";
|
|
15
16
|
|
|
@@ -162,46 +163,50 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
|
|
|
162
163
|
return null;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
betas.push("oauth-2025-04-20", "claude-code-20250219", "prompt-caching-2024-07-31");
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
"anthropic-version": "2023-06-01",
|
|
175
|
-
authorization: `Bearer ${auth.apiKey}`,
|
|
176
|
-
accept: "application/json",
|
|
177
|
-
"content-type": "application/json",
|
|
178
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
179
|
-
"anthropic-beta": betas.join(","),
|
|
180
|
-
"user-agent": "claude-code/2.0.20",
|
|
181
|
-
"x-app": "cli",
|
|
182
|
-
// Stainless SDK telemetry headers (required for OAuth)
|
|
183
|
-
"x-stainless-arch": process.arch,
|
|
184
|
-
"x-stainless-lang": "js",
|
|
185
|
-
"x-stainless-os": process.platform,
|
|
186
|
-
"x-stainless-package-version": "1.0.0",
|
|
187
|
-
"x-stainless-retry-count": "0",
|
|
188
|
-
"x-stainless-runtime": "bun",
|
|
189
|
-
"x-stainless-runtime-version": Bun.version,
|
|
190
|
-
};
|
|
166
|
+
function isAnthropicBaseUrl(baseUrl: string): boolean {
|
|
167
|
+
try {
|
|
168
|
+
const url = new URL(baseUrl);
|
|
169
|
+
return url.protocol === "https:" && url.hostname === "api.anthropic.com";
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
191
172
|
}
|
|
173
|
+
}
|
|
192
174
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
175
|
+
/** Build headers for Anthropic API request */
|
|
176
|
+
export function buildAnthropicHeaders(auth: AnthropicAuthConfig): Record<string, string> {
|
|
177
|
+
const baseBetas = auth.isOAuth
|
|
178
|
+
? [
|
|
179
|
+
"claude-code-20250219",
|
|
180
|
+
"oauth-2025-04-20",
|
|
181
|
+
"interleaved-thinking-2025-05-14",
|
|
182
|
+
"fine-grained-tool-streaming-2025-05-14",
|
|
183
|
+
]
|
|
184
|
+
: ["fine-grained-tool-streaming-2025-05-14"];
|
|
185
|
+
const betaHeader = buildBetaHeader(baseBetas, ["web-search-2025-03-05"]);
|
|
186
|
+
|
|
187
|
+
const headers: Record<string, string> = {
|
|
197
188
|
accept: "application/json",
|
|
198
189
|
"content-type": "application/json",
|
|
199
|
-
"anthropic-
|
|
190
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
191
|
+
"anthropic-beta": betaHeader,
|
|
192
|
+
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
193
|
+
"x-app": "cli",
|
|
194
|
+
"accept-encoding": "gzip, deflate, br, zstd",
|
|
195
|
+
connection: "keep-alive",
|
|
196
|
+
...claudeCodeHeaders,
|
|
200
197
|
};
|
|
198
|
+
|
|
199
|
+
if (auth.isOAuth || !isAnthropicBaseUrl(auth.baseUrl)) {
|
|
200
|
+
headers.authorization = `Bearer ${auth.apiKey}`;
|
|
201
|
+
} else {
|
|
202
|
+
headers["x-api-key"] = auth.apiKey;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return headers;
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/** Build API URL (OAuth requires ?beta=true) */
|
|
204
209
|
export function buildAnthropicUrl(auth: AnthropicAuthConfig): string {
|
|
205
210
|
const base = `${auth.baseUrl}/v1/messages`;
|
|
206
|
-
return
|
|
211
|
+
return `${base}?beta=true`;
|
|
207
212
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Returns synthesized answers with citations and source metadata.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { applyClaudeToolPrefix, buildAnthropicSystemBlocks, stripClaudeToolPrefix } from "@oh-my-pi/pi-ai";
|
|
8
9
|
import { buildAnthropicHeaders, buildAnthropicUrl, findAnthropicAuth, getEnv } from "../auth";
|
|
9
10
|
import type {
|
|
10
11
|
AnthropicApiResponse,
|
|
@@ -18,6 +19,12 @@ import { WebSearchProviderError } from "../types";
|
|
|
18
19
|
|
|
19
20
|
const DEFAULT_MODEL = "claude-haiku-4-5";
|
|
20
21
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
22
|
+
const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
23
|
+
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
24
|
+
|
|
25
|
+
const applySearchToolPrefix = (name: string, isOAuth: boolean): string => {
|
|
26
|
+
return isOAuth ? applyClaudeToolPrefix(name) : name;
|
|
27
|
+
};
|
|
21
28
|
|
|
22
29
|
export interface AnthropicSearchParams {
|
|
23
30
|
query: string;
|
|
@@ -31,6 +38,21 @@ async function getModel(): Promise<string> {
|
|
|
31
38
|
return (await getEnv("ANTHROPIC_SEARCH_MODEL")) ?? DEFAULT_MODEL;
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
function buildSystemBlocks(
|
|
42
|
+
auth: AnthropicAuthConfig,
|
|
43
|
+
model: string,
|
|
44
|
+
systemPrompt?: string,
|
|
45
|
+
): ReturnType<typeof buildAnthropicSystemBlocks> {
|
|
46
|
+
const includeClaudeCode = !model.startsWith("claude-3-5-haiku");
|
|
47
|
+
const extraInstructions = auth.isOAuth ? ["You are a helpful AI assistant with web search capabilities."] : [];
|
|
48
|
+
|
|
49
|
+
return buildAnthropicSystemBlocks(systemPrompt, {
|
|
50
|
+
includeClaudeCodeInstruction: includeClaudeCode,
|
|
51
|
+
includeCacheControl: auth.isOAuth,
|
|
52
|
+
extraInstructions,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
/** Call Anthropic API with web search */
|
|
35
57
|
async function callWebSearch(
|
|
36
58
|
auth: AnthropicAuthConfig,
|
|
@@ -42,34 +64,21 @@ async function callWebSearch(
|
|
|
42
64
|
const url = buildAnthropicUrl(auth);
|
|
43
65
|
const headers = buildAnthropicHeaders(auth);
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = [];
|
|
47
|
-
|
|
48
|
-
if (auth.isOAuth) {
|
|
49
|
-
// OAuth requires Claude Code identity with cache_control
|
|
50
|
-
systemBlocks.push({
|
|
51
|
-
type: "text",
|
|
52
|
-
text: "You are a helpful AI assistant with web search capabilities.",
|
|
53
|
-
cache_control: { type: "ephemeral" },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (systemPrompt) {
|
|
58
|
-
systemBlocks.push({
|
|
59
|
-
type: "text",
|
|
60
|
-
text: systemPrompt,
|
|
61
|
-
...(auth.isOAuth ? { cache_control: { type: "ephemeral" } } : {}),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
67
|
+
const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
|
|
64
68
|
|
|
65
69
|
const body: Record<string, unknown> = {
|
|
66
70
|
model,
|
|
67
71
|
max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
68
72
|
messages: [{ role: "user", content: query }],
|
|
69
|
-
tools: [
|
|
73
|
+
tools: [
|
|
74
|
+
{
|
|
75
|
+
type: WEB_SEARCH_TOOL_TYPE,
|
|
76
|
+
name: applySearchToolPrefix(WEB_SEARCH_TOOL_NAME, auth.isOAuth),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
70
79
|
};
|
|
71
80
|
|
|
72
|
-
if (systemBlocks.length > 0) {
|
|
81
|
+
if (systemBlocks && systemBlocks.length > 0) {
|
|
73
82
|
body.system = systemBlocks;
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -131,7 +140,11 @@ function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
|
|
|
131
140
|
const citations: WebSearchCitation[] = [];
|
|
132
141
|
|
|
133
142
|
for (const block of response.content) {
|
|
134
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
block.type === "server_tool_use" &&
|
|
145
|
+
block.name &&
|
|
146
|
+
stripClaudeToolPrefix(block.name) === WEB_SEARCH_TOOL_NAME
|
|
147
|
+
) {
|
|
135
148
|
// Intermediate search query
|
|
136
149
|
if (block.input?.query) {
|
|
137
150
|
searchQueries.push(block.input.query);
|