@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,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
+ }