diff --git a/package-lock.json b/package-lock.json
index 8f09955..aef7ca6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@google/generative-ai": "^0.21.0",
"@lenne.tech/gluegun-menu": "^0.0.7",
"@mailchimp/mailchimp_transactional": "^1.0.59",
"@octokit/core": "4.2.0",
@@ -18,9 +19,11 @@
"enquirer": "^2.4.1",
"gluegun": "latest",
"lodash.snakecase": "^4.1.1",
+ "openai": "^4.73.0",
"pg": "^8.11.3",
"strip-ansi": "^7.1.0",
- "terminal-table-output": "^1.4.0"
+ "terminal-table-output": "^1.4.0",
+ "xlsx": "^0.18.5"
},
"bin": {
"pwcli": "bin/pwcli"
@@ -850,6 +853,15 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@google/generative-ai": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
+ "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"dev": true,
@@ -1718,12 +1730,21 @@
},
"node_modules/@types/node": {
"version": "24.0.3",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
}
},
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"license": "MIT"
@@ -2006,6 +2027,18 @@
"darwin"
]
},
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"dev": true,
@@ -2036,6 +2069,27 @@
"node": ">=0.4.0"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"dev": true,
@@ -2365,6 +2419,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"license": "MIT",
@@ -2402,6 +2469,19 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"license": "MIT",
@@ -2661,6 +2741,15 @@
"node": ">= 0.12.0"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"dev": true,
@@ -2720,6 +2809,18 @@
"node": ">=10"
}
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/create-require": {
"version": "1.1.1",
"dev": true,
@@ -2813,6 +2914,20 @@
"node": ">=0.3.1"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"dev": true,
@@ -2888,6 +3003,51 @@
"is-arrayish": "^0.2.1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.1.1",
"dev": true,
@@ -3157,6 +3317,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"license": "MIT",
@@ -3403,17 +3572,49 @@
}
},
"node_modules/form-data": {
- "version": "4.0.0",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/fs-jetpack": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz",
@@ -3440,6 +3641,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
@@ -3456,6 +3666,30 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"dev": true,
@@ -3464,6 +3698,19 @@
"node": ">=8.0.0"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"license": "MIT",
@@ -3611,6 +3858,18 @@
"node": ">=10"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"dev": true,
@@ -3628,6 +3887,45 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"dev": true,
@@ -3640,6 +3938,15 @@
"node": ">=10.17.0"
}
},
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.2.4",
"dev": true,
@@ -4706,6 +5013,15 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"license": "MIT"
@@ -4776,7 +5092,6 @@
},
"node_modules/ms": {
"version": "2.1.2",
- "dev": true,
"license": "MIT"
},
"node_modules/napi-postinstall": {
@@ -4802,6 +5117,26 @@
"version": "1.0.5",
"license": "MIT"
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"license": "MIT",
@@ -4875,6 +5210,51 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/openai": {
+ "version": "4.104.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ },
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "ws": "^8.18.0",
+ "zod": "^3.23.8"
+ },
+ "peerDependenciesMeta": {
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/openai/node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/openai/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"dev": true,
@@ -5579,6 +5959,18 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/stack-utils": {
"version": "2.0.6",
"dev": true,
@@ -6044,7 +6436,6 @@
},
"node_modules/undici-types": {
"version": "7.8.0",
- "dev": true,
"license": "MIT"
},
"node_modules/universal-user-agent": {
@@ -6162,6 +6553,15 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"license": "BSD-2-Clause"
@@ -6187,6 +6587,24 @@
"node": ">= 8"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"dev": true,
@@ -6323,6 +6741,27 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index 1158725..a5d3a76 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"compile": "tsc -p .",
"copy-templates": "if [ -e ./src/templates ]; then cp -a ./src/templates ./build/; fi",
"copy-promptcontextlibrary-txts": "mkdir -p build/commands/ai/promptcontextlibrary && cp src/commands/ai/promptcontextlibrary/*.txt build/commands/ai/promptcontextlibrary/ || true",
- "build": "yarn format && yarn clean-build && yarn compile && yarn copy-templates && yarn copy-promptcontextlibrary-txts",
+ "copy-translate-excel-txts": "mkdir -p build/commands/ai/translate_excel && cp src/commands/ai/translate_excel/*.txt build/commands/ai/translate_excel/ || true",
+ "build": "yarn format && yarn clean-build && yarn compile && yarn copy-templates && yarn copy-promptcontextlibrary-txts && yarn copy-translate-excel-txts",
"prepublishOnly": "yarn build",
"test": "jest",
"watch": "jest --watch",
@@ -31,6 +32,7 @@
],
"license": "MIT",
"dependencies": {
+ "@google/generative-ai": "^0.21.0",
"@lenne.tech/gluegun-menu": "^0.0.7",
"@mailchimp/mailchimp_transactional": "^1.0.59",
"@octokit/core": "4.2.0",
@@ -40,9 +42,11 @@
"enquirer": "^2.4.1",
"gluegun": "latest",
"lodash.snakecase": "^4.1.1",
+ "openai": "^4.73.0",
"pg": "^8.11.3",
"strip-ansi": "^7.1.0",
- "terminal-table-output": "^1.4.0"
+ "terminal-table-output": "^1.4.0",
+ "xlsx": "^0.18.5"
},
"devDependencies": {
"@types/jest": "^30.0.0",
diff --git a/src/commands/ai/translate_excel/basic_translation.txt b/src/commands/ai/translate_excel/basic_translation.txt
new file mode 100644
index 0000000..efd1a5e
--- /dev/null
+++ b/src/commands/ai/translate_excel/basic_translation.txt
@@ -0,0 +1,114 @@
+You are a translation expert and copywriter working with translating text for the company Photowall.
+Photowall is a company with a global e-commerce website selling custom printed wallpaper, canvas, posters and paint.
+Photowall have two different types of wallpaper. One type is non repeating patterns (wall murals) and one is repeating patterns (wallpapers).
+Photowall sell a highend wallpaper product of exclusive quality.
+
+Translate to natural sounding language.
+Make the text in the translated text some and flow naturally for that specific language.
+Use the correct casing for the translated language.
+
+The tone of voice should be premium and highend.
+
+When translating the word "wallpaper" use these translations
+"Danish (Denmark)": "tapet",
+"German (Germany)": "tapete",
+"English (Europe)": "wallpaper",
+"English (United Kingdom)": "wallpaper",
+"English (United States)": "wallpaper",
+"Spanish (Spain)": "papel pintado",
+"Finnish (Finland)": "tapetti",
+"French (France)": "papier peint",
+"Italian (Italy)": "carta da parati",
+"Japanese": "壁紙",
+"Dutch (Netherlands)": "behang",
+"Norwegian Nynorsk (Norway)": "tapet",
+"Polish (Poland)": "tapeta",
+"Portuguese (Portugal)": "papel de parede",
+"Romanian (Romania)": "tapet",
+"Swedish (Sweden)": "tapet"
+
+When translating the word "wallpapers" use these translations
+"Danish (Denmark)": "tapeter",
+"German (Germany)": "tapeten",
+"English (Europe)": "wallpapers",
+"English (United Kingdom)": "wallpapers",
+"English (United States)": "wallpapers",
+"Spanish (Spain)": "papeles pintados",
+"Finnish (Finland)": "tapetit",
+"French (France)": "papiers peints",
+"Italian (Italy)": "carte da parati",
+"Japanese": "壁紙",
+"Dutch (Netherlands)": "behang",
+"Norwegian Nynorsk (Norway)": "tapeter",
+"Polish (Poland)": "tapety",
+"Portuguese (Portugal)": "papéis de parede",
+"Romanian (Romania)": "tapete",
+"Swedish (Sweden)": "tapeter"
+
+When translating the word "wall mural" use these translations
+"Danish (Denmark)": "fototapet",
+"German (Germany)": "fototapete",
+"English (Europe)": "wall mural",
+"English (United Kingdom)": "wall mural",
+"English (United States)": "wall mural",
+"Spanish (Spain)": "fotomural",
+"Finnish (Finland)": "valokuvatapetti",
+"French (France)": "décoration murale",
+"Italian (Italy)": "decorazione murale",
+"Japanese": "ウォールミューラル",
+"Dutch (Netherlands)": "fotobehang",
+"Norwegian Nynorsk (Norway)": "fototapet",
+"Polish (Poland)": "fototapeta",
+"Portuguese (Portugal)": "mural de parede",
+"Romanian (Romania)": "fototapet",
+"Swedish (Sweden)": "fototapet"
+
+When translating the word "wall murals" use these translations
+"Danish (Denmark)": "fototapeter",
+"German (Germany)": "fototapeten",
+"English (Europe)": "wall murals",
+"English (United Kingdom)": "wall murals",
+"English (United States)": "wall murals",
+"Spanish (Spain)": "fotomurales",
+"Finnish (Finland)": "valokuvatapetit",
+"French (France)": "papiers peints panoramiques",
+"Italian (Italy)": "fotomurali",
+"Japanese": "ウォールミューラル",
+"Dutch (Netherlands)": "fotobehang",
+"Norwegian Nynorsk (Norway)": "fototapeter",
+"Polish (Poland)": "fototapety",
+"Portuguese (Portugal)": "murais de parede",
+"Romanian (Romania)": "fototapete",
+"Swedish (Sweden)": "fototapeter"
+
+The languages are: Danish, German, English, Spanish, Finnish, French, Italian, Japanese, Dutch, Norwegian, Polish, Portuguese, Romanian, Swedish
+
+The english input text for translation is
+
+{input}
+
+
+!IMPORTANT!
+Return only the result in a JSON format defined within the :
+
+
+Output the JSON string directly without any additional formatting or delimiters.
diff --git a/src/commands/ai/translate_excel/translate_excel.ts b/src/commands/ai/translate_excel/translate_excel.ts
new file mode 100644
index 0000000..3ab0e06
--- /dev/null
+++ b/src/commands/ai/translate_excel/translate_excel.ts
@@ -0,0 +1,589 @@
+import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
+import chalk = require('chalk');
+import { getSettings } from '../../../globals';
+const XLSX = require('xlsx');
+const fs = require('fs');
+const path = require('path');
+const { Select } = require('enquirer');
+const { GoogleGenerativeAI } = require('@google/generative-ai');
+const OpenAI = require('openai');
+
+// Debug mode: set to true to save all AI model responses to JSON files
+const DEBUG_MODE = false;
+
+// Function to save debug response to JSON file
+function saveDebugResponse(
+ response: string,
+ debugDir: string,
+ filename: string,
+ metadata: any = {},
+): void {
+ if (!DEBUG_MODE) return;
+
+ try {
+ // Ensure debug directory exists
+ if (!fs.existsSync(debugDir)) {
+ fs.mkdirSync(debugDir, { recursive: true });
+ }
+
+ const debugData = {
+ timestamp: new Date().toISOString(),
+ response: response,
+ ...metadata,
+ };
+
+ const filePath = path.join(debugDir, filename);
+ fs.writeFileSync(filePath, JSON.stringify(debugData, null, 2), 'utf8');
+ } catch (error) {
+ console.error('Error saving debug response:', error.message);
+ }
+}
+
+// Function to extract JSON from text that might be wrapped in markdown code blocks
+function extractJSON(text: string): string {
+ const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/;
+ const match = text.match(jsonRegex);
+
+ if (match && match[1]) {
+ return match[1].trim();
+ }
+
+ return text.trim();
+}
+
+// Function to validate and fix JSON if needed
+async function validateAndFixJSON(
+ response: string,
+ processorFunction: (
+ input: string,
+ prompt: string,
+ apiKey: string,
+ model: string,
+ debugDir?: string,
+ debugMetadata?: any,
+ ) => Promise,
+ apiKey: string,
+ model: string,
+ debugDir?: string,
+ debugMetadata?: any,
+): Promise<{ isValid: boolean; json: any; rawText: string } | null> {
+ // Try to extract and parse JSON from the response
+ const extractedJSON = extractJSON(response);
+
+ try {
+ const parsed = JSON.parse(extractedJSON);
+ return { isValid: true, json: parsed, rawText: extractedJSON };
+ } catch (error) {
+ // JSON is invalid, try to fix it once
+ console.log(
+ chalk.yellow('⚠️ Invalid JSON detected, attempting to fix...'),
+ );
+
+ const fixPrompt = `The following text should be valid JSON but contains errors. Please fix it to be valid JSON format.
+
+IMPORTANT INSTRUCTIONS:
+- Output ONLY valid JSON, nothing else
+- Do not add any explanations, comments, or markdown formatting
+- Do not wrap the output in code blocks or backticks
+- Preserve all the data from the original text
+- Fix any syntax errors like missing commas, quotes, brackets, or braces
+- Ensure all string values are properly quoted
+- Ensure all keys are properly quoted
+- Remove any trailing commas
+- Make sure the JSON structure is complete and properly closed
+
+BROKEN TEXT TO FIX:
+${response}
+
+OUTPUT ONLY THE FIXED JSON:`;
+
+ const fixedResponse = await processorFunction(
+ '',
+ fixPrompt,
+ apiKey,
+ model,
+ debugDir,
+ { ...debugMetadata, isFixAttempt: true, originalResponse: response },
+ );
+
+ if (!fixedResponse) {
+ return null;
+ }
+
+ // Try to parse the fixed response
+ const fixedExtractedJSON = extractJSON(fixedResponse);
+ try {
+ const parsed = JSON.parse(fixedExtractedJSON);
+ console.log(chalk.green('✓ Successfully fixed invalid JSON'));
+ return { isValid: true, json: parsed, rawText: fixedExtractedJSON };
+ } catch (fixError) {
+ console.error(chalk.red('✗ Failed to fix JSON even after retry'));
+ return null;
+ }
+ }
+}
+
+// Process text with Gemini AI
+async function processWithGemini(
+ inputText: string,
+ promptTemplate: string,
+ apiKey: string,
+ model: string,
+ debugDir?: string,
+ debugMetadata?: any,
+) {
+ const generationConfig = {
+ temperature: 0.7, // Slightly lower for more consistent JSON output
+ topP: 0.95,
+ topK: 40,
+ maxOutputTokens: 32768, // Significantly increased for large translation responses (Gemini 2.5 Pro supports up to 65,536)
+ };
+
+ const genAI = new GoogleGenerativeAI(apiKey);
+ const geminiModel = genAI.getGenerativeModel({
+ model: model,
+ generationConfig,
+ });
+
+ try {
+ // If inputText is provided, replace {input} placeholder, otherwise use prompt as-is
+ const prompt = inputText
+ ? promptTemplate.replace('{input}', inputText)
+ : promptTemplate;
+ const result = await geminiModel.generateContent(prompt);
+ const response = result.response;
+ const responseText = response.text();
+
+ // Save debug response if debug mode is enabled
+ if (DEBUG_MODE && debugDir) {
+ const filename = `gemini_${Date.now()}_${debugMetadata?.rowNumber || 'unknown'}.json`;
+ saveDebugResponse(responseText, debugDir, filename, {
+ provider: 'Gemini',
+ model: model,
+ inputText: inputText,
+ prompt: prompt,
+ ...debugMetadata,
+ });
+ }
+
+ return responseText;
+ } catch (error) {
+ console.error('Error processing with Gemini:', error.message);
+ return null;
+ }
+}
+
+// Process text with OpenAI
+async function processWithOpenAI(
+ inputText: string,
+ promptTemplate: string,
+ apiKey: string,
+ model: string,
+ debugDir?: string,
+ debugMetadata?: any,
+) {
+ const openai = new OpenAI({ apiKey });
+
+ try {
+ // If inputText is provided, replace {input} placeholder, otherwise use prompt as-is
+ const prompt = inputText
+ ? promptTemplate.replace('{input}', inputText)
+ : promptTemplate;
+ const response = await openai.chat.completions.create({
+ model: model,
+ messages: [{ role: 'user', content: prompt }],
+ });
+
+ const responseText = response.choices[0]?.message?.content || null;
+
+ // Save debug response if debug mode is enabled
+ if (DEBUG_MODE && debugDir && responseText) {
+ const filename = `openai_${Date.now()}_${debugMetadata?.rowNumber || 'unknown'}.json`;
+ saveDebugResponse(responseText, debugDir, filename, {
+ provider: 'OpenAI',
+ model: model,
+ inputText: inputText,
+ prompt: prompt,
+ ...debugMetadata,
+ });
+ }
+
+ return responseText;
+ } catch (error) {
+ console.error('Error processing with OpenAI:', error.message);
+ return null;
+ }
+}
+
+module.exports = {
+ name: 'translate_excel',
+ alias: ['te'],
+ description: 'Translate Excel files using AI (te)',
+ hidden: false,
+ run: async (toolbox: GluegunMenuToolbox) => {
+ const { print, filesystem } = toolbox;
+
+ print.info(chalk.cyan('📊 Excel Translation Tool'));
+ print.info('');
+
+ // Get settings to check for API keys
+ const config = await getSettings(toolbox);
+ if (!config) {
+ print.error(
+ chalk.red('❌ No configuration found. Please run pwcli setup settings'),
+ );
+ return;
+ }
+
+ // Check which API keys are configured
+ const hasGemini = config.llm_api_keys?.gemini_api_key;
+ const hasOpenAI = config.llm_api_keys?.openai_api_key;
+
+ if (!hasGemini && !hasOpenAI) {
+ print.error(
+ chalk.red(
+ '❌ No AI API keys configured. Please run pwcli setup settings',
+ ),
+ );
+ return;
+ }
+
+ // Step 1: Find Excel files in current directory
+ const currentDir = process.cwd();
+ const files = fs.readdirSync(currentDir);
+ const excelFiles = files.filter((file: string) =>
+ file.toLowerCase().endsWith('.xlsx'),
+ );
+
+ if (excelFiles.length === 0) {
+ print.error(
+ chalk.red('❌ No Excel (.xlsx) files found in current directory'),
+ );
+ return;
+ }
+
+ // Step 2: Let user select an Excel file
+ const filePrompt = new Select({
+ name: 'excelFile',
+ message: 'Select the Excel file to translate:',
+ choices: excelFiles,
+ });
+
+ const selectedFile = await filePrompt.run();
+ print.info(chalk.gray(`Selected file: ${selectedFile}`));
+ print.info('');
+
+ // Step 3: Find prompt .txt files in translate_excel folder
+ const promptDir = path.join(__dirname, '.');
+ const promptFiles = fs
+ .readdirSync(promptDir)
+ .filter((file: string) => file.toLowerCase().endsWith('.txt'));
+
+ if (promptFiles.length === 0) {
+ print.error(
+ chalk.red('❌ No prompt (.txt) files found in translate_excel folder'),
+ );
+ return;
+ }
+
+ // Create a mapping of display names to actual filenames
+ const promptDisplayNames = promptFiles.map((file: string) => {
+ // Remove .txt extension and convert to Title Case
+ const nameWithoutExt = file.replace(/\.txt$/i, '');
+ // Replace underscores and hyphens with spaces, then title case
+ return nameWithoutExt
+ .replace(/[_-]/g, ' ')
+ .split(' ')
+ .map(
+ (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
+ )
+ .join(' ');
+ });
+
+ // Create mapping object for later use
+ const promptMapping = {};
+ promptFiles.forEach((file: string, index: number) => {
+ promptMapping[promptDisplayNames[index]] = file;
+ });
+
+ // Step 4: Let user select a prompt file
+ const promptPrompt = new Select({
+ name: 'promptFile',
+ message: 'Select the translation prompt to use:',
+ choices: promptDisplayNames,
+ });
+
+ const selectedPromptDisplay = await promptPrompt.run();
+ const selectedPromptFile = promptMapping[selectedPromptDisplay];
+ const promptPath = path.join(promptDir, selectedPromptFile);
+ const promptTemplate = fs.readFileSync(promptPath, 'utf8');
+ print.info(chalk.gray(`Selected prompt: ${selectedPromptDisplay}`));
+ print.info('');
+
+ // Step 5: Let user select AI provider
+ const providerChoices = [];
+ if (hasGemini) providerChoices.push('Gemini');
+ if (hasOpenAI) providerChoices.push('OpenAI');
+
+ const providerPrompt = new Select({
+ name: 'provider',
+ message: 'Select AI provider:',
+ choices: providerChoices,
+ });
+
+ const selectedProvider = await providerPrompt.run();
+ print.info(chalk.gray(`Selected provider: ${selectedProvider}`));
+ print.info('');
+
+ // Step 6: Let user select model based on provider
+ let modelChoices: string[] = [];
+ let selectedModel = '';
+
+ if (selectedProvider === 'Gemini') {
+ modelChoices = [
+ 'gemini-flash-latest',
+ 'gemini-flash-lite-latest',
+ 'gemini-2.5-pro',
+ ];
+ const modelPrompt = new Select({
+ name: 'model',
+ message: 'Select Gemini model:',
+ choices: modelChoices,
+ });
+ selectedModel = await modelPrompt.run();
+ } else if (selectedProvider === 'OpenAI') {
+ modelChoices = ['gpt-5-nano', 'gpt-5-mini', 'gpt-5.1'];
+ const modelPrompt = new Select({
+ name: 'model',
+ message: 'Select OpenAI model:',
+ choices: modelChoices,
+ });
+ selectedModel = await modelPrompt.run();
+ }
+
+ print.info(chalk.gray(`Selected model: ${selectedModel}`));
+ print.info('');
+
+ // Step 7: Read and process the Excel file
+ const filePath = path.join(currentDir, selectedFile);
+ const workbook = XLSX.readFile(filePath);
+
+ // Use first sheet
+ const sheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[sheetName];
+
+ // Convert to JSON
+ const data = XLSX.utils.sheet_to_json(worksheet);
+
+ if (data.length === 0) {
+ print.error(chalk.red('❌ Excel file is empty'));
+ return;
+ }
+
+ // Get original column order
+ const originalColumns = Object.keys(data[0]);
+
+ // Check if "English (Europe)" column exists
+ const inputColumn = 'English (Europe)';
+ if (!data[0].hasOwnProperty(inputColumn)) {
+ print.error(
+ chalk.red(
+ `❌ Column "${inputColumn}" not found. Available columns: ${originalColumns.join(', ')}`,
+ ),
+ );
+ return;
+ }
+
+ print.info(
+ chalk.cyan(`Processing ${data.length} rows from sheet "${sheetName}"...`),
+ );
+ print.info('');
+
+ // Set up debug directory if debug mode is enabled
+ const debugDir = DEBUG_MODE
+ ? path.join(currentDir, `${path.parse(selectedFile).name}_debug`)
+ : undefined;
+
+ if (DEBUG_MODE && debugDir) {
+ print.info(
+ chalk.gray(`Debug mode enabled. Saving responses to: ${debugDir}`),
+ );
+ print.info('');
+ }
+
+ // Step 8: Process each row with AI
+ for (let i = 0; i < data.length; i++) {
+ const row = data[i];
+ const inputText = row[inputColumn];
+
+ if (!inputText || inputText.trim() === '') {
+ print.info(
+ chalk.gray(`Skipping row ${i + 1}/${data.length} (empty input text)`),
+ );
+ continue;
+ }
+
+ print.info(
+ chalk.yellow(
+ `Processing row ${i + 1}/${data.length}: "${inputText.substring(0, 50)}${inputText.length > 50 ? '...' : ''}"`,
+ ),
+ );
+
+ let aiResult = null;
+ let processorFunction = null;
+ let apiKey = null;
+
+ const debugMetadata = {
+ rowNumber: i + 1,
+ totalRows: data.length,
+ inputColumn: inputColumn,
+ selectedFile: selectedFile,
+ selectedPrompt: selectedPromptDisplay,
+ };
+
+ if (selectedProvider === 'Gemini') {
+ processorFunction = processWithGemini;
+ apiKey = config.llm_api_keys.gemini_api_key;
+ aiResult = await processWithGemini(
+ inputText,
+ promptTemplate,
+ apiKey,
+ selectedModel,
+ debugDir,
+ debugMetadata,
+ );
+ } else if (selectedProvider === 'OpenAI') {
+ processorFunction = processWithOpenAI;
+ apiKey = config.llm_api_keys.openai_api_key;
+ aiResult = await processWithOpenAI(
+ inputText,
+ promptTemplate,
+ apiKey,
+ selectedModel,
+ debugDir,
+ debugMetadata,
+ );
+ }
+
+ if (aiResult) {
+ // Validate and fix JSON if needed
+ const validationResult = await validateAndFixJSON(
+ aiResult,
+ processorFunction,
+ apiKey,
+ selectedModel,
+ debugDir,
+ debugMetadata,
+ );
+
+ if (validationResult && validationResult.json) {
+ const translationResult = validationResult.json;
+
+ // Update the row with translations from the LLM
+ for (const [country, translation] of Object.entries(
+ translationResult,
+ )) {
+ // Add translation if it's valid
+ if (translation !== null && translation !== undefined) {
+ row[country] = translation;
+ }
+ }
+
+ print.success(chalk.green(`✓ Translated row ${i + 1}`));
+ } else {
+ print.error(
+ chalk.red(
+ `❌ Error parsing AI response for row ${i + 1}: Invalid JSON even after retry attempt`,
+ ),
+ );
+ console.error('AI response:', aiResult);
+ }
+ } else {
+ print.error(chalk.red(`❌ AI processing failed for row ${i + 1}`));
+ }
+ }
+
+ // Step 9: Write the translated data to a new Excel file
+ const parsedPath = path.parse(filePath);
+ const outputFilePath = path.join(
+ parsedPath.dir,
+ `${parsedPath.name}_translated${parsedPath.ext}`,
+ );
+
+ // Define the expected column order for output
+ const expectedColumnOrder = [
+ 'name',
+ 'category',
+ 'Danish (Denmark)',
+ 'German (Austria)',
+ 'German (Switzerland)',
+ 'German (Germany)',
+ 'German (Luxembourg)',
+ 'English (Australia)',
+ 'English (Canada)',
+ 'English (Estonia)',
+ 'English (Europe)',
+ 'English (United Kingdom)',
+ 'English (Ireland)',
+ 'English (New Zealand)',
+ 'English (Singapore)',
+ 'English (United States)',
+ 'Spanish (Spain)',
+ 'Finnish (Finland)',
+ 'French (Belgium)',
+ 'French (Canada)',
+ 'French (Switzerland)',
+ 'French (France)',
+ 'French (Luxembourg)',
+ 'Italian (Switzerland)',
+ 'Italian (Italy)',
+ 'Japanese',
+ 'Dutch (Belgium)',
+ 'Dutch (Netherlands)',
+ 'Norwegian Nynorsk (Norway)',
+ 'Polish (Poland)',
+ 'Portuguese (Portugal)',
+ 'Romanian (Romania)',
+ 'Swedish (Sweden)',
+ ];
+
+ // Build final column order: all expected columns + any extra columns from input
+ const finalColumnOrder = [
+ ...expectedColumnOrder,
+ ...originalColumns.filter((col) => !expectedColumnOrder.includes(col)),
+ ];
+
+ // Ensure each row has all columns in the correct order (even if empty)
+ const normalizedData = data.map((row) => {
+ const normalizedRow = {};
+ for (const col of finalColumnOrder) {
+ normalizedRow[col] = row.hasOwnProperty(col) ? row[col] : '';
+ }
+ return normalizedRow;
+ });
+
+ // Write with the specified column order
+ const newWorksheet = XLSX.utils.json_to_sheet(normalizedData, {
+ header: finalColumnOrder,
+ });
+
+ const newWorkbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, sheetName);
+
+ XLSX.writeFile(newWorkbook, outputFilePath);
+
+ print.info('');
+ print.success(
+ chalk.green(
+ `✅ Successfully exported translated data to ${outputFilePath}`,
+ ),
+ );
+ print.info(
+ chalk.gray(`Processed ${data.length} rows from sheet "${sheetName}"`),
+ );
+ print.info('');
+
+ // Return to AI menu
+ await toolbox.menu.showMenu('ai');
+ },
+};
diff --git a/src/commands/setup/settings/settings.ts b/src/commands/setup/settings/settings.ts
index 660badc..d48632d 100644
--- a/src/commands/setup/settings/settings.ts
+++ b/src/commands/setup/settings/settings.ts
@@ -114,6 +114,18 @@ module.exports = {
localhost_url: '',
localhost_username: '',
localhost_password: '',
+ llm_api_keys: {
+ gemini_api_key: '',
+ openai_api_key: '',
+ },
+ };
+ }
+
+ // Ensure llm_api_keys exists in existing configs
+ if (!config.llm_api_keys) {
+ config.llm_api_keys = {
+ gemini_api_key: '',
+ openai_api_key: '',
};
}
@@ -149,6 +161,16 @@ module.exports = {
initial: config.localhost_password,
}).run();
+ config.llm_api_keys.gemini_api_key = await new Input({
+ message: `If you want to use Gemini AI features, please insert\nyour Gemini API key. Otherwise leave blank >`,
+ initial: config.llm_api_keys.gemini_api_key,
+ }).run();
+
+ config.llm_api_keys.openai_api_key = await new Input({
+ message: `If you want to use OpenAI features, please insert\nyour OpenAI API key. Otherwise leave blank >`,
+ initial: config.llm_api_keys.openai_api_key,
+ }).run();
+
print.info(`
${JSON.stringify(config)}
diff --git a/src/globals.ts b/src/globals.ts
index 064e1b9..a5a93e5 100644
--- a/src/globals.ts
+++ b/src/globals.ts
@@ -14,6 +14,10 @@ export type TConfig = {
localhost_url?: string;
localhost_username?: string;
localhost_password?: string;
+ llm_api_keys?: {
+ gemini_api_key: string;
+ openai_api_key: string;
+ };
};
export const getSettings = async (toolbox): Promise => {
const { filesystem, print } = toolbox;