openmates 0.11.0-alpha.2 → 0.11.0-alpha.21

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,5 +1,10 @@
1
+ import {
2
+ transcribeUploadedAudio,
3
+ uploadFile
4
+ } from "./chunk-AXNRPVLE.js";
5
+
1
6
  // src/client.ts
2
- import { randomUUID } from "crypto";
7
+ import { randomUUID as randomUUID2 } from "crypto";
3
8
  import { platform as platform2, release } from "os";
4
9
  import { createInterface } from "readline/promises";
5
10
  import { stdin, stdout } from "process";
@@ -87,6 +92,16 @@ async function decryptWithAesGcmCombined(encryptedWithIvB64, rawKeyBytes) {
87
92
  return null;
88
93
  }
89
94
  }
95
+ async function deriveEmailEncryptionKeyB64(email, emailSaltB64) {
96
+ const encoder = new TextEncoder();
97
+ const emailBytes = encoder.encode(email);
98
+ const saltBytes = base64ToBytes(emailSaltB64);
99
+ const combined = new Uint8Array(emailBytes.length + saltBytes.length);
100
+ combined.set(emailBytes);
101
+ combined.set(saltBytes, emailBytes.length);
102
+ const hashBuffer = await cryptoApi.subtle.digest("SHA-256", toArrayBuffer(combined));
103
+ return bytesToBase64(new Uint8Array(hashBuffer));
104
+ }
90
105
  async function decryptBytesWithAesGcm(encryptedWithIvB64, rawKeyBytes) {
91
106
  try {
92
107
  const combined = base64ToBytes(encryptedWithIvB64);
@@ -180,12 +195,66 @@ var OpenMatesHttpClient = class {
180
195
  async post(path, body, headers = {}) {
181
196
  return this.request("POST", path, body, headers);
182
197
  }
183
- async delete(path, headers = {}) {
184
- return this.request("DELETE", path, void 0, headers);
198
+ async delete(path, body, headers = {}) {
199
+ return this.request("DELETE", path, body, headers);
185
200
  }
186
201
  async patch(path, body, headers = {}) {
187
202
  return this.request("PATCH", path, body, headers);
188
203
  }
204
+ async getBinary(path, headers = {}) {
205
+ const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
206
+ const requestHeaders = {
207
+ Accept: "application/pdf,application/octet-stream",
208
+ ...headers
209
+ };
210
+ const cookieHeader = this.formatCookieHeader();
211
+ if (cookieHeader) requestHeaders.Cookie = cookieHeader;
212
+ const response = await fetch(url, { method: "GET", headers: requestHeaders });
213
+ this.captureCookies(response);
214
+ return {
215
+ ok: response.ok,
216
+ status: response.status,
217
+ data: new Uint8Array(await response.arrayBuffer()),
218
+ headers: response.headers
219
+ };
220
+ }
221
+ async *streamSse(path, headers = {}) {
222
+ const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
223
+ const requestHeaders = {
224
+ Accept: "text/event-stream",
225
+ ...headers
226
+ };
227
+ const cookieHeader = this.formatCookieHeader();
228
+ if (cookieHeader) requestHeaders.Cookie = cookieHeader;
229
+ const response = await fetch(url, { method: "GET", headers: requestHeaders });
230
+ this.captureCookies(response);
231
+ if (!response.ok) {
232
+ throw new Error(`SSE request failed with HTTP ${response.status}`);
233
+ }
234
+ if (!response.body) {
235
+ throw new Error("SSE response body is not readable");
236
+ }
237
+ const reader = response.body.getReader();
238
+ const decoder = new TextDecoder();
239
+ let buffer = "";
240
+ try {
241
+ while (true) {
242
+ const { done, value } = await reader.read();
243
+ if (done) break;
244
+ buffer += decoder.decode(value, { stream: true });
245
+ let separatorIndex = buffer.indexOf("\n\n");
246
+ while (separatorIndex >= 0) {
247
+ const rawEvent = buffer.slice(0, separatorIndex);
248
+ buffer = buffer.slice(separatorIndex + 2);
249
+ const message = parseSseMessage(rawEvent);
250
+ if (message) yield message;
251
+ separatorIndex = buffer.indexOf("\n\n");
252
+ }
253
+ }
254
+ } finally {
255
+ reader.releaseLock();
256
+ }
257
+ }
189
258
  async request(method, path, body, headers = {}) {
190
259
  const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
191
260
  const requestHeaders = {
@@ -252,6 +321,19 @@ var OpenMatesHttpClient = class {
252
321
  return single ? [single] : [];
253
322
  }
254
323
  };
324
+ function parseSseMessage(rawEvent) {
325
+ const message = { data: "" };
326
+ for (const line of rawEvent.split("\n")) {
327
+ if (!line || line.startsWith(":")) continue;
328
+ const separator = line.indexOf(":");
329
+ const field = separator >= 0 ? line.slice(0, separator) : line;
330
+ const value = separator >= 0 ? line.slice(separator + 1).replace(/^ /, "") : "";
331
+ if (field === "id") message.id = value;
332
+ if (field === "event") message.event = value;
333
+ if (field === "data") message.data += `${message.data ? "\n" : ""}${value}`;
334
+ }
335
+ return message.data ? message : null;
336
+ }
255
337
 
256
338
  // src/storage.ts
257
339
  import {
@@ -533,6 +615,18 @@ function saveSession(session) {
533
615
  } else if (result.type === "plaintext") {
534
616
  onDisk.masterKeyExportedB64 = session.masterKeyExportedB64;
535
617
  }
618
+ if (session.emailEncryptionKeyB64) {
619
+ const emailKeyResult = storeMasterKey(
620
+ session.emailEncryptionKeyB64,
621
+ `${session.hashedEmail}:email`
622
+ );
623
+ onDisk.emailEncryptionKeyStorage = emailKeyResult.type;
624
+ if (emailKeyResult.type === "encrypted") {
625
+ onDisk.emailEncryptionKeyEncrypted = emailKeyResult.encryptedData;
626
+ } else if (emailKeyResult.type === "plaintext") {
627
+ onDisk.emailEncryptionKeyB64 = session.emailEncryptionKeyB64;
628
+ }
629
+ }
536
630
  writeJsonFile(filePath, onDisk);
537
631
  if (result.type !== "plaintext") {
538
632
  process.stderr.write("Decrypting data...\n");
@@ -543,17 +637,19 @@ function loadSession() {
543
637
  const onDisk = readJsonFile(filePath);
544
638
  if (!onDisk) return null;
545
639
  let masterKey = null;
640
+ let emailEncryptionKey = null;
546
641
  if (!onDisk.masterKeyStorage) {
547
642
  masterKey = onDisk.masterKeyExportedB64 ?? null;
548
643
  if (masterKey) {
549
- const session = buildSession(onDisk, masterKey);
644
+ emailEncryptionKey = getEmailEncryptionKeyFromDisk(onDisk);
645
+ const session = buildSession(onDisk, masterKey, emailEncryptionKey);
550
646
  try {
551
647
  saveSession(session);
552
648
  process.stderr.write("Decrypting data...\n");
553
649
  } catch {
554
650
  }
555
651
  }
556
- return masterKey ? buildSession(onDisk, masterKey) : null;
652
+ return masterKey ? buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk)) : null;
557
653
  }
558
654
  switch (onDisk.masterKeyStorage) {
559
655
  case "keychain":
@@ -577,7 +673,7 @@ function loadSession() {
577
673
  );
578
674
  return null;
579
675
  }
580
- return buildSession(onDisk, masterKey);
676
+ return buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk));
581
677
  }
582
678
  function clearSession() {
583
679
  const filePath = join(ensureStateDir(), "session.json");
@@ -585,17 +681,36 @@ function clearSession() {
585
681
  if (onDisk?.masterKeyStorage) {
586
682
  deleteMasterKey(onDisk.masterKeyStorage, onDisk.hashedEmail);
587
683
  }
684
+ if (onDisk?.emailEncryptionKeyStorage) {
685
+ deleteMasterKey(onDisk.emailEncryptionKeyStorage, `${onDisk.hashedEmail}:email`);
686
+ }
588
687
  if (existsSync2(filePath)) {
589
688
  rmSync(filePath);
590
689
  }
591
690
  }
592
- function buildSession(onDisk, masterKey) {
691
+ function getEmailEncryptionKeyFromDisk(onDisk) {
692
+ if (!onDisk.emailEncryptionKeyStorage) return onDisk.emailEncryptionKeyB64 ?? null;
693
+ switch (onDisk.emailEncryptionKeyStorage) {
694
+ case "keychain":
695
+ return retrieveMasterKey("keychain", `${onDisk.hashedEmail}:email`);
696
+ case "encrypted":
697
+ return retrieveMasterKey(
698
+ "encrypted",
699
+ `${onDisk.hashedEmail}:email`,
700
+ onDisk.emailEncryptionKeyEncrypted
701
+ );
702
+ case "plaintext":
703
+ return onDisk.emailEncryptionKeyB64 ?? null;
704
+ }
705
+ }
706
+ function buildSession(onDisk, masterKey, emailEncryptionKey) {
593
707
  return {
594
708
  apiUrl: onDisk.apiUrl,
595
709
  sessionId: onDisk.sessionId,
596
710
  wsToken: onDisk.wsToken,
597
711
  cookies: onDisk.cookies,
598
712
  masterKeyExportedB64: masterKey,
713
+ emailEncryptionKeyB64: emailEncryptionKey,
599
714
  hashedEmail: onDisk.hashedEmail,
600
715
  userEmailSalt: onDisk.userEmailSalt,
601
716
  createdAt: onDisk.createdAt,
@@ -603,18 +718,6 @@ function buildSession(onDisk, masterKey) {
603
718
  autoLogoutMinutes: onDisk.autoLogoutMinutes
604
719
  };
605
720
  }
606
- function loadIncognitoHistory() {
607
- const filePath = join(ensureStateDir(), "incognito.json");
608
- return readJsonFile(filePath) ?? [];
609
- }
610
- function saveIncognitoHistory(items) {
611
- const filePath = join(ensureStateDir(), "incognito.json");
612
- writeJsonFile(filePath, items);
613
- }
614
- function clearIncognitoHistory() {
615
- const filePath = join(ensureStateDir(), "incognito.json");
616
- writeJsonFile(filePath, []);
617
- }
618
721
  var SYNC_CACHE_FILE = "sync_cache.json";
619
722
  function saveSyncCache(cache) {
620
723
  const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
@@ -624,6 +727,12 @@ function loadSyncCache() {
624
727
  const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
625
728
  return readJsonFile(filePath);
626
729
  }
730
+ function clearSyncCache() {
731
+ const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
732
+ if (existsSync2(filePath)) {
733
+ rmSync(filePath);
734
+ }
735
+ }
627
736
  function isSyncCacheFresh(maxAgeMs = 3e5) {
628
737
  const cache = loadSyncCache();
629
738
  if (!cache) return false;
@@ -631,7 +740,20 @@ function isSyncCacheFresh(maxAgeMs = 3e5) {
631
740
  }
632
741
 
633
742
  // src/ws.ts
634
- import WebSocket from "ws";
743
+ import { createRequire } from "module";
744
+ var require2 = createRequire(import.meta.url);
745
+ var WebSocket = require2("ws");
746
+ var SUB_CHAT_EVENT_TYPES = /* @__PURE__ */ new Set([
747
+ "spawn_sub_chats",
748
+ "sub_chat_progress",
749
+ "sub_chat_confirmation_required",
750
+ "sub_chat_confirmation_resolved",
751
+ "awaiting_sub_chats_completion",
752
+ "sub_chat_completed",
753
+ "awaiting_user_input"
754
+ ]);
755
+ var SUB_CHAT_PARENT_STATUS_MESSAGE = "I've started the sub-chats and will continue once they finish.";
756
+ var SUB_CHAT_COMPLETION_TIMEOUT_MS = 10 * 6e4;
635
757
  var OpenMatesWsClient = class {
636
758
  socket;
637
759
  constructor(options) {
@@ -691,6 +813,14 @@ var OpenMatesWsClient = class {
691
813
  send(type, payload) {
692
814
  this.socket.send(JSON.stringify({ type, payload }));
693
815
  }
816
+ sendAsync(type, payload) {
817
+ return new Promise((resolve4, reject) => {
818
+ this.socket.send(JSON.stringify({ type, payload }), (error) => {
819
+ if (error) reject(error);
820
+ else resolve4();
821
+ });
822
+ });
823
+ }
694
824
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
695
825
  return new Promise((resolve4, reject) => {
696
826
  const onMessage = (rawData) => {
@@ -793,35 +923,119 @@ var OpenMatesWsClient = class {
793
923
  collectAiResponse(userMessageId, chatId, options) {
794
924
  const timeoutMs = options?.timeoutMs ?? 9e4;
795
925
  const onStream = options?.onStream;
926
+ const asyncEmbedWaitMs = options?.asyncEmbedWaitMs ?? 12e4;
796
927
  return new Promise((resolve4, reject) => {
797
928
  let latestContent = "";
929
+ let messageId = null;
930
+ let taskId = null;
798
931
  let category = null;
799
932
  let modelName = null;
800
933
  let followUpSuggestions = [];
934
+ const subChatEvents = [];
935
+ const pendingSubChatHandlers = /* @__PURE__ */ new Set();
936
+ const embeds = /* @__PURE__ */ new Map();
937
+ const processingEmbedIds = /* @__PURE__ */ new Set();
801
938
  let aiResponseDone = false;
939
+ let postProcessingDone = false;
802
940
  const POST_PROCESSING_WINDOW_MS = 12e3;
803
941
  let postProcessingTimer = null;
804
- const timeout = setTimeout(() => {
942
+ let asyncEmbedTimer = null;
943
+ const startTimeout = (ms) => setTimeout(() => {
805
944
  cleanup();
806
945
  reject(new Error("Timed out waiting for AI response"));
807
- }, timeoutMs);
946
+ }, ms);
947
+ let timeout = startTimeout(timeoutMs);
948
+ let awaitingSubChatsCompletion = false;
949
+ const resetTimeout = (ms) => {
950
+ clearTimeout(timeout);
951
+ timeout = startTimeout(ms);
952
+ };
808
953
  const capture = (p) => {
954
+ if (typeof p.message_id === "string" && p.message_id)
955
+ messageId = p.message_id;
956
+ if (typeof p.task_id === "string" && p.task_id) taskId = p.task_id;
809
957
  if (typeof p.category === "string" && p.category) category = p.category;
810
958
  if (typeof p.model_name === "string" && p.model_name)
811
959
  modelName = p.model_name;
812
960
  };
961
+ const extractMessageContent = (message) => {
962
+ if (typeof message.content === "string") return message.content;
963
+ const content = message.content;
964
+ if (!content || typeof content !== "object") return "";
965
+ try {
966
+ const root = content;
967
+ const text = root.content?.[0]?.content?.[0]?.text;
968
+ return typeof text === "string" ? text : "";
969
+ } catch {
970
+ return "";
971
+ }
972
+ };
973
+ const finishPostProcessingWait = () => {
974
+ postProcessingDone = true;
975
+ maybeResolve();
976
+ };
977
+ const maybeResolve = () => {
978
+ if (!aiResponseDone || !postProcessingDone) return;
979
+ if (pendingSubChatHandlers.size > 0) return;
980
+ if (processingEmbedIds.size > 0 && !asyncEmbedTimer) {
981
+ asyncEmbedTimer = setTimeout(() => {
982
+ cleanup();
983
+ resolve4({
984
+ messageId,
985
+ taskId,
986
+ content: latestContent,
987
+ category,
988
+ modelName,
989
+ followUpSuggestions,
990
+ embeds: [...embeds.values()],
991
+ subChatEvents
992
+ });
993
+ }, asyncEmbedWaitMs);
994
+ return;
995
+ }
996
+ if (processingEmbedIds.size > 0) return;
997
+ cleanup();
998
+ resolve4({
999
+ messageId,
1000
+ taskId,
1001
+ content: latestContent,
1002
+ category,
1003
+ modelName,
1004
+ followUpSuggestions,
1005
+ embeds: [...embeds.values()],
1006
+ subChatEvents
1007
+ });
1008
+ };
1009
+ const handleSubChatEvent = (type, p) => {
1010
+ const eventChatId = typeof p.chat_id === "string" ? p.chat_id : typeof p.parent_id === "string" ? p.parent_id : null;
1011
+ if (eventChatId && eventChatId !== chatId) return;
1012
+ const event = { type, payload: p };
1013
+ subChatEvents.push(event);
1014
+ if (type === "awaiting_sub_chats_completion") {
1015
+ awaitingSubChatsCompletion = true;
1016
+ resetTimeout(SUB_CHAT_COMPLETION_TIMEOUT_MS);
1017
+ }
1018
+ const handler = options?.onSubChatEvent;
1019
+ if (!handler) return;
1020
+ const pending = Promise.resolve(handler(event));
1021
+ pendingSubChatHandlers.add(pending);
1022
+ pending.catch((error) => {
1023
+ cleanup();
1024
+ reject(error instanceof Error ? error : new Error(String(error)));
1025
+ }).finally(() => {
1026
+ pendingSubChatHandlers.delete(pending);
1027
+ maybeResolve();
1028
+ });
1029
+ };
813
1030
  const scheduleResolve = (content) => {
1031
+ if (awaitingSubChatsCompletion && content.trim() === SUB_CHAT_PARENT_STATUS_MESSAGE) {
1032
+ latestContent = "";
1033
+ return;
1034
+ }
814
1035
  aiResponseDone = true;
815
1036
  latestContent = content;
816
- postProcessingTimer = setTimeout(() => {
817
- cleanup();
818
- resolve4({
819
- content: latestContent,
820
- category,
821
- modelName,
822
- followUpSuggestions
823
- });
824
- }, POST_PROCESSING_WINDOW_MS);
1037
+ clearTimeout(timeout);
1038
+ postProcessingTimer = setTimeout(finishPostProcessingWait, POST_PROCESSING_WINDOW_MS);
825
1039
  };
826
1040
  const onMessage = (rawData) => {
827
1041
  try {
@@ -837,9 +1051,31 @@ var OpenMatesWsClient = class {
837
1051
  );
838
1052
  return;
839
1053
  }
1054
+ if (SUB_CHAT_EVENT_TYPES.has(type)) {
1055
+ handleSubChatEvent(type, p);
1056
+ return;
1057
+ }
1058
+ if (type === "send_embed_data") {
1059
+ const embedPayload = p.payload && typeof p.payload === "object" ? p.payload : p;
1060
+ if (typeof embedPayload.chat_id === "string" && embedPayload.chat_id !== chatId) {
1061
+ return;
1062
+ }
1063
+ const embedId = embedPayload.embed_id;
1064
+ if (typeof embedId === "string" && embedId) {
1065
+ const status = typeof embedPayload.status === "string" ? embedPayload.status : "finished";
1066
+ embeds.set(embedId, embedPayload);
1067
+ if (status === "processing") {
1068
+ processingEmbedIds.add(embedId);
1069
+ } else {
1070
+ processingEmbedIds.delete(embedId);
1071
+ }
1072
+ maybeResolve();
1073
+ }
1074
+ return;
1075
+ }
840
1076
  if (type === "ai_message_update") {
841
1077
  const msgId = p.user_message_id ?? p.userMessageId;
842
- if (msgId !== userMessageId) return;
1078
+ if (msgId !== userMessageId && p.chat_id !== chatId) return;
843
1079
  capture(p);
844
1080
  if (typeof p.full_content_so_far === "string") {
845
1081
  latestContent = p.full_content_so_far;
@@ -864,7 +1100,7 @@ var OpenMatesWsClient = class {
864
1100
  }
865
1101
  if (type === "ai_background_response_completed") {
866
1102
  const msgId = p.user_message_id ?? p.userMessageId;
867
- if (msgId && msgId !== userMessageId) return;
1103
+ if (msgId && msgId !== userMessageId && p.chat_id !== chatId) return;
868
1104
  if (!msgId && p.chat_id !== chatId) return;
869
1105
  capture(p);
870
1106
  const content = typeof p.full_content === "string" ? p.full_content : latestContent;
@@ -872,6 +1108,25 @@ var OpenMatesWsClient = class {
872
1108
  scheduleResolve(content);
873
1109
  return;
874
1110
  }
1111
+ if (type === "chat_message_added") {
1112
+ if (p.chat_id !== chatId) return;
1113
+ const rawMessage = p.message;
1114
+ if (!rawMessage || typeof rawMessage !== "object") return;
1115
+ const message = rawMessage;
1116
+ if (message.role !== "assistant") return;
1117
+ const content = extractMessageContent(message);
1118
+ if (!content) return;
1119
+ capture(message);
1120
+ if (typeof message.category === "string" && message.category) {
1121
+ category = message.category;
1122
+ }
1123
+ if (typeof message.model_name === "string" && message.model_name) {
1124
+ modelName = message.model_name;
1125
+ }
1126
+ onStream?.({ kind: "done", content, category, modelName });
1127
+ scheduleResolve(content);
1128
+ return;
1129
+ }
875
1130
  if (type === "ai_typing_started") {
876
1131
  capture(p);
877
1132
  onStream?.({
@@ -895,13 +1150,7 @@ var OpenMatesWsClient = class {
895
1150
  clearTimeout(postProcessingTimer);
896
1151
  postProcessingTimer = null;
897
1152
  }
898
- cleanup();
899
- resolve4({
900
- content: latestContent,
901
- category,
902
- modelName,
903
- followUpSuggestions
904
- });
1153
+ finishPostProcessingWait();
905
1154
  }
906
1155
  return;
907
1156
  }
@@ -916,10 +1165,14 @@ var OpenMatesWsClient = class {
916
1165
  if (aiResponseDone) {
917
1166
  cleanup();
918
1167
  resolve4({
1168
+ messageId,
1169
+ taskId,
919
1170
  content: latestContent,
920
1171
  category,
921
1172
  modelName,
922
- followUpSuggestions
1173
+ followUpSuggestions,
1174
+ embeds: [...embeds.values()],
1175
+ subChatEvents
923
1176
  });
924
1177
  return;
925
1178
  }
@@ -932,6 +1185,10 @@ var OpenMatesWsClient = class {
932
1185
  clearTimeout(postProcessingTimer);
933
1186
  postProcessingTimer = null;
934
1187
  }
1188
+ if (asyncEmbedTimer) {
1189
+ clearTimeout(asyncEmbedTimer);
1190
+ asyncEmbedTimer = null;
1191
+ }
935
1192
  this.socket.off("message", onMessage);
936
1193
  this.socket.off("error", onError);
937
1194
  this.socket.off("close", onClose);
@@ -955,6 +1212,7 @@ var CHAT_MODELS = [
955
1212
  { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
956
1213
  { id: "gpt-5.4", name: "GPT-5.4" },
957
1214
  { id: "gpt-oss-120b", name: "GPT-OSS-120b" },
1215
+ { id: "gpt-oss-20b", name: "GPT-OSS-20b" },
958
1216
  { id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
959
1217
  { id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro" },
960
1218
  { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
@@ -1242,9 +1500,139 @@ function escapeRegExp(s) {
1242
1500
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1243
1501
  }
1244
1502
 
1503
+ // src/embedCreator.ts
1504
+ import { randomUUID, createHash as createHash3, randomBytes, webcrypto as webcrypto3 } from "crypto";
1505
+ import { encode as toonEncode } from "@toon-format/toon";
1506
+ var cryptoApi3 = webcrypto3;
1507
+ var AES_GCM_IV_LENGTH3 = 12;
1508
+ function bytesToBase642(input) {
1509
+ return Buffer.from(input).toString("base64");
1510
+ }
1511
+ function toArrayBuffer2(input) {
1512
+ return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
1513
+ }
1514
+ async function encryptAesGcm(plaintext, rawKeyBytes) {
1515
+ const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
1516
+ const key = await cryptoApi3.subtle.importKey(
1517
+ "raw",
1518
+ toArrayBuffer2(rawKeyBytes),
1519
+ { name: "AES-GCM" },
1520
+ false,
1521
+ ["encrypt"]
1522
+ );
1523
+ const encrypted = await cryptoApi3.subtle.encrypt(
1524
+ { name: "AES-GCM", iv: toArrayBuffer2(iv) },
1525
+ key,
1526
+ new TextEncoder().encode(plaintext)
1527
+ );
1528
+ const cipherBytes = new Uint8Array(encrypted);
1529
+ const combined = new Uint8Array(iv.length + cipherBytes.length);
1530
+ combined.set(iv);
1531
+ combined.set(cipherBytes, iv.length);
1532
+ return bytesToBase642(combined);
1533
+ }
1534
+ async function wrapKey(embedKey, wrappingKey) {
1535
+ const cryptoKey = await cryptoApi3.subtle.importKey(
1536
+ "raw",
1537
+ toArrayBuffer2(wrappingKey),
1538
+ { name: "AES-GCM" },
1539
+ false,
1540
+ ["encrypt"]
1541
+ );
1542
+ const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
1543
+ const encrypted = await cryptoApi3.subtle.encrypt(
1544
+ { name: "AES-GCM", iv: toArrayBuffer2(iv) },
1545
+ cryptoKey,
1546
+ toArrayBuffer2(embedKey)
1547
+ );
1548
+ const cipherBytes = new Uint8Array(encrypted);
1549
+ const combined = new Uint8Array(iv.length + cipherBytes.length);
1550
+ combined.set(iv);
1551
+ combined.set(cipherBytes, iv.length);
1552
+ return bytesToBase642(combined);
1553
+ }
1554
+ function generateEmbedKey() {
1555
+ return new Uint8Array(randomBytes(32));
1556
+ }
1557
+ function computeSHA256(content) {
1558
+ return createHash3("sha256").update(content).digest("hex");
1559
+ }
1560
+ function toonEncodeContent(data) {
1561
+ try {
1562
+ return toonEncode(data);
1563
+ } catch {
1564
+ return JSON.stringify(data);
1565
+ }
1566
+ }
1567
+ function generateEmbedId() {
1568
+ return randomUUID();
1569
+ }
1570
+ function createEmbedReferenceBlock(type, embedId, url) {
1571
+ const data = { type, embed_id: embedId };
1572
+ if (url) data.url = url;
1573
+ const ref = JSON.stringify(data, null, 2);
1574
+ return "```json\n" + ref + "\n```";
1575
+ }
1576
+ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId) {
1577
+ try {
1578
+ const embedKey = generateEmbedKey();
1579
+ const encryptedContent = await encryptAesGcm(embed.content, embedKey);
1580
+ const encryptedType = await encryptAesGcm(embed.type, embedKey);
1581
+ const encryptedTextPreview = await encryptAesGcm(embed.textPreview, embedKey);
1582
+ const hashedEmbedId = computeSHA256(embed.embedId);
1583
+ const hashedChatId = computeSHA256(chatId);
1584
+ const hashedMessageId = computeSHA256(messageId);
1585
+ const hashedUserId = computeSHA256(userId);
1586
+ const wrappedWithMaster = await wrapKey(embedKey, masterKey);
1587
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1588
+ const embedKeys = [
1589
+ {
1590
+ hashed_embed_id: hashedEmbedId,
1591
+ key_type: "master",
1592
+ hashed_chat_id: null,
1593
+ encrypted_embed_key: wrappedWithMaster,
1594
+ hashed_user_id: hashedUserId,
1595
+ created_at: nowSeconds
1596
+ }
1597
+ ];
1598
+ if (chatKey) {
1599
+ const wrappedWithChat = await wrapKey(embedKey, chatKey);
1600
+ embedKeys.push({
1601
+ hashed_embed_id: hashedEmbedId,
1602
+ key_type: "chat",
1603
+ hashed_chat_id: hashedChatId,
1604
+ encrypted_embed_key: wrappedWithChat,
1605
+ hashed_user_id: hashedUserId,
1606
+ created_at: nowSeconds
1607
+ });
1608
+ }
1609
+ return {
1610
+ embed_id: embed.embedId,
1611
+ encrypted_type: encryptedType,
1612
+ encrypted_content: encryptedContent,
1613
+ encrypted_text_preview: encryptedTextPreview,
1614
+ status: embed.status,
1615
+ hashed_chat_id: hashedChatId,
1616
+ hashed_message_id: hashedMessageId,
1617
+ hashed_user_id: hashedUserId,
1618
+ file_path: embed.filePath,
1619
+ content_hash: embed.contentHash,
1620
+ text_length_chars: embed.textLengthChars,
1621
+ created_at: nowSeconds,
1622
+ updated_at: nowSeconds,
1623
+ embed_keys: embedKeys
1624
+ };
1625
+ } catch (error) {
1626
+ const msg = error instanceof Error ? error.message : String(error);
1627
+ process.stderr.write(`\x1B[31mError:\x1B[0m Failed to encrypt embed: ${msg}
1628
+ `);
1629
+ return null;
1630
+ }
1631
+ }
1632
+
1245
1633
  // src/shareEncryption.ts
1246
- import { webcrypto as webcrypto3 } from "crypto";
1247
- var crypto = webcrypto3;
1634
+ import { webcrypto as webcrypto4 } from "crypto";
1635
+ var crypto = webcrypto4;
1248
1636
  function base64UrlEncode(data) {
1249
1637
  return Buffer.from(data).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1250
1638
  }
@@ -1356,6 +1744,48 @@ function buildEmbedShareUrl(origin, embedId, blob) {
1356
1744
  }
1357
1745
 
1358
1746
  // src/client.ts
1747
+ function normalizeUnixSeconds(value, fallback) {
1748
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1749
+ return fallback;
1750
+ }
1751
+ return value > 1e10 ? Math.floor(value / 1e3) : Math.floor(value);
1752
+ }
1753
+ function buildSubChatConfirmationPayload(params) {
1754
+ return {
1755
+ chat_id: params.chatId,
1756
+ task_id: params.taskId,
1757
+ action: params.approved ? "approve" : "cancel",
1758
+ approve_count: params.approved ? params.approveCount ?? null : null
1759
+ };
1760
+ }
1761
+ async function buildSubChatEncryptedMetadataPayloads(params) {
1762
+ const createdAt = params.createdAt ?? Math.floor(Date.now() / 1e3);
1763
+ const payloads = [];
1764
+ for (const subChat of params.subChats) {
1765
+ const chatId = subChat.id || subChat.chat_id;
1766
+ const messageId = subChat.user_message_id || subChat.message_id;
1767
+ const prompt = subChat.prompt || "";
1768
+ if (!chatId || !messageId) continue;
1769
+ const title = prompt.substring(0, 30);
1770
+ payloads.push({
1771
+ chat_id: chatId,
1772
+ parent_id: params.parentChatId,
1773
+ is_sub_chat: true,
1774
+ message_id: messageId,
1775
+ encrypted_content: await encryptWithAesGcmCombined(prompt, params.parentChatKey),
1776
+ encrypted_sender_name: await encryptWithAesGcmCombined("User", params.parentChatKey),
1777
+ encrypted_title: await encryptWithAesGcmCombined(title, params.parentChatKey),
1778
+ created_at: createdAt,
1779
+ encrypted_chat_key: params.encryptedParentChatKey,
1780
+ versions: {
1781
+ messages_v: 1,
1782
+ title_v: 0,
1783
+ last_edited_overall_timestamp: createdAt
1784
+ }
1785
+ });
1786
+ }
1787
+ return payloads;
1788
+ }
1359
1789
  var MEMORY_TYPE_REGISTRY = {
1360
1790
  "ai/communication_style": {
1361
1791
  appId: "ai",
@@ -1717,15 +2147,19 @@ var BLOCKED_SETTINGS_MUTATE_PATHS = /* @__PURE__ */ new Set([
1717
2147
  "/v1/auth/setup_password",
1718
2148
  "/v1/auth/2fa/setup/initiate",
1719
2149
  "/v1/auth/2fa/setup/provider",
1720
- "/v1/auth/2fa/setup/verify-signup"
2150
+ "/v1/auth/2fa/setup/verify-signup",
2151
+ "/v1/settings/delete-account",
2152
+ "/v1/settings/request-action-verification",
2153
+ "/v1/settings/verify-action-code",
2154
+ "/v1/settings/user/disable-2fa"
1721
2155
  ]);
1722
2156
  var OpenMatesClient = class _OpenMatesClient {
1723
2157
  apiUrl;
1724
2158
  session;
1725
2159
  http;
1726
2160
  constructor(options = {}) {
1727
- this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
1728
2161
  const diskSession = options.session ?? this.getValidSessionFromDisk();
2162
+ this.apiUrl = (options.apiUrl ?? process.env.OPENMATES_API_URL ?? diskSession?.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
1729
2163
  this.session = diskSession;
1730
2164
  this.http = new OpenMatesHttpClient({
1731
2165
  apiUrl: this.apiUrl,
@@ -1785,7 +2219,7 @@ var OpenMatesClient = class _OpenMatesClient {
1785
2219
  pin: pin.trim().toUpperCase(),
1786
2220
  token
1787
2221
  });
1788
- const sessionId = randomUUID();
2222
+ const sessionId = randomUUID2();
1789
2223
  const login = await this.http.post(
1790
2224
  "/v1/auth/login",
1791
2225
  {
@@ -1814,6 +2248,7 @@ var OpenMatesClient = class _OpenMatesClient {
1814
2248
  authorizerDeviceName: complete.data.authorizer_device_name ?? null,
1815
2249
  autoLogoutMinutes: complete.data.auto_logout_minutes ?? null
1816
2250
  };
2251
+ await this.hydrateEmailEncryptionKey(session);
1817
2252
  saveSession(session);
1818
2253
  }
1819
2254
  async whoAmI() {
@@ -1826,6 +2261,11 @@ var OpenMatesClient = class _OpenMatesClient {
1826
2261
  if (!response.ok || !response.data.success) {
1827
2262
  throw new Error("Session is invalid. Please run `openmates login`.");
1828
2263
  }
2264
+ if (response.data.ws_token) {
2265
+ session.wsToken = response.data.ws_token;
2266
+ }
2267
+ session.cookies = this.http.getCookieMap();
2268
+ saveSession(session);
1829
2269
  return response.data.user ?? {};
1830
2270
  }
1831
2271
  async logout() {
@@ -1833,7 +2273,6 @@ var OpenMatesClient = class _OpenMatesClient {
1833
2273
  await this.http.post("/v1/auth/logout", {}, this.getCliRequestHeaders()).catch(() => void 0);
1834
2274
  }
1835
2275
  clearSession();
1836
- clearIncognitoHistory();
1837
2276
  }
1838
2277
  // -------------------------------------------------------------------------
1839
2278
  // Chats
@@ -2280,7 +2719,7 @@ var OpenMatesClient = class _OpenMatesClient {
2280
2719
  async sendMessage(params) {
2281
2720
  let chatId;
2282
2721
  if (!params.chatId) {
2283
- chatId = randomUUID();
2722
+ chatId = randomUUID2();
2284
2723
  } else if (params.chatId.length < 36) {
2285
2724
  const resolved = await this.resolveFullChatId(params.chatId);
2286
2725
  if (!resolved) {
@@ -2293,7 +2732,7 @@ var OpenMatesClient = class _OpenMatesClient {
2293
2732
  chatId = params.chatId;
2294
2733
  }
2295
2734
  const { ws, session } = await this.openWsClient();
2296
- const messageId = randomUUID();
2735
+ const messageId = randomUUID2();
2297
2736
  const createdAt = Math.floor(Date.now() / 1e3);
2298
2737
  const isNewChat = !params.chatId;
2299
2738
  ws.send("set_active_chat", { chat_id: chatId });
@@ -2313,6 +2752,7 @@ var OpenMatesClient = class _OpenMatesClient {
2313
2752
  };
2314
2753
  let chatKeyBytes = null;
2315
2754
  let encryptedChatKey = null;
2755
+ let baselineMessagesV = 0;
2316
2756
  if (!params.incognito) {
2317
2757
  const masterKey = this.getMasterKeyBytes();
2318
2758
  if (isNewChat) {
@@ -2332,6 +2772,7 @@ var OpenMatesClient = class _OpenMatesClient {
2332
2772
  (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
2333
2773
  );
2334
2774
  if (chat) {
2775
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
2335
2776
  const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
2336
2777
  if (encKey) {
2337
2778
  chatKeyBytes = await decryptBytesWithAesGcm(encKey, masterKey);
@@ -2340,8 +2781,32 @@ var OpenMatesClient = class _OpenMatesClient {
2340
2781
  }
2341
2782
  }
2342
2783
  }
2343
- if (params.encryptedEmbeds && params.encryptedEmbeds.length > 0) {
2344
- messagePayload.encrypted_embeds = params.encryptedEmbeds;
2784
+ if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
2785
+ messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
2786
+ embed_id: embed.embedId,
2787
+ type: embed.type,
2788
+ content: embed.content,
2789
+ status: embed.status,
2790
+ text_preview: embed.textPreview
2791
+ }));
2792
+ }
2793
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
2794
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
2795
+ const masterKey = this.getMasterKeyBytes();
2796
+ for (const embed of params.preparedEmbeds) {
2797
+ const encrypted = await encryptEmbed(
2798
+ embed,
2799
+ masterKey,
2800
+ chatKeyBytes,
2801
+ chatId,
2802
+ messageId,
2803
+ session.hashedEmail
2804
+ );
2805
+ if (encrypted) encryptedEmbeds.push(encrypted);
2806
+ }
2807
+ }
2808
+ if (encryptedEmbeds.length > 0) {
2809
+ messagePayload.encrypted_embeds = encryptedEmbeds;
2345
2810
  }
2346
2811
  ws.send("chat_message_added", messagePayload);
2347
2812
  if (!params.incognito && chatKeyBytes) {
@@ -2369,38 +2834,139 @@ var OpenMatesClient = class _OpenMatesClient {
2369
2834
  ws.send("encrypted_chat_metadata", metadataPayload);
2370
2835
  }
2371
2836
  let assistant = "";
2837
+ let assistantMessageId = null;
2372
2838
  let category = null;
2373
2839
  let modelName = null;
2374
2840
  let followUpSuggestions = [];
2375
- const streamOpts = { onStream: params.onStream };
2376
- if (params.incognito) {
2377
- const history = loadIncognitoHistory();
2378
- history.push({
2379
- role: "user",
2380
- content: params.message,
2381
- createdAt: Date.now()
2382
- });
2383
- try {
2384
- const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2385
- assistant = resp.content;
2386
- category = resp.category;
2387
- modelName = resp.modelName;
2388
- } finally {
2389
- ws.close();
2841
+ let subChatEvents = [];
2842
+ const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2843
+ const isApprovalWithinServerLimits = (request) => {
2844
+ const count = request.subChats.length;
2845
+ if (count === 0) return false;
2846
+ if (request.remainingSubChats !== null && count > request.remainingSubChats) {
2847
+ return false;
2848
+ }
2849
+ if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
2850
+ return false;
2851
+ }
2852
+ return true;
2853
+ };
2854
+ const isAutoApprovalWithinServerLimits = (request) => {
2855
+ if (!isApprovalWithinServerLimits(request)) return false;
2856
+ return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
2857
+ };
2858
+ const handleSubChatEvent = async (event) => {
2859
+ params.onSubChatEvent?.(event);
2860
+ if (event.type === "spawn_sub_chats") {
2861
+ if (params.incognito || !chatKeyBytes) return;
2862
+ const rawSubChats = event.payload.sub_chats;
2863
+ if (!Array.isArray(rawSubChats)) return;
2864
+ const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
2865
+ parentChatId: chatId,
2866
+ parentChatKey: chatKeyBytes,
2867
+ encryptedParentChatKey: encryptedChatKey,
2868
+ subChats: rawSubChats
2869
+ });
2870
+ for (const metadataPayload of metadataPayloads) {
2871
+ await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
2872
+ }
2873
+ return;
2874
+ }
2875
+ if (event.type !== "sub_chat_confirmation_required") return;
2876
+ const payload = event.payload;
2877
+ const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
2878
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
2879
+ const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
2880
+ if (!taskId) return;
2881
+ const request = {
2882
+ chatId: confirmationChatId,
2883
+ taskId,
2884
+ subChats,
2885
+ maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
2886
+ maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
2887
+ existingSubChats: numberOrNull(payload.existing_sub_chats),
2888
+ remainingSubChats: numberOrNull(payload.remaining_sub_chats)
2889
+ };
2890
+ const withinLimits = isApprovalWithinServerLimits(request);
2891
+ const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
2892
+ await ws.sendAsync(
2893
+ "sub_chat_confirmation",
2894
+ buildSubChatConfirmationPayload({
2895
+ chatId: request.chatId,
2896
+ taskId: request.taskId,
2897
+ approved,
2898
+ approveCount: approved ? request.subChats.length : void 0
2899
+ })
2900
+ );
2901
+ };
2902
+ const streamOpts = {
2903
+ onStream: params.onStream,
2904
+ onSubChatEvent: handleSubChatEvent
2905
+ };
2906
+ if (params.incognito) {
2907
+ try {
2908
+ const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2909
+ assistantMessageId = resp.messageId;
2910
+ assistant = resp.content;
2911
+ category = resp.category;
2912
+ modelName = resp.modelName;
2913
+ subChatEvents = resp.subChatEvents;
2914
+ } finally {
2915
+ ws.close();
2390
2916
  }
2391
- history.push({
2392
- role: "assistant",
2393
- content: assistant,
2394
- createdAt: Date.now()
2395
- });
2396
- saveIncognitoHistory(history);
2397
2917
  } else {
2398
2918
  try {
2399
2919
  const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2920
+ assistantMessageId = resp.messageId;
2400
2921
  assistant = resp.content;
2401
2922
  category = resp.category;
2402
2923
  modelName = resp.modelName;
2403
2924
  followUpSuggestions = resp.followUpSuggestions;
2925
+ subChatEvents = resp.subChatEvents;
2926
+ if (chatKeyBytes && assistant) {
2927
+ const completedAt = Math.floor(Date.now() / 1e3);
2928
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
2929
+ assistant,
2930
+ chatKeyBytes
2931
+ );
2932
+ const encryptedCategory = category ? await encryptWithAesGcmCombined(category, chatKeyBytes) : void 0;
2933
+ const encryptedModelName = modelName ? await encryptWithAesGcmCombined(modelName, chatKeyBytes) : void 0;
2934
+ const persistedAssistantMessageId = assistantMessageId ?? randomUUID2();
2935
+ ws.send("ai_response_completed", {
2936
+ chat_id: chatId,
2937
+ message: {
2938
+ message_id: persistedAssistantMessageId,
2939
+ chat_id: chatId,
2940
+ role: "assistant",
2941
+ created_at: completedAt,
2942
+ status: "synced",
2943
+ user_message_id: messageId,
2944
+ encrypted_content: encryptedAssistantContent,
2945
+ encrypted_category: encryptedCategory,
2946
+ encrypted_model_name: encryptedModelName
2947
+ },
2948
+ versions: {
2949
+ messages_v: baselineMessagesV + 2,
2950
+ last_edited_overall_timestamp: completedAt
2951
+ }
2952
+ });
2953
+ await ws.waitForMessage(
2954
+ "ai_response_storage_confirmed",
2955
+ (payload) => {
2956
+ const p = payload;
2957
+ return p.message_id === persistedAssistantMessageId;
2958
+ },
2959
+ 2e4
2960
+ );
2961
+ await this.persistStreamedEmbeds({
2962
+ ws,
2963
+ embeds: resp.embeds,
2964
+ chatId,
2965
+ chatKeyBytes,
2966
+ fallbackMessageId: persistedAssistantMessageId
2967
+ });
2968
+ clearSyncCache();
2969
+ }
2404
2970
  } finally {
2405
2971
  ws.close();
2406
2972
  }
@@ -2412,14 +2978,113 @@ var OpenMatesClient = class _OpenMatesClient {
2412
2978
  category,
2413
2979
  modelName,
2414
2980
  mateName,
2415
- followUpSuggestions
2981
+ followUpSuggestions,
2982
+ subChatEvents
2416
2983
  };
2417
2984
  }
2418
- getIncognitoHistory() {
2419
- return loadIncognitoHistory();
2420
- }
2421
- clearIncognitoHistory() {
2422
- clearIncognitoHistory();
2985
+ async persistStreamedEmbeds(params) {
2986
+ const finalized = new Map(
2987
+ params.embeds.filter((embed) => {
2988
+ const status = embed.status ?? "finished";
2989
+ return embed.embed_id && embed.content && status !== "processing" && status !== "error" && status !== "cancelled";
2990
+ }).map((embed) => [embed.embed_id, embed])
2991
+ );
2992
+ if (finalized.size === 0) return;
2993
+ const session = this.requireSession();
2994
+ const masterKey = this.getMasterKeyBytes();
2995
+ const parentKeys = /* @__PURE__ */ new Map();
2996
+ const processed = /* @__PURE__ */ new Set();
2997
+ const makeKey = () => {
2998
+ const key = new Uint8Array(32);
2999
+ globalThis.crypto.getRandomValues(key);
3000
+ return key;
3001
+ };
3002
+ const persistOne = async (embed, embedKey, isChild) => {
3003
+ const now = Math.floor(Date.now() / 1e3);
3004
+ const createdAt = normalizeUnixSeconds(embed.createdAt, now);
3005
+ const updatedAt = normalizeUnixSeconds(embed.updatedAt, now);
3006
+ const messageId = embed.message_id || params.fallbackMessageId;
3007
+ const userId = embed.user_id || session.hashedEmail;
3008
+ const hashedChatId = computeSHA256(params.chatId);
3009
+ const hashedMessageId = computeSHA256(messageId);
3010
+ const hashedUserId = computeSHA256(userId);
3011
+ const hashedEmbedId = computeSHA256(embed.embed_id);
3012
+ const encryptedContent = await encryptWithAesGcmCombined(
3013
+ embed.content ?? "",
3014
+ embedKey
3015
+ );
3016
+ const encryptedType = await encryptWithAesGcmCombined(
3017
+ embed.type || "app_skill_use",
3018
+ embedKey
3019
+ );
3020
+ const encryptedTextPreview = embed.text_preview ? await encryptWithAesGcmCombined(embed.text_preview, embedKey) : void 0;
3021
+ await params.ws.sendAsync("store_embed", {
3022
+ embed_id: embed.embed_id,
3023
+ encrypted_type: encryptedType,
3024
+ encrypted_content: encryptedContent,
3025
+ encrypted_text_preview: encryptedTextPreview,
3026
+ status: embed.status || "finished",
3027
+ hashed_chat_id: hashedChatId,
3028
+ hashed_message_id: hashedMessageId,
3029
+ hashed_task_id: embed.task_id ? computeSHA256(embed.task_id) : void 0,
3030
+ hashed_user_id: hashedUserId,
3031
+ embed_ids: embed.embed_ids,
3032
+ parent_embed_id: embed.parent_embed_id || void 0,
3033
+ version_number: embed.version_number,
3034
+ file_path: embed.file_path,
3035
+ content_hash: embed.content_hash,
3036
+ text_length_chars: embed.text_length_chars,
3037
+ is_private: embed.is_private ?? false,
3038
+ is_shared: embed.is_shared ?? false,
3039
+ created_at: createdAt,
3040
+ updated_at: updatedAt
3041
+ });
3042
+ if (!isChild) {
3043
+ const keys = [
3044
+ {
3045
+ hashed_embed_id: hashedEmbedId,
3046
+ key_type: "master",
3047
+ hashed_chat_id: null,
3048
+ encrypted_embed_key: await encryptBytesWithAesGcm(embedKey, masterKey),
3049
+ hashed_user_id: hashedUserId,
3050
+ created_at: now
3051
+ },
3052
+ {
3053
+ hashed_embed_id: hashedEmbedId,
3054
+ key_type: "chat",
3055
+ hashed_chat_id: hashedChatId,
3056
+ encrypted_embed_key: await encryptBytesWithAesGcm(
3057
+ embedKey,
3058
+ params.chatKeyBytes
3059
+ ),
3060
+ hashed_user_id: hashedUserId,
3061
+ created_at: now
3062
+ }
3063
+ ];
3064
+ await params.ws.sendAsync("store_embed_keys", { keys });
3065
+ parentKeys.set(embed.embed_id, embedKey);
3066
+ }
3067
+ };
3068
+ let madeProgress = true;
3069
+ while (processed.size < finalized.size && madeProgress) {
3070
+ madeProgress = false;
3071
+ for (const embed of finalized.values()) {
3072
+ if (processed.has(embed.embed_id)) continue;
3073
+ const parentId = embed.parent_embed_id || null;
3074
+ if (parentId && finalized.has(parentId) && !parentKeys.has(parentId)) {
3075
+ continue;
3076
+ }
3077
+ const parentKey = parentId ? parentKeys.get(parentId) : void 0;
3078
+ await persistOne(embed, parentKey ?? makeKey(), Boolean(parentKey));
3079
+ processed.add(embed.embed_id);
3080
+ madeProgress = true;
3081
+ }
3082
+ }
3083
+ for (const embed of finalized.values()) {
3084
+ if (processed.has(embed.embed_id)) continue;
3085
+ await persistOne(embed, makeKey(), false);
3086
+ processed.add(embed.embed_id);
3087
+ }
2423
3088
  }
2424
3089
  /**
2425
3090
  * Delete a chat by ID.
@@ -2733,7 +3398,7 @@ var OpenMatesClient = class _OpenMatesClient {
2733
3398
  }
2734
3399
  return response.data;
2735
3400
  }
2736
- async settingsDelete(path) {
3401
+ async settingsDelete(path, body) {
2737
3402
  this.requireSession();
2738
3403
  const normalizedPath = this.normalizePath(path);
2739
3404
  if (BLOCKED_SETTINGS_MUTATE_PATHS.has(normalizedPath)) {
@@ -2741,6 +3406,7 @@ var OpenMatesClient = class _OpenMatesClient {
2741
3406
  }
2742
3407
  const response = await this.http.delete(
2743
3408
  normalizedPath,
3409
+ body,
2744
3410
  this.getCliRequestHeaders()
2745
3411
  );
2746
3412
  if (!response.ok) {
@@ -2792,6 +3458,149 @@ var OpenMatesClient = class _OpenMatesClient {
2792
3458
  }
2793
3459
  return response.data;
2794
3460
  }
3461
+ async listInvoices() {
3462
+ this.requireSession();
3463
+ const response = await this.http.get(
3464
+ "/v1/payments/invoices",
3465
+ this.getCliRequestHeaders()
3466
+ );
3467
+ if (!response.ok) {
3468
+ throw new Error(`Failed to fetch invoices (HTTP ${response.status})`);
3469
+ }
3470
+ return { invoices: response.data.invoices ?? [] };
3471
+ }
3472
+ async downloadInvoice(invoiceId) {
3473
+ return this.downloadPaymentPdf(
3474
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/download`,
3475
+ `Invoice_${invoiceId}.pdf`
3476
+ );
3477
+ }
3478
+ async downloadCreditNote(invoiceId) {
3479
+ return this.downloadPaymentPdf(
3480
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/credit-note/download`,
3481
+ `CreditNote_${invoiceId}.pdf`
3482
+ );
3483
+ }
3484
+ async requestRefund(invoiceId) {
3485
+ const session = this.requireSession();
3486
+ const emailEncryptionKey = await this.ensureEmailEncryptionKey(session);
3487
+ const response = await this.http.post(
3488
+ "/v1/payments/refund",
3489
+ { invoice_id: invoiceId, email_encryption_key: emailEncryptionKey },
3490
+ this.getCliRequestHeaders()
3491
+ );
3492
+ if (!response.ok) {
3493
+ throw new Error(`Refund request failed (HTTP ${response.status})`);
3494
+ }
3495
+ return response.data;
3496
+ }
3497
+ async updateUsername(username) {
3498
+ return this.settingsPost("user/username", { username });
3499
+ }
3500
+ async updateProfileImage(filePath) {
3501
+ const { uploadProfileImage } = await import("./uploadService-S464XJRA.js");
3502
+ const result = await uploadProfileImage(filePath, this.requireSession());
3503
+ if (result.status === "rejected") {
3504
+ throw new Error(result.detail ?? "Profile image rejected by content safety checks.");
3505
+ }
3506
+ if (result.status === "account_deleted") {
3507
+ throw new Error("Account deleted due to repeated profile image policy violations.");
3508
+ }
3509
+ if (result.status !== "ok") {
3510
+ throw new Error(result.detail ?? `Profile image upload failed with status '${result.status}'.`);
3511
+ }
3512
+ return result;
3513
+ }
3514
+ async getNewsletterCategories() {
3515
+ this.requireSession();
3516
+ const response = await this.http.get(
3517
+ "/v1/newsletter/categories",
3518
+ this.getCliRequestHeaders()
3519
+ );
3520
+ if (!response.ok) {
3521
+ throw new Error(`Failed to fetch newsletter categories (HTTP ${response.status})`);
3522
+ }
3523
+ return response.data;
3524
+ }
3525
+ async updateNewsletterCategories(categories) {
3526
+ this.requireSession();
3527
+ const response = await this.http.patch(
3528
+ "/v1/newsletter/categories",
3529
+ { categories },
3530
+ this.getCliRequestHeaders()
3531
+ );
3532
+ if (!response.ok) {
3533
+ throw new Error(`Failed to update newsletter categories (HTTP ${response.status})`);
3534
+ }
3535
+ return response.data;
3536
+ }
3537
+ async subscribeNewsletter(email, language = "en", darkmode = false) {
3538
+ const response = await this.http.post(
3539
+ "/v1/newsletter/subscribe",
3540
+ { email, language, darkmode },
3541
+ this.getCliRequestHeaders()
3542
+ );
3543
+ if (!response.ok) {
3544
+ throw new Error(`Newsletter subscribe failed (HTTP ${response.status})`);
3545
+ }
3546
+ return response.data;
3547
+ }
3548
+ async confirmNewsletter(token) {
3549
+ const response = await this.http.get(
3550
+ `/v1/newsletter/confirm/${encodeURIComponent(token)}`,
3551
+ this.getCliRequestHeaders()
3552
+ );
3553
+ if (!response.ok) {
3554
+ throw new Error(`Newsletter confirmation failed (HTTP ${response.status})`);
3555
+ }
3556
+ return response.data;
3557
+ }
3558
+ async unsubscribeNewsletter(token) {
3559
+ const response = await this.http.get(
3560
+ `/v1/newsletter/unsubscribe/${encodeURIComponent(token)}`,
3561
+ this.getCliRequestHeaders()
3562
+ );
3563
+ if (!response.ok) {
3564
+ throw new Error(`Newsletter unsubscribe failed (HTTP ${response.status})`);
3565
+ }
3566
+ return response.data;
3567
+ }
3568
+ async updateEmailNotificationSettings(payload) {
3569
+ const { ws } = await this.openWsClient();
3570
+ try {
3571
+ const ackPromise = ws.waitForMessage("email_notification_settings_ack");
3572
+ ws.send("email_notification_settings", payload);
3573
+ const ack = await ackPromise;
3574
+ return ack.payload;
3575
+ } finally {
3576
+ ws.close();
3577
+ }
3578
+ }
3579
+ async listNotifications(limit = 50) {
3580
+ this.requireSession();
3581
+ const response = await this.http.get(
3582
+ `/v1/notifications?limit=${encodeURIComponent(String(limit))}`,
3583
+ this.getCliRequestHeaders()
3584
+ );
3585
+ if (!response.ok) {
3586
+ throw new Error(`Failed to fetch notifications (HTTP ${response.status})`);
3587
+ }
3588
+ return response.data;
3589
+ }
3590
+ async *streamNotifications() {
3591
+ this.requireSession();
3592
+ for await (const message of this.http.streamSse(
3593
+ "/v1/notifications/stream",
3594
+ this.getCliRequestHeaders()
3595
+ )) {
3596
+ if (message.event && message.event !== "notification") continue;
3597
+ try {
3598
+ yield JSON.parse(message.data);
3599
+ } catch {
3600
+ yield { event: message.event ?? "message", data: message.data };
3601
+ }
3602
+ }
3603
+ }
2795
3604
  // -------------------------------------------------------------------------
2796
3605
  // Daily Inspirations
2797
3606
  // -------------------------------------------------------------------------
@@ -3051,7 +3860,7 @@ Required: ${schema.required.join(", ")}`
3051
3860
  );
3052
3861
  }
3053
3862
  }
3054
- const entryId = params.entryId ?? randomUUID();
3863
+ const entryId = params.entryId ?? randomUUID2();
3055
3864
  const now = Math.floor(Date.now() / 1e3);
3056
3865
  const hashedKey = hashItemKey(params.appId, params.itemType);
3057
3866
  const plaintextPayload = {
@@ -3256,6 +4065,45 @@ Required: ${schema.required.join(", ")}`
3256
4065
  if (path.startsWith("/")) return path;
3257
4066
  return `/v1/settings/${path}`;
3258
4067
  }
4068
+ async downloadPaymentPdf(path, fallbackFilename) {
4069
+ this.requireSession();
4070
+ const response = await this.http.getBinary(path, this.getCliRequestHeaders());
4071
+ if (!response.ok) {
4072
+ throw new Error(`Download failed (HTTP ${response.status})`);
4073
+ }
4074
+ return {
4075
+ filename: filenameFromContentDisposition(response.headers.get("content-disposition")) ?? fallbackFilename,
4076
+ data: response.data
4077
+ };
4078
+ }
4079
+ async ensureEmailEncryptionKey(session) {
4080
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
4081
+ await this.hydrateEmailEncryptionKey(session);
4082
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
4083
+ throw new Error(
4084
+ "Email encryption key is missing. Run `openmates login` again to refresh your local encryption keys."
4085
+ );
4086
+ }
4087
+ async hydrateEmailEncryptionKey(session) {
4088
+ const response = await this.http.post(
4089
+ "/v1/auth/session",
4090
+ { session_id: session.sessionId },
4091
+ this.getCliRequestHeaders()
4092
+ );
4093
+ const encryptedEmail = response.data.user?.encrypted_email_with_master_key;
4094
+ if (!response.ok || !response.data.success || typeof encryptedEmail !== "string") {
4095
+ return;
4096
+ }
4097
+ const email = await decryptWithAesGcmCombined(
4098
+ encryptedEmail,
4099
+ base64ToBytes(session.masterKeyExportedB64)
4100
+ );
4101
+ if (!email) return;
4102
+ session.emailEncryptionKeyB64 = await deriveEmailEncryptionKeyB64(
4103
+ email,
4104
+ session.userEmailSalt
4105
+ );
4106
+ }
3259
4107
  requireSession() {
3260
4108
  if (!this.session) {
3261
4109
  throw new Error("Not logged in. Run `openmates login`.");
@@ -3284,7 +4132,7 @@ Required: ${schema.required.join(", ")}`
3284
4132
  makeWsClient(session) {
3285
4133
  return new OpenMatesWsClient({
3286
4134
  apiUrl: session.apiUrl,
3287
- sessionId: randomUUID(),
4135
+ sessionId: randomUUID2(),
3288
4136
  wsToken: session.wsToken,
3289
4137
  refreshToken: session.cookies.auth_refresh_token ?? null,
3290
4138
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -3631,6 +4479,15 @@ function parseNewChatSuggestionText(text) {
3631
4479
  const skillId = raw.slice(dashIdx + 1);
3632
4480
  return { body, appId, skillId };
3633
4481
  }
4482
+ function filenameFromContentDisposition(header2) {
4483
+ if (!header2) return null;
4484
+ const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header2)?.[1];
4485
+ if (encoded) return decodeURIComponent(encoded);
4486
+ const quoted = /filename="([^"]+)"/i.exec(header2)?.[1];
4487
+ if (quoted) return quoted;
4488
+ const plain = /filename=([^;]+)/i.exec(header2)?.[1];
4489
+ return plain?.trim() ?? null;
4490
+ }
3634
4491
  function sleep(ms) {
3635
4492
  return new Promise((resolve4) => setTimeout(resolve4, ms));
3636
4493
  }
@@ -3646,8 +4503,11 @@ function printLogo() {
3646
4503
  stdout.write(lines.join("\n") + "\n");
3647
4504
  }
3648
4505
 
4506
+ // src/cli.ts
4507
+ import { createInterface as createInterface3 } from "readline/promises";
4508
+
3649
4509
  // ../secret-scanner/src/registry.ts
3650
- import { createRequire } from "module";
4510
+ import { createRequire as createRequire2 } from "module";
3651
4511
 
3652
4512
  // ../secret-scanner/src/types.ts
3653
4513
  var SECRET_ENV_PATTERNS = [
@@ -3781,8 +4641,8 @@ var SECRET_PATTERNS = [
3781
4641
  ];
3782
4642
 
3783
4643
  // ../secret-scanner/src/registry.ts
3784
- var require2 = createRequire(import.meta.url);
3785
- var AhoCorasick = require2("ahocorasick");
4644
+ var require3 = createRequire2(import.meta.url);
4645
+ var AhoCorasick = require3("ahocorasick");
3786
4646
  var MIN_SECRET_LENGTH = 8;
3787
4647
  var SecretRegistry = class {
3788
4648
  /** Map from plaintext value → registry entry */
@@ -3920,8 +4780,8 @@ var SecretRegistry = class {
3920
4780
  const sortedResults = [];
3921
4781
  for (const result of results) {
3922
4782
  const endIdx = result[0];
3923
- const matches = result[1];
3924
- for (const match of matches) {
4783
+ const matches2 = result[1];
4784
+ for (const match of matches2) {
3925
4785
  sortedResults.push({ endIndex: endIdx, match });
3926
4786
  }
3927
4787
  }
@@ -4324,228 +5184,108 @@ import { readFileSync as readFileSync3, statSync, existsSync as existsSync3 } fr
4324
5184
  import { basename, extname, resolve } from "path";
4325
5185
  import { homedir as homedir3 } from "os";
4326
5186
  import { createHash as createHash4 } from "crypto";
4327
-
4328
- // src/embedCreator.ts
4329
- import { randomUUID as randomUUID2, createHash as createHash3, randomBytes, webcrypto as webcrypto4 } from "crypto";
4330
- import { encode as toonEncode } from "@toon-format/toon";
4331
- var cryptoApi3 = webcrypto4;
4332
- var AES_GCM_IV_LENGTH3 = 12;
4333
- function bytesToBase643(input) {
4334
- return Buffer.from(input).toString("base64");
4335
- }
4336
- function toArrayBuffer2(input) {
4337
- return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
4338
- }
4339
- async function encryptAesGcm(plaintext, rawKeyBytes) {
4340
- const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
4341
- const key = await cryptoApi3.subtle.importKey(
4342
- "raw",
4343
- toArrayBuffer2(rawKeyBytes),
4344
- { name: "AES-GCM" },
4345
- false,
4346
- ["encrypt"]
4347
- );
4348
- const encrypted = await cryptoApi3.subtle.encrypt(
4349
- { name: "AES-GCM", iv: toArrayBuffer2(iv) },
4350
- key,
4351
- new TextEncoder().encode(plaintext)
4352
- );
4353
- const cipherBytes = new Uint8Array(encrypted);
4354
- const combined = new Uint8Array(iv.length + cipherBytes.length);
4355
- combined.set(iv);
4356
- combined.set(cipherBytes, iv.length);
4357
- return bytesToBase643(combined);
4358
- }
4359
- async function wrapKey(embedKey, wrappingKey) {
4360
- const cryptoKey = await cryptoApi3.subtle.importKey(
4361
- "raw",
4362
- toArrayBuffer2(wrappingKey),
4363
- { name: "AES-GCM" },
4364
- false,
4365
- ["encrypt"]
4366
- );
4367
- const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
4368
- const encrypted = await cryptoApi3.subtle.encrypt(
4369
- { name: "AES-GCM", iv: toArrayBuffer2(iv) },
4370
- cryptoKey,
4371
- toArrayBuffer2(embedKey)
4372
- );
4373
- const cipherBytes = new Uint8Array(encrypted);
4374
- const combined = new Uint8Array(iv.length + cipherBytes.length);
4375
- combined.set(iv);
4376
- combined.set(cipherBytes, iv.length);
4377
- return bytesToBase643(combined);
4378
- }
4379
- function generateEmbedKey() {
4380
- return new Uint8Array(randomBytes(32));
4381
- }
4382
- function computeSHA256(content) {
4383
- return createHash3("sha256").update(content).digest("hex");
4384
- }
4385
- function toonEncodeContent(data) {
4386
- try {
4387
- return toonEncode(data);
4388
- } catch {
4389
- return JSON.stringify(data);
4390
- }
4391
- }
4392
- function generateEmbedId() {
4393
- return randomUUID2();
4394
- }
4395
- function createEmbedReferenceBlock(type, embedId) {
4396
- const ref = JSON.stringify({ type, embed_id: embedId }, null, 2);
4397
- return "```json\n" + ref + "\n```";
4398
- }
4399
- async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId) {
4400
- try {
4401
- const embedKey = generateEmbedKey();
4402
- const encryptedContent = await encryptAesGcm(embed.content, embedKey);
4403
- const encryptedType = await encryptAesGcm(embed.type, embedKey);
4404
- const encryptedTextPreview = await encryptAesGcm(embed.textPreview, embedKey);
4405
- const hashedEmbedId = computeSHA256(embed.embedId);
4406
- const hashedChatId = computeSHA256(chatId);
4407
- const hashedMessageId = computeSHA256(messageId);
4408
- const hashedUserId = computeSHA256(userId);
4409
- const wrappedWithMaster = await wrapKey(embedKey, masterKey);
4410
- const nowSeconds = Math.floor(Date.now() / 1e3);
4411
- const embedKeys = [
4412
- {
4413
- hashed_embed_id: hashedEmbedId,
4414
- key_type: "master",
4415
- hashed_chat_id: null,
4416
- encrypted_embed_key: wrappedWithMaster,
4417
- hashed_user_id: hashedUserId,
4418
- created_at: nowSeconds
4419
- }
4420
- ];
4421
- if (chatKey) {
4422
- const wrappedWithChat = await wrapKey(embedKey, chatKey);
4423
- embedKeys.push({
4424
- hashed_embed_id: hashedEmbedId,
4425
- key_type: "chat",
4426
- hashed_chat_id: hashedChatId,
4427
- encrypted_embed_key: wrappedWithChat,
4428
- hashed_user_id: hashedUserId,
4429
- created_at: nowSeconds
4430
- });
4431
- }
4432
- return {
4433
- embed_id: embed.embedId,
4434
- encrypted_type: encryptedType,
4435
- encrypted_content: encryptedContent,
4436
- encrypted_text_preview: encryptedTextPreview,
4437
- status: embed.status,
4438
- hashed_chat_id: hashedChatId,
4439
- hashed_message_id: hashedMessageId,
4440
- hashed_user_id: hashedUserId,
4441
- file_path: embed.filePath,
4442
- content_hash: embed.contentHash,
4443
- text_length_chars: embed.textLengthChars,
4444
- created_at: nowSeconds,
4445
- updated_at: nowSeconds,
4446
- embed_keys: embedKeys
4447
- };
4448
- } catch (error) {
4449
- const msg = error instanceof Error ? error.message : String(error);
4450
- process.stderr.write(`\x1B[31mError:\x1B[0m Failed to encrypt embed: ${msg}
4451
- `);
4452
- return null;
4453
- }
4454
- }
4455
-
4456
- // src/fileEmbed.ts
4457
- var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
4458
- var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
4459
- ".pem",
4460
- ".key",
4461
- ".p12",
4462
- ".pfx",
4463
- ".keystore",
4464
- ".kdbx",
4465
- ".credentials"
4466
- ]);
4467
- var BLOCKED_NAMES = /* @__PURE__ */ new Set([
4468
- "id_rsa",
4469
- "id_ed25519",
4470
- "id_dsa",
4471
- "id_ecdsa",
4472
- "authorized_keys",
4473
- "known_hosts",
4474
- ".git-credentials",
4475
- ".netrc",
4476
- ".pgpass",
4477
- ".my.cnf"
4478
- ]);
4479
- var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4480
- "py",
4481
- "js",
4482
- "ts",
4483
- "html",
4484
- "css",
4485
- "json",
4486
- "svelte",
4487
- "java",
4488
- "cpp",
4489
- "c",
4490
- "h",
4491
- "hpp",
4492
- "rs",
4493
- "go",
4494
- "rb",
4495
- "php",
4496
- "swift",
4497
- "kt",
4498
- "txt",
4499
- "md",
4500
- "xml",
4501
- "yaml",
4502
- "yml",
4503
- "sh",
4504
- "bash",
4505
- "sql",
4506
- "vue",
4507
- "jsx",
4508
- "tsx",
4509
- "scss",
4510
- "less",
4511
- "sass",
4512
- "dockerfile",
4513
- "toml",
4514
- "ini",
4515
- "cfg",
4516
- "conf",
4517
- "env",
4518
- "envrc",
4519
- "graphql",
4520
- "gql",
4521
- "r",
4522
- "m",
4523
- "pl",
4524
- "lua",
4525
- "ex",
4526
- "exs",
4527
- "erl",
4528
- "hs",
4529
- "scala",
4530
- "dart",
4531
- "tf"
4532
- ]);
4533
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
4534
- "jpg",
4535
- "jpeg",
4536
- "png",
4537
- "webp",
4538
- "gif",
4539
- "heic",
4540
- "heif",
4541
- "bmp",
4542
- "tiff",
4543
- "tif",
4544
- "svg"
4545
- ]);
4546
- function isEnvFile(filename) {
4547
- const lower = filename.toLowerCase();
4548
- return lower === ".env" || lower.startsWith(".env.") || lower === ".envrc";
5187
+ var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
5188
+ var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
5189
+ ".pem",
5190
+ ".key",
5191
+ ".p12",
5192
+ ".pfx",
5193
+ ".keystore",
5194
+ ".kdbx",
5195
+ ".credentials"
5196
+ ]);
5197
+ var BLOCKED_NAMES = /* @__PURE__ */ new Set([
5198
+ "id_rsa",
5199
+ "id_ed25519",
5200
+ "id_dsa",
5201
+ "id_ecdsa",
5202
+ "authorized_keys",
5203
+ "known_hosts",
5204
+ ".git-credentials",
5205
+ ".netrc",
5206
+ ".pgpass",
5207
+ ".my.cnf"
5208
+ ]);
5209
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
5210
+ "py",
5211
+ "js",
5212
+ "ts",
5213
+ "html",
5214
+ "css",
5215
+ "json",
5216
+ "svelte",
5217
+ "java",
5218
+ "cpp",
5219
+ "c",
5220
+ "h",
5221
+ "hpp",
5222
+ "rs",
5223
+ "go",
5224
+ "rb",
5225
+ "php",
5226
+ "swift",
5227
+ "kt",
5228
+ "txt",
5229
+ "md",
5230
+ "xml",
5231
+ "yaml",
5232
+ "yml",
5233
+ "sh",
5234
+ "bash",
5235
+ "sql",
5236
+ "vue",
5237
+ "jsx",
5238
+ "tsx",
5239
+ "scss",
5240
+ "less",
5241
+ "sass",
5242
+ "dockerfile",
5243
+ "toml",
5244
+ "ini",
5245
+ "cfg",
5246
+ "conf",
5247
+ "env",
5248
+ "envrc",
5249
+ "graphql",
5250
+ "gql",
5251
+ "r",
5252
+ "m",
5253
+ "pl",
5254
+ "lua",
5255
+ "ex",
5256
+ "exs",
5257
+ "erl",
5258
+ "hs",
5259
+ "scala",
5260
+ "dart",
5261
+ "tf"
5262
+ ]);
5263
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
5264
+ "jpg",
5265
+ "jpeg",
5266
+ "png",
5267
+ "webp",
5268
+ "gif",
5269
+ "heic",
5270
+ "heif",
5271
+ "bmp",
5272
+ "tiff",
5273
+ "tif",
5274
+ "svg"
5275
+ ]);
5276
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
5277
+ "mp3",
5278
+ "m4a",
5279
+ "mp4",
5280
+ "wav",
5281
+ "webm",
5282
+ "ogg",
5283
+ "oga",
5284
+ "aac"
5285
+ ]);
5286
+ function isEnvFile(filename) {
5287
+ const lower = filename.toLowerCase();
5288
+ return lower === ".env" || lower.startsWith(".env.") || lower === ".envrc";
4549
5289
  }
4550
5290
  function getExt(filename) {
4551
5291
  const ext = extname(filename).toLowerCase();
@@ -4563,6 +5303,9 @@ function isImageFile(filename) {
4563
5303
  function isPDFFile(filename) {
4564
5304
  return getExt(filename) === "pdf";
4565
5305
  }
5306
+ function isAudioFile(filename) {
5307
+ return AUDIO_EXTENSIONS.has(getExt(filename));
5308
+ }
4566
5309
  var LANGUAGE_MAP = {
4567
5310
  ts: "typescript",
4568
5311
  tsx: "typescript",
@@ -4707,10 +5450,14 @@ function processFiles(filePaths, redactor) {
4707
5450
  const result = processPDFFile(resolvedPath, filename);
4708
5451
  if (result) embeds.push(result);
4709
5452
  else errors.push({ path: rawPath, error: "Failed to process PDF" });
5453
+ } else if (isAudioFile(filename)) {
5454
+ const result = processAudioFile(resolvedPath, filename);
5455
+ if (result) embeds.push(result);
5456
+ else errors.push({ path: rawPath, error: "Failed to process audio" });
4710
5457
  } else {
4711
5458
  errors.push({
4712
5459
  path: rawPath,
4713
- error: `Unsupported file type: .${ext}. Supported: code/text, images, PDFs.`
5460
+ error: `Unsupported file type: .${ext}. Supported: code/text, images, PDFs, audio.`
4714
5461
  });
4715
5462
  }
4716
5463
  }
@@ -4839,87 +5586,107 @@ function processPDFFile(filePath, filename) {
4839
5586
  return null;
4840
5587
  }
4841
5588
  }
5589
+ function processAudioFile(filePath, filename) {
5590
+ try {
5591
+ const embedId = generateEmbedId();
5592
+ const embedContent = toonEncodeContent({
5593
+ app_id: "audio",
5594
+ skill_id: "transcribe",
5595
+ type: "audio-recording",
5596
+ status: "uploading",
5597
+ filename
5598
+ });
5599
+ const embed = {
5600
+ embedId,
5601
+ type: "audio-recording",
5602
+ content: embedContent,
5603
+ textPreview: filename,
5604
+ status: "processing"
5605
+ };
5606
+ return {
5607
+ embed,
5608
+ referenceBlock: createEmbedReferenceBlock("audio-recording", embedId),
5609
+ displayName: filename,
5610
+ secretsRedacted: false,
5611
+ zeroKnowledge: false,
5612
+ requiresUpload: true,
5613
+ localPath: filePath
5614
+ };
5615
+ } catch (e) {
5616
+ process.stderr.write(
5617
+ `\x1B[31mError:\x1B[0m Failed to process ${filename}: ${e instanceof Error ? e.message : String(e)}
5618
+ `
5619
+ );
5620
+ return null;
5621
+ }
5622
+ }
4842
5623
  function formatEmbedsForMessage(embeds) {
4843
5624
  if (embeds.length === 0) return "";
4844
5625
  return "\n" + embeds.map((e) => e.referenceBlock).join("\n");
4845
5626
  }
4846
5627
 
4847
- // src/uploadService.ts
4848
- import { readFileSync as readFileSync4 } from "fs";
4849
- import { basename as basename2 } from "path";
4850
- var UPLOAD_MAX_ATTEMPTS = 3;
4851
- var UPLOAD_RETRY_DELAY_MS = 2e3;
4852
- function getUploadUrl(apiUrl) {
4853
- try {
4854
- const url = new URL(apiUrl);
4855
- if (url.hostname === "localhost") return "http://localhost:8001";
4856
- } catch {
4857
- }
4858
- return "https://upload.openmates.org";
5628
+ // src/urlEmbed.ts
5629
+ var URL_PATTERN = /\bhttps?:\/\/[^\s<>()`"']+/gi;
5630
+ var FENCED_BLOCK_PATTERN = /(```[\s\S]*?```)/g;
5631
+ var TRAILING_URL_PUNCTUATION = /[.,!?;:]+$/;
5632
+ function trimTrailingUrlPunctuation(rawUrl) {
5633
+ let url = rawUrl;
5634
+ let suffix = "";
5635
+ const punctuation = url.match(TRAILING_URL_PUNCTUATION)?.[0] ?? "";
5636
+ if (punctuation) {
5637
+ url = url.slice(0, -punctuation.length);
5638
+ suffix = punctuation + suffix;
5639
+ }
5640
+ while (url.endsWith(")")) {
5641
+ const openCount = (url.match(/\(/g) ?? []).length;
5642
+ const closeCount = (url.match(/\)/g) ?? []).length;
5643
+ if (closeCount <= openCount) break;
5644
+ url = url.slice(0, -1);
5645
+ suffix = `)${suffix}`;
5646
+ }
5647
+ return { url, suffix };
5648
+ }
5649
+ function createWebsiteEmbed(url) {
5650
+ const embedId = generateEmbedId();
5651
+ const content = toonEncodeContent({
5652
+ url,
5653
+ title: null,
5654
+ description: null,
5655
+ favicon: null,
5656
+ image: null,
5657
+ site_name: null,
5658
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString()
5659
+ });
5660
+ return {
5661
+ embedId,
5662
+ type: "web-website",
5663
+ content,
5664
+ textPreview: url,
5665
+ status: "finished"
5666
+ };
4859
5667
  }
4860
- async function uploadFile(filePath, session) {
4861
- const filename = basename2(filePath);
4862
- const fileBytes = readFileSync4(filePath);
4863
- const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
4864
- const cookies = [];
4865
- if (session.cookies?.auth_refresh_token) {
4866
- cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
4867
- }
4868
- let response;
4869
- let lastError;
4870
- for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
5668
+ function replaceUrlsInText(text, embeds) {
5669
+ return text.replace(URL_PATTERN, (rawUrl) => {
5670
+ const { url, suffix } = trimTrailingUrlPunctuation(rawUrl);
5671
+ if (!url) return rawUrl;
4871
5672
  try {
4872
- const blob = new Blob([fileBytes]);
4873
- const formData = new FormData();
4874
- formData.append("file", blob, filename);
4875
- response = await fetch(uploadUrl, {
4876
- method: "POST",
4877
- body: formData,
4878
- headers: {
4879
- ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
4880
- },
4881
- signal: AbortSignal.timeout(10 * 60 * 1e3)
4882
- // 10-minute timeout
4883
- });
4884
- break;
4885
- } catch (error) {
4886
- lastError = error;
4887
- if (attempt === UPLOAD_MAX_ATTEMPTS) break;
4888
- await new Promise((resolve4) => setTimeout(resolve4, UPLOAD_RETRY_DELAY_MS));
5673
+ new URL(url);
5674
+ } catch {
5675
+ return rawUrl;
4889
5676
  }
4890
- }
4891
- if (!response) {
4892
- const message = lastError instanceof Error ? lastError.message : String(lastError);
4893
- throw new Error(message || "Upload request failed.");
4894
- }
4895
- if (!response.ok) {
4896
- const status = response.status;
4897
- let errorMessage;
4898
- switch (status) {
4899
- case 401:
4900
- errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
4901
- break;
4902
- case 413:
4903
- errorMessage = "File too large (maximum 100 MB).";
4904
- break;
4905
- case 415:
4906
- errorMessage = "Unsupported file type.";
4907
- break;
4908
- case 422: {
4909
- const body = await response.text().catch(() => "");
4910
- errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
4911
- break;
4912
- }
4913
- case 429:
4914
- errorMessage = "Upload rate limit exceeded. Try again in a minute.";
4915
- break;
4916
- default:
4917
- errorMessage = `Upload failed (HTTP ${status}).`;
4918
- }
4919
- throw new Error(errorMessage);
4920
- }
4921
- const data = await response.json();
4922
- return data;
5677
+ const embed = createWebsiteEmbed(url);
5678
+ embeds.push(embed);
5679
+ return `${createEmbedReferenceBlock("website", embed.embedId, url)}${suffix}`;
5680
+ });
5681
+ }
5682
+ function prepareUrlEmbeds(message) {
5683
+ const embeds = [];
5684
+ const parts = message.split(FENCED_BLOCK_PATTERN);
5685
+ const processed = parts.map((part, index) => {
5686
+ const isFence = index % 2 === 1 && part.startsWith("```");
5687
+ return isFence ? part : replaceUrlsInText(part, embeds);
5688
+ }).join("");
5689
+ return { message: processed, embeds };
4923
5690
  }
4924
5691
 
4925
5692
  // src/embedRenderers.ts
@@ -5971,13 +6738,14 @@ function formatTs(ts) {
5971
6738
 
5972
6739
  // src/server.ts
5973
6740
  import { execSync, spawn as nodeSpawn } from "child_process";
5974
- import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, rmSync as rmSync3 } from "fs";
6741
+ import { randomBytes as randomBytes2 } from "crypto";
6742
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
5975
6743
  import { createInterface as createInterface2 } from "readline";
5976
6744
  import { homedir as homedir5 } from "os";
5977
6745
  import { join as join3, resolve as resolve3 } from "path";
5978
6746
 
5979
6747
  // src/serverConfig.ts
5980
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6748
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5981
6749
  import { homedir as homedir4 } from "os";
5982
6750
  import { join as join2, resolve as resolve2 } from "path";
5983
6751
  var STATE_DIR = join2(homedir4(), ".openmates");
@@ -5998,7 +6766,7 @@ function loadServerConfig() {
5998
6766
  const filePath = join2(STATE_DIR, CONFIG_FILE);
5999
6767
  if (!existsSync4(filePath)) return null;
6000
6768
  try {
6001
- return JSON.parse(readFileSync5(filePath, "utf-8"));
6769
+ return JSON.parse(readFileSync4(filePath, "utf-8"));
6002
6770
  } catch {
6003
6771
  return null;
6004
6772
  }
@@ -6009,16 +6777,17 @@ function removeServerConfig() {
6009
6777
  rmSync2(filePath);
6010
6778
  }
6011
6779
  }
6012
- var COMPOSE_MARKER = join2("backend", "core", "docker-compose.yml");
6780
+ var SOURCE_COMPOSE_MARKER = join2("backend", "core", "docker-compose.yml");
6781
+ var IMAGE_COMPOSE_MARKER = join2("backend", "core", "docker-compose.selfhost.yml");
6013
6782
  function isOpenMatesDir(dir) {
6014
- return existsSync4(join2(dir, COMPOSE_MARKER));
6783
+ return existsSync4(join2(dir, SOURCE_COMPOSE_MARKER)) || existsSync4(join2(dir, IMAGE_COMPOSE_MARKER));
6015
6784
  }
6016
6785
  function resolveServerPath(flags) {
6017
6786
  if (typeof flags.path === "string" && flags.path) {
6018
6787
  const explicit = resolve2(flags.path);
6019
6788
  if (!isOpenMatesDir(explicit)) {
6020
6789
  throw new Error(
6021
- `${explicit} does not appear to be an OpenMates installation (missing ${COMPOSE_MARKER}).`
6790
+ `${explicit} does not appear to be an OpenMates installation (missing ${SOURCE_COMPOSE_MARKER} or ${IMAGE_COMPOSE_MARKER}).`
6022
6791
  );
6023
6792
  }
6024
6793
  return explicit;
@@ -6037,10 +6806,78 @@ function resolveServerPath(flags) {
6037
6806
  }
6038
6807
 
6039
6808
  // src/server.ts
6040
- var COMPOSE_FILE = join3("backend", "core", "docker-compose.yml");
6809
+ var SOURCE_COMPOSE_FILE = join3("backend", "core", "docker-compose.yml");
6810
+ var IMAGE_COMPOSE_FILE = join3("backend", "core", "docker-compose.selfhost.yml");
6041
6811
  var COMPOSE_OVERRIDE = join3("backend", "core", "docker-compose.override.yml");
6042
6812
  var DEFAULT_INSTALL_PATH = join3(homedir5(), "openmates");
6043
6813
  var REPO_URL = "https://github.com/glowingkitty/OpenMates.git";
6814
+ var DEV_BRANCH = "dev";
6815
+ var DEFAULT_IMAGE_REGISTRY = "ghcr.io/glowingkitty";
6816
+ var MINIMAL_ENV_TEMPLATE = `# OpenMates self-host image-mode environment
6817
+ SECRET__MISTRAL_AI__API_KEY=
6818
+ SECRET__CEREBRAS__API_KEY=
6819
+ SECRET__GROQ__API_KEY=
6820
+ SECRET__OPENAI__API_KEY=
6821
+ SECRET__ANTHROPIC__API_KEY=
6822
+ SECRET__GOOGLE_AI_STUDIO__API_KEY=
6823
+ SECRET__OPENROUTER__API_KEY=
6824
+ SECRET__TOGETHER__API_KEY=
6825
+ SECRET__BRAVE__API_KEY=
6826
+ SECRET__FIRECRAWL__API_KEY=
6827
+ SECRET__CONTEXT7__API_KEY=
6828
+ DATABASE_ADMIN_EMAIL=admin@example.com
6829
+ DATABASE_ADMIN_PASSWORD=
6830
+ DATABASE_NAME=directus
6831
+ DATABASE_USERNAME=directus
6832
+ DATABASE_PASSWORD=
6833
+ DIRECTUS_TOKEN=
6834
+ DIRECTUS_SECRET=
6835
+ DRAGONFLY_PASSWORD=
6836
+ OPENOBSERVE_ROOT_EMAIL=admin@openmates.internal
6837
+ OPENOBSERVE_ROOT_PASSWORD=
6838
+ INTERNAL_API_SHARED_TOKEN=
6839
+ TUNNEL_TRIGGER_SECRET=<PLACEHOLDER>
6840
+ SERVER_ENVIRONMENT=production
6841
+ FRONTEND_URLS="http://localhost:5173"
6842
+ PRODUCTION_URL="http://localhost:5173"
6843
+ TRUSTED_PROXY_IPS="172.16.0.0/12"
6844
+ CORE_SIDECAR_URL=http://admin-sidecar:8001
6845
+ CLEAR_CACHE_ON_UPDATE=true
6846
+ SIGNUP_LIMIT=20
6847
+ SELF_HOST_SIGNUP_MODE=invite_only
6848
+ SELF_HOST_SIGNUP_ALLOWED_DOMAINS=
6849
+ SELF_HOST_FIRST_INVITE_CODE=
6850
+ APPLICATION_PREVIEW_ORIGIN=
6851
+ OPENMATES_IMAGE_REGISTRY=${DEFAULT_IMAGE_REGISTRY}
6852
+ OPENMATES_IMAGE_TAG=
6853
+ GIT_WORK_DIR=
6854
+ DOCKER_GID=999
6855
+ `;
6856
+ var VAULT_CONFIG_TEMPLATE = `# Minimal Vault configuration
6857
+ listener "tcp" {
6858
+ address = "0.0.0.0:8200"
6859
+ tls_disable = true
6860
+ }
6861
+
6862
+ storage "file" {
6863
+ path = "/vault/file"
6864
+ }
6865
+
6866
+ api_addr = "http://0.0.0.0:8200"
6867
+ ui = false
6868
+ disable_mlock = true
6869
+ log_level = "info"
6870
+ `;
6871
+ var LLM_PROVIDER_ENV_KEYS = /* @__PURE__ */ new Set([
6872
+ "SECRET__MISTRAL_AI__API_KEY",
6873
+ "SECRET__CEREBRAS__API_KEY",
6874
+ "SECRET__GROQ__API_KEY",
6875
+ "SECRET__OPENAI__API_KEY",
6876
+ "SECRET__ANTHROPIC__API_KEY",
6877
+ "SECRET__GOOGLE_AI_STUDIO__API_KEY",
6878
+ "SECRET__OPENROUTER__API_KEY",
6879
+ "SECRET__TOGETHER__API_KEY"
6880
+ ]);
6044
6881
  function exec(cmd, cwd) {
6045
6882
  return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
6046
6883
  }
@@ -6051,12 +6888,40 @@ function runInteractive(cmd, args, cwd) {
6051
6888
  child.on("error", reject);
6052
6889
  });
6053
6890
  }
6054
- function composeArgs(installPath, withOverrides) {
6055
- const args = ["compose", "--env-file", ".env", "-f", COMPOSE_FILE];
6891
+ function loadConfigForInstallPath(installPath) {
6892
+ const config = loadServerConfig();
6893
+ return config?.installPath === installPath ? config : null;
6894
+ }
6895
+ function getInstallMode(installPath, config = loadConfigForInstallPath(installPath)) {
6896
+ if (config?.installMode) return config.installMode;
6897
+ if (existsSync5(join3(installPath, IMAGE_COMPOSE_FILE))) return "image";
6898
+ return "source";
6899
+ }
6900
+ function shouldPullImages() {
6901
+ return process.env.OPENMATES_SKIP_IMAGE_PULL !== "1";
6902
+ }
6903
+ function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath)) {
6904
+ const composeFile = installMode === "image" ? IMAGE_COMPOSE_FILE : SOURCE_COMPOSE_FILE;
6905
+ const args = ["compose", "--env-file", ".env", "-f", composeFile];
6056
6906
  if (withOverrides && existsSync5(join3(installPath, COMPOSE_OVERRIDE))) {
6057
6907
  args.push("-f", COMPOSE_OVERRIDE);
6058
6908
  }
6059
- return args;
6909
+ return args;
6910
+ }
6911
+ function ensureGitWorkDirEnv(installPath) {
6912
+ const envPath = join3(installPath, ".env");
6913
+ if (!existsSync5(envPath)) return;
6914
+ const content = readFileSync5(envPath, "utf-8");
6915
+ const lineRegex = /^GIT_WORK_DIR=.*$/m;
6916
+ const value = `GIT_WORK_DIR=${installPath}`;
6917
+ if (lineRegex.test(content)) {
6918
+ const next = content.replace(lineRegex, value);
6919
+ if (next !== content) writeFileSync3(envPath, next);
6920
+ return;
6921
+ }
6922
+ const separator = content.endsWith("\n") ? "" : "\n";
6923
+ writeFileSync3(envPath, `${content}${separator}${value}
6924
+ `);
6060
6925
  }
6061
6926
  function requireDocker() {
6062
6927
  try {
@@ -6074,9 +6939,94 @@ function requireGit() {
6074
6939
  throw new Error("git is not installed. Install it first: https://git-scm.com/downloads");
6075
6940
  }
6076
6941
  }
6942
+ function getPackageVersion() {
6943
+ try {
6944
+ const packageJson = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf-8"));
6945
+ return packageJson.version ?? "";
6946
+ } catch {
6947
+ return "";
6948
+ }
6949
+ }
6950
+ function getDefaultImageTag() {
6951
+ const version = getPackageVersion();
6952
+ return version ? `v${version}` : "dev";
6953
+ }
6954
+ function defaultTemplateRefForVersion(version) {
6955
+ return /-(alpha|beta|rc)(\.|\d|$)/.test(version) ? DEV_BRANCH : `v${version}`;
6956
+ }
6957
+ function randomHex(bytes) {
6958
+ return randomBytes2(bytes).toString("hex");
6959
+ }
6960
+ function generateInviteCode() {
6961
+ const digits = Array.from(randomBytes2(12), (byte) => String(byte % 10)).join("");
6962
+ return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
6963
+ }
6964
+ function getEnvVar(content, name) {
6965
+ const match = content.match(new RegExp(`^${name}=(.*)$`, "m"));
6966
+ return match?.[1]?.replace(/^"|"$/g, "") ?? "";
6967
+ }
6968
+ function setEnvVar(content, name, value) {
6969
+ const line = `${name}=${value}`;
6970
+ const pattern = new RegExp(`^${name}=.*$`, "m");
6971
+ if (pattern.test(content)) {
6972
+ return content.replace(pattern, line);
6973
+ }
6974
+ const separator = content.endsWith("\n") ? "" : "\n";
6975
+ return `${content}${separator}${line}
6976
+ `;
6977
+ }
6978
+ function setEnvIfEmpty(content, name, value) {
6979
+ return getEnvVar(content, name) ? content : setEnvVar(content, name, value);
6980
+ }
6981
+ async function fetchText(url) {
6982
+ const response = await fetch(url);
6983
+ if (!response.ok) {
6984
+ throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
6985
+ }
6986
+ return response.text();
6987
+ }
6988
+ async function loadSelfHostComposeTemplate(version) {
6989
+ const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
6990
+ if (templateDir) {
6991
+ return readFileSync5(join3(resolve3(templateDir), IMAGE_COMPOSE_FILE), "utf-8");
6992
+ }
6993
+ const overrideUrl = process.env.OPENMATES_SELFHOST_COMPOSE_URL;
6994
+ if (overrideUrl) {
6995
+ return fetchText(overrideUrl);
6996
+ }
6997
+ const ref = defaultTemplateRefForVersion(version);
6998
+ return fetchText(
6999
+ `https://raw.githubusercontent.com/glowingkitty/OpenMates/${ref}/backend/core/docker-compose.selfhost.yml`
7000
+ );
7001
+ }
7002
+ async function writeImageModeRuntimeFiles(installPath, imageTag) {
7003
+ const coreDir = join3(installPath, "backend", "core");
7004
+ const vaultConfigDir = join3(coreDir, "vault", "config");
7005
+ mkdirSync3(vaultConfigDir, { recursive: true });
7006
+ writeFileSync3(join3(coreDir, "docker-compose.selfhost.yml"), await loadSelfHostComposeTemplate(getPackageVersion()));
7007
+ writeFileSync3(join3(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
7008
+ const envPath = join3(installPath, ".env");
7009
+ let envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : MINIMAL_ENV_TEMPLATE;
7010
+ envContent = setEnvIfEmpty(envContent, "DATABASE_ADMIN_PASSWORD", randomHex(12));
7011
+ envContent = setEnvIfEmpty(envContent, "DATABASE_PASSWORD", randomHex(12));
7012
+ envContent = setEnvIfEmpty(envContent, "DIRECTUS_TOKEN", randomHex(32));
7013
+ envContent = setEnvIfEmpty(envContent, "DIRECTUS_SECRET", randomHex(32));
7014
+ envContent = setEnvIfEmpty(envContent, "DRAGONFLY_PASSWORD", randomHex(12));
7015
+ envContent = setEnvIfEmpty(envContent, "OPENOBSERVE_ROOT_PASSWORD", randomHex(32));
7016
+ envContent = setEnvIfEmpty(envContent, "INTERNAL_API_SHARED_TOKEN", randomHex(32));
7017
+ envContent = setEnvIfEmpty(envContent, "SELF_HOST_FIRST_INVITE_CODE", generateInviteCode());
7018
+ envContent = setEnvVar(envContent, "OPENMATES_IMAGE_TAG", imageTag);
7019
+ envContent = setEnvVar(envContent, "OPENMATES_IMAGE_REGISTRY", DEFAULT_IMAGE_REGISTRY);
7020
+ envContent = setEnvVar(envContent, "GIT_WORK_DIR", installPath);
7021
+ writeFileSync3(envPath, envContent.endsWith("\n") ? envContent : `${envContent}
7022
+ `);
7023
+ }
7024
+ function defaultCloneBranchForVersion(version) {
7025
+ return /-(alpha|beta|rc)(\.|\d|$)/.test(version) ? DEV_BRANCH : null;
7026
+ }
6077
7027
  function hasLlmCredentials(envPath) {
6078
7028
  if (!existsSync5(envPath)) return false;
6079
- const content = readFileSync6(envPath, "utf-8");
7029
+ const content = readFileSync5(envPath, "utf-8");
6080
7030
  for (const line of content.split("\n")) {
6081
7031
  const trimmed = line.trim();
6082
7032
  if (trimmed.startsWith("#") || !trimmed) continue;
@@ -6084,22 +7034,23 @@ function hasLlmCredentials(envPath) {
6084
7034
  if (eqIdx === -1) continue;
6085
7035
  const key = trimmed.slice(0, eqIdx);
6086
7036
  const value = trimmed.slice(eqIdx + 1).trim();
6087
- if (/^SECRET__\w+__API_KEY$/.test(key) && value && value !== "IMPORTED_TO_VAULT") {
7037
+ if (LLM_PROVIDER_ENV_KEYS.has(key) && value && value !== "IMPORTED_TO_VAULT") {
6088
7038
  return true;
6089
7039
  }
6090
7040
  }
6091
7041
  return false;
6092
7042
  }
6093
- function requireLlmCredentials(installPath) {
7043
+ function warnIfMissingLlmCredentials(installPath) {
6094
7044
  const envPath = join3(installPath, ".env");
6095
7045
  if (!existsSync5(envPath)) {
6096
- throw new Error(
7046
+ console.error(
6097
7047
  "No .env file found. Run 'openmates server install' first, or create .env from .env.example."
6098
7048
  );
7049
+ return;
6099
7050
  }
6100
7051
  if (!hasLlmCredentials(envPath)) {
6101
- throw new Error(
6102
- "No LLM provider API key found in .env.\nAt least one AI provider API key is required to start the server.\n\nAdd at least one of these to your .env file:\n SECRET__OPENAI__API_KEY=sk-...\n SECRET__ANTHROPIC__API_KEY=sk-ant-...\n SECRET__GOOGLE__API_KEY=...\n\nThen run 'openmates server start' again."
7052
+ console.error(
7053
+ "No LLM provider API key found in .env.\nOpenMates will start, but AI chat/model processing will stay unavailable until you add one.\n\nAdd at least one of these to your .env file:\n SECRET__OPENAI__API_KEY=sk-...\n SECRET__ANTHROPIC__API_KEY=sk-ant-...\n SECRET__GOOGLE_AI_STUDIO__API_KEY=...\n\nAfter updating .env, run 'openmates server restart'."
6103
7054
  );
6104
7055
  }
6105
7056
  }
@@ -6118,9 +7069,10 @@ function printJson(data) {
6118
7069
  async function serverStatus(flags) {
6119
7070
  requireDocker();
6120
7071
  const installPath = resolveServerPath(flags);
6121
- const config = loadServerConfig();
7072
+ ensureGitWorkDirEnv(installPath);
7073
+ const config = loadConfigForInstallPath(installPath);
6122
7074
  const withOverrides = config?.composeProfile === "full";
6123
- const args = [...composeArgs(installPath, withOverrides), "ps"];
7075
+ const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "ps"];
6124
7076
  if (flags.json === true) {
6125
7077
  args.push("--format", "json");
6126
7078
  }
@@ -6130,20 +7082,30 @@ async function serverStatus(flags) {
6130
7082
  async function serverStart(flags) {
6131
7083
  requireDocker();
6132
7084
  const installPath = resolveServerPath(flags);
6133
- requireLlmCredentials(installPath);
7085
+ ensureGitWorkDirEnv(installPath);
7086
+ warnIfMissingLlmCredentials(installPath);
6134
7087
  const withOverrides = flags["with-overrides"] === true;
6135
- const args = [...composeArgs(installPath, withOverrides), "up", "-d"];
6136
- const config = loadServerConfig();
7088
+ const config = loadConfigForInstallPath(installPath);
7089
+ const installMode = getInstallMode(installPath, config);
7090
+ const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
7091
+ const args = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
6137
7092
  if (config && withOverrides && config.composeProfile !== "full") {
6138
7093
  saveServerConfig({ ...config, composeProfile: "full" });
6139
7094
  }
6140
7095
  console.error("Starting OpenMates server...");
6141
- const code = await runInteractive("docker", args, installPath);
7096
+ let code = 0;
7097
+ if (installMode === "image" && shouldPullImages()) {
7098
+ code = await runInteractive("docker", pullArgs, installPath);
7099
+ if (code !== 0) process.exit(code);
7100
+ }
7101
+ code = await runInteractive("docker", args, installPath);
6142
7102
  if (code !== 0) process.exit(code);
6143
7103
  if (flags.json === true) {
6144
7104
  printJson({ command: "start", status: "success", path: installPath });
6145
7105
  } else {
6146
- console.log("\nServer started. API available at http://localhost:8000");
7106
+ console.log("\nServer started.");
7107
+ console.log("Web app: http://localhost:5173");
7108
+ console.log("API: http://localhost:8000");
6147
7109
  if (withOverrides) {
6148
7110
  console.log("Directus CMS: http://localhost:8055");
6149
7111
  console.log("Grafana: http://localhost:3000");
@@ -6153,9 +7115,10 @@ async function serverStart(flags) {
6153
7115
  async function serverStop(flags) {
6154
7116
  requireDocker();
6155
7117
  const installPath = resolveServerPath(flags);
6156
- const config = loadServerConfig();
7118
+ ensureGitWorkDirEnv(installPath);
7119
+ const config = loadConfigForInstallPath(installPath);
6157
7120
  const withOverrides = config?.composeProfile === "full";
6158
- const args = [...composeArgs(installPath, withOverrides), "down"];
7121
+ const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down"];
6159
7122
  console.error("Stopping OpenMates server...");
6160
7123
  const code = await runInteractive("docker", args, installPath);
6161
7124
  if (code !== 0) process.exit(code);
@@ -6168,26 +7131,33 @@ async function serverStop(flags) {
6168
7131
  async function serverRestart(flags) {
6169
7132
  requireDocker();
6170
7133
  const installPath = resolveServerPath(flags);
6171
- const config = loadServerConfig();
7134
+ ensureGitWorkDirEnv(installPath);
7135
+ const config = loadConfigForInstallPath(installPath);
6172
7136
  const withOverrides = config?.composeProfile === "full";
7137
+ const installMode = getInstallMode(installPath, config);
6173
7138
  if (flags.rebuild === true) {
7139
+ if (installMode === "image") {
7140
+ throw new Error(
7141
+ "Image-mode installs use prebuilt images and cannot rebuild locally. Run 'openmates server update' to pull newer images, or reinstall with --from-source to build from source."
7142
+ );
7143
+ }
6174
7144
  console.error("Rebuilding OpenMates server (this may take a few minutes)...");
6175
- const downArgs = [...composeArgs(installPath, withOverrides), "down"];
7145
+ const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
6176
7146
  let code = await runInteractive("docker", downArgs, installPath);
6177
7147
  if (code !== 0) process.exit(code);
6178
7148
  try {
6179
7149
  exec("docker volume rm openmates-cache-data", installPath);
6180
7150
  } catch {
6181
7151
  }
6182
- const buildArgs = [...composeArgs(installPath, withOverrides), "build"];
7152
+ const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
6183
7153
  code = await runInteractive("docker", buildArgs, installPath);
6184
7154
  if (code !== 0) process.exit(code);
6185
- const upArgs = [...composeArgs(installPath, withOverrides), "up", "-d"];
7155
+ const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
6186
7156
  code = await runInteractive("docker", upArgs, installPath);
6187
7157
  if (code !== 0) process.exit(code);
6188
7158
  } else {
6189
7159
  console.error("Restarting OpenMates server...");
6190
- const args = [...composeArgs(installPath, withOverrides), "restart"];
7160
+ const args = [...composeArgs(installPath, withOverrides, installMode), "restart"];
6191
7161
  const code = await runInteractive("docker", args, installPath);
6192
7162
  if (code !== 0) process.exit(code);
6193
7163
  }
@@ -6200,9 +7170,10 @@ async function serverRestart(flags) {
6200
7170
  async function serverLogs(flags) {
6201
7171
  requireDocker();
6202
7172
  const installPath = resolveServerPath(flags);
6203
- const config = loadServerConfig();
7173
+ ensureGitWorkDirEnv(installPath);
7174
+ const config = loadConfigForInstallPath(installPath);
6204
7175
  const withOverrides = config?.composeProfile === "full";
6205
- const args = [...composeArgs(installPath, withOverrides), "logs"];
7176
+ const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "logs"];
6206
7177
  if (flags.follow === true || flags.f === true) {
6207
7178
  args.push("--follow");
6208
7179
  }
@@ -6220,17 +7191,70 @@ async function serverLogs(flags) {
6220
7191
  if (code !== 0) process.exit(code);
6221
7192
  }
6222
7193
  async function serverInstall(flags) {
6223
- requireGit();
6224
7194
  const installPath = typeof flags.path === "string" ? resolve3(flags.path) : DEFAULT_INSTALL_PATH;
6225
- if (existsSync5(join3(installPath, "setup.sh"))) {
7195
+ const sourcePath = typeof flags["source-path"] === "string" ? resolve3(flags["source-path"]) : null;
7196
+ const fromSource = flags["from-source"] === true || sourcePath !== null;
7197
+ if (existsSync5(join3(installPath, SOURCE_COMPOSE_FILE)) || existsSync5(join3(installPath, IMAGE_COMPOSE_FILE))) {
6226
7198
  console.error(`OpenMates already exists at ${installPath}.`);
6227
7199
  console.error("Use 'openmates server update' to update, or choose a different --path.");
6228
7200
  process.exit(1);
6229
7201
  }
6230
- console.error(`Cloning OpenMates to ${installPath}...`);
7202
+ if (!fromSource) {
7203
+ requireDocker();
7204
+ mkdirSync3(installPath, { recursive: true });
7205
+ if (typeof flags["env-path"] === "string") {
7206
+ const envSource = resolve3(flags["env-path"]);
7207
+ if (!existsSync5(envSource)) {
7208
+ throw new Error(`Env file not found: ${envSource}`);
7209
+ }
7210
+ copyFileSync(envSource, join3(installPath, ".env"));
7211
+ console.error(`Copied ${envSource} to ${installPath}/.env`);
7212
+ }
7213
+ const imageTag = typeof flags["image-tag"] === "string" ? flags["image-tag"] : getDefaultImageTag();
7214
+ console.error(`Preparing OpenMates image-mode install at ${installPath}...`);
7215
+ await writeImageModeRuntimeFiles(installPath, imageTag);
7216
+ try {
7217
+ exec("docker network create openmates", installPath);
7218
+ } catch {
7219
+ }
7220
+ saveServerConfig({
7221
+ installPath,
7222
+ installedAt: Date.now(),
7223
+ composeProfile: "core",
7224
+ installMode: "image",
7225
+ imageTag
7226
+ });
7227
+ if (flags.json === true) {
7228
+ printJson({ command: "install", status: "success", path: installPath, mode: "image", imageTag });
7229
+ } else {
7230
+ const firstInvite = getEnvVar(readFileSync5(join3(installPath, ".env"), "utf-8"), "SELF_HOST_FIRST_INVITE_CODE");
7231
+ console.log(`
7232
+ OpenMates installed at ${installPath}`);
7233
+ console.log(`Mode: image (${DEFAULT_IMAGE_REGISTRY}, tag ${imageTag})`);
7234
+ console.log("\nNext steps:");
7235
+ console.log(" 1. Run: openmates server start");
7236
+ console.log(" 2. Open http://localhost:5173");
7237
+ if (firstInvite) console.log(` 3. Sign up with invite code: ${firstInvite}`);
7238
+ console.log(" 4. After signup, make yourself admin: openmates server make-admin your@email.com");
7239
+ console.log("\nOptional: edit .env first to add LLM provider API keys. Source builds are available with --from-source.");
7240
+ }
7241
+ return;
7242
+ }
7243
+ requireGit();
7244
+ const cloneSource = sourcePath ?? REPO_URL;
7245
+ const cloneBranch = sourcePath ? null : defaultCloneBranchForVersion(getPackageVersion());
7246
+ const cloneArgs = ["clone"];
7247
+ if (cloneBranch) {
7248
+ cloneArgs.push("--branch", cloneBranch);
7249
+ }
7250
+ cloneArgs.push(cloneSource, installPath);
7251
+ console.error(`Cloning OpenMates from ${cloneSource} to ${installPath}...`);
7252
+ if (cloneBranch) {
7253
+ console.error(`Using ${cloneBranch} branch for this prerelease CLI.`);
7254
+ }
6231
7255
  const cloneCode = await runInteractive(
6232
7256
  "git",
6233
- ["clone", REPO_URL, installPath],
7257
+ cloneArgs,
6234
7258
  process.cwd()
6235
7259
  );
6236
7260
  if (cloneCode !== 0) {
@@ -6257,26 +7281,45 @@ async function serverInstall(flags) {
6257
7281
  saveServerConfig({
6258
7282
  installPath,
6259
7283
  installedAt: Date.now(),
6260
- composeProfile: "core"
7284
+ composeProfile: "core",
7285
+ installMode: "source"
6261
7286
  });
6262
7287
  if (flags.json === true) {
6263
- printJson({ command: "install", status: "success", path: installPath });
7288
+ printJson({ command: "install", status: "success", path: installPath, mode: "source" });
6264
7289
  } else {
6265
7290
  console.log(`
6266
7291
  OpenMates installed at ${installPath}`);
6267
7292
  console.log("\nNext steps:");
6268
- console.log(" 1. Edit .env to add your LLM provider API key(s)");
6269
- console.log(" 2. Run: openmates server start");
6270
- console.log(" 3. Find your invite code: openmates server logs --container cms-setup --tail 50");
6271
- console.log(" 4. Open http://localhost:5173 and sign up");
6272
- console.log(" 5. Make yourself admin: openmates server make-admin your@email.com");
7293
+ console.log(" 1. Run: openmates server start");
7294
+ console.log(" 2. Open http://localhost:5173");
7295
+ console.log(" 3. After signup, make yourself admin: openmates server make-admin your@email.com");
7296
+ console.log("\nOptional: edit .env first to add LLM provider API keys. Without keys, the web app and backend still start, but AI model processing is unavailable.");
6273
7297
  }
6274
7298
  }
6275
7299
  async function serverUpdate(flags) {
6276
- requireGit();
6277
7300
  requireDocker();
6278
7301
  const installPath = resolveServerPath(flags);
7302
+ ensureGitWorkDirEnv(installPath);
6279
7303
  console.error("Updating OpenMates...");
7304
+ const config = loadConfigForInstallPath(installPath);
7305
+ const withOverrides = config?.composeProfile === "full";
7306
+ const installMode = getInstallMode(installPath, config);
7307
+ if (installMode === "image") {
7308
+ const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
7309
+ console.error("Pulling prebuilt images...");
7310
+ let code2 = await runInteractive("docker", pullArgs, installPath);
7311
+ if (code2 !== 0) process.exit(code2);
7312
+ const upArgs2 = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
7313
+ code2 = await runInteractive("docker", upArgs2, installPath);
7314
+ if (code2 !== 0) process.exit(code2);
7315
+ if (flags.json === true) {
7316
+ printJson({ command: "update", status: "success", path: installPath, mode: "image" });
7317
+ } else {
7318
+ console.log("Server images pulled and containers restarted.");
7319
+ }
7320
+ return;
7321
+ }
7322
+ requireGit();
6280
7323
  if (flags.force === true) {
6281
7324
  console.error("Stashing local changes...");
6282
7325
  try {
@@ -6304,13 +7347,11 @@ async function serverUpdate(flags) {
6304
7347
  } catch {
6305
7348
  }
6306
7349
  }
6307
- const config = loadServerConfig();
6308
- const withOverrides = config?.composeProfile === "full";
6309
- const buildArgs = [...composeArgs(installPath, withOverrides), "build"];
7350
+ const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
6310
7351
  console.error("Rebuilding containers...");
6311
7352
  let code = await runInteractive("docker", buildArgs, installPath);
6312
7353
  if (code !== 0) process.exit(code);
6313
- const upArgs = [...composeArgs(installPath, withOverrides), "up", "-d"];
7354
+ const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
6314
7355
  code = await runInteractive("docker", upArgs, installPath);
6315
7356
  if (code !== 0) process.exit(code);
6316
7357
  if (flags.json === true) {
@@ -6322,8 +7363,10 @@ async function serverUpdate(flags) {
6322
7363
  async function serverReset(flags) {
6323
7364
  requireDocker();
6324
7365
  const installPath = resolveServerPath(flags);
6325
- const config = loadServerConfig();
7366
+ ensureGitWorkDirEnv(installPath);
7367
+ const config = loadConfigForInstallPath(installPath);
6326
7368
  const withOverrides = config?.composeProfile === "full";
7369
+ const installMode = getInstallMode(installPath, config);
6327
7370
  const userDataOnly = flags["delete-user-data-only"] === true;
6328
7371
  if (userDataOnly) {
6329
7372
  console.error("\nWARNING: This will delete all user data (database and cache).");
@@ -6343,24 +7386,26 @@ async function serverReset(flags) {
6343
7386
  }
6344
7387
  console.error("Resetting server...");
6345
7388
  if (userDataOnly) {
6346
- const downArgs = [...composeArgs(installPath, withOverrides), "down"];
7389
+ const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
6347
7390
  let code = await runInteractive("docker", downArgs, installPath);
6348
7391
  if (code !== 0) process.exit(code);
6349
- for (const vol of ["openmates-cache-data", "openmates-cms-database-data"]) {
7392
+ for (const vol of ["openmates-cache-data", "openmates-postgres-data", "openmates-cms-database-data"]) {
6350
7393
  try {
6351
7394
  exec(`docker volume rm ${vol}`, installPath);
6352
7395
  console.error(` Removed volume: ${vol}`);
6353
7396
  } catch {
6354
7397
  }
6355
7398
  }
6356
- const buildArgs = [...composeArgs(installPath, withOverrides), "build"];
6357
- code = await runInteractive("docker", buildArgs, installPath);
6358
- if (code !== 0) process.exit(code);
6359
- const upArgs = [...composeArgs(installPath, withOverrides), "up", "-d"];
7399
+ if (installMode === "source") {
7400
+ const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
7401
+ code = await runInteractive("docker", buildArgs, installPath);
7402
+ if (code !== 0) process.exit(code);
7403
+ }
7404
+ const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
6360
7405
  code = await runInteractive("docker", upArgs, installPath);
6361
7406
  if (code !== 0) process.exit(code);
6362
7407
  } else {
6363
- const args = [...composeArgs(installPath, withOverrides), "down", "-v"];
7408
+ const args = [...composeArgs(installPath, withOverrides, installMode), "down", "-v"];
6364
7409
  const code = await runInteractive("docker", args, installPath);
6365
7410
  if (code !== 0) process.exit(code);
6366
7411
  }
@@ -6402,7 +7447,8 @@ async function serverMakeAdmin(rest, flags) {
6402
7447
  async function serverUninstall(flags) {
6403
7448
  requireDocker();
6404
7449
  const installPath = resolveServerPath(flags);
6405
- const config = loadServerConfig();
7450
+ ensureGitWorkDirEnv(installPath);
7451
+ const config = loadConfigForInstallPath(installPath);
6406
7452
  const withOverrides = config?.composeProfile === "full";
6407
7453
  const keepData = flags["keep-data"] === true;
6408
7454
  console.error("\nWARNING: This will completely uninstall OpenMates:");
@@ -6424,7 +7470,7 @@ async function serverUninstall(flags) {
6424
7470
  }
6425
7471
  }
6426
7472
  console.error("Uninstalling OpenMates...");
6427
- const downArgs = [...composeArgs(installPath, withOverrides), "down", "--rmi", "local"];
7473
+ const downArgs = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down", "--rmi", "local"];
6428
7474
  if (!keepData) {
6429
7475
  downArgs.push("-v");
6430
7476
  }
@@ -6457,13 +7503,13 @@ OpenMates Server Management
6457
7503
  Usage: openmates server <command> [options]
6458
7504
 
6459
7505
  Commands:
6460
- install Install OpenMates server (clone repo + run setup)
7506
+ install Install OpenMates server (prebuilt GHCR images by default)
6461
7507
  start Start the server
6462
7508
  stop Stop the server
6463
7509
  restart Restart the server
6464
7510
  status Show server status (container health)
6465
7511
  logs Display server logs
6466
- update Update to latest version (git pull + rebuild)
7512
+ update Update to latest version (pull images, or git pull + rebuild for source installs)
6467
7513
  make-admin Grant admin privileges to a user
6468
7514
  reset Reset server data (requires confirmation)
6469
7515
  uninstall Completely remove OpenMates (requires confirmation)
@@ -6477,6 +7523,9 @@ Command Options:
6477
7523
  install:
6478
7524
  --path <dir> Install directory (default: ~/openmates)
6479
7525
  --env-path <file> Copy a pre-existing .env file during install
7526
+ --image-tag <tag> Prebuilt image tag (default: CLI version tag)
7527
+ --from-source Clone/build from source instead of using prebuilt GHCR images
7528
+ --source-path <dir> Clone from a local checkout instead of GitHub (implies --from-source)
6480
7529
 
6481
7530
  start:
6482
7531
  --with-overrides Include admin UIs (Directus CMS, Grafana)
@@ -6710,7 +7759,8 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
6710
7759
  message,
6711
7760
  chatId: void 0,
6712
7761
  incognito: false,
6713
- json: flags.json === true
7762
+ json: flags.json === true,
7763
+ autoApproveSubChats: flags["auto-approve"] === true
6714
7764
  },
6715
7765
  redactor
6716
7766
  );
@@ -6781,7 +7831,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
6781
7831
  message,
6782
7832
  chatId,
6783
7833
  incognito: flags.incognito === true,
6784
- json: flags.json === true
7834
+ json: flags.json === true,
7835
+ autoApproveSubChats: flags["auto-approve"] === true
6785
7836
  },
6786
7837
  redactor
6787
7838
  );
@@ -6799,7 +7850,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
6799
7850
  {
6800
7851
  message,
6801
7852
  incognito: true,
6802
- json: flags.json === true
7853
+ json: flags.json === true,
7854
+ autoApproveSubChats: flags["auto-approve"] === true
6803
7855
  },
6804
7856
  redactor
6805
7857
  );
@@ -6807,17 +7859,11 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
6807
7859
  return;
6808
7860
  }
6809
7861
  if (subcommand === "incognito-history") {
6810
- const history = client.getIncognitoHistory();
6811
- if (flags.json === true) {
6812
- printJson2(history);
6813
- } else {
6814
- printIncognitoHistory(history);
6815
- }
7862
+ printIncognitoNoHistoryNotice(flags.json === true);
6816
7863
  return;
6817
7864
  }
6818
7865
  if (subcommand === "incognito-clear") {
6819
- client.clearIncognitoHistory();
6820
- console.log("Incognito history cleared.");
7866
+ printIncognitoNoHistoryNotice(flags.json === true);
6821
7867
  return;
6822
7868
  }
6823
7869
  if (subcommand === "show") {
@@ -7594,216 +8640,780 @@ function levenshtein(a, b) {
7594
8640
  dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
7595
8641
  }
7596
8642
  }
7597
- return dp[m][n];
7598
- }
7599
- function findClosestMatch(input, candidates) {
7600
- let best = null;
7601
- let bestDist = 4;
7602
- for (const candidate of candidates) {
7603
- const dist = levenshtein(input.toLowerCase(), candidate.toLowerCase());
7604
- if (dist < bestDist) {
7605
- bestDist = dist;
7606
- best = candidate;
7607
- }
8643
+ return dp[m][n];
8644
+ }
8645
+ function findClosestMatch(input, candidates) {
8646
+ let best = null;
8647
+ let bestDist = 4;
8648
+ for (const candidate of candidates) {
8649
+ const dist = levenshtein(input.toLowerCase(), candidate.toLowerCase());
8650
+ if (dist < bestDist) {
8651
+ bestDist = dist;
8652
+ best = candidate;
8653
+ }
8654
+ }
8655
+ return best;
8656
+ }
8657
+ async function handleEmbeds(client, subcommand, rest, flags) {
8658
+ if (!subcommand || subcommand === "help" || flags.help === true) {
8659
+ printEmbedsHelp();
8660
+ return;
8661
+ }
8662
+ if (subcommand === "show") {
8663
+ const embedId = rest[0];
8664
+ if (!embedId) {
8665
+ console.error("Missing embed ID.\n");
8666
+ printEmbedsHelp();
8667
+ process.exit(1);
8668
+ }
8669
+ const embed = await client.getEmbed(embedId);
8670
+ if (flags.json === true) {
8671
+ printJson2(embed);
8672
+ } else {
8673
+ await renderEmbedFullscreen(embed, client);
8674
+ }
8675
+ return;
8676
+ }
8677
+ if (subcommand === "share") {
8678
+ const id = rest[0];
8679
+ if (!id) {
8680
+ console.error(
8681
+ "Missing embed ID. Usage: openmates embeds share <embed-id>"
8682
+ );
8683
+ process.exit(1);
8684
+ }
8685
+ const durationSeconds = typeof flags.expires === "string" ? parseInt(flags.expires, 10) : 0;
8686
+ const password = typeof flags.password === "string" ? flags.password : void 0;
8687
+ if (password && password.length > 10) {
8688
+ console.error("Password must be at most 10 characters.");
8689
+ process.exit(1);
8690
+ }
8691
+ try {
8692
+ const url = await client.createEmbedShareLink(
8693
+ id,
8694
+ durationSeconds,
8695
+ password
8696
+ );
8697
+ if (flags.json === true) {
8698
+ printJson2({
8699
+ url,
8700
+ embed_id: id,
8701
+ expires: durationSeconds,
8702
+ password_protected: !!password
8703
+ });
8704
+ } else {
8705
+ process.stdout.write(`
8706
+ \x1B[1mEmbed share link\x1B[0m
8707
+ `);
8708
+ process.stdout.write(`${url}
8709
+
8710
+ `);
8711
+ if (durationSeconds > 0) {
8712
+ process.stdout.write(
8713
+ `\x1B[2mExpires in ${humanizeDuration(durationSeconds)}\x1B[0m
8714
+ `
8715
+ );
8716
+ }
8717
+ if (password) {
8718
+ process.stdout.write(`\x1B[2mPassword protected\x1B[0m
8719
+ `);
8720
+ }
8721
+ }
8722
+ } catch (err) {
8723
+ const msg = err instanceof Error ? err.message : String(err);
8724
+ console.error(`Share link error: ${msg}`);
8725
+ process.exit(1);
8726
+ }
8727
+ return;
8728
+ }
8729
+ console.error(`Unknown embeds subcommand '${subcommand}'.
8730
+ `);
8731
+ printEmbedsHelp();
8732
+ process.exit(1);
8733
+ }
8734
+ var SETTINGS_EXECUTABLE_COMMANDS = [
8735
+ { path: ["account", "info"], description: "Show account info", examples: ["openmates settings account info --json"] },
8736
+ { path: ["account", "timezone", "set"], description: "Set account timezone", examples: ["openmates settings account timezone set Europe/Berlin"] },
8737
+ { path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
8738
+ { path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
8739
+ { path: ["account", "import-chat"], description: "Import a CLI chat export file", examples: ["openmates settings account import-chat ./chat.yml", "openmates settings account import-chat ./payload.json"] },
8740
+ { path: ["account", "username", "set"], description: "Change account username", examples: ["openmates settings account username set alice_123"] },
8741
+ { path: ["account", "profile-picture", "set"], description: "Upload a profile picture", examples: ["openmates settings account profile-picture set ./avatar.jpg"] },
8742
+ { path: ["account", "chats", "stats"], description: "Show chat statistics", examples: ["openmates settings account chats stats"] },
8743
+ { path: ["account", "delete", "preview"], description: "Preview account deletion impact", examples: ["openmates settings account delete preview"] },
8744
+ { path: ["account", "storage", "overview"], description: "Show storage overview", examples: ["openmates settings account storage overview"] },
8745
+ { path: ["account", "storage", "files"], description: "List stored files", examples: ["openmates settings account storage files --category images"] },
8746
+ { path: ["account", "storage", "delete"], description: "Delete one stored file by file ID", examples: ["openmates settings account storage delete <file-id> --yes"] },
8747
+ { path: ["interface", "language", "set"], description: "Set interface language", examples: ["openmates settings interface language set en"] },
8748
+ { path: ["interface", "dark-mode", "set"], description: "Set dark mode on or off", examples: ["openmates settings interface dark-mode set on"] },
8749
+ { path: ["interface", "font", "set"], description: "Set interface font", examples: ["openmates settings interface font set lexend"] },
8750
+ { path: ["ai", "models", "set-defaults"], description: "Set default AI models", examples: ["openmates settings ai models set-defaults --simple mistral/mistral-small-2506", "openmates settings ai models set-defaults --simple auto"] },
8751
+ { path: ["privacy", "auto-delete", "chats", "set"], description: "Set chat auto-deletion period", examples: ["openmates settings privacy auto-delete chats set 90d"] },
8752
+ { path: ["privacy", "debug-logs", "share"], description: "Create a debug log sharing session", examples: ["openmates settings privacy debug-logs share --duration 1h --confirm"] },
8753
+ { path: ["billing", "overview"], description: "Show billing overview", examples: ["openmates settings billing overview"] },
8754
+ { path: ["billing", "usage"], description: "Show usage history", examples: ["openmates settings billing usage --json"] },
8755
+ { path: ["billing", "usage", "summaries"], description: "Show usage summaries", examples: ["openmates settings billing usage summaries"] },
8756
+ { path: ["billing", "usage", "daily"], description: "Show daily usage overview", examples: ["openmates settings billing usage daily"] },
8757
+ { path: ["billing", "usage", "export"], description: "Export usage data", examples: ["openmates settings billing usage export --json"] },
8758
+ { path: ["billing", "invoices", "list"], description: "List invoices", examples: ["openmates settings billing invoices list --json"] },
8759
+ { path: ["billing", "invoices", "download"], description: "Download an invoice PDF", examples: ["openmates settings billing invoices download <invoice-id> --output ./invoices"] },
8760
+ { path: ["billing", "invoices", "credit-note"], description: "Download a credit note PDF", examples: ["openmates settings billing invoices credit-note <invoice-id> --output ./invoices"] },
8761
+ { path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --yes"] },
8762
+ { path: ["billing", "gift-card", "redeem"], description: "Redeem a gift card", examples: ["openmates settings billing gift-card redeem ABCD-1234"] },
8763
+ { path: ["billing", "gift-card", "list"], description: "List redeemed gift cards", examples: ["openmates settings billing gift-card list"] },
8764
+ { path: ["billing", "auto-topup", "low-balance", "set"], description: "Configure low-balance auto top-up", examples: ["openmates settings billing auto-topup low-balance set --enabled true --amount 1000 --currency eur --email you@example.com"] },
8765
+ { path: ["notifications", "status"], description: "Show notification settings", examples: ["openmates settings notifications status --json"] },
8766
+ { path: ["notifications", "list"], description: "List recent notification events", examples: ["openmates settings notifications list --limit 20 --json"] },
8767
+ { path: ["notifications", "stream"], description: "Stream notification events with SSE", examples: ["openmates settings notifications stream", "openmates settings notifications stream --count 1 --json"] },
8768
+ { path: ["notifications", "email", "set"], description: "Configure email notifications", examples: ["openmates settings notifications email set --enabled true --email you@example.com --ai-responses true --backup-reminder true --webhook-chats true"] },
8769
+ { path: ["notifications", "backup", "set"], description: "Configure backup reminder emails", examples: ["openmates settings notifications backup set --enabled true --interval 30 --email you@example.com"] },
8770
+ { path: ["reminders", "list"], description: "List active reminders", examples: ["openmates settings reminders list"] },
8771
+ { path: ["reminders", "update"], description: "Update a reminder", examples: ["openmates settings reminders update <id> --enabled false"] },
8772
+ { path: ["reminders", "delete"], description: "Delete a reminder", examples: ["openmates settings reminders delete <id> --yes"] },
8773
+ { path: ["developers", "api-keys", "list"], description: "List API keys", examples: ["openmates settings developers api-keys list"] },
8774
+ { path: ["developers", "api-keys", "revoke"], description: "Revoke an API key", examples: ["openmates settings developers api-keys revoke <key-id> --yes"] },
8775
+ { path: ["report-issue", "create"], description: "Report an issue", examples: ['openmates settings report-issue create --title "Bug" --body "What happened"'] },
8776
+ { path: ["report-issue", "status"], description: "Show issue status", examples: ["openmates settings report-issue status <issue-id>"] },
8777
+ { path: ["mates", "list"], description: "List available mates", examples: ["openmates settings mates list"] },
8778
+ { path: ["mates", "info"], description: "Show mate details", examples: ["openmates settings mates info software_development"] },
8779
+ { path: ["mates", "consent"], description: "Record mate settings consent", examples: ["openmates settings mates consent --yes"] },
8780
+ { path: ["newsletter", "categories"], description: "Show newsletter category preferences", examples: ["openmates settings newsletter categories"] },
8781
+ { path: ["newsletter", "categories", "set"], description: "Set newsletter category preferences", examples: ["openmates settings newsletter categories set --updates true --tips true --daily false"] },
8782
+ { path: ["newsletter", "subscribe"], description: "Subscribe an email to the newsletter", examples: ["openmates settings newsletter subscribe you@example.com --language en"] },
8783
+ { path: ["newsletter", "confirm"], description: "Confirm newsletter subscription token", examples: ["openmates settings newsletter confirm <token>"] },
8784
+ { path: ["newsletter", "unsubscribe"], description: "Unsubscribe with newsletter token", examples: ["openmates settings newsletter unsubscribe <token>"] },
8785
+ { path: ["memories"], description: "Manage encrypted memories", examples: ["openmates settings memories list", `openmates settings memories create --app-id code --item-type projects --data '{"name":"OpenMates"}'`] }
8786
+ ];
8787
+ var SETTINGS_INFO_COMMANDS = [
8788
+ { path: ["account", "email"], description: "Email changes are web-only", webPath: "account/email", reason: "Email changes require a guided identity verification flow.", examples: ["openmates settings account email"] },
8789
+ { path: ["account", "delete"], description: "Account deletion is web-only", webPath: "account/delete", reason: "Account deletion requires browser-based reauthentication and explicit confirmation.", examples: ["openmates settings account delete"] },
8790
+ { path: ["security"], description: "Security settings are web-only", webPath: "account/security", reason: "Security settings require browser APIs or high-risk reauthentication.", examples: ["openmates settings security"] },
8791
+ { path: ["security", "passkeys"], description: "Passkeys are web-only", webPath: "account/security/passkeys", reason: "Passkeys require WebAuthn browser APIs.", examples: ["openmates settings security passkeys"] },
8792
+ { path: ["security", "password"], description: "Password changes are web-only", webPath: "account/security/password", reason: "The CLI never asks for account credentials.", examples: ["openmates settings security password"] },
8793
+ { path: ["security", "2fa"], description: "2FA setup and changes are web-only", webPath: "account/security/2fa", reason: "2FA setup requires a guided browser verification flow.", examples: ["openmates settings security 2fa"] },
8794
+ { path: ["security", "recovery-key"], description: "Recovery key settings are web-only", webPath: "account/security/recovery-key", reason: "Recovery keys are a high-risk account recovery surface.", examples: ["openmates settings security recovery-key"] },
8795
+ { path: ["security", "sessions"], description: "Session management is web-only", webPath: "account/security/sessions", reason: "The CLI is a paired restricted session; approval and revocation stay in the browser.", examples: ["openmates settings security sessions"] },
8796
+ { path: ["billing", "buy-credits"], description: "Credit purchase is web-only", webPath: "billing/buy-credits", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing buy-credits"] },
8797
+ { path: ["billing", "gift-card", "buy"], description: "Gift card purchase is web-only", webPath: "billing/gift-cards/buy", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing gift-card buy"] },
8798
+ { path: ["billing", "auto-topup", "monthly"], description: "Monthly auto top-up is web-only for now", webPath: "billing/auto-topup/monthly", reason: "Recurring payment setup needs a payment-flow audit before CLI support.", examples: ["openmates settings billing auto-topup monthly"] },
8799
+ { path: ["privacy", "personal-data"], description: "Personal data management is not CLI-ready yet", webPath: "privacy/hide-personal-data", reason: "The CLI needs a dedicated encrypted personal-data UX before exposing writes.", examples: ["openmates settings privacy personal-data"] },
8800
+ { path: ["shared", "tip"], description: "Tips are web-only", webPath: "shared/tip", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings shared tip"] },
8801
+ { path: ["developers", "api-keys", "create"], description: "API key creation is web-only", webPath: "developers/api-keys", reason: "API key secrets are shown once and need the browser approval flow.", examples: ["openmates settings developers api-keys create"] },
8802
+ { path: ["developers", "devices"], description: "Developer devices are web-only", webPath: "developers/devices", reason: "Device approvals and revocations are sensitive.", examples: ["openmates settings developers devices"] },
8803
+ { path: ["developers", "webhooks"], description: "Developer webhooks are not CLI-ready yet", webPath: "developers/webhooks", reason: "Webhook CRUD needs a backend/API audit before CLI support.", examples: ["openmates settings developers webhooks"] },
8804
+ { path: ["support"], description: "Support payments are web-only", webPath: "support", reason: "Payment flows must use the browser/payment provider UI.", examples: ["openmates settings support"] },
8805
+ { path: ["incognito", "info"], description: "Explain incognito mode", reason: "Incognito chats are sent without saving chat history. The CLI stores no incognito transcript.", examples: ['openmates chats incognito "Private question"'] },
8806
+ { path: ["server"], description: "Server admin settings are web/admin-only", webPath: "server", reason: "Use `openmates server --help` for self-hosted terminal server management.", examples: ["openmates server status"] }
8807
+ ];
8808
+ function matches(actual, expected) {
8809
+ return expected.every((part, index) => actual[index] === part);
8810
+ }
8811
+ function findSettingsInfoCommand(tokens) {
8812
+ const all = [...SETTINGS_INFO_COMMANDS, ...SETTINGS_EXECUTABLE_COMMANDS];
8813
+ return all.sort((a, b) => b.path.length - a.path.length).find((command) => matches(tokens, command.path)) ?? null;
8814
+ }
8815
+ async function printSettingsResult(resultPromise, flags) {
8816
+ const result = await resultPromise;
8817
+ flags.json === true ? printJson2(result) : printGenericObject(result);
8818
+ }
8819
+ async function printSettingsMutationResult(resultPromise, flags) {
8820
+ const result = await resultPromise;
8821
+ if (flags.json === true) {
8822
+ printJson2(result);
8823
+ return;
8824
+ }
8825
+ process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
8826
+ if (result && typeof result === "object") printGenericObject(result);
8827
+ }
8828
+ function addQueryParam(params, key, value) {
8829
+ if (typeof value === "string" && value.length > 0) params.set(key, value);
8830
+ }
8831
+ function parseOnOff(value, label) {
8832
+ if (value === "on" || value === "true" || value === "1") return true;
8833
+ if (value === "off" || value === "false" || value === "0") return false;
8834
+ throw new Error(`Invalid ${label} value '${value ?? ""}'. Use on/off or true/false.`);
8835
+ }
8836
+ function parseModelDefaultFlag(value, flag) {
8837
+ if (typeof value !== "string" || value.length === 0) {
8838
+ throw new Error(`Missing value for ${flag}. Use a provider/model-id or auto.`);
8839
+ }
8840
+ if (value === "auto" || value === "null") return null;
8841
+ return value;
8842
+ }
8843
+ function parseRequiredNumber(value, flag) {
8844
+ if (typeof value !== "string") throw new Error(`Missing ${flag}.`);
8845
+ const parsed = Number(value);
8846
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid ${flag}: ${value}`);
8847
+ return parsed;
8848
+ }
8849
+ function parseOptionalNumber(value, fallback, flag) {
8850
+ if (value === void 0) return fallback;
8851
+ return parseRequiredNumber(value, flag);
8852
+ }
8853
+ function parseOptionalBoolean(value, fallback, label) {
8854
+ if (value === void 0) return fallback;
8855
+ if (typeof value === "boolean") return value;
8856
+ return parseOnOff(value, label);
8857
+ }
8858
+ function parseDataOrFlags(flags, booleanFlags) {
8859
+ if (typeof flags.data === "string") return JSON.parse(flags.data);
8860
+ const body = {};
8861
+ for (const key of booleanFlags) {
8862
+ if (flags[key] !== void 0) body[key] = parseOnOff(String(flags[key]), key);
8863
+ }
8864
+ if (Object.keys(body).length === 0) throw new Error("Provide --data '<json>' or a supported flag.");
8865
+ return body;
8866
+ }
8867
+ function parseChatImportPayload(raw) {
8868
+ const trimmed = raw.trim();
8869
+ if (!trimmed) throw new Error("Import file is empty.");
8870
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
8871
+ const parsed = JSON.parse(trimmed);
8872
+ if (Array.isArray(parsed)) return { chats: parsed };
8873
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.chats)) {
8874
+ return parsed;
8875
+ }
8876
+ if (parsed && typeof parsed === "object") {
8877
+ const object = parsed;
8878
+ if (object.chat || object.messages) return { chats: [normalizeImportedChat(object)] };
8879
+ }
8880
+ throw new Error("JSON import must contain a chats array, a chat object, or messages.");
8881
+ }
8882
+ return { chats: [parseCliExportYaml(trimmed)] };
8883
+ }
8884
+ function normalizeImportedChat(source) {
8885
+ const chat = source.chat && typeof source.chat === "object" ? source.chat : source;
8886
+ const messages = Array.isArray(source.messages) ? source.messages : [];
8887
+ return {
8888
+ title: chat.title ?? null,
8889
+ draft: chat.draft ?? null,
8890
+ summary: chat.summary ?? null,
8891
+ messages: messages.map((message) => normalizeImportedMessage(message))
8892
+ };
8893
+ }
8894
+ function normalizeImportedMessage(message) {
8895
+ return {
8896
+ role: message.role,
8897
+ content: message.content,
8898
+ completed_at: message.completed_at ?? message.timestamp ?? null,
8899
+ assistant_category: message.assistant_category ?? null,
8900
+ thinking: message.thinking ?? null,
8901
+ has_thinking: message.has_thinking ?? null,
8902
+ thinking_tokens: message.thinking_tokens ?? null
8903
+ };
8904
+ }
8905
+ function parseCliExportYaml(raw) {
8906
+ const chat = {};
8907
+ const messages = [];
8908
+ const lines = raw.split("\n");
8909
+ let section = null;
8910
+ let currentMessage = null;
8911
+ const multilineState = { current: null };
8912
+ const setValue = (target, key, value, indent) => {
8913
+ if (value === "|") {
8914
+ target[key] = "";
8915
+ multilineState.current = { target, key, indent: indent + 2 };
8916
+ return;
8917
+ }
8918
+ target[key] = parseYamlScalar(value);
8919
+ };
8920
+ for (const line of lines) {
8921
+ const indent = line.match(/^ */)?.[0].length ?? 0;
8922
+ const trimmed = line.trimEnd();
8923
+ if (!trimmed.trim()) continue;
8924
+ if (multilineState.current) {
8925
+ if (indent >= multilineState.current.indent) {
8926
+ const previous = String(multilineState.current.target[multilineState.current.key] ?? "");
8927
+ const nextLine = line.slice(multilineState.current.indent);
8928
+ multilineState.current.target[multilineState.current.key] = previous ? `${previous}
8929
+ ${nextLine}` : nextLine;
8930
+ continue;
8931
+ }
8932
+ multilineState.current = null;
8933
+ }
8934
+ if (trimmed === "chat:") {
8935
+ section = "chat";
8936
+ currentMessage = null;
8937
+ continue;
8938
+ }
8939
+ if (trimmed === "messages:") {
8940
+ section = "messages";
8941
+ currentMessage = null;
8942
+ continue;
8943
+ }
8944
+ if (section === "messages" && trimmed.trim() === "-") {
8945
+ currentMessage = {};
8946
+ messages.push(currentMessage);
8947
+ continue;
8948
+ }
8949
+ const match = /^\s*([\w-]+):\s*(.*)$/.exec(line);
8950
+ if (!match) continue;
8951
+ const [, key, value] = match;
8952
+ if (section === "chat") setValue(chat, key, value, indent);
8953
+ if (section === "messages" && currentMessage) setValue(currentMessage, key, value, indent);
8954
+ }
8955
+ if (messages.length === 0) throw new Error("Import YAML did not contain any messages.");
8956
+ return normalizeImportedChat({ chat, messages });
8957
+ }
8958
+ function parseYamlScalar(value) {
8959
+ const trimmed = value.trim();
8960
+ if (trimmed === "null") return null;
8961
+ if (trimmed === "true") return true;
8962
+ if (trimmed === "false") return false;
8963
+ if (trimmed !== "" && Number.isFinite(Number(trimmed))) return Number(trimmed);
8964
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
8965
+ return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
8966
+ }
8967
+ return trimmed;
8968
+ }
8969
+ async function saveDownloadedDocument(document, output) {
8970
+ const { mkdir, writeFile } = await import("fs/promises");
8971
+ const { join: join4, basename: basename2, dirname } = await import("path");
8972
+ const target = typeof output === "string" ? output : ".";
8973
+ const filename = basename2(document.filename || "document.pdf");
8974
+ const filePath = target.endsWith(".pdf") ? target : join4(target, filename);
8975
+ await mkdir(dirname(filePath), { recursive: true });
8976
+ await writeFile(filePath, document.data);
8977
+ return filePath;
8978
+ }
8979
+ function printMates(json) {
8980
+ const mates = Object.entries(MATE_NAMES).map(([id, name]) => ({ id, name, mention: `@mate:${id}` }));
8981
+ if (json) {
8982
+ printJson2(mates);
8983
+ return;
8984
+ }
8985
+ header("Mates");
8986
+ for (const mate of mates) console.log(`${mate.id.padEnd(28)} ${mate.name.padEnd(10)} ${mate.mention}`);
8987
+ }
8988
+ function printMateInfo(mateId, json) {
8989
+ const name = MATE_NAMES[mateId];
8990
+ if (!name) throw new Error(`Unknown mate '${mateId}'. Run 'openmates settings mates list'.`);
8991
+ const data = { id: mateId, name, mention: `@mate:${mateId}` };
8992
+ if (json) {
8993
+ printJson2(data);
8994
+ return;
8995
+ }
8996
+ header(`${name} (${mateId})`);
8997
+ console.log(`Mention: ${data.mention}`);
8998
+ console.log(`Use: openmates chats send "${data.mention} <message>"`);
8999
+ }
9000
+ async function confirmOrExit(question) {
9001
+ const rl = await import("readline");
9002
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
9003
+ const answer = await new Promise((resolve4) => iface.question(question, resolve4));
9004
+ iface.close();
9005
+ if (answer.trim().toLowerCase() !== "y") {
9006
+ console.log("Aborted.");
9007
+ process.exit(0);
9008
+ }
9009
+ }
9010
+ function printSettingsInfoCommand(client, command, json) {
9011
+ const appUrl = deriveAppUrl(client.apiUrl);
9012
+ const webUrl = command.webPath ? `${appUrl}/#settings/${command.webPath}` : null;
9013
+ if (json) {
9014
+ printJson2({
9015
+ command: `openmates settings ${command.path.join(" ")}`,
9016
+ supported_in_cli: false,
9017
+ description: command.description,
9018
+ reason: command.reason ?? null,
9019
+ web_url: webUrl,
9020
+ examples: command.examples
9021
+ });
9022
+ return;
9023
+ }
9024
+ header(command.description);
9025
+ if (command.reason) console.log(command.reason);
9026
+ if (webUrl) console.log(`
9027
+ Open in web app:
9028
+ ${webUrl}`);
9029
+ if (command.examples.length > 0) {
9030
+ console.log("\nExamples:");
9031
+ for (const example of command.examples) console.log(` ${example}`);
9032
+ }
9033
+ }
9034
+ async function handleSettings(client, subcommand, rest, flags) {
9035
+ if (!subcommand || subcommand === "help") {
9036
+ printSettingsHelp(client, subcommand ? [] : void 0);
9037
+ return;
9038
+ }
9039
+ const tokens = [subcommand, ...rest].filter((token) => token !== "help");
9040
+ if (rest.includes("help") || Boolean(flags.help)) {
9041
+ printSettingsHelp(client, tokens);
9042
+ return;
9043
+ }
9044
+ if (["get", "post", "patch", "delete"].includes(subcommand)) {
9045
+ console.error(
9046
+ "Raw settings passthrough is no longer supported. Use a predefined settings command.\n"
9047
+ );
9048
+ printSettingsHelp(client);
9049
+ process.exit(1);
9050
+ }
9051
+ if (matches(tokens, ["account", "info"])) {
9052
+ const user = await client.whoAmI();
9053
+ flags.json === true ? printJson2(user) : printWhoAmI(user);
9054
+ return;
9055
+ }
9056
+ if (matches(tokens, ["account", "timezone", "set"])) {
9057
+ const timezone = rest[2];
9058
+ if (!timezone) throw new Error("Missing timezone. Example: openmates settings account timezone set Europe/Berlin");
9059
+ await printSettingsMutationResult(
9060
+ client.settingsPost("user/timezone", { timezone }),
9061
+ flags
9062
+ );
9063
+ return;
9064
+ }
9065
+ if (matches(tokens, ["account", "export", "manifest"])) {
9066
+ await printSettingsResult(client.settingsGet("export-account-manifest"), flags);
9067
+ return;
9068
+ }
9069
+ if (matches(tokens, ["account", "export", "data"])) {
9070
+ await printSettingsResult(client.settingsGet("export-account-data"), flags);
9071
+ return;
9072
+ }
9073
+ if (matches(tokens, ["account", "import-chat"])) {
9074
+ const file = rest[1];
9075
+ if (!file) throw new Error("Missing import file. Example: openmates settings account import-chat ./chat.yml");
9076
+ const { readFile } = await import("fs/promises");
9077
+ const content = await readFile(file, "utf-8");
9078
+ await printSettingsMutationResult(
9079
+ client.settingsPost("import-chat", parseChatImportPayload(content)),
9080
+ flags
9081
+ );
9082
+ return;
9083
+ }
9084
+ if (matches(tokens, ["account", "username", "set"])) {
9085
+ const username = rest[2];
9086
+ if (!username) throw new Error("Missing username. Example: openmates settings account username set alice_123");
9087
+ await printSettingsMutationResult(client.updateUsername(username), flags);
9088
+ return;
9089
+ }
9090
+ if (matches(tokens, ["account", "profile-picture", "set"])) {
9091
+ const file = rest[2];
9092
+ if (!file) throw new Error("Missing image file. Example: openmates settings account profile-picture set ./avatar.jpg");
9093
+ await printSettingsMutationResult(client.updateProfileImage(file), flags);
9094
+ return;
9095
+ }
9096
+ if (matches(tokens, ["account", "chats", "stats"])) {
9097
+ await printSettingsResult(client.settingsGet("chats"), flags);
9098
+ return;
9099
+ }
9100
+ if (matches(tokens, ["account", "delete", "preview"])) {
9101
+ await printSettingsResult(client.settingsGet("delete-account-preview"), flags);
9102
+ return;
9103
+ }
9104
+ if (matches(tokens, ["account", "storage", "overview"])) {
9105
+ await printSettingsResult(client.settingsGet("storage"), flags);
9106
+ return;
9107
+ }
9108
+ if (matches(tokens, ["account", "storage", "files"])) {
9109
+ const params = new URLSearchParams();
9110
+ addQueryParam(params, "category", flags.category ?? flags.type);
9111
+ const query = params.toString();
9112
+ await printSettingsResult(client.settingsGet(`storage/files${query ? `?${query}` : ""}`), flags);
9113
+ return;
9114
+ }
9115
+ if (matches(tokens, ["account", "storage", "delete"])) {
9116
+ const fileId = rest[3];
9117
+ const category = typeof flags.category === "string" ? flags.category : void 0;
9118
+ const scope = flags.all === true ? "all" : category ? "category" : "single";
9119
+ if (scope === "single" && !fileId) throw new Error("Missing file ID.");
9120
+ if (flags.yes !== true) await confirmOrExit(`Delete stored file data (${scope})? This cannot be undone. [y/N] `);
9121
+ await printSettingsMutationResult(
9122
+ client.settingsDelete("storage/files", { scope, file_id: fileId, category }),
9123
+ flags
9124
+ );
9125
+ return;
9126
+ }
9127
+ if (matches(tokens, ["interface", "language", "set"])) {
9128
+ const language = rest[2];
9129
+ if (!language) throw new Error("Missing language code. Example: openmates settings interface language set en");
9130
+ await printSettingsMutationResult(client.settingsPost("user/language", { language }), flags);
9131
+ return;
7608
9132
  }
7609
- return best;
7610
- }
7611
- async function handleEmbeds(client, subcommand, rest, flags) {
7612
- if (!subcommand || subcommand === "help" || flags.help === true) {
7613
- printEmbedsHelp();
9133
+ if (matches(tokens, ["interface", "dark-mode", "set"])) {
9134
+ const value = parseOnOff(rest[2], "dark mode");
9135
+ await printSettingsMutationResult(client.settingsPost("user/darkmode", { dark_mode: value }), flags);
7614
9136
  return;
7615
9137
  }
7616
- if (subcommand === "show") {
7617
- const embedId = rest[0];
7618
- if (!embedId) {
7619
- console.error("Missing embed ID.\n");
7620
- printEmbedsHelp();
7621
- process.exit(1);
7622
- }
7623
- const embed = await client.getEmbed(embedId);
7624
- if (flags.json === true) {
7625
- printJson2(embed);
7626
- } else {
7627
- await renderEmbedFullscreen(embed, client);
7628
- }
9138
+ if (matches(tokens, ["interface", "font", "set"])) {
9139
+ const font = rest[2];
9140
+ if (!font) throw new Error("Missing font. Example: openmates settings interface font set lexend");
9141
+ await printSettingsMutationResult(client.settingsPost("user/ui-font", { ui_font: font }), flags);
7629
9142
  return;
7630
9143
  }
7631
- if (subcommand === "share") {
7632
- const id = rest[0];
7633
- if (!id) {
7634
- console.error(
7635
- "Missing embed ID. Usage: openmates embeds share <embed-id>"
7636
- );
7637
- process.exit(1);
9144
+ if (matches(tokens, ["ai", "models", "set-defaults"])) {
9145
+ const body = {};
9146
+ if (flags.simple !== void 0) {
9147
+ body.default_ai_model_simple = parseModelDefaultFlag(flags.simple, "--simple");
7638
9148
  }
7639
- const durationSeconds = typeof flags.expires === "string" ? parseInt(flags.expires, 10) : 0;
7640
- const password = typeof flags.password === "string" ? flags.password : void 0;
7641
- if (password && password.length > 10) {
7642
- console.error("Password must be at most 10 characters.");
7643
- process.exit(1);
9149
+ if (flags.complex !== void 0) {
9150
+ body.default_ai_model_complex = parseModelDefaultFlag(flags.complex, "--complex");
7644
9151
  }
7645
- try {
7646
- const url = await client.createEmbedShareLink(
7647
- id,
7648
- durationSeconds,
7649
- password
7650
- );
7651
- if (flags.json === true) {
7652
- printJson2({
7653
- url,
7654
- embed_id: id,
7655
- expires: durationSeconds,
7656
- password_protected: !!password
7657
- });
7658
- } else {
7659
- process.stdout.write(`
7660
- \x1B[1mEmbed share link\x1B[0m
7661
- `);
7662
- process.stdout.write(`${url}
7663
-
7664
- `);
7665
- if (durationSeconds > 0) {
7666
- process.stdout.write(
7667
- `\x1B[2mExpires in ${humanizeDuration(durationSeconds)}\x1B[0m
7668
- `
7669
- );
7670
- }
7671
- if (password) {
7672
- process.stdout.write(`\x1B[2mPassword protected\x1B[0m
7673
- `);
7674
- }
7675
- }
7676
- } catch (err) {
7677
- const msg = err instanceof Error ? err.message : String(err);
7678
- console.error(`Share link error: ${msg}`);
7679
- process.exit(1);
9152
+ if (Object.keys(body).length === 0) {
9153
+ throw new Error("Provide --simple <model-id|auto> and/or --complex <model-id|auto>.");
7680
9154
  }
9155
+ await printSettingsMutationResult(client.settingsPost("ai-model-defaults", body), flags);
7681
9156
  return;
7682
9157
  }
7683
- console.error(`Unknown embeds subcommand '${subcommand}'.
7684
- `);
7685
- printEmbedsHelp();
7686
- process.exit(1);
7687
- }
7688
- async function handleSettings(client, subcommand, rest, flags) {
7689
- if (!subcommand || subcommand === "help" || flags.help === true) {
7690
- printSettingsHelp(client);
9158
+ if (matches(tokens, ["privacy", "auto-delete", "chats", "set"])) {
9159
+ const period = rest[3];
9160
+ if (!period) throw new Error("Missing period. Example: openmates settings privacy auto-delete chats set 90d");
9161
+ await printSettingsMutationResult(client.settingsPost("auto-delete-chats", { period }), flags);
7691
9162
  return;
7692
9163
  }
7693
- if (subcommand === "get") {
7694
- const path = rest[0];
7695
- if (!path) {
7696
- console.error("Missing path.\n");
7697
- printSettingsHelp();
7698
- process.exit(1);
7699
- }
7700
- const result = await client.settingsGet(path);
7701
- if (flags.json === true) {
7702
- printJson2(result);
7703
- } else {
7704
- printGenericObject(result);
9164
+ if (matches(tokens, ["privacy", "debug-logs", "share"])) {
9165
+ if (flags.yes !== true && flags.confirm !== true) {
9166
+ await confirmOrExit("Share debug logs with OpenMates support? [y/N] ");
7705
9167
  }
9168
+ const duration = typeof flags.duration === "string" ? flags.duration : "1h";
9169
+ await printSettingsMutationResult(client.settingsPost("debug-session", { duration }), flags);
7706
9170
  return;
7707
9171
  }
7708
- if (subcommand === "post") {
7709
- const path = rest[0];
7710
- if (!path) {
7711
- console.error("Missing path.\n");
7712
- printSettingsHelp();
7713
- process.exit(1);
7714
- }
7715
- const dataRaw = typeof flags.data === "string" ? flags.data : "{}";
7716
- const data = JSON.parse(dataRaw);
7717
- const result = await client.settingsPost(path, data);
7718
- if (flags.json === true) {
7719
- printJson2(result);
7720
- } else {
7721
- printGenericObject(result);
7722
- }
9172
+ if (matches(tokens, ["billing", "overview"])) {
9173
+ await printSettingsResult(client.settingsGet("billing"), flags);
7723
9174
  return;
7724
9175
  }
7725
- if (subcommand === "delete") {
7726
- const path = rest[0];
7727
- if (!path) {
7728
- console.error("Missing path.\n");
7729
- printSettingsHelp();
7730
- process.exit(1);
7731
- }
7732
- const result = await client.settingsDelete(path);
7733
- if (flags.json === true) {
7734
- printJson2(result);
7735
- } else {
7736
- printGenericObject(result);
7737
- }
9176
+ if (matches(tokens, ["billing", "usage", "summaries"])) {
9177
+ await printSettingsResult(client.settingsGet("usage/summaries"), flags);
7738
9178
  return;
7739
9179
  }
7740
- if (subcommand === "patch") {
7741
- const path = rest[0];
7742
- if (!path) {
7743
- console.error("Missing path.\n");
7744
- printSettingsHelp();
7745
- process.exit(1);
7746
- }
7747
- const dataRaw = typeof flags.data === "string" ? flags.data : "{}";
7748
- const data = JSON.parse(dataRaw);
7749
- const result = await client.settingsPatch(path, data);
9180
+ if (matches(tokens, ["billing", "usage", "daily"])) {
9181
+ await printSettingsResult(client.settingsGet("usage/daily-overview"), flags);
9182
+ return;
9183
+ }
9184
+ if (matches(tokens, ["billing", "usage", "export"])) {
9185
+ await printSettingsResult(client.settingsGet("usage/export"), flags);
9186
+ return;
9187
+ }
9188
+ if (matches(tokens, ["billing", "usage"])) {
9189
+ await printSettingsResult(client.settingsGet("usage"), flags);
9190
+ return;
9191
+ }
9192
+ if (matches(tokens, ["billing", "invoices", "list"])) {
9193
+ await printSettingsResult(client.listInvoices(), flags);
9194
+ return;
9195
+ }
9196
+ if (matches(tokens, ["billing", "invoices", "download"])) {
9197
+ const invoiceId = rest[2];
9198
+ if (!invoiceId) throw new Error("Missing invoice ID.");
9199
+ const document = await client.downloadInvoice(invoiceId);
9200
+ const filePath = await saveDownloadedDocument(document, flags.output);
9201
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
9202
+ else console.log(`\x1B[32m\u2713\x1B[0m Invoice saved to ${filePath}`);
9203
+ return;
9204
+ }
9205
+ if (matches(tokens, ["billing", "invoices", "credit-note"])) {
9206
+ const invoiceId = rest[2];
9207
+ if (!invoiceId) throw new Error("Missing invoice ID.");
9208
+ const document = await client.downloadCreditNote(invoiceId);
9209
+ const filePath = await saveDownloadedDocument(document, flags.output);
9210
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
9211
+ else console.log(`\x1B[32m\u2713\x1B[0m Credit note saved to ${filePath}`);
9212
+ return;
9213
+ }
9214
+ if (matches(tokens, ["billing", "invoices", "refund"])) {
9215
+ const invoiceId = rest[2];
9216
+ if (!invoiceId) throw new Error("Missing invoice ID.");
9217
+ if (flags.yes !== true) await confirmOrExit(`Request refund for invoice ${invoiceId}? [y/N] `);
9218
+ await printSettingsMutationResult(client.requestRefund(invoiceId), flags);
9219
+ return;
9220
+ }
9221
+ if (matches(tokens, ["billing", "gift-card", "redeem"]) || subcommand === "gift-card" && rest[0] === "redeem") {
9222
+ const code = matches(tokens, ["billing", "gift-card", "redeem"]) ? rest[2] : rest[1];
9223
+ if (!code) throw new Error("Missing gift card code.");
9224
+ const result = await client.redeemGiftCard(code);
7750
9225
  if (flags.json === true) {
7751
9226
  printJson2(result);
9227
+ } else if (result.success) {
9228
+ process.stdout.write(`\x1B[32m\u2713\x1B[0m Gift card redeemed! +${result.credits_added} credits
9229
+ `);
9230
+ process.stdout.write(` Balance: ${result.current_credits} credits
9231
+ `);
7752
9232
  } else {
7753
- printGenericObject(result);
9233
+ process.stdout.write(`\x1B[31m\u2717\x1B[0m ${result.message}
9234
+ `);
7754
9235
  }
7755
9236
  return;
7756
9237
  }
7757
- if (subcommand === "memories") {
7758
- await handleMemories(client, rest, flags);
9238
+ if (matches(tokens, ["billing", "gift-card", "list"]) || subcommand === "gift-card" && rest[0] === "list") {
9239
+ await printSettingsResult(client.listRedeemedGiftCards(), flags);
7759
9240
  return;
7760
9241
  }
7761
- if (subcommand === "gift-card") {
7762
- const action = rest[0];
7763
- if (action === "redeem") {
7764
- const code = rest[1];
7765
- if (!code) {
7766
- console.error("Missing gift card code.\n");
7767
- console.log("Usage: openmates settings gift-card redeem <CODE>");
7768
- process.exit(1);
7769
- }
7770
- const result = await client.redeemGiftCard(code);
7771
- if (flags.json === true) {
7772
- printJson2(result);
7773
- } else {
7774
- if (result.success) {
7775
- process.stdout.write(
7776
- `\x1B[32m\u2713\x1B[0m Gift card redeemed! +${result.credits_added} credits
7777
- `
7778
- );
7779
- process.stdout.write(
7780
- ` Balance: ${result.current_credits} credits
7781
- `
7782
- );
7783
- } else {
7784
- process.stdout.write(`\x1B[31m\u2717\x1B[0m ${result.message}
7785
- `);
7786
- }
7787
- }
7788
- return;
7789
- }
7790
- if (action === "list") {
7791
- const result = await client.listRedeemedGiftCards();
9242
+ if (matches(tokens, ["billing", "auto-topup", "low-balance", "set"])) {
9243
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "low-balance auto top-up");
9244
+ const amount = parseRequiredNumber(flags.amount, "--amount");
9245
+ const currency = typeof flags.currency === "string" ? flags.currency : "eur";
9246
+ const email = typeof flags.email === "string" ? flags.email : void 0;
9247
+ if (enabled && !email) throw new Error("Provide --email when enabling low-balance auto top-up.");
9248
+ await printSettingsMutationResult(
9249
+ client.settingsPost("auto-topup/low-balance", { enabled, threshold: 100, amount, currency, email }),
9250
+ flags
9251
+ );
9252
+ return;
9253
+ }
9254
+ if (matches(tokens, ["notifications", "status"])) {
9255
+ const user = await client.whoAmI();
9256
+ const status = {
9257
+ enabled: user.email_notifications_enabled ?? false,
9258
+ preferences: user.email_notification_preferences ?? {},
9259
+ backup_reminder_interval_days: user.backup_reminder_interval_days ?? null,
9260
+ encrypted_notification_email_configured: Boolean(user.encrypted_notification_email)
9261
+ };
9262
+ flags.json === true ? printJson2(status) : printGenericObject(status);
9263
+ return;
9264
+ }
9265
+ if (matches(tokens, ["notifications", "list"])) {
9266
+ const limit = parseOptionalNumber(flags.limit, 50, "--limit");
9267
+ await printSettingsResult(client.listNotifications(limit), flags);
9268
+ return;
9269
+ }
9270
+ if (matches(tokens, ["notifications", "stream"])) {
9271
+ const count = flags.count === void 0 ? null : parseRequiredNumber(flags.count, "--count");
9272
+ let received = 0;
9273
+ for await (const event of client.streamNotifications()) {
7792
9274
  if (flags.json === true) {
7793
- printJson2(result);
9275
+ printJson2(event);
7794
9276
  } else {
7795
- printGenericObject(result);
9277
+ printGenericObject(event);
7796
9278
  }
7797
- return;
9279
+ received += 1;
9280
+ if (count !== null && received >= count) break;
7798
9281
  }
7799
- console.log(`Gift card commands:
7800
- openmates settings gift-card redeem <CODE> Redeem a gift card
7801
- openmates settings gift-card list List redeemed gift cards`);
7802
9282
  return;
7803
9283
  }
7804
- console.error(`Unknown settings subcommand '${subcommand}'.
9284
+ if (matches(tokens, ["notifications", "email", "set"])) {
9285
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "email notifications");
9286
+ const email = typeof flags.email === "string" ? flags.email : null;
9287
+ if (enabled && !email) throw new Error("Provide --email when enabling email notifications.");
9288
+ const preferences = {
9289
+ aiResponses: parseOptionalBoolean(flags["ai-responses"], true, "AI response notifications"),
9290
+ backupReminder: parseOptionalBoolean(flags["backup-reminder"], false, "backup reminder notifications"),
9291
+ webhookChats: parseOptionalBoolean(flags["webhook-chats"], false, "webhook chat notifications")
9292
+ };
9293
+ await printSettingsMutationResult(
9294
+ client.updateEmailNotificationSettings({ enabled, email, preferences }),
9295
+ flags
9296
+ );
9297
+ return;
9298
+ }
9299
+ if (matches(tokens, ["notifications", "backup", "set"])) {
9300
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "backup reminders");
9301
+ const email = typeof flags.email === "string" ? flags.email : null;
9302
+ const interval = parseRequiredNumber(flags.interval, "--interval");
9303
+ if (enabled && !email) throw new Error("Provide --email when enabling backup reminders.");
9304
+ await printSettingsMutationResult(
9305
+ client.updateEmailNotificationSettings({
9306
+ enabled,
9307
+ email,
9308
+ preferences: { aiResponses: false, backupReminder: enabled },
9309
+ backup_reminder_interval_days: interval
9310
+ }),
9311
+ flags
9312
+ );
9313
+ return;
9314
+ }
9315
+ if (matches(tokens, ["reminders", "list"])) {
9316
+ await printSettingsResult(client.settingsGet("reminders"), flags);
9317
+ return;
9318
+ }
9319
+ if (matches(tokens, ["reminders", "update"])) {
9320
+ const id = rest[1];
9321
+ if (!id) throw new Error("Missing reminder ID.");
9322
+ const body = parseDataOrFlags(flags, ["enabled"]);
9323
+ await printSettingsMutationResult(client.settingsPatch(`reminders/${id}`, body), flags);
9324
+ return;
9325
+ }
9326
+ if (matches(tokens, ["reminders", "delete"])) {
9327
+ const id = rest[1];
9328
+ if (!id) throw new Error("Missing reminder ID.");
9329
+ if (flags.yes !== true) await confirmOrExit(`Delete reminder ${id}? [y/N] `);
9330
+ await printSettingsMutationResult(client.settingsDelete(`reminders/${id}`), flags);
9331
+ return;
9332
+ }
9333
+ if (matches(tokens, ["developers", "api-keys", "list"])) {
9334
+ await printSettingsResult(client.settingsGet("api-keys"), flags);
9335
+ return;
9336
+ }
9337
+ if (matches(tokens, ["developers", "api-keys", "revoke"])) {
9338
+ const id = rest[2];
9339
+ if (!id) throw new Error("Missing API key ID.");
9340
+ if (flags.yes !== true) await confirmOrExit(`Revoke API key ${id}? [y/N] `);
9341
+ await printSettingsMutationResult(client.settingsDelete(`api-keys/${id}`), flags);
9342
+ return;
9343
+ }
9344
+ if (matches(tokens, ["report-issue", "create"])) {
9345
+ const title = typeof flags.title === "string" ? flags.title : void 0;
9346
+ const body = typeof flags.body === "string" ? flags.body : void 0;
9347
+ if (!title || !body) throw new Error("Provide --title and --body.");
9348
+ await printSettingsMutationResult(client.settingsPost("issues", { title, description: body }), flags);
9349
+ return;
9350
+ }
9351
+ if (matches(tokens, ["report-issue", "status"])) {
9352
+ const id = rest[1];
9353
+ if (!id) throw new Error("Missing issue ID.");
9354
+ await printSettingsResult(client.settingsGet(`issues/${id}/status`), flags);
9355
+ return;
9356
+ }
9357
+ if (matches(tokens, ["mates", "list"])) {
9358
+ printMates(flags.json === true);
9359
+ return;
9360
+ }
9361
+ if (matches(tokens, ["mates", "info"])) {
9362
+ const mateId = rest[1];
9363
+ if (!mateId) throw new Error("Missing mate ID. Example: openmates settings mates info software_development");
9364
+ printMateInfo(mateId, flags.json === true);
9365
+ return;
9366
+ }
9367
+ if (matches(tokens, ["mates", "consent"])) {
9368
+ if (flags.yes !== true) await confirmOrExit("Record consent for mate settings? [y/N] ");
9369
+ await printSettingsMutationResult(client.settingsPost("user/consent/mates", { consent: true }), flags);
9370
+ return;
9371
+ }
9372
+ if (matches(tokens, ["newsletter", "categories"]) && tokens.length === 2) {
9373
+ await printSettingsResult(client.getNewsletterCategories(), flags);
9374
+ return;
9375
+ }
9376
+ if (matches(tokens, ["newsletter", "categories", "set"])) {
9377
+ const categories = {
9378
+ updates_and_announcements: parseOptionalBoolean(flags.updates, true, "updates newsletter"),
9379
+ tips_and_tricks: parseOptionalBoolean(flags.tips, true, "tips newsletter"),
9380
+ daily_inspirations: parseOptionalBoolean(flags.daily, true, "daily inspirations newsletter")
9381
+ };
9382
+ await printSettingsMutationResult(client.updateNewsletterCategories(categories), flags);
9383
+ return;
9384
+ }
9385
+ if (matches(tokens, ["newsletter", "subscribe"])) {
9386
+ const email = rest[1];
9387
+ if (!email) throw new Error("Missing email. Example: openmates settings newsletter subscribe you@example.com");
9388
+ const language = typeof flags.language === "string" ? flags.language : "en";
9389
+ const darkmode = parseOptionalBoolean(flags.darkmode, false, "newsletter dark mode");
9390
+ await printSettingsMutationResult(client.subscribeNewsletter(email, language, darkmode), flags);
9391
+ return;
9392
+ }
9393
+ if (matches(tokens, ["newsletter", "confirm"])) {
9394
+ const token = rest[1];
9395
+ if (!token) throw new Error("Missing confirmation token.");
9396
+ await printSettingsMutationResult(client.confirmNewsletter(token), flags);
9397
+ return;
9398
+ }
9399
+ if (matches(tokens, ["newsletter", "unsubscribe"])) {
9400
+ const token = rest[1];
9401
+ if (!token) throw new Error("Missing unsubscribe token.");
9402
+ await printSettingsMutationResult(client.unsubscribeNewsletter(token), flags);
9403
+ return;
9404
+ }
9405
+ if (subcommand === "memories") {
9406
+ await handleMemories(client, rest, flags);
9407
+ return;
9408
+ }
9409
+ const webOnly = findSettingsInfoCommand(tokens);
9410
+ if (webOnly) {
9411
+ printSettingsInfoCommand(client, webOnly, flags.json === true);
9412
+ return;
9413
+ }
9414
+ console.error(`Unknown settings command '${tokens.join(" ")}'.
7805
9415
  `);
7806
- printSettingsHelp();
9416
+ printSettingsHelp(client, [subcommand]);
7807
9417
  process.exit(1);
7808
9418
  }
7809
9419
  async function handleMemories(client, rest, flags) {
@@ -8132,7 +9742,57 @@ async function sendMessageStreaming(client, params, redactor) {
8132
9742
  processedRawLength = raw.length;
8133
9743
  }
8134
9744
  };
8135
- const encryptedEmbeds = [];
9745
+ const onSubChatEvent = (event) => {
9746
+ if (params.json) return;
9747
+ clearTyping();
9748
+ const payload = event.payload;
9749
+ if (event.type === "spawn_sub_chats") {
9750
+ const count = Array.isArray(payload.sub_chats) ? payload.sub_chats.length : 0;
9751
+ process.stderr.write(
9752
+ `\x1B[2mStarting ${count} sub-chat${count === 1 ? "" : "s"} for parallel research...\x1B[0m
9753
+ `
9754
+ );
9755
+ return;
9756
+ }
9757
+ if (event.type === "sub_chat_progress") {
9758
+ const completed = typeof payload.completed === "number" ? payload.completed : null;
9759
+ const total = typeof payload.total === "number" ? payload.total : null;
9760
+ const status = typeof payload.status === "string" ? payload.status : "running";
9761
+ const progress = completed !== null && total !== null ? `${completed}/${total}` : status;
9762
+ process.stderr.write(`\x1B[2mSub-chats: ${progress} ${status}\x1B[0m
9763
+ `);
9764
+ return;
9765
+ }
9766
+ if (event.type === "sub_chat_confirmation_resolved") {
9767
+ const status = typeof payload.status === "string" ? payload.status : "resolved";
9768
+ process.stderr.write(`\x1B[2mSub-chat approval ${status}.\x1B[0m
9769
+ `);
9770
+ return;
9771
+ }
9772
+ if (event.type === "awaiting_user_input") {
9773
+ process.stderr.write(
9774
+ "\x1B[33mA sub-chat needs additional user input. Continue this chat in the web app if the parent cannot finish.\x1B[0m\n"
9775
+ );
9776
+ }
9777
+ };
9778
+ const onSubChatApprovalRequest = async (request) => {
9779
+ if (params.autoApproveSubChats) return true;
9780
+ clearTyping();
9781
+ const count = request.subChats.length;
9782
+ const remaining = request.remainingSubChats === null ? "" : ` (${request.remainingSubChats} remaining for this parent chat)`;
9783
+ process.stderr.write(
9784
+ `\x1B[33mDeep research wants to start ${count} additional sub-chat${count === 1 ? "" : "s"}${remaining}.\x1B[0m
9785
+ `
9786
+ );
9787
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
9788
+ try {
9789
+ const answer = await rl.question("Approve? [y/N] ");
9790
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
9791
+ } finally {
9792
+ rl.close();
9793
+ }
9794
+ };
9795
+ const preparedEmbeds = [];
8136
9796
  let finalMessage = params.message;
8137
9797
  if (params.message.includes("@")) {
8138
9798
  try {
@@ -8192,8 +9852,44 @@ async function sendMessageStreaming(client, params, redactor) {
8192
9852
  try {
8193
9853
  const session = client.getSession();
8194
9854
  const uploadResult = await uploadFile(fe.localPath, session);
8195
- fe.embed.content = toonEncodeContent({
8196
- type: fe.embed.type === "pdf" ? "pdf" : "image",
9855
+ const embedType = fe.embed.type;
9856
+ const audioTranscription = embedType === "audio-recording" ? await transcribeUploadedAudio(
9857
+ uploadResult,
9858
+ fe.displayName,
9859
+ session,
9860
+ { chatId: params.chatId, requestId: uploadResult.embed_id }
9861
+ ) : null;
9862
+ const uploadedContent = embedType === "audio-recording" ? {
9863
+ app_id: "audio",
9864
+ skill_id: "transcribe",
9865
+ type: "audio-recording",
9866
+ status: "finished",
9867
+ filename: fe.displayName,
9868
+ mime_type: uploadResult.content_type,
9869
+ transcript: audioTranscription?.transcript ?? null,
9870
+ transcript_original: audioTranscription?.transcript_original ?? null,
9871
+ transcript_corrected: audioTranscription?.transcript_corrected ?? null,
9872
+ use_corrected: audioTranscription?.use_corrected ?? null,
9873
+ correction_model: audioTranscription?.correction_model ?? null,
9874
+ model: audioTranscription?.model ?? null,
9875
+ s3_base_url: uploadResult.s3_base_url,
9876
+ files: uploadResult.files,
9877
+ aes_key: uploadResult.aes_key,
9878
+ aes_nonce: uploadResult.aes_nonce,
9879
+ vault_wrapped_aes_key: uploadResult.vault_wrapped_aes_key
9880
+ } : embedType === "pdf" ? {
9881
+ type: "pdf",
9882
+ status: "processing",
9883
+ filename: fe.displayName,
9884
+ page_count: uploadResult.page_count ?? null,
9885
+ content_hash: uploadResult.content_hash,
9886
+ s3_base_url: uploadResult.s3_base_url,
9887
+ files: uploadResult.files,
9888
+ aes_key: uploadResult.aes_key,
9889
+ aes_nonce: uploadResult.aes_nonce,
9890
+ vault_wrapped_aes_key: uploadResult.vault_wrapped_aes_key
9891
+ } : {
9892
+ type: "image",
8197
9893
  app_id: "images",
8198
9894
  skill_id: "upload",
8199
9895
  status: "finished",
@@ -8205,10 +9901,15 @@ async function sendMessageStreaming(client, params, redactor) {
8205
9901
  aes_nonce: uploadResult.aes_nonce,
8206
9902
  vault_wrapped_aes_key: uploadResult.vault_wrapped_aes_key,
8207
9903
  ai_detection: uploadResult.ai_detection
8208
- });
8209
- fe.embed.status = fe.embed.type === "pdf" ? "processing" : "finished";
9904
+ };
9905
+ fe.embed.content = toonEncodeContent(uploadedContent);
9906
+ fe.embed.status = embedType === "pdf" ? "processing" : "finished";
8210
9907
  fe.embed.contentHash = uploadResult.content_hash;
8211
9908
  fe.embed.embedId = uploadResult.embed_id;
9909
+ fe.referenceBlock = createEmbedReferenceBlock(
9910
+ embedType,
9911
+ uploadResult.embed_id
9912
+ );
8212
9913
  if (!params.json) {
8213
9914
  process.stderr.write(
8214
9915
  `\x1B[32m \u2713\x1B[0m \x1B[2m${fe.displayName} uploaded\x1B[0m
@@ -8239,31 +9940,7 @@ async function sendMessageStreaming(client, params, redactor) {
8239
9940
  if (embedRefs) {
8240
9941
  finalMessage += embedRefs;
8241
9942
  }
8242
- try {
8243
- const { masterKey, userId } = client.getEmbedEncryptionKeys();
8244
- for (const fe of fileResult.embeds) {
8245
- const encrypted = await encryptEmbed(
8246
- fe.embed,
8247
- masterKey,
8248
- null,
8249
- // chatKey — not available in CLI yet for new chats
8250
- params.chatId || "new",
8251
- // will be replaced by server
8252
- "pending",
8253
- // messageId set during send
8254
- userId
8255
- );
8256
- if (encrypted) {
8257
- encryptedEmbeds.push(encrypted);
8258
- }
8259
- }
8260
- } catch (err) {
8261
- const msg = err instanceof Error ? err.message : String(err);
8262
- process.stderr.write(
8263
- `\x1B[33mWarning:\x1B[0m Embed encryption failed: ${msg}. Files sent without encryption.
8264
- `
8265
- );
8266
- }
9943
+ preparedEmbeds.push(...fileResult.embeds.map((fileEmbed) => fileEmbed.embed));
8267
9944
  }
8268
9945
  if (parsed.resolved.length > 0 && !params.json) {
8269
9946
  clearTyping();
@@ -8278,12 +9955,18 @@ async function sendMessageStreaming(client, params, redactor) {
8278
9955
  } catch {
8279
9956
  }
8280
9957
  }
9958
+ const urlResult = prepareUrlEmbeds(finalMessage);
9959
+ finalMessage = urlResult.message;
9960
+ preparedEmbeds.push(...urlResult.embeds);
8281
9961
  const result = await client.sendMessage({
8282
9962
  message: finalMessage,
8283
9963
  chatId: params.chatId,
8284
9964
  incognito: params.incognito,
8285
9965
  onStream,
8286
- encryptedEmbeds: encryptedEmbeds.length > 0 ? encryptedEmbeds : void 0
9966
+ onSubChatEvent,
9967
+ onSubChatApprovalRequest,
9968
+ autoApproveSubChats: params.autoApproveSubChats,
9969
+ preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0
8287
9970
  });
8288
9971
  clearTyping();
8289
9972
  if (!params.json) {
@@ -8356,21 +10039,13 @@ async function sendMessageStreaming(client, params, redactor) {
8356
10039
  }
8357
10040
  return result;
8358
10041
  }
8359
- function printIncognitoHistory(history) {
8360
- if (history.length === 0) {
8361
- console.log("No incognito history.");
10042
+ function printIncognitoNoHistoryNotice(json) {
10043
+ const message = "Incognito chats are not stored. There is no incognito history to show or clear.";
10044
+ if (json) {
10045
+ printJson2({ history: [], stored: false, message });
8362
10046
  return;
8363
10047
  }
8364
- header(`Incognito history (${history.length} messages)`);
8365
- console.log();
8366
- for (const msg of history) {
8367
- const ts = formatTimestamp(Math.floor(msg.createdAt / 1e3));
8368
- const roleLabel = msg.role === "user" ? "\x1B[1mYou\x1B[0m" : "\x1B[36mAssistant\x1B[0m";
8369
- process.stdout.write(`${roleLabel} \x1B[2m${ts}\x1B[0m
8370
- `);
8371
- console.log(msg.content);
8372
- console.log();
8373
- }
10048
+ console.log(message);
8374
10049
  }
8375
10050
  var SEP = `\x1B[2m${"\u2500".repeat(60)}\x1B[0m`;
8376
10051
  function parseMessageSegments(content) {
@@ -9337,23 +11012,23 @@ async function handleMentions(client, subcommand, _rest, flags) {
9337
11012
  const context = await client.buildMentionContext();
9338
11013
  const allOptions = listMentionOptions(context);
9339
11014
  const normalizedQuery = query.toLowerCase().replace(/[\s_-]+/g, "");
9340
- const matches = allOptions.filter((opt) => {
11015
+ const matches2 = allOptions.filter((opt) => {
9341
11016
  const normalizedName = opt.displayName.toLowerCase().replace(/[@\s_-]+/g, "");
9342
11017
  const normalizedDesc = opt.description.toLowerCase().replace(/[\s_-]+/g, "");
9343
11018
  return normalizedName.includes(normalizedQuery) || normalizedDesc.includes(normalizedQuery);
9344
11019
  }).slice(0, 15);
9345
11020
  if (flags.json === true) {
9346
- console.log(JSON.stringify(matches, null, 2));
11021
+ console.log(JSON.stringify(matches2, null, 2));
9347
11022
  return;
9348
11023
  }
9349
- if (matches.length === 0) {
11024
+ if (matches2.length === 0) {
9350
11025
  console.log(`No mentions matching '${query}'.`);
9351
11026
  return;
9352
11027
  }
9353
11028
  process.stdout.write(`
9354
11029
  \x1B[1mMatches for '${query}':\x1B[0m
9355
11030
  `);
9356
- for (const m of matches) {
11031
+ for (const m of matches2) {
9357
11032
  const typeLabel = m.type.replace("_", " ");
9358
11033
  process.stdout.write(
9359
11034
  ` \x1B[36m${m.displayName.padEnd(35)}\x1B[0m \x1B[2m${m.description} (${typeLabel})\x1B[0m
@@ -9498,7 +11173,7 @@ Commands:
9498
11173
  openmates apps [--help] App skill commands (list, run, ...)
9499
11174
  openmates mentions [--help] List available @mentions
9500
11175
  openmates embeds [--help] Embed commands (show)
9501
- openmates settings [--help] Memories
11176
+ openmates settings [--help] Predefined settings commands
9502
11177
  openmates inspirations [--lang <code>] [--json] Daily inspirations
9503
11178
  openmates newchatsuggestions [--limit <n>] [--json] Personalized new chat suggestions
9504
11179
  openmates server [--help] Server management (install, start, stop, ...)
@@ -9516,15 +11191,15 @@ function printChatsHelp() {
9516
11191
  openmates chats show <chat-id> [--raw] [--json]
9517
11192
  openmates chats open [<n>] [--json]
9518
11193
  openmates chats search <query> [--json]
9519
- openmates chats new <message> [--json]
9520
- openmates chats send [--chat <id>] [--incognito] <message> [--json]
9521
- openmates chats send --chat <id> --followup <n> [--json]
11194
+ openmates chats new <message> [--json] [--auto-approve]
11195
+ openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve]
11196
+ openmates chats send --chat <id> --followup <n> [--json] [--auto-approve]
9522
11197
  openmates chats download <chat-id> [--output <path>] [--zip] [--json]
9523
11198
  openmates chats delete <id1> [id2] [id3] ... [--yes]
9524
11199
  openmates chats share [<chat-id>] [--expires <seconds>] [--password <pwd>] [--json]
9525
11200
  openmates chats incognito <message> [--json]
9526
- openmates chats incognito-history [--json]
9527
- openmates chats incognito-clear
11201
+ openmates chats incognito-history [--json] Deprecated: incognito stores no history
11202
+ openmates chats incognito-clear Deprecated: incognito stores no history
9528
11203
 
9529
11204
  Options for 'list':
9530
11205
  --limit <n> Number of chats per page (default: 10)
@@ -9545,6 +11220,10 @@ Options for 'send':
9545
11220
  typing the full message (requires --chat)
9546
11221
  --incognito Send without saving to chat history
9547
11222
 
11223
+ Options for 'new', 'send', and 'incognito':
11224
+ --auto-approve Automatically approve server-requested sub-chat batches.
11225
+ Without this, the CLI prompts in the terminal like the web app.
11226
+
9548
11227
  Options for 'download':
9549
11228
  --output <path> Target directory (default: current directory)
9550
11229
  --zip Create a .zip archive instead of a folder
@@ -9648,72 +11327,33 @@ Examples:
9648
11327
  openmates inspirations --lang de
9649
11328
  openmates inspirations --json`);
9650
11329
  }
9651
- function printSettingsHelp(client) {
11330
+ function printSettingsHelp(client, filter) {
11331
+ const commands = [...SETTINGS_EXECUTABLE_COMMANDS, ...SETTINGS_INFO_COMMANDS].filter((command) => {
11332
+ if (!filter || filter.length === 0) return true;
11333
+ return filter.every((part, index) => command.path[index] === part);
11334
+ }).sort((a, b) => a.path.join(" ").localeCompare(b.path.join(" ")));
9652
11335
  const appUrl = client ? deriveAppUrl(client.apiUrl) : "https://openmates.org";
9653
- const s = (path) => `${appUrl}/#settings/${path}`;
9654
- const h = (title) => `
9655
- \x1B[1m${title}\x1B[0m`;
9656
- console.log(`\x1B[1mSettings\x1B[0m
9657
- ${h("Account")}
9658
- openmates settings post user/username --data '{"encrypted_username":"..."}'
9659
- openmates settings post user/timezone --data '{"timezone":"Europe/Berlin"}'
9660
- openmates settings get export-account-manifest GDPR data export manifest
9661
- openmates settings get export-account-data GDPR data export
9662
- openmates settings post import-chat --data '<json>' Import a chat
9663
- openmates settings get storage [--json] Storage overview
9664
- openmates settings get chats [--json] Chat statistics
9665
- openmates settings get delete-account-preview Preview account deletion
9666
- \x1B[2mSecurity (passkeys, password, 2FA, sessions): ${s("account/security")}\x1B[0m
9667
- \x1B[2mDelete account: ${s("account/delete")}\x1B[0m
9668
- ${h("Billing")}
9669
- openmates settings get billing [--json] Balance & billing overview
9670
- openmates settings post auto-topup/low-balance --data '{"enabled":true,"amount":1000,"currency":"eur"}'
9671
- openmates settings get usage [--json] Full usage history
9672
- openmates settings get usage/summaries [--json] Usage summaries by type
9673
- openmates settings get usage/daily-overview [--json]
9674
- openmates settings get usage/export [--json] Export usage as CSV
9675
- openmates settings gift-card redeem <CODE> Redeem a gift card
9676
- openmates settings gift-card list List redeemed gift cards
9677
- \x1B[2mBuy credits: ${s("billing/buy-credits")}\x1B[0m
9678
- \x1B[2mMonthly auto top-up: ${s("billing/auto-topup/monthly")}\x1B[0m
9679
- \x1B[2mInvoices: ${s("billing/invoices")}\x1B[0m
9680
- \x1B[2mGift cards (buy/manage): ${s("billing/gift-cards")}\x1B[0m
9681
- ${h("Privacy")}
9682
- openmates settings post auto-delete-chats --data '{"period":"90d"}'
9683
- \x1B[2mHide personal data / anonymization: ${s("privacy/hide-personal-data")}\x1B[0m
9684
- ${h("Notifications")}
9685
- openmates settings get reminders [--json] Active reminders
9686
- \x1B[2mChat notifications: ${s("notifications/chat")}\x1B[0m
9687
- \x1B[2mBackup reminders: ${s("notifications/backup")}\x1B[0m
9688
- ${h("Interface")}
9689
- openmates settings post user/language --data '{"language":"en"}'
9690
- openmates settings post user/darkmode --data '{"dark_mode":true}'
9691
- openmates settings post ai-model-defaults --data '{"simple":"...","complex":"..."}'
9692
- ${h("Apps")}
9693
- openmates apps list Same as Apps
9694
- openmates apps <app-id> App details
9695
- \x1B[2mWeb: ${s("app_store")}\x1B[0m
9696
- ${h("Mates")}
9697
- \x1B[2m${s("mates")}\x1B[0m
9698
- ${h("Memories & app settings")}
9699
- openmates settings memories list [--app-id <id>] [--item-type <type>] [--json]
9700
- openmates settings memories types [--app-id <id>] [--json]
9701
- openmates settings memories create --app-id <id> --item-type <type> --data '<json>'
9702
- openmates settings memories update --id <id> --app-id <id> --item-type <type> --data '<json>'
9703
- openmates settings memories delete --id <entry-id>
9704
- ${h("Developers")}
9705
- openmates settings get api-keys [--json] List API keys
9706
- openmates settings delete api-keys/<key-id> Revoke API key
9707
- \x1B[2mCreate API key (shows secret once): ${s("developers/api-keys")}\x1B[0m
9708
- \x1B[2mManage devices: ${s("developers/devices")}\x1B[0m
9709
- ${h("Support")}
9710
- openmates settings post issues --data '<json>' Report an issue
9711
-
9712
- \x1B[2mWeb app only (security \u2014 manage in browser):\x1B[0m
9713
- \x1B[2mPasskeys: ${s("account/security/passkeys")}\x1B[0m
9714
- \x1B[2mPassword: ${s("account/security/password")}\x1B[0m
9715
- \x1B[2m2FA: ${s("account/security/2fa")}\x1B[0m
9716
- \x1B[2mSessions: ${s("account/security/sessions")}\x1B[0m`);
11336
+ const title = filter && filter.length > 0 ? `Settings: ${filter.join(" ")}` : "Settings";
11337
+ header(title);
11338
+ console.log("Predefined commands only. Raw settings get/post/patch/delete is not supported.\n");
11339
+ if (commands.length === 0) {
11340
+ console.log("No matching settings commands.");
11341
+ return;
11342
+ }
11343
+ for (const command of commands) {
11344
+ const isInfoOnly = SETTINGS_INFO_COMMANDS.includes(command);
11345
+ const label = `openmates settings ${command.path.join(" ")}`;
11346
+ process.stdout.write(` ${label.padEnd(58)} ${command.description}`);
11347
+ if (isInfoOnly) process.stdout.write(" \x1B[2m(info/web-only)\x1B[0m");
11348
+ process.stdout.write("\n");
11349
+ if (filter && filter.length > 0) {
11350
+ for (const example of command.examples) process.stdout.write(` e.g. ${example}
11351
+ `);
11352
+ if (command.webPath) process.stdout.write(` web: ${appUrl}/#settings/${command.webPath}
11353
+ `);
11354
+ }
11355
+ }
11356
+ console.log("\nUse --help after a group for examples, e.g. openmates settings billing --help");
9717
11357
  }
9718
11358
  function printNewChatSuggestionsHelp() {
9719
11359
  console.log(`New chat suggestions command:
@@ -9889,6 +11529,7 @@ export {
9889
11529
  deriveAppUrl,
9890
11530
  OpenMatesClient,
9891
11531
  parseNewChatSuggestionText,
11532
+ defaultCloneBranchForVersion,
9892
11533
  serializeToYaml,
9893
11534
  getExtForLang
9894
11535
  };