agen-vektor 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/agent/agent.js +90 -0
  4. package/dist/agent/context.js +106 -0
  5. package/dist/agent/loop.js +152 -0
  6. package/dist/agent/memory.js +87 -0
  7. package/dist/agent/planner.js +38 -0
  8. package/dist/agent/prompts.js +41 -0
  9. package/dist/agent/session.js +125 -0
  10. package/dist/cli/index.js +304 -0
  11. package/dist/cli/keyboard.js +283 -0
  12. package/dist/config/config.js +92 -0
  13. package/dist/config/credentials.js +127 -0
  14. package/dist/config/models.js +47 -0
  15. package/dist/providers/anthropic.js +276 -0
  16. package/dist/providers/custom.js +24 -0
  17. package/dist/providers/factory.js +58 -0
  18. package/dist/providers/gemini.js +223 -0
  19. package/dist/providers/ollama.js +195 -0
  20. package/dist/providers/openai-compat.js +296 -0
  21. package/dist/providers/openai.js +17 -0
  22. package/dist/providers/openrouter.js +23 -0
  23. package/dist/providers/provider.js +39 -0
  24. package/dist/security/command-policy.js +99 -0
  25. package/dist/security/permissions.js +66 -0
  26. package/dist/tools/filesystem.js +297 -0
  27. package/dist/tools/git.js +66 -0
  28. package/dist/tools/registry.js +39 -0
  29. package/dist/tools/search.js +184 -0
  30. package/dist/tools/shell.js +108 -0
  31. package/dist/tools/web.js +76 -0
  32. package/dist/tui/app.js +913 -0
  33. package/dist/tui/chat.js +76 -0
  34. package/dist/tui/components.js +85 -0
  35. package/dist/tui/diff.js +105 -0
  36. package/dist/tui/input.js +121 -0
  37. package/dist/tui/statusbar.js +32 -0
  38. package/dist/utils/logger.js +96 -0
  39. package/dist/utils/paths.js +90 -0
  40. package/dist/utils/terminal.js +191 -0
  41. package/package.json +56 -0
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PermissionManager = void 0;
4
+ /**
5
+ * Permission manager — gates tool execution behind user consent.
6
+ */
7
+ const command_policy_1 = require("./command-policy");
8
+ /**
9
+ * Session-scoped permission cache: remember "always allow" decisions
10
+ * for the current run only. Persisted rules are out of scope deliberately.
11
+ */
12
+ class PermissionManager {
13
+ mode;
14
+ ask;
15
+ rememberedAllow = new Set();
16
+ constructor(opts) {
17
+ this.mode = opts.mode;
18
+ this.ask = opts.ask;
19
+ }
20
+ setMode(mode) {
21
+ this.mode = mode;
22
+ }
23
+ getMode() {
24
+ return this.mode;
25
+ }
26
+ static key(tool, summary) {
27
+ return `${tool}::${summary}`;
28
+ }
29
+ /**
30
+ * Decide whether a tool action is allowed.
31
+ *
32
+ * - blocked → always refused
33
+ * - safe → always allowed
34
+ * - ask → allowed in yolo mode; otherwise prompt
35
+ * - dangerous → prompt even in yolo mode (yolo only auto-allows 'ask')
36
+ */
37
+ async decide(req) {
38
+ if (req.level === 'blocked')
39
+ return false;
40
+ if (req.level === 'safe')
41
+ return true;
42
+ const k = PermissionManager.key(req.tool, req.summary);
43
+ if (this.rememberedAllow.has(k))
44
+ return true;
45
+ if (this.mode === 'yolo') {
46
+ if (req.level === 'ask')
47
+ return true;
48
+ // dangerous: still prompt in yolo mode
49
+ }
50
+ if (!this.ask) {
51
+ // Non-interactive: default deny for anything requiring a prompt.
52
+ // --yolo in non-interactive mode still allows 'ask' (handled above).
53
+ return false;
54
+ }
55
+ const decision = await this.ask(req);
56
+ if (decision.allow && decision.remember) {
57
+ this.rememberedAllow.add(k);
58
+ }
59
+ return decision.allow;
60
+ }
61
+ /** Classify a shell command through the command policy. */
62
+ classifyCommand(command) {
63
+ return (0, command_policy_1.classifyCommand)(command).level;
64
+ }
65
+ }
66
+ exports.PermissionManager = PermissionManager;
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createFilesystemTools = createFilesystemTools;
37
+ /**
38
+ * Filesystem tools — read, write, edit, list, delete.
39
+ * All paths are resolved relative to the working directory.
40
+ */
41
+ const fs = __importStar(require("node:fs"));
42
+ const fsp = __importStar(require("node:fs/promises"));
43
+ const path = __importStar(require("node:path"));
44
+ function resolve(cwd, p) {
45
+ return path.resolve(cwd, p);
46
+ }
47
+ function safeRelative(cwd, abs) {
48
+ return path.relative(cwd, abs) || '.';
49
+ }
50
+ function err(msg) {
51
+ return { output: `ERROR: ${msg}` };
52
+ }
53
+ function truncateOutput(text, max) {
54
+ if (text.length <= max)
55
+ return text;
56
+ return text.slice(0, max) + `\n... [truncated ${text.length - max} chars]`;
57
+ }
58
+ function createFilesystemTools(maxDefaultOutput = 30_000) {
59
+ return [
60
+ {
61
+ definition: {
62
+ name: 'read_file',
63
+ description: 'Read the contents of a file. Provide a relative path. Optionally set offset (1-based line) and limit (number of lines). Returns file content with line numbers.',
64
+ parameters: {
65
+ type: 'object',
66
+ properties: {
67
+ path: { type: 'string', description: 'Path to the file, relative to project root' },
68
+ offset: { type: 'number', description: 'Line number to start from (1-based)' },
69
+ limit: { type: 'number', description: 'Max lines to read' },
70
+ },
71
+ required: ['path'],
72
+ },
73
+ },
74
+ async execute(args, ctx) {
75
+ const p = resolve(ctx.cwd, String(args.path || ''));
76
+ try {
77
+ const stat = await fsp.stat(p);
78
+ if (stat.isDirectory()) {
79
+ return err(`"${safeRelative(ctx.cwd, p)}" is a directory`);
80
+ }
81
+ const content = await fsp.readFile(p, 'utf8');
82
+ let lines = content.split('\n');
83
+ // A trailing newline produces an empty last element; don't count it.
84
+ if (content.endsWith('\n'))
85
+ lines = lines.slice(0, -1);
86
+ const offset = Math.max(1, Number(args.offset) || 1);
87
+ const limit = args.limit ? Math.max(1, Number(args.limit)) : lines.length;
88
+ const slice = lines.slice(offset - 1, offset - 1 + limit);
89
+ const numbered = slice
90
+ .map((l, i) => `${String(offset + i).padStart(5)}| ${l}`)
91
+ .join('\n');
92
+ const total = lines.length;
93
+ const header = `file: ${safeRelative(ctx.cwd, p)} (${total} lines)`;
94
+ return {
95
+ output: truncateOutput(`${header}\n${numbered}`, ctx.maxOutput || maxDefaultOutput),
96
+ summary: `read ${safeRelative(ctx.cwd, p)}`,
97
+ data: { path: safeRelative(ctx.cwd, p), lines: slice },
98
+ };
99
+ }
100
+ catch (e) {
101
+ return err(`cannot read "${args.path}": ${e.message}`);
102
+ }
103
+ },
104
+ },
105
+ {
106
+ definition: {
107
+ name: 'write_file',
108
+ description: 'Create a new file or overwrite an existing file with the given content. Creates parent directories as needed. Use for new files or full rewrites; prefer edit_file for small changes.',
109
+ parameters: {
110
+ type: 'object',
111
+ properties: {
112
+ path: { type: 'string', description: 'Path to the file, relative to project root' },
113
+ content: { type: 'string', description: 'Full file content' },
114
+ },
115
+ required: ['path', 'content'],
116
+ },
117
+ },
118
+ async execute(args, ctx) {
119
+ const p = resolve(ctx.cwd, String(args.path || ''));
120
+ const content = String(args.content ?? '');
121
+ try {
122
+ await fsp.mkdir(path.dirname(p), { recursive: true });
123
+ await fsp.writeFile(p, content, 'utf8');
124
+ return {
125
+ output: `wrote ${content.length} bytes to ${safeRelative(ctx.cwd, p)}`,
126
+ summary: `wrote ${safeRelative(ctx.cwd, p)}`,
127
+ permission: 'ask',
128
+ };
129
+ }
130
+ catch (e) {
131
+ return err(`cannot write "${args.path}": ${e.message}`);
132
+ }
133
+ },
134
+ },
135
+ {
136
+ definition: {
137
+ name: 'edit_file',
138
+ description: 'Edit a file by replacing an exact string with a new string. The old string must match exactly (including whitespace). Use for small targeted changes.',
139
+ parameters: {
140
+ type: 'object',
141
+ properties: {
142
+ path: { type: 'string', description: 'Path to the file, relative to project root' },
143
+ old_string: { type: 'string', description: 'Exact text to find and replace' },
144
+ new_string: { type: 'string', description: 'Replacement text' },
145
+ replace_all: { type: 'boolean', description: 'Replace all occurrences (default false)' },
146
+ },
147
+ required: ['path', 'old_string', 'new_string'],
148
+ },
149
+ },
150
+ async execute(args, ctx) {
151
+ const p = resolve(ctx.cwd, String(args.path || ''));
152
+ const oldStr = String(args.old_string ?? '');
153
+ const newStr = String(args.new_string ?? '');
154
+ if (!oldStr)
155
+ return err('old_string must not be empty');
156
+ try {
157
+ const content = await fsp.readFile(p, 'utf8');
158
+ if (args.replace_all) {
159
+ if (!content.includes(oldStr)) {
160
+ return err(`old_string not found in ${safeRelative(ctx.cwd, p)}`);
161
+ }
162
+ const updated = content.split(oldStr).join(newStr);
163
+ await fsp.writeFile(p, updated, 'utf8');
164
+ return {
165
+ output: `replaced all occurrences in ${safeRelative(ctx.cwd, p)}`,
166
+ summary: `edited ${safeRelative(ctx.cwd, p)}`,
167
+ permission: 'ask',
168
+ };
169
+ }
170
+ const idx = content.indexOf(oldStr);
171
+ if (idx === -1) {
172
+ return err(`old_string not found in ${safeRelative(ctx.cwd, p)}. The string must match EXACTLY including whitespace.`);
173
+ }
174
+ const updated = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length);
175
+ await fsp.writeFile(p, updated, 'utf8');
176
+ return {
177
+ output: `edited ${safeRelative(ctx.cwd, p)} (1 replacement)`,
178
+ summary: `edited ${safeRelative(ctx.cwd, p)}`,
179
+ permission: 'ask',
180
+ };
181
+ }
182
+ catch (e) {
183
+ return err(`cannot edit "${args.path}": ${e.message}`);
184
+ }
185
+ },
186
+ },
187
+ {
188
+ definition: {
189
+ name: 'list_directory',
190
+ description: 'List files and directories in a path. Returns entries with type and size. Respects ignore rules.',
191
+ parameters: {
192
+ type: 'object',
193
+ properties: {
194
+ path: { type: 'string', description: 'Directory path, relative to project root (default ".")' },
195
+ depth: { type: 'number', description: 'Max recursion depth (default 1)' },
196
+ },
197
+ required: [],
198
+ },
199
+ },
200
+ async execute(args, ctx) {
201
+ const p = resolve(ctx.cwd, String(args.path || '.'));
202
+ const depth = Math.min(3, Math.max(0, Number(args.depth) || 1));
203
+ const IGNORE = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', '.cache', '.env', '.next', 'target']);
204
+ const lines = [];
205
+ const walk = (dir, level) => {
206
+ let entries;
207
+ try {
208
+ entries = fs.readdirSync(dir, { withFileTypes: true });
209
+ }
210
+ catch {
211
+ return;
212
+ }
213
+ entries.sort((a, b) => {
214
+ if (a.isDirectory() !== b.isDirectory())
215
+ return a.isDirectory() ? -1 : 1;
216
+ return a.name.localeCompare(b.name);
217
+ });
218
+ for (const e of entries) {
219
+ if (e.name.startsWith('.') && e.name !== '.env.example')
220
+ continue;
221
+ if (IGNORE.has(e.name))
222
+ continue;
223
+ const rel = safeRelative(ctx.cwd, path.join(dir, e.name));
224
+ if (e.isDirectory()) {
225
+ lines.push(`${' '.repeat(level)}📁 ${rel}/`);
226
+ if (level < depth)
227
+ walk(path.join(dir, e.name), level + 1);
228
+ }
229
+ else {
230
+ let size = '';
231
+ try {
232
+ size = fmtSize(fs.statSync(path.join(dir, e.name)).size);
233
+ }
234
+ catch {
235
+ /* ignore */
236
+ }
237
+ lines.push(`${' '.repeat(level)}📄 ${rel} (${size})`);
238
+ }
239
+ if (lines.length > 500)
240
+ return;
241
+ }
242
+ };
243
+ walk(p, 0);
244
+ if (lines.length === 0) {
245
+ return { output: `(empty or no visible entries in ${safeRelative(ctx.cwd, p)})`, summary: 'list directory' };
246
+ }
247
+ return {
248
+ output: truncateOutput(`listing ${safeRelative(ctx.cwd, p)}:\n${lines.join('\n')}`, ctx.maxOutput || maxDefaultOutput),
249
+ summary: `listed ${safeRelative(ctx.cwd, p)}`,
250
+ };
251
+ },
252
+ },
253
+ {
254
+ definition: {
255
+ name: 'delete_file',
256
+ description: 'Delete a file (or empty directory). Requires explicit permission. Never deletes outside the project or the project root itself.',
257
+ parameters: {
258
+ type: 'object',
259
+ properties: {
260
+ path: { type: 'string', description: 'Path to the file/directory to delete, relative to project root' },
261
+ },
262
+ required: ['path'],
263
+ },
264
+ },
265
+ async execute(args, ctx) {
266
+ const p = resolve(ctx.cwd, String(args.path || ''));
267
+ const rel = safeRelative(ctx.cwd, p);
268
+ if (rel === '.' || rel === '')
269
+ return err('refusing to delete project root');
270
+ try {
271
+ const stat = await fsp.stat(p);
272
+ if (stat.isDirectory()) {
273
+ await fsp.rmdir(p);
274
+ }
275
+ else {
276
+ await fsp.unlink(p);
277
+ }
278
+ return {
279
+ output: `deleted ${rel}`,
280
+ summary: `deleted ${rel}`,
281
+ permission: 'dangerous',
282
+ };
283
+ }
284
+ catch (e) {
285
+ return err(`cannot delete "${args.path}": ${e.message}`);
286
+ }
287
+ },
288
+ },
289
+ ];
290
+ }
291
+ function fmtSize(bytes) {
292
+ if (bytes < 1024)
293
+ return `${bytes}B`;
294
+ if (bytes < 1024 * 1024)
295
+ return `${(bytes / 1024).toFixed(1)}K`;
296
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
297
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGitTool = createGitTool;
4
+ /**
5
+ * Git tool — read-only git operations by default.
6
+ * `commit` and `push` require explicit user permission (push is blocked by policy).
7
+ */
8
+ const shell_1 = require("./shell");
9
+ const ALLOWED_SUBCOMMANDS = new Set([
10
+ 'status', 'diff', 'log', 'branch', 'show', 'remote', 'rev-parse',
11
+ 'config', 'ls-files', 'check-ignore', 'shortlog', 'blame', 'grep',
12
+ 'stash', 'tag', 'fetch', 'pull',
13
+ ]);
14
+ const PERMISSION_NEEDED = new Set(['commit', 'add', 'reset', 'checkout', 'clean', 'rebase', 'merge', 'cherry-pick', 'switch', 'restore', 'push', 'init', 'mv', 'rm']);
15
+ function classifyGit(args) {
16
+ const sub = args[0] || '';
17
+ if (sub === 'push')
18
+ return 'blocked';
19
+ if (ALLOWED_SUBCOMMANDS.has(sub))
20
+ return 'safe';
21
+ if (PERMISSION_NEEDED.has(sub))
22
+ return 'ask';
23
+ return 'ask';
24
+ }
25
+ function createGitTool() {
26
+ return {
27
+ definition: {
28
+ name: 'git',
29
+ description: 'Run a git operation: status, diff, log, branch, show, remote, rev-parse, config, ls-files, add, commit, etc. Read-only commands are safe; state-changing commands (add/commit/checkout/reset/etc.) require permission. push is blocked.',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {
33
+ args: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ description: 'Git arguments, e.g. ["status"], ["diff"], ["log","--oneline","-5"], ["add","."]',
37
+ },
38
+ },
39
+ required: ['args'],
40
+ },
41
+ },
42
+ async execute(args, ctx) {
43
+ const gitArgs = Array.isArray(args.args) ? args.args.map(String) : [String(args.args || '')];
44
+ if (gitArgs.length === 0 || gitArgs[0] === '') {
45
+ return { output: 'ERROR: git requires args, e.g. ["status"]' };
46
+ }
47
+ const cmd = ['git', ...gitArgs].join(' ');
48
+ ctx.onActivity?.('git', `$ ${cmd}`);
49
+ const level = classifyGit(gitArgs);
50
+ if (level === 'blocked') {
51
+ return { output: `BLOCKED: "git push" is not allowed — VECTOR never pushes without explicit user approval outside this policy.` };
52
+ }
53
+ const allowed = await ctx.permissions.decide({ tool: 'git', summary: cmd, level });
54
+ if (!allowed) {
55
+ return { output: `Permission denied by user: ${cmd}` };
56
+ }
57
+ const res = await (0, shell_1.runShell)(cmd, ctx.cwd, { timeoutMs: 60_000 });
58
+ const out = [res.stdout, res.stderr].filter(Boolean).join('\n[stderr]\n');
59
+ return {
60
+ output: `${res.exitCode === 0 ? 'exit 0' : `exit ${res.exitCode ?? '?'}`}${out ? '\n' + out : ''}`,
61
+ summary: `git ${gitArgs.join(' ')}`,
62
+ data: { exitCode: res.exitCode },
63
+ };
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ToolRegistry = void 0;
4
+ class ToolRegistry {
5
+ tools = new Map();
6
+ register(tool) {
7
+ this.tools.set(tool.definition.name, tool);
8
+ }
9
+ registerMany(tools) {
10
+ for (const t of tools)
11
+ this.register(t);
12
+ }
13
+ get(name) {
14
+ return this.tools.get(name);
15
+ }
16
+ has(name) {
17
+ return this.tools.has(name);
18
+ }
19
+ definitions() {
20
+ return [...this.tools.values()].map((t) => t.definition);
21
+ }
22
+ async execute(name, rawArgs, ctx) {
23
+ const tool = this.tools.get(name);
24
+ if (!tool) {
25
+ return { output: `ERROR: unknown tool "${name}"` };
26
+ }
27
+ let args;
28
+ try {
29
+ args = rawArgs ? JSON.parse(rawArgs) : {};
30
+ }
31
+ catch {
32
+ return { output: `ERROR: invalid JSON arguments for tool "${name}": ${rawArgs.slice(0, 200)}` };
33
+ }
34
+ ctx.onActivity?.(name, `executing ${name}`);
35
+ const result = await tool.execute(args, ctx);
36
+ return result;
37
+ }
38
+ }
39
+ exports.ToolRegistry = ToolRegistry;
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createSearchTools = createSearchTools;
37
+ /**
38
+ * Search tools — search files by name or content.
39
+ * Implementation uses ripgrep (rg) when available, else a pure-Node
40
+ * recursive walk with string matching (slow but dependency-free).
41
+ */
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ const node_util_1 = require("node:util");
46
+ const execFileP = (0, node_util_1.promisify)(node_child_process_1.execFile);
47
+ const IGNORE = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', '.cache', '.env', '.next', 'target', 'coverage']);
48
+ function hasRg() {
49
+ try {
50
+ execFileSyncCheck('rg', ['--version']);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function execFileSyncCheck(cmd, args) {
58
+ (0, node_child_process_1.spawnSync)(cmd, args, { stdio: 'ignore' });
59
+ }
60
+ function walkFiles(dir, out, max) {
61
+ let entries;
62
+ try {
63
+ entries = fs.readdirSync(dir, { withFileTypes: true });
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ for (const e of entries) {
69
+ if (out.length >= max)
70
+ return;
71
+ if (IGNORE.has(e.name))
72
+ continue;
73
+ if (e.name.startsWith('.'))
74
+ continue;
75
+ const full = path.join(dir, e.name);
76
+ if (e.isDirectory())
77
+ walkFiles(full, out, max);
78
+ else
79
+ out.push(full);
80
+ }
81
+ }
82
+ function createSearchTools() {
83
+ const rg = hasRg();
84
+ return [
85
+ {
86
+ definition: {
87
+ name: 'search_files',
88
+ description: 'Search for files by filename pattern or by text content. Returns up to 30 matches with file paths (and line numbers for text search).',
89
+ parameters: {
90
+ type: 'object',
91
+ properties: {
92
+ query: { type: 'string', description: 'Text to search for in file contents' },
93
+ filename: { type: 'string', description: 'Filename pattern (substring or glob-like), e.g. "login", "*.ts"' },
94
+ path: { type: 'string', description: 'Directory to search in, relative to project root (default ".")' },
95
+ max_results: { type: 'number', description: 'Max results (default 30, max 100)' },
96
+ },
97
+ required: [],
98
+ },
99
+ },
100
+ async execute(args, ctx) {
101
+ const base = path.resolve(ctx.cwd, String(args.path || '.'));
102
+ const max = Math.min(100, Math.max(1, Number(args.max_results) || 30));
103
+ const query = String(args.query || '');
104
+ const filename = String(args.filename || '');
105
+ if (!query && !filename)
106
+ return { output: 'ERROR: provide either "query" (text) or "filename" (pattern)' };
107
+ if (rg) {
108
+ return runRg(base, query, filename, max);
109
+ }
110
+ return fallbackSearch(base, query, filename, max, ctx.cwd);
111
+ },
112
+ },
113
+ ];
114
+ }
115
+ async function runRg(base, query, filename, max) {
116
+ try {
117
+ const args = ['--no-ignore', '-m', '1', '--max-count', '3'];
118
+ for (const ig of IGNORE)
119
+ args.push('-g', `!${ig}/**`);
120
+ if (filename) {
121
+ args.push('-g', filename.includes('*') ? filename : `*${filename}*`);
122
+ }
123
+ if (query) {
124
+ args.push('--', query, base);
125
+ }
126
+ else {
127
+ args.push('-l', '--', '', base);
128
+ }
129
+ const { stdout } = await execFileP('rg', args, { maxBuffer: 10 * 1024 * 1024 });
130
+ const lines = stdout.split('\n').filter(Boolean).slice(0, max);
131
+ if (lines.length === 0) {
132
+ return { output: 'no matches found', summary: 'search: no matches' };
133
+ }
134
+ return {
135
+ output: `found ${lines.length} match(es):\n${lines.join('\n')}`,
136
+ summary: `search found ${lines.length} match(es)`,
137
+ };
138
+ }
139
+ catch (e) {
140
+ const code = e.code;
141
+ if (code === 1)
142
+ return { output: 'no matches found', summary: 'search: no matches' };
143
+ return { output: `search error: ${e.message}`, summary: 'search error' };
144
+ }
145
+ }
146
+ function fallbackSearch(base, query, filename, max, cwd) {
147
+ const files = [];
148
+ walkFiles(base, files, 2000);
149
+ const results = [];
150
+ for (const f of files) {
151
+ if (results.length >= max)
152
+ break;
153
+ const rel = path.relative(cwd, f);
154
+ const name = path.basename(f);
155
+ if (filename) {
156
+ if (filename.includes('*')) {
157
+ const re = new RegExp('^' + filename.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
158
+ if (!re.test(name))
159
+ continue;
160
+ }
161
+ else if (!name.includes(filename))
162
+ continue;
163
+ results.push(rel);
164
+ continue;
165
+ }
166
+ if (query) {
167
+ try {
168
+ const content = fs.readFileSync(f, 'utf8');
169
+ if (content.includes(query)) {
170
+ results.push(rel);
171
+ }
172
+ }
173
+ catch {
174
+ /* binary/unreadable */
175
+ }
176
+ }
177
+ }
178
+ if (results.length === 0)
179
+ return { output: 'no matches found', summary: 'search: no matches' };
180
+ return {
181
+ output: `found ${results.length} match(es):\n${results.join('\n')}`,
182
+ summary: `search found ${results.length} match(es)`,
183
+ };
184
+ }