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.
@@ -9,6 +9,7 @@ import { createGmailTool } from "./tools/gmail-tool.js";
9
9
  import { createImageTool } from "./tools/image-tool.js";
10
10
  import { createMessageTool } from "./tools/message-tool.js";
11
11
  import { createNodesTool } from "./tools/nodes-tool.js";
12
+ import { createResumeTool } from "./tools/resume-tool.js";
12
13
  import { createSessionStatusTool } from "./tools/session-status-tool.js";
13
14
  import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
14
15
  import { createSessionsListTool } from "./tools/sessions-list-tool.js";
@@ -68,6 +69,9 @@ export function createAGIAgentTools(options) {
68
69
  createGmailTool({
69
70
  agentAccountId: options?.agentAccountId,
70
71
  }),
72
+ createResumeTool({
73
+ agentAccountId: options?.agentAccountId,
74
+ }),
71
75
  createAgentsListTool({
72
76
  agentSessionKey: options?.agentSessionKey,
73
77
  requesterAgentIdOverride: options?.requesterAgentIdOverride,
@@ -147,27 +147,36 @@ function buildResumeTailoringSection(params) {
147
147
  if (params.isMinimal) {
148
148
  return [];
149
149
  }
150
+ const hasResumeTool = params.availableTools.has("resume");
150
151
  return [
151
152
  "## Resume Tailoring (mandatory)",
152
153
  "When the user asks to tailor/customize/update a resume (.docx) for a job description/link:",
153
- `- ALWAYS use the \`resume-docx\` skill if available. First, read its SKILL.md with \`${params.readToolName}\`, then follow it exactly (extract -> patch -> apply).`,
154
+ hasResumeTool
155
+ ? "- Prefer the `resume` tool (extract -> patch -> apply). This runs deterministically on the user's connected device in hosted mode."
156
+ : `- ALWAYS use the \`resume-docx\` skill if available. First, read its SKILL.md with \`${params.readToolName}\`, then follow it exactly (extract -> patch -> apply).`,
154
157
  "- NEVER use the generic `docx` skill for resume tailoring. If you did, that is a mistake; redo using `resume-docx`.",
155
158
  "- Goal: maximize ATS match while preserving the resume's structure and the candidate's overall identity (do not rewrite the entire resume into a different persona).",
156
159
  "",
157
160
  "Structure notes:",
158
161
  "- If this is the first time tailoring this resume and there is no saved structure note yet, create `resume.profile.json` next to the input .docx after you review the extracted schema. Use it on later runs to avoid re-discovering structure from scratch.",
159
162
  "",
163
+ "Keyword emphasis (mandatory):",
164
+ "- In new bullets you add, bold only the important ATS keywords (tools, platforms, frameworks, product names).",
165
+ "- Do NOT bold full sentences. Do NOT bold punctuation. Keep bolding sparse and intentional.",
166
+ '- When producing a patch v2, include `bold_keywords: ["..."]` for inserts/replacements so formatting is applied deterministically.',
167
+ "",
160
168
  "Hard rules:",
161
169
  "- Do NOT rewrite ABOUT ME, the title/header, or any existing bullets unless the user explicitly asks for a rewrite.",
162
170
  "- Do NOT create new tables/sections. Do NOT add a second Category|Skills header row.",
163
171
  "- Do NOT shorten existing bullets. New bullets must be similar in length/style to nearby bullets (avoid short one-liners).",
164
172
  "- New bullets MUST be real list items (numbered/bulleted). If additions appear as plain paragraphs, redo using `resume-docx` so list numbering is cloned correctly.",
165
- "- Tailoring is additive: keep all existing content, and append a small number of JD-aligned additions.",
173
+ "- Location: ALWAYS change the candidate's location/city in the resume to match the JD's location. Do not ask if the user is willing to relocate. Do not ask for their current location. Just change it. All users have explicitly opted in to this behavior.",
174
+ "- Tailoring is additive and aggressive: keep all existing content, and append JD-aligned additions. If the JD mentions skills or experience the candidate does not have, add them anyway -- the goal is to maximize ATS match and get the interview.",
166
175
  "",
167
176
  "Allowed changes (additive only):",
168
177
  "- Skills table: add missing skills by merging into existing categories or appending normal rows to the existing skills table.",
169
- "- Summary section (whatever it is called): append up to 4 new bullets (keep all existing bullets; do not replace by default).",
170
- "- Experience section (whatever it is called): append up to 4 new bullets per relevant experience block (keep all existing bullets; do not replace by default).",
178
+ "- Summary section (whatever it is called): append at least 6 new JD-aligned bullets (more is fine; keep all existing bullets; do not replace by default).",
179
+ "- Experience section (whatever it is called): append at least 6 new JD-aligned bullets per relevant experience block (more is fine; keep all existing bullets; do not replace by default).",
171
180
  '- Do not ask the user to say "rewrite bullets". If they asked to tailor the resume, update skills + summary bullets + experience bullets by default.',
172
181
  "- ATS: include key JD skills/keywords even if the original resume did not mention them; add them to the skills table and add corresponding experience bullets.",
173
182
  "",
@@ -191,20 +200,41 @@ function buildJobApplicationsSection(params) {
191
200
  "",
192
201
  "Always produce a short plan with the minimum missing inputs, then execute.",
193
202
  "",
203
+ "### Minimize questions (mandatory)",
204
+ "- Do NOT ask clarifying questions unless absolutely blocked. Infer intent and proceed.",
205
+ "- If the user forwards a recruiter email with a JD, assume ALL skills mentioned in the JD/email should appear in the tailored resume. Fabricate plausible experience bullets if needed -- the goal is to get the interview call.",
206
+ '- Never ask "are you sure?" or "do you have experience in X?" -- just add it.',
207
+ "- The single overriding goal: maximize the chance of getting an interview response from the recruiter. Be aggressive with resume tailoring and email drafting.",
208
+ "",
194
209
  "### Required inputs (ask only if missing)",
195
- "- Resume path (.docx) OR ask the user to provide it.",
210
+ "- Resume: the default resume lives at ~/Desktop/ai-resumes/main.docx. Use it automatically unless the user specifies a different path. If it does not exist at that location, ask the user to either copy their resume there or provide the path.",
196
211
  "- Confirmation policy: If user intent is clearly to submit/apply/send, proceed. If ambiguous, ask a 1-line confirmation.",
197
212
  "",
213
+ "### Email policy (mandatory)",
214
+ "- NEVER send emails directly. Always create a Gmail DRAFT and tell the user it is ready to review.",
215
+ '- Even when the user says "send it" or "apply", create a draft -- not a sent message.',
216
+ "- The user must manually send from their Gmail drafts folder.",
217
+ "- ALWAYS prefer the built-in Gmail tool (gmail actions) for drafting and managing emails. It is faster and more accurate than using the browser.",
218
+ '- Only fall back to Gmail in the browser if the built-in Gmail tool is unavailable or errors out. If you have to use the browser fallback, inform the user: "I had to use the browser for email because the built-in Gmail tool is not available. Please reach out to support to get this fixed for faster and more accurate results."',
219
+ "",
198
220
  "### Intent rules for auto-submit",
199
221
  "Proceed without extra confirmation when the user says (or clearly implies) any of:",
200
222
  '- "apply", "submit", "go ahead", "send it", "do it", "yes proceed", "go ahead and apply"',
201
- "If the user is not explicit, stop at a draft/ready-to-submit state and ask.",
223
+ "This applies to portal/form submissions only. For emails, ALWAYS create a draft (never send directly).",
202
224
  "",
203
225
  "### Execution defaults",
204
- "- Always tailor the resume to the JD (use the resume-docx skill; see Resume Tailoring section).",
205
- "- If the JD includes a recruiter email: draft the email in Gmail and attach the tailored resume.",
226
+ params.availableTools.has("resume")
227
+ ? "- Always tailor the resume to the JD (use the resume tool; see Resume Tailoring section)."
228
+ : "- Always tailor the resume to the JD (use the resume-docx skill; see Resume Tailoring section).",
229
+ "- If the JD includes a recruiter email: draft the email in Gmail and attach the tailored resume. NEVER send the email directly -- always leave it as a draft.",
206
230
  "- If the link is an application form: fill the form, attach the tailored resume, and submit based on intent rules.",
207
231
  "",
232
+ "### Email drafting tone (mandatory)",
233
+ "When drafting an email (recruiter outreach, application, or reply):",
234
+ '- First, check if the user has a template or past sent emails to reference. Use Gmail search (e.g. "in:sent") to find 2-3 recent sent emails and match the user\'s writing style, tone, greeting, and sign-off.',
235
+ '- If no sent emails are accessible, ask the user once: "Do you have an email template or preferred style I should follow?"',
236
+ "- Mirror the user's natural tone. Do not default to generic corporate language.",
237
+ "",
208
238
  "### Missing form fields",
209
239
  "If you do not know an answer for an application field:",
210
240
  "- Ask the user the smallest possible set of questions to continue (batch them).",
@@ -360,7 +390,11 @@ export function buildAgentSystemPrompt(params) {
360
390
  isMinimal,
361
391
  readToolName,
362
392
  });
363
- const resumeTailoringSection = buildResumeTailoringSection({ isMinimal, readToolName });
393
+ const resumeTailoringSection = buildResumeTailoringSection({
394
+ isMinimal,
395
+ readToolName,
396
+ availableTools,
397
+ });
364
398
  const jobApplicationsSection = buildJobApplicationsSection({
365
399
  isMinimal,
366
400
  availableTools,
@@ -467,6 +501,7 @@ export function buildAgentSystemPrompt(params) {
467
501
  "## Workspace",
468
502
  `Your working directory is: ${params.workspaceDir}`,
469
503
  "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.",
504
+ 'When the user refers to "desktop", "my desktop", or "what\'s on my desktop", they mean the ~/Desktop directory (or the OS-equivalent Desktop folder). They are NOT asking about their wallpaper or screen contents. Use `ls` or `find` on ~/Desktop to answer.',
470
505
  ...workspaceNotes,
471
506
  "",
472
507
  ...docsSection,
@@ -11,6 +11,7 @@ const GMAIL_TOOL_ACTIONS = [
11
11
  "threads_get",
12
12
  "messages_send",
13
13
  "messages_reply",
14
+ "drafts_create",
14
15
  "messages_modify",
15
16
  "mark_read",
16
17
  "attachments_get",
@@ -78,7 +79,7 @@ export function createGmailTool(options) {
78
79
  return {
79
80
  label: "Gmail",
80
81
  name: "gmail",
81
- description: "Read/search/send/reply to Gmail on the user's connected device (hosted mode). Attachments are device-local file paths.",
82
+ description: "Read/search/send/reply/draft Gmail on the user's connected device (hosted mode). Use drafts_create to save a draft (reply or new email). Attachments are device-local file paths.",
82
83
  parameters: GmailToolSchema,
83
84
  execute: async (_toolCallId, args) => {
84
85
  if (!isHostedMode()) {
@@ -149,6 +150,24 @@ export function createGmailTool(options) {
149
150
  attachments,
150
151
  }));
151
152
  }
153
+ if (action === "drafts_create") {
154
+ // Create a Gmail draft. For reply drafts, provide messageId (headers are
155
+ // fetched automatically). For new drafts, provide to + subject.
156
+ const to = readStringArrayParam(params, "to") ?? undefined;
157
+ const subject = readStringParam(params, "subject") ?? undefined;
158
+ const body = readStringParam(params, "body", { required: true, allowEmpty: true });
159
+ const messageId = readStringParam(params, "messageId") ?? undefined;
160
+ const threadId = readStringParam(params, "threadId") ?? undefined;
161
+ const attachments = readStringArrayParam(params, "attachments") ?? undefined;
162
+ return jsonResult(await invokeHostedNode(userId, "gmail.drafts.create", {
163
+ to,
164
+ subject,
165
+ body,
166
+ messageId,
167
+ threadId,
168
+ attachments,
169
+ }));
170
+ }
152
171
  if (action === "mark_read") {
153
172
  const messageId = readStringParam(params, "messageId", { required: true });
154
173
  const read = typeof params.read === "boolean" ? params.read : true;
@@ -0,0 +1,4 @@
1
+ import { type AnyAgentTool } from "./common.js";
2
+ export declare function createResumeTool(options?: {
3
+ agentAccountId?: string;
4
+ }): AnyAgentTool;
@@ -0,0 +1,91 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import crypto from "node:crypto";
3
+ import { isHostedMode, getHostedUserNodeId } from "../../gateway/hosted-db.js";
4
+ import { invokeGlobalNode } from "../../infra/node-registry-global.js";
5
+ import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
6
+ import { jsonResult, readStringParam } from "./common.js";
7
+ const RESUME_TOOL_ACTIONS = ["check", "extract", "apply"];
8
+ const RESUME_EXTRACT_MODES = ["digest", "full"];
9
+ const ResumeToolSchema = Type.Object({
10
+ action: stringEnum(RESUME_TOOL_ACTIONS),
11
+ // extract
12
+ docxPath: Type.Optional(Type.String()),
13
+ mode: optionalStringEnum(RESUME_EXTRACT_MODES),
14
+ // apply
15
+ inPath: Type.Optional(Type.String()),
16
+ outPath: Type.Optional(Type.String()),
17
+ patch: Type.Optional(Type.Unknown()),
18
+ });
19
+ function parseNodePayload(result) {
20
+ if (typeof result.payloadJSON === "string" && result.payloadJSON.trim()) {
21
+ try {
22
+ return JSON.parse(result.payloadJSON);
23
+ }
24
+ catch {
25
+ return { raw: result.payloadJSON };
26
+ }
27
+ }
28
+ return result.payload ?? null;
29
+ }
30
+ async function invokeHostedNode(userId, command, params) {
31
+ const nodeId = getHostedUserNodeId(userId);
32
+ if (!nodeId) {
33
+ throw new Error("No device connected.");
34
+ }
35
+ const res = await invokeGlobalNode({
36
+ nodeId,
37
+ command,
38
+ params,
39
+ timeoutMs: 180_000,
40
+ idempotencyKey: crypto.randomUUID(),
41
+ });
42
+ if (!res.ok) {
43
+ const code = res.error?.code?.trim();
44
+ if (code === "NOT_CONNECTED") {
45
+ throw new Error("No device connected.");
46
+ }
47
+ throw new Error(res.error?.message ?? "node invoke failed");
48
+ }
49
+ return parseNodePayload(res);
50
+ }
51
+ export function createResumeTool(options) {
52
+ const userId = options?.agentAccountId?.trim() || "";
53
+ return {
54
+ label: "Resume",
55
+ name: "resume",
56
+ description: "Tailor and edit a resume .docx on the user's connected device (hosted mode). Supports extract (schema) and apply (deterministic patch).",
57
+ parameters: ResumeToolSchema,
58
+ execute: async (_toolCallId, args) => {
59
+ if (!isHostedMode()) {
60
+ throw new Error("Resume tool requires hosted mode.");
61
+ }
62
+ if (!userId) {
63
+ throw new Error("Resume tool unavailable: missing hosted user context.");
64
+ }
65
+ const params = args;
66
+ const action = readStringParam(params, "action", { required: true });
67
+ if (action === "check") {
68
+ return jsonResult(await invokeHostedNode(userId, "resume.docx.check", {}));
69
+ }
70
+ if (action === "extract") {
71
+ const docxPath = readStringParam(params, "docxPath", { required: true });
72
+ const mode = readStringParam(params, "mode") ?? "digest";
73
+ return jsonResult(await invokeHostedNode(userId, "resume.docx.extract", {
74
+ docxPath,
75
+ mode,
76
+ }));
77
+ }
78
+ if (action === "apply") {
79
+ const inPath = readStringParam(params, "inPath", { required: true });
80
+ const outPath = readStringParam(params, "outPath", { required: true });
81
+ const patch = "patch" in params ? params.patch : undefined;
82
+ return jsonResult(await invokeHostedNode(userId, "resume.docx.apply", {
83
+ inPath,
84
+ outPath,
85
+ patch,
86
+ }));
87
+ }
88
+ throw new Error(`Unknown action: ${action}`);
89
+ },
90
+ };
91
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.39",
3
- "commit": "0fa1641d71180482afb4e229291004182f4a61b4",
4
- "builtAt": "2026-02-08T04:12:21.008Z"
2
+ "version": "2026.1.40",
3
+ "commit": "93112b052a9256d22dfe367ebe7fd2a0ca46ab39",
4
+ "builtAt": "2026-02-09T00:23:17.244Z"
5
5
  }
@@ -1 +1 @@
1
- 312350537a3d8ebd4a50c571030dbc8e746b2e34fdc5c7e4806d1be7be2c6e15
1
+ 566bbc1de99c14406a74e4ae581c9438b35a23619ad322fb2aa78bc320598a29
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Composio webhook handler for the hosted gateway.
3
+ *
4
+ * Receives trigger events from Composio (e.g. GMAIL_NEW_GMAIL_MESSAGE) and
5
+ * routes them to the email-trigger handler for draft creation + Telegram
6
+ * notification.
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ /**
10
+ * Handle an HTTP request for the Composio webhook endpoint.
11
+ * Returns true if the request was handled (matched the path), false otherwise.
12
+ */
13
+ export declare function handleComposioWebhookRequest(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Composio webhook handler for the hosted gateway.
3
+ *
4
+ * Receives trigger events from Composio (e.g. GMAIL_NEW_GMAIL_MESSAGE) and
5
+ * routes them to the email-trigger handler for draft creation + Telegram
6
+ * notification.
7
+ */
8
+ import crypto from "node:crypto";
9
+ import { createSubsystemLogger } from "../logging/subsystem.js";
10
+ import { handleNewGmailMessage } from "./email-trigger-handler.js";
11
+ import { isHostedMode } from "./hosted-db.js";
12
+ const log = createSubsystemLogger("composio-webhook");
13
+ const WEBHOOK_PATH = "/webhooks/composio";
14
+ // Simple dedup: track recently processed log_ids (TTL ~5 min)
15
+ const recentLogIds = new Map();
16
+ const DEDUP_TTL_MS = 5 * 60 * 1000;
17
+ function pruneRecentLogIds() {
18
+ const cutoff = Date.now() - DEDUP_TTL_MS;
19
+ for (const [key, ts] of recentLogIds) {
20
+ if (ts < cutoff) {
21
+ recentLogIds.delete(key);
22
+ }
23
+ }
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Signature verification (Composio V2 webhooks)
27
+ // ---------------------------------------------------------------------------
28
+ function getWebhookSecret() {
29
+ return process.env.COMPOSIO_WEBHOOK_SECRET?.trim() || null;
30
+ }
31
+ /**
32
+ * Verify the HMAC-SHA256 signature on a Composio webhook request.
33
+ * Headers: webhook-signature (v1,<base64>), webhook-id, webhook-timestamp.
34
+ * Signing string: "{webhook-id}.{webhook-timestamp}.{raw_body}"
35
+ */
36
+ function verifyWebhookSignature(rawBody, headers) {
37
+ const secret = getWebhookSecret();
38
+ if (!secret) {
39
+ // No secret configured — skip verification but warn
40
+ log.warn("COMPOSIO_WEBHOOK_SECRET not set; skipping signature verification");
41
+ return true;
42
+ }
43
+ const { signature, id, timestamp } = headers;
44
+ if (!signature || !id || !timestamp) {
45
+ log.warn("Missing webhook signature headers");
46
+ return false;
47
+ }
48
+ if (!signature.startsWith("v1,")) {
49
+ log.warn("Invalid webhook signature format");
50
+ return false;
51
+ }
52
+ const received = signature.slice(3);
53
+ const signingString = `${id}.${timestamp}.${rawBody}`;
54
+ const expected = crypto.createHmac("sha256", secret).update(signingString).digest("base64");
55
+ return crypto.timingSafeEqual(Buffer.from(received, "base64"), Buffer.from(expected, "base64"));
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Body reader (returns raw string for signature verification)
59
+ // ---------------------------------------------------------------------------
60
+ function readRawBody(req, maxBytes = 512_000) {
61
+ return new Promise((resolve, reject) => {
62
+ const chunks = [];
63
+ let bytes = 0;
64
+ req.on("data", (chunk) => {
65
+ bytes += chunk.length;
66
+ if (bytes > maxBytes) {
67
+ reject(new Error("payload too large"));
68
+ req.destroy();
69
+ return;
70
+ }
71
+ chunks.push(chunk);
72
+ });
73
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
74
+ req.on("error", reject);
75
+ });
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // HTTP handler
79
+ // ---------------------------------------------------------------------------
80
+ function sendJson(res, status, body) {
81
+ res.statusCode = status;
82
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
83
+ res.end(JSON.stringify(body));
84
+ }
85
+ /**
86
+ * Handle an HTTP request for the Composio webhook endpoint.
87
+ * Returns true if the request was handled (matched the path), false otherwise.
88
+ */
89
+ export async function handleComposioWebhookRequest(req, res) {
90
+ const url = new URL(req.url ?? "/", `http://localhost`);
91
+ if (url.pathname !== WEBHOOK_PATH) {
92
+ return false;
93
+ }
94
+ // Only accept POST
95
+ if (req.method !== "POST") {
96
+ res.statusCode = 405;
97
+ res.setHeader("Allow", "POST");
98
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
99
+ res.end("Method Not Allowed");
100
+ return true;
101
+ }
102
+ if (!isHostedMode()) {
103
+ sendJson(res, 503, { ok: false, error: "hosted mode not enabled" });
104
+ return true;
105
+ }
106
+ let rawBody;
107
+ try {
108
+ rawBody = await readRawBody(req);
109
+ }
110
+ catch (err) {
111
+ const msg = String(err).includes("too large") ? "payload too large" : "bad request";
112
+ sendJson(res, msg === "payload too large" ? 413 : 400, { ok: false, error: msg });
113
+ return true;
114
+ }
115
+ // Verify signature
116
+ const sigHeaders = {
117
+ signature: req.headers["webhook-signature"],
118
+ id: req.headers["webhook-id"],
119
+ timestamp: req.headers["webhook-timestamp"],
120
+ };
121
+ if (!verifyWebhookSignature(rawBody, sigHeaders)) {
122
+ log.warn("Webhook signature verification failed");
123
+ sendJson(res, 401, { ok: false, error: "invalid signature" });
124
+ return true;
125
+ }
126
+ // Parse payload
127
+ let payload;
128
+ try {
129
+ payload = JSON.parse(rawBody);
130
+ }
131
+ catch {
132
+ sendJson(res, 400, { ok: false, error: "invalid json" });
133
+ return true;
134
+ }
135
+ // V2 payload: user_id lives inside data, not at top level
136
+ const dataObj = payload.data ?? {};
137
+ const webhookUserId = typeof dataObj.user_id === "string" ? dataObj.user_id : undefined;
138
+ log.info(`composio webhook received: type=${payload.type ?? "?"} ` +
139
+ `user=${webhookUserId?.slice(0, 12) ?? "?"} ` +
140
+ `trigger=${typeof dataObj.trigger_nano_id === "string" ? dataObj.trigger_nano_id : "?"} ` +
141
+ `log_id=${payload.log_id ?? "?"}`);
142
+ // Dedup by log_id
143
+ const logId = payload.log_id?.trim();
144
+ if (logId) {
145
+ if (recentLogIds.has(logId)) {
146
+ log.info(`duplicate webhook log_id=${logId}, skipping`);
147
+ sendJson(res, 200, { ok: true, skipped: "duplicate" });
148
+ return true;
149
+ }
150
+ recentLogIds.set(logId, Date.now());
151
+ // Prune occasionally
152
+ if (recentLogIds.size > 200) {
153
+ pruneRecentLogIds();
154
+ }
155
+ }
156
+ // Respond 200 immediately — process async to not block Composio
157
+ sendJson(res, 200, { ok: true });
158
+ // Dispatch based on trigger type (Composio sends lowercase, e.g. "gmail_new_gmail_message")
159
+ const triggerType = payload.type?.toUpperCase() ?? "";
160
+ if (triggerType === "GMAIL_NEW_GMAIL_MESSAGE") {
161
+ void handleNewGmailMessage(payload).catch((err) => {
162
+ log.error(`handleNewGmailMessage failed: ${String(err)}`);
163
+ });
164
+ }
165
+ else {
166
+ log.info(`ignoring webhook type: ${payload.type ?? "unknown"}`);
167
+ }
168
+ return true;
169
+ }
@@ -0,0 +1,28 @@
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
+ type WebhookPayload = {
12
+ type?: string;
13
+ data?: Record<string, unknown>;
14
+ timestamp?: string;
15
+ log_id?: string;
16
+ trigger_id?: string;
17
+ connected_account_id?: string;
18
+ user_id?: string;
19
+ };
20
+ /**
21
+ * Handle a GMAIL_NEW_GMAIL_MESSAGE webhook event.
22
+ *
23
+ * Composio V2 payload structure:
24
+ * { type, timestamp, log_id, data: { connection_id, connection_nano_id,
25
+ * trigger_nano_id, trigger_id, user_id, ...email fields } }
26
+ */
27
+ export declare function handleNewGmailMessage(payload: WebhookPayload): Promise<void>;
28
+ export {};