From 0d79e2f2b5829b674086d0bbdcb14865c5eec352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arwid=20Thornstro=CC=88m?= Date: Thu, 13 Nov 2025 22:37:01 +0100 Subject: [PATCH] added translate textadmin excel --- package-lock.json | 449 ++++++++++++- package.json | 8 +- .../ai/translate_excel/basic_translation.txt | 114 ++++ .../ai/translate_excel/translate_excel.ts | 589 ++++++++++++++++++ src/commands/setup/settings/settings.ts | 22 + src/globals.ts | 4 + 6 files changed, 1179 insertions(+), 7 deletions(-) create mode 100644 src/commands/ai/translate_excel/basic_translation.txt create mode 100644 src/commands/ai/translate_excel/translate_excel.ts 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 and : + +{ + "Danish (Denmark)": "string", + "German (Germany)": "string", + "English (Europe)": "string", + "English (United Kingdom)": "string", + "English (United States)": "string", + "Spanish (Spain)": "string", + "Finnish (Finland)": "string", + "French (France)": "string", + "Italian (Italy)": "string", + "Japanese": "string", + "Dutch (Netherlands)": "string", + "Norwegian Nynorsk (Norway)": "string", + "Polish (Poland)": "string", + "Portuguese (Portugal)": "string", + "Romanian (Romania)": "string", + "Swedish (Sweden)": "string" +} + + +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;