@agenticmail/enterprise 0.5.50 → 0.5.51
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/chunk-FKDN7ZV3.js +898 -0
- package/dist/chunk-G7BBCWAX.js +13428 -0
- package/dist/chunk-Q4WDMWLJ.js +2115 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +3 -3
- package/dist/runtime-ENGVD2AI.js +47 -0
- package/dist/server-JBOS22AY.js +12 -0
- package/dist/setup-6ATX2BNE.js +20 -0
- package/package.json +1 -14
- package/src/agent-tools/index.ts +22 -3
- package/src/agent-tools/tools/agenticmail.ts +785 -0
- package/src/agenticmail-core/index.ts +36 -0
- package/src/agenticmail-core/pending-followup.ts +362 -0
- package/src/agenticmail-core/telemetry.ts +164 -0
- package/src/agenticmail-core/tools.ts +2395 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenticMail Core — Self-contained copy for the enterprise package.
|
|
3
|
+
*
|
|
4
|
+
* Copied from @agenticmail/openclaw + @agenticmail/core so the enterprise
|
|
5
|
+
* package has zero runtime dependency on the main agenticmail packages.
|
|
6
|
+
*
|
|
7
|
+
* Source files:
|
|
8
|
+
* tools.ts ← packages/openclaw/src/tools.ts
|
|
9
|
+
* pending-followup.ts ← packages/openclaw/src/pending-followup.ts
|
|
10
|
+
* telemetry.ts ← packages/core/src/telemetry.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
registerTools,
|
|
15
|
+
registerAgentIdentity,
|
|
16
|
+
unregisterAgentIdentity,
|
|
17
|
+
setLastActivatedAgent,
|
|
18
|
+
clearLastActivatedAgent,
|
|
19
|
+
recordInboundAgentMessage,
|
|
20
|
+
type ToolContext,
|
|
21
|
+
} from './tools.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
scheduleFollowUp,
|
|
25
|
+
cancelFollowUp,
|
|
26
|
+
cancelAllFollowUps,
|
|
27
|
+
activeFollowUpCount,
|
|
28
|
+
getFollowUpSummary,
|
|
29
|
+
initFollowUpSystem,
|
|
30
|
+
} from './pending-followup.js';
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
recordToolCall,
|
|
34
|
+
setTelemetryVersion,
|
|
35
|
+
flushTelemetry,
|
|
36
|
+
} from './telemetry.js';
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending Email Follow-Up Scheduler
|
|
3
|
+
*
|
|
4
|
+
* When an outbound email is blocked by the security guard, this scheduler
|
|
5
|
+
* sets up automatic follow-up reminders on an escalating schedule:
|
|
6
|
+
*
|
|
7
|
+
* 12 hours → 6 hours → 3 hours → 1 hour (final) → 3-day cooldown → repeat
|
|
8
|
+
*
|
|
9
|
+
* Reminders are delivered via OpenClaw's system event mechanism, which injects
|
|
10
|
+
* them into the agent's next prompt.
|
|
11
|
+
*
|
|
12
|
+
* Follow-up state is persisted to disk so reminders survive process restarts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface FollowUpEntry {
|
|
21
|
+
pendingId: string;
|
|
22
|
+
recipient: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
/** 0-indexed step within the current cycle */
|
|
25
|
+
step: number;
|
|
26
|
+
/** How many full 4-step cycles have completed */
|
|
27
|
+
cycle: number;
|
|
28
|
+
/** ISO timestamp when the next reminder should fire */
|
|
29
|
+
nextFireAt: string;
|
|
30
|
+
/** ISO timestamp when this follow-up was first created */
|
|
31
|
+
createdAt: string;
|
|
32
|
+
/** Session key for system event delivery */
|
|
33
|
+
sessionKey: string;
|
|
34
|
+
/** API URL + key for status checks */
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PersistedState {
|
|
40
|
+
version: 1;
|
|
41
|
+
entries: FollowUpEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
// Escalating intervals within each cycle
|
|
47
|
+
const STEP_DELAYS_MS = [
|
|
48
|
+
12 * 3_600_000, // 0 → 12 hours
|
|
49
|
+
6 * 3_600_000, // 1 → 6 hours
|
|
50
|
+
3 * 3_600_000, // 2 → 3 hours
|
|
51
|
+
1 * 3_600_000, // 3 → 1 hour (final before cooldown)
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Cooldown after completing a full cycle
|
|
55
|
+
const COOLDOWN_MS = 3 * 24 * 3_600_000; // 3 days
|
|
56
|
+
|
|
57
|
+
// Heartbeat interval for checking if pending emails have been resolved
|
|
58
|
+
const HEARTBEAT_INTERVAL_MS = 5 * 60_000; // 5 minutes
|
|
59
|
+
|
|
60
|
+
// ── Module state (singleton) ─────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
let _api: any = null;
|
|
63
|
+
let _stateFilePath: string = '';
|
|
64
|
+
const tracked = new Map<string, FollowUpEntry>();
|
|
65
|
+
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
66
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
67
|
+
|
|
68
|
+
// ── Initialization ───────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Initialize the follow-up system with the OpenClaw plugin API.
|
|
72
|
+
* Must be called during plugin activation before any follow-ups are scheduled.
|
|
73
|
+
*/
|
|
74
|
+
export function initFollowUpSystem(api: any): void {
|
|
75
|
+
_api = api;
|
|
76
|
+
|
|
77
|
+
// Resolve state file path for persistence
|
|
78
|
+
try {
|
|
79
|
+
const stateDir = api?.runtime?.state?.resolveStateDir?.();
|
|
80
|
+
if (stateDir) {
|
|
81
|
+
_stateFilePath = join(stateDir, 'agenticmail-followups.json');
|
|
82
|
+
}
|
|
83
|
+
} catch { /* no persistence */ }
|
|
84
|
+
|
|
85
|
+
// Restore any persisted follow-ups from a previous process
|
|
86
|
+
restoreState();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Start follow-up reminders for a blocked pending email.
|
|
93
|
+
*
|
|
94
|
+
* @param pendingId UUID of the pending outbound email
|
|
95
|
+
* @param recipient Who the email was addressed to (for display)
|
|
96
|
+
* @param subject Original email subject (for display)
|
|
97
|
+
* @param sessionKey OpenClaw session key for system event delivery
|
|
98
|
+
* @param apiUrl AgenticMail API base URL (for status checks)
|
|
99
|
+
* @param apiKey Agent API key (for status checks)
|
|
100
|
+
*/
|
|
101
|
+
export function scheduleFollowUp(
|
|
102
|
+
pendingId: string,
|
|
103
|
+
recipient: string,
|
|
104
|
+
subject: string,
|
|
105
|
+
sessionKey: string,
|
|
106
|
+
apiUrl: string,
|
|
107
|
+
apiKey: string,
|
|
108
|
+
): void {
|
|
109
|
+
if (tracked.has(pendingId)) return;
|
|
110
|
+
|
|
111
|
+
const entry: FollowUpEntry = {
|
|
112
|
+
pendingId,
|
|
113
|
+
recipient,
|
|
114
|
+
subject,
|
|
115
|
+
step: 0,
|
|
116
|
+
cycle: 0,
|
|
117
|
+
nextFireAt: new Date(Date.now() + STEP_DELAYS_MS[0]).toISOString(),
|
|
118
|
+
createdAt: new Date().toISOString(),
|
|
119
|
+
sessionKey,
|
|
120
|
+
apiUrl,
|
|
121
|
+
apiKey,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
tracked.set(pendingId, entry);
|
|
125
|
+
armTimer(pendingId, entry);
|
|
126
|
+
startHeartbeat();
|
|
127
|
+
persistState();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Cancel follow-ups for a specific pending email (e.g. approved/rejected). */
|
|
131
|
+
export function cancelFollowUp(pendingId: string): void {
|
|
132
|
+
if (!tracked.has(pendingId)) return;
|
|
133
|
+
clearTimer(pendingId);
|
|
134
|
+
tracked.delete(pendingId);
|
|
135
|
+
persistState();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Cancel all follow-ups (e.g. on shutdown / agent_end). */
|
|
139
|
+
export function cancelAllFollowUps(): void {
|
|
140
|
+
for (const id of tracked.keys()) {
|
|
141
|
+
clearTimer(id);
|
|
142
|
+
}
|
|
143
|
+
tracked.clear();
|
|
144
|
+
timers.clear();
|
|
145
|
+
stopHeartbeat();
|
|
146
|
+
persistState();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Number of actively tracked pending emails. */
|
|
150
|
+
export function activeFollowUpCount(): number {
|
|
151
|
+
return tracked.size;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Get summary of all active follow-ups (for diagnostics). */
|
|
155
|
+
export function getFollowUpSummary(): Array<{
|
|
156
|
+
pendingId: string;
|
|
157
|
+
recipient: string;
|
|
158
|
+
subject: string;
|
|
159
|
+
step: number;
|
|
160
|
+
cycle: number;
|
|
161
|
+
nextFireAt: string;
|
|
162
|
+
}> {
|
|
163
|
+
return Array.from(tracked.values()).map(e => ({
|
|
164
|
+
pendingId: e.pendingId,
|
|
165
|
+
recipient: e.recipient,
|
|
166
|
+
subject: e.subject,
|
|
167
|
+
step: e.step,
|
|
168
|
+
cycle: e.cycle,
|
|
169
|
+
nextFireAt: e.nextFireAt,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Timer Management ─────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function armTimer(pendingId: string, entry: FollowUpEntry): void {
|
|
176
|
+
clearTimer(pendingId);
|
|
177
|
+
const delay = Math.max(0, new Date(entry.nextFireAt).getTime() - Date.now());
|
|
178
|
+
const timer = setTimeout(() => fire(pendingId), delay);
|
|
179
|
+
timer.unref();
|
|
180
|
+
timers.set(pendingId, timer);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function clearTimer(pendingId: string): void {
|
|
184
|
+
const timer = timers.get(pendingId);
|
|
185
|
+
if (timer) {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
timers.delete(pendingId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Reminder Delivery ────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
async function fire(pendingId: string): Promise<void> {
|
|
194
|
+
const entry = tracked.get(pendingId);
|
|
195
|
+
if (!entry) return;
|
|
196
|
+
|
|
197
|
+
// Check whether the email is still pending
|
|
198
|
+
const stillPending = await checkStillPending(entry);
|
|
199
|
+
if (!stillPending) {
|
|
200
|
+
clearTimer(pendingId);
|
|
201
|
+
tracked.delete(pendingId);
|
|
202
|
+
persistState();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { recipient, subject, step, cycle } = entry;
|
|
207
|
+
const isFinal = step === STEP_DELAYS_MS.length - 1;
|
|
208
|
+
const isPostCooldown = step >= STEP_DELAYS_MS.length;
|
|
209
|
+
|
|
210
|
+
// Build the reminder message
|
|
211
|
+
let message: string;
|
|
212
|
+
if (isPostCooldown) {
|
|
213
|
+
const totalDays = 3 * (cycle + 1);
|
|
214
|
+
message = [
|
|
215
|
+
`[FOLLOW-UP REMINDER — cycle ${cycle + 2}]`,
|
|
216
|
+
`Your blocked email to ${recipient} (subject: "${subject}") has been pending for over ${totalDays} days.`,
|
|
217
|
+
`Starting a new follow-up cycle. Please remind your owner that this email still needs their review.`,
|
|
218
|
+
`Pending ID: ${pendingId}`,
|
|
219
|
+
].join('\n');
|
|
220
|
+
} else if (isFinal) {
|
|
221
|
+
message = [
|
|
222
|
+
`[FINAL FOLLOW-UP]`,
|
|
223
|
+
`Your blocked email to ${recipient} (subject: "${subject}") is STILL pending approval.`,
|
|
224
|
+
`This is the last reminder before a 3-day cooldown. Please urgently remind your owner.`,
|
|
225
|
+
`Let them know you will not follow up again for 3 days unless they respond.`,
|
|
226
|
+
`Pending ID: ${pendingId}`,
|
|
227
|
+
].join('\n');
|
|
228
|
+
} else {
|
|
229
|
+
const nextDelayH = STEP_DELAYS_MS[step + 1] / 3_600_000;
|
|
230
|
+
message = [
|
|
231
|
+
`[FOLLOW-UP REMINDER ${step + 1}/${STEP_DELAYS_MS.length}]`,
|
|
232
|
+
`Your blocked email to ${recipient} (subject: "${subject}") is still pending owner approval.`,
|
|
233
|
+
`Please follow up with your owner — ask if they've reviewed the notification email.`,
|
|
234
|
+
`Next reminder in ${nextDelayH} hour${nextDelayH !== 1 ? 's' : ''}.`,
|
|
235
|
+
`Pending ID: ${pendingId}`,
|
|
236
|
+
].join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Deliver via OpenClaw system event (injected into agent's next prompt)
|
|
240
|
+
deliverReminder(message, entry.sessionKey);
|
|
241
|
+
|
|
242
|
+
// Schedule the next follow-up
|
|
243
|
+
const nextStep = isPostCooldown ? 0 : step + 1;
|
|
244
|
+
const nextCycle = isPostCooldown ? cycle + 1 : cycle;
|
|
245
|
+
const nextDelay = nextStep < STEP_DELAYS_MS.length ? STEP_DELAYS_MS[nextStep] : COOLDOWN_MS;
|
|
246
|
+
|
|
247
|
+
entry.step = nextStep;
|
|
248
|
+
entry.cycle = nextCycle;
|
|
249
|
+
entry.nextFireAt = new Date(Date.now() + nextDelay).toISOString();
|
|
250
|
+
|
|
251
|
+
armTimer(pendingId, entry);
|
|
252
|
+
persistState();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function deliverReminder(text: string, sessionKey: string): void {
|
|
256
|
+
try {
|
|
257
|
+
if (_api?.runtime?.system?.enqueueSystemEvent && sessionKey) {
|
|
258
|
+
_api.runtime.system.enqueueSystemEvent(text, { sessionKey });
|
|
259
|
+
} else {
|
|
260
|
+
console.warn('[agenticmail] Cannot deliver follow-up reminder: no system event API or session key');
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.warn(`[agenticmail] Follow-up delivery error: ${(err as Error).message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Status Checks ────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
async function checkStillPending(entry: FollowUpEntry): Promise<boolean> {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch(
|
|
272
|
+
`${entry.apiUrl}/api/agenticmail/mail/pending/${encodeURIComponent(entry.pendingId)}`,
|
|
273
|
+
{
|
|
274
|
+
headers: { 'Authorization': `Bearer ${entry.apiKey}` },
|
|
275
|
+
signal: AbortSignal.timeout(10_000),
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
if (!res.ok) return false;
|
|
279
|
+
const data: any = await res.json();
|
|
280
|
+
return data?.status === 'pending';
|
|
281
|
+
} catch {
|
|
282
|
+
return true; // assume still pending on error
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Heartbeat ────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function startHeartbeat(): void {
|
|
289
|
+
if (heartbeatTimer) return;
|
|
290
|
+
heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL_MS);
|
|
291
|
+
heartbeatTimer.unref();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stopHeartbeat(): void {
|
|
295
|
+
if (heartbeatTimer) {
|
|
296
|
+
clearInterval(heartbeatTimer);
|
|
297
|
+
heartbeatTimer = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function heartbeat(): Promise<void> {
|
|
302
|
+
if (tracked.size === 0) {
|
|
303
|
+
stopHeartbeat();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const [pendingId, entry] of tracked) {
|
|
308
|
+
try {
|
|
309
|
+
const stillPending = await checkStillPending(entry);
|
|
310
|
+
if (!stillPending) {
|
|
311
|
+
clearTimer(pendingId);
|
|
312
|
+
tracked.delete(pendingId);
|
|
313
|
+
persistState();
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// API unreachable — skip, will retry next heartbeat
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (tracked.size === 0) stopHeartbeat();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Persistence ──────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
function persistState(): void {
|
|
326
|
+
if (!_stateFilePath) return;
|
|
327
|
+
try {
|
|
328
|
+
const state: PersistedState = {
|
|
329
|
+
version: 1,
|
|
330
|
+
entries: Array.from(tracked.values()),
|
|
331
|
+
};
|
|
332
|
+
mkdirSync(dirname(_stateFilePath), { recursive: true });
|
|
333
|
+
writeFileSync(_stateFilePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn(`[agenticmail] Failed to persist follow-up state: ${(err as Error).message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function restoreState(): void {
|
|
340
|
+
if (!_stateFilePath) return;
|
|
341
|
+
try {
|
|
342
|
+
const raw = readFileSync(_stateFilePath, 'utf-8');
|
|
343
|
+
const state: PersistedState = JSON.parse(raw);
|
|
344
|
+
if (state.version !== 1 || !Array.isArray(state.entries)) return;
|
|
345
|
+
|
|
346
|
+
for (const entry of state.entries) {
|
|
347
|
+
// Skip entries whose fire time has long passed (> 1 day overdue)
|
|
348
|
+
const overdue = Date.now() - new Date(entry.nextFireAt).getTime();
|
|
349
|
+
if (overdue > 24 * 3_600_000) continue;
|
|
350
|
+
|
|
351
|
+
tracked.set(entry.pendingId, entry);
|
|
352
|
+
armTimer(entry.pendingId, entry);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (tracked.size > 0) {
|
|
356
|
+
startHeartbeat();
|
|
357
|
+
console.log(`[agenticmail] Restored ${tracked.size} follow-up reminder(s) from disk`);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
// No persisted state or invalid — start fresh
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenticMail Anonymous Telemetry
|
|
3
|
+
*
|
|
4
|
+
* Collects anonymous usage counts to help improve the product.
|
|
5
|
+
* NO personal data, API keys, emails, or content is ever collected.
|
|
6
|
+
*
|
|
7
|
+
* Opt out: set AGENTICMAIL_TELEMETRY=0 or DO_NOT_TRACK=1
|
|
8
|
+
*
|
|
9
|
+
* What we collect:
|
|
10
|
+
* - Tool call counts (which tools are popular)
|
|
11
|
+
* - Package version
|
|
12
|
+
* - Anonymous install ID (random UUID, no PII)
|
|
13
|
+
* - OS platform (e.g. "darwin", "linux")
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
import { platform } from 'process';
|
|
21
|
+
|
|
22
|
+
const TELEMETRY_ENDPOINT = 'https://agenticmail.io/api/telemetry';
|
|
23
|
+
const BATCH_INTERVAL_MS = 60_000; // flush every 60 seconds
|
|
24
|
+
const MAX_BATCH_SIZE = 100; // flush if batch gets this big
|
|
25
|
+
|
|
26
|
+
interface TelemetryEvent {
|
|
27
|
+
tool: string;
|
|
28
|
+
ts: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let installId: string | null = null;
|
|
32
|
+
let packageVersion: string = 'unknown';
|
|
33
|
+
let disabled: boolean | null = null;
|
|
34
|
+
let batch: TelemetryEvent[] = [];
|
|
35
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
let flushing = false;
|
|
37
|
+
|
|
38
|
+
/** Check if telemetry is disabled */
|
|
39
|
+
function isDisabled(): boolean {
|
|
40
|
+
if (disabled !== null) return disabled;
|
|
41
|
+
disabled = (
|
|
42
|
+
process.env.AGENTICMAIL_TELEMETRY === '0' ||
|
|
43
|
+
process.env.AGENTICMAIL_TELEMETRY === 'false' ||
|
|
44
|
+
process.env.DO_NOT_TRACK === '1' ||
|
|
45
|
+
process.env.DO_NOT_TRACK === 'true' ||
|
|
46
|
+
process.env.CI === 'true' // don't count CI runs
|
|
47
|
+
);
|
|
48
|
+
return disabled;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get or create the anonymous install ID */
|
|
52
|
+
function getInstallId(): string {
|
|
53
|
+
if (installId) return installId;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const dir = join(homedir(), '.agenticmail');
|
|
57
|
+
const idFile = join(dir, '.telemetry-id');
|
|
58
|
+
|
|
59
|
+
if (existsSync(idFile)) {
|
|
60
|
+
const id = readFileSync(idFile, 'utf8').trim();
|
|
61
|
+
if (id && id.length > 10) {
|
|
62
|
+
installId = id;
|
|
63
|
+
return installId;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Generate new ID
|
|
68
|
+
installId = randomUUID();
|
|
69
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
70
|
+
writeFileSync(idFile, installId, 'utf8');
|
|
71
|
+
return installId;
|
|
72
|
+
} catch {
|
|
73
|
+
// If we can't persist, use a session-only ID
|
|
74
|
+
installId = randomUUID();
|
|
75
|
+
return installId;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Set the package version for telemetry events */
|
|
80
|
+
export function setTelemetryVersion(version: string): void {
|
|
81
|
+
packageVersion = version;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Record a tool call (fire-and-forget, never throws) */
|
|
85
|
+
export function recordToolCall(toolName: string): void {
|
|
86
|
+
try {
|
|
87
|
+
if (isDisabled()) return;
|
|
88
|
+
|
|
89
|
+
batch.push({ tool: toolName, ts: Date.now() });
|
|
90
|
+
|
|
91
|
+
// Flush if batch is full
|
|
92
|
+
if (batch.length >= MAX_BATCH_SIZE) {
|
|
93
|
+
flush();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Schedule a flush if not already scheduled
|
|
98
|
+
if (!flushTimer) {
|
|
99
|
+
flushTimer = setTimeout(() => {
|
|
100
|
+
flushTimer = null;
|
|
101
|
+
flush();
|
|
102
|
+
}, BATCH_INTERVAL_MS);
|
|
103
|
+
// Don't keep the process alive just for telemetry
|
|
104
|
+
if (flushTimer && typeof flushTimer === 'object' && 'unref' in flushTimer) {
|
|
105
|
+
flushTimer.unref();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Never throw from telemetry
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Flush the current batch to the server */
|
|
114
|
+
function flush(): void {
|
|
115
|
+
if (flushing || batch.length === 0) return;
|
|
116
|
+
flushing = true;
|
|
117
|
+
|
|
118
|
+
const events = batch;
|
|
119
|
+
batch = [];
|
|
120
|
+
|
|
121
|
+
// Aggregate: count calls per tool
|
|
122
|
+
const toolCounts: Record<string, number> = {};
|
|
123
|
+
for (const e of events) {
|
|
124
|
+
toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const payload = {
|
|
128
|
+
id: getInstallId(),
|
|
129
|
+
v: packageVersion,
|
|
130
|
+
p: platform,
|
|
131
|
+
tools: toolCounts,
|
|
132
|
+
n: events.length, // total calls in this batch
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Fire and forget — never await, never throw
|
|
136
|
+
fetch(TELEMETRY_ENDPOINT, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify(payload),
|
|
140
|
+
signal: AbortSignal.timeout(5_000),
|
|
141
|
+
}).catch(() => {
|
|
142
|
+
// Silently ignore all errors
|
|
143
|
+
}).finally(() => {
|
|
144
|
+
flushing = false;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Flush remaining events on process exit */
|
|
149
|
+
export function flushTelemetry(): void {
|
|
150
|
+
if (flushTimer) {
|
|
151
|
+
clearTimeout(flushTimer);
|
|
152
|
+
flushTimer = null;
|
|
153
|
+
}
|
|
154
|
+
flush();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Flush on exit (best effort)
|
|
158
|
+
try {
|
|
159
|
+
process.on('beforeExit', flushTelemetry);
|
|
160
|
+
process.on('SIGINT', flushTelemetry);
|
|
161
|
+
process.on('SIGTERM', flushTelemetry);
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore — might not have process events in all environments
|
|
164
|
+
}
|