aiframe-agent-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.
@@ -0,0 +1,1240 @@
1
+ // src/commands/init.ts
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ import picocolors from "picocolors";
5
+ async function initCommand() {
6
+ const cwd = process.cwd();
7
+ const aiframeDir = path.join(cwd, ".aiframe");
8
+ if (fs.existsSync(aiframeDir)) {
9
+ console.log(picocolors.yellow("\u26A0\uFE0F .aiframe directory already exists. Skipping bootstrap."));
10
+ return;
11
+ }
12
+ try {
13
+ console.log(picocolors.blue("\u{1F680} Bootstrapping .aiframe project structure..."));
14
+ const dirs = ["agents", "tools", "workflows", "instructions", "skills", "state", "output"];
15
+ for (const dir of dirs) {
16
+ await fs.ensureDir(path.join(aiframeDir, dir));
17
+ }
18
+ const configContent = `
19
+ # Global AI Frame Configuration
20
+ default_model: gemini-1.5-flash
21
+ openai_api_key: env.OPENAI_API_KEY
22
+ google_api_key: env.GOOGLE_API_KEY
23
+ system:
24
+ max_loop_limit: 3
25
+ save_checkpoint: true
26
+ `;
27
+ await fs.writeFile(path.join(aiframeDir, "config.yaml"), configContent.trim());
28
+ const agentContent = `
29
+ name: coder
30
+ model: gemini-1.5-flash
31
+ role: "L\u1EADp tr\xECnh vi\xEAn chuy\xEAn nghi\u1EC7p, nh\u1EADn \u0111\u1EB7c t\u1EA3 thi\u1EBFt k\u1EBF v\xE0 vi\u1EBFt code ch\u1EA5t l\u01B0\u1EE3ng cao."
32
+ tools:
33
+ - tool/read_file
34
+ - tool/write_file
35
+ `;
36
+ await fs.writeFile(path.join(aiframeDir, "agents", "coder.yaml"), agentContent.trim());
37
+ const workflowContent = `
38
+ name: sample_tdd_workflow
39
+ description: Chu tr\xECnh vi\u1EBFt code v\xE0 t\u1EF1 \u0111\u1ED9ng ch\u1EA1y test ki\u1EC3m th\u1EED ch\u1EA5t l\u01B0\u1EE3ng.
40
+
41
+ settings:
42
+ max_loop_limit: 3
43
+ save_checkpoint: true
44
+
45
+ steps:
46
+ - step: 1
47
+ name: implement_code
48
+ use: agent/coder
49
+ instruction: "\u0110\u1ECDc file docs/design_spec.md, vi\u1EBFt class Calculator c\xF3 ph\u01B0\u01A1ng th\u1EE9c add(a, b) v\xE0o src/Calculator.js"
50
+ validation:
51
+ command: "node test/Calculator.test.js"
52
+ expect_exit_code: 0
53
+ max_retries: 3
54
+ `;
55
+ await fs.writeFile(path.join(aiframeDir, "workflows", "sample-workflow.yaml"), workflowContent.trim());
56
+ const skillContent = `---
57
+ name: write-docs
58
+ description: Vi\u1EBFt t\xE0i li\u1EC7u h\u01B0\u1EDBng d\u1EABn k\u1EF9 thu\u1EADt chi ti\u1EBFt
59
+ model: gemini-1.5-flash
60
+ ---
61
+
62
+ # K\u1EF9 n\u0103ng vi\u1EBFt t\xE0i li\u1EC7u h\u01B0\u1EDBng d\u1EABn k\u1EF9 thu\u1EADt
63
+
64
+ Khi th\u1EF1c hi\u1EC7n k\u1EF9 n\u0103ng n\xE0y, h\xE3y tu\xE2n th\u1EE7 c\xE1c quy t\u1EAFc sau:
65
+ 1. Lu\xF4n s\u1EED d\u1EE5ng ng\xF4n ng\u1EEF r\xF5 r\xE0ng, m\u1EA1ch l\u1EA1c, d\u1EC5 hi\u1EC3u.
66
+ 2. Cung c\u1EA5p v\xED d\u1EE5 th\u1EF1c t\u1EBF cho m\u1ED7i t\xEDnh n\u0103ng.
67
+ 3. S\u1EED d\u1EE5ng \u0111\u1ECBnh d\u1EA1ng b\u1EA3ng bi\u1EC3u \u0111\u1EC3 so s\xE1nh n\u1EBFu c\xF3 nhi\u1EC1u t\xF9y ch\u1ECDn.
68
+ `;
69
+ await fs.writeFile(path.join(aiframeDir, "skills", "write-docs.md"), skillContent.trim());
70
+ const semgrepContent = `
71
+ name: semgrep
72
+ commands:
73
+ scan:
74
+ args:
75
+ - flag: "--config"
76
+ param: "config"
77
+ - flag: "--error"
78
+ param: "error"
79
+ type: "boolean"
80
+ `;
81
+ await fs.writeFile(path.join(aiframeDir, "tools", "semgrep.yaml"), semgrepContent.trim());
82
+ const sonarContent = `
83
+ name: sonar_scanner
84
+ commands:
85
+ scan:
86
+ args:
87
+ - flag: "-Dsonar.host.url"
88
+ param: "host"
89
+ - flag: "-Dsonar.token"
90
+ param: "token"
91
+ `;
92
+ await fs.writeFile(path.join(aiframeDir, "tools", "sonar_scanner.yaml"), sonarContent.trim());
93
+ const aiRulesContent = `
94
+ # AI Rules for this Workspace
95
+
96
+ Khi l\xE0m vi\u1EC7c trong workspace n\xE0y, Agent ph\u1EA3i tu\xE2n th\u1EE7 nghi\xEAm ng\u1EB7t c\xE1c quy \u0111\u1ECBnh:
97
+ - T\u1EA5t c\u1EA3 m\xE3 ngu\u1ED3n vi\u1EBFt ra ph\u1EA3i tu\xE2n th\u1EE7 chu\u1EA9n ES Modules (ESM).
98
+ - Kh\xF4ng t\u1EF1 \xFD th\xEAm package v\xE0o package.json m\xE0 ch\u01B0a \u0111\u01B0\u1EE3c s\u1EF1 \u0111\u1ED3ng \xFD c\u1EE7a con ng\u01B0\u1EDDi.
99
+ - Lu\xF4n vi\u1EBFt unit test bao ph\u1EE7 cho c\xE1c h\xE0m nghi\u1EC7p v\u1EE5 m\u1EDBi.
100
+ `;
101
+ await fs.writeFile(path.join(cwd, ".ai-rules.md"), aiRulesContent.trim());
102
+ const guidelineContent = `
103
+ # AI Frame CLI Guideline
104
+
105
+ Ch\xE0o m\u1EEBng b\u1EA1n \u0111\u1EBFn v\u1EDBi **AI Frame CLI** - N\u1EC1n t\u1EA3ng x\xE2y d\u1EF1ng AI Agent Framework!
106
+ D\u01B0\u1EDBi \u0111\xE2y l\xE0 c\u1EA5u tr\xFAc th\u01B0 m\u1EE5c \`.aiframe\` v\u1EEBa \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o v\xE0 c\xE1ch b\u1EA1n s\u1EED d\u1EE5ng ch\xFAng:
107
+
108
+ ## C\u1EA5u tr\xFAc th\u01B0 m\u1EE5c
109
+
110
+ - \`.aiframe/agents/\`: Ch\u1EE9a c\xE1c file YAML \u0111\u1ECBnh ngh\u0129a t\xE1c nh\xE2n AI (vai tr\xF2, model, c\xF4ng c\u1EE5).
111
+ - \`.aiframe/workflows/\`: Ch\u1EE9a file YAML \u0111\u1ECBnh ngh\u0129a lu\u1ED3ng l\xE0m vi\u1EC7c (steps, logic, retry).
112
+ - \`.aiframe/tools/\`: \u0110\u1ECBnh ngh\u0129a mapping c\xF4ng c\u1EE5 CLI \u0111\u1EC3 Agent c\xF3 th\u1EC3 g\u1ECDi an to\xE0n.
113
+ - \`.aiframe/skills/\`: N\u01A1i ch\u1EE9a t\xE0i li\u1EC7u k\u1EF9 n\u0103ng (.md) \u0111\u1EC3 chia s\u1EBB ki\u1EBFn th\u1EE9c cho Agent.
114
+ - \`.aiframe/output/\`: Th\u01B0 m\u1EE5c ch\u1EE9a c\xE1c file sinh ra t\u1EEB AI (\u0111\u01B0\u1EE3c ph\xE2n c\xE1ch theo m\xE3 Execution ID c\u1EE7a m\u1ED7i lu\u1ED3ng ch\u1EA1y).
115
+ - \`.aiframe/state/\`: (N\u1ED9i b\u1ED9) Ch\u1EE9a l\u1ECBch s\u1EED ch\u1EA1y v\xE0 checkpoint.
116
+
117
+ ## C\xE1ch s\u1EED d\u1EE5ng c\u01A1 b\u1EA3n
118
+
119
+ 1. **Ch\u1EA1y m\u1ED9t Workflow:**
120
+ \`\`\`bash
121
+ npx aiframe run .aiframe/workflows/sample-workflow.yaml
122
+ \`\`\`
123
+
124
+ 2. **Ch\u1EA1y giao di\u1EC7n Web (Dashboard):**
125
+ \`\`\`bash
126
+ npx aiframe ui -p 3000
127
+ \`\`\`
128
+
129
+ ## Tu\u1EF3 bi\u1EBFn LLM Provider (T\xEDch h\u1EE3p AI)
130
+
131
+ H\u1EC7 th\u1ED1ng h\u1ED7 tr\u1EE3 nhi\u1EC1u ph\u01B0\u01A1ng th\u1EE9c g\u1ECDi AI kh\xE1c nhau tu\u1EF3 thu\u1ED9c v\xE0o nhu c\u1EA7u c\u1EA5u h\xECnh c\u1EE7a b\u1EA1n:
132
+
133
+ ### 1. S\u1EED d\u1EE5ng Cloud API (M\u1EB7c \u0111\u1ECBnh)
134
+ \u0110\u1EC3 s\u1EED d\u1EE5ng s\u1EE9c m\u1EA1nh g\u1ED1c c\u1EE7a Framework, b\u1EA1n c\xF3 th\u1EC3 truy\u1EC1n API Key v\xE0o file \`.aiframe/config.yaml\`:
135
+ \`\`\`yaml
136
+ # .aiframe/config.yaml
137
+ default_model: gemini-1.5-flash
138
+ google_api_key: "AIzaSy..." # Ho\u1EB7c d\xF9ng env.GOOGLE_API_KEY
139
+ openai_api_key: "sk-..." # D\xF9ng cho c\xE1c model gpt-4
140
+ \`\`\`
141
+ *L\u01B0u \xFD: M\u1ECDi Agent YAML kh\xF4ng c\xF3 th\u1EBB \`provider\` s\u1EBD m\u1EB7c \u0111\u1ECBnh s\u1EED d\u1EE5ng c\u1EA5u h\xECnh n\xE0y.*
142
+
143
+ ### 2. S\u1EED d\u1EE5ng Local CLI Agents (Claude Code, Gemini CLI, Antigravity)
144
+ N\u1EBFu b\u1EA1n \u0111\xE3 c\xF3 c\xF4ng c\u1EE5 AI d\u1EA1ng d\xF2ng l\u1EC7nh c\xE0i tr\xEAn m\xE1y, b\u1EA1n c\xF3 th\u1EC3 bi\u1EBFn n\xF3 th\xE0nh "\u0111\u1ED9ng c\u01A1" th\u1EF1c thi cho c\xE1c Agent b\u1EB1ng c\xE1ch khai b\xE1o thu\u1ED9c t\xEDnh \`provider: "cli"\`.
145
+ M\u1EDF file \`.aiframe/agents/t\xEAn_agent.yaml\` v\xE0 c\u1EA5u h\xECnh nh\u01B0 sau:
146
+
147
+ **V\xED d\u1EE5 c\u1EA5u h\xECnh d\xF9ng Claude Code:**
148
+ \`\`\`yaml
149
+ name: "Claude Code Agent"
150
+ provider: "cli"
151
+ cli_command: ["claude", "-p", "{{prompt}}"]
152
+ role: "B\u1EA1n l\xE0 chuy\xEAn gia l\u1EADp tr\xECnh..."
153
+ \`\`\`
154
+
155
+ **V\xED d\u1EE5 c\u1EA5u h\xECnh d\xF9ng Antigravity (VS Code IDE Integration):**
156
+ \`\`\`yaml
157
+ name: "Antigravity Agent"
158
+ provider: "cli"
159
+ cli_command: ["antigravity", "chat", "-m", "agent", "{{prompt}}"]
160
+ role: "B\u1EA1n l\xE0 chuy\xEAn gia thi\u1EBFt k\u1EBF h\u1EC7 th\u1ED1ng..."
161
+ \`\`\`
162
+
163
+ *(L\u01B0u \xFD: C\xE1c l\u1EC7nh CLI n\xE0y s\u1EBD l\u1EA5y \`System Prompt\` v\xE0 \`Instruction\` g\u1ED9p th\xE0nh bi\u1EBFn \`{{prompt}}\` \u0111\u1EC3 truy\u1EC1n th\u1EB3ng cho CLI. B\u1EA1n c\xF3 th\u1EC3 t\u01B0\u01A1ng t\xE1c tr\u1EF1c ti\u1EBFp v\u1EDBi UI c\u1EE7a CLI \u0111\xF3 tr\xEAn terminal c\u1EE7a m\xECnh!)*
164
+
165
+ ### 3. S\u1EED d\u1EE5ng Local LLM (Ollama)
166
+ N\u1EBFu b\u1EA1n mu\u1ED1n ch\u1EA1y ho\xE0n to\xE0n offline b\u1EB1ng m\xE1y c\u1EA5u h\xECnh m\u1EA1nh, s\u1EED d\u1EE5ng Ollama:
167
+ \`\`\`yaml
168
+ # .aiframe/config.yaml
169
+ default_model: ollama/llama3
170
+ \`\`\`
171
+
172
+ ---
173
+
174
+ ## H\u01B0\u1EDBng d\u1EABn thi\u1EBFt k\u1EBF Workflow
175
+
176
+ Lu\u1ED3ng c\xF4ng vi\u1EC7c (Workflow) l\xE0 tr\xE1i tim c\u1EE7a AI Frame CLI. B\u1EA1n \u0111\u1ECBnh ngh\u0129a logic v\xE0o file trong \`.aiframe/workflows/t\xEAn-lu\u1ED3ng.yaml\`.
177
+
178
+ **C\u1EA5u tr\xFAc c\u01A1 b\u1EA3n:**
179
+ \`\`\`yaml
180
+ name: "Demo Workflow"
181
+ description: "M\u1ED9t lu\u1ED3ng c\xF4ng vi\u1EC7c \u0111\u01A1n gi\u1EA3n"
182
+ settings:
183
+ max_loop_limit: 3 # S\u1ED1 l\u1EA7n retry t\u1ED1i \u0111a n\u1EBFu b\u01B0\u1EDBc ch\u1EA1y th\u1EA5t b\u1EA1i
184
+ save_checkpoint: true # T\u1EF1 \u0111\u1ED9ng l\u01B0u checkpoint \u0111\u1EC3 c\xF3 th\u1EC3 Resume
185
+
186
+ steps:
187
+ - name: "B\u01B0\u1EDBc 1: Research"
188
+ use: "agent/researcher" # G\u1ECDi agent t\xEAn l\xE0 researcher.yaml
189
+ instruction: "H\xE3y t\xECm ki\u1EBFm... v\xE0 l\u01B0u file v\xE0o {{output_dir}}/result.md"
190
+
191
+ - name: "B\u01B0\u1EDBc 2: X\u1EED l\xFD file"
192
+ command: "cat {{output_dir}}/result.md | grep 'AI'" # G\u1ECDi l\u1EC7nh bash thu\u1EA7n tu\xFD
193
+ \`\`\`
194
+
195
+ **L\u01B0u \xFD quan tr\u1ECDng:** Lu\xF4n c\u1ED1 g\u1EAFng s\u1EED d\u1EE5ng bi\u1EBFn \`{{output_dir}}\` \u0111\u1EC3 h\u1EC7 th\u1ED1ng l\u01B0u t\u1EA5t c\u1EA3 c\xE1c file t\u1EADp trung v\xE0o chung m\u1ED9t th\u01B0 m\u1EE5c \u0111\u1EA7u ra (\`.aiframe/output/\`), tr\xE1nh l\xE0m r\xE1c th\u01B0 m\u1EE5c g\u1ED1c d\u1EF1 \xE1n c\u1EE7a b\u1EA1n.
196
+
197
+ ---
198
+
199
+ ## T\xE1i s\u1EED d\u1EE5ng Agent & Skill b\xEAn ngo\xE0i
200
+
201
+ N\u1EBFu b\u1EA1n c\xF3 m\u1ED9t kho Agent d\xF9ng chung (v\xED d\u1EE5 \u1EDF \`/home/user/.agents/\`), b\u1EA1n ho\xE0n to\xE0n c\xF3 th\u1EC3 g\u1ECDi ch\xFAng b\u1EB1ng **\u0111\u01B0\u1EDDng d\u1EABn tuy\u1EC7t \u0111\u1ED1i** ho\u1EB7c **\u0111\u01B0\u1EDDng d\u1EABn t\u01B0\u01A1ng \u0111\u1ED1i** thay v\xEC copy file YAML v\xE0o t\u1EEBng d\u1EF1 \xE1n.
202
+
203
+ V\xED d\u1EE5:
204
+ \`\`\`yaml
205
+ steps:
206
+ - name: "G\u1ECDi bmad-agent-dev"
207
+ use: "agent//home/anhvu/Documents/AI/AI Brain/AI-Brain/.agents/skills/bmad-agent-dev/SKILL.md"
208
+ instruction: "L\xE0m..."
209
+ \`\`\`
210
+ *(L\u01B0u \xFD: B\u1EA1n ch\u1EC9 c\u1EA7n ch\xE8n \u0111\u01B0\u1EDDng d\u1EABn file ngay sau \`agent/\`)*
211
+
212
+ ---
213
+
214
+ ## \xC1p d\u1EE5ng TDD v\xE0 SDD v\u1EDBi AI Frame
215
+
216
+ V\u1EDBi c\u01A1 ch\u1EBF **Validation Auto-Healing (V\xF2ng l\u1EB7p t\u1EF1 s\u1EEDa l\u1ED7i qua Validation)**, b\u1EA1n c\xF3 th\u1EC3 thi\u1EBFt k\u1EBF Workflow \u0111\u1EC3 \xE9p AI code theo chu\u1EA9n TDD ho\u1EB7c SDD.
217
+
218
+ ### 1. TDD (Test-Driven Development)
219
+ B\u1EA1n chia lu\u1ED3ng th\xE0nh 2 b\u01B0\u1EDBc (vi\u1EBFt test -> vi\u1EBFt code \u0111\u1EC3 pass test):
220
+ \`\`\`yaml
221
+ name: "TDD Workflow"
222
+ settings:
223
+ max_loop_limit: 5 # Cho AI t\u1EF1 s\u1EEDa sai t\u1ED1i \u0111a 5 l\u1EA7n
224
+
225
+ steps:
226
+ - name: 'Write Tests First'
227
+ use: agent/tester
228
+ instruction: "Vi\u1EBFt Unit Test b\u1EB1ng Jest cho h\xE0m t\xEDnh thu\u1EBF v\xE0o tests/tax.test.ts"
229
+
230
+ - name: 'Implement Code'
231
+ use: agent/coder
232
+ instruction: "\u0110\u1ECDc file tests/tax.test.ts v\xE0 vi\u1EBFt logic code v\xE0o src/tax.ts sao cho pass test."
233
+ validation:
234
+ command: "npx jest tests/tax.test.ts"
235
+ expect_exit_code: 0
236
+ max_retries: 5 # N\u1EBFu ch\u1EA1y Jest l\u1ED7i, text l\u1ED7i s\u1EBD \u0111\u01B0\u1EE3c nh\u1ED3i l\u1EA1i v\xE0o Agent \u0111\u1EC3 t\u1EF1 s\u1EEDa code!
237
+ \`\`\`
238
+
239
+ ### 2. SDD (Specification-Driven Development)
240
+ \u0110\u1EA3m b\u1EA3o AI vi\u1EBFt Spec tr\u01B0\u1EDBc r\u1ED3i code b\xE1m theo Spec:
241
+ \`\`\`yaml
242
+ name: "SDD Workflow"
243
+ steps:
244
+ - name: 'Create Architecture Spec'
245
+ use: agent/architect
246
+ instruction: "Vi\u1EBFt System Design cho t\xEDnh n\u0103ng \u0110\u0103ng nh\u1EADp v\xE0o docs/login_spec.md"
247
+
248
+ - name: 'Coding'
249
+ use: agent/coder
250
+ instruction: "Code t\xEDnh n\u0103ng \u0110\u0103ng nh\u1EADp b\xE1m s\xE1t y\xEAu c\u1EA7u t\u1EA1i docs/login_spec.md"
251
+
252
+ - name: 'Spec Validation (Cross-check)'
253
+ use: agent/reviewer
254
+ instruction: "\u0110\u1ED1i chi\u1EBFu m\xE3 ngu\u1ED3n v\u1EDBi docs/login_spec.md. N\u1EBFu thi\u1EBFu ti\xEAu ch\xED n\xE0o, h\xE3y b\xE1o l\u1ED7i \u0111\u1EC3 Coder s\u1EEDa."
255
+ validation:
256
+ command: "node scripts/ai-review.js" # Script tu\u1EF3 ch\u1EC9nh \u0111\u1EC3 ch\u1EA1y reviewer
257
+ expect_exit_code: 0
258
+ max_retries: 3
259
+ \`\`\`
260
+ `;
261
+ await fs.writeFile(path.join(cwd, "GUIDELINE.md"), guidelineContent.trim());
262
+ console.log(picocolors.green("\u{1F389} Done! Bootstrap completed successfully."));
263
+ console.log(picocolors.cyan("C\u1EA5u tr\xFAc th\u01B0 m\u1EE5c \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o t\u1EA1i: " + aiframeDir));
264
+ } catch (error) {
265
+ console.error(picocolors.red("\u274C Error bootstrapping .aiframe:"), error);
266
+ }
267
+ }
268
+
269
+ // src/core/executor.ts
270
+ import { execa } from "execa";
271
+ async function executeCommand(cmd, args, options = {}) {
272
+ const timeout = options.timeout ?? 6e4;
273
+ const cwd = options.cwd ?? process.cwd();
274
+ try {
275
+ const result = await execa(cmd, args, {
276
+ cwd,
277
+ timeout,
278
+ reject: false
279
+ // Don't throw on non-zero exit codes, return the result
280
+ });
281
+ return {
282
+ stdout: result.stdout,
283
+ stderr: result.stderr,
284
+ exitCode: result.exitCode,
285
+ success: result.exitCode === 0
286
+ };
287
+ } catch (err) {
288
+ return {
289
+ stdout: "",
290
+ stderr: err.message || "Unknown execution error",
291
+ exitCode: err.exitCode ?? -1,
292
+ success: false,
293
+ error: err
294
+ };
295
+ }
296
+ }
297
+
298
+ // src/core/llm.ts
299
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
300
+ import { createOpenAI } from "@ai-sdk/openai";
301
+ import fs2 from "fs-extra";
302
+ import path2 from "path";
303
+ import yaml from "js-yaml";
304
+ function getGoogleModel(modelName = "gemini-1.5-flash") {
305
+ let apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY || "";
306
+ let provider = "google";
307
+ const configPath = path2.join(process.cwd(), ".aiframe", "config.yaml");
308
+ if (fs2.existsSync(configPath)) {
309
+ try {
310
+ const config = yaml.load(fs2.readFileSync(configPath, "utf8"));
311
+ if (config.google_api_key) {
312
+ if (config.google_api_key.startsWith("env.")) {
313
+ const envVar = config.google_api_key.split(".")[1];
314
+ apiKey = process.env[envVar] || "";
315
+ } else {
316
+ apiKey = config.google_api_key;
317
+ }
318
+ }
319
+ if (config.default_model?.startsWith("ollama/")) {
320
+ provider = "ollama";
321
+ modelName = config.default_model.split("/")[1];
322
+ }
323
+ } catch (e) {
324
+ }
325
+ }
326
+ if (provider === "ollama") {
327
+ const openai = createOpenAI({
328
+ baseURL: "http://localhost:11434/v1",
329
+ apiKey: "ollama"
330
+ });
331
+ return openai(modelName);
332
+ }
333
+ if (apiKey) {
334
+ const google = createGoogleGenerativeAI({
335
+ apiKey
336
+ });
337
+ return google(modelName);
338
+ }
339
+ return createMockModel();
340
+ }
341
+ function createMockModel() {
342
+ return {
343
+ modelId: "mock-model",
344
+ specificationVersion: "v1",
345
+ doGenerate: async (options) => {
346
+ const prompt = options.prompt || "";
347
+ let text = "Mock LLM Response";
348
+ if (prompt.includes("CLI command-line argument mapping helper")) {
349
+ if (prompt.includes("commit") || prompt.includes("message")) {
350
+ text = '["commit", "-m", "test commit message", "--amend"]';
351
+ } else {
352
+ text = "[]";
353
+ }
354
+ } else if (prompt.includes("tech_lead")) {
355
+ text = "Tech Lead: I will call the coder sub-agent to create the file.";
356
+ } else if (prompt.includes("coder")) {
357
+ text = "Coder: File written successfully.";
358
+ }
359
+ return {
360
+ text,
361
+ finishReason: "stop",
362
+ usage: { promptTokens: 10, completionTokens: 10 }
363
+ };
364
+ }
365
+ };
366
+ }
367
+
368
+ // src/core/mapper.ts
369
+ import fs3 from "fs-extra";
370
+ import path3 from "path";
371
+ import yaml2 from "js-yaml";
372
+ import { generateText } from "ai";
373
+ async function mapArguments(toolName, subCommand, params) {
374
+ const cwd = process.cwd();
375
+ const toolYamlPath = path3.join(cwd, ".aiframe", "tools", `${toolName}.yaml`);
376
+ if (fs3.existsSync(toolYamlPath)) {
377
+ try {
378
+ const toolConfig = yaml2.load(fs3.readFileSync(toolYamlPath, "utf8"));
379
+ if (toolConfig && toolConfig.commands) {
380
+ const cmdKey = subCommand || "default";
381
+ const cmdConfig = toolConfig.commands[cmdKey];
382
+ if (cmdConfig && cmdConfig.args) {
383
+ const mappedArgs = [];
384
+ if (subCommand) {
385
+ mappedArgs.push(subCommand);
386
+ }
387
+ for (const rule of cmdConfig.args) {
388
+ const val = params[rule.param];
389
+ if (val !== void 0 && val !== null) {
390
+ if (rule.type === "boolean") {
391
+ if (val === true) {
392
+ mappedArgs.push(rule.flag);
393
+ }
394
+ } else {
395
+ mappedArgs.push(rule.flag);
396
+ mappedArgs.push(String(val));
397
+ }
398
+ }
399
+ }
400
+ return mappedArgs;
401
+ }
402
+ }
403
+ } catch (e) {
404
+ console.warn(`\u26A0\uFE0F Error parsing manual mapping for tool ${toolName}:`, e);
405
+ }
406
+ }
407
+ console.log(`\u{1F50D} Mapping arguments for ${toolName} ${subCommand || ""} using AI...`);
408
+ let helpText = "";
409
+ const helpArgs = subCommand ? [subCommand, "--help"] : ["--help"];
410
+ const helpResult = await executeCommand(toolName, helpArgs);
411
+ if (helpResult.success) {
412
+ helpText = helpResult.stdout || helpResult.stderr;
413
+ } else {
414
+ const alternativeHelpArgs = subCommand ? ["help", subCommand] : ["help"];
415
+ const altHelpResult = await executeCommand(toolName, alternativeHelpArgs);
416
+ helpText = altHelpResult.stdout || altHelpResult.stderr;
417
+ }
418
+ if (!helpText) {
419
+ helpText = "No help output available. Use general conventions.";
420
+ }
421
+ if (helpText.length > 5e3) {
422
+ helpText = helpText.substring(0, 5e3) + "... [truncated]";
423
+ }
424
+ const model = getGoogleModel();
425
+ const prompt = `
426
+ You are a CLI command-line argument mapping helper.
427
+ We have the CLI executable name: "${toolName}"
428
+ Subcommand (if any): "${subCommand || ""}"
429
+ Here is the help documentation or description of the tool:
430
+ \`\`\`
431
+ ${helpText}
432
+ \`\`\`
433
+
434
+ Here are the JSON parameters provided by the Agent:
435
+ \`\`\`json
436
+ ${JSON.stringify(params, null, 2)}
437
+ \`\`\`
438
+
439
+ Based on the help docs and general CLI standards, map these parameters to the corresponding CLI flags and arguments.
440
+ Output only a valid JSON string array of the final command arguments.
441
+ Do NOT include the executable name "${toolName}" in the array, but DO include the subcommand "${subCommand || ""}" at the beginning of the array if it is present.
442
+
443
+ Example output format:
444
+ ["commit", "-m", "initial commit", "--amend"]
445
+
446
+ Return ONLY the JSON array inside markdown code block or plain text. Do not write any other explanation or text.
447
+ `;
448
+ try {
449
+ const { text } = await generateText({
450
+ model,
451
+ prompt
452
+ });
453
+ const cleanedText = text.replace(/```json/g, "").replace(/```/g, "").trim();
454
+ const parsed = JSON.parse(cleanedText);
455
+ if (Array.isArray(parsed)) {
456
+ return parsed.map(String);
457
+ }
458
+ } catch (err) {
459
+ console.error("\u274C Failed to auto-map arguments using LLM:", err);
460
+ }
461
+ const fallbackArgs = [];
462
+ if (subCommand) {
463
+ fallbackArgs.push(subCommand);
464
+ }
465
+ for (const [key, val] of Object.entries(params)) {
466
+ if (val === true) {
467
+ fallbackArgs.push(`--${key}`);
468
+ } else if (val !== false && val !== null && val !== void 0) {
469
+ fallbackArgs.push(`--${key}`);
470
+ fallbackArgs.push(String(val));
471
+ }
472
+ }
473
+ return fallbackArgs;
474
+ }
475
+
476
+ // src/core/agent.ts
477
+ import fs4 from "fs-extra";
478
+ import path4 from "path";
479
+ import yaml3 from "js-yaml";
480
+ import picocolors2 from "picocolors";
481
+ import { execa as execa2 } from "execa";
482
+ import { generateText as generateText2, tool } from "ai";
483
+ import { z } from "zod";
484
+ async function executeAgent(agentName, instruction, depth = 0, workflowOutputDir, workingDir) {
485
+ if (depth > 2) {
486
+ return "Error: Maximum sub-agent delegation depth reached (depth limit = 2).";
487
+ }
488
+ const cwd = process.cwd();
489
+ const agentWorkspace = workingDir || cwd;
490
+ let agentYamlPath = path4.join(cwd, ".aiframe", "agents", `${agentName}.yaml`);
491
+ if (path4.isAbsolute(agentName)) {
492
+ agentYamlPath = agentName.endsWith(".yaml") ? agentName : `${agentName}.yaml`;
493
+ } else if (agentName.startsWith(".")) {
494
+ agentYamlPath = path4.resolve(cwd, agentName.endsWith(".yaml") ? agentName : `${agentName}.yaml`);
495
+ }
496
+ if (!fs4.existsSync(agentYamlPath)) {
497
+ throw new Error(`Agent configuration for "${agentName}" not found at ${agentYamlPath}`);
498
+ }
499
+ const fileContent = await fs4.readFile(agentYamlPath, "utf8");
500
+ const agentConfig = yaml3.load(fileContent);
501
+ const rulesPath = path4.join(cwd, ".ai-rules.md");
502
+ let workspaceRules = "";
503
+ if (fs4.existsSync(rulesPath)) {
504
+ workspaceRules = await fs4.readFile(rulesPath, "utf8");
505
+ }
506
+ const outputDir = workflowOutputDir || path4.join(cwd, ".aiframe", "output", "default");
507
+ await fs4.ensureDir(outputDir);
508
+ const systemPrompt = `
509
+ You are the Agent "${agentConfig.name || agentName}".
510
+ Your role and instructions:
511
+ ${agentConfig.role}
512
+
513
+ ${workspaceRules ? `Ensure you follow these workspace rules:
514
+ ${workspaceRules}` : ""}
515
+
516
+ You operate on the local file system. Do not write placeholders; write actual code.
517
+ Always focus on the user's instruction and return a clean summary when you finish.
518
+ Output Directory: ${outputDir}
519
+ (IMPORTANT: Please save all generated artifacts, reports, slides, and results into this Output Directory by default unless the user specifies otherwise.)
520
+ `;
521
+ const resolveSafePath = (requestedPath, allowedRoot) => {
522
+ const resolved = path4.resolve(allowedRoot, requestedPath);
523
+ const relative = path4.relative(allowedRoot, resolved);
524
+ if (relative.startsWith("..") || path4.isAbsolute(relative)) {
525
+ throw new Error(`Access Denied: Path escapes allowed workspace root: ${requestedPath}`);
526
+ }
527
+ return resolved;
528
+ };
529
+ const agentTools = {
530
+ read_file: tool({
531
+ description: "Read the contents of a file in the workspace.",
532
+ parameters: z.object({
533
+ filePath: z.string().describe("The path of the file to read, relative to workspace root.")
534
+ }),
535
+ execute: async ({ filePath }) => {
536
+ try {
537
+ const allowedRoot = agentConfig.workspace?.root ? path4.resolve(agentWorkspace, agentConfig.workspace.root) : agentWorkspace;
538
+ const fullPath = resolveSafePath(filePath, allowedRoot);
539
+ if (!fs4.existsSync(fullPath)) {
540
+ return `Error: File not found at ${filePath}`;
541
+ }
542
+ console.log(picocolors2.gray(` [File Read] ${filePath}`));
543
+ return await fs4.readFile(fullPath, "utf8");
544
+ } catch (e) {
545
+ return `Error: ${e.message}`;
546
+ }
547
+ }
548
+ }),
549
+ write_file: tool({
550
+ description: "Write or overwrite contents of a file in the workspace.",
551
+ parameters: z.object({
552
+ filePath: z.string().describe("The path of the file to write, relative to workspace root."),
553
+ content: z.string().describe("The complete file content to write.")
554
+ }),
555
+ execute: async ({ filePath, content }) => {
556
+ try {
557
+ const allowedRoot = agentConfig.workspace?.root ? path4.resolve(agentWorkspace, agentConfig.workspace.root) : agentWorkspace;
558
+ const fullPath = resolveSafePath(filePath, allowedRoot);
559
+ await fs4.ensureDir(path4.dirname(fullPath));
560
+ await fs4.writeFile(fullPath, content, "utf8");
561
+ console.log(picocolors2.gray(` [File Write] ${filePath}`));
562
+ return `Success: File written successfully to ${filePath}`;
563
+ } catch (e) {
564
+ return `Error: ${e.message}`;
565
+ }
566
+ }
567
+ })
568
+ };
569
+ if (agentConfig.tools) {
570
+ for (const t of agentConfig.tools) {
571
+ if (t.startsWith("tool/")) {
572
+ const toolName = t.split("/")[1];
573
+ agentTools[toolName] = tool({
574
+ description: `Run the CLI tool "${toolName}" with custom JSON parameters.`,
575
+ parameters: z.object({
576
+ subCommand: z.string().nullable().describe('Subcommand to run, if any (e.g. "commit", "status").'),
577
+ params: z.record(z.any()).describe("JSON parameters corresponding to the CLI options.")
578
+ }),
579
+ execute: async ({ subCommand, params }) => {
580
+ const args = await mapArguments(toolName, subCommand, params);
581
+ console.log(picocolors2.gray(` [CLI Exec] Running: ${toolName} ${args.join(" ")}`));
582
+ const result = await executeCommand(toolName, args, { cwd: agentWorkspace });
583
+ return JSON.stringify({
584
+ success: result.success,
585
+ exitCode: result.exitCode,
586
+ stdout: result.stdout,
587
+ stderr: result.stderr
588
+ });
589
+ }
590
+ });
591
+ }
592
+ }
593
+ }
594
+ if (agentConfig.allowed_sub_agents) {
595
+ for (const subAgent of agentConfig.allowed_sub_agents) {
596
+ if (subAgent.startsWith("agent/")) {
597
+ const subAgentName = subAgent.split("/")[1];
598
+ agentTools[`call_${subAgentName}`] = tool({
599
+ description: `Delegate a sub-task to the sub-agent "${subAgentName}".`,
600
+ parameters: z.object({
601
+ subTaskInstruction: z.string().describe("Detailed instruction for the sub-agent.")
602
+ }),
603
+ execute: async ({ subTaskInstruction }) => {
604
+ console.log(picocolors2.magenta(` [Delegating -> ${subAgentName}] Instruction: "${subTaskInstruction}"`));
605
+ return await executeAgent(subAgentName, subTaskInstruction, depth + 1, workflowOutputDir, workingDir);
606
+ }
607
+ });
608
+ }
609
+ }
610
+ }
611
+ if (agentConfig.provider === "cli") {
612
+ if (!agentConfig.cli_command || !Array.isArray(agentConfig.cli_command)) {
613
+ throw new Error(`Agent "${agentName}" uses provider "cli" but missing or invalid "cli_command" array. Example: ["claude", "-p", "{{prompt}}"]`);
614
+ }
615
+ const fullPrompt = `${systemPrompt}
616
+
617
+ Task:
618
+ ${instruction}`;
619
+ const args = agentConfig.cli_command.slice(1).map((arg) => {
620
+ return arg.replace("{{prompt}}", fullPrompt);
621
+ });
622
+ const cmd = agentConfig.cli_command[0];
623
+ console.log(picocolors2.yellow(`
624
+ \u{1F680} [CLI Delegation] U\u1EF7 quy\u1EC1n cho Agent n\u1ED9i b\u1ED9: ${cmd} ...`));
625
+ try {
626
+ await execa2(cmd, args, { stdio: "inherit", cwd: agentWorkspace });
627
+ return `Success: Qu\xE1 tr\xECnh u\u1EF7 quy\u1EC1n cho CLI Agent ${cmd} \u0111\xE3 ho\xE0n t\u1EA5t.`;
628
+ } catch (err) {
629
+ console.error(picocolors2.red(`\u274C Error running CLI Agent "${cmd}":`), err);
630
+ return `Error: CLI Agent exited with code ${err.exitCode}. ${err.message}`;
631
+ }
632
+ }
633
+ const model = getGoogleModel();
634
+ try {
635
+ const { text } = await generateText2({
636
+ model,
637
+ system: systemPrompt,
638
+ prompt: instruction,
639
+ tools: agentTools,
640
+ maxSteps: 10
641
+ // Max steps of tool call-and-response loops
642
+ });
643
+ return text;
644
+ } catch (err) {
645
+ console.error(picocolors2.red(`\u274C Error running Agent "${agentName}":`), err);
646
+ return `Error: ${err.message || err}`;
647
+ }
648
+ }
649
+
650
+ // src/core/workflow.ts
651
+ import fs5 from "fs-extra";
652
+ import path5 from "path";
653
+ import yaml4 from "js-yaml";
654
+ import picocolors3 from "picocolors";
655
+ import crypto from "crypto";
656
+ var WorkflowEngine = class {
657
+ workflowData;
658
+ workflowPath;
659
+ checkpointPath;
660
+ state;
661
+ constructor(workflowPath) {
662
+ this.workflowPath = path5.resolve(workflowPath);
663
+ const cwd = process.cwd();
664
+ const stateDir = path5.join(cwd, ".aiframe", "state");
665
+ fs5.ensureDirSync(stateDir);
666
+ const workflowFileName = path5.basename(workflowPath, path5.extname(workflowPath));
667
+ this.checkpointPath = path5.join(stateDir, `${workflowFileName}-checkpoint.json`);
668
+ }
669
+ async load(resume = false, inputs) {
670
+ if (!fs5.existsSync(this.workflowPath)) {
671
+ throw new Error(`Workflow file not found at ${this.workflowPath}`);
672
+ }
673
+ const fileContent = await fs5.readFile(this.workflowPath, "utf8");
674
+ this.workflowData = yaml4.load(fileContent);
675
+ if (resume && fs5.existsSync(this.checkpointPath)) {
676
+ console.log(picocolors3.cyan("\u{1F504} Loading checkpoint from: " + this.checkpointPath));
677
+ const stateContent = await fs5.readFile(this.checkpointPath, "utf8");
678
+ this.state = JSON.parse(stateContent);
679
+ this.state.workflowPath = this.workflowPath;
680
+ if (inputs) {
681
+ this.state.inputs = { ...this.state.inputs, ...inputs };
682
+ }
683
+ } else {
684
+ const steps = this.workflowData.steps || [];
685
+ const stepStatuses = {};
686
+ steps.forEach((_, idx) => {
687
+ stepStatuses[idx] = "pending";
688
+ });
689
+ this.state = {
690
+ workflowName: this.workflowData.name || "unnamed_workflow",
691
+ workflowPath: this.workflowPath,
692
+ currentStepIndex: 0,
693
+ status: "in_progress",
694
+ errorHistory: {},
695
+ rollbackHistory: {},
696
+ stepStatuses,
697
+ inputs: inputs || {},
698
+ execId: crypto.randomUUID()
699
+ };
700
+ }
701
+ }
702
+ async saveCheckpoint() {
703
+ const saveCheckpointEnabled = this.workflowData.settings?.save_checkpoint !== false;
704
+ if (saveCheckpointEnabled) {
705
+ await fs5.writeFile(this.checkpointPath, JSON.stringify(this.state, null, 2), "utf8");
706
+ }
707
+ }
708
+ async clearCheckpoint() {
709
+ this.state.status = "completed";
710
+ const steps = this.workflowData.steps || [];
711
+ if (this.state.stepStatuses) {
712
+ steps.forEach((_, idx) => {
713
+ this.state.stepStatuses[idx] = "success";
714
+ });
715
+ }
716
+ await this.saveCheckpoint();
717
+ }
718
+ async run() {
719
+ const steps = this.workflowData.steps || [];
720
+ const maxGlobalLoops = this.workflowData.settings?.max_loop_limit ?? 3;
721
+ const isolateWorkspace = this.workflowData.settings?.isolate_workspace === true;
722
+ let workingDir;
723
+ let branchName;
724
+ console.log(picocolors3.green(`
725
+ \u{1F3AC} Starting Workflow: ${this.state.workflowName}`));
726
+ console.log(picocolors3.gray(`Steps total: ${steps.length}. Starting from Step Index: ${this.state.currentStepIndex}
727
+ `));
728
+ if (isolateWorkspace) {
729
+ console.log(picocolors3.cyan(` [Workspace Isolation] Setting up Git Worktree...`));
730
+ branchName = `aiframe/exec-${this.state.execId}`;
731
+ workingDir = path5.join(process.cwd(), ".aiframe", "workspaces", this.state.execId);
732
+ await fs5.ensureDir(path5.dirname(workingDir));
733
+ const gitCheck = await executeCommand("git", ["rev-parse", "--is-inside-work-tree"]);
734
+ if (!gitCheck.success) {
735
+ console.error(picocolors3.red(`\u274C Workspace isolation failed: Not a git repository. Please run 'git init' first.`));
736
+ return false;
737
+ }
738
+ if (!fs5.existsSync(workingDir)) {
739
+ const wtResult = await executeCommand("git", ["worktree", "add", workingDir, "-b", branchName]);
740
+ if (!wtResult.success) {
741
+ console.error(picocolors3.red(`\u274C Failed to create worktree: ${wtResult.stderr}`));
742
+ return false;
743
+ }
744
+ }
745
+ }
746
+ while (this.state.currentStepIndex < steps.length) {
747
+ const step = steps[this.state.currentStepIndex];
748
+ console.log(picocolors3.yellow(`\u{1F449} [Step ${step.step || this.state.currentStepIndex + 1}] Running: ${step.name}`));
749
+ if (!this.state.stepStatuses) {
750
+ this.state.stepStatuses = {};
751
+ }
752
+ this.state.stepStatuses[this.state.currentStepIndex] = "running";
753
+ await this.saveCheckpoint();
754
+ let success = false;
755
+ let retries = 0;
756
+ const maxRetries = step.max_retries ?? 0;
757
+ while (retries <= maxRetries) {
758
+ if (retries > 0) {
759
+ console.log(picocolors3.blue(` \u{1F504} Retry attempt ${retries}/${maxRetries} for step: ${step.name}`));
760
+ }
761
+ await this.runStepAction(step, retries, workingDir);
762
+ if (step.validation) {
763
+ const validationCmd = typeof step.validation === "string" ? step.validation : step.validation.command;
764
+ const resolvedValCmd = this.resolveVariables(validationCmd);
765
+ const expectExitCode = step.validation.expect_exit_code ?? 0;
766
+ const timeout = step.validation.timeout ?? 6e4;
767
+ console.log(picocolors3.gray(` \u{1F50D} Validating with command: "${resolvedValCmd}"`));
768
+ const parsed = this.parseCommand(resolvedValCmd);
769
+ const valResult = await executeCommand(parsed.cmd, parsed.args, { timeout, cwd: workingDir || process.cwd() });
770
+ if (valResult.exitCode === expectExitCode) {
771
+ console.log(picocolors3.green(` \u2705 Validation passed (Exit code: ${valResult.exitCode})`));
772
+ success = true;
773
+ this.state.stepStatuses[this.state.currentStepIndex] = "success";
774
+ break;
775
+ } else {
776
+ const errorOutput = valResult.stderr || valResult.stdout || "Validation returned unexpected exit code";
777
+ console.log(picocolors3.red(` \u274C Validation failed (Exit code: ${valResult.exitCode})`));
778
+ console.log(picocolors3.gray(` Error context: ${errorOutput.trim().split("\n")[0]}`));
779
+ if (!this.state.errorHistory[this.state.currentStepIndex]) {
780
+ this.state.errorHistory[this.state.currentStepIndex] = [];
781
+ }
782
+ this.state.errorHistory[this.state.currentStepIndex].push(errorOutput);
783
+ const similarErrorsCount = this.state.errorHistory[this.state.currentStepIndex].filter(
784
+ (err) => err.trim() === errorOutput.trim()
785
+ ).length;
786
+ if (similarErrorsCount >= maxGlobalLoops) {
787
+ console.log(picocolors3.red(`
788
+ \u{1F6A8} Loop-Detection Guardrail Triggered!`));
789
+ console.log(picocolors3.red(` Error occurred ${similarErrorsCount} times consecutively.`));
790
+ this.state.status = "failed";
791
+ this.state.stepStatuses[this.state.currentStepIndex] = "failed";
792
+ await this.saveCheckpoint();
793
+ return false;
794
+ }
795
+ retries++;
796
+ }
797
+ } else {
798
+ success = true;
799
+ this.state.stepStatuses[this.state.currentStepIndex] = "success";
800
+ break;
801
+ }
802
+ }
803
+ if (!success) {
804
+ console.log(picocolors3.red(`\u274C Step failed after ${maxRetries} retries.`));
805
+ if (step.on_failure) {
806
+ const targetIndex = this.resolveTargetStepIndex(step.on_failure, steps);
807
+ if (targetIndex !== null && targetIndex < this.state.currentStepIndex) {
808
+ if (!this.state.rollbackHistory) {
809
+ this.state.rollbackHistory = {};
810
+ }
811
+ const currentRollbacks = this.state.rollbackHistory[this.state.currentStepIndex] || 0;
812
+ if (currentRollbacks >= maxGlobalLoops) {
813
+ console.log(picocolors3.red(`\u{1F6A8} Loop-Detection Guardrail Triggered: Max rollback limit (${maxGlobalLoops}) reached for Step ${this.state.currentStepIndex + 1}.`));
814
+ this.state.status = "failed";
815
+ this.state.stepStatuses[this.state.currentStepIndex] = "failed";
816
+ await this.saveCheckpoint();
817
+ return false;
818
+ }
819
+ this.state.rollbackHistory[this.state.currentStepIndex] = currentRollbacks + 1;
820
+ console.log(picocolors3.magenta(` \u{1F504} Routing on failure: Jumping from Step ${this.state.currentStepIndex + 1} back to Step ${targetIndex + 1} ("${steps[targetIndex].name}")`));
821
+ this.state.stepStatuses[this.state.currentStepIndex] = "failed";
822
+ for (let i = targetIndex; i < steps.length; i++) {
823
+ this.state.stepStatuses[i] = "pending";
824
+ delete this.state.errorHistory[i];
825
+ }
826
+ this.state.currentStepIndex = targetIndex;
827
+ await this.saveCheckpoint();
828
+ continue;
829
+ } else if (step.on_failure) {
830
+ console.log(picocolors3.yellow(` \u26A0\uFE0F Invalid or non-previous rollback step target: ${step.on_failure}`));
831
+ }
832
+ }
833
+ this.state.status = "failed";
834
+ this.state.stepStatuses[this.state.currentStepIndex] = "failed";
835
+ await this.saveCheckpoint();
836
+ return false;
837
+ }
838
+ this.state.currentStepIndex++;
839
+ await this.saveCheckpoint();
840
+ }
841
+ console.log(picocolors3.green(`
842
+ \u{1F389} Workflow Completed Successfully: ${this.state.workflowName}`));
843
+ this.state.status = "completed";
844
+ await this.clearCheckpoint();
845
+ if (isolateWorkspace && workingDir && branchName) {
846
+ console.log(picocolors3.cyan(` [Workspace Isolation] Committing changes to branch ${branchName}...`));
847
+ await executeCommand("git", ["add", "."], { cwd: workingDir });
848
+ await executeCommand("git", ["commit", "-m", `chore(aiframe): auto-generated workflow results for ${this.state.workflowName}`], { cwd: workingDir });
849
+ console.log(picocolors3.green(` \u2705 Changes have been safely committed to branch: ${branchName}`));
850
+ }
851
+ return true;
852
+ }
853
+ resolveVariables(text) {
854
+ if (!text || typeof text !== "string") return text;
855
+ let resolved = text;
856
+ const inputs = this.state.inputs || {};
857
+ const outputDir = path5.join(process.cwd(), ".aiframe", "output", this.state.execId);
858
+ fs5.ensureDirSync(outputDir);
859
+ resolved = resolved.replace(/{{\s*output_dir\s*}}/g, outputDir);
860
+ for (const key of Object.keys(inputs)) {
861
+ const val = inputs[key] || "";
862
+ resolved = resolved.replace(new RegExp(`{{\\s*${key}\\s*}}`, "g"), val);
863
+ resolved = resolved.replace(new RegExp(`{{\\s*inputs\\.${key}\\s*}}`, "g"), val);
864
+ }
865
+ if (inputs.task_prompt) {
866
+ resolved = resolved.replace(/"?{{\s*task_prompt\s*}}"?/g, inputs.task_prompt);
867
+ }
868
+ return resolved;
869
+ }
870
+ resolveTargetStepIndex(onFailureVal, steps) {
871
+ if (typeof onFailureVal === "number") {
872
+ const idx = onFailureVal - 1;
873
+ if (idx >= 0 && idx < steps.length) return idx;
874
+ return null;
875
+ }
876
+ if (typeof onFailureVal === "string") {
877
+ let target = onFailureVal.trim();
878
+ const prefixes = [/^\s*goto:\s*/i, /^\s*rollback_to:\s*/i, /^\s*rollback:\s*/i, /^\s*rollback_to_step:\s*/i];
879
+ for (const p of prefixes) {
880
+ if (p.test(target)) {
881
+ target = target.replace(p, "").trim();
882
+ break;
883
+ }
884
+ }
885
+ if (/^\d+$/.test(target)) {
886
+ const idx = parseInt(target, 10) - 1;
887
+ if (idx >= 0 && idx < steps.length) return idx;
888
+ return null;
889
+ }
890
+ const targetLower = target.toLowerCase();
891
+ let foundIdx = steps.findIndex((s) => s.name && s.name.trim() === target);
892
+ if (foundIdx !== -1) return foundIdx;
893
+ foundIdx = steps.findIndex((s) => s.name && s.name.trim().toLowerCase() === targetLower);
894
+ if (foundIdx !== -1) return foundIdx;
895
+ foundIdx = steps.findIndex((s) => s.name && s.name.trim().toLowerCase().includes(targetLower));
896
+ if (foundIdx !== -1) return foundIdx;
897
+ }
898
+ return null;
899
+ }
900
+ parseCommand(cmdStr) {
901
+ const parts = [];
902
+ const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g;
903
+ let match;
904
+ while ((match = regex.exec(cmdStr)) !== null) {
905
+ parts.push(match[1] !== void 0 ? match[1] : match[2] !== void 0 ? match[2] : match[0]);
906
+ }
907
+ return {
908
+ cmd: parts[0] || "",
909
+ args: parts.slice(1)
910
+ };
911
+ }
912
+ async runStepAction(step, attempt, workingDir) {
913
+ try {
914
+ if (step.use && step.use.startsWith("agent/")) {
915
+ const agentName = step.use.substring("agent/".length);
916
+ const resolvedInstruction = this.resolveVariables(step.instruction);
917
+ console.log(picocolors3.blue(` \u{1F916} [Agent: ${agentName}] Instruction: "${resolvedInstruction}"`));
918
+ const stepHistory = this.state.errorHistory[this.state.currentStepIndex] || [];
919
+ let finalInstruction = resolvedInstruction;
920
+ if (stepHistory.length > 0) {
921
+ const lastError = stepHistory[stepHistory.length - 1];
922
+ console.log(picocolors3.gray(` Feeding validation error back to agent to self-heal...`));
923
+ finalInstruction = `${resolvedInstruction}
924
+
925
+ ATTENTION: The previous validation failed with the following error:
926
+ ${lastError}
927
+
928
+ Please correct the implementation to fix this error.`;
929
+ }
930
+ const outputDir = path5.join(process.cwd(), ".aiframe", "output", this.state.execId);
931
+ const responseText = await executeAgent(agentName, finalInstruction, 0, outputDir, workingDir);
932
+ console.log(picocolors3.gray(` [Agent Response] ${responseText.trim().split("\n")[0]}...`));
933
+ } else if (step.use && step.use.startsWith("tool/")) {
934
+ const toolName = step.use.substring("tool/".length);
935
+ console.log(picocolors3.blue(` \u{1F6E0}\uFE0F [Tool: ${toolName}] Executing...`));
936
+ const args = await mapArguments(toolName, null, step.params || {});
937
+ await executeCommand(toolName, args, { cwd: workingDir || process.cwd() });
938
+ } else if (step.command) {
939
+ const resolvedCmd = this.resolveVariables(step.command);
940
+ console.log(picocolors3.blue(` \u{1F4BB} [Command] Executing: "${resolvedCmd}"`));
941
+ const parsed = this.parseCommand(resolvedCmd);
942
+ await executeCommand(parsed.cmd, parsed.args, { cwd: workingDir || process.cwd() });
943
+ }
944
+ } catch (error) {
945
+ console.error(picocolors3.red(` \u274C Error running step action: ${error}`));
946
+ throw error;
947
+ }
948
+ }
949
+ };
950
+
951
+ // src/commands/run.ts
952
+ import picocolors4 from "picocolors";
953
+ async function runCommand(workflowPath, options) {
954
+ if (!workflowPath) {
955
+ console.error(picocolors4.red("\u274C Error: Please specify the workflow file path."));
956
+ process.exit(1);
957
+ }
958
+ try {
959
+ const engine = new WorkflowEngine(workflowPath);
960
+ await engine.load(!!options.resume);
961
+ const success = await engine.run();
962
+ if (!success) {
963
+ process.exit(1);
964
+ }
965
+ } catch (error) {
966
+ console.error(picocolors4.red("\u274C Error running workflow:"), error.message || error);
967
+ process.exit(1);
968
+ }
969
+ }
970
+
971
+ // src/core/server.ts
972
+ import Fastify from "fastify";
973
+ import fastifyStatic from "@fastify/static";
974
+ import path6 from "path";
975
+ import fs6 from "fs-extra";
976
+ import yaml5 from "js-yaml";
977
+ import { fileURLToPath } from "url";
978
+ var __filename = fileURLToPath(import.meta.url);
979
+ var __dirname = path6.dirname(__filename);
980
+ async function startServer(port = 3e3) {
981
+ const fastify = Fastify({ logger: false });
982
+ const cwd = process.cwd();
983
+ let publicPath = path6.join(__dirname, "public");
984
+ if (!fs6.existsSync(publicPath)) {
985
+ publicPath = path6.join(__dirname, "..", "public");
986
+ }
987
+ await fs6.ensureDir(publicPath);
988
+ fastify.register(fastifyStatic, {
989
+ root: publicPath,
990
+ prefix: "/"
991
+ });
992
+ fastify.get("/api/workflows", async () => {
993
+ const wfDir = path6.join(cwd, ".aiframe", "workflows");
994
+ if (!fs6.existsSync(wfDir)) return [];
995
+ const files = await fs6.readdir(wfDir);
996
+ const workflows = [];
997
+ for (const f of files) {
998
+ if (f.endsWith(".yaml") || f.endsWith(".yml")) {
999
+ const content = await fs6.readFile(path6.join(wfDir, f), "utf8");
1000
+ try {
1001
+ const parsed = yaml5.load(content);
1002
+ workflows.push({ filename: f, data: parsed });
1003
+ } catch (e) {
1004
+ workflows.push({ filename: f, error: "Parse Error" });
1005
+ }
1006
+ }
1007
+ }
1008
+ return workflows;
1009
+ });
1010
+ fastify.post("/api/workflows", async (request, reply) => {
1011
+ const { filename, workflow } = request.body;
1012
+ if (!filename || !workflow) {
1013
+ reply.status(400).send({ error: "Missing filename or workflow data" });
1014
+ return;
1015
+ }
1016
+ const wfDir = path6.join(cwd, ".aiframe", "workflows");
1017
+ await fs6.ensureDir(wfDir);
1018
+ const yamlContent = yaml5.dump(workflow);
1019
+ await fs6.writeFile(path6.join(wfDir, filename), yamlContent, "utf8");
1020
+ return { success: true };
1021
+ });
1022
+ fastify.get("/api/workflows/:filename/status", async (request, reply) => {
1023
+ const { filename } = request.params;
1024
+ const workflowFileName = path6.basename(filename, path6.extname(filename));
1025
+ const checkpointPath = path6.join(cwd, ".aiframe", "state", `${workflowFileName}-checkpoint.json`);
1026
+ if (fs6.existsSync(checkpointPath)) {
1027
+ try {
1028
+ const stateContent = await fs6.readFile(checkpointPath, "utf8");
1029
+ return JSON.parse(stateContent);
1030
+ } catch (e) {
1031
+ return { status: "idle", currentStepIndex: -1, stepStatuses: {} };
1032
+ }
1033
+ }
1034
+ return { status: "idle", currentStepIndex: -1, stepStatuses: {} };
1035
+ });
1036
+ fastify.get("/api/skills", async () => {
1037
+ const builtIn = [
1038
+ { name: "read_file", type: "tool", use: "tool/read_file", description: "\u0110\u1ECDc n\u1ED9i dung t\u1EC7p tin trong workspace" },
1039
+ { name: "write_file", type: "tool", use: "tool/write_file", description: "Ghi/\u0111\xE8 n\u1ED9i dung t\u1EC7p tin trong workspace" },
1040
+ { name: "semgrep", type: "tool", use: "tool/semgrep", description: "Qu\xE9t b\u1EA3o m\u1EADt m\xE3 ngu\u1ED3n b\u1EB1ng Semgrep" },
1041
+ { name: "sonar_scanner", type: "tool", use: "tool/sonar_scanner", description: "Qu\xE9t ch\u1EA5t l\u01B0\u1EE3ng m\xE3 ngu\u1ED3n b\u1EB1ng SonarQube" },
1042
+ { name: "run_tests", type: "command", use: "npm test", description: "Ch\u1EA1y b\u1ED9 ki\u1EC3m th\u1EED t\u1EF1 \u0111\u1ED9ng c\u1EE7a d\u1EF1 \xE1n" },
1043
+ { name: "build_project", type: "command", use: "npm run build", description: "Bi\xEAn d\u1ECBch/\u0111\xF3ng g\xF3i m\xE3 ngu\u1ED3n d\u1EF1 \xE1n" }
1044
+ ];
1045
+ const skillsDir = path6.join(cwd, ".aiframe", "skills");
1046
+ if (!fs6.existsSync(skillsDir)) return builtIn;
1047
+ try {
1048
+ const files = await fs6.readdir(skillsDir);
1049
+ const customSkills = [];
1050
+ for (const f of files) {
1051
+ if (f.endsWith(".yaml") || f.endsWith(".yml")) {
1052
+ const content = await fs6.readFile(path6.join(skillsDir, f), "utf8");
1053
+ try {
1054
+ const parsed = yaml5.load(content);
1055
+ if (parsed) {
1056
+ customSkills.push({
1057
+ filename: f,
1058
+ name: parsed.name || f.replace(".yaml", ""),
1059
+ type: parsed.type || "command",
1060
+ use: parsed.use || parsed.command || "",
1061
+ description: parsed.description || ""
1062
+ });
1063
+ }
1064
+ } catch (e) {
1065
+ }
1066
+ }
1067
+ }
1068
+ return [...builtIn, ...customSkills];
1069
+ } catch (e) {
1070
+ return builtIn;
1071
+ }
1072
+ });
1073
+ fastify.post("/api/skills", async (request, reply) => {
1074
+ const { filename, skill } = request.body;
1075
+ if (!filename || !skill) {
1076
+ reply.status(400).send({ error: "Missing filename or skill data" });
1077
+ return;
1078
+ }
1079
+ const skillsDir = path6.join(cwd, ".aiframe", "skills");
1080
+ await fs6.ensureDir(skillsDir);
1081
+ await fs6.writeFile(path6.join(skillsDir, filename), yaml5.dump(skill), "utf8");
1082
+ return { success: true };
1083
+ });
1084
+ fastify.delete("/api/skills/:filename", async (request, reply) => {
1085
+ const { filename } = request.params;
1086
+ const skillPath = path6.join(cwd, ".aiframe", "skills", filename);
1087
+ if (fs6.existsSync(skillPath)) {
1088
+ await fs6.remove(skillPath);
1089
+ }
1090
+ return { success: true };
1091
+ });
1092
+ fastify.get("/api/agents", async () => {
1093
+ const agentsDir = path6.join(cwd, ".aiframe", "agents");
1094
+ if (!fs6.existsSync(agentsDir)) return [];
1095
+ const files = await fs6.readdir(agentsDir);
1096
+ const agents = [];
1097
+ for (const f of files) {
1098
+ if (f.endsWith(".yaml") || f.endsWith(".yml")) {
1099
+ const content = await fs6.readFile(path6.join(agentsDir, f), "utf8");
1100
+ try {
1101
+ const parsed = yaml5.load(content);
1102
+ agents.push({ filename: f, data: parsed });
1103
+ } catch (e) {
1104
+ }
1105
+ }
1106
+ }
1107
+ return agents;
1108
+ });
1109
+ fastify.post("/api/agents", async (request, reply) => {
1110
+ const { filename, agent } = request.body;
1111
+ if (!filename || !agent) {
1112
+ reply.status(400).send({ error: "Missing filename or agent data" });
1113
+ return;
1114
+ }
1115
+ const agentsDir = path6.join(cwd, ".aiframe", "agents");
1116
+ await fs6.ensureDir(agentsDir);
1117
+ await fs6.writeFile(path6.join(agentsDir, filename), yaml5.dump(agent), "utf8");
1118
+ return { success: true };
1119
+ });
1120
+ fastify.delete("/api/agents/:filename", async (request, reply) => {
1121
+ const { filename } = request.params;
1122
+ const agentPath = path6.join(cwd, ".aiframe", "agents", filename);
1123
+ if (fs6.existsSync(agentPath)) {
1124
+ await fs6.remove(agentPath);
1125
+ }
1126
+ return { success: true };
1127
+ });
1128
+ fastify.delete("/api/workflows/:filename", async (request, reply) => {
1129
+ const { filename } = request.params;
1130
+ const wfPath = path6.join(cwd, ".aiframe", "workflows", filename);
1131
+ if (fs6.existsSync(wfPath)) {
1132
+ await fs6.remove(wfPath);
1133
+ }
1134
+ return { success: true };
1135
+ });
1136
+ fastify.get("/api/history", async () => {
1137
+ const historyPath = path6.join(cwd, ".aiframe", "state", "history.json");
1138
+ if (!fs6.existsSync(historyPath)) return [];
1139
+ try {
1140
+ const content = await fs6.readFile(historyPath, "utf8");
1141
+ return JSON.parse(content);
1142
+ } catch (e) {
1143
+ return [];
1144
+ }
1145
+ });
1146
+ fastify.post("/api/run", async (request, reply) => {
1147
+ const { filename, inputs } = request.body;
1148
+ if (!filename) {
1149
+ reply.status(400).send({ error: "Missing filename" });
1150
+ return;
1151
+ }
1152
+ const workflowPath = path6.join(cwd, ".aiframe", "workflows", filename);
1153
+ if (!fs6.existsSync(workflowPath)) {
1154
+ reply.status(404).send({ error: "Workflow file not found" });
1155
+ return;
1156
+ }
1157
+ console.log(`
1158
+ \u{1F310} [Web UI] Triggered run for: ${filename} with inputs:`, inputs);
1159
+ const historyPath = path6.join(cwd, ".aiframe", "state", "history.json");
1160
+ await fs6.ensureDir(path6.dirname(historyPath));
1161
+ let history = [];
1162
+ if (fs6.existsSync(historyPath)) {
1163
+ try {
1164
+ history = JSON.parse(await fs6.readFile(historyPath, "utf8"));
1165
+ } catch (e) {
1166
+ }
1167
+ }
1168
+ const newRun = {
1169
+ id: `run-${Date.now()}`,
1170
+ filename,
1171
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
1172
+ status: "in_progress",
1173
+ stepStatuses: {},
1174
+ inputs: inputs || {}
1175
+ };
1176
+ history.unshift(newRun);
1177
+ await fs6.writeFile(historyPath, JSON.stringify(history, null, 2), "utf8");
1178
+ const engine = new WorkflowEngine(workflowPath);
1179
+ await engine.load(false, inputs);
1180
+ engine.run().then(async (success) => {
1181
+ if (fs6.existsSync(historyPath)) {
1182
+ try {
1183
+ const currentHistory = JSON.parse(await fs6.readFile(historyPath, "utf8"));
1184
+ const record = currentHistory.find((h) => h.id === newRun.id);
1185
+ if (record) {
1186
+ record.status = success ? "completed" : "failed";
1187
+ const checkpointFileName = path6.basename(filename, path6.extname(filename));
1188
+ const checkpointPath = path6.join(cwd, ".aiframe", "state", `${checkpointFileName}-checkpoint.json`);
1189
+ if (fs6.existsSync(checkpointPath)) {
1190
+ const cpData = JSON.parse(await fs6.readFile(checkpointPath, "utf8"));
1191
+ record.stepStatuses = cpData.stepStatuses || {};
1192
+ }
1193
+ await fs6.writeFile(historyPath, JSON.stringify(currentHistory, null, 2), "utf8");
1194
+ }
1195
+ } catch (e) {
1196
+ }
1197
+ }
1198
+ }).catch(async (err) => {
1199
+ console.error("Workflow background run failed:", err);
1200
+ if (fs6.existsSync(historyPath)) {
1201
+ try {
1202
+ const currentHistory = JSON.parse(await fs6.readFile(historyPath, "utf8"));
1203
+ const record = currentHistory.find((h) => h.id === newRun.id);
1204
+ if (record) {
1205
+ record.status = "failed";
1206
+ await fs6.writeFile(historyPath, JSON.stringify(currentHistory, null, 2), "utf8");
1207
+ }
1208
+ } catch (e) {
1209
+ }
1210
+ }
1211
+ });
1212
+ return { success: true, status: "triggered", runId: newRun.id };
1213
+ });
1214
+ try {
1215
+ const address = await fastify.listen({ port, host: "0.0.0.0" });
1216
+ console.log(`
1217
+ \u{1F680} Web UI Server listening on: ${address}`);
1218
+ } catch (err) {
1219
+ console.error("Error starting Fastify server:", err);
1220
+ process.exit(1);
1221
+ }
1222
+ }
1223
+
1224
+ // src/commands/ui.ts
1225
+ async function uiCommand(options) {
1226
+ const port = options.port ?? 3e3;
1227
+ await startServer(port);
1228
+ }
1229
+
1230
+ export {
1231
+ initCommand,
1232
+ executeCommand,
1233
+ getGoogleModel,
1234
+ mapArguments,
1235
+ executeAgent,
1236
+ WorkflowEngine,
1237
+ runCommand,
1238
+ startServer,
1239
+ uiCommand
1240
+ };