diff --git a/package-lock.json b/package-lock.json index b768b64..707dbe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/progress.txt b/progress.txt index 61eca7f..6cc81cf 100644 --- a/progress.txt +++ b/progress.txt @@ -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 + diff --git a/src/__tests__/prd.test.ts b/src/__tests__/prd.test.ts index 99f6b46..f80e441 100644 --- a/src/__tests__/prd.test.ts +++ b/src/__tests__/prd.test.ts @@ -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'); diff --git a/src/clients/claude.ts b/src/clients/claude.ts index 8c45dc3..460b5dd 100644 --- a/src/clients/claude.ts +++ b/src/clients/claude.ts @@ -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'); diff --git a/src/generators/prd.ts b/src/generators/prd.ts index 84b21e2..f742dbe 100644 --- a/src/generators/prd.ts +++ b/src/generators/prd.ts @@ -132,33 +132,201 @@ Output the complete PROMPT.md content as raw markdown (no code fences around the ): Promise { 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); -${projectName} + return `# ${projectName} - Implementation Guide -${architecture.appType} +## Overview - -${JSON.stringify(specification.techStack, null, 2)} - +This guide walks you through implementing ${projectName} using the Ralph Method with phase-by-phase execution. - -${specification.features.map((f, i) => `${i + 1}. ${f.name}: ${f.description}`).join('\n')} - +**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( diff --git a/src/generators/scaffold.ts b/src/generators/scaffold.ts index cbda49f..08ab477 100644 --- a/src/generators/scaffold.ts +++ b/src/generators/scaffold.ts @@ -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 { + 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: ${phase.promise} + +If blocked and cannot proceed: +Output: ABORT_BLOCKED +`; + } + + 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 { 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/', ]; diff --git a/src/generators/specification.ts b/src/generators/specification.ts index 5b8fa76..2cf60fc 100644 --- a/src/generators/specification.ts +++ b/src/generators/specification.ts @@ -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}`); } } diff --git a/src/utils/files.ts b/src/utils/files.ts index 8668714..90c066e 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -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 { await mkdir(path, { recursive: true }); }