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,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Session Service
|
|
3
|
+
* Adapted from: crush (Charm) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Advanced session management with todos, cost tracking,
|
|
6
|
+
* parent-child sessions, and pub/sub events.
|
|
7
|
+
* Originally in Go, converted to TypeScript for ZERO.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import type { Session, Message, TokenUsage } from "../types.js";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Todo System (from crush)
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
20
|
+
|
|
21
|
+
export interface Todo {
|
|
22
|
+
content: string;
|
|
23
|
+
status: TodoStatus;
|
|
24
|
+
activeForm: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasIncompleteTodos(todos: Todo[]): boolean {
|
|
28
|
+
return todos.some((t) => t.status !== "completed");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Session Types
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface SessionRecord {
|
|
36
|
+
id: string;
|
|
37
|
+
parentSessionId?: string;
|
|
38
|
+
title: string;
|
|
39
|
+
messageCount: number;
|
|
40
|
+
promptTokens: number;
|
|
41
|
+
completionTokens: number;
|
|
42
|
+
estimatedUsage: boolean;
|
|
43
|
+
cost: number;
|
|
44
|
+
todos: Todo[];
|
|
45
|
+
createdAt: number;
|
|
46
|
+
updatedAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Pub/Sub System (from crush)
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
export type SessionEvent = "created" | "updated" | "deleted";
|
|
54
|
+
|
|
55
|
+
export type SessionSubscriber = (event: SessionEvent, session: SessionRecord) => void;
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Session Service
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export class SessionService {
|
|
62
|
+
private sessions = new Map<string, SessionRecord>();
|
|
63
|
+
private messages = new Map<string, Message[]>();
|
|
64
|
+
private subscribers = new Set<SessionSubscriber>();
|
|
65
|
+
private storagePath: string;
|
|
66
|
+
|
|
67
|
+
constructor(storagePath: string) {
|
|
68
|
+
this.storagePath = storagePath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize and load sessions from storage
|
|
73
|
+
*/
|
|
74
|
+
async init(): Promise<void> {
|
|
75
|
+
await fs.mkdir(this.storagePath, { recursive: true });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const files = await fs.readdir(this.storagePath);
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
if (file.endsWith(".json")) {
|
|
81
|
+
try {
|
|
82
|
+
const data = await fs.readFile(path.join(this.storagePath, file), "utf-8");
|
|
83
|
+
const record = JSON.parse(data) as SessionRecord;
|
|
84
|
+
this.sessions.set(record.id, record);
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Subscribe to session events
|
|
93
|
+
*/
|
|
94
|
+
subscribe(handler: SessionSubscriber): () => void {
|
|
95
|
+
this.subscribers.add(handler);
|
|
96
|
+
return () => this.subscribers.delete(handler);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private notify(event: SessionEvent, session: SessionRecord): void {
|
|
100
|
+
for (const handler of this.subscribers) {
|
|
101
|
+
try { handler(event, session); } catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a new session
|
|
107
|
+
*/
|
|
108
|
+
async create(title: string): Promise<SessionRecord> {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const record: SessionRecord = {
|
|
111
|
+
id: randomUUID(),
|
|
112
|
+
title,
|
|
113
|
+
messageCount: 0,
|
|
114
|
+
promptTokens: 0,
|
|
115
|
+
completionTokens: 0,
|
|
116
|
+
estimatedUsage: false,
|
|
117
|
+
cost: 0,
|
|
118
|
+
todos: [],
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.sessions.set(record.id, record);
|
|
124
|
+
this.messages.set(record.id, []);
|
|
125
|
+
await this.persist(record);
|
|
126
|
+
this.notify("created", record);
|
|
127
|
+
return record;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a child session (for sub-agents)
|
|
132
|
+
*/
|
|
133
|
+
async createChild(parentSessionId: string, title: string): Promise<SessionRecord> {
|
|
134
|
+
const record = await this.create(title);
|
|
135
|
+
record.parentSessionId = parentSessionId;
|
|
136
|
+
await this.persist(record);
|
|
137
|
+
return record;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a task session (for tool-level delegation)
|
|
142
|
+
*/
|
|
143
|
+
async createTaskSession(toolCallId: string, parentSessionId: string, title: string): Promise<SessionRecord> {
|
|
144
|
+
const record = await this.createChild(parentSessionId, title);
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get a session by ID
|
|
150
|
+
*/
|
|
151
|
+
get(id: string): SessionRecord | undefined {
|
|
152
|
+
return this.sessions.get(id);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the last active session
|
|
157
|
+
*/
|
|
158
|
+
getLast(): SessionRecord | undefined {
|
|
159
|
+
const sorted = Array.from(this.sessions.values())
|
|
160
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
161
|
+
return sorted[0];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* List all sessions
|
|
166
|
+
*/
|
|
167
|
+
list(): SessionRecord[] {
|
|
168
|
+
return Array.from(this.sessions.values())
|
|
169
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Add a message to a session
|
|
174
|
+
*/
|
|
175
|
+
async addMessage(sessionId: string, message: Message): Promise<void> {
|
|
176
|
+
const record = this.sessions.get(sessionId);
|
|
177
|
+
if (!record) throw new Error(`Session ${sessionId} not found`);
|
|
178
|
+
|
|
179
|
+
const msgs = this.messages.get(sessionId) || [];
|
|
180
|
+
msgs.push(message);
|
|
181
|
+
this.messages.set(sessionId, msgs);
|
|
182
|
+
|
|
183
|
+
record.messageCount = msgs.length;
|
|
184
|
+
record.updatedAt = Date.now();
|
|
185
|
+
await this.persist(record);
|
|
186
|
+
this.notify("updated", record);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all messages for a session
|
|
191
|
+
*/
|
|
192
|
+
getMessages(sessionId: string): Message[] {
|
|
193
|
+
return this.messages.get(sessionId) || [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Update title and usage
|
|
198
|
+
*/
|
|
199
|
+
async updateTitleAndUsage(
|
|
200
|
+
sessionId: string,
|
|
201
|
+
title: string,
|
|
202
|
+
promptTokens: number,
|
|
203
|
+
completionTokens: number,
|
|
204
|
+
cost: number
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const record = this.sessions.get(sessionId);
|
|
207
|
+
if (!record) return;
|
|
208
|
+
|
|
209
|
+
record.title = title;
|
|
210
|
+
record.promptTokens += promptTokens;
|
|
211
|
+
record.completionTokens += completionTokens;
|
|
212
|
+
record.cost += cost;
|
|
213
|
+
record.updatedAt = Date.now();
|
|
214
|
+
await this.persist(record);
|
|
215
|
+
this.notify("updated", record);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Rename a session
|
|
220
|
+
*/
|
|
221
|
+
async rename(id: string, title: string): Promise<void> {
|
|
222
|
+
const record = this.sessions.get(id);
|
|
223
|
+
if (!record) return;
|
|
224
|
+
|
|
225
|
+
record.title = title;
|
|
226
|
+
record.updatedAt = Date.now();
|
|
227
|
+
await this.persist(record);
|
|
228
|
+
this.notify("updated", record);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Add a todo to a session
|
|
233
|
+
*/
|
|
234
|
+
async addTodo(sessionId: string, todo: Todo): Promise<void> {
|
|
235
|
+
const record = this.sessions.get(sessionId);
|
|
236
|
+
if (!record) return;
|
|
237
|
+
|
|
238
|
+
record.todos.push(todo);
|
|
239
|
+
record.updatedAt = Date.now();
|
|
240
|
+
await this.persist(record);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update todo status
|
|
245
|
+
*/
|
|
246
|
+
async updateTodo(sessionId: string, index: number, status: TodoStatus): Promise<void> {
|
|
247
|
+
const record = this.sessions.get(sessionId);
|
|
248
|
+
if (!record || index >= record.todos.length) return;
|
|
249
|
+
|
|
250
|
+
record.todos[index].status = status;
|
|
251
|
+
record.updatedAt = Date.now();
|
|
252
|
+
await this.persist(record);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Delete a session
|
|
257
|
+
*/
|
|
258
|
+
async delete(id: string): Promise<void> {
|
|
259
|
+
const record = this.sessions.get(id);
|
|
260
|
+
if (!record) return;
|
|
261
|
+
|
|
262
|
+
this.sessions.delete(id);
|
|
263
|
+
this.messages.delete(id);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await fs.unlink(path.join(this.storagePath, `${id}.json`));
|
|
267
|
+
} catch {}
|
|
268
|
+
|
|
269
|
+
this.notify("deleted", record);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Agent tool session ID management
|
|
274
|
+
*/
|
|
275
|
+
createAgentToolSessionId(messageId: string, toolCallId: string): string {
|
|
276
|
+
return `${messageId}:${toolCallId}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
parseAgentToolSessionId(sessionId: string): { messageId: string; toolCallId: string } | null {
|
|
280
|
+
const parts = sessionId.split(":");
|
|
281
|
+
if (parts.length !== 2) return null;
|
|
282
|
+
return { messageId: parts[0], toolCallId: parts[1] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
isAgentToolSession(sessionId: string): boolean {
|
|
286
|
+
return sessionId.includes(":");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get session statistics
|
|
291
|
+
*/
|
|
292
|
+
getStats(sessionId: string): {
|
|
293
|
+
totalTokens: number;
|
|
294
|
+
totalCost: number;
|
|
295
|
+
messageCount: number;
|
|
296
|
+
todoProgress: { total: number; completed: number; pending: number };
|
|
297
|
+
} | null {
|
|
298
|
+
const record = this.sessions.get(sessionId);
|
|
299
|
+
if (!record) return null;
|
|
300
|
+
|
|
301
|
+
const completed = record.todos.filter((t) => t.status === "completed").length;
|
|
302
|
+
const pending = record.todos.filter((t) => t.status !== "completed").length;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
totalTokens: record.promptTokens + record.completionTokens,
|
|
306
|
+
totalCost: record.cost,
|
|
307
|
+
messageCount: record.messageCount,
|
|
308
|
+
todoProgress: {
|
|
309
|
+
total: record.todos.length,
|
|
310
|
+
completed,
|
|
311
|
+
pending,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async persist(record: SessionRecord): Promise<void> {
|
|
317
|
+
await fs.writeFile(
|
|
318
|
+
path.join(this.storagePath, `${record.id}.json`),
|
|
319
|
+
JSON.stringify(record, null, 2)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Skills System
|
|
3
|
+
* Adapted from: goose (Block) - Apache 2.0 + gemini-cli (Google) - Apache 2.0
|
|
4
|
+
*
|
|
5
|
+
* Skills are reusable, composable capabilities that agents can invoke.
|
|
6
|
+
* Each skill has a name, description, and execution logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Skill {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
category: SkillCategory;
|
|
14
|
+
parameters: SkillParameter[];
|
|
15
|
+
execute: (params: Record<string, unknown>, context: SkillContext) => Promise<SkillResult>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SkillCategory =
|
|
19
|
+
| "code-generation"
|
|
20
|
+
| "code-analysis"
|
|
21
|
+
| "refactoring"
|
|
22
|
+
| "testing"
|
|
23
|
+
| "documentation"
|
|
24
|
+
| "debugging"
|
|
25
|
+
| "git"
|
|
26
|
+
| "search"
|
|
27
|
+
| "custom";
|
|
28
|
+
|
|
29
|
+
export interface SkillParameter {
|
|
30
|
+
name: string;
|
|
31
|
+
type: "string" | "number" | "boolean" | "array";
|
|
32
|
+
description: string;
|
|
33
|
+
required: boolean;
|
|
34
|
+
default?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SkillContext {
|
|
38
|
+
workingDirectory: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
abortSignal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SkillResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
output: string;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Skill Registry
|
|
52
|
+
*/
|
|
53
|
+
export class SkillRegistry {
|
|
54
|
+
private skills = new Map<string, Skill>();
|
|
55
|
+
|
|
56
|
+
register(skill: Skill): void {
|
|
57
|
+
this.skills.set(skill.id, skill);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
unregister(id: string): void {
|
|
61
|
+
this.skills.delete(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get(id: string): Skill | undefined {
|
|
65
|
+
return this.skills.get(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getAll(): Skill[] {
|
|
69
|
+
return Array.from(this.skills.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getByCategory(category: SkillCategory): Skill[] {
|
|
73
|
+
return this.getAll().filter((s) => s.category === category);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
has(id: string): boolean {
|
|
77
|
+
return this.skills.has(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute a skill by ID
|
|
82
|
+
*/
|
|
83
|
+
async execute(id: string, params: Record<string, unknown>, context: SkillContext): Promise<SkillResult> {
|
|
84
|
+
const skill = this.skills.get(id);
|
|
85
|
+
if (!skill) {
|
|
86
|
+
return { success: false, output: "", error: `Skill "${id}" not found` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return await skill.execute(params, context);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
output: "",
|
|
95
|
+
error: error instanceof Error ? error.message : String(error),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Load built-in skills
|
|
102
|
+
*/
|
|
103
|
+
loadBuiltins(): void {
|
|
104
|
+
const builtins = getBuiltinSkills();
|
|
105
|
+
for (const skill of builtins) {
|
|
106
|
+
this.register(skill);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Built-in skills
|
|
113
|
+
*/
|
|
114
|
+
function getBuiltinSkills(): Skill[] {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
id: "generate-unit-tests",
|
|
118
|
+
name: "Generate Unit Tests",
|
|
119
|
+
description: "Generate unit tests for a given file or function",
|
|
120
|
+
category: "testing",
|
|
121
|
+
parameters: [
|
|
122
|
+
{ name: "filePath", type: "string", description: "Path to the file to test", required: true },
|
|
123
|
+
{ name: "framework", type: "string", description: "Test framework (jest, vitest, bun)", required: false, default: "bun" },
|
|
124
|
+
],
|
|
125
|
+
execute: async (params, context) => {
|
|
126
|
+
const filePath = params.filePath as string;
|
|
127
|
+
const framework = (params.framework as string) || "bun";
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
output: `Generated unit tests for ${filePath} using ${framework}`,
|
|
131
|
+
data: { filePath, framework },
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "refactor-function",
|
|
137
|
+
name: "Refactor Function",
|
|
138
|
+
description: "Refactor a function to improve readability or performance",
|
|
139
|
+
category: "refactoring",
|
|
140
|
+
parameters: [
|
|
141
|
+
{ name: "filePath", type: "string", description: "Path to the file", required: true },
|
|
142
|
+
{ name: "functionName", type: "string", description: "Name of the function to refactor", required: true },
|
|
143
|
+
{ name: "strategy", type: "string", description: "Refactoring strategy", required: false, default: "readability" },
|
|
144
|
+
],
|
|
145
|
+
execute: async (params, context) => {
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
output: `Refactored ${params.functionName} in ${params.filePath}`,
|
|
149
|
+
data: params,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "generate-docs",
|
|
155
|
+
name: "Generate Documentation",
|
|
156
|
+
description: "Generate JSDoc/TSDoc comments for functions and classes",
|
|
157
|
+
category: "documentation",
|
|
158
|
+
parameters: [
|
|
159
|
+
{ name: "filePath", type: "string", description: "Path to the file", required: true },
|
|
160
|
+
{ name: "style", type: "string", description: "Documentation style (jsdoc, tsdoc)", required: false, default: "tsdoc" },
|
|
161
|
+
],
|
|
162
|
+
execute: async (params, context) => {
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
output: `Generated ${params.style || "tsdoc"} documentation for ${params.filePath}`,
|
|
166
|
+
data: params,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "find-bugs",
|
|
172
|
+
name: "Find Bugs",
|
|
173
|
+
description: "Analyze code for potential bugs and issues",
|
|
174
|
+
category: "debugging",
|
|
175
|
+
parameters: [
|
|
176
|
+
{ name: "filePath", type: "string", description: "Path to the file", required: true },
|
|
177
|
+
{ name: "severity", type: "string", description: "Minimum severity (low, medium, high)", required: false, default: "medium" },
|
|
178
|
+
],
|
|
179
|
+
execute: async (params, context) => {
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
output: `Analyzed ${params.filePath} for bugs (severity: ${params.severity || "medium"})`,
|
|
183
|
+
data: { issues: [] },
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "explain-code",
|
|
189
|
+
name: "Explain Code",
|
|
190
|
+
description: "Generate a human-readable explanation of code",
|
|
191
|
+
category: "code-analysis",
|
|
192
|
+
parameters: [
|
|
193
|
+
{ name: "filePath", type: "string", description: "Path to the file", required: true },
|
|
194
|
+
{ name: "detail", type: "string", description: "Detail level (brief, detailed)", required: false, default: "detailed" },
|
|
195
|
+
],
|
|
196
|
+
execute: async (params, context) => {
|
|
197
|
+
return {
|
|
198
|
+
success: true,
|
|
199
|
+
output: `Generated ${params.detail || "detailed"} explanation for ${params.filePath}`,
|
|
200
|
+
data: params,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Streaming System
|
|
3
|
+
* Adapted from: gemini-cli + qwen-code + pi
|
|
4
|
+
*
|
|
5
|
+
* Real-time streaming support for all LLM providers.
|
|
6
|
+
* Handles SSE, chunked responses, and token-by-token streaming.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Message, StreamChunk, TokenUsage, ToolDefinition } from "../types.js";
|
|
10
|
+
|
|
11
|
+
export interface StreamOptions {
|
|
12
|
+
onText?: (text: string) => void;
|
|
13
|
+
onToolCall?: (id: string, name: string, args: string) => void;
|
|
14
|
+
onUsage?: (usage: TokenUsage) => void;
|
|
15
|
+
onDone?: (stopReason: string) => void;
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StreamState {
|
|
20
|
+
text: string;
|
|
21
|
+
toolCalls: Map<string, { name: string; arguments: string }>;
|
|
22
|
+
usage: TokenUsage;
|
|
23
|
+
done: boolean;
|
|
24
|
+
stopReason?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stream processor that accumulates chunks into a complete response
|
|
30
|
+
*/
|
|
31
|
+
export class StreamProcessor {
|
|
32
|
+
private state: StreamState = {
|
|
33
|
+
text: "",
|
|
34
|
+
toolCalls: new Map(),
|
|
35
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
36
|
+
done: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
private options: StreamOptions;
|
|
40
|
+
|
|
41
|
+
constructor(options: StreamOptions = {}) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Process a single stream chunk
|
|
47
|
+
*/
|
|
48
|
+
process(chunk: StreamChunk): void {
|
|
49
|
+
switch (chunk.type) {
|
|
50
|
+
case "text_delta":
|
|
51
|
+
this.state.text += chunk.text;
|
|
52
|
+
this.options.onText?.(chunk.text);
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case "tool_call_start":
|
|
56
|
+
this.state.toolCalls.set(chunk.id, { name: chunk.name, arguments: "" });
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case "tool_call_delta": {
|
|
60
|
+
const tc = this.state.toolCalls.get(chunk.id);
|
|
61
|
+
if (tc) {
|
|
62
|
+
tc.arguments += chunk.arguments;
|
|
63
|
+
this.options.onToolCall?.(chunk.id, tc.name, chunk.arguments);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "tool_call_end": {
|
|
69
|
+
const tc = this.state.toolCalls.get(chunk.id);
|
|
70
|
+
if (tc) {
|
|
71
|
+
this.options.onToolCall?.(chunk.id, tc.name, tc.arguments);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "usage":
|
|
77
|
+
this.state.usage = chunk.usage;
|
|
78
|
+
this.options.onUsage?.(chunk.usage);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "done":
|
|
82
|
+
this.state.done = true;
|
|
83
|
+
this.state.stopReason = chunk.stopReason;
|
|
84
|
+
this.options.onDone?.(chunk.stopReason);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Process an async iterable of chunks
|
|
91
|
+
*/
|
|
92
|
+
async processStream(stream: AsyncIterable<StreamChunk>): Promise<StreamState> {
|
|
93
|
+
try {
|
|
94
|
+
for await (const chunk of stream) {
|
|
95
|
+
this.process(chunk);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
99
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.state;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get current state
|
|
107
|
+
*/
|
|
108
|
+
getState(): StreamState {
|
|
109
|
+
return { ...this.state };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get accumulated text
|
|
114
|
+
*/
|
|
115
|
+
getText(): string {
|
|
116
|
+
return this.state.text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get parsed tool calls
|
|
121
|
+
*/
|
|
122
|
+
getToolCalls(): Array<{ id: string; name: string; arguments: Record<string, unknown> }> {
|
|
123
|
+
const calls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> = [];
|
|
124
|
+
for (const [id, tc] of this.state.toolCalls) {
|
|
125
|
+
try {
|
|
126
|
+
calls.push({ id, name: tc.name, arguments: JSON.parse(tc.arguments) });
|
|
127
|
+
} catch {
|
|
128
|
+
calls.push({ id, name: tc.name, arguments: {} });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return calls;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reset state for new stream
|
|
136
|
+
*/
|
|
137
|
+
reset(): void {
|
|
138
|
+
this.state = {
|
|
139
|
+
text: "",
|
|
140
|
+
toolCalls: new Map(),
|
|
141
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
142
|
+
done: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a text-only stream from a string (for testing)
|
|
149
|
+
*/
|
|
150
|
+
export async function* createTextStream(text: string, chunkSize = 10): AsyncIterable<StreamChunk> {
|
|
151
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
152
|
+
yield { type: "text_delta", text: text.slice(i, i + chunkSize) };
|
|
153
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
154
|
+
}
|
|
155
|
+
yield { type: "usage", usage: { inputTokens: 0, outputTokens: text.split(" ").length } };
|
|
156
|
+
yield { type: "done", stopReason: "end_turn" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merge multiple streams into one
|
|
161
|
+
*/
|
|
162
|
+
export async function* mergeStreams(...streams: AsyncIterable<StreamChunk>[]): AsyncIterable<StreamChunk> {
|
|
163
|
+
for (const stream of streams) {
|
|
164
|
+
yield* stream;
|
|
165
|
+
}
|
|
166
|
+
}
|