@joshualelon/clawdbot-skill-flow 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Google Sheets integration utilities for logging flow data
3
+ */
4
+
5
+ import { google, sheets_v4 } from "googleapis";
6
+ import type { FlowHooks, FlowSession } from "../types.js";
7
+ import type { SheetsLogOptions, GoogleServiceAccountCredentials, HeaderMode } from "./types.js";
8
+ import { withRetry } from "./common.js";
9
+
10
+ /**
11
+ * Create a Google Sheets API client with authentication
12
+ */
13
+ async function createSheetsClient(
14
+ credentials?: GoogleServiceAccountCredentials
15
+ ): Promise<sheets_v4.Sheets> {
16
+ // If credentials provided, use service account auth
17
+ if (credentials) {
18
+ const auth = new google.auth.GoogleAuth({
19
+ credentials: {
20
+ client_email: credentials.clientEmail,
21
+ private_key: credentials.privateKey,
22
+ },
23
+ scopes: ["https://www.googleapis.com/auth/spreadsheets"],
24
+ });
25
+
26
+ // GoogleAuth is compatible with the auth parameter expected by google.sheets()
27
+ // The typing is complex due to multiple auth types, but runtime behavior is correct
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ return google.sheets({ version: "v4", auth: auth as any });
30
+ }
31
+
32
+ // Otherwise, use application default credentials (from environment)
33
+ const auth = new google.auth.GoogleAuth({
34
+ scopes: ["https://www.googleapis.com/auth/spreadsheets"],
35
+ });
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ return google.sheets({ version: "v4", auth: auth as any });
39
+ }
40
+
41
+ /**
42
+ * Create a hook that logs captured variables to Google Sheets.
43
+ * Returns an onCapture hook function.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * export default {
48
+ * onCapture: createSheetsLogger({
49
+ * spreadsheetId: '1ABC...xyz',
50
+ * worksheetName: 'Workouts',
51
+ * columns: ['set1', 'set2', 'set3'],
52
+ * includeMetadata: true
53
+ * })
54
+ * };
55
+ * ```
56
+ */
57
+ export function createSheetsLogger(
58
+ options: SheetsLogOptions
59
+ ): NonNullable<FlowHooks["onCapture"]> {
60
+ const {
61
+ spreadsheetId,
62
+ worksheetName = "Sheet1",
63
+ columns,
64
+ includeMetadata = true,
65
+ credentials,
66
+ headerMode = 'append',
67
+ } = options;
68
+
69
+ return async (variable: string, value: string | number, session: FlowSession) => {
70
+ try {
71
+ // Prepare row data
72
+ const row: Record<string, unknown> = {};
73
+
74
+ // Add metadata if requested
75
+ if (includeMetadata) {
76
+ row.timestamp = new Date().toISOString();
77
+ row.userId = session.senderId;
78
+ row.flowName = session.flowName;
79
+ row.channel = session.channel;
80
+ }
81
+
82
+ // Add all session variables (or filtered columns)
83
+ if (columns && columns.length > 0) {
84
+ // Only include specified columns
85
+ for (const col of columns) {
86
+ row[col] = session.variables[col] ?? "";
87
+ }
88
+ } else {
89
+ // Include all variables
90
+ Object.assign(row, session.variables);
91
+ }
92
+
93
+ // Also include the just-captured variable
94
+ row[variable] = value;
95
+
96
+ // Append to sheet with retry
97
+ await withRetry(
98
+ () => appendToSheet(spreadsheetId, worksheetName, [row], credentials, headerMode),
99
+ { maxAttempts: 3, delayMs: 1000, backoff: true }
100
+ );
101
+ } catch (error) {
102
+ // Enhanced error message with context
103
+ const errorMessage = error instanceof Error ? error.message : String(error);
104
+ console.error(
105
+ `Failed to log to Google Sheets (spreadsheetId: ${spreadsheetId}, worksheet: ${worksheetName}): ${errorMessage}. ` +
106
+ `Check credentials, permissions, and API quotas. Flow continues without logging.`
107
+ );
108
+ // Don't throw - logging failures shouldn't break the flow
109
+ }
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Low-level utility to append rows to a Google Sheet.
115
+ * Creates the worksheet if it doesn't exist, and adds headers on first write.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * await appendToSheet('1ABC...xyz', 'Workouts', [
120
+ * { date: '2026-01-25', reps: 20, weight: 45 },
121
+ * { date: '2026-01-26', reps: 22, weight: 45 }
122
+ * ], undefined, 'append');
123
+ * ```
124
+ */
125
+ export async function appendToSheet(
126
+ spreadsheetId: string,
127
+ worksheetName: string,
128
+ rows: Array<Record<string, unknown>>,
129
+ credentials?: GoogleServiceAccountCredentials,
130
+ headerMode: HeaderMode = 'append'
131
+ ): Promise<void> {
132
+ if (rows.length === 0) {
133
+ return;
134
+ }
135
+
136
+ const sheets = await createSheetsClient(credentials);
137
+
138
+ // Ensure worksheet exists
139
+ await ensureWorksheetExists(sheets, spreadsheetId, worksheetName);
140
+
141
+ // Get existing data to check if we need to write headers
142
+ const existingData = await sheets.spreadsheets.values.get({
143
+ spreadsheetId,
144
+ range: `${worksheetName}!A1:ZZ1`, // Check first row for headers
145
+ });
146
+
147
+ const existingHeaders = existingData.data.values?.[0] || [];
148
+ const rowKeys = Object.keys(rows[0]!);
149
+
150
+ // Handle headers based on mode
151
+ if (existingHeaders.length === 0) {
152
+ // Empty sheet - always write headers
153
+ await sheets.spreadsheets.values.update({
154
+ spreadsheetId,
155
+ range: `${worksheetName}!A1`,
156
+ valueInputOption: "RAW",
157
+ requestBody: {
158
+ values: [rowKeys],
159
+ },
160
+ });
161
+ } else if (!arraysEqual(existingHeaders, rowKeys)) {
162
+ // Headers don't match - apply mode
163
+ switch (headerMode) {
164
+ case 'strict':
165
+ throw new Error(
166
+ `Header mismatch in sheet "${worksheetName}". ` +
167
+ `Expected: ${existingHeaders.join(', ')}. ` +
168
+ `Got: ${rowKeys.join(', ')}. ` +
169
+ `Set headerMode to 'append' or 'overwrite' to handle this.`
170
+ );
171
+
172
+ case 'append': {
173
+ // Find new columns not in existing headers
174
+ const newColumns = rowKeys.filter(key => !existingHeaders.includes(key));
175
+ if (newColumns.length > 0) {
176
+ // Append new columns to the right
177
+ const updatedHeaders = [...existingHeaders, ...newColumns];
178
+ await sheets.spreadsheets.values.update({
179
+ spreadsheetId,
180
+ range: `${worksheetName}!A1`,
181
+ valueInputOption: "RAW",
182
+ requestBody: {
183
+ values: [updatedHeaders],
184
+ },
185
+ });
186
+ }
187
+ break;
188
+ }
189
+
190
+ case 'overwrite':
191
+ // Replace headers completely
192
+ await sheets.spreadsheets.values.update({
193
+ spreadsheetId,
194
+ range: `${worksheetName}!A1`,
195
+ valueInputOption: "RAW",
196
+ requestBody: {
197
+ values: [rowKeys],
198
+ },
199
+ });
200
+ break;
201
+ }
202
+ }
203
+
204
+ // Convert rows to 2D array for Sheets API
205
+ const values = rows.map((row) => rowKeys.map((key) => row[key] ?? ""));
206
+
207
+ // Append data
208
+ await sheets.spreadsheets.values.append({
209
+ spreadsheetId,
210
+ range: `${worksheetName}!A:A`, // Append to column A (auto-detects next row)
211
+ valueInputOption: "RAW",
212
+ requestBody: {
213
+ values,
214
+ },
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Query historical data from a Google Sheet.
220
+ * Useful for calculating statistics or generating dynamic buttons.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * const history = await querySheetHistory('1ABC...xyz', 'Workouts', {
225
+ * flowName: 'pushups',
226
+ * userId: 'user123',
227
+ * dateRange: [new Date('2026-01-01'), new Date()]
228
+ * });
229
+ * ```
230
+ */
231
+ export async function querySheetHistory(
232
+ spreadsheetId: string,
233
+ worksheetName: string,
234
+ filters?: {
235
+ flowName?: string;
236
+ userId?: string;
237
+ dateRange?: [Date, Date];
238
+ },
239
+ credentials?: GoogleServiceAccountCredentials
240
+ ): Promise<Array<Record<string, unknown>>> {
241
+ const sheets = await createSheetsClient(credentials);
242
+
243
+ // Get all data from worksheet
244
+ const response = await sheets.spreadsheets.values.get({
245
+ spreadsheetId,
246
+ range: `${worksheetName}!A:ZZ`,
247
+ });
248
+
249
+ const values = response.data.values || [];
250
+ if (values.length === 0) {
251
+ return [];
252
+ }
253
+
254
+ // First row is headers
255
+ const headers = values[0] as string[];
256
+ const rows = values.slice(1);
257
+
258
+ // Convert to array of objects
259
+ let data = rows.map((row: unknown[]) => {
260
+ const obj: Record<string, unknown> = {};
261
+ headers.forEach((header, index) => {
262
+ obj[header] = row[index] ?? "";
263
+ });
264
+ return obj;
265
+ });
266
+
267
+ // Apply filters
268
+ if (filters) {
269
+ if (filters.flowName) {
270
+ data = data.filter((row: Record<string, unknown>) => row.flowName === filters.flowName);
271
+ }
272
+ if (filters.userId) {
273
+ data = data.filter((row: Record<string, unknown>) => row.userId === filters.userId);
274
+ }
275
+ if (filters.dateRange) {
276
+ const [start, end] = filters.dateRange;
277
+ data = data.filter((row: Record<string, unknown>) => {
278
+ const timestamp = new Date(row.timestamp as string);
279
+ return timestamp >= start && timestamp <= end;
280
+ });
281
+ }
282
+ }
283
+
284
+ return data;
285
+ }
286
+
287
+ /**
288
+ * Ensure a worksheet exists, create it if not
289
+ */
290
+ async function ensureWorksheetExists(
291
+ sheets: sheets_v4.Sheets,
292
+ spreadsheetId: string,
293
+ worksheetName: string
294
+ ): Promise<void> {
295
+ // Get spreadsheet metadata
296
+ const spreadsheet = await sheets.spreadsheets.get({ spreadsheetId });
297
+ const sheetExists = spreadsheet.data.sheets?.some(
298
+ (sheet: sheets_v4.Schema$Sheet) => sheet.properties?.title === worksheetName
299
+ );
300
+
301
+ if (!sheetExists) {
302
+ // Create new worksheet
303
+ await sheets.spreadsheets.batchUpdate({
304
+ spreadsheetId,
305
+ requestBody: {
306
+ requests: [
307
+ {
308
+ addSheet: {
309
+ properties: {
310
+ title: worksheetName,
311
+ },
312
+ },
313
+ },
314
+ ],
315
+ },
316
+ });
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Check if two arrays are equal (for header comparison)
322
+ */
323
+ function arraysEqual<T>(a: T[], b: T[]): boolean {
324
+ if (a.length !== b.length) return false;
325
+ return a.every((val, index) => val === b[index]);
326
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Hooks Utility Library
3
+ *
4
+ * Optional utilities for common workflow patterns.
5
+ * Import only what you need:
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // Import specific utilities
10
+ * import { createSheetsLogger } from '@joshualelon/clawdbot-skill-flow/hooks/google-sheets';
11
+ * import { createDynamicButtons } from '@joshualelon/clawdbot-skill-flow/hooks/dynamic-buttons';
12
+ *
13
+ * // Or import from main hooks export
14
+ * import { composeHooks, withRetry } from '@joshualelon/clawdbot-skill-flow/hooks';
15
+ * ```
16
+ */
17
+
18
+ // Re-export types
19
+ export type * from "./types.js";
20
+
21
+ // Re-export common utilities
22
+ export {
23
+ composeHooks,
24
+ withRetry,
25
+ validateEmail,
26
+ validateNumber,
27
+ validatePhone,
28
+ whenCondition,
29
+ debounceHook,
30
+ throttleHook,
31
+ } from "./common.js";
32
+
33
+ // Re-export Google Sheets utilities
34
+ export { createSheetsLogger, appendToSheet, querySheetHistory } from "./google-sheets.js";
35
+
36
+ // Re-export dynamic buttons utilities
37
+ export { createDynamicButtons, getRecentAverage, generateButtonRange } from "./dynamic-buttons.js";
38
+
39
+ // Re-export scheduling utilities
40
+ export {
41
+ createScheduler,
42
+ scheduleNextSession,
43
+ checkCalendarConflicts,
44
+ findNextAvailableSlot,
45
+ } from "./scheduling.js";
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Scheduling utilities for recurring workflows
3
+ */
4
+
5
+ import { google, calendar_v3 } from "googleapis";
6
+ import type { FlowHooks, FlowSession } from "../types.js";
7
+ import type { ScheduleConfig, GoogleServiceAccountCredentials } from "./types.js";
8
+
9
+ /**
10
+ * Create a hook that schedules the next workflow session after completion.
11
+ * Returns an onFlowComplete hook function.
12
+ *
13
+ * Note: Scheduling uses local server time. Ensure your server is in the correct timezone.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * export default {
18
+ * onFlowComplete: createScheduler({
19
+ * days: ['mon', 'wed', 'fri'],
20
+ * time: '08:00',
21
+ * calendarCheck: true,
22
+ * calendarId: 'primary'
23
+ * })
24
+ * };
25
+ * ```
26
+ */
27
+ export function createScheduler(
28
+ config: ScheduleConfig
29
+ ): NonNullable<FlowHooks["onFlowComplete"]> {
30
+ const {
31
+ days = ["mon", "wed", "fri"],
32
+ time = "08:00",
33
+ calendarId = "primary",
34
+ credentials,
35
+ calendarCheck = false,
36
+ rescheduleOnConflict = false,
37
+ } = config;
38
+
39
+ return async (session: FlowSession): Promise<void> => {
40
+ try {
41
+ // Calculate next scheduled date
42
+ const nextDate = findNextScheduledDate(days, time);
43
+
44
+ if (!nextDate) {
45
+ console.warn("Unable to calculate next scheduled date");
46
+ return;
47
+ }
48
+
49
+ // Check for calendar conflicts if enabled
50
+ if (calendarCheck) {
51
+ const hasConflict = await checkCalendarConflicts(nextDate, 60, { calendarId, credentials });
52
+
53
+ if (hasConflict && rescheduleOnConflict) {
54
+ // Find alternative slot
55
+ const alternativeDate = await findNextAvailableSlot(
56
+ [nextDate],
57
+ 60, // 60 minutes duration
58
+ { calendarId, credentials }
59
+ );
60
+
61
+ if (alternativeDate) {
62
+ await scheduleNextSession(
63
+ session.flowName,
64
+ session.senderId,
65
+ alternativeDate,
66
+ { calendarId, credentials }
67
+ );
68
+ return;
69
+ }
70
+ } else if (hasConflict) {
71
+ console.warn(
72
+ `Calendar conflict detected for ${nextDate.toISOString()}, not scheduling`
73
+ );
74
+ return;
75
+ }
76
+ }
77
+
78
+ // Schedule next session
79
+ await scheduleNextSession(session.flowName, session.senderId, nextDate, { calendarId, credentials });
80
+ } catch (error) {
81
+ // Enhanced error message with context
82
+ const errorMessage = error instanceof Error ? error.message : String(error);
83
+ console.error(
84
+ `Failed to schedule next session for flow "${session.flowName}": ${errorMessage}. ` +
85
+ `Check calendar credentials and permissions.`
86
+ );
87
+ // Don't throw - scheduling failures shouldn't break the flow
88
+ }
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Schedule the next workflow session for a user by creating a Google Calendar event.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * await scheduleNextSession('pushups', 'user123', new Date('2026-01-27T08:00:00Z'), {
98
+ * calendarId: 'primary',
99
+ * credentials: { clientEmail: '...', privateKey: '...' }
100
+ * });
101
+ * ```
102
+ */
103
+ export async function scheduleNextSession(
104
+ flowName: string,
105
+ userId: string,
106
+ nextDate: Date,
107
+ options?: { calendarId?: string; credentials?: GoogleServiceAccountCredentials }
108
+ ): Promise<void> {
109
+ const calendar = await createCalendarClient(options?.credentials);
110
+ const calendarId = options?.calendarId || 'primary';
111
+
112
+ // Calculate end time (default 30 min duration)
113
+ const endDate = new Date(nextDate);
114
+ endDate.setMinutes(endDate.getMinutes() + 30);
115
+
116
+ await calendar.events.insert({
117
+ calendarId,
118
+ requestBody: {
119
+ summary: `${flowName} Workflow`,
120
+ start: { dateTime: nextDate.toISOString() },
121
+ end: { dateTime: endDate.toISOString() },
122
+ description: `Scheduled session for ${flowName} flow (user: ${userId})`
123
+ }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Check if there are calendar conflicts at the given time by querying Google Calendar.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const hasConflict = await checkCalendarConflicts(
133
+ * new Date('2026-01-27T08:00:00Z'),
134
+ * 60,
135
+ * { calendarId: 'primary', credentials: { ... } }
136
+ * );
137
+ * ```
138
+ */
139
+ export async function checkCalendarConflicts(
140
+ dateTime: Date,
141
+ duration = 60,
142
+ options?: { calendarId?: string; credentials?: GoogleServiceAccountCredentials }
143
+ ): Promise<boolean> {
144
+ const calendar = await createCalendarClient(options?.credentials);
145
+ const calendarId = options?.calendarId || 'primary';
146
+
147
+ const endTime = new Date(dateTime);
148
+ endTime.setMinutes(endTime.getMinutes() + duration);
149
+
150
+ const response = await calendar.events.list({
151
+ calendarId,
152
+ timeMin: dateTime.toISOString(),
153
+ timeMax: endTime.toISOString(),
154
+ singleEvents: true
155
+ });
156
+
157
+ return (response.data.items?.length || 0) > 0;
158
+ }
159
+
160
+ /**
161
+ * Find the next available time slot from a list of preferred dates by checking Google Calendar.
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * const nextSlot = await findNextAvailableSlot(
166
+ * [new Date('2026-01-27T08:00:00Z')],
167
+ * 60,
168
+ * { calendarId: 'primary', credentials: { ... } }
169
+ * );
170
+ * ```
171
+ */
172
+ export async function findNextAvailableSlot(
173
+ preferredDates: Date[],
174
+ duration: number,
175
+ options?: { calendarId?: string; credentials?: GoogleServiceAccountCredentials }
176
+ ): Promise<Date | null> {
177
+ for (const date of preferredDates) {
178
+ const hasConflict = await checkCalendarConflicts(date, duration, options);
179
+ if (!hasConflict) {
180
+ return date;
181
+ }
182
+ }
183
+ return null; // No free slots found
184
+ }
185
+
186
+ /**
187
+ * Find the next scheduled date based on day-of-week and time preferences.
188
+ * Uses local server time.
189
+ */
190
+ function findNextScheduledDate(
191
+ days: string[],
192
+ time: string
193
+ ): Date | null {
194
+ const now = new Date();
195
+
196
+ // Parse target time (HH:MM format)
197
+ const [hours, minutes] = time.split(":").map(Number);
198
+ if (hours === undefined || minutes === undefined || Number.isNaN(hours) || Number.isNaN(minutes)) {
199
+ return null;
200
+ }
201
+
202
+ // Map day names to numbers (0=Sunday, 1=Monday, etc.)
203
+ const dayMap: Record<string, number> = {
204
+ sun: 0,
205
+ mon: 1,
206
+ tue: 2,
207
+ wed: 3,
208
+ thu: 4,
209
+ fri: 5,
210
+ sat: 6,
211
+ };
212
+
213
+ const targetDays = days.map((d) => dayMap[d.toLowerCase()]).filter((d): d is number => d !== undefined);
214
+
215
+ if (targetDays.length === 0) {
216
+ return null;
217
+ }
218
+
219
+ // Find next occurrence
220
+ for (let daysAhead = 1; daysAhead <= 14; daysAhead++) {
221
+ const candidate = new Date(now);
222
+ candidate.setDate(candidate.getDate() + daysAhead);
223
+ candidate.setHours(hours, minutes, 0, 0);
224
+
225
+ const dayOfWeek = candidate.getDay();
226
+
227
+ if (targetDays.includes(dayOfWeek)) {
228
+ return candidate;
229
+ }
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * Create a Google Calendar API client with authentication
237
+ */
238
+ async function createCalendarClient(
239
+ credentials?: GoogleServiceAccountCredentials
240
+ ): Promise<calendar_v3.Calendar> {
241
+ if (credentials) {
242
+ const auth = new google.auth.GoogleAuth({
243
+ credentials: {
244
+ client_email: credentials.clientEmail,
245
+ private_key: credentials.privateKey,
246
+ },
247
+ scopes: ['https://www.googleapis.com/auth/calendar'],
248
+ });
249
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
250
+ return google.calendar({ version: 'v3', auth: auth as any });
251
+ }
252
+
253
+ // Use application default credentials
254
+ const auth = new google.auth.GoogleAuth({
255
+ scopes: ['https://www.googleapis.com/auth/calendar'],
256
+ });
257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
+ return google.calendar({ version: 'v3', auth: auth as any });
259
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Type definitions for hooks utility library
3
+ */
4
+
5
+ import type { FlowSession, FlowHooks } from "../types.js";
6
+
7
+ /**
8
+ * Google Sheets configuration
9
+ */
10
+ export interface SheetsConfig {
11
+ spreadsheetId: string;
12
+ worksheetName?: string;
13
+ credentials?: GoogleServiceAccountCredentials;
14
+ }
15
+
16
+ export interface GoogleServiceAccountCredentials {
17
+ clientEmail: string;
18
+ privateKey: string;
19
+ }
20
+
21
+ export type HeaderMode = 'strict' | 'append' | 'overwrite';
22
+
23
+ export interface SheetsLogOptions {
24
+ spreadsheetId: string;
25
+ worksheetName?: string;
26
+ columns?: string[]; // Variable names to log
27
+ includeMetadata?: boolean; // Add timestamp, userId, flowName
28
+ credentials?: GoogleServiceAccountCredentials;
29
+ headerMode?: HeaderMode; // How to handle header mismatches (default: 'append')
30
+ }
31
+
32
+ /**
33
+ * Dynamic buttons configuration
34
+ */
35
+ export interface DynamicButtonsConfig {
36
+ spreadsheetId?: string; // Source for history (Google Sheets)
37
+ historyFile?: string; // Or use local .jsonl
38
+ variable: string; // Which variable to generate buttons for
39
+ strategy: "centered" | "progressive" | "range";
40
+ buttonCount?: number; // How many buttons (default: 5)
41
+ step?: number; // Increment between buttons
42
+ }
43
+
44
+ export type ButtonStrategy = "centered" | "progressive" | "range";
45
+
46
+ /**
47
+ * Scheduling configuration
48
+ */
49
+ export interface ScheduleConfig {
50
+ days?: string[]; // ['mon', 'wed', 'fri']
51
+ time?: string; // '08:00' (uses local server time)
52
+ calendarId?: string; // Google Calendar ID (default: 'primary')
53
+ credentials?: GoogleServiceAccountCredentials; // For calendar API access
54
+ calendarCheck?: boolean; // Check for conflicts
55
+ rescheduleOnConflict?: boolean;
56
+ }
57
+
58
+ /**
59
+ * Hook composer type - accepts multiple hooks and merges them
60
+ */
61
+ export type HookFunction<T extends keyof FlowHooks> = NonNullable<FlowHooks[T]>;
62
+
63
+ /**
64
+ * Condition function for conditional hooks
65
+ */
66
+ export type ConditionFunction = (session: FlowSession) => boolean | Promise<boolean>;
67
+
68
+ /**
69
+ * Retry configuration
70
+ */
71
+ export interface RetryOptions {
72
+ maxAttempts?: number;
73
+ delayMs?: number;
74
+ backoff?: boolean; // Use exponential backoff
75
+ maxDelayMs?: number; // Maximum delay cap (default: 30000ms)
76
+ }