fluxmail 0.7.0 → 0.8.1

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.
Files changed (46) hide show
  1. package/LICENSE.md +66 -84
  2. package/README.md +1 -1
  3. package/dist/accounts/gmailConnection.d.ts +5 -0
  4. package/dist/accounts/gmailConnection.d.ts.map +1 -1
  5. package/dist/accounts/gmailConnection.js +24 -6
  6. package/dist/accounts/gmailConnection.js.map +1 -1
  7. package/dist/accounts/googleAuth.d.ts.map +1 -1
  8. package/dist/accounts/googleAuth.js +6 -1
  9. package/dist/accounts/googleAuth.js.map +1 -1
  10. package/dist/accounts/microsoftAuth.d.ts +1 -0
  11. package/dist/accounts/microsoftAuth.d.ts.map +1 -1
  12. package/dist/accounts/microsoftAuth.js +12 -0
  13. package/dist/accounts/microsoftAuth.js.map +1 -1
  14. package/dist/accounts/registry.d.ts +3 -1
  15. package/dist/accounts/registry.d.ts.map +1 -1
  16. package/dist/accounts/registry.js +11 -0
  17. package/dist/accounts/registry.js.map +1 -1
  18. package/dist/accounts/telemetry.d.ts +27 -0
  19. package/dist/accounts/telemetry.d.ts.map +1 -0
  20. package/dist/accounts/telemetry.js +42 -0
  21. package/dist/accounts/telemetry.js.map +1 -0
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +163 -130
  24. package/dist/cli.js.map +1 -1
  25. package/dist/cliInstances.d.ts.map +1 -1
  26. package/dist/cliInstances.js +6 -2
  27. package/dist/cliInstances.js.map +1 -1
  28. package/dist/http/admin.d.ts.map +1 -1
  29. package/dist/http/admin.js +17 -0
  30. package/dist/http/admin.js.map +1 -1
  31. package/dist/http/app.d.ts +1 -1
  32. package/dist/http/app.d.ts.map +1 -1
  33. package/dist/http/app.js +76 -12
  34. package/dist/http/app.js.map +1 -1
  35. package/dist/http/identity.d.ts.map +1 -1
  36. package/dist/http/identity.js +12 -0
  37. package/dist/http/identity.js.map +1 -1
  38. package/dist/http/operationTelemetry.d.ts +14 -0
  39. package/dist/http/operationTelemetry.d.ts.map +1 -0
  40. package/dist/http/operationTelemetry.js +8 -0
  41. package/dist/http/operationTelemetry.js.map +1 -0
  42. package/dist/http/rest.d.ts +2 -1
  43. package/dist/http/rest.d.ts.map +1 -1
  44. package/dist/http/rest.js +3 -0
  45. package/dist/http/rest.js.map +1 -1
  46. package/package.json +6 -6
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { createApp } from './http/app.js';
16
16
  import { buildMcpServer } from './mcp/buildServer.js';
17
17
  import { runLoopbackFlow } from './accounts/googleAuth.js';
18
18
  import { runMicrosoftLoopbackFlow } from './accounts/microsoftAuth.js';
19
- import { prepareHostedGmailConnection, prepareHostedOutlookConnection, selectGmailConnectionMode, validateAccountConnectionFlags, } from './accounts/gmailConnection.js';
19
+ import { assertHostedConnectionReady, prepareHostedGmailConnection, prepareHostedOutlookConnection, selectGmailConnectionMode, validateAccountConnectionFlags, } from './accounts/gmailConnection.js';
20
20
  import { listApiKeys } from './storage/apiKeys.js';
21
21
  import { findMember } from './storage/members.js';
22
22
  import { LICENSE_KEY_PATTERN } from './licensing/client.js';
@@ -25,12 +25,13 @@ import { VERSION } from './version.js';
25
25
  import { inspectStoreCompatibility, MIN_SUPPORTED_STORE_FORMAT, openDb, openReadonlyDb, } from './storage/db.js';
26
26
  import { customPermissionPolicy, ADMIN_CAPABILITIES, FULL_PERMISSION_POLICY, isNamedPermissionProfile, MCP_CAPABILITIES, NAMED_PERMISSION_PROFILES, permissionPolicyForProfile, } from './permissions.js';
27
27
  import { captureOperation, getTelemetry, installTelemetryStreamEndHandler, isTelemetryEnabled, setTelemetryEnabled, shutdownTelemetry, telemetryDisabledInAnyEnvironment, } from './telemetry.js';
28
- import { clearSessionToken, instanceClient, loadInstanceConfig, removeInstance, resolveInstance, saveLocalInstance, saveRemoteInstance, saveSessionToken, useInstance, } from './cliInstances.js';
29
- import { authenticateBearer, normalizeAndValidatePassword, recoverAdminPassword, setupInitialAdmin } from './auth.js';
28
+ import { apiErrorCode, clearSessionToken, instanceClient, loadInstanceConfig, removeInstance, resolveInstance, saveLocalInstance, saveRemoteInstance, saveSessionToken, useInstance, } from './cliInstances.js';
29
+ import { authenticateBearer, isBootstrapComplete, normalizeAndValidatePassword, recoverAdminPassword, setupInitialAdmin, } from './auth.js';
30
30
  import { recordAdminAuditEvent } from './storage/adminAudit.js';
31
31
  import { canManageOwnedAccount } from './authorization.js';
32
32
  import { createCliUpdateNotifier } from './updateNotifier.js';
33
33
  import { registerMailCommands } from './cliMail.js';
34
+ import { accountInventoryProperties, configuredOAuthAppKind, connectionProperties } from './accounts/telemetry.js';
34
35
  import { flushLogging, escapeLogTextForConsole, getLogger, logCodedFailure, logFailure, readLocalLogs, shutdownLogging, } from './logging.js';
35
36
  function collectOption(value, previous) {
36
37
  return [...previous, value];
@@ -177,6 +178,8 @@ function readSecretFile(file) {
177
178
  async function secretInput(file, label) {
178
179
  return file === undefined ? hiddenPrompt(label) : readSecretFile(file);
179
180
  }
181
+ /** Offered when a configured public URL, not an explicit --hosted, chose the hosted flow. */
182
+ const LOOPBACK_FLOW_REMEDY = 'Pass --local to use the loopback flow on this computer instead.';
180
183
  function planLine(ent) {
181
184
  const members = `${ent.maxMembers} member${ent.maxMembers === 1 ? '' : 's'}`;
182
185
  return `${ent.plan} (up to ${ent.maxAccounts} mailboxes, ${members})`;
@@ -189,12 +192,45 @@ function warnLicense(db, log = console.error) {
189
192
  }
190
193
  const activeCliOperations = new WeakMap();
191
194
  const activeUpdateNotifiers = new WeakMap();
195
+ /** Attach safe, aggregate properties to the telemetry event for the running command. */
196
+ function recordCliOperationProperties(program, properties) {
197
+ const active = activeCliOperations.get(program);
198
+ if (active)
199
+ active.properties = { ...active.properties, ...properties };
200
+ }
201
+ /** Read local mailbox state without letting telemetry affect the command. */
202
+ async function inspectLocalRegistry(inspect) {
203
+ let context;
204
+ try {
205
+ context = createContext();
206
+ return inspect(context.registry);
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ finally {
212
+ if (context) {
213
+ try {
214
+ await context.registry.close();
215
+ }
216
+ catch {
217
+ // Telemetry cleanup must not affect the command.
218
+ }
219
+ try {
220
+ context.db.$client.close();
221
+ }
222
+ catch {
223
+ // Telemetry cleanup must not affect the command.
224
+ }
225
+ }
226
+ }
227
+ }
192
228
  function finishCliOperation(program, outcome, errorCode, error) {
193
229
  const active = activeCliOperations.get(program);
194
230
  if (!active)
195
231
  return;
196
232
  activeCliOperations.delete(program);
197
- if (outcome === 'error' && !active.errorLogged) {
233
+ if (outcome === 'error') {
198
234
  const context = { productSurface: 'cli', operation: active.operation, skipConsole: true };
199
235
  if (error !== undefined)
200
236
  logFailure(active.logger, 'cli.operation_failed', error, context);
@@ -207,18 +243,14 @@ function finishCliOperation(program, outcome, errorCode, error) {
207
243
  outcome,
208
244
  durationMs: performance.now() - active.startedAt,
209
245
  errorCode,
246
+ ...(active.properties ? { properties: active.properties } : {}),
210
247
  });
211
248
  }
212
- function logActiveCliError(program, error) {
213
- const active = activeCliOperations.get(program);
214
- if (!active || active.errorLogged)
215
- return;
216
- active.errorLogged = true;
217
- logFailure(active.logger, 'cli.operation_failed', error, {
218
- productSurface: 'cli',
219
- operation: active.operation,
220
- skipConsole: true,
221
- });
249
+ /** End a failed command with the error's own telemetry code instead of a generic one. */
250
+ function failCliOperation(program, error, prefix = 'Error: ') {
251
+ finishCliOperation(program, 'error', apiErrorCode(error), error);
252
+ console.error(`${prefix}${error instanceof Error ? error.message : String(error)}`);
253
+ process.exitCode = 1;
222
254
  }
223
255
  export function waitForServerListening(server) {
224
256
  return new Promise((resolve, reject) => {
@@ -390,9 +422,13 @@ export function createCliProgram(options = {}) {
390
422
  .option('--existing-admin <member>', 'Existing administrator id or email for a migrated instance')
391
423
  .action(async (opts) => {
392
424
  try {
425
+ const earlyContext = createContext();
426
+ if (isBootstrapComplete(earlyContext.db)) {
427
+ throw new EmailError('invalid_request', 'This instance has already been set up. Run "fluxmail --instance local login" to log in to the existing instance.');
428
+ }
393
429
  const name = opts.name ?? (opts.existingAdmin ? undefined : await textPrompt('Administrator name'));
394
430
  const email = opts.email ?? (await textPrompt('Administrator email'));
395
- const context = createContext();
431
+ const context = earlyContext;
396
432
  const passwordMember = opts.existingAdmin
397
433
  ? { ...findMember(context.db, opts.existingAdmin), email }
398
434
  : { name: name ?? '', email };
@@ -415,9 +451,7 @@ export function createCliProgram(options = {}) {
415
451
  console.log(`Fluxmail is ready. Logged in to local as ${result.member.name}.`);
416
452
  }
417
453
  catch (err) {
418
- logActiveCliError(program, err);
419
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
420
- process.exitCode = 1;
454
+ failCliOperation(program, err);
421
455
  }
422
456
  });
423
457
  program
@@ -433,7 +467,10 @@ export function createCliProgram(options = {}) {
433
467
  throw new EmailError('invalid_request', '--enroll and --reset cannot be used together.');
434
468
  }
435
469
  const requestedInstance = selectedInstance();
436
- if (!opts.server && requestedInstance === 'local' && !loadInstanceConfig().instances.local) {
470
+ const instanceConfig = loadInstanceConfig();
471
+ if (!opts.server &&
472
+ !instanceConfig.instances.local &&
473
+ (requestedInstance === 'local' || (!requestedInstance && Object.keys(instanceConfig.instances).length === 0))) {
437
474
  saveLocalInstance('local');
438
475
  }
439
476
  const instanceName = requestedInstance ?? (opts.server ? 'remote' : resolveInstance().name);
@@ -465,9 +502,7 @@ export function createCliProgram(options = {}) {
465
502
  console.log(`Logged in to ${instanceName} as ${result.member.name}.`);
466
503
  }
467
504
  catch (err) {
468
- logActiveCliError(program, err);
469
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
470
- process.exitCode = 1;
505
+ failCliOperation(program, err);
471
506
  }
472
507
  });
473
508
  program
@@ -484,9 +519,7 @@ export function createCliProgram(options = {}) {
484
519
  console.log(`Logged out of ${selected.name}.`);
485
520
  }
486
521
  catch (err) {
487
- logActiveCliError(program, err);
488
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
489
- process.exitCode = 1;
522
+ failCliOperation(program, err);
490
523
  }
491
524
  });
492
525
  const instances = program.command('instances').description('Manage local and remote CLI instances');
@@ -495,8 +528,18 @@ export function createCliProgram(options = {}) {
495
528
  .description('List configured instances')
496
529
  .action(() => {
497
530
  const config = loadInstanceConfig();
498
- for (const [name, profile] of Object.entries(config.instances)) {
499
- console.log(`${name}${config.active === name ? ' *' : ''} ${profile.kind}${profile.kind === 'remote' ? ` ${profile.serverUrl}` : ''}`);
531
+ const rows = Object.entries(config.instances).map(([name, profile]) => ({
532
+ name,
533
+ active: config.active === name ? '*' : '',
534
+ kind: profile.kind,
535
+ url: profile.kind === 'remote' ? profile.serverUrl : '',
536
+ }));
537
+ const width = (values) => Math.max(...values.map((value) => value.length));
538
+ const nameWidth = width(['NAME', ...rows.map((row) => row.name)]);
539
+ const kindWidth = width(['KIND', ...rows.map((row) => row.kind)]);
540
+ console.log(`${'NAME'.padEnd(nameWidth)} ACTIVE ${'KIND'.padEnd(kindWidth)} URL`.trimEnd());
541
+ for (const row of rows) {
542
+ console.log(`${row.name.padEnd(nameWidth)} ${(row.active ? '*' : ' ').padEnd('ACTIVE'.length)} ${row.kind.padEnd(kindWidth)} ${row.url}`.trimEnd());
500
543
  }
501
544
  });
502
545
  instances
@@ -509,9 +552,7 @@ export function createCliProgram(options = {}) {
509
552
  console.log(`Using ${name}.`);
510
553
  }
511
554
  catch (err) {
512
- logActiveCliError(program, err);
513
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
514
- process.exitCode = 1;
555
+ failCliOperation(program, err);
515
556
  }
516
557
  });
517
558
  instances
@@ -524,9 +565,7 @@ export function createCliProgram(options = {}) {
524
565
  console.log(`Removed CLI instance ${name}. Server data was not changed.`);
525
566
  }
526
567
  catch (err) {
527
- logActiveCliError(program, err);
528
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
529
- process.exitCode = 1;
568
+ failCliOperation(program, err);
530
569
  }
531
570
  });
532
571
  const authCommand = program.command('auth').description('Manage interactive authentication');
@@ -554,9 +593,7 @@ export function createCliProgram(options = {}) {
554
593
  console.log(`Reset ${member.name}'s password and revoked existing sessions.`);
555
594
  }
556
595
  catch (err) {
557
- logActiveCliError(program, err);
558
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
559
- process.exitCode = 1;
596
+ failCliOperation(program, err);
560
597
  }
561
598
  });
562
599
  authCommand
@@ -570,9 +607,7 @@ export function createCliProgram(options = {}) {
570
607
  }
571
608
  }
572
609
  catch (err) {
573
- logActiveCliError(program, err);
574
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
575
- process.exitCode = 1;
610
+ failCliOperation(program, err);
576
611
  }
577
612
  });
578
613
  authCommand
@@ -587,9 +622,7 @@ export function createCliProgram(options = {}) {
587
622
  console.log(`Revoked ${sessionId}.`);
588
623
  }
589
624
  catch (err) {
590
- logActiveCliError(program, err);
591
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
592
- process.exitCode = 1;
625
+ failCliOperation(program, err);
593
626
  }
594
627
  });
595
628
  program
@@ -708,33 +741,54 @@ export function createCliProgram(options = {}) {
708
741
  return;
709
742
  }
710
743
  const selected = resolveInstance(selectedInstance());
744
+ // A remote instance reconnects an IMAP mailbox that matches the address,
745
+ // so without --reauthorize only the server knows which one this is.
746
+ const reauthorize = opts.reauthorize !== undefined
747
+ ? true
748
+ : selected.profile.kind === 'remote' && provider === 'imap'
749
+ ? undefined
750
+ : false;
751
+ recordCliOperationProperties(program, connectionProperties({ provider, reauthorize }));
711
752
  if (!selected.token) {
712
- console.error(`Error: Log in to instance "${selected.name}" before connecting a mailbox.`);
713
- process.exitCode = 1;
753
+ failCliOperation(program, new EmailError('permission_denied', `Log in to instance "${selected.name}" before connecting a mailbox.`));
714
754
  return;
715
755
  }
716
756
  try {
717
757
  validateAccountConnectionFlags(provider, opts);
718
758
  }
719
759
  catch (err) {
720
- logActiveCliError(program, err);
721
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
722
- process.exitCode = 1;
760
+ failCliOperation(program, err);
723
761
  return;
724
762
  }
725
763
  let useControlPlane = selected.profile.kind === 'remote' || provider === 'imap';
764
+ if (selected.profile.kind === 'remote' && provider !== 'imap') {
765
+ // A remote instance always completes OAuth through its own public URL.
766
+ recordCliOperationProperties(program, connectionProperties({ provider, reauthorize, flow: 'hosted' }));
767
+ }
726
768
  if (selected.profile.kind === 'local' && provider !== 'imap') {
727
769
  try {
728
- useControlPlane = selectGmailConnectionMode(createContext().config, opts) === 'hosted';
770
+ const { config } = createContext();
771
+ const mode = selectGmailConnectionMode(config, opts);
772
+ recordCliOperationProperties(program, connectionProperties({
773
+ provider,
774
+ reauthorize,
775
+ flow: mode === 'hosted' ? 'hosted' : 'loopback',
776
+ oauthApp: configuredOAuthAppKind(config, provider),
777
+ }));
778
+ // A configured public URL selects the hosted flow on its own, so report a
779
+ // missing OAuth app here instead of after a connection grant is minted.
780
+ if (mode === 'hosted') {
781
+ assertHostedConnectionReady(config, provider, opts.hosted ? undefined : LOOPBACK_FLOW_REMEDY);
782
+ }
783
+ useControlPlane = mode === 'hosted';
729
784
  }
730
785
  catch (err) {
731
- logActiveCliError(program, err);
732
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
733
- process.exitCode = 1;
786
+ failCliOperation(program, err);
734
787
  return;
735
788
  }
736
789
  }
737
790
  if (useControlPlane) {
791
+ let localImapReauthorize = reauthorize ?? false;
738
792
  try {
739
793
  if (selected.profile.kind === 'remote' && opts.local) {
740
794
  throw new EmailError('invalid_request', '--local is only available for the local instance.');
@@ -751,6 +805,10 @@ export function createCliProgram(options = {}) {
751
805
  throw new EmailError('invalid_request', '--imap-host is required for IMAP accounts.');
752
806
  if (!opts.smtpHost)
753
807
  throw new EmailError('invalid_request', '--smtp-host is required for IMAP accounts.');
808
+ if (selected.profile.kind === 'local' && !localImapReauthorize) {
809
+ localImapReauthorize =
810
+ (await inspectLocalRegistry((registry) => registry.listAccounts().some((account) => account.email.toLowerCase() === email.toLowerCase()))) ?? false;
811
+ }
754
812
  const imapPassword = await accountSecret(opts.imapPasswordEnv, 'IMAP password');
755
813
  const smtpPassword = opts.smtpPasswordEnv
756
814
  ? await accountSecret(opts.smtpPasswordEnv, 'SMTP password')
@@ -792,15 +850,20 @@ export function createCliProgram(options = {}) {
792
850
  if (result.connectionUrl)
793
851
  console.log(`Open this URL in your browser:\n\n ${result.connectionUrl}\n`);
794
852
  if (result.account) {
853
+ if (selected.profile.kind === 'local' && provider === 'imap') {
854
+ const inventory = await inspectLocalRegistry((registry) => accountInventoryProperties(registry));
855
+ recordCliOperationProperties(program, {
856
+ reauthorize: localImapReauthorize,
857
+ ...inventory,
858
+ });
859
+ }
795
860
  console.log(`Connected ${result.account.email} (account id: ${result.account.id})`);
796
861
  for (const warning of result.warnings ?? [])
797
862
  console.log(`Warning: ${warning.message}.`);
798
863
  }
799
864
  }
800
865
  catch (err) {
801
- logActiveCliError(program, err);
802
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
803
- process.exitCode = 1;
866
+ failCliOperation(program, err);
804
867
  }
805
868
  return;
806
869
  }
@@ -822,6 +885,7 @@ export function createCliProgram(options = {}) {
822
885
  if (!existing)
823
886
  ctx.registry.assertCanAddAccount();
824
887
  const access = { sharedWithAll: false, grantedMemberIds: [] };
888
+ const accountIdsBeforeConnection = new Set(ctx.registry.listAccounts().map((account) => account.id));
825
889
  const auditConnection = (operation, accountId) => {
826
890
  try {
827
891
  recordAdminAuditEvent(ctx.db, {
@@ -868,6 +932,10 @@ export function createCliProgram(options = {}) {
868
932
  return ctx.registry.addOutlookAccount(result.email, result.credentials, result.displayName, owner.id, existing?.id, access);
869
933
  });
870
934
  auditConnection('account.connection.complete', account.id);
935
+ recordCliOperationProperties(program, {
936
+ reauthorize: accountIdsBeforeConnection.has(account.id),
937
+ ...accountInventoryProperties(ctx.registry),
938
+ });
871
939
  console.log(`\nConnected ${account.email} (account id: ${account.id})` +
872
940
  (account.ownerMemberId === owner.id ? ` for member ${owner.name}` : ''));
873
941
  return;
@@ -903,6 +971,10 @@ export function createCliProgram(options = {}) {
903
971
  return ctx.registry.addGmailAccount(result.email, result.tokens, result.displayName, owner.id, access, result.oauthClient);
904
972
  });
905
973
  auditConnection('account.connection.complete', account.id);
974
+ recordCliOperationProperties(program, {
975
+ reauthorize: accountIdsBeforeConnection.has(account.id),
976
+ ...accountInventoryProperties(ctx.registry),
977
+ });
906
978
  console.log(`\nConnected ${account.email} (account id: ${account.id})` +
907
979
  (account.ownerMemberId === owner.id ? ` for member ${owner.name}` : ''));
908
980
  if (account.ownerMemberId !== owner.id) {
@@ -912,9 +984,7 @@ export function createCliProgram(options = {}) {
912
984
  }
913
985
  }
914
986
  catch (err) {
915
- finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal', err);
916
- console.error(`\nError: ${err instanceof Error ? err.message : String(err)}`);
917
- process.exitCode = 1;
987
+ failCliOperation(program, err, '\nError: ');
918
988
  }
919
989
  });
920
990
  accounts
@@ -953,9 +1023,7 @@ export function createCliProgram(options = {}) {
953
1023
  console.log(`Warning: ${warning.message}.`);
954
1024
  }
955
1025
  catch (err) {
956
- logActiveCliError(program, err);
957
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
958
- process.exitCode = 1;
1026
+ failCliOperation(program, err);
959
1027
  }
960
1028
  });
961
1029
  accounts
@@ -978,9 +1046,7 @@ export function createCliProgram(options = {}) {
978
1046
  }
979
1047
  }
980
1048
  catch (err) {
981
- logActiveCliError(program, err);
982
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
983
- process.exitCode = 1;
1049
+ failCliOperation(program, err);
984
1050
  }
985
1051
  });
986
1052
  accounts
@@ -989,15 +1055,24 @@ export function createCliProgram(options = {}) {
989
1055
  .description('Disconnect an account and delete its stored tokens')
990
1056
  .action(async (accountId) => {
991
1057
  try {
992
- await instanceClient(selectedInstance()).json(`/api/v1/accounts/${encodeURIComponent(accountId)}/connection`, {
1058
+ const selected = resolveInstance(selectedInstance());
1059
+ const provider = selected.profile.kind === 'local'
1060
+ ? await inspectLocalRegistry((registry) => registry.getAccount(accountId).provider)
1061
+ : undefined;
1062
+ await instanceClient(selected.name).json(`/api/v1/accounts/${encodeURIComponent(accountId)}/connection`, {
993
1063
  method: 'DELETE',
994
1064
  });
1065
+ if (selected.profile.kind === 'local') {
1066
+ const inventory = await inspectLocalRegistry((registry) => accountInventoryProperties(registry));
1067
+ recordCliOperationProperties(program, {
1068
+ ...(provider ? { provider } : {}),
1069
+ ...inventory,
1070
+ });
1071
+ }
995
1072
  console.log(`Removed ${accountId}`);
996
1073
  }
997
1074
  catch (err) {
998
- logActiveCliError(program, err);
999
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1000
- process.exitCode = 1;
1075
+ failCliOperation(program, err);
1001
1076
  }
1002
1077
  });
1003
1078
  accounts
@@ -1015,9 +1090,7 @@ export function createCliProgram(options = {}) {
1015
1090
  console.log(`${account.email} is now owned by ${account.ownerMemberId}.`);
1016
1091
  }
1017
1092
  catch (err) {
1018
- logActiveCliError(program, err);
1019
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1020
- process.exitCode = 1;
1093
+ failCliOperation(program, err);
1021
1094
  }
1022
1095
  });
1023
1096
  accounts
@@ -1041,9 +1114,7 @@ export function createCliProgram(options = {}) {
1041
1114
  console.log(`Updated access for ${account.email}: ${account.sharedWithAll ? 'all members' : `${account.grantedMemberIds.length} explicit grant(s)`}.`);
1042
1115
  }
1043
1116
  catch (err) {
1044
- logActiveCliError(program, err);
1045
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1046
- process.exitCode = 1;
1117
+ failCliOperation(program, err);
1047
1118
  }
1048
1119
  });
1049
1120
  const membersCmd = program.command('members').description('Manage members (people using this instance)');
@@ -1068,9 +1139,7 @@ export function createCliProgram(options = {}) {
1068
1139
  console.log(`Expires: ${new Date(member.invitation.expiresAt).toISOString()}`);
1069
1140
  }
1070
1141
  catch (err) {
1071
- logActiveCliError(program, err);
1072
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1073
- process.exitCode = 1;
1142
+ failCliOperation(program, err);
1074
1143
  }
1075
1144
  });
1076
1145
  membersCmd
@@ -1088,9 +1157,7 @@ export function createCliProgram(options = {}) {
1088
1157
  }
1089
1158
  }
1090
1159
  catch (err) {
1091
- logActiveCliError(program, err);
1092
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1093
- process.exitCode = 1;
1160
+ failCliOperation(program, err);
1094
1161
  }
1095
1162
  });
1096
1163
  membersCmd
@@ -1105,9 +1172,7 @@ export function createCliProgram(options = {}) {
1105
1172
  console.log(`Removed ${memberId}.`);
1106
1173
  }
1107
1174
  catch (err) {
1108
- logActiveCliError(program, err);
1109
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1110
- process.exitCode = 1;
1175
+ failCliOperation(program, err);
1111
1176
  }
1112
1177
  });
1113
1178
  membersCmd
@@ -1128,9 +1193,7 @@ export function createCliProgram(options = {}) {
1128
1193
  console.log(`${member.name} now has the ${member.role} role.`);
1129
1194
  }
1130
1195
  catch (err) {
1131
- logActiveCliError(program, err);
1132
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1133
- process.exitCode = 1;
1196
+ failCliOperation(program, err);
1134
1197
  }
1135
1198
  });
1136
1199
  membersCmd
@@ -1151,9 +1214,7 @@ export function createCliProgram(options = {}) {
1151
1214
  console.log(`${member.name} is now ${member.status}.`);
1152
1215
  }
1153
1216
  catch (err) {
1154
- logActiveCliError(program, err);
1155
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1156
- process.exitCode = 1;
1217
+ failCliOperation(program, err);
1157
1218
  }
1158
1219
  });
1159
1220
  for (const [commandName, endpoint, label] of [
@@ -1171,9 +1232,7 @@ export function createCliProgram(options = {}) {
1171
1232
  console.log(`Expires: ${new Date(token.expiresAt).toISOString()}`);
1172
1233
  }
1173
1234
  catch (err) {
1174
- logActiveCliError(program, err);
1175
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1176
- process.exitCode = 1;
1235
+ failCliOperation(program, err);
1177
1236
  }
1178
1237
  });
1179
1238
  }
@@ -1189,9 +1248,7 @@ export function createCliProgram(options = {}) {
1189
1248
  }
1190
1249
  }
1191
1250
  catch (err) {
1192
- logActiveCliError(program, err);
1193
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1194
- process.exitCode = 1;
1251
+ failCliOperation(program, err);
1195
1252
  }
1196
1253
  });
1197
1254
  membersCmd
@@ -1205,9 +1262,7 @@ export function createCliProgram(options = {}) {
1205
1262
  console.log(`Revoked ${sessionId}.`);
1206
1263
  }
1207
1264
  catch (err) {
1208
- logActiveCliError(program, err);
1209
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1210
- process.exitCode = 1;
1265
+ failCliOperation(program, err);
1211
1266
  }
1212
1267
  });
1213
1268
  const apikey = program.command('apikey').description('Manage API keys for the HTTP MCP and REST APIs');
@@ -1262,9 +1317,7 @@ export function createCliProgram(options = {}) {
1262
1317
  console.log('Store it now; it cannot be shown again.');
1263
1318
  }
1264
1319
  catch (err) {
1265
- logActiveCliError(program, err);
1266
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1267
- process.exitCode = 1;
1320
+ failCliOperation(program, err);
1268
1321
  }
1269
1322
  });
1270
1323
  apikey
@@ -1289,9 +1342,7 @@ export function createCliProgram(options = {}) {
1289
1342
  }
1290
1343
  }
1291
1344
  catch (err) {
1292
- logActiveCliError(program, err);
1293
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1294
- process.exitCode = 1;
1345
+ failCliOperation(program, err);
1295
1346
  }
1296
1347
  });
1297
1348
  apikey
@@ -1316,9 +1367,7 @@ export function createCliProgram(options = {}) {
1316
1367
  : `Updated ${keyId} to ${accountIds.length} mailbox(es).`);
1317
1368
  }
1318
1369
  catch (err) {
1319
- logActiveCliError(program, err);
1320
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1321
- process.exitCode = 1;
1370
+ failCliOperation(program, err);
1322
1371
  }
1323
1372
  });
1324
1373
  apikey
@@ -1351,9 +1400,7 @@ export function createCliProgram(options = {}) {
1351
1400
  console.log(`Updated ${keyId} to profile ${permissions.profile}`);
1352
1401
  }
1353
1402
  catch (err) {
1354
- logActiveCliError(program, err);
1355
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1356
- process.exitCode = 1;
1403
+ failCliOperation(program, err);
1357
1404
  }
1358
1405
  });
1359
1406
  apikey
@@ -1371,9 +1418,7 @@ export function createCliProgram(options = {}) {
1371
1418
  console.log(`Revoked ${keyId}`);
1372
1419
  }
1373
1420
  catch (err) {
1374
- logActiveCliError(program, err);
1375
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1376
- process.exitCode = 1;
1421
+ failCliOperation(program, err);
1377
1422
  }
1378
1423
  });
1379
1424
  const license = program.command('license').description('Manage the paid-tier license');
@@ -1413,9 +1458,7 @@ export function createCliProgram(options = {}) {
1413
1458
  }
1414
1459
  }
1415
1460
  catch (err) {
1416
- logActiveCliError(program, err);
1417
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1418
- process.exitCode = 1;
1461
+ failCliOperation(program, err);
1419
1462
  }
1420
1463
  });
1421
1464
  license
@@ -1436,9 +1479,7 @@ export function createCliProgram(options = {}) {
1436
1479
  console.log(`Warning: ${status.warning}`);
1437
1480
  }
1438
1481
  catch (err) {
1439
- logActiveCliError(program, err);
1440
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1441
- process.exitCode = 1;
1482
+ failCliOperation(program, err);
1442
1483
  }
1443
1484
  });
1444
1485
  license
@@ -1453,9 +1494,7 @@ export function createCliProgram(options = {}) {
1453
1494
  console.log('This instance is back to Personal plan limits.');
1454
1495
  }
1455
1496
  catch (err) {
1456
- logActiveCliError(program, err);
1457
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1458
- process.exitCode = 1;
1497
+ failCliOperation(program, err);
1459
1498
  }
1460
1499
  });
1461
1500
  const configCmd = program.command('config').description('Inspect and initialize deployment configuration');
@@ -1470,9 +1509,7 @@ export function createCliProgram(options = {}) {
1470
1509
  console.log(`Created ${file}`);
1471
1510
  }
1472
1511
  catch (err) {
1473
- logActiveCliError(program, err);
1474
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1475
- process.exitCode = 1;
1512
+ failCliOperation(program, err);
1476
1513
  }
1477
1514
  });
1478
1515
  configCmd
@@ -1665,9 +1702,7 @@ export function createCliProgram(options = {}) {
1665
1702
  console.log(JSON.stringify(status, null, 2));
1666
1703
  }
1667
1704
  catch (err) {
1668
- logActiveCliError(program, err);
1669
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1670
- process.exitCode = 1;
1705
+ failCliOperation(program, err);
1671
1706
  }
1672
1707
  });
1673
1708
  return program;
@@ -1678,9 +1713,7 @@ export async function runCli(argv = process.argv) {
1678
1713
  await program.parseAsync([...argv]);
1679
1714
  }
1680
1715
  catch (err) {
1681
- finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal', err);
1682
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1683
- process.exitCode = 1;
1716
+ failCliOperation(program, err);
1684
1717
  await Promise.all([shutdownTelemetry(), shutdownLogging()]);
1685
1718
  }
1686
1719
  }