agiagent-dev 2026.1.39 → 2026.1.40

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,192 @@
1
+ /**
2
+ * Email trigger handler for hosted mode.
3
+ *
4
+ * When Composio fires a GMAIL_NEW_GMAIL_MESSAGE webhook:
5
+ * 1. Looks up the hosted user from the Composio user_id
6
+ * 2. If node connected → runs the AI agent with full tool access so it can
7
+ * read the email, decide the correct action (reply vs new email, attach
8
+ * resume, etc.), and create the draft via the gmail tool
9
+ * 3. If node not connected → sends Telegram-only notification
10
+ */
11
+ import { createSubsystemLogger } from "../logging/subsystem.js";
12
+ import { runHostedAgentReply } from "./hosted-agent.js";
13
+ import { getHostedDb, getHostedUserNodeId } from "./hosted-db.js";
14
+ import { getHostedTelegramSession, sendHostedTelegramNotification } from "./hosted-telegram.js";
15
+ const log = createSubsystemLogger("email-trigger");
16
+ /** Try to extract a Gmail message ID from the Composio trigger data. */
17
+ function extractMessageIdFromPayload(data) {
18
+ for (const key of ["messageId", "message_id", "id", "messageID"]) {
19
+ const val = data[key];
20
+ if (typeof val === "string" && val.trim()) {
21
+ return val.trim();
22
+ }
23
+ }
24
+ for (const outer of ["messageData", "message", "email"]) {
25
+ const nested = data[outer];
26
+ if (nested && typeof nested === "object") {
27
+ const rec = nested;
28
+ for (const key of ["id", "messageId", "message_id"]) {
29
+ const val = rec[key];
30
+ if (typeof val === "string" && val.trim()) {
31
+ return val.trim();
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /** Extract sender from trigger payload (best-effort). */
39
+ function extractSenderFromPayload(data) {
40
+ for (const key of ["from", "sender", "senderEmail", "sender_email"]) {
41
+ const val = data[key];
42
+ if (typeof val === "string" && val.trim()) {
43
+ return val.trim();
44
+ }
45
+ }
46
+ return "unknown sender";
47
+ }
48
+ /** Extract subject from trigger payload (best-effort). */
49
+ function extractSubjectFromPayload(data) {
50
+ for (const key of ["subject", "Subject"]) {
51
+ const val = data[key];
52
+ if (typeof val === "string" && val.trim()) {
53
+ return val.trim();
54
+ }
55
+ }
56
+ return "(no subject)";
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // AI agent prompt
60
+ // ---------------------------------------------------------------------------
61
+ /**
62
+ * Build the prompt that instructs the AI agent to handle the incoming email.
63
+ * The agent has full tool access: gmail (read, draft, send), exec (resume
64
+ * skills), browser, etc. It decides the best action autonomously.
65
+ */
66
+ function buildEmailTriggerPrompt(params) {
67
+ const { messageId, senderHint, subjectHint } = params;
68
+ const readStep = messageId
69
+ ? `First, use the gmail tool with action "messages_get" and messageId "${messageId}" to read the full email content.`
70
+ : `The email metadata: From: ${senderHint}, Subject: ${subjectHint}. Try searching for it with the gmail tool if you need the full content.`;
71
+ return (`[EMAIL AUTO-DRAFT] A new email has arrived in the user's Gmail inbox.\n\n` +
72
+ `${readStep}\n\n` +
73
+ `After reading the email, analyze it carefully and take the appropriate action:\n\n` +
74
+ `1. **Determine the intent**: Is this a job posting/requirement, a conversation, a newsletter, a notification, etc.?\n\n` +
75
+ `2. **Determine the recipient**: Read the email body carefully. The reply might NOT go to the sender. ` +
76
+ `For example, if the email says "send your resume to someone@example.com", the draft should go to that address, not the sender. ` +
77
+ `Decide whether to:\n` +
78
+ ` - Create a **reply draft** in the same thread (use gmail drafts_create with messageId)\n` +
79
+ ` - Create a **new email draft** to a different recipient mentioned in the email (use gmail drafts_create with to + subject)\n\n` +
80
+ `3. **Job postings / requirements**: If the email contains a job posting, recruiter outreach, or asks for a resume:\n` +
81
+ ` - Customize the resume for the role using the resume tool (extract -> patch -> apply)\n` +
82
+ ` - In new bullets you add, bold only the important ATS keywords (tools/platforms/product names), not full sentences\n` +
83
+ ` - Attach the customized resume to the draft (pass the file path in the attachments array)\n` +
84
+ ` - Write a professional, concise cover note as the draft body\n\n` +
85
+ `4. **Regular emails**: For normal conversations, compose a professional reply draft that matches a natural email tone.\n\n` +
86
+ `5. **Newsletters / notifications / no-reply**: If the email doesn't need a response, do NOT create a draft. ` +
87
+ `Just respond with a brief note like "No draft needed — this is a notification email."\n\n` +
88
+ `IMPORTANT:\n` +
89
+ `- Always use the gmail tool's "drafts_create" action to save drafts — do NOT use "messages_send" or "messages_reply" (those send immediately).\n` +
90
+ `- Your text response here will be shown to the user on Telegram as a notification. ` +
91
+ `Briefly tell them what you did (e.g. "Drafted a reply to X about Y" or "Drafted an email to recruiter@co.com with your customized resume attached").`);
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Core handler
95
+ // ---------------------------------------------------------------------------
96
+ /**
97
+ * Handle a GMAIL_NEW_GMAIL_MESSAGE webhook event.
98
+ *
99
+ * Composio V2 payload structure:
100
+ * { type, timestamp, log_id, data: { connection_id, connection_nano_id,
101
+ * trigger_nano_id, trigger_id, user_id, ...email fields } }
102
+ */
103
+ export async function handleNewGmailMessage(payload) {
104
+ const data = payload.data ?? {};
105
+ // V2: user_id is inside data, not at the top level
106
+ const userId = (typeof data.user_id === "string" ? data.user_id.trim() : "") || payload.user_id?.trim() || "";
107
+ if (!userId) {
108
+ log.warn("webhook missing user_id, cannot route");
109
+ log.info(`payload keys: ${Object.keys(payload).join(", ")}`);
110
+ log.info(`payload.data keys: ${Object.keys(data).join(", ")}`);
111
+ return;
112
+ }
113
+ const db = getHostedDb();
114
+ if (!db) {
115
+ log.warn("hosted DB unavailable");
116
+ return;
117
+ }
118
+ const user = await db.getUserById(userId);
119
+ if (!user) {
120
+ log.warn(`unknown user_id from webhook: ${userId}`);
121
+ return;
122
+ }
123
+ const senderInfo = extractSenderFromPayload(data);
124
+ const subjectInfo = extractSubjectFromPayload(data);
125
+ const messageId = extractMessageIdFromPayload(data);
126
+ log.info(`new email for ${user.name}: from=${senderInfo.slice(0, 60)} ` +
127
+ `subject=${subjectInfo.slice(0, 60)} msgId=${messageId ?? "?"}`);
128
+ log.info(`trigger data keys: ${Object.keys(data).join(", ")}`);
129
+ // Check if user has an active Telegram session with a connected node
130
+ const telegramSession = getHostedTelegramSession(userId);
131
+ if (!telegramSession) {
132
+ log.info(`user ${user.name} has no active Telegram session, skipping`);
133
+ return;
134
+ }
135
+ const { nodeId, userName, sendNodeEvent } = telegramSession;
136
+ // Check if the node is actually connected
137
+ const connectedNodeId = getHostedUserNodeId(userId);
138
+ if (!connectedNodeId) {
139
+ log.info(`user ${user.name} node not connected, sending notification only`);
140
+ const msg = `New email received from <b>${escapeHtml(senderInfo)}</b>\n` +
141
+ `Subject: <b>${escapeHtml(subjectInfo)}</b>\n\n` +
142
+ `Your device is not connected, so I couldn't draft a reply. ` +
143
+ `Connect with <code>agiagent connect</code> to enable auto-drafting.`;
144
+ await sendHostedTelegramNotification(userId, msg);
145
+ return;
146
+ }
147
+ // Node IS connected — let the AI agent handle everything via tools
148
+ log.info(`running AI agent for email draft — user ${user.name}`);
149
+ const agentPrompt = buildEmailTriggerPrompt({
150
+ messageId,
151
+ senderHint: senderInfo,
152
+ subjectHint: subjectInfo,
153
+ });
154
+ let agentResponse = "";
155
+ try {
156
+ agentResponse = await runHostedAgentReply({
157
+ userId,
158
+ userName,
159
+ nodeId,
160
+ messageText: agentPrompt,
161
+ senderJid: "email-trigger",
162
+ chatJid: `email-draft-${userId}`,
163
+ isGroup: false,
164
+ sendNodeEvent,
165
+ });
166
+ }
167
+ catch (err) {
168
+ log.error(`AI agent failed for user ${user.name}: ${String(err)}`);
169
+ await sendHostedTelegramNotification(userId, `New email from <b>${escapeHtml(senderInfo)}</b> — ` +
170
+ `<b>${escapeHtml(subjectInfo)}</b>\n\n` +
171
+ `I tried to process this email but encountered an error. ` +
172
+ `You can handle it manually in Gmail.`);
173
+ return;
174
+ }
175
+ // Send the AI's response as a Telegram notification.
176
+ // The AI should have already created the draft via the gmail tool.
177
+ if (agentResponse?.trim()) {
178
+ await sendHostedTelegramNotification(userId, agentResponse);
179
+ }
180
+ else {
181
+ await sendHostedTelegramNotification(userId, `New email from <b>${escapeHtml(senderInfo)}</b> — ` +
182
+ `<b>${escapeHtml(subjectInfo)}</b>\n\n` +
183
+ `I processed the email but had nothing to report.`);
184
+ }
185
+ log.info(`email trigger complete for user ${user.name}`);
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Utility
189
+ // ---------------------------------------------------------------------------
190
+ function escapeHtml(text) {
191
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
192
+ }
@@ -27,6 +27,16 @@ export type ComposioConnectedAccountRecord = {
27
27
  createdAt: Date;
28
28
  updatedAt: Date;
29
29
  };
30
+ export type ComposioTriggerRecord = {
31
+ id: string;
32
+ userId: string;
33
+ toolkitSlug: string;
34
+ triggerSlug: string;
35
+ triggerId: string;
36
+ enabled: boolean;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ };
30
40
  export type HostedDbClient = {
31
41
  /** Look up a connection token and return the associated user */
32
42
  lookupToken: (token: string) => Promise<{
@@ -65,6 +75,29 @@ export type HostedDbClient = {
65
75
  userId: string;
66
76
  toolkitSlug: string;
67
77
  }) => Promise<void>;
78
+ /** Upsert a Composio trigger record */
79
+ upsertComposioTrigger: (params: {
80
+ userId: string;
81
+ toolkitSlug: string;
82
+ triggerSlug: string;
83
+ triggerId: string;
84
+ }) => Promise<void>;
85
+ /** Get a Composio trigger record for a user/toolkit/slug */
86
+ getComposioTrigger: (params: {
87
+ userId: string;
88
+ toolkitSlug: string;
89
+ triggerSlug: string;
90
+ }) => Promise<ComposioTriggerRecord | null>;
91
+ /** Delete a Composio trigger record */
92
+ deleteComposioTrigger: (params: {
93
+ userId: string;
94
+ toolkitSlug: string;
95
+ triggerSlug: string;
96
+ }) => Promise<void>;
97
+ /** Look up a user by their UUID (used to map Composio user_id from webhooks). */
98
+ getUserById: (userId: string) => Promise<HostedUser | null>;
99
+ /** Get the connection token record for a user (first match). */
100
+ getTokenByUserId: (userId: string) => Promise<ConnectionToken | null>;
68
101
  /** Close the database connection */
69
102
  close: () => Promise<void>;
70
103
  };
@@ -49,6 +49,19 @@ export async function initHostedDb() {
49
49
  updated_at TIMESTAMPTZ DEFAULT now(),
50
50
  PRIMARY KEY (user_id, toolkit_slug)
51
51
  )
52
+ `;
53
+ await sql `
54
+ CREATE TABLE IF NOT EXISTS composio_triggers (
55
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
56
+ user_id UUID REFERENCES users(id),
57
+ toolkit_slug TEXT NOT NULL,
58
+ trigger_slug TEXT NOT NULL,
59
+ trigger_id TEXT NOT NULL,
60
+ enabled BOOLEAN DEFAULT true,
61
+ created_at TIMESTAMPTZ DEFAULT now(),
62
+ updated_at TIMESTAMPTZ DEFAULT now(),
63
+ UNIQUE (user_id, toolkit_slug, trigger_slug)
64
+ )
52
65
  `;
53
66
  dbClient = {
54
67
  lookupToken: async (token) => {
@@ -208,6 +221,90 @@ export async function initHostedDb() {
208
221
  WHERE user_id = ${params.userId} AND toolkit_slug = ${params.toolkitSlug}
209
222
  `;
210
223
  },
224
+ upsertComposioTrigger: async (params) => {
225
+ await sql `
226
+ INSERT INTO composio_triggers (
227
+ user_id, toolkit_slug, trigger_slug, trigger_id, updated_at
228
+ )
229
+ VALUES (
230
+ ${params.userId}, ${params.toolkitSlug},
231
+ ${params.triggerSlug}, ${params.triggerId}, now()
232
+ )
233
+ ON CONFLICT (user_id, toolkit_slug, trigger_slug) DO UPDATE SET
234
+ trigger_id = EXCLUDED.trigger_id,
235
+ enabled = true,
236
+ updated_at = now()
237
+ `;
238
+ },
239
+ getComposioTrigger: async (params) => {
240
+ const rows = await sql `
241
+ SELECT id, user_id, toolkit_slug, trigger_slug, trigger_id,
242
+ enabled, created_at, updated_at
243
+ FROM composio_triggers
244
+ WHERE user_id = ${params.userId}
245
+ AND toolkit_slug = ${params.toolkitSlug}
246
+ AND trigger_slug = ${params.triggerSlug}
247
+ LIMIT 1
248
+ `;
249
+ if (rows.length === 0) {
250
+ return null;
251
+ }
252
+ const row = rows[0];
253
+ return {
254
+ id: row.id,
255
+ userId: row.user_id,
256
+ toolkitSlug: row.toolkit_slug,
257
+ triggerSlug: row.trigger_slug,
258
+ triggerId: row.trigger_id,
259
+ enabled: row.enabled,
260
+ createdAt: row.created_at,
261
+ updatedAt: row.updated_at,
262
+ };
263
+ },
264
+ deleteComposioTrigger: async (params) => {
265
+ await sql `
266
+ DELETE FROM composio_triggers
267
+ WHERE user_id = ${params.userId}
268
+ AND toolkit_slug = ${params.toolkitSlug}
269
+ AND trigger_slug = ${params.triggerSlug}
270
+ `;
271
+ },
272
+ getUserById: async (userId) => {
273
+ const rows = await sql `
274
+ SELECT id, name, created_at FROM users WHERE id = ${userId} LIMIT 1
275
+ `;
276
+ if (rows.length === 0) {
277
+ return null;
278
+ }
279
+ return {
280
+ id: rows[0].id,
281
+ name: rows[0].name,
282
+ createdAt: rows[0].created_at,
283
+ };
284
+ },
285
+ getTokenByUserId: async (userId) => {
286
+ const rows = await sql `
287
+ SELECT id, user_id, token, whatsapp_account_id, telegram_bot_token,
288
+ telegram_owner_id, created_at, last_connected_at
289
+ FROM connection_tokens
290
+ WHERE user_id = ${userId}
291
+ LIMIT 1
292
+ `;
293
+ if (rows.length === 0) {
294
+ return null;
295
+ }
296
+ const row = rows[0];
297
+ return {
298
+ id: row.id,
299
+ userId: row.user_id,
300
+ token: row.token,
301
+ whatsappAccountId: row.whatsapp_account_id,
302
+ telegramBotToken: row.telegram_bot_token,
303
+ telegramOwnerId: row.telegram_owner_id,
304
+ createdAt: row.created_at,
305
+ lastConnectedAt: row.last_connected_at,
306
+ };
307
+ },
211
308
  close: async () => {
212
309
  await sql.end();
213
310
  dbClient = null;
@@ -27,3 +27,18 @@ export declare function stopHostedTelegramBot(userId: string): Promise<void>;
27
27
  * Check if a hosted user has an active Telegram bot.
28
28
  */
29
29
  export declare function isHostedTelegramBotRunning(userId: string): boolean;
30
+ /**
31
+ * Get the active Telegram session info for a hosted user.
32
+ * Returns null if no active bot or bot not running.
33
+ */
34
+ export declare function getHostedTelegramSession(userId: string): {
35
+ nodeId: string;
36
+ userName: string;
37
+ telegramOwnerId: string | null;
38
+ sendNodeEvent: (event: string, payload: unknown) => void;
39
+ } | null;
40
+ /**
41
+ * Send a notification message to a hosted user's Telegram bot.
42
+ * Sends to the bot owner's DM chat. Returns true if sent, false if bot not available.
43
+ */
44
+ export declare function sendHostedTelegramNotification(userId: string, text: string): Promise<boolean>;
@@ -233,3 +233,44 @@ export function isHostedTelegramBotRunning(userId) {
233
233
  const session = activeHostedBots.get(userId);
234
234
  return session?.running ?? false;
235
235
  }
236
+ /**
237
+ * Get the active Telegram session info for a hosted user.
238
+ * Returns null if no active bot or bot not running.
239
+ */
240
+ export function getHostedTelegramSession(userId) {
241
+ const session = activeHostedBots.get(userId);
242
+ if (!session?.running) {
243
+ return null;
244
+ }
245
+ return {
246
+ nodeId: session.nodeId,
247
+ userName: session.userName,
248
+ telegramOwnerId: session.telegramOwnerId,
249
+ sendNodeEvent: session.sendNodeEvent,
250
+ };
251
+ }
252
+ /**
253
+ * Send a notification message to a hosted user's Telegram bot.
254
+ * Sends to the bot owner's DM chat. Returns true if sent, false if bot not available.
255
+ */
256
+ export async function sendHostedTelegramNotification(userId, text) {
257
+ const session = activeHostedBots.get(userId);
258
+ if (!session?.running || !session.telegramOwnerId) {
259
+ return false;
260
+ }
261
+ try {
262
+ await session.bot.api
263
+ .sendMessage(Number(session.telegramOwnerId), text, { parse_mode: "HTML" })
264
+ .catch(async () => {
265
+ // Fallback to plain text if HTML fails
266
+ await session.bot.api.sendMessage(Number(session.telegramOwnerId), text);
267
+ });
268
+ return true;
269
+ }
270
+ catch (err) {
271
+ log.error(`Failed to send Telegram notification to user ${userId}`, {
272
+ error: String(err),
273
+ });
274
+ return false;
275
+ }
276
+ }
@@ -4,6 +4,7 @@ import { resolveAgentAvatar } from "../agents/identity-avatar.js";
4
4
  import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
5
5
  import { loadConfig } from "../config/config.js";
6
6
  import { handleSlackHttpRequest } from "../slack/http/index.js";
7
+ import { handleComposioWebhookRequest } from "./composio-webhook.js";
7
8
  import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js";
8
9
  import { applyHookMappings } from "./hooks-mapping.js";
9
10
  import { extractHookToken, getHookChannelError, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody, resolveHookChannel, resolveHookDeliver, } from "./hooks.js";
@@ -162,6 +163,10 @@ export function createGatewayHttpServer(opts) {
162
163
  if (await handleHooksRequest(req, res)) {
163
164
  return;
164
165
  }
166
+ // Composio trigger webhooks (e.g. GMAIL_NEW_GMAIL_MESSAGE)
167
+ if (await handleComposioWebhookRequest(req, res)) {
168
+ return;
169
+ }
165
170
  if (await handleToolsInvokeHttpRequest(req, res, {
166
171
  auth: resolvedAuth,
167
172
  trustedProxies,
@@ -1,6 +1,72 @@
1
- import { composioCreateConnectedAccountLink, composioGetConnectedAccount, } from "../../infra/composio.js";
1
+ import { composioCreateConnectedAccountLink, composioCreateGmailTrigger, composioDeleteTrigger, composioGetConnectedAccount, } from "../../infra/composio.js";
2
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
2
3
  import { getHostedDb, isHostedMode, validateHostedToken } from "../hosted-db.js";
3
4
  import { ErrorCodes, errorShape } from "../protocol/index.js";
5
+ const log = createSubsystemLogger("composio-handlers");
6
+ const GMAIL_TRIGGER_SLUG = "GMAIL_NEW_GMAIL_MESSAGE";
7
+ /**
8
+ * Ensure a Gmail trigger exists for a user. Best-effort — logs errors but
9
+ * does not throw so it never blocks the link/status response.
10
+ */
11
+ async function ensureGmailTrigger(userId, connectedAccountId) {
12
+ const db = getHostedDb();
13
+ if (!db) {
14
+ return;
15
+ }
16
+ try {
17
+ const existing = await db.getComposioTrigger({
18
+ userId,
19
+ toolkitSlug: "gmail",
20
+ triggerSlug: GMAIL_TRIGGER_SLUG,
21
+ });
22
+ if (existing) {
23
+ log.info(`gmail trigger already exists for user ${userId}: ${existing.triggerId}`);
24
+ return;
25
+ }
26
+ const { triggerId } = await composioCreateGmailTrigger({ connectedAccountId });
27
+ await db.upsertComposioTrigger({
28
+ userId,
29
+ toolkitSlug: "gmail",
30
+ triggerSlug: GMAIL_TRIGGER_SLUG,
31
+ triggerId,
32
+ });
33
+ log.info(`created gmail trigger ${triggerId} for user ${userId}`);
34
+ }
35
+ catch (err) {
36
+ log.error(`failed to ensure gmail trigger for user ${userId}: ${String(err)}`);
37
+ }
38
+ }
39
+ /**
40
+ * Remove the Gmail trigger for a user. Best-effort.
41
+ */
42
+ async function removeGmailTrigger(userId) {
43
+ const db = getHostedDb();
44
+ if (!db) {
45
+ return;
46
+ }
47
+ try {
48
+ const record = await db.getComposioTrigger({
49
+ userId,
50
+ toolkitSlug: "gmail",
51
+ triggerSlug: GMAIL_TRIGGER_SLUG,
52
+ });
53
+ if (!record) {
54
+ return;
55
+ }
56
+ await composioDeleteTrigger(record.triggerId).catch((err) => {
57
+ log.warn(`composio delete trigger failed (may already be gone): ${String(err)}`);
58
+ });
59
+ await db.deleteComposioTrigger({
60
+ userId,
61
+ toolkitSlug: "gmail",
62
+ triggerSlug: GMAIL_TRIGGER_SLUG,
63
+ });
64
+ log.info(`removed gmail trigger for user ${userId}`);
65
+ }
66
+ catch (err) {
67
+ log.error(`failed to remove gmail trigger for user ${userId}: ${String(err)}`);
68
+ }
69
+ }
4
70
  function requireHostedTokenFromClient(client) {
5
71
  const token = client?.connect?.auth?.token?.trim();
6
72
  if (!token) {
@@ -57,6 +123,8 @@ export const composioHandlers = {
57
123
  statusReason: account.status_reason ?? null,
58
124
  });
59
125
  if (account.status === "ACTIVE") {
126
+ // Ensure email trigger is set up for this user
127
+ void ensureGmailTrigger(hostedUser.userId, existing.connectedAccountId);
60
128
  respond(true, {
61
129
  alreadyConnected: true,
62
130
  status: account.status,
@@ -146,6 +214,10 @@ export const composioHandlers = {
146
214
  status: account.status,
147
215
  statusReason: account.status_reason ?? null,
148
216
  });
217
+ // Ensure email trigger is set up when status confirmed ACTIVE
218
+ if (account.status === "ACTIVE") {
219
+ void ensureGmailTrigger(hostedUser.userId, record.connectedAccountId);
220
+ }
149
221
  respond(true, {
150
222
  linked: true,
151
223
  status: account.status,
@@ -186,6 +258,8 @@ export const composioHandlers = {
186
258
  respond(true, { disconnected: false, linked: false });
187
259
  return;
188
260
  }
261
+ // Remove the email trigger before disconnecting
262
+ await removeGmailTrigger(hostedUser.userId);
189
263
  await db.deleteComposioConnectedAccount({
190
264
  userId: hostedUser.userId,
191
265
  toolkitSlug: "gmail",
@@ -22,6 +22,16 @@ export declare function composioCreateConnectedAccountLink(params: {
22
22
  export declare function composioGetConnectedAccount(params: {
23
23
  connectedAccountId: string;
24
24
  }): Promise<ComposioConnectedAccount>;
25
+ /** Create (or re-enable) a GMAIL_NEW_GMAIL_MESSAGE trigger for a connected account. */
26
+ export declare function composioCreateGmailTrigger(params: {
27
+ connectedAccountId: string;
28
+ }): Promise<{
29
+ triggerId: string;
30
+ }>;
31
+ /** Delete a Composio trigger instance. */
32
+ export declare function composioDeleteTrigger(triggerId: string): Promise<void>;
33
+ /** Enable or disable a Composio trigger instance. */
34
+ export declare function composioSetTriggerEnabled(triggerId: string, enabled: boolean): Promise<void>;
25
35
  export declare function composioExtractAccessToken(connectedAccount: unknown): {
26
36
  accessToken: string;
27
37
  expiresAt?: string;
@@ -97,6 +97,65 @@ function findStringFieldDeep(value, predicate, depth) {
97
97
  }
98
98
  return null;
99
99
  }
100
+ /** Create (or re-enable) a GMAIL_NEW_GMAIL_MESSAGE trigger for a connected account. */
101
+ export async function composioCreateGmailTrigger(params) {
102
+ const apiKey = requireComposioApiKey();
103
+ const id = params.connectedAccountId.trim();
104
+ if (!id) {
105
+ throw new Error("connectedAccountId required");
106
+ }
107
+ const res = await composioFetch("/api/v3/trigger_instances/GMAIL_NEW_GMAIL_MESSAGE/upsert", {
108
+ method: "POST",
109
+ headers: {
110
+ "content-type": "application/json",
111
+ "x-api-key": apiKey,
112
+ },
113
+ body: JSON.stringify({
114
+ connected_account_id: id,
115
+ trigger_config: {},
116
+ }),
117
+ });
118
+ const payload = (await res.json());
119
+ if (!payload || typeof payload.trigger_id !== "string") {
120
+ log.warn(`unexpected trigger upsert response: ${JSON.stringify(payload).slice(0, 500)}`);
121
+ throw new Error("unexpected composio trigger upsert response");
122
+ }
123
+ log.info(`created gmail trigger ${payload.trigger_id} for account ${id}`);
124
+ return { triggerId: payload.trigger_id };
125
+ }
126
+ /** Delete a Composio trigger instance. */
127
+ export async function composioDeleteTrigger(triggerId) {
128
+ const apiKey = requireComposioApiKey();
129
+ const id = triggerId.trim();
130
+ if (!id) {
131
+ throw new Error("triggerId required");
132
+ }
133
+ await composioFetch(`/api/v3/trigger_instances/manage/${encodeURIComponent(id)}`, {
134
+ method: "DELETE",
135
+ headers: { "x-api-key": apiKey },
136
+ });
137
+ log.info(`deleted trigger ${id}`);
138
+ }
139
+ /** Enable or disable a Composio trigger instance. */
140
+ export async function composioSetTriggerEnabled(triggerId, enabled) {
141
+ const apiKey = requireComposioApiKey();
142
+ const id = triggerId.trim();
143
+ if (!id) {
144
+ throw new Error("triggerId required");
145
+ }
146
+ await composioFetch(`/api/v3/trigger_instances/manage/${encodeURIComponent(id)}`, {
147
+ method: "PATCH",
148
+ headers: {
149
+ "content-type": "application/json",
150
+ "x-api-key": apiKey,
151
+ },
152
+ body: JSON.stringify({ enabled }),
153
+ });
154
+ log.info(`trigger ${id} ${enabled ? "enabled" : "disabled"}`);
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Access token extraction
158
+ // ---------------------------------------------------------------------------
100
159
  export function composioExtractAccessToken(connectedAccount) {
101
160
  // Prefer known nesting when present; fall back to a deep search.
102
161
  const record = isRecord(connectedAccount) ? connectedAccount : null;
@@ -0,0 +1,21 @@
1
+ export type ResumeDocxExtractMode = "digest" | "full";
2
+ export declare function resumeDocxCheck(): Promise<{
3
+ ok: boolean;
4
+ python: string;
5
+ }>;
6
+ export declare function resumeDocxExtract(params: {
7
+ docxPath: string;
8
+ mode?: ResumeDocxExtractMode;
9
+ }): Promise<{
10
+ ok: boolean;
11
+ mode: string;
12
+ schema: any;
13
+ }>;
14
+ export declare function resumeDocxApply(params: {
15
+ inPath: string;
16
+ outPath: string;
17
+ patch: unknown;
18
+ }): Promise<{
19
+ ok: boolean;
20
+ outPath: string;
21
+ }>;