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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Agent Engine
|
|
3
|
+
* Core agent loop implementation combining best patterns from:
|
|
4
|
+
* - goose: sub-agent orchestration
|
|
5
|
+
* - gemini-cli: ReAct loop with tool execution
|
|
6
|
+
* - pi: multi-agent orchestrator
|
|
7
|
+
* - cline: tool approval flow
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AgentConfig,
|
|
12
|
+
AgentResult,
|
|
13
|
+
ChatOptions,
|
|
14
|
+
LLMClient,
|
|
15
|
+
Message,
|
|
16
|
+
MessageContent,
|
|
17
|
+
ToolCallContent,
|
|
18
|
+
ToolDefinition,
|
|
19
|
+
ToolResult,
|
|
20
|
+
TokenUsage,
|
|
21
|
+
PermissionHandler,
|
|
22
|
+
SessionState,
|
|
23
|
+
} from "../types.js";
|
|
24
|
+
|
|
25
|
+
export interface AgentEngineOptions {
|
|
26
|
+
config: AgentConfig;
|
|
27
|
+
client: LLMClient;
|
|
28
|
+
tools: Map<string, ToolDefinition>;
|
|
29
|
+
permissionHandler: PermissionHandler;
|
|
30
|
+
workingDirectory: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
onStateChange?: (state: SessionState) => void;
|
|
33
|
+
onMessage?: (message: Message) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class AgentEngine {
|
|
37
|
+
private config: AgentConfig;
|
|
38
|
+
private client: LLMClient;
|
|
39
|
+
private tools: Map<string, ToolDefinition>;
|
|
40
|
+
private permissionHandler: PermissionHandler;
|
|
41
|
+
private workingDirectory: string;
|
|
42
|
+
private sessionId: string;
|
|
43
|
+
private messages: Message[] = [];
|
|
44
|
+
private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
45
|
+
private turnCount = 0;
|
|
46
|
+
private aborted = false;
|
|
47
|
+
private abortController = new AbortController();
|
|
48
|
+
private onStateChange?: (state: SessionState) => void;
|
|
49
|
+
private onMessage?: (message: Message) => void;
|
|
50
|
+
|
|
51
|
+
constructor(options: AgentEngineOptions) {
|
|
52
|
+
this.config = options.config;
|
|
53
|
+
this.client = options.client;
|
|
54
|
+
this.tools = options.tools;
|
|
55
|
+
this.permissionHandler = options.permissionHandler;
|
|
56
|
+
this.workingDirectory = options.workingDirectory;
|
|
57
|
+
this.sessionId = options.sessionId;
|
|
58
|
+
this.onStateChange = options.onStateChange;
|
|
59
|
+
this.onMessage = options.onMessage;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run the agent loop with a user message
|
|
64
|
+
*/
|
|
65
|
+
async run(userMessage: string): Promise<AgentResult> {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
this.aborted = false;
|
|
68
|
+
this.turnCount = 0;
|
|
69
|
+
|
|
70
|
+
// Add user message
|
|
71
|
+
const userMsg: Message = {
|
|
72
|
+
id: this.generateId(),
|
|
73
|
+
role: "user",
|
|
74
|
+
content: [{ type: "text", text: userMessage }],
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
};
|
|
77
|
+
this.messages.push(userMsg);
|
|
78
|
+
this.onMessage?.(userMsg);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Agent loop (ReAct pattern)
|
|
82
|
+
while (this.turnCount < this.config.maxTurns && !this.aborted) {
|
|
83
|
+
this.turnCount++;
|
|
84
|
+
this.updateState({ status: "running", currentAgent: this.config.name, tokenUsage: this.totalUsage });
|
|
85
|
+
|
|
86
|
+
// Get available tools for this agent
|
|
87
|
+
const availableTools = this.getAvailableTools();
|
|
88
|
+
|
|
89
|
+
// Call LLM
|
|
90
|
+
const chatOptions: ChatOptions = {
|
|
91
|
+
tools: availableTools.length > 0 ? availableTools : undefined,
|
|
92
|
+
systemPrompt: this.config.systemPrompt,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const response = await this.client.chat(this.messages, chatOptions);
|
|
96
|
+
|
|
97
|
+
// Track usage
|
|
98
|
+
this.totalUsage.inputTokens += response.usage.inputTokens;
|
|
99
|
+
this.totalUsage.outputTokens += response.usage.outputTokens;
|
|
100
|
+
|
|
101
|
+
// Add assistant message
|
|
102
|
+
const assistantMsg: Message = {
|
|
103
|
+
id: this.generateId(),
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: response.content,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
metadata: { model: response.model, usage: response.usage },
|
|
108
|
+
};
|
|
109
|
+
this.messages.push(assistantMsg);
|
|
110
|
+
this.onMessage?.(assistantMsg);
|
|
111
|
+
|
|
112
|
+
// Check if we should stop
|
|
113
|
+
if (response.stopReason === "end_turn" || response.stopReason === "max_tokens") {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Process tool calls
|
|
118
|
+
if (response.stopReason === "tool_use") {
|
|
119
|
+
const toolCalls = response.content.filter(
|
|
120
|
+
(c): c is ToolCallContent => c.type === "tool_call"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
for (const toolCall of toolCalls) {
|
|
124
|
+
const result = await this.executeTool(toolCall);
|
|
125
|
+
const toolResultMsg: Message = {
|
|
126
|
+
id: this.generateId(),
|
|
127
|
+
role: "tool",
|
|
128
|
+
content: [{
|
|
129
|
+
type: "tool_result",
|
|
130
|
+
id: toolCall.id,
|
|
131
|
+
name: toolCall.name,
|
|
132
|
+
result: result.output,
|
|
133
|
+
error: result.error,
|
|
134
|
+
}],
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
};
|
|
137
|
+
this.messages.push(toolResultMsg);
|
|
138
|
+
this.onMessage?.(toolResultMsg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const duration = Date.now() - startTime;
|
|
144
|
+
const result: AgentResult = {
|
|
145
|
+
success: !this.aborted,
|
|
146
|
+
messages: this.messages,
|
|
147
|
+
tokenUsage: this.totalUsage,
|
|
148
|
+
duration,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.updateState({ status: "completed", tokenUsage: this.totalUsage });
|
|
152
|
+
return result;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const duration = Date.now() - startTime;
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
156
|
+
this.updateState({ status: "error", tokenUsage: this.totalUsage, error: errorMessage });
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
messages: this.messages,
|
|
160
|
+
tokenUsage: this.totalUsage,
|
|
161
|
+
duration,
|
|
162
|
+
error: errorMessage,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Execute a single tool call with permission checking
|
|
169
|
+
*/
|
|
170
|
+
private async executeTool(toolCall: ToolCallContent): Promise<ToolResult> {
|
|
171
|
+
const tool = this.tools.get(toolCall.name);
|
|
172
|
+
|
|
173
|
+
if (!tool) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
output: "",
|
|
177
|
+
error: `Tool "${toolCall.name}" not found`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check permissions if tool requires approval
|
|
182
|
+
if (tool.requiresApproval) {
|
|
183
|
+
this.updateState({ status: "waiting_approval", tokenUsage: this.totalUsage });
|
|
184
|
+
|
|
185
|
+
const decision = await this.permissionHandler({
|
|
186
|
+
type: this.getPermissionType(tool.category),
|
|
187
|
+
resource: toolCall.name,
|
|
188
|
+
details: JSON.stringify(toolCall.arguments),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (decision === "deny") {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
output: "",
|
|
195
|
+
error: "Permission denied by user",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.updateState({ status: "running", tokenUsage: this.totalUsage });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Execute the tool
|
|
203
|
+
try {
|
|
204
|
+
const result = await tool.handler(toolCall.arguments, {
|
|
205
|
+
sessionId: this.sessionId,
|
|
206
|
+
workingDirectory: this.workingDirectory,
|
|
207
|
+
permissions: {
|
|
208
|
+
fileRead: "allow",
|
|
209
|
+
fileWrite: tool.requiresApproval ? "allow" : "ask",
|
|
210
|
+
shellExecution: tool.requiresApproval ? "allow" : "ask",
|
|
211
|
+
networkAccess: "allow",
|
|
212
|
+
mcpAccess: "allow",
|
|
213
|
+
},
|
|
214
|
+
abortSignal: this.abortController.signal,
|
|
215
|
+
});
|
|
216
|
+
return result;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
output: "",
|
|
221
|
+
error: error instanceof Error ? error.message : String(error),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get tools available to this agent
|
|
228
|
+
*/
|
|
229
|
+
private getAvailableTools(): ToolDefinition[] {
|
|
230
|
+
return this.config.tools
|
|
231
|
+
.map((name) => this.tools.get(name))
|
|
232
|
+
.filter((t): t is ToolDefinition => t !== undefined);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private getPermissionType(category: string): "file_read" | "file_write" | "shell" | "network" | "mcp" {
|
|
236
|
+
switch (category) {
|
|
237
|
+
case "file": return "file_write";
|
|
238
|
+
case "shell": return "shell";
|
|
239
|
+
case "web": return "network";
|
|
240
|
+
case "mcp": return "mcp";
|
|
241
|
+
default: return "file_read";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
abort(): void {
|
|
246
|
+
this.aborted = true;
|
|
247
|
+
this.abortController.abort();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getMessages(): Message[] {
|
|
251
|
+
return [...this.messages];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private updateState(partial: Partial<SessionState>): void {
|
|
255
|
+
this.onStateChange?.({
|
|
256
|
+
status: partial.status ?? "idle",
|
|
257
|
+
currentAgent: partial.currentAgent ?? this.config.name,
|
|
258
|
+
tokenUsage: partial.tokenUsage ?? this.totalUsage,
|
|
259
|
+
error: partial.error,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private generateId(): string {
|
|
264
|
+
return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Tool Registry
|
|
3
|
+
* Manages all available tools for agents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ToolDefinition, ToolCategory } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export class ToolRegistry {
|
|
9
|
+
private tools = new Map<string, ToolDefinition>();
|
|
10
|
+
|
|
11
|
+
register(tool: ToolDefinition): void {
|
|
12
|
+
this.tools.set(tool.name, tool);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
unregister(name: string): void {
|
|
16
|
+
this.tools.delete(name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(name: string): ToolDefinition | undefined {
|
|
20
|
+
return this.tools.get(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getAll(): ToolDefinition[] {
|
|
24
|
+
return Array.from(this.tools.values());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getByCategory(category: ToolCategory): ToolDefinition[] {
|
|
28
|
+
return this.getAll().filter((t) => t.category === category);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getMap(): Map<string, ToolDefinition> {
|
|
32
|
+
return new Map(this.tools);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
has(name: string): boolean {
|
|
36
|
+
return this.tools.has(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
names(): string[] {
|
|
40
|
+
return Array.from(this.tools.keys());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load all built-in tools
|
|
45
|
+
*/
|
|
46
|
+
loadBuiltins(): void {
|
|
47
|
+
const builtins = getBuiltinTools();
|
|
48
|
+
for (const tool of builtins) {
|
|
49
|
+
this.register(tool);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Lazy import to avoid circular deps - tools are loaded at runtime
|
|
55
|
+
function getBuiltinTools(): ToolDefinition[] {
|
|
56
|
+
// Will be populated by the tools/index.ts
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Agent Registry
|
|
3
|
+
* Manages multiple agents with orchestration capabilities
|
|
4
|
+
* Inspired by: goose (sub-agents), pi (orchestrator), gemini-cli (a2a)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig, AgentResult, LLMClient, PermissionHandler, ToolDefinition } from "../types.js";
|
|
8
|
+
import { AgentEngine } from "./engine.js";
|
|
9
|
+
|
|
10
|
+
export interface AgentRegistryOptions {
|
|
11
|
+
clientFactory: (model: string) => LLMClient;
|
|
12
|
+
tools: Map<string, ToolDefinition>;
|
|
13
|
+
permissionHandler: PermissionHandler;
|
|
14
|
+
workingDirectory: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AgentRegistry {
|
|
19
|
+
private agents = new Map<string, AgentConfig>();
|
|
20
|
+
private clientFactory: (model: string) => LLMClient;
|
|
21
|
+
private tools: Map<string, ToolDefinition>;
|
|
22
|
+
private permissionHandler: PermissionHandler;
|
|
23
|
+
private workingDirectory: string;
|
|
24
|
+
private sessionId: string;
|
|
25
|
+
|
|
26
|
+
constructor(options: AgentRegistryOptions) {
|
|
27
|
+
this.clientFactory = options.clientFactory;
|
|
28
|
+
this.tools = options.tools;
|
|
29
|
+
this.permissionHandler = options.permissionHandler;
|
|
30
|
+
this.workingDirectory = options.workingDirectory;
|
|
31
|
+
this.sessionId = options.sessionId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
register(config: AgentConfig): void {
|
|
35
|
+
this.agents.set(config.id, config);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
unregister(id: string): void {
|
|
39
|
+
this.agents.delete(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get(id: string): AgentConfig | undefined {
|
|
43
|
+
return this.agents.get(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
list(): AgentConfig[] {
|
|
47
|
+
return Array.from(this.agents.values());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an engine for a specific agent
|
|
52
|
+
*/
|
|
53
|
+
createEngine(agentId: string, options?: {
|
|
54
|
+
onStateChange?: AgentEngine extends { onStateChange: infer T } ? T : never;
|
|
55
|
+
onMessage?: AgentEngine extends { onMessage: infer T } ? T : never;
|
|
56
|
+
}): AgentEngine {
|
|
57
|
+
const config = this.agents.get(agentId);
|
|
58
|
+
if (!config) {
|
|
59
|
+
throw new Error(`Agent "${agentId}" not found in registry`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const client = this.clientFactory(config.model);
|
|
63
|
+
|
|
64
|
+
return new AgentEngine({
|
|
65
|
+
config,
|
|
66
|
+
client,
|
|
67
|
+
tools: this.tools,
|
|
68
|
+
permissionHandler: this.permissionHandler,
|
|
69
|
+
workingDirectory: this.workingDirectory,
|
|
70
|
+
sessionId: this.sessionId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Orchestrate a task across multiple agents
|
|
76
|
+
* The orchestrator agent delegates to sub-agents as needed
|
|
77
|
+
*/
|
|
78
|
+
async orchestrate(orchestratorId: string, task: string): Promise<AgentResult> {
|
|
79
|
+
const orchestrator = this.agents.get(orchestratorId);
|
|
80
|
+
if (!orchestrator) {
|
|
81
|
+
throw new Error(`Orchestrator agent "${orchestratorId}" not found`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create delegation tools for sub-agents
|
|
85
|
+
const delegateTools = new Map(this.tools);
|
|
86
|
+
|
|
87
|
+
// Add a "delegate" tool that lets the orchestrator call sub-agents
|
|
88
|
+
if (orchestrator.subAgents && orchestrator.subAgents.length > 0) {
|
|
89
|
+
const delegateTool: ToolDefinition = {
|
|
90
|
+
name: "delegate_to_agent",
|
|
91
|
+
description: `Delegate a sub-task to another agent. Available agents: ${orchestrator.subAgents.join(", ")}`,
|
|
92
|
+
parameters: {
|
|
93
|
+
agentId: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "ID of the agent to delegate to",
|
|
96
|
+
required: true,
|
|
97
|
+
enum: orchestrator.subAgents,
|
|
98
|
+
},
|
|
99
|
+
task: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "The task to delegate",
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
category: "system",
|
|
106
|
+
requiresApproval: false,
|
|
107
|
+
handler: async (args) => {
|
|
108
|
+
const subAgentId = args.agentId as string;
|
|
109
|
+
const subTask = args.task as string;
|
|
110
|
+
const subConfig = this.agents.get(subAgentId);
|
|
111
|
+
|
|
112
|
+
if (!subConfig) {
|
|
113
|
+
return { success: false, output: "", error: `Agent "${subAgentId}" not found` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const subEngine = this.createEngine(subAgentId);
|
|
117
|
+
const result = await subEngine.run(subTask);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: result.success,
|
|
121
|
+
output: result.messages
|
|
122
|
+
.filter((m) => m.role === "assistant")
|
|
123
|
+
.map((m) => m.content.filter((c) => c.type === "text").map((c) => "text" in c ? c.text : "").join(""))
|
|
124
|
+
.join("\n"),
|
|
125
|
+
data: {
|
|
126
|
+
tokenUsage: result.tokenUsage,
|
|
127
|
+
duration: result.duration,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
delegateTools.set("delegate_to_agent", delegateTool);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const engine = new AgentEngine({
|
|
137
|
+
config: orchestrator,
|
|
138
|
+
client: this.clientFactory(orchestrator.model),
|
|
139
|
+
tools: delegateTools,
|
|
140
|
+
permissionHandler: this.permissionHandler,
|
|
141
|
+
workingDirectory: this.workingDirectory,
|
|
142
|
+
sessionId: this.sessionId,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return engine.run(task);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Built-in Agent Configurations
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
export const DEFAULT_AGENTS: AgentConfig[] = [
|
|
154
|
+
{
|
|
155
|
+
id: "coder",
|
|
156
|
+
name: "Coder",
|
|
157
|
+
description: "Primary coding agent that handles file editing, code generation, and refactoring",
|
|
158
|
+
systemPrompt: `You are ZERO, an expert AI coding assistant. You help users with software development tasks.
|
|
159
|
+
|
|
160
|
+
Your capabilities:
|
|
161
|
+
- Read, write, and edit files
|
|
162
|
+
- Execute shell commands
|
|
163
|
+
- Search codebases
|
|
164
|
+
- Use git for version control
|
|
165
|
+
- Delegate to specialized sub-agents when needed
|
|
166
|
+
|
|
167
|
+
Guidelines:
|
|
168
|
+
- Always read relevant files before making changes
|
|
169
|
+
- Write clean, well-documented code
|
|
170
|
+
- Follow the existing code style and patterns
|
|
171
|
+
- Test your changes when possible
|
|
172
|
+
- Explain your reasoning when asked`,
|
|
173
|
+
tools: [
|
|
174
|
+
"read_file", "write_file", "edit_file", "list_directory",
|
|
175
|
+
"search_files", "search_code", "execute_shell",
|
|
176
|
+
"git_status", "git_diff", "git_commit",
|
|
177
|
+
],
|
|
178
|
+
maxTurns: 50,
|
|
179
|
+
model: "default",
|
|
180
|
+
subAgents: ["reviewer", "researcher"],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "reviewer",
|
|
184
|
+
name: "Code Reviewer",
|
|
185
|
+
description: "Specialized agent for code review and quality analysis",
|
|
186
|
+
systemPrompt: `You are ZERO's code review specialist. You analyze code for:
|
|
187
|
+
- Bugs and potential issues
|
|
188
|
+
- Security vulnerabilities
|
|
189
|
+
- Performance problems
|
|
190
|
+
- Code style and best practices
|
|
191
|
+
- Test coverage gaps
|
|
192
|
+
|
|
193
|
+
Provide clear, actionable feedback with specific line references.`,
|
|
194
|
+
tools: ["read_file", "search_code", "search_files", "list_directory", "git_diff"],
|
|
195
|
+
maxTurns: 20,
|
|
196
|
+
model: "default",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "researcher",
|
|
200
|
+
name: "Researcher",
|
|
201
|
+
description: "Agent specialized in searching documentation and web resources",
|
|
202
|
+
systemPrompt: `You are ZERO's research specialist. You help find:
|
|
203
|
+
- Documentation for libraries and frameworks
|
|
204
|
+
- Solutions to error messages
|
|
205
|
+
- Best practices and design patterns
|
|
206
|
+
- API references and examples
|
|
207
|
+
|
|
208
|
+
Always provide sources and links when available.`,
|
|
209
|
+
tools: ["search_files", "search_code", "read_file", "web_search", "web_fetch"],
|
|
210
|
+
maxTurns: 15,
|
|
211
|
+
model: "default",
|
|
212
|
+
},
|
|
213
|
+
];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Custom Commands System
|
|
3
|
+
* Adapted from: crush (Charm/Go) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Loads custom commands from markdown files with YAML frontmatter.
|
|
6
|
+
* Supports user, project, and MCP-sourced commands.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { parseFrontmatter } from "../agent/config-loader.js";
|
|
12
|
+
|
|
13
|
+
export interface CommandArgument {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CustomCommand {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
content: string;
|
|
25
|
+
arguments: CommandArgument[];
|
|
26
|
+
source: "user" | "project" | "mcp";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load custom commands from a directory of markdown files
|
|
31
|
+
*/
|
|
32
|
+
export async function loadCommandsFromDir(dirPath: string, source: "user" | "project" = "project"): Promise<CustomCommand[]> {
|
|
33
|
+
const commands: CustomCommand[] = [];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const filePath = path.join(dirPath, entry.name);
|
|
43
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
44
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
45
|
+
|
|
46
|
+
const name = (frontmatter.name as string) || path.basename(entry.name, ".md");
|
|
47
|
+
const description = (frontmatter.description as string) || "";
|
|
48
|
+
|
|
49
|
+
// Parse arguments from frontmatter
|
|
50
|
+
const args: CommandArgument[] = [];
|
|
51
|
+
const rawArgs = frontmatter.arguments as any;
|
|
52
|
+
if (Array.isArray(rawArgs)) {
|
|
53
|
+
for (const arg of rawArgs) {
|
|
54
|
+
args.push({
|
|
55
|
+
id: arg.id || arg.name,
|
|
56
|
+
title: arg.title || arg.id || arg.name,
|
|
57
|
+
description: arg.description || "",
|
|
58
|
+
required: arg.required !== false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
commands.push({
|
|
64
|
+
id: `${source}:${name}`,
|
|
65
|
+
name,
|
|
66
|
+
description,
|
|
67
|
+
content: body.trim(),
|
|
68
|
+
arguments: args,
|
|
69
|
+
source,
|
|
70
|
+
});
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
|
|
75
|
+
return commands;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load all commands from standard locations
|
|
80
|
+
*/
|
|
81
|
+
export async function loadAllCommands(workingDir: string): Promise<CustomCommand[]> {
|
|
82
|
+
const commands: CustomCommand[] = [];
|
|
83
|
+
|
|
84
|
+
// Project commands: .zero/commands/
|
|
85
|
+
const projectCmds = await loadCommandsFromDir(
|
|
86
|
+
path.join(workingDir, ".zero", "commands"),
|
|
87
|
+
"project"
|
|
88
|
+
);
|
|
89
|
+
commands.push(...projectCmds);
|
|
90
|
+
|
|
91
|
+
// User commands: ~/.config/zero/commands/
|
|
92
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
93
|
+
if (homeDir) {
|
|
94
|
+
const userCmds = await loadCommandsFromDir(
|
|
95
|
+
path.join(homeDir, ".config", "zero", "commands"),
|
|
96
|
+
"user"
|
|
97
|
+
);
|
|
98
|
+
commands.push(...userCmds);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return commands;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a custom command by substituting arguments
|
|
106
|
+
*/
|
|
107
|
+
export function executeCommand(command: CustomCommand, args: Record<string, string>): string {
|
|
108
|
+
let content = command.content;
|
|
109
|
+
|
|
110
|
+
// Replace $ARG_NAME patterns with values
|
|
111
|
+
for (const [key, value] of Object.entries(args)) {
|
|
112
|
+
content = content.replace(new RegExp(`\\$${key.toUpperCase()}`, "g"), value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return content;
|
|
116
|
+
}
|