├── .example.env ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── auto_assign.yml ├── dependabot.yml ├── pull_request_template.md ├── stale.yml └── workflows │ ├── auto-assign.yml │ ├── build-zip.yml │ ├── e2e.yml │ ├── greetings.yml │ └── prettier.yml ├── .gitignore ├── .husky └── pre-committ ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── bash-scripts ├── copy_env.sh ├── set_global_env.sh └── update_version.sh ├── chrome-extension ├── manifest.ts ├── package.json ├── pre-build.tsconfig.json ├── public │ ├── Cover1.jpg │ ├── Cover2.png │ ├── Cover3.jpg │ ├── content.css │ ├── dragDropListener.js │ ├── icon-128.png │ └── icon-34.png ├── src │ ├── background │ │ └── index.ts │ └── mcpclient │ │ ├── mcpinterfaceToContentScript.ts │ │ └── officialmcpclient.ts ├── tsconfig.json ├── utils │ ├── analytics.ts │ └── plugins │ │ └── make-manifest-plugin.ts └── vite.config.mts ├── eslint.config.ts ├── package.json ├── packages ├── dev-utils │ ├── index.mts │ ├── lib │ │ ├── logger.ts │ │ └── manifest-parser │ │ │ ├── impl.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── package.json │ └── tsconfig.json ├── env │ ├── README.md │ ├── img.png │ ├── index.mts │ ├── lib │ │ ├── const.ts │ │ ├── index.ts │ │ └── types.ts │ ├── package.json │ └── tsconfig.json ├── hmr │ ├── index.mts │ ├── lib │ │ ├── consts.ts │ │ ├── initializers │ │ │ ├── initClient.ts │ │ │ └── initReloadServer.ts │ │ ├── injections │ │ │ ├── refresh.ts │ │ │ └── reload.ts │ │ ├── interpreter │ │ │ └── index.ts │ │ ├── plugins │ │ │ ├── index.ts │ │ │ ├── make-entry-point-plugin.ts │ │ │ ├── watch-public-plugin.ts │ │ │ └── watch-rebuild-plugin.ts │ │ └── types.ts │ ├── package.json │ ├── rollup.config.ts │ └── tsconfig.json ├── i18n │ ├── .gitignore │ ├── README.md │ ├── index.mts │ ├── lib │ │ ├── consts.ts │ │ ├── i18n-dev.ts │ │ ├── i18n-prod.ts │ │ ├── index.ts │ │ ├── prepare_build.ts │ │ ├── set_related_locale_import.ts │ │ └── types.ts │ ├── locales │ │ ├── en │ │ │ └── messages.json │ │ └── ko │ │ │ └── messages.json │ ├── package.json │ ├── prepare-build.tsconfig.json │ └── tsconfig.json ├── module-manager │ ├── README.md │ ├── index.mts │ ├── lib │ │ ├── deleteModules.ts │ │ ├── recoverModules.ts │ │ └── runModuleManager.ts │ ├── package.json │ └── tsconfig.json ├── shared │ ├── README.md │ ├── index.mts │ ├── lib │ │ ├── hoc │ │ │ ├── index.ts │ │ │ ├── withErrorBoundary.tsx │ │ │ └── withSuspense.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useStorage.tsx │ │ └── utils │ │ │ ├── index.ts │ │ │ └── shared-types.ts │ ├── package.json │ ├── src │ │ └── types │ │ │ └── toolCall.ts │ └── tsconfig.json ├── storage │ ├── index.mts │ ├── lib │ │ ├── base │ │ │ ├── base.ts │ │ │ ├── enums.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── impl │ │ │ ├── exampleThemeStorage.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── package.json │ └── tsconfig.json ├── tailwind-config │ ├── package.json │ └── tailwind.config.ts ├── tsconfig │ ├── app.json │ ├── base.json │ ├── module.json │ └── package.json ├── ui │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── components │ │ │ ├── ToggleButton.tsx │ │ │ └── index.ts │ │ ├── global.css │ │ ├── index.ts │ │ ├── utils.ts │ │ └── withUI.ts │ ├── package.json │ └── tsconfig.json ├── vite-config │ ├── index.mts │ ├── lib │ │ ├── index.ts │ │ └── withPageConfig.ts │ ├── package.json │ └── tsconfig.json └── zipper │ ├── index.mts │ ├── lib │ └── index.ts │ ├── package.json │ └── tsconfig.json ├── pages └── content │ ├── components.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── logo.svg │ ├── src │ ├── adapters │ │ ├── adapterRegistry.ts │ │ ├── adaptercomponents │ │ │ ├── aistudio.ts │ │ │ ├── chatgpt.ts │ │ │ ├── common.ts │ │ │ ├── deepseek.ts │ │ │ ├── gemini.ts │ │ │ ├── grok.ts │ │ │ ├── index.ts │ │ │ ├── kagi.ts │ │ │ ├── openrouter.ts │ │ │ ├── perplexity.ts │ │ │ └── t3chat.ts │ │ ├── aistudioAdapter.ts │ │ ├── chatgptAdapter.ts │ │ ├── common │ │ │ ├── baseAdapter.ts │ │ │ └── index.ts │ │ ├── deepseekAdapter.ts │ │ ├── geminiAdapter.ts │ │ ├── grokAdapter.ts │ │ ├── index.ts │ │ ├── kagiAdapter.ts │ │ ├── openrouterAdapter.ts │ │ ├── perplexityAdapter.ts │ │ └── t3chatAdapter.ts │ ├── components │ │ ├── mcpPopover │ │ │ ├── PopoverPortal.tsx │ │ │ └── mcpPopover.tsx │ │ ├── sidebar │ │ │ ├── AvailableTools │ │ │ │ └── AvailableTools.tsx │ │ │ ├── InputArea │ │ │ │ └── InputArea.tsx │ │ │ ├── Instructions │ │ │ │ ├── InstructionManager.tsx │ │ │ │ ├── README.md │ │ │ │ ├── fixed_schema_test.ts │ │ │ │ ├── instructionGenerator.ts │ │ │ │ ├── schema_converter.ts │ │ │ │ ├── schema_test.ts │ │ │ │ ├── scheme_converstion_guide.md │ │ │ │ └── website_specific_instruction │ │ │ │ │ ├── chatgpt.ts │ │ │ │ │ └── gemini.ts │ │ │ ├── ServerStatus │ │ │ │ └── ServerStatus.tsx │ │ │ ├── Sidebar.tsx │ │ │ ├── SidebarManager.tsx │ │ │ ├── base │ │ │ │ └── BaseSidebarManager.tsx │ │ │ ├── components │ │ │ │ └── Toggle.tsx │ │ │ ├── hooks │ │ │ │ └── backgroundCommunication.ts │ │ │ ├── index.ts │ │ │ ├── styles │ │ │ │ └── sidebar.css │ │ │ └── ui │ │ │ │ ├── Icon.tsx │ │ │ │ ├── ResizeHandle.tsx │ │ │ │ ├── Toggle.tsx │ │ │ │ ├── ToggleWithoutLabel.tsx │ │ │ │ ├── Typography.tsx │ │ │ │ └── index.ts │ │ ├── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ └── dialog.tsx │ │ └── websites │ │ │ ├── aistudio │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── chatgpt │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── deepseek │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── gemini │ │ │ ├── README.md │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── grok │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── kagi │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── openrouter │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ ├── perplexity │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ │ │ └── t3chat │ │ │ ├── chatInputHandler.ts │ │ │ └── index.ts │ ├── hooks │ │ └── useShadowDomStyles.ts │ ├── index.ts │ ├── lib │ │ └── utils.ts │ ├── render_prescript │ │ ├── .gitignore │ │ ├── filestructure.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── prescript.js │ │ ├── rollup.config.js │ │ ├── src │ │ │ ├── core │ │ │ │ ├── config.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── mcpexecute │ │ │ │ └── storage.ts │ │ │ ├── observer │ │ │ │ ├── functionResultObserver.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mutationObserver.ts │ │ │ │ ├── stalledStreamHandler.ts │ │ │ │ └── streamObserver.ts │ │ │ ├── parser │ │ │ │ ├── functionParser.ts │ │ │ │ ├── index.ts │ │ │ │ ├── languageParser.ts │ │ │ │ └── parameterParser.ts │ │ │ ├── renderer │ │ │ │ ├── components.ts │ │ │ │ ├── functionBlock.ts │ │ │ │ ├── functionHistory.ts │ │ │ │ ├── functionResult.ts │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ └── utils │ │ │ │ ├── dom.ts │ │ │ │ ├── index.ts │ │ │ │ ├── performance.ts │ │ │ │ └── themeDetector.ts │ │ └── tsconfig.json │ ├── tailwind-input.css │ ├── types │ │ ├── README.md │ │ └── mcp.ts │ └── utils │ │ ├── helpers.ts │ │ ├── mcpHandler.ts │ │ ├── performanceMonitor.ts │ │ ├── shadowDom.ts │ │ ├── siteAdapter.ts │ │ ├── storage.ts │ │ └── toolExecutionStorage.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.mts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.example.env: -------------------------------------------------------------------------------- 1 | # THOSE VALUES ARE EDITABLE ONLY VIA CLI 2 | CLI_CEB_DEV=true 3 | CLI_CEB_FIREFOX=false 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @srbhptl39 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: srbhptl39 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: srbhptl39 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - OS Version: 29 | - Browser: 30 | - Browser Version: 31 | - Node Version: 32 | - Other Necessary Packages Version: 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: srbhptl39 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: author 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - srbhptl39 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 0 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | # - assigneeA 18 | 19 | # A number of assignees to add to the pull request 20 | # Set to 0 to add all of the assignees. 21 | # Uses numberOfReviewers if unset. 22 | # numberOfAssignees: 2 23 | 24 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 25 | # skipKeywords: 26 | # - wip 27 | 28 | filterLabels: 29 | exclude: 30 | - dependencies 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > `*` denotes required fields 4 | 5 | ## Priority* 6 | 7 | - [ ] High: This PR needs to be merged first, before other tasks. 8 | - [x] Medium: This PR should be merged quickly to prevent conflicts due to common changes. (default) 9 | - [ ] Low: This PR does not affect other tasks, so it can be merged later. 10 | 11 | ## Purpose of the PR* 12 | 13 | 14 | ## Changes* 15 | 16 | 17 | ## How to check the feature 18 | 19 | 20 | 21 | 22 | ## Reference 23 | 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an Issue or Pull Request becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale Issue or Pull Request is closed 4 | daysUntilClose: 30 5 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking as stale 10 | staleLabel: stale 11 | # Comment to post when marking as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when removing the stale label. Set to `false` to disable 17 | unmarkComment: false 18 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 19 | closeComment: true 20 | # Limit to only `issues` or `pulls` 21 | only: issues 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | jobs: 7 | add-reviews: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v1.2.5 11 | with: 12 | configuration-path: '.github/auto_assign.yml' 13 | -------------------------------------------------------------------------------- /.github/workflows/build-zip.yml: -------------------------------------------------------------------------------- 1 | name: Build And Upload Extension Zip Via Artifact 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: pnpm 20 | 21 | - run: pnpm install --frozen-lockfile --prefer-offline 22 | 23 | - run: pnpm build 24 | 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | path: dist/* 28 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: Run E2E Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | branches: [ main, dev ] 8 | 9 | jobs: 10 | chrome: 11 | name: E2E tests for Chrome 12 | runs-on: ubuntu-latest 13 | env: 14 | CEB_CI: true 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version-file: '.nvmrc' 21 | cache: pnpm 22 | - run: pnpm install --frozen-lockfile --prefer-offline 23 | - run: pnpm e2e 24 | 25 | firefox: 26 | name: E2E tests for Firefox 27 | runs-on: ubuntu-latest 28 | env: 29 | CEB_CI: true 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: pnpm/action-setup@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version-file: '.nvmrc' 36 | cache: pnpm 37 | - run: pnpm install --frozen-lockfile --prefer-offline 38 | - run: pnpm e2e:firefox 39 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' 16 | pr-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' 17 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Formating validation 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ main, dev ] 7 | 8 | jobs: 9 | prettier: 10 | name: Prettier Check 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Run Prettier 17 | id: prettier-run 18 | uses: rutajdash/prettier-cli-action@v1.0.0 19 | with: 20 | config_path: ./.prettierrc 21 | file_pattern: "*.{js,jsx,ts,tsx,json}" 22 | 23 | # This step only runs if prettier finds errors causing the previous step to fail 24 | # This steps lists the files where errors were found 25 | - name: Prettier Output 26 | if: ${{ failure() }} 27 | shell: bash 28 | run: | 29 | echo "The following files aren't formatted properly:" 30 | echo "${{steps.prettier-run.outputs.prettier_output}}" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules 3 | 4 | # testing 5 | **/coverage 6 | 7 | # build 8 | **/dist 9 | **/build 10 | **/dist-zip 11 | chrome-extension/manifest.js 12 | chrome-extension/pre-build.tsconfig.tsbuildinfo 13 | eslint.config.js 14 | tsconfig.tsbuildinfo 15 | 16 | # env 17 | **/.env.* 18 | **/.env 19 | 20 | # etc 21 | .DS_Store 22 | .idea 23 | **/.turbo 24 | **/.gitt 25 | # compiled 26 | **/tailwind-output.css 27 | 28 | # trash 29 | .trash 30 | .trash/* 31 | mcp-superassistant-proxy/bun.lock 32 | # pnpm-lock.yaml 33 | -------------------------------------------------------------------------------- /.husky/pre-committ: -------------------------------------------------------------------------------- 1 | pnpm dlx lint-staged --allow-empty 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=@testing-library/dom 2 | engine-strict=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .gitignore 4 | .github 5 | .husky 6 | .nvmrc 7 | .prettierignore 8 | LICENSE 9 | *.md 10 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "printWidth": 120, 7 | "bracketSameLine": true, 8 | "htmlWhitespaceSensitivity": "strict" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Saurabh Patel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bash-scripts/copy_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if .env does not exist and .example.env exists 4 | if [ ! -f ".env" ] && [ -f ".example.env" ]; then 5 | # Copy .example.env to .env 6 | cp .example.env .env 7 | echo ".example.env has been copied to .env" 8 | fi 9 | -------------------------------------------------------------------------------- /bash-scripts/set_global_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Default values 4 | CLI_CEB_DEV=false 5 | CLI_CEB_FIREFOX=false 6 | 7 | validate_is_boolean() { 8 | if [[ "$1" != "true" && "$1" != "false" ]]; then 9 | echo "Invalid value for <$2>. Please use 'true' or 'false'." 10 | exit 1 11 | fi 12 | } 13 | 14 | # Validate if a key starts with CLI_CEB_ or CEB_ 15 | validate_key() { 16 | local key="$1" 17 | local is_editable_section="${2:-false}" 18 | 19 | if [[ -n "$key" && ! "$key" =~ ^# ]]; then 20 | if [[ "$is_editable_section" == true && ! "$key" =~ ^CEB_ ]]; then 21 | echo "Invalid key: <$key>. All keys in the editable section must start with 'CEB_'." 22 | exit 1 23 | elif [[ "$is_editable_section" == false && ! "$key" =~ ^CLI_CEB_ ]]; then 24 | echo "Invalid key: <$key>. All CLI keys must start with 'CLI_CEB_'." 25 | exit 1 26 | fi 27 | fi 28 | } 29 | 30 | parse_arguments() { 31 | for arg in "$@" 32 | do 33 | key="${arg%%=*}" 34 | value="${arg#*=}" 35 | 36 | validate_key "$key" 37 | 38 | case $key in 39 | CLI_CEB_DEV) 40 | CLI_CEB_DEV="$value" 41 | validate_is_boolean "$CLI_CEB_DEV" "CLI_CEB_DEV" 42 | ;; 43 | CLI_CEB_FIREFOX) 44 | CLI_CEB_FIREFOX="$value" 45 | validate_is_boolean "$CLI_CEB_FIREFOX" "CLI_CEB_FIREFOX" 46 | ;; 47 | *) 48 | cli_values+=("$key=$value") 49 | ;; 50 | esac 51 | done 52 | } 53 | 54 | # Validate keys in .env file 55 | validate_env_keys() { 56 | editable_section_starts=false 57 | 58 | while IFS= read -r line; do 59 | key="${line%%=*}" 60 | if [[ "$key" =~ ^CLI_CEB_ ]]; then 61 | editable_section_starts=true 62 | elif $editable_section_starts; then 63 | validate_key "$key" true 64 | fi 65 | done < .env 66 | } 67 | 68 | create_new_file() { 69 | temp_file=$(mktemp) 70 | 71 | { 72 | echo "# THOSE VALUES ARE EDITABLE ONLY VIA CLI" 73 | echo "CLI_CEB_DEV=$CLI_CEB_DEV" 74 | echo "CLI_CEB_FIREFOX=$CLI_CEB_FIREFOX" 75 | for value in "${cli_values[@]}"; do 76 | echo "$value" 77 | done 78 | echo "" 79 | echo "# THOSE VALUES ARE EDITABLE" 80 | 81 | # Copy existing env values, without CLI section 82 | grep -E '^CEB_' .env 83 | } > "$temp_file" 84 | 85 | mv "$temp_file" .env 86 | } 87 | 88 | # Main script execution 89 | parse_arguments "$@" 90 | validate_env_keys 91 | create_new_file 92 | -------------------------------------------------------------------------------- /bash-scripts/update_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ./update_version.sh 3 | # FORMAT IS <0.0.0> 4 | 5 | if [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 6 | find . -name 'package.json' -not -path '*/node_modules/*' -exec bash -c ' 7 | # Parse the version from package.json 8 | current_version=$(grep -o "\"version\": \"[^\"]*" "$0" | cut -d"\"" -f4) 9 | 10 | # Update the version 11 | perl -i -pe"s/$current_version/'$1'/" "$0" 12 | ' {} \; 13 | 14 | echo "Updated versions to $1"; 15 | else 16 | echo "Version format <$1> isn't correct, proper format is <0.0.0>"; 17 | fi 18 | -------------------------------------------------------------------------------- /chrome-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-extension", 3 | "version": "0.3.1", 4 | "description": "chrome extension - core settings", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "scripts": { 9 | "clean:node_modules": "pnpm dlx rimraf node_modules", 10 | "clean:turbo": "rimraf .turbo", 11 | "clean": "pnpm clean:turbo && pnpm clean:node_modules", 12 | "ready": "tsc -b pre-build.tsconfig.json", 13 | "build": "vite build", 14 | "dev": "vite build --mode development", 15 | "test": "vitest run", 16 | "lint": "eslint .", 17 | "lint:fix": "pnpm lint --fix", 18 | "prettier": "prettier . --write --ignore-path ../.prettierignore", 19 | "type-check": "tsc --noEmit" 20 | }, 21 | "dependencies": { 22 | "@extension/env": "workspace:*", 23 | "@extension/shared": "workspace:*", 24 | "@extension/storage": "workspace:*", 25 | "@modelcontextprotocol/sdk": "^1.12.1", 26 | "vite-plugin-node-polyfills": "^0.23.0", 27 | "webextension-polyfill": "^0.12.0" 28 | }, 29 | "devDependencies": { 30 | "@extension/dev-utils": "workspace:*", 31 | "@extension/hmr": "workspace:*", 32 | "@extension/tsconfig": "workspace:*", 33 | "@extension/vite-config": "workspace:*", 34 | "@laynezh/vite-plugin-lib-assets": "^0.6.1", 35 | "@types/chrome": "0.0.304", 36 | "@types/node": "^22.5.5", 37 | "magic-string": "^0.30.10", 38 | "ts-loader": "^9.5.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chrome-extension/pre-build.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./", 5 | "noEmit": false 6 | }, 7 | "include": ["manifest.ts"], 8 | "exclude": [""] 9 | } 10 | -------------------------------------------------------------------------------- /chrome-extension/public/Cover1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/Cover1.jpg -------------------------------------------------------------------------------- /chrome-extension/public/Cover2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/Cover2.png -------------------------------------------------------------------------------- /chrome-extension/public/Cover3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/Cover3.jpg -------------------------------------------------------------------------------- /chrome-extension/public/content.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/content.css -------------------------------------------------------------------------------- /chrome-extension/public/dragDropListener.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Avoid multiple listener registration 3 | if (window.__mcpDragListener) return; 4 | window.__mcpDragListener = true; 5 | 6 | window.addEventListener('message', async event => { 7 | if (event.source !== window || !event.data || event.data.type !== 'MCP_DROP_FILE') return; 8 | let file, fileName, fileType, lastModified, randomText; 9 | if (event.data.fileData) { 10 | // Use posted file 11 | fileName = event.data.fileName; 12 | fileType = event.data.fileType; 13 | lastModified = event.data.lastModified; 14 | const blob = await fetch(event.data.fileData).then(res => res.blob()); 15 | file = new File([blob], fileName, { type: fileType, lastModified }); 16 | randomText = null; 17 | } 18 | // else { 19 | // // Fallback: generate random file as in reference 20 | // const timestamp = new Date().toISOString(); 21 | // const randomId = Math.random().toString(36).substring(2, 15); 22 | // randomText = `Random Text File\n------------------\nCreated: ${timestamp}\nID: ${randomId}\nContent: This is sample text content for testing purposes.\nThe random seed value is: ${Math.random().toFixed(6)}\n\nAdditional lines of random text:\n${Array(5).fill(0).map(() => Math.random().toString(36).substring(2)).join('\\n')}`; 23 | // const blob = new Blob([randomText], { type: 'text/plain' }); 24 | // fileName = `random_text_${Date.now()}.txt`; 25 | // file = new File([blob], fileName, { type: 'text/plain', lastModified: Date.now() }); 26 | // } 27 | // Target the drop zone with multiple fallback selectors 28 | const dropZone = 29 | document.querySelector('div[xapfileselectordropzone]') || 30 | document.querySelector('.text-input-field') || 31 | document.querySelector('.input-area') || 32 | document.querySelector('.ql-editor'); 33 | if (!dropZone) { 34 | console.error('Drop zone not found using any of the selectors'); 35 | return; 36 | } 37 | console.log(`Found drop zone: ${dropZone.className}`); 38 | // Create a more complete DataTransfer-like object 39 | const dataTransfer = { 40 | files: [file], 41 | types: ['Files', 'application/x-moz-file'], 42 | items: [ 43 | { 44 | kind: 'file', 45 | type: file.type, 46 | getAsFile: function () { 47 | return file; 48 | }, 49 | }, 50 | ], 51 | getData: function (format) { 52 | if (format && format.toLowerCase() === 'text/plain' && randomText) return randomText; 53 | return ''; 54 | }, 55 | setData: function () {}, 56 | clearData: function () {}, 57 | dropEffect: 'copy', 58 | effectAllowed: 'copyMove', 59 | }; 60 | // Create more complete drag events sequence 61 | const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true }); 62 | const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true }); 63 | const dropEvent = new Event('drop', { bubbles: true, cancelable: true }); 64 | // Attach dataTransfer to all events 65 | [dragEnterEvent, dragOverEvent, dropEvent].forEach(event => { 66 | Object.defineProperty(event, 'dataTransfer', { 67 | value: dataTransfer, 68 | writable: false, 69 | }); 70 | }); 71 | // Simulate preventDefault() calls that would happen in a real drag 72 | dragOverEvent.preventDefault = function () { 73 | console.log('dragover preventDefault called'); 74 | Event.prototype.preventDefault.call(this); 75 | }; 76 | dropEvent.preventDefault = function () { 77 | console.log('drop preventDefault called'); 78 | Event.prototype.preventDefault.call(this); 79 | }; 80 | // Complete drag simulation sequence 81 | console.log('Starting drag simulation sequence'); 82 | console.log('1. Dispatching dragenter event'); 83 | dropZone.dispatchEvent(dragEnterEvent); 84 | console.log('2. Dispatching dragover event'); 85 | dropZone.dispatchEvent(dragOverEvent); 86 | console.log('3. Dispatching drop event'); 87 | dropZone.dispatchEvent(dropEvent); 88 | console.log('Drag and drop simulation completed successfully'); 89 | // Validate the operation worked by checking for file preview element 90 | // setTimeout(() => { 91 | // const filePreview = document.querySelector('.file-preview'); 92 | // if (filePreview) { 93 | // console.log('Success: File preview element found in DOM'); 94 | // } else { 95 | // console.warn('Note: File preview element not found in DOM. The operation might still have worked.'); 96 | // } 97 | // }, 1000); 98 | }); 99 | })(); 100 | -------------------------------------------------------------------------------- /chrome-extension/public/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/icon-128.png -------------------------------------------------------------------------------- /chrome-extension/public/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/chrome-extension/public/icon-34.png -------------------------------------------------------------------------------- /chrome-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/app", 3 | "compilerOptions": { 4 | "types": ["vite/client", "node", "chrome"], 5 | "baseUrl": ".", 6 | "paths": { 7 | "@src/*": ["src/*"] 8 | } 9 | }, 10 | "include": ["src", "utils", "vite.config.mts", "manifest.ts", "../node_modules/@types"] 11 | } 12 | -------------------------------------------------------------------------------- /chrome-extension/utils/plugins/make-manifest-plugin.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import { pathToFileURL } from 'node:url'; 4 | import { platform } from 'node:process'; 5 | import type { Manifest } from '@extension/dev-utils'; 6 | import { colorLog, ManifestParser } from '@extension/dev-utils'; 7 | import type { PluginOption } from 'vite'; 8 | import { IS_DEV, IS_FIREFOX } from '@extension/env'; 9 | 10 | const manifestFile = resolve(import.meta.dirname, '..', '..', 'manifest.js'); 11 | const refreshFilePath = resolve( 12 | import.meta.dirname, 13 | '..', 14 | '..', 15 | '..', 16 | 'packages', 17 | 'hmr', 18 | 'dist', 19 | 'lib', 20 | 'injections', 21 | 'refresh.js', 22 | ); 23 | 24 | const withHMRId = (code: string) => { 25 | return `(function() {let __HMR_ID = 'chrome-extension-hmr';${code}\n})();`; 26 | }; 27 | 28 | const getManifestWithCacheBurst = async () => { 29 | const withCacheBurst = (path: string) => `${path}?${Date.now().toString()}`; 30 | 31 | /** 32 | * In Windows, import() doesn't work without file:// protocol. 33 | * So, we need to convert path to file:// protocol. (url.pathToFileURL) 34 | */ 35 | if (platform === 'win32') { 36 | return (await import(withCacheBurst(pathToFileURL(manifestFile).href))).default; 37 | } else { 38 | return (await import(withCacheBurst(manifestFile))).default; 39 | } 40 | }; 41 | 42 | export default (config: { outDir: string }): PluginOption => { 43 | const makeManifest = (manifest: Manifest, to: string) => { 44 | if (!existsSync(to)) { 45 | mkdirSync(to); 46 | } 47 | 48 | const manifestPath = resolve(to, 'manifest.json'); 49 | 50 | if (IS_DEV) { 51 | addRefreshContentScript(manifest); 52 | } 53 | 54 | writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX)); 55 | 56 | const refreshFileString = readFileSync(refreshFilePath, 'utf-8'); 57 | 58 | if (IS_DEV) { 59 | writeFileSync(resolve(to, 'refresh.js'), withHMRId(refreshFileString)); 60 | } 61 | 62 | colorLog(`Manifest file copy complete: ${manifestPath}`, 'success'); 63 | }; 64 | 65 | return { 66 | name: 'make-manifest', 67 | buildStart() { 68 | this.addWatchFile(manifestFile); 69 | }, 70 | async writeBundle() { 71 | const outDir = config.outDir; 72 | const manifest = await getManifestWithCacheBurst(); 73 | makeManifest(manifest, outDir); 74 | }, 75 | }; 76 | }; 77 | 78 | function addRefreshContentScript(manifest: Manifest) { 79 | manifest.content_scripts = manifest.content_scripts || []; 80 | manifest.content_scripts.push({ 81 | matches: ['http://*/*', 'https://*/*', ''], 82 | js: ['refresh.js'], // for public's HMR(refresh) support 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /chrome-extension/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { defineConfig, type PluginOption } from 'vite'; 3 | import libAssetsPlugin from '@laynezh/vite-plugin-lib-assets'; 4 | import makeManifestPlugin from './utils/plugins/make-manifest-plugin.js'; 5 | import { watchPublicPlugin, watchRebuildPlugin } from '@extension/hmr'; 6 | import { watchOption } from '@extension/vite-config'; 7 | import env, { IS_DEV, IS_PROD } from '@extension/env'; 8 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 9 | 10 | const rootDir = resolve(import.meta.dirname); 11 | const srcDir = resolve(rootDir, 'src'); 12 | 13 | const outDir = resolve(rootDir, '..', 'dist'); 14 | export default defineConfig({ 15 | define: { 16 | 'process.env': env, 17 | }, 18 | envPrefix: ['VITE_', 'CEB_'], 19 | resolve: { 20 | alias: { 21 | '@root': rootDir, 22 | '@src': srcDir, 23 | '@assets': resolve(srcDir, 'assets'), 24 | }, 25 | }, 26 | plugins: [ 27 | libAssetsPlugin({ 28 | outputPath: outDir, 29 | }) as PluginOption, 30 | watchPublicPlugin(), 31 | makeManifestPlugin({ outDir }), 32 | IS_DEV && watchRebuildPlugin({ reload: true, id: 'chrome-extension-hmr' }), 33 | nodePolyfills(), 34 | ], 35 | publicDir: resolve(rootDir, 'public'), 36 | build: { 37 | lib: { 38 | name: 'BackgroundScript', 39 | fileName: 'background', 40 | formats: ['es'], 41 | entry: resolve(srcDir, 'background', 'index.ts'), 42 | }, 43 | outDir, 44 | emptyOutDir: false, 45 | sourcemap: IS_DEV, 46 | minify: IS_PROD, 47 | reportCompressedSize: IS_PROD, 48 | watch: watchOption, 49 | rollupOptions: { 50 | external: ['chrome'], 51 | }, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import type { FixupConfigArray } from '@eslint/compat'; 2 | import { fixupConfigRules } from '@eslint/compat'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | import js from '@eslint/js'; 5 | import eslintPluginImportX from 'eslint-plugin-import-x'; 6 | import jsxA11y from 'eslint-plugin-jsx-a11y'; 7 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 8 | import reactPlugin from 'eslint-plugin-react'; 9 | import globals from 'globals'; 10 | import ts from 'typescript-eslint'; 11 | 12 | export default ts.config( 13 | // Shared configs 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | jsxA11y.flatConfigs.recommended, 17 | eslintPluginImportX.flatConfigs.recommended, 18 | eslintPluginImportX.flatConfigs.typescript, 19 | eslintPluginPrettierRecommended, 20 | ...fixupConfigRules(new FlatCompat().extends('plugin:react-hooks/recommended') as FixupConfigArray), 21 | { 22 | files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], 23 | ...reactPlugin.configs.flat.recommended, 24 | ...reactPlugin.configs.flat['jsx-runtime'], 25 | }, 26 | 27 | // Custom config 28 | { 29 | ignores: ['**/build/**', '**/dist/**', '**/node_modules/**', 'eslint.config.js'], 30 | }, 31 | { 32 | files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], 33 | languageOptions: { 34 | parser: ts.parser, 35 | ecmaVersion: 'latest', 36 | sourceType: 'module', 37 | parserOptions: { 38 | ecmaFeatures: { jsx: true }, 39 | }, 40 | globals: { 41 | ...globals.browser, 42 | ...globals.es2020, 43 | ...globals.node, 44 | chrome: 'readonly', 45 | }, 46 | }, 47 | settings: { 48 | react: { 49 | version: 'detect', 50 | }, 51 | }, 52 | rules: { 53 | 'react/react-in-jsx-scope': 'off', 54 | 'import-x/no-unresolved': 'off', 55 | 'import-x/no-named-as-default-member': 'off', 56 | '@typescript-eslint/consistent-type-imports': 'error', 57 | 'react/prop-types': 'off', 58 | }, 59 | linterOptions: { 60 | reportUnusedDisableDirectives: 'error', 61 | }, 62 | }, 63 | ); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-superassistant", 3 | "version": "0.3.1", 4 | "description": "MCP SuperAssistant", 5 | "license": "MIT", 6 | "private": true, 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/srbhptl39/MCP-SuperAssistant.git" 11 | }, 12 | "type": "module", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist && turbo clean:bundle", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules && pnpm dlx turbo clean:node_modules", 16 | "clean:turbo": "rimraf .turbo && turbo clean:turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:turbo && pnpm clean:node_modules", 18 | "clean:install": "pnpm clean:node_modules && pnpm install --frozen-lockfile", 19 | "type-check": "turbo type-check", 20 | "base-build": "pnpm clean:bundle && turbo build", 21 | "build": "pnpm set-global-env && pnpm base-build", 22 | "build:firefox": "pnpm set-global-env CLI_CEB_FIREFOX=true && pnpm base-build", 23 | "base-dev": "pnpm clean:bundle && turbo ready && turbo watch dev --concurrency 20", 24 | "dev": "pnpm set-global-env CLI_CEB_DEV=true && pnpm base-dev", 25 | "dev:firefox": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_FIREFOX=true && pnpm base-dev", 26 | "build:eslint": "tsc -b", 27 | "zip": "pnpm build && pnpm -F zipper zip", 28 | "zip:firefox": "pnpm build:firefox && pnpm -F zipper zip", 29 | "e2e": "pnpm zip && turbo e2e", 30 | "e2e:firefox": "pnpm zip:firefox && turbo e2e", 31 | "lint": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", 32 | "lint:fix": "turbo lint:fix --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", 33 | "prettier": "turbo prettier --continue -- --cache --cache-location node_modules/.cache/.prettiercache", 34 | "prepare": "husky", 35 | "update-version": "bash bash-scripts/update_version.sh", 36 | "copy_env": "bash bash-scripts/copy_env.sh", 37 | "set-global-env": "bash bash-scripts/set_global_env.sh", 38 | "postinstall": "pnpm build:eslint && pnpm copy_env", 39 | "module-manager": "pnpm -F module-manager start" 40 | }, 41 | "dependencies": { 42 | "react": "19.1.0", 43 | "react-dom": "19.0.0" 44 | }, 45 | "devDependencies": { 46 | "@eslint/compat": "^1.2.5", 47 | "@eslint/eslintrc": "^3.2.0", 48 | "@eslint/js": "^9.20.0", 49 | "@types/chrome": "0.0.304", 50 | "@types/eslint-plugin-jsx-a11y": "^6.10.0", 51 | "@types/eslint__eslintrc": "^2.1.2", 52 | "@types/eslint__js": "^9.14.0", 53 | "@types/node": "^22.5.5", 54 | "@types/react": "^19.0.8", 55 | "@types/react-dom": "^19.0.3", 56 | "autoprefixer": "^10.4.20", 57 | "cross-env": "^7.0.3", 58 | "deepmerge": "^4.3.1", 59 | "esbuild": "^0.25.0", 60 | "eslint": "^9.20.1", 61 | "eslint-config-airbnb-typescript": "18.0.0", 62 | "eslint-config-prettier": "^10.0.1", 63 | "eslint-import-resolver-typescript": "4.3.4", 64 | "eslint-plugin-import": "2.29.1", 65 | "eslint-plugin-import-x": "4.6.1", 66 | "eslint-plugin-jsx-a11y": "6.10.2", 67 | "eslint-plugin-prettier": "5.2.3", 68 | "eslint-plugin-react": "7.37.4", 69 | "eslint-plugin-react-hooks": "5.2.0", 70 | "eslint-plugin-tailwindcss": "^3.17.5", 71 | "fast-glob": "^3.3.3", 72 | "globals": "^15.14.0", 73 | "husky": "^9.1.4", 74 | "lint-staged": "^15.2.7", 75 | "postcss": "^8.5.2", 76 | "postcss-load-config": "^6.0.1", 77 | "prettier": "^3.3.3", 78 | "rimraf": "^6.0.1", 79 | "run-script-os": "^1.1.6", 80 | "tailwindcss": "^3.4.17", 81 | "tslib": "^2.8.1", 82 | "turbo": "^2.4.2", 83 | "typescript": "5.8.1-rc", 84 | "typescript-eslint": "^8.20.0", 85 | "vite": "6.1.0" 86 | }, 87 | "lint-staged": { 88 | "*.{js,jsx,ts,tsx,json}": [ 89 | "prettier --write" 90 | ] 91 | }, 92 | "packageManager": "pnpm@9.15.1", 93 | "engines": { 94 | "node": ">=22.12.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/dev-utils/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/manifest-parser/index.js'; 2 | export * from './lib/logger.js'; 3 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/logger.ts: -------------------------------------------------------------------------------- 1 | type ValueOf = T[keyof T]; 2 | 3 | type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS; 4 | 5 | export const colorLog = (message: string, type: ColorType) => { 6 | let color: ValueOf; 7 | 8 | switch (type) { 9 | case 'success': 10 | color = COLORS.FgGreen; 11 | break; 12 | case 'info': 13 | color = COLORS.FgBlue; 14 | break; 15 | case 'error': 16 | color = COLORS.FgRed; 17 | break; 18 | case 'warning': 19 | color = COLORS.FgYellow; 20 | break; 21 | default: 22 | color = COLORS[type]; 23 | break; 24 | } 25 | 26 | console.log(color, message); 27 | }; 28 | 29 | const COLORS = { 30 | Reset: '\x1b[0m', 31 | Bright: '\x1b[1m', 32 | Dim: '\x1b[2m', 33 | Underscore: '\x1b[4m', 34 | Blink: '\x1b[5m', 35 | Reverse: '\x1b[7m', 36 | Hidden: '\x1b[8m', 37 | FgBlack: '\x1b[30m', 38 | FgRed: '\x1b[31m', 39 | FgGreen: '\x1b[32m', 40 | FgYellow: '\x1b[33m', 41 | FgBlue: '\x1b[34m', 42 | FgMagenta: '\x1b[35m', 43 | FgCyan: '\x1b[36m', 44 | FgWhite: '\x1b[37m', 45 | BgBlack: '\x1b[40m', 46 | BgRed: '\x1b[41m', 47 | BgGreen: '\x1b[42m', 48 | BgYellow: '\x1b[43m', 49 | BgBlue: '\x1b[44m', 50 | BgMagenta: '\x1b[45m', 51 | BgCyan: '\x1b[46m', 52 | BgWhite: '\x1b[47m', 53 | } as const; 54 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/impl.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest, ManifestParserInterface } from './types.js'; 2 | 3 | export const ManifestParserImpl: ManifestParserInterface = { 4 | convertManifestToString: (manifest, isFirefox) => { 5 | if (isFirefox) { 6 | manifest = convertToFirefoxCompatibleManifest(manifest); 7 | } 8 | 9 | return JSON.stringify(manifest, null, 2); 10 | }, 11 | }; 12 | 13 | const convertToFirefoxCompatibleManifest = (manifest: Manifest) => { 14 | const manifestCopy = { 15 | ...manifest, 16 | } as { [key: string]: unknown }; 17 | 18 | if (manifest.background?.service_worker) { 19 | manifestCopy.background = { 20 | scripts: [manifest.background.service_worker], 21 | type: 'module', 22 | }; 23 | } 24 | if (manifest.options_page) { 25 | manifestCopy.options_ui = { 26 | page: manifest.options_page, 27 | browser_style: false, 28 | }; 29 | } 30 | manifestCopy.content_security_policy = { 31 | extension_pages: "script-src 'self'; object-src 'self'", 32 | }; 33 | manifestCopy.permissions = (manifestCopy.permissions as string[]).filter(value => value !== 'sidePanel'); 34 | 35 | delete manifestCopy.options_page; 36 | delete manifestCopy.side_panel; 37 | return manifestCopy as Manifest; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/index.ts: -------------------------------------------------------------------------------- 1 | import { ManifestParserImpl } from './impl.js'; 2 | 3 | export * from './types.js'; 4 | export const ManifestParser = ManifestParserImpl; 5 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/types.ts: -------------------------------------------------------------------------------- 1 | export type Manifest = chrome.runtime.ManifestV3; 2 | 3 | export interface ManifestParserInterface { 4 | convertManifestToString: (manifest: Manifest, isFirefox: boolean) => string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/dev-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/dev-utils", 3 | "version": "0.4.2", 4 | "description": "chrome extension - dev utils", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b", 19 | "lint": "eslint .", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "devDependencies": { 25 | "@extension/tsconfig": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/dev-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/env/README.md: -------------------------------------------------------------------------------- 1 | # Environment Package 2 | 3 | This package contains code which creates env values. 4 | To use the code in the package, you need to follow those steps: 5 | 6 | 1. Add a new record to `.env` (NEED TO CONTAIN `CEB_` PREFIX), 7 | 8 | - If you want via cli: 9 | - Add it as argument like: `pnpm set-global-env CLI_CEB_NEXT_VALUE=new_data ...` (NEED TO CONTAIN `CLI_CEB_` PREFIX) 10 | 11 | > [!IMPORTANT] 12 | > `CLI_CEB_DEV` and `CLI_CEB_FIREFOX` are `false` by default \ 13 | > All CLI values are overwriting in each call, that's mean you'll have access to values from current script run only. 14 | 15 | - If you want dynamic variables go to `lib/index.ts` and edit `dynamicEnvValues` object. 16 | 17 | 2. Use it, for example: 18 | ```ts 19 | console.log(process.env['CEB_EXAMPLE']); 20 | ``` 21 | or 22 | ```ts 23 | console.log(process.env.CEB_EXAMPLE); 24 | ``` 25 | but with first solution, autofill should work for IDE: 26 | ![img.png](img.png) 27 | 3. You are also able to import const like `IS_DEV` from `@extension/env` like: 28 | ```ts 29 | import { IS_DEV } from '@extension/env'; 30 | ``` 31 | For more look [ENV CONST](lib/const.ts) -------------------------------------------------------------------------------- /packages/env/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srbhptl39/MCP-SuperAssistant/8c162ca254a015ffd41668cdcb099a8e5d25e44d/packages/env/img.png -------------------------------------------------------------------------------- /packages/env/index.mts: -------------------------------------------------------------------------------- 1 | import { baseEnv, dynamicEnvValues } from './lib/index.js'; 2 | import type { IEnv } from './lib/types.js'; 3 | 4 | export * from './lib/const.js'; 5 | export * from './lib/index.js'; 6 | 7 | const env = { 8 | ...baseEnv, 9 | ...dynamicEnvValues, 10 | } as IEnv; 11 | 12 | export default env; 13 | -------------------------------------------------------------------------------- /packages/env/lib/const.ts: -------------------------------------------------------------------------------- 1 | export const IS_DEV = process.env['CLI_CEB_DEV'] === 'true'; 2 | export const IS_PROD = !IS_DEV; 3 | export const IS_FIREFOX = process.env['CLI_CEB_FIREFOX'] === 'true'; 4 | export const IS_CI = process.env['CEB_CI'] === 'true'; 5 | -------------------------------------------------------------------------------- /packages/env/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@dotenvx/dotenvx'; 2 | 3 | export const baseEnv = 4 | config({ 5 | path: `${import.meta.dirname}/../../../../.env`, 6 | }).parsed ?? {}; 7 | 8 | export const dynamicEnvValues = { 9 | CEB_NODE_ENV: baseEnv.CEB_DEV === 'true' ? 'development' : 'production', 10 | } as const; 11 | -------------------------------------------------------------------------------- /packages/env/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { dynamicEnvValues } from './index.js'; 2 | 3 | interface ICebEnv { 4 | readonly CEB_EXAMPLE: string; 5 | readonly CEB_DEV_LOCALE: string; 6 | } 7 | 8 | interface ICebCliEnv { 9 | readonly CLI_CEB_DEV: string; 10 | readonly CLI_CEB_FIREFOX: string; 11 | } 12 | 13 | export type IEnv = ICebEnv & ICebCliEnv & typeof dynamicEnvValues; 14 | -------------------------------------------------------------------------------- /packages/env/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/env", 3 | "version": "0.4.2", 4 | "description": "chrome extension - environment variables", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b", 19 | "lint": "eslint .", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "dependencies": { 25 | "@dotenvx/dotenvx": "^1.33.0" 26 | }, 27 | "devDependencies": { 28 | "@extension/tsconfig": "workspace:*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/env/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/hmr/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/plugins/index.js'; 2 | -------------------------------------------------------------------------------- /packages/hmr/lib/consts.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_RELOAD_SOCKET_PORT = 8081; 2 | export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`; 3 | 4 | export const DO_UPDATE = 'do_update'; 5 | export const DONE_UPDATE = 'done_update'; 6 | export const BUILD_COMPLETE = 'build_complete'; 7 | -------------------------------------------------------------------------------- /packages/hmr/lib/initializers/initClient.ts: -------------------------------------------------------------------------------- 1 | import { DO_UPDATE, DONE_UPDATE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js'; 2 | import MessageInterpreter from '../interpreter/index.js'; 3 | 4 | export default ({ id, onUpdate }: { id: string; onUpdate: () => void }) => { 5 | const ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); 6 | 7 | ws.onopen = () => { 8 | ws.addEventListener('message', event => { 9 | const message = MessageInterpreter.receive(String(event.data)); 10 | 11 | if (message.type === DO_UPDATE && message.id === id) { 12 | onUpdate(); 13 | ws.send(MessageInterpreter.send({ type: DONE_UPDATE })); 14 | } 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/hmr/lib/initializers/initReloadServer.ts: -------------------------------------------------------------------------------- 1 | import type { WebSocket } from 'ws'; 2 | import { WebSocketServer } from 'ws'; 3 | import { 4 | BUILD_COMPLETE, 5 | DO_UPDATE, 6 | DONE_UPDATE, 7 | LOCAL_RELOAD_SOCKET_PORT, 8 | LOCAL_RELOAD_SOCKET_URL, 9 | } from '../consts.js'; 10 | import MessageInterpreter from '../interpreter/index.js'; 11 | 12 | const clientsThatNeedToUpdate: Set = new Set(); 13 | 14 | (() => { 15 | const wss = new WebSocketServer({ port: LOCAL_RELOAD_SOCKET_PORT }); 16 | 17 | wss.on('listening', () => { 18 | console.log(`[HMR] Server listening at ${LOCAL_RELOAD_SOCKET_URL}`); 19 | }); 20 | 21 | wss.on('connection', ws => { 22 | clientsThatNeedToUpdate.add(ws); 23 | 24 | ws.addEventListener('close', () => { 25 | clientsThatNeedToUpdate.delete(ws); 26 | }); 27 | 28 | ws.addEventListener('message', event => { 29 | if (typeof event.data !== 'string') return; 30 | 31 | const message = MessageInterpreter.receive(event.data); 32 | 33 | if (message.type === DONE_UPDATE) { 34 | ws.close(); 35 | } 36 | 37 | if (message.type === BUILD_COMPLETE) { 38 | clientsThatNeedToUpdate.forEach((ws: WebSocket) => 39 | ws.send(MessageInterpreter.send({ type: DO_UPDATE, id: message.id })), 40 | ); 41 | } 42 | }); 43 | }); 44 | 45 | wss.on('error', error => { 46 | console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`); 47 | throw error; 48 | }); 49 | })(); 50 | -------------------------------------------------------------------------------- /packages/hmr/lib/injections/refresh.ts: -------------------------------------------------------------------------------- 1 | import initClient from '../initializers/initClient.js'; 2 | 3 | (() => { 4 | let pendingReload = false; 5 | 6 | initClient({ 7 | // @ts-expect-error That's because of the dynamic code loading 8 | id: __HMR_ID, 9 | onUpdate: () => { 10 | // disable reload when tab is hidden 11 | if (document.hidden) { 12 | pendingReload = true; 13 | return; 14 | } 15 | reload(); 16 | }, 17 | }); 18 | 19 | // reload 20 | function reload(): void { 21 | pendingReload = false; 22 | window.location.reload(); 23 | } 24 | 25 | // reload when tab is visible 26 | function reloadWhenTabIsVisible(): void { 27 | if (!document.hidden && pendingReload) { 28 | reload(); 29 | } 30 | } 31 | 32 | document.addEventListener('visibilitychange', reloadWhenTabIsVisible); 33 | })(); 34 | -------------------------------------------------------------------------------- /packages/hmr/lib/injections/reload.ts: -------------------------------------------------------------------------------- 1 | import initClient from '../initializers/initClient.js'; 2 | 3 | (() => { 4 | const reload = () => { 5 | chrome.runtime.reload(); 6 | }; 7 | 8 | initClient({ 9 | // @ts-expect-error That's because of the dynamic code loading 10 | id: __HMR_ID, 11 | onUpdate: reload, 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /packages/hmr/lib/interpreter/index.ts: -------------------------------------------------------------------------------- 1 | import type { SerializedMessage, WebSocketMessage } from '../types.js'; 2 | 3 | export default { 4 | send: (message: WebSocketMessage): SerializedMessage => JSON.stringify(message), 5 | receive: (serializedMessage: SerializedMessage): WebSocketMessage => JSON.parse(serializedMessage), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './watch-rebuild-plugin.js'; 2 | export * from './make-entry-point-plugin.js'; 3 | export * from './watch-public-plugin.js'; 4 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/make-entry-point-plugin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import type { PluginOption } from 'vite'; 4 | import { IS_FIREFOX } from '@extension/env'; 5 | 6 | /** 7 | * make entry point file for content script cache busting 8 | */ 9 | export function makeEntryPointPlugin(): PluginOption { 10 | const cleanupTargets = new Set(); 11 | 12 | return { 13 | name: 'make-entry-point-plugin', 14 | generateBundle(options, bundle) { 15 | const outputDir = options.dir; 16 | 17 | if (!outputDir) { 18 | throw new Error('Output directory not found'); 19 | } 20 | 21 | for (const module of Object.values(bundle)) { 22 | const fileName = path.basename(module.fileName); 23 | const newFileName = fileName.replace('.js', '_dev.js'); 24 | 25 | switch (module.type) { 26 | case 'asset': 27 | if (fileName.endsWith('.map')) { 28 | cleanupTargets.add(path.resolve(outputDir, fileName)); 29 | 30 | const originalFileName = fileName.replace('.map', ''); 31 | const replacedSource = String(module.source).replaceAll(originalFileName, newFileName); 32 | 33 | module.source = ''; 34 | fs.writeFileSync(path.resolve(outputDir, newFileName), replacedSource); 35 | break; 36 | } 37 | break; 38 | 39 | case 'chunk': { 40 | fs.writeFileSync(path.resolve(outputDir, newFileName), module.code); 41 | 42 | if (IS_FIREFOX) { 43 | const contentDirectory = extractContentDir(outputDir); 44 | module.code = `import(browser.runtime.getURL("${contentDirectory}/${newFileName}"));`; 45 | } else { 46 | module.code = `import('./${newFileName}');`; 47 | } 48 | break; 49 | } 50 | } 51 | } 52 | }, 53 | closeBundle() { 54 | cleanupTargets.forEach(target => { 55 | fs.unlinkSync(target); 56 | }); 57 | }, 58 | }; 59 | } 60 | 61 | /** 62 | * Extract content directory from output directory for Firefox 63 | * @param outputDir 64 | */ 65 | function extractContentDir(outputDir: string) { 66 | const parts = outputDir.split(path.sep); 67 | const distIndex = parts.indexOf('dist'); 68 | 69 | if (distIndex !== -1 && distIndex < parts.length - 1) { 70 | return parts.slice(distIndex + 1); 71 | } 72 | 73 | throw new Error('Output directory does not contain "dist"'); 74 | } 75 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/watch-public-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite'; 2 | import fg from 'fast-glob'; 3 | 4 | export function watchPublicPlugin(): PluginOption { 5 | return { 6 | name: 'watch-public-plugin', 7 | async buildStart() { 8 | const files = await fg(['public/**/*']); 9 | 10 | for (const file of files) { 11 | this.addWatchFile(file); 12 | } 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/watch-rebuild-plugin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import type { PluginOption } from 'vite'; 4 | import { WebSocket } from 'ws'; 5 | import MessageInterpreter from '../interpreter/index.js'; 6 | import { BUILD_COMPLETE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js'; 7 | import type { PluginConfig } from '../types.js'; 8 | 9 | const injectionsPath = resolve(import.meta.dirname, '..', 'injections'); 10 | 11 | const refreshCode = fs.readFileSync(resolve(injectionsPath, 'refresh.js'), 'utf-8'); 12 | const reloadCode = fs.readFileSync(resolve(injectionsPath, 'reload.js'), 'utf-8'); 13 | 14 | export const watchRebuildPlugin = (config: PluginConfig): PluginOption => { 15 | const { refresh, reload, id: _id, onStart } = config; 16 | const hmrCode = (refresh ? refreshCode : '') + (reload ? reloadCode : ''); 17 | 18 | let ws: WebSocket | null = null; 19 | 20 | const id = _id ?? Math.random().toString(36); 21 | let reconnectTries = 0; 22 | 23 | const initializeWebSocket = () => { 24 | ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); 25 | 26 | ws.onopen = () => { 27 | console.log(`[HMR] Connected to dev-server at ${LOCAL_RELOAD_SOCKET_URL}`); 28 | }; 29 | 30 | ws.onerror = () => { 31 | console.error(`[HMR] Failed to connect server at ${LOCAL_RELOAD_SOCKET_URL}`); 32 | console.warn('Retrying in 3 seconds...'); 33 | ws = null; 34 | 35 | if (reconnectTries <= 2) { 36 | setTimeout(() => { 37 | reconnectTries++; 38 | initializeWebSocket(); 39 | }, 3_000); 40 | } else { 41 | console.error(`[HMR] Cannot establish connection to server at ${LOCAL_RELOAD_SOCKET_URL}`); 42 | } 43 | }; 44 | }; 45 | 46 | return { 47 | name: 'watch-rebuild', 48 | writeBundle() { 49 | onStart?.(); 50 | if (!ws) { 51 | initializeWebSocket(); 52 | return; 53 | } 54 | /** 55 | * When the build is complete, send a message to the reload server. 56 | * The reload server will send a message to the client to reload or refresh the extension. 57 | */ 58 | ws.send(MessageInterpreter.send({ type: BUILD_COMPLETE, id })); 59 | }, 60 | generateBundle(_options, bundle) { 61 | for (const module of Object.values(bundle)) { 62 | if (module.type === 'chunk') { 63 | module.code = `(function() {let __HMR_ID = "${id}";\n` + hmrCode + '\n' + '})();' + '\n' + module.code; 64 | } 65 | } 66 | }, 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /packages/hmr/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { BUILD_COMPLETE, DO_UPDATE, DONE_UPDATE } from './consts.js'; 2 | 3 | type UpdateRequestMessage = { 4 | type: typeof DO_UPDATE; 5 | id: string; 6 | }; 7 | 8 | type UpdateCompleteMessage = { type: typeof DONE_UPDATE }; 9 | type BuildCompletionMessage = { type: typeof BUILD_COMPLETE; id: string }; 10 | 11 | export type SerializedMessage = string; 12 | 13 | export type WebSocketMessage = UpdateCompleteMessage | UpdateRequestMessage | BuildCompletionMessage; 14 | 15 | export type PluginConfig = { 16 | onStart?: () => void; 17 | reload?: boolean; 18 | refresh?: boolean; 19 | id?: string; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hmr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/hmr", 3 | "version": "0.4.2", 4 | "description": "chrome extension - hot module reload/refresh", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": true, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist && pnpm dlx rimraf build", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b && rollup --config dist/rollup.config.js", 19 | "dev": "node dist/lib/initializers/initReloadServer.js", 20 | "lint": "eslint .", 21 | "lint:fix": "pnpm lint --fix", 22 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 23 | "type-check": "tsc --noEmit" 24 | }, 25 | "devDependencies": { 26 | "@extension/tsconfig": "workspace:*", 27 | "@extension/env": "workspace:*", 28 | "@rollup/plugin-sucrase": "^5.0.2", 29 | "@types/ws": "^8.5.13", 30 | "esm": "^3.2.25", 31 | "rollup": "^4.24.0", 32 | "ts-node": "^10.9.2", 33 | "ws": "8.18.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/hmr/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import sucrase from '@rollup/plugin-sucrase'; 2 | import type { Plugin, RollupOptions } from 'rollup'; 3 | 4 | const plugins = [ 5 | // @ts-expect-error I don't know why error happening here 6 | sucrase({ 7 | exclude: ['node_modules/**'], 8 | transforms: ['typescript'], 9 | }), 10 | ] satisfies Plugin[]; 11 | 12 | export default [ 13 | { 14 | plugins, 15 | input: 'lib/injections/reload.ts', 16 | output: { 17 | format: 'esm', 18 | file: 'dist/lib/injections/reload.js', 19 | }, 20 | }, 21 | { 22 | plugins, 23 | input: 'lib/injections/refresh.ts', 24 | output: { 25 | format: 'esm', 26 | file: 'dist/lib/injections/refresh.js', 27 | }, 28 | }, 29 | ] satisfies RollupOptions[]; 30 | -------------------------------------------------------------------------------- /packages/hmr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["lib", "index.mts", "rollup.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/i18n/.gitignore: -------------------------------------------------------------------------------- 1 | lib/i18n.ts -------------------------------------------------------------------------------- /packages/i18n/README.md: -------------------------------------------------------------------------------- 1 | # I18n Package 2 | 3 | This package provides a set of tools to help you internationalize your Chrome Extension. 4 | 5 | https://developer.chrome.com/docs/extensions/reference/api/i18n 6 | 7 | ## Installation 8 | 9 | If you want to use the i18n translation function in each pages, you need to add the following to the package.json file. 10 | 11 | ```json 12 | { 13 | "dependencies": { 14 | "@extension/i18n": "workspace:*" 15 | } 16 | } 17 | ``` 18 | 19 | Then run the following command to install the package. 20 | 21 | ```bash 22 | pnpm install 23 | ``` 24 | 25 | ## Manage translations 26 | 27 | You can manage translations in the `locales` directory. 28 | 29 | `locales/en/messages.json` 30 | 31 | ```json 32 | { 33 | "helloWorld": { 34 | "message": "Hello, World!" 35 | } 36 | } 37 | ``` 38 | 39 | `locales/ko/messages.json` 40 | 41 | ```json 42 | { 43 | "helloWorld": { 44 | "message": "안녕하세요, 여러분!" 45 | } 46 | } 47 | ``` 48 | 49 | ## Add a new language 50 | 51 | Create folder inside `locales` with name from [languages](https://developer.chrome.com/docs/extensions/reference/api/i18n?hl=pl#support_multiple_languages), which need include `message.json` file. 52 | 53 | ## Usage 54 | 55 | ### Translation function 56 | 57 | Just import the `t` function and use it to translate the key. 58 | 59 | ```typescript 60 | import { t } from '@extension/i18n'; 61 | 62 | console.log(t('loading')); // Loading... 63 | ``` 64 | 65 | ```typescript jsx 66 | import { t } from '@extension/i18n'; 67 | 68 | const Component = () => { 69 | return ( 70 | 73 | ); 74 | }; 75 | ``` 76 | 77 | ### Placeholders 78 | 79 | If you want to use placeholders, you can use the following format. 80 | 81 | > For more information, see the [Message Placeholders](https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders) section. 82 | 83 | `locales/en/messages.json` 84 | 85 | ```json 86 | { 87 | "greeting": { 88 | "description": "Greeting message", 89 | "message": "Hello, My name is $NAME$", 90 | "placeholders": { 91 | "name": { 92 | "content": "$1", 93 | "example": "John Doe" 94 | } 95 | } 96 | }, 97 | "hello": { 98 | "description": "Placeholder example", 99 | "message": "Hello $1" 100 | } 101 | } 102 | ``` 103 | 104 | `locales/ko/messages.json` 105 | 106 | ```json 107 | { 108 | "greeting": { 109 | "description": "인사 메시지", 110 | "message": "안녕하세요, 제 이름은 $NAME$입니다.", 111 | "placeholders": { 112 | "name": { 113 | "content": "$1", 114 | "example": "서종학" 115 | } 116 | } 117 | }, 118 | "hello": { 119 | "description": "Placeholder 예시", 120 | "message": "안녕 $1" 121 | } 122 | } 123 | ``` 124 | 125 | If you want to replace the placeholder, you can pass the value as the second argument. 126 | 127 | Function `t` has exactly the same interface as the `chrome.i18n.getMessage` function. 128 | 129 | ```typescript 130 | import { t } from '@extension/i18n'; 131 | 132 | console.log(t('greeting', 'John Doe')); // Hello, My name is John Doe 133 | console.log(t('greeting', ['John Doe'])); // Hello, My name is John Doe 134 | 135 | console.log(t('hello')); // Hello 136 | console.log(t('hello', 'World')); // Hello World 137 | console.log(t('hello', ['World'])); // Hello World 138 | ``` 139 | 140 | ### Locale setting on development 141 | 142 | If you want to enforce displaying specific language, you need to set `CEB_DEV_LOCALE` in `.env` file (work only for development). 143 | 144 | ### Type Safety 145 | 146 | When you forget to add a key to all language's `messages.json` files, you will get a Typescript error. 147 | 148 | `locales/en/messages.json` 149 | 150 | ```json 151 | { 152 | "hello": { 153 | "message": "Hello World!" 154 | } 155 | } 156 | ``` 157 | 158 | `locales/ko/messages.json` 159 | 160 | ```json 161 | { 162 | "helloWorld": { 163 | "message": "안녕하세요, 여러분!" 164 | } 165 | } 166 | ``` 167 | 168 | ```typescript 169 | import { t } from '@extension/i18n'; 170 | 171 | // Error: TS2345: Argument of type "hello" is not assignable to parameter of type 172 | console.log(t('hello')); 173 | ``` 174 | -------------------------------------------------------------------------------- /packages/i18n/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/index.js'; 2 | -------------------------------------------------------------------------------- /packages/i18n/lib/consts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @url https://developer.chrome.com/docs/extensions/reference/api/i18n#support_multiple_languages 3 | */ 4 | export const SUPPORTED_LANGUAGES = { 5 | ar: 'Arabic', 6 | am: 'Amharic', 7 | bg: 'Bulgarian', 8 | bn: 'Bengali', 9 | ca: 'Catalan', 10 | cs: 'Czech', 11 | da: 'Danish', 12 | de: 'German', 13 | el: 'Greek', 14 | en: 'English', 15 | en_AU: 'English (Australia)', 16 | en_GB: 'English (Great Britain)', 17 | en_US: 'English (USA)', 18 | es: 'Spanish', 19 | es_419: 'Spanish (Latin America and Caribbean)', 20 | et: 'Estonian', 21 | fa: 'Persian', 22 | fi: 'Finnish', 23 | fil: 'Filipino', 24 | fr: 'French', 25 | gu: 'Gujarati', 26 | he: 'Hebrew', 27 | hi: 'Hindi', 28 | hr: 'Croatian', 29 | hu: 'Hungarian', 30 | id: 'Indonesian', 31 | it: 'Italian', 32 | ja: 'Japanese', 33 | kn: 'Kannada', 34 | ko: 'Korean', 35 | lt: 'Lithuanian', 36 | lv: 'Latvian', 37 | ml: 'Malayalam', 38 | mr: 'Marathi', 39 | ms: 'Malay', 40 | nl: 'Dutch', 41 | no: 'Norwegian', 42 | pl: 'Polish', 43 | pt_BR: 'Portuguese (Brazil)', 44 | pt_PT: 'Portuguese (Portugal)', 45 | ro: 'Romanian', 46 | ru: 'Russian', 47 | sk: 'Slovak', 48 | sl: 'Slovenian', 49 | sr: 'Serbian', 50 | sv: 'Swedish', 51 | sw: 'Swahili', 52 | ta: 'Tamil', 53 | te: 'Telugu', 54 | th: 'Thai', 55 | tr: 'Turkish', 56 | uk: 'Ukrainian', 57 | vi: 'Vietnamese', 58 | zh_CN: 'Chinese (China)', 59 | zh_TW: 'Chinese (Taiwan)', 60 | } as const; 61 | 62 | export const I18N_FILE_PATH = './lib/i18n.ts'; 63 | -------------------------------------------------------------------------------- /packages/i18n/lib/i18n-dev.ts: -------------------------------------------------------------------------------- 1 | // IT WILL BE ADJUSTED TO YOUR LANGUAGE DURING BUILD TIME, DON'T MOVE BELOW IMPORT TO OTHER LINE 2 | import localeJSON from '../locales/en/messages.json' with { type: 'json' }; 3 | import type { I18nValueType, LocalesJSONType } from './types.js'; 4 | 5 | const translate = (key: keyof LocalesJSONType, substitutions?: string | string[]) => { 6 | const localeValues = localeJSON[key] as I18nValueType; 7 | let message = localeValues.message; 8 | /** 9 | * This is a placeholder replacement logic. But it's not perfect. 10 | * It just imitates the behavior of the Chrome extension i18n API. 11 | * Please check the official document for more information And double-check the behavior on production build. 12 | * 13 | * @url https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders 14 | */ 15 | if (localeValues.placeholders) { 16 | Object.entries(localeValues.placeholders).forEach(([key, { content }]) => { 17 | if (content) { 18 | message = message.replace(new RegExp(`\\$${key}\\$`, 'gi'), content); 19 | } 20 | }); 21 | } 22 | 23 | if (!substitutions) { 24 | return message; 25 | } else if (Array.isArray(substitutions)) { 26 | return substitutions.reduce((acc, cur, idx) => acc.replace(`$${idx++}`, cur), message); 27 | } 28 | 29 | return message.replace(/\$(\d+)/, substitutions); 30 | }; 31 | 32 | const removePlaceholder = (message: string) => message.replace(/\$\d+/g, ''); 33 | 34 | export const t = (...args: Parameters) => removePlaceholder(translate(...args)); 35 | -------------------------------------------------------------------------------- /packages/i18n/lib/i18n-prod.ts: -------------------------------------------------------------------------------- 1 | import type { MessageKey } from './types.js'; 2 | 3 | export const t = (key: MessageKey, substitutions?: string | string[]) => chrome.i18n.getMessage(key, substitutions); 4 | -------------------------------------------------------------------------------- /packages/i18n/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { t as t_dev_or_prod } from './i18n.js'; 2 | import type { t as t_dev } from './i18n-dev.js'; 3 | 4 | export const t = t_dev_or_prod as unknown as typeof t_dev; 5 | -------------------------------------------------------------------------------- /packages/i18n/lib/prepare_build.ts: -------------------------------------------------------------------------------- 1 | import { cpSync, existsSync, mkdirSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import setRelatedLocaleImports from './set_related_locale_import.js'; 4 | import { IS_DEV } from '@extension/env'; 5 | 6 | (() => { 7 | const i18nPath = IS_DEV ? 'lib/i18n-dev.ts' : 'lib/i18n-prod.ts'; 8 | cpSync(i18nPath, resolve('lib', 'i18n.ts')); 9 | 10 | const outDir = resolve(import.meta.dirname, '..', '..', '..', '..', 'dist'); 11 | if (!existsSync(outDir)) { 12 | mkdirSync(outDir); 13 | } 14 | 15 | const localePath = resolve(outDir, '_locales'); 16 | cpSync(resolve('locales'), localePath, { recursive: true }); 17 | 18 | if (IS_DEV) { 19 | setRelatedLocaleImports(); 20 | } 21 | console.log('I18n build complete'); 22 | })(); 23 | -------------------------------------------------------------------------------- /packages/i18n/lib/set_related_locale_import.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import type { SupportedLanguagesKeysType, SupportedLanguagesWithoutRegionKeysType } from './types.js'; 4 | import { I18N_FILE_PATH } from './consts.js'; 5 | 6 | export default () => { 7 | const locale = Intl.DateTimeFormat().resolvedOptions().locale.replace('-', '_') as SupportedLanguagesKeysType; 8 | const localeWithoutRegion = locale.split('_')[0] as SupportedLanguagesWithoutRegionKeysType; 9 | 10 | const localesDir = resolve(import.meta.dirname, '..', '..', 'locales'); 11 | const readLocalesFolder = readdirSync(localesDir); 12 | 13 | const implementedLocales = readLocalesFolder.map(innerDir => { 14 | if (lstatSync(resolve(localesDir, innerDir)).isDirectory()) { 15 | return innerDir; 16 | } 17 | return; 18 | }); 19 | 20 | const i18nFileSplitContent = readFileSync(I18N_FILE_PATH, 'utf-8').split('\n'); 21 | 22 | if (process.env['CEB_DEV_LOCALE']) { 23 | i18nFileSplitContent[1] = `import localeJSON from '../locales/${process.env['CEB_DEV_LOCALE']}/messages.json' with { type: 'json' };`; 24 | } else { 25 | if (implementedLocales.includes(locale)) { 26 | i18nFileSplitContent[1] = `import localeJSON from '../locales/${locale}/messages.json' with { type: 'json' };`; 27 | } else if (implementedLocales.includes(localeWithoutRegion)) { 28 | i18nFileSplitContent[1] = `import localeJSON from '../locales/${localeWithoutRegion}/messages.json' with { type: 'json' };`; 29 | } else { 30 | i18nFileSplitContent[1] = `import localeJSON from '../locales/en/messages.json' with { type: 'json' };`; 31 | } 32 | } 33 | 34 | // Join lines back together 35 | const updatedI18nFile = i18nFileSplitContent.join('\n'); 36 | 37 | writeFileSync(I18N_FILE_PATH, updatedI18nFile, 'utf-8'); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/i18n/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { SUPPORTED_LANGUAGES } from './consts.js'; 2 | import type enMessage from '../locales/en/messages.json'; 3 | 4 | export type SupportedLanguagesKeysType = keyof typeof SUPPORTED_LANGUAGES; 5 | export type SupportedLanguagesWithoutRegionKeysType = Exclude; 6 | export type I18nValueType = { 7 | message: string; 8 | placeholders?: Record; 9 | }; 10 | 11 | export type MessageKey = keyof typeof enMessage; 12 | export type LocalesJSONType = typeof enMessage; 13 | -------------------------------------------------------------------------------- /packages/i18n/locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "description": "Extension description", 4 | "message": "Chrome extension boilerplate developed with Vite, React and Typescript" 5 | }, 6 | "extensionName": { 7 | "description": "Extension name", 8 | "message": "Chrome extension boilerplate" 9 | }, 10 | "toggleTheme": { 11 | "message": "Toggle theme" 12 | }, 13 | "loading": { 14 | "message": "Loading..." 15 | }, 16 | "greeting": { 17 | "description": "Greeting message", 18 | "message": "Hello, My name is $NAME$", 19 | "placeholders": { 20 | "name": { 21 | "content": "$1", 22 | "example": "John Doe" 23 | } 24 | } 25 | }, 26 | "hello": { 27 | "description": "Placeholder example", 28 | "message": "Hello $1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/i18n/locales/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "description": "Extension description", 4 | "message": "React, Typescript, Vite를 사용한 크롬 익스텐션 보일러플레이트입니다." 5 | }, 6 | "extensionName": { 7 | "description": "Extension name", 8 | "message": "크롬 익스텐션 보일러플레이트" 9 | }, 10 | "toggleTheme": { 11 | "message": "테마 변경" 12 | }, 13 | "loading": { 14 | "message": "로딩 중..." 15 | }, 16 | "greeting": { 17 | "description": "인사 메시지", 18 | "message": "안녕하세요, 제 이름은 $NAME$입니다.", 19 | "placeholders": { 20 | "name": { 21 | "content": "$1", 22 | "example": "서종학" 23 | } 24 | } 25 | }, 26 | "hello": { 27 | "description": "Placeholder 예시", 28 | "message": "안녕 $1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/i18n", 3 | "version": "0.4.2", 4 | "description": "chrome extension - internationalization", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "main": "dist/index.mjs", 12 | "types": "index.mts", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b prepare-build.tsconfig.json && node --env-file=../../.env dist/lib/prepare_build.js && tsc -b", 19 | "lint": "eslint .", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "dependencies": { 25 | "@extension/env": "workspace:*" 26 | }, 27 | "devDependencies": { 28 | "@extension/tsconfig": "workspace:*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/i18n/prepare-build.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["lib/prepare_build.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/module-manager/README.md: -------------------------------------------------------------------------------- 1 | # Module manager 2 | 3 | Module manager is a tool to manage modules in a project. It can be used to delete or recover some pages. 4 | 5 | ## Usage 6 | 7 | ### On Root 8 | 9 | ```bash 10 | $ pnpm module-manager 11 | ``` 12 | 13 | ### On Module 14 | 15 | ```bash 16 | $ pnpm start 17 | ``` 18 | 19 | ### Choose a tool 20 | 21 | ``` 22 | ? Choose a tool (Use arrow keys) 23 | ❯ Delete Feature 24 | Recover Feature 25 | ``` 26 | 27 | ## How it works 28 | 29 | ### Delete Feature 30 | 31 | When you select an unused module, Module Manager compresses the contents of that folder, takes a snapshot of it, and removes it. It also automatically removes anything that needs to be cleared from the manifest. 32 | 33 | ### Recover Feature 34 | 35 | When you select a module that has been deleted, Module Manager will recover the module from the snapshot and add it back to the manifest. 36 | -------------------------------------------------------------------------------- /packages/module-manager/index.mts: -------------------------------------------------------------------------------- 1 | import runModuleManager from 'lib/runModuleManager.ts'; 2 | 3 | void runModuleManager(); 4 | -------------------------------------------------------------------------------- /packages/module-manager/lib/runModuleManager.ts: -------------------------------------------------------------------------------- 1 | import { select } from '@inquirer/prompts'; 2 | import * as fs from 'node:fs'; 3 | import * as path from 'node:path'; 4 | import manifest from '../../../chrome-extension/manifest.ts'; 5 | import deleteModules from './deleteModules.js'; 6 | import recoverModules from './recoverModules.ts'; 7 | import { execSync } from 'node:child_process'; 8 | 9 | const manifestPath = path.resolve(import.meta.dirname, '..', '..', '..', 'chrome-extension', 'manifest.ts'); 10 | 11 | const manifestObject = JSON.parse(JSON.stringify(manifest)) as chrome.runtime.ManifestV3; 12 | const manifestString = fs.readFileSync(manifestPath, 'utf-8'); 13 | 14 | async function runModuleManager() { 15 | const tool = await select({ 16 | message: 'Choose a tool', 17 | choices: [ 18 | { name: 'Delete Feature', value: 'delete' }, 19 | { name: 'Recover Feature', value: 'recover' }, 20 | ], 21 | }); 22 | 23 | switch (tool) { 24 | case 'delete': 25 | await deleteModules(manifestObject); 26 | break; 27 | case 'recover': 28 | await recoverModules(manifestObject); 29 | break; 30 | } 31 | 32 | const updatedManifest = manifestString 33 | .replace( 34 | /const manifest = {[\s\S]*?} satisfies/, 35 | 'const manifest = ' + JSON.stringify(manifestObject, null, 2) + ' satisfies', 36 | ) 37 | .replace(/ {2}"version": "[\s\S]*?",/, ' version: packageJson.version,'); 38 | 39 | fs.writeFileSync(manifestPath, updatedManifest); 40 | execSync('eslint --fix ' + manifestPath, { stdio: 'inherit' }); 41 | await new Promise(resolve => { 42 | setTimeout(resolve, 1500); 43 | }); 44 | execSync('pnpm install', { stdio: 'inherit' }); 45 | } 46 | 47 | export default runModuleManager; 48 | -------------------------------------------------------------------------------- /packages/module-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/module-manager", 3 | "version": "0.4.2", 4 | "description": "chrome extension - module manager", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": true, 8 | "scripts": { 9 | "start": "tsx index.mts", 10 | "lint": "eslint .", 11 | "lint:fix": "pnpm lint --fix", 12 | "prettier": "prettier . --write --ignore-path ../../.prettierignore" 13 | }, 14 | "devDependencies": { 15 | "@extension/tsconfig": "workspace:*", 16 | "@inquirer/prompts": "^7.3.2", 17 | "fflate": "^0.8.2", 18 | "tsx": "^4.19.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/module-manager/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist", 6 | "allowImportingTsExtensions": true 7 | }, 8 | "include": ["lib", "index.mts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared Package 2 | 3 | This package contains code shared with other packages. 4 | To use the code in the package, you need to add the following to the package.json file. 5 | 6 | ```json 7 | { 8 | "dependencies": { 9 | "@extension/shared": "workspace:*" 10 | } 11 | } 12 | ``` 13 | -------------------------------------------------------------------------------- /packages/shared/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/hooks/index.js'; 2 | export * from './lib/hoc/index.js'; 3 | export * from './lib/utils/index.js'; 4 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/index.ts: -------------------------------------------------------------------------------- 1 | export { withSuspense } from './withSuspense.js'; 2 | export { withErrorBoundary } from './withErrorBoundary.js'; 3 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/withErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, ErrorInfo, ReactElement } from 'react'; 2 | import { Component } from 'react'; 3 | 4 | class ErrorBoundary extends Component< 5 | { 6 | children: ReactElement; 7 | fallback: ReactElement; 8 | }, 9 | { 10 | hasError: boolean; 11 | } 12 | > { 13 | state = { hasError: false }; 14 | 15 | static getDerivedStateFromError() { 16 | return { hasError: true }; 17 | } 18 | 19 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 20 | console.error(error, errorInfo); 21 | } 22 | 23 | render() { 24 | if (this.state.hasError) { 25 | return this.props.fallback; 26 | } 27 | 28 | return this.props.children; 29 | } 30 | } 31 | 32 | export function withErrorBoundary>( 33 | Component: ComponentType, 34 | ErrorComponent: ReactElement, 35 | ) { 36 | return function WithErrorBoundary(props: T) { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/withSuspense.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, ReactElement } from 'react'; 2 | import { Suspense } from 'react'; 3 | 4 | export function withSuspense>( 5 | Component: ComponentType, 6 | SuspenseComponent: ReactElement, 7 | ) { 8 | return function WithSuspense(props: T) { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useStorage.js'; 2 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/useStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useSyncExternalStore } from 'react'; 2 | import type { BaseStorage } from '@extension/storage'; 3 | 4 | type WrappedPromise = ReturnType; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const storageMap: Map, WrappedPromise> = new Map(); 7 | 8 | export const useStorage = < 9 | Storage extends BaseStorage, 10 | Data = Storage extends BaseStorage ? Data : unknown, 11 | >( 12 | storage: Storage, 13 | ) => { 14 | const initializedRef = useRef(false); 15 | const _data = useSyncExternalStore(storage.subscribe, storage.getSnapshot); 16 | 17 | if (!storageMap.has(storage)) { 18 | storageMap.set(storage, wrapPromise(storage.get())); 19 | } 20 | if (_data !== null || initializedRef.current) { 21 | storageMap.set(storage, { read: () => _data }); 22 | initializedRef.current = true; 23 | } 24 | 25 | return (_data ?? storageMap.get(storage)!.read()) as Exclude>; 26 | }; 27 | 28 | const wrapPromise = (promise: Promise) => { 29 | let status = 'pending'; 30 | let result: R; 31 | const suspender = promise.then( 32 | r => { 33 | status = 'success'; 34 | result = r; 35 | }, 36 | e => { 37 | status = 'error'; 38 | result = e; 39 | }, 40 | ); 41 | 42 | return { 43 | read() { 44 | switch (status) { 45 | case 'pending': 46 | throw suspender; 47 | case 'error': 48 | throw result; 49 | default: 50 | return result; 51 | } 52 | }, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/shared/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shared-types.js'; 2 | -------------------------------------------------------------------------------- /packages/shared/lib/utils/shared-types.ts: -------------------------------------------------------------------------------- 1 | export type ValueOf = T[keyof T]; 2 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/shared", 3 | "version": "0.4.2", 4 | "description": "chrome extension - shared code", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b", 19 | "lint": "eslint .", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "devDependencies": { 25 | "@extension/storage": "workspace:*", 26 | "@extension/tsconfig": "workspace:*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/shared/src/types/toolCall.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared types for tool call functionality 3 | */ 4 | 5 | export interface ToolCall { 6 | serverName: string; 7 | toolName: string; 8 | arguments: Record; 9 | rawContent: string; 10 | } 11 | 12 | export interface ToolCallMessage { 13 | action: 'RENDER_TOOL_CALLS'; 14 | data: { 15 | toolCalls: ToolCall[]; 16 | nodeInfo: { 17 | path: string; 18 | textContent: string | null; 19 | }; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/storage/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/index.js'; 2 | -------------------------------------------------------------------------------- /packages/storage/lib/base/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage area type for persisting and exchanging data. 3 | * @see https://developer.chrome.com/docs/extensions/reference/storage/#overview 4 | */ 5 | export enum StorageEnum { 6 | /** 7 | * Persist data locally against browser restarts. Will be deleted by uninstalling the extension. 8 | * @default 9 | */ 10 | Local = 'local', 11 | /** 12 | * Uploads data to the users account in the cloud and syncs to the users browsers on other devices. Limits apply. 13 | */ 14 | Sync = 'sync', 15 | /** 16 | * Requires an [enterprise policy](https://www.chromium.org/administrators/configuring-policy-for-extensions) with a 17 | * json schema for company wide config. 18 | */ 19 | Managed = 'managed', 20 | /** 21 | * Only persist data until the browser is closed. Recommended for service workers which can shutdown anytime and 22 | * therefore need to restore their state. Set {@link SessionAccessLevelEnum} for permitting content scripts access. 23 | * @implements Chromes [Session Storage](https://developer.chrome.com/docs/extensions/reference/storage/#property-session) 24 | */ 25 | Session = 'session', 26 | } 27 | 28 | /** 29 | * Global access level requirement for the {@link StorageEnum.Session} Storage Area. 30 | * @implements Chromes [Session Access Level](https://developer.chrome.com/docs/extensions/reference/storage/#method-StorageArea-setAccessLevel) 31 | */ 32 | export enum SessionAccessLevelEnum { 33 | /** 34 | * Storage can only be accessed by Extension pages (not Content scripts). 35 | * @default 36 | */ 37 | ExtensionPagesOnly = 'TRUSTED_CONTEXTS', 38 | /** 39 | * Storage can be accessed by both Extension pages and Content scripts. 40 | */ 41 | ExtensionPagesAndContentScripts = 'TRUSTED_AND_UNTRUSTED_CONTEXTS', 42 | } 43 | -------------------------------------------------------------------------------- /packages/storage/lib/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js'; 2 | export * from './enums.js'; 3 | export * from './types.js'; 4 | -------------------------------------------------------------------------------- /packages/storage/lib/base/types.ts: -------------------------------------------------------------------------------- 1 | import type { StorageEnum } from './enums.js'; 2 | 3 | export type ValueOrUpdate = D | ((prev: D) => Promise | D); 4 | 5 | export type BaseStorage = { 6 | get: () => Promise; 7 | set: (value: ValueOrUpdate) => Promise; 8 | getSnapshot: () => D | null; 9 | subscribe: (listener: () => void) => () => void; 10 | }; 11 | 12 | export type StorageConfig = { 13 | /** 14 | * Assign the {@link StorageEnum} to use. 15 | * @default Local 16 | */ 17 | storageEnum?: StorageEnum; 18 | /** 19 | * Only for {@link StorageEnum.Session}: Grant Content scripts access to storage area? 20 | * @default false 21 | */ 22 | sessionAccessForContentScripts?: boolean; 23 | /** 24 | * Keeps state live in sync between all instances of the extension. Like between popup, side panel and content scripts. 25 | * To allow chrome background scripts to stay in sync as well, use {@link StorageEnum.Session} storage area with 26 | * {@link StorageConfig.sessionAccessForContentScripts} potentially also set to true. 27 | * @see https://stackoverflow.com/a/75637138/2763239 28 | * @default false 29 | */ 30 | liveUpdate?: boolean; 31 | /** 32 | * An optional props for converting values from storage and into it. 33 | * @default undefined 34 | */ 35 | serialization?: { 36 | /** 37 | * convert non-native values to string to be saved in storage 38 | */ 39 | serialize: (value: D) => string; 40 | /** 41 | * convert string value from storage to non-native values 42 | */ 43 | deserialize: (text: string) => D; 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/storage/lib/impl/exampleThemeStorage.ts: -------------------------------------------------------------------------------- 1 | import type { BaseStorage } from '../base/index.js'; 2 | import { createStorage, StorageEnum } from '../base/index.js'; 3 | 4 | type Theme = 'light' | 'dark'; 5 | 6 | type ThemeStorage = BaseStorage & { 7 | toggle: () => Promise; 8 | }; 9 | 10 | const storage = createStorage('theme-storage-key', 'light', { 11 | storageEnum: StorageEnum.Local, 12 | liveUpdate: true, 13 | }); 14 | 15 | // You can extend it with your own methods 16 | export const exampleThemeStorage: ThemeStorage = { 17 | ...storage, 18 | toggle: async () => { 19 | await storage.set(currentTheme => { 20 | return currentTheme === 'light' ? 'dark' : 'light'; 21 | }); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/storage/lib/impl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exampleThemeStorage.js'; 2 | -------------------------------------------------------------------------------- /packages/storage/lib/index.ts: -------------------------------------------------------------------------------- 1 | export type { BaseStorage } from './base/index.js'; 2 | export * from './impl/index.js'; 3 | -------------------------------------------------------------------------------- /packages/storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/storage", 3 | "version": "0.4.2", 4 | "description": "chrome extension - storage", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "ready": "tsc -b", 19 | "lint": "eslint .", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "devDependencies": { 25 | "@extension/tsconfig": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/tailwindcss-config", 3 | "version": "0.4.2", 4 | "description": "chrome extension - tailwindcss configuration", 5 | "main": "tailwind.config.ts", 6 | "private": true, 7 | "sideEffects": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/tailwind-config/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | export default { 4 | prefix: '', 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } as Omit; 10 | -------------------------------------------------------------------------------- /packages/tsconfig/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Chrome Extension App", 4 | "extends": "./base.json" 5 | } 6 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Base", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "noEmit": true, 7 | "downlevelIteration": true, 8 | "isolatedModules": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true, 17 | "noImplicitReturns": true, 18 | "jsx": "react-jsx", 19 | "module": "ESNext", 20 | "moduleResolution": "bundler", 21 | "target": "ESNext", 22 | "lib": ["DOM", "ESNext"], 23 | "types": ["node", "chrome"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/tsconfig/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Module", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "moduleResolution": "NodeNext", 8 | "module": "NodeNext" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/tsconfig", 3 | "version": "0.4.2", 4 | "description": "chrome extension - tsconfig", 5 | "private": true, 6 | "sideEffects": false 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/index'; 2 | -------------------------------------------------------------------------------- /packages/ui/lib/components/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import { exampleThemeStorage } from '@extension/storage'; 2 | import { useStorage } from '@extension/shared'; 3 | import type { ComponentPropsWithoutRef } from 'react'; 4 | import { cn } from '@/lib/utils'; 5 | 6 | type ToggleButtonProps = ComponentPropsWithoutRef<'button'>; 7 | 8 | export const ToggleButton = ({ className, children, ...props }: ToggleButtonProps) => { 9 | const theme = useStorage(exampleThemeStorage); 10 | 11 | return ( 12 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ToggleButton'; 2 | -------------------------------------------------------------------------------- /packages/ui/lib/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/ui/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/index'; 2 | export * from './utils'; 3 | export * from './withUI'; 4 | -------------------------------------------------------------------------------- /packages/ui/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from 'clsx'; 2 | import { clsx } from 'clsx'; 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export const cn = (...inputs: ClassValue[]) => { 6 | return twMerge(clsx(inputs)); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/ui/lib/withUI.ts: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | import type { Config } from 'tailwindcss'; 3 | 4 | export function withUI(tailwindConfig: Config): Config { 5 | return deepmerge(tailwindConfig, { 6 | content: ['../../packages/ui/lib/**/*.{tsx,ts,js,jsx}'], 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/ui", 3 | "version": "0.4.2", 4 | "description": "chrome extension - ui components", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": true, 8 | "files": [ 9 | "dist/**", 10 | "dist/global.css" 11 | ], 12 | "types": "dist/index.d.ts", 13 | "main": "dist/index.js", 14 | "module": "dist/index.js", 15 | "scripts": { 16 | "clean:bundle": "rimraf dist", 17 | "clean:node_modules": "pnpm dlx rimraf node_modules", 18 | "clean:turbo": "rimraf .turbo", 19 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 20 | "ready": "tsc -b && tsc-alias -p tsconfig.json", 21 | "lint": "eslint .", 22 | "lint:fix": "pnpm lint --fix", 23 | "prettier": "prettier . --write", 24 | "type-check": "tsc --noEmit" 25 | }, 26 | "dependencies": { 27 | "@extension/storage": "workspace:*", 28 | "@extension/shared": "workspace:*", 29 | "clsx": "^2.1.1", 30 | "tailwind-merge": "^2.4.0" 31 | }, 32 | "devDependencies": { 33 | "@extension/tsconfig": "workspace:*", 34 | "tsc-alias": "^1.8.10" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/base", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./*"] 8 | }, 9 | "noEmit": false, 10 | "declaration": true 11 | }, 12 | "include": ["index.ts", "lib"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vite-config/index.mts: -------------------------------------------------------------------------------- 1 | export * from './lib/index.js'; 2 | -------------------------------------------------------------------------------- /packages/vite-config/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withPageConfig.js'; 2 | -------------------------------------------------------------------------------- /packages/vite-config/lib/withPageConfig.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'vite'; 2 | import { defineConfig } from 'vite'; 3 | import { watchRebuildPlugin } from '@extension/hmr'; 4 | import react from '@vitejs/plugin-react-swc'; 5 | import deepmerge from 'deepmerge'; 6 | import env, { IS_DEV, IS_PROD } from '@extension/env'; 7 | 8 | export const watchOption = IS_DEV 9 | ? { 10 | exclude: [/\/pages\/content-ui\/dist\/.*\.(css)$/], 11 | chokidar: { 12 | awaitWriteFinish: true, 13 | ignored: [/\/packages\/.*\.(ts|tsx|map)$/, /\/pages\/content-ui\/dist\/.*/], 14 | }, 15 | } 16 | : undefined; 17 | 18 | export const withPageConfig = (config: UserConfig) => 19 | defineConfig( 20 | deepmerge( 21 | { 22 | define: { 23 | 'process.env': env, 24 | }, 25 | base: '', 26 | plugins: [react(), IS_DEV && watchRebuildPlugin({ refresh: true })], 27 | build: { 28 | sourcemap: IS_DEV, 29 | minify: IS_PROD, 30 | reportCompressedSize: IS_PROD, 31 | emptyOutDir: IS_PROD, 32 | watch: watchOption, 33 | rollupOptions: { 34 | external: ['chrome'], 35 | }, 36 | }, 37 | }, 38 | config, 39 | ), 40 | ); 41 | -------------------------------------------------------------------------------- /packages/vite-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/vite-config", 3 | "version": "0.4.2", 4 | "description": "chrome extension - vite base configuration", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": true, 8 | "types": "index.mts", 9 | "main": "dist/index.mjs", 10 | "module": "dist/index.mjs", 11 | "scripts": { 12 | "clean:node_modules": "pnpm dlx rimraf node_modules", 13 | "clean:bundle": "pnpm dlx rimraf dist", 14 | "clean": "pnpm clean:bundle && pnpm clean:node_modules", 15 | "ready": "tsc -b" 16 | }, 17 | "dependencies": { 18 | "@extension/env": "workspace:*" 19 | }, 20 | "devDependencies": { 21 | "@extension/hmr": "workspace:*", 22 | "@extension/tsconfig": "workspace:*", 23 | "@vitejs/plugin-react-swc": "^3.7.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/vite-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["lib", "index.mts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/zipper/index.mts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { zipBundle } from './lib/index.js'; 3 | import { IS_FIREFOX } from '@extension/env'; 4 | 5 | const YYYY_MM_DD = new Date().toISOString().slice(0, 10).replace(/-/g, ''); 6 | const HH_mm_ss = new Date().toISOString().slice(11, 19).replace(/:/g, ''); 7 | const fileName = `extension-${YYYY_MM_DD}-${HH_mm_ss}`; 8 | 9 | await zipBundle({ 10 | distDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist'), 11 | buildDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist-zip'), 12 | archiveName: IS_FIREFOX ? `${fileName}.xpi` : `${fileName}.zip`, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/zipper/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs'; 2 | import { posix, resolve } from 'node:path'; 3 | import fg from 'fast-glob'; 4 | import { AsyncZipDeflate, Zip } from 'fflate'; 5 | 6 | // Converts bytes to megabytes 7 | function toMB(bytes: number): number { 8 | return bytes / 1024 / 1024; 9 | } 10 | 11 | // Creates the build directory if it doesn't exist 12 | function ensureBuildDirectoryExists(buildDirectory: string): void { 13 | if (!existsSync(buildDirectory)) { 14 | mkdirSync(buildDirectory, { recursive: true }); 15 | } 16 | } 17 | 18 | // Logs the package size and duration 19 | function logPackageSize(size: number, startTime: number): void { 20 | console.log(`Zip Package size: ${toMB(size).toFixed(2)} MB in ${Date.now() - startTime}ms`); 21 | } 22 | 23 | // Handles file streaming and zipping 24 | function streamFileToZip( 25 | absPath: string, 26 | relPath: string, 27 | zip: Zip, 28 | onAbort: () => void, 29 | onError: (error: Error) => void, 30 | ): void { 31 | const data = new AsyncZipDeflate(relPath, { level: 9 }); 32 | zip.add(data); 33 | 34 | createReadStream(absPath) 35 | .on('data', chunk => { 36 | const uint8Chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); 37 | data.push(uint8Chunk, false); 38 | }) 39 | .on('end', () => data.push(new Uint8Array(0), true)) 40 | .on('error', error => { 41 | onAbort(); 42 | onError(error); 43 | }); 44 | } 45 | 46 | // Zips the bundle 47 | export const zipBundle = async ( 48 | { 49 | distDirectory, 50 | buildDirectory, 51 | archiveName, 52 | }: { 53 | distDirectory: string; 54 | buildDirectory: string; 55 | archiveName: string; 56 | }, 57 | withMaps = false, 58 | ): Promise => { 59 | ensureBuildDirectoryExists(buildDirectory); 60 | 61 | const zipFilePath = resolve(buildDirectory, archiveName); 62 | const output = createWriteStream(zipFilePath); 63 | 64 | const fileList = await fg( 65 | [ 66 | '**/*', // Pick all nested files 67 | ...(!withMaps ? ['!**/(*.js.map|*.css.map)'] : []), // Exclude source maps conditionally 68 | ], 69 | { 70 | cwd: distDirectory, 71 | onlyFiles: true, 72 | }, 73 | ); 74 | 75 | return new Promise((pResolve, pReject) => { 76 | let aborted = false; 77 | let totalSize = 0; 78 | const timer = Date.now(); 79 | const zip = new Zip((err, data, final) => { 80 | if (err) { 81 | pReject(err); 82 | } else { 83 | totalSize += data.length; 84 | output.write(data); 85 | if (final) { 86 | logPackageSize(totalSize, timer); 87 | output.end(); 88 | pResolve(); 89 | } 90 | } 91 | }); 92 | 93 | // Handle file read streams 94 | for (const file of fileList) { 95 | if (aborted) return; 96 | 97 | const absPath = resolve(distDirectory, file); 98 | const absPosixPath = posix.resolve(distDirectory, file); 99 | const relPosixPath = posix.relative(distDirectory, absPosixPath); 100 | 101 | console.log(`Adding file: ${relPosixPath}`); 102 | streamFileToZip( 103 | absPath, 104 | relPosixPath, 105 | zip, 106 | () => { 107 | aborted = true; 108 | zip.terminate(); 109 | }, 110 | error => pReject(`Error reading file ${absPath}: ${error.message}`), 111 | ); 112 | } 113 | 114 | zip.end(); 115 | }); 116 | }; 117 | -------------------------------------------------------------------------------- /packages/zipper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/zipper", 3 | "version": "0.4.2", 4 | "description": "chrome extension - zipper", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpm dlx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "zip": "node --env-file=../../.env dist/index.mjs", 19 | "lint": "eslint .", 20 | "ready": "tsc -b", 21 | "lint:fix": "pnpm lint --fix", 22 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 23 | "type-check": "tsc --noEmit" 24 | }, 25 | "devDependencies": { 26 | "@extension/tsconfig": "workspace:*", 27 | "@extension/env": "workspace:*", 28 | "fflate": "^0.8.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zipper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@extension/tsconfig/module", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["index.mts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /pages/content/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /pages/content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extension/content-script", 3 | "version": "0.4.2", 4 | "description": "chrome extension - content script", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": true, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "scripts": { 12 | "clean:node_modules": "pnpm dlx rimraf node_modules", 13 | "clean:turbo": "rimraf .turbo", 14 | "clean": "pnpm clean:turbo && pnpm clean:node_modules", 15 | "build": "vite build", 16 | "dev": "vite build --mode development", 17 | "lint": "eslint .", 18 | "lint:fix": "pnpm lint --fix", 19 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 20 | "type-check": "tsc --noEmit" 21 | }, 22 | "dependencies": { 23 | "@extension/env": "workspace:*", 24 | "@extension/shared": "workspace:*", 25 | "@extension/storage": "workspace:*", 26 | "@radix-ui/react-dialog": "^1.1.6", 27 | "@radix-ui/react-icons": "^1.3.2", 28 | "ajv": "^8.17.1", 29 | "class-variance-authority": "^0.7.1", 30 | "clsx": "^2.1.1", 31 | "fast-xml-parser": "^5.0.9", 32 | "lucide-react": "^0.477.0", 33 | "react": "19.1.0", 34 | "react-dom": "19.1.0", 35 | "shadcn-ui": "^0.9.5", 36 | "tailwind-merge": "^2.5.2", 37 | "tailwindcss-animate": "^1.0.7" 38 | }, 39 | "devDependencies": { 40 | "@extension/hmr": "workspace:*", 41 | "@extension/tsconfig": "workspace:*", 42 | "@extension/vite-config": "workspace:*", 43 | "@types/react": "^19.0.8", 44 | "@types/react-dom": "^19.0.3", 45 | "@types/webextension-polyfill": "^0.12.3", 46 | "autoprefixer": "^10.4.20", 47 | "postcss": "^8.5.2", 48 | "tailwindcss": "^3.4.17" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pages/content/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/content/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pages/content/src/adapters/adaptercomponents/aistudio.ts: -------------------------------------------------------------------------------- 1 | // AI Studio website components for MCP-SuperAssistant 2 | // Provides toggle buttons (MCP, Auto Insert, Auto Submit, Auto Execute) and state management 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/client'; 6 | import { MCPPopover } from '../../components/mcpPopover/mcpPopover'; 7 | import type { AdapterConfig, SimpleSiteAdapter } from './common'; 8 | import { initializeAdapter, ToggleStateManager, MCPToggleState } from './common'; 9 | 10 | // Keep AI Studio-specific functions or overrides 11 | 12 | // Find where to insert the MCP popover in AI Studio UI 13 | function findAIStudioButtonInsertionPoint(): { container: Element; insertAfter: Element | null } | null { 14 | // Find the main prompt input wrapper 15 | const promptInputWrapper = document.querySelector('.prompt-input-wrapper'); // Adjust selector if needed 16 | if (!promptInputWrapper) { 17 | console.warn('[AIStudio Adapter] Could not find .prompt-input-wrapper'); 18 | // Add more specific fallbacks if the structure varies 19 | const fallback = document.querySelector('footer .actions-container'); // Example fallback 20 | if (fallback) { 21 | console.debug('[AIStudio Adapter] Found fallback insertion point: footer .actions-container'); 22 | return { container: fallback, insertAfter: null }; // Append 23 | } 24 | return null; 25 | } 26 | console.debug('[AIStudio Adapter] Found insertion point: .prompt-input-wrapper'); 27 | 28 | // Find all .button-wrapper elements inside the prompt input wrapper 29 | const buttonWrappers = promptInputWrapper.querySelectorAll('.button-wrapper'); // Adjust selector if needed 30 | if (buttonWrappers.length > 0) { 31 | // Insert after the last button-wrapper 32 | const lastButtonWrapper = buttonWrappers[buttonWrappers.length - 1]; 33 | return { container: promptInputWrapper, insertAfter: lastButtonWrapper }; 34 | } 35 | 36 | // Fallback: just insert at the end of the prompt input wrapper if no button wrappers found 37 | return { container: promptInputWrapper, insertAfter: null }; 38 | } 39 | 40 | // AI Studio-specific sidebar handling (assuming similar to Gemini/common) 41 | function showAIStudioSidebar(adapter: SimpleSiteAdapter | null): void { 42 | console.debug('[AIStudio Adapter] MCP Enabled - Showing sidebar'); 43 | if (adapter?.showSidebarWithToolOutputs) { 44 | adapter.showSidebarWithToolOutputs(); 45 | } else if (adapter?.toggleSidebar) { 46 | adapter.toggleSidebar(); // Fallback 47 | } else { 48 | console.warn('[AIStudio Adapter] No method found to show sidebar.'); 49 | } 50 | } 51 | 52 | function hideAIStudioSidebar(adapter: SimpleSiteAdapter | null): void { 53 | console.debug('[AIStudio Adapter] MCP Disabled - Hiding sidebar'); 54 | if (adapter?.hideSidebar) { 55 | adapter.hideSidebar(); 56 | } else if (adapter?.sidebarManager?.hide) { 57 | adapter.sidebarManager.hide(); 58 | } else if (adapter?.toggleSidebar) { 59 | adapter.toggleSidebar(); // Fallback 60 | } else { 61 | console.warn('[AIStudio Adapter] No method found to hide sidebar.'); 62 | } 63 | } 64 | 65 | // AI Studio Adapter Configuration 66 | const aiStudioAdapterConfig: AdapterConfig = { 67 | adapterName: 'AiStudio', // Ensure this matches adapter.name check in common handlers 68 | storageKeyPrefix: 'mcp-aistudio-state', // Uses localStorage 69 | findButtonInsertionPoint: findAIStudioButtonInsertionPoint, 70 | getStorage: () => localStorage, // AI Studio uses localStorage 71 | // getCurrentURLKey: default implementation (pathname) likely fine 72 | onMCPEnabled: showAIStudioSidebar, 73 | onMCPDisabled: hideAIStudioSidebar, 74 | }; 75 | 76 | // Initialize AI Studio components using the common initializer 77 | export function initAIStudioComponents(): void { 78 | console.debug('Initializing AI Studio MCP components using common framework'); 79 | const stateManager = initializeAdapter(aiStudioAdapterConfig); 80 | 81 | // Expose manual injection for debugging (optional) 82 | window.injectMCPButtons = () => { 83 | console.debug('Manual injection for AI Studio triggered'); 84 | const insertFn = (window as any)[`injectMCPButtons_${aiStudioAdapterConfig.adapterName}`]; 85 | if (insertFn) { 86 | insertFn(); 87 | } else { 88 | console.warn('Manual injection function not found.'); 89 | } 90 | }; 91 | 92 | console.debug('AI Studio MCP components initialization complete.'); 93 | } 94 | -------------------------------------------------------------------------------- /pages/content/src/adapters/adaptercomponents/gemini.ts: -------------------------------------------------------------------------------- 1 | // Gemini website components for MCP-SuperAssistant 2 | // Provides toggle buttons (MCP, Auto Insert, Auto Submit, Auto Execute) and state management 3 | 4 | // import React from 'react'; 5 | // import ReactDOM from 'react-dom/client'; 6 | // import { MCPPopover } from '../../components/mcpPopover/mcpPopover'; 7 | import type { AdapterConfig, SimpleSiteAdapter } from './common'; 8 | import { initializeAdapter } from './common'; 9 | 10 | // Keep Gemini-specific functions or overrides 11 | 12 | // Find where to insert in Gemini UI 13 | function findGeminiButtonInsertionPoint(): { container: Element; insertAfter: Element | null } | null { 14 | // Try the primary selector first 15 | const wrapper = document.querySelector('.leading-actions-wrapper'); 16 | if (wrapper) { 17 | console.debug('[Gemini Adapter] Found insertion point: .leading-actions-wrapper'); 18 | // Try to insert after the second button for better placement 19 | const btns = wrapper.querySelectorAll('button'); 20 | const after = btns.length > 1 ? btns[1] : btns.length > 0 ? btns[0] : null; 21 | return { container: wrapper, insertAfter: after }; 22 | } 23 | 24 | // Fallback selector (example, adjust if needed) 25 | const fallbackContainer = document.querySelector('.input-area .actions'); 26 | if (fallbackContainer) { 27 | console.debug('[Gemini Adapter] Found fallback insertion point: .input-area .actions'); 28 | return { container: fallbackContainer, insertAfter: null }; // Append 29 | } 30 | 31 | console.warn('[Gemini Adapter] Could not find a suitable insertion point.'); 32 | return null; 33 | } 34 | 35 | // Gemini-specific sidebar handling 36 | function showGeminiSidebar(adapter: SimpleSiteAdapter | null): void { 37 | console.debug('[Gemini Adapter] MCP Enabled - Showing sidebar'); 38 | if (adapter?.showSidebarWithToolOutputs) { 39 | adapter.showSidebarWithToolOutputs(); 40 | } else { 41 | console.warn('[Gemini Adapter] No method found to show sidebar.'); 42 | } 43 | } 44 | 45 | function hideGeminiSidebar(adapter: SimpleSiteAdapter | null): void { 46 | console.debug('[Gemini Adapter] MCP Disabled - Hiding sidebar'); 47 | if (adapter?.hideSidebar) { 48 | adapter.hideSidebar(); 49 | } else if (adapter?.sidebarManager?.hide) { 50 | adapter.sidebarManager.hide(); 51 | } else if (adapter?.toggleSidebar) { 52 | adapter.toggleSidebar(); // Fallback 53 | } else { 54 | console.warn('[Gemini Adapter] No method found to hide sidebar.'); 55 | } 56 | } 57 | 58 | // Gemini Adapter Configuration 59 | const geminiAdapterConfig: AdapterConfig = { 60 | adapterName: 'Gemini', 61 | storageKeyPrefix: 'mcp-gemini-state', // Uses localStorage, prefix + URL path 62 | findButtonInsertionPoint: findGeminiButtonInsertionPoint, 63 | getStorage: () => localStorage, // Gemini uses localStorage 64 | // getCurrentURLKey: default implementation in common.ts (pathname) is likely fine 65 | onMCPEnabled: showGeminiSidebar, 66 | onMCPDisabled: hideGeminiSidebar, 67 | }; 68 | 69 | // Initialize Gemini components using the common initializer 70 | export function initGeminiComponents(): void { 71 | console.debug('Initializing Gemini MCP components using common framework'); 72 | const stateManager = initializeAdapter(geminiAdapterConfig); 73 | 74 | // Expose manual injection for debugging (optional) 75 | window.injectMCPButtons = () => { 76 | console.debug('Manual injection for Gemini triggered'); 77 | const insertFn = (window as any)[`injectMCPButtons_${geminiAdapterConfig.adapterName}`]; 78 | if (insertFn) { 79 | insertFn(); 80 | } else { 81 | console.warn('Manual injection function not found.'); 82 | } 83 | }; 84 | 85 | console.debug('Gemini MCP components initialization complete.'); 86 | } 87 | -------------------------------------------------------------------------------- /pages/content/src/adapters/adaptercomponents/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP-SuperAssistant Adapter Components 3 | * 4 | * This file exports the initialization functions for the adapter components 5 | * for different websites. The common logic is handled within the 'common.ts' 6 | * module and invoked by these initialization functions. 7 | */ 8 | 9 | // Export Grok components initializer 10 | export { initGrokComponents } from './grok'; 11 | 12 | // Export Perplexity components initializer 13 | export { initPerplexityComponents } from './perplexity'; 14 | 15 | // Export Gemini components initializer 16 | export { initGeminiComponents } from './gemini'; 17 | 18 | // Export ChatGPT components initializer 19 | export { initChatGPTComponents } from './chatgpt'; 20 | 21 | // Export AI Studio components initializer 22 | export { initAIStudioComponents } from './aistudio'; 23 | 24 | // Export DeepSeek components initializer 25 | export { initDeepSeekComponents } from './deepseek'; 26 | 27 | // Export Kagi components initializer 28 | export { initKagiComponents } from './kagi'; 29 | 30 | // Export T3 Chat components initializer 31 | export { initT3ChatComponents } from './t3chat'; 32 | 33 | // Note: Functions like insertToggleButtons, handleAutoInsert, handleAutoSubmit 34 | // are now part of the common framework and are not directly exported per adapter. 35 | // The initialization functions configure and start the common framework for each site. 36 | -------------------------------------------------------------------------------- /pages/content/src/adapters/adaptercomponents/openrouter.ts: -------------------------------------------------------------------------------- 1 | // OpenRouter website components for MCP-SuperAssistant 2 | // Provides toggle buttons (MCP, Auto Insert, Auto Submit, Auto Execute) and state management 3 | 4 | import type { 5 | AdapterConfig, 6 | SimpleSiteAdapter, // Assuming SimpleSiteAdapter might be needed for callbacks 7 | } from './common'; 8 | import { 9 | initializeAdapter, // Assuming SimpleSiteAdapter might be needed for callbacks 10 | } from './common'; 11 | 12 | // --- OpenRouter Specific Functions --- 13 | 14 | // Placeholder: Find where to insert the MCP button in the OpenRouter UI 15 | function findOpenRouterButtonInsertionPoint(): { container: Element; insertAfter: Element | null } | null { 16 | // Selector for the container div holding the buttons 17 | const containerSelector = '.relative.flex.w-full.min-w-0.px-1.py-1'; 18 | const container = document.querySelector(containerSelector); 19 | 20 | if (container) { 21 | // Selector for the 'Web Search' button 22 | const webSearchButtonSelector = 'button[title="Enable Web Search"]'; 23 | const webSearchButton = container.querySelector(webSearchButtonSelector); 24 | 25 | if (webSearchButton) { 26 | console.log('[OpenRouter Adapter] Found insertion point after Web Search button.'); 27 | return { container: container, insertAfter: webSearchButton }; 28 | } else { 29 | console.warn( 30 | '[OpenRouter Adapter] Found container, but could not find Web Search button. Appending to container.', 31 | ); 32 | // Fallback: Append to the container if the specific button isn't found 33 | return { container: container, insertAfter: null }; 34 | } 35 | } 36 | 37 | console.warn(`[OpenRouter Adapter] Could not find insertion container: ${containerSelector}`); 38 | return null; 39 | } 40 | 41 | // Placeholder: Define actions when MCP is enabled (if any specific UI changes are needed) 42 | function onOpenRouterMCPEnabled(adapter: SimpleSiteAdapter | null): void { 43 | console.log('[OpenRouter Adapter] MCP Enabled - Showing sidebar.'); 44 | // Use the adapter's sidebarManager to show the sidebar 45 | if (adapter?.sidebarManager?.show) { 46 | adapter.sidebarManager.show(); 47 | } else { 48 | console.warn('[OpenRouter Adapter] Could not find sidebarManager.show() method on adapter.'); 49 | // Optional Fallback: Try a generic toggle if show isn't available 50 | // adapter?.toggleSidebar?.(); 51 | } 52 | } 53 | 54 | // Placeholder: Define actions when MCP is disabled 55 | function onOpenRouterMCPDisabled(adapter: SimpleSiteAdapter | null): void { 56 | console.log('[OpenRouter Adapter] MCP Disabled - Hiding sidebar.'); 57 | // Use the adapter's sidebarManager to hide the sidebar 58 | if (adapter?.sidebarManager?.hide) { 59 | adapter.sidebarManager.hide(); 60 | } else { 61 | console.warn('[OpenRouter Adapter] Could not find sidebarManager.hide() method on adapter.'); 62 | // Optional Fallback: Try a generic toggle if hide isn't available 63 | // adapter?.toggleSidebar?.(); 64 | } 65 | } 66 | 67 | // --- OpenRouter Adapter Configuration --- 68 | 69 | const openRouterAdapterConfig: AdapterConfig = { 70 | adapterName: 'OpenRouter', 71 | storageKeyPrefix: 'mcp-openrouter-state', // Unique prefix for OpenRouter state 72 | findButtonInsertionPoint: findOpenRouterButtonInsertionPoint, 73 | getStorage: () => localStorage, // Assuming OpenRouter uses localStorage, adjust if not 74 | // getCurrentURLKey: Use default (pathname) unless OpenRouter needs specific URL handling 75 | onMCPEnabled: onOpenRouterMCPEnabled, // Optional: Add if specific actions needed 76 | onMCPDisabled: onOpenRouterMCPDisabled, // Optional: Add if specific actions needed 77 | }; 78 | 79 | // --- Initialization --- 80 | 81 | export function initOpenRouterComponents(): void { 82 | console.log('Initializing OpenRouter MCP components using common framework'); 83 | const stateManager = initializeAdapter(openRouterAdapterConfig); 84 | 85 | // Optional: Expose manual injection for debugging 86 | (window as any).injectMCPButtons_OpenRouter = () => { 87 | console.log('Manual injection for OpenRouter triggered'); 88 | const insertFn = (window as any)[`injectMCPButtons_${openRouterAdapterConfig.adapterName}`]; 89 | if (insertFn) { 90 | insertFn(); 91 | } else { 92 | console.warn('Manual injection function not found for OpenRouter.'); 93 | } 94 | }; 95 | 96 | console.log('OpenRouter MCP components initialization complete.'); 97 | } 98 | -------------------------------------------------------------------------------- /pages/content/src/adapters/aistudioAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AiStudio Adapter 3 | * 4 | * This file implements the site adapter for aistudio.google.com 5 | */ 6 | 7 | import { BaseAdapter } from './common'; 8 | import { logMessage } from '../utils/helpers'; 9 | import { insertToolResultToChatInput } from '../components/websites/aistudio'; 10 | import { SidebarManager } from '../components/sidebar'; 11 | import { attachFileToChatInput, submitChatInput } from '../components/websites/aistudio/chatInputHandler'; 12 | import { initAIStudioComponents } from './adaptercomponents'; 13 | 14 | export class AiStudioAdapter extends BaseAdapter { 15 | name = 'AiStudio'; 16 | hostname = ['aistudio.google.com']; 17 | 18 | // Property to store the last URL 19 | private lastUrl: string = ''; 20 | // Property to store the interval ID 21 | private urlCheckInterval: number | null = null; 22 | 23 | constructor() { 24 | super(); 25 | // Create the sidebar manager instance 26 | this.sidebarManager = SidebarManager.getInstance('aistudio'); 27 | logMessage('Created AiStudio sidebar manager instance'); 28 | } 29 | 30 | protected initializeSidebarManager(): void { 31 | this.sidebarManager.initialize(); 32 | } 33 | 34 | protected initializeObserver(forceReset: boolean = false): void { 35 | // Check the current URL immediately 36 | // this.checkCurrentUrl(); 37 | 38 | // Initialize AI Studio components 39 | initAIStudioComponents(); 40 | 41 | // Start URL checking to handle navigation within AiStudio 42 | // if (!this.urlCheckInterval) { 43 | // this.lastUrl = window.location.href; 44 | // this.urlCheckInterval = window.setInterval(() => { 45 | // const currentUrl = window.location.href; 46 | 47 | // if (currentUrl !== this.lastUrl) { 48 | // logMessage(`URL changed from ${this.lastUrl} to ${currentUrl}`); 49 | // this.lastUrl = currentUrl; 50 | 51 | // initAIStudioComponents(); 52 | // // Check if we should show or hide the sidebar based on URL 53 | // this.checkCurrentUrl(); 54 | // } 55 | // }, 1000); // Check every second 56 | // } 57 | } 58 | 59 | cleanup(): void { 60 | // Clear interval for URL checking 61 | if (this.urlCheckInterval) { 62 | window.clearInterval(this.urlCheckInterval); 63 | this.urlCheckInterval = null; 64 | } 65 | 66 | // Call the parent cleanup method 67 | super.cleanup(); 68 | } 69 | 70 | /** 71 | * Insert text into the AiStudio input field 72 | * @param text Text to insert 73 | */ 74 | insertTextIntoInput(text: string): void { 75 | insertToolResultToChatInput(text); 76 | logMessage(`Inserted text into AiStudio input: ${text.substring(0, 20)}...`); 77 | } 78 | 79 | /** 80 | * Trigger submission of the AiStudio input form 81 | */ 82 | triggerSubmission(): void { 83 | // Use the function to submit the form 84 | submitChatInput() 85 | .then((success: boolean) => { 86 | logMessage(`Triggered AiStudio form submission: ${success ? 'success' : 'failed'}`); 87 | }) 88 | .catch((error: Error) => { 89 | logMessage(`Error triggering AiStudio form submission: ${error}`); 90 | }); 91 | } 92 | 93 | /** 94 | * Check if AiStudio supports file upload 95 | * @returns true if file upload is supported 96 | */ 97 | supportsFileUpload(): boolean { 98 | return true; 99 | } 100 | 101 | /** 102 | * Attach a file to the AiStudio input 103 | * @param file The file to attach 104 | * @returns Promise that resolves to true if successful 105 | */ 106 | async attachFile(file: File): Promise { 107 | try { 108 | const result = await attachFileToChatInput(file); 109 | return result; 110 | } catch (error) { 111 | const errorMessage = error instanceof Error ? error.message : String(error); 112 | logMessage(`Error in adapter when attaching file to AiStudio input: ${errorMessage}`); 113 | console.error('Error in adapter when attaching file to AiStudio input:', error); 114 | return false; 115 | } 116 | } 117 | 118 | /** 119 | * Check the current URL and show/hide sidebar accordingly 120 | */ 121 | private checkCurrentUrl(): void { 122 | const currentUrl = window.location.href; 123 | logMessage(`Checking current AiStudio URL: ${currentUrl}`); 124 | 125 | // For AiStudio, we want to show the sidebar on all pages 126 | // You can customize this with specific URL patterns if needed 127 | if (this.sidebarManager && !this.sidebarManager.getIsVisible()) { 128 | logMessage('Showing sidebar for AiStudio URL'); 129 | this.sidebarManager.showWithToolOutputs(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pages/content/src/adapters/chatgptAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ChatGPT Adapter 3 | * 4 | * This file implements the site adapter for chatgpt.com 5 | */ 6 | 7 | import { BaseAdapter } from './common'; 8 | import { logMessage } from '../utils/helpers'; 9 | import { insertToolResultToChatInput, attachFileToChatInput, submitChatInput } from '../components/websites/chatgpt'; 10 | import { SidebarManager } from '../components/sidebar'; 11 | import { initChatGPTComponents } from './adaptercomponents'; 12 | export class ChatGptAdapter extends BaseAdapter { 13 | name = 'ChatGPT'; 14 | hostname = ['chat.openai.com', 'chatgpt.com']; 15 | 16 | // Properties to track navigation 17 | private lastUrl: string = ''; 18 | private urlCheckInterval: number | null = null; 19 | 20 | constructor() { 21 | super(); 22 | // Create the sidebar manager instance 23 | this.sidebarManager = SidebarManager.getInstance('chatgpt'); 24 | logMessage('Created ChatGPT sidebar manager instance'); 25 | // initChatGPTComponents(); 26 | } 27 | 28 | protected initializeSidebarManager(): void { 29 | this.sidebarManager.initialize(); 30 | } 31 | 32 | protected initializeObserver(forceReset: boolean = false): void { 33 | // super.initializeObserver(forceReset); 34 | initChatGPTComponents(); 35 | 36 | // Start URL checking to handle navigation within AiStudio 37 | if (!this.urlCheckInterval) { 38 | this.lastUrl = window.location.href; 39 | this.urlCheckInterval = window.setInterval(() => { 40 | const currentUrl = window.location.href; 41 | 42 | if (currentUrl !== this.lastUrl) { 43 | logMessage(`URL changed from ${this.lastUrl} to ${currentUrl}`); 44 | this.lastUrl = currentUrl; 45 | 46 | initChatGPTComponents(); 47 | // Check if we should show or hide the sidebar based on URL 48 | this.checkCurrentUrl(); 49 | } 50 | }, 1000); // Check every second 51 | } 52 | } 53 | 54 | cleanup(): void { 55 | // Clear interval for URL checking 56 | if (this.urlCheckInterval) { 57 | window.clearInterval(this.urlCheckInterval); 58 | this.urlCheckInterval = null; 59 | } 60 | 61 | // Call the parent cleanup method 62 | super.cleanup(); 63 | } 64 | 65 | /** 66 | * Insert text into the ChatGPT input field 67 | * @param text Text to insert 68 | */ 69 | insertTextIntoInput(text: string): void { 70 | insertToolResultToChatInput(text); 71 | logMessage(`Inserted text into ChatGPT input: ${text.substring(0, 20)}...`); 72 | } 73 | 74 | /** 75 | * Trigger submission of the ChatGPT input form 76 | */ 77 | triggerSubmission(): void { 78 | // Use the function to submit the form 79 | submitChatInput() 80 | .then((success: boolean) => { 81 | logMessage(`Triggered ChatGPT form submission: ${success ? 'success' : 'failed'}`); 82 | }) 83 | .catch((error: Error) => { 84 | logMessage(`Error triggering ChatGPT form submission: ${error}`); 85 | }); 86 | } 87 | 88 | /** 89 | * Check if ChatGPT supports file upload 90 | * @returns true if file upload is supported 91 | */ 92 | supportsFileUpload(): boolean { 93 | return true; 94 | } 95 | 96 | /** 97 | * Attach a file to the ChatGPT input 98 | * @param file The file to attach 99 | * @returns Promise that resolves to true if successful 100 | */ 101 | async attachFile(file: File): Promise { 102 | try { 103 | const result = await attachFileToChatInput(file); 104 | return result; 105 | } catch (error) { 106 | const errorMessage = error instanceof Error ? error.message : String(error); 107 | logMessage(`Error in adapter when attaching file to ChatGPT input: ${errorMessage}`); 108 | console.error('Error in adapter when attaching file to ChatGPT input:', error); 109 | return false; 110 | } 111 | } 112 | 113 | /** 114 | * Force a full document scan for tool commands 115 | * This is useful when we suspect tool commands might have been missed 116 | */ 117 | public forceFullScan(): void { 118 | logMessage('Forcing full document scan for ChatGPT'); 119 | } 120 | 121 | /** 122 | * Check the current URL and show/hide sidebar accordingly 123 | */ 124 | private checkCurrentUrl(): void { 125 | const currentUrl = window.location.href; 126 | logMessage(`Checking current Chatgpt URL: ${currentUrl}`); 127 | 128 | // For AiStudio, we want to show the sidebar on all pages 129 | // You can customize this with specific URL patterns if needed 130 | if (this.sidebarManager && !this.sidebarManager.getIsVisible()) { 131 | logMessage('Showing sidebar for Chatgpt URL'); 132 | this.sidebarManager.showWithToolOutputs(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pages/content/src/adapters/common/baseAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Site Adapter 3 | * 4 | * This file implements a base adapter class with common functionality 5 | * that can be extended by site-specific adapters. 6 | */ 7 | 8 | import type { SiteAdapter } from '../../utils/siteAdapter'; 9 | import { logMessage } from '../../utils/helpers'; 10 | 11 | export abstract class BaseAdapter implements SiteAdapter { 12 | abstract name: string; 13 | abstract hostname: string | string[]; 14 | urlPatterns?: RegExp[]; 15 | protected sidebarManager: any = null; 16 | // protected toolDetector: SimpleToolDetector = createToolDetector(); 17 | 18 | // Abstract methods that must be implemented by site-specific adapters 19 | protected abstract initializeObserver(forceReset?: boolean): void; 20 | protected initializeSidebarManager(): void { 21 | // Default implementation - can be overridden by subclasses 22 | if (this.sidebarManager) { 23 | this.sidebarManager.initialize(); 24 | } 25 | } 26 | 27 | // Abstract methods for text insertion and form submission 28 | abstract insertTextIntoInput(text: string): void; 29 | abstract triggerSubmission(): void; 30 | 31 | initialize(): void { 32 | logMessage(`Initializing ${this.name} adapter`); 33 | 34 | // Initialize the sidebar manager if it exists 35 | if (this.sidebarManager) { 36 | logMessage(`Initializing sidebar manager for ${this.name}`); 37 | this.initializeSidebarManager(); 38 | } else { 39 | logMessage(`No sidebar manager found for ${this.name}`); 40 | } 41 | 42 | // Initialize the unified observer 43 | logMessage(`Initializing unified observer for ${this.name} elements`); 44 | this.initializeObserver(true); 45 | } 46 | 47 | cleanup(): void { 48 | logMessage(`Cleaning up ${this.name} adapter`); 49 | 50 | if (this.sidebarManager) { 51 | this.sidebarManager.destroy(); 52 | this.sidebarManager = null; 53 | } 54 | } 55 | 56 | /** 57 | * Show the sidebar with tool outputs 58 | */ 59 | showSidebarWithToolOutputs(): void { 60 | if (this.sidebarManager) { 61 | this.sidebarManager.showWithToolOutputs(); 62 | logMessage('Showing sidebar with tool outputs'); 63 | } 64 | } 65 | 66 | toggleSidebar(): void { 67 | if (this.sidebarManager) { 68 | if (this.sidebarManager.getIsVisible()) { 69 | this.sidebarManager.hide(); 70 | } else { 71 | this.sidebarManager.showWithToolOutputs(); 72 | logMessage('Showing sidebar with tool outputs'); 73 | } 74 | } 75 | } 76 | 77 | updateConnectionStatus(isConnected: boolean): void { 78 | logMessage(`Updating ${this.name} connection status: ${isConnected}`); 79 | // Implement connection status update if needed 80 | // if (this.overlayManager) { 81 | // this.overlayManager.updateConnectionStatus(isConnected); 82 | // } 83 | } 84 | 85 | /** 86 | * Force refresh the sidebar content 87 | * This can be called to manually refresh the sidebar when needed 88 | */ 89 | refreshSidebarContent(): void { 90 | logMessage(`Forcing sidebar content refresh for ${this.name}`); 91 | if (this.sidebarManager) { 92 | this.sidebarManager.refreshContent(); 93 | logMessage('Sidebar content refreshed'); 94 | } 95 | } 96 | 97 | /** 98 | * Check if the site supports file upload 99 | * Default implementation returns false, override in site-specific adapters if supported 100 | */ 101 | supportsFileUpload(): boolean { 102 | return false; 103 | } 104 | 105 | /** 106 | * Attach a file to the chat input 107 | * Default implementation returns a rejected promise, override in site-specific adapters if supported 108 | * @param file The file to attach 109 | */ 110 | async attachFile(file: File): Promise { 111 | logMessage(`File attachment not supported for ${this.name}`); 112 | return Promise.resolve(false); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pages/content/src/adapters/common/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common Adapter Components 3 | * 4 | * This file exports common adapter functionality that can be used across different site adapters. 5 | */ 6 | 7 | export * from './baseAdapter'; 8 | -------------------------------------------------------------------------------- /pages/content/src/adapters/openrouterAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ChatGPT Adapter 3 | * 4 | * This file implements the site adapter for openrouter.ai 5 | */ 6 | 7 | import { BaseAdapter } from './common'; 8 | import { logMessage } from '../utils/helpers'; 9 | import { insertToolResultToChatInput, attachFileToChatInput, submitChatInput } from '../components/websites/openrouter'; 10 | import { SidebarManager } from '../components/sidebar'; 11 | import { initOpenRouterComponents } from './adaptercomponents/openrouter'; 12 | 13 | /** 14 | * OpenRouter Adapter 15 | */ 16 | export class OpenRouterAdapter extends BaseAdapter { 17 | name = 'OpenRouter'; 18 | hostname = ['openrouter.ai']; 19 | 20 | // Properties to track navigation 21 | private lastUrl: string = ''; 22 | private urlCheckInterval: number | null = null; 23 | 24 | constructor() { 25 | super(); 26 | // Create the sidebar manager instance 27 | this.sidebarManager = SidebarManager.getInstance('openrouter'); 28 | logMessage('Created OpenRouter sidebar manager instance'); 29 | initOpenRouterComponents(); 30 | } 31 | 32 | protected initializeSidebarManager(): void { 33 | this.sidebarManager.initialize(); 34 | } 35 | 36 | protected initializeObserver(forceReset: boolean = false): void {} 37 | 38 | cleanup(): void { 39 | // Clear interval for URL checking 40 | if (this.urlCheckInterval) { 41 | window.clearInterval(this.urlCheckInterval); 42 | this.urlCheckInterval = null; 43 | } 44 | 45 | // Call the parent cleanup method 46 | super.cleanup(); 47 | } 48 | 49 | /** 50 | * Insert text into the OpenRouter input field 51 | * @param text Text to insert 52 | */ 53 | insertTextIntoInput(text: string): void { 54 | insertToolResultToChatInput(text); 55 | logMessage(`Inserted text into OpenRouter input: ${text.substring(0, 20)}...`); 56 | } 57 | 58 | /** 59 | * Trigger submission of the OpenRouter input form 60 | */ 61 | triggerSubmission(): void { 62 | // Use the function to submit the form 63 | submitChatInput() 64 | .then((success: boolean) => { 65 | logMessage(`Triggered OpenRouter form submission: ${success ? 'success' : 'failed'}`); 66 | }) 67 | .catch((error: Error) => { 68 | logMessage(`Error triggering OpenRouter form submission: ${error}`); 69 | }); 70 | } 71 | 72 | /** 73 | * Check if OpenRouter supports file upload 74 | * @returns true if file upload is supported 75 | */ 76 | supportsFileUpload(): boolean { 77 | return true; 78 | } 79 | 80 | /** 81 | * Attach a file to the OpenRouter input 82 | * @param file The file to attach 83 | * @returns Promise that resolves to true if successful 84 | */ 85 | async attachFile(file: File): Promise { 86 | try { 87 | const result = await attachFileToChatInput(file); 88 | return result; 89 | } catch (error) { 90 | const errorMessage = error instanceof Error ? error.message : String(error); 91 | logMessage(`Error in adapter when attaching file to OpenRouter input: ${errorMessage}`); 92 | console.error('Error in adapter when attaching file to OpenRouter input:', error); 93 | return false; 94 | } 95 | } 96 | 97 | /** 98 | * Force a full document scan for tool commands 99 | * This is useful when we suspect tool commands might have been missed 100 | */ 101 | public forceFullScan(): void { 102 | logMessage('Forcing full document scan for OpenRouter'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pages/content/src/components/sidebar/InputArea/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | import type { KeyboardEvent } from 'react'; 3 | import { useState } from 'react'; 4 | import { Typography, Icon, Button } from '../ui'; 5 | import { cn } from '@src/lib/utils'; 6 | import { Card, CardHeader, CardContent } from '@src/components/ui/card'; 7 | 8 | interface InputAreaProps { 9 | onSubmit: (text: string) => void; 10 | onToggleMinimize: () => void; 11 | } 12 | 13 | const InputArea: React.FC = ({ onSubmit, onToggleMinimize }) => { 14 | const [inputText, setInputText] = useState(''); 15 | const [isSubmitting, setIsSubmitting] = useState(false); 16 | 17 | const handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | if (inputText.trim()) { 20 | setIsSubmitting(true); 21 | try { 22 | // Format as user input 23 | const processedText = `\n${inputText}\n`; 24 | 25 | // Wait 200ms before submitting 26 | await new Promise(resolve => setTimeout(resolve, 300)); 27 | onSubmit(processedText); 28 | await new Promise(resolve => setTimeout(resolve, 100)); 29 | setInputText(''); 30 | } catch (error) { 31 | console.error('Error submitting input:', error); 32 | } finally { 33 | setIsSubmitting(false); 34 | } 35 | } 36 | }; 37 | 38 | const handleKeyDown = (e: KeyboardEvent) => { 39 | // If Enter is pressed without Shift, submit the form 40 | if (e.key === 'Enter' && !e.shiftKey) { 41 | e.preventDefault(); 42 | if (inputText.trim()) { 43 | handleSubmit(e as unknown as React.FormEvent); 44 | } 45 | } 46 | // If Shift+Enter, allow default behavior (new line) 47 | }; 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | Input Area 55 | 56 | {/* */} 59 | 60 | 61 |
62 |
63 |