zero-ai-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/MERGE_REPORT.md +265 -0
  2. package/README.md +127 -0
  3. package/package.json +55 -0
  4. package/packages/cli/src/index.ts +271 -0
  5. package/packages/cli/src/interactive.ts +190 -0
  6. package/packages/cli/src/once.ts +50 -0
  7. package/packages/core/src/agent/config-loader.ts +174 -0
  8. package/packages/core/src/agent/engine.ts +266 -0
  9. package/packages/core/src/agent/registry-tools.ts +58 -0
  10. package/packages/core/src/agent/registry.ts +213 -0
  11. package/packages/core/src/commands/index.ts +116 -0
  12. package/packages/core/src/config/index.ts +139 -0
  13. package/packages/core/src/confirmation/message-bus.ts +206 -0
  14. package/packages/core/src/diff/index.ts +232 -0
  15. package/packages/core/src/diff-detect/index.ts +207 -0
  16. package/packages/core/src/edit-coder/index.ts +194 -0
  17. package/packages/core/src/events/index.ts +151 -0
  18. package/packages/core/src/file-tracker/index.ts +183 -0
  19. package/packages/core/src/git/index.ts +305 -0
  20. package/packages/core/src/history/index.ts +236 -0
  21. package/packages/core/src/image/index.ts +131 -0
  22. package/packages/core/src/index.ts +131 -0
  23. package/packages/core/src/lsp/index.ts +251 -0
  24. package/packages/core/src/mcp/index.ts +186 -0
  25. package/packages/core/src/memory/index.ts +141 -0
  26. package/packages/core/src/memory/prompts.ts +52 -0
  27. package/packages/core/src/orchestrator/protocol.ts +140 -0
  28. package/packages/core/src/orchestrator/server.ts +319 -0
  29. package/packages/core/src/output/index.ts +164 -0
  30. package/packages/core/src/permissions/index.ts +126 -0
  31. package/packages/core/src/prompts/engine.ts +188 -0
  32. package/packages/core/src/providers/registry.ts +687 -0
  33. package/packages/core/src/routing/model-router.ts +160 -0
  34. package/packages/core/src/server/index.ts +232 -0
  35. package/packages/core/src/session/index.ts +174 -0
  36. package/packages/core/src/session/service.ts +322 -0
  37. package/packages/core/src/skills/index.ts +205 -0
  38. package/packages/core/src/streaming/index.ts +166 -0
  39. package/packages/core/src/sub-agent/index.ts +179 -0
  40. package/packages/core/src/tools/builtin.ts +568 -0
  41. package/packages/core/src/types.ts +356 -0
  42. package/packages/sdk/src/index.ts +247 -0
  43. package/packages/tui/src/index.ts +141 -0
  44. package/tsconfig.json +26 -0
@@ -0,0 +1,568 @@
1
+ /**
2
+ * ZERO Built-in Tools
3
+ * Core tool implementations combining best patterns from all 9 projects
4
+ *
5
+ * Sources:
6
+ * - File operations: aider (diff editing) + cline (precise edits) + kilocode
7
+ * - Shell execution: gemini-cli (sandboxed) + crush (session management)
8
+ * - Search: opencode (ripgrep integration) + kilocode (indexing)
9
+ * - Git: cline (comprehensive git support) + aider
10
+ * - Memory: kilocode (kilo-memory) + goose (memory MCP)
11
+ */
12
+
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import { execSync, spawn } from "node:child_process";
16
+ import type { ToolDefinition, ToolContext, ToolResult } from "../types.js";
17
+
18
+ // ============================================================================
19
+ // File Operations
20
+ // ============================================================================
21
+
22
+ export const readFileTool: ToolDefinition = {
23
+ name: "read_file",
24
+ description: "Read the contents of a file. Supports text files. Returns the file content as text with line numbers.",
25
+ parameters: {
26
+ path: { type: "string", description: "Path to the file to read (relative to working directory)", required: true },
27
+ startLine: { type: "number", description: "Start reading from this line (1-indexed)", required: false },
28
+ endLine: { type: "number", description: "Stop reading at this line (inclusive)", required: false },
29
+ },
30
+ category: "file",
31
+ requiresApproval: false,
32
+ handler: async (args, context) => {
33
+ const filePath = path.resolve(context.workingDirectory, args.path as string);
34
+ try {
35
+ const content = await fs.readFile(filePath, "utf-8");
36
+ const lines = content.split("\n");
37
+
38
+ const startLine = (args.startLine as number) || 1;
39
+ const endLine = (args.endLine as number) || lines.length;
40
+
41
+ const selectedLines = lines.slice(startLine - 1, endLine);
42
+ const numbered = selectedLines
43
+ .map((line, i) => `${String(startLine + i).padStart(5)} | ${line}`)
44
+ .join("\n");
45
+
46
+ return {
47
+ success: true,
48
+ output: `File: ${args.path}\nLines: ${startLine}-${endLine} of ${lines.length}\n\n${numbered}`,
49
+ data: { path: args.path, lines: lines.length, content },
50
+ };
51
+ } catch (error) {
52
+ return {
53
+ success: false,
54
+ output: "",
55
+ error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
56
+ };
57
+ }
58
+ },
59
+ };
60
+
61
+ export const writeFileTool: ToolDefinition = {
62
+ name: "write_file",
63
+ description: "Write content to a file. Creates the file if it doesn't exist, or overwrites if it does. Parent directories are created automatically.",
64
+ parameters: {
65
+ path: { type: "string", description: "Path to the file to write (relative to working directory)", required: true },
66
+ content: { type: "string", description: "The content to write to the file", required: true },
67
+ },
68
+ category: "file",
69
+ requiresApproval: true,
70
+ handler: async (args, context) => {
71
+ const filePath = path.resolve(context.workingDirectory, args.path as string);
72
+ try {
73
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
74
+ await fs.writeFile(filePath, args.content as string, "utf-8");
75
+ const lineCount = (args.content as string).split("\n").length;
76
+ return {
77
+ success: true,
78
+ output: `Successfully wrote ${lineCount} lines to ${args.path}`,
79
+ data: { path: args.path, lines: lineCount },
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ success: false,
84
+ output: "",
85
+ error: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
86
+ };
87
+ }
88
+ },
89
+ };
90
+
91
+ export const editFileTool: ToolDefinition = {
92
+ name: "edit_file",
93
+ description: `Edit a file by finding and replacing text. Uses exact string matching.
94
+ Provide the exact old text to find and the new text to replace it with.
95
+ Only the first occurrence is replaced. Use sufficient context in oldText to ensure uniqueness.`,
96
+ parameters: {
97
+ path: { type: "string", description: "Path to the file to edit (relative to working directory)", required: true },
98
+ oldText: { type: "string", description: "The exact text to find and replace", required: true },
99
+ newText: { type: "string", description: "The new text to replace with", required: true },
100
+ },
101
+ category: "file",
102
+ requiresApproval: true,
103
+ handler: async (args, context) => {
104
+ const filePath = path.resolve(context.workingDirectory, args.path as string);
105
+ try {
106
+ const content = await fs.readFile(filePath, "utf-8");
107
+ const oldText = args.oldText as string;
108
+ const newText = args.newText as string;
109
+
110
+ // Count occurrences
111
+ const count = content.split(oldText).length - 1;
112
+
113
+ if (count === 0) {
114
+ return {
115
+ success: false,
116
+ output: "",
117
+ error: `Text not found in file. Make sure the oldText matches exactly.`,
118
+ };
119
+ }
120
+
121
+ if (count > 1) {
122
+ return {
123
+ success: false,
124
+ output: "",
125
+ error: `Text found ${count} times. Provide more context in oldText to ensure uniqueness.`,
126
+ };
127
+ }
128
+
129
+ const newContent = content.replace(oldText, newText);
130
+ await fs.writeFile(filePath, newContent, "utf-8");
131
+
132
+ return {
133
+ success: true,
134
+ output: `Successfully edited ${args.path}`,
135
+ data: { path: args.path, occurrences: 1 },
136
+ };
137
+ } catch (error) {
138
+ return {
139
+ success: false,
140
+ output: "",
141
+ error: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
142
+ };
143
+ }
144
+ },
145
+ };
146
+
147
+ export const listDirectoryTool: ToolDefinition = {
148
+ name: "list_directory",
149
+ description: "List files and directories in a path. Shows file sizes and types. Respects .gitignore patterns.",
150
+ parameters: {
151
+ path: { type: "string", description: "Path to list (relative to working directory). Defaults to root.", required: false },
152
+ recursive: { type: "boolean", description: "List recursively", required: false, default: false },
153
+ maxDepth: { type: "number", description: "Maximum depth for recursive listing", required: false, default: 3 },
154
+ },
155
+ category: "file",
156
+ requiresApproval: false,
157
+ handler: async (args, context) => {
158
+ const dirPath = path.resolve(context.workingDirectory, (args.path as string) || ".");
159
+ const recursive = (args.recursive as boolean) || false;
160
+ const maxDepth = (args.maxDepth as number) || 3;
161
+
162
+ try {
163
+ const results: string[] = [];
164
+
165
+ async function listDir(dir: string, prefix: string, depth: number): Promise<void> {
166
+ if (depth > maxDepth) return;
167
+
168
+ const entries = await fs.readdir(dir, { withFileTypes: true });
169
+ const sorted = entries.sort((a, b) => {
170
+ if (a.isDirectory() && !b.isDirectory()) return -1;
171
+ if (!a.isDirectory() && b.isDirectory()) return 1;
172
+ return a.name.localeCompare(b.name);
173
+ });
174
+
175
+ for (const entry of sorted) {
176
+ if (entry.name.startsWith(".") && entry.name !== ".env") continue;
177
+ if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
178
+
179
+ const icon = entry.isDirectory() ? "📁" : "📄";
180
+ results.push(`${prefix}${icon} ${entry.name}`);
181
+
182
+ if (entry.isDirectory() && recursive) {
183
+ await listDir(path.join(dir, entry.name), prefix + " ", depth + 1);
184
+ }
185
+ }
186
+ }
187
+
188
+ await listDir(dirPath, "", 0);
189
+
190
+ return {
191
+ success: true,
192
+ output: results.join("\n") || "(empty directory)",
193
+ data: { path: args.path || ".", count: results.length },
194
+ };
195
+ } catch (error) {
196
+ return {
197
+ success: false,
198
+ output: "",
199
+ error: `Failed to list directory: ${error instanceof Error ? error.message : String(error)}`,
200
+ };
201
+ }
202
+ },
203
+ };
204
+
205
+ // ============================================================================
206
+ // Search Operations
207
+ // ============================================================================
208
+
209
+ export const searchFilesTool: ToolDefinition = {
210
+ name: "search_files",
211
+ description: "Search for files by name pattern using glob syntax. Fast and efficient for finding specific files.",
212
+ parameters: {
213
+ pattern: { type: "string", description: "Glob pattern to match (e.g., '*.ts', 'test/**/*.spec.js')", required: true },
214
+ path: { type: "string", description: "Directory to search in (relative to working directory)", required: false },
215
+ },
216
+ category: "search",
217
+ requiresApproval: false,
218
+ handler: async (args, context) => {
219
+ const searchDir = path.resolve(context.workingDirectory, (args.path as string) || ".");
220
+ const pattern = args.pattern as string;
221
+
222
+ try {
223
+ // Use find command for portability
224
+ const findCmd = `find "${searchDir}" -name "${pattern.replace(/\*/g, "*")}" -not -path "*/node_modules/*" -not -path "*/.git/*" | head -50`;
225
+ const output = execSync(findCmd, { encoding: "utf-8", timeout: 10000 }).trim();
226
+
227
+ const files = output.split("\n").filter(Boolean).map((f) =>
228
+ path.relative(context.workingDirectory, f)
229
+ );
230
+
231
+ return {
232
+ success: true,
233
+ output: files.length > 0
234
+ ? `Found ${files.length} files:\n${files.join("\n")}`
235
+ : "No files found matching pattern",
236
+ data: { files, count: files.length },
237
+ };
238
+ } catch (error) {
239
+ return {
240
+ success: false,
241
+ output: "",
242
+ error: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
243
+ };
244
+ }
245
+ },
246
+ };
247
+
248
+ export const searchCodeTool: ToolDefinition = {
249
+ name: "search_code",
250
+ description: "Search for text/code patterns across all files. Uses grep/ripgrep for fast searching. Shows matching lines with context.",
251
+ parameters: {
252
+ query: { type: "string", description: "Text or regex pattern to search for", required: true },
253
+ path: { type: "string", description: "Directory to search in (relative to working directory)", required: false },
254
+ filePattern: { type: "string", description: "File extension filter (e.g., '*.ts', '*.py')", required: false },
255
+ caseSensitive: { type: "boolean", description: "Case sensitive search", required: false, default: true },
256
+ },
257
+ category: "search",
258
+ requiresApproval: false,
259
+ handler: async (args, context) => {
260
+ const searchDir = path.resolve(context.workingDirectory, (args.path as string) || ".");
261
+ const query = args.query as string;
262
+ const filePattern = args.filePattern as string;
263
+ const caseSensitive = args.caseSensitive !== false;
264
+
265
+ try {
266
+ // Try ripgrep first, fall back to grep
267
+ let cmd: string;
268
+ const ignoreFlags = "--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build";
269
+ const includeFlag = filePattern ? `--include='${filePattern}'` : "";
270
+ const caseFlag = caseSensitive ? "" : "-i";
271
+
272
+ cmd = `grep -rn ${caseFlag} ${ignoreFlags} ${includeFlag} -- "${query.replace(/"/g, '\\"')}" "${searchDir}" | head -50`;
273
+
274
+ const output = execSync(cmd, { encoding: "utf-8", timeout: 15000, maxBuffer: 1024 * 1024 }).trim();
275
+
276
+ if (!output) {
277
+ return { success: true, output: "No matches found.", data: { matches: [], count: 0 } };
278
+ }
279
+
280
+ const matches = output.split("\n").map((line) => {
281
+ const parts = line.split(":");
282
+ return {
283
+ file: path.relative(context.workingDirectory, parts[0] || ""),
284
+ line: parseInt(parts[1] || "0"),
285
+ content: parts.slice(2).join(":").trim(),
286
+ };
287
+ });
288
+
289
+ return {
290
+ success: true,
291
+ output: `Found ${matches.length} matches:\n\n${output}`,
292
+ data: { matches, count: matches.length },
293
+ };
294
+ } catch (error) {
295
+ // grep returns non-zero when no matches
296
+ if ((error as any)?.status === 1) {
297
+ return { success: true, output: "No matches found.", data: { matches: [], count: 0 } };
298
+ }
299
+ return {
300
+ success: false,
301
+ output: "",
302
+ error: `Code search failed: ${error instanceof Error ? error.message : String(error)}`,
303
+ };
304
+ }
305
+ },
306
+ };
307
+
308
+ // ============================================================================
309
+ // Shell Execution
310
+ // ============================================================================
311
+
312
+ export const executeShellTool: ToolDefinition = {
313
+ name: "execute_shell",
314
+ description: `Execute a shell command in the working directory. Use for running tests, installing packages, building projects, etc.
315
+ Commands are executed with a timeout. Output is truncated if too long.
316
+ WARNING: Commands that modify the filesystem or install packages require user approval.`,
317
+ parameters: {
318
+ command: { type: "string", description: "Shell command to execute", required: true },
319
+ timeout: { type: "number", description: "Timeout in seconds (default: 30, max: 120)", required: false, default: 30 },
320
+ },
321
+ category: "shell",
322
+ requiresApproval: true,
323
+ handler: async (args, context) => {
324
+ const command = args.command as string;
325
+ const timeout = Math.min(((args.timeout as number) || 30) * 1000, 120000);
326
+
327
+ try {
328
+ const output = execSync(command, {
329
+ cwd: context.workingDirectory,
330
+ encoding: "utf-8",
331
+ timeout,
332
+ maxBuffer: 5 * 1024 * 1024,
333
+ shell: "/bin/bash",
334
+ env: { ...process.env, ZERO_SESSION_ID: context.sessionId },
335
+ });
336
+
337
+ // Truncate if too long
338
+ const maxOutput = 10000;
339
+ const truncated = output.length > maxOutput
340
+ ? output.slice(0, maxOutput) + `\n\n... [truncated ${output.length - maxOutput} characters]`
341
+ : output;
342
+
343
+ return {
344
+ success: true,
345
+ output: truncated || "(command completed with no output)",
346
+ data: { exitCode: 0 },
347
+ };
348
+ } catch (error: any) {
349
+ const output = (error.stdout || "") + (error.stderr || "");
350
+ const truncated = output.length > 10000
351
+ ? output.slice(0, 10000) + "\n\n... [truncated]"
352
+ : output;
353
+
354
+ return {
355
+ success: false,
356
+ output: truncated,
357
+ error: `Command failed with exit code ${error.status || "unknown"}`,
358
+ data: { exitCode: error.status },
359
+ };
360
+ }
361
+ },
362
+ };
363
+
364
+ // ============================================================================
365
+ // Git Operations
366
+ // ============================================================================
367
+
368
+ export const gitStatusTool: ToolDefinition = {
369
+ name: "git_status",
370
+ description: "Get the current git status showing modified, added, deleted, and untracked files.",
371
+ parameters: {
372
+ path: { type: "string", description: "Repository path (relative to working directory)", required: false },
373
+ },
374
+ category: "git",
375
+ requiresApproval: false,
376
+ handler: async (args, context) => {
377
+ const repoPath = path.resolve(context.workingDirectory, (args.path as string) || ".");
378
+ try {
379
+ const status = execSync("git status --porcelain=v2 --branch", {
380
+ cwd: repoPath,
381
+ encoding: "utf-8",
382
+ timeout: 10000,
383
+ });
384
+
385
+ const log = execSync("git log --oneline -5", {
386
+ cwd: repoPath,
387
+ encoding: "utf-8",
388
+ timeout: 10000,
389
+ }).trim();
390
+
391
+ return {
392
+ success: true,
393
+ output: `Git Status:\n${status}\n\nRecent Commits:\n${log}`,
394
+ };
395
+ } catch (error) {
396
+ return {
397
+ success: false,
398
+ output: "",
399
+ error: `Git status failed: ${error instanceof Error ? error.message : String(error)}`,
400
+ };
401
+ }
402
+ },
403
+ };
404
+
405
+ export const gitDiffTool: ToolDefinition = {
406
+ name: "git_diff",
407
+ description: "Show git diff of changes. Can show staged, unstaged, or changes compared to a specific commit/branch.",
408
+ parameters: {
409
+ path: { type: "string", description: "File or directory to diff", required: false },
410
+ staged: { type: "boolean", description: "Show staged changes", required: false, default: false },
411
+ ref: { type: "string", description: "Compare against this ref (commit, branch, tag)", required: false },
412
+ },
413
+ category: "git",
414
+ requiresApproval: false,
415
+ handler: async (args, context) => {
416
+ const repoPath = context.workingDirectory;
417
+ const filePath = args.path as string;
418
+ const staged = args.staged as boolean;
419
+ const ref = args.ref as string;
420
+
421
+ try {
422
+ let cmd = "git diff";
423
+ if (staged) cmd += " --cached";
424
+ if (ref) cmd += ` ${ref}`;
425
+ if (filePath) cmd += ` -- ${filePath}`;
426
+
427
+ const output = execSync(cmd, {
428
+ cwd: repoPath,
429
+ encoding: "utf-8",
430
+ timeout: 15000,
431
+ maxBuffer: 5 * 1024 * 1024,
432
+ });
433
+
434
+ return {
435
+ success: true,
436
+ output: output || "No changes detected.",
437
+ };
438
+ } catch (error) {
439
+ return {
440
+ success: false,
441
+ output: "",
442
+ error: `Git diff failed: ${error instanceof Error ? error.message : String(error)}`,
443
+ };
444
+ }
445
+ },
446
+ };
447
+
448
+ export const gitCommitTool: ToolDefinition = {
449
+ name: "git_commit",
450
+ description: "Stage all changes and create a git commit with the provided message.",
451
+ parameters: {
452
+ message: { type: "string", description: "Commit message", required: true },
453
+ files: { type: "array", description: "Specific files to stage (if empty, stages all)", required: false },
454
+ },
455
+ category: "git",
456
+ requiresApproval: true,
457
+ handler: async (args, context) => {
458
+ const message = args.message as string;
459
+ const files = args.files as string[] | undefined;
460
+
461
+ try {
462
+ if (files && files.length > 0) {
463
+ for (const file of files) {
464
+ execSync(`git add "${file}"`, { cwd: context.workingDirectory, timeout: 10000 });
465
+ }
466
+ } else {
467
+ execSync("git add -A", { cwd: context.workingDirectory, timeout: 10000 });
468
+ }
469
+
470
+ const output = execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
471
+ cwd: context.workingDirectory,
472
+ encoding: "utf-8",
473
+ timeout: 10000,
474
+ });
475
+
476
+ return {
477
+ success: true,
478
+ output: output.trim(),
479
+ };
480
+ } catch (error) {
481
+ return {
482
+ success: false,
483
+ output: "",
484
+ error: `Git commit failed: ${error instanceof Error ? error.message : String(error)}`,
485
+ };
486
+ }
487
+ },
488
+ };
489
+
490
+ // ============================================================================
491
+ // Web Operations
492
+ // ============================================================================
493
+
494
+ export const webFetchTool: ToolDefinition = {
495
+ name: "web_fetch",
496
+ description: "Fetch content from a URL. Returns the text content of the page.",
497
+ parameters: {
498
+ url: { type: "string", description: "URL to fetch", required: true },
499
+ },
500
+ category: "web",
501
+ requiresApproval: true,
502
+ handler: async (args) => {
503
+ const url = args.url as string;
504
+ try {
505
+ const response = await fetch(url, {
506
+ headers: { "User-Agent": "ZERO/1.0" },
507
+ signal: AbortSignal.timeout(30000),
508
+ });
509
+
510
+ if (!response.ok) {
511
+ return { success: false, output: "", error: `HTTP ${response.status}: ${response.statusText}` };
512
+ }
513
+
514
+ const text = await response.text();
515
+ const truncated = text.length > 50000 ? text.slice(0, 50000) + "\n... [truncated]" : text;
516
+
517
+ return {
518
+ success: true,
519
+ output: truncated,
520
+ data: { url, contentType: response.headers.get("content-type"), status: response.status },
521
+ };
522
+ } catch (error) {
523
+ return {
524
+ success: false,
525
+ output: "",
526
+ error: `Fetch failed: ${error instanceof Error ? error.message : String(error)}`,
527
+ };
528
+ }
529
+ },
530
+ };
531
+
532
+ export const webSearchTool: ToolDefinition = {
533
+ name: "web_search",
534
+ description: "Search the web for information. Returns relevant results with titles, URLs, and snippets.",
535
+ parameters: {
536
+ query: { type: "string", description: "Search query", required: true },
537
+ },
538
+ category: "web",
539
+ requiresApproval: false,
540
+ handler: async (args) => {
541
+ // Placeholder - would integrate with a search API
542
+ return {
543
+ success: true,
544
+ output: `Web search for: "${args.query}"\n(Configure a search API key in ZERO config to enable web search)`,
545
+ };
546
+ },
547
+ };
548
+
549
+ // ============================================================================
550
+ // Export all tools
551
+ // ============================================================================
552
+
553
+ export function getAllBuiltinTools(): ToolDefinition[] {
554
+ return [
555
+ readFileTool,
556
+ writeFileTool,
557
+ editFileTool,
558
+ listDirectoryTool,
559
+ searchFilesTool,
560
+ searchCodeTool,
561
+ executeShellTool,
562
+ gitStatusTool,
563
+ gitDiffTool,
564
+ gitCommitTool,
565
+ webFetchTool,
566
+ webSearchTool,
567
+ ];
568
+ }