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,207 @@
1
+ /**
2
+ * ZERO Diff Detect Service
3
+ * Adapted from: crush (Charm) - MIT
4
+ *
5
+ * Detects diffs between file versions and provides
6
+ * intelligent change analysis.
7
+ */
8
+
9
+ import * as fs from "node:fs/promises";
10
+
11
+ export interface DiffStats {
12
+ additions: number;
13
+ deletions: number;
14
+ modifications: number;
15
+ totalChanges: number;
16
+ }
17
+
18
+ export interface DiffHunk {
19
+ oldStart: number;
20
+ oldLines: number;
21
+ newStart: number;
22
+ newLines: number;
23
+ lines: DiffLine[];
24
+ }
25
+
26
+ export interface DiffLine {
27
+ type: "context" | "add" | "remove";
28
+ content: string;
29
+ oldLineNumber?: number;
30
+ newLineNumber?: number;
31
+ }
32
+
33
+ export interface FileDiff {
34
+ filePath: string;
35
+ stats: DiffStats;
36
+ hunks: DiffHunk[];
37
+ }
38
+
39
+ /**
40
+ * Detect diff between two strings
41
+ */
42
+ export function detectDiff(before: string, after: string, filePath = "file"): FileDiff {
43
+ const beforeLines = before.split("\n");
44
+ const afterLines = after.split("\n");
45
+
46
+ const hunks: DiffHunk[] = [];
47
+ let additions = 0;
48
+ let deletions = 0;
49
+ let modifications = 0;
50
+
51
+ // Simple line-by-line diff using LCS
52
+ const lcs = computeLCS(beforeLines, afterLines);
53
+
54
+ let i = 0;
55
+ let j = 0;
56
+ let k = 0;
57
+ const lines: DiffLine[] = [];
58
+
59
+ while (i < beforeLines.length || j < afterLines.length) {
60
+ if (k < lcs.length && i < beforeLines.length && beforeLines[i] === lcs[k] && j < afterLines.length && afterLines[j] === lcs[k]) {
61
+ lines.push({
62
+ type: "context",
63
+ content: beforeLines[i],
64
+ oldLineNumber: i + 1,
65
+ newLineNumber: j + 1,
66
+ });
67
+ i++;
68
+ j++;
69
+ k++;
70
+ } else if (j < afterLines.length && (k >= lcs.length || afterLines[j] !== lcs[k])) {
71
+ lines.push({
72
+ type: "add",
73
+ content: afterLines[j],
74
+ newLineNumber: j + 1,
75
+ });
76
+ additions++;
77
+ j++;
78
+ } else if (i < beforeLines.length && (k >= lcs.length || beforeLines[i] !== lcs[k])) {
79
+ lines.push({
80
+ type: "remove",
81
+ content: beforeLines[i],
82
+ oldLineNumber: i + 1,
83
+ });
84
+ deletions++;
85
+ i++;
86
+ }
87
+ }
88
+
89
+ // Group lines into hunks
90
+ if (lines.length > 0) {
91
+ hunks.push({
92
+ oldStart: 1,
93
+ oldLines: beforeLines.length,
94
+ newStart: 1,
95
+ newLines: afterLines.length,
96
+ lines,
97
+ });
98
+ }
99
+
100
+ // Detect modifications (paired add/remove)
101
+ const minAddDel = Math.min(additions, deletions);
102
+ modifications = minAddDel;
103
+ additions -= minAddDel;
104
+ deletions -= minAddDel;
105
+
106
+ return {
107
+ filePath,
108
+ stats: {
109
+ additions,
110
+ deletions,
111
+ modifications,
112
+ totalChanges: additions + deletions + modifications,
113
+ },
114
+ hunks,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Detect diff between two files
120
+ */
121
+ export async function detectFileDiff(beforePath: string, afterPath: string): Promise<FileDiff> {
122
+ const [before, after] = await Promise.all([
123
+ fs.readFile(beforePath, "utf-8").catch(() => ""),
124
+ fs.readFile(afterPath, "utf-8").catch(() => ""),
125
+ ]);
126
+
127
+ return detectDiff(before, after, afterPath);
128
+ }
129
+
130
+ /**
131
+ * Format diff as unified diff string
132
+ */
133
+ export function formatDiff(diff: FileDiff): string {
134
+ const lines: string[] = [];
135
+ lines.push(`--- a/${diff.filePath}`);
136
+ lines.push(`+++ b/${diff.filePath}`);
137
+
138
+ for (const hunk of diff.hunks) {
139
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
140
+
141
+ for (const line of hunk.lines) {
142
+ switch (line.type) {
143
+ case "context":
144
+ lines.push(` ${line.content}`);
145
+ break;
146
+ case "add":
147
+ lines.push(`+${line.content}`);
148
+ break;
149
+ case "remove":
150
+ lines.push(`-${line.content}`);
151
+ break;
152
+ }
153
+ }
154
+ }
155
+
156
+ return lines.join("\n");
157
+ }
158
+
159
+ /**
160
+ * Get summary of changes
161
+ */
162
+ export function summarizeDiff(diff: FileDiff): string {
163
+ const { additions, deletions, modifications } = diff.stats;
164
+ const parts: string[] = [];
165
+
166
+ if (additions > 0) parts.push(`+${additions} added`);
167
+ if (deletions > 0) parts.push(`-${deletions} removed`);
168
+ if (modifications > 0) parts.push(`~${modifications} modified`);
169
+
170
+ return parts.length > 0
171
+ ? `${diff.filePath}: ${parts.join(", ")}`
172
+ : `${diff.filePath}: no changes`;
173
+ }
174
+
175
+ // LCS helper
176
+ function computeLCS(a: string[], b: string[]): string[] {
177
+ const m = a.length;
178
+ const n = b.length;
179
+ const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
180
+
181
+ for (let i = 1; i <= m; i++) {
182
+ for (let j = 1; j <= n; j++) {
183
+ if (a[i - 1] === b[j - 1]) {
184
+ dp[i][j] = dp[i - 1][j - 1] + 1;
185
+ } else {
186
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
187
+ }
188
+ }
189
+ }
190
+
191
+ const lcs: string[] = [];
192
+ let i = m;
193
+ let j = n;
194
+ while (i > 0 && j > 0) {
195
+ if (a[i - 1] === b[j - 1]) {
196
+ lcs.unshift(a[i - 1]);
197
+ i--;
198
+ j--;
199
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
200
+ i--;
201
+ } else {
202
+ j--;
203
+ }
204
+ }
205
+
206
+ return lcs;
207
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * ZERO EditBlock Coder
3
+ * Adapted from: aider (Python) - Apache 2.0
4
+ *
5
+ * Aider-style edit block system that uses SEARCH/REPLACE blocks
6
+ * for precise, multi-location file editing.
7
+ *
8
+ * Format:
9
+ * ```
10
+ * <<<<<<< SEARCH
11
+ * exact text to find
12
+ * =======
13
+ * replacement text
14
+ * >>>>>>> REPLACE
15
+ * ```
16
+ */
17
+
18
+ import * as fs from "node:fs/promises";
19
+ import * as path from "node:path";
20
+ import type { ToolDefinition, ToolResult } from "../types.js";
21
+
22
+ export interface EditBlock {
23
+ search: string;
24
+ replace: string;
25
+ lineNumber?: number;
26
+ }
27
+
28
+ export interface EditBlockResult {
29
+ filePath: string;
30
+ applied: number;
31
+ failed: number;
32
+ errors: string[];
33
+ diff: string;
34
+ }
35
+
36
+ /**
37
+ * Parse edit blocks from LLM output
38
+ */
39
+ export function parseEditBlocks(content: string): EditBlock[] {
40
+ const blocks: EditBlock[] = [];
41
+ const regex = /<<<<<<< SEARCH\n([\s\S]*?)=======\n([\s\S]*?)>>>>>>> REPLACE/g;
42
+
43
+ let match;
44
+ while ((match = regex.exec(content)) !== null) {
45
+ blocks.push({
46
+ search: match[1],
47
+ replace: match[2],
48
+ });
49
+ }
50
+
51
+ return blocks;
52
+ }
53
+
54
+ /**
55
+ * Apply edit blocks to file content
56
+ */
57
+ export function applyEditBlocks(content: string, blocks: EditBlock[]): { result: string; applied: number; errors: string[] } {
58
+ let result = content;
59
+ let applied = 0;
60
+ const errors: string[] = [];
61
+
62
+ for (let i = 0; i < blocks.length; i++) {
63
+ const block = blocks[i];
64
+ const searchStr = block.search;
65
+
66
+ // Find the search text
67
+ const idx = result.indexOf(searchStr);
68
+ if (idx === -1) {
69
+ // Try fuzzy match (trim whitespace differences)
70
+ const trimmedSearch = searchStr.trim();
71
+ const lines = result.split("\n");
72
+ let found = false;
73
+
74
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
75
+ const windowSize = trimmedSearch.split("\n").length;
76
+ const window = lines.slice(lineIdx, lineIdx + windowSize).join("\n").trim();
77
+
78
+ if (window === trimmedSearch) {
79
+ // Found a fuzzy match
80
+ const beforeLines = lines.slice(0, lineIdx).join("\n");
81
+ const afterLines = lines.slice(lineIdx + windowSize).join("\n");
82
+ result = (beforeLines ? beforeLines + "\n" : "") + block.replace.trimEnd() + (afterLines ? "\n" + afterLines : "");
83
+ applied++;
84
+ found = true;
85
+ break;
86
+ }
87
+ }
88
+
89
+ if (!found) {
90
+ errors.push(`Block ${i + 1}: Search text not found`);
91
+ }
92
+ } else {
93
+ // Exact match found
94
+ const count = result.split(searchStr).length - 1;
95
+ if (count > 1) {
96
+ errors.push(`Block ${i + 1}: Search text found ${count} times (need unique match)`);
97
+ } else {
98
+ result = result.replace(searchStr, block.replace);
99
+ applied++;
100
+ }
101
+ }
102
+ }
103
+
104
+ return { result, applied, errors };
105
+ }
106
+
107
+ /**
108
+ * Apply edit blocks to a file
109
+ */
110
+ export async function applyEditBlocksToFile(filePath: string, blocks: EditBlock[], workingDir: string): Promise<EditBlockResult> {
111
+ const fullPath = path.resolve(workingDir, filePath);
112
+
113
+ try {
114
+ const content = await fs.readFile(fullPath, "utf-8");
115
+ const { result, applied, errors } = applyEditBlocks(content, blocks);
116
+
117
+ if (applied > 0) {
118
+ await fs.writeFile(fullPath, result, "utf-8");
119
+ }
120
+
121
+ // Generate simple diff
122
+ const diffLines: string[] = [`--- a/${filePath}`, `+++ b/${filePath}`];
123
+ for (const block of blocks.slice(0, applied)) {
124
+ diffLines.push(`-${block.search.split("\n")[0]}`);
125
+ diffLines.push(`+${block.replace.split("\n")[0]}`);
126
+ }
127
+
128
+ return {
129
+ filePath,
130
+ applied,
131
+ failed: blocks.length - applied,
132
+ errors,
133
+ diff: diffLines.join("\n"),
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ filePath,
138
+ applied: 0,
139
+ failed: blocks.length,
140
+ errors: [`Failed to read file: ${error instanceof Error ? error.message : String(error)}`],
141
+ diff: "",
142
+ };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Generate edit block format for LLM
148
+ */
149
+ export function formatEditBlock(search: string, replace: string): string {
150
+ return `<<<<<<< SEARCH\n${search}=======\n${replace}>>>>>>> REPLACE`;
151
+ }
152
+
153
+ /**
154
+ * EditBlock tool definition
155
+ */
156
+ export const editBlockTool: ToolDefinition = {
157
+ name: "edit_block",
158
+ description: `Apply SEARCH/REPLACE edit blocks to a file. This is the most precise way to edit files.
159
+
160
+ Format your edits as:
161
+ <<<<<<< SEARCH
162
+ exact text to find
163
+ =======
164
+ replacement text
165
+ >>>>>>> REPLACE
166
+
167
+ Multiple edit blocks can be applied in a single call. Each SEARCH block must match exactly once in the file.`,
168
+ parameters: {
169
+ path: { type: "string", description: "Path to the file to edit", required: true },
170
+ edits: { type: "string", description: "Edit blocks in SEARCH/REPLACE format", required: true },
171
+ },
172
+ category: "file",
173
+ requiresApproval: true,
174
+ handler: async (args, context): Promise<ToolResult> => {
175
+ const blocks = parseEditBlocks(args.edits as string);
176
+
177
+ if (blocks.length === 0) {
178
+ return {
179
+ success: false,
180
+ output: "",
181
+ error: "No valid edit blocks found. Use <<<<<<< SEARCH / ======= / >>>>>>> REPLACE format.",
182
+ };
183
+ }
184
+
185
+ const result = await applyEditBlocksToFile(args.path as string, blocks, context.workingDirectory);
186
+
187
+ return {
188
+ success: result.applied > 0,
189
+ output: `Applied ${result.applied}/${blocks.length} edit blocks to ${args.path}${result.errors.length > 0 ? "\nErrors:\n" + result.errors.join("\n") : ""}`,
190
+ data: result,
191
+ error: result.applied === 0 ? result.errors.join("; ") : undefined,
192
+ };
193
+ },
194
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ZERO PubSub Events System
3
+ * Adapted from: crush (Charm) - MIT
4
+ *
5
+ * Event-driven pub/sub system for inter-component communication.
6
+ */
7
+
8
+ export type EventHandler<T = unknown> = (data: T) => void | Promise<void>;
9
+
10
+ export interface EventDefinition<T = unknown> {
11
+ type: string;
12
+ description?: string;
13
+ }
14
+
15
+ export class EventBus {
16
+ private handlers = new Map<string, Set<EventHandler>>();
17
+ private history: Array<{ type: string; data: unknown; timestamp: number }> = [];
18
+ private maxHistory = 100;
19
+
20
+ /**
21
+ * Subscribe to an event
22
+ */
23
+ on<T = unknown>(eventType: string, handler: EventHandler<T>): () => void {
24
+ if (!this.handlers.has(eventType)) {
25
+ this.handlers.set(eventType, new Set());
26
+ }
27
+ this.handlers.get(eventType)!.add(handler as EventHandler);
28
+
29
+ // Return unsubscribe function
30
+ return () => {
31
+ this.handlers.get(eventType)?.delete(handler as EventHandler);
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Subscribe to an event (one-time)
37
+ */
38
+ once<T = unknown>(eventType: string, handler: EventHandler<T>): () => void {
39
+ const wrappedHandler: EventHandler<T> = async (data) => {
40
+ unsubscribe();
41
+ await handler(data);
42
+ };
43
+ const unsubscribe = this.on(eventType, wrappedHandler);
44
+ return unsubscribe;
45
+ }
46
+
47
+ /**
48
+ * Emit an event
49
+ */
50
+ async emit<T = unknown>(eventType: string, data: T): Promise<void> {
51
+ // Add to history
52
+ this.history.push({ type: eventType, data, timestamp: Date.now() });
53
+ if (this.history.length > this.maxHistory) {
54
+ this.history.shift();
55
+ }
56
+
57
+ // Call handlers
58
+ const handlers = this.handlers.get(eventType);
59
+ if (handlers) {
60
+ for (const handler of handlers) {
61
+ try {
62
+ await handler(data);
63
+ } catch (error) {
64
+ console.error(`Error in event handler for ${eventType}:`, error);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get event history
72
+ */
73
+ getHistory(eventType?: string): Array<{ type: string; data: unknown; timestamp: number }> {
74
+ if (eventType) {
75
+ return this.history.filter((e) => e.type === eventType);
76
+ }
77
+ return [...this.history];
78
+ }
79
+
80
+ /**
81
+ * Clear event history
82
+ */
83
+ clearHistory(): void {
84
+ this.history = [];
85
+ }
86
+
87
+ /**
88
+ * Remove all handlers for an event type
89
+ */
90
+ off(eventType: string): void {
91
+ this.handlers.delete(eventType);
92
+ }
93
+
94
+ /**
95
+ * Remove all handlers
96
+ */
97
+ removeAll(): void {
98
+ this.handlers.clear();
99
+ }
100
+ }
101
+
102
+ // ============================================================================
103
+ // Pre-defined Event Types
104
+ // ============================================================================
105
+
106
+ export const ZERO_EVENTS = {
107
+ // Session events
108
+ SESSION_CREATED: "session:created",
109
+ SESSION_UPDATED: "session:updated",
110
+ SESSION_DELETED: "session:deleted",
111
+
112
+ // Agent events
113
+ AGENT_STARTED: "agent:started",
114
+ AGENT_COMPLETED: "agent:completed",
115
+ AGENT_ERROR: "agent:error",
116
+
117
+ // Tool events
118
+ TOOL_CALL_START: "tool:call:start",
119
+ TOOL_CALL_COMPLETE: "tool:call:complete",
120
+ TOOL_CALL_ERROR: "tool:call:error",
121
+
122
+ // Message events
123
+ MESSAGE_USER: "message:user",
124
+ MESSAGE_ASSISTANT: "message:assistant",
125
+ MESSAGE_SYSTEM: "message:system",
126
+
127
+ // Permission events
128
+ PERMISSION_REQUESTED: "permission:requested",
129
+ PERMISSION_GRANTED: "permission:granted",
130
+ PERMISSION_DENIED: "permission:denied",
131
+
132
+ // MCP events
133
+ MCP_CONNECTED: "mcp:connected",
134
+ MCP_DISCONNECTED: "mcp:disconnected",
135
+ MCP_ERROR: "mcp:error",
136
+
137
+ // Git events
138
+ GIT_COMMIT: "git:commit",
139
+ GIT_BRANCH: "git:branch",
140
+ GIT_MERGE: "git:merge",
141
+
142
+ // File events
143
+ FILE_CHANGED: "file:changed",
144
+ FILE_CREATED: "file:created",
145
+ FILE_DELETED: "file:deleted",
146
+ } as const;
147
+
148
+ /**
149
+ * Global event bus instance
150
+ */
151
+ export const globalEventBus = new EventBus();