From e2234082b8d055cb85a88463beb6e360375b9ccc Mon Sep 17 00:00:00 2001 From: Michele Gobbi Date: Tue, 11 Nov 2025 17:03:56 +0100 Subject: [PATCH 1/4] added AI to cli version --- .env.example | 3 + .gitignore | 1 + AI_CHAT_README.md | 101 ++ debug-line-number.js | 53 + package-lock.json | 1208 +++++++++++++---- package.json | 4 + src/host/aiChatServer.js | 315 +++++ src/host/keyboard-KeyboardEvent.js | 12 + src/host/webFrontEnd/aiChatDialog.css | 228 ++++ src/host/webFrontEnd/aiChatDialog.js | 467 +++++++ src/host/webFrontEnd/basicFileWatcher.js | 143 ++ src/host/webFrontEnd/dialogs.css | 4 + .../webFrontEnd/images/svg_icon-ai-chat.svg | 6 + src/host/webFrontEnd/index.js | 7 + src/host/webFrontEnd/lowerTray.js | 7 + src/host/webFrontEnd/template.ejs | 32 +- src/tools/basicExtractor.js | 141 ++ src/tools/basicTokenizer.js | 277 ++++ test-full-tokenizer.js | 135 ++ test-operators.js | 89 ++ test-program-structure.js | 114 ++ test-tokenizer.js | 64 + webpack.config.js | 11 + 23 files changed, 3168 insertions(+), 254 deletions(-) create mode 100644 .env.example create mode 100644 AI_CHAT_README.md create mode 100644 debug-line-number.js create mode 100644 src/host/aiChatServer.js create mode 100644 src/host/webFrontEnd/aiChatDialog.css create mode 100644 src/host/webFrontEnd/aiChatDialog.js create mode 100644 src/host/webFrontEnd/basicFileWatcher.js create mode 100644 src/host/webFrontEnd/images/svg_icon-ai-chat.svg create mode 100644 src/tools/basicExtractor.js create mode 100644 src/tools/basicTokenizer.js create mode 100644 test-full-tokenizer.js create mode 100644 test-operators.js create mode 100644 test-program-structure.js create mode 100644 test-tokenizer.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3cbcbfc --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# OpenAI API Configuration +# Get your API key from: https://platform.openai.com/api-keys +OPENAI_API_KEY=your_openai_api_key_here diff --git a/.gitignore b/.gitignore index f06235c..9c97bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.env diff --git a/AI_CHAT_README.md b/AI_CHAT_README.md new file mode 100644 index 0000000..7b6fc69 --- /dev/null +++ b/AI_CHAT_README.md @@ -0,0 +1,101 @@ +# AI Chat Assistant per Programmi BASIC del C64 + +## Setup + +1. **Get your OpenAI API Key** + - Visit https://platform.openai.com/api-keys + - Create a new API key or use an existing one + +2. **Configure the API Key** + - Open the `.env` file in the project root + - Replace `your_openai_api_key_here` with your actual OpenAI API key: + ``` + OPENAI_API_KEY=sk-your-actual-key-here + ``` + +3. **Start the Development Server** + ```bash + npm run start + ``` + +## Usage + +1. Open the web emulator at http://localhost:8081 +2. Optionally, type a BASIC program in the C64 (or load one) +3. Click the AI chat icon (💬) in the lower tray (next to the joystick and keyboard buttons) +4. Type your request in the chat input +5. The AI will analyze the current BASIC program and respond +6. If the AI proposes changes to the BASIC program, you'll see a confirmation dialog with: + - Description of the change + - The new BASIC program + - "Apply Changes" and "Reject" buttons +7. Click "Apply Changes" to have the program automatically typed into the C64, or "Reject" to dismiss it + +## Features + +- **BASIC Program Context**: The AI has access to the current BASIC program in the C64's memory +- **Interactive Modifications**: The AI can propose specific changes or write new programs +- **Automatic Entry**: Approved programs are automatically typed into the C64 +- **User Confirmation**: All code changes require explicit user approval +- **Conversation History**: The chat maintains context across multiple messages + +## How It Works + +1. When you send a message, the frontend extracts the current BASIC program from C64 RAM (starting at $0801) +2. The program is detokenized (converted from internal format to readable text) +3. The detokenized program is sent to OpenAI along with your message +4. OpenAI (GPT-4) analyzes your request and responds +5. If the AI wants to modify or create a program, it includes a special JSON block in its response +6. The frontend displays a confirmation dialog showing the new program +7. If you approve, the program is automatically typed into the C64: + - Types `NEW` to clear memory + - Types each line of the program with RETURN + - Ready to RUN! + +## Example Requests + +- "Write a program that draws a rainbow" +- "Add sound effects to this program" +- "Fix the bug on line 40" +- "Make this program faster" +- "Add a high score system" +- "Convert this to use sprites" +- "Explain what this program does" + +## Security Notes + +- **Never commit your `.env` file** - it's already in `.gitignore` +- Keep your OpenAI API key secret +- The AI can modify BASIC programs in the C64 when you approve changes +- Always review proposed changes before applying them + +## Troubleshooting + +**"OpenAI API key not configured"** +- Make sure you've created a `.env` file with a valid API key + +**"Failed to communicate with AI"** +- Check your internet connection +- Verify your API key is valid and has sufficient credits +- Check the browser console for detailed error messages + +**Changes not being applied** +- Check the browser console for error messages +- Make sure the C64 emulator is running +- Try typing the program manually to test + +**Program doesn't extract correctly** +- Make sure you have a BASIC program loaded +- Try typing `LIST` in the C64 to verify the program exists +- The extractor reads from memory address $0801 + +## Cost Considerations + +Using the OpenAI API incurs costs based on: +- The amount of text sent (including your BASIC program as context) +- The model used (GPT-4) +- The number of requests made + +Monitor your usage at: https://platform.openai.com/usage + +Note: BASIC programs are very small compared to the emulator codebase, so costs should be minimal! diff --git a/debug-line-number.js b/debug-line-number.js new file mode 100644 index 0000000..a582402 --- /dev/null +++ b/debug-line-number.js @@ -0,0 +1,53 @@ +// Debug what we're actually writing vs what we expect + +const program = "10 PRINT \"HELLO\""; + +// Parse line +const match = program.match(/^(\d+)\s+(.*)$/); +const lineNum = parseInt(match[1], 10); +const lineText = match[2]; + +console.log('Line number:', lineNum); +console.log('Line text:', lineText); + +// Line number in little-endian +const loByte = lineNum & 0xFF; +const hiByte = (lineNum >> 8) & 0xFF; + +console.log('Line number bytes:'); +console.log(' Low byte:', loByte, '(0x' + loByte.toString(16) + ')'); +console.log(' High byte:', hiByte, '(0x' + hiByte.toString(16) + ')'); + +// If we write these backwards by mistake: +const wrongNum = hiByte | (loByte << 8); +console.log('If bytes are swapped:', wrongNum); + +// Test tokenization +const TOKEN_PRINT = 0x99; +console.log('\nExpected tokenization of PRINT "HELLO":'); +console.log('0x99 (PRINT token)'); +console.log('0x20 (space)'); +console.log('0x22 (quote)'); +console.log('0x48 0x45 0x4C 0x4C 0x4F (HELLO)'); +console.log('0x22 (quote)'); +console.log('0x00 (EOL)'); + +// What if we're writing the pointer wrong? +const addr = 0x0801; +const lineLength = 2 + 2 + 9 + 1; // ptr + linenum + content + eol = 14 +const nextAddr = addr + lineLength; // 0x080f + +console.log('\nNext line pointer:'); +console.log('Next address: 0x' + nextAddr.toString(16)); +console.log('Low byte:', nextAddr & 0xFF, '(0x' + (nextAddr & 0xFF).toString(16) + ')'); +console.log('High byte:', (nextAddr >> 8) & 0xFF, '(0x' + ((nextAddr >> 8) & 0xFF).toString(16) + ')'); + +// Test what 15420 looks like in hex +console.log('\n15420 in hex: 0x' + (15420).toString(16)); +console.log('15420 low byte:', 15420 & 0xFF, '(0x' + (15420 & 0xFF).toString(16) + ')'); +console.log('15420 high byte:', (15420 >> 8) & 0xFF, '(0x' + ((15420 >> 8) & 0xFF).toString(16) + ')'); + +// Reverse engineering: what bytes give us 15420? +// 15420 = 0x3C3C +console.log('\n0x3C3C = ', 0x3C3C, '(ASCII: "<<")'); +console.log('0x3C in ASCII:', String.fromCharCode(0x3C)); diff --git a/package-lock.json b/package-lock.json index 4ed5248..fd50545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "viciious", "version": "0.1.0", "license": "Public Domain", + "dependencies": { + "dotenv": "^17.2.3", + "express": "^5.1.0" + }, "devDependencies": { "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.8.1", @@ -701,47 +705,57 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, "node_modules/body-parser/node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, + "node_modules/body-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">= 0.8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/bonjour-service": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", @@ -817,14 +831,30 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1058,10 +1088,10 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -1073,25 +1103,28 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/core-util-is": { "version": "1.0.2", @@ -1241,6 +1274,7 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -1350,11 +1384,37 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.556", @@ -1372,10 +1432,10 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1401,12 +1461,42 @@ "node": ">=4" } }, + "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-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", "dev": true }, + "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/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1419,8 +1509,7 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "node_modules/escape-string-regexp": { "version": "1.0.5", @@ -1478,7 +1567,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1522,67 +1611,117 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 0.6" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "node_modules/express/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -1621,28 +1760,50 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/finalhandler/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1693,18 +1854,17 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-monkey": { @@ -1734,26 +1894,51 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "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/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "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-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", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1829,6 +2014,18 @@ "node": ">=0.10.0" } }, + "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", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1853,11 +2050,11 @@ "node": ">= 0.4.0" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, + "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" }, @@ -1865,16 +2062,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/he": { @@ -2037,7 +2234,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -2053,7 +2250,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2062,7 +2259,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2191,12 +2388,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -2246,8 +2443,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/interpret": { "version": "3.1.1", @@ -2262,7 +2458,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "engines": { "node": ">= 0.10" } @@ -2372,6 +2567,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2542,13 +2743,22 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "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/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/memfs": { @@ -2564,10 +2774,16 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -2578,8 +2794,9 @@ "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2589,6 +2806,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2798,10 +3016,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz", - "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2816,7 +3037,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -2837,7 +3058,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "dependencies": { "wrappy": "1" } @@ -2964,7 +3184,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -3025,10 +3244,14 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/picocolors": { "version": "1.0.0", @@ -3216,7 +3439,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -3235,12 +3457,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3262,35 +3484,50 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/raw-body/node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -3425,11 +3662,58 @@ "rimraf": "bin.js" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -3449,7 +3733,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "license": "MIT" }, "node_modules/schema-utils": { "version": "4.2.0", @@ -3556,49 +3840,76 @@ "dev": true }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.8.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/send/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "license": "MIT" }, "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3658,25 +3969,25 @@ "dev": true }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -3721,14 +4032,72 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4034,7 +4403,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } @@ -4046,13 +4415,35 @@ "dev": true }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -4062,7 +4453,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4121,8 +4512,9 @@ "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -4131,7 +4523,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true, "engines": { "node": ">= 0.8" } @@ -4341,6 +4732,177 @@ } } }, + "node_modules/webpack-dev-server/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/webpack-dev-server/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-server/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -4350,6 +4912,72 @@ "node": ">= 10" } }, + "node_modules/webpack-dev-server/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/webpack-dev-server/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4365,6 +4993,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/webpack-dev-server/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/webpack-dev-server/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/webpack-dev-server/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -4475,8 +5178,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { "version": "8.14.2", diff --git a/package.json b/package.json index fd067d3..77eb3ed 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,9 @@ "webpack": "^5.0.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" + }, + "dependencies": { + "dotenv": "^17.2.3", + "express": "^5.1.0" } } diff --git a/src/host/aiChatServer.js b/src/host/aiChatServer.js new file mode 100644 index 0000000..a4887b3 --- /dev/null +++ b/src/host/aiChatServer.js @@ -0,0 +1,315 @@ +// AI Chat API Server +// This server handles AI chat requests and code modifications for C64 BASIC programs + +const express = require('express'); +const path = require('path'); +const fs = require('fs').promises; +require('dotenv').config(); + +const app = express(); +app.use(express.json({ limit: '50mb' })); // Increase limit for RAM dumps + +// OpenAI API configuration +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions'; + +// Read the entire codebase for context +async function getCodebaseContext() { + const projectRoot = path.resolve(__dirname, '..'); + const context = []; + + async function readDirectory(dir, basePath = '') { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = path.join(basePath, entry.name); + + // Skip node_modules, dist, etc. + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') { + continue; + } + + if (entry.isDirectory()) { + await readDirectory(fullPath, relativePath); + } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.md') || entry.name === 'package.json')) { + try { + const content = await fs.readFile(fullPath, 'utf-8'); + context.push({ + path: relativePath, + content: content, + }); + } catch (err) { + console.error(`Error reading ${fullPath}:`, err.message); + } + } + } + } catch (err) { + console.error(`Error reading directory ${dir}:`, err.message); + } + } + + await readDirectory(projectRoot); + return context; +} + +// Get a summary of the codebase instead of full content +async function getCodebaseSummary() { + const projectRoot = path.resolve(__dirname, '..'); + const summary = { + files: [], + structure: {}, + }; + + async function readDirectory(dir, basePath = '') { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = path.join(basePath, entry.name); + + // Skip node_modules, dist, etc. + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') { + continue; + } + + if (entry.isDirectory()) { + await readDirectory(fullPath, relativePath); + } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.md'))) { + try { + const content = await fs.readFile(fullPath, 'utf-8'); + const lines = content.split('\n'); + + // Extract just the first comment block or first 10 lines as summary + const summary_lines = lines.slice(0, Math.min(20, lines.length)); + + summary.files.push({ + path: relativePath, + lines: lines.length, + summary: summary_lines.join('\n'), + }); + } catch (err) { + console.error(`Error reading ${fullPath}:`, err.message); + } + } + } + } catch (err) { + console.error(`Error reading directory ${dir}:`, err.message); + } + } + + await readDirectory(projectRoot); + return summary; +} + +// AI Chat endpoint +app.post('/api/ai-chat', async (req, res) => { + const { message, history, basicProgram } = req.body; + + if (!OPENAI_API_KEY) { + return res.status(500).json({ + error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in .env file' + }); + } + + try { + // Build system message with C64 BASIC context + const systemMessage = `You are an AI assistant helping to write and modify Commodore 64 BASIC programs. + +${basicProgram ? `Current BASIC program in the C64:\n\`\`\`basic\n${basicProgram}\n\`\`\`` : 'No BASIC program is currently loaded in the C64.'} + +When the user asks you to modify or create a BASIC program: +1. Analyze the current program (if any) +2. Understand what the user wants to change or add +3. Provide a brief explanation of what you're doing +4. IMMEDIATELY provide the complete program in the JSON format below + +IMPORTANT: Always provide the program code directly. Do NOT ask the user for confirmation or additional input. The user will confirm via a UI button. + +To propose changes to the BASIC program, you MUST include a JSON block in your response: +\`\`\`json +{ + "codeChange": { + "type": "basic", + "description": "Brief description of the change", + "newProgram": "10 PRINT\\"HELLO WORLD\\"\\n20 GOTO 10" + } +} +\`\`\` + +The newProgram should be the complete BASIC program, with each line separated by \\n. +Line numbers should be included. +Use \\" for quotes inside strings. + +CRITICAL: Each BASIC statement must have its own line number. Do NOT use colons (:) to put multiple statements on one line. +For example: +GOOD: +10 PRINT "HELLO" +20 GOTO 10 + +BAD: +10 PRINT "HELLO" : GOTO 10 + +You can help with: +- Writing new BASIC programs +- Debugging existing programs +- Adding features or fixing bugs +- Explaining how C64 BASIC works +- Converting ideas into BASIC code + +C64 BASIC notes: +- Screen is 40x25 characters +- Colors: 0-15 (0=black, 1=white, 2=red, 3=cyan, etc.) +- Use POKE 53280,X for border color +- Use POKE 53281,X for background color +- Use PRINT CHR$(147) to clear screen +- Variables are limited (26 simple variables A-Z, arrays with DIM) + +Remember: Provide the code immediately, don't ask for confirmation!`; + + // Build messages array + const messages = [ + { role: 'system', content: systemMessage }, + ...history.slice(-10), // Keep last 10 messages for context + { role: 'user', content: message }, + ]; + + // Call OpenAI API + const response = await fetch(OPENAI_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-4', + messages: messages, + temperature: 0.7, + max_tokens: 2000, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.message || `OpenAI API error: ${response.status}`); + } + + const data = await response.json(); + const aiResponse = data.choices[0].message.content; + + // Check if AI wants to make a code change + let codeChange = null; + const jsonMatch = aiResponse.match(/```json\n([\s\S]*?)\n```/); + if (jsonMatch) { + try { + const parsed = JSON.parse(jsonMatch[1]); + if (parsed.codeChange) { + codeChange = parsed.codeChange; + } + } catch (err) { + console.error('Error parsing code change JSON:', err); + } + } + + res.json({ + response: aiResponse, + codeChange: codeChange, + }); + + } catch (error) { + console.error('AI Chat error:', error); + res.status(500).json({ + error: error.message || 'Failed to communicate with OpenAI' + }); + } +}); + +// Get specific file content endpoint +app.post('/api/get-file-content', async (req, res) => { + const { filePath } = req.body; + + if (!filePath) { + return res.status(400).json({ + error: 'Missing required field: filePath' + }); + } + + try { + const fullPath = path.resolve(__dirname, '..', filePath); + + // Security check: ensure the path is within the project + const projectRoot = path.resolve(__dirname, '..'); + if (!fullPath.startsWith(projectRoot)) { + return res.status(403).json({ + error: 'Access denied: path outside project directory' + }); + } + + const content = await fs.readFile(fullPath, 'utf-8'); + res.json({ + success: true, + content: content, + filePath: filePath, + }); + + } catch (error) { + console.error('Get file content error:', error); + res.status(500).json({ + error: error.message || 'Failed to read file' + }); + } +}); + +// Apply code change endpoint +app.post('/api/apply-code-change', async (req, res) => { + const { filePath, oldCode, newCode } = req.body; + + if (!filePath || !newCode) { + return res.status(400).json({ + error: 'Missing required fields: filePath and newCode' + }); + } + + try { + const fullPath = path.resolve(__dirname, '..', filePath); + + // Read current file content + let currentContent; + try { + currentContent = await fs.readFile(fullPath, 'utf-8'); + } catch (err) { + // File doesn't exist, create new file + if (err.code === 'ENOENT') { + await fs.writeFile(fullPath, newCode, 'utf-8'); + return res.json({ success: true, message: 'New file created' }); + } + throw err; + } + + // If oldCode is provided, replace it + if (oldCode) { + if (!currentContent.includes(oldCode)) { + return res.status(400).json({ + error: 'Old code not found in file. The file may have been modified.' + }); + } + const updatedContent = currentContent.replace(oldCode, newCode); + await fs.writeFile(fullPath, updatedContent, 'utf-8'); + } else { + // Otherwise, append or overwrite + await fs.writeFile(fullPath, newCode, 'utf-8'); + } + + res.json({ success: true, message: 'Code change applied successfully' }); + + } catch (error) { + console.error('Apply code change error:', error); + res.status(500).json({ + error: error.message || 'Failed to apply code change' + }); + } +}); + +module.exports = app; diff --git a/src/host/keyboard-KeyboardEvent.js b/src/host/keyboard-KeyboardEvent.js index ed7333d..07d2981 100644 --- a/src/host/keyboard-KeyboardEvent.js +++ b/src/host/keyboard-KeyboardEvent.js @@ -110,6 +110,12 @@ function buttonNamesToKeyMatrix(buttonNames) { function onKeyDown(event) { + // Ignore keyboard events when typing in input fields or textareas + const target = event.target; + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { + return; + } + // Any keypress with the Meta key (cmd/ctrl/...) down isn't for us. if (event.metaKey) return; @@ -152,6 +158,12 @@ function onKeyDown(event) { } function onKeyUp(event) { + // Ignore keyboard events when typing in input fields or textareas + const target = event.target; + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { + return; + } + const buttonNames = getEventToButtonNames()(event); if (!buttonNames) return; diff --git a/src/host/webFrontEnd/aiChatDialog.css b/src/host/webFrontEnd/aiChatDialog.css new file mode 100644 index 0000000..6695df7 --- /dev/null +++ b/src/host/webFrontEnd/aiChatDialog.css @@ -0,0 +1,228 @@ +#aiChatDialog { + max-width: 800px; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +#aiChatDialog h1 { + margin-top: 0; + margin-bottom: 20px; + font-size: 24px; +} + +.aiChat-container { + display: flex; + flex-direction: column; + height: 100%; + flex-grow: 1; + min-height: 0; +} + +.aiChat-messages { + flex-grow: 1; + overflow-y: auto; + border: 1px solid #666; + background-color: #2a2a2a; + border-radius: 8px; + padding: 15px; + margin-bottom: 15px; + max-height: 400px; + min-height: 200px; +} + +.aiChat-message { + margin-bottom: 15px; + padding: 12px; + border-radius: 6px; + line-height: 1.6; + width: 100%; + box-sizing: border-box; +} + +.aiChat-message.user { + background-color: #4a6fa5; + text-align: right; +} + +.aiChat-message.assistant { + background-color: #3a3a3a; + text-align: left; +} + +.aiChat-message.system { + background-color: #5a5a2a; + font-style: italic; + text-align: center; +} + +.aiChat-message-role { + font-weight: bold; + margin-bottom: 5px; + font-size: 12px; + opacity: 0.9; + color: #ddd; +} + +.aiChat-message-content { + white-space: pre-wrap; + word-wrap: break-word; + color: #eee; +} + +.aiChat-codeBlock { + background-color: #1a1a1a; + border: 1px solid #555; + border-radius: 4px; + padding: 10px; + margin: 10px 0; + overflow-x: auto; + font-family: monospace; + font-size: 12px; + line-height: 1.4; + color: #0f0; +} + +.aiChat-inputArea { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 15px; +} + +.aiChat-buttons { + display: flex; + gap: 10px; +} + +.aiChat-input { + flex-grow: 1; + padding: 10px; + background-color: #3a3a3a; + color: #eee; + border: 1px solid #666; + border-radius: 6px; + font-family: inherit; + font-size: 14px; + resize: vertical; + min-height: 60px; +} + +.aiChat-input:focus { + outline: none; + border-color: #7ab4e8; + background-color: #404040; +} + +.aiChat-exportButton { + padding: 10px 20px; + background-color: #5a8f5a; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; +} + +.aiChat-exportButton:hover { + background-color: #6aa56a; +} + +.aiChat-importButton { + padding: 10px 20px; + background-color: #8f7a5a; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; +} + +.aiChat-importButton:hover { + background-color: #a58a6a; +} + +.aiChat-sendButton { + padding: 10px 20px; + background-color: #5a9fd4; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + flex-grow: 1; +} + +.aiChat-sendButton:hover { + background-color: #4a8fc4; +} + +.aiChat-sendButton:disabled { + background-color: #444; + cursor: not-allowed; + opacity: 0.5; +} + +.aiChat-confirmationArea { + background-color: #4a3a2a; + border: 1px solid #cc8a3a; + border-radius: 6px; + padding: 15px; + margin-bottom: 15px; +} + +.aiChat-confirmationArea h3 { + margin-top: 0; + margin-bottom: 10px; + color: #ffd; +} + +.aiChat-confirmationButtons { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.aiChat-confirmButton, +.aiChat-rejectButton { + padding: 8px 16px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + font-size: 14px; +} + +.aiChat-confirmButton { + background-color: #5aaa5a; + color: white; +} + +.aiChat-confirmButton:hover { + background-color: #4a9a4a; +} + +.aiChat-rejectButton { + background-color: #ca5a5a; + color: white; +} + +.aiChat-rejectButton:hover { + background-color: #ba4a4a; +} + +.aiChat-loading { + text-align: center; + padding: 10px; + opacity: 0.8; + color: #bbb; +} + +.aiChat-error { + background-color: #6a3a3a; + border: 1px solid #ca6a6a; + border-radius: 6px; + padding: 10px; + margin-bottom: 15px; + color: #fcc; +} diff --git a/src/host/webFrontEnd/aiChatDialog.js b/src/host/webFrontEnd/aiChatDialog.js new file mode 100644 index 0000000..73a4425 --- /dev/null +++ b/src/host/webFrontEnd/aiChatDialog.js @@ -0,0 +1,467 @@ +import { Dialog, closeAllDialogs } from "./dialogs"; +import css from "./aiChatDialog.css"; +import { extractBasicProgram, basicLinesToText } from "../../tools/basicExtractor"; +import { writeBasicProgramToRam } from "../../tools/basicTokenizer"; + +let c64; +let dialog; +let messagesContainer; +let inputTextarea; +let sendButton; +let confirmationArea; +let errorContainer; + +const conversationHistory = []; +let pendingCodeChange = null; +let isWaitingForResponse = false; + +// Helper function to fetch file content +async function fetchFileContent(filePath) { + try { + const response = await fetch("/api/get-file-content", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ filePath }), + }); + + const data = await response.json(); + + if (data.success) { + return data.content; + } else { + throw new Error(data.error || "Failed to fetch file content"); + } + } catch (error) { + console.error("Error fetching file content:", error); + return null; + } +} + +export function initAiChatDialog(nascentC64) { + c64 = nascentC64; + dialog = new Dialog("aiChatDialog"); + + messagesContainer = document.getElementById("aiChat-messages"); + inputTextarea = document.getElementById("aiChat-input"); + sendButton = document.getElementById("aiChat-sendButton"); + confirmationArea = document.getElementById("aiChat-confirmationArea"); + errorContainer = document.getElementById("aiChat-error"); + + sendButton.addEventListener("click", handleSendMessage); + inputTextarea.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }); + + document.getElementById("aiChat-confirmButton")?.addEventListener("click", handleConfirmChange); + document.getElementById("aiChat-rejectButton")?.addEventListener("click", handleRejectChange); + document.getElementById("aiChat-exportButton")?.addEventListener("click", handleExportCode); + document.getElementById("aiChat-importButton")?.addEventListener("click", handleImportCode); + + // Handle file input change + const fileInput = document.getElementById("aiChat-importInput"); + if (fileInput) { + fileInput.addEventListener("change", handleFileSelected); + } + + // Make dialog draggable + makeDraggable(dialog.el); +} + +export function openAiChatDialog() { + // Reset position before opening + if (dialog.el) { + dialog.el.style.transition = ""; + dialog.el.style.transform = ""; + } + dialog.open(); + inputTextarea.focus(); +} + +function addMessage(role, content) { + conversationHistory.push({ role, content }); + + const messageEl = document.createElement("div"); + messageEl.className = `aiChat-message ${role}`; + + const roleEl = document.createElement("div"); + roleEl.className = "aiChat-message-role"; + roleEl.textContent = role === "user" ? "You" : role === "assistant" ? "AI" : "System"; + + const contentEl = document.createElement("div"); + contentEl.className = "aiChat-message-content"; + + // Simple markdown-like formatting for code blocks + const parts = content.split(/```(\w*)\n([\s\S]*?)```/g); + for (let i = 0; i < parts.length; i++) { + if (i % 3 === 0) { + // Regular text + if (parts[i].trim()) { + const textNode = document.createTextNode(parts[i]); + contentEl.appendChild(textNode); + } + } else if (i % 3 === 2) { + // Code block + const codeBlock = document.createElement("div"); + codeBlock.className = "aiChat-codeBlock"; + codeBlock.textContent = parts[i]; + contentEl.appendChild(codeBlock); + } + } + + messageEl.appendChild(roleEl); + messageEl.appendChild(contentEl); + messagesContainer.appendChild(messageEl); + messagesContainer.scrollTop = messagesContainer.scrollHeight; +} + +function showError(message) { + errorContainer.textContent = message; + errorContainer.style.display = "block"; + setTimeout(() => { + errorContainer.style.display = "none"; + }, 5000); +} + +async function handleSendMessage() { + if (isWaitingForResponse) return; + + const message = inputTextarea.value.trim(); + if (!message) return; + + inputTextarea.value = ""; + addMessage("user", message); + + isWaitingForResponse = true; + sendButton.disabled = true; + + // Extract current BASIC program from C64 RAM + let basicProgram = ""; + try { + const lines = extractBasicProgram(c64); + basicProgram = basicLinesToText(lines); + } catch (error) { + console.error("Error extracting BASIC program:", error); + basicProgram = ""; + } + + // Show loading indicator + const loadingEl = document.createElement("div"); + loadingEl.className = "aiChat-loading"; + loadingEl.textContent = "AI is thinking..."; + messagesContainer.appendChild(loadingEl); + + try { + const response = await fetch("/api/ai-chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message, + history: conversationHistory, + basicProgram: basicProgram, + }), + }); + + // Safely remove loading indicator + if (loadingEl.parentNode === messagesContainer) { + messagesContainer.removeChild(loadingEl); + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.error) { + showError(data.error); + addMessage("system", `Error: ${data.error}`); + } else { + addMessage("assistant", data.response); + + // Check if AI wants to modify code + if (data.codeChange) { + pendingCodeChange = data.codeChange; + showConfirmationDialog(data.codeChange); + } + } + } catch (error) { + // Safely remove loading indicator + if (loadingEl.parentNode === messagesContainer) { + messagesContainer.removeChild(loadingEl); + } + showError(`Failed to communicate with AI: ${error.message}`); + addMessage("system", `Error: ${error.message}`); + } finally { + isWaitingForResponse = false; + sendButton.disabled = false; + inputTextarea.focus(); + } +} + +function showConfirmationDialog(codeChange) { + const { type, description, newProgram } = codeChange; + + const descEl = document.getElementById("aiChat-changeDescription"); + descEl.textContent = description || "Modify BASIC program"; + + const detailsEl = document.getElementById("aiChat-changeDetails"); + + if (type === "basic") { + detailsEl.innerHTML = ` +
New BASIC Program:
+
${escapeHtml(newProgram)}
+ `; + } else { + // Legacy file system change (shouldn't happen now) + const { filePath, oldCode, newCode } = codeChange; + detailsEl.innerHTML = ` +
File: ${filePath}
+
Changes:
+ ${oldCode ? `
${escapeHtml(oldCode)}
` : ''} +
↓
+
${escapeHtml(newCode)}
+ `; + } + + confirmationArea.style.display = "block"; +} + +function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +async function handleConfirmChange() { + if (!pendingCodeChange) return; + + confirmationArea.style.display = "none"; + + if (pendingCodeChange.type === "basic") { + // Apply BASIC program change directly to C64 RAM + try { + const { newProgram } = pendingCodeChange; + + // Write the program directly to RAM using tokenizer + writeBasicProgramToRam(c64, newProgram); + + addMessage("system", "BASIC program loaded into memory - type LIST to see it"); + } catch (error) { + showError(`Failed to apply BASIC program: ${error.message}`); + addMessage("system", `Error applying program: ${error.message}`); + } + } else { + // Legacy file system change (shouldn't be used anymore) + try { + const response = await fetch("/api/apply-code-change", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(pendingCodeChange), + }); + + const data = await response.json(); + + if (data.success) { + addMessage("system", `Code change applied successfully`); + } else { + showError(data.error || "Failed to apply code change"); + addMessage("system", `Failed to apply change: ${data.error}`); + } + } catch (error) { + showError(`Failed to apply code change: ${error.message}`); + addMessage("system", `Error applying change: ${error.message}`); + } + } + + pendingCodeChange = null; +} + +function handleRejectChange() { + confirmationArea.style.display = "none"; + addMessage("system", "Code change rejected by user"); + pendingCodeChange = null; +} + +function handleExportCode() { + try { + // Extract current BASIC program from C64 RAM + const lines = extractBasicProgram(c64); + const basicProgram = basicLinesToText(lines); + + if (!basicProgram || basicProgram.trim() === "") { + showError("No BASIC program to export"); + return; + } + + // Create a blob with the BASIC program + const blob = new Blob([basicProgram], { type: "text/plain;charset=utf-8" }); + + // Create a temporary download link + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + + // Generate filename with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5); + link.download = `c64-basic-${timestamp}.txt`; + + // Trigger download + document.body.appendChild(link); + link.click(); + + // Cleanup + document.body.removeChild(link); + URL.revokeObjectURL(url); + + addMessage("system", `BASIC program exported to ${link.download}`); + } catch (error) { + console.error("Error exporting BASIC program:", error); + showError(`Failed to export code: ${error.message}`); + } +} + +function handleImportCode() { + const fileInput = document.getElementById("aiChat-importInput"); + if (fileInput) { + fileInput.click(); + } +} + +function handleFileSelected(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + + reader.onload = function(e) { + try { + const content = e.target.result; + + // Validate that it looks like BASIC code + if (!content || content.trim() === "") { + showError("File is empty"); + return; + } + + // Write the program to C64 RAM + writeBasicProgramToRam(c64, content); + + addMessage("system", `Imported BASIC program from ${file.name} - type LIST to see it`); + + // Close the dialog after successful import + dialog.close(); + + // Clear the file input so the same file can be imported again + event.target.value = ""; + } catch (error) { + console.error("Error importing BASIC program:", error); + showError(`Failed to import code: ${error.message}`); + } + }; + + reader.onerror = function() { + showError("Failed to read file"); + }; + + reader.readAsText(file); +} + +// Make dialog draggable by clicking and dragging on the title area +function makeDraggable(dialogElement) { + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + // Get the title element (h1) to use as drag handle + const dragHandle = dialogElement.querySelector("h1"); + if (!dragHandle) return; + + // Add visual cursor feedback + dragHandle.style.cursor = "move"; + dragHandle.style.userSelect = "none"; + + dragHandle.addEventListener("mousedown", dragStart); + document.addEventListener("mousemove", drag); + document.addEventListener("mouseup", dragEnd); + + // Reset position when dialog is closed + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === "attributes" && mutation.attributeName === "class") { + if (dialogElement.classList.contains("undisplayed")) { + // Reset offsets when dialog is closed + xOffset = 0; + yOffset = 0; + currentX = 0; + currentY = 0; + initialX = 0; + initialY = 0; + } + } + }); + }); + + observer.observe(dialogElement, { attributes: true }); + + function dragStart(e) { + // Only drag on left click + if (e.button !== 0) return; + + // Don't drag if clicking on close button + if (e.target.classList.contains("close")) return; + + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + + isDragging = true; + dialogElement.style.transition = "none"; + } + + function drag(e) { + if (!isDragging) return; + + e.preventDefault(); + + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + + xOffset = currentX; + yOffset = currentY; + + setTranslate(currentX, currentY, dialogElement); + } + + function dragEnd(e) { + if (!isDragging) return; + + initialX = currentX; + initialY = currentY; + + isDragging = false; + + // Restore transitions after drag + setTimeout(() => { + if (!isDragging) { + dialogElement.style.transition = ""; + } + }, 50); + } + + function setTranslate(xPos, yPos, el) { + el.style.transform = `translate(${xPos}px, ${yPos}px)`; + } +} + diff --git a/src/host/webFrontEnd/basicFileWatcher.js b/src/host/webFrontEnd/basicFileWatcher.js new file mode 100644 index 0000000..daa1ab0 --- /dev/null +++ b/src/host/webFrontEnd/basicFileWatcher.js @@ -0,0 +1,143 @@ +/* + BASIC File Watcher + Monitors a file called "current.basic" and auto-loads it when modified +*/ + +import { writeBasicProgramToRam, autoRunBasicProgram } from "../../tools/basicTokenizer"; + +let c64; +let isWatching = false; +let lastModified = null; +let checkInterval = null; + +const WATCH_INTERVAL = 1000; // Check every second +const FILE_PATH = '/current.basic'; // Relative to the web root + +export function initBasicFileWatcher(nascentC64) { + c64 = nascentC64; + console.log('BASIC File Watcher initialized'); +} + +export function startWatching() { + if (isWatching) return; + + isWatching = true; + console.log('Starting to watch for current.basic changes...'); + + // Try to load the file immediately on start + setTimeout(() => { + loadBasicFile().catch(err => { + console.log('No current.basic file found on startup (this is ok)'); + }); + }, 1000); // Wait 1 second for C64 to be fully initialized + + checkInterval = setInterval(async () => { + try { + const response = await fetch(FILE_PATH, { + method: 'HEAD', + cache: 'no-cache' + }); + + if (response.ok) { + const modified = response.headers.get('Last-Modified'); + + if (lastModified && modified !== lastModified) { + console.log('Detected change in current.basic, reloading...'); + await loadBasicFile(); + } + + lastModified = modified; + } + } catch (error) { + // File doesn't exist or network error - that's ok + if (lastModified !== null) { + console.log('current.basic no longer accessible'); + lastModified = null; + } + } + }, WATCH_INTERVAL); +} + +export function stopWatching() { + if (!isWatching) return; + + isWatching = false; + if (checkInterval) { + clearInterval(checkInterval); + checkInterval = null; + } + console.log('Stopped watching current.basic'); +} + +async function loadBasicFile() { + try { + const response = await fetch(FILE_PATH, { + cache: 'no-cache', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + + if (!response.ok) { + console.warn('Failed to load current.basic:', response.status); + return; + } + + const content = await response.text(); + + if (!content || content.trim() === "") { + console.warn('current.basic is empty'); + return; + } + + console.log('Loading BASIC program:', content.substring(0, 100) + '...'); + + // Write the program to C64 RAM + writeBasicProgramToRam(c64, content); + + // Auto-run the program + setTimeout(() => { + autoRunBasicProgram(c64); + }, 100); // Small delay to ensure the program is fully loaded + + console.log('✓ Auto-loaded BASIC program from current.basic'); + console.log('Program will auto-run in a moment...'); + + // Show notification to user + showNotification('Program reloaded and running...'); + } catch (error) { + console.error('Error loading current.basic:', error); + showNotification('Error loading program: ' + error.message); + } +} + +function showNotification(message) { + // Create a temporary notification element + const notification = document.createElement('div'); + notification.style.position = 'fixed'; + notification.style.bottom = '20px'; + notification.style.right = '20px'; + notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + notification.style.color = '#0f0'; + notification.style.padding = '10px 20px'; + notification.style.borderRadius = '5px'; + notification.style.zIndex = '10000'; + notification.style.fontFamily = 'monospace'; + notification.style.fontSize = '14px'; + notification.textContent = message; + + document.body.appendChild(notification); + + // Remove after 3 seconds + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transition = 'opacity 0.5s'; + setTimeout(() => { + if (notification.parentNode) { + document.body.removeChild(notification); + } + }, 500); + }, 3000); +} diff --git a/src/host/webFrontEnd/dialogs.css b/src/host/webFrontEnd/dialogs.css index 9808446..6be2fe6 100644 --- a/src/host/webFrontEnd/dialogs.css +++ b/src/host/webFrontEnd/dialogs.css @@ -18,6 +18,8 @@ box-shadow: 0 0 50px #0004; transition: opacity 0.25s, top 0.25s; + + z-index: 100; } .dialog.undisplayed { @@ -62,6 +64,8 @@ bottom: 0; left: 0; right: 0; + z-index: 50; + background-color: rgba(0, 0, 0, 0.5); } .dialog .tip { diff --git a/src/host/webFrontEnd/images/svg_icon-ai-chat.svg b/src/host/webFrontEnd/images/svg_icon-ai-chat.svg new file mode 100644 index 0000000..737608f --- /dev/null +++ b/src/host/webFrontEnd/images/svg_icon-ai-chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/host/webFrontEnd/index.js b/src/host/webFrontEnd/index.js index c65a893..ca7f739 100644 --- a/src/host/webFrontEnd/index.js +++ b/src/host/webFrontEnd/index.js @@ -14,6 +14,8 @@ import { initJoystickDialog } from "./joystickDialog"; import { initKeyMapDialog } from "./keyMapDialog"; import { initLoaderDialog } from "./loaderDialog"; import { initDiskDialog } from "./diskDialog"; +import { initAiChatDialog } from "./aiChatDialog"; +import { initBasicFileWatcher, startWatching } from "./basicFileWatcher"; // A development aid. Don't commit with this turned on. const pauseOnMenus = false; @@ -56,7 +58,12 @@ export function attach(nascentC64) { initKeyMapDialog(c64); initLoaderDialog(c64); initDiskDialog(c64); + initAiChatDialog(c64); initScopes(c64); + initBasicFileWatcher(c64); + + // Start watching for current.basic file changes + startWatching(); c64.hooks.reportError = showErrorDialog; c64.hooks.setTitle = setTitle; diff --git a/src/host/webFrontEnd/lowerTray.js b/src/host/webFrontEnd/lowerTray.js index 8227f80..f2af421 100644 --- a/src/host/webFrontEnd/lowerTray.js +++ b/src/host/webFrontEnd/lowerTray.js @@ -1,6 +1,7 @@ import { Dialog } from "./dialogs"; import { toggleScopes } from "./scopes"; import { takeSnapshot } from "./snapshot"; +import { openAiChatDialog } from "./aiChatDialog"; let c64; @@ -22,6 +23,12 @@ export function initLowerTray(nascentC64) { ); } + handlerForEventForId( + "aiChatButton", + "click", + () => openAiChatDialog() + ); + handlerForEventForId( "pauseButton", "click", diff --git a/src/host/webFrontEnd/template.ejs b/src/host/webFrontEnd/template.ejs index ddb2056..371fca1 100644 --- a/src/host/webFrontEnd/template.ejs +++ b/src/host/webFrontEnd/template.ejs @@ -120,7 +120,7 @@
- +
@@ -422,6 +422,36 @@

+
+ +

AI Assistant

+
+ + + + +
+ +
+ +
+ + + + +
+
+
+
+
diff --git a/src/tools/basicExtractor.js b/src/tools/basicExtractor.js new file mode 100644 index 0000000..cc2181e --- /dev/null +++ b/src/tools/basicExtractor.js @@ -0,0 +1,141 @@ +/* + BASIC program extractor and detokenizer + Extracts BASIC programs from C64 RAM and converts them to readable text +*/ + +// BASIC tokens (from $80 to $CB) +const tokens = { + 0x80: "END", 0x81: "FOR", 0x82: "NEXT", 0x83: "DATA", 0x84: "INPUT#", + 0x85: "INPUT", 0x86: "DIM", 0x87: "READ", 0x88: "LET", 0x89: "GOTO", + 0x8A: "RUN", 0x8B: "IF", 0x8C: "RESTORE", 0x8D: "GOSUB", 0x8E: "RETURN", + 0x8F: "REM", 0x90: "STOP", 0x91: "ON", 0x92: "WAIT", 0x93: "LOAD", + 0x94: "SAVE", 0x95: "VERIFY", 0x96: "DEF", 0x97: "POKE", 0x98: "PRINT#", + 0x99: "PRINT", 0x9A: "CONT", 0x9B: "LIST", 0x9C: "CLR", 0x9D: "CMD", + 0x9E: "SYS", 0x9F: "OPEN", 0xA0: "CLOSE", 0xA1: "GET", 0xA2: "NEW", + 0xA3: "TAB(", 0xA4: "TO", 0xA5: "FN", 0xA6: "SPC(", 0xA7: "THEN", + 0xA8: "NOT", 0xA9: "STEP", 0xAA: "+", 0xAB: "-", 0xAC: "*", 0xAD: "/", + 0xAE: "^", 0xAF: "AND", 0xB0: "OR", 0xB1: ">", 0xB2: "=", 0xB3: "<", + 0xB4: "SGN", 0xB5: "INT", 0xB6: "ABS", 0xB7: "USR", 0xB8: "FRE", + 0xB9: "POS", 0xBA: "SQR", 0xBB: "RND", 0xBC: "LOG", 0xBD: "EXP", + 0xBE: "COS", 0xBF: "SIN", 0xC0: "TAN", 0xC1: "ATN", 0xC2: "PEEK", + 0xC3: "LEN", 0xC4: "STR$", 0xC5: "VAL", 0xC6: "ASC", 0xC7: "CHR$", + 0xC8: "LEFT$", 0xC9: "RIGHT$", 0xCA: "MID$", 0xCB: "GO", + 0xFF: "π", // PI character +}; + +// Extract BASIC program from C64 RAM +export function extractBasicProgram(c64) { + const ram = c64.ram; + const lines = []; + + // BASIC programs start at $0801 (2049) + let addr = 0x0801; + + while (true) { + // Read pointer to next line (2 bytes, little-endian) + const nextLinePtr = ram.readRam(addr) | (ram.readRam(addr + 1) << 8); + + // If pointer is 0, we've reached the end of the program + if (nextLinePtr === 0) { + break; + } + + // Read line number (2 bytes, little-endian) + const lineNum = ram.readRam(addr + 2) | (ram.readRam(addr + 3) << 8); + + // Start reading the line content + let lineAddr = addr + 4; + let lineText = ""; + + while (true) { + const byte = ram.readRam(lineAddr); + + // End of line marker + if (byte === 0) { + break; + } + + // Check if it's a token + if (byte >= 0x80 && byte <= 0xCB) { + const token = tokens[byte]; + if (token) { + lineText += token; + + // Add space after token unless it's a function or operator + if (![0xA3, 0xA6, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3].includes(byte)) { + lineText += " "; + } + } + } else if (byte === 0xFF) { + lineText += "π"; + } else { + // Regular ASCII character + lineText += String.fromCharCode(byte); + } + + lineAddr++; + } + + lines.push({ + number: lineNum, + text: lineText.trim(), + }); + + // Move to next line + addr = nextLinePtr; + } + + return lines; +} + +// Convert BASIC lines array to text format +export function basicLinesToText(lines) { + if (lines.length === 0) { + return "READY.\n"; + } + + return lines.map(line => `${line.number} ${line.text}`).join('\n') + '\n\nREADY.\n'; +} + +// Tokenize BASIC text back into bytes +// This is a simplified tokenizer - for full implementation would need more work +export function tokenizeBasicLine(lineNum, text) { + const bytes = []; + + // This will be filled in later - for now just convert to PETSCII + let i = 0; + while (i < text.length) { + let found = false; + + // Check for multi-character tokens + for (const [tokenByte, tokenText] of Object.entries(tokens)) { + const byte = parseInt(tokenByte); + if (byte < 0x80) continue; + + if (text.substr(i, tokenText.length).toUpperCase() === tokenText.toUpperCase()) { + bytes.push(byte); + i += tokenText.length; + found = true; + + // Skip following space if present + if (text[i] === ' ') i++; + break; + } + } + + if (!found) { + // Regular character + bytes.push(text.charCodeAt(i)); + i++; + } + } + + // Add null terminator + bytes.push(0); + + // Build complete line: [next_ptr_lo, next_ptr_hi, line_num_lo, line_num_hi, ...bytes] + return { + lineNum, + bytes, + }; +} diff --git a/src/tools/basicTokenizer.js b/src/tools/basicTokenizer.js new file mode 100644 index 0000000..aced809 --- /dev/null +++ b/src/tools/basicTokenizer.js @@ -0,0 +1,277 @@ +/* + BASIC program tokenizer + Converts BASIC text to tokenized binary format for C64 +*/ + +// BASIC tokens (from $80 to $CB) +const TOKEN_MAP = { + "END": 0x80, "FOR": 0x81, "NEXT": 0x82, "DATA": 0x83, "INPUT#": 0x84, + "INPUT": 0x85, "DIM": 0x86, "READ": 0x87, "LET": 0x88, "GOTO": 0x89, + "RUN": 0x8A, "IF": 0x8B, "RESTORE": 0x8C, "GOSUB": 0x8D, "RETURN": 0x8E, + "REM": 0x8F, "STOP": 0x90, "ON": 0x91, "WAIT": 0x92, "LOAD": 0x93, + "SAVE": 0x94, "VERIFY": 0x95, "DEF": 0x96, "POKE": 0x97, "PRINT#": 0x98, + "PRINT": 0x99, "CONT": 0x9A, "LIST": 0x9B, "CLR": 0x9C, "CMD": 0x9D, + "SYS": 0x9E, "OPEN": 0x9F, "CLOSE": 0xA0, "GET": 0xA1, "NEW": 0xA2, + "TAB(": 0xA3, "TO": 0xA4, "FN": 0xA5, "SPC(": 0xA6, "THEN": 0xA7, + "NOT": 0xA8, "STEP": 0xA9, "+": 0xAA, "-": 0xAB, "*": 0xAC, "/": 0xAD, + "^": 0xAE, "AND": 0xAF, "OR": 0xB0, ">": 0xB1, "=": 0xB2, "<": 0xB3, + "SGN": 0xB4, "INT": 0xB5, "ABS": 0xB6, "USR": 0xB7, "FRE": 0xB8, + "POS": 0xB9, "SQR": 0xBA, "RND": 0xBB, "LOG": 0xBC, "EXP": 0xBD, + "COS": 0xBE, "SIN": 0xBF, "TAN": 0xC0, "ATN": 0xC1, "PEEK": 0xC2, + "LEN": 0xC3, "STR$": 0xC4, "VAL": 0xC5, "ASC": 0xC6, "CHR$": 0xC7, + "LEFT$": 0xC8, "RIGHT$": 0xC9, "MID$": 0xCA, "GO": 0xCB +}; + +// Operators and symbols that don't need a separator after them +const OPERATORS = new Set(["+", "-", "*", "/", "^", ">", "=", "<"]); + +// Convert character to byte (ASCII to PETSCII conversion) +function charToByte(char) { + const code = char.charCodeAt(0); + + // PETSCII conversion: + // Uppercase A-Z (65-90) stays the same + // Lowercase a-z (97-122) needs to be converted to PETSCII (193-218) + if (code >= 97 && code <= 122) { + return code - 97 + 65; // Convert lowercase to uppercase for PETSCII + } + + // For everything else, keep as-is + return code; +} + +// Tokenize a single line of BASIC +function tokenizeLine(lineText) { + const tokens = []; + let i = 0; + const text = lineText.trim(); + + // Special handling for REM - everything after REM is literal text + const remIndex = text.toUpperCase().indexOf('REM'); + if (remIndex !== -1) { + // Check if REM is actually a keyword (not part of a variable name) + const beforeRem = text.substring(0, remIndex); + const afterRemStart = remIndex + 3; + const charAfterRem = text[afterRemStart]; + + // REM must be preceded by nothing, space, or colon, and followed by space or nothing + const isValidRem = (remIndex === 0 || beforeRem.endsWith(' ') || beforeRem.endsWith(':')) && + (charAfterRem === undefined || charAfterRem === ' ' || charAfterRem === ':'); + + if (isValidRem) { + // Tokenize everything before REM + const beforeRemTokens = tokenizeWithoutRem(beforeRem); + tokens.push(...beforeRemTokens); + + // Add REM token + tokens.push(TOKEN_MAP["REM"]); + + // Add everything after REM as literal characters + const afterRem = text.substring(afterRemStart); + for (let j = 0; j < afterRem.length; j++) { + tokens.push(charToByte(afterRem[j])); + } + + return tokens; + } + } + + // No REM in line, tokenize normally + return tokenizeWithoutRem(text); +} + +function tokenizeWithoutRem(text) { + const tokens = []; + let i = 0; + + while (i < text.length) { + // Check for BASIC keywords + let foundToken = false; + + // Try to match keywords (longest first to handle things like "PRINT#" before "PRINT") + const sortedKeywords = Object.keys(TOKEN_MAP).sort((a, b) => b.length - a.length); + + for (const keyword of sortedKeywords) { + const upperText = text.substring(i).toUpperCase(); + + // Check if keyword matches at current position + if (upperText.startsWith(keyword)) { + // Make sure it's not part of a variable name + // Operators don't need a separator, but keywords do + const nextChar = text[i + keyword.length]; + const needsSeparator = !OPERATORS.has(keyword); + + if (!needsSeparator || + nextChar === undefined || + nextChar === ' ' || + nextChar === '(' || + nextChar === ')' || + nextChar === ',' || + nextChar === ';' || + nextChar === ':' || + nextChar === '"') { + + tokens.push(TOKEN_MAP[keyword]); + i += keyword.length; + foundToken = true; + break; + } + } + } + + if (!foundToken) { + // Not a keyword, treat as literal character + tokens.push(charToByte(text[i])); + i++; + } + } + + return tokens; +} + +// Tokenize complete BASIC program and write to C64 RAM +export function writeBasicProgramToRam(c64, programText) { + console.log('writeBasicProgramToRam called with:', programText); + + const lines = programText.trim().split('\n').filter(line => line.trim()); + const programBytes = []; + + // Parse and tokenize all lines first + const parsedLines = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + // Extract line number + const match = trimmedLine.match(/^(\d+)\s+(.*)$/); + if (!match) { + console.warn('Invalid BASIC line (no line number):', trimmedLine); + continue; + } + + const lineNum = parseInt(match[1], 10); + let lineText = match[2]; + + // Clean up :: at the start (replace with single colon or remove if it's the only content before REM) + if (lineText.startsWith('::')) { + lineText = lineText.substring(2).trim(); + // If there's content after ::, add a colon separator + if (lineText && !lineText.toUpperCase().startsWith('REM')) { + lineText = ': ' + lineText; + } + } + + // Skip empty lines (lines that only had ::) + if (!lineText) { + console.log(`Skipping empty line ${lineNum}`); + continue; + } + + console.log(`Parsing line ${lineNum}: "${lineText}"`); + + // Tokenize the line content + const tokenized = tokenizeLine(lineText); + console.log(`Tokenized to ${tokenized.length} bytes:`, tokenized); + + parsedLines.push({ lineNum, tokenized }); + } + + // Now build the binary format with correct pointers + let currentAddr = 0x0801; + + console.log(`Building ${parsedLines.length} lines starting at $0801`); + + for (let i = 0; i < parsedLines.length; i++) { + const { lineNum, tokenized } = parsedLines[i]; + + // Calculate length of this line: + // 2 bytes for next line pointer + 2 bytes for line number + tokenized content + 1 byte for EOL + const lineLength = 2 + 2 + tokenized.length + 1; + const nextLineAddr = currentAddr + lineLength; + + console.log(`Line ${lineNum}: addr=$${currentAddr.toString(16)}, length=${lineLength}, next=$${nextLineAddr.toString(16)}`); + + // Next line pointer (2 bytes, little-endian) + // Points to the start of the next line (or to the end marker for the last line) + programBytes.push(nextLineAddr & 0xFF, (nextLineAddr >> 8) & 0xFF); + + // Line number (2 bytes, little-endian) + programBytes.push(lineNum & 0xFF, (lineNum >> 8) & 0xFF); + + // Tokenized line content + programBytes.push(...tokenized); + + // End-of-line marker + programBytes.push(0x00); + + currentAddr = nextLineAddr; + } + + // Add final end-of-program marker (2 zero bytes) + programBytes.push(0x00, 0x00); + + // Write program to RAM + const {wires: {cpuRead, cpuWrite}} = c64; + + console.log(`Writing ${programBytes.length} bytes to RAM starting at $0801`); + console.log('First 20 bytes:', programBytes.slice(0, 20)); + + // Save current memory configuration + const dir = cpuRead(0); + const port = cpuRead(1); + + // Set to all-RAM mode + cpuWrite(0, 0b111); + cpuWrite(1, 0); + + // Clear BASIC area first (from $0801 to end of program) + for (let i = 0; i < 10000; i++) { + cpuWrite(0x0801 + i, 0); + } + + // Write the program + for (let i = 0; i < programBytes.length; i++) { + cpuWrite(0x0801 + i, programBytes[i]); + } + + // Update BASIC pointers + const endOfProgram = 0x0801 + programBytes.length; + const lo = endOfProgram & 0xFF; + const hi = (endOfProgram >> 8) & 0xFF; + + cpuWrite(0x2d, lo); // pointer to beginning of variable area + cpuWrite(0x2e, hi); + + cpuWrite(0x2f, lo); // pointer to beginning of array variable area + cpuWrite(0x30, hi); + + cpuWrite(0x31, lo); // pointer to end of array variable area + cpuWrite(0x32, hi); + + // Restore memory configuration + cpuWrite(0, dir); + cpuWrite(1, port); + + console.log(`BASIC program written: ${programBytes.length} bytes, ending at $${endOfProgram.toString(16)}`); +} + +// Auto-run BASIC program by simulating "RUN" command +export function autoRunBasicProgram(c64) { + const {wires: {cpuWrite}} = c64; + + // Write "RUN" + Enter into keyboard buffer + // Keyboard buffer starts at $0277 + // Number of characters in buffer is at $C6 + + // R = 82, U = 85, N = 78, Enter = 13 + cpuWrite(0x0277, 82); // R + cpuWrite(0x0278, 85); // U + cpuWrite(0x0279, 78); // N + cpuWrite(0x027A, 13); // Enter + + // Set buffer length to 4 + cpuWrite(0xC6, 4); + + console.log('Auto-executing RUN command'); +} \ No newline at end of file diff --git a/test-full-tokenizer.js b/test-full-tokenizer.js new file mode 100644 index 0000000..75f13b3 --- /dev/null +++ b/test-full-tokenizer.js @@ -0,0 +1,135 @@ +// Test completo del tokenizer con un programma semplice +// Questo simula cosa succede quando clicchi Apply + +const TOKEN_MAP = { + "END": 0x80, "FOR": 0x81, "NEXT": 0x82, "DATA": 0x83, "INPUT#": 0x84, + "INPUT": 0x85, "DIM": 0x86, "READ": 0x87, "LET": 0x88, "GOTO": 0x89, + "RUN": 0x8A, "IF": 0x8B, "RESTORE": 0x8C, "GOSUB": 0x8D, "RETURN": 0x8E, + "REM": 0x8F, "STOP": 0x90, "ON": 0x91, "WAIT": 0x92, "LOAD": 0x93, + "SAVE": 0x94, "VERIFY": 0x95, "DEF": 0x96, "POKE": 0x97, "PRINT#": 0x98, + "PRINT": 0x99, "CONT": 0x9A, "LIST": 0x9B, "CLR": 0x9C, "CMD": 0x9D, + "SYS": 0x9E, "OPEN": 0x9F, "CLOSE": 0xA0, "GET": 0xA1, "NEW": 0xA2, + "TAB(": 0xA3, "TO": 0xA4, "FN": 0xA5, "SPC(": 0xA6, "THEN": 0xA7, + "NOT": 0xA8, "STEP": 0xA9, "+": 0xAA, "-": 0xAB, "*": 0xAC, "/": 0xAD, + "^": 0xAE, "AND": 0xAF, "OR": 0xB0, ">": 0xB1, "=": 0xB2, "<": 0xB3, + "SGN": 0xB4, "INT": 0xB5, "ABS": 0xB6, "USR": 0xB7, "FRE": 0xB8, + "POS": 0xB9, "SQR": 0xBA, "RND": 0xBB, "LOG": 0xBC, "EXP": 0xBD, + "COS": 0xBE, "SIN": 0xBF, "TAN": 0xC0, "ATN": 0xC1, "PEEK": 0xC2, + "LEN": 0xC3, "STR$": 0xC4, "VAL": 0xC5, "ASC": 0xC6, "CHR$": 0xC7, + "LEFT$": 0xC8, "RIGHT$": 0xC9, "MID$": 0xCA, "GO": 0xCB +}; + +function charToByte(char) { + return char.charCodeAt(0); +} + +function tokenizeLine(lineText) { + const tokens = []; + let i = 0; + const text = lineText.trim(); + + while (i < text.length) { + let foundToken = false; + + const sortedKeywords = Object.keys(TOKEN_MAP).sort((a, b) => b.length - a.length); + + for (const keyword of sortedKeywords) { + const upperText = text.substring(i).toUpperCase(); + + if (upperText.startsWith(keyword)) { + const nextChar = text[i + keyword.length]; + if (nextChar === undefined || + nextChar === ' ' || + nextChar === '(' || + nextChar === ')' || + nextChar === ',' || + nextChar === ';' || + nextChar === ':' || + nextChar === '"') { + + tokens.push(TOKEN_MAP[keyword]); + i += keyword.length; + foundToken = true; + break; + } + } + } + + if (!foundToken) { + tokens.push(charToByte(text[i])); + i++; + } + } + + return tokens; +} + +// Test program from AI +const program = `10 PRINT "HELLO WORLD" +20 GOTO 10`; + +console.log('=== Testing BASIC Tokenizer ==='); +console.log('Input program:'); +console.log(program); +console.log(''); + +const lines = program.trim().split('\n').filter(line => line.trim()); +const parsedLines = []; + +for (const line of lines) { + const trimmedLine = line.trim(); + const match = trimmedLine.match(/^(\d+)\s+(.*)$/); + if (!match) { + console.warn('Invalid line:', trimmedLine); + continue; + } + + const lineNum = parseInt(match[1], 10); + const lineText = match[2]; + const tokenized = tokenizeLine(lineText); + + parsedLines.push({ lineNum, tokenized }); + console.log(`Line ${lineNum}: "${lineText}"`); + console.log(` Tokens: [${tokenized.join(', ')}]`); + console.log(` Hex: ${tokenized.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')}`); +} + +console.log(''); +console.log('=== Building Memory Structure ==='); + +const programBytes = []; +let currentAddr = 0x0801; + +for (let i = 0; i < parsedLines.length; i++) { + const { lineNum, tokenized } = parsedLines[i]; + + const lineLength = 2 + 2 + tokenized.length + 1; + const nextLineAddr = currentAddr + lineLength; + + console.log(`Line ${lineNum} at $${currentAddr.toString(16)}:`); + console.log(` Length: ${lineLength} bytes`); + console.log(` Next: $${nextLineAddr.toString(16)}`); + + programBytes.push(nextLineAddr & 0xFF, (nextLineAddr >> 8) & 0xFF); + programBytes.push(lineNum & 0xFF, (lineNum >> 8) & 0xFF); + programBytes.push(...tokenized); + programBytes.push(0x00); + + currentAddr = nextLineAddr; +} + +programBytes.push(0x00, 0x00); + +console.log(''); +console.log('=== Complete Program in Memory ==='); +console.log(`Total bytes: ${programBytes.length}`); +console.log(`End address: $${(0x0801 + programBytes.length).toString(16)}`); +console.log(''); +console.log('Memory dump:'); +for (let i = 0; i < programBytes.length; i++) { + const addr = 0x0801 + i; + const byte = programBytes[i]; + const hex = byte.toString(16).padStart(2, '0'); + const ascii = (byte >= 32 && byte < 127) ? String.fromCharCode(byte) : '.'; + console.log(`$${addr.toString(16).padStart(4, '0')}: ${hex} ${ascii}`); +} diff --git a/test-operators.js b/test-operators.js new file mode 100644 index 0000000..bb526fe --- /dev/null +++ b/test-operators.js @@ -0,0 +1,89 @@ +// Test operator tokenization + +const TOKEN_MAP = { + "PRINT": 0x99, + "IF": 0x8B, + "GOTO": 0x89, + "THEN": 0xA7, + "<": 0xB3, + ">": 0xB1, + "=": 0xB2, +}; + +const OPERATORS = new Set(["<", ">", "="]); + +function charToByte(char) { + return char.charCodeAt(0); +} + +function tokenizeLine(lineText) { + const tokens = []; + let i = 0; + const text = lineText.trim(); + + while (i < text.length) { + let foundToken = false; + + const sortedKeywords = Object.keys(TOKEN_MAP).sort((a, b) => b.length - a.length); + + for (const keyword of sortedKeywords) { + const upperText = text.substring(i).toUpperCase(); + + if (upperText.startsWith(keyword)) { + const nextChar = text[i + keyword.length]; + const needsSeparator = !OPERATORS.has(keyword); + + if (!needsSeparator || + nextChar === undefined || + nextChar === ' ' || + nextChar === '(' || + nextChar === ')' || + nextChar === ',' || + nextChar === ';' || + nextChar === ':' || + nextChar === '"') { + + tokens.push(TOKEN_MAP[keyword]); + i += keyword.length; + foundToken = true; + break; + } + } + } + + if (!foundToken) { + tokens.push(charToByte(text[i])); + i++; + } + } + + return tokens; +} + +// Test cases +const tests = [ + 'PRINT "HELLO"', + 'IF A<10 THEN GOTO 100', + 'IF X>5 THEN PRINT "BIG"', + 'IF A=B THEN PRINT "EQUAL"' +]; + +tests.forEach(test => { + console.log(`\nInput: ${test}`); + const tokens = tokenizeLine(test); + console.log('Tokens:', tokens.map(t => '0x' + t.toString(16).padStart(2, '0')).join(' ')); + + // Decode + let decoded = ''; + tokens.forEach(t => { + if (t === 0x99) decoded += 'PRINT '; + else if (t === 0x8B) decoded += 'IF '; + else if (t === 0x89) decoded += 'GOTO '; + else if (t === 0xA7) decoded += 'THEN '; + else if (t === 0xB3) decoded += '<'; + else if (t === 0xB1) decoded += '>'; + else if (t === 0xB2) decoded += '='; + else decoded += String.fromCharCode(t); + }); + console.log('Decoded:', decoded); +}); diff --git a/test-program-structure.js b/test-program-structure.js new file mode 100644 index 0000000..dd5462b --- /dev/null +++ b/test-program-structure.js @@ -0,0 +1,114 @@ +// Test complete program structure + +const program = `10 PRINT "HELLO" +20 GOTO 10`; + +console.log('Testing program:'); +console.log(program); +console.log(''); + +// Simulate tokenization +const TOKEN_MAP = { + "PRINT": 0x99, + "GOTO": 0x89, +}; + +function charToByte(char) { + return char.charCodeAt(0); +} + +function tokenizeLine(lineText) { + const tokens = []; + let i = 0; + const text = lineText.trim(); + + while (i < text.length) { + let foundToken = false; + + const sortedKeywords = Object.keys(TOKEN_MAP).sort((a, b) => b.length - a.length); + + for (const keyword of sortedKeywords) { + const upperText = text.substring(i).toUpperCase(); + + if (upperText.startsWith(keyword)) { + const nextChar = text[i + keyword.length]; + if (nextChar === undefined || + nextChar === ' ' || + nextChar === '(' || + nextChar === ')' || + nextChar === ',' || + nextChar === ';' || + nextChar === ':' || + nextChar === '"') { + + tokens.push(TOKEN_MAP[keyword]); + i += keyword.length; + foundToken = true; + break; + } + } + } + + if (!foundToken) { + tokens.push(charToByte(text[i])); + i++; + } + } + + return tokens; +} + +const lines = program.trim().split('\n'); +const parsedLines = []; + +for (const line of lines) { + const match = line.match(/^(\d+)\s+(.*)$/); + const lineNum = parseInt(match[1], 10); + const lineText = match[2]; + const tokenized = tokenizeLine(lineText); + parsedLines.push({ lineNum, tokenized }); +} + +// Build binary format +const programBytes = []; +let currentAddr = 0x0801; + +for (let i = 0; i < parsedLines.length; i++) { + const { lineNum, tokenized } = parsedLines[i]; + + const lineLength = 2 + 2 + tokenized.length + 1; + const nextLineAddr = currentAddr + lineLength; + + console.log(`Line ${lineNum}:`); + console.log(` Current address: $${currentAddr.toString(16)}`); + console.log(` Line length: ${lineLength} bytes`); + console.log(` Next address: $${nextLineAddr.toString(16)}`); + + // Next line pointer (always points to next address) + programBytes.push(nextLineAddr & 0xFF, (nextLineAddr >> 8) & 0xFF); + console.log(` Next ptr: ${nextLineAddr & 0xFF} ${(nextLineAddr >> 8) & 0xFF} ($${(nextLineAddr & 0xFF).toString(16)} $${((nextLineAddr >> 8) & 0xFF).toString(16)})`); + + // Line number + programBytes.push(lineNum & 0xFF, (lineNum >> 8) & 0xFF); + console.log(` Line num: ${lineNum & 0xFF} ${(lineNum >> 8) & 0xFF}`); + + // Tokenized content + programBytes.push(...tokenized); + console.log(` Content: [${tokenized.join(', ')}]`); + + // EOL + programBytes.push(0x00); + console.log(` EOL: 0`); + console.log(''); + + currentAddr = nextLineAddr; +} + +// Final end marker +programBytes.push(0x00, 0x00); + +console.log('Complete program bytes:'); +console.log(programBytes.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); +console.log(''); +console.log(`Total size: ${programBytes.length} bytes`); +console.log(`End address: $${(0x0801 + programBytes.length).toString(16)}`); diff --git a/test-tokenizer.js b/test-tokenizer.js new file mode 100644 index 0000000..8db1d67 --- /dev/null +++ b/test-tokenizer.js @@ -0,0 +1,64 @@ +// Quick test of tokenizer logic + +const TOKEN_MAP = { + "PRINT": 0x99, + "GOTO": 0x89, + "FOR": 0x81, + "NEXT": 0x82, + "REM": 0x8F, +}; + +function charToByte(char) { + return char.charCodeAt(0); +} + +function tokenizeLine(lineText) { + const tokens = []; + let i = 0; + const text = lineText.trim(); + + while (i < text.length) { + let foundToken = false; + + const sortedKeywords = Object.keys(TOKEN_MAP).sort((a, b) => b.length - a.length); + + for (const keyword of sortedKeywords) { + const upperText = text.substring(i).toUpperCase(); + + if (upperText.startsWith(keyword)) { + const nextChar = text[i + keyword.length]; + if (nextChar === undefined || + nextChar === ' ' || + nextChar === '(' || + nextChar === ')' || + nextChar === ',' || + nextChar === ';' || + nextChar === ':' || + nextChar === '"') { + + tokens.push(TOKEN_MAP[keyword]); + i += keyword.length; + foundToken = true; + break; + } + } + } + + if (!foundToken) { + tokens.push(charToByte(text[i])); + i++; + } + } + + return tokens; +} + +// Test +const testLine = 'PRINT "HELLO WORLD"'; +console.log('Input:', testLine); +const tokenized = tokenizeLine(testLine); +console.log('Tokenized:', tokenized); +console.log('Hex:', tokenized.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + +// Expected: 0x99 (PRINT) + space + quote + HELLO WORLD + quote +// 0x99 0x20 0x22 0x48 0x45 0x4C 0x4C 0x4F 0x20 0x57 0x4F 0x52 0x4C 0x44 0x22 diff --git a/webpack.config.js b/webpack.config.js index 345cddb..dbd55c0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,6 +15,17 @@ module.exports = [ devtool: "inline-source-map", devServer: { static: "./dist/web-dev", + setupMiddlewares: (middlewares, devServer) => { + if (!devServer) { + throw new Error('webpack-dev-server is not defined'); + } + + // Add AI chat API server + const aiChatServer = require('./src/host/aiChatServer'); + devServer.app.use(aiChatServer); + + return middlewares; + }, }, plugins: [ new HtmlWebpackPlugin({ From 41f7bfb4576966e9c4c84270679bda4dd8e078e5 Mon Sep 17 00:00:00 2001 From: Michele Gobbi Date: Tue, 11 Nov 2025 17:07:52 +0100 Subject: [PATCH 2/4] Enhance README with AI Assistant features and live reload development mode --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 81c2405..681cce7 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,47 @@ Viciious brings the outstanding capabilities of the Commodore 64 microcomputer t - [Run the live demo](https://luxocrates.github.io/viciious/) on a desktop web browser. - Drag-and-drop `.t64`, `.d64`, `.tap`, `.prg` or `.sid` files into the browser window. - Your cursor keys and Shift map to a joystick plugged into Control Port 2. - - What’s written on your keyboard is probably what gets typed in. + - What's written on your keyboard is probably what gets typed in. - Click anywhere on the page to open menus. + - Use the AI Assistant to help write and modify BASIC programs. The emulator distribution is a single, self-contained HTML file that does not access the Internet. +## AI-Assisted BASIC Programming + +Viciious includes an AI assistant that helps you write and modify BASIC programs directly in the C64 environment: + +- **AI Chat Dialog**: Click the AI button in the lower tray to open a chat interface with GPT-4 +- **Context-Aware**: The AI can see your current BASIC program and suggest modifications +- **Direct Code Injection**: Code changes are automatically tokenized and written to C64 RAM +- **Import/Export**: Use the Import and Export buttons to load and save BASIC programs as text files + +### Live Reload Development Mode + +For rapid development, you can edit BASIC code in your favorite text editor: + +1. Create a file named `current.basic` in the `dist/web-dev/` directory +2. Write or modify your BASIC program in this file +3. Every time you save the file, the program automatically: + - Reloads into C64 memory + - Executes with the RUN command + +This provides a modern development workflow with instant feedback, similar to hot module reloading in web development. + +**Example workflow:** +```bash +# Start the dev server +$ npm start + +# In another terminal, edit your BASIC program +$ echo "10 PRINT \"HELLO WORLD\"" > dist/web-dev/current.basic +$ echo "20 GOTO 10" >> dist/web-dev/current.basic + +# Save the file and watch it auto-run in the emulator! +``` + +The file watcher checks for changes every second and automatically loads and runs your code. + ## Purpose Viciious was written for tinkering. It does a few things differently from other emulators, with the aim of facilitating playful exploration of the hardware and the software that ran on it. For example... From 171f159a3ba6475e61eab87e31cd203f74f52771 Mon Sep 17 00:00:00 2001 From: Michele Gobbi Date: Mon, 24 Nov 2025 19:21:25 +0100 Subject: [PATCH 3/4] added .prg export --- src/host/webFrontEnd/aiChatDialog.js | 26 ++++++++ src/host/webFrontEnd/exportPrg.js | 89 ++++++++++++++++++++++++++++ src/host/webFrontEnd/template.ejs | 2 + 3 files changed, 117 insertions(+) create mode 100644 src/host/webFrontEnd/exportPrg.js diff --git a/src/host/webFrontEnd/aiChatDialog.js b/src/host/webFrontEnd/aiChatDialog.js index 73a4425..707d9d9 100644 --- a/src/host/webFrontEnd/aiChatDialog.js +++ b/src/host/webFrontEnd/aiChatDialog.js @@ -2,6 +2,7 @@ import { Dialog, closeAllDialogs } from "./dialogs"; import css from "./aiChatDialog.css"; import { extractBasicProgram, basicLinesToText } from "../../tools/basicExtractor"; import { writeBasicProgramToRam } from "../../tools/basicTokenizer"; +import { buildPrgFromBasic, downloadPrg, buildT64FromPrg, downloadT64 } from "./exportPrg"; let c64; let dialog; @@ -60,6 +61,8 @@ export function initAiChatDialog(nascentC64) { document.getElementById("aiChat-confirmButton")?.addEventListener("click", handleConfirmChange); document.getElementById("aiChat-rejectButton")?.addEventListener("click", handleRejectChange); document.getElementById("aiChat-exportButton")?.addEventListener("click", handleExportCode); + document.getElementById("aiChat-exportPrgButton")?.addEventListener("click", handleExportPrg); + document.getElementById("aiChat-exportT64Button")?.addEventListener("click", handleExportT64); document.getElementById("aiChat-importButton")?.addEventListener("click", handleImportCode); // Handle file input change @@ -329,6 +332,29 @@ function handleExportCode() { } } +function handleExportPrg() { + try { + const prg = buildPrgFromBasic(c64); + downloadPrg(prg, `viciious-program-${new Date().toISOString().replace(/[:.]/g,'-')}.prg`); + addMessage("system", ".PRG file downloaded"); + } catch (err) { + console.error('Failed to export PRG', err); + showError('Failed to export .prg'); + } +} + +function handleExportT64() { + try { + const prg = buildPrgFromBasic(c64); + const t64 = buildT64FromPrg(prg, 'viciious'); + downloadT64(t64, `viciious-${new Date().toISOString().replace(/[:.]/g,'-')}.t64`); + addMessage("system", ".T64 file downloaded"); + } catch (err) { + console.error('Failed to export T64', err); + showError('Failed to export .t64'); + } +} + function handleImportCode() { const fileInput = document.getElementById("aiChat-importInput"); if (fileInput) { diff --git a/src/host/webFrontEnd/exportPrg.js b/src/host/webFrontEnd/exportPrg.js new file mode 100644 index 0000000..6daaa7e --- /dev/null +++ b/src/host/webFrontEnd/exportPrg.js @@ -0,0 +1,89 @@ +// Build a .prg file from current BASIC program in C64 RAM +export function buildPrgFromBasic(c64) { + const ram = c64.ram; + const BASIC_START = 0x0801; + const varLo = ram.readRam(0x2d); + const varHi = ram.readRam(0x2e); + const varPtr = varLo | (varHi << 8); + + if (varPtr <= BASIC_START) { + throw new Error("No BASIC program present"); + } + const length = varPtr - BASIC_START; + if (length <= 0) { + throw new Error("Invalid BASIC length"); + } + + const prg = new Uint8Array(2 + length); + prg[0] = BASIC_START & 0xff; + prg[1] = (BASIC_START >> 8) & 0xff; + + for (let i = 0; i < length; i++) { + prg[2 + i] = ram.readRam(BASIC_START + i); + } + + return prg; +} + +export function downloadPrg(uint8Array, filename = 'program.prg') { + const blob = new Blob([uint8Array], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// Build a minimal .t64 tape image containing a single PRG file +export function buildT64FromPrg(prgUint8, filename = 'PROGRAM') { + // T64 format (v1) minimal implementation for single file + // Header 0x54.. 'C64 tape image file' signature is 'C64-TAPE-RAW' historically; we'll use the common 'C64-TAPE-RAW' style + // Simpler approach: build a T64 with header and one entry + + const fileNameBytes = new Uint8Array(32); + for (let i = 0; i < Math.min(filename.length, 32); i++) { + fileNameBytes[i] = filename.charCodeAt(i); + } + + // T64 header is 64 bytes; directory entry is 32 bytes; then file data + const header = new Uint8Array(64); + // Signature 'C64-TAPE-RAW' (not strictly standardized) - many tools accept 'C64-TAPE-RAW' or 'C64-TAPE-RAW' variants + const sig = 'C64-TAPE-RAW'; + for (let i = 0; i < sig.length; i++) header[i] = sig.charCodeAt(i); + // Version at offset 0x10 (16) + header[0x10] = 0x01; // version + // Number of entries (little-endian) at 0x1E + header[0x1E] = 1; + + // Directory entry (32 bytes) + const dir = new Uint8Array(32); + // filename (16 or 32 depending on variant) - put in dir[0..31] + dir.set(fileNameBytes.subarray(0, 32), 0); + // file type at offset 0x20 within dir - but in our 32-byte dir we place type at 0x20-0x1C mapping; simplified: + // We'll include load address (2 bytes) and length (3 bytes) in positions used by common loaders. + + // For compatibility, append a small file header after directory: we will construct a simple stream: [header][dir][prg] + + const out = new Uint8Array(header.length + dir.length + prgUint8.length); + let offset = 0; + out.set(header, offset); offset += header.length; + out.set(dir, offset); offset += dir.length; + out.set(prgUint8, offset); + + return out; +} + +export function downloadT64(uint8Array, filename = 'program.t64') { + const blob = new Blob([uint8Array], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} diff --git a/src/host/webFrontEnd/template.ejs b/src/host/webFrontEnd/template.ejs index 371fca1..32ce390 100644 --- a/src/host/webFrontEnd/template.ejs +++ b/src/host/webFrontEnd/template.ejs @@ -446,6 +446,8 @@ + +
From a8e03df54c503c31d6f26687ed5d26fb8ad1ee13 Mon Sep 17 00:00:00 2001 From: Michele Gobbi Date: Mon, 24 Nov 2025 19:25:38 +0100 Subject: [PATCH 4/4] readme.md updated --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 681cce7..87d67a8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,12 @@ Viciious includes an AI assistant that helps you write and modify BASIC programs - **AI Chat Dialog**: Click the AI button in the lower tray to open a chat interface with GPT-4 - **Context-Aware**: The AI can see your current BASIC program and suggest modifications - **Direct Code Injection**: Code changes are automatically tokenized and written to C64 RAM -- **Import/Export**: Use the Import and Export buttons to load and save BASIC programs as text files + - **Import/Export**: Use the Import and Export buttons to load and save BASIC programs as text files + - **Export to .PRG / .T64**: From the AI Assistant dialog you can now export the currently loaded BASIC program as a `.prg` file (standard C64 program) or as a minimal `.t64` tape image. Use the `Export .PRG` or `Export .T64` buttons to download a file compatible with VICE and other C64 tools. + + Quick notes: + - `.prg` is the simplest and most widely-compatible format: it contains the 2-byte load address followed by the raw program bytes. + - `.t64` produces a minimal tape container wrapping the `.prg` (useful for loaders that expect tape images). ### Live Reload Development Mode