@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.
Files changed (47) hide show
  1. package/package.json +10 -7
  2. package/packages/core/src/agent/config-loader.ts +174 -0
  3. package/packages/core/src/agent/engine.ts +266 -0
  4. package/packages/core/src/agent/registry-tools.ts +58 -0
  5. package/packages/core/src/agent/registry.ts +213 -0
  6. package/packages/core/src/commands/index.ts +116 -0
  7. package/packages/core/src/config/index.ts +139 -0
  8. package/packages/core/src/confirmation/message-bus.ts +206 -0
  9. package/packages/core/src/diff/index.ts +232 -0
  10. package/packages/core/src/diff-detect/index.ts +207 -0
  11. package/packages/core/src/edit-coder/index.ts +194 -0
  12. package/packages/core/src/events/index.ts +151 -0
  13. package/packages/core/src/file-tracker/index.ts +183 -0
  14. package/packages/core/src/git/index.ts +305 -0
  15. package/packages/core/src/history/index.ts +236 -0
  16. package/packages/core/src/image/index.ts +131 -0
  17. package/packages/core/src/index.ts +131 -0
  18. package/packages/core/src/lsp/index.ts +251 -0
  19. package/packages/core/src/mcp/index.ts +186 -0
  20. package/packages/core/src/memory/index.ts +141 -0
  21. package/packages/core/src/memory/prompts.ts +52 -0
  22. package/packages/core/src/orchestrator/protocol.ts +140 -0
  23. package/packages/core/src/orchestrator/server.ts +319 -0
  24. package/packages/core/src/output/index.ts +164 -0
  25. package/packages/core/src/permissions/index.ts +126 -0
  26. package/packages/core/src/prompts/engine.ts +188 -0
  27. package/packages/core/src/providers/extended.ts +8 -0
  28. package/packages/core/src/providers/registry.ts +687 -0
  29. package/packages/core/src/routing/model-router.ts +160 -0
  30. package/packages/core/src/server/index.ts +232 -0
  31. package/packages/core/src/session/index.ts +174 -0
  32. package/packages/core/src/session/service.ts +322 -0
  33. package/packages/core/src/skills/index.ts +205 -0
  34. package/packages/core/src/streaming/index.ts +166 -0
  35. package/packages/core/src/sub-agent/index.ts +179 -0
  36. package/packages/core/src/tools/builtin.ts +568 -0
  37. package/packages/core/src/tools/linter.ts +26 -0
  38. package/packages/core/src/tools/repo-map.ts +11 -0
  39. package/packages/core/src/types.ts +356 -0
  40. package/packages/sdk/src/index.ts +247 -0
  41. package/packages/tui/src/index.ts +141 -0
  42. package/bun.lock +0 -35
  43. package/bunfig.toml +0 -6
  44. package/packages/cli/package.json +0 -23
  45. package/packages/core/package.json +0 -41
  46. package/packages/sdk/package.json +0 -16
  47. package/packages/tui/package.json +0 -19
@@ -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
+ }