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,29 @@
import { createPiece } from '@activepieces/pieces-framework';
import { fellowAuth, getBaseUrl } from './lib/common/auth';
import { PieceCategory } from '@activepieces/shared';
import { getNoteAction } from './lib/actions/get-note';
import { newRecordingTrigger } from './lib/triggers/new-recording';
import { createCustomApiCallAction } from '@activepieces/pieces-common';
export const fellow = createPiece({
displayName: 'Fellow.ai',
description: 'AI Meeting Assistant and Notetaker',
categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE, PieceCategory.PRODUCTIVITY],
auth: fellowAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/fellow.png',
authors: ['kishanprmr'],
actions: [getNoteAction,
createCustomApiCallAction({
auth: fellowAuth,
baseUrl: (auth) => {
return getBaseUrl(auth?.props.subdomain ?? '')
},
authMapping: async (auth) => {
return {
'X-API-KEY': `${auth.props.apiKey}`
}
}
})],
triggers: [newRecordingTrigger],
});

View File

@@ -0,0 +1,31 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { fellowAuth, getBaseUrl } from "../common/auth";
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
export const getNoteAction = createAction({
name: 'get-note',
auth: fellowAuth,
displayName: 'Get AI Note',
description: 'Retrieves a note by its ID.',
props: {
noteId: Property.ShortText({
displayName: 'Note ID',
required: true
})
},
async run(context) {
const { subdomain, apiKey } = context.auth.props;
const { noteId } = context.propsValue;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getBaseUrl(subdomain) + `/note/${noteId}`,
headers: {
'X-API-KEY': apiKey
}
})
return response.body;
}
})

View File

@@ -0,0 +1,43 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
export const fellowAuth = PieceAuth.CustomAuth({
required: true,
props: {
apiKey: Property.ShortText({
displayName: 'API Key',
description: `You can obtain API key by navigating to **User Settings -> Developer Tools**.`,
required: true,
}),
subdomain: Property.ShortText({
displayName: 'Subdomain',
description: `You can obtain your workspace domain from URL.For example,subdomain for 'https://**test**.fellow.app/' is **test**.`,
required: true,
}),
},
validate: async ({ auth }) => {
try {
await httpClient.sendRequest({
method: HttpMethod.GET,
url: getBaseUrl(auth.subdomain) + '/me',
headers: {
'X-API-KEY': auth.apiKey
}
})
return {
valid: true
}
}
catch {
return {
valid: false,
error: 'Invalid Credentials.'
}
}
}
});
export const getBaseUrl = (subdomain: string) => {
return `https://${subdomain}.fellow.app/api/v1`;
};

View File

@@ -0,0 +1,12 @@
export type ListRecordingsResponse = {
recordings:{
page_info:{
cursor:string|null;
page_size:number;
},
data:{
id:string,
started_at:string
}[];
}
}

View File

@@ -0,0 +1,92 @@
import { AppConnectionValueForAuthProperty, createTrigger, TriggerStrategy } from "@activepieces/pieces-framework";
import { fellowAuth, getBaseUrl } from "../common/auth";
import { DedupeStrategy, httpClient, HttpMethod, Polling, pollingHelper } from "@activepieces/pieces-common";
import dayjs from 'dayjs';
import { ListRecordingsResponse } from "../common/types";
import { isNil } from "@activepieces/shared";
const polling: Polling<AppConnectionValueForAuthProperty<typeof fellowAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
async items({ auth, lastFetchEpochMS }) {
const { subdomain, apiKey } = auth.props;
const isTestMode = lastFetchEpochMS === 0;
let hasMore = true;
let cursor: string | null = null;
const recordings = [];
do {
const requestBody: Record<string, any> = {
pagination: {
page_size: 20,
cursor
},
include: {
transcript: true,
ai_notes: true
},
}
if (!isTestMode) {
requestBody['filters'] = {
created_at_start: dayjs(lastFetchEpochMS).toISOString()
}
}
const response = await httpClient.sendRequest<ListRecordingsResponse>({
method: HttpMethod.POST,
url: getBaseUrl(subdomain) + '/recordings',
headers: {
'X-API-KEY': apiKey
},
body: requestBody
})
for (const recording of response.body.recordings.data ?? []) {
recordings.push(recording);
}
if (isTestMode) break;
cursor = response.body.recordings.page_info.cursor;
hasMore = !isNil(cursor);
} while (hasMore)
return recordings.map((rec) => ({
epochMilliSeconds: dayjs(rec.started_at).valueOf(),
data: rec
}))
},
}
export const newRecordingTrigger = createTrigger({
name: 'new-recording',
auth: fellowAuth,
displayName: 'New Recording',
description: 'Triggers when a new recording is created.',
type: TriggerStrategy.POLLING,
props: {},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async test(context) {
return await pollingHelper.test(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
sampleData: undefined
})