@ottocode/sdk 0.1.301 → 0.1.303
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 +200 -206
- 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.303",
|
|
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.
|