diff --git a/packages/twenty-apps/community/fireflies/.env.example b/packages/twenty-apps/community/fireflies/.env.example index 05135f11e6..9c6771103c 100644 --- a/packages/twenty-apps/community/fireflies/.env.example +++ b/packages/twenty-apps/community/fireflies/.env.example @@ -17,7 +17,7 @@ FIREFLIES_API_KEY=your_fireflies_api_key_here # Fireflies plan level - affects which fields are available # Options: free, pro, business, enterprise # This controls which GraphQL fields are requested to avoid 403 errors -FIREFLIES_PLAN_LEVEL=pro +FIREFLIES_PLAN=free # Twenty CRM API key for authentication # Generate this from your Twenty instance: Settings > Developers > API Keys diff --git a/packages/twenty-apps/community/fireflies/.gitignore b/packages/twenty-apps/community/fireflies/.gitignore new file mode 100644 index 0000000000..61c3bc75a0 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/.gitignore @@ -0,0 +1 @@ +.yarn diff --git a/packages/twenty-apps/community/fireflies/.yarn/install-state.gz b/packages/twenty-apps/community/fireflies/.yarn/install-state.gz deleted file mode 100644 index b009e513d8..0000000000 Binary files a/packages/twenty-apps/community/fireflies/.yarn/install-state.gz and /dev/null differ diff --git a/packages/twenty-apps/community/fireflies/CHANGELOG.md b/packages/twenty-apps/community/fireflies/CHANGELOG.md index 57e5605a05..675a2453c9 100644 --- a/packages/twenty-apps/community/fireflies/CHANGELOG.md +++ b/packages/twenty-apps/community/fireflies/CHANGELOG.md @@ -1,5 +1,76 @@ # Changelog +## [0.3.1] - 2025-12-08 + +Import all + +### Added +- Historical import CLI: `yarn meeting:all` to fetch and insert historical Fireflies meetings with filters (date range, organizers, participants, channel, mine) and dry-run support. +- Fireflies transcripts listing with pagination and date filtering to support bulk imports. + +### Changed +- Deduplication now checks `firefliesMeetingId` before creating meetings (webhook + bulk). +- Shared historical importer pipeline reusing existing note/meeting formatting. + +## [0.3.0] - 2025-12-08 + +Subscription-based query / Full transcript and AI notes for Pro+ / More + +### Added +- **Full transcript capture**: Meeting object now stores the complete meeting transcript with speaker names and timestamps (`transcript` field) +- **Rich AI meeting notes**: Captures detailed AI-generated meeting notes from Fireflies (`notes` field with 7,000+ char summaries) +- **Expanded summary fields**: Now fetches all available Fireflies summary data: + - `notes` - Detailed AI-generated meeting notes with timestamps and section headers + - `bullet_gist` - Emoji-enhanced bullet point summaries + - `outline` / `shorthand_bullet` - Timestamped meeting outline + - `gist` - One-sentence meeting summary + - `short_summary` - Single paragraph summary + - `short_overview` - Brief overview +- **New Meeting fields**: + - `transcript` - Full meeting transcript with speaker attribution + - `notes` - AI-generated detailed notes + - `audioUrl` - Link to audio recording (Pro+) + - `videoUrl` - Link to video recording (Business+) + - `meetingLink` - Original meeting link + - `neutralPercent` - Neutral sentiment percentage +- **Meeting delete utility**: New `yarn meeting:delete ` script for cleanup and re-import +- **Debug meeting utility**: New `scripts/debug-meeting.ts` to inspect raw Fireflies API responses + +### Changed +- **Plan-based GraphQL queries**: Completely redesigned query system with three tiers: + - **Free**: Basic fields only (title, date, duration, participants, transcript_url, meeting_link) + - **Pro**: Adds full transcript (`sentences`), summary fields, speakers, audio_url + - **Business+**: Adds analytics, video_url, speaker stats, meeting metrics +- **Action items parsing**: Fixed parsing of `action_items` which Fireflies returns as newline-separated string, not array +- **Note body format**: Enhanced with Meeting Notes, Outline, Key Points sections from rich Fireflies data +- **Import status**: Added `PARTIAL` status for imports missing summary/analytics data + +### Fixed +- Missing `notes` and `bullet_gist` fields in data transform (were fetched but not passed through) +- Proper fallback: Uses `shorthand_bullet` when `outline` is empty (Fireflies stores outline content there) +- Summary readiness detection now checks `notes` field in addition to `overview` and `action_items` + +### Documentation +- Updated README with complete API access comparison table by subscription plan +- Documented all available Fireflies summary fields and their plan requirements + +## [0.2.3] - 2025-12-06 + +### Added +- **Meeting ingest utility**: New `yarn meeting:ingest ` script to manually fetch and import specific Fireflies meetings into Twenty +- **Plan-based field selection**: Added `FIREFLIES_PLAN` configuration to control which GraphQL fields are requested based on subscription level (free, pro, business, enterprise) +- **Main entry point**: New `src/index.ts` centralizing all exports for cleaner imports + +### Changed +- **Auth configuration**: Disabled authentication requirement for webhook route (`isAuthRequired: false`) to support serverless deployments +- **Signature verification fallback**: Webhook handler now supports signature in payload body as fallback when HTTP headers aren't forwarded to serverless functions (production doesn't work for Fireflies webhook) +- **Improved type safety**: Replaced `any` types with proper TypeScript types throughout codebase + +### Enhanced +- **Webhook debugging**: Added detailed debug output including param keys, header info, and signature comparison details +- **Test webhook script**: Includes signature in both header and payload, with diagnostic output for header forwarding status +- **Documentation**: Added README sections on current twenty headers forward limitations and utility scripts + ## [0.2.2] - 2025-11-04 ### Added diff --git a/packages/twenty-apps/community/fireflies/README.md b/packages/twenty-apps/community/fireflies/README.md index c1e8148e2c..91ef7c4a0e 100644 --- a/packages/twenty-apps/community/fireflies/README.md +++ b/packages/twenty-apps/community/fireflies/README.md @@ -2,6 +2,10 @@ Automatically captures meeting notes with AI-generated summaries and insights from Fireflies.ai into your Twenty CRM. +### Current Status +- Doesn't work with Fireflies webhook yet due to missing headers forwarding in twenty serverless func +- Meeting ingestion utility scripts are available for individual meeting insertion and historical meetings with filters with yarn meeting:all + ## Integration Overview **Fireflies webhook → Fireflies API → Twenty CRM with summary-focused insights** @@ -19,19 +23,54 @@ Automatically captures meeting notes with AI-generated summaries and insights fr ## API Access by Subscription Plan -Fireflies API access varies significantly by subscription tier: +Fireflies API access varies by subscription tier. This integration automatically adapts queries based on your plan and falls back gracefully if restrictions are encountered. + +### Plan Comparison | Feature | Free | Pro | Business | Enterprise | -|---------|------|-----|----------|------------| -| **API Rate Limit** | 50 requests/day | 50 requests/day | 60 requests/minute | 60 requests/minute | -| **Storage** | 800 mins/seat | 8,000 mins/seat | Unlimited | Unlimited | -| **AI Summaries** | Limited (20 credits) | Unlimited | Unlimited | Unlimited | -| **Video Upload** | 100MB max | 1.5GB max | 1.5GB max | 1.5GB max | -| **Advanced Features** | Basic transcription | AI apps, analytics | Team analytics, CI | Full API, SSO, compliance | +|---------|:----:|:---:|:--------:|:----------:| +| **API Rate Limit** | 50/day | 50/day | 60/min | 60/min | +| **Basic Data** (title, date, duration) | ✅ | ✅ | ✅ | ✅ | +| **Participants List** | ✅ | ✅ | ✅ | ✅ | +| **Transcript URL** | ✅ | ✅ | ✅ | ✅ | +| **Speakers** | ❌ | ✅ | ✅ | ✅ | +| **Summary** (overview, keywords) | ❌ | ✅ | ✅ | ✅ | +| **Audio URL** | ❌ | ✅ | ✅ | ✅ | +| **Action Items** | ❌ | ❌ | ✅ | ✅ | +| **Topics Discussed** | ❌ | ❌ | ✅ | ✅ | +| **Video URL** | ❌ | ❌ | ✅ | ✅ | +| **Sentiment Analytics** | ❌ | ❌ | ✅ | ✅ | +| **Meeting Attendees (detailed)** | ❌ | ❌ | ✅ | ✅ | -**Key Design Pattern:** Subscription-based API access uses **tiered rate limiting** rather than feature gating. Lower tiers get severely restricted throughput (50/day vs 60/minute = 1,700x difference), making production integrations effectively require Business+ plans. +### What You'll Get Per Plan -**Pro Plan Limitation:** Despite "unlimited" AI summaries, the 50 requests/day limit severely constrains production usage for meeting-heavy organizations. +**Free Plan:** +- Meeting title, date, duration +- Participant names/emails (basic) +- Link to transcript + +**Pro Plan:** +- Everything in Free, plus: +- Speaker identification +- AI summary (overview + keywords) +- Audio recording URL + +**Business Plan:** +- Everything in Pro, plus: +- Action items extraction +- Topics discussed +- Sentiment analysis (positive/negative/neutral %) +- Video recording URL +- Detailed meeting attendee info + +### Configuration + +Set your plan in `.env`: +```bash +FIREFLIES_PLAN=free # Options: free, pro, business, enterprise +``` + +**Rate Limiting:** Free/Pro plans are limited to 50 API calls/day. The integration uses conservative retry settings by default to stay within limits. ## What Gets Captured @@ -114,11 +153,6 @@ The `setup:fields` script adds 13 custom fields to store rich Fireflies data: | `firefliesMeetingId` | TEXT | Fireflies Meeting ID | Unique identifier from Fireflies | | `organizerEmail` | TEXT | Organizer Email | Email address of the meeting organizer | -Then re-sync: -```bash -npx twenty-cli app sync -``` - **Note:** Without custom fields, meetings will be created with just the title. The rich summary data will only be stored in Notes for 1-on-1 meetings. ## Configuration @@ -160,6 +194,29 @@ The integration uses **HMAC SHA-256 signature verification**: - Twenty verifies signature using your webhook secret - Invalid signatures are rejected immediately +### Current Platform Limitation (Headers) + +- Twenty serverless route triggers currently do **not forward HTTP headers** to functions. Fireflies signatures sent in headers are stripped, so header-based verification does not work in production. +- Workaround: the provided test script also includes the signature inside the payload; the handler falls back to that payload signature. Use this only for testing until header forwarding is supported. + +## Utilities for meeting insertion (workarounds) + +- Ingest a specific Fireflies meeting into Twenty: +`yarn meeting:ingest ` or `MEETING_ID=... yarn meeting:ingest` + +- Fetch all/historical Fireflies meetings into Twenty: +`yarn meeting:all [--from 2024-01-01] [--to 2024-02-01] [--organizer a@x.com] [--participant b@x.com] [--channel ] [--mine] [--dry-run]` + + - Filters (combine as needed): + - `--from` / `--to`: ISO or date string range filter + - `--organizer` / `--participant`: comma-separated emails + - `--channel`: Fireflies channel id + - `--mine`: only meetings for the current Fireflies user + - Controls: + - `--dry-run`: list and transform without writing to Twenty + - `--page-size`: pagination size (default 50) + - `--max-records`: stop after N transcripts (default 500) + ## Development ```bash @@ -247,10 +304,7 @@ Client expressed strong interest in the enterprise plan. ## Future Implementation Opportunities -### Past Meetings Retrieval -- **New trigger to retrieve past meetings from a contact** - Enable users to fetch historical meeting data from Fireflies for specific contacts, allowing retrospective capture and analysis of past interactions. - -Next iteration would enhance the **intelligence layer** to: +Next iterations would enhance the **intelligence layer** to: ### AI-Powered Insights - **Extract pain points, objections & buying signals** automatically from transcripts diff --git a/packages/twenty-apps/community/fireflies/application.config.ts b/packages/twenty-apps/community/fireflies/application.config.ts index d690541fcf..9736caf2c2 100644 --- a/packages/twenty-apps/community/fireflies/application.config.ts +++ b/packages/twenty-apps/community/fireflies/application.config.ts @@ -1,4 +1,4 @@ -import { type ApplicationConfig } from 'twenty-sdk/application'; +import { type ApplicationConfig } from 'twenty-sdk'; const config: ApplicationConfig = { universalIdentifier: 'a4df0c0f-c65e-44e5-8436-24814182d4ac', @@ -9,17 +9,25 @@ const config: ApplicationConfig = { FIREFLIES_WEBHOOK_SECRET: { universalIdentifier: 'f51f7646-be9f-4ba9-9b75-160dd288cd0c', description: 'Secret key for verifying Fireflies webhook signatures', - isSecret: true, + //isSecret: true, + value: '', }, FIREFLIES_API_KEY: { universalIdentifier: 'faa41f07-b28e-4500-b1c0-ce4b3d27924c', description: 'Fireflies GraphQL API key used to fetch meeting summaries', - isSecret: true, + //isSecret: true, + value: '', + }, + FIREFLIES_PLAN: { + universalIdentifier: '57dbb73c-aac5-4247-9fcc-a070bb669f16', + description: 'Fireflies plan: free, pro, business, enterprise', + value: 'free', }, TWENTY_API_KEY: { universalIdentifier: '02756551-5bf7-4fb2-8e08-1f622008d305', description: 'Twenty API key used when running scripts locally', - isSecret: true, + //isSecret: true, + value: '', }, SERVER_URL: { universalIdentifier: '9b3a5e8e-5973-4e6b-a059-2966075652aa', @@ -36,6 +44,11 @@ const config: ApplicationConfig = { description: 'Log level: silent, error, warn, info, debug (default: error)', value: 'error', }, + CAPTURE_LOGS: { + universalIdentifier: 'adbcc267-309d-49b2-af71-76f1299d863e', + description: 'Capture logs in webhook response for debugging (true/false)', + value: 'true', + }, FIREFLIES_SUMMARY_STRATEGY: { universalIdentifier: '562b43d9-cd47-4ec1-ae16-5cc7ebc9729b', description: 'Summary fetch strategy: immediate_only, immediate_with_retry, delayed_polling, or basic_only', diff --git a/packages/twenty-apps/community/fireflies/package.json b/packages/twenty-apps/community/fireflies/package.json index f17f487b0a..3a326042b6 100644 --- a/packages/twenty-apps/community/fireflies/package.json +++ b/packages/twenty-apps/community/fireflies/package.json @@ -1,6 +1,6 @@ { "name": "fireflies", - "version": "0.2.2", + "version": "0.3.0", "license": "MIT", "engines": { "node": "^24.5.0", @@ -11,12 +11,15 @@ "scripts": { "test": "jest", "setup:fields": "tsx scripts/add-meeting-fields.ts", - "test:webhook": "tsx scripts/test-webhook.ts" + "test:webhook": "tsx scripts/test-webhook.ts", + "meeting:ingest": "tsx scripts/ingest-meeting.ts", + "meeting:delete": "tsx scripts/delete-meeting.ts", + "meeting:all": "tsx scripts/fetch-all-meetings.ts" }, "dependencies": { "axios": "^1.13.1", - "dotenv": "^16.3.1", - "twenty-sdk": "0.0.3" + "dotenv": "^17.2.3", + "twenty-sdk": "0.1.3" }, "devDependencies": { "@types/jest": "^29.5.5", diff --git a/packages/twenty-apps/community/fireflies/project.json b/packages/twenty-apps/community/fireflies/project.json index 31c31e1d8b..62935a607c 100644 --- a/packages/twenty-apps/community/fireflies/project.json +++ b/packages/twenty-apps/community/fireflies/project.json @@ -1,7 +1,7 @@ { "name": "fireflies", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/twenty-apps/fireflies/src", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/twenty-apps/community/fireflies/src", "projectType": "application", "tags": [ "scope:apps" @@ -13,7 +13,7 @@ "{workspaceRoot}/coverage/{projectRoot}" ], "options": { - "jestConfig": "packages/twenty-apps/fireflies/jest.config.mjs", + "jestConfig": "packages/twenty-apps/community/fireflies/jest.config.mjs", "passWithNoTests": true }, "configurations": { @@ -36,7 +36,7 @@ ], "options": { "lintFilePatterns": [ - "packages/twenty-apps/fireflies/**/*.{ts,tsx,js,jsx}" + "packages/twenty-apps/community/fireflies/**/*.{ts,tsx,js,jsx}" ] }, "configurations": { diff --git a/packages/twenty-apps/community/fireflies/scripts/add-meeting-fields.ts b/packages/twenty-apps/community/fireflies/scripts/add-meeting-fields.ts index 637ff65d95..594c48901a 100644 --- a/packages/twenty-apps/community/fireflies/scripts/add-meeting-fields.ts +++ b/packages/twenty-apps/community/fireflies/scripts/add-meeting-fields.ts @@ -50,7 +50,11 @@ interface FieldDefinition { options?: FieldOption[]; } +// Meeting fields based on Fireflies GraphQL API transcript schema +// See: https://docs.fireflies.ai/graphql-api/query/transcript +// Note: Some fields require higher plans (Pro, Business, Enterprise) const MEETING_FIELDS: FieldDefinition[] = [ + // === Internal Twenty Relations === { type: 'RELATION', name: 'note', @@ -59,11 +63,21 @@ const MEETING_FIELDS: FieldDefinition[] = [ icon: 'IconNotes', isNullable: true, }, + + // === Basic Fields (All Plans) === + { + type: 'TEXT', + name: 'firefliesMeetingId', + label: 'Fireflies ID', + description: 'Unique transcript ID from Fireflies (maps to: id)', + icon: 'IconKey', + isNullable: true, + }, { type: 'DATE_TIME', name: 'meetingDate', label: 'Meeting Date', - description: 'Date and time when the meeting occurred', + description: 'When the meeting occurred (maps to: date)', icon: 'IconCalendar', isNullable: true, }, @@ -71,39 +85,107 @@ const MEETING_FIELDS: FieldDefinition[] = [ type: 'NUMBER', name: 'duration', label: 'Duration (minutes)', - description: 'Meeting duration in minutes', + description: 'Meeting duration in minutes (maps to: duration)', icon: 'IconClock', isNullable: true, }, { type: 'TEXT', - name: 'meetingType', - label: 'Meeting Type', - description: 'Type of meeting (e.g., Sales Call, Sprint Planning, 1:1)', - icon: 'IconTag', + name: 'organizerEmail', + label: 'Organizer Email', + description: 'Meeting organizer email (maps to: organizer_email)', + icon: 'IconMail', + isNullable: true, + }, + { + type: 'LINKS', + name: 'transcriptUrl', + label: 'Transcript URL', + description: 'Link to full transcript (maps to: transcript_url)', + icon: 'IconFileText', + isNullable: true, + }, + { + type: 'LINKS', + name: 'meetingLink', + label: 'Meeting Link', + description: 'Original meeting link (maps to: meeting_link)', + icon: 'IconLink', + isNullable: true, + }, + + // === Pro+ Fields (summary, speakers, audio_url, transcript) === + { + type: 'TEXT', + name: 'transcript', + label: 'Full Transcript', + description: 'Full meeting transcript with speaker names and timestamps [Pro+]', + icon: 'IconFileText', + isNullable: true, + }, + { + type: 'TEXT', + name: 'overview', + label: 'Overview', + description: 'AI-generated meeting summary (maps to: summary.overview) [Pro+]', + icon: 'IconFileDescription', + isNullable: true, + }, + { + type: 'TEXT', + name: 'notes', + label: 'AI Notes', + description: 'Detailed AI-generated meeting notes (maps to: summary.notes) [Pro+]', + icon: 'IconNotes', isNullable: true, }, { type: 'TEXT', name: 'keywords', label: 'Keywords', - description: 'Key topics and themes discussed (comma-separated)', + description: 'Key topics extracted (maps to: summary.keywords) [Pro+]', icon: 'IconTags', isNullable: true, }, + { + type: 'LINKS', + name: 'audioUrl', + label: 'Audio URL', + description: 'Link to audio recording (maps to: audio_url) [Pro+]', + icon: 'IconHeadphones', + isNullable: true, + }, + + // === Business+ Fields (analytics, video_url, full summary) === + { + type: 'TEXT', + name: 'meetingType', + label: 'Meeting Type', + description: 'AI-detected meeting type (maps to: summary.meeting_type) [Business+]', + icon: 'IconTag', + isNullable: true, + }, + { + type: 'TEXT', + name: 'topics', + label: 'Topics Discussed', + description: 'Topics covered in meeting (maps to: summary.topics_discussed) [Business+]', + icon: 'IconListDetails', + isNullable: true, + }, { type: 'NUMBER', - name: 'sentimentScore', - label: 'Sentiment Score', - description: 'Overall meeting sentiment (0-1 scale, where 1 is most positive)', - icon: 'IconMoodSmile', + name: 'actionItemsCount', + label: 'Action Items', + description: 'Number of action items (count of: summary.action_items) [Business+]', + icon: 'IconCheckbox', isNullable: true, }, { type: 'NUMBER', name: 'positivePercent', label: 'Positive %', - description: 'Percentage of positive sentiment in conversation', + description: 'Positive sentiment % (maps to: analytics.sentiments.positive_pct) [Business+]', icon: 'IconThumbUp', isNullable: true, }, @@ -111,69 +193,48 @@ const MEETING_FIELDS: FieldDefinition[] = [ type: 'NUMBER', name: 'negativePercent', label: 'Negative %', - description: 'Percentage of negative sentiment in conversation', + description: 'Negative sentiment % (maps to: analytics.sentiments.negative_pct) [Business+]', icon: 'IconThumbDown', isNullable: true, }, { type: 'NUMBER', - name: 'actionItemsCount', - label: 'Action Items', - description: 'Number of action items identified', - icon: 'IconCheckbox', + name: 'neutralPercent', + label: 'Neutral %', + description: 'Neutral sentiment % (maps to: analytics.sentiments.neutral_pct) [Business+]', + icon: 'IconMoodNeutral', isNullable: true, }, { type: 'LINKS', - name: 'transcriptUrl', - label: 'Transcript URL', - description: 'Link to full transcript in Fireflies', - icon: 'IconFileText', - isNullable: true, - }, - { - type: 'LINKS', - name: 'recordingUrl', - label: 'Recording URL', - description: 'Link to video/audio recording in Fireflies', + name: 'videoUrl', + label: 'Video URL', + description: 'Link to video recording (maps to: video_url) [Business+]', icon: 'IconVideo', isNullable: true, }, - { - type: 'TEXT', - name: 'firefliesMeetingId', - label: 'Fireflies Meeting ID', - description: 'Unique identifier from Fireflies', - icon: 'IconKey', - isNullable: true, - }, - { - type: 'TEXT', - name: 'organizerEmail', - label: 'Organizer Email', - description: 'Email address of the meeting organizer', - icon: 'IconMail', - isNullable: true, - }, + + // === Import Tracking Fields (Internal) === { type: 'SELECT', name: 'importStatus', label: 'Import Status', - description: 'Status of the meeting import from Fireflies', + description: 'Status of the Fireflies import', icon: 'IconCheck', isNullable: true, options: [ { value: 'SUCCESS', label: 'Success', position: 0, color: 'green' }, - { value: 'FAILED', label: 'Failed', position: 1, color: 'red' }, - { value: 'PENDING', label: 'Pending', position: 2, color: 'yellow' }, - { value: 'RETRYING', label: 'Retrying', position: 3, color: 'orange' }, + { value: 'PARTIAL', label: 'Partial', position: 1, color: 'blue' }, + { value: 'FAILED', label: 'Failed', position: 2, color: 'red' }, + { value: 'PENDING', label: 'Pending', position: 3, color: 'yellow' }, + { value: 'RETRYING', label: 'Retrying', position: 4, color: 'orange' }, ], }, { type: 'TEXT', name: 'importError', label: 'Import Error', - description: 'Error message if meeting import failed', + description: 'Error message if import failed', icon: 'IconAlertTriangle', isNullable: true, }, @@ -181,7 +242,7 @@ const MEETING_FIELDS: FieldDefinition[] = [ type: 'DATE_TIME', name: 'lastImportAttempt', label: 'Last Import Attempt', - description: 'Date and time of the last import attempt', + description: 'When import was last attempted', icon: 'IconClock', isNullable: true, }, @@ -189,7 +250,7 @@ const MEETING_FIELDS: FieldDefinition[] = [ type: 'NUMBER', name: 'importAttempts', label: 'Import Attempts', - description: 'Number of times import has been attempted', + description: 'Number of import attempts', icon: 'IconRepeat', isNullable: true, }, @@ -414,13 +475,9 @@ const main = async () => { } if (createdCount === 0 && skippedCount === MEETING_FIELDS.length) { - console.log('\n✨ All fields already exist. Nothing to do!'); + console.log('\n✨ All fields already exist. Nothing to do!\n'); } else if (createdCount > 0) { - console.log('\n✨ Custom fields added successfully!'); - console.log('\n📝 Next steps:'); - console.log(' 1. Re-sync your app: npx twenty-cli app sync'); - console.log(' 2. Update the createMeetingRecord function to use these fields'); - console.log(' 3. Test the integration with a real meeting'); + console.log('\n✨ Custom fields added successfully!\n'); } } catch (error) { diff --git a/packages/twenty-apps/community/fireflies/scripts/debug-meeting.ts b/packages/twenty-apps/community/fireflies/scripts/debug-meeting.ts new file mode 100644 index 0000000000..0814e17d6e --- /dev/null +++ b/packages/twenty-apps/community/fireflies/scripts/debug-meeting.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-console */ +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.join(__dirname, '../.env') }); + +const FIREFLIES_API_KEY = process.env.FIREFLIES_API_KEY; +const meetingId = process.argv[2] || '01KBMR1ZYQ34YP8D2KB4B16QPH'; + +const main = async (): Promise => { + if (!FIREFLIES_API_KEY) { + console.error('❌ FIREFLIES_API_KEY is required'); + process.exit(1); + } + + const query = ` + query GetTranscript($transcriptId: String!) { + transcript(id: $transcriptId) { + id + title + summary { + overview + notes + gist + bullet_gist + short_summary + short_overview + outline + shorthand_bullet + action_items + keywords + topics_discussed + meeting_type + transcript_chapters + } + } + } + `; + + const response = await fetch('https://api.fireflies.ai/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${FIREFLIES_API_KEY}`, + }, + body: JSON.stringify({ query, variables: { transcriptId: meetingId } }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`❌ API request failed with status ${response.status}`); + console.error(errorText); + process.exit(1); + } + + const json = await response.json(); + console.log('=== Fireflies API Response ===\n'); + console.log(JSON.stringify(json, null, 2)); + + if (json.data?.transcript?.summary) { + const s = json.data.transcript.summary; + console.log('\n=== Summary Fields Status ==='); + console.log('overview:', s.overview ? `✓ (${s.overview.length} chars)` : '✗ empty'); + console.log('notes:', s.notes ? `✓ (${s.notes.length} chars)` : '✗ empty'); + console.log('gist:', s.gist ? `✓ (${s.gist.length} chars)` : '✗ empty'); + console.log('bullet_gist:', s.bullet_gist ? `✓ (${s.bullet_gist.length} chars)` : '✗ empty'); + console.log('outline:', s.outline ? `✓ (${s.outline.length} chars)` : '✗ empty'); + console.log('action_items:', s.action_items?.length || 0, 'items'); + console.log('topics_discussed:', s.topics_discussed?.length || 0, 'topics'); + console.log('keywords:', s.keywords?.length || 0, 'keywords'); + } +}; + +main().catch((error) => { + console.error('❌ Failed to fetch meeting'); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/packages/twenty-apps/community/fireflies/scripts/delete-meeting.ts b/packages/twenty-apps/community/fireflies/scripts/delete-meeting.ts new file mode 100644 index 0000000000..65ef786d3e --- /dev/null +++ b/packages/twenty-apps/community/fireflies/scripts/delete-meeting.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-console */ +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.join(__dirname, '../.env') }); + +const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3000'; +const API_KEY = process.env.TWENTY_API_KEY; +const meetingId = process.argv[2]; + +const main = async (): Promise => { + if (!API_KEY) { + console.error('❌ TWENTY_API_KEY is required'); + process.exit(1); + } + + if (!meetingId) { + console.error('Usage: yarn delete:meeting '); + process.exit(1); + } + + const response = await fetch(`${SERVER_URL}/graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + query: `mutation DeleteMeeting($id: UUID!) { deleteMeeting(id: $id) { id } }`, + variables: { id: meetingId }, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`❌ Delete failed (status ${response.status})`); + console.error(errorText); + process.exit(1); + } + + const result = await response.json(); + const deletedId = result.data?.deleteMeeting?.id; + if (result.errors || !deletedId) { + const message = result.errors?.[0]?.message || 'deleteMeeting returned null'; + console.error('❌ Error:', message); + process.exit(1); + } + + console.log('✅ Deleted meeting:', deletedId); +}; + +main().catch((error) => { + console.error('❌ Failed to delete meeting'); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/packages/twenty-apps/community/fireflies/scripts/fetch-all-meetings.ts b/packages/twenty-apps/community/fireflies/scripts/fetch-all-meetings.ts new file mode 100644 index 0000000000..082d764629 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/scripts/fetch-all-meetings.ts @@ -0,0 +1,222 @@ +/* eslint-disable no-console */ +/** + * Fetch historical Fireflies meetings and insert into Twenty. + * + * Usage: + * yarn meeting:all [--from 2024-01-01] [--to 2024-02-01] [--organizer alice@x.com] [--participant bob@x.com] [--channel ] [--mine] [--dry-run] [--page-size 50] [--max-records 200] + * + * Required env: + * FIREFLIES_API_KEY + * TWENTY_API_KEY + * + * Optional env: + * SERVER_URL (defaults to http://localhost:3000) + * FIREFLIES_PLAN (free|pro|business|enterprise) + * AUTO_CREATE_CONTACTS (true|false) + * FIREFLIES_* retry settings (see README) + */ + +import * as dotenv from 'dotenv'; +import { existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { FirefliesApiClient } from '../src/fireflies-api-client'; +import { type HistoricalImportFilters, HistoricalImporter } from '../src/historical-importer'; +import { createLogger } from '../src/logger'; +import { TwentyCrmService } from '../src/twenty-crm-service'; +import { + getApiUrl, + getFirefliesPlan, + getSummaryFetchConfig, + shouldAutoCreateContacts, +} from '../src/utils'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const envPath = join(__dirname, '..', '.env'); +if (existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +const logger = createLogger('cli:meeting:all'); + +type CliArgs = { + from?: string; + to?: string; + organizer?: string[]; + participant?: string[]; + channel?: string; + host?: string; + mine?: boolean; + dryRun?: boolean; + pageSize?: number; + maxRecords?: number; + limit?: number; +}; + +const parseArgs = (argv: string[]): CliArgs => { + const args: CliArgs = {}; + + const parseNumberArg = (value?: string): number | undefined => { + if (!value) return undefined; + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? undefined : parsed; + }; + + for (let i = 0; i < argv.length; i += 1) { + const current = argv[i]; + const next = argv[i + 1]; + switch (current) { + case '--from': + args.from = next; + i += 1; + break; + case '--to': + args.to = next; + i += 1; + break; + case '--organizer': + args.organizer = next ? next.split(',') : []; + i += 1; + break; + case '--participant': + args.participant = next ? next.split(',') : []; + i += 1; + break; + case '--channel': + args.channel = next; + i += 1; + break; + case '--host': + args.host = next; + i += 1; + break; + case '--mine': + args.mine = true; + break; + case '--dry-run': + args.dryRun = true; + break; + case '--page-size': + args.pageSize = parseNumberArg(next); + i += 1; + break; + case '--max-records': + args.maxRecords = parseNumberArg(next); + i += 1; + break; + case '--limit': + args.limit = parseNumberArg(next); + i += 1; + break; + default: + break; + } + } + + return args; +}; + +const parseDate = (value?: string): number | undefined => { + if (!value) { + return undefined; + } + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? undefined : parsed; +}; + +const main = async (): Promise => { + const args = parseArgs(process.argv.slice(2)); + + const firefliesApiKey = process.env.FIREFLIES_API_KEY || ''; + const twentyApiKey = process.env.TWENTY_API_KEY || ''; + + if (!firefliesApiKey) { + console.error('❌ FIREFLIES_API_KEY is required'); + process.exit(1); + } + + if (!twentyApiKey) { + console.error('❌ TWENTY_API_KEY is required'); + process.exit(1); + } + + const fromDate = parseDate(args.from); + const toDate = parseDate(args.to); + + const filters: HistoricalImportFilters = { + fromDate, + toDate, + organizers: args.organizer, + participants: args.participant, + channelId: args.channel, + hostEmail: args.host, + mine: args.mine, + limit: args.limit, + pageSize: args.pageSize, + maxRecords: args.maxRecords, + }; + + const summaryConfig = getSummaryFetchConfig(); + const plan = getFirefliesPlan(); + const autoCreateContacts = shouldAutoCreateContacts(); + + logger.info( + `Starting historical import (dryRun=${Boolean(args.dryRun)}, plan=${plan}, pageSize=${filters.pageSize ?? 50})`, + ); + + const firefliesClient = new FirefliesApiClient(firefliesApiKey); + const twentyService = new TwentyCrmService(twentyApiKey, getApiUrl()); + const importer = new HistoricalImporter(firefliesClient, twentyService); + + const result = await importer.run(filters, { + dryRun: args.dryRun, + autoCreateContacts, + summaryConfig, + plan, + }); + + console.log('✅ Historical import summary:'); + const summary = { + dryRun: result.dryRun, + totalListed: result.totalListed, + imported: result.imported, + skippedExisting: result.skippedExisting, + summaryPending: result.summaryPending, + failed: result.failed, + }; + console.log(JSON.stringify(summary, null, 2)); + + if (result.statuses.length > 0) { + console.log('Status by meeting:'); + console.table( + result.statuses.map((s) => ({ + meetingId: s.meetingId, + title: s.title ?? '', + status: s.status, + reason: s.reason ?? '', + })), + ); + } + + if (result.failed.length > 0) { + process.exitCode = 1; + } +}; + +main().catch((error) => { + console.error('❌ Failed to import historical meetings'); + if (error instanceof Error) { + console.error(error.message); + if (error.stack) { + console.error(error.stack); + } + } else { + console.error(String(error)); + } + process.exit(1); +}); + + diff --git a/packages/twenty-apps/community/fireflies/scripts/ingest-meeting.ts b/packages/twenty-apps/community/fireflies/scripts/ingest-meeting.ts new file mode 100644 index 0000000000..a6caff8bd6 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/scripts/ingest-meeting.ts @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ +/** + * Fetch a Fireflies meeting by ID and insert it into Twenty using the same path + * as the webhook handler. + * + * Usage: + * yarn meeting:ingest + * Or + * MEETING_ID=... yarn meeting:ingest + * + * Required env: + * FIREFLIES_API_KEY + * FIREFLIES_WEBHOOK_SECRET + * TWENTY_API_KEY + * + * Optional env: + * SERVER_URL (defaults to http://localhost:3000) + * FIREFLIES_PLAN (free|pro|business|enterprise) + */ + +import { createHmac } from 'crypto'; +import * as dotenv from 'dotenv'; +import { existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { WebhookHandler } from '../src/webhook-handler'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const envPath = join(__dirname, '..', '.env'); +if (existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +const args = process.argv.slice(2); +const meetingId = args[0] || process.env.MEETING_ID; + +if (!meetingId) { + console.error('❌ meetingId is required (arg or MEETING_ID env)'); + process.exit(1); +} + +const firefliesApiKey = process.env.FIREFLIES_API_KEY || ''; +const twentyApiKey = process.env.TWENTY_API_KEY || ''; +const webhookSecret = process.env.FIREFLIES_WEBHOOK_SECRET || ''; + +if (!firefliesApiKey) { + console.error('❌ FIREFLIES_API_KEY is required'); + process.exit(1); +} +if (!twentyApiKey) { + console.error('❌ TWENTY_API_KEY is required'); + process.exit(1); +} +if (!webhookSecret) { + console.error('❌ FIREFLIES_WEBHOOK_SECRET is required to generate signature'); + process.exit(1); +} + +const payload = { + meetingId, + eventType: 'Transcription completed', +}; + +const body = JSON.stringify(payload); +const signature = `sha256=${createHmac('sha256', webhookSecret) + .update(body, 'utf8') + .digest('hex')}`; + +const main = async (): Promise => { + console.log(`🚀 Ingesting meeting ${meetingId} via webhook handler`); + const handler = new WebhookHandler(); + const result = await handler.handle(payload, { + 'x-hub-signature': signature, + body, + }); + + console.log('✅ Result:'); + console.log(JSON.stringify(result, null, 2)); + + if (result.errors && result.errors.length > 0) { + process.exitCode = 1; + } +}; + +main().catch((error) => { + console.error('❌ Failed to ingest meeting'); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); + diff --git a/packages/twenty-apps/community/fireflies/scripts/test-webhook.ts b/packages/twenty-apps/community/fireflies/scripts/test-webhook.ts index 0f4b3a962b..df4a97b4e1 100644 --- a/packages/twenty-apps/community/fireflies/scripts/test-webhook.ts +++ b/packages/twenty-apps/community/fireflies/scripts/test-webhook.ts @@ -40,11 +40,13 @@ const FIREFLIES_WEBHOOK_SECRET = process.env.FIREFLIES_WEBHOOK_SECRET || 'test_s const _FIREFLIES_API_KEY = process.env.FIREFLIES_API_KEY || 'test_api_key'; // Test meeting data (simulating Fireflies API response) -const TEST_MEETING_ID = 'test-meeting-local-' + Date.now(); +const TEST_MEETING_ID = process.env.MEETING_ID || 'test-meeting-local-' + Date.now(); +const CLIENT_REFERENCE_ID = process.env.CLIENT_REFERENCE_ID; + const TEST_WEBHOOK_PAYLOAD = { meetingId: TEST_MEETING_ID, eventType: 'Transcription completed', - clientReferenceId: 'test-client-ref', + ...(CLIENT_REFERENCE_ID ? { clientReferenceId: CLIENT_REFERENCE_ID } : {}), }; // Mock Fireflies GraphQL API response @@ -119,11 +121,17 @@ const main = async () => { } // Prepare webhook payload - const body = JSON.stringify(TEST_WEBHOOK_PAYLOAD); - const signature = generateHMACSignature(body, FIREFLIES_WEBHOOK_SECRET); + const unsignedBody = JSON.stringify(TEST_WEBHOOK_PAYLOAD); + const signature = generateHMACSignature(unsignedBody, FIREFLIES_WEBHOOK_SECRET); + const payloadWithSignature = { + ...TEST_WEBHOOK_PAYLOAD, + 'x-hub-signature': signature, + }; + const body = JSON.stringify(payloadWithSignature); console.log('📤 Sending webhook payload:'); - console.log(JSON.stringify(TEST_WEBHOOK_PAYLOAD, null, 2)); + console.log(JSON.stringify(payloadWithSignature, null, 2)); + console.log('\nℹ️ Signature is sent both as header (preferred) and in payload as fallback (headers are not passed to serverless functions)\n'); console.log(`\n🔐 HMAC Signature: ${signature}\n`); // Check if server is reachable @@ -178,6 +186,31 @@ const main = async () => { console.log('📥 Response Body:'); console.log(JSON.stringify(responseData, null, 2)); + // Report whether the server appears to have received the header signature + const debugMessages = Array.isArray((responseData as any)?.debug) + ? ((responseData as any).debug as string[]) + : []; + const headerMissing = + debugMessages.some((msg) => msg.includes('headerKeys=none')) || + debugMessages.some((msg) => msg.includes('providedSignature=undefined')); + const signatureErrors = + Array.isArray((responseData as any)?.errors) && + ((responseData as any).errors as unknown[]).some( + (err) => typeof err === 'string' && err.toLowerCase().includes('signature'), + ); + + if (headerMissing) { + console.log( + '\n⚠️ Server did not report any received headers; it may be using payload fallback for signature verification.', + ); + } else { + console.log('\n✅ Server reported headers present (header-based signature should be used).'); + } + + if (signatureErrors) { + console.log('⚠️ Signature was rejected by the server (check webhook secret / payload).'); + } + if (response.ok) { console.log('\n✅ Webhook test completed successfully!'); console.log('\n📋 Next steps:'); diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts deleted file mode 100644 index b219c2b0d5..0000000000 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/fireflies-api-client.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { createLogger } from './logger'; -import type { FirefliesMeetingData, FirefliesParticipant, SummaryFetchConfig } from './types'; - -const logger = createLogger('fireflies-api'); - -export class FirefliesApiClient { - private apiKey: string; - - constructor(apiKey: string) { - if (!apiKey) { - logger.critical('FIREFLIES_API_KEY is required but not provided - this is a critical configuration error'); - throw new Error('FIREFLIES_API_KEY is required'); - } - this.apiKey = apiKey; - } - - async fetchMeetingData( - meetingId: string, - options?: { timeout?: number } - ): Promise { - const query = ` - query GetTranscript($transcriptId: String!) { - transcript(id: $transcriptId) { - id - title - date - duration - participants - organizer_email - meeting_attendees { - displayName - email - phoneNumber - name - location - } - meeting_attendance { - name - join_time - leave_time - } - speakers { - name - } - summary { - action_items - overview - } - transcript_url - } - } - `; - - const controller = new AbortController(); - const timeoutId = options?.timeout - ? setTimeout(() => controller.abort(), options.timeout) - : null; - - try { - const response = await fetch('https://api.fireflies.ai/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - query, - variables: { transcriptId: meetingId }, - }), - signal: controller.signal, - }); - - if (timeoutId) clearTimeout(timeoutId); - - if (!response.ok) { - let errorDetails = `Fireflies API request failed with status ${response.status}`; - try { - const errorBody = await response.text(); - if (errorBody) { - errorDetails += `: ${errorBody}`; - } - } catch { - // Ignore if we can't read the response body - } - throw new Error(errorDetails); - } - - const json = await response.json() as { - data?: { transcript?: any }; - errors?: Array<{ message?: string }>; - }; - - if (json.errors && json.errors.length > 0) { - throw new Error(`Fireflies API error: ${json.errors[0]?.message || 'Unknown error'}`); - } - - const transcript = json.data?.transcript; - if (!transcript) { - throw new Error('Invalid response from Fireflies API: missing transcript data'); - } - - return this.transformMeetingData(transcript, meetingId); - } catch (error) { - if (timeoutId) clearTimeout(timeoutId); - throw error; - } - } - - async fetchMeetingDataWithRetry( - meetingId: string, - config: SummaryFetchConfig - ): Promise<{ data: FirefliesMeetingData; summaryReady: boolean }> { - // immediate_only: single attempt, no retries - if (config.strategy === 'immediate_only') { - logger.debug(`fetching meeting ${meetingId} (strategy: immediate_only)`); - const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000 }); - const ready = this.isSummaryReady(meetingData); - logger.debug(`summary ready: ${ready}`); - return { data: meetingData, summaryReady: ready }; - } - - // immediate_with_retry: retry with exponential backoff - logger.debug(`fetching meeting ${meetingId} (strategy: immediate_with_retry, maxAttempts: ${config.retryAttempts})`); - - for (let attempt = 1; attempt <= config.retryAttempts; attempt++) { - try { - const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000 }); - const ready = this.isSummaryReady(meetingData); - - logger.debug(`attempt ${attempt}/${config.retryAttempts}: summary ready=${ready}`); - - if (ready) { - return { data: meetingData, summaryReady: true }; - } - - if (attempt < config.retryAttempts) { - const delayMs = config.retryDelay * attempt; - logger.debug(`summary not ready, waiting ${delayMs}ms before retry ${attempt + 1}`); - await new Promise(resolve => setTimeout(resolve, delayMs)); - } else { - logger.debug(`max retries reached, returning partial data`); - return { data: meetingData, summaryReady: false }; - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(`attempt ${attempt}/${config.retryAttempts} failed: ${errorMsg}`); - - if (attempt === config.retryAttempts) { - throw error; - } - - const delayMs = config.retryDelay * attempt; - logger.debug(`retrying in ${delayMs}ms...`); - await new Promise(resolve => setTimeout(resolve, delayMs)); - } - } - - throw new Error('Failed to fetch meeting data after retries'); - } - - private isSummaryReady(meetingData: FirefliesMeetingData): boolean { - return ( - (meetingData.summary?.action_items?.length > 0) || - (meetingData.summary?.overview?.length > 0) || - meetingData.summary_status === 'completed' - ); - } - - private extractAllParticipants(transcript: any): FirefliesParticipant[] { - const participantsWithEmails: FirefliesParticipant[] = []; - const participantsNameOnly: FirefliesParticipant[] = []; - - logger.debug('=== PARTICIPANT EXTRACTION DEBUG ==='); - logger.debug('participants field:', JSON.stringify(transcript.participants)); - logger.debug('meeting_attendees field:', JSON.stringify(transcript.meeting_attendees)); - logger.debug('speakers field:', transcript.speakers?.map((s: any) => s.name)); - logger.debug('meeting_attendance field:', transcript.meeting_attendance?.map((a: any) => a.name)); - logger.debug('organizer_email:', transcript.organizer_email); - - // Helper function to check if a string is an email - const isEmail = (str: string): boolean => { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str.trim()); - }; - - // Helper function to check if already exists - const isDuplicate = (name: string, email: string): boolean => { - const nameLower = name.toLowerCase().trim(); - const emailLower = email.toLowerCase().trim(); - - return participantsWithEmails.some(p => - p.name.toLowerCase().trim() === nameLower || - (email && p.email.toLowerCase() === emailLower) - ) || participantsNameOnly.some(p => - p.name.toLowerCase().trim() === nameLower - ); - }; - - // 1. Extract from legacy participants field (with emails) - if (transcript.participants && Array.isArray(transcript.participants)) { - transcript.participants.forEach((participant: string) => { - // Handle comma-separated emails or names - const parts = participant.split(',').map(p => p.trim()); - - parts.forEach(part => { - const emailMatch = part.match(/<([^>]+)>/); - const email = emailMatch ? emailMatch[1] : ''; - // Extract name properly: if there's an email in angle brackets, get the part before it - const name = emailMatch - ? part.substring(0, part.indexOf('<')).trim() - : part.trim(); - - // Skip if the "name" is actually an email address - if (isEmail(name)) { - logger.debug(`Skipping participant with email as name: "${name}"`); - return; - } - - // Skip if empty name - if (!name) { - return; - } - - // Skip duplicates - if (isDuplicate(name, email)) { - logger.debug(`Skipping duplicate participant: "${name}" <${email}>`); - return; - } - - if (name && email) { - participantsWithEmails.push({ name, email }); - } else if (name) { - participantsNameOnly.push({ name, email: '' }); - } - }); - }); - } - - // 2. Extract from meeting_attendees field (structured) - if (transcript.meeting_attendees && Array.isArray(transcript.meeting_attendees)) { - transcript.meeting_attendees.forEach((attendee: any) => { - const name = attendee.displayName || attendee.name || ''; - const email = attendee.email || ''; - - // Skip if name is actually an email - if (isEmail(name)) { - logger.debug(`Skipping attendee with email as name: "${name}"`); - return; - } - - if (name && !isDuplicate(name, email)) { - if (email) { - participantsWithEmails.push({ name, email }); - } else { - participantsNameOnly.push({ name, email: '' }); - } - } - }); - } - - // 3. Extract from speakers field (name only) - if (transcript.speakers && Array.isArray(transcript.speakers)) { - transcript.speakers.forEach((speaker: any) => { - const name = speaker.name || ''; - - // Skip if name is actually an email - if (isEmail(name)) { - logger.debug(`Skipping speaker with email as name: "${name}"`); - return; - } - - if (name && !isDuplicate(name, '')) { - participantsNameOnly.push({ name, email: '' }); - } - }); - } - - // 4. Extract from meeting_attendance field (name only) - if (transcript.meeting_attendance && Array.isArray(transcript.meeting_attendance)) { - transcript.meeting_attendance.forEach((attendance: any) => { - const name = attendance.name || ''; - - // Skip if name is actually an email or contains comma-separated emails - if (isEmail(name) || name.includes(',')) { - logger.debug(`Skipping attendance with email/list as name: "${name}"`); - return; - } - - if (name && !isDuplicate(name, '')) { - participantsNameOnly.push({ name, email: '' }); - } - }); - } - - // 5. Add organizer email if available and not already included - const organizerEmail = transcript.organizer_email; - if (organizerEmail) { - // Check if organizer email is already in the participants - const existsWithEmail = participantsWithEmails.some(p => - p.email.toLowerCase() === organizerEmail.toLowerCase() - ); - - if (!existsWithEmail) { - // Try to find organizer name from speakers/attendance and match with email - let organizerName = ''; - - // Extract username from organizer email for matching - const emailUsername = organizerEmail.split('@')[0].toLowerCase(); - const emailNameVariations = [emailUsername]; - - // Add common name variations based on email username - if (emailUsername === 'alex') { - emailNameVariations.push('alexander', 'alexandre', 'alex'); - } - - // Look for organizer in speakers by matching email username to speaker names - if (transcript.speakers && Array.isArray(transcript.speakers)) { - const potentialOrganizerSpeaker = transcript.speakers.find((speaker: any) => { - const name = (speaker.name || '').toLowerCase(); - return emailNameVariations.some(variation => - name.includes(variation) || variation.includes(name) - ); - }); - if (potentialOrganizerSpeaker) { - organizerName = potentialOrganizerSpeaker.name; - } - } - - // Look for organizer in attendance - if (!organizerName && transcript.meeting_attendance && Array.isArray(transcript.meeting_attendance)) { - const potentialOrganizerAttendance = transcript.meeting_attendance.find((attendance: any) => { - const name = (attendance.name || '').toLowerCase(); - return emailNameVariations.some(variation => - name.includes(variation) || variation.includes(name) - ); - }); - if (potentialOrganizerAttendance) { - organizerName = potentialOrganizerAttendance.name; - } - } - - // If we found a name match, add as participant with email - if (organizerName) { - participantsWithEmails.push({ name: organizerName, email: organizerEmail }); - - // Remove from name-only participants to avoid duplicates - const nameIndex = participantsNameOnly.findIndex(p => - p.name.toLowerCase().includes(organizerName.toLowerCase()) || - organizerName.toLowerCase().includes(p.name.toLowerCase()) - ); - if (nameIndex !== -1) { - participantsNameOnly.splice(nameIndex, 1); - } - } else { - // If no name found, add with generic organizer name - participantsWithEmails.push({ name: 'Meeting Organizer', email: organizerEmail }); - } - } - } - - // Return participants with emails first, then name-only participants - const allParticipants = [...participantsWithEmails, ...participantsNameOnly]; - - logger.debug('=== EXTRACTED PARTICIPANTS ==='); - logger.debug('With emails:', participantsWithEmails.length, JSON.stringify(participantsWithEmails)); - logger.debug('Name only:', participantsNameOnly.length, JSON.stringify(participantsNameOnly)); - logger.debug('Total:', allParticipants.length); - - return allParticipants; - } - - private transformMeetingData(transcript: any, meetingId: string): FirefliesMeetingData { - // Convert date to ISO string - handle both timestamp and ISO string formats - let dateString: string; - if (transcript.date) { - if (typeof transcript.date === 'number') { - // Unix timestamp in milliseconds - dateString = new Date(transcript.date).toISOString(); - } else if (typeof transcript.date === 'string') { - // Could be ISO string or timestamp string - const parsed = Number(transcript.date); - if (!isNaN(parsed)) { - // It's a numeric string (timestamp) - dateString = new Date(parsed).toISOString(); - } else { - // It's already an ISO string - dateString = transcript.date; - } - } else { - dateString = new Date().toISOString(); - } - } else { - dateString = new Date().toISOString(); - } - - return { - id: transcript.id || meetingId, - title: transcript.title || 'Untitled Meeting', - date: dateString, - duration: transcript.duration || 0, - participants: this.extractAllParticipants(transcript), - organizer_email: transcript.organizer_email, - summary: { - action_items: Array.isArray(transcript.summary?.action_items) - ? transcript.summary.action_items - : (typeof transcript.summary?.action_items === 'string' - ? [transcript.summary.action_items] - : []), - overview: transcript.summary?.overview || '', - keywords: transcript.summary?.keywords, - topics_discussed: transcript.summary?.topics_discussed, - meeting_type: transcript.summary?.meeting_type, - }, - analytics: transcript.sentiments ? { - sentiments: { - positive_pct: transcript.sentiments.positive_pct || 0, - negative_pct: transcript.sentiments.negative_pct || 0, - neutral_pct: transcript.sentiments.neutral_pct || 0, - } - } : undefined, - transcript_url: transcript.transcript_url || `https://app.fireflies.ai/view/${meetingId}`, - recording_url: transcript.video_url || undefined, - summary_status: transcript.summary_status, - }; - } -} - diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/formatters.ts b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/formatters.ts deleted file mode 100644 index 6238228b2a..0000000000 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/formatters.ts +++ /dev/null @@ -1,227 +0,0 @@ -import type { FirefliesMeetingData, MeetingCreateInput } from './types'; - -export class MeetingFormatter { - static formatNoteBody(meetingData: FirefliesMeetingData): string { - const meetingDate = new Date(meetingData.date); - const formattedDate = meetingDate.toLocaleString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - const durationMinutes = Math.round(meetingData.duration); - - let noteBody = `**Date:** ${formattedDate}\n`; - noteBody += `**Duration:** ${durationMinutes} minutes\n`; - - if (meetingData.participants.length > 0) { - const participantNames = meetingData.participants.map(p => p.name).join(', '); - noteBody += `**Participants:** ${participantNames}\n`; - } - - // Overview section - if (meetingData.summary?.overview) { - noteBody += `\n## Overview\n${meetingData.summary.overview}\n`; - } - - // Key topics - if (meetingData.summary?.topics_discussed && Array.isArray(meetingData.summary.topics_discussed) && meetingData.summary.topics_discussed.length > 0) { - noteBody += `\n## Key Topics\n`; - meetingData.summary.topics_discussed.forEach(topic => { - noteBody += `- ${topic}\n`; - }); - } - - // Action items - if (meetingData.summary?.action_items && Array.isArray(meetingData.summary.action_items) && meetingData.summary.action_items.length > 0) { - noteBody += `\n## Action Items\n`; - meetingData.summary.action_items.forEach(item => { - noteBody += `- ${item}\n`; - }); - } - - // Insights section - noteBody += `\n## Insights\n`; - - if (meetingData.summary?.keywords && Array.isArray(meetingData.summary.keywords) && meetingData.summary.keywords.length > 0) { - noteBody += `**Keywords:** ${meetingData.summary.keywords.join(', ')}\n`; - } - - if (meetingData.analytics?.sentiments) { - const sentiments = meetingData.analytics.sentiments; - noteBody += `**Sentiment:** ${sentiments.positive_pct}% positive, ${sentiments.negative_pct}% negative, ${sentiments.neutral_pct}% neutral\n`; - } - - if (meetingData.summary?.meeting_type) { - noteBody += `**Meeting Type:** ${meetingData.summary.meeting_type}\n`; - } - - // Resources section - noteBody += `\n## Resources\n`; - noteBody += `[View Full Transcript](${meetingData.transcript_url})\n`; - - if (meetingData.recording_url) { - noteBody += `[Watch Recording](${meetingData.recording_url})\n`; - } - - return noteBody; - } - - static formatMeetingNotes(meetingData: FirefliesMeetingData): string { - const meetingDate = new Date(meetingData.date); - const formattedDate = meetingDate.toLocaleString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - const durationMinutes = Math.round(meetingData.duration); - - let meetingNotes = `**Date:** ${formattedDate}\n`; - meetingNotes += `**Duration:** ${durationMinutes} minutes\n`; - - if (meetingData.participants.length > 0) { - const participantNames = meetingData.participants.map(p => p.name).join(', '); - meetingNotes += `**Participants:** ${participantNames}\n`; - } - - // Overview section - if (meetingData.summary?.overview) { - meetingNotes += `\n## Overview\n${meetingData.summary.overview}\n`; - } - - // Key topics - if (meetingData.summary?.topics_discussed && Array.isArray(meetingData.summary.topics_discussed) && meetingData.summary.topics_discussed.length > 0) { - meetingNotes += `\n## Key Topics\n`; - meetingData.summary.topics_discussed.forEach(topic => { - meetingNotes += `- ${topic}\n`; - }); - } - - // Action items - if (meetingData.summary?.action_items && Array.isArray(meetingData.summary.action_items) && meetingData.summary.action_items.length > 0) { - meetingNotes += `\n## Action Items\n`; - meetingData.summary.action_items.forEach(item => { - meetingNotes += `- ${item}\n`; - }); - } - - // Insights section - meetingNotes += `\n## Insights\n`; - - if (meetingData.summary?.keywords && Array.isArray(meetingData.summary.keywords) && meetingData.summary.keywords.length > 0) { - meetingNotes += `**Keywords:** ${meetingData.summary.keywords.join(', ')}\n`; - } - - if (meetingData.analytics?.sentiments) { - const sentiments = meetingData.analytics.sentiments; - meetingNotes += `**Sentiment:** ${sentiments.positive_pct}% positive, ${sentiments.negative_pct}% negative, ${sentiments.neutral_pct}% neutral\n`; - } - - if (meetingData.summary?.meeting_type) { - meetingNotes += `**Meeting Type:** ${meetingData.summary.meeting_type}\n`; - } - - // Resources section - meetingNotes += `\n## Resources\n`; - meetingNotes += `[View Full Transcript](${meetingData.transcript_url})\n`; - - if (meetingData.recording_url) { - meetingNotes += `[Watch Recording](${meetingData.recording_url})\n`; - } - - return meetingNotes; - } - - - static toMeetingCreateInput( - meetingData: FirefliesMeetingData, - noteId?: string - ): MeetingCreateInput { - const durationMinutes = Math.round(meetingData.duration); - - // Build input object with only defined values (omit null fields) - const input: MeetingCreateInput = { - name: meetingData.title, - meetingDate: meetingData.date, - duration: durationMinutes, - actionItemsCount: meetingData.summary?.action_items?.length || 0, - firefliesMeetingId: meetingData.id, - }; - - // Add direct relationship to note if noteId is provided - if (noteId) { - input.noteId = noteId; - } - - // Only add optional fields if they have values - if (meetingData.summary?.meeting_type) { - input.meetingType = meetingData.summary.meeting_type; - } - - if (meetingData.summary?.keywords && Array.isArray(meetingData.summary.keywords) && meetingData.summary.keywords.length > 0) { - input.keywords = meetingData.summary.keywords.join(', '); - } - - if (meetingData.analytics?.sentiments?.positive_pct) { - input.sentimentScore = meetingData.analytics.sentiments.positive_pct / 100; - input.positivePercent = meetingData.analytics.sentiments.positive_pct; - } - - if (meetingData.analytics?.sentiments?.negative_pct) { - input.negativePercent = meetingData.analytics.sentiments.negative_pct; - } - - // Only add URLs if they are valid (not empty strings) - if (meetingData.transcript_url && meetingData.transcript_url.trim()) { - input.transcriptUrl = { - primaryLinkUrl: meetingData.transcript_url, - primaryLinkLabel: 'View Transcript' - }; - } - - if (meetingData.recording_url && meetingData.recording_url.trim()) { - input.recordingUrl = { - primaryLinkUrl: meetingData.recording_url, - primaryLinkLabel: 'Watch Recording' - }; - } - - if (meetingData.organizer_email) { - input.organizerEmail = meetingData.organizer_email; - } - - // Set success status and timestamps - input.importStatus = 'SUCCESS'; - input.lastImportAttempt = new Date().toISOString(); - input.importAttempts = 1; - - return input; - } - - static toFailedMeetingCreateInput( - meetingId: string, - title: string, - error: string, - attempts: number = 1 - ): MeetingCreateInput { - const currentDate = new Date().toISOString(); - - return { - name: title || `Failed Meeting Import - ${meetingId}`, - meetingDate: currentDate, - duration: 0, - actionItemsCount: 0, - firefliesMeetingId: meetingId, - importStatus: 'FAILED', - importError: error, - lastImportAttempt: currentDate, - importAttempts: attempts, - }; - } -} - diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/index.ts b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/index.ts index de6383a1bf..15a583e4ba 100644 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/index.ts +++ b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/index.ts @@ -1,4 +1,5 @@ -export { config, main } from './receive-fireflies-notes'; +// Serverless function entry point - re-exports from src/lib +export { config, main } from '../../../src'; export type { FirefliesMeetingData, FirefliesParticipant, @@ -6,5 +7,5 @@ export type { ProcessResult, SummaryFetchConfig, SummaryStrategy -} from './types'; +} from '../../../src'; diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts deleted file mode 100644 index 758740574e..0000000000 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/logger.ts +++ /dev/null @@ -1,95 +0,0 @@ -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; - -export interface LoggerConfig { - logLevel: LogLevel; - isTestEnvironment: boolean; -} - -const LOG_LEVELS: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, - silent: 4, -}; - -/** - * App-level Fireflies application logger with configurable log levels. - */ -export class AppLogger { - private config: LoggerConfig; - private context: string; - - constructor(context: string) { - this.context = context; - this.config = { - logLevel: this.parseLogLevel(process.env.LOG_LEVEL || 'error'), - isTestEnvironment: process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined, - }; - } - - private parseLogLevel(level: string): LogLevel { - const normalizedLevel = level.toLowerCase() as LogLevel; - return Object.keys(LOG_LEVELS).includes(normalizedLevel) ? normalizedLevel : 'error'; - } - - private shouldLog(level: LogLevel): boolean { - return LOG_LEVELS[level] >= LOG_LEVELS[this.config.logLevel]; - } - - /** - * Log debug information (LOG_LEVEL=debug) - */ - debug(message: string, ...args: any[]): void { - if (this.shouldLog('debug')) { - // eslint-disable-next-line no-console - console.log(`[${this.context}] ${message}`, ...args); - } - } - - /** - * Log informational messages (LOG_LEVEL=info or lower) - */ - info(message: string, ...args: any[]): void { - if (this.shouldLog('info')) { - // eslint-disable-next-line no-console - console.log(`[${this.context}] ${message}`, ...args); - } - } - - /** - * Log warnings (LOG_LEVEL=warn or lower) - */ - warn(message: string, ...args: any[]): void { - if (this.shouldLog('warn')) { - // eslint-disable-next-line no-console - console.warn(`[${this.context}] ${message}`, ...args); - } - } - - /** - * Log errors (LOG_LEVEL=error or lower) - */ - error(message: string, ...args: any[]): void { - if (this.shouldLog('error')) { - // eslint-disable-next-line no-console - console.error(`[${this.context}] ${message}`, ...args); - } - } - - /** - * Log critical errors that should ALWAYS be visible regardless of log level - * Use sparingly - only for fatal errors, security issues, or data corruption - */ - critical(message: string, ...args: any[]): void { - // eslint-disable-next-line no-console - console.error(`[${this.context}] CRITICAL: ${message}`, ...args); - } -} - -/** - * Factory function to create loggers with automatic context detection - */ -export const createLogger = (context: string): AppLogger => { - return new AppLogger(context); -}; diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/types.ts b/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/types.ts deleted file mode 100644 index b1596842d2..0000000000 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/types.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Fireflies API Types -export type FirefliesParticipant = { - email: string; - name: string; -}; - -export type FirefliesWebhookPayload = { - meetingId: string; - eventType: string; - clientReferenceId?: string; -}; - -export type FirefliesMeetingData = { - id: string; - title: string; - date: string; - duration: number; - participants: FirefliesParticipant[]; - organizer_email?: string; - summary: { - action_items: string[]; - keywords?: string[]; - overview: string; - gist?: string; - topics_discussed?: string[]; - meeting_type?: string; - bullet_gist?: string; - }; - analytics?: { - sentiments?: { - positive_pct: number; - negative_pct: number; - neutral_pct: number; - }; - }; - transcript_url: string; - recording_url?: string; - summary_status?: string; -}; - -// Configuration Types -export type SummaryStrategy = 'immediate_only' | 'immediate_with_retry' | 'delayed_polling' | 'basic_only'; - -export type SummaryFetchConfig = { - strategy: SummaryStrategy; - retryAttempts: number; - retryDelay: number; - pollInterval: number; - maxPolls: number; -}; - -export type WebhookConfig = { - secret: string; - apiUrl: string; -}; - -// Processing Result Types -export type ProcessResult = { - success: boolean; - meetingId?: string; - noteIds?: string[]; - newContacts?: string[]; - errors?: string[]; - debug?: string[]; - summaryReady?: boolean; - summaryPending?: boolean; - enhancementScheduled?: boolean; - actionItemsCount?: number; - sentimentScore?: number; - meetingType?: string; - keyTopics?: string[]; -}; - -// Twenty CRM Types -export type GraphQLResponse = { - data: T; - errors?: Array<{ - message?: string; - extensions?: { code?: string } - }>; -}; - -export type IdNode = { id: string }; - -export type FindMeetingResponse = { - meetings: { edges: Array<{ node: IdNode }> }; -}; - -export type FindPeopleResponse = { - people: { edges: Array<{ node: { id: string; emails: { primaryEmail: string } } }> }; -}; - -export type CreatePersonResponse = { - createPerson: { id: string } -}; - -export type CreateNoteResponse = { - createNote: { id: string } -}; - -export type CreateMeetingResponse = { - createMeeting: { id: string } -}; - -export type Contact = { - id: string; - email: string; -}; - -export type MeetingCreateInput = { - name: string; - noteId?: string | null; // This is the relation field - meetingDate: string; - duration: number; - meetingType?: string | null; - keywords?: string | null; - sentimentScore?: number | null; - positivePercent?: number | null; - negativePercent?: number | null; - actionItemsCount: number; - transcriptUrl?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; - recordingUrl?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; - firefliesMeetingId: string; - organizerEmail?: string | null; - importStatus?: 'SUCCESS' | 'FAILED' | 'PENDING' | 'RETRYING' | null; - importError?: string | null; - lastImportAttempt?: string | null; - importAttempts?: number | null; -}; - diff --git a/packages/twenty-apps/community/fireflies/src/__tests__/fireflies-webhook.spec.ts b/packages/twenty-apps/community/fireflies/src/__tests__/fireflies-webhook.spec.ts index 99c0ac4317..246d8beb8e 100644 --- a/packages/twenty-apps/community/fireflies/src/__tests__/fireflies-webhook.spec.ts +++ b/packages/twenty-apps/community/fireflies/src/__tests__/fireflies-webhook.spec.ts @@ -4,7 +4,7 @@ import { main, type FirefliesMeetingData, type FirefliesWebhookPayload, -} from '../actions/receive-fireflies-notes'; +} from '../'; // Helper to generate HMAC signature const generateHMACSignature = (body: string, secret: string): string => { @@ -39,10 +39,12 @@ const mockFirefliesApiResponseWithSummary = { meeting_type: 'Sales Call', bullet_gist: '• Demonstrated core product features\n• Discussed enterprise pricing\n• Addressed integration questions', }, - sentiments: { // Note: Raw API has sentiments at top level, not in analytics - positive_pct: 75, - negative_pct: 10, - neutral_pct: 15, + analytics: { + sentiments: { + positive_pct: 75, + negative_pct: 10, + neutral_pct: 15, + }, }, transcript_url: 'https://app.fireflies.ai/transcript/test-001', video_url: 'https://app.fireflies.ai/recording/test-001', @@ -81,7 +83,7 @@ const mockMeetingWithFullSummary: FirefliesMeetingData = { }, }, transcript_url: 'https://app.fireflies.ai/transcript/test-001', - recording_url: 'https://app.fireflies.ai/recording/test-001', + video_url: 'https://app.fireflies.ai/recording/test-001', summary_status: 'completed', }; @@ -404,7 +406,11 @@ describe('Fireflies Webhook Integration v2', () => { expect(result.success).toBe(true); expect(result.summaryReady).toBe(true); expect(result.actionItemsCount).toBe(3); - expect(result.sentimentScore).toBeCloseTo(0.75, 2); // Use toBeCloseTo for floating point comparison + expect(result.sentimentAnalysis).toEqual({ + positive_pct: 75, + negative_pct: 10, + neutral_pct: 15, + }); expect(result.meetingType).toBe('Sales Call'); expect(result.keyTopics).toEqual(['product features', 'pricing discussion', 'integration capabilities', 'support options']); }); @@ -565,7 +571,7 @@ describe('Fireflies Webhook Integration v2', () => { const createNoteMock = jest.fn(); - global.fetch = jest.fn().mockImplementation((url: string, options?: any) => { + global.fetch = jest.fn().mockImplementation((url: string, options?: RequestInit) => { if (url === 'https://api.fireflies.ai/graphql') { return Promise.resolve({ ok: true, @@ -576,9 +582,9 @@ describe('Fireflies Webhook Integration v2', () => { } // Twenty API - const body = options?.body ? JSON.parse(options.body) : {}; - if (body.query?.includes('createNote')) { - createNoteMock(body.variables); + const requestBody = options?.body ? JSON.parse(options.body as string) : {}; + if (requestBody.query?.includes('createNote')) { + createNoteMock(requestBody.variables); return Promise.resolve({ ok: true, json: () => Promise.resolve({ @@ -610,7 +616,7 @@ describe('Fireflies Webhook Integration v2', () => { expect(noteData.data.bodyV2.markdown).toContain('## Overview'); // Markdown header, not bold expect(noteData.data.bodyV2.markdown).toContain('## Action Items'); // Markdown header, not bold expect(noteData.data.bodyV2.markdown).toContain('**Sentiment:**'); // This is bold - expect(noteData.data.bodyV2.markdown).toContain('[View Full Transcript]'); + expect(noteData.data.bodyV2.markdown).toContain('View Full Transcript on Fireflies'); }); it('should create meeting records for multi-party meetings', async () => { @@ -624,7 +630,7 @@ describe('Fireflies Webhook Integration v2', () => { const createMeetingMock = jest.fn(); - global.fetch = jest.fn().mockImplementation((url: string, options?: any) => { + global.fetch = jest.fn().mockImplementation((url: string, options?: RequestInit) => { if (url === 'https://api.fireflies.ai/graphql') { return Promise.resolve({ ok: true, @@ -635,9 +641,9 @@ describe('Fireflies Webhook Integration v2', () => { } // Twenty API - const body = options?.body ? JSON.parse(options.body) : {}; - if (body.query?.includes('createMeeting')) { - createMeetingMock(body.variables); + const requestBody = options?.body ? JSON.parse(options.body as string) : {}; + if (requestBody.query?.includes('createMeeting')) { + createMeetingMock(requestBody.variables); return Promise.resolve({ ok: true, json: () => Promise.resolve({ @@ -645,7 +651,7 @@ describe('Fireflies Webhook Integration v2', () => { }), }); } - if (body.query?.includes('createNote')) { + if (requestBody.query?.includes('createNote')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ @@ -694,7 +700,7 @@ describe('Fireflies Webhook Integration v2', () => { }); it('should handle missing payload gracefully', async () => { - const result = await main(null as any, {}); + const result = await main(null as unknown, {}); expect(result.success).toBe(false); expect(result.errors).toBeDefined(); @@ -703,10 +709,10 @@ describe('Fireflies Webhook Integration v2', () => { it('should handle invalid payload structure', async () => { const invalidPayload = { invalid: 'data' }; - const result = await main(invalidPayload as any, {}); + const result = await main(invalidPayload as unknown, {}); - expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); + expect(result.success).toBe(false); + expect(result.errors).toBeDefined(); }); }); }); diff --git a/packages/twenty-apps/community/fireflies/src/__tests__/historical-importer.spec.ts b/packages/twenty-apps/community/fireflies/src/__tests__/historical-importer.spec.ts new file mode 100644 index 0000000000..4e829c7326 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/__tests__/historical-importer.spec.ts @@ -0,0 +1,84 @@ +import { HistoricalImporter } from '../historical-importer'; +import type { FirefliesMeetingData, SummaryFetchConfig } from '../types'; + +const summaryConfig: SummaryFetchConfig = { + strategy: 'immediate_with_retry', + retryAttempts: 1, + retryDelay: 0, + pollInterval: 0, + maxPolls: 0, +}; + +const sampleMeeting: FirefliesMeetingData = { + id: 'm-1', + title: 'Sample', + date: new Date().toISOString(), + duration: 30, + participants: [], + summary: { action_items: [], overview: '' }, + transcript_url: 'https://example.com', +}; + +describe('HistoricalImporter', () => { + const buildImporter = () => { + const firefliesClient = { + listTranscripts: jest.fn(), + fetchMeetingDataWithRetry: jest.fn(), + } as unknown as jest.Mocked; + + const twentyService = { + findMeetingByFirefliesId: jest.fn(), + matchParticipantsToContacts: jest.fn(), + createContactsForUnmatched: jest.fn(), + createNoteOnly: jest.fn(), + createMeeting: jest.fn(), + createNoteTarget: jest.fn(), + } as unknown as jest.Mocked; + + return { firefliesClient, twentyService }; + }; + + it('skips meetings that already exist by firefliesMeetingId', async () => { + const { firefliesClient, twentyService } = buildImporter(); + + firefliesClient.listTranscripts.mockResolvedValue([{ id: 'existing' }]); + twentyService.findMeetingByFirefliesId.mockResolvedValue({ id: 'twenty-id' }); + + const importer = new HistoricalImporter(firefliesClient, twentyService); + const result = await importer.run( + {}, + { dryRun: false, autoCreateContacts: true, summaryConfig, plan: 'free' }, + ); + + expect(result.skippedExisting).toBe(1); + expect(result.imported).toBe(0); + expect(twentyService.createMeeting).not.toHaveBeenCalled(); + expect(result.statuses[0].status).toBe('skipped_existing'); + }); + + it('supports dry-run without writing to Twenty', async () => { + const { firefliesClient, twentyService } = buildImporter(); + + firefliesClient.listTranscripts.mockResolvedValue([{ id: 'm-2' }]); + firefliesClient.fetchMeetingDataWithRetry.mockResolvedValue({ + data: sampleMeeting, + summaryReady: false, + }); + twentyService.findMeetingByFirefliesId.mockResolvedValue(undefined); + + const importer = new HistoricalImporter(firefliesClient, twentyService); + const result = await importer.run( + {}, + { dryRun: true, autoCreateContacts: false, summaryConfig, plan: 'free' }, + ); + + expect(result.imported).toBe(1); + expect(result.summaryPending).toBe(1); + expect(twentyService.createMeeting).not.toHaveBeenCalled(); + expect(twentyService.createNoteOnly).not.toHaveBeenCalled(); + expect(result.statuses).toHaveLength(1); + expect(result.statuses[0].status).toBe('pending_summary'); + }); +}); + + diff --git a/packages/twenty-apps/community/fireflies/src/__tests__/setup.ts b/packages/twenty-apps/community/fireflies/src/__tests__/setup.ts index 47f5b16e55..79263d9b5b 100644 --- a/packages/twenty-apps/community/fireflies/src/__tests__/setup.ts +++ b/packages/twenty-apps/community/fireflies/src/__tests__/setup.ts @@ -9,6 +9,7 @@ process.env.AUTO_CREATE_CONTACTS = 'true'; process.env.SERVER_URL = 'http://localhost:3000'; process.env.TWENTY_API_KEY = 'test-api-key'; process.env.LOG_LEVEL = 'silent'; +process.env.CAPTURE_LOGS = 'false'; // Reset mocks before each test beforeEach(() => { diff --git a/packages/twenty-apps/community/fireflies/src/__tests__/webhook-handler-logging.spec.ts b/packages/twenty-apps/community/fireflies/src/__tests__/webhook-handler-logging.spec.ts new file mode 100644 index 0000000000..0fde2f1e11 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/__tests__/webhook-handler-logging.spec.ts @@ -0,0 +1,40 @@ +import { WebhookHandler } from '../webhook-handler'; + +describe('WebhookHandler log capture', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { + ...originalEnv, + FIREFLIES_WEBHOOK_SECRET: 'testsecret', + FIREFLIES_API_KEY: '', + TWENTY_API_KEY: '', + CAPTURE_LOGS: 'false', + LOG_LEVEL: 'silent', + }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('includes debug logs in response when CAPTURE_LOGS is true', async () => { + process.env.CAPTURE_LOGS = 'true'; + + const handler = new WebhookHandler(); + const result = await handler.handle(null); + + expect(result.success).toBe(false); + expect(Array.isArray(result.debug)).toBe(true); + }); + + it('omits debug logs when CAPTURE_LOGS is false', async () => { + process.env.CAPTURE_LOGS = 'false'; + + const handler = new WebhookHandler(); + const result = await handler.handle(null); + + expect(result.debug).toBeUndefined(); + }); +}); + diff --git a/packages/twenty-apps/community/fireflies/src/actions/receive-fireflies-notes.ts b/packages/twenty-apps/community/fireflies/src/actions/receive-fireflies-notes.ts deleted file mode 100644 index 6c1c09fde6..0000000000 --- a/packages/twenty-apps/community/fireflies/src/actions/receive-fireflies-notes.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - config, - main, - type FirefliesMeetingData, - type FirefliesParticipant, - type FirefliesWebhookPayload, - type ProcessResult, - type SummaryFetchConfig, - type SummaryStrategy -} from '../../serverlessFunctions/receive-fireflies-notes/src/index'; - diff --git a/packages/twenty-apps/community/fireflies/src/fireflies-api-client.ts b/packages/twenty-apps/community/fireflies/src/fireflies-api-client.ts new file mode 100644 index 0000000000..d60c28fdd1 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/fireflies-api-client.ts @@ -0,0 +1,823 @@ +import { createLogger } from './logger'; +import { + FIREFLIES_PLANS, + type FirefliesMeetingData, + type FirefliesParticipant, + type FirefliesPlan, + type FirefliesTranscriptListItem, + type FirefliesTranscriptListOptions, + type SummaryFetchConfig +} from './types'; + +const logger = createLogger('fireflies-api'); + +export class FirefliesApiClient { + private apiKey: string; + + constructor(apiKey: string) { + if (!apiKey) { + logger.critical('FIREFLIES_API_KEY is required but not provided - this is a critical configuration error'); + throw new Error('FIREFLIES_API_KEY is required'); + } + this.apiKey = apiKey; + } + + async listTranscripts(options: FirefliesTranscriptListOptions = {}): Promise { + const { + organizers, + participants, + hostEmail, + participantEmail, + userId, + channelId, + mine, + fromDate, + toDate, + pageSize = 50, + maxRecords = 500, + } = options; + + const sanitizedOrganizers = organizers?.filter(Boolean); + const sanitizedParticipants = participants?.filter(Boolean); + + const transcripts: FirefliesTranscriptListItem[] = []; + let skip = options.skip ?? 0; + const limit = options.limit ?? pageSize; + + const baseQuery = ` + query Transcripts( + $limit: Int + $skip: Int + $hostEmail: String + $participantEmail: String + $organizers: [String!] + $participants: [String!] + $userId: String + $channelId: String + $mine: Boolean + $date: Float + ) { + transcripts( + limit: $limit + skip: $skip + host_email: $hostEmail + participant_email: $participantEmail + organizers: $organizers + participants: $participants + user_id: $userId + channel_id: $channelId + mine: $mine + date: $date + ) { + id + title + date + duration + organizer_email + participants + transcript_url + meeting_link + meeting_info { summary_status } + } + } + `; + + while (transcripts.length < maxRecords) { + const pageVariables = { + limit, + skip, + hostEmail, + participantEmail, + organizers: sanitizedOrganizers, + participants: sanitizedParticipants, + userId, + channelId, + mine, + date: fromDate, + }; + + const page = await this.executeTranscriptListQuery(baseQuery, pageVariables); + const normalized = page + .map((item) => { + const normalizedDate = this.normalizeDate(item.date); + return { + id: (item.id as string) || '', + title: (item.title as string) || 'Untitled Meeting', + date: normalizedDate, + duration: (item.duration as number) || 0, + organizer_email: item.organizer_email as string | undefined, + participants: Array.isArray(item.participants) + ? (item.participants as string[]) + : undefined, + transcript_url: item.transcript_url as string | undefined, + meeting_link: item.meeting_link as string | undefined, + summary_status: (item.meeting_info as { summary_status?: string } | undefined)?.summary_status, + }; + }) + .filter((item) => { + if (toDate && item.date) { + const itemTime = Date.parse(item.date); + if (!Number.isNaN(itemTime) && itemTime > toDate) { + return false; + } + } + return true; + }); + + transcripts.push(...normalized); + + if (page.length < limit) { + break; + } + + skip += limit; + } + + if (transcripts.length > maxRecords) { + return transcripts.slice(0, maxRecords); + } + + return transcripts; + } + + async fetchMeetingData( + meetingId: string, + options?: { timeout?: number; plan?: FirefliesPlan } + ): Promise { + const plan = options?.plan ?? FIREFLIES_PLANS.FREE; + const isPremiumPlan = + plan === FIREFLIES_PLANS.BUSINESS || plan === FIREFLIES_PLANS.ENTERPRISE; + + // Minimal query for free plans - only basic fields available on all plans + // Note: audio_url requires Pro+, video_url requires Business+ + const freeQuery = ` + query GetTranscriptMinimal($transcriptId: String!) { + transcript(id: $transcriptId) { + id + title + date + duration + participants + organizer_email + transcript_url + meeting_link + } + } + `; + + // Standard query for pro plans - adds speakers, summary, sentences, and audio_url (Pro+) + // Note: video_url requires Business+ + const proQuery = ` + query GetTranscriptBasic($transcriptId: String!) { + transcript(id: $transcriptId) { + id + title + date + duration + participants + organizer_email + speakers { + name + } + sentences { + index + speaker_name + text + start_time + end_time + } + summary { + overview + keywords + action_items + notes + gist + bullet_gist + short_summary + short_overview + outline + shorthand_bullet + } + meeting_info { + summary_status + } + transcript_url + audio_url + meeting_link + } + } + `; + + // Full query for business/enterprise - includes all fields + const businessQuery = ` + query GetTranscriptFull($transcriptId: String!) { + transcript(id: $transcriptId) { + id + title + date + duration + participants + organizer_email + analytics { + sentiments { + positive_pct + negative_pct + neutral_pct + } + categories { + questions + tasks + metrics + date_times + } + speakers { + speaker_id + name + duration + word_count + longest_monologue + filler_words + questions + words_per_minute + } + } + meeting_attendees { + displayName + email + phoneNumber + name + location + } + meeting_attendance { + name + join_time + leave_time + } + speakers { + name + } + sentences { + index + speaker_name + text + start_time + end_time + ai_filters { + task + question + sentiment + } + } + summary { + action_items + overview + keywords + notes + gist + bullet_gist + short_summary + short_overview + outline + shorthand_bullet + topics_discussed + meeting_type + transcript_chapters + } + meeting_info { + summary_status + } + transcript_url + audio_url + video_url + meeting_link + } + } + `; + + // Select query based on plan + const queryToUse = isPremiumPlan ? businessQuery : + (plan === FIREFLIES_PLANS.PRO ? proQuery : freeQuery); + + const planFeatures = { + [FIREFLIES_PLANS.FREE]: 'basic fields only (no summary, no audio/video)', + [FIREFLIES_PLANS.PRO]: 'summary, speakers, audio_url', + [FIREFLIES_PLANS.BUSINESS]: 'full access including analytics, video_url', + [FIREFLIES_PLANS.ENTERPRISE]: 'full access including analytics, video_url', + }; + logger.debug(`using ${plan} plan query (${planFeatures[plan]})`); + + try { + return await this.executeTranscriptQuery({ + meetingId, + query: queryToUse, + timeout: options?.timeout, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + // Detect plan-specific errors + const requiresBusiness = message.toLowerCase().includes('business or higher'); + const requiresPro = message.toLowerCase().includes('pro or higher'); + const planError = requiresBusiness || requiresPro || + message.toLowerCase().includes('higher plan') || + message.includes('Cannot query field'); + + // Fallback cascade: business -> pro -> free + if (planError) { + if (isPremiumPlan) { + logger.warn(`Plan limitation detected (configured: ${plan}), falling back to pro query`); + try { + return await this.executeTranscriptQuery({ + meetingId, + query: proQuery, + timeout: options?.timeout, + }); + } catch (proError) { + const proMessage = proError instanceof Error ? proError.message : String(proError); + if (proMessage.toLowerCase().includes('plan') || proMessage.includes('Cannot query field')) { + logger.warn('Pro query also failed, falling back to minimal free query'); + return this.executeTranscriptQuery({ + meetingId, + query: freeQuery, + timeout: options?.timeout, + }); + } + throw proError; + } + } else if (plan === FIREFLIES_PLANS.PRO) { + logger.warn(`Pro plan query failed (${requiresBusiness ? 'requires Business+' : 'unknown restriction'}), falling back to free query`); + return this.executeTranscriptQuery({ + meetingId, + query: freeQuery, + timeout: options?.timeout, + }); + } else { + // Already using free query - some field might still be restricted + logger.error( + 'Fireflies API rejected the minimal free query. This may indicate: ' + + '1) The transcript ID is invalid, or ' + + '2) Your API key does not have access to this transcript, or ' + + '3) An unexpected API restriction : open an issue' + ); + } + } + + throw error; + } + } + + async fetchMeetingDataWithRetry( + meetingId: string, + config: SummaryFetchConfig, + plan: FirefliesPlan = FIREFLIES_PLANS.FREE + ): Promise<{ data: FirefliesMeetingData; summaryReady: boolean }> { + // immediate_only: single attempt, no retries + if (config.strategy === 'immediate_only') { + logger.debug(`fetching meeting ${meetingId} (strategy: immediate_only)`); + const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000, plan }); + const ready = this.isSummaryReady(meetingData); + logger.debug(`summary ready: ${ready}`); + return { data: meetingData, summaryReady: ready }; + } + + // immediate_with_retry: retry with linear backoff + logger.debug(`fetching meeting ${meetingId} (strategy: immediate_with_retry, maxAttempts: ${config.retryAttempts})`); + + for (let attempt = 1; attempt <= config.retryAttempts; attempt++) { + try { + const meetingData = await this.fetchMeetingData(meetingId, { timeout: 10000, plan }); + const ready = this.isSummaryReady(meetingData); + + logger.debug(`attempt ${attempt}/${config.retryAttempts}: summary ready=${ready}`); + + if (ready) { + return { data: meetingData, summaryReady: true }; + } + + if (attempt < config.retryAttempts) { + const delayMs = config.retryDelay * attempt; + logger.debug(`summary not ready, waiting ${delayMs}ms before retry ${attempt + 1}`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } else { + logger.debug(`max retries reached, returning partial data`); + return { data: meetingData, summaryReady: false }; + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(`attempt ${attempt}/${config.retryAttempts} failed: ${errorMsg}`); + + if (attempt === config.retryAttempts) { + throw error; + } + + const delayMs = config.retryDelay * attempt; + logger.debug(`retrying in ${delayMs}ms...`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } + + throw new Error('Failed to fetch meeting data after retries'); + } + + private async executeTranscriptQuery({ + meetingId, + query, + timeout, + }: { + meetingId: string; + query: string; + timeout?: number; + }): Promise { + const controller = new AbortController(); + const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null; + + try { + const response = await fetch('https://api.fireflies.ai/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + query, + variables: { transcriptId: meetingId }, + }), + signal: controller.signal, + }); + + if (timeoutId) clearTimeout(timeoutId); + + if (!response.ok) { + let errorDetails = `Fireflies API request failed with status ${response.status}`; + try { + const errorBody = await response.text(); + if (errorBody) { + errorDetails += `: ${errorBody}`; + } + } catch { + // Ignore if we can't read the response body + } + throw new Error(errorDetails); + } + + const json = await response.json() as { + data?: { transcript?: Record }; + errors?: Array<{ message?: string }>; + }; + + if (json.errors && json.errors.length > 0) { + throw new Error(`Fireflies API error: ${json.errors[0]?.message || 'Unknown error'}`); + } + + const transcript = json.data?.transcript; + if (!transcript) { + throw new Error('Invalid response from Fireflies API: missing transcript data'); + } + + return this.transformMeetingData(transcript, meetingId); + } finally { + if (timeoutId) clearTimeout(timeoutId); + } + } + + private isSummaryReady(meetingData: FirefliesMeetingData): boolean { + return ( + (meetingData.summary?.action_items?.length > 0) || + (meetingData.summary?.overview?.length > 0) || + meetingData.summary_status === 'completed' + ); + } + + private extractAllParticipants(transcript: Record): FirefliesParticipant[] { + const participantsWithEmails: FirefliesParticipant[] = []; + const participantsNameOnly: FirefliesParticipant[] = []; + + logger.debug('=== PARTICIPANT EXTRACTION DEBUG ==='); + logger.debug('participants field:', JSON.stringify(transcript.participants)); + logger.debug('meeting_attendees field:', JSON.stringify(transcript.meeting_attendees)); + logger.debug('speakers field:', (transcript.speakers as Array<{ name: string }>)?.map((s) => s.name)); + logger.debug('meeting_attendance field:', (transcript.meeting_attendance as Array<{ name: string }>)?.map((a) => a.name)); + logger.debug('organizer_email:', transcript.organizer_email); + + const isEmail = (str: string): boolean => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str.trim()); + }; + + const isDuplicate = (name: string, email: string): boolean => { + const nameLower = name.toLowerCase().trim(); + const emailLower = email.toLowerCase().trim(); + + return participantsWithEmails.some(p => + p.name.toLowerCase().trim() === nameLower || + (email && p.email.toLowerCase() === emailLower) + ) || participantsNameOnly.some(p => + p.name.toLowerCase().trim() === nameLower + ); + }; + + // 1. Extract from legacy participants field (with emails) + if (transcript.participants && Array.isArray(transcript.participants)) { + transcript.participants.forEach((participant: string) => { + const parts = participant.split(',').map(p => p.trim()); + + parts.forEach(part => { + const emailMatch = part.match(/<([^>]+)>/); + const email = emailMatch ? emailMatch[1] : ''; + const name = emailMatch + ? part.substring(0, part.indexOf('<')).trim() + : part.trim(); + + if (isEmail(name)) { + logger.debug(`Skipping participant with email as name: "${name}"`); + return; + } + + if (!name) { + return; + } + + if (isDuplicate(name, email)) { + logger.debug(`Skipping duplicate participant: "${name}" <${email}>`); + return; + } + + if (name && email) { + participantsWithEmails.push({ name, email }); + } else if (name) { + participantsNameOnly.push({ name, email: '' }); + } + }); + }); + } + + // 2. Extract from meeting_attendees field (structured) + if (transcript.meeting_attendees && Array.isArray(transcript.meeting_attendees)) { + transcript.meeting_attendees.forEach((attendee: Record) => { + const name = (attendee.displayName || attendee.name || '') as string; + const email = (attendee.email || '') as string; + + if (isEmail(name)) { + logger.debug(`Skipping attendee with email as name: "${name}"`); + return; + } + + if (name && !isDuplicate(name, email)) { + if (email) { + participantsWithEmails.push({ name, email }); + } else { + participantsNameOnly.push({ name, email: '' }); + } + } + }); + } + + // 3. Extract from speakers field (name only) + if (transcript.speakers && Array.isArray(transcript.speakers)) { + transcript.speakers.forEach((speaker: Record) => { + const name = (speaker.name || '') as string; + + if (isEmail(name)) { + logger.debug(`Skipping speaker with email as name: "${name}"`); + return; + } + + if (name && !isDuplicate(name, '')) { + participantsNameOnly.push({ name, email: '' }); + } + }); + } + + // 4. Extract from meeting_attendance field (name only) + if (transcript.meeting_attendance && Array.isArray(transcript.meeting_attendance)) { + transcript.meeting_attendance.forEach((attendance: Record) => { + const name = (attendance.name || '') as string; + + if (isEmail(name) || name.includes(',')) { + logger.debug(`Skipping attendance with email/list as name: "${name}"`); + return; + } + + if (name && !isDuplicate(name, '')) { + participantsNameOnly.push({ name, email: '' }); + } + }); + } + + // 5. Add organizer email if available and not already included + const organizerEmail = transcript.organizer_email as string | undefined; + if (organizerEmail) { + const existsWithEmail = participantsWithEmails.some(p => + p.email.toLowerCase() === organizerEmail.toLowerCase() + ); + + if (!existsWithEmail) { + let organizerName = ''; + + const emailUsername = organizerEmail.split('@')[0].toLowerCase(); + const emailNameVariations = [emailUsername]; + + if (transcript.speakers && Array.isArray(transcript.speakers)) { + const potentialOrganizerSpeaker = transcript.speakers.find((speaker: Record) => { + const name = ((speaker.name || '') as string).toLowerCase(); + return emailNameVariations.some(variation => + name.includes(variation) || variation.includes(name) + ); + }) as Record | undefined; + if (potentialOrganizerSpeaker) { + organizerName = potentialOrganizerSpeaker.name as string; + } + } + + if (!organizerName && transcript.meeting_attendance && Array.isArray(transcript.meeting_attendance)) { + const potentialOrganizerAttendance = transcript.meeting_attendance.find((attendance: Record) => { + const name = ((attendance.name || '') as string).toLowerCase(); + return emailNameVariations.some(variation => + name.includes(variation) || variation.includes(name) + ); + }) as Record | undefined; + if (potentialOrganizerAttendance) { + organizerName = potentialOrganizerAttendance.name as string; + } + } + + if (organizerName) { + participantsWithEmails.push({ name: organizerName, email: organizerEmail }); + + const nameIndex = participantsNameOnly.findIndex(p => + p.name.toLowerCase().includes(organizerName.toLowerCase()) || + organizerName.toLowerCase().includes(p.name.toLowerCase()) + ); + if (nameIndex !== -1) { + participantsNameOnly.splice(nameIndex, 1); + } + } else { + participantsWithEmails.push({ name: 'Meeting Organizer', email: organizerEmail }); + } + } + } + + const allParticipants = [...participantsWithEmails, ...participantsNameOnly]; + + logger.debug('=== EXTRACTED PARTICIPANTS ==='); + logger.debug('With emails:', participantsWithEmails.length, JSON.stringify(participantsWithEmails)); + logger.debug('Name only:', participantsNameOnly.length, JSON.stringify(participantsNameOnly)); + logger.debug('Total:', allParticipants.length); + + return allParticipants; + } + + private transformMeetingData(transcript: Record, meetingId: string): FirefliesMeetingData { + let dateString: string; + if (transcript.date) { + if (typeof transcript.date === 'number') { + dateString = new Date(transcript.date).toISOString(); + } else if (typeof transcript.date === 'string') { + const parsed = Number(transcript.date); + if (!isNaN(parsed)) { + dateString = new Date(parsed).toISOString(); + } else { + dateString = transcript.date; + } + } else { + dateString = new Date().toISOString(); + } + } else { + dateString = new Date().toISOString(); + } + + const summary = transcript.summary as Record | undefined; + const analytics = transcript.analytics as Record | undefined; + const sentiments = analytics?.sentiments as Record | undefined; + const categories = analytics?.categories as Record | undefined; + const speakersAnalytics = analytics?.speakers as Array> | undefined; + const meetingInfo = transcript.meeting_info as Record | undefined; + + // Transform sentences array + const rawSentences = transcript.sentences as Array> | undefined; + const sentences = rawSentences?.map(s => ({ + index: (s.index as number) || 0, + speaker_name: (s.speaker_name as string) || 'Unknown', + text: (s.text as string) || '', + start_time: (s.start_time as string) || '0', + end_time: (s.end_time as string) || '0', + ai_filters: s.ai_filters as { task?: boolean; question?: boolean; sentiment?: string } | undefined, + })); + + // Transform speaker analytics + const speakers = speakersAnalytics?.map(sp => ({ + speaker_id: (sp.speaker_id as string) || '', + name: (sp.name as string) || 'Unknown', + duration: (sp.duration as number) || 0, + word_count: (sp.word_count as number) || 0, + longest_monologue: (sp.longest_monologue as number) || 0, + filler_words: (sp.filler_words as number) || 0, + questions: (sp.questions as number) || 0, + words_per_minute: (sp.words_per_minute as number) || 0, + })); + + return { + id: (transcript.id as string) || meetingId, + title: (transcript.title as string) || 'Untitled Meeting', + date: dateString, + duration: (transcript.duration as number) || 0, + participants: this.extractAllParticipants(transcript), + organizer_email: transcript.organizer_email as string | undefined, + sentences, + summary: { + // action_items can be string or array - normalize to array + action_items: Array.isArray(summary?.action_items) + ? summary.action_items as string[] + : (typeof summary?.action_items === 'string' && summary.action_items.trim() + ? summary.action_items.split('\n').filter((item: string) => item.trim()) + : []), + overview: (summary?.overview as string) || '', + notes: summary?.notes as string | undefined, + gist: summary?.gist as string | undefined, + bullet_gist: summary?.bullet_gist as string | undefined, + short_summary: summary?.short_summary as string | undefined, + short_overview: summary?.short_overview as string | undefined, + outline: summary?.outline as string | undefined, + shorthand_bullet: summary?.shorthand_bullet as string | undefined, + keywords: summary?.keywords as string[] | undefined, + topics_discussed: summary?.topics_discussed as string[] | undefined, + meeting_type: summary?.meeting_type as string | undefined, + transcript_chapters: summary?.transcript_chapters as string[] | undefined, + }, + analytics: (sentiments || categories || speakers) ? { + sentiments: sentiments ? { + positive_pct: sentiments.positive_pct || 0, + negative_pct: sentiments.negative_pct || 0, + neutral_pct: sentiments.neutral_pct || 0, + } : undefined, + categories: categories ? { + questions: categories.questions || 0, + tasks: categories.tasks || 0, + metrics: categories.metrics || 0, + date_times: categories.date_times || 0, + } : undefined, + speakers, + } : undefined, + meeting_info: meetingInfo ? { + summary_status: meetingInfo.summary_status as string | undefined, + } : undefined, + // URLs by plan availability: + transcript_url: (transcript.transcript_url as string) || `https://app.fireflies.ai/view/${meetingId}`, + audio_url: transcript.audio_url as string | undefined, // Pro+ + video_url: transcript.video_url as string | undefined, // Business+ + meeting_link: transcript.meeting_link as string | undefined, // All plans + summary_status: (meetingInfo?.summary_status as string) || (transcript.summary_status as string) || undefined, + }; + } + + private async executeTranscriptListQuery( + query: string, + variables: Record, + ): Promise>> { + const response = await fetch('https://api.fireflies.ai/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`Fireflies transcripts request failed: ${response.status} ${errorBody}`); + } + + const json = await response.json() as { + data?: { transcripts?: Array> }; + errors?: Array<{ message?: string }>; + }; + + if (json.errors && json.errors.length > 0) { + const message = json.errors[0]?.message || 'Unknown error'; + throw new Error(`Fireflies API error: ${message}`); + } + + return json.data?.transcripts ?? []; + } + + private normalizeDate(dateValue: unknown): string | undefined { + if (!dateValue) { + return undefined; + } + + if (typeof dateValue === 'number') { + return new Date(dateValue).toISOString(); + } + + if (typeof dateValue === 'string') { + const parsed = Number(dateValue); + if (!Number.isNaN(parsed)) { + return new Date(parsed).toISOString(); + } + return dateValue; + } + + return undefined; + } +} + diff --git a/packages/twenty-apps/community/fireflies/src/formatters.ts b/packages/twenty-apps/community/fireflies/src/formatters.ts new file mode 100644 index 0000000000..ef0c69a61b --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/formatters.ts @@ -0,0 +1,259 @@ +import type { FirefliesMeetingData, FirefliesSentence, MeetingCreateInput } from './types'; + +export class MeetingFormatter { + // Format timestamp from seconds to MM:SS + private static formatTimestamp(timeStr: string): string { + const seconds = parseFloat(timeStr); + if (isNaN(seconds)) return '00:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + // Format full transcript from sentences + private static formatTranscript(sentences: FirefliesSentence[]): string { + if (!sentences || sentences.length === 0) return ''; + + let transcript = ''; + let currentSpeaker = ''; + + for (const sentence of sentences) { + const timestamp = this.formatTimestamp(sentence.start_time); + const speaker = sentence.speaker_name || 'Unknown'; + + // Add speaker header when speaker changes + if (speaker !== currentSpeaker) { + currentSpeaker = speaker; + transcript += `\n**${speaker}** [${timestamp}]\n`; + } + + transcript += `${sentence.text} `; + } + + return transcript.trim(); + } + + static formatNoteBody(meetingData: FirefliesMeetingData): string { + const meetingDate = meetingData.date ? new Date(meetingData.date) : null; + const hasValidDate = meetingDate instanceof Date && !Number.isNaN(meetingDate.getTime()); + const formattedDate = hasValidDate + ? meetingDate.toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + : 'Unknown date'; + const durationMinutes = Math.round(meetingData.duration); + + let noteBody = `**Date:** ${formattedDate}\n`; + noteBody += `**Duration:** ${durationMinutes} minutes\n`; + + if (meetingData.participants.length > 0) { + const participantNames = meetingData.participants.map(p => p.name).join(', '); + noteBody += `**Participants:** ${participantNames}\n`; + } + + // Overview section + if (meetingData.summary?.overview) { + noteBody += `\n## Overview\n${meetingData.summary.overview}\n`; + } + + // Detailed AI Notes (the rich content from Fireflies) + if (meetingData.summary?.notes) { + noteBody += `\n## Meeting Notes\n${meetingData.summary.notes}\n`; + } + + // Bullet gist with emojis (if available and different from notes) + if (meetingData.summary?.bullet_gist && !meetingData.summary?.notes) { + noteBody += `\n## Key Points\n${meetingData.summary.bullet_gist}\n`; + } + + // Meeting outline with timestamps (shorthand_bullet contains the timestamped outline) + const outline = meetingData.summary?.outline || meetingData.summary?.shorthand_bullet; + if (outline) { + noteBody += `\n## Outline\n${outline}\n`; + } + + // Key topics + if (meetingData.summary?.topics_discussed?.length) { + noteBody += `\n## Key Topics\n`; + meetingData.summary.topics_discussed.forEach(topic => { + noteBody += `- ${topic}\n`; + }); + } + + // Action items + if (meetingData.summary?.action_items?.length) { + noteBody += `\n## Action Items\n`; + meetingData.summary.action_items.forEach(item => { + noteBody += `- [ ] ${item}\n`; + }); + } + + // Insights section + noteBody += `\n## Insights\n`; + + if (meetingData.summary?.keywords?.length) { + noteBody += `**Keywords:** ${meetingData.summary.keywords.join(', ')}\n`; + } + + if (meetingData.analytics?.sentiments) { + const sentiments = meetingData.analytics.sentiments; + noteBody += `**Sentiment:** ${sentiments.positive_pct}% positive, ${sentiments.negative_pct}% negative, ${sentiments.neutral_pct}% neutral\n`; + } + + if (meetingData.summary?.meeting_type) { + noteBody += `**Meeting Type:** ${meetingData.summary.meeting_type}\n`; + } + + // Speaker analytics (Business+) + if (meetingData.analytics?.speakers?.length) { + noteBody += `\n### Speaker Stats\n`; + for (const speaker of meetingData.analytics.speakers) { + const talkTime = Math.round(speaker.duration / 60); + noteBody += `- **${speaker.name}**: ${talkTime} min talk time, ${speaker.word_count} words, ${speaker.questions} questions\n`; + } + } + + // Meeting metrics (Business+) + if (meetingData.analytics?.categories) { + const cats = meetingData.analytics.categories; + noteBody += `\n### Meeting Metrics\n`; + noteBody += `- Questions asked: ${cats.questions}\n`; + noteBody += `- Tasks identified: ${cats.tasks}\n`; + if (cats.metrics > 0) noteBody += `- Metrics mentioned: ${cats.metrics}\n`; + if (cats.date_times > 0) noteBody += `- Dates/times discussed: ${cats.date_times}\n`; + } + + // Resources section + noteBody += `\n## Resources\n`; + noteBody += `[View Full Transcript on Fireflies](${meetingData.transcript_url})\n`; + + if (meetingData.video_url) { + noteBody += `[Watch Video Recording](${meetingData.video_url})\n`; + } + + if (meetingData.audio_url) { + noteBody += `[Listen to Audio](${meetingData.audio_url})\n`; + } + + if (meetingData.meeting_link) { + noteBody += `[Original Meeting Link](${meetingData.meeting_link})\n`; + } + + return noteBody; + } + + static toMeetingCreateInput( + meetingData: FirefliesMeetingData, + noteId?: string + ): MeetingCreateInput { + const durationMinutes = Math.round(meetingData.duration); + const hasSummary = Boolean(meetingData.summary?.overview || meetingData.summary?.action_items?.length); + const hasAnalytics = Boolean(meetingData.analytics?.sentiments); + + // Build input object with only defined values (omit null fields) + const input: MeetingCreateInput = { + name: meetingData.title, + meetingDate: meetingData.date, + duration: durationMinutes, + actionItemsCount: meetingData.summary?.action_items?.length || 0, + firefliesMeetingId: meetingData.id, + }; + + // Add direct relationship to note if noteId is provided + if (noteId) { + input.noteId = noteId; + } + + // Basic fields (All plans) + if (meetingData.organizer_email) { + input.organizerEmail = meetingData.organizer_email; + } + if (meetingData.transcript_url?.trim()) { + input.transcriptUrl = { + primaryLinkUrl: meetingData.transcript_url, + primaryLinkLabel: 'View Transcript' + }; + } + if (meetingData.meeting_link?.trim()) { + input.meetingLink = { + primaryLinkUrl: meetingData.meeting_link, + primaryLinkLabel: 'Join Meeting' + }; + } + + // Pro+ fields (transcript, summary, notes, keywords, audio) + if (meetingData.sentences?.length) { + input.transcript = this.formatTranscript(meetingData.sentences); + } + if (meetingData.summary?.overview) { + input.overview = meetingData.summary.overview; + } + if (meetingData.summary?.notes) { + input.notes = meetingData.summary.notes; + } + if (meetingData.summary?.keywords?.length) { + input.keywords = meetingData.summary.keywords.join(', '); + } + if (meetingData.audio_url?.trim()) { + input.audioUrl = { + primaryLinkUrl: meetingData.audio_url, + primaryLinkLabel: 'Listen to Audio' + }; + } + + // Business+ fields (analytics, video, detailed summary) + if (meetingData.summary?.meeting_type) { + input.meetingType = meetingData.summary.meeting_type; + } + if (meetingData.summary?.topics_discussed?.length) { + input.topics = meetingData.summary.topics_discussed.join(', '); + } + if (meetingData.analytics?.sentiments) { + const sentiments = meetingData.analytics.sentiments; + input.positivePercent = sentiments.positive_pct; + input.negativePercent = sentiments.negative_pct; + input.neutralPercent = sentiments.neutral_pct; + } + if (meetingData.video_url?.trim()) { + input.videoUrl = { + primaryLinkUrl: meetingData.video_url, + primaryLinkLabel: 'Watch Video' + }; + } + + // Import status based on data completeness + const isPartial = !hasSummary && !hasAnalytics; + input.importStatus = isPartial ? 'PARTIAL' : 'SUCCESS'; + input.lastImportAttempt = new Date().toISOString(); + input.importAttempts = 1; + + return input; + } + + static toFailedMeetingCreateInput( + meetingId: string, + title: string, + error: string, + attempts: number = 1 + ): MeetingCreateInput { + const currentDate = new Date().toISOString(); + + return { + name: title || `Failed Meeting Import - ${meetingId}`, + meetingDate: currentDate, + duration: 0, + actionItemsCount: 0, + firefliesMeetingId: meetingId, + importStatus: 'FAILED', + importError: error, + lastImportAttempt: currentDate, + importAttempts: attempts, + }; + } +} + diff --git a/packages/twenty-apps/community/fireflies/src/historical-importer.ts b/packages/twenty-apps/community/fireflies/src/historical-importer.ts new file mode 100644 index 0000000000..ebebef071c --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/historical-importer.ts @@ -0,0 +1,161 @@ +import { type FirefliesApiClient } from './fireflies-api-client'; +import { MeetingFormatter } from './formatters'; +import { createLogger } from './logger'; +import { type TwentyCrmService } from './twenty-crm-service'; +import type { + FirefliesPlan, + FirefliesTranscriptListOptions, + SummaryFetchConfig, +} from './types'; + +const logger = createLogger('historical-importer'); + +export type HistoricalImportFilters = FirefliesTranscriptListOptions; + +export type HistoricalImportOptions = { + dryRun?: boolean; + autoCreateContacts: boolean; + summaryConfig: SummaryFetchConfig; + plan: FirefliesPlan; +}; + +export type HistoricalImportResult = { + dryRun: boolean; + totalListed: number; + imported: number; + skippedExisting: number; + failed: Array<{ meetingId: string; reason: string }>; + summaryPending: number; + statuses: Array<{ + meetingId: string; + title?: string; + status: 'imported' | 'skipped_existing' | 'failed' | 'dry_run' | 'pending_summary'; + reason?: string; + }>; +}; + +export class HistoricalImporter { + private firefliesClient: FirefliesApiClient; + private twentyService: TwentyCrmService; + + constructor(firefliesClient: FirefliesApiClient, twentyService: TwentyCrmService) { + this.firefliesClient = firefliesClient; + this.twentyService = twentyService; + } + + async run( + filters: HistoricalImportFilters, + options: HistoricalImportOptions, + ): Promise { + const { dryRun = false, autoCreateContacts, summaryConfig, plan } = options; + + logger.info('Listing Fireflies transcripts for historical import...'); + const transcripts = await this.firefliesClient.listTranscripts(filters); + logger.info(`Found ${transcripts.length} transcript(s) to process`); + + let imported = 0; + let skippedExisting = 0; + let summaryPending = 0; + const failed: Array<{ meetingId: string; reason: string }> = []; + const statuses: HistoricalImportResult['statuses'] = []; + + for (const transcript of transcripts) { + const meetingId = transcript.id; + + try { + const existing = await this.twentyService.findMeetingByFirefliesId(meetingId); + if (existing) { + logger.debug(`Skipping ${meetingId}: already exists in Twenty (${existing.id})`); + skippedExisting += 1; + statuses.push({ + meetingId, + title: transcript.title, + status: 'skipped_existing', + }); + continue; + } + + logger.info(`Fetching meeting ${meetingId} details`); + const { data: meetingData, summaryReady } = + await this.firefliesClient.fetchMeetingDataWithRetry( + meetingId, + summaryConfig, + plan, + ); + + const isPendingSummary = summaryReady === false; + if (isPendingSummary) { + summaryPending += 1; + } + + if (dryRun) { + logger.info(`[dry-run] Would import meeting "${meetingData.title}" (${meetingId})`); + imported += 1; + statuses.push({ + meetingId, + title: meetingData.title, + status: isPendingSummary ? 'pending_summary' : 'dry_run', + reason: isPendingSummary ? 'summary not ready' : undefined, + }); + continue; + } + + const { matchedContacts, unmatchedParticipants } = + await this.twentyService.matchParticipantsToContacts( + meetingData.participants, + ); + + const newContactIds = autoCreateContacts + ? await this.twentyService.createContactsForUnmatched(unmatchedParticipants) + : []; + const allContactIds = [...matchedContacts.map(({ id }) => id), ...newContactIds]; + + const noteBody = MeetingFormatter.formatNoteBody(meetingData); + const noteId = await this.twentyService.createNoteOnly( + `Meeting: ${meetingData.title}`, + noteBody, + ); + + const meetingInput = MeetingFormatter.toMeetingCreateInput(meetingData, noteId); + const createdMeetingId = await this.twentyService.createMeeting(meetingInput); + + for (const contactId of allContactIds) { + await this.twentyService.createNoteTarget(noteId, contactId); + } + + logger.info( + `Imported meeting "${meetingData.title}" (${meetingId}) as ${createdMeetingId}`, + ); + imported += 1; + statuses.push({ + meetingId, + title: meetingData.title, + status: isPendingSummary ? 'pending_summary' : 'imported', + reason: isPendingSummary ? 'summary not ready' : undefined, + }); + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + logger.error(`Failed to import meeting ${meetingId}: ${reason}`); + failed.push({ meetingId, reason }); + statuses.push({ + meetingId, + title: transcript.title, + status: 'failed', + reason, + }); + } + } + + return { + dryRun, + totalListed: transcripts.length, + imported, + skippedExisting, + failed, + summaryPending, + statuses, + }; + } +} + + diff --git a/packages/twenty-apps/community/fireflies/src/index.ts b/packages/twenty-apps/community/fireflies/src/index.ts new file mode 100644 index 0000000000..4549f1b1e1 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/index.ts @@ -0,0 +1,31 @@ +// Main exports for the Fireflies integration +export { config, main } from './receive-fireflies-notes'; + +// Types +export type { + FirefliesMeetingData, + FirefliesParticipant, + FirefliesWebhookPayload, + ProcessResult, + SummaryFetchConfig, + SummaryStrategy +} from './types'; + +// Services (for advanced usage) +export { FirefliesApiClient } from './fireflies-api-client'; +export { MeetingFormatter } from './formatters'; +export { TwentyCrmService } from './twenty-crm-service'; +export { WebhookHandler } from './webhook-handler'; + +// Objects +export { Meeting } from './objects'; + +// Utilities +export { createLogger } from './logger'; +export { getApiUrl, getSummaryFetchConfig, shouldAutoCreateContacts, toBoolean } from './utils'; +export { + getWebhookSecretFingerprint, + isValidFirefliesPayload, + verifyWebhookSignature +} from './webhook-validator'; + diff --git a/packages/twenty-apps/community/fireflies/src/logger.ts b/packages/twenty-apps/community/fireflies/src/logger.ts new file mode 100644 index 0000000000..88d49cab47 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/logger.ts @@ -0,0 +1,142 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; + +export type LoggerConfig = { + logLevel: LogLevel; + isTestEnvironment: boolean; + captureForResponse: boolean; +}; + +const LOG_LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +}; + +const loggerRegistry = new Set(); + +export class AppLogger { + private config: LoggerConfig; + private context: string; + private capturedLogs: string[] = []; + + constructor(context: string) { + this.context = context; + this.config = { + logLevel: this.parseLogLevel(process.env.LOG_LEVEL || 'error'), + isTestEnvironment: process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined, + captureForResponse: process.env.CAPTURE_LOGS === 'true', + }; + + // Silence logs in test environment unless explicitly overridden + if (this.config.isTestEnvironment && process.env.LOG_LEVEL === undefined) { + this.config.logLevel = 'silent'; + } + } + + private parseLogLevel(level: string): LogLevel { + const normalizedLevel = level.toLowerCase() as LogLevel; + return Object.keys(LOG_LEVELS).includes(normalizedLevel) ? normalizedLevel : 'error'; + } + + private shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] >= LOG_LEVELS[this.config.logLevel]; + } + + private safeStringify(value: unknown): string { + try { + if (typeof value === 'string') return value; + return JSON.stringify(value); + } catch { + return '[unserializable]'; + } + } + + private captureLog(level: LogLevel, message: string, ...args: unknown[]): void { + if (!this.config.captureForResponse) { + return; + } + + const timestamp = new Date().toISOString(); + const formattedMessage = + args.length > 0 + ? `${message} ${args.map((arg) => this.safeStringify(arg)).join(' ')}` + : message; + + this.capturedLogs.push( + `[${timestamp}] [${level.toUpperCase()}] [${this.context}] ${formattedMessage}`, + ); + } + + debug(message: string, ...args: unknown[]): void { + this.captureLog('debug', message, ...args); + if (this.shouldLog('debug')) { + // eslint-disable-next-line no-console + console.log(`[${this.context}] ${message}`, ...args); + } + } + + info(message: string, ...args: unknown[]): void { + this.captureLog('info', message, ...args); + if (this.shouldLog('info')) { + // eslint-disable-next-line no-console + console.log(`[${this.context}] ${message}`, ...args); + } + } + + warn(message: string, ...args: unknown[]): void { + this.captureLog('warn', message, ...args); + if (this.shouldLog('warn')) { + // eslint-disable-next-line no-console + console.warn(`[${this.context}] ${message}`, ...args); + } + } + + error(message: string, ...args: unknown[]): void { + this.captureLog('error', message, ...args); + if (this.shouldLog('error')) { + // eslint-disable-next-line no-console + console.error(`[${this.context}] ${message}`, ...args); + } + } + + // For fatal errors, security issues, or data corruption - always visible + critical(message: string, ...args: unknown[]): void { + this.captureLog('error', `CRITICAL: ${message}`, ...args); + // eslint-disable-next-line no-console + console.error(`[${this.context}] CRITICAL: ${message}`, ...args); + } + + getCapturedLogs(): string[] { + return [...this.capturedLogs]; + } + + clearCapturedLogs(): void { + this.capturedLogs = []; + } +} + +export const createLogger = (context: string): AppLogger => { + const logger = new AppLogger(context); + loggerRegistry.add(logger); + return logger; +}; + +export const removeLogger = (logger: AppLogger): void => { + loggerRegistry.delete(logger); +}; + +export const getAllCapturedLogs = (): string[] => { + const allLogs: string[] = []; + for (const logger of loggerRegistry) { + allLogs.push(...logger.getCapturedLogs()); + } + return allLogs.sort(); +}; + +export const clearAllCapturedLogs = (): void => { + for (const logger of loggerRegistry) { + logger.clearCapturedLogs(); + } +}; diff --git a/packages/twenty-apps/community/fireflies/src/objects/index.ts b/packages/twenty-apps/community/fireflies/src/objects/index.ts new file mode 100644 index 0000000000..00189441a4 --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/objects/index.ts @@ -0,0 +1,3 @@ +export { Meeting } from './meeting'; + + diff --git a/packages/twenty-apps/community/fireflies/src/objects/meeting.ts b/packages/twenty-apps/community/fireflies/src/objects/meeting.ts index cf83f35cf2..ce78df3dc2 100644 --- a/packages/twenty-apps/community/fireflies/src/objects/meeting.ts +++ b/packages/twenty-apps/community/fireflies/src/objects/meeting.ts @@ -1,6 +1,6 @@ -import { ObjectMetadata } from 'twenty-sdk/application'; +import { Object } from 'twenty-sdk'; -@ObjectMetadata({ +@Object({ universalIdentifier: 'd1831348-b4a4-4426-9c0b-0af19e7a9c27', nameSingular: 'meeting', namePlural: 'meetings', diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/receive-fireflies-notes.ts b/packages/twenty-apps/community/fireflies/src/receive-fireflies-notes.ts similarity index 82% rename from packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/receive-fireflies-notes.ts rename to packages/twenty-apps/community/fireflies/src/receive-fireflies-notes.ts index 5293d1130c..9f8492fcfe 100644 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/receive-fireflies-notes.ts +++ b/packages/twenty-apps/community/fireflies/src/receive-fireflies-notes.ts @@ -1,4 +1,4 @@ -import { type ServerlessFunctionConfig } from 'twenty-sdk/application'; +import { type FunctionConfig } from 'twenty-sdk'; import type { ProcessResult } from './types'; import { WebhookHandler } from './webhook-handler'; @@ -10,7 +10,7 @@ export const main = async ( return handler.handle(params, headers); }; -export const config: ServerlessFunctionConfig = { +export const config: FunctionConfig = { universalIdentifier: '2d3ea303-667c-4bbe-9e3d-db6ffb9d6c74', name: 'receive-fireflies-notes', description: @@ -22,7 +22,8 @@ export const config: ServerlessFunctionConfig = { type: 'route', path: '/webhook/fireflies', httpMethod: 'POST', - isAuthRequired: true, + isAuthRequired: false, }, ], }; + diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts b/packages/twenty-apps/community/fireflies/src/twenty-crm-service.ts similarity index 81% rename from packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts rename to packages/twenty-apps/community/fireflies/src/twenty-crm-service.ts index 051ca4573c..de713ecdd7 100644 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/twenty-crm-service.ts +++ b/packages/twenty-apps/community/fireflies/src/twenty-crm-service.ts @@ -43,6 +43,20 @@ export class TwentyCrmService { return response.data?.meetings?.edges?.[0]?.node; } + async findMeetingByFirefliesId(meetingId: string): Promise { + const query = ` + query FindMeetingByFirefliesId($meetingId: String!) { + meetings(filter: { firefliesMeetingId: { eq: $meetingId } }) { + edges { node { id } } + } + } + `; + + const variables = { meetingId }; + const response = await this.gqlRequest(query, variables); + return response.data?.meetings?.edges?.[0]?.node; + } + async matchParticipantsToContacts( participants: FirefliesParticipant[], ): Promise<{ @@ -53,21 +67,18 @@ export class TwentyCrmService { return { matchedContacts: [], unmatchedParticipants: [] }; } - // Split participants into those with emails and those with names only const participantsWithEmails = participants.filter(p => p.email && p.email.trim()); const participantsNameOnly = participants.filter(p => !p.email || !p.email.trim()); let matchedContacts: Contact[] = []; let unmatchedParticipants: FirefliesParticipant[] = []; - // 1. Match by email first if (participantsWithEmails.length > 0) { const emailMatches = await this.matchByEmail(participantsWithEmails); matchedContacts.push(...emailMatches.matchedContacts); unmatchedParticipants.push(...emailMatches.unmatchedParticipants); } - // 2. For participants without emails, try name-based matching if (participantsNameOnly.length > 0) { const nameMatches = await this.matchByName(participantsNameOnly, matchedContacts); matchedContacts.push(...nameMatches.matchedContacts); @@ -126,7 +137,6 @@ export class TwentyCrmService { const matchedContacts: Contact[] = []; const unmatchedParticipants: FirefliesParticipant[] = []; - // Get set of already matched contact IDs to avoid duplicates const alreadyMatchedIds = new Set(alreadyMatchedContacts.map(c => c.id)); for (const participant of participants) { @@ -152,27 +162,37 @@ export class TwentyCrmService { const firstName = nameParts[0]; const lastName = nameParts.slice(1).join(' '); - // Try exact name match first - let query = ` - query FindPeopleByName($firstName: String!, $lastName: String) { - people(filter: { - and: [ - { name: { firstName: { eq: $firstName } } } - ${lastName ? '{ name: { lastName: { eq: $lastName } } }' : ''} - ] - }) { - edges { node { id emails { primaryEmail } name { firstName lastName } } } - } - } - `; + const hasLastName = Boolean(lastName); - let variables: any = { firstName }; - if (lastName) { - variables.lastName = lastName; - } + const query = hasLastName + ? ` + query FindPeopleByName($firstName: String!, $lastName: String!) { + people(filter: { + and: [ + { name: { firstName: { eq: $firstName } } } + { name: { lastName: { eq: $lastName } } } + ] + }) { + edges { node { id emails { primaryEmail } name { firstName lastName } } } + } + } + ` + : ` + query FindPeopleByName($firstName: String!) { + people(filter: { + name: { firstName: { eq: $firstName } } + }) { + edges { node { id emails { primaryEmail } name { firstName lastName } } } + } + } + `; + + const variables: Record = hasLastName + ? { firstName, lastName } + : { firstName }; try { - const response = await this.gqlRequest(query, variables); + const response = await this.gqlRequest<{ people: { edges: Array<{ node: { id: string; emails?: { primaryEmail?: string }; name?: { firstName?: string; lastName?: string } } }> } }>(query, variables); const people = response.data?.people?.edges; if (people && people.length > 0) { @@ -183,9 +203,8 @@ export class TwentyCrmService { }; } - // If no exact match and we have a last name, try fuzzy matching - if (lastName) { - query = ` + if (hasLastName) { + const fuzzyQuery = ` query FindPeopleByNameFuzzy($firstName: String!) { people(filter: { name: { firstName: { ilike: $firstName } } }) { edges { node { id emails { primaryEmail } name { firstName lastName } } } @@ -193,12 +212,11 @@ export class TwentyCrmService { } `; - const fuzzyResponse = await this.gqlRequest(query, { firstName: `%${firstName}%` }); + const fuzzyResponse = await this.gqlRequest<{ people: { edges: Array<{ node: { id: string; emails?: { primaryEmail?: string }; name?: { firstName?: string; lastName?: string } } }> } }>(fuzzyQuery, { firstName: `%${firstName}%` }); const fuzzyPeople = fuzzyResponse.data?.people?.edges; if (fuzzyPeople && fuzzyPeople.length > 0) { - // Find best match by checking if last name contains our target - const bestMatch = fuzzyPeople.find((edge: any) => { + const bestMatch = fuzzyPeople.find((edge) => { const personLastName = edge.node.name?.lastName || ''; return personLastName.toLowerCase().includes(lastName.toLowerCase()); }); @@ -215,7 +233,6 @@ export class TwentyCrmService { return null; } catch { - // Silently fail - don't break the entire process for a single contact lookup return null; } } @@ -225,17 +242,14 @@ export class TwentyCrmService { ): Promise { const newContactIds: string[] = []; - // Split participants into those with emails and those with names only const participantsWithEmails = participants.filter(p => p.email && p.email.trim()); const participantsNameOnly = participants.filter(p => !p.email || !p.email.trim()); - // Process participants with emails if (participantsWithEmails.length > 0) { const emailContactIds = await this.createContactsWithEmails(participantsWithEmails); newContactIds.push(...emailContactIds); } - // Process participants with names only if (participantsNameOnly.length > 0) { const nameContactIds = await this.createContactsNameOnly(participantsNameOnly); newContactIds.push(...nameContactIds); @@ -247,7 +261,6 @@ export class TwentyCrmService { private async createContactsWithEmails(participants: FirefliesParticipant[]): Promise { const newContactIds: string[] = []; - // Deduplicate participants by email to prevent duplicate contact creation const uniqueParticipants = participants.reduce((unique, participant) => { const existing = unique.find(p => p.email === participant.email); if (!existing) { @@ -276,7 +289,10 @@ export class TwentyCrmService { }; try { - const response = await this.gqlRequest(mutation, variables); + const response = await this.gqlRequest(mutation, variables, { + suppressErrorCodes: ['BAD_USER_INPUT'], + suppressErrorMessageIncludes: ['Duplicate Emails', 'duplicate entry'], + }); if (!response.data?.createPerson?.id) { throw new Error(`Failed to create contact for ${participant.email}`); } @@ -297,7 +313,6 @@ export class TwentyCrmService { private async createContactsNameOnly(participants: FirefliesParticipant[]): Promise { const newContactIds: string[] = []; - // Deduplicate participants by name to prevent duplicate contact creation const uniqueParticipants = participants.reduce((unique, participant) => { const existing = unique.find(p => p.name.toLowerCase().trim() === participant.name.toLowerCase().trim() @@ -311,7 +326,6 @@ export class TwentyCrmService { }, []); for (const participant of uniqueParticipants) { - // Check if we already have a contact with this exact name to avoid duplicates const existingContact = await this.findContactByName(participant.name); if (existingContact) { logger.warn(`Contact with name "${participant.name}" already exists. Skipping creation.`); @@ -330,8 +344,6 @@ export class TwentyCrmService { const variables = { data: { name: { firstName, lastName }, - // Note: We don't set emails for name-only participants - // This will create a contact without an email address }, }; @@ -346,7 +358,6 @@ export class TwentyCrmService { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.warn(`Failed to create contact for ${participant.name}: ${errorMessage}`); - // Continue processing other participants instead of failing completely continue; } } @@ -409,28 +420,7 @@ export class TwentyCrmService { }, }; - await this.gqlRequest(mutation, variables); - } - - async createMeetingTarget(meetingId: string, contactId: string): Promise { - const mutation = ` - mutation CreateMeetingTarget($data: NoteTargetCreateInput!) { - createNoteTarget(data: $data) { - id - meetingId - personId - } - } - `; - - const variables = { - data: { - meetingId, - personId: contactId, - }, - }; - - await this.gqlRequest(mutation, variables); + await this.gqlRequest<{ createNoteTarget: { id: string; noteId: string; personId: string } }>(mutation, variables); } async createMeeting(meetingData: MeetingCreateInput): Promise { @@ -442,7 +432,6 @@ export class TwentyCrmService { const variables = { data: meetingData }; - // Debug: log the variables being sent if (!this.isTestEnvironment) { logger.debug('createMeeting variables:', JSON.stringify(variables, null, 2)); } @@ -456,7 +445,8 @@ export class TwentyCrmService { private async gqlRequest( query: string, - variables?: Record + variables?: Record, + options?: { suppressErrorCodes?: string[]; suppressErrorMessageIncludes?: string[] } ): Promise> { const url = `${this.apiUrl}/graphql`; @@ -500,7 +490,16 @@ export class TwentyCrmService { return json; } catch (error) { - logger.error('GraphQL request error:', error); + const message = error instanceof Error ? error.message : String(error); + const suppressByCode = options?.suppressErrorCodes?.some((code) => + message.includes(code), + ); + const suppressByMessage = options?.suppressErrorMessageIncludes?.some((substring) => + message.includes(substring), + ); + if (!suppressByCode && !suppressByMessage) { + logger.error('GraphQL request error:', error); + } throw error; } } @@ -525,7 +524,15 @@ export class TwentyCrmService { return response.data.createMeeting.id; } - async findFailedMeetings(): Promise { + async findFailedMeetings(): Promise> { const query = ` query FindFailedMeetings { meetings(filter: { importStatus: { eq: "FAILED" } }) { @@ -544,8 +551,8 @@ export class TwentyCrmService { } `; - const response = await this.gqlRequest(query); - return response.data?.meetings?.edges?.map((edge: any) => edge.node) || []; + const response = await this.gqlRequest<{ meetings: { edges: Array<{ node: { id: string; name: string; firefliesMeetingId: string; importError: string; lastImportAttempt: string; importAttempts: number; createdAt: string } }> } }>(query); + return response.data?.meetings?.edges?.map((edge) => edge.node) || []; } async retryFailedMeeting(meetingId: string, updatedData: Partial): Promise { @@ -564,7 +571,7 @@ export class TwentyCrmService { } }; - await this.gqlRequest(mutation, variables); + await this.gqlRequest<{ updateMeeting: { id: string } }>(mutation, variables); } } diff --git a/packages/twenty-apps/community/fireflies/src/types.ts b/packages/twenty-apps/community/fireflies/src/types.ts new file mode 100644 index 0000000000..373e5d35dd --- /dev/null +++ b/packages/twenty-apps/community/fireflies/src/types.ts @@ -0,0 +1,235 @@ +// Fireflies API Types +export type FirefliesParticipant = { + email: string; + name: string; +}; + +export type FirefliesWebhookPayload = { + meetingId: string; + eventType: string; + clientReferenceId?: string; +}; + +// Transcript sentence from Fireflies API +export type FirefliesSentence = { + index: number; + speaker_name: string; + text: string; + start_time: string; + end_time: string; + ai_filters?: { + task?: boolean; + question?: boolean; + sentiment?: string; + }; +}; + +// Speaker analytics from Fireflies API (Business+) +export type FirefliesSpeakerAnalytics = { + speaker_id: string; + name: string; + duration: number; + word_count: number; + longest_monologue: number; + filler_words: number; + questions: number; + words_per_minute: number; +}; + +// Based on Fireflies GraphQL API transcript schema +// See: https://docs.fireflies.ai/graphql-api/query/transcript +export type FirefliesMeetingData = { + id: string; + title: string; + date: string; + duration: number; + participants: FirefliesParticipant[]; + organizer_email?: string; + // Full transcript (Pro+) + sentences?: FirefliesSentence[]; + summary: { + // Pro+ fields + action_items: string[]; + keywords?: string[]; + overview: string; + notes?: string; // Detailed AI-generated meeting notes + gist?: string; // 1-sentence summary + bullet_gist?: string; // Bullet point summary with emojis + short_summary?: string; // Single paragraph summary + short_overview?: string; // Brief overview + outline?: string; // Meeting outline with timestamps + shorthand_bullet?: string; + // Business+ fields + topics_discussed?: string[]; + meeting_type?: string; + transcript_chapters?: string[]; + }; + // Business+ analytics + analytics?: { + sentiments?: { + positive_pct: number; + negative_pct: number; + neutral_pct: number; + }; + categories?: { + questions: number; + tasks: number; + metrics: number; + date_times: number; + }; + speakers?: FirefliesSpeakerAnalytics[]; + }; + meeting_info?: { + summary_status?: string; + }; + // URLs + transcript_url: string; + audio_url?: string; // Pro+ + video_url?: string; // Business+ + meeting_link?: string; // All plans + summary_status?: string; +}; + +export type FirefliesTranscriptListItem = { + id: string; + title: string; + date?: string; + duration?: number; + organizer_email?: string; + participants?: string[]; + transcript_url?: string; + meeting_link?: string; + summary_status?: string; +}; + +export type FirefliesTranscriptListOptions = { + limit?: number; + skip?: number; + organizers?: string[]; + participants?: string[]; + hostEmail?: string; + participantEmail?: string; + userId?: string; + channelId?: string; + mine?: boolean; + fromDate?: number; + toDate?: number; + pageSize?: number; + maxRecords?: number; +}; + +// Configuration Types +export type SummaryStrategy = 'immediate_only' | 'immediate_with_retry' | 'delayed_polling' | 'basic_only'; + +export type SummaryFetchConfig = { + strategy: SummaryStrategy; + retryAttempts: number; + retryDelay: number; + pollInterval: number; + maxPolls: number; +}; + +export const FIREFLIES_PLANS = { + FREE: 'free', + PRO: 'pro', + BUSINESS: 'business', + ENTERPRISE: 'enterprise', +} as const; + +export type FirefliesPlan = typeof FIREFLIES_PLANS[keyof typeof FIREFLIES_PLANS]; + +export type WebhookConfig = { + secret: string; + apiUrl: string; +}; + +// Processing Result Types +export type ProcessResult = { + success: boolean; + meetingId?: string; + noteIds?: string[]; + newContacts?: string[]; + errors?: string[]; + debug?: string[]; + summaryReady?: boolean; + summaryPending?: boolean; + enhancementScheduled?: boolean; + actionItemsCount?: number; + sentimentAnalysis?: { + positive_pct: number; + negative_pct: number; + neutral_pct: number; + }; + meetingType?: string; + keyTopics?: string[]; +}; + +// Twenty CRM Types +export type GraphQLResponse = { + data: T; + errors?: Array<{ + message?: string; + extensions?: { code?: string } + }>; +}; + +export type IdNode = { id: string }; + +export type FindMeetingResponse = { + meetings: { edges: Array<{ node: IdNode }> }; +}; + +export type FindPeopleResponse = { + people: { edges: Array<{ node: { id: string; emails: { primaryEmail: string } } }> }; +}; + +export type CreatePersonResponse = { + createPerson: { id: string } +}; + +export type CreateNoteResponse = { + createNote: { id: string } +}; + +export type CreateMeetingResponse = { + createMeeting: { id: string } +}; + +export type Contact = { + id: string; + email: string; +}; + +// Twenty CRM Meeting custom field input +// Maps to fields defined in add-meeting-fields.ts +export type MeetingCreateInput = { + name: string; + noteId?: string | null; + // Basic fields (All plans) + meetingDate: string; + duration: number; + firefliesMeetingId: string; + organizerEmail?: string | null; + transcriptUrl?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; + meetingLink?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; + // Pro+ fields + transcript?: string | null; + overview?: string | null; + notes?: string | null; + keywords?: string | null; + audioUrl?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; + // Business+ fields + meetingType?: string | null; + topics?: string | null; + actionItemsCount: number; + positivePercent?: number | null; + negativePercent?: number | null; + neutralPercent?: number | null; + videoUrl?: { primaryLinkUrl: string; primaryLinkLabel: string } | null; + // Import tracking + importStatus?: 'SUCCESS' | 'PARTIAL' | 'FAILED' | 'PENDING' | 'RETRYING' | null; + importError?: string | null; + lastImportAttempt?: string | null; + importAttempts?: number | null; +}; + diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/utils.ts b/packages/twenty-apps/community/fireflies/src/utils.ts similarity index 71% rename from packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/utils.ts rename to packages/twenty-apps/community/fireflies/src/utils.ts index 969c655c9e..48cfa1d9df 100644 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/utils.ts +++ b/packages/twenty-apps/community/fireflies/src/utils.ts @@ -1,11 +1,11 @@ +import { FIREFLIES_PLANS, type FirefliesPlan, type SummaryFetchConfig, type SummaryStrategy } from './types'; + export const toBoolean = (value: string | undefined, defaultValue: boolean): boolean => { if (value === undefined) return defaultValue; const normalized = value.trim().toLowerCase(); return normalized === 'true' || normalized === '1' || normalized === 'yes'; }; -import type { SummaryFetchConfig, SummaryStrategy } from './types'; - export const getApiUrl = (): string => { return process.env.SERVER_URL || 'http://localhost:3000'; }; @@ -28,3 +28,14 @@ export const shouldAutoCreateContacts = (): boolean => { return toBoolean(process.env.AUTO_CREATE_CONTACTS, true); }; +export const getFirefliesPlan = (): FirefliesPlan => { + const plan = (process.env.FIREFLIES_PLAN || FIREFLIES_PLANS.FREE).toLowerCase(); + if (plan === FIREFLIES_PLANS.BUSINESS || plan === FIREFLIES_PLANS.ENTERPRISE) { + return plan as FirefliesPlan; + } + if (plan === FIREFLIES_PLANS.PRO) { + return FIREFLIES_PLANS.PRO; + } + return FIREFLIES_PLANS.FREE; +}; + diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts b/packages/twenty-apps/community/fireflies/src/webhook-handler.ts similarity index 60% rename from packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts rename to packages/twenty-apps/community/fireflies/src/webhook-handler.ts index 901d1bc5ce..2a8ae38591 100644 --- a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-handler.ts +++ b/packages/twenty-apps/community/fireflies/src/webhook-handler.ts @@ -1,9 +1,9 @@ import { FirefliesApiClient } from './fireflies-api-client'; import { MeetingFormatter } from './formatters'; -import { createLogger } from './logger'; +import { clearAllCapturedLogs, createLogger, getAllCapturedLogs } from './logger'; import { TwentyCrmService } from './twenty-crm-service'; import type { FirefliesWebhookPayload, ProcessResult } from './types'; -import { getApiUrl, getSummaryFetchConfig, shouldAutoCreateContacts } from './utils'; +import { getApiUrl, getFirefliesPlan, getSummaryFetchConfig, shouldAutoCreateContacts } from './utils'; import { getWebhookSecretFingerprint, isValidFirefliesPayload, @@ -12,17 +12,28 @@ import { declare const process: { env: Record }; -const logger = createLogger('fireflies'); - export class WebhookHandler { private debug: string[] = []; - private isTestEnvironment: boolean; + private logger: ReturnType; constructor() { - this.isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined; + this.logger = createLogger('fireflies'); + } + + private addDebugLogs(result: ProcessResult): ProcessResult { + if (process.env.CAPTURE_LOGS === 'true') { + const captured = getAllCapturedLogs(); + result.debug = [...this.debug, ...captured]; + } else { + delete result.debug; + } + return result; } async handle(params: unknown, headers?: Record): Promise { + clearAllCapturedLogs(); + this.debug = []; + const result: ProcessResult = { success: false, noteIds: [], @@ -31,19 +42,27 @@ export class WebhookHandler { }; try { - logger.debug('invoked'); - logger.debug(`apiUrl=${getApiUrl()}`); + this.logger.debug('invoked'); + this.logger.debug(`apiUrl=${getApiUrl()}`); + const paramKeys = + params && typeof params === 'object' + ? Object.keys(params as Record) + : []; + this.debug.push( + `paramsType=${typeof params}`, + `paramKeys=${paramKeys.join(',') || 'none'}` + ); // 0) Validate environment configuration const firefliesApiKey = process.env.FIREFLIES_API_KEY || ''; const twentyApiKey = process.env.TWENTY_API_KEY || ''; if (!firefliesApiKey) { - logger.critical('FIREFLIES_API_KEY not configured - this is a critical configuration error'); + this.logger.critical('FIREFLIES_API_KEY not configured - this is a critical configuration error'); throw new Error('FIREFLIES_API_KEY environment variable is required'); } if (!twentyApiKey) { - logger.critical('TWENTY_API_KEY not configured - this is a critical configuration error'); + this.logger.critical('TWENTY_API_KEY not configured - this is a critical configuration error'); throw new Error('TWENTY_API_KEY environment variable is required'); } @@ -51,28 +70,30 @@ export class WebhookHandler { const { payload, extractedHeaders } = this.parsePayload(params); const finalHeaders = extractedHeaders || headers; - logger.debug(`payload meetingId=${payload.meetingId} eventType="${payload.eventType}"`); + this.logger.debug(`payload meetingId=${payload.meetingId} eventType="${payload.eventType}"`); // 2) Verify webhook signature const webhookSecret = process.env.FIREFLIES_WEBHOOK_SECRET || ''; const secretFingerprint = getWebhookSecretFingerprint(webhookSecret); - logger.debug(`webhook secret fingerprint=${secretFingerprint}`); + this.logger.debug(`webhook secret fingerprint=${secretFingerprint}`); this.verifySignature(payload, finalHeaders, webhookSecret); - logger.debug('signature verification: ok'); + this.logger.debug('signature verification: ok'); // 3) Fetch meeting data from Fireflies const summaryConfig = getSummaryFetchConfig(); - logger.debug(`summary strategy: ${summaryConfig.strategy} (retryAttempts=${summaryConfig.retryAttempts}, retryDelay=${summaryConfig.retryDelay}ms)`); - logger.debug(`fetching meeting data from Fireflies API`); + const firefliesPlan = getFirefliesPlan(); + this.logger.debug(`summary strategy: ${summaryConfig.strategy} (retryAttempts=${summaryConfig.retryAttempts}, retryDelay=${summaryConfig.retryDelay}ms)`); + this.logger.debug(`fetching meeting data from Fireflies API`); const firefliesClient = new FirefliesApiClient(firefliesApiKey); const { data: meetingData, summaryReady } = await firefliesClient.fetchMeetingDataWithRetry( payload.meetingId, - summaryConfig + summaryConfig, + firefliesPlan ); - logger.debug(`meeting data fetched: title="${meetingData.title}" summaryReady=${summaryReady}`); + this.logger.debug(`meeting data fetched: title="${meetingData.title}" summaryReady=${summaryReady}`); result.summaryReady = summaryReady; result.summaryPending = !summaryReady; @@ -84,8 +105,7 @@ export class WebhookHandler { result.meetingType = meetingData.summary.meeting_type; if (meetingData.analytics?.sentiments) { - const sentiments = meetingData.analytics.sentiments; - result.sentimentScore = sentiments.positive_pct / 100; + result.sentimentAnalysis = meetingData.analytics.sentiments; } } @@ -95,29 +115,36 @@ export class WebhookHandler { getApiUrl() ); - const existingMeeting = await twentyService.findExistingMeeting(meetingData.title); - if (existingMeeting) { - logger.debug(`meeting already exists id=${existingMeeting.id}`); + const existingMeetingById = await twentyService.findMeetingByFirefliesId(meetingData.id); + if (existingMeetingById) { + this.logger.debug(`meeting already exists by firefliesMeetingId id=${existingMeetingById.id}`); result.success = true; - result.meetingId = existingMeeting.id; - result.debug = this.debug; - return result; + result.meetingId = existingMeetingById.id; + return this.addDebugLogs(result); } - logger.debug('no existing meeting found, proceeding'); + + const existingMeetingByTitle = await twentyService.findExistingMeeting(meetingData.title); + if (existingMeetingByTitle) { + this.logger.debug(`meeting already exists by title id=${existingMeetingByTitle.id}`); + result.success = true; + result.meetingId = existingMeetingByTitle.id; + return this.addDebugLogs(result); + } + this.logger.debug('no existing meeting found, proceeding'); // 5) Match participants to existing contacts - logger.debug(`total participants from API: ${meetingData.participants.length}`); + this.logger.debug(`total participants from API: ${meetingData.participants.length}`); meetingData.participants.forEach((p, idx) => { - logger.debug(`participant ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); + this.logger.debug(`participant ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); }); const { matchedContacts, unmatchedParticipants } = await twentyService.matchParticipantsToContacts( meetingData.participants ); - logger.debug(`matched=${matchedContacts.length} unmatched=${unmatchedParticipants.length}`); + this.logger.debug(`matched=${matchedContacts.length} unmatched=${unmatchedParticipants.length}`); unmatchedParticipants.forEach((p, idx) => { - logger.debug(`unmatched ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); + this.logger.debug(`unmatched ${idx + 1}: name="${p.name}" email="${p.email || 'none'}"`); }); // 6) Optionally create contacts @@ -126,7 +153,7 @@ export class WebhookHandler { ? await twentyService.createContactsForUnmatched(unmatchedParticipants) : []; result.newContacts = newContactIds; - logger.debug(`autoCreate=${autoCreate} createdContacts=${newContactIds.length}`); + this.logger.debug(`autoCreate=${autoCreate} createdContacts=${newContactIds.length}`); // 7) Create note first (so we can link to it from the meeting) const allContactIds = [...matchedContacts.map(({ id }) => id), ...newContactIds]; @@ -136,13 +163,13 @@ export class WebhookHandler { noteBody ); result.noteIds = [noteId]; - logger.debug(`created note id=${noteId}`); + this.logger.debug(`created note id=${noteId}`); // 8) Create meeting with direct relationship to the note const meetingInput = MeetingFormatter.toMeetingCreateInput(meetingData, noteId); - logger.debug(`meeting duration: ${meetingData.duration} min (raw from API) → ${meetingInput.duration} min (rounded)`); + this.logger.debug(`meeting duration: ${meetingData.duration} min (raw from API) → ${meetingInput.duration} min (rounded)`); result.meetingId = await twentyService.createMeeting(meetingInput); - logger.debug(`created meeting id=${result.meetingId} with noteId=${noteId}`); + this.logger.debug(`created meeting id=${result.meetingId} with noteId=${noteId}`); // 9) Link note to participants (Meeting link is handled via the relation field) await this.linkNoteToParticipants( @@ -150,20 +177,19 @@ export class WebhookHandler { noteId, allContactIds ); - logger.debug(`linked note to ${allContactIds.length} participants`); + this.logger.debug(`linked note to ${allContactIds.length} participants`); result.success = true; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; - logger.error(`error: ${message}`); + this.logger.error(`error: ${message}`); result.errors?.push(message); // Try to create a failed meeting record for tracking await this.createFailedMeetingRecord(params, message); } - result.debug = this.debug; - return result; + return this.addDebugLogs(result); } private parsePayload(params: unknown): { payload: FirefliesWebhookPayload; extractedHeaders?: Record } { @@ -172,16 +198,16 @@ export class WebhookHandler { // Handle string-encoded params if (typeof normalizedParams === 'string') { - logger.debug(`received params as string length=${normalizedParams.length}`); + this.logger.debug(`received params as string length=${normalizedParams.length}`); try { const parsed = JSON.parse(normalizedParams); normalizedParams = parsed; if (parsed && typeof parsed === 'object') { const parsedKeys = Object.keys(parsed as Record); - logger.debug(`parsed params keys: ${parsedKeys.join(',') || 'none'}`); + this.logger.debug(`parsed params keys: ${parsedKeys.join(',') || 'none'}`); } } catch (parseError) { - logger.error(`error parsing string params: ${String(parseError)}`); + this.logger.error(`error parsing string params: ${String(parseError)}`); throw new Error('Invalid or missing webhook payload'); } } @@ -197,14 +223,14 @@ export class WebhookHandler { if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) { extractedHeaders = wrapper.headers as Record; const headerKeys = Object.keys(extractedHeaders); - logger.debug(`extracted headers from wrapper: ${headerKeys.join(',')}`); + this.logger.debug(`extracted headers from wrapper: ${headerKeys.join(',')}`); } const wrapperKeys = ['params', 'payload', 'body', 'data', 'event']; for (const key of wrapperKeys) { const candidate = wrapper[key]; if (isValidFirefliesPayload(candidate)) { - logger.debug(`detected payload under wrapper key "${key}"`); + this.logger.debug(`detected payload under wrapper key "${key}"`); payload = candidate as FirefliesWebhookPayload; break; } @@ -212,7 +238,7 @@ export class WebhookHandler { } if (!payload) { - logger.error('error: Invalid or missing webhook payload'); + this.logger.error('error: Invalid or missing webhook payload'); throw new Error('Invalid or missing webhook payload'); } @@ -220,7 +246,7 @@ export class WebhookHandler { const payloadRecord = payload as Record; const payloadKeys = Object.keys(payloadRecord); if (payloadKeys.length > 0) { - logger.debug(`payload keys: ${payloadKeys.join(',')}`); + this.logger.debug(`payload keys: ${payloadKeys.join(',')}`); } return { payload, extractedHeaders }; @@ -235,8 +261,9 @@ export class WebhookHandler { const normalizedHeaders = headers || {}; const headerKeys = Object.keys(normalizedHeaders); if (headerKeys.length > 0) { - logger.debug(`header keys: ${headerKeys.join(',')}`); + this.logger.debug(`header keys: ${headerKeys.join(',')}`); } + this.debug.push(`headerKeys=${headerKeys.join(',') || 'none'}`); const headerSignature = Object.entries(normalizedHeaders).find( ([key]) => key.toLowerCase() === 'x-hub-signature', @@ -249,32 +276,40 @@ export class WebhookHandler { : undefined; if (payloadSignature) { - logger.debug('found signature inside payload'); + this.logger.debug('found signature inside payload'); } const signature = (typeof headerSignature === 'string' ? headerSignature : undefined) || payloadSignature; - const body = typeof normalizedHeaders['body'] === 'string' - ? normalizedHeaders['body'] - : JSON.stringify(payloadRecord); + const payloadForSignature = + payloadSignature && 'x-hub-signature' in payloadRecord + ? Object.fromEntries( + Object.entries(payloadRecord).filter( + ([key]) => key.toLowerCase() !== 'x-hub-signature', + ), + ) + : payloadRecord; + + const body = + typeof normalizedHeaders['body'] === 'string' + ? normalizedHeaders['body'] + : JSON.stringify(payloadForSignature); const signatureCheck = verifyWebhookSignature(body, signature, webhookSecret); if (!signatureCheck.isValid) { - logger.debug( + this.debug.push( + `signatureProvided=${Boolean(signature)}`, + `signatureMatched=${signatureCheck.isValid}`, + `webhookSecretFingerprint=${getWebhookSecretFingerprint(webhookSecret)}` + ); + this.logger.debug( `signature check failed. headerPresent=${Boolean( headerSignature, )} payloadSignaturePresent=${Boolean(payloadSignature)}`, ); - if (signature) { - logger.debug(`provided signature=${signature}`); - } else { - logger.debug('provided signature=undefined'); - } - logger.debug( - `computed signature=${signatureCheck.computedSignature ?? 'unavailable'}`, - ); - logger.critical('Invalid webhook signature - potential security threat detected in production'); + this.logger.debug(`provided signature present=${Boolean(signature)}`); + this.logger.critical('Invalid webhook signature - potential security threat detected in production'); throw new Error('Invalid webhook signature'); } } @@ -284,15 +319,13 @@ export class WebhookHandler { noteId: string, contactIds: string[] ): Promise { - // Create Note-Person links for each participant for (const contactId of contactIds) { try { await twentyService.createNoteTarget(noteId, contactId); - logger.debug(`linked note ${noteId} to person ${contactId}`); + this.logger.debug(`linked note ${noteId} to person ${contactId}`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; - logger.error(`failed to link note to person ${contactId}: ${message}`); - // Continue with other participants + this.logger.error(`failed to link note to person ${contactId}: ${message}`); } } } @@ -302,11 +335,10 @@ export class WebhookHandler { try { const twentyApiKey = process.env.TWENTY_API_KEY || ''; if (!twentyApiKey) { - logger.debug('Cannot create failed meeting record: TWENTY_API_KEY not configured'); + this.logger.debug('Cannot create failed meeting record: TWENTY_API_KEY not configured'); return; } - // Try to extract meeting ID and title from the params let meetingId = 'unknown'; let meetingTitle = 'Unknown Meeting'; @@ -314,7 +346,6 @@ export class WebhookHandler { if (payload?.meetingId) { meetingId = payload.meetingId; - // Try to get meeting title from Fireflies API if possible const firefliesApiKey = process.env.FIREFLIES_API_KEY || ''; if (firefliesApiKey) { try { @@ -322,7 +353,11 @@ export class WebhookHandler { const meetingData = await firefliesClient.fetchMeetingData(meetingId); meetingTitle = meetingData.title || meetingTitle; } catch (fetchError) { - logger.debug(`Could not fetch meeting title: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`); + this.logger.debug( + `Could not fetch meeting title: ${ + fetchError instanceof Error ? fetchError.message : 'Unknown error' + }`, + ); } } } @@ -335,10 +370,13 @@ export class WebhookHandler { ); const failedMeetingId = await twentyService.createFailedMeeting(failedMeetingData); - logger.debug(`Created failed meeting record: ${failedMeetingId}`); + this.logger.debug(`Created failed meeting record: ${failedMeetingId}`); } catch (recordError) { - // Don't throw here - we don't want to break the original error handling - logger.error(`Failed to create failed meeting record: ${recordError instanceof Error ? recordError.message : 'Unknown error'}`); + this.logger.error( + `Failed to create failed meeting record: ${ + recordError instanceof Error ? recordError.message : 'Unknown error' + }`, + ); } } } diff --git a/packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-validator.ts b/packages/twenty-apps/community/fireflies/src/webhook-validator.ts similarity index 100% rename from packages/twenty-apps/community/fireflies/serverlessFunctions/receive-fireflies-notes/src/webhook-validator.ts rename to packages/twenty-apps/community/fireflies/src/webhook-validator.ts diff --git a/packages/twenty-apps/community/fireflies/yarn.lock b/packages/twenty-apps/community/fireflies/yarn.lock index a891c93643..59f7e339b7 100644 --- a/packages/twenty-apps/community/fireflies/yarn.lock +++ b/packages/twenty-apps/community/fireflies/yarn.lock @@ -563,6 +563,301 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 10c0/6f8027a8cba7f8f7b736718b013f5a38c0476eea67034c94a0d3c375e2b114366ad4419e6a6fa7ffc2ef9c6d3e0435d76dd584a7a1cbac23962fda7650b579e3 + languageName: node + linkType: hard + +"@genql/cli@npm:^3.0.3": + version: 3.0.5 + resolution: "@genql/cli@npm:3.0.5" + dependencies: + "@graphql-tools/graphql-file-loader": "npm:^7.5.11" + "@graphql-tools/load": "npm:^7.8.6" + fs-extra: "npm:^10.1.0" + graphql: "npm:^16.6.0" + kleur: "npm:^4.1.5" + listr: "npm:^0.14.3" + lodash: "npm:^4.17.21" + mkdirp: "npm:^0.5.1" + native-fetch: "npm:^4.0.2" + prettier: "npm:^2.8.0" + qs: "npm:^6.11.0" + rimraf: "npm:^2.6.3" + undici: "npm:^5.18.0" + yargs: "npm:^15.3.1" + bin: + genql: dist/cli.js + checksum: 10c0/646d4da8986741a8f1b610bd8cfb5bc26cd9a856de671461e4ca87fb05443f37dd0edfb3b30ceaa34cd049077873afa07febfd4714dd2e0681fb07484e19a080 + languageName: node + linkType: hard + +"@graphql-tools/graphql-file-loader@npm:^7.5.11": + version: 7.5.17 + resolution: "@graphql-tools/graphql-file-loader@npm:7.5.17" + dependencies: + "@graphql-tools/import": "npm:6.7.18" + "@graphql-tools/utils": "npm:^9.2.1" + globby: "npm:^11.0.3" + tslib: "npm:^2.4.0" + unixify: "npm:^1.0.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/f737f14357731ad01da57755e1cf26ce375b475209d6ab7e4b656b56191a8979d2ab7dd5d1c54a1f11e04374f7a373fa95ea5ec6a001d0cef913ea208fadc65b + languageName: node + linkType: hard + +"@graphql-tools/import@npm:6.7.18": + version: 6.7.18 + resolution: "@graphql-tools/import@npm:6.7.18" + dependencies: + "@graphql-tools/utils": "npm:^9.2.1" + resolve-from: "npm:5.0.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/d33e37a1879dd43ac2851c9bac2f2873c58bb3687f1c06e159760dbb5e540ef074d688df70cc6dbd3ee5de48d437878df8f65a7c65ae80bd025bf98f2d615732 + languageName: node + linkType: hard + +"@graphql-tools/load@npm:^7.8.6": + version: 7.8.14 + resolution: "@graphql-tools/load@npm:7.8.14" + dependencies: + "@graphql-tools/schema": "npm:^9.0.18" + "@graphql-tools/utils": "npm:^9.2.1" + p-limit: "npm:3.1.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/1fa036ac596ccf48f350aa545d108c173184d9b53247f9e21c0d4ba96c5cba4a0b44281f9154f122e1e8e9d9d6eab93a5b2618ca8a797969bde1e75c1d45e786 + languageName: node + linkType: hard + +"@graphql-tools/merge@npm:^8.4.1": + version: 8.4.2 + resolution: "@graphql-tools/merge@npm:8.4.2" + dependencies: + "@graphql-tools/utils": "npm:^9.2.1" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/2df55222b48e010e683572f456cf265aabae5748c59f7c1260c66dec9794b7a076d3706f04da969b77f0a32c7ccb4551fee30125931d3fe9c98a8806aae9a3f4 + languageName: node + linkType: hard + +"@graphql-tools/schema@npm:^9.0.18": + version: 9.0.19 + resolution: "@graphql-tools/schema@npm:9.0.19" + dependencies: + "@graphql-tools/merge": "npm:^8.4.1" + "@graphql-tools/utils": "npm:^9.2.1" + tslib: "npm:^2.4.0" + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/42fd8ca8d3c8d60b583077c201980518482ff0cd5ed0c1f14bd9b835a2689ad41d02cbd3478f7d7dea7aec1227f7639fd5deb5e6360852a2e542b96b44bfb7a4 + languageName: node + linkType: hard + +"@graphql-tools/utils@npm:^9.2.1": + version: 9.2.1 + resolution: "@graphql-tools/utils@npm:9.2.1" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/37a7bd7e14d28ff1bacc007dca84bc6cef2d7d7af9a547b5dbe52fcd134afddd6d4a7b2148cfbaff5ddba91a868453d597da77bd0457fb0be15928f916901606 + languageName: node + linkType: hard + +"@graphql-typed-document-node/core@npm:^3.1.1": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/94e9d75c1f178bbae8d874f5a9361708a3350c8def7eaeb6920f2c820e82403b7d4f55b3735856d68e145e86c85cbfe2adc444fdc25519cd51f108697e99346c + languageName: node + linkType: hard + +"@inquirer/checkbox@npm:^2.5.0": + version: 2.5.0 + resolution: "@inquirer/checkbox@npm:2.5.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/679d17ffe3aef0825593f3bc8d193b6c37b860c6cf6e0e9a10d4e60cc254a2dfc5da4a982bf5b9b5147018e456fffcb0b0dadf93ee1914b9d600b0c814284e22 + languageName: node + linkType: hard + +"@inquirer/confirm@npm:^3.2.0": + version: 3.2.0 + resolution: "@inquirer/confirm@npm:3.2.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/a2cbfc8ae9c880bba4cce1993f5c399fb0d12741fdd574917c87fceb40ece62ffa60e35aaadf4e62d7c114f54008e45aee5d6d90497bb62d493996c02725d243 + languageName: node + linkType: hard + +"@inquirer/core@npm:^9.1.0": + version: 9.2.1 + resolution: "@inquirer/core@npm:9.2.1" + dependencies: + "@inquirer/figures": "npm:^1.0.6" + "@inquirer/type": "npm:^2.0.0" + "@types/mute-stream": "npm:^0.0.4" + "@types/node": "npm:^22.5.5" + "@types/wrap-ansi": "npm:^3.0.0" + ansi-escapes: "npm:^4.3.2" + cli-width: "npm:^4.1.0" + mute-stream: "npm:^1.0.0" + signal-exit: "npm:^4.1.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^6.2.0" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/11c14be77a9fa85831de799a585721b0a49ab2f3b7d8fd1780c48ea2b29229c6bdc94e7892419086d0f7734136c2ba87b6a32e0782571eae5bbd655b1afad453 + languageName: node + linkType: hard + +"@inquirer/editor@npm:^2.2.0": + version: 2.2.0 + resolution: "@inquirer/editor@npm:2.2.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + external-editor: "npm:^3.1.0" + checksum: 10c0/b8afc0790a7a5d82998bdfe469cbaa83b0cd0700be432cf95256c548e2a6a494997b5e93d65cbf94979c17b510758cf8494d85559f6b9508eb15d239a7f22aee + languageName: node + linkType: hard + +"@inquirer/expand@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/expand@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/f2030cb482a715e4d5153c19b3f0fd8bf47c16cdc16e1c669e90985386edf4f7b0f3b0e97e2990bb228878b93716228eb067d94fc557c25d3c5ee58747c0a995 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.5, @inquirer/figures@npm:^1.0.6": + version: 1.0.15 + resolution: "@inquirer/figures@npm:1.0.15" + checksum: 10c0/6e39a040d260ae234ae220180b7994ff852673e20be925f8aa95e78c7934d732b018cbb4d0ec39e600a410461bcb93dca771e7de23caa10630d255692e440f69 + languageName: node + linkType: hard + +"@inquirer/input@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/input@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/44c8cea38c9192f528cae556f38709135a00230132deab3b9bb9a925375fce0513fecf4e8c1df7c4319e1ed7aa31fb4dd2c4956c8bc9dd39af087aafff5b6f1f + languageName: node + linkType: hard + +"@inquirer/number@npm:^1.1.0": + version: 1.1.0 + resolution: "@inquirer/number@npm:1.1.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/db472dab57c951c4a083b2a749ce58262b1efd9889e7603de6e9c3f9af7d8dce8fbdfa3859f65402d3587470e0397a076e5fb4ed775db33310f17a42c9faeb20 + languageName: node + linkType: hard + +"@inquirer/password@npm:^2.2.0": + version: 2.2.0 + resolution: "@inquirer/password@npm:2.2.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + checksum: 10c0/fa4b335164b2c9c3304d29a7214ef93bac8d3da6788146603ea3d0485b8d811151e49bf66cb0dcc729a9dc21406c3a8c2718c5beec572a91d07026d22842c13f + languageName: node + linkType: hard + +"@inquirer/prompts@npm:^5.5.0": + version: 5.5.0 + resolution: "@inquirer/prompts@npm:5.5.0" + dependencies: + "@inquirer/checkbox": "npm:^2.5.0" + "@inquirer/confirm": "npm:^3.2.0" + "@inquirer/editor": "npm:^2.2.0" + "@inquirer/expand": "npm:^2.3.0" + "@inquirer/input": "npm:^2.3.0" + "@inquirer/number": "npm:^1.1.0" + "@inquirer/password": "npm:^2.2.0" + "@inquirer/rawlist": "npm:^2.3.0" + "@inquirer/search": "npm:^1.1.0" + "@inquirer/select": "npm:^2.5.0" + checksum: 10c0/2d62b50ca761b2bd2d5759f48c03758f1af0665ac602c26ae1ae257ac512cf5c27fd82cde108ee0c6371ec39187adc6f45637f31ca79adf5bf96579f23e77143 + languageName: node + linkType: hard + +"@inquirer/rawlist@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/rawlist@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/d49d5e12b7a54394c140b27c8d8748ba1ab855c67c01fa72b5a63810f12865df3bf4d5ae929f54fad77b5fc2f7431a332ae1e5fe4babb335380c28917002f364 + languageName: node + linkType: hard + +"@inquirer/search@npm:^1.1.0": + version: 1.1.0 + resolution: "@inquirer/search@npm:1.1.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/20d7e910266b9e3f0dc8eef8f3007f487e6149fa8421d293eaf7c11a1e35c3d82aa30af118b3a6e35eed1048a27d7d806f45722abb10005db5d099ea64b00b17 + languageName: node + linkType: hard + +"@inquirer/select@npm:^2.5.0": + version: 2.5.0 + resolution: "@inquirer/select@npm:2.5.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/280fa700187ff29da0ad4bf32aa11db776261584ddf5cc1ceac5caebb242a4ac0c5944af522a2579d78b6ec7d6e8b1b9f6564872101abd8dcc69929b4e33fc4c + languageName: node + linkType: hard + +"@inquirer/type@npm:^1.5.3": + version: 1.5.5 + resolution: "@inquirer/type@npm:1.5.5" + dependencies: + mute-stream: "npm:^1.0.0" + checksum: 10c0/4c41736c09ba9426b5a9e44993bdd54e8f532e791518802e33866f233a2a6126a25c1c82c19d1abbf1df627e57b1b957dd3f8318ea96073d8bfc32193943bcb3 + languageName: node + linkType: hard + +"@inquirer/type@npm:^2.0.0": + version: 2.0.0 + resolution: "@inquirer/type@npm:2.0.0" + dependencies: + mute-stream: "npm:^1.0.0" + checksum: 10c0/8c663d52beb2b89a896d3c3d5cc3d6d024fa149e565555bcb42fa640cbe23fba7ff2c51445342cef1fe6e46305e2d16c1590fa1d11ad0ddf93a67b655ef41f0a + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -880,6 +1175,33 @@ __metadata: languageName: node linkType: hard +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -909,6 +1231,20 @@ __metadata: languageName: node linkType: hard +"@samverschueren/stream-to-observable@npm:^0.3.0": + version: 0.3.1 + resolution: "@samverschueren/stream-to-observable@npm:0.3.1" + dependencies: + any-observable: "npm:^0.3.0" + peerDependenciesMeta: + rxjs: + optional: true + zen-observable: + optional: true + checksum: 10c0/0d874453f6bc2460d71783292291f52feb36c2a75314b1072a6ffe6206562f33e9d664a554348d565a6b54da9041d75070371052545bc329caaa52f64216987f + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1019,6 +1355,15 @@ __metadata: languageName: node linkType: hard +"@types/mute-stream@npm:^0.0.4": + version: 0.0.4 + resolution: "@types/mute-stream@npm:0.0.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/944730fd7b398c5078de3c3d4d0afeec8584283bc694da1803fdfca14149ea385e18b1b774326f1601baf53898ce6d121a952c51eb62d188ef6fcc41f725c0dc + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:^24.9.2": version: 24.9.2 resolution: "@types/node@npm:24.9.2" @@ -1028,6 +1373,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.5.5": + version: 22.19.1 + resolution: "@types/node@npm:22.19.1" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/6edd93aea86da740cb7872626839cd6f4a67a049d3a3a6639cb592c620ec591408a30989ab7410008d1a0b2d4985ce50f1e488e79c033e4476d3bec6833b0a2f + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -1035,6 +1389,13 @@ __metadata: languageName: node linkType: hard +"@types/wrap-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/wrap-ansi@npm:3.0.0" + checksum: 10c0/8d8f53363f360f38135301a06b596c295433ad01debd082078c33c6ed98b05a5c8fe8853a88265432126096084f4a135ec1564e3daad631b83296905509f90b3 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -1065,7 +1426,14 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": +"ansi-escapes@npm:^3.0.0": + version: 3.2.0 + resolution: "ansi-escapes@npm:3.2.0" + checksum: 10c0/084e1ce38139ad2406f18a8e7efe2b850ddd06ce3c00f633392d1ce67756dab44fe290e573d09ef3c9a0cb13c12881e0e35a8f77a017d39a0a4ab85ae2fae04f + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -1074,6 +1442,20 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^2.0.0": + version: 2.1.1 + resolution: "ansi-regex@npm:2.1.1" + checksum: 10c0/78cebaf50bce2cb96341a7230adf28d804611da3ce6bf338efa7b72f06cc6ff648e29f80cd95e582617ba58d5fdbec38abfeed3500a98bce8381a9daec7c548b + languageName: node + linkType: hard + +"ansi-regex@npm:^3.0.0": + version: 3.0.1 + resolution: "ansi-regex@npm:3.0.1" + checksum: 10c0/d108a7498b8568caf4a46eea4f1784ab4e0dfb2e3f3938c697dee21443d622d765c958f2b7e2b9f6b9e55e2e2af0584eaa9915d51782b89a841c28e744e7a167 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -1088,6 +1470,22 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^2.2.1": + version: 2.2.1 + resolution: "ansi-styles@npm:2.2.1" + checksum: 10c0/7c68aed4f1857389e7a12f85537ea5b40d832656babbf511cc7ecd9efc52889b9c3e5653a71a6aade783c3c5e0aa223ad4ff8e83c27ac8a666514e6c79068cab + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b + languageName: node + linkType: hard + "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -1111,6 +1509,13 @@ __metadata: languageName: node linkType: hard +"any-observable@npm:^0.3.0": + version: 0.3.0 + resolution: "any-observable@npm:0.3.0" + checksum: 10c0/104c2b79c2ac7e6c75b35f8fd62babf73015668f22bd25336c6f848350d91f9e7daf2fddbf1c1b76fe795e89fbc91b49f70a2aec5c69f1acf0562c344f36042b + languageName: node + linkType: hard + "anymatch@npm:^3.0.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -1130,6 +1535,13 @@ __metadata: languageName: node linkType: hard +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -1162,6 +1574,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.6.0": + version: 1.13.2 + resolution: "axios@npm:1.13.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b + languageName: node + linkType: hard + "babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -1355,6 +1778,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -1362,7 +1795,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 @@ -1383,6 +1816,30 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^1.0.0, chalk@npm:^1.1.3": + version: 1.1.3 + resolution: "chalk@npm:1.1.3" + dependencies: + ansi-styles: "npm:^2.2.1" + escape-string-regexp: "npm:^1.0.2" + has-ansi: "npm:^2.0.0" + strip-ansi: "npm:^3.0.0" + supports-color: "npm:^2.0.0" + checksum: 10c0/28c3e399ec286bb3a7111fd4225ebedb0d7b813aef38a37bca7c498d032459c265ef43404201d5fbb8d888d29090899c95335b4c0cda13e8b126ff15c541cef8 + languageName: node + linkType: hard + +"chalk@npm:^2.4.1": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: "npm:^3.2.1" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.3.0" + checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 + languageName: node + linkType: hard + "chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -1393,6 +1850,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.3.0": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -1400,6 +1864,22 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^0.7.0": + version: 0.7.0 + resolution: "chardet@npm:0.7.0" + checksum: 10c0/96e4731b9ec8050cbb56ab684e8c48d6c33f7826b755802d14e3ebfdc51c57afeece3ea39bc6b09acc359e4363525388b915e16640c1378053820f5e70d0f27d + languageName: node + linkType: hard + +"chokidar@npm:^4.0.0": + version: 4.0.3 + resolution: "chokidar@npm:4.0.3" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -1421,6 +1901,43 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^2.0.0, cli-cursor@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-cursor@npm:2.1.0" + dependencies: + restore-cursor: "npm:^2.0.0" + checksum: 10c0/09ee6d8b5b818d840bf80ec9561eaf696672197d3a02a7daee2def96d5f52ce6e0bbe7afca754ccf14f04830b5a1b4556273e983507d5029f95bba3016618eda + languageName: node + linkType: hard + +"cli-truncate@npm:^0.2.1": + version: 0.2.1 + resolution: "cli-truncate@npm:0.2.1" + dependencies: + slice-ansi: "npm:0.0.4" + string-width: "npm:^1.0.1" + checksum: 10c0/c6caa5e2b70d841c42f4a2270d6fc7129df915f8911e4afa90c79231ccc857cd819a2c90e0707fde04e51ce56b4d71646b843f6cbaff4f7cdcb3b91ed51f6e89 + languageName: node + linkType: hard + +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f + languageName: node + linkType: hard + +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10c0/35229b1bb48647e882104cac374c9a18e34bbf0bace0e2cf03000326b6ca3050d6b59545d91e17bfe3705f4a0e2988787aa5cde6331bf5cbbf0164732cef6492 + languageName: node + linkType: hard + "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -1439,6 +1956,13 @@ __metadata: languageName: node linkType: hard +"code-point-at@npm:^1.0.0": + version: 1.1.0 + resolution: "code-point-at@npm:1.1.0" + checksum: 10c0/33f6b234084e46e6e369b6f0b07949392651b4dde70fc6a592a8d3dafa08d5bb32e3981a02f31f6fc323a26bc03a4c063a9d56834848695bda7611c2417ea2e6 + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.0": version: 1.0.3 resolution: "collect-v8-coverage@npm:1.0.3" @@ -1446,6 +1970,15 @@ __metadata: languageName: node linkType: hard +"color-convert@npm:^1.9.0": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1455,6 +1988,13 @@ __metadata: languageName: node linkType: hard +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 + languageName: node + linkType: hard + "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -1471,6 +2011,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.0.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -1513,6 +2060,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^1.27.2": + version: 1.30.1 + resolution: "date-fns@npm:1.30.1" + checksum: 10c0/bad6ad7c15180121e15d61ad62a4a214c108d66f35b35f5eeb6ade837a3c29aa4444b9528a93a5374b95ba11231c142276351bf52f4d168676f9a1e17ce3726a + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4": version: 4.4.3 resolution: "debug@npm:4.4.3" @@ -1525,6 +2079,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10c0/85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 + languageName: node + linkType: hard + "dedent@npm:^1.0.0": version: 1.7.0 resolution: "dedent@npm:1.7.0" @@ -1565,13 +2126,29 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.3.1": +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"dotenv@npm:^16.4.0": version: 16.6.1 resolution: "dotenv@npm:16.6.1" checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc languageName: node linkType: hard +"dotenv@npm:^17.2.3": + version: 17.2.3 + resolution: "dotenv@npm:17.2.3" + checksum: 10c0/c884403209f713214a1b64d4d1defa4934c2aa5b0002f5a670ae298a51e3c3ad3ba79dfee2f8df49f01ae74290fcd9acdb1ab1d09c7bfb42b539036108bb2ba0 + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -1597,6 +2174,13 @@ __metadata: languageName: node linkType: hard +"elegant-spinner@npm:^1.0.1": + version: 1.0.1 + resolution: "elegant-spinner@npm:1.0.1" + checksum: 10c0/df607c83c20fc3ce56c514175dd5d1ee7f667da00cee13d04d32c70d55e76555091fa236689e691cf7dedba17b0020fec635e499cdde84dbea2ef8639314e5f8 + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -1781,6 +2365,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 + languageName: node + linkType: hard + "escape-string-regexp@npm:^2.0.0": version: 2.0.0 resolution: "escape-string-regexp@npm:2.0.0" @@ -1842,6 +2433,30 @@ __metadata: languageName: node linkType: hard +"external-editor@npm:^3.1.0": + version: 3.1.0 + resolution: "external-editor@npm:3.1.0" + dependencies: + chardet: "npm:^0.7.0" + iconv-lite: "npm:^0.4.24" + tmp: "npm:^0.0.33" + checksum: 10c0/c98f1ba3efdfa3c561db4447ff366a6adb5c1e2581462522c56a18bf90dfe4da382f9cd1feee3e330108c3595a854b218272539f311ba1b3298f841eb0fbf339 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -1849,6 +2464,15 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.6.0": + version: 1.19.1 + resolution: "fastq@npm:1.19.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/ebc6e50ac7048daaeb8e64522a1ea7a26e92b3cee5cd1c7f2316cdca81ba543aa40a136b53891446ea5c3a67ec215fbaca87ad405f102dd97012f62916905630 + languageName: node + linkType: hard + "fb-watchman@npm:^2.0.0": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" @@ -1870,6 +2494,25 @@ __metadata: languageName: node linkType: hard +"figures@npm:^1.7.0": + version: 1.7.0 + resolution: "figures@npm:1.7.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + object-assign: "npm:^4.1.0" + checksum: 10c0/a10942b0eec3372bf61822ab130d2bbecdf527d551b0b013fbe7175b7a0238ead644ee8930a1a3cb872fb9ab2ec27df30e303765a3b70b97852e2e9ee43bdff3 + languageName: node + linkType: hard + +"figures@npm:^2.0.0": + version: 2.0.0 + resolution: "figures@npm:2.0.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + checksum: 10c0/5dc5a75fec3e7e04ae65d6ce51d28b3e70d4656c51b06996b6fdb2cb5b542df512e3b3c04482f5193a964edddafa5521479ff948fa84e12ff556e53e094ab4ce + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -1896,11 +2539,11 @@ __metadata: "@types/jest": "npm:^29.5.5" "@types/node": "npm:^24.9.2" axios: "npm:^1.13.1" - dotenv: "npm:^16.3.1" + dotenv: "npm:^17.2.3" jest: "npm:^29.7.0" ts-jest: "npm:^29.1.1" tsx: "npm:^4.19.3" - twenty-sdk: "npm:0.0.3" + twenty-sdk: "npm:0.1.3" typescript: "npm:^5.9.3" languageName: unknown linkType: soft @@ -1938,6 +2581,28 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^10.1.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/5f579466e7109719d162a9249abbeffe7f426eb133ea486e020b89bc6d67a741134076bf439983f2eb79276ceaf6bd7b7c1e43c3fd67fe889863e69072fb0a5e + languageName: node + linkType: hard + +"fs-extra@npm:^11.2.0": + version: 11.3.2 + resolution: "fs-extra@npm:11.3.2" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/f5d629e1bb646d5dedb4d8b24c5aad3deb8cc1d5438979d6f237146cd10e113b49a949ae1b54212c2fbc98e2d0995f38009a9a1d0520f0287943335e65fe919b + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -1994,14 +2659,14 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde languageName: node linkType: hard -"get-intrinsic@npm:^1.2.6": +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" dependencies: @@ -2055,6 +2720,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:^5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + "glob@npm:^10.2.2": version: 10.5.0 resolution: "glob@npm:10.5.0" @@ -2085,6 +2759,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^11.0.3": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 + languageName: node + linkType: hard + "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -2092,13 +2780,29 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard +"graphql-sse@npm:^2.5.4": + version: 2.6.0 + resolution: "graphql-sse@npm:2.6.0" + peerDependencies: + graphql: ">=0.11 <=16" + checksum: 10c0/e05f0b5c8539d61e5ce34af8e0bb418c02bf922d6a7f9232a9abd53c77df5654684ca14f674ac771645c8fba9c37ce666c5d06f46eef54ea07e63653468065b0 + languageName: node + linkType: hard + +"graphql@npm:^16.6.0, graphql@npm:^16.8.1": + version: 16.12.0 + resolution: "graphql@npm:16.12.0" + checksum: 10c0/b6fffa4e8a4e4a9933ebe85e7470b346dbf49050c1a482fac5e03e4a1a7bed2ecd3a4c97e29f04457af929464bc5e4f2aac991090c2f320111eef26e902a5c75 + languageName: node + linkType: hard + "handlebars@npm:^4.7.8": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -2117,6 +2821,22 @@ __metadata: languageName: node linkType: hard +"has-ansi@npm:^2.0.0": + version: 2.0.0 + resolution: "has-ansi@npm:2.0.0" + dependencies: + ansi-regex: "npm:^2.0.0" + checksum: 10c0/f54e4887b9f8f3c4bfefd649c48825b3c093987c92c27880ee9898539e6f01aed261e82e73153c3f920fde0db5bf6ebd58deb498ed1debabcb4bc40113ccdf05 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -2190,6 +2910,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: 10c0/c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -2199,6 +2928,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + "import-local@npm:^3.0.2": version: 3.2.0 resolution: "import-local@npm:3.2.0" @@ -2218,6 +2954,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^3.0.0": + version: 3.2.0 + resolution: "indent-string@npm:3.2.0" + checksum: 10c0/91b6d61621d24944c5c4d365d6f1ff4a490264ccaf1162a602faa0d323e69231db2180ad4ccc092c2f49cf8888cdb3da7b73e904cc0fdeec40d0bfb41ceb9478 + languageName: node + linkType: hard + "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -2235,6 +2978,22 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^10.0.0": + version: 10.2.2 + resolution: "inquirer@npm:10.2.2" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/prompts": "npm:^5.5.0" + "@inquirer/type": "npm:^1.5.3" + "@types/mute-stream": "npm:^0.0.4" + ansi-escapes: "npm:^4.3.2" + mute-stream: "npm:^1.0.0" + run-async: "npm:^3.0.0" + rxjs: "npm:^7.8.1" + checksum: 10c0/09bcff887a968ce29d3a8cba749ef35b6531b7fb7bf5b28aaf3e10aebae07af2494dd3ec67ae6f1dabe76da1064cfe4514098d4c7658fcaf3fd480b2975d7163 + languageName: node + linkType: hard + "ip-address@npm:^10.0.1": version: 10.0.1 resolution: "ip-address@npm:10.0.1" @@ -2258,6 +3017,29 @@ __metadata: languageName: node linkType: hard +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^1.0.0": + version: 1.0.0 + resolution: "is-fullwidth-code-point@npm:1.0.0" + dependencies: + number-is-nan: "npm:^1.0.0" + checksum: 10c0/12acfcf16142f2d431bf6af25d68569d3198e81b9799b4ae41058247aafcc666b0127d64384ea28e67a746372611fcbe9b802f69175287aba466da3eddd5ba0f + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^2.0.0": + version: 2.0.0 + resolution: "is-fullwidth-code-point@npm:2.0.0" + checksum: 10c0/e58f3e4a601fc0500d8b2677e26e9fe0cd450980e66adb29d85b6addf7969731e38f8e43ed2ec868a09c101a55ac3d8b78902209269f38c5286bc98f5bc1b4d9 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -2272,6 +3054,15 @@ __metadata: languageName: node linkType: hard +"is-glob@npm:^4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -2279,6 +3070,29 @@ __metadata: languageName: node linkType: hard +"is-observable@npm:^1.1.0": + version: 1.1.0 + resolution: "is-observable@npm:1.1.0" + dependencies: + symbol-observable: "npm:^1.1.0" + checksum: 10c0/cf3166b0822f70ad06e7851e09430166ce658349d54aaa64c93a03320420b9285735821b23164bdce741ff83a86730ac3e53035ce4e2511ed843dbff4105bfa2 + languageName: node + linkType: hard + +"is-promise@npm:^2.1.0": + version: 2.2.2 + resolution: "is-promise@npm:2.2.2" + checksum: 10c0/2dba959812380e45b3df0fb12e7cb4d4528c989c7abb03ececb1d1fd6ab1cbfee956ca9daa587b9db1d8ac3c1e5738cf217bdb3dfd99df8c691be4c00ae09069 + languageName: node + linkType: hard + +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 10c0/b8ae7971e78d2e8488d15f804229c6eed7ed36a28f8807a1815938771f4adff0e705218b7dab968270433f67103e4fef98062a0beea55d64835f705ee72c7002 + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -2861,6 +3675,26 @@ __metadata: languageName: node linkType: hard +"jsonc-parser@npm:^3.2.0": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.2.0 + resolution: "jsonfile@npm:6.2.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/7f4f43b08d1869ded8a6822213d13ae3b99d651151d77efd1557ced0889c466296a7d9684e397bd126acf5eb2cfcb605808c3e681d0fdccd2fe5a04b47e76c0d + languageName: node + linkType: hard + "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -2868,6 +3702,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:^4.1.5": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -2882,6 +3723,60 @@ __metadata: languageName: node linkType: hard +"listr-silent-renderer@npm:^1.1.1": + version: 1.1.1 + resolution: "listr-silent-renderer@npm:1.1.1" + checksum: 10c0/a13e08ebf863516a757bce4887f05290070772113d89095e9f51a07cf0b11a43a7563a67ff3b287c752c08f6d781fdb2123b02957534e3e0675fb564f2a42e1b + languageName: node + linkType: hard + +"listr-update-renderer@npm:^0.5.0": + version: 0.5.0 + resolution: "listr-update-renderer@npm:0.5.0" + dependencies: + chalk: "npm:^1.1.3" + cli-truncate: "npm:^0.2.1" + elegant-spinner: "npm:^1.0.1" + figures: "npm:^1.7.0" + indent-string: "npm:^3.0.0" + log-symbols: "npm:^1.0.2" + log-update: "npm:^2.3.0" + strip-ansi: "npm:^3.0.1" + peerDependencies: + listr: ^0.14.2 + checksum: 10c0/8ade44bf3dc6146c8e0178000619439e8889792c4689b66be6ce82bd459f5fe462ecb34b05147fb206a8ad60e6d4e6f34c9f48038e18366f867fd972688b8edc + languageName: node + linkType: hard + +"listr-verbose-renderer@npm:^0.5.0": + version: 0.5.0 + resolution: "listr-verbose-renderer@npm:0.5.0" + dependencies: + chalk: "npm:^2.4.1" + cli-cursor: "npm:^2.1.0" + date-fns: "npm:^1.27.2" + figures: "npm:^2.0.0" + checksum: 10c0/041cd1e82da7054f27ae0a914e98b40d15faf9f950ef850578fc6241d3fff3c2d7158a4f6226006e566b4c47bf445be2d254dd1ce5c16569a3a5dcd575bec656 + languageName: node + linkType: hard + +"listr@npm:^0.14.3": + version: 0.14.3 + resolution: "listr@npm:0.14.3" + dependencies: + "@samverschueren/stream-to-observable": "npm:^0.3.0" + is-observable: "npm:^1.1.0" + is-promise: "npm:^2.1.0" + is-stream: "npm:^1.1.0" + listr-silent-renderer: "npm:^1.1.1" + listr-update-renderer: "npm:^0.5.0" + listr-verbose-renderer: "npm:^0.5.0" + p-map: "npm:^2.0.0" + rxjs: "npm:^6.3.3" + checksum: 10c0/753d518218c423f46bee8eeacccecadfd2e414ba9c0f602e7f85fe3f6fa18404dfab0812433aeda4683ee2548358488f597ac1a3d321196baec5d3149b200b10 + languageName: node + linkType: hard + "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -2891,6 +3786,27 @@ __metadata: languageName: node linkType: hard +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 + languageName: node + linkType: hard + +"lodash.capitalize@npm:^4.2.1": + version: 4.2.1 + resolution: "lodash.capitalize@npm:4.2.1" + checksum: 10c0/b289326497c2e24d6b8afa2af2ca4e068ef6ef007ade36bfb6f70af77ce10ea3f090eeee947d5fdcf2db4bcfa4703c8c10a5857a2b39e308bddfd1d11ad35970 + languageName: node + linkType: hard + +"lodash.kebabcase@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.kebabcase@npm:4.1.1" + checksum: 10c0/da5d8f41dbb5bc723d4bf9203d5096ca8da804d6aec3d2b56457156ba6c8d999ff448d347ebd97490da853cb36696ea4da09a431499f1ee8deb17b094ecf4e33 + languageName: node + linkType: hard + "lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -2898,6 +3814,40 @@ __metadata: languageName: node linkType: hard +"lodash.startcase@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.startcase@npm:4.4.0" + checksum: 10c0/bd82aa87a45de8080e1c5ee61128c7aee77bf7f1d86f4ff94f4a6d7438fc9e15e5f03374b947be577a93804c8ad6241f0251beaf1452bf716064eeb657b3a9f0 + languageName: node + linkType: hard + +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"log-symbols@npm:^1.0.2": + version: 1.0.2 + resolution: "log-symbols@npm:1.0.2" + dependencies: + chalk: "npm:^1.0.0" + checksum: 10c0/c64e1fe41d0d043840f8b592d043b8607a836b846506f525a53d99d578561f02f97b2cba1d2b3c30bae5311d64b308d5a392a9930d252b906a9042fc2877da7a + languageName: node + linkType: hard + +"log-update@npm:^2.3.0": + version: 2.3.0 + resolution: "log-update@npm:2.3.0" + dependencies: + ansi-escapes: "npm:^3.0.0" + cli-cursor: "npm:^2.0.0" + wrap-ansi: "npm:^3.0.1" + checksum: 10c0/9bf21b138801ab4770a2bfa735161cf005b869360eaf5003a84ba64ddc5f5c3ce7217f4f1fa79d9c1f510d792213b2c9800327228e94df05859d19b716215d90 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -2972,7 +3922,14 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4": +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -2998,6 +3955,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^1.0.0": + version: 1.2.0 + resolution: "mimic-fn@npm:1.2.0" + checksum: 10c0/ad55214aec6094c0af4c0beec1a13787556f8116ed88807cf3f05828500f21f93a9482326bcd5a077ae91e3e8795b4e76b5b4c8bb12237ff0e4043a365516cba + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -3023,7 +3987,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.5": +"minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -3106,6 +4070,17 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^0.5.1": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: "npm:^1.2.6" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/e2e2be789218807b58abced04e7b49851d9e46e88a2f9539242cc8a92c9b5c3a0b9bab360bd3014e02a140fc4fbc58e31176c408b493f8a2a6f4986bd7527b01 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -3113,6 +4088,22 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "mute-stream@npm:1.0.0" + checksum: 10c0/dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c + languageName: node + linkType: hard + +"native-fetch@npm:^4.0.2": + version: 4.0.2 + resolution: "native-fetch@npm:4.0.2" + peerDependencies: + undici: "*" + checksum: 10c0/e3b824721daaa628086d9dcd02e8eb12f0a6c5e13a1d182682bae238d80c9bbf3dfd6314a94692ebe20316aa354476804b4df148201d066b46fc552a5794cfab + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -3179,6 +4170,15 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^2.1.1": + version: 2.1.1 + resolution: "normalize-path@npm:2.1.1" + dependencies: + remove-trailing-separator: "npm:^1.0.1" + checksum: 10c0/db814326ff88057437233361b4c7e9cac7b54815b051b57f2d341ce89b1d8ec8cbd43e7fa95d7652b3b69ea8fcc294b89b8530d556a84d1bdace94229e1e9a8b + languageName: node + linkType: hard + "normalize-path@npm:^3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -3195,6 +4195,27 @@ __metadata: languageName: node linkType: hard +"number-is-nan@npm:^1.0.0": + version: 1.0.1 + resolution: "number-is-nan@npm:1.0.1" + checksum: 10c0/cb97149006acc5cd512c13c1838223abdf202e76ddfa059c5e8e7507aff2c3a78cd19057516885a2f6f5b576543dc4f7b6f3c997cc7df53ae26c260855466df5 + languageName: node + linkType: hard + +"object-assign@npm:^4.1.0": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -3204,6 +4225,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^2.0.0": + version: 2.0.1 + resolution: "onetime@npm:2.0.1" + dependencies: + mimic-fn: "npm:^1.0.0" + checksum: 10c0/b4e44a8c34e70e02251bfb578a6e26d6de6eedbed106cd78211d2fd64d28b6281d54924696554e4e966559644243753ac5df73c87f283b0927533d3315696215 + languageName: node + linkType: hard + "onetime@npm:^5.1.2": version: 5.1.2 resolution: "onetime@npm:5.1.2" @@ -3213,6 +4243,22 @@ __metadata: languageName: node linkType: hard +"os-tmpdir@npm:~1.0.2": + version: 1.0.2 + resolution: "os-tmpdir@npm:1.0.2" + checksum: 10c0/f438450224f8e2687605a8dd318f0db694b6293c5d835ae509a69e97c8de38b6994645337e5577f5001115470414638978cc49da1cdcc25106dad8738dc69990 + languageName: node + linkType: hard + +"p-limit@npm:3.1.0, p-limit@npm:^3.1.0": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -3222,15 +4268,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.1.0": - version: 3.1.0 - resolution: "p-limit@npm:3.1.0" - dependencies: - yocto-queue: "npm:^0.1.0" - checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a - languageName: node - linkType: hard - "p-locate@npm:^4.1.0": version: 4.1.0 resolution: "p-locate@npm:4.1.0" @@ -3240,6 +4277,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^2.0.0": + version: 2.1.0 + resolution: "p-map@npm:2.1.0" + checksum: 10c0/735dae87badd4737a2dd582b6d8f93e49a1b79eabbc9815a4d63a528d5e3523e978e127a21d784cccb637010e32103a40d2aaa3ab23ae60250b1a820ca752043 + languageName: node + linkType: hard + "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -3311,6 +4355,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + "picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -3348,6 +4399,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^2.8.0": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + languageName: node + linkType: hard + "pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -3400,6 +4460,22 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.11.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + "react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -3407,6 +4483,20 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.1.2 + resolution: "readdirp@npm:4.1.2" + checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62 + languageName: node + linkType: hard + +"remove-trailing-separator@npm:^1.0.1": + version: 1.1.0 + resolution: "remove-trailing-separator@npm:1.1.0" + checksum: 10c0/3568f9f8f5af3737b4aee9e6e1e8ec4be65a92da9cb27f989e0893714d50aa95ed2ff02d40d1fa35e1b1a234dc9c2437050ef356704a3999feaca6667d9e9bfc + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -3414,6 +4504,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10c0/db91467d9ead311b4111cbd73a4e67fa7820daed2989a32f7023785a2659008c6d119752d9c4ac011ae07e537eb86523adff99804c5fdb39cd3a017f9b401bb6 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -3423,7 +4520,7 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^5.0.0": +"resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 @@ -3470,6 +4567,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^2.0.0": + version: 2.0.0 + resolution: "restore-cursor@npm:2.0.0" + dependencies: + onetime: "npm:^2.0.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/f5b335bee06f440445e976a7031a3ef53691f9b7c4a9d42a469a0edaf8a5508158a0d561ff2b26a1f4f38783bcca2c0e5c3a44f927326f6694d5b44d7a4993e6 + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -3477,7 +4584,59 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": +"reusify@npm:^1.0.4": + version: 1.1.0 + resolution: "reusify@npm:1.1.0" + checksum: 10c0/4eff0d4a5f9383566c7d7ec437b671cc51b25963bd61bf127c3f3d3f68e44a026d99b8d2f1ad344afff8d278a8fe70a8ea092650a716d22287e8bef7126bb2fa + languageName: node + linkType: hard + +"rimraf@npm:^2.6.3": + version: 2.7.1 + resolution: "rimraf@npm:2.7.1" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: ./bin.js + checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40 + languageName: node + linkType: hard + +"run-async@npm:^3.0.0": + version: 3.0.0 + resolution: "run-async@npm:3.0.0" + checksum: 10c0/b18b562ae37c3020083dcaae29642e4cc360c824fbfb6b7d50d809a9d5227bb986152d09310255842c8dce40526e82ca768f02f00806c91ba92a8dfa6159cb85 + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"rxjs@npm:^6.3.3": + version: 6.6.7 + resolution: "rxjs@npm:6.6.7" + dependencies: + tslib: "npm:^1.9.0" + checksum: 10c0/e556a13a9aa89395e5c9d825eabcfa325568d9c9990af720f3f29f04a888a3b854f25845c2b55875d875381abcae2d8100af9cacdc57576e7ed6be030a01d2fe + languageName: node + linkType: hard + +"rxjs@npm:^7.8.1": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 @@ -3502,6 +4661,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -3518,14 +4684,62 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 @@ -3546,6 +4760,13 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:0.0.4": + version: 0.0.4 + resolution: "slice-ansi@npm:0.0.4" + checksum: 10c0/997d4cc73e34aa8c0f60bdb71701b16c062cc4acd7a95e3b10e8c05d790eb5e735d9b470270dc6f443b1ba21492db7ceb849d5c93011d1256061bf7ed7216c7a + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -3637,6 +4858,27 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^1.0.1": + version: 1.0.2 + resolution: "string-width@npm:1.0.2" + dependencies: + code-point-at: "npm:^1.0.0" + is-fullwidth-code-point: "npm:^1.0.0" + strip-ansi: "npm:^3.0.0" + checksum: 10c0/c558438baed23a9ab9370bb6a939acbdb2b2ffc517838d651aad0f5b2b674fb85d460d9b1d0b6a4c210dffd09e3235222d89a5bd4c0c1587f78b2bb7bc00c65e + languageName: node + linkType: hard + +"string-width@npm:^2.1.1": + version: 2.1.1 + resolution: "string-width@npm:2.1.1" + dependencies: + is-fullwidth-code-point: "npm:^2.0.0" + strip-ansi: "npm:^4.0.0" + checksum: 10c0/e5f2b169fcf8a4257a399f95d069522f056e92ec97dbdcb9b0cdf14d688b7ca0b1b1439a1c7b9773cd79446cbafd582727279d6bfdd9f8edd306ea5e90e5b610 + languageName: node + linkType: hard + "string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" @@ -3657,6 +4899,24 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": + version: 3.0.1 + resolution: "strip-ansi@npm:3.0.1" + dependencies: + ansi-regex: "npm:^2.0.0" + checksum: 10c0/f6e7fbe8e700105dccf7102eae20e4f03477537c74b286fd22cfc970f139002ed6f0d9c10d0e21aa9ed9245e0fa3c9275930e8795c5b947da136e4ecb644a70f + languageName: node + linkType: hard + +"strip-ansi@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-ansi@npm:4.0.0" + dependencies: + ansi-regex: "npm:^3.0.0" + checksum: 10c0/d75d9681e0637ea316ddbd7d4d3be010b1895a17e885155e0ed6a39755ae0fd7ef46e14b22162e66a62db122d3a98ab7917794e255532ab461bb0a04feb03e7d + languageName: node + linkType: hard + "strip-ansi@npm:^7.0.1": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" @@ -3687,6 +4947,22 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^2.0.0": + version: 2.0.0 + resolution: "supports-color@npm:2.0.0" + checksum: 10c0/570e0b63be36cccdd25186350a6cb2eaad332a95ff162fa06d9499982315f2fe4217e69dd98e862fbcd9c81eaff300a825a1fe7bf5cc752e5b84dfed042b0dda + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -3712,6 +4988,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^1.1.0": + version: 1.2.0 + resolution: "symbol-observable@npm:1.2.0" + checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.5.2 resolution: "tar@npm:7.5.2" @@ -3746,6 +5029,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.0.33": + version: 0.0.33 + resolution: "tmp@npm:0.0.33" + dependencies: + os-tmpdir: "npm:~1.0.2" + checksum: 10c0/69863947b8c29cabad43fe0ce65cec5bb4b481d15d4b4b21e036b060b3edbf3bc7a5541de1bacb437bb3f7c4538f669752627fdf9b4aaf034cebd172ba373408 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -3802,6 +5094,20 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.9.0": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + +"tslib@npm:^2.1.0, tslib@npm:^2.4.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + "tsx@npm:^4.19.3": version: 4.20.6 resolution: "tsx@npm:4.20.6" @@ -3818,10 +5124,30 @@ __metadata: languageName: node linkType: hard -"twenty-sdk@npm:0.0.3": - version: 0.0.3 - resolution: "twenty-sdk@npm:0.0.3" - checksum: 10c0/0a3c85c27edb22fb50f7eb0da4f9770e85729fce05e9e0118ad0cdfc36e42425c93340a6cd1c276daf30aeeaa612db0cd905831c0a8287a31bff3da5be9b0562 +"twenty-sdk@npm:0.1.3": + version: 0.1.3 + resolution: "twenty-sdk@npm:0.1.3" + dependencies: + "@genql/cli": "npm:^3.0.3" + axios: "npm:^1.6.0" + chalk: "npm:^5.3.0" + chokidar: "npm:^4.0.0" + commander: "npm:^12.0.0" + dotenv: "npm:^16.4.0" + fs-extra: "npm:^11.2.0" + graphql: "npm:^16.8.1" + graphql-sse: "npm:^2.5.4" + inquirer: "npm:^10.0.0" + jsonc-parser: "npm:^3.2.0" + lodash.camelcase: "npm:^4.3.0" + lodash.capitalize: "npm:^4.2.1" + lodash.kebabcase: "npm:^4.1.1" + lodash.startcase: "npm:^4.4.0" + typescript: "npm:^5.9.2" + uuid: "npm:^13.0.0" + bin: + twenty: dist/cli.cjs + checksum: 10c0/bb7a43331c61d34e250c7850e1746e6aac09fa8c552e0e19598b8ad44f56e3a9d51dc985d07da4c205ed179f49ba1ede52c6a9ed8be002689f70b571d1ac9c6d languageName: node linkType: hard @@ -3846,7 +5172,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.9.3": +"typescript@npm:^5.9.2, typescript@npm:^5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -3856,7 +5182,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.9.2#optional!builtin, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -3875,6 +5201,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 + languageName: node + linkType: hard + "undici-types@npm:~7.16.0": version: 7.16.0 resolution: "undici-types@npm:7.16.0" @@ -3882,6 +5215,15 @@ __metadata: languageName: node linkType: hard +"undici@npm:^5.18.0": + version: 5.29.0 + resolution: "undici@npm:5.29.0" + dependencies: + "@fastify/busboy": "npm:^2.0.0" + checksum: 10c0/e4e4d631ca54ee0ad82d2e90e7798fa00a106e27e6c880687e445cc2f13b4bc87c5eba2a88c266c3eecffb18f26e227b778412da74a23acc374fca7caccec49b + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -3900,6 +5242,22 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10c0/73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a + languageName: node + linkType: hard + +"unixify@npm:^1.0.0": + version: 1.0.0 + resolution: "unixify@npm:1.0.0" + dependencies: + normalize-path: "npm:^2.1.1" + checksum: 10c0/8b89100619ebde9f0ab4024a4d402316fb7b1d4853723410fc828944e8d3d01480f210cddf94d9a1699559f8180d861eb6323da8011b7bcc1bbaf6a11a5b1f1e + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.4": version: 1.1.4 resolution: "update-browserslist-db@npm:1.1.4" @@ -3914,6 +5272,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^13.0.0": + version: 13.0.0 + resolution: "uuid@npm:13.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67 + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.3.0 resolution: "v8-to-istanbul@npm:9.3.0" @@ -3925,6 +5292,13 @@ __metadata: languageName: node linkType: hard +"value-or-promise@npm:^1.0.12": + version: 1.0.12 + resolution: "value-or-promise@npm:1.0.12" + checksum: 10c0/b75657b74e4d17552bd88e0c2857020fbab34a4d091dc058db18c470e7da0336067e72c130b3358e3321ac0a6ff11c0b92b67a382318a3705ad5d57de7ff3262 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -3934,6 +5308,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10c0/087038e7992649eaffa6c7a4f3158d5b53b14cf5b6c1f0e043dccfacb1ba179d12f17545d5b85ebd94a42ce280a6fe65d0cbcab70f4fc6daad1dfae85e0e6a3e + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -3974,6 +5355,27 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^3.0.1": + version: 3.0.1 + resolution: "wrap-ansi@npm:3.0.1" + dependencies: + string-width: "npm:^2.1.1" + strip-ansi: "npm:^4.0.0" + checksum: 10c0/ad6fed8f242c26755badaf452da154122d0d862f8b7aab56e758466857f230efafdc5fbffca026650b947ac3fc0eb563df5c05b9e2190a52a4a68f4eef3d4555 + languageName: node + linkType: hard + +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c + languageName: node + linkType: hard + "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -4002,6 +5404,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10c0/308a2efd7cc296ab2c0f3b9284fd4827be01cfeb647b3ba18230e3a416eb1bc887ac050de9f8c4fd9e7856b2e8246e05d190b53c96c5ad8d8cb56dffb6f81024 + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -4030,6 +5439,16 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10c0/25df918833592a83f52e7e4f91ba7d7bfaa2b891ebf7fe901923c2ee797534f23a176913ff6ff7ebbc1cc1725a044cc6a6539fed8bfd4e13b5b16376875f9499 + languageName: node + linkType: hard + "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -4037,6 +5456,25 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^15.3.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10c0/f1ca680c974333a5822732825cca7e95306c5a1e7750eb7b973ce6dc4f97a6b0a8837203c8b194f461969bfe1fb1176d1d423036635285f6010b392fa498ab2d + languageName: node + linkType: hard + "yargs@npm:^17.3.1": version: 17.7.2 resolution: "yargs@npm:17.7.2" @@ -4058,3 +5496,10 @@ __metadata: checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f languageName: node linkType: hard + +"yoctocolors-cjs@npm:^2.1.2": + version: 2.1.3 + resolution: "yoctocolors-cjs@npm:2.1.3" + checksum: 10c0/584168ef98eb5d913473a4858dce128803c4a6cd87c0f09e954fa01126a59a33ab9e513b633ad9ab953786ed16efdd8c8700097a51635aafaeed3fef7712fa79 + languageName: node + linkType: hard