diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..393458a --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# Supabase Configuration +SUPABASE_URL=your_supabase_project_url +SUPABASE_ANON_KEY=your_supabase_anon_key +SUPABASE_SERVICE_KEY=your_supabase_service_key + +# Notion Configuration +NOTION_API_KEY=your_notion_integration_token +NOTION_DATABASE_ID_CREDIT=your_notion_database_id_for_credit_memos +NOTION_DATABASE_ID_NOVELS=your_notion_database_id_for_novels + +# Google Drive Configuration +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback +GOOGLE_DRIVE_FOLDER_ID=your_google_drive_folder_id + +# JWT Secret for authentication +JWT_SECRET=your_jwt_secret_key_here + +# Server Configuration +PORT=3000 +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9f7c5fa..8b3ed42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ node_modules/ +.env +.env.local uploads/ workspace.db +credentials.json +token.json *.log -.env .DS_Store -Thumbs.db \ No newline at end of file +dist/ +build/ \ No newline at end of file diff --git a/README.md b/README.md index cae1c61..7ced965 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,47 @@ -# Credit Analysis & Novel Planning Workspace +# Commodities Credit Analysis & Novel Planning Workspace -A comprehensive workspace application that combines credit analysis tools for commodities trading companies with novel planning and story development features. +A comprehensive workspace application that combines credit analysis tools for commodities trading companies with novel planning and development features. The application integrates with Supabase for real-time database sync, Notion for documentation, and Google Drive for file storage. ## Features -### Credit Analysis for Commodities Trading -- **Company Management**: Track and manage up to 15 commodities trading companies -- **Financial Document OCR**: Upload and process financial documents (PDF, JPG, PNG, JPEG) -- **Credit Memo Generation**: Create various types of credit memos: - - Annual Review - - Refinancing - - New Deals - - Amendments -- **Financial Metrics Analysis**: Store and analyze financial data for consistent peer comparison -- **Database Storage**: All financial data and credit memos stored in SQLite database - -### Novel Planning & Story Development -- **Novel Project Management**: Create and manage multiple novel projects -- **Chapter Management**: Organize novels into chapters (targeting 25 chapters) -- **Story Beats Tracking**: Track detailed story beats (targeting 250 beats per novel) -- **POV Support**: Built-in support for dual POV, alternating perspective novels -- **Tense Management**: Support for past and present tense narratives -- **Progress Tracking**: Monitor chapter and beat completion - -## Screenshots - -### Credit Analysis Interface -![Credit Analysis](https://github.com/user-attachments/assets/f43fa598-80b6-4711-96db-4368b32bad2d) - -### Novel Planning Interface -![Novel Planning](https://github.com/user-attachments/assets/c93e654c-1e3d-4271-9d9d-50580f4a125d) - -### Chapter Management in Action -![Chapter Management](https://github.com/user-attachments/assets/4dcd3180-067c-45dd-8b8b-8a0c28f8ba3c) - -## Technology Stack +### Credit Analysis +- **Company Management**: Track and manage commodities trading companies +- **Financial Document Processing**: Upload and process financial statements with OCR capabilities +- **Credit Memo Generation**: Create structured credit memos for various purposes (annual review, refinancing, new deals) +- **Financial Metrics Tracking**: Store and analyze key financial metrics -- **Backend**: Node.js with Express.js -- **Database**: SQLite3 -- **Frontend**: HTML5, CSS3, Vanilla JavaScript -- **File Upload**: Multer middleware -- **OCR**: Framework ready for integration with tesseract.js or similar +### Novel Planning +- **Novel Project Management**: Create and organize novel projects with customizable POV styles and tenses +- **Chapter Organization**: Structure your novel with detailed chapter outlines +- **Story Beat Tracking**: Track up to 250+ story beats across your narrative +- **POV Management**: Support for single, dual, or multiple POV narratives + +### Cloud Integrations + +#### Supabase Integration +- Real-time database synchronization +- User authentication support +- Automatic data backups +- Live collaboration features + +#### Notion Integration +- Sync credit memos to Notion databases +- Create structured novel project pages +- Track chapters and story beats in Notion +- Collaborative editing and commenting + +#### Google Drive Integration +- Create Google Docs for each chapter +- Generate financial spreadsheets automatically +- Organize files in dedicated folders +- Share documents with team members ## Installation 1. Clone the repository: ```bash git clone -cd Workspace +cd commodities-credit-workspace ``` 2. Install dependencies: @@ -55,34 +49,103 @@ cd Workspace npm install ``` -3. Start the application: +3. Run the setup wizard: ```bash -npm start +npm run setup +``` + +4. Follow the prompts to configure your integrations. + +## Configuration + +### Manual Configuration +If you prefer to configure manually, create a `.env` file with the following variables: + +```env +# Supabase Configuration +SUPABASE_URL=your_supabase_project_url +SUPABASE_ANON_KEY=your_supabase_anon_key +SUPABASE_SERVICE_KEY=your_supabase_service_key + +# Notion Configuration +NOTION_API_KEY=your_notion_integration_token +NOTION_DATABASE_ID_CREDIT=your_notion_database_id_for_credit_memos +NOTION_DATABASE_ID_NOVELS=your_notion_database_id_for_novels + +# Google Drive Configuration +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback +GOOGLE_DRIVE_FOLDER_ID=your_google_drive_folder_id + +# JWT Secret for authentication +JWT_SECRET=your_jwt_secret_key_here + +# Server Configuration +PORT=3000 +NODE_ENV=development ``` -4. Open your browser and navigate to `http://localhost:3000` +## Setting Up Integrations + +### Supabase Setup +1. Create a new project at [supabase.com](https://supabase.com) +2. Navigate to Settings > API to find your project URL and keys +3. Create the following tables in your Supabase dashboard: + - companies + - financial_data + - credit_memos + - novels + - chapters + - story_beats + +### Notion Setup +1. Create a Notion integration at [notion.so/my-integrations](https://www.notion.so/my-integrations) +2. Create two databases in Notion: + - Credit Memos database with properties: Title, Company, Memo Type, Industry, Date + - Novels database with properties: Title, POV Style, Tense, Target Chapters, Target Beats, Status +3. Share the databases with your integration + +### Google Drive Setup +1. Go to [Google Cloud Console](https://console.cloud.google.com) +2. Create a new project or select existing +3. Enable Google Drive API +4. Create OAuth 2.0 credentials +5. Add `http://localhost:3000/auth/google/callback` to authorized redirect URIs +6. Download credentials and add to `.env` file ## Usage -### Credit Analysis Workflow +### Starting the Application +```bash +npm start +``` + +The application will be available at `http://localhost:3000` -1. **Add Companies**: Start by adding commodities trading companies to the system -2. **Upload Financial Documents**: Upload financial statements, balance sheets, cash flow statements, or income statements -3. **Generate Credit Memos**: Create comprehensive credit memos with financial metrics analysis -4. **Peer Comparison**: Use stored data for consistent analysis across companies +### Using the Application -### Novel Planning Workflow +1. **Credit Analysis Tab**: + - Add companies you want to analyze + - Upload financial documents for OCR processing + - Generate credit memos with financial metrics -1. **Create Novel Project**: Set up a new novel with target chapters (default: 25) and story beats (default: 250) -2. **Add Chapters**: Create individual chapters with POV character assignments and summaries -3. **Track Story Beats**: Add detailed story beats linked to chapters with type classification -4. **Monitor Progress**: Track completion towards your target chapter and beat counts +2. **Novel Planning Tab**: + - Create novel projects with target chapters and beats + - Add chapters with POV characters and summaries + - Track story beats with beat types (setup, climax, resolution, etc.) + +3. **Integrations Tab**: + - Connect to Google Drive for document storage + - Sync data to Supabase for real-time collaboration + - Export to Notion for advanced documentation + - Configure auto-sync settings ## API Endpoints ### Credit Analysis - `GET /api/companies` - List all companies -- `POST /api/companies` - Add new company +- `POST /api/companies` - Create new company - `POST /api/upload-financial` - Upload financial document - `POST /api/credit-memos` - Create credit memo @@ -90,28 +153,65 @@ npm start - `GET /api/novels` - List all novels - `POST /api/novels` - Create new novel - `GET /api/novels/:id/chapters` - Get chapters for a novel -- `POST /api/chapters` - Add new chapter -- `GET /api/novels/:id/beats` - Get story beats for a novel -- `POST /api/beats` - Add new story beat +- `POST /api/chapters` - Create new chapter +- `GET /api/novels/:id/beats` - Get story beats +- `POST /api/beats` - Create story beat + +### Integration Endpoints +- `POST /api/sync/supabase/companies` - Sync companies to Supabase +- `POST /api/sync/notion/credit-memo` - Create Notion credit memo +- `POST /api/sync/notion/novel` - Create Notion novel page +- `GET /api/auth/google` - Get Google auth URL +- `POST /api/drive/create-chapter-doc` - Create Google Doc for chapter +- `POST /api/drive/create-financial-sheet` - Create financial spreadsheet +- `POST /api/sync/all` - Sync to all connected services -## Database Schema +## Development -The application uses SQLite with the following main tables: -- `companies` - Trading company information -- `financial_data` - Uploaded financial documents and OCR data -- `credit_memos` - Generated credit analysis memos -- `novels` - Novel project details -- `chapters` - Individual chapters with POV and summaries -- `story_beats` - Detailed story beats with type classification +### Project Structure +``` +├── server.js # Main server file +├── services/ +│ ├── supabase.js # Supabase integration +│ ├── notion.js # Notion integration +│ └── googleDrive.js # Google Drive integration +├── public/ +│ ├── index.html # Main HTML file +│ ├── script.js # Frontend JavaScript +│ └── styles.css # CSS styles +├── uploads/ # Temporary file uploads +└── workspace.db # Local SQLite database +``` -## Development +### Adding New Features +1. Update the database schema in `server.js` +2. Add corresponding Supabase tables if using Supabase +3. Update frontend in `public/` files +4. Add integration endpoints as needed + +## Troubleshooting + +### Common Issues + +1. **Supabase connection failed**: Check your Supabase URL and keys in `.env` +2. **Notion sync not working**: Ensure your integration has access to the databases +3. **Google Drive authentication error**: Verify redirect URI matches configuration +4. **Port already in use**: Change PORT in `.env` file + +### Debug Mode +Set `NODE_ENV=development` in `.env` for detailed error messages + +## Security Notes + +- Never commit `.env` file to version control +- Use environment variables for all sensitive data +- Regularly rotate API keys and secrets +- Enable 2FA on all cloud service accounts + +## Support -The application is designed to be easily extensible: -- Add OCR libraries like tesseract.js for actual document processing -- Integrate financial analysis libraries for automated metrics calculation -- Add user authentication and multi-tenancy support -- Implement export features for credit memos and novel outlines +For issues, questions, or feature requests, please create an issue in the repository. ## License -MIT License \ No newline at end of file +MIT License - See LICENSE file for details \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc93824..1827f50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,19 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@notionhq/client": "^5.1.0", + "@supabase/supabase-js": "^2.58.0", + "axios": "^1.12.2", + "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^17.2.2", "express": "^4.18.2", "fs": "^0.0.1-security", + "googleapis": "^160.0.0", + "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", + "node-cron": "^4.2.1", "path": "^0.12.7", "sqlite3": "^5.1.6" } @@ -24,6 +33,15 @@ "license": "MIT", "optional": true }, + "node_modules/@notionhq/client": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.1.0.tgz", + "integrity": "sha512-YYVjXYk1XwKQ4XIh+iGjaaXOGHxaDgB3UaGnDMyrZ3X9UiYQsZpzPIvTuhvp97os8a5W5kTQFsyq77+I+COOVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -63,6 +81,80 @@ "node": ">=10" } }, + "node_modules/@supabase/auth-js": { + "version": "2.72.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.72.0.tgz", + "integrity": "sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.5.0.tgz", + "integrity": "sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz", + "integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.5", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz", + "integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.2.tgz", + "integrity": "sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.58.0.tgz", + "integrity": "sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.72.0", + "@supabase/functions-js": "2.5.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.21.4", + "@supabase/realtime-js": "2.15.5", + "@supabase/storage-js": "2.12.2" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -73,6 +165,30 @@ "node": ">= 6" } }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -217,6 +333,23 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -244,6 +377,24 @@ ], "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -337,6 +488,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -464,6 +621,18 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -535,6 +704,28 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -568,6 +759,15 @@ "node": ">=4.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -603,6 +803,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "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", @@ -617,6 +829,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -718,6 +939,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -788,6 +1024,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -812,6 +1077,54 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -891,6 +1204,79 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gaxios": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz", + "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/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/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/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/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -956,6 +1342,62 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/google-auth-library": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.3.0.tgz", + "integrity": "sha512-ylSE3RlCRZfZB56PFJSfUCuiuPq83Fx8hqu1KPWGK8FVdSaxlp/qkeMMX/DT/18xkwXIHvXEXkZsljRwfrdEfQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "160.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-160.0.0.tgz", + "integrity": "sha512-lAGFQVSeYvWKCxeCQKo20xWFFDgnWuJYbLn92IgLrT4UTYVOGdrZ9XTqgWJf316isE9KdfuDY5X8Tu4ZrXSFig==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz", + "integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -975,6 +1417,19 @@ "license": "ISC", "optional": true }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -987,6 +1442,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -1250,6 +1720,127 @@ "license": "ISC", "optional": true }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/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/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1565,6 +2156,53 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -1791,6 +2429,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -2399,6 +3043,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2430,6 +3080,12 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -2459,6 +3115,12 @@ "node": ">= 0.8" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -2498,6 +3160,31 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2530,6 +3217,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 3a686b9..606690b 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,33 @@ "scripts": { "start": "node server.js", "dev": "node server.js", - "test": "echo \"No tests specified\" && exit 0" + "test": "echo \"No tests specified\" && exit 0", + "setup": "node setup.js" }, - "keywords": ["credit-analysis", "commodities", "trading", "novel-planning", "workspace"], + "keywords": [ + "credit-analysis", + "commodities", + "trading", + "novel-planning", + "workspace" + ], "author": "", "license": "MIT", "dependencies": { + "@notionhq/client": "^5.1.0", + "@supabase/supabase-js": "^2.58.0", + "axios": "^1.12.2", + "bcryptjs": "^3.0.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^17.2.2", "express": "^4.18.2", - "sqlite3": "^5.1.6", + "fs": "^0.0.1-security", + "googleapis": "^160.0.0", + "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", - "body-parser": "^1.20.2", + "node-cron": "^4.2.1", "path": "^0.12.7", - "fs": "^0.0.1-security" + "sqlite3": "^5.1.6" } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index 4e35965..e57aa2c 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,7 @@

Workspace

@@ -136,6 +137,96 @@

Story Beats Tracking

+ + +
+

Cloud Integrations

+ +
+

Supabase Integration

+
+ + Checking connection... +
+ +
+

Supabase provides real-time database sync and authentication.

+
    +
  • Real-time data synchronization
  • +
  • User authentication
  • +
  • Automatic backups
  • +
+
+
+ +
+

Notion Integration

+
+ + Checking connection... +
+ +
+

Sync your credit memos and novel projects to Notion databases.

+
    +
  • Create credit memo pages
  • +
  • Organize novel projects
  • +
  • Track chapters and story beats
  • +
+
+
+ +
+

Google Drive Integration

+
+ + Checking connection... +
+ + + +
+

Store documents and spreadsheets in Google Drive.

+
    +
  • Create Google Docs for chapters
  • +
  • Generate financial spreadsheets
  • +
  • Automatic file organization
  • +
+
+
+ +
+

Sync Settings

+
+ + + +
+ + +
+ +
+
+
diff --git a/public/script.js b/public/script.js index aa37199..e389dbb 100644 --- a/public/script.js +++ b/public/script.js @@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', function() { // Navigation document.getElementById('credit-tab').addEventListener('click', () => switchTab('credit')); document.getElementById('novel-tab').addEventListener('click', () => switchTab('novel')); + document.getElementById('integrations-tab').addEventListener('click', () => switchTab('integrations')); // Credit Analysis Forms document.getElementById('company-form').addEventListener('submit', handleCompanySubmit); @@ -19,9 +20,26 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('chapter-form').addEventListener('submit', handleChapterSubmit); document.getElementById('beat-form').addEventListener('submit', handleBeatSubmit); + // Integration buttons + document.getElementById('sync-supabase-btn').addEventListener('click', syncToSupabase); + document.getElementById('sync-notion-btn').addEventListener('click', syncToNotion); + document.getElementById('auth-google-btn').addEventListener('click', authenticateGoogle); + document.getElementById('sync-drive-btn').addEventListener('click', syncToGoogleDrive); + document.getElementById('sync-settings-form').addEventListener('submit', saveSyncSettings); + // Load initial data loadCompanies(); loadNovels(); + checkIntegrationStatus(); + loadGoogleDriveFiles(); + + // Check for Google auth callback + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('google_auth') === 'success') { + showSuccess('Successfully connected to Google Drive!'); + window.history.replaceState({}, document.title, '/'); + loadGoogleDriveFiles(); + } }); // Navigation @@ -35,6 +53,9 @@ function switchTab(tab) { } else if (tab === 'novel') { document.getElementById('novel-tab').classList.add('active'); document.getElementById('novel-section').classList.add('active'); + } else if (tab === 'integrations') { + document.getElementById('integrations-tab').classList.add('active'); + document.getElementById('integrations-section').classList.add('active'); } } @@ -359,4 +380,262 @@ function parseJSON(str) { } catch { return {}; } +} + +// Integration Functions +async function checkIntegrationStatus() { + // Check Supabase status + updateIntegrationStatus('supabase', 'checking'); + try { + const response = await fetch('/api/sync/supabase/companies', { method: 'POST' }); + if (response.ok) { + updateIntegrationStatus('supabase', 'connected'); + } else { + updateIntegrationStatus('supabase', 'disconnected'); + } + } catch { + updateIntegrationStatus('supabase', 'disconnected'); + } + + // Check Notion status (simplified check) + updateIntegrationStatus('notion', process.env.NOTION_API_KEY ? 'connected' : 'disconnected'); + + // Check Google Drive status + updateIntegrationStatus('google', 'checking'); + try { + const response = await fetch('/api/drive/files'); + if (response.ok) { + updateIntegrationStatus('google', 'connected'); + document.getElementById('auth-google-btn').style.display = 'none'; + document.getElementById('sync-drive-btn').style.display = 'block'; + document.getElementById('drive-files').style.display = 'block'; + } else { + updateIntegrationStatus('google', 'disconnected'); + } + } catch { + updateIntegrationStatus('google', 'disconnected'); + } +} + +function updateIntegrationStatus(service, status) { + const statusElement = document.getElementById(`${service}-status`); + const indicator = statusElement.querySelector('.status-indicator'); + const text = statusElement.querySelector('.status-text'); + + indicator.setAttribute('data-status', status); + + switch(status) { + case 'connected': + indicator.textContent = '✓'; + text.textContent = 'Connected'; + break; + case 'disconnected': + indicator.textContent = '✗'; + text.textContent = 'Not connected'; + break; + case 'checking': + indicator.textContent = '⟳'; + text.textContent = 'Checking connection...'; + break; + } +} + +async function syncToSupabase() { + showMessage('Syncing to Supabase...', 'info'); + + try { + // Sync companies + for (const company of companies) { + await fetch('/api/sync/all', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'company', + data: company + }) + }); + } + + // Sync novels + for (const novel of novels) { + await fetch('/api/sync/all', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'novel', + data: novel + }) + }); + } + + showSuccess('Successfully synced to Supabase!'); + } catch (error) { + showError('Error syncing to Supabase: ' + error.message); + } +} + +async function syncToNotion() { + showMessage('Syncing to Notion...', 'info'); + + try { + // Get all credit memos + const memoResponse = await fetch('/api/credit-memos'); + const memos = await memoResponse.json(); + + // Sync each memo to Notion + for (const memo of memos) { + const company = companies.find(c => c.id === memo.company_id); + await fetch('/api/sync/notion/credit-memo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + memoData: { + ...memo, + company_name: company?.name || 'Unknown', + industry: company?.industry || 'Unknown' + } + }) + }); + } + + // Sync novels to Notion + for (const novel of novels) { + await fetch('/api/sync/notion/novel', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ novelData: novel }) + }); + } + + showSuccess('Successfully synced to Notion!'); + } catch (error) { + showError('Error syncing to Notion: ' + error.message); + } +} + +async function authenticateGoogle() { + try { + const response = await fetch('/api/auth/google'); + const data = await response.json(); + + if (data.authUrl) { + window.location.href = data.authUrl; + } else { + showError('Could not generate Google authentication URL'); + } + } catch (error) { + showError('Error authenticating with Google: ' + error.message); + } +} + +async function syncToGoogleDrive() { + showMessage('Syncing to Google Drive...', 'info'); + + try { + // Create financial spreadsheets for companies + for (const company of companies) { + await fetch('/api/drive/create-financial-sheet', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + companyData: company, + financialData: {} // Would be populated with actual financial data + }) + }); + } + + // Create documents for novel chapters + const chaptersResponse = await fetch(`/api/novels/${currentNovelId}/chapters`); + const chapters = await chaptersResponse.json(); + + for (const chapter of chapters) { + await fetch('/api/drive/create-chapter-doc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chapterData: chapter }) + }); + } + + showSuccess('Successfully synced to Google Drive!'); + loadGoogleDriveFiles(); + } catch (error) { + showError('Error syncing to Google Drive: ' + error.message); + } +} + +async function loadGoogleDriveFiles() { + try { + const response = await fetch('/api/drive/files'); + if (response.ok) { + const data = await response.json(); + renderDriveFiles(data.files); + } + } catch (error) { + console.log('Could not load Google Drive files'); + } +} + +function renderDriveFiles(files) { + const container = document.getElementById('drive-files-list'); + if (!files || files.length === 0) { + container.innerHTML = '

No files found

'; + return; + } + + container.innerHTML = files.map(file => ` +
+ ${getFileIcon(file.mimeType)} + ${file.name} + Open +
+ `).join(''); +} + +function getFileIcon(mimeType) { + if (mimeType.includes('document')) return '📄'; + if (mimeType.includes('spreadsheet')) return '📊'; + if (mimeType.includes('folder')) return '📁'; + return '📎'; +} + +async function saveSyncSettings(e) { + e.preventDefault(); + + const settings = { + autoSyncSupabase: document.getElementById('auto-sync-supabase').checked, + autoSyncNotion: document.getElementById('auto-sync-notion').checked, + autoSyncDrive: document.getElementById('auto-sync-drive').checked, + syncInterval: document.getElementById('sync-interval').value + }; + + localStorage.setItem('syncSettings', JSON.stringify(settings)); + + // Set up auto-sync if enabled + if (settings.syncInterval !== 'manual') { + const intervalMinutes = parseInt(settings.syncInterval); + setInterval(() => { + if (settings.autoSyncSupabase) syncToSupabase(); + if (settings.autoSyncNotion) syncToNotion(); + if (settings.autoSyncDrive) syncToGoogleDrive(); + }, intervalMinutes * 60 * 1000); + } + + showSuccess('Sync settings saved!'); +} + +function showMessage(message, type) { + const existingMessage = document.querySelector('.success-message, .error-message, .info-message'); + if (existingMessage) { + existingMessage.remove(); + } + + const messageDiv = document.createElement('div'); + messageDiv.className = type === 'success' ? 'success-message' : type === 'error' ? 'error-message' : 'info-message'; + messageDiv.textContent = message; + + document.querySelector('.container').insertBefore(messageDiv, document.querySelector('.container').firstChild.nextSibling); + + setTimeout(() => { + messageDiv.remove(); + }, 5000); } \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 7285898..0e85b62 100644 --- a/public/styles.css +++ b/public/styles.css @@ -193,6 +193,169 @@ button:disabled { font-size: 0.9rem; } +/* Integration Styles */ +.integration-status { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + padding: 0.75rem; + background: #f8f9fa; + border-radius: 4px; +} + +.status-indicator { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.5rem; +} + +.status-indicator[data-status="connected"] { + background: #27ae60; +} + +.status-indicator[data-status="disconnected"] { + background: #e74c3c; +} + +.status-indicator[data-status="checking"] { + background: #f39c12; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.sync-button, .auth-button { + background: #27ae60; + color: white; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + margin-bottom: 1rem; + transition: background-color 0.3s; +} + +.sync-button:hover, .auth-button:hover { + background: #229954; +} + +.auth-button { + background: #4285f4; +} + +.auth-button:hover { + background: #357ae8; +} + +.integration-info { + background: #f8f9fa; + padding: 1rem; + border-radius: 4px; + margin-top: 1rem; +} + +.integration-info ul { + margin-top: 0.5rem; + margin-left: 1.5rem; +} + +.integration-info li { + margin-bottom: 0.25rem; + color: #6c757d; +} + +.file-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 1rem; + margin-top: 1rem; +} + +.file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + margin-bottom: 0.5rem; + background: white; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.file-item:hover { + background: #f8f9fa; +} + +.file-icon { + margin-right: 0.5rem; +} + +.sync-frequency { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 1rem; +} + +.sync-settings-form label { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.sync-log { + background: #f8f9fa; + padding: 1rem; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + font-family: monospace; + font-size: 0.9rem; +} + +.sync-log-entry { + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #dee2e6; +} + +.sync-log-entry:last-child { + border-bottom: none; +} + +.sync-progress { + display: none; + margin-top: 1rem; +} + +.sync-progress.active { + display: block; +} + +.sync-progress-bar { + background: #e9ecef; + border-radius: 4px; + height: 8px; + overflow: hidden; +} + +.sync-progress-fill { + background: #3498db; + height: 100%; + transition: width 0.3s ease; +} + @media (max-width: 768px) { .container { padding: 10px; @@ -209,4 +372,9 @@ button:disabled { form { gap: 0.5rem; } + + .sync-frequency { + flex-direction: column; + align-items: flex-start; + } } \ No newline at end of file diff --git a/server.js b/server.js index 3e60877..f13afe6 100644 --- a/server.js +++ b/server.js @@ -4,11 +4,19 @@ const multer = require('multer'); const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs'); +const cors = require('cors'); +require('dotenv').config(); + +// Import integration services +const supabaseService = require('./services/supabase'); +const notionService = require('./services/notion'); +const googleDriveService = require('./services/googleDrive'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware +app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static('public')); @@ -221,9 +229,185 @@ app.post('/api/beats', (req, res) => { }); }); +// Integration Routes + +// Supabase sync endpoints +app.post('/api/sync/supabase/companies', async (req, res) => { + try { + const companies = await supabaseService.getCompanies(); + res.json({ success: true, data: companies }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/sync/supabase/novels', async (req, res) => { + try { + const novels = await supabaseService.getNovels(); + res.json({ success: true, data: novels }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Notion sync endpoints +app.post('/api/sync/notion/credit-memo', async (req, res) => { + const { memoData } = req.body; + try { + const pageId = await notionService.createCreditMemoPage(memoData); + res.json({ success: true, notionPageId: pageId }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/sync/notion/novel', async (req, res) => { + const { novelData } = req.body; + try { + const pageId = await notionService.createNovelPage(novelData); + res.json({ success: true, notionPageId: pageId }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Google Drive endpoints +app.get('/api/auth/google', (req, res) => { + const authUrl = googleDriveService.getAuthUrl(); + if (authUrl) { + res.json({ authUrl }); + } else { + res.status(400).json({ error: 'Google Drive not configured' }); + } +}); + +app.get('/auth/google/callback', async (req, res) => { + const { code } = req.query; + if (code) { + const tokens = await googleDriveService.getTokens(code); + if (tokens) { + res.redirect('/?google_auth=success'); + } else { + res.redirect('/?google_auth=failed'); + } + } else { + res.redirect('/?google_auth=failed'); + } +}); + +app.post('/api/drive/create-chapter-doc', async (req, res) => { + const { chapterData, folderId } = req.body; + try { + const doc = await googleDriveService.createChapterDocument(chapterData, folderId); + res.json({ success: true, document: doc }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.post('/api/drive/create-financial-sheet', async (req, res) => { + const { companyData, financialData } = req.body; + try { + const sheet = await googleDriveService.createFinancialSpreadsheet(companyData, financialData); + res.json({ success: true, spreadsheet: sheet }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/drive/files', async (req, res) => { + const { folderId } = req.query; + try { + const files = await googleDriveService.listFiles(folderId); + res.json({ success: true, files }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Combined sync endpoint +app.post('/api/sync/all', async (req, res) => { + const { type, data } = req.body; + const results = {}; + + try { + if (type === 'credit_memo') { + // Sync to Supabase + if (supabaseService.supabase) { + const supabaseResult = await supabaseService.createCreditMemo(data); + results.supabase = supabaseResult; + } + + // Sync to Notion + if (notionService.notion) { + const notionPageId = await notionService.createCreditMemoPage(data); + results.notion = notionPageId; + } + + // Create Google Sheet if financial data exists + if (googleDriveService.drive && data.financial_metrics) { + const sheet = await googleDriveService.createFinancialSpreadsheet( + { name: data.company_name, industry: data.industry }, + data.financial_metrics + ); + results.googleDrive = sheet; + } + } else if (type === 'novel') { + // Sync to Supabase + if (supabaseService.supabase) { + const supabaseResult = await supabaseService.createNovel(data); + results.supabase = supabaseResult; + } + + // Sync to Notion + if (notionService.notion) { + const notionPageId = await notionService.createNovelPage(data); + results.notion = notionPageId; + } + + // Create Google Drive folder for novel + if (googleDriveService.drive) { + const folder = await googleDriveService.createFolder(data.title); + results.googleDrive = folder; + } + } else if (type === 'chapter') { + // Sync to Supabase + if (supabaseService.supabase) { + const supabaseResult = await supabaseService.createChapter(data); + results.supabase = supabaseResult; + } + + // Update Notion + if (notionService.notion && data.notion_page_id) { + await notionService.updateChapterInNotion(data.notion_page_id, data); + results.notion = true; + } + + // Create Google Doc + if (googleDriveService.drive) { + const doc = await googleDriveService.createChapterDocument(data, data.folder_id); + results.googleDrive = doc; + } + } + + res.json({ success: true, results }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Initialize Google Drive tokens on startup +(async () => { + await googleDriveService.loadTokens(); +})(); + // Start server app.listen(PORT, () => { console.log(`Workspace server running on port ${PORT}`); + console.log('Integration services:'); + console.log('- Supabase:', supabaseService.supabase ? 'Connected' : 'Not configured'); + console.log('- Notion:', notionService.notion ? 'Connected' : 'Not configured'); + console.log('- Google Drive:', googleDriveService.oauth2Client ? 'Initialized' : 'Not configured'); }); // Graceful shutdown diff --git a/services/googleDrive.js b/services/googleDrive.js new file mode 100644 index 0000000..e78579a --- /dev/null +++ b/services/googleDrive.js @@ -0,0 +1,394 @@ +const { google } = require('googleapis'); +const fs = require('fs').promises; +const path = require('path'); +require('dotenv').config(); + +class GoogleDriveService { + constructor() { + this.drive = null; + this.docs = null; + this.sheets = null; + this.oauth2Client = null; + this.initializeClient(); + } + + initializeClient() { + if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) { + console.log('Google Drive credentials not configured. Skipping Google Drive initialization.'); + return; + } + + try { + this.oauth2Client = new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET, + process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/auth/google/callback' + ); + + // Initialize Google APIs + this.drive = google.drive({ version: 'v3', auth: this.oauth2Client }); + this.docs = google.docs({ version: 'v1', auth: this.oauth2Client }); + this.sheets = google.sheets({ version: 'v4', auth: this.oauth2Client }); + + console.log('Google Drive client initialized successfully'); + } catch (error) { + console.error('Error initializing Google Drive client:', error); + } + } + + // Generate authentication URL + getAuthUrl() { + if (!this.oauth2Client) return null; + + const scopes = [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/spreadsheets' + ]; + + return this.oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: scopes, + prompt: 'consent' + }); + } + + // Exchange authorization code for tokens + async getTokens(code) { + if (!this.oauth2Client) return null; + + try { + const { tokens } = await this.oauth2Client.getToken(code); + this.oauth2Client.setCredentials(tokens); + + // Save tokens to file for persistence + await this.saveTokens(tokens); + + return tokens; + } catch (error) { + console.error('Error getting tokens:', error); + return null; + } + } + + // Save tokens to file + async saveTokens(tokens) { + try { + const tokenPath = path.join(__dirname, '..', 'token.json'); + await fs.writeFile(tokenPath, JSON.stringify(tokens, null, 2)); + console.log('Tokens saved successfully'); + } catch (error) { + console.error('Error saving tokens:', error); + } + } + + // Load tokens from file + async loadTokens() { + try { + const tokenPath = path.join(__dirname, '..', 'token.json'); + const tokens = JSON.parse(await fs.readFile(tokenPath, 'utf8')); + this.oauth2Client.setCredentials(tokens); + console.log('Tokens loaded successfully'); + return true; + } catch (error) { + console.log('No saved tokens found'); + return false; + } + } + + // Create a folder in Google Drive + async createFolder(folderName, parentFolderId = null) { + if (!this.drive) return null; + + const fileMetadata = { + name: folderName, + mimeType: 'application/vnd.google-apps.folder' + }; + + if (parentFolderId) { + fileMetadata.parents = [parentFolderId]; + } else if (process.env.GOOGLE_DRIVE_FOLDER_ID) { + fileMetadata.parents = [process.env.GOOGLE_DRIVE_FOLDER_ID]; + } + + try { + const response = await this.drive.files.create({ + resource: fileMetadata, + fields: 'id, name, webViewLink' + }); + + return response.data; + } catch (error) { + console.error('Error creating folder:', error); + return null; + } + } + + // Upload a file to Google Drive + async uploadFile(filePath, fileName, mimeType, folderId = null) { + if (!this.drive) return null; + + const fileMetadata = { + name: fileName + }; + + if (folderId) { + fileMetadata.parents = [folderId]; + } else if (process.env.GOOGLE_DRIVE_FOLDER_ID) { + fileMetadata.parents = [process.env.GOOGLE_DRIVE_FOLDER_ID]; + } + + const media = { + mimeType: mimeType, + body: await fs.readFile(filePath) + }; + + try { + const response = await this.drive.files.create({ + resource: fileMetadata, + media: media, + fields: 'id, name, webViewLink' + }); + + return response.data; + } catch (error) { + console.error('Error uploading file:', error); + return null; + } + } + + // Create a Google Doc for a novel chapter + async createChapterDocument(chapterData, folderId = null) { + if (!this.docs || !this.drive) return null; + + try { + // Create the document + const createResponse = await this.docs.documents.create({ + requestBody: { + title: `Chapter ${chapterData.chapter_number}: ${chapterData.title}` + } + }); + + const documentId = createResponse.data.documentId; + + // Add content to the document + const requests = [ + { + insertText: { + location: { index: 1 }, + text: `Chapter ${chapterData.chapter_number}: ${chapterData.title}\n\n` + } + }, + { + updateParagraphStyle: { + range: { + startIndex: 1, + endIndex: `Chapter ${chapterData.chapter_number}: ${chapterData.title}`.length + 1 + }, + paragraphStyle: { + namedStyleType: 'HEADING_1' + }, + fields: 'namedStyleType' + } + }, + { + insertText: { + location: { index: `Chapter ${chapterData.chapter_number}: ${chapterData.title}\n\n`.length + 1 }, + text: `POV: ${chapterData.pov_character}\n\n${chapterData.summary || 'Chapter content goes here...'}` + } + } + ]; + + await this.docs.documents.batchUpdate({ + documentId: documentId, + requestBody: { requests } + }); + + // Move to folder if specified + if (folderId || process.env.GOOGLE_DRIVE_FOLDER_ID) { + await this.drive.files.update({ + fileId: documentId, + addParents: folderId || process.env.GOOGLE_DRIVE_FOLDER_ID, + fields: 'id, parents' + }); + } + + // Get the document link + const fileResponse = await this.drive.files.get({ + fileId: documentId, + fields: 'webViewLink' + }); + + return { + id: documentId, + webViewLink: fileResponse.data.webViewLink + }; + } catch (error) { + console.error('Error creating chapter document:', error); + return null; + } + } + + // Create a Google Sheet for financial data + async createFinancialSpreadsheet(companyData, financialData) { + if (!this.sheets || !this.drive) return null; + + try { + // Create the spreadsheet + const createResponse = await this.sheets.spreadsheets.create({ + requestBody: { + properties: { + title: `${companyData.name} - Financial Analysis` + }, + sheets: [ + { + properties: { + title: 'Overview' + } + }, + { + properties: { + title: 'Financial Statements' + } + }, + { + properties: { + title: 'Ratios' + } + } + ] + } + }); + + const spreadsheetId = createResponse.data.spreadsheetId; + + // Add data to the overview sheet + const overviewData = [ + ['Company Information'], + ['Name', companyData.name], + ['Industry', companyData.industry || 'N/A'], + ['Analysis Date', new Date().toLocaleDateString()], + [''], + ['Key Metrics'], + ['Revenue', financialData.revenue || 'N/A'], + ['EBITDA', financialData.ebitda || 'N/A'], + ['Net Income', financialData.netIncome || 'N/A'], + ['Total Assets', financialData.totalAssets || 'N/A'], + ['Total Liabilities', financialData.totalLiabilities || 'N/A'] + ]; + + await this.sheets.spreadsheets.values.update({ + spreadsheetId: spreadsheetId, + range: 'Overview!A1', + valueInputOption: 'RAW', + requestBody: { + values: overviewData + } + }); + + // Move to folder if specified + if (process.env.GOOGLE_DRIVE_FOLDER_ID) { + await this.drive.files.update({ + fileId: spreadsheetId, + addParents: process.env.GOOGLE_DRIVE_FOLDER_ID, + fields: 'id, parents' + }); + } + + // Get the spreadsheet link + const fileResponse = await this.drive.files.get({ + fileId: spreadsheetId, + fields: 'webViewLink' + }); + + return { + id: spreadsheetId, + webViewLink: fileResponse.data.webViewLink + }; + } catch (error) { + console.error('Error creating financial spreadsheet:', error); + return null; + } + } + + // List files in a folder + async listFiles(folderId = null) { + if (!this.drive) return []; + + try { + const query = folderId + ? `'${folderId}' in parents and trashed = false` + : `'${process.env.GOOGLE_DRIVE_FOLDER_ID || 'root'}' in parents and trashed = false`; + + const response = await this.drive.files.list({ + q: query, + fields: 'files(id, name, mimeType, webViewLink, createdTime, modifiedTime)', + orderBy: 'modifiedTime desc' + }); + + return response.data.files; + } catch (error) { + console.error('Error listing files:', error); + return []; + } + } + + // Download a file from Google Drive + async downloadFile(fileId, destPath) { + if (!this.drive) return false; + + try { + const response = await this.drive.files.get( + { fileId: fileId, alt: 'media' }, + { responseType: 'stream' } + ); + + const dest = fs.createWriteStream(destPath); + response.data.pipe(dest); + + return new Promise((resolve, reject) => { + dest.on('finish', () => resolve(true)); + dest.on('error', reject); + }); + } catch (error) { + console.error('Error downloading file:', error); + return false; + } + } + + // Delete a file from Google Drive + async deleteFile(fileId) { + if (!this.drive) return false; + + try { + await this.drive.files.delete({ fileId: fileId }); + return true; + } catch (error) { + console.error('Error deleting file:', error); + return false; + } + } + + // Share a file + async shareFile(fileId, email, role = 'reader') { + if (!this.drive) return false; + + try { + await this.drive.permissions.create({ + fileId: fileId, + requestBody: { + type: 'user', + role: role, + emailAddress: email + } + }); + + return true; + } catch (error) { + console.error('Error sharing file:', error); + return false; + } + } +} + +module.exports = new GoogleDriveService(); \ No newline at end of file diff --git a/services/notion.js b/services/notion.js new file mode 100644 index 0000000..e958cf2 --- /dev/null +++ b/services/notion.js @@ -0,0 +1,415 @@ +const { Client } = require('@notionhq/client'); +require('dotenv').config(); + +class NotionService { + constructor() { + this.notion = null; + this.initializeClient(); + } + + initializeClient() { + if (!process.env.NOTION_API_KEY) { + console.log('Notion API key not configured. Skipping Notion initialization.'); + return; + } + + try { + this.notion = new Client({ + auth: process.env.NOTION_API_KEY, + }); + console.log('Notion client initialized successfully'); + } catch (error) { + console.error('Error initializing Notion client:', error); + } + } + + // Create a credit memo page in Notion + async createCreditMemoPage(memoData) { + if (!this.notion || !process.env.NOTION_DATABASE_ID_CREDIT) return null; + + try { + const response = await this.notion.pages.create({ + parent: { database_id: process.env.NOTION_DATABASE_ID_CREDIT }, + properties: { + 'Title': { + title: [ + { + text: { + content: memoData.title || 'Untitled Memo' + } + } + ] + }, + 'Company': { + rich_text: [ + { + text: { + content: memoData.company_name || '' + } + } + ] + }, + 'Memo Type': { + select: { + name: memoData.memo_type || 'General' + } + }, + 'Industry': { + rich_text: [ + { + text: { + content: memoData.industry || '' + } + } + ] + }, + 'Date': { + date: { + start: new Date().toISOString().split('T')[0] + } + } + }, + children: [ + { + object: 'block', + type: 'heading_1', + heading_1: { + rich_text: [ + { + type: 'text', + text: { + content: 'Credit Analysis Memo' + } + } + ] + } + }, + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: memoData.content || '' + } + } + ] + } + }, + { + object: 'block', + type: 'heading_2', + heading_2: { + rich_text: [ + { + type: 'text', + text: { + content: 'Financial Metrics' + } + } + ] + } + }, + { + object: 'block', + type: 'code', + code: { + rich_text: [ + { + type: 'text', + text: { + content: JSON.stringify(memoData.financial_metrics || {}, null, 2) + } + } + ], + language: 'json' + } + } + ] + }); + + return response.id; + } catch (error) { + console.error('Error creating Notion credit memo page:', error); + return null; + } + } + + // Create a novel project page in Notion + async createNovelPage(novelData) { + if (!this.notion || !process.env.NOTION_DATABASE_ID_NOVELS) return null; + + try { + const response = await this.notion.pages.create({ + parent: { database_id: process.env.NOTION_DATABASE_ID_NOVELS }, + properties: { + 'Title': { + title: [ + { + text: { + content: novelData.title || 'Untitled Novel' + } + } + ] + }, + 'POV Style': { + select: { + name: novelData.pov_style || 'dual_alternating' + } + }, + 'Tense': { + select: { + name: novelData.tense || 'past' + } + }, + 'Target Chapters': { + number: novelData.target_chapters || 25 + }, + 'Target Beats': { + number: novelData.target_beats || 250 + }, + 'Status': { + select: { + name: 'Planning' + } + } + }, + children: [ + { + object: 'block', + type: 'heading_1', + heading_1: { + rich_text: [ + { + type: 'text', + text: { + content: novelData.title || 'Novel Project' + } + } + ] + } + }, + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: novelData.description || '' + } + } + ] + } + }, + { + object: 'block', + type: 'divider', + divider: {} + }, + { + object: 'block', + type: 'heading_2', + heading_2: { + rich_text: [ + { + type: 'text', + text: { + content: 'Chapter Outline' + } + } + ] + } + }, + { + object: 'block', + type: 'to_do', + to_do: { + rich_text: [ + { + type: 'text', + text: { + content: 'Create chapter structure' + } + } + ], + checked: false + } + } + ] + }); + + return response.id; + } catch (error) { + console.error('Error creating Notion novel page:', error); + return null; + } + } + + // Update a chapter in Notion + async updateChapterInNotion(pageId, chapterData) { + if (!this.notion || !pageId) return null; + + try { + // Append chapter information to the novel page + await this.notion.blocks.children.append({ + block_id: pageId, + children: [ + { + object: 'block', + type: 'heading_3', + heading_3: { + rich_text: [ + { + type: 'text', + text: { + content: `Chapter ${chapterData.chapter_number}: ${chapterData.title}` + } + } + ] + } + }, + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: `POV: ${chapterData.pov_character}` + } + } + ] + } + }, + { + object: 'block', + type: 'quote', + quote: { + rich_text: [ + { + type: 'text', + text: { + content: chapterData.summary || 'No summary provided' + } + } + ] + } + } + ] + }); + + return true; + } catch (error) { + console.error('Error updating chapter in Notion:', error); + return false; + } + } + + // Add story beat to Notion + async addStoryBeatToNotion(pageId, beatData) { + if (!this.notion || !pageId) return null; + + try { + await this.notion.blocks.children.append({ + block_id: pageId, + children: [ + { + object: 'block', + type: 'bulleted_list_item', + bulleted_list_item: { + rich_text: [ + { + type: 'text', + text: { + content: `Beat ${beatData.beat_number} (${beatData.beat_type}): ${beatData.description}`, + annotations: { + bold: beatData.beat_type === 'climax' + } + } + } + ] + } + } + ] + }); + + return true; + } catch (error) { + console.error('Error adding story beat to Notion:', error); + return false; + } + } + + // Search for pages in Notion + async searchPages(query) { + if (!this.notion) return []; + + try { + const response = await this.notion.search({ + query: query, + filter: { + property: 'object', + value: 'page' + } + }); + + return response.results; + } catch (error) { + console.error('Error searching Notion pages:', error); + return []; + } + } + + // Get database schema + async getDatabaseSchema(databaseId) { + if (!this.notion) return null; + + try { + const response = await this.notion.databases.retrieve({ + database_id: databaseId + }); + + return response; + } catch (error) { + console.error('Error getting database schema:', error); + return null; + } + } + + // Sync all credit memos to Notion + async syncCreditMemosToNotion(memos) { + if (!this.notion) return; + + const results = []; + for (const memo of memos) { + if (!memo.notion_page_id) { + const pageId = await this.createCreditMemoPage(memo); + results.push({ memoId: memo.id, notionPageId: pageId }); + } + } + + return results; + } + + // Sync all novels to Notion + async syncNovelsToNotion(novels) { + if (!this.notion) return; + + const results = []; + for (const novel of novels) { + if (!novel.notion_page_id) { + const pageId = await this.createNovelPage(novel); + results.push({ novelId: novel.id, notionPageId: pageId }); + } + } + + return results; + } +} + +module.exports = new NotionService(); \ No newline at end of file diff --git a/services/supabase.js b/services/supabase.js new file mode 100644 index 0000000..24bc88c --- /dev/null +++ b/services/supabase.js @@ -0,0 +1,339 @@ +const { createClient } = require('@supabase/supabase-js'); +require('dotenv').config(); + +class SupabaseService { + constructor() { + this.supabase = null; + this.initializeClient(); + } + + initializeClient() { + if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) { + console.log('Supabase credentials not configured. Skipping Supabase initialization.'); + return; + } + + try { + this.supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY, + { + auth: { + persistSession: false + } + } + ); + console.log('Supabase client initialized successfully'); + } catch (error) { + console.error('Error initializing Supabase client:', error); + } + } + + async createTables() { + if (!this.supabase) return; + + // Note: These tables should ideally be created through Supabase dashboard + // This is just for reference of the schema + const tables = { + companies: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + name text NOT NULL, + industry text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() + `, + financial_data: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + company_id uuid REFERENCES companies(id), + document_type text, + file_url text, + extracted_data jsonb, + upload_date timestamp with time zone DEFAULT now() + `, + credit_memos: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + company_id uuid REFERENCES companies(id), + memo_type text, + title text, + content text, + financial_metrics jsonb, + notion_page_id text, + created_at timestamp with time zone DEFAULT now() + `, + novels: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + title text NOT NULL, + description text, + pov_style text DEFAULT 'dual_alternating', + tense text DEFAULT 'past', + target_chapters integer DEFAULT 25, + target_beats integer DEFAULT 250, + notion_page_id text, + created_at timestamp with time zone DEFAULT now() + `, + chapters: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + novel_id uuid REFERENCES novels(id), + chapter_number integer, + title text, + pov_character text, + summary text, + word_count integer DEFAULT 0, + google_doc_id text, + created_at timestamp with time zone DEFAULT now() + `, + story_beats: ` + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + novel_id uuid REFERENCES novels(id), + chapter_id uuid REFERENCES chapters(id), + beat_number integer, + description text, + beat_type text, + pov_character text, + created_at timestamp with time zone DEFAULT now() + ` + }; + + console.log('Tables should be created in Supabase dashboard with the provided schema'); + } + + // Company operations + async getCompanies() { + if (!this.supabase) return []; + + const { data, error } = await this.supabase + .from('companies') + .select('*') + .order('name'); + + if (error) { + console.error('Error fetching companies:', error); + return []; + } + return data; + } + + async createCompany(company) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('companies') + .insert([company]) + .select() + .single(); + + if (error) { + console.error('Error creating company:', error); + return null; + } + return data; + } + + // Financial data operations + async uploadFinancialData(financialData) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('financial_data') + .insert([financialData]) + .select() + .single(); + + if (error) { + console.error('Error uploading financial data:', error); + return null; + } + return data; + } + + // Credit memo operations + async createCreditMemo(memo) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('credit_memos') + .insert([memo]) + .select() + .single(); + + if (error) { + console.error('Error creating credit memo:', error); + return null; + } + return data; + } + + async getCreditMemos(companyId = null) { + if (!this.supabase) return []; + + let query = this.supabase.from('credit_memos').select('*'); + + if (companyId) { + query = query.eq('company_id', companyId); + } + + const { data, error } = await query.order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching credit memos:', error); + return []; + } + return data; + } + + // Novel operations + async getNovels() { + if (!this.supabase) return []; + + const { data, error } = await this.supabase + .from('novels') + .select('*') + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching novels:', error); + return []; + } + return data; + } + + async createNovel(novel) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('novels') + .insert([novel]) + .select() + .single(); + + if (error) { + console.error('Error creating novel:', error); + return null; + } + return data; + } + + // Chapter operations + async getChapters(novelId) { + if (!this.supabase) return []; + + const { data, error } = await this.supabase + .from('chapters') + .select('*') + .eq('novel_id', novelId) + .order('chapter_number'); + + if (error) { + console.error('Error fetching chapters:', error); + return []; + } + return data; + } + + async createChapter(chapter) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('chapters') + .insert([chapter]) + .select() + .single(); + + if (error) { + console.error('Error creating chapter:', error); + return null; + } + return data; + } + + // Story beats operations + async getStoryBeats(novelId) { + if (!this.supabase) return []; + + const { data, error } = await this.supabase + .from('story_beats') + .select('*') + .eq('novel_id', novelId) + .order('beat_number'); + + if (error) { + console.error('Error fetching story beats:', error); + return []; + } + return data; + } + + async createStoryBeat(beat) { + if (!this.supabase) return null; + + const { data, error } = await this.supabase + .from('story_beats') + .insert([beat]) + .select() + .single(); + + if (error) { + console.error('Error creating story beat:', error); + return null; + } + return data; + } + + // Real-time subscriptions + subscribeToCompanies(callback) { + if (!this.supabase) return null; + + return this.supabase + .channel('companies-changes') + .on('postgres_changes', { event: '*', schema: 'public', table: 'companies' }, callback) + .subscribe(); + } + + subscribeToNovels(callback) { + if (!this.supabase) return null; + + return this.supabase + .channel('novels-changes') + .on('postgres_changes', { event: '*', schema: 'public', table: 'novels' }, callback) + .subscribe(); + } + + // Authentication + async signUp(email, password) { + if (!this.supabase) return { error: 'Supabase not configured' }; + + const { data, error } = await this.supabase.auth.signUp({ + email, + password + }); + + return { data, error }; + } + + async signIn(email, password) { + if (!this.supabase) return { error: 'Supabase not configured' }; + + const { data, error } = await this.supabase.auth.signInWithPassword({ + email, + password + }); + + return { data, error }; + } + + async signOut() { + if (!this.supabase) return { error: 'Supabase not configured' }; + + const { error } = await this.supabase.auth.signOut(); + return { error }; + } + + async getSession() { + if (!this.supabase) return null; + + const { data: { session } } = await this.supabase.auth.getSession(); + return session; + } +} + +module.exports = new SupabaseService(); \ No newline at end of file diff --git a/setup.js b/setup.js new file mode 100644 index 0000000..f148089 --- /dev/null +++ b/setup.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +const question = (query) => new Promise(resolve => rl.question(query, resolve)); + +async function setup() { + console.log('\n================================='); + console.log('Workspace Integration Setup'); + console.log('=================================\n'); + + const config = {}; + + // Supabase Configuration + console.log('--- Supabase Configuration ---'); + const setupSupabase = await question('Do you want to set up Supabase? (y/n): '); + if (setupSupabase.toLowerCase() === 'y') { + config.SUPABASE_URL = await question('Enter your Supabase project URL: '); + config.SUPABASE_ANON_KEY = await question('Enter your Supabase anon key: '); + config.SUPABASE_SERVICE_KEY = await question('Enter your Supabase service key (optional): '); + } + + // Notion Configuration + console.log('\n--- Notion Configuration ---'); + const setupNotion = await question('Do you want to set up Notion? (y/n): '); + if (setupNotion.toLowerCase() === 'y') { + config.NOTION_API_KEY = await question('Enter your Notion integration token: '); + config.NOTION_DATABASE_ID_CREDIT = await question('Enter Notion database ID for credit memos: '); + config.NOTION_DATABASE_ID_NOVELS = await question('Enter Notion database ID for novels: '); + } + + // Google Drive Configuration + console.log('\n--- Google Drive Configuration ---'); + const setupGoogle = await question('Do you want to set up Google Drive? (y/n): '); + if (setupGoogle.toLowerCase() === 'y') { + config.GOOGLE_CLIENT_ID = await question('Enter your Google Client ID: '); + config.GOOGLE_CLIENT_SECRET = await question('Enter your Google Client Secret: '); + config.GOOGLE_REDIRECT_URI = await question('Enter redirect URI (default: http://localhost:3000/auth/google/callback): ') || 'http://localhost:3000/auth/google/callback'; + config.GOOGLE_DRIVE_FOLDER_ID = await question('Enter Google Drive folder ID (optional): '); + } + + // JWT and Server Configuration + console.log('\n--- Server Configuration ---'); + config.JWT_SECRET = await question('Enter a JWT secret key (press enter to generate): ') || generateSecret(); + config.PORT = await question('Enter server port (default: 3000): ') || '3000'; + config.NODE_ENV = 'development'; + + // Write .env file + const envContent = Object.entries(config) + .filter(([key, value]) => value) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + fs.writeFileSync(path.join(__dirname, '.env'), envContent); + + console.log('\n✅ Configuration saved to .env file'); + console.log('\nNext steps:'); + console.log('1. For Supabase: Create the required tables in your Supabase dashboard'); + console.log('2. For Notion: Create databases with the appropriate properties'); + console.log('3. For Google Drive: Enable Drive API in Google Cloud Console'); + console.log('\nRun "npm start" to launch the application\n'); + + rl.close(); +} + +function generateSecret() { + return require('crypto').randomBytes(32).toString('hex'); +} + +setup().catch(console.error); \ No newline at end of file