feat: Add phase-by-phase prompt generation for complex projects

- Generate prompts/phase{1-4}-prompt.txt files with phase-specific tasks
- Update GUIDE.md with phase-by-phase execution instructions
- Include both /ralph-wiggum and bash loop alternatives for each phase
- Each phase prompt references PROMPT.md and focuses on one phase only
- Add tech-stack-specific build/test/lint commands per phase

Also includes fix for JSON parsing with markdown code fences.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-01-11 04:40:05 +00:00
parent da86128d96
commit 75294a2daf
8 changed files with 440 additions and 115 deletions

77
package-lock.json generated
View File

@@ -8,21 +8,23 @@
"name": "ralph-vibe",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"chalk": "^5.6.2",
"commander": "^14.0.2",
"inquirer": "^13.1.0",
"ora": "^9.0.0"
},
"bin": {
"ralph-vibe": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"@eslint/js": "^9.39.2",
"@types/inquirer": "^9.0.9",
"@types/node": "^25.0.5",
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"chalk": "^5.6.2",
"commander": "^14.0.2",
"eslint": "^9.39.2",
"inquirer": "^13.1.0",
"ora": "^9.0.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.52.0",
@@ -36,7 +38,6 @@
"version": "0.71.2",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz",
"integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==",
"dev": true,
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
@@ -56,7 +57,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -704,7 +704,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.2.tgz",
"integrity": "sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww==",
"dev": true,
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
}
@@ -713,7 +712,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.3.tgz",
"integrity": "sha512-xtQP2eXMFlOcAhZ4ReKP2KZvDIBb1AnCfZ81wWXG3DXLVH0f0g4obE0XDPH+ukAEMRcZT0kdX2AS1jrWGXbpxw==",
"dev": true,
"dependencies": {
"@inquirer/ansi": "^2.0.2",
"@inquirer/core": "^11.1.0",
@@ -736,7 +734,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.3.tgz",
"integrity": "sha512-lyEvibDFL+NA5R4xl8FUmNhmu81B+LDL9L/MpKkZlQDJZXzG8InxiqYxiAlQYa9cqLLhYqKLQwZqXmSTqCLjyw==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/type": "^4.0.2"
@@ -757,7 +754,6 @@
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.0.tgz",
"integrity": "sha512-+jD/34T1pK8M5QmZD/ENhOfXdl9Zr+BrQAUc5h2anWgi7gggRq15ZbiBeLoObj0TLbdgW7TAIQRU2boMc9uOKQ==",
"dev": true,
"dependencies": {
"@inquirer/ansi": "^2.0.2",
"@inquirer/figures": "^2.0.2",
@@ -783,7 +779,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.3.tgz",
"integrity": "sha512-wYyQo96TsAqIciP/r5D3cFeV8h4WqKQ/YOvTg5yOfP2sqEbVVpbxPpfV3LM5D0EP4zUI3EZVHyIUIllnoIa8OQ==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/external-editor": "^2.0.2",
@@ -805,7 +800,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.3.tgz",
"integrity": "sha512-2oINvuL27ujjxd95f6K2K909uZOU2x1WiAl7Wb1X/xOtL8CgQ1kSxzykIr7u4xTkXkXOAkCuF45T588/YKee7w==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/type": "^4.0.2"
@@ -826,7 +820,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.2.tgz",
"integrity": "sha512-X/fMXK7vXomRWEex1j8mnj7s1mpnTeP4CO/h2gysJhHLT2WjBnLv4ZQEGpm/kcYI8QfLZ2fgW+9kTKD+jeopLg==",
"dev": true,
"dependencies": {
"chardet": "^2.1.1",
"iconv-lite": "^0.7.0"
@@ -847,7 +840,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.2.tgz",
"integrity": "sha512-qXm6EVvQx/FmnSrCWCIGtMHwqeLgxABP8XgcaAoywsL0NFga9gD5kfG0gXiv80GjK9Hsoz4pgGwF/+CjygyV9A==",
"dev": true,
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
}
@@ -856,7 +848,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.3.tgz",
"integrity": "sha512-4R0TdWl53dtp79Vs6Df2OHAtA2FVNqya1hND1f5wjHWxZJxwDMSNB1X5ADZJSsQKYAJ5JHCTO+GpJZ42mK0Otw==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/type": "^4.0.2"
@@ -877,7 +868,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.3.tgz",
"integrity": "sha512-TjQLe93GGo5snRlu83JxE38ZPqj5ZVggL+QqqAF2oBA5JOJoxx25GG3EGH/XN/Os5WOmKfO8iLVdCXQxXRZIMQ==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/type": "^4.0.2"
@@ -898,7 +888,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.3.tgz",
"integrity": "sha512-rCozGbUMAHedTeYWEN8sgZH4lRCdgG/WinFkit6ZPsp8JaNg2T0g3QslPBS5XbpORyKP/I+xyBO81kFEvhBmjA==",
"dev": true,
"dependencies": {
"@inquirer/ansi": "^2.0.2",
"@inquirer/core": "^11.1.0",
@@ -920,7 +909,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.1.0.tgz",
"integrity": "sha512-LsZMdKcmRNF5LyTRuZE5nWeOjganzmN3zwbtNfcs6GPh3I2TsTtF1UYZlbxVfhxd+EuUqLGs/Lm3Xt4v6Az1wA==",
"dev": true,
"dependencies": {
"@inquirer/checkbox": "^5.0.3",
"@inquirer/confirm": "^6.0.3",
@@ -949,7 +937,6 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.1.0.tgz",
"integrity": "sha512-yUCuVh0jW026Gr2tZlG3kHignxcrLKDR3KBp+eUgNz+BAdSeZk0e18yt2gyBr+giYhj/WSIHCmPDOgp1mT2niQ==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/type": "^4.0.2"
@@ -970,7 +957,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.0.3.tgz",
"integrity": "sha512-lzqVw0YwuKYetk5VwJ81Ba+dyVlhseHPx9YnRKQgwXdFS0kEavCz2gngnNhnMIxg8+j1N/rUl1t5s1npwa7bqg==",
"dev": true,
"dependencies": {
"@inquirer/core": "^11.1.0",
"@inquirer/figures": "^2.0.2",
@@ -992,7 +978,6 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.3.tgz",
"integrity": "sha512-M+ynbwS0ecQFDYMFrQrybA0qL8DV0snpc4kKevCCNaTpfghsRowRY7SlQBeIYNzHqXtiiz4RG9vTOeb/udew7w==",
"dev": true,
"dependencies": {
"@inquirer/ansi": "^2.0.2",
"@inquirer/core": "^11.1.0",
@@ -1015,7 +1000,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.2.tgz",
"integrity": "sha512-cae7mzluplsjSdgFA6ACLygb5jC8alO0UUnFPyu0E7tNRPrL+q/f8VcSXp+cjZQ7l5CMpDpi2G1+IQvkOiL1Lw==",
"dev": true,
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
},
@@ -1436,7 +1420,7 @@
"version": "25.0.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.5.tgz",
"integrity": "sha512-FuLxeLuSVOqHPxSN1fkcD8DLU21gAP7nCKqGRJ/FglbCUBs0NYN6TpHcdmyLeh8C0KwGIaZQJSv+OYG+KZz+Gw==",
"dev": true,
"devOptional": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1817,7 +1801,6 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"engines": {
"node": ">=12"
},
@@ -1829,7 +1812,6 @@
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"engines": {
"node": ">=12"
},
@@ -1919,7 +1901,6 @@
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
@@ -1930,8 +1911,7 @@
"node_modules/chardet": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
"dev": true
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="
},
"node_modules/chokidar": {
"version": "4.0.3",
@@ -1952,7 +1932,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"dependencies": {
"restore-cursor": "^5.0.0"
},
@@ -1967,7 +1946,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz",
"integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==",
"dev": true,
"engines": {
"node": ">=18.20"
},
@@ -1979,7 +1957,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true,
"engines": {
"node": ">= 12"
}
@@ -2006,7 +1983,6 @@
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true,
"engines": {
"node": ">=20"
}
@@ -2072,8 +2048,7 @@
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
@@ -2495,7 +2470,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"dev": true,
"engines": {
"node": ">=18"
},
@@ -2540,7 +2514,6 @@
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -2590,7 +2563,6 @@
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.1.0.tgz",
"integrity": "sha512-4vv4GS/9HLnn0radvmHlXUXiNkd2gYCBQ4U1rxZWBJDisu2Z06bzUM9CFU8pcu1vwuAQjo6O+CFiqCYNsEi6qQ==",
"dev": true,
"dependencies": {
"@inquirer/ansi": "^2.0.2",
"@inquirer/core": "^11.1.0",
@@ -2637,7 +2609,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"dev": true,
"engines": {
"node": ">=12"
},
@@ -2649,7 +2620,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"dev": true,
"engines": {
"node": ">=18"
},
@@ -2694,7 +2664,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
@@ -2789,7 +2758,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
"dev": true,
"dependencies": {
"is-unicode-supported": "^2.0.0",
"yoctocolors": "^2.1.1"
@@ -2814,7 +2782,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"engines": {
"node": ">=18"
},
@@ -2859,7 +2826,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
"integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
"dev": true,
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
@@ -2922,7 +2888,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"dependencies": {
"mimic-function": "^5.0.0"
},
@@ -2954,7 +2919,6 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz",
"integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==",
"dev": true,
"dependencies": {
"chalk": "^5.6.2",
"cli-cursor": "^5.0.0",
@@ -3191,7 +3155,6 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
@@ -3251,7 +3214,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz",
"integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
@@ -3260,7 +3222,6 @@
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -3268,8 +3229,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.7.3",
@@ -3314,7 +3274,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
@@ -3356,7 +3315,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
"dev": true,
"engines": {
"node": ">=18"
},
@@ -3368,7 +3326,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
"dev": true,
"dependencies": {
"get-east-asian-width": "^1.3.0",
"strip-ansi": "^7.1.0"
@@ -3384,7 +3341,6 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -3520,8 +3476,7 @@
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"dev": true
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="
},
"node_modules/ts-api-utils": {
"version": "2.4.0",
@@ -3544,8 +3499,7 @@
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tsup": {
"version": "8.5.1",
@@ -3666,7 +3620,7 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true
"devOptional": true
},
"node_modules/uri-js": {
"version": "4.4.1",
@@ -3881,7 +3835,6 @@
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
@@ -3898,7 +3851,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
@@ -3927,7 +3879,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"dev": true,
"engines": {
"node": ">=18"
},

View File

@@ -125,3 +125,41 @@
9. README.md documents all commands and options ✓
10. npm publish --dry-run succeeds ✓
[2026-01-11T03:21:00Z] [5] [FIX] - Fixed JSON parsing for Claude responses wrapped in markdown code fences
- Added stripCodeFences() utility function to src/utils/files.ts
- Updated src/generators/specification.ts to use stripCodeFences
- Updated src/clients/claude.ts in 3 places:
* parseArchitectureResponse()
* parseQueriesResponse()
* generateSpec()
- The new approach handles edge cases better than the regex match approach
- Build and all 110 tests pass
[2026-01-11T04:37:00Z] [6] [FEATURE] - Phase-by-phase prompt generation
- Modified src/generators/scaffold.ts:
* Added prompts/ directory creation
* Added writePhasePrompts() method to generate 4 phase-specific prompt files
* Added generatePhasePrompt() to create individual phase prompts
* Added helper methods for build/test/lint commands per language
* Updated dry-run structure logging to include prompts/
- Modified src/generators/prd.ts:
* Rewrote generateGuideMd() to generate deterministic phase-by-phase guide
* GUIDE.md now includes 4 phases with:
- /ralph-wiggum:ralph-loop command for each phase
- Verification command after each phase
- Bash loop alternative for each phase
* Still includes full project execution as alternative
- Updated test in src/__tests__/prd.test.ts to match new GUIDE.md format
- Generated files:
* prompts/phase1-prompt.txt - Foundation phase
* prompts/phase2-prompt.txt - Core phase
* prompts/phase3-prompt.txt - Integration phase
* prompts/phase4-prompt.txt - Polish phase
- Each phase prompt includes:
* Reference to PROMPT.md for context
* Phase-specific tasks from prd.json
* Working instructions with tech-stack-specific commands
* Phase-specific completion promise (PHASE_X_COMPLETE)
- Build succeeds, all 110 tests pass, lint clean
- Tested with ralph-vibe new word-counter - all phase files generated correctly

View File

@@ -79,12 +79,9 @@ describe('PRDGenerator', () => {
describe('generate', () => {
it('should generate all PRD files', async () => {
// Mock PROMPT.md response
// Mock PROMPT.md response (GUIDE.md is now generated deterministically, not via Claude)
mockAnalyze.mockResolvedValueOnce('# Project: test-project\n\n## Objective\n\nTest project');
// Mock GUIDE.md response
mockAnalyze.mockResolvedValueOnce('# Step-by-Step Guide\n\n## Getting Started');
const generator = new PRDGenerator({ claudeApiKey: mockApiKey });
const result = await generator.generate(
'test-project',
@@ -95,7 +92,12 @@ describe('PRDGenerator', () => {
);
expect(result.promptMd).toContain('# Project: test-project');
expect(result.guideMd).toContain('# Step-by-Step Guide');
expect(result.guideMd).toContain('# test-project - Implementation Guide');
expect(result.guideMd).toContain('Phase 1: Foundation');
expect(result.guideMd).toContain('Phase 2: Core');
expect(result.guideMd).toContain('Phase 3: Integration');
expect(result.guideMd).toContain('Phase 4: Polish');
expect(result.guideMd).toContain('phase1-prompt.txt');
expect(result.prdJson).toBeDefined();
expect(result.progressTxt).toContain('# Progress Log');
expect(result.claudeMd).toContain('# Claude Code Configuration');

View File

@@ -1,5 +1,6 @@
import Anthropic from '@anthropic-ai/sdk';
import { logger } from '../utils/logger.js';
import { stripCodeFences } from '../utils/files.js';
import type { Architecture, Specification, PRDOutput, ValidationResult, Research } from '../types/index.js';
const MAX_RETRIES = 3;
@@ -71,7 +72,7 @@ export class ClaudeClient {
return this.retryWithBackoff(async () => {
const response = await this.client.messages.create({
model: this.model,
max_tokens: 4096,
max_tokens: 8192,
messages: [{ role: 'user', content: prompt }],
});
@@ -149,14 +150,7 @@ Output ONLY valid JSON (no markdown, no explanation):
}
private parseArchitectureResponse(response: string): Architecture {
// Extract JSON from response (handle markdown code blocks)
let jsonStr = response.trim();
// Remove markdown code blocks if present
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
}
const jsonStr = stripCodeFences(response);
try {
const parsed = JSON.parse(jsonStr);
@@ -198,13 +192,7 @@ Output ONLY a valid JSON array of query strings (no markdown, no explanation):
}
private parseQueriesResponse(response: string): string[] {
let jsonStr = response.trim();
// Remove markdown code blocks if present
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
}
const jsonStr = stripCodeFences(response);
try {
const parsed = JSON.parse(jsonStr);
@@ -251,11 +239,7 @@ Output a specification with features, data models, interfaces, and tech stack as
// Basic parsing - full implementation in Phase 3
try {
let jsonStr = response.trim();
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
}
const jsonStr = stripCodeFences(response);
return JSON.parse(jsonStr) as Specification;
} catch {
throw new Error('Failed to parse specification response');

View File

@@ -132,33 +132,201 @@ Output the complete PROMPT.md content as raw markdown (no code fences around the
): Promise<string> {
logger.debug('Generating GUIDE.md...');
const prompt = `Generate a personalized step-by-step GUIDE.md for this project:
const { techStack } = specification;
const buildCmd = this.getBuildCommand(techStack.language);
const testCmd = this.getTestCommand(techStack.testingFramework);
const lintCmd = this.getLintCommand(techStack.language);
const featureCount = specification.features.length;
const estimatedCost = (featureCount * 0.5).toFixed(2);
<project_name>${projectName}</project_name>
return `# ${projectName} - Implementation Guide
<app_type>${architecture.appType}</app_type>
## Overview
<tech_stack>
${JSON.stringify(specification.techStack, null, 2)}
</tech_stack>
This guide walks you through implementing ${projectName} using the Ralph Method with phase-by-phase execution.
<features>
${specification.features.map((f, i) => `${i + 1}. ${f.name}: ${f.description}`).join('\n')}
</features>
**Tech Stack**: ${techStack.language}${techStack.framework ? ` / ${techStack.framework}` : ''}
**Features**: ${featureCount}
**Estimated Cost**: ~$${estimatedCost} (at ~$0.50/feature)
Generate a GUIDE.md that:
1. Is specific to this ${architecture.appType} project
2. Uses exact commands for ${specification.techStack.language} / ${specification.techStack.framework || 'the tech stack'}
3. References specific phases from PROMPT.md
4. Includes cost estimates based on ${specification.features.length} features
5. Guides through the Ralph Method workflow
## Prerequisites
Format as clear, actionable markdown with numbered steps.
1. Read \`PROMPT.md\` for full project requirements
2. Review \`prd.json\` for feature tracking
3. Ensure your environment is set up for ${techStack.language}
Output the complete GUIDE.md content as raw markdown:`;
## Execution
const response = await this.claudeClient.analyze(prompt);
return this.cleanMarkdownResponse(response);
Execute each phase in order. Do not proceed to the next phase until the current phase is complete.
### Phase 1: Foundation
\`\`\`bash
/ralph-wiggum:ralph-loop "$(cat prompts/phase1-prompt.txt)" --max-iterations 30 --completion-promise "PHASE_1_COMPLETE"
\`\`\`
**Verify**:
\`\`\`bash
${buildCmd} && ${testCmd} && ${lintCmd}
\`\`\`
**Bash loop alternative** (if plugin unavailable):
\`\`\`bash
PROMPT=$(cat prompts/phase1-prompt.txt)
MAX_ITERATIONS=30
COMPLETION_PROMISE="PHASE_1_COMPLETE"
for i in $(seq 1 $MAX_ITERATIONS); do
echo "=== Iteration $i of $MAX_ITERATIONS ==="
RESPONSE=$(claude -p "$PROMPT")
echo "$RESPONSE"
if echo "$RESPONSE" | grep -q "$COMPLETION_PROMISE"; then
echo "Phase 1 complete!"
break
fi
if echo "$RESPONSE" | grep -q "ABORT_BLOCKED"; then
echo "Blocked - manual intervention required"
break
fi
done
\`\`\`
---
### Phase 2: Core
\`\`\`bash
/ralph-wiggum:ralph-loop "$(cat prompts/phase2-prompt.txt)" --max-iterations 40 --completion-promise "PHASE_2_COMPLETE"
\`\`\`
**Verify**:
\`\`\`bash
${buildCmd} && ${testCmd} && ${lintCmd}
\`\`\`
**Bash loop alternative**:
\`\`\`bash
PROMPT=$(cat prompts/phase2-prompt.txt)
MAX_ITERATIONS=40
COMPLETION_PROMISE="PHASE_2_COMPLETE"
for i in $(seq 1 $MAX_ITERATIONS); do
echo "=== Iteration $i of $MAX_ITERATIONS ==="
RESPONSE=$(claude -p "$PROMPT")
echo "$RESPONSE"
if echo "$RESPONSE" | grep -q "$COMPLETION_PROMISE"; then
echo "Phase 2 complete!"
break
fi
if echo "$RESPONSE" | grep -q "ABORT_BLOCKED"; then
echo "Blocked - manual intervention required"
break
fi
done
\`\`\`
---
### Phase 3: Integration
\`\`\`bash
/ralph-wiggum:ralph-loop "$(cat prompts/phase3-prompt.txt)" --max-iterations 40 --completion-promise "PHASE_3_COMPLETE"
\`\`\`
**Verify**:
\`\`\`bash
${buildCmd} && ${testCmd} && ${lintCmd}
\`\`\`
**Bash loop alternative**:
\`\`\`bash
PROMPT=$(cat prompts/phase3-prompt.txt)
MAX_ITERATIONS=40
COMPLETION_PROMISE="PHASE_3_COMPLETE"
for i in $(seq 1 $MAX_ITERATIONS); do
echo "=== Iteration $i of $MAX_ITERATIONS ==="
RESPONSE=$(claude -p "$PROMPT")
echo "$RESPONSE"
if echo "$RESPONSE" | grep -q "$COMPLETION_PROMISE"; then
echo "Phase 3 complete!"
break
fi
if echo "$RESPONSE" | grep -q "ABORT_BLOCKED"; then
echo "Blocked - manual intervention required"
break
fi
done
\`\`\`
---
### Phase 4: Polish
\`\`\`bash
/ralph-wiggum:ralph-loop "$(cat prompts/phase4-prompt.txt)" --max-iterations 30 --completion-promise "PHASE_4_COMPLETE"
\`\`\`
**Verify**:
\`\`\`bash
${buildCmd} && ${testCmd} && ${lintCmd}
\`\`\`
**Bash loop alternative**:
\`\`\`bash
PROMPT=$(cat prompts/phase4-prompt.txt)
MAX_ITERATIONS=30
COMPLETION_PROMISE="PHASE_4_COMPLETE"
for i in $(seq 1 $MAX_ITERATIONS); do
echo "=== Iteration $i of $MAX_ITERATIONS ==="
RESPONSE=$(claude -p "$PROMPT")
echo "$RESPONSE"
if echo "$RESPONSE" | grep -q "$COMPLETION_PROMISE"; then
echo "Phase 4 complete!"
break
fi
if echo "$RESPONSE" | grep -q "ABORT_BLOCKED"; then
echo "Blocked - manual intervention required"
break
fi
done
\`\`\`
---
## Full Project Execution (Alternative)
If you prefer to run the entire project in one loop:
\`\`\`bash
/ralph-wiggum:ralph-loop "$(cat PROMPT.md)" --max-iterations 100 --completion-promise "PROJECT_COMPLETE"
\`\`\`
**Note**: Phase-by-phase execution is recommended for complex projects as it provides better control and verification checkpoints.
## Tracking Progress
- Check \`prd.json\` to see which features have \`passes: true\`
- Review \`progress.txt\` for the implementation log
- All phases complete when all features pass
## Troubleshooting
If the agent outputs \`ABORT_BLOCKED\`:
1. Review the error message
2. Fix the blocking issue manually
3. Re-run the current phase
If iterations exhaust without completion:
1. Check \`progress.txt\` for what was accomplished
2. Review \`prd.json\` for remaining features
3. Increase \`--max-iterations\` and re-run
---
*Generated with [Ralph PRD Generator](https://github.com/your-username/ralph-vibe)*
`;
}
private async generateClaudeMd(

View File

@@ -57,6 +57,10 @@ export class ScaffoldGenerator {
const configFiles = await this.writeConfigFiles(projectPath, input);
files.push(...configFiles);
// Write phase prompt files
const promptFiles = await this.writePhasePrompts(projectPath, input);
files.push(...promptFiles);
// Initialize git
await this.initGit(projectPath);
@@ -70,6 +74,7 @@ export class ScaffoldGenerator {
projectPath,
join(projectPath, 'docs'),
join(projectPath, 'agent_docs'),
join(projectPath, 'prompts'),
join(projectPath, 'src'),
];
@@ -243,6 +248,149 @@ ${input.architecture.suggestedTechStack.reasoning}
return files;
}
private async writePhasePrompts(projectPath: string, input: ScaffoldInput): Promise<string[]> {
const files: string[] = [];
const promptsDir = join(projectPath, 'prompts');
const { prdOutput, specification } = input;
const phaseDefinitions = [
{
number: 1,
name: 'Foundation',
description: 'Project setup, core infrastructure, and initial configuration',
promise: 'PHASE_1_COMPLETE',
},
{
number: 2,
name: 'Core',
description: 'Main functionality and core features implementation',
promise: 'PHASE_2_COMPLETE',
},
{
number: 3,
name: 'Integration',
description: 'External services, error handling, and system integration',
promise: 'PHASE_3_COMPLETE',
},
{
number: 4,
name: 'Polish',
description: 'Documentation, optimization, testing, and final packaging',
promise: 'PHASE_4_COMPLETE',
},
];
for (const phase of phaseDefinitions) {
const phaseFeatures = prdOutput.prdJson.features.filter(f => f.phase === phase.number);
const promptContent = this.generatePhasePrompt(phase, phaseFeatures, specification);
const promptPath = join(promptsDir, `phase${phase.number}-prompt.txt`);
await writeFileAtomic(promptPath, promptContent);
files.push(`prompts/phase${phase.number}-prompt.txt`);
logger.debug(`Created phase ${phase.number} prompt file`);
}
return files;
}
private generatePhasePrompt(
phase: { number: number; name: string; description: string; promise: string },
features: { id: string; name: string; description: string; acceptance: string }[],
specification: Specification
): string {
const { techStack } = specification;
const buildCmd = this.getBuildCommand(techStack.language);
const testCmd = this.getTestCommand(techStack.testingFramework);
const lintCmd = this.getLintCommand(techStack.language);
const featureList = features.length > 0
? features.map(f => `- [ ] ${f.name}: ${f.description}\n - Acceptance: ${f.acceptance}`).join('\n')
: '- [ ] Complete phase setup and infrastructure';
return `# Phase ${phase.number}: ${phase.name}
## Context
Read PROMPT.md for full project requirements and context.
This prompt focuses ONLY on Phase ${phase.number}: ${phase.name}.
## Phase Objective
${phase.description}
## Phase ${phase.number} Tasks
${featureList}
## Working Instructions
1. Read PROMPT.md to understand the full project context
2. Focus ONLY on the tasks listed above for this phase
3. For each task:
- Implement the feature
- Write tests
- Run: ${buildCmd} && ${testCmd} && ${lintCmd}
- Update prd.json to set passes: true for completed features
- Append progress to progress.txt
- Commit with conventional commit message
## Constraints
- Always run tests before committing
- Never commit failing code
- Do not implement features from other phases
- Make reasonable decisions - do not ask questions
- Update prd.json when features complete
## Verification
After completing all Phase ${phase.number} tasks:
\`\`\`bash
${buildCmd} && ${testCmd} && ${lintCmd}
\`\`\`
All commands must pass with zero errors.
## Completion
When ALL Phase ${phase.number} tasks are complete and verified:
- All features for this phase pass their acceptance criteria
- prd.json shows passes: true for all Phase ${phase.number} features
- Build, test, and lint all pass
Output: <promise>${phase.promise}</promise>
If blocked and cannot proceed:
Output: <promise>ABORT_BLOCKED</promise>
`;
}
private getBuildCommand(language: string): string {
const lang = language.toLowerCase();
if (lang.includes('typescript') || lang.includes('javascript')) return 'npm run build';
if (lang.includes('python')) return 'python -m build';
if (lang.includes('rust')) return 'cargo build';
if (lang.includes('go')) return 'go build ./...';
return 'npm run build';
}
private getTestCommand(testFramework: string): string {
const tf = testFramework.toLowerCase();
if (tf.includes('vitest') || tf.includes('jest')) return 'npm run test';
if (tf.includes('pytest')) return 'pytest';
if (tf.includes('cargo') || tf.includes('rust')) return 'cargo test';
if (tf.includes('go')) return 'go test ./...';
return 'npm run test';
}
private getLintCommand(language: string): string {
const lang = language.toLowerCase();
if (lang.includes('typescript') || lang.includes('javascript')) return 'npm run lint';
if (lang.includes('python')) return 'ruff check .';
if (lang.includes('rust')) return 'cargo clippy';
if (lang.includes('go')) return 'golangci-lint run';
return 'npm run lint';
}
private async initGit(projectPath: string): Promise<void> {
try {
execSync('git init', { cwd: projectPath, stdio: 'pipe' });
@@ -276,6 +424,11 @@ ${input.architecture.suggestedTechStack.reasoning}
' tech_stack.md',
' code_patterns.md',
' testing.md',
' prompts/',
' phase1-prompt.txt',
' phase2-prompt.txt',
' phase3-prompt.txt',
' phase4-prompt.txt',
' src/',
];

View File

@@ -1,5 +1,6 @@
import { ClaudeClient } from '../clients/claude.js';
import { logger } from '../utils/logger.js';
import { stripCodeFences } from '../utils/files.js';
import type { Architecture, Research, Specification, Feature, DataModel, InterfaceContract, TechStack } from '../types/index.js';
export interface SpecificationGeneratorOptions {
@@ -127,20 +128,20 @@ Output ONLY valid JSON with this structure (no markdown, no explanation):
}
private parseResponse(response: string): Specification {
let jsonStr = response.trim();
const jsonStr = stripCodeFences(response);
// Remove markdown code blocks if present
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonStr = jsonMatch[1].trim();
}
// Debug logging to diagnose truncation issues
logger.debug(`[DEBUG] Claude response length: ${response.length}`);
logger.debug(`[DEBUG] Claude response preview: ${response.substring(0, 500)}`);
logger.debug(`[DEBUG] Claude response end: ${response.substring(response.length - 200)}`);
try {
const parsed = JSON.parse(jsonStr);
return this.validateAndNormalize(parsed);
} catch (err) {
const error = err as Error;
logger.debug(`Failed to parse specification response: ${response.substring(0, 500)}`);
console.error('[ERROR] Failed to parse JSON. Response was:');
console.error(jsonStr);
throw new Error(`Failed to parse specification: ${error.message}`);
}
}

View File

@@ -2,6 +2,34 @@ import { readFile, writeFile, mkdir, stat, rename, unlink } from 'fs/promises';
import { dirname } from 'path';
import { randomUUID } from 'crypto';
/**
* Strip markdown code fences from text (e.g., ```json ... ```)
* Useful for parsing JSON responses from Claude that may be wrapped in code blocks
*/
export function stripCodeFences(text: string): string {
let result = text.trim();
// Remove opening fence (```json or ``` at the start)
if (result.startsWith('```')) {
// Find the end of the first line (the fence line)
const firstNewline = result.indexOf('\n');
if (firstNewline !== -1) {
result = result.slice(firstNewline + 1);
} else {
// No newline, try to remove just ```json or ```
result = result.replace(/^```(?:json)?\s*/i, '');
}
}
// Remove closing fence (``` at the end)
if (result.trimEnd().endsWith('```')) {
const lastFenceIdx = result.lastIndexOf('```');
result = result.slice(0, lastFenceIdx);
}
return result.trim();
}
export async function ensureDir(path: string): Promise<void> {
await mkdir(path, { recursive: true });
}