@joshualelon/clawdbot-skill-flow 2.3.2 → 2.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshualelon/clawdbot-skill-flow",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "type": "module",
5
5
  "description": "Multi-step workflow orchestration plugin for Clawdbot",
6
6
  "keywords": [
@@ -49,6 +49,9 @@ const sheetsAppendSchema = z.object({
49
49
  privateKey: z.string(),
50
50
  })
51
51
  .optional(),
52
+ useGogOAuth: z.boolean().default(true).describe(
53
+ "Use gog CLI OAuth instead of service account (recommended to avoid quota issues)"
54
+ ),
52
55
  });
53
56
 
54
57
  async function sheetsAppendExecute(
@@ -85,7 +88,8 @@ async function sheetsAppendExecute(
85
88
  cfg.worksheetName,
86
89
  [row],
87
90
  cfg.credentials as GoogleServiceAccountCredentials | undefined,
88
- cfg.headerMode
91
+ cfg.headerMode,
92
+ cfg.useGogOAuth
89
93
  ),
90
94
  { maxAttempts: 3, delayMs: 1000, backoff: true }
91
95
  );
@@ -96,14 +96,30 @@ async function executeStepActions(
96
96
  );
97
97
 
98
98
  // Inject result into session
99
- if (result && typeof result === "object" && varName in result) {
100
- const value = (result as Record<string, unknown>)[varName];
101
- if (typeof value === "string" || typeof value === "number") {
99
+ if (result !== null && result !== undefined) {
100
+ if (typeof result === "object") {
101
+ // If result is an object, inject all its fields as separate variables
102
+ const resultObj = result as Record<string, unknown>;
103
+ for (const [key, value] of Object.entries(resultObj)) {
104
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
105
+ modifiedSession = {
106
+ ...modifiedSession,
107
+ variables: {
108
+ ...modifiedSession.variables,
109
+ [key]: value,
110
+ },
111
+ };
112
+ }
113
+ }
114
+ // Update context for next actions
115
+ context.variables = modifiedSession.variables;
116
+ } else if (typeof result === "string" || typeof result === "number" || typeof result === "boolean") {
117
+ // If result is a primitive, inject it under varName
102
118
  modifiedSession = {
103
119
  ...modifiedSession,
104
120
  variables: {
105
121
  ...modifiedSession.variables,
106
- [varName]: value,
122
+ [varName]: result,
107
123
  },
108
124
  };
109
125
  // Update context for next actions
@@ -192,18 +208,29 @@ async function executeStepActions(
192
208
  modifiedSession,
193
209
  enhancedApi
194
210
  );
195
- if (result && typeof result === "object") {
196
- if (varName in result) {
197
- const value = result[varName];
198
- if (typeof value === "string" || typeof value === "number") {
199
- modifiedSession = {
200
- ...modifiedSession,
201
- variables: {
202
- ...modifiedSession.variables,
203
- [varName]: value,
204
- },
205
- };
211
+ if (result !== null && result !== undefined) {
212
+ if (typeof result === "object") {
213
+ // If result is an object, inject all its fields as separate variables
214
+ for (const [key, value] of Object.entries(result)) {
215
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
216
+ modifiedSession = {
217
+ ...modifiedSession,
218
+ variables: {
219
+ ...modifiedSession.variables,
220
+ [key]: value,
221
+ },
222
+ };
223
+ }
206
224
  }
225
+ } else if (typeof result === "string" || typeof result === "number" || typeof result === "boolean") {
226
+ // If result is a primitive, inject it under varName
227
+ modifiedSession = {
228
+ ...modifiedSession,
229
+ variables: {
230
+ ...modifiedSession.variables,
231
+ [varName]: result,
232
+ },
233
+ };
207
234
  }
208
235
  }
209
236
  }
@@ -339,7 +366,7 @@ export async function processStep(
339
366
  ): Promise<{
340
367
  reply: ReplyPayload;
341
368
  complete: boolean;
342
- updatedVariables: Record<string, string | number>;
369
+ updatedVariables: Record<string, string | number | boolean>;
343
370
  }> {
344
371
  // Load action registry for declarative actions
345
372
  let actionRegistry: ActionRegistry | null = null;
@@ -446,7 +473,7 @@ export async function processStep(
446
473
  */
447
474
  function generateCompletionMessage(
448
475
  flow: FlowMetadata,
449
- variables: Record<string, string | number>
476
+ variables: Record<string, string | number | boolean>
450
477
  ): string {
451
478
  let message = `✅ Flow "${flow.name}" completed!\n\n`;
452
479
 
@@ -17,7 +17,7 @@ import { normalizeButton } from "../validation.js";
17
17
  */
18
18
  function interpolateVariables(
19
19
  text: string,
20
- variables: Record<string, string | number>
20
+ variables: Record<string, string | number | boolean>
21
21
  ): string {
22
22
  return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
23
23
  const value = variables[varName];
@@ -31,7 +31,7 @@ function interpolateVariables(
31
31
  function renderTelegram(
32
32
  flowName: string,
33
33
  step: FlowStep,
34
- variables: Record<string, string | number>
34
+ variables: Record<string, string | number | boolean>
35
35
  ): ReplyPayload {
36
36
  const message = interpolateVariables(step.message, variables);
37
37
 
@@ -83,7 +83,7 @@ function renderTelegram(
83
83
  */
84
84
  function renderFallback(
85
85
  step: FlowStep,
86
- variables: Record<string, string | number>
86
+ variables: Record<string, string | number | boolean>
87
87
  ): ReplyPayload {
88
88
  const message = interpolateVariables(step.message, variables);
89
89
 
@@ -30,7 +30,7 @@ import { createInterpolationContext, interpolateConfig } from "./interpolation.j
30
30
  */
31
31
  function evaluateCondition(
32
32
  condition: FlowStep["condition"],
33
- variables: Record<string, string | number>
33
+ variables: Record<string, string | number | boolean>
34
34
  ): boolean {
35
35
  if (!condition) {
36
36
  return false;
@@ -75,7 +75,7 @@ function evaluateCondition(
75
75
  function findNextStep(
76
76
  step: FlowStep,
77
77
  value: string | number,
78
- variables: Record<string, string | number>
78
+ variables: Record<string, string | number | boolean>
79
79
  ): string | undefined {
80
80
  // 1. Check button-specific next
81
81
  if (step.buttons && step.buttons.length > 0) {
@@ -68,6 +68,7 @@ export function createSheetsLogger(
68
68
  includeMetadata = true,
69
69
  credentials,
70
70
  headerMode = 'append',
71
+ useGogOAuth,
71
72
  } = options;
72
73
 
73
74
  return async (variable: string, value: string | number, session: FlowSession) => {
@@ -99,7 +100,7 @@ export function createSheetsLogger(
99
100
 
100
101
  // Append to sheet with retry
101
102
  await withRetry(
102
- () => appendToSheet(spreadsheetId, worksheetName, [row], credentials, headerMode),
103
+ () => appendToSheet(spreadsheetId, worksheetName, [row], credentials, headerMode, useGogOAuth),
103
104
  { maxAttempts: 3, delayMs: 1000, backoff: true }
104
105
  );
105
106
  } catch (error) {
@@ -114,6 +115,120 @@ export function createSheetsLogger(
114
115
  };
115
116
  }
116
117
 
118
+ /**
119
+ * Append rows to Google Sheet using gog OAuth credentials
120
+ */
121
+ async function appendToSheetWithGogOAuth(
122
+ spreadsheetId: string,
123
+ worksheetName: string,
124
+ rows: Array<Record<string, unknown>>,
125
+ headerMode: HeaderMode = 'append'
126
+ ): Promise<void> {
127
+ if (rows.length === 0) {
128
+ return;
129
+ }
130
+
131
+ const accessToken = await getGogAccessToken();
132
+ const rowKeys = Object.keys(rows[0]!);
133
+
134
+ // Get existing data to check headers
135
+ const getUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${worksheetName}!A1:ZZ1`;
136
+ const getResponse = await fetch(getUrl, {
137
+ headers: { 'Authorization': `Bearer ${accessToken}` },
138
+ });
139
+
140
+ let existingHeaders: string[] = [];
141
+ if (getResponse.ok) {
142
+ const getData = await getResponse.json() as { values?: string[][] };
143
+ existingHeaders = getData.values?.[0] || [];
144
+ }
145
+
146
+ // Handle headers based on mode
147
+ if (existingHeaders.length === 0) {
148
+ // Empty sheet - write headers
149
+ const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${worksheetName}!A1?valueInputOption=RAW`;
150
+ await fetch(updateUrl, {
151
+ method: 'PUT',
152
+ headers: {
153
+ 'Authorization': `Bearer ${accessToken}`,
154
+ 'Content-Type': 'application/json',
155
+ },
156
+ body: JSON.stringify({
157
+ values: [rowKeys],
158
+ }),
159
+ });
160
+ } else if (!arraysEqual(existingHeaders, rowKeys)) {
161
+ // Headers don't match - apply mode
162
+ switch (headerMode) {
163
+ case 'strict':
164
+ throw new Error(
165
+ `Header mismatch in sheet "${worksheetName}". ` +
166
+ `Expected: ${existingHeaders.join(', ')}. ` +
167
+ `Got: ${rowKeys.join(', ')}. ` +
168
+ `Set headerMode to 'append' or 'overwrite' to handle this.`
169
+ );
170
+
171
+ case 'append': {
172
+ // Find new columns not in existing headers
173
+ const newColumns = rowKeys.filter(key => !existingHeaders.includes(key));
174
+ if (newColumns.length > 0) {
175
+ // Append new columns to the right
176
+ const updatedHeaders = [...existingHeaders, ...newColumns];
177
+ const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${worksheetName}!A1?valueInputOption=RAW`;
178
+ await fetch(updateUrl, {
179
+ method: 'PUT',
180
+ headers: {
181
+ 'Authorization': `Bearer ${accessToken}`,
182
+ 'Content-Type': 'application/json',
183
+ },
184
+ body: JSON.stringify({
185
+ values: [updatedHeaders],
186
+ }),
187
+ });
188
+ }
189
+ break;
190
+ }
191
+
192
+ case 'overwrite': {
193
+ // Replace headers completely
194
+ const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${worksheetName}!A1?valueInputOption=RAW`;
195
+ await fetch(updateUrl, {
196
+ method: 'PUT',
197
+ headers: {
198
+ 'Authorization': `Bearer ${accessToken}`,
199
+ 'Content-Type': 'application/json',
200
+ },
201
+ body: JSON.stringify({
202
+ values: [rowKeys],
203
+ }),
204
+ });
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ // Convert rows to 2D array
211
+ const values = rows.map((row) => rowKeys.map((key) => row[key] ?? ""));
212
+
213
+ // Append data
214
+ const appendUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${worksheetName}!A:A:append?valueInputOption=RAW`;
215
+ const appendResponse = await fetch(appendUrl, {
216
+ method: 'POST',
217
+ headers: {
218
+ 'Authorization': `Bearer ${accessToken}`,
219
+ 'Content-Type': 'application/json',
220
+ },
221
+ body: JSON.stringify({
222
+ values,
223
+ }),
224
+ });
225
+
226
+ if (!appendResponse.ok) {
227
+ const errorText = await appendResponse.text();
228
+ throw new Error(`Failed to append to sheet: ${appendResponse.statusText} - ${errorText}`);
229
+ }
230
+ }
231
+
117
232
  /**
118
233
  * Low-level utility to append rows to a Google Sheet.
119
234
  * Creates the worksheet if it doesn't exist, and adds headers on first write.
@@ -131,12 +246,23 @@ export async function appendToSheet(
131
246
  worksheetName: string,
132
247
  rows: Array<Record<string, unknown>>,
133
248
  credentials?: GoogleServiceAccountCredentials,
134
- headerMode: HeaderMode = 'append'
249
+ headerMode: HeaderMode = 'append',
250
+ useGogOAuth?: boolean
135
251
  ): Promise<void> {
136
252
  if (rows.length === 0) {
137
253
  return;
138
254
  }
139
255
 
256
+ // Use gog OAuth if requested
257
+ if (useGogOAuth) {
258
+ return await appendToSheetWithGogOAuth(
259
+ spreadsheetId,
260
+ worksheetName,
261
+ rows,
262
+ headerMode
263
+ );
264
+ }
265
+
140
266
  const sheets = await createSheetsClient(credentials);
141
267
 
142
268
  // Ensure worksheet exists
@@ -27,6 +27,7 @@ export interface SheetsLogOptions {
27
27
  includeMetadata?: boolean; // Add timestamp, userId, flowName
28
28
  credentials?: GoogleServiceAccountCredentials;
29
29
  headerMode?: HeaderMode; // How to handle header mismatches (default: 'append')
30
+ useGogOAuth?: boolean; // Use gog CLI OAuth instead of service account (default: true)
30
31
  }
31
32
 
32
33
  /**
package/src/types.ts CHANGED
@@ -106,14 +106,14 @@ export interface FlowSession {
106
106
  currentStepId: string;
107
107
  senderId: string;
108
108
  channel: string;
109
- variables: Record<string, string | number>;
109
+ variables: Record<string, string | number | boolean>;
110
110
  startedAt: number;
111
111
  lastActivityAt: number;
112
112
  }
113
113
 
114
114
  export interface TransitionResult {
115
115
  nextStepId?: string;
116
- variables: Record<string, string | number>;
116
+ variables: Record<string, string | number | boolean>;
117
117
  complete: boolean;
118
118
  error?: string;
119
119
  message?: string;