@joshualelon/clawdbot-skill-flow 2.2.0 → 2.3.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.
package/README.md CHANGED
@@ -403,7 +403,47 @@ Each step can declare what actions to execute:
403
403
 
404
404
  #### Hooks File Structure
405
405
 
406
- Action functions are exported as **named exports** from your hooks file. Plugin utilities are available via the `api.hooks` object:
406
+ Action functions are exported as **named exports** from your hooks file. All actions receive an `api` parameter as their final argument - plugin utilities are available via `api.hooks`.
407
+
408
+ > **Note:** No imports needed! Just destructure what you need from `api.hooks`.
409
+
410
+ **Action Signatures:**
411
+
412
+ ```typescript
413
+ // Fetch actions - retrieve data before rendering
414
+ export async function myFetch(session: FlowSession, api: EnhancedPluginApi) {
415
+ const { querySheetHistory } = api.hooks;
416
+ return { variableName: value };
417
+ }
418
+
419
+ // BeforeRender actions - modify step before display
420
+ export async function myBeforeRender(step: FlowStep, session: FlowSession, api: EnhancedPluginApi) {
421
+ const { generateButtonRange } = api.hooks;
422
+ return { ...step, buttons: [...] };
423
+ }
424
+
425
+ // AfterCapture actions - side effects after capturing input
426
+ export async function myAfterCapture(variable: string, value: string | number, session: FlowSession, api: EnhancedPluginApi) {
427
+ const { appendToSheet } = api.hooks;
428
+ await appendToSheet('spreadsheet-id', { [variable]: value });
429
+ }
430
+
431
+ // Create a new spreadsheet (typically in a fetch action)
432
+ export async function createWorkoutLog(session, api) {
433
+ const { createSpreadsheet } = api.hooks;
434
+
435
+ const { spreadsheetId, spreadsheetUrl } = await createSpreadsheet({
436
+ title: `${session.flowName} Log - ${new Date().getFullYear()}`,
437
+ worksheetName: 'Sessions',
438
+ headers: ['timestamp', 'userId', 'set1', 'set2', 'set3', 'set4', 'total'],
439
+ folderId: process.env.GOOGLE_DRIVE_FOLDER_ID // Optional: move to specific folder
440
+ });
441
+
442
+ return { spreadsheetId, spreadsheetUrl };
443
+ }
444
+ ```
445
+
446
+ **Complete Example:**
407
447
 
408
448
  ```javascript
409
449
  // ~/.clawdbot/flows/pushups/hooks.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshualelon/clawdbot-skill-flow",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Multi-step workflow orchestration plugin for Clawdbot",
6
6
  "keywords": [
@@ -284,6 +284,155 @@ export async function querySheetHistory(
284
284
  return data;
285
285
  }
286
286
 
287
+ /**
288
+ * Create a new Google Spreadsheet with optional initial data
289
+ *
290
+ * @param options - Configuration for spreadsheet creation
291
+ * @returns Object containing spreadsheetId and spreadsheetUrl
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * // Create a simple spreadsheet
296
+ * const { spreadsheetId, spreadsheetUrl } = await createSpreadsheet({
297
+ * title: 'Pushups Log 2026',
298
+ * worksheetName: 'Sessions'
299
+ * });
300
+ *
301
+ * // Create with initial headers and move to folder
302
+ * const result = await createSpreadsheet({
303
+ * title: 'Pushups Log 2026',
304
+ * worksheetName: 'Sessions',
305
+ * headers: ['timestamp', 'userId', 'set1', 'set2', 'set3', 'set4', 'total'],
306
+ * folderId: '1ABC...xyz'
307
+ * });
308
+ * ```
309
+ */
310
+ export async function createSpreadsheet(options: {
311
+ title: string;
312
+ worksheetName?: string;
313
+ headers?: string[];
314
+ folderId?: string;
315
+ credentials?: GoogleServiceAccountCredentials;
316
+ }): Promise<{ spreadsheetId: string; spreadsheetUrl: string }> {
317
+ const {
318
+ title,
319
+ worksheetName = 'Sheet1',
320
+ headers,
321
+ folderId,
322
+ credentials,
323
+ } = options;
324
+
325
+ const sheets = await createSheetsClient(credentials);
326
+
327
+ // Create the spreadsheet
328
+ const createResponse = await sheets.spreadsheets.create({
329
+ requestBody: {
330
+ properties: {
331
+ title,
332
+ locale: 'en_US',
333
+ timeZone: 'America/Chicago', // Default to CT, users can change
334
+ },
335
+ sheets: [
336
+ {
337
+ properties: {
338
+ title: worksheetName,
339
+ gridProperties: {
340
+ rowCount: 1000,
341
+ columnCount: 26,
342
+ frozenRowCount: headers ? 1 : 0, // Freeze header row if headers provided
343
+ },
344
+ },
345
+ },
346
+ ],
347
+ },
348
+ });
349
+
350
+ const spreadsheetId = createResponse.data.spreadsheetId;
351
+ const spreadsheetUrl = createResponse.data.spreadsheetUrl;
352
+
353
+ if (!spreadsheetId || !spreadsheetUrl) {
354
+ throw new Error('Failed to create spreadsheet: missing ID or URL');
355
+ }
356
+
357
+ // Add headers if provided
358
+ if (headers && headers.length > 0) {
359
+ await sheets.spreadsheets.values.update({
360
+ spreadsheetId,
361
+ range: `${worksheetName}!A1`,
362
+ valueInputOption: 'RAW',
363
+ requestBody: {
364
+ values: [headers],
365
+ },
366
+ });
367
+
368
+ // Bold the header row
369
+ await sheets.spreadsheets.batchUpdate({
370
+ spreadsheetId,
371
+ requestBody: {
372
+ requests: [
373
+ {
374
+ repeatCell: {
375
+ range: {
376
+ sheetId: 0,
377
+ startRowIndex: 0,
378
+ endRowIndex: 1,
379
+ },
380
+ cell: {
381
+ userEnteredFormat: {
382
+ textFormat: {
383
+ bold: true,
384
+ },
385
+ },
386
+ },
387
+ fields: 'userEnteredFormat.textFormat.bold',
388
+ },
389
+ },
390
+ ],
391
+ },
392
+ });
393
+ }
394
+
395
+ // Move to folder if specified
396
+ if (folderId) {
397
+ // Create Drive API client with same auth
398
+ let driveAuth;
399
+ if (credentials) {
400
+ driveAuth = new google.auth.GoogleAuth({
401
+ credentials: {
402
+ client_email: credentials.clientEmail,
403
+ private_key: credentials.privateKey,
404
+ },
405
+ scopes: ['https://www.googleapis.com/auth/drive.file'],
406
+ });
407
+ } else {
408
+ driveAuth = new google.auth.GoogleAuth({
409
+ scopes: ['https://www.googleapis.com/auth/drive.file'],
410
+ });
411
+ }
412
+
413
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
414
+ const drive = google.drive({ version: 'v3', auth: driveAuth as any });
415
+
416
+ // Get current parents (usually root)
417
+ const file = await drive.files.get({
418
+ fileId: spreadsheetId,
419
+ fields: 'parents',
420
+ });
421
+
422
+ const previousParents = file.data.parents?.join(',');
423
+
424
+ // Move to new folder
425
+ await drive.files.update({
426
+ fileId: spreadsheetId,
427
+ addParents: folderId,
428
+ removeParents: previousParents,
429
+ fields: 'id, parents',
430
+ });
431
+ }
432
+
433
+ return { spreadsheetId, spreadsheetUrl };
434
+ }
435
+
287
436
  /**
288
437
  * Ensure a worksheet exists, create it if not
289
438
  */
@@ -31,7 +31,7 @@ export {
31
31
  } from "./common.js";
32
32
 
33
33
  // Re-export Google Sheets utilities
34
- export { createSheetsLogger, appendToSheet, querySheetHistory } from "./google-sheets.js";
34
+ export { createSheetsLogger, appendToSheet, querySheetHistory, createSpreadsheet } from "./google-sheets.js";
35
35
 
36
36
  // Re-export dynamic buttons utilities
37
37
  export { createDynamicButtons, getRecentAverage, generateButtonRange } from "./dynamic-buttons.js";