@joshualelon/clawdbot-skill-flow 2.2.1 → 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
@@ -427,6 +427,20 @@ export async function myAfterCapture(variable: string, value: string | number, s
427
427
  const { appendToSheet } = api.hooks;
428
428
  await appendToSheet('spreadsheet-id', { [variable]: value });
429
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
+ }
430
444
  ```
431
445
 
432
446
  **Complete Example:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshualelon/clawdbot-skill-flow",
3
- "version": "2.2.1",
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";