[Hacktoberfest] AI-Powered Meeting Transcript Analysis Extension for Twenty CRM (#15507)

# 🧠 AI-Powered Meeting Transcript to CRM Data Integration

## **Overview**
This feature automatically transforms meeting transcripts into
structured CRM data using AI.
When unstructured meeting notes are received via a **webhook**, the
system processes them and creates organized **notes, tasks, and
assignments** directly in **Twenty CRM**.

---

## **Key Features**

- **🤖 AI-Powered Analysis:**  
Extracts **summaries, action items, assignees, and due dates** from
natural language transcripts.

- **📋 Smart Task Consolidation:**  
  Merges related sub-tasks into unified deliverables  
  *(e.g., `"draft" + "review" + "present"` → one consolidated task).*

- **👥 Intelligent Assignment:**  
Uses **GraphQL member lookup** to match extracted assignee names to
workspace member IDs with flexible string matching.

- **🔗 Automatic Linking:**  
Links generated **notes and tasks** to relevant contacts using
`noteTargets` and `taskTargets`.

- **🗓️ Date Parsing:**  
Converts **relative date expressions** (e.g., “next Monday”, “end of
week”) into **ISO-formatted dates** for accurate scheduling.

---

## **Technical Stack**

| Component | Description |
|------------|-------------|
| **AI Provider** | Groq (via OpenAI SDK) using the `GPT-OSS-20B` model
|
| **APIs** | Twenty CRM REST API + GraphQL (for member resolution) |
| **Runtime** | Webhook-triggered **serverless function** written in
**TypeScript** |

---

## **Example Input**

```json
{
  "transcript": "During the Project Phoenix Kick-off on November 1st, 2025, we discussed securing the Series B funding. ACTION: Dylan Field is designated to finalize the investor deck layout and needs to present it next Monday, November 4th. Irfan Hussain will review the deck before the presentation by Monday morning. COMMITMENT: Dario Amodei confirmed he would personally review the security protocols for the AI model before the end of this week, by Friday November 7th. Iqra Khan will coordinate the security review process and ensure completion by the Friday deadline.",
  "meetingTitle": "Project Phoenix Kick-off",
  "meetingDate": "2025-11-01",
  "participants": [
    "Brian Chesky",
    "Dario Amodei",
    "Iqra Khan",
    "Irfan Hussain",
    "Dylan Field"
  ],
  "token": "e6d9d54e51953fd5a451cca933c63e7f8783b001f0c45be95be9d09ee06c6cda",
  "relatedPersonId": "6c4b0e98-b69e-42a4-ba0c-fd2eeafca642"
}
```
---

## **Example Output**

```json
{
  "success": true,
  "noteId": "9cc3b4fc-ae37-4b3e-a343-a4c69cf6b1e8",
  "taskIds": [
    "0f408062-0dcc-49f0-9866-1ea05392661d",
    "2b3739bf-0653-4101-9419-6a44ea5135cd"
  ],
  "summary": {
    "noteCreated": true,
    "tasksCreated": 2,
    "actionItemsProcessed": 2,
    "commitmentsProcessed": 0
  },
  "executionLogs": [
    " Validation passed",
    "📝 RelatedPersonId: 6c4b0e98-b69e-42a4-ba0c-fd2eeafca642",
    "🤖 Starting transcript analysis...",
    " Analysis complete: 2 action items, 0 commitments",
    "📄 Creating note in Twenty CRM...",
    " Note created: 9cc3b4fc-ae37-4b3e-a343-a4c69cf6b1e8",
    "📋 Creating tasks from action items...",
    " Action item tasks created: 2",
    "📋 Creating tasks from commitments...",
    " Commitment tasks created: 0"
  ]
}
```
---

Variable Name | Description
-- | --
GROQ_API_KEY | API key for authenticating requests to the Groq AI
service.
TWENTY_API_KEY | Authentication token used to access the Twenty CRM API.
TWENTY_API_URL | Base URL for the Twenty CRM REST API.
WEBHOOK_SECRET | Secret key used to validate incoming webhook requests
for security.
NODE_ENV | Defines the runtime environment (development, production,
etc.).
LOG_LEVEL | Controls verbosity of logs (info, debug, error).

---

## **Attachments**
<img width="649" height="863" alt="swappy-20251103-035128"
src="https://github.com/user-attachments/assets/2f0390af-9538-4fe2-bba8-f38e558935ad"
/>

---




https://github.com/user-attachments/assets/88620035-67ed-4150-b0be-46131083e2c5



---

Co-authored-by: iqra77818 <iqra77818@gmail.com>
This commit is contained in:
Irfan Hussain
2025-11-04 16:34:00 +05:30
committed by GitHub
parent 0cdc61c211
commit 7f3af243c7
10 changed files with 2519 additions and 0 deletions
@@ -0,0 +1,15 @@
# .env.example
# API key for authenticating with the OpenAI-compatible service (Groq, OpenAI, etc.)
AI_PROVIDER_API_KEY=your-openai-or-groq-api-key-here
# API key generated from Twenty CRM instance (Settings > API & Webhooks)
TWENTY_API_KEY=your-twenty-api-key-here
# Base URL for the Twenty CRM server
TWENTY_API_URL=https://your-instance.twenty.com
# Secret token used to authenticate incoming webhook requests
WEBHOOK_SECRET_TOKEN=your-webhook-secret-token-here
# Base URL for OpenAI-compatible API (defaults to OpenAI, can use Groq or other providers)
AI_PROVIDER_API_BASE_URL=https://api.openai.com/v1/
@@ -0,0 +1,2 @@
.yarn/install-state.gz
.env
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
yarnPath: .yarn/releases/yarn-4.9.2.cjs
nodeLinker: node-modules
@@ -0,0 +1,182 @@
## 🧠 AI-Powered Meeting Transcript to CRM Data Integration
---
### Overview
This feature automatically transforms meeting transcripts into structured CRM data using AI.
When unstructured meeting notes are received via a **webhook**, the system processes them and creates organized **notes, tasks, and assignments** directly in Twenty CRM.
---
### Key Features
- 🤖 **AI-Powered Analysis:**
Extracts summaries, action items, assignees, and due dates from natural language transcripts.
- 🗂 **Smart Task Consolidation:**
Merges related sub-tasks into unified deliverables
_(e.g., `"draft"` + `"review"` + `"present"` → one consolidated task)._
- 🧑‍🤝‍🧑 **Intelligent Assignment:**
Uses GraphQL member lookup to match extracted assignee names to workspace member IDs with flexible string matching.
- 🔗 **Automatic Linking:**
Links generated **notes** and **tasks** to relevant contacts using `noteTargets` and `taskTargets`.
- 📅 **Date Parsing:**
Converts relative date expressions (e.g., “next Monday”, “end of week”) into **ISO-formatted dates** for accurate scheduling.
---
## Requirements
- [Twenty CLI](https://www.npmjs.com/package/twenty-cli) - Install globally: `npm install -g twenty-cli`
- Twenty CRM instance with API access
- API key from [Settings > API & Webhooks](https://twenty.com/settings/api-webhooks)
- OpenAI API key or compatible service (Groq, etc.)
## Installation
1. **Authenticate with Twenty CLI:**
```bash
twenty auth login
```
2. **Configure environment variables:**
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cp .env.example .env
```
Required environment variables:
- `AI_PROVIDER_API_KEY`: Your OpenAI or Groq API key
- `TWENTY_API_KEY`: Generated from your Twenty CRM instance
- `TWENTY_API_URL`: Your Twenty CRM instance URL (e.g., https://your-instance.twenty.com)
- `WEBHOOK_SECRET_TOKEN`: Secret token for webhook authentication
- `AI_PROVIDER_API_BASE_URL`: Base URL for OpenAI-compatible API (defaults to https://api.openai.com/v1)
3. **Install dependencies:**
```bash
yarn install
```
4. **Deploy to your Twenty workspace:**
```bash
twenty app sync
```
## Configuration
### Using Groq Instead of OpenAI
To use Groq's API (which is compatible with OpenAI's SDK), set:
```bash
AI_PROVIDER_API_BASE_URL=https://api.groq.com/openai/v1
AI_PROVIDER_API_KEY=your-groq-api-key
```
### Using OpenAI
To use OpenAI's official API:
```bash
AI_PROVIDER_API_BASE_URL=https://api.openai.com/v1
AI_PROVIDER_API_KEY=your-openai-api-key
```
### 🔗 Webhook Setup (inside Twenty)
Navigate to:
**Twenty → Workspace Settings → APIs & Webhook → + New Webhook**
| Field | Value |
|--------|-------------------------------------------|
| **Method** | `POST` |
| **URL** | your deployed endpoint URL |
| **Secret** | Generate one & store same in `.env` |
## Usage
Send a POST request to your webhook endpoint with the following payload:
```json
{
"transcript": "During the Project Phoenix Kick-off on November 1st, 2025...",
"meetingTitle": "Project Phoenix Kick-off",
"meetingDate": "2025-11-01",
"participants": [
"Brian Chesky",
"Dario Amodei",
"Iqra Khan"
],
"token": "your-webhook-secret-token",
"relatedPersonId": "person-uuid-from-crm"
}
```
### Response
```json
{
"success": true,
"noteId": "note-uuid",
"taskIds": ["task-uuid-1", "task-uuid-2"],
"summary": {
"noteCreated": true,
"tasksCreated": 2,
"actionItemsProcessed": 2,
"commitmentsProcessed": 0
},
"executionLogs": [
"✅ Validation passed",
"🤖 Starting transcript analysis...",
"✅ Analysis complete"
]
}
```
## Technical Stack
| Component | Description |
|----------|-------------|
| **AI Provider** | Groq (via OpenAI SDK) using the `GPT-OSS-20B` model |
| **APIs** | Twenty CRM REST API + GraphQL (for member resolution) |
| **Runtime** | Webhook-triggered serverless function written in **TypeScript** |
---
## Development
### Build
```bash
yarn build
```
### Type Check
```bash
yarn type-check
```
## Environment Variables
| Variable | Required | Secret | Description |
|----------|----------|--------|-------------|
| `AI_PROVIDER_API_KEY` | Yes | Yes | API key for OpenAI-compatible service |
| `TWENTY_API_KEY` | Yes | Yes | Twenty CRM API authentication token |
| `TWENTY_API_URL` | Yes | No | Base URL for Twenty CRM instance |
| `WEBHOOK_SECRET_TOKEN` | Yes | Yes | Secret for webhook request validation |
| `AI_PROVIDER_API_BASE_URL` | No | No | Base URL for AI service (defaults to OpenAI) |
## Demo Preview
The demo below shows the full workflow where a raw meeting transcript is processed and automatically converted into structured **Notes + Tasks** inside Twenty CRM.
### Process-Transcript Screenshot
![process](https://drive.google.com/uc?export=view&id=1IMayfb77uAr5lvtY7XLn8Yq-_26pxMCY)
## Demo Video
[Watch Demo Video](https://drive.google.com/file/d/1WpzrNBbKBFD5ztGwNuS_SMi7yJBr5AK8/view?usp=sharing)
@@ -0,0 +1,54 @@
{
"version": "0.0.1",
"type": "module",
"license": "MIT",
"engines": {
"node": "^18.0.0 || ^20.0.0 || ^22.0.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.6.0"
},
"scripts": {
"build": "tsc",
"type-check": "tsc --noEmit"
},
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
"universalIdentifier": "f15a0c72-f7b4-4d20-9e97-ade1122d4bd7",
"name": "Meeting Transcript",
"description": "AI Meeting Transcript Integration for Hacktoberfest",
"env": {
"TWENTY_API_KEY": {
"isSecret": true,
"value": "",
"description": "API key for the Twenty CRM instance (used for authentication)."
},
"AI_PROVIDER_API_KEY": {
"isSecret": true,
"value": "",
"description": "API key for authenticating with the OpenAI-compatible service (supports OpenAI, Groq, and other providers)."
},
"TWENTY_API_URL": {
"isSecret": false,
"value": "https://unpaid-interns.twenty.com",
"description": "The base URL for the Twenty CRM server (e.g., https://your-instance.twenty.com)."
},
"WEBHOOK_SECRET_TOKEN": {
"isSecret": true,
"value": "",
"description": "Secret token used to authenticate incoming webhook requests."
},
"AI_PROVIDER_API_BASE_URL": {
"isSecret": false,
"value": "https://api.openai.com/v1",
"description": "Base URL for OpenAI-compatible API. Defaults to OpenAI, but can be changed to use Groq (https://api.groq.com/openai/v1) or other providers."
}
},
"dependencies": {
"axios": "^1.7.0",
"openai": "^4.67.0"
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
"universalIdentifier": "b5e86982-b9ec-4c3d-8a02-6ea08a5b2d35",
"name": "process-transcript",
"triggers": [
{
"universalIdentifier": "a25fcbbf-1a20-438a-b277-ee8ca9770499",
"type": "route",
"path": "/transcript",
"httpMethod": "POST",
"isAuthRequired": true
}
]
}
@@ -0,0 +1,807 @@
import axios from 'axios';
import OpenAI from 'openai';
type TranscriptWebhookPayload = {
transcript: string;
relatedPersonId: string;
meetingTitle?: string;
meetingDate?: string;
participants?: string[];
metadata?: Record<string, unknown>;
token?: string;
};
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 AI_PROVIDER_API_KEY = process.env.AI_PROVIDER_API_KEY;
const WEBHOOK_SECRET_TOKEN = process.env.WEBHOOK_SECRET_TOKEN;
const TWENTY_API_URL = process.env.TWENTY_API_URL;
const AI_PROVIDER_API_BASE_URL = process.env.AI_PROVIDER_API_BASE_URL;
const LLM_MODEL_ID = 'openai/gpt-oss-20b';
const OPENAI_TEMPERATURE = 0.3;
const openai = new OpenAI({
apiKey: AI_PROVIDER_API_KEY,
baseURL: AI_PROVIDER_API_BASE_URL,
});
const getTwentyApiConfig = () => {
const apiKey = process.env.TWENTY_API_KEY;
if (!apiKey) {
throw new Error('TWENTY_API_KEY environment variable is not set');
}
const baseUrl = TWENTY_API_URL;
if (!baseUrl) {
throw new Error('TWENTY_API_URL environment variable is not set');
}
return { apiKey, baseUrl };
};
const lookupWorkspaceMemberByName = async (
name: string,
): Promise<string | null> => {
const { apiKey, baseUrl } = getTwentyApiConfig();
try {
const graphqlQuery = {
query: `
query GetAllWorkspaceMembers {
workspaceMembers {
edges {
node {
id
name {
firstName
lastName
}
}
}
}
}
`,
};
const response = await axios.post(
`${baseUrl}/graphql`,
graphqlQuery,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
const edges = response.data?.data?.workspaceMembers?.edges;
if (!edges || edges.length === 0) {
console.log('⚠️ No workspace members found');
return null;
}
const searchName = name.trim().toLowerCase();
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
const lastName = edge.node.name?.lastName || '';
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
if (fullName === searchName) {
console.log(`✅ Found workspace member (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
if (firstName.toLowerCase() === searchName) {
const lastName = edge.node.name?.lastName || '';
console.log(`✅ Found workspace member (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const lastName = edge.node.name?.lastName || '';
if (lastName.toLowerCase() === searchName) {
const firstName = edge.node.name?.firstName || '';
console.log(`✅ Found workspace member (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
const lastName = edge.node.name?.lastName || '';
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
if (fullName.includes(searchName) || searchName.includes(fullName)) {
console.log(`✅ Found workspace member (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
console.log(`⚠️ No workspace member found matching: "${name}"`);
return null;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data
? JSON.stringify(error.response.data, null, 2)
: error.message;
console.error(`❌ Failed to lookup workspace member "${name}": ${errorMessage}`);
}
return null;
}
};
const lookupPersonByName = async (
name: string,
): Promise<string | null> => {
const { apiKey, baseUrl } = getTwentyApiConfig();
try {
const graphqlQuery = {
query: `
query GetAllPeople {
people {
edges {
node {
id
name {
firstName
lastName
}
}
}
}
}
`,
};
const response = await axios.post(
`${baseUrl}/graphql`,
graphqlQuery,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
const edges = response.data?.data?.people?.edges;
if (!edges || edges.length === 0) {
console.log('⚠️ No people found in CRM');
return null;
}
const searchName = name.trim().toLowerCase();
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
const lastName = edge.node.name?.lastName || '';
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
if (fullName === searchName) {
console.log(`✅ Found person (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
if (firstName.toLowerCase() === searchName) {
const lastName = edge.node.name?.lastName || '';
console.log(`✅ Found person (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const lastName = edge.node.name?.lastName || '';
if (lastName.toLowerCase() === searchName) {
const firstName = edge.node.name?.firstName || '';
console.log(`✅ Found person (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
for (const edge of edges) {
const firstName = edge.node.name?.firstName || '';
const lastName = edge.node.name?.lastName || '';
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
if (fullName.includes(searchName) || searchName.includes(fullName)) {
console.log(`✅ Found person (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`);
return edge.node.id;
}
}
console.log(`⚠️ No person found matching: "${name}"`);
return null;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data
? JSON.stringify(error.response.data, null, 2)
: error.message;
console.error(`❌ Failed to lookup person "${name}": ${errorMessage}`);
}
return null;
}
};
const extractPersonNamesFromDescription = (description: string, participants: string[]): string[] => {
const foundNames: string[] = [];
for (const participant of participants) {
if (description.includes(participant)) {
foundNames.push(participant);
continue;
}
const firstName = participant.split(' ')[0];
if (firstName && description.includes(firstName)) {
foundNames.push(participant);
}
}
return foundNames;
};
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 linkNoteToPersonREST = async (
noteId: string,
personId: string,
): Promise<void> => {
const { apiKey, baseUrl } = getTwentyApiConfig();
try {
const response = await axios.post(
`${baseUrl}/rest/noteTargets`,
{
noteId: noteId,
personId: personId,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
const noteTargetId = response.data?.data?.createNoteTarget?.id;
if (noteTargetId) {
console.log(`✅ Successfully linked note ${noteId} to person ${personId} (noteTarget: ${noteTargetId})`);
} else {
console.warn(`⚠️ Note linking response received but no ID found`);
}
} 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;
console.error(`❌ Failed to link note to person. Status: ${status}, Error: ${errorMessage}`);
console.error(`Attempted to link noteId: ${noteId} to personId: ${personId}`);
throw new Error(`Failed to link note to person: ${errorMessage}`);
}
throw error;
}
};
const createNoteInTwenty = async (
summary: string,
keyPoints: string[],
relatedPersonId: 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 response = await axios.post(
`${baseUrl}/rest/notes`,
requestData,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
const responseJson = JSON.stringify(response.data);
console.log('📦 Note API Response:', responseJson);
const noteId = response.data?.data?.createNote?.id;
if (!noteId) {
const errorMsg = `Note created but ID not found in response. Response structure: ${responseJson}`;
console.error('❌', errorMsg);
throw new Error(errorMsg);
}
console.log(`✅ Note ID extracted: ${noteId}`);
await linkNoteToPersonREST(noteId, relatedPersonId);
return { id: noteId };
} 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 linkTaskToPersonREST = async (
taskId: string,
personId: string,
): Promise<void> => {
const { apiKey, baseUrl } = getTwentyApiConfig();
try {
const response = await axios.post(
`${baseUrl}/rest/taskTargets`,
{
taskId: taskId,
personId: personId,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
console.log(`✅ Successfully linked task ${taskId} to person ${personId}`, response.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;
console.error(`❌ Failed to link task to person. Status: ${status}, Error: ${errorMessage}`);
console.error(`Attempted to link taskId: ${taskId} to personId: ${personId}`);
}
}
};
const createTaskInTwenty = async (
actionItem: ActionItem,
relatedPersonId?: string,
): Promise<TwentyApiResponse> => {
const { apiKey, baseUrl } = getTwentyApiConfig();
const taskData: {
title: string;
bodyV2: RichTextV2Data;
dueAt?: string;
assigneeId?: string;
} = {
title: actionItem.title,
bodyV2: {
markdown: actionItem.description,
blocknote: null,
},
};
if (actionItem.assignee) {
console.log(`🔍 Looking up assignee: "${actionItem.assignee}"`);
const assigneeId = await lookupWorkspaceMemberByName(actionItem.assignee);
if (assigneeId) {
taskData.assigneeId = assigneeId;
console.log(`✅ Task will be assigned to: ${actionItem.assignee} (${assigneeId})`);
} else {
console.log(`⚠️ Could not find workspace member "${actionItem.assignee}", task will be unassigned`);
}
}
if (actionItem.dueDate) {
const date = new Date(actionItem.dueDate);
if (!isNaN(date.getTime())) {
taskData.dueAt = date.toISOString();
}
}
try {
const response = await axios.post(
`${baseUrl}/rest/tasks`,
taskData,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
);
console.log('📦 Task API Response:', JSON.stringify(response.data));
const taskId = response.data?.data?.createTask?.id;
if (!taskId) {
console.error('❌ Failed to extract task ID from response:', response.data);
throw new Error('Task created but ID not found in response');
}
console.log(`✅ Task created successfully: ${taskId} - "${actionItem.title}"`);
if (relatedPersonId) {
await linkTaskToPersonREST(taskId, relatedPersonId);
}
return { id: taskId };
} 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;
console.error(`❌ Failed to create task "${actionItem.title}". Status: ${status}, Error: ${errorMessage}`);
throw new Error(
`Failed to create task "${actionItem.title}": ${errorMessage}. Status: ${status}`,
);
}
throw error;
}
};
const createTasksFromActionItems = async (
actionItems: ActionItem[],
noteId: string,
relatedPersonId: string,
participants: string[],
): Promise<string[]> => {
const taskIds: string[] = [];
for (const actionItem of actionItems) {
try {
const taskDescription = `${actionItem.description}\n\n*Related to meeting note: ${noteId}*`;
console.log(`Creating task: "${actionItem.title}"`);
const mentionedPeople = extractPersonNamesFromDescription(actionItem.description, participants);
console.log(`📝 People mentioned in task description:`, mentionedPeople);
const task = await createTaskInTwenty({
...actionItem,
description: taskDescription,
});
taskIds.push(task.id);
console.log(`✅ Task created: ${task.id}`);
if (mentionedPeople.length > 0) {
for (const personName of mentionedPeople) {
const personId = await lookupPersonByName(personName);
if (personId) {
await linkTaskToPersonREST(task.id, personId);
} else {
console.log(`⚠️ Could not find person "${personName}" in CRM, skipping link`);
}
}
} else {
console.log(`⚠️ No specific people mentioned, using relatedPersonId as fallback`);
await linkTaskToPersonREST(task.id, relatedPersonId);
}
console.log(`✅ Task linking complete: ${task.id}`);
} catch (error) {
console.error(`❌ Task creation failed for "${actionItem.title}":`, error instanceof Error ? error.message : error);
}
}
return taskIds;
};
const createTasksFromCommitments = async (
commitments: Commitment[],
noteId: string,
relatedPersonId: 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,
assignee: commitment.person,
dueDate: commitment.dueDate || '',
});
taskIds.push(task.id);
const personId = await lookupPersonByName(commitment.person);
if (personId) {
await linkTaskToPersonREST(task.id, personId);
} else {
await linkTaskToPersonREST(task.id, relatedPersonId);
}
} catch (error) {
console.error(`Commitment task creation failed for "${commitment.commitment}":`, error);
}
}
return taskIds;
};
const analyzeTranscript = async (
transcript: string,
openaiApiKey: string,
): Promise<AnalysisResult> => {
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
🚨 CRITICAL RULE FOR ACTION ITEMS - READ CAREFULLY:
When multiple people are mentioned working on THE SAME deliverable/document/outcome:
→ Create EXACTLY ONE task that represents the complete workflow
→ The task title should describe the MAIN deliverable (what needs to be completed)
→ Assign to the person who will do the FINAL/CRITICAL step (approver, reviewer, coordinator)
→ The description MUST mention ALL people involved and their roles
STRICT EXAMPLES - Follow this pattern exactly:
Example 1:
Transcript: "Brian will finalize the investor deck. Irfan will review it before the presentation next Monday."
❌ WRONG OUTPUT (2 tasks):
Task 1: {"title": "Finalize investor deck", "assignee": "Brian", "description": "Brian will finalize..."}
Task 2: {"title": "Review investor deck", "assignee": "Irfan", "description": "Irfan will review..."}
✅ CORRECT OUTPUT (1 task):
Task 1: {
"title": "Finalize and present investor deck",
"assignee": "Irfan",
"dueDate": "2025-11-03",
"description": "Brian Chesky is designated to finalize the investor deck layout and needs to present it next Monday. Irfan will review the deck before the presentation."
}
Example 2:
Transcript: "Dario will review security protocols before end of week. Iqra will coordinate the security review process."
❌ WRONG OUTPUT (2 tasks):
Task 1: {"title": "Review security protocols", "assignee": "Dario", ...}
Task 2: {"title": "Coordinate security review", "assignee": "Iqra", ...}
✅ CORRECT OUTPUT (1 task):
Task 1: {
"title": "Coordinate security protocol review",
"assignee": "Iqra",
"dueDate": "2025-11-07",
"description": "Dario Amodei will personally review the security protocols for the AI model before the end of the week. Iqra Khan will coordinate the security review process."
}
KEY RULES:
1. If multiple people mentioned for same deliverable → ONE task only
2. Task title = main deliverable/outcome
3. Assignee = person doing final/critical step (reviewer, approver, coordinator, presenter)
4. Description = full context with ALL people and their roles mentioned
5. DO NOT split workflows into multiple tasks
6. Look for keywords: "and then", "before", "after", "will review", "will coordinate", "will approve"
7. IMPORTANT: Do NOT extract commitments separately - include all commitments as part of action items
For assignees (MANDATORY - always extract if possible):
- ALWAYS assign the task to someone if ANY person is mentioned doing work
- Priority order: reviewer > creator, coordinator > contributor, approver > submitter, presenter > preparer
- Look for: "X will", "assigned to X", "X is responsible", "X to review/approve/coordinate/present"
- Examples of who to assign:
* "Brian will finalize, Irfan will review" → Assign to Irfan (reviewer)
* "Dario will review, Iqra will coordinate" → Assign to Iqra (coordinator)
* "Sarah will create report" → Assign to Sarah
- If only ONE person mentioned → assign to them
- If MULTIPLE people mentioned → assign to the one doing the FINAL/CRITICAL step
For due dates (MANDATORY - always extract if possible):
- ALWAYS include dueDate if ANY time reference is mentioned
- Current date context: Meeting date is 2025-11-01 (Saturday)
- Convert relative dates to YYYY-MM-DD format:
* "next Monday" → "2025-11-03"
* "this Friday" → "2025-11-07"
* "end of week" → "2025-11-07" (Friday)
* "end of month" → "2025-11-30"
* "next week" → "2025-11-08" (7 days from meeting)
* "in 3 days" → "2025-11-04"
- If specific date mentioned: "November 10" → "2025-11-10"
- Use the LATEST date mentioned if multiple dates in the workflow
Return JSON with this structure (NOTE: commitments array should always be EMPTY):
{
"summary": "string",
"keyPoints": ["string"],
"actionItems": [{"title": "string", "description": "string", "assignee": "string", "dueDate": "YYYY-MM-DD"}],
"commitments": []
}
Transcript:
${transcript}`;
const completion = await openai.chat.completions.create({
model: LLM_MODEL_ID,
messages: [
{
role: 'system',
content:
'You are a meeting analysis assistant. When multiple people work on the same deliverable, create ONE task (not multiple). ALWAYS assign tasks to someone and ALWAYS extract due dates when time references are mentioned. Include all commitments as action items. Commitments array should always be empty. 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 AI API');
}
let parsedResult: AnalysisResult;
try {
parsedResult = JSON.parse(content) as AnalysisResult;
} catch (error) {
throw new Error(`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
if (!parsedResult.summary || typeof parsedResult.summary !== 'string') {
throw new Error('Invalid AI response: missing or invalid summary');
}
if (!Array.isArray(parsedResult.keyPoints)) {
throw new Error('Invalid AI response: keyPoints must be an array');
}
if (!Array.isArray(parsedResult.actionItems)) {
throw new Error('Invalid AI response: actionItems must be an array');
}
if (!Array.isArray(parsedResult.commitments)) {
throw new Error('Invalid AI response: commitments must be an array');
}
return parsedResult;
};
export const main = async (
params: TranscriptWebhookPayload,
): Promise<object> => {
const executionLogs: string[] = [];
const log = (message: string) => {
console.log(message);
executionLogs.push(message);
};
try {
const webhookToken = params.token;
const expectedSecret = process.env.WEBHOOK_SECRET_TOKEN;
if (!expectedSecret || !webhookToken || webhookToken !== expectedSecret) {
throw new Error('Unauthorized webhook access: Invalid or missing token.');
}
const { transcript, meetingTitle, meetingDate, relatedPersonId, participants } = params;
if (!transcript || typeof transcript !== 'string') {
throw new Error('Transcript is required and must be a string');
}
if (!relatedPersonId || typeof relatedPersonId !== 'string') {
throw new Error('relatedPersonId is required and must be a string');
}
const openaiApiKey = process.env.AI_PROVIDER_API_KEY;
if (!openaiApiKey) {
throw new Error('AI_PROVIDER_API_KEY environment variable is not set');
}
log('✅ Validation passed');
log(`📝 RelatedPersonId: ${relatedPersonId}`);
log('🤖 Starting transcript analysis...');
const analysis = await analyzeTranscript(transcript, openaiApiKey);
log(`✅ Analysis complete: ${analysis.actionItems.length} action items, ${analysis.commitments.length} commitments`);
log('📄 Creating note in Twenty CRM...');
const note = await createNoteInTwenty(
analysis.summary,
analysis.keyPoints,
relatedPersonId,
meetingTitle,
meetingDate,
);
log(`✅ Note created: ${note.id}`);
log('📋 Creating tasks from action items...');
const actionItemTaskIds = await createTasksFromActionItems(
analysis.actionItems,
note.id,
relatedPersonId,
participants || [],
);
log(`✅ Action item tasks created: ${actionItemTaskIds.length}`);
log('📋 Creating tasks from commitments...');
const commitmentTaskIds = await createTasksFromCommitments(
analysis.commitments,
note.id,
relatedPersonId,
);
log(`✅ Commitment tasks created: ${commitmentTaskIds.length}`);
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,
},
executionLogs: executionLogs,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`❌ ERROR: ${errorMessage}`);
return {
success: false,
error: errorMessage,
executionLogs: executionLogs,
};
}
}
@@ -0,0 +1,36 @@
{
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",
// Environment Settings
"module": "nodenext",
"target": "esnext",
"moduleResolution": "nodenext",
// <-- add this
"types": ["node"], // Node types
"lib": ["esnext"], // Modern JS libs
"esModuleInterop": true, // <-- add this for axios & other CJS imports
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
},
"include": ["**/*.ts", "**/*.tsx"]
}
@@ -0,0 +1,464 @@
# 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:*":
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
"@types/node@npm:^20.0.0":
version: 20.19.24
resolution: "@types/node@npm:20.19.24"
dependencies:
undici-types: "npm:~6.21.0"
checksum: 10c0/c872ce80a1e832fe035a3c94a27acb2d6e45ffa1209c0241ac6e2d405db8d6f47eea7a2509b5c2dbedae6231dafb9dbed873dd5daaebbad1f11fdaa58726ce5e
languageName: node
linkType: hard
"Meeting Transcript@workspace:.":
version: 0.0.0-use.local
resolution: "Meeting Transcript@workspace:."
dependencies:
"@types/node": "npm:^20.0.0"
axios: "npm:^1.7.0"
openai: "npm:^4.67.0"
typescript: "npm:^5.6.0"
languageName: unknown
linkType: soft
"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
"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.7.0":
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.67.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
"typescript@npm:^5.6.0":
version: 5.9.3
resolution: "typescript@npm:5.9.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.6.0#optional!builtin<compat/typescript>":
version: 5.9.3
resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430
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:~6.21.0":
version: 6.21.0
resolution: "undici-types@npm:6.21.0"
checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
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