Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390b873a2a | ||
|
|
c16ff61cbc |
@@ -12,7 +12,6 @@ This directory contains Twenty's development guidelines and best practices in th
|
||||
### Core Guidelines
|
||||
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
|
||||
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
|
||||
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
|
||||
|
||||
### Code Quality
|
||||
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
|
||||
@@ -41,7 +40,7 @@ You can manually reference any rule using the `@ruleName` syntax:
|
||||
- `@testing-guidelines` - Get testing recommendations
|
||||
|
||||
### Rule Types Used
|
||||
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
|
||||
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
|
||||
- **Auto Attached** - Loaded when matching file patterns are referenced
|
||||
- **Agent Requested** - Available for AI to include when relevant
|
||||
- **Manual** - Only included when explicitly mentioned
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
---
|
||||
description: Process and guidelines for creating release changelogs for Twenty CRM
|
||||
globs: ["**/releases/*.mdx", "**/releases/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
# Twenty Release Changelog Process
|
||||
|
||||
Complete guide for creating release changelogs, including codebase research, file structure, and content guidelines.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, gather the following information:
|
||||
|
||||
### Required Information
|
||||
|
||||
**Version Number**: `{VERSION}` (e.g., 1.9.0, 2.0.0, 2.1.0)
|
||||
|
||||
**Release Date**: Use today's date in format: YYYY-MM-DD
|
||||
|
||||
### Changes/Features to Document
|
||||
|
||||
List the features and changes to include in this release:
|
||||
|
||||
1. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area (workflow, UI, backend, etc.): ______________________________
|
||||
|
||||
2. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area: ______________________________
|
||||
|
||||
3. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area: ______________________________
|
||||
|
||||
## Codebase Research Guide
|
||||
|
||||
If feature descriptions are not provided or need enhancement, research the codebase:
|
||||
|
||||
### Where to Look
|
||||
|
||||
**For Workflow Features:**
|
||||
- Frontend: `packages/twenty-front/src/modules/workflow/`
|
||||
- Backend: `packages/twenty-server/src/modules/workflow/`
|
||||
- Components: `packages/twenty-front/src/modules/workflow/components/`
|
||||
|
||||
**For UI/UX Changes:**
|
||||
- Components: `packages/twenty-front/src/modules/ui/`
|
||||
- Layout: `packages/twenty-front/src/modules/layout/`
|
||||
- Design system: `packages/twenty-ui/src/`
|
||||
|
||||
**For Backend/API Features:**
|
||||
- Server modules: `packages/twenty-server/src/modules/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
- Services: Look for `*.service.ts` files
|
||||
|
||||
**For Database/ORM Changes:**
|
||||
- Migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
|
||||
### Research Commands
|
||||
|
||||
```bash
|
||||
# Find recent merged PRs (adjust date as needed)
|
||||
gh pr list --search "merged:>2025-10-01" --limit 50 --state merged
|
||||
|
||||
# View recent commits
|
||||
git log --since="2 weeks ago" --oneline --no-merges
|
||||
|
||||
# View commits between releases (replace with actual release tags)
|
||||
git log v1.7.0..v1.8.0 --oneline
|
||||
|
||||
# Search for specific feature keywords in code
|
||||
grep -r "iterator" packages/twenty-front/src/modules/workflow/
|
||||
grep -r "bulk select" packages/twenty-front/src/modules/workflow/
|
||||
|
||||
# Find recent changes in specific directory
|
||||
git log --since="2 weeks ago" --oneline -- packages/twenty-front/src/modules/workflow/
|
||||
```
|
||||
|
||||
### Using Codebase Search
|
||||
|
||||
Use the AI codebase search to find:
|
||||
- "How does the workflow iterator node work?"
|
||||
- "Where is bulk select implemented for workflows?"
|
||||
- "What changes were made to the search node limit?"
|
||||
|
||||
## Step-by-Step Process
|
||||
|
||||
### 1. Setup Git Branch
|
||||
|
||||
**IMPORTANT**: Always start from an up-to-date main branch to avoid merge conflicts and ensure the changelog is based on the latest code.
|
||||
|
||||
```bash
|
||||
cd /Users/thomascolasdesfrancs/code/twenty
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b {VERSION}
|
||||
```
|
||||
|
||||
Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
|
||||
|
||||
⚠️ **Do this first** before making any file changes. This ensures your branch is based on the latest main.
|
||||
|
||||
### 2. Create File Structure
|
||||
|
||||
**Create changelog file:**
|
||||
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
|
||||
|
||||
**Create image folder:**
|
||||
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
|
||||
|
||||
```bash
|
||||
# Create the image folder
|
||||
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
### 3. Move Illustration Files
|
||||
|
||||
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
|
||||
|
||||
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
|
||||
**Naming Convention:** `{VERSION}-descriptive-name.png`
|
||||
|
||||
Examples:
|
||||
- `1.9.0-feature-name.png`
|
||||
- `1.9.0-another-feature.png`
|
||||
|
||||
```bash
|
||||
# Move and rename files
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
```
|
||||
|
||||
### 4. Research Features (if needed)
|
||||
|
||||
If descriptions are not provided:
|
||||
1. Use the research commands above to find recent PRs and commits
|
||||
2. Search the codebase for feature-related code
|
||||
3. Read PR descriptions for context
|
||||
4. Check component comments and documentation
|
||||
|
||||
### 5. Write Changelog Content
|
||||
|
||||
Create the MDX file with this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
release: {VERSION}
|
||||
Date: {YYYY-MM-DD}
|
||||
---
|
||||
|
||||
# Feature 1 Name
|
||||
|
||||
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
|
||||
|
||||

|
||||
|
||||
# Feature 2 Name
|
||||
|
||||
Another short description of the second feature.
|
||||
|
||||

|
||||
|
||||
# Feature 3 Name
|
||||
|
||||
Description of the third feature.
|
||||
|
||||

|
||||
```
|
||||
|
||||
**Style Guidelines:**
|
||||
- Use H1 (`#`) for feature names
|
||||
- Keep descriptions to 1-2 sentences
|
||||
- Focus on user benefits, not technical implementation
|
||||
- Use active voice
|
||||
- Start with what the user can now do
|
||||
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
|
||||
|
||||
**Reference Previous Changelogs:**
|
||||
- Check `packages/twenty-website/src/content/releases/` for examples
|
||||
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
|
||||
|
||||
### 6. Review
|
||||
|
||||
Open the changelog file for review:
|
||||
```bash
|
||||
# Open in Cursor
|
||||
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
|
||||
# Open image folder to verify illustrations
|
||||
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
Review checklist:
|
||||
- [ ] Version number is correct in frontmatter
|
||||
- [ ] Date is today's date
|
||||
- [ ] All features are documented
|
||||
- [ ] Image paths are correct
|
||||
- [ ] Image files exist in the folder
|
||||
- [ ] Descriptions are clear and user-focused
|
||||
- [ ] Spelling and grammar are correct
|
||||
|
||||
### 7. Present Changelog for User Approval
|
||||
|
||||
**IMPORTANT**: Before committing and creating the PR, always show the complete changelog content to the user and wait for explicit approval.
|
||||
|
||||
**What to show:**
|
||||
1. Display the full MDX content of the changelog file
|
||||
2. Confirm that illustration files were moved to the correct location
|
||||
3. List the image file names and paths
|
||||
|
||||
**What to say:**
|
||||
```
|
||||
I've created the changelog for version {VERSION}. Here's the content for your review:
|
||||
|
||||
[Show full MDX content]
|
||||
|
||||
Images moved to:
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
|
||||
|
||||
Please review the content. Once you approve, I'll commit the changes and create the pull request.
|
||||
```
|
||||
|
||||
**Wait for user approval before proceeding to step 8.**
|
||||
|
||||
Possible user responses:
|
||||
- "Looks good" / "Approve" / "Create the PR" → Proceed to step 8
|
||||
- Requests changes → Make the requested edits, then show content again
|
||||
- Asks questions → Answer them, then wait for approval
|
||||
|
||||
### 8. Commit Changes
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
git status
|
||||
|
||||
# Add files
|
||||
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
|
||||
# Commit
|
||||
git commit -m "Add {VERSION} release changelog"
|
||||
|
||||
# Push branch
|
||||
git push -u origin {VERSION}
|
||||
```
|
||||
|
||||
### 9. Create Pull Request
|
||||
|
||||
```bash
|
||||
# Create PR using GitHub CLI
|
||||
gh pr create \
|
||||
--title "Release {VERSION}" \
|
||||
--body "## Release {VERSION}
|
||||
|
||||
This release includes:
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
|
||||
Release date: {DATE}" \
|
||||
--base main \
|
||||
--head {VERSION}
|
||||
```
|
||||
|
||||
Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
### Changelog Files
|
||||
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
|
||||
- **Convention**: One file per complete version
|
||||
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
|
||||
- **Location**: `packages/twenty-website/src/content/releases/`
|
||||
|
||||
### Image Folders
|
||||
- **Format**: `{MAJOR}.{MINOR}/`
|
||||
- **Convention**: One folder per minor version (shared across patches)
|
||||
- **Examples**: `1.6/`, `1.7/`, `2.0/`
|
||||
- **Location**: `packages/twenty-website/public/images/releases/`
|
||||
|
||||
### Image Files
|
||||
- **Format**: `{VERSION}-descriptive-name.png`
|
||||
- **Convention**: Kebab-case descriptive names
|
||||
- **Examples**:
|
||||
- `1.8.0-workflow-iterator.png`
|
||||
- `1.8.0-bulk-select.png`
|
||||
- `1.9.0-new-feature.png`
|
||||
|
||||
## Quick Reference Template
|
||||
|
||||
Copy and fill this for each release:
|
||||
|
||||
```
|
||||
VERSION: ___________
|
||||
DATE: ___________
|
||||
MINOR_VERSION: ___________
|
||||
|
||||
Features to document:
|
||||
1. ___________________________
|
||||
2. ___________________________
|
||||
3. ___________________________
|
||||
|
||||
Branch name: {VERSION}
|
||||
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start early**: Begin documenting features as they're developed
|
||||
- **User perspective**: Write for users, not developers
|
||||
- **Be concise**: 1-2 sentences per feature
|
||||
- **Visual first**: Illustrations should showcase the feature clearly
|
||||
- **Consistent style**: Match tone and structure of previous changelogs
|
||||
- **Test links**: Verify all image paths work before committing
|
||||
- **Research thoroughly**: Use codebase search to understand features deeply
|
||||
@@ -30,18 +30,6 @@ type ButtonProps = {}; // Component props suffix with 'Props'
|
||||
// ✅ Files and directories - kebab-case
|
||||
// user-profile.component.tsx
|
||||
// user-profile.styles.ts
|
||||
|
||||
// ❌ NEVER use abbreviations in variable names
|
||||
// Bad
|
||||
const users = data.map((u) => u.name);
|
||||
const field = items.find((f) => f.id === id);
|
||||
|
||||
// Good
|
||||
const users = data.map((user) => user.name);
|
||||
const field = items.find((item) => item.id === id);
|
||||
const fieldMetadata = inlineFields.find(
|
||||
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
|
||||
);
|
||||
```
|
||||
|
||||
## Import Organization
|
||||
@@ -71,11 +59,11 @@ const processUserData = (
|
||||
): ProcessedUser => {
|
||||
const processedUser = transformUserData(user);
|
||||
applyOptions(processedUser, options);
|
||||
|
||||
|
||||
if (callback) {
|
||||
callback(processedUser);
|
||||
}
|
||||
|
||||
|
||||
return processedUser;
|
||||
};
|
||||
```
|
||||
@@ -83,7 +71,7 @@ const processUserData = (
|
||||
## Comments
|
||||
```typescript
|
||||
// ✅ Use short-form comments, NOT JSDoc blocks
|
||||
// ✅ Explain business logic and non-obvious intentions (WHY, not WHAT)
|
||||
// ✅ Explain business logic and non-obvious intentions
|
||||
// Apply 15% discount for premium users with orders > $100
|
||||
const discount = isPremiumUser && orderTotal > 100 ? 0.15 : 0;
|
||||
|
||||
@@ -97,55 +85,12 @@ const calculateTotalPrice = (basePrice: number): number => {
|
||||
// Implementation
|
||||
};
|
||||
|
||||
// ❌ AVOID obvious comments that just describe what code does
|
||||
// Bad: Get all inline fields dynamically
|
||||
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({...});
|
||||
|
||||
// Bad: Define standard fields in display order
|
||||
const standardFieldOrder = ['startsAt', 'endsAt', 'conferenceLink'];
|
||||
|
||||
// Bad: Split fields into standard and custom
|
||||
const standardFields = standardFieldOrder.map(...)
|
||||
|
||||
// ✅ GOOD: Only comment if explaining non-obvious business logic
|
||||
// Calendar events display standard fields first, then custom fields after participants
|
||||
// to maintain consistency with the legacy UI behavior
|
||||
const standardFields = standardFieldOrder.map(...)
|
||||
|
||||
// ❌ AVOID JSDoc blocks - use short comments instead
|
||||
/**
|
||||
* This style is NOT preferred in this codebase
|
||||
*/
|
||||
```
|
||||
|
||||
**Comment Guidelines:**
|
||||
- **DO** comment complex business rules or domain-specific logic
|
||||
- **DO** comment non-obvious algorithmic decisions
|
||||
- **DO** add TODOs for future improvements
|
||||
- **DON'T** comment obvious variable declarations or function calls
|
||||
- **DON'T** comment what is already clear from well-named variables/functions
|
||||
- **DON'T** add comments that just repeat what the code says
|
||||
|
||||
## Utility Helpers
|
||||
```typescript
|
||||
// ✅ Use existing utility helpers instead of manual checks
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isNonEmptyString, isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
// ❌ Manual type guards
|
||||
const validItems = items.filter((item): item is Item => item !== undefined);
|
||||
const hasValue = value !== null && value !== undefined;
|
||||
|
||||
// ✅ Use utility helpers
|
||||
const validItems = items.filter(isDefined);
|
||||
const hasValue = isDefined(value);
|
||||
|
||||
// Other useful helpers:
|
||||
// - isDefined(value) - checks !== null && !== undefined
|
||||
// - isNonEmptyString(value) - checks string is defined and not empty
|
||||
// - isNonEmptyArray(value) - checks array is defined and has items
|
||||
```
|
||||
|
||||
## Security Patterns
|
||||
```typescript
|
||||
// ✅ CSV Export: Always apply security first, then formatting
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
description: Guidelines for generating and managing TypeORM migrations in twenty-server
|
||||
globs: [
|
||||
"packages/twenty-server/src/**/*.entity.ts",
|
||||
"packages/twenty-server/src/database/typeorm/**/*.ts"
|
||||
]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Server Migrations (twenty-server)
|
||||
|
||||
- **When changing an entity, always generate a migration**
|
||||
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
|
||||
- Use the Nx + TypeORM command from the project root:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
|
||||
|
||||
- **Prefer generated migrations over manual edits**
|
||||
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
|
||||
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
|
||||
|
||||
- **Keep migrations consistent and reversible**
|
||||
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
|
||||
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"setup-worktree": [
|
||||
"nvm use",
|
||||
"yarn",
|
||||
"cp $ROOT_WORKTREE_PATH/packages/twenty-server/.env packages/twenty-server/.env || true",
|
||||
"cp $ROOT_WORKTREE_PATH/packages/twenty-front/.env packages/twenty-front/.env || true"
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.cjs text eol=lf
|
||||
|
||||
# Patch files may have mixed line endings by design
|
||||
*.patch -text
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.svg binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Contributing to Twenty
|
||||
|
||||
Thanks for considering contributing to Twenty!
|
||||
Thanks for considering contributing to Twenty!
|
||||
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## Good first issues
|
||||
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
|
||||
## Issue assignment
|
||||
|
||||
To avoid conflicts, we follow these guidelines:
|
||||
|
||||
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://docs.twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
|
||||
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
|
||||
2. For `size: long` Issues, assigned contributors have one week to submit their first draft PR.
|
||||
|
||||
## How to Contribute
|
||||
@@ -63,4 +63,4 @@ git push origin your-branch-name
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you face any issues or have suggestions, please feel free to [create an issue on Twenty's GitHub repository](https://github.com/twentyhq/twenty/issues/new). Please provide as much detail as possible.
|
||||
If you face any issues or have suggestions, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. Please provide as much detail as possible.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
versioning-strategy: "lockfile-only"
|
||||
assignees:
|
||||
- "mabdullahabaid"
|
||||
ignore:
|
||||
- dependency-name: "@graphql-yoga/nestjs"
|
||||
- dependency-name: "@nestjs/graphql"
|
||||
- dependency-name: "@ptc-org/nestjs-query-graphql"
|
||||
- dependency-name: "typeorm"
|
||||
@@ -1,13 +1,8 @@
|
||||
name: CD deploy main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
name: CD deploy tag
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: Changed files reusable workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MAIN_SERVER_PORT: 3000
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: shipfox-8vcpu-ubuntu-2404
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
@@ -124,14 +124,13 @@ jobs:
|
||||
- name: Setup current branch database
|
||||
run: |
|
||||
npx nx reset:env twenty-server
|
||||
# Function to set or update environment variable
|
||||
set_env_var() {
|
||||
local var_name="$1"
|
||||
local var_value="$2"
|
||||
local env_file="packages/twenty-server/.env"
|
||||
|
||||
echo "" >> "$env_file"
|
||||
|
||||
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
@@ -277,14 +276,13 @@ jobs:
|
||||
- name: Setup main branch database
|
||||
run: |
|
||||
npx nx reset:env twenty-server
|
||||
# Function to set or update environment variable
|
||||
set_env_var() {
|
||||
local var_name="$1"
|
||||
local var_value="$2"
|
||||
local env_file="packages/twenty-server/.env"
|
||||
|
||||
echo "" >> "$env_file"
|
||||
|
||||
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
name: CI CLI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-cli/**
|
||||
cli-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test, build]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:cli
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-cli-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, cli-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,55 +0,0 @@
|
||||
name: CI Create App
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/create-twenty-app/**
|
||||
create-app-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test, build]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:create-app
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-create-app-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,47 +0,0 @@
|
||||
name: CI Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-docs/**
|
||||
eslint.config.mjs
|
||||
|
||||
docs-lint:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Docs / Lint English MDX files
|
||||
run: npx eslint "packages/twenty-docs/{developers,user-guide,twenty-ui,getting-started,snippets}/**/*.mdx" --max-warnings 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI E2E Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +11,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
name: CI Emails
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Front
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +11,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
@@ -24,7 +23,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
@@ -141,8 +139,7 @@ jobs:
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
touch .env
|
||||
echo "" >> .env
|
||||
echo "REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL" >> .env
|
||||
echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env
|
||||
- name: Publish to Chromatic
|
||||
run: npx nx run twenty-front:chromatic:ci
|
||||
front-task:
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
name: "Release: create"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -18,42 +13,33 @@ on:
|
||||
default: true
|
||||
description: Create a release after merging the PR
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash --noprofile --norc -euo pipefail {0}
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
|
||||
- name: Sanitize version
|
||||
id: sanitize
|
||||
env:
|
||||
RAW_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
VERSION="${RAW_VERSION#v}"
|
||||
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo version=$(echo ${{ github.event.inputs.version }} | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update versions
|
||||
env:
|
||||
VERSION: ${{ steps.sanitize.outputs.version }}
|
||||
run: |
|
||||
printf '%s\n' "$VERSION" > version.txt
|
||||
echo ${{ steps.sanitize.outputs.version }} > version.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
branch: release/${{ steps.sanitize.outputs.version }}
|
||||
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
|
||||
committer: Github Action Deploy <github-action-deploy@twenty.com>
|
||||
author: Github Action Deploy <github-action-deploy@twenty.com>
|
||||
title: Release v${{ steps.sanitize.outputs.version }}
|
||||
labels: |
|
||||
release
|
||||
${{ github.event.inputs.create_release == true && 'create_release' || '' }}
|
||||
branch: release/${{ steps.sanitize.outputs.version }}
|
||||
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
|
||||
committer: Github Action Deploy <github-action-deploy@twenty.com>
|
||||
author: Github Action Deploy <github-action-deploy@twenty.com>
|
||||
title: Release v${{ steps.sanitize.outputs.version }}
|
||||
labels: |
|
||||
release
|
||||
${{ github.event.inputs.create_release == true && 'create_release' || '' }}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
name: "Release: on merge"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash --noprofile --norc -euo pipefail {0}
|
||||
|
||||
jobs:
|
||||
tag_and_release:
|
||||
timeout-minutes: 10
|
||||
@@ -20,40 +12,33 @@ jobs:
|
||||
steps:
|
||||
- name: Check PR Author
|
||||
id: check_author
|
||||
env:
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$PR_AUTHOR" != "github-actions[bot]" ]]; then
|
||||
echo "PR author ($PR_AUTHOR) is not trusted. Exiting."
|
||||
if [[ "${{ github.event.pull_request.user.login }}" != "github-actions[bot]" ]]; then
|
||||
echo "PR author (${AUTHOR}) is not trusted. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Get version from PR title
|
||||
id: extract_version
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION=$(printf '%s' "$PR_TITLE" | sed -n 's/.*Release v\([0-9][0-9.]*\).*/\1/p')
|
||||
VERSION=$(echo "${{ github.event.pull_request.title }}" | sed -n 's/.*Release v\([0-9.]*\).*/\1/p')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "No valid version found in PR title. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
printf 'VERSION=%s\n' "$VERSION" >> "$GITHUB_ENV"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Push new tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global user.name 'Github Action Deploy'
|
||||
git config --global user.email 'github-action-deploy@twenty.com'
|
||||
git tag "v${{ env.VERSION }}"
|
||||
git push origin "v${{ env.VERSION }}"
|
||||
git tag v${{ env.VERSION }}
|
||||
git push origin v${{ env.VERSION }}
|
||||
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
if: contains(github.event.pull_request.labels.*.name, 'create_release')
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
name: CI SDK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test, build]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NODE_ENV: test
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Append billing config to .env.test
|
||||
working-directory: packages/twenty-server
|
||||
run: |
|
||||
echo "" >> .env.test
|
||||
echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: SDK / Run E2E Tests
|
||||
run: npx nx test:e2e twenty-sdk
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +11,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
@@ -23,7 +22,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-server/**
|
||||
packages/twenty-front/src/generated/**
|
||||
packages/twenty-front/src/generated-metadata/**
|
||||
@@ -117,7 +115,7 @@ jobs:
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*core-migration-check.ts
|
||||
|
||||
|
||||
exit 1
|
||||
fi
|
||||
- name: GraphQL / Check for Pending Generation
|
||||
@@ -126,13 +124,13 @@ jobs:
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
|
||||
# Check if GraphQL generated files were modified
|
||||
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata; then
|
||||
# Check if any files were modified
|
||||
if ! git diff --quiet; then
|
||||
echo "::error::GraphQL schema changes detected. Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
|
||||
echo ""
|
||||
echo "The following GraphQL schema changes were detected:"
|
||||
echo "==================================================="
|
||||
git diff -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata
|
||||
git diff
|
||||
echo "==================================================="
|
||||
echo ""
|
||||
echo "Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
|
||||
@@ -171,7 +169,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
shard: [1, 2, 3, 4]
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -192,7 +190,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
@@ -200,16 +198,16 @@ jobs:
|
||||
- 8123:8123
|
||||
- 9000:9000
|
||||
options: >-
|
||||
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
NODE_ENV: test
|
||||
ANALYTICS_ENABLED: true
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
SHARD_COUNTER: 5
|
||||
SHARD_COUNTER: 4
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -219,7 +217,6 @@ jobs:
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Update .env.test for integrations tests
|
||||
run: |
|
||||
echo "" >> .env.test
|
||||
echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
@@ -255,6 +252,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Shared
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +11,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name: 'Test Docker Compose'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -37,7 +33,6 @@ jobs:
|
||||
echo "Setting up .env file..."
|
||||
cp .env.example .env
|
||||
echo "Generating secrets..."
|
||||
echo "" >> .env
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Utils
|
||||
|
||||
on:
|
||||
# it's usually not recommended to use pull_request_target
|
||||
# but we consider it's safe here if we keep the same steps
|
||||
@@ -7,7 +6,6 @@ on:
|
||||
# and: https://github.com/facebook/react-native/pull/34370/files
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
checks: write
|
||||
@@ -15,7 +13,6 @@ permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# We don't cancel in-progress because this workflow is triggered on
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
name: CI Website
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
name: 'Pull docs translations from Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/twenty-docs/**'
|
||||
- 'crowdin.yml'
|
||||
- '.github/workflows/docs-i18n-pull.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
pull_docs_translations:
|
||||
name: Pull docs translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup i18n branch
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
git add .
|
||||
git stash || true
|
||||
|
||||
- name: Pull translated docs from Crowdin
|
||||
if: github.event_name != 'pull_request' && (inputs.force_pull == true || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: 'packages/twenty-docs/**/*.mdx'
|
||||
translation: 'packages/twenty-docs/l/%two_letters_code%/**/%original_file_name%'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
skip_untranslated_files: true
|
||||
push_translations: false
|
||||
create_pull_request: false
|
||||
skip_ref_checkout: true
|
||||
dryrun_action: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CROWDIN_PROJECT_ID: '1'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Fix file permissions
|
||||
if: github.event_name != 'pull_request'
|
||||
run: sudo chown -R runner:docker . || true
|
||||
|
||||
- name: Fix translated documentation links
|
||||
run: bash packages/twenty-docs/scripts/fix-translated-links.sh
|
||||
|
||||
- name: Regenerate navigation template
|
||||
if: github.event_name == 'pull_request'
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Regenerate docs.json
|
||||
run: yarn docs:generate
|
||||
|
||||
- name: Commit artifacts to pull request branch
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json
|
||||
if git diff --staged --quiet --exit-code; then
|
||||
echo "No navigation/doc changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore: sync docs artifacts"
|
||||
git push origin "HEAD:$HEAD_REF"
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
|
||||
- name: Check for changes and commit
|
||||
if: github.event_name != 'pull_request'
|
||||
id: check_changes
|
||||
run: |
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: update docs translations from Crowdin and fix internal links"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n --title 'i18n - docs translations' --body 'Created by Github action' || true
|
||||
else
|
||||
echo "No file differences between branches, skipping PR creation"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
name: 'Push docs to Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main', 'docs-localized-navigation']
|
||||
paths:
|
||||
- 'packages/twenty-docs/**/*.mdx'
|
||||
- '!packages/twenty-docs/fr/**'
|
||||
- 'crowdin.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
push_docs:
|
||||
name: Push documentation to Crowdin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate navigation template for Crowdin
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate navigation template for Crowdin
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Upload docs to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: 1
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
name: 'Pull translations from Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
@@ -27,12 +23,16 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -74,8 +74,6 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: '**/en.po'
|
||||
translation: '%original_path%/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
@@ -93,7 +91,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CROWDIN_PROJECT_ID: '1'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
|
||||
# As the files are extracted from a Docker container, they belong to root:root
|
||||
# We need to fix this before the next steps
|
||||
- name: Fix file permissions
|
||||
@@ -105,7 +103,7 @@ jobs:
|
||||
# if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
git status
|
||||
git config --global user.name 'github-actions'
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
name: 'Push translations to Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
@@ -12,12 +8,15 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
name: 'Preview Environment Dispatch'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: read
|
||||
|
||||
on:
|
||||
# Using pull_request_target instead of pull_request to have access to secrets for external contributors
|
||||
# Security note: This is safe because we're only using the repository-dispatch action with limited scope
|
||||
@@ -24,6 +19,10 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
trigger-preview:
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: read
|
||||
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
name: 'Preview Environment Keep Alive'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [preview-environment]
|
||||
@@ -37,7 +33,6 @@ jobs:
|
||||
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
|
||||
|
||||
echo "Generating secrets..."
|
||||
echo "" >> packages/twenty-docker/.env
|
||||
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
|
||||
@@ -61,7 +56,6 @@ jobs:
|
||||
# Update the SERVER_URL with the tunnel URL
|
||||
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
|
||||
sed -i '/SERVER_URL=/d' .env
|
||||
echo "" >> .env
|
||||
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
|
||||
|
||||
# Start the services
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "postgres-mcp", "--access-mode=unrestricted"],
|
||||
"env": {
|
||||
"DATABASE_URI": "${PG_DATABASE_URL}"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
Vendored
+14
-7
@@ -8,7 +8,11 @@
|
||||
"runtimeVersion": "24.5",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "run", "twenty-server:start"],
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:start"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
@@ -19,7 +23,11 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "run", "twenty-server:worker"],
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:worker"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
@@ -66,15 +74,14 @@
|
||||
"--",
|
||||
"--config",
|
||||
"./jest-integration.config.ts",
|
||||
"${relativeFile}",
|
||||
"--silent=false",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
@@ -95,7 +102,7 @@
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+1
-3
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": false,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.eol": "auto",
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
@@ -29,9 +29,6 @@ npx nx run twenty-server:test:integration:with-db-reset # Integration tests wit
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests
|
||||
|
||||
|
||||
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
@@ -65,10 +62,10 @@ npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
|
||||
# Generate migration
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Sync metadata
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
### GraphQL
|
||||
@@ -125,8 +122,6 @@ packages/
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting and type checking after code changes
|
||||
2. Test changes with relevant test suites
|
||||
@@ -149,4 +144,4 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Development guidelines and best practices
|
||||
- `.cursor/rules/` - Development guidelines and best practices
|
||||
@@ -3,7 +3,8 @@ DOCKER_NETWORK=twenty_network
|
||||
ensure-docker-network:
|
||||
docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || docker network create $(DOCKER_NETWORK)
|
||||
|
||||
postgres-on-docker: ensure-docker-network
|
||||
postgres-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_pg \
|
||||
-e POSTGRES_USER=postgres \
|
||||
@@ -21,13 +22,16 @@ postgres-on-docker: ensure-docker-network
|
||||
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||
|
||||
redis-on-docker: ensure-docker-network
|
||||
redis-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||
|
||||
clickhouse-on-docker: ensure-docker-network
|
||||
clickhouse-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=devPassword clickhouse/clickhouse-server:latest \
|
||||
|
||||
grafana-on-docker: ensure-docker-network
|
||||
grafana-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_grafana \
|
||||
-p 4000:3000 \
|
||||
@@ -37,7 +41,8 @@ grafana-on-docker: ensure-docker-network
|
||||
-v $(PWD)/packages/twenty-docker/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources \
|
||||
grafana/grafana-oss:latest
|
||||
|
||||
opentelemetry-collector-on-docker: ensure-docker-network
|
||||
opentelemetry-collector-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_otlp_collector \
|
||||
-p 4317:4317 \
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
@@ -6,7 +8,7 @@
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://docs.twenty.com">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<br />
|
||||
|
||||
|
||||
@@ -22,11 +24,11 @@
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
See:
|
||||
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
|
||||
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
|
||||
@@ -42,7 +44,7 @@ We built Twenty for three reasons:
|
||||
|
||||
# What You Can Do With Twenty
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
|
||||
@@ -129,7 +131,7 @@ Below are a few features we have implemented to date:
|
||||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
|
||||
+1
-33
@@ -27,37 +27,5 @@ files: [
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - user-guide
|
||||
# Using md type to preserve JSX component structure
|
||||
# This prevents Crowdin from reformatting <Warning>, <Accordion>, etc.
|
||||
#
|
||||
"source": "packages/twenty-docs/user-guide/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/user-guide/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - developers
|
||||
# Using md type to preserve JSX component structure
|
||||
#
|
||||
"source": "packages/twenty-docs/developers/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/developers/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - twenty-ui
|
||||
# Using md type to preserve JSX component structure
|
||||
#
|
||||
"source": "packages/twenty-docs/twenty-ui/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/twenty-ui/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# Navigation labels template - translated into per-locale navigation.json
|
||||
#
|
||||
"source": "packages/twenty-docs/navigation/navigation.template.json",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/navigation.json",
|
||||
}
|
||||
]
|
||||
]
|
||||
+10
-42
@@ -4,7 +4,6 @@ import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import linguiPlugin from 'eslint-plugin-lingui';
|
||||
import * as mdxPlugin from 'eslint-plugin-mdx';
|
||||
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import unicornPlugin from 'eslint-plugin-unicorn';
|
||||
@@ -20,28 +19,27 @@ export default [
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: ['**/node_modules/**'],
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
],
|
||||
},
|
||||
|
||||
// Base configuration for all files
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
lingui: linguiPlugin,
|
||||
'prettier': prettierPlugin,
|
||||
'lingui': linguiPlugin,
|
||||
'@nx': nxPlugin,
|
||||
'prefer-arrow': preferArrowPlugin,
|
||||
import: importPlugin,
|
||||
'import': importPlugin,
|
||||
'unused-imports': unusedImportsPlugin,
|
||||
unicorn: unicornPlugin,
|
||||
'unicorn': unicornPlugin,
|
||||
},
|
||||
rules: {
|
||||
// General rules
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': [
|
||||
'warn',
|
||||
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
|
||||
],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
'no-control-regex': 0,
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
@@ -55,14 +53,6 @@ export default [
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:apps',
|
||||
onlyDependOnLibsWithTags: ['scope:apps', 'scope:sdk'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
@@ -133,9 +123,9 @@ export default [
|
||||
'@typescript-eslint/ban-ts-comment': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'inline-type-imports',
|
||||
fixStyle: 'inline-type-imports'
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
@@ -192,26 +182,4 @@ export default [
|
||||
parser: jsoncParser,
|
||||
},
|
||||
},
|
||||
|
||||
// MDX files
|
||||
{
|
||||
...mdxPlugin.flat,
|
||||
plugins: {
|
||||
...mdxPlugin.flat.plugins,
|
||||
'@nx': nxPlugin,
|
||||
},
|
||||
},
|
||||
mdxPlugin.flatCodeBlocks,
|
||||
{
|
||||
files: ['**/*.mdx'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'off',
|
||||
'unused-imports/no-unused-vars': 'off',
|
||||
// Enforce JSX tags on separate lines to prevent Crowdin translation issues
|
||||
'@nx/workspace-mdx-component-newlines': 'error',
|
||||
// Disallow angle bracket placeholders to prevent Crowdin translation errors
|
||||
'@nx/workspace-no-angle-bracket-placeholders': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"start": {
|
||||
"cache": false,
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"lint": {
|
||||
@@ -133,7 +133,7 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true storybook build --test",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
@@ -234,7 +234,7 @@
|
||||
"command": "nx storybook:build {projectName}",
|
||||
"forwardAllArgs": false
|
||||
},
|
||||
"chromatic --storybook-build-dir=storybook-static {args.ci}"
|
||||
"cross-var chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --storybook-build-dir=storybook-static {args.ci}"
|
||||
],
|
||||
"parallel": false
|
||||
},
|
||||
@@ -280,7 +280,7 @@
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"version": "22.0.3"
|
||||
"version": "21.3.11"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
|
||||
+39
-23
@@ -2,21 +2,26 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@date-fns/tz": "^1.4.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@react-email/components": "^0.5.3",
|
||||
"@react-email/render": "^1.2.3",
|
||||
"@sentry/profiling-node": "^9.26.0",
|
||||
"@sentry/react": "^9.26.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@wyw-in-js/vite": "^0.7.0",
|
||||
"add": "^2.0.6",
|
||||
"archiver": "^7.0.1",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"deep-equal": "^2.2.2",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"drizzle-orm": "^0.29.3",
|
||||
"file-type": "16.5.4",
|
||||
"framer-motion": "^11.18.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
@@ -37,10 +42,13 @@
|
||||
"lodash.mapvalues": "^4.6.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"lodash.upperfirst": "^4.3.1",
|
||||
"microdiff": "^1.3.2",
|
||||
"moize": "^6.1.6",
|
||||
"patch-package": "^8.0.0",
|
||||
"planer": "^1.2.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -49,6 +57,8 @@
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-behead": "^3.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rxjs": "^7.2.0",
|
||||
"semver": "^7.5.4",
|
||||
@@ -61,7 +71,7 @@
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"xlsx-ugnis": "^0.19.3",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.5",
|
||||
@@ -73,14 +83,15 @@
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@nx/eslint": "22.0.3",
|
||||
"@nx/eslint-plugin": "22.0.3",
|
||||
"@nx/jest": "22.0.3",
|
||||
"@nx/js": "22.0.3",
|
||||
"@nx/react": "22.0.3",
|
||||
"@nx/storybook": "22.0.3",
|
||||
"@nx/vite": "22.0.3",
|
||||
"@nx/web": "22.0.3",
|
||||
"@nx/eslint": "21.3.11",
|
||||
"@nx/eslint-plugin": "21.3.11",
|
||||
"@nx/jest": "21.3.11",
|
||||
"@nx/js": "21.3.11",
|
||||
"@nx/react": "21.3.11",
|
||||
"@nx/storybook": "21.3.11",
|
||||
"@nx/vite": "21.3.11",
|
||||
"@nx/web": "21.3.11",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sentry/types": "^8",
|
||||
"@storybook/addon-actions": "8.6.14",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
@@ -112,6 +123,7 @@
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/imapflow": "^1.0.21",
|
||||
"@types/inquirer": "^9.0.9",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
@@ -125,9 +137,11 @@
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.mapvalues": "^4.6.9",
|
||||
"@types/lodash.omit": "^4.5.9",
|
||||
"@types/lodash.pick": "^4.3.7",
|
||||
"@types/lodash.pickby": "^4.6.9",
|
||||
"@types/lodash.snakecase": "^4.1.7",
|
||||
"@types/lodash.upperfirst": "^4.3.7",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
@@ -143,18 +157,19 @@
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@typescript-eslint/utils": "^8.39.0",
|
||||
"@vitejs/plugin-react-swc": "3.11.0",
|
||||
"@vitest/ui": "1.4.0",
|
||||
"@yarnpkg/types": "^4.0.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"danger": "^13.0.4",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"esbuild": "^0.25.10",
|
||||
"cross-var": "^1.1.0",
|
||||
"danger": "^11.3.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-lingui": "^0.9.0",
|
||||
"eslint-plugin-mdx": "^3.6.2",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-project-structure": "^3.9.1",
|
||||
@@ -173,7 +188,8 @@
|
||||
"jsdom": "~22.1.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"nx": "22.0.3",
|
||||
"nx": "21.3.11",
|
||||
"playwright": "^1.46.0",
|
||||
"prettier": "^3.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -187,7 +203,12 @@
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"vite": "^7.0.0"
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-checker": "^0.10.2",
|
||||
"vite-plugin-cjs-interop": "^2.2.0",
|
||||
"vite-plugin-dts": "3.8.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -198,7 +219,7 @@
|
||||
"name": "twenty",
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"graphql": "16.8.1",
|
||||
"graphql": "16.8.0",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.9.2",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
@@ -208,8 +229,6 @@
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
"scripts": {
|
||||
"docs:generate": "tsx packages/twenty-docs/scripts/generate-docs-json.ts",
|
||||
"docs:generate-navigation-template": "tsx packages/twenty-docs/scripts/generate-navigation-template.ts",
|
||||
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
||||
},
|
||||
"workspaces": {
|
||||
@@ -221,13 +240,10 @@
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<div align="center">
|
||||
<a href="https://twenty.com">
|
||||
<picture>
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
|
||||
</picture>
|
||||
</a>
|
||||
<h1>Create Twenty App</h1>
|
||||
|
||||
<a href="https://www.npmjs.com/package/create-twenty-app"><img alt="NPM version" src="https://img.shields.io/npm/v/create-twenty-app.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<a href="https://github.com/twentyhq/twenty/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/next.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<a href="https://discord.gg/cx5n4Jzs57"><img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=Twenty&labelColor=000000&logoWidth=20"></a>
|
||||
|
||||
</div>
|
||||
|
||||
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
- Zero‑config project bootstrap
|
||||
- Preconfigured scripts for auth, generate, dev sync, one‑off sync, uninstall
|
||||
- Strong TypeScript support and typed client generation
|
||||
|
||||
## Prerequisites
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Quick start
|
||||
```bash
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn create-entity
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn generate
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn dev
|
||||
|
||||
# Or run a one‑time sync
|
||||
yarn sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn logs
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
```
|
||||
|
||||
## What gets scaffolded
|
||||
- A minimal app structure ready for Twenty
|
||||
- TypeScript configuration
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
- Example placeholders to help you add entities, actions, and sync logic
|
||||
|
||||
## Next steps
|
||||
- Explore the generated project and add your first entity with `yarn create-entity`.
|
||||
- Keep your types up‑to‑date using `yarn generate`.
|
||||
- Use `yarn dev` while you iterate to see changes instantly in your workspace.
|
||||
|
||||
|
||||
## Publish your application
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
|
||||
You can share your application with all Twenty users:
|
||||
|
||||
```bash
|
||||
# pull the Twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- Copy your app folder into `twenty/packages/twenty-apps`.
|
||||
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
- Auth prompts not appearing: run `yarn auth` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn generate` runs without errors, then re‑start `yarn dev`.
|
||||
|
||||
## Contributing
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
@@ -1,40 +0,0 @@
|
||||
const jestConfig = {
|
||||
displayName: 'twenty-cli',
|
||||
preset: '../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transformIgnorePatterns: ['../../node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'@swc/jest',
|
||||
{
|
||||
jsc: {
|
||||
parser: { syntax: 'typescript', tsx: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js'],
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
coverageDirectory: './coverage',
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/__tests__/**/*.(test|spec).{js,ts}',
|
||||
'<rootDir>/src/**/?(*.)(test|spec).{js,ts}',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,js}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/cli.ts', // Exclude CLI entry point from coverage
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 1,
|
||||
lines: 1,
|
||||
functions: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default jestConfig;
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.2.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty",
|
||||
"cli",
|
||||
"crm",
|
||||
"application",
|
||||
"development"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/cli.d.ts",
|
||||
"import": "./dist/cli.mjs",
|
||||
"require": "./dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@genql/cli": "^3.0.3",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^10.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"yarn": "^4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:create-app"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/dist"]
|
||||
},
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "packages/create-twenty-app",
|
||||
"command": "tsx src/cli.ts"
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "packages/create-twenty-app",
|
||||
"command": "node dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {
|
||||
"options": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "{projectRoot}/jest.config.mjs"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"coverage": true,
|
||||
"watchAll": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommanderError } from 'commander';
|
||||
import { CreateAppCommand } from '@/create-app.command';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command(packageJson.name)
|
||||
.description('CLI tool to initialize a new Twenty application')
|
||||
.version(
|
||||
packageJson.version,
|
||||
'-v, --version',
|
||||
'Output the current version of create-twenty-app.',
|
||||
)
|
||||
.argument('[directory]')
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(async (directory?: string) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await new CreateAppCommand().execute(directory);
|
||||
});
|
||||
|
||||
program.exitOverride();
|
||||
|
||||
try {
|
||||
program.parse();
|
||||
} catch (error) {
|
||||
if (error instanceof CommanderError) {
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
24.5.0
|
||||
Binary file not shown.
-942
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -1,27 +0,0 @@
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn auth
|
||||
```
|
||||
|
||||
Then, install this app to your workspace:
|
||||
|
||||
```bash
|
||||
yarn sync
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
@@ -1,137 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
|
||||
|
||||
export default [
|
||||
// Base JS rules
|
||||
js.configs.recommended,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
// Base config for TS/JS files
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
import: importPlugin,
|
||||
'prefer-arrow': preferArrowPlugin,
|
||||
'unused-imports': unusedImportsPlugin,
|
||||
},
|
||||
rules: {
|
||||
// General rules (aligned with main project)
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': [
|
||||
'warn',
|
||||
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
|
||||
],
|
||||
'no-control-regex': 0,
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
// Import rules
|
||||
'import/no-relative-packages': 'error',
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
|
||||
// Prefer arrow functions
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
},
|
||||
],
|
||||
|
||||
// Unused imports
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
|
||||
// Prettier (formatting as lint errors if you want)
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// TypeScript-specific configuration
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
rules: {
|
||||
// Turn off base rule and use TS-aware versions
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'inline-type-imports',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Test files (Jest)
|
||||
{
|
||||
files: ['**/*.spec.@(ts|tsx|js|jsx)', '**/*.test.@(ts|tsx|js|jsx)'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
jest: true,
|
||||
describe: true,
|
||||
it: true,
|
||||
expect: true,
|
||||
beforeEach: true,
|
||||
afterEach: true,
|
||||
beforeAll: true,
|
||||
afterAll: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import * as path from 'path';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import { install } from '@/utils/install';
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(directory?: string): Promise<void> {
|
||||
try {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
this.logCreationInfo({ appDirectory, appName });
|
||||
|
||||
await fs.ensureDir(appDirectory);
|
||||
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
});
|
||||
|
||||
await install(appDirectory);
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
this.logSuccess(appDirectory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppInfos(directory?: string): Promise<{
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}> {
|
||||
const { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !directory,
|
||||
default: 'my-awesome-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers?.name ?? directory);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const computedName = name ?? directory;
|
||||
|
||||
const appName = computedName.trim();
|
||||
|
||||
const appDisplayName = displayName.trim();
|
||||
|
||||
const appDescription = description.trim();
|
||||
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
|
||||
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
}
|
||||
|
||||
private async validateDirectory(appDirectory: string): Promise<void> {
|
||||
if (!(await fs.pathExists(appDirectory))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fs.readdir(appDirectory);
|
||||
if (files.length > 0) {
|
||||
throw new Error(
|
||||
`Directory ${appDirectory} already exists and is not empty`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logCreationInfo({
|
||||
appDirectory,
|
||||
appName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
appName: string;
|
||||
}): void {
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private logSuccess(appDirectory: string): void {
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(`cd ${appDirectory.split('/').reverse()[0] ?? ''}`);
|
||||
console.log('yarn auth');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
|
||||
describe('convertToLabel', () => {
|
||||
it('should convert to label', () => {
|
||||
expect(convertToLabel('toto')).toBe('Toto');
|
||||
expect(convertToLabel('totoTata')).toBe('Toto tata');
|
||||
expect(convertToLabel('totoTataTiti')).toBe('Toto tata titi');
|
||||
expect(convertToLabel('toto-tata-titi')).toBe('Toto tata titi');
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const copyBaseApplicationProject = async ({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
}: {
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
await createPackageJson({ appName, appDirectory });
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
await createApplicationConfig({
|
||||
displayName: appDisplayName,
|
||||
description: appDescription,
|
||||
appDirectory,
|
||||
});
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
const createGitignore = async (appDirectory: string) => {
|
||||
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
appDirectory,
|
||||
}: {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
const content = `import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '${v4()}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description ?? ''}',
|
||||
};
|
||||
|
||||
export default config;
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
}: {
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
version: '0.1.0',
|
||||
license: 'MIT',
|
||||
engines: {
|
||||
node: '^24.5.0',
|
||||
npm: 'please-use-yarn',
|
||||
yarn: '>=4.0.2',
|
||||
},
|
||||
packageManager: 'yarn@4.9.2',
|
||||
scripts: {
|
||||
'create-entity': 'twenty app add',
|
||||
dev: 'twenty app dev',
|
||||
generate: 'twenty app generate',
|
||||
sync: 'twenty app sync',
|
||||
logs: 'twenty app logs',
|
||||
uninstall: 'twenty app uninstall',
|
||||
help: 'twenty help',
|
||||
auth: 'twenty auth login',
|
||||
},
|
||||
dependencies: {
|
||||
'twenty-sdk': '0.2.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/node': '^24.7.2',
|
||||
typescript: '^5.9.3',
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { startCase } from 'lodash';
|
||||
|
||||
export const convertToLabel = (str: string) => {
|
||||
const s = startCase(str).toLowerCase();
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const install = async (root: string) => {
|
||||
try {
|
||||
await execPromise('yarn', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('yarn install failed:'), error.stdout);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
const isInGitRepository = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git rev-parse --is-inside-work-tree', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isInMercurialRepository = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('hg --cwd . root', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isDefaultBranchSet = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git config init.defaultBranch', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const tryGitInit = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git --version', { cwd: root });
|
||||
|
||||
if (
|
||||
(await isInGitRepository(root)) ||
|
||||
(await isInMercurialRepository(root))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
await execPromise('git init', { cwd: root });
|
||||
|
||||
try {
|
||||
if (!(await isDefaultBranchSet(root))) {
|
||||
await execPromise('git checkout -b main', { cwd: root });
|
||||
}
|
||||
|
||||
await execPromise('git add -A', { cwd: root });
|
||||
await execPromise(
|
||||
'git commit -m "Initial commit from Create Twenty App"',
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
fs.rm(join(root, '.git'), { recursive: true, force: true });
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/__tests__/**"
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/__mocks__/**/*",
|
||||
"vite.config.ts",
|
||||
"jest.config.mjs",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx"
|
||||
]
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import packageJson from './package.json';
|
||||
|
||||
const moduleEntries = Object.keys((packageJson as any).exports || {})
|
||||
.filter(
|
||||
(key) => key !== './style.css' && key !== '.' && !key.startsWith('./src/'),
|
||||
)
|
||||
.map((module) => `src/${module.replace(/^\.\//, '')}/index.ts`);
|
||||
|
||||
const entries = ['src/cli.ts', ...moduleEntries];
|
||||
|
||||
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
|
||||
if (!chunk.isEntry) {
|
||||
throw new Error(
|
||||
`Should never occurs, encountered a non entry chunk ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const splitFaceModuleId = chunk.facadeModuleId?.split('/');
|
||||
if (splitFaceModuleId === undefined) {
|
||||
throw new Error(
|
||||
`Should never occurs splitFaceModuleId is undefined ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2];
|
||||
if (moduleDirectory === 'src') {
|
||||
return `${chunk.name}.${extension}`;
|
||||
}
|
||||
return `${moduleDirectory}.${extension}`;
|
||||
};
|
||||
|
||||
const copyAssetPlugin = (targets: { src: string; dest: string }[]) => {
|
||||
return {
|
||||
name: 'copy-assets',
|
||||
closeBundle: async () => {
|
||||
for (const target of targets) {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, target.src),
|
||||
path.resolve(__dirname, target.dest),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineConfig(() => {
|
||||
const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json');
|
||||
|
||||
return {
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/packages/create-twenty-app',
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: __dirname,
|
||||
}),
|
||||
dts({ entryRoot: './src', tsconfigPath: tsConfigPath }),
|
||||
copyAssetPlugin([
|
||||
{
|
||||
src: 'src/constants/base-application',
|
||||
dest: 'dist/constants/base-application',
|
||||
},
|
||||
]),
|
||||
],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
lib: { entry: entries, name: 'create-twenty-app' },
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...Object.keys((packageJson as any).dependencies || {}),
|
||||
'path',
|
||||
'fs',
|
||||
'child_process',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'),
|
||||
},
|
||||
{
|
||||
format: 'cjs',
|
||||
interop: 'auto',
|
||||
esModule: true,
|
||||
exports: 'named',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
generated
|
||||
@@ -1,6 +0,0 @@
|
||||
TWENTY_API_KEY=<SET_YOUR_TWENTY_API>
|
||||
DAYS_AGO=<SET_YOUR_DAYS_AGO>
|
||||
DISCORD_WEBHOOK_URL=<SET_YOUR_DISCORD_WEBHOOK_URL>
|
||||
FB_GRAPH_TOKEN=<SET_YOUR_FB_GRAPH_TOKEN>
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER=<SET_YOUR_WHATSAPP_RECIPIENT_PHONE_NUMBER>
|
||||
SLACK_HOOK_URL=<SET_YOUR_SLACK_HOOK_URL>
|
||||
@@ -1,2 +0,0 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
-942
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -1,108 +0,0 @@
|
||||
# Twenty CRM Activity Reporter ��
|
||||
|
||||
A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!
|
||||
|
||||
## Features
|
||||
|
||||
- 🧑💻 **People & Company Tracking**: Summarizes newly created people and companies
|
||||
- 🎯 **Opportunity Monitoring**: Reports on new opportunities created, broken down by stage
|
||||
- ✅ **Task Analytics**:
|
||||
- Tracks task creation
|
||||
- Calculates on-time completion rates
|
||||
- Identifies team members with the most overdue tasks (the "slackers")
|
||||
- 🔔 **Multi-Platform Notifications**: Send reports to Slack, Discord, and/or WhatsApp
|
||||
- ⏰ **Configurable Time Range**: Look back any number of days
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v14 or higher recommended)
|
||||
- TypeScript
|
||||
- A [Twenty CRM](https://twenty.com) account with API access
|
||||
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
|
||||
|
||||
## Installing dependencies
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `TWENTY_API_KEY` | ✅ Yes | Your Twenty CRM API key |
|
||||
| `DAYS_AGO` | ✅ Yes | Number of days to look back for the report |
|
||||
| `SLACK_HOOK_URL` | ❌ No | Slack incoming webhook URL |
|
||||
| `DISCORD_WEBHOOK_URL` | ❌ No | Discord webhook URL |
|
||||
| `FB_GRAPH_TOKEN` | ❌ No | Facebook Graph API token for WhatsApp |
|
||||
| `WHATSAPP_RECIPIENT_PHONE_NUMBER` | ❌ No | WhatsApp recipient phone number (with country code) |
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
.
|
||||
├── index.ts # Main entry point
|
||||
├── people-creation-summariser.ts # Summarizes people/company creation
|
||||
├── opportunity-creation-summariser.ts # Summarizes opportunity creation
|
||||
├── task-creation-summariser.ts # Summarizes task creation & completion
|
||||
├── senders.ts # Handles sending to Slack/Discord/WhatsApp
|
||||
├── utils.ts # API request utility
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Data Collection**: The bot queries the Twenty CRM API for activities within the specified time range
|
||||
2. **Analysis**:
|
||||
- Counts new people and companies
|
||||
- Categorizes opportunities by stage
|
||||
- Calculates task completion rates and identifies overdue tasks
|
||||
3. **Reporting**: Formats the data into friendly messages
|
||||
4. **Distribution**: Sends reports to configured platforms (Slack, Discord, WhatsApp)
|
||||
|
||||
## Report Format
|
||||
|
||||
Each report includes:
|
||||
```
|
||||
Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
|
||||
|
||||
🧑💻 People & Companies
|
||||
- X People and Y Companies were added
|
||||
|
||||
🎯 Opportunities
|
||||
- X Opportunities were added: Y in NEW, Z in PROPOSAL
|
||||
|
||||
📋 Tasks
|
||||
- X Tasks were created
|
||||
- Y% Tasks were completed on time
|
||||
- [Name] slacked the most with Z Tasks overdue
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
This bot uses the [Twenty CRM REST API](https://api.twenty.com/rest/). The following endpoints are used:
|
||||
|
||||
- `GET /people` - Fetch people data
|
||||
- `GET /opportunities` - Fetch opportunity data
|
||||
- `GET /tasks` - Fetch task data
|
||||
- `GET /workspaceMembers/{id}` - Fetch workspace member details
|
||||
|
||||
## Notes
|
||||
|
||||
- The "slacker" detection is lighthearted and identifies team members with the most overdue tasks
|
||||
- At least one messaging platform must be configured for the bot to send reports
|
||||
- The bot uses ISO date format (YYYY-MM-DD) for date filtering
|
||||
- Task completion percentage only considers incomplete tasks (excludes already completed tasks from the calculation)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: No messages being sent
|
||||
- **Solution**: Ensure at least one messaging platform is configured with valid credentials
|
||||
|
||||
**Issue**: API authentication errors
|
||||
- **Solution**: Verify your `TWENTY_API_KEY` is correct and has necessary permissions
|
||||
|
||||
**Issue**: WhatsApp messages not sending
|
||||
- **Solution**: Ensure both `FB_GRAPH_TOKEN` and `WHATSAPP_RECIPIENT_PHONE_NUMBER` are set correctly
|
||||
|
||||
## Contributing
|
||||
Built with ❤️ and 🥖 by Azmat, Ali and Mike from 9dots
|
||||
@@ -1,45 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: 'b53627f5-ca60-478c-bc43-c7ab4904e34a',
|
||||
displayName: 'Activity Summary',
|
||||
description:
|
||||
'A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '304b7d5d-e2bb-4444-9b04-6b3ae8b73730',
|
||||
description: 'Twenty API Key',
|
||||
isSecret: true,
|
||||
},
|
||||
DAYS_AGO: {
|
||||
universalIdentifier: '040a3097-9cee-4f74-b957-c2f9bf636c3f',
|
||||
description:
|
||||
'How far back into the past we want to summarise – defaults to the past 7 days',
|
||||
value: '7',
|
||||
isSecret: false,
|
||||
},
|
||||
SLACK_HOOK_URL: {
|
||||
universalIdentifier: 'fd16e370-934c-4267-83b4-7d88259bf7e1',
|
||||
description: 'Slack hook URL for sending message to channel',
|
||||
isSecret: true,
|
||||
},
|
||||
DISCORD_WEBHOOK_URL: {
|
||||
universalIdentifier: 'f3741075-d525-4988-ba42-55d519c6fd76',
|
||||
description:
|
||||
'Discord webhook URL for sending message to channel of a server',
|
||||
isSecret: true,
|
||||
},
|
||||
FB_GRAPH_TOKEN: {
|
||||
universalIdentifier: 'fb907f49-74ac-4aa5-ba45-cfc9250ecc44',
|
||||
description: 'For Facebook auth',
|
||||
isSecret: true,
|
||||
},
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER: {
|
||||
universalIdentifier: 'c856ee5d-44bf-42f4-9a39-2553a94af518',
|
||||
description: 'Phone number for receiving WhatsApp message',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "activity-summary",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import { summariseOpportunityCreation } from './opportunity-creation-summariser';
|
||||
import { summarisePeopleCreation } from './people-creation-summariser';
|
||||
import { sendToDiscord, sendToSlack, sendToWhatsApp } from './senders';
|
||||
import { summariseTaskCreation } from './task-creation-summariser';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (): Promise<object> => {
|
||||
let date: string | Date = new Date();
|
||||
date.setDate(new Date().getDate() - Number(process.env.DAYS_AGO));
|
||||
date = date.toISOString().substring(0, 10);
|
||||
const peopleCreationSummary = await summarisePeopleCreation(date);
|
||||
const opportunityCreationSummary = await summariseOpportunityCreation(date);
|
||||
const taskCreationSummary = await summariseTaskCreation(date);
|
||||
|
||||
let body = {
|
||||
daysAgo: Number(process.env.DAYS_AGO),
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
discord: {},
|
||||
whatsapp: {},
|
||||
slack: {},
|
||||
};
|
||||
|
||||
if (process.env.SLACK_HOOK_URL) {
|
||||
const slackBody = await sendToSlack({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
slack: slackBody,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK_URL) {
|
||||
const discordBody = await sendToDiscord({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
discord: discordBody,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.FB_GRAPH_TOKEN &&
|
||||
process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER
|
||||
) {
|
||||
const whatsappBody = await sendToWhatsApp({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
whatsapp: whatsappBody,
|
||||
};
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'c5b0e3f7-cbbd-4bd6-b01c-150d52cf2ce9',
|
||||
name: 'summarise-and-send',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '36e1c4c7-8664-4d6d-a88f-ac56f1bd0651',
|
||||
type: 'cron',
|
||||
pattern: '0 9 * * *',
|
||||
},
|
||||
],
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Opportunity = {
|
||||
stage: 'NEW' | 'PROPOSAL'
|
||||
}
|
||||
|
||||
export const summariseOpportunityCreation = async (date: string) => {
|
||||
const { opportunities }: { opportunities: Opportunity[] } = await request(
|
||||
`opportunities?filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
|
||||
if (opportunities.length === 0) {
|
||||
return `- No Opportunities were added`
|
||||
}
|
||||
|
||||
const stageSummary = Object.entries(
|
||||
opportunities.reduce((hash: Record<string,number>, opportunity) => {
|
||||
if (!hash[opportunity.stage]) {
|
||||
hash[opportunity.stage] = 0
|
||||
}
|
||||
|
||||
hash[opportunity.stage] += 1
|
||||
return hash
|
||||
}, {})
|
||||
).map(([stage, value]) => `${value} in ${stage}`).join(', ')
|
||||
|
||||
return `- ${opportunities.length} Opportunities were added: ${stageSummary}`
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Person = {
|
||||
companyId: string
|
||||
company: Company
|
||||
}
|
||||
|
||||
type Company = {
|
||||
id: string
|
||||
accountOwnerId: string
|
||||
}
|
||||
|
||||
export const summarisePeopleCreation = async (date: string) => {
|
||||
const { people }: { people: Person[] } = await request(
|
||||
`people?depth=1&filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
|
||||
if (people.length === 0) {
|
||||
return '- No People were added'
|
||||
}
|
||||
|
||||
let createdForCompanies: Record<string, Company> = {}
|
||||
let numberOfAccountOwnerlessCompanies = 0
|
||||
|
||||
for (const person of people) {
|
||||
const isCompanyTracked = createdForCompanies[person.companyId]
|
||||
if (person.companyId && !isCompanyTracked) {
|
||||
createdForCompanies[person.company.id] = person.company
|
||||
|
||||
if (!person?.company?.accountOwnerId) {
|
||||
numberOfAccountOwnerlessCompanies += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `- ${people.length} People were added for ${Object.keys(createdForCompanies).length} Companies
|
||||
- Out of those, ${numberOfAccountOwnerlessCompanies} Companies don't have account owners yet`
|
||||
}
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
export const sendToSlack = async (params: {
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}) => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
|
||||
const slackMessage = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days`,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '🧑💻 People & Companies',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: peopleCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '🎯 Opportunities',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: opportunityCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '📋 Tasks',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: taskCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.SLACK_HOOK_URL ?? '', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(slackMessage),
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMessage: slackMessage,
|
||||
webhookStatus: response.status,
|
||||
};
|
||||
};
|
||||
|
||||
export const sendToDiscord = async (params: {
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}) => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
**🧑💻 People & Companies**
|
||||
${peopleCreationSummary}
|
||||
|
||||
**🎯 Opportunities**
|
||||
${opportunityCreationSummary}
|
||||
|
||||
**📋 Tasks**
|
||||
${taskCreationSummary}`;
|
||||
|
||||
const body = {
|
||||
username: 'Twenty Bot',
|
||||
content: formattedMessage,
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.DISCORD_WEBHOOK_URL ?? '', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMessage,
|
||||
webhookStatus: response.status,
|
||||
};
|
||||
};
|
||||
|
||||
export const sendToWhatsApp = async (params: {
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}): Promise<object> => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
*🧑💻 People & Companies*
|
||||
${peopleCreationSummary}
|
||||
|
||||
*🎯 Opportunities*
|
||||
${opportunityCreationSummary}
|
||||
|
||||
*📋 Tasks*
|
||||
${taskCreationSummary}`;
|
||||
|
||||
const response = await fetch(
|
||||
'https://graph.facebook.com/v22.0/828771160324576/messages',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.FB_GRAPH_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER,
|
||||
type: 'text',
|
||||
text: {
|
||||
preview_url: true,
|
||||
body: formattedMessage,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const responseBody = await response.json();
|
||||
|
||||
return {
|
||||
formattedMessage,
|
||||
webhookStatus: response.status,
|
||||
webhookResponse: responseBody,
|
||||
};
|
||||
};
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { request } from "./utils"
|
||||
|
||||
type Task = {
|
||||
status: string
|
||||
dueAt: string
|
||||
createdBy: {
|
||||
workspaceMemberId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const summariseTaskCreation = async (date: string) => {
|
||||
const { tasks }: { tasks: Task[] } = await request(
|
||||
`tasks?filter=createdAt[gte]:${date}`,
|
||||
)
|
||||
if (tasks.length === 0) {
|
||||
return '- No Tasks were added'
|
||||
}
|
||||
|
||||
const presentDateISOString = new Date().toISOString()
|
||||
const workspaceMemberIdsDueDateCounter: Record<string, number> = {}
|
||||
let workspaceMembers = []
|
||||
|
||||
tasks.forEach((task) => {
|
||||
if (task.status === 'DONE') {
|
||||
return
|
||||
}
|
||||
|
||||
if (presentDateISOString >= task.dueAt) {
|
||||
if (!workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId]) {
|
||||
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] = 0
|
||||
}
|
||||
|
||||
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] += 1
|
||||
}
|
||||
})
|
||||
|
||||
for (const { userId, count } of findMaxIncompleteKeys(workspaceMemberIdsDueDateCounter)) {
|
||||
const data = await request(`workspaceMembers/${userId}`)
|
||||
if (data.workspaceMember) {
|
||||
workspaceMembers.push({
|
||||
...data.workspaceMember,
|
||||
slackCount: count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let slackerMessage = workspaceMembers.length > 0 ? workspaceMembers.reduce((text, member, index) => {
|
||||
if (index === 0 && workspaceMembers.length === 1) {
|
||||
return `${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`
|
||||
}
|
||||
|
||||
if (index !== workspaceMembers.length - 1) {
|
||||
return text.concat(`, ${member.name.firstName}`)
|
||||
}
|
||||
|
||||
if (index === workspaceMembers.length - 1) {
|
||||
return text.concat(`, and ${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`)
|
||||
}
|
||||
}, '') : 'No one was caught slacking!'
|
||||
|
||||
const tasksCompletedOnTime = tasks.filter(
|
||||
task => task.status === 'DONE' && task.dueAt >= presentDateISOString
|
||||
)
|
||||
|
||||
const taskCompletionPercentage = tasksCompletedOnTime.length > 0
|
||||
? (tasksCompletedOnTime.length / tasks.length) * 100
|
||||
: NaN
|
||||
|
||||
const taskCompletionMessage = isNaN(taskCompletionPercentage)
|
||||
? 'No completed Tasks yet'
|
||||
: `${taskCompletionPercentage.toFixed(2)}% of Tasks were completed on time`
|
||||
|
||||
return `- ${tasks.length} Tasks were created
|
||||
- ${taskCompletionMessage}
|
||||
- ${slackerMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This is generated by AI
|
||||
*/
|
||||
const findMaxIncompleteKeys = (workspaceMemberIdsDueDateCounter: Record<string,number>) => {
|
||||
// Get the maximum incomplete count
|
||||
const max = Math.max(...Object.values(workspaceMemberIdsDueDateCounter));
|
||||
|
||||
// Filter keys that match the maximum value
|
||||
return Object.entries(workspaceMemberIdsDueDateCounter)
|
||||
.filter(([_, count]) => count === max)
|
||||
.map(([userId, count]) => ({ userId, count }))
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
export const request = async (route: string, authToken?: string) => {
|
||||
const response = await fetch(
|
||||
`https://api.twenty.com/rest/${route}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const json = await response.json()
|
||||
return json.data
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"@types/node@npm:^24.7.2":
|
||||
version: 24.9.2
|
||||
resolution: "@types/node@npm:24.9.2"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/7905d43f65cee72ef475fe76316e10bbf6ac5d08a7f0f6c38f2b6285d7ca3009e8fcafc8f8a1d2bf3f55889c9c278dbb203a9081fd0cf2d6d62161703924c6fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"activity-summary@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "activity-summary@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
twenty-sdk: "npm:0.0.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"twenty-sdk@npm:0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "twenty-sdk@npm:0.0.3"
|
||||
checksum: 10c0/0a3c85c27edb22fb50f7eb0da4f9770e85729fce05e9e0118ad0cdfc36e42425c93340a6cd1c276daf30aeeaa612db0cd905831c0a8287a31bff3da5be9b0562
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici-types@npm:7.16.0"
|
||||
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -1,19 +0,0 @@
|
||||
# Set environment values for your application here.
|
||||
# Use the format: KEY=value
|
||||
#
|
||||
# These variables are automatically loaded when running your serverless functions.
|
||||
# You can access them directly in your code using:
|
||||
# const myValue = process.env.KEY;
|
||||
#
|
||||
# To make these variables available to your application,
|
||||
# add them to package.json "env" key. This "env" key defines all
|
||||
# environment variables that will be provided to your serverless
|
||||
# functions at runtime.
|
||||
#
|
||||
# Example:
|
||||
# API_TOKEN=your-api-token
|
||||
# TIMEOUT_MS=3000
|
||||
|
||||
TWENTY_API_URL=
|
||||
TWENTY_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
@@ -1,2 +0,0 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
-942
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -1,159 +0,0 @@
|
||||
# AI Meeting Transcript
|
||||
|
||||
Automatically process meeting transcripts to extract insights, action items, and follow-ups using AI.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Transcript Processing**: Receives meeting transcripts via webhook from Granola or similar transcription tools
|
||||
- **AI-Powered Analysis**: Uses OpenAI to extract:
|
||||
- Meeting summary
|
||||
- Key discussion points
|
||||
- Action items with assignees and due dates
|
||||
- Commitments made by participants
|
||||
- **Rich Note Creation**: Creates formatted notes in Twenty with summary and key points
|
||||
- **Task Generation**: Automatically creates tasks for action items and commitments, linked to the meeting note
|
||||
|
||||
## Requirements
|
||||
|
||||
- `twenty-cli` - Install globally: `npm install -g twenty-cli`
|
||||
- `apiKey` - Go to `https://twenty.com/settings/api-webhooks` to generate one
|
||||
- `OpenAI API Key` - Get your API key from [OpenAI](https://platform.openai.com/api-keys)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Edit `.env` and replace the placeholders:
|
||||
- `<SET_YOUR_TWENTY_API>` with your Twenty API key
|
||||
- `<SET_YOUR_OPENAI_API_KEY>` with your OpenAI API key
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
4. Sync the app to your Twenty workspace:
|
||||
|
||||
```bash
|
||||
twenty auth login
|
||||
twenty app sync
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
After syncing, configure the environment variables in your Twenty workspace:
|
||||
1. Go to Settings → Apps → AI Meeting Transcript
|
||||
2. Set the following environment variables:
|
||||
- `TWENTY_API_KEY` - Your Twenty API key
|
||||
- `TWENTY_API_URL` - Your Twenty instance URL (e.g., `https://api.twenty.com` or `http://localhost:3000` for local development)
|
||||
- `OPENAI_API_KEY` - Your OpenAI API key
|
||||
|
||||
**Important**: `TWENTY_API_URL` is required and must be set to your Twenty instance URL. For local development, use `http://localhost:3000`. For production, use your actual Twenty instance URL.
|
||||
|
||||
## Usage
|
||||
|
||||
### Webhook Endpoint
|
||||
|
||||
The app exposes a public serverless route trigger.
|
||||
```
|
||||
POST /s/webhook/transcript
|
||||
```
|
||||
|
||||
Examples:
|
||||
- Local: `POST http://localhost:3000/s/webhook/transcript`
|
||||
- Hosted: `POST https://your-twenty-instance.com/s/webhook/transcript`
|
||||
|
||||
### Webhook Payload Format
|
||||
|
||||
Send a POST request with the following JSON structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"transcript": "Full meeting transcript text here...",
|
||||
"meetingTitle": "Q4 Planning Meeting",
|
||||
"meetingDate": "2024-01-15",
|
||||
"participants": ["John Doe", "Jane Smith"],
|
||||
"metadata": {
|
||||
"duration": "45 minutes",
|
||||
"location": "Conference Room A"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `transcript` (string): The full meeting transcript text
|
||||
|
||||
**Optional Fields:**
|
||||
- `meetingTitle` (string): Title of the meeting
|
||||
- `meetingDate` (string): Date of the meeting (ISO format or readable date)
|
||||
- `participants` (string[]): List of meeting participants
|
||||
- `metadata` (object): Additional metadata about the meeting
|
||||
|
||||
### Example Webhook Call
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-twenty-instance.com/s/webhook/transcript \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"transcript": "John: Let'\''s start the meeting. Today we need to discuss Q4 goals. Jane: I agree. We should focus on customer retention. John: Great point. Can you prepare a report by Friday? Jane: Yes, I will have it ready.",
|
||||
"meetingTitle": "Q4 Planning Meeting",
|
||||
"meetingDate": "2024-01-15"
|
||||
}'
|
||||
```
|
||||
|
||||
### What Happens
|
||||
|
||||
1. **Transcript Analysis**: The transcript is sent to OpenAI for analysis
|
||||
2. **Note Creation**: A formatted note is created in Twenty with:
|
||||
- Meeting summary
|
||||
- Key discussion points
|
||||
- Reference to the transcript source
|
||||
3. **Task Creation**: Tasks are automatically created for:
|
||||
- Each action item identified
|
||||
- Each commitment made by participants
|
||||
- Tasks include a reference to the meeting note ID in their description
|
||||
|
||||
## Development
|
||||
|
||||
Run dev mode to see application updates on your workspace instantly:
|
||||
|
||||
```bash
|
||||
twenty app dev
|
||||
```
|
||||
|
||||
## Integration with Granola
|
||||
|
||||
To integrate with Granola or similar transcription tools:
|
||||
|
||||
1. Set up a webhook in your transcription service
|
||||
2. Configure it to POST to: `https://your-twenty-instance.com/s/webhook/transcript`
|
||||
3. Map the transcription service's payload format to the expected format above
|
||||
|
||||
### Granola Webhook Setup
|
||||
|
||||
If using Granola, configure the webhook to send:
|
||||
- `transcript` field with the transcript text
|
||||
- Optionally include meeting metadata fields
|
||||
|
||||
## API Response
|
||||
|
||||
The webhook returns a JSON response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"noteId": "uuid-of-created-note",
|
||||
"taskIds": ["uuid-of-task-1", "uuid-of-task-2"],
|
||||
"summary": {
|
||||
"noteCreated": true,
|
||||
"tasksCreated": 2,
|
||||
"actionItemsProcessed": 1,
|
||||
"commitmentsProcessed": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,30 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '028754f1-3235-43b9-9427-fa6a62dbd473',
|
||||
displayName: 'AI Meeting Transcript',
|
||||
description:
|
||||
'Automatically process meeting transcripts to extract insights, action items, and follow-ups',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '1359d05c-4947-4673-809f-abd55bede365',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Twenty API key',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: 'dbe83355-b574-445c-92c0-5c2b94a61ddb',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Twenty API URL',
|
||||
},
|
||||
OPENAI_API_KEY: {
|
||||
universalIdentifier: '9559470d-15eb-4bc2-9cbc-3bc5c869d1fd',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'OpenAI API key for transcript analysis',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "ai-meeting-transcript",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"openai": "^4.28.0",
|
||||
"twenty-sdk": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
}
|
||||
}
|
||||
-306
@@ -1,306 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import OpenAI from 'openai';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
type TranscriptWebhookPayload = {
|
||||
transcript: string;
|
||||
meetingTitle?: string;
|
||||
meetingDate?: string;
|
||||
participants?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ActionItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
assignee?: string;
|
||||
dueDate?: string;
|
||||
};
|
||||
|
||||
type Commitment = {
|
||||
person: string;
|
||||
commitment: string;
|
||||
dueDate?: string;
|
||||
};
|
||||
|
||||
type AnalysisResult = {
|
||||
summary: string;
|
||||
keyPoints: string[];
|
||||
actionItems: ActionItem[];
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextV2Data = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
|
||||
type TwentyApiResponse = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const OPENAI_MODEL = 'gpt-4o-mini';
|
||||
const OPENAI_TEMPERATURE = 0.3;
|
||||
|
||||
const analyzeTranscript = async (
|
||||
transcript: string,
|
||||
openaiApiKey: string,
|
||||
): Promise<AnalysisResult> => {
|
||||
const openai = new OpenAI({ apiKey: openaiApiKey });
|
||||
|
||||
const prompt = `Analyze the following meeting transcript and extract:
|
||||
1. A concise summary (2-3 sentences)
|
||||
2. Key discussion points (bullet list)
|
||||
3. Action items with titles, descriptions, and any mentioned assignees or due dates
|
||||
4. Commitments made by participants with names and any mentioned due dates
|
||||
|
||||
Return the response as a JSON object with this structure:
|
||||
{
|
||||
"summary": "string",
|
||||
"keyPoints": ["string"],
|
||||
"actionItems": [{"title": "string", "description": "string", "assignee": "string (optional)", "dueDate": "string (optional)"}],
|
||||
"commitments": [{"person": "string", "commitment": "string", "dueDate": "string (optional)"}]
|
||||
}
|
||||
|
||||
Transcript:
|
||||
${transcript}`;
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: OPENAI_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are a meeting analysis assistant. Extract key insights, action items, and commitments from meeting transcripts. Always return valid JSON.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: OPENAI_TEMPERATURE,
|
||||
});
|
||||
|
||||
const content = completion.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
throw new Error('No response from OpenAI');
|
||||
}
|
||||
|
||||
return JSON.parse(content) as AnalysisResult;
|
||||
};
|
||||
|
||||
const getTwentyApiConfig = () => {
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('TWENTY_API_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
const baseUrl = process.env.TWENTY_API_URL;
|
||||
|
||||
return { apiKey, baseUrl };
|
||||
};
|
||||
|
||||
const formatNoteBody = (summary: string, keyPoints: string[]): string => {
|
||||
const keyPointsList = keyPoints.map((point) => `- ${point}`).join('\n');
|
||||
return `## Summary\n\n${summary}\n\n## Key Points\n\n${keyPointsList}\n\n*Generated from meeting transcript*`;
|
||||
};
|
||||
|
||||
const createNoteInTwenty = async (
|
||||
summary: string,
|
||||
keyPoints: string[],
|
||||
meetingTitle?: string,
|
||||
meetingDate?: string,
|
||||
): Promise<TwentyApiResponse> => {
|
||||
const { apiKey, baseUrl } = getTwentyApiConfig();
|
||||
const noteTitle =
|
||||
meetingTitle ||
|
||||
`Meeting Notes - ${meetingDate || new Date().toLocaleDateString()}`;
|
||||
const noteBodyMarkdown = formatNoteBody(summary, keyPoints);
|
||||
|
||||
const requestData = {
|
||||
title: noteTitle,
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextV2Data,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await axios.post<TwentyApiResponse>(
|
||||
`${baseUrl}/rest/notes`,
|
||||
requestData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
const status = error.response?.status;
|
||||
throw new Error(
|
||||
`Failed to create note: ${errorMessage}. Status: ${status}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const createTaskInTwenty = async (
|
||||
actionItem: ActionItem,
|
||||
): Promise<TwentyApiResponse> => {
|
||||
const { apiKey, baseUrl } = getTwentyApiConfig();
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextV2Data;
|
||||
dueAt?: string;
|
||||
} = {
|
||||
title: actionItem.title,
|
||||
bodyV2: {
|
||||
markdown: actionItem.description,
|
||||
blocknote: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (actionItem.dueDate) {
|
||||
taskData.dueAt = new Date(actionItem.dueDate).toISOString();
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post<TwentyApiResponse>(
|
||||
`${baseUrl}/rest/tasks`,
|
||||
taskData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
const status = error.response?.status;
|
||||
throw new Error(
|
||||
`Failed to create task "${actionItem.title}": ${errorMessage}. Status: ${status}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const createTasksFromActionItems = async (
|
||||
actionItems: ActionItem[],
|
||||
noteId: string,
|
||||
): Promise<string[]> => {
|
||||
const taskIds: string[] = [];
|
||||
|
||||
for (const actionItem of actionItems) {
|
||||
try {
|
||||
const taskDescription = `${actionItem.description}\n\n*Related to meeting note: ${noteId}*`;
|
||||
const task = await createTaskInTwenty({
|
||||
...actionItem,
|
||||
description: taskDescription,
|
||||
});
|
||||
taskIds.push(task.id);
|
||||
} catch (error) {
|
||||
// Task creation failed, continue with next task
|
||||
}
|
||||
}
|
||||
|
||||
return taskIds;
|
||||
};
|
||||
|
||||
const createTasksFromCommitments = async (
|
||||
commitments: Commitment[],
|
||||
noteId: string,
|
||||
): Promise<string[]> => {
|
||||
const taskIds: string[] = [];
|
||||
|
||||
for (const commitment of commitments) {
|
||||
try {
|
||||
const taskDescription = `Commitment from ${commitment.person}: ${commitment.commitment}\n\n*Related to meeting note: ${noteId}*`;
|
||||
const task = await createTaskInTwenty({
|
||||
title: `Follow up: ${commitment.commitment}`,
|
||||
description: taskDescription,
|
||||
dueDate: commitment.dueDate,
|
||||
});
|
||||
taskIds.push(task.id);
|
||||
} catch (error) {
|
||||
// Commitment task creation failed, continue with next commitment
|
||||
}
|
||||
}
|
||||
|
||||
return taskIds;
|
||||
};
|
||||
|
||||
export const main = async (
|
||||
params: TranscriptWebhookPayload,
|
||||
): Promise<object> => {
|
||||
const { transcript, meetingTitle, meetingDate } = params;
|
||||
|
||||
if (!transcript || typeof transcript !== 'string') {
|
||||
throw new Error('Transcript is required and must be a string');
|
||||
}
|
||||
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
if (!openaiApiKey) {
|
||||
throw new Error('OPENAI_API_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
const analysis = await analyzeTranscript(transcript, openaiApiKey);
|
||||
|
||||
const note = await createNoteInTwenty(
|
||||
analysis.summary,
|
||||
analysis.keyPoints,
|
||||
meetingTitle,
|
||||
meetingDate,
|
||||
);
|
||||
|
||||
const actionItemTaskIds = await createTasksFromActionItems(
|
||||
analysis.actionItems,
|
||||
note.id,
|
||||
);
|
||||
const commitmentTaskIds = await createTasksFromCommitments(
|
||||
analysis.commitments,
|
||||
note.id,
|
||||
);
|
||||
|
||||
const allTaskIds = [...actionItemTaskIds, ...commitmentTaskIds];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId: note.id,
|
||||
taskIds: allTaskIds,
|
||||
summary: {
|
||||
noteCreated: true,
|
||||
tasksCreated: allTaskIds.length,
|
||||
actionItemsProcessed: analysis.actionItems.length,
|
||||
commitmentsProcessed: analysis.commitments.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'dae52ab2-174f-4f81-a031-604ee2e81eba',
|
||||
name: 'ai-meeting-transcriptor',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'b011303d-2c24-44d4-9923-55eb060a1ff6',
|
||||
type: 'route',
|
||||
path: '/webhook/transcript',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"@types/node-fetch@npm:^2.6.4":
|
||||
version: 2.6.13
|
||||
resolution: "@types/node-fetch@npm:2.6.13"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
form-data: "npm:^4.0.4"
|
||||
checksum: 10c0/6313c89f62c50bd0513a6839cdff0a06727ac5495ccbb2eeda51bb2bbbc4f3c0a76c0393a491b7610af703d3d2deb6cf60e37e59c81ceeca803ffde745dbf309
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:^24.7.2":
|
||||
version: 24.9.2
|
||||
resolution: "@types/node@npm:24.9.2"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/7905d43f65cee72ef475fe76316e10bbf6ac5d08a7f0f6c38f2b6285d7ca3009e8fcafc8f8a1d2bf3f55889c9c278dbb203a9081fd0cf2d6d62161703924c6fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18":
|
||||
version: 18.19.130
|
||||
resolution: "@types/node@npm:18.19.130"
|
||||
dependencies:
|
||||
undici-types: "npm:~5.26.4"
|
||||
checksum: 10c0/22ba2bc9f8863101a7e90a56aaeba1eb3ebdc51e847cef4a6d188967ab1acbce9b4f92251372fd0329ecb924bbf610509e122c3dfe346c04dbad04013d4ad7d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abort-controller@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "abort-controller@npm:3.0.0"
|
||||
dependencies:
|
||||
event-target-shim: "npm:^5.0.0"
|
||||
checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agentkeepalive@npm:^4.2.1":
|
||||
version: 4.6.0
|
||||
resolution: "agentkeepalive@npm:4.6.0"
|
||||
dependencies:
|
||||
humanize-ms: "npm:^1.2.1"
|
||||
checksum: 10c0/235c182432f75046835b05f239708107138a40103deee23b6a08caee5136873709155753b394ec212e49e60e94a378189562cb01347765515cff61b692c69187
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ai-meeting-transcript@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "ai-meeting-transcript@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
axios: "npm:^1.12.2"
|
||||
openai: "npm:^4.28.0"
|
||||
twenty-sdk: "npm:^0.0.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"async-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-function@npm:1.0.0"
|
||||
checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-generator-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-generator-function@npm:1.0.0"
|
||||
checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asynckit@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "asynckit@npm:0.4.0"
|
||||
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.12.2":
|
||||
version: 1.13.1
|
||||
resolution: "axios@npm:1.13.1"
|
||||
dependencies:
|
||||
follow-redirects: "npm:^1.15.6"
|
||||
form-data: "npm:^4.0.4"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
checksum: 10c0/de9c3c6de43d3ee1146d3afe78645f19450cac6a5d7235bef8b8e8eeb705c2e47e2d231dea99cecaec4dae1897c521118ca9413b9d474063c719c4d94c5b9adc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
||||
dependencies:
|
||||
es-errors: "npm:^1.3.0"
|
||||
function-bind: "npm:^1.1.2"
|
||||
checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"combined-stream@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "combined-stream@npm:1.0.8"
|
||||
dependencies:
|
||||
delayed-stream: "npm:~1.0.0"
|
||||
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delayed-stream@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delayed-stream@npm:1.0.0"
|
||||
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dunder-proto@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "dunder-proto@npm:1.0.1"
|
||||
dependencies:
|
||||
call-bind-apply-helpers: "npm:^1.0.1"
|
||||
es-errors: "npm:^1.3.0"
|
||||
gopd: "npm:^1.2.0"
|
||||
checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-define-property@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "es-define-property@npm:1.0.1"
|
||||
checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-errors@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "es-errors@npm:1.3.0"
|
||||
checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "es-object-atoms@npm:1.1.1"
|
||||
dependencies:
|
||||
es-errors: "npm:^1.3.0"
|
||||
checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-set-tostringtag@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "es-set-tostringtag@npm:2.1.0"
|
||||
dependencies:
|
||||
es-errors: "npm:^1.3.0"
|
||||
get-intrinsic: "npm:^1.2.6"
|
||||
has-tostringtag: "npm:^1.0.2"
|
||||
hasown: "npm:^2.0.2"
|
||||
checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"follow-redirects@npm:^1.15.6":
|
||||
version: 1.15.11
|
||||
resolution: "follow-redirects@npm:1.15.11"
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data-encoder@npm:1.7.2":
|
||||
version: 1.7.2
|
||||
resolution: "form-data-encoder@npm:1.7.2"
|
||||
checksum: 10c0/56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data@npm:^4.0.4":
|
||||
version: 4.0.4
|
||||
resolution: "form-data@npm:4.0.4"
|
||||
dependencies:
|
||||
asynckit: "npm:^0.4.0"
|
||||
combined-stream: "npm:^1.0.8"
|
||||
es-set-tostringtag: "npm:^2.1.0"
|
||||
hasown: "npm:^2.0.2"
|
||||
mime-types: "npm:^2.1.12"
|
||||
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formdata-node@npm:^4.3.2":
|
||||
version: 4.4.1
|
||||
resolution: "formdata-node@npm:4.4.1"
|
||||
dependencies:
|
||||
node-domexception: "npm:1.0.0"
|
||||
web-streams-polyfill: "npm:4.0.0-beta.3"
|
||||
checksum: 10c0/74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"function-bind@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "function-bind@npm:1.1.2"
|
||||
checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"generator-function@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "generator-function@npm:2.0.1"
|
||||
checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-intrinsic@npm:^1.2.6":
|
||||
version: 1.3.1
|
||||
resolution: "get-intrinsic@npm:1.3.1"
|
||||
dependencies:
|
||||
async-function: "npm:^1.0.0"
|
||||
async-generator-function: "npm:^1.0.0"
|
||||
call-bind-apply-helpers: "npm:^1.0.2"
|
||||
es-define-property: "npm:^1.0.1"
|
||||
es-errors: "npm:^1.3.0"
|
||||
es-object-atoms: "npm:^1.1.1"
|
||||
function-bind: "npm:^1.1.2"
|
||||
generator-function: "npm:^2.0.0"
|
||||
get-proto: "npm:^1.0.1"
|
||||
gopd: "npm:^1.2.0"
|
||||
has-symbols: "npm:^1.1.0"
|
||||
hasown: "npm:^2.0.2"
|
||||
math-intrinsics: "npm:^1.1.0"
|
||||
checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-proto@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "get-proto@npm:1.0.1"
|
||||
dependencies:
|
||||
dunder-proto: "npm:^1.0.1"
|
||||
es-object-atoms: "npm:^1.0.0"
|
||||
checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "gopd@npm:1.2.0"
|
||||
checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "has-symbols@npm:1.1.0"
|
||||
checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-tostringtag@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "has-tostringtag@npm:1.0.2"
|
||||
dependencies:
|
||||
has-symbols: "npm:^1.0.3"
|
||||
checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hasown@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "hasown@npm:2.0.2"
|
||||
dependencies:
|
||||
function-bind: "npm:^1.1.2"
|
||||
checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"humanize-ms@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "humanize-ms@npm:1.2.1"
|
||||
dependencies:
|
||||
ms: "npm:^2.0.0"
|
||||
checksum: 10c0/f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"math-intrinsics@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "math-intrinsics@npm:1.1.0"
|
||||
checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-db@npm:1.52.0":
|
||||
version: 1.52.0
|
||||
resolution: "mime-db@npm:1.52.0"
|
||||
checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:^2.1.12":
|
||||
version: 2.1.35
|
||||
resolution: "mime-types@npm:2.1.35"
|
||||
dependencies:
|
||||
mime-db: "npm:1.52.0"
|
||||
checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.0.0":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-domexception@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "node-domexception@npm:1.0.0"
|
||||
checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.7":
|
||||
version: 2.7.0
|
||||
resolution: "node-fetch@npm:2.7.0"
|
||||
dependencies:
|
||||
whatwg-url: "npm:^5.0.0"
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openai@npm:^4.28.0":
|
||||
version: 4.104.0
|
||||
resolution: "openai@npm:4.104.0"
|
||||
dependencies:
|
||||
"@types/node": "npm:^18.11.18"
|
||||
"@types/node-fetch": "npm:^2.6.4"
|
||||
abort-controller: "npm:^3.0.0"
|
||||
agentkeepalive: "npm:^4.2.1"
|
||||
form-data-encoder: "npm:1.7.2"
|
||||
formdata-node: "npm:^4.3.2"
|
||||
node-fetch: "npm:^2.6.7"
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
bin:
|
||||
openai: bin/cli
|
||||
checksum: 10c0/c4f2e837684ed96b8cec58c65a584646d667c69918f29052775e2e8c05ff5c860d8b58214a7770bc6895ca8602480420c1db6a5392dd250179eb0b91c2b19a2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proxy-from-env@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "proxy-from-env@npm:1.1.0"
|
||||
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tr46@npm:~0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "tr46@npm:0.0.3"
|
||||
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:^0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "twenty-sdk@npm:0.0.2"
|
||||
checksum: 10c0/99e6fe86059d847b548c1f03e0f0c59a4d540caf1d28dd4500f1f5f0094196985ded955801274de9e72ff03e3d1f41e9a509b4c2c5a02ffc8a027277b1e35d8e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~5.26.4":
|
||||
version: 5.26.5
|
||||
resolution: "undici-types@npm:5.26.5"
|
||||
checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici-types@npm:7.16.0"
|
||||
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:4.0.0-beta.3":
|
||||
version: 4.0.0-beta.3
|
||||
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
|
||||
checksum: 10c0/a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-url@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "whatwg-url@npm:5.0.0"
|
||||
dependencies:
|
||||
tr46: "npm:~0.0.3"
|
||||
webidl-conversions: "npm:^3.0.0"
|
||||
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -1,109 +0,0 @@
|
||||
# Fireflies Integration Environment Variables
|
||||
# Copy this file to .env and fill in your actual values
|
||||
|
||||
# =============================================================================
|
||||
# REQUIRED: Authentication & API Keys
|
||||
# =============================================================================
|
||||
|
||||
# Secret key for HMAC signature verification of Fireflies webhooks
|
||||
# This must match the secret configured in your Fireflies webhook settings
|
||||
# Get this from: https://app.fireflies.ai/settings#DeveloperSettings
|
||||
FIREFLIES_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
|
||||
# Fireflies API key for fetching meeting data from GraphQL API
|
||||
# Get this from: https://app.fireflies.ai/settings#DeveloperSettings
|
||||
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=free
|
||||
|
||||
# Twenty CRM API key for authentication
|
||||
# Generate this from your Twenty instance: Settings > Developers > API Keys
|
||||
TWENTY_API_KEY=your_twenty_api_key_here
|
||||
|
||||
# =============================================================================
|
||||
# Server Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Twenty CRM server URL
|
||||
# Use http://localhost:3000 for local development
|
||||
# Use your production URL for deployed instances
|
||||
SERVER_URL=http://localhost:3000
|
||||
|
||||
# =============================================================================
|
||||
# Contact Management
|
||||
# =============================================================================
|
||||
|
||||
# Automatically create contacts for unknown participants (true/false)
|
||||
# If true, new Person records will be created for meeting participants not found in CRM
|
||||
# If false, only meetings with existing contacts will be fully processed
|
||||
AUTO_CREATE_CONTACTS=true
|
||||
|
||||
# =============================================================================
|
||||
# Summary Processing Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Strategy for handling async summary generation
|
||||
# Options:
|
||||
# - immediate_with_retry: Attempts immediate fetch with retry logic (RECOMMENDED)
|
||||
# - delayed_polling: Schedules background polling for summaries
|
||||
# - basic_only: Creates records without waiting for summaries
|
||||
FIREFLIES_SUMMARY_STRATEGY=immediate_with_retry
|
||||
|
||||
# Number of retry attempts when fetching summary data
|
||||
# Used with immediate_with_retry strategy
|
||||
# Recommended: 3-5 attempts (summaries can take up to 10 minutes but rate limit is low)
|
||||
FIREFLIES_RETRY_ATTEMPTS=5
|
||||
|
||||
# Delay in milliseconds between retry attempts (with exponential backoff)
|
||||
# Each retry will wait: RETRY_DELAY * attempt_number
|
||||
# Example: 120000ms means 2min, 4min, 6min... for extended backoff
|
||||
# Total max time with 5 attempts: ~15 minutes
|
||||
FIREFLIES_RETRY_DELAY=120000
|
||||
|
||||
# Polling interval in milliseconds for delayed_polling strategy
|
||||
# How often to check if summary is ready
|
||||
# Recommended: 60000 (60 seconds) for extended processing
|
||||
FIREFLIES_POLL_INTERVAL=120000
|
||||
|
||||
# Maximum number of polling attempts for delayed_polling strategy
|
||||
# Total max time = POLL_INTERVAL * MAX_POLLS
|
||||
# Example: 2min * 5 = 10 minutes maximum
|
||||
FIREFLIES_MAX_POLLS=5
|
||||
|
||||
# =============================================================================
|
||||
# Debugging & Logging
|
||||
# =============================================================================
|
||||
|
||||
# Log level: silent, error, warn, info, debug (default: error)
|
||||
# Controls verbosity of console output
|
||||
# - silent: No console output
|
||||
# - error: Only errors (production default)
|
||||
# - warn: Warnings and errors
|
||||
# - info: Info, warnings, and errors
|
||||
# - debug: All logs including detailed debugging
|
||||
LOG_LEVEL=error
|
||||
|
||||
# =============================================================================
|
||||
# Configuration Notes
|
||||
# =============================================================================
|
||||
#
|
||||
# Webhook Setup:
|
||||
# 1. Configure your Fireflies webhook at: https://app.fireflies.ai/settings#DeveloperSettings
|
||||
# 2. Webhook URL: https://your-twenty-instance.com/s/webhook/fireflies
|
||||
# 3. Event Type: "Transcription completed"
|
||||
# 4. Secret: Same value as FIREFLIES_WEBHOOK_SECRET above, genereate it there
|
||||
#
|
||||
# Summary Strategy Guide:
|
||||
# - immediate_with_retry: Best for most use cases - fast with reliability
|
||||
# - delayed_polling: Use if your server is heavily loaded
|
||||
# - basic_only: Use if you only need transcript links without AI summaries
|
||||
#
|
||||
# Performance Tuning:
|
||||
# - Fireflies summaries can take 5-15 minutes to generate after transcription
|
||||
# - Use 30+ retry attempts with 30s delay for 15-minute coverage
|
||||
# - Consider delayed_polling strategy for heavily loaded servers
|
||||
# - Monitor DEBUG_LOGS to tune timing for your Fireflies account
|
||||
#
|
||||
@@ -1 +0,0 @@
|
||||
.yarn
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -1,167 +0,0 @@
|
||||
# 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 <meetingId>` 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 <meetingId>` 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
|
||||
- **Enhanced logging system**: Introduced configurable `AppLogger` class with log level support (debug, info, warn, error, silent)
|
||||
- Environment-based log level configuration via `LOG_LEVEL` environment variable
|
||||
- Test environment detection to prevent log noise during testing
|
||||
- Context-aware logging with proper prefixes for better debugging
|
||||
- **Improved error handling**: Enhanced webhook signature verification with detailed debug logging
|
||||
- **Better debugging capabilities**: Added comprehensive logging throughout webhook processing pipeline
|
||||
|
||||
### Enhanced
|
||||
- **Webhook signature verification**: Improved signature validation with detailed logging for troubleshooting
|
||||
- **Error messages**: More descriptive error logging for failed operations and security violations
|
||||
- **Development experience**: Better debugging information for webhook processing and API interactions
|
||||
|
||||
|
||||
## [0.2.1] - 2025-11-03
|
||||
|
||||
### Added
|
||||
- **Import status tracking**: Added four new meeting fields to track import status and failure handling:
|
||||
- `importStatus` (SELECT) - Tracks SUCCESS, FAILED, PENDING, RETRYING states
|
||||
- `importError` (TEXT) - Stores error messages when imports fail
|
||||
- `lastImportAttempt` (DATE_TIME) - Timestamp of the last import attempt
|
||||
- `importAttempts` (NUMBER) - Counter for number of import attempts
|
||||
- **Automatic failure tracking**: Enhanced webhook handler to automatically create failed meeting records when processing fails
|
||||
- **Failed meeting formatter**: Added `toFailedMeetingCreateInput()` method to create standardized failed meeting records
|
||||
|
||||
### Enhanced
|
||||
- **Meeting type definition**: Extended `MeetingCreateInput` type with import tracking fields
|
||||
- **Success status tracking**: Successful meeting imports now automatically set `importStatus: 'SUCCESS'` and track timestamps
|
||||
- **Error handling**: Webhook processing failures are now captured and stored as meeting records for visibility and potential retry
|
||||
|
||||
## [0.2.0] - 2025-11-03
|
||||
|
||||
### Changed
|
||||
- **Major refactoring**: Split monolithic `receive-fireflies-notes.ts` into modular architecture:
|
||||
- `fireflies-api-client.ts` - Fireflies GraphQL API integration with retry logic
|
||||
- `twenty-crm-service.ts` - Twenty CRM operations (contacts, notes, meetings)
|
||||
- `formatters.ts` - Meeting and note body formatting
|
||||
- `webhook-handler.ts` - Main webhook orchestration
|
||||
- `webhook-validator.ts` - HMAC signature verification
|
||||
- `utils.ts` - Shared utility functions
|
||||
- `types.ts` - Centralized type definitions
|
||||
- **Schema update**: Changed Meeting `notes` field from `RICH_TEXT` to `RELATION` type linking to Note object
|
||||
- Enhanced participant extraction from multiple Fireflies API data sources (participants, meeting_attendees, speakers, meeting_attendance)
|
||||
- Improved organizer email matching with name-based heuristics
|
||||
- Updated note creation to use `bodyV2.markdown` format instead of legacy `body` field
|
||||
- Modernized Meeting object schema with proper link field types for transcriptUrl and recordingUrl
|
||||
- Enhanced test suite with improved mocking for new modular structure
|
||||
- **Configuration optimization**: Reduced default retry attempts from 30 to 5 with increased delay (120s) to better respect Fireflies API rate limits (50 requests/day for free/pro plans)
|
||||
- Updated field setup script to support relation field creation with Note object
|
||||
- Restructured exports: types now exported from `types.ts`, runtime functions from `index.ts`
|
||||
- Updated import paths in action handlers to use centralized index exports
|
||||
- Added TypeScript path mappings for `twenty-sdk` in workspace configuration
|
||||
|
||||
### Added
|
||||
- `createNoteTarget` method for linking notes to multiple participants
|
||||
- Support for extracting participants from extended Fireflies API response formats
|
||||
- Better organizer identification logic matching email usernames to speaker names
|
||||
- `axios` dependency for improved HTTP client capabilities
|
||||
- API subscription plan documentation highlighting rate limit differences (50/day vs 60/minute)
|
||||
- Enhanced README with rate limiting guidance and configuration documentation
|
||||
- Relation field creation support in field provisioning script
|
||||
|
||||
### Fixed
|
||||
- Note linking now properly associates a single note with multiple participants in 1:1 meetings
|
||||
- Participant extraction handles missing email addresses gracefully
|
||||
- Improved handling of various Fireflies participant data structures
|
||||
- Test mocks updated to use string format for participants (`"Name <email>"`) matching Fireflies API response format
|
||||
- Test assertions updated to validate `bodyV2.markdown` instead of deprecated `body` field
|
||||
|
||||
## [0.1.0] - 2025-11-02
|
||||
|
||||
### Added
|
||||
- HMAC SHA-256 signature verification for incoming Fireflies webhooks
|
||||
- Fireflies GraphQL client with retry logic, timeout handling, and summary readiness detection
|
||||
- Summary-focused meeting processing that extracts action items, sentiment, keywords, and transcript/recording links
|
||||
- Scripted custom field provisioning via `yarn setup:fields`
|
||||
- Local webhook testing workflow via `yarn test:webhook`
|
||||
- Comprehensive Jest suite (15 tests) covering authentication, API integration, summary strategies, and error handling
|
||||
|
||||
### Changed
|
||||
- Replaced legacy JSON manifests with TypeScript configuration:
|
||||
- `application.config.ts` now declares app metadata and configuration variables
|
||||
- `src/objects/meeting.ts` defines the Meeting object via `@ObjectMetadata`
|
||||
- `src/actions/receive-fireflies-notes.ts` exports the Fireflies webhook action plus its runtime config
|
||||
- Updated documentation (README, Deployment Guide, Testing) to reflect the new project layout and workflows
|
||||
- Switched utility scripts to `tsx` and aligned package management with the hello-world example
|
||||
|
||||
### Fixed
|
||||
- Resolved real-world Fireflies payload mismatch by adopting the minimal webhook schema
|
||||
- Replaced body-based secrets with header-driven HMAC verification
|
||||
- Ensured graceful degradation when summaries are pending or Fireflies is temporarily unavailable
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
# Fireflies
|
||||
|
||||
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**
|
||||
|
||||
- **Summary-first approach** - Prioritizes action items, keywords, and sentiment over raw transcripts
|
||||
- **HMAC signature verification** - Secure webhook authentication
|
||||
- **Two-phase architecture** - Webhook notification → API data fetch → CRM record creation
|
||||
- **Contact identification** - Matches participants to existing contacts or creates new ones
|
||||
- **One-on-one meetings** (2 people) → Individual notes linked to each contact
|
||||
- **Multi-party meetings** (3+ people) → Meeting records with all attendees
|
||||
- **Business intelligence extraction** - Action items, sentiment scores, topics, meeting types
|
||||
- **Smart retry logic** - Handles async summary generation with exponential backoff
|
||||
- **Links transcripts and recordings** - Easy access to full Fireflies content
|
||||
- **Duplicate prevention** - Checks for existing meetings by title
|
||||
|
||||
## API Access by Subscription Plan
|
||||
|
||||
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/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)** | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
### What You'll Get Per Plan
|
||||
|
||||
**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
|
||||
|
||||
### Summary & Insights
|
||||
- **Action Items** - Concrete next steps and commitments
|
||||
- **Keywords** - Key topics and themes discussed
|
||||
- **Overview** - Executive summary of the meeting
|
||||
- **Topics Discussed** - Main discussion points
|
||||
- **Meeting Type** - Context (sales call, standup, demo, etc.)
|
||||
|
||||
### Analytics
|
||||
- **Sentiment Analysis** - Positive/negative/neutral percentages for deal health
|
||||
- **Engagement Metrics** - Participation levels (future)
|
||||
|
||||
### Resources
|
||||
- **Transcript Link** - Quick access to full Fireflies transcript
|
||||
- **Recording Link** - Video/audio recording when available
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Step 1: Authenticate with Twenty
|
||||
npx twenty-cli auth login
|
||||
|
||||
# Step 2: Sync the app to create Meeting object
|
||||
npx twenty-cli app sync packages/twenty-apps/fireflies
|
||||
|
||||
# Step 3: Install dependencies
|
||||
yarn install
|
||||
|
||||
# Step 4: Add custom fields
|
||||
yarn setup:fields
|
||||
```
|
||||
|
||||
(TODO: change when fields setup internal support)
|
||||
|
||||
### Configuration
|
||||
|
||||
⚠️ **Important**: The integration uses **conservative retry settings** to respect Fireflies' 50 requests/day API limit with free/pro plans. You may increase for more reactivity with higher plans.
|
||||
|
||||
**Required Environment Variables:**
|
||||
```bash
|
||||
FIREFLIES_API_KEY=your_api_key # From Fireflies settings
|
||||
TWENTY_API_KEY=your_api_key # From Twenty CRM settings
|
||||
SERVER_URL=https://your-domain.twenty.com
|
||||
```
|
||||
|
||||
**Optional (Recommended):**
|
||||
```bash
|
||||
FIREFLIES_WEBHOOK_SECRET=your_secret # For webhook security
|
||||
```
|
||||
|
||||
📖 **For detailed configuration, troubleshooting, and rate limit management**, see [WEBHOOK_CONFIGURATION.md](./WEBHOOK_CONFIGURATION.md)
|
||||
|
||||
### What Gets Created
|
||||
|
||||
#### Basic Installation (Step 2)
|
||||
The `app sync` command creates:
|
||||
- ✅ Meeting object with basic `name` field
|
||||
- ✅ Webhook endpoint at `/s/webhook/fireflies`
|
||||
|
||||
#### After Custom Fields Setup (Step 4)
|
||||
The `setup:fields` script adds 13 custom fields to store rich Fireflies data:
|
||||
|
||||
| Field Name | Type | Label | Description |
|
||||
|------------|------|-------|-------------|
|
||||
| `notes` | RICH_TEXT | Meeting Notes | AI-generated summary with overview, topics, action items, and insights |
|
||||
| `meetingDate` | DATE_TIME | Meeting Date | Date and time when the meeting occurred |
|
||||
| `duration` | NUMBER | Duration (minutes) | Meeting duration in minutes |
|
||||
| `meetingType` | TEXT | Meeting Type | Type of meeting (e.g., Sales Call, Sprint Planning, 1:1) |
|
||||
| `keywords` | TEXT | Keywords | Key topics and themes discussed (comma-separated) |
|
||||
| `sentimentScore` | NUMBER | Sentiment Score | Overall meeting sentiment (0-1 scale, 1 = most positive) |
|
||||
| `positivePercent` | NUMBER | Positive % | Percentage of positive sentiment in conversation |
|
||||
| `negativePercent` | NUMBER | Negative % | Percentage of negative sentiment in conversation |
|
||||
| `actionItemsCount` | NUMBER | Action Items | Number of action items identified |
|
||||
| `transcriptUrl` | LINKS | Transcript URL | Link to full transcript in Fireflies |
|
||||
| `recordingUrl` | LINKS | Recording URL | Link to video/audio recording in Fireflies |
|
||||
| `firefliesMeetingId` | TEXT | Fireflies Meeting ID | Unique identifier from Fireflies |
|
||||
| `organizerEmail` | TEXT | Organizer Email | Email address of the meeting organizer |
|
||||
|
||||
**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
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
Check [.env.example](./.env.example)
|
||||
|
||||
### Summary Processing Strategies
|
||||
|
||||
| Strategy | Description | Use Case |
|
||||
|----------|-------------|----------|
|
||||
| `immediate_only` | Single fetch attempt, no retries | Fast processing, accept missing summaries if not ready |
|
||||
| `immediate_with_retry` | Attempts immediate fetch, retries with backoff | **Recommended** - Balances speed and reliability |
|
||||
| `delayed_polling` | Schedules background polling | For heavily loaded systems |
|
||||
| `basic_only` | Creates records without waiting for summaries | For basic transcript archival only |
|
||||
|
||||
## Webhook Setup
|
||||
|
||||
### Step 1: Get Your Webhook URL
|
||||
|
||||
Your webhook endpoint will be:
|
||||
```
|
||||
https://your-twenty-instance.com/s/webhook/fireflies
|
||||
```
|
||||
|
||||
### Step 2: Configure Fireflies Webhook
|
||||
|
||||
1. Log into Fireflies.ai
|
||||
2. https://app.fireflies.ai/settings#DeveloperSettings
|
||||
4. Enter your webhook URL
|
||||
5. Set **Secret**: Generate from there and set value of `FIREFLIES_WEBHOOK_SECRET`
|
||||
6. Save configuration
|
||||
|
||||
### Step 3: Verify Webhook
|
||||
|
||||
The integration uses **HMAC SHA-256 signature verification**:
|
||||
- Fireflies sends `x-hub-signature` header
|
||||
- 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 <meetingId>` 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 <channelId>] [--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
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test -- --watch
|
||||
|
||||
# Development mode with live sync
|
||||
npx twenty-cli app dev
|
||||
|
||||
# Type checking
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The integration includes comprehensive test coverage:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run specific test suite
|
||||
npm test -- fireflies-webhook.spec.ts
|
||||
|
||||
# Run with coverage
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- HMAC signature verification
|
||||
- Fireflies GraphQL API integration
|
||||
- Summary processing with retry logic
|
||||
- Summary-focused CRM record creation
|
||||
- One-on-one vs multi-party meeting detection
|
||||
- Contact matching and creation
|
||||
- Duplicate prevention
|
||||
- Error handling and resilience
|
||||
|
||||
## CRM Record Structure
|
||||
|
||||
### One-on-One Meeting Note Example
|
||||
|
||||
```markdown
|
||||
# Meeting: Product Demo with Client (Sales Call)
|
||||
|
||||
**Date:** Monday, November 2, 2024, 02:00 PM
|
||||
**Duration:** 30 minutes
|
||||
**Participants:** Sarah Sales, John Client
|
||||
|
||||
## Overview
|
||||
Successful product demonstration with positive client feedback.
|
||||
Client expressed strong interest in the enterprise plan.
|
||||
|
||||
## Key Topics
|
||||
- product features
|
||||
- pricing discussion
|
||||
- integration capabilities
|
||||
- support options
|
||||
|
||||
## Action Items
|
||||
- Follow up with pricing proposal by Friday
|
||||
- Schedule technical deep-dive next week
|
||||
- Share case studies from similar clients
|
||||
|
||||
## Insights
|
||||
**Keywords:** product demo, pricing, technical requirements, integration
|
||||
**Sentiment:** 75% positive, 10% negative, 15% neutral
|
||||
**Meeting Type:** Sales Call
|
||||
|
||||
## Resources
|
||||
[View Full Transcript](https://app.fireflies.ai/transcript/xxx)
|
||||
[Watch Recording](https://app.fireflies.ai/recording/xxx)
|
||||
```
|
||||
|
||||
### Multi-Party Meeting Record
|
||||
|
||||
- Meeting object with title, date, and all attendees
|
||||
- Summary stored as meeting notes (structure same as above)
|
||||
- Action items potentially converted to separate tasks (future)
|
||||
- Keywords as tags/categories (future)
|
||||
|
||||
## Future Implementation Opportunities
|
||||
|
||||
Next iterations would enhance the **intelligence layer** to:
|
||||
|
||||
### AI-Powered Insights
|
||||
- **Extract pain points, objections & buying signals** automatically from transcripts
|
||||
- **Calculate deal health scores** based on conversation sentiment trends
|
||||
- **Auto-create contextualized tasks** with AI-suggested next steps and priorities
|
||||
- **Proactively flag at-risk deals** when negative signals appear
|
||||
- **Track conversation patterns** that correlate with deal success
|
||||
|
||||
### Enhanced Analytics
|
||||
- **Action item completion tracking** across deals
|
||||
- **Sentiment trend analysis** over time for account health
|
||||
- **Speaking time analysis** for meeting engagement insights
|
||||
- **Topic clustering** for product/feature interest patterns
|
||||
|
||||
### Workflow Automation
|
||||
- **Auto-assign follow-up tasks** based on action items
|
||||
- **Smart notifications** for urgent follow-ups
|
||||
- **Deal stage progression** based on meeting outcomes
|
||||
- **Competitive intelligence** extraction from conversations
|
||||
|
||||
**Integration**: Fireflies webhook → AI processing layer → Enhanced Twenty records
|
||||
|
||||
*This would require the current MVP to be stabilized and discussions about intelligence layer architecture and data privacy considerations.*
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: 'a4df0c0f-c65e-44e5-8436-24814182d4ac',
|
||||
displayName: 'Fireflies',
|
||||
description: 'Sync Fireflies meeting summaries, sentiment, and action items into Twenty.',
|
||||
icon: 'IconMicrophone',
|
||||
applicationVariables: {
|
||||
FIREFLIES_WEBHOOK_SECRET: {
|
||||
universalIdentifier: 'f51f7646-be9f-4ba9-9b75-160dd288cd0c',
|
||||
description: 'Secret key for verifying Fireflies webhook signatures',
|
||||
//isSecret: true,
|
||||
value: '',
|
||||
},
|
||||
FIREFLIES_API_KEY: {
|
||||
universalIdentifier: 'faa41f07-b28e-4500-b1c0-ce4b3d27924c',
|
||||
description: 'Fireflies GraphQL API key used to fetch meeting summaries',
|
||||
//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,
|
||||
value: '',
|
||||
},
|
||||
SERVER_URL: {
|
||||
universalIdentifier: '9b3a5e8e-5973-4e6b-a059-2966075652aa',
|
||||
description: 'Base URL for the Twenty workspace (default: http://localhost:3000)',
|
||||
value: 'http://localhost:3000',
|
||||
},
|
||||
AUTO_CREATE_CONTACTS: {
|
||||
universalIdentifier: 'c4fa946e-e06b-4d54-afb6-288b0ac75bdf',
|
||||
description: 'Whether to auto-create contacts for unknown participants',
|
||||
value: 'true',
|
||||
},
|
||||
LOG_LEVEL: {
|
||||
universalIdentifier: '2b019cf1-d198-48dd-943e-110571aa541e',
|
||||
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',
|
||||
value: 'immediate_with_retry',
|
||||
},
|
||||
FIREFLIES_RETRY_ATTEMPTS: {
|
||||
universalIdentifier: '670ca203-01ce-4ae8-8294-eb38b29434f2',
|
||||
description: 'Number of retry attempts when fetching summaries',
|
||||
value: '3',
|
||||
},
|
||||
FIREFLIES_RETRY_DELAY: {
|
||||
universalIdentifier: '2e8ccb82-9390-47ba-b628-ca2726931bce',
|
||||
description: 'Delay in milliseconds between retry attempts',
|
||||
value: '5000',
|
||||
},
|
||||
FIREFLIES_POLL_INTERVAL: {
|
||||
universalIdentifier: '904538f7-7bec-4ee6-9bac-5d43c619b667',
|
||||
description: 'Polling interval (ms) when using delayed polling strategy',
|
||||
value: '30000',
|
||||
},
|
||||
FIREFLIES_MAX_POLLS: {
|
||||
universalIdentifier: '84d54c97-5572-4c01-9039-764ab3aa87b8',
|
||||
description: 'Maximum number of polling attempts when waiting for summaries',
|
||||
value: '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
const jestConfig = {
|
||||
displayName: 'fireflies',
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['ts', 'js'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
},
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/__tests__/**/*.(test|spec).{js,ts}',
|
||||
'<rootDir>/src/**/?(*.)(test|spec).{js,ts}',
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/__tests__/setup.ts'
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,js}',
|
||||
'!src/**/*.d.ts',
|
||||
],
|
||||
coverageDirectory: './coverage',
|
||||
};
|
||||
|
||||
export default jestConfig;
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "fireflies",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"setup:fields": "tsx scripts/add-meeting-fields.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": "^17.2.3",
|
||||
"twenty-sdk": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^24.9.2",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user