openmates 0.27.0-alpha.61 → 0.27.0-alpha.63

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.
@@ -8,6 +8,7 @@ import {
8
8
  getCredentialStorageMode,
9
9
  installPrivacyModel,
10
10
  loadAnonymousId,
11
+ loadCachedDirectChatAccountKey,
11
12
  loadLocalTeamKey,
12
13
  loadSession,
13
14
  loadStoredSessionApiUrl,
@@ -25,6 +26,7 @@ import {
25
26
  readPrivacyPreferences,
26
27
  resolveStateDir,
27
28
  saveAnonymousId,
29
+ saveCachedDirectChatAccountKey,
28
30
  saveLocalTeamKey,
29
31
  saveSession,
30
32
  saveSyncCache,
@@ -34,7 +36,7 @@ import {
34
36
  verifyInstalledModel,
35
37
  verifyPrivacyRuntime,
36
38
  withSessionRefreshLock
37
- } from "./chunk-5SFXCJJ4.js";
39
+ } from "./chunk-Q6PGA2B3.js";
38
40
  import {
39
41
  RemoteAccessReplayGuard,
40
42
  createRemoteAccessHandshake,
@@ -1634,6 +1636,183 @@ async function prepareCliMessagePrivacy(text7, memories, existing = [], scope2 =
1634
1636
 
1635
1637
  // src/client.ts
1636
1638
  import { createHash as createHash13, randomBytes as randomBytes4, randomUUID as randomUUID9, timingSafeEqual } from "crypto";
1639
+
1640
+ // ../pairing-crypto/src/directChatCrypto.ts
1641
+ import nacl from "tweetnacl";
1642
+ var PREFIX = "OMDM1.";
1643
+ var MAX_ENVELOPE_LENGTH = 2048;
1644
+ var MAX_PAYLOAD_BYTES = 768;
1645
+ var BASE64URL = /^[A-Za-z0-9_-]+$/;
1646
+ var KEY_ID = /^[0-9a-f]{64}$/;
1647
+ var encoder = new TextEncoder();
1648
+ var decoder = new TextDecoder("utf-8", { fatal: true });
1649
+ function ownedBuffer(bytes2) {
1650
+ const copy = new Uint8Array(bytes2.length);
1651
+ copy.set(bytes2);
1652
+ return copy.buffer;
1653
+ }
1654
+ function directChatBase64Url(bytes2) {
1655
+ let binary = "";
1656
+ for (let offset = 0; offset < bytes2.length; offset += 8192) {
1657
+ binary += String.fromCharCode(...bytes2.subarray(offset, offset + 8192));
1658
+ }
1659
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1660
+ }
1661
+ function directChatBase64UrlBytes(value, length, field3 = "key", maxLength = length ?? MAX_PAYLOAD_BYTES) {
1662
+ if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxLength * 4 / 3) + 2 || !BASE64URL.test(value)) {
1663
+ throw new Error(`Invalid direct-chat ${field3}`);
1664
+ }
1665
+ let decoded;
1666
+ try {
1667
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4));
1668
+ decoded = Uint8Array.from(binary, (character) => character.charCodeAt(0));
1669
+ } catch {
1670
+ throw new Error(`Invalid direct-chat ${field3}`);
1671
+ }
1672
+ if (length !== void 0 && decoded.length !== length || decoded.length > maxLength || directChatBase64Url(decoded) !== value) throw new Error(`Invalid direct-chat ${field3}`);
1673
+ return decoded;
1674
+ }
1675
+ function boundedIdentity(identity3) {
1676
+ if (!identity3 || typeof identity3 !== "object") throw new Error("Invalid direct-chat identity");
1677
+ for (const value of [identity3.teamId, identity3.chatId, identity3.recipientHash]) {
1678
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9_-]+$/.test(value)) {
1679
+ throw new Error("Invalid direct-chat identity");
1680
+ }
1681
+ }
1682
+ if (!KEY_ID.test(identity3.keyId)) throw new Error("Invalid direct-chat key ID");
1683
+ }
1684
+ function validNonzeroKey(value, field3) {
1685
+ if (value.length !== 32 || value.every((byte) => byte === 0)) throw new Error(`Invalid direct-chat ${field3}`);
1686
+ }
1687
+ function validatePublicKey(value) {
1688
+ const bytes2 = directChatBase64UrlBytes(value, 32, "public key");
1689
+ validNonzeroKey(bytes2, "public key");
1690
+ const probe = nacl.scalarMult(new Uint8Array(32).fill(7), bytes2);
1691
+ if (probe.every((byte) => byte === 0)) throw new Error("Invalid direct-chat public key");
1692
+ return bytes2;
1693
+ }
1694
+ async function directChatKeyId(publicKey) {
1695
+ const bytes2 = validatePublicKey(publicKey);
1696
+ const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer(bytes2)));
1697
+ return [...digest4].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1698
+ }
1699
+ async function generateIdentity() {
1700
+ const pair = nacl.box.keyPair();
1701
+ const publicKey = directChatBase64Url(pair.publicKey);
1702
+ return { publicKey, privateKey: directChatBase64Url(pair.secretKey), keyId: await directChatKeyId(publicKey) };
1703
+ }
1704
+ async function sealDirectChatKey(rawKey, identity3, recipientPublicKey) {
1705
+ boundedIdentity(identity3);
1706
+ validNonzeroKey(rawKey, "chat key");
1707
+ const recipient = validatePublicKey(recipientPublicKey);
1708
+ if (identity3.keyId !== await directChatKeyId(recipientPublicKey)) throw new Error("Direct-chat recipient key ID mismatch");
1709
+ const ephemeral = nacl.box.keyPair();
1710
+ const nonce = nacl.randomBytes(24);
1711
+ const payload = encoder.encode(JSON.stringify({
1712
+ v: 1,
1713
+ team_id: identity3.teamId,
1714
+ chat_id: identity3.chatId,
1715
+ recipient_hash: identity3.recipientHash,
1716
+ key_id: identity3.keyId,
1717
+ key: directChatBase64Url(rawKey)
1718
+ }));
1719
+ const ciphertext = nacl.box(payload, nonce, recipient, ephemeral.secretKey);
1720
+ const envelope = PREFIX + directChatBase64Url(encoder.encode(JSON.stringify({
1721
+ v: 1,
1722
+ ephemeral_public_key: directChatBase64Url(ephemeral.publicKey),
1723
+ nonce: directChatBase64Url(nonce),
1724
+ ciphertext: directChatBase64Url(ciphertext)
1725
+ })));
1726
+ ephemeral.secretKey.fill(0);
1727
+ if (envelope.length > MAX_ENVELOPE_LENGTH) throw new Error("Direct-chat envelope too large");
1728
+ return envelope;
1729
+ }
1730
+ async function openDirectChatKey(envelope, expected, privateKey) {
1731
+ boundedIdentity(expected);
1732
+ if (typeof envelope !== "string" || !envelope.startsWith(PREFIX) || envelope.length > MAX_ENVELOPE_LENGTH) {
1733
+ throw new Error("Invalid direct-chat envelope");
1734
+ }
1735
+ const encoded = envelope.slice(PREFIX.length);
1736
+ const decoded = directChatBase64UrlBytes(encoded, void 0, "envelope");
1737
+ if (decoded.length > MAX_PAYLOAD_BYTES) throw new Error("Direct-chat envelope too large");
1738
+ let parsed;
1739
+ try {
1740
+ parsed = JSON.parse(decoder.decode(decoded));
1741
+ } catch {
1742
+ throw new Error("Invalid direct-chat envelope");
1743
+ }
1744
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid direct-chat envelope");
1745
+ const outer = parsed;
1746
+ if (Object.keys(outer).sort().join(",") !== "ciphertext,ephemeral_public_key,nonce,v" || outer.v !== 1) {
1747
+ throw new Error("Invalid direct-chat envelope version or fields");
1748
+ }
1749
+ const sender = validatePublicKey(String(outer.ephemeral_public_key));
1750
+ const nonce = directChatBase64UrlBytes(String(outer.nonce), 24, "nonce");
1751
+ const ciphertext = directChatBase64UrlBytes(String(outer.ciphertext), void 0, "ciphertext", 512);
1752
+ if (ciphertext.length < 17) throw new Error("Invalid direct-chat ciphertext");
1753
+ const secret = directChatBase64UrlBytes(privateKey, 32, "private key");
1754
+ validNonzeroKey(secret, "private key");
1755
+ const ownPublic = directChatBase64Url(nacl.scalarMult.base(secret));
1756
+ if (await directChatKeyId(ownPublic) !== expected.keyId) throw new Error("Direct-chat private key ID mismatch");
1757
+ const plaintext = nacl.box.open(ciphertext, nonce, sender, secret);
1758
+ if (!plaintext) throw new Error("Direct-chat key authentication failed");
1759
+ let inner;
1760
+ try {
1761
+ inner = JSON.parse(decoder.decode(plaintext));
1762
+ } catch {
1763
+ throw new Error("Invalid direct-chat key payload");
1764
+ }
1765
+ if (!inner || typeof inner !== "object" || Array.isArray(inner)) throw new Error("Invalid direct-chat key payload");
1766
+ const payload = inner;
1767
+ if (Object.keys(payload).sort().join(",") !== "chat_id,key,key_id,recipient_hash,team_id,v" || payload.v !== 1 || payload.team_id !== expected.teamId || payload.chat_id !== expected.chatId || payload.recipient_hash !== expected.recipientHash || payload.key_id !== expected.keyId) {
1768
+ throw new Error("Direct-chat key binding mismatch");
1769
+ }
1770
+ const key = directChatBase64UrlBytes(String(payload.key), 32, "chat key");
1771
+ validNonzeroKey(key, "chat key");
1772
+ return key;
1773
+ }
1774
+
1775
+ // src/directChats.ts
1776
+ function validatePublicChatEncryptionKey(value) {
1777
+ const row3 = value;
1778
+ if (!row3 || typeof row3.public_key !== "string" || typeof row3.key_id !== "string" || !/^[0-9a-f]{64}$/.test(row3.key_id)) {
1779
+ throw new Error("Invalid member chat encryption key");
1780
+ }
1781
+ directChatBase64UrlBytes(row3.public_key, 32, "public key");
1782
+ return { key_id: row3.key_id, public_key: row3.public_key };
1783
+ }
1784
+ async function validatePublicChatEncryptionKeyId(value) {
1785
+ const key = validatePublicChatEncryptionKey(value);
1786
+ if (await directChatKeyId(key.public_key) !== key.key_id) throw new Error("Member chat encryption key ID mismatch");
1787
+ return key;
1788
+ }
1789
+ async function recoverAccountChatIdentity(record10, masterKey) {
1790
+ if (record10.version !== 1 || record10.algorithm !== "x25519-xsalsa20poly1305") throw new Error("Unsupported account chat encryption key");
1791
+ const publicKey = await validatePublicChatEncryptionKeyId(record10);
1792
+ if (typeof record10.encrypted_private_key !== "string" || record10.encrypted_private_key.length > 2048) throw new Error("Invalid account chat private key backup");
1793
+ const encoded = record10.encrypted_private_key;
1794
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded)) throw new Error("Invalid account chat private key backup");
1795
+ const privateKey = await decryptWithAesGcmCombined(Buffer.from(encoded, "base64url").toString("base64"), masterKey);
1796
+ if (!privateKey) throw new Error("Could not decrypt account chat private key");
1797
+ const secret = directChatBase64UrlBytes(privateKey, 32, "private key");
1798
+ if (secret.every((byte) => byte === 0)) throw new Error("Invalid account chat private key");
1799
+ const nacl2 = (await import("tweetnacl")).default;
1800
+ if (directChatBase64Url(nacl2.scalarMult.base(secret)) !== publicKey.public_key) throw new Error("Account chat key pair mismatch");
1801
+ return { publicKey: publicKey.public_key, privateKey, keyId: publicKey.key_id };
1802
+ }
1803
+ async function createAccountChatIdentity(masterKey) {
1804
+ const identity3 = await generateIdentity();
1805
+ const backup = await encryptWithAesGcmCombined(identity3.privateKey, masterKey);
1806
+ return { identity: identity3, record: {
1807
+ version: 1,
1808
+ algorithm: "x25519-xsalsa20poly1305",
1809
+ key_id: identity3.keyId,
1810
+ public_key: identity3.publicKey,
1811
+ encrypted_private_key: Buffer.from(backup, "base64").toString("base64url")
1812
+ } };
1813
+ }
1814
+
1815
+ // src/client.ts
1637
1816
  import { arch, platform, release } from "os";
1638
1817
  import { createInterface as createInterface3 } from "readline/promises";
1639
1818
  import { stdin as stdin2, stdout } from "process";
@@ -2475,13 +2654,13 @@ var OpenMatesHttpClient = class {
2475
2654
  throw new Error("SSE response body is not readable");
2476
2655
  }
2477
2656
  const reader = response.body.getReader();
2478
- const decoder2 = new TextDecoder();
2657
+ const decoder3 = new TextDecoder();
2479
2658
  let buffer = "";
2480
2659
  try {
2481
2660
  while (true) {
2482
2661
  const { done, value } = await reader.read();
2483
2662
  if (done) break;
2484
- buffer += decoder2.decode(value, { stream: true });
2663
+ buffer += decoder3.decode(value, { stream: true });
2485
2664
  let separatorIndex = buffer.indexOf("\n\n");
2486
2665
  while (separatorIndex >= 0) {
2487
2666
  const rawEvent = buffer.slice(0, separatorIndex);
@@ -2701,12 +2880,12 @@ import { randomUUID as randomUUID3 } from "crypto";
2701
2880
 
2702
2881
  // ../ui/src/utils/projectIgnoredReadGrant.ts
2703
2882
  var GRANT_LIFETIME_MS = 5 * 60 * 1e3;
2704
- var encoder = new TextEncoder();
2883
+ var encoder2 = new TextEncoder();
2705
2884
  function validScope(scope2) {
2706
2885
  return [scope2.projectId, scope2.sourceId, scope2.requestId, scope2.chatId, scope2.operationId].every((value) => typeof value === "string" && value.length > 0 && value.length <= 128) && typeof scope2.path === "string" && scope2.path.length > 0 && scope2.path.length <= 4096 && !scope2.path.includes("\\") && !scope2.path.startsWith("/") && !/^[a-z]:/i.test(scope2.path) && !Array.from(scope2.path).some((value) => value.charCodeAt(0) < 32 || value.charCodeAt(0) === 127) && scope2.path.split("/").every((part) => part && part !== "." && part !== "..");
2707
2886
  }
2708
2887
  function material(grant) {
2709
- return encoder.encode(JSON.stringify([
2888
+ return encoder2.encode(JSON.stringify([
2710
2889
  "openmates-ignored-read-v1",
2711
2890
  grant.projectId,
2712
2891
  grant.sourceId,
@@ -3923,6 +4102,14 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
3923
4102
  hashed_user_id: hashedUserId,
3924
4103
  file_path: embed.filePath,
3925
4104
  content_hash: embed.contentHash,
4105
+ ...embed.appId && embed.skillId ? {
4106
+ app_id: embed.appId,
4107
+ skill_id: embed.skillId,
4108
+ root_embed_id: embed.embedId,
4109
+ workspace_origin: "chat"
4110
+ } : {},
4111
+ ...embed.versionNumber ? { version_number: embed.versionNumber } : {},
4112
+ encryption_mode: "client",
3926
4113
  text_length_chars: embed.textLengthChars,
3927
4114
  created_at: nowSeconds3,
3928
4115
  updated_at: nowSeconds3,
@@ -7311,6 +7498,35 @@ import { basename as basename2, extname as extname2, resolve as resolve3 } from
7311
7498
  import { homedir as homedir2 } from "os";
7312
7499
  import { createHash as createHash10 } from "crypto";
7313
7500
  import JSZip2 from "jszip";
7501
+
7502
+ // src/diagramHtml.ts
7503
+ function inspectDiagramHtml(source) {
7504
+ if (!source || Buffer.byteLength(source, "utf8") > 1024 * 1024) return null;
7505
+ if (!/^\s*<!doctype html\s*>/i.test(source) || !/<html\b[^>]*>[\s\S]*<head\b[^>]*>[\s\S]*<\/head\s*>[\s\S]*<body\b[^>]*>[\s\S]*<svg\b/i.test(source) || !/<\/svg\s*>[\s\S]*<\/body\s*>[\s\S]*<\/html\s*>\s*$/i.test(source) || !/<g\b[^>]*\bdata-om-component\s*=\s*["'][a-z][a-z0-9_-]{0,63}["']/i.test(source)) return null;
7506
+ let checked = source.replace(/\sxmlns(?::[\w-]+)?\s*=\s*["']http:\/\/www\.w3\.org\/[A-Za-z0-9/.-]+["']/gi, "");
7507
+ checked = checked.replace(/<!--[\s\S]*?-->/g, "").replace(/&amp;/gi, "&");
7508
+ checked = checked.replace(/data:font\/woff2;base64,([A-Za-z0-9+/]+={0,2})/gi, (whole, encoded) => {
7509
+ const bytes2 = Buffer.from(encoded, "base64");
7510
+ return bytes2.length > 0 && bytes2.length <= 256 * 1024 && bytes2.subarray(0, 4).toString() === "wOF2" && /@font-face\s*\{[^}]*$/i.test(checked.slice(0, checked.indexOf(whole))) ? "#embedded-font" : "unsafe-font-data";
7511
+ });
7512
+ if (/<!--|-->|<![^d]|<\?|&#|&[a-z][a-z0-9]+;/i.test(checked) || /<\s*\/?\s*(?:script|iframe|frame|object|embed|applet|link|base|img|image|video|audio|source|track|form|input|textarea|select|button|template|portal|foreignobject|animate|set|discard|feimage|math|noscript)\b/i.test(source) || /\s(?:on[a-z]+|srcdoc|src|srcset|poster|data|action|formaction|background|http-equiv)\s*=/i.test(source) || /(?:https?:|file:|javascript:|data:|\/\/|unsafe-font-data)/i.test(checked) || /@\s*(?:import|namespace|document)|expression\s*\(|image-set\s*\(|behavior\s*:|-moz-binding\s*:|\\/i.test(checked)) return null;
7513
+ for (const match of checked.matchAll(/url\(\s*(['"]?)([^)"']+)\1\s*\)/gi)) {
7514
+ if (!/^#[a-z_][\w.:-]{0,119}$/i.test(match[2].trim())) return null;
7515
+ }
7516
+ if (/url\s*\(/i.test(checked.replace(/url\(\s*(['"]?)([^)"']+)\1\s*\)/gi, ""))) return null;
7517
+ const title = /<title\b[^>]*>([^<>]{1,160})<\/title\s*>/i.exec(source)?.[1]?.trim();
7518
+ const viewBox = /<svg\b[^>]*\bviewBox\s*=\s*["']([^"']+)["']/i.exec(source)?.[1];
7519
+ if (!title || !viewBox) return null;
7520
+ const parts = viewBox.trim().split(/[\s,]+/).map(Number);
7521
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)) || parts[2] < 1 || parts[2] > 8192 || parts[3] < 1 || parts[3] > 8192) return null;
7522
+ const kind = /<svg\b[^>]*\bdata-diagram-kind\s*=\s*["']([a-z][a-z0-9_-]{0,63})["']/i.exec(source)?.[1] ?? /<svg\b[^>]*\bdata-om-diagram\s*=\s*["']([a-z][a-z0-9_-]{0,63})["']/i.exec(source)?.[1] ?? /<meta\b[^>]*\bname\s*=\s*["']diagram-kind["'][^>]*\bcontent\s*=\s*["']([a-z][a-z0-9_-]{0,63})["']/i.exec(source)?.[1] ?? "diagram";
7523
+ const renderWidth = Number(/<html\b[^>]*\bdata-om-render-width\s*=\s*["']([0-9]{1,4})["']/i.exec(source)?.[1] ?? parts[2]);
7524
+ const renderHeight = Number(/<html\b[^>]*\bdata-om-render-height\s*=\s*["']([0-9]{1,4})["']/i.exec(source)?.[1] ?? parts[3]);
7525
+ if (!(renderWidth >= 1 && renderWidth <= 8192 && renderHeight >= 1 && renderHeight <= 8192)) return null;
7526
+ return { title, diagramKind: kind, renderWidth: Math.round(renderWidth), renderHeight: Math.round(renderHeight) };
7527
+ }
7528
+
7529
+ // src/fileEmbed.ts
7314
7530
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
7315
7531
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
7316
7532
  ".pem",
@@ -7833,6 +8049,47 @@ function processCodeFile(filePath, filename, redactor) {
7833
8049
  }
7834
8050
  }
7835
8051
  const language = detectLanguage(filename);
8052
+ if (language === "html") {
8053
+ const diagram = inspectDiagramHtml(content);
8054
+ if (diagram) {
8055
+ const embedId2 = generateEmbedId();
8056
+ const embedRef2 = createEmbedRef("diagram_html", `${filename}:${embedId2}`);
8057
+ return {
8058
+ embed: {
8059
+ embedId: embedId2,
8060
+ embedRef: embedRef2,
8061
+ type: "diagram_html",
8062
+ content: toonEncodeContent({
8063
+ type: "diagram_html",
8064
+ app_id: "diagrams",
8065
+ skill_id: "html",
8066
+ html_source: content,
8067
+ title: diagram.title,
8068
+ diagram_kind: diagram.diagramKind,
8069
+ render_width: diagram.renderWidth,
8070
+ render_height: diagram.renderHeight,
8071
+ schema_version: 1,
8072
+ version_number: 1,
8073
+ embed_ref: embedRef2,
8074
+ status: "finished"
8075
+ }),
8076
+ textPreview: `${diagram.title} (${diagram.diagramKind})`,
8077
+ status: "finished",
8078
+ filePath: filename,
8079
+ contentHash: createHash10("sha256").update(content).digest("hex"),
8080
+ textLengthChars: content.length,
8081
+ appId: "diagrams",
8082
+ skillId: "html",
8083
+ versionNumber: 1
8084
+ },
8085
+ referenceBlock: createEmbedReferenceBlock(embedRef2),
8086
+ displayName: filename,
8087
+ secretsRedacted,
8088
+ zeroKnowledge,
8089
+ requiresUpload: false
8090
+ };
8091
+ }
8092
+ }
7836
8093
  const lineCount = content.split("\n").length;
7837
8094
  const embedId = generateEmbedId();
7838
8095
  const embedRef = createEmbedRef("code", `${filename}:${embedId}`);
@@ -9906,13 +10163,13 @@ import { client, ready, server } from "@serenity-kit/opaque";
9906
10163
  var PAIR_PIN_ALPHABET = "ABCDEFGHJKLMNPQRTUVWXY3468";
9907
10164
  var PIN_LENGTH = 6;
9908
10165
  var KSF = { "argon2id-custom": { memory: 8192, iterations: 3, parallelism: 1 } };
9909
- var encoder2 = new TextEncoder();
9910
- var decoder = new TextDecoder("utf-8", { fatal: true });
9911
- var BUNDLE_INFO = encoder2.encode("openmates-pair-v2/bundle");
9912
- var BASE64URL = /^[A-Za-z0-9_-]+$/;
10166
+ var encoder3 = new TextEncoder();
10167
+ var decoder2 = new TextDecoder("utf-8", { fatal: true });
10168
+ var BUNDLE_INFO = encoder3.encode("openmates-pair-v2/bundle");
10169
+ var BASE64URL2 = /^[A-Za-z0-9_-]+$/;
9913
10170
  var HEX_256 = /^[0-9a-f]{64}$/;
9914
10171
  var BUNDLE_KEYS = /* @__PURE__ */ new Set(["protocol_version", "master_key_exported", "grant_secret", "user_email_salt", "hashed_email", "user_id", "account_context"]);
9915
- function ownedBuffer(bytes2) {
10172
+ function ownedBuffer2(bytes2) {
9916
10173
  return bytes2;
9917
10174
  }
9918
10175
  function requireAscii(value, name, max = 128) {
@@ -9952,7 +10209,7 @@ function validateContext(context) {
9952
10209
  auto_logout_minutes: parsed[6]
9953
10210
  };
9954
10211
  if (createPairContext(fields) !== context) throw new Error("Noncanonical pairing context");
9955
- return { text: context, bytes: encoder2.encode(context), fields };
10212
+ return { text: context, bytes: encoder3.encode(context), fields };
9956
10213
  }
9957
10214
  function identifiers(context) {
9958
10215
  return { client: `openmates-pair-v2/client/${context}`, server: `openmates-pair-v2/server/${context}` };
@@ -9968,7 +10225,7 @@ function base64url(bytes2) {
9968
10225
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
9969
10226
  }
9970
10227
  function decodeBase64url(value, maxBytes) {
9971
- if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxBytes * 4 / 3) + 2 || !BASE64URL.test(value)) {
10228
+ if (typeof value !== "string" || value.length < 1 || value.length > Math.ceil(maxBytes * 4 / 3) + 2 || !BASE64URL2.test(value)) {
9972
10229
  throw new Error("Invalid pairing encoding");
9973
10230
  }
9974
10231
  let bytes2;
@@ -9995,7 +10252,7 @@ async function hashPairSecret(secret) {
9995
10252
  const bytes2 = decodeBase64url(secret, 32);
9996
10253
  if (bytes2.length !== 32) throw new Error("Invalid pairing secret");
9997
10254
  try {
9998
- const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer(bytes2)));
10255
+ const digest4 = new Uint8Array(await crypto.subtle.digest("SHA-256", ownedBuffer2(bytes2)));
9999
10256
  return [...digest4].map((byte) => byte.toString(16).padStart(2, "0")).join("");
10000
10257
  } finally {
10001
10258
  bytes2.fill(0);
@@ -10011,10 +10268,10 @@ async function deriveBundleKey(sessionKey, contextBytes) {
10011
10268
  const raw = decodeBase64url(sessionKey, 64);
10012
10269
  if (raw.length !== 64) throw new Error("Invalid pairing session key");
10013
10270
  try {
10014
- const salt = await crypto.subtle.digest("SHA-256", ownedBuffer(contextBytes));
10015
- const ikm = await crypto.subtle.importKey("raw", ownedBuffer(raw), "HKDF", false, ["deriveKey"]);
10271
+ const salt = await crypto.subtle.digest("SHA-256", ownedBuffer2(contextBytes));
10272
+ const ikm = await crypto.subtle.importKey("raw", ownedBuffer2(raw), "HKDF", false, ["deriveKey"]);
10016
10273
  return crypto.subtle.deriveKey(
10017
- { name: "HKDF", hash: "SHA-256", salt, info: ownedBuffer(BUNDLE_INFO) },
10274
+ { name: "HKDF", hash: "SHA-256", salt, info: ownedBuffer2(BUNDLE_INFO) },
10018
10275
  ikm,
10019
10276
  { name: "AES-GCM", length: 256 },
10020
10277
  false,
@@ -10090,13 +10347,13 @@ async function createPairReceiver(context, pin) {
10090
10347
  if (nonce.length !== 12) throw new Error("Invalid pairing IV");
10091
10348
  const ciphertext = decodeBase64url(encryptedBundle, 65536);
10092
10349
  const plaintext = new Uint8Array(await crypto.subtle.decrypt(
10093
- { name: "AES-GCM", iv: ownedBuffer(nonce), additionalData: ownedBuffer(binding.bytes), tagLength: 128 },
10350
+ { name: "AES-GCM", iv: ownedBuffer2(nonce), additionalData: ownedBuffer2(binding.bytes), tagLength: 128 },
10094
10351
  key,
10095
- ownedBuffer(ciphertext)
10352
+ ownedBuffer2(ciphertext)
10096
10353
  ));
10097
10354
  try {
10098
10355
  assertActive();
10099
- return validateBundle(JSON.parse(decoder.decode(plaintext)), binding.fields.authorizer_user_id);
10356
+ return validateBundle(JSON.parse(decoder2.decode(plaintext)), binding.fields.authorizer_user_id);
10100
10357
  } finally {
10101
10358
  nonce.fill(0);
10102
10359
  ciphertext.fill(0);
@@ -11157,6 +11414,7 @@ function extractVersionedEmbedContent(content) {
11157
11414
  ].join("\n").trim();
11158
11415
  }
11159
11416
  if (typeof content.remotion_source === "string") return content.remotion_source;
11417
+ if (typeof content.html_source === "string") return content.html_source;
11160
11418
  if (typeof content.code === "string") return content.code;
11161
11419
  if (typeof content.html === "string") return content.html;
11162
11420
  if (typeof content.table === "string") return content.table;
@@ -11172,6 +11430,7 @@ function buildRestoredEmbedContentObject(current2, restoredContent, newVersion)
11172
11430
  if (typeof current2.remotion_source === "string") {
11173
11431
  return { ...restored, remotion_source: restoredContent, current_source_version: newVersion };
11174
11432
  }
11433
+ if (typeof current2.html_source === "string") return { ...restored, html_source: restoredContent };
11175
11434
  if (typeof current2.code === "string") return { ...restored, code: restoredContent };
11176
11435
  if (typeof current2.html === "string") return { ...restored, html: restoredContent };
11177
11436
  if (typeof current2.table === "string") return { ...restored, table: restoredContent };
@@ -11286,6 +11545,8 @@ var OpenMatesClient = class _OpenMatesClient {
11286
11545
  session;
11287
11546
  http;
11288
11547
  explicitSession;
11548
+ directChatIdentity;
11549
+ directChatUserHash;
11289
11550
  constructor(options = {}) {
11290
11551
  this.explicitSession = options.session !== void 0;
11291
11552
  const diskSession = options.session ?? (options.loadStoredSession === false ? null : this.getValidSessionFromDisk());
@@ -11551,6 +11812,8 @@ var OpenMatesClient = class _OpenMatesClient {
11551
11812
  if (!response.ok) throw new Error(`Team list failed with HTTP ${response.status}`);
11552
11813
  if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11553
11814
  const teams = response.data.teams ?? [];
11815
+ if (teams.length) await this.ensureDirectChatIdentity();
11816
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11554
11817
  await Promise.all(teams.map((team) => this.cacheTeamKeyFromRecord(team, owner2.isCurrent)));
11555
11818
  if (!owner2.isCurrent()) throw new Error("Account changed while loading Teams.");
11556
11819
  const teamIds = teams.map((team) => team.team_id).filter((teamId) => typeof teamId === "string" && teamId.length > 0);
@@ -11560,6 +11823,60 @@ var OpenMatesClient = class _OpenMatesClient {
11560
11823
  }
11561
11824
  return teams;
11562
11825
  }
11826
+ async openTeamDirectChat(teamId, recipientHash) {
11827
+ if (!teamId || !/^[0-9a-f]{64}$/.test(recipientHash)) throw new Error("A Team ID and member hash are required.");
11828
+ const owner2 = this.captureTeamAccountOwner();
11829
+ const [ownHash, ownIdentity] = await Promise.all([this.ownDirectChatHash(), this.ensureDirectChatIdentity()]);
11830
+ if (recipientHash === ownHash) throw new Error("Choose another Team member to message.");
11831
+ const cache = await this.ensureSynced(false, [], { teamId });
11832
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11833
+ const existing = cache.chats.find((row4) => row4.details.chat_visibility === "direct" && (!row4.details.team_id || row4.details.team_id === teamId) && (!row4.details.hashed_team_id || row4.details.hashed_team_id === computeSHA256(teamId)) && Array.isArray(row4.details.participant_user_hashes) && row4.details.participant_user_hashes.length === 2 && row4.details.participant_user_hashes.includes(ownHash) && row4.details.participant_user_hashes.includes(recipientHash));
11834
+ if (existing) {
11835
+ const wrapping2 = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
11836
+ return this.decryptChatListItem(existing, wrapping2, cache, teamId);
11837
+ }
11838
+ const recipient = await this.http.get(
11839
+ `/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(recipientHash)}/chat-encryption-key`,
11840
+ this.getCliRequestHeaders()
11841
+ );
11842
+ if (recipient.status === 404) throw new Error("Direct message pending: this Team member has not enrolled a chat encryption key yet.");
11843
+ if (!recipient.ok) throw new Error(`Team member chat encryption key failed with HTTP ${recipient.status}`);
11844
+ const recipientKey = await validatePublicChatEncryptionKeyId(recipient.data);
11845
+ const chatId = randomUUID9();
11846
+ const chatKey = new Uint8Array(randomBytes4(32));
11847
+ const ownEnvelope = await sealDirectChatKey(chatKey, { teamId, chatId, recipientHash: ownHash, keyId: ownIdentity.keyId }, ownIdentity.publicKey);
11848
+ const recipientEnvelope = await sealDirectChatKey(chatKey, { teamId, chatId, recipientHash, keyId: recipientKey.key_id }, recipientKey.public_key);
11849
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11850
+ const response = await this.http.post(
11851
+ `/v1/teams/${encodeURIComponent(teamId)}/direct-chats`,
11852
+ {
11853
+ chat_id: chatId,
11854
+ recipient_user_hash: recipientHash,
11855
+ participant_key_envelopes: {
11856
+ [ownHash]: { key_id: ownIdentity.keyId, encrypted_chat_key: ownEnvelope },
11857
+ [recipientHash]: { key_id: recipientKey.key_id, encrypted_chat_key: recipientEnvelope }
11858
+ },
11859
+ encrypted_title: await encryptWithAesGcmCombined("Private Team message", chatKey)
11860
+ },
11861
+ this.getCliRequestHeaders()
11862
+ );
11863
+ if (!owner2.isCurrent()) throw new Error("Account changed while opening the direct chat.");
11864
+ if (!response.ok || !response.data.chat) throw new Error(`Direct chat creation failed with HTTP ${response.status}`);
11865
+ const details = response.data.chat;
11866
+ if (details.chat_visibility !== "direct" || details.team_id !== teamId || details.id !== chatId) {
11867
+ throw new Error("Direct chat creation returned mismatched metadata.");
11868
+ }
11869
+ const latest = loadSyncCache(teamId) ?? cache;
11870
+ const row3 = { details, messages: [] };
11871
+ saveSyncCache({
11872
+ ...latest,
11873
+ chats: [row3, ...latest.chats],
11874
+ loadedChatCount: latest.loadedChatCount + 1,
11875
+ totalChatCount: latest.totalChatCount + 1
11876
+ }, teamId);
11877
+ const wrapping = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
11878
+ return this.decryptChatListItem(row3, wrapping, loadSyncCache(teamId), teamId);
11879
+ }
11563
11880
  async approveTeamName(name) {
11564
11881
  this.requireSession();
11565
11882
  const normalized = name.trim().toLowerCase();
@@ -11610,6 +11927,7 @@ var OpenMatesClient = class _OpenMatesClient {
11610
11927
  return response.data.team;
11611
11928
  }
11612
11929
  async getTeam(teamId) {
11930
+ await this.ensureDirectChatIdentity();
11613
11931
  const owner2 = this.captureTeamAccountOwner();
11614
11932
  const response = await this.http.get(`/v1/teams/${encodeURIComponent(teamId)}`, this.getCliRequestHeaders());
11615
11933
  if (!response.ok || !response.data.team) throw new Error(`Team get failed with HTTP ${response.status}`);
@@ -11659,6 +11977,7 @@ var OpenMatesClient = class _OpenMatesClient {
11659
11977
  }
11660
11978
  async listTeamMembers(teamId) {
11661
11979
  this.requireSession();
11980
+ await this.ensureDirectChatIdentity();
11662
11981
  const response = await this.http.get(`/v1/teams/${encodeURIComponent(teamId)}/members`, this.getCliRequestHeaders());
11663
11982
  if (!response.ok) throw new Error(`Team members failed with HTTP ${response.status}`);
11664
11983
  return Promise.all((response.data.members ?? []).map((member) => this.decryptTeamMember(teamId, member)));
@@ -12785,6 +13104,111 @@ var OpenMatesClient = class _OpenMatesClient {
12785
13104
  return response.data.user ?? {};
12786
13105
  });
12787
13106
  }
13107
+ async ownDirectChatHash() {
13108
+ const session = this.requireSession();
13109
+ const owner2 = this.captureTeamAccountOwner();
13110
+ const scope2 = `${session.apiUrl}:${session.sessionId}:${session.hashedEmail}:${session.masterKeyExportedB64}`;
13111
+ if (this.directChatUserHash?.scope !== scope2) {
13112
+ const cached = loadCachedDirectChatAccountKey(session, this.apiUrl);
13113
+ const promise = (cached ? Promise.resolve({ id: cached.accountId }) : this.whoAmI()).then((user2) => {
13114
+ const id = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : null;
13115
+ if (!id) throw new Error("Authenticated identity is required for direct chats.");
13116
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading direct chat identity.");
13117
+ return createHash13("sha256").update(id).digest("hex");
13118
+ }).catch((error) => {
13119
+ if (!(error instanceof TypeError)) throw error;
13120
+ const cached2 = loadCachedDirectChatAccountKey(session, this.apiUrl);
13121
+ if (!cached2) throw error;
13122
+ if (!owner2.isCurrent()) throw new Error("Account changed while loading direct chat identity.");
13123
+ return createHash13("sha256").update(cached2.accountId).digest("hex");
13124
+ }).catch((error) => {
13125
+ if (this.directChatUserHash?.promise === promise) this.directChatUserHash = void 0;
13126
+ throw error;
13127
+ });
13128
+ this.directChatUserHash = { scope: scope2, promise };
13129
+ }
13130
+ return this.directChatUserHash.promise;
13131
+ }
13132
+ async ensureDirectChatIdentity() {
13133
+ const session = this.requireSession();
13134
+ const scope2 = `${session.apiUrl}:${session.sessionId}:${session.hashedEmail}:${session.masterKeyExportedB64}`;
13135
+ if (this.directChatIdentity?.scope !== scope2) {
13136
+ const promise = this.loadDirectChatIdentity().catch((error) => {
13137
+ if (this.directChatIdentity?.promise === promise) this.directChatIdentity = void 0;
13138
+ throw error;
13139
+ });
13140
+ this.directChatIdentity = { scope: scope2, promise };
13141
+ }
13142
+ return this.directChatIdentity.promise;
13143
+ }
13144
+ async loadDirectChatIdentity() {
13145
+ const owner2 = this.captureTeamAccountOwner();
13146
+ const masterKey = this.getMasterKeyBytes();
13147
+ const recover = async (record11) => {
13148
+ const identity4 = await recoverAccountChatIdentity(record11, masterKey);
13149
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13150
+ return identity4;
13151
+ };
13152
+ const local = loadCachedDirectChatAccountKey(this.requireSession(), this.apiUrl);
13153
+ if (local) return recover(local.record);
13154
+ const cacheVerifiedRecord = async (record11) => {
13155
+ try {
13156
+ const user2 = await this.whoAmI();
13157
+ if (!owner2.isCurrent()) return;
13158
+ const accountId = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : null;
13159
+ const trustedAccountId = loadTrustedAccountId();
13160
+ if (accountId && (trustedAccountId ? trustedAccountId === accountId : process.env.OPENMATES_ACCOUNT_GUARD !== "required")) {
13161
+ saveCachedDirectChatAccountKey(this.requireSession(), this.apiUrl, accountId, record11);
13162
+ }
13163
+ } catch {
13164
+ }
13165
+ };
13166
+ let existing;
13167
+ try {
13168
+ existing = await this.http.get(
13169
+ "/v1/account/chat-encryption-key",
13170
+ this.getCliRequestHeaders()
13171
+ );
13172
+ } catch (error) {
13173
+ if (!(error instanceof TypeError)) throw error;
13174
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13175
+ const cached = loadCachedDirectChatAccountKey(this.requireSession(), this.apiUrl);
13176
+ if (!cached) throw error;
13177
+ return recover(cached.record);
13178
+ }
13179
+ if (!existing.ok && existing.status !== 404) throw new Error(`Account chat encryption key failed with HTTP ${existing.status}`);
13180
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key enrollment.");
13181
+ if (existing.data.key) {
13182
+ const identity4 = await recover(existing.data.key);
13183
+ await cacheVerifiedRecord(existing.data.key);
13184
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13185
+ return identity4;
13186
+ }
13187
+ const generated = await createAccountChatIdentity(masterKey);
13188
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key enrollment.");
13189
+ const saved = await this.http.put(
13190
+ "/v1/account/chat-encryption-key",
13191
+ generated.record,
13192
+ this.getCliRequestHeaders()
13193
+ );
13194
+ if (saved.status === 409) {
13195
+ const current2 = await this.http.get(
13196
+ "/v1/account/chat-encryption-key",
13197
+ this.getCliRequestHeaders()
13198
+ );
13199
+ if (!owner2.isCurrent() || !current2.ok || !current2.data.key) throw new Error("Account chat encryption key enrollment conflicted. Retry.");
13200
+ const identity4 = await recover(current2.data.key);
13201
+ await cacheVerifiedRecord(current2.data.key);
13202
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13203
+ return identity4;
13204
+ }
13205
+ if (!owner2.isCurrent() || !saved.ok) throw new Error(`Account chat encryption key enrollment failed with HTTP ${saved.status}`);
13206
+ const record10 = saved.data.key ?? generated.record;
13207
+ const identity3 = await recover(record10);
13208
+ await cacheVerifiedRecord(record10);
13209
+ if (!owner2.isCurrent()) throw new Error("Account changed during chat encryption key recovery.");
13210
+ return identity3;
13211
+ }
12788
13212
  async getTopicPreferences() {
12789
13213
  const user2 = await this.whoAmI();
12790
13214
  return await this.decryptTopicPreferences(user2.encrypted_settings);
@@ -13151,6 +13575,20 @@ var OpenMatesClient = class _OpenMatesClient {
13151
13575
  }
13152
13576
  async resolveChatKey(cache, cached, wrappingKey, teamId = null) {
13153
13577
  const chatId = String(cached.details.id ?? "");
13578
+ if (cached.details.chat_visibility === "direct") {
13579
+ if (!teamId || !chatId || cached.details.team_id && cached.details.team_id !== teamId || cached.details.hashed_team_id && cached.details.hashed_team_id !== computeSHA256(teamId)) {
13580
+ throw new Error("Direct chat Team binding mismatch.");
13581
+ }
13582
+ const ownHash = await this.ownDirectChatHash();
13583
+ const participants = cached.details.participant_user_hashes;
13584
+ if (!Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash)) {
13585
+ throw new Error("Direct chat participant binding mismatch.");
13586
+ }
13587
+ const encrypted = cached.details.encrypted_chat_key;
13588
+ if (typeof encrypted !== "string") throw new Error("Direct chat envelope unavailable.");
13589
+ const identity3 = await this.ensureDirectChatIdentity();
13590
+ return openDirectChatKey(encrypted, { teamId, chatId, recipientHash: ownHash, keyId: identity3.keyId }, identity3.privateKey);
13591
+ }
13154
13592
  const wrapperKey = chatId ? this.getContextWrapperEncryptedChatKey(cache, chatId, teamId) : null;
13155
13593
  const encryptedChatKey = wrapperKey ?? (typeof cached.details.encrypted_chat_key === "string" ? cached.details.encrypted_chat_key : null);
13156
13594
  return encryptedChatKey ? this.decryptChatKey(encryptedChatKey, wrappingKey) : null;
@@ -13182,6 +13620,8 @@ var OpenMatesClient = class _OpenMatesClient {
13182
13620
  const slug = chatKeyBytes && typeof d.encrypted_slug === "string" ? slugValue ?? "" : null;
13183
13621
  return {
13184
13622
  id,
13623
+ chatVisibility: d.chat_visibility === "direct" ? "direct" : teamId ? "team" : "personal",
13624
+ participantUserHashes: Array.isArray(d.participant_user_hashes) ? d.participant_user_hashes.filter((value) => typeof value === "string") : void 0,
13185
13625
  shortId: id.slice(0, 8),
13186
13626
  slug,
13187
13627
  title,
@@ -14328,17 +14768,40 @@ var OpenMatesClient = class _OpenMatesClient {
14328
14768
  const selectedTeamId = this.getActiveTeamId();
14329
14769
  const current2 = () => {
14330
14770
  const active = this.session;
14331
- if (!active || active.apiUrl !== owner2[0] || active.hashedEmail !== owner2[1] || active.sessionId !== owner2[2] || active.createdAt !== owner2[3] || active.masterKeyExportedB64 !== owner2[4] || options.preferCache && this.getActiveTeamId() !== selectedTeamId)
14771
+ if (!active || active.apiUrl !== owner2[0] || active.hashedEmail !== owner2[1] || active.sessionId !== owner2[2] || active.createdAt !== owner2[3] || active.masterKeyExportedB64 !== owner2[4] || this.getActiveTeamId() !== selectedTeamId || this.resolveTeamContext(options) !== teamId)
14332
14772
  throw new Error("Chat workspace changed");
14333
14773
  };
14334
- const cache = options.preferCache ? loadSyncCache(teamId) : await this.ensureSynced(false, [], options);
14774
+ let cache = options.preferCache ? loadSyncCache(teamId) : await this.ensureSynced(false, [], options);
14335
14775
  current2();
14336
14776
  if (!cache) throw new Error("Chat cache unavailable. Open the chat to refresh it.");
14777
+ const diskCacheAtStart = loadSyncCache(teamId);
14778
+ const diskChatIdsAtStart = diskCacheAtStart?.chats.map((chat) => String(chat.details.id));
14337
14779
  const localTeamKey = options.preferCache && teamId ? loadLocalTeamKey(owner2[1], teamId) : null;
14338
14780
  if (options.preferCache && teamId && !localTeamKey) throw new Error("Team key unavailable offline. Reconnect to refresh Team access.");
14339
14781
  const wrappingKey = localTeamKey ? base64ToBytes(localTeamKey) : await this.getChatWrappingKey(teamId, masterKey);
14340
14782
  current2();
14341
- const found = await this.resolveCachedChatForQuery(query, cache, wrappingKey, teamId);
14783
+ let found;
14784
+ let fetchedMetadata = false;
14785
+ try {
14786
+ found = await this.resolveCachedChatForQuery(query, cache, wrappingKey, teamId);
14787
+ } catch (error) {
14788
+ if (options.preferCache || !(error instanceof Error) || !/^Chat '.*' not found\./.test(error.message) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(query)) throw error;
14789
+ const response2 = await this.http.post(
14790
+ this.appendTeamQuery("/v1/chats/metadata/batch", { teamId }),
14791
+ { chat_ids: [query] },
14792
+ this.getCliRequestHeaders()
14793
+ );
14794
+ current2();
14795
+ if (!response2.ok) throw new Error(`Chat metadata failed with HTTP ${response2.status}`);
14796
+ const details = response2.data.chats.find((chat) => String(chat.id ?? "") === query);
14797
+ if (!details) throw error;
14798
+ found = { details, messages: [] };
14799
+ fetchedMetadata = true;
14800
+ cache = { ...cache, chats: [...cache.chats, found], chatKeyWrappers: [
14801
+ ...cache.chatKeyWrappers ?? [],
14802
+ ...Array.isArray(details.chat_key_wrappers) ? details.chat_key_wrappers : []
14803
+ ] };
14804
+ }
14342
14805
  current2();
14343
14806
  const chatItem = await this.decryptChatListItem(found, wrappingKey, cache, teamId);
14344
14807
  current2();
@@ -14366,17 +14829,19 @@ var OpenMatesClient = class _OpenMatesClient {
14366
14829
  const rawMessages = Array.isArray(response.data.messages) ? response.data.messages : [];
14367
14830
  const messages = await this.decryptRawChatMessages(rawMessages, chatItem, chatKeyBytes, cache.embeds);
14368
14831
  current2();
14369
- if (options.preferCache && teamId && (options.direction ?? "latest") === "latest") {
14832
+ if ((options.direction ?? "latest") === "latest") {
14370
14833
  const latestCache = loadSyncCache(teamId);
14371
14834
  const row3 = latestCache?.chats.find((chat) => String(chat.details.id) === chatId);
14372
- if (row3 && row3.details.messages_v === found.details.messages_v && row3.details.encrypted_chat_key === found.details.encrypted_chat_key && row3.messages.length === found.messages.length && row3.messages.every((raw, index) => raw === found.messages[index])) {
14835
+ const sameVersion = row3 && row3.details.messages_v === found.details.messages_v && row3.details.encrypted_chat_key === found.details.encrypted_chat_key && row3.messages.length === found.messages.length && row3.messages.every((raw, index) => raw === found.messages[index]);
14836
+ if (latestCache && (sameVersion || fetchedMetadata && !row3 && diskCacheAtStart && latestCache.syncedAt === diskCacheAtStart.syncedAt && latestCache.totalChatCount === diskCacheAtStart.totalChatCount && latestCache.loadedChatCount === diskCacheAtStart.loadedChatCount && latestCache.chats.length === diskChatIdsAtStart?.length && latestCache.chats.every((chat, index) => String(chat.details.id) === diskChatIdsAtStart[index]))) {
14373
14837
  const canonicalId = (raw) => {
14374
14838
  const value = typeof raw === "string" ? JSON.parse(raw) : raw;
14375
14839
  return String(value.client_message_id ?? value.id ?? value.message_id ?? "");
14376
14840
  };
14377
- const existingById = new Map(row3.messages.map((raw, index) => [canonicalId(raw), index]));
14841
+ const existing = row3?.messages ?? [];
14842
+ const existingById = new Map(existing.map((raw, index) => [canonicalId(raw), index]));
14378
14843
  const overlap = rawMessages.find((raw) => !!canonicalId(raw) && existingById.has(canonicalId(raw)));
14379
- const older = response.data.has_more_before !== true ? [] : overlap ? row3.messages.slice(0, existingById.get(canonicalId(overlap))) : row3.messages;
14844
+ const older = response.data.has_more_before !== true ? [] : overlap ? existing.slice(0, existingById.get(canonicalId(overlap))) : existing;
14380
14845
  const byId = /* @__PURE__ */ new Map();
14381
14846
  for (const raw of [...older, ...rawMessages]) {
14382
14847
  const value = typeof raw === "string" ? JSON.parse(raw) : raw;
@@ -14386,8 +14851,21 @@ var OpenMatesClient = class _OpenMatesClient {
14386
14851
  }
14387
14852
  const merged = [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).map((message) => message.raw);
14388
14853
  current2();
14389
- if (merged.length !== row3.messages.length || merged.some((raw, index) => raw !== row3.messages[index]))
14390
- saveSyncCache({ ...latestCache, chats: latestCache.chats.map((chat) => chat === row3 ? { ...row3, messages: merged } : chat) }, teamId);
14854
+ try {
14855
+ if (!row3) {
14856
+ const wrappers = Array.isArray(found.details.chat_key_wrappers) ? found.details.chat_key_wrappers : [];
14857
+ saveSyncCache({
14858
+ ...latestCache,
14859
+ chats: [...latestCache.chats, { details: found.details, messages: merged }],
14860
+ loadedChatCount: latestCache.loadedChatCount + 1,
14861
+ totalChatCount: Math.max(latestCache.totalChatCount, latestCache.loadedChatCount + 1),
14862
+ chatKeyWrappers: [...latestCache.chatKeyWrappers ?? [], ...wrappers]
14863
+ }, teamId);
14864
+ } else if (merged.length !== row3.messages.length || merged.some((raw, index) => raw !== row3.messages[index])) {
14865
+ saveSyncCache({ ...latestCache, chats: latestCache.chats.map((chat) => chat === row3 ? { ...row3, messages: merged } : chat) }, teamId);
14866
+ }
14867
+ } catch {
14868
+ }
14391
14869
  }
14392
14870
  }
14393
14871
  return {
@@ -15232,7 +15710,8 @@ var OpenMatesClient = class _OpenMatesClient {
15232
15710
  code: "code",
15233
15711
  pcb_schematic: "code",
15234
15712
  sheet: "table",
15235
- mermaid: "diagram_code"
15713
+ mermaid: "diagram_code",
15714
+ diagram_html: "html_source"
15236
15715
  };
15237
15716
  const field3 = historicalFieldByType[content.type];
15238
15717
  if (!field3 || typeof sealedEmbed[field3] !== "string") {
@@ -15683,7 +16162,7 @@ var OpenMatesClient = class _OpenMatesClient {
15683
16162
  baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
15684
16163
  const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
15685
16164
  if (encKey) {
15686
- chatKeyBytes = await decryptBytesWithAesGcm(encKey, wrappingKey);
16165
+ chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
15687
16166
  encryptedChatKey = encKey;
15688
16167
  if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
15689
16168
  if (typeof chat.details.encrypted_title === "string") {
@@ -16598,7 +17077,8 @@ var OpenMatesClient = class _OpenMatesClient {
16598
17077
  chatId,
16599
17078
  chatKeyBytes,
16600
17079
  fallbackMessageId: assistantId,
16601
- ownerId: ownerId2
17080
+ ownerId: ownerId2,
17081
+ teamId
16602
17082
  });
16603
17083
  clearSyncCache(teamId);
16604
17084
  await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
@@ -16749,7 +17229,8 @@ var OpenMatesClient = class _OpenMatesClient {
16749
17229
  chatId,
16750
17230
  chatKeyBytes,
16751
17231
  fallbackMessageId: assistantId,
16752
- ownerId: ownerId2
17232
+ ownerId: ownerId2,
17233
+ teamId
16753
17234
  });
16754
17235
  if (isNewChat && (resp.generatedTitle || resp.generatedIcon)) {
16755
17236
  const metadataStored = ws.waitForMessage(
@@ -17196,6 +17677,12 @@ var OpenMatesClient = class _OpenMatesClient {
17196
17677
  version_number: embed.version_number,
17197
17678
  file_path: embed.file_path,
17198
17679
  content_hash: embed.content_hash,
17680
+ ...embed.type === "diagram_html" ? {
17681
+ app_id: "diagrams",
17682
+ skill_id: "html",
17683
+ chat_id: params.chatId,
17684
+ ...params.teamId ? { team_id: params.teamId } : {}
17685
+ } : {},
17199
17686
  text_length_chars: embed.text_length_chars,
17200
17687
  is_private: embed.is_private ?? false,
17201
17688
  is_shared: embed.is_shared ?? false,
@@ -21004,6 +21491,66 @@ Required: ${schema.required.join(", ")}`
21004
21491
  const now = Math.floor(Date.now() / 1e3);
21005
21492
  const { ws } = await this.openWsClient();
21006
21493
  try {
21494
+ if (currentObject.type === "diagram_html") {
21495
+ const chat = cache.chats.find((entry) => computeSHA256(String(entry.details.id ?? "")) === embed.hashed_chat_id);
21496
+ const chatId = String(chat?.details.id ?? "");
21497
+ if (!chatId || typeof embed.content_hash !== "string" || embed.content_hash !== computeSHA256(currentContent)) {
21498
+ throw new Error("Diagram restore requires a current chat and matching source hash.");
21499
+ }
21500
+ const teamId = this.resolveTeamContext();
21501
+ const versions = await this.listEmbedVersions(embedId, { chatId, teamId: teamId ?? void 0 });
21502
+ if (versions.readonly) throw new Error("Diagram version history is read-only.");
21503
+ const historyRows = [];
21504
+ if (currentVersion === 1 && !versions.versions.some((row3) => row3.version_number === 1)) {
21505
+ historyRows.push({
21506
+ version_number: 1,
21507
+ encrypted_snapshot: await encryptWithAesGcmCombined(currentContent, embedKey),
21508
+ created_at: normalizeUnixSeconds(embed.created_at, now)
21509
+ });
21510
+ }
21511
+ historyRows.push({
21512
+ version_number: newVersion,
21513
+ encrypted_patch: encryptedPatch,
21514
+ encrypted_snapshot: encryptedSnapshot,
21515
+ created_at: now
21516
+ });
21517
+ const requestId = randomUUID9();
21518
+ const operationId = randomUUID9();
21519
+ const receipt = ws.waitForMessage(
21520
+ "commit_chat_diagram_revision_result",
21521
+ (payload) => payload.request_id === requestId,
21522
+ 3e4
21523
+ );
21524
+ await ws.sendAsync("commit_chat_diagram_revision", {
21525
+ request_id: requestId,
21526
+ operation_id: operationId,
21527
+ embed_id: embedId,
21528
+ chat_id: chatId,
21529
+ ...teamId ? { team_id: teamId } : {},
21530
+ expected_revision: currentVersion,
21531
+ expected_content_hash: embed.content_hash,
21532
+ head: {
21533
+ encrypted_content: encryptedRestoredContent,
21534
+ encrypted_text_preview: embed.encrypted_text_preview ?? null,
21535
+ content_hash: contentHash,
21536
+ status: "finished",
21537
+ updated_at: now
21538
+ },
21539
+ history_rows: historyRows
21540
+ });
21541
+ const result = (await receipt).payload;
21542
+ if (result.status !== "committed" || result.current_revision !== newVersion || result.current_content_hash !== contentHash) {
21543
+ throw new Error(result.status === "conflict" ? "Diagram advanced on another device; reload and retry restore." : `Diagram restore was rejected: ${String(result.code ?? "unknown")}`);
21544
+ }
21545
+ clearSyncCache(teamId);
21546
+ return {
21547
+ embed_id: embedId,
21548
+ restored_from_version: version,
21549
+ version_number: newVersion,
21550
+ content: target.content,
21551
+ content_hash: contentHash
21552
+ };
21553
+ }
21007
21554
  const storeRequestId = randomUUID9();
21008
21555
  const storeConfirmed = ws.waitForMessage(
21009
21556
  "store_embed_confirmed",
@@ -28870,6 +29417,7 @@ var OpenMates = class {
28870
29417
  sdkName;
28871
29418
  sdkSessionPromise;
28872
29419
  masterKeyPromise;
29420
+ chatIdentityPromise;
28873
29421
  constructor(options = {}) {
28874
29422
  this.apiKey = options.apiKey ?? process.env.OPENMATES_API_KEY;
28875
29423
  this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL2).replace(/\/$/, "");
@@ -28974,6 +29522,34 @@ var OpenMates = class {
28974
29522
  sdkSession() {
28975
29523
  return this.getSdkSession();
28976
29524
  }
29525
+ async chatIdentity() {
29526
+ if (!this.chatIdentityPromise) this.chatIdentityPromise = this.loadChatIdentity().catch((error) => {
29527
+ this.chatIdentityPromise = void 0;
29528
+ throw error;
29529
+ });
29530
+ return this.chatIdentityPromise;
29531
+ }
29532
+ async loadChatIdentity() {
29533
+ const masterKey = await this.masterKey();
29534
+ let response;
29535
+ try {
29536
+ response = await this.get("/v1/account/chat-encryption-key");
29537
+ } catch (error) {
29538
+ if (!(error instanceof OpenMatesApiError) || error.status !== 404) throw error;
29539
+ response = { key: null };
29540
+ }
29541
+ if (response.key) return recoverAccountChatIdentity(response.key, masterKey);
29542
+ const generated = await createAccountChatIdentity(masterKey);
29543
+ try {
29544
+ const saved = await this.put("/v1/account/chat-encryption-key", generated.record);
29545
+ return recoverAccountChatIdentity(saved.key ?? generated.record, masterKey);
29546
+ } catch (error) {
29547
+ if (!(error instanceof OpenMatesApiError) || error.status !== 409) throw error;
29548
+ const current2 = await this.get("/v1/account/chat-encryption-key");
29549
+ if (!current2.key) throw new OpenMatesConfigError("Account chat encryption key enrollment conflicted; retry.");
29550
+ return recoverAccountChatIdentity(current2.key, masterKey);
29551
+ }
29552
+ }
28977
29553
  /** @internal Resource-key resolver shared by the Project namespace. */
28978
29554
  async _getProjectGrantKey(projectId) {
28979
29555
  const session = await this.getSdkSession();
@@ -29048,6 +29624,26 @@ var OpenMates = class {
29048
29624
  return { ...payload, chat: decryptedChat, messages, embeds };
29049
29625
  }
29050
29626
  async resolveLoadedChatKey(chat, chatKeyWrappers) {
29627
+ if (chat.chat_visibility === "direct") {
29628
+ const session = await this.sdkSession();
29629
+ const userId = session.user?.id;
29630
+ if (!userId) throw new OpenMatesConfigError("Authenticated identity is required for a direct chat");
29631
+ const ownHash = createHash14("sha256").update(userId).digest("hex");
29632
+ const participants = chat.participant_user_hashes;
29633
+ if (!Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash)) {
29634
+ throw new OpenMatesConfigError("Direct chat participant binding mismatch");
29635
+ }
29636
+ const identity3 = await this.chatIdentity();
29637
+ if (typeof chat.team_id !== "string" || typeof chat.encrypted_chat_key !== "string") {
29638
+ throw new OpenMatesConfigError("Direct chat key metadata is incomplete");
29639
+ }
29640
+ return openDirectChatKey(chat.encrypted_chat_key, {
29641
+ teamId: chat.team_id,
29642
+ chatId: chat.id,
29643
+ recipientHash: ownHash,
29644
+ keyId: identity3.keyId
29645
+ }, identity3.privateKey);
29646
+ }
29051
29647
  const hashedChatId = createHash14("sha256").update(chat.id).digest("hex");
29052
29648
  const wrapper = (chatKeyWrappers ?? []).find(
29053
29649
  (entry) => entry.key_type === "master" && entry.hashed_chat_id === hashedChatId && typeof entry.encrypted_chat_key === "string"
@@ -32684,9 +33280,11 @@ var OpenMatesTeams = class {
32684
33280
  }
32685
33281
  async list() {
32686
33282
  const result = await this.client.get("/v1/teams");
33283
+ if (result.teams?.length) await this.client.chatIdentity();
32687
33284
  return result.teams ?? [];
32688
33285
  }
32689
33286
  async get(teamId) {
33287
+ await this.client.chatIdentity();
32690
33288
  const result = await this.client.get(`/v1/teams/${encodeURIComponent(teamId)}`);
32691
33289
  return result.team ?? result;
32692
33290
  }
@@ -32744,6 +33342,62 @@ var OpenMatesTeams = class {
32744
33342
  async members(teamId) {
32745
33343
  return this.client.get(`/v1/teams/${encodeURIComponent(teamId)}/members`);
32746
33344
  }
33345
+ async memberChatEncryptionKey(teamId, memberHash) {
33346
+ if (!/^[0-9a-f]{64}$/.test(memberHash)) throw new OpenMatesConfigError("Member hash must be a SHA-256 hex digest");
33347
+ try {
33348
+ const result = await this.client.get(
33349
+ `/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(memberHash)}/chat-encryption-key`
33350
+ );
33351
+ return validatePublicChatEncryptionKeyId(result);
33352
+ } catch (error) {
33353
+ if (error instanceof OpenMatesApiError && error.status === 404) {
33354
+ throw new OpenMatesConfigError("Direct message pending: this Team member has not enrolled a chat encryption key yet.");
33355
+ }
33356
+ throw error;
33357
+ }
33358
+ }
33359
+ async openDirectChat(teamId, recipientHash) {
33360
+ if (!teamId || !/^[0-9a-f]{64}$/.test(recipientHash)) throw new OpenMatesConfigError("A Team ID and member hash are required");
33361
+ const session = await this.client.sdkSession();
33362
+ const userId = session.user?.id;
33363
+ if (!userId) throw new OpenMatesConfigError("Authenticated identity is required for a direct chat");
33364
+ const ownHash = createHash14("sha256").update(userId).digest("hex");
33365
+ if (ownHash === recipientHash) throw new OpenMatesConfigError("Choose another Team member to message");
33366
+ const [ownIdentity, recipientKey] = await Promise.all([
33367
+ this.client.chatIdentity(),
33368
+ this.memberChatEncryptionKey(teamId, recipientHash)
33369
+ ]);
33370
+ const chatId = randomUUID10();
33371
+ const chatKey = new Uint8Array(randomBytes6(32));
33372
+ const ownEnvelope = await sealDirectChatKey(
33373
+ chatKey,
33374
+ { teamId, chatId, recipientHash: ownHash, keyId: ownIdentity.keyId },
33375
+ ownIdentity.publicKey
33376
+ );
33377
+ const recipientEnvelope = await sealDirectChatKey(
33378
+ chatKey,
33379
+ { teamId, chatId, recipientHash, keyId: recipientKey.key_id },
33380
+ recipientKey.public_key
33381
+ );
33382
+ const result = await this.client.request(
33383
+ `/v1/teams/${encodeURIComponent(teamId)}/direct-chats`,
33384
+ {
33385
+ chat_id: chatId,
33386
+ recipient_user_hash: recipientHash,
33387
+ participant_key_envelopes: {
33388
+ [ownHash]: { key_id: ownIdentity.keyId, encrypted_chat_key: ownEnvelope },
33389
+ [recipientHash]: { key_id: recipientKey.key_id, encrypted_chat_key: recipientEnvelope }
33390
+ },
33391
+ encrypted_title: await encryptWithAesGcmCombined("Private Team message", chatKey)
33392
+ }
33393
+ );
33394
+ if (!result.chat) throw new OpenMatesConfigError("Direct chat creation returned no metadata");
33395
+ const participants = result.chat.participant_user_hashes;
33396
+ if (result.chat.chat_visibility !== "direct" || result.chat.team_id !== teamId || result.chat.id !== chatId || !Array.isArray(participants) || participants.length !== 2 || !participants.includes(ownHash) || !participants.includes(recipientHash)) {
33397
+ throw new OpenMatesConfigError("Direct chat creation returned mismatched metadata");
33398
+ }
33399
+ return this.client.decryptChatMetadata(result.chat);
33400
+ }
32747
33401
  async decryptMemberProfile(teamKey, member) {
32748
33402
  const encrypted = member.encrypted_member_profile;
32749
33403
  if (typeof encrypted !== "string" || !encrypted) return { ...member, profile: null };
@@ -34367,6 +35021,8 @@ var DIRECT_TYPES = /* @__PURE__ */ new Set([
34367
35021
  "recording",
34368
35022
  "mail-email",
34369
35023
  "math-plot",
35024
+ "diagram_html",
35025
+ "diagrams-html",
34370
35026
  "mindmap",
34371
35027
  "mindmaps-mindmap",
34372
35028
  "events-event",
@@ -34398,6 +35054,8 @@ var DIRECT_TYPE_LABELS = {
34398
35054
  "recording": "recording",
34399
35055
  "mail-email": "email",
34400
35056
  "math-plot": "plot",
35057
+ "diagram_html": "diagram",
35058
+ "diagrams-html": "diagram",
34401
35059
  "mindmap": "Mind Map",
34402
35060
  "mindmaps-mindmap": "Mind Map",
34403
35061
  "events-event": "event",
@@ -35469,6 +36127,15 @@ function renderByDirectType(embed, c, ln) {
35469
36127
  ln("\x1B[2m[mathematical plot]\x1B[0m");
35470
36128
  break;
35471
36129
  }
36130
+ case "diagram_html":
36131
+ case "diagrams-html": {
36132
+ ln(str(c.title) ?? "Diagram");
36133
+ const kind = str(c.diagram_kind) ?? "diagram";
36134
+ const width = typeof c.render_width === "number" ? c.render_width : 0;
36135
+ const height = typeof c.render_height === "number" ? c.render_height : 0;
36136
+ ln(`\x1B[2m${kind}${width && height ? ` \xB7 ${width} \xD7 ${height}` : ""}\x1B[0m`);
36137
+ break;
36138
+ }
35472
36139
  case "mindmap":
35473
36140
  case "mindmaps-mindmap": {
35474
36141
  const document = mindMapDocumentFromContent(c);
@@ -35660,6 +36327,18 @@ function renderDirectTypeFullscreen(embed, c, write) {
35660
36327
  }
35661
36328
  break;
35662
36329
  }
36330
+ case "diagram_html":
36331
+ case "diagrams-html": {
36332
+ writeEmbedLine(write, `Title: ${str(c.title) ?? "Diagram"}`);
36333
+ writeEmbedLine(write, `Kind: ${str(c.diagram_kind) ?? "diagram"}`);
36334
+ if (typeof c.render_width === "number" && typeof c.render_height === "number") {
36335
+ writeEmbedLine(write, `Size: ${c.render_width} \xD7 ${c.render_height}`);
36336
+ }
36337
+ writeEmbedLine(write, `Version: ${typeof c.version_number === "number" ? c.version_number : 1}`);
36338
+ writeEmbedLine(write, `Source: ${typeof c.html_source === "string" ? c.html_source.length : 0} characters`);
36339
+ writeEmbedLine(write, "Export with: openmates chats download <chat> --output <directory>");
36340
+ break;
36341
+ }
35663
36342
  case "mindmap":
35664
36343
  case "mindmaps-mindmap": {
35665
36344
  const document = mindMapDocumentFromContent(c);
@@ -67743,6 +68422,62 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
67743
68422
  }
67744
68423
  }
67745
68424
  },
68425
+ diagrams: {
68426
+ html: {
68427
+ text: "Editable Diagram"
68428
+ },
68429
+ edit: {
68430
+ text: "Edit diagram"
68431
+ },
68432
+ loading: {
68433
+ text: "Preparing diagram\u2026"
68434
+ },
68435
+ invalid_source: {
68436
+ text: "This diagram contains unsupported content."
68437
+ },
68438
+ conflict: {
68439
+ text: "The diagram changed on another device. Reload before editing."
68440
+ },
68441
+ saved: {
68442
+ text: "Diagram saved"
68443
+ },
68444
+ saved_refresh: {
68445
+ text: "Diagram saved. Refresh this page to sync this device before editing again."
68446
+ },
68447
+ history_unavailable: {
68448
+ text: "This diagram version could not be loaded. Choose another version to retry."
68449
+ },
68450
+ history_version: {
68451
+ text: "Viewing version"
68452
+ },
68453
+ save_failed: {
68454
+ text: "Could not save diagram"
68455
+ },
68456
+ zoom_out: {
68457
+ text: "Zoom out"
68458
+ },
68459
+ zoom_in: {
68460
+ text: "Zoom in"
68461
+ },
68462
+ fit: {
68463
+ text: "Fit diagram"
68464
+ },
68465
+ export: {
68466
+ text: "Export HTML"
68467
+ },
68468
+ undo: {
68469
+ text: "Undo"
68470
+ },
68471
+ cancel: {
68472
+ text: "Cancel"
68473
+ },
68474
+ save: {
68475
+ text: "Save changes"
68476
+ },
68477
+ saving: {
68478
+ text: "Saving\u2026"
68479
+ }
68480
+ },
67746
68481
  digital_team_mates: {
67747
68482
  digital_team_mates: {
67748
68483
  text: "Digital Team Mates"
@@ -70474,6 +71209,9 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
70474
71209
  diagrams: {
70475
71210
  mermaid: {
70476
71211
  text: "Mermaid Diagram"
71212
+ },
71213
+ html: {
71214
+ text: "Editable Diagram"
70477
71215
  }
70478
71216
  },
70479
71217
  mail: {
@@ -78196,6 +78934,14 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
78196
78934
  }
78197
78935
  },
78198
78936
  notifications: {
78937
+ team_direct_chat: {
78938
+ title: {
78939
+ text: "Private Team message"
78940
+ },
78941
+ body: {
78942
+ text: "You have a new private conversation in your Team."
78943
+ }
78944
+ },
78199
78945
  auto_logout: {
78200
78946
  title: {
78201
78947
  text: "Auto logout"
@@ -86391,6 +87137,18 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
86391
87137
  members_description: {
86392
87138
  text: "Manage your team members and their permissions."
86393
87139
  },
87140
+ direct_message_action: {
87141
+ text: "Message"
87142
+ },
87143
+ direct_message_scope: {
87144
+ text: "Only you and this member can read this chat."
87145
+ },
87146
+ direct_message_pending: {
87147
+ text: "Secure messaging setup is pending. Ask this member to open OpenMates."
87148
+ },
87149
+ direct_message_failed: {
87150
+ text: "Could not open a private message. Try again."
87151
+ },
86394
87152
  admins: {
86395
87153
  text: "Admins"
86396
87154
  },
@@ -99434,6 +100192,8 @@ var TUI_ACTIONS = [
99434
100192
  { label: "Show all workspace items", command: "/browse" },
99435
100193
  { label: "Use selected app skill", command: "/app-run" },
99436
100194
  { label: "Recent chats", command: "/chats" },
100195
+ { label: "Team members", command: "/members" },
100196
+ { label: "Message Team member", command: "/message" },
99437
100197
  { label: "Example chats", command: "/examples" },
99438
100198
  { label: "Projects", command: "/projects" },
99439
100199
  { label: "Tasks", command: "/tasks" },
@@ -102073,7 +102833,7 @@ function relativeTime(timestamp) {
102073
102833
  return `${Math.floor(seconds / 86400)}d ago`;
102074
102834
  }
102075
102835
  function renderStatus(state, width) {
102076
- return ["OpenMates", "", state.status ?? "Working..."].flatMap((line3) => wrap(line3, width));
102836
+ return ["OpenMates", "", ...(state.status ?? "Working...").split("\n")].flatMap((line3) => wrap(line3, width).map((text7) => line3 === "Message \xB7 Enter" ? pointerLine(text7, { kind: "key", name: "return" }) : line3 === "Next member \xB7 \u2193" ? pointerLine(text7, { kind: "key", name: "down" }) : text7));
102077
102837
  }
102078
102838
  function prepareMessageContent(content, width, state, questionBlocks = true) {
102079
102839
  const segments2 = parseMessageSegments(content, { preserveCodeFences: true }).map((segment) => segment.type === "embed" ? segment : {
@@ -104801,6 +105561,19 @@ function createChatModelPreferences(client2) {
104801
105561
  }
104802
105562
 
104803
105563
  // src/tui.ts
105564
+ var teamMemberActions = /* @__PURE__ */ new WeakMap();
105565
+ function showSelectedTeamMember(state) {
105566
+ const selection = teamMemberActions.get(state);
105567
+ if (!selection?.members.length) {
105568
+ state.status = "No other active Team members are available.";
105569
+ return;
105570
+ }
105571
+ const member = selection.members[state.selectedIndex] ?? selection.members[0];
105572
+ state.status = `Team members ${state.selectedIndex + 1}/${selection.members.length}
105573
+ ${member.label}
105574
+ Message \xB7 Enter
105575
+ Next member \xB7 \u2193`;
105576
+ }
104804
105577
  function defaultModeForStreams(input, output) {
104805
105578
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
104806
105579
  }
@@ -105083,6 +105856,21 @@ async function handleKey(params) {
105083
105856
  modelShell,
105084
105857
  mentionShell
105085
105858
  };
105859
+ const memberActions = teamMemberActions.get(state);
105860
+ if (state.screen === "status" && memberActions && !memberActions.isCurrent()) teamMemberActions.delete(state);
105861
+ if (state.screen === "status" && memberActions && memberActions.isCurrent() && memberActions.teamId === client2.getActiveTeamId()) {
105862
+ if (memberActions.members.length > 0 && (key.name === "up" || key.name === "down")) {
105863
+ state.selectedIndex = (state.selectedIndex + (key.name === "down" ? 1 : -1) + memberActions.members.length) % memberActions.members.length;
105864
+ showSelectedTeamMember(state);
105865
+ render();
105866
+ return;
105867
+ }
105868
+ if (key.name === "return" && !state.input.trim()) {
105869
+ const member = memberActions.members[state.selectedIndex];
105870
+ if (member) await handleCommand({ command: `/message ${member.hash}`, state, client: client2, terminal, render, finish, modelShell });
105871
+ return;
105872
+ }
105873
+ }
105086
105874
  if (key.name === "mouseclick") {
105087
105875
  if (!key.mouse || key.ctrl || key.meta || key.shift) return;
105088
105876
  const action2 = pointerTargetAt(state, key.mouse.column, key.mouse.row, terminal.width, terminal.height);
@@ -105258,6 +106046,50 @@ async function handleCommand(params) {
105258
106046
  rememberDraft(state);
105259
106047
  state.input = "";
105260
106048
  state.inputCursor = null;
106049
+ if (name === "/members") {
106050
+ const teamId = client2.getActiveTeamId();
106051
+ if (!teamId) {
106052
+ state.status = "Select a Team to view members.";
106053
+ state.screen = "status";
106054
+ render();
106055
+ return;
106056
+ }
106057
+ const isCurrent = captureTuiWorkspaceOwner(client2);
106058
+ const members = await client2.listTeamMembers(teamId);
106059
+ if (!isCurrent() || client2.getActiveTeamId() !== teamId) return;
106060
+ const user2 = await client2.whoAmI();
106061
+ if (!isCurrent() || client2.getActiveTeamId() !== teamId) return;
106062
+ const ownId = typeof user2.id === "string" ? user2.id : typeof user2.user_id === "string" ? user2.user_id : "";
106063
+ teamMemberActions.set(state, { teamId, isCurrent, members: members.filter((member) => member.status === "active" && member.hashed_user_id && member.user_id !== ownId).map((member) => ({ hash: member.hashed_user_id, label: `${member.profile?.display_name || member.user_id} \xB7 ${member.role}` })) });
106064
+ state.selectedIndex = 0;
106065
+ state.screen = "status";
106066
+ showSelectedTeamMember(state);
106067
+ render();
106068
+ return;
106069
+ }
106070
+ if (name === "/message") {
106071
+ const teamId = client2.getActiveTeamId();
106072
+ if (!teamId) {
106073
+ state.status = "Select a Team to message a member.";
106074
+ state.screen = "status";
106075
+ render();
106076
+ return;
106077
+ }
106078
+ const memberHash = parts[0];
106079
+ if (!memberHash) {
106080
+ state.status = "Use /members to find a member, then /message <member-hash>.";
106081
+ state.screen = "status";
106082
+ render();
106083
+ return;
106084
+ }
106085
+ const chat = await client2.openTeamDirectChat(teamId, memberHash);
106086
+ if (client2.getActiveTeamId() !== teamId) return;
106087
+ state.recentChats = [chat, ...state.recentChats.filter((item) => item.id !== chat.id)];
106088
+ await openSavedChat({ state, client: client2, terminal, render, command: async () => {
106089
+ }, send: async () => {
106090
+ } }, chat.id);
106091
+ return;
106092
+ }
105261
106093
  if (name === "/privacy") {
105262
106094
  const action2 = parts[0] ?? "status";
105263
106095
  if (action2 === "later") {
@@ -115109,24 +115941,33 @@ async function handleTeams(client2, subcommand, rest, flags) {
115109
115941
  else printTeamRecord(team, client2.getActiveTeamId());
115110
115942
  return;
115111
115943
  }
115112
- if (subcommand === "members") {
115944
+ if (subcommand === "members" || subcommand === "member") {
115113
115945
  const action2 = rest[0];
115114
115946
  const teamId = requiredStringFlag(rest[1], "<team-id>");
115947
+ if (action2 === "message") {
115948
+ const memberHash = requiredStringFlag(rest[2], "<member-hash>");
115949
+ const chat = await client2.openTeamDirectChat(teamId, memberHash);
115950
+ const message = rest.slice(3).join(" ").trim();
115951
+ const response = message ? await client2.sendMessage({ message, chatId: chat.id, teamId }) : void 0;
115952
+ if (flags.json === true) printJson2({ chat_id: chat.id, chat, ...response ? { response } : {} });
115953
+ else console.log(message ? `Message sent in direct chat ${chat.id}` : `Direct chat: ${chat.id}`);
115954
+ return;
115955
+ }
115115
115956
  if (action2 === "list") {
115116
115957
  const members = (await client2.listTeamMembers(teamId)).map(publicTeamMember);
115117
115958
  if (flags.json === true) printJson2({ members });
115118
115959
  else if (!members.length) console.log("No Team members.");
115119
- else for (const member of members) console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.user_id}`.trim());
115960
+ else for (const member of members) console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.hashed_user_id || member.user_id}`.trim());
115120
115961
  return;
115121
115962
  }
115122
115963
  if (action2 === "show") {
115123
115964
  const userId = requiredStringFlag(rest[2], "<member-user-id>");
115124
115965
  const member = publicTeamMember(await client2.getTeamMember(teamId, userId));
115125
115966
  if (flags.json === true) printJson2({ member });
115126
- else console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.user_id}`.trim());
115967
+ else console.log(`${member.display_name || member.user_id} ${member.role} ${member.status || ""} ${member.hashed_user_id || member.user_id}`.trim());
115127
115968
  return;
115128
115969
  }
115129
- throw new Error("Use teams members list <team-id> or teams members show <team-id> <member-user-id>.");
115970
+ throw new Error("Use teams members list, show, or message <team-id> <member-hash> [text].");
115130
115971
  }
115131
115972
  if (subcommand === "profile") {
115132
115973
  const action2 = rest[0];
@@ -116889,6 +117730,7 @@ ${deleted}/${resolved.length} chat(s) deleted.`);
116889
117730
  }
116890
117731
  const codeEmbeds = [];
116891
117732
  const transcriptEmbeds = [];
117733
+ const diagramEmbeds = [];
116892
117734
  const processedEmbedIds = /* @__PURE__ */ new Set();
116893
117735
  const allEmbedIds = [];
116894
117736
  for (const msg of messages) {
@@ -116905,6 +117747,9 @@ ${deleted}/${resolved.length} chat(s) deleted.`);
116905
117747
  const eid = embedQueue.shift();
116906
117748
  try {
116907
117749
  const embed = await client2.getEmbed(eid);
117750
+ if ((embed.type === "diagram_html" || embed.type === "diagrams-html") && typeof embed.content?.html_source === "string" && embed.content.html_source) {
117751
+ diagramEmbeds.push({ embedId: embed.embedId, content: embed.content.html_source });
117752
+ }
116908
117753
  if (embed.type === "code" && embed.content) {
116909
117754
  const code = typeof embed.content.code === "string" ? embed.content.code : typeof embed.content.content === "string" ? embed.content.content : null;
116910
117755
  if (code) {
@@ -116982,6 +117827,13 @@ ${deleted}/${resolved.length} chat(s) deleted.`);
116982
117827
  await writeFile3(join31(tDir, te.filename), te.content);
116983
117828
  }
116984
117829
  }
117830
+ if (diagramEmbeds.length > 0) {
117831
+ const dDir = join31(tmpDir, "diagrams");
117832
+ await mkdir4(dDir, { recursive: true });
117833
+ for (const diagram of diagramEmbeds) {
117834
+ await writeFile3(join31(dDir, `${diagram.embedId}.html`), diagram.content);
117835
+ }
117836
+ }
116985
117837
  const zipPath = join31(outputDir, `${filenameBase}.zip`);
116986
117838
  const { execSync: execSync2 } = await import("child_process");
116987
117839
  try {
@@ -117023,6 +117875,15 @@ ${deleted}/${resolved.length} chat(s) deleted.`);
117023
117875
  written.push(`transcripts/${te.filename}`);
117024
117876
  }
117025
117877
  }
117878
+ if (diagramEmbeds.length > 0) {
117879
+ const dDir = join31(chatDir, "diagrams");
117880
+ await mkdir4(dDir, { recursive: true });
117881
+ for (const diagram of diagramEmbeds) {
117882
+ const filename = `${diagram.embedId}.html`;
117883
+ await writeFile3(join31(dDir, filename), diagram.content);
117884
+ written.push(`diagrams/${filename}`);
117885
+ }
117886
+ }
117026
117887
  const label4 = chat.title ? `"${chat.title}"` : chatHandle(chat);
117027
117888
  process.stdout.write(
117028
117889
  `\x1B[1mDownloaded chat ${label4}\x1B[0m \u2192 ${chatDir}
@@ -124360,6 +125221,7 @@ function printTeamsHelp() {
124360
125221
  openmates teams role <team-id> --user <user-id> --role admin|member|viewer [--json]
124361
125222
  openmates teams members list <team-id> [--json]
124362
125223
  openmates teams members show <team-id> <member-user-id> [--json]
125224
+ openmates teams member message <team-id> <member-hash> [text] [--json]
124363
125225
  openmates teams profile show <team-id> [--json]
124364
125226
  openmates teams profile edit <team-id> [--name <display-name>] [--avatar <value>] [--json]
124365
125227
  openmates teams remove-member <team-id> --user <user-id> [--json]