openmates 0.12.0-alpha.6 → 0.12.0-alpha.8

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.
@@ -730,6 +730,18 @@ function writeJsonFile(filePath, data) {
730
730
  });
731
731
  chmodSync(filePath, 384);
732
732
  }
733
+ function loadAnonymousId() {
734
+ const filePath = join(getStateDir(), "anonymous.json");
735
+ const data = readJsonFile(filePath);
736
+ return typeof data?.anonymousId === "string" && data.anonymousId.length > 0 ? data.anonymousId : null;
737
+ }
738
+ function saveAnonymousId(anonymousId) {
739
+ const filePath = join(ensureStateDir(), "anonymous.json");
740
+ writeJsonFile(filePath, {
741
+ anonymousId,
742
+ createdAt: Math.floor(Date.now() / 1e3)
743
+ });
744
+ }
733
745
  function saveSession(session) {
734
746
  const filePath = join(ensureStateDir(), "session.json");
735
747
  const result = storeMasterKey(session.masterKeyExportedB64, session.hashedEmail);
@@ -2759,6 +2771,65 @@ var OpenMatesClient = class _OpenMatesClient {
2759
2771
  hasSession() {
2760
2772
  return this.session !== null;
2761
2773
  }
2774
+ async getAnonymousFreeUsageStatus() {
2775
+ const response = await this.http.get("/v1/anonymous/free-usage/status");
2776
+ if (response.status === 404) {
2777
+ return {
2778
+ active: false,
2779
+ reason: "self_hosted",
2780
+ resetAt: null,
2781
+ cta: "Anonymous free chat is not available on this server."
2782
+ };
2783
+ }
2784
+ if (!response.ok) {
2785
+ const detail = response.data.detail;
2786
+ const message = typeof detail === "object" && detail?.message ? detail.message : typeof detail === "string" ? detail : `Anonymous free usage status failed with HTTP ${response.status}`;
2787
+ throw new Error(message);
2788
+ }
2789
+ return {
2790
+ active: response.data.active === true,
2791
+ reason: response.data.reason ?? null,
2792
+ resetAt: response.data.reset_at ?? null,
2793
+ cta: response.data.cta ?? null
2794
+ };
2795
+ }
2796
+ async sendAnonymousMessage(params) {
2797
+ const availability = await this.getAnonymousFreeUsageStatus();
2798
+ if (!availability.active) {
2799
+ throw new Error(availability.cta ?? "Create an account to keep using OpenMates.");
2800
+ }
2801
+ let anonymousId = loadAnonymousId();
2802
+ if (!anonymousId) {
2803
+ anonymousId = randomUUID2();
2804
+ saveAnonymousId(anonymousId);
2805
+ }
2806
+ const chatId = `anonymous-${randomUUID2()}`;
2807
+ const messageId = `anonymous-message-${randomUUID2()}`;
2808
+ const response = await this.http.post("/v1/anonymous/chat/stream", {
2809
+ anonymous_id: anonymousId,
2810
+ client_chat_id: chatId,
2811
+ client_message_id: messageId,
2812
+ plaintext_message: params.message,
2813
+ message_history: []
2814
+ });
2815
+ if (!response.ok) {
2816
+ const detail = response.data.detail;
2817
+ const message = typeof detail === "object" && detail?.message ? detail.message : typeof detail === "string" ? detail : `Anonymous chat failed with HTTP ${response.status}`;
2818
+ throw new Error(message);
2819
+ }
2820
+ return {
2821
+ status: "completed",
2822
+ chatId: response.data.chatId ?? chatId,
2823
+ messageId: response.data.messageId ?? messageId,
2824
+ assistant: response.data.assistant ?? "",
2825
+ category: response.data.category ?? null,
2826
+ modelName: response.data.modelName ?? null,
2827
+ mateName: null,
2828
+ followUpSuggestions: response.data.followUpSuggestions ?? [],
2829
+ subChatEvents: [],
2830
+ appSettingsMemoryRequests: []
2831
+ };
2832
+ }
2762
2833
  async createTurnTokenRefs(params) {
2763
2834
  if (params.refs.length === 0) return [];
2764
2835
  const response = await this.http.post(
@@ -2783,6 +2854,35 @@ var OpenMatesClient = class _OpenMatesClient {
2783
2854
  };
2784
2855
  });
2785
2856
  }
2857
+ async cancelConnectedAccountAction(params) {
2858
+ const response = await this.http.post(
2859
+ `/v1/connected-accounts/actions/${encodeURIComponent(params.actionId)}/cancel`,
2860
+ {
2861
+ chat_id: params.chatId,
2862
+ message_id: params.messageId
2863
+ },
2864
+ this.getCliRequestHeaders()
2865
+ );
2866
+ if (!response.ok) {
2867
+ throw new Error(`Failed to cancel connected-account action (HTTP ${response.status})`);
2868
+ }
2869
+ return response.data;
2870
+ }
2871
+ async undoConnectedAccountAction(params) {
2872
+ const response = await this.http.post(
2873
+ `/v1/connected-accounts/actions/${encodeURIComponent(params.actionId)}/undo`,
2874
+ {
2875
+ chat_id: params.chatId,
2876
+ message_id: params.messageId,
2877
+ turn_token_ref: params.turnTokenRef
2878
+ },
2879
+ this.getCliRequestHeaders()
2880
+ );
2881
+ if (!response.ok) {
2882
+ throw new Error(`Failed to undo connected-account action (HTTP ${response.status})`);
2883
+ }
2884
+ return response.data;
2885
+ }
2786
2886
  // -------------------------------------------------------------------------
2787
2887
  // Auth
2788
2888
  // -------------------------------------------------------------------------
@@ -3553,6 +3653,7 @@ var OpenMatesClient = class _OpenMatesClient {
3553
3653
  refs: params.connectedAccountTokenRefInputs
3554
3654
  }) : [];
3555
3655
  assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
3656
+ const piiMappings = params.piiMappings ?? [];
3556
3657
  const messagePayload = {
3557
3658
  chat_id: chatId,
3558
3659
  is_incognito: Boolean(params.incognito),
@@ -3666,6 +3767,12 @@ var OpenMatesClient = class _OpenMatesClient {
3666
3767
  last_edited_overall_timestamp: createdAt
3667
3768
  }
3668
3769
  };
3770
+ if (piiMappings.length > 0) {
3771
+ metadataPayload.encrypted_pii_mappings = await encryptWithAesGcmCombined(
3772
+ JSON.stringify(piiMappings),
3773
+ chatKeyBytes
3774
+ );
3775
+ }
3669
3776
  ws.send("encrypted_chat_metadata", metadataPayload);
3670
3777
  }
3671
3778
  let assistant = "";
@@ -5574,6 +5681,7 @@ Required: ${schema.required.join(", ")}`
5574
5681
  const embedKeys = [];
5575
5682
  let newChatSuggestions = [];
5576
5683
  let totalChatCount = 0;
5684
+ const pendingAIResponses = [];
5577
5685
  try {
5578
5686
  ws.send("phased_sync_request", {
5579
5687
  phase: "all",
@@ -5613,6 +5721,8 @@ Required: ${schema.required.join(", ")}`
5613
5721
  }
5614
5722
  if (p.embeds) embeds.push(...p.embeds);
5615
5723
  if (p.embed_keys) embedKeys.push(...p.embed_keys);
5724
+ } else if (frame.type === "pending_ai_response") {
5725
+ pendingAIResponses.push(frame.payload);
5616
5726
  }
5617
5727
  }
5618
5728
  for (const chat of chats) {
@@ -5623,8 +5733,9 @@ Required: ${schema.required.join(", ")}`
5623
5733
  }
5624
5734
  }
5625
5735
  if (totalChatCount === 0) totalChatCount = chats.length;
5626
- } finally {
5736
+ } catch (error) {
5627
5737
  ws.close();
5738
+ throw error;
5628
5739
  }
5629
5740
  if (existingCache) {
5630
5741
  const cachedById = new Map(
@@ -5681,6 +5792,11 @@ Required: ${schema.required.join(", ")}`
5681
5792
  if (totalChatCount > 0 && chats.length > totalChatCount) {
5682
5793
  chats.length = totalChatCount;
5683
5794
  }
5795
+ try {
5796
+ await this.persistPendingAIResponsesFromSync(ws, chats, pendingAIResponses);
5797
+ } finally {
5798
+ ws.close();
5799
+ }
5684
5800
  const cache = {
5685
5801
  syncedAt: Date.now(),
5686
5802
  totalChatCount,
@@ -5693,6 +5809,76 @@ Required: ${schema.required.join(", ")}`
5693
5809
  saveSyncCache(cache);
5694
5810
  return cache;
5695
5811
  }
5812
+ async persistPendingAIResponsesFromSync(ws, chats, pendingResponses) {
5813
+ if (pendingResponses.length === 0) return;
5814
+ const masterKey = this.getMasterKeyBytes();
5815
+ for (const pending of pendingResponses) {
5816
+ const chatId = pending.chat_id;
5817
+ const messageId = pending.message_id;
5818
+ const content = pending.content;
5819
+ if (!chatId || !messageId || !content) continue;
5820
+ const chat = chats.find((candidate) => String(candidate.details.id ?? "") === chatId);
5821
+ if (!chat) continue;
5822
+ const existingIndex = chat.messages.findIndex((raw) => {
5823
+ try {
5824
+ const message = typeof raw === "string" ? JSON.parse(raw) : raw;
5825
+ return message?.message_id === messageId || message?.id === messageId;
5826
+ } catch {
5827
+ return false;
5828
+ }
5829
+ });
5830
+ if (existingIndex >= 0) {
5831
+ const existingRaw = chat.messages[existingIndex];
5832
+ try {
5833
+ const existing = typeof existingRaw === "string" ? JSON.parse(existingRaw) : existingRaw;
5834
+ if (existing?.role === "assistant" && existing?.encrypted_content) continue;
5835
+ } catch {
5836
+ }
5837
+ }
5838
+ const encryptedChatKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
5839
+ if (!encryptedChatKey) continue;
5840
+ const chatKeyBytes = await this.decryptChatKey(encryptedChatKey, masterKey);
5841
+ if (!chatKeyBytes) continue;
5842
+ const completedAt = normalizeUnixSeconds(
5843
+ pending.fired_at,
5844
+ Math.floor(Date.now() / 1e3)
5845
+ );
5846
+ const currentMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
5847
+ const nextMessagesV = currentMessagesV + 1;
5848
+ const encryptedMessage = {
5849
+ message_id: messageId,
5850
+ chat_id: chatId,
5851
+ role: "assistant",
5852
+ created_at: completedAt,
5853
+ status: "synced",
5854
+ encrypted_content: await encryptWithAesGcmCombined(content, chatKeyBytes),
5855
+ encrypted_category: pending.category ? await encryptWithAesGcmCombined(pending.category, chatKeyBytes) : void 0,
5856
+ encrypted_model_name: pending.model_name ? await encryptWithAesGcmCombined(pending.model_name, chatKeyBytes) : void 0
5857
+ };
5858
+ const confirmed = ws.waitForMessage(
5859
+ "ai_response_storage_confirmed",
5860
+ (payload) => {
5861
+ const p = payload;
5862
+ return p.message_id === messageId;
5863
+ },
5864
+ 2e4
5865
+ );
5866
+ await ws.sendAsync("ai_response_completed", {
5867
+ chat_id: chatId,
5868
+ message: encryptedMessage,
5869
+ versions: {
5870
+ messages_v: nextMessagesV,
5871
+ last_edited_overall_timestamp: completedAt
5872
+ }
5873
+ });
5874
+ await confirmed;
5875
+ const serializedMessage = JSON.stringify(encryptedMessage);
5876
+ if (existingIndex >= 0) chat.messages[existingIndex] = serializedMessage;
5877
+ else chat.messages.push(serializedMessage);
5878
+ chat.details.messages_v = nextMessagesV;
5879
+ chat.details.last_edited_overall_timestamp = completedAt;
5880
+ }
5881
+ }
5696
5882
  async prompt(question) {
5697
5883
  const rl = createInterface({ input: stdin, output: stdout });
5698
5884
  try {
@@ -5991,6 +6177,31 @@ var SECRET_PATTERNS = [
5991
6177
  placeholderPrefix: "JWT_TOKEN"
5992
6178
  }
5993
6179
  ];
6180
+ var GENERAL_PII_PATTERNS = [
6181
+ {
6182
+ type: "EMAIL",
6183
+ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
6184
+ label: "Email Address",
6185
+ placeholderPrefix: "EMAIL"
6186
+ },
6187
+ {
6188
+ type: "PHONE",
6189
+ regex: /(?:(?:\+|00)[1-9]\d{0,2}[-.\s/]?(?:\(?\d{1,5}\)?[-.\s/]?){1,4}\d{2,4})|(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}|(?:0\d[-.\s/]?(?:\(?\d{1,5}\)?[-.\s/]?){1,4}\d{2,4})/g,
6190
+ label: "Phone Number",
6191
+ placeholderPrefix: "PHONE",
6192
+ validate: (match) => {
6193
+ const trimmed = match.trim();
6194
+ const digits = trimmed.replace(/\D/g, "");
6195
+ if (digits.length < 7 || digits.length > 15) return false;
6196
+ if (/^\d{4}$/.test(trimmed)) return false;
6197
+ if (/^\d{4}[-/.]\d{1,2}[-/.]\d{1,2}$/.test(trimmed)) return false;
6198
+ if (/^\d{1,2}[-/.]\d{1,2}[-/.]\d{4}$/.test(trimmed)) return false;
6199
+ if (/^(?:19|20)\d{6}$/.test(trimmed)) return false;
6200
+ if (/^0\d{1,2}[-/.]\d{1,2}[-/.]\d{1,2}$/.test(trimmed)) return false;
6201
+ return true;
6202
+ }
6203
+ }
6204
+ ];
5994
6205
 
5995
6206
  // ../secret-scanner/src/registry.ts
5996
6207
  var require3 = createRequire2(import.meta.url);
@@ -6328,6 +6539,7 @@ var SecretScanner = class {
6328
6539
  if (!text) return { redacted: text, mappings: [] };
6329
6540
  const allMappings = [];
6330
6541
  const coveredRanges = [];
6542
+ const urlExclusionZones = getUrlExclusionZones(text);
6331
6543
  if (this.options.enableRegistryDetection && this.registry.size > 0) {
6332
6544
  const registryMappings = this.registry.findAll(text);
6333
6545
  for (const mapping of registryMappings) {
@@ -6345,13 +6557,14 @@ var SecretScanner = class {
6345
6557
  }
6346
6558
  const typeCounts = {};
6347
6559
  if (this.options.enablePatternDetection) {
6348
- for (const pattern of SECRET_PATTERNS) {
6560
+ for (const pattern of [...SECRET_PATTERNS, ...GENERAL_PII_PATTERNS]) {
6349
6561
  pattern.regex.lastIndex = 0;
6350
6562
  let match;
6351
6563
  while ((match = pattern.regex.exec(text)) !== null) {
6352
6564
  const matchText = match[0];
6353
6565
  const startIndex = match.index;
6354
6566
  const endIndex = startIndex + matchText.length;
6567
+ if (isInsideAnyRange(startIndex, endIndex, urlExclusionZones)) continue;
6355
6568
  const overlaps = coveredRanges.some(
6356
6569
  (range) => startIndex >= range.start && startIndex < range.end || endIndex > range.start && endIndex <= range.end || startIndex <= range.start && endIndex >= range.end
6357
6570
  );
@@ -6427,6 +6640,18 @@ var SecretScanner = class {
6427
6640
  function escapeRegExp2(string) {
6428
6641
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6429
6642
  }
6643
+ function getUrlExclusionZones(text) {
6644
+ const urlExclusionRegex = /https?:\/\/[^\s]+/g;
6645
+ const zones = [];
6646
+ let match;
6647
+ while ((match = urlExclusionRegex.exec(text)) !== null) {
6648
+ zones.push({ start: match.index, end: match.index + match[0].length });
6649
+ }
6650
+ return zones;
6651
+ }
6652
+ function isInsideAnyRange(startIndex, endIndex, ranges) {
6653
+ return ranges.some((range) => startIndex >= range.start && endIndex <= range.end);
6654
+ }
6430
6655
 
6431
6656
  // src/outputRedactor.ts
6432
6657
  var PRIVACY_APP_ID = "privacy";
@@ -7080,6 +7305,8 @@ var DIRECT_TYPES = /* @__PURE__ */ new Set([
7080
7305
  "recording",
7081
7306
  "mail-email",
7082
7307
  "math-plot",
7308
+ "mermaid",
7309
+ "diagrams-mermaid",
7083
7310
  "events-event",
7084
7311
  "health-appointment",
7085
7312
  "shopping-product",
@@ -7107,6 +7334,8 @@ var DIRECT_TYPE_LABELS = {
7107
7334
  "recording": "recording",
7108
7335
  "mail-email": "email",
7109
7336
  "math-plot": "plot",
7337
+ "mermaid": "Mermaid diagram",
7338
+ "diagrams-mermaid": "Mermaid diagram",
7110
7339
  "events-event": "event",
7111
7340
  "health-appointment": "appointment",
7112
7341
  "shopping-product": "product",
@@ -8063,6 +8292,11 @@ function renderByDirectType(embed, c, ln) {
8063
8292
  ln("\x1B[2m[mathematical plot]\x1B[0m");
8064
8293
  break;
8065
8294
  }
8295
+ case "mermaid":
8296
+ case "diagrams-mermaid": {
8297
+ renderMermaidPreview(c, ln);
8298
+ break;
8299
+ }
8066
8300
  case "images-image-result": {
8067
8301
  const title = str(c.title) ?? "";
8068
8302
  const source = str(c.source) ?? str(c.url) ?? "";
@@ -8228,6 +8462,11 @@ function renderDirectTypeFullscreen(embed, c) {
8228
8462
  }
8229
8463
  break;
8230
8464
  }
8465
+ case "mermaid":
8466
+ case "diagrams-mermaid": {
8467
+ renderMermaidFullscreen(c);
8468
+ break;
8469
+ }
8231
8470
  default: {
8232
8471
  for (const [k, v] of Object.entries(c)) {
8233
8472
  if (v === null || v === void 0 || k.startsWith("_")) continue;
@@ -8246,6 +8485,38 @@ function renderDirectTypeFullscreen(embed, c) {
8246
8485
  }
8247
8486
  }
8248
8487
  }
8488
+ function renderMermaidPreview(c, ln) {
8489
+ const title = str(c.title) ?? "Mermaid diagram";
8490
+ const kind = str(c.diagram_kind) ?? "mermaid";
8491
+ const source = str(c.diagram_code) ?? str(c.code) ?? str(c.source) ?? "";
8492
+ ln(`\x1B[1m${title}\x1B[0m`);
8493
+ ln(`\x1B[2mMermaid diagram \xB7 ${kind}\x1B[0m`);
8494
+ if (source) {
8495
+ for (const line of source.split("\n").slice(0, 4)) {
8496
+ if (line.trim()) ln(` ${trunc(line, 80)}`);
8497
+ }
8498
+ if (source.split("\n").length > 4) ln(" ...");
8499
+ }
8500
+ }
8501
+ function renderMermaidFullscreen(c) {
8502
+ const title = str(c.title) ?? "Mermaid diagram";
8503
+ const kind = str(c.diagram_kind) ?? "mermaid";
8504
+ const status = str(c.status);
8505
+ const source = str(c.diagram_code) ?? str(c.code) ?? str(c.source) ?? "";
8506
+ process.stdout.write(`\x1B[1m${title}\x1B[0m
8507
+ `);
8508
+ process.stdout.write(`\x1B[2mMermaid diagram \xB7 ${kind}${status ? ` \xB7 ${status}` : ""}\x1B[0m
8509
+
8510
+ `);
8511
+ if (source) {
8512
+ process.stdout.write("```mermaid\n");
8513
+ process.stdout.write(`${source.trim()}
8514
+ `);
8515
+ process.stdout.write("```\n");
8516
+ } else {
8517
+ process.stdout.write("No Mermaid source available.\n");
8518
+ }
8519
+ }
8249
8520
  function resolveResultCount(c) {
8250
8521
  if (typeof c.result_count === "number") return c.result_count;
8251
8522
  const results = c.results;
@@ -8293,7 +8564,16 @@ var COMPOSE_OVERRIDE = join3("backend", "core", "docker-compose.override.yml");
8293
8564
  var DEFAULT_INSTALL_PATH = join3(homedir5(), "openmates");
8294
8565
  var REPO_URL = "https://github.com/glowingkitty/OpenMates.git";
8295
8566
  var DEV_BRANCH = "dev";
8567
+ var MAIN_BRANCH = "main";
8296
8568
  var DEFAULT_IMAGE_REGISTRY = "ghcr.io/glowingkitty";
8569
+ var UPDATE_HEALTH_TIMEOUT_MS = 12e4;
8570
+ var UPDATE_HEALTH_INTERVAL_MS = 5e3;
8571
+ var HEALTH_REQUEST_TIMEOUT_MS = 5e3;
8572
+ var IMAGE_CHANNEL_TAGS = {
8573
+ stable: MAIN_BRANCH,
8574
+ main: MAIN_BRANCH,
8575
+ dev: DEV_BRANCH
8576
+ };
8297
8577
  var MINIMAL_ENV_TEMPLATE = `# OpenMates self-host image-mode environment
8298
8578
  SECRET__MISTRAL_AI__API_KEY=
8299
8579
  SECRET__CEREBRAS__API_KEY=
@@ -8428,13 +8708,52 @@ function getPackageVersion() {
8428
8708
  return "";
8429
8709
  }
8430
8710
  }
8711
+ function getDefaultImageTagForVersion(version) {
8712
+ return version ? `v${version}` : DEV_BRANCH;
8713
+ }
8431
8714
  function getDefaultImageTag() {
8432
8715
  const version = getPackageVersion();
8433
- return version ? `v${version}` : "dev";
8716
+ return getDefaultImageTagForVersion(version);
8434
8717
  }
8435
8718
  function defaultTemplateRefForVersion(version) {
8436
8719
  return /-(alpha|beta|rc)(\.|\d|$)/.test(version) ? DEV_BRANCH : `v${version}`;
8437
8720
  }
8721
+ function templateRefForImageTag(imageTag, packageVersion = "") {
8722
+ const channelTag = IMAGE_CHANNEL_TAGS[imageTag];
8723
+ if (channelTag) return channelTag;
8724
+ if (imageTag.startsWith("v")) return defaultTemplateRefForVersion(imageTag.slice(1));
8725
+ if (!imageTag && packageVersion) return defaultTemplateRefForVersion(packageVersion);
8726
+ return DEV_BRANCH;
8727
+ }
8728
+ function resolveTargetImageTag(flags, currentTag, packageVersion) {
8729
+ const imageTag = flags["image-tag"];
8730
+ const channel = flags.channel;
8731
+ if (imageTag === true) {
8732
+ throw new Error("Provide an image tag value: --image-tag <tag>.");
8733
+ }
8734
+ if (channel === true) {
8735
+ throw new Error("Provide an update channel value: --channel stable, --channel main, or --channel dev.");
8736
+ }
8737
+ if (typeof imageTag === "string" && typeof channel === "string") {
8738
+ throw new Error("Use either --image-tag or --channel, not both.");
8739
+ }
8740
+ if (typeof imageTag === "string") {
8741
+ const trimmed = imageTag.trim();
8742
+ if (!trimmed) throw new Error("--image-tag cannot be empty.");
8743
+ return { tag: trimmed };
8744
+ }
8745
+ if (typeof channel === "string") {
8746
+ const normalized = channel.trim().toLowerCase();
8747
+ const tag = IMAGE_CHANNEL_TAGS[normalized];
8748
+ if (!tag) {
8749
+ throw new Error("Unsupported update channel. Use --channel stable, --channel main, or --channel dev.");
8750
+ }
8751
+ return { tag, channel: tag };
8752
+ }
8753
+ const installedChannel = IMAGE_CHANNEL_TAGS[currentTag];
8754
+ if (installedChannel) return { tag: installedChannel, channel: installedChannel };
8755
+ return { tag: getDefaultImageTagForVersion(packageVersion) };
8756
+ }
8438
8757
  function randomHex(bytes) {
8439
8758
  return randomBytes2(bytes).toString("hex");
8440
8759
  }
@@ -8475,7 +8794,7 @@ async function fetchText(url) {
8475
8794
  }
8476
8795
  return response.text();
8477
8796
  }
8478
- async function loadSelfHostComposeTemplate(version) {
8797
+ async function loadSelfHostComposeTemplate(templateRef) {
8479
8798
  const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
8480
8799
  if (templateDir) {
8481
8800
  return readFileSync5(join3(resolve3(templateDir), IMAGE_COMPOSE_FILE), "utf-8");
@@ -8484,16 +8803,15 @@ async function loadSelfHostComposeTemplate(version) {
8484
8803
  if (overrideUrl) {
8485
8804
  return fetchText(overrideUrl);
8486
8805
  }
8487
- const ref = defaultTemplateRefForVersion(version);
8488
8806
  return fetchText(
8489
- `https://raw.githubusercontent.com/glowingkitty/OpenMates/${ref}/backend/core/docker-compose.selfhost.yml`
8807
+ `https://raw.githubusercontent.com/glowingkitty/OpenMates/${templateRef}/backend/core/docker-compose.selfhost.yml`
8490
8808
  );
8491
8809
  }
8492
8810
  async function writeImageModeRuntimeFiles(installPath, imageTag) {
8493
8811
  const coreDir = join3(installPath, "backend", "core");
8494
8812
  const vaultConfigDir = join3(coreDir, "vault", "config");
8495
8813
  mkdirSync3(vaultConfigDir, { recursive: true });
8496
- writeFileSync3(join3(coreDir, "docker-compose.selfhost.yml"), await loadSelfHostComposeTemplate(getPackageVersion()));
8814
+ writeFileSync3(join3(coreDir, "docker-compose.selfhost.yml"), await loadSelfHostComposeTemplate(templateRefForImageTag(imageTag, getPackageVersion())));
8497
8815
  writeFileSync3(join3(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
8498
8816
  const envPath = join3(installPath, ".env");
8499
8817
  let envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : MINIMAL_ENV_TEMPLATE;
@@ -8511,6 +8829,48 @@ async function writeImageModeRuntimeFiles(installPath, imageTag) {
8511
8829
  writeFileSync3(envPath, envContent.endsWith("\n") ? envContent : `${envContent}
8512
8830
  `);
8513
8831
  }
8832
+ function getImageTagFromEnv(installPath, config) {
8833
+ const envPath = join3(installPath, ".env");
8834
+ if (existsSync5(envPath)) {
8835
+ const envTag = getEnvVar(readFileSync5(envPath, "utf-8"), "OPENMATES_IMAGE_TAG");
8836
+ if (envTag) return envTag;
8837
+ }
8838
+ return config?.imageTag ?? "";
8839
+ }
8840
+ function trailingSlashTrimmed(value) {
8841
+ return value.replace(/\/+$/, "");
8842
+ }
8843
+ function sleep2(ms) {
8844
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
8845
+ }
8846
+ async function checkUrl(url) {
8847
+ const controller = new AbortController();
8848
+ const timeout = setTimeout(() => controller.abort(), HEALTH_REQUEST_TIMEOUT_MS);
8849
+ try {
8850
+ const response = await fetch(url, { signal: controller.signal });
8851
+ return response.ok;
8852
+ } catch {
8853
+ return false;
8854
+ } finally {
8855
+ clearTimeout(timeout);
8856
+ }
8857
+ }
8858
+ async function waitForServerHealth(installPath) {
8859
+ const envPath = join3(installPath, ".env");
8860
+ const envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : "";
8861
+ const urls = deriveSelfHostCliUrls(envContent);
8862
+ const apiHealthUrl = `${trailingSlashTrimmed(urls.apiUrl)}/health`;
8863
+ const appUrl = trailingSlashTrimmed(urls.appUrl);
8864
+ const deadline = Date.now() + UPDATE_HEALTH_TIMEOUT_MS;
8865
+ while (Date.now() < deadline) {
8866
+ const [apiOk, appOk] = await Promise.all([checkUrl(apiHealthUrl), checkUrl(appUrl)]);
8867
+ if (apiOk && appOk) return;
8868
+ await sleep2(UPDATE_HEALTH_INTERVAL_MS);
8869
+ }
8870
+ throw new Error(
8871
+ `Updated containers started, but health checks did not pass in time. Check 'openmates server status' and 'openmates server logs --tail 200'. Tried ${apiHealthUrl} and ${appUrl}.`
8872
+ );
8873
+ }
8514
8874
  function defaultCloneBranchForVersion(version) {
8515
8875
  return /-(alpha|beta|rc)(\.|\d|$)/.test(version) ? DEV_BRANCH : null;
8516
8876
  }
@@ -8796,25 +9156,85 @@ CLI default API: ${cliUrls.apiUrl}`);
8796
9156
  }
8797
9157
  }
8798
9158
  async function serverUpdate(flags) {
8799
- requireDocker();
8800
9159
  const installPath = resolveServerPath(flags);
8801
- ensureGitWorkDirEnv(installPath);
8802
- console.error("Updating OpenMates...");
9160
+ const dryRun = flags["dry-run"] === true;
9161
+ if (!dryRun) ensureGitWorkDirEnv(installPath);
8803
9162
  const config = loadConfigForInstallPath(installPath);
8804
9163
  const withOverrides = config?.composeProfile === "full";
8805
9164
  const installMode = getInstallMode(installPath, config);
9165
+ if (installMode === "source" && (flags["image-tag"] !== void 0 || flags.channel !== void 0)) {
9166
+ throw new Error("--image-tag and --channel only apply to image-mode installs. Source-mode installs update from Git.");
9167
+ }
9168
+ if (!dryRun) requireDocker();
9169
+ console.error("Updating OpenMates...");
8806
9170
  if (installMode === "image") {
9171
+ const currentTag = getImageTagFromEnv(installPath, config);
9172
+ const target = resolveTargetImageTag(flags, currentTag, getPackageVersion());
9173
+ const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
9174
+ const plan = {
9175
+ command: "update",
9176
+ path: installPath,
9177
+ mode: "image",
9178
+ currentImageTag: currentTag || null,
9179
+ targetImageTag: target.tag,
9180
+ channel: target.channel ?? null,
9181
+ templateRef,
9182
+ dryRun
9183
+ };
9184
+ if (dryRun) {
9185
+ if (flags.json === true) {
9186
+ printJson({ ...plan, status: "planned" });
9187
+ } else {
9188
+ console.log("Update plan:");
9189
+ console.log(` Mode: image`);
9190
+ console.log(` Current tag: ${currentTag || "unknown"}`);
9191
+ console.log(` Target tag: ${target.tag}`);
9192
+ console.log(` Template ref: ${templateRef}`);
9193
+ console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
9194
+ }
9195
+ return;
9196
+ }
9197
+ console.error(`Mode: image`);
9198
+ console.error(`Current image tag: ${currentTag || "unknown"}`);
9199
+ console.error(`Target image tag: ${target.tag}`);
9200
+ console.error(`Refreshing self-host runtime files from ${templateRef}...`);
9201
+ await writeImageModeRuntimeFiles(installPath, target.tag);
8807
9202
  const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
8808
- console.error("Pulling prebuilt images...");
8809
- let code2 = await runInteractive("docker", pullArgs, installPath);
8810
- if (code2 !== 0) process.exit(code2);
9203
+ let code2 = 0;
9204
+ if (shouldPullImages()) {
9205
+ console.error("Pulling prebuilt images...");
9206
+ code2 = await runInteractive("docker", pullArgs, installPath);
9207
+ if (code2 !== 0) process.exit(code2);
9208
+ } else {
9209
+ console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
9210
+ }
8811
9211
  const upArgs2 = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
8812
9212
  code2 = await runInteractive("docker", upArgs2, installPath);
8813
9213
  if (code2 !== 0) process.exit(code2);
9214
+ console.error("Waiting for API and web health checks...");
9215
+ try {
9216
+ await waitForServerHealth(installPath);
9217
+ } catch (error) {
9218
+ await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
9219
+ throw error;
9220
+ }
9221
+ if (config) {
9222
+ saveServerConfig({ ...config, imageTag: target.tag, imageChannel: target.channel });
9223
+ }
8814
9224
  if (flags.json === true) {
8815
- printJson({ command: "update", status: "success", path: installPath, mode: "image" });
9225
+ printJson({ ...plan, status: "success", dryRun: false });
8816
9226
  } else {
8817
- console.log("Server images pulled and containers restarted.");
9227
+ console.log("Server images updated, containers restarted, and health checks passed.");
9228
+ }
9229
+ return;
9230
+ }
9231
+ if (dryRun) {
9232
+ if (flags.json === true) {
9233
+ printJson({ command: "update", status: "planned", path: installPath, mode: "source", dryRun: true });
9234
+ } else {
9235
+ console.log("Update plan:");
9236
+ console.log(" Mode: source");
9237
+ console.log(" Commands: git pull --ff-only, docker compose build, docker compose up -d, health checks");
8818
9238
  }
8819
9239
  return;
8820
9240
  }
@@ -8829,7 +9249,7 @@ async function serverUpdate(flags) {
8829
9249
  try {
8830
9250
  const pullOutput = exec("git pull --ff-only", installPath);
8831
9251
  console.error(pullOutput || "Already up to date.");
8832
- } catch (e) {
9252
+ } catch {
8833
9253
  if (flags.force === true) {
8834
9254
  try {
8835
9255
  exec("git stash pop", installPath);
@@ -8853,10 +9273,17 @@ async function serverUpdate(flags) {
8853
9273
  const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
8854
9274
  code = await runInteractive("docker", upArgs, installPath);
8855
9275
  if (code !== 0) process.exit(code);
9276
+ console.error("Waiting for API and web health checks...");
9277
+ try {
9278
+ await waitForServerHealth(installPath);
9279
+ } catch (error) {
9280
+ await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
9281
+ throw error;
9282
+ }
8856
9283
  if (flags.json === true) {
8857
- printJson({ command: "update", status: "success", path: installPath });
9284
+ printJson({ command: "update", status: "success", path: installPath, mode: "source" });
8858
9285
  } else {
8859
- console.log("Server updated and restarted.");
9286
+ console.log("Server updated, restarted, and health checks passed.");
8860
9287
  }
8861
9288
  }
8862
9289
  async function serverReset(flags) {
@@ -9038,7 +9465,10 @@ Command Options:
9038
9465
  --tail <n> Number of lines to show (default: 100)
9039
9466
 
9040
9467
  update:
9041
- --force Stash local changes before pulling
9468
+ --dry-run Show update plan without changing files or containers
9469
+ --image-tag <tag> Image mode: update to a specific prebuilt image tag
9470
+ --channel <name> Image mode: update using stable/main or dev channel tags
9471
+ --force Source mode: stash local changes before pulling
9042
9472
 
9043
9473
  reset:
9044
9474
  --delete-user-data-only Only delete database and cache (preserve config)
@@ -9057,6 +9487,9 @@ Examples:
9057
9487
  openmates server logs --container api --follow
9058
9488
  openmates server make-admin user@example.com
9059
9489
  openmates server update
9490
+ openmates server update --dry-run
9491
+ openmates server update --image-tag v0.12.0-alpha.1
9492
+ openmates server update --channel dev
9060
9493
  openmates server restart --rebuild
9061
9494
  `.trim());
9062
9495
  }
@@ -22151,6 +22584,18 @@ var memoryTravelPreferredTransportChat = {
22151
22584
  "content": "example_chats.memory_travel_preferred_transport.message_1",
22152
22585
  "created_at": 1781280405
22153
22586
  },
22587
+ {
22588
+ "id": "32b6170b-a0f8-4e21-b48f-581782e5957e-memory-response",
22589
+ "role": "system",
22590
+ "content": '{"type":"app_settings_memories_response","user_message_id":"32b6170b-a0f8-4e21-b48f-581782e5957e","action":"included","categories":[{"appId":"travel","itemType":"preferred_transport_methods","entryCount":2}]}',
22591
+ "created_at": 1781280406
22592
+ },
22593
+ {
22594
+ "id": "32b6170b-a0f8-4e21-b48f-581782e5957e-memory-request",
22595
+ "role": "system",
22596
+ "content": '{"type":"app_settings_memories_request","user_message_id":"32b6170b-a0f8-4e21-b48f-581782e5957e","request_id":"32b6170b-a0f8-4e21-b48f-581782e5957e-memory-request","requested_keys":["travel-preferred_transport_methods"],"categories":[{"appId":"travel","itemType":"preferred_transport_methods","entryCount":0}]}',
22597
+ "created_at": 1781280406
22598
+ },
22154
22599
  {
22155
22600
  "id": "6fad1476-1f2f-4977-97ca-b9f5ba773f3d",
22156
22601
  "role": "assistant",
@@ -22185,6 +22630,18 @@ var memoryTravelPreferredActivitiesChat = {
22185
22630
  "content": "example_chats.memory_travel_preferred_activities.message_1",
22186
22631
  "created_at": 1781280434
22187
22632
  },
22633
+ {
22634
+ "id": "b59b7592-a869-4bda-ab68-e87a1663ed11-memory-response",
22635
+ "role": "system",
22636
+ "content": '{"type":"app_settings_memories_response","user_message_id":"b59b7592-a869-4bda-ab68-e87a1663ed11","action":"included","categories":[{"appId":"travel","itemType":"preferred_activities","entryCount":2}]}',
22637
+ "created_at": 1781280435
22638
+ },
22639
+ {
22640
+ "id": "b59b7592-a869-4bda-ab68-e87a1663ed11-memory-request",
22641
+ "role": "system",
22642
+ "content": '{"type":"app_settings_memories_request","user_message_id":"b59b7592-a869-4bda-ab68-e87a1663ed11","request_id":"b59b7592-a869-4bda-ab68-e87a1663ed11-memory-request","requested_keys":["travel-preferred_activities"],"categories":[{"appId":"travel","itemType":"preferred_activities","entryCount":0}]}',
22643
+ "created_at": 1781280435
22644
+ },
22188
22645
  {
22189
22646
  "id": "5dfa7876-6b55-47f8-a472-fd542e19589d",
22190
22647
  "role": "assistant",
@@ -22219,6 +22676,18 @@ var memoryTvWatchedMoviesChat = {
22219
22676
  "content": "example_chats.memory_tv_watched_movies.message_1",
22220
22677
  "created_at": 1781280521
22221
22678
  },
22679
+ {
22680
+ "id": "7909540d-9830-48b0-a7ea-ec3438cb7ff6-memory-response",
22681
+ "role": "system",
22682
+ "content": '{"type":"app_settings_memories_response","user_message_id":"7909540d-9830-48b0-a7ea-ec3438cb7ff6","action":"included","categories":[{"appId":"tv","itemType":"watched_movies","entryCount":2}]}',
22683
+ "created_at": 1781280522
22684
+ },
22685
+ {
22686
+ "id": "7909540d-9830-48b0-a7ea-ec3438cb7ff6-memory-request",
22687
+ "role": "system",
22688
+ "content": '{"type":"app_settings_memories_request","user_message_id":"7909540d-9830-48b0-a7ea-ec3438cb7ff6","request_id":"7909540d-9830-48b0-a7ea-ec3438cb7ff6-memory-request","requested_keys":["tv-watched_movies"],"categories":[{"appId":"tv","itemType":"watched_movies","entryCount":0}]}',
22689
+ "created_at": 1781280522
22690
+ },
22222
22691
  {
22223
22692
  "id": "0bc111c3-32e3-4451-aa54-cae5dfcd1d8a",
22224
22693
  "role": "assistant",
@@ -22253,6 +22722,18 @@ var memoryTvWatchedShowsChat = {
22253
22722
  "content": "example_chats.memory_tv_watched_shows.message_1",
22254
22723
  "created_at": 1781280553
22255
22724
  },
22725
+ {
22726
+ "id": "e54287c0-54dc-4961-93f1-0caed8596ba7-memory-response",
22727
+ "role": "system",
22728
+ "content": '{"type":"app_settings_memories_response","user_message_id":"e54287c0-54dc-4961-93f1-0caed8596ba7","action":"included","categories":[{"appId":"tv","itemType":"watched_tv_shows","entryCount":2}]}',
22729
+ "created_at": 1781280554
22730
+ },
22731
+ {
22732
+ "id": "e54287c0-54dc-4961-93f1-0caed8596ba7-memory-request",
22733
+ "role": "system",
22734
+ "content": '{"type":"app_settings_memories_request","user_message_id":"e54287c0-54dc-4961-93f1-0caed8596ba7","request_id":"e54287c0-54dc-4961-93f1-0caed8596ba7-memory-request","requested_keys":["tv-watched_tv_shows"],"categories":[{"appId":"tv","itemType":"watched_tv_shows","entryCount":0}]}',
22735
+ "created_at": 1781280554
22736
+ },
22256
22737
  {
22257
22738
  "id": "7cf4ba64-2134-4341-9a65-ac3ef207349e",
22258
22739
  "role": "assistant",
@@ -22287,6 +22768,18 @@ var memoryTvToWatchListChat = {
22287
22768
  "content": "example_chats.memory_tv_to_watch_list.message_1",
22288
22769
  "created_at": 1781280581
22289
22770
  },
22771
+ {
22772
+ "id": "794b4c25-ad08-4088-adb4-5729b5fc89d2-memory-response",
22773
+ "role": "system",
22774
+ "content": '{"type":"app_settings_memories_response","user_message_id":"794b4c25-ad08-4088-adb4-5729b5fc89d2","action":"included","categories":[{"appId":"tv","itemType":"to_watch_list","entryCount":2}]}',
22775
+ "created_at": 1781280582
22776
+ },
22777
+ {
22778
+ "id": "794b4c25-ad08-4088-adb4-5729b5fc89d2-memory-request",
22779
+ "role": "system",
22780
+ "content": '{"type":"app_settings_memories_request","user_message_id":"794b4c25-ad08-4088-adb4-5729b5fc89d2","request_id":"794b4c25-ad08-4088-adb4-5729b5fc89d2-memory-request","requested_keys":["tv-to_watch_list"],"categories":[{"appId":"tv","itemType":"to_watch_list","entryCount":0}]}',
22781
+ "created_at": 1781280582
22782
+ },
22290
22783
  {
22291
22784
  "id": "4c8d7bb0-2b55-4428-aa22-c9ab6aa8f01e",
22292
22785
  "role": "assistant",
@@ -22321,6 +22814,18 @@ var memoryVideosToWatchListChat = {
22321
22814
  "content": "example_chats.memory_videos_to_watch_list.message_1",
22322
22815
  "created_at": 1781280606
22323
22816
  },
22817
+ {
22818
+ "id": "9df4f00f-8468-482f-886c-4c323fdc7ef8-memory-response",
22819
+ "role": "system",
22820
+ "content": '{"type":"app_settings_memories_response","user_message_id":"9df4f00f-8468-482f-886c-4c323fdc7ef8","action":"included","categories":[{"appId":"videos","itemType":"to_watch_list","entryCount":2}]}',
22821
+ "created_at": 1781280607
22822
+ },
22823
+ {
22824
+ "id": "9df4f00f-8468-482f-886c-4c323fdc7ef8-memory-request",
22825
+ "role": "system",
22826
+ "content": '{"type":"app_settings_memories_request","user_message_id":"9df4f00f-8468-482f-886c-4c323fdc7ef8","request_id":"9df4f00f-8468-482f-886c-4c323fdc7ef8-memory-request","requested_keys":["videos-to_watch_list"],"categories":[{"appId":"videos","itemType":"to_watch_list","entryCount":0}]}',
22827
+ "created_at": 1781280607
22828
+ },
22324
22829
  {
22325
22830
  "id": "16c2890e-358f-419f-a607-98d8ecb447b2",
22326
22831
  "role": "assistant",
@@ -22355,6 +22860,18 @@ var memoryWebBookmarksChat = {
22355
22860
  "content": "example_chats.memory_web_bookmarks.message_1",
22356
22861
  "created_at": 1781280634
22357
22862
  },
22863
+ {
22864
+ "id": "9a448c41-309c-49ef-a90d-ae00b16b1e10-memory-response",
22865
+ "role": "system",
22866
+ "content": '{"type":"app_settings_memories_response","user_message_id":"9a448c41-309c-49ef-a90d-ae00b16b1e10","action":"included","categories":[{"appId":"web","itemType":"bookmarks","entryCount":2}]}',
22867
+ "created_at": 1781280635
22868
+ },
22869
+ {
22870
+ "id": "9a448c41-309c-49ef-a90d-ae00b16b1e10-memory-request",
22871
+ "role": "system",
22872
+ "content": '{"type":"app_settings_memories_request","user_message_id":"9a448c41-309c-49ef-a90d-ae00b16b1e10","request_id":"9a448c41-309c-49ef-a90d-ae00b16b1e10-memory-request","requested_keys":["web-bookmarks"],"categories":[{"appId":"web","itemType":"bookmarks","entryCount":0}]}',
22873
+ "created_at": 1781280635
22874
+ },
22358
22875
  {
22359
22876
  "id": "f3be11d8-e466-4649-8078-ee5f6ad7920e",
22360
22877
  "role": "assistant",
@@ -22389,6 +22906,18 @@ var memoryWebReadLaterChat = {
22389
22906
  "content": "example_chats.memory_web_read_later.message_1",
22390
22907
  "created_at": 1781280720
22391
22908
  },
22909
+ {
22910
+ "id": "7365e817-390f-4e70-a020-fa4eef5f637c-memory-response",
22911
+ "role": "system",
22912
+ "content": '{"type":"app_settings_memories_response","user_message_id":"7365e817-390f-4e70-a020-fa4eef5f637c","action":"included","categories":[{"appId":"web","itemType":"read_later","entryCount":2}]}',
22913
+ "created_at": 1781280721
22914
+ },
22915
+ {
22916
+ "id": "7365e817-390f-4e70-a020-fa4eef5f637c-memory-request",
22917
+ "role": "system",
22918
+ "content": '{"type":"app_settings_memories_request","user_message_id":"7365e817-390f-4e70-a020-fa4eef5f637c","request_id":"7365e817-390f-4e70-a020-fa4eef5f637c-memory-request","requested_keys":["web-read_later"],"categories":[{"appId":"web","itemType":"read_later","entryCount":0}]}',
22919
+ "created_at": 1781280721
22920
+ },
22392
22921
  {
22393
22922
  "id": "b1bdbb77-38e4-4974-ac51-4fff6ea5c6fd",
22394
22923
  "role": "assistant",
@@ -22892,6 +23421,108 @@ var classicCarReverseImageSearchChat = {
22892
23421
  }
22893
23422
  };
22894
23423
 
23424
+ // ../ui/src/demo_chats/data/example_chats/usb-c-3v3-ldo-pcb-schematic.ts
23425
+ var usbC3v3LdoPcbSchematicChat = {
23426
+ chat_id: "example-usb-c-3v3-ldo",
23427
+ slug: "usb-c-3v3-ldo-pcb-schematic",
23428
+ title: "example_chats.usb_c_3v3_ldo_pcb_schematic.title",
23429
+ summary: "example_chats.usb_c_3v3_ldo_pcb_schematic.summary",
23430
+ icon: "circuit-board",
23431
+ category: "software_development",
23432
+ keywords: ["PCB schematic", "Atopile", "USB-C", "LDO", "3.3V regulator", "electronics design"],
23433
+ follow_up_suggestions: ["example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_1", "example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_2", "example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_3", "example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_4", "example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_5", "example_chats.usb_c_3v3_ldo_pcb_schematic.follow_up_6"],
23434
+ messages: [
23435
+ {
23436
+ "id": "verified-pcb-user-1",
23437
+ "role": "user",
23438
+ "content": "example_chats.usb_c_3v3_ldo_pcb_schematic.message_1",
23439
+ "created_at": 1781772935
23440
+ },
23441
+ {
23442
+ "id": "verified-pcb-assistant-1",
23443
+ "role": "assistant",
23444
+ "content": "example_chats.usb_c_3v3_ldo_pcb_schematic.message_2",
23445
+ "created_at": 1781772936,
23446
+ "category": "software_development",
23447
+ "model_name": "Gemini 3.1 Pro"
23448
+ }
23449
+ ],
23450
+ embeds: [
23451
+ {
23452
+ "embed_id": "46d63593-516e-49b5-8e7d-600c4fe18836",
23453
+ "type": "pcb_schematic",
23454
+ "content": 'type: pcb_schematic\napp_id: electronics\nskill_id: schematic\nlanguage: atopile\ncode: "import Resistor\\nimport Capacitor\\nimport Diode\\nimport ElectricPower\\n\\n# Modeling the USB-C input behavior\\nmodule USBCInput:\\n power = new ElectricPower\\n signal vbus\\n signal gnd\\n \\n # Associate the interface pins with local power signals\\n power.vcc ~ vbus\\n power.gnd ~ gnd\\n\\n# Modeling the LDO regulator behavior\\nmodule LDO:\\n power_in = new ElectricPower\\n power_out = new ElectricPower\\n \\n # Regulators typically share a common ground reference\\n power_in.gnd ~ power_out.gnd\\n\\n# Top-level application module\\nmodule App:\\n # 1. Instantiate functional blocks\\n usb_input = new USBCInput\\n regulator = new LDO\\n\\n # 2. Establish main power path (5V Input to LDO)\\n usb_input.power ~ regulator.power_in\\n\\n # 3. Input Decoupling Capacitor (5V rail)\\n c_in = new Capacitor\\n c_in.capacitance = 10uF +/- 20%\\n regulator.power_in.vcc ~ c_in.unnamed[0]\\n regulator.power_in.gnd ~ c_in.unnamed[1]\\n\\n # 4. Output Decoupling Capacitor (3.3V rail)\\n c_out = new Capacitor\\n c_out.capacitance = 1uF +/- 20%\\n regulator.power_out.vcc ~ c_out.unnamed[0]\\n regulator.power_out.gnd ~ c_out.unnamed[1]\\n\\n # 5. Power Status Indicator (Modeled as Diode) on the 3.3V Rail\\n status_led = new Diode\\n limit_res = new Resistor\\n limit_res.resistance = 1kohm +/- 5%\\n\\n # Connections: VCC -> Diode Anode -> Diode Cathode -> Resistor -> GND\\n regulator.power_out.vcc ~ status_led.anode\\n status_led.cathode ~ limit_res.unnamed[0]\\n limit_res.unnamed[1] ~ regulator.power_out.gnd\\n"\nfilename: main.ato\ntitle: USB-C 3.3V LDO PCB Schematic\nmodule_name: App\nembed_ref: main.ato-46d635\nstatus: finished\nline_count: 54\ncompile_id: null\ncompile_status: null\nartifact_manifest: null\natopile_version: 0.15.7\natopile_docs_version: 0.15.7\ncompile_logs: null',
23455
+ "parent_embed_id": null,
23456
+ "embed_ids": null
23457
+ }
23458
+ ],
23459
+ metadata: {
23460
+ featured: true,
23461
+ order: 103
23462
+ }
23463
+ };
23464
+
23465
+ // ../ui/src/demo_chats/data/example_chats/berlin-morning-bike-forecast.ts
23466
+ var berlinMorningBikeForecastChat = {
23467
+ chat_id: "example-berlin-morning-bike-forecast",
23468
+ slug: "berlin-morning-bike-forecast",
23469
+ title: "example_chats.berlin_morning_bike_forecast.title",
23470
+ summary: "example_chats.berlin_morning_bike_forecast.summary",
23471
+ icon: "bike",
23472
+ category: "general_knowledge",
23473
+ keywords: ["Berlin weather", "Tempelhofer Feld", "bike commute", "weather forecast"],
23474
+ follow_up_suggestions: ["example_chats.berlin_morning_bike_forecast.follow_up_1", "example_chats.berlin_morning_bike_forecast.follow_up_2", "example_chats.berlin_morning_bike_forecast.follow_up_3", "example_chats.berlin_morning_bike_forecast.follow_up_4", "example_chats.berlin_morning_bike_forecast.follow_up_5", "example_chats.berlin_morning_bike_forecast.follow_up_6"],
23475
+ messages: [
23476
+ {
23477
+ "id": "40fe35ff-396a-40aa-ae7e-463067a91833",
23478
+ "role": "user",
23479
+ "content": "example_chats.berlin_morning_bike_forecast.message_1",
23480
+ "created_at": 1781772332
23481
+ },
23482
+ {
23483
+ "id": "560ef618-e8df-4bd2-99fb-68a737cee232",
23484
+ "role": "assistant",
23485
+ "content": "example_chats.berlin_morning_bike_forecast.message_2",
23486
+ "created_at": 1781772346,
23487
+ "user_message_id": "40fe35ff-396a-40aa-ae7e-463067a91833",
23488
+ "response_credits": 25,
23489
+ "category": "general_knowledge",
23490
+ "model_name": "Gemini 3 Flash"
23491
+ }
23492
+ ],
23493
+ embeds: [
23494
+ {
23495
+ "embed_id": "324eba33-3622-409c-b401-183f95d6183e",
23496
+ "type": "app_skill_use",
23497
+ "content": 'app_id: weather\nskill_id: forecast\nresult_count: 2\nembed_ids: c72c110f-9513-4ce8-a943-d66888c6bad0|4d855526-af41-47cf-a354-c769f3098c94\nstatus: finished\nembed_id: 324eba33-3622-409c-b401-183f95d6183e\ndays: 2\nlocation: Berlin\nquery: Berlin weather forecast\ndays_requested: 2\nprovider: Deutscher Wetterdienst (DWD) + Open-Meteo\npreview_results[2]{title,source_provider,source_station_name,source_dwd_station_id,source_wmo_station_id,source_observation_type,source_distance_m,source_lat,source_lon,source_height_m,source_first_record,source_last_record,date,provider}:\n Berlin weather 2026-06-18,Deutscher Wetterdienst (DWD),BERLIN-ALEX.,"00399","10389",forecast,805.0,52.52,13.42,37.0,"2026-06-18T08:00:00+02:00","2026-06-28T12:00:00+02:00",2026-06-18,Deutscher Wetterdienst (DWD)\n Berlin weather 2026-06-19,Deutscher Wetterdienst (DWD),BERLIN-ALEX.,"00399","10389",forecast,805.0,52.52,13.42,37.0,"2026-06-18T08:00:00+02:00","2026-06-28T12:00:00+02:00",2026-06-19,Deutscher Wetterdienst (DWD)',
23498
+ "parent_embed_id": null,
23499
+ "embed_ids": [
23500
+ "c72c110f-9513-4ce8-a943-d66888c6bad0",
23501
+ "4d855526-af41-47cf-a354-c769f3098c94"
23502
+ ]
23503
+ },
23504
+ {
23505
+ "embed_id": "c72c110f-9513-4ce8-a943-d66888c6bad0",
23506
+ "type": "weather_day",
23507
+ "content": 'type: weather_day\ntitle: Berlin weather 2026-06-18\ndate: 2026-06-18\nlabel: today\nlocation_name: Berlin\ncountry_code: DE\ntimezone: Europe/Berlin\nprovider: Deutscher Wetterdienst (DWD)\ncondition: dry\nicon: partly-cloudy-day\ntemperature_min_c: 20.1\ntemperature_max_c: 28.3\nprecipitation_total_mm: 0\nprecipitation_probability_max_pct: 7\nrain_hours: 0\nwind_speed_max_kmh: 15.8\nwind_gust_speed_max_kmh: 31.5\ncloud_cover_avg_pct: 79\nrelative_humidity_avg_pct: 69.2\nhourly[24]:\n - time: "00:00"\n timestamp: "2026-06-18T00:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 21.0\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 65\n dew_point_c: 14.1\n pressure_msl_hpa: 1018.9\n visibility_m: 39690\n wind_direction_deg: 290\n wind_speed_kmh: 6.8\n wind_gust_direction_deg: 280\n wind_gust_speed_kmh: 10.4\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 303712\n fallback_source_ids_sunshine: 6885\n fallback_source_ids_solar: 255262\n fallback_source_ids_dew_point: 6150\n fallback_source_ids_relative_humidity: 6150\n fallback_source_ids_wind_direction: 6150\n fallback_source_ids_precipitation: 6150\n fallback_source_ids_condition: 6150\n fallback_source_ids_temperature: 6150\n fallback_source_ids_wind_speed: 6150\n - time: "01:00"\n timestamp: "2026-06-18T01:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 21.0\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 65\n dew_point_c: 14.2\n pressure_msl_hpa: 1018.9\n visibility_m: 41770\n wind_direction_deg: 240\n wind_speed_kmh: 6.1\n wind_gust_direction_deg: 230\n wind_gust_speed_kmh: 8.6\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 303712\n fallback_source_ids_sunshine: 6885\n fallback_source_ids_solar: 255262\n fallback_source_ids_dew_point: 6150\n fallback_source_ids_relative_humidity: 6150\n fallback_source_ids_wind_direction: 6150\n fallback_source_ids_precipitation: 6150\n fallback_source_ids_condition: 6150\n fallback_source_ids_temperature: 6150\n fallback_source_ids_wind_speed: 6150\n - time: "02:00"\n timestamp: "2026-06-18T02:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 20.5\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 68\n dew_point_c: 14.4\n pressure_msl_hpa: 1019.1\n visibility_m: 42400\n wind_direction_deg: 240\n wind_speed_kmh: 6.1\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 9.0\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "03:00"\n timestamp: "2026-06-18T03:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 20.5\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 68\n dew_point_c: 14.5\n pressure_msl_hpa: 1019.1\n visibility_m: 43300\n wind_direction_deg: 240\n wind_speed_kmh: 6.1\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 7.9\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "04:00"\n timestamp: "2026-06-18T04:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 20.4\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 70\n dew_point_c: 14.7\n pressure_msl_hpa: 1018.9\n visibility_m: 39100\n wind_direction_deg: 230\n wind_speed_kmh: 6.8\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 11.9\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "05:00"\n timestamp: "2026-06-18T05:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 20.1\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 72\n dew_point_c: 15.0\n pressure_msl_hpa: 1018.8\n visibility_m: 14400\n wind_direction_deg: 230\n wind_speed_kmh: 5.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 11.2\n sunshine_minutes: 0.0\n solar: 0.001\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "06:00"\n timestamp: "2026-06-18T06:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 20.2\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 73\n dew_point_c: 15.3\n pressure_msl_hpa: 1019.0\n visibility_m: 32500\n wind_direction_deg: 240\n wind_speed_kmh: 9.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 15.1\n sunshine_minutes: 0.0\n solar: 0.014\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "07:00"\n timestamp: "2026-06-18T07:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 20.7\n precipitation_mm: 0.0\n precipitation_probability_pct: null\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 75\n relative_humidity_pct: 74\n dew_point_c: 15.8\n pressure_msl_hpa: 1018.9\n visibility_m: 37200\n wind_direction_deg: 230\n wind_speed_kmh: 7.9\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 15.8\n sunshine_minutes: 6.0\n solar: 0.069\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n - time: "08:00"\n timestamp: "2026-06-18T08:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 22.2\n precipitation_mm: 0.0\n precipitation_probability_pct: 7\n precipitation_probability_6h_pct: 10\n cloud_cover_pct: 75\n relative_humidity_pct: 68\n dew_point_c: 16.1\n pressure_msl_hpa: 1019.1\n visibility_m: 54700\n wind_direction_deg: 280\n wind_speed_kmh: 14.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 22.0\n sunshine_minutes: 36.0\n solar: 0.218\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n fallback_source_ids_precipitation_probability_6h: 2382\n fallback_source_ids_precipitation_probability: 2382\n - time: "09:00"\n timestamp: "2026-06-18T09:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 22.4\n precipitation_mm: 0.0\n precipitation_probability_pct: 6\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 70\n dew_point_c: 16.6\n pressure_msl_hpa: 1019.6\n visibility_m: 59800\n wind_direction_deg: 280\n wind_speed_kmh: 15.8\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 27.0\n sunshine_minutes: 12.0\n solar: 0.248\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n fallback_source_ids_precipitation_probability: 2382\n - time: "10:00"\n timestamp: "2026-06-18T10:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 23.2\n precipitation_mm: 0.0\n precipitation_probability_pct: 5\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 100\n relative_humidity_pct: 68\n dew_point_c: 17.0\n pressure_msl_hpa: 1019.7\n visibility_m: 45500\n wind_direction_deg: 280\n wind_speed_kmh: 15.8\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 27.0\n sunshine_minutes: 42.0\n solar: 0.517\n source_id: 6150\n fallback_source_ids_sunshine: 6148\n fallback_source_ids_solar: 6147\n fallback_source_ids_precipitation_probability: 2382\n - time: "11:00"\n timestamp: "2026-06-18T11:00:00+02:00"\n condition: dry\n icon: cloudy\n temperature_c: 23.7\n precipitation_mm: 0.0\n precipitation_probability_pct: 5\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 82\n relative_humidity_pct: null\n dew_point_c: 16.3\n pressure_msl_hpa: 1019.4\n visibility_m: 33400\n wind_direction_deg: 269\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 27.8\n sunshine_minutes: 32.0\n solar: 0.514\n source_id: 2382\n fallback_source_ids: null\n - time: "12:00"\n timestamp: "2026-06-18T12:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 24.6\n precipitation_mm: 0.0\n precipitation_probability_pct: 3\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 78\n relative_humidity_pct: null\n dew_point_c: 16.4\n pressure_msl_hpa: 1019.0\n visibility_m: 32800\n wind_direction_deg: 274\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 29.6\n sunshine_minutes: 32.0\n solar: 0.6\n source_id: 2382\n fallback_source_ids: null\n - time: "13:00"\n timestamp: "2026-06-18T13:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 25.4\n precipitation_mm: 0.0\n precipitation_probability_pct: 4\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 74\n relative_humidity_pct: null\n dew_point_c: 16.4\n pressure_msl_hpa: 1018.7\n visibility_m: 31600\n wind_direction_deg: 282\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 31.5\n sunshine_minutes: 33.0\n solar: 0.653\n source_id: 2382\n fallback_source_ids: null\n - time: "14:00"\n timestamp: "2026-06-18T14:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 26.4\n precipitation_mm: 0.0\n precipitation_probability_pct: 3\n precipitation_probability_6h_pct: 8\n cloud_cover_pct: 72\n relative_humidity_pct: null\n dew_point_c: 16.0\n pressure_msl_hpa: 1018.4\n visibility_m: 32600\n wind_direction_deg: 283\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 31.5\n sunshine_minutes: 34.0\n solar: 0.656\n source_id: 2382\n fallback_source_ids: null\n - time: "15:00"\n timestamp: "2026-06-18T15:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 27.1\n precipitation_mm: 0.0\n precipitation_probability_pct: 4\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 71\n relative_humidity_pct: null\n dew_point_c: 15.9\n pressure_msl_hpa: 1018.1\n visibility_m: 35000\n wind_direction_deg: 288\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 31.5\n sunshine_minutes: 34.0\n solar: 0.614\n source_id: 2382\n fallback_source_ids: null\n - time: "16:00"\n timestamp: "2026-06-18T16:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 27.5\n precipitation_mm: 0.0\n precipitation_probability_pct: 3\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 70\n relative_humidity_pct: null\n dew_point_c: 15.9\n pressure_msl_hpa: 1017.8\n visibility_m: 34900\n wind_direction_deg: 291\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 31.5\n sunshine_minutes: 37.0\n solar: 0.564\n source_id: 2382\n fallback_source_ids: null\n - time: "17:00"\n timestamp: "2026-06-18T17:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 28.1\n precipitation_mm: 0.0\n precipitation_probability_pct: 1\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 67\n relative_humidity_pct: null\n dew_point_c: 15.7\n pressure_msl_hpa: 1017.6\n visibility_m: 35200\n wind_direction_deg: 294\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 31.5\n sunshine_minutes: 42.0\n solar: 0.517\n source_id: 2382\n fallback_source_ids: null\n - time: "18:00"\n timestamp: "2026-06-18T18:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 28.3\n precipitation_mm: 0.0\n precipitation_probability_pct: 1\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 63\n relative_humidity_pct: null\n dew_point_c: 15.6\n pressure_msl_hpa: 1017.3\n visibility_m: 36200\n wind_direction_deg: 299\n wind_speed_kmh: 13.0\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 29.6\n sunshine_minutes: 43.0\n solar: 0.422\n source_id: 2382\n fallback_source_ids: null\n - time: "19:00"\n timestamp: "2026-06-18T19:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 28.1\n precipitation_mm: 0.0\n precipitation_probability_pct: 1\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 62\n relative_humidity_pct: null\n dew_point_c: 15.7\n pressure_msl_hpa: 1017.3\n visibility_m: 36600\n wind_direction_deg: 304\n wind_speed_kmh: 11.1\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 25.9\n sunshine_minutes: 37.0\n solar: 0.297\n source_id: 2382\n fallback_source_ids: null\n - time: "20:00"\n timestamp: "2026-06-18T20:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 27.2\n precipitation_mm: 0.0\n precipitation_probability_pct: 1\n precipitation_probability_6h_pct: 2\n cloud_cover_pct: 56\n relative_humidity_pct: null\n dew_point_c: 15.8\n pressure_msl_hpa: 1017.3\n visibility_m: 37300\n wind_direction_deg: 311\n wind_speed_kmh: 11.1\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 24.1\n sunshine_minutes: 33.0\n solar: 0.172\n source_id: 2382\n fallback_source_ids: null\n - time: "21:00"\n timestamp: "2026-06-18T21:00:00+02:00"\n condition: dry\n icon: partly-cloudy-day\n temperature_c: 25.8\n precipitation_mm: 0.0\n precipitation_probability_pct: 2\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 55\n relative_humidity_pct: null\n dew_point_c: 15.9\n pressure_msl_hpa: 1017.4\n visibility_m: 37100\n wind_direction_deg: 337\n wind_speed_kmh: 9.3\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 20.4\n sunshine_minutes: 22.0\n solar: 0.058\n source_id: 2382\n fallback_source_ids: null\n - time: "22:00"\n timestamp: "2026-06-18T22:00:00+02:00"\n condition: dry\n icon: partly-cloudy-night\n temperature_c: 24.0\n precipitation_mm: 0.0\n precipitation_probability_pct: 2\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 50\n relative_humidity_pct: null\n dew_point_c: 16.1\n pressure_msl_hpa: 1017.7\n visibility_m: 36100\n wind_direction_deg: 17\n wind_speed_kmh: 9.3\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 16.7\n sunshine_minutes: 11.0\n solar: 0.006\n source_id: 2382\n fallback_source_ids: null\n - time: "23:00"\n timestamp: "2026-06-18T23:00:00+02:00"\n condition: dry\n icon: partly-cloudy-night\n temperature_c: 22.6\n precipitation_mm: 0.0\n precipitation_probability_pct: 1\n precipitation_probability_6h_pct: null\n cloud_cover_pct: 46\n relative_humidity_pct: null\n dew_point_c: 16.4\n pressure_msl_hpa: 1018.0\n visibility_m: 35700\n wind_direction_deg: 61\n wind_speed_kmh: 9.3\n wind_gust_direction_deg: null\n wind_gust_speed_kmh: 16.7\n sunshine_minutes: 0.0\n solar: 0.0\n source_id: 2382\n fallback_source_ids: null\nsource_provider: Deutscher Wetterdienst (DWD)\nsource_station_name: BERLIN-ALEX.\nsource_dwd_station_id: "00399"\nsource_wmo_station_id: "10389"\nsource_observation_type: forecast\nsource_distance_m: 805.0\nsource_lat: 52.52\nsource_lon: 13.42\nsource_height_m: 37.0\nsource_first_record: "2026-06-18T08:00:00+02:00"\nsource_last_record: "2026-06-28T12:00:00+02:00"\ndata_quality_hourly_rows: 24\ndata_quality_raw_source_count: 7\nembed_ref: berlin-weather-2026-mF7\napp_id: weather\nskill_id: forecast',
23508
+ "parent_embed_id": "324eba33-3622-409c-b401-183f95d6183e",
23509
+ "embed_ids": null
23510
+ },
23511
+ {
23512
+ "embed_id": "4d855526-af41-47cf-a354-c769f3098c94",
23513
+ "type": "weather_day",
23514
+ "content": 'type: weather_day\ntitle: Berlin weather 2026-06-19\ndate: 2026-06-19\nlabel: tomorrow\nlocation_name: Berlin\ncountry_code: DE\ntimezone: Europe/Berlin\nprovider: Deutscher Wetterdienst (DWD)\ncondition: dry\nicon: partly-cloudy-day\ntemperature_min_c: 18.4\ntemperature_max_c: 32.0\nprecipitation_total_mm: 0\nprecipitation_probability_max_pct: 4\nrain_hours: 0\nwind_speed_max_kmh: 11.1\nwind_gust_speed_max_kmh: 25.9\ncloud_cover_avg_pct: 58.7\nrelative_humidity_avg_pct: null\nhourly[24]{time,timestamp,condition,icon,temperature_c,precipitation_mm,precipitation_probability_pct,precipitation_probability_6h_pct,cloud_cover_pct,relative_humidity_pct,dew_point_c,pressure_msl_hpa,visibility_m,wind_direction_deg,wind_speed_kmh,wind_gust_direction_deg,wind_gust_speed_kmh,sunshine_minutes,solar,source_id,fallback_source_ids}:\n "00:00","2026-06-19T00:00:00+02:00",dry,partly-cloudy-night,21.3,0.0,1,null,43,null,16.7,1018.2,37100,91,9.3,null,16.7,0.0,0.0,2382,null\n "01:00","2026-06-19T01:00:00+02:00",dry,partly-cloudy-night,20.6,0.0,1,null,39,null,16.7,1018.4,38100,96,9.3,null,16.7,0.0,0.0,2382,null\n "02:00","2026-06-19T02:00:00+02:00",dry,partly-cloudy-night,19.7,0.0,1,1,42,null,16.8,1018.2,39100,110,9.3,null,16.7,0.0,0.0,2382,null\n "03:00","2026-06-19T03:00:00+02:00",dry,partly-cloudy-night,19.2,0.0,1,null,49,null,16.7,1018.2,37900,113,9.3,null,16.7,0.0,0.0,2382,null\n "04:00","2026-06-19T04:00:00+02:00",dry,partly-cloudy-night,18.9,0.0,1,null,53,null,16.8,1018.1,36200,113,9.3,null,16.7,0.0,0.0,2382,null\n "05:00","2026-06-19T05:00:00+02:00",dry,partly-cloudy-day,18.4,0.0,2,null,58,null,16.9,1018.1,34900,121,9.3,null,16.7,6.0,0.0,2382,null\n "06:00","2026-06-19T06:00:00+02:00",dry,partly-cloudy-day,18.7,0.0,1,null,64,null,17.2,1018.2,31700,127,7.4,null,14.8,15.0,0.022,2382,null\n "07:00","2026-06-19T07:00:00+02:00",dry,partly-cloudy-day,20.0,0.0,1,null,66,null,17.6,1018.3,29900,131,7.4,null,14.8,24.0,0.097,2382,null\n "08:00","2026-06-19T08:00:00+02:00",dry,partly-cloudy-day,21.9,0.0,2,3,68,null,17.8,1018.3,31100,137,7.4,null,13.0,30.0,0.192,2382,null\n "09:00","2026-06-19T09:00:00+02:00",dry,partly-cloudy-day,23.9,0.0,2,null,69,null,17.7,1018.4,30400,145,7.4,null,14.8,36.0,0.311,2382,null\n "10:00","2026-06-19T10:00:00+02:00",dry,partly-cloudy-day,25.7,0.0,3,null,69,null,17.6,1018.3,31600,151,7.4,null,16.7,40.0,0.442,2382,null\n "11:00","2026-06-19T11:00:00+02:00",dry,partly-cloudy-day,28.2,0.0,2,null,68,null,17.3,1018.2,32900,141,7.4,null,18.5,42.0,0.533,2382,null\n "12:00","2026-06-19T12:00:00+02:00",dry,partly-cloudy-day,29.2,0.0,4,null,70,null,17.4,1018.2,32500,155,9.3,null,20.4,40.0,0.608,2382,null\n "13:00","2026-06-19T13:00:00+02:00",dry,partly-cloudy-day,30.2,0.0,3,null,67,null,17.2,1017.9,31600,156,9.3,null,24.1,41.0,0.65,2382,null\n "14:00","2026-06-19T14:00:00+02:00",dry,partly-cloudy-day,31.2,0.0,2,3,65,null,16.9,1017.5,29900,151,9.3,null,24.1,43.0,0.683,2382,null\n "15:00","2026-06-19T15:00:00+02:00",dry,partly-cloudy-day,31.6,0.0,2,null,66,null,17.0,1017.3,29200,153,9.3,null,25.9,44.0,0.658,2382,null\n "16:00","2026-06-19T16:00:00+02:00",dry,partly-cloudy-day,31.8,0.0,3,null,66,null,16.9,1016.8,28800,157,9.3,null,24.1,47.0,0.603,2382,null\n "17:00","2026-06-19T17:00:00+02:00",dry,partly-cloudy-day,32.0,0.0,2,null,64,null,17.1,1016.4,29200,140,9.3,null,24.1,49.0,0.536,2382,null\n "18:00","2026-06-19T18:00:00+02:00",dry,partly-cloudy-day,31.7,0.0,2,null,61,null,17.1,1016.2,31800,142,9.3,null,22.2,47.0,0.431,2382,null\n "19:00","2026-06-19T19:00:00+02:00",dry,partly-cloudy-day,30.8,0.0,2,null,62,null,17.1,1016.2,30700,122,9.3,null,22.2,41.0,0.294,2382,null\n "20:00","2026-06-19T20:00:00+02:00",dry,partly-cloudy-day,29.4,0.0,2,7,57,null,17.3,1016.1,31700,112,9.3,null,22.2,34.0,0.167,2382,null\n "21:00","2026-06-19T21:00:00+02:00",dry,partly-cloudy-day,27.6,0.0,1,null,54,null,17.3,1016.4,31900,108,9.3,null,22.2,23.0,0.058,2382,null\n "22:00","2026-06-19T22:00:00+02:00",dry,partly-cloudy-night,26.1,0.0,4,null,49,null,17.8,1016.9,31700,114,11.1,null,22.2,12.0,0.008,2382,null\n "23:00","2026-06-19T23:00:00+02:00",dry,partly-cloudy-night,24.6,0.0,1,null,40,null,17.9,1017.1,32400,115,11.1,null,22.2,0.0,0.0,2382,null\nsource_provider: Deutscher Wetterdienst (DWD)\nsource_station_name: BERLIN-ALEX.\nsource_dwd_station_id: "00399"\nsource_wmo_station_id: "10389"\nsource_observation_type: forecast\nsource_distance_m: 805.0\nsource_lat: 52.52\nsource_lon: 13.42\nsource_height_m: 37.0\nsource_first_record: "2026-06-18T08:00:00+02:00"\nsource_last_record: "2026-06-28T12:00:00+02:00"\ndata_quality_hourly_rows: 24\ndata_quality_raw_source_count: 7\nembed_ref: berlin-weather-2026-Pa5\napp_id: weather\nskill_id: forecast',
23515
+ "parent_embed_id": "324eba33-3622-409c-b401-183f95d6183e",
23516
+ "embed_ids": null
23517
+ }
23518
+ ],
23519
+ metadata: {
23520
+ featured: true,
23521
+ order: 104,
23522
+ app_skill_examples: ["weather.forecast"]
23523
+ }
23524
+ };
23525
+
22895
23526
  // ../ui/src/demo_chats/exampleChatData.ts
22896
23527
  var ALL_EXAMPLE_CHATS = [
22897
23528
  giganticAirplanesChat,
@@ -22977,7 +23608,9 @@ var ALL_EXAMPLE_CHATS = [
22977
23608
  berlinCentralStationMapLocationChat,
22978
23609
  launchReadinessChecklistDocChat,
22979
23610
  rostockHeavyRainRadarChat,
22980
- classicCarReverseImageSearchChat
23611
+ classicCarReverseImageSearchChat,
23612
+ usbC3v3LdoPcbSchematicChat,
23613
+ berlinMorningBikeForecastChat
22981
23614
  ].sort((a, b) => a.metadata.order - b.metadata.order);
22982
23615
 
22983
23616
  // ../ui/src/i18n/locales/en.json
@@ -24607,6 +25240,30 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
24607
25240
  ai_generated: {
24608
25241
  text: "AI generated"
24609
25242
  },
25243
+ authentic: {
25244
+ text: "Authentic"
25245
+ },
25246
+ ai_generated_chance: {
25247
+ text: "{percentage} chance it's AI generated"
25248
+ },
25249
+ via_provider_prefix: {
25250
+ text: "via"
25251
+ },
25252
+ via_provider: {
25253
+ text: "via {provider}"
25254
+ },
25255
+ detection_limitations: {
25256
+ text: "Pixel-based detection is useful for photos and art, including many compressed or re-shared images. It is not definitive; heavy degradation, UI-heavy screenshots, screen or print recaptures, and new generators may need human judgment."
25257
+ },
25258
+ detection_failed_title: {
25259
+ text: "AI detection unavailable"
25260
+ },
25261
+ detection_failed_retry: {
25262
+ text: "AI detection failed temporarily. Try again later."
25263
+ },
25264
+ detection_failed_description: {
25265
+ text: "The image upload succeeded, but Sightengine did not return a usable AI-detection score for this attempt."
25266
+ },
24610
25267
  signup_to_upload: {
24611
25268
  text: "Signup to upload\u2026"
24612
25269
  }
@@ -25398,6 +26055,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
25398
26055
  text: "Get coding help, docs & project advice."
25399
26056
  }
25400
26057
  },
26058
+ diagrams: {
26059
+ text: "Diagrams",
26060
+ description: {
26061
+ text: "Create flowcharts, sequences, timelines & more."
26062
+ }
26063
+ },
25401
26064
  docs: {
25402
26065
  text: "Docs",
25403
26066
  description: {
@@ -25582,9 +26245,40 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
25582
26245
  }
25583
26246
  },
25584
26247
  chat: {
26248
+ generated_by_cost: {
26249
+ text: "Cost: {credits} credits"
26250
+ },
25585
26251
  new_chat: {
25586
26252
  text: "New chat"
25587
26253
  },
26254
+ request_feature: {
26255
+ text: "Request feature"
26256
+ },
26257
+ request_feature_prefill: {
26258
+ text: "Requested feature:"
26259
+ },
26260
+ assistant_feedback: {
26261
+ rate_label: {
26262
+ text: "Rate assistant response:"
26263
+ },
26264
+ star_label: {
26265
+ text: "Rate {count} out of 5"
26266
+ },
26267
+ submit: {
26268
+ text: "Submit"
26269
+ },
26270
+ thanks: {
26271
+ text: "Thanks for the feedback!"
26272
+ },
26273
+ report_title: {
26274
+ text: "Assistant response quality bad:"
26275
+ }
26276
+ },
26277
+ anonymous_free_usage: {
26278
+ feature_notice: {
26279
+ text: "You are using free anonymous credits. File uploads, memories, chat sync, and some other features require creating an account."
26280
+ }
26281
+ },
25588
26282
  interactive_question_failed: {
25589
26283
  text: "Failed to display question."
25590
26284
  },
@@ -25651,8 +26345,38 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
25651
26345
  approve: {
25652
26346
  text: "Approve once"
25653
26347
  },
26348
+ approve_selected: {
26349
+ text: "Approve selected"
26350
+ },
25654
26351
  loading: {
25655
26352
  text: "Approving..."
26353
+ },
26354
+ account: {
26355
+ text: "Account"
26356
+ },
26357
+ request_title: {
26358
+ text: "Requested {action} action"
26359
+ },
26360
+ calendar: {
26361
+ text: "Calendar"
26362
+ },
26363
+ event_title: {
26364
+ text: "Event"
26365
+ },
26366
+ event_id: {
26367
+ text: "Event ID"
26368
+ },
26369
+ start: {
26370
+ text: "Start"
26371
+ },
26372
+ end: {
26373
+ text: "End"
26374
+ },
26375
+ from: {
26376
+ text: "From"
26377
+ },
26378
+ to: {
26379
+ text: "To"
25656
26380
  }
25657
26381
  },
25658
26382
  connected_account_receipts: {
@@ -25668,6 +26392,9 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
25668
26392
  cancelled: {
25669
26393
  text: "{action} cancelled."
25670
26394
  },
26395
+ pending_cancel: {
26396
+ text: "{action} auto-approved. Press to cancel before it starts."
26397
+ },
25671
26398
  failed: {
25672
26399
  text: "{action} could not be completed."
25673
26400
  },
@@ -25683,6 +26410,18 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
25683
26410
  undo_failed: {
25684
26411
  text: "Could not undo this action."
25685
26412
  },
26413
+ press_to_cancel: {
26414
+ text: "Press to cancel"
26415
+ },
26416
+ cancelling: {
26417
+ text: "Cancelling..."
26418
+ },
26419
+ cancel_started: {
26420
+ text: "Action cancelled"
26421
+ },
26422
+ cancel_failed: {
26423
+ text: "Could not cancel this action."
26424
+ },
25686
26425
  undo_available: {
25687
26426
  text: "Undo is available."
25688
26427
  },
@@ -28989,6 +29728,50 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
28989
29728
  },
28990
29729
  electrical: {
28991
29730
  text: "Electrical"
29731
+ },
29732
+ pcb_schematic: {
29733
+ title: {
29734
+ text: "PCB schematic"
29735
+ },
29736
+ prepare_files: {
29737
+ text: "Prepare PCB files"
29738
+ },
29739
+ preparing: {
29740
+ text: "Preparing PCB files"
29741
+ },
29742
+ prepared: {
29743
+ text: "PCB files prepared"
29744
+ },
29745
+ prepare_failed: {
29746
+ text: "PCB file preparation failed"
29747
+ },
29748
+ show_logs: {
29749
+ text: "Show logs"
29750
+ },
29751
+ hide_logs: {
29752
+ text: "Hide logs"
29753
+ },
29754
+ logs: {
29755
+ text: "Compile logs"
29756
+ },
29757
+ no_logs: {
29758
+ text: "No logs available."
29759
+ },
29760
+ artifacts: {
29761
+ text: "Prepared files"
29762
+ },
29763
+ safety_note: {
29764
+ text: "Atopile source is compiled in a sandbox before KiCad and project files are prepared."
29765
+ },
29766
+ copied: {
29767
+ text: "PCB schematic copied"
29768
+ },
29769
+ copy_failed: {
29770
+ text: "Could not copy PCB schematic"
29771
+ },
29772
+ download_failed: {
29773
+ text: "Could not download PCB schematic"
29774
+ }
28992
29775
  }
28993
29776
  },
28994
29777
  math: {
@@ -28999,6 +29782,11 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
28999
29782
  text: "Plot"
29000
29783
  }
29001
29784
  },
29785
+ diagrams: {
29786
+ mermaid: {
29787
+ text: "Mermaid Diagram"
29788
+ }
29789
+ },
29002
29790
  mail: {
29003
29791
  email: {
29004
29792
  text: "Email Draft"
@@ -29178,6 +29966,15 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29178
29966
  },
29179
29967
  drop_files: {
29180
29968
  text: "Drop files to upload"
29969
+ },
29970
+ signup_required_title: {
29971
+ text: "Create an account to upload files"
29972
+ },
29973
+ signup_required_body: {
29974
+ text: "Your message draft will be kept, but files are only uploaded after signup. Remove the file to keep chatting without an account."
29975
+ },
29976
+ remove_pending_file: {
29977
+ text: "Remove file"
29181
29978
  }
29182
29979
  },
29183
29980
  file_size_limits: {
@@ -29249,6 +30046,9 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29249
30046
  click_to_add_text: {
29250
30047
  text: "Click here to add text"
29251
30048
  },
30049
+ anonymous_terms_reminder: {
30050
+ text: "By sending a message you accept the terms & privacy policy of OpenMates."
30051
+ },
29252
30052
  send: {
29253
30053
  text: "Send"
29254
30054
  },
@@ -29620,6 +30420,38 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29620
30420
  text: '```json\n{"type": "app_skill_use", "embed_id": "7fbf38fc-1126-46b3-bdf4-60bfb1e3197f", "app_id": "health", "skill_id": "search_appointments"}\n```\n\nI found several excellent options for an English-speaking [dermatology](wiki:Dermatology) routine skin check (Hautkrebsvorsorge) in [Berlin Mitte](wiki:Mitte_(locality)) with immediate availability.\n\nBased on your request for English-speaking doctors in Mitte, here are the best options for a routine skin cancer screening:\n\n### **Top Recommendations in Berlin Mitte**\n\n```json\n{"type": "sheet", "embed_id": "08e6061c-e224-46d4-8ac7-7318d0279b57"}\n```\n\n\n### **Additional Options (Nearby)**\nIf those do not suit your schedule, there are also appointments available slightly outside of Mitte:\n* **HLCP Haut- und Lasercentrum**: Located at Kurf\xFCrstendamm 96 (Charlottenburg). They speak several languages including English and have slots starting tomorrow, June 9th.\n* **Meike Schr\xF6der**: Located at Clayallee 343 (Zehlendorf). They have slots available on June 9th and June 11th.\n\n**Important Note on Insurance:**\nMost of the immediate appointments found are in private practices or for "self-paying" patients. If you have public health insurance (GKV), please confirm during booking if the screening is covered or if it is a private service (IGeL).\n\nWould you like me to help you [set a reminder](/#settings/apps/reminder) to follow up on this appointment, or should I search for more options further into next week?\n---\n*\u26A0\uFE0F This is educational information only and not medical advice. Consult your doctor for medical decisions.*'
29621
30421
  }
29622
30422
  },
30423
+ berlin_morning_bike_forecast: {
30424
+ title: {
30425
+ text: "Berlin Morning Bike Forecast"
30426
+ },
30427
+ summary: {
30428
+ text: "A concise Berlin weather forecast that helps decide whether to bike to Tempelhofer Feld."
30429
+ },
30430
+ message_1: {
30431
+ text: "@skill:weather:forecast What's the weather forecast for Berlin tomorrow morning, and should I bike to Tempelhofer Feld? Keep the answer concise."
30432
+ },
30433
+ message_2: {
30434
+ text: '```json\n{"type": "app_skill_use", "embed_id": "324eba33-3622-409c-b401-183f95d6183e", "app_id": "weather", "skill_id": "forecast", "query": "Berlin weather forecast", "provider": "Deutscher Wetterdienst (DWD) + Open-Meteo", "location": "Berlin"}\n```\n\nTomorrow morning in [Berlin](wiki:Berlin) (Friday, June 19, 2026), the weather will be dry and partly cloudy, with temperatures starting around 18\u201320\xB0C and rising quickly toward a high of 32\xB0C.\n\n**Should you bike to [Tempelhofer Feld](wiki:Tempelhofer_Feld)?**\nYes, it\'s a great idea! Conditions are ideal for biking:\n* **Dry:** No rain expected (4% probability).\n* **Calm:** Wind speeds are light (approx. 11 km/h).\n* **Morning Window:** Biking in the morning is highly recommended to avoid the afternoon heat peak of 32\xB0C, especially since the airfield offers very little shade.\n\n**What\'s your plan for the rest of the day?**\n* Would you like to find a [beer garden](wiki:Beer_garden) or cafe near Tempelhof to cool off afterward?\n* Do you need a more detailed [hourly forecast](/#settings/apps/weather) to time your ride perfectly?'
30435
+ },
30436
+ follow_up_1: {
30437
+ text: "Search for bike-friendly cafes near Tempelhofer Feld"
30438
+ },
30439
+ follow_up_2: {
30440
+ text: "What are some must-see spots at Tempelhofer Feld?"
30441
+ },
30442
+ follow_up_3: {
30443
+ text: "Find the hourly weather forecast for Berlin tomorrow"
30444
+ },
30445
+ follow_up_4: {
30446
+ text: "How should I prepare for biking in Berlin's summer heat?"
30447
+ },
30448
+ follow_up_5: {
30449
+ text: "Show me the best cycling routes to Tempelhofer Feld"
30450
+ },
30451
+ follow_up_6: {
30452
+ text: "Are there any events happening at Tempelhofer Feld tomorrow?"
30453
+ }
30454
+ },
29623
30455
  berlin_weather_bike_commute: {
29624
30456
  title: {
29625
30457
  text: "Berlin Weather and Bike Commute Advice"
@@ -31021,6 +31853,38 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
31021
31853
  sub_chat_3_message_1: {
31022
31854
  text: "Research market concentration in the US egg producer industry (e.g., Cal-Maine Foods' profit margins, price-fixing lawsuits) and the role of retailer/grocery store markup margins in keeping egg prices high post-avian flu. Distinguish confirmed data from inferred conclusions."
31023
31855
  }
31856
+ },
31857
+ usb_c_3v3_ldo_pcb_schematic: {
31858
+ title: {
31859
+ text: "USB-C 3.3V LDO PCB Schematic"
31860
+ },
31861
+ summary: {
31862
+ text: "Design and prepare a compilable Atopile PCB schematic for a USB-C powered 3.3V regulator circuit."
31863
+ },
31864
+ message_1: {
31865
+ text: "Create a simple compilable Atopile 0.15.7 PCB schematic for a 5V USB-C input feeding a 3.3V LDO rail with a power indicator diode and input/output decoupling capacitors."
31866
+ },
31867
+ message_2: {
31868
+ text: 'Here is a compact Atopile 0.15.7 starting point for a USB-C powered 3.3V regulator stage. It keeps the schematic self-contained, uses `module App` as the build entry, models the USB-C input and LDO as simple power-interface modules, and adds 10uF/1uF decoupling plus a diode indicator on the regulated rail.\n\n```json\n{"type": "pcb_schematic", "embed_id": "46d63593-516e-49b5-8e7d-600c4fe18836"}\n```\n\nThe source is ready for the PCB preparation flow: OpenMates can send it to the sandboxed Atopile compiler, show the build status, and expose the generated source bundle when the build succeeds.'
31869
+ },
31870
+ follow_up_1: {
31871
+ text: "Add voltage constraints to the 5V and 3.3V rails"
31872
+ },
31873
+ follow_up_2: {
31874
+ text: "Choose manufacturer parts for the LDO and diode"
31875
+ },
31876
+ follow_up_3: {
31877
+ text: "Add USB-C connector details and CC resistors"
31878
+ },
31879
+ follow_up_4: {
31880
+ text: "Add an enable pin to the regulator"
31881
+ },
31882
+ follow_up_5: {
31883
+ text: "Prepare PCB files from this schematic"
31884
+ },
31885
+ follow_up_6: {
31886
+ text: "Explain the decoupling capacitor choices"
31887
+ }
31024
31888
  }
31025
31889
  },
31026
31890
  focus_modes: {
@@ -32176,1014 +33040,1014 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
32176
33040
  text: "Groq Privacy Policy"
32177
33041
  }
32178
33042
  },
32179
- revolut_business: {
32180
- heading: {
32181
- text: "3.15 Revolut Business (SEPA Bank Transfer)"
32182
- },
32183
- description: {
32184
- text: "We use Revolut Bank UAB (Lithuania, EU) as our receiving bank for SEPA bank transfers. When you choose to pay via bank transfer, we display our Revolut IBAN to you. Revolut notifies us via a cryptographically signed webhook when a transfer arrives. We do not send your personal data to Revolut \u2014 only the transaction reference, amount, and Revolut's own transaction ID are processed."
32185
- },
32186
- privacy_policy_link: {
32187
- text: "Revolut Privacy Policy"
32188
- }
32189
- },
32190
- flightradar24: {
32191
- heading: {
32192
- text: "3.16 Flightradar24"
32193
- },
32194
- description: {
32195
- text: 'When you request real flight track data using the "Get flight track" skill, we query Flightradar24 to retrieve historical GPS track data for completed flights. Flightradar24 receives the flight number and departure date. No personal data is shared.'
33043
+ providers: {
33044
+ payments: {
33045
+ revolut_business: {
33046
+ heading: {
33047
+ text: "3.15 Revolut Business (SEPA Bank Transfer)"
33048
+ },
33049
+ description: {
33050
+ text: "We use Revolut Bank UAB (Lithuania, EU) as our receiving bank for SEPA bank transfers. When you choose to pay via bank transfer, we display our Revolut IBAN to you. Revolut notifies us via a cryptographically signed webhook when a transfer arrives. We do not send your personal data to Revolut \u2014 only the transaction reference, amount, and Revolut's own transaction ID are processed."
33051
+ },
33052
+ privacy_policy_link: {
33053
+ text: "Revolut Privacy Policy"
33054
+ }
33055
+ },
33056
+ heading: {
33057
+ text: "Payment providers (only when you purchase credits)"
33058
+ },
33059
+ description: {
33060
+ text: "These providers are only used when you purchase credits, subscribe to monthly auto top-up, or receive a refund. If you never make a purchase, none of your data is shared with them."
33061
+ },
33062
+ stripe: {
33063
+ heading: {
33064
+ text: "Stripe (primary payment processor)"
33065
+ },
33066
+ description: {
33067
+ text: "Stripe processes one-time credit purchases and monthly auto top-up subscriptions. We send Stripe your email address, a tokenized payment method (we never see your card number), billing address and VAT information when required for invoicing, and transaction metadata."
33068
+ }
33069
+ }
32196
33070
  },
32197
- privacy_policy_link: {
32198
- text: "Flightradar24 Privacy Policy"
32199
- }
32200
- },
32201
- security_measures: {
32202
33071
  heading: {
32203
- text: "4. Security Measures"
33072
+ text: "When each provider is used"
32204
33073
  },
32205
33074
  intro: {
32206
- text: "We implement various security measures to protect your account and data in the web application:"
33075
+ text: "Most of the providers below are only reached when you use a specific feature \u2014 for example, image generation providers are only used when you invoke an image skill. The groups below tell you exactly which providers are involved for each feature, so you can opt in or out of data sharing by choosing which features to use."
32207
33076
  },
32208
- device_fingerprinting: {
32209
- subheading: {
32210
- text: "Device Fingerprinting"
33077
+ always_active: {
33078
+ heading: {
33079
+ text: "Always active (used for every user)"
32211
33080
  },
32212
- purpose: {
32213
- text: "To enhance account security, prevent fraudulent access, and distinguish between new and recognized devices during login and session validation, we collect User-Agent strings (browser, OS, device type information) and IP addresses (used temporarily to derive approximate geo-location like country/city, then discarded from the device record)."
33081
+ description: {
33082
+ text: "These providers are used for every user, regardless of which features you use. Using OpenMates at all means your data passes through them."
32214
33083
  },
32215
- storage: {
32216
- text: "A unique, stable hash is generated based on a combination of the collected signals. This hash is the only value stored in the device record. Browser/OS type and derived country code may be used transiently for risk assessment and notifications but are not stored with the device record. Raw IP addresses are NOT stored as part of the device record itself."
33084
+ vercel: {
33085
+ heading: {
33086
+ text: "Vercel (frontend hosting)"
33087
+ },
33088
+ description: {
33089
+ text: "We use Vercel to host the static frontend of our website and web app. Vercel receives page requests and serves static assets."
33090
+ }
32217
33091
  },
32218
- ip_logging: {
32219
- text: "For security monitoring and abuse prevention, IP addresses associated with failed login attempts (invalid password or 2FA code) may be logged with limited retention. In addition, certain security/compliance events (e.g., successful backup-code logins) may be logged with IP for audit purposes. IP addresses are also used for rate limiting login attempts."
33092
+ hetzner: {
33093
+ heading: {
33094
+ text: "Hetzner (backend, database, storage, observability)"
33095
+ },
33096
+ description: {
33097
+ text: "We host all of our backend services \u2014 API servers, Directus CMS, Postgres database, Redis caches, S3-compatible object storage, and our self-hosted OpenObserve observability platform \u2014 on Hetzner infrastructure in the EU. OpenObserve runs on our own Hetzner servers and is not a third-party service; traces never leave the EU."
33098
+ }
33099
+ },
33100
+ brevo: {
33101
+ heading: {
33102
+ text: "Brevo (transactional email)"
33103
+ },
33104
+ description: {
33105
+ text: "We use Brevo to send transactional email \u2014 account verification, password reset links, payment receipts, and credit notes \u2014 and, if you opt in, the OpenMates newsletter. If you are an existing OpenMates customer (i.e. you have completed a purchase), we may also send you occasional product announcements and service updates about OpenMates' own similar services under the existing-customer exception (\xA77 Abs. 3 UWG / ePrivacy Directive Art. 13(2)); you can object in Settings \u2192 Newsletter or via the one-click unsubscribe link in any such email. Brevo sees recipient email addresses and the content of the emails we send you."
33106
+ }
33107
+ },
33108
+ ip_api: {
33109
+ heading: {
33110
+ text: "IP-API (IP geolocation)"
33111
+ },
33112
+ description: {
33113
+ text: "We use IP-API to resolve IP addresses to approximate geolocation for abuse prevention, rate limiting, and regional compliance. The resolved country and city are used transiently and are not stored against your account."
33114
+ }
33115
+ },
33116
+ sightengine: {
33117
+ heading: {
33118
+ text: "Sightengine (image and video moderation)"
33119
+ },
33120
+ description: {
33121
+ text: "Every image and video you upload is scanned by Sightengine for safety moderation before it is stored. Sightengine sees the image or video content."
33122
+ }
33123
+ },
33124
+ api_video: {
33125
+ heading: {
33126
+ text: "api.video (product video hosting)"
33127
+ },
33128
+ description: {
33129
+ text: "We use api.video (EU company, France) to host and stream product demo and feature walkthrough videos shown on our website and web app. When you watch a video, your IP address and basic viewing data are processed by api.video. No user account identifiers are shared."
33130
+ }
32220
33131
  }
32221
- }
32222
- },
32223
- data_categories: {
32224
- heading: {
32225
- text: "5. Data Categories We Collect and Process"
32226
- },
32227
- intro: {
32228
- text: "We collect and process the following categories of data to operate and secure our services:"
32229
- },
32230
- account: {
32231
- text: "Account: Email address, username/display name, profile image, locale, and security settings (e.g., 2FA enabled). Email and username are encrypted with your key before storage. We also keep a separate server-side Vault-encrypted copy of your verified email address for mandatory account lifecycle notices, such as account verification, security alerts, and deletion reminders. Passwords are stored as salted hashes."
32232
- },
32233
- usage: {
32234
- text: "Usage: Server logs, event timestamps, feature usage, error logs, and device recognition identifiers (hashed). IP addresses may be temporarily processed for security and rate limiting."
32235
- },
32236
- content: {
32237
- text: "Content: Chat messages, prompts, attachments, and uploaded images/videos necessary to deliver the service (subject to moderation where applicable)."
32238
- },
32239
- payments: {
32240
- text: "Payments: Payment method tokens, transaction IDs, billing address and VAT information as required for invoicing (processed primarily by Stripe). We do not store full card numbers."
32241
- },
32242
- newsletter: {
32243
- text: "Newsletter: If you choose to subscribe to our newsletter, we store your email address in encrypted form. Newsletter subscription is optional and separate from account creation \u2014 creating an account does not automatically subscribe you to the newsletter. If you are an OpenMates customer (i.e. you have completed a purchase), we may additionally send you occasional product announcements and service updates about OpenMates' own similar services under the existing-customer exception (\xA77 Abs. 3 UWG / ePrivacy Directive Art. 13(2)). You can object to this at any time in Settings \u2192 Newsletter or via the one-click unsubscribe link in any such email, without affecting your account."
32244
- },
32245
- stability_logs: {
32246
- text: "Stability Logs: To diagnose technical errors, anonymized browser console logs (error messages, component status, connection events) are collected from authenticated users and stored for up to 48 hours. If an error occurs, surrounding context is retained for up to 14 days. No message content, chat titles, names, email addresses, or other personally identifiable information is included. A random session identifier (generated per browser tab, not linked to your account) is used for log correlation. You can disable this at any time in Settings > Privacy > Stability Logs."
32247
- }
32248
- },
32249
- data_retention: {
32250
- heading: {
32251
- text: "6. Data Retention"
32252
- },
32253
- account: {
32254
- text: "Account data: Retained until account deletion and completion of related legal obligations."
32255
- },
32256
- usage_and_logs: {
32257
- text: "Usage and logs: Operational logs retained up to 12 months; IPs tied to failed logins up to 30 days for security."
32258
- },
32259
- device_fingerprints: {
32260
- text: "Device fingerprints: Retained until account deletion or manual removal from account settings (when available)."
32261
- },
32262
- content: {
32263
- text: "Content: Retained until the user deletes the content or closes the account, subject to backup cycles."
32264
- },
32265
- payments_and_invoices: {
32266
- text: "Payments and invoices: Retained up to 10 years where required by tax/commercial law."
32267
- },
32268
- compliance_logs: {
32269
- text: "**Compliance logs:** audit logs retained 2 years (BSI \xA734 BDSG); financial compliance logs retained 10 years (AO \xA7147 and HGB \xA7257)"
32270
- },
32271
- observability_traces: {
32272
- text: "**Observability traces:** up to 30 days; user IDs in traces use a hash that rotates every 24 hours, so older traces cannot be joined to a specific user"
32273
- },
32274
- user_data_backups: {
32275
- text: "**User data backups:** 60 days via S3 lifecycle; backups contain only ciphertext encrypted with your own key"
32276
- }
32277
- },
32278
- legal_basis: {
32279
- heading: {
32280
- text: "7. Legal Basis for Processing (GDPR)"
32281
- },
32282
- contract: {
32283
- text: "Contract: Processing necessary to perform our contract with you or to take steps at your request prior to entering into a contract (e.g., account creation, delivering web app features)."
32284
- },
32285
- consent: {
32286
- text: "Consent: Processing based on your consent, where applicable (e.g., optional communications, certain analytics or marketing where used). You can withdraw consent at any time."
32287
- },
32288
- legitimate_interests: {
32289
- text: "Legitimate interests: Processing necessary for our legitimate interests, such as service security, fraud prevention, abuse detection, and improving the service, provided these interests are not overridden by your rights and interests."
32290
33132
  },
32291
- direct_marketing: {
32292
- text: "Direct marketing to existing customers: For existing OpenMates customers (users who have completed a purchase), we may rely on Art. 6(1)(f) GDPR (legitimate interests) in combination with \xA77 Abs. 3 UWG and ePrivacy Directive Art. 13(2) to send occasional product announcements, service updates, and marketing about OpenMates' own similar services. Each such email includes a clearly visible one-click unsubscribe link, and you can also manage these preferences at any time in Settings \u2192 Newsletter. You can object to this processing at any time without giving reasons (Art. 21 GDPR)."
32293
- },
32294
- legal_obligation: {
32295
- text: "Legal obligation: Processing necessary to comply with legal obligations (e.g., tax and accounting requirements for invoices and transaction records)."
32296
- }
32297
- },
32298
- legal_rights: {
32299
- heading: {
32300
- text: "8. Your Privacy Rights"
32301
- },
32302
- intro: {
32303
- text: "Depending on your jurisdiction, you may have certain rights over your personal data. The following summarizes key rights under GDPR and CCPA/CPRA and how to exercise them:"
32304
- },
32305
- gdpr: {
32306
- subheading: {
32307
- text: "GDPR (EU/EEA)"
33133
+ ai_models: {
33134
+ heading: {
33135
+ text: "AI model providers (only when you use a model that routes through them)"
32308
33136
  },
32309
- access: {
32310
- text: "Right of access"
33137
+ description: {
33138
+ text: 'OpenMates routes AI requests to different providers depending on which model you select. Before any message is sent to a provider in this group, your browser replaces real names, emails, and physical addresses with placeholders (see the "PII placeholder substitution" measure above). You can always see which provider a model uses in the model details panel in AI settings.'
32311
33139
  },
32312
- rectification: {
32313
- text: "Right to rectification"
33140
+ mistral: {
33141
+ heading: {
33142
+ text: "Mistral (EU direct)"
33143
+ },
33144
+ description: {
33145
+ text: "Mistral hosts its own family of models in the EU. Only used when you select a Mistral model. Mistral receives your chat messages with PII placeholders already substituted client-side."
33146
+ }
32314
33147
  },
32315
- erasure: {
32316
- text: "Right to erasure ('right to be forgotten')"
33148
+ aws_bedrock: {
33149
+ heading: {
33150
+ text: "AWS Bedrock (EU region, primary Claude path)"
33151
+ },
33152
+ description: {
33153
+ text: "AWS Bedrock hosts Anthropic Claude models in its Frankfurt EU region. This is the primary path for Claude requests. AWS receives your chat messages with PII placeholders already substituted client-side."
33154
+ }
32317
33155
  },
32318
- restriction: {
32319
- text: "Right to restriction of processing"
33156
+ anthropic: {
33157
+ heading: {
33158
+ text: "Anthropic (US, fallback Claude path)"
33159
+ },
33160
+ description: {
33161
+ text: "Anthropic's direct API is used as a fallback path for Claude models when AWS Bedrock is unreachable. Anthropic is based in the US and receives your chat messages with PII placeholders already substituted client-side."
33162
+ }
32320
33163
  },
32321
- portability: {
32322
- text: "Right to data portability"
33164
+ openai: {
33165
+ heading: {
33166
+ text: "OpenAI (US)"
33167
+ },
33168
+ description: {
33169
+ text: "OpenAI processes requests for GPT-family models when you select one. OpenAI receives your chat messages with PII placeholders already substituted client-side."
33170
+ }
32323
33171
  },
32324
- objection: {
32325
- text: "Right to object"
33172
+ openrouter: {
33173
+ heading: {
33174
+ text: "OpenRouter (US, routing aggregator)"
33175
+ },
33176
+ description: {
33177
+ text: "OpenRouter is used as a routing aggregator for several third-party models, including Cerebras-hosted models. OpenRouter sees your chat messages with PII placeholders substituted client-side and forwards them to the underlying model host."
33178
+ }
32326
33179
  },
32327
- withdraw_consent: {
32328
- text: "Right to withdraw consent"
33180
+ cerebras: {
33181
+ heading: {
33182
+ text: "Cerebras (US, via OpenRouter)"
33183
+ },
33184
+ description: {
33185
+ text: "Cerebras hosts some of the high-speed models routed through OpenRouter. Only used when you select a Cerebras-hosted model."
33186
+ }
32329
33187
  },
32330
- exercise: {
32331
- text: "To exercise your GDPR rights, contact us at contact@openmates.org or via the Imprint. We may need to verify your identity before fulfilling your request."
33188
+ google_gemini: {
33189
+ heading: {
33190
+ text: "Google Gemini (US)"
33191
+ },
33192
+ description: {
33193
+ text: "Google Gemini processes requests for Gemini-family models when you select one. Google receives your chat messages with PII placeholders already substituted client-side."
33194
+ }
32332
33195
  },
32333
- manual_note: {
32334
- text: "The following rights are available on request (please contact us by email at contact@openmates.org and we will handle them manually):"
33196
+ google_vertex_maas: {
33197
+ heading: {
33198
+ text: "Google Vertex AI Model-as-a-Service (US)"
33199
+ },
33200
+ description: {
33201
+ text: "Google Vertex AI Model-as-a-Service hosts third-party open models (such as DeepSeek) on Google infrastructure via an OpenAI-compatible endpoint. This is a separate service from Google Gemini: the model weights are third-party, but the inference runs on Google's servers. Only used when you select a MaaS-hosted model."
33202
+ }
33203
+ },
33204
+ together: {
33205
+ heading: {
33206
+ text: "Together AI (US)"
33207
+ },
33208
+ description: {
33209
+ text: "Together AI hosts third-party models such as Kimi K2.6 on an OpenAI-compatible endpoint. Only used when you select a Together-hosted model. Together receives your chat messages with PII placeholders already substituted client-side."
33210
+ }
33211
+ },
33212
+ groq: {
33213
+ heading: {
33214
+ text: "Groq (US, content sanitization)"
33215
+ },
33216
+ description: {
33217
+ text: "Groq is used server-side to sanitize external content (search results and scraped web pages) before it is included in AI responses. Groq sees the external content we fetched on your behalf, not your chat messages, and never learns which user requested each piece of content."
33218
+ }
33219
+ },
33220
+ alibaba: {
33221
+ heading: {
33222
+ text: "Alibaba Cloud (Qwen models)"
33223
+ },
33224
+ description: {
33225
+ text: "Alibaba Cloud hosts Qwen-family models. Alibaba receives your chat messages with PII placeholders already substituted client-side only when you select an Alibaba-hosted model."
33226
+ }
33227
+ },
33228
+ deepseek: {
33229
+ heading: {
33230
+ text: "DeepSeek (reasoning models)"
33231
+ },
33232
+ description: {
33233
+ text: "DeepSeek provides DeepSeek-family reasoning models. DeepSeek receives your chat messages with PII placeholders already substituted client-side only when you select a DeepSeek model."
33234
+ }
33235
+ },
33236
+ moonshot: {
33237
+ heading: {
33238
+ text: "Moonshot AI / Kimi (language models)"
33239
+ },
33240
+ description: {
33241
+ text: "Moonshot AI provides Kimi-family models. Moonshot receives your chat messages with PII placeholders already substituted client-side only when you select a Moonshot-hosted Kimi model."
33242
+ }
33243
+ },
33244
+ zai: {
33245
+ heading: {
33246
+ text: "Z.ai (GLM models)"
33247
+ },
33248
+ description: {
33249
+ text: "Z.ai provides GLM-family models. Z.ai receives your chat messages with PII placeholders already substituted client-side only when you select a Z.ai model."
33250
+ }
32335
33251
  }
32336
33252
  },
32337
- ccpa_cpra: {
32338
- subheading: {
32339
- text: "CCPA/CPRA (California)"
33253
+ image_generation: {
33254
+ heading: {
33255
+ text: "Image generation and editing (only when you use an image skill)"
32340
33256
  },
32341
- right_to_know: {
32342
- text: "Right to know/access"
33257
+ description: {
33258
+ text: "These providers are only used when you invoke an image generation or editing skill. If you never use the images app, none of your data is shared with them."
32343
33259
  },
32344
- right_to_delete: {
32345
- text: "Right to delete"
33260
+ fal: {
33261
+ heading: {
33262
+ text: "FAL (Flux models)"
33263
+ },
33264
+ description: {
33265
+ text: "FAL hosts Flux image generation models used by the images.generate_draft skill. FAL receives your image generation prompts and, when editing, the source images you supply."
33266
+ }
32346
33267
  },
32347
- right_to_correct: {
32348
- text: "Right to correct"
33268
+ recraft: {
33269
+ heading: {
33270
+ text: "Recraft (vector and raster image generation)"
33271
+ },
33272
+ description: {
33273
+ text: "Recraft powers the images.generate and images.vectorize skills, producing vector and raster images and converting bitmaps to SVG. Recraft receives your prompts, style preferences, and \u2014 when vectorizing \u2014 your source images."
33274
+ }
32349
33275
  },
32350
- right_to_opt_out_of_sale_or_sharing: {
32351
- text: "Right to opt-out of sale or sharing"
33276
+ bfl: {
33277
+ heading: {
33278
+ text: "Black Forest Labs (FLUX image models)"
33279
+ },
33280
+ description: {
33281
+ text: "Black Forest Labs powers draft image generation via FLUX models. BFL receives your image generation prompts and, when editing, the source images you provide."
33282
+ }
33283
+ }
33284
+ },
33285
+ music_generation: {
33286
+ heading: {
33287
+ text: "Music generation (only when you use a music skill)"
32352
33288
  },
32353
- right_to_non_discrimination: {
32354
- text: "Right to non-discrimination"
33289
+ description: {
33290
+ text: "These providers are only used when you invoke a music generation skill. If you never use the music app, none of your data is shared with them."
32355
33291
  },
32356
- exercise: {
32357
- text: "California residents can exercise CCPA/CPRA rights by contacting us at contact@openmates.org. We do not sell personal information."
33292
+ google_vertex_ai: {
33293
+ heading: {
33294
+ text: "Google Vertex AI (Lyria music generation)"
33295
+ },
33296
+ description: {
33297
+ text: "Google Vertex AI hosts the Lyria models used by the music.generate skill. Google receives your music generation prompts, optional lyrics, style preferences, and generation settings only when you ask OpenMates to generate music or background music."
33298
+ }
32358
33299
  }
32359
- }
32360
- },
32361
- discord_integration: {
32362
- heading: {
32363
- text: "9. Discord Integration"
32364
33300
  },
32365
- description: {
32366
- text: "If you choose to join our Discord community through links provided on this website, please note that Discord will collect and process your data according to their privacy policy. This includes account information, usage data, communication content, and other information as outlined in Discord's privacy policy."
32367
- },
32368
- admin_access: {
32369
- text: "As Discord server administrators, we have access to all public communications, member lists, and other information shared within our Discord community. However, this information is hosted and primarily processed by Discord. We do not extract, store, or process this data outside of Discord's platform. For more information about how Discord handles your data, please review Discord's Privacy Policy."
33301
+ code_execution: {
33302
+ heading: {
33303
+ text: "Code and developer tools (only when you use code skills)"
33304
+ },
33305
+ description: {
33306
+ text: "These providers are only used when you invoke a code app skill, such as searching public repositories, running code in an isolated sandbox, creating a generated application that auto-starts a short screenshot preview, or explicitly starting a generated application live preview. If you never use the code app, none of your data is shared with them."
33307
+ },
33308
+ github: {
33309
+ heading: {
33310
+ text: "GitHub (public repository search)"
33311
+ },
33312
+ description: {
33313
+ text: "GitHub powers the code app's repository search skill. GitHub receives public repository search queries without an OpenMates user identifier."
33314
+ }
33315
+ },
33316
+ context7: {
33317
+ heading: {
33318
+ text: "Context7 (programming documentation lookup)"
33319
+ },
33320
+ description: {
33321
+ text: "Context7 powers the code app's documentation lookup skill. Context7 receives library names and documentation questions without an OpenMates user identifier."
33322
+ }
33323
+ },
33324
+ e2b: {
33325
+ heading: {
33326
+ text: "E2B (isolated code sandbox)"
33327
+ },
33328
+ description: {
33329
+ text: "E2B runs selected code embeds, generated application previews, and Remotion video renders inside isolated sandboxes. E2B receives the code or generated application/Remotion files and assets needed to run or render the result, runtime inputs, preview/render logs, and execution output needed to display the sandbox result. This sharing happens when you explicitly run code, create a generated application that auto-starts a short screenshot preview, start/resume an application live preview, or when a Remotion video render auto-starts after the assistant closes an explicit remotion code fence."
33330
+ }
33331
+ }
32370
33332
  },
32371
- privacy_policy_link: {
32372
- text: "Discord Privacy Policy"
32373
- }
32374
- },
32375
- contact: {
32376
- heading: {
32377
- text: "10. Contact Information"
32378
- },
32379
- questions: {
32380
- text: "For questions about data protection:"
32381
- },
32382
- email: {
32383
- text: "Email"
32384
- },
32385
- postal: {
32386
- text: "Postal address: See Legal Notice (Imprint) for the current registered address."
32387
- },
32388
- controller: {
32389
- text: "Controller: OpenMates (see Imprint for legal representative details)."
32390
- }
32391
- },
32392
- aggregate_analytics: {
32393
- heading: {
32394
- text: "11. Anonymous Aggregate Analytics"
32395
- },
32396
- overview: {
32397
- text: "We collect anonymous, aggregate statistics about how our website and web application are used. This data is genuinely anonymous \u2014 it cannot be linked to any individual user, and no cookies or tracking identifiers are set. No consent banner is required because no personal data is collected."
32398
- },
32399
- what_we_collect: {
32400
- text: "What we collect (all aggregate, never individual):"
32401
- },
32402
- items: {
32403
- text: "Daily page load counts; approximate unique visit counts (probabilistic HyperLogLog, ~0.81% error); country distribution (GeoIP lookup \u2014 IP discarded immediately, never stored); device class (mobile/tablet/desktop); browser family and major version; OS family; referrer domain (domain only, never full URL); screen size class; session duration distribution (bucketed)"
32404
- },
32405
- no_pii: {
32406
- text: "IP addresses are used transiently for GeoIP lookup only and are never written to disk or any database. User-Agent strings are parsed to metadata only (browser name, OS) \u2014 the raw string is never stored. All data is stored as daily aggregate counters, not as individual records."
32407
- }
32408
- },
32409
- follow_up_1: {
32410
- text: "How is my chat content encrypted?"
32411
- },
32412
- follow_up_2: {
32413
- text: "How does client-side encryption work?"
32414
- },
32415
- follow_up_3: {
32416
- text: "What if I lose my encryption key?"
32417
- },
32418
- follow_up_4: {
32419
- text: "Do AI providers see my messages?"
32420
- },
32421
- follow_up_5: {
32422
- text: "How do I delete my account?"
32423
- },
32424
- follow_up_6: {
32425
- text: "What data do you share with third parties?"
32426
- },
32427
- provider_link_label: {
32428
- text: "Privacy policy"
32429
- },
32430
- overview: {
32431
- heading: {
32432
- text: "Overview"
32433
- },
32434
- summary: {
32435
- text: "OpenMates is designed so that most of your data is encrypted on your device before it reaches our servers. This is **not** end-to-end encryption: our servers briefly decrypt your content in memory to run AI responses, render invoices, and deliver reminders. But we never write decrypted content to disk, logs, or traces, and when you delete your account we destroy the encryption key that protects your data \u2014 cryptographically shredding every encrypted field we still hold."
32436
- }
32437
- },
32438
- protection: {
32439
- heading: {
32440
- text: "How we protect your data"
32441
- },
32442
- intro: {
32443
- text: "We rely on six technical measures to protect your data. Each is verifiable in our open-source code and maps to a specific GDPR obligation."
32444
- },
32445
- client_side_encryption: {
32446
- heading: {
32447
- text: "Client-side encryption of your content"
32448
- },
32449
- description: {
32450
- text: "Your chat content, titles, summaries, tags, per-chat keys, app settings and memories, reminders, and sensitive profile fields (encrypted username, auto-top-up email, 2FA secrets) are encrypted in your browser before being sent to our servers. The browser uses a key derived from your login credential; our servers reject writes that contain anything but ciphertext. This is *not* end-to-end encryption \u2014 our servers can decrypt your content in memory when they need to (for AI responses, invoice rendering, reminder delivery). The difference from end-to-end encryption is that decryption happens transiently in process memory and the plaintext is never persisted to disk, logs, or traces."
32451
- }
32452
- },
32453
- pii_placeholder_substitution: {
32454
- heading: {
32455
- text: "PII placeholder substitution before AI calls"
32456
- },
32457
- description: {
32458
- text: "Before any message is sent to a third-party AI model, your browser scans your message for real names, email addresses, and physical addresses that you have saved in your privacy settings (or that match common patterns) and replaces any matches with neutral placeholders (for example [USER_0]). The mapping back to your real values is stored encrypted with your key and is never decrypted on our servers. **Important limitation:** this substitution only protects personal data that our client-side detector actually recognizes \u2014 typically the values you have explicitly saved in your privacy settings and values highlighted in the message field before you send. Anything the detector does not recognize (typos of your own name, uncommon address formats, other people's personal data, sensitive topics that are not formally PII) will be transmitted as you typed it. Please treat every message as you would treat a note passed to a third-party AI provider and be cautious with personal data."
32459
- }
32460
- },
32461
- encrypted_at_rest: {
32462
- heading: {
32463
- text: "Encrypted at rest, decrypted only in memory"
32464
- },
32465
- description: {
32466
- text: "Our PostgreSQL database, Redis caches, S3 object storage, and backup snapshots hold only ciphertext for any field containing user content. When our servers need to read your content to run an AI response, render an invoice, or deliver a reminder, decryption happens transiently in process memory and is discarded immediately after. Key material is released on demand by HashiCorp Vault and never stored in raw form outside it."
32467
- }
32468
- },
32469
- hashed_identifiers: {
32470
- heading: {
32471
- text: "Hashed identifiers throughout"
32472
- },
32473
- description: {
32474
- text: "Your user ID is stored in most database tables as a SHA-256 hash rather than as a direct reference. Login uses a zero-knowledge password verification flow: we verify that you know your password without ever learning it. API-key device records anonymize the IP address to its first two octets before encryption, so we can recognize a device without storing a precise location."
32475
- }
32476
- },
32477
- cryptographic_erasure: {
32478
- heading: {
32479
- text: "Cryptographically shredded on deletion"
32480
- },
32481
- description: {
32482
- text: "When you delete your account we destroy your HashiCorp Vault Transit key in addition to removing your rows from our database. Any encrypted field still sitting in a backup, cache, or running process becomes mathematically unrecoverable ciphertext at that moment \u2014 cryptographic erasure on top of row-level deletion."
32483
- }
32484
- },
32485
- observability_without_tracking: {
32486
- heading: {
32487
- text: "Observability without tracking"
32488
- },
32489
- description: {
32490
- text: "Our internal tracing pipeline (OpenTelemetry) replaces your user ID with a hash that rotates every 24 hours, strips authentication headers and cookies from every span, and redacts chat content before any trace leaves the backend. Our web analytics use a server-side beacon with no cookies and no stored individual identifiers. We do not use Google Analytics, Plausible, PostHog, or any third-party analytics platform."
32491
- }
32492
- }
32493
- },
32494
- promises: {
32495
- intro: {
32496
- text: "Below are the privacy promises we make to every user. Each one is backed by code in our open-source repository and verified by automated tests. When the enforcing code changes, a registry-linked check reminds us to rerun those tests before release."
32497
- },
32498
- client_side_chat_encryption: {
32499
- heading: {
32500
- text: "Client-side chat encryption"
32501
- },
32502
- description: {
32503
- text: "Your chats, settings, and memories are encrypted on your device with AES-256-GCM before they leave your browser. The server stores only ciphertext on disk, in caches, and in backups. This is not end-to-end encryption: when a server-side task needs to read content (to run an AI response, render an invoice, or deliver a reminder), it decrypts in memory via HashiCorp Vault and discards the plaintext immediately after use."
32504
- }
32505
- },
32506
- email_encryption_at_rest: {
32507
- heading: {
32508
- text: "Email addresses encrypted at rest"
32509
- },
32510
- description: {
32511
- text: "Your email address is stored only as ciphertext. The server decrypts it transiently in memory during login, billing, and notification delivery, and never writes plaintext to disk or logs."
32512
- }
32513
- },
32514
- no_third_party_tracking: {
32515
- heading: {
32516
- text: "No third-party tracking cookies or analytics"
32517
- },
32518
- description: {
32519
- text: "We do not use Google Analytics, Plausible, PostHog, Mixpanel, Amplitude, Segment, or any other third-party analytics platform. Our telemetry is server-side, cookie-free, and contains no individually stored identifiers. A pre-edit check in our repository blocks the introduction of analytics SDKs."
32520
- }
32521
- },
32522
- pii_placeholder_substitution: {
32523
- heading: {
32524
- text: "Personal information never reaches AI providers"
32525
- },
32526
- description: {
32527
- text: "Before any message is sent to an AI provider, your browser detects 32 categories of personal data \u2014 emails, phone numbers, credit card numbers, API keys, and more \u2014 and replaces them with placeholders like `[EMAIL_com]`. The substitution map is encrypted with your chat key so only your devices can restore the original values."
32528
- }
32529
- },
32530
- telemetry_privacy_filter: {
32531
- heading: {
32532
- text: "Telemetry stripped of sensitive data"
32533
- },
32534
- description: {
32535
- text: "Our internal tracing pipeline (OpenTelemetry) strips authentication headers, cookies, and database statements from every span, and pseudonymises your user ID with a salt that rotates every 24 hours. Regular users never have their raw identifiers in telemetry."
32536
- }
32537
- },
32538
- cryptographic_erasure: {
32539
- heading: {
32540
- text: "Deleted accounts are cryptographically erased"
32541
- },
32542
- description: {
32543
- text: "When you delete your account, the first step destroys your encryption keys. Any residual ciphertext in backups, caches, or audit logs is rendered permanently unreadable \u2014 no key, no content."
32544
- }
32545
- },
32546
- argon2_password_hashing: {
32547
- heading: {
32548
- text: "Passwords stored as Argon2 hashes"
32549
- },
32550
- description: {
32551
- text: "We store password and backup-code verifiers as Argon2 hashes. Plaintext passwords are never persisted to disk, never written to logs, and never recoverable \u2014 even by us."
32552
- }
32553
- },
32554
- payment_data_minimization: {
32555
- heading: {
32556
- text: "No card numbers ever touch our servers"
32557
- },
32558
- description: {
32559
- text: "Payments flow directly from your browser to Stripe. We store only provider tokens and customer identifiers. We never see, receive, or persist full card numbers, security codes, or bank account details."
32560
- }
32561
- },
32562
- logging_redaction: {
32563
- heading: {
32564
- text: "Logs are scrubbed of sensitive data"
32565
- },
32566
- description: {
32567
- text: "Every log line passes through a redaction filter that strips email addresses, IP addresses, bearer tokens, and password values. Compliance logs preserve pseudonymous user IDs but remove all other personal data."
32568
- }
32569
- },
32570
- prompt_injection_defense: {
32571
- heading: {
32572
- text: "Defense against prompt injection"
32573
- },
32574
- description: {
32575
- text: "Two layers protect your conversations from malicious content in web pages, files, and URLs you reference. Invisible Unicode characters are stripped first, then a dedicated safety model detects semantic injection attempts and blocks or replaces high-risk content before it reaches your assistant."
32576
- }
32577
- },
32578
- no_training_on_user_data: {
32579
- heading: {
32580
- text: "Your conversations are not used to train AI models"
32581
- },
32582
- description: {
32583
- text: "All AI chat providers we use (Anthropic, OpenAI, Mistral, Google Gemini, Google Vertex AI, Together AI, Groq, Cerebras, and OpenRouter) have explicit no-training clauses in their terms \u2014 your conversations are never used to fine-tune, evaluate, or otherwise train AI models. For image generation, Recraft has a training opt-out which we have activated. fal.ai (used for draft image generation) may use anonymized, aggregated derivatives of prompts for model improvement under their standard API terms; no full opt-out is available outside an enterprise contract. A per-provider audit with verbatim policy quotes and verification dates is published in our open-source repository."
32584
- }
32585
- },
32586
- no_external_resources: {
32587
- heading: {
32588
- text: "The web app never loads external images or scripts"
32589
- },
32590
- description: {
32591
- text: "Every external image, favicon, or preview shown in the app is fetched through our own preview.openmates.org proxy \u2014 the originating website never sees your IP address. The only third-party script the web app loads is the Stripe payment SDK needed to process a purchase, and only when you explicitly open the payment flow. There is no ad network, tag manager, CDN font loader, or third-party analytics library."
32592
- }
32593
- },
32594
- cli_no_credential_prompts: {
32595
- heading: {
32596
- text: "The command-line tool never asks for your password"
32597
- },
32598
- description: {
32599
- text: "The OpenMates CLI never prompts you for your email, password, 2FA code, or any other login credential. Authentication uses a browser-based pair-auth handshake: the CLI shows a QR code and URL, you approve the session in your logged-in web browser, and you type a short 6-character binding PIN back into the CLI to confirm. Your credentials never pass through the terminal."
32600
- }
32601
- },
32602
- open_source_transparency: {
33333
+ web_and_search: {
32603
33334
  heading: {
32604
- text: "Every claim on this page is independently auditable"
33335
+ text: "Web, search, and content retrieval (only when you use a web or search skill)"
32605
33336
  },
32606
33337
  description: {
32607
- text: "OpenMates is open source. Every promise above is backed by code you can read, tests you can run, and architecture documents you can audit in our public repository. Our privacy-promises registry links each claim to the exact files that enforce it."
32608
- }
32609
- }
32610
- },
32611
- providers: {
32612
- heading: {
32613
- text: "When each provider is used"
32614
- },
32615
- intro: {
32616
- text: "Most of the providers below are only reached when you use a specific feature \u2014 for example, image generation providers are only used when you invoke an image skill. The groups below tell you exactly which providers are involved for each feature, so you can opt in or out of data sharing by choosing which features to use."
32617
- },
32618
- always_active: {
32619
- heading: {
32620
- text: "Always active (used for every user)"
33338
+ text: "These providers are only used when you invoke a web, videos, news, or web-read skill. They see the content of what you search for or the URLs you ask to read, but no user identifier \u2014 they do not know which OpenMates user made each request."
32621
33339
  },
32622
- description: {
32623
- text: "These providers are used for every user, regardless of which features you use. Using OpenMates at all means your data passes through them."
33340
+ brave: {
33341
+ heading: {
33342
+ text: "Brave Search (web, videos, news)"
33343
+ },
33344
+ description: {
33345
+ text: "Brave Search powers the web, videos, and news search skills. Brave sees the text of your search queries and your country and language preferences for localized results, but no user identifier."
33346
+ }
32624
33347
  },
32625
- vercel: {
33348
+ firecrawl: {
32626
33349
  heading: {
32627
- text: "Vercel (frontend hosting)"
33350
+ text: "Firecrawl (web page scraping)"
32628
33351
  },
32629
33352
  description: {
32630
- text: "We use Vercel to host the static frontend of our website and web app. Vercel receives page requests and serves static assets."
33353
+ text: "Firecrawl powers the web read skill, scraping and extracting content from web pages you ask about. Firecrawl sees the URL you requested and the full content of the scraped page."
32631
33354
  }
32632
33355
  },
32633
- hetzner: {
33356
+ webshare: {
32634
33357
  heading: {
32635
- text: "Hetzner (backend, database, storage, observability)"
33358
+ text: "Webshare (rotating proxy)"
32636
33359
  },
32637
33360
  description: {
32638
- text: "We host all of our backend services \u2014 API servers, Directus CMS, Postgres database, Redis caches, S3-compatible object storage, and our self-hosted OpenObserve observability platform \u2014 on Hetzner infrastructure in the EU. OpenObserve runs on our own Hetzner servers and is not a third-party service; traces never leave the EU."
33361
+ text: "Webshare provides a rotating proxy for requests to services that would otherwise block us. We route YouTube transcript requests (for the videos get-transcript skill) and doctor appointment searches (for the health search-appointments skill \u2014 see Group H) through Webshare. Webshare sees the URLs we route through it."
32639
33362
  }
32640
33363
  },
32641
- brevo: {
33364
+ google_maps: {
32642
33365
  heading: {
32643
- text: "Brevo (transactional email)"
33366
+ text: "Google Maps Platform (places search)"
32644
33367
  },
32645
33368
  description: {
32646
- text: "We use Brevo to send transactional email \u2014 account verification, password reset links, payment receipts, and credit notes \u2014 and, if you opt in, the OpenMates newsletter. If you are an existing OpenMates customer (i.e. you have completed a purchase), we may also send you occasional product announcements and service updates about OpenMates' own similar services under the existing-customer exception (\xA77 Abs. 3 UWG / ePrivacy Directive Art. 13(2)); you can object in Settings \u2192 Newsletter or via the one-click unsubscribe link in any such email. Brevo sees recipient email addresses and the content of the emails we send you."
33369
+ text: "Google Maps powers the places search skill. Google sees the text of your place queries and an approximate location for localized results, but no user identifier."
32647
33370
  }
32648
33371
  },
32649
- ip_api: {
33372
+ youtube: {
32650
33373
  heading: {
32651
- text: "IP-API (IP geolocation)"
33374
+ text: "YouTube (video transcripts)"
32652
33375
  },
32653
33376
  description: {
32654
- text: "We use IP-API to resolve IP addresses to approximate geolocation for abuse prevention, rate limiting, and regional compliance. The resolved country and city are used transiently and are not stored against your account."
33377
+ text: "YouTube is queried when you ask OpenMates to retrieve a transcript for a YouTube video. YouTube receives video URLs and requested transcript language preferences without an OpenMates user identifier."
32655
33378
  }
33379
+ }
33380
+ },
33381
+ travel: {
33382
+ heading: {
33383
+ text: "Travel (only when you use a travel skill)"
32656
33384
  },
32657
- sightengine: {
33385
+ description: {
33386
+ text: "These providers are only used when you invoke a travel skill \u2014 flight search, hotel search, or flight tracking. If you never use the travel app, none of your data is shared with them."
33387
+ },
33388
+ serpapi: {
32658
33389
  heading: {
32659
- text: "Sightengine (image and video moderation)"
33390
+ text: "SerpAPI (flight and hotel search)"
32660
33391
  },
32661
33392
  description: {
32662
- text: "Every image and video you upload is scanned by Sightengine for safety moderation before it is stored. Sightengine sees the image or video content."
33393
+ text: "SerpAPI powers flight and hotel search via its Google Flights and Google Hotels endpoints. SerpAPI receives your search queries \u2014 origin, destination, dates, and preferences \u2014 without a user identifier."
32663
33394
  }
32664
33395
  },
32665
- api_video: {
33396
+ flightradar24: {
32666
33397
  heading: {
32667
- text: "api.video (product video hosting)"
33398
+ text: "Flightradar24 (flight tracking)"
32668
33399
  },
32669
33400
  description: {
32670
- text: "We use api.video (EU company, France) to host and stream product demo and feature walkthrough videos shown on our website and web app. When you watch a video, your IP address and basic viewing data are processed by api.video. No user account identifiers are shared."
33401
+ text: "Flightradar24 provides real flight track data when you ask about a specific flight number. It receives IATA flight numbers and departure dates without a user identifier."
32671
33402
  }
32672
- }
32673
- },
32674
- payments: {
32675
- heading: {
32676
- text: "Payment providers (only when you purchase credits)"
32677
33403
  },
32678
- description: {
32679
- text: "These providers are only used when you purchase credits, subscribe to monthly auto top-up, or receive a refund. If you never make a purchase, none of your data is shared with them."
33404
+ deutsche_bahn: {
33405
+ heading: {
33406
+ text: "Deutsche Bahn (train connections)"
33407
+ },
33408
+ description: {
33409
+ text: "Deutsche Bahn provides train connection results when you search for rail routes. Deutsche Bahn receives origin, destination, and date search details without an OpenMates user identifier."
33410
+ }
32680
33411
  },
32681
- stripe: {
33412
+ flix: {
32682
33413
  heading: {
32683
- text: "Stripe (primary payment processor)"
33414
+ text: "FlixBus / FlixTrain (bus and train connections)"
32684
33415
  },
32685
33416
  description: {
32686
- text: "Stripe processes one-time credit purchases and monthly auto top-up subscriptions. We send Stripe your email address, a tokenized payment method (we never see your card number), billing address and VAT information when required for invoicing, and transaction metadata."
33417
+ text: "Flix provides intercity bus and rail connection results when you search for FlixBus or FlixTrain routes. Flix receives origin, destination, and date search details without an OpenMates user identifier."
32687
33418
  }
32688
33419
  }
32689
33420
  },
32690
- ai_models: {
33421
+ events: {
32691
33422
  heading: {
32692
- text: "AI model providers (only when you use a model that routes through them)"
33423
+ text: "Events (only when you use the events skill)"
32693
33424
  },
32694
33425
  description: {
32695
- text: 'OpenMates routes AI requests to different providers depending on which model you select. Before any message is sent to a provider in this group, your browser replaces real names, emails, and physical addresses with placeholders (see the "PII placeholder substitution" measure above). You can always see which provider a model uses in the model details panel in AI settings.'
33426
+ text: "These providers are only used when you invoke the events search skill. The specific set of providers called depends on the city and event type you search for."
32696
33427
  },
32697
- mistral: {
33428
+ meetup: {
32698
33429
  heading: {
32699
- text: "Mistral (EU direct)"
33430
+ text: "Meetup"
32700
33431
  },
32701
33432
  description: {
32702
- text: "Mistral hosts its own family of models in the EU. Only used when you select a Mistral model. Mistral receives your chat messages with PII placeholders already substituted client-side."
33433
+ text: "Meetup powers event search for community events. Meetup receives your search queries \u2014 city, category, dates \u2014 without a user identifier."
32703
33434
  }
32704
33435
  },
32705
- aws_bedrock: {
33436
+ luma: {
32706
33437
  heading: {
32707
- text: "AWS Bedrock (EU region, primary Claude path)"
33438
+ text: "Luma"
32708
33439
  },
32709
33440
  description: {
32710
- text: "AWS Bedrock hosts Anthropic Claude models in its Frankfurt EU region. This is the primary path for Claude requests. AWS receives your chat messages with PII placeholders already substituted client-side."
33441
+ text: "Luma powers event search for tech, creative, and community events. Luma receives your search queries without a user identifier."
32711
33442
  }
32712
33443
  },
32713
- anthropic: {
33444
+ resident_advisor: {
32714
33445
  heading: {
32715
- text: "Anthropic (US, fallback Claude path)"
33446
+ text: "Resident Advisor"
32716
33447
  },
32717
33448
  description: {
32718
- text: "Anthropic's direct API is used as a fallback path for Claude models when AWS Bedrock is unreachable. Anthropic is based in the US and receives your chat messages with PII placeholders already substituted client-side."
33449
+ text: "Resident Advisor powers event search for electronic music events. RA receives your search queries without a user identifier."
32719
33450
  }
33451
+ }
33452
+ },
33453
+ health: {
33454
+ heading: {
33455
+ text: "Health appointment search (only when you use the health skill)"
32720
33456
  },
32721
- openai: {
33457
+ description: {
33458
+ text: `These upstream services are reached through the Webshare rotating proxy (see Group E) only when you invoke the health app's appointment search skill. The queries you send \u2014 for example "dermatologist in Berlin next Tuesday" \u2014 are transmitted without user identifiers, but the content of the query itself can imply a health concern. By invoking the skill you consent to this processing under GDPR Art. 9(2)(a) (explicit consent for special-category data).`
33459
+ },
33460
+ doctolib: {
32722
33461
  heading: {
32723
- text: "OpenAI (US)"
33462
+ text: "Doctolib (EU)"
32724
33463
  },
32725
33464
  description: {
32726
- text: "OpenAI processes requests for GPT-family models when you select one. OpenAI receives your chat messages with PII placeholders already substituted client-side."
33465
+ text: "Doctolib lists doctors and specialists across supported EU regions. We query Doctolib for available appointments matching your criteria, routed through the Webshare proxy, without a user identifier."
32727
33466
  }
32728
33467
  },
32729
- openrouter: {
33468
+ jameda: {
32730
33469
  heading: {
32731
- text: "OpenRouter (US, routing aggregator)"
33470
+ text: "Jameda (Germany)"
32732
33471
  },
32733
33472
  description: {
32734
- text: "OpenRouter is used as a routing aggregator for several third-party models, including Cerebras-hosted models. OpenRouter sees your chat messages with PII placeholders substituted client-side and forwards them to the underlying model host."
33473
+ text: "Jameda lists doctors and specialists across Germany. We query Jameda for available appointments matching your criteria, routed through the Webshare proxy, without a user identifier."
32735
33474
  }
33475
+ }
33476
+ },
33477
+ shopping: {
33478
+ heading: {
33479
+ text: "Shopping (only when you use the shopping skill)"
32736
33480
  },
32737
- cerebras: {
33481
+ description: {
33482
+ text: "These providers are only used when you invoke the shopping search skill for live product and price lookups."
33483
+ },
33484
+ rewe: {
32738
33485
  heading: {
32739
- text: "Cerebras (US, via OpenRouter)"
33486
+ text: "REWE (German grocery)"
32740
33487
  },
32741
33488
  description: {
32742
- text: "Cerebras hosts some of the high-speed models routed through OpenRouter. Only used when you select a Cerebras-hosted model."
33489
+ text: "REWE's product catalogue is queried for live product and price data when you search German grocery items. REWE receives your search queries without a user identifier."
32743
33490
  }
32744
33491
  },
32745
- google_gemini: {
33492
+ amazon: {
32746
33493
  heading: {
32747
- text: "Google Gemini (US)"
33494
+ text: "Amazon"
32748
33495
  },
32749
33496
  description: {
32750
- text: "Google Gemini processes requests for Gemini-family models when you select one. Google receives your chat messages with PII placeholders already substituted client-side."
33497
+ text: "Amazon's product catalogue is queried for live product and price data. Amazon receives your search queries without a user identifier."
32751
33498
  }
33499
+ }
33500
+ },
33501
+ nutrition: {
33502
+ heading: {
33503
+ text: "Nutrition (only when you use the recipe search skill)"
32752
33504
  },
32753
- google_vertex_maas: {
33505
+ description: {
33506
+ text: "These providers are only used when you invoke the nutrition recipe search skill. They receive recipe search queries and selected dietary filters without a user identifier."
33507
+ },
33508
+ edamam: {
32754
33509
  heading: {
32755
- text: "Google Vertex AI Model-as-a-Service (US)"
33510
+ text: "Edamam"
32756
33511
  },
32757
33512
  description: {
32758
- text: "Google Vertex AI Model-as-a-Service hosts third-party open models (such as DeepSeek) on Google infrastructure via an OpenAI-compatible endpoint. This is a separate service from Google Gemini: the model weights are third-party, but the inference runs on Google's servers. Only used when you select a MaaS-hosted model."
33513
+ text: "Edamam's Recipe Search API is queried when you search for recipes. Edamam receives the recipe query and selected dietary or nutrition filters without a user identifier."
32759
33514
  }
33515
+ }
33516
+ },
33517
+ electronics: {
33518
+ heading: {
33519
+ text: "Electronics (only when you use the electronics skill)"
32760
33520
  },
32761
- together: {
33521
+ description: {
33522
+ text: "These providers are only used when you invoke the electronics component search skill for live component and reference-design lookups."
33523
+ },
33524
+ ti_webench: {
32762
33525
  heading: {
32763
- text: "Together AI (US)"
33526
+ text: "Texas Instruments WEBENCH (US)"
32764
33527
  },
32765
33528
  description: {
32766
- text: "Together AI hosts third-party models such as Kimi K2.6 on an OpenAI-compatible endpoint. Only used when you select a Together-hosted model. Together receives your chat messages with PII placeholders already substituted client-side."
33529
+ text: "TI WEBENCH is queried for power converter component candidates and reference design summaries. Texas Instruments receives the electrical requirements you search for, such as input voltage, output voltage, output current, temperature, isolation, and optimization goal, without a user identifier."
32767
33530
  }
33531
+ }
33532
+ },
33533
+ mail: {
33534
+ heading: {
33535
+ text: "Mail (only when you use mail skills)"
32768
33536
  },
32769
- groq: {
33537
+ description: {
33538
+ text: "This provider is only used when you invoke mail skills against a connected mailbox. If you never use the mail app, none of your data is shared with it."
33539
+ },
33540
+ protonmail: {
32770
33541
  heading: {
32771
- text: "Groq (US, content sanitization)"
33542
+ text: "Proton Mail (mail search)"
32772
33543
  },
32773
33544
  description: {
32774
- text: "Groq is used server-side to sanitize external content (search results and scraped web pages) before it is included in AI responses. Groq sees the external content we fetched on your behalf, not your chat messages, and never learns which user requested each piece of content."
33545
+ text: "Proton Mail is queried when you search a connected Proton Mail mailbox. Proton receives the mail search query and mailbox data needed to return matching results."
32775
33546
  }
33547
+ }
33548
+ },
33549
+ home: {
33550
+ heading: {
33551
+ text: "Home and housing (only when you use housing search)"
32776
33552
  },
32777
- alibaba: {
33553
+ description: {
33554
+ text: "These providers are only used when you invoke the home app's housing search skill. If you never search for housing, none of your data is shared with them."
33555
+ },
33556
+ immoscout24: {
32778
33557
  heading: {
32779
- text: "Alibaba Cloud (Qwen models)"
33558
+ text: "ImmoScout24 (German housing search)"
32780
33559
  },
32781
33560
  description: {
32782
- text: "Alibaba Cloud hosts Qwen-family models. Alibaba receives your chat messages with PII placeholders already substituted client-side only when you select an Alibaba-hosted model."
33561
+ text: "ImmoScout24 is queried when you search German housing listings. ImmoScout24 receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
32783
33562
  }
32784
33563
  },
32785
- deepseek: {
33564
+ kleinanzeigen: {
32786
33565
  heading: {
32787
- text: "DeepSeek (reasoning models)"
33566
+ text: "Kleinanzeigen (German classified housing search)"
32788
33567
  },
32789
33568
  description: {
32790
- text: "DeepSeek provides DeepSeek-family reasoning models. DeepSeek receives your chat messages with PII placeholders already substituted client-side only when you select a DeepSeek model."
33569
+ text: "Kleinanzeigen is queried when you search German classified housing listings. Kleinanzeigen receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
32791
33570
  }
32792
33571
  },
32793
- moonshot: {
33572
+ wg_gesucht: {
32794
33573
  heading: {
32795
- text: "Moonshot AI / Kimi (language models)"
33574
+ text: "WG-Gesucht (German shared-apartment search)"
32796
33575
  },
32797
33576
  description: {
32798
- text: "Moonshot AI provides Kimi-family models. Moonshot receives your chat messages with PII placeholders already substituted client-side only when you select a Moonshot-hosted Kimi model."
33577
+ text: "WG-Gesucht is queried when you search German shared-apartment or rental listings. WG-Gesucht receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
32799
33578
  }
33579
+ }
33580
+ },
33581
+ community: {
33582
+ heading: {
33583
+ text: "Community and developer channels (only if you choose to use them)"
32800
33584
  },
32801
- zai: {
33585
+ description: {
33586
+ text: "These providers are only involved if you choose to join our community or developer channels. You can use OpenMates without ever touching them."
33587
+ },
33588
+ discord: {
32802
33589
  heading: {
32803
- text: "Z.ai (GLM models)"
33590
+ text: "Discord (community server)"
32804
33591
  },
32805
33592
  description: {
32806
- text: "Z.ai provides GLM-family models. Z.ai receives your chat messages with PII placeholders already substituted client-side only when you select a Z.ai model."
33593
+ text: "If you join the OpenMates community Discord server, Discord will see anything you post on its platform, plus your Discord username and whatever data Discord itself collects per its own privacy policy. We have no control over what Discord does with that data."
33594
+ },
33595
+ admin_access: {
33596
+ text: "OpenMates administrators with Discord server admin rights can read all messages posted in the OpenMates Discord server. Do not share sensitive information on Discord."
32807
33597
  }
32808
33598
  }
32809
33599
  },
32810
- image_generation: {
33600
+ social_media: {
32811
33601
  heading: {
32812
- text: "Image generation and editing (only when you use an image skill)"
33602
+ text: "Social media (only when you use social media skills)"
32813
33603
  },
32814
33604
  description: {
32815
- text: "These providers are only used when you invoke an image generation or editing skill. If you never use the images app, none of your data is shared with them."
33605
+ text: "These providers are only used when you invoke the social media app's get-posts or search skills. They receive public profile, page, or search queries without an OpenMates user identifier."
32816
33606
  },
32817
- fal: {
33607
+ reddit: {
32818
33608
  heading: {
32819
- text: "FAL (Flux models)"
33609
+ text: "Reddit (public posts and discussions)"
32820
33610
  },
32821
33611
  description: {
32822
- text: "FAL hosts Flux image generation models used by the images.generate_draft skill. FAL receives your image generation prompts and, when editing, the source images you supply."
33612
+ text: "Reddit is queried when you fetch or search public Reddit posts. Reddit receives subreddit names, public post URLs, and search queries without an OpenMates user identifier."
32823
33613
  }
32824
33614
  },
32825
- recraft: {
33615
+ bluesky: {
32826
33616
  heading: {
32827
- text: "Recraft (vector and raster image generation)"
33617
+ text: "Bluesky (public posts and profile feeds)"
32828
33618
  },
32829
33619
  description: {
32830
- text: "Recraft powers the images.generate and images.vectorize skills, producing vector and raster images and converting bitmaps to SVG. Recraft receives your prompts, style preferences, and \u2014 when vectorizing \u2014 your source images."
33620
+ text: "Bluesky is queried when you fetch or search public Bluesky posts. Bluesky receives handles and search queries without an OpenMates user identifier."
33621
+ }
33622
+ },
33623
+ mastodon: {
33624
+ heading: {
33625
+ text: "Mastodon (public profile posts)"
33626
+ },
33627
+ description: {
33628
+ text: "Mastodon is queried when you fetch public Mastodon profile posts. The relevant Mastodon server receives profile identifiers or public profile URLs without an OpenMates user identifier."
32831
33629
  }
33630
+ }
33631
+ }
33632
+ },
33633
+ flightradar24: {
33634
+ heading: {
33635
+ text: "3.16 Flightradar24"
33636
+ },
33637
+ description: {
33638
+ text: 'When you request real flight track data using the "Get flight track" skill, we query Flightradar24 to retrieve historical GPS track data for completed flights. Flightradar24 receives the flight number and departure date. No personal data is shared.'
33639
+ },
33640
+ privacy_policy_link: {
33641
+ text: "Flightradar24 Privacy Policy"
33642
+ }
33643
+ },
33644
+ security_measures: {
33645
+ heading: {
33646
+ text: "4. Security Measures"
33647
+ },
33648
+ intro: {
33649
+ text: "We implement various security measures to protect your account and data in the web application:"
33650
+ },
33651
+ device_fingerprinting: {
33652
+ subheading: {
33653
+ text: "Device Fingerprinting"
33654
+ },
33655
+ purpose: {
33656
+ text: "To enhance account security, prevent fraudulent access, and distinguish between new and recognized devices during login and session validation, we collect User-Agent strings (browser, OS, device type information) and IP addresses (used temporarily to derive approximate geo-location like country/city, then discarded from the device record)."
33657
+ },
33658
+ storage: {
33659
+ text: "A unique, stable hash is generated based on a combination of the collected signals. This hash is the only value stored in the device record. Browser/OS type and derived country code may be used transiently for risk assessment and notifications but are not stored with the device record. Raw IP addresses are NOT stored as part of the device record itself."
33660
+ },
33661
+ ip_logging: {
33662
+ text: "For security monitoring and abuse prevention, IP addresses associated with failed login attempts (invalid password or 2FA code) may be logged with limited retention. In addition, certain security/compliance events (e.g., successful backup-code logins) may be logged with IP for audit purposes. IP addresses are also used for rate limiting login attempts."
33663
+ }
33664
+ }
33665
+ },
33666
+ data_categories: {
33667
+ heading: {
33668
+ text: "5. Data Categories We Collect and Process"
33669
+ },
33670
+ intro: {
33671
+ text: "We collect and process the following categories of data to operate and secure our services:"
33672
+ },
33673
+ account: {
33674
+ text: "Account: Email address, username/display name, profile image, locale, and security settings (e.g., 2FA enabled). Email and username are encrypted with your key before storage. We also keep a separate server-side Vault-encrypted copy of your verified email address for mandatory account lifecycle notices, such as account verification, security alerts, and deletion reminders. Passwords are stored as salted hashes."
33675
+ },
33676
+ usage: {
33677
+ text: "Usage: Server logs, event timestamps, feature usage, error logs, and device recognition identifiers (hashed). IP addresses may be temporarily processed for security and rate limiting."
33678
+ },
33679
+ content: {
33680
+ text: "Content: Chat messages, prompts, attachments, and uploaded images/videos necessary to deliver the service (subject to moderation where applicable)."
33681
+ },
33682
+ payments: {
33683
+ text: "Payments: Payment method tokens, transaction IDs, billing address and VAT information as required for invoicing (processed primarily by Stripe). We do not store full card numbers."
33684
+ },
33685
+ newsletter: {
33686
+ text: "Newsletter: If you choose to subscribe to our newsletter, we store your email address in encrypted form. Newsletter subscription is optional and separate from account creation \u2014 creating an account does not automatically subscribe you to the newsletter. If you are an OpenMates customer (i.e. you have completed a purchase), we may additionally send you occasional product announcements and service updates about OpenMates' own similar services under the existing-customer exception (\xA77 Abs. 3 UWG / ePrivacy Directive Art. 13(2)). You can object to this at any time in Settings \u2192 Newsletter or via the one-click unsubscribe link in any such email, without affecting your account."
33687
+ },
33688
+ stability_logs: {
33689
+ text: "Stability Logs: To diagnose technical errors, anonymized browser console logs (error messages, component status, connection events) are collected from authenticated users and stored for up to 48 hours. If an error occurs, surrounding context is retained for up to 14 days. No message content, chat titles, names, email addresses, or other personally identifiable information is included. A random session identifier (generated per browser tab, not linked to your account) is used for log correlation. You can disable this at any time in Settings > Privacy > Stability Logs."
33690
+ }
33691
+ },
33692
+ data_retention: {
33693
+ heading: {
33694
+ text: "6. Data Retention"
33695
+ },
33696
+ account: {
33697
+ text: "Account data: Retained until account deletion and completion of related legal obligations."
33698
+ },
33699
+ usage_and_logs: {
33700
+ text: "Usage and logs: Operational logs retained up to 12 months; IPs tied to failed logins up to 30 days for security."
33701
+ },
33702
+ device_fingerprints: {
33703
+ text: "Device fingerprints: Retained until account deletion or manual removal from account settings (when available)."
33704
+ },
33705
+ content: {
33706
+ text: "Content: Retained until the user deletes the content or closes the account, subject to backup cycles."
33707
+ },
33708
+ payments_and_invoices: {
33709
+ text: "Payments and invoices: Retained up to 10 years where required by tax/commercial law."
33710
+ },
33711
+ compliance_logs: {
33712
+ text: "**Compliance logs:** audit logs retained 2 years (BSI \xA734 BDSG); financial compliance logs retained 10 years (AO \xA7147 and HGB \xA7257)"
33713
+ },
33714
+ observability_traces: {
33715
+ text: "**Observability traces:** up to 30 days; user IDs in traces use a hash that rotates every 24 hours, so older traces cannot be joined to a specific user"
33716
+ },
33717
+ user_data_backups: {
33718
+ text: "**User data backups:** 60 days via S3 lifecycle; backups contain only ciphertext encrypted with your own key"
33719
+ }
33720
+ },
33721
+ legal_basis: {
33722
+ heading: {
33723
+ text: "7. Legal Basis for Processing (GDPR)"
33724
+ },
33725
+ contract: {
33726
+ text: "Contract: Processing necessary to perform our contract with you or to take steps at your request prior to entering into a contract (e.g., account creation, delivering web app features)."
33727
+ },
33728
+ consent: {
33729
+ text: "Consent: Processing based on your consent, where applicable (e.g., optional communications, certain analytics or marketing where used). You can withdraw consent at any time."
33730
+ },
33731
+ legitimate_interests: {
33732
+ text: "Legitimate interests: Processing necessary for our legitimate interests, such as service security, fraud prevention, abuse detection, and improving the service, provided these interests are not overridden by your rights and interests."
33733
+ },
33734
+ direct_marketing: {
33735
+ text: "Direct marketing to existing customers: For existing OpenMates customers (users who have completed a purchase), we may rely on Art. 6(1)(f) GDPR (legitimate interests) in combination with \xA77 Abs. 3 UWG and ePrivacy Directive Art. 13(2) to send occasional product announcements, service updates, and marketing about OpenMates' own similar services. Each such email includes a clearly visible one-click unsubscribe link, and you can also manage these preferences at any time in Settings \u2192 Newsletter. You can object to this processing at any time without giving reasons (Art. 21 GDPR)."
33736
+ },
33737
+ legal_obligation: {
33738
+ text: "Legal obligation: Processing necessary to comply with legal obligations (e.g., tax and accounting requirements for invoices and transaction records)."
33739
+ }
33740
+ },
33741
+ legal_rights: {
33742
+ heading: {
33743
+ text: "8. Your Privacy Rights"
33744
+ },
33745
+ intro: {
33746
+ text: "Depending on your jurisdiction, you may have certain rights over your personal data. The following summarizes key rights under GDPR and CCPA/CPRA and how to exercise them:"
33747
+ },
33748
+ gdpr: {
33749
+ subheading: {
33750
+ text: "GDPR (EU/EEA)"
33751
+ },
33752
+ access: {
33753
+ text: "Right of access"
33754
+ },
33755
+ rectification: {
33756
+ text: "Right to rectification"
33757
+ },
33758
+ erasure: {
33759
+ text: "Right to erasure ('right to be forgotten')"
33760
+ },
33761
+ restriction: {
33762
+ text: "Right to restriction of processing"
33763
+ },
33764
+ portability: {
33765
+ text: "Right to data portability"
33766
+ },
33767
+ objection: {
33768
+ text: "Right to object"
33769
+ },
33770
+ withdraw_consent: {
33771
+ text: "Right to withdraw consent"
33772
+ },
33773
+ exercise: {
33774
+ text: "To exercise your GDPR rights, contact us at contact@openmates.org or via the Imprint. We may need to verify your identity before fulfilling your request."
33775
+ },
33776
+ manual_note: {
33777
+ text: "The following rights are available on request (please contact us by email at contact@openmates.org and we will handle them manually):"
33778
+ }
33779
+ },
33780
+ ccpa_cpra: {
33781
+ subheading: {
33782
+ text: "CCPA/CPRA (California)"
33783
+ },
33784
+ right_to_know: {
33785
+ text: "Right to know/access"
33786
+ },
33787
+ right_to_delete: {
33788
+ text: "Right to delete"
33789
+ },
33790
+ right_to_correct: {
33791
+ text: "Right to correct"
33792
+ },
33793
+ right_to_opt_out_of_sale_or_sharing: {
33794
+ text: "Right to opt-out of sale or sharing"
33795
+ },
33796
+ right_to_non_discrimination: {
33797
+ text: "Right to non-discrimination"
33798
+ },
33799
+ exercise: {
33800
+ text: "California residents can exercise CCPA/CPRA rights by contacting us at contact@openmates.org. We do not sell personal information."
33801
+ }
33802
+ }
33803
+ },
33804
+ discord_integration: {
33805
+ heading: {
33806
+ text: "9. Discord Integration"
33807
+ },
33808
+ description: {
33809
+ text: "If you choose to join our Discord community through links provided on this website, please note that Discord will collect and process your data according to their privacy policy. This includes account information, usage data, communication content, and other information as outlined in Discord's privacy policy."
33810
+ },
33811
+ admin_access: {
33812
+ text: "As Discord server administrators, we have access to all public communications, member lists, and other information shared within our Discord community. However, this information is hosted and primarily processed by Discord. We do not extract, store, or process this data outside of Discord's platform. For more information about how Discord handles your data, please review Discord's Privacy Policy."
33813
+ },
33814
+ privacy_policy_link: {
33815
+ text: "Discord Privacy Policy"
33816
+ }
33817
+ },
33818
+ contact: {
33819
+ heading: {
33820
+ text: "10. Contact Information"
33821
+ },
33822
+ questions: {
33823
+ text: "For questions about data protection:"
33824
+ },
33825
+ email: {
33826
+ text: "Email"
33827
+ },
33828
+ postal: {
33829
+ text: "Postal address: See Legal Notice (Imprint) for the current registered address."
33830
+ },
33831
+ controller: {
33832
+ text: "Controller: OpenMates (see Imprint for legal representative details)."
33833
+ }
33834
+ },
33835
+ aggregate_analytics: {
33836
+ heading: {
33837
+ text: "11. Anonymous Aggregate Analytics"
33838
+ },
33839
+ overview: {
33840
+ text: "We collect anonymous, aggregate statistics about how our website and web application are used. This data is genuinely anonymous \u2014 it cannot be linked to any individual user, and no cookies or tracking identifiers are set. No consent banner is required because no personal data is collected."
33841
+ },
33842
+ what_we_collect: {
33843
+ text: "What we collect (all aggregate, never individual):"
33844
+ },
33845
+ items: {
33846
+ text: "Daily page load counts; approximate unique visit counts (probabilistic HyperLogLog, ~0.81% error); country distribution (GeoIP lookup \u2014 IP discarded immediately, never stored); device class (mobile/tablet/desktop); browser family and major version; OS family; referrer domain (domain only, never full URL); screen size class; session duration distribution (bucketed)"
33847
+ },
33848
+ no_pii: {
33849
+ text: "IP addresses are used transiently for GeoIP lookup only and are never written to disk or any database. User-Agent strings are parsed to metadata only (browser name, OS) \u2014 the raw string is never stored. All data is stored as daily aggregate counters, not as individual records."
33850
+ }
33851
+ },
33852
+ follow_up_1: {
33853
+ text: "How is my chat content encrypted?"
33854
+ },
33855
+ follow_up_2: {
33856
+ text: "How does client-side encryption work?"
33857
+ },
33858
+ follow_up_3: {
33859
+ text: "What if I lose my encryption key?"
33860
+ },
33861
+ follow_up_4: {
33862
+ text: "Do AI providers see my messages?"
33863
+ },
33864
+ follow_up_5: {
33865
+ text: "How do I delete my account?"
33866
+ },
33867
+ follow_up_6: {
33868
+ text: "What data do you share with third parties?"
33869
+ },
33870
+ provider_link_label: {
33871
+ text: "Privacy policy"
33872
+ },
33873
+ overview: {
33874
+ heading: {
33875
+ text: "Overview"
33876
+ },
33877
+ summary: {
33878
+ text: "OpenMates is designed so that most of your data is encrypted on your device before it reaches our servers. This is **not** end-to-end encryption: our servers briefly decrypt your content in memory to run AI responses, render invoices, and deliver reminders. But we never write decrypted content to disk, logs, or traces, and when you delete your account we destroy the encryption key that protects your data \u2014 cryptographically shredding every encrypted field we still hold."
33879
+ }
33880
+ },
33881
+ protection: {
33882
+ heading: {
33883
+ text: "How we protect your data"
33884
+ },
33885
+ intro: {
33886
+ text: "We rely on six technical measures to protect your data. Each is verifiable in our open-source code and maps to a specific GDPR obligation."
33887
+ },
33888
+ client_side_encryption: {
33889
+ heading: {
33890
+ text: "Client-side encryption of your content"
32832
33891
  },
32833
- bfl: {
32834
- heading: {
32835
- text: "Black Forest Labs (FLUX image models)"
32836
- },
32837
- description: {
32838
- text: "Black Forest Labs powers draft image generation via FLUX models. BFL receives your image generation prompts and, when editing, the source images you provide."
32839
- }
33892
+ description: {
33893
+ text: "Your chat content, titles, summaries, tags, per-chat keys, app settings and memories, reminders, and sensitive profile fields (encrypted username, auto-top-up email, 2FA secrets) are encrypted in your browser before being sent to our servers. The browser uses a key derived from your login credential; our servers reject writes that contain anything but ciphertext. This is *not* end-to-end encryption \u2014 our servers can decrypt your content in memory when they need to (for AI responses, invoice rendering, reminder delivery). The difference from end-to-end encryption is that decryption happens transiently in process memory and the plaintext is never persisted to disk, logs, or traces."
32840
33894
  }
32841
33895
  },
32842
- music_generation: {
33896
+ pii_placeholder_substitution: {
32843
33897
  heading: {
32844
- text: "Music generation (only when you use a music skill)"
33898
+ text: "PII placeholder substitution before AI calls"
32845
33899
  },
32846
33900
  description: {
32847
- text: "These providers are only used when you invoke a music generation skill. If you never use the music app, none of your data is shared with them."
32848
- },
32849
- google_vertex_ai: {
32850
- heading: {
32851
- text: "Google Vertex AI (Lyria music generation)"
32852
- },
32853
- description: {
32854
- text: "Google Vertex AI hosts the Lyria models used by the music.generate skill. Google receives your music generation prompts, optional lyrics, style preferences, and generation settings only when you ask OpenMates to generate music or background music."
32855
- }
33901
+ text: "Before any message is sent to a third-party AI model, your browser scans your message for real names, email addresses, and physical addresses that you have saved in your privacy settings (or that match common patterns) and replaces any matches with neutral placeholders (for example [USER_0]). The mapping back to your real values is stored encrypted with your key and is never decrypted on our servers. **Important limitation:** this substitution only protects personal data that our client-side detector actually recognizes \u2014 typically the values you have explicitly saved in your privacy settings and values highlighted in the message field before you send. Anything the detector does not recognize (typos of your own name, uncommon address formats, other people's personal data, sensitive topics that are not formally PII) will be transmitted as you typed it. Please treat every message as you would treat a note passed to a third-party AI provider and be cautious with personal data."
32856
33902
  }
32857
33903
  },
32858
- code_execution: {
33904
+ encrypted_at_rest: {
32859
33905
  heading: {
32860
- text: "Code and developer tools (only when you use code skills)"
33906
+ text: "Encrypted at rest, decrypted only in memory"
32861
33907
  },
32862
33908
  description: {
32863
- text: "These providers are only used when you invoke a code app skill, such as searching public repositories, running code in an isolated sandbox, creating a generated application that auto-starts a short screenshot preview, or explicitly starting a generated application live preview. If you never use the code app, none of your data is shared with them."
32864
- },
32865
- github: {
32866
- heading: {
32867
- text: "GitHub (public repository search)"
32868
- },
32869
- description: {
32870
- text: "GitHub powers the code app's repository search skill. GitHub receives public repository search queries without an OpenMates user identifier."
32871
- }
32872
- },
32873
- context7: {
32874
- heading: {
32875
- text: "Context7 (programming documentation lookup)"
32876
- },
32877
- description: {
32878
- text: "Context7 powers the code app's documentation lookup skill. Context7 receives library names and documentation questions without an OpenMates user identifier."
32879
- }
32880
- },
32881
- e2b: {
32882
- heading: {
32883
- text: "E2B (isolated code sandbox)"
32884
- },
32885
- description: {
32886
- text: "E2B runs selected code embeds, generated application previews, and Remotion video renders inside isolated sandboxes. E2B receives the code or generated application/Remotion files and assets needed to run or render the result, runtime inputs, preview/render logs, and execution output needed to display the sandbox result. This sharing happens when you explicitly run code, create a generated application that auto-starts a short screenshot preview, start/resume an application live preview, or when a Remotion video render auto-starts after the assistant closes an explicit remotion code fence."
32887
- }
33909
+ text: "Our PostgreSQL database, Redis caches, S3 object storage, and backup snapshots hold only ciphertext for any field containing user content. When our servers need to read your content to run an AI response, render an invoice, or deliver a reminder, decryption happens transiently in process memory and is discarded immediately after. Key material is released on demand by HashiCorp Vault and never stored in raw form outside it."
32888
33910
  }
32889
33911
  },
32890
- web_and_search: {
33912
+ hashed_identifiers: {
32891
33913
  heading: {
32892
- text: "Web, search, and content retrieval (only when you use a web or search skill)"
33914
+ text: "Hashed identifiers throughout"
32893
33915
  },
32894
33916
  description: {
32895
- text: "These providers are only used when you invoke a web, videos, news, or web-read skill. They see the content of what you search for or the URLs you ask to read, but no user identifier \u2014 they do not know which OpenMates user made each request."
32896
- },
32897
- brave: {
32898
- heading: {
32899
- text: "Brave Search (web, videos, news)"
32900
- },
32901
- description: {
32902
- text: "Brave Search powers the web, videos, and news search skills. Brave sees the text of your search queries and your country and language preferences for localized results, but no user identifier."
32903
- }
32904
- },
32905
- firecrawl: {
32906
- heading: {
32907
- text: "Firecrawl (web page scraping)"
32908
- },
32909
- description: {
32910
- text: "Firecrawl powers the web read skill, scraping and extracting content from web pages you ask about. Firecrawl sees the URL you requested and the full content of the scraped page."
32911
- }
32912
- },
32913
- webshare: {
32914
- heading: {
32915
- text: "Webshare (rotating proxy)"
32916
- },
32917
- description: {
32918
- text: "Webshare provides a rotating proxy for requests to services that would otherwise block us. We route YouTube transcript requests (for the videos get-transcript skill) and doctor appointment searches (for the health search-appointments skill \u2014 see Group H) through Webshare. Webshare sees the URLs we route through it."
32919
- }
32920
- },
32921
- google_maps: {
32922
- heading: {
32923
- text: "Google Maps Platform (places search)"
32924
- },
32925
- description: {
32926
- text: "Google Maps powers the places search skill. Google sees the text of your place queries and an approximate location for localized results, but no user identifier."
32927
- }
32928
- },
32929
- youtube: {
32930
- heading: {
32931
- text: "YouTube (video transcripts)"
32932
- },
32933
- description: {
32934
- text: "YouTube is queried when you ask OpenMates to retrieve a transcript for a YouTube video. YouTube receives video URLs and requested transcript language preferences without an OpenMates user identifier."
32935
- }
33917
+ text: "Your user ID is stored in most database tables as a SHA-256 hash rather than as a direct reference. Login uses a zero-knowledge password verification flow: we verify that you know your password without ever learning it. API-key device records anonymize the IP address to its first two octets before encryption, so we can recognize a device without storing a precise location."
32936
33918
  }
32937
33919
  },
32938
- travel: {
33920
+ cryptographic_erasure: {
32939
33921
  heading: {
32940
- text: "Travel (only when you use a travel skill)"
33922
+ text: "Cryptographically shredded on deletion"
32941
33923
  },
32942
33924
  description: {
32943
- text: "These providers are only used when you invoke a travel skill \u2014 flight search, hotel search, or flight tracking. If you never use the travel app, none of your data is shared with them."
32944
- },
32945
- serpapi: {
32946
- heading: {
32947
- text: "SerpAPI (flight and hotel search)"
32948
- },
32949
- description: {
32950
- text: "SerpAPI powers flight and hotel search via its Google Flights and Google Hotels endpoints. SerpAPI receives your search queries \u2014 origin, destination, dates, and preferences \u2014 without a user identifier."
32951
- }
32952
- },
32953
- flightradar24: {
32954
- heading: {
32955
- text: "Flightradar24 (flight tracking)"
32956
- },
32957
- description: {
32958
- text: "Flightradar24 provides real flight track data when you ask about a specific flight number. It receives IATA flight numbers and departure dates without a user identifier."
32959
- }
32960
- },
32961
- deutsche_bahn: {
32962
- heading: {
32963
- text: "Deutsche Bahn (train connections)"
32964
- },
32965
- description: {
32966
- text: "Deutsche Bahn provides train connection results when you search for rail routes. Deutsche Bahn receives origin, destination, and date search details without an OpenMates user identifier."
32967
- }
32968
- },
32969
- flix: {
32970
- heading: {
32971
- text: "FlixBus / FlixTrain (bus and train connections)"
32972
- },
32973
- description: {
32974
- text: "Flix provides intercity bus and rail connection results when you search for FlixBus or FlixTrain routes. Flix receives origin, destination, and date search details without an OpenMates user identifier."
32975
- }
33925
+ text: "When you delete your account we destroy your HashiCorp Vault Transit key in addition to removing your rows from our database. Any encrypted field still sitting in a backup, cache, or running process becomes mathematically unrecoverable ciphertext at that moment \u2014 cryptographic erasure on top of row-level deletion."
32976
33926
  }
32977
33927
  },
32978
- events: {
33928
+ observability_without_tracking: {
32979
33929
  heading: {
32980
- text: "Events (only when you use the events skill)"
33930
+ text: "Observability without tracking"
32981
33931
  },
32982
33932
  description: {
32983
- text: "These providers are only used when you invoke the events search skill. The specific set of providers called depends on the city and event type you search for."
32984
- },
32985
- meetup: {
32986
- heading: {
32987
- text: "Meetup"
32988
- },
32989
- description: {
32990
- text: "Meetup powers event search for community events. Meetup receives your search queries \u2014 city, category, dates \u2014 without a user identifier."
32991
- }
32992
- },
32993
- luma: {
32994
- heading: {
32995
- text: "Luma"
32996
- },
32997
- description: {
32998
- text: "Luma powers event search for tech, creative, and community events. Luma receives your search queries without a user identifier."
32999
- }
33000
- },
33001
- resident_advisor: {
33002
- heading: {
33003
- text: "Resident Advisor"
33004
- },
33005
- description: {
33006
- text: "Resident Advisor powers event search for electronic music events. RA receives your search queries without a user identifier."
33007
- }
33933
+ text: "Our internal tracing pipeline (OpenTelemetry) replaces your user ID with a hash that rotates every 24 hours, strips authentication headers and cookies from every span, and redacts chat content before any trace leaves the backend. Our web analytics use a server-side beacon with no cookies and no stored individual identifiers. We do not use Google Analytics, Plausible, PostHog, or any third-party analytics platform."
33008
33934
  }
33935
+ }
33936
+ },
33937
+ promises: {
33938
+ intro: {
33939
+ text: "Below are the privacy promises we make to every user. Each one is backed by code in our open-source repository and verified by automated tests. When the enforcing code changes, a registry-linked check reminds us to rerun those tests before release."
33009
33940
  },
33010
- health: {
33941
+ client_side_chat_encryption: {
33011
33942
  heading: {
33012
- text: "Health appointment search (only when you use the health skill)"
33943
+ text: "Client-side chat encryption"
33013
33944
  },
33014
33945
  description: {
33015
- text: `These upstream services are reached through the Webshare rotating proxy (see Group E) only when you invoke the health app's appointment search skill. The queries you send \u2014 for example "dermatologist in Berlin next Tuesday" \u2014 are transmitted without user identifiers, but the content of the query itself can imply a health concern. By invoking the skill you consent to this processing under GDPR Art. 9(2)(a) (explicit consent for special-category data).`
33016
- },
33017
- doctolib: {
33018
- heading: {
33019
- text: "Doctolib (EU)"
33020
- },
33021
- description: {
33022
- text: "Doctolib lists doctors and specialists across supported EU regions. We query Doctolib for available appointments matching your criteria, routed through the Webshare proxy, without a user identifier."
33023
- }
33024
- },
33025
- jameda: {
33026
- heading: {
33027
- text: "Jameda (Germany)"
33028
- },
33029
- description: {
33030
- text: "Jameda lists doctors and specialists across Germany. We query Jameda for available appointments matching your criteria, routed through the Webshare proxy, without a user identifier."
33031
- }
33946
+ text: "Your chats, settings, and memories are encrypted on your device with AES-256-GCM before they leave your browser. The server stores only ciphertext on disk, in caches, and in backups. This is not end-to-end encryption: when a server-side task needs to read content (to run an AI response, render an invoice, or deliver a reminder), it decrypts in memory via HashiCorp Vault and discards the plaintext immediately after use."
33032
33947
  }
33033
33948
  },
33034
- shopping: {
33949
+ email_encryption_at_rest: {
33035
33950
  heading: {
33036
- text: "Shopping (only when you use the shopping skill)"
33951
+ text: "Email addresses encrypted at rest"
33037
33952
  },
33038
33953
  description: {
33039
- text: "These providers are only used when you invoke the shopping search skill for live product and price lookups."
33040
- },
33041
- rewe: {
33042
- heading: {
33043
- text: "REWE (German grocery)"
33044
- },
33045
- description: {
33046
- text: "REWE's product catalogue is queried for live product and price data when you search German grocery items. REWE receives your search queries without a user identifier."
33047
- }
33048
- },
33049
- amazon: {
33050
- heading: {
33051
- text: "Amazon"
33052
- },
33053
- description: {
33054
- text: "Amazon's product catalogue is queried for live product and price data. Amazon receives your search queries without a user identifier."
33055
- }
33954
+ text: "Your email address is stored only as ciphertext. The server decrypts it transiently in memory during login, billing, and notification delivery, and never writes plaintext to disk or logs."
33056
33955
  }
33057
33956
  },
33058
- nutrition: {
33957
+ no_third_party_tracking: {
33059
33958
  heading: {
33060
- text: "Nutrition (only when you use the recipe search skill)"
33959
+ text: "No third-party tracking cookies or analytics"
33061
33960
  },
33062
33961
  description: {
33063
- text: "These providers are only used when you invoke the nutrition recipe search skill. They receive recipe search queries and selected dietary filters without a user identifier."
33064
- },
33065
- edamam: {
33066
- heading: {
33067
- text: "Edamam"
33068
- },
33069
- description: {
33070
- text: "Edamam's Recipe Search API is queried when you search for recipes. Edamam receives the recipe query and selected dietary or nutrition filters without a user identifier."
33071
- }
33962
+ text: "We do not use Google Analytics, Plausible, PostHog, Mixpanel, Amplitude, Segment, or any other third-party analytics platform. Our telemetry is server-side, cookie-free, and contains no individually stored identifiers. A pre-edit check in our repository blocks the introduction of analytics SDKs."
33072
33963
  }
33073
33964
  },
33074
- electronics: {
33965
+ pii_placeholder_substitution: {
33075
33966
  heading: {
33076
- text: "Electronics (only when you use the electronics skill)"
33967
+ text: "Personal information never reaches AI providers"
33077
33968
  },
33078
33969
  description: {
33079
- text: "These providers are only used when you invoke the electronics component search skill for live component and reference-design lookups."
33080
- },
33081
- ti_webench: {
33082
- heading: {
33083
- text: "Texas Instruments WEBENCH (US)"
33084
- },
33085
- description: {
33086
- text: "TI WEBENCH is queried for power converter component candidates and reference design summaries. Texas Instruments receives the electrical requirements you search for, such as input voltage, output voltage, output current, temperature, isolation, and optimization goal, without a user identifier."
33087
- }
33970
+ text: "Before any message is sent to an AI provider, your browser detects 32 categories of personal data \u2014 emails, phone numbers, credit card numbers, API keys, and more \u2014 and replaces them with placeholders like `[EMAIL_com]`. The substitution map is encrypted with your chat key so only your devices can restore the original values."
33088
33971
  }
33089
33972
  },
33090
- mail: {
33973
+ telemetry_privacy_filter: {
33091
33974
  heading: {
33092
- text: "Mail (only when you use mail skills)"
33975
+ text: "Telemetry stripped of sensitive data"
33093
33976
  },
33094
33977
  description: {
33095
- text: "This provider is only used when you invoke mail skills against a connected mailbox. If you never use the mail app, none of your data is shared with it."
33096
- },
33097
- protonmail: {
33098
- heading: {
33099
- text: "Proton Mail (mail search)"
33100
- },
33101
- description: {
33102
- text: "Proton Mail is queried when you search a connected Proton Mail mailbox. Proton receives the mail search query and mailbox data needed to return matching results."
33103
- }
33978
+ text: "Our internal tracing pipeline (OpenTelemetry) strips authentication headers, cookies, and database statements from every span, and pseudonymises your user ID with a salt that rotates every 24 hours. Regular users never have their raw identifiers in telemetry."
33104
33979
  }
33105
33980
  },
33106
- home: {
33981
+ cryptographic_erasure: {
33107
33982
  heading: {
33108
- text: "Home and housing (only when you use housing search)"
33983
+ text: "Deleted accounts are cryptographically erased"
33109
33984
  },
33110
33985
  description: {
33111
- text: "These providers are only used when you invoke the home app's housing search skill. If you never search for housing, none of your data is shared with them."
33112
- },
33113
- immoscout24: {
33114
- heading: {
33115
- text: "ImmoScout24 (German housing search)"
33116
- },
33117
- description: {
33118
- text: "ImmoScout24 is queried when you search German housing listings. ImmoScout24 receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
33119
- }
33986
+ text: "When you delete your account, the first step destroys your encryption keys. Any residual ciphertext in backups, caches, or audit logs is rendered permanently unreadable \u2014 no key, no content."
33987
+ }
33988
+ },
33989
+ argon2_password_hashing: {
33990
+ heading: {
33991
+ text: "Passwords stored as Argon2 hashes"
33120
33992
  },
33121
- kleinanzeigen: {
33122
- heading: {
33123
- text: "Kleinanzeigen (German classified housing search)"
33124
- },
33125
- description: {
33126
- text: "Kleinanzeigen is queried when you search German classified housing listings. Kleinanzeigen receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
33127
- }
33993
+ description: {
33994
+ text: "We store password and backup-code verifiers as Argon2 hashes. Plaintext passwords are never persisted to disk, never written to logs, and never recoverable \u2014 even by us."
33995
+ }
33996
+ },
33997
+ payment_data_minimization: {
33998
+ heading: {
33999
+ text: "No card numbers ever touch our servers"
33128
34000
  },
33129
- wg_gesucht: {
33130
- heading: {
33131
- text: "WG-Gesucht (German shared-apartment search)"
33132
- },
33133
- description: {
33134
- text: "WG-Gesucht is queried when you search German shared-apartment or rental listings. WG-Gesucht receives housing search queries, locations, filters, and listing pages requested without an OpenMates user identifier."
33135
- }
34001
+ description: {
34002
+ text: "Payments flow directly from your browser to Stripe. We store only provider tokens and customer identifiers. We never see, receive, or persist full card numbers, security codes, or bank account details."
33136
34003
  }
33137
34004
  },
33138
- community: {
34005
+ logging_redaction: {
33139
34006
  heading: {
33140
- text: "Community and developer channels (only if you choose to use them)"
34007
+ text: "Logs are scrubbed of sensitive data"
33141
34008
  },
33142
34009
  description: {
33143
- text: "These providers are only involved if you choose to join our community or developer channels. You can use OpenMates without ever touching them."
34010
+ text: "Every log line passes through a redaction filter that strips email addresses, IP addresses, bearer tokens, and password values. Compliance logs preserve pseudonymous user IDs but remove all other personal data."
34011
+ }
34012
+ },
34013
+ prompt_injection_defense: {
34014
+ heading: {
34015
+ text: "Defense against prompt injection"
33144
34016
  },
33145
- discord: {
33146
- heading: {
33147
- text: "Discord (community server)"
33148
- },
33149
- description: {
33150
- text: "If you join the OpenMates community Discord server, Discord will see anything you post on its platform, plus your Discord username and whatever data Discord itself collects per its own privacy policy. We have no control over what Discord does with that data."
33151
- },
33152
- admin_access: {
33153
- text: "OpenMates administrators with Discord server admin rights can read all messages posted in the OpenMates Discord server. Do not share sensitive information on Discord."
33154
- }
34017
+ description: {
34018
+ text: "Two layers protect your conversations from malicious content in web pages, files, and URLs you reference. Invisible Unicode characters are stripped first, then a dedicated safety model detects semantic injection attempts and blocks or replaces high-risk content before it reaches your assistant."
33155
34019
  }
33156
34020
  },
33157
- social_media: {
34021
+ no_training_on_user_data: {
33158
34022
  heading: {
33159
- text: "Social media (only when you use social media skills)"
34023
+ text: "Your conversations are not used to train AI models"
33160
34024
  },
33161
34025
  description: {
33162
- text: "These providers are only used when you invoke the social media app's get-posts or search skills. They receive public profile, page, or search queries without an OpenMates user identifier."
34026
+ text: "All AI chat providers we use (Anthropic, OpenAI, Mistral, Google Gemini, Google Vertex AI, Together AI, Groq, Cerebras, and OpenRouter) have explicit no-training clauses in their terms \u2014 your conversations are never used to fine-tune, evaluate, or otherwise train AI models. For image generation, Recraft has a training opt-out which we have activated. fal.ai (used for draft image generation) may use anonymized, aggregated derivatives of prompts for model improvement under their standard API terms; no full opt-out is available outside an enterprise contract. A per-provider audit with verbatim policy quotes and verification dates is published in our open-source repository."
34027
+ }
34028
+ },
34029
+ no_external_resources: {
34030
+ heading: {
34031
+ text: "The web app never loads external images or scripts"
33163
34032
  },
33164
- reddit: {
33165
- heading: {
33166
- text: "Reddit (public posts and discussions)"
33167
- },
33168
- description: {
33169
- text: "Reddit is queried when you fetch or search public Reddit posts. Reddit receives subreddit names, public post URLs, and search queries without an OpenMates user identifier."
33170
- }
34033
+ description: {
34034
+ text: "Every external image, favicon, or preview shown in the app is fetched through our own preview.openmates.org proxy \u2014 the originating website never sees your IP address. The only third-party script the web app loads is the Stripe payment SDK needed to process a purchase, and only when you explicitly open the payment flow. There is no ad network, tag manager, CDN font loader, or third-party analytics library."
34035
+ }
34036
+ },
34037
+ cli_no_credential_prompts: {
34038
+ heading: {
34039
+ text: "The command-line tool never asks for your password"
33171
34040
  },
33172
- bluesky: {
33173
- heading: {
33174
- text: "Bluesky (public posts and profile feeds)"
33175
- },
33176
- description: {
33177
- text: "Bluesky is queried when you fetch or search public Bluesky posts. Bluesky receives handles and search queries without an OpenMates user identifier."
33178
- }
34041
+ description: {
34042
+ text: "The OpenMates CLI never prompts you for your email, password, 2FA code, or any other login credential. Authentication uses a browser-based pair-auth handshake: the CLI shows a QR code and URL, you approve the session in your logged-in web browser, and you type a short 6-character binding PIN back into the CLI to confirm. Your credentials never pass through the terminal."
34043
+ }
34044
+ },
34045
+ open_source_transparency: {
34046
+ heading: {
34047
+ text: "Every claim on this page is independently auditable"
33179
34048
  },
33180
- mastodon: {
33181
- heading: {
33182
- text: "Mastodon (public profile posts)"
33183
- },
33184
- description: {
33185
- text: "Mastodon is queried when you fetch public Mastodon profile posts. The relevant Mastodon server receives profile identifiers or public profile URLs without an OpenMates user identifier."
33186
- }
34049
+ description: {
34050
+ text: "OpenMates is open source. Every promise above is backed by code you can read, tests you can run, and architecture documents you can audit in our public repository. Our privacy-promises registry links each claim to the exact files that enforce it."
33187
34051
  }
33188
34052
  }
33189
34053
  },
@@ -35439,6 +36303,33 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
35439
36303
  connect_button: {
35440
36304
  text: "Connect Google Calendar"
35441
36305
  },
36306
+ manage_account: {
36307
+ text: "Manage {label}"
36308
+ },
36309
+ capability_read: {
36310
+ text: "Read events"
36311
+ },
36312
+ capability_read_description: {
36313
+ text: "Let OpenMates request permission before reading calendar events."
36314
+ },
36315
+ capability_write: {
36316
+ text: "Write events"
36317
+ },
36318
+ capability_write_description: {
36319
+ text: "Let OpenMates request permission before creating or updating events."
36320
+ },
36321
+ capability_delete: {
36322
+ text: "Delete events"
36323
+ },
36324
+ capability_delete_description: {
36325
+ text: "Let OpenMates request permission before deleting events."
36326
+ },
36327
+ oauth_summary: {
36328
+ text: "OAuth request: {capabilities}"
36329
+ },
36330
+ no_capabilities_selected: {
36331
+ text: "No access selected"
36332
+ },
35442
36333
  finalizing: {
35443
36334
  text: "Encrypting and saving your connected account..."
35444
36335
  },
@@ -36966,7 +37857,10 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
36966
37857
  }
36967
37858
  },
36968
37859
  apps: {
36969
- text: "Apps"
37860
+ text: "Apps",
37861
+ all: {
37862
+ text: "Apps"
37863
+ }
36970
37864
  },
36971
37865
  mates: {
36972
37866
  description: {
@@ -37444,6 +38338,71 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
37444
38338
  text: "OpenMates will auto replace sensitive data with placeholders, before sending them to the OpenMates server and any external provider, to protect your privacy. Replaced data are visually highlighted. You can always click on a replaced data entry to overwrite the replacement, or turn off the functionality completely here."
37445
38339
  }
37446
38340
  },
38341
+ connected_accounts: {
38342
+ title: {
38343
+ text: "Connected accounts"
38344
+ },
38345
+ subtitle: {
38346
+ text: "Apps | Providers"
38347
+ },
38348
+ description: {
38349
+ text: "Connected accounts are stored as encrypted browser-managed records. OpenMates only receives encrypted account data and opaque hashes."
38350
+ },
38351
+ loading: {
38352
+ text: "Loading connected accounts..."
38353
+ },
38354
+ empty: {
38355
+ text: "No connected accounts yet."
38356
+ },
38357
+ accounts: {
38358
+ text: "Accounts"
38359
+ },
38360
+ details: {
38361
+ text: "Account details"
38362
+ },
38363
+ account_label: {
38364
+ text: "Account label"
38365
+ },
38366
+ provider: {
38367
+ text: "Provider"
38368
+ },
38369
+ provider_google_calendar: {
38370
+ text: "Google Calendar"
38371
+ },
38372
+ app: {
38373
+ text: "App"
38374
+ },
38375
+ capabilities: {
38376
+ text: "Capabilities"
38377
+ },
38378
+ runtime_modes: {
38379
+ text: "Runtime modes"
38380
+ },
38381
+ none: {
38382
+ text: "None"
38383
+ },
38384
+ add_calendar_write: {
38385
+ text: "Add Calendar write access"
38386
+ },
38387
+ finalizing: {
38388
+ text: "Encrypting and saving connected account update..."
38389
+ },
38390
+ updated_success: {
38391
+ text: "Connected account updated. New access was encrypted before saving."
38392
+ },
38393
+ sign_in_required: {
38394
+ text: "Sign in to manage connected accounts."
38395
+ },
38396
+ load_error: {
38397
+ text: "Could not load connected accounts."
38398
+ },
38399
+ update_error: {
38400
+ text: "Could not start connected account update."
38401
+ },
38402
+ finalize_error: {
38403
+ text: "Could not finish updating connected account."
38404
+ }
38405
+ },
37447
38406
  nearby_by_default: {
37448
38407
  text: "Nearby by default"
37449
38408
  },
@@ -37787,7 +38746,10 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
37787
38746
  text: "Community Suggestions"
37788
38747
  },
37789
38748
  stats: {
37790
- text: "Stats"
38749
+ text: "Stats",
38750
+ subtitle: {
38751
+ text: "View global server usage and growth metrics"
38752
+ }
37791
38753
  },
37792
38754
  gift_cards: {
37793
38755
  text: "Gift Cards",
@@ -37938,6 +38900,89 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
37938
38900
  free_testing_credits: {
37939
38901
  text: "Free testing credits"
37940
38902
  },
38903
+ anonymous_free_usage_budget: {
38904
+ title: {
38905
+ text: "Anonymous free usage"
38906
+ },
38907
+ subtitle: {
38908
+ text: "Configure logged-out free chat budget caps"
38909
+ },
38910
+ loading: {
38911
+ text: "Loading anonymous free usage budget..."
38912
+ },
38913
+ load_error: {
38914
+ text: "Failed to load anonymous free usage budget."
38915
+ },
38916
+ save_success: {
38917
+ text: "Anonymous free usage budget saved."
38918
+ },
38919
+ save_error: {
38920
+ text: "Failed to save anonymous free usage budget."
38921
+ },
38922
+ status_active: {
38923
+ text: "Anonymous free usage is active for new logged-out users."
38924
+ },
38925
+ status_daily_exhausted: {
38926
+ text: "The anonymous free usage daily cap is exhausted until the next reset."
38927
+ },
38928
+ status_weekly_exhausted: {
38929
+ text: "The anonymous free usage weekly cap is exhausted."
38930
+ },
38931
+ status_disabled: {
38932
+ text: "Anonymous free usage is currently inactive."
38933
+ },
38934
+ daily_cap: {
38935
+ text: "Daily cap"
38936
+ },
38937
+ weekly_cap: {
38938
+ text: "Weekly cap"
38939
+ },
38940
+ daily_used: {
38941
+ text: "Daily used"
38942
+ },
38943
+ weekly_used: {
38944
+ text: "Weekly used"
38945
+ },
38946
+ daily_remaining: {
38947
+ text: "Daily remaining"
38948
+ },
38949
+ weekly_remaining: {
38950
+ text: "Weekly remaining"
38951
+ },
38952
+ reset_at: {
38953
+ text: "Daily reset"
38954
+ },
38955
+ updated_at: {
38956
+ text: "Updated"
38957
+ },
38958
+ never: {
38959
+ text: "Never"
38960
+ },
38961
+ enabled: {
38962
+ text: "Enable anonymous free usage for logged-out users"
38963
+ },
38964
+ monthly_budget: {
38965
+ text: "Monthly budget credits"
38966
+ },
38967
+ daily_percent: {
38968
+ text: "Daily hard cap percent"
38969
+ },
38970
+ weekly_percent: {
38971
+ text: "Weekly cap percent"
38972
+ },
38973
+ per_identity_cap: {
38974
+ text: "Per-identity daily cap credits"
38975
+ },
38976
+ derived_daily_cap: {
38977
+ text: "Derived daily cap"
38978
+ },
38979
+ derived_weekly_cap: {
38980
+ text: "Derived weekly cap"
38981
+ },
38982
+ validation_percent: {
38983
+ text: "Percent values must be between 0 and 100."
38984
+ }
38985
+ },
37941
38986
  tests: {
37942
38987
  text: "Tests",
37943
38988
  subtitle: {
@@ -40433,6 +41478,28 @@ function toWaitingForUserResult(params) {
40433
41478
  };
40434
41479
  }
40435
41480
 
41481
+ // src/feedback.ts
41482
+ var ASSISTANT_FEEDBACK_THANKS = "Thanks for the feedback!";
41483
+ var ASSISTANT_FEEDBACK_REPORT_TITLE = "Assistant response quality bad:";
41484
+ function buildAssistantFeedbackDecision(rating) {
41485
+ if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
41486
+ throw new Error("Rating must be an integer from 1 to 5.");
41487
+ }
41488
+ if (rating <= 3) {
41489
+ return {
41490
+ rating,
41491
+ action: "report_issue",
41492
+ message: ASSISTANT_FEEDBACK_THANKS,
41493
+ reportTitle: ASSISTANT_FEEDBACK_REPORT_TITLE
41494
+ };
41495
+ }
41496
+ return {
41497
+ rating,
41498
+ action: "thanks",
41499
+ message: ASSISTANT_FEEDBACK_THANKS
41500
+ };
41501
+ }
41502
+
40436
41503
  // src/cli.ts
40437
41504
  async function main() {
40438
41505
  const parsed = parseArgs(process.argv.slice(2));
@@ -40441,11 +41508,13 @@ async function main() {
40441
41508
  apiUrl: typeof parsed.flags["api-url"] === "string" ? parsed.flags["api-url"] : void 0
40442
41509
  });
40443
41510
  const redactor = new OutputRedactor();
40444
- if (client.hasSession() && shouldInitializeRedactor(command, subcommand)) {
41511
+ const piiDetectionEnabled = parsed.flags["no-pii-detection"] !== true;
41512
+ if (piiDetectionEnabled && shouldInitializeRedactor(command, subcommand)) {
40445
41513
  try {
40446
- const memories = await client.listMemories();
41514
+ const memories = client.hasSession() ? await client.listMemories() : [];
40447
41515
  redactor.initializeFromMemories(memories);
40448
41516
  } catch {
41517
+ redactor.initializeFromMemories([]);
40449
41518
  }
40450
41519
  }
40451
41520
  if (!command || command === "help") {
@@ -40493,6 +41562,10 @@ async function main() {
40493
41562
  printServerHelp();
40494
41563
  return;
40495
41564
  }
41565
+ if (command === "feedback") {
41566
+ printFeedbackHelp();
41567
+ return;
41568
+ }
40496
41569
  if (command === "docs") {
40497
41570
  printDocsHelp();
40498
41571
  return;
@@ -40563,6 +41636,10 @@ async function main() {
40563
41636
  await handleNewChatSuggestions(client, parsed.flags);
40564
41637
  return;
40565
41638
  }
41639
+ if (command === "feedback") {
41640
+ handleFeedback(subcommand, rest, parsed.flags);
41641
+ return;
41642
+ }
40566
41643
  throw new Error(`Unknown command '${command}'. Run 'openmates help'.`);
40567
41644
  }
40568
41645
  function shouldInitializeRedactor(command, subcommand) {
@@ -40618,7 +41695,8 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
40618
41695
  incognito: false,
40619
41696
  json: flags.json === true,
40620
41697
  autoApproveSubChats: flags["auto-approve"] === true,
40621
- autoApproveMemories: flags["auto-approve-memories"] === true
41698
+ autoApproveMemories: flags["auto-approve-memories"] === true,
41699
+ piiDetection: flags["no-pii-detection"] !== true
40622
41700
  },
40623
41701
  redactor
40624
41702
  );
@@ -40691,7 +41769,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
40691
41769
  incognito: flags.incognito === true,
40692
41770
  json: flags.json === true,
40693
41771
  autoApproveSubChats: flags["auto-approve"] === true,
40694
- autoApproveMemories: flags["auto-approve-memories"] === true
41772
+ autoApproveMemories: flags["auto-approve-memories"] === true,
41773
+ piiDetection: flags["no-pii-detection"] !== true
40695
41774
  },
40696
41775
  redactor
40697
41776
  );
@@ -40711,7 +41790,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
40711
41790
  incognito: true,
40712
41791
  json: flags.json === true,
40713
41792
  autoApproveSubChats: flags["auto-approve"] === true,
40714
- autoApproveMemories: flags["auto-approve-memories"] === true
41793
+ autoApproveMemories: flags["auto-approve-memories"] === true,
41794
+ piiDetection: flags["no-pii-detection"] !== true
40715
41795
  },
40716
41796
  redactor
40717
41797
  );
@@ -43013,6 +44093,29 @@ Usage: openmates settings memories <list|types|create|update|delete>`
43013
44093
  );
43014
44094
  process.exit(1);
43015
44095
  }
44096
+ function handleFeedback(subcommand, _rest, flags) {
44097
+ if (!subcommand || subcommand === "help" || flags.help === true) {
44098
+ printFeedbackHelp();
44099
+ return;
44100
+ }
44101
+ if (subcommand !== "assistant-response") {
44102
+ throw new Error(`Unknown feedback command '${subcommand}'. Run 'openmates feedback --help'.`);
44103
+ }
44104
+ const rawRating = flags.rating;
44105
+ if (typeof rawRating !== "string") {
44106
+ throw new Error("Missing --rating <1-5>.");
44107
+ }
44108
+ const decision = buildAssistantFeedbackDecision(Number(rawRating));
44109
+ if (flags.json === true) {
44110
+ printJson2(decision);
44111
+ return;
44112
+ }
44113
+ console.log(decision.message);
44114
+ if (decision.action === "report_issue") {
44115
+ console.log(`Report issue title: ${decision.reportTitle}`);
44116
+ console.log("Open the report issue form and include the affected assistant response.");
44117
+ }
44118
+ }
43016
44119
  function parseArgs(argv) {
43017
44120
  const positionals = [];
43018
44121
  const flags = {};
@@ -43301,6 +44404,20 @@ async function sendMessageStreaming(client, params, redactor) {
43301
44404
  process.exit(1);
43302
44405
  }
43303
44406
  if (parsed.filePaths.length > 0) {
44407
+ if (!client.hasSession()) {
44408
+ clearTyping();
44409
+ const result2 = {
44410
+ status: "signup_required",
44411
+ reason: "file_upload_requires_signup",
44412
+ signup_required: true,
44413
+ message: "File uploads require signup. Your message text can be kept as a draft, but files must be attached after creating an account."
44414
+ };
44415
+ if (!params.json) {
44416
+ process.stderr.write(`${result2.message}
44417
+ `);
44418
+ }
44419
+ return result2;
44420
+ }
43304
44421
  const fileResult = processFiles(parsed.filePaths, redactor ?? null);
43305
44422
  for (const b of fileResult.blocked) {
43306
44423
  clearTyping();
@@ -43439,6 +44556,29 @@ async function sendMessageStreaming(client, params, redactor) {
43439
44556
  } catch {
43440
44557
  }
43441
44558
  }
44559
+ const piiResult = params.piiDetection !== false && redactor?.isInitialized ? redactor.redactWithMappings(finalMessage) : { redacted: finalMessage, mappings: [] };
44560
+ finalMessage = piiResult.redacted;
44561
+ if (!client.hasSession()) {
44562
+ let result2;
44563
+ try {
44564
+ result2 = await client.sendAnonymousMessage({ message: finalMessage });
44565
+ } finally {
44566
+ clearTyping();
44567
+ }
44568
+ if (!params.json) {
44569
+ const mateBlock = ansiMateBlock(result2.category, result2.mateName);
44570
+ const modelSuffix = result2.modelName ? ` \x1B[2m${result2.modelName}\x1B[0m` : "";
44571
+ process.stdout.write(`${SEP}
44572
+ `);
44573
+ process.stdout.write(`${mateBlock}${modelSuffix}
44574
+ `);
44575
+ process.stdout.write(`${SEP}
44576
+ `);
44577
+ process.stdout.write(`${result2.assistant}
44578
+ `);
44579
+ }
44580
+ return result2;
44581
+ }
43442
44582
  const urlResult = prepareUrlEmbeds(finalMessage);
43443
44583
  finalMessage = urlResult.message;
43444
44584
  preparedEmbeds.push(...urlResult.embeds);
@@ -43451,7 +44591,12 @@ async function sendMessageStreaming(client, params, redactor) {
43451
44591
  onSubChatApprovalRequest,
43452
44592
  autoApproveSubChats: params.autoApproveSubChats,
43453
44593
  autoApproveMemories: params.autoApproveMemories,
43454
- preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0
44594
+ preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0,
44595
+ piiMappings: piiResult.mappings.map((mapping) => ({
44596
+ placeholder: mapping.placeholder,
44597
+ original: mapping.original,
44598
+ type: mapping.type
44599
+ }))
43455
44600
  });
43456
44601
  clearTyping();
43457
44602
  if (result.status === "waiting_for_user") {
@@ -44754,6 +45899,7 @@ Commands:
44754
45899
  openmates settings [--help] Predefined settings commands
44755
45900
  openmates inspirations [--lang <code>] [--json] Daily inspirations
44756
45901
  openmates newchatsuggestions [--limit <n>] [--json] Personalized new chat suggestions
45902
+ openmates feedback [--help] Assistant response feedback helpers
44757
45903
  openmates server [--help] Server management (install, start, stop, ...)
44758
45904
  openmates docs [--help] Browse, search, and download documentation
44759
45905
  openmates e2e provision-auth-accounts Provision local E2E auth-account artifacts
@@ -44764,6 +45910,18 @@ Flags:
44764
45910
  --api-key <key> Optional API key override (or set OPENMATES_API_KEY)
44765
45911
  --help Show contextual help for any command`);
44766
45912
  }
45913
+ function printFeedbackHelp() {
45914
+ console.log(`Feedback commands:
45915
+ openmates feedback assistant-response --rating <1-5> [--json]
45916
+
45917
+ Mirrors the web chat assistant-response feedback decision:
45918
+ 4-5 stars Thank the user only
45919
+ 1-3 stars Thank the user and prompt a report issue with the standard prefill
45920
+
45921
+ Options:
45922
+ --rating <1-5> Required star rating
45923
+ --json Output the decision contract as JSON`);
45924
+ }
44767
45925
  function printSignupHelp() {
44768
45926
  console.log(`Signup command:
44769
45927
  openmates signup --email <email> --username <name> --invite-code <code>
@@ -44804,13 +45962,13 @@ function printChatsHelp() {
44804
45962
  openmates chats show <chat-id> [--raw] [--json]
44805
45963
  openmates chats open [<n|example-id|slug>] [--json]
44806
45964
  openmates chats search <query> [--json]
44807
- openmates chats new <message> [--json] [--auto-approve] [--auto-approve-memories]
44808
- openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories]
45965
+ openmates chats new <message> [--json] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
45966
+ openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
44809
45967
  openmates chats send --chat <id> --followup <n> [--json] [--auto-approve] [--auto-approve-memories]
44810
45968
  openmates chats download <chat-id> [--output <path>] [--zip] [--json]
44811
45969
  openmates chats delete <id1> [id2] [id3] ... [--yes]
44812
45970
  openmates chats share [<chat-id>] [--expires <seconds>] [--password <pwd>] [--json]
44813
- openmates chats incognito <message> [--json]
45971
+ openmates chats incognito <message> [--json] [--no-pii-detection]
44814
45972
  openmates chats incognito-history [--json] Deprecated: incognito stores no history
44815
45973
  openmates chats incognito-clear Deprecated: incognito stores no history
44816
45974
 
@@ -44839,8 +45997,10 @@ Options for 'new', 'send', and 'incognito':
44839
45997
  --auto-approve Automatically approve server-requested sub-chat batches.
44840
45998
  Without this, the CLI prompts in the terminal like the web app.
44841
45999
  --auto-approve-memories Explicitly approve server-requested memory categories.
44842
- Memories are never approved by default.
44843
- Use only for trusted non-interactive runs.
46000
+ Memories are never approved by default.
46001
+ Use only for trusted non-interactive runs.
46002
+ --no-pii-detection Send the message exactly as typed. By default, the CLI
46003
+ replaces detected PII with placeholders before send.
44844
46004
 
44845
46005
  Options for 'download':
44846
46006
  --output <path> Target directory (default: current directory)
@@ -45172,6 +46332,9 @@ export {
45172
46332
  deriveAppUrl,
45173
46333
  OpenMatesClient,
45174
46334
  defaultCloneBranchForVersion,
46335
+ ASSISTANT_FEEDBACK_THANKS,
46336
+ ASSISTANT_FEEDBACK_REPORT_TITLE,
46337
+ buildAssistantFeedbackDecision,
45175
46338
  serializeToYaml,
45176
46339
  getExtForLang
45177
46340
  };