@agenticmail/enterprise 0.5.83 → 0.5.85
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/browser-tool-KJMHSNK3.js +3997 -0
- package/dist/chunk-4DXQQPEC.js +15441 -0
- package/dist/chunk-AQH4DFYV.js +142 -0
- package/dist/chunk-CETB63LZ.js +15813 -0
- package/dist/chunk-LOA5LNXR.js +898 -0
- package/dist/chunk-PHZS6OIO.js +2191 -0
- package/dist/chunk-QISZRTVR.js +2154 -0
- package/dist/chunk-SEGTMIPI.js +2191 -0
- package/dist/chunk-TGOSFAYW.js +194 -0
- package/dist/chunk-ZTOVB5OQ.js +898 -0
- package/dist/cli.js +1 -1
- package/dist/environment-L6BN6KGK.js +11 -0
- package/dist/index.js +5 -3
- package/dist/pw-ai-IRORDXFW.js +2212 -0
- package/dist/routes-4NHH2DJW.js +6849 -0
- package/dist/routes-VIK7WDVW.js +6859 -0
- package/dist/runtime-DAVEFGHR.js +47 -0
- package/dist/runtime-NEO2ZB6O.js +49 -0
- package/dist/server-F6GBGLJY.js +12 -0
- package/dist/server-ZIB4HUUC.js +12 -0
- package/dist/setup-NKJBKCEG.js +20 -0
- package/dist/setup-YD3LJ6DT.js +20 -0
- package/package.json +1 -1
- package/scripts/vm-setup.sh +309 -0
- package/src/agent-tools/index.ts +70 -3
- package/src/agent-tools/tools/google/index.ts +4 -1
- package/src/agent-tools/tools/google/meetings.ts +468 -0
- package/src/agent-tools/tools/meeting-lifecycle.ts +437 -0
- package/src/engine/agent-routes.ts +19 -0
- package/src/runtime/agent-loop.ts +1 -1
- package/src/runtime/environment.ts +290 -0
- package/src/runtime/index.ts +3 -3
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting Lifecycle Tools
|
|
3
|
+
*
|
|
4
|
+
* Manages the full meeting lifecycle:
|
|
5
|
+
* - Pre-meeting: Create Drive folder, prep notes, check calendar
|
|
6
|
+
* - During: Join (if capable), take notes, record
|
|
7
|
+
* - Post-meeting: Upload recording, transcribe, extract action items, organize in Drive
|
|
8
|
+
*
|
|
9
|
+
* Works on ALL deployments:
|
|
10
|
+
* - Container (Fly.io): API-only — prep, notes, Drive organization, NO joining
|
|
11
|
+
* - VM: Full lifecycle including joining, recording, transcription
|
|
12
|
+
* - Local: Full lifecycle
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AnyAgentTool, ToolCreationOptions } from '../types.js';
|
|
16
|
+
import { jsonResult, errorResult } from '../common.js';
|
|
17
|
+
import type { TokenProvider } from './oauth-token-provider.js';
|
|
18
|
+
import { detectCapabilities, getCapabilitySummary, type SystemCapabilities } from '../../runtime/environment.js';
|
|
19
|
+
|
|
20
|
+
const CALENDAR_BASE = 'https://www.googleapis.com/calendar/v3';
|
|
21
|
+
const DRIVE_BASE = 'https://www.googleapis.com/drive/v3';
|
|
22
|
+
const DRIVE_UPLOAD = 'https://www.googleapis.com/upload/drive/v3';
|
|
23
|
+
|
|
24
|
+
async function api(token: string, base: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string> }): Promise<any> {
|
|
25
|
+
const url = new URL(base + path);
|
|
26
|
+
if (opts?.query) for (const [k, v] of Object.entries(opts.query)) { if (v) url.searchParams.set(k, v); }
|
|
27
|
+
const res = await fetch(url.toString(), {
|
|
28
|
+
method: opts?.method || 'GET',
|
|
29
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
33
|
+
if (res.status === 204) return {};
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function driveUploadText(token: string, name: string, content: string, parentId: string, mimeType = 'text/plain'): Promise<any> {
|
|
38
|
+
const boundary = '===agenticmail===';
|
|
39
|
+
const metadata = JSON.stringify({ name, parents: [parentId], mimeType });
|
|
40
|
+
const body = `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n--${boundary}\r\nContent-Type: ${mimeType}; charset=UTF-8\r\n\r\n${content}\r\n--${boundary}--`;
|
|
41
|
+
const res = await fetch(`${DRIVE_UPLOAD}/files?uploadType=multipart&fields=id,name,webViewLink`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${token}`,
|
|
45
|
+
'Content-Type': `multipart/related; boundary=${boundary}`,
|
|
46
|
+
},
|
|
47
|
+
body,
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) throw new Error(`Drive upload failed: ${res.status}`);
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureDriveFolder(token: string, name: string, parentId?: string): Promise<{ id: string; name: string }> {
|
|
54
|
+
// Check if folder exists
|
|
55
|
+
const parts = [`name = '${name.replace(/'/g, "\\'")}'`, "mimeType = 'application/vnd.google-apps.folder'", 'trashed = false'];
|
|
56
|
+
if (parentId) parts.push(`'${parentId}' in parents`);
|
|
57
|
+
const existing = await api(token, DRIVE_BASE, '/files', { query: { q: parts.join(' and '), fields: 'files(id,name)', pageSize: '1' } });
|
|
58
|
+
if (existing.files?.length) return existing.files[0];
|
|
59
|
+
|
|
60
|
+
// Create
|
|
61
|
+
const body: any = { name, mimeType: 'application/vnd.google-apps.folder' };
|
|
62
|
+
if (parentId) body.parents = [parentId];
|
|
63
|
+
return api(token, DRIVE_BASE, '/files', { method: 'POST', body, query: { fields: 'id,name' } });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MeetingLifecycleConfig {
|
|
67
|
+
tokenProvider: TokenProvider;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createMeetingLifecycleTools(config: MeetingLifecycleConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
|
|
71
|
+
const tp = config.tokenProvider;
|
|
72
|
+
let caps: SystemCapabilities | null = null;
|
|
73
|
+
function getCaps() { if (!caps) caps = detectCapabilities(); return caps; }
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
// ─── System Capabilities Check ─────────────────────
|
|
77
|
+
{
|
|
78
|
+
name: 'system_capabilities',
|
|
79
|
+
description: 'Check what this deployment can do. Shows browser, display, audio, video meeting, and recording capabilities. Use this first to understand what tools are available on this system.',
|
|
80
|
+
category: 'utility' as const,
|
|
81
|
+
parameters: { type: 'object' as const, properties: {}, required: [] },
|
|
82
|
+
async execute() {
|
|
83
|
+
const c = getCaps();
|
|
84
|
+
const summary = getCapabilitySummary(c);
|
|
85
|
+
return jsonResult({
|
|
86
|
+
...summary,
|
|
87
|
+
raw: {
|
|
88
|
+
deployment: c.deployment,
|
|
89
|
+
hasBrowser: c.hasBrowser,
|
|
90
|
+
browserPath: c.browserPath,
|
|
91
|
+
hasDisplay: c.hasDisplay,
|
|
92
|
+
hasAudio: c.hasAudio,
|
|
93
|
+
hasVirtualCamera: c.hasVirtualCamera,
|
|
94
|
+
canRunHeadedBrowser: c.canRunHeadedBrowser,
|
|
95
|
+
canJoinMeetings: c.canJoinMeetings,
|
|
96
|
+
canRecordMeetings: c.canRecordMeetings,
|
|
97
|
+
hasFfmpeg: c.hasFfmpeg,
|
|
98
|
+
hasPersistentDisk: c.hasPersistentDisk,
|
|
99
|
+
platform: c.platform,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// ─── Prepare Meeting (works everywhere) ────────────
|
|
106
|
+
{
|
|
107
|
+
name: 'meeting_prepare',
|
|
108
|
+
description: 'Prepare for a meeting: create a Google Drive folder structure, generate meeting notes template with attendees and agenda, and return everything needed. Works on ALL deployments (container + VM).',
|
|
109
|
+
category: 'utility' as const,
|
|
110
|
+
parameters: {
|
|
111
|
+
type: 'object' as const,
|
|
112
|
+
properties: {
|
|
113
|
+
eventId: { type: 'string', description: 'Google Calendar event ID (will auto-fetch details)' },
|
|
114
|
+
title: { type: 'string', description: 'Meeting title (if no eventId)' },
|
|
115
|
+
date: { type: 'string', description: 'Meeting date ISO string (if no eventId)' },
|
|
116
|
+
attendees: { type: 'string', description: 'Comma-separated attendee emails (if no eventId)' },
|
|
117
|
+
agenda: { type: 'string', description: 'Meeting agenda text' },
|
|
118
|
+
driveRootFolderId: { type: 'string', description: 'Root "Meetings" folder ID in Drive (will create if not provided)' },
|
|
119
|
+
},
|
|
120
|
+
required: [],
|
|
121
|
+
},
|
|
122
|
+
async execute(_id: string, params: any) {
|
|
123
|
+
try {
|
|
124
|
+
const token = await tp.getAccessToken();
|
|
125
|
+
|
|
126
|
+
// Fetch event details from calendar if eventId provided
|
|
127
|
+
let title = params.title || 'Meeting';
|
|
128
|
+
let date = params.date || new Date().toISOString();
|
|
129
|
+
let attendees: Array<{ email: string; name?: string; status?: string }> = [];
|
|
130
|
+
let organizer = '';
|
|
131
|
+
let meetingLink: string | null = null;
|
|
132
|
+
let platform = '';
|
|
133
|
+
let description = '';
|
|
134
|
+
|
|
135
|
+
if (params.eventId) {
|
|
136
|
+
const event = await api(token, CALENDAR_BASE, `/calendars/primary/events/${params.eventId}`);
|
|
137
|
+
title = event.summary || title;
|
|
138
|
+
date = event.start?.dateTime || event.start?.date || date;
|
|
139
|
+
description = event.description || '';
|
|
140
|
+
organizer = event.organizer?.email || '';
|
|
141
|
+
attendees = (event.attendees || []).map((a: any) => ({
|
|
142
|
+
email: a.email, name: a.displayName, status: a.responseStatus,
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
// Extract meeting link
|
|
146
|
+
if (event.conferenceData?.entryPoints) {
|
|
147
|
+
for (const ep of event.conferenceData.entryPoints) {
|
|
148
|
+
if (ep.entryPointType === 'video' && ep.uri) {
|
|
149
|
+
meetingLink = ep.uri;
|
|
150
|
+
platform = 'google_meet';
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!meetingLink && event.hangoutLink) {
|
|
156
|
+
meetingLink = event.hangoutLink;
|
|
157
|
+
platform = 'google_meet';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (params.attendees && !attendees.length) {
|
|
162
|
+
attendees = params.attendees.split(',').map((e: string) => ({ email: e.trim() }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Build folder structure: Meetings / YYYY / MM-Month / YYYY-MM-DD - Title - Attendees
|
|
166
|
+
const d = new Date(date);
|
|
167
|
+
const year = String(d.getFullYear());
|
|
168
|
+
const monthNum = String(d.getMonth() + 1).padStart(2, '0');
|
|
169
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
170
|
+
const monthName = `${monthNum}-${months[d.getMonth()]}`;
|
|
171
|
+
const dateStr = `${year}-${monthNum}-${String(d.getDate()).padStart(2, '0')}`;
|
|
172
|
+
const attendeeNames = attendees.slice(0, 3).map(a => a.name || a.email.split('@')[0]).join(', ');
|
|
173
|
+
const folderName = `${dateStr} - ${title}${attendeeNames ? ` - ${attendeeNames}` : ''}`;
|
|
174
|
+
|
|
175
|
+
// Create folder hierarchy
|
|
176
|
+
const rootFolder = params.driveRootFolderId
|
|
177
|
+
? { id: params.driveRootFolderId, name: 'Meetings' }
|
|
178
|
+
: await ensureDriveFolder(token, 'Meetings');
|
|
179
|
+
const yearFolder = await ensureDriveFolder(token, year, rootFolder.id);
|
|
180
|
+
const monthFolder = await ensureDriveFolder(token, monthName, yearFolder.id);
|
|
181
|
+
const meetingFolder = await ensureDriveFolder(token, folderName, monthFolder.id);
|
|
182
|
+
|
|
183
|
+
// Generate meeting notes template
|
|
184
|
+
const notesContent = [
|
|
185
|
+
`# ${title}`,
|
|
186
|
+
'',
|
|
187
|
+
`**Date:** ${d.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} at ${d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}`,
|
|
188
|
+
organizer ? `**Organizer:** ${organizer}` : '',
|
|
189
|
+
meetingLink ? `**Meeting Link:** ${meetingLink} (${platform})` : '',
|
|
190
|
+
'',
|
|
191
|
+
'## Attendees',
|
|
192
|
+
...attendees.map(a => `- ${a.name || a.email} (${a.email})${a.status ? ` — ${a.status}` : ''}`),
|
|
193
|
+
'',
|
|
194
|
+
'## Agenda',
|
|
195
|
+
params.agenda || description || '_No agenda provided_',
|
|
196
|
+
'',
|
|
197
|
+
'## Notes',
|
|
198
|
+
'_Meeting notes will be added here..._',
|
|
199
|
+
'',
|
|
200
|
+
'## Action Items',
|
|
201
|
+
'- [ ] _Action items from the meeting..._',
|
|
202
|
+
'',
|
|
203
|
+
'## Decisions Made',
|
|
204
|
+
'- _Key decisions..._',
|
|
205
|
+
'',
|
|
206
|
+
'---',
|
|
207
|
+
`_Prepared by AI Agent on ${new Date().toISOString()}_`,
|
|
208
|
+
].filter(Boolean).join('\n');
|
|
209
|
+
|
|
210
|
+
// Upload meeting notes template
|
|
211
|
+
const notesFile = await driveUploadText(token, 'meeting-notes.md', notesContent, meetingFolder.id);
|
|
212
|
+
|
|
213
|
+
// Check system capabilities for meeting joining
|
|
214
|
+
const c = getCaps();
|
|
215
|
+
const canJoin = c.canJoinMeetings;
|
|
216
|
+
|
|
217
|
+
return jsonResult({
|
|
218
|
+
prepared: true,
|
|
219
|
+
folder: {
|
|
220
|
+
id: meetingFolder.id,
|
|
221
|
+
path: `Meetings/${year}/${monthName}/${folderName}`,
|
|
222
|
+
hierarchy: {
|
|
223
|
+
root: rootFolder.id,
|
|
224
|
+
year: yearFolder.id,
|
|
225
|
+
month: monthFolder.id,
|
|
226
|
+
meeting: meetingFolder.id,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
notesFile: { id: notesFile.id, name: notesFile.name, webViewLink: notesFile.webViewLink },
|
|
230
|
+
meeting: {
|
|
231
|
+
title, date, organizer, attendees,
|
|
232
|
+
meetingLink, platform,
|
|
233
|
+
},
|
|
234
|
+
capabilities: {
|
|
235
|
+
canJoinMeeting: canJoin,
|
|
236
|
+
canRecord: c.canRecordMeetings,
|
|
237
|
+
deployment: c.deployment,
|
|
238
|
+
...(!canJoin ? {
|
|
239
|
+
limitation: 'This deployment cannot join video meetings. Meeting prep, notes, and post-meeting organization are fully available. To enable meeting joining, deploy on a VM with display + audio.',
|
|
240
|
+
} : {}),
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// ─── Post-Meeting: Save & Organize ─────────────────
|
|
248
|
+
{
|
|
249
|
+
name: 'meeting_save',
|
|
250
|
+
description: 'Save meeting artifacts to the meeting\'s Google Drive folder. Upload notes, transcript, recording, action items, and any shared files. Organizes everything neatly. Works on ALL deployments.',
|
|
251
|
+
category: 'utility' as const,
|
|
252
|
+
parameters: {
|
|
253
|
+
type: 'object' as const,
|
|
254
|
+
properties: {
|
|
255
|
+
folderId: { type: 'string', description: 'Meeting folder ID in Google Drive (from meeting_prepare)' },
|
|
256
|
+
notes: { type: 'string', description: 'Meeting notes content (markdown)' },
|
|
257
|
+
transcript: { type: 'string', description: 'Meeting transcript text' },
|
|
258
|
+
actionItems: { type: 'string', description: 'Action items as markdown list' },
|
|
259
|
+
summary: { type: 'string', description: 'Meeting summary text' },
|
|
260
|
+
decisions: { type: 'string', description: 'Key decisions made' },
|
|
261
|
+
followUps: { type: 'string', description: 'Follow-up tasks/meetings needed' },
|
|
262
|
+
recordingPath: { type: 'string', description: 'Local file path to recording (VM/local only)' },
|
|
263
|
+
},
|
|
264
|
+
required: ['folderId'],
|
|
265
|
+
},
|
|
266
|
+
async execute(_id: string, params: any) {
|
|
267
|
+
try {
|
|
268
|
+
const token = await tp.getAccessToken();
|
|
269
|
+
const folderId = params.folderId;
|
|
270
|
+
const uploaded: any[] = [];
|
|
271
|
+
|
|
272
|
+
if (params.notes) {
|
|
273
|
+
const f = await driveUploadText(token, 'meeting-notes.md', params.notes, folderId);
|
|
274
|
+
uploaded.push({ type: 'notes', id: f.id, name: f.name, webViewLink: f.webViewLink });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (params.transcript) {
|
|
278
|
+
const f = await driveUploadText(token, 'transcript.txt', params.transcript, folderId);
|
|
279
|
+
uploaded.push({ type: 'transcript', id: f.id, name: f.name, webViewLink: f.webViewLink });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (params.actionItems) {
|
|
283
|
+
const content = `# Action Items\n\n${params.actionItems}\n\n---\n_Extracted by AI Agent on ${new Date().toISOString()}_`;
|
|
284
|
+
const f = await driveUploadText(token, 'action-items.md', content, folderId);
|
|
285
|
+
uploaded.push({ type: 'action_items', id: f.id, name: f.name, webViewLink: f.webViewLink });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (params.summary) {
|
|
289
|
+
const content = `# Meeting Summary\n\n${params.summary}${params.decisions ? `\n\n## Decisions\n${params.decisions}` : ''}${params.followUps ? `\n\n## Follow-ups\n${params.followUps}` : ''}\n\n---\n_Generated by AI Agent on ${new Date().toISOString()}_`;
|
|
290
|
+
const f = await driveUploadText(token, 'summary.md', content, folderId);
|
|
291
|
+
uploaded.push({ type: 'summary', id: f.id, name: f.name, webViewLink: f.webViewLink });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (params.recordingPath) {
|
|
295
|
+
const c = getCaps();
|
|
296
|
+
if (!c.hasPersistentDisk) {
|
|
297
|
+
uploaded.push({ type: 'recording', error: 'Recording upload skipped — ephemeral filesystem. Recording may have been lost.' });
|
|
298
|
+
} else {
|
|
299
|
+
// For large file upload, we'd use resumable upload. For now, note the path.
|
|
300
|
+
uploaded.push({
|
|
301
|
+
type: 'recording', status: 'pending_upload',
|
|
302
|
+
localPath: params.recordingPath,
|
|
303
|
+
hint: 'Use google_drive_upload_file tool for large file upload, or the agent can use the browser to upload via drive.google.com',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return jsonResult({ saved: true, folderId, files: uploaded, count: uploaded.length });
|
|
309
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// ─── Meeting Record (VM only) ──────────────────────
|
|
314
|
+
{
|
|
315
|
+
name: 'meeting_record',
|
|
316
|
+
description: 'Start or stop recording the current meeting. Captures screen + audio using ffmpeg. REQUIRES: VM deployment with display + audio + ffmpeg. Not available on container deployments.',
|
|
317
|
+
category: 'utility' as const,
|
|
318
|
+
parameters: {
|
|
319
|
+
type: 'object' as const,
|
|
320
|
+
properties: {
|
|
321
|
+
action: { type: 'string', description: '"start" or "stop"' },
|
|
322
|
+
outputPath: { type: 'string', description: 'Output file path (default: /tmp/meeting-recording-{timestamp}.mp4)' },
|
|
323
|
+
display: { type: 'string', description: 'X11 display (default: :99)' },
|
|
324
|
+
audioSource: { type: 'string', description: 'PulseAudio source (default: auto-detect virtual monitor)' },
|
|
325
|
+
},
|
|
326
|
+
required: ['action'],
|
|
327
|
+
},
|
|
328
|
+
async execute(_id: string, params: any) {
|
|
329
|
+
const c = getCaps();
|
|
330
|
+
if (!c.canRecordMeetings) {
|
|
331
|
+
const summary = getCapabilitySummary(c);
|
|
332
|
+
return errorResult(
|
|
333
|
+
`Meeting recording is not available on this ${summary.deployment} deployment.\n` +
|
|
334
|
+
`Missing: ${summary.unavailable.join(', ')}\n\n` +
|
|
335
|
+
`${summary.recommendations.join('\n')}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (params.action === 'start') {
|
|
340
|
+
const output = params.outputPath || `/tmp/meeting-recording-${Date.now()}.mp4`;
|
|
341
|
+
const display = params.display || process.env.DISPLAY || ':99';
|
|
342
|
+
const audioSource = params.audioSource || 'default';
|
|
343
|
+
|
|
344
|
+
// Return ffmpeg command for the agent to execute via bash tool
|
|
345
|
+
const ffmpegCmd = [
|
|
346
|
+
'ffmpeg', '-y',
|
|
347
|
+
'-f', 'x11grab', '-video_size', '1920x1080', '-framerate', '15', '-i', display,
|
|
348
|
+
'-f', 'pulse', '-i', audioSource,
|
|
349
|
+
'-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '28',
|
|
350
|
+
'-c:a', 'aac', '-b:a', '128k',
|
|
351
|
+
'-movflags', '+faststart',
|
|
352
|
+
output,
|
|
353
|
+
].join(' ');
|
|
354
|
+
|
|
355
|
+
return jsonResult({
|
|
356
|
+
action: 'start',
|
|
357
|
+
command: ffmpegCmd,
|
|
358
|
+
outputPath: output,
|
|
359
|
+
hint: 'Run this command in the background using the bash tool: `nohup ' + ffmpegCmd + ' &`\nTo stop recording later, use meeting_record with action="stop".',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (params.action === 'stop') {
|
|
364
|
+
return jsonResult({
|
|
365
|
+
action: 'stop',
|
|
366
|
+
command: "pkill -INT -f 'ffmpeg.*meeting-recording'",
|
|
367
|
+
hint: 'Run this command via bash tool to gracefully stop ffmpeg recording. Then use meeting_save to upload the recording to Drive.',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return errorResult('action must be "start" or "stop"');
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// ─── Check Meeting Joinability ─────────────────────
|
|
376
|
+
{
|
|
377
|
+
name: 'meeting_can_join',
|
|
378
|
+
description: 'Check if this agent can join a video meeting on the current deployment. Returns capabilities and specific instructions based on what is available.',
|
|
379
|
+
category: 'utility' as const,
|
|
380
|
+
parameters: {
|
|
381
|
+
type: 'object' as const,
|
|
382
|
+
properties: {
|
|
383
|
+
platform: { type: 'string', description: 'Meeting platform: google_meet, zoom, teams' },
|
|
384
|
+
},
|
|
385
|
+
required: [],
|
|
386
|
+
},
|
|
387
|
+
async execute(_id: string, params: any) {
|
|
388
|
+
const c = getCaps();
|
|
389
|
+
const summary = getCapabilitySummary(c);
|
|
390
|
+
|
|
391
|
+
if (c.canJoinMeetings) {
|
|
392
|
+
return jsonResult({
|
|
393
|
+
canJoin: true,
|
|
394
|
+
deployment: summary.deployment,
|
|
395
|
+
capabilities: summary.available,
|
|
396
|
+
instructions: [
|
|
397
|
+
'Use the browser tool to navigate to the meeting URL.',
|
|
398
|
+
'Take a snapshot to identify the pre-join screen.',
|
|
399
|
+
'Use act to click mute/camera toggles and the join button.',
|
|
400
|
+
'For Google Meet: use keyboard shortcuts Ctrl+D (mute) and Ctrl+E (camera).',
|
|
401
|
+
params.platform === 'zoom' ? 'For Zoom: click "Join from Your Browser" to avoid the Zoom client.' : null,
|
|
402
|
+
params.platform === 'teams' ? 'For Teams: click "Continue on this browser" to join in-browser.' : null,
|
|
403
|
+
].filter(Boolean),
|
|
404
|
+
tips: [
|
|
405
|
+
'Join with mic muted and camera off by default.',
|
|
406
|
+
'Use meeting_record to capture the meeting.',
|
|
407
|
+
'Use meeting_prepare first to create Drive folder + notes template.',
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Can't join — give helpful alternatives
|
|
413
|
+
return jsonResult({
|
|
414
|
+
canJoin: false,
|
|
415
|
+
deployment: summary.deployment,
|
|
416
|
+
missing: summary.unavailable,
|
|
417
|
+
recommendations: summary.recommendations,
|
|
418
|
+
alternatives: [
|
|
419
|
+
'Use meeting_prepare to create Drive folder with notes template.',
|
|
420
|
+
'Use meetings_upcoming to monitor the calendar for meeting details.',
|
|
421
|
+
'Use meetings_scan_inbox to find meeting links from email invites.',
|
|
422
|
+
'Use meeting_rsvp to accept/decline meetings via Calendar API.',
|
|
423
|
+
'After the meeting, use meeting_save to organize notes/recordings in Drive.',
|
|
424
|
+
'For the agent to actually join meetings, deploy on a VM with: Xvfb + PulseAudio + Chromium + ffmpeg.',
|
|
425
|
+
],
|
|
426
|
+
whatWorksHere: [
|
|
427
|
+
'Calendar management (create, update, RSVP)',
|
|
428
|
+
'Meeting prep (Drive folder + notes template)',
|
|
429
|
+
'Post-meeting organization (notes, transcript, action items → Drive)',
|
|
430
|
+
'Email/inbox scanning for meeting invites',
|
|
431
|
+
'Scheduling and rescheduling meetings',
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
}
|
|
@@ -832,6 +832,11 @@ export function createAgentRoutes(opts: {
|
|
|
832
832
|
tools: ['google_contacts_list', 'google_contacts_search', 'google_contacts_search_directory',
|
|
833
833
|
'google_contacts_create', 'google_contacts_update'],
|
|
834
834
|
},
|
|
835
|
+
{
|
|
836
|
+
id: 'meetings', name: 'Meetings', description: 'Join Google Meet, Zoom, Teams calls. Scan calendar and inbox for meeting invites. RSVP, take notes, send summaries.',
|
|
837
|
+
icon: '🎥', requiresOAuth: 'google',
|
|
838
|
+
tools: ['meetings_upcoming', 'meeting_join', 'meeting_action', 'meetings_scan_inbox', 'meeting_rsvp'],
|
|
839
|
+
},
|
|
835
840
|
{
|
|
836
841
|
id: 'enterprise_database', name: 'Database', description: 'SQL queries, schema inspection, data sampling',
|
|
837
842
|
icon: '🗄️',
|
|
@@ -917,6 +922,20 @@ export function createAgentRoutes(opts: {
|
|
|
917
922
|
];
|
|
918
923
|
|
|
919
924
|
// ═══════════════════════════════════════════════════════════
|
|
925
|
+
// SYSTEM CAPABILITIES
|
|
926
|
+
// ═══════════════════════════════════════════════════════════
|
|
927
|
+
|
|
928
|
+
router.get('/bridge/system/capabilities', async (c) => {
|
|
929
|
+
try {
|
|
930
|
+
const { detectCapabilities, getCapabilitySummary } = await import('../runtime/environment.js');
|
|
931
|
+
const caps = detectCapabilities();
|
|
932
|
+
const summary = getCapabilitySummary(caps);
|
|
933
|
+
return c.json({ ...summary, raw: caps });
|
|
934
|
+
} catch (e: any) {
|
|
935
|
+
return c.json({ error: e.message }, 500);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
920
939
|
// BROWSER CONFIGURATION
|
|
921
940
|
// ═══════════════════════════════════════════════════════════
|
|
922
941
|
|
|
@@ -20,7 +20,7 @@ import { ToolRegistry, executeTool } from './tool-executor.js';
|
|
|
20
20
|
const DEFAULT_MAX_TURNS = 0; // 0 = unlimited
|
|
21
21
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
22
22
|
const DEFAULT_TEMPERATURE = 0.7;
|
|
23
|
-
const DEFAULT_CONTEXT_WINDOW =
|
|
23
|
+
const DEFAULT_CONTEXT_WINDOW = 2_000_000; // 1M — most frontier models support this (Feb 2026)
|
|
24
24
|
const COMPACTION_THRESHOLD = 0.8; // compact when 80% of context used
|
|
25
25
|
|
|
26
26
|
// ─── Agent Loop ──────────────────────────────────────────
|