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.
- package/MERGE_REPORT.md +265 -0
- package/README.md +127 -0
- package/package.json +55 -0
- package/packages/cli/src/index.ts +271 -0
- package/packages/cli/src/interactive.ts +190 -0
- package/packages/cli/src/once.ts +50 -0
- package/packages/core/src/agent/config-loader.ts +174 -0
- package/packages/core/src/agent/engine.ts +266 -0
- package/packages/core/src/agent/registry-tools.ts +58 -0
- package/packages/core/src/agent/registry.ts +213 -0
- package/packages/core/src/commands/index.ts +116 -0
- package/packages/core/src/config/index.ts +139 -0
- package/packages/core/src/confirmation/message-bus.ts +206 -0
- package/packages/core/src/diff/index.ts +232 -0
- package/packages/core/src/diff-detect/index.ts +207 -0
- package/packages/core/src/edit-coder/index.ts +194 -0
- package/packages/core/src/events/index.ts +151 -0
- package/packages/core/src/file-tracker/index.ts +183 -0
- package/packages/core/src/git/index.ts +305 -0
- package/packages/core/src/history/index.ts +236 -0
- package/packages/core/src/image/index.ts +131 -0
- package/packages/core/src/index.ts +131 -0
- package/packages/core/src/lsp/index.ts +251 -0
- package/packages/core/src/mcp/index.ts +186 -0
- package/packages/core/src/memory/index.ts +141 -0
- package/packages/core/src/memory/prompts.ts +52 -0
- package/packages/core/src/orchestrator/protocol.ts +140 -0
- package/packages/core/src/orchestrator/server.ts +319 -0
- package/packages/core/src/output/index.ts +164 -0
- package/packages/core/src/permissions/index.ts +126 -0
- package/packages/core/src/prompts/engine.ts +188 -0
- package/packages/core/src/providers/registry.ts +687 -0
- package/packages/core/src/routing/model-router.ts +160 -0
- package/packages/core/src/server/index.ts +232 -0
- package/packages/core/src/session/index.ts +174 -0
- package/packages/core/src/session/service.ts +322 -0
- package/packages/core/src/skills/index.ts +205 -0
- package/packages/core/src/streaming/index.ts +166 -0
- package/packages/core/src/sub-agent/index.ts +179 -0
- package/packages/core/src/tools/builtin.ts +568 -0
- package/packages/core/src/types.ts +356 -0
- package/packages/sdk/src/index.ts +247 -0
- package/packages/tui/src/index.ts +141 -0
- 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();
|