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,266 @@
1
+ /**
2
+ * ZERO Agent Engine
3
+ * Core agent loop implementation combining best patterns from:
4
+ * - goose: sub-agent orchestration
5
+ * - gemini-cli: ReAct loop with tool execution
6
+ * - pi: multi-agent orchestrator
7
+ * - cline: tool approval flow
8
+ */
9
+
10
+ import type {
11
+ AgentConfig,
12
+ AgentResult,
13
+ ChatOptions,
14
+ LLMClient,
15
+ Message,
16
+ MessageContent,
17
+ ToolCallContent,
18
+ ToolDefinition,
19
+ ToolResult,
20
+ TokenUsage,
21
+ PermissionHandler,
22
+ SessionState,
23
+ } from "../types.js";
24
+
25
+ export interface AgentEngineOptions {
26
+ config: AgentConfig;
27
+ client: LLMClient;
28
+ tools: Map<string, ToolDefinition>;
29
+ permissionHandler: PermissionHandler;
30
+ workingDirectory: string;
31
+ sessionId: string;
32
+ onStateChange?: (state: SessionState) => void;
33
+ onMessage?: (message: Message) => void;
34
+ }
35
+
36
+ export class AgentEngine {
37
+ private config: AgentConfig;
38
+ private client: LLMClient;
39
+ private tools: Map<string, ToolDefinition>;
40
+ private permissionHandler: PermissionHandler;
41
+ private workingDirectory: string;
42
+ private sessionId: string;
43
+ private messages: Message[] = [];
44
+ private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
45
+ private turnCount = 0;
46
+ private aborted = false;
47
+ private abortController = new AbortController();
48
+ private onStateChange?: (state: SessionState) => void;
49
+ private onMessage?: (message: Message) => void;
50
+
51
+ constructor(options: AgentEngineOptions) {
52
+ this.config = options.config;
53
+ this.client = options.client;
54
+ this.tools = options.tools;
55
+ this.permissionHandler = options.permissionHandler;
56
+ this.workingDirectory = options.workingDirectory;
57
+ this.sessionId = options.sessionId;
58
+ this.onStateChange = options.onStateChange;
59
+ this.onMessage = options.onMessage;
60
+ }
61
+
62
+ /**
63
+ * Run the agent loop with a user message
64
+ */
65
+ async run(userMessage: string): Promise<AgentResult> {
66
+ const startTime = Date.now();
67
+ this.aborted = false;
68
+ this.turnCount = 0;
69
+
70
+ // Add user message
71
+ const userMsg: Message = {
72
+ id: this.generateId(),
73
+ role: "user",
74
+ content: [{ type: "text", text: userMessage }],
75
+ timestamp: Date.now(),
76
+ };
77
+ this.messages.push(userMsg);
78
+ this.onMessage?.(userMsg);
79
+
80
+ try {
81
+ // Agent loop (ReAct pattern)
82
+ while (this.turnCount < this.config.maxTurns && !this.aborted) {
83
+ this.turnCount++;
84
+ this.updateState({ status: "running", currentAgent: this.config.name, tokenUsage: this.totalUsage });
85
+
86
+ // Get available tools for this agent
87
+ const availableTools = this.getAvailableTools();
88
+
89
+ // Call LLM
90
+ const chatOptions: ChatOptions = {
91
+ tools: availableTools.length > 0 ? availableTools : undefined,
92
+ systemPrompt: this.config.systemPrompt,
93
+ };
94
+
95
+ const response = await this.client.chat(this.messages, chatOptions);
96
+
97
+ // Track usage
98
+ this.totalUsage.inputTokens += response.usage.inputTokens;
99
+ this.totalUsage.outputTokens += response.usage.outputTokens;
100
+
101
+ // Add assistant message
102
+ const assistantMsg: Message = {
103
+ id: this.generateId(),
104
+ role: "assistant",
105
+ content: response.content,
106
+ timestamp: Date.now(),
107
+ metadata: { model: response.model, usage: response.usage },
108
+ };
109
+ this.messages.push(assistantMsg);
110
+ this.onMessage?.(assistantMsg);
111
+
112
+ // Check if we should stop
113
+ if (response.stopReason === "end_turn" || response.stopReason === "max_tokens") {
114
+ break;
115
+ }
116
+
117
+ // Process tool calls
118
+ if (response.stopReason === "tool_use") {
119
+ const toolCalls = response.content.filter(
120
+ (c): c is ToolCallContent => c.type === "tool_call"
121
+ );
122
+
123
+ for (const toolCall of toolCalls) {
124
+ const result = await this.executeTool(toolCall);
125
+ const toolResultMsg: Message = {
126
+ id: this.generateId(),
127
+ role: "tool",
128
+ content: [{
129
+ type: "tool_result",
130
+ id: toolCall.id,
131
+ name: toolCall.name,
132
+ result: result.output,
133
+ error: result.error,
134
+ }],
135
+ timestamp: Date.now(),
136
+ };
137
+ this.messages.push(toolResultMsg);
138
+ this.onMessage?.(toolResultMsg);
139
+ }
140
+ }
141
+ }
142
+
143
+ const duration = Date.now() - startTime;
144
+ const result: AgentResult = {
145
+ success: !this.aborted,
146
+ messages: this.messages,
147
+ tokenUsage: this.totalUsage,
148
+ duration,
149
+ };
150
+
151
+ this.updateState({ status: "completed", tokenUsage: this.totalUsage });
152
+ return result;
153
+ } catch (error) {
154
+ const duration = Date.now() - startTime;
155
+ const errorMessage = error instanceof Error ? error.message : String(error);
156
+ this.updateState({ status: "error", tokenUsage: this.totalUsage, error: errorMessage });
157
+ return {
158
+ success: false,
159
+ messages: this.messages,
160
+ tokenUsage: this.totalUsage,
161
+ duration,
162
+ error: errorMessage,
163
+ };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Execute a single tool call with permission checking
169
+ */
170
+ private async executeTool(toolCall: ToolCallContent): Promise<ToolResult> {
171
+ const tool = this.tools.get(toolCall.name);
172
+
173
+ if (!tool) {
174
+ return {
175
+ success: false,
176
+ output: "",
177
+ error: `Tool "${toolCall.name}" not found`,
178
+ };
179
+ }
180
+
181
+ // Check permissions if tool requires approval
182
+ if (tool.requiresApproval) {
183
+ this.updateState({ status: "waiting_approval", tokenUsage: this.totalUsage });
184
+
185
+ const decision = await this.permissionHandler({
186
+ type: this.getPermissionType(tool.category),
187
+ resource: toolCall.name,
188
+ details: JSON.stringify(toolCall.arguments),
189
+ });
190
+
191
+ if (decision === "deny") {
192
+ return {
193
+ success: false,
194
+ output: "",
195
+ error: "Permission denied by user",
196
+ };
197
+ }
198
+
199
+ this.updateState({ status: "running", tokenUsage: this.totalUsage });
200
+ }
201
+
202
+ // Execute the tool
203
+ try {
204
+ const result = await tool.handler(toolCall.arguments, {
205
+ sessionId: this.sessionId,
206
+ workingDirectory: this.workingDirectory,
207
+ permissions: {
208
+ fileRead: "allow",
209
+ fileWrite: tool.requiresApproval ? "allow" : "ask",
210
+ shellExecution: tool.requiresApproval ? "allow" : "ask",
211
+ networkAccess: "allow",
212
+ mcpAccess: "allow",
213
+ },
214
+ abortSignal: this.abortController.signal,
215
+ });
216
+ return result;
217
+ } catch (error) {
218
+ return {
219
+ success: false,
220
+ output: "",
221
+ error: error instanceof Error ? error.message : String(error),
222
+ };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Get tools available to this agent
228
+ */
229
+ private getAvailableTools(): ToolDefinition[] {
230
+ return this.config.tools
231
+ .map((name) => this.tools.get(name))
232
+ .filter((t): t is ToolDefinition => t !== undefined);
233
+ }
234
+
235
+ private getPermissionType(category: string): "file_read" | "file_write" | "shell" | "network" | "mcp" {
236
+ switch (category) {
237
+ case "file": return "file_write";
238
+ case "shell": return "shell";
239
+ case "web": return "network";
240
+ case "mcp": return "mcp";
241
+ default: return "file_read";
242
+ }
243
+ }
244
+
245
+ abort(): void {
246
+ this.aborted = true;
247
+ this.abortController.abort();
248
+ }
249
+
250
+ getMessages(): Message[] {
251
+ return [...this.messages];
252
+ }
253
+
254
+ private updateState(partial: Partial<SessionState>): void {
255
+ this.onStateChange?.({
256
+ status: partial.status ?? "idle",
257
+ currentAgent: partial.currentAgent ?? this.config.name,
258
+ tokenUsage: partial.tokenUsage ?? this.totalUsage,
259
+ error: partial.error,
260
+ });
261
+ }
262
+
263
+ private generateId(): string {
264
+ return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
265
+ }
266
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ZERO Tool Registry
3
+ * Manages all available tools for agents
4
+ */
5
+
6
+ import type { ToolDefinition, ToolCategory } from "../types.js";
7
+
8
+ export class ToolRegistry {
9
+ private tools = new Map<string, ToolDefinition>();
10
+
11
+ register(tool: ToolDefinition): void {
12
+ this.tools.set(tool.name, tool);
13
+ }
14
+
15
+ unregister(name: string): void {
16
+ this.tools.delete(name);
17
+ }
18
+
19
+ get(name: string): ToolDefinition | undefined {
20
+ return this.tools.get(name);
21
+ }
22
+
23
+ getAll(): ToolDefinition[] {
24
+ return Array.from(this.tools.values());
25
+ }
26
+
27
+ getByCategory(category: ToolCategory): ToolDefinition[] {
28
+ return this.getAll().filter((t) => t.category === category);
29
+ }
30
+
31
+ getMap(): Map<string, ToolDefinition> {
32
+ return new Map(this.tools);
33
+ }
34
+
35
+ has(name: string): boolean {
36
+ return this.tools.has(name);
37
+ }
38
+
39
+ names(): string[] {
40
+ return Array.from(this.tools.keys());
41
+ }
42
+
43
+ /**
44
+ * Load all built-in tools
45
+ */
46
+ loadBuiltins(): void {
47
+ const builtins = getBuiltinTools();
48
+ for (const tool of builtins) {
49
+ this.register(tool);
50
+ }
51
+ }
52
+ }
53
+
54
+ // Lazy import to avoid circular deps - tools are loaded at runtime
55
+ function getBuiltinTools(): ToolDefinition[] {
56
+ // Will be populated by the tools/index.ts
57
+ return [];
58
+ }
@@ -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
+ }