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,52 @@
import { propsValidation } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { PromptHubClient } from '../common/client';
import { getProjectHeadProps, getProjectHeadSchema, sanitizeVariables } from '../common/props';
import { prompthubAuth } from '../..';
export const getProjectHead = createAction({
name: 'get_project_head',
displayName: 'Get Project Head',
description: 'Get the production-ready version of a PromptHub project (typically the last commit on master/main branch). Useful for integrating prompts into your application.',
props: getProjectHeadProps,
auth: prompthubAuth,
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, getProjectHeadSchema);
const client = new PromptHubClient(auth.secret_text);
const q: Record<string, any> = {};
if (propsValue['branch']) {
q['branch'] = propsValue['branch'];
}
if (propsValue['fallback'] !== undefined) {
q['fallback'] = propsValue['fallback'] ? 1 : 0;
}
const vars = sanitizeVariables(propsValue['variables'] ?? {});
for (const [k, v] of Object.entries(vars)) {
q[`variables[${encodeURIComponent(k)}]`] = encodeURIComponent(String(v));
}
const result = await client.getProjectHead(propsValue['projectId'], q);
const data = result?.data ?? result;
return {
id: data?.id,
provider: data?.provider,
model: data?.model,
prompt: data?.prompt,
system_message: data?.system_message,
formatted_request: data?.formatted_request,
hash: data?.hash,
commit_title: data?.commit_title,
commit_description: data?.commit_description,
variables: data?.variables,
project: data?.project,
branch: data?.branch,
configuration: data?.configuration,
};
},
});

View File

@@ -0,0 +1,47 @@
import { propsValidation } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { PromptHubClient } from '../common/client';
import { listProjectsProps, listProjectsSchema } from '../common/props';
import { prompthubAuth } from '../..';
export const listProjects = createAction({
name: 'list_projects',
displayName: 'List Projects',
description: 'List PromptHub projects for a team. Returns information about each project\'s head revision and groups.',
props: listProjectsProps,
auth: prompthubAuth,
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, listProjectsSchema);
const client = new PromptHubClient(auth.secret_text);
const result = await client.listProjects(propsValue['teamId'], {
group: propsValue['group'],
model: propsValue['model'],
provider: propsValue['provider'],
});
const data = result?.data ?? result;
return Array.isArray(data)
? data.map((p: any) => ({
id: p.id,
name: p.name,
description: p.description,
head: p.head ? {
provider: p.head.provider,
model: p.head.model,
prompt: p.head.prompt,
system_message: p.head.system_message,
formatted_request: p.head.formatted_request,
hash: p.head.hash,
commit_title: p.head.commit_title,
commit_description: p.head.commit_description,
variables: p.head.variables,
branch: p.head.branch,
configuration: p.head.configuration,
} : null,
groups: p.groups || [],
}))
: data;
},
});

View File

@@ -0,0 +1,29 @@
import { propsValidation } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { PromptHubClient } from '../common/client';
import { runPromptProps, runPromptSchema, sanitizeVariables } from '../common/props';
import { prompthubAuth } from '../..';
export const runPrompt = createAction({
name: 'run_prompt',
displayName: 'Run Prompt',
description: 'Run a PromptHub project with optional variables, branch/hash, and chat payload',
props: runPromptProps,
auth: prompthubAuth,
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, runPromptSchema);
const client = new PromptHubClient(auth.secret_text);
const body: Record<string, any> = {};
if (propsValue['branch']) body['branch'] = propsValue['branch'];
if (propsValue['hash']) body['hash'] = propsValue['hash'];
if (propsValue['variables']) body['variables'] = sanitizeVariables(propsValue['variables']);
if (propsValue['messages']) body['messages'] = propsValue['messages'];
if (propsValue['prompt']) body['prompt'] = propsValue['prompt'];
if (propsValue['metadata']) body['metadata'] = propsValue['metadata'];
const timeoutMs = propsValue['timeoutSeconds'] ? propsValue['timeoutSeconds'] * 1000 : undefined;
const result = await client.runProject(propsValue['projectId'], body, timeoutMs);
return result?.data ?? result;
},
});

View File

@@ -0,0 +1,139 @@
import { httpClient, HttpMethod, HttpRequest, AuthenticationType } from '@activepieces/pieces-common';
export interface PromptHubClientOptions {
baseUrl?: string;
timeoutMs?: number;
maxRetries?: number;
initialBackoffMs?: number;
}
export class PromptHubClient {
private readonly baseUrl: string;
private readonly timeoutMs: number;
private readonly maxRetries: number;
private readonly initialBackoffMs: number;
constructor(private readonly apiKey: string, options?: PromptHubClientOptions) {
this.baseUrl = (options?.baseUrl ?? 'https://app.prompthub.us/api/v1').replace(/\/$/, '');
this.timeoutMs = options?.timeoutMs ?? 30000;
this.maxRetries = options?.maxRetries ?? 3;
this.initialBackoffMs = options?.initialBackoffMs ?? 500;
}
async validateToken(): Promise<boolean> {
try {
await this.get('/me');
return true;
} catch (e: any) {
if (e?.statusCode === 401 || e?.statusCode === 403) return false;
throw e;
}
}
async listProjects(teamId: number, query?: Record<string, string | number | boolean | undefined>) {
return this.get(`/teams/${teamId}/projects`, query);
}
async getProjectHead(projectId: number, query?: Record<string, string | number | boolean | undefined>) {
return this.get(`/projects/${projectId}/head`, query);
}
async runProject(projectId: number, body: any, timeoutMs?: number) {
return this.post(`/projects/${projectId}/run`, body, timeoutMs);
}
private async get(path: string, query?: Record<string, string | number | boolean | undefined>) {
const url = `${this.baseUrl}${path}`;
const req: HttpRequest = {
method: HttpMethod.GET,
url,
queryParams: this.buildQuery(query),
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: this.apiKey,
},
timeout: this.timeoutMs,
headers: {
Accept: 'application/json',
},
};
return this.sendWithRetry(req);
}
private async post(path: string, body: unknown, timeoutOverrideMs?: number) {
const url = `${this.baseUrl}${path}`;
const req: HttpRequest = {
method: HttpMethod.POST,
url,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: this.apiKey,
},
timeout: timeoutOverrideMs ?? this.timeoutMs,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
};
return this.sendWithRetry(req);
}
private async sendWithRetry(request: HttpRequest): Promise<any> {
let attempt = 0;
let lastError: any;
while (attempt <= this.maxRetries) {
try {
const res = await httpClient.sendRequest<any>(request);
const status = res.status ?? 200;
if (status >= 200 && status < 300) {
return res.body;
}
const error = new Error(`PromptHub API error ${status}`) as any;
error.statusCode = status;
error.body = res.body;
throw error;
} catch (err: any) {
lastError = err;
const status = err?.statusCode ?? err?.status;
const isRateLimit = status === 429;
const isAuth = status === 401 || status === 403;
const isServer = status >= 500 && status < 600;
if (isAuth) {
err.message = 'Unauthorized or forbidden. Check PromptHub token and permissions (team/project access).';
throw err;
}
if (!(isRateLimit || isServer)) {
throw err;
}
if (attempt === this.maxRetries) {
break;
}
const backoff = this.exponentialBackoffWithJitter(attempt);
await this.sleep(backoff);
attempt++;
}
}
throw lastError;
}
private buildQuery(query?: Record<string, string | number | boolean | undefined>): Record<string, string> | undefined {
if (!query) return undefined;
const out: Record<string, string> = {};
Object.entries(query).forEach(([k, v]) => {
if (v === undefined || v === null) return;
out[k] = String(v);
});
return out;
}
private exponentialBackoffWithJitter(attempt: number): number {
const base = this.initialBackoffMs * Math.pow(2, attempt);
const jitter = Math.floor(Math.random() * this.initialBackoffMs);
return base + jitter;
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,156 @@
import { Property } from '@activepieces/pieces-framework';
import z, { ZodTypeAny } from 'zod';
export const listProjectsProps = {
teamId: Property.Number({
displayName: 'Team ID',
description: 'The ID of the team. Can be found in your browser\'s URL bar when viewing Team Settings, or use current_team_id from /me endpoint.',
required: true
}),
group: Property.ShortText({
displayName: 'Group Name',
description: 'Filter projects from a specific group. Must be URL-encoded if the group name contains spaces.',
required: false
}),
model: Property.ShortText({
displayName: 'Model',
description: 'Filter projects where the head uses a specific model (e.g., gpt-4, claude-2, etc.).',
required: false,
}),
provider: Property.StaticDropdown({
displayName: 'Provider',
description: 'Filter projects where the head uses a specific model provider.',
required: false,
options: {
options: [
{ label: 'OpenAI', value: 'OpenAI' },
{ label: 'Anthropic', value: 'Anthropic' },
{ label: 'Azure', value: 'Azure' },
{ label: 'Google', value: 'Google' },
{ label: 'Amazon', value: 'Amazon' },
],
},
}),
};
export const getProjectHeadProps = {
projectId: Property.Number({
displayName: 'Project ID',
description: 'The ID of the project. Can be found in your browser\'s URL bar when viewing that project.',
required: true
}),
branch: Property.ShortText({
displayName: 'Branch',
description: 'Use the head of a specific branch. Defaults to your project\'s master/main branch. Must be URL-encoded if the branch name contains spaces or special characters.',
required: false
}),
fallback: Property.Checkbox({
displayName: 'Use Fallback',
description: 'When enabled, any placeholders not provided in variables will fall back to your Project/Team defaults. Your variable overrides always take precedence.',
required: false
}),
variables: Property.Object({
displayName: 'Variables',
description: 'Key-value pairs to override placeholders in the prompt. Both keys and values will be URL-encoded automatically.',
required: false
}),
};
export const runPromptProps = {
projectId: Property.Number({
displayName: 'Project ID',
description: 'The ID of the project to run.',
required: true
}),
branch: Property.ShortText({
displayName: 'Branch',
description: 'The branch name to run from (defaults to main/master).',
required: false
}),
hash: Property.ShortText({
displayName: 'Hash',
description: 'Specific commit hash to run from.',
required: false
}),
variables: Property.Object({
displayName: 'Variables',
description: 'Key-value pairs to pass as variables to the prompt.',
required: false
}),
messages: Property.Array({
displayName: 'Messages',
description: 'Chat messages for chat-based projects.',
required: false,
properties: {
role: Property.StaticDropdown({
displayName: 'Role',
required: true,
options: {
options: [
{ label: 'system', value: 'system' },
{ label: 'user', value: 'user' },
{ label: 'assistant', value: 'assistant' },
],
},
}),
content: Property.LongText({ displayName: 'Content', required: true }),
},
}),
prompt: Property.LongText({
displayName: 'Prompt',
description: 'Override prompt text for the project.',
required: false
}),
metadata: Property.Object({
displayName: 'Metadata',
description: 'Additional metadata to include with the run.',
required: false
}),
timeoutSeconds: Property.Number({
displayName: 'Timeout Seconds',
description: 'Request timeout in seconds (max 600).',
required: false
}),
};
export const listProjectsSchema: Record<string, ZodTypeAny> = {
teamId: z.number().int().positive(),
group: z.string().optional(),
};
export const getProjectHeadSchema: Record<string, ZodTypeAny> = {
projectId: z.number().int().positive(),
branch: z.string().optional(),
variables: z.record(z.any()).optional(),
fallback: z.boolean().optional(),
};
export const runPromptSchema: Record<string, ZodTypeAny> = {
projectId: z.number().int().positive(),
branch: z.string().optional(),
hash: z.string().optional(),
variables: z.record(z.any()).optional(),
messages: z.array(z.object({
role: z.enum(['system', 'user', 'assistant']),
content: z.string(),
})).optional(),
prompt: z.string().optional(),
metadata: z.record(z.any()).optional(),
timeoutSeconds: z.number().int().positive().max(600).optional(),
};
export function sanitizeVariables(vars: Record<string, unknown>): Record<string, string | number | boolean | null> {
const out: Record<string, string | number | boolean | null> = {};
for (const [k, v] of Object.entries(vars)) {
if (v === undefined) continue;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) {
out[k] = v;
} else if (typeof v === 'object') {
out[k] = JSON.stringify(v);
} else {
out[k] = String(v);
}
}
return out;
}