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,160 @@
1
+ /**
2
+ * ZERO Model Router Service
3
+ * Adapted from: gemini-cli (Google) - Apache 2.0
4
+ *
5
+ * A centralized service for making intelligent model routing decisions.
6
+ * Supports multiple routing strategies with fallback chains.
7
+ */
8
+
9
+ import type { Message } from "../types.js";
10
+
11
+ export interface RoutingDecision {
12
+ model: string;
13
+ metadata: {
14
+ source: string;
15
+ latencyMs: number;
16
+ reasoning: string;
17
+ error?: string;
18
+ };
19
+ }
20
+
21
+ export interface RoutingContext {
22
+ history: readonly Message[];
23
+ request: string;
24
+ signal: AbortSignal;
25
+ requestedModel?: string;
26
+ }
27
+
28
+ export interface RoutingStrategy {
29
+ readonly name: string;
30
+ route(context: RoutingContext): Promise<RoutingDecision | null>;
31
+ }
32
+
33
+ export interface TerminalStrategy extends RoutingStrategy {
34
+ route(context: RoutingContext): Promise<RoutingDecision>;
35
+ }
36
+
37
+ /**
38
+ * Default routing strategy that always returns the configured model
39
+ */
40
+ export class DefaultStrategy implements TerminalStrategy {
41
+ readonly name = "default";
42
+
43
+ constructor(private defaultModel: string) {}
44
+
45
+ async route(context: RoutingContext): Promise<RoutingDecision> {
46
+ return {
47
+ model: context.requestedModel || this.defaultModel,
48
+ metadata: {
49
+ source: "default",
50
+ latencyMs: 0,
51
+ reasoning: "Default model selection",
52
+ },
53
+ };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Override strategy that forces a specific model
59
+ */
60
+ export class OverrideStrategy implements RoutingStrategy {
61
+ readonly name = "override";
62
+
63
+ constructor(private overrideModel?: string) {}
64
+
65
+ async route(context: RoutingContext): Promise<RoutingDecision | null> {
66
+ if (!this.overrideModel) return null;
67
+
68
+ return {
69
+ model: this.overrideModel,
70
+ metadata: {
71
+ source: "override",
72
+ latencyMs: 0,
73
+ reasoning: `Override model: ${this.overrideModel}`,
74
+ },
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Composite strategy that chains multiple strategies
81
+ */
82
+ export class CompositeStrategy implements TerminalStrategy {
83
+ readonly name = "composite";
84
+
85
+ constructor(
86
+ private strategies: (RoutingStrategy | TerminalStrategy)[],
87
+ private fallbackModel: string
88
+ ) {}
89
+
90
+ async route(context: RoutingContext): Promise<RoutingDecision> {
91
+ const startTime = Date.now();
92
+
93
+ for (const strategy of this.strategies) {
94
+ try {
95
+ const decision = await strategy.route(context);
96
+ if (decision) {
97
+ return {
98
+ ...decision,
99
+ metadata: {
100
+ ...decision.metadata,
101
+ latencyMs: Date.now() - startTime,
102
+ },
103
+ };
104
+ }
105
+ } catch (error) {
106
+ // Continue to next strategy
107
+ }
108
+ }
109
+
110
+ // Fallback
111
+ return {
112
+ model: this.fallbackModel,
113
+ metadata: {
114
+ source: "composite-fallback",
115
+ latencyMs: Date.now() - startTime,
116
+ reasoning: "All strategies declined, using fallback",
117
+ },
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Main Model Router Service
124
+ */
125
+ export class ModelRouterService {
126
+ private strategy: TerminalStrategy;
127
+
128
+ constructor(
129
+ private defaultModel: string,
130
+ private strategies: RoutingStrategy[] = []
131
+ ) {
132
+ const allStrategies: (RoutingStrategy | TerminalStrategy)[] = [
133
+ ...strategies,
134
+ new DefaultStrategy(defaultModel),
135
+ ];
136
+
137
+ this.strategy = new CompositeStrategy(allStrategies, defaultModel);
138
+ }
139
+
140
+ async route(context: RoutingContext): Promise<RoutingDecision> {
141
+ const startTime = Date.now();
142
+ let decision: RoutingDecision;
143
+
144
+ try {
145
+ decision = await this.strategy.route(context);
146
+ } catch (error) {
147
+ decision = {
148
+ model: this.defaultModel,
149
+ metadata: {
150
+ source: "router-exception",
151
+ latencyMs: Date.now() - startTime,
152
+ reasoning: "Exception during routing, using default",
153
+ error: error instanceof Error ? error.message : String(error),
154
+ },
155
+ };
156
+ }
157
+
158
+ return decision;
159
+ }
160
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * ZERO HTTP API Server
3
+ * Adapted from: opencode (SST) - MIT
4
+ *
5
+ * REST API server for programmatic access to ZERO.
6
+ * Supports SSE (Server-Sent Events) for streaming.
7
+ */
8
+
9
+ import * as http from "node:http";
10
+ import type { AddressInfo } from "node:net";
11
+ import type { AgentResult, SessionConfig } from "../types.js";
12
+
13
+ export interface ServerOptions {
14
+ port?: number;
15
+ host?: string;
16
+ cors?: boolean;
17
+ apiKey?: string;
18
+ }
19
+
20
+ export interface RouteHandler {
21
+ (req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): Promise<void>;
22
+ }
23
+
24
+ export class ZeroServer {
25
+ private server?: http.Server;
26
+ private routes = new Map<string, { method: string; handler: RouteHandler }>();
27
+ private options: ServerOptions;
28
+
29
+ constructor(options: ServerOptions = {}) {
30
+ this.options = {
31
+ port: options.port || 3700,
32
+ host: options.host || "localhost",
33
+ cors: options.cors !== false,
34
+ apiKey: options.apiKey,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Register a route
40
+ */
41
+ route(method: string, path: string, handler: RouteHandler): void {
42
+ this.routes.set(`${method}:${path}`, { method, handler });
43
+ }
44
+
45
+ /**
46
+ * Start the server
47
+ */
48
+ async start(): Promise<{ host: string; port: number }> {
49
+ return new Promise((resolve, reject) => {
50
+ this.server = http.createServer(async (req, res) => {
51
+ // CORS
52
+ if (this.options.cors) {
53
+ res.setHeader("Access-Control-Allow-Origin", "*");
54
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
55
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
56
+ if (req.method === "OPTIONS") {
57
+ res.writeHead(204);
58
+ res.end();
59
+ return;
60
+ }
61
+ }
62
+
63
+ // Auth
64
+ if (this.options.apiKey) {
65
+ const auth = req.headers.authorization;
66
+ if (!auth || auth !== `Bearer ${this.options.apiKey}`) {
67
+ res.writeHead(401, { "Content-Type": "application/json" });
68
+ res.end(JSON.stringify({ error: "Unauthorized" }));
69
+ return;
70
+ }
71
+ }
72
+
73
+ // Route matching
74
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
75
+ const routeKey = `${req.method}:${url.pathname}`;
76
+ const route = this.routes.get(routeKey);
77
+
78
+ if (route) {
79
+ try {
80
+ await route.handler(req, res, {});
81
+ } catch (error) {
82
+ res.writeHead(500, { "Content-Type": "application/json" });
83
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
84
+ }
85
+ } else {
86
+ // Try parameterized routes
87
+ let matched = false;
88
+ for (const [key, { handler }] of this.routes) {
89
+ const [method, pattern] = key.split(":");
90
+ if (method !== req.method) continue;
91
+
92
+ const params = this.matchRoute(pattern, url.pathname);
93
+ if (params) {
94
+ try {
95
+ await handler(req, res, params);
96
+ } catch (error) {
97
+ res.writeHead(500, { "Content-Type": "application/json" });
98
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
99
+ }
100
+ matched = true;
101
+ break;
102
+ }
103
+ }
104
+
105
+ if (!matched) {
106
+ res.writeHead(404, { "Content-Type": "application/json" });
107
+ res.end(JSON.stringify({ error: "Not found" }));
108
+ }
109
+ }
110
+ });
111
+
112
+ this.server.listen(this.options.port, this.options.host, () => {
113
+ const addr = this.server!.address() as AddressInfo;
114
+ resolve({ host: addr.address, port: addr.port });
115
+ });
116
+
117
+ this.server.on("error", reject);
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Stop the server
123
+ */
124
+ async stop(): Promise<void> {
125
+ return new Promise((resolve) => {
126
+ if (this.server) {
127
+ this.server.close(() => resolve());
128
+ } else {
129
+ resolve();
130
+ }
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Send SSE (Server-Sent Events) stream
136
+ */
137
+ static sendSSE(res: http.ServerResponse, event: string, data: unknown): void {
138
+ res.write(`event: ${event}\n`);
139
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
140
+ }
141
+
142
+ /**
143
+ * Send JSON response
144
+ */
145
+ static sendJSON(res: http.ServerResponse, data: unknown, status = 200): void {
146
+ res.writeHead(status, { "Content-Type": "application/json" });
147
+ res.end(JSON.stringify(data));
148
+ }
149
+
150
+ /**
151
+ * Read request body as JSON
152
+ */
153
+ static async readBody<T = unknown>(req: http.IncomingMessage): Promise<T> {
154
+ return new Promise((resolve, reject) => {
155
+ let body = "";
156
+ req.on("data", (chunk) => { body += chunk; });
157
+ req.on("end", () => {
158
+ try {
159
+ resolve(body ? JSON.parse(body) : {});
160
+ } catch (error) {
161
+ reject(new Error("Invalid JSON body"));
162
+ }
163
+ });
164
+ req.on("error", reject);
165
+ });
166
+ }
167
+
168
+ private matchRoute(pattern: string, pathname: string): Record<string, string> | null {
169
+ const patternParts = pattern.split("/");
170
+ const pathParts = pathname.split("/");
171
+
172
+ if (patternParts.length !== pathParts.length) return null;
173
+
174
+ const params: Record<string, string> = {};
175
+ for (let i = 0; i < patternParts.length; i++) {
176
+ if (patternParts[i].startsWith(":")) {
177
+ params[patternParts[i].slice(1)] = pathParts[i];
178
+ } else if (patternParts[i] !== pathParts[i]) {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ return params;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Create a pre-configured ZERO API server
189
+ */
190
+ export function createAPIServer(options?: ServerOptions): ZeroServer {
191
+ const server = new ZeroServer(options);
192
+
193
+ // Health check
194
+ server.route("GET", "/health", async (_, res) => {
195
+ ZeroServer.sendJSON(res, { status: "ok", version: "1.0.0", uptime: process.uptime() });
196
+ });
197
+
198
+ // List agents
199
+ server.route("GET", "/agents", async (_, res) => {
200
+ ZeroServer.sendJSON(res, {
201
+ agents: [
202
+ { id: "coder", name: "Coder", description: "Primary coding agent" },
203
+ { id: "reviewer", name: "Reviewer", description: "Code review specialist" },
204
+ { id: "researcher", name: "Researcher", description: "Documentation research" },
205
+ ],
206
+ });
207
+ });
208
+
209
+ // List tools
210
+ server.route("GET", "/tools", async (_, res) => {
211
+ ZeroServer.sendJSON(res, {
212
+ tools: [
213
+ "read_file", "write_file", "edit_file", "list_directory",
214
+ "search_files", "search_code", "execute_shell",
215
+ "git_status", "git_diff", "git_commit",
216
+ "web_fetch", "web_search", "lint", "repo_map",
217
+ ],
218
+ });
219
+ });
220
+
221
+ // List providers
222
+ server.route("GET", "/providers", async (_, res) => {
223
+ ZeroServer.sendJSON(res, {
224
+ providers: [
225
+ "openai", "anthropic", "google", "ollama", "custom",
226
+ "deepseek", "grok", "openrouter", "together", "fireworks",
227
+ ],
228
+ });
229
+ });
230
+
231
+ return server;
232
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * ZERO Session Manager
3
+ * Manages conversation sessions with persistence
4
+ *
5
+ * Sources:
6
+ * - opencode: session management with SQLite
7
+ * - crush: session state management
8
+ * - gemini-cli: context window management
9
+ */
10
+
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
13
+ import type { Session, SessionConfig, Message, TokenUsage } from "../types.js";
14
+
15
+ export interface SessionManagerOptions {
16
+ storagePath: string;
17
+ defaultConfig: Partial<SessionConfig>;
18
+ }
19
+
20
+ export class SessionManager {
21
+ private sessions = new Map<string, Session>();
22
+ private storagePath: string;
23
+ private defaultConfig: Partial<SessionConfig>;
24
+
25
+ constructor(options: SessionManagerOptions) {
26
+ this.storagePath = options.storagePath;
27
+ this.defaultConfig = options.defaultConfig;
28
+ }
29
+
30
+ /**
31
+ * Create a new session
32
+ */
33
+ async create(name?: string, configOverrides?: Partial<SessionConfig>): Promise<Session> {
34
+ const id = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
35
+ const now = Date.now();
36
+
37
+ const session: Session = {
38
+ id,
39
+ name: name || `Session ${this.sessions.size + 1}`,
40
+ createdAt: now,
41
+ updatedAt: now,
42
+ messages: [],
43
+ config: {
44
+ provider: configOverrides?.provider || this.defaultConfig.provider || "openai",
45
+ model: configOverrides?.model || this.defaultConfig.model || "gpt-4o",
46
+ systemPrompt: configOverrides?.systemPrompt || this.defaultConfig.systemPrompt || "",
47
+ workingDirectory: configOverrides?.workingDirectory || this.defaultConfig.workingDirectory || process.cwd(),
48
+ permissions: configOverrides?.permissions || this.defaultConfig.permissions || {
49
+ fileRead: "allow",
50
+ fileWrite: "ask",
51
+ shellExecution: "ask",
52
+ networkAccess: "ask",
53
+ mcpAccess: "allow",
54
+ },
55
+ tools: configOverrides?.tools || this.defaultConfig.tools || [],
56
+ mcpServers: configOverrides?.mcpServers || this.defaultConfig.mcpServers || [],
57
+ },
58
+ state: {
59
+ status: "idle",
60
+ tokenUsage: { inputTokens: 0, outputTokens: 0 },
61
+ },
62
+ };
63
+
64
+ this.sessions.set(id, session);
65
+ await this.save(session);
66
+ return session;
67
+ }
68
+
69
+ /**
70
+ * Get an existing session
71
+ */
72
+ get(id: string): Session | undefined {
73
+ return this.sessions.get(id);
74
+ }
75
+
76
+ /**
77
+ * List all sessions
78
+ */
79
+ list(): Session[] {
80
+ return Array.from(this.sessions.values()).sort((a, b) => b.updatedAt - a.updatedAt);
81
+ }
82
+
83
+ /**
84
+ * Add a message to a session
85
+ */
86
+ async addMessage(sessionId: string, message: Message): Promise<void> {
87
+ const session = this.sessions.get(sessionId);
88
+ if (!session) throw new Error(`Session "${sessionId}" not found`);
89
+
90
+ session.messages.push(message);
91
+ session.updatedAt = Date.now();
92
+ await this.save(session);
93
+ }
94
+
95
+ /**
96
+ * Update session state
97
+ */
98
+ async updateState(sessionId: string, updates: Partial<Session["state"]>): Promise<void> {
99
+ const session = this.sessions.get(sessionId);
100
+ if (!session) throw new Error(`Session "${sessionId}" not found`);
101
+
102
+ Object.assign(session.state, updates);
103
+ session.updatedAt = Date.now();
104
+ await this.save(session);
105
+ }
106
+
107
+ /**
108
+ * Delete a session
109
+ */
110
+ async delete(id: string): Promise<void> {
111
+ this.sessions.delete(id);
112
+ try {
113
+ await fs.unlink(path.join(this.storagePath, `${id}.json`));
114
+ } catch {}
115
+ }
116
+
117
+ /**
118
+ * Load sessions from storage
119
+ */
120
+ async loadAll(): Promise<void> {
121
+ try {
122
+ await fs.mkdir(this.storagePath, { recursive: true });
123
+ const files = await fs.readdir(this.storagePath);
124
+
125
+ for (const file of files) {
126
+ if (file.endsWith(".json")) {
127
+ try {
128
+ const data = await fs.readFile(path.join(this.storagePath, file), "utf-8");
129
+ const session = JSON.parse(data) as Session;
130
+ this.sessions.set(session.id, session);
131
+ } catch {}
132
+ }
133
+ }
134
+ } catch {}
135
+ }
136
+
137
+ /**
138
+ * Save session to storage
139
+ */
140
+ private async save(session: Session): Promise<void> {
141
+ await fs.mkdir(this.storagePath, { recursive: true });
142
+ await fs.writeFile(
143
+ path.join(this.storagePath, `${session.id}.json`),
144
+ JSON.stringify(session, null, 2)
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Get session statistics
150
+ */
151
+ getStats(sessionId: string): {
152
+ messageCount: number;
153
+ totalTokens: TokenUsage;
154
+ duration: number;
155
+ toolsUsed: string[];
156
+ } {
157
+ const session = this.sessions.get(sessionId);
158
+ if (!session) throw new Error(`Session "${sessionId}" not found`);
159
+
160
+ const toolsUsed = new Set<string>();
161
+ for (const msg of session.messages) {
162
+ for (const content of msg.content) {
163
+ if (content.type === "tool_call") toolsUsed.add(content.name);
164
+ }
165
+ }
166
+
167
+ return {
168
+ messageCount: session.messages.length,
169
+ totalTokens: session.state.tokenUsage,
170
+ duration: session.updatedAt - session.createdAt,
171
+ toolsUsed: Array.from(toolsUsed),
172
+ };
173
+ }
174
+ }