@lobu/cli 6.0.1 → 6.1.1

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.
Files changed (217) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +11 -11
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +2 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +26 -0
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +1 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.js +4 -4
  10. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  11. package/dist/commands/agent.d.ts +7 -0
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +65 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/chat.d.ts +12 -9
  16. package/dist/commands/chat.d.ts.map +1 -1
  17. package/dist/commands/chat.js +117 -56
  18. package/dist/commands/chat.js.map +1 -1
  19. package/dist/commands/dev.d.ts +15 -7
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +79 -44
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +1 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +136 -0
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/eval.d.ts +8 -0
  28. package/dist/commands/eval.d.ts.map +1 -1
  29. package/dist/commands/eval.js +56 -1
  30. package/dist/commands/eval.js.map +1 -1
  31. package/dist/commands/init.d.ts +20 -5
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +332 -183
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/link.d.ts +11 -0
  36. package/dist/commands/link.d.ts.map +1 -0
  37. package/dist/commands/link.js +28 -0
  38. package/dist/commands/link.js.map +1 -0
  39. package/dist/commands/login.d.ts.map +1 -1
  40. package/dist/commands/login.js +14 -2
  41. package/dist/commands/login.js.map +1 -1
  42. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  43. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  44. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  45. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  46. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  47. package/dist/commands/memory/_lib/mcp.js +24 -12
  48. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  49. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  50. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  51. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  52. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  53. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  54. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  55. package/dist/commands/memory/_lib/schema.d.ts +1 -1
  56. package/dist/commands/memory/_lib/schema.js +1 -1
  57. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  58. package/dist/commands/memory/_lib/seed-cmd.js +5 -6
  59. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  60. package/dist/commands/memory/run.d.ts.map +1 -1
  61. package/dist/commands/memory/run.js +2 -2
  62. package/dist/commands/memory/run.js.map +1 -1
  63. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  64. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  65. package/dist/commands/platforms/platform-prompts.js +54 -8
  66. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  67. package/dist/commands/telemetry.d.ts +10 -0
  68. package/dist/commands/telemetry.d.ts.map +1 -0
  69. package/dist/commands/telemetry.js +68 -0
  70. package/dist/commands/telemetry.js.map +1 -0
  71. package/dist/commands/whoami.d.ts.map +1 -1
  72. package/dist/commands/whoami.js +1 -1
  73. package/dist/commands/whoami.js.map +1 -1
  74. package/dist/connectors/README.md +534 -0
  75. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  76. package/dist/connectors/browser-scraper-utils.ts +214 -0
  77. package/dist/connectors/capterra.ts +273 -0
  78. package/dist/connectors/g2.ts +286 -0
  79. package/dist/connectors/github.ts +1553 -0
  80. package/dist/connectors/glassdoor.ts +291 -0
  81. package/dist/connectors/gmaps.ts +197 -0
  82. package/dist/connectors/google_calendar.ts +631 -0
  83. package/dist/connectors/google_gmail.ts +751 -0
  84. package/dist/connectors/google_photos.ts +776 -0
  85. package/dist/connectors/google_play.ts +342 -0
  86. package/dist/connectors/hackernews.ts +471 -0
  87. package/dist/connectors/index.ts +23 -0
  88. package/dist/connectors/ios_appstore.ts +226 -0
  89. package/dist/connectors/linkedin.ts +471 -0
  90. package/dist/connectors/microsoft_outlook.ts +410 -0
  91. package/dist/connectors/producthunt.ts +471 -0
  92. package/dist/connectors/reddit.ts +600 -0
  93. package/dist/connectors/rss.ts +448 -0
  94. package/dist/connectors/spotify.ts +590 -0
  95. package/dist/connectors/trustpilot.ts +199 -0
  96. package/dist/connectors/website.ts +629 -0
  97. package/dist/connectors/whatsapp.ts +1073 -0
  98. package/dist/connectors/x.ts +526 -0
  99. package/dist/connectors/youtube.ts +666 -0
  100. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  101. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  102. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  103. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  104. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  105. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  106. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  107. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  108. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  109. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  110. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  111. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  112. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  113. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  114. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  115. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  116. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  117. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  118. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  119. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  120. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  121. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  122. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  123. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  124. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  125. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  126. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  127. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  128. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  129. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  130. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  131. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  132. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  133. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  134. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  135. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  136. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  137. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  138. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  139. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  140. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  141. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  142. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  143. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  144. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  145. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +147 -23
  148. package/dist/index.js.map +1 -1
  149. package/dist/internal/api-client.d.ts +4 -8
  150. package/dist/internal/api-client.d.ts.map +1 -1
  151. package/dist/internal/api-client.js +1 -1
  152. package/dist/internal/api-client.js.map +1 -1
  153. package/dist/internal/context.js +2 -2
  154. package/dist/internal/context.js.map +1 -1
  155. package/dist/internal/credentials.d.ts.map +1 -1
  156. package/dist/internal/credentials.js +6 -1
  157. package/dist/internal/credentials.js.map +1 -1
  158. package/dist/internal/index.d.ts +2 -3
  159. package/dist/internal/index.d.ts.map +1 -1
  160. package/dist/internal/index.js +2 -2
  161. package/dist/internal/index.js.map +1 -1
  162. package/dist/internal/oauth.d.ts +6 -5
  163. package/dist/internal/oauth.d.ts.map +1 -1
  164. package/dist/internal/oauth.js +2 -2
  165. package/dist/internal/project-link.d.ts +10 -0
  166. package/dist/internal/project-link.d.ts.map +1 -0
  167. package/dist/internal/project-link.js +48 -0
  168. package/dist/internal/project-link.js.map +1 -0
  169. package/dist/providers.json +2 -2
  170. package/dist/server.bundle.mjs +3090 -4321
  171. package/dist/start-local.bundle.mjs +71481 -0
  172. package/dist/templates/README.md.tmpl +10 -11
  173. package/package.json +14 -12
  174. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  175. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  176. package/dist/__tests__/chat.integration.test.js +0 -337
  177. package/dist/__tests__/chat.integration.test.js.map +0 -1
  178. package/dist/__tests__/dev.test.d.ts +0 -2
  179. package/dist/__tests__/dev.test.d.ts.map +0 -1
  180. package/dist/__tests__/dev.test.js +0 -25
  181. package/dist/__tests__/dev.test.js.map +0 -1
  182. package/dist/__tests__/init-memory.test.d.ts +0 -2
  183. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  184. package/dist/__tests__/init-memory.test.js +0 -45
  185. package/dist/__tests__/init-memory.test.js.map +0 -1
  186. package/dist/__tests__/token.test.d.ts +0 -2
  187. package/dist/__tests__/token.test.d.ts.map +0 -1
  188. package/dist/__tests__/token.test.js +0 -52
  189. package/dist/__tests__/token.test.js.map +0 -1
  190. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  191. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  192. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  193. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  194. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  195. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  196. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  197. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  198. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  199. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  200. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  201. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  202. package/dist/commands/apply.d.ts +0 -3
  203. package/dist/commands/apply.d.ts.map +0 -1
  204. package/dist/commands/apply.js +0 -5
  205. package/dist/commands/apply.js.map +0 -1
  206. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  207. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  208. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  209. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  210. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  211. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  212. package/dist/internal/__tests__/api-client.test.js +0 -95
  213. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  214. package/dist/internal/__tests__/context.test.d.ts +0 -2
  215. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  216. package/dist/internal/__tests__/context.test.js +0 -77
  217. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,751 @@
1
+ /**
2
+ * Gmail Connector (V1 runtime)
3
+ *
4
+ * Syncs email threads from Gmail and supports sending emails
5
+ * via the Gmail API v1.
6
+ */
7
+
8
+ import {
9
+ type ActionContext,
10
+ type ActionResult,
11
+ type ConnectorDefinition,
12
+ ConnectorRuntime,
13
+ type EventEnvelope,
14
+ IDENTITY,
15
+ type SyncContext,
16
+ type SyncResult,
17
+ } from '@lobu/connector-sdk';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Gmail API types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface GmailMessage {
24
+ id: string;
25
+ threadId: string;
26
+ labelIds?: string[];
27
+ snippet: string;
28
+ payload: GmailMessagePayload;
29
+ internalDate: string;
30
+ }
31
+
32
+ interface GmailMessagePayload {
33
+ headers: GmailHeader[];
34
+ mimeType: string;
35
+ body?: { data?: string; size?: number };
36
+ parts?: GmailMessagePayload[];
37
+ }
38
+
39
+ interface GmailHeader {
40
+ name: string;
41
+ value: string;
42
+ }
43
+
44
+ interface GmailThreadListResponse {
45
+ threads?: Array<{ id: string; historyId: string; snippet: string }>;
46
+ nextPageToken?: string;
47
+ resultSizeEstimate?: number;
48
+ }
49
+
50
+ interface GmailThreadGetResponse {
51
+ id: string;
52
+ historyId: string;
53
+ messages: GmailMessage[];
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Checkpoint
58
+ // ---------------------------------------------------------------------------
59
+
60
+ interface GmailCheckpoint {
61
+ last_sync_at?: string;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Connector
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export default class GmailConnector extends ConnectorRuntime {
69
+ readonly definition: ConnectorDefinition = {
70
+ key: 'google.gmail',
71
+ name: 'Gmail',
72
+ description: 'Syncs email threads from Gmail and supports sending emails.',
73
+ version: '1.0.0',
74
+ faviconDomain: 'mail.google.com',
75
+ authSchema: {
76
+ methods: [
77
+ {
78
+ type: 'oauth',
79
+ provider: 'google',
80
+ requiredScopes: ['https://www.googleapis.com/auth/gmail.readonly'],
81
+ optionalScopes: ['https://www.googleapis.com/auth/gmail.send'],
82
+ loginScopes: ['openid', 'email', 'profile'],
83
+ clientIdKey: 'GOOGLE_CLIENT_ID',
84
+ clientSecretKey: 'GOOGLE_CLIENT_SECRET',
85
+ tokenUrl: 'https://oauth2.googleapis.com/token',
86
+ tokenEndpointAuthMethod: 'client_secret_post',
87
+ loginProvisioning: {
88
+ autoCreateConnection: true,
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ feeds: {
94
+ threads: {
95
+ key: 'threads',
96
+ name: 'Threads',
97
+ requiredScopes: ['https://www.googleapis.com/auth/gmail.readonly'],
98
+ description: 'Syncs email threads from Gmail.',
99
+ configSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ label: {
103
+ type: 'string',
104
+ default: 'INBOX',
105
+ description: 'Gmail label to sync (e.g. "INBOX", "SENT", "STARRED").',
106
+ },
107
+ max_results: {
108
+ type: 'integer',
109
+ minimum: 1,
110
+ maximum: 500,
111
+ default: 50,
112
+ description: 'Maximum threads to fetch per sync.',
113
+ },
114
+ lookback_days: {
115
+ type: 'integer',
116
+ minimum: 1,
117
+ maximum: 365,
118
+ default: 30,
119
+ description: 'Number of days to look back on initial sync.',
120
+ },
121
+ },
122
+ },
123
+ eventKinds: {
124
+ thread: {
125
+ description: 'A Gmail email thread',
126
+ metadataSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ message_count: { type: 'number' },
130
+ label_ids: { type: 'array', items: { type: 'string' } },
131
+ snippet: { type: 'string' },
132
+ from_email: { type: 'string' },
133
+ from_name: { type: 'string' },
134
+ },
135
+ },
136
+ entityLinks: [
137
+ {
138
+ entityType: '$member',
139
+ autoCreate: true,
140
+ titlePath: 'metadata.from_name',
141
+ identities: [{ namespace: IDENTITY.EMAIL, eventPath: 'metadata.from_email' }],
142
+ traits: {
143
+ from_name: {
144
+ eventPath: 'metadata.from_name',
145
+ behavior: 'prefer_non_empty',
146
+ },
147
+ last_email_at: {
148
+ eventPath: 'occurred_at',
149
+ behavior: 'overwrite',
150
+ },
151
+ },
152
+ },
153
+ ],
154
+ },
155
+ },
156
+ },
157
+ },
158
+ actions: {
159
+ send_email: {
160
+ key: 'send_email',
161
+ name: 'Send Email',
162
+ description: 'Send an email via Gmail.',
163
+ requiresApproval: true,
164
+ inputSchema: {
165
+ type: 'object',
166
+ required: ['to', 'subject', 'body'],
167
+ properties: {
168
+ to: { type: 'string', description: 'Recipient email address.' },
169
+ subject: { type: 'string', description: 'Email subject line.' },
170
+ body: { type: 'string', description: 'Email body (plain text).' },
171
+ cc: { type: 'string', description: 'CC recipients (comma-separated).' },
172
+ bcc: { type: 'string', description: 'BCC recipients (comma-separated).' },
173
+ },
174
+ },
175
+ },
176
+ create_draft: {
177
+ key: 'create_draft',
178
+ name: 'Create Draft',
179
+ description: 'Create a draft email in Gmail.',
180
+ inputSchema: {
181
+ type: 'object',
182
+ required: ['to', 'subject', 'body'],
183
+ properties: {
184
+ to: { type: 'string', description: 'Recipient email address.' },
185
+ subject: { type: 'string', description: 'Email subject line.' },
186
+ body: { type: 'string', description: 'Email body (plain text).' },
187
+ cc: { type: 'string', description: 'CC recipients (comma-separated).' },
188
+ bcc: { type: 'string', description: 'BCC recipients (comma-separated).' },
189
+ thread_id: {
190
+ type: 'string',
191
+ description: 'Thread ID to attach the draft to (for replies).',
192
+ },
193
+ },
194
+ },
195
+ },
196
+ reply: {
197
+ key: 'reply',
198
+ name: 'Reply to Thread',
199
+ description: 'Send a reply to an existing email thread.',
200
+ requiresApproval: true,
201
+ inputSchema: {
202
+ type: 'object',
203
+ required: ['thread_id', 'body'],
204
+ properties: {
205
+ thread_id: { type: 'string', description: 'Thread ID to reply to.' },
206
+ body: { type: 'string', description: 'Reply body (plain text).' },
207
+ to: {
208
+ type: 'string',
209
+ description: 'Override recipient (defaults to original sender).',
210
+ },
211
+ cc: { type: 'string', description: 'CC recipients (comma-separated).' },
212
+ },
213
+ },
214
+ },
215
+ search: {
216
+ key: 'search',
217
+ name: 'Search Emails',
218
+ description: 'Search emails by query.',
219
+ inputSchema: {
220
+ type: 'object',
221
+ required: ['query'],
222
+ properties: {
223
+ query: {
224
+ type: 'string',
225
+ description: "Gmail search query e.g. 'from:someone subject:hello'.",
226
+ },
227
+ max_results: {
228
+ type: 'integer',
229
+ description: 'Maximum number of results to return (default 10).',
230
+ },
231
+ },
232
+ },
233
+ },
234
+ get_thread: {
235
+ key: 'get_thread',
236
+ name: 'Get Thread',
237
+ description: 'Read full thread content.',
238
+ inputSchema: {
239
+ type: 'object',
240
+ required: ['thread_id'],
241
+ properties: {
242
+ thread_id: { type: 'string', description: 'Thread ID to read.' },
243
+ },
244
+ },
245
+ },
246
+ },
247
+ };
248
+
249
+ private readonly BASE_URL = 'https://www.googleapis.com/gmail/v1/users/me';
250
+ private readonly RATE_LIMIT_MS = 100;
251
+
252
+ // -------------------------------------------------------------------------
253
+ // sync
254
+ // -------------------------------------------------------------------------
255
+
256
+ async sync(ctx: SyncContext): Promise<SyncResult> {
257
+ const token = ctx.credentials?.accessToken;
258
+ if (!token) {
259
+ throw new Error('Gmail requires Google OAuth credentials.');
260
+ }
261
+
262
+ const label = (ctx.config.label as string) || 'INBOX';
263
+ const maxResults = Math.min((ctx.config.max_results as number) ?? 50, 500);
264
+ const lookbackDays = (ctx.config.lookback_days as number) ?? 30;
265
+
266
+ const checkpoint = (ctx.checkpoint ?? {}) as GmailCheckpoint;
267
+
268
+ // Determine the "after" date for the query
269
+ const afterDate = checkpoint.last_sync_at
270
+ ? new Date(checkpoint.last_sync_at)
271
+ : (() => {
272
+ const d = new Date();
273
+ d.setDate(d.getDate() - lookbackDays);
274
+ return d;
275
+ })();
276
+
277
+ const afterStr = `${afterDate.getFullYear()}/${String(afterDate.getMonth() + 1).padStart(2, '0')}/${String(afterDate.getDate()).padStart(2, '0')}`;
278
+ const query = `after:${afterStr} label:${label}`;
279
+
280
+ const events: EventEnvelope[] = [];
281
+ let pageToken: string | undefined;
282
+ let totalCollected = 0;
283
+
284
+ while (totalCollected < maxResults) {
285
+ const params = new URLSearchParams({
286
+ q: query,
287
+ maxResults: String(Math.min(100, maxResults - totalCollected)),
288
+ });
289
+ if (pageToken) {
290
+ params.set('pageToken', pageToken);
291
+ }
292
+
293
+ const listUrl = `${this.BASE_URL}/threads?${params.toString()}`;
294
+ const listResponse = await this.apiGet(listUrl, token);
295
+
296
+ if (!listResponse.ok) {
297
+ throw new Error(
298
+ `Gmail threads.list error (${listResponse.status}): ${await listResponse.text()}`
299
+ );
300
+ }
301
+
302
+ const listData = (await listResponse.json()) as GmailThreadListResponse;
303
+
304
+ if (!listData.threads || listData.threads.length === 0) break;
305
+
306
+ // Fetch each thread with metadata format
307
+ for (const threadStub of listData.threads) {
308
+ try {
309
+ const threadUrl = `${this.BASE_URL}/threads/${threadStub.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`;
310
+ const threadResponse = await this.apiGet(threadUrl, token);
311
+
312
+ if (!threadResponse.ok) continue;
313
+
314
+ const thread = (await threadResponse.json()) as GmailThreadGetResponse;
315
+
316
+ if (!thread.messages || thread.messages.length === 0) continue;
317
+
318
+ const firstMessage = thread.messages[0];
319
+ const subject = this.getHeader(firstMessage, 'Subject') || '(no subject)';
320
+ const from = this.getHeader(firstMessage, 'From') || 'Unknown';
321
+ const { name: fromName, email: fromEmail } = this.parseFromHeader(from);
322
+ const dateHeader = this.getHeader(firstMessage, 'Date');
323
+ const occurredAt = dateHeader
324
+ ? new Date(dateHeader)
325
+ : new Date(parseInt(firstMessage.internalDate, 10));
326
+
327
+ if (Number.isNaN(occurredAt.getTime())) continue;
328
+
329
+ const event: EventEnvelope = {
330
+ origin_id: thread.id,
331
+ title: subject,
332
+ payload_text: firstMessage.snippet || '',
333
+ author_name: from,
334
+ source_url: `https://mail.google.com/mail/u/0/#inbox/${thread.id}`,
335
+ occurred_at: occurredAt,
336
+ origin_type: 'thread',
337
+ metadata: {
338
+ message_count: thread.messages.length,
339
+ label_ids: firstMessage.labelIds ?? [],
340
+ snippet: firstMessage.snippet,
341
+ ...(fromEmail ? { from_email: fromEmail } : {}),
342
+ ...(fromName ? { from_name: fromName } : {}),
343
+ },
344
+ };
345
+
346
+ events.push(event);
347
+ totalCollected++;
348
+
349
+ await this.sleep(this.RATE_LIMIT_MS);
350
+ } catch {
351
+ /* skip individual thread failures */
352
+ }
353
+ }
354
+
355
+ pageToken = listData.nextPageToken;
356
+ if (!pageToken) break;
357
+
358
+ await this.sleep(this.RATE_LIMIT_MS);
359
+ }
360
+
361
+ // Sort by occurred_at descending
362
+ events.sort((a, b) => b.occurred_at.getTime() - a.occurred_at.getTime());
363
+
364
+ const newCheckpoint: GmailCheckpoint = {
365
+ last_sync_at: new Date().toISOString(),
366
+ };
367
+
368
+ return {
369
+ events,
370
+ checkpoint: newCheckpoint as Record<string, unknown>,
371
+ metadata: {
372
+ items_found: events.length,
373
+ },
374
+ };
375
+ }
376
+
377
+ // -------------------------------------------------------------------------
378
+ // execute
379
+ // -------------------------------------------------------------------------
380
+
381
+ async execute(ctx: ActionContext): Promise<ActionResult> {
382
+ try {
383
+ const token = ctx.credentials?.accessToken;
384
+ if (!token) {
385
+ return { success: false, error: 'Gmail actions require Google OAuth credentials.' };
386
+ }
387
+
388
+ switch (ctx.actionKey) {
389
+ case 'send_email':
390
+ return await this.sendEmail(token, ctx.input);
391
+ case 'create_draft':
392
+ return await this.createDraft(token, ctx.input);
393
+ case 'reply':
394
+ return await this.replyToThread(token, ctx.input);
395
+ case 'search':
396
+ return await this.searchEmails(token, ctx.input);
397
+ case 'get_thread':
398
+ return await this.getThread(token, ctx.input);
399
+ default:
400
+ return { success: false, error: `Unknown action: ${ctx.actionKey}` };
401
+ }
402
+ } catch (error) {
403
+ return {
404
+ success: false,
405
+ error: error instanceof Error ? error.message : String(error),
406
+ };
407
+ }
408
+ }
409
+
410
+ // -------------------------------------------------------------------------
411
+ // Actions
412
+ // -------------------------------------------------------------------------
413
+
414
+ private async sendEmail(token: string, input: Record<string, unknown>): Promise<ActionResult> {
415
+ const to = input.to as string;
416
+ const subject = input.subject as string;
417
+ const body = input.body as string;
418
+ const cc = input.cc as string | undefined;
419
+ const bcc = input.bcc as string | undefined;
420
+
421
+ if (!to || !subject || !body) {
422
+ return { success: false, error: 'to, subject, and body are required.' };
423
+ }
424
+
425
+ // Build RFC 2822 message
426
+ const messageParts: string[] = [`To: ${to}`, `Subject: ${subject}`];
427
+ if (cc) messageParts.push(`Cc: ${cc}`);
428
+ if (bcc) messageParts.push(`Bcc: ${bcc}`);
429
+ messageParts.push('Content-Type: text/plain; charset=utf-8');
430
+ messageParts.push('');
431
+ messageParts.push(body);
432
+
433
+ const rawMessage = messageParts.join('\r\n');
434
+ const encoded = this.base64UrlEncode(rawMessage);
435
+
436
+ const sendUrl = `${this.BASE_URL}/messages/send`;
437
+ const response = await fetch(sendUrl, {
438
+ method: 'POST',
439
+ headers: {
440
+ Authorization: `Bearer ${token}`,
441
+ 'Content-Type': 'application/json',
442
+ },
443
+ body: JSON.stringify({ raw: encoded }),
444
+ });
445
+
446
+ if (!response.ok) {
447
+ const errText = await response.text();
448
+ return { success: false, error: `Gmail send error (${response.status}): ${errText}` };
449
+ }
450
+
451
+ const result = (await response.json()) as { id: string; threadId: string; labelIds: string[] };
452
+
453
+ return {
454
+ success: true,
455
+ output: {
456
+ message_id: result.id,
457
+ thread_id: result.threadId,
458
+ url: `https://mail.google.com/mail/u/0/#inbox/${result.threadId}`,
459
+ },
460
+ };
461
+ }
462
+
463
+ private async createDraft(token: string, input: Record<string, unknown>): Promise<ActionResult> {
464
+ const to = input.to as string;
465
+ const subject = input.subject as string;
466
+ const body = input.body as string;
467
+ const cc = input.cc as string | undefined;
468
+ const bcc = input.bcc as string | undefined;
469
+ const threadId = input.thread_id as string | undefined;
470
+
471
+ if (!to || !subject || !body) {
472
+ return { success: false, error: 'to, subject, and body are required.' };
473
+ }
474
+
475
+ const messageParts: string[] = [`To: ${to}`, `Subject: ${subject}`];
476
+ if (cc) messageParts.push(`Cc: ${cc}`);
477
+ if (bcc) messageParts.push(`Bcc: ${bcc}`);
478
+ messageParts.push('Content-Type: text/plain; charset=utf-8', '', body);
479
+
480
+ const raw = this.base64UrlEncode(messageParts.join('\r\n'));
481
+ const draftBody: { message: { raw: string; threadId?: string } } = { message: { raw } };
482
+ if (threadId) draftBody.message.threadId = threadId;
483
+
484
+ const response = await fetch(`${this.BASE_URL}/drafts`, {
485
+ method: 'POST',
486
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
487
+ body: JSON.stringify(draftBody),
488
+ });
489
+
490
+ if (!response.ok) {
491
+ const errText = await response.text();
492
+ return { success: false, error: `Gmail draft error (${response.status}): ${errText}` };
493
+ }
494
+
495
+ const result = (await response.json()) as {
496
+ id: string;
497
+ message: { id: string; threadId: string };
498
+ };
499
+ return {
500
+ success: true,
501
+ output: {
502
+ draft_id: result.id,
503
+ message_id: result.message.id,
504
+ thread_id: result.message.threadId,
505
+ url: `https://mail.google.com/mail/u/0/#drafts/${result.message.id}`,
506
+ },
507
+ };
508
+ }
509
+
510
+ private async replyToThread(
511
+ token: string,
512
+ input: Record<string, unknown>
513
+ ): Promise<ActionResult> {
514
+ const threadId = input.thread_id as string;
515
+ const body = input.body as string;
516
+ const cc = input.cc as string | undefined;
517
+
518
+ if (!threadId || !body) {
519
+ return { success: false, error: 'thread_id and body are required.' };
520
+ }
521
+
522
+ // Fetch the thread to get the last message's headers
523
+ const threadRes = await this.apiGet(
524
+ `${this.BASE_URL}/threads/${threadId}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Message-ID`,
525
+ token
526
+ );
527
+ if (!threadRes.ok) {
528
+ return {
529
+ success: false,
530
+ error: `Failed to fetch thread (${threadRes.status}): ${await threadRes.text()}`,
531
+ };
532
+ }
533
+
534
+ const thread = (await threadRes.json()) as { messages: GmailMessage[] };
535
+ const lastMsg = thread.messages[thread.messages.length - 1];
536
+ const subject = this.getHeader(lastMsg, 'Subject') || '';
537
+ const from = this.getHeader(lastMsg, 'From') || '';
538
+ const messageId = this.getHeader(lastMsg, 'Message-ID') || '';
539
+ const to = (input.to as string) || from;
540
+
541
+ const messageParts: string[] = [
542
+ `To: ${to}`,
543
+ `Subject: ${subject.startsWith('Re:') ? subject : `Re: ${subject}`}`,
544
+ `In-Reply-To: ${messageId}`,
545
+ `References: ${messageId}`,
546
+ ];
547
+ if (cc) messageParts.push(`Cc: ${cc}`);
548
+ messageParts.push('Content-Type: text/plain; charset=utf-8', '', body);
549
+
550
+ const raw = this.base64UrlEncode(messageParts.join('\r\n'));
551
+
552
+ const response = await fetch(`${this.BASE_URL}/messages/send`, {
553
+ method: 'POST',
554
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
555
+ body: JSON.stringify({ raw, threadId }),
556
+ });
557
+
558
+ if (!response.ok) {
559
+ const errText = await response.text();
560
+ return { success: false, error: `Gmail reply error (${response.status}): ${errText}` };
561
+ }
562
+
563
+ const result = (await response.json()) as { id: string; threadId: string };
564
+ return {
565
+ success: true,
566
+ output: {
567
+ message_id: result.id,
568
+ thread_id: result.threadId,
569
+ url: `https://mail.google.com/mail/u/0/#inbox/${result.threadId}`,
570
+ },
571
+ };
572
+ }
573
+
574
+ private async searchEmails(token: string, input: Record<string, unknown>): Promise<ActionResult> {
575
+ const query = input.query as string;
576
+ const maxResults = (input.max_results as number) || 10;
577
+
578
+ if (!query) {
579
+ return { success: false, error: 'query is required.' };
580
+ }
581
+
582
+ const params = new URLSearchParams({
583
+ q: query,
584
+ maxResults: String(maxResults),
585
+ });
586
+ const listUrl = `${this.BASE_URL}/messages?${params.toString()}`;
587
+ const listResponse = await this.apiGet(listUrl, token);
588
+
589
+ if (!listResponse.ok) {
590
+ const errText = await listResponse.text();
591
+ return { success: false, error: `Gmail search error (${listResponse.status}): ${errText}` };
592
+ }
593
+
594
+ const listData = (await listResponse.json()) as {
595
+ messages?: Array<{ id: string; threadId: string }>;
596
+ };
597
+
598
+ if (!listData.messages || listData.messages.length === 0) {
599
+ return { success: true, output: { messages: [] } };
600
+ }
601
+
602
+ const messages: Array<{
603
+ id: string;
604
+ thread_id: string;
605
+ subject: string;
606
+ from: string;
607
+ date: string;
608
+ url: string;
609
+ }> = [];
610
+
611
+ for (const msg of listData.messages) {
612
+ const msgUrl = `${this.BASE_URL}/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`;
613
+ const msgResponse = await this.apiGet(msgUrl, token);
614
+ if (!msgResponse.ok) continue;
615
+
616
+ const msgData = (await msgResponse.json()) as GmailMessage;
617
+ messages.push({
618
+ id: msgData.id,
619
+ thread_id: msgData.threadId,
620
+ subject: this.getHeader(msgData, 'Subject') || '(no subject)',
621
+ from: this.getHeader(msgData, 'From') || 'Unknown',
622
+ date: this.getHeader(msgData, 'Date') || '',
623
+ url: `https://mail.google.com/mail/u/0/#inbox/${msgData.threadId}`,
624
+ });
625
+ }
626
+
627
+ return { success: true, output: { messages } };
628
+ }
629
+
630
+ private async getThread(token: string, input: Record<string, unknown>): Promise<ActionResult> {
631
+ const threadId = input.thread_id as string;
632
+
633
+ if (!threadId) {
634
+ return { success: false, error: 'thread_id is required.' };
635
+ }
636
+
637
+ const url = `${this.BASE_URL}/threads/${threadId}?format=full`;
638
+ const response = await this.apiGet(url, token);
639
+
640
+ if (!response.ok) {
641
+ const errText = await response.text();
642
+ return { success: false, error: `Gmail thread error (${response.status}): ${errText}` };
643
+ }
644
+
645
+ const thread = (await response.json()) as GmailThreadGetResponse;
646
+ const subject =
647
+ thread.messages.length > 0
648
+ ? this.getHeader(thread.messages[0], 'Subject') || '(no subject)'
649
+ : '(no subject)';
650
+
651
+ const messages = thread.messages.map((msg) => ({
652
+ id: msg.id,
653
+ from: this.getHeader(msg, 'From') || 'Unknown',
654
+ date: this.getHeader(msg, 'Date') || '',
655
+ snippet: msg.snippet,
656
+ body: this.extractBody(msg.payload),
657
+ }));
658
+
659
+ return {
660
+ success: true,
661
+ output: {
662
+ thread_id: thread.id,
663
+ subject,
664
+ messages,
665
+ url: `https://mail.google.com/mail/u/0/#inbox/${thread.id}`,
666
+ },
667
+ };
668
+ }
669
+
670
+ // -------------------------------------------------------------------------
671
+ // Helpers
672
+ // -------------------------------------------------------------------------
673
+
674
+ private getHeader(message: GmailMessage, name: string): string | undefined {
675
+ const header = message.payload.headers.find((h) => h.name.toLowerCase() === name.toLowerCase());
676
+ return header?.value;
677
+ }
678
+
679
+ /**
680
+ * Parse an RFC 5322 From header into display name and email address.
681
+ * Accepts: "Name <addr@host>", "<addr@host>", "addr@host", or quoted names.
682
+ */
683
+ private parseFromHeader(raw: string): { name: string | null; email: string | null } {
684
+ const trimmed = raw.trim();
685
+ if (!trimmed || trimmed === 'Unknown') return { name: null, email: null };
686
+
687
+ const angleMatch = trimmed.match(/^(.*?)<([^>]+)>\s*$/);
688
+ if (angleMatch) {
689
+ const name = angleMatch[1].trim().replace(/^"|"$/g, '').trim();
690
+ const email = angleMatch[2].trim();
691
+ return { name: name || null, email: email || null };
692
+ }
693
+
694
+ if (trimmed.includes('@') && !trimmed.includes(' ')) {
695
+ return { name: null, email: trimmed };
696
+ }
697
+
698
+ return { name: trimmed, email: null };
699
+ }
700
+
701
+ private extractBody(payload: GmailMessagePayload): string {
702
+ // Try to get body from payload.body.data directly
703
+ if (payload.body?.data) {
704
+ return this.base64UrlDecode(payload.body.data);
705
+ }
706
+
707
+ // Search through parts for text/plain or text/html
708
+ if (payload.parts) {
709
+ for (const part of payload.parts) {
710
+ if (part.mimeType === 'text/plain' && part.body?.data) {
711
+ return this.base64UrlDecode(part.body.data);
712
+ }
713
+ }
714
+ // Fallback to text/html if no plain text
715
+ for (const part of payload.parts) {
716
+ if (part.mimeType === 'text/html' && part.body?.data) {
717
+ return this.base64UrlDecode(part.body.data);
718
+ }
719
+ }
720
+ // Recurse into nested parts (e.g. multipart/alternative inside multipart/mixed)
721
+ for (const part of payload.parts) {
722
+ if (part.parts) {
723
+ const nested = this.extractBody(part);
724
+ if (nested) return nested;
725
+ }
726
+ }
727
+ }
728
+
729
+ return '';
730
+ }
731
+
732
+ private base64UrlDecode(data: string): string {
733
+ const padded = data.replace(/-/g, '+').replace(/_/g, '/');
734
+ return Buffer.from(padded, 'base64').toString('utf-8');
735
+ }
736
+
737
+ private base64UrlEncode(str: string): string {
738
+ const encoded = Buffer.from(str, 'utf-8').toString('base64');
739
+ return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
740
+ }
741
+
742
+ private async apiGet(url: string, token: string): Promise<Response> {
743
+ return fetch(url, {
744
+ headers: { Authorization: `Bearer ${token}` },
745
+ });
746
+ }
747
+
748
+ private sleep(ms: number): Promise<void> {
749
+ return new Promise((resolve) => setTimeout(resolve, ms));
750
+ }
751
+ }