diff --git a/apps/bubble-studio/src/pages/CredentialsPage.tsx b/apps/bubble-studio/src/pages/CredentialsPage.tsx index a5698a49..75ced4a1 100644 --- a/apps/bubble-studio/src/pages/CredentialsPage.tsx +++ b/apps/bubble-studio/src/pages/CredentialsPage.tsx @@ -117,6 +117,7 @@ const getServiceNameForCredentialType = ( [CredentialType.METABASE_CRED]: 'Metabase', [CredentialType.CLERK_CRED]: 'Clerk', [CredentialType.CLERK_API_KEY]: 'Clerk', + [CredentialType.GRANOLA_API_KEY]: 'Granola', }; return typeToServiceMap[credentialType] || credentialType; diff --git a/packages/bubble-core/package.json b/packages/bubble-core/package.json index ef7a2d37..f621b2f4 100644 --- a/packages/bubble-core/package.json +++ b/packages/bubble-core/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/bubble-core", - "version": "0.1.289", + "version": "0.1.291", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/bubble-core/src/bubble-factory.ts b/packages/bubble-core/src/bubble-factory.ts index a773e150..a7a64e4d 100644 --- a/packages/bubble-core/src/bubble-factory.ts +++ b/packages/bubble-core/src/bubble-factory.ts @@ -191,6 +191,7 @@ export class BubbleFactory { 'docusign', 'metabase', 'clerk', + 'granola', ]; } @@ -462,6 +463,9 @@ export class BubbleFactory { const { ClerkBubble } = await import( './bubbles/service-bubble/clerk/index.js' ); + const { GranolaBubble } = await import( + './bubbles/service-bubble/granola/index.js' + ); // Create the default factory instance this.register('hello-world', HelloWorldBubble as BubbleClassWithMetadata); @@ -638,6 +642,7 @@ export class BubbleFactory { this.register('docusign', DocuSignBubble as BubbleClassWithMetadata); this.register('metabase', MetabaseBubble as BubbleClassWithMetadata); this.register('clerk', ClerkBubble as BubbleClassWithMetadata); + this.register('granola', GranolaBubble as BubbleClassWithMetadata); // After all default bubbles are registered, auto-populate bubbleDependencies if (!BubbleFactory.dependenciesPopulated) { diff --git a/packages/bubble-core/src/bubbles/service-bubble/granola/granola.integration.flow.ts b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.integration.flow.ts new file mode 100644 index 00000000..9e07c30a --- /dev/null +++ b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.integration.flow.ts @@ -0,0 +1,132 @@ +import { + BubbleFlow, + GranolaBubble, + type WebhookEvent, +} from '@bubblelab/bubble-core'; + +export interface Output { + noteId: string; + testResults: { + operation: string; + success: boolean; + details?: string; + }[]; +} + +export interface TestPayload extends WebhookEvent { + testName?: string; +} + +export class GranolaIntegrationTest extends BubbleFlow<'webhook/http'> { + async handle(payload: TestPayload): Promise { + const results: Output['testResults'] = []; + + // 1. List notes + const listResult = await new GranolaBubble({ + operation: 'list_notes', + page_size: 5, + }).action(); + + results.push({ + operation: 'list_notes', + success: listResult.success, + details: listResult.success + ? `Retrieved ${listResult.notes?.length ?? 0} notes, hasMore: ${listResult.hasMore}` + : listResult.error, + }); + + // 2. List notes with date filter + const filteredResult = await new GranolaBubble({ + operation: 'list_notes', + page_size: 3, + created_after: '2024-01-01', + }).action(); + + results.push({ + operation: 'list_notes (date filter)', + success: filteredResult.success, + details: filteredResult.success + ? `Retrieved ${filteredResult.notes?.length ?? 0} notes after 2024-01-01` + : filteredResult.error, + }); + + // 3. Get a specific note (use the first note from list if available) + const firstNoteId = listResult.success + ? listResult.notes?.[0]?.id + : undefined; + const noteId = firstNoteId || ''; + + if (firstNoteId) { + const getResult = await new GranolaBubble({ + operation: 'get_note', + note_id: firstNoteId, + include_transcript: false, + }).action(); + + results.push({ + operation: 'get_note', + success: getResult.success, + details: getResult.success + ? `Retrieved note: "${getResult.note?.title}" with ${getResult.note?.attendees?.length ?? 0} attendees` + : getResult.error, + }); + + // 4. Get same note with transcript + const transcriptResult = await new GranolaBubble({ + operation: 'get_note', + note_id: firstNoteId, + include_transcript: true, + }).action(); + + results.push({ + operation: 'get_note (with transcript)', + success: transcriptResult.success, + details: transcriptResult.success + ? `Transcript entries: ${transcriptResult.note?.transcript?.length ?? 'null (no transcript)'}` + : transcriptResult.error, + }); + } else { + results.push({ + operation: 'get_note', + success: false, + details: 'Skipped - no notes available from list_notes', + }); + } + + // 5. Test pagination with cursor + if (listResult.success && listResult.hasMore && listResult.cursor) { + const paginatedResult = await new GranolaBubble({ + operation: 'list_notes', + page_size: 5, + cursor: listResult.cursor, + }).action(); + + results.push({ + operation: 'list_notes (pagination)', + success: paginatedResult.success, + details: paginatedResult.success + ? `Page 2: ${paginatedResult.notes?.length ?? 0} notes` + : paginatedResult.error, + }); + } + + // 6. Test error handling - invalid note ID + const invalidResult = await new GranolaBubble({ + operation: 'get_note', + note_id: 'not_INVALID000000', + }).action(); + + results.push({ + operation: 'get_note (invalid ID)', + success: !invalidResult.success, // We expect this to fail + details: !invalidResult.success + ? `Correctly returned error: ${invalidResult.error}` + : 'Unexpectedly succeeded with invalid ID', + }); + + return { + noteId, + testResults: results, + }; + } +} diff --git a/packages/bubble-core/src/bubbles/service-bubble/granola/granola.schema.ts b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.schema.ts new file mode 100644 index 00000000..bb60151f --- /dev/null +++ b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.schema.ts @@ -0,0 +1,245 @@ +import { z } from 'zod'; +import { CredentialType } from '@bubblelab/shared-schemas'; + +// ============================================================================ +// DATA SCHEMAS - Granola API Response Types +// ============================================================================ + +/** + * User/owner object from Granola API + */ +export const GranolaUserSchema = z + .object({ + name: z.string().nullable().describe('User display name'), + email: z.string().describe('User email address'), + }) + .describe('Granola user information'); + +/** + * Calendar invitee from Granola API + */ +export const GranolaCalendarInviteeSchema = z + .object({ + email: z.string().describe('Invitee email address'), + }) + .describe('Calendar event invitee'); + +/** + * Calendar event associated with a note + */ +export const GranolaCalendarEventSchema = z + .object({ + event_title: z.string().nullable().describe('Calendar event title'), + invitees: z + .array(GranolaCalendarInviteeSchema) + .describe('List of event invitees'), + organiser: z.string().nullable().describe('Event organiser email'), + calendar_event_id: z + .string() + .nullable() + .describe('External calendar event ID'), + scheduled_start_time: z + .string() + .nullable() + .describe('Scheduled start time (ISO 8601)'), + scheduled_end_time: z + .string() + .nullable() + .describe('Scheduled end time (ISO 8601)'), + }) + .describe('Calendar event details'); + +/** + * Folder membership from Granola API + */ +export const GranolaFolderSchema = z + .object({ + id: z.string().describe('Folder ID'), + object: z.literal('folder').describe('Object type'), + name: z.string().describe('Folder name'), + }) + .describe('Granola folder'); + +/** + * Transcript speaker from Granola API + */ +export const GranolaSpeakerSchema = z + .object({ + source: z + .enum(['microphone', 'speaker']) + .describe('Audio source (microphone or speaker)'), + diarization_label: z + .string() + .optional() + .describe('Speaker label (iOS only when diarization available)'), + }) + .describe('Transcript speaker information'); + +/** + * Transcript entry from Granola API + */ +export const GranolaTranscriptEntrySchema = z + .object({ + speaker: GranolaSpeakerSchema.describe('Speaker information'), + text: z.string().describe('Transcript text content'), + start_time: z.string().describe('Start time (ISO 8601)'), + end_time: z.string().describe('End time (ISO 8601)'), + }) + .describe('Single transcript entry'); + +/** + * Note summary (returned by list_notes) + */ +export const GranolaNoteSummarySchema = z + .object({ + id: z.string().describe('Note ID (format: not_XXXXXXXXXXXXXX)'), + object: z.literal('note').describe('Object type'), + title: z.string().nullable().describe('Meeting title'), + owner: GranolaUserSchema.describe('Note owner'), + created_at: z.string().describe('Creation timestamp (ISO 8601)'), + updated_at: z.string().describe('Last updated timestamp (ISO 8601)'), + }) + .describe('Granola note summary'); + +/** + * Full note (returned by get_note) + */ +export const GranolaNoteSchema = GranolaNoteSummarySchema.extend({ + calendar_event: GranolaCalendarEventSchema.nullable().describe( + 'Associated calendar event' + ), + attendees: z.array(GranolaUserSchema).describe('Meeting attendees'), + folder_membership: z + .array(GranolaFolderSchema) + .describe('Folders this note belongs to'), + summary_text: z.string().describe('Plain text summary of the meeting'), + summary_markdown: z + .string() + .nullable() + .describe('Markdown-formatted summary'), + transcript: z + .array(GranolaTranscriptEntrySchema) + .nullable() + .describe('Transcript entries (null unless include=transcript)'), +}).describe('Full Granola note with details'); + +// ============================================================================ +// PARAMETER SCHEMAS - Operation-specific input types +// ============================================================================ + +const credentialsField = z + .record(z.nativeEnum(CredentialType), z.string()) + .optional() + .describe('Credential map for authentication'); + +/** + * List notes operation parameters + */ +const ListNotesSchema = z.object({ + operation: z + .literal('list_notes') + .transform(() => 'list_notes' as const) + .describe('List accessible meeting notes with pagination'), + created_before: z + .string() + .optional() + .describe( + 'Filter notes created before this date (ISO 8601 date or datetime)' + ), + created_after: z + .string() + .optional() + .describe( + 'Filter notes created after this date (ISO 8601 date or datetime)' + ), + updated_after: z + .string() + .optional() + .describe( + 'Filter notes updated after this date (ISO 8601 date or datetime)' + ), + cursor: z + .string() + .optional() + .describe('Pagination cursor from previous response'), + page_size: z + .number() + .int() + .min(1) + .max(30) + .optional() + .default(10) + .describe('Number of notes per page (1-30, default 10)'), + credentials: credentialsField, +}); + +/** + * Get note operation parameters + */ +const GetNoteSchema = z.object({ + operation: z + .literal('get_note') + .transform(() => 'get_note' as const) + .describe('Retrieve a single note with full details'), + note_id: z + .string() + .describe('The note ID to retrieve (format: not_XXXXXXXXXXXXXX)'), + include_transcript: z + .boolean() + .optional() + .default(false) + .describe('Whether to include the transcript in the response'), + credentials: credentialsField, +}); + +// ============================================================================ +// COMBINED SCHEMAS +// ============================================================================ + +export const GranolaParamsSchema = z.discriminatedUnion('operation', [ + ListNotesSchema, + GetNoteSchema, +]); + +export const GranolaResultSchema = z.discriminatedUnion('operation', [ + z.object({ + operation: z.literal('list_notes'), + success: z.boolean(), + notes: z.array(GranolaNoteSummarySchema).optional(), + hasMore: z.boolean().optional(), + cursor: z.string().nullable().optional(), + error: z.string().describe('Error message if operation failed'), + }), + z.object({ + operation: z.literal('get_note'), + success: z.boolean(), + note: GranolaNoteSchema.optional(), + error: z.string().describe('Error message if operation failed'), + }), +]); + +// ============================================================================ +// TYPE EXPORTS +// ============================================================================ + +export type GranolaParams = z.output; +export type GranolaParamsInput = z.input; +export type GranolaResult = z.output; + +export type GranolaListNotesParams = Extract< + GranolaParams, + { operation: 'list_notes' } +>; +export type GranolaGetNoteParams = Extract< + GranolaParams, + { operation: 'get_note' } +>; + +export type GranolaNoteSummary = z.output; +export type GranolaNote = z.output; +export type GranolaUser = z.output; +export type GranolaCalendarEvent = z.output; +export type GranolaFolder = z.output; +export type GranolaTranscriptEntry = z.output< + typeof GranolaTranscriptEntrySchema +>; diff --git a/packages/bubble-core/src/bubbles/service-bubble/granola/granola.ts b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.ts new file mode 100644 index 00000000..d83132b8 --- /dev/null +++ b/packages/bubble-core/src/bubbles/service-bubble/granola/granola.ts @@ -0,0 +1,244 @@ +import { CredentialType } from '@bubblelab/shared-schemas'; +import { ServiceBubble } from '../../../types/service-bubble-class.js'; +import type { BubbleContext } from '../../../types/bubble.js'; +import { + GranolaParamsSchema, + GranolaResultSchema, + type GranolaParams, + type GranolaParamsInput, + type GranolaResult, + type GranolaListNotesParams, + type GranolaGetNoteParams, +} from './granola.schema.js'; + +const GRANOLA_API_BASE = 'https://public-api.granola.ai'; + +/** + * GranolaBubble - Integration with Granola meeting notes API + * + * Provides read-only operations for accessing Granola meeting notes: + * - List notes with pagination and date filtering + * - Get note details with optional transcript + * + * @example + * ```typescript + * // List recent notes + * const result = await new GranolaBubble({ + * operation: 'list_notes', + * page_size: 10, + * }).action(); + * + * // Get a specific note with transcript + * const note = await new GranolaBubble({ + * operation: 'get_note', + * note_id: 'not_1d3tmYTlCICgjy', + * include_transcript: true, + * }).action(); + * ``` + */ +export class GranolaBubble< + T extends GranolaParamsInput = GranolaParamsInput, +> extends ServiceBubble< + T, + Extract +> { + static readonly service = 'granola'; + static readonly authType = 'apikey' as const; + static readonly bubbleName = 'granola' as const; + static readonly type = 'service' as const; + static readonly schema = GranolaParamsSchema; + static readonly resultSchema = GranolaResultSchema; + static readonly shortDescription = + 'Granola meeting notes and transcription integration'; + static readonly longDescription = ` + Granola is an AI meeting notes tool that captures and summarizes meetings. + This bubble provides read-only access to: + - List meeting notes with filtering by creation/update dates + - Paginate through notes using cursor-based pagination + - Get full note details including summaries, attendees, and calendar events + - Optionally retrieve meeting transcripts + + Authentication: + - Uses Bearer token (API key) from Granola Settings > API + - Requires Business or Enterprise plan for personal keys + - Enterprise keys access all Team space notes + + Rate Limits: + - 25 request burst capacity + - 5 requests/second sustained rate + `; + static readonly alias = 'granola-notes'; + + constructor( + params: T = { + operation: 'list_notes', + page_size: 10, + } as T, + context?: BubbleContext + ) { + super(params, context); + } + + protected chooseCredential(): string | undefined { + const params = this.params as GranolaParams; + const credentials = params.credentials; + if (!credentials || typeof credentials !== 'object') { + return undefined; + } + return credentials[CredentialType.GRANOLA_API_KEY]; + } + + async testCredential(): Promise { + const apiKey = this.chooseCredential(); + if (!apiKey) { + return false; + } + + // Validate by listing notes with page_size=1 + const response = await fetch(`${GRANOLA_API_BASE}/v1/notes?page_size=1`, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Granola API key validation failed: ${response.status}`); + } + return true; + } + + protected async performAction( + context?: BubbleContext + ): Promise> { + void context; + + const params = this.params as GranolaParams; + const { operation } = params; + + try { + switch (operation) { + case 'list_notes': + return (await this.listNotes( + params as GranolaListNotesParams + )) as Extract; + + case 'get_note': + return (await this.getNote( + params as GranolaGetNoteParams + )) as Extract; + + default: + return { + operation: operation as T['operation'], + success: false, + error: `Unknown operation: ${operation}`, + } as unknown as Extract; + } + } catch (error) { + return { + operation: operation as T['operation'], + success: false, + error: error instanceof Error ? error.message : String(error), + } as unknown as Extract; + } + } + + // ============================================================================ + // API Methods + // ============================================================================ + + private async listNotes( + params: GranolaListNotesParams + ): Promise> { + const queryParams = new URLSearchParams(); + + if (params.created_before) + queryParams.set('created_before', params.created_before); + if (params.created_after) + queryParams.set('created_after', params.created_after); + if (params.updated_after) + queryParams.set('updated_after', params.updated_after); + if (params.cursor) queryParams.set('cursor', params.cursor); + if (params.page_size) + queryParams.set('page_size', String(params.page_size)); + + const url = `${GRANOLA_API_BASE}/v1/notes${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; + const response = await this.makeGranolaRequest(url); + + if (!response.ok) { + const errorText = await response.text(); + return { + operation: 'list_notes', + success: false, + error: `Granola API error (${response.status}): ${errorText}`, + }; + } + + const data = (await response.json()) as { + notes: Extract['notes']; + hasMore: boolean; + cursor: string | null; + }; + return { + operation: 'list_notes', + success: true, + error: '', + notes: data.notes, + hasMore: data.hasMore, + cursor: data.cursor, + }; + } + + private async getNote( + params: GranolaGetNoteParams + ): Promise> { + const queryParams = new URLSearchParams(); + if (params.include_transcript) { + queryParams.set('include', 'transcript'); + } + + const url = `${GRANOLA_API_BASE}/v1/notes/${params.note_id}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; + const response = await this.makeGranolaRequest(url); + + if (!response.ok) { + const errorText = await response.text(); + return { + operation: 'get_note', + success: false, + error: `Granola API error (${response.status}): ${errorText}`, + }; + } + + const note = (await response.json()) as Extract< + GranolaResult, + { operation: 'get_note' } + >['note']; + return { + operation: 'get_note', + success: true, + error: '', + note, + }; + } + + // ============================================================================ + // Helpers + // ============================================================================ + + private async makeGranolaRequest(url: string): Promise { + const apiKey = this.chooseCredential(); + if (!apiKey) { + throw new Error('No Granola API key provided'); + } + + return fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + } +} diff --git a/packages/bubble-core/src/bubbles/service-bubble/granola/index.ts b/packages/bubble-core/src/bubbles/service-bubble/granola/index.ts new file mode 100644 index 00000000..a0695ce2 --- /dev/null +++ b/packages/bubble-core/src/bubbles/service-bubble/granola/index.ts @@ -0,0 +1,24 @@ +export { GranolaBubble } from './granola.js'; +export { + GranolaParamsSchema, + GranolaResultSchema, + GranolaNoteSummarySchema, + GranolaNoteSchema, + GranolaUserSchema, + GranolaCalendarEventSchema, + GranolaFolderSchema, + GranolaTranscriptEntrySchema, + GranolaSpeakerSchema, + GranolaCalendarInviteeSchema, + type GranolaParams, + type GranolaParamsInput, + type GranolaResult, + type GranolaListNotesParams, + type GranolaGetNoteParams, + type GranolaNoteSummary, + type GranolaNote, + type GranolaUser, + type GranolaCalendarEvent, + type GranolaFolder, + type GranolaTranscriptEntry, +} from './granola.schema.js'; diff --git a/packages/bubble-core/src/index.ts b/packages/bubble-core/src/index.ts index 1d80edd7..5d779a85 100644 --- a/packages/bubble-core/src/index.ts +++ b/packages/bubble-core/src/index.ts @@ -118,6 +118,16 @@ export { type ClerkParamsInput, type ClerkResult, } from './bubbles/service-bubble/clerk/index.js'; +export { + GranolaBubble, + GranolaParamsSchema, + GranolaResultSchema, + type GranolaParams, + type GranolaParamsInput, + type GranolaResult, + type GranolaNoteSummary, + type GranolaNote, +} from './bubbles/service-bubble/granola/index.js'; export { ConfluenceBubble } from './bubbles/service-bubble/confluence/index.js'; export type { ConfluenceParamsInput } from './bubbles/service-bubble/confluence/index.js'; export { AshbyBubble } from './bubbles/service-bubble/ashby/index.js'; diff --git a/packages/bubble-runtime/package.json b/packages/bubble-runtime/package.json index a7ce8d54..ff1e82d3 100644 --- a/packages/bubble-runtime/package.json +++ b/packages/bubble-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/bubble-runtime", - "version": "0.1.289", + "version": "0.1.291", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/bubble-scope-manager/package.json b/packages/bubble-scope-manager/package.json index 935c2b0c..3b36a9f3 100644 --- a/packages/bubble-scope-manager/package.json +++ b/packages/bubble-scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/ts-scope-manager", - "version": "0.1.289", + "version": "0.1.291", "private": false, "license": "MIT", "type": "commonjs", diff --git a/packages/bubble-shared-schemas/package.json b/packages/bubble-shared-schemas/package.json index a94d6427..6e007767 100644 --- a/packages/bubble-shared-schemas/package.json +++ b/packages/bubble-shared-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/shared-schemas", - "version": "0.1.289", + "version": "0.1.291", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/bubble-shared-schemas/src/bubble-definition-schema.ts b/packages/bubble-shared-schemas/src/bubble-definition-schema.ts index 1275c573..236c50ff 100644 --- a/packages/bubble-shared-schemas/src/bubble-definition-schema.ts +++ b/packages/bubble-shared-schemas/src/bubble-definition-schema.ts @@ -87,6 +87,7 @@ export const CREDENTIAL_CONFIGURATION_MAP: Record< [CredentialType.METABASE_CRED]: {}, [CredentialType.CLERK_CRED]: {}, [CredentialType.CLERK_API_KEY]: {}, + [CredentialType.GRANOLA_API_KEY]: {}, [CredentialType.CREDENTIAL_WILDCARD]: {}, // Wildcard marker, not a real credential }; diff --git a/packages/bubble-shared-schemas/src/capability-schema.ts b/packages/bubble-shared-schemas/src/capability-schema.ts index a15b92fd..06743f4d 100644 --- a/packages/bubble-shared-schemas/src/capability-schema.ts +++ b/packages/bubble-shared-schemas/src/capability-schema.ts @@ -49,7 +49,8 @@ export type CapabilityId = | 'discord-assistant' | 'docusign-assistant' | 'metabase-assistant' - | 'clerk-assistant'; + | 'clerk-assistant' + | 'granola-assistant'; /** * Schema for a provider entry in a capability's metadata. diff --git a/packages/bubble-shared-schemas/src/credential-schema.ts b/packages/bubble-shared-schemas/src/credential-schema.ts index f67264ab..fab528f1 100644 --- a/packages/bubble-shared-schemas/src/credential-schema.ts +++ b/packages/bubble-shared-schemas/src/credential-schema.ts @@ -687,6 +687,14 @@ export const CREDENTIAL_TYPE_CONFIG: Record = namePlaceholder: 'My Clerk Secret Key', credentialConfigurations: {}, }, + [CredentialType.GRANOLA_API_KEY]: { + label: 'Granola', + description: + 'API key for Granola meeting notes (requires Business or Enterprise plan)', + placeholder: 'Enter your Granola API key...', + namePlaceholder: 'My Granola API Key', + credentialConfigurations: {}, + }, [CredentialType.CREDENTIAL_WILDCARD]: { label: 'Any Credential', description: @@ -772,6 +780,7 @@ export const CREDENTIAL_ENV_MAP: Record = { [CredentialType.METABASE_CRED]: '', // Multi-field credential (url + apiKey), no single env var [CredentialType.CLERK_CRED]: '', // OAuth credential, no env var [CredentialType.CLERK_API_KEY]: '', // User-provided Secret Key, no env var + [CredentialType.GRANOLA_API_KEY]: 'GRANOLA_API_KEY', [CredentialType.CREDENTIAL_WILDCARD]: '', // Wildcard marker, not a real credential }; @@ -2719,6 +2728,7 @@ export const BUBBLE_CREDENTIAL_OPTIONS: Record< docusign: [CredentialType.DOCUSIGN_CRED], metabase: [CredentialType.METABASE_CRED], clerk: [CredentialType.CLERK_CRED], + granola: [CredentialType.GRANOLA_API_KEY], }; export interface CredentialSiblingEntry { diff --git a/packages/bubble-shared-schemas/src/types.ts b/packages/bubble-shared-schemas/src/types.ts index d6d7a870..ffff2f2c 100644 --- a/packages/bubble-shared-schemas/src/types.ts +++ b/packages/bubble-shared-schemas/src/types.ts @@ -133,6 +133,9 @@ export enum CredentialType { // Clerk Credentials CLERK_CRED = 'CLERK_CRED', CLERK_API_KEY = 'CLERK_API_KEY', + + // Granola Credentials + GRANOLA_API_KEY = 'GRANOLA_API_KEY', } // Define all bubble names as a union type for type safety @@ -222,4 +225,5 @@ export type BubbleName = | 'sortly' | 'docusign' | 'metabase' - | 'clerk'; + | 'clerk' + | 'granola'; diff --git a/packages/create-bubblelab-app/package.json b/packages/create-bubblelab-app/package.json index 72bc4651..d3d60d8d 100644 --- a/packages/create-bubblelab-app/package.json +++ b/packages/create-bubblelab-app/package.json @@ -1,6 +1,6 @@ { "name": "create-bubblelab-app", - "version": "0.1.289", + "version": "0.1.291", "type": "module", "license": "Apache-2.0", "description": "Create BubbleLab AI agent applications with one command", diff --git a/packages/create-bubblelab-app/templates/basic/package.json b/packages/create-bubblelab-app/templates/basic/package.json index 609df297..060efb0b 100644 --- a/packages/create-bubblelab-app/templates/basic/package.json +++ b/packages/create-bubblelab-app/templates/basic/package.json @@ -11,9 +11,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bubblelab/bubble-core": "^0.1.289", - "@bubblelab/bubble-runtime": "^0.1.289", - "@bubblelab/shared-schemas": "^0.1.289", + "@bubblelab/bubble-core": "^0.1.291", + "@bubblelab/bubble-runtime": "^0.1.291", + "@bubblelab/shared-schemas": "^0.1.291", "dotenv": "^16.4.5" }, "devDependencies": { diff --git a/packages/create-bubblelab-app/templates/reddit-scraper/package.json b/packages/create-bubblelab-app/templates/reddit-scraper/package.json index fe4593d5..1a434867 100644 --- a/packages/create-bubblelab-app/templates/reddit-scraper/package.json +++ b/packages/create-bubblelab-app/templates/reddit-scraper/package.json @@ -11,8 +11,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bubblelab/bubble-core": "^0.1.289", - "@bubblelab/bubble-runtime": "^0.1.289", + "@bubblelab/bubble-core": "^0.1.291", + "@bubblelab/bubble-runtime": "^0.1.291", "dotenv": "^16.4.5" }, "devDependencies": {