openmates 0.12.0-alpha.2 → 0.12.0-alpha.4

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.
@@ -2037,6 +2037,33 @@ function buildSubChatConfirmationPayload(params) {
2037
2037
  approve_count: params.approved ? params.approveCount ?? null : null
2038
2038
  };
2039
2039
  }
2040
+ function assertNoConnectedAccountSecretLeak(value) {
2041
+ const serialized = JSON.stringify(value ?? {});
2042
+ const forbidden = [
2043
+ "refresh_token",
2044
+ "access_token",
2045
+ "provider_email",
2046
+ "account_email",
2047
+ "provider_account_id"
2048
+ ];
2049
+ for (const key of forbidden) {
2050
+ if (serialized.includes(`"${key}"`)) {
2051
+ throw new Error(`Connected account payload contains forbidden field: ${key}`);
2052
+ }
2053
+ }
2054
+ }
2055
+ function buildConnectedAccountDirectoryPayload(entries) {
2056
+ if (!entries || entries.length === 0) return void 0;
2057
+ assertNoConnectedAccountSecretLeak(entries);
2058
+ return entries.map((entry) => ({ ...entry, capabilities: [...entry.capabilities] }));
2059
+ }
2060
+ function buildTurnTokenRefsRequestPayload(params) {
2061
+ return {
2062
+ chat_id: params.chatId,
2063
+ message_id: params.messageId,
2064
+ refs: params.refs.map((ref) => ({ ...ref }))
2065
+ };
2066
+ }
2040
2067
  function categoryFromMemoryKey(key) {
2041
2068
  const separator = key.indexOf("-");
2042
2069
  if (separator <= 0 || separator === key.length - 1) return null;
@@ -2581,6 +2608,124 @@ var BLOCKED_SETTINGS_MUTATE_PATHS = /* @__PURE__ */ new Set([
2581
2608
  "/v1/settings/verify-action-code",
2582
2609
  "/v1/settings/user/disable-2fa"
2583
2610
  ]);
2611
+ function applyUnifiedDiffForEmbedVersion(content, patch) {
2612
+ const contentLines = content.split("\n");
2613
+ const lines = patch.split("\n");
2614
+ const hunks = [];
2615
+ let current = null;
2616
+ for (const line of lines) {
2617
+ const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
2618
+ if (match) {
2619
+ if (current) hunks.push(current);
2620
+ current = { start: Number(match[1]) - 1, oldLines: [], newLines: [] };
2621
+ continue;
2622
+ }
2623
+ if (!current) continue;
2624
+ if (line.startsWith(" ")) {
2625
+ current.oldLines.push(line.slice(1));
2626
+ current.newLines.push(line.slice(1));
2627
+ } else if (line.startsWith("-")) {
2628
+ current.oldLines.push(line.slice(1));
2629
+ } else if (line.startsWith("+")) {
2630
+ current.newLines.push(line.slice(1));
2631
+ }
2632
+ }
2633
+ if (current) hunks.push(current);
2634
+ for (const hunk of hunks.sort((a, b) => b.start - a.start)) {
2635
+ const actual = contentLines.slice(hunk.start, hunk.start + hunk.oldLines.length);
2636
+ if (actual.join("\n") !== hunk.oldLines.join("\n")) {
2637
+ throw new Error("Version patch context does not match local content");
2638
+ }
2639
+ contentLines.splice(hunk.start, hunk.oldLines.length, ...hunk.newLines);
2640
+ }
2641
+ return contentLines.join("\n");
2642
+ }
2643
+ function buildUnifiedDiffForEmbedRestore(currentContent, restoredContent, currentVersion, newVersion) {
2644
+ const currentLines = currentContent.split("\n");
2645
+ const restoredLines = restoredContent.split("\n");
2646
+ return [
2647
+ `--- v${currentVersion}`,
2648
+ `+++ v${newVersion}`,
2649
+ `@@ -1,${Math.max(1, currentLines.length)} +1,${Math.max(1, restoredLines.length)} @@`,
2650
+ ...currentLines.map((line) => `-${line}`),
2651
+ ...restoredLines.map((line) => `+${line}`)
2652
+ ].join("\n");
2653
+ }
2654
+ function parseEmbedContentObject(rawContent) {
2655
+ try {
2656
+ return JSON.parse(rawContent);
2657
+ } catch {
2658
+ return parseYamlLikeContent(rawContent);
2659
+ }
2660
+ }
2661
+ async function encodeEmbedContentObject(content) {
2662
+ try {
2663
+ const { encode } = await import("@toon-format/toon");
2664
+ return encode(content);
2665
+ } catch {
2666
+ return JSON.stringify(content);
2667
+ }
2668
+ }
2669
+ function extractVersionedEmbedContent(content) {
2670
+ if (typeof content.receiver === "string" || typeof content.subject === "string") {
2671
+ return [
2672
+ `To: ${typeof content.receiver === "string" ? content.receiver : ""}`,
2673
+ `Subject: ${typeof content.subject === "string" ? content.subject : ""}`,
2674
+ "",
2675
+ typeof content.content === "string" ? content.content : "",
2676
+ typeof content.footer === "string" ? content.footer : ""
2677
+ ].join("\n").trim();
2678
+ }
2679
+ if (typeof content.remotion_source === "string") return content.remotion_source;
2680
+ if (typeof content.code === "string") return content.code;
2681
+ if (typeof content.html === "string") return content.html;
2682
+ if (typeof content.table === "string") return content.table;
2683
+ if (content.docx_model) return JSON.stringify(content.docx_model, null, 2);
2684
+ if (typeof content.content === "string") return content.content;
2685
+ throw new Error("Unsupported embed content shape for version restore.");
2686
+ }
2687
+ function buildRestoredEmbedContentObject(current, restoredContent, newVersion) {
2688
+ const restored = { ...current, version_number: newVersion };
2689
+ if (typeof current.receiver === "string" || typeof current.subject === "string") {
2690
+ return { ...restored, ...parseMailVersionContent(restoredContent) };
2691
+ }
2692
+ if (typeof current.remotion_source === "string") {
2693
+ return { ...restored, remotion_source: restoredContent, current_source_version: newVersion };
2694
+ }
2695
+ if (typeof current.code === "string") return { ...restored, code: restoredContent };
2696
+ if (typeof current.html === "string") return { ...restored, html: restoredContent };
2697
+ if (typeof current.table === "string") return { ...restored, table: restoredContent };
2698
+ if (current.docx_model) {
2699
+ try {
2700
+ return { ...restored, docx_model: JSON.parse(restoredContent) };
2701
+ } catch {
2702
+ return { ...restored, content: restoredContent };
2703
+ }
2704
+ }
2705
+ if (typeof current.content === "string") return { ...restored, content: restoredContent };
2706
+ throw new Error("Unsupported embed content shape for version restore.");
2707
+ }
2708
+ function parseMailVersionContent(versionContent) {
2709
+ const lines = versionContent.split("\n");
2710
+ let receiver = "";
2711
+ let subject = "";
2712
+ const bodyLines = [];
2713
+ for (const line of lines) {
2714
+ if (line.toLowerCase().startsWith("to:")) {
2715
+ receiver = line.slice(3).trim();
2716
+ } else if (line.toLowerCase().startsWith("subject:")) {
2717
+ subject = line.slice(8).trim();
2718
+ } else {
2719
+ bodyLines.push(line);
2720
+ }
2721
+ }
2722
+ return {
2723
+ receiver,
2724
+ subject,
2725
+ content: bodyLines.join("\n").trim(),
2726
+ footer: ""
2727
+ };
2728
+ }
2584
2729
  var OpenMatesClient = class _OpenMatesClient {
2585
2730
  apiUrl;
2586
2731
  session;
@@ -2600,6 +2745,30 @@ var OpenMatesClient = class _OpenMatesClient {
2600
2745
  hasSession() {
2601
2746
  return this.session !== null;
2602
2747
  }
2748
+ async createTurnTokenRefs(params) {
2749
+ if (params.refs.length === 0) return [];
2750
+ const response = await this.http.post(
2751
+ "/v1/token-broker/turn-token-refs",
2752
+ buildTurnTokenRefsRequestPayload(params),
2753
+ this.getCliRequestHeaders()
2754
+ );
2755
+ if (!response.ok || !Array.isArray(response.data.refs)) {
2756
+ throw new Error(`Failed to create connected-account token refs (HTTP ${response.status})`);
2757
+ }
2758
+ return response.data.refs.map((ref) => {
2759
+ const input = params.refs.find(
2760
+ (item) => item.connected_account_id === ref.connected_account_id && item.app_id === ref.app_id
2761
+ );
2762
+ return {
2763
+ connected_account_id: ref.connected_account_id,
2764
+ app_id: ref.app_id,
2765
+ turn_token_ref: ref.turn_token_ref,
2766
+ expires_at: ref.expires_at,
2767
+ allowed_actions: input?.allowed_actions ?? [],
2768
+ action_scope: input?.action_scope
2769
+ };
2770
+ });
2771
+ }
2603
2772
  // -------------------------------------------------------------------------
2604
2773
  // Auth
2605
2774
  // -------------------------------------------------------------------------
@@ -3362,6 +3531,15 @@ var OpenMatesClient = class _OpenMatesClient {
3362
3531
  const createdAt = Math.floor(Date.now() / 1e3);
3363
3532
  const isNewChat = !params.chatId;
3364
3533
  ws.send("set_active_chat", { chat_id: chatId });
3534
+ const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
3535
+ params.connectedAccountDirectory
3536
+ );
3537
+ const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
3538
+ chatId,
3539
+ messageId,
3540
+ refs: params.connectedAccountTokenRefInputs
3541
+ }) : [];
3542
+ assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
3365
3543
  const messagePayload = {
3366
3544
  chat_id: chatId,
3367
3545
  is_incognito: Boolean(params.incognito),
@@ -3379,6 +3557,12 @@ var OpenMatesClient = class _OpenMatesClient {
3379
3557
  if (memoryMetadataKeys.length > 0) {
3380
3558
  messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
3381
3559
  }
3560
+ if (connectedAccountDirectory) {
3561
+ messagePayload.connected_account_directory = connectedAccountDirectory;
3562
+ }
3563
+ if (connectedAccountTokenRefs.length > 0) {
3564
+ messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
3565
+ }
3382
3566
  let chatKeyBytes = null;
3383
3567
  let encryptedChatKey = null;
3384
3568
  let baselineMessagesV = 0;
@@ -3437,7 +3621,16 @@ var OpenMatesClient = class _OpenMatesClient {
3437
3621
  if (encryptedEmbeds.length > 0) {
3438
3622
  messagePayload.encrypted_embeds = encryptedEmbeds;
3439
3623
  }
3440
- ws.send("chat_message_added", messagePayload);
3624
+ const confirmed = ws.waitForMessage(
3625
+ "chat_message_confirmed",
3626
+ (payload) => {
3627
+ const eventPayload = payload;
3628
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
3629
+ },
3630
+ 2e4
3631
+ );
3632
+ await ws.sendAsync("chat_message_added", messagePayload);
3633
+ await confirmed;
3441
3634
  if (!params.incognito && chatKeyBytes) {
3442
3635
  const encryptedContent = await encryptWithAesGcmCombined(
3443
3636
  params.message,
@@ -3801,6 +3994,22 @@ var OpenMatesClient = class _OpenMatesClient {
3801
3994
  created_at: createdAt,
3802
3995
  updated_at: updatedAt
3803
3996
  });
3997
+ if (Array.isArray(embed.version_history_rows)) {
3998
+ for (const row of embed.version_history_rows) {
3999
+ if (!row.embed_id || typeof row.version_number !== "number") continue;
4000
+ const encryptedSnapshot = typeof row.snapshot === "string" ? await encryptWithAesGcmCombined(row.snapshot, embedKey) : void 0;
4001
+ const encryptedPatch = typeof row.patch === "string" ? await encryptWithAesGcmCombined(row.patch, embedKey) : void 0;
4002
+ if (!encryptedSnapshot && !encryptedPatch) continue;
4003
+ await params.ws.sendAsync("store_embed_diff", {
4004
+ embed_id: row.embed_id,
4005
+ version_number: row.version_number,
4006
+ encrypted_snapshot: encryptedSnapshot ?? null,
4007
+ encrypted_patch: encryptedPatch ?? null,
4008
+ hashed_user_id: hashedUserId,
4009
+ created_at: normalizeUnixSeconds(row.created_at, now)
4010
+ });
4011
+ }
4012
+ }
3804
4013
  if (!isChild) {
3805
4014
  const keys = [
3806
4015
  {
@@ -4031,7 +4240,8 @@ var OpenMatesClient = class _OpenMatesClient {
4031
4240
  if (!topSchema) return [];
4032
4241
  const requestsProp = topSchema.properties?.requests;
4033
4242
  const itemsRef = requestsProp?.items;
4034
- const itemSchema = itemsRef ? resolveSchema(itemsRef) : null;
4243
+ const itemSchema = itemsRef ? resolveSchema(itemsRef) : topSchema;
4244
+ const inputShape = itemsRef ? "requests" : "flat";
4035
4245
  if (!itemSchema?.properties) return [];
4036
4246
  const required = new Set(
4037
4247
  Array.isArray(itemSchema.required) ? itemSchema.required : []
@@ -4064,7 +4274,8 @@ var OpenMatesClient = class _OpenMatesClient {
4064
4274
  type: typeStr,
4065
4275
  description: resolved.description ?? "",
4066
4276
  required: required.has(name),
4067
- default: resolved.default
4277
+ default: resolved.default,
4278
+ inputShape
4068
4279
  };
4069
4280
  });
4070
4281
  }
@@ -4944,6 +5155,188 @@ Required: ${schema.required.join(", ")}`
4944
5155
  const origin = deriveWebOrigin(session.apiUrl);
4945
5156
  return buildEmbedShareUrl(origin, embedId, blob);
4946
5157
  }
5158
+ async listEmbedVersions(embedIdOrShort) {
5159
+ const embedId = await this.resolveEmbedId(embedIdOrShort);
5160
+ const response = await this.http.get(
5161
+ `/v1/embeds/${encodeURIComponent(embedId)}/versions`,
5162
+ this.getCliRequestHeaders()
5163
+ );
5164
+ if (!response.ok || !response.data) {
5165
+ throw new Error(this.formatEmbedVersionError(response.data, `Failed to list embed versions (HTTP ${response.status})`));
5166
+ }
5167
+ return response.data;
5168
+ }
5169
+ async getEmbedVersion(embedIdOrShort, version) {
5170
+ const embedId = await this.resolveEmbedId(embedIdOrShort);
5171
+ const response = await this.http.get(
5172
+ `/v1/embeds/${encodeURIComponent(embedId)}/versions/${version}`,
5173
+ this.getCliRequestHeaders()
5174
+ );
5175
+ if (!response.ok || !response.data) {
5176
+ throw new Error(this.formatEmbedVersionError(response.data, `Failed to load embed version ${version} (HTTP ${response.status})`));
5177
+ }
5178
+ if (typeof response.data.content === "string" || !Array.isArray(response.data.rows)) {
5179
+ return response.data;
5180
+ }
5181
+ return {
5182
+ ...response.data,
5183
+ content: await this.reconstructEncryptedEmbedVersion(embedId, response.data.rows)
5184
+ };
5185
+ }
5186
+ async restoreEmbedVersion(embedIdOrShort, version) {
5187
+ const embedId = await this.resolveEmbedId(embedIdOrShort);
5188
+ const cache = await this.ensureSynced();
5189
+ const masterKey = this.getMasterKeyBytes();
5190
+ const embed = cache.embeds.find(
5191
+ (entry) => String(entry.embed_id ?? entry.id ?? "") === embedId
5192
+ );
5193
+ if (!embed) {
5194
+ throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
5195
+ }
5196
+ const { createHash: createHash5 } = await import("crypto");
5197
+ const hashedEmbedId = createHash5("sha256").update(embedId).digest("hex");
5198
+ const embedKey = await this.resolveEmbedKey(
5199
+ cache,
5200
+ masterKey,
5201
+ embed,
5202
+ embedId,
5203
+ hashedEmbedId
5204
+ );
5205
+ if (!embedKey) {
5206
+ throw new Error("Could not resolve embed encryption key for version restore.");
5207
+ }
5208
+ const target = await this.getEmbedVersion(embedId, version);
5209
+ if (typeof target.content !== "string") {
5210
+ throw new Error("Embed version content was not available for restore.");
5211
+ }
5212
+ const encryptedCurrentContent = embed.encrypted_content;
5213
+ if (typeof encryptedCurrentContent !== "string") {
5214
+ throw new Error("Current embed content is not available for encrypted restore.");
5215
+ }
5216
+ const currentToon = await decryptWithAesGcmCombined(encryptedCurrentContent, embedKey);
5217
+ if (!currentToon) {
5218
+ throw new Error("Could not decrypt current embed content for restore.");
5219
+ }
5220
+ const currentObject = parseEmbedContentObject(currentToon);
5221
+ const currentVersion = typeof embed.version_number === "number" ? embed.version_number : target.current_version;
5222
+ if (version === currentVersion) {
5223
+ throw new Error("Selected version is already current.");
5224
+ }
5225
+ const currentContent = extractVersionedEmbedContent(currentObject);
5226
+ const newVersion = currentVersion + 1;
5227
+ const restoredObject = buildRestoredEmbedContentObject(currentObject, target.content, newVersion);
5228
+ const restoredToon = await encodeEmbedContentObject(restoredObject);
5229
+ const encryptedRestoredContent = await encryptWithAesGcmCombined(restoredToon, embedKey);
5230
+ const restorePatch = buildUnifiedDiffForEmbedRestore(
5231
+ currentContent,
5232
+ target.content,
5233
+ currentVersion,
5234
+ newVersion
5235
+ );
5236
+ const encryptedPatch = await encryptWithAesGcmCombined(restorePatch, embedKey);
5237
+ const contentHash = computeSHA256(target.content);
5238
+ const now = Math.floor(Date.now() / 1e3);
5239
+ const { ws } = await this.openWsClient();
5240
+ try {
5241
+ await ws.sendAsync("store_embed", {
5242
+ embed_id: embedId,
5243
+ encrypted_type: embed.encrypted_type,
5244
+ encrypted_content: encryptedRestoredContent,
5245
+ encrypted_text_preview: embed.encrypted_text_preview,
5246
+ status: embed.status || "finished",
5247
+ hashed_chat_id: embed.hashed_chat_id,
5248
+ hashed_message_id: embed.hashed_message_id,
5249
+ hashed_task_id: embed.hashed_task_id,
5250
+ hashed_user_id: embed.hashed_user_id,
5251
+ embed_ids: embed.embed_ids,
5252
+ parent_embed_id: embed.parent_embed_id,
5253
+ version_number: newVersion,
5254
+ file_path: embed.file_path,
5255
+ content_hash: contentHash,
5256
+ text_length_chars: target.content.length,
5257
+ is_private: embed.is_private ?? false,
5258
+ is_shared: embed.is_shared ?? false,
5259
+ created_at: normalizeUnixSeconds(embed.created_at, now),
5260
+ updated_at: now
5261
+ });
5262
+ await ws.sendAsync("store_embed_diff", {
5263
+ embed_id: embedId,
5264
+ version_number: newVersion,
5265
+ encrypted_snapshot: null,
5266
+ encrypted_patch: encryptedPatch,
5267
+ hashed_user_id: embed.hashed_user_id,
5268
+ created_at: now
5269
+ });
5270
+ } finally {
5271
+ ws.close();
5272
+ }
5273
+ clearSyncCache();
5274
+ return {
5275
+ embed_id: embedId,
5276
+ restored_from_version: version,
5277
+ version_number: newVersion,
5278
+ content: target.content,
5279
+ content_hash: contentHash
5280
+ };
5281
+ }
5282
+ async reconstructEncryptedEmbedVersion(embedId, rows) {
5283
+ const cache = await this.ensureSynced();
5284
+ const masterKey = this.getMasterKeyBytes();
5285
+ const embed = cache.embeds.find(
5286
+ (entry) => String(entry.embed_id ?? entry.id ?? "") === embedId
5287
+ );
5288
+ if (!embed) {
5289
+ throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
5290
+ }
5291
+ const { createHash: createHash5 } = await import("crypto");
5292
+ const hashedEmbedId = createHash5("sha256").update(embedId).digest("hex");
5293
+ const embedKey = await this.resolveEmbedKey(
5294
+ cache,
5295
+ masterKey,
5296
+ embed,
5297
+ embedId,
5298
+ hashedEmbedId
5299
+ );
5300
+ if (!embedKey) {
5301
+ throw new Error("Could not resolve embed encryption key for version history.");
5302
+ }
5303
+ const sortedRows = [...rows].sort((a, b) => a.version_number - b.version_number);
5304
+ let content = null;
5305
+ for (const row of sortedRows) {
5306
+ if (row.encrypted_snapshot) {
5307
+ content = await decryptWithAesGcmCombined(row.encrypted_snapshot, embedKey);
5308
+ continue;
5309
+ }
5310
+ if (row.encrypted_patch && content !== null) {
5311
+ const patch = await decryptWithAesGcmCombined(row.encrypted_patch, embedKey);
5312
+ content = applyUnifiedDiffForEmbedVersion(content, patch ?? "");
5313
+ }
5314
+ }
5315
+ if (content === null) {
5316
+ throw new Error("Version history is missing the initial snapshot");
5317
+ }
5318
+ return content;
5319
+ }
5320
+ formatEmbedVersionError(data, fallback) {
5321
+ if (data && typeof data === "object") {
5322
+ const detail = data.detail;
5323
+ const message = data.message;
5324
+ if (typeof detail === "string" && detail.trim()) return detail;
5325
+ if (typeof message === "string" && message.trim()) return message;
5326
+ }
5327
+ return fallback;
5328
+ }
5329
+ async resolveEmbedId(embedIdOrShort) {
5330
+ if (embedIdOrShort.length >= 32) return embedIdOrShort;
5331
+ const cache = await this.ensureSynced();
5332
+ const embed = cache.embeds.find(
5333
+ (entry) => String(entry.embed_id ?? "").startsWith(embedIdOrShort) || String(entry.id ?? "").startsWith(embedIdOrShort)
5334
+ );
5335
+ if (!embed) {
5336
+ throw new Error(`Embed '${embedIdOrShort}' not found in local cache. Run 'openmates chats list' to sync first.`);
5337
+ }
5338
+ return String(embed.embed_id ?? embed.id ?? "");
5339
+ }
4947
5340
  // ── Mention context builder ─────────────────────────────────────────
4948
5341
  /**
4949
5342
  * Build the context needed for CLI mention resolution.
@@ -5425,7 +5818,7 @@ function printLogo() {
5425
5818
 
5426
5819
  // src/cli.ts
5427
5820
  import { createInterface as createInterface3 } from "readline/promises";
5428
- import { realpathSync } from "fs";
5821
+ import { realpathSync, writeFileSync as writeFileSync4 } from "fs";
5429
5822
  import { fileURLToPath } from "url";
5430
5823
  import { basename as basename3, dirname } from "path";
5431
5824
  import WebSocket2 from "ws";
@@ -22359,6 +22752,111 @@ var searchParentPreviewStressTestChat = {
22359
22752
  }
22360
22753
  };
22361
22754
 
22755
+ // ../ui/src/demo_chats/data/example_chats/rostock-heavy-rain-radar.ts
22756
+ var rostockHeavyRainRadarChat = {
22757
+ chat_id: "example-rostock-heavy-rain-radar",
22758
+ slug: "rostock-heavy-rain-radar",
22759
+ title: "example_chats.rostock_heavy_rain_radar.title",
22760
+ summary: "example_chats.rostock_heavy_rain_radar.summary",
22761
+ icon: "weather",
22762
+ category: "general_knowledge",
22763
+ keywords: ["Rostock rain radar", "DWD weather", "heavy rain", "weather app", "rain map"],
22764
+ follow_up_suggestions: [],
22765
+ messages: [
22766
+ {
22767
+ "id": "cd0dbe83-29cf-476e-b8e7-856ac0c529be",
22768
+ "role": "user",
22769
+ "content": "example_chats.rostock_heavy_rain_radar.message_1",
22770
+ "created_at": 1781465378
22771
+ },
22772
+ {
22773
+ "id": "6c84c422-a71e-47b8-979d-6e2e9e91aad9",
22774
+ "role": "assistant",
22775
+ "content": "example_chats.rostock_heavy_rain_radar.message_2",
22776
+ "created_at": 1781465401,
22777
+ "category": "general_knowledge",
22778
+ "model_name": "Gemini 3 Flash"
22779
+ }
22780
+ ],
22781
+ embeds: [
22782
+ {
22783
+ "embed_id": "deb8e4c2-e07c-49c3-ae5c-300a21859563",
22784
+ "type": "app_skill_use",
22785
+ "content": "app_id: weather\nskill_id: rain_radar\nstatus: finished\nembed_id: deb8e4c2-e07c-49c3-ae5c-300a21859563\nquery: Rostock rain radar\nprovider: Deutscher Wetterdienst (DWD) via Bright Sky\nlocation:\n name: Rostock\n country: Germany\n country_code: DE\n admin1: Mecklenburg-Vorpommern\n latitude: 54.0887\n longitude: 12.14049\n timezone: Europe/Berlin\ncoverage:\n status: available\n radius_km: 5\nsummary:\n rain_expected: true\n in_10_min: Heavy rain visible near Rostock.\n next_2_hours: Heavy rain appears in the radar timeline near Rostock.\n peak_intensity: heavy\n preview_frame_id: frame-15\ntimeline[25]{frame_id,timestamp,kind,label,rain_at_location_mm_5min,max_intensity,rain_area_pct}:\n frame-0,2026-06-14T20:25:00+02:00,past,-65 min,0,heavy,12.4\n frame-1,2026-06-14T20:30:00+02:00,past,-60 min,0,heavy,38.8\n frame-2,2026-06-14T20:35:00+02:00,past,-55 min,20.48,heavy,82.6\n frame-3,2026-06-14T20:40:00+02:00,past,-50 min,33.28,heavy,100\n frame-4,2026-06-14T20:45:00+02:00,past,-45 min,115.2,heavy,100\n frame-5,2026-06-14T20:50:00+02:00,past,-40 min,89.6,heavy,100\n frame-6,2026-06-14T20:55:00+02:00,past,-35 min,76.8,heavy,100\n frame-7,2026-06-14T21:00:00+02:00,past,-30 min,102.4,heavy,100\n frame-8,2026-06-14T21:05:00+02:00,past,-25 min,94.72,heavy,100\n frame-9,2026-06-14T21:10:00+02:00,past,-20 min,66.56,heavy,100\n frame-10,2026-06-14T21:15:00+02:00,past,-15 min,64,heavy,100\n frame-11,2026-06-14T21:20:00+02:00,past,-10 min,120.32,heavy,100\n frame-12,2026-06-14T21:25:00+02:00,past,-5 min,130.56,heavy,100\n frame-13,2026-06-14T21:30:00+02:00,forecast,now,58.88,heavy,100\n frame-14,2026-06-14T21:35:00+02:00,forecast,+5 min,87.04,heavy,100\n frame-15,2026-06-14T21:40:00+02:00,forecast,+10 min,87.04,heavy,100\n frame-16,2026-06-14T21:45:00+02:00,forecast,+15 min,61.44,heavy,100\n frame-17,2026-06-14T21:50:00+02:00,forecast,+20 min,92.16,heavy,100\n frame-18,2026-06-14T21:55:00+02:00,forecast,+25 min,94.72,heavy,100\n frame-19,2026-06-14T22:00:00+02:00,forecast,+30 min,99.84,heavy,100\n frame-20,2026-06-14T22:05:00+02:00,forecast,+35 min,140.8,heavy,100\n frame-21,2026-06-14T22:10:00+02:00,forecast,+40 min,107.52,heavy,100\n frame-22,2026-06-14T22:15:00+02:00,forecast,+45 min,145.92,heavy,100\n frame-23,2026-06-14T22:20:00+02:00,forecast,+50 min,120.32,heavy,100\n frame-24,2026-06-14T22:25:00+02:00,forecast,+55 min,151.04,heavy,100",
22786
+ "parent_embed_id": null,
22787
+ "embed_ids": null
22788
+ }
22789
+ ],
22790
+ metadata: {
22791
+ featured: true,
22792
+ order: 101,
22793
+ app_skill_examples: ["weather.rain_radar"]
22794
+ }
22795
+ };
22796
+
22797
+ // ../ui/src/demo_chats/data/example_chats/classic-car-reverse-image-search.ts
22798
+ var classicCarReverseImageSearchChat = {
22799
+ chat_id: "example-classic-car-reverse-image",
22800
+ slug: "classic-car-reverse-image-search",
22801
+ title: "example_chats.classic_car_reverse_image_search.title",
22802
+ summary: "example_chats.classic_car_reverse_image_search.summary",
22803
+ icon: "search",
22804
+ category: "general_knowledge",
22805
+ keywords: ["reverse image search", "classic cars", "Mercedes-Benz 300 SL", "car identification", "Google Lens"],
22806
+ follow_up_suggestions: [],
22807
+ messages: [
22808
+ {
22809
+ "id": "0cbd9872-31f9-483a-9159-3361764e7982",
22810
+ "role": "user",
22811
+ "content": "example_chats.classic_car_reverse_image_search.message_1",
22812
+ "created_at": 1781525084
22813
+ },
22814
+ {
22815
+ "id": "c2dd4e8f-6026-4540-9355-13d02d94018c",
22816
+ "role": "assistant",
22817
+ "content": "example_chats.classic_car_reverse_image_search.message_2",
22818
+ "created_at": 1781525128,
22819
+ "category": "general_knowledge",
22820
+ "model_name": "Gemini 3 Flash"
22821
+ }
22822
+ ],
22823
+ embeds: [
22824
+ {
22825
+ "embed_id": "d7f2f391-7c8f-44e7-beb4-66bd846dd0ae",
22826
+ "type": "app_skill_use",
22827
+ "content": "app_id: images\nskill_id: search\nresult_count: 0\nembed_ids[0]:\nstatus: finished\nembed_id: d7f2f391-7c8f-44e7-beb4-66bd846dd0ae\nquery: Mercedes-Benz 300 SL Gullwing\nprovider: Brave Search",
22828
+ "parent_embed_id": null,
22829
+ "embed_ids": []
22830
+ },
22831
+ {
22832
+ "embed_id": "45e9c346-341e-466e-aed1-67ee1412dce5",
22833
+ "type": "app_skill_use",
22834
+ "content": "app_id: web\nskill_id: search\nresult_count: 0\nembed_ids[0]:\nstatus: finished\nembed_id: 45e9c346-341e-466e-aed1-67ee1412dce5\nquery: Mercedes-Benz 300 SL Gullwing visual identification features\nprovider: Brave",
22835
+ "parent_embed_id": null,
22836
+ "embed_ids": []
22837
+ },
22838
+ {
22839
+ "embed_id": "228d2a03-63cb-483a-b495-45489e55bc10",
22840
+ "type": "app_skill_use",
22841
+ "content": "app_id: images\nskill_id: search\nresult_count: 0\nembed_ids[0]:\nstatus: finished\nembed_id: 228d2a03-63cb-483a-b495-45489e55bc10\nfile_path: mercedes-300-sl-gullwing-example-jpg-b41-baf4d7\nprovider: Brave Search",
22842
+ "parent_embed_id": null,
22843
+ "embed_ids": []
22844
+ },
22845
+ {
22846
+ "embed_id": "c512601d-508c-44ee-bdb9-b444c18ffe10",
22847
+ "type": "image",
22848
+ "content": "type: image\napp_id: images\nskill_id: upload\nstatus: finished\nfilename: mercedes-300-sl-gullwing-example.jpg\nsrc: https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Mercedes-Benz_300_SL_Gullwing.jpg/960px-Mercedes-Benz_300_SL_Gullwing.jpg\nembed_ref: mercedes-300-sl-gullwing-example-jpg-b41-baf4d7\ncontent_hash: 6620266404d276d92cf456ad5affb62ce35bf3fb50deca205fd0435d5145b017\nai_detection:\n ai_generated: 0.001\n provider: sightengine",
22849
+ "parent_embed_id": null,
22850
+ "embed_ids": null
22851
+ }
22852
+ ],
22853
+ metadata: {
22854
+ featured: true,
22855
+ order: 102,
22856
+ app_skill_examples: ["images.search", "web.search"]
22857
+ }
22858
+ };
22859
+
22362
22860
  // ../ui/src/demo_chats/exampleChatData.ts
22363
22861
  var ALL_EXAMPLE_CHATS = [
22364
22862
  giganticAirplanesChat,
@@ -22442,7 +22940,9 @@ var ALL_EXAMPLE_CHATS = [
22442
22940
  productTeaserRemotionVideoChat,
22443
22941
  dampedSineWavePlotChat,
22444
22942
  berlinCentralStationMapLocationChat,
22445
- launchReadinessChecklistDocChat
22943
+ launchReadinessChecklistDocChat,
22944
+ rostockHeavyRainRadarChat,
22945
+ classicCarReverseImageSearchChat
22446
22946
  ].sort((a, b) => a.metadata.order - b.metadata.order);
22447
22947
 
22448
22948
  // ../ui/src/i18n/locales/en.json
@@ -24966,6 +25466,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
24966
25466
  text: "Get daily and hourly weather forecasts."
24967
25467
  }
24968
25468
  },
25469
+ rain_radar: {
25470
+ text: "Rain radar",
25471
+ description: {
25472
+ text: "Show nearby rain radar with an interactive timeline."
25473
+ }
25474
+ },
24969
25475
  day: {
24970
25476
  text: "Weather day"
24971
25477
  }
@@ -27577,6 +28083,34 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
27577
28083
  }
27578
28084
  },
27579
28085
  embeds: {
28086
+ weather: {
28087
+ rain_radar: {
28088
+ no_rain: {
28089
+ text: "No rain visible near this location."
28090
+ },
28091
+ unavailable: {
28092
+ text: "Rain radar is unavailable for this location."
28093
+ },
28094
+ peak: {
28095
+ text: "Peak"
28096
+ },
28097
+ at_location: {
28098
+ text: "At location"
28099
+ },
28100
+ play: {
28101
+ text: "Play"
28102
+ },
28103
+ pause: {
28104
+ text: "Pause"
28105
+ },
28106
+ frames: {
28107
+ text: "frames"
28108
+ },
28109
+ load_failed: {
28110
+ text: "Radar data could not be loaded. Showing the summary view instead."
28111
+ }
28112
+ }
28113
+ },
27580
28114
  search: {
27581
28115
  text: "Search"
27582
28116
  },
@@ -29031,6 +29565,20 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29031
29565
  text: '```json\n{"type":"app_skill_use","embed_id":"d7f21a46-8f13-4f14-82c4-d868ba62bb8a","app_id":"nutrition","skill_id":"search_recipes","query":"chickpea and spinach dinner","provider":"Edamam","status":"finished"}\n```\n\nHere are three delicious, protein-packed vegetarian chickpea and spinach dinner recipes that you can whip up in under 35 minutes.\n\n### 1. 10-Minute Chickpea and Spinach Curry\nThis is an incredibly fast [curry](wiki:Curry) that relies on aromatics and pantry staples for a rich, satisfying flavor in record time.\n\n[!](embed:thefieryvegetarian.com-3sr)\n[!](embed:vegancocotte.com-IZt)\n[!](embed:vegancocotte.com-rFn)\n\n* **Total Time:** 10 minutes\n* **Protein Content:** ~10.5g per serving\n* **Servings:** 4\n\n**Ingredients:**\n* 1 (15 oz) can [chickpeas](wiki:Chickpea) (drained and rinsed)\n* 1 (15 oz) can diced [tomatoes](wiki:Tomato)\n* 1 [onion](wiki:Onion), 4 [garlic](wiki:Garlic) cloves, 1 red chili, 1 tbsp ginger (all finely diced)\n* 3 tbsp curry paste & 2 tbsp [tomato paste](wiki:Tomato_paste)\n* 3 cups fresh [spinach](wiki:Spinach)\n* 1 tsp vegetable oil\n\n**Instructions:**\n1. **Saut\xE9 Aromatics:** Heat oil in a large pot. Add diced onion, garlic, ginger, and chili. Saut\xE9 for about 1 minute until fragrant.\n2. **Build Flavor:** Stir in the tomato paste and curry paste, saut\xE9ing for another minute to release the oils.\n3. **Simmer:** Pour in the canned tomatoes and chickpeas. Cover and simmer on medium-high heat for 3\u20135 minutes.\n4. **Wilt Spinach:** Stir in the fresh spinach leaves until they wilt (about 1\u20132 minutes). Serve immediately.\n\n---\n\n### 2. 15-Minute Skillet Chickpeas with Spinach and Tomatoes\nThis Mediterranean-style dish uses spices like [turmeric](wiki:Turmeric) and a finishing touch of lemon juice for a bright, healthy meal.\n\n[!](embed:runningonrealfood.com-R8H)\n[!](embed:americanhomecook.com-Alt)\n[!](embed:shortgirltallorder.com-QXv)\n\n* **Total Time:** 15 minutes\n* **Protein Content:** ~23.1g per serving\n* **Servings:** 2\n\n**Ingredients:**\n* 1 (15 oz) can chickpeas (drained and rinsed)\n* 1 (14.5 oz) can diced tomatoes\n* 5 oz baby spinach\n* 1 small onion (diced) & 4 garlic cloves (sliced)\n* \xBD tsp ground turmeric & \xBD tsp [red pepper flakes](wiki:Crushed_red_pepper)\n* 3 tbsp [olive oil](wiki:Olive_oil), lemon juice, and fresh herbs ([cilantro](wiki:Coriander) or [parsley](wiki:Parsley))\n\n**Instructions:**\n1. **Fry Aromatics:** Heat oil in a skillet over low heat. Cook onion and garlic until translucent and slightly browned (3\u20135 minutes).\n2. **Toast Spices:** Add turmeric and red pepper flakes. Toast for 1 minute, then add chickpeas with a drizzle of oil and salt/pepper. Sizzle for 5\u20137 minutes.\n3. **Combine:** Stir in the tomatoes and simmer until the liquid reduces slightly.\n4. **Finish:** Add spinach and stir until wilted. Off the heat, stir in lemon juice and top with fresh herbs and a dollop of [yogurt](wiki:Yogurt).\n\n---\n\n### 3. 20-Minute Middle Eastern Spinach and Chickpeas\nA simpler, cleaner version of the classic [tapas](wiki:Tapas) dish, focusing on the natural sweetness of saut\xE9ed onions and white beans.\n\n[!](embed:carolinescooking.com-Dy7)\n[!](embed:simpleveganblog.com-8dt)\n[!](embed:ourplantbasedworld.com-Krr)\n\n* **Total Time:** 20 minutes\n* **Protein Content:** ~8.7g per serving\n* **Servings:** 4\n\n**Ingredients:**\n* 3 cups low-sodium canned chickpeas (drained and rinsed)\n* 2 cups baby spinach\n* 2/3 cup diced tomatoes\n* \xBD large white onion (diced)\n* 2 tsp minced garlic\n* 1\xBD tbsp [extra-virgin olive oil](wiki:Olive_oil#Grades)\n\n**Instructions:**\n1. **Soften Onions:** Heat olive oil in a pan. Saut\xE9 the onion for 2\u20133 minutes.\n2. **Add Seasoning:** Stir in the garlic and saut\xE9 for another minute.\n3. **Cook Down:** Pour in the tomatoes and cook for 1 minute.\n4. **Simmer Beans:** Add the chickpeas and about 3\u20134 tablespoons of water (or [vegetable broth](wiki:Broth)). Cook for 10 minutes.\n5. **Fold Spinach:** Gently fold in the baby spinach until just wilted. Season to taste and serve.\n\n---\n\n**Next steps for your meal prep:**\n* Would you like to find some side dish suggestions, like a [basmati rice](wiki:Basmati) or [naan](wiki:Naan) recipe, to go with these?\n* Are you interested in seeing some high-protein vegan dessert ideas to round out your dinner?'
29032
29566
  }
29033
29567
  },
29568
+ classic_car_reverse_image_search: {
29569
+ title: {
29570
+ text: "Identify a Classic Car from a Photo"
29571
+ },
29572
+ summary: {
29573
+ text: "Use reverse image search to identify a Mercedes-Benz 300 SL Gullwing from an uploaded photo."
29574
+ },
29575
+ message_1: {
29576
+ text: "Use reverse image search on this uploaded classic car photo and identify the exact car model. Reply with the model name and one sentence explaining the visual clues.\n[!](embed:mercedes-300-sl-gullwing-example-jpg-b41-baf4d7)"
29577
+ },
29578
+ message_2: {
29579
+ text: '```json\n{"type": "app_skill_use", "embed_id": "228d2a03-63cb-483a-b495-45489e55bc10", "app_id": "images", "skill_id": "search", "provider": "Brave Search"}\n```\n\n```json\n{"type": "app_skill_use", "embed_id": "45e9c346-341e-466e-aed1-67ee1412dce5", "app_id": "web", "skill_id": "search", "query": "Mercedes-Benz 300 SL Gullwing visual identification features", "provider": "Brave"}\n```\n\n```json\n{"type": "app_skill_use", "embed_id": "d7f2f391-7c8f-44e7-beb4-66bd846dd0ae", "app_id": "images", "skill_id": "search", "query": "Mercedes-Benz 300 SL Gullwing", "provider": "Brave Search"}\n```\n\nThe car in the photo is a **[Mercedes-Benz 300 SL Gullwing](wiki:Mercedes-Benz_300_SL)**.\n\nThis legendary model is most easily identified by its signature upward-opening "gullwing" doors, the prominent horizontal chrome bars flanking the large star in the grille, and the distinctive aerodynamic "eyebrows" positioned over the wheel arches.\n\nBased on the current date of June 15, 2026, this vehicle remains one of the most coveted and valuable collector cars in history.\n\n**Would you like to know more about this car?**\n- I can look up the current [market value and auction history](/#settings/apps/web) for the 300 SL.\n- I can find [technical specifications](/#settings/apps/web) or the history of its development as a racing car.'
29580
+ }
29581
+ },
29034
29582
  creativity_drawing_meetups_berlin: {
29035
29583
  title: {
29036
29584
  text: "Creativity Drawing Meetups Berlin"
@@ -30165,6 +30713,20 @@ Like [Denis Villeneuve](wiki:Denis_Villeneuve)\u2019s work, *Contact* avoids the
30165
30713
  text: '```json\n{"type": "app_skill_use", "embed_id": "3031e3c2-e832-40fa-99e0-126fe7a5077f", "app_id": "news", "skill_id": "search", "query": "right-to-repair laws EU recent news June 2026", "provider": "Brave"}\n```\n\n```json\n{"type": "app_skill_use", "embed_id": "e7366974-55a3-4025-8a0b-6d40f600fa6a", "app_id": "news", "skill_id": "search", "query": "right-to-repair laws US recent news June 2026", "provider": "Brave"}\n```\n\n```json\n{"type": "app_skill_use", "embed_id": "af660c08-2d9e-4684-ad3c-deceafd8ab0c", "app_id": "web", "skill_id": "search", "query": "expert analysis right-to-repair laws 2026 EU US", "provider": "Brave"}\n```\n\n```json\n{"type": "app_skill_use", "embed_id": "e4146998-76de-4683-a010-ceffcdb53005", "app_id": "images", "skill_id": "search", "query": "right to repair electronics repairability label EU", "provider": "Brave Search"}\n```\n\nAs of June 2026, the "Right to Repair" movement has reached a critical implementation phase in both the [European Union](wiki:European_Union) and the [United States](wiki:United_States). While the EU is moving toward a unified continental mandate, the US continues to see a patchwork of state-level victories and intense lobbying battles.\n\n### Recent Developments in the European Union\n\nThe most significant recent event is the EU\'s unveiling of its **"Tech Sovereignty" package** on June 3, 2026. This initiative aims to reduce dependence on US Big Tech by fostering homegrown alternatives and strengthening consumer rights.\n\n* **Implementation Countdown:** The landmark [EU Right to Repair Directive](wiki:Right_to_repair) (Directive (EU) 2024/1799) is set to be fully enforceable across all member states by **July 31, 2026**. \n* **Digital Product Passports:** Starting July 19, 2026, the EU will launch the **Digital Product Passport (DPP)** registry. This will require products (starting with batteries and textiles) to carry a QR code that provides data on durability and repairability.\n* **Legal Warnings:** On June 4, 2026, the European Commission issued formal warnings to **20 member states** that missed the March 2026 deadline to transpose the "Green Transition" directive into national law. This directive is crucial for preventing [greenwashing](wiki:Greenwashing) and "premature [obsolescence](wiki:Obsolescence)."\n\n[!](embed:euronewsweek.co.uk-aq1)\n[!](embed:termopasty.com-0tB)\n[!](embed:ecomondo.com-9xC)\n\n### Recent Developments in the United States\n\nIn the US, the movement remains focused on the state level, as federal legislation remains stalled in [Congress](wiki:United_States_Congress).\n\n* **State-Level Expansion:** As of mid-2026, over **one-quarter of Americans** live in states with enforceable Right to Repair laws. This is expected to jump to **35% by fall 2026** as new laws in **Texas** and **Connecticut** take full effect.\n* **Corporate Lobbying:** Advocacy groups like the [Public Interest Research Group (PIRG)](wiki:Public_Interest_Research_Group) have reported continued lobbying efforts from companies like [Google](wiki:Google) and [Apple](wiki:Apple_Inc.) against "parts pairing" restrictions\u2014a practice where software locks replacement parts to a specific device.\n\n### What Experts are Saying\n\nLegal and technical experts are offering a mix of optimism and caution regarding these new laws:\n\n1. **The "Tech Liberation" Debate:** While EU officials have hailed June 3, 2026, as "Tech Liberation Day," experts from the *Computer & Communications Industry Association* argue that true independence from [Big Tech](wiki:Big_Tech) will take decades, noting that Europe still lacks a rival to [Nvidia](wiki:Nvidia) or [TSMC](wiki:TSMC).\n2. **Loopholes and Parts Pairing:** Experts at *Cybernews* and *Earth911* warn that while the laws mandate the *right* to repair, manufacturers still "hold the cards" through the high cost of original parts and [software locks](wiki:Digital_rights_management) (parts pairing). They argue that without stricter price controls on spare parts, repair may remain more expensive than replacement.\n3. **Warranty Incentives:** Legal analysts from *Fieldfisher* highlight a "hidden gem" in the EU law: if a consumer chooses repair over replacement during the warranty period, the **warranty is extended by 12 months**. This is viewed as a major incentive to shift consumer behavior away from a "throwaway culture."\n\n> [Manufacturers will be obliged to repair in scope products, even if those products were purchased by consumers before 31 July 2026.](embed:fieldfisher.com-e9q)\n\n### Summary Table: 2026 Milestones\n\n```json\n{"type": "sheet", "embed_id": "e4d7b998-1c4c-4680-9ef6-2a0f34ce0249"}\n```\n\n\nHow would you like to proceed? I can help you look into specific state laws in the US or provide more details on the Digital Product Passport requirements.\n\n* [Check US state-by-state status](/#settings/apps/web)\n* [Find a local repair shop](/#settings/apps/maps)\n---\n*\u26A0\uFE0F This is educational information only and not legal advice. Consult a lawyer for legal advice.*'
30166
30714
  }
30167
30715
  },
30716
+ rostock_heavy_rain_radar: {
30717
+ title: {
30718
+ text: "Rostock Heavy Rain Radar"
30719
+ },
30720
+ summary: {
30721
+ text: "A live weather chat that checks DWD rain radar for Rostock and shows heavy rain on the radar timeline."
30722
+ },
30723
+ message_1: {
30724
+ text: "Is it raining in Rostock right now? Show me the rain radar."
30725
+ },
30726
+ message_2: {
30727
+ text: '```json\n{"type": "app_skill_use", "embed_id": "deb8e4c2-e07c-49c3-ae5c-300a21859563", "app_id": "weather", "skill_id": "rain_radar", "query": "Rostock rain radar", "provider": "Deutscher Wetterdienst (DWD) via Bright Sky", "location": "Rostock"}\n```\n\nYes, it is currently raining heavily in [Rostock](wiki:Rostock). According to the latest data from the [Deutscher Wetterdienst](wiki:Deutscher_Wetterdienst), the city is experiencing significant precipitation, which is expected to continue for at least the next two hours.\n\nThe radar indicates a 100% rain area coverage in your vicinity, with intensities reaching peak levels. You might want to stay indoors for a while!\n\nDo you want to see the [detailed hourly forecast](/#settings/apps/weather) for the rest of the night?'
30728
+ }
30729
+ },
30168
30730
  rust_vector_database_repos: {
30169
30731
  title: {
30170
30732
  text: "Rust Vector Database Repos"
@@ -40634,7 +41196,7 @@ The booking_token is shown in the output of:
40634
41196
  } catch {
40635
41197
  }
40636
41198
  if (!hasExplicitInput && inlineTokens.length === 0) {
40637
- const required = schemaParams.filter((p) => p.required);
41199
+ const required = getEffectiveRequiredParams(schemaParams);
40638
41200
  if (required.length > 0) {
40639
41201
  const data = await client.getSkillInfo(app, skill, apiKey);
40640
41202
  await printSkillInfo(client, app, data);
@@ -40642,7 +41204,7 @@ The booking_token is shown in the output of:
40642
41204
  }
40643
41205
  }
40644
41206
  if (!hasExplicitInput && inlineTokens.length > 0 && schemaParams.length > 0) {
40645
- const required = schemaParams.filter((p) => p.required);
41207
+ const required = getEffectiveRequiredParams(schemaParams);
40646
41208
  if (required.length > 1) {
40647
41209
  const example = {};
40648
41210
  for (const p of required) example[p.name] = `<${p.name}>`;
@@ -40650,7 +41212,7 @@ The booking_token is shown in the output of:
40650
41212
  `This skill requires ${required.length} fields: ${required.map((p) => p.name).join(", ")}
40651
41213
 
40652
41214
  Use --input to provide all fields:
40653
- openmates apps ${app} ${skill} --input '{"requests": [${JSON.stringify(example)}]}'
41215
+ openmates apps ${app} ${skill} --input '${buildInputUsageExample(schemaParams, example)}'
40654
41216
 
40655
41217
  Run with --help for full parameter details:
40656
41218
  openmates apps ${app} ${skill} --help
@@ -40699,7 +41261,7 @@ App details: openmates apps info <app-id>`
40699
41261
  `
40700
41262
  Usage:
40701
41263
  openmates apps ${app} ${skill} <value>
40702
- openmates apps ${app} ${skill} --input '{"requests": [{"${schemaParams[0]?.name ?? "query"}": "..."}]}'`
41264
+ openmates apps ${app} ${skill} --input '${buildInputUsageExample(schemaParams)}'`
40703
41265
  );
40704
41266
  } else {
40705
41267
  console.error(
@@ -40844,17 +41406,38 @@ async function pollCodeRunStatus(client, statusPath, apiKey, jsonMode) {
40844
41406
  }
40845
41407
  }
40846
41408
  function buildSkillInput(flags, inlineTokens, schemaParams) {
41409
+ const usesFlatInput = schemaParams?.some((p) => p.inputShape === "flat") ?? false;
40847
41410
  if (typeof flags.input === "string") {
40848
- return JSON.parse(flags.input);
41411
+ const parsed = JSON.parse(flags.input);
41412
+ if (usesFlatInput && Array.isArray(parsed.requests) && parsed.requests.length === 1) {
41413
+ const firstRequest = parsed.requests[0];
41414
+ if (firstRequest && typeof firstRequest === "object") {
41415
+ return firstRequest;
41416
+ }
41417
+ }
41418
+ return parsed;
40849
41419
  }
40850
41420
  const inlineText = inlineTokens.join(" ").trim();
40851
41421
  if (inlineText) {
40852
- const required = (schemaParams ?? []).filter((p) => p.required);
40853
- const paramName = required.length === 1 ? required[0].name : "query";
41422
+ const required = getEffectiveRequiredParams(schemaParams ?? []);
41423
+ const flatLocationParam = usesFlatInput ? (schemaParams ?? []).find((p) => p.name === "location") : void 0;
41424
+ const paramName = required.length === 1 ? required[0].name : flatLocationParam?.name ?? "query";
41425
+ if (usesFlatInput) return { [paramName]: inlineText };
40854
41426
  return { requests: [{ [paramName]: inlineText }] };
40855
41427
  }
40856
41428
  return {};
40857
41429
  }
41430
+ function getEffectiveRequiredParams(schemaParams) {
41431
+ const required = schemaParams.filter((p) => p.required);
41432
+ if (required.length > 0) return required;
41433
+ const flatLocationParam = schemaParams.find((p) => p.inputShape === "flat" && p.name === "location");
41434
+ return flatLocationParam ? [flatLocationParam] : [];
41435
+ }
41436
+ function buildInputUsageExample(schemaParams, exampleItem) {
41437
+ const item = exampleItem ?? { [schemaParams[0]?.name ?? "query"]: "..." };
41438
+ if (schemaParams.some((p) => p.inputShape === "flat")) return JSON.stringify(item);
41439
+ return JSON.stringify({ requests: [item] });
41440
+ }
40858
41441
  async function suggestAppOrSkill(client, app, skill, apiKey) {
40859
41442
  try {
40860
41443
  const data = await client.listApps(apiKey);
@@ -40982,6 +41565,75 @@ async function handleEmbeds(client, subcommand, rest, flags) {
40982
41565
  }
40983
41566
  return;
40984
41567
  }
41568
+ if (subcommand === "versions") {
41569
+ const action = rest[0];
41570
+ const embedId = rest[1];
41571
+ if (!action || !embedId || !["list", "show", "restore"].includes(action)) {
41572
+ console.error("Usage: openmates embeds versions <list|show|restore> <embed-id> [--version <n>]\n");
41573
+ printEmbedsHelp();
41574
+ process.exit(1);
41575
+ }
41576
+ if (action === "list") {
41577
+ const versions = await client.listEmbedVersions(embedId);
41578
+ if (flags.json === true) {
41579
+ printJson2(versions);
41580
+ } else {
41581
+ process.stdout.write(`
41582
+ \x1B[1mEmbed versions\x1B[0m ${versions.embed_id}
41583
+ `);
41584
+ for (const version2 of versions.versions) {
41585
+ const marker = version2.version_number === versions.current_version ? " (current)" : "";
41586
+ const date = new Date(version2.created_at * 1e3).toISOString();
41587
+ process.stdout.write(` v${version2.version_number}${marker} ${date}
41588
+ `);
41589
+ }
41590
+ if (versions.readonly) process.stdout.write("\x1B[2mRead-only shared history\x1B[0m\n");
41591
+ }
41592
+ return;
41593
+ }
41594
+ const version = typeof flags.version === "string" ? parseInt(flags.version, 10) : NaN;
41595
+ if (!Number.isFinite(version) || version <= 0) {
41596
+ console.error("Missing or invalid --version <n>.");
41597
+ process.exit(1);
41598
+ }
41599
+ if (action === "show") {
41600
+ const result = await client.getEmbedVersion(embedId, version);
41601
+ if (typeof result.content !== "string") {
41602
+ throw new Error("Embed version content was not available after local reconstruction.");
41603
+ }
41604
+ if (typeof flags.output === "string") {
41605
+ writeFileSync4(flags.output, result.content, "utf-8");
41606
+ if (flags.json === true) {
41607
+ printJson2({ ...result, output: flags.output });
41608
+ } else {
41609
+ process.stdout.write(`Wrote ${result.embed_id} v${result.version_number} to ${flags.output}
41610
+ `);
41611
+ }
41612
+ } else if (flags.json === true) {
41613
+ printJson2(result);
41614
+ } else {
41615
+ process.stdout.write(`
41616
+ \x1B[1m${result.embed_id} v${result.version_number}\x1B[0m
41617
+ `);
41618
+ process.stdout.write(`${result.content}
41619
+ `);
41620
+ }
41621
+ return;
41622
+ }
41623
+ if (flags.yes !== true) {
41624
+ await confirmOrExit(`Restore embed ${embedId} to version ${version}? This creates a new latest version. [y/N] `);
41625
+ }
41626
+ const restored = await client.restoreEmbedVersion(embedId, version);
41627
+ if (flags.json === true) {
41628
+ printJson2(restored);
41629
+ } else {
41630
+ process.stdout.write(
41631
+ `Restored v${restored.restored_from_version} as new v${restored.version_number} for ${restored.embed_id}.
41632
+ `
41633
+ );
41634
+ }
41635
+ return;
41636
+ }
40985
41637
  console.error(`Unknown embeds subcommand '${subcommand}'.
40986
41638
  `);
40987
41639
  printEmbedsHelp();
@@ -43092,7 +43744,9 @@ ${data.description}
43092
43744
  }
43093
43745
  process.stdout.write(`\x1B[1mExample\x1B[0m
43094
43746
  `);
43095
- const exampleJson = JSON.stringify({ requests: [exampleItem] }, null, 2).split("\n").map((l) => ` ${l}`).join("\n");
43747
+ const usesFlatInput = params.some((p) => p.inputShape === "flat");
43748
+ const examplePayload = usesFlatInput ? exampleItem : { requests: [exampleItem] };
43749
+ const exampleJson = JSON.stringify(examplePayload, null, 2).split("\n").map((l) => ` ${l}`).join("\n");
43096
43750
  process.stdout.write(
43097
43751
  ` \x1B[2mopenmates apps ${appId} ${data.id} --input '\x1B[0m
43098
43752
  `
@@ -44056,13 +44710,20 @@ function printEmbedsHelp() {
44056
44710
  console.log(`Embeds commands:
44057
44711
  openmates embeds show <embed-id> [--json]
44058
44712
  openmates embeds share <embed-id> [--expires <seconds>] [--password <pwd>] [--json]
44713
+ openmates embeds versions list <embed-id> [--json]
44714
+ openmates embeds versions show <embed-id> --version <n> [--output <path>] [--json]
44715
+ openmates embeds versions restore <embed-id> --version <n> [--yes] [--json]
44059
44716
 
44060
44717
  'show' displays the full decrypted content of an embed.
44718
+ The 'versions' commands list, inspect, and non-destructively restore history.
44061
44719
  The embed ID can be the full UUID or just the first 8 characters.
44062
44720
  Embed IDs are shown when viewing chat conversations (openmates chats show).
44063
44721
 
44064
44722
  Examples:
44065
- openmates embeds show a3f2b1c4`);
44723
+ openmates embeds show a3f2b1c4
44724
+ openmates embeds versions list a3f2b1c4
44725
+ openmates embeds versions show a3f2b1c4 --version 1
44726
+ openmates embeds versions restore a3f2b1c4 --version 1 --yes`);
44066
44727
  }
44067
44728
  function printInspirationsHelp() {
44068
44729
  console.log(`Inspirations command:
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-UG4LLEOG.js";
5
+ } from "./chunk-MWM6T3MG.js";
6
6
  import "./chunk-AXNRPVLE.js";
7
7
  export {
8
8
  getExtForLang,
package/dist/index.d.ts CHANGED
@@ -226,6 +226,29 @@ interface SubChatApprovalRequest {
226
226
  existingSubChats: number | null;
227
227
  remainingSubChats: number | null;
228
228
  }
229
+ interface ConnectedAccountDirectoryEntry {
230
+ connected_account_id: string;
231
+ app_id: string;
232
+ account_ref: string;
233
+ label: string;
234
+ capabilities: string[];
235
+ runtime_modes?: Record<string, string>;
236
+ }
237
+ interface ConnectedAccountTurnTokenRefInput {
238
+ connected_account_id: string;
239
+ app_id: string;
240
+ allowed_actions: string[];
241
+ refresh_token_envelope: Record<string, unknown>;
242
+ action_scope?: Record<string, unknown>;
243
+ }
244
+ interface ConnectedAccountTurnTokenRef {
245
+ connected_account_id: string;
246
+ app_id: string;
247
+ turn_token_ref: string;
248
+ allowed_actions: string[];
249
+ action_scope?: Record<string, unknown>;
250
+ expires_at: number;
251
+ }
229
252
  /** A single field definition within a memory type schema. */
230
253
  interface MemoryFieldDef {
231
254
  type: string;
@@ -266,6 +289,7 @@ interface SkillParam {
266
289
  description: string;
267
290
  required: boolean;
268
291
  default?: unknown;
292
+ inputShape?: "requests" | "flat";
269
293
  }
270
294
  interface ChatListPage {
271
295
  chats: ChatListItem[];
@@ -297,6 +321,35 @@ interface DecryptedEmbed {
297
321
  skillId: string | null;
298
322
  createdAt: number | null;
299
323
  }
324
+ interface EmbedVersionMeta {
325
+ version_number: number;
326
+ created_at: number;
327
+ has_snapshot: boolean;
328
+ has_patch: boolean;
329
+ encrypted_snapshot?: string | null;
330
+ encrypted_patch?: string | null;
331
+ }
332
+ interface EmbedVersionsResponse {
333
+ embed_id: string;
334
+ current_version: number;
335
+ versions: EmbedVersionMeta[];
336
+ readonly: boolean;
337
+ }
338
+ interface EmbedVersionContentResponse {
339
+ embed_id: string;
340
+ version_number: number;
341
+ current_version: number;
342
+ content?: string;
343
+ rows?: EmbedVersionMeta[];
344
+ readonly: boolean;
345
+ }
346
+ interface EmbedVersionRestoreResponse {
347
+ embed_id: string;
348
+ restored_from_version: number;
349
+ version_number: number;
350
+ content: string;
351
+ content_hash: string;
352
+ }
300
353
  /** Video metadata attached to a daily inspiration. */
301
354
  interface DailyInspirationVideo {
302
355
  youtube_id: string;
@@ -449,6 +502,11 @@ declare class OpenMatesClient {
449
502
  constructor(options?: OpenMatesClientOptions);
450
503
  static load(options?: OpenMatesClientOptions): OpenMatesClient;
451
504
  hasSession(): boolean;
505
+ createTurnTokenRefs(params: {
506
+ chatId: string;
507
+ messageId: string;
508
+ refs: ConnectedAccountTurnTokenRefInput[];
509
+ }): Promise<ConnectedAccountTurnTokenRef[]>;
452
510
  loginWithPairAuth(): Promise<void>;
453
511
  whoAmI(): Promise<Record<string, unknown>>;
454
512
  logout(): Promise<void>;
@@ -589,6 +647,10 @@ declare class OpenMatesClient {
589
647
  encryptedEmbeds?: EncryptedEmbed[];
590
648
  /** Prepared embeds to encrypt after the real chat/message IDs are known. */
591
649
  preparedEmbeds?: PreparedEmbed[];
650
+ /** Redacted connected-account directory for AI-visible account selection. */
651
+ connectedAccountDirectory?: ConnectedAccountDirectoryEntry[];
652
+ /** Refresh-token envelopes to convert into short-lived token refs before send. */
653
+ connectedAccountTokenRefInputs?: ConnectedAccountTurnTokenRefInput[];
592
654
  }): Promise<{
593
655
  status: "completed" | "waiting_for_user";
594
656
  chatId: string;
@@ -823,6 +885,12 @@ declare class OpenMatesClient {
823
885
  * @returns Full share URL, e.g. https://openmates.org/share/embed/{id}#key={blob}
824
886
  */
825
887
  createEmbedShareLink(embedIdOrShort: string, durationSeconds?: ShareDuration, password?: string): Promise<string>;
888
+ listEmbedVersions(embedIdOrShort: string): Promise<EmbedVersionsResponse>;
889
+ getEmbedVersion(embedIdOrShort: string, version: number): Promise<EmbedVersionContentResponse>;
890
+ restoreEmbedVersion(embedIdOrShort: string, version: number): Promise<EmbedVersionRestoreResponse>;
891
+ private reconstructEncryptedEmbedVersion;
892
+ private formatEmbedVersionError;
893
+ private resolveEmbedId;
826
894
  /**
827
895
  * Build the context needed for CLI mention resolution.
828
896
  * Fetches apps (with skills, focus modes, memory categories) and
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getExtForLang,
8
8
  parseNewChatSuggestionText,
9
9
  serializeToYaml
10
- } from "./chunk-UG4LLEOG.js";
10
+ } from "./chunk-MWM6T3MG.js";
11
11
  import "./chunk-AXNRPVLE.js";
12
12
  export {
13
13
  MATE_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.12.0-alpha.2",
3
+ "version": "0.12.0-alpha.4",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",