@oh-my-pi/pi-coding-agent 10.5.0 → 10.6.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 +27 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +22 -2
- package/src/cursor.ts +1 -1
- package/src/prompts/tools/grep.md +4 -8
- package/src/tools/grep.ts +111 -107
- package/src/web/search/index.ts +15 -4
- package/src/web/search/providers/jina.ts +76 -0
- package/src/web/search/render.ts +3 -1
- package/src/web/search/types.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.6.0] - 2026-02-04
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Removed `output_mode` parameter from grep tool—results now always use content mode with formatted match output
|
|
9
|
+
- Renamed grep context parameters from `context_pre`/`context_post` to `pre`/`post`
|
|
10
|
+
- Removed `n` (show line numbers) parameter—line numbers are now always displayed in grep results
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added Jina as a web search provider option alongside Exa, Perplexity, and Anthropic
|
|
15
|
+
- Added support for Jina Reader API integration with automatic provider detection when JINA_API_KEY is configured
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Reformatted grep output to display matches grouped by file with numbered match headers and aligned context lines
|
|
20
|
+
- Updated grep output to use `>>` prefix for match lines and aligned spacing for context lines for improved readability
|
|
21
|
+
- Changed multiline matching to automatically enable when pattern contains literal newlines (`
|
|
22
|
+
`)
|
|
23
|
+
- Split grep context parameter into separate `context_pre` and `context_post` options for independent control of lines before and after matches
|
|
24
|
+
- Updated grep tool to use configurable default context settings from `grep.contextBefore` and `grep.contextAfter` configuration
|
|
25
|
+
- Added configurable grep context defaults and reduced the default to 1 line before, 3 lines after
|
|
26
|
+
- Enabled the browser tool by default
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- Removed `filesWithMatches` and `count` output modes from grep tool
|
|
31
|
+
|
|
5
32
|
## [10.5.0] - 2026-02-04
|
|
6
33
|
|
|
7
34
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.6.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@mozilla/readability": "0.6.0",
|
|
83
|
-
"@oh-my-pi/omp-stats": "10.
|
|
84
|
-
"@oh-my-pi/pi-agent-core": "10.
|
|
85
|
-
"@oh-my-pi/pi-ai": "10.
|
|
86
|
-
"@oh-my-pi/pi-natives": "10.
|
|
87
|
-
"@oh-my-pi/pi-tui": "10.
|
|
88
|
-
"@oh-my-pi/pi-utils": "10.
|
|
83
|
+
"@oh-my-pi/omp-stats": "10.6.0",
|
|
84
|
+
"@oh-my-pi/pi-agent-core": "10.6.0",
|
|
85
|
+
"@oh-my-pi/pi-ai": "10.6.0",
|
|
86
|
+
"@oh-my-pi/pi-natives": "10.6.0",
|
|
87
|
+
"@oh-my-pi/pi-tui": "10.6.0",
|
|
88
|
+
"@oh-my-pi/pi-utils": "10.6.0",
|
|
89
89
|
"@openai/agents": "^0.4.5",
|
|
90
90
|
"@sinclair/typebox": "^0.34.48",
|
|
91
91
|
"ajv": "^8.17.1",
|
|
@@ -329,6 +329,26 @@ export const SETTINGS_SCHEMA = {
|
|
|
329
329
|
default: true,
|
|
330
330
|
ui: { tab: "tools", label: "Enable Grep", description: "Enable the grep tool for content searching" },
|
|
331
331
|
},
|
|
332
|
+
"grep.contextBefore": {
|
|
333
|
+
type: "number",
|
|
334
|
+
default: 1,
|
|
335
|
+
ui: {
|
|
336
|
+
tab: "tools",
|
|
337
|
+
label: "Grep context before",
|
|
338
|
+
description: "Lines of context before each grep match",
|
|
339
|
+
submenu: true,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
"grep.contextAfter": {
|
|
343
|
+
type: "number",
|
|
344
|
+
default: 3,
|
|
345
|
+
ui: {
|
|
346
|
+
tab: "tools",
|
|
347
|
+
label: "Grep context after",
|
|
348
|
+
description: "Lines of context after each grep match",
|
|
349
|
+
submenu: true,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
332
352
|
"notebook.enabled": {
|
|
333
353
|
type: "boolean",
|
|
334
354
|
default: true,
|
|
@@ -360,7 +380,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
360
380
|
},
|
|
361
381
|
"browser.enabled": {
|
|
362
382
|
type: "boolean",
|
|
363
|
-
default:
|
|
383
|
+
default: true,
|
|
364
384
|
ui: {
|
|
365
385
|
tab: "tools",
|
|
366
386
|
label: "Enable Browser",
|
|
@@ -485,7 +505,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
485
505
|
// ─────────────────────────────────────────────────────────────────────────
|
|
486
506
|
"providers.webSearch": {
|
|
487
507
|
type: "enum",
|
|
488
|
-
values: ["auto", "exa", "perplexity", "anthropic"] as const,
|
|
508
|
+
values: ["auto", "exa", "jina", "perplexity", "anthropic"] as const,
|
|
489
509
|
default: "auto",
|
|
490
510
|
ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
|
|
491
511
|
},
|
package/src/cursor.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
167
167
|
pattern: args.pattern,
|
|
168
168
|
path: args.path || undefined,
|
|
169
169
|
glob: args.glob || undefined,
|
|
170
|
-
|
|
170
|
+
mode: args.outputMode || undefined,
|
|
171
171
|
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
|
172
172
|
ignore_case: args.caseInsensitive || undefined,
|
|
173
173
|
type: args.type || undefined,
|
|
@@ -6,17 +6,13 @@ Powerful search tool built on ripgrep.
|
|
|
6
6
|
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`)
|
|
7
7
|
- Filter files with `glob` (e.g., `*.js`, `**/*.tsx`) or `type` (e.g., `js`, `py`, `rust`)
|
|
8
8
|
- Pattern syntax uses ripgrep—literal braces need escaping (`interface\\{\\}` to find `interface{}` in Go)
|
|
9
|
-
- For cross-line patterns like `struct \\{[\\s\\S]*?field`,
|
|
9
|
+
- For cross-line patterns like `struct \\{[\\s\\S]*?field`, set `multiline: true` if needed
|
|
10
|
+
- If the pattern contains a literal `\n`, multiline defaults to true
|
|
10
11
|
</instruction>
|
|
11
12
|
|
|
12
13
|
<output>
|
|
13
|
-
Results
|
|
14
|
-
|
|
15
|
-
- `files_with_matches`: File paths only (one per line)
|
|
16
|
-
- `count`: Match counts per file
|
|
17
|
-
|
|
18
|
-
In `content` mode, truncated at 100 matches default (configurable via `limit`).
|
|
19
|
-
For `files_with_matches` and `count` modes, use `limit` truncate results.
|
|
14
|
+
Results are always content mode: matching lines with file paths and line numbers.
|
|
15
|
+
Truncated at 100 matches by default (configurable via `limit`).
|
|
20
16
|
</output>
|
|
21
17
|
|
|
22
18
|
<critical>
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodePath from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
|
|
4
|
+
import { grep as wasmGrep } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -10,7 +10,7 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
|
13
|
-
import {
|
|
13
|
+
import { renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import type { ToolSession } from ".";
|
|
15
15
|
import type { OutputMeta } from "./output-meta";
|
|
16
16
|
import { resolveToCwd } from "./path-utils";
|
|
@@ -24,16 +24,11 @@ const grepSchema = Type.Object({
|
|
|
24
24
|
path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
|
|
25
25
|
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
|
26
26
|
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
|
27
|
-
output_mode: Type.Optional(
|
|
28
|
-
StringEnum(["filesWithMatches", "content", "count"], {
|
|
29
|
-
description: "Output format (default: files_with_matches)",
|
|
30
|
-
}),
|
|
31
|
-
),
|
|
32
27
|
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching
|
|
36
|
-
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches (default: 100
|
|
28
|
+
pre: Type.Optional(Type.Number({ description: "Lines of context before matches" })),
|
|
29
|
+
post: Type.Optional(Type.Number({ description: "Lines of context after matches" })),
|
|
30
|
+
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching" })),
|
|
31
|
+
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches (default: 100)" })),
|
|
37
32
|
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit (default: 0)" })),
|
|
38
33
|
});
|
|
39
34
|
|
|
@@ -50,7 +45,6 @@ export interface GrepToolDetails {
|
|
|
50
45
|
fileCount?: number;
|
|
51
46
|
files?: string[];
|
|
52
47
|
fileMatches?: Array<{ path: string; count: number }>;
|
|
53
|
-
mode?: "content" | "filesWithMatches" | "count";
|
|
54
48
|
truncated?: boolean;
|
|
55
49
|
error?: string;
|
|
56
50
|
}
|
|
@@ -86,7 +80,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
86
80
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
87
81
|
_toolContext?: AgentToolContext,
|
|
88
82
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
89
|
-
const { pattern, path: searchDir, glob, type,
|
|
83
|
+
const { pattern, path: searchDir, glob, type, i, pre, post, multiline, limit, offset } = params;
|
|
90
84
|
|
|
91
85
|
return untilAborted(signal, async () => {
|
|
92
86
|
const normalizedPattern = pattern.trim();
|
|
@@ -105,10 +99,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
105
99
|
}
|
|
106
100
|
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
|
107
101
|
|
|
108
|
-
const
|
|
109
|
-
const
|
|
102
|
+
const defaultContextBefore = this.session.settings.get("grep.contextBefore");
|
|
103
|
+
const defaultContextAfter = this.session.settings.get("grep.contextAfter");
|
|
104
|
+
const normalizedContextBefore = pre ?? defaultContextBefore;
|
|
105
|
+
const normalizedContextAfter = post ?? defaultContextAfter;
|
|
110
106
|
const ignoreCase = i ?? false;
|
|
111
|
-
const
|
|
107
|
+
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
108
|
+
const effectiveMultiline = multiline ?? patternHasNewline;
|
|
112
109
|
|
|
113
110
|
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
|
114
111
|
const scopePath = (() => {
|
|
@@ -124,9 +121,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
124
121
|
throw new ToolError(`Path not found: ${searchPath}`);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
const effectiveOutputMode =
|
|
128
|
-
const effectiveLimit =
|
|
129
|
-
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
|
124
|
+
const effectiveOutputMode = "content";
|
|
125
|
+
const effectiveLimit = normalizedLimit ?? DEFAULT_MATCH_LIMIT;
|
|
130
126
|
|
|
131
127
|
// Run WASM grep
|
|
132
128
|
let result: Awaited<ReturnType<typeof wasmGrep>>;
|
|
@@ -137,11 +133,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
137
133
|
glob: glob?.trim() || undefined,
|
|
138
134
|
type: type?.trim() || undefined,
|
|
139
135
|
ignoreCase,
|
|
140
|
-
multiline:
|
|
136
|
+
multiline: effectiveMultiline,
|
|
141
137
|
hidden: true,
|
|
142
138
|
maxCount: effectiveLimit,
|
|
143
139
|
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
|
144
|
-
|
|
140
|
+
contextBefore: normalizedContextBefore,
|
|
141
|
+
contextAfter: normalizedContextAfter,
|
|
145
142
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
146
143
|
mode: effectiveOutputMode,
|
|
147
144
|
});
|
|
@@ -180,71 +177,66 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
180
177
|
matchCount: 0,
|
|
181
178
|
fileCount: 0,
|
|
182
179
|
files: [],
|
|
183
|
-
mode: effectiveOutputMode,
|
|
184
180
|
truncated: false,
|
|
185
181
|
};
|
|
186
182
|
return toolResult(details).text("No matches found").done();
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
|
|
185
|
+
const outputLines: string[] = [];
|
|
190
186
|
let linesTruncated = false;
|
|
187
|
+
let matchIndex = 0;
|
|
191
188
|
|
|
192
189
|
for (const match of result.matches) {
|
|
193
190
|
recordFile(match.path);
|
|
194
191
|
const relativePath = formatPath(match.path);
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
matchIndex += 1;
|
|
194
|
+
if (matchIndex > 1) {
|
|
195
|
+
outputLines.push("");
|
|
196
|
+
}
|
|
197
|
+
outputLines.push(`${matchIndex}. ${relativePath}:${match.lineNumber}`);
|
|
198
|
+
|
|
199
|
+
const lineNumbers: number[] = [match.lineNumber];
|
|
200
|
+
if (match.contextBefore) {
|
|
201
|
+
for (const ctx of match.contextBefore) {
|
|
202
|
+
lineNumbers.push(ctx.lineNumber);
|
|
206
203
|
}
|
|
204
|
+
}
|
|
205
|
+
if (match.contextAfter) {
|
|
206
|
+
for (const ctx of match.contextAfter) {
|
|
207
|
+
lineNumbers.push(ctx.lineNumber);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
: `${relativePath}: ${match.line}`,
|
|
213
|
-
);
|
|
212
|
+
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
|
|
213
|
+
const padded = lineNumber.toString().padStart(lineWidth, " ");
|
|
214
|
+
return isMatch ? `>>${padded} ${line}` : ` ${padded} ${line}`;
|
|
215
|
+
};
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
// Add context before
|
|
218
|
+
if (match.contextBefore) {
|
|
219
|
+
for (const ctx of match.contextBefore) {
|
|
220
|
+
outputLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
217
221
|
}
|
|
222
|
+
}
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (const ctx of match.contextAfter) {
|
|
222
|
-
outputLines.push(
|
|
223
|
-
showLineNumbers
|
|
224
|
-
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
|
225
|
-
: `${relativePath}- ${ctx.line}`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
224
|
+
// Add match line
|
|
225
|
+
outputLines.push(formatLine(match.lineNumber, match.line, true));
|
|
229
226
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} else if (effectiveOutputMode === "filesWithMatches") {
|
|
233
|
-
// One line per file
|
|
234
|
-
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
|
235
|
-
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 1);
|
|
236
|
-
} else {
|
|
237
|
-
// count mode
|
|
238
|
-
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
|
239
|
-
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 0);
|
|
227
|
+
if (match.truncated) {
|
|
228
|
+
linesTruncated = true;
|
|
240
229
|
}
|
|
241
|
-
}
|
|
242
230
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
231
|
+
// Add context after
|
|
232
|
+
if (match.contextAfter) {
|
|
233
|
+
for (const ctx of match.contextAfter) {
|
|
234
|
+
outputLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Track per-file counts
|
|
239
|
+
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
248
240
|
}
|
|
249
241
|
|
|
250
242
|
const rawOutput = outputLines.join("\n");
|
|
@@ -261,7 +253,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
261
253
|
path,
|
|
262
254
|
count: fileMatchCounts.get(path) ?? 0,
|
|
263
255
|
})),
|
|
264
|
-
mode: effectiveOutputMode,
|
|
265
256
|
truncated,
|
|
266
257
|
matchLimitReached: result.limitReached ? effectiveLimit : undefined,
|
|
267
258
|
};
|
|
@@ -295,15 +286,13 @@ interface GrepRenderArgs {
|
|
|
295
286
|
glob?: string;
|
|
296
287
|
type?: string;
|
|
297
288
|
i?: boolean;
|
|
298
|
-
|
|
299
|
-
|
|
289
|
+
pre?: number;
|
|
290
|
+
post?: number;
|
|
300
291
|
multiline?: boolean;
|
|
301
|
-
output_mode?: string;
|
|
302
292
|
limit?: number;
|
|
303
293
|
offset?: number;
|
|
304
294
|
}
|
|
305
295
|
|
|
306
|
-
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
307
296
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
308
297
|
|
|
309
298
|
export const grepToolRenderer = {
|
|
@@ -313,10 +302,13 @@ export const grepToolRenderer = {
|
|
|
313
302
|
if (args.path) meta.push(`in ${args.path}`);
|
|
314
303
|
if (args.glob) meta.push(`glob:${args.glob}`);
|
|
315
304
|
if (args.type) meta.push(`type:${args.type}`);
|
|
316
|
-
if (args.output_mode && args.output_mode !== "filesWithMatches") meta.push(`mode:${args.output_mode}`);
|
|
317
305
|
if (args.i) meta.push("case:insensitive");
|
|
318
|
-
if (args.
|
|
319
|
-
|
|
306
|
+
if (args.pre !== undefined && args.pre > 0) {
|
|
307
|
+
meta.push(`pre:${args.pre}`);
|
|
308
|
+
}
|
|
309
|
+
if (args.post !== undefined && args.post > 0) {
|
|
310
|
+
meta.push(`post:${args.post}`);
|
|
311
|
+
}
|
|
320
312
|
if (args.multiline) meta.push("multiline");
|
|
321
313
|
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
|
322
314
|
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
|
@@ -369,13 +361,11 @@ export const grepToolRenderer = {
|
|
|
369
361
|
|
|
370
362
|
const matchCount = details?.matchCount ?? 0;
|
|
371
363
|
const fileCount = details?.fileCount ?? 0;
|
|
372
|
-
const mode = details?.mode ?? "filesWithMatches";
|
|
373
364
|
const truncation = details?.meta?.truncation;
|
|
374
365
|
const limits = details?.meta?.limits;
|
|
375
366
|
const truncated = Boolean(
|
|
376
367
|
details?.truncated || truncation || limits?.matchLimit || limits?.resultLimit || limits?.columnTruncated,
|
|
377
368
|
);
|
|
378
|
-
const files = details?.files ?? [];
|
|
379
369
|
|
|
380
370
|
if (matchCount === 0) {
|
|
381
371
|
const header = renderStatusLine(
|
|
@@ -385,10 +375,7 @@ export const grepToolRenderer = {
|
|
|
385
375
|
return new Text([header, formatEmptyMessage("No matches found", uiTheme)].join("\n"), 0, 0);
|
|
386
376
|
}
|
|
387
377
|
|
|
388
|
-
const summaryParts =
|
|
389
|
-
mode === "filesWithMatches"
|
|
390
|
-
? [formatCount("file", fileCount)]
|
|
391
|
-
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
378
|
+
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
392
379
|
const meta = [...summaryParts];
|
|
393
380
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
394
381
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
@@ -398,34 +385,51 @@ export const grepToolRenderer = {
|
|
|
398
385
|
uiTheme,
|
|
399
386
|
);
|
|
400
387
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
388
|
+
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
389
|
+
const rawLines = textContent.split("\n");
|
|
390
|
+
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
391
|
+
const matchGroups: string[][] = [];
|
|
392
|
+
if (hasSeparators) {
|
|
393
|
+
let current: string[] = [];
|
|
394
|
+
for (const line of rawLines) {
|
|
395
|
+
if (line.trim().length === 0) {
|
|
396
|
+
if (current.length > 0) {
|
|
397
|
+
matchGroups.push(current);
|
|
398
|
+
current = [];
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
current.push(line);
|
|
403
|
+
}
|
|
404
|
+
if (current.length > 0) matchGroups.push(current);
|
|
405
|
+
} else {
|
|
406
|
+
for (const line of rawLines) {
|
|
407
|
+
if (line.trim().length === 0) continue;
|
|
408
|
+
matchGroups.push([line]);
|
|
409
|
+
}
|
|
415
410
|
}
|
|
416
411
|
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
412
|
+
const getCollapsedMatchLimit = (groups: string[][], maxLines: number): number => {
|
|
413
|
+
if (groups.length === 0) return 0;
|
|
414
|
+
let usedLines = 0;
|
|
415
|
+
let count = 0;
|
|
416
|
+
for (const group of groups) {
|
|
417
|
+
if (count > 0 && usedLines + group.length > maxLines) break;
|
|
418
|
+
usedLines += group.length;
|
|
419
|
+
count += 1;
|
|
420
|
+
if (usedLines >= maxLines) break;
|
|
421
|
+
}
|
|
422
|
+
return count;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const maxCollapsed = expanded ? matchGroups.length : getCollapsedMatchLimit(matchGroups, COLLAPSED_TEXT_LIMIT);
|
|
426
|
+
const matchLines = renderTreeList(
|
|
421
427
|
{
|
|
422
|
-
|
|
423
|
-
path: entry.path,
|
|
424
|
-
isDirectory: entry.path.endsWith("/"),
|
|
425
|
-
meta: entry.count !== undefined ? `(${entry.count} match${entry.count !== 1 ? "es" : ""})` : undefined,
|
|
426
|
-
})),
|
|
428
|
+
items: matchGroups,
|
|
427
429
|
expanded,
|
|
428
|
-
maxCollapsed
|
|
430
|
+
maxCollapsed,
|
|
431
|
+
itemType: "match",
|
|
432
|
+
renderItem: group => group.map(line => uiTheme.fg("toolOutput", line)),
|
|
429
433
|
},
|
|
430
434
|
uiTheme,
|
|
431
435
|
);
|
|
@@ -440,7 +444,7 @@ export const grepToolRenderer = {
|
|
|
440
444
|
const extraLines =
|
|
441
445
|
truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
|
|
442
446
|
|
|
443
|
-
return new Text([header, ...
|
|
447
|
+
return new Text([header, ...matchLines, ...extraLines].join("\n"), 0, 0);
|
|
444
448
|
},
|
|
445
449
|
mergeCallAndResult: true,
|
|
446
450
|
};
|
package/src/web/search/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified Web Search Tool
|
|
3
3
|
*
|
|
4
|
-
* Single tool supporting Anthropic, Perplexity, and
|
|
4
|
+
* Single tool supporting Anthropic, Perplexity, Exa, and Jina providers with
|
|
5
5
|
* provider-specific parameters exposed conditionally.
|
|
6
6
|
*
|
|
7
7
|
* When EXA_API_KEY is available, additional specialized tools are exposed:
|
|
@@ -27,6 +27,7 @@ import { formatAge } from "../../tools/render-utils";
|
|
|
27
27
|
import { findAnthropicAuth } from "./auth";
|
|
28
28
|
import { searchAnthropic } from "./providers/anthropic";
|
|
29
29
|
import { searchExa } from "./providers/exa";
|
|
30
|
+
import { findApiKey as findJinaKey, searchJina } from "./providers/jina";
|
|
30
31
|
import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
|
|
31
32
|
import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render";
|
|
32
33
|
import type { WebSearchProvider, WebSearchResponse } from "./types";
|
|
@@ -36,7 +37,7 @@ import { WebSearchProviderError } from "./types";
|
|
|
36
37
|
export const webSearchSchema = Type.Object({
|
|
37
38
|
query: Type.String({ description: "Search query" }),
|
|
38
39
|
provider: Type.Optional(
|
|
39
|
-
StringEnum(["auto", "exa", "anthropic", "perplexity"], {
|
|
40
|
+
StringEnum(["auto", "exa", "jina", "anthropic", "perplexity"], {
|
|
40
41
|
description: "Search provider (default: auto)",
|
|
41
42
|
}),
|
|
42
43
|
),
|
|
@@ -50,7 +51,7 @@ export const webSearchSchema = Type.Object({
|
|
|
50
51
|
|
|
51
52
|
export type WebSearchParams = {
|
|
52
53
|
query: string;
|
|
53
|
-
provider?: "auto" | "exa" | "anthropic" | "perplexity";
|
|
54
|
+
provider?: "auto" | "exa" | "jina" | "anthropic" | "perplexity";
|
|
54
55
|
recency?: "day" | "week" | "month" | "year";
|
|
55
56
|
limit?: number;
|
|
56
57
|
};
|
|
@@ -70,6 +71,9 @@ async function getAvailableProviders(): Promise<WebSearchProvider[]> {
|
|
|
70
71
|
const exaKey = await findExaKey();
|
|
71
72
|
if (exaKey) providers.push("exa");
|
|
72
73
|
|
|
74
|
+
const jinaKey = await findJinaKey();
|
|
75
|
+
if (jinaKey) providers.push("jina");
|
|
76
|
+
|
|
73
77
|
const perplexityKey = await findPerplexityKey();
|
|
74
78
|
if (perplexityKey) providers.push("perplexity");
|
|
75
79
|
|
|
@@ -83,6 +87,8 @@ function formatProviderLabel(provider: WebSearchProvider): string {
|
|
|
83
87
|
switch (provider) {
|
|
84
88
|
case "exa":
|
|
85
89
|
return "Exa";
|
|
90
|
+
case "jina":
|
|
91
|
+
return "Jina";
|
|
86
92
|
case "perplexity":
|
|
87
93
|
return "Perplexity";
|
|
88
94
|
case "anthropic":
|
|
@@ -237,6 +243,11 @@ async function executeWebSearch(
|
|
|
237
243
|
query: params.query,
|
|
238
244
|
num_results: params.limit,
|
|
239
245
|
});
|
|
246
|
+
} else if (provider === "jina") {
|
|
247
|
+
response = await searchJina({
|
|
248
|
+
query: params.query,
|
|
249
|
+
num_results: params.limit,
|
|
250
|
+
});
|
|
240
251
|
} else if (provider === "anthropic") {
|
|
241
252
|
response = await searchAnthropic({
|
|
242
253
|
query: params.query,
|
|
@@ -279,7 +290,7 @@ async function executeWebSearch(
|
|
|
279
290
|
/**
|
|
280
291
|
* Web search tool implementation.
|
|
281
292
|
*
|
|
282
|
-
* Supports Anthropic, Perplexity, and
|
|
293
|
+
* Supports Anthropic, Perplexity, Exa, and Jina providers with automatic fallback.
|
|
283
294
|
* Session is accepted for interface consistency but not used.
|
|
284
295
|
*/
|
|
285
296
|
export class WebSearchTool implements AgentTool<typeof webSearchSchema, WebSearchRenderDetails> {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jina Reader Web Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses the Jina Reader `s.jina.ai` endpoint to fetch search results with
|
|
5
|
+
* cleaned content.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { WebSearchResponse, WebSearchSource } from "../../../web/search/types";
|
|
10
|
+
import { WebSearchProviderError } from "../../../web/search/types";
|
|
11
|
+
|
|
12
|
+
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
13
|
+
|
|
14
|
+
export interface JinaSearchParams {
|
|
15
|
+
query: string;
|
|
16
|
+
num_results?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface JinaSearchResult {
|
|
20
|
+
title?: string | null;
|
|
21
|
+
url?: string | null;
|
|
22
|
+
content?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type JinaSearchResponse = JinaSearchResult[];
|
|
26
|
+
|
|
27
|
+
/** Find JINA_API_KEY from environment or .env files. */
|
|
28
|
+
export function findApiKey(): string | null {
|
|
29
|
+
return getEnvApiKey("jina") ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Call Jina Reader search API. */
|
|
33
|
+
async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearchResponse> {
|
|
34
|
+
const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
|
|
35
|
+
const response = await fetch(requestUrl, {
|
|
36
|
+
headers: {
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
Authorization: `Bearer ${apiKey}`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
throw new WebSearchProviderError("jina", `Jina API error (${response.status}): ${errorText}`, response.status);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = (await response.json()) as unknown;
|
|
48
|
+
return Array.isArray(data) ? (data as JinaSearchResponse) : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Execute Jina web search. */
|
|
52
|
+
export async function searchJina(params: JinaSearchParams): Promise<WebSearchResponse> {
|
|
53
|
+
const apiKey = findApiKey();
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await callJinaSearch(apiKey, params.query);
|
|
59
|
+
const sources: WebSearchSource[] = [];
|
|
60
|
+
|
|
61
|
+
for (const result of response) {
|
|
62
|
+
if (!result?.url) continue;
|
|
63
|
+
sources.push({
|
|
64
|
+
title: result.title ?? result.url,
|
|
65
|
+
url: result.url,
|
|
66
|
+
snippet: result.content ?? undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const limitedSources = params.num_results ? sources.slice(0, params.num_results) : sources;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
provider: "jina",
|
|
74
|
+
sources: limitedSources,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/web/search/render.ts
CHANGED
|
@@ -113,7 +113,9 @@ export function renderWebSearchResult(
|
|
|
113
113
|
? "Perplexity"
|
|
114
114
|
: provider === "exa"
|
|
115
115
|
? "Exa"
|
|
116
|
-
: "
|
|
116
|
+
: provider === "jina"
|
|
117
|
+
? "Jina"
|
|
118
|
+
: "Unknown";
|
|
117
119
|
const queryPreview = args?.query
|
|
118
120
|
? truncateToWidth(args.query, 80)
|
|
119
121
|
: searchQueries[0]
|
package/src/web/search/types.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Web Search Types
|
|
3
3
|
*
|
|
4
|
-
* Unified types for web search responses across
|
|
4
|
+
* Unified types for web search responses across supported providers.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Supported web search providers */
|
|
8
|
-
export type WebSearchProvider = "exa" | "anthropic" | "perplexity" | "gemini" | "codex";
|
|
8
|
+
export type WebSearchProvider = "exa" | "jina" | "anthropic" | "perplexity" | "gemini" | "codex";
|
|
9
9
|
|
|
10
10
|
/** Source returned by search (all providers) */
|
|
11
11
|
export interface WebSearchSource {
|