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,183 @@
1
+ /**
2
+ * ZERO File Tracker Service
3
+ * Adapted from: crush (Charm) - MIT
4
+ *
5
+ * Tracks file changes and provides file system monitoring.
6
+ */
7
+
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import { watch, type FSWatcher } from "node:fs";
11
+ import { globalEventBus, ZERO_EVENTS } from "../events/index.js";
12
+
13
+ export interface FileChange {
14
+ path: string;
15
+ type: "created" | "modified" | "deleted";
16
+ timestamp: number;
17
+ size?: number;
18
+ }
19
+
20
+ export interface FileTrackerOptions {
21
+ workingDirectory: string;
22
+ ignorePatterns?: string[];
23
+ debounceMs?: number;
24
+ }
25
+
26
+ export class FileTracker {
27
+ private workingDirectory: string;
28
+ private ignorePatterns: RegExp[];
29
+ private debounceMs: number;
30
+ private watcher?: FSWatcher;
31
+ private changeHistory: FileChange[] = [];
32
+ private pendingChanges = new Map<string, NodeJS.Timeout>();
33
+ private fileHashes = new Map<string, string>();
34
+
35
+ constructor(options: FileTrackerOptions) {
36
+ this.workingDirectory = options.workingDirectory;
37
+ this.ignorePatterns = (options.ignorePatterns || [
38
+ "node_modules",
39
+ ".git",
40
+ "__pycache__",
41
+ "dist",
42
+ "build",
43
+ "coverage",
44
+ ".next",
45
+ ".nuxt",
46
+ "target",
47
+ "vendor",
48
+ ".venv",
49
+ "venv",
50
+ ".cache",
51
+ ".turbo",
52
+ "*.log",
53
+ ]).map((p) => new RegExp(p.replace(/\*/g, ".*")));
54
+ this.debounceMs = options.debounceMs || 100;
55
+ }
56
+
57
+ /**
58
+ * Start watching for file changes
59
+ */
60
+ async start(): Promise<void> {
61
+ // Initial scan
62
+ await this.scanDirectory(this.workingDirectory);
63
+
64
+ // Start watcher
65
+ this.watcher = watch(this.workingDirectory, { recursive: true }, (eventType, filename) => {
66
+ if (!filename) return;
67
+ const fullPath = path.join(this.workingDirectory, filename);
68
+
69
+ // Check ignore patterns
70
+ if (this.shouldIgnore(fullPath)) return;
71
+
72
+ // Debounce
73
+ const existing = this.pendingChanges.get(fullPath);
74
+ if (existing) clearTimeout(existing);
75
+
76
+ this.pendingChanges.set(
77
+ fullPath,
78
+ setTimeout(() => this.handleFileChange(fullPath), this.debounceMs)
79
+ );
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Stop watching
85
+ */
86
+ stop(): void {
87
+ if (this.watcher) {
88
+ this.watcher.close();
89
+ this.watcher = undefined;
90
+ }
91
+
92
+ // Clear pending changes
93
+ for (const timeout of this.pendingChanges.values()) {
94
+ clearTimeout(timeout);
95
+ }
96
+ this.pendingChanges.clear();
97
+ }
98
+
99
+ /**
100
+ * Get change history
101
+ */
102
+ getHistory(limit?: number): FileChange[] {
103
+ const history = [...this.changeHistory].sort((a, b) => b.timestamp - a.timestamp);
104
+ return limit ? history.slice(0, limit) : history;
105
+ }
106
+
107
+ /**
108
+ * Clear history
109
+ */
110
+ clearHistory(): void {
111
+ this.changeHistory = [];
112
+ }
113
+
114
+ /**
115
+ * Get recently changed files
116
+ */
117
+ getRecentChanges(sinceMs: number): FileChange[] {
118
+ const cutoff = Date.now() - sinceMs;
119
+ return this.changeHistory.filter((c) => c.timestamp > cutoff);
120
+ }
121
+
122
+ private shouldIgnore(filePath: string): boolean {
123
+ const relativePath = path.relative(this.workingDirectory, filePath);
124
+ return this.ignorePatterns.some((pattern) => pattern.test(relativePath));
125
+ }
126
+
127
+ private async handleFileChange(filePath: string): Promise<void> {
128
+ this.pendingChanges.delete(filePath);
129
+
130
+ try {
131
+ const stats = await fs.stat(filePath);
132
+ const change: FileChange = {
133
+ path: path.relative(this.workingDirectory, filePath),
134
+ type: "modified",
135
+ timestamp: Date.now(),
136
+ size: stats.size,
137
+ };
138
+
139
+ if (!this.fileHashes.has(filePath)) {
140
+ change.type = "created";
141
+ }
142
+
143
+ this.fileHashes.set(filePath, `${stats.size}-${stats.mtimeMs}`);
144
+ this.changeHistory.push(change);
145
+
146
+ // Emit event
147
+ await globalEventBus.emit(ZERO_EVENTS.FILE_CHANGED, change);
148
+ } catch (error) {
149
+ // File was deleted
150
+ if (this.fileHashes.has(filePath)) {
151
+ const change: FileChange = {
152
+ path: path.relative(this.workingDirectory, filePath),
153
+ type: "deleted",
154
+ timestamp: Date.now(),
155
+ };
156
+ this.fileHashes.delete(filePath);
157
+ this.changeHistory.push(change);
158
+ await globalEventBus.emit(ZERO_EVENTS.FILE_DELETED, change);
159
+ }
160
+ }
161
+ }
162
+
163
+ private async scanDirectory(dir: string): Promise<void> {
164
+ try {
165
+ const entries = await fs.readdir(dir, { withFileTypes: true });
166
+
167
+ for (const entry of entries) {
168
+ const fullPath = path.join(dir, entry.name);
169
+
170
+ if (this.shouldIgnore(fullPath)) continue;
171
+
172
+ if (entry.isDirectory()) {
173
+ await this.scanDirectory(fullPath);
174
+ } else if (entry.isFile()) {
175
+ try {
176
+ const stats = await fs.stat(fullPath);
177
+ this.fileHashes.set(fullPath, `${stats.size}-${stats.mtimeMs}`);
178
+ } catch {}
179
+ }
180
+ }
181
+ } catch {}
182
+ }
183
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * ZERO Git Service
3
+ * Adapted from: opencode (SST) - MIT
4
+ *
5
+ * Comprehensive git integration with support for status, diff,
6
+ * patches, branching, and merge operations.
7
+ */
8
+
9
+ import { execSync } from "node:child_process";
10
+
11
+ const GIT_CONFIG_ARGS = [
12
+ "--no-optional-locks",
13
+ "-c", "core.autocrlf=false",
14
+ "-c", "core.fsmonitor=false",
15
+ "-c", "core.longpaths=true",
16
+ "-c", "core.symlinks=true",
17
+ "-c", "core.quotepath=false",
18
+ ] as const;
19
+
20
+ export type ChangeKind = "added" | "deleted" | "modified";
21
+
22
+ export interface GitItem {
23
+ readonly file: string;
24
+ readonly code: string;
25
+ readonly status: ChangeKind;
26
+ }
27
+
28
+ export interface GitStat {
29
+ readonly file: string;
30
+ readonly additions: number;
31
+ readonly deletions: number;
32
+ }
33
+
34
+ export interface GitPatch {
35
+ readonly text: string;
36
+ readonly truncated: boolean;
37
+ }
38
+
39
+ export interface GitBase {
40
+ readonly name: string;
41
+ readonly ref: string;
42
+ }
43
+
44
+ export interface PatchOptions {
45
+ readonly context?: number;
46
+ readonly maxOutputBytes?: number;
47
+ }
48
+
49
+ export interface GitResult {
50
+ readonly exitCode: number;
51
+ readonly stdout: string;
52
+ readonly stderr: string;
53
+ readonly truncated: boolean;
54
+ }
55
+
56
+ export class GitService {
57
+ constructor(private cwd: string) {}
58
+
59
+ // ========================================================================
60
+ // Core Operations
61
+ // ========================================================================
62
+
63
+ private run(args: string[], options?: { stdin?: string; maxOutputBytes?: number }): GitResult {
64
+ try {
65
+ const fullArgs = [...GIT_CONFIG_ARGS, ...args];
66
+ const stdout = execSync(`git ${fullArgs.join(" ")}`, {
67
+ cwd: this.cwd,
68
+ encoding: "utf-8",
69
+ maxBuffer: options?.maxOutputBytes || 10 * 1024 * 1024,
70
+ input: options?.stdin,
71
+ timeout: 30000,
72
+ });
73
+
74
+ return {
75
+ exitCode: 0,
76
+ stdout: stdout.trim(),
77
+ stderr: "",
78
+ truncated: false,
79
+ };
80
+ } catch (error: any) {
81
+ return {
82
+ exitCode: error.status || 1,
83
+ stdout: (error.stdout || "").trim(),
84
+ stderr: (error.stderr || error.message || "").trim(),
85
+ truncated: false,
86
+ };
87
+ }
88
+ }
89
+
90
+ // ========================================================================
91
+ // Branch Operations
92
+ // ========================================================================
93
+
94
+ branch(): string | undefined {
95
+ const result = this.run(["rev-parse", "--abbrev-ref", "HEAD"]);
96
+ return result.exitCode === 0 ? result.stdout : undefined;
97
+ }
98
+
99
+ defaultBranch(): GitBase | undefined {
100
+ // Try common default branch names
101
+ for (const name of ["main", "master", "develop"]) {
102
+ const result = this.run(["rev-parse", "--verify", `refs/heads/${name}`]);
103
+ if (result.exitCode === 0) {
104
+ return { name, ref: result.stdout };
105
+ }
106
+ }
107
+
108
+ // Fall back to git config
109
+ const result = this.run(["config", "init.defaultBranch"]);
110
+ if (result.exitCode === 0 && result.stdout) {
111
+ const refResult = this.run(["rev-parse", "--verify", `refs/heads/${result.stdout}`]);
112
+ if (refResult.exitCode === 0) {
113
+ return { name: result.stdout, ref: refResult.stdout };
114
+ }
115
+ }
116
+
117
+ return undefined;
118
+ }
119
+
120
+ hasHead(): boolean {
121
+ return this.run(["rev-parse", "HEAD"]).exitCode === 0;
122
+ }
123
+
124
+ mergeBase(base: string, head?: string): string | undefined {
125
+ const args = ["merge-base", base];
126
+ if (head) args.push(head);
127
+ const result = this.run(args);
128
+ return result.exitCode === 0 ? result.stdout : undefined;
129
+ }
130
+
131
+ // ========================================================================
132
+ // Status & Diff
133
+ // ========================================================================
134
+
135
+ status(): GitItem[] {
136
+ const result = this.run(["status", "--porcelain=v1", "-z"]);
137
+ if (result.exitCode !== 0) return [];
138
+
139
+ const kind = (code: string): ChangeKind => {
140
+ if (code === "??") return "added";
141
+ if (code.includes("U")) return "modified";
142
+ if (code.includes("A") && !code.includes("D")) return "added";
143
+ if (code.includes("D") && !code.includes("A")) return "deleted";
144
+ return "modified";
145
+ };
146
+
147
+ const entries = result.stdout.split("\0").filter(Boolean);
148
+ const items: GitItem[] = [];
149
+
150
+ for (const entry of entries) {
151
+ if (entry.length < 4) continue;
152
+ const code = entry.slice(0, 2).trim();
153
+ const file = entry.slice(3);
154
+ items.push({ file, code, status: kind(code) });
155
+ }
156
+
157
+ return items;
158
+ }
159
+
160
+ diff(ref: string): GitItem[] {
161
+ const result = this.run(["diff", "--name-status", "-z", ref]);
162
+ if (result.exitCode !== 0) return [];
163
+
164
+ const parts = result.stdout.split("\0").filter(Boolean);
165
+ const items: GitItem[] = [];
166
+
167
+ for (let i = 0; i < parts.length; i += 2) {
168
+ const code = parts[i]?.trim();
169
+ const file = parts[i + 1];
170
+ if (!code || !file) continue;
171
+
172
+ const status: ChangeKind = code.startsWith("A") ? "added"
173
+ : code.startsWith("D") ? "deleted"
174
+ : "modified";
175
+
176
+ items.push({ file, code, status });
177
+ }
178
+
179
+ return items;
180
+ }
181
+
182
+ stats(ref: string): GitStat[] {
183
+ const result = this.run(["diff", "--numstat", ref]);
184
+ if (result.exitCode !== 0) return [];
185
+
186
+ return result.stdout.split("\n")
187
+ .filter(Boolean)
188
+ .map((line) => {
189
+ const [additions, deletions, file] = line.split("\t");
190
+ return {
191
+ file: file || "",
192
+ additions: parseInt(additions) || 0,
193
+ deletions: parseInt(deletions) || 0,
194
+ };
195
+ });
196
+ }
197
+
198
+ // ========================================================================
199
+ // Patch Operations
200
+ // ========================================================================
201
+
202
+ patch(ref: string, file: string, options?: PatchOptions): GitPatch {
203
+ const context = options?.context ?? 3;
204
+ const args = ["diff", `--unified=${context}`, ref, "--", file];
205
+ const result = this.run(args, { maxOutputBytes: options?.maxOutputBytes });
206
+
207
+ return {
208
+ text: result.stdout,
209
+ truncated: result.truncated,
210
+ };
211
+ }
212
+
213
+ patchAll(ref: string, options?: PatchOptions): GitPatch {
214
+ const context = options?.context ?? 3;
215
+ const args = ["diff", `--unified=${context}`, ref];
216
+ const result = this.run(args, { maxOutputBytes: options?.maxOutputBytes });
217
+
218
+ return {
219
+ text: result.stdout,
220
+ truncated: result.truncated,
221
+ };
222
+ }
223
+
224
+ patchUntracked(file: string, options?: PatchOptions): GitPatch {
225
+ // For untracked files, show the entire content as additions
226
+ const result = this.run(["diff", "--no-index", "/dev/null", file], {
227
+ maxOutputBytes: options?.maxOutputBytes,
228
+ });
229
+
230
+ return {
231
+ text: result.stdout || result.stderr,
232
+ truncated: result.truncated,
233
+ };
234
+ }
235
+
236
+ applyPatch(patch: string): GitResult {
237
+ return this.run(["apply", "--allow-empty", "-"], { stdin: patch });
238
+ }
239
+
240
+ // ========================================================================
241
+ // Show & Log
242
+ // ========================================================================
243
+
244
+ show(ref: string, file: string): string {
245
+ const result = this.run(["show", `${ref}:${file}`]);
246
+ return result.exitCode === 0 ? result.stdout : "";
247
+ }
248
+
249
+ log(options?: { maxCount?: number; ref?: string }): Array<{ hash: string; message: string; date: string }> {
250
+ const maxCount = options?.maxCount || 10;
251
+ const args = ["log", `--max-count=${maxCount}`, "--format=%H|%s|%aI"];
252
+ if (options?.ref) args.push(options.ref);
253
+
254
+ const result = this.run(args);
255
+ if (result.exitCode !== 0) return [];
256
+
257
+ return result.stdout.split("\n")
258
+ .filter(Boolean)
259
+ .map((line) => {
260
+ const [hash, message, date] = line.split("|");
261
+ return { hash, message, date };
262
+ });
263
+ }
264
+
265
+ // ========================================================================
266
+ // Commit Operations
267
+ // ========================================================================
268
+
269
+ add(files: string[] | "."): GitResult {
270
+ const args = ["add"];
271
+ if (files === ".") {
272
+ args.push("-A");
273
+ } else {
274
+ args.push("--", ...files);
275
+ }
276
+ return this.run(args);
277
+ }
278
+
279
+ commit(message: string, options?: { amend?: boolean; noVerify?: boolean }): GitResult {
280
+ const args = ["commit", "-m", message];
281
+ if (options?.amend) args.push("--amend");
282
+ if (options?.noVerify) args.push("--no-verify");
283
+ return this.run(args);
284
+ }
285
+
286
+ // ========================================================================
287
+ // Stash Operations
288
+ // ========================================================================
289
+
290
+ stash(options?: { message?: string }): GitResult {
291
+ const args = ["stash", "push"];
292
+ if (options?.message) args.push("-m", options.message);
293
+ return this.run(args);
294
+ }
295
+
296
+ stashPop(): GitResult {
297
+ return this.run(["stash", "pop"]);
298
+ }
299
+
300
+ stashList(): string[] {
301
+ const result = this.run(["stash", "list"]);
302
+ if (result.exitCode !== 0) return [];
303
+ return result.stdout.split("\n").filter(Boolean);
304
+ }
305
+ }