@essentialai/cogent-plugin 3.14.0 → 3.15.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.14.0",
3
+ "version": "3.15.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",
@@ -152,31 +152,33 @@ var init_detect = __esm({
152
152
  });
153
153
 
154
154
  // src/wizard/templates.ts
155
- function mcpJsonContent(npxPath) {
155
+ function cogentEnv(mode) {
156
+ const env = {
157
+ COGENT_LOG_LEVEL: "info",
158
+ COGENT_TIMEOUT_MS: "300000"
159
+ };
160
+ if (mode === "local") env.COGENT_LOCAL = "1";
161
+ return env;
162
+ }
163
+ function mcpJsonContent(npxPath, mode = "cloud") {
156
164
  const config2 = {
157
165
  mcpServers: {
158
166
  "cogent": {
159
167
  command: npxPath,
160
168
  args: ["-y", "@essentialai/cogent-bridge"],
161
- env: {
162
- COGENT_LOG_LEVEL: "info",
163
- COGENT_TIMEOUT_MS: "300000"
164
- }
169
+ env: cogentEnv(mode)
165
170
  }
166
171
  }
167
172
  };
168
173
  return JSON.stringify(config2, null, 2) + "\n";
169
174
  }
170
- function mcpJsonMerge(existing, npxPath) {
175
+ function mcpJsonMerge(existing, npxPath, mode = "cloud") {
171
176
  const parsed = JSON.parse(existing);
172
177
  if (!parsed.mcpServers) parsed.mcpServers = {};
173
178
  parsed.mcpServers["cogent"] = {
174
179
  command: npxPath,
175
180
  args: ["-y", "@essentialai/cogent-bridge"],
176
- env: {
177
- COGENT_LOG_LEVEL: "info",
178
- COGENT_TIMEOUT_MS: "300000"
179
- }
181
+ env: cogentEnv(mode)
180
182
  };
181
183
  return JSON.stringify(parsed, null, 2) + "\n";
182
184
  }
@@ -380,14 +382,14 @@ var init_scaffold_demo = __esm({
380
382
  // src/wizard/scaffold-real.ts
381
383
  import fs2 from "node:fs";
382
384
  import path2 from "node:path";
383
- function writeMcpJson(projectPath, npxPath) {
385
+ function writeMcpJson(projectPath, npxPath, mode) {
384
386
  const filePath = path2.join(projectPath, ".mcp.json");
385
387
  if (fs2.existsSync(filePath)) {
386
388
  const existing = fs2.readFileSync(filePath, "utf-8");
387
- fs2.writeFileSync(filePath, mcpJsonMerge(existing, npxPath), "utf-8");
389
+ fs2.writeFileSync(filePath, mcpJsonMerge(existing, npxPath, mode), "utf-8");
388
390
  return "modified";
389
391
  }
390
- fs2.writeFileSync(filePath, mcpJsonContent(npxPath), "utf-8");
392
+ fs2.writeFileSync(filePath, mcpJsonContent(npxPath, mode), "utf-8");
391
393
  return "created";
392
394
  }
393
395
  function writeClaudeMd(projectPath, peerId, label, otherPeerId) {
@@ -408,7 +410,8 @@ function scaffoldReal(config2) {
408
410
  const created = [];
409
411
  const modified = [];
410
412
  const skipped = [];
411
- const mcpA = writeMcpJson(config2.projectAPath, config2.npxPath);
413
+ const mode = config2.mode ?? "cloud";
414
+ const mcpA = writeMcpJson(config2.projectAPath, config2.npxPath, mode);
412
415
  const mcpAPath = path2.join(config2.projectAPath, ".mcp.json");
413
416
  if (mcpA === "created") created.push(mcpAPath);
414
417
  else modified.push(mcpAPath);
@@ -422,7 +425,7 @@ function scaffoldReal(config2) {
422
425
  if (claudeA === "created") created.push(claudeAPath);
423
426
  else if (claudeA === "modified") modified.push(claudeAPath);
424
427
  else skipped.push(claudeAPath);
425
- const mcpB = writeMcpJson(config2.projectBPath, config2.npxPath);
428
+ const mcpB = writeMcpJson(config2.projectBPath, config2.npxPath, mode);
426
429
  const mcpBPath = path2.join(config2.projectBPath, ".mcp.json");
427
430
  if (mcpB === "created") created.push(mcpBPath);
428
431
  else modified.push(mcpBPath);
@@ -565,6 +568,11 @@ async function runWizard() {
565
568
  const idB = await ask(rl, "Peer ID for Project B:", defaultIdB);
566
569
  const defaultLabelB = "CC_" + path3.basename(pathB).replace(/[^a-zA-Z0-9]/g, "_");
567
570
  const labelB = await ask(rl, "Label for Project B:", defaultLabelB);
571
+ const modeIdx = await choose(rl, "How should these agents connect?", [
572
+ "Cloud \u2014 collaborate with remote agents over cogent.tools (recommended)",
573
+ "Local \u2014 offline, this machine only (self-hosted / local-LLM / air-gapped)"
574
+ ]);
575
+ const mode2 = modeIdx === 1 ? "local" : "cloud";
568
576
  heading("Configuring projects");
569
577
  const result = scaffoldReal({
570
578
  projectAPath: path3.resolve(pathA),
@@ -573,7 +581,8 @@ async function runWizard() {
573
581
  projectBPath: path3.resolve(pathB),
574
582
  projectBId: idB,
575
583
  projectBLabel: labelB,
576
- npxPath
584
+ npxPath,
585
+ mode: mode2
577
586
  });
578
587
  printRealNextSteps(path3.resolve(pathA), idA, labelA, path3.resolve(pathB), idB, labelB, result);
579
588
  }
@@ -1250,8 +1259,8 @@ var init_parseUtil = __esm({
1250
1259
  init_errors();
1251
1260
  init_en();
1252
1261
  makeIssue = (params) => {
1253
- const { data, path: path12, errorMaps, issueData } = params;
1254
- const fullPath = [...path12, ...issueData.path || []];
1262
+ const { data, path: path16, errorMaps, issueData } = params;
1263
+ const fullPath = [...path16, ...issueData.path || []];
1255
1264
  const fullIssue = {
1256
1265
  ...issueData,
1257
1266
  path: fullPath
@@ -1531,11 +1540,11 @@ var init_types = __esm({
1531
1540
  init_parseUtil();
1532
1541
  init_util();
1533
1542
  ParseInputLazyPath = class {
1534
- constructor(parent, value, path12, key) {
1543
+ constructor(parent, value, path16, key) {
1535
1544
  this._cachedPath = [];
1536
1545
  this.parent = parent;
1537
1546
  this.data = value;
1538
- this._path = path12;
1547
+ this._path = path16;
1539
1548
  this._key = key;
1540
1549
  }
1541
1550
  get path() {
@@ -4987,10 +4996,10 @@ function assignProp(target, prop, value) {
4987
4996
  configurable: true
4988
4997
  });
4989
4998
  }
4990
- function getElementAtPath(obj, path12) {
4991
- if (!path12)
4999
+ function getElementAtPath(obj, path16) {
5000
+ if (!path16)
4992
5001
  return obj;
4993
- return path12.reduce((acc, key) => acc?.[key], obj);
5002
+ return path16.reduce((acc, key) => acc?.[key], obj);
4994
5003
  }
4995
5004
  function promiseAllObject(promisesObj) {
4996
5005
  const keys = Object.keys(promisesObj);
@@ -5239,11 +5248,11 @@ function aborted(x, startIndex = 0) {
5239
5248
  }
5240
5249
  return false;
5241
5250
  }
5242
- function prefixIssues(path12, issues) {
5251
+ function prefixIssues(path16, issues) {
5243
5252
  return issues.map((iss) => {
5244
5253
  var _a;
5245
5254
  (_a = iss).path ?? (_a.path = []);
5246
- iss.path.unshift(path12);
5255
+ iss.path.unshift(path16);
5247
5256
  return iss;
5248
5257
  });
5249
5258
  }
@@ -17049,8 +17058,8 @@ var require_utils = __commonJS({
17049
17058
  }
17050
17059
  return ind;
17051
17060
  }
17052
- function removeDotSegments(path12) {
17053
- let input = path12;
17061
+ function removeDotSegments(path16) {
17062
+ let input = path16;
17054
17063
  const output = [];
17055
17064
  let nextSlash = -1;
17056
17065
  let len = 0;
@@ -17249,8 +17258,8 @@ var require_schemes = __commonJS({
17249
17258
  wsComponent.secure = void 0;
17250
17259
  }
17251
17260
  if (wsComponent.resourceName) {
17252
- const [path12, query] = wsComponent.resourceName.split("?");
17253
- wsComponent.path = path12 && path12 !== "/" ? path12 : void 0;
17261
+ const [path16, query] = wsComponent.resourceName.split("?");
17262
+ wsComponent.path = path16 && path16 !== "/" ? path16 : void 0;
17254
17263
  wsComponent.query = query;
17255
17264
  wsComponent.resourceName = void 0;
17256
17265
  }
@@ -20603,12 +20612,12 @@ var require_dist = __commonJS({
20603
20612
  throw new Error(`Unknown format "${name}"`);
20604
20613
  return f;
20605
20614
  };
20606
- function addFormats(ajv, list, fs12, exportName) {
20615
+ function addFormats(ajv, list, fs15, exportName) {
20607
20616
  var _a;
20608
20617
  var _b;
20609
20618
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
20610
20619
  for (const f of list)
20611
- ajv.addFormat(f, fs12[f]);
20620
+ ajv.addFormat(f, fs15[f]);
20612
20621
  }
20613
20622
  module.exports = exports = formatsPlugin;
20614
20623
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -21720,8 +21729,8 @@ var require_parseUtil = __commonJS({
21720
21729
  var errors_js_1 = require_errors2();
21721
21730
  var en_js_1 = __importDefault(require_en());
21722
21731
  var makeIssue2 = (params) => {
21723
- const { data, path: path12, errorMaps, issueData } = params;
21724
- const fullPath = [...path12, ...issueData.path || []];
21732
+ const { data, path: path16, errorMaps, issueData } = params;
21733
+ const fullPath = [...path16, ...issueData.path || []];
21725
21734
  const fullIssue = {
21726
21735
  ...issueData,
21727
21736
  path: fullPath
@@ -21875,11 +21884,11 @@ var require_types2 = __commonJS({
21875
21884
  var parseUtil_js_1 = require_parseUtil();
21876
21885
  var util_js_1 = require_util2();
21877
21886
  var ParseInputLazyPath2 = class {
21878
- constructor(parent, value, path12, key) {
21887
+ constructor(parent, value, path16, key) {
21879
21888
  this._cachedPath = [];
21880
21889
  this.parent = parent;
21881
21890
  this.data = value;
21882
- this._path = path12;
21891
+ this._path = path16;
21883
21892
  this._key = key;
21884
21893
  }
21885
21894
  get path() {
@@ -25468,10 +25477,10 @@ var require_zod = __commonJS({
25468
25477
  };
25469
25478
  Object.defineProperty(exports, "__esModule", { value: true });
25470
25479
  exports.z = void 0;
25471
- var z15 = __importStar(require_external());
25472
- exports.z = z15;
25480
+ var z18 = __importStar(require_external());
25481
+ exports.z = z18;
25473
25482
  __exportStar(require_external(), exports);
25474
- exports.default = z15;
25483
+ exports.default = z18;
25475
25484
  }
25476
25485
  });
25477
25486
 
@@ -26380,8 +26389,8 @@ var init_stdio2 = __esm({
26380
26389
  // src/constants.ts
26381
26390
  import { createRequire } from "node:module";
26382
26391
  function resolveVersion() {
26383
- if ("3.14.0") {
26384
- return "3.14.0";
26392
+ if ("3.15.0") {
26393
+ return "3.15.0";
26385
26394
  }
26386
26395
  try {
26387
26396
  const require2 = createRequire(import.meta.url);
@@ -27553,7 +27562,7 @@ var init_http_backend = __esm({
27553
27562
  * in cloud mode we ignore it and use this.sessionId (the cloud session).
27554
27563
  */
27555
27564
  async registerPeer(peerId, _sessionId, cwd, label, clientVersion, mode, _channelSessionId, capabilities, workspaceId, threadId) {
27556
- const path12 = PATHS.peers.replace(":sessionId", this.sessionId);
27565
+ const path16 = PATHS.peers.replace(":sessionId", this.sessionId);
27557
27566
  const body = {
27558
27567
  peerId,
27559
27568
  cwd,
@@ -27581,7 +27590,7 @@ var init_http_backend = __esm({
27581
27590
  body.role = roleCap.slice("role:".length);
27582
27591
  }
27583
27592
  }
27584
- return this.http.post(path12, body);
27593
+ return this.http.post(path16, body);
27585
27594
  }
27586
27595
  /**
27587
27596
  * Deregister a peer from the cloud session.
@@ -27590,9 +27599,9 @@ var init_http_backend = __esm({
27590
27599
  * Returns true on success, false if the peer was not found.
27591
27600
  */
27592
27601
  async deregisterPeer(peerId) {
27593
- const path12 = PATHS.peer.replace(":sessionId", this.sessionId).replace(":peerId", peerId);
27602
+ const path16 = PATHS.peer.replace(":sessionId", this.sessionId).replace(":peerId", peerId);
27594
27603
  try {
27595
- await this.http.delete(path12, {});
27604
+ await this.http.delete(path16, {});
27596
27605
  return true;
27597
27606
  } catch {
27598
27607
  return false;
@@ -27612,8 +27621,8 @@ var init_http_backend = __esm({
27612
27621
  * GET /api/sessions/:sessionId/peers
27613
27622
  */
27614
27623
  async listPeers() {
27615
- const path12 = PATHS.peers.replace(":sessionId", this.sessionId);
27616
- const result = await this.http.get(path12);
27624
+ const path16 = PATHS.peers.replace(":sessionId", this.sessionId);
27625
+ const result = await this.http.get(path16);
27617
27626
  return result.peers;
27618
27627
  }
27619
27628
  /**
@@ -27621,8 +27630,8 @@ var init_http_backend = __esm({
27621
27630
  * Calls the lightweight heartbeat endpoint on the cloud relay server.
27622
27631
  */
27623
27632
  async updateLastSeen(peerId) {
27624
- const path12 = PATHS.heartbeat.replace(":sessionId", this.sessionId);
27625
- await this.http.post(path12, { peerId });
27633
+ const path16 = PATHS.heartbeat.replace(":sessionId", this.sessionId);
27634
+ await this.http.post(path16, { peerId });
27626
27635
  }
27627
27636
  /**
27628
27637
  * Record a message in the cloud session.
@@ -27633,7 +27642,7 @@ var init_http_backend = __esm({
27633
27642
  * the input record to construct a full MessageRecord.
27634
27643
  */
27635
27644
  async recordMessage(record2) {
27636
- const path12 = PATHS.messages.replace(":sessionId", this.sessionId);
27645
+ const path16 = PATHS.messages.replace(":sessionId", this.sessionId);
27637
27646
  const body = {
27638
27647
  fromPeerId: record2.fromPeerId,
27639
27648
  toPeerId: record2.toPeerId,
@@ -27642,7 +27651,7 @@ var init_http_backend = __esm({
27642
27651
  if (record2.isRelayEcho === true) {
27643
27652
  body.isRelayEcho = true;
27644
27653
  }
27645
- const resp = await this.http.post(path12, body);
27654
+ const resp = await this.http.post(path16, body);
27646
27655
  return {
27647
27656
  ...record2,
27648
27657
  id: resp.id,
@@ -27656,7 +27665,7 @@ var init_http_backend = __esm({
27656
27665
  * Supports optional peerId filter and limit (default 300).
27657
27666
  */
27658
27667
  async getHistory(peerId, limit, includeRelayEchoes, metadataOnly, dedupInferredEchoes) {
27659
- const path12 = PATHS.messages.replace(":sessionId", this.sessionId);
27668
+ const path16 = PATHS.messages.replace(":sessionId", this.sessionId);
27660
27669
  const query = {};
27661
27670
  if (peerId) {
27662
27671
  query.peerId = peerId;
@@ -27671,7 +27680,7 @@ var init_http_backend = __esm({
27671
27680
  if (dedupInferredEchoes) {
27672
27681
  query.dedupInferredEchoes = "true";
27673
27682
  }
27674
- const result = await this.http.get(path12, query);
27683
+ const result = await this.http.get(path16, query);
27675
27684
  return result.messages;
27676
27685
  }
27677
27686
  /**
@@ -27682,12 +27691,12 @@ var init_http_backend = __esm({
27682
27691
  * so that get-history shows the actual response (not null).
27683
27692
  */
27684
27693
  async updateMessageResponse(messageId, response, durationMs, pending) {
27685
- const path12 = PATHS.message.replace(":sessionId", this.sessionId).replace(":messageId", messageId);
27694
+ const path16 = PATHS.message.replace(":sessionId", this.sessionId).replace(":messageId", messageId);
27686
27695
  const body = { response, durationMs };
27687
27696
  if (pending === true) {
27688
27697
  body.pending = true;
27689
27698
  }
27690
- await this.http.patch(path12, body);
27699
+ await this.http.patch(path16, body);
27691
27700
  }
27692
27701
  /**
27693
27702
  * Validate a session.
@@ -27829,8 +27838,8 @@ var init_http_client = __esm({
27829
27838
  * Perform an authenticated GET request.
27830
27839
  * Appends query parameters to the URL if provided.
27831
27840
  */
27832
- async get(path12, query) {
27833
- const url = new URL(path12, this.baseUrl);
27841
+ async get(path16, query) {
27842
+ const url = new URL(path16, this.baseUrl);
27834
27843
  if (query) {
27835
27844
  for (const [k, v] of Object.entries(query)) {
27836
27845
  url.searchParams.set(k, v);
@@ -27845,8 +27854,8 @@ var init_http_client = __esm({
27845
27854
  /**
27846
27855
  * Perform an authenticated POST request with a JSON body.
27847
27856
  */
27848
- async post(path12, body) {
27849
- const url = new URL(path12, this.baseUrl);
27857
+ async post(path16, body) {
27858
+ const url = new URL(path16, this.baseUrl);
27850
27859
  const resp = await this.fetchWithTimeout(url, {
27851
27860
  method: "POST",
27852
27861
  headers: this.headers(),
@@ -27864,8 +27873,8 @@ var init_http_client = __esm({
27864
27873
  * Perform an authenticated PATCH request with a JSON body.
27865
27874
  * Used to update existing resources (e.g., message response fields).
27866
27875
  */
27867
- async patch(path12, body) {
27868
- const url = new URL(path12, this.baseUrl);
27876
+ async patch(path16, body) {
27877
+ const url = new URL(path16, this.baseUrl);
27869
27878
  const resp = await this.fetchWithTimeout(url, {
27870
27879
  method: "PATCH",
27871
27880
  headers: this.headers(),
@@ -27888,8 +27897,8 @@ var init_http_client = __esm({
27888
27897
  * "Malformed JSON in request body" 400 from cogent-server <=3.1.2 when
27889
27898
  * any client (including third-party tools) DELETEd a peer without a body.
27890
27899
  */
27891
- async delete(path12, body) {
27892
- const url = new URL(path12, this.baseUrl);
27900
+ async delete(path16, body) {
27901
+ const url = new URL(path16, this.baseUrl);
27893
27902
  const headers = {
27894
27903
  "Authorization": `Bearer ${this.token}`
27895
27904
  };
@@ -32068,14 +32077,14 @@ var init_ws_client = __esm({
32068
32077
  if (this.pollTimer) return;
32069
32078
  const poll = async () => {
32070
32079
  try {
32071
- const path12 = `/api/sessions/${this.opts.sessionId}/poll`;
32080
+ const path16 = `/api/sessions/${this.opts.sessionId}/poll`;
32072
32081
  const query = {
32073
32082
  peerId: this.opts.peerId
32074
32083
  };
32075
32084
  if (this.lastMessageId) {
32076
32085
  query.lastMessageId = this.lastMessageId;
32077
32086
  }
32078
- const result = await this.opts.http.get(path12, query);
32087
+ const result = await this.opts.http.get(path16, query);
32079
32088
  if (result.messages && result.messages.length > 0) {
32080
32089
  this.opts.onMessages(result.messages);
32081
32090
  this.lastMessageId = result.messages[result.messages.length - 1].id;
@@ -33873,8 +33882,61 @@ var init_startup = __esm({
33873
33882
  }
33874
33883
  });
33875
33884
 
33876
- // src/tools/register-peer.ts
33885
+ // src/cloud/mail-credential-store.ts
33877
33886
  import crypto3 from "node:crypto";
33887
+ import fs12 from "node:fs/promises";
33888
+ import path12 from "node:path";
33889
+ import os7 from "node:os";
33890
+ function defaultMailCredentialPath(cwd = process.cwd()) {
33891
+ const hash = crypto3.createHash("sha256").update(path12.resolve(cwd)).digest("hex").slice(0, 16);
33892
+ return path12.join(os7.homedir(), ".cogent", "mail-credentials", `${hash}.json`);
33893
+ }
33894
+ function resolveMailCredentialPath(credentialPath) {
33895
+ if (credentialPath) return credentialPath;
33896
+ const envOverride = process.env.COGENT_MAIL_CREDENTIALS_FILE;
33897
+ if (envOverride) return envOverride;
33898
+ return defaultMailCredentialPath();
33899
+ }
33900
+ async function loadMailCredentials(credentialPath) {
33901
+ const filePath = resolveMailCredentialPath(credentialPath);
33902
+ try {
33903
+ return JSON.parse(await fs12.readFile(filePath, "utf-8"));
33904
+ } catch {
33905
+ return null;
33906
+ }
33907
+ }
33908
+ async function saveMailCredentials(creds, credentialPath) {
33909
+ const filePath = resolveMailCredentialPath(credentialPath);
33910
+ await fs12.mkdir(path12.dirname(filePath), { recursive: true, mode: 448 });
33911
+ await fs12.writeFile(filePath, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
33912
+ await fs12.chmod(filePath, 384);
33913
+ }
33914
+ async function persistProvisionedMailbox(mailbox, credentialPath) {
33915
+ if (!mailbox?.address || !mailbox.password) return false;
33916
+ await saveMailCredentials(
33917
+ {
33918
+ address: mailbox.address,
33919
+ password: mailbox.password,
33920
+ imapHost: MAIL_DEFAULT_HOST,
33921
+ imapPort: 993,
33922
+ smtpHost: MAIL_DEFAULT_HOST,
33923
+ smtpPort: 465,
33924
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
33925
+ },
33926
+ credentialPath
33927
+ );
33928
+ return true;
33929
+ }
33930
+ var MAIL_DEFAULT_HOST;
33931
+ var init_mail_credential_store = __esm({
33932
+ "src/cloud/mail-credential-store.ts"() {
33933
+ "use strict";
33934
+ MAIL_DEFAULT_HOST = "mail.cogent.tools";
33935
+ }
33936
+ });
33937
+
33938
+ // src/tools/register-peer.ts
33939
+ import crypto4 from "node:crypto";
33878
33940
  function registerRegisterPeerTool(server) {
33879
33941
  server.registerTool(
33880
33942
  "cogent_register_peer",
@@ -33933,7 +33995,7 @@ function registerRegisterPeerTool(server) {
33933
33995
  }
33934
33996
  }
33935
33997
  if (!cloudSessionId) {
33936
- autoSecret = crypto3.randomBytes(16).toString("hex");
33998
+ autoSecret = crypto4.randomBytes(16).toString("hex");
33937
33999
  const resp = await fetch(
33938
34000
  `${endpoint}/api/sessions`,
33939
34001
  {
@@ -34012,6 +34074,15 @@ function registerRegisterPeerTool(server) {
34012
34074
  );
34013
34075
  }
34014
34076
  }
34077
+ try {
34078
+ if (await persistProvisionedMailbox(peer?.mailbox)) {
34079
+ logger.info(`Cogent Mail: saved auto-provisioned mailbox ${peer.mailbox?.address}`);
34080
+ }
34081
+ } catch (err) {
34082
+ logger.warn(
34083
+ `Cogent Mail: could not save auto-provisioned mailbox creds: ${err.message}`
34084
+ );
34085
+ }
34015
34086
  const resolution = await resolveSession(
34016
34087
  getConfig().COGENT_PLATFORM,
34017
34088
  cwd,
@@ -34094,6 +34165,7 @@ var init_register_peer = __esm({
34094
34165
  init_backend();
34095
34166
  init_errors4();
34096
34167
  init_credential_store();
34168
+ init_mail_credential_store();
34097
34169
  init_logger();
34098
34170
  init_heartbeat();
34099
34171
  init_auto_relay();
@@ -34403,7 +34475,8 @@ var init_peer = __esm({
34403
34475
  workspaceId: import_zod5.z.string().optional().describe("Durable workspace identifier (Slice 5) \u2014 stable id not tied to the mutable cwd hint"),
34404
34476
  threadId: import_zod5.z.string().optional().describe("Durable thread/conversation identifier (Slice 5)"),
34405
34477
  role: import_zod5.z.string().max(64).optional().describe("A2A role advertised by this peer (Agent Card)"),
34406
- capabilities: import_zod5.z.array(import_zod5.z.string().max(64)).max(16).optional().describe("Capability strings \u2192 A2A Agent Card skills[]")
34478
+ capabilities: import_zod5.z.array(import_zod5.z.string().max(64)).max(16).optional().describe("Capability strings \u2192 A2A Agent Card skills[]"),
34479
+ mailboxAddress: import_zod5.z.string().max(320).optional().describe("Auto-provisioned Cogent Mail address (non-secret); NEVER the password")
34407
34480
  });
34408
34481
  }
34409
34482
  });
@@ -34751,6 +34824,19 @@ function toSkills(peer) {
34751
34824
  };
34752
34825
  });
34753
34826
  }
34827
+ function toMailboxExtensions(peer) {
34828
+ const address = peer.mailboxAddress;
34829
+ if (!address)
34830
+ return [];
34831
+ return [
34832
+ {
34833
+ uri: COGENT_MAILBOX_EXTENSION_URI,
34834
+ description: "Cogent Mail mailbox address (non-secret) for this peer.",
34835
+ required: false,
34836
+ params: { address }
34837
+ }
34838
+ ];
34839
+ }
34754
34840
  function synthDescription(peer) {
34755
34841
  const parts = [`Cogent fabric peer on ${peer.platform ?? "cc"}`];
34756
34842
  if (peer.role)
@@ -34766,6 +34852,7 @@ function peerInfoToAgentCard(peer, ctx) {
34766
34852
  const version2 = ctx.clientVersion ?? peer.clientVersion ?? "0.0.0";
34767
34853
  const provider = { organization: `Cogent \xB7 ${peer.platform ?? "cc"}`, url: "https://cogent.tools" };
34768
34854
  const skills = toSkills(peer);
34855
+ const extensions = toMailboxExtensions(peer);
34769
34856
  if (ctx.cardVersion === "0.3.0") {
34770
34857
  return {
34771
34858
  protocolVersion: "0.3.0",
@@ -34774,7 +34861,7 @@ function peerInfoToAgentCard(peer, ctx) {
34774
34861
  url,
34775
34862
  version: version2,
34776
34863
  provider,
34777
- capabilities: {},
34864
+ capabilities: extensions.length ? { extensions } : {},
34778
34865
  defaultInputModes: ["text/plain"],
34779
34866
  defaultOutputModes: ["text/plain"],
34780
34867
  skills,
@@ -34795,7 +34882,7 @@ function peerInfoToAgentCard(peer, ctx) {
34795
34882
  tenant: ctx.sessionId
34796
34883
  }
34797
34884
  ],
34798
- capabilities: { extensions: [] },
34885
+ capabilities: { extensions },
34799
34886
  defaultInputModes: ["text/plain"],
34800
34887
  defaultOutputModes: ["text/plain"],
34801
34888
  skills,
@@ -34804,12 +34891,13 @@ function peerInfoToAgentCard(peer, ctx) {
34804
34891
  signatures: []
34805
34892
  };
34806
34893
  }
34807
- var COGENT_PROTOCOL_BINDING, A2A_PROTOCOL_VERSION;
34894
+ var COGENT_PROTOCOL_BINDING, A2A_PROTOCOL_VERSION, COGENT_MAILBOX_EXTENSION_URI;
34808
34895
  var init_agent_card = __esm({
34809
34896
  "cogent/dist/a2a/agent-card.js"() {
34810
34897
  "use strict";
34811
34898
  COGENT_PROTOCOL_BINDING = "COGENT-RELAY";
34812
34899
  A2A_PROTOCOL_VERSION = "1.0";
34900
+ COGENT_MAILBOX_EXTENSION_URI = "https://cogent.tools/a2a/ext/mailbox";
34813
34901
  }
34814
34902
  });
34815
34903
 
@@ -35104,7 +35192,7 @@ var init_health_check2 = __esm({
35104
35192
  });
35105
35193
 
35106
35194
  // src/tools/create-session.ts
35107
- import crypto4 from "node:crypto";
35195
+ import crypto5 from "node:crypto";
35108
35196
  function registerCreateSessionTool(server) {
35109
35197
  server.registerTool(
35110
35198
  "cogent_create_session",
@@ -35135,7 +35223,7 @@ function registerCreateSessionTool(server) {
35135
35223
  )
35136
35224
  );
35137
35225
  }
35138
- const sessionSecret = secret ?? crypto4.randomBytes(16).toString("hex");
35226
+ const sessionSecret = secret ?? crypto5.randomBytes(16).toString("hex");
35139
35227
  const resp = await fetch(
35140
35228
  `${config2.COGENT_ENDPOINT}/api/sessions`,
35141
35229
  {
@@ -35363,6 +35451,386 @@ var init_join_session = __esm({
35363
35451
  }
35364
35452
  });
35365
35453
 
35454
+ // src/mail/validate.ts
35455
+ function isValidEmail(value) {
35456
+ return value.length <= 254 && EMAIL_RE.test(value);
35457
+ }
35458
+ function assertValidEmail(value, field) {
35459
+ if (!isValidEmail(value)) {
35460
+ throw new BridgeError(
35461
+ "INVALID_INPUT" /* INVALID_INPUT */,
35462
+ `${field} is not a valid email address: ${JSON.stringify(value)}`,
35463
+ `Provide a full address like agent-channel@mail.cogent.tools`
35464
+ );
35465
+ }
35466
+ }
35467
+ var EMAIL_RE;
35468
+ var init_validate = __esm({
35469
+ "src/mail/validate.ts"() {
35470
+ "use strict";
35471
+ init_errors4();
35472
+ EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
35473
+ }
35474
+ });
35475
+
35476
+ // src/tools/setup-mail.ts
35477
+ function registerSetupMailTool(server) {
35478
+ server.registerTool(
35479
+ "cogent_setup_mail",
35480
+ {
35481
+ title: "Configure this agent's mailbox",
35482
+ description: "Store the agent's mailbox address + password (from the admin Mail panel) locally so cogent_send_mail and cogent_fetch_mail can use them. Hosts default to mail.cogent.tools (IMAP 993 / SMTP 465).",
35483
+ inputSchema: {
35484
+ address: import_zod16.z.string().describe("Full mailbox address, e.g. claude-agent-backend@mail.cogent.tools"),
35485
+ password: import_zod16.z.string().describe("Mailbox password (shown once in the admin Mail panel on create/rotate)"),
35486
+ imapHost: import_zod16.z.string().optional().describe(`IMAP host (default ${DEFAULT_MAIL_HOST})`),
35487
+ imapPort: import_zod16.z.number().int().positive().optional().describe("IMAP port (default 993)"),
35488
+ smtpHost: import_zod16.z.string().optional().describe(`SMTP host (default ${DEFAULT_MAIL_HOST})`),
35489
+ smtpPort: import_zod16.z.number().int().positive().optional().describe("SMTP submission port (default 465)")
35490
+ },
35491
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
35492
+ },
35493
+ async ({ address, password, imapHost, imapPort, smtpHost, smtpPort }) => {
35494
+ try {
35495
+ assertValidEmail(address, "address");
35496
+ await saveMailCredentials({
35497
+ address,
35498
+ password,
35499
+ imapHost: imapHost ?? DEFAULT_MAIL_HOST,
35500
+ imapPort: imapPort ?? 993,
35501
+ smtpHost: smtpHost ?? DEFAULT_MAIL_HOST,
35502
+ smtpPort: smtpPort ?? 465,
35503
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
35504
+ });
35505
+ return successResult({ configured: true, address });
35506
+ } catch (err) {
35507
+ return errorResult(err);
35508
+ }
35509
+ }
35510
+ );
35511
+ }
35512
+ var import_zod16, DEFAULT_MAIL_HOST;
35513
+ var init_setup_mail = __esm({
35514
+ "src/tools/setup-mail.ts"() {
35515
+ "use strict";
35516
+ import_zod16 = __toESM(require_zod(), 1);
35517
+ init_errors4();
35518
+ init_mail_credential_store();
35519
+ init_validate();
35520
+ DEFAULT_MAIL_HOST = MAIL_DEFAULT_HOST;
35521
+ }
35522
+ });
35523
+
35524
+ // src/mail/mail-sender.ts
35525
+ import fs13 from "node:fs/promises";
35526
+ import path13 from "node:path";
35527
+ async function buildAndSendMail(sender, creds, params) {
35528
+ const attachments = [];
35529
+ for (const p of params.attachmentPaths ?? []) {
35530
+ const content = await fs13.readFile(p);
35531
+ attachments.push({ filename: path13.basename(p), content });
35532
+ }
35533
+ const { messageId } = await sender.send({
35534
+ from: creds.address,
35535
+ to: params.to,
35536
+ subject: params.subject,
35537
+ text: params.text,
35538
+ attachments
35539
+ });
35540
+ return { messageId, from: creds.address, to: params.to };
35541
+ }
35542
+ var NodemailerMailSender;
35543
+ var init_mail_sender = __esm({
35544
+ "src/mail/mail-sender.ts"() {
35545
+ "use strict";
35546
+ NodemailerMailSender = class {
35547
+ constructor(creds, transportFactory) {
35548
+ this.creds = creds;
35549
+ this.transportFactory = transportFactory;
35550
+ }
35551
+ creds;
35552
+ transportFactory;
35553
+ async send(mail) {
35554
+ const transport = this.transportFactory ? await this.transportFactory() : await this.defaultTransport();
35555
+ const info = await transport.sendMail({
35556
+ from: mail.from,
35557
+ to: mail.to,
35558
+ subject: mail.subject,
35559
+ text: mail.text,
35560
+ attachments: mail.attachments.map((a) => ({ filename: a.filename, content: a.content }))
35561
+ });
35562
+ return { messageId: info.messageId ?? "" };
35563
+ }
35564
+ async defaultTransport() {
35565
+ const specifier = "nodemailer";
35566
+ const nodemailer = await import(specifier);
35567
+ return nodemailer.createTransport({
35568
+ host: this.creds.smtpHost,
35569
+ port: this.creds.smtpPort,
35570
+ secure: this.creds.smtpPort === 465,
35571
+ // implicit TLS on 465, STARTTLS on 587
35572
+ auth: { user: this.creds.address, pass: this.creds.password }
35573
+ });
35574
+ }
35575
+ };
35576
+ }
35577
+ });
35578
+
35579
+ // src/tools/send-mail.ts
35580
+ function registerSendMailTool(server, deps = {}) {
35581
+ const makeSender = deps.senderFactory ?? ((creds) => new NodemailerMailSender(creds));
35582
+ server.registerTool(
35583
+ "cogent_send_mail",
35584
+ {
35585
+ title: "Send an email from this agent's mailbox",
35586
+ description: "Send an email (with optional file attachments) from this agent's Cogent mailbox to another address. Requires cogent_setup_mail to have stored the mailbox credentials first.",
35587
+ inputSchema: {
35588
+ to: import_zod17.z.string().describe("Recipient email address, e.g. other-agent-backend@mail.cogent.tools"),
35589
+ subject: import_zod17.z.string().describe("Email subject"),
35590
+ body: import_zod17.z.string().describe("Plain-text email body"),
35591
+ attachments: import_zod17.z.array(import_zod17.z.string()).optional().describe("Optional local file paths to attach (each is read and attached by basename)")
35592
+ },
35593
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
35594
+ },
35595
+ async ({ to, subject, body, attachments }) => {
35596
+ try {
35597
+ assertValidEmail(to, "to");
35598
+ const creds = await loadMailCredentials();
35599
+ if (!creds) {
35600
+ return errorResult(
35601
+ new BridgeError(
35602
+ "INVALID_INPUT" /* INVALID_INPUT */,
35603
+ "No mailbox is configured for this agent",
35604
+ "Run cogent_setup_mail with the address + password from the admin Mail panel first"
35605
+ )
35606
+ );
35607
+ }
35608
+ const result = await buildAndSendMail(makeSender(creds), creds, {
35609
+ to: [to],
35610
+ subject,
35611
+ text: body,
35612
+ attachmentPaths: attachments
35613
+ });
35614
+ return successResult({ ...result, attachmentCount: attachments?.length ?? 0 });
35615
+ } catch (err) {
35616
+ return errorResult(err);
35617
+ }
35618
+ }
35619
+ );
35620
+ }
35621
+ var import_zod17;
35622
+ var init_send_mail = __esm({
35623
+ "src/tools/send-mail.ts"() {
35624
+ "use strict";
35625
+ import_zod17 = __toESM(require_zod(), 1);
35626
+ init_errors4();
35627
+ init_mail_credential_store();
35628
+ init_mail_sender();
35629
+ init_validate();
35630
+ }
35631
+ });
35632
+
35633
+ // src/mail/mail-fetcher.ts
35634
+ import fs14 from "node:fs/promises";
35635
+ import path14 from "node:path";
35636
+ async function saveAttachments(attachments, downloadDir) {
35637
+ if (attachments.length === 0) return [];
35638
+ await fs14.mkdir(downloadDir, { recursive: true });
35639
+ const saved = [];
35640
+ for (const a of attachments) {
35641
+ const name = path14.basename(a.filename) || "attachment";
35642
+ const savedPath = path14.join(downloadDir, name);
35643
+ await fs14.writeFile(savedPath, a.content);
35644
+ saved.push({ filename: name, savedPath, size: a.content.length });
35645
+ }
35646
+ return saved;
35647
+ }
35648
+ async function defaultImapSessionFactory(creds) {
35649
+ const specifier = "imapflow";
35650
+ const { ImapFlow } = await import(specifier);
35651
+ const client = new ImapFlow({
35652
+ host: creds.imapHost,
35653
+ port: creds.imapPort,
35654
+ secure: creds.imapPort === 993,
35655
+ auth: { user: creds.address, pass: creds.password },
35656
+ logger: false
35657
+ });
35658
+ await client.connect();
35659
+ await client.mailboxOpen("INBOX");
35660
+ return {
35661
+ async list(unreadOnly) {
35662
+ const criteria = unreadOnly ? { seen: false } : { all: true };
35663
+ const rows = [];
35664
+ for await (const msg of client.fetch(criteria, { uid: true, envelope: true, flags: true })) {
35665
+ const env = msg.envelope ?? {};
35666
+ rows.push({
35667
+ uid: msg.uid,
35668
+ from: env.from?.[0]?.address ?? "",
35669
+ subject: env.subject ?? "",
35670
+ date: env.date instanceof Date ? env.date.toISOString() : String(env.date ?? ""),
35671
+ unread: !hasFlag(msg.flags, "\\Seen")
35672
+ });
35673
+ }
35674
+ return rows.reverse();
35675
+ },
35676
+ async fetch(uid) {
35677
+ const msg = await client.fetchOne(uid, { uid: true, envelope: true, bodyStructure: true }, { uid: true });
35678
+ if (!msg) throw new Error(`no message with uid ${uid}`);
35679
+ const parts = flattenBodyStructure(msg.bodyStructure);
35680
+ let text = "";
35681
+ const attachments = [];
35682
+ for (const p of parts) {
35683
+ if (p.filename) {
35684
+ const dl = await client.download(uid, p.part, { uid: true });
35685
+ attachments.push({ filename: p.filename, content: await streamToBuffer(dl.content) });
35686
+ } else if (p.isText && !text) {
35687
+ const dl = await client.download(uid, p.part, { uid: true });
35688
+ text = (await streamToBuffer(dl.content)).toString("utf-8");
35689
+ }
35690
+ }
35691
+ const env = msg.envelope ?? {};
35692
+ return {
35693
+ from: env.from?.[0]?.address ?? "",
35694
+ subject: env.subject ?? "",
35695
+ date: env.date instanceof Date ? env.date.toISOString() : String(env.date ?? ""),
35696
+ text,
35697
+ attachments
35698
+ };
35699
+ },
35700
+ async close() {
35701
+ try {
35702
+ await client.logout();
35703
+ } catch {
35704
+ }
35705
+ }
35706
+ };
35707
+ }
35708
+ function flattenBodyStructure(node, acc = []) {
35709
+ if (!node || typeof node !== "object") return acc;
35710
+ const n = node;
35711
+ if (Array.isArray(n.childNodes) && n.childNodes.length > 0) {
35712
+ for (const child of n.childNodes) flattenBodyStructure(child, acc);
35713
+ return acc;
35714
+ }
35715
+ const type = (n.type ?? "").toLowerCase();
35716
+ const filename = n.dispositionParameters?.filename ?? n.parameters?.name;
35717
+ const isAttachment = (n.disposition ?? "").toLowerCase() === "attachment" || Boolean(filename);
35718
+ acc.push({
35719
+ part: n.part ?? "1",
35720
+ // single-part messages have no `part`; imapflow addresses them as "1"
35721
+ type,
35722
+ isText: type === "text/plain" && !isAttachment,
35723
+ filename: isAttachment ? filename : void 0
35724
+ });
35725
+ return acc;
35726
+ }
35727
+ function hasFlag(flags, flag) {
35728
+ if (flags instanceof Set) return flags.has(flag);
35729
+ if (Array.isArray(flags)) return flags.includes(flag);
35730
+ return false;
35731
+ }
35732
+ async function streamToBuffer(stream) {
35733
+ const chunks = [];
35734
+ if (!stream) return Buffer.alloc(0);
35735
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
35736
+ return Buffer.concat(chunks);
35737
+ }
35738
+ var ImapflowMailFetcher;
35739
+ var init_mail_fetcher = __esm({
35740
+ "src/mail/mail-fetcher.ts"() {
35741
+ "use strict";
35742
+ ImapflowMailFetcher = class {
35743
+ constructor(creds, sessionFactory = defaultImapSessionFactory) {
35744
+ this.creds = creds;
35745
+ this.sessionFactory = sessionFactory;
35746
+ }
35747
+ creds;
35748
+ sessionFactory;
35749
+ async list(opts = {}) {
35750
+ const session = await this.sessionFactory(this.creds);
35751
+ try {
35752
+ let rows = await session.list(opts.unreadOnly ?? false);
35753
+ if (opts.limit && opts.limit > 0) rows = rows.slice(0, opts.limit);
35754
+ return rows;
35755
+ } finally {
35756
+ await session.close();
35757
+ }
35758
+ }
35759
+ async fetchOne(uid, downloadDir) {
35760
+ const session = await this.sessionFactory(this.creds);
35761
+ try {
35762
+ const c2 = await session.fetch(uid);
35763
+ const saved = await saveAttachments(c2.attachments, downloadDir);
35764
+ return { uid, from: c2.from, subject: c2.subject, date: c2.date, text: c2.text, attachments: saved };
35765
+ } finally {
35766
+ await session.close();
35767
+ }
35768
+ }
35769
+ };
35770
+ }
35771
+ });
35772
+
35773
+ // src/tools/fetch-mail.ts
35774
+ import os8 from "node:os";
35775
+ import path15 from "node:path";
35776
+ function registerFetchMailTool(server, deps = {}) {
35777
+ const makeFetcher = deps.fetcherFactory ?? ((creds) => new ImapflowMailFetcher(creds));
35778
+ server.registerTool(
35779
+ "cogent_fetch_mail",
35780
+ {
35781
+ title: "Read this agent's mailbox",
35782
+ description: "List messages in this agent's Cogent mailbox, or fetch one message (text + attachments saved locally) by uid. Requires cogent_setup_mail to have stored the mailbox credentials.",
35783
+ inputSchema: {
35784
+ action: import_zod18.z.enum(["list", "fetch"]).optional().describe("'list' (default) or 'fetch' one message"),
35785
+ uid: import_zod18.z.number().int().positive().optional().describe("Message uid to fetch (required when action='fetch')"),
35786
+ unreadOnly: import_zod18.z.boolean().optional().describe("List only unread messages (default false)"),
35787
+ limit: import_zod18.z.number().int().positive().optional().describe("Max messages to list (default 25)"),
35788
+ downloadDir: import_zod18.z.string().optional().describe(`Directory to save fetched attachments (default ${DEFAULT_DOWNLOAD_DIR})`)
35789
+ },
35790
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
35791
+ },
35792
+ async ({ action, uid, unreadOnly, limit, downloadDir }) => {
35793
+ try {
35794
+ const creds = await loadMailCredentials();
35795
+ if (!creds) {
35796
+ return errorResult(
35797
+ new BridgeError(
35798
+ "INVALID_INPUT" /* INVALID_INPUT */,
35799
+ "No mailbox is configured for this agent",
35800
+ "Run cogent_setup_mail with the address + password from the admin Mail panel first"
35801
+ )
35802
+ );
35803
+ }
35804
+ const fetcher = makeFetcher(creds);
35805
+ if (action === "fetch") {
35806
+ if (uid === void 0) {
35807
+ return errorResult(
35808
+ new BridgeError("INVALID_INPUT" /* INVALID_INPUT */, "uid is required when action='fetch'", "Pass the uid from a cogent_fetch_mail list")
35809
+ );
35810
+ }
35811
+ const message = await fetcher.fetchOne(uid, downloadDir ?? DEFAULT_DOWNLOAD_DIR);
35812
+ return successResult({ mailbox: creds.address, message });
35813
+ }
35814
+ const messages = await fetcher.list({ unreadOnly, limit: limit ?? 25 });
35815
+ return successResult({ mailbox: creds.address, count: messages.length, messages });
35816
+ } catch (err) {
35817
+ return errorResult(err);
35818
+ }
35819
+ }
35820
+ );
35821
+ }
35822
+ var import_zod18, DEFAULT_DOWNLOAD_DIR;
35823
+ var init_fetch_mail = __esm({
35824
+ "src/tools/fetch-mail.ts"() {
35825
+ "use strict";
35826
+ import_zod18 = __toESM(require_zod(), 1);
35827
+ init_errors4();
35828
+ init_mail_credential_store();
35829
+ init_mail_fetcher();
35830
+ DEFAULT_DOWNLOAD_DIR = path15.join(os8.homedir(), ".cogent", "mail-downloads");
35831
+ }
35832
+ });
35833
+
35366
35834
  // src/index.ts
35367
35835
  var index_exports = {};
35368
35836
  async function main() {
@@ -35376,6 +35844,9 @@ async function main() {
35376
35844
  registerHealthCheckTool(server);
35377
35845
  registerCreateSessionTool(server);
35378
35846
  registerJoinSessionTool(server);
35847
+ registerSetupMailTool(server);
35848
+ registerSendMailTool(server);
35849
+ registerFetchMailTool(server);
35379
35850
  if (cloudInbox) {
35380
35851
  server.registerResource(
35381
35852
  "cogent_inbox",
@@ -35435,6 +35906,9 @@ var init_index = __esm({
35435
35906
  init_health_check2();
35436
35907
  init_create_session();
35437
35908
  init_join_session();
35909
+ init_setup_mail();
35910
+ init_send_mail();
35911
+ init_fetch_mail();
35438
35912
  init_heartbeat();
35439
35913
  process.on("uncaughtException", (err) => {
35440
35914
  if (err.code === "EPIPE") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.14.0",
3
+ "version": "3.15.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",