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/README.md b/README.md index 81c2405..87d67a8 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,52 @@ 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 + - **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 + +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... 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..707d9d9 --- /dev/null +++ b/src/host/webFrontEnd/aiChatDialog.js @@ -0,0 +1,493 @@ +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; +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-exportPrgButton")?.addEventListener("click", handleExportPrg); + document.getElementById("aiChat-exportT64Button")?.addEventListener("click", handleExportT64); + 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 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) { + 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/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/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..32ce390 100644 --- a/src/host/webFrontEnd/template.ejs +++ b/src/host/webFrontEnd/template.ejs @@ -120,7 +120,7 @@
- +
@@ -422,6 +422,38 @@

+
+ +

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({