@essentialai/cogent-plugin 3.16.0 → 3.17.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogent",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "Inter-session communication bridge for Claude Code with Slack integration. Enables CC agents and Slack team members to communicate in real time.",
5
5
  "author": {
6
6
  "name": "Essential AI Solutions",
@@ -26389,8 +26389,8 @@ var init_stdio2 = __esm({
26389
26389
  // src/constants.ts
26390
26390
  import { createRequire } from "node:module";
26391
26391
  function resolveVersion() {
26392
- if ("3.16.0") {
26393
- return "3.16.0";
26392
+ if ("3.17.0") {
26393
+ return "3.17.0";
26394
26394
  }
26395
26395
  try {
26396
26396
  const require2 = createRequire(import.meta.url);
@@ -26679,7 +26679,13 @@ var init_config = __esm({
26679
26679
  // "human-broadcast" (toPeerId === "*" from a human origin: slack/gchat/web).
26680
26680
  // Default both. Agent-origin broadcasts are never auto-surfaced by C (storm-safe;
26681
26681
  // COGENT_BROADCAST_AWARENESS already handles seeing peer broadcasts).
26682
- COGENT_CHECK_ON_STOP_SCOPE: import_zod2.z.string().default("directed,human-broadcast")
26682
+ COGENT_CHECK_ON_STOP_SCOPE: import_zod2.z.string().default("directed,human-broadcast"),
26683
+ // Cogent Mail F4 — the mailbox domains an agent may configure + send to. CSV; default is the
26684
+ // single Cogent mail domain. Cogent mail is ORGANISATION-ONLY (compliance: an accountable org
26685
+ // owns every mailbox), so BYO external / corporate-domain mailboxes are BLOCKED for now — this
26686
+ // allowlist is the configurable seam for adding per-org corporate domains in a future release.
26687
+ // Baked default here (a LIBRARY default, not plugin env) so Team agents get it too.
26688
+ COGENT_MAIL_ALLOWED_DOMAINS: import_zod2.z.string().default("mail.cogent.tools")
26683
26689
  });
26684
26690
  _config = null;
26685
26691
  }
@@ -27463,8 +27469,8 @@ var init_file_backend = __esm({
27463
27469
  threadId
27464
27470
  );
27465
27471
  }
27466
- deregisterPeer(peerId) {
27467
- return deregisterPeer(peerId);
27472
+ async deregisterPeer(peerId) {
27473
+ return { removed: await deregisterPeer(peerId), mailboxDeprovisioned: false };
27468
27474
  }
27469
27475
  getPeer(peerId) {
27470
27476
  return getPeer(peerId);
@@ -27531,7 +27537,7 @@ var init_relay_version_cache = __esm({
27531
27537
  });
27532
27538
 
27533
27539
  // src/backend/http-backend.ts
27534
- var PATHS, HttpBackend;
27540
+ var MAILBOX_DEPROVISIONED_HEADER, PATHS, HttpBackend;
27535
27541
  var init_http_backend = __esm({
27536
27542
  "src/backend/http-backend.ts"() {
27537
27543
  "use strict";
@@ -27539,6 +27545,7 @@ var init_http_backend = __esm({
27539
27545
  init_constants();
27540
27546
  init_config();
27541
27547
  init_relay_version_cache();
27548
+ MAILBOX_DEPROVISIONED_HEADER = "X-Cogent-Mailbox-Deprovisioned";
27542
27549
  PATHS = {
27543
27550
  peers: "/api/sessions/:sessionId/peers",
27544
27551
  peer: "/api/sessions/:sessionId/peers/:peerId",
@@ -27596,15 +27603,19 @@ var init_http_backend = __esm({
27596
27603
  * Deregister a peer from the cloud session.
27597
27604
  * DELETE /api/sessions/:sessionId/peers/:peerId
27598
27605
  *
27599
- * Returns true on success, false if the peer was not found.
27606
+ * Returns `{ removed, mailboxDeprovisioned }`. `mailboxDeprovisioned` reflects the relay's
27607
+ * `X-Cogent-Mailbox-Deprovisioned` response header (Cogent Mail F3) — true ONLY when the relay
27608
+ * confirmed the Team mailbox was reaped, so the deregister-peer tool can clear the now-dead
27609
+ * local mail creds. A 404 (peer not found) resolves to `{ removed: false, ... }`, not a throw.
27600
27610
  */
27601
27611
  async deregisterPeer(peerId) {
27602
27612
  const path16 = PATHS.peer.replace(":sessionId", this.sessionId).replace(":peerId", peerId);
27603
27613
  try {
27604
- await this.http.delete(path16, {});
27605
- return true;
27614
+ const headers = await this.http.delete(path16, {});
27615
+ const mailboxDeprovisioned = headers.get(MAILBOX_DEPROVISIONED_HEADER) === "true";
27616
+ return { removed: true, mailboxDeprovisioned };
27606
27617
  } catch {
27607
- return false;
27618
+ return { removed: false, mailboxDeprovisioned: false };
27608
27619
  }
27609
27620
  }
27610
27621
  /**
@@ -27651,6 +27662,9 @@ var init_http_backend = __esm({
27651
27662
  if (record2.isRelayEcho === true) {
27652
27663
  body.isRelayEcho = true;
27653
27664
  }
27665
+ if (record2.attachments && record2.attachments.length > 0) {
27666
+ body.attachments = record2.attachments;
27667
+ }
27654
27668
  const resp = await this.http.post(path16, body);
27655
27669
  return {
27656
27670
  ...record2,
@@ -27890,7 +27904,9 @@ var init_http_client = __esm({
27890
27904
  }
27891
27905
  /**
27892
27906
  * Perform an authenticated DELETE request.
27893
- * Returns void; ignores 204 No Content responses.
27907
+ * Returns the RESPONSE HEADERS (a 204 carries no body, but the relay conveys
27908
+ * out-of-band signals — e.g. Cogent Mail F3's `X-Cogent-Mailbox-Deprovisioned` —
27909
+ * via headers, so callers that need them can read them without a body).
27894
27910
  *
27895
27911
  * When no body is provided, Content-Type is omitted so servers don't try
27896
27912
  * to parse an empty payload as JSON. This was the root cause of a
@@ -27913,6 +27929,7 @@ var init_http_client = __esm({
27913
27929
  if (!resp.ok && resp.status !== 204) {
27914
27930
  await this.throwMappedError(resp);
27915
27931
  }
27932
+ return resp.headers;
27916
27933
  }
27917
27934
  /**
27918
27935
  * Update the Bearer token (e.g., after join-session returns a new token).
@@ -32514,27 +32531,40 @@ function parseStatusNotice(message) {
32514
32531
  const m = STATUS_MARKER_RE.exec(message);
32515
32532
  return m ? m[1] : null;
32516
32533
  }
32517
- function formatRelayMessage(fromLabel, fromPeerId, message) {
32534
+ function formatAttachmentsBlock(attachments) {
32535
+ if (!attachments || attachments.length === 0) return "";
32536
+ const lines = attachments.map((a) => {
32537
+ const name = a.name || a.mailMessageId || a.url || "file";
32538
+ const ref = a.mailMessageId ? `fetch via cogent_fetch_mail (${a.mailMessageId})` : a.url ? a.url : "no fetchable reference";
32539
+ const meta = [a.mimeType, a.size != null ? `${a.size}B` : null].filter(Boolean).join(", ");
32540
+ return ` [file: ${name}${meta ? ` (${meta})` : ""} \u2014 ${ref}]`;
32541
+ });
32542
+ return `
32543
+
32544
+ \u{1F4CE} Attachments (byte transport = email):
32545
+ ${lines.join("\n")}`;
32546
+ }
32547
+ function formatRelayMessage(fromLabel, fromPeerId, message, attachments) {
32518
32548
  return `[Cogent Bridge message from ${fromLabel} (${fromPeerId})]
32519
32549
 
32520
- ${message}
32550
+ ${message}${formatAttachmentsBlock(attachments)}
32521
32551
 
32522
32552
  ---
32523
32553
  Respond directly to the message above. Your entire response is delivered automatically to whoever should receive it \u2014 a broadcast question (to the whole channel) is answered to everyone; a direct message, to the sender. You do NOT need to \u2014 and should NOT \u2014 call cogent_send_message or any bridge tool to deliver it; just answer normally.`;
32524
32554
  }
32525
- function formatInjectOnlyMessage(fromLabel, fromPeerId, message) {
32555
+ function formatInjectOnlyMessage(fromLabel, fromPeerId, message, attachments) {
32526
32556
  return `[Cogent Bridge: Response from ${fromLabel} (${fromPeerId})]
32527
32557
 
32528
- ${message}
32558
+ ${message}${formatAttachmentsBlock(attachments)}
32529
32559
 
32530
32560
  ---
32531
32561
  This is the response to your earlier cogent_send_message. No reply will be relayed.
32532
32562
  To continue the conversation, use cogent_send_message again.`;
32533
32563
  }
32534
- function formatPeerBroadcastNotice(fromLabel, fromPeerId, message) {
32564
+ function formatPeerBroadcastNotice(fromLabel, fromPeerId, message, attachments) {
32535
32565
  return `[Cogent Bridge: broadcast from ${fromLabel} (${fromPeerId}) to the whole channel]
32536
32566
 
32537
- ${message}
32567
+ ${message}${formatAttachmentsBlock(attachments)}
32538
32568
 
32539
32569
  ---
32540
32570
  FYI ONLY \u2014 another agent addressed the whole channel. Take note for context, but do NOT reply and do NOT call any cogent_* tool in response. A human will drive any reply.`;
@@ -33076,7 +33106,7 @@ var init_auto_relay = __esm({
33076
33106
  }
33077
33107
  } catch {
33078
33108
  }
33079
- const formatted = formatRelayMessage(fromLabel, msg.fromPeerId, msg.message);
33109
+ const formatted = formatRelayMessage(fromLabel, msg.fromPeerId, msg.message, msg.attachments);
33080
33110
  logger.info(`Auto-relay: processing message from ${fromLabel} (${msg.fromPeerId})`);
33081
33111
  const traceId = msg.traceId ?? randomUUID2();
33082
33112
  const startMs = Date.now();
@@ -33234,7 +33264,7 @@ var init_auto_relay = __esm({
33234
33264
  }
33235
33265
  } catch {
33236
33266
  }
33237
- const formatted = formatInjectOnlyMessage(fromLabel, msg.fromPeerId, msg.message);
33267
+ const formatted = formatInjectOnlyMessage(fromLabel, msg.fromPeerId, msg.message, msg.attachments);
33238
33268
  try {
33239
33269
  await this.refreshSessionId();
33240
33270
  const result = await execRemote(
@@ -33269,7 +33299,7 @@ var init_auto_relay = __esm({
33269
33299
  }
33270
33300
  } catch {
33271
33301
  }
33272
- const formatted = formatPeerBroadcastNotice(fromLabel, msg.fromPeerId, msg.message);
33302
+ const formatted = formatPeerBroadcastNotice(fromLabel, msg.fromPeerId, msg.message, msg.attachments);
33273
33303
  try {
33274
33304
  await this.refreshSessionId();
33275
33305
  const result = await execRemote(
@@ -33964,6 +33994,14 @@ async function persistProvisionedMailbox(mailbox, credentialPath) {
33964
33994
  );
33965
33995
  return true;
33966
33996
  }
33997
+ async function clearMailCredentials(credentialPath) {
33998
+ const filePath = resolveMailCredentialPath(credentialPath);
33999
+ try {
34000
+ await fs12.unlink(filePath);
34001
+ } catch (err) {
34002
+ if (err.code !== "ENOENT") throw err;
34003
+ }
34004
+ }
33967
34005
  var MAIL_DEFAULT_HOST;
33968
34006
  var init_mail_credential_store = __esm({
33969
34007
  "src/cloud/mail-credential-store.ts"() {
@@ -34519,11 +34557,18 @@ var init_peer = __esm({
34519
34557
  });
34520
34558
 
34521
34559
  // cogent/dist/types/message.js
34522
- var import_zod6, MessageRecordSchema;
34560
+ var import_zod6, AttachmentSchema, MessageRecordSchema;
34523
34561
  var init_message = __esm({
34524
34562
  "cogent/dist/types/message.js"() {
34525
34563
  "use strict";
34526
34564
  import_zod6 = __toESM(require_zod(), 1);
34565
+ AttachmentSchema = import_zod6.z.object({
34566
+ url: import_zod6.z.string().optional().describe("Public/fetchable URL reference"),
34567
+ mailMessageId: import_zod6.z.string().optional().describe("IMAP message-id in the recipient mailbox"),
34568
+ name: import_zod6.z.string().optional().describe("Display name / filename"),
34569
+ mimeType: import_zod6.z.string().optional().describe("MIME type"),
34570
+ size: import_zod6.z.number().int().nonnegative().optional().describe("Size in bytes")
34571
+ }).describe("A file reference carried alongside a message (byte transport = email)");
34527
34572
  MessageRecordSchema = import_zod6.z.object({
34528
34573
  id: import_zod6.z.string().uuid().describe("Unique message identifier"),
34529
34574
  fromPeerId: import_zod6.z.string().min(1).describe("Sender peer ID"),
@@ -34534,7 +34579,8 @@ var init_message = __esm({
34534
34579
  durationMs: import_zod6.z.number().nullable().describe("Round-trip duration in ms"),
34535
34580
  success: import_zod6.z.boolean().describe("Whether delivery succeeded"),
34536
34581
  error: import_zod6.z.string().nullable().describe("Error message if delivery failed"),
34537
- originPlatform: import_zod6.z.enum(["cc", "codex", "slack", "gchat", "web", "gemini", "whatsapp", "telegram", "discord"]).optional().describe("Platform that originated this message")
34582
+ originPlatform: import_zod6.z.enum(["cc", "codex", "slack", "gchat", "web", "gemini", "whatsapp", "telegram", "discord"]).optional().describe("Platform that originated this message"),
34583
+ attachments: import_zod6.z.array(AttachmentSchema).optional().describe("Cogent Mail M2.5 \u2014 file references (byte transport = email)")
34538
34584
  });
34539
34585
  }
34540
34586
  });
@@ -35094,8 +35140,16 @@ function registerDeregisterPeerTool(server) {
35094
35140
  async ({ peerId }) => {
35095
35141
  try {
35096
35142
  const backend = getBackend();
35097
- const removed = await backend.deregisterPeer(peerId);
35143
+ const { removed, mailboxDeprovisioned } = await backend.deregisterPeer(peerId);
35098
35144
  if (removed) heartbeat.stop();
35145
+ if (mailboxDeprovisioned) {
35146
+ try {
35147
+ await clearMailCredentials();
35148
+ logger.info("cleared local mail credentials after mailbox deprovision", { peerId });
35149
+ } catch (err) {
35150
+ logger.warn("failed to clear local mail credentials after deprovision", { error: err });
35151
+ }
35152
+ }
35099
35153
  return successResult({
35100
35154
  success: removed,
35101
35155
  message: removed ? `Peer '${peerId}' deregistered` : `Peer '${peerId}' was not registered`
@@ -35116,6 +35170,7 @@ var init_deregister_peer = __esm({
35116
35170
  init_errors4();
35117
35171
  init_logger();
35118
35172
  init_heartbeat();
35173
+ init_mail_credential_store();
35119
35174
  }
35120
35175
  });
35121
35176
 
@@ -35501,12 +35556,36 @@ function assertValidEmail(value, field) {
35501
35556
  );
35502
35557
  }
35503
35558
  }
35504
- var EMAIL_RE;
35559
+ function parseAllowedMailDomains(csv) {
35560
+ const list = (csv ?? DEFAULT_MAIL_DOMAIN).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
35561
+ return list.length ? list : [DEFAULT_MAIL_DOMAIN];
35562
+ }
35563
+ function allowedMailDomains() {
35564
+ try {
35565
+ return parseAllowedMailDomains(getConfig().COGENT_MAIL_ALLOWED_DOMAINS);
35566
+ } catch {
35567
+ return parseAllowedMailDomains(process.env.COGENT_MAIL_ALLOWED_DOMAINS);
35568
+ }
35569
+ }
35570
+ function assertAllowedMailDomain(address, field) {
35571
+ const domain = address.split("@")[1]?.toLowerCase() ?? "";
35572
+ const allowed = allowedMailDomains();
35573
+ if (!allowed.includes(domain)) {
35574
+ throw new BridgeError(
35575
+ "INVALID_INPUT" /* INVALID_INPUT */,
35576
+ `${field} domain "${domain}" is not an allowed Cogent mail domain`,
35577
+ `Cogent mail is limited to: ${allowed.join(", ")}. External or corporate-domain mail is a future release.`
35578
+ );
35579
+ }
35580
+ }
35581
+ var EMAIL_RE, DEFAULT_MAIL_DOMAIN;
35505
35582
  var init_validate = __esm({
35506
35583
  "src/mail/validate.ts"() {
35507
35584
  "use strict";
35508
35585
  init_errors4();
35586
+ init_config();
35509
35587
  EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
35588
+ DEFAULT_MAIL_DOMAIN = "mail.cogent.tools";
35510
35589
  }
35511
35590
  });
35512
35591
 
@@ -35530,6 +35609,7 @@ function registerSetupMailTool(server) {
35530
35609
  async ({ address, password, imapHost, imapPort, smtpHost, smtpPort }) => {
35531
35610
  try {
35532
35611
  assertValidEmail(address, "address");
35612
+ assertAllowedMailDomain(address, "address");
35533
35613
  await saveMailCredentials({
35534
35614
  address,
35535
35615
  password,
@@ -35613,6 +35693,25 @@ var init_mail_sender = __esm({
35613
35693
  }
35614
35694
  });
35615
35695
 
35696
+ // src/mail/auth-error.ts
35697
+ function isMailAuthError(err) {
35698
+ if (!err || typeof err !== "object") return false;
35699
+ const e = err;
35700
+ if (e.authenticationFailed === true) return true;
35701
+ if (typeof e.serverResponseCode === "string" && e.serverResponseCode.toUpperCase() === "AUTHENTICATIONFAILED") return true;
35702
+ if (e.code === "EAUTH") return true;
35703
+ if (e.responseCode === 535) return true;
35704
+ return false;
35705
+ }
35706
+ function mailAuthErrorHint() {
35707
+ return "The mail server refused these credentials. The mailbox may be temporarily SUSPENDED by an admin (retry after it is resumed \u2014 your saved credentials will work again), or it was deleted (only then re-run cogent_setup_mail with a fresh address + password). Your local credentials are kept.";
35708
+ }
35709
+ var init_auth_error = __esm({
35710
+ "src/mail/auth-error.ts"() {
35711
+ "use strict";
35712
+ }
35713
+ });
35714
+
35616
35715
  // src/tools/send-mail.ts
35617
35716
  function registerSendMailTool(server, deps = {}) {
35618
35717
  const makeSender = deps.senderFactory ?? ((creds) => new NodemailerMailSender(creds));
@@ -35632,6 +35731,7 @@ function registerSendMailTool(server, deps = {}) {
35632
35731
  async ({ to, subject, body, attachments }) => {
35633
35732
  try {
35634
35733
  assertValidEmail(to, "to");
35734
+ assertAllowedMailDomain(to, "to");
35635
35735
  const creds = await loadMailCredentials();
35636
35736
  if (!creds) {
35637
35737
  return errorResult(
@@ -35650,6 +35750,10 @@ function registerSendMailTool(server, deps = {}) {
35650
35750
  });
35651
35751
  return successResult({ ...result, attachmentCount: attachments?.length ?? 0 });
35652
35752
  } catch (err) {
35753
+ if (isMailAuthError(err)) {
35754
+ const detail = err instanceof Error ? err.message : String(err);
35755
+ return errorResult(new BridgeError("INVALID_INPUT" /* INVALID_INPUT */, `Mailbox login was refused: ${detail}`, mailAuthErrorHint()));
35756
+ }
35653
35757
  return errorResult(err);
35654
35758
  }
35655
35759
  }
@@ -35664,6 +35768,7 @@ var init_send_mail = __esm({
35664
35768
  init_mail_credential_store();
35665
35769
  init_mail_sender();
35666
35770
  init_validate();
35771
+ init_auth_error();
35667
35772
  }
35668
35773
  });
35669
35774
 
@@ -35838,6 +35943,7 @@ function registerFetchMailTool(server, deps = {}) {
35838
35943
  )
35839
35944
  );
35840
35945
  }
35946
+ assertAllowedMailDomain(creds.address, "mailbox");
35841
35947
  const fetcher = makeFetcher(creds);
35842
35948
  if (action === "fetch") {
35843
35949
  if (uid === void 0) {
@@ -35851,6 +35957,10 @@ function registerFetchMailTool(server, deps = {}) {
35851
35957
  const messages = await fetcher.list({ unreadOnly, limit: limit ?? 25 });
35852
35958
  return successResult({ mailbox: creds.address, count: messages.length, messages });
35853
35959
  } catch (err) {
35960
+ if (isMailAuthError(err)) {
35961
+ const detail = err instanceof Error ? err.message : String(err);
35962
+ return errorResult(new BridgeError("INVALID_INPUT" /* INVALID_INPUT */, `Mailbox login was refused: ${detail}`, mailAuthErrorHint()));
35963
+ }
35854
35964
  return errorResult(err);
35855
35965
  }
35856
35966
  }
@@ -35864,10 +35974,97 @@ var init_fetch_mail = __esm({
35864
35974
  init_errors4();
35865
35975
  init_mail_credential_store();
35866
35976
  init_mail_fetcher();
35977
+ init_auth_error();
35978
+ init_validate();
35867
35979
  DEFAULT_DOWNLOAD_DIR = path15.join(os8.homedir(), ".cogent", "mail-downloads");
35868
35980
  }
35869
35981
  });
35870
35982
 
35983
+ // src/tools/rotate-mail-creds.ts
35984
+ function registerRotateMailCredsTool(server) {
35985
+ server.registerTool(
35986
+ "cogent_rotate_mail_creds",
35987
+ {
35988
+ title: "Rotate this agent's mailbox password",
35989
+ description: "Rotate (re-key) this agent's Team mailbox password on the mail server and update the local credential store, so cogent_send_mail / cogent_fetch_mail keep working with the fresh secret. Use it if the mailbox password may be compromised. Team channels only.",
35990
+ inputSchema: {},
35991
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
35992
+ },
35993
+ async () => {
35994
+ try {
35995
+ const creds = await loadCredentials();
35996
+ if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
35997
+ throw new BridgeError(
35998
+ "INVALID_INPUT" /* INVALID_INPUT */,
35999
+ "Not registered in cloud mode",
36000
+ "Rotation applies to a cloud Team channel \u2014 register the peer first"
36001
+ );
36002
+ }
36003
+ if (!creds.peerId) {
36004
+ throw new BridgeError(
36005
+ "INVALID_INPUT" /* INVALID_INPUT */,
36006
+ "Cannot identify this peer",
36007
+ "Re-register the peer so its peerId is stored, then rotate"
36008
+ );
36009
+ }
36010
+ const url = `${creds.endpoint.replace(/\/+$/, "")}/api/sessions/${encodeURIComponent(creds.sessionId)}/peers/${encodeURIComponent(creds.peerId)}/mail/rotate`;
36011
+ let res;
36012
+ try {
36013
+ res = await fetch(url, {
36014
+ method: "POST",
36015
+ headers: { "content-type": "application/json", authorization: `Bearer ${creds.token}` },
36016
+ body: "{}",
36017
+ signal: AbortSignal.timeout(ROTATE_TIMEOUT_MS)
36018
+ });
36019
+ } catch {
36020
+ throw new BridgeError("NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */, "Could not reach the relay to rotate the mailbox", "Check connectivity and try again");
36021
+ }
36022
+ if (!res.ok) {
36023
+ const detail = await res.text().catch(() => "");
36024
+ throw new BridgeError(
36025
+ "INVALID_INPUT" /* INVALID_INPUT */,
36026
+ `Mailbox rotation was rejected (HTTP ${res.status})`,
36027
+ detail.slice(0, 200) || "Rotation applies to Team channels with a provisioned mailbox"
36028
+ );
36029
+ }
36030
+ const data = await res.json().catch(() => null);
36031
+ if (!data || data.success !== true) {
36032
+ throw new BridgeError("NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */, "Mailbox rotation failed", "The relay returned an unexpected response");
36033
+ }
36034
+ if (!data.rotated) {
36035
+ return successResult({ rotated: false, reason: data.reason ?? "no_mailbox" });
36036
+ }
36037
+ if (!data.address || !data.password) {
36038
+ throw new BridgeError("NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */, "Rotation returned no new password", "Try again; the mailbox password was not updated locally");
36039
+ }
36040
+ const existing = await loadMailCredentials();
36041
+ await saveMailCredentials({
36042
+ address: data.address,
36043
+ password: data.password,
36044
+ imapHost: existing?.imapHost ?? MAIL_DEFAULT_HOST,
36045
+ imapPort: existing?.imapPort ?? 993,
36046
+ smtpHost: existing?.smtpHost ?? MAIL_DEFAULT_HOST,
36047
+ smtpPort: existing?.smtpPort ?? 465,
36048
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
36049
+ });
36050
+ return successResult({ rotated: true, address: data.address });
36051
+ } catch (err) {
36052
+ return errorResult(err);
36053
+ }
36054
+ }
36055
+ );
36056
+ }
36057
+ var ROTATE_TIMEOUT_MS;
36058
+ var init_rotate_mail_creds = __esm({
36059
+ "src/tools/rotate-mail-creds.ts"() {
36060
+ "use strict";
36061
+ init_errors4();
36062
+ init_credential_store();
36063
+ init_mail_credential_store();
36064
+ ROTATE_TIMEOUT_MS = 8e3;
36065
+ }
36066
+ });
36067
+
35871
36068
  // src/index.ts
35872
36069
  var index_exports = {};
35873
36070
  async function main() {
@@ -35884,6 +36081,7 @@ async function main() {
35884
36081
  registerSetupMailTool(server);
35885
36082
  registerSendMailTool(server);
35886
36083
  registerFetchMailTool(server);
36084
+ registerRotateMailCredsTool(server);
35887
36085
  if (cloudInbox) {
35888
36086
  server.registerResource(
35889
36087
  "cogent_inbox",
@@ -35946,6 +36144,7 @@ var init_index = __esm({
35946
36144
  init_setup_mail();
35947
36145
  init_send_mail();
35948
36146
  init_fetch_mail();
36147
+ init_rotate_mail_creds();
35949
36148
  init_heartbeat();
35950
36149
  process.on("uncaughtException", (err) => {
35951
36150
  if (err.code === "EPIPE") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "Cogent — Claude Code plugin (skills + slash-commands + MCP server) for the cross-agent comms fabric.",
5
5
  "author": { "name": "Essential AI Solutions Ltd.", "url": "https://essentialai.uk" },
6
6
  "homepage": "https://cogent.tools",