@c4ccz/zero 1.0.0 → 1.0.1
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/package.json +10 -7
- 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/extended.ts +8 -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/tools/linter.ts +26 -0
- package/packages/core/src/tools/repo-map.ts +11 -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/bun.lock +0 -35
- package/bunfig.toml +0 -6
- package/packages/cli/package.json +0 -23
- package/packages/core/package.json +0 -41
- package/packages/sdk/package.json +0 -16
- package/packages/tui/package.json +0 -19
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Configuration System
|
|
3
|
+
* Handles loading, validating, and managing ZERO configuration
|
|
4
|
+
*
|
|
5
|
+
* Sources:
|
|
6
|
+
* - gemini-cli: config with extensions
|
|
7
|
+
* - crush: JSON config with schema
|
|
8
|
+
* - opencode: configuration management
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import type { ZeroConfig } from "../types.js";
|
|
14
|
+
|
|
15
|
+
const CONFIG_FILE_NAME = "zero.config.json";
|
|
16
|
+
const ZERO_DIR = ".zero";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CONFIG: ZeroConfig = {
|
|
19
|
+
version: 1,
|
|
20
|
+
defaultProvider: "openai",
|
|
21
|
+
defaultModel: "gpt-4o",
|
|
22
|
+
providers: {
|
|
23
|
+
openai: {
|
|
24
|
+
type: "openai",
|
|
25
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
26
|
+
},
|
|
27
|
+
anthropic: {
|
|
28
|
+
type: "anthropic",
|
|
29
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
30
|
+
},
|
|
31
|
+
google: {
|
|
32
|
+
type: "google",
|
|
33
|
+
apiKey: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY,
|
|
34
|
+
},
|
|
35
|
+
ollama: {
|
|
36
|
+
type: "ollama",
|
|
37
|
+
baseUrl: "http://localhost:11434/v1",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
permissions: {
|
|
41
|
+
fileRead: "allow",
|
|
42
|
+
fileWrite: "ask",
|
|
43
|
+
shellExecution: "ask",
|
|
44
|
+
networkAccess: "ask",
|
|
45
|
+
mcpAccess: "allow",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class ConfigManager {
|
|
50
|
+
private config: ZeroConfig;
|
|
51
|
+
private configPath: string;
|
|
52
|
+
private projectPath: string;
|
|
53
|
+
|
|
54
|
+
constructor(projectPath: string) {
|
|
55
|
+
this.projectPath = projectPath;
|
|
56
|
+
this.configPath = path.join(projectPath, CONFIG_FILE_NAME);
|
|
57
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load configuration from file
|
|
62
|
+
*/
|
|
63
|
+
async load(): Promise<ZeroConfig> {
|
|
64
|
+
try {
|
|
65
|
+
const data = await fs.readFile(this.configPath, "utf-8");
|
|
66
|
+
const loaded = JSON.parse(data);
|
|
67
|
+
this.config = { ...DEFAULT_CONFIG, ...loaded };
|
|
68
|
+
} catch {
|
|
69
|
+
// Use defaults if config file doesn't exist
|
|
70
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also check for .zero/ directory config
|
|
74
|
+
try {
|
|
75
|
+
const zeroDirConfig = path.join(this.projectPath, ZERO_DIR, "config.json");
|
|
76
|
+
const data = await fs.readFile(zeroDirConfig, "utf-8");
|
|
77
|
+
const loaded = JSON.parse(data);
|
|
78
|
+
this.config = { ...this.config, ...loaded };
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
return this.config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Save configuration to file
|
|
86
|
+
*/
|
|
87
|
+
async save(): Promise<void> {
|
|
88
|
+
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get current configuration
|
|
93
|
+
*/
|
|
94
|
+
get(): ZeroConfig {
|
|
95
|
+
return { ...this.config };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update configuration
|
|
100
|
+
*/
|
|
101
|
+
update(partial: Partial<ZeroConfig>): ZeroConfig {
|
|
102
|
+
this.config = { ...this.config, ...partial };
|
|
103
|
+
return this.config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the ZERO data directory path
|
|
108
|
+
*/
|
|
109
|
+
getDataDir(): string {
|
|
110
|
+
return path.join(this.projectPath, ZERO_DIR);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Initialize ZERO project structure
|
|
115
|
+
*/
|
|
116
|
+
async init(): Promise<void> {
|
|
117
|
+
const zeroDir = this.getDataDir();
|
|
118
|
+
await fs.mkdir(zeroDir, { recursive: true });
|
|
119
|
+
await fs.mkdir(path.join(zeroDir, "sessions"), { recursive: true });
|
|
120
|
+
await fs.mkdir(path.join(zeroDir, "memory"), { recursive: true });
|
|
121
|
+
await fs.mkdir(path.join(zeroDir, "logs"), { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Create default config
|
|
124
|
+
await this.save();
|
|
125
|
+
|
|
126
|
+
// Create .gitignore for .zero directory
|
|
127
|
+
const gitignore = path.join(zeroDir, ".gitignore");
|
|
128
|
+
try {
|
|
129
|
+
await fs.writeFile(gitignore, "sessions/\nmemory/\nlogs/\n*.log\n");
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve environment variables in config
|
|
135
|
+
*/
|
|
136
|
+
resolveEnv(value: string): string {
|
|
137
|
+
return value.replace(/\$\{(\w+)\}/g, (_, key) => process.env[key] || "");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Message Bus
|
|
3
|
+
* Adapted from: qwen-code (Alibaba) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Event-driven message bus for tool confirmations and hook execution.
|
|
6
|
+
* Supports request-response patterns with correlation IDs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
|
|
12
|
+
export enum MessageBusType {
|
|
13
|
+
TOOL_CONFIRMATION_REQUEST = "tool-confirmation-request",
|
|
14
|
+
TOOL_CONFIRMATION_RESPONSE = "tool-confirmation-response",
|
|
15
|
+
TOOL_EXECUTION_SUCCESS = "tool-execution-success",
|
|
16
|
+
TOOL_EXECUTION_FAILURE = "tool-execution-failure",
|
|
17
|
+
HOOK_EXECUTION_REQUEST = "hook-execution-request",
|
|
18
|
+
HOOK_EXECUTION_RESPONSE = "hook-execution-response",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ToolConfirmationRequest {
|
|
22
|
+
type: MessageBusType.TOOL_CONFIRMATION_REQUEST;
|
|
23
|
+
toolName: string;
|
|
24
|
+
toolArgs: Record<string, unknown>;
|
|
25
|
+
correlationId: string;
|
|
26
|
+
details?: SerializableConfirmationDetails;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToolConfirmationResponse {
|
|
30
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE;
|
|
31
|
+
correlationId: string;
|
|
32
|
+
confirmed: boolean;
|
|
33
|
+
payload?: unknown;
|
|
34
|
+
requiresUserConfirmation?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SerializableConfirmationDetails =
|
|
38
|
+
| {
|
|
39
|
+
type: "info";
|
|
40
|
+
title: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
urls?: string[];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: "edit";
|
|
46
|
+
title: string;
|
|
47
|
+
fileName: string;
|
|
48
|
+
filePath: string;
|
|
49
|
+
fileDiff: string;
|
|
50
|
+
originalContent: string | null;
|
|
51
|
+
newContent: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: "exec";
|
|
55
|
+
title: string;
|
|
56
|
+
command: string;
|
|
57
|
+
rootCommand: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
type: "mcp";
|
|
61
|
+
title: string;
|
|
62
|
+
serverName: string;
|
|
63
|
+
toolName: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface ToolExecutionSuccess<T = unknown> {
|
|
67
|
+
type: MessageBusType.TOOL_EXECUTION_SUCCESS;
|
|
68
|
+
toolName: string;
|
|
69
|
+
result: T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ToolExecutionFailure {
|
|
73
|
+
type: MessageBusType.TOOL_EXECUTION_FAILURE;
|
|
74
|
+
toolName: string;
|
|
75
|
+
error: Error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface HookExecutionRequest {
|
|
79
|
+
type: MessageBusType.HOOK_EXECUTION_REQUEST;
|
|
80
|
+
eventName: string;
|
|
81
|
+
input: Record<string, unknown>;
|
|
82
|
+
correlationId: string;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HookExecutionResponse {
|
|
87
|
+
type: MessageBusType.HOOK_EXECUTION_RESPONSE;
|
|
88
|
+
correlationId: string;
|
|
89
|
+
success: boolean;
|
|
90
|
+
output?: Record<string, unknown>;
|
|
91
|
+
error?: Error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Message =
|
|
95
|
+
| ToolConfirmationRequest
|
|
96
|
+
| ToolConfirmationResponse
|
|
97
|
+
| ToolExecutionSuccess
|
|
98
|
+
| ToolExecutionFailure
|
|
99
|
+
| HookExecutionRequest
|
|
100
|
+
| HookExecutionResponse;
|
|
101
|
+
|
|
102
|
+
export class MessageBus extends EventEmitter {
|
|
103
|
+
private isValidMessage(message: Message): boolean {
|
|
104
|
+
if (!message || !message.type) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST &&
|
|
110
|
+
!("correlationId" in message)
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async publish(message: Message): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
if (!this.isValidMessage(message)) {
|
|
121
|
+
throw new Error(`Invalid message structure: ${JSON.stringify(message)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
|
|
125
|
+
// Auto-confirm by default
|
|
126
|
+
this.emit(MessageBusType.TOOL_CONFIRMATION_RESPONSE, {
|
|
127
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
128
|
+
correlationId: message.correlationId,
|
|
129
|
+
confirmed: true,
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
this.emit(message.type, message);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.emit("error", error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
subscribe<T extends Message>(
|
|
140
|
+
type: T["type"],
|
|
141
|
+
listener: (message: T) => void
|
|
142
|
+
): void {
|
|
143
|
+
this.on(type, listener);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
unsubscribe<T extends Message>(
|
|
147
|
+
type: T["type"],
|
|
148
|
+
listener: (message: T) => void
|
|
149
|
+
): void {
|
|
150
|
+
this.off(type, listener);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Request-response pattern: Publish a message and wait for a correlated response
|
|
155
|
+
*/
|
|
156
|
+
async request<TRequest extends Message, TResponse extends Message>(
|
|
157
|
+
request: Omit<TRequest, "correlationId">,
|
|
158
|
+
responseType: TResponse["type"],
|
|
159
|
+
timeoutMs: number = 60000,
|
|
160
|
+
signal?: AbortSignal
|
|
161
|
+
): Promise<TResponse> {
|
|
162
|
+
const correlationId = randomUUID();
|
|
163
|
+
|
|
164
|
+
return new Promise<TResponse>((resolve, reject) => {
|
|
165
|
+
if (signal?.aborted) {
|
|
166
|
+
reject(new Error("Request aborted"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const timeoutId = setTimeout(() => {
|
|
171
|
+
cleanup();
|
|
172
|
+
reject(new Error(`Request timed out waiting for ${responseType}`));
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
|
|
175
|
+
const cleanup = () => {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
this.unsubscribe(responseType, responseHandler);
|
|
178
|
+
if (signal) {
|
|
179
|
+
signal.removeEventListener("abort", abortHandler);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const abortHandler = () => {
|
|
184
|
+
cleanup();
|
|
185
|
+
reject(new Error("Request aborted"));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (signal) {
|
|
189
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const responseHandler = (response: TResponse) => {
|
|
193
|
+
if (
|
|
194
|
+
"correlationId" in response &&
|
|
195
|
+
response.correlationId === correlationId
|
|
196
|
+
) {
|
|
197
|
+
cleanup();
|
|
198
|
+
resolve(response);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
this.subscribe<TResponse>(responseType, responseHandler);
|
|
203
|
+
this.publish({ ...request, correlationId } as TRequest);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|