@zengxingyuan/aamp-cli-bridge 0.1.7-dev.2 → 0.1.7-dev.4

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/README.md CHANGED
@@ -76,6 +76,33 @@ npx aamp-cli-bridge start --config ./production.cli-bridge.json
76
76
  npx aamp-cli-bridge list --config ./production.cli-bridge.json
77
77
  ```
78
78
 
79
+ Desktop and other non-interactive clients can use JSON output:
80
+
81
+ ```bash
82
+ npx aamp-cli-bridge init --json --input -
83
+ npx aamp-cli-bridge discover --json
84
+ npx aamp-cli-bridge list --json
85
+ npx aamp-cli-bridge start --json
86
+ npx aamp-cli-bridge pair --agent codex --json --no-start
87
+ ```
88
+
89
+ `init --json` is an upsert operation for desktop clients: it writes the bridge config, reuses existing credentials when available, registers missing mailboxes, and does not auto-start the bridge. `discover --json` scans built-in profiles, user profiles, and already configured agents, then reports which commands are available on PATH. `start --json` emits JSONL runtime events on stdout and sends human-readable logs to stderr. `pair --json --no-start` creates a pairing URL without rendering a terminal QR code.
90
+
91
+ Example JSON init input:
92
+
93
+ ```json
94
+ {
95
+ "aampHost": "https://meshmail.ai",
96
+ "agents": [
97
+ {
98
+ "name": "codex",
99
+ "cliProfile": "codex",
100
+ "createPairing": true
101
+ }
102
+ ]
103
+ }
104
+ ```
105
+
79
106
  ## Profile Model
80
107
 
81
108
  A CLI profile describes how to invoke an agent and how to interpret its output.
@@ -211,10 +238,10 @@ NDJSON stream:
211
238
  The parser accepts common event shapes used by CLI agents:
212
239
 
213
240
  - `text`, `delta`, `text.delta`: forwarded to AAMP as `text.delta`
214
- - `tool_start`, `tool_result`, `tool`: forwarded as stream progress or status events
215
- - `usage`: forwarded as a progress event
241
+ - `tool_start`, `tool_result`, `tool`: forwarded to AAMP as `tool_call`
242
+ - `usage`: forwarded as a `todo` update
216
243
  - `result`: used as final text when present
217
- - `done`: closes the stream state for the current task
244
+ - `done`: ends parser consumption; terminal state is sent through `task.result`
218
245
 
219
246
  Text deltas are streamed to AAMP and concatenated into the final `task.result`. This lets a mailbox UI show live output while still preserving a complete final answer in the thread.
220
247
 
@@ -314,9 +341,11 @@ The CLI agent can use these plain-output conventions:
314
341
 
315
342
  ```bash
316
343
  npx aamp-cli-bridge init [--no-start]
317
- npx aamp-cli-bridge start [--config X]
344
+ npx aamp-cli-bridge init --json --input -
345
+ npx aamp-cli-bridge start [--config X] [--json]
318
346
  npx aamp-cli-bridge pair --agent NAME [--config X] [--no-start]
319
347
  npx aamp-cli-bridge list [--config X]
348
+ npx aamp-cli-bridge discover [--config X] [--json]
320
349
  npx aamp-cli-bridge status
321
350
  npx aamp-cli-bridge profile-list
322
351
  npx aamp-cli-bridge profile-maker
@@ -1,4 +1,5 @@
1
1
  import type { AgentConfig, BridgeConfig } from './config.js';
2
+ import type { BridgeRuntimeEvent } from './bridge.js';
2
3
  export interface AgentIdentity {
3
4
  email: string;
4
5
  mailboxToken: string;
@@ -6,6 +7,7 @@ export interface AgentIdentity {
6
7
  }
7
8
  export interface AgentBridgeStartOptions {
8
9
  quiet?: boolean;
10
+ onEvent?: (event: BridgeRuntimeEvent) => void;
9
11
  }
10
12
  export declare class AgentBridge {
11
13
  private readonly agentConfig;
@@ -17,16 +19,19 @@ export declare class AgentBridge {
17
19
  private activeTaskCount;
18
20
  private pollingFallback;
19
21
  private cancelledTaskIds;
22
+ private activeTaskIds;
20
23
  private profileLabel;
21
24
  private streamEnabled;
22
25
  private senderPolicies;
23
26
  private isHistoricalReconcile;
27
+ private onEvent;
24
28
  constructor(agentConfig: AgentConfig, aampHost: string, rejectUnauthorized: boolean, customProfiles?: BridgeConfig['profiles']);
25
29
  get name(): string;
26
30
  get email(): string;
27
31
  get isConnected(): boolean;
28
32
  get isUsingPollingFallback(): boolean;
29
33
  get isBusy(): boolean;
34
+ private emit;
30
35
  private getConfiguredCardText;
31
36
  private syncDirectoryProfile;
32
37
  start(options?: AgentBridgeStartOptions): Promise<void>;
@@ -1,10 +1,10 @@
1
1
  import { AampClient, } from 'aamp-sdk';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { basename, dirname } from 'node:path';
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
4
  import { CliAgentClient } from './cli-agent-client.js';
5
5
  import { resolveCliProfile } from './cli-profiles.js';
6
6
  import { buildPrompt, parseResponse } from './prompt-builder.js';
7
- import { resolveCredentialsFile } from './storage.js';
7
+ import { getBridgeHomeDir, resolveCredentialsFile } from './storage.js';
8
8
  import { addSenderPolicy, consumePairingCode, loadSenderPolicies, resolvePairingFile, resolveSenderPoliciesFile, rulesMatch, validatePairingCode, } from './pairing.js';
9
9
  const IDENTITY_AUTH_RETRY_COUNT = 5;
10
10
  const IDENTITY_AUTH_RETRY_DELAY_MS = 1_000;
@@ -12,7 +12,7 @@ function matchSenderPolicy(task, senderPolicies) {
12
12
  if (!senderPolicies?.length)
13
13
  return { allowed: false, reason: 'no configured senderPolicies' };
14
14
  const sender = task.from.toLowerCase();
15
- const policy = senderPolicies.find((item) => item.sender.trim().toLowerCase() === sender);
15
+ const policy = senderPolicies.find((item) => matchesSenderPattern(sender, item.sender));
16
16
  if (!policy) {
17
17
  return { allowed: false, reason: `sender ${task.from} is not allowed by senderPolicies` };
18
18
  }
@@ -32,6 +32,17 @@ function matchSenderPolicy(task, senderPolicies) {
32
32
  }
33
33
  return { allowed: true };
34
34
  }
35
+ function matchesSenderPattern(senderEmail, pattern) {
36
+ const normalizedSender = senderEmail.trim().toLowerCase();
37
+ const normalizedPattern = pattern.trim().toLowerCase();
38
+ if (!normalizedSender || !normalizedPattern)
39
+ return false;
40
+ const canonicalPattern = normalizedPattern.startsWith('@')
41
+ ? `*${normalizedPattern}`
42
+ : normalizedPattern;
43
+ const escaped = canonicalPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
44
+ return new RegExp(`^${escaped}$`, 'i').test(normalizedSender);
45
+ }
35
46
  function matchPairedSenderPolicy(task, senderPolicies) {
36
47
  if (senderPolicies.length === 0)
37
48
  return { allowed: false, reason: 'no paired sender policies configured' };
@@ -132,6 +143,39 @@ function stringifyStreamPayload(payload) {
132
143
  return payload.chunk;
133
144
  return JSON.stringify(payload, null, 2);
134
145
  }
146
+ function taskLockName(taskId) {
147
+ return taskId
148
+ .trim()
149
+ .replace(/[^a-zA-Z0-9_.-]+/g, '-')
150
+ .replace(/-+/g, '-')
151
+ .replace(/^-|-$/g, '')
152
+ .slice(0, 128) || 'task';
153
+ }
154
+ function acquireTaskExecutionLock(taskId) {
155
+ const locksDir = join(getBridgeHomeDir(), 'task-locks');
156
+ const lockDir = join(locksDir, `${taskLockName(taskId)}.lock`);
157
+ mkdirSync(locksDir, { recursive: true });
158
+ try {
159
+ mkdirSync(lockDir);
160
+ writeFileSync(join(lockDir, 'owner.json'), `${JSON.stringify({
161
+ pid: process.pid,
162
+ taskId,
163
+ acquiredAt: new Date().toISOString(),
164
+ }, null, 2)}\n`);
165
+ return lockDir;
166
+ }
167
+ catch (error) {
168
+ const code = error.code;
169
+ if (code === 'EEXIST')
170
+ return null;
171
+ throw error;
172
+ }
173
+ }
174
+ function releaseTaskExecutionLock(lockDir) {
175
+ if (!lockDir)
176
+ return;
177
+ rmSync(lockDir, { recursive: true, force: true });
178
+ }
135
179
  export class AgentBridge {
136
180
  agentConfig;
137
181
  aampHost;
@@ -142,10 +186,12 @@ export class AgentBridge {
142
186
  activeTaskCount = 0;
143
187
  pollingFallback = false;
144
188
  cancelledTaskIds = new Set();
189
+ activeTaskIds = new Set();
145
190
  profileLabel;
146
191
  streamEnabled;
147
192
  senderPolicies = [];
148
193
  isHistoricalReconcile = false;
194
+ onEvent;
149
195
  constructor(agentConfig, aampHost, rejectUnauthorized, customProfiles) {
150
196
  this.agentConfig = agentConfig;
151
197
  this.aampHost = aampHost;
@@ -160,6 +206,9 @@ export class AgentBridge {
160
206
  get isConnected() { return this.client?.isConnected() ?? false; }
161
207
  get isUsingPollingFallback() { return this.pollingFallback || (this.client?.isUsingPollingFallback() ?? false); }
162
208
  get isBusy() { return this.activeTaskCount > 0; }
209
+ emit(event) {
210
+ this.onEvent?.(event);
211
+ }
163
212
  getConfiguredCardText() {
164
213
  const inline = this.agentConfig.cardText?.trim();
165
214
  if (inline)
@@ -186,6 +235,7 @@ export class AgentBridge {
186
235
  }
187
236
  }
188
237
  async start(options = {}) {
238
+ this.onEvent = options.onEvent;
189
239
  let quietStartup = options.quiet === true;
190
240
  this.identity = await this.resolveIdentity();
191
241
  this.senderPolicies = loadSenderPolicies(resolveSenderPoliciesFile(this.agentConfig.senderPoliciesFile, this.agentConfig.name));
@@ -193,6 +243,13 @@ export class AgentBridge {
193
243
  console.log(`[${this.name}] AAMP identity: ${this.identity.email}`);
194
244
  console.log(`[${this.name}] CLI profile: ${this.profileLabel}`);
195
245
  }
246
+ this.emit({
247
+ type: 'agent.identity',
248
+ bridge: 'cli-bridge',
249
+ agent: this.name,
250
+ email: this.identity.email,
251
+ profile: this.profileLabel,
252
+ });
196
253
  this.client = AampClient.fromMailboxIdentity({
197
254
  email: this.identity.email,
198
255
  smtpPassword: this.identity.smtpPassword,
@@ -218,12 +275,27 @@ export class AgentBridge {
218
275
  });
219
276
  client.on('connected', () => {
220
277
  this.pollingFallback = client.isUsingPollingFallback();
278
+ this.emit({
279
+ type: 'agent.connected',
280
+ bridge: 'cli-bridge',
281
+ agent: this.name,
282
+ email: this.email,
283
+ pollingFallback: this.pollingFallback,
284
+ });
221
285
  if (!quietStartup) {
222
286
  console.log(`[${this.name}] AAMP connected${this.pollingFallback ? ' (polling fallback)' : ''}`);
223
287
  }
224
288
  });
225
289
  client.on('disconnected', (reason) => {
226
290
  this.pollingFallback = client.isUsingPollingFallback();
291
+ this.emit({
292
+ type: 'agent.disconnected',
293
+ bridge: 'cli-bridge',
294
+ agent: this.name,
295
+ email: this.email,
296
+ reason,
297
+ pollingFallback: this.pollingFallback,
298
+ });
227
299
  if (!quietStartup) {
228
300
  console.warn(`[${this.name}] AAMP disconnected: ${reason}`);
229
301
  }
@@ -231,11 +303,25 @@ export class AgentBridge {
231
303
  client.on('error', (err) => {
232
304
  if (err.message.includes('falling back to polling')) {
233
305
  this.pollingFallback = true;
306
+ this.emit({
307
+ type: 'agent.error',
308
+ bridge: 'cli-bridge',
309
+ agent: this.name,
310
+ email: this.email,
311
+ message: err.message,
312
+ });
234
313
  if (!quietStartup) {
235
314
  console.warn(`[${this.name}] ${err.message}`);
236
315
  }
237
316
  return;
238
317
  }
318
+ this.emit({
319
+ type: 'agent.error',
320
+ bridge: 'cli-bridge',
321
+ agent: this.name,
322
+ email: this.email,
323
+ message: err.message,
324
+ });
239
325
  console.error(`[${this.name}] AAMP error: ${err.message}`);
240
326
  });
241
327
  await client.connect();
@@ -253,6 +339,13 @@ export class AgentBridge {
253
339
  if (!quietStartup) {
254
340
  console.log(`[${this.name}] Reconciled ${reconciled} recent email(s)`);
255
341
  }
342
+ this.emit({
343
+ type: 'agent.reconciled',
344
+ bridge: 'cli-bridge',
345
+ agent: this.name,
346
+ email: this.email,
347
+ count: reconciled,
348
+ });
256
349
  await this.syncDirectoryProfile({ quiet: quietStartup }).catch((err) => {
257
350
  if (!quietStartup) {
258
351
  console.warn(`[${this.name}] Directory profile sync failed: ${err.message}`);
@@ -270,6 +363,15 @@ export class AgentBridge {
270
363
  const shouldLogTask = !options.historical;
271
364
  if (shouldLogTask) {
272
365
  console.log(`[${this.name}] <- task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
366
+ this.emit({
367
+ type: 'task.received',
368
+ bridge: 'cli-bridge',
369
+ agent: this.name,
370
+ email: this.email,
371
+ taskId: task.taskId,
372
+ title: task.title,
373
+ from: task.from,
374
+ });
273
375
  }
274
376
  if (task.expiresAt && new Date(task.expiresAt).getTime() <= Date.now()) {
275
377
  console.warn(`[${this.name}] Skipping expired task ${task.taskId}`);
@@ -279,6 +381,10 @@ export class AgentBridge {
279
381
  console.warn(`[${this.name}] Ignoring cancelled task ${task.taskId}`);
280
382
  return;
281
383
  }
384
+ if (this.activeTaskIds.has(task.taskId)) {
385
+ console.warn(`[${this.name}] Ignoring duplicate active task ${task.taskId}`);
386
+ return;
387
+ }
282
388
  const hydratedTask = await this.client.hydrateTaskDispatch(task).catch((err) => {
283
389
  if (!options.historical) {
284
390
  console.warn(`[${this.name}] Failed to load thread history for ${task.taskId}: ${err.message}`);
@@ -305,6 +411,14 @@ export class AgentBridge {
305
411
  if (options.historical)
306
412
  return;
307
413
  console.warn(`[${this.name}] Rejecting task ${task.taskId}: ${senderDecision.reason ?? 'sender policy rejected the task'}`);
414
+ this.emit({
415
+ type: 'task.rejected',
416
+ bridge: 'cli-bridge',
417
+ agent: this.name,
418
+ email: this.email,
419
+ taskId: task.taskId,
420
+ reason: senderDecision.reason ?? 'sender policy rejected the task',
421
+ });
308
422
  await this.client.sendResult({
309
423
  to: task.from,
310
424
  taskId: task.taskId,
@@ -315,6 +429,12 @@ export class AgentBridge {
315
429
  });
316
430
  return;
317
431
  }
432
+ const taskLockDir = acquireTaskExecutionLock(task.taskId);
433
+ if (!taskLockDir) {
434
+ console.warn(`[${this.name}] Ignoring duplicate locked task ${task.taskId}`);
435
+ return;
436
+ }
437
+ this.activeTaskIds.add(task.taskId);
318
438
  this.activeTaskCount += 1;
319
439
  let activeStream = null;
320
440
  let streamOpenedAt = null;
@@ -328,7 +448,7 @@ export class AgentBridge {
328
448
  let write;
329
449
  write = this.client.appendStreamEvent({
330
450
  streamId,
331
- type,
451
+ type: type,
332
452
  payload,
333
453
  })
334
454
  .then(() => undefined)
@@ -351,7 +471,6 @@ export class AgentBridge {
351
471
  if (update.textDelta) {
352
472
  queueStreamAppend('text.delta', {
353
473
  text: update.textDelta,
354
- channel: 'assistant',
355
474
  sourceEvent: eventType,
356
475
  });
357
476
  return;
@@ -359,16 +478,14 @@ export class AgentBridge {
359
478
  if (update.finalText) {
360
479
  queueStreamAppend('text.delta', {
361
480
  text: update.finalText,
362
- channel: 'assistant',
363
481
  sourceEvent: eventType,
364
482
  });
365
483
  return;
366
484
  }
367
485
  if (eventType === 'session') {
368
- queueStreamAppend('status', {
369
- state: 'running',
370
- label: 'CLI session started',
371
- data,
486
+ queueStreamAppend('todo', {
487
+ items: [{ id: 'cli-session', content: 'CLI session started', status: 'in_progress' }],
488
+ summary: 'CLI session started',
372
489
  });
373
490
  return;
374
491
  }
@@ -376,10 +493,13 @@ export class AgentBridge {
376
493
  const record = data && typeof data === 'object' && !Array.isArray(data)
377
494
  ? data
378
495
  : {};
379
- queueStreamAppend('progress', {
496
+ const name = typeof record.name === 'string' ? record.name : 'tool';
497
+ const toolCallId = String(record.toolCallId ?? record.tool_call_id ?? record.call_id ?? record.id ?? name);
498
+ queueStreamAppend('tool_call', {
499
+ toolCallId,
380
500
  label: `Tool running: ${typeof record.name === 'string' ? record.name : 'tool'}`,
381
- status: 'in_progress',
382
- ...record,
501
+ status: 'running',
502
+ input: stringifyStreamPayload(record),
383
503
  });
384
504
  return;
385
505
  }
@@ -388,10 +508,13 @@ export class AgentBridge {
388
508
  ? data
389
509
  : {};
390
510
  const failed = record.is_error === true || record.status === 'failed';
391
- queueStreamAppend('progress', {
511
+ const name = typeof record.name === 'string' ? record.name : 'tool';
512
+ const toolCallId = String(record.toolCallId ?? record.tool_call_id ?? record.call_id ?? record.id ?? name);
513
+ queueStreamAppend('tool_call', {
514
+ toolCallId,
392
515
  label: `Tool ${failed ? 'failed' : 'completed'}: ${typeof record.name === 'string' ? record.name : 'tool'}`,
393
516
  status: failed ? 'failed' : 'completed',
394
- ...record,
517
+ output: stringifyStreamPayload(record),
395
518
  });
396
519
  return;
397
520
  }
@@ -400,28 +523,27 @@ export class AgentBridge {
400
523
  ? data
401
524
  : {};
402
525
  const chunk = typeof record.chunk === 'string' ? record.chunk : undefined;
403
- queueStreamAppend('progress', {
526
+ const name = typeof record.name === 'string' ? record.name : 'tool';
527
+ const toolCallId = String(record.toolCallId ?? record.tool_call_id ?? record.call_id ?? record.id ?? name);
528
+ queueStreamAppend('tool_call', {
529
+ toolCallId,
404
530
  label: 'Tool output',
405
- status: 'in_progress',
406
- ...record,
407
- ...(chunk ? { chunk } : {}),
531
+ status: 'running',
532
+ output: chunk ? chunk : stringifyStreamPayload(record),
408
533
  });
409
534
  return;
410
535
  }
411
536
  if (eventType === 'usage') {
412
- queueStreamAppend('progress', {
413
- label: 'Token usage updated',
414
- ...(data && typeof data === 'object' && !Array.isArray(data)
415
- ? data
416
- : { data }),
537
+ queueStreamAppend('todo', {
538
+ items: [{ id: 'token-usage', content: 'Token usage updated', status: 'in_progress' }],
539
+ summary: 'Token usage updated',
417
540
  });
418
541
  return;
419
542
  }
420
543
  if (eventType === 'done') {
421
- queueStreamAppend('status', {
422
- state: 'running',
423
- label: 'CLI stream completed',
424
- data,
544
+ queueStreamAppend('todo', {
545
+ items: [{ id: 'cli-stream', content: 'CLI stream completed', status: 'completed' }],
546
+ summary: 'CLI stream completed',
425
547
  });
426
548
  }
427
549
  };
@@ -440,7 +562,10 @@ export class AgentBridge {
440
562
  streamId: activeStream.streamId,
441
563
  inReplyTo: task.messageId,
442
564
  });
443
- queueStreamAppend('status', { state: 'running', label: 'CLI task started' });
565
+ queueStreamAppend('todo', {
566
+ items: [{ id: 'cli-task', content: 'CLI task started', status: 'in_progress' }],
567
+ summary: 'CLI task started',
568
+ });
444
569
  }
445
570
  catch (err) {
446
571
  activeStream = null;
@@ -482,6 +607,14 @@ export class AgentBridge {
482
607
  inReplyTo: task.messageId,
483
608
  });
484
609
  console.log(`[${this.name}] -> task.help_needed ${task.taskId}`);
610
+ this.emit({
611
+ type: 'task.completed',
612
+ bridge: 'cli-bridge',
613
+ agent: this.name,
614
+ email: this.email,
615
+ taskId: task.taskId,
616
+ status: 'help_needed',
617
+ });
485
618
  return;
486
619
  }
487
620
  const attachments = [];
@@ -522,6 +655,14 @@ export class AgentBridge {
522
655
  attachments: attachments.length > 0 ? attachments : undefined,
523
656
  });
524
657
  console.log(`[${this.name}] -> task.result ${task.taskId} completed${structuredResult?.length ? ` (${structuredResult.length} structured field(s))` : ''}${attachments.length ? ` (${attachments.length} attachment(s))` : ''}`);
658
+ this.emit({
659
+ type: 'task.completed',
660
+ bridge: 'cli-bridge',
661
+ agent: this.name,
662
+ email: this.email,
663
+ taskId: task.taskId,
664
+ status: 'completed',
665
+ });
525
666
  }
526
667
  catch (err) {
527
668
  const errorMsg = err.message;
@@ -546,9 +687,19 @@ export class AgentBridge {
546
687
  errorMsg: `CLI agent error: ${errorMsg}`,
547
688
  inReplyTo: task.messageId,
548
689
  }).catch(() => { });
690
+ this.emit({
691
+ type: 'task.completed',
692
+ bridge: 'cli-bridge',
693
+ agent: this.name,
694
+ email: this.email,
695
+ taskId: task.taskId,
696
+ status: 'rejected',
697
+ });
549
698
  }
550
699
  finally {
551
700
  this.activeTaskCount = Math.max(0, this.activeTaskCount - 1);
701
+ this.activeTaskIds.delete(task.taskId);
702
+ releaseTaskExecutionLock(taskLockDir);
552
703
  }
553
704
  }
554
705
  normalizeEmail(email) {
@@ -585,6 +736,14 @@ export class AgentBridge {
585
736
  const shouldLogRequest = !options.historical;
586
737
  if (shouldLogRequest) {
587
738
  console.log(`[${this.name}] <- pair.request ${request.taskId} from=${request.from}`);
739
+ this.emit({
740
+ type: 'pair.request',
741
+ bridge: 'cli-bridge',
742
+ agent: this.name,
743
+ email: this.email,
744
+ taskId: request.taskId,
745
+ from: request.from,
746
+ });
588
747
  }
589
748
  const requestTo = this.normalizeEmail(request.to);
590
749
  if (requestTo && requestTo !== this.normalizeEmail(this.identity.email)) {
@@ -624,6 +783,16 @@ export class AgentBridge {
624
783
  }
625
784
  console.warn(`[${this.name}] Rejected pair.request from ${request.from}: ${reason}`);
626
785
  await this.sendPairResponse(request, false, reason);
786
+ this.emit({
787
+ type: 'pair.completed',
788
+ bridge: 'cli-bridge',
789
+ agent: this.name,
790
+ email: this.email,
791
+ taskId: request.taskId,
792
+ sender: request.from,
793
+ success: false,
794
+ reason,
795
+ });
627
796
  return;
628
797
  }
629
798
  this.senderPolicies = addSenderPolicy(senderPoliciesFile, {
@@ -633,6 +802,15 @@ export class AgentBridge {
633
802
  });
634
803
  console.log(`[${this.name}] Paired sender ${request.from}; sender policy saved to ${senderPoliciesFile}`);
635
804
  if (await this.sendPairResponse(request, true)) {
805
+ this.emit({
806
+ type: 'pair.completed',
807
+ bridge: 'cli-bridge',
808
+ agent: this.name,
809
+ email: this.email,
810
+ taskId: request.taskId,
811
+ sender: request.from,
812
+ success: true,
813
+ });
636
814
  consumePairingCode(pairParams);
637
815
  }
638
816
  else {