Files
elizaos--eliza/plugins/plugin-calendly/src/services/CalendlyService.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

269 lines
7.5 KiB
TypeScript

/**
* @module CalendlyService
* @description Service that owns Calendly API access and exposes a domain-shaped
* surface to actions. Wraps calendly-client.ts and supports N accounts via the
* standard accounts.ts pattern.
*/
import { type IAgentRuntime, logger, Service } from "@elizaos/core";
import {
type CalendlyAccountConfig,
readCalendlyAccounts,
resolveCalendlyAccountId,
} from "../accounts.js";
import {
type CalendlyAvailabilityNormalized,
type CalendlyCredentials,
CalendlyError,
type CalendlyEventTypeNormalized,
type CalendlyScheduledEventNormalized,
type CalendlySingleUseLink,
cancelCalendlyScheduledEvent,
createCalendlySingleUseLink,
getCalendlyAvailability as fetchCalendlyAvailability,
getCalendlyUser,
listCalendlyEventTypes,
listCalendlyScheduledEvents,
} from "../calendly-client.js";
import type {
BookingLinkQuery,
CalendlyAvailability,
CalendlyEventType,
CalendlyScheduledEvent,
} from "../types.js";
export const CALENDLY_SERVICE_TYPE = "calendly";
interface CalendlyClientEntry {
config: CalendlyAccountConfig;
credentials: CalendlyCredentials;
cachedUserUri?: string;
}
function toEventType(
normalized: CalendlyEventTypeNormalized,
): CalendlyEventType {
return {
uri: normalized.uri,
name: normalized.name,
active: normalized.active,
slug: normalized.slug,
scheduling_url: normalized.schedulingUrl,
duration: normalized.durationMinutes,
kind: "solo",
type: "StandardEventType",
description_plain: null,
};
}
function toScheduledEvent(
normalized: CalendlyScheduledEventNormalized,
): CalendlyScheduledEvent {
return {
uri: normalized.uri,
name: normalized.name,
startTime: normalized.startTime,
endTime: normalized.endTime,
status: normalized.status,
invitees: normalized.invitees,
};
}
function toAvailability(
normalized: CalendlyAvailabilityNormalized,
): CalendlyAvailability {
return {
date: normalized.date,
slots: normalized.slots,
};
}
export class CalendlyService extends Service {
static override serviceType = CALENDLY_SERVICE_TYPE;
capabilityDescription =
"Connects the agent to Calendly v2 for event types, scheduled events, availability, and booking-link handoff.";
private clientsByAccountId = new Map<string, CalendlyClientEntry>();
static override async start(
runtime: IAgentRuntime,
): Promise<CalendlyService> {
const service = new CalendlyService(runtime);
service.initialize();
return service;
}
private initialize(): void {
if (!this.runtime) return;
const accounts = readCalendlyAccounts(this.runtime);
if (accounts.length === 0) {
logger.info(
{ src: "plugin:calendly" },
"Calendly access token not configured -- service inactive",
);
return;
}
for (const account of accounts) {
this.clientsByAccountId.set(account.accountId, {
config: account,
credentials: { personalAccessToken: account.accessToken },
});
}
logger.info(
{
src: "plugin:calendly",
accountCount: this.clientsByAccountId.size,
},
`Calendly service initialized with ${this.clientsByAccountId.size} account(s)`,
);
}
/**
* Test hook. Bypasses env-var resolution and registers a single account.
*/
attach(options: {
accountId?: string;
credentials: CalendlyCredentials;
cachedUserUri?: string;
}): void {
const accountId = options.accountId ?? "default";
this.clientsByAccountId.set(accountId, {
config: {
accountId,
accessToken: options.credentials.personalAccessToken,
},
credentials: options.credentials,
cachedUserUri: options.cachedUserUri,
});
}
isConnected(accountId?: string): boolean {
const id =
accountId ??
(this.runtime ? resolveCalendlyAccountId(this.runtime) : undefined);
if (!id) return this.clientsByAccountId.size > 0;
return this.clientsByAccountId.has(id);
}
private requireEntry(accountId?: string): CalendlyClientEntry {
const id =
accountId ??
(this.runtime ? resolveCalendlyAccountId(this.runtime) : undefined);
if (id) {
const entry = this.clientsByAccountId.get(id);
if (entry) return entry;
}
const first = this.clientsByAccountId.values().next().value;
if (first) return first;
throw new CalendlyError(
"Calendly is not connected -- configure CALENDLY_ACCESS_TOKEN.",
0,
);
}
async listEventTypes(accountId?: string): Promise<CalendlyEventType[]> {
const entry = this.requireEntry(accountId);
const normalized = await listCalendlyEventTypes(entry.credentials);
return normalized.map(toEventType);
}
async listScheduledEvents(
options?: Record<string, unknown>,
accountId?: string,
): Promise<CalendlyScheduledEvent[]> {
const entry = this.requireEntry(accountId);
const minStartTime =
typeof options?.minStartTime === "string"
? options.minStartTime
: undefined;
const maxStartTime =
typeof options?.maxStartTime === "string"
? options.maxStartTime
: undefined;
const status =
options?.status === "active" || options?.status === "canceled"
? options.status
: undefined;
const limit =
typeof options?.limit === "number" ? options.limit : undefined;
const normalized = await listCalendlyScheduledEvents(entry.credentials, {
minStartTime,
maxStartTime,
status,
limit,
});
return normalized.map(toScheduledEvent);
}
async getAvailability(
eventTypeUri: string,
options: { startDate: string; endDate: string; timezone?: string },
accountId?: string,
): Promise<CalendlyAvailability[]> {
const entry = this.requireEntry(accountId);
const normalized = await fetchCalendlyAvailability(
entry.credentials,
eventTypeUri,
options,
);
return normalized.map(toAvailability);
}
async createSingleUseLink(
eventTypeUri: string,
accountId?: string,
): Promise<CalendlySingleUseLink> {
const entry = this.requireEntry(accountId);
return createCalendlySingleUseLink(entry.credentials, eventTypeUri);
}
async getBookingUrl(
query?: BookingLinkQuery,
accountId?: string,
): Promise<string | null> {
const entry = this.requireEntry(accountId);
const eventTypes = await listCalendlyEventTypes(entry.credentials);
const active = eventTypes.filter((et) => et.active);
if (active.length === 0) return null;
if (query?.slug) {
const match = active.find((et) => et.slug === query.slug);
if (match) return match.schedulingUrl;
}
if (typeof query?.durationMinutes === "number") {
const match = active.find(
(et) => et.durationMinutes === query.durationMinutes,
);
if (match) return match.schedulingUrl;
}
return active[0]?.schedulingUrl ?? null;
}
async cancelBooking(
uuid: string,
reason?: string,
accountId?: string,
): Promise<void> {
const entry = this.requireEntry(accountId);
await cancelCalendlyScheduledEvent(entry.credentials, uuid, reason);
}
async getCachedUserUri(accountId?: string): Promise<string | undefined> {
const entry = this.requireEntry(accountId);
if (entry.cachedUserUri) return entry.cachedUserUri;
try {
const user = await getCalendlyUser(entry.credentials);
entry.cachedUserUri = user.uri;
return user.uri;
} catch {
return undefined;
}
}
async stop(): Promise<void> {
this.clientsByAccountId.clear();
}
}
export default CalendlyService;