@agenticmail/enterprise 0.5.115 → 0.5.116
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/agenticmail-ZHSN5JCF.js +19 -0
- package/dist/chunk-EQH6ZJYN.js +1125 -0
- package/dist/cli-agent-LQVCYFE3.js +469 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/agenticmail/providers/google.ts +56 -4
- package/src/agenticmail/types.ts +2 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import "./chunk-KFQGP6VL.js";
|
|
2
|
+
|
|
3
|
+
// src/cli-agent.ts
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { serve } from "@hono/node-server";
|
|
6
|
+
async function runAgent(_args) {
|
|
7
|
+
const DATABASE_URL = process.env.DATABASE_URL;
|
|
8
|
+
const JWT_SECRET = process.env.JWT_SECRET;
|
|
9
|
+
const AGENT_ID = process.env.AGENTICMAIL_AGENT_ID;
|
|
10
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
11
|
+
if (!DATABASE_URL) {
|
|
12
|
+
console.error("ERROR: DATABASE_URL is required");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (!JWT_SECRET) {
|
|
16
|
+
console.error("ERROR: JWT_SECRET is required");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
if (!AGENT_ID) {
|
|
20
|
+
console.error("ERROR: AGENTICMAIL_AGENT_ID is required");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log("\u{1F916} AgenticMail Agent Runtime");
|
|
24
|
+
console.log(` Agent ID: ${AGENT_ID}`);
|
|
25
|
+
console.log(" Connecting to database...");
|
|
26
|
+
const { createAdapter } = await import("./factory-GT6SUZSQ.js");
|
|
27
|
+
const db = await createAdapter({
|
|
28
|
+
type: DATABASE_URL.startsWith("postgres") ? "postgres" : "sqlite",
|
|
29
|
+
connectionString: DATABASE_URL
|
|
30
|
+
});
|
|
31
|
+
await db.migrate();
|
|
32
|
+
const { EngineDatabase } = await import("./db-adapter-65QMSGCB.js");
|
|
33
|
+
const engineDbInterface = db.getEngineDB();
|
|
34
|
+
if (!engineDbInterface) {
|
|
35
|
+
console.error("ERROR: Database does not support engine queries");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const adapterDialect = db.getDialect();
|
|
39
|
+
const dialectMap = {
|
|
40
|
+
sqlite: "sqlite",
|
|
41
|
+
postgres: "postgres",
|
|
42
|
+
supabase: "postgres",
|
|
43
|
+
neon: "postgres",
|
|
44
|
+
cockroachdb: "postgres"
|
|
45
|
+
};
|
|
46
|
+
const engineDialect = dialectMap[adapterDialect] || adapterDialect;
|
|
47
|
+
const engineDb = new EngineDatabase(engineDbInterface, engineDialect);
|
|
48
|
+
await engineDb.migrate();
|
|
49
|
+
const agentRow = await engineDb.query(
|
|
50
|
+
`SELECT id, name, display_name, config, state FROM managed_agents WHERE id = $1`,
|
|
51
|
+
[AGENT_ID]
|
|
52
|
+
);
|
|
53
|
+
if (!agentRow || agentRow.length === 0) {
|
|
54
|
+
console.error(`ERROR: Agent ${AGENT_ID} not found in database`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const agent = agentRow[0];
|
|
58
|
+
console.log(` Agent: ${agent.display_name || agent.name}`);
|
|
59
|
+
console.log(` State: ${agent.state}`);
|
|
60
|
+
const { AgentLifecycleManager } = await import("./lifecycle-IFPWWGQ3.js");
|
|
61
|
+
const lifecycle = new AgentLifecycleManager({ db: engineDb });
|
|
62
|
+
await lifecycle.loadFromDb();
|
|
63
|
+
const managed = lifecycle.getAgent(AGENT_ID);
|
|
64
|
+
if (!managed) {
|
|
65
|
+
console.error(`ERROR: Could not load agent ${AGENT_ID} from lifecycle`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const config = managed.config;
|
|
69
|
+
console.log(` Model: ${config.model?.provider}/${config.model?.modelId}`);
|
|
70
|
+
let memoryManager;
|
|
71
|
+
try {
|
|
72
|
+
const { AgentMemoryManager } = await import("./agent-memory-G7YFTMDO.js");
|
|
73
|
+
memoryManager = new AgentMemoryManager(engineDb);
|
|
74
|
+
console.log(" Memory: DB-backed");
|
|
75
|
+
} catch {
|
|
76
|
+
console.log(" Memory: file-based fallback");
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const settings = await db.getSettings();
|
|
80
|
+
const keys = settings?.modelPricingConfig?.providerApiKeys;
|
|
81
|
+
if (keys && typeof keys === "object") {
|
|
82
|
+
const { PROVIDER_REGISTRY } = await import("./providers-DZDNNJTY.js");
|
|
83
|
+
for (const [providerId, apiKey] of Object.entries(keys)) {
|
|
84
|
+
const envVar = PROVIDER_REGISTRY[providerId]?.envKey;
|
|
85
|
+
if (envVar && apiKey && !process.env[envVar]) {
|
|
86
|
+
process.env[envVar] = apiKey;
|
|
87
|
+
console.log(` \u{1F511} Loaded API key for ${providerId}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
const { createAgentRuntime } = await import("./runtime-GB7V3PPC.js");
|
|
94
|
+
const getEmailConfig = (agentId) => {
|
|
95
|
+
const m = lifecycle.getAgent(agentId);
|
|
96
|
+
return m?.config?.emailConfig || null;
|
|
97
|
+
};
|
|
98
|
+
const onTokenRefresh = (agentId, tokens) => {
|
|
99
|
+
const m = lifecycle.getAgent(agentId);
|
|
100
|
+
if (m?.config?.emailConfig) {
|
|
101
|
+
if (tokens.accessToken) m.config.emailConfig.oauthAccessToken = tokens.accessToken;
|
|
102
|
+
if (tokens.refreshToken) m.config.emailConfig.oauthRefreshToken = tokens.refreshToken;
|
|
103
|
+
if (tokens.expiresAt) m.config.emailConfig.oauthTokenExpiry = tokens.expiresAt;
|
|
104
|
+
lifecycle.saveAgent(agentId).catch(() => {
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
let defaultModel;
|
|
109
|
+
const modelStr = process.env.AGENTICMAIL_MODEL || `${config.model?.provider}/${config.model?.modelId}`;
|
|
110
|
+
if (modelStr && modelStr.includes("/")) {
|
|
111
|
+
const [provider, ...rest] = modelStr.split("/");
|
|
112
|
+
defaultModel = {
|
|
113
|
+
provider,
|
|
114
|
+
modelId: rest.join("/"),
|
|
115
|
+
thinkingLevel: process.env.AGENTICMAIL_THINKING || config.model?.thinkingLevel
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const runtime = createAgentRuntime({
|
|
119
|
+
engineDb,
|
|
120
|
+
adminDb: db,
|
|
121
|
+
defaultModel,
|
|
122
|
+
apiKeys: {},
|
|
123
|
+
gatewayEnabled: true,
|
|
124
|
+
getEmailConfig,
|
|
125
|
+
onTokenRefresh,
|
|
126
|
+
agentMemoryManager: memoryManager,
|
|
127
|
+
resumeOnStartup: true
|
|
128
|
+
});
|
|
129
|
+
await runtime.start();
|
|
130
|
+
const runtimeApp = runtime.getApp();
|
|
131
|
+
const app = new Hono();
|
|
132
|
+
app.get("/health", (c) => c.json({
|
|
133
|
+
status: "ok",
|
|
134
|
+
agentId: AGENT_ID,
|
|
135
|
+
agentName: agent.display_name || agent.name,
|
|
136
|
+
uptime: process.uptime()
|
|
137
|
+
}));
|
|
138
|
+
app.get("/ready", (c) => c.json({ ready: true, agentId: AGENT_ID }));
|
|
139
|
+
if (runtimeApp) {
|
|
140
|
+
app.route("/api/runtime", runtimeApp);
|
|
141
|
+
}
|
|
142
|
+
serve({ fetch: app.fetch, port: PORT }, (info) => {
|
|
143
|
+
console.log(`
|
|
144
|
+
\u2705 Agent runtime started`);
|
|
145
|
+
console.log(` Health: http://localhost:${info.port}/health`);
|
|
146
|
+
console.log(` Runtime: http://localhost:${info.port}/api/runtime`);
|
|
147
|
+
console.log("");
|
|
148
|
+
});
|
|
149
|
+
const shutdown = () => {
|
|
150
|
+
console.log("\n\u23F3 Shutting down agent...");
|
|
151
|
+
runtime.stop().then(() => db.disconnect()).then(() => {
|
|
152
|
+
console.log("\u2705 Agent shutdown complete");
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
setTimeout(() => process.exit(1), 1e4).unref();
|
|
156
|
+
};
|
|
157
|
+
process.on("SIGINT", shutdown);
|
|
158
|
+
process.on("SIGTERM", shutdown);
|
|
159
|
+
try {
|
|
160
|
+
await engineDb.query(
|
|
161
|
+
`UPDATE managed_agents SET state = 'running', updated_at = $1 WHERE id = $2`,
|
|
162
|
+
[(/* @__PURE__ */ new Date()).toISOString(), AGENT_ID]
|
|
163
|
+
);
|
|
164
|
+
console.log(" State: running");
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
setTimeout(async () => {
|
|
168
|
+
try {
|
|
169
|
+
const orgRows = await engineDb.query(
|
|
170
|
+
`SELECT org_id FROM managed_agents WHERE id = $1`,
|
|
171
|
+
[AGENT_ID]
|
|
172
|
+
);
|
|
173
|
+
const orgId = orgRows?.[0]?.org_id;
|
|
174
|
+
if (!orgId) {
|
|
175
|
+
console.log("[onboarding] No org ID found, skipping");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const pendingRows = await engineDb.query(
|
|
179
|
+
`SELECT r.id, r.policy_id, p.name as policy_name, p.content as policy_content, p.priority
|
|
180
|
+
FROM onboarding_records r
|
|
181
|
+
JOIN org_policies p ON r.policy_id = p.id
|
|
182
|
+
WHERE r.agent_id = $1 AND r.status = 'pending'`,
|
|
183
|
+
[AGENT_ID]
|
|
184
|
+
);
|
|
185
|
+
if (!pendingRows || pendingRows.length === 0) {
|
|
186
|
+
console.log("[onboarding] Already complete or no records");
|
|
187
|
+
} else {
|
|
188
|
+
console.log(`[onboarding] ${pendingRows.length} pending policies \u2014 auto-acknowledging...`);
|
|
189
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
190
|
+
const policyNames = [];
|
|
191
|
+
for (const row of pendingRows) {
|
|
192
|
+
const policyName = row.policy_name || row.policy_id;
|
|
193
|
+
policyNames.push(policyName);
|
|
194
|
+
console.log(`[onboarding] Reading: ${policyName}`);
|
|
195
|
+
const { createHash } = await import("crypto");
|
|
196
|
+
const hash = createHash("sha256").update(row.policy_content || "").digest("hex").slice(0, 16);
|
|
197
|
+
await engineDb.query(
|
|
198
|
+
`UPDATE onboarding_records SET status = 'acknowledged', acknowledged_at = $1, verification_hash = $2, updated_at = $1 WHERE id = $3`,
|
|
199
|
+
[ts, hash, row.id]
|
|
200
|
+
);
|
|
201
|
+
console.log(`[onboarding] \u2705 Acknowledged: ${policyName}`);
|
|
202
|
+
if (memoryManager) {
|
|
203
|
+
try {
|
|
204
|
+
await memoryManager.storeMemory(AGENT_ID, {
|
|
205
|
+
content: `Organization policy "${policyName}" (${row.priority}): ${(row.policy_content || "").slice(0, 500)}`,
|
|
206
|
+
category: "org_knowledge",
|
|
207
|
+
importance: row.priority === "mandatory" ? "high" : "medium",
|
|
208
|
+
confidence: 1
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (memoryManager) {
|
|
215
|
+
try {
|
|
216
|
+
await memoryManager.storeMemory(AGENT_ID, {
|
|
217
|
+
content: `Completed onboarding: read and acknowledged ${policyNames.length} organization policies: ${policyNames.join(", ")}.`,
|
|
218
|
+
category: "org_knowledge",
|
|
219
|
+
importance: "high",
|
|
220
|
+
confidence: 1
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
console.log(`[onboarding] \u2705 Onboarding complete \u2014 ${policyNames.length} policies acknowledged`);
|
|
226
|
+
}
|
|
227
|
+
const managerEmail = config.managerEmail || (config.manager?.type === "external" ? config.manager.email : null);
|
|
228
|
+
const emailConfig = config.emailConfig;
|
|
229
|
+
if (managerEmail && emailConfig) {
|
|
230
|
+
console.log(`[welcome] Sending introduction email to ${managerEmail}...`);
|
|
231
|
+
try {
|
|
232
|
+
const { createEmailProvider } = await import("./agenticmail-ZHSN5JCF.js");
|
|
233
|
+
const providerType = emailConfig.provider || (emailConfig.oauthProvider === "google" ? "google" : emailConfig.oauthProvider === "microsoft" ? "microsoft" : "imap");
|
|
234
|
+
const emailProvider = createEmailProvider(providerType);
|
|
235
|
+
let currentAccessToken = emailConfig.oauthAccessToken;
|
|
236
|
+
const refreshTokenFn = emailConfig.oauthRefreshToken ? async () => {
|
|
237
|
+
const clientId = emailConfig.oauthClientId;
|
|
238
|
+
const clientSecret = emailConfig.oauthClientSecret;
|
|
239
|
+
const refreshToken = emailConfig.oauthRefreshToken;
|
|
240
|
+
const tokenUrl = providerType === "google" ? "https://oauth2.googleapis.com/token" : "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
|
241
|
+
const res = await fetch(tokenUrl, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
244
|
+
body: new URLSearchParams({
|
|
245
|
+
client_id: clientId,
|
|
246
|
+
client_secret: clientSecret,
|
|
247
|
+
refresh_token: refreshToken,
|
|
248
|
+
grant_type: "refresh_token"
|
|
249
|
+
})
|
|
250
|
+
});
|
|
251
|
+
const data = await res.json();
|
|
252
|
+
if (data.access_token) {
|
|
253
|
+
currentAccessToken = data.access_token;
|
|
254
|
+
emailConfig.oauthAccessToken = data.access_token;
|
|
255
|
+
if (data.expires_in) emailConfig.oauthTokenExpiry = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
256
|
+
lifecycle.saveAgent(AGENT_ID).catch(() => {
|
|
257
|
+
});
|
|
258
|
+
return data.access_token;
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Token refresh failed: ${JSON.stringify(data)}`);
|
|
261
|
+
} : void 0;
|
|
262
|
+
if (refreshTokenFn) {
|
|
263
|
+
try {
|
|
264
|
+
currentAccessToken = await refreshTokenFn();
|
|
265
|
+
console.log("[welcome] Refreshed OAuth token");
|
|
266
|
+
} catch (refreshErr) {
|
|
267
|
+
console.error(`[welcome] Token refresh failed: ${refreshErr.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await emailProvider.connect({
|
|
271
|
+
agentId: AGENT_ID,
|
|
272
|
+
name: config.displayName || config.name,
|
|
273
|
+
email: emailConfig.email || config.email?.address || "",
|
|
274
|
+
orgId,
|
|
275
|
+
accessToken: currentAccessToken,
|
|
276
|
+
refreshToken: refreshTokenFn,
|
|
277
|
+
provider: providerType
|
|
278
|
+
});
|
|
279
|
+
const agentName = config.displayName || config.name;
|
|
280
|
+
const role = config.identity?.role || "AI Agent";
|
|
281
|
+
const traits = config.identity?.traits || {};
|
|
282
|
+
const traitLines = Object.entries(traits).filter(([, v]) => v).map(([k, v]) => ` - ${k}: ${v}`).join("\n");
|
|
283
|
+
const policyCount = pendingRows?.length || 0;
|
|
284
|
+
const body = `Hello,
|
|
285
|
+
|
|
286
|
+
I'm ${agentName}, your new ${role}. I've just been deployed and have completed my onboarding \u2014 I've read and acknowledged all ${policyCount} organization policies.
|
|
287
|
+
|
|
288
|
+
About me:
|
|
289
|
+
- Role: ${role}
|
|
290
|
+
- Communication style: ${config.identity?.tone || "professional"}
|
|
291
|
+
- Language: ${config.identity?.language || "English"}
|
|
292
|
+
${traitLines ? `- Personality traits:
|
|
293
|
+
${traitLines}` : ""}
|
|
294
|
+
|
|
295
|
+
I'm ready to start working. Here's what I can help with:
|
|
296
|
+
- Responding to emails and communications
|
|
297
|
+
- Processing tasks and requests
|
|
298
|
+
- Following organization guidelines and escalation protocols
|
|
299
|
+
|
|
300
|
+
You can reach me by email at ${config.email?.address || emailConfig?.email || "my configured email address"}, or through the enterprise dashboard.
|
|
301
|
+
|
|
302
|
+
Looking forward to working with you!
|
|
303
|
+
|
|
304
|
+
Best regards,
|
|
305
|
+
${agentName}`;
|
|
306
|
+
await emailProvider.send({
|
|
307
|
+
to: managerEmail,
|
|
308
|
+
subject: `Hi! I'm ${agentName}, your new ${role}`,
|
|
309
|
+
text: body
|
|
310
|
+
});
|
|
311
|
+
console.log(`[welcome] \u2705 Welcome email sent to ${managerEmail}`);
|
|
312
|
+
if (memoryManager) {
|
|
313
|
+
try {
|
|
314
|
+
await memoryManager.storeMemory(AGENT_ID, {
|
|
315
|
+
content: `Sent welcome introduction email to manager at ${managerEmail}. Introduced myself as ${agentName}, ${role}.`,
|
|
316
|
+
category: "interaction_pattern",
|
|
317
|
+
importance: "medium",
|
|
318
|
+
confidence: 1
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(`[welcome] Failed to send welcome email: ${err.message}`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
if (!managerEmail) console.log("[welcome] No manager email configured, skipping welcome email");
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(`[onboarding] Error: ${err.message}`);
|
|
331
|
+
}
|
|
332
|
+
startEmailPolling(AGENT_ID, config, lifecycle, runtime, engineDb, memoryManager);
|
|
333
|
+
}, 3e3);
|
|
334
|
+
}
|
|
335
|
+
async function startEmailPolling(agentId, config, lifecycle, runtime, engineDb, memoryManager) {
|
|
336
|
+
const emailConfig = config.emailConfig;
|
|
337
|
+
if (!emailConfig) {
|
|
338
|
+
console.log("[email-poll] No email config, inbox polling disabled");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const providerType = emailConfig.provider || (emailConfig.oauthProvider === "google" ? "google" : emailConfig.oauthProvider === "microsoft" ? "microsoft" : "imap");
|
|
342
|
+
const refreshTokenFn = emailConfig.oauthRefreshToken ? async () => {
|
|
343
|
+
const tokenUrl = providerType === "google" ? "https://oauth2.googleapis.com/token" : "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
|
344
|
+
const res = await fetch(tokenUrl, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
347
|
+
body: new URLSearchParams({
|
|
348
|
+
client_id: emailConfig.oauthClientId,
|
|
349
|
+
client_secret: emailConfig.oauthClientSecret,
|
|
350
|
+
refresh_token: emailConfig.oauthRefreshToken,
|
|
351
|
+
grant_type: "refresh_token"
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
const data = await res.json();
|
|
355
|
+
if (data.access_token) {
|
|
356
|
+
emailConfig.oauthAccessToken = data.access_token;
|
|
357
|
+
if (data.expires_in) emailConfig.oauthTokenExpiry = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
358
|
+
lifecycle.saveAgent(agentId).catch(() => {
|
|
359
|
+
});
|
|
360
|
+
return data.access_token;
|
|
361
|
+
}
|
|
362
|
+
throw new Error(`Token refresh failed: ${JSON.stringify(data)}`);
|
|
363
|
+
} : void 0;
|
|
364
|
+
const { createEmailProvider } = await import("./agenticmail-ZHSN5JCF.js");
|
|
365
|
+
const emailProvider = createEmailProvider(providerType);
|
|
366
|
+
let accessToken = emailConfig.oauthAccessToken;
|
|
367
|
+
if (refreshTokenFn) {
|
|
368
|
+
try {
|
|
369
|
+
accessToken = await refreshTokenFn();
|
|
370
|
+
} catch (e) {
|
|
371
|
+
console.error(`[email-poll] Token refresh failed: ${e.message}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const orgRows = await engineDb.query(`SELECT org_id FROM managed_agents WHERE id = $1`, [agentId]);
|
|
375
|
+
const orgId = orgRows?.[0]?.org_id || "";
|
|
376
|
+
await emailProvider.connect({
|
|
377
|
+
agentId,
|
|
378
|
+
name: config.displayName || config.name,
|
|
379
|
+
email: emailConfig.email || config.email?.address || "",
|
|
380
|
+
orgId,
|
|
381
|
+
accessToken,
|
|
382
|
+
refreshToken: refreshTokenFn,
|
|
383
|
+
provider: providerType
|
|
384
|
+
});
|
|
385
|
+
console.log("[email-poll] \u2705 Email provider connected, starting inbox polling (every 30s)");
|
|
386
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
387
|
+
try {
|
|
388
|
+
const existing = await emailProvider.listMessages("INBOX", { limit: 50 });
|
|
389
|
+
for (const msg of existing) {
|
|
390
|
+
processedIds.add(msg.uid);
|
|
391
|
+
}
|
|
392
|
+
console.log(`[email-poll] Loaded ${processedIds.size} existing messages (will skip)`);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
console.error(`[email-poll] Failed to load existing messages: ${e.message}`);
|
|
395
|
+
}
|
|
396
|
+
const POLL_INTERVAL = 3e4;
|
|
397
|
+
const agentEmail = (emailConfig.email || config.email?.address || "").toLowerCase();
|
|
398
|
+
async function pollOnce() {
|
|
399
|
+
try {
|
|
400
|
+
const messages = await emailProvider.listMessages("INBOX", { limit: 20 });
|
|
401
|
+
const unread = messages.filter((m) => !m.flags?.includes("\\Seen") && !processedIds.has(m.uid));
|
|
402
|
+
for (const envelope of unread) {
|
|
403
|
+
processedIds.add(envelope.uid);
|
|
404
|
+
if (envelope.from?.email?.toLowerCase() === agentEmail) continue;
|
|
405
|
+
console.log(`[email-poll] New email from ${envelope.from?.email}: "${envelope.subject}"`);
|
|
406
|
+
const fullMsg = await emailProvider.readMessage(envelope.uid, "INBOX");
|
|
407
|
+
try {
|
|
408
|
+
await emailProvider.markRead(envelope.uid, "INBOX");
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
const emailText = [
|
|
412
|
+
`[Inbound Email]`,
|
|
413
|
+
`From: ${fullMsg.from?.name ? `${fullMsg.from.name} <${fullMsg.from.email}>` : fullMsg.from?.email}`,
|
|
414
|
+
`Subject: ${fullMsg.subject}`,
|
|
415
|
+
fullMsg.inReplyTo ? `In-Reply-To: ${fullMsg.inReplyTo}` : "",
|
|
416
|
+
"",
|
|
417
|
+
fullMsg.text || fullMsg.html || "(empty body)"
|
|
418
|
+
].filter(Boolean).join("\n");
|
|
419
|
+
try {
|
|
420
|
+
const agentName = config.displayName || config.name;
|
|
421
|
+
const role = config.identity?.role || "AI Agent";
|
|
422
|
+
const senderName = fullMsg.from?.name || fullMsg.from?.email || "someone";
|
|
423
|
+
const senderEmail = fullMsg.from?.email || "";
|
|
424
|
+
const emailSystemPrompt = `You are ${agentName}, a ${role}. You just received an email and need to respond to it.
|
|
425
|
+
|
|
426
|
+
Your email address: ${agentEmail}
|
|
427
|
+
Sender: ${senderName} <${senderEmail}>
|
|
428
|
+
Subject: ${fullMsg.subject}
|
|
429
|
+
|
|
430
|
+
IMPORTANT: After composing your response, you MUST reply using the email_reply tool or email_send tool. Do NOT just generate text \u2014 actually send the email reply.
|
|
431
|
+
If you have an email_reply tool, use it with the message UID. If not, use email_send to send to ${senderEmail} with subject "Re: ${fullMsg.subject}".
|
|
432
|
+
|
|
433
|
+
Be helpful, professional, and concise. Follow all organization policies you've been onboarded with.`;
|
|
434
|
+
const session = await runtime.spawnSession({
|
|
435
|
+
agentId,
|
|
436
|
+
message: emailText,
|
|
437
|
+
systemPrompt: emailSystemPrompt
|
|
438
|
+
});
|
|
439
|
+
console.log(`[email-poll] Session ${session.id} created for email from ${senderEmail}`);
|
|
440
|
+
} catch (sessErr) {
|
|
441
|
+
console.error(`[email-poll] Failed to create session: ${sessErr.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.error(`[email-poll] Poll error: ${err.message}`);
|
|
446
|
+
if (err.message.includes("401") && refreshTokenFn) {
|
|
447
|
+
try {
|
|
448
|
+
const newToken = await refreshTokenFn();
|
|
449
|
+
await emailProvider.connect({
|
|
450
|
+
agentId,
|
|
451
|
+
name: config.displayName || config.name,
|
|
452
|
+
email: emailConfig.email || config.email?.address || "",
|
|
453
|
+
orgId,
|
|
454
|
+
accessToken: newToken,
|
|
455
|
+
refreshToken: refreshTokenFn,
|
|
456
|
+
provider: providerType
|
|
457
|
+
});
|
|
458
|
+
console.log("[email-poll] Reconnected with fresh token");
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
setInterval(pollOnce, POLL_INTERVAL);
|
|
465
|
+
setTimeout(pollOnce, 5e3);
|
|
466
|
+
}
|
|
467
|
+
export {
|
|
468
|
+
runAgent
|
|
469
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ Skill Development:
|
|
|
50
50
|
import("./cli-serve-QZA3G4VA.js").then((m) => m.runServe(args.slice(1))).catch(fatal);
|
|
51
51
|
break;
|
|
52
52
|
case "agent":
|
|
53
|
-
import("./cli-agent-
|
|
53
|
+
import("./cli-agent-LQVCYFE3.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
|
|
54
54
|
break;
|
|
55
55
|
case "setup":
|
|
56
56
|
default:
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -133,15 +133,22 @@ export class GoogleEmailProvider implements IEmailProvider {
|
|
|
133
133
|
|
|
134
134
|
async send(options: SendEmailOptions): Promise<{ messageId: string }> {
|
|
135
135
|
const raw = this.buildRawEmail(options);
|
|
136
|
+
const sendBody: any = { raw };
|
|
137
|
+
// Include threadId to keep replies in the same Gmail thread
|
|
138
|
+
if (options.threadId) sendBody.threadId = options.threadId;
|
|
136
139
|
const data = await this.gmailFetch('/messages/send', {
|
|
137
140
|
method: 'POST',
|
|
138
|
-
body: JSON.stringify(
|
|
141
|
+
body: JSON.stringify(sendBody),
|
|
139
142
|
});
|
|
140
143
|
return { messageId: data.id };
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
async reply(uid: string, body: string, replyAll = false): Promise<{ messageId: string }> {
|
|
144
|
-
|
|
147
|
+
// Fetch original with threadId
|
|
148
|
+
const originalData = await this.gmailFetch(`/messages/${uid}?format=full`);
|
|
149
|
+
const original = this.fullToMessage(originalData);
|
|
150
|
+
const threadId = originalData.threadId;
|
|
151
|
+
|
|
145
152
|
const to = replyAll
|
|
146
153
|
? [original.from.email, ...(original.to || []).map(t => t.email), ...(original.cc || []).map(c => c.email)].filter(e => e !== this.identity?.email).join(', ')
|
|
147
154
|
: original.from.email;
|
|
@@ -152,6 +159,7 @@ export class GoogleEmailProvider implements IEmailProvider {
|
|
|
152
159
|
body,
|
|
153
160
|
inReplyTo: original.messageId,
|
|
154
161
|
references: original.references ? [...original.references, original.messageId!] : [original.messageId!],
|
|
162
|
+
threadId,
|
|
155
163
|
});
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -236,6 +244,47 @@ export class GoogleEmailProvider implements IEmailProvider {
|
|
|
236
244
|
await Promise.all(uids.map(uid => this.deleteMessage(uid)));
|
|
237
245
|
}
|
|
238
246
|
|
|
247
|
+
// ─── Gmail Push Notifications ────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set up Gmail push notifications via Google Cloud Pub/Sub.
|
|
251
|
+
* Gmail will POST to your Pub/Sub topic when new emails arrive.
|
|
252
|
+
* Requires: Pub/Sub topic created, Gmail API granted publish permission.
|
|
253
|
+
* Returns: historyId and expiration (watch lasts ~7 days, must renew).
|
|
254
|
+
*/
|
|
255
|
+
async watchInbox(topicName: string): Promise<{ historyId: string; expiration: string }> {
|
|
256
|
+
const data = await this.gmailFetch('/watch', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
topicName,
|
|
260
|
+
labelIds: ['INBOX'],
|
|
261
|
+
labelFilterBehavior: 'INCLUDE',
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
return { historyId: data.historyId, expiration: data.expiration };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Stop Gmail push notifications.
|
|
269
|
+
*/
|
|
270
|
+
async stopWatch(): Promise<void> {
|
|
271
|
+
await this.gmailFetch('/stop', { method: 'POST' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get message history since a historyId (for processing push notification deltas).
|
|
276
|
+
*/
|
|
277
|
+
async getHistory(startHistoryId: string): Promise<{ messages: Array<{ id: string; threadId: string }>; historyId: string }> {
|
|
278
|
+
const data = await this.gmailFetch(`/history?startHistoryId=${startHistoryId}&historyTypes=messageAdded&labelId=INBOX`);
|
|
279
|
+
const messages: Array<{ id: string; threadId: string }> = [];
|
|
280
|
+
for (const h of (data.history || [])) {
|
|
281
|
+
for (const added of (h.messagesAdded || [])) {
|
|
282
|
+
if (added.message?.id) messages.push({ id: added.message.id, threadId: added.message.threadId });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { messages, historyId: data.historyId || startHistoryId };
|
|
286
|
+
}
|
|
287
|
+
|
|
239
288
|
// ─── Helpers ────────────────────────────────────────
|
|
240
289
|
|
|
241
290
|
private resolveLabelId(folder: string): string {
|
|
@@ -252,12 +301,15 @@ export class GoogleEmailProvider implements IEmailProvider {
|
|
|
252
301
|
}
|
|
253
302
|
|
|
254
303
|
private buildRawEmail(options: SendEmailOptions): string {
|
|
304
|
+
const fromAddr = this.identity?.email;
|
|
305
|
+
const fromName = this.identity?.name;
|
|
255
306
|
const lines = [
|
|
307
|
+
fromAddr ? `From: ${fromName ? `${fromName} <${fromAddr}>` : fromAddr}` : '',
|
|
256
308
|
`To: ${options.to}`,
|
|
257
309
|
`Subject: ${options.subject}`,
|
|
258
310
|
`Content-Type: text/plain; charset=utf-8`,
|
|
259
|
-
];
|
|
260
|
-
if (options.cc) lines.splice(
|
|
311
|
+
].filter(Boolean);
|
|
312
|
+
if (options.cc) lines.splice(2, 0, `Cc: ${options.cc}`);
|
|
261
313
|
if (options.inReplyTo) lines.push(`In-Reply-To: ${options.inReplyTo}`);
|
|
262
314
|
if (options.references?.length) lines.push(`References: ${options.references.join(' ')}`);
|
|
263
315
|
lines.push('', options.body);
|
package/src/agenticmail/types.ts
CHANGED
|
@@ -81,6 +81,8 @@ export interface SendEmailOptions {
|
|
|
81
81
|
inReplyTo?: string;
|
|
82
82
|
references?: string[];
|
|
83
83
|
attachments?: { filename: string; content: string; contentType?: string; encoding?: string }[];
|
|
84
|
+
/** Gmail thread ID — keeps reply in the same thread */
|
|
85
|
+
threadId?: string;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
export interface SearchCriteria {
|