@agenticmail/enterprise 0.5.82 → 0.5.84

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.
@@ -3988,6 +3988,190 @@ function ToolsSection(props) {
3988
3988
  // BROWSER CONFIG CARD — Configurable browser settings per agent
3989
3989
  // ════════════════════════════════════════════════════════════
3990
3990
 
3991
+ // ════════════════════════════════════════════════════════════
3992
+ // MEETING CAPABILITIES — Simple toggle, everything auto-managed
3993
+ // ════════════════════════════════════════════════════════════
3994
+
3995
+ function MeetingCapabilitiesSection(props) {
3996
+ var agentId = props.agentId;
3997
+ var cfg = props.cfg;
3998
+ var update = props.update;
3999
+ var sectionStyle = props.sectionStyle;
4000
+ var sectionTitle = props.sectionTitle;
4001
+ var labelStyle = props.labelStyle;
4002
+ var helpStyle = props.helpStyle;
4003
+ var _d = useApp(); var toast = _d.toast;
4004
+ var _launching = useState(false); var launching = _launching[0]; var setLaunching = _launching[1];
4005
+ var _browserStatus = useState(null); var browserStatus = _browserStatus[0]; var setBrowserStatus = _browserStatus[1];
4006
+
4007
+ function checkMeetingBrowser() {
4008
+ engineCall('/bridge/agents/' + agentId + '/browser-config/test', { method: 'POST' })
4009
+ .then(function(d) { setBrowserStatus(d); })
4010
+ .catch(function() { setBrowserStatus(null); });
4011
+ }
4012
+
4013
+ useEffect(function() {
4014
+ if (cfg.meetingsEnabled) checkMeetingBrowser();
4015
+ }, [cfg.meetingsEnabled]);
4016
+
4017
+ function launchMeetingBrowser() {
4018
+ setLaunching(true);
4019
+ engineCall('/bridge/agents/' + agentId + '/browser-config/launch-meeting-browser', { method: 'POST' })
4020
+ .then(function(d) {
4021
+ if (d.error) { toast(d.error, 'error'); }
4022
+ else { toast('Meeting browser ready', 'success'); setBrowserStatus(d); }
4023
+ setLaunching(false);
4024
+ })
4025
+ .catch(function(e) { toast(e.message, 'error'); setLaunching(false); });
4026
+ }
4027
+
4028
+ var meetingsOn = cfg.meetingsEnabled === true;
4029
+
4030
+ return h('div', { style: sectionStyle },
4031
+ sectionTitle('\uD83C\uDFA5', 'Meetings & Video Calls'),
4032
+
4033
+ // Main toggle
4034
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 16, marginBottom: meetingsOn ? 16 : 0 } },
4035
+ h('div', {
4036
+ onClick: function() { update('meetingsEnabled', !meetingsOn); },
4037
+ style: {
4038
+ width: 52, height: 28, borderRadius: 14, position: 'relative', cursor: 'pointer',
4039
+ background: meetingsOn ? 'var(--accent)' : 'var(--border)', transition: 'background 0.2s', flexShrink: 0,
4040
+ },
4041
+ },
4042
+ h('div', { style: {
4043
+ width: 24, height: 24, borderRadius: 12, background: '#fff', position: 'absolute', top: 2,
4044
+ left: meetingsOn ? 26 : 2, transition: 'left 0.2s',
4045
+ boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
4046
+ } })
4047
+ ),
4048
+ h('div', null,
4049
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, meetingsOn ? 'Meeting participation enabled' : 'Meeting participation disabled'),
4050
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginTop: 2 } },
4051
+ meetingsOn
4052
+ ? 'Agent can join Google Meet, Microsoft Teams, and Zoom calls automatically'
4053
+ : 'Enable to let this agent join video calls and meetings on behalf of your organization'
4054
+ )
4055
+ )
4056
+ ),
4057
+
4058
+ // When enabled, show status + options
4059
+ meetingsOn && h('div', null,
4060
+
4061
+ // Status card
4062
+ h('div', { style: { display: 'flex', gap: 12, marginBottom: 16 } },
4063
+ h('div', { className: 'card', style: { flex: 1, padding: '12px 16px' } },
4064
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 } },
4065
+ h('div', { style: {
4066
+ width: 8, height: 8, borderRadius: 4,
4067
+ background: browserStatus?.ok ? 'var(--success)' : 'var(--warning)',
4068
+ } }),
4069
+ h('span', { style: { fontSize: 13, fontWeight: 600 } }, 'Meeting Browser'),
4070
+ browserStatus?.ok && h('span', { className: 'badge', style: { fontSize: 10, padding: '1px 6px', background: 'var(--success-soft)', color: 'var(--success)' } }, 'Running')
4071
+ ),
4072
+ browserStatus?.ok
4073
+ ? h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, browserStatus.browserVersion || 'Chromium ready')
4074
+ : h('div', null,
4075
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 8 } }, 'A dedicated browser instance will be launched for video calls with virtual display and audio.'),
4076
+ h('button', { className: 'btn btn-sm', disabled: launching, onClick: launchMeetingBrowser },
4077
+ launching ? 'Launching...' : '\u25B6\uFE0F Launch Meeting Browser'
4078
+ )
4079
+ )
4080
+ )
4081
+ ),
4082
+
4083
+ // Supported platforms
4084
+ h('div', { style: { display: 'grid', gap: 8, gridTemplateColumns: '1fr 1fr 1fr', marginBottom: 16 } },
4085
+ [
4086
+ { name: 'Google Meet', icon: '\uD83D\uDFE2', enabled: cfg.meetingGoogleMeet !== false, key: 'meetingGoogleMeet', desc: 'Join via Google Calendar integration' },
4087
+ { name: 'Microsoft Teams', icon: '\uD83D\uDFE3', enabled: cfg.meetingTeams !== false, key: 'meetingTeams', desc: 'Join via meeting links' },
4088
+ { name: 'Zoom', icon: '\uD83D\uDD35', enabled: cfg.meetingZoom !== false, key: 'meetingZoom', desc: 'Join via meeting links' },
4089
+ ].map(function(p) {
4090
+ return h('div', { key: p.key, className: 'card', style: { padding: '10px 12px', cursor: 'pointer', border: '1px solid ' + (p.enabled ? 'var(--accent)' : 'var(--border)') },
4091
+ onClick: function() { update(p.key, !p.enabled); }
4092
+ },
4093
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } },
4094
+ h('span', null, p.icon),
4095
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, p.name),
4096
+ h('span', { style: { marginLeft: 'auto', fontSize: 11, color: p.enabled ? 'var(--success)' : 'var(--text-muted)' } }, p.enabled ? 'ON' : 'OFF')
4097
+ ),
4098
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, p.desc)
4099
+ );
4100
+ })
4101
+ ),
4102
+
4103
+ // Meeting behavior
4104
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4105
+ h('div', { className: 'form-group' },
4106
+ h('label', { style: labelStyle }, 'Auto-Join Calendar Meetings'),
4107
+ h('select', { className: 'input', value: cfg.meetingAutoJoin || 'ask',
4108
+ onChange: function(e) { update('meetingAutoJoin', e.target.value); }
4109
+ },
4110
+ h('option', { value: 'always' }, 'Always — Join all meetings automatically'),
4111
+ h('option', { value: 'invited' }, 'When Invited — Only join meetings the agent is invited to'),
4112
+ h('option', { value: 'ask' }, 'Ask First — Request approval before joining'),
4113
+ h('option', { value: 'never' }, 'Manual Only — Agent only joins when explicitly told')
4114
+ ),
4115
+ h('div', { style: helpStyle }, 'How the agent decides when to join meetings.')
4116
+ ),
4117
+ h('div', { className: 'form-group' },
4118
+ h('label', { style: labelStyle }, 'Meeting Role'),
4119
+ h('select', { className: 'input', value: cfg.meetingRole || 'observer',
4120
+ onChange: function(e) { update('meetingRole', e.target.value); }
4121
+ },
4122
+ h('option', { value: 'observer' }, 'Observer — Listen and take notes only'),
4123
+ h('option', { value: 'participant' }, 'Participant — Can speak and interact'),
4124
+ h('option', { value: 'presenter' }, 'Presenter — Can share screen and present')
4125
+ ),
4126
+ h('div', { style: helpStyle }, 'What the agent is allowed to do in meetings.')
4127
+ ),
4128
+ h('div', { className: 'form-group' },
4129
+ h('label', { style: labelStyle }, 'Join Timing'),
4130
+ h('select', { className: 'input', value: cfg.meetingJoinTiming || 'ontime',
4131
+ onChange: function(e) { update('meetingJoinTiming', e.target.value); }
4132
+ },
4133
+ h('option', { value: 'early' }, 'Early — Join 2 minutes before start'),
4134
+ h('option', { value: 'ontime' }, 'On Time — Join at scheduled start'),
4135
+ h('option', { value: 'late' }, 'Fashionably Late — Join 2 minutes after start')
4136
+ )
4137
+ ),
4138
+ h('div', { className: 'form-group' },
4139
+ h('label', { style: labelStyle }, 'After Meeting'),
4140
+ h('select', { className: 'input', value: cfg.meetingAfterAction || 'notes',
4141
+ onChange: function(e) { update('meetingAfterAction', e.target.value); }
4142
+ },
4143
+ h('option', { value: 'notes' }, 'Send meeting notes to organizer'),
4144
+ h('option', { value: 'summary' }, 'Post summary to team channel'),
4145
+ h('option', { value: 'transcript' }, 'Save full transcript'),
4146
+ h('option', { value: 'nothing' }, 'Do nothing')
4147
+ ),
4148
+ h('div', { style: helpStyle }, 'What happens after the meeting ends.')
4149
+ )
4150
+ ),
4151
+
4152
+ // Display name in meetings
4153
+ h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr', marginTop: 12 } },
4154
+ h('div', { className: 'form-group' },
4155
+ h('label', { style: labelStyle }, 'Display Name in Meetings'),
4156
+ h('input', { className: 'input', placeholder: 'Agent name (e.g. "Fola - AI Assistant")',
4157
+ value: cfg.meetingDisplayName || '',
4158
+ onChange: function(e) { update('meetingDisplayName', e.target.value || undefined); }
4159
+ }),
4160
+ h('div', { style: helpStyle }, 'How the agent appears to other participants.')
4161
+ ),
4162
+ h('div', { className: 'form-group' },
4163
+ h('label', { style: labelStyle }, 'Max Meeting Duration (minutes)'),
4164
+ h('input', { className: 'input', type: 'number', min: 5, max: 480,
4165
+ value: cfg.meetingMaxDuration || 120,
4166
+ onChange: function(e) { update('meetingMaxDuration', parseInt(e.target.value) || 120); }
4167
+ }),
4168
+ h('div', { style: helpStyle }, 'Agent will leave after this duration to prevent runaway sessions.')
4169
+ )
4170
+ )
4171
+ )
4172
+ );
4173
+ }
4174
+
3991
4175
  function BrowserConfigCard(props) {
3992
4176
  var agentId = props.agentId;
3993
4177
  var _d = useApp(); var toast = _d.toast;
@@ -4420,57 +4604,7 @@ function BrowserConfigCard(props) {
4420
4604
  ),
4421
4605
 
4422
4606
  // ─── Section 4: Meeting & Video Capabilities ─────
4423
- h('div', { style: sectionStyle },
4424
- sectionTitle('\uD83C\uDFA5', 'Meeting & Video Call Capabilities'),
4425
- h('div', { style: { padding: '10px 14px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', marginBottom: 12, fontSize: 12, lineHeight: 1.6 } },
4426
- h('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'For Google Meet, Teams, and Zoom:'),
4427
- h('div', null, '\u2022 Use ', h('strong', null, 'Remote Browser (CDP)'), ' provider pointed at a VM with display + virtual camera'),
4428
- h('div', null, '\u2022 The remote machine needs: X11/Wayland display, PulseAudio/PipeWire (audio), v4l2loopback (virtual camera)'),
4429
- h('div', null, '\u2022 Agent will navigate to meeting URL, handle permissions, and interact with the meeting UI'),
4430
- h('div', null, '\u2022 For voice participation: integrate with a Speech-to-Text / Text-to-Speech service'),
4431
- h('div', { style: { marginTop: 8 } },
4432
- h('strong', null, 'Quick VM Setup (Linux):'),
4433
- h('pre', { style: { margin: '6px 0', padding: 8, background: 'var(--bg-tertiary)', borderRadius: 4, fontSize: 11, overflow: 'auto' } },
4434
- '# Install deps\nsudo apt install -y xvfb pulseaudio chromium v4l2loopback-dkms\n\n# Start virtual display + audio\nXvfb :99 -screen 0 1920x1080x24 &\nexport DISPLAY=:99\npulseaudio --start\n\n# Launch Chrome with remote debugging\nchromium --remote-debugging-port=9222 \\\n --no-first-run --disable-gpu \\\n --use-fake-ui-for-media-stream \\\n --use-fake-device-for-media-stream \\\n --auto-accept-camera-and-microphone-capture'
4435
- )
4436
- )
4437
- ),
4438
- h('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: '1fr 1fr' } },
4439
- h('div', { className: 'form-group' },
4440
- h('label', { style: labelStyle }, 'Auto-Accept Permissions'),
4441
- h('select', { className: 'input', value: cfg.autoAcceptPermissions !== false ? 'true' : 'false',
4442
- onChange: function(e) { update('autoAcceptPermissions', e.target.value === 'true'); }
4443
- },
4444
- h('option', { value: 'true' }, 'Yes — Auto-grant camera/microphone access'),
4445
- h('option', { value: 'false' }, 'No — Require manual permission grants')
4446
- )
4447
- ),
4448
- h('div', { className: 'form-group' },
4449
- h('label', { style: labelStyle }, 'Virtual Camera Feed'),
4450
- h('input', { className: 'input', placeholder: '/dev/video0 or URL to video feed',
4451
- value: cfg.virtualCameraSource || '',
4452
- onChange: function(e) { update('virtualCameraSource', e.target.value || undefined); }
4453
- }),
4454
- h('div', { style: helpStyle }, 'Video source for the agent\'s "camera" in meetings.')
4455
- ),
4456
- h('div', { className: 'form-group' },
4457
- h('label', { style: labelStyle }, 'Audio Input'),
4458
- h('input', { className: 'input', placeholder: 'PulseAudio source (e.g. virtual_mic)',
4459
- value: cfg.audioInput || '',
4460
- onChange: function(e) { update('audioInput', e.target.value || undefined); }
4461
- }),
4462
- h('div', { style: helpStyle }, 'Audio source for the agent to speak in meetings.')
4463
- ),
4464
- h('div', { className: 'form-group' },
4465
- h('label', { style: labelStyle }, 'Audio Output'),
4466
- h('input', { className: 'input', placeholder: 'PulseAudio sink (e.g. virtual_speaker)',
4467
- value: cfg.audioOutput || '',
4468
- onChange: function(e) { update('audioOutput', e.target.value || undefined); }
4469
- }),
4470
- h('div', { style: helpStyle }, 'Audio sink for the agent to hear meeting participants.')
4471
- )
4472
- )
4473
- ),
4607
+ h(MeetingCapabilitiesSection, { agentId: agentId, cfg: cfg, update: update, labelStyle: labelStyle, helpStyle: helpStyle, sectionStyle: sectionStyle, sectionTitle: sectionTitle }),
4474
4608
 
4475
4609
  // ─── Section 5: Persistent Sessions ──────────────
4476
4610
  h('div', { style: { paddingTop: 12 } },
@@ -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: '🗄️',
@@ -927,6 +932,106 @@ export function createAgentRoutes(opts: {
927
932
  return c.json({ config: managed.config?.browserConfig || {} });
928
933
  });
929
934
 
935
+ /**
936
+ * POST /bridge/agents/:id/browser-config/launch-meeting-browser
937
+ * Launches a meeting-ready headed Chrome instance with virtual display + audio.
938
+ * Returns the CDP URL for the agent to connect to.
939
+ */
940
+ router.post('/bridge/agents/:id/browser-config/launch-meeting-browser', async (c) => {
941
+ const agentId = c.req.param('id');
942
+ const managed = lifecycle.getAgent(agentId);
943
+ if (!managed) return c.json({ error: 'Agent not found' }, 404);
944
+
945
+ try {
946
+ const { execSync, spawn } = await import('node:child_process');
947
+ const chromePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || '/usr/bin/chromium';
948
+
949
+ // Check if a meeting browser is already running for this agent
950
+ const existingPort = (managed.config as any)?.meetingBrowserPort;
951
+ if (existingPort) {
952
+ try {
953
+ const resp = await fetch(`http://127.0.0.1:${existingPort}/json/version`, { signal: AbortSignal.timeout(2000) });
954
+ if (resp.ok) {
955
+ const data = await resp.json() as any;
956
+ return c.json({ ok: true, alreadyRunning: true, cdpUrl: data.webSocketDebuggerUrl, port: existingPort, browserVersion: data.Browser });
957
+ }
958
+ } catch { /* not running, will launch new one */ }
959
+ }
960
+
961
+ // Find available port
962
+ const net = await import('node:net');
963
+ const port = await new Promise<number>((resolve, reject) => {
964
+ const srv = net.createServer();
965
+ srv.listen(0, '127.0.0.1', () => {
966
+ const p = (srv.address() as any).port;
967
+ srv.close(() => resolve(p));
968
+ });
969
+ srv.on('error', reject);
970
+ });
971
+
972
+ // Launch Chrome with meeting-optimized flags
973
+ const chromeArgs = [
974
+ `--remote-debugging-port=${port}`,
975
+ '--remote-debugging-address=127.0.0.1',
976
+ '--no-first-run',
977
+ '--no-default-browser-check',
978
+ '--disable-background-networking',
979
+ '--disable-sync',
980
+ '--disable-translate',
981
+ '--metrics-recording-only',
982
+ '--no-sandbox',
983
+ // Meeting-specific: auto-grant camera/mic permissions
984
+ '--use-fake-ui-for-media-stream',
985
+ '--auto-accept-camera-and-microphone-capture',
986
+ // Use virtual audio
987
+ '--use-fake-device-for-media-stream',
988
+ // Window size for meeting UI
989
+ '--window-size=1920,1080',
990
+ '--start-maximized',
991
+ // User data dir for persistent logins
992
+ `/tmp/meeting-browser-${agentId.slice(0, 8)}`,
993
+ ];
994
+
995
+ const child = spawn(chromePath, chromeArgs, {
996
+ detached: true,
997
+ stdio: 'ignore',
998
+ env: { ...process.env, DISPLAY: ':99' },
999
+ });
1000
+ child.unref();
1001
+
1002
+ // Wait for Chrome to be ready
1003
+ let cdpUrl = '';
1004
+ let browserVersion = '';
1005
+ for (let i = 0; i < 30; i++) {
1006
+ await new Promise(r => setTimeout(r, 500));
1007
+ try {
1008
+ const resp = await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(2000) });
1009
+ if (resp.ok) {
1010
+ const data = await resp.json() as any;
1011
+ cdpUrl = data.webSocketDebuggerUrl;
1012
+ browserVersion = data.Browser;
1013
+ break;
1014
+ }
1015
+ } catch { /* retry */ }
1016
+ }
1017
+
1018
+ if (!cdpUrl) {
1019
+ return c.json({ error: 'Chrome launched but CDP not responding after 15s' });
1020
+ }
1021
+
1022
+ // Save port to agent config for reuse
1023
+ if (!managed.config) managed.config = {} as any;
1024
+ (managed.config as any).meetingBrowserPort = port;
1025
+ (managed.config as any).meetingBrowserCdpUrl = cdpUrl;
1026
+ managed.updatedAt = new Date().toISOString();
1027
+ await lifecycle.saveAgent(agentId);
1028
+
1029
+ return c.json({ ok: true, cdpUrl, port, browserVersion, pid: child.pid });
1030
+ } catch (e: any) {
1031
+ return c.json({ error: 'Failed to launch meeting browser: ' + e.message });
1032
+ }
1033
+ });
1034
+
930
1035
  router.post('/bridge/agents/:id/browser-config/test', async (c) => {
931
1036
  const agentId = c.req.param('id');
932
1037
  const managed = lifecycle.getAgent(agentId);