mirror of
https://github.com/twentyhq/twenty.git
synced 2026-04-18 05:54:42 -04:00
# Twenty Browser Extension A Chrome browser extension for capturing LinkedIn profiles (people and companies) directly into Twenty CRM. This is a basic **v0** focused mostly on establishing a architectural foundation. ## Overview This extension integrates with LinkedIn to extract profile information and create records in Twenty CRM. It uses **WXT** as the framework - initially tried Plasmo, but found WXT to be significantly better due to its extensibility and closer alignment with the Chrome extension APIs, providing more control and flexibility. ## Architecture ### Package Structure The extension consists of two main packages: 1. **`twenty-browser-extension`** - The main extension package (WXT + React) 2. **`twenty-apps/browser-extension`** - Serverless functions for API interactions ### Extension Components #### Entrypoints - **Background Script** (`src/entrypoints/background/index.ts`) - Handles extension messaging protocol - Manages API calls to serverless functions - Coordinates communication between content scripts and popup - **Content Scripts** - **`add-person.content`** - Injects UI button on LinkedIn person profiles - **`add-company.content`** - Injects UI button on LinkedIn company profiles - Both scripts use WXT's `createIntegratedUi` for seamless DOM injection - Extract profile data from LinkedIn DOM - **Popup** (`src/entrypoints/popup/`) - React-based popup UI - Displays extracted profile information - Provides buttons to save person/company to Twenty #### Messaging System Uses `@webext-core/messaging` for type-safe communication between extension components: ```typescript // Defined in src/utils/messaging.ts - getPersonviaRelay() - Relays extraction from content script - getCompanyviaRelay() - Relays extraction from content script - extractPerson() - Extracts person data from LinkedIn DOM - extractCompany() - Extracts company data from LinkedIn DOM - createPerson() - Creates person record via serverless function - createCompany() - Creates company record via serverless function - openPopup() - Opens extension popup ``` #### Serverless Functions Located in `packages/twenty-apps/browser-extension/serverlessFunctions/`: - **`/s/create/person`** - Creates a new person record in Twenty - **`/s/create/company`** - Creates a new company record in Twenty - **`/s/get/person`** - Retrieves existing person record (placeholder) - **`/s/get/company`** - Retrieves existing company record (placeholder) ## Development Guide ### Prerequisites - Twenty CLI installed globally: `npm install -g twenty-cli` - API key from Twenty: https://twenty.com/settings/api-webhooks ### Setup ``` 1. **Configure environment variables:** - Set `TWENTY_API_URL` in the serverless function configuration - Set `TWENTY_API_KEY` (marked as secret) in the serverless function configuration - For local development, create a `.env` file or configure via `wxt.config.ts` ### Development Commands ```bash # Start development server with hot reload npx nx run dev twenty-browser-extension # Build for production npx nx run build twenty-browser-extension # Package extension for distribution npx nx run package twenty-browser-extension ``` ### Development Workflow 1. **Start the dev server:** ```bash npx nx run dev twenty-browser-extension ``` This starts WXT in development mode with hot module reloading. 2. **Load extension in Chrome:** - Navigate to `chrome://extensions/` - Enable "Developer mode" - Click "Load unpacked" - Select `packages/twenty-browser-extension/dist/chrome-mv3-dev/` 3. **Test on LinkedIn:** - Navigate to a LinkedIn person profile: `https://www.linkedin.com/in/...` - Navigate to a LinkedIn company profile: `https://www.linkedin.com/company/...` - The "Add to Twenty" button should appear in the profile header - Click the button to open the popup and save to Twenty ### Project Structure ``` packages/twenty-browser-extension/ ├── src/ │ ├── common/ │ │ └── constants/ # LinkedIn URL patterns │ ├── entrypoints/ │ │ ├── background/ # Background service worker │ │ ├── popup/ # Extension popup UI │ │ ├── add-person.content/ # Content script for person profiles │ │ └── add-company.content/ # Content script for company profiles │ ├── ui/ # Shared UI components and theme │ └── utils/ # Messaging utilities ├── public/ # Static assets (icons) ├── wxt.config.ts # WXT configuration └── project.json # Nx project configuration ``` ## Current Status (v0) This is a foundational version focused on architecture. Current features: ✅ Inject UI buttons into LinkedIn profiles ✅ Extract person and company data from LinkedIn ✅ Display extracted data in popup ✅ Create person records in Twenty ✅ Create company records in Twenty ## Planned Features - [ ] Provide a way to have API key and custom remote URLs. - [ ] Detect if record already exists and prevent duplicates - [ ] Open existing Twenty record when clicked (instead of creating duplicate) - [ ] Sidepanel Overlay UI for rich profile viewing/editing - [ ] Enhanced data extraction (email, phone, etc.) - [ ] Better error handling # Demo https://github.com/user-attachments/assets/0bbed724-a429-4af0-a0f1-fdad6997685e https://github.com/user-attachments/assets/85d2301d-19ee-43ba-b7f9-13ed3915f676
241 lines
7.6 KiB
JSON
241 lines
7.6 KiB
JSON
{
|
|
"private": true,
|
|
"dependencies": {
|
|
"@apollo/client": "^3.7.17",
|
|
"@date-fns/tz": "^1.4.1",
|
|
"@emotion/react": "^11.11.1",
|
|
"@emotion/styled": "^11.11.0",
|
|
"@floating-ui/react": "^0.24.3",
|
|
"@linaria/core": "^6.2.0",
|
|
"@linaria/react": "^6.2.1",
|
|
"@radix-ui/colors": "^3.0.0",
|
|
"@sentry/profiling-node": "^9.26.0",
|
|
"@sentry/react": "^9.26.0",
|
|
"@sniptt/guards": "^0.2.0",
|
|
"@tabler/icons-react": "^3.31.0",
|
|
"@wyw-in-js/vite": "^0.7.0",
|
|
"archiver": "^7.0.1",
|
|
"danger-plugin-todos": "^1.3.1",
|
|
"date-fns": "^2.30.0",
|
|
"date-fns-tz": "^2.0.0",
|
|
"deep-equal": "^2.2.2",
|
|
"file-type": "16.5.4",
|
|
"framer-motion": "^11.18.0",
|
|
"fuse.js": "^7.1.0",
|
|
"googleapis": "105",
|
|
"hex-rgb": "^5.0.0",
|
|
"immer": "^10.1.1",
|
|
"libphonenumber-js": "^1.10.26",
|
|
"lodash.camelcase": "^4.3.0",
|
|
"lodash.chunk": "^4.2.0",
|
|
"lodash.compact": "^3.0.1",
|
|
"lodash.escaperegexp": "^4.1.2",
|
|
"lodash.groupby": "^4.6.0",
|
|
"lodash.identity": "^3.0.0",
|
|
"lodash.isempty": "^4.4.0",
|
|
"lodash.isequal": "^4.5.0",
|
|
"lodash.isobject": "^3.0.2",
|
|
"lodash.kebabcase": "^4.1.1",
|
|
"lodash.mapvalues": "^4.6.0",
|
|
"lodash.merge": "^4.6.2",
|
|
"lodash.omit": "^4.5.0",
|
|
"lodash.pickby": "^4.6.0",
|
|
"lodash.snakecase": "^4.1.1",
|
|
"lodash.upperfirst": "^4.3.1",
|
|
"microdiff": "^1.3.2",
|
|
"planer": "^1.2.0",
|
|
"pluralize": "^8.0.0",
|
|
"react": "^18.2.0",
|
|
"react-dom": "^18.2.0",
|
|
"react-responsive": "^9.0.2",
|
|
"react-router-dom": "^6.4.4",
|
|
"react-tooltip": "^5.13.1",
|
|
"recoil": "^0.7.7",
|
|
"remark-gfm": "^3.0.1",
|
|
"rxjs": "^7.2.0",
|
|
"semver": "^7.5.4",
|
|
"slash": "^5.1.0",
|
|
"storybook-addon-mock-date": "^0.6.0",
|
|
"ts-key-enum": "^2.0.12",
|
|
"tslib": "^2.8.1",
|
|
"type-fest": "4.10.1",
|
|
"typescript": "5.9.2",
|
|
"uuid": "^9.0.0",
|
|
"vite-tsconfig-paths": "^4.2.1",
|
|
"xlsx-ugnis": "^0.19.3",
|
|
"zod": "^4.1.11"
|
|
},
|
|
"devDependencies": {
|
|
"@babel/core": "^7.14.5",
|
|
"@babel/preset-react": "^7.14.5",
|
|
"@babel/preset-typescript": "^7.24.6",
|
|
"@chromatic-com/storybook": "^3",
|
|
"@graphql-codegen/cli": "^3.3.1",
|
|
"@graphql-codegen/client-preset": "^4.1.0",
|
|
"@graphql-codegen/typescript": "^3.0.4",
|
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
|
"@nx/eslint": "21.3.11",
|
|
"@nx/eslint-plugin": "21.3.11",
|
|
"@nx/jest": "21.3.11",
|
|
"@nx/js": "21.3.11",
|
|
"@nx/react": "21.3.11",
|
|
"@nx/storybook": "21.3.11",
|
|
"@nx/vite": "21.3.11",
|
|
"@nx/web": "21.3.11",
|
|
"@playwright/test": "^1.46.0",
|
|
"@sentry/types": "^8",
|
|
"@storybook/addon-actions": "8.6.14",
|
|
"@storybook/addon-coverage": "^1.0.0",
|
|
"@storybook/addon-essentials": "8.6.14",
|
|
"@storybook/addon-interactions": "8.6.14",
|
|
"@storybook/addon-links": "8.6.14",
|
|
"@storybook/blocks": "8.6.14",
|
|
"@storybook/core-server": "8.6.14",
|
|
"@storybook/icons": "^1.2.9",
|
|
"@storybook/preview-api": "8.6.14",
|
|
"@storybook/react": "8.6.14",
|
|
"@storybook/react-vite": "8.6.14",
|
|
"@storybook/test": "8.6.14",
|
|
"@storybook/test-runner": "^0.23.0",
|
|
"@storybook/types": "8.6.14",
|
|
"@stylistic/eslint-plugin": "^1.5.0",
|
|
"@swc-node/register": "1.8.0",
|
|
"@swc/cli": "^0.3.12",
|
|
"@swc/core": "1.13.3",
|
|
"@swc/helpers": "~0.5.2",
|
|
"@swc/jest": "^0.2.39",
|
|
"@testing-library/jest-dom": "^6.6.3",
|
|
"@testing-library/react": "^16.3.0",
|
|
"@types/addressparser": "^1.0.3",
|
|
"@types/bcrypt": "^5.0.0",
|
|
"@types/bytes": "^3.1.1",
|
|
"@types/chrome": "^0.0.267",
|
|
"@types/deep-equal": "^1.0.1",
|
|
"@types/express": "^4.17.13",
|
|
"@types/fs-extra": "^11.0.4",
|
|
"@types/graphql-fields": "^1.3.6",
|
|
"@types/inquirer": "^9.0.9",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/lodash.camelcase": "^4.3.7",
|
|
"@types/lodash.compact": "^3.0.9",
|
|
"@types/lodash.escaperegexp": "^4.1.9",
|
|
"@types/lodash.groupby": "^4.6.9",
|
|
"@types/lodash.identity": "^3.0.9",
|
|
"@types/lodash.isempty": "^4.4.7",
|
|
"@types/lodash.isequal": "^4.5.7",
|
|
"@types/lodash.isobject": "^3.0.7",
|
|
"@types/lodash.kebabcase": "^4.1.7",
|
|
"@types/lodash.mapvalues": "^4.6.9",
|
|
"@types/lodash.omit": "^4.5.9",
|
|
"@types/lodash.pickby": "^4.6.9",
|
|
"@types/lodash.snakecase": "^4.1.7",
|
|
"@types/lodash.upperfirst": "^4.3.7",
|
|
"@types/ms": "^0.7.31",
|
|
"@types/node": "^24.0.0",
|
|
"@types/passport-google-oauth20": "^2.0.11",
|
|
"@types/passport-jwt": "^3.0.8",
|
|
"@types/passport-microsoft": "^2.1.0",
|
|
"@types/pluralize": "^0.0.33",
|
|
"@types/react": "^18.2.39",
|
|
"@types/react-datepicker": "^6.2.0",
|
|
"@types/react-dom": "^18.2.15",
|
|
"@types/supertest": "^2.0.11",
|
|
"@types/uuid": "^9.0.2",
|
|
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
"@typescript-eslint/parser": "^8.39.0",
|
|
"@typescript-eslint/utils": "^8.39.0",
|
|
"@vitejs/plugin-react-swc": "3.11.0",
|
|
"@yarnpkg/types": "^4.0.0",
|
|
"chromatic": "^6.18.0",
|
|
"concurrently": "^8.2.2",
|
|
"cross-var": "^1.1.0",
|
|
"danger": "^13.0.4",
|
|
"dotenv-cli": "^7.4.4",
|
|
"esbuild": "^0.25.10",
|
|
"eslint": "^9.32.0",
|
|
"eslint-config-prettier": "^9.1.0",
|
|
"eslint-plugin-import": "^2.31.0",
|
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
"eslint-plugin-lingui": "^0.9.0",
|
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
"eslint-plugin-prettier": "^5.1.2",
|
|
"eslint-plugin-project-structure": "^3.9.1",
|
|
"eslint-plugin-react": "^7.37.2",
|
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
"eslint-plugin-react-refresh": "^0.4.4",
|
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
"eslint-plugin-storybook": "^0.9.0",
|
|
"eslint-plugin-unicorn": "^56.0.1",
|
|
"eslint-plugin-unused-imports": "^3.0.0",
|
|
"http-server": "^14.1.1",
|
|
"jest": "29.7.0",
|
|
"jest-environment-jsdom": "30.0.0-beta.3",
|
|
"jest-environment-node": "^29.4.1",
|
|
"jest-fetch-mock": "^3.0.3",
|
|
"jsdom": "~22.1.0",
|
|
"msw": "^2.0.11",
|
|
"msw-storybook-addon": "^2.0.5",
|
|
"nx": "21.3.11",
|
|
"playwright": "^1.46.0",
|
|
"prettier": "^3.1.1",
|
|
"raw-loader": "^4.0.2",
|
|
"rimraf": "^5.0.5",
|
|
"source-map-support": "^0.5.20",
|
|
"storybook": "8.6.14",
|
|
"storybook-addon-cookie": "^3.2.0",
|
|
"storybook-addon-pseudo-states": "^2.1.2",
|
|
"supertest": "^6.1.3",
|
|
"ts-jest": "^29.1.1",
|
|
"ts-loader": "^9.2.3",
|
|
"ts-node": "10.9.1",
|
|
"tsconfig-paths": "^4.2.0",
|
|
"tsx": "^4.17.0",
|
|
"vite": "^7.0.0",
|
|
"vite-plugin-checker": "^0.10.2",
|
|
"vite-plugin-cjs-interop": "^2.2.0",
|
|
"vite-plugin-dts": "3.8.1",
|
|
"vite-plugin-svgr": "^4.2.0"
|
|
},
|
|
"engines": {
|
|
"node": "^24.5.0",
|
|
"npm": "please-use-yarn",
|
|
"yarn": ">=4.0.2"
|
|
},
|
|
"license": "AGPL-3.0",
|
|
"name": "twenty",
|
|
"packageManager": "yarn@4.9.2",
|
|
"resolutions": {
|
|
"graphql": "16.8.1",
|
|
"type-fest": "4.10.1",
|
|
"typescript": "5.9.2",
|
|
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
|
"prosemirror-view": "1.40.0",
|
|
"prosemirror-transform": "1.10.4"
|
|
},
|
|
"version": "0.2.1",
|
|
"nx": {},
|
|
"scripts": {
|
|
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
|
},
|
|
"workspaces": {
|
|
"packages": [
|
|
"packages/twenty-front",
|
|
"packages/twenty-server",
|
|
"packages/twenty-emails",
|
|
"packages/twenty-ui",
|
|
"packages/twenty-utils",
|
|
"packages/twenty-zapier",
|
|
"packages/twenty-website",
|
|
"packages/twenty-docs",
|
|
"packages/twenty-e2e-testing",
|
|
"packages/twenty-shared",
|
|
"packages/twenty-sdk",
|
|
"packages/twenty-apps",
|
|
"packages/twenty-cli",
|
|
"packages/twenty-browser-extension",
|
|
"tools/eslint-rules"
|
|
]
|
|
}
|
|
}
|