Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-tl-dv
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-tl-dv` to build the library.

View File

@@ -0,0 +1,10 @@
{
"name": "@activepieces/piece-tl-dv",
"version": "0.0.1",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,65 @@
{
"name": "pieces-tl-dv",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/tl-dv/src",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"dist/{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/tl-dv",
"tsConfig": "packages/pieces/community/tl-dv/tsconfig.lib.json",
"packageJson": "packages/pieces/community/tl-dv/package.json",
"main": "packages/pieces/community/tl-dv/src/index.ts",
"assets": [
"packages/pieces/community/tl-dv/*.md",
{
"input": "packages/pieces/community/tl-dv/src/i18n",
"output": "./src/i18n",
"glob": "**/!(i18n.json)"
}
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"prebuild",
"^build"
]
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"prebuild": {
"dependsOn": [
"^build"
],
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/tl-dv",
"command": "bun install --no-save --silent"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
}
}
}

View File

@@ -0,0 +1,33 @@
import { createPiece } from "@activepieces/pieces-framework";
import { PieceCategory } from "@activepieces/shared";
import { tldvAuth } from "./lib/common/auth";
import { uploadRecording } from "./lib/actions/upload-recording";
import { listMeetings } from "./lib/actions/list-meetings";
import { getMeeting } from "./lib/actions/get-meeting";
import { getTranscript } from "./lib/actions/get-transcript";
import { getHighlights } from "./lib/actions/get-highlights";
import { meetingReady } from "./lib/triggers/meeting-ready";
import { transcriptReady } from "./lib/triggers/transcript-ready";
export { tldvAuth } from "./lib/common/auth";
export const tlDv = createPiece({
displayName: "tl;dv",
description: "Record meetings, get transcripts, and access meeting notes automatically.",
auth: tldvAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: "https://cdn.activepieces.com/pieces/tl-dv.png",
categories: [PieceCategory.PRODUCTIVITY],
authors: ["onyedikachi-david"],
actions: [
uploadRecording,
listMeetings,
getMeeting,
getTranscript,
getHighlights,
],
triggers: [
meetingReady,
transcriptReady,
],
});

View File

@@ -0,0 +1,35 @@
import { createAction } from '@activepieces/pieces-framework';
import { tldvAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from '../common/client';
import { meetingIdProperty } from '../common/props';
export const getHighlights = createAction({
auth: tldvAuth,
name: 'get_highlights',
displayName: 'Get Highlights',
description: 'Get meeting highlights (notes) by meeting ID',
props: {
meetingId: meetingIdProperty,
},
async run(context) {
const { meetingId } = context.propsValue;
const response = await tldvCommon.apiCall<{
meetingId: string;
data: Array<{
id?: string;
text?: string;
timestamp?: number;
[key: string]: any;
}>;
}>({
method: HttpMethod.GET,
url: `/v1alpha1/meetings/${meetingId}/highlights`,
auth: { apiKey: context.auth.secret_text },
});
return response;
},
});

View File

@@ -0,0 +1,43 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { tldvAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from '../common/client';
import { meetingIdProperty } from '../common/props';
export const getMeeting = createAction({
auth: tldvAuth,
name: 'get_meeting',
displayName: 'Get Meeting',
description: 'Get meeting details by ID',
props: {
meetingId: meetingIdProperty,
},
async run(context) {
const { meetingId } = context.propsValue;
const response = await tldvCommon.apiCall<{
id: string;
name: string;
happenedAt: string;
url: string;
duration: number;
organizer: {
name: string;
email: string;
};
invitees: Array<{
name: string;
email: string;
}>;
template: string;
extraProperties: Record<string, any>;
}>({
method: HttpMethod.GET,
url: `/v1alpha1/meetings/${meetingId}`,
auth: { apiKey: context.auth.secret_text },
});
return response;
},
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import { tldvAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from '../common/client';
import { meetingIdProperty } from '../common/props';
export const getTranscript = createAction({
auth: tldvAuth,
name: 'get_transcript',
displayName: 'Get Transcript',
description: 'Get meeting transcript by meeting ID',
props: {
meetingId: meetingIdProperty,
},
async run(context) {
const { meetingId } = context.propsValue;
const response = await tldvCommon.apiCall<{
id: string;
meetingId: string;
data: Array<{
speaker: string;
text: string;
startTime: number;
endTime: number;
}>;
}>({
method: HttpMethod.GET,
url: `/v1alpha1/meetings/${meetingId}/transcript`,
auth: { apiKey: context.auth.secret_text },
});
return response;
},
});

View File

@@ -0,0 +1,103 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { tldvAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from '../common/client';
export const listMeetings = createAction({
auth: tldvAuth,
name: 'list_meetings',
displayName: 'List Meetings',
description: 'Search and list meetings',
props: {
query: Property.ShortText({
displayName: 'Search Query',
description: 'Search for meetings by name or content',
required: false,
}),
page: Property.Number({
displayName: 'Page',
description: 'Page number to return (default: 1)',
required: false,
defaultValue: 1,
}),
limit: Property.Number({
displayName: 'Results Per Page',
description: 'Number of results per page (default: 50)',
required: false,
defaultValue: 50,
}),
from: Property.DateTime({
displayName: 'From Date',
description: 'Search meetings from this date',
required: false,
}),
to: Property.DateTime({
displayName: 'To Date',
description: 'Search meetings up to this date',
required: false,
}),
onlyParticipated: Property.Checkbox({
displayName: 'Only Participated',
description: 'Only return meetings you participated in',
required: false,
defaultValue: false,
}),
meetingType: Property.StaticDropdown({
displayName: 'Meeting Type',
description: 'Filter by meeting type',
required: false,
options: {
options: [
{ label: 'All', value: '' },
{ label: 'Internal', value: 'internal' },
{ label: 'External', value: 'external' },
],
},
}),
},
async run(context) {
const { query, page, limit, from, to, onlyParticipated, meetingType } = context.propsValue;
const params = new URLSearchParams();
if (query) {
params.append('query', query);
}
if (page) {
params.append('page', page.toString());
}
if (limit) {
params.append('limit', limit.toString());
}
if (from) {
params.append('from', new Date(from).toISOString());
}
if (to) {
params.append('to', new Date(to).toISOString());
}
if (onlyParticipated !== undefined) {
params.append('onlyParticipated', onlyParticipated.toString());
}
if (meetingType) {
params.append('meetingType', meetingType);
}
const queryString = params.toString();
const url = `/v1alpha1/meetings${queryString ? `?${queryString}` : ''}`;
const response = await tldvCommon.apiCall<{
page: number;
pages: number;
total: number;
pageSize: number;
results: any[];
}>({
method: HttpMethod.GET,
url,
auth: { apiKey: context.auth.secret_text },
});
return response;
},
});

View File

@@ -0,0 +1,97 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { tldvAuth } from '../common/auth';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from '../common/client';
export const uploadRecording = createAction({
auth: tldvAuth,
name: 'upload_recording',
displayName: 'Upload Recording',
description: 'Import a meeting or recording from a URL',
props: {
name: Property.ShortText({
displayName: 'Meeting Name',
description: 'The name of the meeting or recording',
required: true,
}),
url: Property.ShortText({
displayName: 'Recording URL',
description: 'Publicly accessible URL of the recording. Supported formats: .mp3, .mp4, .wav, .m4a, .mkv, .mov, .avi, .wma, .flac',
required: true,
}),
happenedAt: Property.DateTime({
displayName: 'Meeting Date',
description: 'The date and time when the meeting occurred. If not provided, the current date will be used.',
required: false,
}),
dryRun: Property.Checkbox({
displayName: 'Dry Run',
description: 'Test the import without persisting to the database',
required: false,
defaultValue: false,
}),
participants: Property.Array({
displayName: 'Participants',
description: 'Email addresses of meeting participants',
required: false,
properties: {
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
},
}),
},
async run(context) {
const { name, url, happenedAt, dryRun, participants } = context.propsValue;
try {
new URL(url);
} catch {
throw new Error('Invalid URL format. Please provide a valid, publicly accessible URL.');
}
if (participants && Array.isArray(participants)) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
for (const participant of participants) {
const email = typeof participant === 'string' ? participant : (participant as { email: string }).email;
if (email && !emailRegex.test(email)) {
throw new Error(`Invalid email format: ${email}`);
}
}
}
const body: Record<string, any> = {
name,
url,
};
if (happenedAt) {
body['happenedAt'] = new Date(happenedAt).toISOString();
}
if (dryRun !== undefined) {
body['dryRun'] = dryRun;
}
if (participants && Array.isArray(participants)) {
body['participants'] = participants.map((p: unknown) =>
typeof p === 'string' ? p : (p as { email: string }).email
);
}
const response = await tldvCommon.apiCall<{
success: boolean;
jobId: string;
message: string;
}>({
method: HttpMethod.POST,
url: '/v1alpha1/meetings/import',
auth: { apiKey: context.auth.secret_text },
body,
});
return response;
},
});

View File

@@ -0,0 +1,36 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from './client';
export const tldvAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: 'Your tl;dv API key. You can find this at https://tldv.io/app/settings/personal-settings/api-keys',
required: true,
validate: async ({ auth }) => {
try {
await tldvCommon.apiCall({
method: HttpMethod.GET,
url: '/v1alpha1/health',
auth: { apiKey: auth as string },
});
return {
valid: true,
message: 'API key validated successfully. Connected to tl;dv.'
};
} catch (error: any) {
if (error.message.includes('401') || error.message.includes('403')) {
return {
valid: false,
error: 'Invalid API key. Please check your API key and try again.',
};
}
return {
valid: false,
error: `Authentication failed: ${error.message}. Please verify your API key is correct.`,
};
}
},
});

View File

@@ -0,0 +1,39 @@
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const tldvCommon = {
baseUrl: 'https://pasta.tldv.io',
async apiCall<T>({
method,
url,
body,
auth,
headers,
}: {
method: HttpMethod;
url: string;
body?: any;
auth: string | { apiKey: string };
headers?: Record<string, string>;
}): Promise<T> {
const apiKey = typeof auth === 'string' ? auth : auth.apiKey;
const response = await httpClient.sendRequest<T>({
method,
url: `${this.baseUrl}${url}`,
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json',
...headers,
},
body,
});
if (response.status >= 400) {
throw new Error(`tl;dv API error: ${response.status} - ${JSON.stringify(response.body)}`);
}
return response.body;
},
};

View File

@@ -0,0 +1,58 @@
import { Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { tldvCommon } from './client';
import { tldvAuth } from './auth';
export const meetingIdProperty = Property.Dropdown({
displayName: 'Meeting',
description: 'Select a meeting',
auth: tldvAuth,
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
try {
const response = await tldvCommon.apiCall<{
results: Array<{
id: string;
name: string;
happenedAt?: string;
}>;
}>({
method: HttpMethod.GET,
url: '/v1alpha1/meetings?limit=100',
auth: { apiKey: auth.secret_text },
});
if (response.results && response.results.length > 0) {
return {
disabled: false,
options: response.results.map((meeting) => ({
label: `${meeting.name}${meeting.happenedAt ? ` (${new Date(meeting.happenedAt).toLocaleDateString()})` : ''}`,
value: meeting.id,
})),
};
}
return {
disabled: true,
placeholder: 'No meetings found',
options: [],
};
} catch (error) {
return {
disabled: true,
placeholder: 'Error loading meetings',
options: [],
};
}
},
});

View File

@@ -0,0 +1,98 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { tldvAuth } from '../common/auth';
export const meetingReady = createTrigger({
auth: tldvAuth,
name: 'meeting_ready',
displayName: 'Meeting Ready',
description: 'Triggers when a meeting has finished processing and is ready',
props: {
webhookInstructions: Property.MarkDown({
value: `
## Setup Instructions
To use this trigger, configure a webhook in tl;dv:
1. Go to your tl;dv dashboard
2. Navigate to Settings > Webhooks
3. Add a new webhook
4. Set the **URL** to:
\`\`\`text
{{webhookUrl}}
\`\`\`
5. Select the **MeetingReady** event type
6. Choose the scope (User, Team, or Organization level)
7. Save the webhook
The webhook will trigger whenever a meeting finishes processing.
`,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
id: 'webhook-123',
event: 'MeetingReady',
data: {
id: 'meeting-123',
name: 'Team Standup Meeting',
happenedAt: '2024-01-15T10:00:00Z',
url: 'https://app.tldv.io/meetings/meeting-123',
duration: 1800,
organizer: {
name: 'John Doe',
email: 'john@example.com',
},
invitees: [
{
name: 'Jane Smith',
email: 'jane@example.com',
},
],
template: '{}',
extraProperties: {},
},
executedAt: '2024-01-15T10:30:00Z',
},
async onEnable(context) {
// Webhook URL is automatically provided by Activepieces
// User needs to manually configure the webhook in tl;dv dashboard
},
async onDisable(context) {
// User should remove webhook from tl;dv dashboard
},
async run(context) {
const payload = context.payload.body as {
id: string;
event: string;
data: {
id: string;
name: string;
happenedAt: string;
url: string;
duration: number;
organizer: {
name: string;
email: string;
};
invitees: Array<{
name: string;
email: string;
}>;
template: string;
extraProperties: Record<string, any>;
};
executedAt: string;
};
if (payload.event === 'MeetingReady') {
return [payload];
}
return [];
},
});

View File

@@ -0,0 +1,90 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { tldvAuth } from '../common/auth';
export const transcriptReady = createTrigger({
auth: tldvAuth,
name: 'transcript_ready',
displayName: 'Transcript Ready',
description: 'Triggers when a meeting transcript has been generated',
props: {
webhookInstructions: Property.MarkDown({
value: `
## Setup Instructions
To use this trigger, configure a webhook in tl;dv:
1. Go to your tl;dv dashboard
2. Navigate to Settings > Webhooks
3. Add a new webhook
4. Set the **URL** to:
\`\`\`text
{{webhookUrl}}
\`\`\`
5. Select the **TranscriptReady** event type
6. Choose the scope (User, Team, or Organization level)
7. Save the webhook
The webhook will trigger whenever a meeting transcript is generated.
`,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
id: 'webhook-456',
event: 'TranscriptReady',
data: {
id: 'transcript-123',
meetingId: 'meeting-123',
data: [
{
speaker: 'John Doe',
text: 'Hello everyone, welcome to today\'s meeting.',
startTime: 0,
endTime: 3,
},
{
speaker: 'Jane Smith',
text: 'Thanks for having me.',
startTime: 4,
endTime: 6,
},
],
},
executedAt: '2024-01-15T10:35:00Z',
},
async onEnable(context) {
// Webhook URL is automatically provided by Activepieces
// User needs to manually configure the webhook in tl;dv dashboard
},
async onDisable(context) {
// User should remove webhook from tl;dv dashboard
},
async run(context) {
const payload = context.payload.body as {
id: string;
event: string;
data: {
id: string;
meetingId: string;
data: Array<{
speaker: string;
text: string;
startTime: number;
endTime: number;
}>;
};
executedAt: string;
};
if (payload.event === 'TranscriptReady') {
return [payload];
}
return [];
},
});

View File

@@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}