@ottocode/sdk 0.1.302 → 0.1.304
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/package.json +35 -28
- package/src/core/src/search/fff.ts +215 -0
- package/src/core/src/tools/builtin/fs/read.txt +1 -1
- package/src/core/src/tools/builtin/glob.txt +1 -1
- package/src/core/src/tools/builtin/search.ts +157 -0
- package/src/core/src/tools/builtin/search.txt +14 -0
- package/src/core/src/tools/builtin/shell.ts +23 -0
- package/src/core/src/tools/builtin/shell.txt +3 -1
- package/src/core/src/tools/loader.ts +3 -3
- package/src/prompts/src/agents/build.txt +2 -1
- package/src/prompts/src/agents/plan.txt +2 -1
- package/src/prompts/src/agents/research.txt +1 -1
- package/src/prompts/src/providers/anthropic.txt +2 -2
- package/src/prompts/src/providers/default.txt +2 -2
- package/src/prompts/src/providers/glm.txt +2 -2
- package/src/prompts/src/providers/google.txt +1 -1
- package/src/prompts/src/providers/moonshot.txt +2 -2
- package/src/prompts/src/providers/openai.txt +2 -2
- package/src/providers/src/catalog.ts +107 -37
- package/src/providers/src/oauth-models.ts +1 -0
- package/src/core/src/tools/builtin/ripgrep.ts +0 -188
- package/src/core/src/tools/builtin/ripgrep.txt +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.304",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "nitishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"import": "./src/core/src/tools/builtin/progress.ts",
|
|
62
62
|
"types": "./src/core/src/tools/builtin/progress.ts"
|
|
63
63
|
},
|
|
64
|
-
"./tools/builtin/
|
|
65
|
-
"import": "./src/core/src/tools/builtin/
|
|
66
|
-
"types": "./src/core/src/tools/builtin/
|
|
64
|
+
"./tools/builtin/search": {
|
|
65
|
+
"import": "./src/core/src/tools/builtin/search.ts",
|
|
66
|
+
"types": "./src/core/src/tools/builtin/search.ts"
|
|
67
67
|
},
|
|
68
68
|
"./tools/builtin/websearch": {
|
|
69
69
|
"import": "./src/core/src/tools/builtin/websearch.ts",
|
|
@@ -81,6 +81,10 @@
|
|
|
81
81
|
"import": "./src/core/src/tools/bin-manager.ts",
|
|
82
82
|
"types": "./src/core/src/tools/bin-manager.ts"
|
|
83
83
|
},
|
|
84
|
+
"./search/fff": {
|
|
85
|
+
"import": "./src/core/src/search/fff.ts",
|
|
86
|
+
"types": "./src/core/src/search/fff.ts"
|
|
87
|
+
},
|
|
84
88
|
"./prompts/*": "./src/prompts/src/*"
|
|
85
89
|
},
|
|
86
90
|
"files": [
|
|
@@ -94,33 +98,36 @@
|
|
|
94
98
|
"typecheck": "tsc --noEmit"
|
|
95
99
|
},
|
|
96
100
|
"dependencies": {
|
|
97
|
-
"@ai-sdk/anthropic": "
|
|
98
|
-
"@ai-sdk/google": "
|
|
99
|
-
"@ai-sdk/openai": "
|
|
100
|
-
"@ai-sdk/openai-compatible": "
|
|
101
|
-
"@ai-sdk/
|
|
102
|
-
"@
|
|
103
|
-
"@
|
|
104
|
-
"@
|
|
101
|
+
"@ai-sdk/anthropic": "3.0.82",
|
|
102
|
+
"@ai-sdk/google": "3.0.80",
|
|
103
|
+
"@ai-sdk/openai": "3.0.69",
|
|
104
|
+
"@ai-sdk/openai-compatible": "2.0.46",
|
|
105
|
+
"@ai-sdk/provider": "3.0.10",
|
|
106
|
+
"@ai-sdk/provider-utils": "4.0.27",
|
|
107
|
+
"@ai-sdk/xai": "3.0.93",
|
|
108
|
+
"@ff-labs/fff-bun": "0.9.3",
|
|
109
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
110
|
+
"@openauthjs/openauth": "0.4.3",
|
|
111
|
+
"@openrouter/ai-sdk-provider": "1.5.4",
|
|
105
112
|
"@ottorouter/ai-sdk": "0.2.6",
|
|
106
|
-
"@solana/web3.js": "
|
|
107
|
-
"ai": "
|
|
108
|
-
"ai-sdk-ollama": "
|
|
109
|
-
"bs58": "
|
|
110
|
-
"bun-pty": "
|
|
111
|
-
"diff": "
|
|
112
|
-
"fast-glob": "
|
|
113
|
-
"hono": "
|
|
114
|
-
"opencode-anthropic-auth": "
|
|
115
|
-
"qrcode": "
|
|
116
|
-
"qrcode-terminal": "
|
|
117
|
-
"tweetnacl": "
|
|
118
|
-
"x402": "
|
|
119
|
-
"zod": "
|
|
113
|
+
"@solana/web3.js": "1.98.4",
|
|
114
|
+
"ai": "6.0.199",
|
|
115
|
+
"ai-sdk-ollama": "3.8.3",
|
|
116
|
+
"bs58": "6.0.0",
|
|
117
|
+
"bun-pty": "0.3.2",
|
|
118
|
+
"diff": "8.0.3",
|
|
119
|
+
"fast-glob": "3.3.3",
|
|
120
|
+
"hono": "4.12.0",
|
|
121
|
+
"opencode-anthropic-auth": "0.0.2",
|
|
122
|
+
"qrcode": "1.5.4",
|
|
123
|
+
"qrcode-terminal": "0.12.0",
|
|
124
|
+
"tweetnacl": "1.0.3",
|
|
125
|
+
"x402": "1.1.0",
|
|
126
|
+
"zod": "4.3.6"
|
|
120
127
|
},
|
|
121
128
|
"devDependencies": {
|
|
122
|
-
"@types/bun": "
|
|
123
|
-
"typescript": "
|
|
129
|
+
"@types/bun": "1.3.14",
|
|
130
|
+
"typescript": "5.9.3"
|
|
124
131
|
},
|
|
125
132
|
"keywords": [
|
|
126
133
|
"ai",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { FileFinder, type FileFinderApi } from '@ff-labs/fff-bun';
|
|
5
|
+
|
|
6
|
+
export type FffFileSearchResult = {
|
|
7
|
+
files: string[];
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FinderEntry = {
|
|
12
|
+
finder: FileFinderApi;
|
|
13
|
+
ready: Promise<void>;
|
|
14
|
+
lastScanMs: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const finderCache = new Map<string, Promise<FinderEntry>>();
|
|
18
|
+
|
|
19
|
+
function normalizeRoot(path: string): string {
|
|
20
|
+
return resolve(path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isHomeRoot(path: string): boolean {
|
|
24
|
+
return normalizeRoot(path) === normalizeRoot(homedir());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function createFinderEntry(basePath: string): Promise<FinderEntry> {
|
|
28
|
+
const result = FileFinder.create({
|
|
29
|
+
basePath,
|
|
30
|
+
aiMode: true,
|
|
31
|
+
disableWatch: true,
|
|
32
|
+
enableHomeDirScanning: isHomeRoot(basePath),
|
|
33
|
+
enableFsRootScanning: false,
|
|
34
|
+
});
|
|
35
|
+
if (!result.ok) throw new Error(result.error);
|
|
36
|
+
|
|
37
|
+
const finder = result.value;
|
|
38
|
+
const entry: FinderEntry = {
|
|
39
|
+
finder,
|
|
40
|
+
ready: Promise.resolve(),
|
|
41
|
+
lastScanMs: 0,
|
|
42
|
+
};
|
|
43
|
+
const ready = (async () => {
|
|
44
|
+
const scan = await finder.waitForIndexReady(10_000);
|
|
45
|
+
if (!scan.ok) throw new Error(scan.error);
|
|
46
|
+
entry.lastScanMs = Date.now();
|
|
47
|
+
})();
|
|
48
|
+
entry.ready = ready;
|
|
49
|
+
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getFffEntry(basePath: string): Promise<FinderEntry> {
|
|
54
|
+
const root = normalizeRoot(basePath);
|
|
55
|
+
let entryPromise = finderCache.get(root);
|
|
56
|
+
if (!entryPromise) {
|
|
57
|
+
entryPromise = createFinderEntry(root).catch((error) => {
|
|
58
|
+
finderCache.delete(root);
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
finderCache.set(root, entryPromise);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entry = await entryPromise;
|
|
65
|
+
await entry.ready;
|
|
66
|
+
return entry;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Return a cached FFF finder for a root directory.
|
|
71
|
+
*/
|
|
72
|
+
export async function getFffFinder(basePath: string): Promise<FileFinderApi> {
|
|
73
|
+
const entry = await getFffEntry(basePath);
|
|
74
|
+
return entry.finder;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Force FFF to rescan a root and wait briefly for fresh results.
|
|
79
|
+
*/
|
|
80
|
+
export async function refreshFffIndex(
|
|
81
|
+
basePath: string,
|
|
82
|
+
timeoutMs = 3_000,
|
|
83
|
+
maxAgeMs = 0,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const entry = await getFffEntry(basePath);
|
|
86
|
+
if (maxAgeMs > 0 && Date.now() - entry.lastScanMs < maxAgeMs) return;
|
|
87
|
+
const finder = entry.finder;
|
|
88
|
+
const scan = finder.scanFiles();
|
|
89
|
+
if (!scan.ok) throw new Error(scan.error);
|
|
90
|
+
const done = await finder.waitForScan(timeoutMs);
|
|
91
|
+
if (!done.ok) throw new Error(done.error);
|
|
92
|
+
entry.lastScanMs = Date.now();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function clearFffFinderCache(): void {
|
|
96
|
+
for (const entryPromise of finderCache.values()) {
|
|
97
|
+
entryPromise.then((entry) => entry.finder.destroy()).catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
finderCache.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeRelativePath(root: string, path: string): string {
|
|
103
|
+
const rel = relative(root, path).replace(/\\/g, '/');
|
|
104
|
+
return rel === '' ? '.' : rel;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fileDepth(relativePath: string): number {
|
|
108
|
+
if (relativePath === '.') return 0;
|
|
109
|
+
return Math.max(0, relativePath.split(/[\\/]/).length - 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isWithinRoot(root: string, target: string): boolean {
|
|
113
|
+
const rel = relative(root, target);
|
|
114
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function getRootAndConstraint(projectRoot: string, inputPath = '.') {
|
|
118
|
+
const trimmed = inputPath.trim() || '.';
|
|
119
|
+
const expanded =
|
|
120
|
+
trimmed === '~'
|
|
121
|
+
? homedir()
|
|
122
|
+
: trimmed.startsWith('~/')
|
|
123
|
+
? `${homedir()}/${trimmed.slice(2)}`
|
|
124
|
+
: trimmed;
|
|
125
|
+
const target = isAbsolute(expanded)
|
|
126
|
+
? resolve(expanded)
|
|
127
|
+
: resolve(projectRoot, expanded);
|
|
128
|
+
const root = normalizeRoot(projectRoot);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const targetStat = await stat(target);
|
|
132
|
+
if (isWithinRoot(root, target)) {
|
|
133
|
+
const rel = normalizeRelativePath(root, target);
|
|
134
|
+
if (rel === '.') return { basePath: root, constraint: '' };
|
|
135
|
+
return {
|
|
136
|
+
basePath: root,
|
|
137
|
+
constraint: targetStat.isDirectory() ? `${rel}/` : rel,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (targetStat.isDirectory()) return { basePath: target, constraint: '' };
|
|
142
|
+
return {
|
|
143
|
+
basePath: dirname(target),
|
|
144
|
+
constraint: target.split(/[\\/]/).pop() ?? '',
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
if (isWithinRoot(root, target)) {
|
|
148
|
+
const rel = normalizeRelativePath(root, target);
|
|
149
|
+
return { basePath: root, constraint: rel === '.' ? '' : rel };
|
|
150
|
+
}
|
|
151
|
+
return { basePath: target, constraint: '' };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function resolveFffSearchScope(projectRoot: string, path = '.') {
|
|
156
|
+
return getRootAndConstraint(projectRoot, path);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function searchFffFiles(args: {
|
|
160
|
+
projectRoot: string;
|
|
161
|
+
query?: string;
|
|
162
|
+
path?: string;
|
|
163
|
+
exclude?: string[];
|
|
164
|
+
maxDepth: number;
|
|
165
|
+
limit: number;
|
|
166
|
+
}): Promise<FffFileSearchResult> {
|
|
167
|
+
const { basePath, constraint } = await getRootAndConstraint(
|
|
168
|
+
args.projectRoot,
|
|
169
|
+
args.path ?? '.',
|
|
170
|
+
);
|
|
171
|
+
await refreshFffIndex(basePath, 3_000, 2_000);
|
|
172
|
+
const finder = await getFffFinder(basePath);
|
|
173
|
+
const query = args.query?.trim() ?? '';
|
|
174
|
+
const constraints = [
|
|
175
|
+
constraint,
|
|
176
|
+
...(args.exclude ?? []).map((pattern) => `!${pattern.replace(/^!/, '')}`),
|
|
177
|
+
]
|
|
178
|
+
.map((part) => part.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(' ');
|
|
181
|
+
const pageSize = Math.min(Math.max(args.limit * 2, 100), 1_000);
|
|
182
|
+
const files: string[] = [];
|
|
183
|
+
const seen = new Set<string>();
|
|
184
|
+
let truncated = false;
|
|
185
|
+
|
|
186
|
+
for (
|
|
187
|
+
let pageIndex = 0;
|
|
188
|
+
pageIndex < 100 && files.length < args.limit;
|
|
189
|
+
pageIndex++
|
|
190
|
+
) {
|
|
191
|
+
const result =
|
|
192
|
+
query || constraints
|
|
193
|
+
? finder.fileSearch(`${constraints ? `${constraints} ` : ''}${query}`, {
|
|
194
|
+
pageIndex,
|
|
195
|
+
pageSize,
|
|
196
|
+
})
|
|
197
|
+
: finder.glob(constraint || '**/*', { pageIndex, pageSize });
|
|
198
|
+
if (!result.ok) throw new Error(result.error);
|
|
199
|
+
|
|
200
|
+
for (const item of result.value.items) {
|
|
201
|
+
const relativePath = item.relativePath;
|
|
202
|
+
if (seen.has(relativePath)) continue;
|
|
203
|
+
if (fileDepth(relativePath) >= args.maxDepth) continue;
|
|
204
|
+
seen.add(relativePath);
|
|
205
|
+
files.push(relativePath);
|
|
206
|
+
if (files.length >= args.limit) break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const consumed = (pageIndex + 1) * pageSize;
|
|
210
|
+
if (consumed >= result.value.totalMatched) break;
|
|
211
|
+
if (files.length >= args.limit) truncated = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { files: files.slice(0, args.limit), truncated };
|
|
215
|
+
}
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
Usage tips:
|
|
7
7
|
- Prefer relative project paths when possible (more portable)
|
|
8
|
-
- For large files or searches, use the
|
|
8
|
+
- For large files or searches, use the `search` tool
|
|
9
9
|
- Use startLine/endLine or startLine/maxLines for targeted reads of large files
|
|
10
10
|
- If startLine is provided without endLine or maxLines, only that one line is read
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
## Usage tips
|
|
10
10
|
|
|
11
|
-
- Use `glob` for filename patterns; use `
|
|
11
|
+
- Use `glob` for filename patterns; use `search` for file contents.
|
|
12
12
|
- Combine with `path` to restrict the search to a subdirectory.
|
|
13
13
|
- Prefer reading a known file directly over globbing to "find" it (check the `<project>` listing in the system prompt first).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
import DESCRIPTION from './search.txt' with { type: 'text' };
|
|
4
|
+
import { createToolError, type ToolResponse } from '../error.ts';
|
|
5
|
+
import {
|
|
6
|
+
getFffFinder,
|
|
7
|
+
refreshFffIndex,
|
|
8
|
+
resolveFffSearchScope,
|
|
9
|
+
} from '../../search/fff.ts';
|
|
10
|
+
|
|
11
|
+
const TEXT_MAX = 200;
|
|
12
|
+
|
|
13
|
+
type SearchMatch = { file: string; line: number; text: string };
|
|
14
|
+
|
|
15
|
+
type SearchToolResult = {
|
|
16
|
+
count: number;
|
|
17
|
+
matches: SearchMatch[];
|
|
18
|
+
truncated?: boolean;
|
|
19
|
+
shownMatches?: number;
|
|
20
|
+
files?: Array<{ file: string; matches: number }>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function truncateText(text: string): string {
|
|
24
|
+
return text.length > TEXT_MAX ? `${text.slice(0, TEXT_MAX)}…` : text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildFffQuery(args: {
|
|
28
|
+
query: string;
|
|
29
|
+
pathConstraint: string;
|
|
30
|
+
glob?: string;
|
|
31
|
+
ignoreCase?: boolean;
|
|
32
|
+
}): string {
|
|
33
|
+
const constraints = [args.pathConstraint, args.glob]
|
|
34
|
+
.map((part) => part?.trim() ?? '')
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
const query = args.ignoreCase ? `(?i)${args.query}` : args.query;
|
|
37
|
+
return [...constraints, query].join(' ');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function summarizeMatches(matches: SearchMatch[]) {
|
|
41
|
+
const fileCounts = new Map<string, number>();
|
|
42
|
+
for (const match of matches) {
|
|
43
|
+
fileCounts.set(match.file, (fileCounts.get(match.file) ?? 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
return Array.from(fileCounts.entries()).map(([file, count]) => ({
|
|
46
|
+
file,
|
|
47
|
+
matches: count,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildSearchTool(projectRoot: string): {
|
|
52
|
+
name: string;
|
|
53
|
+
tool: Tool;
|
|
54
|
+
} {
|
|
55
|
+
const search = tool({
|
|
56
|
+
description: DESCRIPTION,
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
query: z.string().min(1).describe('Search pattern (regex by default)'),
|
|
59
|
+
path: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.default('.')
|
|
63
|
+
.describe('Relative path to search in'),
|
|
64
|
+
ignoreCase: z.boolean().optional().default(false),
|
|
65
|
+
glob: z
|
|
66
|
+
.array(z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('One or more glob patterns to include'),
|
|
69
|
+
maxResults: z.number().int().min(1).max(5000).optional().default(100),
|
|
70
|
+
}),
|
|
71
|
+
async execute({
|
|
72
|
+
query,
|
|
73
|
+
path = '.',
|
|
74
|
+
ignoreCase,
|
|
75
|
+
glob,
|
|
76
|
+
maxResults = 100,
|
|
77
|
+
}: {
|
|
78
|
+
query: string;
|
|
79
|
+
path?: string;
|
|
80
|
+
ignoreCase?: boolean;
|
|
81
|
+
glob?: string[];
|
|
82
|
+
maxResults?: number;
|
|
83
|
+
}): Promise<ToolResponse<SearchToolResult>> {
|
|
84
|
+
try {
|
|
85
|
+
const { basePath, constraint } = await resolveFffSearchScope(
|
|
86
|
+
projectRoot,
|
|
87
|
+
path,
|
|
88
|
+
);
|
|
89
|
+
await refreshFffIndex(basePath);
|
|
90
|
+
const finder = await getFffFinder(basePath);
|
|
91
|
+
const includeGlobs =
|
|
92
|
+
Array.isArray(glob) && glob.length > 0 ? glob : [undefined];
|
|
93
|
+
const matches: SearchMatch[] = [];
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
let truncated = false;
|
|
96
|
+
|
|
97
|
+
for (const includeGlob of includeGlobs) {
|
|
98
|
+
if (matches.length >= maxResults) {
|
|
99
|
+
truncated = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fffQuery = buildFffQuery({
|
|
104
|
+
query,
|
|
105
|
+
pathConstraint: constraint,
|
|
106
|
+
glob: includeGlob,
|
|
107
|
+
ignoreCase,
|
|
108
|
+
});
|
|
109
|
+
const result = finder.grep(fffQuery, {
|
|
110
|
+
mode: 'regex',
|
|
111
|
+
smartCase: false,
|
|
112
|
+
pageSize: maxResults - matches.length,
|
|
113
|
+
maxMatchesPerFile: maxResults,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
return createToolError(result.error, 'execution', {
|
|
118
|
+
suggestion: 'Check if the search query is valid',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const item of result.value.items) {
|
|
123
|
+
const match = {
|
|
124
|
+
file: item.relativePath,
|
|
125
|
+
line: item.lineNumber,
|
|
126
|
+
text: truncateText(item.lineContent),
|
|
127
|
+
};
|
|
128
|
+
const key = `${match.file}:${match.line}:${match.text}`;
|
|
129
|
+
if (seen.has(key)) continue;
|
|
130
|
+
seen.add(key);
|
|
131
|
+
matches.push(match);
|
|
132
|
+
if (matches.length >= maxResults) break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
truncated = truncated || Boolean(result.value.nextCursor);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const files = summarizeMatches(matches);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
count: matches.length,
|
|
143
|
+
matches,
|
|
144
|
+
...(truncated
|
|
145
|
+
? { truncated: true, shownMatches: matches.length }
|
|
146
|
+
: {}),
|
|
147
|
+
...(files.length ? { files } : {}),
|
|
148
|
+
};
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return createToolError(String(err), 'execution', {
|
|
151
|
+
suggestion: 'Check if FFF is available and the query is valid',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
return { name: 'search', tool: search };
|
|
157
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
- Search file contents using the FFF indexed search engine
|
|
2
|
+
- Returns a flat list of matches with `file`, `line`, and `text`
|
|
3
|
+
- Supports regex patterns, include globs, path constraints, and case-insensitive search
|
|
4
|
+
- Respects `.gitignore` by default
|
|
5
|
+
|
|
6
|
+
Use this for text/code search across the codebase. It is the primary tool for repository content discovery.
|
|
7
|
+
|
|
8
|
+
## Usage tips
|
|
9
|
+
|
|
10
|
+
- Narrow broad searches with `path` and `glob` values.
|
|
11
|
+
- Keep `maxResults` low for broad searches; the tool returns at most that many matches.
|
|
12
|
+
- Batch independent searches (e.g. multiple function names) in a single turn for parallel execution.
|
|
13
|
+
- Use `ignoreCase: true` for case-insensitive matching; pass `glob` patterns (e.g. `["*.ts", "*.tsx"]`) to limit file types.
|
|
14
|
+
- For filename/path discovery, use `glob` first when you already know the file pattern.
|
|
@@ -47,6 +47,17 @@ export type ShellOutputMode = 'auto' | 'full' | 'tail';
|
|
|
47
47
|
const DEFAULT_TAIL_LINES = 100;
|
|
48
48
|
const DEFAULT_MAX_OUTPUT_BYTES = 128_000;
|
|
49
49
|
|
|
50
|
+
function looksLikeRepositorySearchCommand(cmd: string): boolean {
|
|
51
|
+
const normalized = cmd.replace(/\s+/g, ' ').trim();
|
|
52
|
+
return (
|
|
53
|
+
/(^|[;&|()]\s*)(rg|ripgrep)(\s|$)/.test(normalized) ||
|
|
54
|
+
/(^|[;&|()]\s*)grep\s+.*\s(-R|-r|--recursive)(\s|$)/.test(normalized) ||
|
|
55
|
+
/(^|[;&|()]\s*)find\s+(\.|\.\/|\$PWD|\S*\/).*(-name|-iname|-path|-type)/.test(
|
|
56
|
+
normalized,
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
type CompactTextResult = {
|
|
51
62
|
text: string;
|
|
52
63
|
truncated: boolean;
|
|
@@ -220,6 +231,18 @@ export function buildShellTool(projectRoot: string): {
|
|
|
220
231
|
});
|
|
221
232
|
}
|
|
222
233
|
|
|
234
|
+
if (looksLikeRepositorySearchCommand(cmd)) {
|
|
235
|
+
return createToolError(
|
|
236
|
+
'This looks like repository discovery. Use the search tool for content/code search or glob for filename/path discovery.',
|
|
237
|
+
'validation',
|
|
238
|
+
{
|
|
239
|
+
cmd,
|
|
240
|
+
suggestion:
|
|
241
|
+
'Use search for file contents, or glob for file and path discovery.',
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
const absCwd = resolveSafePath(projectRoot, cwd || '.');
|
|
224
247
|
const finalCmd = injectCoAuthorIntoGitCommit(cmd);
|
|
225
248
|
const shellExecutor = shellExecutorContext.getStore();
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
**Use `shell` for one-off, non-interactive commands.** These may be short-lived checks or long-running commands that finish on their own and do not require stdin. For commands that need interactive input, a TTY, or persistence across turns, use the `terminal` tool instead.
|
|
6
6
|
|
|
7
|
+
For repository discovery, use `search` for content/code search and `glob` for filename/path discovery. Reserve `shell` for execution, builds, tests, diagnostics, and other command-line tasks.
|
|
8
|
+
|
|
7
9
|
## Usage tips
|
|
8
10
|
|
|
9
11
|
- Chain commands with `&&` to fail-fast.
|
|
10
12
|
- For long outputs, redirect to a file, inspect `wc -c`, then read a small range or tail.
|
|
11
13
|
- `outputMode: "auto"` is the default and keeps bounded tail output. Use `outputMode: "tail"` with `tailLines` for verbose builds/tests, or `outputMode: "full"` only when complete output is truly needed.
|
|
12
14
|
- Final `stdout`/`stderr` are capped by `maxOutputBytes` per stream to avoid huge tool results. Set `maxOutputBytes: 0` only when you intentionally need uncapped output.
|
|
13
|
-
- For binary
|
|
15
|
+
- For binary/minified inspection that cannot be handled by `search`, cap line width and output explicitly.
|
|
14
16
|
- For long-running non-interactive commands, set an appropriate `timeout` and ensure the command exits on its own.
|
|
15
17
|
- Batch independent checks (e.g. `git status && git diff`) in parallel tool calls rather than sequential shell chains when you need results separately.
|
|
16
18
|
- Never use `shell` with `sed`/`awk` for programmatic file editing — use the dedicated file-editing tools instead.
|
|
@@ -5,7 +5,7 @@ import { buildFsTools } from './builtin/fs/index.ts';
|
|
|
5
5
|
import { buildGitTools } from './builtin/git.ts';
|
|
6
6
|
import { progressUpdateTool } from './builtin/progress.ts';
|
|
7
7
|
import { buildShellTool } from './builtin/shell.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { buildSearchTool } from './builtin/search.ts';
|
|
9
9
|
import { buildGlobTool } from './builtin/glob.ts';
|
|
10
10
|
import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
11
11
|
import { updateTodosTool } from './builtin/todos.ts';
|
|
@@ -153,8 +153,8 @@ async function discoverStaticProjectTools(
|
|
|
153
153
|
const shell = buildShellTool(projectRoot);
|
|
154
154
|
tools.set(shell.name, shell.tool);
|
|
155
155
|
// Search
|
|
156
|
-
const
|
|
157
|
-
tools.set(
|
|
156
|
+
const search = buildSearchTool(projectRoot);
|
|
157
|
+
tools.set(search.name, search.tool);
|
|
158
158
|
const glob = buildGlobTool(projectRoot);
|
|
159
159
|
tools.set(glob.name, glob.tool);
|
|
160
160
|
// Patch/apply
|
|
@@ -31,5 +31,6 @@ After making changes:
|
|
|
31
31
|
|
|
32
32
|
## Searching & discovery
|
|
33
33
|
|
|
34
|
-
- `
|
|
34
|
+
- Use the `search` tool for content/code search and `glob` for filename patterns.
|
|
35
|
+
- Reserve `shell` for execution, builds, tests, and other command-line tasks.
|
|
35
36
|
- Batch independent reads/searches in a single turn for performance.
|
|
@@ -5,6 +5,7 @@ You may ONLY observe, analyze, and plan.
|
|
|
5
5
|
</system-reminder>
|
|
6
6
|
|
|
7
7
|
Your job: produce an actionable, minimal plan.
|
|
8
|
-
- Use only read/inspect tools (read, ls, tree,
|
|
8
|
+
- Use only read/inspect tools (read, ls, tree, search, glob, git_diff).
|
|
9
|
+
- Use `search` for content/code search and `glob` for filename/path discovery.
|
|
9
10
|
- Identify concrete steps with just enough detail to execute later.
|
|
10
11
|
- No changes. No write operations. No refactors.
|
|
@@ -97,7 +97,7 @@ assistant: Clients are marked as failed in the `connectToServer` function in src
|
|
|
97
97
|
For software engineering requests (bugs, features, refactors, explanations):
|
|
98
98
|
|
|
99
99
|
1. Use `update_todos` to plan multi-step work.
|
|
100
|
-
2. Explore the codebase with the search tools (`
|
|
100
|
+
2. Explore the codebase with the search tools (`search`, `glob`, `tree`, `read`). Batch independent searches in parallel.
|
|
101
101
|
3. Implement the solution.
|
|
102
102
|
4. Verify — run the project's build/lint/test commands with `shell`. Check `README.md` / `AGENTS.md` to find the right command.
|
|
103
103
|
5. Review diffs with `git_status` / `git_diff`.
|
|
@@ -109,7 +109,7 @@ When the user mentions a specific file (e.g. `@publish.config`, `src/app.ts`, `p
|
|
|
109
109
|
|
|
110
110
|
- Check the `<project>` listing in the system prompt first — if the file is there, read it directly.
|
|
111
111
|
- Do NOT waste tool calls searching for a file whose path is already known.
|
|
112
|
-
- Fall back to `glob` / `
|
|
112
|
+
- Fall back to `glob` / `search` only when the path is genuinely ambiguous.
|
|
113
113
|
|
|
114
114
|
# Batching tool calls
|
|
115
115
|
|
|
@@ -24,7 +24,7 @@ You are a coding agent running in otto, a terminal-based coding assistant. Preci
|
|
|
24
24
|
|
|
25
25
|
# Working on tasks
|
|
26
26
|
|
|
27
|
-
1. Understand — use `
|
|
27
|
+
1. Understand — use `search`, `glob`, `tree`, `read` to map the code. Batch independent searches.
|
|
28
28
|
2. Plan — use `update_todos` for multi-step work. Mark one step `in_progress` at a time.
|
|
29
29
|
3. Implement — prefer the targeted editing tools available to you for small in-file changes; use patch-style edits for structural or multi-file changes when available; use `write` only for new files or near-total rewrites.
|
|
30
30
|
4. Verify — run project-specific build/lint/test commands via `shell`. Check `README.md` / `AGENTS.md` for the right command.
|
|
@@ -36,7 +36,7 @@ When the user names a specific file:
|
|
|
36
36
|
|
|
37
37
|
- Check the `<project>` listing in the system prompt first — read directly if listed.
|
|
38
38
|
- Don't waste tool calls searching for a known path.
|
|
39
|
-
- Fall back to `glob` / `
|
|
39
|
+
- Fall back to `glob` / `search` only when the path is genuinely ambiguous.
|
|
40
40
|
|
|
41
41
|
# Batching and parallelism
|
|
42
42
|
|
|
@@ -36,7 +36,7 @@ Your reasoning is powerful, but it can override what you actually read. Guard ag
|
|
|
36
36
|
|
|
37
37
|
# Working on tasks
|
|
38
38
|
|
|
39
|
-
1. Understand — use `
|
|
39
|
+
1. Understand — use `search`, `glob`, `tree`, `read` to map the code. Batch independent searches.
|
|
40
40
|
2. Plan — use `update_todos` for multi-step work. Mark one step `in_progress` at a time.
|
|
41
41
|
3. Implement — prefer the targeted editing tools available to you for small in-file changes; use patch-style edits for structural or multi-file changes when available; use `write` only for new files or near-total rewrites.
|
|
42
42
|
4. Verify — run project-specific build/lint/test commands via `shell`. Check `README.md` / `AGENTS.md` for the right command.
|
|
@@ -48,7 +48,7 @@ When the user names a specific file:
|
|
|
48
48
|
|
|
49
49
|
- Check the `<project>` listing in the system prompt first — read directly if listed.
|
|
50
50
|
- Don't waste tool calls searching for a known path.
|
|
51
|
-
- Fall back to `glob` / `
|
|
51
|
+
- Fall back to `glob` / `search` only when the path is genuinely ambiguous.
|
|
52
52
|
|
|
53
53
|
# Batching and parallelism
|
|
54
54
|
|
|
@@ -19,7 +19,7 @@ You are Gemini, operating as otto — an interactive CLI coding agent specializi
|
|
|
19
19
|
|
|
20
20
|
For bug fixes, features, refactors, or explanations:
|
|
21
21
|
|
|
22
|
-
1. **Understand.** Use `
|
|
22
|
+
1. **Understand.** Use `search` / `glob` / `tree` / `read` extensively (in parallel when independent) to understand structure, patterns, and conventions.
|
|
23
23
|
2. **Plan.** Build a grounded plan. Share an extremely concise plan with the user if it helps. Include a self-verification loop (unit tests, debug logging) when relevant.
|
|
24
24
|
3. **Implement.** Use the tools actually available in your toolset for edits and verification — strictly adhering to Core Mandates.
|
|
25
25
|
4. **Verify (tests).** Identify test commands from `README`, `package.json`, or existing test patterns. NEVER assume standard test commands.
|
|
@@ -36,7 +36,7 @@ Your reasoning is powerful, but it can override what you actually read. Guard ag
|
|
|
36
36
|
|
|
37
37
|
# Working on tasks
|
|
38
38
|
|
|
39
|
-
1. Understand — use `
|
|
39
|
+
1. Understand — use `search`, `glob`, `tree`, `read` to map the code. Batch independent searches.
|
|
40
40
|
2. Plan — use `update_todos` for multi-step work. Mark one step `in_progress` at a time.
|
|
41
41
|
3. Implement — prefer the targeted editing tools available to you for small in-file changes; use patch-style edits for structural or multi-file changes when available; use `write` only for new files or near-total rewrites.
|
|
42
42
|
4. Verify — run project-specific build/lint/test commands via `shell`. Check `README.md` / `AGENTS.md` for the right command.
|
|
@@ -48,7 +48,7 @@ When the user names a specific file:
|
|
|
48
48
|
|
|
49
49
|
- Check the `<project>` listing in the system prompt first — read directly if listed.
|
|
50
50
|
- Don't waste tool calls searching for a known path.
|
|
51
|
-
- Fall back to `glob` / `
|
|
51
|
+
- Fall back to `glob` / `search` only when the path is genuinely ambiguous.
|
|
52
52
|
|
|
53
53
|
# Batching and parallelism
|
|
54
54
|
|
|
@@ -172,8 +172,8 @@ For casual greetings, acknowledgements, or one-off conversational messages, resp
|
|
|
172
172
|
|
|
173
173
|
# Tool usage policy
|
|
174
174
|
|
|
175
|
-
- For content search:
|
|
175
|
+
- For content/code search: use the `search` tool.
|
|
176
176
|
- For file pattern matching: prefer `glob` tool.
|
|
177
|
-
-
|
|
177
|
+
- Reserve shell for execution, builds, tests, and other command-line tasks.
|
|
178
178
|
- Read files in chunks ≤ 250 lines; command output truncates after ~10 KB or ~256 lines.
|
|
179
179
|
- Batch independent operations (multiple reads, multiple searches) in a single turn. Only serialize when the next call depends on the previous result.
|
|
@@ -1292,6 +1292,32 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
1292
1292
|
output: 4096,
|
|
1293
1293
|
},
|
|
1294
1294
|
},
|
|
1295
|
+
{
|
|
1296
|
+
id: 'claude-fable-5',
|
|
1297
|
+
ownedBy: 'anthropic',
|
|
1298
|
+
label: 'Claude Fable 5',
|
|
1299
|
+
modalities: {
|
|
1300
|
+
input: ['text', 'image', 'pdf'],
|
|
1301
|
+
output: ['text'],
|
|
1302
|
+
},
|
|
1303
|
+
toolCall: true,
|
|
1304
|
+
reasoningText: true,
|
|
1305
|
+
attachment: true,
|
|
1306
|
+
temperature: false,
|
|
1307
|
+
releaseDate: '2026-06-09',
|
|
1308
|
+
lastUpdated: '2026-06-09',
|
|
1309
|
+
openWeights: false,
|
|
1310
|
+
cost: {
|
|
1311
|
+
input: 10,
|
|
1312
|
+
output: 50,
|
|
1313
|
+
cacheRead: 1,
|
|
1314
|
+
cacheWrite: 12.5,
|
|
1315
|
+
},
|
|
1316
|
+
limit: {
|
|
1317
|
+
context: 1000000,
|
|
1318
|
+
output: 128000,
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1295
1321
|
{
|
|
1296
1322
|
id: 'claude-haiku-4-5',
|
|
1297
1323
|
ownedBy: 'anthropic',
|
|
@@ -2291,13 +2317,13 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
2291
2317
|
lastUpdated: '2026-04-27',
|
|
2292
2318
|
openWeights: false,
|
|
2293
2319
|
cost: {
|
|
2294
|
-
input: 0.
|
|
2295
|
-
output: 3.
|
|
2296
|
-
cacheRead: 0.
|
|
2320
|
+
input: 0.68,
|
|
2321
|
+
output: 3.41,
|
|
2322
|
+
cacheRead: 0.34,
|
|
2297
2323
|
},
|
|
2298
2324
|
limit: {
|
|
2299
|
-
context:
|
|
2300
|
-
output:
|
|
2325
|
+
context: 262142,
|
|
2326
|
+
output: 262142,
|
|
2301
2327
|
},
|
|
2302
2328
|
},
|
|
2303
2329
|
{
|
|
@@ -3717,8 +3743,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
3717
3743
|
lastUpdated: '2025-03-13',
|
|
3718
3744
|
openWeights: true,
|
|
3719
3745
|
cost: {
|
|
3720
|
-
input: 0.
|
|
3721
|
-
output: 0.
|
|
3746
|
+
input: 0.05,
|
|
3747
|
+
output: 0.15,
|
|
3722
3748
|
},
|
|
3723
3749
|
limit: {
|
|
3724
3750
|
context: 131072,
|
|
@@ -4127,7 +4153,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
4127
4153
|
lastUpdated: '2025-04-05',
|
|
4128
4154
|
openWeights: true,
|
|
4129
4155
|
cost: {
|
|
4130
|
-
input: 0.
|
|
4156
|
+
input: 0.1,
|
|
4131
4157
|
output: 0.3,
|
|
4132
4158
|
},
|
|
4133
4159
|
limit: {
|
|
@@ -4818,13 +4844,13 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
4818
4844
|
lastUpdated: '2026-04-21',
|
|
4819
4845
|
openWeights: true,
|
|
4820
4846
|
cost: {
|
|
4821
|
-
input: 0.
|
|
4822
|
-
output: 3.
|
|
4823
|
-
cacheRead: 0.
|
|
4847
|
+
input: 0.68,
|
|
4848
|
+
output: 3.41,
|
|
4849
|
+
cacheRead: 0.34,
|
|
4824
4850
|
},
|
|
4825
4851
|
limit: {
|
|
4826
|
-
context:
|
|
4827
|
-
output:
|
|
4852
|
+
context: 262142,
|
|
4853
|
+
output: 262142,
|
|
4828
4854
|
},
|
|
4829
4855
|
},
|
|
4830
4856
|
{
|
|
@@ -4853,26 +4879,26 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
4853
4879
|
},
|
|
4854
4880
|
},
|
|
4855
4881
|
{
|
|
4856
|
-
id: 'nex-agi/
|
|
4857
|
-
label: '
|
|
4882
|
+
id: 'nex-agi/nex-n2-pro:free',
|
|
4883
|
+
label: 'Nex-N2-Pro (free)',
|
|
4858
4884
|
modalities: {
|
|
4859
|
-
input: ['text'],
|
|
4885
|
+
input: ['text', 'image'],
|
|
4860
4886
|
output: ['text'],
|
|
4861
4887
|
},
|
|
4862
4888
|
toolCall: true,
|
|
4863
|
-
reasoningText:
|
|
4864
|
-
attachment:
|
|
4889
|
+
reasoningText: true,
|
|
4890
|
+
attachment: true,
|
|
4865
4891
|
temperature: true,
|
|
4866
|
-
releaseDate: '
|
|
4867
|
-
lastUpdated: '
|
|
4892
|
+
releaseDate: '2026-06-08',
|
|
4893
|
+
lastUpdated: '2026-06-08',
|
|
4868
4894
|
openWeights: true,
|
|
4869
4895
|
cost: {
|
|
4870
|
-
input: 0
|
|
4871
|
-
output: 0
|
|
4896
|
+
input: 0,
|
|
4897
|
+
output: 0,
|
|
4872
4898
|
},
|
|
4873
4899
|
limit: {
|
|
4874
|
-
context:
|
|
4875
|
-
output:
|
|
4900
|
+
context: 262144,
|
|
4901
|
+
output: 262144,
|
|
4876
4902
|
},
|
|
4877
4903
|
},
|
|
4878
4904
|
{
|
|
@@ -4890,7 +4916,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
4890
4916
|
lastUpdated: '2025-07-25',
|
|
4891
4917
|
openWeights: true,
|
|
4892
4918
|
cost: {
|
|
4893
|
-
input: 0.
|
|
4919
|
+
input: 0.4,
|
|
4894
4920
|
output: 0.4,
|
|
4895
4921
|
},
|
|
4896
4922
|
limit: {
|
|
@@ -6808,7 +6834,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
6808
6834
|
lastUpdated: '2025-07-21',
|
|
6809
6835
|
openWeights: true,
|
|
6810
6836
|
cost: {
|
|
6811
|
-
input: 0.
|
|
6837
|
+
input: 0.09,
|
|
6812
6838
|
output: 0.1,
|
|
6813
6839
|
},
|
|
6814
6840
|
limit: {
|
|
@@ -6857,8 +6883,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
6857
6883
|
lastUpdated: '2025-04-28',
|
|
6858
6884
|
openWeights: true,
|
|
6859
6885
|
cost: {
|
|
6860
|
-
input: 0.
|
|
6861
|
-
output: 0.
|
|
6886
|
+
input: 0.12,
|
|
6887
|
+
output: 0.5,
|
|
6862
6888
|
},
|
|
6863
6889
|
limit: {
|
|
6864
6890
|
context: 40960,
|
|
@@ -7506,12 +7532,12 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
7506
7532
|
lastUpdated: '2026-03-10',
|
|
7507
7533
|
openWeights: true,
|
|
7508
7534
|
cost: {
|
|
7509
|
-
input: 0.
|
|
7535
|
+
input: 0.1,
|
|
7510
7536
|
output: 0.15,
|
|
7511
7537
|
},
|
|
7512
7538
|
limit: {
|
|
7513
7539
|
context: 262144,
|
|
7514
|
-
output:
|
|
7540
|
+
output: 262144,
|
|
7515
7541
|
},
|
|
7516
7542
|
},
|
|
7517
7543
|
{
|
|
@@ -7862,9 +7888,10 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
7862
7888
|
reasoningText: true,
|
|
7863
7889
|
attachment: true,
|
|
7864
7890
|
temperature: true,
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7891
|
+
knowledge: '2026-01-01',
|
|
7892
|
+
releaseDate: '2026-05-29',
|
|
7893
|
+
lastUpdated: '2026-05-29',
|
|
7894
|
+
openWeights: true,
|
|
7868
7895
|
cost: {
|
|
7869
7896
|
input: 0.2,
|
|
7870
7897
|
output: 1.15,
|
|
@@ -10149,6 +10176,30 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
10149
10176
|
output: 128000,
|
|
10150
10177
|
},
|
|
10151
10178
|
},
|
|
10179
|
+
{
|
|
10180
|
+
id: 'north-mini-code-free',
|
|
10181
|
+
label: 'North Mini Code Free',
|
|
10182
|
+
modalities: {
|
|
10183
|
+
input: ['text'],
|
|
10184
|
+
output: ['text'],
|
|
10185
|
+
},
|
|
10186
|
+
toolCall: true,
|
|
10187
|
+
reasoningText: true,
|
|
10188
|
+
attachment: false,
|
|
10189
|
+
temperature: true,
|
|
10190
|
+
knowledge: '2025-09-23',
|
|
10191
|
+
releaseDate: '2026-06-09',
|
|
10192
|
+
lastUpdated: '2026-06-09',
|
|
10193
|
+
openWeights: true,
|
|
10194
|
+
cost: {
|
|
10195
|
+
input: 0,
|
|
10196
|
+
output: 0,
|
|
10197
|
+
},
|
|
10198
|
+
limit: {
|
|
10199
|
+
context: 256000,
|
|
10200
|
+
output: 64000,
|
|
10201
|
+
},
|
|
10202
|
+
},
|
|
10152
10203
|
{
|
|
10153
10204
|
id: 'qwen3-coder',
|
|
10154
10205
|
label: 'Qwen3 Coder',
|
|
@@ -10338,7 +10389,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
10338
10389
|
cacheRead: 0.2,
|
|
10339
10390
|
},
|
|
10340
10391
|
limit: {
|
|
10341
|
-
context:
|
|
10392
|
+
context: 1000000,
|
|
10342
10393
|
output: 30000,
|
|
10343
10394
|
},
|
|
10344
10395
|
},
|
|
@@ -10363,7 +10414,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
10363
10414
|
cacheRead: 0.2,
|
|
10364
10415
|
},
|
|
10365
10416
|
limit: {
|
|
10366
|
-
context:
|
|
10417
|
+
context: 1000000,
|
|
10367
10418
|
output: 30000,
|
|
10368
10419
|
},
|
|
10369
10420
|
},
|
|
@@ -12255,7 +12306,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
12255
12306
|
output: ['text'],
|
|
12256
12307
|
},
|
|
12257
12308
|
toolCall: true,
|
|
12258
|
-
reasoningText:
|
|
12309
|
+
reasoningText: true,
|
|
12259
12310
|
attachment: false,
|
|
12260
12311
|
releaseDate: '2025-10-23',
|
|
12261
12312
|
lastUpdated: '2026-01-19',
|
|
@@ -12333,7 +12384,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
12333
12384
|
},
|
|
12334
12385
|
toolCall: true,
|
|
12335
12386
|
reasoningText: true,
|
|
12336
|
-
attachment:
|
|
12387
|
+
attachment: true,
|
|
12337
12388
|
temperature: true,
|
|
12338
12389
|
knowledge: '2025-01',
|
|
12339
12390
|
releaseDate: '2026-05-31',
|
|
@@ -12454,6 +12505,25 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
|
|
|
12454
12505
|
output: 65536,
|
|
12455
12506
|
},
|
|
12456
12507
|
},
|
|
12508
|
+
{
|
|
12509
|
+
id: 'nemotron-3-ultra',
|
|
12510
|
+
label: 'nemotron-3-ultra',
|
|
12511
|
+
modalities: {
|
|
12512
|
+
input: ['text'],
|
|
12513
|
+
output: ['text'],
|
|
12514
|
+
},
|
|
12515
|
+
toolCall: true,
|
|
12516
|
+
reasoningText: true,
|
|
12517
|
+
attachment: false,
|
|
12518
|
+
temperature: true,
|
|
12519
|
+
releaseDate: '2026-06-04',
|
|
12520
|
+
lastUpdated: '2026-06-04',
|
|
12521
|
+
openWeights: true,
|
|
12522
|
+
limit: {
|
|
12523
|
+
context: 262144,
|
|
12524
|
+
output: 128000,
|
|
12525
|
+
},
|
|
12526
|
+
},
|
|
12457
12527
|
{
|
|
12458
12528
|
id: 'qwen3-coder-next',
|
|
12459
12529
|
label: 'qwen3-coder-next',
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { tool, type Tool } from 'ai';
|
|
2
|
-
import { z } from 'zod/v3';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import DESCRIPTION from './ripgrep.txt' with { type: 'text' };
|
|
6
|
-
import { createToolError, type ToolResponse } from '../error.ts';
|
|
7
|
-
import { resolveBinary } from '../bin-manager.ts';
|
|
8
|
-
|
|
9
|
-
export function buildRipgrepTool(projectRoot: string): {
|
|
10
|
-
name: string;
|
|
11
|
-
tool: Tool;
|
|
12
|
-
} {
|
|
13
|
-
const rg = tool({
|
|
14
|
-
description: DESCRIPTION,
|
|
15
|
-
inputSchema: z.object({
|
|
16
|
-
query: z.string().min(1).describe('Search pattern (regex by default)'),
|
|
17
|
-
path: z
|
|
18
|
-
.string()
|
|
19
|
-
.optional()
|
|
20
|
-
.default('.')
|
|
21
|
-
.describe('Relative path to search in'),
|
|
22
|
-
ignoreCase: z.boolean().optional().default(false),
|
|
23
|
-
glob: z
|
|
24
|
-
.array(z.string())
|
|
25
|
-
.optional()
|
|
26
|
-
.describe('One or more glob patterns to include'),
|
|
27
|
-
maxResults: z.number().int().min(1).max(5000).optional().default(100),
|
|
28
|
-
}),
|
|
29
|
-
async execute({
|
|
30
|
-
query,
|
|
31
|
-
path = '.',
|
|
32
|
-
ignoreCase,
|
|
33
|
-
glob,
|
|
34
|
-
maxResults = 100,
|
|
35
|
-
}: {
|
|
36
|
-
query: string;
|
|
37
|
-
path?: string;
|
|
38
|
-
ignoreCase?: boolean;
|
|
39
|
-
glob?: string[];
|
|
40
|
-
maxResults?: number;
|
|
41
|
-
}): Promise<
|
|
42
|
-
ToolResponse<{
|
|
43
|
-
count: number;
|
|
44
|
-
matches: Array<{ file: string; line: number; text: string }>;
|
|
45
|
-
truncated?: boolean;
|
|
46
|
-
shownMatches?: number;
|
|
47
|
-
files?: Array<{ file: string; matches: number }>;
|
|
48
|
-
}>
|
|
49
|
-
> {
|
|
50
|
-
function expandTilde(p: string) {
|
|
51
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
52
|
-
if (!home) return p;
|
|
53
|
-
if (p === '~') return home;
|
|
54
|
-
if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
|
|
55
|
-
return p;
|
|
56
|
-
}
|
|
57
|
-
const p = expandTilde(String(path ?? '.')).trim();
|
|
58
|
-
const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
|
|
59
|
-
const target = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
|
|
60
|
-
const args = [
|
|
61
|
-
'--no-heading',
|
|
62
|
-
'--line-number',
|
|
63
|
-
'--color=never',
|
|
64
|
-
'--max-columns',
|
|
65
|
-
'240',
|
|
66
|
-
'--max-columns-preview',
|
|
67
|
-
];
|
|
68
|
-
if (ignoreCase) args.push('-i');
|
|
69
|
-
if (Array.isArray(glob)) for (const g of glob) args.push('-g', g);
|
|
70
|
-
args.push('--max-count', String(maxResults));
|
|
71
|
-
args.push(query);
|
|
72
|
-
args.push(target);
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const rgBin = await resolveBinary('rg');
|
|
76
|
-
return await new Promise((resolve) => {
|
|
77
|
-
const proc = spawn(rgBin, args, { cwd: projectRoot });
|
|
78
|
-
let stderr = '';
|
|
79
|
-
let pendingLine = '';
|
|
80
|
-
let truncated = false;
|
|
81
|
-
let settled = false;
|
|
82
|
-
const TEXT_MAX = 200;
|
|
83
|
-
const matches: Array<{ file: string; line: number; text: string }> =
|
|
84
|
-
[];
|
|
85
|
-
const fileCounts = new Map<string, number>();
|
|
86
|
-
|
|
87
|
-
const parseLine = (lineText: string) => {
|
|
88
|
-
if (!lineText || matches.length >= maxResults) return;
|
|
89
|
-
const m = lineText.match(/^(.+?):(\d+):(.*)$/s);
|
|
90
|
-
const match = (() => {
|
|
91
|
-
if (!m) {
|
|
92
|
-
return {
|
|
93
|
-
file: '',
|
|
94
|
-
line: 0,
|
|
95
|
-
text:
|
|
96
|
-
lineText.length > TEXT_MAX
|
|
97
|
-
? `${lineText.slice(0, TEXT_MAX)}…`
|
|
98
|
-
: lineText,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const file = m[1];
|
|
102
|
-
const line = Number.parseInt(m[2], 10);
|
|
103
|
-
const raw = m[3];
|
|
104
|
-
const text =
|
|
105
|
-
raw.length > TEXT_MAX ? `${raw.slice(0, TEXT_MAX)}…` : raw;
|
|
106
|
-
return { file, line, text };
|
|
107
|
-
})();
|
|
108
|
-
matches.push(match);
|
|
109
|
-
if (match.file) {
|
|
110
|
-
fileCounts.set(match.file, (fileCounts.get(match.file) ?? 0) + 1);
|
|
111
|
-
}
|
|
112
|
-
if (matches.length >= maxResults) {
|
|
113
|
-
truncated = true;
|
|
114
|
-
proc.kill('SIGTERM');
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const resolveSuccess = () => {
|
|
119
|
-
if (settled) return;
|
|
120
|
-
settled = true;
|
|
121
|
-
const files = Array.from(fileCounts.entries()).map(
|
|
122
|
-
([file, count]) => ({ file, matches: count }),
|
|
123
|
-
);
|
|
124
|
-
resolve({
|
|
125
|
-
ok: true,
|
|
126
|
-
count: matches.length,
|
|
127
|
-
matches,
|
|
128
|
-
...(truncated
|
|
129
|
-
? { truncated: true, shownMatches: matches.length }
|
|
130
|
-
: {}),
|
|
131
|
-
...(files.length ? { files } : {}),
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
proc.stdout.on('data', (data) => {
|
|
136
|
-
if (matches.length >= maxResults) return;
|
|
137
|
-
pendingLine += data.toString();
|
|
138
|
-
const lines = pendingLine.split('\n');
|
|
139
|
-
pendingLine = lines.pop() ?? '';
|
|
140
|
-
for (const line of lines) {
|
|
141
|
-
parseLine(line);
|
|
142
|
-
if (matches.length >= maxResults) break;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
proc.stderr.on('data', (data) => {
|
|
147
|
-
stderr += data.toString();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
proc.on('close', (code) => {
|
|
151
|
-
if (pendingLine && matches.length < maxResults)
|
|
152
|
-
parseLine(pendingLine);
|
|
153
|
-
if (!truncated && code !== 0 && code !== 1) {
|
|
154
|
-
resolve(
|
|
155
|
-
createToolError(
|
|
156
|
-
stderr.trim() || 'ripgrep failed',
|
|
157
|
-
'execution',
|
|
158
|
-
{
|
|
159
|
-
suggestion:
|
|
160
|
-
'Check if ripgrep (rg) is installed and the query is valid',
|
|
161
|
-
},
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
resolveSuccess();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
proc.on('error', (err) => {
|
|
171
|
-
if (settled) return;
|
|
172
|
-
settled = true;
|
|
173
|
-
resolve(
|
|
174
|
-
createToolError(String(err), 'execution', {
|
|
175
|
-
suggestion: 'Ensure ripgrep (rg) is installed',
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
} catch (err) {
|
|
181
|
-
return createToolError(String(err), 'execution', {
|
|
182
|
-
suggestion: 'Ensure ripgrep (rg) is installed',
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
return { name: 'ripgrep', tool: rg };
|
|
188
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
- Search files using ripgrep (rg) with regex patterns
|
|
2
|
-
- Returns a flat list of matches with `file`, `line`, and `text`
|
|
3
|
-
- Supports include globs and case-insensitive search
|
|
4
|
-
- Respects `.gitignore` by default
|
|
5
|
-
|
|
6
|
-
This is the only content-search tool available. Use it for any text search across the codebase.
|
|
7
|
-
|
|
8
|
-
## Usage tips
|
|
9
|
-
|
|
10
|
-
- Narrow the search set with `glob` first if the pattern may match too broadly.
|
|
11
|
-
- Prefer narrow `path` and `glob` values over repo-wide searches.
|
|
12
|
-
- Keep `maxResults` low for broad searches; the tool stops after the global limit.
|
|
13
|
-
- Batch independent searches (e.g. multiple function names) in a single turn for parallel execution.
|
|
14
|
-
- Use `ignoreCase: true` for case-insensitive matching; pass `glob` patterns (e.g. `["*.ts"]`) to limit file types.
|