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.
- package/dist/agents/agiagent-tools.js +4 -0
- package/dist/agents/system-prompt.js +44 -9
- package/dist/agents/tools/gmail-tool.js +20 -1
- package/dist/agents/tools/resume-tool.d.ts +4 -0
- package/dist/agents/tools/resume-tool.js +91 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/gateway/composio-webhook.d.ts +13 -0
- package/dist/gateway/composio-webhook.js +169 -0
- package/dist/gateway/email-trigger-handler.d.ts +28 -0
- package/dist/gateway/email-trigger-handler.js +192 -0
- package/dist/gateway/hosted-db.d.ts +33 -0
- package/dist/gateway/hosted-db.js +97 -0
- package/dist/gateway/hosted-telegram.d.ts +15 -0
- package/dist/gateway/hosted-telegram.js +41 -0
- package/dist/gateway/server-http.js +5 -0
- package/dist/gateway/server-methods/composio.js +75 -1
- package/dist/infra/composio.d.ts +10 -0
- package/dist/infra/composio.js +59 -0
- package/dist/node-host/resume-docx.d.ts +21 -0
- package/dist/node-host/resume-docx.js +266 -0
- package/dist/node-host/runner.js +271 -54
- package/extensions/lobster/src/lobster-tool.test.ts +3 -3
- package/extensions/memory-lancedb/index.ts +13 -2
- package/package.json +3 -2
- package/skills/resume-docx/SKILL.md +214 -0
- package/skills/resume-docx/scripts/resume_docx_apply.py +1135 -0
- package/skills/resume-docx/scripts/resume_docx_extract.py +302 -0
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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",
|
package/dist/infra/composio.d.ts
CHANGED
|
@@ -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;
|
package/dist/infra/composio.js
CHANGED
|
@@ -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
|
+
}>;
|